@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,32 @@
|
|
|
1
|
+
import type { BaseGameState } from './game-state';
|
|
2
|
+
import type { TextEntry } from './events';
|
|
3
|
+
|
|
4
|
+
/** Background resolution entry — time-based or default */
|
|
5
|
+
export interface BackgroundEntry {
|
|
6
|
+
path: string;
|
|
7
|
+
/** Time range in game hours, e.g. [6, 18] for daytime */
|
|
8
|
+
timeRange?: [number, number];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Location definition */
|
|
12
|
+
export interface VylosLocation {
|
|
13
|
+
/** Unique location ID */
|
|
14
|
+
id: string;
|
|
15
|
+
|
|
16
|
+
/** Display name */
|
|
17
|
+
name: string | TextEntry;
|
|
18
|
+
|
|
19
|
+
/** Background images (resolved by time of day) */
|
|
20
|
+
backgrounds: BackgroundEntry[];
|
|
21
|
+
|
|
22
|
+
/** Whether this location is accessible — defaults to true */
|
|
23
|
+
accessible?<TState extends BaseGameState>(state: TState): boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Location link (navigation graph edge) */
|
|
27
|
+
export interface LocationLink {
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
/** Whether this link is currently traversable */
|
|
31
|
+
condition?<TState extends BaseGameState>(state: TState): boolean;
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DependencyContainer } from 'tsyringe';
|
|
2
|
+
import type { Component } from 'vue';
|
|
3
|
+
|
|
4
|
+
/** Plugin interface for project-level engine customization */
|
|
5
|
+
export interface VylosPlugin {
|
|
6
|
+
/** Register DI overrides (custom managers, etc.) */
|
|
7
|
+
setup?(container: DependencyContainer): void;
|
|
8
|
+
|
|
9
|
+
/** Override Vue components by component ID */
|
|
10
|
+
components?: Record<string, Component>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BaseGameState } from './game-state';
|
|
2
|
+
import type { Checkpoint } from './checkpoint';
|
|
3
|
+
|
|
4
|
+
/** A single save slot */
|
|
5
|
+
export interface SaveSlot {
|
|
6
|
+
/** Slot number */
|
|
7
|
+
slot: number;
|
|
8
|
+
/** When this save was created */
|
|
9
|
+
timestamp: number;
|
|
10
|
+
/** Save format version for migration */
|
|
11
|
+
version: number;
|
|
12
|
+
/** Game state snapshot */
|
|
13
|
+
gameState: BaseGameState;
|
|
14
|
+
/** Current event ID (if mid-event) */
|
|
15
|
+
eventId: string | null;
|
|
16
|
+
/** Checkpoint step number (if mid-event) */
|
|
17
|
+
stepNumber: number;
|
|
18
|
+
/** Player-visible label */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Screenshot thumbnail (data URL) */
|
|
21
|
+
thumbnail: string | null;
|
|
22
|
+
/** Checkpoints for mid-event resume */
|
|
23
|
+
checkpoints?: Checkpoint[];
|
|
24
|
+
/** Game state before event started (for redo support after load) */
|
|
25
|
+
initialState?: BaseGameState;
|
|
26
|
+
/** Completed event history */
|
|
27
|
+
history?: Array<{ eventId: string; checkpoints: Checkpoint[] }>;
|
|
28
|
+
/** Current history navigation index */
|
|
29
|
+
historyIndex?: number;
|
|
30
|
+
/** IDs of events that were locked (completed) at save time */
|
|
31
|
+
lockedEventIds?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Save metadata (displayed in save/load menu without loading full state) */
|
|
35
|
+
export interface SaveMeta {
|
|
36
|
+
slot: number;
|
|
37
|
+
timestamp: number;
|
|
38
|
+
label: string;
|
|
39
|
+
thumbnail: string | null;
|
|
40
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { BackgroundEntry } from '../types';
|
|
2
|
+
|
|
3
|
+
/** Resolve the correct background for a given game time */
|
|
4
|
+
export function resolveBackground(backgrounds: BackgroundEntry[], gameTime: number): string | null {
|
|
5
|
+
if (backgrounds.length === 0) return null;
|
|
6
|
+
|
|
7
|
+
// Find time-specific background first
|
|
8
|
+
for (const bg of backgrounds) {
|
|
9
|
+
if (bg.timeRange) {
|
|
10
|
+
const [start, end] = bg.timeRange;
|
|
11
|
+
const hour = gameTime % 24;
|
|
12
|
+
if (start <= end) {
|
|
13
|
+
// Normal range (e.g., 6-18)
|
|
14
|
+
if (hour >= start && hour < end) return bg.path;
|
|
15
|
+
} else {
|
|
16
|
+
// Wrapping range (e.g., 22-6)
|
|
17
|
+
if (hour >= start || hour < end) return bg.path;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Fall back to first entry without time range, or first entry overall
|
|
23
|
+
const defaultBg = backgrounds.find(bg => !bg.timeRange) ?? backgrounds[0];
|
|
24
|
+
return defaultBg.path;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Format game time as HH:MM string */
|
|
28
|
+
export function formatGameTime(gameTime: number): string {
|
|
29
|
+
const hours = Math.floor(gameTime % 24);
|
|
30
|
+
const minutes = Math.floor((gameTime % 1) * 60);
|
|
31
|
+
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Interpolate variables in text: "Hello {name}!" → "Hello Alice!" */
|
|
35
|
+
export function interpolate(text: string, variables: Record<string, string | number>): string {
|
|
36
|
+
return text.replace(/\{(\w+)\}/g, (match, key: string) => {
|
|
37
|
+
return key in variables ? String(variables[key]) : match;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export enum LogLevel {
|
|
2
|
+
Debug = 0,
|
|
3
|
+
Info = 1,
|
|
4
|
+
Warn = 2,
|
|
5
|
+
Error = 3,
|
|
6
|
+
Silent = 4,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let currentLevel: LogLevel = typeof import.meta !== 'undefined' &&
|
|
10
|
+
(import.meta as Record<string, unknown>).env &&
|
|
11
|
+
(import.meta as Record<string, Record<string, unknown>>).env.DEV
|
|
12
|
+
? LogLevel.Debug
|
|
13
|
+
: LogLevel.Warn;
|
|
14
|
+
|
|
15
|
+
export const logger = {
|
|
16
|
+
setLevel(level: LogLevel) {
|
|
17
|
+
currentLevel = level;
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
debug(...args: unknown[]) {
|
|
21
|
+
if (currentLevel <= LogLevel.Debug) {
|
|
22
|
+
console.debug('[Vylos]', ...args);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
info(...args: unknown[]) {
|
|
27
|
+
if (currentLevel <= LogLevel.Info) {
|
|
28
|
+
console.info('[Vylos]', ...args);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
warn(...args: unknown[]) {
|
|
33
|
+
if (currentLevel <= LogLevel.Warn) {
|
|
34
|
+
console.warn('[Vylos]', ...args);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
error(...args: unknown[]) {
|
|
39
|
+
if (currentLevel <= LogLevel.Error) {
|
|
40
|
+
console.error('[Vylos]', ...args);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
declare module '*.vue' {
|
|
4
|
+
import type { DefineComponent } from 'vue';
|
|
5
|
+
const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>;
|
|
6
|
+
export default component;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare module 'vylos:project' {
|
|
10
|
+
export const config: Record<string, unknown>;
|
|
11
|
+
export const plugin: import('./engine/types/plugin').VylosPlugin | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module 'vylos:texts/*' {
|
|
15
|
+
const texts: Record<string, Record<string, string>>;
|
|
16
|
+
export default texts;
|
|
17
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
|
|
3
|
+
// Types
|
|
4
|
+
export * from './engine/types';
|
|
5
|
+
|
|
6
|
+
// Schemas
|
|
7
|
+
export { baseGameStateSchema, extendGameStateSchema } from './engine/schemas/baseGameState.schema';
|
|
8
|
+
export { engineStateSchema, engineSettingsSchema } from './engine/schemas/engineState.schema';
|
|
9
|
+
export { checkpointSchema } from './engine/schemas/checkpoint.schema';
|
|
10
|
+
export { locationSchema, locationLinkSchema } from './engine/schemas/location.schema';
|
|
11
|
+
|
|
12
|
+
// Errors
|
|
13
|
+
export { JumpSignal } from './engine/errors/JumpSignal';
|
|
14
|
+
export { EventEndError } from './engine/errors/EventEndError';
|
|
15
|
+
export { InterruptSignal } from './engine/errors/InterruptSignal';
|
|
16
|
+
export { StateValidationError } from './engine/errors/StateValidationError';
|
|
17
|
+
|
|
18
|
+
// Utils
|
|
19
|
+
export { logger, LogLevel } from './engine/utils/logger';
|
|
20
|
+
export { resolveBackground, formatGameTime, interpolate } from './engine/utils/TimeHelper';
|
|
21
|
+
|
|
22
|
+
// Stores
|
|
23
|
+
export { useEngineStateStore } from './stores/engineState';
|
|
24
|
+
export { useGameStateStore } from './stores/gameState';
|
|
25
|
+
|
|
26
|
+
// Core engine
|
|
27
|
+
export { Engine } from './engine/core/Engine';
|
|
28
|
+
export type { EngineLoopCallbacks } from './engine/core/Engine';
|
|
29
|
+
export { EventRunner } from './engine/core/EventRunner';
|
|
30
|
+
export type { EventRunnerCallbacks, HistoryStep } from './engine/core/EventRunner';
|
|
31
|
+
export { CheckpointManager } from './engine/core/CheckpointManager';
|
|
32
|
+
export { createEngine, getComponentOverride, clearComponentOverrides, DI_TOKENS } from './engine/core/EngineFactory';
|
|
33
|
+
|
|
34
|
+
// Managers
|
|
35
|
+
export { EventManager } from './engine/managers/EventManager';
|
|
36
|
+
export { HistoryManager } from './engine/managers/HistoryManager';
|
|
37
|
+
export { NavigationManager, NavigationAction } from './engine/managers/NavigationManager';
|
|
38
|
+
export { WaitManager } from './engine/managers/WaitManager';
|
|
39
|
+
export { LocationManager } from './engine/managers/LocationManager';
|
|
40
|
+
export { ActionManager } from './engine/managers/ActionManager';
|
|
41
|
+
export { SaveManager } from './engine/managers/SaveManager';
|
|
42
|
+
export { SettingsManager } from './engine/managers/SettingsManager';
|
|
43
|
+
export { LanguageManager } from './engine/managers/LanguageManager';
|
|
44
|
+
export { InputManager } from './engine/managers/InputManager';
|
|
45
|
+
|
|
46
|
+
// Storage
|
|
47
|
+
export { VylosStorage } from './engine/storage/VylosStorage';
|
|
48
|
+
|
|
49
|
+
// Composables
|
|
50
|
+
export { useEngine, ENGINE_INJECT_KEY } from './composables/useEngine';
|
|
51
|
+
export { useGameState } from './composables/useGameState';
|
|
52
|
+
export { useLanguage } from './composables/useLanguage';
|
|
53
|
+
export { CONFIG_INJECT_KEY } from './composables/useConfig';
|
|
54
|
+
|
|
55
|
+
// Components — app
|
|
56
|
+
export { default as GameShell } from './components/app/GameShell.vue';
|
|
57
|
+
export { default as EngineView } from './components/app/EngineView.vue';
|
|
58
|
+
export { default as LoadingScreen } from './components/app/LoadingScreen.vue';
|
|
59
|
+
export { default as MainMenu } from './components/app/MainMenu.vue';
|
|
60
|
+
|
|
61
|
+
// Components — core
|
|
62
|
+
export { default as BackgroundLayer } from './components/core/BackgroundLayer.vue';
|
|
63
|
+
export { default as ForegroundLayer } from './components/core/ForegroundLayer.vue';
|
|
64
|
+
export { default as DialogueBox } from './components/core/DialogueBox.vue';
|
|
65
|
+
export { default as ChoicePanel } from './components/core/ChoicePanel.vue';
|
|
66
|
+
export { default as CustomOverlay } from './components/core/CustomOverlay.vue';
|
|
67
|
+
export { default as DrawableOverlay } from './components/core/DrawableOverlay.vue';
|
|
68
|
+
|
|
69
|
+
// Components — menu
|
|
70
|
+
export { default as ActionOverlay } from './components/menu/ActionOverlay.vue';
|
|
71
|
+
export { default as LocationOverlay } from './components/menu/LocationOverlay.vue';
|
|
72
|
+
export { default as TopBar } from './components/menu/TopBar.vue';
|
|
73
|
+
export { default as SaveLoadMenu } from './components/menu/SaveLoadMenu.vue';
|
|
74
|
+
export { default as SettingsMenu } from './components/menu/SettingsMenu.vue';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { defineStore } from 'pinia';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import {
|
|
4
|
+
EnginePhase,
|
|
5
|
+
MenuType,
|
|
6
|
+
type DialogueState,
|
|
7
|
+
type ChoiceState,
|
|
8
|
+
type ActionEntry,
|
|
9
|
+
type LocationEntry,
|
|
10
|
+
type DrawableEventEntry,
|
|
11
|
+
} from '../engine/types';
|
|
12
|
+
|
|
13
|
+
export const useEngineStateStore = defineStore('engineState', () => {
|
|
14
|
+
const phase = ref<EnginePhase>(EnginePhase.Created);
|
|
15
|
+
const background = ref<string | null>(null);
|
|
16
|
+
const foreground = ref<string | null>(null);
|
|
17
|
+
const dialogue = ref<DialogueState | null>(null);
|
|
18
|
+
const choices = ref<ChoiceState | null>(null);
|
|
19
|
+
const currentLocationId = ref<string | null>(null);
|
|
20
|
+
const availableActions = ref<ActionEntry[]>([]);
|
|
21
|
+
const availableLocations = ref<LocationEntry[]>([]);
|
|
22
|
+
const menuOpen = ref<MenuType | null>(null);
|
|
23
|
+
const skipMode = ref(false);
|
|
24
|
+
const autoMode = ref(false);
|
|
25
|
+
const historyBrowsing = ref(false);
|
|
26
|
+
const drawableEvents = ref<DrawableEventEntry[]>([]);
|
|
27
|
+
const overlayId = ref<string | null>(null);
|
|
28
|
+
const overlayProps = ref<Record<string, unknown> | null>(null);
|
|
29
|
+
|
|
30
|
+
function setDialogue(state: DialogueState | null) {
|
|
31
|
+
dialogue.value = state;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setChoices(state: ChoiceState | null) {
|
|
35
|
+
choices.value = state;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setBackground(path: string | null) {
|
|
39
|
+
background.value = path;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setForeground(path: string | null) {
|
|
43
|
+
foreground.value = path;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setPhase(p: EnginePhase) {
|
|
47
|
+
phase.value = p;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setLocation(id: string | null) {
|
|
51
|
+
currentLocationId.value = id;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setActions(actions: ActionEntry[]) {
|
|
55
|
+
availableActions.value = actions;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setLocations(locations: LocationEntry[]) {
|
|
59
|
+
availableLocations.value = locations;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function openMenu(type: MenuType) {
|
|
63
|
+
menuOpen.value = type;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function closeMenu() {
|
|
67
|
+
menuOpen.value = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setDrawableEvents(events: DrawableEventEntry[]) {
|
|
71
|
+
drawableEvents.value = events;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function setOverlay(id: string | null, props?: Record<string, unknown>) {
|
|
75
|
+
overlayId.value = id;
|
|
76
|
+
overlayProps.value = props ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function $reset() {
|
|
80
|
+
phase.value = EnginePhase.Created;
|
|
81
|
+
background.value = null;
|
|
82
|
+
foreground.value = null;
|
|
83
|
+
dialogue.value = null;
|
|
84
|
+
choices.value = null;
|
|
85
|
+
currentLocationId.value = null;
|
|
86
|
+
availableActions.value = [];
|
|
87
|
+
availableLocations.value = [];
|
|
88
|
+
menuOpen.value = null;
|
|
89
|
+
skipMode.value = false;
|
|
90
|
+
autoMode.value = false;
|
|
91
|
+
historyBrowsing.value = false;
|
|
92
|
+
drawableEvents.value = [];
|
|
93
|
+
overlayId.value = null;
|
|
94
|
+
overlayProps.value = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
phase,
|
|
99
|
+
background,
|
|
100
|
+
foreground,
|
|
101
|
+
dialogue,
|
|
102
|
+
choices,
|
|
103
|
+
currentLocationId,
|
|
104
|
+
availableActions,
|
|
105
|
+
availableLocations,
|
|
106
|
+
menuOpen,
|
|
107
|
+
skipMode,
|
|
108
|
+
autoMode,
|
|
109
|
+
historyBrowsing,
|
|
110
|
+
drawableEvents,
|
|
111
|
+
overlayId,
|
|
112
|
+
overlayProps,
|
|
113
|
+
setDialogue,
|
|
114
|
+
setChoices,
|
|
115
|
+
setBackground,
|
|
116
|
+
setForeground,
|
|
117
|
+
setPhase,
|
|
118
|
+
setLocation,
|
|
119
|
+
setActions,
|
|
120
|
+
setLocations,
|
|
121
|
+
openMenu,
|
|
122
|
+
closeMenu,
|
|
123
|
+
setDrawableEvents,
|
|
124
|
+
setOverlay,
|
|
125
|
+
$reset,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineStore } from 'pinia';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import type { BaseGameState } from '../engine/types';
|
|
4
|
+
|
|
5
|
+
/** Default initial game state */
|
|
6
|
+
function createDefaultState(): BaseGameState {
|
|
7
|
+
return {
|
|
8
|
+
locationId: '',
|
|
9
|
+
gameTime: 8,
|
|
10
|
+
flags: {},
|
|
11
|
+
counters: {},
|
|
12
|
+
player: {
|
|
13
|
+
name: 'Player',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const useGameStateStore = defineStore('gameState', () => {
|
|
19
|
+
const state = ref<BaseGameState>(createDefaultState());
|
|
20
|
+
|
|
21
|
+
function getState(): BaseGameState {
|
|
22
|
+
return state.value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setState(newState: BaseGameState) {
|
|
26
|
+
state.value = newState;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getSnapshot(): BaseGameState {
|
|
30
|
+
return structuredClone(state.value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function restoreSnapshot(snapshot: BaseGameState) {
|
|
34
|
+
state.value = structuredClone(snapshot);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function $reset() {
|
|
38
|
+
state.value = createDefaultState();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
state,
|
|
43
|
+
getState,
|
|
44
|
+
setState,
|
|
45
|
+
getSnapshot,
|
|
46
|
+
restoreSnapshot,
|
|
47
|
+
$reset,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { ActionManager } from '../../src/engine/managers/ActionManager';
|
|
3
|
+
import type { VylosAction, BaseGameState } from '../../src/engine/types';
|
|
4
|
+
|
|
5
|
+
function makeState(overrides: Partial<BaseGameState> = {}): BaseGameState {
|
|
6
|
+
return {
|
|
7
|
+
locationId: 'cafe',
|
|
8
|
+
gameTime: 12,
|
|
9
|
+
flags: {},
|
|
10
|
+
counters: {},
|
|
11
|
+
player: { name: 'Alice' },
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('ActionManager', () => {
|
|
17
|
+
let am: ActionManager;
|
|
18
|
+
|
|
19
|
+
const sleepAction: VylosAction = {
|
|
20
|
+
id: 'sleep',
|
|
21
|
+
label: 'Go to Sleep',
|
|
22
|
+
locationId: 'bedroom',
|
|
23
|
+
execute(state) {
|
|
24
|
+
state.gameTime += 8;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const orderCoffee: VylosAction = {
|
|
29
|
+
id: 'order_coffee',
|
|
30
|
+
label: 'Order Coffee',
|
|
31
|
+
locationId: 'cafe',
|
|
32
|
+
unlocked: (state) => state.counters['coffee_count'] === undefined || state.counters['coffee_count'] < 3,
|
|
33
|
+
execute(state) {
|
|
34
|
+
state.counters['coffee_count'] = (state.counters['coffee_count'] ?? 0) + 1;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const globalAction: VylosAction = {
|
|
39
|
+
id: 'check_phone',
|
|
40
|
+
label: 'Check Phone',
|
|
41
|
+
execute(state) {
|
|
42
|
+
state.flags['checked_phone'] = true;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
am = new ActionManager();
|
|
48
|
+
am.registerAll([sleepAction, orderCoffee, globalAction]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('registers and retrieves actions', () => {
|
|
52
|
+
expect(am.get('sleep')).toBeDefined();
|
|
53
|
+
expect(am.get('order_coffee')).toBeDefined();
|
|
54
|
+
expect(am.get('nope')).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('getAvailable filters by location', () => {
|
|
58
|
+
const state = makeState();
|
|
59
|
+
const cafeActions = am.getAvailable('cafe', state);
|
|
60
|
+
expect(cafeActions.map(a => a.id)).toContain('order_coffee');
|
|
61
|
+
expect(cafeActions.map(a => a.id)).toContain('check_phone'); // global
|
|
62
|
+
expect(cafeActions.map(a => a.id)).not.toContain('sleep'); // bedroom only
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('getAvailable includes global actions everywhere', () => {
|
|
66
|
+
const state = makeState();
|
|
67
|
+
const bedroomActions = am.getAvailable('bedroom', state);
|
|
68
|
+
expect(bedroomActions.map(a => a.id)).toContain('check_phone');
|
|
69
|
+
expect(bedroomActions.map(a => a.id)).toContain('sleep');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('getAvailable respects unlock conditions', () => {
|
|
73
|
+
const state = makeState({ counters: { coffee_count: 3 } });
|
|
74
|
+
const actions = am.getAvailable('cafe', state);
|
|
75
|
+
expect(actions.map(a => a.id)).not.toContain('order_coffee');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('execute runs action and mutates state', () => {
|
|
79
|
+
const state = makeState();
|
|
80
|
+
const result = am.execute('order_coffee', state);
|
|
81
|
+
expect(result).toBe(true);
|
|
82
|
+
expect(state.counters['coffee_count']).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('execute returns false for unknown action', () => {
|
|
86
|
+
const state = makeState();
|
|
87
|
+
expect(am.execute('nope', state)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('clear removes all actions', () => {
|
|
91
|
+
am.clear();
|
|
92
|
+
expect(am.get('sleep')).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { CheckpointManager } from '../../src/engine/core/CheckpointManager';
|
|
3
|
+
import type { BaseGameState } from '../../src/engine/types';
|
|
4
|
+
import { CheckpointType } from '../../src/engine/types';
|
|
5
|
+
|
|
6
|
+
function makeState(overrides: Partial<BaseGameState> = {}): BaseGameState {
|
|
7
|
+
return {
|
|
8
|
+
locationId: 'cafe',
|
|
9
|
+
gameTime: 8,
|
|
10
|
+
flags: {},
|
|
11
|
+
counters: {},
|
|
12
|
+
player: { name: 'Alice' },
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('CheckpointManager', () => {
|
|
18
|
+
let cm: CheckpointManager;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
cm = new CheckpointManager();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('starts empty', () => {
|
|
25
|
+
expect(cm.count).toBe(0);
|
|
26
|
+
expect(cm.isReplaying).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('captures checkpoints incrementally', () => {
|
|
30
|
+
cm.capture(makeState(), CheckpointType.Say);
|
|
31
|
+
cm.capture(makeState({ gameTime: 9 }), CheckpointType.Say);
|
|
32
|
+
cm.capture(makeState({ gameTime: 10 }), CheckpointType.Choice, 'coffee');
|
|
33
|
+
expect(cm.count).toBe(3);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('stores deep clones (mutations do not affect checkpoints)', () => {
|
|
37
|
+
const state = makeState();
|
|
38
|
+
cm.capture(state, CheckpointType.Say);
|
|
39
|
+
state.gameTime = 999;
|
|
40
|
+
const restored = cm.getStateAt(0);
|
|
41
|
+
expect(restored?.gameTime).toBe(8);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('retrieves state at a specific step', () => {
|
|
45
|
+
cm.capture(makeState({ gameTime: 8 }), CheckpointType.Say);
|
|
46
|
+
cm.capture(makeState({ gameTime: 9 }), CheckpointType.Say);
|
|
47
|
+
cm.capture(makeState({ gameTime: 10 }), CheckpointType.Choice, 'tea');
|
|
48
|
+
|
|
49
|
+
expect(cm.getStateAt(0)?.gameTime).toBe(8);
|
|
50
|
+
expect(cm.getStateAt(1)?.gameTime).toBe(9);
|
|
51
|
+
expect(cm.getStateAt(2)?.gameTime).toBe(10);
|
|
52
|
+
expect(cm.getStateAt(3)).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('prepareRollback sets up replay mode', () => {
|
|
56
|
+
cm.capture(makeState({ gameTime: 8 }), CheckpointType.Say);
|
|
57
|
+
cm.capture(makeState({ gameTime: 9 }), CheckpointType.Say);
|
|
58
|
+
cm.capture(makeState({ gameTime: 10 }), CheckpointType.Choice, 'tea');
|
|
59
|
+
|
|
60
|
+
const state = cm.prepareRollback(1);
|
|
61
|
+
expect(state?.gameTime).toBe(9);
|
|
62
|
+
expect(cm.isReplaying).toBe(true);
|
|
63
|
+
// Checkpoints after target are trimmed
|
|
64
|
+
expect(cm.count).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('replay advances step by step', () => {
|
|
68
|
+
cm.capture(makeState({ gameTime: 8 }), CheckpointType.Say);
|
|
69
|
+
cm.capture(makeState({ gameTime: 9 }), CheckpointType.Choice, 'coffee');
|
|
70
|
+
cm.capture(makeState({ gameTime: 10 }), CheckpointType.Say);
|
|
71
|
+
|
|
72
|
+
cm.prepareRollback(2);
|
|
73
|
+
// After prepareRollback(2), we have checkpoints [0,1] and replayIndex=0
|
|
74
|
+
expect(cm.isReplaying).toBe(true);
|
|
75
|
+
expect(cm.count).toBe(2);
|
|
76
|
+
|
|
77
|
+
// Advance past checkpoint 0
|
|
78
|
+
cm.advanceReplay();
|
|
79
|
+
expect(cm.isReplaying).toBe(true);
|
|
80
|
+
|
|
81
|
+
// Advance past checkpoint 1
|
|
82
|
+
cm.advanceReplay();
|
|
83
|
+
// Now replayIndex=2 >= count=2, so replaying is done
|
|
84
|
+
expect(cm.isReplaying).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('getReplayChoiceResult returns stored value during replay', () => {
|
|
88
|
+
cm.capture(makeState(), CheckpointType.Say);
|
|
89
|
+
cm.capture(makeState(), CheckpointType.Choice, 'coffee');
|
|
90
|
+
cm.capture(makeState(), CheckpointType.Say); // step 2
|
|
91
|
+
|
|
92
|
+
// Rollback to step 1 — trims to [step0], replays from beginning
|
|
93
|
+
cm.prepareRollback(1);
|
|
94
|
+
expect(cm.count).toBe(1);
|
|
95
|
+
// replayIndex starts at 0, checkpoints[0] is Say (no choice result)
|
|
96
|
+
expect(cm.getReplayChoiceResult()).toBeUndefined();
|
|
97
|
+
cm.advanceReplay();
|
|
98
|
+
// replayIndex=1 >= count=1, replay done — choice at step 1 was trimmed
|
|
99
|
+
expect(cm.isReplaying).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('clear removes all checkpoints', () => {
|
|
103
|
+
cm.capture(makeState(), CheckpointType.Say);
|
|
104
|
+
cm.capture(makeState(), CheckpointType.Say);
|
|
105
|
+
cm.clear();
|
|
106
|
+
expect(cm.count).toBe(0);
|
|
107
|
+
expect(cm.isReplaying).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('getAll returns deep clones', () => {
|
|
111
|
+
cm.capture(makeState(), CheckpointType.Say);
|
|
112
|
+
const all = cm.getAll();
|
|
113
|
+
expect(all.length).toBe(1);
|
|
114
|
+
all[0].step = 999;
|
|
115
|
+
expect(cm.getAll()[0].step).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('restore loads checkpoints from save data', () => {
|
|
119
|
+
const checkpoints = [
|
|
120
|
+
{ step: 0, gameState: makeState({ gameTime: 8 }), type: CheckpointType.Say },
|
|
121
|
+
{ step: 1, gameState: makeState({ gameTime: 9 }), type: CheckpointType.Choice, choiceResult: 'tea' },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
cm.restore(checkpoints);
|
|
125
|
+
expect(cm.count).toBe(2);
|
|
126
|
+
expect(cm.getStateAt(0)?.gameTime).toBe(8);
|
|
127
|
+
expect(cm.getStateAt(1)?.gameTime).toBe(9);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('prepareRollback returns undefined for invalid step', () => {
|
|
131
|
+
expect(cm.prepareRollback(0)).toBeUndefined();
|
|
132
|
+
cm.capture(makeState(), CheckpointType.Say);
|
|
133
|
+
expect(cm.prepareRollback(5)).toBeUndefined();
|
|
134
|
+
expect(cm.prepareRollback(-1)).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
});
|