Skip to content

Bounty Hunter

hobbs_bountyhunter, current version 1.0.1

Overview

Puts a bounty board at every sheriff office you configure. Players read the posters, ride out alone or as a posse of up to four, deal with a gang of bandits and bring the wanted man back to the office, dead or alive. Some jobs ask for the man alive, some ask you to free hostages from a hideout and walk them home. Every finished job pays cash and XP, XP moves the player up five ranks, and the harder posters only open up at higher ranks.

The script ships with 70 posters: 55 gang jobs in five difficulties and 15 hostage rescues. Bandits spawn at one of 37 spawn points spread over the whole map, hostage jobs use one of 16 hideouts. Jobs, ranks and cooldowns are stored in the database, so nothing is lost on a restart. If you want, every hunt start, payout, failure, new rank and admin action goes to Discord.

How it plays

  1. You walk up to the bounty board at a sheriff office and press E. A board opens with three tabs: Posters, Posse and Career.
  2. On the Posse tab you type the server ID of a friend and invite them. They get a notification and answer at any board. Up to four riders, the one who invites is the leader.
  3. On the Posters tab you pick a job. Each poster shows the wanted man, what he is wanted for, the difficulty, the money range for your rank and the XP. Posters above your rank are shown but locked, posters any posse took in the last 90 minutes say "Taken".
  4. The leader accepts. Every member has to stand within 10 meters of the board. A route and a blip point to the spawn area, which is drawn fresh for every hunt.
  5. When the first member gets within 100 meters of the spot, the gang spawns and attacks. The leader's game spawns them, the server checks that they really arrived. The wanted man is marked with his own blip and name, the rest are plain bandits.
  6. Dead or alive: shoot everyone, or leave the boss standing and bring him in alive. Alive only: the boss has to survive, if he dies the job fails. Once the gang is down you get a message to bring the target back to the same office you took the poster from.
  7. Hostage rescue: the hostages kneel tied up next to the guards. Once the last guard is dead they are free and each one follows a member of the posse, on foot or riding behind on the horse. Every hostage that reaches the office counts.
  8. Back at the office the leader drops the target near the turn-in spot. The turn-in happens on its own a few seconds later, or you press "Turn in bounty" on the board. Every member standing within 100 meters gets their share of the cash and XP, a message shows the sum, and a new rank is announced if you crossed the line.
  9. After a hunt every member waits 29 minutes before the next one, and the same poster stays locked for 90 minutes for the whole server.

Requirements

Resource Note
vorp_core Characters, cash, notifications and the death and revive events
nss_libs Prompts, blips, the database layer, character data and client-server events
oxmysql Database behind nss_libs

No job is needed. Any character can read a board and take a job, the only gate is the rank. The admin command needs the VORP group admin or the admin flag on the character.

Installation

  1. Unpack the resource into resources and keep the folder name hobbs_bountyhunter.
  2. Run sql/install.sql on your database. It creates three tables: hobbs_bountyhunter_xp (XP, rank and statistics per character), hobbs_bountyhunter_hunts (every hunt with state, members, payout and XP) and hobbs_bountyhunter_member_cooldowns (when each character last finished a hunt). The resource also creates the tables on its own at start and adds the streak and payout columns, so the file is a convenience, not a must.
  3. There are no items and no item images to add, and no jobs to create.
  4. Add it to server.cfg after its dependencies:

    ensure oxmysql
    ensure vorp_core
    ensure nss_libs
    ensure hobbs_bountyhunter
    
  5. Open server_config.lua and paste your Discord webhook URLs if you want the log. Empty means off.

  6. Set Config.Locale in config/config.lua to en or de.
  7. Start the server. Watch the console for a yellow warning about empty spawn points, the list is filled by default, so you should see none. Any hunt that was still running when the server went down is set to failed on boot and the cooldown for its members is written.

Note

server_config.lua stays on the server. Everything in config/ is sent to every client, so never put a webhook URL into a config file.

Configuration

Settings are split over config/config.lua (general), config/boards.lua (the boards), config/bounties.lua (the posters and spawn points), config/hostages.lua (hideouts and hostage behaviour), config/npc.lua (bandits), config/payouts.lua (money and XP), config/ratelimit.lua and server_config.lua (Discord). All texts live in locales.lua.

General

Config.Locale = 'en'       -- "en" or "de"
Config.Debug  = false      -- console output, and it switches cooldowns and the poster lock off

Config.AdminGroups  = { 'admin' }   -- VORP groups that may use the admin command
Config.AdminCommand = 'bounty'      -- /bounty admin ...

Config.Notify = {
    dict     = 'generic_textures',
    duration = 5000,
    topics = {
        info    = { icon = 'tick' },
        warn    = { icon = 'cross', duration = 6000 },
        posse   = { icon = 'tick' },
        reward  = { icon = 'tick', duration = 7000 },
        hostage = { icon = 'tick' },
        rank    = { icon = 'tick', duration = 7000 },
    },
}

Notifications go through vorp_core (NotifyLeft). Each topic has its own title in locales.lua (notify_title_<topic>), so the player sees "Posse", "Bounty" or "Hostage rescue" above the text.

Config.Debug is for testing on a development server. Besides the console output it turns off the 29 minute cooldown and the 90 minute poster lock, so you can run the same job again and again. Leave it false on a live server.

Posse

Config.Posse = {
    Max              = 4,       -- members including the leader
    InviteTtlMs      = 20000,   -- an invite expires after this time
    ShowMemberBlips  = false,   -- blips of the posse members on the map
    LockedDuringHunt = true,    -- no leave, kick or invite while a hunt runs
}

A player is always in a posse, alone until they invite someone. The leader invites by server ID on the Posse tab. The invited player has 20 seconds to accept at a board. Only a player who is riding alone and not on a hunt can be invited, otherwise the leader sees "is already in a posse".

With LockedDuringHunt = true the posse is frozen from accept to payout. I kept it that way because a member who leaves in the middle of a job would still count for the payout split, and a new one would appear out of nowhere at the office. If a member disconnects during a hunt they are dropped anyway. When the leader drops, the next member becomes leader. When nobody is left, the hunt fails.

ShowMemberBlips = true puts a blip on every posse member. Handy on a small server, noisy on a big one.

Boards

Config.Boards = {
    ChunkRadius           = 50.0,   -- the board prop exists within this distance
    PromptRadius          = 2.0,    -- default prompt distance, a board can override it with interactDist
    StartAllMembersRadius = 10.0,   -- every member has to be this close to the board to start a hunt
    PromptKey             = 0xCEFD9220, -- E
}

The boards themselves are in config/boards.lua. Seven ship with the script: Blackwater, Valentine, Annesburg, Rhodes, Strawberry, Saint Denis and Tumbleweed. Treat them as examples, move or replace them to fit your map.

Config.SheriffBoards = {
    {
        id = 'valentine',
        name = 'Valentine',
        vector = vector4(-270.01, 807.2, 119.36, 100.0),
        turnInVector = vector3(-276.21, 815.75, 119.14),
        prop = 'mp005_p_mp_bountyboard01x',
        interactDist = 1.5,
        turnInDistance = 5.0,
    },
    -- ...
}
Key Meaning
id Short name, used in the database and in notifications. Keep it unique
name Town name. The label becomes board_label from the locale, "Sheriff office Valentine"
vector Position and heading of the board
turnInVector Where the target or the hostages have to be brought. Leave it out to use the board position
prop Board model. Leave it out where the town already has a board, as in Annesburg, Rhodes and Tumbleweed
interactDist How close you have to stand for the prompt, default Config.Boards.PromptRadius
turnInDistance How close the target or hostage has to be to the turn-in spot, default 8
blipSprite Optional, replaces Config.Blips.Boards.Sprite for this board only

The prop is only created while a player is within ChunkRadius, so an empty town costs nothing. A job is always turned in at the board where it was taken, no matter where the gang spawned. Put turnInVector in front of the office door or next to the jail, somewhere a player can drop a body without blocking the board.

Bounties

Everything about the posters is in config/bounties.lua. The texts (label, description, wanted for) are in locales.lua under bounty_<key>_label, bounty_<key>_desc and bounty_<key>_wanted.

Config.BountySpawnAnchors = {
    vector3(1931.41, 1959.48, 263.65),
    vector3(1597.04, -1844.32, 52.4),
    -- ... 37 in total
}

Config.Bounties = {
    west_dakota_raiders = {
        type        = 'dead_or_alive',
        target_name = 'Elias Crowe',
        min_tier    = 'bronze',
        difficulty  = 1,
        enemies = {
            { count = 4 },
        },
        payout      = { xp = 10 },
        time_limit_sec = 60 * 45,
    },
    dakota_horse_thieves = {
        type        = 'alive_only',
        target_name = 'Silas Boone',
        min_tier    = 'bronze',
        difficulty  = 2,
        enemies = {
            { count = 3 },
            { count = 2, mounted_count = 2 },
        },
        payout      = { xp = 16 },
        time_limit_sec = 60 * 45,
    },
    hostage_stagecoach_passengers = {
        type        = 'prisoner_rescue',
        min_tier    = 'gold',
        difficulty  = 3,
        enemies = {
            { count = 5 },
            { weapon = 'WEAPON_REPEATER_CARBINE', count = 3 },
        },
        hostages    = 2,
        payout      = { xp = 24 },
        time_limit_sec = 60 * 45,
    },
    -- ...
}
Key Meaning
type dead_or_alive, alive_only or prisoner_rescue
target_name Name of the wanted man, shown on the poster, on his blip and above his head. Not used for rescues
min_tier Rank needed to take the poster: bronze, silver, gold, outlaw or legend
difficulty 1 to 5. Scales bandit health and accuracy (config/npc.lua) and the money (config/payouts.lua)
enemies Groups of { count, weapon?, mounted_count? }. Without weapon each bandit gets a random one from RandomWeaponPool. mounted_count bandits of the group come on horseback
hostages Rescues only, 1 or 2. The hideout needs at least that many kneeling spots
fail_on_target_death true fails the job when the target dies. Always on for alive_only
payout { xp }. The money is not set here, it comes from the difficulty
time_limit_sec Optional, default Config.HuntWatchdog.MaxDurationSec

The first bandit the gang spawns is the wanted man. On a dead or alive job you can shoot him, or kill the others and bring him in breathing, which counts as "caught alive" in the career stats. On an alive only job his death fails the hunt on the spot. A spawn point is drawn at random from Config.BountySpawnAnchors for every start, never the same one twice in a row for the same poster, so players cannot learn where a gang waits.

The shipped posters are grouped like this:

Difficulty Rank Bandits XP Posters
1 bronze 4 10 13
2 bronze 5 to 6 16 13
3 silver 7 to 9 24 11
4 gold 10 to 13 34 9
5 legend 14 to 20 50 9
rescue 1 to 5 gold like the gang jobs 10 to 50 15

To add a poster, copy an entry, give it a new key, and add the three texts to every language block in locales.lua. To take one away, delete the entry or set its type off:

Config.EnabledBountyTypes = {
    dead_or_alive   = true,
    alive_only      = true,
    prisoner_rescue = true,
}

A type set to false disappears from every board. If you do not want the hostage walk on your server, that is the switch.

Timing and the gang are controlled here:

Config.HuntWatchdog = {
    TickMs           = 60000,
    MaxDurationSec   = 60 * 45,   -- a hunt fails after this time
    BountyKeyLockSec = 60 * 90,   -- the same bounty cannot be taken again for this long, server wide
    CooldownSec      = 60 * 29,   -- pause for every member after a hunt
}

Config.NPC = {
    CleanupGraceMs             = 30000,  -- bandits are removed this long after the last member left the area
    SpawnTriggerDistance       = 100.0,  -- bandits spawn when a member is this close to the spawn point
    SpawnIntervalMs            = 100,    -- pause between two bandit spawns
    LeaderFailDistance         = 200.0,  -- hunt fails when the leader is further away from the spawn point
    EnableDistanceFailCheck    = true,
    RetargetTickMs             = 3000,   -- how often a bandit looks for a new target
    SpawnValidationRadius      = 150.0,  -- max. distance of a reported bandit from the spawn point
    SpawnReportMinFraction     = 0.75,   -- less than this share of the bandits reported = hunt fails
}

Config.Performance = {
    MaxEnemiesPerBounty = 20,    -- hard cap, larger jobs are cut down at start
}

The cooldown is written for every member when a hunt ends, whichever way it ends. The poster lock keeps a posse from farming the same gang, and because it is server wide it also keeps two posses from riding to the same spot. With CooldownSec = 60 * 29 and a 45 minute limit a group can do roughly one job an hour, which is what the payout table is balanced for. If you cut the cooldown, cut the money too.

LeaderFailDistance kicks in once the gang has spawned. A leader who rides 200 meters away from the spawn point loses the job, so bandits are not left standing around. Set EnableDistanceFailCheck = false if your players like to pull the gang towards a town.

The leader's game spawns the gang and reports every bandit to the server. The server drops any bandit that is too far from the spawn point or has the wrong model, and if fewer than 75 percent of them arrive the hunt fails with "The gang could not be tracked down". That is the guard against a client sending made-up bandits, do not loosen it.

Bandits

config/npc.lua controls how the bandits fight and look.

Config.NPCDefaults = {
    Combat = {
        attributes   = { [46] = true, [5] = true, [1] = true },  -- fights unarmed, always fights, uses cover
        flee_disable = true,
        range        = 2,     -- 0 near, 1 medium, 2 far
        movement     = 3,     -- 0 stationary, 1 defensive, 2 offensive, 3 charge
        accuracy     = 60,    -- fallback, see AccuracyRangeByDifficulty
        health_mult  = { 1.2, 1.4, 1.6, 2.0, 2.5 },   -- by difficulty 1 to 5
        relationship = 'HATES_PLAYER',
    },

    RandomOutfit      = true,
    HorseRandomOutfit = true,

    AccuracyRangeByDifficulty = {
        [1] = { min = 60, max = 70 },
        [2] = { min = 70, max = 80 },
        [3] = { min = 80, max = 90 },
        [4] = { min = 85, max = 95 },
        [5] = { min = 95, max = 100 },
    },

    BanditModelPool = {
        'G_M_M_UNIBANDITOS_01',
        'G_M_M_UNIDUSTER_01',
        -- ...
    },
    RandomWeaponPool = {
        'WEAPON_REVOLVER_CATTLEMAN',
        'WEAPON_REVOLVER_SCHOFIELD',
        'WEAPON_REPEATER_CARBINE',
        'WEAPON_SHOTGUN_DOUBLEBARREL',
    },
    HorseModels = {
        'A_C_HORSE_KENTUCKYSADDLE_BLACK',
        -- ...
    },
    HorseTack = { Enabled = true, Randomize = true, Saddles = { ... }, Bridles = { ... } },
}

Every bandit gets a random model from the pool, a random outfit and, if the group has no weapon, a random gun from RandomWeaponPool. Accuracy is drawn per bandit from the range of the job's difficulty, health is the base health times health_mult. A difficulty 5 bandit has two and a half times the health and shoots at 95 to 100 percent accuracy, that is on purpose, legend posters are meant for a full posse.

If your players find the gangs too hard, lower the top of AccuracyRangeByDifficulty first, for example [5] = { min = 80, max = 90 }. Health comes second, because a bandit who takes a whole cylinder to drop feels wrong in this game.

Hostages

config/hostages.lua holds the hostage behaviour and the hideouts.

Config.Hostages = {
    Models = {
        'a_f_m_skpprisononline_01',
        'a_m_m_skpprisoner_01',
        'a_m_m_skpprisonline_01',
    },
    Anims = {
        { dict = 'mech_busted@unapproved', name = 'idle_b' },
        { dict = 'script_proc@robberies@shop@rhodes@gunsmith@inside_basement_reshoot', name = 'cry_loop_captor' },
    },
    Health  = 300,      -- after being freed. Tied up they cannot be hurt.
    Spacing = 1.5,      -- distance between hostages when a hideout has only one point

    RideAlong        = true,    -- mount behind the rider or on a free wagon seat
    RideAlongSpeed   = 2.0,
    RideAlongTries   = 2,
    RideAlongRetryMs = 3500,

    DeliverRadius  = 10.0,   -- a hostage is safe this close to the turn in point of the board
    WalkOffSeconds = 8,      -- a delivered hostage walks away and disappears after this time
    CollectSeconds = 600,    -- time the posse has to reach the board when the last hostage died on the way

    LostMeters     = 500.0,  -- no living member closer than this ...
    LostSeconds    = 120,    -- ... for this long = the hostage counts as lost
    MissingSeconds = 20,     -- a hostage the server does not see for this long is lost, a bandit counts as dead

    TickMs  = 2500,
    SweepMs = 15000,         -- the server removes leftovers this long after the end

    Locations = {
        { name = 'Near Flatneck Station', guards = { x = -339.92, y = -140.3, z = 49.17 },
          hostages = {
            { x = -328.68, y = -151.09, z = 51.08, h = 21.4 },
            { x = -326.65, y = -149.11, z = 51.08, h = 113.95 },
        } },
        -- ... 16 in total
    },
}

The hostage models are civilians on purpose. Bandit models bring a horse along when they spawn, and a hostage with a horse would ride off. While tied up a hostage cannot be hurt, so a careless shot into the camp does not cost the job. After the last guard falls the hostages stand up, each one is given to the closest member who is alive, spread evenly, and follows that member. On a horse they climb up behind the rider, on a wagon they take a free seat, on foot they walk.

A hostage is "safe" once it is within DeliverRadius of the board's turn-in spot. It walks off and disappears after WalkOffSeconds. A hostage is lost when no living member has been within LostMeters for LostSeconds, or when the server does not see it for MissingSeconds. A hostage shot after being freed is dead. Pay and XP are scaled by hostages delivered divided by hostages total, so one of two hostages brought home pays half. No hostage delivered means the job failed.

There is no turn-in step for a rescue. As soon as every hostage is delivered, dead or lost, the server pays out on its own if the posse is at the board. If nobody is within PresenceRadius at that moment, the members get "Come to the office to collect your pay" and have CollectSeconds to get there.

Hideouts are written in two ways. With hostages you set one kneeling spot per hostage, position plus the facing h, and you need at least as many spots as the job has hostages. With coords and heading the script lines the hostages up along that heading with Spacing between them. guards is where the bandits spawn and where the route leads, leave it out to use the middle of the hostage spots. The guards spot must be within Config.NPC.SpawnValidationRadius of the hostages or the gang is rejected. A hideout is drawn at random for every rescue, never the one the same poster used last time and never one another posse is in right now. The shipped hideouts are examples, replace them with spots that fit your map.

Payouts and ranks

config/payouts.lua decides the money, Config.Tiers and Config.XP in config/config.lua the ranks.

Config.Payouts = {
    TierMoneyMultiplier = {
        bronze = 1.00,
        silver = 1.10,
        gold   = 1.25,
        outlaw = 1.35,
        legend = 1.50,
    },
    PosseSizeMultiplier = {
        [1] = 1.0,
        [2] = 1.5,
        [3] = 2.0,
        [4] = 3.0,
    },
    Split = 'even',            -- 'even' or 'leader_60'
    PresenceRadius = 100.0,    -- members have to be this close to the turn in point, 0 = off
    CurrencyType = { money = 0 },   -- vorp currency: 0 cash, 1 gold
    DifficultyMoneyRange = {
        [1] = { min = 10, max = 15 },
        [2] = { min = 15, max = 19 },
        [3] = { min = 19, max = 23 },
        [4] = { min = 23, max = 26 },
        [5] = { min = 26, max = 30 },
    },
}

Config.Tiers = {
    { key = 'bronze', min = 0,    color = '#cd7f32' },
    { key = 'silver', min = 500,  color = '#c0c0c0' },
    { key = 'gold',   min = 1500, color = '#c8a84b' },
    { key = 'outlaw', min = 3000, color = '#c41e1e' },
    { key = 'legend', min = 5000, color = '#9b30ff' },
}

Config.XP = {
    TurnInBonusFraction = 0.5,   -- bonus on top of the job XP when turning in
}

The money is worked out at turn-in, in this order:

  1. A base sum is drawn at random from DifficultyMoneyRange for the job's difficulty.
  2. It is multiplied by the highest TierMoneyMultiplier among the members present. One legend hunter in the posse lifts the whole pot.
  3. It is multiplied by PosseSizeMultiplier for the number of members present. A missing size uses the last entry.
  4. On a rescue it is multiplied by hostages delivered divided by hostages total.
  5. The pot is split. even gives everyone the same, leader_60 gives the leader 60 percent and shares the rest. Money is whole dollars, any dollar left over from the split goes to the leader.

Only members within PresenceRadius of the turn-in spot at that moment are paid, get XP and get the statistics. A member who stayed at the spawn point gets nothing. The cash is added to each member with vorp_core, nothing is taken from anyone.

XP works the same way: the poster's XP times the posse multiplier, divided by the members present, plus TurnInBonusFraction of that as a bonus. The sum is compared against Config.Tiers and the rank moves up when the XP crosses min. A rank is never taken away.

Example: a posse of two turns in a difficulty 3 job, one of them gold. The base roll gives 20, times 1.25 for the gold member, times 1.5 for two riders is 37 dollars, split 19 and 18. XP is 24 times 1.5 divided by 2, that is 18, plus a 9 bonus, 27 each.

The posse multiplier climbs steeply (a full posse triples the pot) because four riders split it four ways and a four man job at difficulty 5 is a real fight. If you run a lot of solo hunters and want them to earn more, raise DifficultyMoneyRange rather than flattening the posse table, or the big gangs are not worth the ride.

The Career tab shows rank, XP to the next rank, bounties completed and failed, brought in dead, caught alive, hostages rescued, current and best streak, best time and total payout. A failed or cancelled hunt counts as failed for every member and resets the streak.

Blips

Config.Blips = {
    Boards     = { Enabled = true, Sprite = 'blip_wanted_poster', Color = 'BLIP_MODIFIER_MP_COLOR_32', Size = 'small' },
    Members    = { Sprite = 'blip_ambient_hunter', Color = 'BLIP_MODIFIER_MP_COLOR_32', Size = 'small' },
    HuntArea   = { Sprite = 'blip_ambient_marked_for_death', Color = 'BLIP_MODIFIER_MP_COLOR_32', Size = 'small' },
    HuntTurnIn = { Sprite = 'blip_mp_cash_checkpoint', Color = 'BLIP_MODIFIER_MP_COLOR_20', Size = 'small' },
    Target     = { Sprite = 'blip_ambient_hunter', Color = 'BLIP_MODIFIER_MP_COLOR_6', Size = 'small' },
    Targets    = { Sprite = 'blip_ambient_corpse', Color = 'BLIP_MODIFIER_MP_COLOR_32', Size = 'small' },
    Hostage    = { Sprite = 'blip_ambient_companion', Color = 'BLIP_MODIFIER_MP_COLOR_3', Size = 'small' },
    Route      = { Enabled = true, ColorHash = 'COLOR_YELLOW' },
}

Sprites are RedM blip names, colors BLIP_MODIFIER_MP_COLOR_*, sizes small, medium or big. Boards.Enabled = false hides the board blips if you want players to find the offices themselves. Route.Enabled = false turns off the GPS line to the spawn point and back to the office, the blips stay.

Rate limit

Config.RateLimit = {
    windowMs = 3000,
    default  = 8,
    ['bh:hunt:start']  = 3,
    ['bh:hunt:kill']   = 30,
    -- ...
}

How many requests of one kind a player may send within windowMs. Above the limit the request is dropped and the player sees "Not so fast, stranger." once. The defaults are generous for normal play, you only need to touch them if a legit action gets blocked on a server with very high ping.

Discord webhooks

server_config.lua is the only file that never reaches a client.

Config.Webhooks = {
    Start    = '',   -- hunt started
    Complete = '',   -- hunt completed and paid
    Failed   = '',   -- hunt failed or cancelled
    TierUp   = '',   -- new rank
    Admin    = '',   -- admin command used

    BigPayoutThreshold = 100,   -- payouts from this sum get the bigwin color

    Username = 'Bounty office',
    Avatar   = '',

    Colors = {
        start    = 15844367,
        complete = 5763719,
        failed   = 15158332,
        tierup   = 3447003,
        admin    = 10181046,
        bigwin   = 16766720,
    },
}

One URL per event, all five can point to the same channel. Each post is an embed with the poster, difficulty, leader and members as character IDs, hunt id, and for a completed hunt the payout, XP and duration. A payout at or above BigPayoutThreshold gets the bigwin color and the title "Big bounty collected!". Embed titles and field names come from locales.lua, colors are Discord decimal colors.

Language

Config.Locale = 'en'

locales.lua has an en and a de block with every prompt, notification, UI label, rank name, webhook text and the 70 poster texts. To add a language, copy a block, translate it and set Config.Locale to its key. %s and %d are filled in order, keep them.

Commands and keys

Command Who Does
/bounty admin list admin Lists every running hunt with id, poster, leader, members and kills
/bounty admin cancel <hunt_id> admin Cancels a hunt, the members are told it was an admin
/bounty admin xp <charid> <amount> admin Gives XP to a character, the rank follows
/bounty admin tier <charid> <tier> admin Sets a rank directly: bronze, silver, gold, outlaw or legend

/bounty without admin prints the help line. The command name is Config.AdminCommand, the groups are Config.AdminGroups. Every admin action is posted to the Admin webhook.

At a board:

Key Action
E Open the board (Config.Boards.PromptKey)

Inside the board:

Key Action
Q / E Switch tabs
Enter Select the highlighted entry
Esc Back, or close the board
Hold the Remove button Kick a member (leader only)

Everything else is a click: accept a poster, invite by server ID, accept or decline an invitation, leave the posse, cancel the running hunt, turn in the bounty. Cancel and leave ask for a confirmation first.

There are no exports. The script talks to other resources only through nss_libs and vorp_core.

FAQ

The board prompt does not show. Stand within interactDist of the board position (1.5 meters on the shipped boards). The prompt is only built once the board is within ChunkRadius, so if you just teleported, wait a second. The board needs no job, so the rank is the only thing that can hide a poster, not the prompt.

I accepted a poster and nothing happened. Every member has to be within StartAllMembersRadius (10 meters) of the board, and the leader sees who is not. The leader also sees a message when a member is still on cooldown, when the poster is locked for the server or when the rank is too low. Only the leader can accept.

The gang does not spawn. Bandits spawn when a member of the posse gets within SpawnTriggerDistance (100 meters) of the spawn point, and the leader's game does the spawning. If the leader is far behind the group, the first member at the spot will see nobody until the leader catches up. The hunt fails with "The gang could not be tracked down" when fewer than 75 percent of the bandits could be spawned near the point, which usually means a spawn anchor sits inside a rock or under water. Check the Config.BountySpawnAnchors entry the route pointed to.

"Drop the body first, then turn in." The target cannot be turned in while a player carries it. Put it down within turnInDistance of the board's turnInVector and the turn-in runs on its own, or press "Turn in bounty".

The job failed with "Too far from the target". The leader rode more than LeaderFailDistance (200 meters) from the spawn point after the gang had spawned. Lure the gang if you must, but keep the leader close, or raise the distance.

Who gets paid? Every member who is online and within PresenceRadius (100 meters) of the turn-in spot when the hunt is turned in. The pot is scaled by the number of members present, not the number who started. A member who logs out before the turn-in gets nothing and still gets the cooldown.

Can I turn in at a different office? No. A poster is always turned in where it was taken, the blip and the route point there once the gang is down. The notification names the office.

A player cannot take a hostage poster. Every rescue needs the gold rank (min_tier = 'gold'), which is 1500 XP. Lower min_tier in config/bounties.lua if you want them earlier.

What happens on a server restart? Running hunts are set to failed on boot, the bandits and hostages are gone with the restart anyway. Members get the 29 minute cooldown as after any failed hunt. XP, ranks, statistics and cooldowns are in the database and survive. Posses are in memory only, so players form them again at a board.

Can I run two posses at once? Yes. Every posse has its own hunt, its own spawn point and, for rescues, its own hideout. A poster another posse is on shows as "Taken" until its 90 minute lock runs out.

How much does this cost in performance? Outside a hunt the client only watches the distance to the boards and streams the board props. The gang is at most MaxEnemiesPerBounty (20) peds per posse, spawned by one client and removed 30 seconds after the last member leaves the area. The server checks running hunts once a minute (HuntWatchdog.TickMs) and hostages every 2.5 seconds (Hostages.TickMs). Kill statistics are written in batches, not per shot.

Changelog

1.0.1

First release.