@sharpee/engine 3.0.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/action-context-factory.d.ts +9 -2
- package/action-context-factory.d.ts.map +1 -1
- package/action-context-factory.js +15 -5
- package/action-context-factory.js.map +1 -1
- package/combatant-health-validation.d.ts +38 -0
- package/combatant-health-validation.d.ts.map +1 -0
- package/combatant-health-validation.js +54 -0
- package/combatant-health-validation.js.map +1 -0
- package/command-executor.d.ts +10 -4
- package/command-executor.d.ts.map +1 -1
- package/command-executor.js +27 -20
- package/command-executor.js.map +1 -1
- package/game-engine.d.ts +65 -11
- package/game-engine.d.ts.map +1 -1
- package/game-engine.js +185 -85
- package/game-engine.js.map +1 -1
- package/index.d.ts +20 -17
- package/index.d.ts.map +1 -1
- package/index.js +26 -22
- package/index.js.map +1 -1
- package/language-provider-interface.d.ts +1 -1
- package/language-provider-interface.d.ts.map +1 -1
- package/narrative/index.d.ts +1 -1
- package/narrative/index.d.ts.map +1 -1
- package/narrative/index.js +3 -3
- package/narrative/index.js.map +1 -1
- package/package.json +12 -12
- package/platform-operations.d.ts +2 -2
- package/platform-operations.d.ts.map +1 -1
- package/platform-operations.js +4 -4
- package/platform-operations.js.map +1 -1
- package/prose-pipeline/assemble.js +2 -2
- package/prose-pipeline/assemble.js.map +1 -1
- package/prose-pipeline/decorations/index.d.ts +4 -4
- package/prose-pipeline/decorations/index.d.ts.map +1 -1
- package/prose-pipeline/decorations/index.js +7 -7
- package/prose-pipeline/decorations/index.js.map +1 -1
- package/prose-pipeline/decorations/parser.d.ts +1 -1
- package/prose-pipeline/decorations/parser.d.ts.map +1 -1
- package/prose-pipeline/decorations/parser.js +6 -6
- package/prose-pipeline/decorations/parser.js.map +1 -1
- package/prose-pipeline/decorations/resolver.js +2 -2
- package/prose-pipeline/decorations/resolver.js.map +1 -1
- package/prose-pipeline/handlers/audibility.d.ts +1 -1
- package/prose-pipeline/handlers/audibility.d.ts.map +1 -1
- package/prose-pipeline/handlers/audibility.js +7 -7
- package/prose-pipeline/handlers/audibility.js.map +1 -1
- package/prose-pipeline/handlers/banner.d.ts +1 -1
- package/prose-pipeline/handlers/banner.d.ts.map +1 -1
- package/prose-pipeline/handlers/banner.js +8 -8
- package/prose-pipeline/handlers/banner.js.map +1 -1
- package/prose-pipeline/handlers/client-query.d.ts +1 -1
- package/prose-pipeline/handlers/client-query.d.ts.map +1 -1
- package/prose-pipeline/handlers/client-query.js +2 -2
- package/prose-pipeline/handlers/client-query.js.map +1 -1
- package/prose-pipeline/handlers/command-failed.d.ts +1 -1
- package/prose-pipeline/handlers/command-failed.d.ts.map +1 -1
- package/prose-pipeline/handlers/command-failed.js +4 -4
- package/prose-pipeline/handlers/command-failed.js.map +1 -1
- package/prose-pipeline/handlers/domain-message.d.ts +1 -1
- package/prose-pipeline/handlers/domain-message.d.ts.map +1 -1
- package/prose-pipeline/handlers/domain-message.js +15 -8
- package/prose-pipeline/handlers/domain-message.js.map +1 -1
- package/prose-pipeline/handlers/game.d.ts +1 -1
- package/prose-pipeline/handlers/game.d.ts.map +1 -1
- package/prose-pipeline/handlers/game.js +2 -2
- package/prose-pipeline/handlers/game.js.map +1 -1
- package/prose-pipeline/handlers/generic.d.ts +1 -1
- package/prose-pipeline/handlers/generic.d.ts.map +1 -1
- package/prose-pipeline/handlers/generic.js +12 -12
- package/prose-pipeline/handlers/generic.js.map +1 -1
- package/prose-pipeline/handlers/implicit-take.d.ts +1 -1
- package/prose-pipeline/handlers/implicit-take.d.ts.map +1 -1
- package/prose-pipeline/handlers/implicit-take.js +2 -2
- package/prose-pipeline/handlers/implicit-take.js.map +1 -1
- package/prose-pipeline/handlers/index.d.ts +11 -11
- package/prose-pipeline/handlers/index.d.ts.map +1 -1
- package/prose-pipeline/handlers/index.js +21 -21
- package/prose-pipeline/handlers/index.js.map +1 -1
- package/prose-pipeline/handlers/platform.d.ts +1 -1
- package/prose-pipeline/handlers/platform.d.ts.map +1 -1
- package/prose-pipeline/handlers/platform.js +5 -5
- package/prose-pipeline/handlers/platform.js.map +1 -1
- package/prose-pipeline/handlers/revealed.d.ts +1 -1
- package/prose-pipeline/handlers/revealed.d.ts.map +1 -1
- package/prose-pipeline/handlers/revealed.js +4 -4
- package/prose-pipeline/handlers/revealed.js.map +1 -1
- package/prose-pipeline/handlers/room.d.ts +1 -1
- package/prose-pipeline/handlers/room.d.ts.map +1 -1
- package/prose-pipeline/handlers/room.js +9 -9
- package/prose-pipeline/handlers/room.js.map +1 -1
- package/prose-pipeline/handlers/types.d.ts +7 -1
- package/prose-pipeline/handlers/types.d.ts.map +1 -1
- package/prose-pipeline/index.d.ts +11 -9
- package/prose-pipeline/index.d.ts.map +1 -1
- package/prose-pipeline/index.js +36 -32
- package/prose-pipeline/index.js.map +1 -1
- package/prose-pipeline/phrase-render.d.ts +28 -1
- package/prose-pipeline/phrase-render.d.ts.map +1 -1
- package/prose-pipeline/phrase-render.js +29 -2
- package/prose-pipeline/phrase-render.js.map +1 -1
- package/prose-pipeline/pipeline.d.ts +4 -5
- package/prose-pipeline/pipeline.d.ts.map +1 -1
- package/prose-pipeline/pipeline.js +31 -31
- package/prose-pipeline/pipeline.js.map +1 -1
- package/prose-pipeline/render-context.d.ts +2 -0
- package/prose-pipeline/render-context.d.ts.map +1 -1
- package/prose-pipeline/render-context.js.map +1 -1
- package/prose-pipeline/stages/index.d.ts +2 -2
- package/prose-pipeline/stages/index.d.ts.map +1 -1
- package/prose-pipeline/stages/index.js +5 -5
- package/prose-pipeline/stages/index.js.map +1 -1
- package/save-restore-service.d.ts +9 -3
- package/save-restore-service.d.ts.map +1 -1
- package/save-restore-service.js +15 -1
- package/save-restore-service.js.map +1 -1
- package/sound/dispatcher.js +2 -2
- package/sound/dispatcher.js.map +1 -1
- package/sound/index.d.ts +2 -2
- package/sound/index.d.ts.map +1 -1
- package/sound/index.js +6 -6
- package/sound/index.js.map +1 -1
- package/story.d.ts +10 -3
- package/story.d.ts.map +1 -1
- package/story.js +2 -2
- package/story.js.map +1 -1
- package/test-helpers/mock-prose-pipeline.d.ts +1 -1
- package/test-helpers/mock-prose-pipeline.d.ts.map +1 -1
- package/turn-event-processor.d.ts +1 -1
- package/turn-event-processor.d.ts.map +1 -1
package/game-engine.js
CHANGED
|
@@ -10,24 +10,25 @@ exports.splitChainedInput = splitChainedInput;
|
|
|
10
10
|
const world_model_1 = require("@sharpee/world-model");
|
|
11
11
|
const event_processor_1 = require("@sharpee/event-processor");
|
|
12
12
|
const stdlib_1 = require("@sharpee/stdlib");
|
|
13
|
-
const
|
|
13
|
+
const index_js_1 = require("./prose-pipeline/index.js");
|
|
14
14
|
const text_blocks_1 = require("@sharpee/text-blocks");
|
|
15
15
|
const channel_service_1 = require("@sharpee/channel-service");
|
|
16
16
|
const core_1 = require("@sharpee/core");
|
|
17
17
|
const plugins_1 = require("@sharpee/plugins");
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
18
|
+
const scene_evaluation_plugin_js_1 = require("./scene-evaluation-plugin.js");
|
|
19
|
+
const types_js_1 = require("./types.js");
|
|
20
|
+
const index_js_2 = require("./narrative/index.js");
|
|
21
|
+
const snippet_validation_js_1 = require("./snippet-validation.js");
|
|
22
|
+
const combatant_health_validation_js_1 = require("./combatant-health-validation.js");
|
|
23
|
+
const command_executor_js_1 = require("./command-executor.js");
|
|
24
|
+
const action_context_factory_js_1 = require("./action-context-factory.js");
|
|
25
|
+
const index_js_3 = require("./sound/index.js");
|
|
26
|
+
const turn_event_processor_js_1 = require("./turn-event-processor.js");
|
|
27
|
+
const parser_interface_js_1 = require("./parser-interface.js");
|
|
28
|
+
const language_provider_interface_js_1 = require("./language-provider-interface.js");
|
|
29
|
+
const vocabulary_manager_js_1 = require("./vocabulary-manager.js");
|
|
30
|
+
const save_restore_service_js_1 = require("./save-restore-service.js");
|
|
31
|
+
const turn_event_processor_js_2 = require("./turn-event-processor.js");
|
|
31
32
|
/**
|
|
32
33
|
* Conservative client-capability profile used when `start()` is called
|
|
33
34
|
* without an explicit `capabilities` option. Mirrors a CLI / text-only
|
|
@@ -96,8 +97,16 @@ class GameEngine {
|
|
|
96
97
|
* room for future extension seams (e.g., custom propagate injection
|
|
97
98
|
* via `setSoundDispatcher` in tests).
|
|
98
99
|
*/
|
|
99
|
-
soundDispatcher = new
|
|
100
|
+
soundDispatcher = new index_js_3.SoundDispatcher();
|
|
100
101
|
random;
|
|
102
|
+
/**
|
|
103
|
+
* Dedicated action RNG stream (ADR-231 D6), exposed to actions as
|
|
104
|
+
* `ActionContext.random`. A separate instance from `random` (the
|
|
105
|
+
* turn-plugin stream) so plugin draws can never shift action rolls;
|
|
106
|
+
* its seed rides the save blob (`IEngineState.actionRngSeed`) so
|
|
107
|
+
* post-restore action outcomes replay deterministically.
|
|
108
|
+
*/
|
|
109
|
+
actionRandom;
|
|
101
110
|
narrativeSettings;
|
|
102
111
|
// Alternate input mode handlers (ADR-137)
|
|
103
112
|
inputModeHandlers = new Map();
|
|
@@ -187,29 +196,37 @@ class GameEngine {
|
|
|
187
196
|
});
|
|
188
197
|
});
|
|
189
198
|
this.pluginRegistry = new plugins_1.PluginRegistry();
|
|
190
|
-
this.pluginRegistry.register(new
|
|
199
|
+
this.pluginRegistry.register(new scene_evaluation_plugin_js_1.SceneEvaluationPlugin());
|
|
191
200
|
this.random = (0, core_1.createSeededRandom)();
|
|
192
|
-
|
|
201
|
+
// ADR-231 D6: dedicated action stream — unseeded construction gives a
|
|
202
|
+
// time-based initial seed (same pattern as `random` above)
|
|
203
|
+
this.actionRandom = (0, core_1.createSeededRandom)();
|
|
204
|
+
this.narrativeSettings = (0, index_js_2.buildNarrativeSettings)(); // Default: 2nd person
|
|
193
205
|
// Initialize extracted services (Phase 4 remediation)
|
|
194
|
-
this.vocabularyManager = (0,
|
|
195
|
-
this.saveRestoreService = (0,
|
|
206
|
+
this.vocabularyManager = (0, vocabulary_manager_js_1.createVocabularyManager)();
|
|
207
|
+
this.saveRestoreService = (0, save_restore_service_js_1.createSaveRestoreService)({
|
|
196
208
|
maxSnapshots: this.config.maxUndoSnapshots ?? 10
|
|
197
209
|
});
|
|
198
|
-
this.turnEventProcessor = (0,
|
|
210
|
+
this.turnEventProcessor = (0, turn_event_processor_js_2.createTurnEventProcessor)(this.perceptionService);
|
|
199
211
|
// Set provided dependencies
|
|
200
212
|
this.languageProvider = options.language;
|
|
201
213
|
this.parser = options.parser;
|
|
202
|
-
this.textService = new
|
|
214
|
+
this.textService = new index_js_1.ProsePipeline(this.languageProvider, this.world);
|
|
203
215
|
// Update action registry with language provider
|
|
204
216
|
this.actionRegistry.setLanguageProvider(this.languageProvider);
|
|
205
217
|
// Wire parser with platform events if supported
|
|
206
|
-
if (this.parser && (0,
|
|
218
|
+
if (this.parser && (0, parser_interface_js_1.hasPlatformEventEmitter)(this.parser)) {
|
|
207
219
|
this.parser.setPlatformEventEmitter((event) => {
|
|
208
220
|
this.platformEvents.addEvent(event);
|
|
209
221
|
});
|
|
210
222
|
}
|
|
211
223
|
// Create command executor with dependencies
|
|
212
|
-
this.commandExecutor = (0,
|
|
224
|
+
this.commandExecutor = (0, command_executor_js_1.createCommandExecutor)(this.world, this.actionRegistry, this.eventProcessor, this.parser, this.systemEventSource, this.actionRandom);
|
|
225
|
+
// ADR-224: auto-register the deadly-room death transformer so every story
|
|
226
|
+
// (TS or Chord) gets the deadly-room verb-allowlist / probabilistic hazard for
|
|
227
|
+
// free — no story wiring needed. It early-returns when the player's room has no
|
|
228
|
+
// DeadlyRoomTrait, and uses the engine's seeded RNG for the `chance` variant.
|
|
229
|
+
this.commandExecutor.registerParsedCommandTransformer((0, stdlib_1.createDeadlyRoomTransformer)(this.random));
|
|
213
230
|
// Query handling is now managed by the platform layer
|
|
214
231
|
// Platform owns the QueryManager and handles all queries
|
|
215
232
|
// Note: game.initialized event is emitted in start() to avoid race condition
|
|
@@ -224,7 +241,7 @@ class GameEngine {
|
|
|
224
241
|
this.emitGameEvent(loadingEvent);
|
|
225
242
|
this.story = story;
|
|
226
243
|
// Build narrative settings from story config (ADR-089)
|
|
227
|
-
this.narrativeSettings = (0,
|
|
244
|
+
this.narrativeSettings = (0, index_js_2.buildNarrativeSettings)(story.config.narrative);
|
|
228
245
|
// Create player first so initializeWorld() can place them
|
|
229
246
|
const newPlayer = story.createPlayer(this.world);
|
|
230
247
|
this.context.player = newPlayer;
|
|
@@ -248,7 +265,9 @@ class GameEngine {
|
|
|
248
265
|
story.initializeWorld(this.world);
|
|
249
266
|
// ADR-209 AC-5: fail load synchronously (naming room and marker) if any
|
|
250
267
|
// snippet-bearing room's description carries an unbound {snippet:name}.
|
|
251
|
-
(0,
|
|
268
|
+
(0, snippet_validation_js_1.validateRoomSnippets)(this.world);
|
|
269
|
+
// ADR-226 AC-7: every combatant must carry the HealthTrait combat operates on.
|
|
270
|
+
(0, combatant_health_validation_js_1.validateCombatantHealth)(this.world);
|
|
252
271
|
// Configure language provider with narrative settings (ADR-089)
|
|
253
272
|
this.configureLanguageProviderNarrative(newPlayer);
|
|
254
273
|
// Update metadata
|
|
@@ -628,6 +647,30 @@ class GameEngine {
|
|
|
628
647
|
});
|
|
629
648
|
this.emit('channel:packet', packet, turn);
|
|
630
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Resume a stopped engine without touching world state.
|
|
652
|
+
*
|
|
653
|
+
* The post-mortem revival seam: after `stop('defeat')`, a harness (or a
|
|
654
|
+
* story resurrection policy) that has restored the world to a live-player
|
|
655
|
+
* snapshot — e.g. the transcript-tester's RETRY block via
|
|
656
|
+
* `world.loadJSON()` — needs turn execution back without any world
|
|
657
|
+
* teardown (a full reboot would clear the world it just restored).
|
|
658
|
+
* Flips `running` back on; emits nothing, rebuilds nothing.
|
|
659
|
+
*
|
|
660
|
+
* No-op when already running. Throws if the engine was never started
|
|
661
|
+
* (no command executor) — resuming presumes a completed `start()`.
|
|
662
|
+
*/
|
|
663
|
+
resume() {
|
|
664
|
+
if (this.running) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
// The channel service exists only once start() has completed — resuming
|
|
668
|
+
// an engine that never started would bypass the whole bootstrap order.
|
|
669
|
+
if (!this.channelService) {
|
|
670
|
+
throw new Error('Engine must have been started before it can resume');
|
|
671
|
+
}
|
|
672
|
+
this.running = true;
|
|
673
|
+
}
|
|
631
674
|
/**
|
|
632
675
|
* Stop the game engine
|
|
633
676
|
*/
|
|
@@ -674,41 +717,22 @@ class GameEngine {
|
|
|
674
717
|
this.emit('game:over', this.context);
|
|
675
718
|
}
|
|
676
719
|
/**
|
|
677
|
-
*
|
|
720
|
+
* Build the restart acknowledgment event (ADR-248).
|
|
678
721
|
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
722
|
+
* On confirmed restart the engine does NOT rebuild in place — it renders
|
|
723
|
+
* this acknowledgment ("The story restarts.") in the final packet, then
|
|
724
|
+
* stops with reason 'restart'; the client owns the reboot via its own
|
|
725
|
+
* boot path. No pre-emptive restart_completed(true) is emitted: the new
|
|
726
|
+
* boot's opening banner is the success signal.
|
|
681
727
|
*/
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if (this.parser && (0, parser_interface_1.hasPronounContext)(this.parser)) {
|
|
691
|
-
this.parser.resetPronounContext();
|
|
692
|
-
}
|
|
693
|
-
// Clear world state (entities, spatial index, relationships, etc.)
|
|
694
|
-
this.world.clear();
|
|
695
|
-
// Reset engine context
|
|
696
|
-
this.context.currentTurn = 1;
|
|
697
|
-
this.context.history = [];
|
|
698
|
-
this.context.metadata.started = new Date();
|
|
699
|
-
this.context.metadata.lastPlayed = new Date();
|
|
700
|
-
// Clear engine bookkeeping
|
|
701
|
-
this.turnEvents.clear();
|
|
702
|
-
this.pendingPlatformOps = [];
|
|
703
|
-
this.soundBuffer.length = 0;
|
|
704
|
-
this.saveRestoreService.clearUndoSnapshots();
|
|
705
|
-
this.hasEmittedInitialized = false;
|
|
706
|
-
// Clear plugin registry so story can re-register plugins
|
|
707
|
-
this.pluginRegistry.clear();
|
|
708
|
-
// Re-initialize the story (creates entities, player, custom actions, etc.)
|
|
709
|
-
this.setStory(this.story);
|
|
710
|
-
// Start the engine (emits game.initialized + game.started)
|
|
711
|
-
this.start();
|
|
728
|
+
createRestartAckEvent() {
|
|
729
|
+
return {
|
|
730
|
+
id: `restart_ack_${Date.now()}`,
|
|
731
|
+
type: 'game.message',
|
|
732
|
+
timestamp: Date.now(),
|
|
733
|
+
data: { messageId: 'if.action.restarting.game_restarting' },
|
|
734
|
+
entities: {}
|
|
735
|
+
};
|
|
712
736
|
}
|
|
713
737
|
/**
|
|
714
738
|
* Execute a turn
|
|
@@ -726,7 +750,7 @@ class GameEngine {
|
|
|
726
750
|
// the line, matching player expectations from Inform-style interpreters.
|
|
727
751
|
// Skipped while an alternate input mode is active (ADR-137) — mode
|
|
728
752
|
// handlers own the raw line, punctuation and all.
|
|
729
|
-
if (typeof input === 'string' && !this.world.getStateValue(
|
|
753
|
+
if (typeof input === 'string' && !this.world.getStateValue(types_js_1.INPUT_MODE_STATE_KEY)) {
|
|
730
754
|
const statements = splitChainedInput(input);
|
|
731
755
|
if (statements.length > 1) {
|
|
732
756
|
let result;
|
|
@@ -785,7 +809,7 @@ class GameEngine {
|
|
|
785
809
|
}
|
|
786
810
|
this.emit('turn:start', turn, input);
|
|
787
811
|
// Check for alternate input mode (ADR-137)
|
|
788
|
-
const activeModeId = this.world.getStateValue(
|
|
812
|
+
const activeModeId = this.world.getStateValue(types_js_1.INPUT_MODE_STATE_KEY);
|
|
789
813
|
if (activeModeId) {
|
|
790
814
|
const handler = this.inputModeHandlers.get(activeModeId);
|
|
791
815
|
if (handler) {
|
|
@@ -800,7 +824,7 @@ class GameEngine {
|
|
|
800
824
|
if (this.parser) {
|
|
801
825
|
// Set world context for parser
|
|
802
826
|
const player = this.world.getPlayer();
|
|
803
|
-
if (player && (0,
|
|
827
|
+
if (player && (0, parser_interface_js_1.hasWorldContext)(this.parser)) {
|
|
804
828
|
const playerLocation = this.world.getLocation(player.id) || '';
|
|
805
829
|
this.parser.setWorldContext(this.world, player.id, playerLocation);
|
|
806
830
|
}
|
|
@@ -843,7 +867,7 @@ class GameEngine {
|
|
|
843
867
|
locationId: playerLocation
|
|
844
868
|
};
|
|
845
869
|
// Store events for this turn (process through enrichment pipeline)
|
|
846
|
-
let semanticEvents = result.events.map(e => (0,
|
|
870
|
+
let semanticEvents = result.events.map(e => (0, turn_event_processor_js_1.processEvent)(e, enrichmentContext));
|
|
847
871
|
// Apply perception filtering if service is configured
|
|
848
872
|
// This transforms events based on what the player can perceive
|
|
849
873
|
if (this.perceptionService) {
|
|
@@ -865,7 +889,7 @@ class GameEngine {
|
|
|
865
889
|
if (result.success) {
|
|
866
890
|
this.updateCommandHistory(result, input, turn);
|
|
867
891
|
// Update pronoun context for "it"/"them"/"him"/"her" resolution (ADR-089)
|
|
868
|
-
if (this.parser && (0,
|
|
892
|
+
if (this.parser && (0, parser_interface_js_1.hasPronounContext)(this.parser) && result.validatedCommand) {
|
|
869
893
|
this.parser.updatePronounContext(result.validatedCommand, turn);
|
|
870
894
|
}
|
|
871
895
|
}
|
|
@@ -900,6 +924,16 @@ class GameEngine {
|
|
|
900
924
|
const playerLocation = this.world.getLocation(this.context.player.id);
|
|
901
925
|
// Plugin tick loop (ADR-120)
|
|
902
926
|
// Plugins run in priority order (NPC at 100, state machines at 75, scheduler at 50)
|
|
927
|
+
//
|
|
928
|
+
// actionResult.success must reflect GENUINE action success (Phase 7):
|
|
929
|
+
// result.success only checks for action.error events, but modern
|
|
930
|
+
// blocked() paths reuse the primary event type with blocked:true /
|
|
931
|
+
// failed:true — a refused action would otherwise report success and
|
|
932
|
+
// (e.g.) advance state-machine transitions it never earned.
|
|
933
|
+
const actionRefused = semanticEvents.some(e => {
|
|
934
|
+
const data = e.data;
|
|
935
|
+
return data?.blocked === true || data?.failed === true;
|
|
936
|
+
});
|
|
903
937
|
const pluginContext = {
|
|
904
938
|
world: this.world,
|
|
905
939
|
turn,
|
|
@@ -908,7 +942,7 @@ class GameEngine {
|
|
|
908
942
|
random: this.random,
|
|
909
943
|
actionResult: {
|
|
910
944
|
actionId: result.actionId || '',
|
|
911
|
-
success: result.success,
|
|
945
|
+
success: result.success && !actionRefused,
|
|
912
946
|
targetId: result.validatedCommand?.directObject?.entity?.id,
|
|
913
947
|
},
|
|
914
948
|
actionEvents: semanticEvents,
|
|
@@ -979,6 +1013,13 @@ class GameEngine {
|
|
|
979
1013
|
// channels still re-emit.
|
|
980
1014
|
this.emitChannelPacket(turnEvents, blocks, turn);
|
|
981
1015
|
}
|
|
1016
|
+
// Detect a canonical player-death event (ADR-224) *before* the turn's
|
|
1017
|
+
// events are cleared below. The death may have been emitted this turn by
|
|
1018
|
+
// the action, an interceptor, or a scheduler daemon — all have landed in
|
|
1019
|
+
// this turn's event stream by now, and story policy (event handlers in the
|
|
1020
|
+
// executor + state-machine plugins in the tick loop above) has already had
|
|
1021
|
+
// its "first crack" to veto by resetting the player's HealthTrait.
|
|
1022
|
+
const deathCause = this.playerDeathCauseThisTurn(turn);
|
|
982
1023
|
// Clear turn events after processing to prevent accumulation on same turn (meta commands)
|
|
983
1024
|
this.turnEvents.set(turn, []);
|
|
984
1025
|
// Emit completion
|
|
@@ -988,6 +1029,14 @@ class GameEngine {
|
|
|
988
1029
|
this.stop('victory', victoryDetails);
|
|
989
1030
|
return result;
|
|
990
1031
|
}
|
|
1032
|
+
// Route a still-dead player to game.lost. Only if the player's *derived*
|
|
1033
|
+
// life-state is still dead do we end the game — the re-check of live state,
|
|
1034
|
+
// not the event's `terminal` flag, is the engine's final word (ADR-224
|
|
1035
|
+
// Q-2, AC-3). A story reincarnation policy that cleared `dead` above wins.
|
|
1036
|
+
if (deathCause !== undefined && this.isPlayerDead()) {
|
|
1037
|
+
this.stop('defeat', { reason: 'You have died.', cause: deathCause });
|
|
1038
|
+
return result;
|
|
1039
|
+
}
|
|
991
1040
|
// Check for game over via story.isComplete()
|
|
992
1041
|
if (this.isGameOver()) {
|
|
993
1042
|
// Check if it's a victory (story completed successfully)
|
|
@@ -1078,7 +1127,7 @@ class GameEngine {
|
|
|
1078
1127
|
}
|
|
1079
1128
|
// Create action context for meta-command execution
|
|
1080
1129
|
const scopeResolver = (0, stdlib_1.createScopeResolver)(this.world);
|
|
1081
|
-
const actionContext = (0,
|
|
1130
|
+
const actionContext = (0, action_context_factory_js_1.createActionContext)(this.world, this.context, command, action, scopeResolver, undefined, this.actionRandom);
|
|
1082
1131
|
// Run action's four-phase pattern
|
|
1083
1132
|
const actionValidation = action.validate(actionContext);
|
|
1084
1133
|
let actionEvents;
|
|
@@ -1244,20 +1293,19 @@ class GameEngine {
|
|
|
1244
1293
|
}
|
|
1245
1294
|
case core_1.PlatformEventType.RESTART_REQUESTED: {
|
|
1246
1295
|
const context = platformOp.payload.context;
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1296
|
+
const shouldRestart = this.saveRestoreHooks?.onRestartRequested
|
|
1297
|
+
? await this.saveRestoreHooks.onRestartRequested(context)
|
|
1298
|
+
: true; // No restart hook — auto-confirm
|
|
1299
|
+
if (shouldRestart) {
|
|
1300
|
+
// ADR-248: ack in the final packet, then stop('restart'). The
|
|
1301
|
+
// client's hook is the reboot trigger; the stop reason is
|
|
1302
|
+
// bookkeeping. No restart_completed(true) — the reboot's opening
|
|
1303
|
+
// banner is the success signal.
|
|
1304
|
+
completionEvents.push(this.createRestartAckEvent());
|
|
1305
|
+
this.stop('restart');
|
|
1256
1306
|
}
|
|
1257
1307
|
else {
|
|
1258
|
-
|
|
1259
|
-
await this.restartGame();
|
|
1260
|
-
completionEvents.push((0, core_1.createRestartCompletedEvent)(true));
|
|
1308
|
+
completionEvents.push((0, core_1.createRestartCompletedEvent)(false));
|
|
1261
1309
|
}
|
|
1262
1310
|
break;
|
|
1263
1311
|
}
|
|
@@ -1383,7 +1431,7 @@ class GameEngine {
|
|
|
1383
1431
|
*/
|
|
1384
1432
|
configureLanguageProviderNarrative(player) {
|
|
1385
1433
|
// Check if language provider supports narrative settings
|
|
1386
|
-
if (!this.languageProvider || !(0,
|
|
1434
|
+
if (!this.languageProvider || !(0, language_provider_interface_js_1.hasNarrativeSettings)(this.languageProvider)) {
|
|
1387
1435
|
return;
|
|
1388
1436
|
}
|
|
1389
1437
|
// Build narrative context for language provider
|
|
@@ -1424,11 +1472,11 @@ class GameEngine {
|
|
|
1424
1472
|
throw new Error(`Cannot sync player state: entity '${newPlayerId}' not found`);
|
|
1425
1473
|
}
|
|
1426
1474
|
this.context.player = newPlayer;
|
|
1427
|
-
if (this.parser && (0,
|
|
1475
|
+
if (this.parser && (0, parser_interface_js_1.hasWorldContext)(this.parser)) {
|
|
1428
1476
|
const playerLocation = this.world.getLocation(newPlayerId) || '';
|
|
1429
1477
|
this.parser.setWorldContext(this.world, newPlayerId, playerLocation);
|
|
1430
1478
|
}
|
|
1431
|
-
if (this.parser && (0,
|
|
1479
|
+
if (this.parser && (0, parser_interface_js_1.hasPronounContext)(this.parser)) {
|
|
1432
1480
|
this.parser.resetPronounContext();
|
|
1433
1481
|
}
|
|
1434
1482
|
this.updateScopeVocabulary();
|
|
@@ -1440,6 +1488,23 @@ class GameEngine {
|
|
|
1440
1488
|
getPluginRegistry() {
|
|
1441
1489
|
return this.pluginRegistry;
|
|
1442
1490
|
}
|
|
1491
|
+
/**
|
|
1492
|
+
* The negotiated client capabilities for this session (ADR-216): the
|
|
1493
|
+
* `client has <capability>` predicate reads these live, and channel
|
|
1494
|
+
* gating uses the same flags at manifest time. Text-only before
|
|
1495
|
+
* `start({ capabilities })` runs or when none were negotiated.
|
|
1496
|
+
*/
|
|
1497
|
+
getClientCapabilities() {
|
|
1498
|
+
return this.clientCapabilities ?? exports.DEFAULT_TEXT_CAPABILITIES;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Get the dedicated action RNG stream (ADR-231 D6). Part of the
|
|
1502
|
+
* ISaveRestoreStateProvider contract — the save service persists this
|
|
1503
|
+
* stream's seed and the restore path re-seeds it.
|
|
1504
|
+
*/
|
|
1505
|
+
getActionRandom() {
|
|
1506
|
+
return this.actionRandom;
|
|
1507
|
+
}
|
|
1443
1508
|
/**
|
|
1444
1509
|
* Get event processor for handler registration (ADR-075)
|
|
1445
1510
|
*/
|
|
@@ -1675,7 +1740,7 @@ class GameEngine {
|
|
|
1675
1740
|
playerId: this.context.player.id,
|
|
1676
1741
|
locationId: playerLocation ?? undefined
|
|
1677
1742
|
};
|
|
1678
|
-
let processed = events.map(e => (0,
|
|
1743
|
+
let processed = events.map(e => (0, turn_event_processor_js_1.processEvent)(e, enrichmentContext));
|
|
1679
1744
|
if (this.perceptionService) {
|
|
1680
1745
|
processed = this.perceptionService.filterEvents(processed, this.context.player, this.world);
|
|
1681
1746
|
}
|
|
@@ -1986,10 +2051,14 @@ class GameEngine {
|
|
|
1986
2051
|
shouldRestart = await this.saveRestoreHooks.onRestartRequested(context);
|
|
1987
2052
|
}
|
|
1988
2053
|
if (shouldRestart) {
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2054
|
+
// ADR-248: ack in the final packet, then stop('restart').
|
|
2055
|
+
// No restart_completed(true) — the client reboots and its
|
|
2056
|
+
// opening banner is the success signal.
|
|
2057
|
+
const ackEvent = this.createRestartAckEvent();
|
|
2058
|
+
this.eventSource.emit(ackEvent);
|
|
2059
|
+
this.turnEvents.get(currentTurn)?.push(ackEvent);
|
|
2060
|
+
this.emit('event', ackEvent);
|
|
2061
|
+
this.stop('restart');
|
|
1993
2062
|
}
|
|
1994
2063
|
else {
|
|
1995
2064
|
const cancelEvent = (0, core_1.createRestartCompletedEvent)(false);
|
|
@@ -2118,6 +2187,37 @@ class GameEngine {
|
|
|
2118
2187
|
// Default: game never ends
|
|
2119
2188
|
return false;
|
|
2120
2189
|
}
|
|
2190
|
+
/**
|
|
2191
|
+
* The `cause` of a canonical player-death event (ADR-224) emitted during the
|
|
2192
|
+
* given turn, or `undefined` if the player did not die this turn. Scans the
|
|
2193
|
+
* turn's accumulated events, so it sees deaths from the action, interceptors,
|
|
2194
|
+
* and scheduler daemons alike. When several fire in one turn (rare), the first
|
|
2195
|
+
* is authoritative — `killPlayer` is idempotent, so later calls emit nothing.
|
|
2196
|
+
* @param turn the turn number whose events to scan
|
|
2197
|
+
*/
|
|
2198
|
+
playerDeathCauseThisTurn(turn) {
|
|
2199
|
+
const events = this.turnEvents.get(turn) || [];
|
|
2200
|
+
for (const event of events) {
|
|
2201
|
+
if (event.type === stdlib_1.PLAYER_DIED_EVENT) {
|
|
2202
|
+
const cause = event.data?.cause;
|
|
2203
|
+
return typeof cause === 'string' ? cause : 'unknown';
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return undefined;
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
2209
|
+
* Whether the player is currently dead by their derived `HealthTrait` state
|
|
2210
|
+
* (ADR-226/ADR-224). A player with no `HealthTrait` is alive by default (the
|
|
2211
|
+
* opt-in rule) — `killPlayer` lazily attaches one, so a real death always has a
|
|
2212
|
+
* trait to read. This is the engine's "final word" after story policy has run.
|
|
2213
|
+
*/
|
|
2214
|
+
isPlayerDead() {
|
|
2215
|
+
const player = this.context.player;
|
|
2216
|
+
if (!player)
|
|
2217
|
+
return false;
|
|
2218
|
+
const health = player.get(world_model_1.TraitType.HEALTH);
|
|
2219
|
+
return health ? !world_model_1.HealthBehavior.isAlive(health) : false;
|
|
2220
|
+
}
|
|
2121
2221
|
/**
|
|
2122
2222
|
* Add event listener
|
|
2123
2223
|
*/
|