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¶
-1000 ≤ Player.personal_reputation ≤ +1000always (clamped on every write).Player.reputation_tierandPlayer.name_colorare in sync with the score.- A bounty's
amount ≥ BOUNTY_MIN_AMOUNT (1000). Placer.credits ≥ 0after placement (pre-flight).- Self-bounty prohibited (
placer_id != target_id). - Bounty collection happens at most once per kill — both rows locked, settings cleared atomically.
- System bounties are derived from current reputation, never persisted; they appear / disappear based on the score.
- Reputation decay never crosses 0 (decay stops at zero — sign-preserving).
- The 10% placement fee is non-refundable on cancel.
- 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 |
Related¶
- DATA_MODELS:
../DATA_MODELS/player.md,../DATA_MODELS/combat.md. - FEATURES:
../FEATURES/gameplay/combat.md,../FEATURES/gameplay/factions-and-teams.md. - SYSTEMS: combat-resolver.md, market-pricing.md (reputation modifier on prices), realtime-bus.md.
- API: bounty + reputation routes.