clibuddy 1.0.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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/adventure/adventureUI.d.ts +24 -0
  4. package/dist/adventure/adventureUI.js +290 -0
  5. package/dist/adventure/adventures.d.ts +4 -0
  6. package/dist/adventure/adventures.js +206 -0
  7. package/dist/adventure/biomes.d.ts +30 -0
  8. package/dist/adventure/biomes.js +80 -0
  9. package/dist/adventure/combat/combat.d.ts +14 -0
  10. package/dist/adventure/combat/combat.js +638 -0
  11. package/dist/adventure/combat/combatUI.d.ts +5 -0
  12. package/dist/adventure/combat/combatUI.js +116 -0
  13. package/dist/adventure/combat/conditions.d.ts +20 -0
  14. package/dist/adventure/combat/conditions.js +111 -0
  15. package/dist/adventure/combat/enemies.d.ts +4 -0
  16. package/dist/adventure/combat/enemies.js +430 -0
  17. package/dist/adventure/combat/gear.d.ts +3 -0
  18. package/dist/adventure/combat/gear.js +199 -0
  19. package/dist/adventure/combat/skills.d.ts +6 -0
  20. package/dist/adventure/combat/skills.js +197 -0
  21. package/dist/adventure/combat.d.ts +31 -0
  22. package/dist/adventure/combat.js +732 -0
  23. package/dist/adventure/combatUI.d.ts +5 -0
  24. package/dist/adventure/combatUI.js +116 -0
  25. package/dist/adventure/endless.d.ts +18 -0
  26. package/dist/adventure/endless.js +154 -0
  27. package/dist/adventure/enemies.d.ts +4 -0
  28. package/dist/adventure/enemies.js +320 -0
  29. package/dist/adventure/engine.d.ts +20 -0
  30. package/dist/adventure/engine.js +137 -0
  31. package/dist/adventure/gear.d.ts +3 -0
  32. package/dist/adventure/gear.js +149 -0
  33. package/dist/adventure/generation/biomes.d.ts +30 -0
  34. package/dist/adventure/generation/biomes.js +102 -0
  35. package/dist/adventure/generation/endless.d.ts +18 -0
  36. package/dist/adventure/generation/endless.js +154 -0
  37. package/dist/adventure/generation/generator.d.ts +9 -0
  38. package/dist/adventure/generation/generator.js +245 -0
  39. package/dist/adventure/generation/templates.d.ts +25 -0
  40. package/dist/adventure/generation/templates.js +228 -0
  41. package/dist/adventure/generator.d.ts +9 -0
  42. package/dist/adventure/generator.js +245 -0
  43. package/dist/adventure/skills.d.ts +6 -0
  44. package/dist/adventure/skills.js +197 -0
  45. package/dist/adventure/templates.d.ts +25 -0
  46. package/dist/adventure/templates.js +228 -0
  47. package/dist/adventure/types.d.ts +236 -0
  48. package/dist/adventure/types.js +97 -0
  49. package/dist/app/state.d.ts +49 -0
  50. package/dist/app/state.js +51 -0
  51. package/dist/buddy/activities.d.ts +16 -0
  52. package/dist/buddy/activities.js +90 -0
  53. package/dist/buddy/decay.d.ts +3 -0
  54. package/dist/buddy/decay.js +45 -0
  55. package/dist/buddy/leveling.d.ts +11 -0
  56. package/dist/buddy/leveling.js +25 -0
  57. package/dist/buddy/roll.d.ts +4 -0
  58. package/dist/buddy/roll.js +61 -0
  59. package/dist/buddy/species.d.ts +4 -0
  60. package/dist/buddy/species.js +592 -0
  61. package/dist/buddy/titles.d.ts +17 -0
  62. package/dist/buddy/titles.js +89 -0
  63. package/dist/buddy/types.d.ts +92 -0
  64. package/dist/buddy/types.js +21 -0
  65. package/dist/commands/actions.d.ts +2 -0
  66. package/dist/commands/actions.js +141 -0
  67. package/dist/commands/admin.d.ts +2 -0
  68. package/dist/commands/admin.js +202 -0
  69. package/dist/commands/registry.d.ts +25 -0
  70. package/dist/commands/registry.js +31 -0
  71. package/dist/commands/social.d.ts +2 -0
  72. package/dist/commands/social.js +92 -0
  73. package/dist/dialogue/engine.d.ts +7 -0
  74. package/dist/dialogue/engine.js +68 -0
  75. package/dist/dialogue/lines.d.ts +26 -0
  76. package/dist/dialogue/lines.js +294 -0
  77. package/dist/events/engine.d.ts +20 -0
  78. package/dist/events/engine.js +51 -0
  79. package/dist/events/events.d.ts +13 -0
  80. package/dist/events/events.js +149 -0
  81. package/dist/index.d.ts +10 -0
  82. package/dist/index.js +1665 -0
  83. package/dist/inventory/finding.d.ts +11 -0
  84. package/dist/inventory/finding.js +99 -0
  85. package/dist/inventory/items.d.ts +31 -0
  86. package/dist/inventory/items.js +63 -0
  87. package/dist/minigames/copycat.d.ts +2 -0
  88. package/dist/minigames/copycat.js +153 -0
  89. package/dist/minigames/fetch.d.ts +2 -0
  90. package/dist/minigames/fetch.js +179 -0
  91. package/dist/minigames/moodmatch.d.ts +2 -0
  92. package/dist/minigames/moodmatch.js +144 -0
  93. package/dist/minigames/quickpaws.d.ts +2 -0
  94. package/dist/minigames/quickpaws.js +142 -0
  95. package/dist/minigames/registry.d.ts +5 -0
  96. package/dist/minigames/registry.js +16 -0
  97. package/dist/minigames/rpsplus.d.ts +2 -0
  98. package/dist/minigames/rpsplus.js +168 -0
  99. package/dist/minigames/treasurehunt.d.ts +2 -0
  100. package/dist/minigames/treasurehunt.js +146 -0
  101. package/dist/minigames/types.d.ts +30 -0
  102. package/dist/minigames/types.js +69 -0
  103. package/dist/rendering/commandPalette.d.ts +16 -0
  104. package/dist/rendering/commandPalette.js +77 -0
  105. package/dist/rendering/display.d.ts +9 -0
  106. package/dist/rendering/display.js +231 -0
  107. package/dist/rendering/inventoryUI.d.ts +14 -0
  108. package/dist/rendering/inventoryUI.js +99 -0
  109. package/dist/rendering/items.d.ts +7 -0
  110. package/dist/rendering/items.js +34 -0
  111. package/dist/rendering/listUtils.d.ts +3 -0
  112. package/dist/rendering/listUtils.js +24 -0
  113. package/dist/rendering/minigameUI.d.ts +11 -0
  114. package/dist/rendering/minigameUI.js +37 -0
  115. package/dist/rendering/overlayUI.d.ts +24 -0
  116. package/dist/rendering/overlayUI.js +184 -0
  117. package/dist/rendering/scene.d.ts +8 -0
  118. package/dist/rendering/scene.js +87 -0
  119. package/dist/rendering/screen.d.ts +43 -0
  120. package/dist/rendering/screen.js +97 -0
  121. package/dist/sound/sound.d.ts +11 -0
  122. package/dist/sound/sound.js +55 -0
  123. package/dist/state/save.d.ts +5 -0
  124. package/dist/state/save.js +100 -0
  125. package/dist/state/settings.d.ts +7 -0
  126. package/dist/state/settings.js +81 -0
  127. package/dist/story/mainStory.d.ts +4 -0
  128. package/dist/story/mainStory.js +3111 -0
  129. package/dist/story/npcs.d.ts +17 -0
  130. package/dist/story/npcs.js +137 -0
  131. package/dist/story/progress.d.ts +26 -0
  132. package/dist/story/progress.js +168 -0
  133. package/dist/story/seasonal.d.ts +6 -0
  134. package/dist/story/seasonal.js +96 -0
  135. package/dist/story/shop.d.ts +7 -0
  136. package/dist/story/shop.js +26 -0
  137. package/dist/story/sideQuests.d.ts +4 -0
  138. package/dist/story/sideQuests.js +151 -0
  139. package/dist/story/types.d.ts +61 -0
  140. package/dist/story/types.js +36 -0
  141. package/dist/updates.d.ts +23 -0
  142. package/dist/updates.js +142 -0
  143. package/package.json +53 -0
package/dist/index.js ADDED
@@ -0,0 +1,1665 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const readline = __importStar(require("readline"));
38
+ const types_1 = require("./buddy/types");
39
+ const roll_1 = require("./buddy/roll");
40
+ const decay_1 = require("./buddy/decay");
41
+ const activities_1 = require("./buddy/activities");
42
+ const species_1 = require("./buddy/species");
43
+ const save_1 = require("./state/save");
44
+ const screen_1 = require("./rendering/screen");
45
+ const display_1 = require("./rendering/display");
46
+ const commandPalette_1 = require("./rendering/commandPalette");
47
+ const registry_1 = require("./commands/registry");
48
+ const engine_1 = require("./dialogue/engine");
49
+ const lines_1 = require("./dialogue/lines");
50
+ const engine_2 = require("./events/engine");
51
+ const titles_1 = require("./buddy/titles");
52
+ const finding_1 = require("./inventory/finding");
53
+ const items_1 = require("./inventory/items");
54
+ const inventoryUI_1 = require("./rendering/inventoryUI");
55
+ const overlayUI_1 = require("./rendering/overlayUI");
56
+ const shop_1 = require("./story/shop");
57
+ const progress_1 = require("./story/progress");
58
+ const settings_1 = require("./state/settings");
59
+ const sound_1 = require("./sound/sound");
60
+ const types_2 = require("./adventure/types");
61
+ const engine_3 = require("./adventure/engine");
62
+ const combat_1 = require("./adventure/combat/combat");
63
+ const enemies_1 = require("./adventure/combat/enemies");
64
+ const skills_1 = require("./adventure/combat/skills");
65
+ const combatUI_1 = require("./adventure/combat/combatUI");
66
+ const adventureUI_1 = require("./adventure/adventureUI");
67
+ const progress_2 = require("./story/progress");
68
+ const mainStory_1 = require("./story/mainStory");
69
+ const generator_1 = require("./adventure/generation/generator");
70
+ const endless_1 = require("./adventure/generation/endless");
71
+ const minigameUI_1 = require("./rendering/minigameUI");
72
+ const types_3 = require("./minigames/types");
73
+ const registry_2 = require("./minigames/registry");
74
+ const titles_2 = require("./buddy/titles");
75
+ const leveling_1 = require("./buddy/leveling");
76
+ // Register all commands
77
+ require("./commands/actions");
78
+ if (process.env.CLIBUDDY_DEV)
79
+ require("./commands/admin");
80
+ require("./commands/social");
81
+ // Register all mini-games
82
+ require("./minigames/copycat");
83
+ require("./minigames/quickpaws");
84
+ require("./minigames/treasurehunt");
85
+ require("./minigames/moodmatch");
86
+ require("./minigames/fetch");
87
+ require("./minigames/rpsplus");
88
+ // ─── App State ───────────────────────────────────────────────
89
+ const screen = new screen_1.Screen();
90
+ let buddyState = null;
91
+ let animFrame = 0;
92
+ let animTimer = null;
93
+ let decayTimer = null;
94
+ let feedbackMessage;
95
+ let feedbackTimeout = null;
96
+ let dialogueMessage;
97
+ let dialogueSource;
98
+ let dialogueTimeout = null;
99
+ let dialogueTick = 0;
100
+ let activeEvent = null;
101
+ let inventoryUI = (0, inventoryUI_1.createInventoryUI)();
102
+ let overlay = (0, overlayUI_1.createOverlay)();
103
+ let minigameMenu = (0, minigameUI_1.createMinigameMenu)();
104
+ let activeMiniGame = (0, types_3.createInactiveGame)();
105
+ let activeAdventure = (0, types_2.createInactiveAdventure)();
106
+ let currentAdventureDef = null;
107
+ let endlessDepth = 0;
108
+ let isEndlessMode = false;
109
+ let adventureMenu = (0, adventureUI_1.createAdventureMenu)();
110
+ let showingSkillMenu = false;
111
+ let petSequence = null;
112
+ let petTick = 0;
113
+ let palette = (0, commandPalette_1.createPalette)();
114
+ let inputBuffer = "";
115
+ let pendingCommand = null; // command selected, waiting for args
116
+ let argsBuffer = "";
117
+ let appPhase = "roll-wait";
118
+ let nameBuffer = "";
119
+ // ─── Rendering ───────────────────────────────────────────────
120
+ function redraw() {
121
+ if (appPhase === "playing" && buddyState) {
122
+ // Active adventure takes over the screen
123
+ if (activeAdventure.active) {
124
+ const adv = currentAdventureDef;
125
+ if (adv) {
126
+ if (activeAdventure.phase === "combat" && activeAdventure.combat) {
127
+ let screen_ = (0, combatUI_1.renderCombat)(activeAdventure.combat, buddyState, adv.name);
128
+ if (showingSkillMenu) {
129
+ screen_ += "\n" + (0, combatUI_1.renderSkillMenu)(activeAdventure.combat, buddyState);
130
+ }
131
+ screen.draw(screen_, ` ${screen_1.ansi.dim}Esc to flee${screen_1.ansi.reset}`);
132
+ }
133
+ else if (activeAdventure.phase === "result") {
134
+ screen.draw((0, adventureUI_1.renderResult)(activeAdventure, adv, buddyState.name), ` ${screen_1.ansi.dim}Press Enter${screen_1.ansi.reset}`);
135
+ }
136
+ else {
137
+ screen.draw((0, adventureUI_1.renderRoom)(activeAdventure, adv, buddyState.name), ` ${screen_1.ansi.dim}Esc to retreat${screen_1.ansi.reset}`);
138
+ }
139
+ }
140
+ return;
141
+ }
142
+ // Active mini-game takes over the screen entirely
143
+ if (activeMiniGame.active) {
144
+ const handler = (0, registry_2.getGameHandler)(activeMiniGame.gameId);
145
+ if (handler) {
146
+ const gameScreen = handler.render(activeMiniGame, buddyState.name);
147
+ screen.draw(gameScreen, ` ${screen_1.ansi.dim}Esc to quit${screen_1.ansi.reset}`);
148
+ }
149
+ return;
150
+ }
151
+ const main = (0, display_1.renderMainScreen)(buddyState, animFrame, feedbackMessage, dialogueMessage, dialogueSource);
152
+ // Minigame menu overlay
153
+ if (adventureMenu.active) {
154
+ const menuLines = (0, adventureUI_1.renderAdventureMenu)(adventureMenu, buddyState);
155
+ const combined = main + "\n" + menuLines.join("\n");
156
+ screen.draw(combined, ` ${screen_1.ansi.dim}Esc to close${screen_1.ansi.reset}`);
157
+ }
158
+ else if (minigameMenu.active) {
159
+ const menuLines = (0, minigameUI_1.renderMinigameMenu)(minigameMenu);
160
+ const combined = main + "\n" + menuLines.join("\n");
161
+ screen.draw(combined, ` ${screen_1.ansi.dim}Esc to close${screen_1.ansi.reset}`);
162
+ }
163
+ else if (overlay.type) {
164
+ let overlayLines;
165
+ if (overlay.type === "titles") {
166
+ overlayLines = (0, overlayUI_1.renderTitlesOverlay)(buddyState, overlay);
167
+ }
168
+ else if (overlay.type === "notifications") {
169
+ overlayLines = (0, overlayUI_1.renderNotificationsOverlay)(buddyState.notifications, overlay);
170
+ }
171
+ else if (overlay.type === "gear") {
172
+ overlayLines = (0, overlayUI_1.renderGearOverlay)(buddyState);
173
+ }
174
+ else if (overlay.type === "combatinfo") {
175
+ overlayLines = (0, overlayUI_1.renderCombatInfoOverlay)(buddyState);
176
+ }
177
+ else if (overlay.type === "shop") {
178
+ overlayLines = (0, overlayUI_1.renderShopOverlay)(buddyState, overlay);
179
+ }
180
+ else {
181
+ overlayLines = (0, overlayUI_1.renderSettingsOverlay)((0, settings_1.getSettings)(), overlay);
182
+ }
183
+ const combined = main + "\n" + overlayLines.join("\n");
184
+ screen.draw(combined, ` ${screen_1.ansi.dim}Esc to close${screen_1.ansi.reset}`);
185
+ }
186
+ else if (inventoryUI.active) {
187
+ const s = buddyState.adventureStats;
188
+ const invLines = (0, inventoryUI_1.renderInventoryUI)(inventoryUI, s.equippedWeapon, s.equippedArmor, s.equippedHelmet, s.equippedBoots, s.equippedAccessory, buddyState.questProgress.gold);
189
+ const combined = main + "\n" + invLines.join("\n");
190
+ screen.draw(combined, ` ${screen_1.ansi.dim}Inventory open${screen_1.ansi.reset}`);
191
+ }
192
+ else if (palette.active) {
193
+ const paletteLines = (0, commandPalette_1.renderPalette)(palette);
194
+ const combined = main + "\n" + paletteLines.join("\n");
195
+ const inputDisplay = ` ${screen_1.ansi.dim}>${screen_1.ansi.reset} /${palette.filter}`;
196
+ screen.draw(combined, inputDisplay);
197
+ }
198
+ else if (pendingCommand) {
199
+ const inputDisplay = ` ${screen_1.ansi.dim}>${screen_1.ansi.reset} /${pendingCommand} ${argsBuffer}`;
200
+ screen.draw(main, inputDisplay);
201
+ }
202
+ else {
203
+ const inputDisplay = inputBuffer
204
+ ? ` ${screen_1.ansi.dim}>${screen_1.ansi.reset} ${inputBuffer}`
205
+ : ` ${screen_1.ansi.dim}> Type / for commands${screen_1.ansi.reset}`;
206
+ screen.draw(main, inputDisplay);
207
+ }
208
+ }
209
+ }
210
+ function showFeedback(msg, durationMs = 3000) {
211
+ feedbackMessage = msg;
212
+ if (feedbackTimeout)
213
+ clearTimeout(feedbackTimeout);
214
+ feedbackTimeout = setTimeout(() => {
215
+ feedbackMessage = undefined;
216
+ redraw();
217
+ }, durationMs);
218
+ redraw();
219
+ }
220
+ // ─── Animation Loop ──────────────────────────────────────────
221
+ function startAnimation() {
222
+ animTimer = setInterval(() => {
223
+ animFrame++;
224
+ // Tick active adventure combat
225
+ if (activeAdventure.active && activeAdventure.combat) {
226
+ const phase = activeAdventure.combat.phase;
227
+ if (phase === "player_animate" || phase === "enemy_turn" || phase === "enemy_animate") {
228
+ activeAdventure = {
229
+ ...activeAdventure,
230
+ combat: (0, combat_1.tickCombat)(activeAdventure.combat),
231
+ };
232
+ }
233
+ redraw();
234
+ return;
235
+ }
236
+ // Tick active mini-game
237
+ if (activeMiniGame.active) {
238
+ const handler = (0, registry_2.getGameHandler)(activeMiniGame.gameId);
239
+ if (handler) {
240
+ activeMiniGame = handler.tick(activeMiniGame);
241
+ // Check if game ended during tick
242
+ if (activeMiniGame.phase === "result" && activeMiniGame.ticksInPhase > 8) {
243
+ finishMiniGame();
244
+ }
245
+ }
246
+ redraw();
247
+ return; // Skip normal processing during games
248
+ }
249
+ // Process activity progress each frame
250
+ if (buddyState?.activity) {
251
+ const beforeType = buddyState.activity.type;
252
+ const beforeLevel = buddyState.level;
253
+ buddyState = (0, activities_1.applyActivityProgress)(buddyState);
254
+ // Activity just completed
255
+ if (beforeType !== "idle" && !buddyState.activity) {
256
+ const label = beforeType === "sleeping" ? "wakes up feeling refreshed!" :
257
+ beforeType === "eating" ? "finishes eating. Yum!" :
258
+ "is done playing!";
259
+ let msg = `${buddyState.name} ${label}`;
260
+ if (buddyState.level > beforeLevel) {
261
+ msg += ` LEVEL UP! Now level ${buddyState.level}!`;
262
+ (0, sound_1.soundLevelUp)();
263
+ }
264
+ // Check for new titles after activity
265
+ const newTitles = (0, titles_1.checkForNewTitles)(buddyState);
266
+ if (newTitles.length > 0) {
267
+ buddyState = { ...buddyState, titles: [...buddyState.titles, ...newTitles], activeTitle: newTitles[newTitles.length - 1] };
268
+ msg += ` New title earned!`;
269
+ (0, sound_1.soundTitleEarned)();
270
+ }
271
+ showFeedback(msg);
272
+ (0, save_1.saveState)(buddyState);
273
+ }
274
+ }
275
+ // Periodic title check (for age/level/inventory titles)
276
+ if (buddyState && animFrame % 50 === 0) {
277
+ const newTitles = (0, titles_1.checkForNewTitles)(buddyState);
278
+ if (newTitles.length > 0) {
279
+ buddyState = { ...buddyState, titles: [...buddyState.titles, ...newTitles], activeTitle: newTitles[newTitles.length - 1] };
280
+ showFeedback(`New title earned!`);
281
+ (0, save_1.saveState)(buddyState);
282
+ }
283
+ }
284
+ // Item finding — during idle
285
+ if (buddyState && !buddyState.activity && !activeEvent) {
286
+ const found = (0, finding_1.checkForItemFind)(buddyState);
287
+ if (found) {
288
+ const inventory = (0, items_1.addItemToInventory)(buddyState.inventory, found.item.id);
289
+ const itemCounts = { ...buddyState.actionCounts, itemsFound: buddyState.actionCounts.itemsFound + 1 };
290
+ buddyState = {
291
+ ...buddyState,
292
+ inventory,
293
+ actionCounts: itemCounts,
294
+ notifications: [...buddyState.notifications, { message: found.flavorText, timestamp: Date.now() }],
295
+ };
296
+ showFeedback(found.flavorText);
297
+ (0, sound_1.soundItemFound)();
298
+ (0, save_1.saveState)(buddyState);
299
+ }
300
+ }
301
+ // Advance pet sequence
302
+ if (petSequence) {
303
+ petTick++;
304
+ // Each line shows for 3 ticks (~2.4s)
305
+ const frameIndex = Math.floor(petTick / 3);
306
+ if (frameIndex < petSequence.length) {
307
+ dialogueMessage = petSequence[frameIndex];
308
+ dialogueSource = "Petting";
309
+ }
310
+ else {
311
+ // Sequence done — keep last line visible briefly
312
+ petSequence = null;
313
+ petTick = 0;
314
+ dialogueTimeout = setTimeout(() => {
315
+ dialogueMessage = undefined;
316
+ dialogueSource = undefined;
317
+ redraw();
318
+ }, 3000);
319
+ }
320
+ }
321
+ // Idle events — only when no activity and not petting
322
+ if (buddyState && !buddyState.activity && !petSequence && activeEvent) {
323
+ const result = (0, engine_2.advanceEvent)(activeEvent);
324
+ // Override dialogue with event's current frame
325
+ dialogueMessage = result.dialogue;
326
+ dialogueSource = activeEvent.event.name.replace(/_/g, " ");
327
+ if (dialogueTimeout) {
328
+ clearTimeout(dialogueTimeout);
329
+ dialogueTimeout = null;
330
+ }
331
+ if (result.done) {
332
+ activeEvent = null;
333
+ // Keep last event line visible a bit longer
334
+ dialogueTimeout = setTimeout(() => {
335
+ dialogueMessage = undefined;
336
+ dialogueSource = undefined;
337
+ redraw();
338
+ }, 4000);
339
+ }
340
+ }
341
+ else if (buddyState && !buddyState.activity && !activeEvent) {
342
+ const event = (0, engine_2.checkForEvent)(buddyState);
343
+ if (event) {
344
+ activeEvent = event;
345
+ }
346
+ }
347
+ // Random dialogue — every ~30 ticks (24s), only during idle with no event
348
+ dialogueTick++;
349
+ if (buddyState && !buddyState.activity && !activeEvent && !petSequence && dialogueTick >= 30) {
350
+ dialogueTick = Math.floor(Math.random() * 10); // randomize next interval
351
+ const line = (0, engine_1.pickDialogue)(buddyState);
352
+ if (line) {
353
+ dialogueMessage = line;
354
+ dialogueSource = "Idle";
355
+ if (dialogueTimeout)
356
+ clearTimeout(dialogueTimeout);
357
+ dialogueTimeout = setTimeout(() => {
358
+ dialogueMessage = undefined;
359
+ dialogueSource = undefined;
360
+ redraw();
361
+ }, 8000);
362
+ }
363
+ }
364
+ redraw();
365
+ }, 800);
366
+ }
367
+ function stopAnimation() {
368
+ if (animTimer) {
369
+ clearInterval(animTimer);
370
+ animTimer = null;
371
+ }
372
+ }
373
+ // ─── Real-time Decay ─────────────────────────────────────────
374
+ function startDecay() {
375
+ // Apply decay every 30 seconds for visible stat movement
376
+ decayTimer = setInterval(() => {
377
+ if (buddyState && buddyState.alive) {
378
+ const wasAlive = buddyState.alive;
379
+ buddyState = (0, decay_1.applyDecay)(buddyState);
380
+ if (wasAlive && !buddyState.alive) {
381
+ (0, sound_1.soundDeath)();
382
+ showFeedback(`${buddyState.name} has passed away...`);
383
+ }
384
+ (0, save_1.saveState)(buddyState);
385
+ redraw();
386
+ }
387
+ }, 30_000);
388
+ }
389
+ // ─── Input Handling ──────────────────────────────────────────
390
+ function setupInput() {
391
+ if (process.stdin.isTTY) {
392
+ process.stdin.setRawMode(true);
393
+ }
394
+ process.stdin.resume();
395
+ process.stdin.setEncoding("utf8");
396
+ // Use readline's keypress parser for proper escape sequence handling
397
+ readline.emitKeypressEvents(process.stdin);
398
+ process.stdin.on("keypress", (str, key) => {
399
+ // Normalize into a single string for our handler
400
+ if (key.ctrl && key.name === "c") {
401
+ handleKeypress("\x03");
402
+ }
403
+ else if (key.name === "up") {
404
+ handleKeypress("\x1b[A");
405
+ }
406
+ else if (key.name === "down") {
407
+ handleKeypress("\x1b[B");
408
+ }
409
+ else if (key.name === "escape") {
410
+ handleKeypress("\x1b");
411
+ }
412
+ else if (key.name === "return") {
413
+ handleKeypress("\r");
414
+ }
415
+ else if (key.name === "backspace") {
416
+ handleKeypress("\x7f");
417
+ }
418
+ else if (key.name === "tab") {
419
+ handleKeypress("\t");
420
+ }
421
+ else if (str && str.length === 1 && str >= " ") {
422
+ handleKeypress(str);
423
+ }
424
+ });
425
+ }
426
+ function handleKeypress(key) {
427
+ // Ctrl+C — always exit
428
+ if (key === "\x03") {
429
+ shutdown();
430
+ return;
431
+ }
432
+ switch (appPhase) {
433
+ case "roll-wait":
434
+ handleRollWaitInput(key);
435
+ break;
436
+ case "rolling":
437
+ // ignore input during roll animation
438
+ break;
439
+ case "reveal":
440
+ handleRevealInput(key);
441
+ break;
442
+ case "naming":
443
+ handleNamingInput(key);
444
+ break;
445
+ case "playing":
446
+ handlePlayingInput(key);
447
+ break;
448
+ }
449
+ }
450
+ // ─── Roll Flow ───────────────────────────────────────────────
451
+ function handleRollWaitInput(key) {
452
+ if (key === "\r" || key === "\n") {
453
+ startRollAnimation();
454
+ }
455
+ }
456
+ function startRollAnimation() {
457
+ appPhase = "rolling";
458
+ // Pre-roll the result
459
+ const result = (0, roll_1.rollBuddy)();
460
+ // Build a shuffled sequence of species to cycle through
461
+ const shuffled = [...species_1.SPECIES].sort(() => Math.random() - 0.5);
462
+ // Ensure the actual result is last
463
+ const filtered = shuffled.filter((s) => s.id !== result.id);
464
+ const sequence = [...filtered, result];
465
+ // Tick schedule: fast → slow → reveal
466
+ // 8 fast ticks (250ms), 3 medium (500ms), 2 slow (800ms), then reveal
467
+ const schedule = [
468
+ ...Array(8).fill(250),
469
+ 500, 500, 500,
470
+ 800, 800,
471
+ ];
472
+ let tick = 0;
473
+ function nextTick() {
474
+ if (tick >= schedule.length) {
475
+ // Show reveal
476
+ showRollReveal(result);
477
+ return;
478
+ }
479
+ const speciesIndex = tick % sequence.length;
480
+ const species = sequence[speciesIndex];
481
+ const art = species.animations.idle[0];
482
+ // Render cycling silhouette in dim gray
483
+ screen.draw((0, display_1.renderRollScreen)("rolling", species.name, art));
484
+ tick++;
485
+ setTimeout(nextTick, schedule[tick - 1]);
486
+ }
487
+ nextTick();
488
+ }
489
+ let rolledSpecies = null;
490
+ function showRollReveal(result) {
491
+ appPhase = "reveal";
492
+ rolledSpecies = result;
493
+ const color = types_1.RARITY_COLORS[result.rarity];
494
+ const label = types_1.RARITY_LABELS[result.rarity];
495
+ // Sound for epic/legendary
496
+ if (result.rarity === "epic" || result.rarity === "legendary") {
497
+ (0, sound_1.soundRareRoll)();
498
+ }
499
+ const content = (0, display_1.renderRollScreen)("reveal", result.name, result.animations.idle[0], color, label);
500
+ screen.draw(content + (0, display_1.renderNamePrompt)(result.name), ` ${screen_1.ansi.dim}>${screen_1.ansi.reset} `);
501
+ appPhase = "naming";
502
+ nameBuffer = "";
503
+ }
504
+ function handleRevealInput(key) {
505
+ if (key === "\r" || key === "\n") {
506
+ appPhase = "naming";
507
+ }
508
+ }
509
+ function handleNamingInput(key) {
510
+ if (!rolledSpecies)
511
+ return;
512
+ if (key === "\r" || key === "\n") {
513
+ // Confirm name
514
+ const name = nameBuffer.trim() || rolledSpecies.name;
515
+ buddyState = (0, roll_1.createBuddyState)(rolledSpecies, name);
516
+ (0, save_1.saveState)(buddyState);
517
+ appPhase = "playing";
518
+ showFeedback(`Welcome, ${name}! Take good care of them.`, 4000);
519
+ startAnimation();
520
+ startDecay();
521
+ redraw();
522
+ return;
523
+ }
524
+ if (key === "\x7f" || key === "\b") {
525
+ // Backspace
526
+ nameBuffer = nameBuffer.slice(0, -1);
527
+ }
528
+ else if (key.length === 1 && key >= " ") {
529
+ nameBuffer += key;
530
+ }
531
+ // Redraw name input
532
+ const color = types_1.RARITY_COLORS[rolledSpecies.rarity];
533
+ const label = types_1.RARITY_LABELS[rolledSpecies.rarity];
534
+ const content = (0, display_1.renderRollScreen)("reveal", rolledSpecies.name, rolledSpecies.animations.idle[0], color, label);
535
+ screen.draw(content + (0, display_1.renderNamePrompt)(rolledSpecies.name), ` ${screen_1.ansi.dim}>${screen_1.ansi.reset} ${nameBuffer}`);
536
+ }
537
+ // ─── Playing Input ───────────────────────────────────────────
538
+ function handlePlayingInput(key) {
539
+ if (!buddyState)
540
+ return;
541
+ // Active adventure gets highest priority
542
+ if (activeAdventure.active) {
543
+ handleAdventureInput(key);
544
+ return;
545
+ }
546
+ // Adventure menu
547
+ if (adventureMenu.active) {
548
+ handleAdventureMenuInput(key);
549
+ return;
550
+ }
551
+ // Active mini-game
552
+ if (activeMiniGame.active) {
553
+ handleMiniGameInput(key);
554
+ return;
555
+ }
556
+ // Minigame menu
557
+ if (minigameMenu.active) {
558
+ handleMinigameMenuInput(key);
559
+ return;
560
+ }
561
+ // Overlay menus (titles, notifications)
562
+ if (overlay.type) {
563
+ handleOverlayInput(key);
564
+ return;
565
+ }
566
+ // Inventory UI open
567
+ if (inventoryUI.active) {
568
+ handleInventoryInput(key);
569
+ return;
570
+ }
571
+ // Args input mode — typing arguments for a selected command
572
+ if (pendingCommand) {
573
+ if (key === "\r" || key === "\n") {
574
+ const cmd = pendingCommand;
575
+ const args = argsBuffer.trim().split(/\s+/).filter(Boolean);
576
+ pendingCommand = null;
577
+ argsBuffer = "";
578
+ executeCommand(cmd, args);
579
+ return;
580
+ }
581
+ if (key === "\x1b") {
582
+ pendingCommand = null;
583
+ argsBuffer = "";
584
+ redraw();
585
+ return;
586
+ }
587
+ if (key === "\x7f" || key === "\b") {
588
+ argsBuffer = argsBuffer.slice(0, -1);
589
+ redraw();
590
+ return;
591
+ }
592
+ if (key.length === 1 && key >= " ") {
593
+ argsBuffer += key;
594
+ redraw();
595
+ return;
596
+ }
597
+ return;
598
+ }
599
+ // Fast-forward: any key during an activity skips to completion (unless unskippable)
600
+ if (buddyState.activity && buddyState.activity.type !== "idle" && !buddyState.activity.unskippable) {
601
+ // Force completion by setting startedAt far in the past
602
+ buddyState = {
603
+ ...buddyState,
604
+ activity: {
605
+ ...buddyState.activity,
606
+ startedAt: Date.now() - buddyState.activity.durationMs - 1000,
607
+ },
608
+ };
609
+ const beforeLevel = buddyState.level;
610
+ buddyState = (0, activities_1.applyActivityProgress)(buddyState);
611
+ const label = buddyState.activity ? "" :
612
+ "Done!";
613
+ let msg = label;
614
+ if (buddyState.level > beforeLevel) {
615
+ msg += ` LEVEL UP! Now level ${buddyState.level}!`;
616
+ }
617
+ showFeedback(msg);
618
+ (0, save_1.saveState)(buddyState);
619
+ redraw();
620
+ return;
621
+ }
622
+ if (palette.active) {
623
+ handlePaletteInput(key);
624
+ return;
625
+ }
626
+ if (key === "/") {
627
+ // Open command palette
628
+ palette = (0, commandPalette_1.openPalette)(palette);
629
+ inputBuffer = "/";
630
+ redraw();
631
+ return;
632
+ }
633
+ if (key === "\x7f" || key === "\b") {
634
+ inputBuffer = inputBuffer.slice(0, -1);
635
+ redraw();
636
+ return;
637
+ }
638
+ if (key === "\r" || key === "\n") {
639
+ const trimmed = inputBuffer.trim();
640
+ if (trimmed.startsWith("/")) {
641
+ // Parse typed command with arguments
642
+ const parts = trimmed.slice(1).split(/\s+/);
643
+ const cmdName = parts[0].toLowerCase();
644
+ const args = parts.slice(1);
645
+ inputBuffer = "";
646
+ executeCommand(cmdName, args);
647
+ }
648
+ else if (trimmed) {
649
+ showFeedback(`${buddyState.name} tilts their head. Try / for commands.`);
650
+ inputBuffer = "";
651
+ redraw();
652
+ }
653
+ else {
654
+ inputBuffer = "";
655
+ redraw();
656
+ }
657
+ return;
658
+ }
659
+ // Regular character
660
+ if (key.length === 1 && key >= " ") {
661
+ inputBuffer += key;
662
+ redraw();
663
+ }
664
+ }
665
+ function handlePaletteInput(key) {
666
+ if (!buddyState)
667
+ return;
668
+ // Escape — close palette
669
+ if (key === "\x1b") {
670
+ palette = (0, commandPalette_1.createPalette)();
671
+ inputBuffer = "";
672
+ redraw();
673
+ return;
674
+ }
675
+ // Arrow up
676
+ if (key === "\x1b[A") {
677
+ palette = (0, commandPalette_1.moveSelection)(palette, -1);
678
+ redraw();
679
+ return;
680
+ }
681
+ // Arrow down
682
+ if (key === "\x1b[B") {
683
+ palette = (0, commandPalette_1.moveSelection)(palette, 1);
684
+ redraw();
685
+ return;
686
+ }
687
+ // Enter — execute selected command
688
+ if (key === "\r" || key === "\n") {
689
+ const cmdName = (0, commandPalette_1.getSelectedCommand)(palette);
690
+ palette = (0, commandPalette_1.createPalette)();
691
+ inputBuffer = "";
692
+ if (cmdName) {
693
+ executeCommand(cmdName);
694
+ }
695
+ return;
696
+ }
697
+ // Backspace
698
+ if (key === "\x7f" || key === "\b") {
699
+ if (palette.filter.length > 0) {
700
+ palette = (0, commandPalette_1.updateFilter)(palette, palette.filter.slice(0, -1));
701
+ }
702
+ else {
703
+ // Close palette if filter is empty and backspace is pressed
704
+ palette = (0, commandPalette_1.createPalette)();
705
+ inputBuffer = "";
706
+ }
707
+ redraw();
708
+ return;
709
+ }
710
+ // Tab — confirm top match
711
+ if (key === "\t") {
712
+ const cmdName = (0, commandPalette_1.getSelectedCommand)(palette);
713
+ palette = (0, commandPalette_1.createPalette)();
714
+ inputBuffer = "";
715
+ if (cmdName) {
716
+ executeCommand(cmdName);
717
+ }
718
+ return;
719
+ }
720
+ // Space — if the filter matches a command, switch to args mode
721
+ if (key === " ") {
722
+ const cmdName = (0, commandPalette_1.getSelectedCommand)(palette);
723
+ if (cmdName && palette.filteredCommands.length === 1) {
724
+ // Exact single match — enter args input for this command
725
+ palette = (0, commandPalette_1.createPalette)();
726
+ inputBuffer = "";
727
+ pendingCommand = cmdName;
728
+ argsBuffer = "";
729
+ redraw();
730
+ return;
731
+ }
732
+ // Otherwise just add space to filter
733
+ palette = (0, commandPalette_1.updateFilter)(palette, palette.filter + key);
734
+ redraw();
735
+ return;
736
+ }
737
+ // Regular typing — filter commands
738
+ if (key.length === 1 && key >= " ") {
739
+ palette = (0, commandPalette_1.updateFilter)(palette, palette.filter + key);
740
+ redraw();
741
+ }
742
+ }
743
+ function executeCommand(cmdName, args = []) {
744
+ if (!buddyState)
745
+ return;
746
+ const cmd = (0, registry_1.getCommand)(cmdName);
747
+ if (!cmd)
748
+ return;
749
+ // Special handling for rename — need to enter a name
750
+ // For now, just execute directly
751
+ const result = cmd.execute(buddyState, args);
752
+ if (result.state) {
753
+ buddyState = result.state;
754
+ (0, save_1.saveState)(buddyState);
755
+ }
756
+ if (result.quit) {
757
+ shutdown();
758
+ return;
759
+ }
760
+ if (result.message === "__OPEN_INVENTORY__") {
761
+ inventoryUI = (0, inventoryUI_1.openInventoryUI)(buddyState.inventory);
762
+ redraw();
763
+ return;
764
+ }
765
+ if (result.message === "__OPEN_TITLES__") {
766
+ overlay = (0, overlayUI_1.openOverlay)("titles");
767
+ redraw();
768
+ return;
769
+ }
770
+ if (result.message === "__OPEN_NOTIFICATIONS__") {
771
+ overlay = (0, overlayUI_1.openOverlay)("notifications");
772
+ redraw();
773
+ return;
774
+ }
775
+ if (result.message === "__OPEN_MINIGAMES__") {
776
+ minigameMenu = (0, minigameUI_1.openMinigameMenu)((0, registry_2.getAllGames)());
777
+ redraw();
778
+ return;
779
+ }
780
+ if (result.message === "__OPEN_GEAR__") {
781
+ overlay = (0, overlayUI_1.openOverlay)("gear");
782
+ redraw();
783
+ return;
784
+ }
785
+ if (result.message === "__OPEN_COMBATINFO__") {
786
+ overlay = (0, overlayUI_1.openOverlay)("combatinfo");
787
+ redraw();
788
+ return;
789
+ }
790
+ if (result.message === "__OPEN_SHOP__") {
791
+ overlay = (0, overlayUI_1.openOverlay)("shop");
792
+ redraw();
793
+ return;
794
+ }
795
+ if (result.message === "__OPEN_ADVENTURE__") {
796
+ adventureMenu = (0, adventureUI_1.openAdventureMenu)((0, mainStory_1.getAllQuestChains)(buddyState?.questProgress.completedChapters ?? []));
797
+ redraw();
798
+ return;
799
+ }
800
+ if (result.message === "__OPEN_SETTINGS__") {
801
+ overlay = (0, overlayUI_1.openOverlay)("settings");
802
+ redraw();
803
+ return;
804
+ }
805
+ if (result.message === "__PET__") {
806
+ startPetSequence();
807
+ redraw();
808
+ return;
809
+ }
810
+ if (result.message) {
811
+ showFeedback(result.message);
812
+ }
813
+ redraw();
814
+ }
815
+ function pickRandom(arr) {
816
+ return arr[Math.floor(Math.random() * arr.length)];
817
+ }
818
+ function startPetSequence() {
819
+ if (!buddyState)
820
+ return;
821
+ // 50% chance of a multi-step sequence, 50% chance of a quick single + species line
822
+ if (Math.random() < 0.5) {
823
+ petSequence = [...pickRandom(lines_1.PET_SEQUENCES)];
824
+ }
825
+ else {
826
+ const single = pickRandom(lines_1.PET_SINGLES);
827
+ const speciesLines = lines_1.SPECIES_PET[buddyState.speciesId];
828
+ const speciesLine = speciesLines ? pickRandom(speciesLines) : null;
829
+ petSequence = speciesLine ? [single, speciesLine] : [single];
830
+ }
831
+ petTick = 0;
832
+ // Show first line immediately
833
+ dialogueMessage = petSequence[0];
834
+ dialogueSource = "Petting";
835
+ if (dialogueTimeout)
836
+ clearTimeout(dialogueTimeout);
837
+ dialogueTimeout = null;
838
+ }
839
+ // ─── Adventure Handlers ──────────────────────────────────────
840
+ function handleAdventureMenuInput(key) {
841
+ if (!buddyState)
842
+ return;
843
+ if (key === "\x1b") {
844
+ adventureMenu = (0, adventureUI_1.createAdventureMenu)();
845
+ redraw();
846
+ return;
847
+ }
848
+ if (key === "\t") {
849
+ adventureMenu = (0, adventureUI_1.switchTab)(adventureMenu);
850
+ redraw();
851
+ return;
852
+ }
853
+ if (key === "\x1b[A") {
854
+ adventureMenu = (0, adventureUI_1.moveAdventureSelection)(adventureMenu, -1);
855
+ redraw();
856
+ return;
857
+ }
858
+ if (key === "\x1b[B") {
859
+ adventureMenu = (0, adventureUI_1.moveAdventureSelection)(adventureMenu, 1);
860
+ redraw();
861
+ return;
862
+ }
863
+ if (key === "\x1b[C" || key === "\x1b[D") {
864
+ if (adventureMenu.tab === "story") {
865
+ adventureMenu = (0, adventureUI_1.changeQuestChain)(adventureMenu, key === "\x1b[C" ? 1 : -1);
866
+ redraw();
867
+ }
868
+ else if (adventureMenu.tab === "expedition") {
869
+ adventureMenu = (0, adventureUI_1.changeExpeditionBiome)(adventureMenu, key === "\x1b[C" ? 1 : -1);
870
+ redraw();
871
+ }
872
+ return;
873
+ }
874
+ if (key === "\r" || key === "\n") {
875
+ let adv;
876
+ if (adventureMenu.tab === "story") {
877
+ const chain = adventureMenu.questChains[adventureMenu.selectedChainIndex];
878
+ if (!chain)
879
+ return;
880
+ const chapter = chain.chapters[adventureMenu.selectedIndex];
881
+ if (!chapter)
882
+ return;
883
+ const { canStartChapter } = require("./story/types");
884
+ const blockReason = canStartChapter(chapter, buddyState.questProgress, buddyState.level);
885
+ if (blockReason) {
886
+ showFeedback(blockReason);
887
+ redraw();
888
+ return;
889
+ }
890
+ if (!chapter.acts || chapter.acts.length === 0) {
891
+ showFeedback("This chapter isn't available yet — coming soon!");
892
+ redraw();
893
+ return;
894
+ }
895
+ // Check for saved progress to resume
896
+ const saved = buddyState.questProgress.savedAdventure;
897
+ const resumeActIndex = buddyState.questProgress.activeActIndex ?? 0;
898
+ if (saved) {
899
+ const act = chapter.acts[resumeActIndex];
900
+ if (act) {
901
+ try {
902
+ adv = act.buildAdventure();
903
+ }
904
+ catch {
905
+ showFeedback("Coming soon!");
906
+ redraw();
907
+ return;
908
+ }
909
+ currentAdventureDef = adv;
910
+ activeAdventure = (0, progress_2.restoreAdventureState)(saved);
911
+ buddyState = (0, progress_2.clearSavedAdventure)(buddyState);
912
+ buddyState = (0, progress_2.setActiveQuest)(buddyState, chain.id, chapter.id);
913
+ adventureMenu = (0, adventureUI_1.createAdventureMenu)();
914
+ const resumeRoom = (0, types_2.getRoom)(adv, activeAdventure.currentRoomId);
915
+ if (resumeRoom && (resumeRoom.type === "combat" || resumeRoom.type === "boss") && resumeRoom.enemyId) {
916
+ const enemy = (0, enemies_1.getEnemy)(resumeRoom.enemyId);
917
+ if (enemy)
918
+ activeAdventure = { ...activeAdventure, phase: "combat", combat: (0, combat_1.startCombat)(buddyState, enemy, activeAdventure.morale) };
919
+ }
920
+ (0, save_1.saveState)(buddyState);
921
+ showFeedback("Resuming quest where you left off!");
922
+ redraw();
923
+ return;
924
+ }
925
+ }
926
+ // Start first act
927
+ const firstAct = chapter.acts[0];
928
+ try {
929
+ adv = firstAct.buildAdventure();
930
+ }
931
+ catch {
932
+ showFeedback("This chapter isn't available yet — coming soon!");
933
+ redraw();
934
+ return;
935
+ }
936
+ isEndlessMode = false;
937
+ buddyState = (0, progress_2.setActiveQuest)(buddyState, chain.id, chapter.id);
938
+ buddyState = { ...buddyState, questProgress: { ...buddyState.questProgress, activeActIndex: 0 } };
939
+ }
940
+ else if (adventureMenu.tab === "expedition") {
941
+ const biomeId = (0, adventureUI_1.getSelectedBiomeId)(adventureMenu);
942
+ adv = (0, generator_1.generateExpedition)({
943
+ difficulty: adventureMenu.expeditionDifficulty,
944
+ biomeId,
945
+ buddyState,
946
+ });
947
+ isEndlessMode = false;
948
+ }
949
+ else {
950
+ // Endless Depths
951
+ adv = (0, endless_1.createEndlessAdventure)(buddyState);
952
+ isEndlessMode = true;
953
+ endlessDepth = 0;
954
+ }
955
+ const err = (0, engine_3.canStartAdventure)(buddyState, adv);
956
+ if (err) {
957
+ showFeedback(err);
958
+ redraw();
959
+ return;
960
+ }
961
+ const result = (0, engine_3.beginAdventure)(buddyState, adv);
962
+ buddyState = result.state;
963
+ activeAdventure = result.adventure;
964
+ // Store the generated adventure so we can reference it
965
+ currentAdventureDef = adv;
966
+ adventureMenu = (0, adventureUI_1.createAdventureMenu)();
967
+ // If start room is combat, start it
968
+ const startRoom = (0, types_2.getRoom)(adv, adv.startRoomId);
969
+ if (startRoom && (startRoom.type === "combat" || startRoom.type === "boss") && startRoom.enemyId) {
970
+ const enemy = (0, enemies_1.getEnemy)(startRoom.enemyId);
971
+ if (enemy) {
972
+ activeAdventure = { ...activeAdventure, combat: (0, combat_1.startCombat)(buddyState, enemy, activeAdventure.morale) };
973
+ }
974
+ }
975
+ (0, save_1.saveState)(buddyState);
976
+ redraw();
977
+ }
978
+ }
979
+ function handleAdventureInput(key) {
980
+ if (!buddyState)
981
+ return;
982
+ if (activeAdventure.phase === "combat" && activeAdventure.combat) {
983
+ handleCombatInput(key);
984
+ return;
985
+ }
986
+ if (activeAdventure.phase === "result") {
987
+ if (key === "\r" || key === "\n") {
988
+ finishAdventure();
989
+ }
990
+ return;
991
+ }
992
+ // Room input
993
+ if (key === "\x1b") {
994
+ // Retreat from adventure — save progress if it's a quest
995
+ if (buddyState.questProgress.activeChapter) {
996
+ buddyState = (0, progress_2.saveQuestAdventure)(buddyState, activeAdventure);
997
+ (0, save_1.saveState)(buddyState);
998
+ showFeedback("Quest progress saved. Resume anytime!");
999
+ }
1000
+ else {
1001
+ showFeedback("Retreated from adventure.");
1002
+ }
1003
+ activeAdventure = (0, types_2.createInactiveAdventure)();
1004
+ currentAdventureDef = null;
1005
+ redraw();
1006
+ return;
1007
+ }
1008
+ const adv = currentAdventureDef;
1009
+ if (!adv)
1010
+ return;
1011
+ const room = (0, types_2.getRoom)(adv, activeAdventure.currentRoomId);
1012
+ if (!room)
1013
+ return;
1014
+ // Text paging — advance page on Enter/Space if text isn't fully shown
1015
+ if (!activeAdventure.textFullyShown) {
1016
+ if (key === "\r" || key === "\n" || key === " ") {
1017
+ const LINES_PER_PAGE = 3;
1018
+ const totalPages = Math.ceil(room.text.length / LINES_PER_PAGE);
1019
+ const nextPage = activeAdventure.textPage + 1;
1020
+ if (nextPage >= totalPages) {
1021
+ // Beyond last page — already fully shown, FALL THROUGH to choice handling
1022
+ activeAdventure = { ...activeAdventure, textFullyShown: true };
1023
+ redraw();
1024
+ // DON'T return — let the rest of the handler process this Enter
1025
+ }
1026
+ else {
1027
+ // Advancing to next page — mark fully shown if this IS the last page
1028
+ const isLastPage = nextPage >= totalPages - 1;
1029
+ activeAdventure = { ...activeAdventure, textPage: nextPage, textFullyShown: isLastPage };
1030
+ redraw();
1031
+ // Only fall through on last page if room has choices (so 1/2 keys work immediately)
1032
+ // Otherwise always return — let the user SEE the last page before advancing
1033
+ const hasChoices = (room.type === "narrative" && room.choices) || (room.type === "event" && room.eventChoices);
1034
+ if (!isLastPage || !hasChoices)
1035
+ return;
1036
+ // Last page with choices — fall through so choices are immediately selectable
1037
+ }
1038
+ }
1039
+ else {
1040
+ return; // Block non-Enter input during paging
1041
+ }
1042
+ }
1043
+ if (room.type === "narrative" && room.choices) {
1044
+ const choice = parseInt(key) - 1;
1045
+ if (choice >= 0 && choice < room.choices.length) {
1046
+ activeAdventure = (0, engine_3.makeChoice)(activeAdventure, adv, choice);
1047
+ // Check if new room is combat
1048
+ const newRoom = (0, types_2.getRoom)(adv, activeAdventure.currentRoomId);
1049
+ if (newRoom && (newRoom.type === "combat" || newRoom.type === "boss") && newRoom.enemyId) {
1050
+ const enemy = (0, enemies_1.getEnemy)(newRoom.enemyId);
1051
+ if (enemy) {
1052
+ activeAdventure = { ...activeAdventure, combat: (0, combat_1.startCombat)(buddyState, enemy, activeAdventure.morale) };
1053
+ }
1054
+ }
1055
+ redraw();
1056
+ }
1057
+ return;
1058
+ }
1059
+ if (room.type === "rest") {
1060
+ if (key === "1" && room.healAmount) {
1061
+ buddyState = { ...buddyState, stats: { ...buddyState.stats, health: Math.min(100, buddyState.stats.health + room.healAmount) } };
1062
+ activeAdventure = (0, engine_3.adjustMorale)(activeAdventure, 10);
1063
+ advanceEndlessOrNormal(adv);
1064
+ redraw();
1065
+ }
1066
+ else if (key === "2") {
1067
+ advanceEndlessOrNormal(adv);
1068
+ redraw();
1069
+ }
1070
+ return;
1071
+ }
1072
+ if (room.type === "event" && room.eventChoices) {
1073
+ // Check if event was already resolved (buddy reaction is an outcome)
1074
+ const alreadyResolved = room.eventChoices.some((c) => c.outcome === activeAdventure.buddyReaction);
1075
+ if (alreadyResolved) {
1076
+ // Any key advances to next room
1077
+ advanceEndlessOrNormal(adv);
1078
+ redraw();
1079
+ return;
1080
+ }
1081
+ const choice = parseInt(key) - 1;
1082
+ if (choice >= 0 && choice < room.eventChoices.length) {
1083
+ const eventChoice = room.eventChoices[choice];
1084
+ activeAdventure = { ...activeAdventure, buddyReaction: eventChoice.outcome };
1085
+ // Apply effect
1086
+ if (eventChoice.effect?.itemGain) {
1087
+ buddyState = { ...buddyState, inventory: (0, items_1.addItemToInventory)(buddyState.inventory, eventChoice.effect.itemGain) };
1088
+ activeAdventure = { ...activeAdventure, lootCollected: [...activeAdventure.lootCollected, eventChoice.effect.itemGain] };
1089
+ }
1090
+ redraw();
1091
+ return;
1092
+ }
1093
+ return;
1094
+ }
1095
+ if (room.type === "treasure") {
1096
+ if (key === "\r" || key === "\n") {
1097
+ const lootId = (0, engine_3.rollTreasureLoot)(room);
1098
+ if (lootId) {
1099
+ const prevLen = buddyState.inventory.length;
1100
+ buddyState = { ...buddyState, inventory: (0, items_1.addItemToInventory)(buddyState.inventory, lootId) };
1101
+ const wasNew = buddyState.inventory.length > prevLen;
1102
+ activeAdventure = (0, engine_3.adjustMorale)({ ...activeAdventure, lootCollected: [...activeAdventure.lootCollected, lootId] }, 5);
1103
+ showFeedback(wasNew ? `Found: ${lootId.replace(/_/g, " ")}!` : `Found: ${lootId.replace(/_/g, " ")} (already owned)`);
1104
+ }
1105
+ advanceEndlessOrNormal(adv);
1106
+ redraw();
1107
+ }
1108
+ return;
1109
+ }
1110
+ // Default: Enter to advance
1111
+ if (key === "\r" || key === "\n") {
1112
+ advanceEndlessOrNormal(adv);
1113
+ redraw();
1114
+ }
1115
+ }
1116
+ function advanceEndlessOrNormal(adv) {
1117
+ if (!buddyState)
1118
+ return;
1119
+ if (isEndlessMode && currentAdventureDef) {
1120
+ endlessDepth++;
1121
+ // XP per room
1122
+ const roomXp = 5 + endlessDepth * 3;
1123
+ activeAdventure = { ...activeAdventure, xpEarned: activeAdventure.xpEarned + roomXp };
1124
+ // Generate next room
1125
+ const result = (0, endless_1.addEndlessRoom)(currentAdventureDef, endlessDepth, buddyState);
1126
+ currentAdventureDef = result.adventure;
1127
+ activeAdventure = {
1128
+ ...activeAdventure,
1129
+ currentRoomId: result.roomId,
1130
+ roomsVisited: [...activeAdventure.roomsVisited, result.roomId],
1131
+ phase: "room",
1132
+ combat: undefined,
1133
+ textPage: 0,
1134
+ textFullyShown: true, // endless rooms are short, show immediately
1135
+ buddyReaction: `Depth ${endlessDepth + 1}... deeper we go!`,
1136
+ };
1137
+ // Check if new room is combat
1138
+ const newRoom = (0, types_2.getRoom)(currentAdventureDef, result.roomId);
1139
+ if (newRoom && (newRoom.type === "combat" || newRoom.type === "boss") && newRoom.enemyId) {
1140
+ const enemy = (0, enemies_1.getEnemy)(newRoom.enemyId);
1141
+ if (enemy) {
1142
+ // Scale enemy stats for endless depth
1143
+ const scaled = (0, endless_1.scaleEndlessEnemy)(enemy.baseHp, enemy.baseAtk, enemy.baseDef, endlessDepth);
1144
+ const scaledEnemy = { ...enemy, baseHp: scaled.hp, baseAtk: scaled.atk, baseDef: scaled.def };
1145
+ activeAdventure = { ...activeAdventure, phase: "combat", combat: (0, combat_1.startCombat)(buddyState, scaledEnemy, activeAdventure.morale) };
1146
+ }
1147
+ }
1148
+ }
1149
+ else {
1150
+ activeAdventure = (0, engine_3.advanceToNextRoom)(activeAdventure, adv);
1151
+ // Scene acts skip the result screen — auto-finish when the last room ends
1152
+ if (activeAdventure.phase === "result" && adv.isScene) {
1153
+ finishAdventure();
1154
+ return;
1155
+ }
1156
+ checkAndStartCombat(adv);
1157
+ }
1158
+ }
1159
+ function checkAndStartCombat(adv) {
1160
+ if (!buddyState)
1161
+ return;
1162
+ const room = (0, types_2.getRoom)(adv, activeAdventure.currentRoomId);
1163
+ if (activeAdventure.phase === "combat" && (room?.type === "combat" || room?.type === "boss") && room.enemyId) {
1164
+ const enemy = (0, enemies_1.getEnemy)(room.enemyId);
1165
+ if (enemy) {
1166
+ activeAdventure = { ...activeAdventure, combat: (0, combat_1.startCombat)(buddyState, enemy, activeAdventure.morale) };
1167
+ }
1168
+ }
1169
+ }
1170
+ function handleCombatInput(key) {
1171
+ if (!buddyState || !activeAdventure.combat)
1172
+ return;
1173
+ const combat = activeAdventure.combat;
1174
+ // Victory/Defeat/Fled — Enter to continue
1175
+ if (combat.phase === "victory" || combat.phase === "defeat" || combat.phase === "fled") {
1176
+ if (key === "\r" || key === "\n") {
1177
+ const adv = currentAdventureDef;
1178
+ if (!adv)
1179
+ return;
1180
+ if (combat.phase === "victory") {
1181
+ (0, sound_1.soundBattleWin)();
1182
+ // Track combos from this battle
1183
+ if (combat.comboTriggeredThisBattle > 0) {
1184
+ const cc = { ...buddyState.actionCounts, combosTriggered: buddyState.actionCounts.combosTriggered + combat.comboTriggeredThisBattle };
1185
+ buddyState = { ...buddyState, actionCounts: cc };
1186
+ }
1187
+ activeAdventure = (0, engine_3.adjustMorale)({
1188
+ ...activeAdventure,
1189
+ combat: undefined,
1190
+ battlesWon: activeAdventure.battlesWon + 1,
1191
+ xpEarned: activeAdventure.xpEarned + combat.enemy.xpReward,
1192
+ damageTaken: activeAdventure.damageTaken + combat.damageTakenThisBattle,
1193
+ }, 10);
1194
+ // Check for loot drop
1195
+ if (Math.random() < combat.enemy.dropChance && combat.enemy.dropPool.length > 0) {
1196
+ const dropId = combat.enemy.dropPool[Math.floor(Math.random() * combat.enemy.dropPool.length)];
1197
+ const prevLen = buddyState.inventory.length;
1198
+ buddyState = { ...buddyState, inventory: (0, items_1.addItemToInventory)(buddyState.inventory, dropId) };
1199
+ const wasNew = buddyState.inventory.length > prevLen;
1200
+ activeAdventure = { ...activeAdventure, lootCollected: [...activeAdventure.lootCollected, dropId] };
1201
+ if (wasNew)
1202
+ showFeedback(`Enemy dropped: ${dropId.replace(/_/g, " ")}!`);
1203
+ else
1204
+ showFeedback(`Enemy dropped: ${dropId.replace(/_/g, " ")} (already owned)`);
1205
+ }
1206
+ // Advance to next room (or next depth in endless)
1207
+ advanceEndlessOrNormal(adv);
1208
+ }
1209
+ else if (combat.phase === "defeat") {
1210
+ // Adventure over — buddy is hurt and needs mandatory recovery
1211
+ activeAdventure = { ...activeAdventure, battlesLost: activeAdventure.battlesLost + 1, phase: "result", combat: undefined, buddyReaction: "Ow... I need to rest..." };
1212
+ // Penalty: lose happiness + hunger, and forced into 5-minute unskippable recovery
1213
+ const newStats = {
1214
+ ...buddyState.stats,
1215
+ happiness: Math.max(0, buddyState.stats.happiness - 20),
1216
+ hunger: Math.max(0, buddyState.stats.hunger - 15),
1217
+ };
1218
+ // Start a 5-minute recovery activity (not skippable — we'll block fast-forward for it)
1219
+ const recoveryActivity = {
1220
+ type: "sleeping",
1221
+ startedAt: Date.now(),
1222
+ durationMs: 300_000, // 5 minutes
1223
+ statBaseline: { energy: newStats.energy },
1224
+ statTarget: { energy: Math.min(100, newStats.energy + 15) },
1225
+ unskippable: true,
1226
+ };
1227
+ buddyState = { ...buddyState, stats: newStats, activity: recoveryActivity };
1228
+ }
1229
+ else {
1230
+ // Fled — advance past this room
1231
+ activeAdventure = { ...activeAdventure, battlesFled: activeAdventure.battlesFled + 1, combat: undefined };
1232
+ advanceEndlessOrNormal(adv);
1233
+ }
1234
+ (0, save_1.saveState)(buddyState);
1235
+ redraw();
1236
+ }
1237
+ return;
1238
+ }
1239
+ // Player turn actions
1240
+ if (combat.phase !== "player_turn")
1241
+ return;
1242
+ if (showingSkillMenu) {
1243
+ // Skill selection
1244
+ if (key === "0") {
1245
+ showingSkillMenu = false;
1246
+ redraw();
1247
+ return;
1248
+ }
1249
+ const skills = (0, skills_1.getAvailableSkills)(buddyState.speciesId, buddyState.level);
1250
+ const idx = parseInt(key) - 1;
1251
+ if (idx >= 0 && idx < skills.length) {
1252
+ activeAdventure = { ...activeAdventure, combat: (0, combat_1.playerUseSkill)(combat, skills[idx]) };
1253
+ showingSkillMenu = false;
1254
+ }
1255
+ redraw();
1256
+ return;
1257
+ }
1258
+ switch (key) {
1259
+ case "1": // Attack
1260
+ activeAdventure = { ...activeAdventure, combat: (0, combat_1.playerAttack)(combat) };
1261
+ break;
1262
+ case "2": // Skill menu
1263
+ showingSkillMenu = true;
1264
+ break;
1265
+ case "3": // Item (use potion/food from inventory mid-combat)
1266
+ // Simple: use first medicine in inventory
1267
+ const medicine = buddyState.inventory.find((s) => {
1268
+ const item = (0, items_1.getItem)(s.itemId);
1269
+ return item?.type === "medicine" && s.count > 0;
1270
+ });
1271
+ if (medicine) {
1272
+ const item = (0, items_1.getItem)(medicine.itemId);
1273
+ const healAmount = item.statEffect.health ?? 0;
1274
+ const newHp = Math.min(combat.playerStats.maxHp, combat.playerStats.hp + healAmount);
1275
+ activeAdventure = {
1276
+ ...activeAdventure,
1277
+ combat: { ...combat, playerStats: { ...combat.playerStats, hp: newHp }, phase: "player_animate", log: [...combat.log, `Used ${item.name}! +${healAmount} HP`], ticksInPhase: 0 },
1278
+ };
1279
+ buddyState = { ...buddyState, inventory: buddyState.inventory.map((s) => s.itemId === medicine.itemId ? { ...s, count: s.count - 1 } : s).filter((s) => s.count > 0) };
1280
+ }
1281
+ else {
1282
+ activeAdventure = { ...activeAdventure, combat: { ...combat, log: [...combat.log, "No medicine in inventory!"] } };
1283
+ }
1284
+ break;
1285
+ case "4": // Defend
1286
+ activeAdventure = { ...activeAdventure, combat: (0, combat_1.playerDefend)(combat) };
1287
+ break;
1288
+ case "5": // Flee
1289
+ case "\x1b":
1290
+ activeAdventure = { ...activeAdventure, combat: (0, combat_1.playerFlee)(combat) };
1291
+ break;
1292
+ default:
1293
+ return;
1294
+ }
1295
+ redraw();
1296
+ }
1297
+ function finishAdventure() {
1298
+ if (!buddyState)
1299
+ return;
1300
+ (0, sound_1.soundAdventureComplete)();
1301
+ const adv = currentAdventureDef;
1302
+ // Award XP
1303
+ buddyState = { ...buddyState, xp: buddyState.xp + activeAdventure.xpEarned };
1304
+ const { state: leveled } = (0, leveling_1.checkLevelUp)(buddyState);
1305
+ buddyState = leveled;
1306
+ // Update adventure stats
1307
+ const stats = { ...buddyState.adventureStats };
1308
+ stats.adventuresCompleted++;
1309
+ stats.battlesWon += activeAdventure.battlesWon;
1310
+ stats.battlesLost += activeAdventure.battlesLost;
1311
+ stats.battlesFled += activeAdventure.battlesFled;
1312
+ if (activeAdventure.damageTaken === 0 && activeAdventure.battlesWon > 0) {
1313
+ stats.perfectAdventures++;
1314
+ }
1315
+ if (adv?.difficulty === "boss" && activeAdventure.battlesLost === 0) {
1316
+ stats.bossesDefeated++;
1317
+ }
1318
+ buddyState = { ...buddyState, adventureStats: stats };
1319
+ // Complete quest chapter if this was a quest adventure
1320
+ if (buddyState.questProgress.activeChapter) {
1321
+ const chainId = buddyState.questProgress.activeChain;
1322
+ const chapterId = buddyState.questProgress.activeChapter;
1323
+ if (chainId) {
1324
+ const chain = (0, mainStory_1.getQuestChain)(chainId);
1325
+ const chapter = chain?.chapters.find((c) => c.id === chapterId);
1326
+ if (chapter) {
1327
+ const actIndex = buddyState.questProgress.activeActIndex ?? 0;
1328
+ const nextActIndex = actIndex + 1;
1329
+ if (nextActIndex < chapter.acts.length) {
1330
+ // More acts — load the next one
1331
+ buddyState = { ...buddyState, questProgress: { ...buddyState.questProgress, activeActIndex: nextActIndex } };
1332
+ try {
1333
+ const nextAct = chapter.acts[nextActIndex];
1334
+ const nextAdv = nextAct.buildAdventure();
1335
+ currentAdventureDef = nextAdv;
1336
+ const startResult = (0, engine_3.beginAdventure)(buddyState, nextAdv);
1337
+ buddyState = startResult.state;
1338
+ activeAdventure = startResult.adventure;
1339
+ // Check if first room is combat
1340
+ const startRoom = (0, types_2.getRoom)(nextAdv, nextAdv.startRoomId);
1341
+ if (startRoom && (startRoom.type === "combat" || startRoom.type === "boss") && startRoom.enemyId) {
1342
+ const enemy = (0, enemies_1.getEnemy)(startRoom.enemyId);
1343
+ if (enemy)
1344
+ activeAdventure = { ...activeAdventure, phase: "combat", combat: (0, combat_1.startCombat)(buddyState, enemy, activeAdventure.morale) };
1345
+ }
1346
+ (0, save_1.saveState)(buddyState);
1347
+ showFeedback(`${nextAct.name}`);
1348
+ redraw();
1349
+ return; // Don't fall through to normal finish
1350
+ }
1351
+ catch {
1352
+ // Act not built yet — complete chapter
1353
+ }
1354
+ }
1355
+ // Final act or error — complete the chapter
1356
+ buddyState = (0, progress_2.completeChapter)(buddyState, chapter);
1357
+ buddyState = (0, progress_2.clearSavedAdventure)(buddyState);
1358
+ buddyState = { ...buddyState, questProgress: { ...buddyState.questProgress, activeActIndex: undefined } };
1359
+ showFeedback(`Chapter complete! +${chapter.rewards.xp} XP, +${chapter.rewards.gold} gold`);
1360
+ }
1361
+ }
1362
+ }
1363
+ (0, save_1.saveState)(buddyState);
1364
+ const depthMsg = isEndlessMode ? ` Reached Depth ${endlessDepth + 1}!` : "";
1365
+ if (!buddyState.questProgress.activeChapter) {
1366
+ showFeedback(`Adventure complete! +${activeAdventure.xpEarned} XP${depthMsg}`);
1367
+ }
1368
+ activeAdventure = (0, types_2.createInactiveAdventure)();
1369
+ currentAdventureDef = null;
1370
+ isEndlessMode = false;
1371
+ endlessDepth = 0;
1372
+ redraw();
1373
+ }
1374
+ function handleMiniGameInput(key) {
1375
+ if (!buddyState)
1376
+ return;
1377
+ // Escape — quit game
1378
+ if (key === "\x1b") {
1379
+ activeMiniGame = (0, types_3.createInactiveGame)();
1380
+ redraw();
1381
+ return;
1382
+ }
1383
+ // Delegate to game handler
1384
+ const handler = (0, registry_2.getGameHandler)(activeMiniGame.gameId);
1385
+ if (handler) {
1386
+ activeMiniGame = handler.handleInput(key, activeMiniGame);
1387
+ // If game just entered result phase, let it show for a few ticks
1388
+ if (activeMiniGame.phase === "result" && activeMiniGame.ticksInPhase === 0) {
1389
+ activeMiniGame = { ...activeMiniGame, ticksInPhase: 1 };
1390
+ }
1391
+ }
1392
+ redraw();
1393
+ }
1394
+ function handleMinigameMenuInput(key) {
1395
+ if (key === "\x1b") {
1396
+ minigameMenu = (0, minigameUI_1.createMinigameMenu)();
1397
+ redraw();
1398
+ return;
1399
+ }
1400
+ if (key === "\x1b[A") {
1401
+ minigameMenu = (0, minigameUI_1.moveMinigameSelection)(minigameMenu, -1);
1402
+ redraw();
1403
+ return;
1404
+ }
1405
+ if (key === "\x1b[B") {
1406
+ minigameMenu = (0, minigameUI_1.moveMinigameSelection)(minigameMenu, 1);
1407
+ redraw();
1408
+ return;
1409
+ }
1410
+ if (key === "\r" || key === "\n") {
1411
+ const game = minigameMenu.games[minigameMenu.selectedIndex];
1412
+ if (game) {
1413
+ const handler = (0, registry_2.getGameHandler)(game.id);
1414
+ if (handler) {
1415
+ minigameMenu = (0, minigameUI_1.createMinigameMenu)();
1416
+ activeMiniGame = handler.start();
1417
+ redraw();
1418
+ }
1419
+ }
1420
+ return;
1421
+ }
1422
+ }
1423
+ function finishMiniGame() {
1424
+ if (!buddyState)
1425
+ return;
1426
+ const xp = (0, types_3.getXpReward)(activeMiniGame.score, activeMiniGame.maxRounds, 20);
1427
+ const gameCounts = { ...buddyState.actionCounts, minigamesWon: buddyState.actionCounts.minigamesWon + 1 };
1428
+ buddyState = { ...buddyState, xp: buddyState.xp + xp, actionCounts: gameCounts };
1429
+ const { state: leveled } = (0, leveling_1.checkLevelUp)(buddyState);
1430
+ buddyState = leveled;
1431
+ (0, save_1.saveState)(buddyState);
1432
+ showFeedback(`Game over! +${xp} XP`);
1433
+ (0, sound_1.soundGameEnd)();
1434
+ activeMiniGame = (0, types_3.createInactiveGame)();
1435
+ }
1436
+ function handleOverlayInput(key) {
1437
+ if (!buddyState)
1438
+ return;
1439
+ // Escape — close
1440
+ if (key === "\x1b") {
1441
+ overlay = (0, overlayUI_1.createOverlay)();
1442
+ redraw();
1443
+ return;
1444
+ }
1445
+ // Arrow up/down — scroll
1446
+ if (key === "\x1b[A") {
1447
+ const maxItems = overlay.type === "titles" ? titles_2.TITLE_RULES.length :
1448
+ overlay.type === "settings" ? (0, overlayUI_1.getSettingEntries)().length :
1449
+ overlay.type === "notifications" ? buddyState.notifications.length :
1450
+ overlay.type === "shop" ? shop_1.SHOP_ITEMS.length :
1451
+ 0;
1452
+ overlay = (0, overlayUI_1.scrollOverlay)(overlay, -1, maxItems);
1453
+ redraw();
1454
+ return;
1455
+ }
1456
+ if (key === "\x1b[B") {
1457
+ const maxItems = overlay.type === "titles" ? titles_2.TITLE_RULES.length :
1458
+ overlay.type === "settings" ? (0, overlayUI_1.getSettingEntries)().length :
1459
+ overlay.type === "notifications" ? buddyState.notifications.length :
1460
+ overlay.type === "shop" ? shop_1.SHOP_ITEMS.length :
1461
+ 0;
1462
+ overlay = (0, overlayUI_1.scrollOverlay)(overlay, 1, maxItems);
1463
+ redraw();
1464
+ return;
1465
+ }
1466
+ // Enter — action depending on overlay type
1467
+ if (key === "\r" || key === "\n") {
1468
+ if (overlay.type === "titles") {
1469
+ const rule = titles_2.TITLE_RULES[overlay.scrollIndex];
1470
+ if (rule && buddyState.titles.includes(rule.id)) {
1471
+ buddyState = { ...buddyState, activeTitle: rule.id };
1472
+ (0, save_1.saveState)(buddyState);
1473
+ showFeedback(`Title equipped: ${rule.label}`);
1474
+ }
1475
+ }
1476
+ else if (overlay.type === "notifications") {
1477
+ buddyState = { ...buddyState, notifications: [] };
1478
+ (0, save_1.saveState)(buddyState);
1479
+ showFeedback("Notifications cleared.");
1480
+ }
1481
+ else if (overlay.type === "settings") {
1482
+ // Toggle the selected setting
1483
+ const entries = (0, overlayUI_1.getSettingEntries)();
1484
+ const entry = entries[overlay.scrollIndex];
1485
+ if (entry) {
1486
+ const current = (0, settings_1.getSettings)();
1487
+ const updated = { ...current, [entry.key]: !current[entry.key] };
1488
+ (0, settings_1.saveSettings)(updated);
1489
+ showFeedback(`${entry.name}: ${updated[entry.key] ? "ON" : "OFF"}`);
1490
+ }
1491
+ // Don't close — stay in settings to toggle more
1492
+ redraw();
1493
+ return;
1494
+ }
1495
+ else if (overlay.type === "shop") {
1496
+ // Buy selected item
1497
+ const shopItem = shop_1.SHOP_ITEMS[overlay.scrollIndex];
1498
+ if (shopItem) {
1499
+ const result = (0, progress_1.spendGold)(buddyState, shopItem.price);
1500
+ if (result) {
1501
+ buddyState = { ...result, inventory: (0, items_1.addItemToInventory)(result.inventory, shopItem.itemId) };
1502
+ (0, save_1.saveState)(buddyState);
1503
+ const item = (0, items_1.getItem)(shopItem.itemId);
1504
+ showFeedback(`Bought ${item?.name ?? shopItem.itemId}! (-${shopItem.price}g)`);
1505
+ }
1506
+ else {
1507
+ showFeedback("Not enough gold!");
1508
+ }
1509
+ }
1510
+ // Stay in shop
1511
+ redraw();
1512
+ return;
1513
+ }
1514
+ overlay = (0, overlayUI_1.createOverlay)();
1515
+ redraw();
1516
+ return;
1517
+ }
1518
+ }
1519
+ function handleInventoryInput(key) {
1520
+ if (!buddyState)
1521
+ return;
1522
+ if (key === "\x1b") {
1523
+ inventoryUI = (0, inventoryUI_1.createInventoryUI)();
1524
+ redraw();
1525
+ return;
1526
+ }
1527
+ if (key === "\t") {
1528
+ inventoryUI = (0, inventoryUI_1.switchInventoryTab)(inventoryUI);
1529
+ redraw();
1530
+ return;
1531
+ }
1532
+ if (key === "\x1b[A") {
1533
+ inventoryUI = (0, inventoryUI_1.moveInventorySelection)(inventoryUI, -1);
1534
+ redraw();
1535
+ return;
1536
+ }
1537
+ if (key === "\x1b[B") {
1538
+ inventoryUI = (0, inventoryUI_1.moveInventorySelection)(inventoryUI, 1);
1539
+ redraw();
1540
+ return;
1541
+ }
1542
+ if (key === "\r" || key === "\n") {
1543
+ const slot = (0, inventoryUI_1.getSelectedSlot)(inventoryUI);
1544
+ if (!slot)
1545
+ return;
1546
+ const item = (0, items_1.getItem)(slot.itemId);
1547
+ if (!item)
1548
+ return;
1549
+ // Use or equip the item
1550
+ inventoryUI = (0, inventoryUI_1.createInventoryUI)();
1551
+ if (item.type === "weapon") {
1552
+ // Equip weapon (don't consume)
1553
+ const advStats = { ...buddyState.adventureStats, equippedWeapon: item.id };
1554
+ buddyState = { ...buddyState, adventureStats: advStats };
1555
+ showFeedback(`Equipped ${item.name}! (+${item.combatATK} ATK)`);
1556
+ }
1557
+ else if (item.type === "armor") {
1558
+ const advStats = { ...buddyState.adventureStats, equippedArmor: item.id };
1559
+ buddyState = { ...buddyState, adventureStats: advStats };
1560
+ showFeedback(`Equipped ${item.name}! (+${item.combatDEF} DEF)`);
1561
+ }
1562
+ else if (item.type === "helmet") {
1563
+ const advStats = { ...buddyState.adventureStats, equippedHelmet: item.id };
1564
+ buddyState = { ...buddyState, adventureStats: advStats };
1565
+ showFeedback(`Equipped ${item.name}! (${item.description})`);
1566
+ }
1567
+ else if (item.type === "boots") {
1568
+ const advStats = { ...buddyState.adventureStats, equippedBoots: item.id };
1569
+ buddyState = { ...buddyState, adventureStats: advStats };
1570
+ showFeedback(`Equipped ${item.name}! (${item.description})`);
1571
+ }
1572
+ else if (item.type === "accessory") {
1573
+ const advStats = { ...buddyState.adventureStats, equippedAccessory: item.id };
1574
+ buddyState = { ...buddyState, adventureStats: advStats };
1575
+ showFeedback(`Equipped ${item.name}! (${item.description})`);
1576
+ }
1577
+ else {
1578
+ // Consumable — decrement count
1579
+ const inventory = buddyState.inventory
1580
+ .map((s) => s.itemId === item.id ? { ...s, count: s.count - 1 } : s)
1581
+ .filter((s) => s.count > 0);
1582
+ buddyState = { ...buddyState, inventory };
1583
+ if (item.type === "food") {
1584
+ const activity = (0, activities_1.startActivity)(buddyState, "eating", item.statEffect);
1585
+ buddyState = { ...buddyState, activity };
1586
+ showFeedback(`${buddyState.name} eats a ${item.name}!`);
1587
+ }
1588
+ else if (item.type === "toy") {
1589
+ const activity = (0, activities_1.startActivity)(buddyState, "playing", item.statEffect);
1590
+ buddyState = { ...buddyState, activity };
1591
+ showFeedback(`${buddyState.name} plays with the ${item.name}!`);
1592
+ }
1593
+ else if (item.type === "medicine") {
1594
+ const bStats = { ...buddyState.stats };
1595
+ for (const [key, delta] of Object.entries(item.statEffect)) {
1596
+ bStats[key] = Math.min(100, Math.max(0, bStats[key] + delta));
1597
+ }
1598
+ buddyState = { ...buddyState, stats: bStats, xp: buddyState.xp + item.xpBonus };
1599
+ showFeedback(`${buddyState.name} uses a ${item.name}!`);
1600
+ }
1601
+ }
1602
+ (0, save_1.saveState)(buddyState);
1603
+ redraw();
1604
+ return;
1605
+ }
1606
+ }
1607
+ // ─── Lifecycle ───────────────────────────────────────────────
1608
+ function shutdown() {
1609
+ stopAnimation();
1610
+ if (decayTimer)
1611
+ clearInterval(decayTimer);
1612
+ if (feedbackTimeout)
1613
+ clearTimeout(feedbackTimeout);
1614
+ if (buddyState)
1615
+ (0, save_1.saveState)(buddyState);
1616
+ screen.cleanup();
1617
+ console.log(` ${screen_1.ansi.dim}Goodbye! Your buddy will be waiting.${screen_1.ansi.reset}\n`);
1618
+ process.exit(0);
1619
+ }
1620
+ async function main() {
1621
+ // Check for updates in the background (non-blocking)
1622
+ const { checkForUpdates, getUpdateBanner } = require("./updates");
1623
+ checkForUpdates();
1624
+ // Show update banner after a short delay (gives the HTTP request time to complete)
1625
+ setTimeout(() => {
1626
+ const banner = getUpdateBanner();
1627
+ if (banner)
1628
+ showFeedback("New version available! Type /update to install.", 5000);
1629
+ }, 4000);
1630
+ // Load settings
1631
+ (0, settings_1.loadSettings)();
1632
+ // Handle clean exit
1633
+ process.on("SIGINT", shutdown);
1634
+ process.on("SIGTERM", shutdown);
1635
+ buddyState = (0, save_1.loadState)();
1636
+ setupInput();
1637
+ if (buddyState) {
1638
+ // Returning player — catch up on activities and decay
1639
+ buddyState = (0, activities_1.applyActivityProgress)(buddyState);
1640
+ buddyState = (0, decay_1.applyDecay)(buddyState);
1641
+ (0, save_1.saveState)(buddyState);
1642
+ appPhase = "playing";
1643
+ const hoursAway = (Date.now() - buddyState.lastUpdated) / (1000 * 60 * 60);
1644
+ if (hoursAway > 1) {
1645
+ showFeedback(`You were away for ${Math.round(hoursAway)} hours. ${buddyState.name} missed you!`, 4000);
1646
+ }
1647
+ else {
1648
+ showFeedback(`Welcome back! ${buddyState.name} is happy to see you.`, 3000);
1649
+ }
1650
+ startAnimation();
1651
+ startDecay();
1652
+ redraw();
1653
+ }
1654
+ else {
1655
+ // New player — show roll screen
1656
+ appPhase = "roll-wait";
1657
+ screen.draw((0, display_1.renderRollScreen)("waiting"), ` ${screen_1.ansi.dim}Press Enter to roll...${screen_1.ansi.reset}`);
1658
+ }
1659
+ }
1660
+ main().catch((err) => {
1661
+ screen.cleanup();
1662
+ console.error("Fatal error:", err);
1663
+ process.exit(1);
1664
+ });
1665
+ //# sourceMappingURL=index.js.map