@doodle-engine/core 0.0.1

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/dist/core.js ADDED
@@ -0,0 +1,1192 @@
1
+ var L = Object.defineProperty;
2
+ var k = (e, t, n) => t in e ? L(e, t, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[t] = n;
3
+ var v = (e, t, n) => k(e, typeof t != "symbol" ? t + "" : t, n);
4
+ function x(e, t) {
5
+ switch (e.type) {
6
+ case "hasFlag":
7
+ return O(e.flag, t);
8
+ case "notFlag":
9
+ return V(e.flag, t);
10
+ case "hasItem":
11
+ return R(e.itemId, t);
12
+ case "variableEquals":
13
+ return P(e.variable, e.value, t);
14
+ case "variableGreaterThan":
15
+ return D(e.variable, e.value, t);
16
+ case "variableLessThan":
17
+ return q(e.variable, e.value, t);
18
+ case "atLocation":
19
+ return W(e.locationId, t);
20
+ case "questAtStage":
21
+ return F(e.questId, e.stageId, t);
22
+ case "characterAt":
23
+ return M(e.characterId, e.locationId, t);
24
+ case "characterInParty":
25
+ return G(e.characterId, t);
26
+ case "relationshipAbove":
27
+ return $(e.characterId, e.value, t);
28
+ case "relationshipBelow":
29
+ return H(e.characterId, e.value, t);
30
+ case "timeIs":
31
+ return U(e.startHour, e.endHour, t);
32
+ case "itemAt":
33
+ return j(e.itemId, e.locationId, t);
34
+ default:
35
+ return !1;
36
+ }
37
+ }
38
+ function A(e, t) {
39
+ return e.every((n) => x(n, t));
40
+ }
41
+ function O(e, t) {
42
+ return t.flags[e] === !0;
43
+ }
44
+ function V(e, t) {
45
+ return t.flags[e] !== !0;
46
+ }
47
+ function R(e, t) {
48
+ return t.inventory.includes(e);
49
+ }
50
+ function P(e, t, n) {
51
+ return n.variables[e] === t;
52
+ }
53
+ function D(e, t, n) {
54
+ const r = n.variables[e];
55
+ return typeof r == "number" && r > t;
56
+ }
57
+ function q(e, t, n) {
58
+ const r = n.variables[e];
59
+ return typeof r == "number" && r < t;
60
+ }
61
+ function W(e, t) {
62
+ return t.currentLocation === e;
63
+ }
64
+ function F(e, t, n) {
65
+ return n.questProgress[e] === t;
66
+ }
67
+ function M(e, t, n) {
68
+ const r = n.characterState[e];
69
+ return (r == null ? void 0 : r.location) === t;
70
+ }
71
+ function G(e, t) {
72
+ const n = t.characterState[e];
73
+ return (n == null ? void 0 : n.inParty) === !0;
74
+ }
75
+ function $(e, t, n) {
76
+ const r = n.characterState[e];
77
+ return r !== void 0 && r.relationship > t;
78
+ }
79
+ function H(e, t, n) {
80
+ const r = n.characterState[e];
81
+ return r !== void 0 && r.relationship < t;
82
+ }
83
+ function U(e, t, n) {
84
+ const r = n.currentTime.hour;
85
+ return e < t ? r >= e && r < t : r >= e || r < t;
86
+ }
87
+ function j(e, t, n) {
88
+ return n.itemLocations[e] === t;
89
+ }
90
+ function J(e, t) {
91
+ switch (e.type) {
92
+ case "setFlag":
93
+ return Q(e.flag, t);
94
+ case "clearFlag":
95
+ return B(e.flag, t);
96
+ case "setVariable":
97
+ return _(e.variable, e.value, t);
98
+ case "addVariable":
99
+ return z(e.variable, e.value, t);
100
+ case "addItem":
101
+ return Y(e.itemId, t);
102
+ case "removeItem":
103
+ return K(e.itemId, t);
104
+ case "moveItem":
105
+ return X(e.itemId, e.locationId, t);
106
+ case "goToLocation":
107
+ return Z(e.locationId, t);
108
+ case "advanceTime":
109
+ return tt(e.hours, t);
110
+ case "setQuestStage":
111
+ return et(e.questId, e.stageId, t);
112
+ case "addJournalEntry":
113
+ return nt(e.entryId, t);
114
+ case "startDialogue":
115
+ return rt(e.dialogueId, t);
116
+ case "endDialogue":
117
+ return at(t);
118
+ case "setCharacterLocation":
119
+ return it(e.characterId, e.locationId, t);
120
+ case "addToParty":
121
+ return st(e.characterId, t);
122
+ case "removeFromParty":
123
+ return ot(e.characterId, t);
124
+ case "setRelationship":
125
+ return ct(e.characterId, e.value, t);
126
+ case "addRelationship":
127
+ return ut(e.characterId, e.value, t);
128
+ case "setCharacterStat":
129
+ return lt(e.characterId, e.stat, e.value, t);
130
+ case "addCharacterStat":
131
+ return dt(e.characterId, e.stat, e.value, t);
132
+ case "setMapEnabled":
133
+ return ht(e.enabled, t);
134
+ case "playMusic":
135
+ return t;
136
+ case "playSound":
137
+ return ft(e.sound, t);
138
+ case "notify":
139
+ return pt(e.message, t);
140
+ case "playVideo":
141
+ return mt(e.file, t);
142
+ default:
143
+ return t;
144
+ }
145
+ }
146
+ function y(e, t) {
147
+ return e.reduce((n, r) => J(r, n), t);
148
+ }
149
+ function Q(e, t) {
150
+ return {
151
+ ...t,
152
+ flags: {
153
+ ...t.flags,
154
+ [e]: !0
155
+ }
156
+ };
157
+ }
158
+ function B(e, t) {
159
+ return {
160
+ ...t,
161
+ flags: {
162
+ ...t.flags,
163
+ [e]: !1
164
+ }
165
+ };
166
+ }
167
+ function _(e, t, n) {
168
+ return {
169
+ ...n,
170
+ variables: {
171
+ ...n.variables,
172
+ [e]: t
173
+ }
174
+ };
175
+ }
176
+ function z(e, t, n) {
177
+ const r = n.variables[e], a = typeof r == "number" ? r + t : t;
178
+ return {
179
+ ...n,
180
+ variables: {
181
+ ...n.variables,
182
+ [e]: a
183
+ }
184
+ };
185
+ }
186
+ function Y(e, t) {
187
+ return t.inventory.includes(e) ? t : {
188
+ ...t,
189
+ inventory: [...t.inventory, e],
190
+ itemLocations: {
191
+ ...t.itemLocations,
192
+ [e]: "inventory"
193
+ }
194
+ };
195
+ }
196
+ function K(e, t) {
197
+ return {
198
+ ...t,
199
+ inventory: t.inventory.filter((n) => n !== e)
200
+ // Note: itemLocation is NOT updated here - the item stays at "inventory"
201
+ // or wherever it was. Use moveItem to relocate it.
202
+ };
203
+ }
204
+ function X(e, t, n) {
205
+ return {
206
+ ...n,
207
+ inventory: n.inventory.filter((r) => r !== e),
208
+ itemLocations: {
209
+ ...n.itemLocations,
210
+ [e]: t
211
+ }
212
+ };
213
+ }
214
+ function Z(e, t) {
215
+ return {
216
+ ...t,
217
+ currentLocation: e
218
+ };
219
+ }
220
+ function tt(e, t) {
221
+ const n = t.currentTime.hour + e, r = Math.floor(n / 24), a = n % 24;
222
+ return {
223
+ ...t,
224
+ currentTime: {
225
+ day: t.currentTime.day + r,
226
+ hour: a
227
+ }
228
+ };
229
+ }
230
+ function et(e, t, n) {
231
+ return {
232
+ ...n,
233
+ questProgress: {
234
+ ...n.questProgress,
235
+ [e]: t
236
+ }
237
+ };
238
+ }
239
+ function nt(e, t) {
240
+ return t.unlockedJournalEntries.includes(e) ? t : {
241
+ ...t,
242
+ unlockedJournalEntries: [...t.unlockedJournalEntries, e]
243
+ };
244
+ }
245
+ function rt(e, t) {
246
+ return {
247
+ ...t,
248
+ dialogueState: {
249
+ dialogueId: e,
250
+ nodeId: ""
251
+ // Will be set by engine when it looks up the dialogue
252
+ }
253
+ };
254
+ }
255
+ function at(e) {
256
+ return {
257
+ ...e,
258
+ dialogueState: null
259
+ };
260
+ }
261
+ function it(e, t, n) {
262
+ const r = n.characterState[e];
263
+ return r ? {
264
+ ...n,
265
+ characterState: {
266
+ ...n.characterState,
267
+ [e]: {
268
+ ...r,
269
+ location: t
270
+ }
271
+ }
272
+ } : n;
273
+ }
274
+ function st(e, t) {
275
+ const n = t.characterState[e];
276
+ return n ? {
277
+ ...t,
278
+ characterState: {
279
+ ...t.characterState,
280
+ [e]: {
281
+ ...n,
282
+ inParty: !0
283
+ }
284
+ }
285
+ } : t;
286
+ }
287
+ function ot(e, t) {
288
+ const n = t.characterState[e];
289
+ return n ? {
290
+ ...t,
291
+ characterState: {
292
+ ...t.characterState,
293
+ [e]: {
294
+ ...n,
295
+ inParty: !1
296
+ }
297
+ }
298
+ } : t;
299
+ }
300
+ function ct(e, t, n) {
301
+ const r = n.characterState[e];
302
+ return r ? {
303
+ ...n,
304
+ characterState: {
305
+ ...n.characterState,
306
+ [e]: {
307
+ ...r,
308
+ relationship: t
309
+ }
310
+ }
311
+ } : n;
312
+ }
313
+ function ut(e, t, n) {
314
+ const r = n.characterState[e];
315
+ return r ? {
316
+ ...n,
317
+ characterState: {
318
+ ...n.characterState,
319
+ [e]: {
320
+ ...r,
321
+ relationship: r.relationship + t
322
+ }
323
+ }
324
+ } : n;
325
+ }
326
+ function lt(e, t, n, r) {
327
+ const a = r.characterState[e];
328
+ return a ? {
329
+ ...r,
330
+ characterState: {
331
+ ...r.characterState,
332
+ [e]: {
333
+ ...a,
334
+ stats: {
335
+ ...a.stats,
336
+ [t]: n
337
+ }
338
+ }
339
+ }
340
+ } : r;
341
+ }
342
+ function dt(e, t, n, r) {
343
+ const a = r.characterState[e];
344
+ if (!a)
345
+ return r;
346
+ const s = a.stats[t], i = typeof s == "number" ? s + n : n;
347
+ return {
348
+ ...r,
349
+ characterState: {
350
+ ...r.characterState,
351
+ [e]: {
352
+ ...a,
353
+ stats: {
354
+ ...a.stats,
355
+ [t]: i
356
+ }
357
+ }
358
+ }
359
+ };
360
+ }
361
+ function ht(e, t) {
362
+ return {
363
+ ...t,
364
+ mapEnabled: e
365
+ };
366
+ }
367
+ function pt(e, t) {
368
+ return {
369
+ ...t,
370
+ notifications: [...t.notifications, e]
371
+ };
372
+ }
373
+ function ft(e, t) {
374
+ return {
375
+ ...t,
376
+ pendingSounds: [...t.pendingSounds, e]
377
+ };
378
+ }
379
+ function mt(e, t) {
380
+ return {
381
+ ...t,
382
+ pendingVideo: e
383
+ };
384
+ }
385
+ function w(e, t) {
386
+ if (!e.startsWith("@"))
387
+ return e;
388
+ const n = e.slice(1);
389
+ return t[n] ?? e;
390
+ }
391
+ function Ot(e) {
392
+ return (t) => w(t, e);
393
+ }
394
+ function gt(e, t) {
395
+ const n = t.locales[e.currentLocale] ?? {}, r = (C) => w(C, n), a = bt(e.currentLocation, t, r), s = yt(e, t, r), i = St(e, t, r), { dialogue: o, choices: c } = vt(e, t, r), u = It(e, t, r), d = Tt(e, t, r), m = Et(e, t, r), g = At(e, t, r), l = e.mapEnabled ? wt(e, t, r) : null, f = t.locations[e.currentLocation], h = (f == null ? void 0 : f.music) ?? "", p = (f == null ? void 0 : f.ambient) ?? "", b = e.notifications.map(r), S = [...e.pendingSounds], N = e.pendingVideo;
396
+ return {
397
+ location: a,
398
+ charactersHere: s,
399
+ itemsHere: i,
400
+ choices: c,
401
+ dialogue: o,
402
+ party: u,
403
+ inventory: d,
404
+ quests: m,
405
+ journal: g,
406
+ variables: { ...e.variables },
407
+ time: e.currentTime,
408
+ map: l,
409
+ music: h,
410
+ ambient: p,
411
+ notifications: b,
412
+ pendingSounds: S,
413
+ pendingVideo: N
414
+ };
415
+ }
416
+ function bt(e, t, n) {
417
+ const r = t.locations[e];
418
+ return r ? {
419
+ id: r.id,
420
+ name: n(r.name),
421
+ description: n(r.description),
422
+ banner: r.banner
423
+ } : {
424
+ id: e,
425
+ name: e,
426
+ description: `Location not found: ${e}`,
427
+ banner: ""
428
+ };
429
+ }
430
+ function yt(e, t, n) {
431
+ const r = [];
432
+ for (const [a, s] of Object.entries(e.characterState))
433
+ if (s.location === e.currentLocation) {
434
+ const i = t.characters[a];
435
+ i && r.push({
436
+ id: i.id,
437
+ name: n(i.name),
438
+ biography: n(i.biography),
439
+ portrait: i.portrait,
440
+ location: s.location,
441
+ inParty: s.inParty,
442
+ relationship: s.relationship,
443
+ stats: s.stats
444
+ });
445
+ }
446
+ return r;
447
+ }
448
+ function St(e, t, n) {
449
+ const r = [];
450
+ for (const [a, s] of Object.entries(e.itemLocations))
451
+ if (s === e.currentLocation) {
452
+ const i = t.items[a];
453
+ i && r.push({
454
+ id: i.id,
455
+ name: n(i.name),
456
+ description: n(i.description),
457
+ icon: i.icon,
458
+ image: i.image,
459
+ stats: i.stats
460
+ });
461
+ }
462
+ return r;
463
+ }
464
+ function vt(e, t, n) {
465
+ var c, u;
466
+ if (!e.dialogueState)
467
+ return { dialogue: null, choices: [] };
468
+ const r = t.dialogues[e.dialogueState.dialogueId];
469
+ if (!r)
470
+ return { dialogue: null, choices: [] };
471
+ const a = r.nodes.find((d) => {
472
+ var m;
473
+ return d.id === ((m = e.dialogueState) == null ? void 0 : m.nodeId);
474
+ });
475
+ if (!a)
476
+ return { dialogue: null, choices: [] };
477
+ const s = a.speaker ? n(((c = t.characters[a.speaker]) == null ? void 0 : c.name) ?? a.speaker) : "Narrator", i = {
478
+ speaker: a.speaker,
479
+ speakerName: s,
480
+ text: n(a.text),
481
+ portrait: a.portrait ?? ((u = t.characters[a.speaker ?? ""]) == null ? void 0 : u.portrait),
482
+ voice: a.voice
483
+ }, o = a.choices.filter((d) => !d.conditions || d.conditions.length === 0 ? !0 : A(d.conditions, e)).map((d) => ({
484
+ id: d.id,
485
+ text: n(d.text)
486
+ }));
487
+ return { dialogue: i, choices: o };
488
+ }
489
+ function It(e, t, n) {
490
+ const r = [];
491
+ for (const [a, s] of Object.entries(e.characterState))
492
+ if (s.inParty) {
493
+ const i = t.characters[a];
494
+ i && r.push({
495
+ id: i.id,
496
+ name: n(i.name),
497
+ biography: n(i.biography),
498
+ portrait: i.portrait,
499
+ location: s.location,
500
+ inParty: s.inParty,
501
+ relationship: s.relationship,
502
+ stats: s.stats
503
+ });
504
+ }
505
+ return r;
506
+ }
507
+ function Tt(e, t, n) {
508
+ return e.inventory.map((r) => {
509
+ const a = t.items[r];
510
+ return a ? {
511
+ id: a.id,
512
+ name: n(a.name),
513
+ description: n(a.description),
514
+ icon: a.icon,
515
+ image: a.image,
516
+ stats: a.stats
517
+ } : null;
518
+ }).filter((r) => r !== null);
519
+ }
520
+ function Et(e, t, n) {
521
+ const r = [];
522
+ for (const [a, s] of Object.entries(e.questProgress)) {
523
+ const i = t.quests[a];
524
+ if (!i) continue;
525
+ const o = i.stages.find((c) => c.id === s);
526
+ o && r.push({
527
+ id: i.id,
528
+ name: n(i.name),
529
+ description: n(i.description),
530
+ currentStage: o.id,
531
+ currentStageDescription: n(o.description)
532
+ });
533
+ }
534
+ return r;
535
+ }
536
+ function At(e, t, n) {
537
+ return e.unlockedJournalEntries.map((r) => {
538
+ const a = t.journalEntries[r];
539
+ return a ? {
540
+ id: a.id,
541
+ title: n(a.title),
542
+ text: n(a.text),
543
+ category: a.category
544
+ } : null;
545
+ }).filter((r) => r !== null);
546
+ }
547
+ function wt(e, t, n) {
548
+ const r = Object.keys(t.maps);
549
+ if (r.length === 0) return null;
550
+ const a = t.maps[r[0]];
551
+ if (!a) return null;
552
+ const s = a.locations.map((i) => {
553
+ const o = t.locations[i.id];
554
+ return {
555
+ id: i.id,
556
+ name: o ? n(o.name) : i.id,
557
+ x: i.x,
558
+ y: i.y,
559
+ isCurrent: i.id === e.currentLocation
560
+ };
561
+ });
562
+ return {
563
+ id: a.id,
564
+ name: n(a.name),
565
+ image: a.image,
566
+ scale: a.scale,
567
+ locations: s
568
+ };
569
+ }
570
+ class Vt {
571
+ /**
572
+ * Create a new engine instance.
573
+ *
574
+ * @param registry - Content registry with all game entities
575
+ * @param state - Initial game state
576
+ */
577
+ constructor(t, n) {
578
+ v(this, "registry");
579
+ v(this, "state");
580
+ this.registry = t, this.state = n;
581
+ }
582
+ // ===========================================================================
583
+ // Core API Methods
584
+ // ===========================================================================
585
+ /**
586
+ * Start a new game from configuration.
587
+ *
588
+ * Initializes game state from the provided config and builds the initial snapshot.
589
+ *
590
+ * @param config - Game configuration with starting conditions
591
+ * @returns Initial snapshot
592
+ */
593
+ newGame(t) {
594
+ const n = {};
595
+ for (const [a, s] of Object.entries(this.registry.characters))
596
+ n[a] = {
597
+ location: s.location,
598
+ inParty: !1,
599
+ relationship: 0,
600
+ stats: { ...s.stats }
601
+ // Clone stats
602
+ };
603
+ const r = {};
604
+ for (const [a, s] of Object.entries(this.registry.items))
605
+ r[a] = s.location;
606
+ return this.state = {
607
+ currentLocation: t.startLocation,
608
+ currentTime: { ...t.startTime },
609
+ flags: { ...t.startFlags },
610
+ variables: { ...t.startVariables },
611
+ inventory: [...t.startInventory],
612
+ questProgress: {},
613
+ unlockedJournalEntries: [],
614
+ playerNotes: [],
615
+ dialogueState: null,
616
+ characterState: n,
617
+ itemLocations: r,
618
+ mapEnabled: !0,
619
+ notifications: [],
620
+ pendingSounds: [],
621
+ pendingVideo: null,
622
+ currentLocale: "en"
623
+ // Default locale
624
+ }, this.checkTriggeredDialogues(), this.buildSnapshotAndClearTransients();
625
+ }
626
+ /**
627
+ * Load a game from save data.
628
+ *
629
+ * Restores game state and builds a snapshot.
630
+ *
631
+ * @param saveData - Saved game data
632
+ * @returns Snapshot of the loaded game
633
+ */
634
+ loadGame(t) {
635
+ return this.state = { ...t.state }, this.buildSnapshotAndClearTransients();
636
+ }
637
+ /**
638
+ * Save the current game state.
639
+ *
640
+ * Returns save data that can be serialized and stored.
641
+ *
642
+ * @returns Save data with current state
643
+ */
644
+ saveGame() {
645
+ return {
646
+ version: "1.0",
647
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
648
+ state: { ...this.state }
649
+ };
650
+ }
651
+ /**
652
+ * Player selected a dialogue choice.
653
+ *
654
+ * Processes the choice effects and advances to the next node.
655
+ *
656
+ * @param choiceId - ID of the selected choice
657
+ * @returns New snapshot after processing the choice
658
+ */
659
+ selectChoice(t) {
660
+ if (!this.state.dialogueState)
661
+ return this.buildSnapshotAndClearTransients();
662
+ const n = this.registry.dialogues[this.state.dialogueState.dialogueId];
663
+ if (!n)
664
+ return this.buildSnapshotAndClearTransients();
665
+ const r = n.nodes.find((i) => {
666
+ var o;
667
+ return i.id === ((o = this.state.dialogueState) == null ? void 0 : o.nodeId);
668
+ });
669
+ if (!r)
670
+ return this.buildSnapshotAndClearTransients();
671
+ const a = r.choices.find((i) => i.id === t);
672
+ if (!a)
673
+ return this.buildSnapshotAndClearTransients();
674
+ a.effects && (this.state = y(a.effects, this.state));
675
+ const s = n.nodes.find((i) => i.id === a.next);
676
+ return s ? (this.state = {
677
+ ...this.state,
678
+ dialogueState: {
679
+ dialogueId: n.id,
680
+ nodeId: s.id
681
+ }
682
+ }, s.effects && (this.state = y(s.effects, this.state))) : this.state = {
683
+ ...this.state,
684
+ dialogueState: null
685
+ }, this.buildSnapshotAndClearTransients();
686
+ }
687
+ /**
688
+ * Player clicked on a character to talk.
689
+ *
690
+ * Starts the character's dialogue if they have one.
691
+ *
692
+ * @param characterId - ID of the character to talk to
693
+ * @returns New snapshot with dialogue started
694
+ */
695
+ talkTo(t) {
696
+ const n = this.registry.characters[t];
697
+ if (!n || !n.dialogue)
698
+ return this.buildSnapshotAndClearTransients();
699
+ const r = this.registry.dialogues[n.dialogue];
700
+ if (!r)
701
+ return this.buildSnapshotAndClearTransients();
702
+ const a = r.nodes.find((s) => s.id === r.startNode);
703
+ return a ? (this.state = {
704
+ ...this.state,
705
+ dialogueState: {
706
+ dialogueId: r.id,
707
+ nodeId: a.id
708
+ }
709
+ }, a.effects && (this.state = y(a.effects, this.state)), this.buildSnapshotAndClearTransients()) : this.buildSnapshotAndClearTransients();
710
+ }
711
+ /**
712
+ * Player clicked on an item to pick it up.
713
+ *
714
+ * Adds the item to inventory if it's at the current location.
715
+ *
716
+ * @param itemId - ID of the item to take
717
+ * @returns New snapshot with item in inventory
718
+ */
719
+ takeItem(t) {
720
+ return this.state.itemLocations[t] !== this.state.currentLocation ? this.buildSnapshotAndClearTransients() : (this.state = {
721
+ ...this.state,
722
+ inventory: [...this.state.inventory, t],
723
+ itemLocations: {
724
+ ...this.state.itemLocations,
725
+ [t]: "inventory"
726
+ }
727
+ }, this.buildSnapshotAndClearTransients());
728
+ }
729
+ /**
730
+ * Player clicked on a map location to travel.
731
+ *
732
+ * Changes location, advances time based on distance, and checks for triggered dialogues.
733
+ *
734
+ * @param locationId - ID of the destination location
735
+ * @returns New snapshot at the new location
736
+ */
737
+ travelTo(t) {
738
+ if (!this.state.mapEnabled)
739
+ return this.buildSnapshotAndClearTransients();
740
+ const n = Object.keys(this.registry.maps);
741
+ if (n.length === 0)
742
+ return this.buildSnapshotAndClearTransients();
743
+ const r = this.registry.maps[n[0]];
744
+ if (!r)
745
+ return this.buildSnapshotAndClearTransients();
746
+ const a = r.locations.find((m) => m.id === this.state.currentLocation), s = r.locations.find((m) => m.id === t);
747
+ if (!a || !s)
748
+ return this.buildSnapshotAndClearTransients();
749
+ const i = Math.sqrt(
750
+ Math.pow(s.x - a.x, 2) + Math.pow(s.y - a.y, 2)
751
+ ), o = Math.round(i * r.scale), c = this.state.currentTime.hour + o, u = Math.floor(c / 24), d = c % 24;
752
+ return this.state = {
753
+ ...this.state,
754
+ currentLocation: t,
755
+ dialogueState: null,
756
+ currentTime: {
757
+ day: this.state.currentTime.day + u,
758
+ hour: d
759
+ }
760
+ }, this.checkTriggeredDialogues(), this.buildSnapshotAndClearTransients();
761
+ }
762
+ /**
763
+ * Player wrote a note.
764
+ *
765
+ * Adds a note to the player's journal.
766
+ *
767
+ * @param title - Note title
768
+ * @param text - Note content
769
+ * @returns New snapshot with note added
770
+ */
771
+ writeNote(t, n) {
772
+ const r = {
773
+ id: `note_${Date.now()}`,
774
+ title: t,
775
+ text: n
776
+ };
777
+ return this.state = {
778
+ ...this.state,
779
+ playerNotes: [...this.state.playerNotes, r]
780
+ }, this.buildSnapshotAndClearTransients();
781
+ }
782
+ /**
783
+ * Player deleted a note.
784
+ *
785
+ * Removes a note from the player's journal.
786
+ *
787
+ * @param noteId - ID of the note to delete
788
+ * @returns New snapshot with note removed
789
+ */
790
+ deleteNote(t) {
791
+ return this.state = {
792
+ ...this.state,
793
+ playerNotes: this.state.playerNotes.filter((n) => n.id !== t)
794
+ }, this.buildSnapshotAndClearTransients();
795
+ }
796
+ /**
797
+ * Change the current language.
798
+ *
799
+ * Updates the locale and rebuilds the snapshot with new translations.
800
+ *
801
+ * @param locale - Language code (e.g., "en", "es")
802
+ * @returns New snapshot with updated locale
803
+ */
804
+ setLocale(t) {
805
+ return this.state = {
806
+ ...this.state,
807
+ currentLocale: t
808
+ }, this.buildSnapshotAndClearTransients();
809
+ }
810
+ /**
811
+ * Get the current snapshot without making any changes.
812
+ *
813
+ * Useful for initial rendering or refreshing the view.
814
+ *
815
+ * @returns Current snapshot
816
+ */
817
+ getSnapshot() {
818
+ return this.buildSnapshotAndClearTransients();
819
+ }
820
+ // ===========================================================================
821
+ // Internal Helper Methods
822
+ // ===========================================================================
823
+ /**
824
+ * Build a snapshot and clear transient state (notifications, pendingSounds).
825
+ * Transient state is data that should only appear in one snapshot.
826
+ */
827
+ buildSnapshotAndClearTransients() {
828
+ const t = gt(this.state, this.registry);
829
+ return this.state = {
830
+ ...this.state,
831
+ notifications: [],
832
+ pendingSounds: [],
833
+ pendingVideo: null
834
+ }, t;
835
+ }
836
+ /**
837
+ * Check for dialogues that should auto-trigger at the current location.
838
+ *
839
+ * If a dialogue matches the current location and all its conditions pass,
840
+ * start that dialogue.
841
+ *
842
+ * This is called after location changes (newGame, travelTo).
843
+ */
844
+ checkTriggeredDialogues() {
845
+ for (const t of Object.values(this.registry.dialogues)) {
846
+ if (t.triggerLocation !== this.state.currentLocation || t.conditions && !A(t.conditions, this.state))
847
+ continue;
848
+ const n = t.nodes.find((r) => r.id === t.startNode);
849
+ if (n) {
850
+ this.state = {
851
+ ...this.state,
852
+ dialogueState: {
853
+ dialogueId: t.id,
854
+ nodeId: n.id
855
+ }
856
+ }, n.effects && (this.state = y(n.effects, this.state));
857
+ break;
858
+ }
859
+ }
860
+ }
861
+ }
862
+ function Nt(e) {
863
+ return e.split(`
864
+ `).map((t, n) => ({
865
+ original: t,
866
+ lineNumber: n + 1
867
+ })).map(({ original: t, lineNumber: n }) => {
868
+ let r = t;
869
+ const a = t.indexOf("#");
870
+ if (a === -1)
871
+ r = t;
872
+ else {
873
+ const o = t.match(/"[^"]*"/);
874
+ if (o) {
875
+ const c = t.indexOf(o[0]), u = c + o[0].length;
876
+ a < c ? r = t.substring(0, a) : a >= c && a < u ? r = t.substring(0, u) + t.substring(u).split("#")[0] : r = t.substring(0, a);
877
+ } else
878
+ r = t.substring(0, a);
879
+ }
880
+ const s = r.length - r.trimStart().length;
881
+ return { line: r.trim(), lineNumber: n, indent: s };
882
+ }).filter((t) => t.line.length > 0);
883
+ }
884
+ function I(e) {
885
+ const t = e.trim();
886
+ return t.startsWith("@") ? t : t.startsWith('"') && t.endsWith('"') ? t.substring(1, t.length - 1) : t;
887
+ }
888
+ function T(e) {
889
+ const t = e.trim().split(/\s+/), n = t[0];
890
+ switch (n) {
891
+ case "hasFlag":
892
+ return { type: "hasFlag", flag: t[1] };
893
+ case "notFlag":
894
+ return { type: "notFlag", flag: t[1] };
895
+ case "hasItem":
896
+ return { type: "hasItem", itemId: t[1] };
897
+ case "variableEquals":
898
+ return {
899
+ type: "variableEquals",
900
+ variable: t[1],
901
+ value: isNaN(Number(t[2])) ? t[2] : Number(t[2])
902
+ };
903
+ case "variableGreaterThan":
904
+ return {
905
+ type: "variableGreaterThan",
906
+ variable: t[1],
907
+ value: Number(t[2])
908
+ };
909
+ case "variableLessThan":
910
+ return {
911
+ type: "variableLessThan",
912
+ variable: t[1],
913
+ value: Number(t[2])
914
+ };
915
+ case "atLocation":
916
+ return { type: "atLocation", locationId: t[1] };
917
+ case "questAtStage":
918
+ return { type: "questAtStage", questId: t[1], stageId: t[2] };
919
+ case "characterAt":
920
+ return {
921
+ type: "characterAt",
922
+ characterId: t[1],
923
+ locationId: t[2]
924
+ };
925
+ case "characterInParty":
926
+ return { type: "characterInParty", characterId: t[1] };
927
+ case "relationshipAbove":
928
+ return {
929
+ type: "relationshipAbove",
930
+ characterId: t[1],
931
+ value: Number(t[2])
932
+ };
933
+ case "relationshipBelow":
934
+ return {
935
+ type: "relationshipBelow",
936
+ characterId: t[1],
937
+ value: Number(t[2])
938
+ };
939
+ case "timeIs":
940
+ return { type: "timeIs", startHour: Number(t[1]), endHour: Number(t[2]) };
941
+ case "itemAt":
942
+ return { type: "itemAt", itemId: t[1], locationId: t[2] };
943
+ default:
944
+ throw new Error(`Unknown condition type: ${n}`);
945
+ }
946
+ }
947
+ function E(e) {
948
+ const t = e.trim();
949
+ if (t.startsWith("NOTIFY "))
950
+ return { type: "notify", message: I(t.substring(7)) };
951
+ if (t.startsWith("MUSIC "))
952
+ return { type: "playMusic", track: t.substring(6).trim() };
953
+ if (t.startsWith("SOUND "))
954
+ return { type: "playSound", sound: t.substring(6).trim() };
955
+ if (t.startsWith("VIDEO "))
956
+ return { type: "playVideo", file: t.substring(6).trim() };
957
+ const n = t.split(/\s+/), r = n[0];
958
+ switch (r) {
959
+ case "SET":
960
+ if (n[1] === "flag")
961
+ return { type: "setFlag", flag: n[2] };
962
+ if (n[1] === "variable")
963
+ return {
964
+ type: "setVariable",
965
+ variable: n[2],
966
+ value: isNaN(Number(n[3])) ? n[3] : Number(n[3])
967
+ };
968
+ if (n[1] === "questStage")
969
+ return { type: "setQuestStage", questId: n[2], stageId: n[3] };
970
+ if (n[1] === "characterLocation")
971
+ return {
972
+ type: "setCharacterLocation",
973
+ characterId: n[2],
974
+ locationId: n[3]
975
+ };
976
+ if (n[1] === "relationship")
977
+ return {
978
+ type: "setRelationship",
979
+ characterId: n[2],
980
+ value: Number(n[3])
981
+ };
982
+ if (n[1] === "characterStat")
983
+ return {
984
+ type: "setCharacterStat",
985
+ characterId: n[2],
986
+ stat: n[3],
987
+ value: isNaN(Number(n[4])) ? n[4] : Number(n[4])
988
+ };
989
+ if (n[1] === "mapEnabled")
990
+ return { type: "setMapEnabled", enabled: n[2] === "true" };
991
+ throw new Error(`Unknown SET effect: ${n[1]}`);
992
+ case "CLEAR":
993
+ if (n[1] === "flag")
994
+ return { type: "clearFlag", flag: n[2] };
995
+ throw new Error(`Unknown CLEAR effect: ${n[1]}`);
996
+ case "ADD":
997
+ if (n[1] === "variable")
998
+ return {
999
+ type: "addVariable",
1000
+ variable: n[2],
1001
+ value: Number(n[3])
1002
+ };
1003
+ if (n[1] === "item")
1004
+ return { type: "addItem", itemId: n[2] };
1005
+ if (n[1] === "journalEntry")
1006
+ return { type: "addJournalEntry", entryId: n[2] };
1007
+ if (n[1] === "toParty")
1008
+ return { type: "addToParty", characterId: n[2] };
1009
+ if (n[1] === "relationship")
1010
+ return {
1011
+ type: "addRelationship",
1012
+ characterId: n[2],
1013
+ value: Number(n[3])
1014
+ };
1015
+ if (n[1] === "characterStat")
1016
+ return {
1017
+ type: "addCharacterStat",
1018
+ characterId: n[2],
1019
+ stat: n[3],
1020
+ value: Number(n[4])
1021
+ };
1022
+ throw new Error(`Unknown ADD effect: ${n[1]}`);
1023
+ case "REMOVE":
1024
+ if (n[1] === "item")
1025
+ return { type: "removeItem", itemId: n[2] };
1026
+ if (n[1] === "fromParty")
1027
+ return { type: "removeFromParty", characterId: n[2] };
1028
+ throw new Error(`Unknown REMOVE effect: ${n[1]}`);
1029
+ case "MOVE":
1030
+ if (n[1] === "item")
1031
+ return { type: "moveItem", itemId: n[2], locationId: n[3] };
1032
+ throw new Error(`Unknown MOVE effect: ${n[1]}`);
1033
+ case "GOTO":
1034
+ if (n[1] === "location")
1035
+ return { type: "goToLocation", locationId: n[2] };
1036
+ throw new Error("GOTO should not be parsed as an effect");
1037
+ case "ADVANCE":
1038
+ if (n[1] === "time")
1039
+ return { type: "advanceTime", hours: Number(n[2]) };
1040
+ throw new Error(`Unknown ADVANCE effect: ${n[1]}`);
1041
+ case "START":
1042
+ if (n[1] === "dialogue")
1043
+ return { type: "startDialogue", dialogueId: n[2] };
1044
+ throw new Error(`Unknown START effect: ${n[1]}`);
1045
+ case "END":
1046
+ if (n[1] === "dialogue")
1047
+ return { type: "endDialogue" };
1048
+ throw new Error("END should not be parsed as an effect");
1049
+ default:
1050
+ throw new Error(`Unknown effect keyword: ${r}`);
1051
+ }
1052
+ }
1053
+ function Ct(e, t, n) {
1054
+ const r = e[t], a = I(r.line.substring(7)), s = [], i = [];
1055
+ let o = "", c = t + 1;
1056
+ const u = r.indent;
1057
+ for (; c < e.length; ) {
1058
+ const l = e[c];
1059
+ if (l.line === "END" && l.indent === u) {
1060
+ c++;
1061
+ break;
1062
+ }
1063
+ if (l.line.startsWith("REQUIRE ")) {
1064
+ const f = l.line.substring(8).trim();
1065
+ s.push(T(f)), c++;
1066
+ } else if (l.line.startsWith("GOTO ")) {
1067
+ const f = l.line.substring(5).trim();
1068
+ if (f.startsWith("location ")) {
1069
+ const h = f.substring(9).trim();
1070
+ i.push({ type: "goToLocation", locationId: h }), i.push({ type: "endDialogue" }), o = "";
1071
+ } else
1072
+ o = f;
1073
+ c++;
1074
+ } else l.line.includes(":") || i.push(E(l.line)), c++;
1075
+ }
1076
+ const d = a.replace(/[@"]/g, "").replace(/[^a-z0-9]/gi, "_");
1077
+ return { choice: {
1078
+ id: `${n}_choice_${d.toLowerCase().substring(0, 30)}`,
1079
+ text: a,
1080
+ conditions: s.length > 0 ? s : void 0,
1081
+ effects: i.length > 0 ? i : void 0,
1082
+ next: o || ""
1083
+ }, nextIndex: c };
1084
+ }
1085
+ function Lt(e, t) {
1086
+ const n = e[t], r = n.line.substring(3).trim(), a = T(r);
1087
+ let s;
1088
+ const i = [];
1089
+ let o = t + 1;
1090
+ const c = n.indent;
1091
+ for (; o < e.length; ) {
1092
+ const u = e[o];
1093
+ if (u.line === "END" && u.indent === c) {
1094
+ o++;
1095
+ break;
1096
+ }
1097
+ u.line.startsWith("GOTO ") ? (s = u.line.substring(5).trim(), o++) : (i.push(E(u.line)), o++);
1098
+ }
1099
+ return { condition: a, next: s, effects: i, nextIndex: o };
1100
+ }
1101
+ function kt(e, t) {
1102
+ const r = e[t].line.substring(5).trim();
1103
+ let a = null, s = "", i, o;
1104
+ const c = [], u = [], d = [];
1105
+ let m;
1106
+ const g = [];
1107
+ let l = t + 1;
1108
+ for (; l < e.length; ) {
1109
+ const h = e[l];
1110
+ if (h.line.startsWith("NODE "))
1111
+ break;
1112
+ if (h.line.includes(":") && !h.line.startsWith("VOICE")) {
1113
+ const p = h.line.indexOf(":"), b = h.line.substring(0, p).trim(), S = h.line.substring(p + 1).trim();
1114
+ b === "NARRATOR" ? a = null : a = b.toLowerCase(), s = I(S), l++;
1115
+ } else if (h.line.startsWith("VOICE "))
1116
+ i = h.line.substring(6).trim(), l++;
1117
+ else if (h.line.startsWith("PORTRAIT "))
1118
+ o = h.line.substring(9).trim(), l++;
1119
+ else if (h.line.startsWith("CHOICE ")) {
1120
+ const p = Ct(e, l, r);
1121
+ u.push(p.choice), l = p.nextIndex;
1122
+ } else if (h.line.startsWith("IF ")) {
1123
+ const p = Lt(e, l);
1124
+ p.next && g.push({
1125
+ condition: p.condition,
1126
+ next: p.next
1127
+ }), d.push(...p.effects), l = p.nextIndex;
1128
+ } else if (h.line.startsWith("GOTO ")) {
1129
+ const p = h.line.substring(5).trim();
1130
+ if (p.startsWith("location ")) {
1131
+ const b = p.substring(9).trim();
1132
+ d.push({ type: "goToLocation", locationId: b }), d.push({ type: "endDialogue" });
1133
+ } else
1134
+ m = p;
1135
+ l++;
1136
+ } else
1137
+ d.push(E(h.line)), l++;
1138
+ }
1139
+ const f = {
1140
+ id: r,
1141
+ speaker: a,
1142
+ text: s,
1143
+ voice: i,
1144
+ portrait: o,
1145
+ conditions: c.length > 0 ? c : void 0,
1146
+ choices: u,
1147
+ effects: d.length > 0 ? d : void 0,
1148
+ next: m
1149
+ };
1150
+ return g.length > 0 && (f.conditionalNext = g), { node: f, nextIndex: l };
1151
+ }
1152
+ function Rt(e, t) {
1153
+ const n = Nt(e);
1154
+ let r;
1155
+ const a = [], s = [];
1156
+ let i = "", o = 0;
1157
+ for (; o < n.length; ) {
1158
+ const c = n[o];
1159
+ if (c.line.startsWith("TRIGGER "))
1160
+ r = c.line.substring(8).trim(), o++;
1161
+ else if (c.line.startsWith("REQUIRE ")) {
1162
+ const u = c.line.substring(8).trim();
1163
+ a.push(T(u)), o++;
1164
+ } else if (c.line.startsWith("NODE ")) {
1165
+ const u = kt(n, o);
1166
+ s.push(u.node), i || (i = u.node.id), o = u.nextIndex;
1167
+ } else
1168
+ throw new Error(`Unexpected token at line ${c.lineNumber}: ${c.line}`);
1169
+ }
1170
+ return {
1171
+ id: t,
1172
+ triggerLocation: r,
1173
+ conditions: a.length > 0 ? a : void 0,
1174
+ startNode: i,
1175
+ nodes: s
1176
+ };
1177
+ }
1178
+ const Pt = "0.0.1";
1179
+ export {
1180
+ Vt as Engine,
1181
+ Pt as VERSION,
1182
+ J as applyEffect,
1183
+ y as applyEffects,
1184
+ gt as buildSnapshot,
1185
+ Ot as createResolver,
1186
+ x as evaluateCondition,
1187
+ A as evaluateConditions,
1188
+ T as parseCondition,
1189
+ Rt as parseDialogue,
1190
+ E as parseEffect,
1191
+ w as resolveText
1192
+ };