@linxin666/dsh-pet 0.3.5 → 0.3.6

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 (50) hide show
  1. package/contracts/voice-pack-v1.schema.json +30 -0
  2. package/cordis.patch.yml +1 -1
  3. package/lib/client.js +144 -16
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +56 -17
  6. package/lib/invariant.js +1 -1
  7. package/lib/{state-DrMX22GL.js → state-NvDEoCln.js} +5 -2
  8. package/lib/types/access.d.ts +5 -4
  9. package/lib/types/access.d.ts.map +1 -1
  10. package/lib/types/access.js +2 -12
  11. package/lib/types/affinity.d.ts +2 -0
  12. package/lib/types/affinity.d.ts.map +1 -1
  13. package/lib/types/affinity.js +12 -0
  14. package/lib/types/client/PetSprite.d.ts.map +1 -1
  15. package/lib/types/client/PetSprite.js +7 -1
  16. package/lib/types/client/locales.d.ts +18 -0
  17. package/lib/types/client/locales.d.ts.map +1 -1
  18. package/lib/types/client/locales.js +18 -0
  19. package/lib/types/client/renderers/frames2d.d.ts +12 -0
  20. package/lib/types/client/renderers/frames2d.d.ts.map +1 -1
  21. package/lib/types/client/renderers/frames2d.js +143 -18
  22. package/lib/types/ledger.d.ts +4 -2
  23. package/lib/types/ledger.d.ts.map +1 -1
  24. package/lib/types/ledger.js +4 -4
  25. package/lib/types/pair-access.d.ts +35 -0
  26. package/lib/types/pair-access.d.ts.map +1 -0
  27. package/lib/types/pair-access.js +19 -0
  28. package/lib/types/remarks.d.ts +1 -1
  29. package/lib/types/remarks.d.ts.map +1 -1
  30. package/lib/types/remarks.js +11 -2
  31. package/lib/types/service.d.ts.map +1 -1
  32. package/lib/types/service.js +4 -1
  33. package/lib/types/voice-pack.d.ts +5 -0
  34. package/lib/types/voice-pack.d.ts.map +1 -1
  35. package/lib/types/voice-pack.js +36 -2
  36. package/package.json +1 -1
  37. package/src/access.ts +7 -27
  38. package/src/affinity.ts +13 -0
  39. package/src/client/PetSprite.test.tsx +18 -0
  40. package/src/client/PetSprite.tsx +9 -1
  41. package/src/client/locales.ts +18 -0
  42. package/src/client/renderers/frames2d.test.ts +101 -0
  43. package/src/client/renderers/frames2d.ts +138 -18
  44. package/src/ledger.ts +6 -4
  45. package/src/pair-access.ts +49 -0
  46. package/src/remarks.test.ts +10 -0
  47. package/src/remarks.ts +9 -2
  48. package/src/service.ts +4 -1
  49. package/src/voice-pack.test.ts +39 -0
  50. package/src/voice-pack.ts +37 -2
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as RemarkPicker, a as AFFINITY_MAX, c as applyInteraction, d as emptyAffinity, f as rankOf, g as REMARK_LINE_MAX, h as REMARK_LINES_MAX, i as rowOf, l as applyTurnReward, m as REMARK_KINDS, n as animationForPhase, o as AFFINITY_RANKS, p as BUILTIN_REMARKS, r as defaultPetStateConfig, s as affinityViewOf, t as PetStateMachine, u as defaultAffinityConfig, v as builtinRemark, y as normalizePetRemarks } from "./state-DrMX22GL.js";
1
+ import { _ as RemarkPicker, a as AFFINITY_MAX, c as applyInteraction, d as emptyAffinity, f as rankOf, g as REMARK_LINE_MAX, h as REMARK_LINES_MAX, i as rowOf, l as applyTurnReward, m as REMARK_KINDS, n as animationForPhase, o as AFFINITY_RANKS, p as BUILTIN_REMARKS, r as defaultPetStateConfig, s as affinityViewOf, t as PetStateMachine, u as defaultAffinityConfig, v as builtinRemark, y as normalizePetRemarks } from "./state-NvDEoCln.js";
2
2
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  import z from "schemastery";
4
4
  import { Service } from "@deepseek-ai/cordis";
@@ -1037,7 +1037,7 @@ var PetLedger = class {
1037
1037
  ...defaultTreatConfig,
1038
1038
  ...config.treats ?? {}
1039
1039
  };
1040
- this.picker = new RemarkPicker(config.remarks);
1040
+ this.picker = new RemarkPicker(config.remarks, config.voiceRemarks);
1041
1041
  this.current = persist;
1042
1042
  }
1043
1043
  /** Affinity cooldown/rank tuning (read-only). */
@@ -1106,10 +1106,10 @@ var PetLedger = class {
1106
1106
  }
1107
1107
  /**
1108
1108
  * Swap the reaction pools to another pet's custom remarks (called on pet
1109
- * selection). Slots the pet does not declare fall back to built-ins.
1109
+ * selection). Slots the pet does not declare fall back to voice packs or built-ins.
1110
1110
  */
1111
- setRemarks(remarks) {
1112
- this.picker = new RemarkPicker(remarks);
1111
+ setRemarks(remarks, voiceRemarks) {
1112
+ this.picker = new RemarkPicker(remarks, voiceRemarks);
1113
1113
  }
1114
1114
  /**
1115
1115
  * Settle the treat economy (work + time output since the last settlement).
@@ -1600,7 +1600,9 @@ const VOICE_PACK_KEYS = /* @__PURE__ */ new Set([
1600
1600
  "tools",
1601
1601
  "toolRemaining",
1602
1602
  "whispers",
1603
- "panel"
1603
+ "panel",
1604
+ "remarks",
1605
+ "ranks"
1604
1606
  ]);
1605
1607
  /** Allowed whisper-section fields (drift-locked in tests). */
1606
1608
  const WHISPER_KEYS = /* @__PURE__ */ new Set(["categories", "results"]);
@@ -1663,10 +1665,22 @@ function normalizeVoicePack(raw, onWarning = () => {}) {
1663
1665
  };
1664
1666
  }
1665
1667
  const panel = raw.panel === void 0 ? void 0 : normalizePanel(raw.panel, onWarning);
1666
- if (overrides.status === void 0 && overrides.tools === void 0 && overrides.toolRemaining === void 0 && overrides.whispers === void 0 && panel === void 0) return;
1668
+ const remarks = raw.remarks === void 0 ? void 0 : normalizePetRemarks(raw.remarks, onWarning);
1669
+ const ranksRaw = raw.ranks;
1670
+ let ranks;
1671
+ if (ranksRaw !== void 0) if (!isRecord$3(ranksRaw)) onWarning("ranks must be an object");
1672
+ else {
1673
+ const out = {};
1674
+ for (const [key, value] of Object.entries(ranksRaw)) if (typeof value === "string" && value.trim() !== "") out[key] = value.trim().slice(0, 80);
1675
+ else onWarning("invalid rank name for " + key);
1676
+ if (Object.keys(out).length > 0) ranks = out;
1677
+ }
1678
+ if (overrides.status === void 0 && overrides.tools === void 0 && overrides.toolRemaining === void 0 && overrides.whispers === void 0 && panel === void 0 && remarks === void 0 && ranks === void 0) return;
1667
1679
  return {
1668
1680
  overrides,
1669
- ...panel === void 0 ? {} : { panel }
1681
+ ...panel === void 0 ? {} : { panel },
1682
+ ...remarks === void 0 ? {} : { remarks },
1683
+ ...ranks === void 0 ? {} : { ranks }
1670
1684
  };
1671
1685
  }
1672
1686
  /**
@@ -1679,6 +1693,8 @@ function mergeVoicePacks(...layers) {
1679
1693
  const overrides = {};
1680
1694
  const labels = {};
1681
1695
  const stats = {};
1696
+ let remarks;
1697
+ let ranks;
1682
1698
  let actions;
1683
1699
  let panelSeen = false;
1684
1700
  let any = false;
@@ -1704,6 +1720,14 @@ function mergeVoicePacks(...layers) {
1704
1720
  if (layer.panel.stats !== void 0) Object.assign(stats, layer.panel.stats);
1705
1721
  if (layer.panel.actions !== void 0) actions = layer.panel.actions;
1706
1722
  }
1723
+ if (layer.remarks !== void 0) remarks = {
1724
+ ...remarks ?? {},
1725
+ ...layer.remarks
1726
+ };
1727
+ if (layer.ranks !== void 0) ranks = {
1728
+ ...ranks ?? {},
1729
+ ...layer.ranks
1730
+ };
1707
1731
  }
1708
1732
  if (!any) return void 0;
1709
1733
  const panel = {
@@ -1714,7 +1738,9 @@ function mergeVoicePacks(...layers) {
1714
1738
  const panelEmpty = panel.labels === void 0 && panel.stats === void 0 && panel.actions === void 0;
1715
1739
  return {
1716
1740
  overrides,
1717
- ...panelSeen && !panelEmpty ? { panel } : {}
1741
+ ...panelSeen && !panelEmpty ? { panel } : {},
1742
+ ...remarks !== void 0 ? { remarks } : {},
1743
+ ...ranks !== void 0 ? { ranks } : {}
1718
1744
  };
1719
1745
  }
1720
1746
  //#endregion
@@ -4099,10 +4125,12 @@ var PetService = class extends Service {
4099
4125
  petId: this.registry.defaultEntry().id
4100
4126
  };
4101
4127
  const selected = this.registry.byId(persist.petId) ?? this.registry.defaultEntry();
4128
+ const voiceRemarks = mergeVoicePacks(this.registry.globalVoice, selected.voice)?.remarks;
4102
4129
  const ledgerConfig = {
4103
4130
  affinity: config.affinity,
4104
4131
  treats: config.treats,
4105
- remarks: selected.remarks
4132
+ remarks: selected.remarks,
4133
+ voiceRemarks
4106
4134
  };
4107
4135
  this.ledger = new PetLedger(persist, ledgerConfig);
4108
4136
  this.stateConfig = {
@@ -4186,7 +4214,8 @@ var PetService = class extends Service {
4186
4214
  error: "unknown-pet"
4187
4215
  };
4188
4216
  this.ledger.setPetId(entry.id);
4189
- this.ledger.setRemarks(entry.remarks);
4217
+ const voiceRemarks = mergeVoicePacks(this.registry.globalVoice, entry.voice)?.remarks;
4218
+ this.ledger.setRemarks(entry.remarks, voiceRemarks);
4190
4219
  this.flush();
4191
4220
  this.syncSettingsFromPet();
4192
4221
  return {
@@ -4691,23 +4720,33 @@ function isLoopbackRequest(request) {
4691
4720
  }
4692
4721
  }
4693
4722
  //#endregion
4694
- //#region src/access.ts
4723
+ //#region src/pair-access.ts
4695
4724
  /**
4696
- * Whether this request may enter any /api/pet or /pet asset route.
4725
+ * Whether this request may enter the plugin's host routes.
4697
4726
  * @param ctx - host context; may expose remoteWebUiPairing.
4698
4727
  * @param request - the incoming HTTP request.
4699
4728
  * @returns true for loopback, or a live paired-device cookie.
4700
4729
  */
4701
- function isPetAllowed(ctx, request) {
4730
+ function isPairedOrLoopbackAllowed(ctx, request) {
4702
4731
  if (isLoopbackRequest(request)) return true;
4703
- const bag = ctx;
4704
- const fromGet = typeof bag.get === "function" ? bag.get("remoteWebUiPairing", false) : void 0;
4705
- return (isPairingAccess(fromGet) ? fromGet : bag.remoteWebUiPairing)?.isPairedDevice(request) === true;
4732
+ const fromGet = typeof ctx.get === "function" ? ctx.get("remoteWebUiPairing", false) : void 0;
4733
+ return (isPairingAccess(fromGet) ? fromGet : ctx.remoteWebUiPairing)?.isPairedDevice(request) === true;
4706
4734
  }
4707
4735
  function isPairingAccess(value) {
4708
4736
  return value !== void 0 && value !== null && typeof value.isPairedDevice === "function";
4709
4737
  }
4710
4738
  //#endregion
4739
+ //#region src/access.ts
4740
+ /**
4741
+ * Whether this request may enter any /api/pet or /pet asset route.
4742
+ * @param ctx - host context; may expose remoteWebUiPairing.
4743
+ * @param request - the incoming HTTP request.
4744
+ * @returns true for loopback, or a live paired-device cookie.
4745
+ */
4746
+ function isPetAllowed(ctx, request) {
4747
+ return isPairedOrLoopbackAllowed(ctx, request);
4748
+ }
4749
+ //#endregion
4711
4750
  //#region src/http.ts
4712
4751
  /** Default body cap for readJsonBody: 64 KiB. */
4713
4752
  const DEFAULT_JSON_BODY_MAX_BYTES = 64 * 1024;
package/lib/invariant.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as AFFINITY_MAX, n as animationForPhase, o as AFFINITY_RANKS, u as defaultAffinityConfig } from "./state-DrMX22GL.js";
1
+ import { a as AFFINITY_MAX, n as animationForPhase, o as AFFINITY_RANKS, u as defaultAffinityConfig } from "./state-NvDEoCln.js";
2
2
  //#region src/invariant.ts
3
3
  /**
4
4
  * Package invariants — cheap structural checks run at import time on the
@@ -149,11 +149,14 @@ function normalizePetRemarks(raw, onWarning = () => {}) {
149
149
  var RemarkPicker = class {
150
150
  counters = /* @__PURE__ */ new Map();
151
151
  pools;
152
- constructor(overrides) {
152
+ constructor(overrides, voiceOverrides) {
153
153
  this.pools = {};
154
154
  for (const kind of REMARK_KINDS) {
155
155
  const custom = overrides?.[kind];
156
- this.pools[kind] = custom !== void 0 && custom.length > 0 ? custom : BUILTIN_REMARKS[kind];
156
+ const voice = voiceOverrides?.[kind];
157
+ if (custom !== void 0 && custom.length > 0) this.pools[kind] = custom;
158
+ else if (voice !== void 0 && voice.length > 0) this.pools[kind] = voice;
159
+ else this.pools[kind] = BUILTIN_REMARKS[kind];
157
160
  }
158
161
  }
159
162
  /** The effective pool for one slot (custom override or built-in). */
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Pet trust fence: loopback (the desktop) always passes; a live paired-device
3
- * cookie is an additional allow path when remote-web-ui is loaded. The pet
4
- * never depends on that plugin — without the service the fence stays
5
- * loopback-only (same pattern as skill-explorer / aionui-panel).
2
+ * Pet trust fence: loopback (the desktop) always passes; a live
3
+ * paired-device cookie is an additional allow path when remote-web-ui is
4
+ * loaded. The pet never depends on that plugin — without the service the
5
+ * fence stays loopback-only. The decision logic lives in the generated
6
+ * pair-access.ts copy (shared by git-graph / pet / skill-explorer).
6
7
  */
7
8
  import type { IncomingMessage } from 'node:http';
8
9
  import type { Context } from '@deepseek-ai/cordis';
@@ -1 +1 @@
1
- {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/access.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAclD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAM5E"}
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/access.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAE5E"}
@@ -1,4 +1,4 @@
1
- import { isLoopbackRequest } from "./loopback.js";
1
+ import { isPairedOrLoopbackAllowed } from "./pair-access.js";
2
2
  /**
3
3
  * Whether this request may enter any /api/pet or /pet asset route.
4
4
  * @param ctx - host context; may expose remoteWebUiPairing.
@@ -6,15 +6,5 @@ import { isLoopbackRequest } from "./loopback.js";
6
6
  * @returns true for loopback, or a live paired-device cookie.
7
7
  */
8
8
  export function isPetAllowed(ctx, request) {
9
- if (isLoopbackRequest(request))
10
- return true;
11
- const bag = ctx;
12
- const fromGet = typeof bag.get === 'function' ? bag.get('remoteWebUiPairing', false) : undefined;
13
- const pairing = (isPairingAccess(fromGet) ? fromGet : bag.remoteWebUiPairing);
14
- return pairing?.isPairedDevice(request) === true;
15
- }
16
- function isPairingAccess(value) {
17
- return value !== undefined
18
- && value !== null
19
- && typeof value.isPairedDevice === 'function';
9
+ return isPairedOrLoopbackAllowed(ctx, request);
20
10
  }
@@ -75,6 +75,8 @@ export declare const AFFINITY_RANKS: readonly [{
75
75
  readonly name: "鲸生共渡";
76
76
  readonly emoji: "*********";
77
77
  }];
78
+ /** English translations for the 9 built-in affinity rank names (#1226). */
79
+ export declare const AFFINITY_RANKS_EN: Readonly<Record<string, string>>;
78
80
  /** Interaction tuning (all in points / ms). */
79
81
  export interface AffinityConfig {
80
82
  /** Points per completed turn. */
@@ -1 +1 @@
1
- {"version":3,"file":"affinity.d.ts","sourceRoot":"","sources":["../../src/affinity.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,uDAAuD;AACvD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,CAAA;AAE3C,mCAAmC;AACnC,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAA;IACd,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAA;IACjB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAA;IAClB,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAA;IACnB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,YAAc,CAAA;AAEvC;;;;8EAI8E;AAC9E,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUjB,CAAA;AAEV,+CAA+C;AAC/C,MAAM,WAAW,cAAc;IAC7B,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAA;IACjB,iCAAiC;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,kCAAkC;IAClC,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,qBAAqB,EAAE,cAMnC,CAAA;AAED,wBAAgB,aAAa,IAAI,aAAa,CAW7C;AAED,kCAAkC;AAClC,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,QAAQ,EAAE,aAAa,CAAA;IACvB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAA;IAChB,iEAAiE;IACjE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,8BAA8B;AAC9B,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAMtE;AAED,iEAAiE;AACjE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAA;IACpB,kDAAkD;IAClD,YAAY,EAAE,OAAO,CAAA;CACtB;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAC5B,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,cAAsC,GAC7C,eAAe,CAYjB;AAYD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,cAAsC,GAC7C,kBAAkB,CA2CpB;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,MAAM,GAAE,cAAsC,GAC7C,aAAa,CAKf"}
1
+ {"version":3,"file":"affinity.d.ts","sourceRoot":"","sources":["../../src/affinity.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,uDAAuD;AACvD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,CAAA;AAE3C,mCAAmC;AACnC,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAA;IACd,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAA;IACjB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAA;IAClB,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAA;IACnB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,YAAc,CAAA;AAEvC;;;;8EAI8E;AAC9E,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUjB,CAAA;AAEV,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAU9D,CAAA;AAED,+CAA+C;AAC/C,MAAM,WAAW,cAAc;IAC7B,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAA;IACjB,iCAAiC;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,kCAAkC;IAClC,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,qBAAqB,EAAE,cAMnC,CAAA;AAED,wBAAgB,aAAa,IAAI,aAAa,CAW7C;AAED,kCAAkC;AAClC,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,QAAQ,EAAE,aAAa,CAAA;IACvB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAA;IAChB,iEAAiE;IACjE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,8BAA8B;AAC9B,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAMtE;AAED,iEAAiE;AACjE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAA;IACpB,kDAAkD;IAClD,YAAY,EAAE,OAAO,CAAA;CACtB;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAC5B,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,cAAsC,GAC7C,eAAe,CAYjB;AAYD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,cAAsC,GAC7C,kBAAkB,CA2CpB;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,MAAM,GAAE,cAAsC,GAC7C,aAAa,CAKf"}
@@ -29,6 +29,18 @@ export const AFFINITY_RANKS = [
29
29
  { min: 10_000, name: '永恒之契', emoji: '********' },
30
30
  { min: 100_000, name: '鲸生共渡', emoji: '*********' },
31
31
  ];
32
+ /** English translations for the 9 built-in affinity rank names (#1226). */
33
+ export const AFFINITY_RANKS_EN = {
34
+ '幼鲸': 'Baby Whale',
35
+ '伙伴': 'Companion',
36
+ '挚友': 'Close Friend',
37
+ '深海羁绊': 'Deep Sea Bond',
38
+ '心有灵犀': 'Kindred Spirit',
39
+ '传说羁绊': 'Legendary Bond',
40
+ '神话羁绊': 'Mythic Bond',
41
+ '永恒之契': 'Eternal Covenant',
42
+ '鲸生共渡': 'Lifelong Companion',
43
+ };
32
44
  export const defaultAffinityConfig = {
33
45
  turnReward: 1,
34
46
  petReward: 1,
@@ -1 +1 @@
1
- {"version":3,"file":"PetSprite.d.ts","sourceRoot":"","sources":["../../../src/client/PetSprite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAkE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAGnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAIjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,UAAU,EAAE,aAAa,CAAA;IACzB,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9C,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4DAA4D;IAC5D,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,SAAS,CAAA;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9D;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,qEAAqE;IACrE,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AAgGD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,CAqiB5D"}
1
+ {"version":3,"file":"PetSprite.d.ts","sourceRoot":"","sources":["../../../src/client/PetSprite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAkE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAGnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAIjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,UAAU,EAAE,aAAa,CAAA;IACzB,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9C,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4DAA4D;IAC5D,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,SAAS,CAAA;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9D;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,qEAAqE;IACrE,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AAgGD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,CA6iB5D"}
@@ -498,7 +498,13 @@ export function PetSprite(props) {
498
498
  props.onRename(trimmed);
499
499
  setRenaming(false);
500
500
  }
501
- }, children: panelLabel('confirm', props.t('pet.confirm')) })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.nameCell, children: displayName }), _jsx("span", { className: styles.statRank, children: panelStat('rank', 'pet.rank', { rank: snapshot?.affinity.rank ?? '?' }) })] }), _jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.statTreats, children: panelStat('treats', 'pet.treats', { n: snapshot?.treats.stocked ?? 0 }) }), _jsx("span", { className: styles.statPoints, children: panelStat('points', 'pet.points', { points: snapshot?.affinity.points ?? 0 }) })] }), _jsxs("div", { className: styles.actions, children: [panelShows('feed') && (_jsx("button", { type: "button", className: styles.action, onClick: props.onFeed, children: panelLabel('feed', props.t('pet.feed')) })), panelShows('rename') && (_jsx("button", { type: "button", className: styles.action, onClick: () => {
501
+ }, children: panelLabel('confirm', props.t('pet.confirm')) })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.nameCell, children: displayName }), _jsx("span", { className: styles.statRank, children: (() => {
502
+ const rawRank = snapshot?.affinity.rank ?? '?';
503
+ const rankKey = `pet.rank.name.${rawRank}`;
504
+ const localized = props.t(rankKey);
505
+ const rankName = localized !== rankKey ? localized : rawRank;
506
+ return panelStat('rank', 'pet.rank', { rank: rankName });
507
+ })() })] }), _jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.statTreats, children: panelStat('treats', 'pet.treats', { n: snapshot?.treats.stocked ?? 0 }) }), _jsx("span", { className: styles.statPoints, children: panelStat('points', 'pet.points', { points: snapshot?.affinity.points ?? 0 }) })] }), _jsxs("div", { className: styles.actions, children: [panelShows('feed') && (_jsx("button", { type: "button", className: styles.action, onClick: props.onFeed, children: panelLabel('feed', props.t('pet.feed')) })), panelShows('rename') && (_jsx("button", { type: "button", className: styles.action, onClick: () => {
502
508
  // Cancel any pending hide so the rename box cannot
503
509
  // unmount right as the user starts typing (#303).
504
510
  clearHideTimer();
@@ -13,6 +13,15 @@ export declare const zh: {
13
13
  readonly 'pet.namePlaceholder': "输入新名字";
14
14
  readonly 'pet.summon': "召唤{name}";
15
15
  readonly 'pet.rank': "亲密度 {rank}";
16
+ readonly 'pet.rank.name.\u5E7C\u9CB8': "幼鲸";
17
+ readonly 'pet.rank.name.\u4F19\u4F34': "伙伴";
18
+ readonly 'pet.rank.name.\u631A\u53CB': "挚友";
19
+ readonly 'pet.rank.name.\u6DF1\u6D77\u7F81\u7ECA': "深海羁绊";
20
+ readonly 'pet.rank.name.\u5FC3\u6709\u7075\u7280': "心有灵犀";
21
+ readonly 'pet.rank.name.\u4F20\u8BF4\u7F81\u7ECA': "传说羁绊";
22
+ readonly 'pet.rank.name.\u795E\u8BDD\u7F81\u7ECA': "神话羁绊";
23
+ readonly 'pet.rank.name.\u6C38\u6052\u4E4B\u5951': "永恒之契";
24
+ readonly 'pet.rank.name.\u9CB8\u751F\u5171\u6E21': "鲸生共渡";
16
25
  readonly 'pet.points': "{points} 点";
17
26
  readonly 'pet.treats': "小鱼干 ×{n}";
18
27
  readonly 'pet.state.loading': "宠物正在赶来…";
@@ -83,6 +92,15 @@ export declare const en: {
83
92
  readonly 'pet.namePlaceholder': "Enter a new name";
84
93
  readonly 'pet.summon': "Summon {name}";
85
94
  readonly 'pet.rank': "Affinity {rank}";
95
+ readonly 'pet.rank.name.\u5E7C\u9CB8': "Baby Whale";
96
+ readonly 'pet.rank.name.\u4F19\u4F34': "Companion";
97
+ readonly 'pet.rank.name.\u631A\u53CB': "Close Friend";
98
+ readonly 'pet.rank.name.\u6DF1\u6D77\u7F81\u7ECA': "Deep Sea Bond";
99
+ readonly 'pet.rank.name.\u5FC3\u6709\u7075\u7280': "Kindred Spirit";
100
+ readonly 'pet.rank.name.\u4F20\u8BF4\u7F81\u7ECA': "Legendary Bond";
101
+ readonly 'pet.rank.name.\u795E\u8BDD\u7F81\u7ECA': "Mythic Bond";
102
+ readonly 'pet.rank.name.\u6C38\u6052\u4E4B\u5951': "Eternal Covenant";
103
+ readonly 'pet.rank.name.\u9CB8\u751F\u5171\u6E21': "Lifelong Companion";
86
104
  readonly 'pet.points': "{points} pts";
87
105
  readonly 'pet.treats': "Treats ×{n}";
88
106
  readonly 'pet.state.loading': "The pet is on its way…";
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsEL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsEL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+EL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+EL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
@@ -13,6 +13,15 @@ export const zh = {
13
13
  'pet.namePlaceholder': '输入新名字',
14
14
  'pet.summon': '召唤{name}',
15
15
  'pet.rank': '亲密度 {rank}',
16
+ 'pet.rank.name.幼鲸': '幼鲸',
17
+ 'pet.rank.name.伙伴': '伙伴',
18
+ 'pet.rank.name.挚友': '挚友',
19
+ 'pet.rank.name.深海羁绊': '深海羁绊',
20
+ 'pet.rank.name.心有灵犀': '心有灵犀',
21
+ 'pet.rank.name.传说羁绊': '传说羁绊',
22
+ 'pet.rank.name.神话羁绊': '神话羁绊',
23
+ 'pet.rank.name.永恒之契': '永恒之契',
24
+ 'pet.rank.name.鲸生共渡': '鲸生共渡',
16
25
  'pet.points': '{points} 点',
17
26
  'pet.treats': '小鱼干 ×{n}',
18
27
  'pet.state.loading': '宠物正在赶来…',
@@ -85,6 +94,15 @@ export const en = {
85
94
  'pet.namePlaceholder': 'Enter a new name',
86
95
  'pet.summon': 'Summon {name}',
87
96
  'pet.rank': 'Affinity {rank}',
97
+ 'pet.rank.name.幼鲸': 'Baby Whale',
98
+ 'pet.rank.name.伙伴': 'Companion',
99
+ 'pet.rank.name.挚友': 'Close Friend',
100
+ 'pet.rank.name.深海羁绊': 'Deep Sea Bond',
101
+ 'pet.rank.name.心有灵犀': 'Kindred Spirit',
102
+ 'pet.rank.name.传说羁绊': 'Legendary Bond',
103
+ 'pet.rank.name.神话羁绊': 'Mythic Bond',
104
+ 'pet.rank.name.永恒之契': 'Eternal Covenant',
105
+ 'pet.rank.name.鲸生共渡': 'Lifelong Companion',
88
106
  'pet.points': '{points} pts',
89
107
  'pet.treats': 'Treats ×{n}',
90
108
  'pet.state.loading': 'The pet is on its way…',
@@ -8,6 +8,18 @@
8
8
  * phase-mapped track. Rendering never throws: a broken track list degrades
9
9
  * to the first decodable frame, and the 1.2 s stall watchdog re-kicks the
10
10
  * playback chain after timer throttling.
11
+ *
12
+ * Frame presentation has two modes picked once at mount by capability
13
+ * probing:
14
+ * - Canvas bitmap buffer (default where createImageBitmap/fetch/2D context
15
+ * exist): every frame is decoded exactly once into an ImageBitmap during
16
+ * the warm pass and drawn onto one <canvas> - steady-state playback issues
17
+ * zero DOM mutations and zero re-decodes (measured hotspot: swapping
18
+ * <img>.src per frame drove image decode + invalidation every tick).
19
+ * - Classic <img> fallback (jsdom/tests or missing APIs): identical to the
20
+ * historical behavior - cache-warm Image elements plus guarded src swaps,
21
+ * so environments without modern decoding keep working unchanged.
22
+ *
11
23
  * @module @linxin666/dsh-pet/client/renderers/frames2d
12
24
  */
13
25
  import { type PetRenderer, type PetRendererHandle } from '../../contracts/renderer.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"frames2d.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/frames2d.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAA4B,KAAK,WAAW,EAA2B,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AACzI,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IAC3C,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAClE;AAED,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IACzC,2DAA2D;IAC3D,YAAY,IAAI,MAAM,CAAA;CACvB;AAoCD,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,iBAAiB,CAgJ3D,CAAA"}
1
+ {"version":3,"file":"frames2d.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/frames2d.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAA4B,KAAK,WAAW,EAA2B,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AACzI,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IAC3C,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAClE;AAED,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IACzC,2DAA2D;IAC3D,YAAY,IAAI,MAAM,CAAA;CACvB;AA0CD,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,iBAAiB,CAsP3D,CAAA"}
@@ -8,6 +8,18 @@
8
8
  * phase-mapped track. Rendering never throws: a broken track list degrades
9
9
  * to the first decodable frame, and the 1.2 s stall watchdog re-kicks the
10
10
  * playback chain after timer throttling.
11
+ *
12
+ * Frame presentation has two modes picked once at mount by capability
13
+ * probing:
14
+ * - Canvas bitmap buffer (default where createImageBitmap/fetch/2D context
15
+ * exist): every frame is decoded exactly once into an ImageBitmap during
16
+ * the warm pass and drawn onto one <canvas> - steady-state playback issues
17
+ * zero DOM mutations and zero re-decodes (measured hotspot: swapping
18
+ * <img>.src per frame drove image decode + invalidation every tick).
19
+ * - Classic <img> fallback (jsdom/tests or missing APIs): identical to the
20
+ * historical behavior - cache-warm Image elements plus guarded src swaps,
21
+ * so environments without modern decoding keep working unchanged.
22
+ *
11
23
  * @module @linxin666/dsh-pet/client/renderers/frames2d
12
24
  */
13
25
  import { PET_RENDERER_API_VERSION } from "../../contracts/renderer.js";
@@ -50,23 +62,83 @@ export const frames2dRenderer = {
50
62
  const reducedMotion = typeof window !== 'undefined'
51
63
  && typeof window.matchMedia === 'function'
52
64
  && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
53
- const img = document.createElement('img');
54
- img.dataset.dshPetFrames2d = ctx.petId;
55
- img.alt = '';
56
- img.draggable = false;
57
- img.style.width = '100%';
58
- img.style.height = '100%';
59
- img.style.objectFit = 'contain';
60
- img.style.pointerEvents = 'none';
61
- ctx.container.appendChild(img);
62
- // Warm the browser cache for every frame up front (same-origin, small
63
- // webp files); playback itself swaps img.src without React state.
64
- for (const track of Object.values(config.tracks)) {
65
- for (const url of track.frames) {
66
- const pre = new Image();
67
- pre.src = url;
65
+ // Capability probe: the canvas path needs decode-to-bitmap, same-origin
66
+ // fetch, and a real 2D context (jsdom returns null and falls back below).
67
+ let canvas = null;
68
+ let context2d = null;
69
+ let img = null;
70
+ try {
71
+ if (typeof createImageBitmap === 'function' && typeof fetch === 'function') {
72
+ const probe = document.createElement('canvas');
73
+ const c2d = probe.getContext('2d');
74
+ if (c2d !== null) {
75
+ canvas = probe;
76
+ context2d = c2d;
77
+ }
68
78
  }
69
79
  }
80
+ catch {
81
+ canvas = null;
82
+ context2d = null;
83
+ }
84
+ if (canvas !== null && context2d !== null) {
85
+ canvas.dataset.dshPetFrames2d = ctx.petId;
86
+ canvas.draggable = false;
87
+ canvas.style.width = '100%';
88
+ canvas.style.height = '100%';
89
+ canvas.style.objectFit = 'contain';
90
+ canvas.style.pointerEvents = 'none';
91
+ ctx.container.appendChild(canvas);
92
+ }
93
+ else {
94
+ img = document.createElement('img');
95
+ img.dataset.dshPetFrames2d = ctx.petId;
96
+ img.alt = '';
97
+ img.draggable = false;
98
+ img.style.width = '100%';
99
+ img.style.height = '100%';
100
+ img.style.objectFit = 'contain';
101
+ img.style.pointerEvents = 'none';
102
+ ctx.container.appendChild(img);
103
+ }
104
+ // Decode cache: one concurrent decode per URL, memoized forever (frames
105
+ // are tiny same-origin webp files; failures resolve undefined and keep
106
+ // the last painted frame instead of breaking playback).
107
+ const decoding = new Map();
108
+ const decodedAll = [];
109
+ const loadFrame = (url) => {
110
+ const cached = decoding.get(url);
111
+ if (cached !== undefined)
112
+ return cached;
113
+ const job = (async () => {
114
+ try {
115
+ const response = await fetch(url);
116
+ if (!response.ok)
117
+ throw new Error('http ' + response.status);
118
+ const bitmap = await createImageBitmap(await response.blob());
119
+ return { source: bitmap, width: bitmap.width, height: bitmap.height };
120
+ }
121
+ catch {
122
+ // Fail-open: classic Image decode keeps non-modern runtimes alive.
123
+ return await new Promise((resolve) => {
124
+ try {
125
+ const pre = new Image();
126
+ pre.onload = () => {
127
+ resolve(pre.naturalWidth > 0 ? { source: pre, width: pre.naturalWidth, height: pre.naturalHeight } : undefined);
128
+ };
129
+ pre.onerror = () => resolve(undefined);
130
+ pre.src = url;
131
+ }
132
+ catch {
133
+ resolve(undefined);
134
+ }
135
+ });
136
+ }
137
+ })();
138
+ decoding.set(url, job);
139
+ decodedAll.push(job.then(() => undefined, () => undefined));
140
+ return job;
141
+ };
70
142
  let disposed = false;
71
143
  let timer;
72
144
  let watchdog;
@@ -74,15 +146,45 @@ export const frames2dRenderer = {
74
146
  let frameIndex = 0;
75
147
  let lastAdvance = Date.now();
76
148
  let override;
149
+ let drawToken = 0;
150
+ let lastDrawnUrl;
77
151
  const trackForPhase = (phase) => {
78
152
  const mapped = config.phases[phase];
79
153
  return mapped !== undefined && config.tracks[mapped] !== undefined ? mapped : config.phases.idle;
80
154
  };
155
+ /** Canvas path: paints the newest requested frame; stale draws drop out. */
156
+ const paintCanvas = (url) => {
157
+ if (context2d === null || canvas === null)
158
+ return;
159
+ const myToken = ++drawToken;
160
+ void loadFrame(url).then((frame) => {
161
+ if (disposed || frame === undefined || myToken !== drawToken)
162
+ return;
163
+ if (lastDrawnUrl === url)
164
+ return;
165
+ lastDrawnUrl = url;
166
+ // Resizing clears the canvas, so size only when it actually differs.
167
+ if (canvas.width !== frame.width || canvas.height !== frame.height) {
168
+ canvas.width = frame.width;
169
+ canvas.height = frame.height;
170
+ }
171
+ else {
172
+ context2d.clearRect(0, 0, canvas.width, canvas.height);
173
+ }
174
+ context2d.drawImage(frame.source, 0, 0);
175
+ }).catch(() => { });
176
+ };
81
177
  const show = (trackId, index) => {
82
178
  const def = config.tracks[trackId];
83
179
  const url = def?.frames[index];
84
- if (url !== undefined && img.getAttribute('src') !== url)
85
- img.src = url;
180
+ if (url === undefined)
181
+ return;
182
+ if (img !== null) {
183
+ if (img.getAttribute('src') !== url)
184
+ img.src = url;
185
+ return;
186
+ }
187
+ paintCanvas(url);
86
188
  };
87
189
  const schedule = (ms) => {
88
190
  if (disposed)
@@ -155,6 +257,13 @@ export const frames2dRenderer = {
155
257
  tick();
156
258
  }, WATCHDOG_MS);
157
259
  }
260
+ // Warm pass: decode every frame up front (tiny same-origin webp files)
261
+ // so loops and phase switches never wait on a first decode - same intent
262
+ // as the historical Image-cache warm loop, now feeding the decode cache.
263
+ for (const warmTrack of Object.values(config.tracks)) {
264
+ for (const warmUrl of warmTrack.frames)
265
+ void loadFrame(warmUrl);
266
+ }
158
267
  play(track);
159
268
  let disposedOnce = false;
160
269
  const dispose = () => {
@@ -167,7 +276,23 @@ export const frames2dRenderer = {
167
276
  clearTimeout(timer);
168
277
  if (watchdog !== undefined)
169
278
  clearInterval(watchdog);
170
- img.remove();
279
+ // Release decoded bitmaps after pending decodes settle; close() is
280
+ // browser-only, so guard it for exotic hosts.
281
+ void Promise.allSettled(decodedAll).then(() => {
282
+ for (const job of decoding.values()) {
283
+ void job.then((frame) => {
284
+ try {
285
+ const maybeClose = frame?.source?.close;
286
+ if (typeof maybeClose === 'function' && frame !== undefined)
287
+ maybeClose.call(frame.source);
288
+ }
289
+ catch { /* already released */ }
290
+ }).catch(() => { });
291
+ }
292
+ decoding.clear();
293
+ });
294
+ canvas?.remove();
295
+ img?.remove();
171
296
  };
172
297
  ctx.onCleanup(dispose);
173
298
  return {
@@ -17,6 +17,8 @@ export interface LedgerConfig {
17
17
  treats?: Partial<TreatConfig>;
18
18
  /** Per-pet remark pools for the selected pet (custom slots override built-ins). */
19
19
  remarks?: PetRemarks;
20
+ /** Voice-pack fallback remark pools (global or pet voice pack). */
21
+ voiceRemarks?: PetRemarks;
20
22
  }
21
23
  /** Result of one ledger interaction (the shape the pet RPC returns). */
22
24
  export interface LedgerInteractionResult {
@@ -66,9 +68,9 @@ export declare class PetLedger {
66
68
  setPetName(petId: string, name: string): void;
67
69
  /**
68
70
  * Swap the reaction pools to another pet's custom remarks (called on pet
69
- * selection). Slots the pet does not declare fall back to built-ins.
71
+ * selection). Slots the pet does not declare fall back to voice packs or built-ins.
70
72
  */
71
- setRemarks(remarks?: PetRemarks): void;
73
+ setRemarks(remarks?: PetRemarks, voiceRemarks?: PetRemarks): void;
72
74
  /**
73
75
  * Settle the treat economy (work + time output since the last settlement).
74
76
  * 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,EAAgB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAErD,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7B,mFAAmF;IACnF,OAAO,CAAC,EAAE,UAAU,CAAA;CACrB;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,0EAA0E;IAC1E,OAAO,CAAC,MAAM,CAAc;IAC5B,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;IAO1D,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;;;OAGG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAItC,mEAAmE;IACnE,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAK3C,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM7B,qFAAqF;IACrF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IAK5D,0EAA0E;IAC1E,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAK7C;;;OAGG;IACH,UAAU,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI;IAItC;;;;;OAKG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAapC;;;;;OAKG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IASpC,8EAA8E;IAC9E,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE;IAQ5C;;;;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;IAwCtE,kDAAkD;IAClD,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe;CAG7C"}
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/ledger.ts"],"names":[],"mappings":"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"}