Skip to content

Bounty and Reputation

Purpose

The bounty-and-reputation subsystem is the moral-alignment machinery: every PvP-consequential action (attacking innocents, defending against aggressors, destroying escape pods, fulfilling bounty contracts) shifts the actor's personal_reputation score. The score is bucketed into eight tiers; at low tiers, the system automatically posts a bounty on the player. Player-placed bounties stack on top of system bounties. Bounty payouts are awarded atomically on combat win.

Inputs

The system reads: - Player.personal_reputation — integer, clamped [-1000, +1000]. - Player.reputation_tier — denormalized cache. - Player.name_color — denormalized cache. - Player.credits, Player.settings.bounties (JSONB list of player-placed bounties on this player). - Player.is_active. - The action / event that triggered the call (reason string).

The system fires on: - Combat resolution (see combat-resolver.md) — adjusts both sides' reputation, possibly collects bounties. - POST /api/v1/bounty/place — player places a bounty. - POST /api/v1/bounty/{id}/cancel — placer cancels (refund). - A weekly tick — applies decay toward 0 for extreme scores. - Trade completion (small +1 per legitimate trade), drone-defense events, etc.

Process

Reputation tier table

Score range          Tier         Color    Notable effects
[-1000, -750]        Villain      #FF0000  +20% station markup, bounty hunters target
[ -749, -500]        Criminal     #FF4400  +20% markup
[ -499, -250]        Outlaw       #FF8800  +10% markup
[ -249,   -1]        Suspicious   #FFCC00
[    0,    0]        Neutral      #FFFFFF
[    1,  249]        Lawful       #88FF88  -5% station discount
[  250,  499]        Heroic       #00FF00  -5% discount, +5 faction bonus
[  500, 1000]        Legendary    #00FFFF  -10% discount, +5 faction bonus

Reputation deltas (combat-driven)

Triggered inline by the combat resolver (post-resolution hooks):

Event Delta Reason key
Attacker wins — defender had bounties +100 defeat_bounty_target
Attacker wins — defender had NO bounty (innocent) -100 attack_innocent
Attacker wins — defender was in escape pod -500 kill_escape_pod (additive)
Defender wins +50 defend_against_attacker
Trade completed +1 complete_trade
Destroyed pirate sector drones +10 destroy_pirate_drones

adjust_reputation(player_id, amount, reason) is the single entry point. It: 1. Loads the player. 2. Clamps new_score = max(-1000, min(1000, old + amount)). 3. Updates personal_reputation, reputation_tier, name_color. 4. Logs an audit line. 5. Returns old/new pair for caller use.

Bounty placement

POST /api/v1/bounty/place {target_id, amount}

1. amount >= 1000  (BOUNTY_MIN_AMOUNT)
2. lock placer
3. fee = 10% of amount
4. total_cost = amount + fee
5. placer.credits >= total_cost
6. placer.credits -= total_cost
7. append entry to target.settings.bounties:
     { id, placed_by, placed_by_name, amount, placed_at, type: "player" }
8. flag_modified(target, "settings")
9. flush
10. emit `bounty_placed` event

The 10% fee is non-refundable; the principal is escrowed in the bounty entry until collected.

System bounties (automatic)

When Player.personal_reputation falls below thresholds, the system auto-attaches bounties at read time:

Reputation System bounty
≤ −500 5,000
≤ −750 25,000
≤ −1,000 100,000

These are not stored in the player's settings.bounties — they are computed on demand by _get_system_bounties(player). Lower thresholds are cumulative? No — only the lowest applicable tier applies (the highest-value bounty for the deepest pit).

Implementation note: the current code returns one entry per matched threshold; the target spec is single-tier (the deepest applicable). When migrating, replace the loop with a max-match.

Bounty collection (on kill)

Triggered automatically by the combat resolver when the attacker wins ship-vs-ship combat:

collect_bounty(collector_id, target_id):
1. lock collector + target rows
2. player_bounties = target.settings.bounties
3. system_bounties = _get_system_bounties(target)  (computed from rep)
4. total_player = sum(b.amount for b in player_bounties)
5. total_system = sum(b.amount for b in system_bounties)
6. total = total_player + total_system
7. if total == 0: return {success: False}
8. collector.credits += total
9. target.settings.bounties = []        (clear player-placed)
10. flag_modified(target, "settings")
11. flush
12. emit `bounty_collected` event

System bounties are paid out by the federation treasury (no off-system debit modeled — they are a credit sink/source). Player-placed bounties are paid from escrow held in the entry.

Cancellation

Placer can cancel a not-yet-collected bounty: 1. Locate entry by id in target's bounties. 2. Verify placed_by == placer.id. 3. Refund amount (NOT the 10% fee) to placer. 4. Remove entry. 5. Emit bounty_cancelled.

Reputation decay

A weekly scheduler tick calls apply_weekly_decay(player_id) for every active player: - If score == 0: no-op. - If score > 0: new = max(0, score - 5). - If score < 0: new = min(0, score + 5). - Updates tier + color.

Decay rate: 5 points / week. Hardcoded; intentionally slow so reputation extremes feel sticky.

Side-effects on pricing & encounters

  • personal_reputation ≤ −500 → +20% markup at stations.
  • personal_reputation ≥ +500 → −10% discount at stations.
  • personal_reputation ≤ −500 → bounty-hunter NPC encounter rate increased (see ../FEATURES/gameplay/factions-and-teams.md).

Outputs / state changes

Per reputation adjust: - Player.personal_reputation, Player.reputation_tier, Player.name_color mutated. - Audit row (target: reputation_history).

Per bounty place: - Placer.credits decremented (amount + 0.10*amount). - Target.settings.bounties appended.

Per bounty collect: - Collector.credits incremented. - Target.settings.bounties cleared (player-placed only).

Per cancel: - Placer.credits incremented (refund). - Target.settings.bounties entry removed.

Events emitted: - reputation_changed — personal to affected player. - bounty_placed — global + participants. - bounty_cancelled — placer + target. - bounty_collected — global + participants.

Invariants

  1. -1000 ≤ Player.personal_reputation ≤ +1000 always (clamped on every write).
  2. Player.reputation_tier and Player.name_color are in sync with the score.
  3. A bounty's amount ≥ BOUNTY_MIN_AMOUNT (1000).
  4. Placer.credits ≥ 0 after placement (pre-flight).
  5. Self-bounty prohibited (placer_id != target_id).
  6. Bounty collection happens at most once per kill — both rows locked, settings cleared atomically.
  7. System bounties are derived from current reputation, never persisted; they appear / disappear based on the score.
  8. Reputation decay never crosses 0 (decay stops at zero — sign-preserving).
  9. The 10% placement fee is non-refundable on cancel.
  10. Hooks fired by the combat resolver run in best-effort blocks: a failure to award a bounty does not roll back the underlying combat result.

Failure modes

Mode Target handling
Concurrent kill (two players claim same target) Row lock on both ends; combat resolver only fires collect_bounty for the actual winner; second loses race naturally.
Bounty placed on player who deletes account Bounty stays attached to deleted target's settings; mark unclaimable; admin tool refunds placers.
Reputation overflow from large delta (e.g. design-time tuning) Clamp catches; new value pinned at extreme.
Bounty list grows unbounded (many small placements) Soft cap: 50 entries; older entries collapsed by placer (sum amounts under one entry).
Decay tick missed Next tick processes everyone past their last decay timestamp; no events lost.
Tier change mid-action reputation_tier updated on every adjust; downstream readers always see fresh tier.
Settings JSONB write race flag_modified + row-locked transaction.
System-bounty stacking bug Target spec: only deepest-tier system bounty applies; collection logic must match.

Source map

Concern Path (target)
Reputation service services/gameserver/src/services/personal_reputation_service.py
Reputation tier table + triggers same file (REPUTATION_TIERS, REPUTATION_TRIGGERS)
Bounty service services/gameserver/src/services/bounty_service.py
System bounty tiers same file (SYSTEM_BOUNTY_TIERS)
Combat hooks calling these services/gameserver/src/services/combat_service.py:attack_player
Decay scheduler services/gameserver/src/services/personal_reputation_service.py:apply_weekly_decay
Player fields services/gameserver/src/models/player.py (personal_reputation, reputation_tier, name_color, settings)
Bounty REST API services/gameserver/src/api/routes/player.py (or dedicated bounty.py route — target)
Realtime broadcast services/gameserver/src/services/websocket_service.py