@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.
Files changed (74) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +73 -38
  3. package/README.zh.md +91 -55
  4. package/assets/whale/pet.json +12 -1
  5. package/cordis.patch.yml +4 -4
  6. package/lib/client.js +270 -334
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +641 -87
  9. package/lib/types/client/PetDockEntry.d.ts +14 -12
  10. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  11. package/lib/types/client/PetDockEntry.js +13 -8
  12. package/lib/types/client/PetSettingsCard.d.ts +22 -9
  13. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/PetSettingsCard.js +42 -7
  15. package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
  16. package/lib/types/client/PetSprite.d.ts.map +1 -0
  17. package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
  18. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/PluginSettingsCard.js +3 -1
  20. package/lib/types/client/index.d.ts +11 -8
  21. package/lib/types/client/index.d.ts.map +1 -1
  22. package/lib/types/client/index.js +30 -12
  23. package/lib/types/client/locales.d.ts +10 -10
  24. package/lib/types/client/locales.js +10 -10
  25. package/lib/types/client/pet-store.d.ts +5 -0
  26. package/lib/types/client/pet-store.d.ts.map +1 -1
  27. package/lib/types/client/pet-store.js +4 -0
  28. package/lib/types/client/settings-form.d.ts +62 -0
  29. package/lib/types/client/settings-form.d.ts.map +1 -1
  30. package/lib/types/client/settings-form.js +61 -12
  31. package/lib/types/client/spritesheet.d.ts +14 -48
  32. package/lib/types/client/spritesheet.d.ts.map +1 -1
  33. package/lib/types/client/spritesheet.js +13 -81
  34. package/lib/types/index.d.ts +24 -11
  35. package/lib/types/index.d.ts.map +1 -1
  36. package/lib/types/index.js +53 -33
  37. package/lib/types/ledger.d.ts +4 -2
  38. package/lib/types/ledger.d.ts.map +1 -1
  39. package/lib/types/ledger.js +11 -4
  40. package/lib/types/persist.d.ts +10 -3
  41. package/lib/types/persist.d.ts.map +1 -1
  42. package/lib/types/persist.js +32 -5
  43. package/lib/types/registry.d.ts +158 -0
  44. package/lib/types/registry.d.ts.map +1 -0
  45. package/lib/types/registry.js +276 -0
  46. package/lib/types/routes.d.ts +10 -10
  47. package/lib/types/routes.d.ts.map +1 -1
  48. package/lib/types/routes.js +169 -51
  49. package/lib/types/service.d.ts +46 -13
  50. package/lib/types/service.d.ts.map +1 -1
  51. package/lib/types/service.js +73 -16
  52. package/package.json +2 -2
  53. package/src/client/PetDockEntry.tsx +34 -26
  54. package/src/client/PetSettingsCard.tsx +67 -22
  55. package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
  56. package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
  57. package/src/client/PluginSettingsCard.tsx +7 -1
  58. package/src/client/index.ts +36 -14
  59. package/src/client/locales.ts +10 -10
  60. package/src/client/pet-store.ts +9 -0
  61. package/src/client/settings-card.module.css +3 -0
  62. package/src/client/settings-form.ts +114 -12
  63. package/src/client/spritesheet.ts +16 -92
  64. package/src/index.test.ts +23 -0
  65. package/src/index.ts +89 -45
  66. package/src/ledger.test.ts +5 -3
  67. package/src/ledger.ts +11 -4
  68. package/src/persist.test.ts +52 -3
  69. package/src/persist.ts +42 -8
  70. package/src/registry.test.ts +132 -0
  71. package/src/registry.ts +400 -0
  72. package/src/routes.ts +166 -53
  73. package/src/service.ts +102 -20
  74. package/lib/types/client/WhalePet.d.ts.map +0 -1
@@ -1,42 +1,16 @@
1
1
  /**
2
- * Whale-girl spritesheet geometry and animation tracks.
3
- *
4
- * The atlas follows the Codex/hatch-pet contract: 8 columns × 9 rows of
5
- * 192×208 cells (1536×1872 total), rows in this order:
6
- * 0 idle, 1 running-right, 2 running-left, 3 waving, 4 jumping,
7
- * 5 failed, 6 waiting, 7 running, 8 review
8
- *
9
- * Frame counts and per-frame durations are per-track definitions below; the
10
- * whale-girl atlas is produced by the hatch-pet pipeline, so calibrate
11
- * `TRACKS` against the actual run (`pet_request.json` frame counts) when the
12
- * asset lands. Tracks that do not loop hand off to `fallback`.
2
+ * Spritesheet geometry helpers — parameterized by the pet definition the
3
+ * host serves over '/api/pet/pets', so the browser half renders any registry
4
+ * entry without per-pet code. The per-track tables (frames, durations, loop,
5
+ * fallback) also come from the registry; these helpers only place frames,
6
+ * guard track lengths, and map the fixed 9-row animation contract.
13
7
  * @module @linxin666/dsh-pet/client/spritesheet
14
8
  */
15
9
  import type { PetAnimation } from '../state.ts';
16
- /** Atlas cell size in px (Codex contract). */
17
- export declare const FRAME_WIDTH = 192;
18
- export declare const FRAME_HEIGHT = 208;
19
- /** Columns per row (max frames per track). */
20
- export declare const FRAME_COLUMNS = 8;
21
- /** One animation track: frame indices into the row + per-frame durations. */
22
- export interface TrackDef {
23
- /** Frame indices (columns) played in order; must be < FRAME_COLUMNS. */
24
- frames: readonly number[];
25
- /** Per-frame duration in ms; same length as frames. */
26
- durations: readonly number[];
27
- /** Whether the track loops; a non-looping track hands off to fallback. */
28
- loop: boolean;
29
- /** Track to play after a non-looping track finishes. */
30
- fallback?: PetAnimation;
31
- }
32
- /**
33
- * Track definitions for the whale-girl. Durations are tuned for a soft,
34
- * slow-healing feel (roughly 2.5× the earlier fast draft — the pet should
35
- * breathe, not race); calibrate frame counts against the hatch-pet run when
36
- * the asset lands (rows may carry 4–8 frames).
37
- */
38
- export declare const TRACKS: Record<PetAnimation, TrackDef>;
39
- /** Row index of one animation track (mirrors state.ts rowOf). */
10
+ import type { PetCell, PetTrackDef } from '../registry.ts';
11
+ /** Animation track shape the frame loop consumes. */
12
+ export type TrackDef = PetTrackDef;
13
+ /** Row index of one animation track (the fixed 9-row contract). */
40
14
  export declare function rowOfTrack(animation: PetAnimation): number;
41
15
  /**
42
16
  * Background-position (px) of one frame cell within the scaled atlas.
@@ -45,25 +19,17 @@ export declare function rowOfTrack(animation: PetAnimation): number;
45
19
  * atlas coordinates here would drift each frame by the scale factor and
46
20
  * render torn/overlapping frames.
47
21
  */
48
- export declare function framePosition(row: number, col: number, scale?: number): {
22
+ export declare function framePosition(cell: PetCell, columns: number, row: number, col: number, scale?: number): {
49
23
  x: number;
50
24
  y: number;
51
25
  };
52
26
  /** Total duration of one track, ms. */
53
27
  export declare function trackDuration(track: TrackDef): number;
54
28
  /**
55
- * Detect how many frames each row actually carries by scanning the decoded
56
- * atlas for non-transparent cells (hatch-pet rows may hold 4–8 frames; the
57
- * unused trailing cells are fully transparent). Rows whose every sample is
58
- * transparent report 0.
59
- * @param image - the fully decoded spritesheet (natural size 1536×1872).
60
- * @returns per-row frame counts, length 9.
61
- */
62
- export declare function detectFrameCounts(image: HTMLImageElement): number[];
63
- /**
64
- * Trim a track to the actual frame count of its row. A row with 0 detected
65
- * frames degrades to the first frame (the atlas is still loading or corrupt)
66
- * so the pet never renders blank.
29
+ * Trim a track to the actual frame count of its row (the manifest's per-row
30
+ * counts are authoritative; this is a last-line guard against a definition
31
+ * whose row count disagrees with its track table). A row with 0 detected
32
+ * frames degrades to the first frame so the pet never renders blank.
67
33
  */
68
34
  export declare function trimTrack(track: TrackDef, frameCount: number): TrackDef;
69
35
  //# sourceMappingURL=spritesheet.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"spritesheet.d.ts","sourceRoot":"","sources":["../../../src/client/spritesheet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,8CAA8C;AAC9C,eAAO,MAAM,WAAW,MAAM,CAAA;AAC9B,eAAO,MAAM,YAAY,MAAM,CAAA;AAC/B,8CAA8C;AAC9C,eAAO,MAAM,aAAa,IAAI,CAAA;AAE9B,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,wEAAwE;IACxE,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB,uDAAuD;IACvD,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,0EAA0E;IAC1E,IAAI,EAAE,OAAO,CAAA;IACb,wDAAwD;IACxD,QAAQ,CAAC,EAAE,YAAY,CAAA;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,QAAQ,CAUjD,CAAA;AAED,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,SAAS,EAAE,YAAY,GAAG,MAAM,CAa1D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAE3F;AAED,uCAAuC;AACvC,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAErD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,EAAE,CA6BnE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,QAAQ,CAQvE"}
1
+ {"version":3,"file":"spritesheet.d.ts","sourceRoot":"","sources":["../../../src/client/spritesheet.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE1D,qDAAqD;AACrD,MAAM,MAAM,QAAQ,GAAG,WAAW,CAAA;AAElC,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,SAAS,EAAE,YAAY,GAAG,MAAM,CAa1D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAE3H;AAED,uCAAuC;AACvC,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAErD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,QAAQ,CAQvE"}
@@ -1,40 +1,12 @@
1
1
  /**
2
- * Whale-girl spritesheet geometry and animation tracks.
3
- *
4
- * The atlas follows the Codex/hatch-pet contract: 8 columns × 9 rows of
5
- * 192×208 cells (1536×1872 total), rows in this order:
6
- * 0 idle, 1 running-right, 2 running-left, 3 waving, 4 jumping,
7
- * 5 failed, 6 waiting, 7 running, 8 review
8
- *
9
- * Frame counts and per-frame durations are per-track definitions below; the
10
- * whale-girl atlas is produced by the hatch-pet pipeline, so calibrate
11
- * `TRACKS` against the actual run (`pet_request.json` frame counts) when the
12
- * asset lands. Tracks that do not loop hand off to `fallback`.
2
+ * Spritesheet geometry helpers — parameterized by the pet definition the
3
+ * host serves over '/api/pet/pets', so the browser half renders any registry
4
+ * entry without per-pet code. The per-track tables (frames, durations, loop,
5
+ * fallback) also come from the registry; these helpers only place frames,
6
+ * guard track lengths, and map the fixed 9-row animation contract.
13
7
  * @module @linxin666/dsh-pet/client/spritesheet
14
8
  */
15
- /** Atlas cell size in px (Codex contract). */
16
- export const FRAME_WIDTH = 192;
17
- export const FRAME_HEIGHT = 208;
18
- /** Columns per row (max frames per track). */
19
- export const FRAME_COLUMNS = 8;
20
- /**
21
- * Track definitions for the whale-girl. Durations are tuned for a soft,
22
- * slow-healing feel (roughly 2.5× the earlier fast draft — the pet should
23
- * breathe, not race); calibrate frame counts against the hatch-pet run when
24
- * the asset lands (rows may carry 4–8 frames).
25
- */
26
- export const TRACKS = {
27
- idle: { frames: [0, 1, 2, 3, 4, 5], durations: [400, 400, 500, 400, 400, 500], loop: true },
28
- 'running-right': { frames: [0, 1, 2, 3, 4, 5, 6, 7], durations: [225, 225, 225, 225, 225, 225, 225, 225], loop: true },
29
- 'running-left': { frames: [0, 1, 2, 3, 4, 5, 6, 7], durations: [225, 225, 225, 225, 225, 225, 225, 225], loop: true },
30
- waving: { frames: [0, 1, 2, 3], durations: [350, 350, 350, 350], loop: true },
31
- jumping: { frames: [0, 1, 2, 3, 4], durations: [300, 300, 300, 350, 350], loop: false, fallback: 'idle' },
32
- failed: { frames: [0, 1, 2, 3, 4, 5, 6, 7], durations: [450, 450, 450, 500, 550, 600, 450, 450], loop: false, fallback: 'idle' },
33
- waiting: { frames: [0, 1, 2, 3, 4, 5], durations: [450, 450, 500, 450, 450, 500], loop: true },
34
- running: { frames: [0, 1, 2, 3, 4, 5], durations: [250, 250, 250, 250, 250, 250], loop: true },
35
- review: { frames: [0, 1, 2, 3, 4, 5], durations: [550, 550, 550, 550, 550, 550], loop: true },
36
- };
37
- /** Row index of one animation track (mirrors state.ts rowOf). */
9
+ /** Row index of one animation track (the fixed 9-row contract). */
38
10
  export function rowOfTrack(animation) {
39
11
  const rows = {
40
12
  idle: 0,
@@ -56,61 +28,21 @@ export function rowOfTrack(animation) {
56
28
  * atlas coordinates here would drift each frame by the scale factor and
57
29
  * render torn/overlapping frames.
58
30
  */
59
- export function framePosition(row, col, scale = 1) {
60
- return { x: -col * FRAME_WIDTH * scale, y: -row * FRAME_HEIGHT * scale };
31
+ export function framePosition(cell, columns, row, col, scale = 1) {
32
+ return { x: -col * cell.width * scale, y: -row * cell.height * scale };
61
33
  }
62
34
  /** Total duration of one track, ms. */
63
35
  export function trackDuration(track) {
64
36
  return track.durations.reduce((sum, d) => sum + d, 0);
65
37
  }
66
38
  /**
67
- * Detect how many frames each row actually carries by scanning the decoded
68
- * atlas for non-transparent cells (hatch-pet rows may hold 4–8 frames; the
69
- * unused trailing cells are fully transparent). Rows whose every sample is
70
- * transparent report 0.
71
- * @param image - the fully decoded spritesheet (natural size 1536×1872).
72
- * @returns per-row frame counts, length 9.
73
- */
74
- export function detectFrameCounts(image) {
75
- const canvas = document.createElement('canvas');
76
- canvas.width = image.naturalWidth;
77
- canvas.height = image.naturalHeight;
78
- const ctx = canvas.getContext('2d');
79
- if (ctx === null)
80
- return Array.from({ length: 9 }, () => FRAME_COLUMNS);
81
- ctx.drawImage(image, 0, 0);
82
- const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
83
- const counts = [];
84
- const stride = FRAME_COLUMNS * FRAME_WIDTH;
85
- const probeStep = 8;
86
- const margin = 12;
87
- for (let row = 0; row < 9; row++) {
88
- let count = 0;
89
- for (let col = 0; col < FRAME_COLUMNS; col++) {
90
- let hasContent = false;
91
- const x0 = col * FRAME_WIDTH;
92
- const y0 = row * FRAME_HEIGHT;
93
- for (let y = y0 + margin; y < y0 + FRAME_HEIGHT - margin && !hasContent; y += probeStep) {
94
- for (let x = x0 + margin; x < x0 + FRAME_WIDTH - margin && !hasContent; x += probeStep) {
95
- const idx = (y * stride + x) * 4;
96
- if ((data[idx + 3] ?? 0) > 8)
97
- hasContent = true;
98
- }
99
- }
100
- if (hasContent)
101
- count += 1;
102
- }
103
- counts.push(count);
104
- }
105
- return counts;
106
- }
107
- /**
108
- * Trim a track to the actual frame count of its row. A row with 0 detected
109
- * frames degrades to the first frame (the atlas is still loading or corrupt)
110
- * so the pet never renders blank.
39
+ * Trim a track to the actual frame count of its row (the manifest's per-row
40
+ * counts are authoritative; this is a last-line guard against a definition
41
+ * whose row count disagrees with its track table). A row with 0 detected
42
+ * frames degrades to the first frame so the pet never renders blank.
111
43
  */
112
44
  export function trimTrack(track, frameCount) {
113
- const n = Math.max(1, Math.min(frameCount, track.frames.length));
45
+ const n = Math.max(1, Math.min(frameCount, track.frames.length, track.durations.length));
114
46
  return {
115
47
  frames: track.frames.slice(0, n),
116
48
  durations: track.durations.slice(0, n),
@@ -1,43 +1,56 @@
1
1
  /**
2
2
  * dsh-pet host half — mounts the pet service and its HTTP routes. The
3
- * browser half (the `./client` entry) renders the whale-girl companion and
4
- * drives it through the same-origin `/api/pet/*` JSON endpoints plus the
5
- * `/pet/whale/*` media route. Install via `dsh plugin --profile web add
6
- * link:<dsh-web-ui>/packages/dsh-pet`; the cordis.patch.yml inserts this plugin row.
3
+ * browser half (the './client' entry) renders the selected pet and drives it
4
+ * through the same-origin '/api/pet/*' JSON endpoints plus the '/pet/<id>/*'
5
+ * media route. The host builds the multi-pet registry once at startup from
6
+ * the package assets, the hatch-pet custom pets directory, and composed
7
+ * config entries; adding a pet means dropping a manifest + atlas into one of
8
+ * those sources, never touching host or client code. Install via
9
+ * 'dsh plugin --profile web add link:<dsh-web-ui>/packages/dsh-pet'; the
10
+ * cordis.patch.yml inserts this plugin row.
7
11
  * @module @linxin666/dsh-pet
8
12
  */
9
13
  import { Context } from '@deepseek-ai/cordis';
10
14
  import z from 'schemastery';
11
15
  import { type PetConfig } from './service.ts';
12
16
  export { PetService } from './service.ts';
13
- export type { PetConfig, PetInteractResult, PetStateView, } from './service.ts';
17
+ export type { PetConfig, PetInteractResult, PetSettingsSection, PetStateView, } from './service.ts';
14
18
  export { AFFINITY_MAX, AFFINITY_RANKS, applyInteraction, applyTurnReward, emptyAffinity, rankOf, } from './affinity.ts';
15
19
  export type { AffinityConfig, AffinityState, InteractionOutcome, PetInteraction, } from './affinity.ts';
16
20
  export { animationForPhase, PetStateMachine, rowOf, } from './state.ts';
17
21
  export type { ActivityPhase, PetAnimation, PetStateConfig, PetStateInput, PetStateSnapshot, } from './state.ts';
18
22
  export { consumeTreat, defaultTreatConfig, emptyTreatLedger, settleTreatGrants, } from './treats.ts';
19
23
  export type { TreatConfig, TreatLedger, TreatSettlement } from './treats.ts';
20
- export { defaultDisplayConfig, emptyPersist, loadPetPersist, petHomeDir, savePetPersist, } from './persist.ts';
24
+ export { DEFAULT_PET_ID, DEFAULT_PET_NAME, PET_NAME_MAX_LENGTH, defaultDisplayConfig, emptyPersist, loadPetPersist, petHomeDir, savePetPersist, } from './persist.ts';
21
25
  export type { PetDisplayConfig, PetPersist } from './persist.ts';
22
- export { makePetRoutes, petPackageRoot, PET_API_PREFIX, PET_ASSET_PREFIX, } from './routes.ts';
26
+ export { DEFAULT_FRAME_COUNTS, DEFAULT_PET_CELL, DEFAULT_PET_COLUMNS, DEFAULT_PET_ROW_COUNT, DEFAULT_TRACK_PATTERNS, PET_ROW_ORDER, codexPetsDir, loadPetRegistry, petEntryView, petPackageRoot, resolvePetManifest, } from './registry.ts';
27
+ export type { PetDefinition, PetEntry, PetManifest, PetRegistry, PetRegistryOptions, PetTrackDef, PetTrackOverride, } from './registry.ts';
28
+ export { makePetRoutes, PET_API_PREFIX, PET_ASSET_PREFIX, } from './routes.ts';
23
29
  /** Stable cordis plugin name (matches cordis.patch.yml insert id). */
24
30
  export declare const name = "pet";
25
31
  /** Services required before the pet can mount its surfaces. */
26
32
  export declare const inject: string[];
27
- /** Settings section schema: the display fields and name the web settings surface edits. */
28
- export declare const PET_SETTINGS_SCHEMA: z<Schemastery.ObjectS<{
33
+ /**
34
+ * Settings section schema: pet selection and display fields the web settings
35
+ * surface edits. petId is a plain string on purpose: the service clamps the
36
+ * resolved value against the registry, so a stored selection that points at
37
+ * a removed pet cannot invalidate the section (a strict union would refuse
38
+ * the whole registration). The settings card renders the actual registry
39
+ * choices itself from '/api/pet/pets'.
40
+ */
41
+ export declare function makePetSettingsSchema(fallbackPetId: string): z<Schemastery.ObjectS<{
29
42
  visible: z<boolean, boolean>;
30
43
  size: z<number, number>;
31
44
  right: z<number, number>;
32
45
  bottom: z<number, number>;
33
- name: z<string, string>;
46
+ petId: z<string, string>;
34
47
  enabled: z<boolean, boolean>;
35
48
  }>, Schemastery.ObjectT<{
36
49
  visible: z<boolean, boolean>;
37
50
  size: z<number, number>;
38
51
  right: z<number, number>;
39
52
  bottom: z<number, number>;
40
- name: z<string, string>;
53
+ petId: z<string, string>;
41
54
  enabled: z<boolean, boolean>;
42
55
  }>>;
43
56
  /** Register the pet service and its API + asset routes on the context. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAG7C,OAAO,CAAC,MAAM,aAAa,CAAA;AAC3B,OAAO,EAAsC,KAAK,SAAS,EAA2B,MAAM,cAAc,CAAA;AAU1G,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,YAAY,GACb,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,MAAM,GACP,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,KAAK,GACN,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,aAAa,EACb,YAAY,EACZ,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC5E,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,UAAU,EACV,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEhE,OAAO,EACL,aAAa,EACb,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,MAAM,aAAa,CAAA;AAEpB,sEAAsE;AACtE,eAAO,MAAM,IAAI,QAAQ,CAAA;AAEzB,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAAgB,CAAA;AAEnC,2FAA2F;AAC3F,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;GAO9B,CAAA;AAEF,0EAA0E;AAC1E,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,SAAc,GAAG,IAAI,CAoDhE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAG7C,OAAO,CAAC,MAAM,aAAa,CAAA;AAC3B,OAAO,EAAsC,KAAK,SAAS,EAA2B,MAAM,cAAc,CAAA;AAK1G,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,GACb,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,MAAM,GACP,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,KAAK,GACN,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,aAAa,EACb,YAAY,EACZ,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC5E,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,UAAU,EACV,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAChE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,cAAc,EACd,kBAAkB,GACnB,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,aAAa,EACb,QAAQ,EACR,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,WAAW,EACX,gBAAgB,GACjB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,aAAa,CAAA;AAEpB,sEAAsE;AACtE,eAAO,MAAM,IAAI,QAAQ,CAAA;AAEzB,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAAgB,CAAA;AAEnC;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM;;;;;;;;;;;;;;IAS1D;AAED,0EAA0E;AAC1E,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,SAAc,GAAG,IAAI,CA+DhE"}
@@ -1,61 +1,81 @@
1
1
  /**
2
2
  * dsh-pet host half — mounts the pet service and its HTTP routes. The
3
- * browser half (the `./client` entry) renders the whale-girl companion and
4
- * drives it through the same-origin `/api/pet/*` JSON endpoints plus the
5
- * `/pet/whale/*` media route. Install via `dsh plugin --profile web add
6
- * link:<dsh-web-ui>/packages/dsh-pet`; the cordis.patch.yml inserts this plugin row.
3
+ * browser half (the './client' entry) renders the selected pet and drives it
4
+ * through the same-origin '/api/pet/*' JSON endpoints plus the '/pet/<id>/*'
5
+ * media route. The host builds the multi-pet registry once at startup from
6
+ * the package assets, the hatch-pet custom pets directory, and composed
7
+ * config entries; adding a pet means dropping a manifest + atlas into one of
8
+ * those sources, never touching host or client code. Install via
9
+ * 'dsh plugin --profile web add link:<dsh-web-ui>/packages/dsh-pet'; the
10
+ * cordis.patch.yml inserts this plugin row.
7
11
  * @module @linxin666/dsh-pet
8
12
  */
9
13
  import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
10
14
  import z from 'schemastery';
11
15
  import { PetService, PET_SETTINGS_NAMESPACE } from "./service.js";
12
- import { makePetRoutes, petPackageRoot } from "./routes.js";
13
- import { DEFAULT_PET_NAME, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, PET_NAME_MAX_LENGTH, } from "./persist.js";
16
+ import { makePetRoutes } from "./routes.js";
17
+ import { loadPetRegistry, petPackageRoot } from "./registry.js";
18
+ import { DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN } from "./persist.js";
14
19
  export { PetService } from "./service.js";
15
20
  export { AFFINITY_MAX, AFFINITY_RANKS, applyInteraction, applyTurnReward, emptyAffinity, rankOf, } from "./affinity.js";
16
21
  export { animationForPhase, PetStateMachine, rowOf, } from "./state.js";
17
22
  export { consumeTreat, defaultTreatConfig, emptyTreatLedger, settleTreatGrants, } from "./treats.js";
18
- export { defaultDisplayConfig, emptyPersist, loadPetPersist, petHomeDir, savePetPersist, } from "./persist.js";
19
- export { makePetRoutes, petPackageRoot, PET_API_PREFIX, PET_ASSET_PREFIX, } from "./routes.js";
23
+ export { DEFAULT_PET_ID, DEFAULT_PET_NAME, PET_NAME_MAX_LENGTH, defaultDisplayConfig, emptyPersist, loadPetPersist, petHomeDir, savePetPersist, } from "./persist.js";
24
+ export { DEFAULT_FRAME_COUNTS, DEFAULT_PET_CELL, DEFAULT_PET_COLUMNS, DEFAULT_PET_ROW_COUNT, DEFAULT_TRACK_PATTERNS, PET_ROW_ORDER, codexPetsDir, loadPetRegistry, petEntryView, petPackageRoot, resolvePetManifest, } from "./registry.js";
25
+ export { makePetRoutes, PET_API_PREFIX, PET_ASSET_PREFIX, } from "./routes.js";
20
26
  /** Stable cordis plugin name (matches cordis.patch.yml insert id). */
21
27
  export const name = 'pet';
22
28
  /** Services required before the pet can mount its surfaces. */
23
29
  export const inject = ['webServer'];
24
- /** Settings section schema: the display fields and name the web settings surface edits. */
25
- export const PET_SETTINGS_SCHEMA = z.object({
26
- visible: z.boolean().default(true),
27
- size: z.number().step(1).min(DISPLAY_SIZE_MIN).max(DISPLAY_SIZE_MAX).default(160),
28
- right: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(24),
29
- bottom: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(20),
30
- name: z.string().min(1).max(PET_NAME_MAX_LENGTH).pattern(/\S/).default(DEFAULT_PET_NAME),
31
- enabled: z.boolean().default(true),
32
- });
30
+ /**
31
+ * Settings section schema: pet selection and display fields the web settings
32
+ * surface edits. petId is a plain string on purpose: the service clamps the
33
+ * resolved value against the registry, so a stored selection that points at
34
+ * a removed pet cannot invalidate the section (a strict union would refuse
35
+ * the whole registration). The settings card renders the actual registry
36
+ * choices itself from '/api/pet/pets'.
37
+ */
38
+ export function makePetSettingsSchema(fallbackPetId) {
39
+ return z.object({
40
+ visible: z.boolean().default(true),
41
+ size: z.number().step(1).min(DISPLAY_SIZE_MIN).max(DISPLAY_SIZE_MAX).default(160),
42
+ right: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(24),
43
+ bottom: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(20),
44
+ petId: z.string().default(fallbackPetId),
45
+ enabled: z.boolean().default(true),
46
+ });
47
+ }
33
48
  /** Register the pet service and its API + asset routes on the context. */
34
49
  export function apply(ctx, config = {}) {
35
- const service = new PetService(ctx, config);
36
- // The settings surface edits the display config through the `pet`
37
- // namespace. The composition `base` starts as the persisted pet.json
38
- // values (clamped to schema bounds), so an empty user layer resolves to
39
- // exactly what the pet already shows — a fresh deployment never
40
- // overwrites a customized layout, and reset re-inherits it. Runtime drag
41
- // interactions mirror back into the settings document through the service
42
- // (see syncSettingsFromPet), keeping both views consistent.
50
+ const registry = config.registry
51
+ ?? loadPetRegistry({
52
+ packageRoot: petPackageRoot(import.meta.url),
53
+ ...(config.pets === undefined ? {} : { extra: config.pets }),
54
+ });
55
+ const service = new PetService(ctx, { ...config, registry });
56
+ // The settings surface edits the pet selection + display config through
57
+ // the 'pet' namespace. The composition 'base' starts as the persisted
58
+ // pet.json values (clamped to schema bounds), so an empty user layer
59
+ // resolves to exactly what the pet already shows — a fresh deployment
60
+ // never overwrites a customized layout, and reset re-inherits it. Runtime
61
+ // drag interactions mirror back into the settings document through the
62
+ // service (see syncSettingsFromPet), keeping both views consistent.
43
63
  let current = () => base;
44
64
  const base = {
45
65
  visible: service.display().visible,
46
66
  size: service.display().size,
47
67
  right: service.display().right,
48
68
  bottom: service.display().bottom,
49
- name: service.petName(),
69
+ petId: service.selectedPetId(),
50
70
  enabled: config.enabled ?? true,
51
71
  };
52
72
  // The browser half talks to the pet through same-origin JSON endpoints and
53
- // loads the atlas from the pet's own media route (RPC domains are
54
- // platform-registered, so the pet serves its own API — the same pattern as
55
- // dsh-remote-web-ui's /api/pair family). The routes are registered while
56
- // the plugin is enabled; toggling the setting off makes the pet API
57
- // disappear until it is re-enabled.
58
- const routes = makePetRoutes({ service, packageRoot: petPackageRoot(import.meta.url) });
73
+ // loads each pet's atlas from the registry's own media route (RPC domains
74
+ // are platform-registered, so the pet serves its own API — the same
75
+ // pattern as dsh-remote-web-ui's /api/pair family). The routes are
76
+ // registered while the plugin is enabled; toggling the setting off makes
77
+ // the pet API disappear until it is re-enabled.
78
+ const routes = makePetRoutes({ service });
59
79
  let disposeRoutes;
60
80
  const syncRoutes = () => {
61
81
  const enabled = current().enabled ?? true;
@@ -71,7 +91,7 @@ export function apply(ctx, config = {}) {
71
91
  disposeRoutes = undefined;
72
92
  }
73
93
  };
74
- installSettingsSection(ctx, settingsNamespace(PET_SETTINGS_NAMESPACE), PET_SETTINGS_SCHEMA, base, {
94
+ installSettingsSection(ctx, settingsNamespace(PET_SETTINGS_NAMESPACE), makePetSettingsSchema(service.selectedPetId()), base, {
75
95
  setSource: (source) => { current = source; },
76
96
  onChange: () => {
77
97
  const section = current();
@@ -47,8 +47,10 @@ export declare class PetLedger {
47
47
  takeDirty(): boolean;
48
48
  /** Replace the display block (clamping stays a caller concern). */
49
49
  setDisplay(display: PetDisplayConfig): void;
50
- /** Replace the pet display name (validation stays a caller concern). */
51
- setName(name: string): void;
50
+ /** Replace the selected pet id (validation stays a caller concern). */
51
+ setPetId(petId: string): void;
52
+ /** Replace one pet's display name (validation stays a caller concern). */
53
+ setPetName(petId: string, name: string): void;
52
54
  /**
53
55
  * Settle the treat economy (work + time output since the last settlement).
54
56
  * A zero-gain first settlement still starts the time clock (anchor write),
@@ -1 +1 @@
1
- {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAKL,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,eAAe,CAAA;AACtB,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEhE,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;CAC9B;AAED,wEAAwE;AACxE,MAAM,WAAW,uBAAuB;IACtC,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,QAAQ,EAAE,eAAe,CAAA;CAC1B;AAED;;;;GAIG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IACzC,OAAO,CAAC,OAAO,CAAY;IAC3B,oFAAoF;IACpF,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,sBAAsB,CAAI;IAClC,OAAO,CAAC,KAAK,CAAQ;gBAET,OAAO,EAAE,UAAU,EAAE,MAAM,GAAE,YAAiB;IAM1D,iDAAiD;IACjD,IAAI,QAAQ,IAAI,cAAc,CAE7B;IAED,qEAAqE;IACrE,IAAI,QAAQ,IAAI,UAAU,CAEzB;IAED,qCAAqC;IACrC,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,+DAA+D;IAC/D,SAAS,IAAI,OAAO;IAMpB,mEAAmE;IACnE,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAK3C,wEAAwE;IACxE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK3B;;;;;OAKG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAapC;;;;OAIG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IASnE,8EAA8E;IAC9E,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IASxC,OAAO,CAAC,eAAe;IASvB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB;IAyBtE,kDAAkD;IAClD,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe;CAG7C"}
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAKL,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,eAAe,CAAA;AACtB,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEhE,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;CAC9B;AAED,wEAAwE;AACxE,MAAM,WAAW,uBAAuB;IACtC,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,QAAQ,EAAE,eAAe,CAAA;CAC1B;AAED;;;;GAIG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IACzC,OAAO,CAAC,OAAO,CAAY;IAC3B,oFAAoF;IACpF,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,sBAAsB,CAAI;IAClC,OAAO,CAAC,KAAK,CAAQ;gBAET,OAAO,EAAE,UAAU,EAAE,MAAM,GAAE,YAAiB;IAM1D,iDAAiD;IACjD,IAAI,QAAQ,IAAI,cAAc,CAE7B;IAED,qEAAqE;IACrE,IAAI,QAAQ,IAAI,UAAU,CAEzB;IAED,qCAAqC;IACrC,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,+DAA+D;IAC/D,SAAS,IAAI,OAAO;IAMpB,mEAAmE;IACnE,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAK3C,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM7B,0EAA0E;IAC1E,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAK7C;;;;;OAKG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAapC;;;;OAIG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IASnE,8EAA8E;IAC9E,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IASxC,OAAO,CAAC,eAAe;IASvB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB;IAyBtE,kDAAkD;IAClD,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe;CAG7C"}
@@ -49,9 +49,16 @@ export class PetLedger {
49
49
  this.current = { ...this.current, display };
50
50
  this.dirty = true;
51
51
  }
52
- /** Replace the pet display name (validation stays a caller concern). */
53
- setName(name) {
54
- this.current = { ...this.current, name };
52
+ /** Replace the selected pet id (validation stays a caller concern). */
53
+ setPetId(petId) {
54
+ if (this.current.petId === petId)
55
+ return;
56
+ this.current = { ...this.current, petId };
57
+ this.dirty = true;
58
+ }
59
+ /** Replace one pet's display name (validation stays a caller concern). */
60
+ setPetName(petId, name) {
61
+ this.current = { ...this.current, names: { ...this.current.names, [petId]: name } };
55
62
  this.dirty = true;
56
63
  }
57
64
  /**
@@ -118,7 +125,7 @@ export class PetLedger {
118
125
  const consume = consumeTreat(this.current.treats);
119
126
  if (!consume.ok) {
120
127
  return {
121
- reaction: '没有小鱼干了,多陪鲸鱼娘工作一会儿吧~',
128
+ reaction: '没有小鱼干了,多陪我工作一会儿吧~',
122
129
  delta: 0,
123
130
  affinity: this.affinityView(nowMs),
124
131
  };
@@ -24,14 +24,21 @@ export declare const DISPLAY_SIZE_MAX = 512;
24
24
  export declare const DISPLAY_INSET_MAX = 10000;
25
25
  /** Everything persisted for the pet. */
26
26
  export interface PetPersist {
27
- /** User-customizable pet display name. */
28
- name: string;
27
+ /** Selected pet id (a registry entry; clamped at service startup). */
28
+ petId: string;
29
+ /**
30
+ * Per-pet display names keyed by pet id. A pet without an entry falls back
31
+ * to its manifest displayName, so only user renames are stored here.
32
+ */
33
+ names: Record<string, string>;
29
34
  affinity: AffinityState;
30
35
  /** Treat (小鱼干) stock ledger. */
31
36
  treats: TreatLedger;
32
37
  display: PetDisplayConfig;
33
38
  }
34
- /** Default pet name (used until the user renames the pet). */
39
+ /** Pet id the legacy single-pet installs resolve to on migration. */
40
+ export declare const DEFAULT_PET_ID = "whale-girl";
41
+ /** Default pet name (used only when a manifest carries no displayName). */
35
42
  export declare const DEFAULT_PET_NAME = "\u9CB8\u9C7C\u5A18";
36
43
  /** Name constraints. */
37
44
  export declare const PET_NAME_MAX_LENGTH = 20;
@@ -1 +1 @@
1
- {"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../src/persist.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAA+B,KAAK,aAAa,EAAE,MAAM,eAAe,CAAA;AAC/E,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAA;AAEpF,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAA;CACf;AAED,eAAO,MAAM,oBAAoB,EAAE,gBAKlC,CAAA;AAED,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,KAAK,CAAA;AAClC,eAAO,MAAM,gBAAgB,MAAM,CAAA;AACnC,eAAO,MAAM,iBAAiB,QAAS,CAAA;AAEvC,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,aAAa,CAAA;IACvB,gCAAgC;IAChC,MAAM,EAAE,WAAW,CAAA;IACnB,OAAO,EAAE,gBAAgB,CAAA;CAC1B;AAED,8DAA8D;AAC9D,eAAO,MAAM,gBAAgB,uBAAQ,CAAA;AAErC,wBAAwB;AACxB,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,wBAAgB,YAAY,IAAI,UAAU,CAOzC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAYD,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAqB,GAAG,UAAU,CAwCrE;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,GAAE,MAAqB,GAAG,IAAI,CAMjF"}
1
+ {"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../src/persist.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAA+B,KAAK,aAAa,EAAE,MAAM,eAAe,CAAA;AAC/E,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAA;AAEpF,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAA;CACf;AAED,eAAO,MAAM,oBAAoB,EAAE,gBAKlC,CAAA;AAED,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,KAAK,CAAA;AAClC,eAAO,MAAM,gBAAgB,MAAM,CAAA;AACnC,eAAO,MAAM,iBAAiB,QAAS,CAAA;AAEvC,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,QAAQ,EAAE,aAAa,CAAA;IACvB,gCAAgC;IAChC,MAAM,EAAE,WAAW,CAAA;IACnB,OAAO,EAAE,gBAAgB,CAAA;CAC1B;AAED,qEAAqE;AACrE,eAAO,MAAM,cAAc,eAAe,CAAA;AAE1C,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,uBAAQ,CAAA;AAErC,wBAAwB;AACxB,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,wBAAgB,YAAY,IAAI,UAAU,CAQzC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AA4BD,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAqB,GAAG,UAAU,CAiDrE;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,GAAE,MAAqB,GAAG,IAAI,CAMjF"}
@@ -19,13 +19,16 @@ export const defaultDisplayConfig = {
19
19
  export const DISPLAY_SIZE_MIN = 32;
20
20
  export const DISPLAY_SIZE_MAX = 512;
21
21
  export const DISPLAY_INSET_MAX = 10_000;
22
- /** Default pet name (used until the user renames the pet). */
22
+ /** Pet id the legacy single-pet installs resolve to on migration. */
23
+ export const DEFAULT_PET_ID = 'whale-girl';
24
+ /** Default pet name (used only when a manifest carries no displayName). */
23
25
  export const DEFAULT_PET_NAME = '鲸鱼娘';
24
26
  /** Name constraints. */
25
27
  export const PET_NAME_MAX_LENGTH = 20;
26
28
  export function emptyPersist() {
27
29
  return {
28
- name: DEFAULT_PET_NAME,
30
+ petId: DEFAULT_PET_ID,
31
+ names: {},
29
32
  affinity: emptyAffinity(),
30
33
  treats: emptyTreatLedger(),
31
34
  display: { ...defaultDisplayConfig },
@@ -43,6 +46,21 @@ export function petHomeDir() {
43
46
  function finiteNum(value, fallback) {
44
47
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
45
48
  }
49
+ /** Sanitize the per-pet names map (string keys, non-empty trimmed values). */
50
+ function loadPetNames(parsed) {
51
+ const names = {};
52
+ if (typeof parsed.names !== 'object' || parsed.names === null)
53
+ return names;
54
+ for (const [id, value] of Object.entries(parsed.names)) {
55
+ if (id === '' || typeof value !== 'string')
56
+ continue;
57
+ const name = value.trim();
58
+ if (name === '')
59
+ continue;
60
+ names[id] = name.slice(0, PET_NAME_MAX_LENGTH);
61
+ }
62
+ return names;
63
+ }
46
64
  /** Clamp one count/score into [0, max]. */
47
65
  function clamp(value, max) {
48
66
  return Math.min(max, Math.max(0, value));
@@ -77,10 +95,19 @@ export function loadPetPersist(dir = petHomeDir()) {
77
95
  right: Math.round(clamp(finiteNum(rawDisplay.right, base.display.right), DISPLAY_INSET_MAX)),
78
96
  bottom: Math.round(clamp(finiteNum(rawDisplay.bottom, base.display.bottom), DISPLAY_INSET_MAX)),
79
97
  };
98
+ const petId = typeof parsed.petId === 'string' && parsed.petId.trim() !== ''
99
+ ? parsed.petId.trim()
100
+ : base.petId;
101
+ const names = loadPetNames(parsed);
102
+ // Legacy migration: pre-registry installs persisted one flat `name`
103
+ // field. Move it onto the selected pet (the legacy whale-girl unless the
104
+ // file already names another pet) so renames survive the upgrade.
105
+ if (typeof parsed.name === 'string' && parsed.name.trim() !== '' && names[petId] === undefined) {
106
+ names[petId] = parsed.name.trim().slice(0, PET_NAME_MAX_LENGTH);
107
+ }
80
108
  return {
81
- name: typeof parsed.name === 'string' && parsed.name.trim() !== ''
82
- ? parsed.name
83
- : base.name,
109
+ petId,
110
+ names,
84
111
  affinity,
85
112
  treats,
86
113
  display,