NPC Traders¶
Status: π§ Partial β TRADER wallet/cargo, market participation, player-visible presence (windshield, targeting, annunciator), and visible supply-delivery restock spawn are β shipped (supply-delivery land tip
5969c9e5).tick_productionthin-floor dial-down is β tip-shipped on46bce720via PR #704 / board#1141(TICK_PRODUCTION_THIN_FLOOR_RATIO=0.15). (re-verified 2026-08-21 vs Sectorwars210246bce720.)
TRADER-archetype NPCs are full economic actors, not flavour. They carry real credit wallets, buy and sell real goods at stations, pay the same tariffs and trigger the same hostility metrics as players, occupy real docking slips, and run cross-region routes through the galaxy. The guiding principle: the economy is driven by real product moved by real actors, not by invisible auto-supplement. When a station restocks, it is because a trader hauled goods in and sold them β not because a regen tick conjured stock from nothing.
This doc covers the trader wallet and cargo model, the restock-by-delivery loop, market participation (tariffs, hostility, docking, attribution), cross-region routing and warp-gate access, the notoriety system that decides which traders are lawful targets, and which numbers are canon vs. gameserver-tunable.
The closely-related docs:
- ./trading.md β station classes, dynamic pricing, and the transaction flow traders share with players.
- ./port-ownership.md β station owner tariffs and port-takeover hostility, both of which apply to trader trades.
- ./docking-slips.md β the finite-slip model traders compete for alongside players.
- ../../SYSTEMS/npc-lifecycle.md β the TRADER archetype, succession, and lifecycle this doc gives economic teeth.
- ../../SYSTEMS/npc-scheduler.md β the scheduler that spawns and steps trader rosters.
Trader wallet and cargo¶
Every trader carries a real credits wallet, seeded variably by ship profile β a larger, more valuable trading hull spawns with proportionally more credits on hand, so a Freighter captain is richer than a Clipper courier. A trader spends real currency to buy goods and is stalled when broke until a profitable leg refills the wallet. Accumulated profit advances the canon 100-route career metric.
The cargo a trader holds is real goods it actually purchased at a station β never spawned-from-nothing cargo. Because both the wallet and any undelivered cargo are real, they are lootable on KIA: kill a trader carrying credits or goods and that value drops.
β Shipped. Traders carry a real
creditswallet (models/npc_character.py:credits) seeded by ship profile at spawn (services/npc_spawn_service.py), and buy/sell real goods at station stops, spending and refilling the wallet leg by leg (services/npc_trading_service.py).
Restock by delivery¶
Station stock movement is hybrid, real-transaction-dominant. Passive tick_production regen survives only as a thin baseline floor; the great majority of station stock movement is real product moved by NPC and player transactions. The auto-supplement weight is dialed down so the economy reflects actual product flow.
A station's restock is realized as a visible NPC supply-delivery. When a station runs low, a supply trader spawns in an empty sector carrying goods and delivers them to the station to sell, rather than the station conjuring stock from a regen tick. The scheduler still spawns the trader, but the goods arrive by a visible haul β the abstraction is one step further back, where players can see and intercept it.
Trader roster size scales to a region's trading-station count, with randomness; an operator override via NPCRoster.target_count is always available.
π§ Partial. Roster sizing and the buy/sell trade loop are β Shipped on origin/feat
5969c9e5. Visible supply-delivery restock is β Shipped on5969c9e5(LEG-418 / PR #702). Thin-floor dial-down is β Shipped on live tip46bce720.
- β Visible supply-delivery restock β shipped LEG-418 / PR #702 on tip
5969c9e5β low-stock station triggers a goods-carrying TRADER spawn that hauls in and sells viarun_trade_stop.- β tick_production thin floor dial-down β tip-shipped LEG-423 / PR #704 / board
#1141on46bce720βTICK_PRODUCTION_THIN_FLOOR_RATIO=0.15caps passive regen toward baseline floor (trading_service.py+tests/unit/test_tick_production_thin_floor.py).
Market participation¶
Traders are first-class market actors. A trader trade is treated identically to a player transaction wherever it touches the shared market:
- Tariffs. Station-owner tariffs apply to trader buys and sells, just as they do to players (see
./port-ownership.md). - Hostility. Port-takeover hostility metrics accumulate from trader trades. Anti-exploit guard: hostility accounting distinguishes NPC- from player-driven contribution, so an NPC route can never trigger a takeover cascade a player never caused.
- Docking slips. Traders occupy real docking slips like players β a busy station can be full because traders are berthed there (see
./docking-slips.md). Anti-camp guard: a slip-tenure limit forces traders to release slips promptly so they cannot be used to grief player docking. - Attribution. Every trader trade is attributed via the
enhanced_market_transactions.npc_idnullable FK β the canonical NPC attribution column. Future actor types add their own nullable FK; theplayer_idindex stays hot for player trade-history reads.
β Shipped β attribution.
enhanced_market_transactions.npc_id(nullable FK with its own index,models/market_transaction.py) attributes TRADER trades; exactly one ofplayer_id/npc_idis set per row.β Shipped β tariffs, platform fee, and region tax. NPC trade stops reuse the player path's
compute_region_tariff_multiplier(services/npc_trading_service.py~553) for buy/sell pricing, the same Class-0 platform transaction fee (transaction_fee_rate/STANDARD_TRANSACTION_FEE) charged on top of goods cost, and region tax (viarealize_region_tax) β full player/NPC parity, plus station-owner tax via_station_tax_rate/_realize_station_tax(same 40/30/30realize_port_revenuesplit as players).β Shipped β hostility. Port-takeover hostility from trader trades accrues into a separate, capped NPC sub-ledger (
npc_takeover_hostilityonStation.ownershipvia_accrue_npc_hostility,npc_trading_service.py~441β459 / ~721β722). Structural isolation: NPC trades writeplayer_id=None, so they cannot satisfy a player challenger's >50% volume takeover threshold; the isolated ledger is never read by the takeover engine. π NO-CANON βNPC_HOSTILITY_PER_TRADE_WEIGHT/NPC_HOSTILITY_CAPmagnitudes only.β Shipped β slip occupancy. Traders claim real slips through
docking_service.acquire_for_npcat trade stops (npc_trading_service.py~525β538);release_stale_trader_slips(~762β808) releases on schedule-block exit or the anti-camp tenure ceilingTRADER_SLIP_TENURE_CEILING_HOURS = 2.0(~173). π NO-CANON β the 2.0h magnitude only.
Cross-region routing and warp gates¶
Trader routes may cross region boundaries freely within the route hop budget, exactly like players. Regional starvation is monitored in tandem with roster sizing; a region in liquidation (see ../../SYSTEMS/region-lifecycle.md) still receives explicit handling.
NPC movement may traverse a player-built warp gate only when the gate owner has granted access to the relevant faction. This is a per-faction permission surface on WarpTunnel, and the default is no access β player infrastructure stays a player advantage unless the owner deliberately opens it.
β Shipped. Cross-region routing within the hop budget (
services/npc_movement_service.py) and the per-faction warp-gate access-grant model onWarpTunnel(default-deny) are both live β reads via_npc_gate_access_granted(npc_movement_service.py:69), the write-sidenpc_factionsgrant validated bywarp_gate_service._validate_npc_factions(warp_gate_service.py:2373). (status corrected 2026-08-07, was stale β this was previously drifted fromwarp-gates.md:269's own "β Shipped" note for the same underlying mechanism)
Notoriety¶
Each trader carries a scruples axis, notoriety (0β100), that decides whether attacking it is a crime or a public service.
| Band | Notoriety | Reputation | Lawful target? |
|---|---|---|---|
| Reputable | 0β24 | reputable merchant | No |
| Standard | 25β49 | standard trader | No |
| Unscrupulous | 50β74 | unscrupulous operator | Yes |
| Notorious | 75β100 | notorious | Yes |
The lawful-target threshold is notoriety β₯ 50. Spawn distribution across the bands is approximately 50 / 25 / 17 / 8 (reputable / standard / unscrupulous / notorious).
Consequences of a kill fire on the kill, not on the act (matching PvP):
- Killing a reputable or standard trader (notoriety < 50) fires
attack_innocentβ a β100 personal reputation penalty per../gameplay/police-forces.md(offense triggers, ADR-0042). - Killing a trader also carries faction consequences: killing a faction-affiliated or station-buyer-aligned trader costs standing with that faction. Lawful targets are exempt.
- Killing a notorious trader (notoriety β₯ 50) yields a positive incentive β a kill-notorious reward of +25 reputation (bounty / faction approval), not merely the absence of penalty.
Notoriety drifts dynamically: a trader caught smuggling rises toward notorious, while honest trade decays it back toward reputable. A trader's standing is therefore earned by behaviour, not fixed at spawn.
β Shipped. The
notorietyscruples axis (models/npc_character.py:notoriety), the β₯ 50 lawful-target threshold, the band definitions and spawn distribution, and theattack_innocentβ100 penalty on killing a below-threshold trader are in place. Dynamic notoriety drift is also shipped β_drift_notoriety(npc_trading_service.py:345) raises notoriety on illicit activity and decays it on honest trade, block-gated. The kill-notorious reward is shipped too βNOTORIOUS_TRADER_KILL_REWARD = 25(combat_service.py:1391-1420). π NO-CANON β the +25 magnitude is flagged in the code's own comment as a placeholder pending balance-tuning; only the value, not the mechanism, is unsettled.
Tunables: canon vs. gameserver¶
Player-visible numbers are canon. Anything a player experiences directly β notably the pirate same-identity respawn cooldown (players see the re-appearance) and the runtime tuning values surfaced in-client β is canon and player-visible.
Internal route and demand math is gameserver-tunable, not canon and not region-owner-tunable. This includes ROUTE_HOP_BUDGET, SURPLUS_RATIO / DEFICIT_RATIO, the DEMAND_SCORE bounds, the trader wallet seed amount, and the supply-delivery thresholds. Keeping these off-canon avoids difficulty-cheese (region owners cannot tune the economy in their favour) and lets the still-evolving delivery model be re-tuned without a canon edit.
π§ Partial. The internal route/demand/seed/threshold constants are gameserver-tunable today (
services/npc_trading_service.py,services/npc_spawn_service.py); surfacing the player-visible runtime tuning values in-client is the launch target.
Player-facing affordances¶
Verified vs Sectorwars2102 origin/feat 5969c9e5. No dedicated βNPC trader panelβ exists β do not invent one.
- β
Sector presence β TRADER-class NPC hulls appear in the flight windshield (
WindshieldTableau.tsxpassesnotorietyintoshipFaction) and are graded on the system view (SolarSystemViewscreen.tsx:331-361notoriety bands). - β
Tactical targeting / fair-game β
TacticalTargetPage.tsx:77-78buckets lawful targets asHOSTILE_RAIDERornotoriety>=50(same threshold as combat). - β
Annunciator contacts β sector NPC contacts carry archetype + notoriety;
useAnnunciatorState.ts+contactClassification.ts(tests citeFREIGHT_HAULER+ notoriety). LAW lamp is law archetypes, not traders. - β
Market demand signal (indirect) β
TradingInterface.tsx:32-34documents ADR-0062 E-V4: NPC trader activity feeds a separate field and must not skew the player-trade demand indicator. There is no trader-roster UI. - β
Visible supply-delivery restock spawn β shipped on tip
5969c9e5(LEG-418 / PR #702); low-stock station spawns a visible TRADER haul-in vianpc_trading_service.py/npc_tick_loops.py. - π§ In-client GS tunables β
ROUTE_HOP_BUDGET/ demand ratios / seed amounts stay gameserver-only; Β§ Tunables launch-target (surfacing those constants in-client) is not shipped.
Source map¶
| Concern | Target path |
|---|---|
| Flight windshield trader hulls | services/player-client/src/components/tactical/WindshieldTableau.tsx (shipFaction + notoriety) |
| System-view notoriety coloring | services/player-client/src/components/tactical/SolarSystemViewscreen.tsx (shipFaction, ~331-361) |
| Fair-game targeting | services/player-client/src/components/tactical/pages/TacticalTargetPage.tsx |
| Annunciator / contact class | services/player-client/src/components/hud/useAnnunciatorState.ts; β¦/tactical/contactClassification.ts |
| Indirect demand comment | services/player-client/src/components/trading/TradingInterface.tsx |
Cross-references¶
./trading.mdβ station classes, dynamic pricing, and the transaction flow shared with players../port-ownership.mdβ station tariffs and port-takeover hostility that apply to trader trades../docking-slips.mdβ the finite-slip model traders compete for.../../SYSTEMS/region-lifecycle.mdβ region liquidation handling for cross-region routes.../gameplay/factions-and-teams.mdβ faction reputation affected by trader kills and warp-gate access grants.../gameplay/police-forces.mdβ theattack_innocentoffense trigger fired on killing a below-threshold trader.../../SYSTEMS/npc-lifecycle.mdβ the TRADER archetype and lifecycle.../../SYSTEMS/npc-scheduler.mdβ roster spawning and route stepping.../../DATA_MODELS/βnpc_characters,enhanced_market_transactions, andWarpTunnelschema.- ADR-0042 β police engagement and the
attack_innocentoffense trigger. - ADR-0062 β economy tariffs and contracts.
- ADR-0063 β NPC scheduler lifecycle.