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.
- package/dist/adventure/adventureUI.d.ts +2 -2
- package/dist/adventure/adventureUI.js +95 -53
- package/dist/adventure/combat/combatUI.d.ts +10 -1
- package/dist/adventure/combat/combatUI.js +110 -29
- package/dist/buddy/roll.js +1 -1
- package/dist/buddy/types.d.ts +1 -0
- package/dist/commands/actions.js +6 -20
- package/dist/index.js +209 -35
- package/dist/rendering/commandPalette.d.ts +6 -1
- package/dist/rendering/commandPalette.js +11 -2
- package/dist/rendering/display.js +100 -26
- package/dist/rendering/inventoryUI.d.ts +2 -2
- package/dist/rendering/inventoryUI.js +14 -1
- package/dist/rendering/overlayUI.js +18 -18
- package/dist/rendering/scene.d.ts +6 -0
- package/dist/rendering/scene.js +20 -0
- package/dist/rendering/screen.d.ts +16 -3
- package/dist/rendering/screen.js +55 -13
- package/package.json +3 -2
|
@@ -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,
|
|
23
|
-
export declare function renderResult(advState: AdventureState, adventure: AdventureDef,
|
|
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("
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
113
|
-
const blockReason = (0,
|
|
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
|
-
|
|
154
|
-
const
|
|
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
|
-
|
|
174
|
-
const
|
|
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,
|
|
185
|
-
const room = (0,
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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 —
|
|
231
|
-
lines.push(` ${
|
|
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,
|
|
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.
|
|
269
|
-
lines.push(` ${screen_1.ansi.
|
|
270
|
-
lines.push("");
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
312
|
+
const lootText = `Loot: ${lootNames.join(", ")}`;
|
|
313
|
+
lines.push(padLine(lootText, lootText.length));
|
|
277
314
|
}
|
|
278
|
-
|
|
279
|
-
lines.push(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
108
|
+
// ── Action menu / result ──
|
|
70
109
|
if (combat.phase === "player_turn") {
|
|
71
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
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}
|
|
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}
|
|
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(
|
|
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(
|
|
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
|
package/dist/buddy/roll.js
CHANGED
package/dist/buddy/types.d.ts
CHANGED
|
@@ -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>;
|
package/dist/commands/actions.js
CHANGED
|
@@ -38,12 +38,12 @@ function registerActivityCommand(name, description, activityType, countKey, mess
|
|
|
38
38
|
},
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
-
registerActivityCommand("feed", "Feed
|
|
42
|
-
registerActivityCommand("play", "Play
|
|
43
|
-
registerActivityCommand("sleep", "
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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)({
|