@readme/cli 0.0.26
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/README.md +55 -0
- package/bin/readme.js +8 -0
- package/package.json +58 -0
- package/src/bootstrap.js +97 -0
- package/src/cli.js +189 -0
- package/src/commands/dev.js +119 -0
- package/src/commands/eyes.js +37 -0
- package/src/commands/import.js +2565 -0
- package/src/commands/lint.js +70 -0
- package/src/commands/oas-sync.js +364 -0
- package/src/commands/oas-validate.js +208 -0
- package/src/commands/play.js +17 -0
- package/src/commands/pretty.js +133 -0
- package/src/commands/setup.js +256 -0
- package/src/commands/versions.js +81 -0
- package/src/dev/.next/app-build-manifest.json +20 -0
- package/src/dev/.next/build-manifest.json +31 -0
- package/src/dev/.next/cache/.rscinfo +1 -0
- package/src/dev/.next/cache/next-devtools-config.json +1 -0
- package/src/dev/.next/cache/webpack/client-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/10.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/11.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/2.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/3.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/3.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/4.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/5.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/5.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/6.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/7.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/7.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/8.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/9.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/server-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/10.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/11.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/12.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/13.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/14.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/15.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/2.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/2.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/3.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/3.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/4.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/5.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/6.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/6.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/7.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/7.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/8.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/9.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/9.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
- package/src/dev/.next/package.json +1 -0
- package/src/dev/.next/prerender-manifest.json +11 -0
- package/src/dev/.next/react-loadable-manifest.json +1 -0
- package/src/dev/.next/routes-manifest.json +1 -0
- package/src/dev/.next/server/app/[...slug]/page.js +360 -0
- package/src/dev/.next/server/app/[...slug]/page_client-reference-manifest.js +1 -0
- package/src/dev/.next/server/app/page.js +349 -0
- package/src/dev/.next/server/app/page_client-reference-manifest.js +1 -0
- package/src/dev/.next/server/app-paths-manifest.json +3 -0
- package/src/dev/.next/server/edge-runtime-webpack.js +1151 -0
- package/src/dev/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/src/dev/.next/server/middleware-build-manifest.js +33 -0
- package/src/dev/.next/server/middleware-manifest.json +32 -0
- package/src/dev/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/src/dev/.next/server/middleware.js +1113 -0
- package/src/dev/.next/server/next-font-manifest.js +1 -0
- package/src/dev/.next/server/next-font-manifest.json +1 -0
- package/src/dev/.next/server/pages-manifest.json +5 -0
- package/src/dev/.next/server/server-reference-manifest.js +1 -0
- package/src/dev/.next/server/server-reference-manifest.json +5 -0
- package/src/dev/.next/server/static/webpack/633457081244afec._.hot-update.json +1 -0
- package/src/dev/.next/server/vendor-chunks/@readme.js +25 -0
- package/src/dev/.next/server/vendor-chunks/@swc.js +55 -0
- package/src/dev/.next/server/vendor-chunks/next.js +3659 -0
- package/src/dev/.next/server/webpack-runtime.js +209 -0
- package/src/dev/.next/static/chunks/app/[...slug]/loading.js +28 -0
- package/src/dev/.next/static/chunks/app/[...slug]/page.js +28 -0
- package/src/dev/.next/static/chunks/app/layout.js +171 -0
- package/src/dev/.next/static/chunks/app/page.js +28 -0
- package/src/dev/.next/static/chunks/app-pages-internals.js +182 -0
- package/src/dev/.next/static/chunks/main-app.js +1882 -0
- package/src/dev/.next/static/chunks/polyfills.js +1 -0
- package/src/dev/.next/static/chunks/webpack.js +1393 -0
- package/src/dev/.next/static/css/app/layout.css +559 -0
- package/src/dev/.next/static/development/_buildManifest.js +1 -0
- package/src/dev/.next/static/development/_ssgManifest.js +1 -0
- package/src/dev/.next/static/webpack/633457081244afec._.hot-update.json +1 -0
- package/src/dev/.next/static/webpack/ec52a3fce0f78db0.webpack.hot-update.json +1 -0
- package/src/dev/.next/static/webpack/webpack.ec52a3fce0f78db0.hot-update.js +12 -0
- package/src/dev/.next/trace +21 -0
- package/src/dev/.next/types/app/[...slug]/page.ts +84 -0
- package/src/dev/.next/types/app/layout.ts +84 -0
- package/src/dev/.next/types/app/page.ts +84 -0
- package/src/dev/.next/types/cache-life.d.ts +141 -0
- package/src/dev/.next/types/package.json +1 -0
- package/src/dev/.next/types/routes.d.ts +55 -0
- package/src/dev/app/Sidebar.js +149 -0
- package/src/dev/app/[...slug]/loading.js +16 -0
- package/src/dev/app/[...slug]/page.js +43 -0
- package/src/dev/app/globals.css +167 -0
- package/src/dev/app/layout.js +73 -0
- package/src/dev/app/page.js +19 -0
- package/src/dev/lib/docs.js +337 -0
- package/src/dev/middleware.js +7 -0
- package/src/dev/next.config.mjs +22 -0
- package/src/index.js +12 -0
- package/src/prompts/index.js +352 -0
- package/src/utils/claude.js +15 -0
- package/src/utils/eyes.js +365 -0
- package/src/utils/git.js +143 -0
- package/src/utils/lint.js +99 -0
- package/src/utils/reporter.js +319 -0
- package/src/utils/setup-templates.js +323 -0
- package/src/utils/styles.js +50 -0
- package/src/utils/tamagotchi.js +1139 -0
- package/src/utils/tips.js +90 -0
- package/src/validators/components.js +230 -0
- package/src/validators/content.js +53 -0
- package/src/validators/duplicates.js +45 -0
- package/src/validators/frontmatter.js +247 -0
- package/src/validators/links.js +68 -0
- package/src/validators/nesting.js +50 -0
- package/src/validators/numbering.js +136 -0
- package/src/validators/oas-reference.js +126 -0
- package/src/validators/oas-schema.js +106 -0
- package/src/validators/ordering.js +121 -0
- package/src/validators/recipes.js +143 -0
- package/vendor/TOOLS.md +19 -0
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { eyes, expressions, palettes, setPalette } from './eyes.js';
|
|
7
|
+
|
|
8
|
+
// ── Data directory (XDG spec) ───────────────────────────
|
|
9
|
+
|
|
10
|
+
function getDataDir() {
|
|
11
|
+
const base = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
|
12
|
+
return path.join(base, 'readme');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getSavePath() {
|
|
16
|
+
return path.join(getDataDir(), 'eyes.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── State management ────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const MAX_STAT = 10;
|
|
22
|
+
const DECAY_INTERVAL_MS = 1000 * 60 * 5; // stats decay every 5 minutes of real time
|
|
23
|
+
|
|
24
|
+
const ALL_TRICKS = ['hide and seek', 'spin', 'fetch', 'bow', 'chameleon', 'dance', 'owl impression', 'writes an OAS file'];
|
|
25
|
+
|
|
26
|
+
function newPet(name = 'Eyes', color = 'blue') {
|
|
27
|
+
return {
|
|
28
|
+
name,
|
|
29
|
+
color,
|
|
30
|
+
born: Date.now(),
|
|
31
|
+
lastVisit: Date.now(),
|
|
32
|
+
hunger: 8, // 0 = starving, 10 = full
|
|
33
|
+
happiness: 8, // 0 = miserable, 10 = ecstatic
|
|
34
|
+
energy: 8, // 0 = exhausted, 10 = wide awake
|
|
35
|
+
sleeping: false,
|
|
36
|
+
age: 0, // total real-time minutes alive
|
|
37
|
+
tricks: [], // learned trick names
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function loadPet() {
|
|
42
|
+
try {
|
|
43
|
+
const data = JSON.parse(fs.readFileSync(getSavePath(), 'utf8'));
|
|
44
|
+
return data;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function savePet(pet) {
|
|
51
|
+
const dir = getDataDir();
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
fs.writeFileSync(getSavePath(), JSON.stringify(pet, null, 2));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function applyDecay(pet) {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const elapsed = now - pet.lastVisit;
|
|
59
|
+
const ticks = Math.floor(elapsed / DECAY_INTERVAL_MS);
|
|
60
|
+
|
|
61
|
+
if (ticks > 0) {
|
|
62
|
+
pet.hunger = Math.max(0, pet.hunger - ticks);
|
|
63
|
+
pet.happiness = Math.max(0, pet.happiness - Math.floor(ticks * 0.7));
|
|
64
|
+
pet.energy = Math.min(MAX_STAT, pet.energy + Math.floor(ticks * 0.3)); // rests while you're away
|
|
65
|
+
pet.lastVisit = now;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Update age in minutes
|
|
69
|
+
pet.age = Math.floor((now - pet.born) / 60000);
|
|
70
|
+
return pet;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Actions ─────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
// Returns "does a spin" vs "writes an OAS file" depending on the trick name
|
|
76
|
+
function trickPhrase(trick) {
|
|
77
|
+
// Tricks with custom phrasing
|
|
78
|
+
const custom = { 'writes an OAS file': 'writes an OAS file', 'hide and seek': 'plays hide and seek' };
|
|
79
|
+
if (custom[trick]) return custom[trick];
|
|
80
|
+
const vowels = 'aeiou';
|
|
81
|
+
const article = vowels.includes(trick[0].toLowerCase()) ? 'an' : 'a';
|
|
82
|
+
return `does ${article} ${trick}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ok = (message, extra = {}) => ({ message, ok: true, ...extra });
|
|
86
|
+
const fail = (message) => ({ message, ok: false });
|
|
87
|
+
|
|
88
|
+
function feed(pet) {
|
|
89
|
+
if (pet.hunger >= MAX_STAT) return fail('Already full!');
|
|
90
|
+
pet.sleeping = false;
|
|
91
|
+
pet.hunger = Math.min(MAX_STAT, pet.hunger + 3);
|
|
92
|
+
pet.energy = Math.min(MAX_STAT, pet.energy + 1);
|
|
93
|
+
return ok(`${pet.name} munches happily!`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function play(pet) {
|
|
97
|
+
if (pet.energy <= 0) return fail('Too tired to play...');
|
|
98
|
+
pet.sleeping = false;
|
|
99
|
+
pet.happiness = Math.min(MAX_STAT, pet.happiness + 3);
|
|
100
|
+
pet.hunger = Math.max(0, pet.hunger - 1);
|
|
101
|
+
pet.energy = Math.max(0, pet.energy - 2);
|
|
102
|
+
return ok(`${pet.name} bounces around!`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function nap(pet) {
|
|
106
|
+
if (pet.sleeping) {
|
|
107
|
+
pet.sleeping = false;
|
|
108
|
+
return ok(`${pet.name} wakes up! Good morning!`);
|
|
109
|
+
}
|
|
110
|
+
if (pet.energy >= MAX_STAT) return fail('Not sleepy!');
|
|
111
|
+
pet.sleeping = true;
|
|
112
|
+
pet.energy = Math.min(MAX_STAT, pet.energy + 4);
|
|
113
|
+
pet.happiness = Math.min(MAX_STAT, pet.happiness + 1);
|
|
114
|
+
return ok(`${pet.name} curls up for a nap... zzz`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function petAction(pet) {
|
|
118
|
+
if (pet.energy <= 1) return fail(`${pet.name} is too tired for pets...`);
|
|
119
|
+
pet.sleeping = false;
|
|
120
|
+
pet.happiness = Math.min(MAX_STAT, pet.happiness + 2);
|
|
121
|
+
return ok(`${pet.name} loves the attention!`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function teach(pet) {
|
|
125
|
+
if (pet.energy <= 1) return fail('Too tired to learn right now...');
|
|
126
|
+
pet.sleeping = false;
|
|
127
|
+
|
|
128
|
+
const known = (pet.tricks || []).filter(t => ALL_TRICKS.includes(t));
|
|
129
|
+
pet.tricks = known; // prune stale tricks from old saves
|
|
130
|
+
const unknown = ALL_TRICKS.filter(t => !known.includes(t));
|
|
131
|
+
|
|
132
|
+
pet.energy = Math.max(0, pet.energy - 1);
|
|
133
|
+
pet.hunger = Math.max(0, pet.hunger - 1);
|
|
134
|
+
|
|
135
|
+
if (unknown.length === 0) {
|
|
136
|
+
const trick = known[Math.floor(Math.random() * known.length)];
|
|
137
|
+
pet.happiness = Math.min(MAX_STAT, pet.happiness + 1);
|
|
138
|
+
return ok(`${pet.name} ${trickPhrase(trick)}!`, { trick });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const learnChance = 0.3 + (pet.happiness / MAX_STAT) * 0.4;
|
|
142
|
+
if (Math.random() < learnChance) {
|
|
143
|
+
const trick = unknown[Math.floor(Math.random() * unknown.length)];
|
|
144
|
+
pet.tricks = [...known, trick];
|
|
145
|
+
pet.happiness = Math.min(MAX_STAT, pet.happiness + 2);
|
|
146
|
+
return ok(`${pet.name} learned ${trick}! (${pet.tricks.length}/${ALL_TRICKS.length} tricks)`, { trick });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const reactions = [
|
|
150
|
+
`${pet.name} tilts their head... not quite`,
|
|
151
|
+
`${pet.name} got distracted. Try again!`,
|
|
152
|
+
`Almost! ${pet.name} is getting the hang of it`,
|
|
153
|
+
`${pet.name} tries their best but needs more practice`,
|
|
154
|
+
];
|
|
155
|
+
pet.happiness = Math.min(MAX_STAT, pet.happiness + 1);
|
|
156
|
+
return ok(reactions[Math.floor(Math.random() * reactions.length)]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Expression based on mood ────────────────────────────
|
|
160
|
+
|
|
161
|
+
function getMood(pet) {
|
|
162
|
+
if (pet.sleeping) return 'sleeping';
|
|
163
|
+
if (pet.energy <= 1) return 'tired';
|
|
164
|
+
if (pet.hunger <= 1) return 'sad';
|
|
165
|
+
if (pet.happiness >= 8 && pet.hunger >= 6) return 'happy';
|
|
166
|
+
if (pet.happiness <= 2) return 'sad';
|
|
167
|
+
return 'normal';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getExpression(mood) {
|
|
171
|
+
switch (mood) {
|
|
172
|
+
case 'sleeping': return 'closed';
|
|
173
|
+
case 'tired': return 'squint';
|
|
174
|
+
case 'sad': return 'squint';
|
|
175
|
+
case 'happy': return 'right';
|
|
176
|
+
default: return 'right';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get a greeting message and expression for the CLI header.
|
|
182
|
+
* Returns { greeting, expression } based on current pet state.
|
|
183
|
+
*/
|
|
184
|
+
export function getPetHeader(pet) {
|
|
185
|
+
const name = pet.name;
|
|
186
|
+
|
|
187
|
+
if (pet.sleeping) {
|
|
188
|
+
return { greeting: `${name} is sleeping... zzz`, expression: 'closed' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const expression = Math.random() < 0.5 ? 'left' : 'right';
|
|
192
|
+
return { greeting: null, expression };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Animated expression sequence based on mood
|
|
196
|
+
function getAnimFrames(mood) {
|
|
197
|
+
switch (mood) {
|
|
198
|
+
case 'sleeping':
|
|
199
|
+
return {
|
|
200
|
+
frames: ['closed', 'closed', 'closed', 'closed', 'squint', 'closed', 'closed', 'closed', 'squint', 'closed'],
|
|
201
|
+
durations: [2000, 1000, 2000, 500, 100, 800, 1500, 500, 100, 1000],
|
|
202
|
+
};
|
|
203
|
+
case 'tired':
|
|
204
|
+
return {
|
|
205
|
+
frames: ['half-blink', 'half-blink', 'half-blink-left', 'half-blink', 'half-blink', 'half-blink-right', 'squint', 'half-blink', 'half-blink-left', 'half-blink', 'half-blink', 'half-blink-right', 'closed', 'half-blink', 'half-blink'],
|
|
206
|
+
durations: [800, 600, 500, 1000, 400, 500, 150, 800, 500, 600, 800, 500, 200, 300, 1200],
|
|
207
|
+
};
|
|
208
|
+
case 'sad':
|
|
209
|
+
return {
|
|
210
|
+
frames: ['left', 'left', 'right', 'right', 'left', 'half-blink', 'squint', 'half-blink', 'left', 'left', 'right', 'left'],
|
|
211
|
+
durations: [800, 400, 200, 800, 600, 80, 400, 80, 600, 400, 300, 800],
|
|
212
|
+
};
|
|
213
|
+
case 'happy':
|
|
214
|
+
return {
|
|
215
|
+
frames: ['right', 'right', 'right:up', 'right:up', 'right', 'left', 'right', 'half-blink', 'squint', 'closed', 'squint', 'half-blink', 'right', 'right'],
|
|
216
|
+
durations: [600, 300, 150, 150, 300, 400, 400, 50, 50, 50, 50, 50, 300, 800],
|
|
217
|
+
};
|
|
218
|
+
default:
|
|
219
|
+
return {
|
|
220
|
+
frames: ['right', 'right', 'right', 'left', 'left', 'right', 'half-blink', 'squint', 'closed', 'squint', 'half-blink', 'right', 'right'],
|
|
221
|
+
durations: [1200, 200, 400, 120, 600, 120, 50, 50, 50, 50, 50, 200, 1000],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Rendering ───────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
function statBar(value, max, color) {
|
|
229
|
+
const filled = Math.round((value / max) * 10);
|
|
230
|
+
const empty = 10 - filled;
|
|
231
|
+
return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatAge(minutes) {
|
|
235
|
+
if (minutes < 60) return `${minutes}m`;
|
|
236
|
+
const hours = Math.floor(minutes / 60);
|
|
237
|
+
if (hours < 24) return `${hours}h ${minutes % 60}m`;
|
|
238
|
+
const days = Math.floor(hours / 24);
|
|
239
|
+
return `${days}d ${hours % 24}h`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const BOX_WIDTH = 53;
|
|
243
|
+
|
|
244
|
+
function boxVisLen(s) {
|
|
245
|
+
const stripped = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
246
|
+
let len = 0;
|
|
247
|
+
for (const ch of stripped) {
|
|
248
|
+
const code = ch.codePointAt(0);
|
|
249
|
+
len += code >= 0x1F000 ? 2 : 1;
|
|
250
|
+
}
|
|
251
|
+
return len;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function boxLine(content = '', bg = null) {
|
|
255
|
+
const inner = BOX_WIDTH - 2;
|
|
256
|
+
const padding = Math.max(0, inner - boxVisLen(content));
|
|
257
|
+
const filled = content + ' '.repeat(padding);
|
|
258
|
+
return chalk.dim('│') + (bg ? chalk.bgHex(bg)(filled) : filled) + chalk.dim('│');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function boxTop() {
|
|
262
|
+
return chalk.dim('┌' + '─'.repeat(BOX_WIDTH - 2) + '┐');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function boxBottom() {
|
|
266
|
+
return chalk.dim('└' + '─'.repeat(BOX_WIDTH - 2) + '┘');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function boxDivider() {
|
|
270
|
+
return chalk.dim('├' + '─'.repeat(BOX_WIDTH - 2) + '┤');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const STATUS_LEFT = 22; // inner width of left column
|
|
274
|
+
const STATUS_RIGHT = BOX_WIDTH - 2 - STATUS_LEFT - 1; // -2 borders, -1 middle divider
|
|
275
|
+
|
|
276
|
+
function statusRow(leftContent, rightContent) {
|
|
277
|
+
const lVis = boxVisLen(leftContent);
|
|
278
|
+
const rVis = boxVisLen(rightContent);
|
|
279
|
+
const lPad = Math.max(0, STATUS_LEFT - lVis);
|
|
280
|
+
const rPad = Math.max(0, STATUS_RIGHT - rVis);
|
|
281
|
+
return leftContent + ' '.repeat(lPad) + chalk.dim('│') + rightContent + ' '.repeat(rPad);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function statusDivider() {
|
|
285
|
+
return chalk.dim('─'.repeat(STATUS_LEFT) + '┼' + '─'.repeat(STATUS_RIGHT));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function renderStatus(pet) {
|
|
289
|
+
const mood = getMood(pet);
|
|
290
|
+
const moodEmoji = { sleeping: 'sleeping', tired: 'tired', sad: 'lonely', happy: 'happy!', normal: 'content' }[mood];
|
|
291
|
+
|
|
292
|
+
const lines = [
|
|
293
|
+
statusRow(
|
|
294
|
+
` ${chalk.bold(pet.name)}`,
|
|
295
|
+
` ${chalk.dim('vitals')}`,
|
|
296
|
+
),
|
|
297
|
+
statusRow(
|
|
298
|
+
` ${chalk.dim('age:')} ${formatAge(pet.age)}`,
|
|
299
|
+
` ${chalk.hex('#ff6b6b')('hunger')} ${statBar(pet.hunger, MAX_STAT, chalk.hex('#ff6b6b'))} ${String(pet.hunger).padStart(2)}/${MAX_STAT}`,
|
|
300
|
+
),
|
|
301
|
+
statusRow(
|
|
302
|
+
` ${chalk.dim('mood:')} ${moodEmoji}`,
|
|
303
|
+
` ${chalk.hex('#ffd93d')('happy')} ${statBar(pet.happiness, MAX_STAT, chalk.hex('#ffd93d'))} ${String(pet.happiness).padStart(2)}/${MAX_STAT}`,
|
|
304
|
+
),
|
|
305
|
+
statusRow(
|
|
306
|
+
` ${chalk.dim('tricks:')} ${(pet.tricks || []).filter(t => ALL_TRICKS.includes(t)).length}/${ALL_TRICKS.length}`,
|
|
307
|
+
` ${chalk.hex('#6bcb77')('energy')} ${statBar(pet.energy, MAX_STAT, chalk.hex('#6bcb77'))} ${String(pet.energy).padStart(2)}/${MAX_STAT}`,
|
|
308
|
+
),
|
|
309
|
+
];
|
|
310
|
+
return lines;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderActions(pet, activeKey = null) {
|
|
314
|
+
const knownTricks = (pet.tricks || []).filter(t => ALL_TRICKS.includes(t));
|
|
315
|
+
const allLearned = knownTricks.length >= ALL_TRICKS.length;
|
|
316
|
+
const items = [
|
|
317
|
+
{ key: 'f', label: 'feed' },
|
|
318
|
+
{ key: 'p', label: 'play' },
|
|
319
|
+
{ key: pet.sleeping ? 'w' : 's', label: pet.sleeping ? 'wake' : 'sleep' },
|
|
320
|
+
{ key: 'h', label: 'pet' },
|
|
321
|
+
{ key: 't', label: allLearned ? 'trick' : 'teach' },
|
|
322
|
+
];
|
|
323
|
+
const parts = items.map(({ key, label }) => {
|
|
324
|
+
const text = `[${key}] ${label}`;
|
|
325
|
+
return key === activeKey ? chalk.hex('#63D2FF')(text) : chalk.white(text);
|
|
326
|
+
});
|
|
327
|
+
return [` ${parts.join(' ')}`];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Reset ───────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
export function resetPet() {
|
|
333
|
+
const savePath = getSavePath();
|
|
334
|
+
try {
|
|
335
|
+
fs.unlinkSync(savePath);
|
|
336
|
+
console.log(chalk.dim(' Save data cleared. A new friend will hatch next time!'));
|
|
337
|
+
} catch {
|
|
338
|
+
console.log(chalk.dim(' No save data found.'));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Game loop ───────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
async function askSetup() {
|
|
345
|
+
const { printEyes } = await import('./eyes.js');
|
|
346
|
+
|
|
347
|
+
// Show the default eyes
|
|
348
|
+
console.log('');
|
|
349
|
+
printEyes('right', ' ');
|
|
350
|
+
console.log('');
|
|
351
|
+
|
|
352
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
353
|
+
const ask = (q) => new Promise((resolve) => {
|
|
354
|
+
rl.question(q, (answer) => resolve(answer.trim()));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Ask name
|
|
358
|
+
const name = (await ask(chalk.hex('#63D2FF')(' A new friend hatched! What will you name them? '))) || 'Eyes';
|
|
359
|
+
|
|
360
|
+
// Show color options
|
|
361
|
+
console.log('');
|
|
362
|
+
const colorKeys = Object.keys(palettes).filter(k => !palettes[k].hidden);
|
|
363
|
+
for (let i = 0; i < colorKeys.length; i++) {
|
|
364
|
+
const p = palettes[colorKeys[i]];
|
|
365
|
+
console.log(` ${chalk.hex(p.body)('██')} ${chalk.bold(`${i + 1}.`)} ${p.name}`);
|
|
366
|
+
}
|
|
367
|
+
console.log('');
|
|
368
|
+
|
|
369
|
+
const colorChoice = await ask(chalk.hex('#63D2FF')(` Pick a color (1-${colorKeys.length}): `));
|
|
370
|
+
const colorIndex = parseInt(colorChoice, 10) - 1;
|
|
371
|
+
const color = colorKeys[colorIndex] || 'blue';
|
|
372
|
+
|
|
373
|
+
rl.close();
|
|
374
|
+
return { name, color };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function startGame() {
|
|
378
|
+
let petState = loadPet();
|
|
379
|
+
let isNew = false;
|
|
380
|
+
|
|
381
|
+
if (!petState) {
|
|
382
|
+
isNew = true;
|
|
383
|
+
const { name, color } = await askSetup();
|
|
384
|
+
petState = newPet(name, color);
|
|
385
|
+
savePet(petState);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Apply saved color palette
|
|
389
|
+
if (petState.color) setPalette(petState.color);
|
|
390
|
+
|
|
391
|
+
petState = applyDecay(petState);
|
|
392
|
+
savePet(petState);
|
|
393
|
+
|
|
394
|
+
// Set up raw stdin for keypresses
|
|
395
|
+
if (!process.stdin.isTTY) {
|
|
396
|
+
console.log('Tamagotchi requires an interactive terminal.');
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
process.stdin.setRawMode(true);
|
|
401
|
+
process.stdin.resume();
|
|
402
|
+
process.stdin.setEncoding('utf8');
|
|
403
|
+
|
|
404
|
+
// Hide cursor
|
|
405
|
+
process.stdout.write('\x1b[?25l');
|
|
406
|
+
|
|
407
|
+
let message = isNew
|
|
408
|
+
? chalk.hex('#63D2FF')(`${petState.name} hatched! Take good care of them.`)
|
|
409
|
+
: chalk.hex('#63D2FF')(`Welcome back! ${petState.name} missed you.`);
|
|
410
|
+
let messageTimeout = null;
|
|
411
|
+
let activeAction = null;
|
|
412
|
+
let stopped = false;
|
|
413
|
+
let animFrame = 0;
|
|
414
|
+
let currentAnim = getAnimFrames(getMood(petState));
|
|
415
|
+
|
|
416
|
+
function resolveFrame(frameName) {
|
|
417
|
+
const isUp = frameName.endsWith(':up');
|
|
418
|
+
const isDown = frameName.endsWith(':down');
|
|
419
|
+
const expr = isUp ? frameName.slice(0, -3) : isDown ? frameName.slice(0, -5) : frameName;
|
|
420
|
+
const px = expressions[expr];
|
|
421
|
+
if (!px) return eyes('right');
|
|
422
|
+
const normal = eyes(expr);
|
|
423
|
+
const empty = ' '.repeat(7);
|
|
424
|
+
if (isUp) {
|
|
425
|
+
// Shift down: empty top, trim bottom
|
|
426
|
+
return [empty, ...normal.slice(0, -1)];
|
|
427
|
+
}
|
|
428
|
+
if (isDown) {
|
|
429
|
+
// Shift up: trim top, empty bottom
|
|
430
|
+
return [...normal.slice(1), empty];
|
|
431
|
+
}
|
|
432
|
+
return normal;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const INNER = BOX_WIDTH - 2;
|
|
436
|
+
const ICON_WIDTH = 14; // 7 pixels × 2 chars
|
|
437
|
+
const PAD_LEFT = Math.floor((INNER - ICON_WIDTH) / 2);
|
|
438
|
+
const PAD_RIGHT = INNER - ICON_WIDTH - PAD_LEFT;
|
|
439
|
+
|
|
440
|
+
// Measure visible length of an ANSI string (accounts for double-width emoji)
|
|
441
|
+
function visLen(s) {
|
|
442
|
+
const stripped = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
443
|
+
let len = 0;
|
|
444
|
+
for (const ch of stripped) {
|
|
445
|
+
const code = ch.codePointAt(0);
|
|
446
|
+
// Emoji (surrogate pairs / high codepoints) are 2 cols, most symbols are 1
|
|
447
|
+
len += code >= 0x1F000 ? 2 : 1;
|
|
448
|
+
}
|
|
449
|
+
return len;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Pad a string (possibly with ANSI) to a fixed visible width
|
|
453
|
+
function padRight(s, width) {
|
|
454
|
+
const diff = width - visLen(s);
|
|
455
|
+
return diff > 0 ? s + ' '.repeat(diff) : s;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function draw() {
|
|
459
|
+
const mood = getMood(petState);
|
|
460
|
+
currentAnim = getAnimFrames(mood);
|
|
461
|
+
const frameName = frameOverride || currentAnim.frames[animFrame % currentAnim.frames.length];
|
|
462
|
+
let iconLines = iconFlipped ? resolveFrame(frameName).slice().reverse() : resolveFrame(frameName);
|
|
463
|
+
if (iconShift > 0) {
|
|
464
|
+
const emptyLine = ' '.repeat(ICON_WIDTH);
|
|
465
|
+
for (let i = 0; i < iconShift; i++) iconLines = [emptyLine, ...iconLines.slice(0, -1)];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
process.stdout.write('\x1b[H\x1b[J');
|
|
469
|
+
|
|
470
|
+
// Gather overlays
|
|
471
|
+
const foodLines = getFoodLines();
|
|
472
|
+
const isSleeping = petState.sleeping;
|
|
473
|
+
const sleepStars = isSleeping ? getSleepScene(animFrame) : null;
|
|
474
|
+
const hearts = getHeartOverlay();
|
|
475
|
+
const sparkles = getPlayOverlay();
|
|
476
|
+
|
|
477
|
+
// Build scene rows: [left area] [icon centered] [right area]
|
|
478
|
+
const effLeft = PAD_LEFT + iconHShift;
|
|
479
|
+
const effRight = PAD_RIGHT - iconHShift;
|
|
480
|
+
const sceneRows = iconLines.map((iconLine, r) => {
|
|
481
|
+
let leftStr = ' '.repeat(effLeft);
|
|
482
|
+
let rightStr = ' '.repeat(effRight);
|
|
483
|
+
|
|
484
|
+
// Pick the active overlay (only one can be active at a time)
|
|
485
|
+
if (heartFrame >= 0 && hearts.left) {
|
|
486
|
+
leftStr = padRight(hearts.left[r] || '', PAD_LEFT);
|
|
487
|
+
rightStr = padRight(hearts.right[r] || '', PAD_RIGHT);
|
|
488
|
+
} else if (playFrame >= 0 && sparkles.left) {
|
|
489
|
+
leftStr = padRight(sparkles.left[r] || '', PAD_LEFT);
|
|
490
|
+
rightStr = padRight(sparkles.right[r] || '', PAD_RIGHT);
|
|
491
|
+
} else if (feedFrame >= 0 && foodLines && r < foodLines.length) {
|
|
492
|
+
rightStr = padRight(foodLines[r] || '', effRight);
|
|
493
|
+
} else if (sleepStars && sleepStars.left && r < sleepStars.left.length) {
|
|
494
|
+
leftStr = padRight(sleepStars.left[r] || '', PAD_LEFT);
|
|
495
|
+
rightStr = padRight(sleepStars.right[r] || '', PAD_RIGHT);
|
|
496
|
+
} else if (trickSceneOverlay && (trickSceneOverlay.right || trickSceneOverlay[r])) {
|
|
497
|
+
if (trickSceneOverlay.left) leftStr = padRight(trickSceneOverlay.left[r] || '', effLeft);
|
|
498
|
+
if (trickSceneOverlay.right) rightStr = padRight(trickSceneOverlay.right[r] || '', effRight);
|
|
499
|
+
if (!trickSceneOverlay.left && !trickSceneOverlay.right) rightStr = padRight(' ' + (trickSceneOverlay[r] || ''), effRight);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return leftStr + iconLine + rightStr;
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Status + actions
|
|
506
|
+
const statusLines = renderStatus(petState);
|
|
507
|
+
const actionLines = renderActions(petState, activeAction);
|
|
508
|
+
|
|
509
|
+
// Petting hand overlay — two rows: palm + fingers, swaying
|
|
510
|
+
function getPetHand() {
|
|
511
|
+
if (petFrame < 0) return [];
|
|
512
|
+
const offsets = [0, 1, 2, 3, 2, 1, 0, -1, -2, -1, 0, 1, 2];
|
|
513
|
+
const offset = offsets[petFrame % offsets.length];
|
|
514
|
+
const pos = PAD_LEFT + 2 + offset;
|
|
515
|
+
const pad = ' '.repeat(Math.max(0, pos));
|
|
516
|
+
const skin = chalk.hex('#FFCC88');
|
|
517
|
+
return [
|
|
518
|
+
pad + skin(' ╷╷╷╷'),
|
|
519
|
+
pad + skin('╭┴┴┴┴╮'),
|
|
520
|
+
];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Render with box border
|
|
524
|
+
const footer = chalk.dim('[r] reset [q] quit');
|
|
525
|
+
const footerVis = '[r] reset [q] quit'.length;
|
|
526
|
+
console.log('');
|
|
527
|
+
console.log(' ' + ' '.repeat(BOX_WIDTH - 2 - footerVis + 1) + footer);
|
|
528
|
+
console.log(' ' + boxTop());
|
|
529
|
+
const sceneBg = petState.sleeping ? '#05051a' : null;
|
|
530
|
+
const handLines = getPetHand();
|
|
531
|
+
if (handLines.length) {
|
|
532
|
+
// Hand takes 2 rows, so skip the empty top row and trim the last scene row
|
|
533
|
+
console.log(' ' + boxLine(handLines[0], sceneBg));
|
|
534
|
+
console.log(' ' + boxLine(handLines[1], sceneBg));
|
|
535
|
+
for (let i = 0; i < sceneRows.length - 1; i++) {
|
|
536
|
+
console.log(' ' + boxLine(sceneRows[i], sceneBg));
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
console.log(' ' + boxLine('', sceneBg));
|
|
540
|
+
for (const row of sceneRows) {
|
|
541
|
+
console.log(' ' + boxLine(row, sceneBg));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Message centered between scene and vitals
|
|
545
|
+
const inner = BOX_WIDTH - 2;
|
|
546
|
+
let displayMsg = message || '';
|
|
547
|
+
if (boxVisLen(displayMsg) > inner) {
|
|
548
|
+
// Strip ANSI, truncate, re-apply no color (plain truncation)
|
|
549
|
+
const plain = displayMsg.replace(/\x1b\[[0-9;]*m/g, '');
|
|
550
|
+
displayMsg = plain.slice(0, inner - 1) + '…';
|
|
551
|
+
}
|
|
552
|
+
const msgVis = boxVisLen(displayMsg);
|
|
553
|
+
const msgPadL = Math.max(0, Math.floor((inner - msgVis) / 2));
|
|
554
|
+
const msgPadR = Math.max(0, inner - msgVis - msgPadL);
|
|
555
|
+
console.log(' ' + boxDivider());
|
|
556
|
+
console.log(' ' + chalk.dim('│') + ' '.repeat(msgPadL) + displayMsg + ' '.repeat(msgPadR) + chalk.dim('│'));
|
|
557
|
+
console.log(' ' + chalk.dim('├' + '─'.repeat(STATUS_LEFT) + '┬' + '─'.repeat(STATUS_RIGHT) + '┤'));
|
|
558
|
+
for (const line of statusLines) {
|
|
559
|
+
console.log(' ' + boxLine(line));
|
|
560
|
+
}
|
|
561
|
+
console.log(' ' + chalk.dim('├' + '─'.repeat(STATUS_LEFT) + '┴' + '─'.repeat(STATUS_RIGHT) + '┤'));
|
|
562
|
+
for (const line of actionLines) {
|
|
563
|
+
console.log(' ' + boxLine(line));
|
|
564
|
+
}
|
|
565
|
+
console.log(' ' + boxBottom());
|
|
566
|
+
if (confirmingReset) {
|
|
567
|
+
console.log('');
|
|
568
|
+
console.log(' ' + chalk.hex('#ff6b6b')(`Reset ${petState.name}? They'll be gone forever!`) + chalk.dim(' [y] yes [n] no'));
|
|
569
|
+
} else {
|
|
570
|
+
console.log('');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function clearMessage(delay = 3000) {
|
|
575
|
+
if (messageTimeout) clearTimeout(messageTimeout);
|
|
576
|
+
messageTimeout = setTimeout(() => {
|
|
577
|
+
message = '';
|
|
578
|
+
draw();
|
|
579
|
+
}, delay);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ── Sleep scene ──────────────────────────────────────
|
|
583
|
+
function getSleepScene(frame) {
|
|
584
|
+
// Stars on both sides of the centered icon
|
|
585
|
+
function starChar(seed) {
|
|
586
|
+
const t = (frame + seed) % 6;
|
|
587
|
+
if (t < 2) return chalk.hex('#FFD700')('·');
|
|
588
|
+
if (t < 4) return chalk.hex('#FFF8DC')('*');
|
|
589
|
+
return chalk.dim('·');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const leftGrid = [
|
|
593
|
+
[{ col: 3, seed: 0 }, { col: PAD_LEFT - 4, seed: 8 }],
|
|
594
|
+
[{ col: 7, seed: 5 }],
|
|
595
|
+
[{ col: 2, seed: 2 }, { col: PAD_LEFT - 3, seed: 9 }],
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
const rightGrid = [
|
|
599
|
+
[{ col: 2, seed: 3 }, { col: Math.min(10, PAD_RIGHT - 3), seed: 6 }],
|
|
600
|
+
[{ col: 5, seed: 1 }, { col: Math.min(12, PAD_RIGHT - 4), seed: 10 }],
|
|
601
|
+
[{ col: 1, seed: 7 }, { col: Math.min(8, PAD_RIGHT - 2), seed: 4 }],
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
const leftLines = leftGrid.map(row => {
|
|
605
|
+
const cells = new Array(PAD_LEFT).fill(' ');
|
|
606
|
+
for (const star of row) {
|
|
607
|
+
if (star.col >= 0 && star.col < PAD_LEFT) cells[star.col] = starChar(star.seed);
|
|
608
|
+
}
|
|
609
|
+
return cells.join('');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const rightLines = rightGrid.map((row, ri) => {
|
|
613
|
+
const cells = new Array(PAD_RIGHT).fill(' ');
|
|
614
|
+
for (const star of row) {
|
|
615
|
+
if (star.col >= 0 && star.col < PAD_RIGHT) cells[star.col] = starChar(star.seed);
|
|
616
|
+
}
|
|
617
|
+
// Moon on first row, right side (emoji is 2 chars wide, so use col and blank the next)
|
|
618
|
+
if (ri === 0) {
|
|
619
|
+
const moonCol = Math.min(PAD_RIGHT - 3, 13);
|
|
620
|
+
cells[moonCol] = '🌙';
|
|
621
|
+
if (moonCol + 1 < PAD_RIGHT) cells[moonCol + 1] = '';
|
|
622
|
+
}
|
|
623
|
+
return cells.join('');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
return { left: leftLines, right: rightLines };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let feedAnimating = false;
|
|
630
|
+
let feedFrame = -1;
|
|
631
|
+
const allFood = ['🍕', '🌮', '🍎', '🧀', '🍪', '🥐', '🍣', '🍩', '🍔', '🌯', '🥨', '🍇', '🥕', '🍰'];
|
|
632
|
+
let feedEmojis = [];
|
|
633
|
+
|
|
634
|
+
function getFoodLines() {
|
|
635
|
+
if (feedFrame < 0) return null;
|
|
636
|
+
// 3 food items scrolling right-to-left across the full right area
|
|
637
|
+
const maxPos = PAD_RIGHT;
|
|
638
|
+
const lines = [];
|
|
639
|
+
for (let row = 0; row < 3; row++) {
|
|
640
|
+
const stagger = row * 3;
|
|
641
|
+
const pos = feedFrame - stagger;
|
|
642
|
+
if (pos >= 0 && pos < maxPos) {
|
|
643
|
+
const spaces = Math.max(0, maxPos - pos - 2);
|
|
644
|
+
lines.push(' '.repeat(spaces) + feedEmojis[row]);
|
|
645
|
+
} else {
|
|
646
|
+
lines.push('');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return lines;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function animateFeed() {
|
|
653
|
+
feedAnimating = true;
|
|
654
|
+
// Pick 3 random foods
|
|
655
|
+
feedEmojis = [];
|
|
656
|
+
const pool = [...allFood];
|
|
657
|
+
for (let i = 0; i < 3; i++) {
|
|
658
|
+
const idx = Math.floor(Math.random() * pool.length);
|
|
659
|
+
feedEmojis.push(pool.splice(idx, 1)[0]);
|
|
660
|
+
}
|
|
661
|
+
const totalFrames = PAD_RIGHT + 8; // enough for last food to cross fully
|
|
662
|
+
for (feedFrame = 0; feedFrame <= totalFrames; feedFrame++) {
|
|
663
|
+
draw();
|
|
664
|
+
await new Promise(r => setTimeout(r, 100));
|
|
665
|
+
}
|
|
666
|
+
feedFrame = -1;
|
|
667
|
+
draw();
|
|
668
|
+
feedAnimating = false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let petAnimating = false;
|
|
672
|
+
let petFrame = -1;
|
|
673
|
+
let heartFrame = -1;
|
|
674
|
+
|
|
675
|
+
// Hearts float upward: 3 hearts at staggered positions
|
|
676
|
+
function getHeartOverlay() {
|
|
677
|
+
if (heartFrame < 0) return { left: null, right: null };
|
|
678
|
+
const hearts = [
|
|
679
|
+
{ startFrame: 0, side: 'right', startRow: 2, offset: 2 },
|
|
680
|
+
{ startFrame: 1, side: 'left', startRow: 2, offset: 2 },
|
|
681
|
+
{ startFrame: 3, side: 'right', startRow: 2, offset: 6 },
|
|
682
|
+
{ startFrame: 4, side: 'left', startRow: 1, offset: 5 },
|
|
683
|
+
{ startFrame: 6, side: 'right', startRow: 2, offset: 10 },
|
|
684
|
+
{ startFrame: 7, side: 'left', startRow: 2, offset: 9 },
|
|
685
|
+
];
|
|
686
|
+
const leftLines = ['', '', '', ''];
|
|
687
|
+
const rightLines = ['', '', '', ''];
|
|
688
|
+
for (const h of hearts) {
|
|
689
|
+
const age = heartFrame - h.startFrame;
|
|
690
|
+
if (age < 0 || age > 4) continue;
|
|
691
|
+
const row = h.startRow - age;
|
|
692
|
+
if (row < 0 || row > 3) continue;
|
|
693
|
+
const heart = age < 2 ? chalk.hex('#ff6b9d')('♥') : chalk.hex('#ff6b9d').dim('♥');
|
|
694
|
+
if (h.side === 'left') {
|
|
695
|
+
leftLines[row] = ' '.repeat(Math.max(0, PAD_LEFT - h.offset - 1)) + heart + ' '.repeat(h.offset);
|
|
696
|
+
} else {
|
|
697
|
+
rightLines[row] = ' '.repeat(h.offset) + heart;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return { left: leftLines, right: rightLines };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function animatePet() {
|
|
704
|
+
petAnimating = true;
|
|
705
|
+
// Phase 1: petting hand sways
|
|
706
|
+
for (petFrame = 0; petFrame <= 10; petFrame++) {
|
|
707
|
+
draw();
|
|
708
|
+
await new Promise(r => setTimeout(r, 120));
|
|
709
|
+
}
|
|
710
|
+
petFrame = -1;
|
|
711
|
+
// Phase 2: hearts float up
|
|
712
|
+
for (heartFrame = 0; heartFrame <= 12; heartFrame++) {
|
|
713
|
+
draw();
|
|
714
|
+
await new Promise(r => setTimeout(r, 120));
|
|
715
|
+
}
|
|
716
|
+
heartFrame = -1;
|
|
717
|
+
draw();
|
|
718
|
+
petAnimating = false;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
let playAnimating = false;
|
|
722
|
+
let playFrame = -1;
|
|
723
|
+
const confettiColors = ['#FF6B6B', '#FFD93D', '#6BCB77', '#4D96FF', '#FF6BD6', '#63D2FF'];
|
|
724
|
+
|
|
725
|
+
function getPlayOverlay() {
|
|
726
|
+
if (playFrame < 0) return { left: null, right: null };
|
|
727
|
+
// Build cell arrays for left and right, then render with chalk
|
|
728
|
+
const leftCells = Array.from({ length: 4 }, () => new Array(PAD_LEFT).fill(null));
|
|
729
|
+
const rightCells = Array.from({ length: 4 }, () => new Array(PAD_RIGHT).fill(null));
|
|
730
|
+
const spots = [
|
|
731
|
+
{ r: 0, side: 'left', seed: 0, col: 3 },
|
|
732
|
+
{ r: 0, side: 'right', seed: 2, col: 4 },
|
|
733
|
+
{ r: 0, side: 'left', seed: 7, col: 12 },
|
|
734
|
+
{ r: 0, side: 'right', seed: 8, col: 12 },
|
|
735
|
+
{ r: 1, side: 'left', seed: 1, col: 5 },
|
|
736
|
+
{ r: 1, side: 'right', seed: 4, col: 7 },
|
|
737
|
+
{ r: 1, side: 'left', seed: 6, col: 14 },
|
|
738
|
+
{ r: 1, side: 'right', seed: 9, col: 14 },
|
|
739
|
+
{ r: 2, side: 'left', seed: 3, col: 2 },
|
|
740
|
+
{ r: 2, side: 'right', seed: 5, col: 3 },
|
|
741
|
+
{ r: 2, side: 'right', seed: 10, col: 10 },
|
|
742
|
+
];
|
|
743
|
+
for (const s of spots) {
|
|
744
|
+
const visible = (playFrame + s.seed) % 3 !== 0;
|
|
745
|
+
if (!visible) continue;
|
|
746
|
+
const color = confettiColors[(playFrame + s.seed) % confettiColors.length];
|
|
747
|
+
const ch = (playFrame + s.seed) % 2 === 0 ? '✦' : '·';
|
|
748
|
+
const cells = s.side === 'left' ? leftCells[s.r] : rightCells[s.r];
|
|
749
|
+
if (s.col < cells.length) cells[s.col] = chalk.hex(color)(ch);
|
|
750
|
+
}
|
|
751
|
+
const toStr = (cells) => cells.map(c => c || ' ').join('');
|
|
752
|
+
return {
|
|
753
|
+
left: leftCells.map(toStr),
|
|
754
|
+
right: rightCells.map(toStr),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function animatePlay() {
|
|
759
|
+
playAnimating = true;
|
|
760
|
+
const bounceFrames = ['right:up', 'right:up', 'right', 'right', 'right:up', 'right:up', 'right', 'right', 'right:up', 'right:up', 'right', 'right', 'right'];
|
|
761
|
+
for (playFrame = 0; playFrame <= 12; playFrame++) {
|
|
762
|
+
frameOverride = bounceFrames[playFrame % bounceFrames.length];
|
|
763
|
+
draw();
|
|
764
|
+
await new Promise(r => setTimeout(r, 180));
|
|
765
|
+
}
|
|
766
|
+
playFrame = -1;
|
|
767
|
+
frameOverride = null;
|
|
768
|
+
draw();
|
|
769
|
+
playAnimating = false;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
let teachAnimating = false;
|
|
773
|
+
|
|
774
|
+
async function animateTrick(name) {
|
|
775
|
+
const wait = (ms) => new Promise(r => setTimeout(r, ms));
|
|
776
|
+
const setFrame = async (f, ms) => { frameOverride = f; draw(); await wait(ms); };
|
|
777
|
+
|
|
778
|
+
switch (name) {
|
|
779
|
+
case 'hide and seek': {
|
|
780
|
+
// sink down progressively
|
|
781
|
+
for (const shift of [0, 1, 2, 3]) {
|
|
782
|
+
iconShift = shift;
|
|
783
|
+
await setFrame('right', 180);
|
|
784
|
+
}
|
|
785
|
+
// peek — bob up slightly then back down
|
|
786
|
+
iconShift = 2; await setFrame('right', 200);
|
|
787
|
+
iconShift = 3; await setFrame('right', 300);
|
|
788
|
+
iconShift = 2; await setFrame('right', 150);
|
|
789
|
+
iconShift = 3; await setFrame('right', 400);
|
|
790
|
+
// come back up
|
|
791
|
+
for (const shift of [2, 1, 0]) {
|
|
792
|
+
iconShift = shift;
|
|
793
|
+
await setFrame('right', 180);
|
|
794
|
+
}
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
case 'spin': {
|
|
798
|
+
for (const f of ['left', 'up-left', 'up-right', 'right', 'up-right', 'up-left', 'left', 'up-left', 'right'])
|
|
799
|
+
await setFrame(f, 110);
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
case 'fetch': {
|
|
803
|
+
iconHShift = -8;
|
|
804
|
+
const method = chalk.hex('#63D2FF')('POST');
|
|
805
|
+
const pending = chalk.dim('○') + ' ' + method + ' /api/pets';
|
|
806
|
+
const d = chalk.dim;
|
|
807
|
+
const json = [
|
|
808
|
+
d(' "id": 1,'),
|
|
809
|
+
d(' "name": "Max",'),
|
|
810
|
+
d(' "type": "dog",'),
|
|
811
|
+
d(' "breed": "husky",'),
|
|
812
|
+
d(' "age": 3,'),
|
|
813
|
+
d(' "tag": "friendly"'),
|
|
814
|
+
];
|
|
815
|
+
|
|
816
|
+
// show request
|
|
817
|
+
trickSceneOverlay = [pending, '', '', ''];
|
|
818
|
+
await setFrame('up-right', 300);
|
|
819
|
+
|
|
820
|
+
// loading dots
|
|
821
|
+
for (const dots of [' ·', ' · ·', ' · · ·', ' · · · ·', ' · · ·', ' · ·', ' · · ·', ' · · · ·']) {
|
|
822
|
+
trickSceneOverlay = [pending, chalk.dim(dots), '', ''];
|
|
823
|
+
await setFrame('up-right', 180);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// json scrolls in line by line (pending pinned at top, 3 visible body rows)
|
|
827
|
+
const body = [d('{'), ...json, d('}')];
|
|
828
|
+
for (let i = 0; i < body.length; i++) {
|
|
829
|
+
const win = body.slice(Math.max(0, i - 2), i + 1);
|
|
830
|
+
while (win.length < 3) win.push('');
|
|
831
|
+
trickSceneOverlay = [pending, win[0], win[1], win[2]];
|
|
832
|
+
const expr = i % 2 === 0 ? 'up-left' : 'up-right';
|
|
833
|
+
await setFrame(expr, 200);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// 200 OK — hold on last 3 body lines
|
|
837
|
+
const tail = body.slice(-3);
|
|
838
|
+
trickSceneOverlay = [chalk.hex('#6bcb77')('● 200 OK'), tail[0], tail[1], tail[2]];
|
|
839
|
+
await setFrame('right', 700);
|
|
840
|
+
|
|
841
|
+
trickSceneOverlay = null;
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
case 'bow': {
|
|
845
|
+
for (const [f, ms] of [['right', 200], ['right:up', 300], ['right:up', 300], ['squint', 500], ['right:up', 300], ['right', 200]])
|
|
846
|
+
await setFrame(f, ms);
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
case 'chameleon': {
|
|
850
|
+
const colorKeys = Object.keys(palettes).filter(k => !palettes[k].hidden);
|
|
851
|
+
for (const color of [...colorKeys, ...colorKeys, petState.color]) {
|
|
852
|
+
setPalette(color);
|
|
853
|
+
await setFrame('right', 200);
|
|
854
|
+
}
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
case 'dance': {
|
|
858
|
+
const notes = ['♩', '♪', '♫', '♬'];
|
|
859
|
+
const nc = chalk.hex('#ffd93d');
|
|
860
|
+
const place = (width, col, note) => ' '.repeat(Math.min(col, width - 1)) + nc(note) + ' '.repeat(Math.max(0, width - col - 1));
|
|
861
|
+
// Each frame: scattered note positions on left and right, shifting each beat
|
|
862
|
+
const frames = ['left:up', 'left', 'right:up', 'right', 'left:up', 'left', 'right:up', 'right', 'right:up', 'right'];
|
|
863
|
+
const spots = [
|
|
864
|
+
{ lr: [14, 3], ll: [2, 11], rr: [4, 15], rl: [8, 1] },
|
|
865
|
+
{ lr: [6, 16], ll: [9, 4], rr: [12, 2], rl: [14, 7] },
|
|
866
|
+
];
|
|
867
|
+
for (let i = 0; i < frames.length; i++) {
|
|
868
|
+
const s = spots[i % 2];
|
|
869
|
+
const ni = (k) => notes[(i + k) % notes.length];
|
|
870
|
+
trickSceneOverlay = {
|
|
871
|
+
left: [place(PAD_LEFT, s.ll[0], ni(0)), place(PAD_LEFT, s.ll[1], ni(2)), place(PAD_LEFT, s.lr[0], ni(1)), place(PAD_LEFT, s.lr[1], ni(3))],
|
|
872
|
+
right: [place(PAD_RIGHT, s.rr[0], ni(2)), place(PAD_RIGHT, s.rl[0], ni(0)), place(PAD_RIGHT, s.rr[1], ni(3)), place(PAD_RIGHT, s.rl[1], ni(1))],
|
|
873
|
+
};
|
|
874
|
+
await setFrame(frames[i], 130);
|
|
875
|
+
}
|
|
876
|
+
trickSceneOverlay = null;
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
case 'owl impression': {
|
|
880
|
+
setPalette('owl');
|
|
881
|
+
const hoot = chalk.hex('#ffd93d');
|
|
882
|
+
for (const [f, ms] of [['left', 200], ['right', 200], ['left', 200], ['right', 200]])
|
|
883
|
+
await setFrame(f, ms);
|
|
884
|
+
const rpad = () => ' '.repeat(2 + Math.floor(Math.random() * 8));
|
|
885
|
+
trickSceneOverlay = { right: [rpad() + hoot('hoot!'), '', '', ''] };
|
|
886
|
+
await setFrame('up-right', 500);
|
|
887
|
+
trickSceneOverlay = { right: [rpad() + hoot('hoot!'), rpad() + hoot('hoot!'), '', ''] };
|
|
888
|
+
await setFrame('up-left', 600);
|
|
889
|
+
trickSceneOverlay = null;
|
|
890
|
+
await setFrame('right', 300);
|
|
891
|
+
setPalette(petState.color);
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
case 'writes an OAS file': {
|
|
895
|
+
iconHShift = -8;
|
|
896
|
+
const c = chalk.dim;
|
|
897
|
+
const groups = [
|
|
898
|
+
[c('openapi: 3.0.0'), c('info:'), c(' title: Petstore'), c(' version: 1.0.0')],
|
|
899
|
+
[c(' contact:'), c(' name: Petstore'), c(' url: petstore'), c(' license: MIT')],
|
|
900
|
+
[c('paths:'), c(' /pets:'), c(' get: listPets'), c(' post: createPet')],
|
|
901
|
+
[c(' /pets/{petId}:'), c(' get: getPetById'), c(' put: updatePet'), c(' delete: delPet')],
|
|
902
|
+
[c('components:'), c(' schemas:'), c(' Pet:'), c(' type: object')],
|
|
903
|
+
[c(' properties:'), c(' id: integer'), c(' name: string'), c(' tag: string')],
|
|
904
|
+
];
|
|
905
|
+
const exprs = ['up-right', 'up-left', 'up-right', 'up-left', 'up-right', 'up-left'];
|
|
906
|
+
for (let i = 0; i < groups.length; i++) {
|
|
907
|
+
trickSceneOverlay = groups[i];
|
|
908
|
+
await setFrame(exprs[i], 450);
|
|
909
|
+
}
|
|
910
|
+
trickSceneOverlay = null;
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
default: {
|
|
914
|
+
for (const f of ['right:up', 'right', 'right:up', 'right'])
|
|
915
|
+
await setFrame(f, 150);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
frameOverride = null;
|
|
919
|
+
iconFlipped = false;
|
|
920
|
+
iconShift = 0;
|
|
921
|
+
iconHShift = 0;
|
|
922
|
+
trickSceneOverlay = null;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function animateTeach(trickName) {
|
|
926
|
+
teachAnimating = true;
|
|
927
|
+
// Thinking phase
|
|
928
|
+
for (const expr of ['right', 'up-left', 'up-right', 'up-left', 'right']) {
|
|
929
|
+
frameOverride = expr;
|
|
930
|
+
draw();
|
|
931
|
+
await new Promise(r => setTimeout(r, 300));
|
|
932
|
+
}
|
|
933
|
+
frameOverride = null;
|
|
934
|
+
|
|
935
|
+
if (trickName) {
|
|
936
|
+
await animateTrick(trickName);
|
|
937
|
+
} else {
|
|
938
|
+
// Didn't learn: squint then back
|
|
939
|
+
frameOverride = 'squint';
|
|
940
|
+
draw();
|
|
941
|
+
await new Promise(r => setTimeout(r, 400));
|
|
942
|
+
frameOverride = null;
|
|
943
|
+
}
|
|
944
|
+
draw();
|
|
945
|
+
teachAnimating = false;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
let frameOverride = null;
|
|
949
|
+
let iconFlipped = false;
|
|
950
|
+
let iconShift = 0; // rows to shift icon downward (for hiding)
|
|
951
|
+
let iconHShift = 0; // columns to shift icon left (negative = left, increases right area)
|
|
952
|
+
let trickSceneOverlay = null; // array of strings per scene row (right side)
|
|
953
|
+
|
|
954
|
+
async function animateSleep(falling) {
|
|
955
|
+
// falling = true: eyes close. false: eyes open.
|
|
956
|
+
const sequence = falling
|
|
957
|
+
? ['right', 'half-blink', 'squint', 'closed']
|
|
958
|
+
: ['closed', 'squint', 'half-blink', 'right'];
|
|
959
|
+
for (const expr of sequence) {
|
|
960
|
+
frameOverride = expr;
|
|
961
|
+
draw();
|
|
962
|
+
await new Promise(r => setTimeout(r, 120));
|
|
963
|
+
}
|
|
964
|
+
frameOverride = null;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function doPerformTrick(name) {
|
|
968
|
+
if (actionBusy) return;
|
|
969
|
+
message = `${petState.name} ${trickPhrase(name)}!`;
|
|
970
|
+
actionBusy = true;
|
|
971
|
+
animateTrick(name).then(() => {
|
|
972
|
+
draw();
|
|
973
|
+
actionBusy = false;
|
|
974
|
+
clearMessage();
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const actionKeyMap = new Map([[feed, 'f'], [play, 'p'], [petAction, 'h'], [teach, 't']]);
|
|
979
|
+
function getnapKey() { return petState.sleeping ? 'w' : 's'; }
|
|
980
|
+
|
|
981
|
+
function doAction(actionFn) {
|
|
982
|
+
if (actionBusy) return;
|
|
983
|
+
const tricksBefore = (petState.tricks || []).length;
|
|
984
|
+
const result = actionFn(petState);
|
|
985
|
+
message = result.ok ? result.message : chalk.hex('#ff9540')(result.message);
|
|
986
|
+
petState.lastVisit = Date.now();
|
|
987
|
+
savePet(petState);
|
|
988
|
+
animFrame = 0;
|
|
989
|
+
actionBusy = true;
|
|
990
|
+
activeAction = actionFn === nap ? getnapKey() : (actionKeyMap.get(actionFn) || null);
|
|
991
|
+
|
|
992
|
+
function done() {
|
|
993
|
+
activeAction = null;
|
|
994
|
+
actionBusy = false;
|
|
995
|
+
clearMessage();
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (!result.ok) {
|
|
999
|
+
draw();
|
|
1000
|
+
clearMessage();
|
|
1001
|
+
done();
|
|
1002
|
+
} else if (actionFn === nap) {
|
|
1003
|
+
// Only nap/wake gets the eye open/close animation
|
|
1004
|
+
animateSleep(petState.sleeping).then(() => { draw(); done(); });
|
|
1005
|
+
} else if (actionFn === feed) {
|
|
1006
|
+
animateFeed().then(done);
|
|
1007
|
+
} else if (actionFn === petAction) {
|
|
1008
|
+
animatePet().then(done);
|
|
1009
|
+
} else if (actionFn === play) {
|
|
1010
|
+
animatePlay().then(done);
|
|
1011
|
+
} else if (actionFn === teach) {
|
|
1012
|
+
const tricksAfter = (petState.tricks || []).length;
|
|
1013
|
+
const learnedTrick = tricksAfter > tricksBefore
|
|
1014
|
+
? petState.tricks[petState.tricks.length - 1]
|
|
1015
|
+
: (result.trick || null);
|
|
1016
|
+
const learned = tricksAfter > tricksBefore;
|
|
1017
|
+
animateTeach(learnedTrick).then(() => {
|
|
1018
|
+
activeAction = null;
|
|
1019
|
+
actionBusy = false;
|
|
1020
|
+
clearMessage(learned ? 6000 : 3000);
|
|
1021
|
+
});
|
|
1022
|
+
} else {
|
|
1023
|
+
draw();
|
|
1024
|
+
done();
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function cleanup() {
|
|
1029
|
+
stopped = true;
|
|
1030
|
+
process.stdout.write('\x1b[?25h'); // show cursor
|
|
1031
|
+
process.stdin.setRawMode(false);
|
|
1032
|
+
process.stdin.pause();
|
|
1033
|
+
savePet(petState);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Input handler
|
|
1037
|
+
process.stdin.on('data', (key) => {
|
|
1038
|
+
if (stopped) return;
|
|
1039
|
+
|
|
1040
|
+
if (confirmingReset) {
|
|
1041
|
+
if (key === 'y' || key === 'Y') {
|
|
1042
|
+
cleanup();
|
|
1043
|
+
const savePath = getSavePath();
|
|
1044
|
+
try { fs.unlinkSync(savePath); } catch {}
|
|
1045
|
+
console.log('');
|
|
1046
|
+
console.log(` ${chalk.dim('Save data cleared. A new friend will hatch next time!')}`);
|
|
1047
|
+
console.log('');
|
|
1048
|
+
process.exit();
|
|
1049
|
+
} else {
|
|
1050
|
+
confirmingReset = false;
|
|
1051
|
+
message = chalk.dim('Reset cancelled.');
|
|
1052
|
+
draw();
|
|
1053
|
+
clearMessage();
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
switch (key) {
|
|
1059
|
+
case 'f':
|
|
1060
|
+
doAction(feed);
|
|
1061
|
+
break;
|
|
1062
|
+
case 'p':
|
|
1063
|
+
doAction(play);
|
|
1064
|
+
break;
|
|
1065
|
+
case 's':
|
|
1066
|
+
case 'w':
|
|
1067
|
+
doAction(nap);
|
|
1068
|
+
break;
|
|
1069
|
+
case 'h':
|
|
1070
|
+
doAction(petAction);
|
|
1071
|
+
break;
|
|
1072
|
+
case 't':
|
|
1073
|
+
doAction(teach);
|
|
1074
|
+
break;
|
|
1075
|
+
case '1': case '2': case '3': case '4':
|
|
1076
|
+
case '5': case '6': case '7': case '8': {
|
|
1077
|
+
const idx = parseInt(key) - 1;
|
|
1078
|
+
const trickName = ALL_TRICKS[idx];
|
|
1079
|
+
const known = (petState.tricks || []).filter(t => ALL_TRICKS.includes(t));
|
|
1080
|
+
if (trickName && known.includes(trickName)) {
|
|
1081
|
+
doPerformTrick(trickName);
|
|
1082
|
+
} else if (trickName) {
|
|
1083
|
+
message = chalk.dim(`${petState.name} hasn't learned ${trickName} yet`);
|
|
1084
|
+
draw();
|
|
1085
|
+
clearMessage();
|
|
1086
|
+
}
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
case 'r':
|
|
1090
|
+
confirmingReset = true;
|
|
1091
|
+
message = '';
|
|
1092
|
+
draw();
|
|
1093
|
+
break;
|
|
1094
|
+
case 'q':
|
|
1095
|
+
case '\u0003': // Ctrl+C
|
|
1096
|
+
message = chalk.dim(`${petState.name} waves goodbye!`);
|
|
1097
|
+
draw();
|
|
1098
|
+
cleanup();
|
|
1099
|
+
process.exit();
|
|
1100
|
+
break;
|
|
1101
|
+
default:
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
let confirmingReset = false;
|
|
1107
|
+
let actionBusy = false;
|
|
1108
|
+
|
|
1109
|
+
// Animation + decay loop
|
|
1110
|
+
async function gameLoop() {
|
|
1111
|
+
while (!stopped) {
|
|
1112
|
+
if (!actionBusy) {
|
|
1113
|
+
draw();
|
|
1114
|
+
const duration = currentAnim.durations[animFrame % currentAnim.durations.length];
|
|
1115
|
+
await new Promise(r => setTimeout(r, duration));
|
|
1116
|
+
animFrame++;
|
|
1117
|
+
} else {
|
|
1118
|
+
await new Promise(r => setTimeout(r, 100));
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Apply passive decay every loop cycle
|
|
1122
|
+
const now = Date.now();
|
|
1123
|
+
const elapsed = now - petState.lastVisit;
|
|
1124
|
+
if (elapsed >= DECAY_INTERVAL_MS) {
|
|
1125
|
+
const moodBefore = getMood(petState);
|
|
1126
|
+
petState = applyDecay(petState);
|
|
1127
|
+
savePet(petState);
|
|
1128
|
+
const moodAfter = getMood(petState);
|
|
1129
|
+
if (moodBefore !== moodAfter) {
|
|
1130
|
+
if (moodAfter === 'tired') message = chalk.hex('#63D2FF')(`${petState.name} is getting sleepy...`);
|
|
1131
|
+
else if (moodAfter === 'sad' && petState.hunger <= 1) message = chalk.hex('#ff6b6b')(`${petState.name} is hungry!`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
await gameLoop();
|
|
1139
|
+
}
|