Spaces:
Running on Zero
Running on Zero
File size: 5,514 Bytes
c670567 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | /**
* ============================================================================
* GAME DATA MANAGER - Global persistent state management
* ============================================================================
*
* Singleton that manages all persistent game state across scenes:
* - Numeric stats (HP, score, love points, money)
* - Boolean flags (chapter completed, item obtained, choice made)
* - Inventory items
* - Chapter progress
* - Ending determination
*
* Data persists across scene transitions because the singleton lives
* in module scope (not tied to any scene lifecycle).
* Optionally saves/loads from localStorage for cross-session persistence.
*
* USAGE:
* const gd = GameDataManager.getInstance();
*
* // Set values
* gd.set('playerHP', 100);
* gd.addTo('score', 50);
* gd.setFlag('chapter1_complete', true);
*
* // Get values
* const hp = gd.get('playerHP', 100); // default 100
* const done = gd.getFlag('chapter1_complete');
*
* // Inventory
* gd.addItem('health_potion');
* gd.hasItem('health_potion'); // true
*
* // Save/Load
* gd.saveToStorage('my_game_save');
* gd.loadFromStorage('my_game_save');
*
* // Ending
* const ending = gd.determineEnding(endingRules);
*/
export interface EndingRule {
/** Ending scene key */
endingKey: string;
/** Condition function -- first matching rule wins */
condition: (data: GameDataManager) => boolean;
/** Priority (higher = checked first) */
priority?: number;
}
export class GameDataManager {
// Singleton pattern
private static instance: GameDataManager;
private numericData: Map<string, number> = new Map();
private flagData: Map<string, boolean> = new Map();
private inventory: Set<string> = new Set();
private choiceHistory: Map<string, string> = new Map();
private constructor() {}
static getInstance(): GameDataManager {
if (!GameDataManager.instance) {
GameDataManager.instance = new GameDataManager();
}
return GameDataManager.instance;
}
// -- Numeric Data --
/** Set a numeric value. */
set(key: string, value: number): void {
this.numericData.set(key, value);
}
/** Get a numeric value with optional default. */
get(key: string, defaultValue: number = 0): number {
return this.numericData.get(key) ?? defaultValue;
}
/** Add to a numeric value. */
addTo(key: string, amount: number): number {
const current = this.get(key);
const newValue = current + amount;
this.set(key, newValue);
return newValue;
}
// -- Flags --
/** Set a boolean flag. */
setFlag(key: string, value: boolean): void {
this.flagData.set(key, value);
}
/** Get a boolean flag. */
getFlag(key: string): boolean {
return this.flagData.get(key) ?? false;
}
// -- Inventory --
/** Add an item to inventory. */
addItem(itemId: string): void {
this.inventory.add(itemId);
}
/** Remove an item from inventory. */
removeItem(itemId: string): boolean {
return this.inventory.delete(itemId);
}
/** Check if player has an item. */
hasItem(itemId: string): boolean {
return this.inventory.has(itemId);
}
/** Get all inventory items. */
getInventory(): string[] {
return [...this.inventory];
}
// -- Choice History --
/** Record a choice the player made. */
recordChoice(choiceId: string, selectedOption: string): void {
this.choiceHistory.set(choiceId, selectedOption);
}
/** Get what the player chose for a given choice. */
getChoice(choiceId: string): string | undefined {
return this.choiceHistory.get(choiceId);
}
// -- Ending Determination --
/**
* Evaluate ending rules and return the matching ending key.
* Rules are sorted by priority (highest first).
*/
determineEnding(rules: EndingRule[]): string | undefined {
const sorted = [...rules].sort(
(a, b) => (b.priority ?? 0) - (a.priority ?? 0),
);
for (const rule of sorted) {
if (rule.condition(this)) {
return rule.endingKey;
}
}
return undefined;
}
// -- Persistence --
/** Save all data to localStorage. */
saveToStorage(slotName: string): void {
try {
const data = {
numeric: Object.fromEntries(this.numericData),
flags: Object.fromEntries(this.flagData),
inventory: [...this.inventory],
choices: Object.fromEntries(this.choiceHistory),
};
localStorage.setItem(slotName, JSON.stringify(data));
} catch (e) {
console.warn('[GameDataManager] Failed to save:', e);
}
}
/** Load data from localStorage. Returns true if data was loaded. */
loadFromStorage(slotName: string): boolean {
try {
const raw = localStorage.getItem(slotName);
if (!raw) return false;
const data = JSON.parse(raw);
if (data.numeric) {
this.numericData = new Map(Object.entries(data.numeric));
}
if (data.flags) {
this.flagData = new Map(Object.entries(data.flags));
}
if (data.inventory) {
this.inventory = new Set(data.inventory);
}
if (data.choices) {
this.choiceHistory = new Map(Object.entries(data.choices));
}
return true;
} catch (e) {
console.warn('[GameDataManager] Failed to load:', e);
return false;
}
}
/** Reset all data to defaults. */
reset(): void {
this.numericData.clear();
this.flagData.clear();
this.inventory.clear();
this.choiceHistory.clear();
}
}
|