@linxin666/dsh-pet 0.3.2 → 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.
- package/README.i18n.yaml +2 -2
- package/README.md +25 -7
- package/README.zh.md +25 -7
- package/assets/ouo-neko/pet.json +31 -0
- package/assets/ouo-neko/previews/failed.gif +0 -0
- package/assets/ouo-neko/previews/idle.gif +0 -0
- package/assets/ouo-neko/previews/jumping.gif +0 -0
- package/assets/ouo-neko/previews/review.gif +0 -0
- package/assets/ouo-neko/previews/running-left.gif +0 -0
- package/assets/ouo-neko/previews/running-right.gif +0 -0
- package/assets/ouo-neko/previews/running.gif +0 -0
- package/assets/ouo-neko/previews/waiting.gif +0 -0
- package/assets/ouo-neko/previews/waving.gif +0 -0
- package/assets/ouo-neko/spritesheet.webp +0 -0
- package/contracts/pet-manifest-v2.schema.json +102 -1
- package/lib/client.js +830 -26
- package/lib/client.js.map +1 -1
- package/lib/index.js +1226 -24
- package/lib/types/client/PetDockEntry.d.ts +3 -0
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +18 -2
- package/lib/types/client/PetSprite.d.ts +23 -0
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +34 -29
- package/lib/types/client/PluginSettingsCard.d.ts +2 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/drag-stream.d.ts +19 -0
- package/lib/types/client/drag-stream.d.ts.map +1 -0
- package/lib/types/client/drag-stream.js +26 -0
- package/lib/types/client/gameplay-hud.d.ts +50 -0
- package/lib/types/client/gameplay-hud.d.ts.map +1 -0
- package/lib/types/client/gameplay-hud.js +274 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +16 -0
- package/lib/types/client/locales.d.ts +34 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -0
- package/lib/types/client/pet-store.d.ts +7 -1
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/renderers/Frames2dVisualMount.d.ts +29 -0
- package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -0
- package/lib/types/client/renderers/Frames2dVisualMount.js +83 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts +6 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -1
- package/lib/types/client/renderers/PetRendererSwitch.js +14 -1
- package/lib/types/client/renderers/frames2d.d.ts +36 -0
- package/lib/types/client/renderers/frames2d.d.ts.map +1 -0
- package/lib/types/client/renderers/frames2d.js +196 -0
- package/lib/types/client/settings-form.d.ts +1 -1
- package/lib/types/client/settings-form.js +1 -1
- package/lib/types/client/telemetry.d.ts +27 -0
- package/lib/types/client/telemetry.d.ts.map +1 -0
- package/lib/types/client/telemetry.js +97 -0
- package/lib/types/defaults.d.ts +6 -0
- package/lib/types/defaults.d.ts.map +1 -0
- package/lib/types/defaults.js +5 -0
- package/lib/types/dsh-home.d.ts.map +1 -1
- package/lib/types/dsh-home.js +9 -3
- package/lib/types/gameplay.d.ts +182 -0
- package/lib/types/gameplay.d.ts.map +1 -0
- package/lib/types/gameplay.js +618 -0
- package/lib/types/http.d.ts +1 -1
- package/lib/types/http.js +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.js +1 -1
- package/lib/types/ledger.d.ts +14 -0
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +30 -0
- package/lib/types/loopback.d.ts +1 -1
- package/lib/types/loopback.js +1 -1
- package/lib/types/manifest-v2.d.ts +38 -1
- package/lib/types/manifest-v2.d.ts.map +1 -1
- package/lib/types/manifest-v2.js +182 -2
- package/lib/types/mount-once.d.ts +1 -1
- package/lib/types/mount-once.js +2 -2
- package/lib/types/persist.d.ts +4 -4
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +41 -4
- package/lib/types/registry.d.ts +29 -0
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +170 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +22 -0
- package/lib/types/service.d.ts +56 -0
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +181 -0
- package/package.json +6 -3
- package/src/client/PetDockEntry.test.tsx +6 -0
- package/src/client/PetDockEntry.tsx +32 -1
- package/src/client/PetSprite.test.tsx +36 -6
- package/src/client/PetSprite.tsx +42 -21
- package/src/client/PluginSettingsCard.tsx +4 -2
- package/src/client/drag-stream.ts +35 -0
- package/src/client/gameplay-hud.test.tsx +338 -0
- package/src/client/gameplay-hud.tsx +403 -0
- package/src/client/index.ts +23 -2
- package/src/client/locales.ts +36 -0
- package/src/client/pet-store.ts +10 -1
- package/src/client/pet.module.css +192 -29
- package/src/client/renderers/Frames2dVisualMount.tsx +111 -0
- package/src/client/renderers/PetRendererSwitch.tsx +28 -1
- package/src/client/renderers/frames2d.test.ts +137 -0
- package/src/client/renderers/frames2d.ts +216 -0
- package/src/client/settings-card.module.css +5 -0
- package/src/client/settings-form.ts +1 -1
- package/src/client/telemetry.ts +105 -0
- package/src/defaults.ts +7 -0
- package/src/dsh-home.ts +9 -3
- package/src/gameplay.test.ts +239 -0
- package/src/gameplay.ts +692 -0
- package/src/http.ts +1 -1
- package/src/index.ts +1 -1
- package/src/ledger.test.ts +16 -0
- package/src/ledger.ts +31 -0
- package/src/loopback.ts +1 -1
- package/src/manifest-v2.test.ts +130 -1
- package/src/manifest-v2.ts +181 -2
- package/src/mount-once.ts +2 -2
- package/src/persist.test.ts +3 -0
- package/src/persist.ts +43 -6
- package/src/registry.test.ts +168 -1
- package/src/registry.ts +199 -1
- package/src/routes.ts +19 -0
- package/src/service.ts +208 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Gameplay HUD — the client half of the manifest 'gameplay' block (miku-pet
|
|
4
|
+
* generalization). One component owns everything the block needs: the stat
|
|
5
|
+
* bars and shop card (menu / shop pages), the touch-zone tap
|
|
6
|
+
* handling, the idle director rolls, the work and sleep loops, and the
|
|
7
|
+
* float-text toasts. It talks to the host through the injected verb API,
|
|
8
|
+
* writes results straight back into the store (the 2 s poll stays the
|
|
9
|
+
* backstop), and steers the frames2d renderer through the per-pet bus.
|
|
10
|
+
* @module @linxin666/dsh-pet/client/GameplayHud
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
13
|
+
import { touchZoneAt } from "../gameplay.js";
|
|
14
|
+
import styles from './pet.module.css';
|
|
15
|
+
let floatSeq = 0;
|
|
16
|
+
/** The gameplay overlay for one frames2d pet that declares 'gameplay'. */
|
|
17
|
+
export function GameplayHud(props) {
|
|
18
|
+
const { definition, store, api, bus } = props;
|
|
19
|
+
const ui = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
20
|
+
const def = definition.gameplay;
|
|
21
|
+
const view = ui.snapshot?.gameplay;
|
|
22
|
+
const phase = ui.snapshot?.phase ?? 'idle';
|
|
23
|
+
const [open, setOpen] = useState(false);
|
|
24
|
+
const [page, setPage] = useState('root');
|
|
25
|
+
const hudRef = useRef(null);
|
|
26
|
+
const cardRef = useRef(null);
|
|
27
|
+
const [floats, setFloats] = useState([]);
|
|
28
|
+
// Mutable driver state (refs so intervals never re-arm on a poll tick).
|
|
29
|
+
const modeRef = useRef(view?.mode ?? null);
|
|
30
|
+
modeRef.current = view?.mode ?? null;
|
|
31
|
+
const phaseRef = useRef(phase);
|
|
32
|
+
phaseRef.current = phase;
|
|
33
|
+
const draggingRef = useRef(false);
|
|
34
|
+
const touchLockUntilRef = useRef(0);
|
|
35
|
+
const missRef = useRef(0);
|
|
36
|
+
const busyRef = useRef(false);
|
|
37
|
+
// Dynamic-key lookups (stat ids / currency ids are manifest data).
|
|
38
|
+
const tr = props.t;
|
|
39
|
+
const statLabel = (name) => tr('pet.gameplay.stat.' + name);
|
|
40
|
+
const currencyLabel = (name) => tr('pet.gameplay.currency.' + name);
|
|
41
|
+
const pushFloat = (text) => {
|
|
42
|
+
const id = ++floatSeq;
|
|
43
|
+
setFloats(list => [...list.slice(-3), { id, text }]);
|
|
44
|
+
window.setTimeout(() => {
|
|
45
|
+
setFloats(list => list.filter(entry => entry.id !== id));
|
|
46
|
+
}, 1100);
|
|
47
|
+
};
|
|
48
|
+
const applyResult = (result) => {
|
|
49
|
+
if (result.view !== undefined)
|
|
50
|
+
store.actions.setGameplayView(result.view);
|
|
51
|
+
};
|
|
52
|
+
// Tap handling (registered on the bus; PetSprite reports sprite-box
|
|
53
|
+
// fractions). Sleep wakes on tap; work blocks taps; a held touch
|
|
54
|
+
// animation turns taps into the plain-click boost.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (def === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
bus.tap = (fx, fy) => {
|
|
59
|
+
if (modeRef.current === 'sleep') {
|
|
60
|
+
void api.setMode(null).then(applyResult, () => undefined);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (modeRef.current === 'work')
|
|
64
|
+
return;
|
|
65
|
+
const box = def.hitBox ?? { x0: 0, y0: 0, x1: 1, y1: 1 };
|
|
66
|
+
const hx = (fx - box.x0) / (box.x1 - box.x0);
|
|
67
|
+
const hy = (fy - box.y0) / (box.y1 - box.y0);
|
|
68
|
+
if (hx < 0 || hx > 1 || hy < 0 || hy > 1)
|
|
69
|
+
return;
|
|
70
|
+
if (Date.now() < touchLockUntilRef.current) {
|
|
71
|
+
void api.touch().then(applyResult, () => undefined);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const zone = def.touch === undefined ? undefined : touchZoneAt(def.touch, hy);
|
|
75
|
+
if (zone === undefined)
|
|
76
|
+
return;
|
|
77
|
+
void api.touch(zone.name).then((result) => {
|
|
78
|
+
applyResult(result);
|
|
79
|
+
if (result.hit !== true)
|
|
80
|
+
return;
|
|
81
|
+
if (result.state !== undefined) {
|
|
82
|
+
bus.setTrack?.(result.state);
|
|
83
|
+
const holdMs = result.stateMs ?? 3000;
|
|
84
|
+
touchLockUntilRef.current = Date.now() + holdMs;
|
|
85
|
+
window.setTimeout(() => {
|
|
86
|
+
if (Date.now() >= touchLockUntilRef.current)
|
|
87
|
+
bus.setTrack?.(undefined);
|
|
88
|
+
}, holdMs);
|
|
89
|
+
}
|
|
90
|
+
if (result.phrase !== undefined) {
|
|
91
|
+
store.actions.setFeedback({ text: result.phrase, kind: 'none', at: Date.now() });
|
|
92
|
+
}
|
|
93
|
+
}, () => undefined);
|
|
94
|
+
};
|
|
95
|
+
return () => { bus.tap = undefined; };
|
|
96
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one registration per pet definition
|
|
97
|
+
}, [definition.id, def]);
|
|
98
|
+
// Panel entry (chrome -> HUD): the hover panel's 玩法 button drives the
|
|
99
|
+
// card through this channel so the chrome never needs the card's state.
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
bus.openCard = (next) => {
|
|
102
|
+
setOpen(prev => next ?? !prev);
|
|
103
|
+
setPage('root');
|
|
104
|
+
};
|
|
105
|
+
return () => { bus.openCard = undefined; };
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one registration per bus
|
|
107
|
+
}, [bus]);
|
|
108
|
+
// Card placement: the card must not enter the bubble band above the sprite
|
|
109
|
+
// or the hover-panel band below it, so it opens beside the sprite instead
|
|
110
|
+
// of growing upward from the pet's feet. It is vertically centered on the
|
|
111
|
+
// sprite box, clamped to the sprite's height, and the side is chosen by
|
|
112
|
+
// the space available to each side (right first; a pet parked near the
|
|
113
|
+
// right viewport edge flips the card to the left).
|
|
114
|
+
useLayoutEffect(() => {
|
|
115
|
+
if (!open)
|
|
116
|
+
return undefined;
|
|
117
|
+
const hud = hudRef.current;
|
|
118
|
+
const card = cardRef.current;
|
|
119
|
+
if (hud === null || card === null)
|
|
120
|
+
return undefined;
|
|
121
|
+
const place = () => {
|
|
122
|
+
const box = hud.parentElement?.getBoundingClientRect();
|
|
123
|
+
if (box === undefined)
|
|
124
|
+
return;
|
|
125
|
+
const gap = 8;
|
|
126
|
+
const width = card.getBoundingClientRect().width;
|
|
127
|
+
const toRight = window.innerWidth - box.right;
|
|
128
|
+
const x = toRight >= width + gap ? box.width + gap : -(width + gap);
|
|
129
|
+
card.style.transform = 'translate(' + Math.round(x) + 'px, ' + Math.round(-box.height / 2) + 'px) translateY(50%)';
|
|
130
|
+
card.style.maxHeight = Math.round(box.height) + 'px';
|
|
131
|
+
};
|
|
132
|
+
place();
|
|
133
|
+
window.addEventListener('resize', place);
|
|
134
|
+
return () => { window.removeEventListener('resize', place); };
|
|
135
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one placement per open/page change
|
|
136
|
+
}, [open, page]);
|
|
137
|
+
// Drag gestures wake a sleeping pet (miku behavior); the drag stream also
|
|
138
|
+
// feeds the idle director's suppression check.
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
return props.drag.subscribe((dragging) => {
|
|
141
|
+
draggingRef.current = dragging;
|
|
142
|
+
if (dragging && modeRef.current === 'sleep') {
|
|
143
|
+
void api.setMode(null).then(applyResult, () => undefined);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one subscription per drag stream
|
|
147
|
+
}, [props.drag]);
|
|
148
|
+
// Idle director: weighted rolls between staying idle and playing an act.
|
|
149
|
+
// Runs only while the phase mapping owns the visual (idle phase, no mode,
|
|
150
|
+
// no drag, no held touch animation); maxMiss forces an act after too many
|
|
151
|
+
// idle rolls in a row.
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const director = def?.idleDirector;
|
|
154
|
+
if (def === undefined || director === undefined)
|
|
155
|
+
return undefined;
|
|
156
|
+
const total = director.idleWeight + director.acts.reduce((sum, act) => sum + act.weight, 0);
|
|
157
|
+
if (total <= 0)
|
|
158
|
+
return undefined;
|
|
159
|
+
const timer = window.setInterval(() => {
|
|
160
|
+
if (phaseRef.current !== 'idle')
|
|
161
|
+
return;
|
|
162
|
+
if (modeRef.current !== null || draggingRef.current)
|
|
163
|
+
return;
|
|
164
|
+
if (Date.now() < touchLockUntilRef.current)
|
|
165
|
+
return;
|
|
166
|
+
let pickedAct;
|
|
167
|
+
if (missRef.current >= director.maxMiss) {
|
|
168
|
+
// Forced act: pick among the acts only.
|
|
169
|
+
const actTotal = director.acts.reduce((sum, act) => sum + act.weight, 0);
|
|
170
|
+
let actRoll = Math.random() * actTotal;
|
|
171
|
+
for (const act of director.acts) {
|
|
172
|
+
actRoll -= act.weight;
|
|
173
|
+
if (actRoll < 0) {
|
|
174
|
+
pickedAct = act;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
let roll = Math.random() * total;
|
|
181
|
+
for (const act of director.acts) {
|
|
182
|
+
roll -= act.weight;
|
|
183
|
+
if (roll < 0) {
|
|
184
|
+
pickedAct = act;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (pickedAct === undefined) {
|
|
190
|
+
missRef.current += 1;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
missRef.current = 0;
|
|
194
|
+
bus.setTrack?.(pickedAct.track);
|
|
195
|
+
// Acts with a phrase pool speak one line while they play (miku parity).
|
|
196
|
+
if (pickedAct.phrases !== undefined && pickedAct.phrases.length > 0) {
|
|
197
|
+
const phrase = pickedAct.phrases[Math.floor(Math.random() * pickedAct.phrases.length)];
|
|
198
|
+
store.actions.setFeedback({ text: phrase, kind: 'none', at: Date.now() });
|
|
199
|
+
}
|
|
200
|
+
}, director.intervalMs);
|
|
201
|
+
return () => window.clearInterval(timer);
|
|
202
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one director per pet definition
|
|
203
|
+
}, [definition.id, def]);
|
|
204
|
+
// Work loop: hold the work track, adjudicate one round per tick, play the
|
|
205
|
+
// result track for its hold window, then resume. Leaving the mode
|
|
206
|
+
// releases the override so the phase mapping takes over.
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const work = def?.work;
|
|
209
|
+
if (def === undefined || work === undefined || view?.mode !== 'work')
|
|
210
|
+
return undefined;
|
|
211
|
+
bus.setTrack?.(work.state);
|
|
212
|
+
let resultTimer = 0;
|
|
213
|
+
const timer = window.setInterval(() => {
|
|
214
|
+
if (busyRef.current)
|
|
215
|
+
return;
|
|
216
|
+
busyRef.current = true;
|
|
217
|
+
void api.workTick().then((result) => {
|
|
218
|
+
busyRef.current = false;
|
|
219
|
+
applyResult(result);
|
|
220
|
+
if (result.ok !== true || result.outcome === undefined)
|
|
221
|
+
return;
|
|
222
|
+
const resultTrack = result.outcome === 'success' ? work.successState : work.failState;
|
|
223
|
+
const hold = result.outcome === 'success' ? work.resultMs?.success ?? 1300 : work.resultMs?.fail ?? 1900;
|
|
224
|
+
bus.setTrack?.(resultTrack);
|
|
225
|
+
resultTimer = window.setTimeout(() => {
|
|
226
|
+
if (modeRef.current === 'work')
|
|
227
|
+
bus.setTrack?.(work.state);
|
|
228
|
+
}, hold);
|
|
229
|
+
}, () => { busyRef.current = false; });
|
|
230
|
+
}, work.tickMs);
|
|
231
|
+
return () => {
|
|
232
|
+
window.clearInterval(timer);
|
|
233
|
+
window.clearTimeout(resultTimer);
|
|
234
|
+
bus.setTrack?.(undefined);
|
|
235
|
+
};
|
|
236
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- the loop keys on the mode value
|
|
237
|
+
}, [definition.id, def, view?.mode]);
|
|
238
|
+
// Sleep loop: hold the sleep track; restore is host-side (lazy settle).
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const sleep = def?.sleep;
|
|
241
|
+
if (def === undefined || sleep === undefined || view?.mode !== 'sleep')
|
|
242
|
+
return undefined;
|
|
243
|
+
bus.setTrack?.(sleep.state);
|
|
244
|
+
return () => bus.setTrack?.(undefined);
|
|
245
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- the loop keys on the mode value
|
|
246
|
+
}, [definition.id, def, view?.mode]);
|
|
247
|
+
if (def === undefined || view === undefined)
|
|
248
|
+
return null;
|
|
249
|
+
const mode = view.mode;
|
|
250
|
+
const stats = def.stats ?? {};
|
|
251
|
+
const shop = def.shop;
|
|
252
|
+
const buy = (itemId) => {
|
|
253
|
+
void api.buy(itemId).then((result) => {
|
|
254
|
+
applyResult(result);
|
|
255
|
+
if (result.ok !== true) {
|
|
256
|
+
if (result.error === 'insufficient-funds') {
|
|
257
|
+
const item = shop?.items.find(entry => entry.id === itemId);
|
|
258
|
+
pushFloat(tr('pet.gameplay.insufficient', { currency: currencyLabel(item?.currency ?? 'treats') }));
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (result.prize !== undefined) {
|
|
263
|
+
pushFloat(tr('pet.gameplay.prize', { amount: result.prize.amount, currency: currencyLabel(result.prize.currency) }));
|
|
264
|
+
}
|
|
265
|
+
}, () => undefined);
|
|
266
|
+
};
|
|
267
|
+
const setMode = (next) => {
|
|
268
|
+
void api.setMode(next).then(applyResult, () => undefined);
|
|
269
|
+
};
|
|
270
|
+
return (_jsxs("div", { ref: hudRef, className: styles.gameplayHud, "data-dsh-pet-gameplay": definition.id, children: [floats.map(entry => (_jsx("div", { className: styles.gameplayFloat, children: entry.text }, entry.id))), mode !== null && (_jsx("div", { className: styles.gameplayModeChip, children: tr(mode === 'work' ? 'pet.gameplay.working' : 'pet.gameplay.sleeping') })), open && (_jsxs("div", { ref: cardRef, className: styles.gameplayCard, "data-page": page, children: [page === 'root' && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.gameplayBars, children: Object.entries(stats).map(([name, stat]) => {
|
|
271
|
+
const value = view.stats[name] ?? 0;
|
|
272
|
+
return (_jsxs("div", { className: styles.gameplayBarRow, title: statLabel(name) + ' ' + String(value) + '/' + String(stat.max), children: [_jsx("span", { className: styles.gameplayBarLabel, children: statLabel(name) }), _jsx("span", { className: styles.gameplayBarTrack, children: _jsx("span", { className: styles.gameplayBarFill, style: { width: Math.round((value / stat.max) * 100) + '%' } }) })] }, name));
|
|
273
|
+
}) }), _jsxs("div", { className: styles.gameplayActions, children: [def.work !== undefined && (_jsx("button", { type: "button", className: styles.action, onClick: () => setMode(mode === 'work' ? null : 'work'), children: tr(mode === 'work' ? 'pet.gameplay.stopWork' : 'pet.gameplay.work') })), def.sleep !== undefined && (_jsx("button", { type: "button", className: styles.action, onClick: () => setMode(mode === 'sleep' ? null : 'sleep'), children: tr(mode === 'sleep' ? 'pet.gameplay.wake' : 'pet.gameplay.sleep') })), shop !== undefined && (_jsx("button", { type: "button", className: styles.action, onClick: () => setPage('shop'), children: tr('pet.gameplay.shop') }))] })] })), page === 'shop' && shop !== undefined && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.gameplayShopItems, children: shop.items.map(item => (_jsxs("button", { type: "button", className: styles.gameplayShopItem, onClick: () => buy(item.id), title: item.label + ' — ' + String(item.price) + ' ' + currencyLabel(item.currency), children: [item.image !== undefined && (_jsx("img", { className: styles.gameplayShopItemImage, src: item.image, alt: "", draggable: false })), _jsx("span", { className: styles.gameplayShopItemLabel, children: item.label }), _jsxs("span", { className: styles.gameplayShopItemPrice, children: [item.price, " ", currencyLabel(item.currency)] })] }, item.id))) }), _jsx("div", { className: styles.gameplayActions, children: _jsx("button", { type: "button", className: styles.action, onClick: () => setPage('root'), children: tr('pet.gameplay.back') }) })] })), _jsx("button", { type: "button", className: styles.gameplayClose, "aria-label": tr('pet.gameplay.back'), onClick: () => setOpen(false), children: "\u00D7" })] }))] }));
|
|
274
|
+
}
|
|
@@ -23,7 +23,7 @@ export type { PetDefinition } from '../registry.ts';
|
|
|
23
23
|
declare module '@deepseek-ai/cordis' {
|
|
24
24
|
interface Context {
|
|
25
25
|
/**
|
|
26
|
-
* Optional rc.6 compatibility binder provided by dsh-web-
|
|
26
|
+
* Optional rc.6 compatibility binder provided by dsh-web-settings;
|
|
27
27
|
* absent when that group plugin is not installed, so callers fall back to
|
|
28
28
|
* the official settings scope.
|
|
29
29
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AA0EnI,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA+R9C"}
|
|
@@ -16,9 +16,11 @@ import { createPetStore } from "./pet-store.js";
|
|
|
16
16
|
import { PetDockEntry } from "./PetDockEntry.js";
|
|
17
17
|
import { defaultPetRendererRegistry } from "./renderers/registry.js";
|
|
18
18
|
import { live2dRenderer } from "./renderers/live2d.js";
|
|
19
|
+
import { frames2dRenderer } from "./renderers/frames2d.js";
|
|
19
20
|
import { registerPetUiTeardown, takeoverPetUiTeardown } from "./ui-teardown.js";
|
|
20
21
|
import { PetSettingsSection, PetSettingsCardController } from "./PetSettingsCard.js";
|
|
21
22
|
import { NS, en, zh, t } from "./locales.js";
|
|
23
|
+
import { reportDailyHeartbeat } from "./telemetry.js";
|
|
22
24
|
/** Same-origin JSON fetch helper (GET without body, POST with JSON body). */
|
|
23
25
|
async function petFetch(path, body) {
|
|
24
26
|
const response = await fetch(path, body === undefined
|
|
@@ -42,6 +44,10 @@ const petApi = {
|
|
|
42
44
|
setConfig: (patch) => petFetch('/api/pet/set-config', patch),
|
|
43
45
|
setName: (name) => petFetch('/api/pet/set-name', { name }),
|
|
44
46
|
setPet: (petId) => petFetch('/api/pet/set-pet', { petId }),
|
|
47
|
+
gameplayTouch: (zone) => petFetch('/api/pet/gameplay/touch', zone === undefined ? {} : { zone }),
|
|
48
|
+
gameplaySetMode: (mode) => petFetch('/api/pet/gameplay/mode', { mode }),
|
|
49
|
+
gameplayWorkTick: () => petFetch('/api/pet/gameplay/work-tick', {}),
|
|
50
|
+
gameplayBuy: (item) => petFetch('/api/pet/gameplay/buy', { item }),
|
|
45
51
|
};
|
|
46
52
|
/** Poll interval for the host snapshot. */
|
|
47
53
|
const POLL_MS = 2000;
|
|
@@ -56,6 +62,9 @@ export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote
|
|
|
56
62
|
* @param ctx - client root context.
|
|
57
63
|
*/
|
|
58
64
|
export function apply(ctx) {
|
|
65
|
+
// Anonymous install heartbeat (docs/telemetry.md): one beat per browser per
|
|
66
|
+
// UTC day, package name only, silent failure.
|
|
67
|
+
reportDailyHeartbeat([{ name: '@linxin666/dsh-pet' }]);
|
|
59
68
|
ctx.effect(() => {
|
|
60
69
|
try {
|
|
61
70
|
return ctx.locale.register(NS, { zh, en });
|
|
@@ -67,6 +76,7 @@ export function apply(ctx) {
|
|
|
67
76
|
// Built-in renderers dispatch through the plugin-wide registry (pet-center
|
|
68
77
|
// M3). Registration is idempotent (id wins), so re-applies stay clean.
|
|
69
78
|
defaultPetRendererRegistry.register(live2dRenderer);
|
|
79
|
+
defaultPetRendererRegistry.register(frames2dRenderer);
|
|
70
80
|
const binder = ctx.get('webUiSettings') ?? ctx.settingsScope;
|
|
71
81
|
const settingsScope = binder.bind({ namespace: PET_SETTINGS_NS });
|
|
72
82
|
const enabled = () => {
|
|
@@ -268,6 +278,12 @@ export function apply(ctx) {
|
|
|
268
278
|
feedbackDone: () => {
|
|
269
279
|
setFeedback(null);
|
|
270
280
|
},
|
|
281
|
+
gameplay: {
|
|
282
|
+
touch: (zone) => petApi.gameplayTouch(zone),
|
|
283
|
+
setMode: (mode) => petApi.gameplaySetMode(mode),
|
|
284
|
+
workTick: () => petApi.gameplayWorkTick(),
|
|
285
|
+
buy: (item) => petApi.gameplayBuy(item),
|
|
286
|
+
},
|
|
271
287
|
});
|
|
272
288
|
// The pet is host-global (its state/display/interactions have no session
|
|
273
289
|
// dimension), and the official rc.6 shell declares no root-scoped slot
|
|
@@ -22,6 +22,23 @@ export declare const zh: {
|
|
|
22
22
|
readonly 'pet.live2d.vendor-missing': "Live2D 组件缺失,请升级宠物插件。";
|
|
23
23
|
readonly 'pet.live2d.load-failed': "Live2D 模型加载失败,请检查该宠物目录的完整性。";
|
|
24
24
|
readonly 'pet.openSessionHint': "点击跳转到对应会话";
|
|
25
|
+
readonly 'pet.gameplay.menu': "玩法";
|
|
26
|
+
readonly 'pet.gameplay.work': "打工";
|
|
27
|
+
readonly 'pet.gameplay.stopWork': "收工";
|
|
28
|
+
readonly 'pet.gameplay.sleep': "睡觉";
|
|
29
|
+
readonly 'pet.gameplay.wake': "起床";
|
|
30
|
+
readonly 'pet.gameplay.shop': "商店";
|
|
31
|
+
readonly 'pet.gameplay.back': "返回";
|
|
32
|
+
readonly 'pet.gameplay.buy': "购买";
|
|
33
|
+
readonly 'pet.gameplay.insufficient': "{currency}不足";
|
|
34
|
+
readonly 'pet.gameplay.prize': "中奖 +{amount} {currency}";
|
|
35
|
+
readonly 'pet.gameplay.working': "打工中";
|
|
36
|
+
readonly 'pet.gameplay.sleeping': "睡觉中";
|
|
37
|
+
readonly 'pet.gameplay.stat.hunger': "饱食";
|
|
38
|
+
readonly 'pet.gameplay.stat.mood': "心情";
|
|
39
|
+
readonly 'pet.gameplay.stat.energy': "精力";
|
|
40
|
+
readonly 'pet.gameplay.stat.affection': "好感";
|
|
41
|
+
readonly 'pet.gameplay.currency.treats': "小鱼干";
|
|
25
42
|
readonly 'pet.moreSessions': "展开其余 {n} 个会话的气泡";
|
|
26
43
|
readonly 'pet.collapseSessions': "收起会话气泡";
|
|
27
44
|
readonly 'settings.title': "宠物";
|
|
@@ -75,6 +92,23 @@ export declare const en: {
|
|
|
75
92
|
readonly 'pet.live2d.vendor-missing': "The Live2D component is missing; please update the pet plugin.";
|
|
76
93
|
readonly 'pet.live2d.load-failed': "The Live2D model failed to load; check the pet directory is complete.";
|
|
77
94
|
readonly 'pet.openSessionHint': "Click to jump to this session";
|
|
95
|
+
readonly 'pet.gameplay.menu': "Play";
|
|
96
|
+
readonly 'pet.gameplay.work': "Work";
|
|
97
|
+
readonly 'pet.gameplay.stopWork': "Stop work";
|
|
98
|
+
readonly 'pet.gameplay.sleep': "Sleep";
|
|
99
|
+
readonly 'pet.gameplay.wake': "Wake up";
|
|
100
|
+
readonly 'pet.gameplay.shop': "Shop";
|
|
101
|
+
readonly 'pet.gameplay.back': "Back";
|
|
102
|
+
readonly 'pet.gameplay.buy': "Buy";
|
|
103
|
+
readonly 'pet.gameplay.insufficient': "Not enough {currency}";
|
|
104
|
+
readonly 'pet.gameplay.prize': "Prize +{amount} {currency}";
|
|
105
|
+
readonly 'pet.gameplay.working': "Working";
|
|
106
|
+
readonly 'pet.gameplay.sleeping': "Sleeping";
|
|
107
|
+
readonly 'pet.gameplay.stat.hunger': "Hunger";
|
|
108
|
+
readonly 'pet.gameplay.stat.mood': "Mood";
|
|
109
|
+
readonly 'pet.gameplay.stat.energy': "Energy";
|
|
110
|
+
readonly 'pet.gameplay.stat.affection': "Affection";
|
|
111
|
+
readonly 'pet.gameplay.currency.treats': "Treats";
|
|
78
112
|
readonly 'pet.moreSessions': "Expand {n} more session bubbles";
|
|
79
113
|
readonly 'pet.collapseSessions': "Collapse session bubbles";
|
|
80
114
|
readonly 'settings.title': "Pet";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsEL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsEL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
|
|
@@ -22,6 +22,24 @@ export const zh = {
|
|
|
22
22
|
'pet.live2d.vendor-missing': 'Live2D 组件缺失,请升级宠物插件。',
|
|
23
23
|
'pet.live2d.load-failed': 'Live2D 模型加载失败,请检查该宠物目录的完整性。',
|
|
24
24
|
'pet.openSessionHint': '点击跳转到对应会话',
|
|
25
|
+
// 玩法 HUD(miku-pet 泛化:属性条 / 打工 / 睡觉 / 商店;统一小鱼干经济)。
|
|
26
|
+
'pet.gameplay.menu': '玩法',
|
|
27
|
+
'pet.gameplay.work': '打工',
|
|
28
|
+
'pet.gameplay.stopWork': '收工',
|
|
29
|
+
'pet.gameplay.sleep': '睡觉',
|
|
30
|
+
'pet.gameplay.wake': '起床',
|
|
31
|
+
'pet.gameplay.shop': '商店',
|
|
32
|
+
'pet.gameplay.back': '返回',
|
|
33
|
+
'pet.gameplay.buy': '购买',
|
|
34
|
+
'pet.gameplay.insufficient': '{currency}不足',
|
|
35
|
+
'pet.gameplay.prize': '中奖 +{amount} {currency}',
|
|
36
|
+
'pet.gameplay.working': '打工中',
|
|
37
|
+
'pet.gameplay.sleeping': '睡觉中',
|
|
38
|
+
'pet.gameplay.stat.hunger': '饱食',
|
|
39
|
+
'pet.gameplay.stat.mood': '心情',
|
|
40
|
+
'pet.gameplay.stat.energy': '精力',
|
|
41
|
+
'pet.gameplay.stat.affection': '好感',
|
|
42
|
+
'pet.gameplay.currency.treats': '小鱼干',
|
|
25
43
|
'pet.moreSessions': '展开其余 {n} 个会话的气泡',
|
|
26
44
|
'pet.collapseSessions': '收起会话气泡',
|
|
27
45
|
// 一级设置页(settings.section 席位)。
|
|
@@ -76,6 +94,24 @@ export const en = {
|
|
|
76
94
|
'pet.live2d.vendor-missing': 'The Live2D component is missing; please update the pet plugin.',
|
|
77
95
|
'pet.live2d.load-failed': 'The Live2D model failed to load; check the pet directory is complete.',
|
|
78
96
|
'pet.openSessionHint': 'Click to jump to this session',
|
|
97
|
+
// Gameplay HUD (miku-pet generalization: stat bars / work / sleep / shop; unified treats economy).
|
|
98
|
+
'pet.gameplay.menu': 'Play',
|
|
99
|
+
'pet.gameplay.work': 'Work',
|
|
100
|
+
'pet.gameplay.stopWork': 'Stop work',
|
|
101
|
+
'pet.gameplay.sleep': 'Sleep',
|
|
102
|
+
'pet.gameplay.wake': 'Wake up',
|
|
103
|
+
'pet.gameplay.shop': 'Shop',
|
|
104
|
+
'pet.gameplay.back': 'Back',
|
|
105
|
+
'pet.gameplay.buy': 'Buy',
|
|
106
|
+
'pet.gameplay.insufficient': 'Not enough {currency}',
|
|
107
|
+
'pet.gameplay.prize': 'Prize +{amount} {currency}',
|
|
108
|
+
'pet.gameplay.working': 'Working',
|
|
109
|
+
'pet.gameplay.sleeping': 'Sleeping',
|
|
110
|
+
'pet.gameplay.stat.hunger': 'Hunger',
|
|
111
|
+
'pet.gameplay.stat.mood': 'Mood',
|
|
112
|
+
'pet.gameplay.stat.energy': 'Energy',
|
|
113
|
+
'pet.gameplay.stat.affection': 'Affection',
|
|
114
|
+
'pet.gameplay.currency.treats': 'Treats',
|
|
79
115
|
'pet.moreSessions': 'Expand {n} more session bubbles',
|
|
80
116
|
'pet.collapseSessions': 'Collapse session bubbles',
|
|
81
117
|
// First-level settings section (the `settings.section` seat).
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module @linxin666/dsh-pet/client/pet-store
|
|
7
7
|
*/
|
|
8
8
|
import type { EngineStoreHandle, EngineStoreInstance } from '@deepseek-ai/dsh-client-runtime/client';
|
|
9
|
-
import type { PetStateView } from '../service.ts';
|
|
9
|
+
import type { PetGameplayStateView, PetStateView } from '../service.ts';
|
|
10
10
|
import type { PetInteraction } from '../affinity.ts';
|
|
11
11
|
import type { PetDefinition } from '../registry.ts';
|
|
12
12
|
/** One transient reaction bubble on the pet. */
|
|
@@ -41,6 +41,12 @@ export type PetUiActions = {
|
|
|
41
41
|
setState: (draft: PetUiState, state: PetUiState['state'], error: string | null) => void;
|
|
42
42
|
/** Show a reaction bubble. */
|
|
43
43
|
setFeedback: (draft: PetUiState, feedback: PetFeedback | null) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Patch the gameplay slice of the snapshot (verb write-back), so the HUD
|
|
46
|
+
* reflects a touch/mode/buy result immediately instead of waiting one
|
|
47
|
+
* poll tick (2 s).
|
|
48
|
+
*/
|
|
49
|
+
setGameplayView: (draft: PetUiState, view: PetGameplayStateView) => void;
|
|
44
50
|
};
|
|
45
51
|
/** Create the pet store handle (apply world only; never module-level). */
|
|
46
52
|
export declare function createPetStore(): EngineStoreHandle<PetUiState, PetUiActions>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pet-store.d.ts","sourceRoot":"","sources":["../../../src/client/pet-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AACpG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"pet-store.d.ts","sourceRoot":"","sources":["../../../src/client/pet-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AACpG,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,IAAI,EAAE,cAAc,GAAG,MAAM,CAAA;IAC7B,sDAAsD;IACtD,EAAE,EAAE,MAAM,CAAA;CACX;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,0EAA0E;IAC1E,IAAI,EAAE,aAAa,EAAE,CAAA;IACrB,uBAAuB;IACvB,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;IACpC,iEAAiE;IACjE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;CAC7B;AAED,uBAAuB;AACvB,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;IAChE,iCAAiC;IACjC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,IAAI,CAAA;IAC3D,gCAAgC;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvF,8BAA8B;IAC9B,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IACtE;;;;OAIG;IACH,eAAe,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAA;CACzE,CAAA;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,IAAI,iBAAiB,CAAC,UAAU,EAAE,YAAY,CAAC,CA8B5E;AAED,YAAY,EAAE,cAAc,EAAE,CAAA;AAE9B;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA"}
|
|
@@ -32,6 +32,10 @@ export function createPetStore() {
|
|
|
32
32
|
setFeedback: (draft, feedback) => {
|
|
33
33
|
draft.feedback = feedback;
|
|
34
34
|
},
|
|
35
|
+
setGameplayView: (draft, view) => {
|
|
36
|
+
if (draft.snapshot !== null)
|
|
37
|
+
draft.snapshot = { ...draft.snapshot, gameplay: view };
|
|
38
|
+
},
|
|
35
39
|
},
|
|
36
40
|
});
|
|
37
41
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frames2d visual mount — the React bridge between the pet center chrome
|
|
3
|
+
* and the imperative frames2d renderer, mirroring the live2d mount. The
|
|
4
|
+
* bridge owns the contract context (asset base, phase stream, interaction
|
|
5
|
+
* write-back, activation cleanups), feeds the polled phase into the stream,
|
|
6
|
+
* forwards the chrome's drag gesture onto the conventional 'drag' track
|
|
7
|
+
* (when the pet declares one), and renders the localized fallback card when
|
|
8
|
+
* the served config is invalid.
|
|
9
|
+
* @module @linxin666/dsh-pet/client/renderers/Frames2dVisualMount
|
|
10
|
+
*/
|
|
11
|
+
import { type ReactElement } from 'react';
|
|
12
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
13
|
+
import type { PetDefinition } from '../../registry.ts';
|
|
14
|
+
import type { ActivityPhase } from '../../state.ts';
|
|
15
|
+
import type { DragStream } from '../drag-stream.ts';
|
|
16
|
+
import type { GameplayBus } from '../gameplay-hud.tsx';
|
|
17
|
+
import type { NS } from '../locales.ts';
|
|
18
|
+
/** Mount the frames2d renderer as the sprite's visual (inside the chrome). */
|
|
19
|
+
export declare function Frames2dVisualMount(props: {
|
|
20
|
+
definition: PetDefinition;
|
|
21
|
+
phase: ActivityPhase;
|
|
22
|
+
onPet: () => void;
|
|
23
|
+
/** Chrome drag gesture stream (the renderer switch owns it). */
|
|
24
|
+
drag: DragStream;
|
|
25
|
+
/** Gameplay coordination bus; the mount registers its track override. */
|
|
26
|
+
bus?: GameplayBus;
|
|
27
|
+
t: PropsLocale<typeof NS>['t'];
|
|
28
|
+
}): ReactElement;
|
|
29
|
+
//# sourceMappingURL=Frames2dVisualMount.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Frames2dVisualMount.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/Frames2dVisualMount.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAA+B,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAInD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAEvC,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,UAAU,EAAE,aAAa,CAAA;IACzB,KAAK,EAAE,aAAa,CAAA;IACpB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,gEAAgE;IAChE,IAAI,EAAE,UAAU,CAAA;IAChB,yEAAyE;IACzE,GAAG,CAAC,EAAE,WAAW,CAAA;IACjB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;CAC/B,GAAG,YAAY,CA6Ef"}
|
|
@@ -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,
|
|
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"}
|