clibuddy 1.0.0 → 1.2.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.
@@ -19,6 +19,6 @@ export declare function changeQuestChain(menu: AdventureMenuState, delta: number
19
19
  export declare function changeExpeditionBiome(menu: AdventureMenuState, delta: number): AdventureMenuState;
20
20
  export declare function getSelectedBiomeId(menu: AdventureMenuState): string;
21
21
  export declare function renderAdventureMenu(menu: AdventureMenuState, state: BuddyState): string[];
22
- export declare function renderRoom(advState: AdventureState, adventure: AdventureDef, buddyName: string): string;
23
- export declare function renderResult(advState: AdventureState, adventure: AdventureDef, buddyName: string): string;
22
+ export declare function renderRoom(advState: AdventureState, adventure: AdventureDef, state: BuddyState): string;
23
+ export declare function renderResult(advState: AdventureState, adventure: AdventureDef, state: BuddyState): string;
24
24
  //# sourceMappingURL=adventureUI.d.ts.map
@@ -10,11 +10,14 @@ exports.getSelectedBiomeId = getSelectedBiomeId;
10
10
  exports.renderAdventureMenu = renderAdventureMenu;
11
11
  exports.renderRoom = renderRoom;
12
12
  exports.renderResult = renderResult;
13
- const types_1 = require("./types");
13
+ const types_1 = require("../buddy/types");
14
+ const species_1 = require("../buddy/species");
15
+ const types_2 = require("./types");
14
16
  const biomes_1 = require("./generation/biomes");
15
- const types_2 = require("../story/types");
17
+ const types_3 = require("../story/types");
16
18
  const npcs_1 = require("../story/npcs");
17
19
  const listUtils_1 = require("../rendering/listUtils");
20
+ const scene_1 = require("../rendering/scene");
18
21
  const screen_1 = require("../rendering/screen");
19
22
  function createAdventureMenu() {
20
23
  return { active: false, tab: "story", selectedIndex: 0, expeditionDifficulty: 1, expeditionBiomeIndex: 0, questChains: [], selectedChainIndex: 0 };
@@ -72,13 +75,13 @@ const DIFF_LABELS = {
72
75
  };
73
76
  function renderAdventureMenu(menu, state) {
74
77
  const lines = [];
75
- lines.push(` ${screen_1.ansi.dim}${"─".repeat(48)}${screen_1.ansi.reset}`);
78
+ lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
76
79
  // Tab bar
77
80
  const storyTab = menu.tab === "story" ? `${screen_1.ansi.bold}${screen_1.ansi.colors.yellow}[Story]${screen_1.ansi.reset}` : `${screen_1.ansi.dim}[Story]${screen_1.ansi.reset}`;
78
81
  const expTab = menu.tab === "expedition" ? `${screen_1.ansi.bold}${screen_1.ansi.colors.cyan}[Expedition]${screen_1.ansi.reset}` : `${screen_1.ansi.dim}[Expedition]${screen_1.ansi.reset}`;
79
82
  const endTab = menu.tab === "endless" ? `${screen_1.ansi.bold}${screen_1.ansi.colors.magenta}[Endless]${screen_1.ansi.reset}` : `${screen_1.ansi.dim}[Endless]${screen_1.ansi.reset}`;
80
83
  lines.push(` ${storyTab} ${expTab} ${endTab} ${screen_1.ansi.dim}Tab${screen_1.ansi.reset}`);
81
- lines.push(` ${screen_1.ansi.dim}${"─".repeat(48)}${screen_1.ansi.reset}`);
84
+ lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
82
85
  if (menu.tab === "story") {
83
86
  renderQuestsTab(lines, menu, state);
84
87
  }
@@ -88,7 +91,7 @@ function renderAdventureMenu(menu, state) {
88
91
  else {
89
92
  renderEndlessTab(lines, state);
90
93
  }
91
- lines.push(` ${screen_1.ansi.dim}${"─".repeat(48)}${screen_1.ansi.reset}`);
94
+ lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
92
95
  return lines;
93
96
  }
94
97
  function renderQuestsTab(lines, menu, state) {
@@ -109,8 +112,8 @@ function renderQuestsTab(lines, menu, state) {
109
112
  // Chapter list
110
113
  for (let i = 0; i < chain.chapters.length; i++) {
111
114
  const chapter = chain.chapters[i];
112
- const completed = (0, types_2.isChapterComplete)(chapter.id, progress);
113
- const blockReason = (0, types_2.canStartChapter)(chapter, progress, state.level);
115
+ const completed = (0, types_3.isChapterComplete)(chapter.id, progress);
116
+ const blockReason = (0, types_3.canStartChapter)(chapter, progress, state.level);
114
117
  const selected = i === menu.selectedIndex;
115
118
  const pointer = selected ? `${screen_1.ansi.colors.cyan}▸${screen_1.ansi.reset}` : " ";
116
119
  if (completed) {
@@ -148,16 +151,20 @@ function renderExpeditionTab(lines, menu, state) {
148
151
  lines.push("");
149
152
  lines.push(` ${screen_1.ansi.bold}Biome:${screen_1.ansi.reset} ${biome?.name ?? biomeId} ${screen_1.ansi.dim}←→ to change${screen_1.ansi.reset}`);
150
153
  lines.push("");
154
+ const requiredLevel = Math.max(1, (diff - 1) * 2);
151
155
  const energyCost = 10 + diff * 3;
152
156
  const hungerCost = 5 + diff * 2;
153
- lines.push(` ${screen_1.ansi.dim}Cost: ${energyCost} energy, ${hungerCost} hunger${screen_1.ansi.reset}`);
154
- const canGo = state.level >= Math.max(1, (diff - 1) * 2) && state.stats.energy >= energyCost && state.stats.hunger >= hungerCost;
157
+ const levelOk = state.level >= requiredLevel;
158
+ const energyOk = state.stats.energy >= energyCost;
159
+ const hungerOk = state.stats.hunger >= hungerCost;
160
+ const canGo = levelOk && energyOk && hungerOk;
161
+ const check = (ok) => ok ? `${screen_1.ansi.colors.green}✓${screen_1.ansi.reset}` : `${screen_1.ansi.colors.red}✗${screen_1.ansi.reset}`;
162
+ lines.push(` ${check(levelOk)} Level: ${state.level} ${screen_1.ansi.dim}(need ${requiredLevel}+)${screen_1.ansi.reset}`);
163
+ lines.push(` ${check(energyOk)} Energy: ${Math.round(state.stats.energy)} ${screen_1.ansi.dim}(costs ${energyCost})${screen_1.ansi.reset}`);
164
+ lines.push(` ${check(hungerOk)} Hunger: ${Math.round(state.stats.hunger)} ${screen_1.ansi.dim}(costs ${hungerCost})${screen_1.ansi.reset}`);
155
165
  if (canGo) {
156
166
  lines.push(` ${screen_1.ansi.colors.green}Ready to embark!${screen_1.ansi.reset}`);
157
167
  }
158
- else {
159
- lines.push(` ${screen_1.ansi.colors.red}Not ready — check level/energy/hunger${screen_1.ansi.reset}`);
160
- }
161
168
  lines.push(` ${screen_1.ansi.dim}Enter to generate & start, Esc to close${screen_1.ansi.reset}`);
162
169
  }
163
170
  function renderEndlessTab(lines, state) {
@@ -170,65 +177,90 @@ function renderEndlessTab(lines, state) {
170
177
  lines.push(` ${screen_1.ansi.dim}• Biome rotates each room${screen_1.ansi.reset}`);
171
178
  lines.push(` ${screen_1.ansi.dim}• Defeat = run over${screen_1.ansi.reset}`);
172
179
  lines.push("");
173
- lines.push(` ${screen_1.ansi.dim}Requires: Level 5+, 20 energy, 15 hunger${screen_1.ansi.reset}`);
174
- const canGo = state.level >= 5 && state.stats.energy >= 20 && state.stats.hunger >= 15;
180
+ const levelOk = state.level >= 5;
181
+ const energyOk = state.stats.energy >= 20;
182
+ const hungerOk = state.stats.hunger >= 15;
183
+ const canGo = levelOk && energyOk && hungerOk;
184
+ const check = (ok) => ok ? `${screen_1.ansi.colors.green}✓${screen_1.ansi.reset}` : `${screen_1.ansi.colors.red}✗${screen_1.ansi.reset}`;
185
+ lines.push(` ${check(levelOk)} Level: ${state.level} ${screen_1.ansi.dim}(need 5+)${screen_1.ansi.reset}`);
186
+ lines.push(` ${check(energyOk)} Energy: ${Math.round(state.stats.energy)} ${screen_1.ansi.dim}(costs 20)${screen_1.ansi.reset}`);
187
+ lines.push(` ${check(hungerOk)} Hunger: ${Math.round(state.stats.hunger)} ${screen_1.ansi.dim}(costs 15)${screen_1.ansi.reset}`);
175
188
  if (canGo) {
176
189
  lines.push(` ${screen_1.ansi.colors.green}Ready to descend!${screen_1.ansi.reset}`);
177
190
  }
178
- else {
179
- lines.push(` ${screen_1.ansi.colors.red}Not ready — check level/energy/hunger${screen_1.ansi.reset}`);
180
- }
181
191
  lines.push(` ${screen_1.ansi.dim}Enter to begin your descent${screen_1.ansi.reset}`);
182
192
  }
183
193
  // ─── Adventure Room Rendering ────────────────────────────────
184
- function renderRoom(advState, adventure, buddyName) {
185
- const room = (0, types_1.getRoom)(adventure, advState.currentRoomId);
194
+ function renderRoom(advState, adventure, state) {
195
+ const room = (0, types_2.getRoom)(adventure, advState.currentRoomId);
186
196
  if (!room)
187
197
  return " Room not found!";
188
- const progress = (0, types_1.getRoomProgress)(advState);
198
+ const progress = (0, types_2.getRoomProgress)(advState);
189
199
  const lines = [];
200
+ // ── Header with box-drawn frame ──
190
201
  lines.push("");
191
202
  if (adventure.isScene || adventure.hideRoomCounter) {
192
- lines.push(` ${screen_1.ansi.bold}${adventure.name}${screen_1.ansi.reset}`);
203
+ lines.push(` ${screen_1.ansi.bold}╔══ ${adventure.name} ${"═".repeat(Math.max(1, screen_1.UI_WIDTH - adventure.name.length - 6))}╗${screen_1.ansi.reset}`);
193
204
  }
194
205
  else {
195
- lines.push(` ${screen_1.ansi.bold}${adventure.name}${screen_1.ansi.reset} ${screen_1.ansi.dim}Room ${progress}/${adventure.rooms.length}${screen_1.ansi.reset} ${screen_1.ansi.dim}Morale: ${advState.morale}%${screen_1.ansi.reset}`);
206
+ const roomInfo = `Room ${progress}/${adventure.rooms.length}`;
207
+ const fillLen = Math.max(1, screen_1.UI_WIDTH - adventure.name.length - roomInfo.length - 8);
208
+ lines.push(` ${screen_1.ansi.bold}╔══ ${adventure.name} ${"═".repeat(fillLen)} ${roomInfo} ═╗${screen_1.ansi.reset}`);
209
+ }
210
+ if (!adventure.isScene && !adventure.hideRoomCounter) {
211
+ lines.push(` ${screen_1.ansi.dim}Morale: ${advState.morale}%${screen_1.ansi.reset}`);
196
212
  }
197
- lines.push(` ${screen_1.ansi.dim}${"─".repeat(44)}${screen_1.ansi.reset}`);
198
213
  lines.push("");
199
214
  lines.push(` ${screen_1.ansi.bold}${room.title}${screen_1.ansi.reset}`);
200
215
  lines.push("");
201
- // Show NPC ASCII art if present
216
+ // ── Side-by-side buddy + NPC art (or just buddy art) ──
217
+ const species = (0, species_1.getSpecies)(state.speciesId);
218
+ const buddyColor = species ? types_1.RARITY_COLORS[species.rarity] : "";
219
+ const buddyArtLines = species ? species.animations.idle[0].split("\n") : [];
202
220
  if (room.npcsPresent && room.npcsPresent.length > 0) {
203
- for (const npcId of room.npcsPresent) {
204
- const npc = (0, npcs_1.getNPC)(npcId);
205
- if (npc) {
206
- lines.push(` ${screen_1.ansi.dim}${npc.name} the ${npc.title}${screen_1.ansi.reset}`);
207
- for (const artLine of npc.art) {
208
- lines.push(` ${screen_1.ansi.colors.cyan}${artLine}${screen_1.ansi.reset}`);
209
- }
210
- lines.push("");
221
+ const npc = (0, npcs_1.getNPC)(room.npcsPresent[0]);
222
+ if (npc && species) {
223
+ // Side-by-side: buddy (left) + NPC (right)
224
+ const artLines = (0, scene_1.composeDualArt)(buddyArtLines, npc.art, buddyColor, screen_1.ansi.colors.cyan);
225
+ for (const line of artLines) {
226
+ lines.push(` ${line}`);
211
227
  }
228
+ // Labels below the art pair
229
+ const buddyLabel = `${buddyColor}${state.name}${screen_1.ansi.reset}`;
230
+ const npcLabel = `${screen_1.ansi.colors.cyan}${npc.name}${screen_1.ansi.reset} ${screen_1.ansi.dim}${npc.title}${screen_1.ansi.reset}`;
231
+ lines.push(` ${buddyLabel} ${npcLabel}`);
232
+ lines.push("");
212
233
  }
213
234
  }
214
- // Paged text display — show LINES_PER_PAGE lines per page
235
+ else if (species) {
236
+ // No NPC — show buddy art centered, dimmed slightly
237
+ for (const artLine of buddyArtLines) {
238
+ lines.push(` ${buddyColor}${artLine}${screen_1.ansi.reset}`);
239
+ }
240
+ lines.push("");
241
+ }
242
+ // ── Sliding window text — show only current page ──
215
243
  const LINES_PER_PAGE = 3;
216
244
  const totalPages = Math.ceil(room.text.length / LINES_PER_PAGE);
217
245
  const currentPage = advState.textPage;
218
246
  const textFullyShown = advState.textFullyShown || currentPage >= totalPages - 1;
219
- // Show text up to current page
220
- const linesToShow = Math.min(room.text.length, (currentPage + 1) * LINES_PER_PAGE);
221
- for (let i = 0; i < linesToShow; i++) {
247
+ // Scroll indicator: prior text exists
248
+ if (currentPage > 0) {
249
+ lines.push(` ${screen_1.ansi.dim} ▲${screen_1.ansi.reset}`);
250
+ }
251
+ // Show only current page's lines (sliding window)
252
+ const startLine = currentPage * LINES_PER_PAGE;
253
+ const endLine = Math.min(room.text.length, startLine + LINES_PER_PAGE);
254
+ for (let i = startLine; i < endLine; i++) {
222
255
  lines.push(` ${room.text[i]}`);
223
256
  }
224
257
  lines.push("");
225
258
  if (!textFullyShown) {
226
- // More text to show prompt to continue reading
227
- lines.push(` ${screen_1.ansi.dim}▼ Press Enter to continue reading... (${currentPage + 1}/${totalPages})${screen_1.ansi.reset}`);
259
+ lines.push(` ${screen_1.ansi.dim}▼ Enter to continue... (${currentPage + 1}/${totalPages})${screen_1.ansi.reset}`);
228
260
  }
229
261
  else {
230
- // All text shown — show buddy line + choices
231
- lines.push(` ${buddyName}: "${advState.buddyReaction}"`);
262
+ // All text shown — buddy reaction + choices
263
+ lines.push(` ${buddyColor}${state.name}:${screen_1.ansi.reset} "${advState.buddyReaction}"`);
232
264
  lines.push("");
233
265
  if (room.type === "narrative" && room.choices) {
234
266
  for (let i = 0; i < room.choices.length; i++) {
@@ -236,7 +268,6 @@ function renderRoom(advState, adventure, buddyName) {
236
268
  }
237
269
  }
238
270
  else if (room.type === "event" && room.eventChoices) {
239
- // Check if event was already resolved
240
271
  const alreadyResolved = room.eventChoices.some((c) => c.outcome === advState.buddyReaction);
241
272
  if (alreadyResolved) {
242
273
  lines.push(` ${screen_1.ansi.dim}Press Enter to continue...${screen_1.ansi.reset}`);
@@ -262,26 +293,37 @@ function renderRoom(advState, adventure, buddyName) {
262
293
  return lines.join("\n");
263
294
  }
264
295
  // ─── Result Screen ───────────────────────────────────────────
265
- function renderResult(advState, adventure, buddyName) {
296
+ function renderResult(advState, adventure, state) {
266
297
  const lines = [];
298
+ const w = screen_1.UI_WIDTH;
267
299
  lines.push("");
268
- lines.push(` ${screen_1.ansi.bold}${screen_1.ansi.colors.green}Adventure Complete!${screen_1.ansi.reset}`);
269
- lines.push(` ${screen_1.ansi.dim}${"".repeat(44)}${screen_1.ansi.reset}`);
270
- lines.push("");
271
- lines.push(` ${screen_1.ansi.bold}${adventure.name}${screen_1.ansi.reset} Cleared!`);
272
- lines.push("");
273
- lines.push(` Battles: ${advState.battlesWon} won, ${advState.battlesLost} lost, ${advState.battlesFled} fled`);
300
+ lines.push(` ${screen_1.ansi.colors.green}╔${"═".repeat(w)}╗${screen_1.ansi.reset}`);
301
+ lines.push(` ${screen_1.ansi.colors.green}║${screen_1.ansi.reset} ${screen_1.ansi.bold}★ ADVENTURE COMPLETE ★${screen_1.ansi.reset}${" ".repeat(Math.max(0, w - 24))}${screen_1.ansi.colors.green}║${screen_1.ansi.reset}`);
302
+ lines.push(` ${screen_1.ansi.colors.green}╠${"".repeat(w)}╣${screen_1.ansi.reset}`);
303
+ const padLine = (text, visLen) => {
304
+ return ` ${screen_1.ansi.colors.green}║${screen_1.ansi.reset} ${text}${" ".repeat(Math.max(0, w - visLen - 2))}${screen_1.ansi.colors.green}║${screen_1.ansi.reset}`;
305
+ };
306
+ lines.push(padLine(`${screen_1.ansi.bold}${adventure.name}${screen_1.ansi.reset}`, adventure.name.length));
307
+ lines.push(padLine("", 0));
308
+ const battleText = `Battles: ${advState.battlesWon} won, ${advState.battlesLost} lost, ${advState.battlesFled} fled`;
309
+ lines.push(padLine(battleText, battleText.length));
274
310
  if (advState.lootCollected.length > 0) {
275
311
  const lootNames = advState.lootCollected.map((id) => id.replace(/_/g, " "));
276
- lines.push(` Loot: ${lootNames.join(", ")}`);
312
+ const lootText = `Loot: ${lootNames.join(", ")}`;
313
+ lines.push(padLine(lootText, lootText.length));
277
314
  }
278
- lines.push(` XP earned: ${screen_1.ansi.bold}${advState.xpEarned}${screen_1.ansi.reset}`);
279
- lines.push(` Final morale: ${advState.morale}%`);
315
+ const xpText = `XP earned: ${advState.xpEarned}`;
316
+ lines.push(padLine(`${screen_1.ansi.bold}${xpText}${screen_1.ansi.reset}`, xpText.length));
317
+ lines.push(padLine(`Final morale: ${advState.morale}%`, `Final morale: ${advState.morale}%`.length));
280
318
  if (advState.damageTaken === 0 && advState.battlesWon > 0) {
281
- lines.push(` ${screen_1.ansi.colors.yellow}★ PERFECT — No damage taken!${screen_1.ansi.reset}`);
319
+ const perfText = "★ PERFECT — No damage taken!";
320
+ lines.push(padLine(`${screen_1.ansi.colors.yellow}${perfText}${screen_1.ansi.reset}`, perfText.length));
282
321
  }
322
+ lines.push(` ${screen_1.ansi.colors.green}╚${"═".repeat(w)}╝${screen_1.ansi.reset}`);
283
323
  lines.push("");
284
- lines.push(` ${buddyName}: "${advState.buddyReaction}"`);
324
+ const species = (0, species_1.getSpecies)(state.speciesId);
325
+ const buddyColor = species ? types_1.RARITY_COLORS[species.rarity] : "";
326
+ lines.push(` ${buddyColor}${state.name}:${screen_1.ansi.reset} "${advState.buddyReaction}"`);
285
327
  lines.push("");
286
328
  lines.push(` ${screen_1.ansi.dim}Press Enter to return...${screen_1.ansi.reset}`);
287
329
  lines.push("");
@@ -1,5 +1,14 @@
1
- import { BuddyState } from "../../buddy/types";
1
+ import { BuddyState, InventorySlot } from "../../buddy/types";
2
2
  import { CombatState } from "../types";
3
+ import { ItemRarity } from "../../inventory/items";
3
4
  export declare function renderCombat(combat: CombatState, state: BuddyState, adventureName: string): string;
4
5
  export declare function renderSkillMenu(combat: CombatState, state: BuddyState): string;
6
+ /** Get inventory slots usable in combat (medicine items with health effect) */
7
+ export declare function getCombatItems(inventory: InventorySlot[]): {
8
+ slot: InventorySlot;
9
+ name: string;
10
+ healAmount: number;
11
+ rarity: ItemRarity;
12
+ }[];
13
+ export declare function renderItemMenu(combat: CombatState, inventory: InventorySlot[], selectedIndex: number): string;
5
14
  //# sourceMappingURL=combatUI.d.ts.map
@@ -2,12 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderCombat = renderCombat;
4
4
  exports.renderSkillMenu = renderSkillMenu;
5
+ exports.getCombatItems = getCombatItems;
6
+ exports.renderItemMenu = renderItemMenu;
5
7
  const types_1 = require("../../buddy/types");
6
8
  const species_1 = require("../../buddy/species");
7
9
  const types_2 = require("../types");
8
10
  const lines_1 = require("../../dialogue/lines");
9
11
  const skills_1 = require("./skills");
12
+ const items_1 = require("../../inventory/items");
10
13
  const screen_1 = require("../../rendering/screen");
14
+ const scene_1 = require("../../rendering/scene");
11
15
  const CONDITION_ICONS = {
12
16
  burn: "🔥", poison: "☠", freeze: "❄", stun: "⚡", charm: "💕",
13
17
  blind: "👁", regen: "💚", shield: "🛡", buff: "↑", debuff: "↓",
@@ -34,41 +38,76 @@ function hpBar(current, max, width = 15) {
34
38
  function renderCombat(combat, state, adventureName) {
35
39
  const species = (0, species_1.getSpecies)(state.speciesId);
36
40
  const lines = [];
37
- lines.push("");
38
- lines.push(` ${screen_1.ansi.bold}${adventureName}${screen_1.ansi.reset} ${screen_1.ansi.dim}Combat${screen_1.ansi.reset}`);
39
- lines.push(` ${screen_1.ansi.dim}${"".repeat(44)}${screen_1.ansi.reset}`);
40
- lines.push("");
41
- // Enemy
41
+ const termWidth = process.stdout.columns || 80;
42
+ const speciesColor = species ? types_1.RARITY_COLORS[species.rarity] : "";
43
+ const playerElemIcon = types_2.ELEMENT_ICONS[combat.playerElement] ?? "";
42
44
  const enemyElemIcon = types_2.ELEMENT_ICONS[combat.enemyElement] ?? "";
43
- lines.push(` ${screen_1.ansi.colors.red}${screen_1.ansi.bold}${combat.enemy.name}${screen_1.ansi.reset} ${enemyElemIcon}`);
44
- lines.push(` HP: ${hpBar(combat.enemyStats.hp, combat.enemyStats.maxHp)} ${screen_1.ansi.dim}ATK:${combat.enemyStats.atk} DEF:${combat.enemyStats.def}${screen_1.ansi.reset}${formatConditions(combat.enemyConditions)}`);
45
- for (const artLine of combat.enemy.art) {
46
- lines.push(` ${screen_1.ansi.colors.red}${artLine}${screen_1.ansi.reset}`);
47
- }
45
+ const buddyArtLines = species ? species.animations.idle[0].split("\n") : [];
46
+ const enemyArtLines = combat.enemy.art;
47
+ // Pre-calculate content width for the frame
48
+ const buddyHPpreview = `HP: ${hpBar(combat.playerStats.hp, combat.playerStats.maxHp)}`;
49
+ const enemyHPpreview = `HP: ${hpBar(combat.enemyStats.hp, combat.enemyStats.maxHp)}`;
50
+ const leftColWidth = (0, screen_1.stripAnsi)(buddyHPpreview).length + 2;
51
+ const contentWidth = leftColWidth + (0, screen_1.stripAnsi)(enemyHPpreview).length;
52
+ // ── Header with box frame matched to content ──
53
+ const frameWidth = Math.max(screen_1.UI_WIDTH, contentWidth + 4); // +4 for " " indent
48
54
  lines.push("");
49
- lines.push(` ${screen_1.ansi.dim} vs${screen_1.ansi.reset}`);
55
+ const battleLabel = "⚔ BATTLE";
56
+ const fillLen = Math.max(1, frameWidth - adventureName.length - battleLabel.length - 8);
57
+ lines.push(` ${screen_1.ansi.bold}╔══ ${adventureName} ${"═".repeat(fillLen)} ${battleLabel} ═╗${screen_1.ansi.reset}`);
50
58
  lines.push("");
51
- // Player buddy
52
- const speciesColor = species ? types_1.RARITY_COLORS[species.rarity] : "";
53
- const playerElemIcon = types_2.ELEMENT_ICONS[combat.playerElement] ?? "";
54
- lines.push(` ${speciesColor}${screen_1.ansi.bold}${state.name}${screen_1.ansi.reset} ${playerElemIcon}`);
55
- lines.push(` HP: ${hpBar(combat.playerStats.hp, combat.playerStats.maxHp)} ${screen_1.ansi.dim}ATK:${combat.playerStats.atk} DEF:${combat.playerStats.def}${screen_1.ansi.reset}${formatConditions(combat.playerConditions)}`);
56
- if (species) {
57
- const frame = species.animations.idle[0].split("\n");
58
- for (const artLine of frame) {
59
- lines.push(` ${speciesColor}${artLine}${screen_1.ansi.reset}`);
59
+ // ── Side-by-side layout (buddy left, enemy right) ──
60
+ if (termWidth >= 60 && species) {
61
+ const leftArtWidth = Math.max(...buddyArtLines.map(l => (0, screen_1.stripAnsi)(l).length));
62
+ // Art gap calculated so enemy art starts at same column as enemy HP
63
+ const artGap = Math.max(4, leftColWidth - leftArtWidth);
64
+ const artLines = (0, scene_1.composeDualArt)(buddyArtLines, enemyArtLines, speciesColor, screen_1.ansi.colors.red, artGap);
65
+ // Name labels above art, aligned to same columns
66
+ const buddyLabel = `${speciesColor}${screen_1.ansi.bold}${state.name}${screen_1.ansi.reset} ${playerElemIcon}`;
67
+ const enemyLabel = `${screen_1.ansi.colors.red}${screen_1.ansi.bold}${combat.enemy.name}${screen_1.ansi.reset} ${enemyElemIcon}`;
68
+ lines.push(` ${(0, screen_1.visPad)(buddyLabel, leftColWidth)}${screen_1.ansi.dim}VS${screen_1.ansi.reset} ${enemyLabel}`);
69
+ lines.push("");
70
+ // Art side by side
71
+ for (const line of artLines) {
72
+ lines.push(` ${line}`);
73
+ }
74
+ lines.push("");
75
+ // HP bars side-by-side — aligned to same columns as art
76
+ const buddyHPstr = `HP: ${hpBar(combat.playerStats.hp, combat.playerStats.maxHp)}`;
77
+ const enemyHPstr = `HP: ${hpBar(combat.enemyStats.hp, combat.enemyStats.maxHp)}`;
78
+ const buddyStats = `${screen_1.ansi.dim}ATK:${combat.playerStats.atk} DEF:${combat.playerStats.def}${screen_1.ansi.reset}${formatConditions(combat.playerConditions)}`;
79
+ const enemyStats = `${screen_1.ansi.dim}ATK:${combat.enemyStats.atk} DEF:${combat.enemyStats.def}${screen_1.ansi.reset}${formatConditions(combat.enemyConditions)}`;
80
+ lines.push(` ${(0, screen_1.visPad)(buddyHPstr, leftColWidth)}${enemyHPstr}`);
81
+ lines.push(` ${(0, screen_1.visPad)(buddyStats, leftColWidth)}${enemyStats}`);
82
+ }
83
+ else {
84
+ // ── Narrow fallback: vertical stack (original layout) ──
85
+ lines.push(` ${screen_1.ansi.colors.red}${screen_1.ansi.bold}${combat.enemy.name}${screen_1.ansi.reset} ${enemyElemIcon}`);
86
+ lines.push(` HP: ${hpBar(combat.enemyStats.hp, combat.enemyStats.maxHp)} ${screen_1.ansi.dim}ATK:${combat.enemyStats.atk} DEF:${combat.enemyStats.def}${screen_1.ansi.reset}${formatConditions(combat.enemyConditions)}`);
87
+ for (const artLine of enemyArtLines) {
88
+ lines.push(` ${screen_1.ansi.colors.red}${artLine}${screen_1.ansi.reset}`);
89
+ }
90
+ lines.push("");
91
+ lines.push(` ${screen_1.ansi.dim} vs${screen_1.ansi.reset}`);
92
+ lines.push("");
93
+ lines.push(` ${speciesColor}${screen_1.ansi.bold}${state.name}${screen_1.ansi.reset} ${playerElemIcon}`);
94
+ lines.push(` HP: ${hpBar(combat.playerStats.hp, combat.playerStats.maxHp)} ${screen_1.ansi.dim}ATK:${combat.playerStats.atk} DEF:${combat.playerStats.def}${screen_1.ansi.reset}${formatConditions(combat.playerConditions)}`);
95
+ if (species) {
96
+ for (const artLine of buddyArtLines) {
97
+ lines.push(` ${speciesColor}${artLine}${screen_1.ansi.reset}`);
98
+ }
60
99
  }
61
100
  }
62
101
  lines.push("");
63
- // Combat log (last 2 messages)
102
+ // ── Combat log (last 2 messages) ──
64
103
  const recentLog = combat.log.slice(-2);
65
104
  for (const msg of recentLog) {
66
105
  lines.push(` ${screen_1.ansi.dim}${msg}${screen_1.ansi.reset}`);
67
106
  }
68
107
  lines.push("");
69
- // Action menu (only during player turn)
108
+ // ── Action menu / result ──
70
109
  if (combat.phase === "player_turn") {
71
- lines.push(` ${screen_1.ansi.dim}${"─".repeat(44)}${screen_1.ansi.reset}`);
110
+ lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
72
111
  lines.push(` [1] Attack [2] Skill [3] Item`);
73
112
  lines.push(` [4] Defend [5] Flee`);
74
113
  lines.push(` ${screen_1.ansi.dim}Energy: ${"◆".repeat(combat.combatEnergy)}${"◇".repeat(combat.maxCombatEnergy - combat.combatEnergy)}${screen_1.ansi.reset}`);
@@ -76,15 +115,15 @@ function renderCombat(combat, state, adventureName) {
76
115
  else if (combat.phase === "victory") {
77
116
  const victoryLines = lines_1.COMBAT_VICTORY_LINES[state.speciesId];
78
117
  const victoryLine = victoryLines ? victoryLines[Math.floor(Math.random() * victoryLines.length)] : "We did it!";
79
- lines.push(` ${screen_1.ansi.colors.green}${screen_1.ansi.bold}Victory!${screen_1.ansi.reset} ${screen_1.ansi.dim}+${combat.enemy.xpReward} XP${screen_1.ansi.reset}`);
80
- lines.push(` ${state.name}: "${victoryLine}"`);
118
+ lines.push(` ${screen_1.ansi.colors.green}${screen_1.ansi.bold}Victory!${screen_1.ansi.reset} ${screen_1.ansi.dim}+${combat.enemy.xpReward} XP${screen_1.ansi.reset}`);
119
+ lines.push(` ${speciesColor}${state.name}:${screen_1.ansi.reset} "${victoryLine}"`);
81
120
  lines.push(` ${screen_1.ansi.dim}Press Enter to continue...${screen_1.ansi.reset}`);
82
121
  }
83
122
  else if (combat.phase === "defeat") {
84
123
  const defeatLines = lines_1.COMBAT_DEFEAT_LINES[state.speciesId];
85
124
  const defeatLine = defeatLines ? defeatLines[Math.floor(Math.random() * defeatLines.length)] : "We'll get them next time...";
86
125
  lines.push(` ${screen_1.ansi.colors.red}${screen_1.ansi.bold}Defeated!${screen_1.ansi.reset}`);
87
- lines.push(` ${state.name}: "${defeatLine}"`);
126
+ lines.push(` ${speciesColor}${state.name}:${screen_1.ansi.reset} "${defeatLine}"`);
88
127
  lines.push(` ${screen_1.ansi.dim}Press Enter to retreat...${screen_1.ansi.reset}`);
89
128
  }
90
129
  else if (combat.phase === "fled") {
@@ -92,7 +131,6 @@ function renderCombat(combat, state, adventureName) {
92
131
  lines.push(` ${screen_1.ansi.dim}Press Enter to continue...${screen_1.ansi.reset}`);
93
132
  }
94
133
  else {
95
- // Animating
96
134
  lines.push(` ${screen_1.ansi.dim}...${screen_1.ansi.reset}`);
97
135
  }
98
136
  lines.push("");
@@ -101,7 +139,7 @@ function renderCombat(combat, state, adventureName) {
101
139
  function renderSkillMenu(combat, state) {
102
140
  const skills = (0, skills_1.getAvailableSkills)(state.speciesId, state.level);
103
141
  const lines = [];
104
- lines.push(` ${screen_1.ansi.dim}${"─".repeat(44)}${screen_1.ansi.reset}`);
142
+ lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
105
143
  lines.push(` ${screen_1.ansi.bold}Skills${screen_1.ansi.reset} ${screen_1.ansi.dim}Energy: ${"◆".repeat(combat.combatEnergy)}${"◇".repeat(combat.maxCombatEnergy - combat.combatEnergy)}${screen_1.ansi.reset}`);
106
144
  for (let i = 0; i < skills.length; i++) {
107
145
  const s = skills[i];
@@ -110,7 +148,50 @@ function renderSkillMenu(combat, state) {
110
148
  lines.push(` ${color}[${i + 1}] ${s.name} (${s.energyCost}◆) — ${s.description}${screen_1.ansi.reset}`);
111
149
  }
112
150
  lines.push(` ${screen_1.ansi.dim}[0] Back${screen_1.ansi.reset}`);
113
- lines.push(` ${screen_1.ansi.dim}${"─".repeat(44)}${screen_1.ansi.reset}`);
151
+ lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
152
+ return lines.join("\n");
153
+ }
154
+ /** Get inventory slots usable in combat (medicine items with health effect) */
155
+ function getCombatItems(inventory) {
156
+ const results = [];
157
+ for (const slot of inventory) {
158
+ if (slot.count <= 0)
159
+ continue;
160
+ const item = (0, items_1.getItem)(slot.itemId);
161
+ if (!item || item.type !== "medicine")
162
+ continue;
163
+ const healAmount = item.statEffect.health ?? 0;
164
+ if (healAmount > 0) {
165
+ results.push({ slot, name: item.name, healAmount, rarity: item.rarity });
166
+ }
167
+ }
168
+ return results;
169
+ }
170
+ function renderItemMenu(combat, inventory, selectedIndex) {
171
+ const items = getCombatItems(inventory);
172
+ const lines = [];
173
+ lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
174
+ lines.push(` ${screen_1.ansi.bold}Items${screen_1.ansi.reset} ${screen_1.ansi.dim}HP: ${combat.playerStats.hp}/${combat.playerStats.maxHp}${screen_1.ansi.reset}`);
175
+ if (items.length === 0) {
176
+ lines.push(` ${screen_1.ansi.dim} No medicine in inventory${screen_1.ansi.reset}`);
177
+ }
178
+ else {
179
+ for (let i = 0; i < items.length; i++) {
180
+ const { name, healAmount, slot, rarity } = items[i];
181
+ const rarityColor = items_1.ITEM_RARITY_COLORS[rarity] ?? "";
182
+ const hpAfter = Math.min(combat.playerStats.maxHp, combat.playerStats.hp + healAmount);
183
+ const selected = i === selectedIndex;
184
+ const pointer = selected ? `${screen_1.ansi.colors.cyan}▸${screen_1.ansi.reset}` : " ";
185
+ if (selected) {
186
+ lines.push(` ${pointer} ${rarityColor}${screen_1.ansi.bold}${name}${screen_1.ansi.reset} x${slot.count} ${screen_1.ansi.colors.green}+${healAmount} HP${screen_1.ansi.reset} ${screen_1.ansi.dim}(→${hpAfter})${screen_1.ansi.reset}`);
187
+ }
188
+ else {
189
+ lines.push(` ${pointer} ${screen_1.ansi.dim}${name} x${slot.count}${screen_1.ansi.reset}`);
190
+ }
191
+ }
192
+ }
193
+ lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
194
+ lines.push(` ${screen_1.ansi.dim}Enter to use, Esc to close${screen_1.ansi.reset}`);
114
195
  return lines.join("\n");
115
196
  }
116
197
  //# sourceMappingURL=combatUI.js.map
@@ -36,7 +36,7 @@ function createBuddyState(species, name) {
36
36
  name,
37
37
  speciesId: species.id,
38
38
  stats: {
39
- hunger: 80,
39
+ hunger: 75,
40
40
  happiness: 80,
41
41
  health: 100,
42
42
  energy: 100,
@@ -83,6 +83,7 @@ export interface BuddyState {
83
83
  notifications: Notification[];
84
84
  adventureStats: AdventureStats;
85
85
  questProgress: import("../story/types").QuestProgress;
86
+ tutorialStep?: number;
86
87
  }
87
88
  export declare const RARITY_COLORS: Record<Rarity, string>;
88
89
  export declare const RARITY_LABELS: Record<Rarity, string>;
@@ -38,12 +38,12 @@ function registerActivityCommand(name, description, activityType, countKey, mess
38
38
  },
39
39
  });
40
40
  }
41
- registerActivityCommand("feed", "Feed your buddy to restore hunger", "eating", "feeds", (name) => `${name} starts eating! Nom nom...`);
42
- registerActivityCommand("play", "Play with your buddy to boost happiness", "playing", "plays", (name) => `${name} starts playing!`, (state) => state.stats.energy < 10 ? `${state.name} is too tired to play. Let them /sleep first.` : null);
43
- registerActivityCommand("sleep", "Let your buddy rest to restore energy", "sleeping", "sleeps", (name) => `${name} curls up and falls asleep... Zzz`);
41
+ registerActivityCommand("feed", "Feed Hunger +25 (10s)", "eating", "feeds", (name) => `${name} starts eating! Nom nom...`);
42
+ registerActivityCommand("play", "Play Happiness +20, Energy -15 (15s)", "playing", "plays", (name) => `${name} starts playing!`, (state) => state.stats.energy < 10 ? `${state.name} is too tired to play. Let them /sleep first.` : null);
43
+ registerActivityCommand("sleep", "Sleep Energy +30 (2m, skippable)", "sleeping", "sleeps", (name) => `${name} curls up and falls asleep... Zzz`);
44
44
  (0, registry_1.registerCommand)({
45
45
  name: "heal",
46
- description: "Heal your buddy to restore health (instant)",
46
+ description: "Heal Health +20, +3 XP (instant)",
47
47
  execute: (state) => {
48
48
  if (!state.alive) {
49
49
  return { state: null, message: `${state.name} is no longer with us... Use /revive first.`, rerender: false };
@@ -66,21 +66,7 @@ registerActivityCommand("sleep", "Let your buddy rest to restore energy", "sleep
66
66
  if (state.alive) {
67
67
  return { state: null, message: `${state.name} is alive and well!`, rerender: false };
68
68
  }
69
- const counts = { ...state.actionCounts, revives: state.actionCounts.revives + 1 };
70
- const newState = {
71
- ...state,
72
- alive: true,
73
- xp: Math.floor(state.xp / 2),
74
- stats: { hunger: 50, happiness: 50, health: 50, energy: 50 },
75
- lastUpdated: Date.now(),
76
- activity: undefined,
77
- actionCounts: counts,
78
- };
79
- return {
80
- state: newState,
81
- message: `${state.name} stirs back to life! They lost half their XP but they're back.`,
82
- rerender: true,
83
- };
69
+ return { state: null, message: "__CONFIRM_REVIVE__", rerender: false };
84
70
  },
85
71
  });
86
72
  (0, registry_1.registerCommand)({
@@ -100,7 +86,7 @@ registerActivityCommand("sleep", "Let your buddy rest to restore energy", "sleep
100
86
  name: "help",
101
87
  description: "Show all available commands",
102
88
  execute: () => {
103
- return { state: null, message: "Press / to open the command palette!", rerender: false };
89
+ return { state: null, message: "Quick: [f]eed [p]lay [s]leep or press / for all commands", rerender: false };
104
90
  },
105
91
  });
106
92
  (0, registry_1.registerCommand)({