@linxin666/dsh-pet 0.3.20 → 0.3.22

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 (54) hide show
  1. package/lib/client.js +129 -27
  2. package/lib/client.js.map +1 -1
  3. package/lib/index.js +99 -4
  4. package/lib/types/client/PetSprite.d.ts.map +1 -1
  5. package/lib/types/client/PetSprite.js +5 -1
  6. package/lib/types/client/gameplay-hud.d.ts +15 -0
  7. package/lib/types/client/gameplay-hud.d.ts.map +1 -1
  8. package/lib/types/client/gameplay-hud.js +48 -6
  9. package/lib/types/client/index.d.ts.map +1 -1
  10. package/lib/types/client/index.js +21 -13
  11. package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -1
  12. package/lib/types/client/renderers/Frames2dVisualMount.js +5 -0
  13. package/lib/types/client/renderers/frames2d.d.ts +5 -4
  14. package/lib/types/client/renderers/frames2d.d.ts.map +1 -1
  15. package/lib/types/client/renderers/frames2d.js +27 -18
  16. package/lib/types/client/work-tick-gate.d.ts +40 -0
  17. package/lib/types/client/work-tick-gate.d.ts.map +1 -0
  18. package/lib/types/client/work-tick-gate.js +49 -0
  19. package/lib/types/gameplay.d.ts +4 -0
  20. package/lib/types/gameplay.d.ts.map +1 -1
  21. package/lib/types/gameplay.js +11 -2
  22. package/lib/types/ledger.d.ts +8 -0
  23. package/lib/types/ledger.d.ts.map +1 -1
  24. package/lib/types/ledger.js +25 -0
  25. package/lib/types/persist.d.ts +6 -0
  26. package/lib/types/persist.d.ts.map +1 -1
  27. package/lib/types/persist.js +25 -1
  28. package/lib/types/routes.d.ts.map +1 -1
  29. package/lib/types/routes.js +6 -0
  30. package/lib/types/service.d.ts +24 -0
  31. package/lib/types/service.d.ts.map +1 -1
  32. package/lib/types/service.js +32 -0
  33. package/package.json +1 -1
  34. package/src/client/PetDockEntry.test.tsx +1 -0
  35. package/src/client/PetSprite.test.tsx +62 -0
  36. package/src/client/PetSprite.tsx +5 -1
  37. package/src/client/gameplay-hud.test.tsx +165 -2
  38. package/src/client/gameplay-hud.tsx +62 -6
  39. package/src/client/index.ts +23 -13
  40. package/src/client/pet.module.css +1 -1
  41. package/src/client/renderers/Frames2dVisualMount.test.tsx +73 -0
  42. package/src/client/renderers/Frames2dVisualMount.tsx +4 -0
  43. package/src/client/renderers/frames2d.test.ts +38 -6
  44. package/src/client/renderers/frames2d.ts +27 -19
  45. package/src/client/work-tick-gate.test.ts +53 -0
  46. package/src/client/work-tick-gate.ts +63 -0
  47. package/src/gameplay.test.ts +36 -0
  48. package/src/gameplay.ts +14 -2
  49. package/src/ledger.test.ts +19 -0
  50. package/src/ledger.ts +24 -0
  51. package/src/persist.test.ts +13 -0
  52. package/src/persist.ts +29 -1
  53. package/src/routes.ts +5 -0
  54. package/src/service.ts +38 -0
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Page-wide work-tick gate for the pet's work mode. Hot reloads can leave
3
+ * several GameplayHud instances alive, each running its own work interval;
4
+ * without a shared gate every interval would call workTick and each stale call
5
+ * re-rolls, re-grants treats and re-plays the success/fail track, so the
6
+ * outcome appears to play several times per window. The gate accepts the first
7
+ * adjudication of a window and silently suppresses the duplicates that follow.
8
+ *
9
+ * The window is the active pet's own `gameplay.work.tickMs`, not a constant: a
10
+ * pet configured with a shorter cadence must actually adjudicate that often,
11
+ * while a fixed window would downgrade it without saying so (#1494).
12
+ * @module @linxin666/dsh-pet/client/work-tick-gate
13
+ */
14
+ /** Window used when the active pet declares no work cadence. */
15
+ export const DEFAULT_WORK_TICK_MS = 10_000;
16
+ /** Manifest bounds for `gameplay.work.tickMs` (src/gameplay.ts). */
17
+ const MIN_WORK_TICK_MS = 1_000;
18
+ const MAX_WORK_TICK_MS = 60_000;
19
+ /**
20
+ * The gate window for one pet: its configured cadence, clamped to the manifest's
21
+ * own bounds so a malformed registry entry cannot disable or flood the gate.
22
+ * @param tickMs - the active definition's `gameplay.work.tickMs`, when it has one.
23
+ * @returns the window in milliseconds.
24
+ */
25
+ export function workTickWindowMs(tickMs) {
26
+ if (typeof tickMs !== 'number' || !Number.isFinite(tickMs))
27
+ return DEFAULT_WORK_TICK_MS;
28
+ return Math.min(MAX_WORK_TICK_MS, Math.max(MIN_WORK_TICK_MS, tickMs));
29
+ }
30
+ /**
31
+ * Create a gate.
32
+ * @param now - the clock, injectable so tests control the window.
33
+ * @returns the gate over that clock.
34
+ */
35
+ export function createWorkTickGate(now = Date.now) {
36
+ let lastAdjudicatedAt = 0;
37
+ return {
38
+ allow: (tickMs) => {
39
+ const at = now();
40
+ if (at - lastAdjudicatedAt < workTickWindowMs(tickMs))
41
+ return false;
42
+ lastAdjudicatedAt = at;
43
+ return true;
44
+ },
45
+ reset: () => {
46
+ lastAdjudicatedAt = 0;
47
+ },
48
+ };
49
+ }
@@ -150,6 +150,10 @@ export interface PetGameplayState {
150
150
  mode: 'work' | 'sleep' | null;
151
151
  /** Epoch ms of the last lazy settle. */
152
152
  settledAt: number;
153
+ /** Accumulated remainder ms towards the next passive income tick. */
154
+ incomeCarryMs?: number;
155
+ /** Accumulated remainder ms towards the next sleep restore tick. */
156
+ restoreCarryMs?: number;
153
157
  }
154
158
  /** Fresh state for one pet: stats at their initial (default max), no currency. */
155
159
  export declare function initialGameplayState(manifest: PetGameplayManifest, now: number): PetGameplayState;
@@ -1 +1 @@
1
- {"version":3,"file":"gameplay.d.ts","sourceRoot":"","sources":["../../src/gameplay.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gameplay.d.ts","sourceRoot":"","sources":["../../src/gameplay.ts"],"names":[],"mappings":"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"}
@@ -555,7 +555,10 @@ export function settleGameplay(state, manifest, now, options) {
555
555
  }
556
556
  }
557
557
  if (manifest.passiveIncome !== undefined) {
558
- const ticks = Math.floor(elapsedMs / manifest.passiveIncome.intervalMs);
558
+ const incomeElapsed = elapsedMs + (state.incomeCarryMs ?? 0);
559
+ const interval = manifest.passiveIncome.intervalMs;
560
+ const ticks = Math.floor(incomeElapsed / interval);
561
+ state.incomeCarryMs = incomeElapsed % interval;
559
562
  if (ticks > 0) {
560
563
  const currency = manifest.passiveIncome.currency;
561
564
  state.currencies[currency] = (state.currencies[currency] ?? 0) + ticks * manifest.passiveIncome.amount;
@@ -563,13 +566,19 @@ export function settleGameplay(state, manifest, now, options) {
563
566
  }
564
567
  }
565
568
  if (state.mode === 'sleep' && manifest.sleep !== undefined) {
566
- const ticks = Math.floor(elapsedMs / manifest.sleep.restore.intervalMs);
569
+ const restoreElapsed = elapsedMs + (state.restoreCarryMs ?? 0);
570
+ const interval = manifest.sleep.restore.intervalMs;
571
+ const ticks = Math.floor(restoreElapsed / interval);
572
+ state.restoreCarryMs = restoreElapsed % interval;
567
573
  if (ticks > 0) {
568
574
  const stat = manifest.sleep.restore.stat;
569
575
  state.stats[stat] = (state.stats[stat] ?? 0) + ticks * manifest.sleep.restore.amount;
570
576
  changed = true;
571
577
  }
572
578
  }
579
+ else {
580
+ state.restoreCarryMs = 0;
581
+ }
573
582
  state.settledAt = now;
574
583
  clampGameplay(state, manifest);
575
584
  return changed;
@@ -66,6 +66,14 @@ export declare class PetLedger {
66
66
  setGameplay(petId: string, gameplay: PetGameplayState): void;
67
67
  /** Replace one pet's display name (validation stays a caller concern). */
68
68
  setPetName(petId: string, name: string): void;
69
+ /**
70
+ * Select one pet's frames2d skin; `undefined` clears the choice back to the
71
+ * pet's default look. Manifest validation stays a caller concern, exactly
72
+ * like setPetName's length check.
73
+ */
74
+ setPetSkin(petId: string, skinId: string | undefined): void;
75
+ /** The persisted skin id for one pet (undefined = the pet's default look). */
76
+ petSkin(petId: string): string | undefined;
69
77
  /**
70
78
  * Swap the reaction pools to another pet's custom remarks (called on pet
71
79
  * selection). Slots the pet does not declare fall back to voice packs or built-ins.
@@ -1 +1 @@
1
- {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/ledger.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/ledger.ts"],"names":[],"mappings":"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"}
@@ -77,6 +77,31 @@ export class PetLedger {
77
77
  this.current = { ...this.current, names: { ...this.current.names, [petId]: name } };
78
78
  this.dirty = true;
79
79
  }
80
+ /**
81
+ * Select one pet's frames2d skin; `undefined` clears the choice back to the
82
+ * pet's default look. Manifest validation stays a caller concern, exactly
83
+ * like setPetName's length check.
84
+ */
85
+ setPetSkin(petId, skinId) {
86
+ const skins = this.current.skins;
87
+ if (skinId === undefined) {
88
+ if (skins[petId] === undefined)
89
+ return;
90
+ const next = { ...skins };
91
+ delete next[petId];
92
+ this.current = { ...this.current, skins: next };
93
+ }
94
+ else {
95
+ if (skins[petId] === skinId)
96
+ return;
97
+ this.current = { ...this.current, skins: { ...skins, [petId]: skinId } };
98
+ }
99
+ this.dirty = true;
100
+ }
101
+ /** The persisted skin id for one pet (undefined = the pet's default look). */
102
+ petSkin(petId) {
103
+ return this.current.skins[petId];
104
+ }
80
105
  /**
81
106
  * Swap the reaction pools to another pet's custom remarks (called on pet
82
107
  * selection). Slots the pet does not declare fall back to voice packs or built-ins.
@@ -33,6 +33,12 @@ export interface PetPersist {
33
33
  * to its manifest displayName, so only user renames are stored here.
34
34
  */
35
35
  names: Record<string, string>;
36
+ /**
37
+ * Per-pet selected frames2d skin id (keyed by pet id). Skin ids are manifest
38
+ * data, so a stale entry (skin renamed or removed, pet swapped) is ignored
39
+ * when the state view is built instead of pinning an unresolvable track.
40
+ */
41
+ skins: Record<string, string>;
36
42
  affinity: AffinityState;
37
43
  /** Treat (小鱼干) stock ledger. */
38
44
  treats: TreatLedger;
@@ -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,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAErD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAEhE,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,OAAO,CAAA;AACpC,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;IACzB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;CAC3C;AAED,wBAAwB;AACxB,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,wBAAgB,YAAY,IAAI,UAAU,CASzC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AA+DD,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAqB,GAAG,UAAU,CAoDrE;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,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAErD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAEhE,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,OAAO,CAAA;AACpC,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;;;;OAIG;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;IACzB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;CAC3C;AAED,wBAAwB;AACxB,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,wBAAgB,YAAY,IAAI,UAAU,CAUzC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAmFD,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAqB,GAAG,UAAU,CAqDrE;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,GAAE,MAAqB,GAAG,IAAI,CAMjF"}
@@ -27,6 +27,7 @@ export function emptyPersist() {
27
27
  return {
28
28
  petId: DEFAULT_PET_ID,
29
29
  names: {},
30
+ skins: {},
30
31
  affinity: emptyAffinity(),
31
32
  treats: emptyTreatLedger(),
32
33
  display: { ...defaultDisplayConfig },
@@ -60,6 +61,21 @@ function loadPetNames(parsed) {
60
61
  }
61
62
  return names;
62
63
  }
64
+ /** Sanitize the per-pet skin selection map (string keys, non-empty trimmed values). */
65
+ function loadPetSkins(parsed) {
66
+ const skins = {};
67
+ if (typeof parsed.skins !== 'object' || parsed.skins === null)
68
+ return skins;
69
+ for (const [id, value] of Object.entries(parsed.skins)) {
70
+ if (id === '' || typeof value !== 'string')
71
+ continue;
72
+ const skin = value.trim();
73
+ if (skin === '')
74
+ continue;
75
+ skins[id] = skin;
76
+ }
77
+ return skins;
78
+ }
63
79
  /** Clamp one count/score into [0, max]. */
64
80
  function clamp(value, max) {
65
81
  return Math.min(max, Math.max(0, value));
@@ -92,12 +108,19 @@ function loadGameplay(parsed) {
92
108
  currencies[key] = Math.min(GAMEPLAY_LOAD_CURRENCY_CAP, Math.max(0, Math.floor(value)));
93
109
  }
94
110
  }
95
- result[petId] = {
111
+ const item = {
96
112
  stats,
97
113
  currencies,
98
114
  mode: record.mode === 'work' || record.mode === 'sleep' ? record.mode : null,
99
115
  settledAt: clamp(finiteNum(record.settledAt, 0), Number.MAX_SAFE_INTEGER),
100
116
  };
117
+ if (typeof record.incomeCarryMs === 'number' && Number.isFinite(record.incomeCarryMs)) {
118
+ item.incomeCarryMs = Math.max(0, record.incomeCarryMs);
119
+ }
120
+ if (typeof record.restoreCarryMs === 'number' && Number.isFinite(record.restoreCarryMs)) {
121
+ item.restoreCarryMs = Math.max(0, record.restoreCarryMs);
122
+ }
123
+ result[petId] = item;
101
124
  }
102
125
  return result;
103
126
  }
@@ -146,6 +169,7 @@ export function loadPetPersist(dir = petHomeDir()) {
146
169
  return {
147
170
  petId,
148
171
  names,
172
+ skins: loadPetSkins(parsed),
149
173
  affinity,
150
174
  treats,
151
175
  display,
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAO9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;;IAEzB,iDAAiD;;IAEjD,8EAA8E;;CAEtE,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAsBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcrF;AAqPD,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA8B,CAAA;AAe7D,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA4MD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CAgFjI;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAO9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;;IAEzB,iDAAiD;;IAEjD,8EAA8E;;CAEtE,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAsBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcrF;AAqPD,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA8B,CAAA;AAe7D,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA4MD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CAqFjI;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
@@ -586,6 +586,12 @@ export function makePetRoutes(deps) {
586
586
  return Promise.reject(new Error('invalid-name'));
587
587
  return service.setName(name);
588
588
  }),
589
+ postRoute(ctx, PET_API_PREFIX + '/set-skin', (body) => {
590
+ const skin = body.skin;
591
+ if (skin !== undefined && typeof skin !== 'string')
592
+ return Promise.reject(new Error('invalid-skin'));
593
+ return service.setSkin(skin === undefined || skin === '' ? undefined : skin);
594
+ }),
589
595
  postRoute(ctx, PET_API_PREFIX + '/set-pet', (body) => {
590
596
  const petId = body.petId;
591
597
  if (typeof petId !== 'string')
@@ -117,6 +117,12 @@ export interface PetStateView {
117
117
  };
118
118
  /** The selected pet's display name (user rename or manifest default). */
119
119
  name: string;
120
+ /**
121
+ * The selected frames2d skin id for this pet (absent = the pet's default
122
+ * look). Persisted host-side, so a page reload or client restart restores
123
+ * the user's last choice instead of falling back to the default look.
124
+ */
125
+ skin?: string;
120
126
  /** Treat (小鱼干) stock snapshot. */
121
127
  treats: {
122
128
  /** Stocked treats now. */
@@ -308,6 +314,24 @@ export declare class PetService extends Service {
308
314
  ok: true;
309
315
  display: PetDisplayConfig;
310
316
  }>;
317
+ /**
318
+ * The persisted skin for one entry, when the manifest still declares it: a
319
+ * stale id (skin removed from the manifest, pet swapped) reads as "default"
320
+ * rather than pinning a track the browser half cannot resolve.
321
+ */
322
+ private persistedSkin;
323
+ /**
324
+ * RPC: select the current pet's frames2d skin (`undefined` restores the
325
+ * pet's default look). The choice is stored per pet, so every later state
326
+ * view (reload, client restart, pet re-selection) serves it back.
327
+ */
328
+ setSkin(skin: string | undefined): Promise<{
329
+ ok: true;
330
+ skin?: string;
331
+ } | {
332
+ ok: false;
333
+ error: string;
334
+ }>;
311
335
  /** RPC: update display config (size / position). Values are clamped to whole pixels. */
312
336
  setConfig(patch: Partial<PetDisplayConfig>): Promise<{
313
337
  ok: true;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
@@ -502,6 +502,36 @@ export class PetService extends Service {
502
502
  this.syncSettingsFromPet();
503
503
  return { ok: true, display: this.ledger.snapshot.display };
504
504
  }
505
+ /**
506
+ * The persisted skin for one entry, when the manifest still declares it: a
507
+ * stale id (skin removed from the manifest, pet swapped) reads as "default"
508
+ * rather than pinning a track the browser half cannot resolve.
509
+ */
510
+ persistedSkin(entry) {
511
+ const stored = this.ledger.petSkin(entry.id);
512
+ if (stored === undefined)
513
+ return undefined;
514
+ return entry.frames2d?.skins?.some(skin => skin.id === stored) === true ? stored : undefined;
515
+ }
516
+ /**
517
+ * RPC: select the current pet's frames2d skin (`undefined` restores the
518
+ * pet's default look). The choice is stored per pet, so every later state
519
+ * view (reload, client restart, pet re-selection) serves it back.
520
+ */
521
+ async setSkin(skin) {
522
+ const entry = this.activeEntry();
523
+ const declared = entry.frames2d?.skins ?? [];
524
+ if (skin === undefined) {
525
+ this.ledger.setPetSkin(entry.id, undefined);
526
+ this.flush();
527
+ return { ok: true };
528
+ }
529
+ if (!declared.some(candidate => candidate.id === skin))
530
+ return { ok: false, error: 'unknown-skin' };
531
+ this.ledger.setPetSkin(entry.id, skin);
532
+ this.flush();
533
+ return { ok: true, skin };
534
+ }
505
535
  /** RPC: update display config (size / position). Values are clamped to whole pixels. */
506
536
  async setConfig(patch) {
507
537
  const next = { ...this.ledger.snapshot.display, ...patch };
@@ -636,6 +666,7 @@ export class PetService extends Service {
636
666
  const announcement = this.announcement !== undefined && announcementFresh(this.announcement, Date.now())
637
667
  ? this.announcement
638
668
  : undefined;
669
+ const skin = this.persistedSkin(entry);
639
670
  return {
640
671
  animation: snapshot.animation,
641
672
  ...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
@@ -652,6 +683,7 @@ export class PetService extends Service {
652
683
  description: entry.description,
653
684
  },
654
685
  name: this.petName(),
686
+ ...(skin === undefined ? {} : { skin }),
655
687
  treats: {
656
688
  stocked: this.ledger.snapshot.treats.treats,
657
689
  max: this.ledger.treatMax,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-pet",
3
3
  "description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
4
- "version": "0.3.20",
4
+ "version": "0.3.22",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -84,6 +84,7 @@ function injected(): PetInjected {
84
84
  setMode: vi.fn(),
85
85
  workTick: vi.fn(),
86
86
  buy: vi.fn(),
87
+ setSkin: vi.fn(async () => ({ ok: true })),
87
88
  },
88
89
  }
89
90
  }
@@ -858,6 +858,68 @@ describe('PetSprite status decoration (pet-center M5, #567)', () => {
858
858
  expect(el.style.backgroundPosition).toBe('0px 0px')
859
859
  })
860
860
 
861
+ it('advances uneven per-frame durations on each frame own clock', () => {
862
+ vi.spyOn(window, 'matchMedia').mockReturnValue({
863
+ matches: false,
864
+ media: '(prefers-reduced-motion: reduce)',
865
+ onchange: null,
866
+ addEventListener: () => {},
867
+ removeEventListener: () => {},
868
+ addListener: () => {},
869
+ removeListener: () => {},
870
+ dispatchEvent: () => false,
871
+ })
872
+ let now = 0
873
+ vi.spyOn(performance, 'now').mockImplementation(() => now)
874
+ const timers: { at: number; callback: () => void }[] = []
875
+ let timerId = 0
876
+ vi.spyOn(window, 'setTimeout').mockImplementation(((callback: () => void, delay = 0) => {
877
+ timers.push({ at: now + delay, callback })
878
+ return ++timerId
879
+ }) as typeof window.setTimeout)
880
+ vi.spyOn(window, 'clearTimeout').mockImplementation(() => {})
881
+ const frames: FrameRequestCallback[] = []
882
+ vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
883
+ frames.push(callback)
884
+ return frames.length
885
+ })
886
+ vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {})
887
+ const uneven: DecorationView = { ...decoration, durations: [100, 300, 50, 50] }
888
+ renderPet({ snapshot: { ...snapshot, bubble: '正在思考', phase: 'thinking', decoration: uneven } })
889
+ const el = ornament()!
890
+ const step = (ms: number): void => {
891
+ for (const callback of frames.splice(0)) callback(now)
892
+ now += ms
893
+ for (;;) {
894
+ const due = timers.filter(t => t.at <= now)
895
+ if (due.length === 0) break
896
+ for (const t of due) {
897
+ const idx = timers.indexOf(t)
898
+ if (idx >= 0) timers.splice(idx, 1)
899
+ t.callback()
900
+ }
901
+ }
902
+ }
903
+ expect(el.style.backgroundPosition).toBe('0px 0px')
904
+ // One 500 ms segment (100 + 300 + 50 + 50) plus 1 ms: the catch-up loop
905
+ // must subtract each frame's own duration, so it lands one full cycle
906
+ // later on frame 0 with 99 ms of its 100 ms left — not on frame 1.
907
+ act(() => { step(501) })
908
+ expect(el.style.backgroundPosition).toBe('0px 0px')
909
+ act(() => { step(99) })
910
+ expect(el.style.backgroundPosition).toBe('-24px 0px')
911
+ // Frame 1 lasts 300 ms and the next wake uses that duration.
912
+ act(() => { step(299) })
913
+ expect(el.style.backgroundPosition).toBe('-24px 0px')
914
+ act(() => { step(1) })
915
+ expect(el.style.backgroundPosition).toBe('-48px 0px')
916
+ // Frames 2 and 3 are 50 ms each.
917
+ act(() => { step(50) })
918
+ expect(el.style.backgroundPosition).toBe('-72px 0px')
919
+ act(() => { step(50) })
920
+ expect(el.style.backgroundPosition).toBe('0px 0px')
921
+ })
922
+
861
923
  it('holds the segment last frame when the loop is off and stops scheduling', () => {
862
924
  vi.spyOn(window, 'matchMedia').mockReturnValue({
863
925
  matches: false,
@@ -139,7 +139,7 @@ function StatusOrnament(props: { decoration: DecorationView; phase: ActivityPhas
139
139
  const delta = now - last
140
140
  last = now
141
141
  elapsed += delta
142
- const duration = decoration.durations[index] ?? 120
142
+ let duration = decoration.durations[index] ?? 120
143
143
  // The segment's frame rate (duration ms, typically 90-160) is far
144
144
  // below the rAF cadence, so a 60fps loop would spend ~90% of its
145
145
  // ticks doing nothing. Schedule by the remaining time to the next
@@ -151,6 +151,10 @@ function StatusOrnament(props: { decoration: DecorationView; phase: ActivityPhas
151
151
  elapsed -= duration
152
152
  if (index < segment.to) index += 1
153
153
  else if (decoration.loop) index = segment.from
154
+ // Durations are per frame: a catch-up that crosses frames must
155
+ // subtract and schedule with the frame it lands on, not the one
156
+ // the tick started from.
157
+ duration = decoration.durations[index] ?? 120
154
158
  } while (elapsed >= duration)
155
159
  // Only advance the background when the frame actually changes.
156
160
  el.style.backgroundPosition = position(index)