clibuddy 1.1.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 +78 -43
- package/dist/adventure/combat/combatUI.js +66 -31
- package/dist/index.js +2 -2
- package/dist/rendering/display.js +86 -24
- 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 +8 -0
- package/dist/rendering/screen.js +19 -1
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -188,54 +191,76 @@ function renderEndlessTab(lines, state) {
|
|
|
188
191
|
lines.push(` ${screen_1.ansi.dim}Enter to begin your descent${screen_1.ansi.reset}`);
|
|
189
192
|
}
|
|
190
193
|
// ─── Adventure Room Rendering ────────────────────────────────
|
|
191
|
-
function renderRoom(advState, adventure,
|
|
192
|
-
const room = (0,
|
|
194
|
+
function renderRoom(advState, adventure, state) {
|
|
195
|
+
const room = (0, types_2.getRoom)(adventure, advState.currentRoomId);
|
|
193
196
|
if (!room)
|
|
194
197
|
return " Room not found!";
|
|
195
|
-
const progress = (0,
|
|
198
|
+
const progress = (0, types_2.getRoomProgress)(advState);
|
|
196
199
|
const lines = [];
|
|
200
|
+
// ── Header with box-drawn frame ──
|
|
197
201
|
lines.push("");
|
|
198
202
|
if (adventure.isScene || adventure.hideRoomCounter) {
|
|
199
|
-
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}`);
|
|
200
204
|
}
|
|
201
205
|
else {
|
|
202
|
-
|
|
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}`);
|
|
203
212
|
}
|
|
204
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(44)}${screen_1.ansi.reset}`);
|
|
205
213
|
lines.push("");
|
|
206
214
|
lines.push(` ${screen_1.ansi.bold}${room.title}${screen_1.ansi.reset}`);
|
|
207
215
|
lines.push("");
|
|
208
|
-
//
|
|
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") : [];
|
|
209
220
|
if (room.npcsPresent && room.npcsPresent.length > 0) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
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}`);
|
|
218
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("");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
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}`);
|
|
219
239
|
}
|
|
240
|
+
lines.push("");
|
|
220
241
|
}
|
|
221
|
-
//
|
|
242
|
+
// ── Sliding window text — show only current page ──
|
|
222
243
|
const LINES_PER_PAGE = 3;
|
|
223
244
|
const totalPages = Math.ceil(room.text.length / LINES_PER_PAGE);
|
|
224
245
|
const currentPage = advState.textPage;
|
|
225
246
|
const textFullyShown = advState.textFullyShown || currentPage >= totalPages - 1;
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
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++) {
|
|
229
255
|
lines.push(` ${room.text[i]}`);
|
|
230
256
|
}
|
|
231
257
|
lines.push("");
|
|
232
258
|
if (!textFullyShown) {
|
|
233
|
-
|
|
234
|
-
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}`);
|
|
235
260
|
}
|
|
236
261
|
else {
|
|
237
|
-
// All text shown —
|
|
238
|
-
lines.push(` ${
|
|
262
|
+
// All text shown — buddy reaction + choices
|
|
263
|
+
lines.push(` ${buddyColor}${state.name}:${screen_1.ansi.reset} "${advState.buddyReaction}"`);
|
|
239
264
|
lines.push("");
|
|
240
265
|
if (room.type === "narrative" && room.choices) {
|
|
241
266
|
for (let i = 0; i < room.choices.length; i++) {
|
|
@@ -243,7 +268,6 @@ function renderRoom(advState, adventure, buddyName) {
|
|
|
243
268
|
}
|
|
244
269
|
}
|
|
245
270
|
else if (room.type === "event" && room.eventChoices) {
|
|
246
|
-
// Check if event was already resolved
|
|
247
271
|
const alreadyResolved = room.eventChoices.some((c) => c.outcome === advState.buddyReaction);
|
|
248
272
|
if (alreadyResolved) {
|
|
249
273
|
lines.push(` ${screen_1.ansi.dim}Press Enter to continue...${screen_1.ansi.reset}`);
|
|
@@ -269,26 +293,37 @@ function renderRoom(advState, adventure, buddyName) {
|
|
|
269
293
|
return lines.join("\n");
|
|
270
294
|
}
|
|
271
295
|
// ─── Result Screen ───────────────────────────────────────────
|
|
272
|
-
function renderResult(advState, adventure,
|
|
296
|
+
function renderResult(advState, adventure, state) {
|
|
273
297
|
const lines = [];
|
|
298
|
+
const w = screen_1.UI_WIDTH;
|
|
274
299
|
lines.push("");
|
|
275
|
-
lines.push(` ${screen_1.ansi.
|
|
276
|
-
lines.push(` ${screen_1.ansi.
|
|
277
|
-
lines.push("");
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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));
|
|
281
310
|
if (advState.lootCollected.length > 0) {
|
|
282
311
|
const lootNames = advState.lootCollected.map((id) => id.replace(/_/g, " "));
|
|
283
|
-
|
|
312
|
+
const lootText = `Loot: ${lootNames.join(", ")}`;
|
|
313
|
+
lines.push(padLine(lootText, lootText.length));
|
|
284
314
|
}
|
|
285
|
-
|
|
286
|
-
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));
|
|
287
318
|
if (advState.damageTaken === 0 && advState.battlesWon > 0) {
|
|
288
|
-
|
|
319
|
+
const perfText = "★ PERFECT — No damage taken!";
|
|
320
|
+
lines.push(padLine(`${screen_1.ansi.colors.yellow}${perfText}${screen_1.ansi.reset}`, perfText.length));
|
|
289
321
|
}
|
|
322
|
+
lines.push(` ${screen_1.ansi.colors.green}╚${"═".repeat(w)}╝${screen_1.ansi.reset}`);
|
|
290
323
|
lines.push("");
|
|
291
|
-
|
|
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}"`);
|
|
292
327
|
lines.push("");
|
|
293
328
|
lines.push(` ${screen_1.ansi.dim}Press Enter to return...${screen_1.ansi.reset}`);
|
|
294
329
|
lines.push("");
|
|
@@ -11,6 +11,7 @@ const lines_1 = require("../../dialogue/lines");
|
|
|
11
11
|
const skills_1 = require("./skills");
|
|
12
12
|
const items_1 = require("../../inventory/items");
|
|
13
13
|
const screen_1 = require("../../rendering/screen");
|
|
14
|
+
const scene_1 = require("../../rendering/scene");
|
|
14
15
|
const CONDITION_ICONS = {
|
|
15
16
|
burn: "🔥", poison: "☠", freeze: "❄", stun: "⚡", charm: "💕",
|
|
16
17
|
blind: "👁", regen: "💚", shield: "🛡", buff: "↑", debuff: "↓",
|
|
@@ -37,41 +38,76 @@ function hpBar(current, max, width = 15) {
|
|
|
37
38
|
function renderCombat(combat, state, adventureName) {
|
|
38
39
|
const species = (0, species_1.getSpecies)(state.speciesId);
|
|
39
40
|
const lines = [];
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
lines.push("");
|
|
44
|
-
// 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] ?? "";
|
|
45
44
|
const enemyElemIcon = types_2.ELEMENT_ICONS[combat.enemyElement] ?? "";
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
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
|
|
51
54
|
lines.push("");
|
|
52
|
-
|
|
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}`);
|
|
53
58
|
lines.push("");
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|
|
63
99
|
}
|
|
64
100
|
}
|
|
65
101
|
lines.push("");
|
|
66
|
-
// Combat log (last 2 messages)
|
|
102
|
+
// ── Combat log (last 2 messages) ──
|
|
67
103
|
const recentLog = combat.log.slice(-2);
|
|
68
104
|
for (const msg of recentLog) {
|
|
69
105
|
lines.push(` ${screen_1.ansi.dim}${msg}${screen_1.ansi.reset}`);
|
|
70
106
|
}
|
|
71
107
|
lines.push("");
|
|
72
|
-
// Action menu
|
|
108
|
+
// ── Action menu / result ──
|
|
73
109
|
if (combat.phase === "player_turn") {
|
|
74
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
110
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
75
111
|
lines.push(` [1] Attack [2] Skill [3] Item`);
|
|
76
112
|
lines.push(` [4] Defend [5] Flee`);
|
|
77
113
|
lines.push(` ${screen_1.ansi.dim}Energy: ${"◆".repeat(combat.combatEnergy)}${"◇".repeat(combat.maxCombatEnergy - combat.combatEnergy)}${screen_1.ansi.reset}`);
|
|
@@ -79,15 +115,15 @@ function renderCombat(combat, state, adventureName) {
|
|
|
79
115
|
else if (combat.phase === "victory") {
|
|
80
116
|
const victoryLines = lines_1.COMBAT_VICTORY_LINES[state.speciesId];
|
|
81
117
|
const victoryLine = victoryLines ? victoryLines[Math.floor(Math.random() * victoryLines.length)] : "We did it!";
|
|
82
|
-
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}`);
|
|
83
|
-
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}"`);
|
|
84
120
|
lines.push(` ${screen_1.ansi.dim}Press Enter to continue...${screen_1.ansi.reset}`);
|
|
85
121
|
}
|
|
86
122
|
else if (combat.phase === "defeat") {
|
|
87
123
|
const defeatLines = lines_1.COMBAT_DEFEAT_LINES[state.speciesId];
|
|
88
124
|
const defeatLine = defeatLines ? defeatLines[Math.floor(Math.random() * defeatLines.length)] : "We'll get them next time...";
|
|
89
125
|
lines.push(` ${screen_1.ansi.colors.red}${screen_1.ansi.bold}Defeated!${screen_1.ansi.reset}`);
|
|
90
|
-
lines.push(` ${state.name}
|
|
126
|
+
lines.push(` ${speciesColor}${state.name}:${screen_1.ansi.reset} "${defeatLine}"`);
|
|
91
127
|
lines.push(` ${screen_1.ansi.dim}Press Enter to retreat...${screen_1.ansi.reset}`);
|
|
92
128
|
}
|
|
93
129
|
else if (combat.phase === "fled") {
|
|
@@ -95,7 +131,6 @@ function renderCombat(combat, state, adventureName) {
|
|
|
95
131
|
lines.push(` ${screen_1.ansi.dim}Press Enter to continue...${screen_1.ansi.reset}`);
|
|
96
132
|
}
|
|
97
133
|
else {
|
|
98
|
-
// Animating
|
|
99
134
|
lines.push(` ${screen_1.ansi.dim}...${screen_1.ansi.reset}`);
|
|
100
135
|
}
|
|
101
136
|
lines.push("");
|
|
@@ -104,7 +139,7 @@ function renderCombat(combat, state, adventureName) {
|
|
|
104
139
|
function renderSkillMenu(combat, state) {
|
|
105
140
|
const skills = (0, skills_1.getAvailableSkills)(state.speciesId, state.level);
|
|
106
141
|
const lines = [];
|
|
107
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
142
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
108
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}`);
|
|
109
144
|
for (let i = 0; i < skills.length; i++) {
|
|
110
145
|
const s = skills[i];
|
|
@@ -113,7 +148,7 @@ function renderSkillMenu(combat, state) {
|
|
|
113
148
|
lines.push(` ${color}[${i + 1}] ${s.name} (${s.energyCost}◆) — ${s.description}${screen_1.ansi.reset}`);
|
|
114
149
|
}
|
|
115
150
|
lines.push(` ${screen_1.ansi.dim}[0] Back${screen_1.ansi.reset}`);
|
|
116
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
151
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
117
152
|
return lines.join("\n");
|
|
118
153
|
}
|
|
119
154
|
/** Get inventory slots usable in combat (medicine items with health effect) */
|
|
@@ -135,7 +170,7 @@ function getCombatItems(inventory) {
|
|
|
135
170
|
function renderItemMenu(combat, inventory, selectedIndex) {
|
|
136
171
|
const items = getCombatItems(inventory);
|
|
137
172
|
const lines = [];
|
|
138
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
173
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
139
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}`);
|
|
140
175
|
if (items.length === 0) {
|
|
141
176
|
lines.push(` ${screen_1.ansi.dim} No medicine in inventory${screen_1.ansi.reset}`);
|
|
@@ -155,7 +190,7 @@ function renderItemMenu(combat, inventory, selectedIndex) {
|
|
|
155
190
|
}
|
|
156
191
|
}
|
|
157
192
|
}
|
|
158
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
193
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
159
194
|
lines.push(` ${screen_1.ansi.dim}Enter to use, Esc to close${screen_1.ansi.reset}`);
|
|
160
195
|
return lines.join("\n");
|
|
161
196
|
}
|
package/dist/index.js
CHANGED
|
@@ -137,10 +137,10 @@ function redraw() {
|
|
|
137
137
|
screen.draw(screen_, ` ${screen_1.ansi.dim}Esc to flee${screen_1.ansi.reset}`);
|
|
138
138
|
}
|
|
139
139
|
else if (activeAdventure.phase === "result") {
|
|
140
|
-
screen.draw((0, adventureUI_1.renderResult)(activeAdventure, adv, buddyState
|
|
140
|
+
screen.draw((0, adventureUI_1.renderResult)(activeAdventure, adv, buddyState), ` ${screen_1.ansi.dim}Press Enter${screen_1.ansi.reset}`);
|
|
141
141
|
}
|
|
142
142
|
else {
|
|
143
|
-
screen.draw((0, adventureUI_1.renderRoom)(activeAdventure, adv, buddyState
|
|
143
|
+
screen.draw((0, adventureUI_1.renderRoom)(activeAdventure, adv, buddyState), ` ${screen_1.ansi.dim}Esc to retreat${screen_1.ansi.reset}`);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
return;
|
|
@@ -11,18 +11,26 @@ const items_1 = require("./items");
|
|
|
11
11
|
const scene_1 = require("./scene");
|
|
12
12
|
const screen_1 = require("./screen");
|
|
13
13
|
const updates_1 = require("../updates");
|
|
14
|
+
/** Per-stat identity colors */
|
|
15
|
+
const STAT_COLORS = {
|
|
16
|
+
Hunger: screen_1.ansi.colors.green,
|
|
17
|
+
Happiness: screen_1.ansi.colors.magenta,
|
|
18
|
+
Health: screen_1.ansi.colors.cyan,
|
|
19
|
+
Energy: screen_1.ansi.colors.blue,
|
|
20
|
+
};
|
|
14
21
|
function statBar(value, label, width = 20) {
|
|
15
22
|
const filled = Math.round((value / 100) * width);
|
|
16
23
|
const empty = width - filled;
|
|
24
|
+
// Use per-stat color when healthy, override to yellow/red when low
|
|
17
25
|
let color;
|
|
18
|
-
if (value
|
|
19
|
-
color = screen_1.ansi.colors.
|
|
20
|
-
else if (value
|
|
26
|
+
if (value <= 30)
|
|
27
|
+
color = screen_1.ansi.colors.red;
|
|
28
|
+
else if (value <= 60)
|
|
21
29
|
color = screen_1.ansi.colors.yellow;
|
|
22
30
|
else
|
|
23
|
-
color = screen_1.ansi.colors.
|
|
24
|
-
const bar = color + "█".repeat(filled) + screen_1.ansi.
|
|
25
|
-
return
|
|
31
|
+
color = STAT_COLORS[label] ?? screen_1.ansi.colors.green;
|
|
32
|
+
const bar = color + "█".repeat(filled) + screen_1.ansi.colors.gray + "░".repeat(empty) + screen_1.ansi.reset;
|
|
33
|
+
return `${label.padEnd(12)} ${bar} ${color}${Math.round(value).toString().padStart(3)}%${screen_1.ansi.reset}`;
|
|
26
34
|
}
|
|
27
35
|
/**
|
|
28
36
|
* Renders the full main screen content (buddy + stats + mood).
|
|
@@ -38,7 +46,7 @@ function renderMainScreen(state, frame, message, dialogue, dialogueSource) {
|
|
|
38
46
|
// Header bar
|
|
39
47
|
lines.push("");
|
|
40
48
|
lines.push(` ${screen_1.ansi.bold}CLI Buddy${screen_1.ansi.reset} ${screen_1.ansi.dim}v${updates_1.CURRENT_VERSION}${screen_1.ansi.reset}`);
|
|
41
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
49
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
42
50
|
if (!state.alive) {
|
|
43
51
|
lines.push("");
|
|
44
52
|
lines.push(` ${screen_1.ansi.bold}${screen_1.ansi.colors.red}${state.name} has passed away...${screen_1.ansi.reset}`);
|
|
@@ -89,33 +97,87 @@ function renderMainScreen(state, frame, message, dialogue, dialogueSource) {
|
|
|
89
97
|
// Compose scene with item
|
|
90
98
|
const item = items_1.ACTIVITY_ITEMS[activityType] ?? null;
|
|
91
99
|
const sceneLines = (0, scene_1.composeScene)(buddyArtLines, item, activityType === "idle");
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
// Activity timer
|
|
100
|
+
// Activity timer line
|
|
101
|
+
let timerLine = "";
|
|
96
102
|
if (state.activity && state.activity.type !== "idle") {
|
|
97
103
|
const timerLabel = state.activity.type === "sleeping" ? "Sleeping" :
|
|
98
104
|
state.activity.type === "eating" ? "Eating" : "Playing";
|
|
99
105
|
const isRecovery = state.activity.unskippable;
|
|
100
106
|
const label2 = isRecovery ? "Recovering" : timerLabel;
|
|
101
107
|
const skipHint = isRecovery ? "" : " (press any key to skip)";
|
|
102
|
-
|
|
108
|
+
timerLine = `${screen_1.ansi.dim}${label2}... ${(0, activities_1.formatTimeRemaining)(state.activity)} remaining${skipHint}${screen_1.ansi.reset}`;
|
|
103
109
|
}
|
|
104
|
-
lines
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
110
|
+
// Build stat lines
|
|
111
|
+
const statLines = [
|
|
112
|
+
statBar(state.stats.hunger, "Hunger"),
|
|
113
|
+
statBar(state.stats.happiness, "Happiness"),
|
|
114
|
+
statBar(state.stats.health, "Health"),
|
|
115
|
+
statBar(state.stats.energy, "Energy"),
|
|
116
|
+
];
|
|
117
|
+
// Build mood/dialogue line
|
|
118
|
+
let moodLine;
|
|
113
119
|
if (dialogue) {
|
|
114
120
|
const sourceTag = dialogueSource ? `${screen_1.ansi.dim}[${dialogueSource}]${screen_1.ansi.reset} ` : "";
|
|
115
|
-
|
|
121
|
+
moodLine = `${sourceTag}${screen_1.ansi.colors.cyan}"${dialogue}"${screen_1.ansi.reset}`;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
moodLine = `${screen_1.ansi.dim}${getMoodMessage(state)}${screen_1.ansi.reset}`;
|
|
125
|
+
}
|
|
126
|
+
// Side-by-side layout: buddy left, stats right (if terminal wide enough)
|
|
127
|
+
const termWidth = process.stdout.columns || 80;
|
|
128
|
+
const minWidthForSideBySide = 65;
|
|
129
|
+
// Trim trailing whitespace from scene lines so the left column is tight
|
|
130
|
+
const trimmedSceneLines = sceneLines.map((l) => l.trimEnd());
|
|
131
|
+
// Measure the actual widest scene line for dynamic left column width
|
|
132
|
+
const leftColumnWidth = Math.max(...trimmedSceneLines.map((l) => (0, screen_1.stripAnsi)(l).length), 12) + 2;
|
|
133
|
+
if (termWidth >= minWidthForSideBySide) {
|
|
134
|
+
// Build left column: buddy art only
|
|
135
|
+
const leftLines = [];
|
|
136
|
+
for (const line of trimmedSceneLines) {
|
|
137
|
+
leftLines.push(`${color}${line}${screen_1.ansi.reset}`);
|
|
138
|
+
}
|
|
139
|
+
// Build right column: stats only
|
|
140
|
+
const rightLines = [];
|
|
141
|
+
// Vertical offset to center stats against art
|
|
142
|
+
const artHeight = leftLines.length;
|
|
143
|
+
const statsHeight = statLines.length;
|
|
144
|
+
const topPad = Math.max(0, Math.floor((artHeight - statsHeight) / 2));
|
|
145
|
+
for (let i = 0; i < topPad; i++)
|
|
146
|
+
rightLines.push("");
|
|
147
|
+
for (const sl of statLines) {
|
|
148
|
+
rightLines.push(sl);
|
|
149
|
+
}
|
|
150
|
+
// Merge columns side by side
|
|
151
|
+
const maxRows = Math.max(leftLines.length, rightLines.length);
|
|
152
|
+
for (let i = 0; i < maxRows; i++) {
|
|
153
|
+
const left = leftLines[i] ?? "";
|
|
154
|
+
const right = rightLines[i] ?? "";
|
|
155
|
+
// Pad left column to fixed width (strip ANSI for length calc)
|
|
156
|
+
const leftVisible = (0, screen_1.stripAnsi)(left);
|
|
157
|
+
const pad = Math.max(0, leftColumnWidth - leftVisible.length);
|
|
158
|
+
lines.push(` ${left}${" ".repeat(pad)}${right}`);
|
|
159
|
+
}
|
|
160
|
+
// Timer and mood below the side-by-side section
|
|
161
|
+
if (timerLine) {
|
|
162
|
+
lines.push(` ${timerLine}`);
|
|
163
|
+
}
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push(` ${moodLine}`);
|
|
116
166
|
}
|
|
117
167
|
else {
|
|
118
|
-
|
|
168
|
+
// Narrow terminal: vertical stack (original layout)
|
|
169
|
+
for (const line of sceneLines) {
|
|
170
|
+
lines.push(` ${color}${line}${screen_1.ansi.reset}`);
|
|
171
|
+
}
|
|
172
|
+
if (timerLine) {
|
|
173
|
+
lines.push(` ${timerLine}`);
|
|
174
|
+
}
|
|
175
|
+
lines.push("");
|
|
176
|
+
for (const sl of statLines) {
|
|
177
|
+
lines.push(` ${sl}`);
|
|
178
|
+
}
|
|
179
|
+
lines.push("");
|
|
180
|
+
lines.push(` ${moodLine}`);
|
|
119
181
|
}
|
|
120
182
|
// Action feedback message (fades after a few seconds)
|
|
121
183
|
if (message) {
|
|
@@ -137,7 +199,7 @@ function renderRollScreen(phase, speciesName, speciesArt, rarityColor, rarityLab
|
|
|
137
199
|
const lines = [];
|
|
138
200
|
lines.push("");
|
|
139
201
|
lines.push(` ${screen_1.ansi.bold}CLI Buddy${screen_1.ansi.reset} ${screen_1.ansi.dim}v${updates_1.CURRENT_VERSION}${screen_1.ansi.reset}`);
|
|
140
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
202
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
141
203
|
lines.push("");
|
|
142
204
|
if (phase === "waiting") {
|
|
143
205
|
lines.push(` ${screen_1.ansi.dim}Welcome! You don't have a buddy yet.${screen_1.ansi.reset}`);
|
|
@@ -40,9 +40,9 @@ function formatTimeAgo(timestamp) {
|
|
|
40
40
|
}
|
|
41
41
|
function renderTitlesOverlay(state, overlay) {
|
|
42
42
|
const lines = [];
|
|
43
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
43
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
44
44
|
lines.push(` ${screen_1.ansi.bold}Titles${screen_1.ansi.reset} ${screen_1.ansi.dim}(${state.titles.length}/${titles_1.TITLE_RULES.length} earned) Esc to close${screen_1.ansi.reset}`);
|
|
45
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
45
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
46
46
|
for (let i = 0; i < titles_1.TITLE_RULES.length; i++) {
|
|
47
47
|
const rule = titles_1.TITLE_RULES[i];
|
|
48
48
|
const earned = state.titles.includes(rule.id);
|
|
@@ -57,15 +57,15 @@ function renderTitlesOverlay(state, overlay) {
|
|
|
57
57
|
lines.push(` ${pointer} ${screen_1.ansi.dim}✗ ??? — ${rule.hint}${screen_1.ansi.reset}`);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
60
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
61
61
|
lines.push(` ${screen_1.ansi.dim}Enter to equip selected title${screen_1.ansi.reset}`);
|
|
62
62
|
return lines;
|
|
63
63
|
}
|
|
64
64
|
function renderNotificationsOverlay(notifications, overlay) {
|
|
65
65
|
const lines = [];
|
|
66
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
66
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
67
67
|
lines.push(` ${screen_1.ansi.bold}Notifications${screen_1.ansi.reset} ${screen_1.ansi.dim}(${notifications.length}) Esc to close${screen_1.ansi.reset}`);
|
|
68
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
68
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
69
69
|
if (notifications.length === 0) {
|
|
70
70
|
lines.push(` ${screen_1.ansi.dim} No notifications.${screen_1.ansi.reset}`);
|
|
71
71
|
}
|
|
@@ -81,7 +81,7 @@ function renderNotificationsOverlay(notifications, overlay) {
|
|
|
81
81
|
lines.push(` ${screen_1.ansi.dim}${timeAgo}${screen_1.ansi.reset}`);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
84
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
85
85
|
lines.push(` ${screen_1.ansi.dim}Enter to clear all${screen_1.ansi.reset}`);
|
|
86
86
|
return lines;
|
|
87
87
|
}
|
|
@@ -93,9 +93,9 @@ function getSettingEntries() {
|
|
|
93
93
|
}
|
|
94
94
|
function renderSettingsOverlay(settings, overlay) {
|
|
95
95
|
const lines = [];
|
|
96
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
96
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
97
97
|
lines.push(` ${screen_1.ansi.bold}Settings${screen_1.ansi.reset} ${screen_1.ansi.dim}Enter to toggle, Esc to close${screen_1.ansi.reset}`);
|
|
98
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
98
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
99
99
|
for (let i = 0; i < SETTING_ENTRIES.length; i++) {
|
|
100
100
|
const entry = SETTING_ENTRIES[i];
|
|
101
101
|
const value = settings[entry.key];
|
|
@@ -106,7 +106,7 @@ function renderSettingsOverlay(settings, overlay) {
|
|
|
106
106
|
: `${screen_1.ansi.colors.red}[OFF]${screen_1.ansi.reset}`;
|
|
107
107
|
lines.push(` ${pointer} ${toggle} ${screen_1.ansi.bold}${entry.name}${screen_1.ansi.reset} ${screen_1.ansi.dim}${entry.description}${screen_1.ansi.reset}`);
|
|
108
108
|
}
|
|
109
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
109
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
110
110
|
return lines;
|
|
111
111
|
}
|
|
112
112
|
function renderGearOverlay(state) {
|
|
@@ -117,9 +117,9 @@ function renderGearOverlay(state) {
|
|
|
117
117
|
const boots = s.equippedBoots ? (0, items_1.getItem)(s.equippedBoots) : null;
|
|
118
118
|
const accessory = s.equippedAccessory ? (0, items_1.getItem)(s.equippedAccessory) : null;
|
|
119
119
|
const lines = [];
|
|
120
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
120
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
121
121
|
lines.push(` ${screen_1.ansi.bold}Equipped Gear${screen_1.ansi.reset} ${screen_1.ansi.dim}Esc to close${screen_1.ansi.reset}`);
|
|
122
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
122
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
123
123
|
lines.push(` ⚔ Weapon: ${weapon ? `${weapon.name} (+${weapon.combatATK} ATK)${weapon.gearElement ? ` [${weapon.gearElement}]` : ""}` : `${screen_1.ansi.dim}None${screen_1.ansi.reset}`}`);
|
|
124
124
|
if (weapon?.onHitEffect)
|
|
125
125
|
lines.push(` ${screen_1.ansi.dim}${Math.round((weapon.onHitChance ?? 0) * 100)}% ${weapon.onHitEffect} on hit${screen_1.ansi.reset}`);
|
|
@@ -129,7 +129,7 @@ function renderGearOverlay(state) {
|
|
|
129
129
|
lines.push(` 🪖 Helmet: ${helmet ? `${helmet.name} (${helmet.description})` : `${screen_1.ansi.dim}None${screen_1.ansi.reset}`}`);
|
|
130
130
|
lines.push(` 👢 Boots: ${boots ? `${boots.name} (${boots.description})` : `${screen_1.ansi.dim}None${screen_1.ansi.reset}`}`);
|
|
131
131
|
lines.push(` 💍 Acc: ${accessory ? `${accessory.name} (${accessory.description})` : `${screen_1.ansi.dim}None${screen_1.ansi.reset}`}`);
|
|
132
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
132
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
133
133
|
lines.push(` ${screen_1.ansi.dim}Equip gear from /inventory${screen_1.ansi.reset}`);
|
|
134
134
|
return lines;
|
|
135
135
|
}
|
|
@@ -139,9 +139,9 @@ function renderCombatInfoOverlay(state) {
|
|
|
139
139
|
const weapon = equippedWeapon ? (0, items_1.getItem)(equippedWeapon) : null;
|
|
140
140
|
const armor = equippedArmor ? (0, items_1.getItem)(equippedArmor) : null;
|
|
141
141
|
const lines = [];
|
|
142
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
142
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
143
143
|
lines.push(` ${screen_1.ansi.bold}Combat Stats${screen_1.ansi.reset} ${screen_1.ansi.dim}Esc to close${screen_1.ansi.reset}`);
|
|
144
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
144
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
145
145
|
lines.push(` HP: ${screen_1.ansi.colors.green}${stats.maxHp}${screen_1.ansi.reset}`);
|
|
146
146
|
lines.push(` ATK: ${screen_1.ansi.colors.red}${stats.atk}${screen_1.ansi.reset}${weapon ? ` (base + ${weapon.combatATK} from ${weapon.name})` : ""}`);
|
|
147
147
|
lines.push(` DEF: ${screen_1.ansi.colors.blue}${stats.def}${screen_1.ansi.reset}${armor ? ` (base + ${armor.combatDEF} from ${armor.name})` : ""}`);
|
|
@@ -150,15 +150,15 @@ function renderCombatInfoOverlay(state) {
|
|
|
150
150
|
lines.push("");
|
|
151
151
|
lines.push(` ${screen_1.ansi.dim}Adventures: ${state.adventureStats.adventuresCompleted} | Battles won: ${state.adventureStats.battlesWon}${screen_1.ansi.reset}`);
|
|
152
152
|
lines.push(` ${screen_1.ansi.dim}Bosses defeated: ${state.adventureStats.bossesDefeated} | Perfect runs: ${state.adventureStats.perfectAdventures}${screen_1.ansi.reset}`);
|
|
153
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
153
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
154
154
|
return lines;
|
|
155
155
|
}
|
|
156
156
|
function renderShopOverlay(state, overlay) {
|
|
157
157
|
const lines = [];
|
|
158
158
|
const gold = state.questProgress.gold;
|
|
159
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
159
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
160
160
|
lines.push(` ${screen_1.ansi.bold}Pip's Shop${screen_1.ansi.reset} ${screen_1.ansi.colors.yellow}Gold: ${gold}${screen_1.ansi.reset} ${screen_1.ansi.dim}Esc to close${screen_1.ansi.reset}`);
|
|
161
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
161
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
162
162
|
for (let i = 0; i < shop_1.SHOP_ITEMS.length; i++) {
|
|
163
163
|
const shopItem = shop_1.SHOP_ITEMS[i];
|
|
164
164
|
const item = (0, items_1.getItem)(shopItem.itemId);
|
|
@@ -177,7 +177,7 @@ function renderShopOverlay(state, overlay) {
|
|
|
177
177
|
lines.push(` ${pointer} ${screen_1.ansi.dim}${item.name} ${shopItem.price}g${screen_1.ansi.reset}`);
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
-
lines.push(` ${screen_1.ansi.dim}${"─".repeat(
|
|
180
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(screen_1.UI_WIDTH)}${screen_1.ansi.reset}`);
|
|
181
181
|
lines.push(` ${screen_1.ansi.dim}Enter to buy${screen_1.ansi.reset}`);
|
|
182
182
|
return lines;
|
|
183
183
|
}
|
|
@@ -5,4 +5,10 @@ import { SceneItem } from "./items";
|
|
|
5
5
|
* Returns an array of lines ready to render.
|
|
6
6
|
*/
|
|
7
7
|
export declare function composeScene(buddyArt: string[], item: SceneItem | null, groundLine?: boolean): string[];
|
|
8
|
+
/**
|
|
9
|
+
* Compose two ASCII art blocks side-by-side with colors.
|
|
10
|
+
* Bottom-aligns the shorter art. Returns array of composed lines.
|
|
11
|
+
* Used for buddy+NPC in adventure rooms and buddy+enemy in combat.
|
|
12
|
+
*/
|
|
13
|
+
export declare function composeDualArt(leftArt: string[], rightArt: string[], leftColor: string, rightColor: string, gap?: number, reset?: string): string[];
|
|
8
14
|
//# sourceMappingURL=scene.d.ts.map
|
package/dist/rendering/scene.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.composeScene = composeScene;
|
|
4
|
+
exports.composeDualArt = composeDualArt;
|
|
4
5
|
const SCENE_WIDTH = 50;
|
|
5
6
|
/**
|
|
6
7
|
* Compose a scene by placing buddy art and an optional item
|
|
@@ -84,4 +85,23 @@ function centerPad(str, targetWidth) {
|
|
|
84
85
|
const leftPad = Math.floor((targetWidth - len) / 2);
|
|
85
86
|
return " ".repeat(leftPad) + str;
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Compose two ASCII art blocks side-by-side with colors.
|
|
90
|
+
* Bottom-aligns the shorter art. Returns array of composed lines.
|
|
91
|
+
* Used for buddy+NPC in adventure rooms and buddy+enemy in combat.
|
|
92
|
+
*/
|
|
93
|
+
function composeDualArt(leftArt, rightArt, leftColor, rightColor, gap = 6, reset = "\x1b[0m") {
|
|
94
|
+
const maxHeight = Math.max(leftArt.length, rightArt.length);
|
|
95
|
+
const paddedLeft = topPad(leftArt, maxHeight);
|
|
96
|
+
const paddedRight = topPad(rightArt, maxHeight);
|
|
97
|
+
const leftWidth = Math.max(...leftArt.map(visibleLength));
|
|
98
|
+
const gapStr = " ".repeat(gap);
|
|
99
|
+
const lines = [];
|
|
100
|
+
for (let i = 0; i < maxHeight; i++) {
|
|
101
|
+
const lLine = rightPad(paddedLeft[i], leftWidth);
|
|
102
|
+
const rLine = paddedRight[i];
|
|
103
|
+
lines.push(`${leftColor}${lLine}${reset}${gapStr}${rightColor}${rLine}${reset}`);
|
|
104
|
+
}
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
87
107
|
//# sourceMappingURL=scene.js.map
|
|
@@ -26,6 +26,14 @@ export declare const ansi: {
|
|
|
26
26
|
altScreenOn: string;
|
|
27
27
|
altScreenOff: string;
|
|
28
28
|
};
|
|
29
|
+
/** Standard content width for dividers and framing */
|
|
30
|
+
export declare const UI_WIDTH = 48;
|
|
31
|
+
/** Strip ANSI escape codes for visible length calculation */
|
|
32
|
+
export declare function stripAnsi(s: string): string;
|
|
33
|
+
/** Visible length of a string (excluding ANSI codes) */
|
|
34
|
+
export declare function visibleLen(s: string): number;
|
|
35
|
+
/** Right-pad a string with spaces to target visible width */
|
|
36
|
+
export declare function visPad(s: string, targetWidth: number): string;
|
|
29
37
|
export declare class Screen {
|
|
30
38
|
private regions;
|
|
31
39
|
private footer;
|
package/dist/rendering/screen.js
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* Redraws by moving to home and overwriting — no clear, no flicker.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.Screen = exports.ansi = void 0;
|
|
8
|
+
exports.Screen = exports.UI_WIDTH = exports.ansi = void 0;
|
|
9
|
+
exports.stripAnsi = stripAnsi;
|
|
10
|
+
exports.visibleLen = visibleLen;
|
|
11
|
+
exports.visPad = visPad;
|
|
9
12
|
const ESC = "\x1b";
|
|
10
13
|
exports.ansi = {
|
|
11
14
|
clearScreen: `${ESC}[2J${ESC}[H`,
|
|
@@ -31,6 +34,21 @@ exports.ansi = {
|
|
|
31
34
|
altScreenOn: `${ESC}[?1049h`,
|
|
32
35
|
altScreenOff: `${ESC}[?1049l`,
|
|
33
36
|
};
|
|
37
|
+
/** Standard content width for dividers and framing */
|
|
38
|
+
exports.UI_WIDTH = 48;
|
|
39
|
+
/** Strip ANSI escape codes for visible length calculation */
|
|
40
|
+
function stripAnsi(s) {
|
|
41
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
42
|
+
}
|
|
43
|
+
/** Visible length of a string (excluding ANSI codes) */
|
|
44
|
+
function visibleLen(s) {
|
|
45
|
+
return stripAnsi(s).length;
|
|
46
|
+
}
|
|
47
|
+
/** Right-pad a string with spaces to target visible width */
|
|
48
|
+
function visPad(s, targetWidth) {
|
|
49
|
+
const pad = targetWidth - visibleLen(s);
|
|
50
|
+
return pad > 0 ? s + " ".repeat(pad) : s;
|
|
51
|
+
}
|
|
34
52
|
class Screen {
|
|
35
53
|
regions = [];
|
|
36
54
|
footer = "";
|