NPCs¶
Status: π§ Partial β NPCCharacter, NPCRoster, NPCDeathLog models exist with most fields; lodging FKs (home_barracks_id/home_outlaw_base_id) are explicitly deferred and the NPCBarracks/OutlawBase tables do not exist β¦ Β· β οΈ contains codeβspec divergence (impl audit 2026-06-16)
Persistent named characters that populate the universe β Federation Marshals, Nexus Sentinels, pirate captains, faction leaders, station masters, contract issuers, NPC traders, Nova researchers, civilian background population. Each NPC is a real persistent individual with single-place presence (one sector at a time), a backstory, a faction allegiance, a daily schedule (sleep / commute / work / off-duty), a career arc (recruit β active β senior β decorated β retired/KIA β succession), and an identity that survives across encounters.
The runtime that moves NPCs along their schedules and processes lifecycle transitions lives in ../SYSTEMS/npc-scheduler.md (movement, KIA, engagement routing) and ../SYSTEMS/npc-lifecycle.md (the wider framework β schedules, careers, succession, archetypes). This doc covers the data model.
The design replaces the legacy "anonymous squad slots" treatment where Sector.defenses.patrol_ships carried only faction_code + squad_kind + ship_count. NPCs are now first-class persistent entities; patrol squad records reference specific NPC rows by ID. NPCs aren't 24/7 automata β they live full days like players do, with sleep / off-duty cycles that mean a Marshal isn't always at her patrol post.
Schema status¶
Per ADR-0066 D-V1, schema-level implementation status is consolidated here. Field descriptions describe the target schema.
Design-only:
- All four entities on this page (
NPCCharacter,NPCDeathLog,NPCRoster, plus theroster_targetsreference table) β model classes target paths that do not yet exist; the schemas below describe the launch target. NPCCharacter.npc_social_affinityis operator-tunable future-work.
NPCCharacter¶
Source: services/gameserver/src/models/npc_character.py.
Purpose: Single source of truth for a named NPC's identity, faction allegiance, current location, status, and assigned ship. One row per character; the NPC's identity persists across encounters, deaths, reassignments, and admin tooling.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| name | String(100) | not null | Player-visible name (e.g., "Marshal Cassandra Vance"). Non-unique; multiple NPCs can share a name. |
| title | String(50) | nullable | Display title / rank (e.g., "Marshal", "Sentinel-Captain", "Pirate-Lord"). Rendered before the name in player-facing UI. |
| faction_code | String(50) | not null, indexed | Matches Faction.code. Pirates and Cabal NPCs use a sentinel faction code (pirates / cabal) since those entities are hostile-only and not in the standing reputation list. |
| home_region_id | UUID FK regions.id | not null, CASCADE | The region this NPC belongs to. Federation Marshals are scoped to one player region's roster; Nexus Sentinels all home to the Central Nexus region. NPCs do not cross region boundaries on their own β re-assignment is admin-driven. |
| current_sector_id | Integer | nullable | The sector the NPC is in right now. NULL = sleeping / on personal time / between assignments / deceased. The compound canonical identifier is (home_region_id, current_sector_id) β the integer matches Sector.sector_number within home_region_id. Single-presence invariant: current_sector_id may only reference a sector belonging to home_region_id; never cross-region except during reassigned lifecycle transitions. |
| current_activity | Enum npc_activity |
default sleep |
What the NPC is doing right now β derived from daily_schedule + overrides; cached for fast reads. Vocabulary: sleep / commute / patrol / work_station / socialize / dine / train / personal / raid / survey / engaged / reassigned. See ../SYSTEMS/npc-lifecycle.md#activities. |
| archetype | Enum npc_archetype |
not null | One of nine archetypes: LAW_ENFORCEMENT, FACTION_PATROL, HOSTILE_RAIDER, FACTION_LEADER, STATION_OFFICIAL, MISSION_GIVER, TRADER, RESEARCHER, CIVILIAN. Drives default schedule template, default behavior tree, and engagement-routing eligibility. |
| lifecycle_stage | Enum npc_lifecycle_stage |
default recruit |
recruit (training, 7-day) / active (default) / senior (β₯90-day tenure) / decorated (medal earned) / retired (voluntary) / kia (killed) / reassigned (region transfer transient). See ../SYSTEMS/npc-lifecycle.md#lifecycle-states. |
| status | Enum npc_status |
default on_duty |
Legacy short-form duty marker. on_duty / off_duty / engaged / kia / retired / reassigned. Largely supplanted by current_activity + lifecycle_stage; kept as a denormalized fast-read for engagement-routing queries. |
| ship_id | UUID FK ships.id | nullable | The ship this NPC pilots. 1:1 β an NPC pilots exactly one ship; that ship has the NPC as its sole pilot. NULL means "between ships" (typically during a respawn cooldown or while on personal/sleep activities). The ship must be an is_npc_only hull (e.g., NPC_MARSHAL_INTERDICTOR) for LAW_ENFORCEMENT archetype; TRADER archetype NPCs pilot standard merchant hulls; FACTION_LEADER archetype may pilot any ship including capital classes. |
| daily_schedule | JSONB | not null | The 24-hour schedule template + weekly overrides. Shape: {timezone: "UTC", shift_offset_hours: int, blocks: [{start_minute, end_minute, activity, location_type, location_ref}], weekly_overrides: [{weekday: int, blocks: [...]}]}. Generated from the archetype's default template at spawn time; mutated by promotions / reassignments. See ../SYSTEMS/npc-lifecycle.md#schedule-mechanism. |
| role_history | JSONB | default {} |
Career arc: {current_role, role_history: [{role, started_at, ended_at, reason}], promotion_eligible_at, decorations: [{medal_id, awarded_at, reason}]}. Mutated by the career processor. Player-visible in faction news feeds and NPC profile views. |
| mentor_id | UUID FK npc_characters.id | nullable | The NPC's predecessor or trainer. Set on succession (e.g., Marshal Reyna's mentor_id points at the KIA Marshal Vance she replaced). Player-visible succession lore. |
| patrol_route | JSONB | nullable | Legacy β per-character patrol pattern in the v1 design. Now superseded by daily_schedule.blocks where activity = "patrol" carries the route via location_ref. Retained nullable for migration; new NPCs leave it NULL. |
| home_barracks_id | UUID FK npc_barracks.id | nullable | The NPC's permanent lodging assignment for lawful-faction archetypes. Set at spawn from NPCRoster.default_lodging_id. NULL for HOSTILE_RAIDER (uses home_outlaw_base_id instead) and CIVILIAN (ghost presence). See ./npc-lodging.md. |
| home_outlaw_base_id | UUID FK outlaw_bases.id | nullable | Same as home_barracks_id but for HOSTILE_RAIDER archetype NPCs. Mutually exclusive with home_barracks_id. |
| shift_preempt_pending | Boolean | default false | Set when this NPC's shift end-time arrived while they were engaged in combat. The relieving NPC begins their shift but covers a "preempted" gap until this flag clears. See ../SYSTEMS/npc-lifecycle.md#engagement-preemption. |
| preempted_prior_shift_npc_id | UUID FK npc_characters.id | nullable | When a relieving NPC begins their shift while their predecessor is still engaged, this points at the predecessor. Cleared when the predecessor's combat resolves. |
| promotion_pending_at | DateTime | nullable | Set by the daily career processor when the NPC is eligible for promotion; the schedule executor applies the new role's template at the next block boundary. |
| npc_social_affinity | JSONB | default {} |
Optional allies/rivals graph: {"allies": [npc_id, β¦], "rivals": [npc_id, β¦]}. Used by the off-duty social-colocation enhancement (allies share station-sectors during socialize blocks; rivals are separated). Empty default; non-blocking for launch. |
| spawned_at | DateTime | not null | When this character first entered the universe. Persists across reassignments. |
| last_seen_at | DateTime | not null | Last sector-presence event (arrival, departure, engagement). Updated by the NPC scheduler and by combat / encounter handlers. |
| destroyed_at | DateTime | nullable | When this character was killed in combat. Set on status = kia. |
| respawn_eligible_at | DateTime | nullable | Cooldown for the NPC's role (not the same character) to be replaced by a new named NPC after KIA. The new replacement gets a different id and a different procedurally-generated name β Marshal Vance is gone forever; Marshal Reyna takes the seat. See ../SYSTEMS/npc-scheduler.md#kia-and-replacement. |
| replaced_by_id | UUID FK npc_characters.id | nullable | When this character was KIA, the new NPC that took the assignment. Forms a succession chain β players can see "Marshal Vance (KIA 2026-04-12, succeeded by Marshal Reyna)". |
| backstory | JSONB | default {} |
{"origin": str, "personality_traits": [str], "skills": [str], "voice_pack": str, "portrait_id": str} β flavor metadata. Procedurally generated at NPC creation; can be admin-edited for hand-crafted faction leaders. |
Indexes:
- (home_region_id, status) β fast scan for "all on-duty Federation Marshals in region X".
- (current_sector_id) β fast scan for "who is currently in sector Y" (paired with home_region_id for compound match).
- (faction_code, home_region_id) β fast roster lookup per region per faction.
Relationships:
- home_region β Region (FK home_region_id, CASCADE delete).
- current_sector β Sector (compound home_region_id + current_sector_id lookup; not a FK because Sector uses compound PK).
- ship β Ship (FK ship_id, SET NULL on ship destruction so the NPC stays alive even if the ship dies β ship destruction triggers an NPC-respawn flow that may give the NPC a fresh hull).
- replaced_by β NPCCharacter (self-FK).
- death_logs β NPCDeathLog (1:many β tracks every death this NPC has had; usually 0 or 1).
npc_status enum (legacy fast-read marker)¶
on_dutyβ actively performing assignment percurrent_activity. Visible incurrent_sector_id. Engagement-routing eligible.off_dutyβ taking scheduled rest, dining, or socializing.current_sector_idtypically points to a station or rest sector. Engagement-routing skips.engagedβ currently in a combat encounter. Locked from being routed to another response.engaged_pending_arrivalβ per ADR-0042 (and confirmed in ADR-0065 R-I2). NPC has accepted an engagement assignment but is still en-route. Engagement-routing skips this NPC (already assigned); fleet engagement counts include it (committed to the fight). Distinct fromengagedso the routing handler can prevent a second offense from picking the same officer before they arrive at the first.kiaβ killed in action.respawningβ same NPC identity in 15-minute respawn cooldown after KIA per ADR-0063 N-D2. Slot is vacant; engagement-routing skips. After cooldown the NPC re-enters its existing slot aton_dutywith full stats β career and reputation persist.retiredβ voluntarily removed from active duty.reassignedβ moved to a different region (transient).
duty_role (per ADR-0063 N-F1) is mutable mid-shift. When a primary Marshal goes KIA, the on-duty backup NPC's duty_role flips from backup_marshal to primary_marshal immediately (zero-gap promotion). The previously-fixed-per-shift assumption is dropped; scheduler logic reads live duty_role, not a snapshot from shift start.
The richer state is captured by current_activity + lifecycle_stage; status is the denormalized fast-read for engagement-routing's hot path. The runtime maintains coherence between the three.
npc_archetype enum¶
The ten archetypes that drive default schedule templates and behavior trees. See ../SYSTEMS/npc-lifecycle.md#npc-archetypes for the full breakdown of patterns and example NPCs per archetype.
LAW_ENFORCEMENTβ Federation Marshals, Nexus Sentinels.FACTION_PATROLβ non-police faction patrols (Federation Navy, AM enforcers, Frontier militia).STATION_SECURITYβ hired station guards (Standard / Premium tier protected stations); station-funded, station-jurisdictioned. See../FEATURES/economy/station-protection.md.HOSTILE_RAIDERβ named pirate captains, Cabal lieutenants.FACTION_LEADERβ admirals, council members, pirate lords; high-rank named NPCs at fixed locations.STATION_OFFICIALβ port masters, dock managers, contract clerks; stationary, run station business.MISSION_GIVERβ quest issuers, rep representatives.TRADERβ NPC merchant captains; full economic actors who buy and sell at market.RESEARCHERβ Nova Scientific surveyors; expedition cycles.CIVILIANβ refugees, colonists-in-transit, background population; mostly passive.
npc_lifecycle_stage enum¶
recruitβ newly spawned; in 7-day training period. Stat reductions; flavor cue ("Marshal Vance, Trainee").activeβ full duty. Dominant lifecycle stage.seniorβ tenure β₯ 90 days. Slight stat buffs. Eligible for promotion.decoratedβ medal earned. Stat buff scales with medal count.retiredβ voluntary exit. Persists in lore; cannot be reactivated.kiaβ killed in action. Permanent.reassignedβ region transfer transient.
npc_activity enum¶
The vocabulary of activities an NPC can be performing right now (driven by daily_schedule + combat overrides). See ../SYSTEMS/npc-lifecycle.md#activities for engagement-eligibility per activity.
sleepβ at home; not interactive; not in any sector room.commuteβ in transit between activities.patrolβ on assigned route, fully active; engagement-eligible (police archetypes).work_stationβ at a station performing job duties.socializeβ at a station bar / social area; off-duty but visible.dineβ taking a meal.trainβ at a faction training facility.personalβ off-grid; not visible.raidβ hostile-archetype actively raiding; engagement-eligible.surveyβ researcher scanning a nebula / anomaly.engagedβ combat in progress; suspends schedule.reassignedβ region transfer in progress.shift_handoffβ overlap window between outgoing and incoming NPCs on a shifted patrol; both NPCs visible on the route while threat-notes pass viashift_handoff_stateJSONB.shift_rerouteβ set on a relieving NPC when their predecessor is KIA mid-handoff; the relieving NPC takes coverage of the prior shift's route.error_strandedβ set when the schedule executor cannot route a commute due to disconnected warp graph; ops-only state requiring manual intervention.
NPCDeathLog¶
Source: services/gameserver/src/models/npc_death_log.py.
Region-deletion handling (per ADR-0050 SK24): rows gain a region_id_snapshot UUID column populated at row creation, and the sector_id FK becomes ON DELETE SET NULL. Survives region regeneration and termination cleanup so KIA history is preserved.
Purpose: Audit trail for named-NPC kills. Lets players see "I've defeated Marshal Vance" in their personal kill history; lets ops query "how many Federation Marshals has player X killed this month."
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| npc_id | UUID FK npc_characters.id | not null, CASCADE | The NPC that died. |
| killed_by_player_id | UUID FK players.id | nullable | The player whose action caused the kill. NULL for environmental kills (sector hazard, anomaly mishap). |
| sector_id | Integer | not null | Where the kill happened. Compound (home_region_id, sector_id) β home_region_id snapshot from the NPC at kill time. |
| home_region_id | UUID FK regions.id | not null | NPC's region at kill time, snapshot for orphan-resilience after region termination. |
| combat_log_id | UUID FK combat_logs.id | nullable | Cross-link to the combat row that resolved the kill (if combat-driven). |
| destruction_cause | Enum destruction_cause |
not null | Same enum as Ship.destruction_cause. COMBAT is overwhelmingly common; HAZARD fires for environmental deaths. |
| killed_at | DateTime | not null | Timestamp. |
Indexes:
- (killed_by_player_id, killed_at DESC) β player-side kill history.
- (home_region_id, killed_at DESC) β per-region death audit.
NPCRoster¶
Source: services/gameserver/src/models/npc_roster.py.
Purpose: Per-region per-faction roster targets. Tells the NPC scheduler "this region should have N active Federation Marshals; if fewer, spawn replacements." Decoupled from the actual NPC rows so admins can rebalance roster sizes without touching NPCs directly.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| region_id | UUID FK regions.id | not null, CASCADE | The region this roster targets. Central Nexus is one region; each player region is its own. |
| faction_code | String(50) | not null | Matches Faction.code. |
| role | String(50) | not null | Role within the faction. Each role binds to one of the nine npc_archetype values via the default_archetype field. Examples: marshal (LAW_ENFORCEMENT), sentinel (LAW_ENFORCEMENT), pirate_captain (HOSTILE_RAIDER), faction_leader (FACTION_LEADER), station_master (STATION_OFFICIAL), contract_issuer (MISSION_GIVER), enforcer (FACTION_PATROL), merchant_captain (TRADER), nebula_surveyor (RESEARCHER). |
| default_archetype | Enum npc_archetype |
not null | The archetype this role spawns NPCs as. Drives the default daily_schedule template and behavior_tree_id at spawn. |
| schedule_template | JSONB | not null | The daily_schedule template applied at NPC creation. Per-NPC offsets are added at spawn time so different NPCs in the same role run at staggered shifts. |
| default_lodging_id | UUID | nullable | UUID of the canonical lodging entity (NPCBarracks or OutlawBase) for this role. Loop B reads this at NPC spawn and sets the new NPC's home_barracks_id or home_outlaw_base_id. Generator Phase 12.5 sets this. |
| default_lodging_type | Enum npc_lodging_type |
not null | barracks or outlaw_base β disambiguates which table default_lodging_id references. |
| target_count | Integer | not null | How many active NPCs of this role the region should have. The scheduler maintains the roster against this target. |
| name_pool | JSONB | default {} |
Procedural-generation seed and constraints for new NPC names: {"first_names": [str], "surnames": [str], "title_template": str}. Admins can hand-curate name pools per faction for flavor (e.g., Cabal lieutenants get sinister-sounding name pools). |
Indexes:
- (region_id, faction_code, role) UNIQUE β exactly one roster target per (region, faction, role) tuple.
Roster targets at launch¶
Reference targets (operator-tunable):
| Region kind | Faction | Role | Target count |
|---|---|---|---|
| Central Nexus | Galactic Concord | sentinel | 24 |
| Central Nexus | Galactic Concord | sentinel_captain | 4 |
| Player-owned region | Terran Federation | marshal | 8 |
| Player-owned region | Terran Federation | marshal_captain | 1 |
| Player-owned region | Pirates | pirate_captain | 2 |
| Player-owned region | Pirates | enforcer | 4β6 |
| Terran Space | Terran Federation | marshal | 12 |
| Terran Space | Terran Federation | marshal_captain | 2 |
| Terran Space | The Cabal | cabal_lieutenant | 1 |
A player region with 8 Federation Marshals + 1 Captain means a player traversing the region's Federation Zone could meet any of 9 named officers depending on which sector they enter. Re-encounters with the same officer build a real relationship. Killing an officer leaves the roster short-handed for the cooldown period before a new named officer takes the seat.
Patrol-squad linkage¶
The existing Sector.defenses.patrol_ships JSONB is extended, not replaced. The shape gains an npc_character_ids array; the legacy ship_count field becomes derived (len(npc_character_ids)) but is retained for read-fast denormalization.
{
"patrol_id": "<uuid>",
"faction_code": "terran_federation",
"squad_kind": "federation_marshal",
"npc_character_ids": ["<npc-uuid-1>", "<npc-uuid-2>", "<npc-uuid-3>"],
"ship_count": 3,
"wanted_threshold": -500,
"deployed_at": "<iso8601>",
"scheduled_clear_at": null
}
When npc_character_ids is set, every NPC in the array has current_sector_id matching the row's parent sector β the JSONB row and the per-NPC location are always coherent. The scheduler enforces this invariant on every NPC-move event.
A patrol squad is not a separate persistent entity β when the NPCs leave (off-duty rotation, response routing elsewhere, KIA), the squad row is deleted and the per-NPC current_sector_id updates point elsewhere.
Schema-level invariants¶
- Single-presence:
NPCCharacter.current_sector_idreferences at most one sector at any time. The runtime scheduler enforces this on every move; a transactional update changes the row atomically. - Single-pilot:
NPCCharacter.ship_idis 1:1. No NPC pilots two ships; no ship has two NPC pilots. (Hangared player ships inside an NPC Carrier are not "piloted by" the NPC β the hangar payload is separate.) - Region-scoped identity: An NPC's
home_region_idplusnameis not unique across the universe β Marshal Vance can exist in Region A and Region B independently. They're different individuals. - Roster targets vs. live count: The scheduler tries to keep
count(NPCCharacter where region+faction+role = roster_target.* AND status in ['on_duty', 'off_duty', 'engaged']) β₯ roster_target.target_count. KIA / retired / reassigned NPCs don't count.
Cross-references¶
../SYSTEMS/npc-scheduler.mdβ runtime scheduler that moves NPCs, routes engagement responses, and respawns after KIA.../FEATURES/gameplay/police-forces.mdβ Federation Police and Nexus Sentinel design that consumes this schema.../FEATURES/gameplay/faction-lore.mdβ faction lore that informs name pools and personality traits../jsonb-schema.mdβSector.defenses.patrol_shipsextended withnpc_character_idsarray../galaxy.mdβ Region / Sector compound identity that NPCs inherit../ships.mdβShip.is_npc_onlyflag that gates NPC-only hull classes.