@linxin666/dsh-pet 0.3.19 → 0.3.21
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 +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +191 -53
- package/lib/client.js.map +1 -1
- package/lib/index.js +174 -61
- 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 +96 -39
- 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/event-projection.d.ts +14 -0
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +32 -18
- 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 +43 -1
- package/package.json +14 -14
- package/src/client/PetDockEntry.test.tsx +1 -0
- 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 +129 -7
- package/src/client/renderers/frames2d.ts +94 -36
- package/src/client/work-tick-gate.test.ts +53 -0
- package/src/client/work-tick-gate.ts +63 -0
- package/src/event-projection.ts +37 -18
- 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 +49 -0
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# node scripts/verify-docs.mjs --write <dir>
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 8b92fda99dc04c8f95ff2e42bf39b42a6a4c1f6f
|
|
6
|
+
README.zh.md: d684b6381a9515c6bc7cef55f418ab4e3ab7ee0c
|
package/README.md
CHANGED
|
@@ -289,7 +289,7 @@ global React root (createRoot → document.body) <-- polling 2s -- pet-client (b
|
|
|
289
289
|
PetSprite floating layer (portal + rAF)
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
-
- **Status source**: the host projects official `turn/start`, `step/start`, `assistant/
|
|
292
|
+
- **Status source**: the host projects official `turn/start`, `step/start`, `assistant/message`, `tool/call`, `tool/result`, and `turn/end` events plus live `agent/assistant-stream` chunks into waiting/thinking/tool/review/done/failed states. Optional legacy `activity/status` events remain a compatibility input.
|
|
293
293
|
- **Registry**: the host normalizes every manifest into a full render definition (geometry, per-row frame counts, per-track durations) and serves it over `/api/pet/pets`; the browser half renders any entry from that definition and carries no per-pet code.
|
|
294
294
|
- **Selection & naming**: `petId` lives in the settings namespace; per-pet names live in `pet.json` under `names`, edited through the hover-panel rename of the active pet. Legacy installs migrate their flat `name` onto the whale girl.
|
|
295
295
|
- **Multi-session semantics**: the API and browser mount are host-global and expose no foreground-session identity. Concurrent sessions each keep their own projected state: the most recent meaningful event drives the sprite animation, while every active TOP-LEVEL session reports its stage in its own bubble (the state view's sessions list, capped at 12 most-recent). Subagent children are tracked for animation, rewards, and the single display bubble but render no bubble of their own, so N conversations never multiply into an N-plus-subagents stack. Every session's completed turns are still rewarded independently; disposing a session removes its bubble, and disposing the display session falls back to the most recent remaining one.
|
package/README.zh.md
CHANGED
|
@@ -289,7 +289,7 @@ dsh-pet/
|
|
|
289
289
|
PetSprite 浮层(portal + rAF)
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
-
- **状态来源**:宿主把官方 `turn/start`、`step/start`、`assistant/
|
|
292
|
+
- **状态来源**:宿主把官方 `turn/start`、`step/start`、`assistant/message`、`tool/call`、`tool/result`、`turn/end` 事件与实时 `agent/assistant-stream` 增量投影为 waiting/thinking/tool/review/done/failed 状态。可选兼容 `activity/status` 事件仍作为输入。
|
|
293
293
|
- **注册表**:宿主把每份 manifest 归一化为完整渲染定义(几何、每行帧数、每轨时长),经 `/api/pet/pets` 下发;浏览器半区用该定义渲染任意条目,不携带任何宠物专属代码。
|
|
294
294
|
- **选择与命名**:`petId` 存于设置命名空间;每只宠物的名字存于 `pet.json` 的 `names`,通过悬浮面板对当前宠物改名编辑。旧版安装的平铺 `name` 自动迁移到鲸鱼娘名下。
|
|
295
295
|
- **多会话语义**:API 与浏览器挂载都是宿主全局的,不暴露前台会话身份。并行会话各自保留投影状态:最近一次有意义事件驱动精灵动画,同时每个活动的顶层会话在独立气泡里报告自己的阶段(state 视图的 sessions 列表,最多保留最近 12 个)。子代理会话仍参与动画、计奖与单一显示气泡,但不占独立气泡位——N 个对话不会变成"N + 子代理数"的气泡堆。每个会话完成的轮次仍独立计奖;销毁会话移除它的气泡,销毁当前显示会话则回退到最近仍在活动的会话。
|
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 = "";
|
|
@@ -1095,6 +1144,7 @@ window.__ModuleLoader__.load({
|
|
|
1095
1144
|
gameplayBus.setIdleTrack = (track) => {
|
|
1096
1145
|
handleRef.current?.setIdleTrack(track);
|
|
1097
1146
|
};
|
|
1147
|
+
if (gameplayBus.idleTrack !== void 0) handle.setIdleTrack(gameplayBus.idleTrack);
|
|
1098
1148
|
cleanups.push(() => {
|
|
1099
1149
|
gameplayBus.setTrack = void 0;
|
|
1100
1150
|
gameplayBus.setIdleTrack = void 0;
|
|
@@ -1208,6 +1258,7 @@ window.__ModuleLoader__.load({
|
|
|
1208
1258
|
const def = definition.gameplay;
|
|
1209
1259
|
const view = ui.snapshot?.gameplay;
|
|
1210
1260
|
const phase = ui.snapshot?.phase ?? "idle";
|
|
1261
|
+
const persistedSkin = ui.snapshot?.skin;
|
|
1211
1262
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1212
1263
|
const [page, setPage] = (0, react.useState)("root");
|
|
1213
1264
|
const [skinId, setSkinId] = (0, react.useState)(void 0);
|
|
@@ -1400,20 +1451,24 @@ window.__ModuleLoader__.load({
|
|
|
1400
1451
|
(0, react.useEffect)(() => {
|
|
1401
1452
|
const work = def?.work;
|
|
1402
1453
|
if (def === void 0 || work === void 0 || view?.mode !== "work") return void 0;
|
|
1403
|
-
|
|
1454
|
+
const skinGameplay = definition.frames2d?.skins?.find((skin) => skin.id === skinIdRef.current)?.gameplayTracks;
|
|
1455
|
+
/** The state's track, swapped for the skin's override when it declares one. */
|
|
1456
|
+
const trackOf = (state) => skinGameplay?.[state] ?? state;
|
|
1457
|
+
bus.setTrack?.(trackOf(work.state));
|
|
1404
1458
|
let resultTimer = 0;
|
|
1405
1459
|
const timer = window.setInterval(() => {
|
|
1406
1460
|
if (busyRef.current) return;
|
|
1407
1461
|
busyRef.current = true;
|
|
1408
1462
|
api.workTick().then((result) => {
|
|
1409
1463
|
busyRef.current = false;
|
|
1464
|
+
if (modeRef.current !== "work") return;
|
|
1410
1465
|
applyResult(result);
|
|
1411
1466
|
if (result.ok !== true || result.outcome === void 0) return;
|
|
1412
|
-
const resultTrack = result.outcome === "success" ? work.successState : work.failState;
|
|
1467
|
+
const resultTrack = trackOf(result.outcome === "success" ? work.successState : work.failState);
|
|
1413
1468
|
const hold = result.outcome === "success" ? work.resultMs?.success ?? 1300 : work.resultMs?.fail ?? 1900;
|
|
1414
1469
|
bus.setTrack?.(resultTrack);
|
|
1415
1470
|
resultTimer = window.setTimeout(() => {
|
|
1416
|
-
if (modeRef.current === "work") bus.setTrack?.(work.state);
|
|
1471
|
+
if (modeRef.current === "work") bus.setTrack?.(trackOf(work.state));
|
|
1417
1472
|
}, hold);
|
|
1418
1473
|
}, () => {
|
|
1419
1474
|
busyRef.current = false;
|
|
@@ -1427,7 +1482,8 @@ window.__ModuleLoader__.load({
|
|
|
1427
1482
|
}, [
|
|
1428
1483
|
definition.id,
|
|
1429
1484
|
def,
|
|
1430
|
-
view?.mode
|
|
1485
|
+
view?.mode,
|
|
1486
|
+
skinId
|
|
1431
1487
|
]);
|
|
1432
1488
|
(0, react.useEffect)(() => {
|
|
1433
1489
|
const sleep = def?.sleep;
|
|
@@ -1464,10 +1520,32 @@ window.__ModuleLoader__.load({
|
|
|
1464
1520
|
const setMode = (next) => {
|
|
1465
1521
|
api.setMode(next).then(applyResult, () => void 0);
|
|
1466
1522
|
};
|
|
1523
|
+
(0, react.useEffect)(() => {
|
|
1524
|
+
setSkinId(persistedSkin);
|
|
1525
|
+
}, [definition.id, persistedSkin]);
|
|
1526
|
+
(0, react.useEffect)(() => {
|
|
1527
|
+
const skin = definition.frames2d?.skins?.find((candidate) => candidate.id === skinId);
|
|
1528
|
+
bus.idleTrack = skin?.idleTrack;
|
|
1529
|
+
bus.setIdleTrack?.(skin?.idleTrack);
|
|
1530
|
+
}, [
|
|
1531
|
+
definition.id,
|
|
1532
|
+
skinId,
|
|
1533
|
+
persistedSkin
|
|
1534
|
+
]);
|
|
1467
1535
|
const skins = definition.frames2d?.skins;
|
|
1536
|
+
/** The base idle track one skin id resolves to (undefined = default look). */
|
|
1537
|
+
const skinTrackOf = (id) => id === void 0 ? void 0 : definition.frames2d?.skins?.find((candidate) => candidate.id === id)?.idleTrack;
|
|
1468
1538
|
const selectSkin = (skin) => {
|
|
1469
1539
|
setSkinId(skin?.id);
|
|
1470
1540
|
bus.setIdleTrack?.(skin?.idleTrack);
|
|
1541
|
+
const restore = () => {
|
|
1542
|
+
setSkinId(persistedSkin);
|
|
1543
|
+
bus.setIdleTrack?.(skinTrackOf(persistedSkin));
|
|
1544
|
+
};
|
|
1545
|
+
api.setSkin(skin?.id).then((result) => {
|
|
1546
|
+
if (result.ok) return;
|
|
1547
|
+
restore();
|
|
1548
|
+
}, restore);
|
|
1471
1549
|
};
|
|
1472
1550
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1473
1551
|
ref: hudRef,
|
|
@@ -2000,10 +2078,11 @@ window.__ModuleLoader__.load({
|
|
|
2000
2078
|
* Frame presentation has two modes picked once at mount by capability
|
|
2001
2079
|
* probing:
|
|
2002
2080
|
* - 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
|
|
2081
|
+
* exist): frames are decoded once into an ImageBitmap on demand, with a
|
|
2082
|
+
* bounded look-ahead window over the playing track, and drawn onto one
|
|
2083
|
+
* <canvas> - steady-state playback issues zero DOM mutations and zero
|
|
2084
|
+
* re-decodes (measured hotspot: swapping <img>.src per frame drove image
|
|
2085
|
+
* decode + invalidation every tick).
|
|
2007
2086
|
* - Classic <img> fallback (jsdom/tests or missing APIs): identical to the
|
|
2008
2087
|
* historical behavior - cache-warm Image elements plus guarded src swaps,
|
|
2009
2088
|
* so environments without modern decoding keep working unchanged.
|
|
@@ -2119,42 +2198,93 @@ window.__ModuleLoader__.load({
|
|
|
2119
2198
|
}
|
|
2120
2199
|
const decoding = /* @__PURE__ */ new Map();
|
|
2121
2200
|
const decodedAll = [];
|
|
2122
|
-
const
|
|
2201
|
+
const FRAME_POOL_LIMIT = 8;
|
|
2202
|
+
const frameQueue = [];
|
|
2203
|
+
let activeFrames = 0;
|
|
2204
|
+
const decodeFrame = async (url) => {
|
|
2205
|
+
try {
|
|
2206
|
+
const response = await fetch(url);
|
|
2207
|
+
if (!response.ok) throw new Error("http " + response.status);
|
|
2208
|
+
const bitmap = await createImageBitmap(await response.blob());
|
|
2209
|
+
return {
|
|
2210
|
+
source: bitmap,
|
|
2211
|
+
width: bitmap.width,
|
|
2212
|
+
height: bitmap.height
|
|
2213
|
+
};
|
|
2214
|
+
} catch {
|
|
2215
|
+
return await new Promise((resolve) => {
|
|
2216
|
+
try {
|
|
2217
|
+
const pre = new Image();
|
|
2218
|
+
pre.onload = () => {
|
|
2219
|
+
resolve(pre.naturalWidth > 0 ? {
|
|
2220
|
+
source: pre,
|
|
2221
|
+
width: pre.naturalWidth,
|
|
2222
|
+
height: pre.naturalHeight
|
|
2223
|
+
} : void 0);
|
|
2224
|
+
};
|
|
2225
|
+
pre.onerror = () => resolve(void 0);
|
|
2226
|
+
pre.src = url;
|
|
2227
|
+
} catch {
|
|
2228
|
+
resolve(void 0);
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
const pumpFrames = () => {
|
|
2234
|
+
while (activeFrames < FRAME_POOL_LIMIT && frameQueue.length > 0) {
|
|
2235
|
+
const queued = frameQueue.shift();
|
|
2236
|
+
activeFrames += 1;
|
|
2237
|
+
queued.release();
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
const loadFrame = (url, jump = false) => {
|
|
2241
|
+
if (jump) {
|
|
2242
|
+
const index = frameQueue.findIndex((queued) => queued.url === url);
|
|
2243
|
+
if (index > 0) frameQueue.unshift(frameQueue.splice(index, 1)[0]);
|
|
2244
|
+
}
|
|
2123
2245
|
const cached = decoding.get(url);
|
|
2124
2246
|
if (cached !== void 0) return cached;
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
return await new Promise((resolve) => {
|
|
2137
|
-
try {
|
|
2138
|
-
const pre = new Image();
|
|
2139
|
-
pre.onload = () => {
|
|
2140
|
-
resolve(pre.naturalWidth > 0 ? {
|
|
2141
|
-
source: pre,
|
|
2142
|
-
width: pre.naturalWidth,
|
|
2143
|
-
height: pre.naturalHeight
|
|
2144
|
-
} : void 0);
|
|
2145
|
-
};
|
|
2146
|
-
pre.onerror = () => resolve(void 0);
|
|
2147
|
-
pre.src = url;
|
|
2148
|
-
} catch {
|
|
2149
|
-
resolve(void 0);
|
|
2150
|
-
}
|
|
2151
|
-
});
|
|
2152
|
-
}
|
|
2153
|
-
})();
|
|
2247
|
+
let release;
|
|
2248
|
+
const job = new Promise((resolve) => {
|
|
2249
|
+
release = resolve;
|
|
2250
|
+
}).then(() => disposed ? void 0 : decodeFrame(url));
|
|
2251
|
+
job.then((frame) => {
|
|
2252
|
+
if (frame === void 0) decoding.delete(url);
|
|
2253
|
+
}, () => decoding.delete(url));
|
|
2254
|
+
job.finally(() => {
|
|
2255
|
+
activeFrames -= 1;
|
|
2256
|
+
pumpFrames();
|
|
2257
|
+
});
|
|
2154
2258
|
decoding.set(url, job);
|
|
2155
2259
|
decodedAll.push(job.then(() => void 0, () => void 0));
|
|
2260
|
+
const entry = {
|
|
2261
|
+
url,
|
|
2262
|
+
release
|
|
2263
|
+
};
|
|
2264
|
+
if (jump) frameQueue.unshift(entry);
|
|
2265
|
+
else frameQueue.push(entry);
|
|
2266
|
+
pumpFrames();
|
|
2156
2267
|
return job;
|
|
2157
2268
|
};
|
|
2269
|
+
/**
|
|
2270
|
+
* Bounded look-ahead window: decode only the frames playback is about to
|
|
2271
|
+
* need. The historical warm pass decoded every frame of every track up
|
|
2272
|
+
* front - a shipped pet carries ~1.1k 512x683 frames, so that pass pulls
|
|
2273
|
+
* tens of megabytes and retains every decoded bitmap for the life of the
|
|
2274
|
+
* page. Prefetching the playing track's next frames keeps loops and phase
|
|
2275
|
+
* switches warm while unplayed tracks (and every unselected skin's
|
|
2276
|
+
* frames) stay on demand, where playback jumps the queue anyway.
|
|
2277
|
+
*/
|
|
2278
|
+
const PREFETCH_AHEAD = 12;
|
|
2279
|
+
const prefetchAhead = (trackId, index) => {
|
|
2280
|
+
const def = config.tracks[trackId];
|
|
2281
|
+
if (def === void 0) return;
|
|
2282
|
+
const end = Math.min(def.frames.length, index + 1 + PREFETCH_AHEAD);
|
|
2283
|
+
for (let ahead = index + 1; ahead < end; ahead += 1) {
|
|
2284
|
+
const url = def.frames[ahead];
|
|
2285
|
+
if (url !== void 0) loadFrame(url);
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2158
2288
|
let disposed = false;
|
|
2159
2289
|
let timer;
|
|
2160
2290
|
let watchdog;
|
|
@@ -2175,7 +2305,7 @@ window.__ModuleLoader__.load({
|
|
|
2175
2305
|
const paintCanvas = (url) => {
|
|
2176
2306
|
if (context2d === null || canvas === null) return;
|
|
2177
2307
|
const myToken = ++drawToken;
|
|
2178
|
-
loadFrame(url).then((frame) => {
|
|
2308
|
+
loadFrame(url, true).then((frame) => {
|
|
2179
2309
|
if (disposed || frame === void 0 || myToken !== drawToken) return;
|
|
2180
2310
|
if (lastDrawnUrl === url) return;
|
|
2181
2311
|
lastDrawnUrl = url;
|
|
@@ -2189,6 +2319,7 @@ window.__ModuleLoader__.load({
|
|
|
2189
2319
|
const show = (trackId, index) => {
|
|
2190
2320
|
const url = config.tracks[trackId]?.frames[index];
|
|
2191
2321
|
if (url === void 0) return;
|
|
2322
|
+
prefetchAhead(trackId, index);
|
|
2192
2323
|
if (img !== null) {
|
|
2193
2324
|
if (img.getAttribute("src") !== url) img.src = url;
|
|
2194
2325
|
return;
|
|
@@ -2245,7 +2376,6 @@ window.__ModuleLoader__.load({
|
|
|
2245
2376
|
const expected = (def.durations[frameIndex] ?? 200) + WATCHDOG_MS;
|
|
2246
2377
|
if (Date.now() - lastAdvance > expected) tick();
|
|
2247
2378
|
}, WATCHDOG_MS);
|
|
2248
|
-
for (const warmTrack of Object.values(config.tracks)) for (const warmUrl of warmTrack.frames) loadFrame(warmUrl);
|
|
2249
2379
|
play(track);
|
|
2250
2380
|
let disposedOnce = false;
|
|
2251
2381
|
const dispose = () => {
|
|
@@ -2255,6 +2385,7 @@ window.__ModuleLoader__.load({
|
|
|
2255
2385
|
unsubscribe();
|
|
2256
2386
|
if (timer !== void 0) clearTimeout(timer);
|
|
2257
2387
|
if (watchdog !== void 0) clearInterval(watchdog);
|
|
2388
|
+
for (const queued of frameQueue.splice(0)) queued.release();
|
|
2258
2389
|
Promise.allSettled(decodedAll).then(() => {
|
|
2259
2390
|
for (const job of decoding.values()) job.then((frame) => {
|
|
2260
2391
|
try {
|
|
@@ -3619,7 +3750,7 @@ window.__ModuleLoader__.load({
|
|
|
3619
3750
|
/** The building package's version, when the bundle carries it. */
|
|
3620
3751
|
function bakedVersion() {
|
|
3621
3752
|
try {
|
|
3622
|
-
return "0.3.
|
|
3753
|
+
return "0.3.21";
|
|
3623
3754
|
} catch {
|
|
3624
3755
|
return;
|
|
3625
3756
|
}
|
|
@@ -3702,6 +3833,7 @@ window.__ModuleLoader__.load({
|
|
|
3702
3833
|
setConfig: (patch) => petFetch("/api/pet/set-config", patch),
|
|
3703
3834
|
setName: (name) => petFetch("/api/pet/set-name", { name }),
|
|
3704
3835
|
setPet: (petId) => petFetch("/api/pet/set-pet", { petId }),
|
|
3836
|
+
setSkin: (skin) => petFetch("/api/pet/set-skin", skin === void 0 ? {} : { skin }),
|
|
3705
3837
|
gameplayTouch: (zone) => petFetch("/api/pet/gameplay/touch", zone === void 0 ? {} : { zone }),
|
|
3706
3838
|
gameplaySetMode: (mode) => petFetch("/api/pet/gameplay/mode", { mode }),
|
|
3707
3839
|
gameplayWorkTick: () => petFetch("/api/pet/gameplay/work-tick", {}),
|
|
@@ -3726,16 +3858,17 @@ window.__ModuleLoader__.load({
|
|
|
3726
3858
|
* first-level settings section.
|
|
3727
3859
|
* @param ctx - client root context.
|
|
3728
3860
|
*/
|
|
3861
|
+
/** The page-wide work-tick gate; its window follows the active pet's cadence. */
|
|
3862
|
+
const workTickGate = createWorkTickGate();
|
|
3729
3863
|
/**
|
|
3730
|
-
*
|
|
3731
|
-
*
|
|
3732
|
-
*
|
|
3733
|
-
* call re-rolls, re-grants treats and re-plays the success/fail track, so
|
|
3734
|
-
* the outcome appears to play several times per window. This shared marker
|
|
3735
|
-
* accepts the first adjudication of a window and silently suppresses the
|
|
3736
|
-
* duplicates that follow. Reset when (re-)entering work mode.
|
|
3864
|
+
* The work cadence the active pet declares, when its registry entry is known.
|
|
3865
|
+
* @param store - the pet store holding the host snapshot and the registry list.
|
|
3866
|
+
* @returns the configured `gameplay.work.tickMs`, or undefined when unknown.
|
|
3737
3867
|
*/
|
|
3738
|
-
|
|
3868
|
+
function activeWorkTickMs(store) {
|
|
3869
|
+
const state = store.getSnapshot();
|
|
3870
|
+
return state.pets.find((entry) => entry.id === state.snapshot?.pet.id)?.gameplay?.work?.tickMs;
|
|
3871
|
+
}
|
|
3739
3872
|
function apply(ctx) {
|
|
3740
3873
|
reportDailyHeartbeat([{ name: "@linxin666/dsh-pet" }]);
|
|
3741
3874
|
ctx.effect(() => {
|
|
@@ -3897,14 +4030,19 @@ window.__ModuleLoader__.load({
|
|
|
3897
4030
|
},
|
|
3898
4031
|
gameplay: {
|
|
3899
4032
|
touch: (zone) => petApi.gameplayTouch(zone),
|
|
4033
|
+
setSkin: (skin) => petApi.setSkin(skin).then((result) => {
|
|
4034
|
+
if (result.ok) pollNow();
|
|
4035
|
+
return result;
|
|
4036
|
+
}, () => ({
|
|
4037
|
+
ok: false,
|
|
4038
|
+
error: "transport"
|
|
4039
|
+
})),
|
|
3900
4040
|
setMode: async (mode) => {
|
|
3901
|
-
if (mode === "work")
|
|
4041
|
+
if (mode === "work") workTickGate.reset();
|
|
3902
4042
|
return petApi.gameplaySetMode(mode);
|
|
3903
4043
|
},
|
|
3904
4044
|
workTick: async () => {
|
|
3905
|
-
|
|
3906
|
-
if (now - lastWorkTickAt < 8500) return { ok: true };
|
|
3907
|
-
lastWorkTickAt = now;
|
|
4045
|
+
if (!workTickGate.allow(activeWorkTickMs(petStore))) return { ok: true };
|
|
3908
4046
|
return petApi.gameplayWorkTick();
|
|
3909
4047
|
},
|
|
3910
4048
|
buy: (item) => petApi.gameplayBuy(item)
|