@linxin666/dsh-pet 0.3.20 → 0.3.22
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/lib/client.js +129 -27
- package/lib/client.js.map +1 -1
- package/lib/index.js +99 -4
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +5 -1
- package/lib/types/client/gameplay-hud.d.ts +15 -0
- package/lib/types/client/gameplay-hud.d.ts.map +1 -1
- package/lib/types/client/gameplay-hud.js +48 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +21 -13
- package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -1
- package/lib/types/client/renderers/Frames2dVisualMount.js +5 -0
- package/lib/types/client/renderers/frames2d.d.ts +5 -4
- package/lib/types/client/renderers/frames2d.d.ts.map +1 -1
- package/lib/types/client/renderers/frames2d.js +27 -18
- package/lib/types/client/work-tick-gate.d.ts +40 -0
- package/lib/types/client/work-tick-gate.d.ts.map +1 -0
- package/lib/types/client/work-tick-gate.js +49 -0
- package/lib/types/gameplay.d.ts +4 -0
- package/lib/types/gameplay.d.ts.map +1 -1
- package/lib/types/gameplay.js +11 -2
- package/lib/types/ledger.d.ts +8 -0
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +25 -0
- package/lib/types/persist.d.ts +6 -0
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +25 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +6 -0
- package/lib/types/service.d.ts +24 -0
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +32 -0
- package/package.json +1 -1
- package/src/client/PetDockEntry.test.tsx +1 -0
- package/src/client/PetSprite.test.tsx +62 -0
- package/src/client/PetSprite.tsx +5 -1
- package/src/client/gameplay-hud.test.tsx +165 -2
- package/src/client/gameplay-hud.tsx +62 -6
- package/src/client/index.ts +23 -13
- package/src/client/pet.module.css +1 -1
- package/src/client/renderers/Frames2dVisualMount.test.tsx +73 -0
- package/src/client/renderers/Frames2dVisualMount.tsx +4 -0
- package/src/client/renderers/frames2d.test.ts +38 -6
- package/src/client/renderers/frames2d.ts +27 -19
- package/src/client/work-tick-gate.test.ts +53 -0
- package/src/client/work-tick-gate.ts +63 -0
- package/src/gameplay.test.ts +36 -0
- package/src/gameplay.ts +14 -2
- package/src/ledger.test.ts +19 -0
- package/src/ledger.ts +24 -0
- package/src/persist.test.ts +13 -0
- package/src/persist.ts +29 -1
- package/src/routes.ts +5 -0
- package/src/service.ts +38 -0
package/lib/client.js
CHANGED
|
@@ -75,6 +75,55 @@ window.__ModuleLoader__.load({
|
|
|
75
75
|
return previous !== null && JSON.stringify(previous) === JSON.stringify(next);
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
|
+
//#region src/client/work-tick-gate.ts
|
|
79
|
+
/**
|
|
80
|
+
* Page-wide work-tick gate for the pet's work mode. Hot reloads can leave
|
|
81
|
+
* several GameplayHud instances alive, each running its own work interval;
|
|
82
|
+
* without a shared gate every interval would call workTick and each stale call
|
|
83
|
+
* re-rolls, re-grants treats and re-plays the success/fail track, so the
|
|
84
|
+
* outcome appears to play several times per window. The gate accepts the first
|
|
85
|
+
* adjudication of a window and silently suppresses the duplicates that follow.
|
|
86
|
+
*
|
|
87
|
+
* The window is the active pet's own `gameplay.work.tickMs`, not a constant: a
|
|
88
|
+
* pet configured with a shorter cadence must actually adjudicate that often,
|
|
89
|
+
* while a fixed window would downgrade it without saying so (#1494).
|
|
90
|
+
* @module @linxin666/dsh-pet/client/work-tick-gate
|
|
91
|
+
*/
|
|
92
|
+
/** Window used when the active pet declares no work cadence. */
|
|
93
|
+
const DEFAULT_WORK_TICK_MS = 1e4;
|
|
94
|
+
/** Manifest bounds for `gameplay.work.tickMs` (src/gameplay.ts). */
|
|
95
|
+
const MIN_WORK_TICK_MS = 1e3;
|
|
96
|
+
const MAX_WORK_TICK_MS = 6e4;
|
|
97
|
+
/**
|
|
98
|
+
* The gate window for one pet: its configured cadence, clamped to the manifest's
|
|
99
|
+
* own bounds so a malformed registry entry cannot disable or flood the gate.
|
|
100
|
+
* @param tickMs - the active definition's `gameplay.work.tickMs`, when it has one.
|
|
101
|
+
* @returns the window in milliseconds.
|
|
102
|
+
*/
|
|
103
|
+
function workTickWindowMs(tickMs) {
|
|
104
|
+
if (typeof tickMs !== "number" || !Number.isFinite(tickMs)) return DEFAULT_WORK_TICK_MS;
|
|
105
|
+
return Math.min(MAX_WORK_TICK_MS, Math.max(MIN_WORK_TICK_MS, tickMs));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Create a gate.
|
|
109
|
+
* @param now - the clock, injectable so tests control the window.
|
|
110
|
+
* @returns the gate over that clock.
|
|
111
|
+
*/
|
|
112
|
+
function createWorkTickGate(now = Date.now) {
|
|
113
|
+
let lastAdjudicatedAt = 0;
|
|
114
|
+
return {
|
|
115
|
+
allow: (tickMs) => {
|
|
116
|
+
const at = now();
|
|
117
|
+
if (at - lastAdjudicatedAt < workTickWindowMs(tickMs)) return false;
|
|
118
|
+
lastAdjudicatedAt = at;
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
reset: () => {
|
|
122
|
+
lastAdjudicatedAt = 0;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
78
127
|
//#region ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
79
128
|
function r(e) {
|
|
80
129
|
var t, f, n = "";
|
|
@@ -322,12 +371,13 @@ window.__ModuleLoader__.load({
|
|
|
322
371
|
const delta = now - last;
|
|
323
372
|
last = now;
|
|
324
373
|
elapsed += delta;
|
|
325
|
-
|
|
374
|
+
let duration = decoration.durations[index] ?? 120;
|
|
326
375
|
if (elapsed >= duration) {
|
|
327
376
|
do {
|
|
328
377
|
elapsed -= duration;
|
|
329
378
|
if (index < segment.to) index += 1;
|
|
330
379
|
else if (decoration.loop) index = segment.from;
|
|
380
|
+
duration = decoration.durations[index] ?? 120;
|
|
331
381
|
} while (elapsed >= duration);
|
|
332
382
|
el.style.backgroundPosition = position(index);
|
|
333
383
|
}
|
|
@@ -1095,6 +1145,7 @@ window.__ModuleLoader__.load({
|
|
|
1095
1145
|
gameplayBus.setIdleTrack = (track) => {
|
|
1096
1146
|
handleRef.current?.setIdleTrack(track);
|
|
1097
1147
|
};
|
|
1148
|
+
if (gameplayBus.idleTrack !== void 0) handle.setIdleTrack(gameplayBus.idleTrack);
|
|
1098
1149
|
cleanups.push(() => {
|
|
1099
1150
|
gameplayBus.setTrack = void 0;
|
|
1100
1151
|
gameplayBus.setIdleTrack = void 0;
|
|
@@ -1208,6 +1259,7 @@ window.__ModuleLoader__.load({
|
|
|
1208
1259
|
const def = definition.gameplay;
|
|
1209
1260
|
const view = ui.snapshot?.gameplay;
|
|
1210
1261
|
const phase = ui.snapshot?.phase ?? "idle";
|
|
1262
|
+
const persistedSkin = ui.snapshot?.skin;
|
|
1211
1263
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1212
1264
|
const [page, setPage] = (0, react.useState)("root");
|
|
1213
1265
|
const [skinId, setSkinId] = (0, react.useState)(void 0);
|
|
@@ -1400,20 +1452,24 @@ window.__ModuleLoader__.load({
|
|
|
1400
1452
|
(0, react.useEffect)(() => {
|
|
1401
1453
|
const work = def?.work;
|
|
1402
1454
|
if (def === void 0 || work === void 0 || view?.mode !== "work") return void 0;
|
|
1403
|
-
|
|
1455
|
+
const skinGameplay = definition.frames2d?.skins?.find((skin) => skin.id === skinIdRef.current)?.gameplayTracks;
|
|
1456
|
+
/** The state's track, swapped for the skin's override when it declares one. */
|
|
1457
|
+
const trackOf = (state) => skinGameplay?.[state] ?? state;
|
|
1458
|
+
bus.setTrack?.(trackOf(work.state));
|
|
1404
1459
|
let resultTimer = 0;
|
|
1405
1460
|
const timer = window.setInterval(() => {
|
|
1406
1461
|
if (busyRef.current) return;
|
|
1407
1462
|
busyRef.current = true;
|
|
1408
1463
|
api.workTick().then((result) => {
|
|
1409
1464
|
busyRef.current = false;
|
|
1465
|
+
if (modeRef.current !== "work") return;
|
|
1410
1466
|
applyResult(result);
|
|
1411
1467
|
if (result.ok !== true || result.outcome === void 0) return;
|
|
1412
|
-
const resultTrack = result.outcome === "success" ? work.successState : work.failState;
|
|
1468
|
+
const resultTrack = trackOf(result.outcome === "success" ? work.successState : work.failState);
|
|
1413
1469
|
const hold = result.outcome === "success" ? work.resultMs?.success ?? 1300 : work.resultMs?.fail ?? 1900;
|
|
1414
1470
|
bus.setTrack?.(resultTrack);
|
|
1415
1471
|
resultTimer = window.setTimeout(() => {
|
|
1416
|
-
if (modeRef.current === "work") bus.setTrack?.(work.state);
|
|
1472
|
+
if (modeRef.current === "work") bus.setTrack?.(trackOf(work.state));
|
|
1417
1473
|
}, hold);
|
|
1418
1474
|
}, () => {
|
|
1419
1475
|
busyRef.current = false;
|
|
@@ -1427,7 +1483,8 @@ window.__ModuleLoader__.load({
|
|
|
1427
1483
|
}, [
|
|
1428
1484
|
definition.id,
|
|
1429
1485
|
def,
|
|
1430
|
-
view?.mode
|
|
1486
|
+
view?.mode,
|
|
1487
|
+
skinId
|
|
1431
1488
|
]);
|
|
1432
1489
|
(0, react.useEffect)(() => {
|
|
1433
1490
|
const sleep = def?.sleep;
|
|
@@ -1464,10 +1521,32 @@ window.__ModuleLoader__.load({
|
|
|
1464
1521
|
const setMode = (next) => {
|
|
1465
1522
|
api.setMode(next).then(applyResult, () => void 0);
|
|
1466
1523
|
};
|
|
1524
|
+
(0, react.useEffect)(() => {
|
|
1525
|
+
setSkinId(persistedSkin);
|
|
1526
|
+
}, [definition.id, persistedSkin]);
|
|
1527
|
+
(0, react.useEffect)(() => {
|
|
1528
|
+
const skin = definition.frames2d?.skins?.find((candidate) => candidate.id === skinId);
|
|
1529
|
+
bus.idleTrack = skin?.idleTrack;
|
|
1530
|
+
bus.setIdleTrack?.(skin?.idleTrack);
|
|
1531
|
+
}, [
|
|
1532
|
+
definition.id,
|
|
1533
|
+
skinId,
|
|
1534
|
+
persistedSkin
|
|
1535
|
+
]);
|
|
1467
1536
|
const skins = definition.frames2d?.skins;
|
|
1537
|
+
/** The base idle track one skin id resolves to (undefined = default look). */
|
|
1538
|
+
const skinTrackOf = (id) => id === void 0 ? void 0 : definition.frames2d?.skins?.find((candidate) => candidate.id === id)?.idleTrack;
|
|
1468
1539
|
const selectSkin = (skin) => {
|
|
1469
1540
|
setSkinId(skin?.id);
|
|
1470
1541
|
bus.setIdleTrack?.(skin?.idleTrack);
|
|
1542
|
+
const restore = () => {
|
|
1543
|
+
setSkinId(persistedSkin);
|
|
1544
|
+
bus.setIdleTrack?.(skinTrackOf(persistedSkin));
|
|
1545
|
+
};
|
|
1546
|
+
api.setSkin(skin?.id).then((result) => {
|
|
1547
|
+
if (result.ok) return;
|
|
1548
|
+
restore();
|
|
1549
|
+
}, restore);
|
|
1471
1550
|
};
|
|
1472
1551
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1473
1552
|
ref: hudRef,
|
|
@@ -2000,10 +2079,11 @@ window.__ModuleLoader__.load({
|
|
|
2000
2079
|
* Frame presentation has two modes picked once at mount by capability
|
|
2001
2080
|
* probing:
|
|
2002
2081
|
* - Canvas bitmap buffer (default where createImageBitmap/fetch/2D context
|
|
2003
|
-
* exist):
|
|
2004
|
-
* the
|
|
2005
|
-
* zero DOM mutations and zero
|
|
2006
|
-
* <img>.src per frame drove image
|
|
2082
|
+
* exist): frames are decoded once into an ImageBitmap on demand, with a
|
|
2083
|
+
* bounded look-ahead window over the playing track, and drawn onto one
|
|
2084
|
+
* <canvas> - steady-state playback issues zero DOM mutations and zero
|
|
2085
|
+
* re-decodes (measured hotspot: swapping <img>.src per frame drove image
|
|
2086
|
+
* decode + invalidation every tick).
|
|
2007
2087
|
* - Classic <img> fallback (jsdom/tests or missing APIs): identical to the
|
|
2008
2088
|
* historical behavior - cache-warm Image elements plus guarded src swaps,
|
|
2009
2089
|
* so environments without modern decoding keep working unchanged.
|
|
@@ -2187,6 +2267,25 @@ window.__ModuleLoader__.load({
|
|
|
2187
2267
|
pumpFrames();
|
|
2188
2268
|
return job;
|
|
2189
2269
|
};
|
|
2270
|
+
/**
|
|
2271
|
+
* Bounded look-ahead window: decode only the frames playback is about to
|
|
2272
|
+
* need. The historical warm pass decoded every frame of every track up
|
|
2273
|
+
* front - a shipped pet carries ~1.1k 512x683 frames, so that pass pulls
|
|
2274
|
+
* tens of megabytes and retains every decoded bitmap for the life of the
|
|
2275
|
+
* page. Prefetching the playing track's next frames keeps loops and phase
|
|
2276
|
+
* switches warm while unplayed tracks (and every unselected skin's
|
|
2277
|
+
* frames) stay on demand, where playback jumps the queue anyway.
|
|
2278
|
+
*/
|
|
2279
|
+
const PREFETCH_AHEAD = 12;
|
|
2280
|
+
const prefetchAhead = (trackId, index) => {
|
|
2281
|
+
const def = config.tracks[trackId];
|
|
2282
|
+
if (def === void 0) return;
|
|
2283
|
+
const end = Math.min(def.frames.length, index + 1 + PREFETCH_AHEAD);
|
|
2284
|
+
for (let ahead = index + 1; ahead < end; ahead += 1) {
|
|
2285
|
+
const url = def.frames[ahead];
|
|
2286
|
+
if (url !== void 0) loadFrame(url);
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2190
2289
|
let disposed = false;
|
|
2191
2290
|
let timer;
|
|
2192
2291
|
let watchdog;
|
|
@@ -2221,6 +2320,7 @@ window.__ModuleLoader__.load({
|
|
|
2221
2320
|
const show = (trackId, index) => {
|
|
2222
2321
|
const url = config.tracks[trackId]?.frames[index];
|
|
2223
2322
|
if (url === void 0) return;
|
|
2323
|
+
prefetchAhead(trackId, index);
|
|
2224
2324
|
if (img !== null) {
|
|
2225
2325
|
if (img.getAttribute("src") !== url) img.src = url;
|
|
2226
2326
|
return;
|
|
@@ -2277,11 +2377,6 @@ window.__ModuleLoader__.load({
|
|
|
2277
2377
|
const expected = (def.durations[frameIndex] ?? 200) + WATCHDOG_MS;
|
|
2278
2378
|
if (Date.now() - lastAdvance > expected) tick();
|
|
2279
2379
|
}, WATCHDOG_MS);
|
|
2280
|
-
const warmTrackIds = [.../* @__PURE__ */ new Set([...Object.values(config.phases), config.phases.idle])];
|
|
2281
|
-
for (const warmTrack of [...warmTrackIds, ...Object.keys(config.tracks)].map((id) => config.tracks[id])) {
|
|
2282
|
-
if (warmTrack === void 0) continue;
|
|
2283
|
-
for (const warmUrl of warmTrack.frames) loadFrame(warmUrl);
|
|
2284
|
-
}
|
|
2285
2380
|
play(track);
|
|
2286
2381
|
let disposedOnce = false;
|
|
2287
2382
|
const dispose = () => {
|
|
@@ -3656,7 +3751,7 @@ window.__ModuleLoader__.load({
|
|
|
3656
3751
|
/** The building package's version, when the bundle carries it. */
|
|
3657
3752
|
function bakedVersion() {
|
|
3658
3753
|
try {
|
|
3659
|
-
return "0.3.
|
|
3754
|
+
return "0.3.22";
|
|
3660
3755
|
} catch {
|
|
3661
3756
|
return;
|
|
3662
3757
|
}
|
|
@@ -3739,6 +3834,7 @@ window.__ModuleLoader__.load({
|
|
|
3739
3834
|
setConfig: (patch) => petFetch("/api/pet/set-config", patch),
|
|
3740
3835
|
setName: (name) => petFetch("/api/pet/set-name", { name }),
|
|
3741
3836
|
setPet: (petId) => petFetch("/api/pet/set-pet", { petId }),
|
|
3837
|
+
setSkin: (skin) => petFetch("/api/pet/set-skin", skin === void 0 ? {} : { skin }),
|
|
3742
3838
|
gameplayTouch: (zone) => petFetch("/api/pet/gameplay/touch", zone === void 0 ? {} : { zone }),
|
|
3743
3839
|
gameplaySetMode: (mode) => petFetch("/api/pet/gameplay/mode", { mode }),
|
|
3744
3840
|
gameplayWorkTick: () => petFetch("/api/pet/gameplay/work-tick", {}),
|
|
@@ -3763,16 +3859,17 @@ window.__ModuleLoader__.load({
|
|
|
3763
3859
|
* first-level settings section.
|
|
3764
3860
|
* @param ctx - client root context.
|
|
3765
3861
|
*/
|
|
3862
|
+
/** The page-wide work-tick gate; its window follows the active pet's cadence. */
|
|
3863
|
+
const workTickGate = createWorkTickGate();
|
|
3766
3864
|
/**
|
|
3767
|
-
*
|
|
3768
|
-
*
|
|
3769
|
-
*
|
|
3770
|
-
* call re-rolls, re-grants treats and re-plays the success/fail track, so
|
|
3771
|
-
* the outcome appears to play several times per window. This shared marker
|
|
3772
|
-
* accepts the first adjudication of a window and silently suppresses the
|
|
3773
|
-
* duplicates that follow. Reset when (re-)entering work mode.
|
|
3865
|
+
* The work cadence the active pet declares, when its registry entry is known.
|
|
3866
|
+
* @param store - the pet store holding the host snapshot and the registry list.
|
|
3867
|
+
* @returns the configured `gameplay.work.tickMs`, or undefined when unknown.
|
|
3774
3868
|
*/
|
|
3775
|
-
|
|
3869
|
+
function activeWorkTickMs(store) {
|
|
3870
|
+
const state = store.getSnapshot();
|
|
3871
|
+
return state.pets.find((entry) => entry.id === state.snapshot?.pet.id)?.gameplay?.work?.tickMs;
|
|
3872
|
+
}
|
|
3776
3873
|
function apply(ctx) {
|
|
3777
3874
|
reportDailyHeartbeat([{ name: "@linxin666/dsh-pet" }]);
|
|
3778
3875
|
ctx.effect(() => {
|
|
@@ -3934,14 +4031,19 @@ window.__ModuleLoader__.load({
|
|
|
3934
4031
|
},
|
|
3935
4032
|
gameplay: {
|
|
3936
4033
|
touch: (zone) => petApi.gameplayTouch(zone),
|
|
4034
|
+
setSkin: (skin) => petApi.setSkin(skin).then((result) => {
|
|
4035
|
+
if (result.ok) pollNow();
|
|
4036
|
+
return result;
|
|
4037
|
+
}, () => ({
|
|
4038
|
+
ok: false,
|
|
4039
|
+
error: "transport"
|
|
4040
|
+
})),
|
|
3937
4041
|
setMode: async (mode) => {
|
|
3938
|
-
if (mode === "work")
|
|
4042
|
+
if (mode === "work") workTickGate.reset();
|
|
3939
4043
|
return petApi.gameplaySetMode(mode);
|
|
3940
4044
|
},
|
|
3941
4045
|
workTick: async () => {
|
|
3942
|
-
|
|
3943
|
-
if (now - lastWorkTickAt < 8500) return { ok: true };
|
|
3944
|
-
lastWorkTickAt = now;
|
|
4046
|
+
if (!workTickGate.allow(activeWorkTickMs(petStore))) return { ok: true };
|
|
3945
4047
|
return petApi.gameplayWorkTick();
|
|
3946
4048
|
},
|
|
3947
4049
|
buy: (item) => petApi.gameplayBuy(item)
|