@vylos/core 0.3.0 → 0.4.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 +2 -3
- package/src/components/core/DialogueBox.vue +16 -4
- package/src/engine/core/CheckpointManager.ts +4 -4
- package/src/engine/core/Engine.ts +14 -8
- package/src/engine/core/EngineFactory.ts +7 -2
- package/src/engine/core/EventRunner.ts +31 -14
- package/src/engine/managers/ActionManager.ts +3 -3
- package/src/engine/managers/EventManager.ts +5 -5
- package/src/engine/managers/InventoryManager.ts +149 -0
- package/src/engine/managers/LocationManager.ts +2 -2
- package/src/engine/types/actions.ts +2 -2
- package/src/engine/types/checkpoint.ts +2 -2
- package/src/engine/types/dialogue.ts +2 -9
- package/src/engine/types/engine.ts +2 -1
- package/src/engine/types/events.ts +18 -3
- package/src/engine/types/game-state.ts +9 -6
- package/src/engine/types/index.ts +1 -0
- package/src/engine/types/inventory.ts +26 -0
- package/src/engine/types/locations.ts +3 -3
- package/src/engine/types/save.ts +3 -3
- package/src/engine/utils/deepMerge.ts +40 -0
- package/src/engine/utils/devConsole.ts +53 -0
- package/src/index.ts +5 -7
- package/src/stores/gameState.ts +9 -7
- package/tests/engine/ActionManager.test.ts +4 -3
- package/tests/engine/CheckpointManager.test.ts +4 -3
- package/tests/engine/EventManager.test.ts +4 -3
- package/tests/engine/EventRunner.test.ts +99 -9
- package/tests/engine/HistoryManager.test.ts +1 -1
- package/tests/engine/InventoryManager.test.ts +289 -0
- package/tests/engine/LocationManager.test.ts +4 -3
- package/tests/integration/game-loop.test.ts +13 -5
- package/src/engine/errors/StateValidationError.ts +0 -13
- package/src/engine/schemas/baseGameState.schema.ts +0 -19
- package/src/engine/schemas/checkpoint.schema.ts +0 -11
- package/src/engine/schemas/engineState.schema.ts +0 -59
- package/src/engine/schemas/location.schema.ts +0 -21
- package/tests/engine/schemas.test.ts +0 -250
- package/tests/safety/event-validation.test.ts +0 -102
- package/tests/safety/state-schema.test.ts +0 -96
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vylos/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/DevOpsBenjamin/Vylos"
|
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
"pinia-plugin-persistedstate": "^3.2.3",
|
|
20
20
|
"reflect-metadata": "^0.2.2",
|
|
21
21
|
"tsyringe": "^4.8.0",
|
|
22
|
-
"vue": "^3.5.13"
|
|
23
|
-
"zod": "^3.24.1"
|
|
22
|
+
"vue": "^3.5.13"
|
|
24
23
|
},
|
|
25
24
|
"devDependencies": {
|
|
26
25
|
"@vue/test-utils": "^2.4.6",
|
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
>
|
|
8
8
|
<div class="dlg-box" :class="{ 'dlg-box--history': engineState.historyBrowsing }">
|
|
9
9
|
<!-- Speaker name -->
|
|
10
|
-
<div
|
|
11
|
-
|
|
10
|
+
<div
|
|
11
|
+
v-if="engineState.dialogue.speaker"
|
|
12
|
+
class="dlg-speaker"
|
|
13
|
+
:style="engineState.dialogue.speaker.color ? { color: engineState.dialogue.speaker.color } : undefined"
|
|
14
|
+
>
|
|
15
|
+
{{ speakerName }}
|
|
12
16
|
</div>
|
|
13
17
|
|
|
14
18
|
<!-- Dialogue text -->
|
|
@@ -31,13 +35,21 @@
|
|
|
31
35
|
</template>
|
|
32
36
|
|
|
33
37
|
<script setup lang="ts">
|
|
34
|
-
import { inject } from 'vue';
|
|
38
|
+
import { inject, computed } from 'vue';
|
|
35
39
|
import { useEngineStateStore } from '../../stores/engineState';
|
|
36
40
|
import { ENGINE_INJECT_KEY } from '../../composables/useEngine';
|
|
41
|
+
import { useLanguage } from '../../composables/useLanguage';
|
|
37
42
|
import type { Engine } from '../../engine/core/Engine';
|
|
38
43
|
|
|
39
44
|
const engineState = useEngineStateStore();
|
|
40
45
|
const engine = inject<Engine>(ENGINE_INJECT_KEY);
|
|
46
|
+
const { resolveText } = useLanguage();
|
|
47
|
+
|
|
48
|
+
const speakerName = computed(() => {
|
|
49
|
+
const speaker = engineState.dialogue?.speaker;
|
|
50
|
+
if (!speaker) return '';
|
|
51
|
+
return resolveText(speaker.name);
|
|
52
|
+
});
|
|
41
53
|
|
|
42
54
|
function handleClick(): void {
|
|
43
55
|
if (!engine) return;
|
|
@@ -115,7 +127,7 @@ function handleClick(): void {
|
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
.dlg-speaker {
|
|
118
|
-
color: #fde047;
|
|
130
|
+
color: #fde047; /* default — overridden by Character.color via :style */
|
|
119
131
|
font-weight: 700;
|
|
120
132
|
font-size: 1.8cqw;
|
|
121
133
|
margin-bottom: 1cqh;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toRaw } from 'vue';
|
|
2
|
-
import type {
|
|
2
|
+
import type { VylosGameState, Checkpoint, CheckpointType, ChoiceOption } from '../types';
|
|
3
3
|
import type { DialogueState } from '../types/engine';
|
|
4
4
|
|
|
5
5
|
export interface CaptureDisplayData {
|
|
@@ -38,7 +38,7 @@ export class CheckpointManager {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** Record a checkpoint at the current interaction point */
|
|
41
|
-
capture(gameState:
|
|
41
|
+
capture(gameState: VylosGameState, type: CheckpointType, choiceResult?: string, display?: CaptureDisplayData): void {
|
|
42
42
|
const checkpoint: Checkpoint = {
|
|
43
43
|
step: this.checkpoints.length,
|
|
44
44
|
gameState: structuredClone(toRaw(gameState)),
|
|
@@ -72,7 +72,7 @@ export class CheckpointManager {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/** Get game state snapshot at a specific step */
|
|
75
|
-
getStateAt(step: number):
|
|
75
|
+
getStateAt(step: number): VylosGameState | undefined {
|
|
76
76
|
const checkpoint = this.checkpoints[step];
|
|
77
77
|
if (!checkpoint) return undefined;
|
|
78
78
|
return structuredClone(checkpoint.gameState);
|
|
@@ -84,7 +84,7 @@ export class CheckpointManager {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/** Prepare for rollback: set replay index to target step */
|
|
87
|
-
prepareRollback(targetStep: number):
|
|
87
|
+
prepareRollback(targetStep: number): VylosGameState | undefined {
|
|
88
88
|
if (targetStep < 0 || targetStep >= this.checkpoints.length) return undefined;
|
|
89
89
|
this.replayIndex = 0;
|
|
90
90
|
// Trim checkpoints after target (re-execution will recreate them)
|
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
import type { VylosEvent,
|
|
1
|
+
import type { VylosEvent, VylosGameState, Checkpoint, SaveSlot } from '../types';
|
|
2
2
|
import { EventManager } from '../managers/EventManager';
|
|
3
3
|
import { HistoryManager } from '../managers/HistoryManager';
|
|
4
|
+
import { InventoryManager } from '../managers/InventoryManager';
|
|
4
5
|
import { NavigationManager, NavigationAction } from '../managers/NavigationManager';
|
|
5
6
|
import { SaveManager } from '../managers/SaveManager';
|
|
6
7
|
import { SettingsManager } from '../managers/SettingsManager';
|
|
7
8
|
import { EventRunner } from './EventRunner';
|
|
8
9
|
import { JumpSignal } from '../errors/JumpSignal';
|
|
9
10
|
import { logger } from '../utils/logger';
|
|
11
|
+
import { attachDevConsole } from '../utils/devConsole';
|
|
10
12
|
|
|
11
13
|
export interface EngineLoopCallbacks {
|
|
12
14
|
/** Called each loop iteration (update UI: available locations, actions, background) */
|
|
13
|
-
onTick?(state:
|
|
15
|
+
onTick?(state: VylosGameState): void;
|
|
14
16
|
/** Called when player selects an action */
|
|
15
|
-
onAction?(actionId: string, state:
|
|
17
|
+
onAction?(actionId: string, state: VylosGameState): void;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface EngineDeps {
|
|
19
21
|
eventManager: EventManager;
|
|
20
22
|
historyManager: HistoryManager;
|
|
23
|
+
inventoryManager: InventoryManager;
|
|
21
24
|
navigationManager: NavigationManager;
|
|
22
25
|
eventRunner: EventRunner;
|
|
23
26
|
saveManager: SaveManager;
|
|
@@ -31,6 +34,7 @@ export interface EngineDeps {
|
|
|
31
34
|
export class Engine {
|
|
32
35
|
readonly eventManager: EventManager;
|
|
33
36
|
readonly historyManager: HistoryManager;
|
|
37
|
+
readonly inventoryManager: InventoryManager;
|
|
34
38
|
readonly navigationManager: NavigationManager;
|
|
35
39
|
readonly eventRunner: EventRunner;
|
|
36
40
|
readonly saveManager: SaveManager;
|
|
@@ -41,7 +45,7 @@ export class Engine {
|
|
|
41
45
|
private pendingResume: {
|
|
42
46
|
eventId: string;
|
|
43
47
|
checkpoints: Checkpoint[];
|
|
44
|
-
initialState:
|
|
48
|
+
initialState: VylosGameState;
|
|
45
49
|
} | null = null;
|
|
46
50
|
|
|
47
51
|
/** Flag to skip event lock/push when interrupted by a load */
|
|
@@ -50,6 +54,7 @@ export class Engine {
|
|
|
50
54
|
constructor(deps: EngineDeps) {
|
|
51
55
|
this.eventManager = deps.eventManager;
|
|
52
56
|
this.historyManager = deps.historyManager;
|
|
57
|
+
this.inventoryManager = deps.inventoryManager;
|
|
53
58
|
this.navigationManager = deps.navigationManager;
|
|
54
59
|
this.eventRunner = deps.eventRunner;
|
|
55
60
|
this.saveManager = deps.saveManager;
|
|
@@ -57,9 +62,10 @@ export class Engine {
|
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/** Register events and start the game loop */
|
|
60
|
-
async run(events: VylosEvent[], getState: () =>
|
|
65
|
+
async run(events: VylosEvent[], getState: () => VylosGameState, loop?: EngineLoopCallbacks): Promise<void> {
|
|
61
66
|
this.eventManager.registerAll(events);
|
|
62
67
|
this.running = true;
|
|
68
|
+
attachDevConsole(this, getState);
|
|
63
69
|
|
|
64
70
|
logger.info('Engine started');
|
|
65
71
|
|
|
@@ -131,7 +137,7 @@ export class Engine {
|
|
|
131
137
|
* Load a save and resume execution.
|
|
132
138
|
* Restores game state, history, event lock state, and sets up mid-event resume if needed.
|
|
133
139
|
*/
|
|
134
|
-
loadSave(saveData: SaveSlot, setState: (state:
|
|
140
|
+
loadSave(saveData: SaveSlot, setState: (state: VylosGameState) => void): void {
|
|
135
141
|
// Restore game state
|
|
136
142
|
setState(JSON.parse(JSON.stringify(saveData.gameState)));
|
|
137
143
|
|
|
@@ -164,7 +170,7 @@ export class Engine {
|
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
/** Execute a single event, handling jumps */
|
|
167
|
-
private async executeEvent(event: VylosEvent, getState: () =>
|
|
173
|
+
private async executeEvent(event: VylosEvent, getState: () => VylosGameState): Promise<void> {
|
|
168
174
|
let currentEvent: VylosEvent | undefined = event;
|
|
169
175
|
|
|
170
176
|
while (currentEvent) {
|
|
@@ -208,7 +214,7 @@ export class Engine {
|
|
|
208
214
|
}
|
|
209
215
|
|
|
210
216
|
/** Handle resuming a mid-event save */
|
|
211
|
-
private async handleResume(getState: () =>
|
|
217
|
+
private async handleResume(getState: () => VylosGameState): Promise<void> {
|
|
212
218
|
const resume = this.pendingResume!;
|
|
213
219
|
this.pendingResume = null;
|
|
214
220
|
|
|
@@ -10,6 +10,7 @@ import { SaveManager } from '../managers/SaveManager';
|
|
|
10
10
|
import { SettingsManager } from '../managers/SettingsManager';
|
|
11
11
|
import { EventRunner, type EventRunnerCallbacks } from './EventRunner';
|
|
12
12
|
import { CheckpointManager } from './CheckpointManager';
|
|
13
|
+
import { InventoryManager } from '../managers/InventoryManager';
|
|
13
14
|
import { VylosStorage } from '../storage/VylosStorage';
|
|
14
15
|
import { Engine } from './Engine';
|
|
15
16
|
|
|
@@ -20,6 +21,7 @@ export const DI_TOKENS = {
|
|
|
20
21
|
NavigationManager: 'NavigationManager',
|
|
21
22
|
WaitManager: 'WaitManager',
|
|
22
23
|
CheckpointManager: 'CheckpointManager',
|
|
24
|
+
InventoryManager: 'InventoryManager',
|
|
23
25
|
EventRunner: 'EventRunner',
|
|
24
26
|
Engine: 'Engine',
|
|
25
27
|
} as const;
|
|
@@ -34,6 +36,7 @@ function registerDefaults(c: DependencyContainer): void {
|
|
|
34
36
|
c.register(DI_TOKENS.NavigationManager, { useClass: NavigationManager });
|
|
35
37
|
c.register(DI_TOKENS.WaitManager, { useClass: WaitManager });
|
|
36
38
|
c.register(DI_TOKENS.CheckpointManager, { useClass: CheckpointManager });
|
|
39
|
+
c.register(DI_TOKENS.InventoryManager, { useClass: InventoryManager });
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
export interface CreateEngineOptions {
|
|
@@ -72,9 +75,10 @@ export function createEngine(options: CreateEngineOptions): Engine {
|
|
|
72
75
|
const eventManager = childContainer.resolve<EventManager>(DI_TOKENS.EventManager);
|
|
73
76
|
const historyManager = childContainer.resolve<HistoryManager>(DI_TOKENS.HistoryManager);
|
|
74
77
|
const navigationManager = childContainer.resolve<NavigationManager>(DI_TOKENS.NavigationManager);
|
|
78
|
+
const inventoryManager = childContainer.resolve<InventoryManager>(DI_TOKENS.InventoryManager);
|
|
75
79
|
|
|
76
|
-
// EventRunner needs callbacks, so we construct it directly
|
|
77
|
-
const eventRunner = new EventRunner(options.callbacks);
|
|
80
|
+
// EventRunner needs callbacks + inventoryManager, so we construct it directly
|
|
81
|
+
const eventRunner = new EventRunner(options.callbacks, inventoryManager);
|
|
78
82
|
|
|
79
83
|
// Storage + persistence managers
|
|
80
84
|
const storage = new VylosStorage(options.projectId ?? 'default');
|
|
@@ -88,6 +92,7 @@ export function createEngine(options: CreateEngineOptions): Engine {
|
|
|
88
92
|
eventRunner,
|
|
89
93
|
saveManager,
|
|
90
94
|
settingsManager,
|
|
95
|
+
inventoryManager,
|
|
91
96
|
});
|
|
92
97
|
}
|
|
93
98
|
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { toRaw } from 'vue';
|
|
2
2
|
import type {
|
|
3
3
|
VylosAPI,
|
|
4
|
+
InventoryAPI,
|
|
4
5
|
VylosEvent,
|
|
5
|
-
|
|
6
|
+
VylosGameState,
|
|
6
7
|
TextEntry,
|
|
7
8
|
SayOptions,
|
|
8
9
|
ChoiceItem,
|
|
9
10
|
CheckpointType,
|
|
10
11
|
ChoiceOption,
|
|
11
12
|
DialogueState,
|
|
13
|
+
VylosCharacter,
|
|
12
14
|
} from '../types';
|
|
15
|
+
import { InventoryManager } from '../managers/InventoryManager';
|
|
13
16
|
import { CheckpointManager } from './CheckpointManager';
|
|
14
17
|
import { WaitManager } from '../managers/WaitManager';
|
|
15
18
|
import { JumpSignal } from '../errors/JumpSignal';
|
|
@@ -20,7 +23,7 @@ import { interpolate } from '../utils/TimeHelper';
|
|
|
20
23
|
|
|
21
24
|
export interface EventRunnerCallbacks {
|
|
22
25
|
/** Called when dialogue should be displayed */
|
|
23
|
-
onSay(text: string, speaker:
|
|
26
|
+
onSay(text: string, speaker: VylosCharacter | null): void;
|
|
24
27
|
/** Called when choices should be displayed */
|
|
25
28
|
onChoice(options: Array<{ text: string; value: string; disabled?: boolean }>): void;
|
|
26
29
|
/** Called to update background */
|
|
@@ -38,9 +41,9 @@ export interface EventRunnerCallbacks {
|
|
|
38
41
|
/** Resolve a TextEntry to a string using current language */
|
|
39
42
|
resolveText(entry: string | TextEntry): string;
|
|
40
43
|
/** Get current game state */
|
|
41
|
-
getState():
|
|
44
|
+
getState(): VylosGameState;
|
|
42
45
|
/** Set game state (after rollback) */
|
|
43
|
-
setState(state:
|
|
46
|
+
setState(state: VylosGameState): void;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/** Data returned when browsing history steps */
|
|
@@ -71,22 +74,39 @@ export class EventRunner implements VylosAPI {
|
|
|
71
74
|
/** History browsing index (-1 = live, not browsing) */
|
|
72
75
|
private browseIndex = -1;
|
|
73
76
|
/** The live dialogue being displayed when history browsing started */
|
|
74
|
-
private liveDialogue: { text: string; speaker:
|
|
77
|
+
private liveDialogue: { text: string; speaker: VylosCharacter | null } | null = null;
|
|
75
78
|
/** Current background path (tracked for checkpoint storage) */
|
|
76
79
|
private currentBackground: string | null = null;
|
|
77
80
|
|
|
78
81
|
/** Snapshot of game state before event started (for redo) */
|
|
79
|
-
private initialState:
|
|
82
|
+
private initialState: VylosGameState | null = null;
|
|
80
83
|
/** Reference to the currently executing event (for redo) */
|
|
81
84
|
private currentEvent: VylosEvent | null = null;
|
|
82
85
|
/** Pending redo request (set by UI, consumed by redo loop) */
|
|
83
86
|
private pendingRedo: { step: number; choice: string } | null = null;
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
private inventoryManager: InventoryManager;
|
|
89
|
+
|
|
90
|
+
constructor(callbacks: EventRunnerCallbacks, inventoryManager: InventoryManager) {
|
|
86
91
|
this.callbacks = callbacks;
|
|
92
|
+
this.inventoryManager = inventoryManager;
|
|
87
93
|
this.checkpoints = new CheckpointManager();
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
get inventory(): InventoryAPI {
|
|
97
|
+
const inv = () => this.callbacks.getState().inventories;
|
|
98
|
+
const im = this.inventoryManager;
|
|
99
|
+
return {
|
|
100
|
+
add: (bag, itemId, qty) => im.add(inv(), bag, itemId, qty),
|
|
101
|
+
remove: (bag, itemId, qty) => im.remove(inv(), bag, itemId, qty),
|
|
102
|
+
has: (bag, itemId, qty) => im.has(inv(), bag, itemId, qty),
|
|
103
|
+
hasAll: (bag, items) => im.hasAll(inv(), bag, items),
|
|
104
|
+
count: (bag, itemId) => im.count(inv(), bag, itemId),
|
|
105
|
+
list: (bag) => im.list(inv(), bag),
|
|
106
|
+
clear: (bag) => im.clearBag(inv(), bag),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
/** Whether the player is browsing text history */
|
|
91
111
|
get isBrowsingHistory(): boolean {
|
|
92
112
|
return this.browseIndex >= 0;
|
|
@@ -98,12 +118,12 @@ export class EventRunner implements VylosAPI {
|
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
/** Get initial state snapshot for save (deep clone) */
|
|
101
|
-
getInitialState():
|
|
121
|
+
getInitialState(): VylosGameState | null {
|
|
102
122
|
return this.initialState ? structuredClone(this.initialState) : null;
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
/** Get the live dialogue for restoring display after exiting history */
|
|
106
|
-
getLiveDialogue(): { text: string; speaker:
|
|
126
|
+
getLiveDialogue(): { text: string; speaker: VylosCharacter | null } | null {
|
|
107
127
|
return this.liveDialogue;
|
|
108
128
|
}
|
|
109
129
|
|
|
@@ -207,7 +227,7 @@ export class EventRunner implements VylosAPI {
|
|
|
207
227
|
}
|
|
208
228
|
|
|
209
229
|
/** Resume a saved mid-event execution (for load). Checkpoints must be restored externally first. */
|
|
210
|
-
async resumeEvent(event: VylosEvent, savedInitialState:
|
|
230
|
+
async resumeEvent(event: VylosEvent, savedInitialState: VylosGameState): Promise<void> {
|
|
211
231
|
this.initialState = structuredClone(savedInitialState);
|
|
212
232
|
this.currentEvent = event;
|
|
213
233
|
this.currentStep = 0;
|
|
@@ -248,10 +268,7 @@ export class EventRunner implements VylosAPI {
|
|
|
248
268
|
}
|
|
249
269
|
|
|
250
270
|
// Resolve speaker
|
|
251
|
-
|
|
252
|
-
if (options?.from) {
|
|
253
|
-
speaker = this.callbacks.resolveText(options.from);
|
|
254
|
-
}
|
|
271
|
+
const speaker: VylosCharacter | null = options?.from ?? null;
|
|
255
272
|
|
|
256
273
|
// If replaying, fast-forward: capture checkpoint and resolve immediately
|
|
257
274
|
if (this.checkpoints.isReplaying) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VylosAction,
|
|
1
|
+
import type { VylosAction, VylosGameState, TextEntry } from '../types';
|
|
2
2
|
import { logger } from '../utils/logger';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -26,7 +26,7 @@ export class ActionManager {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Get all available actions for a location, filtered by unlock conditions */
|
|
29
|
-
getAvailable(locationId: string, state:
|
|
29
|
+
getAvailable(locationId: string, state: VylosGameState): VylosAction[] {
|
|
30
30
|
const available: VylosAction[] = [];
|
|
31
31
|
|
|
32
32
|
for (const action of this.actions.values()) {
|
|
@@ -43,7 +43,7 @@ export class ActionManager {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** Execute an action by ID */
|
|
46
|
-
execute(id: string, state:
|
|
46
|
+
execute(id: string, state: VylosGameState): boolean {
|
|
47
47
|
const action = this.actions.get(id);
|
|
48
48
|
if (!action) {
|
|
49
49
|
logger.warn(`Action not found: ${id}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VylosEvent,
|
|
1
|
+
import type { VylosEvent, VylosGameState, DrawableEventEntry } from '../types';
|
|
2
2
|
import { EventStatus } from '../types';
|
|
3
3
|
import { logger } from '../utils/logger';
|
|
4
4
|
|
|
@@ -46,7 +46,7 @@ export class EventManager {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/** Evaluate conditions and update statuses. Returns newly unlocked events. */
|
|
49
|
-
evaluate(state:
|
|
49
|
+
evaluate(state: VylosGameState): VylosEvent[] {
|
|
50
50
|
const unlocked: VylosEvent[] = [];
|
|
51
51
|
|
|
52
52
|
for (const [id, entry] of this.events) {
|
|
@@ -68,7 +68,7 @@ export class EventManager {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/** Get the next unlocked event matching the current location (first registered wins). Skips drawable events. */
|
|
71
|
-
getNextUnlocked(state:
|
|
71
|
+
getNextUnlocked(state: VylosGameState): VylosEvent | undefined {
|
|
72
72
|
for (const entry of this.events.values()) {
|
|
73
73
|
if (entry.status !== EventStatus.Unlocked) continue;
|
|
74
74
|
if (entry.event.draw) continue; // Drawable events don't auto-trigger
|
|
@@ -79,7 +79,7 @@ export class EventManager {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/** Get all unlocked drawable events at the current location */
|
|
82
|
-
getDrawableEvents(state:
|
|
82
|
+
getDrawableEvents(state: VylosGameState, resolveText?: (text: string | Record<string, string>) => string): DrawableEventEntry[] {
|
|
83
83
|
const result: DrawableEventEntry[] = [];
|
|
84
84
|
for (const entry of this.events.values()) {
|
|
85
85
|
if (entry.status !== EventStatus.Unlocked) continue;
|
|
@@ -108,7 +108,7 @@ export class EventManager {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/** Mark event as locked (completed) */
|
|
111
|
-
setLocked(id: string, state:
|
|
111
|
+
setLocked(id: string, state: VylosGameState): void {
|
|
112
112
|
const entry = this.events.get(id);
|
|
113
113
|
if (entry) {
|
|
114
114
|
entry.status = EventStatus.Locked;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { VylosCategory, VylosItem, InventoryData } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages item/category definitions and inventory operations.
|
|
5
|
+
*
|
|
6
|
+
* No constructor args — compatible with tsyringe `{ useClass }` DI resolution.
|
|
7
|
+
* Projects can extend this class and override any method via the plugin system.
|
|
8
|
+
*/
|
|
9
|
+
export class InventoryManager {
|
|
10
|
+
private categories = new Map<string, VylosCategory>();
|
|
11
|
+
private items = new Map<string, VylosItem>();
|
|
12
|
+
|
|
13
|
+
// --- Category registry ---
|
|
14
|
+
|
|
15
|
+
registerCategory(cat: VylosCategory): void {
|
|
16
|
+
this.categories.set(cat.id, cat);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
registerCategories(cats: VylosCategory[]): void {
|
|
20
|
+
for (const cat of cats) {
|
|
21
|
+
this.registerCategory(cat);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getCategory(id: string): VylosCategory | undefined {
|
|
26
|
+
return this.categories.get(id);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Returns all categories sorted by sortOrder (ascending, undefined last) */
|
|
30
|
+
getCategories(): VylosCategory[] {
|
|
31
|
+
return [...this.categories.values()].sort((a, b) => {
|
|
32
|
+
const sa = a.sortOrder ?? Infinity;
|
|
33
|
+
const sb = b.sortOrder ?? Infinity;
|
|
34
|
+
return sa - sb;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Item registry ---
|
|
39
|
+
|
|
40
|
+
registerItem(item: VylosItem): void {
|
|
41
|
+
this.items.set(item.id, item);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
registerItems(items: VylosItem[]): void {
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
this.registerItem(item);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getItem(id: string): VylosItem | undefined {
|
|
51
|
+
return this.items.get(id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getItemsByCategory(categoryId: string): VylosItem[] {
|
|
55
|
+
return [...this.items.values()].filter((i) => i.category === categoryId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Inventory operations (on state.inventories) ---
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add items to a bag. Auto-creates bag if missing.
|
|
62
|
+
* Respects maxStack from the item definition.
|
|
63
|
+
* @returns actual quantity added
|
|
64
|
+
*/
|
|
65
|
+
add(inv: InventoryData, bag: string, itemId: string, qty = 1): number {
|
|
66
|
+
if (qty <= 0) return 0;
|
|
67
|
+
|
|
68
|
+
if (!inv[bag]) inv[bag] = {};
|
|
69
|
+
|
|
70
|
+
const current = inv[bag][itemId] ?? 0;
|
|
71
|
+
const item = this.items.get(itemId);
|
|
72
|
+
const maxStack = item?.maxStack ?? Infinity;
|
|
73
|
+
|
|
74
|
+
// Check maxSlots: if the item isn't already in the bag and we'd exceed slot count
|
|
75
|
+
if (current === 0 && item) {
|
|
76
|
+
const category = this.categories.get(item.category);
|
|
77
|
+
if (category?.maxSlots !== undefined) {
|
|
78
|
+
const slotCount = Object.keys(inv[bag]).length;
|
|
79
|
+
if (slotCount >= category.maxSlots) return 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const actual = Math.min(qty, maxStack - current);
|
|
84
|
+
if (actual <= 0) return 0;
|
|
85
|
+
|
|
86
|
+
inv[bag][itemId] = current + actual;
|
|
87
|
+
return actual;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Remove items from a bag. Cleans up zero entries.
|
|
92
|
+
* @returns actual quantity removed
|
|
93
|
+
*/
|
|
94
|
+
remove(inv: InventoryData, bag: string, itemId: string, qty = 1): number {
|
|
95
|
+
if (qty <= 0) return 0;
|
|
96
|
+
if (!inv[bag]) return 0;
|
|
97
|
+
|
|
98
|
+
const current = inv[bag][itemId] ?? 0;
|
|
99
|
+
if (current <= 0) return 0;
|
|
100
|
+
|
|
101
|
+
const actual = Math.min(qty, current);
|
|
102
|
+
const remaining = current - actual;
|
|
103
|
+
|
|
104
|
+
if (remaining <= 0) {
|
|
105
|
+
delete inv[bag][itemId];
|
|
106
|
+
} else {
|
|
107
|
+
inv[bag][itemId] = remaining;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return actual;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Get the quantity of an item in a bag */
|
|
114
|
+
count(inv: InventoryData, bag: string, itemId: string): number {
|
|
115
|
+
return inv[bag]?.[itemId] ?? 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Check if a bag has at least `qty` of an item */
|
|
119
|
+
has(inv: InventoryData, bag: string, itemId: string, qty = 1): boolean {
|
|
120
|
+
return this.count(inv, bag, itemId) >= qty;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Check if a bag has all the specified items in required quantities */
|
|
124
|
+
hasAll(inv: InventoryData, bag: string, items: Record<string, number>): boolean {
|
|
125
|
+
for (const [itemId, qty] of Object.entries(items)) {
|
|
126
|
+
if (!this.has(inv, bag, itemId, qty)) return false;
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** List all items in a bag as [itemId, quantity] pairs */
|
|
132
|
+
list(inv: InventoryData, bag: string): Array<[string, number]> {
|
|
133
|
+
if (!inv[bag]) return [];
|
|
134
|
+
return Object.entries(inv[bag]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Clear all items from a bag */
|
|
138
|
+
clearBag(inv: InventoryData, bag: string): void {
|
|
139
|
+
delete inv[bag];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Cleanup ---
|
|
143
|
+
|
|
144
|
+
/** Clear all registered categories and items */
|
|
145
|
+
clear(): void {
|
|
146
|
+
this.categories.clear();
|
|
147
|
+
this.items.clear();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VylosLocation, LocationLink,
|
|
1
|
+
import type { VylosLocation, LocationLink, VylosGameState, BackgroundEntry } from '../types';
|
|
2
2
|
import { resolveBackground } from '../utils/TimeHelper';
|
|
3
3
|
import { logger } from '../utils/logger';
|
|
4
4
|
|
|
@@ -38,7 +38,7 @@ export class LocationManager {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** Get locations accessible from a given location, considering state conditions */
|
|
41
|
-
getAccessibleFrom(locationId: string, state:
|
|
41
|
+
getAccessibleFrom(locationId: string, state: VylosGameState): VylosLocation[] {
|
|
42
42
|
const linkedIds = this.links
|
|
43
43
|
.filter(link => {
|
|
44
44
|
if (link.from !== locationId) return false;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { VylosGameState } from './game-state';
|
|
2
2
|
import type { TextEntry } from './events';
|
|
3
3
|
|
|
4
4
|
/** An action available to the player at a location */
|
|
5
|
-
export interface VylosAction<TState extends
|
|
5
|
+
export interface VylosAction<TState extends VylosGameState = VylosGameState> {
|
|
6
6
|
/** Unique action ID */
|
|
7
7
|
id: string;
|
|
8
8
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { VylosGameState } from './game-state';
|
|
2
2
|
import type { DialogueState, ChoiceOption } from './engine';
|
|
3
3
|
|
|
4
4
|
/** A checkpoint captured at each interaction point */
|
|
@@ -6,7 +6,7 @@ export interface Checkpoint {
|
|
|
6
6
|
/** Sequential step number within the event */
|
|
7
7
|
step: number;
|
|
8
8
|
/** Deep clone of game state at this point */
|
|
9
|
-
gameState:
|
|
9
|
+
gameState: VylosGameState;
|
|
10
10
|
/** Type of interaction at this step */
|
|
11
11
|
type: CheckpointType;
|
|
12
12
|
/** Stored choice result (for replay during rollback) */
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import type { TextEntry } from './events';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
export interface
|
|
5
|
-
text: string;
|
|
6
|
-
speaker: string | null;
|
|
7
|
-
isNarration: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Character definition for speaker resolution */
|
|
11
|
-
export interface Character {
|
|
3
|
+
/** Base character — projects extend this into their own Character */
|
|
4
|
+
export interface VylosCharacter {
|
|
12
5
|
id: string;
|
|
13
6
|
name: string | TextEntry;
|
|
14
7
|
color?: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DrawableEventEntry } from './events';
|
|
2
|
+
import type { VylosCharacter } from './dialogue';
|
|
2
3
|
|
|
3
4
|
/** Engine lifecycle phases */
|
|
4
5
|
export enum EnginePhase {
|
|
@@ -32,7 +33,7 @@ export interface EngineState {
|
|
|
32
33
|
|
|
33
34
|
export interface DialogueState {
|
|
34
35
|
text: string;
|
|
35
|
-
speaker:
|
|
36
|
+
speaker: VylosCharacter | null;
|
|
36
37
|
isNarration: boolean;
|
|
37
38
|
}
|
|
38
39
|
|