@linxin666/dsh-pet 0.2.2 → 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 (125) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +129 -3
  3. package/README.zh.md +129 -3
  4. package/assets/decorations/whale/decoration.json +20 -0
  5. package/assets/decorations/whale/whale-frames.png +0 -0
  6. package/assets/whale/pet.json +169 -20
  7. package/assets/whale-refined/pet.json +17 -2
  8. package/contracts/pet-manifest-v2.schema.json +281 -0
  9. package/contracts/status-decoration-v1.schema.json +144 -0
  10. package/contracts/voice-pack-v1.schema.json +234 -0
  11. package/lib/client.js +581 -38
  12. package/lib/client.js.map +1 -1
  13. package/lib/index.js +1684 -59
  14. package/lib/live2d-vendor.js +995 -0
  15. package/lib/live2d-vendor.js.map +1 -0
  16. package/lib/types/chatter.d.ts +61 -3
  17. package/lib/types/chatter.d.ts.map +1 -1
  18. package/lib/types/chatter.js +71 -12
  19. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  20. package/lib/types/client/PetDockEntry.js +2 -1
  21. package/lib/types/client/PetSettingsCard.d.ts +14 -0
  22. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  23. package/lib/types/client/PetSettingsCard.js +24 -1
  24. package/lib/types/client/PetSprite.d.ts +7 -1
  25. package/lib/types/client/PetSprite.d.ts.map +1 -1
  26. package/lib/types/client/PetSprite.js +120 -12
  27. package/lib/types/client/index.d.ts.map +1 -1
  28. package/lib/types/client/index.js +5 -0
  29. package/lib/types/client/locales.d.ts +14 -0
  30. package/lib/types/client/locales.d.ts.map +1 -1
  31. package/lib/types/client/locales.js +14 -0
  32. package/lib/types/client/phase-stream.d.ts +22 -0
  33. package/lib/types/client/phase-stream.d.ts.map +1 -0
  34. package/lib/types/client/phase-stream.js +28 -0
  35. package/lib/types/client/renderers/PetRendererSwitch.d.ts +25 -0
  36. package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -0
  37. package/lib/types/client/renderers/PetRendererSwitch.js +24 -0
  38. package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts +22 -0
  39. package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -0
  40. package/lib/types/client/renderers/live2d/Live2dVisualMount.js +75 -0
  41. package/lib/types/client/renderers/live2d/runtime.d.ts +89 -0
  42. package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -0
  43. package/lib/types/client/renderers/live2d/runtime.js +63 -0
  44. package/lib/types/client/renderers/live2d/vendor-entry.d.ts +15 -0
  45. package/lib/types/client/renderers/live2d/vendor-entry.d.ts.map +1 -0
  46. package/lib/types/client/renderers/live2d/vendor-entry.js +14 -0
  47. package/lib/types/client/renderers/live2d.d.ts +52 -0
  48. package/lib/types/client/renderers/live2d.d.ts.map +1 -0
  49. package/lib/types/client/renderers/live2d.js +192 -0
  50. package/lib/types/client/renderers/registry.d.ts +32 -0
  51. package/lib/types/client/renderers/registry.d.ts.map +1 -0
  52. package/lib/types/client/renderers/registry.js +49 -0
  53. package/lib/types/client/settings-form.d.ts.map +1 -1
  54. package/lib/types/client/settings-form.js +9 -6
  55. package/lib/types/contracts/renderer.d.ts +50 -0
  56. package/lib/types/contracts/renderer.d.ts.map +1 -0
  57. package/lib/types/contracts/renderer.js +15 -0
  58. package/lib/types/contracts/status-decoration.d.ts +85 -0
  59. package/lib/types/contracts/status-decoration.d.ts.map +1 -0
  60. package/lib/types/contracts/status-decoration.js +21 -0
  61. package/lib/types/decoration.d.ts +39 -0
  62. package/lib/types/decoration.d.ts.map +1 -0
  63. package/lib/types/decoration.js +210 -0
  64. package/lib/types/event-projection.d.ts +8 -3
  65. package/lib/types/event-projection.d.ts.map +1 -1
  66. package/lib/types/event-projection.js +9 -4
  67. package/lib/types/index.d.ts +2 -0
  68. package/lib/types/index.d.ts.map +1 -1
  69. package/lib/types/index.js +2 -0
  70. package/lib/types/manifest-v2.d.ts +129 -0
  71. package/lib/types/manifest-v2.d.ts.map +1 -0
  72. package/lib/types/manifest-v2.js +387 -0
  73. package/lib/types/model3.d.ts +26 -0
  74. package/lib/types/model3.d.ts.map +1 -0
  75. package/lib/types/model3.js +96 -0
  76. package/lib/types/registry.d.ts +112 -5
  77. package/lib/types/registry.d.ts.map +1 -1
  78. package/lib/types/registry.js +395 -38
  79. package/lib/types/routes.d.ts +39 -2
  80. package/lib/types/routes.d.ts.map +1 -1
  81. package/lib/types/routes.js +312 -13
  82. package/lib/types/service.d.ts +38 -1
  83. package/lib/types/service.d.ts.map +1 -1
  84. package/lib/types/service.js +46 -3
  85. package/lib/types/voice-pack.d.ts +98 -0
  86. package/lib/types/voice-pack.d.ts.map +1 -0
  87. package/lib/types/voice-pack.js +384 -0
  88. package/package.json +13 -10
  89. package/src/chatter.test.ts +89 -2
  90. package/src/chatter.ts +120 -14
  91. package/src/client/PetDockEntry.tsx +19 -11
  92. package/src/client/PetSettingsCard.tsx +57 -0
  93. package/src/client/PetSprite.test.tsx +265 -12
  94. package/src/client/PetSprite.tsx +165 -33
  95. package/src/client/index.ts +6 -0
  96. package/src/client/locales.ts +14 -0
  97. package/src/client/phase-stream.test.ts +76 -0
  98. package/src/client/phase-stream.ts +39 -0
  99. package/src/client/renderers/PetRendererSwitch.test.tsx +66 -0
  100. package/src/client/renderers/PetRendererSwitch.tsx +48 -0
  101. package/src/client/renderers/live2d/Live2dVisualMount.tsx +98 -0
  102. package/src/client/renderers/live2d/runtime.test.ts +61 -0
  103. package/src/client/renderers/live2d/runtime.ts +119 -0
  104. package/src/client/renderers/live2d/vendor-entry.ts +14 -0
  105. package/src/client/renderers/live2d.test.ts +263 -0
  106. package/src/client/renderers/live2d.ts +231 -0
  107. package/src/client/renderers/registry.ts +58 -0
  108. package/src/client/settings-form.ts +8 -6
  109. package/src/client/settings-section.module.css +23 -0
  110. package/src/contracts/renderer.ts +54 -0
  111. package/src/contracts/status-decoration.ts +78 -0
  112. package/src/decoration.test.ts +178 -0
  113. package/src/decoration.ts +220 -0
  114. package/src/event-projection.ts +10 -5
  115. package/src/index.ts +2 -0
  116. package/src/manifest-v2.test.ts +251 -0
  117. package/src/manifest-v2.ts +414 -0
  118. package/src/model3.test.ts +79 -0
  119. package/src/model3.ts +91 -0
  120. package/src/registry.test.ts +438 -2
  121. package/src/registry.ts +470 -38
  122. package/src/routes.ts +328 -14
  123. package/src/service.ts +65 -3
  124. package/src/voice-pack.test.ts +216 -0
  125. package/src/voice-pack.ts +413 -0
@@ -20,6 +20,80 @@ import styles from './pet.module.css';
20
20
  function clampOffset(value, max) {
21
21
  return Math.max(0, Math.min(max, value));
22
22
  }
23
+ /**
24
+ * The status decoration ornament (pet-center M5, #567). Renders the active
25
+ * phase's frame segment as a CSS-background strip at a compact bubble
26
+ * height; prefers-reduced-motion holds the segment's first frame, and a
27
+ * missing or undecodable asset simply paints nothing (CSS background
28
+ * failure) — the bubble text is never disturbed. The span is aria-hidden;
29
+ * the bubble keeps its own semantics untouched.
30
+ */
31
+ function StatusOrnament(props) {
32
+ const { decoration, phase } = props;
33
+ const segment = decoration.phases[phase];
34
+ const shown = segment !== undefined && segment !== 'hide';
35
+ const segmentKey = segment !== undefined && segment !== 'hide' ? segment.from + ':' + segment.to : 'none';
36
+ const spanRef = useRef(null);
37
+ const scale = 18 / decoration.cell.height;
38
+ const frameWidth = Math.round(decoration.cell.width * scale);
39
+ const stripWidth = decoration.columns * frameWidth;
40
+ // Value-stable dependency key: the host serves a fresh DecorationView
41
+ // object on every state poll (2 s), so the effect must not depend on the
42
+ // object identity — otherwise each poll would cancel and restart the
43
+ // frame loop and the animation would jump back to its first frame.
44
+ const durationsKey = decoration.durations.join(',');
45
+ useEffect(() => {
46
+ if (segment === undefined || segment === 'hide')
47
+ return;
48
+ const el = spanRef.current;
49
+ if (el === null)
50
+ return;
51
+ const position = (index) => (-index * frameWidth) + 'px 0px';
52
+ const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
53
+ el.style.backgroundPosition = position(segment.from);
54
+ if (reduceMotion)
55
+ return;
56
+ let raf = 0;
57
+ let index = segment.from;
58
+ let elapsed = 0;
59
+ let last = performance.now();
60
+ const tick = (ts) => {
61
+ const delta = ts - last;
62
+ last = ts;
63
+ elapsed += delta;
64
+ const duration = decoration.durations[index] ?? 160;
65
+ if (elapsed >= duration) {
66
+ elapsed = 0;
67
+ if (index < segment.to)
68
+ index += 1;
69
+ else if (decoration.loop)
70
+ index = segment.from;
71
+ }
72
+ el.style.backgroundPosition = position(index);
73
+ // A non-looping segment settles on its last frame; stop scheduling
74
+ // instead of repainting the same position every frame.
75
+ if (!decoration.loop && index === segment.to)
76
+ return;
77
+ raf = requestAnimationFrame(tick);
78
+ };
79
+ raf = requestAnimationFrame(tick);
80
+ return () => cancelAnimationFrame(raf);
81
+ }, [shown, segmentKey, frameWidth, decoration.loop, durationsKey]);
82
+ if (!shown)
83
+ return null;
84
+ return (_jsx("span", { ref: spanRef, "aria-hidden": "true", "data-dsh-pet-decoration": decoration.id, style: {
85
+ display: 'inline-block',
86
+ width: frameWidth,
87
+ height: 18,
88
+ marginRight: 6,
89
+ verticalAlign: 'middle',
90
+ flexShrink: 0,
91
+ backgroundImage: 'url(' + decoration.entryUrl + ')',
92
+ backgroundSize: stripWidth + 'px 18px',
93
+ backgroundRepeat: 'no-repeat',
94
+ backgroundPosition: '0px 0px',
95
+ } }));
96
+ }
23
97
  /**
24
98
  * The floating pet. The spritesheet frame advances on requestAnimationFrame
25
99
  * with per-frame durations from the definition's tracks; the atlas image is
@@ -66,9 +140,35 @@ export function PetSprite(props) {
66
140
  const rows = definition.rows;
67
141
  const tracks = definition.tracks;
68
142
  const sequences = definition.sequences;
143
+ // Hover-panel chrome from the pet's voice pack (pet-center M4, issue
144
+ // #677): every slot falls back to the i18n dictionary when unset. Stat
145
+ // formats carry {rank}/{n}/{points} placeholders the host validated.
146
+ const panel = definition.panel;
147
+ const panelLabel = (slot, i18n) => panel?.labels?.[slot] ?? i18n;
148
+ const panelStat = (slot, i18nKey, values) => {
149
+ const format = panel?.stats?.[slot] ?? props.t(i18nKey, values);
150
+ if (panel?.stats?.[slot] === undefined)
151
+ return format;
152
+ // The host whitelists {rank}/{n}/{points} in every stat slot, so a pack
153
+ // format may reference any of them; substitute all three live values
154
+ // (the slot's own value plus the siblings) instead of only the slot's.
155
+ const all = {
156
+ rank: snapshot?.affinity.rank ?? '?',
157
+ n: snapshot?.treats.stocked ?? 0,
158
+ points: snapshot?.affinity.points ?? 0,
159
+ };
160
+ let text = format;
161
+ for (const [name, value] of Object.entries(all))
162
+ text = text.replaceAll('{' + name + '}', String(value));
163
+ return text;
164
+ };
165
+ const panelShows = (action) => panel?.actions === undefined || panel.actions.includes(action);
69
166
  // Load the atlas once; the definition carries the authoritative per-row
70
- // frame counts and per-track durations, so nothing else is fetched.
167
+ // frame counts and per-track durations, so nothing else is fetched. A
168
+ // custom visual (pet-center M3) replaces the atlas entirely.
71
169
  useEffect(() => {
170
+ if (props.visual !== undefined)
171
+ return;
72
172
  let cancelled = false;
73
173
  const img = new Image();
74
174
  img.onload = () => {
@@ -80,7 +180,7 @@ export function PetSprite(props) {
80
180
  cancelled = true;
81
181
  img.onload = null;
82
182
  };
83
- }, [definition.atlasUrl]);
183
+ }, [definition.atlasUrl, props.visual]);
84
184
  // Frame loop: advance the current track and write background-position.
85
185
  // Offsets must be in SCALED coordinates (background-position applies to the
86
186
  // scaled background image), so the current sprite scale rides a ref that
@@ -93,6 +193,8 @@ export function PetSprite(props) {
93
193
  const scaleRef = useRef(spriteScale);
94
194
  scaleRef.current = spriteScale;
95
195
  useEffect(() => {
196
+ if (props.visual !== undefined)
197
+ return;
96
198
  const reduceMotion = typeof window !== 'undefined'
97
199
  && window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
98
200
  const sequence = animation === animationForPhase(phase) ? sequences?.[phase] : undefined;
@@ -160,7 +262,7 @@ export function PetSprite(props) {
160
262
  };
161
263
  raf = requestAnimationFrame(tick);
162
264
  return () => cancelAnimationFrame(raf);
163
- }, [animation, phase, cell, columns, rows, tracks, sequences]);
265
+ }, [animation, phase, cell, columns, rows, tracks, sequences, props.visual]);
164
266
  // Auto-clear the feedback bubble after its CSS animation. The callback
165
267
  // rides a ref so re-renders never reset the timer: the 2s poll rebuilds
166
268
  // `props` every tick, and depending on it would starve the timeout.
@@ -237,6 +339,8 @@ export function PetSprite(props) {
237
339
  const whisper = feedback === null ? snapshot?.whisper : undefined;
238
340
  const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== undefined || whisper !== undefined;
239
341
  const displayName = snapshot?.name ?? definition.displayName;
342
+ // The host-served status decoration (M5, #567); absent = text-only bubbles.
343
+ const decoration = snapshot?.decoration;
240
344
  // A settled session list can no longer stay pinned open.
241
345
  useEffect(() => {
242
346
  if (sessionBubbles.length <= 1)
@@ -292,10 +396,14 @@ export function PetSprite(props) {
292
396
  }, children: [_jsx("div", { ref: spriteRef, className: styles.sprite, style: {
293
397
  width: spriteWidth,
294
398
  height: spriteHeight,
295
- backgroundImage: imageReady ? 'url(' + definition.atlasUrl + ')' : undefined,
296
- backgroundSize: (cell.width * columns * spriteScale) + 'px ' + (cell.height * (definition.atlasRows ?? rows.length) * spriteScale) + 'px',
297
- backgroundRepeat: 'no-repeat',
298
- backgroundPosition: '0 0',
399
+ ...(props.visual === undefined
400
+ ? {
401
+ backgroundImage: imageReady ? 'url(' + definition.atlasUrl + ')' : undefined,
402
+ backgroundSize: (cell.width * columns * spriteScale) + 'px ' + (cell.height * (definition.atlasRows ?? rows.length) * spriteScale) + 'px',
403
+ backgroundRepeat: 'no-repeat',
404
+ backgroundPosition: '0 0',
405
+ }
406
+ : {}),
299
407
  cursor: dragRef.current === null ? 'grab' : 'grabbing',
300
408
  }, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onClick: () => {
301
409
  // A pointer sequence that moved (dragged) still fires a trailing
@@ -303,14 +411,14 @@ export function PetSprite(props) {
303
411
  if (draggedRef.current)
304
412
  return;
305
413
  props.onPet();
306
- }, role: "button", "aria-label": definition.displayName }), feedback !== null && (_jsx("div", { ref: bubbleRef, className: clsx(styles.bubble, feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet), children: feedback.text }, feedback.at)), feedback === null && (sessionBubbles.length > 0 || statusBubble !== undefined || whisper !== undefined) && (_jsxs("div", { ref: bubbleRef, className: styles.bubbleStack, onPointerEnter: () => setStackPeek(true), onPointerLeave: () => setStackPeek(false), children: [visibleSessions.map((session, index) => {
414
+ }, role: "button", "aria-label": definition.displayName, children: props.visual }), feedback !== null && (_jsx("div", { ref: bubbleRef, className: clsx(styles.bubble, feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet), children: feedback.text }, feedback.at)), feedback === null && (sessionBubbles.length > 0 || statusBubble !== undefined || whisper !== undefined) && (_jsxs("div", { ref: bubbleRef, className: styles.bubbleStack, onPointerEnter: () => setStackPeek(true), onPointerLeave: () => setStackPeek(false), children: [visibleSessions.map((session, index) => {
307
415
  // The whisper rides the display session's bubble — the stack's
308
416
  // primary entry (DOM-first, rendered bottom-most by the reversed
309
417
  // column so it stays glued to the sprite when extras open above).
310
418
  // The key swap restarts the entrance animation so the mood change
311
419
  // reads as the bubble re-speaking.
312
420
  const speaksWhisper = index === 0 && whisper !== undefined;
313
- const bubble = (_jsx("button", { type: "button", className: clsx(styles.bubble, styles.bubbleStatus, styles.bubbleClickable, speaksWhisper && styles.bubbleWhisper), title: props.t('pet.openSessionHint'), onClick: () => { props.onOpenSession(session.sessionId); }, children: speaksWhisper ? whisper : session.bubble }, speaksWhisper ? 'whisper:' + whisper : session.sessionId));
421
+ const bubble = (_jsxs("button", { type: "button", className: clsx(styles.bubble, styles.bubbleStatus, styles.bubbleClickable, speaksWhisper && styles.bubbleWhisper), title: props.t('pet.openSessionHint'), onClick: () => { props.onOpenSession(session.sessionId); }, children: [index === 0 && !speaksWhisper && decoration !== undefined && (_jsx(StatusOrnament, { decoration: decoration, phase: phase })), speaksWhisper ? whisper : session.bubble] }, speaksWhisper ? 'whisper:' + whisper : session.sessionId));
314
422
  // The primary bubble carries the '+N' badge while other sessions
315
423
  // hide behind it; the badge toggles the pinned (touch) expansion.
316
424
  if (index !== 0 || sessionBubbles.length <= 1)
@@ -323,7 +431,7 @@ export function PetSprite(props) {
323
431
  e.stopPropagation();
324
432
  setStackPinned(open => !open);
325
433
  }, children: stackOpen ? '×' : '+' + String(sessionBubbles.length - 1) })] }, "primary"));
326
- }), sessionBubbles.length === 0 && (statusBubble !== undefined || whisper !== undefined) && (_jsx("div", { className: clsx(styles.bubble, styles.bubbleStatus, whisper !== undefined && styles.bubbleWhisper), role: "status", "aria-live": "polite", children: whisper ?? statusBubble }, whisper === undefined ? 'status' : 'whisper:' + whisper))] })), hovered && dragRef.current === null && (_jsx("div", { ref: panelRef, className: clsx(styles.panel, panelAbove && styles.panelAbove), "data-placement": panelAbove ? 'above' : 'below', style: panelAbove && panelLift > 0
434
+ }), sessionBubbles.length === 0 && (statusBubble !== undefined || whisper !== undefined) && (_jsxs("div", { className: clsx(styles.bubble, styles.bubbleStatus, whisper !== undefined && styles.bubbleWhisper), role: "status", "aria-live": "polite", children: [whisper === undefined && decoration !== undefined && (_jsx(StatusOrnament, { decoration: decoration, phase: phase })), whisper ?? statusBubble] }, whisper === undefined ? 'status' : 'whisper:' + whisper))] })), hovered && dragRef.current === null && (_jsx("div", { ref: panelRef, className: clsx(styles.panel, panelAbove && styles.panelAbove), "data-placement": panelAbove ? 'above' : 'below', style: panelAbove && panelLift > 0
327
435
  ? { marginBottom: panelLift }
328
436
  : undefined, onPointerEnter: () => {
329
437
  // Reaching the panel (or its bridge) must cancel any hide timer
@@ -355,12 +463,12 @@ export function PetSprite(props) {
355
463
  props.onRename(trimmed);
356
464
  setRenaming(false);
357
465
  }
358
- }, children: 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: props.t('pet.rank', { rank: snapshot?.affinity.rank ?? '?' }) })] }), _jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.statTreats, children: props.t('pet.treats', { n: snapshot?.treats.stocked ?? 0 }) }), _jsx("span", { className: styles.statPoints, children: props.t('pet.points', { points: snapshot?.affinity.points ?? 0 }) })] }), _jsxs("div", { className: styles.actions, children: [_jsx("button", { type: "button", className: styles.action, onClick: props.onFeed, children: props.t('pet.feed') }), _jsx("button", { type: "button", className: styles.action, onClick: () => {
466
+ }, 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: () => {
359
467
  // Cancel any pending hide so the rename box cannot
360
468
  // unmount right as the user starts typing (#303).
361
469
  clearHideTimer();
362
470
  setNameDraft(displayName);
363
471
  setRenaming(true);
364
- }, children: props.t('pet.rename') }), _jsx("button", { type: "button", className: styles.action, onClick: props.onHide, children: props.t('pet.hide') })] })] })) }))] }));
472
+ }, children: panelLabel('rename', props.t('pet.rename')) })), panelShows('hide') && (_jsx("button", { type: "button", className: styles.action, onClick: props.onHide, children: panelLabel('hide', props.t('pet.hide')) }))] })] })) }))] }));
365
473
  return createPortal(float, document.body);
366
474
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AA6DnI,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAkN9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AA+DnI,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAsN9C"}
@@ -14,6 +14,8 @@ import { createElement } from 'react';
14
14
  import { createRoot } from 'react-dom/client';
15
15
  import { createPetStore } from './pet-store.js';
16
16
  import { PetDockEntry } from './PetDockEntry.js';
17
+ import { defaultPetRendererRegistry } from './renderers/registry.js';
18
+ import { live2dRenderer } from './renderers/live2d.js';
17
19
  import { PetSettingsSection, PetSettingsCardController } from './PetSettingsCard.js';
18
20
  import { NS, en, zh, t } from './locales.js';
19
21
  /** Same-origin JSON fetch helper (GET without body, POST with JSON body). */
@@ -54,6 +56,9 @@ export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote
54
56
  */
55
57
  export function apply(ctx) {
56
58
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'pet: dictionaries');
59
+ // Built-in renderers dispatch through the plugin-wide registry (pet-center
60
+ // M3). Registration is idempotent (id wins), so re-applies stay clean.
61
+ defaultPetRendererRegistry.register(live2dRenderer);
57
62
  const binder = ctx.get('webUiSettings') ?? ctx.settingsScope;
58
63
  const settingsScope = binder.bind({ namespace: PET_SETTINGS_NS });
59
64
  const enabled = () => {
@@ -17,15 +17,22 @@ export declare const zh: {
17
17
  readonly 'pet.treats': '小鱼干 ×{n}';
18
18
  readonly 'pet.state.loading': '宠物正在赶来…';
19
19
  readonly 'pet.state.error': '宠物迷路了(连接失败)';
20
+ readonly 'pet.renderer.unavailable': '这只宠物需要的渲染器({renderer})在当前版本不可用。';
21
+ readonly 'pet.live2d.core-missing': 'Live2D 核心未安装:请把官方 live2dcubismcore.min.js 放入 $DSH_HOME/pets/.runtime/ 后刷新(步骤见宠物插件 README)。';
22
+ readonly 'pet.live2d.vendor-missing': 'Live2D 组件缺失,请升级宠物插件。';
23
+ readonly 'pet.live2d.load-failed': 'Live2D 模型加载失败,请检查该宠物目录的完整性。';
20
24
  readonly 'pet.openSessionHint': '点击跳转到对应会话';
21
25
  readonly 'pet.moreSessions': '展开其余 {n} 个会话的气泡';
22
26
  readonly 'pet.collapseSessions': '收起会话气泡';
23
27
  readonly 'settings.title': '宠物';
28
+ readonly 'settings.diagnosticsTitle': '宠物目录诊断';
24
29
  readonly 'settings.description': '选择宠物并调整它的显示布局。';
25
30
  readonly 'settings.pet': '宠物';
26
31
  readonly 'settings.petHint': '选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。';
27
32
  readonly 'settings.enabled': '启用宠物';
28
33
  readonly 'settings.enabledHint': '关闭后隐藏宠物并停止轮询,可在设置里重新启用。';
34
+ readonly 'settings.decoration': '状态装饰';
35
+ readonly 'settings.decorationHint': '在宠物状态气泡里显示喷水鲸鱼等状态装饰;关闭后气泡只剩文字。';
29
36
  readonly 'settings.visible': '显示宠物';
30
37
  readonly 'settings.visibleHint': '关闭后宠物隐藏,可从聊天输入区重新召唤。';
31
38
  readonly 'settings.size': '大小(px)';
@@ -63,15 +70,22 @@ export declare const en: {
63
70
  readonly 'pet.treats': 'Treats ×{n}';
64
71
  readonly 'pet.state.loading': 'The pet is on its way…';
65
72
  readonly 'pet.state.error': 'The pet is lost (connection failed)';
73
+ readonly 'pet.renderer.unavailable': 'This pet needs a renderer ({renderer}) that is not available in this build.';
74
+ readonly 'pet.live2d.core-missing': 'Live2D Cubism Core is not installed: place the official live2dcubismcore.min.js under $DSH_HOME/pets/.runtime/ and refresh (see the pet plugin README).';
75
+ readonly 'pet.live2d.vendor-missing': 'The Live2D component is missing; please update the pet plugin.';
76
+ readonly 'pet.live2d.load-failed': 'The Live2D model failed to load; check the pet directory is complete.';
66
77
  readonly 'pet.openSessionHint': 'Click to jump to this session';
67
78
  readonly 'pet.moreSessions': 'Expand {n} more session bubbles';
68
79
  readonly 'pet.collapseSessions': 'Collapse session bubbles';
69
80
  readonly 'settings.title': 'Pet';
81
+ readonly 'settings.diagnosticsTitle': 'Pet directory diagnostics';
70
82
  readonly 'settings.description': 'Pick a pet and tune its display layout.';
71
83
  readonly 'settings.pet': 'Pet';
72
84
  readonly 'settings.petHint': 'Choose which pet shows. Names are stored per pet; rename from the pet hover panel.';
73
85
  readonly 'settings.enabled': 'Enable the pet';
74
86
  readonly 'settings.enabledHint': 'When off, the pet hides and polling stops; re-enable it here.';
87
+ readonly 'settings.decoration': 'Status decoration';
88
+ readonly 'settings.decorationHint': 'Show ornaments like the spouting whale inside the pet status bubbles; when off, bubbles stay text-only.';
75
89
  readonly 'settings.visible': 'Show the pet';
76
90
  readonly 'settings.visibleHint': 'When off, the pet hides; summon it again from the input row.';
77
91
  readonly 'settings.size': 'Size (px)';
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"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"}
@@ -17,16 +17,23 @@ export const zh = {
17
17
  'pet.treats': '小鱼干 ×{n}',
18
18
  'pet.state.loading': '宠物正在赶来…',
19
19
  'pet.state.error': '宠物迷路了(连接失败)',
20
+ 'pet.renderer.unavailable': '这只宠物需要的渲染器({renderer})在当前版本不可用。',
21
+ 'pet.live2d.core-missing': 'Live2D 核心未安装:请把官方 live2dcubismcore.min.js 放入 $DSH_HOME/pets/.runtime/ 后刷新(步骤见宠物插件 README)。',
22
+ 'pet.live2d.vendor-missing': 'Live2D 组件缺失,请升级宠物插件。',
23
+ 'pet.live2d.load-failed': 'Live2D 模型加载失败,请检查该宠物目录的完整性。',
20
24
  'pet.openSessionHint': '点击跳转到对应会话',
21
25
  'pet.moreSessions': '展开其余 {n} 个会话的气泡',
22
26
  'pet.collapseSessions': '收起会话气泡',
23
27
  // 一级设置页(settings.section 席位)。
24
28
  'settings.title': '宠物',
29
+ 'settings.diagnosticsTitle': '宠物目录诊断',
25
30
  'settings.description': '选择宠物并调整它的显示布局。',
26
31
  'settings.pet': '宠物',
27
32
  'settings.petHint': '选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。',
28
33
  'settings.enabled': '启用宠物',
29
34
  'settings.enabledHint': '关闭后隐藏宠物并停止轮询,可在设置里重新启用。',
35
+ 'settings.decoration': '状态装饰',
36
+ 'settings.decorationHint': '在宠物状态气泡里显示喷水鲸鱼等状态装饰;关闭后气泡只剩文字。',
30
37
  'settings.visible': '显示宠物',
31
38
  'settings.visibleHint': '关闭后宠物隐藏,可从聊天输入区重新召唤。',
32
39
  'settings.size': '大小(px)',
@@ -64,16 +71,23 @@ export const en = {
64
71
  'pet.treats': 'Treats ×{n}',
65
72
  'pet.state.loading': 'The pet is on its way…',
66
73
  'pet.state.error': 'The pet is lost (connection failed)',
74
+ 'pet.renderer.unavailable': 'This pet needs a renderer ({renderer}) that is not available in this build.',
75
+ 'pet.live2d.core-missing': 'Live2D Cubism Core is not installed: place the official live2dcubismcore.min.js under $DSH_HOME/pets/.runtime/ and refresh (see the pet plugin README).',
76
+ 'pet.live2d.vendor-missing': 'The Live2D component is missing; please update the pet plugin.',
77
+ 'pet.live2d.load-failed': 'The Live2D model failed to load; check the pet directory is complete.',
67
78
  'pet.openSessionHint': 'Click to jump to this session',
68
79
  'pet.moreSessions': 'Expand {n} more session bubbles',
69
80
  'pet.collapseSessions': 'Collapse session bubbles',
70
81
  // First-level settings section (the `settings.section` seat).
71
82
  'settings.title': 'Pet',
83
+ 'settings.diagnosticsTitle': 'Pet directory diagnostics',
72
84
  'settings.description': 'Pick a pet and tune its display layout.',
73
85
  'settings.pet': 'Pet',
74
86
  'settings.petHint': 'Choose which pet shows. Names are stored per pet; rename from the pet hover panel.',
75
87
  'settings.enabled': 'Enable the pet',
76
88
  'settings.enabledHint': 'When off, the pet hides and polling stops; re-enable it here.',
89
+ 'settings.decoration': 'Status decoration',
90
+ 'settings.decorationHint': 'Show ornaments like the spouting whale inside the pet status bubbles; when off, bubbles stay text-only.',
77
91
  'settings.visible': 'Show the pet',
78
92
  'settings.visibleHint': 'When off, the pet hides; summon it again from the input row.',
79
93
  'settings.size': 'Size (px)',
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Phase stream — bridges the polled host snapshots onto the renderer
3
+ * contract's { get, subscribe } shape (pet-center M2 P4, issue #623). The
4
+ * existing poll loop pushes each snapshot's ActivityPhase; subscribers are
5
+ * dispatched on CHANGE only (phase transitions are sparse — done/failed hold
6
+ * a timed window before falling back to idle — and renderers like Live2D pay
7
+ * per transition, not per tick).
8
+ * @module @linxin666/dsh-pet/client/phase-stream
9
+ */
10
+ import type { ActivityPhase } from '../state.ts';
11
+ /** The renderer-facing phase stream. */
12
+ export interface PhaseStream {
13
+ /** The latest pushed phase. */
14
+ get(): ActivityPhase;
15
+ /** Subscribe to phase changes; returns the unsubscribe. */
16
+ subscribe(listener: (phase: ActivityPhase) => void): () => void;
17
+ /** Feed a fresh snapshot phase; no-op when unchanged. */
18
+ push(phase: ActivityPhase): void;
19
+ }
20
+ /** Create the stream (one per pet entry lifetime, owned by the plugin body). */
21
+ export declare function createPhaseStream(initial?: ActivityPhase): PhaseStream;
22
+ //# sourceMappingURL=phase-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phase-stream.d.ts","sourceRoot":"","sources":["../../../src/client/phase-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD,wCAAwC;AACxC,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,GAAG,IAAI,aAAa,CAAA;IACpB,2DAA2D;IAC3D,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IAC/D,yDAAyD;IACzD,IAAI,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAA;CACjC;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,aAAsB,GAAG,WAAW,CAe9E"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Phase stream — bridges the polled host snapshots onto the renderer
3
+ * contract's { get, subscribe } shape (pet-center M2 P4, issue #623). The
4
+ * existing poll loop pushes each snapshot's ActivityPhase; subscribers are
5
+ * dispatched on CHANGE only (phase transitions are sparse — done/failed hold
6
+ * a timed window before falling back to idle — and renderers like Live2D pay
7
+ * per transition, not per tick).
8
+ * @module @linxin666/dsh-pet/client/phase-stream
9
+ */
10
+ /** Create the stream (one per pet entry lifetime, owned by the plugin body). */
11
+ export function createPhaseStream(initial = 'idle') {
12
+ let current = initial;
13
+ const listeners = new Set();
14
+ return {
15
+ get: () => current,
16
+ subscribe(listener) {
17
+ listeners.add(listener);
18
+ return () => { listeners.delete(listener); };
19
+ },
20
+ push(phase) {
21
+ if (phase === current)
22
+ return;
23
+ current = phase;
24
+ for (const listener of [...listeners])
25
+ listener(phase);
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Renderer switch — the client dispatch seam of the pet center (issue #623,
3
+ * milestone M2 P5 / M3). The pet's manifest picks the renderer: sprite2d
4
+ * hands straight through to the sprite; live2d injects its visual INTO the
5
+ * sprite chrome (the dock, bubbles and panel belong to the pet center, not
6
+ * the renderer); a renderer this build cannot serve renders a clear
7
+ * diagnostic card instead of blanking.
8
+ * @module @linxin666/dsh-pet/client/renderers/PetRendererSwitch
9
+ */
10
+ import { type ReactElement, type ReactNode } from 'react';
11
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
12
+ import type { PetDefinition } from '../../registry.ts';
13
+ import type { ActivityPhase } from '../../state.ts';
14
+ import type { NS } from '../locales.ts';
15
+ /** Dispatch one pet definition to its renderer; unknown kinds get a card. */
16
+ export declare function PetRendererSwitch(props: {
17
+ definition: PetDefinition;
18
+ /** Current activity phase (fed to renderer visuals). */
19
+ phase: ActivityPhase;
20
+ /** The chrome's pet interaction (affinity write-back owner). */
21
+ onPet: () => void;
22
+ t: PropsLocale<typeof NS>['t'];
23
+ children?: ReactNode;
24
+ }): ReactElement;
25
+ //# sourceMappingURL=PetRendererSwitch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PetRendererSwitch.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/PetRendererSwitch.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAgC,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAInD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAEvC,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,UAAU,EAAE,aAAa,CAAA;IACzB,wDAAwD;IACxD,KAAK,EAAE,aAAa,CAAA;IACpB,gEAAgE;IAChE,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;IAC9B,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,GAAG,YAAY,CAmBf"}
@@ -0,0 +1,24 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Renderer switch — the client dispatch seam of the pet center (issue #623,
4
+ * milestone M2 P5 / M3). The pet's manifest picks the renderer: sprite2d
5
+ * hands straight through to the sprite; live2d injects its visual INTO the
6
+ * sprite chrome (the dock, bubbles and panel belong to the pet center, not
7
+ * the renderer); a renderer this build cannot serve renders a clear
8
+ * diagnostic card instead of blanking.
9
+ * @module @linxin666/dsh-pet/client/renderers/PetRendererSwitch
10
+ */
11
+ import { cloneElement, isValidElement } from 'react';
12
+ import { defaultPetRendererRegistry } from './registry.js';
13
+ import { Live2dVisualMount } from './live2d/Live2dVisualMount.js';
14
+ /** Dispatch one pet definition to its renderer; unknown kinds get a card. */
15
+ export function PetRendererSwitch(props) {
16
+ const renderer = props.definition.renderer ?? 'sprite2d';
17
+ if (renderer === 'sprite2d')
18
+ return _jsx(_Fragment, { children: props.children });
19
+ if (renderer === 'live2d' && defaultPetRendererRegistry.has('live2d') && isValidElement(props.children)) {
20
+ const visual = (_jsx(Live2dVisualMount, { definition: props.definition, phase: props.phase, onPet: props.onPet, t: props.t }));
21
+ return cloneElement(props.children, { visual });
22
+ }
23
+ return (_jsx("span", { "data-dsh-pet-renderer-fallback": renderer, children: props.t('pet.renderer.unavailable', { renderer }) }));
24
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Live2D visual mount (pet-center M3) — the React bridge between the pet
3
+ * center chrome and the imperative live2d renderer. The bridge owns the
4
+ * contract context (asset base, phase stream, interaction write-back,
5
+ * activation cleanups), feeds the polled phase into the stream, forwards
6
+ * sub-4px taps as hit-test coordinates, and renders the localized error
7
+ * card when the renderer reports a fatal boot failure.
8
+ * @module @linxin666/dsh-pet/client/renderers/live2d/Live2dVisualMount
9
+ */
10
+ import { type ReactElement } from 'react';
11
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
12
+ import type { PetDefinition } from '../../../registry.ts';
13
+ import type { ActivityPhase } from '../../../state.ts';
14
+ import type { NS } from '../../locales.ts';
15
+ /** Mount the live2d renderer as the sprite's visual (inside the chrome). */
16
+ export declare function Live2dVisualMount(props: {
17
+ definition: PetDefinition;
18
+ phase: ActivityPhase;
19
+ onPet: () => void;
20
+ t: PropsLocale<typeof NS>['t'];
21
+ }): ReactElement;
22
+ //# sourceMappingURL=Live2dVisualMount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Live2dVisualMount.d.ts","sourceRoot":"","sources":["../../../../../src/client/renderers/live2d/Live2dVisualMount.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAA+B,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAKtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAA;AAE1C,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,UAAU,EAAE,aAAa,CAAA;IACzB,KAAK,EAAE,aAAa,CAAA;IACpB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;CAC/B,GAAG,YAAY,CAuEf"}
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Live2D visual mount (pet-center M3) — the React bridge between the pet
4
+ * center chrome and the imperative live2d renderer. The bridge owns the
5
+ * contract context (asset base, phase stream, interaction write-back,
6
+ * activation cleanups), feeds the polled phase into the stream, forwards
7
+ * sub-4px taps as hit-test coordinates, and renders the localized error
8
+ * card when the renderer reports a fatal boot failure.
9
+ * @module @linxin666/dsh-pet/client/renderers/live2d/Live2dVisualMount
10
+ */
11
+ import { useEffect, useRef, useState } from 'react';
12
+ import { createPhaseStream } from '../../phase-stream.js';
13
+ import { defaultPetRendererRegistry } from '../registry.js';
14
+ /** Mount the live2d renderer as the sprite's visual (inside the chrome). */
15
+ export function Live2dVisualMount(props) {
16
+ const containerRef = useRef(null);
17
+ const streamRef = useRef(null);
18
+ const handleRef = useRef(null);
19
+ const downRef = useRef(null);
20
+ const [error, setError] = useState(null);
21
+ // One activation per pet definition: build the contract context and mount.
22
+ useEffect(() => {
23
+ const container = containerRef.current;
24
+ const live2d = props.definition.live2d;
25
+ if (container === null || live2d === undefined)
26
+ return undefined;
27
+ streamRef.current ??= createPhaseStream(props.phase);
28
+ const cleanups = [];
29
+ const ctx = {
30
+ petId: props.definition.id,
31
+ assetBase: '/pet/' + encodeURIComponent(props.definition.id),
32
+ container,
33
+ phase: streamRef.current,
34
+ interact: props.onPet,
35
+ onCleanup: (fn) => { cleanups.push(fn); },
36
+ };
37
+ let handle;
38
+ try {
39
+ handle = defaultPetRendererRegistry.mount('live2d', ctx, live2d);
40
+ }
41
+ catch {
42
+ setError('load-failed');
43
+ return () => { for (const fn of cleanups.splice(0))
44
+ fn(); };
45
+ }
46
+ handleRef.current = handle;
47
+ handle.onError?.(setError);
48
+ return () => {
49
+ handleRef.current = null;
50
+ for (const fn of cleanups.splice(0))
51
+ fn();
52
+ handle.dispose();
53
+ };
54
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- one activation per pet identity
55
+ }, [props.definition]);
56
+ // Feed the polled phase into the activation's stream (change-only).
57
+ useEffect(() => {
58
+ streamRef.current?.push(props.phase);
59
+ }, [props.phase]);
60
+ return (_jsx("div", { ref: containerRef, "data-dsh-pet-live2d": props.definition.id, style: { width: '100%', height: '100%' }, onPointerDown: (e) => { downRef.current = { x: e.clientX, y: e.clientY }; }, onPointerUp: (e) => {
61
+ const down = downRef.current;
62
+ downRef.current = null;
63
+ if (down === null)
64
+ return;
65
+ // A moved pointer is a drag (the chrome owns it), not a tap.
66
+ if (Math.abs(e.clientX - down.x) > 4 || Math.abs(e.clientY - down.y) > 4)
67
+ return;
68
+ const rect = e.currentTarget.getBoundingClientRect();
69
+ handleRef.current?.tap(e.clientX - rect.left, e.clientY - rect.top);
70
+ }, children: error !== null && (_jsx("span", { "data-dsh-pet-live2d-error": error, children: error === 'core-missing'
71
+ ? props.t('pet.live2d.core-missing')
72
+ : error === 'vendor-missing'
73
+ ? props.t('pet.live2d.vendor-missing')
74
+ : props.t('pet.live2d.load-failed') })) }));
75
+ }