@linxin666/dsh-pet 0.2.2 → 0.2.3

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 (90) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +66 -3
  3. package/README.zh.md +66 -3
  4. package/assets/whale/pet.json +169 -20
  5. package/assets/whale-refined/pet.json +17 -2
  6. package/lib/client.js +438 -21
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +752 -36
  9. package/lib/live2d-vendor.js +995 -0
  10. package/lib/live2d-vendor.js.map +1 -0
  11. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  12. package/lib/types/client/PetDockEntry.js +2 -1
  13. package/lib/types/client/PetSettingsCard.d.ts +10 -0
  14. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  15. package/lib/types/client/PetSettingsCard.js +22 -1
  16. package/lib/types/client/PetSprite.d.ts +7 -1
  17. package/lib/types/client/PetSprite.d.ts.map +1 -1
  18. package/lib/types/client/PetSprite.js +17 -8
  19. package/lib/types/client/index.d.ts.map +1 -1
  20. package/lib/types/client/index.js +5 -0
  21. package/lib/types/client/locales.d.ts +10 -0
  22. package/lib/types/client/locales.d.ts.map +1 -1
  23. package/lib/types/client/locales.js +10 -0
  24. package/lib/types/client/phase-stream.d.ts +22 -0
  25. package/lib/types/client/phase-stream.d.ts.map +1 -0
  26. package/lib/types/client/phase-stream.js +28 -0
  27. package/lib/types/client/renderers/PetRendererSwitch.d.ts +25 -0
  28. package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -0
  29. package/lib/types/client/renderers/PetRendererSwitch.js +24 -0
  30. package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts +22 -0
  31. package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -0
  32. package/lib/types/client/renderers/live2d/Live2dVisualMount.js +75 -0
  33. package/lib/types/client/renderers/live2d/runtime.d.ts +89 -0
  34. package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -0
  35. package/lib/types/client/renderers/live2d/runtime.js +63 -0
  36. package/lib/types/client/renderers/live2d/vendor-entry.d.ts +15 -0
  37. package/lib/types/client/renderers/live2d/vendor-entry.d.ts.map +1 -0
  38. package/lib/types/client/renderers/live2d/vendor-entry.js +14 -0
  39. package/lib/types/client/renderers/live2d.d.ts +52 -0
  40. package/lib/types/client/renderers/live2d.d.ts.map +1 -0
  41. package/lib/types/client/renderers/live2d.js +180 -0
  42. package/lib/types/client/renderers/registry.d.ts +32 -0
  43. package/lib/types/client/renderers/registry.d.ts.map +1 -0
  44. package/lib/types/client/renderers/registry.js +49 -0
  45. package/lib/types/contracts/renderer.d.ts +50 -0
  46. package/lib/types/contracts/renderer.d.ts.map +1 -0
  47. package/lib/types/contracts/renderer.js +15 -0
  48. package/lib/types/manifest-v2.d.ts +129 -0
  49. package/lib/types/manifest-v2.d.ts.map +1 -0
  50. package/lib/types/manifest-v2.js +387 -0
  51. package/lib/types/model3.d.ts +26 -0
  52. package/lib/types/model3.d.ts.map +1 -0
  53. package/lib/types/model3.js +96 -0
  54. package/lib/types/registry.d.ts +57 -3
  55. package/lib/types/registry.d.ts.map +1 -1
  56. package/lib/types/registry.js +208 -33
  57. package/lib/types/routes.d.ts +39 -2
  58. package/lib/types/routes.d.ts.map +1 -1
  59. package/lib/types/routes.js +183 -12
  60. package/lib/types/service.d.ts +5 -1
  61. package/lib/types/service.d.ts.map +1 -1
  62. package/lib/types/service.js +4 -0
  63. package/package.json +3 -1
  64. package/src/client/PetDockEntry.tsx +19 -11
  65. package/src/client/PetSettingsCard.tsx +39 -0
  66. package/src/client/PetSprite.test.tsx +11 -0
  67. package/src/client/PetSprite.tsx +24 -9
  68. package/src/client/index.ts +6 -0
  69. package/src/client/locales.ts +10 -0
  70. package/src/client/phase-stream.test.ts +76 -0
  71. package/src/client/phase-stream.ts +39 -0
  72. package/src/client/renderers/PetRendererSwitch.test.tsx +66 -0
  73. package/src/client/renderers/PetRendererSwitch.tsx +48 -0
  74. package/src/client/renderers/live2d/Live2dVisualMount.tsx +98 -0
  75. package/src/client/renderers/live2d/runtime.test.ts +61 -0
  76. package/src/client/renderers/live2d/runtime.ts +119 -0
  77. package/src/client/renderers/live2d/vendor-entry.ts +14 -0
  78. package/src/client/renderers/live2d.test.ts +242 -0
  79. package/src/client/renderers/live2d.ts +218 -0
  80. package/src/client/renderers/registry.ts +58 -0
  81. package/src/client/settings-section.module.css +23 -0
  82. package/src/contracts/renderer.ts +54 -0
  83. package/src/manifest-v2.test.ts +251 -0
  84. package/src/manifest-v2.ts +414 -0
  85. package/src/model3.test.ts +79 -0
  86. package/src/model3.ts +91 -0
  87. package/src/registry.test.ts +215 -2
  88. package/src/registry.ts +246 -34
  89. package/src/routes.ts +202 -13
  90. package/src/service.ts +6 -0
package/lib/client.js CHANGED
@@ -254,6 +254,7 @@ window.__ModuleLoader__.load({
254
254
  const tracks = definition.tracks;
255
255
  const sequences = definition.sequences;
256
256
  (0, react.useEffect)(() => {
257
+ if (props.visual !== void 0) return;
257
258
  let cancelled = false;
258
259
  const img = new Image();
259
260
  img.onload = () => {
@@ -264,13 +265,14 @@ window.__ModuleLoader__.load({
264
265
  cancelled = true;
265
266
  img.onload = null;
266
267
  };
267
- }, [definition.atlasUrl]);
268
+ }, [definition.atlasUrl, props.visual]);
268
269
  const spriteScale = display.size / cell.height;
269
270
  const phase = snapshot?.phase ?? "idle";
270
271
  const animation = snapshot?.animation ?? "idle";
271
272
  const scaleRef = (0, react.useRef)(spriteScale);
272
273
  scaleRef.current = spriteScale;
273
274
  (0, react.useEffect)(() => {
275
+ if (props.visual !== void 0) return;
274
276
  const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
275
277
  const sequence = animation === animationForPhase(phase) ? sequences?.[phase] : void 0;
276
278
  const leadAnimation = sequence?.[0] ?? animation;
@@ -326,7 +328,8 @@ window.__ModuleLoader__.load({
326
328
  columns,
327
329
  rows,
328
330
  tracks,
329
- sequences
331
+ sequences,
332
+ props.visual
330
333
  ]);
331
334
  const feedbackDoneRef = (0, react.useRef)(props.onFeedbackDone);
332
335
  feedbackDoneRef.current = props.onFeedbackDone;
@@ -447,10 +450,12 @@ window.__ModuleLoader__.load({
447
450
  style: {
448
451
  width: spriteWidth,
449
452
  height: spriteHeight,
450
- backgroundImage: imageReady ? "url(" + definition.atlasUrl + ")" : void 0,
451
- backgroundSize: cell.width * columns * spriteScale + "px " + cell.height * (definition.atlasRows ?? rows.length) * spriteScale + "px",
452
- backgroundRepeat: "no-repeat",
453
- backgroundPosition: "0 0",
453
+ ...props.visual === void 0 ? {
454
+ backgroundImage: imageReady ? "url(" + definition.atlasUrl + ")" : void 0,
455
+ backgroundSize: cell.width * columns * spriteScale + "px " + cell.height * (definition.atlasRows ?? rows.length) * spriteScale + "px",
456
+ backgroundRepeat: "no-repeat",
457
+ backgroundPosition: "0 0"
458
+ } : {},
454
459
  cursor: dragRef.current === null ? "grab" : "grabbing"
455
460
  },
456
461
  onPointerDown,
@@ -461,7 +466,8 @@ window.__ModuleLoader__.load({
461
466
  props.onPet();
462
467
  },
463
468
  role: "button",
464
- "aria-label": definition.displayName
469
+ "aria-label": definition.displayName,
470
+ children: props.visual
465
471
  }),
466
472
  feedback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
467
473
  ref: bubbleRef,
@@ -606,6 +612,181 @@ window.__ModuleLoader__.load({
606
612
  }), document.body);
607
613
  }
608
614
  //#endregion
615
+ //#region src/client/renderers/registry.ts
616
+ /** Renderer dispatch table. */
617
+ var RendererRegistry = class {
618
+ renderers = /* @__PURE__ */ new Map();
619
+ /** Register one renderer implementation (id wins on re-register). */
620
+ register(renderer) {
621
+ this.renderers.set(renderer.id, renderer);
622
+ }
623
+ /** Whether a renderer kind is available in this build. */
624
+ has(id) {
625
+ return this.renderers.has(id);
626
+ }
627
+ /** The registered renderer kinds (for diagnostics). */
628
+ kinds() {
629
+ return [...this.renderers.keys()].sort();
630
+ }
631
+ /** Remove every registration (tests; the client index registers once). */
632
+ clear() {
633
+ this.renderers.clear();
634
+ }
635
+ /**
636
+ * Mount a renderer for one activation. An unknown kind renders a clear
637
+ * diagnostic card into the container instead of failing silently.
638
+ */
639
+ mount(kind, ctx, config) {
640
+ const renderer = this.renderers.get(kind);
641
+ if (renderer === void 0) {
642
+ const note = document.createElement("div");
643
+ note.dataset.dshPetRendererFallback = kind;
644
+ note.textContent = "Pet renderer \"" + kind + "\" is not available in this build (supported: " + this.kinds().join(", ") + ").";
645
+ ctx.container.appendChild(note);
646
+ ctx.onCleanup(() => note.remove());
647
+ return { dispose: () => note.remove() };
648
+ }
649
+ return renderer.mount(ctx, renderer.validateConfig(config));
650
+ }
651
+ };
652
+ /**
653
+ * The plugin-wide renderer registry. The client entry registers the
654
+ * built-in renderers at apply time; the renderer switch and the live2d
655
+ * bridge dispatch through this instance.
656
+ */
657
+ const defaultPetRendererRegistry = new RendererRegistry();
658
+ //#endregion
659
+ //#region src/client/phase-stream.ts
660
+ /** Create the stream (one per pet entry lifetime, owned by the plugin body). */
661
+ function createPhaseStream(initial = "idle") {
662
+ let current = initial;
663
+ const listeners = /* @__PURE__ */ new Set();
664
+ return {
665
+ get: () => current,
666
+ subscribe(listener) {
667
+ listeners.add(listener);
668
+ return () => {
669
+ listeners.delete(listener);
670
+ };
671
+ },
672
+ push(phase) {
673
+ if (phase === current) return;
674
+ current = phase;
675
+ for (const listener of [...listeners]) listener(phase);
676
+ }
677
+ };
678
+ }
679
+ //#endregion
680
+ //#region src/client/renderers/live2d/Live2dVisualMount.tsx
681
+ /**
682
+ * Live2D visual mount (pet-center M3) — the React bridge between the pet
683
+ * center chrome and the imperative live2d renderer. The bridge owns the
684
+ * contract context (asset base, phase stream, interaction write-back,
685
+ * activation cleanups), feeds the polled phase into the stream, forwards
686
+ * sub-4px taps as hit-test coordinates, and renders the localized error
687
+ * card when the renderer reports a fatal boot failure.
688
+ * @module @linxin666/dsh-pet/client/renderers/live2d/Live2dVisualMount
689
+ */
690
+ /** Mount the live2d renderer as the sprite's visual (inside the chrome). */
691
+ function Live2dVisualMount(props) {
692
+ const containerRef = (0, react.useRef)(null);
693
+ const streamRef = (0, react.useRef)(null);
694
+ const handleRef = (0, react.useRef)(null);
695
+ const downRef = (0, react.useRef)(null);
696
+ const [error, setError] = (0, react.useState)(null);
697
+ (0, react.useEffect)(() => {
698
+ const container = containerRef.current;
699
+ const live2d = props.definition.live2d;
700
+ if (container === null || live2d === void 0) return void 0;
701
+ streamRef.current ??= createPhaseStream(props.phase);
702
+ const cleanups = [];
703
+ const ctx = {
704
+ petId: props.definition.id,
705
+ assetBase: "/pet/" + encodeURIComponent(props.definition.id),
706
+ container,
707
+ phase: streamRef.current,
708
+ interact: props.onPet,
709
+ onCleanup: (fn) => {
710
+ cleanups.push(fn);
711
+ }
712
+ };
713
+ let handle;
714
+ try {
715
+ handle = defaultPetRendererRegistry.mount("live2d", ctx, live2d);
716
+ } catch {
717
+ setError("load-failed");
718
+ return () => {
719
+ for (const fn of cleanups.splice(0)) fn();
720
+ };
721
+ }
722
+ handleRef.current = handle;
723
+ handle.onError?.(setError);
724
+ return () => {
725
+ handleRef.current = null;
726
+ for (const fn of cleanups.splice(0)) fn();
727
+ handle.dispose();
728
+ };
729
+ }, [props.definition]);
730
+ (0, react.useEffect)(() => {
731
+ streamRef.current?.push(props.phase);
732
+ }, [props.phase]);
733
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
734
+ ref: containerRef,
735
+ "data-dsh-pet-live2d": props.definition.id,
736
+ style: {
737
+ width: "100%",
738
+ height: "100%"
739
+ },
740
+ onPointerDown: (e) => {
741
+ downRef.current = {
742
+ x: e.clientX,
743
+ y: e.clientY
744
+ };
745
+ },
746
+ onPointerUp: (e) => {
747
+ const down = downRef.current;
748
+ downRef.current = null;
749
+ if (down === null) return;
750
+ if (Math.abs(e.clientX - down.x) > 4 || Math.abs(e.clientY - down.y) > 4) return;
751
+ const rect = e.currentTarget.getBoundingClientRect();
752
+ handleRef.current?.tap(e.clientX - rect.left, e.clientY - rect.top);
753
+ },
754
+ children: error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
755
+ "data-dsh-pet-live2d-error": error,
756
+ 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")
757
+ })
758
+ });
759
+ }
760
+ //#endregion
761
+ //#region src/client/renderers/PetRendererSwitch.tsx
762
+ /**
763
+ * Renderer switch — the client dispatch seam of the pet center (issue #623,
764
+ * milestone M2 P5 / M3). The pet's manifest picks the renderer: sprite2d
765
+ * hands straight through to the sprite; live2d injects its visual INTO the
766
+ * sprite chrome (the dock, bubbles and panel belong to the pet center, not
767
+ * the renderer); a renderer this build cannot serve renders a clear
768
+ * diagnostic card instead of blanking.
769
+ * @module @linxin666/dsh-pet/client/renderers/PetRendererSwitch
770
+ */
771
+ /** Dispatch one pet definition to its renderer; unknown kinds get a card. */
772
+ function PetRendererSwitch(props) {
773
+ const renderer = props.definition.renderer ?? "sprite2d";
774
+ if (renderer === "sprite2d") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: props.children });
775
+ if (renderer === "live2d" && defaultPetRendererRegistry.has("live2d") && (0, react.isValidElement)(props.children)) {
776
+ const visual = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Live2dVisualMount, {
777
+ definition: props.definition,
778
+ phase: props.phase,
779
+ onPet: props.onPet,
780
+ t: props.t
781
+ });
782
+ return (0, react.cloneElement)(props.children, { visual });
783
+ }
784
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
785
+ "data-dsh-pet-renderer-fallback": renderer,
786
+ children: props.t("pet.renderer.unavailable", { renderer })
787
+ });
788
+ }
789
+ //#endregion
609
790
  //#region src/client/PetDockEntry.tsx
610
791
  /**
611
792
  * Global floating pet entry. The pet is host-global (its state, display and
@@ -645,19 +826,25 @@ window.__ModuleLoader__.load({
645
826
  if (visible) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
646
827
  "data-pet-dock": true,
647
828
  "data-testid": "pet-dock",
648
- children: snapshot === null || definition === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetSprite, {
649
- snapshot,
829
+ children: snapshot === null || definition === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetRendererSwitch, {
650
830
  definition,
651
- display: snapshot.display,
652
- feedback,
831
+ phase: snapshot?.phase ?? "idle",
653
832
  onPet: props.pet,
654
- onFeed: props.feed,
655
- onHide: props.hide,
656
- onDragEnd: props.dragEnd,
657
- onRename: props.rename,
658
- onOpenSession: props.openSession,
659
- onFeedbackDone: props.feedbackDone,
660
- t: props.t
833
+ t: props.t,
834
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetSprite, {
835
+ snapshot,
836
+ definition,
837
+ display: snapshot.display,
838
+ feedback,
839
+ onPet: props.pet,
840
+ onFeed: props.feed,
841
+ onHide: props.hide,
842
+ onDragEnd: props.dragEnd,
843
+ onRename: props.rename,
844
+ onOpenSession: props.openSession,
845
+ onFeedbackDone: props.feedbackDone,
846
+ t: props.t
847
+ })
661
848
  })
662
849
  });
663
850
  const display = snapshot?.display ?? DEFAULT_DISPLAY;
@@ -677,6 +864,192 @@ window.__ModuleLoader__.load({
677
864
  });
678
865
  }
679
866
  //#endregion
867
+ //#region src/contracts/renderer.ts
868
+ /** Contract version renderers declare against (independent of the manifest). */
869
+ const PET_RENDERER_API_VERSION = "x-org.linxin666.pet-center/v1alpha1";
870
+ //#endregion
871
+ //#region src/client/renderers/live2d/runtime.ts
872
+ /**
873
+ * Live2D runtime loading (pet-center M3) — the two scripts a live2d mount
874
+ * needs, fetched lazily through the plugin's own runtime route: the
875
+ * user-supplied Cubism Core (proprietary; the plugin never bundles or
876
+ * downloads it — issue #623 M1 §0) and the plugin-shipped MIT vendor bundle
877
+ * (pixi.js + untitled-pixi-live2d-engine). Each loads at most once per page;
878
+ * concurrent mounts share the in-flight promise, and a failure is cached as
879
+ * 'absent' so a broken install stops retrying the network every mount.
880
+ *
881
+ * The vendor surface below is the structural slice the renderer consumes;
882
+ * the real objects come from 'window.__dshPetLive2d' (lib/live2d-vendor.js),
883
+ * so this module never imports pixi — the client bundle stays lean.
884
+ * @module @linxin666/dsh-pet/client/renderers/live2d/runtime
885
+ */
886
+ /** Runtime file URLs the host serves ('/api/pet/runtime/<name>', M3-2). */
887
+ const CORE_URL = "/api/pet/runtime/live2dcubismcore.min.js";
888
+ const VENDOR_URL = "/api/pet/runtime/live2d-vendor.js";
889
+ const defaultInjector = (src) => new Promise((resolve, reject) => {
890
+ const tag = document.createElement("script");
891
+ tag.src = src;
892
+ tag.onload = () => resolve();
893
+ tag.onerror = () => reject(/* @__PURE__ */ new Error("script failed to load: " + src));
894
+ document.head.appendChild(tag);
895
+ });
896
+ let corePromise;
897
+ let vendorPromise;
898
+ /**
899
+ * Ensure the Cubism Core global exists, injecting the runtime-route script
900
+ * once when absent. Resolves false when the user has not installed the core
901
+ * (a normal state — the renderer turns it into install guidance).
902
+ */
903
+ function ensureCubismCore(probe = {}) {
904
+ if (typeof window !== "undefined" && window.Live2DCubismCore !== void 0) return Promise.resolve(true);
905
+ if (probe.inject !== void 0) return probe.inject(CORE_URL).then(() => typeof window !== "undefined" && window.Live2DCubismCore !== void 0).catch(() => false);
906
+ corePromise ??= defaultInjector(CORE_URL).then(() => typeof window !== "undefined" && window.Live2DCubismCore !== void 0).catch(() => false);
907
+ return corePromise;
908
+ }
909
+ /** Ensure the plugin vendor bundle global exists (same caching discipline). */
910
+ function ensureLive2dVendor(probe = {}) {
911
+ if (typeof window !== "undefined" && window.__dshPetLive2d !== void 0) return Promise.resolve(window.__dshPetLive2d);
912
+ if (probe.inject !== void 0) return probe.inject(VENDOR_URL).then(() => typeof window !== "undefined" ? window.__dshPetLive2d : void 0).catch(() => void 0);
913
+ vendorPromise ??= defaultInjector(VENDOR_URL).then(() => typeof window !== "undefined" ? window.__dshPetLive2d : void 0).catch(() => void 0);
914
+ return vendorPromise;
915
+ }
916
+ //#endregion
917
+ //#region src/client/renderers/live2d.ts
918
+ /** The de-facto tap-motion group of Cubism sample models. */
919
+ const TAP_GROUP = "TapBody";
920
+ let vendorConfigured = false;
921
+ /** Configure pixi extensions + the Cubism SDK once per page. */
922
+ function configureOnce(vendor) {
923
+ if (vendorConfigured) return;
924
+ vendorConfigured = true;
925
+ vendor.extensions.add(vendor.Live2DPlugin);
926
+ vendor.configureCubismSDK({ memorySizeMB: 32 });
927
+ }
928
+ /** Fail-closed config validation (contract: unknown manifest block in). */
929
+ function validateLive2dConfig(config) {
930
+ if (typeof config !== "object" || config === null) throw new Error("live2d config is not an object");
931
+ const source = config;
932
+ if (typeof source.modelUrl !== "string" || source.modelUrl === "") throw new Error("live2d config modelUrl is required");
933
+ const motions = source.motions;
934
+ if (typeof motions !== "object" || motions === null || typeof motions.idle !== "string") throw new Error("live2d config motions.idle is required");
935
+ return config;
936
+ }
937
+ /** The live2d renderer implementation. */
938
+ const live2dRenderer = {
939
+ id: "live2d",
940
+ apiVersion: PET_RENDERER_API_VERSION,
941
+ validateConfig: validateLive2dConfig,
942
+ mount(ctx, config) {
943
+ let disposed = false;
944
+ let app;
945
+ let model;
946
+ let errorListener;
947
+ let unsubscribe;
948
+ /** The motion group the current phase maps to (resume target after taps). */
949
+ let phaseGroup = config.motions.idle;
950
+ let tapPlaying = false;
951
+ const playGroup = (group) => {
952
+ if (model === void 0) return;
953
+ const groups = model.internalModel.settings.motions ?? {};
954
+ const count = Array.isArray(groups[group]) ? groups[group].length : 0;
955
+ if (count === 0) {
956
+ if (group !== config.motions.idle) playGroup(config.motions.idle);
957
+ return;
958
+ }
959
+ const index = count > 1 ? Math.floor(Math.random() * count) : 0;
960
+ model.motion(group, index);
961
+ };
962
+ const applyPhase = (phase) => {
963
+ phaseGroup = config.motions[phase] ?? config.motions.idle;
964
+ playGroup(phaseGroup);
965
+ const expression = config.expressions?.[phase];
966
+ if (expression !== void 0 && model !== void 0) model.expression(expression);
967
+ };
968
+ const boot = async () => {
969
+ if (!await ensureCubismCore()) {
970
+ if (!disposed) errorListener?.("core-missing");
971
+ return;
972
+ }
973
+ const vendor = await ensureLive2dVendor();
974
+ if (vendor === void 0) {
975
+ if (!disposed) errorListener?.("vendor-missing");
976
+ return;
977
+ }
978
+ configureOnce(vendor);
979
+ const pixiApp = new vendor.Application();
980
+ await pixiApp.init({
981
+ width: Math.max(1, ctx.container.clientWidth || 160),
982
+ height: Math.max(1, ctx.container.clientHeight || 174),
983
+ backgroundAlpha: 0,
984
+ antialias: true,
985
+ autoDensity: true,
986
+ preference: "webgl"
987
+ });
988
+ if (disposed) {
989
+ pixiApp.destroy(true, { children: true });
990
+ return;
991
+ }
992
+ pixiApp.canvas.style.display = "block";
993
+ pixiApp.canvas.style.width = "100%";
994
+ pixiApp.canvas.style.height = "100%";
995
+ ctx.container.appendChild(pixiApp.canvas);
996
+ const loaded = await vendor.Live2DModel.from(config.modelUrl, {
997
+ autoHitTest: false,
998
+ autoFocus: false
999
+ });
1000
+ if (disposed) {
1001
+ pixiApp.destroy(true, { children: true });
1002
+ return;
1003
+ }
1004
+ app = pixiApp;
1005
+ model = loaded;
1006
+ const fit = Math.min(pixiApp.renderer.width / Math.max(1, loaded.width), pixiApp.renderer.height / Math.max(1, loaded.height)) * .92;
1007
+ loaded.scale.set(fit * (config.scale ?? 1));
1008
+ loaded.anchor.set(.5);
1009
+ loaded.position.set(pixiApp.renderer.width / 2 + (config.translate?.x ?? 0), pixiApp.renderer.height / 2 + (config.translate?.y ?? 0));
1010
+ pixiApp.stage.addChild(loaded);
1011
+ loaded.on("motionFinish", () => {
1012
+ if (tapPlaying) {
1013
+ tapPlaying = false;
1014
+ playGroup(phaseGroup);
1015
+ }
1016
+ });
1017
+ applyPhase(ctx.phase.get());
1018
+ unsubscribe = ctx.phase.subscribe(applyPhase);
1019
+ };
1020
+ boot().catch(() => {
1021
+ if (!disposed) errorListener?.("load-failed");
1022
+ });
1023
+ return {
1024
+ dispose() {
1025
+ if (disposed) return;
1026
+ disposed = true;
1027
+ unsubscribe?.();
1028
+ model = void 0;
1029
+ if (app !== void 0) {
1030
+ app.destroy(true, { children: true });
1031
+ app = void 0;
1032
+ }
1033
+ },
1034
+ tap(x, y) {
1035
+ const current = model;
1036
+ if (disposed || current === void 0) return;
1037
+ const hits = current.hitTest(x, y);
1038
+ const allowed = config.hitAreas;
1039
+ if (!(allowed === void 0 ? hits.length > 0 : hits.some((name) => allowed.includes(name)))) return;
1040
+ const group = (current.internalModel.settings.motions ?? {})[TAP_GROUP];
1041
+ if (!Array.isArray(group) || group.length === 0) return;
1042
+ tapPlaying = true;
1043
+ const index = group.length > 1 ? Math.floor(Math.random() * group.length) : 0;
1044
+ current.motion(TAP_GROUP, index);
1045
+ },
1046
+ onError(listener) {
1047
+ errorListener = listener;
1048
+ }
1049
+ };
1050
+ }
1051
+ };
1052
+ //#endregion
680
1053
  //#region \0dsh-css:packages/dsh-pet/src/client/settings-card.module.css.mjs
681
1054
  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
1055
  const tagId$1 = "@linxin666/dsh-pet/settings-card.module.css";
@@ -1461,7 +1834,7 @@ window.__ModuleLoader__.load({
1461
1834
  };
1462
1835
  //#endregion
1463
1836
  //#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}";
1837
+ 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
1838
  const tagId = "@linxin666/dsh-pet/settings-section.module.css";
1466
1839
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1467
1840
  const tag = document.createElement("style");
@@ -1470,7 +1843,11 @@ window.__ModuleLoader__.load({
1470
1843
  tag.textContent = css;
1471
1844
  document.head.appendChild(tag);
1472
1845
  }
1473
- var settings_section_module_css_default = { "sectionList": "t0P0pa_sectionList" };
1846
+ var settings_section_module_css_default = {
1847
+ "diagnostics": "t0P0pa_diagnostics",
1848
+ "diagnosticsTitle": "t0P0pa_diagnosticsTitle",
1849
+ "sectionList": "t0P0pa_sectionList"
1850
+ };
1474
1851
  //#endregion
1475
1852
  //#region src/client/PetSettingsCard.tsx
1476
1853
  /** Fetch the registry list (the same data the sprite renders from). */
@@ -1479,12 +1856,19 @@ window.__ModuleLoader__.load({
1479
1856
  if (!response.ok) throw new Error("pet pets failed: " + response.status);
1480
1857
  return await response.json();
1481
1858
  }
1859
+ /** Fetch the registry diagnostics (v1 migration hints, invalid entries). */
1860
+ async function fetchPetDiagnostics() {
1861
+ const response = await fetch("/api/pet/diagnostics");
1862
+ if (!response.ok) throw new Error("pet diagnostics failed: " + response.status);
1863
+ return (await response.json()).diagnostics ?? [];
1864
+ }
1482
1865
  /** Bridges the 'pet' scope onto the card's staged form. */
1483
1866
  var PetSettingsCardController = class {
1484
1867
  form;
1485
1868
  store;
1486
1869
  petChoices = [];
1487
1870
  petLabels = /* @__PURE__ */ new Map();
1871
+ diagnostics = [];
1488
1872
  loaded = false;
1489
1873
  attempts = 0;
1490
1874
  /** @param scope - the bound settings scope for the 'pet' namespace. */
@@ -1499,6 +1883,16 @@ window.__ModuleLoader__.load({
1499
1883
  ]);
1500
1884
  this.store = this.form.bind(() => this.projection());
1501
1885
  this.loadPets();
1886
+ this.loadDiagnostics();
1887
+ }
1888
+ /** Fetch registry diagnostics once (soft-fail: an empty list on error). */
1889
+ async loadDiagnostics() {
1890
+ try {
1891
+ this.diagnostics = await fetchPetDiagnostics();
1892
+ this.store.set(this.projection());
1893
+ } catch {
1894
+ this.diagnostics = [];
1895
+ }
1502
1896
  }
1503
1897
  /** Resolve the registry choices once (retried a few times on failure). */
1504
1898
  async loadPets() {
@@ -1528,7 +1922,8 @@ window.__ModuleLoader__.load({
1528
1922
  petChoices: this.petChoices.map((id) => ({
1529
1923
  value: id,
1530
1924
  label: this.petLabels.get(id) ?? id
1531
- }))
1925
+ })),
1926
+ petDiagnostics: this.diagnostics
1532
1927
  };
1533
1928
  }
1534
1929
  /**
@@ -1604,6 +1999,17 @@ window.__ModuleLoader__.load({
1604
1999
  props.resetField("petId");
1605
2000
  }
1606
2001
  }),
2002
+ state.petDiagnostics.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2003
+ className: settings_section_module_css_default.diagnostics,
2004
+ "data-dsh-part": "diagnostics",
2005
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2006
+ className: settings_section_module_css_default.diagnosticsTitle,
2007
+ children: t("settings.diagnosticsTitle")
2008
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.petDiagnostics.map((diagnostic, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
2009
+ "data-level": diagnostic.level,
2010
+ children: diagnostic.message
2011
+ }, index)) })]
2012
+ }),
1607
2013
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
1608
2014
  id: "settings-pet-visible",
1609
2015
  label: t("settings.visible"),
@@ -1695,10 +2101,15 @@ window.__ModuleLoader__.load({
1695
2101
  "pet.treats": "小鱼干 ×{n}",
1696
2102
  "pet.state.loading": "宠物正在赶来…",
1697
2103
  "pet.state.error": "宠物迷路了(连接失败)",
2104
+ "pet.renderer.unavailable": "这只宠物需要的渲染器({renderer})在当前版本不可用。",
2105
+ "pet.live2d.core-missing": "Live2D 核心未安装:请把官方 live2dcubismcore.min.js 放入 $DSH_HOME/pets/.runtime/ 后刷新(步骤见宠物插件 README)。",
2106
+ "pet.live2d.vendor-missing": "Live2D 组件缺失,请升级宠物插件。",
2107
+ "pet.live2d.load-failed": "Live2D 模型加载失败,请检查该宠物目录的完整性。",
1698
2108
  "pet.openSessionHint": "点击跳转到对应会话",
1699
2109
  "pet.moreSessions": "展开其余 {n} 个会话的气泡",
1700
2110
  "pet.collapseSessions": "收起会话气泡",
1701
2111
  "settings.title": "宠物",
2112
+ "settings.diagnosticsTitle": "宠物目录诊断",
1702
2113
  "settings.description": "选择宠物并调整它的显示布局。",
1703
2114
  "settings.pet": "宠物",
1704
2115
  "settings.petHint": "选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。",
@@ -1741,10 +2152,15 @@ window.__ModuleLoader__.load({
1741
2152
  "pet.treats": "Treats ×{n}",
1742
2153
  "pet.state.loading": "The pet is on its way…",
1743
2154
  "pet.state.error": "The pet is lost (connection failed)",
2155
+ "pet.renderer.unavailable": "This pet needs a renderer ({renderer}) that is not available in this build.",
2156
+ "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).",
2157
+ "pet.live2d.vendor-missing": "The Live2D component is missing; please update the pet plugin.",
2158
+ "pet.live2d.load-failed": "The Live2D model failed to load; check the pet directory is complete.",
1744
2159
  "pet.openSessionHint": "Click to jump to this session",
1745
2160
  "pet.moreSessions": "Expand {n} more session bubbles",
1746
2161
  "pet.collapseSessions": "Collapse session bubbles",
1747
2162
  "settings.title": "Pet",
2163
+ "settings.diagnosticsTitle": "Pet directory diagnostics",
1748
2164
  "settings.description": "Pick a pet and tune its display layout.",
1749
2165
  "settings.pet": "Pet",
1750
2166
  "settings.petHint": "Choose which pet shows. Names are stored per pet; rename from the pet hover panel.",
@@ -1842,6 +2258,7 @@ window.__ModuleLoader__.load({
1842
2258
  zh,
1843
2259
  en
1844
2260
  }), "pet: dictionaries");
2261
+ defaultPetRendererRegistry.register(live2dRenderer);
1845
2262
  const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: PET_SETTINGS_NS });
1846
2263
  const enabled = () => {
1847
2264
  const snapshot = settingsScope.getSnapshot();