Skip to content

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_production thin-floor dial-down is βœ… tip-shipped on 46bce720 via PR #704 / board #1141 (TICK_PRODUCTION_THIN_FLOOR_RATIO=0.15). (re-verified 2026-08-21 vs Sectorwars2102 46bce720.)

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 credits wallet (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 on 5969c9e5 (LEG-418 / PR #702). Thin-floor dial-down is βœ… Shipped on live tip 46bce720.

  • βœ… 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 via run_trade_stop.
  • βœ… tick_production thin floor dial-down β€” tip-shipped LEG-423 / PR #704 / board #1141 on 46bce720 β€” TICK_PRODUCTION_THIN_FLOOR_RATIO=0.15 caps 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_id nullable FK β€” the canonical NPC attribution column. Future actor types add their own nullable FK; the player_id index 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 of player_id / npc_id is 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 (via realize_region_tax) β€” full player/NPC parity, plus station-owner tax via _station_tax_rate / _realize_station_tax (same 40/30/30 realize_port_revenue split as players).

βœ… Shipped β€” hostility. Port-takeover hostility from trader trades accrues into a separate, capped NPC sub-ledger (npc_takeover_hostility on Station.ownership via _accrue_npc_hostility, npc_trading_service.py ~441–459 / ~721–722). Structural isolation: NPC trades write player_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_CAP magnitudes only.

βœ… Shipped β€” slip occupancy. Traders claim real slips through docking_service.acquire_for_npc at trade stops (npc_trading_service.py ~525–538); release_stale_trader_slips (~762–808) releases on schedule-block exit or the anti-camp tenure ceiling TRADER_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 on WarpTunnel (default-deny) are both live β€” reads via _npc_gate_access_granted (npc_movement_service.py:69), the write-side npc_factions grant validated by warp_gate_service._validate_npc_factions (warp_gate_service.py:2373). (status corrected 2026-08-07, was stale β€” this was previously drifted from warp-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 notoriety scruples axis (models/npc_character.py:notoriety), the β‰₯ 50 lawful-target threshold, the band definitions and spawn distribution, and the attack_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.tsx passes notoriety into shipFaction) and are graded on the system view (SolarSystemViewscreen.tsx:331-361 notoriety bands).
  • βœ… Tactical targeting / fair-game β€” TacticalTargetPage.tsx:77-78 buckets lawful targets as HOSTILE_RAIDER or notoriety>=50 (same threshold as combat).
  • βœ… Annunciator contacts β€” sector NPC contacts carry archetype + notoriety; useAnnunciatorState.ts + contactClassification.ts (tests cite FREIGHT_HAULER + notoriety). LAW lamp is law archetypes, not traders.
  • βœ… Market demand signal (indirect) β€” TradingInterface.tsx:32-34 documents 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 via npc_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