@linxin666/dsh-pet 0.1.15 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +73 -38
- package/README.zh.md +91 -55
- package/assets/whale/pet.json +12 -1
- package/cordis.patch.yml +4 -4
- package/lib/client.js +270 -334
- package/lib/client.js.map +1 -1
- package/lib/index.js +641 -87
- package/lib/types/client/PetDockEntry.d.ts +14 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +13 -8
- package/lib/types/client/PetSettingsCard.d.ts +22 -9
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +42 -7
- package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
- package/lib/types/client/PetSprite.d.ts.map +1 -0
- package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -1
- package/lib/types/client/index.d.ts +11 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +30 -12
- package/lib/types/client/locales.d.ts +10 -10
- package/lib/types/client/locales.js +10 -10
- package/lib/types/client/pet-store.d.ts +5 -0
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/settings-form.d.ts +62 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +61 -12
- package/lib/types/client/spritesheet.d.ts +14 -48
- package/lib/types/client/spritesheet.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +13 -81
- package/lib/types/index.d.ts +24 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +53 -33
- package/lib/types/ledger.d.ts +4 -2
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +11 -4
- package/lib/types/persist.d.ts +10 -3
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +32 -5
- package/lib/types/registry.d.ts +158 -0
- package/lib/types/registry.d.ts.map +1 -0
- package/lib/types/registry.js +276 -0
- package/lib/types/routes.d.ts +10 -10
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +169 -51
- package/lib/types/service.d.ts +46 -13
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +73 -16
- package/package.json +2 -2
- package/src/client/PetDockEntry.tsx +34 -26
- package/src/client/PetSettingsCard.tsx +67 -22
- package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
- package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
- package/src/client/PluginSettingsCard.tsx +7 -1
- package/src/client/index.ts +36 -14
- package/src/client/locales.ts +10 -10
- package/src/client/pet-store.ts +9 -0
- package/src/client/settings-card.module.css +3 -0
- package/src/client/settings-form.ts +114 -12
- package/src/client/spritesheet.ts +16 -92
- package/src/index.test.ts +23 -0
- package/src/index.ts +89 -45
- package/src/ledger.test.ts +5 -3
- package/src/ledger.ts +11 -4
- package/src/persist.test.ts +52 -3
- package/src/persist.ts +42 -8
- package/src/registry.test.ts +132 -0
- package/src/registry.ts +400 -0
- package/src/routes.ts +166 -53
- package/src/service.ts +102 -20
- package/lib/types/client/WhalePet.d.ts.map +0 -1
package/lib/index.js
CHANGED
|
@@ -2,11 +2,11 @@ import { a as AFFINITY_MAX, c as applyInteraction, d as emptyAffinity, f as rank
|
|
|
2
2
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
3
|
import z from "schemastery";
|
|
4
4
|
import { Service } from "@deepseek-ai/cordis";
|
|
5
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { isAbsolute, join } from "node:path";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
-
import { readFile } from "node:fs/promises";
|
|
9
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
10
|
//#region src/event-projection.ts
|
|
11
11
|
/** Fresh projection runtime for a newly seen session. */
|
|
12
12
|
function emptyProjectionRuntime() {
|
|
@@ -259,11 +259,23 @@ var PetLedger = class {
|
|
|
259
259
|
};
|
|
260
260
|
this.dirty = true;
|
|
261
261
|
}
|
|
262
|
-
/** Replace the pet
|
|
263
|
-
|
|
262
|
+
/** Replace the selected pet id (validation stays a caller concern). */
|
|
263
|
+
setPetId(petId) {
|
|
264
|
+
if (this.current.petId === petId) return;
|
|
264
265
|
this.current = {
|
|
265
266
|
...this.current,
|
|
266
|
-
|
|
267
|
+
petId
|
|
268
|
+
};
|
|
269
|
+
this.dirty = true;
|
|
270
|
+
}
|
|
271
|
+
/** Replace one pet's display name (validation stays a caller concern). */
|
|
272
|
+
setPetName(petId, name) {
|
|
273
|
+
this.current = {
|
|
274
|
+
...this.current,
|
|
275
|
+
names: {
|
|
276
|
+
...this.current.names,
|
|
277
|
+
[petId]: name
|
|
278
|
+
}
|
|
267
279
|
};
|
|
268
280
|
this.dirty = true;
|
|
269
281
|
}
|
|
@@ -327,7 +339,7 @@ var PetLedger = class {
|
|
|
327
339
|
if (kind === "feed") {
|
|
328
340
|
const consume = consumeTreat(this.current.treats);
|
|
329
341
|
if (!consume.ok) return {
|
|
330
|
-
reaction: "
|
|
342
|
+
reaction: "没有小鱼干了,多陪我工作一会儿吧~",
|
|
331
343
|
delta: 0,
|
|
332
344
|
affinity: this.affinityView(nowMs)
|
|
333
345
|
};
|
|
@@ -401,11 +413,16 @@ const defaultDisplayConfig = {
|
|
|
401
413
|
bottom: 20
|
|
402
414
|
};
|
|
403
415
|
const DISPLAY_INSET_MAX = 1e4;
|
|
404
|
-
/**
|
|
416
|
+
/** Pet id the legacy single-pet installs resolve to on migration. */
|
|
417
|
+
const DEFAULT_PET_ID = "whale-girl";
|
|
418
|
+
/** Default pet name (used only when a manifest carries no displayName). */
|
|
405
419
|
const DEFAULT_PET_NAME = "鲸鱼娘";
|
|
420
|
+
/** Name constraints. */
|
|
421
|
+
const PET_NAME_MAX_LENGTH = 20;
|
|
406
422
|
function emptyPersist() {
|
|
407
423
|
return {
|
|
408
|
-
|
|
424
|
+
petId: DEFAULT_PET_ID,
|
|
425
|
+
names: {},
|
|
409
426
|
affinity: emptyAffinity(),
|
|
410
427
|
treats: emptyTreatLedger(),
|
|
411
428
|
display: { ...defaultDisplayConfig }
|
|
@@ -423,6 +440,18 @@ function petHomeDir() {
|
|
|
423
440
|
function finiteNum(value, fallback) {
|
|
424
441
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
425
442
|
}
|
|
443
|
+
/** Sanitize the per-pet names map (string keys, non-empty trimmed values). */
|
|
444
|
+
function loadPetNames(parsed) {
|
|
445
|
+
const names = {};
|
|
446
|
+
if (typeof parsed.names !== "object" || parsed.names === null) return names;
|
|
447
|
+
for (const [id, value] of Object.entries(parsed.names)) {
|
|
448
|
+
if (id === "" || typeof value !== "string") continue;
|
|
449
|
+
const name = value.trim();
|
|
450
|
+
if (name === "") continue;
|
|
451
|
+
names[id] = name.slice(0, 20);
|
|
452
|
+
}
|
|
453
|
+
return names;
|
|
454
|
+
}
|
|
426
455
|
/** Clamp one count/score into [0, max]. */
|
|
427
456
|
function clamp(value, max) {
|
|
428
457
|
return Math.min(max, Math.max(0, value));
|
|
@@ -455,8 +484,12 @@ function loadPetPersist(dir = petHomeDir()) {
|
|
|
455
484
|
right: Math.round(clamp(finiteNum(rawDisplay.right, base.display.right), DISPLAY_INSET_MAX)),
|
|
456
485
|
bottom: Math.round(clamp(finiteNum(rawDisplay.bottom, base.display.bottom), DISPLAY_INSET_MAX))
|
|
457
486
|
};
|
|
487
|
+
const petId = typeof parsed.petId === "string" && parsed.petId.trim() !== "" ? parsed.petId.trim() : base.petId;
|
|
488
|
+
const names = loadPetNames(parsed);
|
|
489
|
+
if (typeof parsed.name === "string" && parsed.name.trim() !== "" && names[petId] === void 0) names[petId] = parsed.name.trim().slice(0, 20);
|
|
458
490
|
return {
|
|
459
|
-
|
|
491
|
+
petId,
|
|
492
|
+
names,
|
|
460
493
|
affinity,
|
|
461
494
|
treats,
|
|
462
495
|
display
|
|
@@ -473,6 +506,352 @@ function savePetPersist(data, dir = petHomeDir()) {
|
|
|
473
506
|
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
474
507
|
renameSync(tmp, target);
|
|
475
508
|
}
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/registry.ts
|
|
511
|
+
/**
|
|
512
|
+
* Pet registry — the multi-pet contract. One pet is a directory holding a
|
|
513
|
+
* 'pet.json' manifest plus an atlas image; nothing else is required, and no
|
|
514
|
+
* host or client code changes when a pet is added. The registry scans three
|
|
515
|
+
* sources, later sources overriding earlier ones on an id collision:
|
|
516
|
+
*
|
|
517
|
+
* 1. the package's own 'assets' subdirectories (built-in pets);
|
|
518
|
+
* 2. '${CODEX_HOME:-~/.codex}/pets' subdirectories (hatch-pet custom pets);
|
|
519
|
+
* 3. 'PetConfig.pets' manifests composed by the embedding application
|
|
520
|
+
* (highest precedence).
|
|
521
|
+
*
|
|
522
|
+
* The manifest follows the Codex/hatch-pet contract (8 columns x 9 rows of
|
|
523
|
+
* 192x208 cells, the 9-state row order below). Legacy whale-girl manifests
|
|
524
|
+
* that only carry 'frames' keep working: geometry, per-row frame counts and
|
|
525
|
+
* per-track rhythm all fall back to the hatch-pet contract defaults, and the
|
|
526
|
+
* whale-girl manifest overrides its own durations.
|
|
527
|
+
* @module @linxin666/dsh-pet/registry
|
|
528
|
+
*/
|
|
529
|
+
/** Fixed row order of the 9-state animation contract. */
|
|
530
|
+
const PET_ROW_ORDER = [
|
|
531
|
+
"idle",
|
|
532
|
+
"running-right",
|
|
533
|
+
"running-left",
|
|
534
|
+
"waving",
|
|
535
|
+
"jumping",
|
|
536
|
+
"failed",
|
|
537
|
+
"waiting",
|
|
538
|
+
"running",
|
|
539
|
+
"review"
|
|
540
|
+
];
|
|
541
|
+
/** Atlas cell size in px (Codex/hatch-pet contract). */
|
|
542
|
+
const DEFAULT_PET_CELL = {
|
|
543
|
+
width: 192,
|
|
544
|
+
height: 208
|
|
545
|
+
};
|
|
546
|
+
/** Columns per row (max frames per track). */
|
|
547
|
+
const DEFAULT_PET_COLUMNS = 8;
|
|
548
|
+
/** Rows in the atlas (fixed by the animation contract). */
|
|
549
|
+
const DEFAULT_PET_ROW_COUNT = 9;
|
|
550
|
+
/**
|
|
551
|
+
* Per-row used-column counts from the hatch-pet contract table. Manifests
|
|
552
|
+
* that carry no 'frames' field (the Codex custom-pet shape) resolve here.
|
|
553
|
+
*/
|
|
554
|
+
const DEFAULT_FRAME_COUNTS = [
|
|
555
|
+
6,
|
|
556
|
+
8,
|
|
557
|
+
8,
|
|
558
|
+
4,
|
|
559
|
+
5,
|
|
560
|
+
8,
|
|
561
|
+
6,
|
|
562
|
+
6,
|
|
563
|
+
6
|
|
564
|
+
];
|
|
565
|
+
/** Absolute package root, resolved from a module URL (lib/ or src/). */
|
|
566
|
+
function petPackageRoot(importMetaUrl) {
|
|
567
|
+
return fileURLToPath(new URL("../", importMetaUrl));
|
|
568
|
+
}
|
|
569
|
+
/** Resolve the hatch-pet custom pets directory (CODEX_HOME or ~/.codex). */
|
|
570
|
+
function codexPetsDir(env = process.env, home = homedir()) {
|
|
571
|
+
const raw = env.CODEX_HOME !== void 0 && env.CODEX_HOME.trim() !== "" ? env.CODEX_HOME.trim() : join(home, ".codex");
|
|
572
|
+
return join(raw === "~" ? home : raw.startsWith("~/") || raw.startsWith("~\\") ? join(home, raw.slice(2)) : raw, "pets");
|
|
573
|
+
}
|
|
574
|
+
/** Finite non-negative integer guard, else the fallback. */
|
|
575
|
+
function finiteInt(value, fallback, max) {
|
|
576
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= max ? value : fallback;
|
|
577
|
+
}
|
|
578
|
+
/** Build the browser URL of one pet asset. */
|
|
579
|
+
function assetUrl(prefix, id, file) {
|
|
580
|
+
const path = file.split("/").filter((segment) => segment !== "").join("/");
|
|
581
|
+
return prefix + "/" + encodeURIComponent(id) + "/" + path;
|
|
582
|
+
}
|
|
583
|
+
/** Default per-track rhythm (hatch-pet contract table). */
|
|
584
|
+
const DEFAULT_TRACK_PATTERNS = {
|
|
585
|
+
idle: {
|
|
586
|
+
durations: [
|
|
587
|
+
280,
|
|
588
|
+
110,
|
|
589
|
+
110,
|
|
590
|
+
140,
|
|
591
|
+
140,
|
|
592
|
+
320
|
|
593
|
+
],
|
|
594
|
+
loop: true
|
|
595
|
+
},
|
|
596
|
+
"running-right": {
|
|
597
|
+
durations: [
|
|
598
|
+
120,
|
|
599
|
+
120,
|
|
600
|
+
120,
|
|
601
|
+
120,
|
|
602
|
+
120,
|
|
603
|
+
120,
|
|
604
|
+
120,
|
|
605
|
+
220
|
|
606
|
+
],
|
|
607
|
+
loop: true
|
|
608
|
+
},
|
|
609
|
+
"running-left": {
|
|
610
|
+
durations: [
|
|
611
|
+
120,
|
|
612
|
+
120,
|
|
613
|
+
120,
|
|
614
|
+
120,
|
|
615
|
+
120,
|
|
616
|
+
120,
|
|
617
|
+
120,
|
|
618
|
+
220
|
|
619
|
+
],
|
|
620
|
+
loop: true
|
|
621
|
+
},
|
|
622
|
+
waving: {
|
|
623
|
+
durations: [
|
|
624
|
+
140,
|
|
625
|
+
140,
|
|
626
|
+
140,
|
|
627
|
+
280
|
|
628
|
+
],
|
|
629
|
+
loop: true
|
|
630
|
+
},
|
|
631
|
+
jumping: {
|
|
632
|
+
durations: [
|
|
633
|
+
140,
|
|
634
|
+
140,
|
|
635
|
+
140,
|
|
636
|
+
140,
|
|
637
|
+
280
|
|
638
|
+
],
|
|
639
|
+
loop: false,
|
|
640
|
+
fallback: "idle"
|
|
641
|
+
},
|
|
642
|
+
failed: {
|
|
643
|
+
durations: [
|
|
644
|
+
140,
|
|
645
|
+
140,
|
|
646
|
+
140,
|
|
647
|
+
140,
|
|
648
|
+
140,
|
|
649
|
+
140,
|
|
650
|
+
140,
|
|
651
|
+
240
|
|
652
|
+
],
|
|
653
|
+
loop: false,
|
|
654
|
+
fallback: "idle"
|
|
655
|
+
},
|
|
656
|
+
waiting: {
|
|
657
|
+
durations: [
|
|
658
|
+
150,
|
|
659
|
+
150,
|
|
660
|
+
150,
|
|
661
|
+
150,
|
|
662
|
+
150,
|
|
663
|
+
260
|
|
664
|
+
],
|
|
665
|
+
loop: true
|
|
666
|
+
},
|
|
667
|
+
running: {
|
|
668
|
+
durations: [
|
|
669
|
+
120,
|
|
670
|
+
120,
|
|
671
|
+
120,
|
|
672
|
+
120,
|
|
673
|
+
120,
|
|
674
|
+
220
|
|
675
|
+
],
|
|
676
|
+
loop: true
|
|
677
|
+
},
|
|
678
|
+
review: {
|
|
679
|
+
durations: [
|
|
680
|
+
150,
|
|
681
|
+
150,
|
|
682
|
+
150,
|
|
683
|
+
150,
|
|
684
|
+
150,
|
|
685
|
+
280
|
|
686
|
+
],
|
|
687
|
+
loop: true
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
/** Stable id charset: keeps asset URLs plain and filesystem-safe. */
|
|
691
|
+
const PET_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
692
|
+
/** Safe path-segment charset for atlas files. */
|
|
693
|
+
const PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
694
|
+
const PET_NAME_MAX_LENGTH$1 = 80;
|
|
695
|
+
/**
|
|
696
|
+
* Normalize one parsed manifest into a renderable pet entry, or undefined
|
|
697
|
+
* (with a warning recorded) when the manifest violates the contract.
|
|
698
|
+
*/
|
|
699
|
+
function resolvePetManifest(raw, dir, options = {}) {
|
|
700
|
+
const { assetPrefix = "/pet", warnings = [] } = options;
|
|
701
|
+
const warn = (message) => {
|
|
702
|
+
warnings.push(message);
|
|
703
|
+
};
|
|
704
|
+
if (typeof raw !== "object" || raw === null) {
|
|
705
|
+
warn("manifest is not an object");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const source = raw;
|
|
709
|
+
const id = typeof source.id === "string" ? source.id.trim() : "";
|
|
710
|
+
if (!PET_ID_PATTERN.test(id)) {
|
|
711
|
+
warn("manifest id " + JSON.stringify(String(source.id)) + " is not a lowercase kebab id");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const displayName = typeof source.displayName === "string" && source.displayName.trim() !== "" ? source.displayName.trim().slice(0, PET_NAME_MAX_LENGTH$1) : id;
|
|
715
|
+
const description = typeof source.description === "string" ? source.description.trim() : "";
|
|
716
|
+
const spritesheet = typeof source.spritesheetPath === "string" && source.spritesheetPath.trim() !== "" ? source.spritesheetPath.trim() : "spritesheet.webp";
|
|
717
|
+
const spritesheetPath = spritesheet.split("/").filter((segment) => segment !== "");
|
|
718
|
+
if (spritesheetPath.length === 0 || isAbsolute(spritesheet) || spritesheet.includes("\\") || spritesheetPath.some((segment) => segment === ".." || !PATH_SEGMENT_PATTERN.test(segment))) {
|
|
719
|
+
warn("manifest spritesheetPath " + JSON.stringify(spritesheet) + " is not a safe relative path");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const rawCell = typeof source.cell === "object" && source.cell !== null ? source.cell : {};
|
|
723
|
+
const cell = {
|
|
724
|
+
width: finiteInt(rawCell.width, DEFAULT_PET_CELL.width, 2048),
|
|
725
|
+
height: finiteInt(rawCell.height, DEFAULT_PET_CELL.height, 2048)
|
|
726
|
+
};
|
|
727
|
+
const columns = finiteInt(source.columns, 8, 32);
|
|
728
|
+
const rows = DEFAULT_FRAME_COUNTS.map((fallback, index) => {
|
|
729
|
+
return finiteInt(Array.isArray(source.frames) ? source.frames[index] : void 0, fallback, columns);
|
|
730
|
+
});
|
|
731
|
+
const trackOverrides = typeof source.tracks === "object" && source.tracks !== null ? source.tracks : {};
|
|
732
|
+
const tracks = {};
|
|
733
|
+
for (const [row, animation] of PET_ROW_ORDER.entries()) {
|
|
734
|
+
const pattern = DEFAULT_TRACK_PATTERNS[animation];
|
|
735
|
+
const override = trackOverrides[animation];
|
|
736
|
+
const durations = Array.isArray(override?.durations) && override.durations.length > 0 ? override.durations.filter((value) => typeof value === "number" && Number.isFinite(value) && value > 0) : pattern.durations;
|
|
737
|
+
if (durations.length === 0) {
|
|
738
|
+
warn("manifest " + id + ": track " + animation + " carries no usable durations");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const frameCount = Math.max(1, Math.min(rows[row], columns));
|
|
742
|
+
const sized = durations.length >= frameCount ? durations.slice(0, frameCount) : Array.from({ length: frameCount }, (_, index) => durations[index % durations.length]);
|
|
743
|
+
tracks[animation] = {
|
|
744
|
+
frames: Array.from({ length: frameCount }, (_, index) => index),
|
|
745
|
+
durations: sized,
|
|
746
|
+
loop: typeof override?.loop === "boolean" ? override.loop : pattern.loop,
|
|
747
|
+
...override?.fallback === void 0 ? pattern.fallback === void 0 ? {} : { fallback: pattern.fallback } : PET_ROW_ORDER.includes(override.fallback) ? { fallback: override.fallback } : pattern.fallback === void 0 ? {} : { fallback: pattern.fallback }
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
id,
|
|
752
|
+
displayName,
|
|
753
|
+
description,
|
|
754
|
+
cell,
|
|
755
|
+
columns,
|
|
756
|
+
rows,
|
|
757
|
+
tracks,
|
|
758
|
+
atlasUrl: assetUrl(assetPrefix, id, spritesheet),
|
|
759
|
+
manifestUrl: assetUrl(assetPrefix, id, "pet.json"),
|
|
760
|
+
dir,
|
|
761
|
+
spritesheetPath: spritesheetPath.join("/")
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
/** Scan one directory of pet folders; entries come back in name order. */
|
|
765
|
+
function scanPetDir(dir, options) {
|
|
766
|
+
if (!existsSync(dir)) return [];
|
|
767
|
+
let names = [];
|
|
768
|
+
try {
|
|
769
|
+
names = readdirSync(dir).filter((name) => !name.startsWith("."));
|
|
770
|
+
} catch {
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
names.sort();
|
|
774
|
+
const entries = [];
|
|
775
|
+
for (const name of names) {
|
|
776
|
+
const manifestFile = join(dir, name, "pet.json");
|
|
777
|
+
if (!existsSync(manifestFile)) continue;
|
|
778
|
+
const parsed = readPetJson(manifestFile, options.warnings);
|
|
779
|
+
if (parsed === void 0) continue;
|
|
780
|
+
const entry = resolvePetManifest(parsed, join(dir, name), options);
|
|
781
|
+
if (entry !== void 0) entries.push(entry);
|
|
782
|
+
}
|
|
783
|
+
return entries;
|
|
784
|
+
}
|
|
785
|
+
/** Read and parse one manifest file; undefined (warning recorded) on failure. */
|
|
786
|
+
function readPetJson(file, warnings) {
|
|
787
|
+
try {
|
|
788
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
789
|
+
} catch (error) {
|
|
790
|
+
warnings?.push("skipping " + file + ": " + (error instanceof Error ? error.message : String(error)));
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Load the pet registry: built-in 'assets/*' first, then the hatch-pet
|
|
796
|
+
* custom pets directory, then composed 'extra' manifests (each later source
|
|
797
|
+
* overrides an earlier one on id collision). The registry never throws on a
|
|
798
|
+
* bad manifest: it skips it and records a warning.
|
|
799
|
+
*/
|
|
800
|
+
function loadPetRegistry(options) {
|
|
801
|
+
const { packageRoot, assetPrefix = "/pet" } = options;
|
|
802
|
+
const warnings = [];
|
|
803
|
+
const byId = /* @__PURE__ */ new Map();
|
|
804
|
+
const builtinIds = /* @__PURE__ */ new Set();
|
|
805
|
+
for (const entry of scanPetDir(join(packageRoot, "assets"), {
|
|
806
|
+
assetPrefix,
|
|
807
|
+
warnings
|
|
808
|
+
})) {
|
|
809
|
+
if (byId.has(entry.id)) {
|
|
810
|
+
warnings.push("duplicate built-in pet id " + entry.id + "; the first one wins");
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
byId.set(entry.id, entry);
|
|
814
|
+
builtinIds.add(entry.id);
|
|
815
|
+
}
|
|
816
|
+
const petsDir = options.petsDir ?? codexPetsDir();
|
|
817
|
+
if (petsDir !== "") for (const entry of scanPetDir(petsDir, {
|
|
818
|
+
assetPrefix,
|
|
819
|
+
warnings
|
|
820
|
+
})) {
|
|
821
|
+
if (byId.has(entry.id)) warnings.push("custom pet " + entry.id + " overrides the built-in one");
|
|
822
|
+
byId.set(entry.id, entry);
|
|
823
|
+
}
|
|
824
|
+
for (const manifest of options.extra ?? []) {
|
|
825
|
+
const entry = resolvePetManifest(manifest, manifest.spritesheetPath === void 0 || isAbsolute(manifest.spritesheetPath) ? join(packageRoot, "assets", "extra") : dirname(resolve(packageRoot, manifest.spritesheetPath)), {
|
|
826
|
+
assetPrefix,
|
|
827
|
+
warnings
|
|
828
|
+
});
|
|
829
|
+
if (entry === void 0) continue;
|
|
830
|
+
if (byId.has(entry.id)) warnings.push("composed pet " + entry.id + " overrides an earlier registration");
|
|
831
|
+
byId.set(entry.id, entry);
|
|
832
|
+
}
|
|
833
|
+
const entries = [...byId.values()];
|
|
834
|
+
return {
|
|
835
|
+
entries,
|
|
836
|
+
warnings,
|
|
837
|
+
byId: (id) => byId.get(id),
|
|
838
|
+
defaultEntry: () => entries.find((entry) => builtinIds.has(entry.id)) ?? entries[0]
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
/** Strip host-only fields, leaving the client-visible definition. */
|
|
842
|
+
function petEntryView(entry) {
|
|
843
|
+
return {
|
|
844
|
+
id: entry.id,
|
|
845
|
+
displayName: entry.displayName,
|
|
846
|
+
description: entry.description,
|
|
847
|
+
cell: entry.cell,
|
|
848
|
+
columns: entry.columns,
|
|
849
|
+
rows: entry.rows,
|
|
850
|
+
tracks: entry.tracks,
|
|
851
|
+
atlasUrl: entry.atlasUrl,
|
|
852
|
+
manifestUrl: entry.manifestUrl
|
|
853
|
+
};
|
|
854
|
+
}
|
|
476
855
|
/**
|
|
477
856
|
* Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
|
|
478
857
|
* written until an economic event or interaction arrives; event listeners
|
|
@@ -483,6 +862,7 @@ var PetService = class extends Service {
|
|
|
483
862
|
static inject = [];
|
|
484
863
|
machine;
|
|
485
864
|
ledger;
|
|
865
|
+
registry;
|
|
486
866
|
persistDir;
|
|
487
867
|
enabled;
|
|
488
868
|
disposeActivity;
|
|
@@ -492,11 +872,21 @@ var PetService = class extends Service {
|
|
|
492
872
|
constructor(ctx, config = {}) {
|
|
493
873
|
super(ctx, "pet");
|
|
494
874
|
this.persistDir = config.persistDir ?? petHomeDir();
|
|
875
|
+
this.registry = config.registry ?? loadPetRegistry({
|
|
876
|
+
packageRoot: petPackageRoot(import.meta.url),
|
|
877
|
+
...config.pets === void 0 ? {} : { extra: config.pets }
|
|
878
|
+
});
|
|
879
|
+
if (this.registry.entries.length === 0) throw new Error("[dsh-pet] no valid pet manifests found; nothing to render");
|
|
880
|
+
let persist = loadPetPersist(this.persistDir);
|
|
881
|
+
if (this.registry.byId(persist.petId) === void 0) persist = {
|
|
882
|
+
...persist,
|
|
883
|
+
petId: this.registry.defaultEntry().id
|
|
884
|
+
};
|
|
495
885
|
const ledgerConfig = {
|
|
496
886
|
affinity: config.affinity,
|
|
497
887
|
treats: config.treats
|
|
498
888
|
};
|
|
499
|
-
this.ledger = new PetLedger(
|
|
889
|
+
this.ledger = new PetLedger(persist, ledgerConfig);
|
|
500
890
|
this.machine = new PetStateMachine({
|
|
501
891
|
...defaultPetStateConfig,
|
|
502
892
|
...config.state ?? {}
|
|
@@ -516,9 +906,42 @@ var PetService = class extends Service {
|
|
|
516
906
|
display() {
|
|
517
907
|
return { ...this.ledger.snapshot.display };
|
|
518
908
|
}
|
|
519
|
-
/**
|
|
520
|
-
|
|
521
|
-
return this.
|
|
909
|
+
/** RPC: the registry entries the browser half renders and selects from. */
|
|
910
|
+
async pets() {
|
|
911
|
+
return this.registry.entries.map(petEntryView);
|
|
912
|
+
}
|
|
913
|
+
/** The loaded registry (the asset routes serve its entries). */
|
|
914
|
+
registrySnapshot() {
|
|
915
|
+
return this.registry;
|
|
916
|
+
}
|
|
917
|
+
/** The selected pet's registry entry. */
|
|
918
|
+
activeEntry() {
|
|
919
|
+
return this.registry.byId(this.selectedPetId()) ?? this.registry.defaultEntry();
|
|
920
|
+
}
|
|
921
|
+
/** Currently selected pet id (persisted). */
|
|
922
|
+
selectedPetId() {
|
|
923
|
+
return this.ledger.snapshot.petId;
|
|
924
|
+
}
|
|
925
|
+
/** The display name of one pet (user rename or manifest displayName). */
|
|
926
|
+
petName(petId = this.selectedPetId()) {
|
|
927
|
+
const stored = this.ledger.snapshot.names[petId];
|
|
928
|
+
if (stored !== void 0 && stored.trim() !== "") return stored;
|
|
929
|
+
return this.registry.byId(petId)?.displayName ?? "鲸鱼娘";
|
|
930
|
+
}
|
|
931
|
+
/** RPC: switch the selected pet (persisted, settings document mirrored). */
|
|
932
|
+
async setPetId(petId) {
|
|
933
|
+
const entry = this.registry.byId(petId);
|
|
934
|
+
if (entry === void 0) return {
|
|
935
|
+
ok: false,
|
|
936
|
+
error: "unknown-pet"
|
|
937
|
+
};
|
|
938
|
+
this.ledger.setPetId(entry.id);
|
|
939
|
+
this.flush();
|
|
940
|
+
this.syncSettingsFromPet();
|
|
941
|
+
return {
|
|
942
|
+
ok: true,
|
|
943
|
+
petId: entry.id
|
|
944
|
+
};
|
|
522
945
|
}
|
|
523
946
|
/** Start or stop the session-activity listeners that drive the pet. */
|
|
524
947
|
setEnabled(enabled) {
|
|
@@ -612,7 +1035,7 @@ var PetService = class extends Service {
|
|
|
612
1035
|
display: this.ledger.snapshot.display
|
|
613
1036
|
};
|
|
614
1037
|
}
|
|
615
|
-
/** RPC: rename the pet (trimmed, 1–20 chars). */
|
|
1038
|
+
/** RPC: rename the selected pet (trimmed, 1–20 chars, per-pet storage). */
|
|
616
1039
|
async setName(name) {
|
|
617
1040
|
const trimmed = name.trim();
|
|
618
1041
|
if (trimmed === "") return {
|
|
@@ -623,28 +1046,28 @@ var PetService = class extends Service {
|
|
|
623
1046
|
ok: false,
|
|
624
1047
|
error: "name-too-long"
|
|
625
1048
|
};
|
|
626
|
-
this.ledger.
|
|
1049
|
+
this.ledger.setPetName(this.selectedPetId(), trimmed);
|
|
627
1050
|
this.flush();
|
|
628
|
-
this.syncSettingsFromPet();
|
|
629
1051
|
return {
|
|
630
1052
|
ok: true,
|
|
631
1053
|
name: trimmed
|
|
632
1054
|
};
|
|
633
1055
|
}
|
|
634
1056
|
/**
|
|
635
|
-
* Apply a committed settings section to the persisted
|
|
636
|
-
* by the settings surface on every change; values are
|
|
637
|
-
* the setConfig RPC so both write paths converge.
|
|
1057
|
+
* Apply a committed settings section to the persisted selection and display
|
|
1058
|
+
* config. Called by the settings surface on every change; values are
|
|
1059
|
+
* clamped exactly like the setConfig RPC so both write paths converge.
|
|
638
1060
|
* @param section - the resolved settings section.
|
|
639
1061
|
*/
|
|
640
1062
|
applySettingsSection(section) {
|
|
1063
|
+
if (typeof section.petId === "string" && this.registry.byId(section.petId) !== void 0) this.ledger.setPetId(section.petId);
|
|
1064
|
+
else if (section.petId !== void 0) this.syncSettingsFromPet();
|
|
641
1065
|
const next = { ...this.ledger.snapshot.display };
|
|
642
1066
|
next.visible = section.visible && (section.enabled ?? true);
|
|
643
1067
|
next.size = Math.round(Math.min(512, Math.max(32, section.size)));
|
|
644
1068
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)));
|
|
645
1069
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)));
|
|
646
1070
|
this.ledger.setDisplay(next);
|
|
647
|
-
this.ledger.setName(section.name.trim());
|
|
648
1071
|
this.flush();
|
|
649
1072
|
}
|
|
650
1073
|
/** Mirror the persisted display config into the settings document (best-effort). */
|
|
@@ -657,7 +1080,7 @@ var PetService = class extends Service {
|
|
|
657
1080
|
size: snapshot.display.size,
|
|
658
1081
|
right: snapshot.display.right,
|
|
659
1082
|
bottom: snapshot.display.bottom,
|
|
660
|
-
|
|
1083
|
+
petId: snapshot.petId
|
|
661
1084
|
}).catch(() => {});
|
|
662
1085
|
}
|
|
663
1086
|
/** Award the turn reward once per completed turn (idempotent per session + turn). */
|
|
@@ -670,6 +1093,7 @@ var PetService = class extends Service {
|
|
|
670
1093
|
}
|
|
671
1094
|
view() {
|
|
672
1095
|
const snapshot = this.machine.render();
|
|
1096
|
+
const entry = this.activeEntry();
|
|
673
1097
|
return {
|
|
674
1098
|
animation: snapshot.animation,
|
|
675
1099
|
...snapshot.bubble === void 0 ? {} : { bubble: snapshot.bubble },
|
|
@@ -677,7 +1101,12 @@ var PetService = class extends Service {
|
|
|
677
1101
|
sessionActive: snapshot.sessionActive,
|
|
678
1102
|
affinity: this.ledger.affinityView(Date.now()),
|
|
679
1103
|
display: { ...this.ledger.snapshot.display },
|
|
680
|
-
|
|
1104
|
+
pet: {
|
|
1105
|
+
id: entry.id,
|
|
1106
|
+
displayName: entry.displayName,
|
|
1107
|
+
description: entry.description
|
|
1108
|
+
},
|
|
1109
|
+
name: this.petName(),
|
|
681
1110
|
treats: {
|
|
682
1111
|
stocked: this.ledger.snapshot.treats.treats,
|
|
683
1112
|
max: this.ledger.treatMax
|
|
@@ -692,21 +1121,36 @@ var PetService = class extends Service {
|
|
|
692
1121
|
};
|
|
693
1122
|
//#endregion
|
|
694
1123
|
//#region src/routes.ts
|
|
1124
|
+
/**
|
|
1125
|
+
* Pet HTTP routes — the browser half talks to the host through plain
|
|
1126
|
+
* same-origin JSON endpoints ('/api/pet/*') and loads pet assets from
|
|
1127
|
+
* '/pet/<id>/*'. The '/plugins/' endpoint only serves client bundles and RPC
|
|
1128
|
+
* domains are platform-registered, so the pet serves its own API and media —
|
|
1129
|
+
* the same pattern as dsh-remote-web-ui's '/api/pair' family. The asset route
|
|
1130
|
+
* is one prefix registration serving every registry entry (manifest, atlas,
|
|
1131
|
+
* optional previews), so adding a pet never touches route wiring.
|
|
1132
|
+
* @module @linxin666/dsh-pet/routes
|
|
1133
|
+
*/
|
|
695
1134
|
/** Browser-facing base path of the pet API. */
|
|
696
1135
|
const PET_API_PREFIX = "/api/pet";
|
|
697
|
-
/** Browser-facing base path of the pet asset routes. */
|
|
698
|
-
const PET_ASSET_PREFIX = "/pet
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1136
|
+
/** Browser-facing base path of the pet asset routes ('/pet/<id>/...'). */
|
|
1137
|
+
const PET_ASSET_PREFIX = "/pet";
|
|
1138
|
+
const MANIFEST_FILE = "pet.json";
|
|
1139
|
+
const PREVIEW_DIR = "previews";
|
|
1140
|
+
const PREVIEW_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
1141
|
+
const MIME_BY_EXT = {
|
|
1142
|
+
".webp": "image/webp",
|
|
1143
|
+
".png": "image/png",
|
|
1144
|
+
".gif": "image/gif",
|
|
1145
|
+
".jpg": "image/jpeg",
|
|
1146
|
+
".jpeg": "image/jpeg",
|
|
1147
|
+
".json": "application/json"
|
|
1148
|
+
};
|
|
1149
|
+
/** Content type by file extension (safe fallback: octet-stream). */
|
|
1150
|
+
function mimeFor(file) {
|
|
1151
|
+
const dot = file.lastIndexOf(".");
|
|
1152
|
+
if (dot < 0) return "application/octet-stream";
|
|
1153
|
+
return MIME_BY_EXT[file.slice(dot).toLowerCase()] ?? "application/octet-stream";
|
|
710
1154
|
}
|
|
711
1155
|
/** Write one JSON response. */
|
|
712
1156
|
function json(res, status, body) {
|
|
@@ -789,60 +1233,157 @@ function postRoute(path, run) {
|
|
|
789
1233
|
}
|
|
790
1234
|
};
|
|
791
1235
|
}
|
|
792
|
-
/**
|
|
1236
|
+
/** Legacy URL aliases: each entry's directory basename (e.g. 'whale'). */
|
|
1237
|
+
function dirAliases(registry) {
|
|
1238
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1239
|
+
for (const entry of registry.entries) {
|
|
1240
|
+
const alias = entry.dir.split(/[\\/]/).pop() ?? "";
|
|
1241
|
+
if (alias !== "" && !aliases.has(alias)) aliases.set(alias, entry);
|
|
1242
|
+
}
|
|
1243
|
+
return aliases;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* The one asset handler behind the '/pet' prefix. Resolves the pet by id (or
|
|
1247
|
+
* legacy directory alias), then serves exactly the files a manifest declares:
|
|
1248
|
+
* pet.json, the declared spritesheet path, and optional 'previews/<name>'
|
|
1249
|
+
* media. Composed pets without a manifest file get a synthesized pet.json.
|
|
1250
|
+
*/
|
|
1251
|
+
function assetHandler(registry) {
|
|
1252
|
+
const aliases = dirAliases(registry);
|
|
1253
|
+
return (req, res) => {
|
|
1254
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1255
|
+
res.writeHead(405);
|
|
1256
|
+
res.end();
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
let pathname;
|
|
1260
|
+
try {
|
|
1261
|
+
pathname = new URL(req.url ?? "/", "http://pet.local").pathname;
|
|
1262
|
+
} catch {
|
|
1263
|
+
res.writeHead(400);
|
|
1264
|
+
res.end();
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const segments = pathname.split("/").filter((segment) => segment !== "");
|
|
1268
|
+
if (segments[0] !== "pet" || segments[1] === void 0) {
|
|
1269
|
+
res.writeHead(404);
|
|
1270
|
+
res.end();
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
let id;
|
|
1274
|
+
try {
|
|
1275
|
+
id = decodeURIComponent(segments[1]);
|
|
1276
|
+
} catch {
|
|
1277
|
+
res.writeHead(400);
|
|
1278
|
+
res.end();
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const entry = registry.byId(id) ?? aliases.get(id);
|
|
1282
|
+
if (entry === void 0) {
|
|
1283
|
+
res.writeHead(404);
|
|
1284
|
+
res.end();
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const rest = [];
|
|
1288
|
+
for (const segment of segments.slice(2)) {
|
|
1289
|
+
let decoded;
|
|
1290
|
+
try {
|
|
1291
|
+
decoded = decodeURIComponent(segment);
|
|
1292
|
+
} catch {
|
|
1293
|
+
res.writeHead(400);
|
|
1294
|
+
res.end();
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
rest.push(decoded);
|
|
1298
|
+
}
|
|
1299
|
+
const rel = rest.join("/");
|
|
1300
|
+
let file;
|
|
1301
|
+
let synthesized = false;
|
|
1302
|
+
if (rest.length === 1 && rest[0] === MANIFEST_FILE) {
|
|
1303
|
+
const manifestFile = join(entry.dir, MANIFEST_FILE);
|
|
1304
|
+
file = existsSync(manifestFile) ? manifestFile : void 0;
|
|
1305
|
+
if (file === void 0) synthesized = true;
|
|
1306
|
+
} else if (rest.length > 0 && rel === entry.spritesheetPath) file = join(entry.dir, entry.spritesheetPath);
|
|
1307
|
+
else if (rest.length === 2 && rest[0] === PREVIEW_DIR && PREVIEW_PATTERN.test(rest[1])) {
|
|
1308
|
+
const preview = join(entry.dir, PREVIEW_DIR, rest[1]);
|
|
1309
|
+
file = existsSync(preview) ? preview : void 0;
|
|
1310
|
+
}
|
|
1311
|
+
if (synthesized) {
|
|
1312
|
+
const body = Buffer.from(JSON.stringify(petEntryView(entry), null, 2), "utf8");
|
|
1313
|
+
res.writeHead(200, {
|
|
1314
|
+
"content-type": "application/json; charset=utf-8",
|
|
1315
|
+
"content-length": String(body.byteLength),
|
|
1316
|
+
"cache-control": "no-cache"
|
|
1317
|
+
});
|
|
1318
|
+
if (req.method === "HEAD") {
|
|
1319
|
+
res.end();
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
res.end(body);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (file === void 0) {
|
|
1326
|
+
res.writeHead(404);
|
|
1327
|
+
res.end();
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
const resolved = file;
|
|
1331
|
+
readFile(resolved).then((body) => {
|
|
1332
|
+
res.writeHead(200, {
|
|
1333
|
+
"content-type": mimeFor(resolved),
|
|
1334
|
+
"content-length": String(body.byteLength),
|
|
1335
|
+
"cache-control": "no-cache"
|
|
1336
|
+
});
|
|
1337
|
+
if (req.method === "HEAD") {
|
|
1338
|
+
res.end();
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
res.end(body);
|
|
1342
|
+
}, () => {
|
|
1343
|
+
res.writeHead(404);
|
|
1344
|
+
res.end();
|
|
1345
|
+
});
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
/** Build the full route family (API + assets) for one service. */
|
|
793
1349
|
function makePetRoutes(deps) {
|
|
794
|
-
const { service
|
|
1350
|
+
const { service } = deps;
|
|
795
1351
|
const apiRoutes = [
|
|
796
|
-
getRoute(
|
|
797
|
-
|
|
1352
|
+
getRoute("/api/pet/state", () => service.state()),
|
|
1353
|
+
getRoute("/api/pet/pets", () => service.pets()),
|
|
1354
|
+
postRoute("/api/pet/interact", (body) => {
|
|
798
1355
|
const kind = body.kind;
|
|
799
1356
|
if (kind !== "pet" && kind !== "feed") return Promise.reject(/* @__PURE__ */ new Error("invalid-kind"));
|
|
800
1357
|
return service.interact(kind);
|
|
801
1358
|
}),
|
|
802
|
-
postRoute(
|
|
1359
|
+
postRoute("/api/pet/set-visible", (body) => {
|
|
803
1360
|
const visible = body.visible;
|
|
804
1361
|
if (typeof visible !== "boolean") return Promise.reject(/* @__PURE__ */ new Error("invalid-visible"));
|
|
805
1362
|
return service.setVisible(visible);
|
|
806
1363
|
}),
|
|
807
|
-
postRoute(
|
|
1364
|
+
postRoute("/api/pet/set-config", (body) => service.setConfig({
|
|
808
1365
|
...typeof body.size === "number" ? { size: body.size } : {},
|
|
809
1366
|
...typeof body.right === "number" ? { right: body.right } : {},
|
|
810
1367
|
...typeof body.bottom === "number" ? { bottom: body.bottom } : {},
|
|
811
1368
|
...typeof body.visible === "boolean" ? { visible: body.visible } : {}
|
|
812
1369
|
})),
|
|
813
|
-
postRoute(
|
|
1370
|
+
postRoute("/api/pet/set-name", (body) => {
|
|
814
1371
|
const name = body.name;
|
|
815
1372
|
if (typeof name !== "string") return Promise.reject(/* @__PURE__ */ new Error("invalid-name"));
|
|
816
1373
|
return service.setName(name);
|
|
1374
|
+
}),
|
|
1375
|
+
postRoute("/api/pet/set-pet", (body) => {
|
|
1376
|
+
const petId = body.petId;
|
|
1377
|
+
if (typeof petId !== "string") return Promise.reject(/* @__PURE__ */ new Error("invalid-pet"));
|
|
1378
|
+
return service.setPetId(petId);
|
|
817
1379
|
})
|
|
818
1380
|
];
|
|
819
|
-
const
|
|
820
|
-
kind: "
|
|
821
|
-
path:
|
|
822
|
-
handler: (
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
res.end();
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
return readFile(join(packageRoot, "assets", "whale", file.name)).then((body) => {
|
|
829
|
-
res.writeHead(200, {
|
|
830
|
-
"content-type": file.mime,
|
|
831
|
-
"content-length": String(body.byteLength),
|
|
832
|
-
"cache-control": "no-cache"
|
|
833
|
-
});
|
|
834
|
-
if (req.method === "HEAD") {
|
|
835
|
-
res.end();
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
res.end(body);
|
|
839
|
-
}, () => {
|
|
840
|
-
res.writeHead(404);
|
|
841
|
-
res.end();
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
}));
|
|
845
|
-
return [...apiRoutes, ...assetRoutes];
|
|
1381
|
+
const assetRoute = {
|
|
1382
|
+
kind: "prefix",
|
|
1383
|
+
path: PET_ASSET_PREFIX,
|
|
1384
|
+
handler: assetHandler(service.registrySnapshot())
|
|
1385
|
+
};
|
|
1386
|
+
return [...apiRoutes, assetRoute];
|
|
846
1387
|
}
|
|
847
1388
|
//#endregion
|
|
848
1389
|
//#region src/index.ts
|
|
@@ -850,31 +1391,44 @@ function makePetRoutes(deps) {
|
|
|
850
1391
|
const name = "pet";
|
|
851
1392
|
/** Services required before the pet can mount its surfaces. */
|
|
852
1393
|
const inject = ["webServer"];
|
|
853
|
-
/**
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1394
|
+
/**
|
|
1395
|
+
* Settings section schema: pet selection and display fields the web settings
|
|
1396
|
+
* surface edits. petId is a plain string on purpose: the service clamps the
|
|
1397
|
+
* resolved value against the registry, so a stored selection that points at
|
|
1398
|
+
* a removed pet cannot invalidate the section (a strict union would refuse
|
|
1399
|
+
* the whole registration). The settings card renders the actual registry
|
|
1400
|
+
* choices itself from '/api/pet/pets'.
|
|
1401
|
+
*/
|
|
1402
|
+
function makePetSettingsSchema(fallbackPetId) {
|
|
1403
|
+
return z.object({
|
|
1404
|
+
visible: z.boolean().default(true),
|
|
1405
|
+
size: z.number().step(1).min(32).max(512).default(160),
|
|
1406
|
+
right: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(24),
|
|
1407
|
+
bottom: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(20),
|
|
1408
|
+
petId: z.string().default(fallbackPetId),
|
|
1409
|
+
enabled: z.boolean().default(true)
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
862
1412
|
/** Register the pet service and its API + asset routes on the context. */
|
|
863
1413
|
function apply(ctx, config = {}) {
|
|
864
|
-
const
|
|
1414
|
+
const registry = config.registry ?? loadPetRegistry({
|
|
1415
|
+
packageRoot: petPackageRoot(import.meta.url),
|
|
1416
|
+
...config.pets === void 0 ? {} : { extra: config.pets }
|
|
1417
|
+
});
|
|
1418
|
+
const service = new PetService(ctx, {
|
|
1419
|
+
...config,
|
|
1420
|
+
registry
|
|
1421
|
+
});
|
|
865
1422
|
let current = () => base;
|
|
866
1423
|
const base = {
|
|
867
1424
|
visible: service.display().visible,
|
|
868
1425
|
size: service.display().size,
|
|
869
1426
|
right: service.display().right,
|
|
870
1427
|
bottom: service.display().bottom,
|
|
871
|
-
|
|
1428
|
+
petId: service.selectedPetId(),
|
|
872
1429
|
enabled: config.enabled ?? true
|
|
873
1430
|
};
|
|
874
|
-
const routes = makePetRoutes({
|
|
875
|
-
service,
|
|
876
|
-
packageRoot: petPackageRoot(import.meta.url)
|
|
877
|
-
});
|
|
1431
|
+
const routes = makePetRoutes({ service });
|
|
878
1432
|
let disposeRoutes;
|
|
879
1433
|
const syncRoutes = () => {
|
|
880
1434
|
const enabled = current().enabled ?? true;
|
|
@@ -889,7 +1443,7 @@ function apply(ctx, config = {}) {
|
|
|
889
1443
|
disposeRoutes = void 0;
|
|
890
1444
|
}
|
|
891
1445
|
};
|
|
892
|
-
installSettingsSection(ctx, settingsNamespace("pet"),
|
|
1446
|
+
installSettingsSection(ctx, settingsNamespace("pet"), makePetSettingsSchema(service.selectedPetId()), base, {
|
|
893
1447
|
setSource: (source) => {
|
|
894
1448
|
current = source;
|
|
895
1449
|
},
|
|
@@ -903,4 +1457,4 @@ function apply(ctx, config = {}) {
|
|
|
903
1457
|
syncRoutes();
|
|
904
1458
|
}
|
|
905
1459
|
//#endregion
|
|
906
|
-
export { AFFINITY_MAX, AFFINITY_RANKS, PET_API_PREFIX, PET_ASSET_PREFIX,
|
|
1460
|
+
export { AFFINITY_MAX, AFFINITY_RANKS, DEFAULT_FRAME_COUNTS, DEFAULT_PET_CELL, DEFAULT_PET_COLUMNS, DEFAULT_PET_ID, DEFAULT_PET_NAME, DEFAULT_PET_ROW_COUNT, DEFAULT_TRACK_PATTERNS, PET_API_PREFIX, PET_ASSET_PREFIX, PET_NAME_MAX_LENGTH, PET_ROW_ORDER, PetService, PetStateMachine, animationForPhase, apply, applyInteraction, applyTurnReward, codexPetsDir, consumeTreat, defaultDisplayConfig, defaultTreatConfig, emptyAffinity, emptyPersist, emptyTreatLedger, inject, loadPetPersist, loadPetRegistry, makePetRoutes, makePetSettingsSchema, name, petEntryView, petHomeDir, petPackageRoot, rankOf, resolvePetManifest, rowOf, savePetPersist, settleTreatGrants };
|