@linxin666/dsh-pet 0.2.3 → 0.2.4

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 (70) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +64 -1
  3. package/README.zh.md +64 -1
  4. package/assets/decorations/whale/decoration.json +20 -0
  5. package/assets/decorations/whale/whale-frames.png +0 -0
  6. package/contracts/pet-manifest-v2.schema.json +281 -0
  7. package/contracts/status-decoration-v1.schema.json +144 -0
  8. package/contracts/voice-pack-v1.schema.json +234 -0
  9. package/lib/client.js +144 -18
  10. package/lib/client.js.map +1 -1
  11. package/lib/index.js +979 -70
  12. package/lib/types/chatter.d.ts +61 -3
  13. package/lib/types/chatter.d.ts.map +1 -1
  14. package/lib/types/chatter.js +71 -12
  15. package/lib/types/client/PetSettingsCard.d.ts +4 -0
  16. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  17. package/lib/types/client/PetSettingsCard.js +3 -1
  18. package/lib/types/client/PetSprite.d.ts.map +1 -1
  19. package/lib/types/client/PetSprite.js +103 -4
  20. package/lib/types/client/locales.d.ts +4 -0
  21. package/lib/types/client/locales.d.ts.map +1 -1
  22. package/lib/types/client/locales.js +4 -0
  23. package/lib/types/client/renderers/live2d.d.ts.map +1 -1
  24. package/lib/types/client/renderers/live2d.js +13 -1
  25. package/lib/types/client/settings-form.d.ts.map +1 -1
  26. package/lib/types/client/settings-form.js +9 -6
  27. package/lib/types/contracts/status-decoration.d.ts +85 -0
  28. package/lib/types/contracts/status-decoration.d.ts.map +1 -0
  29. package/lib/types/contracts/status-decoration.js +21 -0
  30. package/lib/types/decoration.d.ts +39 -0
  31. package/lib/types/decoration.d.ts.map +1 -0
  32. package/lib/types/decoration.js +210 -0
  33. package/lib/types/event-projection.d.ts +8 -3
  34. package/lib/types/event-projection.d.ts.map +1 -1
  35. package/lib/types/event-projection.js +9 -4
  36. package/lib/types/index.d.ts +2 -0
  37. package/lib/types/index.d.ts.map +1 -1
  38. package/lib/types/index.js +2 -0
  39. package/lib/types/registry.d.ts +55 -2
  40. package/lib/types/registry.d.ts.map +1 -1
  41. package/lib/types/registry.js +198 -16
  42. package/lib/types/routes.d.ts.map +1 -1
  43. package/lib/types/routes.js +131 -3
  44. package/lib/types/service.d.ts +33 -0
  45. package/lib/types/service.d.ts.map +1 -1
  46. package/lib/types/service.js +42 -3
  47. package/lib/types/voice-pack.d.ts +98 -0
  48. package/lib/types/voice-pack.d.ts.map +1 -0
  49. package/lib/types/voice-pack.js +384 -0
  50. package/package.json +11 -10
  51. package/src/chatter.test.ts +89 -2
  52. package/src/chatter.ts +120 -14
  53. package/src/client/PetSettingsCard.tsx +18 -0
  54. package/src/client/PetSprite.test.tsx +254 -12
  55. package/src/client/PetSprite.tsx +142 -25
  56. package/src/client/locales.ts +4 -0
  57. package/src/client/renderers/live2d.test.ts +23 -2
  58. package/src/client/renderers/live2d.ts +14 -1
  59. package/src/client/settings-form.ts +8 -6
  60. package/src/contracts/status-decoration.ts +78 -0
  61. package/src/decoration.test.ts +178 -0
  62. package/src/decoration.ts +220 -0
  63. package/src/event-projection.ts +10 -5
  64. package/src/index.ts +2 -0
  65. package/src/registry.test.ts +223 -0
  66. package/src/registry.ts +235 -15
  67. package/src/routes.ts +128 -3
  68. package/src/service.ts +59 -3
  69. package/src/voice-pack.test.ts +216 -0
  70. package/src/voice-pack.ts +413 -0
@@ -0,0 +1,234 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://schemas.linxin666.org/dsh-pet/voice-pack-v1.schema.json",
4
+ "title": "DSH Pet Voice Pack v1",
5
+ "description": "Pet-center voice pack (issue #677). Optional voice.json inside a pet directory, or the global $DSH_HOME/pets/.voice.json override — pure JSON content layering status / tool / whisper copy over the built-in pools, plus hover-panel chrome. Structure is fail-closed per file (a non-object root drops the pack); content is warn-and-drop per slot.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string"
11
+ },
12
+ "voicePackVersion": {
13
+ "const": 1,
14
+ "description": "Optional; absent reads as v1."
15
+ },
16
+ "status": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "description": "Status copy pools keyed by the eleven StatusScene ids; each key replaces that scene's built-in pool.",
20
+ "properties": {
21
+ "prepare": {
22
+ "$ref": "#/definitions/pool"
23
+ },
24
+ "waiting": {
25
+ "$ref": "#/definitions/pool"
26
+ },
27
+ "thinking": {
28
+ "$ref": "#/definitions/pool"
29
+ },
30
+ "review": {
31
+ "$ref": "#/definitions/pool"
32
+ },
33
+ "toolResult": {
34
+ "$ref": "#/definitions/pool"
35
+ },
36
+ "done": {
37
+ "$ref": "#/definitions/pool"
38
+ },
39
+ "failed": {
40
+ "$ref": "#/definitions/pool"
41
+ },
42
+ "toolFailed": {
43
+ "$ref": "#/definitions/pool"
44
+ },
45
+ "maxTokens": {
46
+ "$ref": "#/definitions/pool"
47
+ },
48
+ "interrupted": {
49
+ "$ref": "#/definitions/pool"
50
+ },
51
+ "blocked": {
52
+ "$ref": "#/definitions/pool"
53
+ }
54
+ }
55
+ },
56
+ "tools": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "description": "Tool copy pools keyed by the seventeen ToolCategory ids; {tool} and {hint} interpolate.",
60
+ "properties": {
61
+ "read": {
62
+ "$ref": "#/definitions/pool"
63
+ },
64
+ "write": {
65
+ "$ref": "#/definitions/pool"
66
+ },
67
+ "edit": {
68
+ "$ref": "#/definitions/pool"
69
+ },
70
+ "shell": {
71
+ "$ref": "#/definitions/pool"
72
+ },
73
+ "grep": {
74
+ "$ref": "#/definitions/pool"
75
+ },
76
+ "find": {
77
+ "$ref": "#/definitions/pool"
78
+ },
79
+ "ls": {
80
+ "$ref": "#/definitions/pool"
81
+ },
82
+ "webSearch": {
83
+ "$ref": "#/definitions/pool"
84
+ },
85
+ "webFetch": {
86
+ "$ref": "#/definitions/pool"
87
+ },
88
+ "mcp": {
89
+ "$ref": "#/definitions/pool"
90
+ },
91
+ "memory": {
92
+ "$ref": "#/definitions/pool"
93
+ },
94
+ "subagent": {
95
+ "$ref": "#/definitions/pool"
96
+ },
97
+ "todo": {
98
+ "$ref": "#/definitions/pool"
99
+ },
100
+ "browser": {
101
+ "$ref": "#/definitions/pool"
102
+ },
103
+ "git": {
104
+ "$ref": "#/definitions/pool"
105
+ },
106
+ "ask": {
107
+ "$ref": "#/definitions/pool"
108
+ },
109
+ "generic": {
110
+ "$ref": "#/definitions/pool"
111
+ }
112
+ }
113
+ },
114
+ "toolRemaining": {
115
+ "$ref": "#/definitions/pool",
116
+ "description": "The parallel-tools count line pool; {n} interpolates the count."
117
+ },
118
+ "whispers": {
119
+ "type": "object",
120
+ "additionalProperties": false,
121
+ "description": "Murmur pools; each section replaces the built-in one as a whole. An explicit empty array mutes that channel.",
122
+ "properties": {
123
+ "generic": {
124
+ "$ref": "#/definitions/pool"
125
+ },
126
+ "rules": {
127
+ "type": "array",
128
+ "maxItems": 32,
129
+ "items": {
130
+ "type": "object",
131
+ "additionalProperties": false,
132
+ "required": [
133
+ "keywords",
134
+ "pool"
135
+ ],
136
+ "properties": {
137
+ "keywords": {
138
+ "type": "array",
139
+ "maxItems": 16,
140
+ "items": {
141
+ "type": "string",
142
+ "maxLength": 40,
143
+ "minLength": 1
144
+ },
145
+ "description": "Lowercase substrings matched against the model's output."
146
+ },
147
+ "pool": {
148
+ "$ref": "#/definitions/pool"
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ },
155
+ "panel": {
156
+ "type": "object",
157
+ "additionalProperties": false,
158
+ "description": "Hover-panel chrome; unset slots keep the client's i18n dictionary copy.",
159
+ "properties": {
160
+ "labels": {
161
+ "type": "object",
162
+ "additionalProperties": false,
163
+ "properties": {
164
+ "feed": {
165
+ "type": "string",
166
+ "maxLength": 40
167
+ },
168
+ "rename": {
169
+ "type": "string",
170
+ "maxLength": 40
171
+ },
172
+ "hide": {
173
+ "type": "string",
174
+ "maxLength": 40
175
+ },
176
+ "confirm": {
177
+ "type": "string",
178
+ "maxLength": 40
179
+ }
180
+ }
181
+ },
182
+ "stats": {
183
+ "type": "object",
184
+ "additionalProperties": false,
185
+ "description": "{rank} / {n} / {points} interpolate the live values.",
186
+ "properties": {
187
+ "rank": {
188
+ "type": "string",
189
+ "maxLength": 80
190
+ },
191
+ "treats": {
192
+ "type": "string",
193
+ "maxLength": 80
194
+ },
195
+ "points": {
196
+ "type": "string",
197
+ "maxLength": 80
198
+ }
199
+ }
200
+ },
201
+ "actions": {
202
+ "type": "array",
203
+ "uniqueItems": true,
204
+ "items": {
205
+ "enum": [
206
+ "feed",
207
+ "rename",
208
+ "hide"
209
+ ]
210
+ },
211
+ "description": "Subset of actions to render in canonical order; absent = all three, [] = none."
212
+ }
213
+ }
214
+ }
215
+ },
216
+ "definitions": {
217
+ "pool": {
218
+ "oneOf": [
219
+ {
220
+ "type": "string",
221
+ "maxLength": 160
222
+ },
223
+ {
224
+ "type": "array",
225
+ "maxItems": 64,
226
+ "items": {
227
+ "type": "string",
228
+ "maxLength": 160
229
+ }
230
+ }
231
+ ]
232
+ }
233
+ }
234
+ }
package/lib/client.js CHANGED
@@ -220,6 +220,78 @@ window.__ModuleLoader__.load({
220
220
  return Math.max(0, Math.min(max, value));
221
221
  }
222
222
  /**
223
+ * The status decoration ornament (pet-center M5, #567). Renders the active
224
+ * phase's frame segment as a CSS-background strip at a compact bubble
225
+ * height; prefers-reduced-motion holds the segment's first frame, and a
226
+ * missing or undecodable asset simply paints nothing (CSS background
227
+ * failure) — the bubble text is never disturbed. The span is aria-hidden;
228
+ * the bubble keeps its own semantics untouched.
229
+ */
230
+ function StatusOrnament(props) {
231
+ const { decoration, phase } = props;
232
+ const segment = decoration.phases[phase];
233
+ const shown = segment !== void 0 && segment !== "hide";
234
+ const segmentKey = segment !== void 0 && segment !== "hide" ? segment.from + ":" + segment.to : "none";
235
+ const spanRef = (0, react.useRef)(null);
236
+ const scale = 18 / decoration.cell.height;
237
+ const frameWidth = Math.round(decoration.cell.width * scale);
238
+ const stripWidth = decoration.columns * frameWidth;
239
+ const durationsKey = decoration.durations.join(",");
240
+ (0, react.useEffect)(() => {
241
+ if (segment === void 0 || segment === "hide") return;
242
+ const el = spanRef.current;
243
+ if (el === null) return;
244
+ const position = (index) => -index * frameWidth + "px 0px";
245
+ const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
246
+ el.style.backgroundPosition = position(segment.from);
247
+ if (reduceMotion) return;
248
+ let raf = 0;
249
+ let index = segment.from;
250
+ let elapsed = 0;
251
+ let last = performance.now();
252
+ const tick = (ts) => {
253
+ const delta = ts - last;
254
+ last = ts;
255
+ elapsed += delta;
256
+ const duration = decoration.durations[index] ?? 160;
257
+ if (elapsed >= duration) {
258
+ elapsed = 0;
259
+ if (index < segment.to) index += 1;
260
+ else if (decoration.loop) index = segment.from;
261
+ }
262
+ el.style.backgroundPosition = position(index);
263
+ if (!decoration.loop && index === segment.to) return;
264
+ raf = requestAnimationFrame(tick);
265
+ };
266
+ raf = requestAnimationFrame(tick);
267
+ return () => cancelAnimationFrame(raf);
268
+ }, [
269
+ shown,
270
+ segmentKey,
271
+ frameWidth,
272
+ decoration.loop,
273
+ durationsKey
274
+ ]);
275
+ if (!shown) return null;
276
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
277
+ ref: spanRef,
278
+ "aria-hidden": "true",
279
+ "data-dsh-pet-decoration": decoration.id,
280
+ style: {
281
+ display: "inline-block",
282
+ width: frameWidth,
283
+ height: 18,
284
+ marginRight: 6,
285
+ verticalAlign: "middle",
286
+ flexShrink: 0,
287
+ backgroundImage: "url(" + decoration.entryUrl + ")",
288
+ backgroundSize: stripWidth + "px 18px",
289
+ backgroundRepeat: "no-repeat",
290
+ backgroundPosition: "0px 0px"
291
+ }
292
+ });
293
+ }
294
+ /**
223
295
  * The floating pet. The spritesheet frame advances on requestAnimationFrame
224
296
  * with per-frame durations from the definition's tracks; the atlas image is
225
297
  * loaded once and the background position is written straight to the sprite
@@ -253,6 +325,21 @@ window.__ModuleLoader__.load({
253
325
  const rows = definition.rows;
254
326
  const tracks = definition.tracks;
255
327
  const sequences = definition.sequences;
328
+ const panel = definition.panel;
329
+ const panelLabel = (slot, i18n) => panel?.labels?.[slot] ?? i18n;
330
+ const panelStat = (slot, i18nKey, values) => {
331
+ const format = panel?.stats?.[slot] ?? props.t(i18nKey, values);
332
+ if (panel?.stats?.[slot] === void 0) return format;
333
+ const all = {
334
+ rank: snapshot?.affinity.rank ?? "?",
335
+ n: snapshot?.treats.stocked ?? 0,
336
+ points: snapshot?.affinity.points ?? 0
337
+ };
338
+ let text = format;
339
+ for (const [name, value] of Object.entries(all)) text = text.replaceAll("{" + name + "}", String(value));
340
+ return text;
341
+ };
342
+ const panelShows = (action) => panel?.actions === void 0 || panel.actions.includes(action);
256
343
  (0, react.useEffect)(() => {
257
344
  if (props.visual !== void 0) return;
258
345
  let cancelled = false;
@@ -392,6 +479,7 @@ window.__ModuleLoader__.load({
392
479
  const whisper = feedback === null ? snapshot?.whisper : void 0;
393
480
  const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== void 0 || whisper !== void 0;
394
481
  const displayName = snapshot?.name ?? definition.displayName;
482
+ const decoration = snapshot?.decoration;
395
483
  (0, react.useEffect)(() => {
396
484
  if (sessionBubbles.length <= 1) setStackPinned(false);
397
485
  }, [sessionBubbles.length]);
@@ -481,14 +569,17 @@ window.__ModuleLoader__.load({
481
569
  onPointerLeave: () => setStackPeek(false),
482
570
  children: [visibleSessions.map((session, index) => {
483
571
  const speaksWhisper = index === 0 && whisper !== void 0;
484
- const bubble = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
572
+ const bubble = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
485
573
  type: "button",
486
574
  className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, pet_module_css_default.bubbleClickable, speaksWhisper && pet_module_css_default.bubbleWhisper),
487
575
  title: props.t("pet.openSessionHint"),
488
576
  onClick: () => {
489
577
  props.onOpenSession(session.sessionId);
490
578
  },
491
- children: speaksWhisper ? whisper : session.bubble
579
+ children: [index === 0 && !speaksWhisper && decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
580
+ decoration,
581
+ phase
582
+ }), speaksWhisper ? whisper : session.bubble]
492
583
  }, speaksWhisper ? "whisper:" + whisper : session.sessionId);
493
584
  if (index !== 0 || sessionBubbles.length <= 1) return bubble;
494
585
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -506,11 +597,14 @@ window.__ModuleLoader__.load({
506
597
  children: stackOpen ? "×" : "+" + String(sessionBubbles.length - 1)
507
598
  })]
508
599
  }, "primary");
509
- }), sessionBubbles.length === 0 && (statusBubble !== void 0 || whisper !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
600
+ }), sessionBubbles.length === 0 && (statusBubble !== void 0 || whisper !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
510
601
  className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, whisper !== void 0 && pet_module_css_default.bubbleWhisper),
511
602
  role: "status",
512
603
  "aria-live": "polite",
513
- children: whisper ?? statusBubble
604
+ children: [whisper === void 0 && decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
605
+ decoration,
606
+ phase
607
+ }), whisper ?? statusBubble]
514
608
  }, whisper === void 0 ? "status" : "whisper:" + whisper)]
515
609
  }),
516
610
  hovered && dragRef.current === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -556,7 +650,7 @@ window.__ModuleLoader__.load({
556
650
  setRenaming(false);
557
651
  }
558
652
  },
559
- children: props.t("pet.confirm")
653
+ children: panelLabel("confirm", props.t("pet.confirm"))
560
654
  })]
561
655
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
562
656
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -566,29 +660,29 @@ window.__ModuleLoader__.load({
566
660
  children: displayName
567
661
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
568
662
  className: pet_module_css_default.statRank,
569
- children: props.t("pet.rank", { rank: snapshot?.affinity.rank ?? "?" })
663
+ children: panelStat("rank", "pet.rank", { rank: snapshot?.affinity.rank ?? "?" })
570
664
  })]
571
665
  }),
572
666
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
573
667
  className: pet_module_css_default.rankRow,
574
668
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
575
669
  className: pet_module_css_default.statTreats,
576
- children: props.t("pet.treats", { n: snapshot?.treats.stocked ?? 0 })
670
+ children: panelStat("treats", "pet.treats", { n: snapshot?.treats.stocked ?? 0 })
577
671
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
578
672
  className: pet_module_css_default.statPoints,
579
- children: props.t("pet.points", { points: snapshot?.affinity.points ?? 0 })
673
+ children: panelStat("points", "pet.points", { points: snapshot?.affinity.points ?? 0 })
580
674
  })]
581
675
  }),
582
676
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
583
677
  className: pet_module_css_default.actions,
584
678
  children: [
585
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
679
+ panelShows("feed") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
586
680
  type: "button",
587
681
  className: pet_module_css_default.action,
588
682
  onClick: props.onFeed,
589
- children: props.t("pet.feed")
683
+ children: panelLabel("feed", props.t("pet.feed"))
590
684
  }),
591
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
685
+ panelShows("rename") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
592
686
  type: "button",
593
687
  className: pet_module_css_default.action,
594
688
  onClick: () => {
@@ -596,13 +690,13 @@ window.__ModuleLoader__.load({
596
690
  setNameDraft(displayName);
597
691
  setRenaming(true);
598
692
  },
599
- children: props.t("pet.rename")
693
+ children: panelLabel("rename", props.t("pet.rename"))
600
694
  }),
601
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
695
+ panelShows("hide") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
602
696
  type: "button",
603
697
  className: pet_module_css_default.action,
604
698
  onClick: props.onHide,
605
- children: props.t("pet.hide")
699
+ children: panelLabel("hide", props.t("pet.hide"))
606
700
  })
607
701
  ]
608
702
  })
@@ -917,6 +1011,14 @@ window.__ModuleLoader__.load({
917
1011
  //#region src/client/renderers/live2d.ts
918
1012
  /** The de-facto tap-motion group of Cubism sample models. */
919
1013
  const TAP_GROUP = "TapBody";
1014
+ /**
1015
+ * Keep one screen-appropriate atlas LOD instead of asking Pixi for the
1016
+ * engine's default full mip chain. A user model can legitimately carry an
1017
+ * 8192px texture while the pet itself is only a few hundred pixels tall;
1018
+ * `single-auto` preserves the source for larger renders and generates one
1019
+ * downsampled atlas only when the effective on-screen scale warrants it.
1020
+ */
1021
+ const TEXTURE_OPTIONS = { lod: "single-auto" };
920
1022
  let vendorConfigured = false;
921
1023
  /** Configure pixi extensions + the Cubism SDK once per page. */
922
1024
  function configureOnce(vendor) {
@@ -995,7 +1097,8 @@ window.__ModuleLoader__.load({
995
1097
  ctx.container.appendChild(pixiApp.canvas);
996
1098
  const loaded = await vendor.Live2DModel.from(config.modelUrl, {
997
1099
  autoHitTest: false,
998
- autoFocus: false
1100
+ autoFocus: false,
1101
+ textureOptions: TEXTURE_OPTIONS
999
1102
  });
1000
1103
  if (disposed) {
1001
1104
  pixiApp.destroy(true, { children: true });
@@ -1717,7 +1820,8 @@ window.__ModuleLoader__.load({
1717
1820
  const valid = plan.filter((item) => item.run !== void 0);
1718
1821
  if (plan.length === 0 || this.saving || valid.length !== plan.length) return;
1719
1822
  const plannedWrites = valid.map((item) => item.op);
1720
- const fields = new Set(plan.map((item) => item.field));
1823
+ const pending = /* @__PURE__ */ new Map();
1824
+ for (const item of plan) pending.set(item.field, this.staged.get(item.field));
1721
1825
  this.saving = true;
1722
1826
  this.failed = false;
1723
1827
  this.failedReason = void 0;
@@ -1730,9 +1834,9 @@ window.__ModuleLoader__.load({
1730
1834
  for (const field of result.fields) if (field.landed) landed.add(field.field);
1731
1835
  } else this.failedReason = result.message;
1732
1836
  } else for (const item of valid) if (await item.run()) landed.add(item.field);
1733
- for (const field of fields) if (landed.has(field)) this.staged.delete(field);
1837
+ for (const [field, before] of pending) if (landed.has(field) && this.staged.get(field) === before) this.staged.delete(field);
1734
1838
  this.saving = false;
1735
- this.failed = landed.size !== fields.size;
1839
+ this.failed = landed.size !== pending.size;
1736
1840
  this.publish();
1737
1841
  }
1738
1842
  /** The scope's batch surface when it supports one; undefined conservatively otherwise. */
@@ -1875,6 +1979,7 @@ window.__ModuleLoader__.load({
1875
1979
  constructor(scope) {
1876
1980
  this.form = new CardForm(scope, [
1877
1981
  booleanField("enabled"),
1982
+ booleanField("decorationEnabled"),
1878
1983
  booleanField("visible"),
1879
1984
  numberField("size"),
1880
1985
  numberField("right"),
@@ -1914,6 +2019,7 @@ window.__ModuleLoader__.load({
1914
2019
  return {
1915
2020
  ...this.form.shell(),
1916
2021
  enabled: this.form.field("enabled"),
2022
+ decorationEnabled: this.form.field("decorationEnabled"),
1917
2023
  visible: this.form.field("visible"),
1918
2024
  size: this.form.field("size"),
1919
2025
  right: this.form.field("right"),
@@ -1984,6 +2090,22 @@ window.__ModuleLoader__.load({
1984
2090
  props.resetField("enabled");
1985
2091
  }
1986
2092
  }),
2093
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
2094
+ id: "settings-pet-decoration",
2095
+ label: t("settings.decoration"),
2096
+ hint: t("settings.decorationHint"),
2097
+ inheritLabel: t("settings.inherit"),
2098
+ onLabel: t("settings.on"),
2099
+ offLabel: t("settings.off"),
2100
+ ...fieldProps,
2101
+ ...state.decorationEnabled,
2102
+ onEdit: (text) => {
2103
+ props.edit("decorationEnabled", text);
2104
+ },
2105
+ onReset: () => {
2106
+ props.resetField("decorationEnabled");
2107
+ }
2108
+ }),
1987
2109
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChoiceField, {
1988
2110
  id: "settings-pet-pet",
1989
2111
  label: t("settings.pet"),
@@ -2115,6 +2237,8 @@ window.__ModuleLoader__.load({
2115
2237
  "settings.petHint": "选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。",
2116
2238
  "settings.enabled": "启用宠物",
2117
2239
  "settings.enabledHint": "关闭后隐藏宠物并停止轮询,可在设置里重新启用。",
2240
+ "settings.decoration": "状态装饰",
2241
+ "settings.decorationHint": "在宠物状态气泡里显示喷水鲸鱼等状态装饰;关闭后气泡只剩文字。",
2118
2242
  "settings.visible": "显示宠物",
2119
2243
  "settings.visibleHint": "关闭后宠物隐藏,可从聊天输入区重新召唤。",
2120
2244
  "settings.size": "大小(px)",
@@ -2166,6 +2290,8 @@ window.__ModuleLoader__.load({
2166
2290
  "settings.petHint": "Choose which pet shows. Names are stored per pet; rename from the pet hover panel.",
2167
2291
  "settings.enabled": "Enable the pet",
2168
2292
  "settings.enabledHint": "When off, the pet hides and polling stops; re-enable it here.",
2293
+ "settings.decoration": "Status decoration",
2294
+ "settings.decorationHint": "Show ornaments like the spouting whale inside the pet status bubbles; when off, bubbles stay text-only.",
2169
2295
  "settings.visible": "Show the pet",
2170
2296
  "settings.visibleHint": "When off, the pet hides; summon it again from the input row.",
2171
2297
  "settings.size": "Size (px)",