@linxin666/dsh-pet 0.1.15 → 0.1.16
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 +73 -38
- package/README.zh.md +91 -55
- package/assets/whale/pet.json +12 -1
- package/cordis.patch.yml +4 -4
- package/lib/client.js +270 -334
- package/lib/client.js.map +1 -1
- package/lib/index.js +641 -87
- package/lib/types/client/PetDockEntry.d.ts +14 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +13 -8
- package/lib/types/client/PetSettingsCard.d.ts +22 -9
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +42 -7
- package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
- package/lib/types/client/PetSprite.d.ts.map +1 -0
- package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -1
- package/lib/types/client/index.d.ts +11 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +30 -12
- package/lib/types/client/locales.d.ts +10 -10
- package/lib/types/client/locales.js +10 -10
- package/lib/types/client/pet-store.d.ts +5 -0
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/settings-form.d.ts +62 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +61 -12
- package/lib/types/client/spritesheet.d.ts +14 -48
- package/lib/types/client/spritesheet.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +13 -81
- package/lib/types/index.d.ts +24 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +53 -33
- package/lib/types/ledger.d.ts +4 -2
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +11 -4
- package/lib/types/persist.d.ts +10 -3
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +32 -5
- package/lib/types/registry.d.ts +158 -0
- package/lib/types/registry.d.ts.map +1 -0
- package/lib/types/registry.js +276 -0
- package/lib/types/routes.d.ts +10 -10
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +169 -51
- package/lib/types/service.d.ts +46 -13
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +73 -16
- package/package.json +2 -2
- package/src/client/PetDockEntry.tsx +34 -26
- package/src/client/PetSettingsCard.tsx +67 -22
- package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
- package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
- package/src/client/PluginSettingsCard.tsx +7 -1
- package/src/client/index.ts +36 -14
- package/src/client/locales.ts +10 -10
- package/src/client/pet-store.ts +9 -0
- package/src/client/settings-card.module.css +3 -0
- package/src/client/settings-form.ts +114 -12
- package/src/client/spritesheet.ts +16 -92
- package/src/index.test.ts +23 -0
- package/src/index.ts +89 -45
- package/src/ledger.test.ts +5 -3
- package/src/ledger.ts +11 -4
- package/src/persist.test.ts +52 -3
- package/src/persist.ts +42 -8
- package/src/registry.test.ts +132 -0
- package/src/registry.ts +400 -0
- package/src/routes.ts +166 -53
- package/src/service.ts +102 -20
- package/lib/types/client/WhalePet.d.ts.map +0 -1
package/lib/client.js
CHANGED
|
@@ -22,6 +22,7 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
|
|
23
23
|
init: () => ({
|
|
24
24
|
snapshot: null,
|
|
25
|
+
pets: [],
|
|
25
26
|
state: "loading",
|
|
26
27
|
error: null,
|
|
27
28
|
feedback: null
|
|
@@ -32,6 +33,9 @@ window.__ModuleLoader__.load({
|
|
|
32
33
|
draft.state = "ready";
|
|
33
34
|
draft.error = null;
|
|
34
35
|
},
|
|
36
|
+
setPets: (draft, pets) => {
|
|
37
|
+
draft.pets = pets;
|
|
38
|
+
},
|
|
35
39
|
setState: (draft, state, error) => {
|
|
36
40
|
draft.state = state;
|
|
37
41
|
draft.error = error;
|
|
@@ -42,194 +46,24 @@ window.__ModuleLoader__.load({
|
|
|
42
46
|
}
|
|
43
47
|
});
|
|
44
48
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
400,
|
|
64
|
-
500,
|
|
65
|
-
400,
|
|
66
|
-
400,
|
|
67
|
-
500
|
|
68
|
-
],
|
|
69
|
-
loop: true
|
|
70
|
-
},
|
|
71
|
-
"running-right": {
|
|
72
|
-
frames: [
|
|
73
|
-
0,
|
|
74
|
-
1,
|
|
75
|
-
2,
|
|
76
|
-
3,
|
|
77
|
-
4,
|
|
78
|
-
5,
|
|
79
|
-
6,
|
|
80
|
-
7
|
|
81
|
-
],
|
|
82
|
-
durations: [
|
|
83
|
-
225,
|
|
84
|
-
225,
|
|
85
|
-
225,
|
|
86
|
-
225,
|
|
87
|
-
225,
|
|
88
|
-
225,
|
|
89
|
-
225,
|
|
90
|
-
225
|
|
91
|
-
],
|
|
92
|
-
loop: true
|
|
93
|
-
},
|
|
94
|
-
"running-left": {
|
|
95
|
-
frames: [
|
|
96
|
-
0,
|
|
97
|
-
1,
|
|
98
|
-
2,
|
|
99
|
-
3,
|
|
100
|
-
4,
|
|
101
|
-
5,
|
|
102
|
-
6,
|
|
103
|
-
7
|
|
104
|
-
],
|
|
105
|
-
durations: [
|
|
106
|
-
225,
|
|
107
|
-
225,
|
|
108
|
-
225,
|
|
109
|
-
225,
|
|
110
|
-
225,
|
|
111
|
-
225,
|
|
112
|
-
225,
|
|
113
|
-
225
|
|
114
|
-
],
|
|
115
|
-
loop: true
|
|
116
|
-
},
|
|
117
|
-
waving: {
|
|
118
|
-
frames: [
|
|
119
|
-
0,
|
|
120
|
-
1,
|
|
121
|
-
2,
|
|
122
|
-
3
|
|
123
|
-
],
|
|
124
|
-
durations: [
|
|
125
|
-
350,
|
|
126
|
-
350,
|
|
127
|
-
350,
|
|
128
|
-
350
|
|
129
|
-
],
|
|
130
|
-
loop: true
|
|
131
|
-
},
|
|
132
|
-
jumping: {
|
|
133
|
-
frames: [
|
|
134
|
-
0,
|
|
135
|
-
1,
|
|
136
|
-
2,
|
|
137
|
-
3,
|
|
138
|
-
4
|
|
139
|
-
],
|
|
140
|
-
durations: [
|
|
141
|
-
300,
|
|
142
|
-
300,
|
|
143
|
-
300,
|
|
144
|
-
350,
|
|
145
|
-
350
|
|
146
|
-
],
|
|
147
|
-
loop: false,
|
|
148
|
-
fallback: "idle"
|
|
149
|
-
},
|
|
150
|
-
failed: {
|
|
151
|
-
frames: [
|
|
152
|
-
0,
|
|
153
|
-
1,
|
|
154
|
-
2,
|
|
155
|
-
3,
|
|
156
|
-
4,
|
|
157
|
-
5,
|
|
158
|
-
6,
|
|
159
|
-
7
|
|
160
|
-
],
|
|
161
|
-
durations: [
|
|
162
|
-
450,
|
|
163
|
-
450,
|
|
164
|
-
450,
|
|
165
|
-
500,
|
|
166
|
-
550,
|
|
167
|
-
600,
|
|
168
|
-
450,
|
|
169
|
-
450
|
|
170
|
-
],
|
|
171
|
-
loop: false,
|
|
172
|
-
fallback: "idle"
|
|
173
|
-
},
|
|
174
|
-
waiting: {
|
|
175
|
-
frames: [
|
|
176
|
-
0,
|
|
177
|
-
1,
|
|
178
|
-
2,
|
|
179
|
-
3,
|
|
180
|
-
4,
|
|
181
|
-
5
|
|
182
|
-
],
|
|
183
|
-
durations: [
|
|
184
|
-
450,
|
|
185
|
-
450,
|
|
186
|
-
500,
|
|
187
|
-
450,
|
|
188
|
-
450,
|
|
189
|
-
500
|
|
190
|
-
],
|
|
191
|
-
loop: true
|
|
192
|
-
},
|
|
193
|
-
running: {
|
|
194
|
-
frames: [
|
|
195
|
-
0,
|
|
196
|
-
1,
|
|
197
|
-
2,
|
|
198
|
-
3,
|
|
199
|
-
4,
|
|
200
|
-
5
|
|
201
|
-
],
|
|
202
|
-
durations: [
|
|
203
|
-
250,
|
|
204
|
-
250,
|
|
205
|
-
250,
|
|
206
|
-
250,
|
|
207
|
-
250,
|
|
208
|
-
250
|
|
209
|
-
],
|
|
210
|
-
loop: true
|
|
211
|
-
},
|
|
212
|
-
review: {
|
|
213
|
-
frames: [
|
|
214
|
-
0,
|
|
215
|
-
1,
|
|
216
|
-
2,
|
|
217
|
-
3,
|
|
218
|
-
4,
|
|
219
|
-
5
|
|
220
|
-
],
|
|
221
|
-
durations: [
|
|
222
|
-
550,
|
|
223
|
-
550,
|
|
224
|
-
550,
|
|
225
|
-
550,
|
|
226
|
-
550,
|
|
227
|
-
550
|
|
228
|
-
],
|
|
229
|
-
loop: true
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
/** Row index of one animation track (mirrors state.ts rowOf). */
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
51
|
+
function r(e) {
|
|
52
|
+
var t, f, n = "";
|
|
53
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
54
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
55
|
+
var o = e.length;
|
|
56
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
57
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
function clsx() {
|
|
61
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
62
|
+
return n;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/client/spritesheet.ts
|
|
66
|
+
/** Row index of one animation track (the fixed 9-row contract). */
|
|
233
67
|
function rowOfTrack(animation) {
|
|
234
68
|
return {
|
|
235
69
|
idle: 0,
|
|
@@ -250,52 +84,20 @@ window.__ModuleLoader__.load({
|
|
|
250
84
|
* atlas coordinates here would drift each frame by the scale factor and
|
|
251
85
|
* render torn/overlapping frames.
|
|
252
86
|
*/
|
|
253
|
-
function framePosition(row, col, scale = 1) {
|
|
87
|
+
function framePosition(cell, columns, row, col, scale = 1) {
|
|
254
88
|
return {
|
|
255
|
-
x: -col *
|
|
256
|
-
y: -row *
|
|
89
|
+
x: -col * cell.width * scale,
|
|
90
|
+
y: -row * cell.height * scale
|
|
257
91
|
};
|
|
258
92
|
}
|
|
259
93
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* @param image - the fully decoded spritesheet (natural size 1536×1872).
|
|
265
|
-
* @returns per-row frame counts, length 9.
|
|
266
|
-
*/
|
|
267
|
-
function detectFrameCounts(image) {
|
|
268
|
-
const canvas = document.createElement("canvas");
|
|
269
|
-
canvas.width = image.naturalWidth;
|
|
270
|
-
canvas.height = image.naturalHeight;
|
|
271
|
-
const ctx = canvas.getContext("2d");
|
|
272
|
-
if (ctx === null) return Array.from({ length: 9 }, () => 8);
|
|
273
|
-
ctx.drawImage(image, 0, 0);
|
|
274
|
-
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
275
|
-
const counts = [];
|
|
276
|
-
const stride = 1536;
|
|
277
|
-
const probeStep = 8;
|
|
278
|
-
const margin = 12;
|
|
279
|
-
for (let row = 0; row < 9; row++) {
|
|
280
|
-
let count = 0;
|
|
281
|
-
for (let col = 0; col < 8; col++) {
|
|
282
|
-
let hasContent = false;
|
|
283
|
-
const x0 = col * 192;
|
|
284
|
-
const y0 = row * 208;
|
|
285
|
-
for (let y = y0 + margin; y < y0 + 208 - margin && !hasContent; y += probeStep) for (let x = x0 + margin; x < x0 + 192 - margin && !hasContent; x += probeStep) if ((data[(y * stride + x) * 4 + 3] ?? 0) > 8) hasContent = true;
|
|
286
|
-
if (hasContent) count += 1;
|
|
287
|
-
}
|
|
288
|
-
counts.push(count);
|
|
289
|
-
}
|
|
290
|
-
return counts;
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Trim a track to the actual frame count of its row. A row with 0 detected
|
|
294
|
-
* frames degrades to the first frame (the atlas is still loading or corrupt)
|
|
295
|
-
* so the pet never renders blank.
|
|
94
|
+
* Trim a track to the actual frame count of its row (the manifest's per-row
|
|
95
|
+
* counts are authoritative; this is a last-line guard against a definition
|
|
96
|
+
* whose row count disagrees with its track table). A row with 0 detected
|
|
97
|
+
* frames degrades to the first frame so the pet never renders blank.
|
|
296
98
|
*/
|
|
297
99
|
function trimTrack(track, frameCount) {
|
|
298
|
-
const n = Math.max(1, Math.min(frameCount, track.frames.length));
|
|
100
|
+
const n = Math.max(1, Math.min(frameCount, track.frames.length, track.durations.length));
|
|
299
101
|
return {
|
|
300
102
|
frames: track.frames.slice(0, n),
|
|
301
103
|
durations: track.durations.slice(0, n),
|
|
@@ -332,35 +134,32 @@ window.__ModuleLoader__.load({
|
|
|
332
134
|
"summon": "kz2Bea_summon"
|
|
333
135
|
};
|
|
334
136
|
//#endregion
|
|
335
|
-
//#region src/client/
|
|
137
|
+
//#region src/client/PetSprite.tsx
|
|
336
138
|
/**
|
|
337
|
-
*
|
|
139
|
+
* Pet sprite companion component — the browser half's centerpiece. Renders a
|
|
338
140
|
* fixed-position floating sprite (React portal onto document.body), plays
|
|
339
|
-
* the
|
|
340
|
-
*
|
|
341
|
-
* reposition (persisted via setConfig).
|
|
342
|
-
*
|
|
141
|
+
* the track matching the host animation snapshot, and exposes the
|
|
142
|
+
* interaction surface: click to pet, hover panel with feed/rename/hide, drag
|
|
143
|
+
* to reposition (persisted via setConfig). Everything visual comes from the
|
|
144
|
+
* pet definition the host serves ('/api/pet/pets' + the state snapshot's
|
|
145
|
+
* pet id), so one component renders every registry entry.
|
|
146
|
+
* @module @linxin666/dsh-pet/client/PetSprite
|
|
343
147
|
*/
|
|
344
|
-
/** Browser URL of the whale-girl atlas (served by the host half's own route). */
|
|
345
|
-
const PET_SPRITESHEET_URL = "/pet/whale/spritesheet.webp";
|
|
346
|
-
/** Browser URL of the whale-girl manifest (authoritative per-row frame counts). */
|
|
347
|
-
const PET_MANIFEST_URL = "/pet/whale/pet.json";
|
|
348
148
|
/** Clamp a drag offset inside the viewport with a margin. */
|
|
349
149
|
function clampOffset(value, max) {
|
|
350
150
|
return Math.max(0, Math.min(max, value));
|
|
351
151
|
}
|
|
352
152
|
/**
|
|
353
153
|
* The floating pet. The spritesheet frame advances on requestAnimationFrame
|
|
354
|
-
* with per-frame durations from
|
|
355
|
-
* the background position is written straight to the sprite
|
|
356
|
-
* per-frame React state).
|
|
154
|
+
* with per-frame durations from the definition's tracks; the atlas image is
|
|
155
|
+
* loaded once and the background position is written straight to the sprite
|
|
156
|
+
* element (no per-frame React state).
|
|
357
157
|
*/
|
|
358
|
-
function
|
|
359
|
-
const { snapshot, display, feedback } = props;
|
|
158
|
+
function PetSprite(props) {
|
|
159
|
+
const { snapshot, definition, display, feedback } = props;
|
|
360
160
|
const spriteRef = (0, react.useRef)(null);
|
|
361
161
|
const floatRef = (0, react.useRef)(null);
|
|
362
162
|
const [imageReady, setImageReady] = (0, react.useState)(false);
|
|
363
|
-
const [frameCounts, setFrameCounts] = (0, react.useState)(null);
|
|
364
163
|
const [hovered, setHovered] = (0, react.useState)(false);
|
|
365
164
|
const [renaming, setRenaming] = (0, react.useState)(false);
|
|
366
165
|
const [nameDraft, setNameDraft] = (0, react.useState)("");
|
|
@@ -372,37 +171,32 @@ window.__ModuleLoader__.load({
|
|
|
372
171
|
index: 0,
|
|
373
172
|
elapsed: 0
|
|
374
173
|
});
|
|
174
|
+
const cell = definition.cell;
|
|
175
|
+
const columns = definition.columns;
|
|
176
|
+
const rows = definition.rows;
|
|
177
|
+
const tracks = definition.tracks;
|
|
375
178
|
(0, react.useEffect)(() => {
|
|
376
179
|
let cancelled = false;
|
|
377
180
|
const img = new Image();
|
|
378
181
|
img.onload = () => {
|
|
379
|
-
if (cancelled)
|
|
380
|
-
setImageReady(true);
|
|
381
|
-
fetch(PET_MANIFEST_URL).then((res) => res.ok ? res.json() : Promise.resolve({})).then((manifest) => {
|
|
382
|
-
if (cancelled) return;
|
|
383
|
-
const frames = manifest.frames;
|
|
384
|
-
if (Array.isArray(frames) && frames.length === 9 && frames.every((n) => typeof n === "number")) setFrameCounts(frames);
|
|
385
|
-
else setFrameCounts(detectFrameCounts(img));
|
|
386
|
-
}).catch(() => {
|
|
387
|
-
if (!cancelled) setFrameCounts(detectFrameCounts(img));
|
|
388
|
-
});
|
|
182
|
+
if (!cancelled) setImageReady(true);
|
|
389
183
|
};
|
|
390
|
-
img.src =
|
|
184
|
+
img.src = definition.atlasUrl;
|
|
391
185
|
return () => {
|
|
392
186
|
cancelled = true;
|
|
393
187
|
img.onload = null;
|
|
394
188
|
};
|
|
395
|
-
}, []);
|
|
396
|
-
const spriteScale = display.size /
|
|
189
|
+
}, [definition.atlasUrl]);
|
|
190
|
+
const spriteScale = display.size / cell.height;
|
|
397
191
|
const animation = snapshot?.animation ?? "idle";
|
|
398
192
|
const scaleRef = (0, react.useRef)(spriteScale);
|
|
399
193
|
scaleRef.current = spriteScale;
|
|
400
194
|
(0, react.useEffect)(() => {
|
|
401
195
|
const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
|
|
402
196
|
const row = rowOfTrack(animation);
|
|
403
|
-
const leadCol =
|
|
404
|
-
const lead = framePosition(row, leadCol, scaleRef.current);
|
|
405
|
-
if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition =
|
|
197
|
+
const leadCol = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length).frames[0];
|
|
198
|
+
const lead = framePosition(cell, columns, row, leadCol, scaleRef.current);
|
|
199
|
+
if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition = lead.x + "px " + lead.y + "px";
|
|
406
200
|
if (reduceMotion) return;
|
|
407
201
|
let raf = 0;
|
|
408
202
|
let last = performance.now();
|
|
@@ -410,7 +204,7 @@ window.__ModuleLoader__.load({
|
|
|
410
204
|
const delta = ts - last;
|
|
411
205
|
last = ts;
|
|
412
206
|
const row = rowOfTrack(animation);
|
|
413
|
-
const track =
|
|
207
|
+
const track = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length);
|
|
414
208
|
const st = frameRef.current;
|
|
415
209
|
if (st.track !== animation) {
|
|
416
210
|
st.track = animation;
|
|
@@ -428,13 +222,19 @@ window.__ModuleLoader__.load({
|
|
|
428
222
|
st.index = 0;
|
|
429
223
|
} else st.index = maxIndex;
|
|
430
224
|
const col = track.frames[st.index];
|
|
431
|
-
const
|
|
432
|
-
if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition =
|
|
225
|
+
const pos = framePosition(cell, columns, row, col, scaleRef.current);
|
|
226
|
+
if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition = pos.x + "px " + pos.y + "px";
|
|
433
227
|
raf = requestAnimationFrame(tick);
|
|
434
228
|
};
|
|
435
229
|
raf = requestAnimationFrame(tick);
|
|
436
230
|
return () => cancelAnimationFrame(raf);
|
|
437
|
-
}, [
|
|
231
|
+
}, [
|
|
232
|
+
animation,
|
|
233
|
+
cell,
|
|
234
|
+
columns,
|
|
235
|
+
rows,
|
|
236
|
+
tracks
|
|
237
|
+
]);
|
|
438
238
|
const feedbackDoneRef = (0, react.useRef)(props.onFeedbackDone);
|
|
439
239
|
feedbackDoneRef.current = props.onFeedbackDone;
|
|
440
240
|
(0, react.useEffect)(() => {
|
|
@@ -486,9 +286,10 @@ window.__ModuleLoader__.load({
|
|
|
486
286
|
right: display.right,
|
|
487
287
|
bottom: display.bottom
|
|
488
288
|
};
|
|
489
|
-
const spriteWidth = Math.round(
|
|
490
|
-
const spriteHeight = Math.round(
|
|
289
|
+
const spriteWidth = Math.round(cell.width * spriteScale);
|
|
290
|
+
const spriteHeight = Math.round(cell.height * spriteScale);
|
|
491
291
|
const statusBubble = feedback === null && !hovered ? snapshot?.bubble : void 0;
|
|
292
|
+
const displayName = snapshot?.name ?? definition.displayName;
|
|
492
293
|
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
493
294
|
ref: floatRef,
|
|
494
295
|
className: pet_module_css_default.float,
|
|
@@ -514,8 +315,8 @@ window.__ModuleLoader__.load({
|
|
|
514
315
|
style: {
|
|
515
316
|
width: spriteWidth,
|
|
516
317
|
height: spriteHeight,
|
|
517
|
-
backgroundImage: imageReady ?
|
|
518
|
-
backgroundSize:
|
|
318
|
+
backgroundImage: imageReady ? "url(" + definition.atlasUrl + ")" : void 0,
|
|
319
|
+
backgroundSize: cell.width * columns * spriteScale + "px " + cell.height * rows.length * spriteScale + "px",
|
|
519
320
|
backgroundRepeat: "no-repeat",
|
|
520
321
|
backgroundPosition: "0 0",
|
|
521
322
|
cursor: dragRef.current === null ? "grab" : "grabbing"
|
|
@@ -528,14 +329,14 @@ window.__ModuleLoader__.load({
|
|
|
528
329
|
props.onPet();
|
|
529
330
|
},
|
|
530
331
|
role: "button",
|
|
531
|
-
"aria-label":
|
|
332
|
+
"aria-label": definition.displayName
|
|
532
333
|
}),
|
|
533
334
|
feedback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
534
|
-
className:
|
|
335
|
+
className: clsx(pet_module_css_default.bubble, feedback.kind === "feed" ? pet_module_css_default.bubbleFeed : pet_module_css_default.bubblePet),
|
|
535
336
|
children: feedback.text
|
|
536
337
|
}, feedback.at),
|
|
537
338
|
statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
538
|
-
className:
|
|
339
|
+
className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
|
|
539
340
|
role: "status",
|
|
540
341
|
"aria-live": "polite",
|
|
541
342
|
children: statusBubble
|
|
@@ -581,7 +382,7 @@ window.__ModuleLoader__.load({
|
|
|
581
382
|
className: pet_module_css_default.rankRow,
|
|
582
383
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
583
384
|
className: pet_module_css_default.nameCell,
|
|
584
|
-
children:
|
|
385
|
+
children: displayName
|
|
585
386
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: props.t("pet.rank", { rank: snapshot?.affinity.rank ?? "?" }) })]
|
|
586
387
|
}),
|
|
587
388
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -601,7 +402,7 @@ window.__ModuleLoader__.load({
|
|
|
601
402
|
type: "button",
|
|
602
403
|
className: pet_module_css_default.action,
|
|
603
404
|
onClick: () => {
|
|
604
|
-
setNameDraft(
|
|
405
|
+
setNameDraft(displayName);
|
|
605
406
|
setRenaming(true);
|
|
606
407
|
},
|
|
607
408
|
children: props.t("pet.rename")
|
|
@@ -623,12 +424,14 @@ window.__ModuleLoader__.load({
|
|
|
623
424
|
//#region src/client/PetDockEntry.tsx
|
|
624
425
|
/**
|
|
625
426
|
* Global floating pet entry. The pet is host-global (its state, display and
|
|
626
|
-
* interactions live on
|
|
427
|
+
* interactions live on '/api/pet/*' endpoints with no session dimension), so
|
|
627
428
|
* it must not ride a session-scoped slot — on the new-conversation screen no
|
|
628
429
|
* session exists to scope a slot by, and the pet would vanish (issue #48).
|
|
629
|
-
* The client half therefore mounts this entry straight onto
|
|
630
|
-
* (see index.ts): while visible it renders the floating
|
|
631
|
-
* while hidden it renders a fixed-position summon button.
|
|
430
|
+
* The client half therefore mounts this entry straight onto 'document.body'
|
|
431
|
+
* (see index.ts): while visible it renders the floating PetSprite (a
|
|
432
|
+
* portal), while hidden it renders a fixed-position summon button. Which
|
|
433
|
+
* sprite renders is decided by the host snapshot's pet id resolved against
|
|
434
|
+
* the registry list — no per-pet component exists.
|
|
632
435
|
* @module @linxin666/dsh-pet/client/PetDockEntry
|
|
633
436
|
*/
|
|
634
437
|
const DEFAULT_DISPLAY = {
|
|
@@ -638,7 +441,7 @@ window.__ModuleLoader__.load({
|
|
|
638
441
|
bottom: 20
|
|
639
442
|
};
|
|
640
443
|
/**
|
|
641
|
-
* Dock entry: while the pet is visible, mount the floating
|
|
444
|
+
* Dock entry: while the pet is visible, mount the floating PetSprite (it
|
|
642
445
|
* portals itself onto document.body); while hidden, render the summon
|
|
643
446
|
* button so the pet can always come back. The store is the plugin-owned
|
|
644
447
|
* single instance — the slot system provides none because the pet is
|
|
@@ -649,6 +452,7 @@ window.__ModuleLoader__.load({
|
|
|
649
452
|
const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
|
|
650
453
|
const snapshot = ui.snapshot;
|
|
651
454
|
const feedback = ui.feedback;
|
|
455
|
+
const definition = ui.pets.find((entry) => entry.id === snapshot?.pet.id) ?? null;
|
|
652
456
|
const visible = snapshot?.display.visible ?? true;
|
|
653
457
|
(0, react.useEffect)(() => {
|
|
654
458
|
ensure();
|
|
@@ -656,9 +460,10 @@ window.__ModuleLoader__.load({
|
|
|
656
460
|
if (visible) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
657
461
|
"data-pet-dock": true,
|
|
658
462
|
"data-testid": "pet-dock",
|
|
659
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
463
|
+
children: snapshot === null || definition === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetSprite, {
|
|
660
464
|
snapshot,
|
|
661
|
-
|
|
465
|
+
definition,
|
|
466
|
+
display: snapshot.display,
|
|
662
467
|
feedback,
|
|
663
468
|
onPet: props.pet,
|
|
664
469
|
onFeed: props.feed,
|
|
@@ -681,12 +486,12 @@ window.__ModuleLoader__.load({
|
|
|
681
486
|
},
|
|
682
487
|
onClick: props.summon,
|
|
683
488
|
"data-testid": "pet-summon",
|
|
684
|
-
children: props.t("pet.summon", { name: snapshot?.name ?? "
|
|
489
|
+
children: props.t("pet.summon", { name: snapshot?.name ?? "" })
|
|
685
490
|
});
|
|
686
491
|
}
|
|
687
492
|
//#endregion
|
|
688
493
|
//#region \0dsh-css:packages/dsh-pet/src/client/settings-card.module.css.mjs
|
|
689
|
-
const css = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save{transition:none}}";
|
|
494
|
+
const css = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save{transition:none}}";
|
|
690
495
|
const tagId = "@linxin666/dsh-pet/settings-card.module.css";
|
|
691
496
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
692
497
|
const tag = document.createElement("style");
|
|
@@ -842,10 +647,10 @@ window.__ModuleLoader__.load({
|
|
|
842
647
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
843
648
|
className: settings_card_module_css_default.footer,
|
|
844
649
|
children: [
|
|
845
|
-
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
650
|
+
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
846
651
|
className: settings_card_module_css_default.failed,
|
|
847
652
|
role: "status",
|
|
848
|
-
children: props.t("settings.saveFailed")
|
|
653
|
+
children: [props.t("settings.saveFailed"), state.failedReason ? " - " + state.failedReason : ""]
|
|
849
654
|
}) : null,
|
|
850
655
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
851
656
|
type: "button",
|
|
@@ -967,6 +772,54 @@ window.__ModuleLoader__.load({
|
|
|
967
772
|
]
|
|
968
773
|
});
|
|
969
774
|
}
|
|
775
|
+
/** A staged enumerated field rendered as a select. */
|
|
776
|
+
function ChoiceField(props) {
|
|
777
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
778
|
+
className: settings_card_module_css_default.field,
|
|
779
|
+
children: [
|
|
780
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
781
|
+
className: settings_card_module_css_default.head,
|
|
782
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
783
|
+
className: settings_card_module_css_default.label,
|
|
784
|
+
htmlFor: props.id,
|
|
785
|
+
children: props.label
|
|
786
|
+
}), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
787
|
+
className: settings_card_module_css_default.badges,
|
|
788
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
789
|
+
className: settings_card_module_css_default.badge,
|
|
790
|
+
children: props.overriddenLabel
|
|
791
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
792
|
+
type: "button",
|
|
793
|
+
className: settings_card_module_css_default.reset,
|
|
794
|
+
disabled: props.disabled,
|
|
795
|
+
onClick: props.onReset,
|
|
796
|
+
children: props.resetLabel
|
|
797
|
+
})]
|
|
798
|
+
}) : null]
|
|
799
|
+
}),
|
|
800
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
801
|
+
id: props.id,
|
|
802
|
+
className: settings_card_module_css_default.select,
|
|
803
|
+
value: props.text,
|
|
804
|
+
disabled: props.disabled,
|
|
805
|
+
onChange: (event) => {
|
|
806
|
+
props.onEdit(event.target.value);
|
|
807
|
+
},
|
|
808
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
809
|
+
value: "",
|
|
810
|
+
children: props.inheritLabel
|
|
811
|
+
}), props.choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
812
|
+
value: choice.value,
|
|
813
|
+
children: choice.label
|
|
814
|
+
}, choice.value))]
|
|
815
|
+
}),
|
|
816
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
817
|
+
className: props.invalid ? settings_card_module_css_default.invalid : settings_card_module_css_default.hint,
|
|
818
|
+
children: props.invalid ? props.invalidLabel : props.hint
|
|
819
|
+
})
|
|
820
|
+
]
|
|
821
|
+
});
|
|
822
|
+
}
|
|
970
823
|
//#endregion
|
|
971
824
|
//#region src/client/settings-form.ts
|
|
972
825
|
/** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints blocks the save. */
|
|
@@ -989,20 +842,6 @@ window.__ModuleLoader__.load({
|
|
|
989
842
|
}
|
|
990
843
|
};
|
|
991
844
|
}
|
|
992
|
-
/** A free-text field. An empty draft clears the field. */
|
|
993
|
-
function textField(field) {
|
|
994
|
-
return {
|
|
995
|
-
field,
|
|
996
|
-
format: (value) => typeof value === "string" ? value : "",
|
|
997
|
-
parse: (text) => {
|
|
998
|
-
const trimmed = text.trim();
|
|
999
|
-
return trimmed === "" ? { kind: "clear" } : {
|
|
1000
|
-
kind: "set",
|
|
1001
|
-
value: trimmed
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
845
|
/** A boolean field, edited through true/false draft text. */
|
|
1007
846
|
function booleanField(field) {
|
|
1008
847
|
return {
|
|
@@ -1022,6 +861,20 @@ window.__ModuleLoader__.load({
|
|
|
1022
861
|
}
|
|
1023
862
|
};
|
|
1024
863
|
}
|
|
864
|
+
/** An enumerated string field; only the listed choices are accepted. An empty draft clears the field. */
|
|
865
|
+
function choiceField(field, choices) {
|
|
866
|
+
return {
|
|
867
|
+
field,
|
|
868
|
+
format: (value) => typeof value === "string" && choices.includes(value) ? value : "",
|
|
869
|
+
parse: (text) => {
|
|
870
|
+
if (text === "") return { kind: "clear" };
|
|
871
|
+
return choices.includes(text) ? {
|
|
872
|
+
kind: "set",
|
|
873
|
+
value: text
|
|
874
|
+
} : void 0;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
1025
878
|
/**
|
|
1026
879
|
* Stages one card's edits over one settings namespace and writes them on save.
|
|
1027
880
|
*
|
|
@@ -1037,6 +890,7 @@ window.__ModuleLoader__.load({
|
|
|
1037
890
|
listeners = /* @__PURE__ */ new Set();
|
|
1038
891
|
saving = false;
|
|
1039
892
|
failed = false;
|
|
893
|
+
failedReason;
|
|
1040
894
|
/** @param scope - the bound settings scope for this card's namespace. */
|
|
1041
895
|
constructor(scope, specs) {
|
|
1042
896
|
this.scope = scope;
|
|
@@ -1064,7 +918,8 @@ window.__ModuleLoader__.load({
|
|
|
1064
918
|
dirty: plan.length > 0,
|
|
1065
919
|
invalid: plan.some((item) => item.run === void 0),
|
|
1066
920
|
saving: this.saving,
|
|
1067
|
-
failed: this.failed
|
|
921
|
+
failed: this.failed,
|
|
922
|
+
...this.failedReason === void 0 ? {} : { failedReason: this.failedReason }
|
|
1068
923
|
};
|
|
1069
924
|
}
|
|
1070
925
|
/** Read one field's state from the effective section and its staged draft. */
|
|
@@ -1105,29 +960,50 @@ window.__ModuleLoader__.load({
|
|
|
1105
960
|
if (this.staged.size === 0 && !this.failed) return;
|
|
1106
961
|
this.staged.clear();
|
|
1107
962
|
this.failed = false;
|
|
963
|
+
this.failedReason = void 0;
|
|
1108
964
|
this.publish();
|
|
1109
965
|
}
|
|
1110
966
|
};
|
|
1111
967
|
}
|
|
1112
968
|
/**
|
|
1113
969
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
970
|
+
*
|
|
971
|
+
* When the scope carries the optional batch surface (the dsh-web-ui
|
|
972
|
+
* bridge scope), every planned write rides one mutation so cross-field
|
|
973
|
+
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
974
|
+
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|
|
975
|
+
* A field lands only when the Host reports it held the staged value; a
|
|
976
|
+
* landed field's draft is dropped, a failed one stays staged for the user.
|
|
1114
977
|
* @returns settlement after every write and the read-back.
|
|
1115
978
|
*/
|
|
1116
979
|
async save() {
|
|
1117
980
|
const plan = this.plan();
|
|
1118
|
-
const
|
|
1119
|
-
if (plan.length === 0 || this.saving ||
|
|
981
|
+
const valid = plan.filter((item) => item.run !== void 0);
|
|
982
|
+
if (plan.length === 0 || this.saving || valid.length !== plan.length) return;
|
|
983
|
+
const plannedWrites = valid.map((item) => item.op);
|
|
1120
984
|
const fields = new Set(plan.map((item) => item.field));
|
|
1121
985
|
this.saving = true;
|
|
1122
986
|
this.failed = false;
|
|
987
|
+
this.failedReason = void 0;
|
|
1123
988
|
this.publish();
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
if (
|
|
989
|
+
const landed = /* @__PURE__ */ new Set();
|
|
990
|
+
const batch = this.batchedScope();
|
|
991
|
+
if (batch !== void 0) {
|
|
992
|
+
const result = await batch.mutate(plannedWrites);
|
|
993
|
+
if (result.ok) {
|
|
994
|
+
for (const field of result.fields) if (field.landed) landed.add(field.field);
|
|
995
|
+
} else this.failedReason = result.message;
|
|
996
|
+
} else for (const item of valid) if (await item.run()) landed.add(item.field);
|
|
997
|
+
for (const field of fields) if (landed.has(field)) this.staged.delete(field);
|
|
1127
998
|
this.saving = false;
|
|
1128
|
-
this.failed =
|
|
999
|
+
this.failed = landed.size !== fields.size;
|
|
1129
1000
|
this.publish();
|
|
1130
1001
|
}
|
|
1002
|
+
/** The scope's batch surface when it supports one; undefined conservatively otherwise. */
|
|
1003
|
+
batchedScope() {
|
|
1004
|
+
const candidate = this.scope;
|
|
1005
|
+
return typeof candidate?.mutate === "function" ? candidate : void 0;
|
|
1006
|
+
}
|
|
1131
1007
|
/**
|
|
1132
1008
|
* Every staged edit a save would write. An entry whose draft is not a value
|
|
1133
1009
|
* its field accepts carries no write: the form is still dirty, and the save
|
|
@@ -1142,6 +1018,10 @@ window.__ModuleLoader__.load({
|
|
|
1142
1018
|
if (staged.clear) {
|
|
1143
1019
|
if (this.stored(field)) plan.push({
|
|
1144
1020
|
field,
|
|
1021
|
+
op: {
|
|
1022
|
+
field,
|
|
1023
|
+
op: "unset"
|
|
1024
|
+
},
|
|
1145
1025
|
run: () => this.clear(field)
|
|
1146
1026
|
});
|
|
1147
1027
|
continue;
|
|
@@ -1150,14 +1030,27 @@ window.__ModuleLoader__.load({
|
|
|
1150
1030
|
const write = spec.parse(staged.text);
|
|
1151
1031
|
if (write === void 0) plan.push({
|
|
1152
1032
|
field,
|
|
1033
|
+
op: {
|
|
1034
|
+
field,
|
|
1035
|
+
op: "unset"
|
|
1036
|
+
},
|
|
1153
1037
|
run: void 0
|
|
1154
1038
|
});
|
|
1155
1039
|
else if (write.kind === "clear") plan.push({
|
|
1156
1040
|
field,
|
|
1041
|
+
op: {
|
|
1042
|
+
field,
|
|
1043
|
+
op: "unset"
|
|
1044
|
+
},
|
|
1157
1045
|
run: () => this.clear(field)
|
|
1158
1046
|
});
|
|
1159
1047
|
else plan.push({
|
|
1160
1048
|
field,
|
|
1049
|
+
op: {
|
|
1050
|
+
field,
|
|
1051
|
+
op: "set",
|
|
1052
|
+
value: write.value
|
|
1053
|
+
},
|
|
1161
1054
|
run: () => this.store(field, write.value)
|
|
1162
1055
|
});
|
|
1163
1056
|
}
|
|
@@ -1169,11 +1062,13 @@ window.__ModuleLoader__.load({
|
|
|
1169
1062
|
}
|
|
1170
1063
|
async store(field, value) {
|
|
1171
1064
|
await this.scope.set(field, value);
|
|
1065
|
+
if (this.specOf(field).secret) return true;
|
|
1172
1066
|
return this.userLayer()?.[field] === value;
|
|
1173
1067
|
}
|
|
1174
1068
|
stage(field, edit) {
|
|
1175
1069
|
this.staged.set(field, edit);
|
|
1176
1070
|
this.failed = false;
|
|
1071
|
+
this.failedReason = void 0;
|
|
1177
1072
|
this.publish();
|
|
1178
1073
|
}
|
|
1179
1074
|
specOf(field) {
|
|
@@ -1203,11 +1098,21 @@ window.__ModuleLoader__.load({
|
|
|
1203
1098
|
};
|
|
1204
1099
|
//#endregion
|
|
1205
1100
|
//#region src/client/PetSettingsCard.tsx
|
|
1206
|
-
/**
|
|
1101
|
+
/** Fetch the registry list (the same data the sprite renders from). */
|
|
1102
|
+
async function fetchPetChoices() {
|
|
1103
|
+
const response = await fetch("/api/pet/pets");
|
|
1104
|
+
if (!response.ok) throw new Error("pet pets failed: " + response.status);
|
|
1105
|
+
return await response.json();
|
|
1106
|
+
}
|
|
1107
|
+
/** Bridges the 'pet' scope onto the card's staged form. */
|
|
1207
1108
|
var PetSettingsCardController = class {
|
|
1208
1109
|
form;
|
|
1209
1110
|
store;
|
|
1210
|
-
|
|
1111
|
+
petChoices = [];
|
|
1112
|
+
petLabels = /* @__PURE__ */ new Map();
|
|
1113
|
+
loaded = false;
|
|
1114
|
+
attempts = 0;
|
|
1115
|
+
/** @param scope - the bound settings scope for the 'pet' namespace. */
|
|
1211
1116
|
constructor(scope) {
|
|
1212
1117
|
this.form = new CardForm(scope, [
|
|
1213
1118
|
booleanField("enabled"),
|
|
@@ -1215,9 +1120,26 @@ window.__ModuleLoader__.load({
|
|
|
1215
1120
|
numberField("size"),
|
|
1216
1121
|
numberField("right"),
|
|
1217
1122
|
numberField("bottom"),
|
|
1218
|
-
|
|
1123
|
+
choiceField("petId", this.petChoices)
|
|
1219
1124
|
]);
|
|
1220
1125
|
this.store = this.form.bind(() => this.projection());
|
|
1126
|
+
this.loadPets();
|
|
1127
|
+
}
|
|
1128
|
+
/** Resolve the registry choices once (retried a few times on failure). */
|
|
1129
|
+
async loadPets() {
|
|
1130
|
+
if (this.loaded) return;
|
|
1131
|
+
try {
|
|
1132
|
+
const list = await fetchPetChoices();
|
|
1133
|
+
this.petChoices.splice(0, this.petChoices.length, ...list.map((choice) => choice.id));
|
|
1134
|
+
for (const choice of list) this.petLabels.set(choice.id, choice.displayName);
|
|
1135
|
+
this.loaded = true;
|
|
1136
|
+
this.store.set(this.projection());
|
|
1137
|
+
} catch {
|
|
1138
|
+
this.attempts += 1;
|
|
1139
|
+
if (this.attempts < 3) window.setTimeout(() => {
|
|
1140
|
+
this.loadPets();
|
|
1141
|
+
}, 3e3);
|
|
1142
|
+
}
|
|
1221
1143
|
}
|
|
1222
1144
|
projection() {
|
|
1223
1145
|
return {
|
|
@@ -1227,7 +1149,11 @@ window.__ModuleLoader__.load({
|
|
|
1227
1149
|
size: this.form.field("size"),
|
|
1228
1150
|
right: this.form.field("right"),
|
|
1229
1151
|
bottom: this.form.field("bottom"),
|
|
1230
|
-
|
|
1152
|
+
petId: this.form.field("petId"),
|
|
1153
|
+
petChoices: this.petChoices.map((id) => ({
|
|
1154
|
+
value: id,
|
|
1155
|
+
label: this.petLabels.get(id) ?? id
|
|
1156
|
+
}))
|
|
1231
1157
|
};
|
|
1232
1158
|
}
|
|
1233
1159
|
/**
|
|
@@ -1280,6 +1206,21 @@ window.__ModuleLoader__.load({
|
|
|
1280
1206
|
props.resetField("enabled");
|
|
1281
1207
|
}
|
|
1282
1208
|
}),
|
|
1209
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChoiceField, {
|
|
1210
|
+
id: "settings-pet-pet",
|
|
1211
|
+
label: t("settings.pet"),
|
|
1212
|
+
hint: t("settings.petHint"),
|
|
1213
|
+
inheritLabel: t("settings.inherit"),
|
|
1214
|
+
...fieldProps,
|
|
1215
|
+
...state.petId,
|
|
1216
|
+
choices: state.petChoices,
|
|
1217
|
+
onEdit: (text) => {
|
|
1218
|
+
props.edit("petId", text);
|
|
1219
|
+
},
|
|
1220
|
+
onReset: () => {
|
|
1221
|
+
props.resetField("petId");
|
|
1222
|
+
}
|
|
1223
|
+
}),
|
|
1283
1224
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
1284
1225
|
id: "settings-pet-visible",
|
|
1285
1226
|
label: t("settings.visible"),
|
|
@@ -1337,19 +1278,6 @@ window.__ModuleLoader__.load({
|
|
|
1337
1278
|
onReset: () => {
|
|
1338
1279
|
props.resetField("bottom");
|
|
1339
1280
|
}
|
|
1340
|
-
}),
|
|
1341
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
|
|
1342
|
-
id: "settings-pet-name",
|
|
1343
|
-
label: t("settings.name"),
|
|
1344
|
-
hint: t("settings.nameHint"),
|
|
1345
|
-
...fieldProps,
|
|
1346
|
-
...state.name,
|
|
1347
|
-
onEdit: (text) => {
|
|
1348
|
-
props.edit("name", text);
|
|
1349
|
-
},
|
|
1350
|
-
onReset: () => {
|
|
1351
|
-
props.resetField("name");
|
|
1352
|
-
}
|
|
1353
1281
|
})
|
|
1354
1282
|
]
|
|
1355
1283
|
});
|
|
@@ -1367,10 +1295,12 @@ window.__ModuleLoader__.load({
|
|
|
1367
1295
|
"pet.rank": "亲密度 {rank}",
|
|
1368
1296
|
"pet.points": "{points} 点",
|
|
1369
1297
|
"pet.treats": "小鱼干 ×{n}",
|
|
1370
|
-
"pet.state.loading": "
|
|
1371
|
-
"pet.state.error": "
|
|
1298
|
+
"pet.state.loading": "宠物正在赶来…",
|
|
1299
|
+
"pet.state.error": "宠物迷路了(连接失败)",
|
|
1372
1300
|
"settings.title": "宠物",
|
|
1373
|
-
"settings.description": "
|
|
1301
|
+
"settings.description": "选择宠物并调整它的显示布局。",
|
|
1302
|
+
"settings.pet": "宠物",
|
|
1303
|
+
"settings.petHint": "选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。",
|
|
1374
1304
|
"settings.enabled": "启用宠物",
|
|
1375
1305
|
"settings.enabledHint": "关闭后隐藏宠物并停止轮询,可在设置里重新启用。",
|
|
1376
1306
|
"settings.visible": "显示宠物",
|
|
@@ -1381,8 +1311,6 @@ window.__ModuleLoader__.load({
|
|
|
1381
1311
|
"settings.rightHint": "距视口右边缘的水平内缩距离。",
|
|
1382
1312
|
"settings.bottom": "距底部(px)",
|
|
1383
1313
|
"settings.bottomHint": "距视口底边的垂直内缩距离。",
|
|
1384
|
-
"settings.name": "名字",
|
|
1385
|
-
"settings.nameHint": "宠物显示名,1–20 个字符。",
|
|
1386
1314
|
"settings.inherit": "继承",
|
|
1387
1315
|
"settings.on": "开",
|
|
1388
1316
|
"settings.off": "关",
|
|
@@ -1410,10 +1338,12 @@ window.__ModuleLoader__.load({
|
|
|
1410
1338
|
"pet.rank": "Affinity {rank}",
|
|
1411
1339
|
"pet.points": "{points} pts",
|
|
1412
1340
|
"pet.treats": "Treats ×{n}",
|
|
1413
|
-
"pet.state.loading": "The
|
|
1414
|
-
"pet.state.error": "The
|
|
1341
|
+
"pet.state.loading": "The pet is on its way…",
|
|
1342
|
+
"pet.state.error": "The pet is lost (connection failed)",
|
|
1415
1343
|
"settings.title": "Pet",
|
|
1416
|
-
"settings.description": "
|
|
1344
|
+
"settings.description": "Pick a pet and tune its display layout.",
|
|
1345
|
+
"settings.pet": "Pet",
|
|
1346
|
+
"settings.petHint": "Choose which pet shows. Names are stored per pet; rename from the pet hover panel.",
|
|
1417
1347
|
"settings.enabled": "Enable the pet",
|
|
1418
1348
|
"settings.enabledHint": "When off, the pet hides and polling stops; re-enable it here.",
|
|
1419
1349
|
"settings.visible": "Show the pet",
|
|
@@ -1424,8 +1354,6 @@ window.__ModuleLoader__.load({
|
|
|
1424
1354
|
"settings.rightHint": "Horizontal inset from the viewport right edge.",
|
|
1425
1355
|
"settings.bottom": "Bottom inset (px)",
|
|
1426
1356
|
"settings.bottomHint": "Vertical inset from the viewport bottom edge.",
|
|
1427
|
-
"settings.name": "Name",
|
|
1428
|
-
"settings.nameHint": "The pet’s display name, 1–20 characters.",
|
|
1429
1357
|
"settings.inherit": "Inherit",
|
|
1430
1358
|
"settings.on": "On",
|
|
1431
1359
|
"settings.off": "Off",
|
|
@@ -1473,16 +1401,18 @@ window.__ModuleLoader__.load({
|
|
|
1473
1401
|
headers: { "content-type": "application/json" },
|
|
1474
1402
|
body: JSON.stringify(body)
|
|
1475
1403
|
});
|
|
1476
|
-
if (!response.ok) throw new Error(
|
|
1404
|
+
if (!response.ok) throw new Error("pet " + path + " failed: " + response.status);
|
|
1477
1405
|
return await response.json();
|
|
1478
1406
|
}
|
|
1479
1407
|
/** The live host API instance (always defined; failures surface per call). */
|
|
1480
1408
|
const petApi = {
|
|
1481
1409
|
state: () => petFetch("/api/pet/state"),
|
|
1410
|
+
pets: () => petFetch("/api/pet/pets"),
|
|
1482
1411
|
interact: (kind) => petFetch("/api/pet/interact", { kind }),
|
|
1483
1412
|
setVisible: (visible) => petFetch("/api/pet/set-visible", { visible }),
|
|
1484
1413
|
setConfig: (patch) => petFetch("/api/pet/set-config", patch),
|
|
1485
|
-
setName: (name) => petFetch("/api/pet/set-name", { name })
|
|
1414
|
+
setName: (name) => petFetch("/api/pet/set-name", { name }),
|
|
1415
|
+
setPet: (petId) => petFetch("/api/pet/set-pet", { petId })
|
|
1486
1416
|
};
|
|
1487
1417
|
/** Poll interval for the host snapshot. */
|
|
1488
1418
|
const POLL_MS = 2e3;
|
|
@@ -1525,9 +1455,15 @@ window.__ModuleLoader__.load({
|
|
|
1525
1455
|
if (enabled() && disposeUi === void 0) {
|
|
1526
1456
|
const petStore = createPetStore().create();
|
|
1527
1457
|
const setSnapshot = petStore.actions.setSnapshot;
|
|
1458
|
+
const setPets = petStore.actions.setPets;
|
|
1528
1459
|
const setState = petStore.actions.setState;
|
|
1529
1460
|
const setFeedback = petStore.actions.setFeedback;
|
|
1461
|
+
let petsLoaded = false;
|
|
1530
1462
|
const pollNow = () => {
|
|
1463
|
+
if (!petsLoaded) petApi.pets().then((list) => {
|
|
1464
|
+
petsLoaded = true;
|
|
1465
|
+
setPets(list);
|
|
1466
|
+
}, () => {});
|
|
1531
1467
|
petApi.state().then((snapshot) => {
|
|
1532
1468
|
setSnapshot(snapshot);
|
|
1533
1469
|
}, () => {
|