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.
@@ -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
- lines.push(` ${screen_1.ansi.dim}Cost: ${energyCost} energy, ${hungerCost} hunger${screen_1.ansi.reset}`);
154
- const canGo = state.level >= Math.max(1, (diff - 1) * 2) && state.stats.energy >= energyCost && state.stats.hunger >= hungerCost;
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
- lines.push(` ${screen_1.ansi.dim}Requires: Level 5+, 20 energy, 15 hunger${screen_1.ansi.reset}`);
174
- const canGo = state.level >= 5 && state.stats.energy >= 20 && state.stats.hunger >= 15;
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
@@ -36,7 +36,7 @@ function createBuddyState(species, name) {
36
36
  name,
37
37
  speciesId: species.id,
38
38
  stats: {
39
- hunger: 80,
39
+ hunger: 75,
40
40
  happiness: 80,
41
41
  health: 100,
42
42
  energy: 100,
@@ -83,6 +83,7 @@ export interface BuddyState {
83
83
  notifications: Notification[];
84
84
  adventureStats: AdventureStats;
85
85
  questProgress: import("../story/types").QuestProgress;
86
+ tutorialStep?: number;
86
87
  }
87
88
  export declare const RARITY_COLORS: Record<Rarity, string>;
88
89
  export declare const RARITY_LABELS: Record<Rarity, string>;
@@ -38,12 +38,12 @@ function registerActivityCommand(name, description, activityType, countKey, mess
38
38
  },
39
39
  });
40
40
  }
41
- registerActivityCommand("feed", "Feed your buddy to restore hunger", "eating", "feeds", (name) => `${name} starts eating! Nom nom...`);
42
- registerActivityCommand("play", "Play with your buddy to boost happiness", "playing", "plays", (name) => `${name} starts playing!`, (state) => state.stats.energy < 10 ? `${state.name} is too tired to play. Let them /sleep first.` : null);
43
- registerActivityCommand("sleep", "Let your buddy rest to restore energy", "sleeping", "sleeps", (name) => `${name} curls up and falls asleep... Zzz`);
41
+ registerActivityCommand("feed", "Feed Hunger +25 (10s)", "eating", "feeds", (name) => `${name} starts eating! Nom nom...`);
42
+ registerActivityCommand("play", "Play Happiness +20, Energy -15 (15s)", "playing", "plays", (name) => `${name} starts playing!`, (state) => state.stats.energy < 10 ? `${state.name} is too tired to play. Let them /sleep first.` : null);
43
+ registerActivityCommand("sleep", "Sleep Energy +30 (2m, skippable)", "sleeping", "sleeps", (name) => `${name} curls up and falls asleep... Zzz`);
44
44
  (0, registry_1.registerCommand)({
45
45
  name: "heal",
46
- description: "Heal your buddy to restore health (instant)",
46
+ description: "Heal Health +20, +3 XP (instant)",
47
47
  execute: (state) => {
48
48
  if (!state.alive) {
49
49
  return { state: null, message: `${state.name} is no longer with us... Use /revive first.`, rerender: false };
@@ -66,21 +66,7 @@ registerActivityCommand("sleep", "Let your buddy rest to restore energy", "sleep
66
66
  if (state.alive) {
67
67
  return { state: null, message: `${state.name} is alive and well!`, rerender: false };
68
68
  }
69
- const counts = { ...state.actionCounts, revives: state.actionCounts.revives + 1 };
70
- const newState = {
71
- ...state,
72
- alive: true,
73
- xp: Math.floor(state.xp / 2),
74
- stats: { hunger: 50, happiness: 50, health: 50, energy: 50 },
75
- lastUpdated: Date.now(),
76
- activity: undefined,
77
- actionCounts: counts,
78
- };
79
- return {
80
- state: newState,
81
- message: `${state.name} stirs back to life! They lost half their XP but they're back.`,
82
- rerender: true,
83
- };
69
+ return { state: null, message: "__CONFIRM_REVIVE__", rerender: false };
84
70
  },
85
71
  });
86
72
  (0, registry_1.registerCommand)({
@@ -100,7 +86,7 @@ registerActivityCommand("sleep", "Let your buddy rest to restore energy", "sleep
100
86
  name: "help",
101
87
  description: "Show all available commands",
102
88
  execute: () => {
103
- return { state: null, message: "Press / to open the command palette!", rerender: false };
89
+ return { state: null, message: "Quick: [f]eed [p]lay [s]leep or press / for all commands", rerender: false };
104
90
  },
105
91
  });
106
92
  (0, registry_1.registerCommand)({
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
- const inputDisplay = inputBuffer
204
- ? ` ${screen_1.ansi.dim}>${screen_1.ansi.reset} ${inputBuffer}`
205
- : ` ${screen_1.ansi.dim}> Type / for commands${screen_1.ansi.reset}`;
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
- switch (key) {
1259
- case "1": // Attack
1260
- activeAdventure = { ...activeAdventure, combat: (0, combat_1.playerAttack)(combat) };
1261
- break;
1262
- case "2": // Skill menu
1263
- showingSkillMenu = true;
1264
- break;
1265
- case "3": // Item (use potion/food from inventory mid-combat)
1266
- // Simple: use first medicine in inventory
1267
- const medicine = buddyState.inventory.find((s) => {
1268
- const item = (0, items_1.getItem)(s.itemId);
1269
- return item?.type === "medicine" && s.count > 0;
1270
- });
1271
- if (medicine) {
1272
- const item = (0, items_1.getItem)(medicine.itemId);
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 === medicine.itemId ? { ...s, count: s.count - 1 } : s).filter((s) => s.count > 0) };
1280
- }
1281
- else {
1282
- activeAdventure = { ...activeAdventure, combat: { ...combat, log: [...combat.log, "No medicine in inventory!"] } };
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 result = (0, progress_1.spendGold)(buddyState, shopItem.price);
1500
- if (result) {
1501
- buddyState = { ...result, inventory: (0, items_1.addItemToInventory)(result.inventory, shopItem.itemId) };
1502
- (0, save_1.saveState)(buddyState);
1503
- const item = (0, items_1.getItem)(shopItem.itemId);
1504
- showFeedback(`Bought ${item?.name ?? shopItem.itemId}! (-${shopItem.price}g)`);
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
- showFeedback("Not enough gold!");
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): string[];
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().startsWith(filter.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}v0.1${screen_1.ansi.reset}`);
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}v0.1${screen_1.ansi.reset}`);
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
- * Owns the entire terminal clears and redraws each frame.
4
- * No scrolling, no message spam.
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
- /** Replace all content and redraw */
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;
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Full-screen terminal renderer.
4
- * Owns the entire terminal clears and redraws each frame.
5
- * No scrolling, no message spam.
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
- /** Replace all content and redraw */
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
- out.write(exports.ansi.clearScreen);
49
- out.write(content);
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
- // Position cursor at input line
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.eraseDown);
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("\x1b[2K"); // erase line
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("\x1b[2K"); // erase line
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("\x1b[2K");
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.clearScreen);
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.0.0",
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
  }