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();
  }
}