@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.
- package/README.i18n.yaml +2 -2
- package/README.md +129 -3
- package/README.zh.md +129 -3
- package/assets/decorations/whale/decoration.json +20 -0
- package/assets/decorations/whale/whale-frames.png +0 -0
- package/assets/whale/pet.json +169 -20
- package/assets/whale-refined/pet.json +17 -2
- package/contracts/pet-manifest-v2.schema.json +281 -0
- package/contracts/status-decoration-v1.schema.json +144 -0
- package/contracts/voice-pack-v1.schema.json +234 -0
- package/lib/client.js +581 -38
- package/lib/client.js.map +1 -1
- package/lib/index.js +1684 -59
- package/lib/live2d-vendor.js +995 -0
- package/lib/live2d-vendor.js.map +1 -0
- package/lib/types/chatter.d.ts +61 -3
- package/lib/types/chatter.d.ts.map +1 -1
- package/lib/types/chatter.js +71 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +2 -1
- package/lib/types/client/PetSettingsCard.d.ts +14 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +24 -1
- package/lib/types/client/PetSprite.d.ts +7 -1
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +120 -12
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +5 -0
- package/lib/types/client/locales.d.ts +14 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +14 -0
- package/lib/types/client/phase-stream.d.ts +22 -0
- package/lib/types/client/phase-stream.d.ts.map +1 -0
- package/lib/types/client/phase-stream.js +28 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts +25 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -0
- package/lib/types/client/renderers/PetRendererSwitch.js +24 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts +22 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.js +75 -0
- package/lib/types/client/renderers/live2d/runtime.d.ts +89 -0
- package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/runtime.js +63 -0
- package/lib/types/client/renderers/live2d/vendor-entry.d.ts +15 -0
- package/lib/types/client/renderers/live2d/vendor-entry.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/vendor-entry.js +14 -0
- package/lib/types/client/renderers/live2d.d.ts +52 -0
- package/lib/types/client/renderers/live2d.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d.js +192 -0
- package/lib/types/client/renderers/registry.d.ts +32 -0
- package/lib/types/client/renderers/registry.d.ts.map +1 -0
- package/lib/types/client/renderers/registry.js +49 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +9 -6
- package/lib/types/contracts/renderer.d.ts +50 -0
- package/lib/types/contracts/renderer.d.ts.map +1 -0
- package/lib/types/contracts/renderer.js +15 -0
- package/lib/types/contracts/status-decoration.d.ts +85 -0
- package/lib/types/contracts/status-decoration.d.ts.map +1 -0
- package/lib/types/contracts/status-decoration.js +21 -0
- package/lib/types/decoration.d.ts +39 -0
- package/lib/types/decoration.d.ts.map +1 -0
- package/lib/types/decoration.js +210 -0
- package/lib/types/event-projection.d.ts +8 -3
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +9 -4
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +2 -0
- package/lib/types/manifest-v2.d.ts +129 -0
- package/lib/types/manifest-v2.d.ts.map +1 -0
- package/lib/types/manifest-v2.js +387 -0
- package/lib/types/model3.d.ts +26 -0
- package/lib/types/model3.d.ts.map +1 -0
- package/lib/types/model3.js +96 -0
- package/lib/types/registry.d.ts +112 -5
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +395 -38
- package/lib/types/routes.d.ts +39 -2
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +312 -13
- package/lib/types/service.d.ts +38 -1
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +46 -3
- package/lib/types/voice-pack.d.ts +98 -0
- package/lib/types/voice-pack.d.ts.map +1 -0
- package/lib/types/voice-pack.js +384 -0
- package/package.json +13 -10
- package/src/chatter.test.ts +89 -2
- package/src/chatter.ts +120 -14
- package/src/client/PetDockEntry.tsx +19 -11
- package/src/client/PetSettingsCard.tsx +57 -0
- package/src/client/PetSprite.test.tsx +265 -12
- package/src/client/PetSprite.tsx +165 -33
- package/src/client/index.ts +6 -0
- package/src/client/locales.ts +14 -0
- package/src/client/phase-stream.test.ts +76 -0
- package/src/client/phase-stream.ts +39 -0
- package/src/client/renderers/PetRendererSwitch.test.tsx +66 -0
- package/src/client/renderers/PetRendererSwitch.tsx +48 -0
- package/src/client/renderers/live2d/Live2dVisualMount.tsx +98 -0
- package/src/client/renderers/live2d/runtime.test.ts +61 -0
- package/src/client/renderers/live2d/runtime.ts +119 -0
- package/src/client/renderers/live2d/vendor-entry.ts +14 -0
- package/src/client/renderers/live2d.test.ts +263 -0
- package/src/client/renderers/live2d.ts +231 -0
- package/src/client/renderers/registry.ts +58 -0
- package/src/client/settings-form.ts +8 -6
- package/src/client/settings-section.module.css +23 -0
- package/src/contracts/renderer.ts +54 -0
- package/src/contracts/status-decoration.ts +78 -0
- package/src/decoration.test.ts +178 -0
- package/src/decoration.ts +220 -0
- package/src/event-projection.ts +10 -5
- package/src/index.ts +2 -0
- package/src/manifest-v2.test.ts +251 -0
- package/src/manifest-v2.ts +414 -0
- package/src/model3.test.ts +79 -0
- package/src/model3.ts +91 -0
- package/src/registry.test.ts +438 -2
- package/src/registry.ts +470 -38
- package/src/routes.ts +328 -14
- package/src/service.ts +65 -3
- package/src/voice-pack.test.ts +216 -0
- package/src/voice-pack.ts +413 -0
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,7 +325,23 @@ 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)(() => {
|
|
344
|
+
if (props.visual !== void 0) return;
|
|
257
345
|
let cancelled = false;
|
|
258
346
|
const img = new Image();
|
|
259
347
|
img.onload = () => {
|
|
@@ -264,13 +352,14 @@ window.__ModuleLoader__.load({
|
|
|
264
352
|
cancelled = true;
|
|
265
353
|
img.onload = null;
|
|
266
354
|
};
|
|
267
|
-
}, [definition.atlasUrl]);
|
|
355
|
+
}, [definition.atlasUrl, props.visual]);
|
|
268
356
|
const spriteScale = display.size / cell.height;
|
|
269
357
|
const phase = snapshot?.phase ?? "idle";
|
|
270
358
|
const animation = snapshot?.animation ?? "idle";
|
|
271
359
|
const scaleRef = (0, react.useRef)(spriteScale);
|
|
272
360
|
scaleRef.current = spriteScale;
|
|
273
361
|
(0, react.useEffect)(() => {
|
|
362
|
+
if (props.visual !== void 0) return;
|
|
274
363
|
const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
|
|
275
364
|
const sequence = animation === animationForPhase(phase) ? sequences?.[phase] : void 0;
|
|
276
365
|
const leadAnimation = sequence?.[0] ?? animation;
|
|
@@ -326,7 +415,8 @@ window.__ModuleLoader__.load({
|
|
|
326
415
|
columns,
|
|
327
416
|
rows,
|
|
328
417
|
tracks,
|
|
329
|
-
sequences
|
|
418
|
+
sequences,
|
|
419
|
+
props.visual
|
|
330
420
|
]);
|
|
331
421
|
const feedbackDoneRef = (0, react.useRef)(props.onFeedbackDone);
|
|
332
422
|
feedbackDoneRef.current = props.onFeedbackDone;
|
|
@@ -389,6 +479,7 @@ window.__ModuleLoader__.load({
|
|
|
389
479
|
const whisper = feedback === null ? snapshot?.whisper : void 0;
|
|
390
480
|
const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== void 0 || whisper !== void 0;
|
|
391
481
|
const displayName = snapshot?.name ?? definition.displayName;
|
|
482
|
+
const decoration = snapshot?.decoration;
|
|
392
483
|
(0, react.useEffect)(() => {
|
|
393
484
|
if (sessionBubbles.length <= 1) setStackPinned(false);
|
|
394
485
|
}, [sessionBubbles.length]);
|
|
@@ -447,10 +538,12 @@ window.__ModuleLoader__.load({
|
|
|
447
538
|
style: {
|
|
448
539
|
width: spriteWidth,
|
|
449
540
|
height: spriteHeight,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
541
|
+
...props.visual === void 0 ? {
|
|
542
|
+
backgroundImage: imageReady ? "url(" + definition.atlasUrl + ")" : void 0,
|
|
543
|
+
backgroundSize: cell.width * columns * spriteScale + "px " + cell.height * (definition.atlasRows ?? rows.length) * spriteScale + "px",
|
|
544
|
+
backgroundRepeat: "no-repeat",
|
|
545
|
+
backgroundPosition: "0 0"
|
|
546
|
+
} : {},
|
|
454
547
|
cursor: dragRef.current === null ? "grab" : "grabbing"
|
|
455
548
|
},
|
|
456
549
|
onPointerDown,
|
|
@@ -461,7 +554,8 @@ window.__ModuleLoader__.load({
|
|
|
461
554
|
props.onPet();
|
|
462
555
|
},
|
|
463
556
|
role: "button",
|
|
464
|
-
"aria-label": definition.displayName
|
|
557
|
+
"aria-label": definition.displayName,
|
|
558
|
+
children: props.visual
|
|
465
559
|
}),
|
|
466
560
|
feedback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
467
561
|
ref: bubbleRef,
|
|
@@ -475,14 +569,17 @@ window.__ModuleLoader__.load({
|
|
|
475
569
|
onPointerLeave: () => setStackPeek(false),
|
|
476
570
|
children: [visibleSessions.map((session, index) => {
|
|
477
571
|
const speaksWhisper = index === 0 && whisper !== void 0;
|
|
478
|
-
const bubble = /* @__PURE__ */ (0, react_jsx_runtime.
|
|
572
|
+
const bubble = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
479
573
|
type: "button",
|
|
480
574
|
className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, pet_module_css_default.bubbleClickable, speaksWhisper && pet_module_css_default.bubbleWhisper),
|
|
481
575
|
title: props.t("pet.openSessionHint"),
|
|
482
576
|
onClick: () => {
|
|
483
577
|
props.onOpenSession(session.sessionId);
|
|
484
578
|
},
|
|
485
|
-
children: speaksWhisper
|
|
579
|
+
children: [index === 0 && !speaksWhisper && decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
|
|
580
|
+
decoration,
|
|
581
|
+
phase
|
|
582
|
+
}), speaksWhisper ? whisper : session.bubble]
|
|
486
583
|
}, speaksWhisper ? "whisper:" + whisper : session.sessionId);
|
|
487
584
|
if (index !== 0 || sessionBubbles.length <= 1) return bubble;
|
|
488
585
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -500,11 +597,14 @@ window.__ModuleLoader__.load({
|
|
|
500
597
|
children: stackOpen ? "×" : "+" + String(sessionBubbles.length - 1)
|
|
501
598
|
})]
|
|
502
599
|
}, "primary");
|
|
503
|
-
}), sessionBubbles.length === 0 && (statusBubble !== void 0 || whisper !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
600
|
+
}), sessionBubbles.length === 0 && (statusBubble !== void 0 || whisper !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
504
601
|
className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, whisper !== void 0 && pet_module_css_default.bubbleWhisper),
|
|
505
602
|
role: "status",
|
|
506
603
|
"aria-live": "polite",
|
|
507
|
-
children: whisper
|
|
604
|
+
children: [whisper === void 0 && decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
|
|
605
|
+
decoration,
|
|
606
|
+
phase
|
|
607
|
+
}), whisper ?? statusBubble]
|
|
508
608
|
}, whisper === void 0 ? "status" : "whisper:" + whisper)]
|
|
509
609
|
}),
|
|
510
610
|
hovered && dragRef.current === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -550,7 +650,7 @@ window.__ModuleLoader__.load({
|
|
|
550
650
|
setRenaming(false);
|
|
551
651
|
}
|
|
552
652
|
},
|
|
553
|
-
children: props.t("pet.confirm")
|
|
653
|
+
children: panelLabel("confirm", props.t("pet.confirm"))
|
|
554
654
|
})]
|
|
555
655
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
556
656
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -560,29 +660,29 @@ window.__ModuleLoader__.load({
|
|
|
560
660
|
children: displayName
|
|
561
661
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
562
662
|
className: pet_module_css_default.statRank,
|
|
563
|
-
children:
|
|
663
|
+
children: panelStat("rank", "pet.rank", { rank: snapshot?.affinity.rank ?? "?" })
|
|
564
664
|
})]
|
|
565
665
|
}),
|
|
566
666
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
567
667
|
className: pet_module_css_default.rankRow,
|
|
568
668
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
569
669
|
className: pet_module_css_default.statTreats,
|
|
570
|
-
children:
|
|
670
|
+
children: panelStat("treats", "pet.treats", { n: snapshot?.treats.stocked ?? 0 })
|
|
571
671
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
572
672
|
className: pet_module_css_default.statPoints,
|
|
573
|
-
children:
|
|
673
|
+
children: panelStat("points", "pet.points", { points: snapshot?.affinity.points ?? 0 })
|
|
574
674
|
})]
|
|
575
675
|
}),
|
|
576
676
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
577
677
|
className: pet_module_css_default.actions,
|
|
578
678
|
children: [
|
|
579
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
679
|
+
panelShows("feed") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
580
680
|
type: "button",
|
|
581
681
|
className: pet_module_css_default.action,
|
|
582
682
|
onClick: props.onFeed,
|
|
583
|
-
children: props.t("pet.feed")
|
|
683
|
+
children: panelLabel("feed", props.t("pet.feed"))
|
|
584
684
|
}),
|
|
585
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
685
|
+
panelShows("rename") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
586
686
|
type: "button",
|
|
587
687
|
className: pet_module_css_default.action,
|
|
588
688
|
onClick: () => {
|
|
@@ -590,13 +690,13 @@ window.__ModuleLoader__.load({
|
|
|
590
690
|
setNameDraft(displayName);
|
|
591
691
|
setRenaming(true);
|
|
592
692
|
},
|
|
593
|
-
children: props.t("pet.rename")
|
|
693
|
+
children: panelLabel("rename", props.t("pet.rename"))
|
|
594
694
|
}),
|
|
595
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
695
|
+
panelShows("hide") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
596
696
|
type: "button",
|
|
597
697
|
className: pet_module_css_default.action,
|
|
598
698
|
onClick: props.onHide,
|
|
599
|
-
children: props.t("pet.hide")
|
|
699
|
+
children: panelLabel("hide", props.t("pet.hide"))
|
|
600
700
|
})
|
|
601
701
|
]
|
|
602
702
|
})
|
|
@@ -606,6 +706,181 @@ window.__ModuleLoader__.load({
|
|
|
606
706
|
}), document.body);
|
|
607
707
|
}
|
|
608
708
|
//#endregion
|
|
709
|
+
//#region src/client/renderers/registry.ts
|
|
710
|
+
/** Renderer dispatch table. */
|
|
711
|
+
var RendererRegistry = class {
|
|
712
|
+
renderers = /* @__PURE__ */ new Map();
|
|
713
|
+
/** Register one renderer implementation (id wins on re-register). */
|
|
714
|
+
register(renderer) {
|
|
715
|
+
this.renderers.set(renderer.id, renderer);
|
|
716
|
+
}
|
|
717
|
+
/** Whether a renderer kind is available in this build. */
|
|
718
|
+
has(id) {
|
|
719
|
+
return this.renderers.has(id);
|
|
720
|
+
}
|
|
721
|
+
/** The registered renderer kinds (for diagnostics). */
|
|
722
|
+
kinds() {
|
|
723
|
+
return [...this.renderers.keys()].sort();
|
|
724
|
+
}
|
|
725
|
+
/** Remove every registration (tests; the client index registers once). */
|
|
726
|
+
clear() {
|
|
727
|
+
this.renderers.clear();
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Mount a renderer for one activation. An unknown kind renders a clear
|
|
731
|
+
* diagnostic card into the container instead of failing silently.
|
|
732
|
+
*/
|
|
733
|
+
mount(kind, ctx, config) {
|
|
734
|
+
const renderer = this.renderers.get(kind);
|
|
735
|
+
if (renderer === void 0) {
|
|
736
|
+
const note = document.createElement("div");
|
|
737
|
+
note.dataset.dshPetRendererFallback = kind;
|
|
738
|
+
note.textContent = "Pet renderer \"" + kind + "\" is not available in this build (supported: " + this.kinds().join(", ") + ").";
|
|
739
|
+
ctx.container.appendChild(note);
|
|
740
|
+
ctx.onCleanup(() => note.remove());
|
|
741
|
+
return { dispose: () => note.remove() };
|
|
742
|
+
}
|
|
743
|
+
return renderer.mount(ctx, renderer.validateConfig(config));
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
/**
|
|
747
|
+
* The plugin-wide renderer registry. The client entry registers the
|
|
748
|
+
* built-in renderers at apply time; the renderer switch and the live2d
|
|
749
|
+
* bridge dispatch through this instance.
|
|
750
|
+
*/
|
|
751
|
+
const defaultPetRendererRegistry = new RendererRegistry();
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src/client/phase-stream.ts
|
|
754
|
+
/** Create the stream (one per pet entry lifetime, owned by the plugin body). */
|
|
755
|
+
function createPhaseStream(initial = "idle") {
|
|
756
|
+
let current = initial;
|
|
757
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
758
|
+
return {
|
|
759
|
+
get: () => current,
|
|
760
|
+
subscribe(listener) {
|
|
761
|
+
listeners.add(listener);
|
|
762
|
+
return () => {
|
|
763
|
+
listeners.delete(listener);
|
|
764
|
+
};
|
|
765
|
+
},
|
|
766
|
+
push(phase) {
|
|
767
|
+
if (phase === current) return;
|
|
768
|
+
current = phase;
|
|
769
|
+
for (const listener of [...listeners]) listener(phase);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/client/renderers/live2d/Live2dVisualMount.tsx
|
|
775
|
+
/**
|
|
776
|
+
* Live2D visual mount (pet-center M3) — the React bridge between the pet
|
|
777
|
+
* center chrome and the imperative live2d renderer. The bridge owns the
|
|
778
|
+
* contract context (asset base, phase stream, interaction write-back,
|
|
779
|
+
* activation cleanups), feeds the polled phase into the stream, forwards
|
|
780
|
+
* sub-4px taps as hit-test coordinates, and renders the localized error
|
|
781
|
+
* card when the renderer reports a fatal boot failure.
|
|
782
|
+
* @module @linxin666/dsh-pet/client/renderers/live2d/Live2dVisualMount
|
|
783
|
+
*/
|
|
784
|
+
/** Mount the live2d renderer as the sprite's visual (inside the chrome). */
|
|
785
|
+
function Live2dVisualMount(props) {
|
|
786
|
+
const containerRef = (0, react.useRef)(null);
|
|
787
|
+
const streamRef = (0, react.useRef)(null);
|
|
788
|
+
const handleRef = (0, react.useRef)(null);
|
|
789
|
+
const downRef = (0, react.useRef)(null);
|
|
790
|
+
const [error, setError] = (0, react.useState)(null);
|
|
791
|
+
(0, react.useEffect)(() => {
|
|
792
|
+
const container = containerRef.current;
|
|
793
|
+
const live2d = props.definition.live2d;
|
|
794
|
+
if (container === null || live2d === void 0) return void 0;
|
|
795
|
+
streamRef.current ??= createPhaseStream(props.phase);
|
|
796
|
+
const cleanups = [];
|
|
797
|
+
const ctx = {
|
|
798
|
+
petId: props.definition.id,
|
|
799
|
+
assetBase: "/pet/" + encodeURIComponent(props.definition.id),
|
|
800
|
+
container,
|
|
801
|
+
phase: streamRef.current,
|
|
802
|
+
interact: props.onPet,
|
|
803
|
+
onCleanup: (fn) => {
|
|
804
|
+
cleanups.push(fn);
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
let handle;
|
|
808
|
+
try {
|
|
809
|
+
handle = defaultPetRendererRegistry.mount("live2d", ctx, live2d);
|
|
810
|
+
} catch {
|
|
811
|
+
setError("load-failed");
|
|
812
|
+
return () => {
|
|
813
|
+
for (const fn of cleanups.splice(0)) fn();
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
handleRef.current = handle;
|
|
817
|
+
handle.onError?.(setError);
|
|
818
|
+
return () => {
|
|
819
|
+
handleRef.current = null;
|
|
820
|
+
for (const fn of cleanups.splice(0)) fn();
|
|
821
|
+
handle.dispose();
|
|
822
|
+
};
|
|
823
|
+
}, [props.definition]);
|
|
824
|
+
(0, react.useEffect)(() => {
|
|
825
|
+
streamRef.current?.push(props.phase);
|
|
826
|
+
}, [props.phase]);
|
|
827
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
828
|
+
ref: containerRef,
|
|
829
|
+
"data-dsh-pet-live2d": props.definition.id,
|
|
830
|
+
style: {
|
|
831
|
+
width: "100%",
|
|
832
|
+
height: "100%"
|
|
833
|
+
},
|
|
834
|
+
onPointerDown: (e) => {
|
|
835
|
+
downRef.current = {
|
|
836
|
+
x: e.clientX,
|
|
837
|
+
y: e.clientY
|
|
838
|
+
};
|
|
839
|
+
},
|
|
840
|
+
onPointerUp: (e) => {
|
|
841
|
+
const down = downRef.current;
|
|
842
|
+
downRef.current = null;
|
|
843
|
+
if (down === null) return;
|
|
844
|
+
if (Math.abs(e.clientX - down.x) > 4 || Math.abs(e.clientY - down.y) > 4) return;
|
|
845
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
846
|
+
handleRef.current?.tap(e.clientX - rect.left, e.clientY - rect.top);
|
|
847
|
+
},
|
|
848
|
+
children: error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
849
|
+
"data-dsh-pet-live2d-error": error,
|
|
850
|
+
children: error === "core-missing" ? props.t("pet.live2d.core-missing") : error === "vendor-missing" ? props.t("pet.live2d.vendor-missing") : props.t("pet.live2d.load-failed")
|
|
851
|
+
})
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/client/renderers/PetRendererSwitch.tsx
|
|
856
|
+
/**
|
|
857
|
+
* Renderer switch — the client dispatch seam of the pet center (issue #623,
|
|
858
|
+
* milestone M2 P5 / M3). The pet's manifest picks the renderer: sprite2d
|
|
859
|
+
* hands straight through to the sprite; live2d injects its visual INTO the
|
|
860
|
+
* sprite chrome (the dock, bubbles and panel belong to the pet center, not
|
|
861
|
+
* the renderer); a renderer this build cannot serve renders a clear
|
|
862
|
+
* diagnostic card instead of blanking.
|
|
863
|
+
* @module @linxin666/dsh-pet/client/renderers/PetRendererSwitch
|
|
864
|
+
*/
|
|
865
|
+
/** Dispatch one pet definition to its renderer; unknown kinds get a card. */
|
|
866
|
+
function PetRendererSwitch(props) {
|
|
867
|
+
const renderer = props.definition.renderer ?? "sprite2d";
|
|
868
|
+
if (renderer === "sprite2d") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: props.children });
|
|
869
|
+
if (renderer === "live2d" && defaultPetRendererRegistry.has("live2d") && (0, react.isValidElement)(props.children)) {
|
|
870
|
+
const visual = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Live2dVisualMount, {
|
|
871
|
+
definition: props.definition,
|
|
872
|
+
phase: props.phase,
|
|
873
|
+
onPet: props.onPet,
|
|
874
|
+
t: props.t
|
|
875
|
+
});
|
|
876
|
+
return (0, react.cloneElement)(props.children, { visual });
|
|
877
|
+
}
|
|
878
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
879
|
+
"data-dsh-pet-renderer-fallback": renderer,
|
|
880
|
+
children: props.t("pet.renderer.unavailable", { renderer })
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
//#endregion
|
|
609
884
|
//#region src/client/PetDockEntry.tsx
|
|
610
885
|
/**
|
|
611
886
|
* Global floating pet entry. The pet is host-global (its state, display and
|
|
@@ -645,19 +920,25 @@ window.__ModuleLoader__.load({
|
|
|
645
920
|
if (visible) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
646
921
|
"data-pet-dock": true,
|
|
647
922
|
"data-testid": "pet-dock",
|
|
648
|
-
children: snapshot === null || definition === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
649
|
-
snapshot,
|
|
923
|
+
children: snapshot === null || definition === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetRendererSwitch, {
|
|
650
924
|
definition,
|
|
651
|
-
|
|
652
|
-
feedback,
|
|
925
|
+
phase: snapshot?.phase ?? "idle",
|
|
653
926
|
onPet: props.pet,
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
927
|
+
t: props.t,
|
|
928
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetSprite, {
|
|
929
|
+
snapshot,
|
|
930
|
+
definition,
|
|
931
|
+
display: snapshot.display,
|
|
932
|
+
feedback,
|
|
933
|
+
onPet: props.pet,
|
|
934
|
+
onFeed: props.feed,
|
|
935
|
+
onHide: props.hide,
|
|
936
|
+
onDragEnd: props.dragEnd,
|
|
937
|
+
onRename: props.rename,
|
|
938
|
+
onOpenSession: props.openSession,
|
|
939
|
+
onFeedbackDone: props.feedbackDone,
|
|
940
|
+
t: props.t
|
|
941
|
+
})
|
|
661
942
|
})
|
|
662
943
|
});
|
|
663
944
|
const display = snapshot?.display ?? DEFAULT_DISPLAY;
|
|
@@ -677,6 +958,201 @@ window.__ModuleLoader__.load({
|
|
|
677
958
|
});
|
|
678
959
|
}
|
|
679
960
|
//#endregion
|
|
961
|
+
//#region src/contracts/renderer.ts
|
|
962
|
+
/** Contract version renderers declare against (independent of the manifest). */
|
|
963
|
+
const PET_RENDERER_API_VERSION = "x-org.linxin666.pet-center/v1alpha1";
|
|
964
|
+
//#endregion
|
|
965
|
+
//#region src/client/renderers/live2d/runtime.ts
|
|
966
|
+
/**
|
|
967
|
+
* Live2D runtime loading (pet-center M3) — the two scripts a live2d mount
|
|
968
|
+
* needs, fetched lazily through the plugin's own runtime route: the
|
|
969
|
+
* user-supplied Cubism Core (proprietary; the plugin never bundles or
|
|
970
|
+
* downloads it — issue #623 M1 §0) and the plugin-shipped MIT vendor bundle
|
|
971
|
+
* (pixi.js + untitled-pixi-live2d-engine). Each loads at most once per page;
|
|
972
|
+
* concurrent mounts share the in-flight promise, and a failure is cached as
|
|
973
|
+
* 'absent' so a broken install stops retrying the network every mount.
|
|
974
|
+
*
|
|
975
|
+
* The vendor surface below is the structural slice the renderer consumes;
|
|
976
|
+
* the real objects come from 'window.__dshPetLive2d' (lib/live2d-vendor.js),
|
|
977
|
+
* so this module never imports pixi — the client bundle stays lean.
|
|
978
|
+
* @module @linxin666/dsh-pet/client/renderers/live2d/runtime
|
|
979
|
+
*/
|
|
980
|
+
/** Runtime file URLs the host serves ('/api/pet/runtime/<name>', M3-2). */
|
|
981
|
+
const CORE_URL = "/api/pet/runtime/live2dcubismcore.min.js";
|
|
982
|
+
const VENDOR_URL = "/api/pet/runtime/live2d-vendor.js";
|
|
983
|
+
const defaultInjector = (src) => new Promise((resolve, reject) => {
|
|
984
|
+
const tag = document.createElement("script");
|
|
985
|
+
tag.src = src;
|
|
986
|
+
tag.onload = () => resolve();
|
|
987
|
+
tag.onerror = () => reject(/* @__PURE__ */ new Error("script failed to load: " + src));
|
|
988
|
+
document.head.appendChild(tag);
|
|
989
|
+
});
|
|
990
|
+
let corePromise;
|
|
991
|
+
let vendorPromise;
|
|
992
|
+
/**
|
|
993
|
+
* Ensure the Cubism Core global exists, injecting the runtime-route script
|
|
994
|
+
* once when absent. Resolves false when the user has not installed the core
|
|
995
|
+
* (a normal state — the renderer turns it into install guidance).
|
|
996
|
+
*/
|
|
997
|
+
function ensureCubismCore(probe = {}) {
|
|
998
|
+
if (typeof window !== "undefined" && window.Live2DCubismCore !== void 0) return Promise.resolve(true);
|
|
999
|
+
if (probe.inject !== void 0) return probe.inject(CORE_URL).then(() => typeof window !== "undefined" && window.Live2DCubismCore !== void 0).catch(() => false);
|
|
1000
|
+
corePromise ??= defaultInjector(CORE_URL).then(() => typeof window !== "undefined" && window.Live2DCubismCore !== void 0).catch(() => false);
|
|
1001
|
+
return corePromise;
|
|
1002
|
+
}
|
|
1003
|
+
/** Ensure the plugin vendor bundle global exists (same caching discipline). */
|
|
1004
|
+
function ensureLive2dVendor(probe = {}) {
|
|
1005
|
+
if (typeof window !== "undefined" && window.__dshPetLive2d !== void 0) return Promise.resolve(window.__dshPetLive2d);
|
|
1006
|
+
if (probe.inject !== void 0) return probe.inject(VENDOR_URL).then(() => typeof window !== "undefined" ? window.__dshPetLive2d : void 0).catch(() => void 0);
|
|
1007
|
+
vendorPromise ??= defaultInjector(VENDOR_URL).then(() => typeof window !== "undefined" ? window.__dshPetLive2d : void 0).catch(() => void 0);
|
|
1008
|
+
return vendorPromise;
|
|
1009
|
+
}
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/client/renderers/live2d.ts
|
|
1012
|
+
/** The de-facto tap-motion group of Cubism sample models. */
|
|
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" };
|
|
1022
|
+
let vendorConfigured = false;
|
|
1023
|
+
/** Configure pixi extensions + the Cubism SDK once per page. */
|
|
1024
|
+
function configureOnce(vendor) {
|
|
1025
|
+
if (vendorConfigured) return;
|
|
1026
|
+
vendorConfigured = true;
|
|
1027
|
+
vendor.extensions.add(vendor.Live2DPlugin);
|
|
1028
|
+
vendor.configureCubismSDK({ memorySizeMB: 32 });
|
|
1029
|
+
}
|
|
1030
|
+
/** Fail-closed config validation (contract: unknown manifest block in). */
|
|
1031
|
+
function validateLive2dConfig(config) {
|
|
1032
|
+
if (typeof config !== "object" || config === null) throw new Error("live2d config is not an object");
|
|
1033
|
+
const source = config;
|
|
1034
|
+
if (typeof source.modelUrl !== "string" || source.modelUrl === "") throw new Error("live2d config modelUrl is required");
|
|
1035
|
+
const motions = source.motions;
|
|
1036
|
+
if (typeof motions !== "object" || motions === null || typeof motions.idle !== "string") throw new Error("live2d config motions.idle is required");
|
|
1037
|
+
return config;
|
|
1038
|
+
}
|
|
1039
|
+
/** The live2d renderer implementation. */
|
|
1040
|
+
const live2dRenderer = {
|
|
1041
|
+
id: "live2d",
|
|
1042
|
+
apiVersion: PET_RENDERER_API_VERSION,
|
|
1043
|
+
validateConfig: validateLive2dConfig,
|
|
1044
|
+
mount(ctx, config) {
|
|
1045
|
+
let disposed = false;
|
|
1046
|
+
let app;
|
|
1047
|
+
let model;
|
|
1048
|
+
let errorListener;
|
|
1049
|
+
let unsubscribe;
|
|
1050
|
+
/** The motion group the current phase maps to (resume target after taps). */
|
|
1051
|
+
let phaseGroup = config.motions.idle;
|
|
1052
|
+
let tapPlaying = false;
|
|
1053
|
+
const playGroup = (group) => {
|
|
1054
|
+
if (model === void 0) return;
|
|
1055
|
+
const groups = model.internalModel.settings.motions ?? {};
|
|
1056
|
+
const count = Array.isArray(groups[group]) ? groups[group].length : 0;
|
|
1057
|
+
if (count === 0) {
|
|
1058
|
+
if (group !== config.motions.idle) playGroup(config.motions.idle);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const index = count > 1 ? Math.floor(Math.random() * count) : 0;
|
|
1062
|
+
model.motion(group, index);
|
|
1063
|
+
};
|
|
1064
|
+
const applyPhase = (phase) => {
|
|
1065
|
+
phaseGroup = config.motions[phase] ?? config.motions.idle;
|
|
1066
|
+
playGroup(phaseGroup);
|
|
1067
|
+
const expression = config.expressions?.[phase];
|
|
1068
|
+
if (expression !== void 0 && model !== void 0) model.expression(expression);
|
|
1069
|
+
};
|
|
1070
|
+
const boot = async () => {
|
|
1071
|
+
if (!await ensureCubismCore()) {
|
|
1072
|
+
if (!disposed) errorListener?.("core-missing");
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const vendor = await ensureLive2dVendor();
|
|
1076
|
+
if (vendor === void 0) {
|
|
1077
|
+
if (!disposed) errorListener?.("vendor-missing");
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
configureOnce(vendor);
|
|
1081
|
+
const pixiApp = new vendor.Application();
|
|
1082
|
+
await pixiApp.init({
|
|
1083
|
+
width: Math.max(1, ctx.container.clientWidth || 160),
|
|
1084
|
+
height: Math.max(1, ctx.container.clientHeight || 174),
|
|
1085
|
+
backgroundAlpha: 0,
|
|
1086
|
+
antialias: true,
|
|
1087
|
+
autoDensity: true,
|
|
1088
|
+
preference: "webgl"
|
|
1089
|
+
});
|
|
1090
|
+
if (disposed) {
|
|
1091
|
+
pixiApp.destroy(true, { children: true });
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
pixiApp.canvas.style.display = "block";
|
|
1095
|
+
pixiApp.canvas.style.width = "100%";
|
|
1096
|
+
pixiApp.canvas.style.height = "100%";
|
|
1097
|
+
ctx.container.appendChild(pixiApp.canvas);
|
|
1098
|
+
const loaded = await vendor.Live2DModel.from(config.modelUrl, {
|
|
1099
|
+
autoHitTest: false,
|
|
1100
|
+
autoFocus: false,
|
|
1101
|
+
textureOptions: TEXTURE_OPTIONS
|
|
1102
|
+
});
|
|
1103
|
+
if (disposed) {
|
|
1104
|
+
pixiApp.destroy(true, { children: true });
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
app = pixiApp;
|
|
1108
|
+
model = loaded;
|
|
1109
|
+
const fit = Math.min(pixiApp.renderer.width / Math.max(1, loaded.width), pixiApp.renderer.height / Math.max(1, loaded.height)) * .92;
|
|
1110
|
+
loaded.scale.set(fit * (config.scale ?? 1));
|
|
1111
|
+
loaded.anchor.set(.5);
|
|
1112
|
+
loaded.position.set(pixiApp.renderer.width / 2 + (config.translate?.x ?? 0), pixiApp.renderer.height / 2 + (config.translate?.y ?? 0));
|
|
1113
|
+
pixiApp.stage.addChild(loaded);
|
|
1114
|
+
loaded.on("motionFinish", () => {
|
|
1115
|
+
if (tapPlaying) {
|
|
1116
|
+
tapPlaying = false;
|
|
1117
|
+
playGroup(phaseGroup);
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
applyPhase(ctx.phase.get());
|
|
1121
|
+
unsubscribe = ctx.phase.subscribe(applyPhase);
|
|
1122
|
+
};
|
|
1123
|
+
boot().catch(() => {
|
|
1124
|
+
if (!disposed) errorListener?.("load-failed");
|
|
1125
|
+
});
|
|
1126
|
+
return {
|
|
1127
|
+
dispose() {
|
|
1128
|
+
if (disposed) return;
|
|
1129
|
+
disposed = true;
|
|
1130
|
+
unsubscribe?.();
|
|
1131
|
+
model = void 0;
|
|
1132
|
+
if (app !== void 0) {
|
|
1133
|
+
app.destroy(true, { children: true });
|
|
1134
|
+
app = void 0;
|
|
1135
|
+
}
|
|
1136
|
+
},
|
|
1137
|
+
tap(x, y) {
|
|
1138
|
+
const current = model;
|
|
1139
|
+
if (disposed || current === void 0) return;
|
|
1140
|
+
const hits = current.hitTest(x, y);
|
|
1141
|
+
const allowed = config.hitAreas;
|
|
1142
|
+
if (!(allowed === void 0 ? hits.length > 0 : hits.some((name) => allowed.includes(name)))) return;
|
|
1143
|
+
const group = (current.internalModel.settings.motions ?? {})[TAP_GROUP];
|
|
1144
|
+
if (!Array.isArray(group) || group.length === 0) return;
|
|
1145
|
+
tapPlaying = true;
|
|
1146
|
+
const index = group.length > 1 ? Math.floor(Math.random() * group.length) : 0;
|
|
1147
|
+
current.motion(TAP_GROUP, index);
|
|
1148
|
+
},
|
|
1149
|
+
onError(listener) {
|
|
1150
|
+
errorListener = listener;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
//#endregion
|
|
680
1156
|
//#region \0dsh-css:packages/dsh-pet/src/client/settings-card.module.css.mjs
|
|
681
1157
|
const css$1 = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_selectWrap{position:relative}.kKk9aW_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.kKk9aW_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.kKk9aW_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_selectChevronOpen{transform:rotate(180deg)}.kKk9aW_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.kKk9aW_selectPopupOpen{opacity:1;transform:none}.kKk9aW_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.kKk9aW_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.kKk9aW_selectOption:hover,.kKk9aW_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.kKk9aW_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save,.kKk9aW_selectChevron,.kKk9aW_selectChevronOpen,.kKk9aW_selectPopup{transition:none}}";
|
|
682
1158
|
const tagId$1 = "@linxin666/dsh-pet/settings-card.module.css";
|
|
@@ -1344,7 +1820,8 @@ window.__ModuleLoader__.load({
|
|
|
1344
1820
|
const valid = plan.filter((item) => item.run !== void 0);
|
|
1345
1821
|
if (plan.length === 0 || this.saving || valid.length !== plan.length) return;
|
|
1346
1822
|
const plannedWrites = valid.map((item) => item.op);
|
|
1347
|
-
const
|
|
1823
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1824
|
+
for (const item of plan) pending.set(item.field, this.staged.get(item.field));
|
|
1348
1825
|
this.saving = true;
|
|
1349
1826
|
this.failed = false;
|
|
1350
1827
|
this.failedReason = void 0;
|
|
@@ -1357,9 +1834,9 @@ window.__ModuleLoader__.load({
|
|
|
1357
1834
|
for (const field of result.fields) if (field.landed) landed.add(field.field);
|
|
1358
1835
|
} else this.failedReason = result.message;
|
|
1359
1836
|
} else for (const item of valid) if (await item.run()) landed.add(item.field);
|
|
1360
|
-
for (const field of
|
|
1837
|
+
for (const [field, before] of pending) if (landed.has(field) && this.staged.get(field) === before) this.staged.delete(field);
|
|
1361
1838
|
this.saving = false;
|
|
1362
|
-
this.failed = landed.size !==
|
|
1839
|
+
this.failed = landed.size !== pending.size;
|
|
1363
1840
|
this.publish();
|
|
1364
1841
|
}
|
|
1365
1842
|
/** The scope's batch surface when it supports one; undefined conservatively otherwise. */
|
|
@@ -1461,7 +1938,7 @@ window.__ModuleLoader__.load({
|
|
|
1461
1938
|
};
|
|
1462
1939
|
//#endregion
|
|
1463
1940
|
//#region \0dsh-css:packages/dsh-pet/src/client/settings-section.module.css.mjs
|
|
1464
|
-
const css = ".t0P0pa_sectionList{margin:0;padding:0;list-style:none}";
|
|
1941
|
+
const css = ".t0P0pa_sectionList{margin:0;padding:0;list-style:none}.t0P0pa_diagnostics{border:1px dashed var(--dsw-alias-border-l2);color:var(--dsw-alias-label-dimmed);border-radius:8px;margin:8px 0;padding:8px 12px;font-size:12px}.t0P0pa_diagnosticsTitle{color:var(--dsw-alias-label);margin-bottom:4px;font-weight:600;display:block}.t0P0pa_diagnostics ul{margin:0;padding-left:16px}.t0P0pa_diagnostics li[data-level=error]{color:var(--dsw-alias-error,#c04848)}";
|
|
1465
1942
|
const tagId = "@linxin666/dsh-pet/settings-section.module.css";
|
|
1466
1943
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1467
1944
|
const tag = document.createElement("style");
|
|
@@ -1470,7 +1947,11 @@ window.__ModuleLoader__.load({
|
|
|
1470
1947
|
tag.textContent = css;
|
|
1471
1948
|
document.head.appendChild(tag);
|
|
1472
1949
|
}
|
|
1473
|
-
var settings_section_module_css_default = {
|
|
1950
|
+
var settings_section_module_css_default = {
|
|
1951
|
+
"diagnostics": "t0P0pa_diagnostics",
|
|
1952
|
+
"diagnosticsTitle": "t0P0pa_diagnosticsTitle",
|
|
1953
|
+
"sectionList": "t0P0pa_sectionList"
|
|
1954
|
+
};
|
|
1474
1955
|
//#endregion
|
|
1475
1956
|
//#region src/client/PetSettingsCard.tsx
|
|
1476
1957
|
/** Fetch the registry list (the same data the sprite renders from). */
|
|
@@ -1479,18 +1960,26 @@ window.__ModuleLoader__.load({
|
|
|
1479
1960
|
if (!response.ok) throw new Error("pet pets failed: " + response.status);
|
|
1480
1961
|
return await response.json();
|
|
1481
1962
|
}
|
|
1963
|
+
/** Fetch the registry diagnostics (v1 migration hints, invalid entries). */
|
|
1964
|
+
async function fetchPetDiagnostics() {
|
|
1965
|
+
const response = await fetch("/api/pet/diagnostics");
|
|
1966
|
+
if (!response.ok) throw new Error("pet diagnostics failed: " + response.status);
|
|
1967
|
+
return (await response.json()).diagnostics ?? [];
|
|
1968
|
+
}
|
|
1482
1969
|
/** Bridges the 'pet' scope onto the card's staged form. */
|
|
1483
1970
|
var PetSettingsCardController = class {
|
|
1484
1971
|
form;
|
|
1485
1972
|
store;
|
|
1486
1973
|
petChoices = [];
|
|
1487
1974
|
petLabels = /* @__PURE__ */ new Map();
|
|
1975
|
+
diagnostics = [];
|
|
1488
1976
|
loaded = false;
|
|
1489
1977
|
attempts = 0;
|
|
1490
1978
|
/** @param scope - the bound settings scope for the 'pet' namespace. */
|
|
1491
1979
|
constructor(scope) {
|
|
1492
1980
|
this.form = new CardForm(scope, [
|
|
1493
1981
|
booleanField("enabled"),
|
|
1982
|
+
booleanField("decorationEnabled"),
|
|
1494
1983
|
booleanField("visible"),
|
|
1495
1984
|
numberField("size"),
|
|
1496
1985
|
numberField("right"),
|
|
@@ -1499,6 +1988,16 @@ window.__ModuleLoader__.load({
|
|
|
1499
1988
|
]);
|
|
1500
1989
|
this.store = this.form.bind(() => this.projection());
|
|
1501
1990
|
this.loadPets();
|
|
1991
|
+
this.loadDiagnostics();
|
|
1992
|
+
}
|
|
1993
|
+
/** Fetch registry diagnostics once (soft-fail: an empty list on error). */
|
|
1994
|
+
async loadDiagnostics() {
|
|
1995
|
+
try {
|
|
1996
|
+
this.diagnostics = await fetchPetDiagnostics();
|
|
1997
|
+
this.store.set(this.projection());
|
|
1998
|
+
} catch {
|
|
1999
|
+
this.diagnostics = [];
|
|
2000
|
+
}
|
|
1502
2001
|
}
|
|
1503
2002
|
/** Resolve the registry choices once (retried a few times on failure). */
|
|
1504
2003
|
async loadPets() {
|
|
@@ -1520,6 +2019,7 @@ window.__ModuleLoader__.load({
|
|
|
1520
2019
|
return {
|
|
1521
2020
|
...this.form.shell(),
|
|
1522
2021
|
enabled: this.form.field("enabled"),
|
|
2022
|
+
decorationEnabled: this.form.field("decorationEnabled"),
|
|
1523
2023
|
visible: this.form.field("visible"),
|
|
1524
2024
|
size: this.form.field("size"),
|
|
1525
2025
|
right: this.form.field("right"),
|
|
@@ -1528,7 +2028,8 @@ window.__ModuleLoader__.load({
|
|
|
1528
2028
|
petChoices: this.petChoices.map((id) => ({
|
|
1529
2029
|
value: id,
|
|
1530
2030
|
label: this.petLabels.get(id) ?? id
|
|
1531
|
-
}))
|
|
2031
|
+
})),
|
|
2032
|
+
petDiagnostics: this.diagnostics
|
|
1532
2033
|
};
|
|
1533
2034
|
}
|
|
1534
2035
|
/**
|
|
@@ -1589,6 +2090,22 @@ window.__ModuleLoader__.load({
|
|
|
1589
2090
|
props.resetField("enabled");
|
|
1590
2091
|
}
|
|
1591
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
|
+
}),
|
|
1592
2109
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChoiceField, {
|
|
1593
2110
|
id: "settings-pet-pet",
|
|
1594
2111
|
label: t("settings.pet"),
|
|
@@ -1604,6 +2121,17 @@ window.__ModuleLoader__.load({
|
|
|
1604
2121
|
props.resetField("petId");
|
|
1605
2122
|
}
|
|
1606
2123
|
}),
|
|
2124
|
+
state.petDiagnostics.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
2125
|
+
className: settings_section_module_css_default.diagnostics,
|
|
2126
|
+
"data-dsh-part": "diagnostics",
|
|
2127
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2128
|
+
className: settings_section_module_css_default.diagnosticsTitle,
|
|
2129
|
+
children: t("settings.diagnosticsTitle")
|
|
2130
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.petDiagnostics.map((diagnostic, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
2131
|
+
"data-level": diagnostic.level,
|
|
2132
|
+
children: diagnostic.message
|
|
2133
|
+
}, index)) })]
|
|
2134
|
+
}),
|
|
1607
2135
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
1608
2136
|
id: "settings-pet-visible",
|
|
1609
2137
|
label: t("settings.visible"),
|
|
@@ -1695,15 +2223,22 @@ window.__ModuleLoader__.load({
|
|
|
1695
2223
|
"pet.treats": "小鱼干 ×{n}",
|
|
1696
2224
|
"pet.state.loading": "宠物正在赶来…",
|
|
1697
2225
|
"pet.state.error": "宠物迷路了(连接失败)",
|
|
2226
|
+
"pet.renderer.unavailable": "这只宠物需要的渲染器({renderer})在当前版本不可用。",
|
|
2227
|
+
"pet.live2d.core-missing": "Live2D 核心未安装:请把官方 live2dcubismcore.min.js 放入 $DSH_HOME/pets/.runtime/ 后刷新(步骤见宠物插件 README)。",
|
|
2228
|
+
"pet.live2d.vendor-missing": "Live2D 组件缺失,请升级宠物插件。",
|
|
2229
|
+
"pet.live2d.load-failed": "Live2D 模型加载失败,请检查该宠物目录的完整性。",
|
|
1698
2230
|
"pet.openSessionHint": "点击跳转到对应会话",
|
|
1699
2231
|
"pet.moreSessions": "展开其余 {n} 个会话的气泡",
|
|
1700
2232
|
"pet.collapseSessions": "收起会话气泡",
|
|
1701
2233
|
"settings.title": "宠物",
|
|
2234
|
+
"settings.diagnosticsTitle": "宠物目录诊断",
|
|
1702
2235
|
"settings.description": "选择宠物并调整它的显示布局。",
|
|
1703
2236
|
"settings.pet": "宠物",
|
|
1704
2237
|
"settings.petHint": "选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。",
|
|
1705
2238
|
"settings.enabled": "启用宠物",
|
|
1706
2239
|
"settings.enabledHint": "关闭后隐藏宠物并停止轮询,可在设置里重新启用。",
|
|
2240
|
+
"settings.decoration": "状态装饰",
|
|
2241
|
+
"settings.decorationHint": "在宠物状态气泡里显示喷水鲸鱼等状态装饰;关闭后气泡只剩文字。",
|
|
1707
2242
|
"settings.visible": "显示宠物",
|
|
1708
2243
|
"settings.visibleHint": "关闭后宠物隐藏,可从聊天输入区重新召唤。",
|
|
1709
2244
|
"settings.size": "大小(px)",
|
|
@@ -1741,15 +2276,22 @@ window.__ModuleLoader__.load({
|
|
|
1741
2276
|
"pet.treats": "Treats ×{n}",
|
|
1742
2277
|
"pet.state.loading": "The pet is on its way…",
|
|
1743
2278
|
"pet.state.error": "The pet is lost (connection failed)",
|
|
2279
|
+
"pet.renderer.unavailable": "This pet needs a renderer ({renderer}) that is not available in this build.",
|
|
2280
|
+
"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).",
|
|
2281
|
+
"pet.live2d.vendor-missing": "The Live2D component is missing; please update the pet plugin.",
|
|
2282
|
+
"pet.live2d.load-failed": "The Live2D model failed to load; check the pet directory is complete.",
|
|
1744
2283
|
"pet.openSessionHint": "Click to jump to this session",
|
|
1745
2284
|
"pet.moreSessions": "Expand {n} more session bubbles",
|
|
1746
2285
|
"pet.collapseSessions": "Collapse session bubbles",
|
|
1747
2286
|
"settings.title": "Pet",
|
|
2287
|
+
"settings.diagnosticsTitle": "Pet directory diagnostics",
|
|
1748
2288
|
"settings.description": "Pick a pet and tune its display layout.",
|
|
1749
2289
|
"settings.pet": "Pet",
|
|
1750
2290
|
"settings.petHint": "Choose which pet shows. Names are stored per pet; rename from the pet hover panel.",
|
|
1751
2291
|
"settings.enabled": "Enable the pet",
|
|
1752
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.",
|
|
1753
2295
|
"settings.visible": "Show the pet",
|
|
1754
2296
|
"settings.visibleHint": "When off, the pet hides; summon it again from the input row.",
|
|
1755
2297
|
"settings.size": "Size (px)",
|
|
@@ -1842,6 +2384,7 @@ window.__ModuleLoader__.load({
|
|
|
1842
2384
|
zh,
|
|
1843
2385
|
en
|
|
1844
2386
|
}), "pet: dictionaries");
|
|
2387
|
+
defaultPetRendererRegistry.register(live2dRenderer);
|
|
1845
2388
|
const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: PET_SETTINGS_NS });
|
|
1846
2389
|
const enabled = () => {
|
|
1847
2390
|
const snapshot = settingsScope.getSnapshot();
|