@linxin666/dsh-pet 0.3.3 → 0.3.5

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 (123) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +30 -13
  3. package/README.zh.md +30 -13
  4. package/assets/ouo-neko/pet.json +31 -0
  5. package/assets/ouo-neko/previews/failed.gif +0 -0
  6. package/assets/ouo-neko/previews/idle.gif +0 -0
  7. package/assets/ouo-neko/previews/jumping.gif +0 -0
  8. package/assets/ouo-neko/previews/review.gif +0 -0
  9. package/assets/ouo-neko/previews/running-left.gif +0 -0
  10. package/assets/ouo-neko/previews/running-right.gif +0 -0
  11. package/assets/ouo-neko/previews/running.gif +0 -0
  12. package/assets/ouo-neko/previews/waiting.gif +0 -0
  13. package/assets/ouo-neko/previews/waving.gif +0 -0
  14. package/assets/ouo-neko/spritesheet.webp +0 -0
  15. package/contracts/pet-manifest-v2.schema.json +102 -1
  16. package/contracts/voice-pack-v1.schema.json +23 -28
  17. package/lib/client.js +781 -40
  18. package/lib/client.js.map +1 -1
  19. package/lib/index.js +1567 -482
  20. package/lib/types/chatter.d.ts +68 -46
  21. package/lib/types/chatter.d.ts.map +1 -1
  22. package/lib/types/chatter.js +243 -301
  23. package/lib/types/client/PetDockEntry.d.ts +3 -0
  24. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  25. package/lib/types/client/PetDockEntry.js +18 -2
  26. package/lib/types/client/PetSprite.d.ts +23 -0
  27. package/lib/types/client/PetSprite.d.ts.map +1 -1
  28. package/lib/types/client/PetSprite.js +49 -45
  29. package/lib/types/client/PluginSettingsCard.d.ts +2 -0
  30. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  31. package/lib/types/client/PluginSettingsCard.js +2 -2
  32. package/lib/types/client/drag-stream.d.ts +19 -0
  33. package/lib/types/client/drag-stream.d.ts.map +1 -0
  34. package/lib/types/client/drag-stream.js +26 -0
  35. package/lib/types/client/gameplay-hud.d.ts +50 -0
  36. package/lib/types/client/gameplay-hud.d.ts.map +1 -0
  37. package/lib/types/client/gameplay-hud.js +274 -0
  38. package/lib/types/client/index.d.ts.map +1 -1
  39. package/lib/types/client/index.js +38 -9
  40. package/lib/types/client/locales.d.ts +34 -0
  41. package/lib/types/client/locales.d.ts.map +1 -1
  42. package/lib/types/client/locales.js +36 -0
  43. package/lib/types/client/pet-store.d.ts +7 -1
  44. package/lib/types/client/pet-store.d.ts.map +1 -1
  45. package/lib/types/client/pet-store.js +4 -0
  46. package/lib/types/client/renderers/Frames2dVisualMount.d.ts +29 -0
  47. package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -0
  48. package/lib/types/client/renderers/Frames2dVisualMount.js +83 -0
  49. package/lib/types/client/renderers/PetRendererSwitch.d.ts +6 -0
  50. package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -1
  51. package/lib/types/client/renderers/PetRendererSwitch.js +14 -1
  52. package/lib/types/client/renderers/frames2d.d.ts +36 -0
  53. package/lib/types/client/renderers/frames2d.d.ts.map +1 -0
  54. package/lib/types/client/renderers/frames2d.js +196 -0
  55. package/lib/types/defaults.d.ts +6 -0
  56. package/lib/types/defaults.d.ts.map +1 -0
  57. package/lib/types/defaults.js +5 -0
  58. package/lib/types/dsh-home.d.ts.map +1 -1
  59. package/lib/types/dsh-home.js +9 -3
  60. package/lib/types/event-projection.d.ts +9 -4
  61. package/lib/types/event-projection.d.ts.map +1 -1
  62. package/lib/types/event-projection.js +46 -14
  63. package/lib/types/gameplay.d.ts +182 -0
  64. package/lib/types/gameplay.d.ts.map +1 -0
  65. package/lib/types/gameplay.js +618 -0
  66. package/lib/types/ledger.d.ts +14 -0
  67. package/lib/types/ledger.d.ts.map +1 -1
  68. package/lib/types/ledger.js +30 -0
  69. package/lib/types/manifest-v2.d.ts +38 -1
  70. package/lib/types/manifest-v2.d.ts.map +1 -1
  71. package/lib/types/manifest-v2.js +182 -2
  72. package/lib/types/persist.d.ts +4 -4
  73. package/lib/types/persist.d.ts.map +1 -1
  74. package/lib/types/persist.js +41 -4
  75. package/lib/types/registry.d.ts +29 -0
  76. package/lib/types/registry.d.ts.map +1 -1
  77. package/lib/types/registry.js +170 -1
  78. package/lib/types/routes.d.ts.map +1 -1
  79. package/lib/types/routes.js +30 -3
  80. package/lib/types/service.d.ts +63 -11
  81. package/lib/types/service.d.ts.map +1 -1
  82. package/lib/types/service.js +208 -19
  83. package/lib/types/voice-pack.d.ts +6 -7
  84. package/lib/types/voice-pack.d.ts.map +1 -1
  85. package/lib/types/voice-pack.js +44 -47
  86. package/package.json +5 -2
  87. package/src/chatter.test.ts +68 -38
  88. package/src/chatter.ts +269 -316
  89. package/src/client/PetDockEntry.test.tsx +6 -0
  90. package/src/client/PetDockEntry.tsx +32 -1
  91. package/src/client/PetSprite.test.tsx +68 -21
  92. package/src/client/PetSprite.tsx +63 -45
  93. package/src/client/PluginSettingsCard.tsx +4 -2
  94. package/src/client/drag-stream.ts +35 -0
  95. package/src/client/gameplay-hud.test.tsx +338 -0
  96. package/src/client/gameplay-hud.tsx +403 -0
  97. package/src/client/index.test.tsx +10 -1
  98. package/src/client/index.ts +46 -11
  99. package/src/client/locales.ts +36 -0
  100. package/src/client/pet-store.ts +10 -1
  101. package/src/client/pet.module.css +192 -29
  102. package/src/client/renderers/Frames2dVisualMount.tsx +111 -0
  103. package/src/client/renderers/PetRendererSwitch.tsx +28 -1
  104. package/src/client/renderers/frames2d.test.ts +137 -0
  105. package/src/client/renderers/frames2d.ts +216 -0
  106. package/src/client/settings-card.module.css +5 -0
  107. package/src/defaults.ts +7 -0
  108. package/src/dsh-home.ts +9 -3
  109. package/src/event-projection.ts +57 -16
  110. package/src/gameplay.test.ts +239 -0
  111. package/src/gameplay.ts +692 -0
  112. package/src/ledger.test.ts +16 -0
  113. package/src/ledger.ts +31 -0
  114. package/src/manifest-v2.test.ts +130 -1
  115. package/src/manifest-v2.ts +181 -2
  116. package/src/persist.test.ts +3 -0
  117. package/src/persist.ts +43 -6
  118. package/src/registry.test.ts +168 -1
  119. package/src/registry.ts +199 -1
  120. package/src/routes.ts +28 -4
  121. package/src/service.ts +240 -29
  122. package/src/voice-pack.test.ts +48 -25
  123. package/src/voice-pack.ts +50 -47
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Frames2d visual mount — the React bridge between the pet center chrome
4
+ * and the imperative frames2d renderer, mirroring the live2d mount. The
5
+ * bridge owns the contract context (asset base, phase stream, interaction
6
+ * write-back, activation cleanups), feeds the polled phase into the stream,
7
+ * forwards the chrome's drag gesture onto the conventional 'drag' track
8
+ * (when the pet declares one), and renders the localized fallback card when
9
+ * the served config is invalid.
10
+ * @module @linxin666/dsh-pet/client/renderers/Frames2dVisualMount
11
+ */
12
+ import { useEffect, useRef, useState } from 'react';
13
+ import { createPhaseStream } from "../phase-stream.js";
14
+ import { defaultPetRendererRegistry } from "./registry.js";
15
+ /** Mount the frames2d renderer as the sprite's visual (inside the chrome). */
16
+ export function Frames2dVisualMount(props) {
17
+ const containerRef = useRef(null);
18
+ const streamRef = useRef(null);
19
+ const handleRef = useRef(null);
20
+ const [invalid, setInvalid] = useState(false);
21
+ // One activation per pet definition: build the contract context and mount.
22
+ useEffect(() => {
23
+ setInvalid(false);
24
+ const container = containerRef.current;
25
+ const frames2d = props.definition.frames2d;
26
+ if (container === null || frames2d === undefined)
27
+ return undefined;
28
+ streamRef.current ??= createPhaseStream(props.phase);
29
+ const cleanups = [];
30
+ const ctx = {
31
+ petId: props.definition.id,
32
+ assetBase: '/pet/' + encodeURIComponent(props.definition.id),
33
+ container,
34
+ phase: streamRef.current,
35
+ interact: props.onPet,
36
+ onCleanup: (fn) => { cleanups.push(fn); },
37
+ };
38
+ let handle;
39
+ try {
40
+ handle = defaultPetRendererRegistry.mount('frames2d', ctx, frames2d);
41
+ }
42
+ catch {
43
+ setInvalid(true);
44
+ return () => { for (const fn of cleanups.splice(0))
45
+ fn(); };
46
+ }
47
+ handleRef.current = handle;
48
+ // The gameplay HUD steers one shared override slot through the bus;
49
+ // mode rules (work blocks drag, sleep wakes on it) keep the two
50
+ // producers from fighting over the slot.
51
+ if (props.bus !== undefined) {
52
+ const gameplayBus = props.bus;
53
+ gameplayBus.setTrack = (track) => { handleRef.current?.setState(track); };
54
+ cleanups.push(() => { gameplayBus.setTrack = undefined; });
55
+ }
56
+ // The drag gesture drives the conventional 'drag' track when declared.
57
+ // On release, a declared gameplay.dragEndState (miku: standup) plays
58
+ // once; its fallback auto-releases the override back to the phase map.
59
+ const dragTrack = props.definition.gameplay?.dragState ?? (frames2d.tracks.drag === undefined ? undefined : 'drag');
60
+ const offDrag = props.drag.subscribe((dragging) => {
61
+ if (dragTrack === undefined)
62
+ return;
63
+ if (dragging) {
64
+ handle.setState(dragTrack);
65
+ return;
66
+ }
67
+ handle.setState(props.definition.gameplay?.dragEndState);
68
+ });
69
+ cleanups.push(offDrag);
70
+ return () => {
71
+ handleRef.current = null;
72
+ for (const fn of cleanups.splice(0))
73
+ fn();
74
+ handle.dispose();
75
+ };
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- one activation per pet identity
77
+ }, [props.definition]);
78
+ // Feed the polled phase into the activation's stream (change-only).
79
+ useEffect(() => {
80
+ streamRef.current?.push(props.phase);
81
+ }, [props.phase]);
82
+ return (_jsx("div", { ref: containerRef, "data-dsh-pet-frames2d": props.definition.id, style: { width: '100%', height: '100%', pointerEvents: 'none' }, children: invalid && (_jsx("span", { "data-dsh-pet-frames2d-error": "invalid-config", children: props.t('pet.renderer.unavailable', { renderer: 'frames2d' }) })) }));
83
+ }
@@ -11,6 +11,8 @@ import { type ReactElement, type ReactNode } from 'react';
11
11
  import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
12
12
  import type { PetDefinition } from '../../registry.ts';
13
13
  import type { ActivityPhase } from '../../state.ts';
14
+ import { type DragStream } from '../drag-stream.ts';
15
+ import type { GameplayBus } from '../gameplay-hud.tsx';
14
16
  import type { NS } from '../locales.ts';
15
17
  /** Dispatch one pet definition to its renderer; unknown kinds get a card. */
16
18
  export declare function PetRendererSwitch(props: {
@@ -19,6 +21,10 @@ export declare function PetRendererSwitch(props: {
19
21
  phase: ActivityPhase;
20
22
  /** The chrome's pet interaction (affinity write-back owner). */
21
23
  onPet: () => void;
24
+ /** External drag stream (gameplay HUD shares it); created locally otherwise. */
25
+ drag?: DragStream;
26
+ /** Gameplay coordination bus forwarded to the frames2d visual mount. */
27
+ bus?: GameplayBus;
22
28
  t: PropsLocale<typeof NS>['t'];
23
29
  children?: ReactNode;
24
30
  }): ReactElement;
@@ -1 +1 @@
1
- {"version":3,"file":"PetRendererSwitch.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/PetRendererSwitch.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAgC,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAInD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAEvC,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,UAAU,EAAE,aAAa,CAAA;IACzB,wDAAwD;IACxD,KAAK,EAAE,aAAa,CAAA;IACpB,gEAAgE;IAChE,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;IAC9B,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,GAAG,YAAY,CAmBf"}
1
+ {"version":3,"file":"PetRendererSwitch.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/PetRendererSwitch.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAwC,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAC/F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAItD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAEvC,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,UAAU,EAAE,aAAa,CAAA;IACzB,wDAAwD;IACxD,KAAK,EAAE,aAAa,CAAA;IACpB,gEAAgE;IAChE,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,gFAAgF;IAChF,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,wEAAwE;IACxE,GAAG,CAAC,EAAE,WAAW,CAAA;IACjB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;IAC9B,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,GAAG,YAAY,CAuCf"}
@@ -8,14 +8,27 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
8
8
  * diagnostic card instead of blanking.
9
9
  * @module @linxin666/dsh-pet/client/renderers/PetRendererSwitch
10
10
  */
11
- import { cloneElement, isValidElement } from 'react';
11
+ import { cloneElement, isValidElement, useRef } from 'react';
12
+ import { createDragStream } from "../drag-stream.js";
12
13
  import { defaultPetRendererRegistry } from "./registry.js";
13
14
  import { Live2dVisualMount } from "./live2d/Live2dVisualMount.js";
15
+ import { Frames2dVisualMount } from "./Frames2dVisualMount.js";
14
16
  /** Dispatch one pet definition to its renderer; unknown kinds get a card. */
15
17
  export function PetRendererSwitch(props) {
16
18
  const renderer = props.definition.renderer ?? 'sprite2d';
19
+ // The frames2d drag stream lives one level up: the chrome (PetSprite)
20
+ // reports the gesture through onDraggingChange, the visual subscribes.
21
+ const dragRef = useRef(null);
22
+ if (dragRef.current === null || dragRef.current.id !== props.definition.id) {
23
+ dragRef.current = { id: props.definition.id, stream: createDragStream() };
24
+ }
25
+ const drag = props.drag ?? dragRef.current.stream;
17
26
  if (renderer === 'sprite2d')
18
27
  return _jsx(_Fragment, { children: props.children });
28
+ if (renderer === 'frames2d' && defaultPetRendererRegistry.has('frames2d') && isValidElement(props.children)) {
29
+ const visual = (_jsx(Frames2dVisualMount, { definition: props.definition, phase: props.phase, onPet: props.onPet, drag: drag, ...(props.bus === undefined ? {} : { bus: props.bus }), t: props.t }));
30
+ return cloneElement(props.children, { visual, onDraggingChange: (dragging) => drag.push(dragging) });
31
+ }
19
32
  if (renderer === 'live2d' && defaultPetRendererRegistry.has('live2d') && isValidElement(props.children)) {
20
33
  const visual = (_jsx(Live2dVisualMount, { definition: props.definition, phase: props.phase, onPet: props.onPet, t: props.t }));
21
34
  return cloneElement(props.children, { visual });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * frames2d renderer — plays the free-form named frame-sequence tracks of a
3
+ * frames2d pet (manifest v2 'frames2d' block). The pet center picks the
4
+ * track from the ActivityPhase stream (phase -> track map, idle fallback);
5
+ * the gameplay driver may force a track through the handle's setState
6
+ * override (drag/work/sleep/shop...), and a finished non-loop track settles
7
+ * into its fallback, releasing the override when the fallback matches the
8
+ * phase-mapped track. Rendering never throws: a broken track list degrades
9
+ * to the first decodable frame, and the 1.2 s stall watchdog re-kicks the
10
+ * playback chain after timer throttling.
11
+ * @module @linxin666/dsh-pet/client/renderers/frames2d
12
+ */
13
+ import { type PetRenderer, type PetRendererHandle } from '../../contracts/renderer.ts';
14
+ import type { ActivityPhase } from '../../state.ts';
15
+ /** One track as served inside the pet definition (browser URLs). */
16
+ export interface Frames2dTrackConfig {
17
+ frames: string[];
18
+ durations: number[];
19
+ loop: boolean;
20
+ fallback?: string;
21
+ }
22
+ /** The frames2d block as served inside the pet definition. */
23
+ export interface PetFrames2dConfig {
24
+ tracks: Record<string, Frames2dTrackConfig>;
25
+ phases: Partial<Record<ActivityPhase, string>> & {
26
+ idle: string;
27
+ };
28
+ }
29
+ export interface Frames2dRendererHandle extends PetRendererHandle {
30
+ /** Force a track id (gameplay override); undefined returns to phase mapping. */
31
+ setState(track: string | undefined): void;
32
+ /** The track currently playing (diagnostics and tests). */
33
+ currentTrack(): string;
34
+ }
35
+ export declare const frames2dRenderer: PetRenderer<PetFrames2dConfig>;
36
+ //# sourceMappingURL=frames2d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames2d.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/frames2d.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAA4B,KAAK,WAAW,EAA2B,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AACzI,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IAC3C,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAClE;AAED,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IACzC,2DAA2D;IAC3D,YAAY,IAAI,MAAM,CAAA;CACvB;AAoCD,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,iBAAiB,CAgJ3D,CAAA"}
@@ -0,0 +1,196 @@
1
+ /**
2
+ * frames2d renderer — plays the free-form named frame-sequence tracks of a
3
+ * frames2d pet (manifest v2 'frames2d' block). The pet center picks the
4
+ * track from the ActivityPhase stream (phase -> track map, idle fallback);
5
+ * the gameplay driver may force a track through the handle's setState
6
+ * override (drag/work/sleep/shop...), and a finished non-loop track settles
7
+ * into its fallback, releasing the override when the fallback matches the
8
+ * phase-mapped track. Rendering never throws: a broken track list degrades
9
+ * to the first decodable frame, and the 1.2 s stall watchdog re-kicks the
10
+ * playback chain after timer throttling.
11
+ * @module @linxin666/dsh-pet/client/renderers/frames2d
12
+ */
13
+ import { PET_RENDERER_API_VERSION } from "../../contracts/renderer.js";
14
+ /** Stall watchdog period: a looping track stuck longer re-kicks its chain. */
15
+ const WATCHDOG_MS = 1200;
16
+ function isRecord(value) {
17
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
18
+ }
19
+ /** Fail-closed client-side validation of the served frames2d block. */
20
+ function validateFrames2dConfig(config) {
21
+ if (!isRecord(config) || !isRecord(config.tracks) || !isRecord(config.phases)) {
22
+ throw new Error('frames2d config requires tracks and phases objects');
23
+ }
24
+ const tracks = {};
25
+ for (const [name, raw] of Object.entries(config.tracks)) {
26
+ if (!isRecord(raw) || !Array.isArray(raw.frames) || raw.frames.length === 0
27
+ || raw.frames.some(f => typeof f !== 'string' || f === '')
28
+ || !Array.isArray(raw.durations) || raw.durations.length !== raw.frames.length
29
+ || raw.durations.some(d => typeof d !== 'number' || !(d > 0))) {
30
+ throw new Error('frames2d track ' + JSON.stringify(name) + ' needs same-length frames/durations');
31
+ }
32
+ tracks[name] = {
33
+ frames: raw.frames,
34
+ durations: raw.durations,
35
+ loop: raw.loop !== false,
36
+ ...(typeof raw.fallback === 'string' ? { fallback: raw.fallback } : {}),
37
+ };
38
+ }
39
+ const phases = config.phases;
40
+ if (typeof phases.idle !== 'string' || tracks[phases.idle] === undefined) {
41
+ throw new Error('frames2d phases.idle must name an existing track');
42
+ }
43
+ return { tracks, phases: phases };
44
+ }
45
+ export const frames2dRenderer = {
46
+ id: 'frames2d',
47
+ apiVersion: PET_RENDERER_API_VERSION,
48
+ validateConfig: validateFrames2dConfig,
49
+ mount(ctx, config) {
50
+ const reducedMotion = typeof window !== 'undefined'
51
+ && typeof window.matchMedia === 'function'
52
+ && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
53
+ const img = document.createElement('img');
54
+ img.dataset.dshPetFrames2d = ctx.petId;
55
+ img.alt = '';
56
+ img.draggable = false;
57
+ img.style.width = '100%';
58
+ img.style.height = '100%';
59
+ img.style.objectFit = 'contain';
60
+ img.style.pointerEvents = 'none';
61
+ ctx.container.appendChild(img);
62
+ // Warm the browser cache for every frame up front (same-origin, small
63
+ // webp files); playback itself swaps img.src without React state.
64
+ for (const track of Object.values(config.tracks)) {
65
+ for (const url of track.frames) {
66
+ const pre = new Image();
67
+ pre.src = url;
68
+ }
69
+ }
70
+ let disposed = false;
71
+ let timer;
72
+ let watchdog;
73
+ let track = config.phases.idle;
74
+ let frameIndex = 0;
75
+ let lastAdvance = Date.now();
76
+ let override;
77
+ const trackForPhase = (phase) => {
78
+ const mapped = config.phases[phase];
79
+ return mapped !== undefined && config.tracks[mapped] !== undefined ? mapped : config.phases.idle;
80
+ };
81
+ const show = (trackId, index) => {
82
+ const def = config.tracks[trackId];
83
+ const url = def?.frames[index];
84
+ if (url !== undefined && img.getAttribute('src') !== url)
85
+ img.src = url;
86
+ };
87
+ const schedule = (ms) => {
88
+ if (disposed)
89
+ return;
90
+ timer = setTimeout(tick, ms);
91
+ };
92
+ function tick() {
93
+ if (disposed)
94
+ return;
95
+ const def = config.tracks[track];
96
+ if (def === undefined || def.frames.length === 0)
97
+ return;
98
+ lastAdvance = Date.now();
99
+ const next = frameIndex + 1;
100
+ if (next < def.frames.length) {
101
+ frameIndex = next;
102
+ show(track, frameIndex);
103
+ schedule(def.durations[frameIndex] ?? 200);
104
+ return;
105
+ }
106
+ if (def.loop) {
107
+ frameIndex = 0;
108
+ show(track, frameIndex);
109
+ schedule(def.durations[frameIndex] ?? 200);
110
+ return;
111
+ }
112
+ // Non-loop completion: settle into the fallback; when the fallback is
113
+ // what the phase map would play anyway, release the gameplay override.
114
+ const target = def.fallback !== undefined && config.tracks[def.fallback] !== undefined
115
+ ? def.fallback
116
+ : config.phases.idle;
117
+ if (target === trackForPhase(ctx.phase.get()))
118
+ override = undefined;
119
+ play(target);
120
+ }
121
+ function play(trackId) {
122
+ if (disposed)
123
+ return;
124
+ if (config.tracks[trackId] === undefined)
125
+ trackId = config.phases.idle;
126
+ if (timer !== undefined)
127
+ clearTimeout(timer);
128
+ track = trackId;
129
+ frameIndex = 0;
130
+ lastAdvance = Date.now();
131
+ show(track, frameIndex);
132
+ if (reducedMotion)
133
+ return;
134
+ const def = config.tracks[track];
135
+ schedule(def.durations[0] ?? 200);
136
+ }
137
+ const unsubscribe = ctx.phase.subscribe((phase) => {
138
+ if (override !== undefined)
139
+ return;
140
+ const target = trackForPhase(phase);
141
+ if (target !== track)
142
+ play(target);
143
+ });
144
+ // Watchdog: a throttled-away timer (background tab) leaves the chain
145
+ // dead; re-kick when a looping track has not advanced on schedule.
146
+ if (!reducedMotion) {
147
+ watchdog = setInterval(() => {
148
+ if (disposed)
149
+ return;
150
+ const def = config.tracks[track];
151
+ if (def === undefined || !def.loop)
152
+ return;
153
+ const expected = (def.durations[frameIndex] ?? 200) + WATCHDOG_MS;
154
+ if (Date.now() - lastAdvance > expected)
155
+ tick();
156
+ }, WATCHDOG_MS);
157
+ }
158
+ play(track);
159
+ let disposedOnce = false;
160
+ const dispose = () => {
161
+ if (disposedOnce)
162
+ return;
163
+ disposedOnce = true;
164
+ disposed = true;
165
+ unsubscribe();
166
+ if (timer !== undefined)
167
+ clearTimeout(timer);
168
+ if (watchdog !== undefined)
169
+ clearInterval(watchdog);
170
+ img.remove();
171
+ };
172
+ ctx.onCleanup(dispose);
173
+ return {
174
+ dispose,
175
+ setState(next) {
176
+ if (disposed)
177
+ return;
178
+ if (next === undefined) {
179
+ override = undefined;
180
+ const target = trackForPhase(ctx.phase.get());
181
+ if (target !== track)
182
+ play(target);
183
+ return;
184
+ }
185
+ if (config.tracks[next] === undefined)
186
+ return;
187
+ override = next;
188
+ if (next !== track)
189
+ play(next);
190
+ },
191
+ currentTrack() {
192
+ return track;
193
+ },
194
+ };
195
+ },
196
+ };
@@ -0,0 +1,6 @@
1
+ /** Shared defaults used by registry selection and persisted-state migration. */
2
+ /** Pet id that legacy single-pet installs and fresh registries resolve to. */
3
+ export declare const DEFAULT_PET_ID = "whale-girl";
4
+ /** Default pet name used only when a manifest carries no displayName. */
5
+ export declare const DEFAULT_PET_NAME = "\u9CB8\u9C7C\u5A18";
6
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/defaults.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,8EAA8E;AAC9E,eAAO,MAAM,cAAc,eAAe,CAAA;AAE1C,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,uBAAQ,CAAA"}
@@ -0,0 +1,5 @@
1
+ /** Shared defaults used by registry selection and persisted-state migration. */
2
+ /** Pet id that legacy single-pet installs and fresh registries resolve to. */
3
+ export const DEFAULT_PET_ID = 'whale-girl';
4
+ /** Default pet name used only when a manifest carries no displayName. */
5
+ export const DEFAULT_PET_NAME = '鲸鱼娘';
@@ -1 +1 @@
1
- {"version":3,"file":"dsh-home.d.ts","sourceRoot":"","sources":["../../src/dsh-home.ts"],"names":[],"mappings":"AACA;;;;GAIG;AAKH,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAIzE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAOrG;AAED,gEAAgE;AAChE,wBAAgB,OAAO,IAAI,MAAM,CAEhC"}
1
+ {"version":3,"file":"dsh-home.d.ts","sourceRoot":"","sources":["../../src/dsh-home.ts"],"names":[],"mappings":"AACA;;;;GAIG;AAMH,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAMzE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAUrG;AAED,gEAAgE;AAChE,wBAAgB,OAAO,IAAI,MAAM,CAEhC"}
@@ -6,12 +6,15 @@
6
6
  */
7
7
  import { homedir } from 'node:os';
8
8
  import { isAbsolute, join } from 'node:path';
9
+ import { isAbsolute as posixIsAbsolute, join as posixJoin } from 'node:path/posix';
9
10
  /** Expand a leading ~ (or ~user) in a path, platform-style. */
10
11
  export function expandHome(path, home = homedir()) {
12
+ const isPosix = home.startsWith('/');
13
+ const j = isPosix ? posixJoin : join;
11
14
  if (path === '~')
12
15
  return home;
13
16
  if (path.startsWith('~/') || path.startsWith('~\\'))
14
- return join(home, path.slice(2));
17
+ return j(home, path.slice(2));
15
18
  return path;
16
19
  }
17
20
  /**
@@ -21,12 +24,15 @@ export function expandHome(path, home = homedir()) {
21
24
  * @returns the absolute DSH home path.
22
25
  */
23
26
  export function resolveDshHome(env = process.env, home = homedir()) {
27
+ const isPosix = home.startsWith('/');
28
+ const j = isPosix ? posixJoin : join;
29
+ const isAbs = isPosix ? posixIsAbsolute : isAbsolute;
24
30
  const raw = env.DSH_HOME;
25
31
  if (raw !== undefined && raw.trim() !== '') {
26
32
  const expanded = expandHome(raw.trim(), home);
27
- return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded);
33
+ return isAbs(expanded) ? expanded : j(process.cwd(), expanded);
28
34
  }
29
- return join(home, '.dsh');
35
+ return j(home, '.dsh');
30
36
  }
31
37
  /** Resolve the DSH home directory from the live environment. */
32
38
  export function dshHome() {
@@ -5,8 +5,11 @@
5
5
  * {@link ProjectionRuntime} per session and feed events in arrival order.
6
6
  *
7
7
  * Status copy comes from the chatter voice (big rotating pools, per-tool
8
- * families, real-argument hints), and streamed model output feeds the murmur
9
- * engine so the pet can whisper its inner voice (碎碎念). The wall clock is
8
+ * families, real-argument hints), and the projection feeds the murmur engine
9
+ * (碎碎念) with the SITUATION thinking/writing during the stream, the
10
+ * running tool family on tool calls, and the STRUCTURED outcome on
11
+ * tool/result and turn/end — so the pet's inner voice always roughly knows
12
+ * what is going on and never mis-fires on output text. The wall clock is
10
13
  * injected by the caller, keeping every projection reproducible.
11
14
  * @module @linxin666/dsh-pet/event-projection
12
15
  */
@@ -22,18 +25,20 @@ export interface ActivityStatusEventLike {
22
25
  /** Per-session facts needed to project the official event stream. */
23
26
  export interface ProjectionRuntime {
24
27
  activeTools: Set<string>;
28
+ /** callIds whose tool looked like a test run, marked at tool/call (pet M6). */
29
+ testCalls: Set<string>;
25
30
  officialEventsSeen: boolean;
26
31
  stepHadFailure: boolean;
27
32
  /** Round-robin status copy voice (scene-stable, cadence-rotated). */
28
33
  voice: StatusVoice;
29
- /** Inner-whisper engine fed by the model's own streamed output. */
34
+ /** Inner-whisper engine fed by the projection's situation and outcomes. */
30
35
  whispers: WhisperEngine;
31
36
  }
32
37
  /** One official event projection, optionally carrying a completed turn reward. */
33
38
  export interface PetActivityTransition {
34
39
  input: PetStateInput;
35
40
  completedTurn?: number;
36
- /** A fresh inner whisper woken by this event's model output, when any. */
41
+ /** A fresh inner whisper woken by this event, when any. */
37
42
  whisper?: string;
38
43
  }
39
44
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"event-projection.d.ts","sourceRoot":"","sources":["../../src/event-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAe,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAE/F,2DAA2D;AAC3D,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,qEAAqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,kBAAkB,EAAE,OAAO,CAAA;IAC3B,cAAc,EAAE,OAAO,CAAA;IACvB,qEAAqE;IACrE,KAAK,EAAE,WAAW,CAAA;IAClB,mEAAmE;IACnE,QAAQ,EAAE,aAAa,CAAA;CACxB;AAED,kFAAkF;AAClF,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,CAAC,EAAE,kBAAkB,GAAG,iBAAiB,CAQpF;AAQD,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,aAAa,CAAC,OAAO,CAAC,CAE9E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,iBAAiB,EAC1B,KAAK,GAAE,MAAmB,GACzB,qBAAqB,GAAG,SAAS,CAwFnC"}
1
+ {"version":3,"file":"event-projection.d.ts","sourceRoot":"","sources":["../../src/event-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EACL,WAAW,EAGX,aAAa,EAGb,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAA;AAErB,2DAA2D;AAC3D,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,qEAAqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,+EAA+E;IAC/E,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACtB,kBAAkB,EAAE,OAAO,CAAA;IAC3B,cAAc,EAAE,OAAO,CAAA;IACvB,qEAAqE;IACrE,KAAK,EAAE,WAAW,CAAA;IAClB,2EAA2E;IAC3E,QAAQ,EAAE,aAAa,CAAA;CACxB;AAED,kFAAkF;AAClF,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,CAAC,EAAE,kBAAkB,GAAG,iBAAiB,CASpF;AAQD,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,aAAa,CAAC,OAAO,CAAC,CAE9E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,iBAAiB,EAC1B,KAAK,GAAE,MAAmB,GACzB,qBAAqB,GAAG,SAAS,CAmHnC"}
@@ -5,12 +5,15 @@
5
5
  * {@link ProjectionRuntime} per session and feed events in arrival order.
6
6
  *
7
7
  * Status copy comes from the chatter voice (big rotating pools, per-tool
8
- * families, real-argument hints), and streamed model output feeds the murmur
9
- * engine so the pet can whisper its inner voice (碎碎念). The wall clock is
8
+ * families, real-argument hints), and the projection feeds the murmur engine
9
+ * (碎碎念) with the SITUATION thinking/writing during the stream, the
10
+ * running tool family on tool calls, and the STRUCTURED outcome on
11
+ * tool/result and turn/end — so the pet's inner voice always roughly knows
12
+ * what is going on and never mis-fires on output text. The wall clock is
10
13
  * injected by the caller, keeping every projection reproducible.
11
14
  * @module @linxin666/dsh-pet/event-projection
12
15
  */
13
- import { StatusVoice, toolArgHint, WhisperEngine } from "./chatter.js";
16
+ import { StatusVoice, toolArgHint, toolCategory, WhisperEngine, whisperCategoryOf, looksLikeTestTool, } from "./chatter.js";
14
17
  /**
15
18
  * Fresh projection runtime for a newly seen session. The optional voice-pack
16
19
  * provider (pet-center M4, issue #677) hands both chatter engines their
@@ -20,6 +23,7 @@ import { StatusVoice, toolArgHint, WhisperEngine } from "./chatter.js";
20
23
  export function emptyProjectionRuntime(pools) {
21
24
  return {
22
25
  activeTools: new Set(),
26
+ testCalls: new Set(),
23
27
  officialEventsSeen: false,
24
28
  stepHadFailure: false,
25
29
  voice: new StatusVoice(pools),
@@ -44,6 +48,7 @@ export function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
44
48
  switch (event.type) {
45
49
  case 'turn/start':
46
50
  runtime.activeTools.clear();
51
+ runtime.testCalls.clear();
47
52
  runtime.stepHadFailure = false;
48
53
  return { input: { phase: 'waiting', line: runtime.voice.scene('prepare', nowMs) } };
49
54
  case 'step/start':
@@ -53,14 +58,14 @@ export function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
53
58
  case 'assistant/chunk': {
54
59
  const { chunk } = event.data;
55
60
  if (chunk.type === 'reasoning-delta' && chunk.text.length > 0) {
56
- const whisper = runtime.whispers.feed(chunk.text, nowMs);
61
+ const whisper = runtime.whispers.feed('thinking', nowMs);
57
62
  return {
58
63
  input: { phase: 'thinking', line: runtime.voice.scene('thinking', nowMs) },
59
64
  ...(whisper === undefined ? {} : { whisper }),
60
65
  };
61
66
  }
62
67
  if (chunk.type === 'text-delta' && chunk.text.length > 0) {
63
- const whisper = runtime.whispers.feed(chunk.text, nowMs);
68
+ const whisper = runtime.whispers.feed('writing', nowMs);
64
69
  return {
65
70
  input: { phase: 'review', line: runtime.voice.scene('review', nowMs) },
66
71
  ...(whisper === undefined ? {} : { whisper }),
@@ -70,40 +75,67 @@ export function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
70
75
  }
71
76
  case 'assistant/message':
72
77
  return { input: { phase: 'review', line: runtime.voice.scene('review', nowMs) } };
73
- case 'tool/call':
74
- runtime.activeTools.add(String(event.data.callId));
78
+ case 'tool/call': {
79
+ const callId = String(event.data.callId);
80
+ runtime.activeTools.add(callId);
81
+ // A test-looking call is remembered so its result (the only place the
82
+ // outcome is known) can wake the test-green mood later.
83
+ if (looksLikeTestTool(event.data.name, event.data.arguments))
84
+ runtime.testCalls.add(callId);
85
+ const whisper = runtime.whispers.feed(whisperCategoryOf(toolCategory(event.data.name)), nowMs);
75
86
  return {
76
87
  input: {
77
88
  phase: 'tool',
78
89
  line: runtime.voice.tool(event.data.name, displayToolName(event.data.name), toolArgHint(event.data.name, event.data.arguments), nowMs),
79
90
  },
91
+ ...(whisper === undefined ? {} : { whisper }),
80
92
  };
93
+ }
81
94
  case 'tool/result': {
82
95
  const block = event.data.message.content[0];
83
- runtime.activeTools.delete(String(event.data.message.source.callId));
84
- runtime.stepHadFailure ||= event.data.error !== undefined || block.isError === true;
96
+ const callId = String(event.data.message.source.callId);
97
+ const failed = event.data.error !== undefined || block.isError === true;
98
+ const wasTest = runtime.testCalls.delete(callId);
99
+ runtime.activeTools.delete(callId);
100
+ runtime.stepHadFailure ||= failed;
101
+ const whisper = failed
102
+ ? runtime.whispers.result('fail', nowMs)
103
+ : wasTest
104
+ ? runtime.whispers.result('pass', nowMs)
105
+ : undefined;
106
+ const whisperSpread = whisper === undefined ? {} : { whisper };
85
107
  if (runtime.activeTools.size > 0) {
86
108
  return {
87
109
  input: {
88
110
  phase: 'tool',
89
111
  line: runtime.voice.toolRemaining(runtime.activeTools.size, nowMs),
90
112
  },
113
+ ...whisperSpread,
91
114
  };
92
115
  }
93
116
  return runtime.stepHadFailure
94
- ? { input: { phase: 'failed', line: runtime.voice.scene('toolFailed', nowMs) } }
95
- : { input: { phase: 'thinking', line: runtime.voice.scene('toolResult', nowMs) } };
117
+ ? { input: { phase: 'failed', line: runtime.voice.scene('toolFailed', nowMs) }, ...whisperSpread }
118
+ : { input: { phase: 'thinking', line: runtime.voice.scene('toolResult', nowMs) }, ...whisperSpread };
96
119
  }
97
120
  case 'turn/end': {
98
121
  runtime.activeTools.clear();
122
+ runtime.testCalls.clear();
99
123
  switch (event.data.reason.kind) {
100
- case 'completed':
124
+ case 'completed': {
125
+ const whisper = runtime.whispers.result('done', nowMs);
101
126
  return {
102
127
  input: { phase: 'done', line: runtime.voice.scene('done', nowMs) },
103
128
  completedTurn: event.data.turn,
129
+ ...(whisper === undefined ? {} : { whisper }),
130
+ };
131
+ }
132
+ case 'error': {
133
+ const whisper = runtime.whispers.result('fail', nowMs);
134
+ return {
135
+ input: { phase: 'failed', line: runtime.voice.scene('failed', nowMs) },
136
+ ...(whisper === undefined ? {} : { whisper }),
104
137
  };
105
- case 'error':
106
- return { input: { phase: 'failed', line: runtime.voice.scene('failed', nowMs) } };
138
+ }
107
139
  case 'max-tokens':
108
140
  return { input: { phase: 'failed', line: runtime.voice.scene('maxTokens', nowMs) } };
109
141
  case 'interrupted':