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.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/adventure/adventureUI.d.ts +24 -0
- package/dist/adventure/adventureUI.js +290 -0
- package/dist/adventure/adventures.d.ts +4 -0
- package/dist/adventure/adventures.js +206 -0
- package/dist/adventure/biomes.d.ts +30 -0
- package/dist/adventure/biomes.js +80 -0
- package/dist/adventure/combat/combat.d.ts +14 -0
- package/dist/adventure/combat/combat.js +638 -0
- package/dist/adventure/combat/combatUI.d.ts +5 -0
- package/dist/adventure/combat/combatUI.js +116 -0
- package/dist/adventure/combat/conditions.d.ts +20 -0
- package/dist/adventure/combat/conditions.js +111 -0
- package/dist/adventure/combat/enemies.d.ts +4 -0
- package/dist/adventure/combat/enemies.js +430 -0
- package/dist/adventure/combat/gear.d.ts +3 -0
- package/dist/adventure/combat/gear.js +199 -0
- package/dist/adventure/combat/skills.d.ts +6 -0
- package/dist/adventure/combat/skills.js +197 -0
- package/dist/adventure/combat.d.ts +31 -0
- package/dist/adventure/combat.js +732 -0
- package/dist/adventure/combatUI.d.ts +5 -0
- package/dist/adventure/combatUI.js +116 -0
- package/dist/adventure/endless.d.ts +18 -0
- package/dist/adventure/endless.js +154 -0
- package/dist/adventure/enemies.d.ts +4 -0
- package/dist/adventure/enemies.js +320 -0
- package/dist/adventure/engine.d.ts +20 -0
- package/dist/adventure/engine.js +137 -0
- package/dist/adventure/gear.d.ts +3 -0
- package/dist/adventure/gear.js +149 -0
- package/dist/adventure/generation/biomes.d.ts +30 -0
- package/dist/adventure/generation/biomes.js +102 -0
- package/dist/adventure/generation/endless.d.ts +18 -0
- package/dist/adventure/generation/endless.js +154 -0
- package/dist/adventure/generation/generator.d.ts +9 -0
- package/dist/adventure/generation/generator.js +245 -0
- package/dist/adventure/generation/templates.d.ts +25 -0
- package/dist/adventure/generation/templates.js +228 -0
- package/dist/adventure/generator.d.ts +9 -0
- package/dist/adventure/generator.js +245 -0
- package/dist/adventure/skills.d.ts +6 -0
- package/dist/adventure/skills.js +197 -0
- package/dist/adventure/templates.d.ts +25 -0
- package/dist/adventure/templates.js +228 -0
- package/dist/adventure/types.d.ts +236 -0
- package/dist/adventure/types.js +97 -0
- package/dist/app/state.d.ts +49 -0
- package/dist/app/state.js +51 -0
- package/dist/buddy/activities.d.ts +16 -0
- package/dist/buddy/activities.js +90 -0
- package/dist/buddy/decay.d.ts +3 -0
- package/dist/buddy/decay.js +45 -0
- package/dist/buddy/leveling.d.ts +11 -0
- package/dist/buddy/leveling.js +25 -0
- package/dist/buddy/roll.d.ts +4 -0
- package/dist/buddy/roll.js +61 -0
- package/dist/buddy/species.d.ts +4 -0
- package/dist/buddy/species.js +592 -0
- package/dist/buddy/titles.d.ts +17 -0
- package/dist/buddy/titles.js +89 -0
- package/dist/buddy/types.d.ts +92 -0
- package/dist/buddy/types.js +21 -0
- package/dist/commands/actions.d.ts +2 -0
- package/dist/commands/actions.js +141 -0
- package/dist/commands/admin.d.ts +2 -0
- package/dist/commands/admin.js +202 -0
- package/dist/commands/registry.d.ts +25 -0
- package/dist/commands/registry.js +31 -0
- package/dist/commands/social.d.ts +2 -0
- package/dist/commands/social.js +92 -0
- package/dist/dialogue/engine.d.ts +7 -0
- package/dist/dialogue/engine.js +68 -0
- package/dist/dialogue/lines.d.ts +26 -0
- package/dist/dialogue/lines.js +294 -0
- package/dist/events/engine.d.ts +20 -0
- package/dist/events/engine.js +51 -0
- package/dist/events/events.d.ts +13 -0
- package/dist/events/events.js +149 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1665 -0
- package/dist/inventory/finding.d.ts +11 -0
- package/dist/inventory/finding.js +99 -0
- package/dist/inventory/items.d.ts +31 -0
- package/dist/inventory/items.js +63 -0
- package/dist/minigames/copycat.d.ts +2 -0
- package/dist/minigames/copycat.js +153 -0
- package/dist/minigames/fetch.d.ts +2 -0
- package/dist/minigames/fetch.js +179 -0
- package/dist/minigames/moodmatch.d.ts +2 -0
- package/dist/minigames/moodmatch.js +144 -0
- package/dist/minigames/quickpaws.d.ts +2 -0
- package/dist/minigames/quickpaws.js +142 -0
- package/dist/minigames/registry.d.ts +5 -0
- package/dist/minigames/registry.js +16 -0
- package/dist/minigames/rpsplus.d.ts +2 -0
- package/dist/minigames/rpsplus.js +168 -0
- package/dist/minigames/treasurehunt.d.ts +2 -0
- package/dist/minigames/treasurehunt.js +146 -0
- package/dist/minigames/types.d.ts +30 -0
- package/dist/minigames/types.js +69 -0
- package/dist/rendering/commandPalette.d.ts +16 -0
- package/dist/rendering/commandPalette.js +77 -0
- package/dist/rendering/display.d.ts +9 -0
- package/dist/rendering/display.js +231 -0
- package/dist/rendering/inventoryUI.d.ts +14 -0
- package/dist/rendering/inventoryUI.js +99 -0
- package/dist/rendering/items.d.ts +7 -0
- package/dist/rendering/items.js +34 -0
- package/dist/rendering/listUtils.d.ts +3 -0
- package/dist/rendering/listUtils.js +24 -0
- package/dist/rendering/minigameUI.d.ts +11 -0
- package/dist/rendering/minigameUI.js +37 -0
- package/dist/rendering/overlayUI.d.ts +24 -0
- package/dist/rendering/overlayUI.js +184 -0
- package/dist/rendering/scene.d.ts +8 -0
- package/dist/rendering/scene.js +87 -0
- package/dist/rendering/screen.d.ts +43 -0
- package/dist/rendering/screen.js +97 -0
- package/dist/sound/sound.d.ts +11 -0
- package/dist/sound/sound.js +55 -0
- package/dist/state/save.d.ts +5 -0
- package/dist/state/save.js +100 -0
- package/dist/state/settings.d.ts +7 -0
- package/dist/state/settings.js +81 -0
- package/dist/story/mainStory.d.ts +4 -0
- package/dist/story/mainStory.js +3111 -0
- package/dist/story/npcs.d.ts +17 -0
- package/dist/story/npcs.js +137 -0
- package/dist/story/progress.d.ts +26 -0
- package/dist/story/progress.js +168 -0
- package/dist/story/seasonal.d.ts +6 -0
- package/dist/story/seasonal.js +96 -0
- package/dist/story/shop.d.ts +7 -0
- package/dist/story/shop.js +26 -0
- package/dist/story/sideQuests.d.ts +4 -0
- package/dist/story/sideQuests.js +151 -0
- package/dist/story/types.d.ts +61 -0
- package/dist/story/types.js +36 -0
- package/dist/updates.d.ts +23 -0
- package/dist/updates.js +142 -0
- 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
|