Cockfight¶
hobbs_cockfight, current version 1.0.1
Overview¶
Cockfighting for your server. A rooster is an inventory item that a player names once and then owns for good, with its own level, record, trait and plumage. Fights happen in fixed pits that you place in the config, or in a wild pit a player stakes out anywhere in the open. The server runs the fight as the referee, both owners and everyone standing around see a HUD, hear the bell and the crowing, and can bet on the outcome before the bell rings. A bookmaker NPC at each fixed pit keeps the book (leaderboard, your roosters, statistics, rules), offers sparring matches against his own roosters and runs knockout tournaments with an entry fee and a pot.
There is no job to set up. Anyone who holds a rooster can fight, anyone standing at the pit can bet.
How it plays¶
- A player gets a
roosteritem and uses it. A dialog asks for a name, the name stays with this rooster forever. Right after that the rooster shows its trait ("a daredevil who never walks away from a fight") and, if plumage is on, the player picks one of three looks while the rooster struts in front of them. - The player walks to a pit and uses the rooster again. It appears in its corner and waits for a rival. While it waits, the owner can recall it with G, or strap on
rooster_spursby using that item. - A second player drops their rooster in. The betting window opens for 15 seconds, spectators near the pit press L, pick a side and enter a stake. Then a 5 second countdown, the bell, and the fight.
- The roosters circle, peck, dodge and clash. Every hit is decided on the server, the clients only play what they are told. The HUD shows both health bars, the pot and the odds. Spectators at a fixed pit get a small cheering crowd.
- One rooster goes down. The winner gets XP, a level maybe, a longer streak and a title in the book. The loser is injured for 30 minutes by default, or dies if you turn that on. Bets are paid out, a banner shows the winner, everyone cheers.
- No rival around? The owner uses the rooster away from any pit and stakes out a wild pit: a red ring on the ground that they move with the arrow keys and confirm with Enter. It shows as a blip on the map and lasts five minutes without a rival. Or they put the rooster in a fixed pit, talk to the bookmaker and pay $2 for a sparring match against one of his roosters at the same level.
- At the bookmaker anyone can call a tournament. Entrants use their rooster at the bookmaker and pay the entry fee into the pot. After the signup time the bookmaker pairs them round by round, calls each fight by name, and the last rooster standing takes the pot.
Requirements¶
| Resource | Note |
|---|---|
vorp_core |
Characters, cash and gold, notifications |
vorp_inventory |
The rooster and spurs items, their metadata and the usable item hooks |
nss_libs |
Prompts, the bookmaker NPC, blips, database access, client and server events, the Discord webhook |
oxmysql |
Database behind nss_libs |
All four are hard dependencies, the resource does not start without them.
Installation¶
- Unpack the resource into
resourcesand keep the folder namehobbs_cockfight. - Run
sql/install.sqlon your database. It inserts the two itemsroosterandrooster_spursintoitemsand creates the tableshobbs_cockfight_roosters(one row per named rooster) andhobbs_cockfight_owed(money owed to players who were offline at a payout). The tables are also created by the resource on first start, so if the SQL file only fails on theCREATE TABLElines, you are fine. - Put item images named
rooster.pngandrooster_spurs.pnginto your inventory's image folder. -
Add it to
server.cfgafter its dependencies: -
Start the server. The console prints
[hobbs_cockfight] ready, item "rooster" registered.and, with spurs enabled, a second line forrooster_spurs. If one of those lines is missing,vorp_inventorywas not up when the resource started. - Open
config/config.lua, setConfig.Localeand replace the pit inConfig.Spotswith your own (see below). Give players a rooster through a shop or an admin command of your choice.
The config is downloaded by every client. Only server_config.lua stays on the server, that is why the Discord webhook lives there and nowhere else.
Configuration¶
Everything a player can see or touch lives in config/config.lua. Texts live in locales.lua, the Discord settings in server_config.lua.
Language and notifications¶
Config.Locale = 'en' -- "en" or "de"
Config.Debug = false
Config.Notify = {
dict = 'generic_textures',
duration = 5000,
icons = { rooster = 'tick', tourney = 'tick', spurs = 'tick', money = 'tick' },
}
English and German ship with the script. To add a language copy a block in locales.lua, translate it and set Config.Locale to its key. The texts are written in character, the bookmaker talks back to the player, so keep that tone if you translate.
Notifications go through vorp_core (NotifyLeft). The four icon keys are the four notification topics, the titles are notify_rooster, notify_money, notify_spurs and notify_tourney in the locale.
Config.Debug prints extra lines to the console, adds the /hahnfx test command for the blood effect and lets one player put both of their roosters into the same pit for testing. Leave it off on a live server.
The rooster item¶
Config.ItemName = 'rooster'
Config.RoosterModel = 'a_c_rooster_01'
Config.Coat = {
enabled = true,
count = 3, -- outfit presets of the model
keys = {
prev = 0xA65EBAB4, -- arrow left
next = 0xDEB34313, -- arrow right
confirm = 0xC7B5340A, -- enter
cancel = 0x4AF4D473, -- delete, pick later
},
}
Config.NameRules = { min = 3, max = 24 }
The item is never used up. The first use names the rooster and writes a roosterId into the item's metadata, every use after that sends it into a pit. The item label and description are rewritten after every fight so the inventory shows name, title, level, record, streak and state ("INJURED (12 min)" or "FALLEN").
A rooster belongs to the character who uses it. If the item is traded, the next owner who uses it takes it over with everything it has earned, the book shows the last owner's name.
Plumage is one of the model's outfit presets. The owner picks it once, after naming, and it stays. Set enabled = false and the plumage step is skipped, every rooster then gets a random look each time it spawns.
Keys¶
Config.Keys = {
recall = 0x760A9C6F, -- G, recall a waiting rooster
bet = 0x80F28E95, -- L, open the betting bar
book = 0xCEFD9220, -- E, open the book at the bookmaker
spar = 0xE30CD707, -- R, sparring match at the bookmaker
}
Values are RedM control hashes. The keys for placing a wild pit are in Config.Arena.keys, the keys for picking plumage in Config.Coat.keys.
Names above the roosters¶
Shows the rooster's name over its head for everyone within range. Costs a little on the client while roosters stand in a pit, so lower the range or turn it off on a busy server.
Fixed pits¶
Config.DefaultSpotRadius = 1.5
Config.Spots = {
{
id = 'vanhorn_pit',
label = 'Van Horn pit',
center = vector3(2982.08, 584.83, 44.19),
radius = Config.DefaultSpotRadius,
corners = {
vector4(2981.89, 584.03, 44.24, 345.77),
vector4(2982.4, 586.07, 44.12, 165.82),
},
blip = { enabled = false, sprite = 'blip_ambient_hunting', color = 'RED', scale = 0.2 },
npc = {
enabled = true,
model = 'A_M_M_FiveFingerFilletPlayers_01',
coords = vector4(2980.31, 589.92, 44.47, 181.72),
scenario = 'WORLD_HUMAN_SMOKE',
},
},
}
Config.SpotProximityPadding = 2.0
| Key | Meaning |
|---|---|
id |
Unique name of the pit, used internally and in the Discord log |
label |
Shown in the HUD and the book |
center |
Middle of the pit. Using the rooster within radius of it puts the rooster in |
corners |
The two spots where the roosters appear, w is the heading so they face each other |
blip |
Map blip of the pit, off by default |
npc |
The bookmaker: book, sparring and tournaments. Set enabled = false for a pit without one |
The one pit that ships is an example in Van Horn, replace it with your own. A pit without a bookmaker still works for normal fights and bets, but nobody can spar or call a tournament there.
SpotProximityPadding is slack the server adds to every distance check because it sees player positions a little late. Raise it if players at the edge of the pit get "There is no cockfight pit here".
Tip
Put the two corners about two metres apart and the bookmaker a few steps outside the ring, otherwise the crowd (see below) has no room and the bookmaker stands in the fight.
Wild pits¶
Config.Arena = {
enabled = true,
previewOffset = 2.2, -- metres in front of the player at the start
offsetMin = 1.2,
offsetMax = 5.0,
offsetStep = 0.2,
maxSlope = 0.55, -- max height difference in metres along the ring
minDistanceToSpot = 25.0, -- to a fixed pit
minDistanceToArena = 20.0, -- to another wild pit
maxActive = 6, -- wild pits at once, server wide
onePerPlayer = true,
cooldownSec = 60, -- between two placements by the same player
waitTimeoutSec = 300, -- no opponent = the pit expires
claimExtra = 0.0, -- extra buffer while somebody places, negative = no reservation
claimTtlSec = 120,
blip = { enabled = true, sprite = 'blip_ambient_hunting', color = 'RED', scale = 0.15 },
ring = {
drawDistance = 30.0,
height = 0.35,
zOffset = -0.05,
colorIdle = { 196, 30, 30, 60 },
colorFight = { 200, 168, 75, 75 },
},
keys = {
confirm = 0xC7B5340A, -- enter
cancel = 0x4AF4D473, -- delete
further = 0x6319DB71, -- arrow up
nearer = 0x05CA7C52, -- arrow down
},
}
Using the rooster away from every fixed pit opens placement mode: a ring on the ground that the player pushes further away or closer with the arrow keys and confirms with Enter. The server checks the distance to fixed pits and other wild pits, the slope, the global cap and the cooldown before it accepts the spot. The pit lives until the fight is over, the owner leaves, or nobody shows up for waitTimeoutSec.
While a player is placing, the area around them is reserved so two players cannot stake out the same patch at once. claimExtra widens that reservation, a negative value turns it off.
Set enabled = false and fights happen at the fixed pits only. The player then sees "Fights happen at the fixed pits only." and the rules tab in the book drops the wild pit paragraph.
Countdown and fight¶
Config.Countdown = { seconds = 5 } -- after the betting window, until the bell
Config.Fight = {
lightDamageMin = 6,
lightDamageMax = 12,
heavyDamageMin = 16,
heavyDamageMax = 28,
heavyHitChance = 0.28,
safetyMaxMs = 180000, -- longer fights end, the rooster with less HP loses
despawnDelayMs = 10000, -- winner and loser stay this long after the end
clashChance = 0.58, -- share of beats that are a real clash, the rest is choreography
arenaRadius = 3.8, -- size of the fight and of a wild pit
runSpeed = 3.2,
circleSpeed = 1.9,
desperationBelow = 25, -- below this HP the rooster speeds up and crows once, 0 = off
desperationSpeedMult = 1.35,
}
Both roosters start with 100 HP. The server runs the fight in beats, a beat is either a clash (one attacks, the other dodges, damage only if the peck lands) or choreography (circling, closing in, taunting). The block has many more timing values for the chase, the dodge, the knockdown and the recovery. They are tuned to the animations, so change the damage and chance values first and leave the millisecond values alone unless a fight looks wrong to you.
A fight that runs longer than safetyMaxMs ends on time, the rooster with less HP loses. The Discord log and the banner then say "on time" instead of "by knockout".
How much strength matters¶
Config.Outcome = {
powerInfluence = 0.60, -- share of the attacks the stronger one gets, 0 = even, 1 = by strength
upsetClamp = 0.28, -- the attack share stays within [0.28, 0.72]
damageInfluence = 0.25,
damageRatioMin = 0.80,
damageRatioMax = 1.35,
}
Strength comes from level and spurs (see Progression). powerInfluence decides how much of the attack share the stronger rooster gets, upsetClamp keeps a weak rooster in the fight so an upset stays possible. I kept the clamp fairly wide because fights nobody can lose are no fun to bet on. Set powerInfluence = 0 for coin flip fights where level is only a title.
Loser, cooldowns, same owner¶
Config.LoserOutcome = CF.LOSER.INJURED -- INJURED, PERMADEATH or CHANCE
Config.LoserDeathChance = 0.5 -- only with CHANCE
Config.Cooldowns = {
injuredSeconds = 1800, -- loser
restSeconds = 180, -- winner
}
Config.SameOwnerAllowed = false
| Outcome | What happens to the loser |
|---|---|
INJURED |
Cannot fight for injuredSeconds, then it is back |
PERMADEATH |
The rooster is marked fallen. It stays in the inventory with "FALLEN" in its description but never fights again |
CHANCE |
Permadeath with LoserDeathChance, otherwise injured |
The winner rests for restSeconds. A player disconnecting mid fight loses by forfeit, with the same outcome as a normal loss.
SameOwnerAllowed = false stops a player from putting two of their own roosters into one pit to farm wins or bets.
Progression, size and titles¶
Config.Progression = {
enabled = true,
xpPerWin = 3,
xpPerKill = 2, -- extra for a knockout win
xpPerLoss = 1,
xpPerLevel = 20, -- level = 1 + floor(xp / xpPerLevel)
maxLevel = 31,
powerBase = 100,
powerPerLevel = 12,
}
Config.PedScale = {
default = 0.8,
steps = {
{ min = 3, scale = 0.9 },
{ min = 6, scale = 1.0 },
-- ...
{ min = 30, scale = 1.6 },
},
}
Config.Titles = {
{ minWins = 0 }, -- rank_0 in locales.lua: Greenhorn
{ minWins = 5 }, -- Pit fighter
{ minWins = 12 }, -- Daredevil
{ minWins = 25 }, -- Veteran of the pit
{ minWins = 45 }, -- Champion
{ minWins = 75 }, -- Living legend
{ minWins = 99 }, -- Immortal
}
Config.Streak = {
milestoneEvery = 2, -- every X wins in a row an extra notification, 0 = off
bonusXpEvery = 1,
bonusXp = 2,
}
Every fight gives XP, wins give more, a knockout gives extra. Level raises the rooster's power (powerBase + powerPerLevel * (level - 1)) and its size in the pit, so an old hand visibly struts wider. Titles come from total wins, the names are rank_<minWins> in locales.lua, so if you add a step at 150 wins you also add rank_150 to every language.
With default values a rooster needs 20 XP per level, that is about six or seven wins per level early on. Lower xpPerLevel if your players fight rarely and you want them to see progress.
Sparring¶
Config.Sparring = {
enabled = true,
fee = 2, -- per sparring match, 0 = free
currency = 0, -- 0 cash, 1 gold
xp = 1,
maxLevel = 15, -- sparring takes a rooster up to this level only
perDay = 10, -- sparring matches per rooster and day, 0 = no cap
restSeconds = 180,
npcRange = 1.5, -- distance to the bookmaker (plus SpotProximityPadding)
}
A rooster waiting alone in a fixed pit can spar: the owner presses R at the bookmaker, pays the fee and the bookmaker puts one of his own roosters in at the same level. No bets, no win or loss on the record, no injury, only xp and a short rest. Sparring stops at maxLevel, after that only real fights take the rooster further. The bookmaker's name and his rooster names (spar_owner_name, spar_rooster_names) are in locales.lua.
The fee is simply taken from the player, it does not go anywhere.
Tournaments¶
Config.Tournament = {
enabled = true,
sizes = { 4, 6, 8, 10, 12, 14, 16, 18, 20 }, -- choices when calling one
minEntries = 4,
entryFee = 10,
currency = 0, -- 0 cash, 1 gold
houseMargin = 0.10, -- share of the pot the bookmaker keeps
payout = { 0.7, 0.3 }, -- share of the rest for first and second
signupMinutes = 10,
breakSec = 20, -- between the call and the fight
presenceRange = 25.0, -- this close to the pit when called
announceRange = 400.0, -- who hears the announcement, entrants always
cooldownMin = 10, -- no new tournament at this pit for this long
npcRange = 1.5,
}
Anyone standing at a bookmaker can call a tournament from the book's Tournament tab, picking a size from sizes. Calling is free. Entrants use their rooster at the bookmaker during the signup time, pay entryFee into the pot, one rooster per person. With fewer than minEntries the tournament is off and every fee is refunded.
Rounds are knockout. Each pairing is announced with a breakSec countdown, both owners have to be within presenceRange of the pit when called, whoever is missing is out (walkover). A rooster with no rival in a round advances without a fight. Tournament fights run like normal fights, with a betting window for the spectators. At the end the bookmaker keeps houseMargin of the pot, the winner gets 70 percent of the rest and the runner up 30 percent. The pit is reserved for the tournament while it runs.
Spurs¶
The owner uses the spurs item while their rooster waits in the pit. The rooster then attacks more often and hits damageBonus harder for that fight. One pair lasts uses fights, the item's description counts down, then it breaks and is removed. Bettors see a spurs mark in the betting bar, so nobody is tricked. Spurs have to go on before the second rooster enters, "Too late for spurs" otherwise.
Traits¶
Config.Traits = {
enabled = true,
list = {
{ key = 'daredevil', initiative = 0.06 }, -- attacks this much more often
{ key = 'nimble', heavyTaken = 0.5 }, -- factor on the chance to take a heavy hit
{ key = 'tough', damageTaken = 0.88 }, -- factor on the damage taken
{ key = 'heart', initiativeLow = 0.12 }, -- initiative, but only below Fight.desperationBelow HP
{ key = 'beak', damageDealt = 1.10 }, -- factor on the damage dealt
},
}
Every rooster gets one trait at random when it is named and keeps it. Names and texts are trait_<key>_name, trait_<key>_desc and trait_<key>_reveal in locales.lua. Add a trait by adding a line here and the three texts in every language. The trait shows in the book and in the betting bar.
Betting¶
Config.Betting = {
enabled = true,
windowSec = 15, -- betting window once two roosters stand, then the countdown
promptRange = 4.0,
currency = 0, -- 0 cash, 1 gold
min = 5,
max = 100, -- 0 = only the cash counts
houseMargin = 0.08, -- share of the losers' pot the house keeps
quickBets = { 5, 10, 20, 50 },
}
Config.Hud = {
range = 15.0, -- spectators this close see the fight HUD
}
Pool betting. All stakes go into a pot, one bet per character and fight, the stake is taken when the bet is placed. After the fight the losers' pot minus houseMargin is shared among the winners in proportion to their stakes, on top of their own stake back. With fewer than two bettors, or when nobody bet on the winner, every stake comes back. A cancelled or forfeited fight refunds everything.
The house margin is simply removed from the game, it is not paid to a society or an account. Owners may bet on their own fight.
The betting bar opens with L for anyone within promptRange of the pit while the window is open. The HUD, the banner, the bell and the pot updates reach everyone within Hud.range.
Sounds, crowd, animations, blood¶
Config.Sound = {
enabled = true,
bell = { file = 'bell.mp3', radius = 15.0, volume = 0.2, falloff = 1.5, durationMs = 4000 },
crow = { files = { 'crow1.mp3', 'crow2.mp3', 'crow3.mp3', 'crow4.mp3', 'crow5.mp3' }, radius = 20.0, volume = 0.4, falloff = 1.5, durationMs = 2500 },
cheer = { file = 'cheer.mp3', radius = 15.0, volume = 0.3, falloff = 1.5, durationMs = 5000 },
}
Config.Crowd = {
enabled = true,
count = 5,
radius = 5.2, -- distance to the middle of the pit
avoidNpc = 1.8, -- room around the bookmaker
models = { 'a_m_m_valtownfolk_01', 'a_m_m_valtownfolk_02', --[[ ... ]] },
-- stand, kinds and cheer: the animations the spectators play
}
Config.Particles = {
enabled = true,
dict = 'core',
onHit = { effect = 'blood_entry_head_fatal', scale = 0.6, z = 0.0 },
onHeavy = { effect = 'blood_entry_critical', scale = 1.0, z = 0.0 },
}
Sounds are the files in html/sounds, played on the client by distance. The crowd is five local peds (no network traffic) that stand around a fixed pit while betting or fighting and clap, cheer and point, then cheer once more when the fight is decided. A pit can bring its own spectator points with crowd = { points = { vector4(...), ... } } in its Config.Spots entry, or crowd = false for none. Config.Anims holds the rooster animations and is tuned to the sounds, leave it.
Turn Particles.enabled off if you do not want blood.
Rate limits and the book¶
Config.RateLimit = {
default = 8,
windowMs = 3000,
bet = 6,
name = 4,
-- ...
}
Config.Leaderboard = {
command = 'cockfightbook',
limit = 25,
scoreWin = 2, -- score = wins * scoreWin + kills * scoreKill
scoreKill = 1,
}
Rate limits cap how many requests a player may send per bucket within windowMs, a guard against spam from modified clients. The book's leaderboard shows the top limit roosters by score.
Discord (server_config.lua)¶
Config.Discord = {
enabled = false,
webhook = '',
name = 'Cockfight',
avatar = '', -- optional avatar url of the bot
thumbnail = '', -- optional thumbnail of the embed
}
With a webhook the server posts an embed after every fight (winner, loser, knockout or time, level, title, streak, pot, pit) and a message when a tournament is called and when it is won. The texts are the discord_* keys in locales.lua. Sparring matches are not logged.
Items¶
| Item | What it does |
|---|---|
rooster |
The fighting rooster. First use names it, every use after that at a pit sends it in, away from the pits it stakes out a wild pit. Never used up. Stack limit 5 in install.sql |
rooster_spurs |
Used on your own waiting rooster. More attacks and 30 percent more damage for 10 fights, then the pair breaks and is removed |
Both are item_standard, usable, and need an image in your inventory.
Commands and keys¶
| Command | What it does |
|---|---|
/cockfightbook |
Opens the book on the leaderboard tab from anywhere. The Tournament tab only works when the book was opened at a bookmaker. Name set by Config.Leaderboard.command |
/hahnfx [effect] [scale] [dict] |
Test the blood effect in front of you. Only exists with Config.Debug = true |
Keys in the pit:
| Key | Where | Action |
|---|---|---|
| G | Standing at your waiting rooster | Recall it |
| L | Within promptRange of a pit while bets are open |
Open the betting bar |
| Esc | In the betting bar | Close it |
| E | At the bookmaker | Open the book |
| R | At the bookmaker, your rooster waiting alone | Sparring match |
Staking out a wild pit:
| Key | Action |
|---|---|
| Arrow Up / Arrow Down | Push the ring further away or closer |
| Enter | Stake out the pit |
| Delete | Cancel |
Picking plumage:
| Key | Action |
|---|---|
| Arrow Left / Arrow Right | Previous or next plumage |
| Enter | Keep this one |
| Delete | Decide later |
In the book:
| Key | Action |
|---|---|
| Q / E | Switch tabs |
| Enter | Select the highlighted row |
| Esc | Back, or close the book |
Exports and events¶
The script offers no exports and no events for other resources. All traffic between client and server runs over nss_libs channels that belong to this resource.
FAQ¶
The rooster does nothing when I use it.
Check the console for ready, item "rooster" registered. at start. If it is missing, vorp_inventory started after hobbs_cockfight, fix the server.cfg order. Also make sure the item in your database is called exactly what Config.ItemName says and is marked usable.
I use the rooster at the pit and get "There is no cockfight pit here".
The player has to be within radius of the pit's center, plus SpotProximityPadding. Raise one of them a little, or move center. With Config.Arena.enabled = true a use away from every pit starts placement mode instead of that message.
How do I add a pit?
Copy the entry in Config.Spots, give it a unique id, set center, the two corners facing each other and, if you want sparring and tournaments there, the bookmaker's coords. Restart the resource. Blips are off by default, turn them on per pit under blip.
Where does the house margin go? Nowhere. The 8 percent of the losers' pot and the 10 percent of a tournament pot are taken out of the game, the same for the sparring fee. There is no society or account to receive them.
A bettor logged off before the fight ended. Is the money lost?
No. Winnings and refunds for a character who is offline are written to hobbs_cockfight_owed and paid the next time that character is selected, with a notification from the bookmaker.
What happens on a resource restart? Running fights are aborted and every open bet is refunded, wild pits are removed, running tournaments are cancelled with all entry fees refunded. Roosters in a pit go back to being normal items, nothing is lost. The rooster records live in the database and survive.
What happens when a player disconnects? Mid fight the other rooster wins by forfeit, bets are refunded, the leaver's rooster takes a normal loss. A rooster that was only waiting is taken out of the pit. A wild pit whose owner leaves is removed, a tournament entrant who leaves during signup is taken off the list, their fee stays in the pot and only comes back if the tournament is cancelled. During the rounds a leaver loses their next fight by walkover.
Can a player bet on their own rooster? Yes. The only limits are one bet per character and fight, the min and max stake, and standing close enough to the pit.
Can I give or sell a rooster to someone else? Yes, it is a normal item. The next character who uses it becomes the owner, the record, level and trait stay with the rooster.
The loser died but the item is still in the inventory.
That is how PERMADEATH works: the rooster is marked fallen, its description says "FALLEN" and it can never fight again. Players can throw it away or keep it.
Does the script cost performance?
The fight logic runs on the server, the clients only play animations. The crowd is local peds without network traffic, the name labels and the ring drawing only run while a rooster stands in a pit near the player. Rate limits stop clients from flooding the server. Most of the cost is inside Hud.range of an active pit.
Changelog¶
1.0.1¶
First release.