Skip to content

NPC Lifecycle

Status: 🚧 Partial — Lifecycle has a real spine — archetype/stage/activity enums, daily_schedule execution (Loop A), recruit->active, KIA+NPCDeathLog+succession … (impl audit 2026-06-16)

The unified framework that gives every named NPC in the universe a full-life workflow — patrol shifts, off-duty rotations, sleep cycles, career progression, retirement, succession. NPCs aren't 24/7 automata; they live full days like players do. They wake, commute, work, take meals, socialize, retire to rest, and (eventually) hand off their role to a successor when they retire or are killed in action.

This doc covers the lifecycle layer that sits above the npc-scheduler. The scheduler executes the lifecycle's plans — moving NPCs along their schedules, processing role transitions, and applying state changes. The data model is in ../DATA_MODELS/npcs.md. Specific NPC archetypes (police, faction patrols, station officials, traders) are listed below; archetype-specific behavior lives in their own feature docs (e.g., ../FEATURES/gameplay/police-forces.md for Marshals and Sentinels).

Design intent

A persistent named NPC should feel like a real person whose life intersects with the player's at specific times and places. That means:

  • Schedules. A Federation Marshal works an 8-hour patrol shift, then goes off-duty, then sleeps. They aren't always at the same sector waiting to engage you — and they aren't anywhere at all during their off-hours.
  • Careers. A Marshal can be a recruit who learns the ropes, an active officer who builds reputation, a senior officer who mentors recruits, and eventually a Captain or a retired veteran. The role they hold today isn't necessarily the role they'll hold in three months.
  • Succession. When Marshal Cassandra Vance is killed in action, her seat doesn't immediately fill with a faceless replacement. There's a recruitment delay, a successor steps in with her own name and personality, and Vance becomes part of the region's history (player-visible KIA log; her successor may even be named "Marshal Reyna, Vance's apprentice").
  • Generality. This framework doesn't apply only to police. Pirate captains have schedules (raiding hours vs. drinking-at-the-Fringe-port hours), faction leaders have career arcs (Admiral Chen retires to a Council seat), station masters have shift rotations. The lifecycle is the canonical mechanism for any persistent named NPC.

NPC archetypes

The universe carries NPCs across ten archetypes, each with default behavior templates:

Archetype Examples Schedule pattern Movement pattern
LAW_ENFORCEMENT Federation Marshal, Nexus Sentinel 8h patrol / 8h off-duty / 8h sleep Patrol cycle through assigned sectors
FACTION_PATROL Federation Navy, AM enforcer, Frontier militia 8h patrol / 8h off-duty / 8h sleep Patrol cycle within faction territory
STATION_SECURITY Hired station guards, Guard-Captains 12h shift / 12h off-station (1h handoff overlap) Patrol the station's docking ring + host sector + 1-hop ring
HOSTILE_RAIDER Named pirate captain, Cabal lieutenant Variable raiding hours; off-duty at home base Random or themed (raids high-traffic sectors during peak hours)
FACTION_LEADER Admiral Chen, Council member, Pirate Lord Mostly stationary at HQ sector; ceremonial appearances elsewhere Rare moves; significant when they happen
STATION_OFFICIAL Station master, dock manager, contract clerk 12h shift / 12h off-station Stationary at one station; off-duty location nearby
MISSION_GIVER Quest issuer, faction-rep representative Office hours (e.g., 09:00–17:00 station-time) Stationary; players visit them
TRADER NPC merchant captain Travel route between 2–4 stations on a schedule Multi-day route with rest stops
RESEARCHER Nova Scientific surveyor Field expedition cycles (8h scanning / 4h analysis / 12h rest) Cycles through nebula clusters
CIVILIAN Refugee, colonist-in-transit, background population Schedule varies; often passive / non-interactive Slow drift through fed/border zones

STATION_SECURITY is distinct from LAW_ENFORCEMENT and FACTION_PATROL in two important ways: (1) station-funded — paid by the host station's defense budget, not the faction treasury; (2) station-jurisdictioned — patrols the host sector + 1-hop ring only, not a regional route. Used by hired security guards at Standard- and Premium-tier protected stations. See ../FEATURES/economy/station-protection.md.

Each archetype defines default schedule templates and default behavior trees. A specific NPC's schedule is derived from the archetype template plus per-NPC offsets (so 8 Marshals don't all sleep at the same time — staggered shifts give continuous coverage).

The NPCCharacter.archetype enum carries the value; the per-archetype default templates live in the NPCRoster.config JSONB and can be operator-tuned per region per archetype per role.

Schedule mechanism

Every NPC carries a daily_schedule JSONB on the NPCCharacter row (per the schema in ../DATA_MODELS/npcs.md, extended for the lifecycle layer):

{
  "timezone": "UTC",
  "shift_offset_hours": 0,
  "blocks": [
    {
      "start_minute": 0,
      "end_minute": 480,
      "activity": "sleep",
      "location_type": "home_sector",
      "location_ref": "<sector_number_in_home_region>"
    },
    {
      "start_minute": 480,
      "end_minute": 510,
      "activity": "commute",
      "location_type": "transit",
      "location_ref": null
    },
    {
      "start_minute": 510,
      "end_minute": 990,
      "activity": "patrol",
      "location_type": "patrol_route",
      "location_ref": "<route_id>"
    },
    {
      "start_minute": 990,
      "end_minute": 1020,
      "activity": "commute",
      "location_type": "transit",
      "location_ref": null
    },
    {
      "start_minute": 1020,
      "end_minute": 1320,
      "activity": "socialize",
      "location_type": "station",
      "location_ref": "<station_id>"
    },
    {
      "start_minute": 1320,
      "end_minute": 1440,
      "activity": "personal",
      "location_type": "home_sector",
      "location_ref": "<sector_number_in_home_region>"
    }
  ]
}

start_minute and end_minute are minute-of-day in UTC. shift_offset_hours is added to the canonical archetype template so different NPCs in the same role run at different wall-clock times — Marshal Vance's 06:00 patrol start aligns with another Marshal's 14:00 patrol start, providing continuous coverage with handoff.

Activities

The canonical activity vocabulary (extends as new archetypes are added):

  • sleep — NPC is at their home sector / barracks; not interactive; not in any sector room. Other NPCs and players don't see them. Engagement-routing skips sleeping NPCs.
  • commute — NPC is in transit between activities. Visible in any sector they pass through; not engagement-eligible.
  • patrol — NPC is on assigned route, fully active. Engagement-eligible (e.g., Marshal pursues Wanted players).
  • work_station — NPC is at a station performing job duties (issuing contracts, running market trades, etc.). Visible to players who dock; not engagement-eligible (mostly).
  • socialize — NPC is at a station or sector socializing (off-duty Marshals at the bar, traders comparing notes). Visible; can be interacted with for flavor dialog; not engagement-eligible.
  • dine — NPC is taking a meal at a station mess hall or on their ship. Not engagement-eligible.
  • train — NPC is at a faction training facility honing skills. Not engagement-eligible.
  • personal — NPC is performing private business (family, hobbies). Off-grid; not visible.
  • raid — Hostile-archetype NPC is actively raiding. Engagement-eligible by definition (they engage anyone they encounter).
  • survey — Researcher archetype is scanning a nebula / anomaly. Engagement-eligible only if Wanted (research is non-violent).
  • engaged — Override state set by the scheduler when the NPC enters combat. Suspends the schedule until combat resolves.
  • reassigned — Override state during a region transfer; NPC is in transit to a new home region.

Schedule scopes

Three nested schedule scopes:

  1. Daily template (the JSONB above). 24-hour cycle.
  2. Weekly variations. A Marshal may have one rest day per week (no patrol; full off-duty). The daily_schedule carries a weekly_overrides array indexed by ISO weekday: {"weekday": 0, "blocks": […]} (where 0 = Monday).
  3. Career-event overrides. Promotions, retirements, KIA, and special assignments override the schedule for a defined period. These are stored as NPCScheduleOverride rows with start/end timestamps.

Schedule timezone

All schedules are UTC by default. A future expansion (📐 Design-only) supports per-region "local time" anchored to the region's timezone_offset config (so a Frontier-Coalition region might run on a different shift cadence than Federation regions). At launch, every NPC schedule is UTC.

Lifecycle states

Each NPC carries a lifecycle_stage enum on NPCCharacter:

  • recruit — Newly spawned; in training. Lasts 7 real-time days (📐 tunable per archetype). Stats reduced (combat effectiveness, scanner range, etc.); flavor cue ("Marshal Vance, Trainee" in display).
  • active — Full duty. The dominant lifecycle stage; most NPCs are here.
  • senior — Tenure ≥ 90 real-time days. Slight stat buffs (combat +5%, scanner range +1). Flavor cue ("Senior Marshal Vance"). Eligible for promotion to a higher role (e.g., Marshal-Captain).
  • decorated — Earned an in-faction medal (e.g., killed N Wanted players, completed N successful patrols). Stat buff scales with medal count. Flavor cue ("Captain Vance, Decorated").
  • retired — Voluntarily left active duty (administrative or story-driven). Persists in lore and KIA logs but does not appear in any sector. Cannot be reactivated (a returning veteran is a new NPC with reference to the retired predecessor).
  • kia — Killed in action. Same persistence rules as retired but distinguishable in audit; the kill is logged in NPCDeathLog with killed_by_player_id set.
  • reassigned — Transferred to a different region. Brief transient state during the transfer; flips back to active at arrival.

Stage transitions are driven by the scheduler:

  • recruitactive — automatic at end of training period (7 days post-spawn).
  • activesenior — automatic at 90 days tenure.
  • active / seniordecorated — on medal earned (combat stats, mission completions).
  • active / senior / decoratedretired — operator-initiated or story-event-driven (📐).
  • Any → kia — combat resolver fires _handle_npc_kia.
  • activereassigned — operator admin action.

Career progression

NPCs can hold multiple roles over their lifetime. Career history lives in the role_history JSONB on NPCCharacter:

{
  "current_role": "marshal",
  "role_history": [
    {"role": "marshal_recruit", "started_at": "2026-04-01T00:00:00Z", "ended_at": "2026-04-08T00:00:00Z", "reason": "training_complete"},
    {"role": "marshal", "started_at": "2026-04-08T00:00:00Z", "ended_at": null, "reason": "active"}
  ],
  "promotion_eligible_at": "2026-07-08T00:00:00Z",
  "decorations": [
    {"medal_id": "<uuid>", "awarded_at": "2026-05-15T...", "reason": "100_wanted_kills"}
  ]
}

Promotion rules (📐 Design-only, archetype-specific): - Marshal → Marshal-Captain: 90+ days tenure, 50+ successful pursuits, no KIA marks against them. The senior Marshal is promoted when a Captain seat opens (KIA, retirement, transfer). - Captain → Regional Director: 1+ year tenure, no major incidents. Operator-tunable. - Trader → Senior Trader: 100+ successful trade routes, established faction reputation.

Retirement rules (📐): - Voluntary at any tenure ≥ 1 year (operator-tunable per archetype). - Forced retirement at 5 years tenure for archetypes with hard caps (e.g., field NPCs whose ship class is being phased out).

When an NPC is promoted to a new role, the role's archetype may change (e.g., Senior Marshal stays LAW_ENFORCEMENT; promotion to Federation Council member shifts to FACTION_LEADER). The schedule is regenerated from the new role's archetype template; the new role's stats and behavior tree apply.

Succession on death / retirement

When an NPC vacates their seat (KIA or retirement), the NPCRoster target slot becomes empty. The scheduler's Loop B (roster maintenance) detects this and spawns a successor:

  • The new NPC's name is drawn from the NPCRoster.name_pool.
  • The new NPC starts at lifecycle_stage = recruit with a 7-day training period.
  • The new NPC's replaced_by_id link points at them from the predecessor's row, forming a succession chain visible in player UI.
  • Optional: the new NPC's backstory.mentor_id references their predecessor (especially for KIA — "Marshal Reyna, who took up Vance's badge after the Hephaestus Incident").

For high-tier roles (Marshal-Captain, Faction Leader), succession may go through internal promotion instead of new-recruit-spawn: a Senior Marshal is promoted to Captain, and a fresh recruit fills the Marshal seat. This is the "internal succession" path; the alternative "fresh succession" path is for archetypes with no internal promotion candidates (most Civilian and Trader archetypes).

Respawn vs recruit-stage timers

Per ADR-0063 N-D2, three timers/states are distinct:

  • Respawn cooldown — same NPC identity returns. 15 minutes. Applies when the NPC's archetype permits respawn (most named pirates, some trader archetypes). NPCCharacter.status = respawning during the cooldown; the slot is vacant. After cooldown the NPC re-enters its existing slot at full stats — career and reputation persist.
  • Recruit lifecycle stage — fresh NPC entering the roster after KIA-without-respawn. 7 days at reduced stats (existing rule). The recruit is engagement-eligible during this window — they're a learner on duty, not a benchwarmer. Reduced combat effectiveness + scanner range; "Trainee" flavor cue in display.
  • train activity block — within a single day's schedule, the NPC may have a train block at a training facility. During that block engagement_eligible = false for that NPC (regardless of lifecycle stage). Active veterans may have train blocks too.

The respawn-cooldown and recruit-stage timers are independent. Slot quorum counts the slot as filled the moment a recruit is spawned — even though they're at reduced effectiveness.

Barracks advisory and hard caps

Per ADR-0063 N-F4, barracks have two cap thresholds:

  • Advisory cap — soft limit per barracks. Recruits added past advisory cap enter delayed_train status: they exist in the roster but their lifecycle-stage timer is paused until an active slot frees up.
  • Hard cap = 2 × advisory cap. Attempts to add a recruit past hard cap reject with ERR_BARRACKS_FULL. Faction recruit-generation logic stops queueing for this barracks until the queue drains.

Per-class defaults:

Barracks class Advisory cap Hard cap (computed)
Class-1 Frontier post 4 8
Class-3 Garrison 8 16
Class-5 Mega-barracks 24 48

Operators tune advisory_cap per-barracks; hard_cap is computed at read-time as advisory_cap × 2 and is non-configurable (the 2× ratio is a deliberate ceiling against unbounded queue depth).

Genocide rapid-recovery

Per ADR-0063 N-V4, coordinated Marshal-tier kills trigger faction-led rapid recovery without a region-wide debuff:

  • Trigger: ≥3 Marshal-tier NPC kills within a 30-minute window in the same region.
  • Response: faction recruit-generation rate doubles for 1 hour in the affected region. Recruits spawned during the rapid-recovery window reach active stage at half the normal duration (3.5 days instead of 7).
  • No region-wide debuff: patrol response, faction-rep grants, and policing all continue at normal rates. The region recovers naturally; players don't suffer ambient penalty for the wipe.

ARIA narrates: "Heavy losses among regional authorities — faction reinforcements arriving in waves." A npc.coordinated_genocide_detected realtime event logs to the operator dashboard, giving ops an early signal of coordinated griefing patterns even without an automatic gameplay penalty. If griefing patterns later prove the rapid-recovery flood insufficient, a follow-up ADR can promote the response to a region-state change without changing the detection signal.

Social affinity / rivals graph (future)

📐 Design-only future work. A future iteration will add an NPCRelationship table for affinity / rivalry / mentor relationships between NPCs in the same faction or sector. Effects could include morale modifiers when NPCs work alongside friends or rivals, narrative beats when a rival NPC is killed, and ARIA-narrated faction politics. Not Launch-blocking.

Player-like workflows

NPCs perform real workflows that interact with player-visible game state. The lifecycle layer enables this; specific behaviors are archetype-defined.

Movement

NPCs move between sectors using the warp graph the player uses. The scheduler picks routes based on the activity (patrol cycle, commute home, raid run) and pays the same WarpTunnel.turn_cost players pay. NPCs don't have player turn pools — the scheduler is the authoritative actor — but the time a movement takes is honored: a 3-turn long tunnel on a Marshal's commute means the Marshal isn't visible at either endpoint for ~4 minutes wall-clock at L1-equivalent regen pacing. The schedule's commute blocks budget time for this realistically.

Cross-region movement is rare and reserved for reassigned lifecycle transitions. A Marshal doesn't normally leave their home region; a Trader does, regularly, on their multi-region trade route.

Combat

NPCs use their assigned ship and engage per the combat-resolver pipeline. KIA fires the standard destruction handler with a special branch (_handle_npc_kia) that updates the lifecycle layer. NPCs can flee, surrender (📐 — high-stage decorated NPCs may surrender to higher-rank attackers under specific conditions), or be destroyed.

Trade

TRADER archetype NPCs literally trade at stations — buy commodities at one station, transit to another, sell at higher prices. The market-pricing service treats their transactions identically to player transactions (they affect supply/price). This means traders can be intercepted, robbed, or out-bid by players — they're full economic actors.

Contracts

MISSION_GIVER archetype NPCs issue contracts visible to players. The Contract.issuer_npc_id field (📐 Design-only — extension to the contract schema) references the named NPC. Players can develop relationships with specific issuers (e.g., "Quartermaster Hara always pays well; she's at Station Prime in sector 247 from 09:00 to 17:00 UTC").

Faction interactions

NPCs honor faction allegiances. Federation Marshals don't engage Nova scientists. AM enforcers patrol AM-claimed sectors and engage non-licensed miners. The faction-reputation rules apply to NPC-vs-player interactions identically to NPC-vs-NPC where both sides have factional allegiance.

Schema extensions

The lifecycle layer extends NPCCharacter with the following new fields (the DATA_MODELS/npcs.md doc carries the full table):

Field Type Purpose
archetype Enum npc_archetype One of the 9 archetypes above.
lifecycle_stage Enum npc_lifecycle_stage recruit / active / senior / decorated / retired / kia / reassigned.
daily_schedule JSONB Schedule blocks; supersedes the simpler patrol_route from the original v1.
role_history JSONB Career arc; current_role, role_history array, promotion_eligible_at, decorations.
mentor_id UUID FK npc_characters.id, nullable The predecessor or trainer. Player-visible succession lore.
current_activity Enum npc_activity Live-state denormalization of "what is this NPC doing right now" — derived from schedule + overrides; cached for fast reads.

The legacy patrol_route field becomes a kind of schedule block (activity = patrol, location_ref = <route_id>); existing rows migrate by wrapping the route into a single patrol block in the new daily_schedule JSONB.

New runtime services

Two new services join the npc-scheduler family:

Schedule executor

Runs every 5 minutes. For each NPCCharacter where lifecycle_stage is not retired/kia:

  1. Compute current_minute_of_day_utc.
  2. Find the matching schedule block (or weekly override or career override).
  3. Compare to current_activity; if different, transition.
  4. Update current_sector_id per the new block's location_ref (or set NULL for sleep / personal).
  5. Update Sector.defenses.patrol_ships JSONB on both the prior and new sectors to keep npc_character_ids coherent.
  6. Emit appropriate realtime events (npc_off_duty, npc_began_patrol, npc_arrived_home).

Career processor

Runs daily. For each NPCCharacter:

  1. Check lifecycle_stage transitions: tenure-based (recruitactive at 7 days; activesenior at 90 days), decoration-based (medal-earned triggers).
  2. Check promotion eligibility for archetypes with internal promotion paths.
  3. If eligible and a higher role's seat is open, fire promotion event: update role_history, change archetype if applicable, regenerate daily_schedule from the new role's template.
  4. Emit npc_promoted realtime event (visible in faction news feed).

Example — Federation Marshal Cassandra Vance

A worked example of a Marshal's life across one week.

Day 0 (spawn): Loop B detects an open Marshal slot in Region X. Spawns "Marshal Cassandra Vance" with archetype = LAW_ENFORCEMENT, lifecycle_stage = recruit, daily_schedule from the Federation Marshal recruit template (heavy training blocks, light patrol). current_role = marshal_recruit.

Day 7: Career processor flips lifecycle_stage = active, current_role = marshal. New schedule template: 06:00–14:00 patrol on assigned route (sectors 12, 34, 47, 89, 102, 156), 14:00–18:00 socializing at the Capital station, 18:00–22:00 dining + personal, 22:00–06:00 sleep at the Capital station's barracks.

Days 7–60: Vance executes her schedule. Players in sector 12 between 06:00 and 08:00 may encounter her. Players in sector 47 between 10:00 and 12:00 may encounter her. Players entering during her sleep block see no Marshal.

Day 30: Player attacks an innocent in sector 47 at 10:30 UTC. Schedule executor reports Vance is patrolling sector 47 right now. Engagement routing dispatches her. She wins; offender flees. Returns to schedule.

Day 75: Player kills another player on Vance's beat. Vance pursues, attacks, and is destroyed in the engagement. Scheduler fires _handle_npc_kia(vance.id, killer_player_id, sector_id). Vance's lifecycle_stage = kia, current_sector_id = NULL, destroyed_at = now, respawn_eligible_at = now + 7 days. NPCDeathLog row inserted. Squad row at sector 47 updated to remove Vance's id. Realtime broadcast: "Marshal Cassandra Vance has been killed." The killer takes a −250 Federation rep hit per police-forces.md rules.

Day 82: Loop B detects the open Marshal slot. Spawns "Marshal Astrid Reyna" with archetype = LAW_ENFORCEMENT, lifecycle_stage = recruit, mentor_id = vance.id, backstory.origin = "took up Vance's badge after the Hephaestus Incident". The succession chain is now vance.replaced_by_id = reyna.id.

Day 89: Reyna flips to active. The region is fully staffed again.

The player who killed Vance now sees, on their next patrol-encounter screen, Marshal Reyna with the lore line about Vance. The universe has a memory.

Physical realism — ship parking, lodging, and shift transitions

The schedule mechanism above defines when an NPC is doing what; this section spells out where their ship is parked, where they physically sleep, and how shifts hand over realistically. NPCs aren't 24/7 automata — they need places to put their ships, beds to sleep in, and orderly transitions when one shift ends and the next begins.

Per-archetype lodging mapping

Every NPC has a home_barracks_id FK (or home_outlaw_base_id for hostile factions) pointing at their canonical sleep location. Per archetype:

Archetype Lodging model Location
LAW_ENFORCEMENT (Marshals) Shared NPCBarracks at host station Capital station; capacity 9 (8 Marshals + 1 Captain)
LAW_ENFORCEMENT (Sentinels) Shared NPCBarracks in dedicated sector Gateway Plaza cluster, dedicated barracks sector; capacity 28
FACTION_PATROL (Federation Navy / AM enforcers / Frontier militia) Shared NPCBarracks at faction-flagged station Per-faction; capacity 12
FACTION_LEADER (Admiral, Council member, Pirate Lord) Two NPCBarracks rows — faction_hq (office) + private_residence (sleep) Each capacity 1; distinct sectors
STATION_OFFICIAL Shared NPCBarracks at the station they work at Capacity 6
MISSION_GIVER Shared NPCBarracks at the station they work at Capacity 4
TRADER On-ship lodging during transit + home-station NPCBarracks for rest days Capacity 1 (per trader)
RESEARCHER (Nova surveyor) Shared NPCBarracks at a Nova-flagged Class-11 station Capacity 6
HOSTILE_RAIDER (pirates, Cabal) Shared OutlawBase at hidden sector Capacity 12; gated by is_player_discoverable
CIVILIAN None — ghost presence; current_sector_id = NULL during sleep n/a

Lodging entities are first-class — see ../DATA_MODELS/npc-lodging.md.

Ship parking

Every NPC has a permanent 1:1 ship assignment. Ships are not shared between shifts — each NPC has their own hull for the lifetime of their lifecycle (KIA destroys both NPC and ship via the canonical destruction handler; spawn of a successor creates a new hull). This avoids the complexity of inter-shift handoff protocols at the ship level.

When the schedule executor (Loop A) transitions an NPC from active duty to off-duty / sleep:

  1. The NPC's ship Ship.status flips to DOCKED.
  2. The ship's Ship.sector_id is set to the host sector of the NPC's home_barracks (or home_outlaw_base for hostile factions).
  3. An entry is added to Sector.defenses.docked_npc_ships JSONB array: {npc_id, ship_id, status: "docked_off_duty", docked_at: now}.
  4. The combat-resolver checks docked_npc_ships: ships docked with status = "docked_off_duty" at a barracks-flagged station/sector are shielded from player attack (rejection error ERR_NPC_SHIP_AT_BARRACKS).

When the schedule transitions back to active duty:

  1. Ship.status flips to IN_SPACE.
  2. The docked_npc_ships entry is removed.
  3. The ship is back in regular sector circulation; player attacks are permitted (subject to standard combat rules).

Off-duty ship vulnerability rules. A ship docked at a public outlaw station (where pirates rest) is NOT at a barracks; the docked_npc_ships entry uses status: "docked_standby" and is not shielded — players who infiltrate a Fringe-Alliance port can attack the resting pirate's ship. Off-duty ships at Federation barracks stations are protected; off-duty ships at non-barracks docking facilities are not.

Auto-maintenance during sleep. Loop A's sleep transition queues a free auto-repair order at the host barracks (📐 Design-only): if the NPC's ship maintenance.current_integrity < 75% × max_integrity, the maintenance ticks up to 90% during the sleep block. Pirates have a 10–20% degradation multiplier — outlaws don't maintain as rigorously.

Trader cargo persistence. When a TRADER archetype NPC docks for off-duty rest, their Ship.cargo JSONB is preserved across the docking — partially-sold cargo carries over to the next active block. Players can observe the same trader with the same partial cargo across multiple sessions, enabling emergent storytelling ("I've been watching this trader's cargo for three days; they keep getting outbid").

Shift overlap and handoff briefing

Hard handoffs at exactly 14:00 lose intelligence. The design includes a 30-minute overlap window for any role that runs shifted patrol coverage:

  • Marshal Vance's 06:00–14:00 shift and Marshal Singh's 14:00–22:00 shift overlap from 13:30 to 14:00. During that 30-min window, both are simultaneously on the patrol route in the shift_handoff activity (a new value in the npc_activity enum).
  • Vance shares her threat notes with Singh via a shift_handoff_state JSONB on the squad row in Sector.defenses.patrol_ships:
{
  "shift_handoff_state": {
    "outgoing_npc_id": "<vance-uuid>",
    "incoming_npc_id": "<singh-uuid>",
    "overlap_started_at": "<iso8601>",
    "threat_notes": [
      {"player_id": "<uuid>", "sector_id": 47, "note": "encountered with Wanted Status; fled north", "timestamp": "<iso8601>"}
    ],
    "engaged_player_ids": ["<player-uuid-of-active-pursuit>"]
  }
}
  • The schedule executor reads shift_handoff_state on Singh's transition to active duty and flags him to any threats Vance passed.
  • After Vance's 14:00 transition out, the shift_handoff_state is cleared and the squad row reflects only Singh.

The schedule encoder represents overlap via a new overlap_start_minute field on each block:

{
  "start_minute": 360,
  "end_minute": 840,
  "overlap_start_minute": 810,
  "activity": "patrol",
  "location_type": "patrol_route",
  "location_ref": "<route_id>"
}

The overlap_start_minute (840 − 30 = 810) tells the executor "begin handoff 30 minutes before block end." The incoming NPC's block has a corresponding overlap_start_minute 30 min before their start_minute so they can pre-arrive.

Player-visible: realtime bus emits npc_handoff_started and npc_handoff_completed events with both NPC names; the player sees "Marshal Vance is briefing Marshal Singh on patrol" in the sector log.

Late or absent relief

If the relieving NPC isn't available at handoff time (status = engaged elsewhere, KIA, off-duty, or unspawned), the design cascades through fallback options:

  1. Cascade-hold (15 min). Outgoing NPC extends their patrol end-time by +15 min; the schedule executor recomputes downstream blocks. Vance's 14:00–22:00 off-duty becomes 14:15–22:15.
  2. Secondary recall. If still no relief at +15 min, the scheduler recalls the next-most-senior off-duty NPC of the same role from their off-duty / personal block. Their schedule is preempted; they take a 15-min commute to the patrol post.
  3. Coverage gap. If no secondary is available (everyone is engaged / KIA / on rest day), the route runs uncovered. The bus emits npc_coverage_gap (player-visible in region news). The next free NPC is dispatched when one becomes available; the offender's response-grace window per the engagement-routing fallback applies.

shift_handoff_state carries an additional coverage_gap_started_at field when the gap is active.

KIA mid-handoff

If Vance is KIA at 13:55 (in the middle of her overlap window with Singh), the destruction handler's _handle_npc_kia calls _reroute_incoming_relief(vance.id):

  1. Detect Singh is the pending relief (he's in the shift_handoff activity).
  2. Set Singh's current_activity = shift_reroute (new enum value).
  3. Teleport Singh's current_sector_id to the patrol route's start sector.
  4. Extend Singh's shift end_minute by 30 min to cover Vance's lost overlap window.
  5. Emit npc_emergency_coverage with both NPC names: "Marshal Singh assumes coverage of sectors 12–47 following Marshal Vance's KIA."
  6. The succession chain (per WR1) fires Loop B's recruit spawn at cooldown expiry.

Engagement preemption

If Vance is engaged (in combat) at her shift end-time:

  1. Set NPCCharacter.shift_preempt_pending = true.
  2. Singh's relief begins on schedule but the squad row carries preempted_prior_shift_npc_id = vance.id.
  3. When Vance's combat resolves (win, flee, or KIA), the scheduler atomically:
  4. If Vance survives: transitions her to off_duty (taking her overdue rest); clears shift_preempt_pending; clears Singh's preempted_prior_shift_npc_id.
  5. If Vance is KIA: handles the KIA flow per above.
  6. Bus emits npc_preempt_coverage_start and npc_preempt_coverage_end events.

Commute feasibility

The schedule encodes commute durations (typically 30 min in the example template), but actual sector-traversal time depends on the warp graph between source and destination sectors. Loop A validates feasibility before transitioning to a commute block:

  1. Query the warp graph for the path from current sector → destination sector.
  2. Sum WarpTunnel.turn_cost for each edge.
  3. Compute wall-clock duration at standard regen pacing (~86 sec/turn at L1 ARIA).
  4. If actual_duration > block.end_minute − block.start_minute, dynamically extend the block's end_minute and cascade downstream blocks.
  5. If route is impossible (graph disconnected, all warps one-way against the direction of travel), mark the NPC's current_activity = error_stranded and emit npc_commute_impossible to the ops bus. (This shouldn't happen in normal operation; if it does, ops manually reassigns or destroys the NPC.)

Promotion handoff

When the career processor (daily) detects promotion eligibility, it doesn't preempt the NPC mid-shift. Instead:

  1. Set NPCCharacter.promotion_pending_at = end_of_current_block.
  2. Loop A checks this on each block transition.
  3. At the next block boundary, the new role's schedule template is generated and replaces the existing daily_schedule.
  4. If the new role uses a different barracks (e.g., Senior Marshal → Marshal-Captain who lives in private quarters instead of the shared barracks), home_barracks_id is updated.
  5. Bus emits npc_promoted (public).

Reassignment travel

A region transfer is a multi-step transit. When an admin triggers a reassignment from Region A → Region B:

  1. Set NPCCharacter.lifecycle_stage = reassigned.
  2. Compute warp-graph path from current sector → destination region's home sector (typically via the Nexus).
  3. Insert a synthetic reassigned activity block with calculated duration (sum of warp tunnel turn costs translated to wall-clock).
  4. The NPC is non-engagement-eligible and not visible in any sector room during the transit (akin to commute).
  5. On arrival: flip home_region_id to Region B, set lifecycle_stage = active, regenerate daily_schedule from the destination roster's template, set home_barracks_id to the destination's barracks.
  6. Bus emits npc_reassigned with before/after region names.

Off-duty social proximity (📐 future enhancement)

For lore-rich gameplay, the design supports an optional social-affinity layer (📐 Design-only). NPCCharacter.npc_social_affinity JSONB carries {"allies": [npc_id, …], "rivals": [npc_id, …]}. When multiple off-duty NPCs schedule the same station's socialize block, the scheduler can colocate allies and separate rivals. Bus emits npc_socialize_together / npc_socialize_apart for flavor narration.

This is non-blocking for launch — the affinity field defaults to empty, social blocks place NPCs at their default station's sector regardless.

Humanized realtime events

Instead of bare npc_arrived / npc_departed payloads, the realtime bus extends NPC events with narrative metadata:

{
  "event_type": "npc_departed",
  "npc_id": "<vance-uuid>",
  "npc_name": "Marshal Cassandra Vance",
  "prior_activity": "patrol",
  "new_activity": "commute",
  "reason": "shift_end",
  "estimated_return_at": "<iso8601>",
  "narrative": "Marshal Vance is heading back to the Capital, shift ending"
}

Player-client renders the narrative in the sector event log. The lore service generates the narrative string from the structured payload via a small templating layer (📐 Design-only).

Engagement-gap visibility

When the engagement-routing fallback fires (no Marshal available in-region), the bus emits npc_response_delay:

{
  "event_type": "npc_response_delay",
  "region_id": "<uuid>",
  "reason": "all_marshals_engaged | all_marshals_kia | roster_short",
  "estimated_response_at": "<iso8601>",
  "grace_duration_minutes": 12
}

Player UI surfaces "Law enforcement response delayed (estimated 12 min)" — emergent universe state, visible to players.

Schema impact summary

Field / table Status Notes
NPCBarracks table New (see ../DATA_MODELS/npc-lodging.md) Lawful-faction lodging
OutlawBase table New Hostile-faction lodging
NPCCharacter.home_barracks_id UUID FK New NPC's lodging assignment
NPCCharacter.shift_preempt_pending Boolean New Engagement-preemption marker
NPCCharacter.preempted_prior_shift_npc_id UUID FK New Coverage-cascade marker
NPCCharacter.promotion_pending_at DateTime New Promotion deferral marker
NPCCharacter.npc_social_affinity JSONB New, optional Allies/rivals for social colocation
NPCRoster.default_lodging_id UUID + default_lodging_type Enum New Spawn-time lodging resolution
Sector.defenses.docked_npc_ships JSONB array New Off-duty ship parking + player-attack shielding
Sector.is_outlaw_zone Boolean New Pirate base sector flag
Sector.is_npc_barracks_sector Boolean New Sentinel-style barracks sector flag
daily_schedule.blocks[].overlap_start_minute Integer New Shift handoff window
daily_schedule.blocks[].location_type Enum Extended Add barracks, outlaw_base, ship, faction_hq, private_residence, transit, error_stranded
npc_activity enum Extended Add shift_handoff, shift_reroute, error_stranded
Squad.shift_handoff_state JSONB New Inter-shift intel passing

Status

📐 Design-only. The lifecycle framework, schedule mechanism, archetype enum, lifecycle stages, career processor, and schedule executor are all design targets. The simpler v1 npc-scheduler Loop A (patrol movement) is a strict subset of the schedule executor described here; this doc generalizes that loop to handle non-patrol activities. The daily_schedule JSONB extends the legacy patrol_route field; legacy rows migrate cleanly by wrapping their route in a single patrol block.

Cross-references