clibuddy 1.0.0 → 1.1.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.js +17 -10
- package/dist/adventure/combat/combatUI.d.ts +10 -1
- package/dist/adventure/combat/combatUI.js +46 -0
- 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 +207 -33
- package/dist/rendering/commandPalette.d.ts +6 -1
- package/dist/rendering/commandPalette.js +11 -2
- package/dist/rendering/display.js +14 -2
- package/dist/rendering/inventoryUI.d.ts +2 -2
- package/dist/rendering/inventoryUI.js +14 -1
- package/dist/rendering/screen.d.ts +8 -3
- package/dist/rendering/screen.js +36 -12
- package/package.json +3 -2
|
@@ -148,16 +148,20 @@ function renderExpeditionTab(lines, menu, state) {
|
|
|
148
148
|
lines.push("");
|
|
149
149
|
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
150
|
lines.push("");
|
|
151
|
+
const requiredLevel = Math.max(1, (diff - 1) * 2);
|
|
151
152
|
const energyCost = 10 + diff * 3;
|
|
152
153
|
const hungerCost = 5 + diff * 2;
|
|
153
|
-
|
|
154
|
-
const
|
|
154
|
+
const levelOk = state.level >= requiredLevel;
|
|
155
|
+
const energyOk = state.stats.energy >= energyCost;
|
|
156
|
+
const hungerOk = state.stats.hunger >= hungerCost;
|
|
157
|
+
const canGo = levelOk && energyOk && hungerOk;
|
|
158
|
+
const check = (ok) => ok ? `${screen_1.ansi.colors.green}✓${screen_1.ansi.reset}` : `${screen_1.ansi.colors.red}✗${screen_1.ansi.reset}`;
|
|
159
|
+
lines.push(` ${check(levelOk)} Level: ${state.level} ${screen_1.ansi.dim}(need ${requiredLevel}+)${screen_1.ansi.reset}`);
|
|
160
|
+
lines.push(` ${check(energyOk)} Energy: ${Math.round(state.stats.energy)} ${screen_1.ansi.dim}(costs ${energyCost})${screen_1.ansi.reset}`);
|
|
161
|
+
lines.push(` ${check(hungerOk)} Hunger: ${Math.round(state.stats.hunger)} ${screen_1.ansi.dim}(costs ${hungerCost})${screen_1.ansi.reset}`);
|
|
155
162
|
if (canGo) {
|
|
156
163
|
lines.push(` ${screen_1.ansi.colors.green}Ready to embark!${screen_1.ansi.reset}`);
|
|
157
164
|
}
|
|
158
|
-
else {
|
|
159
|
-
lines.push(` ${screen_1.ansi.colors.red}Not ready — check level/energy/hunger${screen_1.ansi.reset}`);
|
|
160
|
-
}
|
|
161
165
|
lines.push(` ${screen_1.ansi.dim}Enter to generate & start, Esc to close${screen_1.ansi.reset}`);
|
|
162
166
|
}
|
|
163
167
|
function renderEndlessTab(lines, state) {
|
|
@@ -170,14 +174,17 @@ function renderEndlessTab(lines, state) {
|
|
|
170
174
|
lines.push(` ${screen_1.ansi.dim}• Biome rotates each room${screen_1.ansi.reset}`);
|
|
171
175
|
lines.push(` ${screen_1.ansi.dim}• Defeat = run over${screen_1.ansi.reset}`);
|
|
172
176
|
lines.push("");
|
|
173
|
-
|
|
174
|
-
const
|
|
177
|
+
const levelOk = state.level >= 5;
|
|
178
|
+
const energyOk = state.stats.energy >= 20;
|
|
179
|
+
const hungerOk = state.stats.hunger >= 15;
|
|
180
|
+
const canGo = levelOk && energyOk && hungerOk;
|
|
181
|
+
const check = (ok) => ok ? `${screen_1.ansi.colors.green}✓${screen_1.ansi.reset}` : `${screen_1.ansi.colors.red}✗${screen_1.ansi.reset}`;
|
|
182
|
+
lines.push(` ${check(levelOk)} Level: ${state.level} ${screen_1.ansi.dim}(need 5+)${screen_1.ansi.reset}`);
|
|
183
|
+
lines.push(` ${check(energyOk)} Energy: ${Math.round(state.stats.energy)} ${screen_1.ansi.dim}(costs 20)${screen_1.ansi.reset}`);
|
|
184
|
+
lines.push(` ${check(hungerOk)} Hunger: ${Math.round(state.stats.hunger)} ${screen_1.ansi.dim}(costs 15)${screen_1.ansi.reset}`);
|
|
175
185
|
if (canGo) {
|
|
176
186
|
lines.push(` ${screen_1.ansi.colors.green}Ready to descend!${screen_1.ansi.reset}`);
|
|
177
187
|
}
|
|
178
|
-
else {
|
|
179
|
-
lines.push(` ${screen_1.ansi.colors.red}Not ready — check level/energy/hunger${screen_1.ansi.reset}`);
|
|
180
|
-
}
|
|
181
188
|
lines.push(` ${screen_1.ansi.dim}Enter to begin your descent${screen_1.ansi.reset}`);
|
|
182
189
|
}
|
|
183
190
|
// ─── Adventure Room Rendering ────────────────────────────────
|
|
@@ -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,11 +2,14 @@
|
|
|
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");
|
|
11
14
|
const CONDITION_ICONS = {
|
|
12
15
|
burn: "🔥", poison: "☠", freeze: "❄", stun: "⚡", charm: "💕",
|
|
@@ -113,4 +116,47 @@ function renderSkillMenu(combat, state) {
|
|
|
113
116
|
lines.push(` ${screen_1.ansi.dim}${"─".repeat(44)}${screen_1.ansi.reset}`);
|
|
114
117
|
return lines.join("\n");
|
|
115
118
|
}
|
|
119
|
+
/** Get inventory slots usable in combat (medicine items with health effect) */
|
|
120
|
+
function getCombatItems(inventory) {
|
|
121
|
+
const results = [];
|
|
122
|
+
for (const slot of inventory) {
|
|
123
|
+
if (slot.count <= 0)
|
|
124
|
+
continue;
|
|
125
|
+
const item = (0, items_1.getItem)(slot.itemId);
|
|
126
|
+
if (!item || item.type !== "medicine")
|
|
127
|
+
continue;
|
|
128
|
+
const healAmount = item.statEffect.health ?? 0;
|
|
129
|
+
if (healAmount > 0) {
|
|
130
|
+
results.push({ slot, name: item.name, healAmount, rarity: item.rarity });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
function renderItemMenu(combat, inventory, selectedIndex) {
|
|
136
|
+
const items = getCombatItems(inventory);
|
|
137
|
+
const lines = [];
|
|
138
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(44)}${screen_1.ansi.reset}`);
|
|
139
|
+
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
|
+
if (items.length === 0) {
|
|
141
|
+
lines.push(` ${screen_1.ansi.dim} No medicine in inventory${screen_1.ansi.reset}`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
for (let i = 0; i < items.length; i++) {
|
|
145
|
+
const { name, healAmount, slot, rarity } = items[i];
|
|
146
|
+
const rarityColor = items_1.ITEM_RARITY_COLORS[rarity] ?? "";
|
|
147
|
+
const hpAfter = Math.min(combat.playerStats.maxHp, combat.playerStats.hp + healAmount);
|
|
148
|
+
const selected = i === selectedIndex;
|
|
149
|
+
const pointer = selected ? `${screen_1.ansi.colors.cyan}▸${screen_1.ansi.reset}` : " ";
|
|
150
|
+
if (selected) {
|
|
151
|
+
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}`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
lines.push(` ${pointer} ${screen_1.ansi.dim}${name} x${slot.count}${screen_1.ansi.reset}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(44)}${screen_1.ansi.reset}`);
|
|
159
|
+
lines.push(` ${screen_1.ansi.dim}Enter to use, Esc to close${screen_1.ansi.reset}`);
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
|
116
162
|
//# 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)({
|
package/dist/index.js
CHANGED
|
@@ -108,6 +108,8 @@ let endlessDepth = 0;
|
|
|
108
108
|
let isEndlessMode = false;
|
|
109
109
|
let adventureMenu = (0, adventureUI_1.createAdventureMenu)();
|
|
110
110
|
let showingSkillMenu = false;
|
|
111
|
+
let showingItemMenu = false;
|
|
112
|
+
let itemMenuIndex = 0;
|
|
111
113
|
let petSequence = null;
|
|
112
114
|
let petTick = 0;
|
|
113
115
|
let palette = (0, commandPalette_1.createPalette)();
|
|
@@ -116,6 +118,7 @@ let pendingCommand = null; // command selected, waiting for args
|
|
|
116
118
|
let argsBuffer = "";
|
|
117
119
|
let appPhase = "roll-wait";
|
|
118
120
|
let nameBuffer = "";
|
|
121
|
+
let pendingConfirmation = null;
|
|
119
122
|
// ─── Rendering ───────────────────────────────────────────────
|
|
120
123
|
function redraw() {
|
|
121
124
|
if (appPhase === "playing" && buddyState) {
|
|
@@ -128,6 +131,9 @@ function redraw() {
|
|
|
128
131
|
if (showingSkillMenu) {
|
|
129
132
|
screen_ += "\n" + (0, combatUI_1.renderSkillMenu)(activeAdventure.combat, buddyState);
|
|
130
133
|
}
|
|
134
|
+
else if (showingItemMenu) {
|
|
135
|
+
screen_ += "\n" + (0, combatUI_1.renderItemMenu)(activeAdventure.combat, buddyState.inventory, itemMenuIndex);
|
|
136
|
+
}
|
|
131
137
|
screen.draw(screen_, ` ${screen_1.ansi.dim}Esc to flee${screen_1.ansi.reset}`);
|
|
132
138
|
}
|
|
133
139
|
else if (activeAdventure.phase === "result") {
|
|
@@ -185,24 +191,42 @@ function redraw() {
|
|
|
185
191
|
}
|
|
186
192
|
else if (inventoryUI.active) {
|
|
187
193
|
const s = buddyState.adventureStats;
|
|
188
|
-
const invLines = (0, inventoryUI_1.renderInventoryUI)(inventoryUI, s.equippedWeapon, s.equippedArmor, s.equippedHelmet, s.equippedBoots, s.equippedAccessory, buddyState.questProgress.gold);
|
|
194
|
+
const invLines = (0, inventoryUI_1.renderInventoryUI)(inventoryUI, s.equippedWeapon, s.equippedArmor, s.equippedHelmet, s.equippedBoots, s.equippedAccessory, buddyState.questProgress.gold, buddyState.stats);
|
|
189
195
|
const combined = main + "\n" + invLines.join("\n");
|
|
190
196
|
screen.draw(combined, ` ${screen_1.ansi.dim}Inventory open${screen_1.ansi.reset}`);
|
|
191
197
|
}
|
|
192
198
|
else if (palette.active) {
|
|
193
|
-
const paletteLines = (0, commandPalette_1.renderPalette)(palette);
|
|
199
|
+
const paletteLines = (0, commandPalette_1.renderPalette)(palette, buddyState.stats);
|
|
194
200
|
const combined = main + "\n" + paletteLines.join("\n");
|
|
195
201
|
const inputDisplay = ` ${screen_1.ansi.dim}>${screen_1.ansi.reset} /${palette.filter}`;
|
|
196
202
|
screen.draw(combined, inputDisplay);
|
|
197
203
|
}
|
|
204
|
+
else if (pendingConfirmation) {
|
|
205
|
+
const confirmLines = [
|
|
206
|
+
` ${screen_1.ansi.dim}${"─".repeat(40)}${screen_1.ansi.reset}`,
|
|
207
|
+
` ${screen_1.ansi.colors.yellow}${pendingConfirmation.message}${screen_1.ansi.reset}`,
|
|
208
|
+
` ${screen_1.ansi.dim}${"─".repeat(40)}${screen_1.ansi.reset}`,
|
|
209
|
+
];
|
|
210
|
+
const combined = main + "\n" + confirmLines.join("\n");
|
|
211
|
+
screen.draw(combined, ` ${screen_1.ansi.colors.yellow}Enter${screen_1.ansi.reset} to confirm, ${screen_1.ansi.colors.yellow}Esc${screen_1.ansi.reset} to cancel`);
|
|
212
|
+
}
|
|
198
213
|
else if (pendingCommand) {
|
|
199
214
|
const inputDisplay = ` ${screen_1.ansi.dim}>${screen_1.ansi.reset} /${pendingCommand} ${argsBuffer}`;
|
|
200
215
|
screen.draw(main, inputDisplay);
|
|
201
216
|
}
|
|
202
217
|
else {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
218
|
+
let inputDisplay;
|
|
219
|
+
if (buddyState.activity && buddyState.activity.type !== "idle" && !buddyState.activity.unskippable) {
|
|
220
|
+
const timerLabel = buddyState.activity.type === "sleeping" ? "Sleeping" :
|
|
221
|
+
buddyState.activity.type === "eating" ? "Eating" : "Playing";
|
|
222
|
+
inputDisplay = ` ${screen_1.ansi.colors.cyan}${timerLabel}... (${(0, activities_1.formatTimeRemaining)(buddyState.activity)}) — press any key to skip${screen_1.ansi.reset}`;
|
|
223
|
+
}
|
|
224
|
+
else if (inputBuffer) {
|
|
225
|
+
inputDisplay = ` ${screen_1.ansi.dim}>${screen_1.ansi.reset} ${inputBuffer}`;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
inputDisplay = ` ${screen_1.ansi.dim}> [f]eed [p]lay [s]leep /commands${screen_1.ansi.reset}`;
|
|
229
|
+
}
|
|
206
230
|
screen.draw(main, inputDisplay);
|
|
207
231
|
}
|
|
208
232
|
}
|
|
@@ -513,11 +537,20 @@ function handleNamingInput(key) {
|
|
|
513
537
|
// Confirm name
|
|
514
538
|
const name = nameBuffer.trim() || rolledSpecies.name;
|
|
515
539
|
buddyState = (0, roll_1.createBuddyState)(rolledSpecies, name);
|
|
540
|
+
buddyState = { ...buddyState, tutorialStep: 0 };
|
|
516
541
|
(0, save_1.saveState)(buddyState);
|
|
517
542
|
appPhase = "playing";
|
|
518
543
|
showFeedback(`Welcome, ${name}! Take good care of them.`, 4000);
|
|
519
544
|
startAnimation();
|
|
520
545
|
startDecay();
|
|
546
|
+
// Start tutorial — show first hint after a short delay
|
|
547
|
+
setTimeout(() => {
|
|
548
|
+
if (buddyState && (buddyState.tutorialStep ?? 99) === 0) {
|
|
549
|
+
dialogueMessage = `I'm hungry! Try typing / and selecting "feed".`;
|
|
550
|
+
dialogueSource = "Tutorial";
|
|
551
|
+
redraw();
|
|
552
|
+
}
|
|
553
|
+
}, 5000);
|
|
521
554
|
redraw();
|
|
522
555
|
return;
|
|
523
556
|
}
|
|
@@ -558,6 +591,20 @@ function handlePlayingInput(key) {
|
|
|
558
591
|
handleMinigameMenuInput(key);
|
|
559
592
|
return;
|
|
560
593
|
}
|
|
594
|
+
// Confirmation prompt
|
|
595
|
+
if (pendingConfirmation) {
|
|
596
|
+
if (key === "\r" || key === "\n") {
|
|
597
|
+
const cb = pendingConfirmation.onConfirm;
|
|
598
|
+
pendingConfirmation = null;
|
|
599
|
+
cb();
|
|
600
|
+
}
|
|
601
|
+
else if (key === "\x1b") {
|
|
602
|
+
pendingConfirmation = null;
|
|
603
|
+
showFeedback("Cancelled.");
|
|
604
|
+
}
|
|
605
|
+
redraw();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
561
608
|
// Overlay menus (titles, notifications)
|
|
562
609
|
if (overlay.type) {
|
|
563
610
|
handleOverlayInput(key);
|
|
@@ -630,6 +677,15 @@ function handlePlayingInput(key) {
|
|
|
630
677
|
redraw();
|
|
631
678
|
return;
|
|
632
679
|
}
|
|
680
|
+
// Quick shortcuts — only when idle on main screen with empty input
|
|
681
|
+
if (!inputBuffer && !buddyState.activity) {
|
|
682
|
+
const shortcut = { f: "feed", p: "play", s: "sleep" };
|
|
683
|
+
const cmd = shortcut[key];
|
|
684
|
+
if (cmd) {
|
|
685
|
+
executeCommand(cmd);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
633
689
|
if (key === "\x7f" || key === "\b") {
|
|
634
690
|
inputBuffer = inputBuffer.slice(0, -1);
|
|
635
691
|
redraw();
|
|
@@ -802,6 +858,30 @@ function executeCommand(cmdName, args = []) {
|
|
|
802
858
|
redraw();
|
|
803
859
|
return;
|
|
804
860
|
}
|
|
861
|
+
if (result.message === "__CONFIRM_REVIVE__") {
|
|
862
|
+
const xpLoss = Math.floor(buddyState.xp / 2);
|
|
863
|
+
pendingConfirmation = {
|
|
864
|
+
message: `Revive ${buddyState.name}? This costs ${xpLoss} XP (${buddyState.xp} → ${buddyState.xp - xpLoss}).`,
|
|
865
|
+
onConfirm: () => {
|
|
866
|
+
if (!buddyState)
|
|
867
|
+
return;
|
|
868
|
+
const counts = { ...buddyState.actionCounts, revives: buddyState.actionCounts.revives + 1 };
|
|
869
|
+
buddyState = {
|
|
870
|
+
...buddyState,
|
|
871
|
+
alive: true,
|
|
872
|
+
xp: buddyState.xp - xpLoss,
|
|
873
|
+
stats: { hunger: 50, happiness: 50, health: 50, energy: 50 },
|
|
874
|
+
lastUpdated: Date.now(),
|
|
875
|
+
activity: undefined,
|
|
876
|
+
actionCounts: counts,
|
|
877
|
+
};
|
|
878
|
+
(0, save_1.saveState)(buddyState);
|
|
879
|
+
showFeedback(`${buddyState.name} stirs back to life! They lost ${xpLoss} XP but they're back.`);
|
|
880
|
+
},
|
|
881
|
+
};
|
|
882
|
+
redraw();
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
805
885
|
if (result.message === "__PET__") {
|
|
806
886
|
startPetSequence();
|
|
807
887
|
redraw();
|
|
@@ -810,11 +890,62 @@ function executeCommand(cmdName, args = []) {
|
|
|
810
890
|
if (result.message) {
|
|
811
891
|
showFeedback(result.message);
|
|
812
892
|
}
|
|
893
|
+
// Advance tutorial based on commands used
|
|
894
|
+
if (buddyState && buddyState.tutorialStep !== undefined && buddyState.tutorialStep < 3) {
|
|
895
|
+
advanceTutorial(cmdName);
|
|
896
|
+
}
|
|
813
897
|
redraw();
|
|
814
898
|
}
|
|
815
899
|
function pickRandom(arr) {
|
|
816
900
|
return arr[Math.floor(Math.random() * arr.length)];
|
|
817
901
|
}
|
|
902
|
+
function advanceTutorial(cmdName) {
|
|
903
|
+
if (!buddyState)
|
|
904
|
+
return;
|
|
905
|
+
const step = buddyState.tutorialStep ?? 99;
|
|
906
|
+
if (step === 0 && cmdName === "feed") {
|
|
907
|
+
buddyState = { ...buddyState, tutorialStep: 1 };
|
|
908
|
+
(0, save_1.saveState)(buddyState);
|
|
909
|
+
setTimeout(() => {
|
|
910
|
+
if (buddyState && buddyState.tutorialStep === 1) {
|
|
911
|
+
dialogueMessage = "Yum! My stats decay over time. Tip: press f, p, or s for quick feed/play/sleep!";
|
|
912
|
+
dialogueSource = "Tutorial";
|
|
913
|
+
if (dialogueTimeout)
|
|
914
|
+
clearTimeout(dialogueTimeout);
|
|
915
|
+
dialogueTimeout = setTimeout(() => {
|
|
916
|
+
if (buddyState && buddyState.tutorialStep === 1) {
|
|
917
|
+
dialogueMessage = "Now press p to play, or /play — it makes me happy! (Costs energy though.)";
|
|
918
|
+
dialogueSource = "Tutorial";
|
|
919
|
+
redraw();
|
|
920
|
+
}
|
|
921
|
+
}, 6000);
|
|
922
|
+
redraw();
|
|
923
|
+
}
|
|
924
|
+
}, 3000);
|
|
925
|
+
}
|
|
926
|
+
else if (step === 1 && cmdName === "play") {
|
|
927
|
+
buddyState = { ...buddyState, tutorialStep: 2 };
|
|
928
|
+
(0, save_1.saveState)(buddyState);
|
|
929
|
+
setTimeout(() => {
|
|
930
|
+
if (buddyState && buddyState.tutorialStep === 2) {
|
|
931
|
+
dialogueMessage = "I gain XP from activities! Try /sleep, /heal, /pet, and / to see everything.";
|
|
932
|
+
dialogueSource = "Tutorial";
|
|
933
|
+
if (dialogueTimeout)
|
|
934
|
+
clearTimeout(dialogueTimeout);
|
|
935
|
+
dialogueTimeout = setTimeout(() => {
|
|
936
|
+
if (buddyState) {
|
|
937
|
+
buddyState = { ...buddyState, tutorialStep: 3 };
|
|
938
|
+
(0, save_1.saveState)(buddyState);
|
|
939
|
+
dialogueMessage = undefined;
|
|
940
|
+
dialogueSource = undefined;
|
|
941
|
+
redraw();
|
|
942
|
+
}
|
|
943
|
+
}, 10000);
|
|
944
|
+
redraw();
|
|
945
|
+
}
|
|
946
|
+
}, 3000);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
818
949
|
function startPetSequence() {
|
|
819
950
|
if (!buddyState)
|
|
820
951
|
return;
|
|
@@ -1174,6 +1305,8 @@ function handleCombatInput(key) {
|
|
|
1174
1305
|
// Victory/Defeat/Fled — Enter to continue
|
|
1175
1306
|
if (combat.phase === "victory" || combat.phase === "defeat" || combat.phase === "fled") {
|
|
1176
1307
|
if (key === "\r" || key === "\n") {
|
|
1308
|
+
showingSkillMenu = false;
|
|
1309
|
+
showingItemMenu = false;
|
|
1177
1310
|
const adv = currentAdventureDef;
|
|
1178
1311
|
if (!adv)
|
|
1179
1312
|
return;
|
|
@@ -1255,32 +1388,60 @@ function handleCombatInput(key) {
|
|
|
1255
1388
|
redraw();
|
|
1256
1389
|
return;
|
|
1257
1390
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
//
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1391
|
+
if (showingItemMenu) {
|
|
1392
|
+
const combatItems = (0, combatUI_1.getCombatItems)(buddyState.inventory);
|
|
1393
|
+
if (key === "\x1b") {
|
|
1394
|
+
showingItemMenu = false;
|
|
1395
|
+
redraw();
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (key === "\x1b[A") {
|
|
1399
|
+
// Arrow up
|
|
1400
|
+
if (combatItems.length > 0) {
|
|
1401
|
+
itemMenuIndex = itemMenuIndex <= 0 ? combatItems.length - 1 : itemMenuIndex - 1;
|
|
1402
|
+
}
|
|
1403
|
+
redraw();
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
if (key === "\x1b[B") {
|
|
1407
|
+
// Arrow down
|
|
1408
|
+
if (combatItems.length > 0) {
|
|
1409
|
+
itemMenuIndex = itemMenuIndex >= combatItems.length - 1 ? 0 : itemMenuIndex + 1;
|
|
1410
|
+
}
|
|
1411
|
+
redraw();
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (key === "\r" || key === "\n") {
|
|
1415
|
+
// Use selected item
|
|
1416
|
+
if (combatItems.length > 0 && itemMenuIndex < combatItems.length) {
|
|
1417
|
+
const chosen = combatItems[itemMenuIndex];
|
|
1418
|
+
const item = (0, items_1.getItem)(chosen.slot.itemId);
|
|
1273
1419
|
const healAmount = item.statEffect.health ?? 0;
|
|
1274
1420
|
const newHp = Math.min(combat.playerStats.maxHp, combat.playerStats.hp + healAmount);
|
|
1275
1421
|
activeAdventure = {
|
|
1276
1422
|
...activeAdventure,
|
|
1277
1423
|
combat: { ...combat, playerStats: { ...combat.playerStats, hp: newHp }, phase: "player_animate", log: [...combat.log, `Used ${item.name}! +${healAmount} HP`], ticksInPhase: 0 },
|
|
1278
1424
|
};
|
|
1279
|
-
buddyState = { ...buddyState, inventory: buddyState.inventory.map((s) => s.itemId ===
|
|
1280
|
-
|
|
1281
|
-
else {
|
|
1282
|
-
activeAdventure = { ...activeAdventure, combat: { ...combat, log: [...combat.log, "No medicine in inventory!"] } };
|
|
1425
|
+
buddyState = { ...buddyState, inventory: buddyState.inventory.map((s) => s.itemId === chosen.slot.itemId ? { ...s, count: s.count - 1 } : s).filter((s) => s.count > 0) };
|
|
1426
|
+
showingItemMenu = false;
|
|
1283
1427
|
}
|
|
1428
|
+
redraw();
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
switch (key) {
|
|
1434
|
+
case "1": // Attack
|
|
1435
|
+
activeAdventure = { ...activeAdventure, combat: (0, combat_1.playerAttack)(combat) };
|
|
1436
|
+
break;
|
|
1437
|
+
case "2": // Skill menu
|
|
1438
|
+
showingSkillMenu = true;
|
|
1439
|
+
showingItemMenu = false;
|
|
1440
|
+
break;
|
|
1441
|
+
case "3": // Item menu
|
|
1442
|
+
showingItemMenu = true;
|
|
1443
|
+
showingSkillMenu = false;
|
|
1444
|
+
itemMenuIndex = 0;
|
|
1284
1445
|
break;
|
|
1285
1446
|
case "4": // Defend
|
|
1286
1447
|
activeAdventure = { ...activeAdventure, combat: (0, combat_1.playerDefend)(combat) };
|
|
@@ -1493,21 +1654,32 @@ function handleOverlayInput(key) {
|
|
|
1493
1654
|
return;
|
|
1494
1655
|
}
|
|
1495
1656
|
else if (overlay.type === "shop") {
|
|
1496
|
-
// Buy selected item
|
|
1657
|
+
// Buy selected item — with confirmation
|
|
1497
1658
|
const shopItem = shop_1.SHOP_ITEMS[overlay.scrollIndex];
|
|
1498
1659
|
if (shopItem) {
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
(
|
|
1503
|
-
const item = (0, items_1.getItem)(shopItem.itemId);
|
|
1504
|
-
showFeedback(`Bought ${item?.name ?? shopItem.itemId}! (-${shopItem.price}g)`);
|
|
1660
|
+
const item = (0, items_1.getItem)(shopItem.itemId);
|
|
1661
|
+
const gold = buddyState.questProgress.gold;
|
|
1662
|
+
if (gold < shopItem.price) {
|
|
1663
|
+
showFeedback("Not enough gold!");
|
|
1505
1664
|
}
|
|
1506
1665
|
else {
|
|
1507
|
-
|
|
1666
|
+
const itemName = item?.name ?? shopItem.itemId;
|
|
1667
|
+
overlay = (0, overlayUI_1.createOverlay)();
|
|
1668
|
+
pendingConfirmation = {
|
|
1669
|
+
message: `Buy ${itemName} for ${shopItem.price}g? (Gold: ${gold} → ${gold - shopItem.price})`,
|
|
1670
|
+
onConfirm: () => {
|
|
1671
|
+
if (!buddyState)
|
|
1672
|
+
return;
|
|
1673
|
+
const result = (0, progress_1.spendGold)(buddyState, shopItem.price);
|
|
1674
|
+
if (result) {
|
|
1675
|
+
buddyState = { ...result, inventory: (0, items_1.addItemToInventory)(result.inventory, shopItem.itemId) };
|
|
1676
|
+
(0, save_1.saveState)(buddyState);
|
|
1677
|
+
showFeedback(`Bought ${itemName}! (-${shopItem.price}g)`);
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
};
|
|
1508
1681
|
}
|
|
1509
1682
|
}
|
|
1510
|
-
// Stay in shop
|
|
1511
1683
|
redraw();
|
|
1512
1684
|
return;
|
|
1513
1685
|
}
|
|
@@ -1629,6 +1801,8 @@ async function main() {
|
|
|
1629
1801
|
}, 4000);
|
|
1630
1802
|
// Load settings
|
|
1631
1803
|
(0, settings_1.loadSettings)();
|
|
1804
|
+
// Enter alternate screen buffer (no scrollback pollution)
|
|
1805
|
+
screen.enterAltScreen();
|
|
1632
1806
|
// Handle clean exit
|
|
1633
1807
|
process.on("SIGINT", shutdown);
|
|
1634
1808
|
process.on("SIGTERM", shutdown);
|
|
@@ -12,5 +12,10 @@ export declare function openPalette(palette: PaletteState): PaletteState;
|
|
|
12
12
|
export declare function updateFilter(palette: PaletteState, filter: string): PaletteState;
|
|
13
13
|
export declare function moveSelection(palette: PaletteState, delta: number): PaletteState;
|
|
14
14
|
export declare function getSelectedCommand(palette: PaletteState): string | null;
|
|
15
|
-
export declare function renderPalette(palette: PaletteState
|
|
15
|
+
export declare function renderPalette(palette: PaletteState, stats?: {
|
|
16
|
+
hunger: number;
|
|
17
|
+
happiness: number;
|
|
18
|
+
health: number;
|
|
19
|
+
energy: number;
|
|
20
|
+
}): string[];
|
|
16
21
|
//# sourceMappingURL=commandPalette.d.ts.map
|
|
@@ -27,8 +27,9 @@ function openPalette(palette) {
|
|
|
27
27
|
}
|
|
28
28
|
function updateFilter(palette, filter) {
|
|
29
29
|
const all = (0, registry_1.getAllCommands)().map((c) => ({ name: c.name, description: c.description }));
|
|
30
|
+
const lowerFilter = filter.toLowerCase();
|
|
30
31
|
const filtered = filter
|
|
31
|
-
? all.filter((c) => c.name.toLowerCase().
|
|
32
|
+
? all.filter((c) => c.name.toLowerCase().includes(lowerFilter))
|
|
32
33
|
: all;
|
|
33
34
|
return {
|
|
34
35
|
...palette,
|
|
@@ -53,9 +54,17 @@ function getSelectedCommand(palette) {
|
|
|
53
54
|
return null;
|
|
54
55
|
return palette.filteredCommands[palette.selectedIndex].name;
|
|
55
56
|
}
|
|
56
|
-
function renderPalette(palette) {
|
|
57
|
+
function renderPalette(palette, stats) {
|
|
57
58
|
const lines = [];
|
|
58
59
|
lines.push(` ${screen_1.ansi.dim}${"─".repeat(40)}${screen_1.ansi.reset}`);
|
|
60
|
+
if (stats) {
|
|
61
|
+
const fmt = (val, label) => {
|
|
62
|
+
const color = val > 60 ? screen_1.ansi.colors.green : val > 30 ? screen_1.ansi.colors.yellow : screen_1.ansi.colors.red;
|
|
63
|
+
return `${color}${label}:${Math.round(val)}%${screen_1.ansi.reset}`;
|
|
64
|
+
};
|
|
65
|
+
lines.push(` ${fmt(stats.hunger, "Hun")} ${fmt(stats.happiness, "Hap")} ${fmt(stats.health, "HP")} ${fmt(stats.energy, "Eng")}`);
|
|
66
|
+
lines.push(` ${screen_1.ansi.dim}${"─".repeat(40)}${screen_1.ansi.reset}`);
|
|
67
|
+
}
|
|
59
68
|
if (palette.filteredCommands.length === 0) {
|
|
60
69
|
lines.push(` ${screen_1.ansi.dim} No matching commands${screen_1.ansi.reset}`);
|
|
61
70
|
}
|
|
@@ -10,6 +10,7 @@ const titles_1 = require("../buddy/titles");
|
|
|
10
10
|
const items_1 = require("./items");
|
|
11
11
|
const scene_1 = require("./scene");
|
|
12
12
|
const screen_1 = require("./screen");
|
|
13
|
+
const updates_1 = require("../updates");
|
|
13
14
|
function statBar(value, label, width = 20) {
|
|
14
15
|
const filled = Math.round((value / 100) * width);
|
|
15
16
|
const empty = width - filled;
|
|
@@ -36,7 +37,7 @@ function renderMainScreen(state, frame, message, dialogue, dialogueSource) {
|
|
|
36
37
|
const lines = [];
|
|
37
38
|
// Header bar
|
|
38
39
|
lines.push("");
|
|
39
|
-
lines.push(` ${screen_1.ansi.bold}CLI Buddy${screen_1.ansi.reset} ${screen_1.ansi.dim}
|
|
40
|
+
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}`);
|
|
40
41
|
lines.push(` ${screen_1.ansi.dim}${"─".repeat(40)}${screen_1.ansi.reset}`);
|
|
41
42
|
if (!state.alive) {
|
|
42
43
|
lines.push("");
|
|
@@ -71,6 +72,17 @@ function renderMainScreen(state, frame, message, dialogue, dialogueSource) {
|
|
|
71
72
|
else if (state.stats.happiness < 30 && species.animations.sad) {
|
|
72
73
|
animKey = "sad";
|
|
73
74
|
}
|
|
75
|
+
// Status tag for distressed states
|
|
76
|
+
const STATUS_TAGS = {
|
|
77
|
+
sick: `${screen_1.ansi.colors.red}[SICK]${screen_1.ansi.reset}`,
|
|
78
|
+
hungry: `${screen_1.ansi.colors.yellow}[HUNGRY]${screen_1.ansi.reset}`,
|
|
79
|
+
tired: `${screen_1.ansi.colors.blue}[TIRED]${screen_1.ansi.reset}`,
|
|
80
|
+
sad: `${screen_1.ansi.colors.magenta}[SAD]${screen_1.ansi.reset}`,
|
|
81
|
+
};
|
|
82
|
+
const statusTag = STATUS_TAGS[animKey];
|
|
83
|
+
if (statusTag) {
|
|
84
|
+
lines.push(` ${statusTag}`);
|
|
85
|
+
}
|
|
74
86
|
const frames = species.animations[animKey] ?? species.animations.idle;
|
|
75
87
|
const frameIndex = frame % frames.length;
|
|
76
88
|
const buddyArtLines = frames[frameIndex].split("\n");
|
|
@@ -124,7 +136,7 @@ function renderMainScreen(state, frame, message, dialogue, dialogueSource) {
|
|
|
124
136
|
function renderRollScreen(phase, speciesName, speciesArt, rarityColor, rarityLabel) {
|
|
125
137
|
const lines = [];
|
|
126
138
|
lines.push("");
|
|
127
|
-
lines.push(` ${screen_1.ansi.bold}CLI Buddy${screen_1.ansi.reset} ${screen_1.ansi.dim}
|
|
139
|
+
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}`);
|
|
128
140
|
lines.push(` ${screen_1.ansi.dim}${"─".repeat(40)}${screen_1.ansi.reset}`);
|
|
129
141
|
lines.push("");
|
|
130
142
|
if (phase === "waiting") {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InventorySlot } from "../buddy/types";
|
|
1
|
+
import { BuddyStats, InventorySlot } from "../buddy/types";
|
|
2
2
|
export interface InventoryUIState {
|
|
3
3
|
active: boolean;
|
|
4
4
|
tab: "consumables" | "gear";
|
|
@@ -10,5 +10,5 @@ export declare function openInventoryUI(slots: InventorySlot[]): InventoryUIStat
|
|
|
10
10
|
export declare function switchInventoryTab(ui: InventoryUIState): InventoryUIState;
|
|
11
11
|
export declare function moveInventorySelection(ui: InventoryUIState, delta: number): InventoryUIState;
|
|
12
12
|
export declare function getSelectedSlot(ui: InventoryUIState): InventorySlot | null;
|
|
13
|
-
export declare function renderInventoryUI(ui: InventoryUIState, equippedWeapon?: string, equippedArmor?: string, equippedHelmet?: string, equippedBoots?: string, equippedAccessory?: string, gold?: number): string[];
|
|
13
|
+
export declare function renderInventoryUI(ui: InventoryUIState, equippedWeapon?: string, equippedArmor?: string, equippedHelmet?: string, equippedBoots?: string, equippedAccessory?: string, gold?: number, buddyStats?: BuddyStats): string[];
|
|
14
14
|
//# sourceMappingURL=inventoryUI.d.ts.map
|
|
@@ -36,7 +36,7 @@ function getSelectedSlot(ui) {
|
|
|
36
36
|
return null;
|
|
37
37
|
return filtered[ui.selectedIndex] ?? null;
|
|
38
38
|
}
|
|
39
|
-
function renderInventoryUI(ui, equippedWeapon, equippedArmor, equippedHelmet, equippedBoots, equippedAccessory, gold) {
|
|
39
|
+
function renderInventoryUI(ui, equippedWeapon, equippedArmor, equippedHelmet, equippedBoots, equippedAccessory, gold, buddyStats) {
|
|
40
40
|
const lines = [];
|
|
41
41
|
lines.push(` ${screen_1.ansi.dim}${"─".repeat(48)}${screen_1.ansi.reset}`);
|
|
42
42
|
// Tab bar + gold
|
|
@@ -80,6 +80,19 @@ function renderInventoryUI(ui, equippedWeapon, equippedArmor, equippedHelmet, eq
|
|
|
80
80
|
// Consumables: show count, description
|
|
81
81
|
if (selected) {
|
|
82
82
|
lines.push(` ${pointer} ${rarityColor}${screen_1.ansi.bold}${item.name}${screen_1.ansi.reset} x${slot.count} ${screen_1.ansi.dim}${item.description}${screen_1.ansi.reset}`);
|
|
83
|
+
// Preview stat outcome
|
|
84
|
+
if (buddyStats && item.statEffect) {
|
|
85
|
+
const previews = [];
|
|
86
|
+
const STAT_LABELS = { hunger: "Hunger", happiness: "Happiness", health: "Health", energy: "Energy" };
|
|
87
|
+
for (const [key, delta] of Object.entries(item.statEffect)) {
|
|
88
|
+
const current = Math.round(buddyStats[key]);
|
|
89
|
+
const after = Math.min(100, Math.round(buddyStats[key] + delta));
|
|
90
|
+
previews.push(`${STAT_LABELS[key] ?? key}: ${current}%→${after}%`);
|
|
91
|
+
}
|
|
92
|
+
if (previews.length > 0) {
|
|
93
|
+
lines.push(` ${screen_1.ansi.colors.green}→ ${previews.join(", ")}${screen_1.ansi.reset}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
83
96
|
}
|
|
84
97
|
else {
|
|
85
98
|
lines.push(` ${pointer} ${screen_1.ansi.dim}${item.name} x${slot.count}${screen_1.ansi.reset}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Full-screen terminal renderer.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Uses the alternate screen buffer so nothing leaks into scrollback.
|
|
4
|
+
* Redraws by moving to home and overwriting — no clear, no flicker.
|
|
5
5
|
*/
|
|
6
6
|
export declare const ansi: {
|
|
7
7
|
clearScreen: string;
|
|
@@ -9,6 +9,7 @@ export declare const ansi: {
|
|
|
9
9
|
showCursor: string;
|
|
10
10
|
moveTo: (row: number, col: number) => string;
|
|
11
11
|
eraseDown: string;
|
|
12
|
+
eraseLine: string;
|
|
12
13
|
bold: string;
|
|
13
14
|
dim: string;
|
|
14
15
|
reset: string;
|
|
@@ -22,6 +23,8 @@ export declare const ansi: {
|
|
|
22
23
|
white: string;
|
|
23
24
|
gray: string;
|
|
24
25
|
};
|
|
26
|
+
altScreenOn: string;
|
|
27
|
+
altScreenOff: string;
|
|
25
28
|
};
|
|
26
29
|
export declare class Screen {
|
|
27
30
|
private regions;
|
|
@@ -30,7 +33,9 @@ export declare class Screen {
|
|
|
30
33
|
private width;
|
|
31
34
|
private height;
|
|
32
35
|
constructor();
|
|
33
|
-
/**
|
|
36
|
+
/** Enter alternate screen buffer and hide cursor */
|
|
37
|
+
enterAltScreen(): void;
|
|
38
|
+
/** Replace all content and redraw — no clear, just overwrite in place */
|
|
34
39
|
draw(content: string, footer?: string): void;
|
|
35
40
|
/** Update just the input line at the bottom without full redraw */
|
|
36
41
|
drawInputLine(input: string): void;
|
package/dist/rendering/screen.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Full-screen terminal renderer.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Uses the alternate screen buffer so nothing leaks into scrollback.
|
|
5
|
+
* Redraws by moving to home and overwriting — no clear, no flicker.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.Screen = exports.ansi = void 0;
|
|
@@ -13,6 +13,7 @@ exports.ansi = {
|
|
|
13
13
|
showCursor: `${ESC}[?25h`,
|
|
14
14
|
moveTo: (row, col) => `${ESC}[${row};${col}H`,
|
|
15
15
|
eraseDown: `${ESC}[J`,
|
|
16
|
+
eraseLine: `${ESC}[2K`,
|
|
16
17
|
bold: `${ESC}[1m`,
|
|
17
18
|
dim: `${ESC}[2m`,
|
|
18
19
|
reset: `${ESC}[0m`,
|
|
@@ -26,6 +27,9 @@ exports.ansi = {
|
|
|
26
27
|
white: `${ESC}[37m`,
|
|
27
28
|
gray: `${ESC}[90m`,
|
|
28
29
|
},
|
|
30
|
+
// Alternate screen buffer
|
|
31
|
+
altScreenOn: `${ESC}[?1049h`,
|
|
32
|
+
altScreenOff: `${ESC}[?1049l`,
|
|
29
33
|
};
|
|
30
34
|
class Screen {
|
|
31
35
|
regions = [];
|
|
@@ -41,20 +45,40 @@ class Screen {
|
|
|
41
45
|
this.height = process.stdout.rows || 24;
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
|
-
/**
|
|
48
|
+
/** Enter alternate screen buffer and hide cursor */
|
|
49
|
+
enterAltScreen() {
|
|
50
|
+
process.stdout.write(exports.ansi.altScreenOn);
|
|
51
|
+
process.stdout.write(exports.ansi.hideCursor);
|
|
52
|
+
}
|
|
53
|
+
/** Replace all content and redraw — no clear, just overwrite in place */
|
|
45
54
|
draw(content, footer) {
|
|
46
55
|
const out = process.stdout;
|
|
47
56
|
out.write(exports.ansi.hideCursor);
|
|
48
|
-
|
|
49
|
-
out.write(
|
|
57
|
+
// Move to top-left and write content
|
|
58
|
+
out.write(`${ESC}[H`);
|
|
59
|
+
// Split content into lines and write each, padding to full width to overwrite old content
|
|
60
|
+
const contentLines = content.split("\n");
|
|
61
|
+
const maxContentRow = this.height - 2; // leave 2 rows for footer + input
|
|
62
|
+
for (let i = 0; i < maxContentRow; i++) {
|
|
63
|
+
out.write(exports.ansi.moveTo(i + 1, 1));
|
|
64
|
+
out.write(exports.ansi.eraseLine);
|
|
65
|
+
if (i < contentLines.length) {
|
|
66
|
+
out.write(contentLines[i]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Draw footer at bottom - 1
|
|
50
70
|
if (footer) {
|
|
51
|
-
// Draw footer at bottom
|
|
52
71
|
out.write(exports.ansi.moveTo(this.height - 1, 1));
|
|
72
|
+
out.write(exports.ansi.eraseLine);
|
|
53
73
|
out.write(footer);
|
|
54
74
|
}
|
|
55
|
-
|
|
75
|
+
else {
|
|
76
|
+
out.write(exports.ansi.moveTo(this.height - 1, 1));
|
|
77
|
+
out.write(exports.ansi.eraseLine);
|
|
78
|
+
}
|
|
79
|
+
// Position cursor at input line (bottom row)
|
|
56
80
|
out.write(exports.ansi.moveTo(this.height, 1));
|
|
57
|
-
out.write(exports.ansi.
|
|
81
|
+
out.write(exports.ansi.eraseLine);
|
|
58
82
|
out.write(this.inputLine);
|
|
59
83
|
out.write(exports.ansi.showCursor);
|
|
60
84
|
}
|
|
@@ -63,7 +87,7 @@ class Screen {
|
|
|
63
87
|
this.inputLine = input;
|
|
64
88
|
const out = process.stdout;
|
|
65
89
|
out.write(exports.ansi.moveTo(this.height, 1));
|
|
66
|
-
out.write(
|
|
90
|
+
out.write(exports.ansi.eraseLine);
|
|
67
91
|
out.write(input);
|
|
68
92
|
}
|
|
69
93
|
/** Draw an overlay (command palette) over the bottom portion of the screen */
|
|
@@ -73,12 +97,12 @@ class Screen {
|
|
|
73
97
|
// Draw overlay lines
|
|
74
98
|
for (let i = 0; i < lines.length; i++) {
|
|
75
99
|
out.write(exports.ansi.moveTo(startRow + i, 1));
|
|
76
|
-
out.write(
|
|
100
|
+
out.write(exports.ansi.eraseLine);
|
|
77
101
|
out.write(lines[i]);
|
|
78
102
|
}
|
|
79
103
|
// Draw input line at bottom
|
|
80
104
|
out.write(exports.ansi.moveTo(this.height, 1));
|
|
81
|
-
out.write(
|
|
105
|
+
out.write(exports.ansi.eraseLine);
|
|
82
106
|
out.write(inputLine);
|
|
83
107
|
out.write(exports.ansi.showCursor);
|
|
84
108
|
}
|
|
@@ -90,7 +114,7 @@ class Screen {
|
|
|
90
114
|
}
|
|
91
115
|
cleanup() {
|
|
92
116
|
process.stdout.write(exports.ansi.showCursor);
|
|
93
|
-
process.stdout.write(exports.ansi.
|
|
117
|
+
process.stdout.write(exports.ansi.altScreenOff);
|
|
94
118
|
}
|
|
95
119
|
}
|
|
96
120
|
exports.Screen = Screen;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clibuddy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A tamagotchi-style virtual pet that lives in your terminal — feed, play, adventure, and battle!",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc",
|
|
19
|
-
"dev": "CLIBUDDY_DEV=1 tsx src/index.ts",
|
|
19
|
+
"dev": "cross-env CLIBUDDY_DEV=1 tsx src/index.ts",
|
|
20
20
|
"start": "node dist/index.js",
|
|
21
21
|
"prepublishOnly": "npm run build",
|
|
22
22
|
"sim": "tsx src/tests/combatSim.ts"
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"homepage": "https://github.com/ZippyDevlabs/CLIBuddy#readme",
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^25.5.0",
|
|
50
|
+
"cross-env": "^10.1.0",
|
|
50
51
|
"tsx": "^4.21.0",
|
|
51
52
|
"typescript": "^6.0.2"
|
|
52
53
|
}
|