@vylos/core 0.1.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/package.json +34 -0
- package/src/components/app/EngineView.vue +40 -0
- package/src/components/app/GameShell.vue +265 -0
- package/src/components/app/LoadingScreen.vue +10 -0
- package/src/components/app/MainMenu.vue +243 -0
- package/src/components/core/BackgroundLayer.vue +27 -0
- package/src/components/core/ChoicePanel.vue +115 -0
- package/src/components/core/CustomOverlay.vue +27 -0
- package/src/components/core/DialogueBox.vue +144 -0
- package/src/components/core/DrawableOverlay.vue +109 -0
- package/src/components/core/ForegroundLayer.vue +31 -0
- package/src/components/menu/ActionOverlay.vue +126 -0
- package/src/components/menu/LocationOverlay.vue +136 -0
- package/src/components/menu/PauseMenu.vue +196 -0
- package/src/components/menu/SaveLoadMenu.vue +377 -0
- package/src/components/menu/SettingsMenu.vue +111 -0
- package/src/components/menu/TopBar.vue +65 -0
- package/src/composables/useConfig.ts +4 -0
- package/src/composables/useEngine.ts +16 -0
- package/src/composables/useGameState.ts +9 -0
- package/src/composables/useLanguage.ts +31 -0
- package/src/engine/core/CheckpointManager.ts +122 -0
- package/src/engine/core/Engine.ts +272 -0
- package/src/engine/core/EngineFactory.ts +102 -0
- package/src/engine/core/EventRunner.ts +488 -0
- package/src/engine/errors/EventEndError.ts +7 -0
- package/src/engine/errors/InterruptSignal.ts +10 -0
- package/src/engine/errors/JumpSignal.ts +10 -0
- package/src/engine/errors/StateValidationError.ts +13 -0
- package/src/engine/managers/ActionManager.ts +62 -0
- package/src/engine/managers/EventManager.ts +166 -0
- package/src/engine/managers/HistoryManager.ts +84 -0
- package/src/engine/managers/InputManager.ts +117 -0
- package/src/engine/managers/LanguageManager.ts +51 -0
- package/src/engine/managers/LocationManager.ts +76 -0
- package/src/engine/managers/NavigationManager.ts +75 -0
- package/src/engine/managers/SaveManager.ts +86 -0
- package/src/engine/managers/SettingsManager.ts +70 -0
- package/src/engine/managers/WaitManager.ts +47 -0
- package/src/engine/schemas/baseGameState.schema.ts +19 -0
- package/src/engine/schemas/checkpoint.schema.ts +11 -0
- package/src/engine/schemas/engineState.schema.ts +59 -0
- package/src/engine/schemas/location.schema.ts +21 -0
- package/src/engine/storage/VylosStorage.ts +131 -0
- package/src/engine/types/actions.ts +20 -0
- package/src/engine/types/checkpoint.ts +31 -0
- package/src/engine/types/config.ts +9 -0
- package/src/engine/types/dialogue.ts +15 -0
- package/src/engine/types/engine.ts +85 -0
- package/src/engine/types/events.ts +117 -0
- package/src/engine/types/game-state.ts +15 -0
- package/src/engine/types/index.ts +10 -0
- package/src/engine/types/locations.ts +32 -0
- package/src/engine/types/plugin.ts +11 -0
- package/src/engine/types/save.ts +40 -0
- package/src/engine/utils/TimeHelper.ts +39 -0
- package/src/engine/utils/logger.ts +43 -0
- package/src/env.d.ts +17 -0
- package/src/index.ts +74 -0
- package/src/stores/engineState.ts +127 -0
- package/src/stores/gameState.ts +49 -0
- package/tests/engine/ActionManager.test.ts +94 -0
- package/tests/engine/CheckpointManager.test.ts +136 -0
- package/tests/engine/EventManager.test.ts +145 -0
- package/tests/engine/EventRunner.test.ts +318 -0
- package/tests/engine/HistoryManager.test.ts +113 -0
- package/tests/engine/LocationManager.test.ts +128 -0
- package/tests/engine/schemas.test.ts +250 -0
- package/tests/engine/utils.test.ts +75 -0
- package/tests/integration/game-loop.test.ts +201 -0
- package/tests/safety/event-validation.test.ts +102 -0
- package/tests/safety/state-schema.test.ts +96 -0
- package/tests/setup.ts +2 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="topbar">
|
|
3
|
+
<div class="topbar__inner">
|
|
4
|
+
<span v-if="locationName" class="topbar__item">📍 {{ locationName }}</span>
|
|
5
|
+
<span class="topbar__separator"></span>
|
|
6
|
+
<span class="topbar__item">⏰ {{ gameTimeFormatted }}</span>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import { computed } from 'vue';
|
|
13
|
+
import { useEngineStateStore } from '../../stores/engineState';
|
|
14
|
+
import { useGameStateStore } from '../../stores/gameState';
|
|
15
|
+
|
|
16
|
+
const engineState = useEngineStateStore();
|
|
17
|
+
const gameState = useGameStateStore();
|
|
18
|
+
|
|
19
|
+
const locationName = computed(() => engineState.currentLocationId ?? '');
|
|
20
|
+
|
|
21
|
+
const gameTimeFormatted = computed(() => {
|
|
22
|
+
const t = gameState.state.gameTime % 24;
|
|
23
|
+
const h = Math.floor(t);
|
|
24
|
+
const m = Math.round((t - h) * 60);
|
|
25
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<style scoped>
|
|
30
|
+
.topbar {
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: 0;
|
|
33
|
+
left: 0;
|
|
34
|
+
right: 0;
|
|
35
|
+
z-index: 25;
|
|
36
|
+
display: flex;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
padding: 1.2cqh 2cqw;
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.topbar__inner {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 1.5cqw;
|
|
46
|
+
padding: 0.8cqh 2cqw;
|
|
47
|
+
background: rgba(0, 0, 0, 0.3);
|
|
48
|
+
backdrop-filter: blur(4px);
|
|
49
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
50
|
+
border-radius: 0.8cqw;
|
|
51
|
+
pointer-events: auto;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.topbar__item {
|
|
55
|
+
color: rgba(255, 255, 255, 0.85);
|
|
56
|
+
font-size: 1.6cqw;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.topbar__separator {
|
|
61
|
+
width: 1px;
|
|
62
|
+
height: 2cqh;
|
|
63
|
+
background: rgba(255, 255, 255, 0.2);
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { inject } from 'vue';
|
|
2
|
+
import type { Engine } from '../engine/core/Engine';
|
|
3
|
+
|
|
4
|
+
export const ENGINE_INJECT_KEY = 'vylos-engine';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Composable that provides access to the engine instance via inject.
|
|
8
|
+
* Must be used inside a component that is a descendant of GameShell.
|
|
9
|
+
*/
|
|
10
|
+
export function useEngine(): Engine {
|
|
11
|
+
const engine = inject<Engine>(ENGINE_INJECT_KEY);
|
|
12
|
+
if (!engine) {
|
|
13
|
+
throw new Error('[Vylos] useEngine() called outside of GameShell context. Make sure the component is a descendant of GameShell.');
|
|
14
|
+
}
|
|
15
|
+
return engine;
|
|
16
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ref, computed } from 'vue';
|
|
2
|
+
import type { TextEntry } from '../engine/types';
|
|
3
|
+
|
|
4
|
+
const currentLanguage = ref<string>('en');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Composable for language management.
|
|
8
|
+
*/
|
|
9
|
+
export function useLanguage() {
|
|
10
|
+
const language = computed(() => currentLanguage.value);
|
|
11
|
+
|
|
12
|
+
function setLanguage(lang: string): void {
|
|
13
|
+
currentLanguage.value = lang;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a TextEntry to a string for the current language.
|
|
18
|
+
* Falls back to 'en', then to the first available key.
|
|
19
|
+
*/
|
|
20
|
+
function resolveText(entry: string | TextEntry): string {
|
|
21
|
+
if (typeof entry === 'string') return entry;
|
|
22
|
+
return (
|
|
23
|
+
entry[currentLanguage.value] ??
|
|
24
|
+
entry['en'] ??
|
|
25
|
+
Object.values(entry)[0] ??
|
|
26
|
+
''
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { language, setLanguage, resolveText };
|
|
31
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { toRaw } from 'vue';
|
|
2
|
+
import type { BaseGameState, Checkpoint, CheckpointType, ChoiceOption } from '../types';
|
|
3
|
+
import type { DialogueState } from '../types/engine';
|
|
4
|
+
|
|
5
|
+
export interface CaptureDisplayData {
|
|
6
|
+
dialogue?: DialogueState | null;
|
|
7
|
+
background?: string | null;
|
|
8
|
+
choiceOptions?: ChoiceOption[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Manages checkpoints for event execution and rollback.
|
|
13
|
+
* Each interaction point (say, choice, wait) creates a checkpoint
|
|
14
|
+
* containing a deep clone of game state + the result of that interaction.
|
|
15
|
+
*/
|
|
16
|
+
export class CheckpointManager {
|
|
17
|
+
private checkpoints: Checkpoint[] = [];
|
|
18
|
+
private replayIndex = -1;
|
|
19
|
+
|
|
20
|
+
/** Whether we're currently in replay mode (fast-forwarding during rollback) */
|
|
21
|
+
get isReplaying(): boolean {
|
|
22
|
+
return this.replayIndex >= 0 && this.replayIndex < this.checkpoints.length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Current replay step (or -1 if not replaying) */
|
|
26
|
+
get currentReplayStep(): number {
|
|
27
|
+
return this.replayIndex;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Total number of checkpoints stored */
|
|
31
|
+
get count(): number {
|
|
32
|
+
return this.checkpoints.length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Get checkpoint at a specific index */
|
|
36
|
+
getAt(index: number): Checkpoint | undefined {
|
|
37
|
+
return this.checkpoints[index];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Record a checkpoint at the current interaction point */
|
|
41
|
+
capture(gameState: BaseGameState, type: CheckpointType, choiceResult?: string, display?: CaptureDisplayData): void {
|
|
42
|
+
const checkpoint: Checkpoint = {
|
|
43
|
+
step: this.checkpoints.length,
|
|
44
|
+
gameState: structuredClone(toRaw(gameState)),
|
|
45
|
+
type,
|
|
46
|
+
choiceResult,
|
|
47
|
+
dialogue: display?.dialogue ? structuredClone(toRaw(display.dialogue)) : undefined,
|
|
48
|
+
background: display?.background,
|
|
49
|
+
choiceOptions: display?.choiceOptions ? structuredClone(toRaw(display.choiceOptions)) : undefined,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// If replaying past the current index, trim future checkpoints
|
|
53
|
+
if (this.replayIndex >= 0) {
|
|
54
|
+
this.checkpoints.length = this.replayIndex;
|
|
55
|
+
this.replayIndex = -1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.checkpoints.push(checkpoint);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Get the stored choice result for the current replay step (or undefined) */
|
|
62
|
+
getReplayChoiceResult(): string | undefined {
|
|
63
|
+
if (!this.isReplaying) return undefined;
|
|
64
|
+
return this.checkpoints[this.replayIndex]?.choiceResult;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Advance replay index by one step. Returns false if replay is done. */
|
|
68
|
+
advanceReplay(): boolean {
|
|
69
|
+
if (!this.isReplaying) return false;
|
|
70
|
+
this.replayIndex++;
|
|
71
|
+
return this.replayIndex < this.checkpoints.length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Get game state snapshot at a specific step */
|
|
75
|
+
getStateAt(step: number): BaseGameState | undefined {
|
|
76
|
+
const checkpoint = this.checkpoints[step];
|
|
77
|
+
if (!checkpoint) return undefined;
|
|
78
|
+
return structuredClone(checkpoint.gameState);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Get all checkpoints (for save/load) */
|
|
82
|
+
getAll(): Checkpoint[] {
|
|
83
|
+
return this.checkpoints.map(c => structuredClone(c));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Prepare for rollback: set replay index to target step */
|
|
87
|
+
prepareRollback(targetStep: number): BaseGameState | undefined {
|
|
88
|
+
if (targetStep < 0 || targetStep >= this.checkpoints.length) return undefined;
|
|
89
|
+
this.replayIndex = 0;
|
|
90
|
+
// Trim checkpoints after target (re-execution will recreate them)
|
|
91
|
+
const targetState = structuredClone(this.checkpoints[targetStep].gameState);
|
|
92
|
+
this.checkpoints.length = targetStep;
|
|
93
|
+
return targetState;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Update the stored choice result at a specific step */
|
|
97
|
+
updateChoiceAt(step: number, newChoice: string): void {
|
|
98
|
+
if (step >= 0 && step < this.checkpoints.length) {
|
|
99
|
+
this.checkpoints[step].choiceResult = newChoice;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Set replay from beginning, trimming checkpoints after endStep */
|
|
104
|
+
setReplayTo(endStep: number): void {
|
|
105
|
+
this.replayIndex = 0;
|
|
106
|
+
if (endStep < this.checkpoints.length) {
|
|
107
|
+
this.checkpoints.length = endStep;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Clear all checkpoints (new event or new game) */
|
|
112
|
+
clear(): void {
|
|
113
|
+
this.checkpoints = [];
|
|
114
|
+
this.replayIndex = -1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Restore checkpoints from save data */
|
|
118
|
+
restore(checkpoints: Checkpoint[]): void {
|
|
119
|
+
this.checkpoints = structuredClone(checkpoints);
|
|
120
|
+
this.replayIndex = -1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { VylosEvent, BaseGameState, Checkpoint, SaveSlot } from '../types';
|
|
2
|
+
import { EventManager } from '../managers/EventManager';
|
|
3
|
+
import { HistoryManager } from '../managers/HistoryManager';
|
|
4
|
+
import { NavigationManager, NavigationAction } from '../managers/NavigationManager';
|
|
5
|
+
import { SaveManager } from '../managers/SaveManager';
|
|
6
|
+
import { SettingsManager } from '../managers/SettingsManager';
|
|
7
|
+
import { EventRunner } from './EventRunner';
|
|
8
|
+
import { JumpSignal } from '../errors/JumpSignal';
|
|
9
|
+
import { logger } from '../utils/logger';
|
|
10
|
+
|
|
11
|
+
export interface EngineLoopCallbacks {
|
|
12
|
+
/** Called each loop iteration (update UI: available locations, actions, background) */
|
|
13
|
+
onTick?(state: BaseGameState): void;
|
|
14
|
+
/** Called when player selects an action */
|
|
15
|
+
onAction?(actionId: string, state: BaseGameState): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface EngineDeps {
|
|
19
|
+
eventManager: EventManager;
|
|
20
|
+
historyManager: HistoryManager;
|
|
21
|
+
navigationManager: NavigationManager;
|
|
22
|
+
eventRunner: EventRunner;
|
|
23
|
+
saveManager: SaveManager;
|
|
24
|
+
settingsManager: SettingsManager;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Main engine orchestrator.
|
|
29
|
+
* Runs the game loop: evaluate events → execute → wait for navigation → repeat.
|
|
30
|
+
*/
|
|
31
|
+
export class Engine {
|
|
32
|
+
readonly eventManager: EventManager;
|
|
33
|
+
readonly historyManager: HistoryManager;
|
|
34
|
+
readonly navigationManager: NavigationManager;
|
|
35
|
+
readonly eventRunner: EventRunner;
|
|
36
|
+
readonly saveManager: SaveManager;
|
|
37
|
+
readonly settingsManager: SettingsManager;
|
|
38
|
+
private running = false;
|
|
39
|
+
|
|
40
|
+
/** Pending mid-event resume after load */
|
|
41
|
+
private pendingResume: {
|
|
42
|
+
eventId: string;
|
|
43
|
+
checkpoints: Checkpoint[];
|
|
44
|
+
initialState: BaseGameState;
|
|
45
|
+
} | null = null;
|
|
46
|
+
|
|
47
|
+
/** Flag to skip event lock/push when interrupted by a load */
|
|
48
|
+
private loadInterrupted = false;
|
|
49
|
+
|
|
50
|
+
constructor(deps: EngineDeps) {
|
|
51
|
+
this.eventManager = deps.eventManager;
|
|
52
|
+
this.historyManager = deps.historyManager;
|
|
53
|
+
this.navigationManager = deps.navigationManager;
|
|
54
|
+
this.eventRunner = deps.eventRunner;
|
|
55
|
+
this.saveManager = deps.saveManager;
|
|
56
|
+
this.settingsManager = deps.settingsManager;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Register events and start the game loop */
|
|
60
|
+
async run(events: VylosEvent[], getState: () => BaseGameState, loop?: EngineLoopCallbacks): Promise<void> {
|
|
61
|
+
this.eventManager.registerAll(events);
|
|
62
|
+
this.running = true;
|
|
63
|
+
|
|
64
|
+
logger.info('Engine started');
|
|
65
|
+
|
|
66
|
+
while (this.running) {
|
|
67
|
+
// Handle pending load resume before anything else
|
|
68
|
+
if (this.pendingResume) {
|
|
69
|
+
this.loadInterrupted = false;
|
|
70
|
+
await this.handleResume(getState);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const state = getState();
|
|
75
|
+
|
|
76
|
+
// Update UI (locations, actions, background)
|
|
77
|
+
loop?.onTick?.(state);
|
|
78
|
+
|
|
79
|
+
// Evaluate event conditions
|
|
80
|
+
this.eventManager.evaluate(state);
|
|
81
|
+
|
|
82
|
+
// Get next unlocked event
|
|
83
|
+
const event = this.eventManager.getNextUnlocked(state);
|
|
84
|
+
|
|
85
|
+
if (event) {
|
|
86
|
+
await this.executeEvent(event, getState);
|
|
87
|
+
continue; // Re-evaluate after event execution
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!this.running) break;
|
|
91
|
+
|
|
92
|
+
// Wait for navigation input
|
|
93
|
+
const nav = await this.navigationManager.waitForNavigation();
|
|
94
|
+
|
|
95
|
+
switch (nav.action) {
|
|
96
|
+
case NavigationAction.Back:
|
|
97
|
+
this.handleBack();
|
|
98
|
+
break;
|
|
99
|
+
case NavigationAction.Forward:
|
|
100
|
+
this.handleForward();
|
|
101
|
+
break;
|
|
102
|
+
case NavigationAction.Continue:
|
|
103
|
+
// Continue to next loop iteration
|
|
104
|
+
break;
|
|
105
|
+
case NavigationAction.Location:
|
|
106
|
+
if (nav.payload) {
|
|
107
|
+
const s = getState();
|
|
108
|
+
s.locationId = nav.payload;
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case NavigationAction.Action:
|
|
112
|
+
if (nav.payload) {
|
|
113
|
+
loop?.onAction?.(nav.payload, getState());
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
case NavigationAction.DrawableEvent:
|
|
117
|
+
if (nav.payload) {
|
|
118
|
+
const drawableEvent = this.eventManager.get(nav.payload);
|
|
119
|
+
if (drawableEvent) {
|
|
120
|
+
await this.executeEvent(drawableEvent, getState);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
logger.info('Engine stopped');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Load a save and resume execution.
|
|
132
|
+
* Restores game state, history, event lock state, and sets up mid-event resume if needed.
|
|
133
|
+
*/
|
|
134
|
+
loadSave(saveData: SaveSlot, setState: (state: BaseGameState) => void): void {
|
|
135
|
+
// Restore game state
|
|
136
|
+
setState(JSON.parse(JSON.stringify(saveData.gameState)));
|
|
137
|
+
|
|
138
|
+
// Restore event lock state
|
|
139
|
+
this.eventManager.resetAll();
|
|
140
|
+
if (saveData.lockedEventIds) {
|
|
141
|
+
this.eventManager.restoreLockedIds(saveData.lockedEventIds);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Restore history
|
|
145
|
+
if (saveData.history) {
|
|
146
|
+
this.historyManager.restore(saveData.history, saveData.historyIndex ?? -1);
|
|
147
|
+
} else {
|
|
148
|
+
this.historyManager.clear();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Set up mid-event resume if saved during an event
|
|
152
|
+
if (saveData.eventId && saveData.checkpoints?.length && saveData.initialState) {
|
|
153
|
+
this.pendingResume = {
|
|
154
|
+
eventId: saveData.eventId,
|
|
155
|
+
checkpoints: saveData.checkpoints,
|
|
156
|
+
initialState: saveData.initialState,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Interrupt current execution so the loop picks up the new state
|
|
161
|
+
this.loadInterrupted = true;
|
|
162
|
+
this.eventRunner.interrupt('load');
|
|
163
|
+
this.navigationManager.cancel();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Execute a single event, handling jumps */
|
|
167
|
+
private async executeEvent(event: VylosEvent, getState: () => BaseGameState): Promise<void> {
|
|
168
|
+
let currentEvent: VylosEvent | undefined = event;
|
|
169
|
+
|
|
170
|
+
while (currentEvent) {
|
|
171
|
+
this.eventManager.setRunning(currentEvent.id);
|
|
172
|
+
logger.debug(`Executing event: ${currentEvent.id}`);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await this.eventRunner.executeEvent(currentEvent);
|
|
176
|
+
|
|
177
|
+
// Skip lock/push if interrupted by a load
|
|
178
|
+
if (this.loadInterrupted) return;
|
|
179
|
+
|
|
180
|
+
// Event completed successfully
|
|
181
|
+
const state = getState();
|
|
182
|
+
this.eventManager.setLocked(currentEvent.id, state);
|
|
183
|
+
this.historyManager.push(currentEvent.id, this.eventRunner.checkpoints.getAll());
|
|
184
|
+
currentEvent = undefined;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (this.loadInterrupted) return;
|
|
187
|
+
|
|
188
|
+
if (error instanceof JumpSignal) {
|
|
189
|
+
// Lock current event, find jump target
|
|
190
|
+
const state = getState();
|
|
191
|
+
this.eventManager.setLocked(currentEvent.id, state);
|
|
192
|
+
this.historyManager.push(currentEvent.id, this.eventRunner.checkpoints.getAll());
|
|
193
|
+
|
|
194
|
+
const target = this.eventManager.get(error.targetEventId);
|
|
195
|
+
if (target) {
|
|
196
|
+
logger.debug(`Jump to: ${error.targetEventId}`);
|
|
197
|
+
currentEvent = target;
|
|
198
|
+
} else {
|
|
199
|
+
logger.error(`Jump target not found: ${error.targetEventId}`);
|
|
200
|
+
currentEvent = undefined;
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
logger.error('Event execution error:', error);
|
|
204
|
+
currentEvent = undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Handle resuming a mid-event save */
|
|
211
|
+
private async handleResume(getState: () => BaseGameState): Promise<void> {
|
|
212
|
+
const resume = this.pendingResume!;
|
|
213
|
+
this.pendingResume = null;
|
|
214
|
+
|
|
215
|
+
const event = this.eventManager.get(resume.eventId);
|
|
216
|
+
if (!event) {
|
|
217
|
+
logger.error(`Resume: event not found: ${resume.eventId}`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.eventManager.setRunning(resume.eventId);
|
|
222
|
+
this.eventRunner.checkpoints.restore(resume.checkpoints);
|
|
223
|
+
logger.debug(`Resuming event: ${resume.eventId} (${resume.checkpoints.length} checkpoints)`);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await this.eventRunner.resumeEvent(event, resume.initialState);
|
|
227
|
+
|
|
228
|
+
if (this.loadInterrupted) return;
|
|
229
|
+
|
|
230
|
+
const state = getState();
|
|
231
|
+
this.eventManager.setLocked(resume.eventId, state);
|
|
232
|
+
this.historyManager.push(resume.eventId, this.eventRunner.checkpoints.getAll());
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (this.loadInterrupted) return;
|
|
235
|
+
|
|
236
|
+
if (error instanceof JumpSignal) {
|
|
237
|
+
const state = getState();
|
|
238
|
+
this.eventManager.setLocked(resume.eventId, state);
|
|
239
|
+
this.historyManager.push(resume.eventId, this.eventRunner.checkpoints.getAll());
|
|
240
|
+
|
|
241
|
+
const target = this.eventManager.get(error.targetEventId);
|
|
242
|
+
if (target) {
|
|
243
|
+
logger.debug(`Resume jump to: ${error.targetEventId}`);
|
|
244
|
+
await this.executeEvent(target, getState);
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
logger.error('Resume execution error:', error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private handleBack(): void {
|
|
253
|
+
const entry = this.historyManager.goBack();
|
|
254
|
+
if (entry) {
|
|
255
|
+
logger.debug(`History back: ${entry.eventId}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private handleForward(): void {
|
|
260
|
+
const entry = this.historyManager.goForward();
|
|
261
|
+
if (entry) {
|
|
262
|
+
logger.debug(`History forward: ${entry.eventId}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Stop the engine loop */
|
|
267
|
+
stop(): void {
|
|
268
|
+
this.running = false;
|
|
269
|
+
this.eventRunner.interrupt('engine stopped');
|
|
270
|
+
this.navigationManager.cancel();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { container as globalContainer, type DependencyContainer } from 'tsyringe';
|
|
3
|
+
import type { VylosPlugin } from '../types';
|
|
4
|
+
import type { Component } from 'vue';
|
|
5
|
+
import { EventManager } from '../managers/EventManager';
|
|
6
|
+
import { HistoryManager } from '../managers/HistoryManager';
|
|
7
|
+
import { NavigationManager } from '../managers/NavigationManager';
|
|
8
|
+
import { WaitManager } from '../managers/WaitManager';
|
|
9
|
+
import { SaveManager } from '../managers/SaveManager';
|
|
10
|
+
import { SettingsManager } from '../managers/SettingsManager';
|
|
11
|
+
import { EventRunner, type EventRunnerCallbacks } from './EventRunner';
|
|
12
|
+
import { CheckpointManager } from './CheckpointManager';
|
|
13
|
+
import { VylosStorage } from '../storage/VylosStorage';
|
|
14
|
+
import { Engine } from './Engine';
|
|
15
|
+
|
|
16
|
+
/** Tokens used for DI registration */
|
|
17
|
+
export const DI_TOKENS = {
|
|
18
|
+
EventManager: 'EventManager',
|
|
19
|
+
HistoryManager: 'HistoryManager',
|
|
20
|
+
NavigationManager: 'NavigationManager',
|
|
21
|
+
WaitManager: 'WaitManager',
|
|
22
|
+
CheckpointManager: 'CheckpointManager',
|
|
23
|
+
EventRunner: 'EventRunner',
|
|
24
|
+
Engine: 'Engine',
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
/** Component override map (separate from DI) */
|
|
28
|
+
const componentOverrides = new Map<string, Component>();
|
|
29
|
+
|
|
30
|
+
/** Register all default managers in a DI container */
|
|
31
|
+
function registerDefaults(c: DependencyContainer): void {
|
|
32
|
+
c.register(DI_TOKENS.EventManager, { useClass: EventManager });
|
|
33
|
+
c.register(DI_TOKENS.HistoryManager, { useClass: HistoryManager });
|
|
34
|
+
c.register(DI_TOKENS.NavigationManager, { useClass: NavigationManager });
|
|
35
|
+
c.register(DI_TOKENS.WaitManager, { useClass: WaitManager });
|
|
36
|
+
c.register(DI_TOKENS.CheckpointManager, { useClass: CheckpointManager });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CreateEngineOptions {
|
|
40
|
+
/** Project plugin for DI overrides and component overrides */
|
|
41
|
+
plugin?: VylosPlugin;
|
|
42
|
+
/** Callbacks for EventRunner (UI integration) */
|
|
43
|
+
callbacks: EventRunnerCallbacks;
|
|
44
|
+
/** Project ID for storage isolation (default: 'default') */
|
|
45
|
+
projectId?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create and wire an Engine instance with all managers.
|
|
50
|
+
* Projects can provide a plugin to override any manager.
|
|
51
|
+
*/
|
|
52
|
+
export function createEngine(options: CreateEngineOptions): Engine {
|
|
53
|
+
// Create a child container so tests/multiple instances don't conflict
|
|
54
|
+
const childContainer = globalContainer.createChildContainer();
|
|
55
|
+
|
|
56
|
+
// Register defaults
|
|
57
|
+
registerDefaults(childContainer);
|
|
58
|
+
|
|
59
|
+
// Apply plugin overrides (plugin can re-register any token)
|
|
60
|
+
if (options.plugin?.setup) {
|
|
61
|
+
options.plugin.setup(childContainer);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Register component overrides
|
|
65
|
+
if (options.plugin?.components) {
|
|
66
|
+
for (const [id, component] of Object.entries(options.plugin.components)) {
|
|
67
|
+
componentOverrides.set(id, component);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Resolve managers
|
|
72
|
+
const eventManager = childContainer.resolve<EventManager>(DI_TOKENS.EventManager);
|
|
73
|
+
const historyManager = childContainer.resolve<HistoryManager>(DI_TOKENS.HistoryManager);
|
|
74
|
+
const navigationManager = childContainer.resolve<NavigationManager>(DI_TOKENS.NavigationManager);
|
|
75
|
+
|
|
76
|
+
// EventRunner needs callbacks, so we construct it directly
|
|
77
|
+
const eventRunner = new EventRunner(options.callbacks);
|
|
78
|
+
|
|
79
|
+
// Storage + persistence managers
|
|
80
|
+
const storage = new VylosStorage(options.projectId ?? 'default');
|
|
81
|
+
const saveManager = new SaveManager(storage);
|
|
82
|
+
const settingsManager = new SettingsManager(storage);
|
|
83
|
+
|
|
84
|
+
return new Engine({
|
|
85
|
+
eventManager,
|
|
86
|
+
historyManager,
|
|
87
|
+
navigationManager,
|
|
88
|
+
eventRunner,
|
|
89
|
+
saveManager,
|
|
90
|
+
settingsManager,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Get a component override by ID (returns undefined if no override) */
|
|
95
|
+
export function getComponentOverride(id: string): Component | undefined {
|
|
96
|
+
return componentOverrides.get(id);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Clear all component overrides (for testing) */
|
|
100
|
+
export function clearComponentOverrides(): void {
|
|
101
|
+
componentOverrides.clear();
|
|
102
|
+
}
|