@whaletech/pet 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +71 -0
  2. package/dist/main.js +4802 -0
  3. package/package.json +46 -0
package/dist/main.js ADDED
@@ -0,0 +1,4802 @@
1
+ #!/usr/bin/env node
2
+ // src/main.tsx
3
+ import { useState as useState10, useCallback as useCallback2 } from "react";
4
+ import { render } from "ink";
5
+
6
+ // src/storage.ts
7
+ import * as fs from "node:fs/promises";
8
+ import { constants as fsConstants } from "node:fs";
9
+ import * as path from "node:path";
10
+ import * as os from "node:os";
11
+ import * as crypto from "node:crypto";
12
+
13
+ // src/core/petState.ts
14
+ function createDefaultNeeds() {
15
+ return {
16
+ belly: 80,
17
+ energy: 80,
18
+ affinity: 80,
19
+ bond: 0
20
+ };
21
+ }
22
+ function createInitialWhaleState() {
23
+ return {
24
+ mood: "calm",
25
+ needs: createDefaultNeeds()
26
+ };
27
+ }
28
+ function timeBucketHash(seed) {
29
+ let h = seed | 0;
30
+ h = (h >>> 16 ^ h) * 73244475;
31
+ h = (h >>> 16 ^ h) * 73244475;
32
+ h = h >>> 16 ^ h;
33
+ return (h >>> 0) / 4294967295;
34
+ }
35
+ var MID_MOODS = [
36
+ { mood: "calm", baseWeight: 3 },
37
+ { mood: "happy", baseWeight: 2, boostWhen: (n) => n.affinity >= 60 ? 3 : 0 },
38
+ { mood: "cozy", baseWeight: 2, boostWhen: (n) => n.belly >= 70 ? 2 : 0 },
39
+ { mood: "curious", baseWeight: 2, boostWhen: (n) => n.bond >= 40 ? 2 : 0 },
40
+ { mood: "playful", baseWeight: 2, boostWhen: (n) => n.energy >= 60 ? 2 : 0 },
41
+ { mood: "dreamy", baseWeight: 1, boostWhen: (n) => n.energy <= 40 ? 2 : 0 },
42
+ { mood: "mischievous", baseWeight: 1, boostWhen: (n) => n.affinity >= 50 && n.energy >= 50 ? 2 : 0 },
43
+ { mood: "hype", baseWeight: 1, boostWhen: (n) => n.affinity >= 80 && n.energy >= 50 ? 3 : 0 }
44
+ ];
45
+ function deriveMood(state) {
46
+ if (state.needs.energy <= 20)
47
+ return "sleepy";
48
+ if (state.needs.belly <= 20)
49
+ return "hungry";
50
+ if (state.needs.affinity <= 20)
51
+ return "lonely";
52
+ const bucket = Math.floor(Date.now() / (10 * 60000));
53
+ const rand = timeBucketHash(bucket);
54
+ let totalWeight = 0;
55
+ const entries = [];
56
+ for (const m of MID_MOODS) {
57
+ const w = m.baseWeight + (m.boostWhen?.(state.needs) ?? 0);
58
+ entries.push({ mood: m.mood, weight: w });
59
+ totalWeight += w;
60
+ }
61
+ let pick = rand * totalWeight;
62
+ for (const e of entries) {
63
+ pick -= e.weight;
64
+ if (pick <= 0)
65
+ return e.mood;
66
+ }
67
+ return "calm";
68
+ }
69
+ function tickWhaleNeeds(state, idleMs, now = Date.now()) {
70
+ const TWO_HOURS = 2 * 3600000;
71
+ const cycles = Math.floor(idleMs / TWO_HOURS);
72
+ if (cycles <= 0)
73
+ return state;
74
+ const needs = { ...state.needs };
75
+ const keys = ["belly", "energy", "affinity", "bond"];
76
+ const baseSeed = state.lastInteractionAt ?? now;
77
+ for (let i = 0;i < cycles; i++) {
78
+ const seed = baseSeed + i * 7919;
79
+ const pick = (seed >>> 0 ^ seed * 2654435761 >>> 0) % 4;
80
+ const key = keys[pick];
81
+ needs[key] = Math.max(10, needs[key] - 10);
82
+ }
83
+ const next = {
84
+ ...state,
85
+ lastInteractionAt: now,
86
+ needs
87
+ };
88
+ return { ...next, mood: deriveMood(next) };
89
+ }
90
+
91
+ // src/storage.ts
92
+ var SAVE_FILE = "save.json";
93
+ var SAVE_DIR_CANDIDATES = [
94
+ process.env["WHALE_SAVE_DIR"],
95
+ path.join(os.homedir(), ".whale"),
96
+ path.join(process.cwd(), ".whale"),
97
+ path.join(os.tmpdir(), "whale")
98
+ ].filter((dir) => Boolean(dir));
99
+ var cachedReadPath = null;
100
+ var cachedWritePath = null;
101
+ function getSavePath(dir) {
102
+ return path.join(dir, SAVE_FILE);
103
+ }
104
+ async function canWriteDir(dir) {
105
+ try {
106
+ await fs.mkdir(dir, { recursive: true });
107
+ await fs.access(dir, fsConstants.W_OK);
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+ async function getLatestExistingSavePath() {
114
+ if (cachedReadPath)
115
+ return cachedReadPath;
116
+ let latestPath = null;
117
+ let latestMtime = -1;
118
+ for (const dir of SAVE_DIR_CANDIDATES) {
119
+ const savePath = getSavePath(dir);
120
+ try {
121
+ const stat2 = await fs.stat(savePath);
122
+ const mtime = stat2.mtimeMs;
123
+ if (mtime > latestMtime) {
124
+ latestMtime = mtime;
125
+ latestPath = savePath;
126
+ }
127
+ } catch {}
128
+ }
129
+ cachedReadPath = latestPath;
130
+ return latestPath;
131
+ }
132
+ async function getWritableSavePath() {
133
+ if (cachedWritePath)
134
+ return cachedWritePath;
135
+ const existingPath = await getLatestExistingSavePath();
136
+ if (existingPath) {
137
+ const existingDir = path.dirname(existingPath);
138
+ if (await canWriteDir(existingDir)) {
139
+ cachedWritePath = existingPath;
140
+ return existingPath;
141
+ }
142
+ }
143
+ for (const dir of SAVE_DIR_CANDIDATES) {
144
+ if (await canWriteDir(dir)) {
145
+ const savePath = getSavePath(dir);
146
+ cachedWritePath = savePath;
147
+ cachedReadPath = savePath;
148
+ return savePath;
149
+ }
150
+ }
151
+ throw new Error("No writable save directory available");
152
+ }
153
+ function migrateState(raw) {
154
+ const state = raw;
155
+ if (!state.needs)
156
+ return createInitialWhaleState();
157
+ const needs = state.needs;
158
+ if ("hunger" in needs && !("belly" in needs)) {
159
+ const hunger = needs["hunger"] ?? 30;
160
+ needs["belly"] = 100 - hunger;
161
+ delete needs["hunger"];
162
+ }
163
+ if (needs["bond"] === undefined) {
164
+ needs["bond"] = 60;
165
+ }
166
+ return state;
167
+ }
168
+ async function loadState() {
169
+ try {
170
+ const savePath = await getLatestExistingSavePath();
171
+ if (!savePath)
172
+ throw new Error("save not found");
173
+ const raw = await fs.readFile(savePath, "utf-8");
174
+ const data = JSON.parse(raw);
175
+ const userId = data.userId ?? crypto.randomUUID();
176
+ const token = data.token;
177
+ const state = migrateState(data.state);
178
+ const now = Date.now();
179
+ const lastInteraction = state.lastInteractionAt ?? now;
180
+ const idleMs = now - lastInteraction;
181
+ const decayed = idleMs > 0 ? tickWhaleNeeds(state, idleMs, now) : state;
182
+ return { userId, token, state: decayed };
183
+ } catch {
184
+ const userId = crypto.randomUUID();
185
+ return { userId, state: createInitialWhaleState() };
186
+ }
187
+ }
188
+ async function saveState(userId, state, quotaCache, token) {
189
+ try {
190
+ const savePath = await getWritableSavePath();
191
+ let nextToken = token;
192
+ if (nextToken === undefined) {
193
+ try {
194
+ const raw = await fs.readFile(savePath, "utf-8");
195
+ const existing = JSON.parse(raw);
196
+ nextToken = existing.token;
197
+ } catch {}
198
+ }
199
+ const data = { userId, token: nextToken, state };
200
+ const tmp = savePath + ".tmp";
201
+ await fs.writeFile(tmp, JSON.stringify(data, null, 2));
202
+ await fs.rename(tmp, savePath);
203
+ cachedReadPath = savePath;
204
+ } catch {}
205
+ }
206
+
207
+ // src/core/types.ts
208
+ var RARITIES = [
209
+ "common",
210
+ "uncommon",
211
+ "rare",
212
+ "epic",
213
+ "legendary"
214
+ ];
215
+ var c = String.fromCharCode;
216
+ var duck = c(100, 117, 99, 107);
217
+ var cat = c(99, 97, 116);
218
+ var blob = c(98, 108, 111, 98);
219
+ var alien = c(97, 108, 105, 101, 110);
220
+ var axolotl = c(97, 120, 111, 108, 111, 116, 108);
221
+ var bat = c(98, 97, 116);
222
+ var cactus = c(99, 97, 99, 116, 117, 115);
223
+ var capybara = c(99, 97, 112, 121, 98, 97, 114, 97);
224
+ var ghost = c(103, 104, 111, 115, 116);
225
+ var jellyfish = c(106, 101, 108, 108, 121, 102, 105, 115, 104);
226
+ var owl = c(111, 119, 108);
227
+ var penguin = c(112, 101, 110, 103, 117, 105, 110);
228
+ var rabbit = c(114, 97, 98, 98, 105, 116);
229
+ var robot = c(114, 111, 98, 111, 116);
230
+ var snail = c(115, 110, 97, 105, 108);
231
+ var turtle = c(116, 117, 114, 116, 108, 101);
232
+ var whale = c(119, 104, 97, 108, 101);
233
+ var SPECIES = [
234
+ duck,
235
+ cat,
236
+ blob,
237
+ alien,
238
+ axolotl,
239
+ bat,
240
+ cactus,
241
+ capybara,
242
+ ghost,
243
+ jellyfish,
244
+ owl,
245
+ penguin,
246
+ rabbit,
247
+ robot,
248
+ snail,
249
+ turtle,
250
+ whale
251
+ ];
252
+ var EYES = ["o", "•", ".", "^", "-", "@", "°", "·", "Ø", "✦", "×", "◉", "'"];
253
+ var DEFAULT_EYES = {
254
+ [duck]: ".",
255
+ [cat]: "^",
256
+ [blob]: "'",
257
+ [alien]: "Ø",
258
+ [axolotl]: "•",
259
+ [bat]: "o",
260
+ [cactus]: "o",
261
+ [capybara]: "•",
262
+ [ghost]: "•",
263
+ [jellyfish]: "o",
264
+ [owl]: "o",
265
+ [penguin]: "o",
266
+ [rabbit]: "-",
267
+ [robot]: "o",
268
+ [snail]: "@",
269
+ [turtle]: "o",
270
+ [whale]: "o"
271
+ };
272
+ var HATS = [
273
+ "none",
274
+ "crown",
275
+ "tophat",
276
+ "propeller",
277
+ "halo",
278
+ "wizard",
279
+ "beanie",
280
+ "tinyduck"
281
+ ];
282
+ var STAT_NAMES = [
283
+ "DEBUGGING",
284
+ "PATIENCE",
285
+ "CHAOS",
286
+ "WISDOM",
287
+ "SNARK"
288
+ ];
289
+ var HAT_COLLECTION_CAP = {
290
+ common: 0,
291
+ uncommon: 0,
292
+ rare: 2,
293
+ epic: 3,
294
+ legendary: 4
295
+ };
296
+ var RARITY_WEIGHTS = {
297
+ common: 60,
298
+ uncommon: 25,
299
+ rare: 10,
300
+ epic: 4,
301
+ legendary: 1
302
+ };
303
+ var RARITY_STARS = {
304
+ common: "★",
305
+ uncommon: "★★",
306
+ rare: "★★★",
307
+ epic: "★★★★",
308
+ legendary: "★★★★★"
309
+ };
310
+
311
+ // src/core/companion.ts
312
+ function mulberry32(seed) {
313
+ let a = seed >>> 0;
314
+ return function() {
315
+ a |= 0;
316
+ a = a + 1831565813 | 0;
317
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
318
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
319
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
320
+ };
321
+ }
322
+ function hashString(value) {
323
+ if (typeof Bun !== "undefined") {
324
+ return Number(BigInt(Bun.hash(value)) & 0xffffffffn);
325
+ }
326
+ let hash = 2166136261;
327
+ for (let index = 0;index < value.length; index++) {
328
+ hash ^= value.charCodeAt(index);
329
+ hash = Math.imul(hash, 16777619);
330
+ }
331
+ return hash >>> 0;
332
+ }
333
+ function pick(rng, items) {
334
+ return items[Math.floor(rng() * items.length)];
335
+ }
336
+ function rollRarity(rng) {
337
+ const total = Object.values(RARITY_WEIGHTS).reduce((a, b) => a + b, 0);
338
+ let roll = rng() * total;
339
+ for (const rarity of RARITIES) {
340
+ roll -= RARITY_WEIGHTS[rarity];
341
+ if (roll < 0)
342
+ return rarity;
343
+ }
344
+ return "common";
345
+ }
346
+ var RARITY_FLOOR = {
347
+ common: 5,
348
+ uncommon: 15,
349
+ rare: 25,
350
+ epic: 35,
351
+ legendary: 50
352
+ };
353
+ function rollStats(rng, rarity) {
354
+ const floor = RARITY_FLOOR[rarity];
355
+ const peak = pick(rng, STAT_NAMES);
356
+ let dump = pick(rng, STAT_NAMES);
357
+ while (dump === peak)
358
+ dump = pick(rng, STAT_NAMES);
359
+ const stats = {};
360
+ for (const name of STAT_NAMES) {
361
+ if (name === peak) {
362
+ stats[name] = Math.min(100, floor + 50 + Math.floor(rng() * 30));
363
+ } else if (name === dump) {
364
+ stats[name] = Math.max(1, floor - 10 + Math.floor(rng() * 15));
365
+ } else {
366
+ stats[name] = floor + Math.floor(rng() * 40);
367
+ }
368
+ }
369
+ return stats;
370
+ }
371
+ var SALT = "whale-2026-401";
372
+ function rollFrom(rng) {
373
+ const rarity = rollRarity(rng);
374
+ const species = pick(rng, SPECIES);
375
+ const bones = {
376
+ rarity,
377
+ species,
378
+ eye: DEFAULT_EYES[species],
379
+ hat: rarity === "common" ? "none" : pick(rng, HATS),
380
+ shiny: rng() < 0.01,
381
+ stats: rollStats(rng, rarity)
382
+ };
383
+ return { bones, inspirationSeed: Math.floor(rng() * 1e9) };
384
+ }
385
+ var rollCache;
386
+ function roll(userId) {
387
+ const key = userId + SALT;
388
+ if (rollCache?.key === key)
389
+ return rollCache.value;
390
+ const value = rollFrom(mulberry32(hashString(key)));
391
+ rollCache = { key, value };
392
+ return value;
393
+ }
394
+ function hatchCompanion(userId, soul, hatchedAt = Date.now()) {
395
+ const { bones } = roll(userId);
396
+ return { ...soul, hatchedAt, ...bones };
397
+ }
398
+ function getCompanionFromStored(userId, stored) {
399
+ if (!stored)
400
+ return;
401
+ const { bones } = roll(userId);
402
+ const finalSpecies = stored.speciesOverride ?? bones.species;
403
+ const finalEye = stored.eyeOverride ?? DEFAULT_EYES[finalSpecies];
404
+ const finalHat = stored.hatOverride ?? bones.hat;
405
+ const finalRarity = stored.rarityOverride ?? bones.rarity;
406
+ const finalShiny = stored.shinyOverride ?? bones.shiny;
407
+ const finalBones = { ...bones, species: finalSpecies, eye: finalEye, hat: finalHat, rarity: finalRarity, shiny: finalShiny };
408
+ return { ...stored, ...finalBones };
409
+ }
410
+
411
+ // src/api.ts
412
+ var API_URL = process.env["WHALE_API_URL"] ?? "https://api.pet.whaletech.app";
413
+ var _token = null;
414
+ function setToken(token) {
415
+ _token = token;
416
+ }
417
+ async function post(path2, body) {
418
+ try {
419
+ if (_token)
420
+ body.token = _token;
421
+ const res = await fetch(`${API_URL}${path2}`, {
422
+ method: "POST",
423
+ headers: { "Content-Type": "application/json" },
424
+ body: JSON.stringify(body)
425
+ });
426
+ const data = await res.json();
427
+ if (!res.ok) {
428
+ const isQuotaExceeded = data.error === "quota_exceeded";
429
+ return {
430
+ ok: false,
431
+ error: String(data.error ?? `HTTP ${res.status}`),
432
+ quotaExceeded: isQuotaExceeded,
433
+ paymentType: isQuotaExceeded ? String(data.paymentType) : undefined
434
+ };
435
+ }
436
+ return { ok: true, data };
437
+ } catch (err) {
438
+ const msg = err instanceof Error ? err.message : "unknown error";
439
+ return { ok: false, error: msg };
440
+ }
441
+ }
442
+ function syncState(userId, localState) {
443
+ return post("/api/sync", { userId, localState });
444
+ }
445
+ async function persistState(userId, state, quota) {
446
+ await saveState(userId, state, quota ?? undefined, _token ?? undefined);
447
+ const result = await syncState(userId, state);
448
+ if (result.ok) {
449
+ setToken(result.data.token);
450
+ await saveState(userId, result.data.state, result.data.quota, result.data.token);
451
+ }
452
+ return result;
453
+ }
454
+ async function sendChatStream(userId, companion, state, message, onToken) {
455
+ try {
456
+ const body = {
457
+ userId,
458
+ name: companion.name,
459
+ personality: companion.personality,
460
+ species: companion.species,
461
+ mood: deriveMood(state),
462
+ needs: state.needs,
463
+ message
464
+ };
465
+ if (_token)
466
+ body.token = _token;
467
+ const res = await fetch(`${API_URL}/api/chat`, {
468
+ method: "POST",
469
+ headers: { "Content-Type": "application/json" },
470
+ body: JSON.stringify(body)
471
+ });
472
+ if (!res.ok) {
473
+ const data = await res.json();
474
+ const isQuotaExceeded = data.error === "quota_exceeded";
475
+ return {
476
+ ok: false,
477
+ error: String(data.error ?? `HTTP ${res.status}`),
478
+ quotaExceeded: isQuotaExceeded,
479
+ paymentType: isQuotaExceeded ? String(data.paymentType) : undefined
480
+ };
481
+ }
482
+ const reader = res.body.getReader();
483
+ const decoder = new TextDecoder;
484
+ let buffer = "";
485
+ let finalState = null;
486
+ let finalQuota = null;
487
+ while (true) {
488
+ const { done, value } = await reader.read();
489
+ if (done)
490
+ break;
491
+ buffer += decoder.decode(value, { stream: true });
492
+ const lines = buffer.split(`
493
+ `);
494
+ buffer = lines.pop();
495
+ for (const line of lines) {
496
+ const trimmed = line.trim();
497
+ if (!trimmed || !trimmed.startsWith("data: "))
498
+ continue;
499
+ try {
500
+ const data = JSON.parse(trimmed.slice(6));
501
+ if (data.token)
502
+ onToken(data.token);
503
+ if (data.done && data.state && data.quota) {
504
+ finalState = data.state;
505
+ finalQuota = data.quota;
506
+ }
507
+ } catch {}
508
+ }
509
+ }
510
+ if (finalState && finalQuota) {
511
+ return { ok: true, state: finalState, quota: finalQuota };
512
+ }
513
+ return { ok: false, error: "stream ended without state" };
514
+ } catch (err) {
515
+ const msg = err instanceof Error ? err.message : "unknown error";
516
+ return { ok: false, error: msg };
517
+ }
518
+ }
519
+ function sendInteraction(userId, type) {
520
+ return post("/api/interaction", { userId, type });
521
+ }
522
+ function createPurchase(userId, type) {
523
+ return post("/api/purchase", { userId, type });
524
+ }
525
+ function verifyPurchase(userId, outTradeNo) {
526
+ return post("/api/purchase/verify", { userId, outTradeNo });
527
+ }
528
+
529
+ // src/app.tsx
530
+ import { useState as useState8, useEffect as useEffect7, useCallback, useRef as useRef6 } from "react";
531
+ import { Box as Box8, Text as Text8, useStdout } from "ink";
532
+ import TextInput from "ink-text-input";
533
+
534
+ // src/core/sprites.ts
535
+ var BODIES = {
536
+ [duck]: [
537
+ [
538
+ " ",
539
+ " __ ",
540
+ " <( {E} ) ",
541
+ " ( )> ",
542
+ " ` ` "
543
+ ],
544
+ [
545
+ " ",
546
+ " __ ",
547
+ " <( {E} ) ",
548
+ " ( )> ~ ",
549
+ " ` ` "
550
+ ],
551
+ [
552
+ " ",
553
+ " __ ",
554
+ " <( {E} ) ",
555
+ " ( )> ",
556
+ " ` ` "
557
+ ]
558
+ ],
559
+ [cat]: [
560
+ [
561
+ " ",
562
+ " /\\__/\\ ",
563
+ " ( ={E}.{E}= ) ",
564
+ ' (")_(") '
565
+ ],
566
+ [
567
+ " ",
568
+ " /\\__/\\ ",
569
+ " ( ={E}.{E}= ) ",
570
+ ' (")_(") ~ '
571
+ ],
572
+ [
573
+ " ",
574
+ " /\\__/\\ ",
575
+ " ( ={E}.{E}= ) ",
576
+ ' (") (") '
577
+ ]
578
+ ],
579
+ [blob]: [
580
+ [
581
+ " ",
582
+ " .-^-. ",
583
+ " ( {E}u{E} ) ",
584
+ " (_____) "
585
+ ],
586
+ [
587
+ " ",
588
+ " .-^^-. ",
589
+ " ( {E}u{E} ) ",
590
+ " (______-) "
591
+ ],
592
+ [
593
+ " ",
594
+ " .--. ",
595
+ " ( {E}u{E}) ",
596
+ " (____) "
597
+ ]
598
+ ],
599
+ [alien]: [
600
+ [
601
+ " ",
602
+ " .---. ",
603
+ " ({E}_{E}) ",
604
+ " /|=|\\ ",
605
+ " / \\ "
606
+ ],
607
+ [
608
+ " ",
609
+ " .---. ",
610
+ " ({E}_{E}) ",
611
+ " /|=|\\ ",
612
+ " / \\ "
613
+ ],
614
+ [
615
+ " * ",
616
+ " .---. ",
617
+ " ({E}_{E}) ",
618
+ " /|=|\\ ",
619
+ " / \\ "
620
+ ]
621
+ ],
622
+ [axolotl]: [
623
+ [
624
+ " ",
625
+ " \\\\\\ /// ",
626
+ " ( {E}ᴗ{E} ) ",
627
+ " /|_____|\\ ",
628
+ " / \\ "
629
+ ],
630
+ [
631
+ " ",
632
+ " /// \\\\\\ ",
633
+ " ( {E}ᴗ{E} ) ",
634
+ " /|_____|\\ ",
635
+ " / \\ "
636
+ ],
637
+ [
638
+ " ",
639
+ " \\\\\\ /// ",
640
+ " ( {E}ᴗ{E} ) ",
641
+ " /|_____|\\ ",
642
+ " / \\ "
643
+ ]
644
+ ],
645
+ [bat]: [
646
+ [
647
+ " ",
648
+ " /\\_._/\\ ",
649
+ " ( ({E} {E}) ) ",
650
+ " \\ v / ",
651
+ " `---´ "
652
+ ],
653
+ [
654
+ " ",
655
+ " \\_._ /\\ ",
656
+ " ( ({E} {E}) ) ",
657
+ " \\ v / ",
658
+ " `---´ "
659
+ ],
660
+ [
661
+ " ",
662
+ " /\\_._/\\ ",
663
+ " ( ({E} {E}) ) ",
664
+ " \\ v / ",
665
+ " `---´ "
666
+ ]
667
+ ],
668
+ [cactus]: [
669
+ [
670
+ " ",
671
+ " _ _ ",
672
+ " _| | |_ ",
673
+ " | {E} {E} | ",
674
+ " | ^ | ",
675
+ " |__~____| "
676
+ ],
677
+ [
678
+ " ",
679
+ " _ _ ",
680
+ " | | |_ ",
681
+ " | {E} {E} | ",
682
+ " | ^ | ",
683
+ " |__~____| "
684
+ ],
685
+ [
686
+ " ",
687
+ " _ _ ",
688
+ " _| | | ",
689
+ " | {E} {E} | ",
690
+ " | ^ | ",
691
+ " |__~____| "
692
+ ]
693
+ ],
694
+ [capybara]: [
695
+ [
696
+ " ",
697
+ " .-~~~~-. ",
698
+ " / {E} {E} \\ ",
699
+ " ( (oo) ) ",
700
+ " \\__.__.__/ "
701
+ ],
702
+ [
703
+ " ",
704
+ " .-~~~~-. ",
705
+ " / {E} {E} \\ ",
706
+ " ( (oo) ) ",
707
+ " \\__.__.__/ ~"
708
+ ],
709
+ [
710
+ " ",
711
+ " .-~~~~-. ",
712
+ " / {E} {E} \\ ",
713
+ " ( (oo) ) ",
714
+ " \\__. .__/ "
715
+ ]
716
+ ],
717
+ [ghost]: [
718
+ [
719
+ " ",
720
+ " .----. ",
721
+ " ( {E}ᴗ{E} ) ",
722
+ " / ~~~ \\ ",
723
+ " ~_/___\\_~ "
724
+ ],
725
+ [
726
+ " ",
727
+ " .----. ",
728
+ " ( {E}ᴗ{E} ) ",
729
+ " / ~~~ \\ ",
730
+ " ~_/___\\_~ "
731
+ ],
732
+ [
733
+ " ~ ",
734
+ " .----. ",
735
+ " ( {E}ᴗ{E} ) ",
736
+ " / ~~~ \\ ",
737
+ " ~_/___\\_~ "
738
+ ]
739
+ ],
740
+ [jellyfish]: [
741
+ [
742
+ " ",
743
+ " .---. ",
744
+ " ( {E} {E} ) ",
745
+ " \\_^_/ ",
746
+ " / | \\ ",
747
+ " / | \\ "
748
+ ],
749
+ [
750
+ " ",
751
+ " .---. ",
752
+ " ( {E} {E} ) ",
753
+ " \\_^_/ ",
754
+ " \\ | / ",
755
+ " \\|/ "
756
+ ],
757
+ [
758
+ " ~ ",
759
+ " .---. ",
760
+ " ( {E} {E} ) ",
761
+ " \\_^_/ ",
762
+ " / | \\ ",
763
+ " / | \\ "
764
+ ]
765
+ ],
766
+ [owl]: [
767
+ [
768
+ " ",
769
+ " ,_, ",
770
+ " ( {E}.{E} ) ",
771
+ " /)_) "
772
+ ],
773
+ [
774
+ " ",
775
+ " ,_, ",
776
+ " ( {E}.{E} ) ",
777
+ " /)_) "
778
+ ],
779
+ [
780
+ " ",
781
+ " ,_, ",
782
+ " ( {E}.{E} ) ",
783
+ " /)_) "
784
+ ]
785
+ ],
786
+ [penguin]: [
787
+ [
788
+ " ",
789
+ " _._ ",
790
+ " ({E} {E}) ",
791
+ " /( : )\\ ",
792
+ " ^^ ^^ "
793
+ ],
794
+ [
795
+ " ",
796
+ " _._ ",
797
+ " ({E} {E}) ",
798
+ " /( : )\\ ",
799
+ " ^^ ^^ "
800
+ ],
801
+ [
802
+ " ",
803
+ " _._ ",
804
+ " ({E} {E}) ",
805
+ " |( : )| ",
806
+ " ^^ ^^ "
807
+ ]
808
+ ],
809
+ [rabbit]: [
810
+ [
811
+ " ",
812
+ " (\\(\\ ",
813
+ " ( {E}.{E}) ",
814
+ ' o_(")(") '
815
+ ],
816
+ [
817
+ " ",
818
+ " (|(\\ ",
819
+ " ( {E}.{E}) ",
820
+ ' o_(")(") '
821
+ ],
822
+ [
823
+ " ",
824
+ " (\\(\\ ",
825
+ " ( {E}.{E}) ",
826
+ ' o_(")(") '
827
+ ]
828
+ ],
829
+ [robot]: [
830
+ [
831
+ " ",
832
+ " ]||[ ",
833
+ " .------. ",
834
+ " | [{E} {E}] | ",
835
+ " | [====] | ",
836
+ " \\------/ ",
837
+ " d b "
838
+ ],
839
+ [
840
+ " * ",
841
+ " ]||[ ",
842
+ " .------. ",
843
+ " | [{E} {E}] | ",
844
+ " | [-==-] | ",
845
+ " \\------/ ",
846
+ " d b "
847
+ ],
848
+ [
849
+ " ",
850
+ " ]||[ ",
851
+ " .------. ",
852
+ " | [{E} {E}] | ",
853
+ " | [====] | ",
854
+ " \\------/ ",
855
+ " d b "
856
+ ]
857
+ ],
858
+ [snail]: [
859
+ [
860
+ " ",
861
+ " ___ ",
862
+ " ({E} {E}) ",
863
+ " /|___|_) ",
864
+ " / / "
865
+ ],
866
+ [
867
+ " ",
868
+ " ___ ",
869
+ " ({E} {E}) ",
870
+ " /|___|_) ",
871
+ " / / "
872
+ ],
873
+ [
874
+ " ",
875
+ " ___ ",
876
+ " ({E} {E}) ",
877
+ " /|___|_) ",
878
+ " / / "
879
+ ]
880
+ ],
881
+ [turtle]: [
882
+ [
883
+ " ",
884
+ " ___ ",
885
+ " _/{E} {E}\\_ ",
886
+ " ( ^ ) ",
887
+ " `-___-´ ",
888
+ " / \\ "
889
+ ],
890
+ [
891
+ " ",
892
+ " ___ ",
893
+ " _/{E} {E}\\_ ",
894
+ " ( ^ ) ",
895
+ " `-___-´ ",
896
+ " / \\ "
897
+ ],
898
+ [
899
+ " ",
900
+ " ___ ",
901
+ " _/{E} {E}\\_ ",
902
+ " ( ^ ) ",
903
+ " `-___-´ ",
904
+ " / \\ "
905
+ ]
906
+ ],
907
+ [whale]: [
908
+ [
909
+ " . ",
910
+ ' ":" ',
911
+ ' ___:____ |"\\/"|',
912
+ " ,' `. \\ / ",
913
+ " | {E} \\___/ /",
914
+ "~^~^~^~^~^~^~^~^~^~^~ "
915
+ ],
916
+ [
917
+ " . ",
918
+ ' ":" ',
919
+ ' ___:____ |"\\/"|',
920
+ " ,' `. \\ / ",
921
+ " | {E} \\___/ /",
922
+ "^~^~^~^~^~^~^~^~^~^~^~"
923
+ ],
924
+ [
925
+ " . ",
926
+ ' ":" ',
927
+ ' ___:____ |"\\/"|',
928
+ " ,' `. \\ / ",
929
+ " | {E} \\___/ /",
930
+ "~~^~^~^~^~^~^~^~^~^~~ "
931
+ ]
932
+ ]
933
+ };
934
+ var HAT_LINES = {
935
+ none: "",
936
+ crown: " \\^^^/ ",
937
+ tophat: " [___] ",
938
+ propeller: " -+- ",
939
+ halo: " ( ) ",
940
+ wizard: " /^\\ ",
941
+ beanie: " (___) ",
942
+ tinyduck: " ,> "
943
+ };
944
+ function renderSprite(bones, frame = 0) {
945
+ const frames = BODIES[bones.species];
946
+ const body = frames[frame % frames.length].map((line) => line.replaceAll("{E}", bones.eye));
947
+ const lines = [...body];
948
+ if (bones.hat !== "none" && !lines[0].trim()) {
949
+ lines[0] = HAT_LINES[bones.hat];
950
+ }
951
+ if (!lines[0].trim() && frames.every((frameLine) => !frameLine[0].trim())) {
952
+ lines.shift();
953
+ }
954
+ return lines;
955
+ }
956
+ function spriteFrameCount(species) {
957
+ return BODIES[species].length;
958
+ }
959
+ function renderFace(bones) {
960
+ const eye = bones.eye;
961
+ switch (bones.species) {
962
+ case duck:
963
+ return `<(${eye})>`;
964
+ case cat:
965
+ return `=${eye}.${eye}=`;
966
+ case blob:
967
+ return `${eye}u${eye}`;
968
+ case alien:
969
+ return `(${eye}_${eye})`;
970
+ case axolotl:
971
+ return `(${eye}ᴗ${eye})`;
972
+ case bat:
973
+ return `(${eye}v${eye})`;
974
+ case cactus:
975
+ return `|${eye}^${eye}|`;
976
+ case capybara:
977
+ return `${eye}(oo)${eye}`;
978
+ case ghost:
979
+ return `(${eye}ᴗ${eye})`;
980
+ case jellyfish:
981
+ return `(${eye}^${eye})`;
982
+ case owl:
983
+ return `(${eye}.${eye})`;
984
+ case penguin:
985
+ return `(${eye}:${eye})`;
986
+ case rabbit:
987
+ return `(${eye}.${eye})`;
988
+ case robot:
989
+ return `[${eye}=${eye}]`;
990
+ case snail:
991
+ return `(${eye}@${eye})`;
992
+ case turtle:
993
+ return `(${eye}^${eye})`;
994
+ case whale:
995
+ return `(${eye}~${eye})`;
996
+ }
997
+ }
998
+
999
+ // src/ui/animation.ts
1000
+ var TICK_MS = 500;
1001
+ var BUBBLE_SHOW_TICKS = 20;
1002
+ var FADE_WINDOW_TICKS = 6;
1003
+ var MIN_COLS_FOR_FULL_SPRITE = 100;
1004
+ var NARROW_QUIP_CAP = 24;
1005
+ var IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0];
1006
+ function isCJK(ch) {
1007
+ const code = ch.codePointAt(0) ?? 0;
1008
+ return code >= 12288 && code <= 40959 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65519;
1009
+ }
1010
+ function wrapSpeech(text, width) {
1011
+ const result = [];
1012
+ for (const paragraph of text.split(`
1013
+ `)) {
1014
+ if (!paragraph) {
1015
+ result.push("");
1016
+ continue;
1017
+ }
1018
+ let current = "";
1019
+ let currentWidth = 0;
1020
+ for (const ch of paragraph) {
1021
+ const chWidth = isCJK(ch) ? 2 : 1;
1022
+ if (ch === " " && currentWidth + 1 > width && current) {
1023
+ result.push(current);
1024
+ current = "";
1025
+ currentWidth = 0;
1026
+ continue;
1027
+ }
1028
+ if (currentWidth + chWidth > width && current) {
1029
+ result.push(current);
1030
+ current = ch;
1031
+ currentWidth = chWidth;
1032
+ } else {
1033
+ current += ch;
1034
+ currentWidth += chWidth;
1035
+ }
1036
+ }
1037
+ if (current)
1038
+ result.push(current);
1039
+ }
1040
+ return result;
1041
+ }
1042
+ function truncateReaction(reaction, cap = NARROW_QUIP_CAP) {
1043
+ if (!reaction || reaction.length <= cap)
1044
+ return reaction;
1045
+ return `${reaction.slice(0, cap - 1)}…`;
1046
+ }
1047
+ var FEED_OVERLAYS = [" *nom* ", " ~munch~ ", " *nom* ", " *chomp* "];
1048
+ var PET_OVERLAYS = [" ♥ ♥ ", " ♥ ♥ ♥ ", " ♥ ♥ ♥ ", " ♥ ♥ ♥ "];
1049
+ var SLEEP_OVERLAYS = [" z z z ", " z z ", " z z z ", " z z "];
1050
+ var SHINY_ABOVE = [" ✦ ⋆ ", " ✧ ✦ ", " ⋆ ✧ ", " ✦ ⋆ "];
1051
+ var SHINY_BELOW = [" ⋆ ✧ ", " ✦ ⋆ ", " ✧ ✦ ", " ⋆ ✦ "];
1052
+ function getSpriteViewModel(input) {
1053
+ const {
1054
+ companion,
1055
+ columns,
1056
+ tick,
1057
+ focused = false,
1058
+ reaction,
1059
+ bubbleAgeTicks = 0,
1060
+ petting = false,
1061
+ action = null,
1062
+ bubbleShowDuration = BUBBLE_SHOW_TICKS
1063
+ } = input;
1064
+ const fading = reaction !== undefined && bubbleAgeTicks >= bubbleShowDuration - FADE_WINDOW_TICKS;
1065
+ if (columns < MIN_COLS_FOR_FULL_SPRITE) {
1066
+ const quip = truncateReaction(reaction);
1067
+ const label = quip ? `"${quip}"` : focused ? ` ${companion.name} ` : companion.name;
1068
+ return {
1069
+ face: renderFace(companion),
1070
+ lines: [],
1071
+ overlayAbove: [],
1072
+ overlayBelow: [],
1073
+ label,
1074
+ fading,
1075
+ narrow: true
1076
+ };
1077
+ }
1078
+ const frameCount = spriteFrameCount(companion.species);
1079
+ let spriteFrame = 0;
1080
+ let blink = false;
1081
+ if (action === "sleep") {
1082
+ spriteFrame = 0;
1083
+ blink = true;
1084
+ } else if (action || reaction || petting) {
1085
+ spriteFrame = tick % frameCount;
1086
+ } else {
1087
+ const step = IDLE_SEQUENCE[tick % IDLE_SEQUENCE.length];
1088
+ if (step === -1) {
1089
+ spriteFrame = 0;
1090
+ blink = true;
1091
+ } else {
1092
+ spriteFrame = step % frameCount;
1093
+ }
1094
+ }
1095
+ const lines = renderSprite(companion, spriteFrame).map((line) => blink ? line.replaceAll(companion.eye, "-") : line);
1096
+ let overlayAbove = [];
1097
+ let overlayBelow = [];
1098
+ if (action === "feed") {
1099
+ overlayBelow = [FEED_OVERLAYS[tick % FEED_OVERLAYS.length]];
1100
+ } else if (action === "pet") {
1101
+ overlayAbove = [PET_OVERLAYS[tick % PET_OVERLAYS.length]];
1102
+ } else if (action === "sleep") {
1103
+ overlayAbove = [SLEEP_OVERLAYS[tick % SLEEP_OVERLAYS.length]];
1104
+ }
1105
+ if (companion.shiny) {
1106
+ if (overlayAbove.length === 0)
1107
+ overlayAbove = [SHINY_ABOVE[tick % SHINY_ABOVE.length]];
1108
+ if (overlayBelow.length === 0)
1109
+ overlayBelow = [SHINY_BELOW[tick % SHINY_BELOW.length]];
1110
+ }
1111
+ return {
1112
+ face: renderFace(companion),
1113
+ lines,
1114
+ overlayAbove,
1115
+ overlayBelow,
1116
+ label: focused ? ` ${companion.name} ` : companion.name,
1117
+ fading,
1118
+ narrow: false
1119
+ };
1120
+ }
1121
+
1122
+ // src/commands.ts
1123
+ function formatBar(value) {
1124
+ const filled = Math.round(value / 10);
1125
+ return "█".repeat(filled) + "░".repeat(10 - filled);
1126
+ }
1127
+ function statsText(companion, state) {
1128
+ const stars = RARITY_STARS[companion.rarity];
1129
+ const mood = deriveMood(state);
1130
+ return [
1131
+ `${companion.name} ${stars} [${companion.species}]`,
1132
+ `mood: ${mood} | personality: ${companion.personality}`,
1133
+ `belly: ${formatBar(state.needs.belly)} ${state.needs.belly.toFixed(0)}`,
1134
+ `energy: ${formatBar(state.needs.energy)} ${state.needs.energy.toFixed(0)}`,
1135
+ `affinity: ${formatBar(state.needs.affinity)} ${state.needs.affinity.toFixed(0)}`,
1136
+ `bond: ${formatBar(state.needs.bond)} ${state.needs.bond.toFixed(0)}`
1137
+ ].join(`
1138
+ `);
1139
+ }
1140
+ var FEED_REACTIONS = [
1141
+ "yum! *munch munch*",
1142
+ "*nom nom nom* so good!",
1143
+ "mmm, delicious~",
1144
+ "thank you! *happy munch*"
1145
+ ];
1146
+ var PET_REACTIONS = [
1147
+ "*purrs happily*",
1148
+ "*nuzzles your hand*",
1149
+ "that feels nice~",
1150
+ "*wiggles with joy*"
1151
+ ];
1152
+ var SLEEP_REACTIONS = [
1153
+ "zzz... *curls up*",
1154
+ "*yawns* good night~",
1155
+ "so sleepy... zzz"
1156
+ ];
1157
+ function randomPick(arr) {
1158
+ return arr[Math.floor(Math.random() * arr.length)];
1159
+ }
1160
+ var ZH_SWITCH_KEYWORDS = ["换中文", "中文", "切换中文", "chinese", "zh"];
1161
+ var EN_SWITCH_KEYWORDS = ["english", "en", "换英文", "英文", "切换英文"];
1162
+ var ACTION_KEYWORDS = [
1163
+ { action: "feed", keywords: ["喂", "喂食", "吃", "吃饭", "吃东西", "eat", "hungry", "饿", "food", "饭"] },
1164
+ { action: "pet", keywords: ["摸", "摸摸", "抱", "抱抱", "撸", "hug", "pat", "stroke", "亲"] },
1165
+ { action: "sleep", keywords: ["睡", "睡觉", "休息", "rest", "nap", "困", "tired"] },
1166
+ { action: "stats", keywords: ["状态", "状态栏", "status"] },
1167
+ { action: "help", keywords: ["帮助", "怎么玩", "怎么办", "说明书", "说明"] },
1168
+ { action: "changepet", keywords: ["换宠物", "换宠", "换人物", "change pet", "new pet"] },
1169
+ { action: "changeeye", keywords: ["换眼睛", "换眼", "change eye", "new eye"] },
1170
+ { action: "changehat", keywords: ["换帽子", "换帽", "帽子", "change hat", "new hat", "hat"] },
1171
+ { action: "upgraderarity", keywords: ["升级稀有度", "升级", "upgrade rarity", "upgrade"] },
1172
+ { action: "storylist", keywords: ["剧情", "故事", "剧情回顾", "story", "stories"] }
1173
+ ];
1174
+ function matchAction(input) {
1175
+ const lower = input.trim().toLowerCase();
1176
+ for (const { action, keywords } of ACTION_KEYWORDS) {
1177
+ for (const kw of keywords) {
1178
+ if (lower === kw)
1179
+ return action;
1180
+ }
1181
+ }
1182
+ return;
1183
+ }
1184
+ var HELP_TEXT = {
1185
+ en: [
1186
+ "\uD83D\uDCD6 【Guide】",
1187
+ "\uD83D\uDCAC Chat: just type anything to chat",
1188
+ "\uD83C\uDFAE Interact: type feed / pet / sleep",
1189
+ "\uD83D\uDCCA Stats: type stats",
1190
+ "\uD83D\uDD04 Change pet/eyes/hat: type change pet/eyes/hat",
1191
+ "⭐ Upgrade rarity: type upgrade — hats unlock at 2★, sparkle at 5★",
1192
+ "\uD83D\uDCDC Story recap: type story",
1193
+ "\uD83D\uDD2E Hidden story: need fullness/energy/affinity/bond all at 100",
1194
+ "plus 10 chats + 3 interactions"
1195
+ ].join(`
1196
+ `),
1197
+ zh: [
1198
+ "\uD83D\uDCD6 【小说明书】",
1199
+ "\uD83D\uDCAC 聊天: 直接输入内容就可以聊天",
1200
+ "\uD83C\uDFAE 互动: 输入 喂食 / 摸摸 / 睡觉",
1201
+ "\uD83D\uDCCA 状态: 输入 状态",
1202
+ "\uD83D\uDD04 换人物/眼睛/帽子: 输入 换宠物/人物/眼睛/帽子",
1203
+ "⭐ 升级稀有度: 输入 升级 ,2星开始有帽子保留位, 5星有闪闪特效",
1204
+ "\uD83D\uDCDC 剧情回顾: 输入 剧情",
1205
+ "\uD83D\uDD2E 隐藏剧情: 需要 饱饱/精力/好感/羁绊 都到 100",
1206
+ "并新增聊天 10 次 + 互动 3 次"
1207
+ ].join(`
1208
+ `)
1209
+ };
1210
+ function handleCommand(input, state, companion, locale) {
1211
+ const trimmed = input.trim();
1212
+ if (!trimmed)
1213
+ return {};
1214
+ const lower = trimmed.toLowerCase();
1215
+ if (ZH_SWITCH_KEYWORDS.includes(lower)) {
1216
+ return { locale: "zh", reaction: "已切换到中文~" };
1217
+ }
1218
+ if (EN_SWITCH_KEYWORDS.includes(lower)) {
1219
+ return { locale: "en", reaction: "Switched to English~" };
1220
+ }
1221
+ if (lower === "/feed" || lower === "feed") {
1222
+ return { action: "feed", needsBackend: true, reaction: randomPick(FEED_REACTIONS) };
1223
+ }
1224
+ if (lower === "/pet" || lower === "pet") {
1225
+ return { action: "pet", needsBackend: true, reaction: randomPick(PET_REACTIONS) };
1226
+ }
1227
+ if (lower === "/sleep" || lower === "sleep") {
1228
+ return { action: "sleep", needsBackend: true, reaction: randomPick(SLEEP_REACTIONS) };
1229
+ }
1230
+ if (lower === "/stats" || lower === "stats") {
1231
+ return { reaction: statsText(companion, state), bubbleDuration: 120 };
1232
+ }
1233
+ if (lower === "/help" || lower === "help") {
1234
+ return { reaction: HELP_TEXT[locale], bubbleDuration: 120 };
1235
+ }
1236
+ const matched = matchAction(lower);
1237
+ if (matched) {
1238
+ switch (matched) {
1239
+ case "feed":
1240
+ return { action: "feed", needsBackend: true, reaction: randomPick(FEED_REACTIONS) };
1241
+ case "pet":
1242
+ return { action: "pet", needsBackend: true, reaction: randomPick(PET_REACTIONS) };
1243
+ case "sleep":
1244
+ return { action: "sleep", needsBackend: true, reaction: randomPick(SLEEP_REACTIONS) };
1245
+ case "stats":
1246
+ return { reaction: statsText(companion, state), bubbleDuration: 120 };
1247
+ case "help":
1248
+ return { reaction: HELP_TEXT[locale], bubbleDuration: 120 };
1249
+ case "changepet":
1250
+ return { navigate: "changepet" };
1251
+ case "changeeye":
1252
+ return { navigate: "changeeye" };
1253
+ case "changehat":
1254
+ return { navigate: "changehat" };
1255
+ case "upgraderarity":
1256
+ return { navigate: "upgraderarity" };
1257
+ case "storylist":
1258
+ return { navigate: "storylist" };
1259
+ }
1260
+ }
1261
+ return { isChat: true, chatMessage: trimmed, needsBackend: true };
1262
+ }
1263
+
1264
+ // src/ui/ChangePet.tsx
1265
+ import { useState, useEffect, useRef } from "react";
1266
+ import { Box, Text, useInput } from "ink";
1267
+ import QRCode from "qrcode";
1268
+ import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
1269
+ var POLL_INTERVAL = 3000;
1270
+ var MAX_POLL_TIME = 5 * 60000;
1271
+ function ChangePet({ userId, currentSpecies, onComplete, onCancel }) {
1272
+ const [phase, setPhase] = useState("select");
1273
+ const [outTradeNo, setOutTradeNo] = useState(null);
1274
+ const [qrText, setQrText] = useState(null);
1275
+ const [error, setError] = useState(null);
1276
+ const [selected, setSelected] = useState(() => {
1277
+ const idx = SPECIES.indexOf(currentSpecies);
1278
+ return idx >= 0 ? idx : 0;
1279
+ });
1280
+ const [chosenSpecies, setChosenSpecies] = useState(null);
1281
+ const [pollError, setPollError] = useState(null);
1282
+ const [pollFails, setPollFails] = useState(0);
1283
+ const onCompleteRef = useRef(onComplete);
1284
+ onCompleteRef.current = onComplete;
1285
+ useEffect(() => {
1286
+ if (phase !== "qr")
1287
+ return;
1288
+ let cancelled = false;
1289
+ (async () => {
1290
+ const purchaseType = chosenSpecies === "whale" ? "changewhale" : "changepet";
1291
+ const res = await createPurchase(userId, purchaseType);
1292
+ if (cancelled)
1293
+ return;
1294
+ if (!res.ok) {
1295
+ setError(res.error);
1296
+ return;
1297
+ }
1298
+ setOutTradeNo(res.data.outTradeNo);
1299
+ const text = await QRCode.toString(res.data.codeUrl, { type: "terminal", small: true });
1300
+ if (cancelled)
1301
+ return;
1302
+ setQrText(text);
1303
+ })();
1304
+ return () => {
1305
+ cancelled = true;
1306
+ };
1307
+ }, [phase, userId]);
1308
+ useEffect(() => {
1309
+ if (phase !== "qr" || !outTradeNo)
1310
+ return;
1311
+ let cancelled = false;
1312
+ const start = Date.now();
1313
+ const id = setInterval(async () => {
1314
+ if (cancelled)
1315
+ return;
1316
+ if (Date.now() - start > MAX_POLL_TIME) {
1317
+ setError("支付超时,请重试");
1318
+ clearInterval(id);
1319
+ return;
1320
+ }
1321
+ const res = await verifyPurchase(userId, outTradeNo);
1322
+ if (cancelled)
1323
+ return;
1324
+ if (res.ok && res.data.status === "SUCCESS") {
1325
+ clearInterval(id);
1326
+ setPhase("reveal");
1327
+ } else if (!res.ok) {
1328
+ setPollError(res.error);
1329
+ setPollFails((f) => {
1330
+ if (f + 1 >= 5) {
1331
+ setError(`查询支付状态失败: ${res.error}`);
1332
+ clearInterval(id);
1333
+ }
1334
+ return f + 1;
1335
+ });
1336
+ } else {
1337
+ setPollError(null);
1338
+ setPollFails(0);
1339
+ }
1340
+ }, POLL_INTERVAL);
1341
+ return () => {
1342
+ cancelled = true;
1343
+ clearInterval(id);
1344
+ };
1345
+ }, [phase, outTradeNo, userId]);
1346
+ useEffect(() => {
1347
+ if (phase !== "reveal" || !chosenSpecies)
1348
+ return;
1349
+ const timeout = setTimeout(() => onCompleteRef.current(chosenSpecies), 2000);
1350
+ return () => clearTimeout(timeout);
1351
+ }, [phase, chosenSpecies]);
1352
+ useInput((input, key) => {
1353
+ if (key.escape) {
1354
+ onCancel();
1355
+ return;
1356
+ }
1357
+ if (phase === "select") {
1358
+ if (key.upArrow || input === "k") {
1359
+ setSelected((s) => Math.max(0, s - 1));
1360
+ } else if (key.downArrow || input === "j") {
1361
+ setSelected((s) => Math.min(SPECIES.length - 1, s + 1));
1362
+ } else if (key.return) {
1363
+ const species = SPECIES[selected];
1364
+ setChosenSpecies(species);
1365
+ setOutTradeNo(null);
1366
+ setQrText(null);
1367
+ setError(null);
1368
+ setPollError(null);
1369
+ setPollFails(0);
1370
+ setPhase("qr");
1371
+ }
1372
+ const num = parseInt(input);
1373
+ if (num >= 1 && num <= 9) {
1374
+ setSelected(num - 1);
1375
+ }
1376
+ }
1377
+ });
1378
+ const { bones } = roll(userId);
1379
+ if (error) {
1380
+ return /* @__PURE__ */ jsxDEV(Box, {
1381
+ flexDirection: "column",
1382
+ marginTop: 1,
1383
+ marginLeft: 2,
1384
+ children: [
1385
+ /* @__PURE__ */ jsxDEV(Text, {
1386
+ color: "red",
1387
+ children: [
1388
+ "支付出错: ",
1389
+ error
1390
+ ]
1391
+ }, undefined, true, undefined, this),
1392
+ /* @__PURE__ */ jsxDEV(Text, {
1393
+ dimColor: true,
1394
+ children: "按 Esc 返回"
1395
+ }, undefined, false, undefined, this)
1396
+ ]
1397
+ }, undefined, true, undefined, this);
1398
+ }
1399
+ if (phase === "qr") {
1400
+ return /* @__PURE__ */ jsxDEV(Box, {
1401
+ flexDirection: "column",
1402
+ marginTop: 1,
1403
+ marginLeft: 2,
1404
+ children: [
1405
+ /* @__PURE__ */ jsxDEV(Text, {
1406
+ bold: true,
1407
+ color: "yellow",
1408
+ children: [
1409
+ "换宠物 — 微信支付 ",
1410
+ chosenSpecies === "whale" ? "¥5.00" : "¥0.80"
1411
+ ]
1412
+ }, undefined, true, undefined, this),
1413
+ chosenSpecies && /* @__PURE__ */ jsxDEV(Text, {
1414
+ dimColor: true,
1415
+ children: [
1416
+ "已选择: ",
1417
+ renderFace({ ...bones, species: chosenSpecies }),
1418
+ " ",
1419
+ chosenSpecies
1420
+ ]
1421
+ }, undefined, true, undefined, this),
1422
+ !qrText ? /* @__PURE__ */ jsxDEV(Text, {
1423
+ dimColor: true,
1424
+ children: "正在创建订单..."
1425
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(Fragment, {
1426
+ children: [
1427
+ /* @__PURE__ */ jsxDEV(Text, {
1428
+ dimColor: true,
1429
+ children: "请用微信扫码支付:"
1430
+ }, undefined, false, undefined, this),
1431
+ /* @__PURE__ */ jsxDEV(Box, {
1432
+ marginTop: 1,
1433
+ children: /* @__PURE__ */ jsxDEV(Text, {
1434
+ children: qrText
1435
+ }, undefined, false, undefined, this)
1436
+ }, undefined, false, undefined, this),
1437
+ /* @__PURE__ */ jsxDEV(Text, {
1438
+ dimColor: true,
1439
+ children: "等待支付中... (按 Esc 取消)"
1440
+ }, undefined, false, undefined, this),
1441
+ pollError && /* @__PURE__ */ jsxDEV(Text, {
1442
+ color: "yellow",
1443
+ children: [
1444
+ "查询: ",
1445
+ pollError
1446
+ ]
1447
+ }, undefined, true, undefined, this)
1448
+ ]
1449
+ }, undefined, true, undefined, this)
1450
+ ]
1451
+ }, undefined, true, undefined, this);
1452
+ }
1453
+ if (phase === "select") {
1454
+ return /* @__PURE__ */ jsxDEV(Box, {
1455
+ flexDirection: "column",
1456
+ marginTop: 1,
1457
+ marginLeft: 2,
1458
+ children: [
1459
+ /* @__PURE__ */ jsxDEV(Text, {
1460
+ bold: true,
1461
+ color: "green",
1462
+ children: "选择你的新宠物:"
1463
+ }, undefined, false, undefined, this),
1464
+ /* @__PURE__ */ jsxDEV(Box, {
1465
+ marginTop: 1,
1466
+ flexDirection: "column",
1467
+ children: SPECIES.map((sp, i) => {
1468
+ const face = renderFace({ ...bones, species: sp });
1469
+ const isCurrent = sp === currentSpecies;
1470
+ const isSelected = i === selected;
1471
+ return /* @__PURE__ */ jsxDEV(Text, {
1472
+ children: [
1473
+ isSelected ? "▸ " : " ",
1474
+ /* @__PURE__ */ jsxDEV(Text, {
1475
+ color: isSelected ? "cyan" : undefined,
1476
+ bold: isSelected,
1477
+ children: [
1478
+ face,
1479
+ " ",
1480
+ sp
1481
+ ]
1482
+ }, undefined, true, undefined, this),
1483
+ isCurrent ? /* @__PURE__ */ jsxDEV(Text, {
1484
+ dimColor: true,
1485
+ children: " (当前)"
1486
+ }, undefined, false, undefined, this) : null
1487
+ ]
1488
+ }, sp, true, undefined, this);
1489
+ })
1490
+ }, undefined, false, undefined, this),
1491
+ /* @__PURE__ */ jsxDEV(Box, {
1492
+ marginTop: 1,
1493
+ children: /* @__PURE__ */ jsxDEV(Text, {
1494
+ dimColor: true,
1495
+ children: "↑↓ 选择 Enter 去支付 Esc 取消"
1496
+ }, undefined, false, undefined, this)
1497
+ }, undefined, false, undefined, this)
1498
+ ]
1499
+ }, undefined, true, undefined, this);
1500
+ }
1501
+ if (phase === "reveal" && chosenSpecies) {
1502
+ const newFace = renderFace({ ...bones, species: chosenSpecies });
1503
+ return /* @__PURE__ */ jsxDEV(Box, {
1504
+ flexDirection: "column",
1505
+ marginTop: 1,
1506
+ marginLeft: 2,
1507
+ alignItems: "center",
1508
+ children: [
1509
+ /* @__PURE__ */ jsxDEV(Text, {
1510
+ bold: true,
1511
+ color: "green",
1512
+ children: "宠物已更换!"
1513
+ }, undefined, false, undefined, this),
1514
+ /* @__PURE__ */ jsxDEV(Text, {
1515
+ children: newFace
1516
+ }, undefined, false, undefined, this),
1517
+ /* @__PURE__ */ jsxDEV(Text, {
1518
+ children: [
1519
+ "你的新伙伴是 ",
1520
+ /* @__PURE__ */ jsxDEV(Text, {
1521
+ bold: true,
1522
+ children: chosenSpecies
1523
+ }, undefined, false, undefined, this),
1524
+ "!"
1525
+ ]
1526
+ }, undefined, true, undefined, this)
1527
+ ]
1528
+ }, undefined, true, undefined, this);
1529
+ }
1530
+ return null;
1531
+ }
1532
+
1533
+ // src/ui/ChangeEye.tsx
1534
+ import { useState as useState2, useEffect as useEffect2, useRef as useRef2 } from "react";
1535
+ import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
1536
+ import QRCode2 from "qrcode";
1537
+ import { jsxDEV as jsxDEV2, Fragment as Fragment2 } from "react/jsx-dev-runtime";
1538
+ var POLL_INTERVAL2 = 3000;
1539
+ var MAX_POLL_TIME2 = 5 * 60000;
1540
+ function ChangeEye({ userId, currentEye, onComplete, onCancel }) {
1541
+ const [phase, setPhase] = useState2("select");
1542
+ const [outTradeNo, setOutTradeNo] = useState2(null);
1543
+ const [qrText, setQrText] = useState2(null);
1544
+ const [error, setError] = useState2(null);
1545
+ const [selected, setSelected] = useState2(() => {
1546
+ const idx = EYES.indexOf(currentEye);
1547
+ return idx >= 0 ? idx : 0;
1548
+ });
1549
+ const [chosenEye, setChosenEye] = useState2(null);
1550
+ const [pollError, setPollError] = useState2(null);
1551
+ const [pollFails, setPollFails] = useState2(0);
1552
+ const onCompleteRef = useRef2(onComplete);
1553
+ onCompleteRef.current = onComplete;
1554
+ useEffect2(() => {
1555
+ if (phase !== "qr")
1556
+ return;
1557
+ let cancelled = false;
1558
+ (async () => {
1559
+ const res = await createPurchase(userId, "changeeye");
1560
+ if (cancelled)
1561
+ return;
1562
+ if (!res.ok) {
1563
+ setError(res.error);
1564
+ return;
1565
+ }
1566
+ setOutTradeNo(res.data.outTradeNo);
1567
+ const text = await QRCode2.toString(res.data.codeUrl, { type: "terminal", small: true });
1568
+ if (cancelled)
1569
+ return;
1570
+ setQrText(text);
1571
+ })();
1572
+ return () => {
1573
+ cancelled = true;
1574
+ };
1575
+ }, [phase, userId]);
1576
+ useEffect2(() => {
1577
+ if (phase !== "qr" || !outTradeNo)
1578
+ return;
1579
+ let cancelled = false;
1580
+ const start = Date.now();
1581
+ const id = setInterval(async () => {
1582
+ if (cancelled)
1583
+ return;
1584
+ if (Date.now() - start > MAX_POLL_TIME2) {
1585
+ setError("支付超时,请重试");
1586
+ clearInterval(id);
1587
+ return;
1588
+ }
1589
+ const res = await verifyPurchase(userId, outTradeNo);
1590
+ if (cancelled)
1591
+ return;
1592
+ if (res.ok && res.data.status === "SUCCESS") {
1593
+ clearInterval(id);
1594
+ setPhase("reveal");
1595
+ } else if (!res.ok) {
1596
+ setPollError(res.error);
1597
+ setPollFails((f) => {
1598
+ if (f + 1 >= 5) {
1599
+ setError(`查询支付状态失败: ${res.error}`);
1600
+ clearInterval(id);
1601
+ }
1602
+ return f + 1;
1603
+ });
1604
+ } else {
1605
+ setPollError(null);
1606
+ setPollFails(0);
1607
+ }
1608
+ }, POLL_INTERVAL2);
1609
+ return () => {
1610
+ cancelled = true;
1611
+ clearInterval(id);
1612
+ };
1613
+ }, [phase, outTradeNo, userId]);
1614
+ useEffect2(() => {
1615
+ if (phase !== "reveal" || !chosenEye)
1616
+ return;
1617
+ const timeout = setTimeout(() => onCompleteRef.current(chosenEye), 2000);
1618
+ return () => clearTimeout(timeout);
1619
+ }, [phase, chosenEye]);
1620
+ useInput2((input, key) => {
1621
+ if (key.escape) {
1622
+ onCancel();
1623
+ return;
1624
+ }
1625
+ if (phase === "select") {
1626
+ if (key.upArrow || input === "k") {
1627
+ setSelected((s) => Math.max(0, s - 1));
1628
+ } else if (key.downArrow || input === "j") {
1629
+ setSelected((s) => Math.min(EYES.length - 1, s + 1));
1630
+ } else if (key.return) {
1631
+ const eye = EYES[selected];
1632
+ if (eye === currentEye) {
1633
+ onCancel();
1634
+ return;
1635
+ }
1636
+ setChosenEye(eye);
1637
+ setOutTradeNo(null);
1638
+ setQrText(null);
1639
+ setError(null);
1640
+ setPollError(null);
1641
+ setPollFails(0);
1642
+ setPhase("qr");
1643
+ }
1644
+ const num = parseInt(input);
1645
+ if (num >= 1 && num <= 9) {
1646
+ setSelected(num - 1);
1647
+ }
1648
+ }
1649
+ });
1650
+ const { bones } = roll(userId);
1651
+ if (error) {
1652
+ return /* @__PURE__ */ jsxDEV2(Box2, {
1653
+ flexDirection: "column",
1654
+ marginTop: 1,
1655
+ marginLeft: 2,
1656
+ children: [
1657
+ /* @__PURE__ */ jsxDEV2(Text2, {
1658
+ color: "red",
1659
+ children: [
1660
+ "支付出错: ",
1661
+ error
1662
+ ]
1663
+ }, undefined, true, undefined, this),
1664
+ /* @__PURE__ */ jsxDEV2(Text2, {
1665
+ dimColor: true,
1666
+ children: "按 Esc 返回"
1667
+ }, undefined, false, undefined, this)
1668
+ ]
1669
+ }, undefined, true, undefined, this);
1670
+ }
1671
+ if (phase === "qr") {
1672
+ return /* @__PURE__ */ jsxDEV2(Box2, {
1673
+ flexDirection: "column",
1674
+ marginTop: 1,
1675
+ marginLeft: 2,
1676
+ children: [
1677
+ /* @__PURE__ */ jsxDEV2(Text2, {
1678
+ bold: true,
1679
+ color: "yellow",
1680
+ children: "换眼睛 — 微信支付 ¥0.30"
1681
+ }, undefined, false, undefined, this),
1682
+ chosenEye && /* @__PURE__ */ jsxDEV2(Text2, {
1683
+ dimColor: true,
1684
+ children: [
1685
+ "已选择: ",
1686
+ chosenEye,
1687
+ " ",
1688
+ renderFace({ ...bones, eye: chosenEye })
1689
+ ]
1690
+ }, undefined, true, undefined, this),
1691
+ !qrText ? /* @__PURE__ */ jsxDEV2(Text2, {
1692
+ dimColor: true,
1693
+ children: "正在创建订单..."
1694
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV2(Fragment2, {
1695
+ children: [
1696
+ /* @__PURE__ */ jsxDEV2(Text2, {
1697
+ dimColor: true,
1698
+ children: "请用微信扫码支付:"
1699
+ }, undefined, false, undefined, this),
1700
+ /* @__PURE__ */ jsxDEV2(Box2, {
1701
+ marginTop: 1,
1702
+ children: /* @__PURE__ */ jsxDEV2(Text2, {
1703
+ children: qrText
1704
+ }, undefined, false, undefined, this)
1705
+ }, undefined, false, undefined, this),
1706
+ /* @__PURE__ */ jsxDEV2(Text2, {
1707
+ dimColor: true,
1708
+ children: "等待支付中... (按 Esc 取消)"
1709
+ }, undefined, false, undefined, this),
1710
+ pollError && /* @__PURE__ */ jsxDEV2(Text2, {
1711
+ color: "yellow",
1712
+ children: [
1713
+ "查询: ",
1714
+ pollError
1715
+ ]
1716
+ }, undefined, true, undefined, this)
1717
+ ]
1718
+ }, undefined, true, undefined, this)
1719
+ ]
1720
+ }, undefined, true, undefined, this);
1721
+ }
1722
+ if (phase === "select") {
1723
+ return /* @__PURE__ */ jsxDEV2(Box2, {
1724
+ flexDirection: "column",
1725
+ marginTop: 1,
1726
+ marginLeft: 2,
1727
+ children: [
1728
+ /* @__PURE__ */ jsxDEV2(Text2, {
1729
+ bold: true,
1730
+ color: "green",
1731
+ children: "选择新眼睛:"
1732
+ }, undefined, false, undefined, this),
1733
+ /* @__PURE__ */ jsxDEV2(Box2, {
1734
+ marginTop: 1,
1735
+ flexDirection: "column",
1736
+ children: EYES.map((eye, i) => {
1737
+ const face = renderFace({ ...bones, eye });
1738
+ const isCurrent = eye === currentEye;
1739
+ const isSelected = i === selected;
1740
+ return /* @__PURE__ */ jsxDEV2(Text2, {
1741
+ children: [
1742
+ isSelected ? "▸ " : " ",
1743
+ /* @__PURE__ */ jsxDEV2(Text2, {
1744
+ color: isSelected ? "cyan" : undefined,
1745
+ bold: isSelected,
1746
+ children: [
1747
+ eye,
1748
+ " ",
1749
+ face
1750
+ ]
1751
+ }, undefined, true, undefined, this),
1752
+ isCurrent ? /* @__PURE__ */ jsxDEV2(Text2, {
1753
+ dimColor: true,
1754
+ children: " (当前)"
1755
+ }, undefined, false, undefined, this) : null
1756
+ ]
1757
+ }, eye, true, undefined, this);
1758
+ })
1759
+ }, undefined, false, undefined, this),
1760
+ /* @__PURE__ */ jsxDEV2(Box2, {
1761
+ marginTop: 1,
1762
+ children: /* @__PURE__ */ jsxDEV2(Text2, {
1763
+ dimColor: true,
1764
+ children: "↑↓ 选择 Enter 去支付 Esc 取消"
1765
+ }, undefined, false, undefined, this)
1766
+ }, undefined, false, undefined, this)
1767
+ ]
1768
+ }, undefined, true, undefined, this);
1769
+ }
1770
+ if (phase === "reveal" && chosenEye) {
1771
+ const newFace = renderFace({ ...bones, eye: chosenEye });
1772
+ return /* @__PURE__ */ jsxDEV2(Box2, {
1773
+ flexDirection: "column",
1774
+ marginTop: 1,
1775
+ marginLeft: 2,
1776
+ alignItems: "center",
1777
+ children: [
1778
+ /* @__PURE__ */ jsxDEV2(Text2, {
1779
+ bold: true,
1780
+ color: "green",
1781
+ children: "眼睛已更换!"
1782
+ }, undefined, false, undefined, this),
1783
+ /* @__PURE__ */ jsxDEV2(Text2, {
1784
+ children: newFace
1785
+ }, undefined, false, undefined, this)
1786
+ ]
1787
+ }, undefined, true, undefined, this);
1788
+ }
1789
+ return null;
1790
+ }
1791
+
1792
+ // src/ui/QuotaPayment.tsx
1793
+ import { useState as useState3, useEffect as useEffect3, useRef as useRef3 } from "react";
1794
+ import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
1795
+ import QRCode3 from "qrcode";
1796
+ import { jsxDEV as jsxDEV3, Fragment as Fragment3 } from "react/jsx-dev-runtime";
1797
+ var POLL_INTERVAL3 = 3000;
1798
+ var MAX_POLL_TIME3 = 5 * 60000;
1799
+ var LABELS = {
1800
+ conversation: { title: "购买对话次数", desc: "20次对话 ¥0.50", amount: "¥0.50" },
1801
+ interaction: { title: "购买互动次数", desc: "10次互动 ¥0.50", amount: "¥0.50" }
1802
+ };
1803
+ function QuotaPayment({ userId, paymentType, onComplete, onCancel }) {
1804
+ const [phase, setPhase] = useState3("qr");
1805
+ const [outTradeNo, setOutTradeNo] = useState3(null);
1806
+ const [qrText, setQrText] = useState3(null);
1807
+ const [error, setError] = useState3(null);
1808
+ const [pollError, setPollError] = useState3(null);
1809
+ const [pollFails, setPollFails] = useState3(0);
1810
+ const onCompleteRef = useRef3(onComplete);
1811
+ onCompleteRef.current = onComplete;
1812
+ const labels = LABELS[paymentType];
1813
+ useInput3((input, key) => {
1814
+ if (key.escape) {
1815
+ onCancel();
1816
+ }
1817
+ });
1818
+ useEffect3(() => {
1819
+ if (phase !== "qr")
1820
+ return;
1821
+ let cancelled = false;
1822
+ (async () => {
1823
+ const res = await createPurchase(userId, paymentType);
1824
+ if (cancelled)
1825
+ return;
1826
+ if (!res.ok) {
1827
+ setError(res.error);
1828
+ return;
1829
+ }
1830
+ setOutTradeNo(res.data.outTradeNo);
1831
+ const text = await QRCode3.toString(res.data.codeUrl, { type: "terminal", small: true });
1832
+ if (cancelled)
1833
+ return;
1834
+ setQrText(text);
1835
+ })();
1836
+ return () => {
1837
+ cancelled = true;
1838
+ };
1839
+ }, [phase, userId, paymentType]);
1840
+ useEffect3(() => {
1841
+ if (phase !== "qr" || !outTradeNo)
1842
+ return;
1843
+ let cancelled = false;
1844
+ const start = Date.now();
1845
+ const id = setInterval(async () => {
1846
+ if (cancelled)
1847
+ return;
1848
+ if (Date.now() - start > MAX_POLL_TIME3) {
1849
+ setError("支付超时,请重试");
1850
+ clearInterval(id);
1851
+ return;
1852
+ }
1853
+ const res = await verifyPurchase(userId, outTradeNo);
1854
+ if (cancelled)
1855
+ return;
1856
+ if (res.ok && res.data.status === "SUCCESS" && res.data.quota) {
1857
+ clearInterval(id);
1858
+ setPhase("success");
1859
+ setTimeout(() => onCompleteRef.current(res.data.quota), 2000);
1860
+ } else if (!res.ok) {
1861
+ setPollError(res.error);
1862
+ setPollFails((f) => {
1863
+ if (f + 1 >= 5) {
1864
+ setError(`查询支付状态失败: ${res.error}`);
1865
+ clearInterval(id);
1866
+ }
1867
+ return f + 1;
1868
+ });
1869
+ } else {
1870
+ setPollError(null);
1871
+ setPollFails(0);
1872
+ }
1873
+ }, POLL_INTERVAL3);
1874
+ return () => {
1875
+ cancelled = true;
1876
+ clearInterval(id);
1877
+ };
1878
+ }, [phase, outTradeNo, userId]);
1879
+ if (error) {
1880
+ return /* @__PURE__ */ jsxDEV3(Box3, {
1881
+ flexDirection: "column",
1882
+ marginTop: 1,
1883
+ marginLeft: 2,
1884
+ children: [
1885
+ /* @__PURE__ */ jsxDEV3(Text3, {
1886
+ color: "red",
1887
+ children: [
1888
+ "支付出错: ",
1889
+ error
1890
+ ]
1891
+ }, undefined, true, undefined, this),
1892
+ /* @__PURE__ */ jsxDEV3(Text3, {
1893
+ dimColor: true,
1894
+ children: "按 Esc 返回"
1895
+ }, undefined, false, undefined, this)
1896
+ ]
1897
+ }, undefined, true, undefined, this);
1898
+ }
1899
+ if (phase === "qr") {
1900
+ return /* @__PURE__ */ jsxDEV3(Box3, {
1901
+ flexDirection: "column",
1902
+ marginTop: 1,
1903
+ marginLeft: 2,
1904
+ children: [
1905
+ /* @__PURE__ */ jsxDEV3(Text3, {
1906
+ bold: true,
1907
+ color: "yellow",
1908
+ children: [
1909
+ labels.title,
1910
+ " — 微信支付 ",
1911
+ labels.amount
1912
+ ]
1913
+ }, undefined, true, undefined, this),
1914
+ !qrText ? /* @__PURE__ */ jsxDEV3(Text3, {
1915
+ dimColor: true,
1916
+ children: "正在创建订单..."
1917
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Fragment3, {
1918
+ children: [
1919
+ /* @__PURE__ */ jsxDEV3(Text3, {
1920
+ dimColor: true,
1921
+ children: "请用微信扫码支付:"
1922
+ }, undefined, false, undefined, this),
1923
+ /* @__PURE__ */ jsxDEV3(Box3, {
1924
+ marginTop: 1,
1925
+ children: /* @__PURE__ */ jsxDEV3(Text3, {
1926
+ children: qrText
1927
+ }, undefined, false, undefined, this)
1928
+ }, undefined, false, undefined, this),
1929
+ /* @__PURE__ */ jsxDEV3(Text3, {
1930
+ dimColor: true,
1931
+ children: "等待支付中... (按 Esc 取消)"
1932
+ }, undefined, false, undefined, this),
1933
+ pollError && /* @__PURE__ */ jsxDEV3(Text3, {
1934
+ color: "yellow",
1935
+ children: [
1936
+ "查询: ",
1937
+ pollError
1938
+ ]
1939
+ }, undefined, true, undefined, this)
1940
+ ]
1941
+ }, undefined, true, undefined, this)
1942
+ ]
1943
+ }, undefined, true, undefined, this);
1944
+ }
1945
+ if (phase === "success") {
1946
+ return /* @__PURE__ */ jsxDEV3(Box3, {
1947
+ flexDirection: "column",
1948
+ marginTop: 1,
1949
+ marginLeft: 2,
1950
+ children: [
1951
+ /* @__PURE__ */ jsxDEV3(Text3, {
1952
+ bold: true,
1953
+ color: "green",
1954
+ children: "支付成功!"
1955
+ }, undefined, false, undefined, this),
1956
+ /* @__PURE__ */ jsxDEV3(Text3, {
1957
+ children: [
1958
+ labels.desc,
1959
+ " 已到账"
1960
+ ]
1961
+ }, undefined, true, undefined, this)
1962
+ ]
1963
+ }, undefined, true, undefined, this);
1964
+ }
1965
+ return null;
1966
+ }
1967
+
1968
+ // src/ui/ChangeHat.tsx
1969
+ import { useState as useState4, useEffect as useEffect4, useRef as useRef4 } from "react";
1970
+ import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
1971
+ import QRCode4 from "qrcode";
1972
+ import { jsxDEV as jsxDEV4, Fragment as Fragment4 } from "react/jsx-dev-runtime";
1973
+ var DISPLAY_HATS = [...HATS];
1974
+ var POLL_INTERVAL4 = 3000;
1975
+ var MAX_POLL_TIME4 = 5 * 60000;
1976
+ function ChangeHat({ userId, currentHat, ownedHats, collectionCap, onComplete, onCancel }) {
1977
+ const [phase, setPhase] = useState4("select");
1978
+ const [outTradeNo, setOutTradeNo] = useState4(null);
1979
+ const [qrText, setQrText] = useState4(null);
1980
+ const [error, setError] = useState4(null);
1981
+ const [selected, setSelected] = useState4(0);
1982
+ const [chosenHat, setChosenHat] = useState4(null);
1983
+ const [pollError, setPollError] = useState4(null);
1984
+ const [pollFails, setPollFails] = useState4(0);
1985
+ const [pendingOwned, setPendingOwned] = useState4([]);
1986
+ const [manageChecked, setManageChecked] = useState4(new Set);
1987
+ const [manageSelected, setManageSelected] = useState4(0);
1988
+ const onCompleteRef = useRef4(onComplete);
1989
+ onCompleteRef.current = onComplete;
1990
+ useEffect4(() => {
1991
+ if (phase !== "qr")
1992
+ return;
1993
+ let cancelled = false;
1994
+ (async () => {
1995
+ const res = await createPurchase(userId, "changehat");
1996
+ if (cancelled)
1997
+ return;
1998
+ if (!res.ok) {
1999
+ setError(res.error);
2000
+ return;
2001
+ }
2002
+ setOutTradeNo(res.data.outTradeNo);
2003
+ const text = await QRCode4.toString(res.data.codeUrl, { type: "terminal", small: true });
2004
+ if (cancelled)
2005
+ return;
2006
+ setQrText(text);
2007
+ })();
2008
+ return () => {
2009
+ cancelled = true;
2010
+ };
2011
+ }, [phase, userId]);
2012
+ useEffect4(() => {
2013
+ if (phase !== "qr" || !outTradeNo)
2014
+ return;
2015
+ let cancelled = false;
2016
+ const start = Date.now();
2017
+ const id = setInterval(async () => {
2018
+ if (cancelled)
2019
+ return;
2020
+ if (Date.now() - start > MAX_POLL_TIME4) {
2021
+ setError("支付超时,请重试");
2022
+ clearInterval(id);
2023
+ return;
2024
+ }
2025
+ const res = await verifyPurchase(userId, outTradeNo);
2026
+ if (cancelled)
2027
+ return;
2028
+ if (res.ok && res.data.status === "SUCCESS") {
2029
+ clearInterval(id);
2030
+ handlePaymentSuccess();
2031
+ } else if (!res.ok) {
2032
+ setPollError(res.error);
2033
+ setPollFails((f) => {
2034
+ if (f + 1 >= 5) {
2035
+ setError(`查询支付状态失败: ${res.error}`);
2036
+ clearInterval(id);
2037
+ }
2038
+ return f + 1;
2039
+ });
2040
+ } else {
2041
+ setPollError(null);
2042
+ setPollFails(0);
2043
+ }
2044
+ }, POLL_INTERVAL4);
2045
+ return () => {
2046
+ cancelled = true;
2047
+ clearInterval(id);
2048
+ };
2049
+ }, [phase, outTradeNo, userId]);
2050
+ function handlePaymentSuccess() {
2051
+ if (!chosenHat)
2052
+ return;
2053
+ if (collectionCap > 0) {
2054
+ const newOwned = [...new Set([...ownedHats, chosenHat])];
2055
+ if (newOwned.length > collectionCap) {
2056
+ setPendingOwned(newOwned);
2057
+ setManageChecked(new Set(newOwned.slice(0, collectionCap)));
2058
+ setManageSelected(0);
2059
+ setPhase("manage");
2060
+ return;
2061
+ }
2062
+ setPendingOwned(newOwned);
2063
+ } else {
2064
+ setPendingOwned([]);
2065
+ }
2066
+ setPhase("reveal");
2067
+ }
2068
+ useEffect4(() => {
2069
+ if (phase !== "reveal" || !chosenHat)
2070
+ return;
2071
+ const timeout = setTimeout(() => onCompleteRef.current(chosenHat, pendingOwned), 2000);
2072
+ return () => clearTimeout(timeout);
2073
+ }, [phase, chosenHat, pendingOwned]);
2074
+ useInput4((input, key) => {
2075
+ if (phase === "reveal")
2076
+ return;
2077
+ if (key.escape) {
2078
+ onCancel();
2079
+ return;
2080
+ }
2081
+ if (phase === "select") {
2082
+ if (key.upArrow || input === "k")
2083
+ setSelected((s) => Math.max(0, s - 1));
2084
+ else if (key.downArrow || input === "j")
2085
+ setSelected((s) => Math.min(DISPLAY_HATS.length - 1, s + 1));
2086
+ else if (key.return) {
2087
+ const hat = DISPLAY_HATS[selected];
2088
+ setChosenHat(hat);
2089
+ if (hat === "none") {
2090
+ setPendingOwned(ownedHats);
2091
+ setPhase("reveal");
2092
+ return;
2093
+ }
2094
+ if (ownedHats.includes(hat)) {
2095
+ setPendingOwned(ownedHats);
2096
+ setPhase("reveal");
2097
+ return;
2098
+ }
2099
+ setOutTradeNo(null);
2100
+ setQrText(null);
2101
+ setError(null);
2102
+ setPollError(null);
2103
+ setPollFails(0);
2104
+ setPhase("qr");
2105
+ }
2106
+ const num = parseInt(input);
2107
+ if (num >= 1 && num <= DISPLAY_HATS.length)
2108
+ setSelected(num - 1);
2109
+ }
2110
+ if (phase === "manage") {
2111
+ if (key.upArrow || input === "k")
2112
+ setManageSelected((s) => Math.max(0, s - 1));
2113
+ else if (key.downArrow || input === "j")
2114
+ setManageSelected((s) => Math.min(pendingOwned.length - 1, s + 1));
2115
+ else if (input === " " || key.return) {
2116
+ const hat = pendingOwned[manageSelected];
2117
+ setManageChecked((prev) => {
2118
+ const next = new Set(prev);
2119
+ if (next.has(hat)) {
2120
+ next.delete(hat);
2121
+ } else if (next.size < collectionCap) {
2122
+ next.add(hat);
2123
+ }
2124
+ return next;
2125
+ });
2126
+ } else if (input === "c" || input === "C") {
2127
+ if (manageChecked.size === collectionCap) {
2128
+ setPendingOwned([...manageChecked]);
2129
+ setPhase("reveal");
2130
+ }
2131
+ }
2132
+ }
2133
+ });
2134
+ if (error) {
2135
+ return /* @__PURE__ */ jsxDEV4(Box4, {
2136
+ flexDirection: "column",
2137
+ marginTop: 1,
2138
+ marginLeft: 2,
2139
+ children: [
2140
+ /* @__PURE__ */ jsxDEV4(Text4, {
2141
+ color: "red",
2142
+ children: [
2143
+ "支付出错: ",
2144
+ error
2145
+ ]
2146
+ }, undefined, true, undefined, this),
2147
+ /* @__PURE__ */ jsxDEV4(Text4, {
2148
+ dimColor: true,
2149
+ children: "按 Esc 返回"
2150
+ }, undefined, false, undefined, this)
2151
+ ]
2152
+ }, undefined, true, undefined, this);
2153
+ }
2154
+ if (phase === "qr") {
2155
+ return /* @__PURE__ */ jsxDEV4(Box4, {
2156
+ flexDirection: "column",
2157
+ marginTop: 1,
2158
+ marginLeft: 2,
2159
+ children: [
2160
+ /* @__PURE__ */ jsxDEV4(Text4, {
2161
+ bold: true,
2162
+ color: "yellow",
2163
+ children: "换帽子 — 微信支付 ¥3.00"
2164
+ }, undefined, false, undefined, this),
2165
+ chosenHat && /* @__PURE__ */ jsxDEV4(Text4, {
2166
+ dimColor: true,
2167
+ children: [
2168
+ "已选择: ",
2169
+ chosenHat,
2170
+ " ",
2171
+ HAT_LINES[chosenHat].trim()
2172
+ ]
2173
+ }, undefined, true, undefined, this),
2174
+ !qrText ? /* @__PURE__ */ jsxDEV4(Text4, {
2175
+ dimColor: true,
2176
+ children: "正在创建订单..."
2177
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV4(Fragment4, {
2178
+ children: [
2179
+ /* @__PURE__ */ jsxDEV4(Text4, {
2180
+ dimColor: true,
2181
+ children: "请用微信扫码支付:"
2182
+ }, undefined, false, undefined, this),
2183
+ /* @__PURE__ */ jsxDEV4(Box4, {
2184
+ marginTop: 1,
2185
+ children: /* @__PURE__ */ jsxDEV4(Text4, {
2186
+ children: qrText
2187
+ }, undefined, false, undefined, this)
2188
+ }, undefined, false, undefined, this),
2189
+ /* @__PURE__ */ jsxDEV4(Text4, {
2190
+ dimColor: true,
2191
+ children: "等待支付中... (按 Esc 取消)"
2192
+ }, undefined, false, undefined, this),
2193
+ pollError && /* @__PURE__ */ jsxDEV4(Text4, {
2194
+ color: "yellow",
2195
+ children: [
2196
+ "查询: ",
2197
+ pollError
2198
+ ]
2199
+ }, undefined, true, undefined, this)
2200
+ ]
2201
+ }, undefined, true, undefined, this)
2202
+ ]
2203
+ }, undefined, true, undefined, this);
2204
+ }
2205
+ if (phase === "manage") {
2206
+ return /* @__PURE__ */ jsxDEV4(Box4, {
2207
+ flexDirection: "column",
2208
+ marginTop: 1,
2209
+ marginLeft: 2,
2210
+ children: [
2211
+ /* @__PURE__ */ jsxDEV4(Text4, {
2212
+ bold: true,
2213
+ color: "yellow",
2214
+ children: [
2215
+ "帽子太多了!选择保留 ",
2216
+ collectionCap,
2217
+ " 个:"
2218
+ ]
2219
+ }, undefined, true, undefined, this),
2220
+ /* @__PURE__ */ jsxDEV4(Box4, {
2221
+ marginTop: 1,
2222
+ flexDirection: "column",
2223
+ children: pendingOwned.map((hat, i) => {
2224
+ const checked = manageChecked.has(hat);
2225
+ const isSelected = i === manageSelected;
2226
+ return /* @__PURE__ */ jsxDEV4(Text4, {
2227
+ children: [
2228
+ isSelected ? "▸ " : " ",
2229
+ /* @__PURE__ */ jsxDEV4(Text4, {
2230
+ color: isSelected ? "cyan" : undefined,
2231
+ bold: isSelected,
2232
+ children: [
2233
+ checked ? "☑" : "☐",
2234
+ " ",
2235
+ hat,
2236
+ " ",
2237
+ HAT_LINES[hat].trim()
2238
+ ]
2239
+ }, undefined, true, undefined, this)
2240
+ ]
2241
+ }, hat, true, undefined, this);
2242
+ })
2243
+ }, undefined, false, undefined, this),
2244
+ /* @__PURE__ */ jsxDEV4(Box4, {
2245
+ marginTop: 1,
2246
+ children: /* @__PURE__ */ jsxDEV4(Text4, {
2247
+ dimColor: true,
2248
+ children: [
2249
+ "↑↓ 选择 空格/Enter 切换 C 确认 (",
2250
+ manageChecked.size,
2251
+ "/",
2252
+ collectionCap,
2253
+ ") Esc 取消"
2254
+ ]
2255
+ }, undefined, true, undefined, this)
2256
+ }, undefined, false, undefined, this)
2257
+ ]
2258
+ }, undefined, true, undefined, this);
2259
+ }
2260
+ if (phase === "select") {
2261
+ return /* @__PURE__ */ jsxDEV4(Box4, {
2262
+ flexDirection: "column",
2263
+ marginTop: 1,
2264
+ marginLeft: 2,
2265
+ children: [
2266
+ /* @__PURE__ */ jsxDEV4(Text4, {
2267
+ bold: true,
2268
+ color: "green",
2269
+ children: "选择帽子:"
2270
+ }, undefined, false, undefined, this),
2271
+ /* @__PURE__ */ jsxDEV4(Box4, {
2272
+ marginTop: 1,
2273
+ flexDirection: "column",
2274
+ children: DISPLAY_HATS.map((hat, i) => {
2275
+ const isCurrent = hat === currentHat;
2276
+ const isOwned = ownedHats.includes(hat) || hat === "none";
2277
+ const isSelected = i === selected;
2278
+ const displayName = hat === "none" ? "不戴帽子 (no hat)" : hat;
2279
+ const preview = hat === "none" ? "" : ` ${HAT_LINES[hat].trim()}`;
2280
+ return /* @__PURE__ */ jsxDEV4(Text4, {
2281
+ children: [
2282
+ isSelected ? "▸ " : " ",
2283
+ /* @__PURE__ */ jsxDEV4(Text4, {
2284
+ color: isSelected ? "cyan" : undefined,
2285
+ bold: isSelected,
2286
+ children: [
2287
+ displayName,
2288
+ preview
2289
+ ]
2290
+ }, undefined, true, undefined, this),
2291
+ isCurrent ? /* @__PURE__ */ jsxDEV4(Text4, {
2292
+ dimColor: true,
2293
+ children: " (当前)"
2294
+ }, undefined, false, undefined, this) : null,
2295
+ isOwned && !isCurrent && hat !== "none" ? /* @__PURE__ */ jsxDEV4(Text4, {
2296
+ color: "green",
2297
+ children: " ✓"
2298
+ }, undefined, false, undefined, this) : null
2299
+ ]
2300
+ }, hat, true, undefined, this);
2301
+ })
2302
+ }, undefined, false, undefined, this),
2303
+ /* @__PURE__ */ jsxDEV4(Box4, {
2304
+ marginTop: 1,
2305
+ children: /* @__PURE__ */ jsxDEV4(Text4, {
2306
+ dimColor: true,
2307
+ children: [
2308
+ "↑↓ 选择 Enter ",
2309
+ collectionCap > 0 ? "(已有免费/新帽¥3.00)" : "(¥3.00)",
2310
+ " Esc 取消"
2311
+ ]
2312
+ }, undefined, true, undefined, this)
2313
+ }, undefined, false, undefined, this)
2314
+ ]
2315
+ }, undefined, true, undefined, this);
2316
+ }
2317
+ if (phase === "reveal" && chosenHat) {
2318
+ const isRemove = chosenHat === "none";
2319
+ const isReEquip = ownedHats.includes(chosenHat);
2320
+ return /* @__PURE__ */ jsxDEV4(Box4, {
2321
+ flexDirection: "column",
2322
+ marginTop: 1,
2323
+ marginLeft: 2,
2324
+ alignItems: "center",
2325
+ children: [
2326
+ /* @__PURE__ */ jsxDEV4(Text4, {
2327
+ bold: true,
2328
+ color: "green",
2329
+ children: isRemove ? "帽子已摘下!" : isReEquip ? "帽子已换装!" : "新帽子到手!"
2330
+ }, undefined, false, undefined, this),
2331
+ !isRemove && /* @__PURE__ */ jsxDEV4(Text4, {
2332
+ children: HAT_LINES[chosenHat]
2333
+ }, undefined, false, undefined, this)
2334
+ ]
2335
+ }, undefined, true, undefined, this);
2336
+ }
2337
+ return null;
2338
+ }
2339
+
2340
+ // src/ui/UpgradeRarity.tsx
2341
+ import { useState as useState5, useEffect as useEffect5, useRef as useRef5 } from "react";
2342
+ import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
2343
+ import QRCode5 from "qrcode";
2344
+ import { jsxDEV as jsxDEV5, Fragment as Fragment5 } from "react/jsx-dev-runtime";
2345
+ var UPGRADE_TIERS = [
2346
+ { from: "common", to: "uncommon", toShiny: false, label: "★ → ★★", price: "¥10.00", purchaseType: "upgrade_common_uncommon" },
2347
+ { from: "uncommon", to: "rare", toShiny: false, label: "★★ → ★★★", price: "¥20.00", purchaseType: "upgrade_uncommon_rare" },
2348
+ { from: "rare", to: "epic", toShiny: false, label: "★★★ → ★★★★", price: "¥30.00", purchaseType: "upgrade_rare_epic" },
2349
+ { from: "epic", to: "legendary", toShiny: false, label: "★★★★ → ★★★★★", price: "¥50.00", purchaseType: "upgrade_epic_legendary" },
2350
+ { from: "legendary", to: "legendary", toShiny: true, label: "★★★★★ → ★★★★★✦", price: "¥100.00", purchaseType: "upgrade_legendary_shiny" }
2351
+ ];
2352
+ var RARITY_ZH = {
2353
+ common: "普通",
2354
+ uncommon: "非凡",
2355
+ rare: "稀有",
2356
+ epic: "史诗",
2357
+ legendary: "传说"
2358
+ };
2359
+ var POLL_INTERVAL5 = 3000;
2360
+ var MAX_POLL_TIME5 = 5 * 60000;
2361
+ function UpgradeRarity({ userId, currentRarity, isShiny, onComplete, onCancel }) {
2362
+ const [phase, setPhase] = useState5("info");
2363
+ const [outTradeNo, setOutTradeNo] = useState5(null);
2364
+ const [qrText, setQrText] = useState5(null);
2365
+ const [error, setError] = useState5(null);
2366
+ const [pollError, setPollError] = useState5(null);
2367
+ const [pollFails, setPollFails] = useState5(0);
2368
+ const onCompleteRef = useRef5(onComplete);
2369
+ onCompleteRef.current = onComplete;
2370
+ const tier = isShiny ? null : UPGRADE_TIERS.find((t) => t.from === currentRarity);
2371
+ const isMaxed = isShiny || currentRarity === "legendary" && isShiny;
2372
+ useEffect5(() => {
2373
+ if (phase !== "qr" || !tier)
2374
+ return;
2375
+ let cancelled = false;
2376
+ (async () => {
2377
+ const res = await createPurchase(userId, tier.purchaseType);
2378
+ if (cancelled)
2379
+ return;
2380
+ if (!res.ok) {
2381
+ setError(res.error);
2382
+ return;
2383
+ }
2384
+ setOutTradeNo(res.data.outTradeNo);
2385
+ const text = await QRCode5.toString(res.data.codeUrl, { type: "terminal", small: true });
2386
+ if (cancelled)
2387
+ return;
2388
+ setQrText(text);
2389
+ })();
2390
+ return () => {
2391
+ cancelled = true;
2392
+ };
2393
+ }, [phase, userId, tier]);
2394
+ useEffect5(() => {
2395
+ if (phase !== "qr" || !outTradeNo || !tier)
2396
+ return;
2397
+ let cancelled = false;
2398
+ const start = Date.now();
2399
+ const id = setInterval(async () => {
2400
+ if (cancelled)
2401
+ return;
2402
+ if (Date.now() - start > MAX_POLL_TIME5) {
2403
+ setError("支付超时,请重试");
2404
+ clearInterval(id);
2405
+ return;
2406
+ }
2407
+ const res = await verifyPurchase(userId, outTradeNo);
2408
+ if (cancelled)
2409
+ return;
2410
+ if (res.ok && res.data.status === "SUCCESS") {
2411
+ clearInterval(id);
2412
+ setPhase("reveal");
2413
+ } else if (!res.ok) {
2414
+ setPollError(res.error);
2415
+ setPollFails((f) => {
2416
+ if (f + 1 >= 5) {
2417
+ setError(`查询支付状态失败: ${res.error}`);
2418
+ clearInterval(id);
2419
+ }
2420
+ return f + 1;
2421
+ });
2422
+ } else {
2423
+ setPollError(null);
2424
+ setPollFails(0);
2425
+ }
2426
+ }, POLL_INTERVAL5);
2427
+ return () => {
2428
+ cancelled = true;
2429
+ clearInterval(id);
2430
+ };
2431
+ }, [phase, outTradeNo, userId, tier]);
2432
+ useEffect5(() => {
2433
+ if (phase !== "reveal" || !tier)
2434
+ return;
2435
+ const timeout = setTimeout(() => onCompleteRef.current(tier.to, tier.toShiny), 2000);
2436
+ return () => clearTimeout(timeout);
2437
+ }, [phase, tier]);
2438
+ useInput5((input, key) => {
2439
+ if (key.escape) {
2440
+ onCancel();
2441
+ return;
2442
+ }
2443
+ if (phase === "info" && key.return && tier) {
2444
+ setOutTradeNo(null);
2445
+ setQrText(null);
2446
+ setError(null);
2447
+ setPollError(null);
2448
+ setPollFails(0);
2449
+ setPhase("qr");
2450
+ }
2451
+ });
2452
+ if (error) {
2453
+ return /* @__PURE__ */ jsxDEV5(Box5, {
2454
+ flexDirection: "column",
2455
+ marginTop: 1,
2456
+ marginLeft: 2,
2457
+ children: [
2458
+ /* @__PURE__ */ jsxDEV5(Text5, {
2459
+ color: "red",
2460
+ children: [
2461
+ "支付出错: ",
2462
+ error
2463
+ ]
2464
+ }, undefined, true, undefined, this),
2465
+ /* @__PURE__ */ jsxDEV5(Text5, {
2466
+ dimColor: true,
2467
+ children: "按 Esc 返回"
2468
+ }, undefined, false, undefined, this)
2469
+ ]
2470
+ }, undefined, true, undefined, this);
2471
+ }
2472
+ if (phase === "info") {
2473
+ if (isMaxed) {
2474
+ return /* @__PURE__ */ jsxDEV5(Box5, {
2475
+ flexDirection: "column",
2476
+ marginTop: 1,
2477
+ marginLeft: 2,
2478
+ children: [
2479
+ /* @__PURE__ */ jsxDEV5(Text5, {
2480
+ bold: true,
2481
+ color: "magenta",
2482
+ children: "✨ 已满级! ★★★★★✦ 闪光传说"
2483
+ }, undefined, false, undefined, this),
2484
+ /* @__PURE__ */ jsxDEV5(Text5, {
2485
+ dimColor: true,
2486
+ children: "你的宝贝已经是最高稀有度了~"
2487
+ }, undefined, false, undefined, this),
2488
+ /* @__PURE__ */ jsxDEV5(Box5, {
2489
+ marginTop: 1,
2490
+ children: /* @__PURE__ */ jsxDEV5(Text5, {
2491
+ dimColor: true,
2492
+ children: "按 Esc 返回"
2493
+ }, undefined, false, undefined, this)
2494
+ }, undefined, false, undefined, this)
2495
+ ]
2496
+ }, undefined, true, undefined, this);
2497
+ }
2498
+ return /* @__PURE__ */ jsxDEV5(Box5, {
2499
+ flexDirection: "column",
2500
+ marginTop: 1,
2501
+ marginLeft: 2,
2502
+ children: [
2503
+ /* @__PURE__ */ jsxDEV5(Text5, {
2504
+ bold: true,
2505
+ color: "yellow",
2506
+ children: "升级稀有度"
2507
+ }, undefined, false, undefined, this),
2508
+ /* @__PURE__ */ jsxDEV5(Box5, {
2509
+ marginTop: 1,
2510
+ flexDirection: "column",
2511
+ children: [
2512
+ /* @__PURE__ */ jsxDEV5(Text5, {
2513
+ children: [
2514
+ "当前: ",
2515
+ /* @__PURE__ */ jsxDEV5(Text5, {
2516
+ bold: true,
2517
+ children: RARITY_ZH[currentRarity]
2518
+ }, undefined, false, undefined, this),
2519
+ " ",
2520
+ RARITY_STARS[currentRarity]
2521
+ ]
2522
+ }, undefined, true, undefined, this),
2523
+ tier && /* @__PURE__ */ jsxDEV5(Fragment5, {
2524
+ children: [
2525
+ /* @__PURE__ */ jsxDEV5(Text5, {
2526
+ children: [
2527
+ "升级: ",
2528
+ /* @__PURE__ */ jsxDEV5(Text5, {
2529
+ bold: true,
2530
+ color: "green",
2531
+ children: tier.toShiny ? "闪光传说" : RARITY_ZH[tier.to]
2532
+ }, undefined, false, undefined, this),
2533
+ " ",
2534
+ tier.label
2535
+ ]
2536
+ }, undefined, true, undefined, this),
2537
+ /* @__PURE__ */ jsxDEV5(Text5, {
2538
+ children: [
2539
+ "费用: ",
2540
+ /* @__PURE__ */ jsxDEV5(Text5, {
2541
+ bold: true,
2542
+ color: "yellow",
2543
+ children: tier.price
2544
+ }, undefined, false, undefined, this)
2545
+ ]
2546
+ }, undefined, true, undefined, this)
2547
+ ]
2548
+ }, undefined, true, undefined, this)
2549
+ ]
2550
+ }, undefined, true, undefined, this),
2551
+ /* @__PURE__ */ jsxDEV5(Box5, {
2552
+ marginTop: 1,
2553
+ flexDirection: "column",
2554
+ children: [
2555
+ /* @__PURE__ */ jsxDEV5(Text5, {
2556
+ dimColor: true,
2557
+ children: "升级路线:"
2558
+ }, undefined, false, undefined, this),
2559
+ UPGRADE_TIERS.map((t) => {
2560
+ const isCurrent = t.from === currentRarity && !isShiny;
2561
+ return /* @__PURE__ */ jsxDEV5(Text5, {
2562
+ dimColor: !isCurrent,
2563
+ children: [
2564
+ isCurrent ? "▸ " : " ",
2565
+ t.label,
2566
+ " ",
2567
+ t.price
2568
+ ]
2569
+ }, t.purchaseType, true, undefined, this);
2570
+ })
2571
+ ]
2572
+ }, undefined, true, undefined, this),
2573
+ /* @__PURE__ */ jsxDEV5(Box5, {
2574
+ marginTop: 1,
2575
+ children: /* @__PURE__ */ jsxDEV5(Text5, {
2576
+ dimColor: true,
2577
+ children: "Enter 去支付 Esc 取消"
2578
+ }, undefined, false, undefined, this)
2579
+ }, undefined, false, undefined, this)
2580
+ ]
2581
+ }, undefined, true, undefined, this);
2582
+ }
2583
+ if (phase === "qr" && tier) {
2584
+ return /* @__PURE__ */ jsxDEV5(Box5, {
2585
+ flexDirection: "column",
2586
+ marginTop: 1,
2587
+ marginLeft: 2,
2588
+ children: [
2589
+ /* @__PURE__ */ jsxDEV5(Text5, {
2590
+ bold: true,
2591
+ color: "yellow",
2592
+ children: [
2593
+ "升级稀有度 — 微信支付 ",
2594
+ tier.price
2595
+ ]
2596
+ }, undefined, true, undefined, this),
2597
+ /* @__PURE__ */ jsxDEV5(Text5, {
2598
+ dimColor: true,
2599
+ children: tier.label
2600
+ }, undefined, false, undefined, this),
2601
+ !qrText ? /* @__PURE__ */ jsxDEV5(Text5, {
2602
+ dimColor: true,
2603
+ children: "正在创建订单..."
2604
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV5(Fragment5, {
2605
+ children: [
2606
+ /* @__PURE__ */ jsxDEV5(Text5, {
2607
+ dimColor: true,
2608
+ children: "请用微信扫码支付:"
2609
+ }, undefined, false, undefined, this),
2610
+ /* @__PURE__ */ jsxDEV5(Box5, {
2611
+ marginTop: 1,
2612
+ children: /* @__PURE__ */ jsxDEV5(Text5, {
2613
+ children: qrText
2614
+ }, undefined, false, undefined, this)
2615
+ }, undefined, false, undefined, this),
2616
+ /* @__PURE__ */ jsxDEV5(Text5, {
2617
+ dimColor: true,
2618
+ children: "等待支付中... (按 Esc 取消)"
2619
+ }, undefined, false, undefined, this),
2620
+ pollError && /* @__PURE__ */ jsxDEV5(Text5, {
2621
+ color: "yellow",
2622
+ children: [
2623
+ "查询: ",
2624
+ pollError
2625
+ ]
2626
+ }, undefined, true, undefined, this)
2627
+ ]
2628
+ }, undefined, true, undefined, this)
2629
+ ]
2630
+ }, undefined, true, undefined, this);
2631
+ }
2632
+ if (phase === "reveal" && tier) {
2633
+ return /* @__PURE__ */ jsxDEV5(Box5, {
2634
+ flexDirection: "column",
2635
+ marginTop: 1,
2636
+ marginLeft: 2,
2637
+ alignItems: "center",
2638
+ children: [
2639
+ /* @__PURE__ */ jsxDEV5(Text5, {
2640
+ bold: true,
2641
+ color: "magenta",
2642
+ children: "稀有度提升!"
2643
+ }, undefined, false, undefined, this),
2644
+ /* @__PURE__ */ jsxDEV5(Text5, {
2645
+ bold: true,
2646
+ children: tier.toShiny ? "★★★★★✦ 闪光传说 ✨" : `${RARITY_STARS[tier.to]} ${RARITY_ZH[tier.to]}`
2647
+ }, undefined, false, undefined, this)
2648
+ ]
2649
+ }, undefined, true, undefined, this);
2650
+ }
2651
+ return null;
2652
+ }
2653
+
2654
+ // src/ui/StoryOverlay.tsx
2655
+ import { useState as useState6, useEffect as useEffect6 } from "react";
2656
+ import { Box as Box6, Text as Text6, useInput as useInput6 } from "ink";
2657
+
2658
+ // src/stories/storyData.ts
2659
+ var STORIES = [
2660
+ {
2661
+ id: 0,
2662
+ titleZh: "第一颗星",
2663
+ titleEn: "First Star",
2664
+ frames: [
2665
+ {
2666
+ art: [
2667
+ "",
2668
+ "",
2669
+ " ·",
2670
+ "",
2671
+ "",
2672
+ ""
2673
+ ],
2674
+ textZh: "...好像...有什么在发光",
2675
+ textEn: "...something is...glowing",
2676
+ textColor: "gray",
2677
+ durationTicks: 6,
2678
+ showPet: true
2679
+ },
2680
+ {
2681
+ art: [
2682
+ "",
2683
+ "",
2684
+ " ✧",
2685
+ "",
2686
+ "",
2687
+ ""
2688
+ ],
2689
+ artColors: ["", "", "yellow", "", "", ""],
2690
+ durationTicks: 4,
2691
+ showPet: true
2692
+ },
2693
+ {
2694
+ art: [
2695
+ "",
2696
+ " · ✧ ·",
2697
+ " · ·",
2698
+ "",
2699
+ ""
2700
+ ],
2701
+ artColors: ["", "yellow", "yellow", "", ""],
2702
+ durationTicks: 4,
2703
+ showPet: true
2704
+ },
2705
+ {
2706
+ art: [
2707
+ " · ⋆ ✧ ⋆ ·",
2708
+ " ⋆ ★ ★ ⋆",
2709
+ " · ⋆ ✧ ⋆ ·",
2710
+ " · · · ·",
2711
+ ""
2712
+ ],
2713
+ artColors: ["cyan", "yellow", "cyan", "gray", ""],
2714
+ textZh: "是你...照亮了这片夜空",
2715
+ textEn: "You...lit up this sky",
2716
+ textColor: "cyan",
2717
+ durationTicks: 6,
2718
+ showPet: true
2719
+ },
2720
+ {
2721
+ art: [
2722
+ " ˚ · ⋆ ✧ ⋆ · ˚",
2723
+ " ˚ ★ ˚",
2724
+ " ✧ ★ ✧",
2725
+ " ⋆ ⋆",
2726
+ ""
2727
+ ],
2728
+ artColors: ["gray", "yellow", "yellow", "cyan", ""],
2729
+ durationTicks: 6,
2730
+ showPet: true
2731
+ },
2732
+ {
2733
+ art: [
2734
+ "",
2735
+ " ˚ · ⋆ · ✧ · ⋆ · ˚",
2736
+ "",
2737
+ " ━━━━━━━━━━━━━━━━━━━",
2738
+ "",
2739
+ ""
2740
+ ],
2741
+ artColors: ["", "yellow", "", "gray", "", ""],
2742
+ textZh: "第一颗星,因你而亮。",
2743
+ textEn: "The first star shines because of you.",
2744
+ textColor: "yellow",
2745
+ durationTicks: 8,
2746
+ showPet: true
2747
+ },
2748
+ {
2749
+ art: [
2750
+ "",
2751
+ "",
2752
+ " ✧ 第一颗星 ✧",
2753
+ " First Star",
2754
+ "",
2755
+ " [ 新剧情解锁 ]",
2756
+ ""
2757
+ ],
2758
+ artColors: ["", "", "yellow", "gray", "", "yellow", ""],
2759
+ durationTicks: 6
2760
+ }
2761
+ ]
2762
+ },
2763
+ {
2764
+ id: 1,
2765
+ titleZh: "雨的歌",
2766
+ titleEn: "Rain Song",
2767
+ frames: [
2768
+ {
2769
+ art: [
2770
+ " · ·",
2771
+ " ·",
2772
+ " · ·",
2773
+ " · ·",
2774
+ ""
2775
+ ],
2776
+ artColors: ["cyan", "cyan", "cyan", "cyan", ""],
2777
+ textZh: "下雨了...",
2778
+ textEn: "It's raining...",
2779
+ textColor: "cyan",
2780
+ durationTicks: 5,
2781
+ showPet: true
2782
+ },
2783
+ {
2784
+ art: [
2785
+ " │ ╎ │ ╎ │ ╎",
2786
+ " ╎ │ ╎ │ ╎",
2787
+ " ╎ │ ╎ │ ╎ │",
2788
+ " │ ╎ │ ╎ │",
2789
+ " ~~~~~~~~~~"
2790
+ ],
2791
+ artColors: ["cyan", "cyan", "cyan", "cyan", "blue"],
2792
+ durationTicks: 4,
2793
+ showPet: true
2794
+ },
2795
+ {
2796
+ art: [
2797
+ " │ ♪ │ ╎ │ ♫",
2798
+ " ╎ │ ╎ ♪ ╎",
2799
+ " ╎ │ ♫ │ ╎ │",
2800
+ " ♪ ╎ │ ╎ │",
2801
+ " ~~~~~~~~~~"
2802
+ ],
2803
+ artColors: ["magenta", "magenta", "magenta", "magenta", "blue"],
2804
+ textZh: "听...雨在唱歌",
2805
+ textEn: "Listen...the rain is singing",
2806
+ textColor: "cyan",
2807
+ durationTicks: 6,
2808
+ showPet: true
2809
+ },
2810
+ {
2811
+ art: [
2812
+ " · ♪ ·",
2813
+ "",
2814
+ " ╭──────────────────╮",
2815
+ " ╭────────────────────╮",
2816
+ " ╭──────────────────────╮",
2817
+ " ╰────────────────────╯",
2818
+ " ╰──────────────────╯"
2819
+ ],
2820
+ artColors: ["gray", "", "red", "yellow", "green", "cyan", "blue"],
2821
+ durationTicks: 6,
2822
+ showPet: true
2823
+ },
2824
+ {
2825
+ art: [
2826
+ " · ˚ · ˚ ·",
2827
+ "",
2828
+ "",
2829
+ ""
2830
+ ],
2831
+ artColors: ["gray", "", "", ""],
2832
+ textZh: "淋过雨的人,更懂温暖",
2833
+ textEn: "Those who've known rain understand warmth",
2834
+ textColor: "cyan",
2835
+ durationTicks: 8,
2836
+ showPet: true
2837
+ },
2838
+ {
2839
+ art: [
2840
+ "",
2841
+ "",
2842
+ " ♪ 雨的歌 ♫",
2843
+ " Rain Song",
2844
+ "",
2845
+ " [ 新剧情解锁 ]",
2846
+ ""
2847
+ ],
2848
+ artColors: ["", "", "cyan", "gray", "", "yellow", ""],
2849
+ durationTicks: 6
2850
+ }
2851
+ ]
2852
+ },
2853
+ {
2854
+ id: 2,
2855
+ titleZh: "夜花园",
2856
+ titleEn: "Night Garden",
2857
+ frames: [
2858
+ {
2859
+ art: [
2860
+ " ░░░░░░░░░░░░░░░░░░",
2861
+ " ░░░░░░░░░░░░░░░░░░",
2862
+ " ░░░░░░░░░░░░░░░░░░",
2863
+ ""
2864
+ ],
2865
+ artColors: ["gray", "gray", "gray", ""],
2866
+ textZh: "闭上眼睛...",
2867
+ textEn: "Close your eyes...",
2868
+ textColor: "gray",
2869
+ durationTicks: 5,
2870
+ showPet: true
2871
+ },
2872
+ {
2873
+ art: [
2874
+ " ░░░░░░░░░░░░░░░░░░",
2875
+ " ░░░░░░░░░░░░░░░░░░",
2876
+ " ░░░░░░░❁░░░░░░░░░░",
2877
+ " ░░░░░░░|░░░░░░░░░░",
2878
+ ""
2879
+ ],
2880
+ artColors: ["gray", "gray", "green", "green", ""],
2881
+ durationTicks: 4,
2882
+ showPet: true
2883
+ },
2884
+ {
2885
+ art: [
2886
+ " ░░░░░░░░░░░░░░░░░░",
2887
+ " ░░❀░░░░░░░░░❀░░░░",
2888
+ " ░░░░░✿░❁░✿░░░░░░░",
2889
+ " ░░░░░░|░░░|░░░░░░",
2890
+ ""
2891
+ ],
2892
+ artColors: ["gray", "magenta", "magenta", "green", ""],
2893
+ durationTicks: 4,
2894
+ showPet: true
2895
+ },
2896
+ {
2897
+ art: [
2898
+ " ❀ ✿ ❁ ✿ ❀",
2899
+ " ✿ ❁ ❀ ✿ ❁ ❀ ✿ ❁",
2900
+ " ❁ ✿ ❀ ❁ ✿ ❀",
2901
+ " ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌",
2902
+ ""
2903
+ ],
2904
+ artColors: ["magenta", "red", "magenta", "green", ""],
2905
+ textZh: "你的爱,让花开了",
2906
+ textEn: "Your love made the flowers bloom",
2907
+ textColor: "magenta",
2908
+ durationTicks: 6,
2909
+ showPet: true
2910
+ },
2911
+ {
2912
+ art: [
2913
+ " · ✿ · ❀ ·",
2914
+ " ❁ ✿",
2915
+ " · ❀ ❁ ·",
2916
+ "",
2917
+ " ✿ · ❀ · ✿"
2918
+ ],
2919
+ artColors: ["magenta", "red", "magenta", "", "magenta"],
2920
+ durationTicks: 6,
2921
+ showPet: true
2922
+ },
2923
+ {
2924
+ art: [
2925
+ "",
2926
+ "",
2927
+ " ❀ 夜花园 ✿",
2928
+ " Night Garden",
2929
+ "",
2930
+ " [ 新剧情解锁 ]",
2931
+ ""
2932
+ ],
2933
+ artColors: ["", "", "magenta", "gray", "", "yellow", ""],
2934
+ durationTicks: 6
2935
+ }
2936
+ ]
2937
+ },
2938
+ {
2939
+ id: 3,
2940
+ titleZh: "星座",
2941
+ titleEn: "Constellation",
2942
+ frames: [
2943
+ {
2944
+ art: [
2945
+ " · · · ·",
2946
+ " · · ·",
2947
+ " · · · ·",
2948
+ " · · ·",
2949
+ " · · · ·"
2950
+ ],
2951
+ artColors: ["gray", "gray", "gray", "gray", "gray"],
2952
+ textZh: "你看,天上的星星...",
2953
+ textEn: "Look, the stars above...",
2954
+ textColor: "gray",
2955
+ durationTicks: 5,
2956
+ showPet: true
2957
+ },
2958
+ {
2959
+ art: [
2960
+ " · ✧ · ✧",
2961
+ " · ✧ ·",
2962
+ " ✧ · ✧ ·",
2963
+ " · · ✧",
2964
+ " · ✧ · ·"
2965
+ ],
2966
+ artColors: ["yellow", "yellow", "yellow", "yellow", "yellow"],
2967
+ durationTicks: 4,
2968
+ showPet: true
2969
+ },
2970
+ {
2971
+ art: [
2972
+ " · ✧━━━━━✧ ·",
2973
+ " · ╲ ╱ ·",
2974
+ " · ★ ★ ·",
2975
+ " · ╲ ╱ ·",
2976
+ " · · ━★━ · ·"
2977
+ ],
2978
+ artColors: ["cyan", "cyan", "yellow", "cyan", "yellow"],
2979
+ textZh: "它们连在一起了!",
2980
+ textEn: "They're connecting!",
2981
+ textColor: "cyan",
2982
+ durationTicks: 6,
2983
+ showPet: true
2984
+ },
2985
+ {
2986
+ art: [
2987
+ " ★━━━━━━━★",
2988
+ " ┃ ✧ ✧ ┃",
2989
+ " ★ ━ ★",
2990
+ " ╲ ╱",
2991
+ " ★━━━★"
2992
+ ],
2993
+ artColors: ["yellow", "cyan", "yellow", "cyan", "yellow"],
2994
+ textZh: "是你的形状!",
2995
+ textEn: "It's shaped like you!",
2996
+ textColor: "yellow",
2997
+ durationTicks: 6,
2998
+ showPet: true
2999
+ },
3000
+ {
3001
+ art: [
3002
+ " ˚ · ⋆ · ✧ · ⋆ · ˚",
3003
+ " ⋆ ★━━━━━━━★ ⋆",
3004
+ " ┃ ✧ ✧ ┃",
3005
+ " ★ ━ ★",
3006
+ " ˚ ╲ ╱ ˚",
3007
+ " ⋆ ★━━━★ ⋆",
3008
+ " ˚ · ⋆ · ✧ · ⋆ · ˚"
3009
+ ],
3010
+ artColors: ["gray", "yellow", "cyan", "yellow", "gray", "yellow", "gray"],
3011
+ textZh: "从今天起,天上有一个属于你的星座",
3012
+ textEn: "From today, there's a constellation just for you",
3013
+ textColor: "cyan",
3014
+ durationTicks: 8
3015
+ },
3016
+ {
3017
+ art: [
3018
+ "",
3019
+ "",
3020
+ " ★ 星座 ★",
3021
+ " Constellation",
3022
+ "",
3023
+ " [ 新剧情解锁 ]",
3024
+ ""
3025
+ ],
3026
+ artColors: ["", "", "yellow", "gray", "", "yellow", ""],
3027
+ durationTicks: 6
3028
+ }
3029
+ ]
3030
+ },
3031
+ {
3032
+ id: 4,
3033
+ titleZh: "摇篮曲",
3034
+ titleEn: "Lullaby",
3035
+ frames: [
3036
+ {
3037
+ art: [
3038
+ " ☾",
3039
+ " · · · · · ·",
3040
+ "",
3041
+ ""
3042
+ ],
3043
+ artColors: ["yellow", "gray", "", ""],
3044
+ textZh: "夜深了...",
3045
+ textEn: "It's late...",
3046
+ textColor: "gray",
3047
+ durationTicks: 5,
3048
+ showPet: true
3049
+ },
3050
+ {
3051
+ art: [
3052
+ " ☾",
3053
+ " · · · · · ·",
3054
+ " ♪",
3055
+ " ♫",
3056
+ " ♪"
3057
+ ],
3058
+ artColors: ["yellow", "gray", "magenta", "magenta", "magenta"],
3059
+ textZh: "让我唱首歌给你听吧",
3060
+ textEn: "Let me sing you a song",
3061
+ textColor: "magenta",
3062
+ durationTicks: 6,
3063
+ showPet: true
3064
+ },
3065
+ {
3066
+ art: [
3067
+ " ☾",
3068
+ " ♪ · ♫ · ♪ · ♫ · ♪",
3069
+ " ~~~~~~~~~~~",
3070
+ " ♫ ♪",
3071
+ ""
3072
+ ],
3073
+ artColors: ["yellow", "magenta", "blue", "magenta", ""],
3074
+ durationTicks: 6,
3075
+ showPet: true
3076
+ },
3077
+ {
3078
+ art: [
3079
+ " ☾ z",
3080
+ " ♪ · ♫ · ♪ · z · ♪",
3081
+ " ~~~~~~z~~~~",
3082
+ " z",
3083
+ ""
3084
+ ],
3085
+ artColors: ["yellow", "magenta", "blue", "cyan", ""],
3086
+ textZh: "你一直在照顾我",
3087
+ textEn: "You've always taken care of me",
3088
+ textColor: "cyan",
3089
+ durationTicks: 6,
3090
+ showPet: true
3091
+ },
3092
+ {
3093
+ art: [
3094
+ " ☾",
3095
+ " · ˚ · ˚ · ˚ · ˚ ·",
3096
+ "",
3097
+ "",
3098
+ ""
3099
+ ],
3100
+ artColors: ["yellow", "gray", "", "", ""],
3101
+ textZh: "现在...换我守着你",
3102
+ textEn: "Now...it's my turn to watch over you",
3103
+ textColor: "yellow",
3104
+ durationTicks: 8,
3105
+ showPet: true
3106
+ },
3107
+ {
3108
+ art: [
3109
+ "",
3110
+ "",
3111
+ " ♫ 摇篮曲 ♪",
3112
+ " Lullaby",
3113
+ "",
3114
+ " [ 新剧情解锁 ]",
3115
+ ""
3116
+ ],
3117
+ artColors: ["", "", "magenta", "gray", "", "yellow", ""],
3118
+ durationTicks: 6
3119
+ }
3120
+ ]
3121
+ },
3122
+ {
3123
+ id: 5,
3124
+ titleZh: "一封信",
3125
+ titleEn: "A Letter",
3126
+ frames: [
3127
+ {
3128
+ art: [
3129
+ "",
3130
+ " ╭─────────────╮",
3131
+ " │ ╲ ╱ │",
3132
+ " │ ╲ ╱ │",
3133
+ " │ ╳ │",
3134
+ " │ ╱ ╲ │",
3135
+ " ╰─────────────╯"
3136
+ ],
3137
+ artColors: ["", "white", "white", "white", "white", "white", "white"],
3138
+ textZh: "这是...给你的",
3139
+ textEn: "This is...for you",
3140
+ textColor: "gray",
3141
+ durationTicks: 5,
3142
+ showPet: true
3143
+ },
3144
+ {
3145
+ art: [
3146
+ " ╲ ╱",
3147
+ " ╲ ╱",
3148
+ " ╭─────────────╮",
3149
+ " │ │",
3150
+ " │ │",
3151
+ " │ │",
3152
+ " ╰─────────────╯"
3153
+ ],
3154
+ artColors: ["white", "white", "white", "white", "white", "white", "white"],
3155
+ durationTicks: 4
3156
+ },
3157
+ {
3158
+ art: [
3159
+ " ╭─────────────╮",
3160
+ " │ 亲爱的你: │",
3161
+ " │ │",
3162
+ " │ 谢谢你每天 │",
3163
+ " │ 来看我 │",
3164
+ " │ │",
3165
+ " │ 有你真好 ❤ │",
3166
+ " ╰─────────────╯"
3167
+ ],
3168
+ artColors: ["white", "cyan", "white", "cyan", "cyan", "white", "red", "white"],
3169
+ durationTicks: 10
3170
+ },
3171
+ {
3172
+ art: [
3173
+ " · · · · ·",
3174
+ "",
3175
+ " ❤❤",
3176
+ " ❤ ❤",
3177
+ " ❤",
3178
+ ""
3179
+ ],
3180
+ artColors: ["gray", "", "red", "red", "red", ""],
3181
+ durationTicks: 6,
3182
+ showPet: true
3183
+ },
3184
+ {
3185
+ art: [
3186
+ " ˚ · ˚ · ˚ · ˚ · ˚",
3187
+ "",
3188
+ " ❤",
3189
+ "",
3190
+ ""
3191
+ ],
3192
+ artColors: ["gray", "", "red", "", ""],
3193
+ textZh: "世界很大,谢谢你选择陪伴我",
3194
+ textEn: "The world is vast — thank you for choosing me",
3195
+ textColor: "red",
3196
+ durationTicks: 8,
3197
+ showPet: true
3198
+ },
3199
+ {
3200
+ art: [
3201
+ "",
3202
+ "",
3203
+ " ❤ 一封信 ❤",
3204
+ " A Letter",
3205
+ "",
3206
+ " [ 新剧情解锁 ]",
3207
+ ""
3208
+ ],
3209
+ artColors: ["", "", "red", "gray", "", "yellow", ""],
3210
+ durationTicks: 6
3211
+ }
3212
+ ]
3213
+ },
3214
+ {
3215
+ id: 6,
3216
+ titleZh: "极光",
3217
+ titleEn: "Aurora",
3218
+ frames: [
3219
+ {
3220
+ art: [
3221
+ " · · · · ·",
3222
+ " · · · ·",
3223
+ " · · · ·",
3224
+ ""
3225
+ ],
3226
+ artColors: ["gray", "gray", "gray", ""],
3227
+ textZh: "抬头看...",
3228
+ textEn: "Look up...",
3229
+ textColor: "gray",
3230
+ durationTicks: 5,
3231
+ showPet: true
3232
+ },
3233
+ {
3234
+ art: [
3235
+ " ░▒▓░▒░▒▓░▒░▒▓░▒░",
3236
+ " · ░▒░ · ░▒░ ·",
3237
+ " · · · ·",
3238
+ ""
3239
+ ],
3240
+ artColors: ["green", "green", "gray", ""],
3241
+ durationTicks: 4,
3242
+ showPet: true
3243
+ },
3244
+ {
3245
+ art: [
3246
+ " ▓▒░▓▒░▒▓░▒▓▒░▓▒░",
3247
+ " ░▒▓░▒▓▒░▓░▒▓░▒▓░",
3248
+ " ▒░▓▒░▓░▒▓▒░▓▒░▓▒",
3249
+ " · · ˚ · ˚ · ·",
3250
+ ""
3251
+ ],
3252
+ artColors: ["green", "cyan", "magenta", "gray", ""],
3253
+ textZh: "极光...",
3254
+ textEn: "Aurora...",
3255
+ textColor: "green",
3256
+ durationTicks: 6,
3257
+ showPet: true
3258
+ },
3259
+ {
3260
+ art: [
3261
+ " ░▒▓▒░▒▓▒░▒▓▒░▒▓▒░",
3262
+ " ▓▒░▓▒░▓▒░▓▒░▓▒░▓",
3263
+ " ▒░▓▒░▓▒░▓▒░▓▒░▓▒░",
3264
+ " · ˚ · ˚ · ˚ ·",
3265
+ ""
3266
+ ],
3267
+ artColors: ["green", "cyan", "magenta", "gray", ""],
3268
+ textZh: "只有心满意足的时候,才看得见的光",
3269
+ textEn: "A light visible only when your heart is full",
3270
+ textColor: "green",
3271
+ durationTicks: 8,
3272
+ showPet: true
3273
+ },
3274
+ {
3275
+ art: [
3276
+ " · ˚ ░▒░ ˚ · ˚ ░▒░ ˚ ·",
3277
+ " ˚ · ˚ ░▒░ ˚ · ˚ ░▒░ ˚",
3278
+ "",
3279
+ ""
3280
+ ],
3281
+ artColors: ["cyan", "green", "", ""],
3282
+ durationTicks: 6,
3283
+ showPet: true
3284
+ },
3285
+ {
3286
+ art: [
3287
+ "",
3288
+ "",
3289
+ " ✧ 极光 ✧",
3290
+ " Aurora",
3291
+ "",
3292
+ " [ 新剧情解锁 ]",
3293
+ ""
3294
+ ],
3295
+ artColors: ["", "", "green", "gray", "", "yellow", ""],
3296
+ durationTicks: 6
3297
+ }
3298
+ ]
3299
+ },
3300
+ {
3301
+ id: 7,
3302
+ titleZh: "梦",
3303
+ titleEn: "Dream",
3304
+ frames: [
3305
+ {
3306
+ art: [
3307
+ " ~~~~~~~~~~~",
3308
+ " ~ ~ ~ ~ ~ ~",
3309
+ " ~~~~~~~~~~~",
3310
+ ""
3311
+ ],
3312
+ artColors: ["blue", "blue", "blue", ""],
3313
+ textZh: "我做了一个梦...",
3314
+ textEn: "I had a dream...",
3315
+ textColor: "blue",
3316
+ durationTicks: 5,
3317
+ showPet: true
3318
+ },
3319
+ {
3320
+ art: [
3321
+ " · ˚ · ˚ ·",
3322
+ " ~~~~~~~~~~~",
3323
+ "",
3324
+ " ┊",
3325
+ " ┊",
3326
+ " ╱╲"
3327
+ ],
3328
+ artColors: ["gray", "blue", "", "white", "white", "white"],
3329
+ textZh: "梦里有你",
3330
+ textEn: "You were in it",
3331
+ textColor: "cyan",
3332
+ durationTicks: 6,
3333
+ showPet: true
3334
+ },
3335
+ {
3336
+ art: [
3337
+ " · ˚ · ˚ ·",
3338
+ " ~~~~~~~~~~~",
3339
+ "",
3340
+ " ┊",
3341
+ " ┊",
3342
+ " ╱╲"
3343
+ ],
3344
+ artColors: ["gray", "blue", "", "white", "white", "white"],
3345
+ durationTicks: 5,
3346
+ showPet: true
3347
+ },
3348
+ {
3349
+ art: [
3350
+ " · ˚ · ˚ ·",
3351
+ " ~~~~~~~~~~~",
3352
+ "",
3353
+ " ✧",
3354
+ " ┊┊",
3355
+ " ╱╲"
3356
+ ],
3357
+ artColors: ["gray", "blue", "", "yellow", "white", "white"],
3358
+ textZh: "在梦里,我能摸到你的手",
3359
+ textEn: "In the dream, I could touch your hand",
3360
+ textColor: "yellow",
3361
+ durationTicks: 8,
3362
+ showPet: true
3363
+ },
3364
+ {
3365
+ art: [
3366
+ " ˚ · ˚ · ˚ ·",
3367
+ " · ˚ · ˚ · ˚",
3368
+ " ˚ · ˚ · ˚",
3369
+ "",
3370
+ ""
3371
+ ],
3372
+ artColors: ["yellow", "yellow", "yellow", "", ""],
3373
+ textZh: "就算醒来也没关系,因为你还在这里",
3374
+ textEn: "Even when we wake, it's okay — you're still here",
3375
+ textColor: "yellow",
3376
+ durationTicks: 8,
3377
+ showPet: true
3378
+ },
3379
+ {
3380
+ art: [
3381
+ "",
3382
+ "",
3383
+ " ˚ 梦 ˚",
3384
+ " Dream",
3385
+ "",
3386
+ " [ 新剧情解锁 ]",
3387
+ ""
3388
+ ],
3389
+ artColors: ["", "", "blue", "gray", "", "yellow", ""],
3390
+ durationTicks: 6
3391
+ }
3392
+ ]
3393
+ },
3394
+ {
3395
+ id: 8,
3396
+ titleZh: "四季",
3397
+ titleEn: "Seasons",
3398
+ frames: [
3399
+ {
3400
+ art: [
3401
+ " ✿ ❀ ✿ ❀ ✿",
3402
+ " ❀ ✿ ❀ ✿ ❀ ✿ ❀",
3403
+ " ━━━━━━━━━━━━━━━━━━",
3404
+ " ❀· · ❀ · ·❀",
3405
+ ""
3406
+ ],
3407
+ artColors: ["magenta", "magenta", "green", "magenta", ""],
3408
+ textZh: "春",
3409
+ textEn: "Spring",
3410
+ textColor: "magenta",
3411
+ durationTicks: 5,
3412
+ showPet: true
3413
+ },
3414
+ {
3415
+ art: [
3416
+ " ╲ │ ╱",
3417
+ " ─── ☀ ───",
3418
+ " ╱ │ ╲",
3419
+ " ━━━━━━━━━━━━━━━━━━",
3420
+ ""
3421
+ ],
3422
+ artColors: ["yellow", "yellow", "yellow", "green", ""],
3423
+ textZh: "夏",
3424
+ textEn: "Summer",
3425
+ textColor: "yellow",
3426
+ durationTicks: 5,
3427
+ showPet: true
3428
+ },
3429
+ {
3430
+ art: [
3431
+ " ❋ · ❋ · ❋",
3432
+ " · ❋ · ❋ ·",
3433
+ " ━━━━━━━━━━━━━━━━━━",
3434
+ " ❋ · ❋",
3435
+ ""
3436
+ ],
3437
+ artColors: ["red", "red", "yellow", "red", ""],
3438
+ textZh: "秋",
3439
+ textEn: "Autumn",
3440
+ textColor: "red",
3441
+ durationTicks: 5,
3442
+ showPet: true
3443
+ },
3444
+ {
3445
+ art: [
3446
+ " ❅ · ❆ · ❅",
3447
+ " · ❆ · ❅ ·",
3448
+ " ━━━━━━━━━━━━━━━━━━",
3449
+ " ❅ · ❆",
3450
+ ""
3451
+ ],
3452
+ artColors: ["cyan", "cyan", "white", "cyan", ""],
3453
+ textZh: "冬",
3454
+ textEn: "Winter",
3455
+ textColor: "cyan",
3456
+ durationTicks: 5,
3457
+ showPet: true
3458
+ },
3459
+ {
3460
+ art: [
3461
+ " ✿ ☀ ❋ ❅",
3462
+ " ❅ ✿",
3463
+ " ❋ ☀",
3464
+ " ☀ ❋",
3465
+ " ❅ ❋ ☀ ✿"
3466
+ ],
3467
+ artColors: ["magenta", "cyan", "red", "yellow", "magenta"],
3468
+ textZh: "四季更替,你一直都在",
3469
+ textEn: "Seasons change, but you've always been here",
3470
+ textColor: "yellow",
3471
+ durationTicks: 8,
3472
+ showPet: true
3473
+ },
3474
+ {
3475
+ art: [
3476
+ " ˚ · ˚ · ˚ · ˚",
3477
+ "",
3478
+ "",
3479
+ ""
3480
+ ],
3481
+ artColors: ["gray", "", "", ""],
3482
+ textZh: "不管什么季节,和你在一起就是好天气",
3483
+ textEn: "No matter the season, being with you makes it beautiful",
3484
+ textColor: "yellow",
3485
+ durationTicks: 8,
3486
+ showPet: true
3487
+ },
3488
+ {
3489
+ art: [
3490
+ "",
3491
+ "",
3492
+ " ✿ 四季 ❅",
3493
+ " Seasons",
3494
+ "",
3495
+ " [ 新剧情解锁 ]",
3496
+ ""
3497
+ ],
3498
+ artColors: ["", "", "yellow", "gray", "", "yellow", ""],
3499
+ durationTicks: 6
3500
+ }
3501
+ ]
3502
+ },
3503
+ {
3504
+ id: 9,
3505
+ titleZh: "家",
3506
+ titleEn: "Home",
3507
+ frames: [
3508
+ {
3509
+ art: [
3510
+ "",
3511
+ "",
3512
+ " ▌",
3513
+ "",
3514
+ "",
3515
+ ""
3516
+ ],
3517
+ artColors: ["", "", "green", "", "", ""],
3518
+ durationTicks: 5
3519
+ },
3520
+ {
3521
+ art: [
3522
+ "",
3523
+ "",
3524
+ " > 你知道吗...▌",
3525
+ "",
3526
+ "",
3527
+ ""
3528
+ ],
3529
+ artColors: ["", "", "green", "", "", ""],
3530
+ durationTicks: 5
3531
+ },
3532
+ {
3533
+ art: [
3534
+ "",
3535
+ " > 你知道吗...",
3536
+ " > 我只是一些字符 ▌",
3537
+ "",
3538
+ "",
3539
+ ""
3540
+ ],
3541
+ artColors: ["", "green", "green", "", "", ""],
3542
+ durationTicks: 5
3543
+ },
3544
+ {
3545
+ art: [
3546
+ "",
3547
+ " > 你知道吗...",
3548
+ " > 我只是一些字符",
3549
+ " > 几行代码 ▌",
3550
+ "",
3551
+ ""
3552
+ ],
3553
+ artColors: ["", "green", "green", "green", "", ""],
3554
+ durationTicks: 5
3555
+ },
3556
+ {
3557
+ art: [
3558
+ " > 你知道吗...",
3559
+ " > 我只是一些字符",
3560
+ " > 几行代码",
3561
+ " > 一个在终端里跳动的小东西 ▌",
3562
+ "",
3563
+ ""
3564
+ ],
3565
+ artColors: ["green", "green", "green", "green", "", ""],
3566
+ durationTicks: 6
3567
+ },
3568
+ {
3569
+ art: [
3570
+ " > 你知道吗...",
3571
+ " > 我只是一些字符",
3572
+ " > 几行代码",
3573
+ " > 一个在终端里跳动的小东西",
3574
+ "",
3575
+ "",
3576
+ " > 但你对我好得像我是真的 ▌"
3577
+ ],
3578
+ artColors: ["gray", "gray", "gray", "gray", "", "", "yellow"],
3579
+ textColor: "yellow",
3580
+ durationTicks: 10,
3581
+ showPet: true
3582
+ },
3583
+ {
3584
+ art: [
3585
+ "",
3586
+ " ˚ · ˚ · ˚ · ˚ · ˚",
3587
+ "",
3588
+ "",
3589
+ ""
3590
+ ],
3591
+ artColors: ["", "yellow", "", "", ""],
3592
+ textZh: "这个小小的终端窗口,就是我的家",
3593
+ textEn: "This little terminal window is my home",
3594
+ textColor: "yellow",
3595
+ durationTicks: 8,
3596
+ showPet: true
3597
+ },
3598
+ {
3599
+ art: [
3600
+ " ╱╲",
3601
+ " ╱ ╲",
3602
+ " ╱ ╲",
3603
+ " ╱──────╲",
3604
+ " │ │",
3605
+ " │ │",
3606
+ " │ │",
3607
+ " ╰──────╯"
3608
+ ],
3609
+ artColors: ["red", "red", "red", "yellow", "yellow", "yellow", "yellow", "yellow"],
3610
+ textZh: "因为你在这里。",
3611
+ textEn: "Because you are here.",
3612
+ textColor: "yellow",
3613
+ durationTicks: 8,
3614
+ showPet: true
3615
+ },
3616
+ {
3617
+ art: [
3618
+ "",
3619
+ " ˚ · ˚ · ˚ · ˚",
3620
+ "",
3621
+ " ❤ 家 ❤",
3622
+ " Home",
3623
+ "",
3624
+ " ★ ✧ ★",
3625
+ "",
3626
+ " ✧ 这就是你的故事 ✧",
3627
+ " This is your story"
3628
+ ],
3629
+ artColors: ["", "yellow", "", "red", "gray", "", "yellow", "", "magenta", "gray"],
3630
+ durationTicks: 10
3631
+ }
3632
+ ]
3633
+ }
3634
+ ];
3635
+
3636
+ // src/stories/storyEngine.ts
3637
+ var CLIMAX_INDEX = 9;
3638
+ function isPerfectState(needs) {
3639
+ return needs.belly >= 100 && needs.energy >= 100 && needs.affinity >= 100 && needs.bond >= 100;
3640
+ }
3641
+ function shuffleArray(arr) {
3642
+ const result = [...arr];
3643
+ for (let i = result.length - 1;i > 0; i--) {
3644
+ const j = Math.floor(Math.random() * (i + 1));
3645
+ [result[i], result[j]] = [result[j], result[i]];
3646
+ }
3647
+ return result;
3648
+ }
3649
+ function generateStoryOrder(storiesSeen) {
3650
+ const seen = new Set(storiesSeen);
3651
+ const unseen = [];
3652
+ for (let i = 0;i < STORIES.length; i++) {
3653
+ if (i !== CLIMAX_INDEX && !seen.has(i))
3654
+ unseen.push(i);
3655
+ }
3656
+ return [...storiesSeen, ...shuffleArray(unseen), CLIMAX_INDEX];
3657
+ }
3658
+ function pickNextStory(storiesSeen, storyOrder) {
3659
+ const order = storyOrder ?? generateStoryOrder(storiesSeen);
3660
+ const seen = new Set(storiesSeen);
3661
+ for (const idx of order) {
3662
+ if (!seen.has(idx)) {
3663
+ return { nextIndex: idx, storyOrder: order };
3664
+ }
3665
+ }
3666
+ return { nextIndex: null, storyOrder: order };
3667
+ }
3668
+ function createPlayback(storyIndex) {
3669
+ return {
3670
+ storyIndex,
3671
+ frameIndex: 0,
3672
+ ticksInFrame: 0,
3673
+ finished: false
3674
+ };
3675
+ }
3676
+ function advancePlayback(playback) {
3677
+ if (playback.finished)
3678
+ return playback;
3679
+ const story = STORIES[playback.storyIndex];
3680
+ if (!story)
3681
+ return { ...playback, finished: true };
3682
+ const frame = story.frames[playback.frameIndex];
3683
+ if (!frame)
3684
+ return { ...playback, finished: true };
3685
+ const nextTick = playback.ticksInFrame + 1;
3686
+ if (nextTick >= frame.durationTicks) {
3687
+ const nextFrame = playback.frameIndex + 1;
3688
+ if (nextFrame >= story.frames.length) {
3689
+ return { ...playback, finished: true };
3690
+ }
3691
+ return { ...playback, frameIndex: nextFrame, ticksInFrame: 0 };
3692
+ }
3693
+ return { ...playback, ticksInFrame: nextTick };
3694
+ }
3695
+ function getCurrentFrame(playback) {
3696
+ if (playback.finished)
3697
+ return null;
3698
+ const story = STORIES[playback.storyIndex];
3699
+ if (!story)
3700
+ return null;
3701
+ return story.frames[playback.frameIndex] ?? null;
3702
+ }
3703
+ function getStory(index) {
3704
+ return STORIES[index] ?? null;
3705
+ }
3706
+
3707
+ // src/ui/StoryOverlay.tsx
3708
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
3709
+ function StoryOverlay({ companion, storyIndex, locale, onComplete }) {
3710
+ const [playback, setPlayback] = useState6(() => createPlayback(storyIndex));
3711
+ useEffect6(() => {
3712
+ const id = setInterval(() => {
3713
+ setPlayback((prev) => {
3714
+ const next = advancePlayback(prev);
3715
+ if (next.finished) {
3716
+ clearInterval(id);
3717
+ }
3718
+ return next;
3719
+ });
3720
+ }, TICK_MS);
3721
+ return () => clearInterval(id);
3722
+ }, []);
3723
+ useEffect6(() => {
3724
+ if (playback.finished) {
3725
+ const timeout = setTimeout(onComplete, 500);
3726
+ return () => clearTimeout(timeout);
3727
+ }
3728
+ }, [playback.finished, onComplete]);
3729
+ useInput6((_input, key) => {
3730
+ if (key.escape) {
3731
+ onComplete();
3732
+ }
3733
+ });
3734
+ const frame = getCurrentFrame(playback);
3735
+ const story = getStory(storyIndex);
3736
+ if (!frame || !story) {
3737
+ return null;
3738
+ }
3739
+ const petFrame = (playback.frameIndex + playback.ticksInFrame) % spriteFrameCount(companion.species);
3740
+ const petLines = frame.showPet ? renderSprite(companion, petFrame) : [];
3741
+ const text = locale === "zh" ? frame.textZh : frame.textEn ?? frame.textZh;
3742
+ return /* @__PURE__ */ jsxDEV6(Box6, {
3743
+ flexDirection: "column",
3744
+ width: "100%",
3745
+ paddingTop: 1,
3746
+ children: [
3747
+ /* @__PURE__ */ jsxDEV6(Box6, {
3748
+ flexDirection: "column",
3749
+ marginLeft: 2,
3750
+ children: frame.art.map((line, i) => {
3751
+ const color = frame.artColors?.[i];
3752
+ return /* @__PURE__ */ jsxDEV6(Text6, {
3753
+ color: color || undefined,
3754
+ children: line
3755
+ }, i, false, undefined, this);
3756
+ })
3757
+ }, undefined, false, undefined, this),
3758
+ petLines.length > 0 && /* @__PURE__ */ jsxDEV6(Box6, {
3759
+ flexDirection: "column",
3760
+ marginLeft: 6,
3761
+ marginTop: 1,
3762
+ children: petLines.map((line, i) => /* @__PURE__ */ jsxDEV6(Text6, {
3763
+ children: line
3764
+ }, `pet${i}`, false, undefined, this))
3765
+ }, undefined, false, undefined, this),
3766
+ text && /* @__PURE__ */ jsxDEV6(Box6, {
3767
+ marginTop: 1,
3768
+ marginLeft: 4,
3769
+ children: /* @__PURE__ */ jsxDEV6(Text6, {
3770
+ color: frame.textColor || "white",
3771
+ italic: true,
3772
+ children: text
3773
+ }, undefined, false, undefined, this)
3774
+ }, undefined, false, undefined, this),
3775
+ /* @__PURE__ */ jsxDEV6(Box6, {
3776
+ marginTop: 2,
3777
+ marginLeft: 4,
3778
+ children: /* @__PURE__ */ jsxDEV6(Text6, {
3779
+ dimColor: true,
3780
+ children: locale === "zh" ? "[Esc 跳过]" : "[Esc to skip]"
3781
+ }, undefined, false, undefined, this)
3782
+ }, undefined, false, undefined, this)
3783
+ ]
3784
+ }, undefined, true, undefined, this);
3785
+ }
3786
+
3787
+ // src/ui/StoryLibrary.tsx
3788
+ import { useState as useState7 } from "react";
3789
+ import { Box as Box7, Text as Text7, useInput as useInput7 } from "ink";
3790
+ import { jsxDEV as jsxDEV7, Fragment as Fragment6 } from "react/jsx-dev-runtime";
3791
+ function StoryLibrary({ locale, unlockedStoryIndexes, onSelect, onCancel }) {
3792
+ const seen = new Set;
3793
+ const stories = [];
3794
+ for (const index of unlockedStoryIndexes) {
3795
+ if (seen.has(index))
3796
+ continue;
3797
+ seen.add(index);
3798
+ const story = getStory(index);
3799
+ if (!story)
3800
+ continue;
3801
+ stories.push({
3802
+ index,
3803
+ title: locale === "zh" ? story.titleZh : story.titleEn
3804
+ });
3805
+ }
3806
+ const [selected, setSelected] = useState7(0);
3807
+ useInput7((input, key) => {
3808
+ if (key.escape) {
3809
+ onCancel();
3810
+ return;
3811
+ }
3812
+ if (stories.length === 0)
3813
+ return;
3814
+ if (key.upArrow || input === "k") {
3815
+ setSelected((current) => Math.max(0, current - 1));
3816
+ return;
3817
+ }
3818
+ if (key.downArrow || input === "j") {
3819
+ setSelected((current) => Math.min(stories.length - 1, current + 1));
3820
+ return;
3821
+ }
3822
+ if (key.return) {
3823
+ onSelect(stories[selected].index);
3824
+ return;
3825
+ }
3826
+ const num = Number.parseInt(input, 10);
3827
+ if (Number.isNaN(num) || num < 1 || num > stories.length)
3828
+ return;
3829
+ setSelected(num - 1);
3830
+ });
3831
+ return /* @__PURE__ */ jsxDEV7(Box7, {
3832
+ flexDirection: "column",
3833
+ marginTop: 1,
3834
+ marginLeft: 2,
3835
+ children: [
3836
+ /* @__PURE__ */ jsxDEV7(Text7, {
3837
+ bold: true,
3838
+ color: "cyan",
3839
+ children: locale === "zh" ? "剧情回顾" : "Story Replay"
3840
+ }, undefined, false, undefined, this),
3841
+ stories.length === 0 ? /* @__PURE__ */ jsxDEV7(Fragment6, {
3842
+ children: [
3843
+ /* @__PURE__ */ jsxDEV7(Box7, {
3844
+ marginTop: 1,
3845
+ children: /* @__PURE__ */ jsxDEV7(Text7, {
3846
+ dimColor: true,
3847
+ children: locale === "zh" ? "还没有已解锁剧情。先去养满属性并触发一次吧。" : "No unlocked stories yet. Unlock one first."
3848
+ }, undefined, false, undefined, this)
3849
+ }, undefined, false, undefined, this),
3850
+ /* @__PURE__ */ jsxDEV7(Box7, {
3851
+ marginTop: 1,
3852
+ children: /* @__PURE__ */ jsxDEV7(Text7, {
3853
+ dimColor: true,
3854
+ children: locale === "zh" ? "按 Esc 返回" : "Press Esc to go back"
3855
+ }, undefined, false, undefined, this)
3856
+ }, undefined, false, undefined, this)
3857
+ ]
3858
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV7(Fragment6, {
3859
+ children: [
3860
+ /* @__PURE__ */ jsxDEV7(Box7, {
3861
+ marginTop: 1,
3862
+ flexDirection: "column",
3863
+ children: stories.map((story, index) => {
3864
+ const isSelected = index === selected;
3865
+ return /* @__PURE__ */ jsxDEV7(Text7, {
3866
+ children: [
3867
+ isSelected ? "▸ " : " ",
3868
+ /* @__PURE__ */ jsxDEV7(Text7, {
3869
+ color: isSelected ? "green" : undefined,
3870
+ bold: isSelected,
3871
+ children: [
3872
+ index + 1,
3873
+ ". ",
3874
+ story.title
3875
+ ]
3876
+ }, undefined, true, undefined, this)
3877
+ ]
3878
+ }, story.index, true, undefined, this);
3879
+ })
3880
+ }, undefined, false, undefined, this),
3881
+ /* @__PURE__ */ jsxDEV7(Box7, {
3882
+ marginTop: 1,
3883
+ children: /* @__PURE__ */ jsxDEV7(Text7, {
3884
+ dimColor: true,
3885
+ children: locale === "zh" ? "↑↓ 选择 Enter 重播 Esc 返回" : "↑↓ select Enter replay Esc back"
3886
+ }, undefined, false, undefined, this)
3887
+ }, undefined, false, undefined, this)
3888
+ ]
3889
+ }, undefined, true, undefined, this)
3890
+ ]
3891
+ }, undefined, true, undefined, this);
3892
+ }
3893
+
3894
+ // src/core/idleQuips.ts
3895
+ var QUIPS = {
3896
+ zh: {
3897
+ sleepy: ["好困啊...", "打个盹...zzz", "眼皮好重...", "想睡觉了~", "哈欠~"],
3898
+ calm: ["今天天气不错~", "嗯...挺好的", "安安静静~", "发呆中...", "~"],
3899
+ happy: ["嘿嘿~", "今天好开心!", "哼哼哼~♪", "心情超好!", "嘻嘻~"],
3900
+ hungry: ["肚子咕咕叫...", "好饿啊...", "想吃东西!", "有吃的吗?", "饿..."],
3901
+ lonely: ["有人在吗...", "好想你...", "陪陪我嘛~", "别走...", "..."],
3902
+ hype: ["耶耶耶!!", "太嗨了!", "冲冲冲!!", "好兴奋!", "嗷呜!!"],
3903
+ curious: ["这是什么?", "好想知道~", "让我看看...", "嗯...好奇!", "为什么呢?"],
3904
+ playful: ["来玩来玩!", "哈哈~追我呀!", "嘻嘻~", "一起玩吧!", "无聊~想玩~"],
3905
+ dreamy: ["嗯...想什么呢", "飘飘的~", "好像在做梦...", "远方...", "...恍惚"],
3906
+ mischievous: ["嘿嘿嘿~", "我有个坏主意~", "偷偷的...", "嘻嘻~别告诉别人", "搞点事情~"],
3907
+ cozy: ["好暖和~", "舒服~", "不想动了~", "好惬意啊", "窝着真好~"]
3908
+ },
3909
+ en: {
3910
+ sleepy: ["so sleepy...", "*yawns*", "zzz...", "need a nap...", "*dozing off*"],
3911
+ calm: ["nice day~", "hmm...", "*stares into space*", "peaceful~", "~"],
3912
+ happy: ["hehe~", "great day!", "♪ la la la~", "feeling good!", ":D"],
3913
+ hungry: ["*tummy grumbles*", "so hungry...", "feed me?", "want food...", "*rumble*"],
3914
+ lonely: ["anyone there?", "miss you...", "come hang out~", "don't go...", "..."],
3915
+ hype: ["YEAH!!", "so excited!", "LET'S GO!!", "wooooo!", "HYPE!!"],
3916
+ curious: ["what's that?", "hmm, interesting...", "tell me more~", "I wonder...", "ooh!"],
3917
+ playful: ["play with me!", "*bounces around*", "catch me~", "hehe! tag!", "*zooms*"],
3918
+ dreamy: ["*stares at clouds*", "mmm...", "*lost in thought*", "so floaty~", "...huh?"],
3919
+ mischievous: ["hehehe~", "I have an idea...", "*sneaky*", "don't mind me~", "*plotting*"],
3920
+ cozy: ["so warm~", "*snuggles in*", "comfy...", "don't wanna move~", "*cozy purr*"]
3921
+ }
3922
+ };
3923
+ function pickIdleQuip(mood, locale) {
3924
+ const pool = QUIPS[locale][mood];
3925
+ return pool[Math.floor(Math.random() * pool.length)];
3926
+ }
3927
+
3928
+ // src/app.tsx
3929
+ import { jsxDEV as jsxDEV8, Fragment as Fragment7 } from "react/jsx-dev-runtime";
3930
+ var MOOD_EMOJI = {
3931
+ sleepy: "\uD83D\uDCA4",
3932
+ calm: "\uD83D\uDE0C",
3933
+ happy: "\uD83D\uDE0A",
3934
+ hungry: "\uD83C\uDF7D️",
3935
+ lonely: "\uD83E\uDD7A",
3936
+ hype: "\uD83C\uDF89",
3937
+ curious: "\uD83D\uDD0D",
3938
+ playful: "\uD83C\uDFAE",
3939
+ dreamy: "☁️",
3940
+ mischievous: "\uD83D\uDE08",
3941
+ cozy: "\uD83E\uDDE3"
3942
+ };
3943
+ var MOOD_LABEL = {
3944
+ en: { sleepy: "sleepy", calm: "calm", happy: "happy", hungry: "hungry", lonely: "lonely", hype: "hype", curious: "curious", playful: "playful", dreamy: "dreamy", mischievous: "mischievous", cozy: "cozy" },
3945
+ zh: { sleepy: "困了", calm: "平静", happy: "开心", hungry: "饿了", lonely: "孤单", hype: "嗨皮", curious: "好奇", playful: "贪玩", dreamy: "发呆", mischievous: "调皮", cozy: "舒适" }
3946
+ };
3947
+ var UI_LABELS = {
3948
+ en: { belly: "belly", energy: "energy", affinity: "affinity", bond: "bond", placeholder: "say something... (feed, sleep, pet, help)", switchHint: 'type "中文" to switch to Chinese' },
3949
+ zh: { belly: "饱饱", energy: "精力", affinity: "好感", bond: "羁绊", placeholder: "说点什么... (喂食, 睡觉, 摸摸, 帮助)", switchHint: 'type "english" to switch to English' }
3950
+ };
3951
+ function formatBar2(value) {
3952
+ const filled = Math.round(value / 10);
3953
+ return "█".repeat(filled) + "░".repeat(10 - filled);
3954
+ }
3955
+ function App({ companion, initialState, userId, initialQuota, connected: initialConnected, latestVersion, onCompanionChange, onEyeChange, onHatChange, onRarityChange }) {
3956
+ const [state, setState] = useState8(initialState);
3957
+ const [quota, setQuota] = useState8(initialQuota);
3958
+ const [connected, setConnected] = useState8(initialConnected);
3959
+ const [tick, setTick] = useState8(0);
3960
+ const [reaction, setReaction] = useState8(null);
3961
+ const [bubbleAge, setBubbleAge] = useState8(0);
3962
+ const [bubbleShowDuration, setBubbleShowDuration] = useState8(BUBBLE_SHOW_TICKS);
3963
+ const [input, setInput] = useState8("");
3964
+ const [thinking, setThinking] = useState8(false);
3965
+ const [action, setAction] = useState8(null);
3966
+ const [locale, setLocale] = useState8("zh");
3967
+ const [screen, setScreen] = useState8("main");
3968
+ const [paymentType, setPaymentType] = useState8("conversation");
3969
+ const [pendingStoryIndex, setPendingStoryIndex] = useState8(null);
3970
+ const [activeStoryIndex, setActiveStoryIndex] = useState8(null);
3971
+ const [activeStoryMode, setActiveStoryMode] = useState8("unlock");
3972
+ const { stdout } = useStdout();
3973
+ const columns = stdout?.columns ?? 80;
3974
+ const hasMountedStoryCheckRef = useRef6(false);
3975
+ const storyProgressRef = useRef6({
3976
+ chats: initialState.chatsSinceStory ?? 0,
3977
+ interactions: initialState.interactionsSinceStory ?? 0
3978
+ });
3979
+ const persistNextState = useCallback(async (nextState, nextQuota = quota) => {
3980
+ const result = await persistState(userId, nextState, nextQuota);
3981
+ if (result.ok) {
3982
+ setConnected(true);
3983
+ setState(result.data.state);
3984
+ setQuota(result.data.quota);
3985
+ return result.data.state;
3986
+ }
3987
+ setConnected(false);
3988
+ return nextState;
3989
+ }, [userId, quota]);
3990
+ useEffect7(() => {
3991
+ const id = setInterval(() => {
3992
+ setTick((t) => t + 1);
3993
+ setBubbleAge((a) => a + 1);
3994
+ }, TICK_MS);
3995
+ return () => clearInterval(id);
3996
+ }, []);
3997
+ useEffect7(() => {
3998
+ if (reaction && bubbleAge >= bubbleShowDuration) {
3999
+ setReaction(null);
4000
+ setBubbleAge(0);
4001
+ setBubbleShowDuration(BUBBLE_SHOW_TICKS);
4002
+ }
4003
+ }, [reaction, bubbleAge, bubbleShowDuration]);
4004
+ useEffect7(() => {
4005
+ const chats = state.chatsSinceStory ?? 0;
4006
+ const interactions = state.interactionsSinceStory ?? 0;
4007
+ const previousProgress = storyProgressRef.current;
4008
+ const progressedThisSession = chats > previousProgress.chats || interactions > previousProgress.interactions;
4009
+ storyProgressRef.current = { chats, interactions };
4010
+ if (!hasMountedStoryCheckRef.current) {
4011
+ hasMountedStoryCheckRef.current = true;
4012
+ return;
4013
+ }
4014
+ if (screen !== "main")
4015
+ return;
4016
+ if (pendingStoryIndex !== null || activeStoryIndex !== null)
4017
+ return;
4018
+ if (!progressedThisSession)
4019
+ return;
4020
+ if (!isPerfectState(state.needs))
4021
+ return;
4022
+ if (chats < 10 || interactions < 3)
4023
+ return;
4024
+ const seen = state.storiesSeen ?? [];
4025
+ const { nextIndex, storyOrder } = pickNextStory(seen, state.storyOrder);
4026
+ if (!state.storyOrder) {
4027
+ const newState = { ...state, storyOrder };
4028
+ setState(newState);
4029
+ persistNextState(newState, quota);
4030
+ }
4031
+ if (nextIndex !== null) {
4032
+ setActiveStoryMode("unlock");
4033
+ setReaction(locale === "zh" ? "新剧情解锁" : "New story unlocked");
4034
+ setBubbleAge(0);
4035
+ setPendingStoryIndex(nextIndex);
4036
+ }
4037
+ }, [state, screen, pendingStoryIndex, activeStoryIndex, userId, quota, locale]);
4038
+ useEffect7(() => {
4039
+ if (pendingStoryIndex === null)
4040
+ return;
4041
+ const timeout = setTimeout(() => {
4042
+ setActiveStoryIndex(pendingStoryIndex);
4043
+ setPendingStoryIndex(null);
4044
+ setScreen("story");
4045
+ }, 2000);
4046
+ return () => clearTimeout(timeout);
4047
+ }, [pendingStoryIndex]);
4048
+ const handleStoryComplete = useCallback(() => {
4049
+ if (activeStoryIndex === null)
4050
+ return;
4051
+ if (activeStoryMode === "replay") {
4052
+ setPendingStoryIndex(null);
4053
+ setScreen("storylist");
4054
+ setActiveStoryIndex(null);
4055
+ return;
4056
+ }
4057
+ const seen = state.storiesSeen ?? [];
4058
+ const newSeen = seen.includes(activeStoryIndex) ? seen : [...seen, activeStoryIndex];
4059
+ const newState = { ...state, storiesSeen: newSeen, chatsSinceStory: 0, interactionsSinceStory: 0 };
4060
+ setState(newState);
4061
+ persistNextState(newState, quota);
4062
+ setPendingStoryIndex(null);
4063
+ setScreen("main");
4064
+ setActiveStoryIndex(null);
4065
+ }, [activeStoryIndex, activeStoryMode, state, quota, persistNextState]);
4066
+ const showReaction = useCallback((text, duration = BUBBLE_SHOW_TICKS) => {
4067
+ setReaction(text);
4068
+ setBubbleAge(0);
4069
+ setBubbleShowDuration(duration);
4070
+ }, []);
4071
+ useEffect7(() => {
4072
+ if (screen !== "main")
4073
+ return;
4074
+ const IDLE_INTERVAL = 2 * 60 * 1000;
4075
+ const id = setInterval(() => {
4076
+ if (reaction || thinking)
4077
+ return;
4078
+ const currentMood = deriveMood(state);
4079
+ showReaction(pickIdleQuip(currentMood, locale));
4080
+ }, IDLE_INTERVAL);
4081
+ return () => clearInterval(id);
4082
+ }, [screen, locale, state, reaction, thinking, showReaction]);
4083
+ const handleSubmit = useCallback(async (value) => {
4084
+ if (!value.trim() || thinking)
4085
+ return;
4086
+ setInput("");
4087
+ const result = handleCommand(value, state, companion, locale);
4088
+ if (result.locale) {
4089
+ setLocale(result.locale);
4090
+ }
4091
+ if (result.navigate === "changepet") {
4092
+ if (!connected) {
4093
+ showReaction(locale === "zh" ? "需要连接服务器才能操作" : "Server connection required");
4094
+ return;
4095
+ }
4096
+ setScreen("changepet");
4097
+ return;
4098
+ }
4099
+ if (result.navigate === "changeeye") {
4100
+ if (!connected) {
4101
+ showReaction(locale === "zh" ? "需要连接服务器才能操作" : "Server connection required");
4102
+ return;
4103
+ }
4104
+ setScreen("changeeye");
4105
+ return;
4106
+ }
4107
+ if (result.navigate === "changehat") {
4108
+ if (!connected) {
4109
+ showReaction(locale === "zh" ? "需要连接服务器才能操作" : "Server connection required");
4110
+ return;
4111
+ }
4112
+ setScreen("changehat");
4113
+ return;
4114
+ }
4115
+ if (result.navigate === "upgraderarity") {
4116
+ if (!connected) {
4117
+ showReaction(locale === "zh" ? "需要连接服务器才能操作" : "Server connection required");
4118
+ return;
4119
+ }
4120
+ setScreen("upgraderarity");
4121
+ return;
4122
+ }
4123
+ if (result.navigate === "storylist") {
4124
+ setScreen("storylist");
4125
+ return;
4126
+ }
4127
+ if (!result.needsBackend) {
4128
+ if (result.reaction)
4129
+ showReaction(result.reaction, result.bubbleDuration);
4130
+ return;
4131
+ }
4132
+ if (!connected) {
4133
+ showReaction(locale === "zh" ? "需要连接服务器才能互动" : "Server connection required");
4134
+ return;
4135
+ }
4136
+ if (result.action && !result.isChat) {
4137
+ setAction(result.action);
4138
+ setTimeout(() => setAction(null), 3500);
4139
+ const res = await sendInteraction(userId, result.action);
4140
+ if (res.ok) {
4141
+ const intCount = (state.interactionsSinceStory ?? 0) + 1;
4142
+ const updatedState = { ...res.data.state, storiesSeen: state.storiesSeen, storyOrder: state.storyOrder, chatsSinceStory: state.chatsSinceStory ?? 0, interactionsSinceStory: intCount };
4143
+ setState(updatedState);
4144
+ setQuota(res.data.quota);
4145
+ persistNextState(updatedState, res.data.quota);
4146
+ if (result.reaction)
4147
+ showReaction(result.reaction);
4148
+ } else if (res.quotaExceeded && res.paymentType) {
4149
+ setPaymentType(res.paymentType);
4150
+ setScreen("quotapay");
4151
+ } else {
4152
+ setConnected(false);
4153
+ showReaction(locale === "zh" ? "连接服务器失败..." : "Server error...");
4154
+ }
4155
+ return;
4156
+ }
4157
+ if (result.isChat && result.chatMessage) {
4158
+ setThinking(true);
4159
+ showReaction("");
4160
+ const res = await sendChatStream(userId, companion, state, result.chatMessage, (token) => {
4161
+ setReaction((prev) => (prev ?? "") + token);
4162
+ setBubbleAge(0);
4163
+ });
4164
+ if (res.ok) {
4165
+ const chatCount = (res.state.chatsSinceStory ?? state.chatsSinceStory ?? 0) + 1;
4166
+ const updatedState = { ...res.state, storiesSeen: state.storiesSeen, storyOrder: state.storyOrder, chatsSinceStory: chatCount, interactionsSinceStory: state.interactionsSinceStory ?? 0 };
4167
+ setState(updatedState);
4168
+ setQuota(res.quota);
4169
+ persistNextState(updatedState, res.quota);
4170
+ setBubbleAge(0);
4171
+ } else if (res.quotaExceeded && res.paymentType) {
4172
+ setPaymentType(res.paymentType);
4173
+ setScreen("quotapay");
4174
+ } else {
4175
+ setConnected(false);
4176
+ showReaction(locale === "zh" ? "连接服务器失败..." : "Server error...");
4177
+ }
4178
+ setThinking(false);
4179
+ }
4180
+ }, [state, companion, userId, thinking, showReaction, locale, connected]);
4181
+ const mood = deriveMood(state);
4182
+ const stars = RARITY_STARS[companion.rarity];
4183
+ const vm = getSpriteViewModel({
4184
+ companion,
4185
+ columns,
4186
+ tick,
4187
+ reaction: reaction ?? undefined,
4188
+ bubbleAgeTicks: bubbleAge,
4189
+ bubbleShowDuration,
4190
+ action
4191
+ });
4192
+ const bubbleLines = [];
4193
+ if (reaction) {
4194
+ const maxWidth = Math.min(40, columns - 20);
4195
+ const wrapped = wrapSpeech(reaction, maxWidth);
4196
+ if (wrapped.length === 1) {
4197
+ bubbleLines.push(`「${wrapped[0]}」`);
4198
+ } else {
4199
+ bubbleLines.push(`「${wrapped[0]}`);
4200
+ for (let i = 1;i < wrapped.length - 1; i++) {
4201
+ bubbleLines.push(` ${wrapped[i]}`);
4202
+ }
4203
+ bubbleLines.push(` ${wrapped[wrapped.length - 1]}」`);
4204
+ }
4205
+ }
4206
+ if (screen === "changepet") {
4207
+ return /* @__PURE__ */ jsxDEV8(ChangePet, {
4208
+ userId,
4209
+ currentSpecies: companion.species,
4210
+ onComplete: async (newSpecies) => {
4211
+ const newState = {
4212
+ ...state,
4213
+ companion: state.companion ? { ...state.companion, speciesOverride: newSpecies } : undefined
4214
+ };
4215
+ setState(newState);
4216
+ onCompanionChange(newSpecies);
4217
+ await persistNextState(newState, quota);
4218
+ setScreen("main");
4219
+ },
4220
+ onCancel: () => setScreen("main")
4221
+ }, undefined, false, undefined, this);
4222
+ }
4223
+ if (screen === "changeeye") {
4224
+ return /* @__PURE__ */ jsxDEV8(ChangeEye, {
4225
+ userId,
4226
+ currentEye: companion.eye,
4227
+ onComplete: async (newEye) => {
4228
+ const newState = {
4229
+ ...state,
4230
+ companion: state.companion ? { ...state.companion, eyeOverride: newEye } : undefined
4231
+ };
4232
+ setState(newState);
4233
+ onEyeChange(newEye);
4234
+ await persistNextState(newState, quota);
4235
+ setScreen("main");
4236
+ },
4237
+ onCancel: () => setScreen("main")
4238
+ }, undefined, false, undefined, this);
4239
+ }
4240
+ if (screen === "changehat") {
4241
+ const cap = companion.shiny ? 4 : HAT_COLLECTION_CAP[companion.rarity];
4242
+ return /* @__PURE__ */ jsxDEV8(ChangeHat, {
4243
+ userId,
4244
+ currentHat: companion.hat,
4245
+ ownedHats: state.companion?.ownedHats ?? [],
4246
+ collectionCap: cap,
4247
+ onComplete: async (newHat, updatedOwnedHats) => {
4248
+ const newState = {
4249
+ ...state,
4250
+ companion: state.companion ? { ...state.companion, hatOverride: newHat, ownedHats: updatedOwnedHats } : undefined
4251
+ };
4252
+ setState(newState);
4253
+ onHatChange(newHat, updatedOwnedHats);
4254
+ await persistNextState(newState, quota);
4255
+ setScreen("main");
4256
+ },
4257
+ onCancel: () => setScreen("main")
4258
+ }, undefined, false, undefined, this);
4259
+ }
4260
+ if (screen === "upgraderarity") {
4261
+ return /* @__PURE__ */ jsxDEV8(UpgradeRarity, {
4262
+ userId,
4263
+ currentRarity: companion.rarity,
4264
+ isShiny: companion.shiny,
4265
+ onComplete: async (newRarity, newShiny) => {
4266
+ const newState = {
4267
+ ...state,
4268
+ companion: state.companion ? {
4269
+ ...state.companion,
4270
+ rarityOverride: newRarity,
4271
+ shinyOverride: newShiny || undefined
4272
+ } : undefined
4273
+ };
4274
+ setState(newState);
4275
+ onRarityChange(newRarity, newShiny);
4276
+ await persistNextState(newState, quota);
4277
+ setScreen("main");
4278
+ },
4279
+ onCancel: () => setScreen("main")
4280
+ }, undefined, false, undefined, this);
4281
+ }
4282
+ if (screen === "quotapay") {
4283
+ return /* @__PURE__ */ jsxDEV8(QuotaPayment, {
4284
+ userId,
4285
+ paymentType,
4286
+ onComplete: (newQuota) => {
4287
+ setQuota(newQuota);
4288
+ setScreen("main");
4289
+ },
4290
+ onCancel: () => setScreen("main")
4291
+ }, undefined, false, undefined, this);
4292
+ }
4293
+ if (screen === "storylist") {
4294
+ return /* @__PURE__ */ jsxDEV8(StoryLibrary, {
4295
+ locale,
4296
+ unlockedStoryIndexes: state.storiesSeen ?? [],
4297
+ onSelect: (storyIndex) => {
4298
+ setActiveStoryMode("replay");
4299
+ setActiveStoryIndex(storyIndex);
4300
+ setScreen("story");
4301
+ },
4302
+ onCancel: () => setScreen("main")
4303
+ }, undefined, false, undefined, this);
4304
+ }
4305
+ if (screen === "story" && activeStoryIndex !== null) {
4306
+ return /* @__PURE__ */ jsxDEV8(StoryOverlay, {
4307
+ companion,
4308
+ storyIndex: activeStoryIndex,
4309
+ locale,
4310
+ onComplete: handleStoryComplete
4311
+ }, `${activeStoryMode}-${activeStoryIndex}`, false, undefined, this);
4312
+ }
4313
+ const labels = UI_LABELS[locale];
4314
+ return /* @__PURE__ */ jsxDEV8(Box8, {
4315
+ flexDirection: "column",
4316
+ width: "100%",
4317
+ children: [
4318
+ !connected && /* @__PURE__ */ jsxDEV8(Box8, {
4319
+ marginTop: 1,
4320
+ marginLeft: 2,
4321
+ children: /* @__PURE__ */ jsxDEV8(Text8, {
4322
+ color: "red",
4323
+ bold: true,
4324
+ children: [
4325
+ "⚠ ",
4326
+ locale === "zh" ? "无法连接服务器 — 所有操作暂停" : "Cannot connect to server — all actions paused"
4327
+ ]
4328
+ }, undefined, true, undefined, this)
4329
+ }, undefined, false, undefined, this),
4330
+ latestVersion && /* @__PURE__ */ jsxDEV8(Box8, {
4331
+ marginLeft: 2,
4332
+ children: /* @__PURE__ */ jsxDEV8(Text8, {
4333
+ color: "yellow",
4334
+ children: locale === "zh" ? `有新版本啦 v${latestVersion}!终端输入 npm i -g @whaletech/pet 更新` : `New version v${latestVersion} available! Run: npm i -g @whaletech/pet`
4335
+ }, undefined, false, undefined, this)
4336
+ }, undefined, false, undefined, this),
4337
+ /* @__PURE__ */ jsxDEV8(Box8, {
4338
+ flexDirection: "row",
4339
+ marginTop: 1,
4340
+ children: [
4341
+ /* @__PURE__ */ jsxDEV8(Box8, {
4342
+ flexDirection: "column",
4343
+ marginLeft: 2,
4344
+ children: vm.narrow ? /* @__PURE__ */ jsxDEV8(Text8, {
4345
+ children: [
4346
+ vm.face,
4347
+ " ",
4348
+ vm.label
4349
+ ]
4350
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV8(Fragment7, {
4351
+ children: [
4352
+ vm.overlayAbove.map((line, i) => /* @__PURE__ */ jsxDEV8(Text8, {
4353
+ color: "magenta",
4354
+ children: line
4355
+ }, `a${i}`, false, undefined, this)),
4356
+ vm.lines.map((line, i) => /* @__PURE__ */ jsxDEV8(Text8, {
4357
+ children: line
4358
+ }, i, false, undefined, this)),
4359
+ vm.overlayBelow.map((line, i) => /* @__PURE__ */ jsxDEV8(Text8, {
4360
+ color: "yellow",
4361
+ children: line
4362
+ }, `b${i}`, false, undefined, this))
4363
+ ]
4364
+ }, undefined, true, undefined, this)
4365
+ }, undefined, false, undefined, this),
4366
+ bubbleLines.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
4367
+ flexDirection: "column",
4368
+ marginLeft: 2,
4369
+ children: bubbleLines.map((line, i) => /* @__PURE__ */ jsxDEV8(Text8, {
4370
+ dimColor: vm.fading,
4371
+ children: line
4372
+ }, i, false, undefined, this))
4373
+ }, undefined, false, undefined, this)
4374
+ ]
4375
+ }, undefined, true, undefined, this),
4376
+ /* @__PURE__ */ jsxDEV8(Box8, {
4377
+ marginTop: 1,
4378
+ marginLeft: 2,
4379
+ flexDirection: "column",
4380
+ children: [
4381
+ /* @__PURE__ */ jsxDEV8(Text8, {
4382
+ bold: true,
4383
+ children: [
4384
+ companion.name,
4385
+ " ",
4386
+ stars,
4387
+ companion.shiny ? " ✨" : "",
4388
+ " ",
4389
+ MOOD_EMOJI[mood],
4390
+ " ",
4391
+ MOOD_LABEL[locale][mood]
4392
+ ]
4393
+ }, undefined, true, undefined, this),
4394
+ /* @__PURE__ */ jsxDEV8(Text8, {
4395
+ children: [
4396
+ /* @__PURE__ */ jsxDEV8(Text8, {
4397
+ dimColor: true,
4398
+ children: [
4399
+ labels.belly,
4400
+ ":"
4401
+ ]
4402
+ }, undefined, true, undefined, this),
4403
+ " ",
4404
+ formatBar2(state.needs.belly),
4405
+ " ",
4406
+ " ",
4407
+ /* @__PURE__ */ jsxDEV8(Text8, {
4408
+ dimColor: true,
4409
+ children: [
4410
+ labels.energy,
4411
+ ":"
4412
+ ]
4413
+ }, undefined, true, undefined, this),
4414
+ " ",
4415
+ formatBar2(state.needs.energy),
4416
+ " ",
4417
+ " ",
4418
+ /* @__PURE__ */ jsxDEV8(Text8, {
4419
+ dimColor: true,
4420
+ children: [
4421
+ labels.affinity,
4422
+ ":"
4423
+ ]
4424
+ }, undefined, true, undefined, this),
4425
+ " ",
4426
+ formatBar2(state.needs.affinity),
4427
+ " ",
4428
+ " ",
4429
+ /* @__PURE__ */ jsxDEV8(Text8, {
4430
+ dimColor: true,
4431
+ children: [
4432
+ labels.bond,
4433
+ ":"
4434
+ ]
4435
+ }, undefined, true, undefined, this),
4436
+ " ",
4437
+ formatBar2(state.needs.bond)
4438
+ ]
4439
+ }, undefined, true, undefined, this),
4440
+ quota && /* @__PURE__ */ jsxDEV8(Text8, {
4441
+ dimColor: true,
4442
+ children: [
4443
+ locale === "zh" ? "聊天剩余" : "chat left",
4444
+ " ",
4445
+ quota.conversationsRemaining,
4446
+ " | ",
4447
+ locale === "zh" ? "互动剩余" : "act left",
4448
+ " ",
4449
+ quota.interactionsRemaining
4450
+ ]
4451
+ }, undefined, true, undefined, this)
4452
+ ]
4453
+ }, undefined, true, undefined, this),
4454
+ /* @__PURE__ */ jsxDEV8(Box8, {
4455
+ marginTop: 1,
4456
+ marginLeft: 2,
4457
+ children: [
4458
+ /* @__PURE__ */ jsxDEV8(Text8, {
4459
+ dimColor: true,
4460
+ children: [
4461
+ thinking ? "⏳" : ">",
4462
+ " "
4463
+ ]
4464
+ }, undefined, true, undefined, this),
4465
+ /* @__PURE__ */ jsxDEV8(TextInput, {
4466
+ value: input,
4467
+ onChange: setInput,
4468
+ onSubmit: handleSubmit,
4469
+ placeholder: labels.placeholder
4470
+ }, undefined, false, undefined, this)
4471
+ ]
4472
+ }, undefined, true, undefined, this)
4473
+ ]
4474
+ }, undefined, true, undefined, this);
4475
+ }
4476
+
4477
+ // src/hatch.tsx
4478
+ import { useState as useState9, useEffect as useEffect8 } from "react";
4479
+ import { Box as Box9, Text as Text9 } from "ink";
4480
+ import TextInput2 from "ink-text-input";
4481
+ import { jsxDEV as jsxDEV9, Fragment as Fragment8 } from "react/jsx-dev-runtime";
4482
+ var EGG_FRAMES = [
4483
+ [" ", " .--. ", " / \\ ", "| |", " \\ / ", " '--' "],
4484
+ [" ", " .--. ", " / .. \\ ", "| |", " \\ / ", " '--' "],
4485
+ [" * ", " .--. ", " / \\ ", "| .. |", " \\ / ", " '--' "]
4486
+ ];
4487
+ var PERSONALITIES = [
4488
+ "cheerful and curious",
4489
+ "mischievous little troublemaker",
4490
+ "quiet but deeply loyal",
4491
+ "energetic and easily excited",
4492
+ "wise beyond their years",
4493
+ "endlessly hungry and proud of it",
4494
+ "dramatic about everything",
4495
+ "sleepy but surprisingly sharp",
4496
+ "obsessed with shiny things",
4497
+ "acts tough but secretly sweet",
4498
+ "chaos gremlin with a heart of gold",
4499
+ "gentle soul who loves rainy days",
4500
+ "talks big but scares easily",
4501
+ "perpetually confused but vibing",
4502
+ "old soul in a tiny body",
4503
+ "competitive about literally everything",
4504
+ "hopeless romantic daydreamer",
4505
+ "grumpy in the morning, cuddly at night",
4506
+ "thinks they are royalty",
4507
+ "mysteriously appears and disappears",
4508
+ "sings to themselves constantly",
4509
+ "collects random things obsessively",
4510
+ "fiercely protective of snacks",
4511
+ "zen master of doing nothing"
4512
+ ];
4513
+ function Hatch({ userId, onComplete }) {
4514
+ const [phase, setPhase] = useState9("egg");
4515
+ const [eggFrame, setEggFrame] = useState9(0);
4516
+ const [soul, setSoul] = useState9(null);
4517
+ const [nameInput, setNameInput] = useState9("");
4518
+ const { bones } = roll(userId);
4519
+ useEffect8(() => {
4520
+ if (phase !== "egg")
4521
+ return;
4522
+ const id = setInterval(() => {
4523
+ setEggFrame((f) => (f + 1) % EGG_FRAMES.length);
4524
+ }, 600);
4525
+ const timeout = setTimeout(() => {
4526
+ setPhase("rolling");
4527
+ }, 3000);
4528
+ return () => {
4529
+ clearInterval(id);
4530
+ clearTimeout(timeout);
4531
+ };
4532
+ }, [phase]);
4533
+ useEffect8(() => {
4534
+ if (phase !== "rolling")
4535
+ return;
4536
+ const timeout = setTimeout(() => setPhase("naming"), 1500);
4537
+ return () => clearTimeout(timeout);
4538
+ }, [phase]);
4539
+ useEffect8(() => {
4540
+ if (phase !== "reveal" || !soul)
4541
+ return;
4542
+ const timeout = setTimeout(() => {
4543
+ onComplete(hatchCompanion(userId, soul));
4544
+ }, 3000);
4545
+ return () => clearTimeout(timeout);
4546
+ }, [phase, soul, userId, onComplete]);
4547
+ const handleNameSubmit = (value) => {
4548
+ const name = value.trim();
4549
+ if (!name || name.length > 20)
4550
+ return;
4551
+ const personality = PERSONALITIES[Math.floor(Math.random() * PERSONALITIES.length)];
4552
+ setSoul({ name, personality });
4553
+ setPhase("reveal");
4554
+ };
4555
+ const spriteLines = renderSprite(bones);
4556
+ const stars = RARITY_STARS[bones.rarity];
4557
+ return /* @__PURE__ */ jsxDEV9(Box9, {
4558
+ flexDirection: "column",
4559
+ alignItems: "center",
4560
+ marginTop: 2,
4561
+ children: [
4562
+ phase === "egg" && /* @__PURE__ */ jsxDEV9(Fragment8, {
4563
+ children: [
4564
+ /* @__PURE__ */ jsxDEV9(Text9, {
4565
+ bold: true,
4566
+ color: "yellow",
4567
+ children: "an egg appears..."
4568
+ }, undefined, false, undefined, this),
4569
+ /* @__PURE__ */ jsxDEV9(Box9, {
4570
+ marginTop: 1,
4571
+ flexDirection: "column",
4572
+ children: EGG_FRAMES[eggFrame].map((line, i) => /* @__PURE__ */ jsxDEV9(Text9, {
4573
+ children: line
4574
+ }, i, false, undefined, this))
4575
+ }, undefined, false, undefined, this)
4576
+ ]
4577
+ }, undefined, true, undefined, this),
4578
+ phase === "rolling" && /* @__PURE__ */ jsxDEV9(Fragment8, {
4579
+ children: [
4580
+ /* @__PURE__ */ jsxDEV9(Text9, {
4581
+ bold: true,
4582
+ color: "cyan",
4583
+ children: "*crack* *crack*"
4584
+ }, undefined, false, undefined, this),
4585
+ /* @__PURE__ */ jsxDEV9(Box9, {
4586
+ marginTop: 1,
4587
+ flexDirection: "column",
4588
+ children: spriteLines.map((line, i) => /* @__PURE__ */ jsxDEV9(Text9, {
4589
+ children: line
4590
+ }, i, false, undefined, this))
4591
+ }, undefined, false, undefined, this),
4592
+ /* @__PURE__ */ jsxDEV9(Text9, {
4593
+ dimColor: true,
4594
+ children: [
4595
+ "a ",
4596
+ bones.rarity,
4597
+ " ",
4598
+ bones.species,
4599
+ "! ",
4600
+ stars
4601
+ ]
4602
+ }, undefined, true, undefined, this)
4603
+ ]
4604
+ }, undefined, true, undefined, this),
4605
+ phase === "naming" && /* @__PURE__ */ jsxDEV9(Fragment8, {
4606
+ children: [
4607
+ /* @__PURE__ */ jsxDEV9(Box9, {
4608
+ marginTop: 1,
4609
+ flexDirection: "column",
4610
+ children: spriteLines.map((line, i) => /* @__PURE__ */ jsxDEV9(Text9, {
4611
+ children: line
4612
+ }, i, false, undefined, this))
4613
+ }, undefined, false, undefined, this),
4614
+ /* @__PURE__ */ jsxDEV9(Text9, {
4615
+ color: "yellow",
4616
+ children: [
4617
+ "give your ",
4618
+ bones.species,
4619
+ " a name:"
4620
+ ]
4621
+ }, undefined, true, undefined, this),
4622
+ /* @__PURE__ */ jsxDEV9(Box9, {
4623
+ marginTop: 1,
4624
+ children: [
4625
+ /* @__PURE__ */ jsxDEV9(Text9, {
4626
+ dimColor: true,
4627
+ children: "> "
4628
+ }, undefined, false, undefined, this),
4629
+ /* @__PURE__ */ jsxDEV9(TextInput2, {
4630
+ value: nameInput,
4631
+ onChange: setNameInput,
4632
+ onSubmit: handleNameSubmit,
4633
+ placeholder: "type a name..."
4634
+ }, undefined, false, undefined, this)
4635
+ ]
4636
+ }, undefined, true, undefined, this)
4637
+ ]
4638
+ }, undefined, true, undefined, this),
4639
+ phase === "reveal" && soul && /* @__PURE__ */ jsxDEV9(Fragment8, {
4640
+ children: [
4641
+ /* @__PURE__ */ jsxDEV9(Box9, {
4642
+ marginTop: 1,
4643
+ flexDirection: "column",
4644
+ children: spriteLines.map((line, i) => /* @__PURE__ */ jsxDEV9(Text9, {
4645
+ children: line
4646
+ }, i, false, undefined, this))
4647
+ }, undefined, false, undefined, this),
4648
+ /* @__PURE__ */ jsxDEV9(Text9, {
4649
+ bold: true,
4650
+ color: "green",
4651
+ children: [
4652
+ "it's ",
4653
+ soul.name,
4654
+ " the ",
4655
+ bones.species,
4656
+ "! ",
4657
+ stars
4658
+ ]
4659
+ }, undefined, true, undefined, this),
4660
+ /* @__PURE__ */ jsxDEV9(Text9, {
4661
+ italic: true,
4662
+ dimColor: true,
4663
+ children: soul.personality
4664
+ }, undefined, false, undefined, this),
4665
+ /* @__PURE__ */ jsxDEV9(Box9, {
4666
+ marginTop: 1,
4667
+ children: /* @__PURE__ */ jsxDEV9(Text9, {
4668
+ dimColor: true,
4669
+ children: "starting in a moment..."
4670
+ }, undefined, false, undefined, this)
4671
+ }, undefined, false, undefined, this)
4672
+ ]
4673
+ }, undefined, true, undefined, this)
4674
+ ]
4675
+ }, undefined, true, undefined, this);
4676
+ }
4677
+
4678
+ // src/main.tsx
4679
+ import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
4680
+ var VERSION = "0.2.0";
4681
+ async function checkUpdate() {
4682
+ try {
4683
+ const res = await fetch("https://registry.npmjs.org/@whaletech%2fpet/latest", {
4684
+ signal: AbortSignal.timeout(3000)
4685
+ });
4686
+ const data = await res.json();
4687
+ if (data.version && data.version !== VERSION)
4688
+ return data.version;
4689
+ } catch {}
4690
+ }
4691
+ function Root({ userId, initialCompanion, initialState, initialQuota, connected, latestVersion }) {
4692
+ const [companion, setCompanion] = useState10(initialCompanion);
4693
+ const [state, setState] = useState10(initialState);
4694
+ const handleHatchComplete = useCallback2(async (newCompanion) => {
4695
+ setCompanion(newCompanion);
4696
+ const stateWithCompanion = {
4697
+ ...state,
4698
+ companion: {
4699
+ name: newCompanion.name,
4700
+ personality: newCompanion.personality,
4701
+ hatchedAt: newCompanion.hatchedAt
4702
+ }
4703
+ };
4704
+ setState(stateWithCompanion);
4705
+ await persistState(userId, stateWithCompanion, initialQuota);
4706
+ }, [userId, state, initialQuota]);
4707
+ const handleCompanionChange = useCallback2(async (newSpecies) => {
4708
+ if (!companion)
4709
+ return;
4710
+ const updated = { ...companion, species: newSpecies };
4711
+ setCompanion(updated);
4712
+ setState((prev) => ({
4713
+ ...prev,
4714
+ companion: prev.companion ? { ...prev.companion, speciesOverride: newSpecies } : prev.companion
4715
+ }));
4716
+ }, [companion]);
4717
+ const handleEyeChange = useCallback2(async (newEye) => {
4718
+ if (!companion)
4719
+ return;
4720
+ const updated = { ...companion, eye: newEye };
4721
+ setCompanion(updated);
4722
+ setState((prev) => ({
4723
+ ...prev,
4724
+ companion: prev.companion ? { ...prev.companion, eyeOverride: newEye } : prev.companion
4725
+ }));
4726
+ }, [companion]);
4727
+ const handleHatChange = useCallback2(async (newHat, ownedHats) => {
4728
+ if (!companion)
4729
+ return;
4730
+ const updated = { ...companion, hat: newHat };
4731
+ setCompanion(updated);
4732
+ setState((prev) => ({
4733
+ ...prev,
4734
+ companion: prev.companion ? { ...prev.companion, hatOverride: newHat, ownedHats } : prev.companion
4735
+ }));
4736
+ }, [companion]);
4737
+ const handleRarityChange = useCallback2(async (newRarity, newShiny) => {
4738
+ if (!companion)
4739
+ return;
4740
+ const updated = { ...companion, rarity: newRarity, shiny: newShiny };
4741
+ setCompanion(updated);
4742
+ setState((prev) => ({
4743
+ ...prev,
4744
+ companion: prev.companion ? { ...prev.companion, rarityOverride: newRarity, shinyOverride: newShiny || undefined } : prev.companion
4745
+ }));
4746
+ }, [companion]);
4747
+ if (!companion) {
4748
+ return /* @__PURE__ */ jsxDEV10(Hatch, {
4749
+ userId,
4750
+ onComplete: handleHatchComplete
4751
+ }, undefined, false, undefined, this);
4752
+ }
4753
+ return /* @__PURE__ */ jsxDEV10(App, {
4754
+ companion,
4755
+ initialState: state,
4756
+ userId,
4757
+ initialQuota,
4758
+ connected,
4759
+ latestVersion,
4760
+ onCompanionChange: handleCompanionChange,
4761
+ onEyeChange: handleEyeChange,
4762
+ onHatChange: handleHatChange,
4763
+ onRarityChange: handleRarityChange
4764
+ }, undefined, false, undefined, this);
4765
+ }
4766
+ async function main() {
4767
+ const { userId, token: localToken, state } = await loadState();
4768
+ if (localToken)
4769
+ setToken(localToken);
4770
+ let syncedState = state;
4771
+ let quota = null;
4772
+ let connected = false;
4773
+ const [syncResult, latestVersion] = await Promise.all([
4774
+ syncState(userId, localToken ? undefined : state),
4775
+ checkUpdate()
4776
+ ]);
4777
+ if (syncResult.ok) {
4778
+ syncedState = syncResult.data.state;
4779
+ quota = syncResult.data.quota;
4780
+ connected = true;
4781
+ const serverToken = syncResult.data.token;
4782
+ setToken(serverToken);
4783
+ await saveState(userId, syncedState, quota, serverToken);
4784
+ }
4785
+ const companion = getCompanionFromStored(userId, syncedState.companion);
4786
+ const { waitUntilExit } = render(/* @__PURE__ */ jsxDEV10(Root, {
4787
+ userId,
4788
+ initialCompanion: companion,
4789
+ initialState: syncedState,
4790
+ initialQuota: quota,
4791
+ connected,
4792
+ latestVersion
4793
+ }, undefined, false, undefined, this));
4794
+ process.on("SIGINT", async () => {
4795
+ process.exit(0);
4796
+ });
4797
+ await waitUntilExit();
4798
+ }
4799
+ main().catch((err) => {
4800
+ console.error(err);
4801
+ process.exit(1);
4802
+ });