This document summarizes the comprehensive NPC behavior system implementation for the Northgate General Hospital Healthcare Scenario (SIS01), including countdown timer widget, event-driven NPC state management, and patrol overrides.
New Files:
npc_bed4_patient.ink- Stationary patient expressing alarm states (resting → distressed → critical → attended)npc_bed2_patient.ink- Post-surgical patient showing infusion pump consequences (stable → sedated → critical)
Updated Files:
npc_patrol_nurse.ink- Added knots: rushing_bed4, at_bed4, major_incident_linenpc_sarah.ink- Added knots: post_escalation, major_incident_linenpc_helen.ink- Added knot: post_drug_tamper (with #set_global pharmacist_on_ward=true)npc_chair_patient.ink- Added witness knots: state_stable, state_sedated, state_critical
globalVariables added:
"major_incident": false,
"major_incident_declared_time": 0,
"ward_monitoring_timeout_started": falseNPCs Updated:
patrol_nurse- Waypoint patrol with dwellTime, major_incident eventMappingsarah_mitchell- Added bed4_escalated and major_incident eventMappings (person-chat mode)bed4_patient- NEW stationary patient with state-driven eventMappingsbed2_patient- NEW stationary patient with state-driven eventMappingschairPatient(Mrs Kowalski) - Added witness eventMappings for bed2_state changespharmacist_npc- Position corrected, initiallyHidden=true, eventMapping for pharmacist_on_wardhelen_carver- Added drug_library_compromised eventMapping dispatching pharmacist
Extended setState() method:
- New case:
'patrolSpeed'- Dynamically change patrol speed during runtime - New case:
'dwellMultiplier'- Scale waypoint dwell times (prevents compounding on repeated calls)
New Methods:
goToAndStay(worldX, worldY, speed)- Move NPC to coordinates, stop permanently on arrival- Clears patrol state
- Sets single non-looping waypoint
- Manages
_stopOnArrivalflag to prevent freeze-in-place bugs - Includes detailed logging for debugging
NPCBehaviorManager Extensions:
goToAndStay(npcId, worldX, worldY, speed)- Public API for emergency responsessetNPCVisible(npcId, visible)- Toggle NPC visibility (alpha + physics state)
Config Forwarding in _setupEventMappings():
setVisible: mapping.setVisible ?? undefined,
patrolOverride: mapping.patrolOverride || null,
setPatrolSpeed: mapping.setPatrolSpeed ?? undefined,
setDwellMultiplier: mapping.setDwellMultiplier ?? undefinedNew Action Handlers in _handleEventMapping():
-
patrolOverride - Coordinates emergency response movement
- Resolves tile coordinates to world space
- Calls
goToAndStay()with target + speed - Logs destination for debugging
-
setVisible - Controls NPC appearance
- Toggles visibility on/off
- Enables delayed NPC reveal (e.g., pharmacist dispatch)
-
setPatrolSpeed - Dynamic speed changes
- Sets patrol.speed during runtime
- Enables faster movement during major incidents
-
setDwellMultiplier - Waypoint pause timing
- Scales dwell times multiplicatively
- Enables faster patrol response (less time at stations)
pharmacist_npcconfigured with:"initiallyHidden": true- Sprite created at load-time with alpha=0"setVisible": trueaction in eventMapping - Triggers reveal whenpharmacist_on_ward=true- Patrol waypoints pre-configured: nursing station → bed2 → bed4 cycle
setNPCVisible(npcId, visible)method:- Sets sprite alpha (0 = hidden, 1 = visible)
- Enables/disables physics body
- Adds to interaction system when revealed
patrol_nurse major_incident eventMapping:
"setPatrolSpeed": 150, // vs normal 80px/s
"setDwellMultiplier": 0.3 // scales waypoint dwelling to 30% of baselineEffect:
- Nurse visibly moves faster when major_incident=true
- Reduces station pauses (3000ms → 900ms, 8000ms → 2400ms)
- Creates urgency feedback without scripted cutscenes
scenario-timer.js (~150 lines)
ScenarioTimerUIclass: HUD countdown widget- Displays next pending timer with mm:ss format
- Urgency states: amber (< 5min), red/pulsing (< 1min)
- Evaluates conditions to filter active timers
- Integrates visual states with CSS animations
scenario-timer-dispatcher.js (~150 lines)
ScenarioTimerDispatcherclass: Timer event executor- Tracks elapsed time since dispatcher initialization
- Fires timers when delay threshold reached
- Executes
setGlobalactions (broadcasts events to NPC eventMappings) - Calls
markFired()to remove from UI countdown
CSS Additions (hud.css)
#scenario-timer-display /* Fixed position, top-right */
.scenario-timer--amber /* < 5 min amber text/border */
.scenario-timer--red /* < 1 min red + pulse animation */
@keyframes scenario-timer-pulse /* 0.5s pulse effect */- Imports:
ScenarioTimerUI,ScenarioTimerDispatcherwith v=1 cache busting - Initialization: After game_loaded event, both UI and dispatcher created
- Update loop:
scenarioTimerDispatcher.update(Date.now())called every frame
"timers": [
{
"id": "bed4_deterioration_1",
"label": "Patient Deterioration",
"delayMs": 480000, // 8 minutes
"condition": "!globalVars.bed4_escalated",
"setGlobal": { "patient_bed4_state": "distressed" },
"showCountdown": true,
"onceOnly": true
},
{
"id": "bed4_deterioration_2",
"label": "Patient Deterioration",
"delayMs": 900000, // 15 minutes
"condition": "!globalVars.bed4_escalated",
"setGlobal": { "patient_bed4_state": "critical" },
"showCountdown": true,
"onceOnly": true
},
{
"id": "bed4_deterioration_3",
"label": "Critical Event",
"delayMs": 1320000, // 22 minutes
"condition": "!globalVars.bed4_escalated && !globalVars.major_incident",
"setGlobal": { "major_incident": true, "patient_bed4_deceased": true },
"showCountdown": true,
"onceOnly": true
}
]- Player interacts with bed4_patient (sees monitor offline)
- Global:
bed4_escalated = truebroadcast - NPC Reactions:
- patrol_nurse: Walks to bed4 (patrolOverride), fires rushing_bed4 knot
- sarah_mitchell: Person-chat fires post_escalation knot
- bed4_patient: Transitions to state_distressed (knot change)
- Player interacts with drug_library object
- Global:
drug_library_compromised = truebroadcast - NPC Reactions:
- helen_carver: Fires post_drug_tamper knot, sets
pharmacist_on_ward = true - pharmacist_npc: Becomes visible (setVisible=true), begins patrol
- helen_carver: Fires post_drug_tamper knot, sets
- Timer fires OR Helen discovers tampering + escalation
- Global:
major_incident = truebroadcast - NPC Reactions:
- patrol_nurse: Fires major_incident_line knot, increases speed (150px/s), reduces dwell (0.3x)
- sarah_mitchell: Fires major_incident_line knot
- bed4_patient: Shows critical state concern
- UI Update: Countdown widget reprocesses timers (removes bed4_deterioration conditions)
- Player escalates Bed 4 → see nurse walk to bed4_escalated position
- Nurse says "rushing_bed4" dialogue
- Sarah manager acknowledges with "post_escalation" dialogue
- Pharmacist starts hidden (not visible in ward)
- Helen detects drug tampering → pharmacist_on_ward becomes true
- Pharmacist appears and begins patrol
- Major incident triggered
- Nurse moves visibly faster (150px/s vs normal 80px/s)
- Nurse pauses at stations briefly (0.3x original dwell)
- Countdown widget appears top-right upon game load
- Shows "Patient Deterioration — 08:00" at start
- Timer counts down in mm:ss format
- At 5 minutes: text/border turns amber
- At 1 minute: text/border turns red + pulses
- At 8 minutes: bed4_patient transitions to distressed (visual in Ink dialogue)
- At 15 minutes: bed4_patient transitions to critical
- At 22 minutes: major_incident trigger, patient_bed4_deceased=true (unless bed4_escalated before this)
import { ScenarioTimerUI } from '../ui/scenario-timer.js?v=1';
import { ScenarioTimerDispatcher } from '../ui/scenario-timer-dispatcher.js?v=1';window.scenarioTimerUI = new ScenarioTimerUI(this, gameScenario);
window.scenarioTimerDispatcher = new ScenarioTimerDispatcher(gameScenario);if (window.scenarioTimerDispatcher) {
window.scenarioTimerDispatcher.update(Date.now());
}| File | Lines Added | Purpose |
|---|---|---|
npc_bed4_patient.ink |
120 | NEW: Patient alarm states |
npc_bed2_patient.ink |
83 | NEW: Surgery patient states |
npc_patrol_nurse.ink |
+43 | Emergency response knots |
npc_sarah.ink |
+30 | Escalation knots |
npc_helen.ink |
+18 | Pharmacist dispatch knot |
npc_chair_patient.ink |
+27 | Witness state knots |
scenario.json.erb |
+250 | Timers + eventMappings + NPC updates |
npc-behavior.js |
+100 | setState extensions + goToAndStay() |
npc-manager.js |
+80 | Event action handlers |
scenario-timer.js |
150 | NEW: Timer UI widget |
scenario-timer-dispatcher.js |
150 | NEW: Timer executor |
hud.css |
+60 | Timer widget styling |
game.js |
+40 | Timer imports + initialization + update loop |
Total New Lines: ~1,150 across 13 files
- Timer Updates: 100ms tick interval for UI refresh (non-blocking)
- Event Dispatch: Each timer calls window.eventDispatcher.emit() (async, no blocking)
- NPC Updates: Existing patrol system reuses EasyStar.js pathfinding (O(n log n))
- Visibility Toggle: Simple alpha change + physics enable/disable (O(1))
- Condition Evaluation: String parsing with regex (cached per timer per update cycle)
- Phase 3 Refinement: Sprite creation pipeline modification to support initiallyHidden at load-time
- Timer Persistence: Save timer state on game pause/resume
- Sound Design: Audio cues for timer urgency levels (amber beep, red alert)
- Advanced Conditions: Nested boolean expression evaluator (currently supports basic negation/equality)
- Repeating Timers: Support
onceOnly: falsefor periodic events - Custom Callbacks: Allow arbitrary JavaScript execution on timer fire (security review needed)
Architecture Decision: Event-Driven Cascades
- Timer fires → setGlobal → eventDispatcher broadcasts → NPC eventMappings react
- No explicit orchestration code; entirely declarative in scenario JSON
- Enables non-linear player choice (escalate early = bypass some timers)
Design Pattern: Condition Guards
- Timers check conditions before executing
- Allows same scenario to branch based on player actions
- Example: bed4_deterioration_3 skips if major_incident already triggered
UI/Engine Separation
- ScenarioTimerUI: Pure display (countdown, urgency colors)
- ScenarioTimerDispatcher: Pure logic (timer tracking, event firing)
- No tight coupling; either component can be disabled independently
All 5 phases of the NPC behavior system have been successfully implemented:
✅ Phase 1: 6 Ink files created/updated, scenario configuration complete
✅ Phase 2: Engine methods (goToAndStay, setState extensions) + 4 event handlers in npc-manager.js
✅ Phase 3: Pharmacist visibility toggle (setVisible action) integrated
✅ Phase 4: Patrol overrides (speed + dwell) applied to major_incident eventMapping
✅ Phase 5: Countdown timer UI + dispatcher system + scenario integration complete
The system is ready for playtesting. All TODO comments preserved; remaining work items documented in scenario.json.erb with [Phase X] markers for future implementation or refinement.