@linxin666/dsh-pet 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +17 -3
  3. package/README.zh.md +17 -3
  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/lib/client.js +759 -26
  17. package/lib/client.js.map +1 -1
  18. package/lib/index.js +1224 -22
  19. package/lib/types/client/PetDockEntry.d.ts +3 -0
  20. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  21. package/lib/types/client/PetDockEntry.js +18 -2
  22. package/lib/types/client/PetSprite.d.ts +23 -0
  23. package/lib/types/client/PetSprite.d.ts.map +1 -1
  24. package/lib/types/client/PetSprite.js +34 -29
  25. package/lib/types/client/PluginSettingsCard.d.ts +2 -0
  26. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  27. package/lib/types/client/PluginSettingsCard.js +2 -2
  28. package/lib/types/client/drag-stream.d.ts +19 -0
  29. package/lib/types/client/drag-stream.d.ts.map +1 -0
  30. package/lib/types/client/drag-stream.js +26 -0
  31. package/lib/types/client/gameplay-hud.d.ts +50 -0
  32. package/lib/types/client/gameplay-hud.d.ts.map +1 -0
  33. package/lib/types/client/gameplay-hud.js +274 -0
  34. package/lib/types/client/index.d.ts.map +1 -1
  35. package/lib/types/client/index.js +12 -0
  36. package/lib/types/client/locales.d.ts +34 -0
  37. package/lib/types/client/locales.d.ts.map +1 -1
  38. package/lib/types/client/locales.js +36 -0
  39. package/lib/types/client/pet-store.d.ts +7 -1
  40. package/lib/types/client/pet-store.d.ts.map +1 -1
  41. package/lib/types/client/pet-store.js +4 -0
  42. package/lib/types/client/renderers/Frames2dVisualMount.d.ts +29 -0
  43. package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -0
  44. package/lib/types/client/renderers/Frames2dVisualMount.js +83 -0
  45. package/lib/types/client/renderers/PetRendererSwitch.d.ts +6 -0
  46. package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -1
  47. package/lib/types/client/renderers/PetRendererSwitch.js +14 -1
  48. package/lib/types/client/renderers/frames2d.d.ts +36 -0
  49. package/lib/types/client/renderers/frames2d.d.ts.map +1 -0
  50. package/lib/types/client/renderers/frames2d.js +196 -0
  51. package/lib/types/defaults.d.ts +6 -0
  52. package/lib/types/defaults.d.ts.map +1 -0
  53. package/lib/types/defaults.js +5 -0
  54. package/lib/types/dsh-home.d.ts.map +1 -1
  55. package/lib/types/dsh-home.js +9 -3
  56. package/lib/types/gameplay.d.ts +182 -0
  57. package/lib/types/gameplay.d.ts.map +1 -0
  58. package/lib/types/gameplay.js +618 -0
  59. package/lib/types/ledger.d.ts +14 -0
  60. package/lib/types/ledger.d.ts.map +1 -1
  61. package/lib/types/ledger.js +30 -0
  62. package/lib/types/manifest-v2.d.ts +38 -1
  63. package/lib/types/manifest-v2.d.ts.map +1 -1
  64. package/lib/types/manifest-v2.js +182 -2
  65. package/lib/types/persist.d.ts +4 -4
  66. package/lib/types/persist.d.ts.map +1 -1
  67. package/lib/types/persist.js +41 -4
  68. package/lib/types/registry.d.ts +29 -0
  69. package/lib/types/registry.d.ts.map +1 -1
  70. package/lib/types/registry.js +170 -1
  71. package/lib/types/routes.d.ts.map +1 -1
  72. package/lib/types/routes.js +22 -0
  73. package/lib/types/service.d.ts +56 -0
  74. package/lib/types/service.d.ts.map +1 -1
  75. package/lib/types/service.js +181 -0
  76. package/package.json +5 -2
  77. package/src/client/PetDockEntry.test.tsx +6 -0
  78. package/src/client/PetDockEntry.tsx +32 -1
  79. package/src/client/PetSprite.test.tsx +36 -6
  80. package/src/client/PetSprite.tsx +42 -21
  81. package/src/client/PluginSettingsCard.tsx +4 -2
  82. package/src/client/drag-stream.ts +35 -0
  83. package/src/client/gameplay-hud.test.tsx +338 -0
  84. package/src/client/gameplay-hud.tsx +403 -0
  85. package/src/client/index.ts +17 -1
  86. package/src/client/locales.ts +36 -0
  87. package/src/client/pet-store.ts +10 -1
  88. package/src/client/pet.module.css +192 -29
  89. package/src/client/renderers/Frames2dVisualMount.tsx +111 -0
  90. package/src/client/renderers/PetRendererSwitch.tsx +28 -1
  91. package/src/client/renderers/frames2d.test.ts +137 -0
  92. package/src/client/renderers/frames2d.ts +216 -0
  93. package/src/client/settings-card.module.css +5 -0
  94. package/src/defaults.ts +7 -0
  95. package/src/dsh-home.ts +9 -3
  96. package/src/gameplay.test.ts +239 -0
  97. package/src/gameplay.ts +692 -0
  98. package/src/ledger.test.ts +16 -0
  99. package/src/ledger.ts +31 -0
  100. package/src/manifest-v2.test.ts +130 -1
  101. package/src/manifest-v2.ts +181 -2
  102. package/src/persist.test.ts +3 -0
  103. package/src/persist.ts +43 -6
  104. package/src/registry.test.ts +168 -1
  105. package/src/registry.ts +199 -1
  106. package/src/routes.ts +19 -0
  107. package/src/service.ts +208 -0
@@ -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() {
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Pet gameplay — the optional manifest 'gameplay' block and its host engine.
3
+ * The block layers an opt-in mini-game over any frames2d pet: decaying stat
4
+ * bars, currencies, a weighted idle director, touch zones, a work loop, a
5
+ * sleep loop, passive income and a shop (issue: miku-pet generalization).
6
+ *
7
+ * Discipline split matches manifest-v2: STRUCTURE is fail-closed (types,
8
+ * ranges, references into stats/tracks); the host engine is pure — every
9
+ * verb takes an explicit clock and rng so tests stay deterministic. Decay,
10
+ * passive income and sleep restore are lazy-settled on read (the treats.ts
11
+ * discipline): the host runs no timers for gameplay.
12
+ * @module @linxin666/dsh-pet/gameplay
13
+ */
14
+ /** One gameplay effect: add amount to a declared stat or a currency. */
15
+ export interface PetGameplayEffect {
16
+ stat?: string;
17
+ currency?: string;
18
+ amount: number;
19
+ }
20
+ /** One roll branch inside a touch zone; uncovered roll mass is a no-op. */
21
+ export interface PetGameplayTouchBranch {
22
+ probability: number;
23
+ effects?: PetGameplayEffect[];
24
+ /** Track played on hit (held for stateMs, then the renderer settles). */
25
+ state?: string;
26
+ stateMs?: number;
27
+ /** Bubble phrase pool; one is picked on hit. */
28
+ phrases?: string[];
29
+ }
30
+ export interface PetGameplayTouchZone {
31
+ name: string;
32
+ /** Vertical slice of the hit box (fractions, y0 < y1). */
33
+ y0: number;
34
+ y1: number;
35
+ branches: PetGameplayTouchBranch[];
36
+ }
37
+ export interface PetGameplayStatDef {
38
+ max: number;
39
+ initial?: number;
40
+ decayPerMinute?: number;
41
+ /** Decay rate while the work mode is active (defaults to decayPerMinute). */
42
+ workingDecayPerMinute?: number;
43
+ /** Extra decay rate while no session is active. */
44
+ idleDecayPerMinute?: number;
45
+ }
46
+ export interface PetGameplayShopItem {
47
+ id: string;
48
+ label: string;
49
+ /** Optional frame path (manifest-relative) shown as the item icon. */
50
+ image?: string;
51
+ price: number;
52
+ currency: string;
53
+ effects?: PetGameplayEffect[];
54
+ lottery?: {
55
+ effects?: PetGameplayEffect[];
56
+ /** Default currency the prize is paid in (tiers may override). */
57
+ currency?: string;
58
+ tiers: {
59
+ probability: number;
60
+ prize: number;
61
+ currency?: string;
62
+ }[];
63
+ };
64
+ }
65
+ /** The validated manifest 'gameplay' block. */
66
+ export interface PetGameplayManifest {
67
+ idleDirector?: {
68
+ intervalMs: number;
69
+ maxMiss: number;
70
+ idleWeight: number;
71
+ acts: {
72
+ track: string;
73
+ weight: number;
74
+ phrases?: string[];
75
+ }[];
76
+ };
77
+ stats?: Record<string, PetGameplayStatDef>;
78
+ /** Click hit box inside the sprite box (fractions). */
79
+ hitBox?: {
80
+ x0: number;
81
+ y0: number;
82
+ x1: number;
83
+ y1: number;
84
+ };
85
+ touch?: {
86
+ zones: PetGameplayTouchZone[];
87
+ /** Plain-click effect while a touch animation holds (miku: mood +0..3). */
88
+ clickBoost?: {
89
+ stat: string;
90
+ min: number;
91
+ max: number;
92
+ };
93
+ };
94
+ work?: {
95
+ state: string;
96
+ successState: string;
97
+ failState: string;
98
+ tickMs: number;
99
+ /** Hold time of the result track before the next round. */
100
+ resultMs?: {
101
+ success: number;
102
+ fail: number;
103
+ };
104
+ successProbability: number;
105
+ success?: {
106
+ effects: PetGameplayEffect[];
107
+ };
108
+ fail?: {
109
+ effects: PetGameplayEffect[];
110
+ };
111
+ };
112
+ sleep?: {
113
+ state: string;
114
+ wakeState?: string;
115
+ restore: {
116
+ stat: string;
117
+ amount: number;
118
+ intervalMs: number;
119
+ };
120
+ };
121
+ passiveIncome?: {
122
+ currency: string;
123
+ amount: number;
124
+ intervalMs: number;
125
+ };
126
+ shop?: {
127
+ state?: string;
128
+ items: PetGameplayShopItem[];
129
+ };
130
+ /** Track played while the chrome reports dragging (default 'drag'). */
131
+ dragState?: string;
132
+ /** Track played once when a drag ends (miku: standup), before settling. */
133
+ dragEndState?: string;
134
+ }
135
+ export interface GameplayParseHooks {
136
+ /** State names the renderer can play (frames2d track ids). */
137
+ stateNames: ReadonlySet<string>;
138
+ error: (message: string) => void;
139
+ }
140
+ /**
141
+ * Validate the manifest 'gameplay' block (fail-closed). Only frames2d pets
142
+ * may declare gameplay today: every state reference checks against the
143
+ * declared track names.
144
+ */
145
+ export declare function parseGameplayManifest(raw: unknown, hooks: GameplayParseHooks): PetGameplayManifest | undefined;
146
+ /** Persisted per-pet gameplay state (pet.json 'gameplay' map values). */
147
+ export interface PetGameplayState {
148
+ stats: Record<string, number>;
149
+ currencies: Record<string, number>;
150
+ mode: 'work' | 'sleep' | null;
151
+ /** Epoch ms of the last lazy settle. */
152
+ settledAt: number;
153
+ }
154
+ /** Fresh state for one pet: stats at their initial (default max), no currency. */
155
+ export declare function initialGameplayState(manifest: PetGameplayManifest, now: number): PetGameplayState;
156
+ /** Clamp one stat value into [0, max]; currencies into [0, CURRENCY_MAX]. */
157
+ export declare function clampGameplay(state: PetGameplayState, manifest: PetGameplayManifest): void;
158
+ /**
159
+ * Lazy settle: apply stat decay, passive income and sleep restore for the
160
+ * elapsed wall time since the last settle. Mirrors the treats.ts discipline
161
+ * (no host timers; read paths settle). Returns whether anything changed.
162
+ */
163
+ export declare function settleGameplay(state: PetGameplayState, manifest: PetGameplayManifest, now: number, options: {
164
+ sessionActive: boolean;
165
+ }): boolean;
166
+ /** Apply one effect vector (touch/work/shop), clamped. */
167
+ export declare function applyGameplayEffects(state: PetGameplayState, manifest: PetGameplayManifest, effects: readonly PetGameplayEffect[]): void;
168
+ /** Roll one touch zone branch; undefined when the roll lands in no-op mass. */
169
+ export declare function rollTouchBranch(zone: PetGameplayTouchZone, rng: () => number): PetGameplayTouchBranch | undefined;
170
+ /** Roll one work tick outcome. */
171
+ export declare function rollWorkOutcome(work: NonNullable<PetGameplayManifest['work']>, rng: () => number): 'success' | 'fail';
172
+ /** Draw one lottery prize tier; uncovered mass falls through to the last tier. */
173
+ export declare function drawLotteryTier(lottery: NonNullable<PetGameplayShopItem['lottery']>, rng: () => number): {
174
+ probability: number;
175
+ prize: number;
176
+ currency?: string;
177
+ };
178
+ /** The zone one normalized hit-box point lands in, if any. */
179
+ export declare function touchZoneAt(touch: {
180
+ zones: PetGameplayTouchZone[];
181
+ }, yFraction: number): PetGameplayTouchZone | undefined;
182
+ //# sourceMappingURL=gameplay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gameplay.d.ts","sourceRoot":"","sources":["../../src/gameplay.ts"],"names":[],"mappings":"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"}