@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/lib/index.js
CHANGED
|
@@ -876,30 +876,6 @@ function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
|
|
|
876
876
|
phase: "waiting",
|
|
877
877
|
line: runtime.voice.scene("waiting", nowMs)
|
|
878
878
|
} };
|
|
879
|
-
case "assistant/chunk": {
|
|
880
|
-
const { chunk } = event.data;
|
|
881
|
-
if (chunk.type === "reasoning-delta" && chunk.text.length > 0) {
|
|
882
|
-
const whisper = runtime.whispers.feed("thinking", nowMs);
|
|
883
|
-
return {
|
|
884
|
-
input: {
|
|
885
|
-
phase: "thinking",
|
|
886
|
-
line: runtime.voice.scene("thinking", nowMs)
|
|
887
|
-
},
|
|
888
|
-
...whisper === void 0 ? {} : { whisper }
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
if (chunk.type === "text-delta" && chunk.text.length > 0) {
|
|
892
|
-
const whisper = runtime.whispers.feed("writing", nowMs);
|
|
893
|
-
return {
|
|
894
|
-
input: {
|
|
895
|
-
phase: "review",
|
|
896
|
-
line: runtime.voice.scene("review", nowMs)
|
|
897
|
-
},
|
|
898
|
-
...whisper === void 0 ? {} : { whisper }
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
879
|
case "assistant/message": return { input: {
|
|
904
880
|
phase: "review",
|
|
905
881
|
line: runtime.voice.scene("review", nowMs)
|
|
@@ -990,6 +966,36 @@ function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
|
|
|
990
966
|
default: return;
|
|
991
967
|
}
|
|
992
968
|
}
|
|
969
|
+
/**
|
|
970
|
+
* Project one live `agent/assistant-stream` publication into the pet's visual
|
|
971
|
+
* phases. Chunk frames are the alpha.2 replacement for the retired durable
|
|
972
|
+
* `assistant/chunk` event: a reasoning delta keeps the pet thinking, a text
|
|
973
|
+
* delta moves it to review; start, end, and non-delta chunks change nothing.
|
|
974
|
+
*/
|
|
975
|
+
function projectAssistantStreamFrame(frame, runtime, nowMs = Date.now()) {
|
|
976
|
+
if (frame.type !== "chunk") return void 0;
|
|
977
|
+
const { chunk } = frame;
|
|
978
|
+
if (chunk.type === "reasoning-delta" && chunk.text.length > 0) {
|
|
979
|
+
const whisper = runtime.whispers.feed("thinking", nowMs);
|
|
980
|
+
return {
|
|
981
|
+
input: {
|
|
982
|
+
phase: "thinking",
|
|
983
|
+
line: runtime.voice.scene("thinking", nowMs)
|
|
984
|
+
},
|
|
985
|
+
...whisper === void 0 ? {} : { whisper }
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
if (chunk.type === "text-delta" && chunk.text.length > 0) {
|
|
989
|
+
const whisper = runtime.whispers.feed("writing", nowMs);
|
|
990
|
+
return {
|
|
991
|
+
input: {
|
|
992
|
+
phase: "review",
|
|
993
|
+
line: runtime.voice.scene("review", nowMs)
|
|
994
|
+
},
|
|
995
|
+
...whisper === void 0 ? {} : { whisper }
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
993
999
|
//#endregion
|
|
994
1000
|
//#region src/treats.ts
|
|
995
1001
|
const defaultTreatConfig = {
|
|
@@ -1165,6 +1171,37 @@ var PetLedger = class {
|
|
|
1165
1171
|
this.dirty = true;
|
|
1166
1172
|
}
|
|
1167
1173
|
/**
|
|
1174
|
+
* Select one pet's frames2d skin; `undefined` clears the choice back to the
|
|
1175
|
+
* pet's default look. Manifest validation stays a caller concern, exactly
|
|
1176
|
+
* like setPetName's length check.
|
|
1177
|
+
*/
|
|
1178
|
+
setPetSkin(petId, skinId) {
|
|
1179
|
+
const skins = this.current.skins;
|
|
1180
|
+
if (skinId === void 0) {
|
|
1181
|
+
if (skins[petId] === void 0) return;
|
|
1182
|
+
const next = { ...skins };
|
|
1183
|
+
delete next[petId];
|
|
1184
|
+
this.current = {
|
|
1185
|
+
...this.current,
|
|
1186
|
+
skins: next
|
|
1187
|
+
};
|
|
1188
|
+
} else {
|
|
1189
|
+
if (skins[petId] === skinId) return;
|
|
1190
|
+
this.current = {
|
|
1191
|
+
...this.current,
|
|
1192
|
+
skins: {
|
|
1193
|
+
...skins,
|
|
1194
|
+
[petId]: skinId
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
this.dirty = true;
|
|
1199
|
+
}
|
|
1200
|
+
/** The persisted skin id for one pet (undefined = the pet's default look). */
|
|
1201
|
+
petSkin(petId) {
|
|
1202
|
+
return this.current.skins[petId];
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1168
1205
|
* Swap the reaction pools to another pet's custom remarks (called on pet
|
|
1169
1206
|
* selection). Slots the pet does not declare fall back to voice packs or built-ins.
|
|
1170
1207
|
*/
|
|
@@ -1364,6 +1401,7 @@ function emptyPersist() {
|
|
|
1364
1401
|
return {
|
|
1365
1402
|
petId: DEFAULT_PET_ID,
|
|
1366
1403
|
names: {},
|
|
1404
|
+
skins: {},
|
|
1367
1405
|
affinity: emptyAffinity(),
|
|
1368
1406
|
treats: emptyTreatLedger(),
|
|
1369
1407
|
display: { ...defaultDisplayConfig },
|
|
@@ -1394,6 +1432,18 @@ function loadPetNames(parsed) {
|
|
|
1394
1432
|
}
|
|
1395
1433
|
return names;
|
|
1396
1434
|
}
|
|
1435
|
+
/** Sanitize the per-pet skin selection map (string keys, non-empty trimmed values). */
|
|
1436
|
+
function loadPetSkins(parsed) {
|
|
1437
|
+
const skins = {};
|
|
1438
|
+
if (typeof parsed.skins !== "object" || parsed.skins === null) return skins;
|
|
1439
|
+
for (const [id, value] of Object.entries(parsed.skins)) {
|
|
1440
|
+
if (id === "" || typeof value !== "string") continue;
|
|
1441
|
+
const skin = value.trim();
|
|
1442
|
+
if (skin === "") continue;
|
|
1443
|
+
skins[id] = skin;
|
|
1444
|
+
}
|
|
1445
|
+
return skins;
|
|
1446
|
+
}
|
|
1397
1447
|
/** Clamp one count/score into [0, max]. */
|
|
1398
1448
|
function clamp(value, max) {
|
|
1399
1449
|
return Math.min(max, Math.max(0, value));
|
|
@@ -1418,12 +1468,15 @@ function loadGameplay(parsed) {
|
|
|
1418
1468
|
if (key === "" || typeof value !== "number" || !Number.isFinite(value)) continue;
|
|
1419
1469
|
currencies[key] = Math.min(GAMEPLAY_LOAD_CURRENCY_CAP, Math.max(0, Math.floor(value)));
|
|
1420
1470
|
}
|
|
1421
|
-
|
|
1471
|
+
const item = {
|
|
1422
1472
|
stats,
|
|
1423
1473
|
currencies,
|
|
1424
1474
|
mode: record.mode === "work" || record.mode === "sleep" ? record.mode : null,
|
|
1425
1475
|
settledAt: clamp(finiteNum(record.settledAt, 0), Number.MAX_SAFE_INTEGER)
|
|
1426
1476
|
};
|
|
1477
|
+
if (typeof record.incomeCarryMs === "number" && Number.isFinite(record.incomeCarryMs)) item.incomeCarryMs = Math.max(0, record.incomeCarryMs);
|
|
1478
|
+
if (typeof record.restoreCarryMs === "number" && Number.isFinite(record.restoreCarryMs)) item.restoreCarryMs = Math.max(0, record.restoreCarryMs);
|
|
1479
|
+
result[petId] = item;
|
|
1427
1480
|
}
|
|
1428
1481
|
return result;
|
|
1429
1482
|
}
|
|
@@ -1463,6 +1516,7 @@ function loadPetPersist(dir = petHomeDir()) {
|
|
|
1463
1516
|
return {
|
|
1464
1517
|
petId,
|
|
1465
1518
|
names,
|
|
1519
|
+
skins: loadPetSkins(parsed),
|
|
1466
1520
|
affinity,
|
|
1467
1521
|
treats,
|
|
1468
1522
|
display,
|
|
@@ -2329,7 +2383,10 @@ function settleGameplay(state, manifest, now, options) {
|
|
|
2329
2383
|
}
|
|
2330
2384
|
}
|
|
2331
2385
|
if (manifest.passiveIncome !== void 0) {
|
|
2332
|
-
const
|
|
2386
|
+
const incomeElapsed = elapsedMs + (state.incomeCarryMs ?? 0);
|
|
2387
|
+
const interval = manifest.passiveIncome.intervalMs;
|
|
2388
|
+
const ticks = Math.floor(incomeElapsed / interval);
|
|
2389
|
+
state.incomeCarryMs = incomeElapsed % interval;
|
|
2333
2390
|
if (ticks > 0) {
|
|
2334
2391
|
const currency = manifest.passiveIncome.currency;
|
|
2335
2392
|
state.currencies[currency] = (state.currencies[currency] ?? 0) + ticks * manifest.passiveIncome.amount;
|
|
@@ -2337,13 +2394,16 @@ function settleGameplay(state, manifest, now, options) {
|
|
|
2337
2394
|
}
|
|
2338
2395
|
}
|
|
2339
2396
|
if (state.mode === "sleep" && manifest.sleep !== void 0) {
|
|
2340
|
-
const
|
|
2397
|
+
const restoreElapsed = elapsedMs + (state.restoreCarryMs ?? 0);
|
|
2398
|
+
const interval = manifest.sleep.restore.intervalMs;
|
|
2399
|
+
const ticks = Math.floor(restoreElapsed / interval);
|
|
2400
|
+
state.restoreCarryMs = restoreElapsed % interval;
|
|
2341
2401
|
if (ticks > 0) {
|
|
2342
2402
|
const stat = manifest.sleep.restore.stat;
|
|
2343
2403
|
state.stats[stat] = (state.stats[stat] ?? 0) + ticks * manifest.sleep.restore.amount;
|
|
2344
2404
|
changed = true;
|
|
2345
2405
|
}
|
|
2346
|
-
}
|
|
2406
|
+
} else state.restoreCarryMs = 0;
|
|
2347
2407
|
state.settledAt = now;
|
|
2348
2408
|
clampGameplay(state, manifest);
|
|
2349
2409
|
return changed;
|
|
@@ -4474,39 +4534,51 @@ var PetService = class extends Service {
|
|
|
4474
4534
|
}
|
|
4475
4535
|
if (!this.enabled) return;
|
|
4476
4536
|
this.disposeActivity = (() => {
|
|
4477
|
-
const disposers = [
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
this.
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
this.
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4537
|
+
const disposers = [
|
|
4538
|
+
this.ctx.on("session/event", (session, event) => {
|
|
4539
|
+
const runtime = this.activityOf(session).runtime;
|
|
4540
|
+
if (event.type === "activity/status") {
|
|
4541
|
+
const payload = event.data ?? {};
|
|
4542
|
+
if (typeof payload.phase !== "string" || !isActivityPhase(payload.phase)) return;
|
|
4543
|
+
this.applyActivity(session, {
|
|
4544
|
+
phase: payload.phase,
|
|
4545
|
+
...typeof payload.line === "string" ? { line: payload.line } : {},
|
|
4546
|
+
...typeof payload.phrase === "string" ? { phrase: payload.phrase } : {}
|
|
4547
|
+
});
|
|
4548
|
+
if (payload.phase === "done" && !runtime.officialEventsSeen) this.rewardLegacyTurn();
|
|
4549
|
+
return;
|
|
4550
|
+
}
|
|
4551
|
+
const transition = projectOfficialEvent(event, runtime);
|
|
4552
|
+
if (transition === void 0) return;
|
|
4553
|
+
runtime.officialEventsSeen = true;
|
|
4554
|
+
this.officialEventSessions.add(session);
|
|
4555
|
+
this.applyActivity(session, transition.input, transition.whisper);
|
|
4556
|
+
if (transition.completedTurn !== void 0) this.rewardTurn(String(session.id), transition.completedTurn);
|
|
4557
|
+
}),
|
|
4558
|
+
this.ctx.on("agent/assistant-stream", ({ agent, frame }) => {
|
|
4559
|
+
const session = agent.session;
|
|
4560
|
+
const runtime = this.activityOf(session).runtime;
|
|
4561
|
+
const transition = projectAssistantStreamFrame(frame, runtime);
|
|
4562
|
+
if (transition === void 0) return;
|
|
4563
|
+
runtime.officialEventsSeen = true;
|
|
4564
|
+
this.officialEventSessions.add(session);
|
|
4565
|
+
this.applyActivity(session, transition.input, transition.whisper);
|
|
4566
|
+
}),
|
|
4567
|
+
this.ctx.on("session/disposed", (session) => {
|
|
4568
|
+
this.ledger.forgetSession(String(session.id));
|
|
4569
|
+
this.officialEventSessions.delete(session);
|
|
4570
|
+
this.sessionActivity.delete(session);
|
|
4571
|
+
if (session !== this.displaySession) return;
|
|
4572
|
+
this.displaySession = void 0;
|
|
4573
|
+
const remaining = [...this.sessionActivity.entries()].at(-1);
|
|
4574
|
+
if (remaining !== void 0) {
|
|
4575
|
+
const [nextSession, activity] = remaining;
|
|
4576
|
+
this.displaySession = nextSession;
|
|
4577
|
+
if (activity.lastInput !== void 0) this.machine.onActivityStatus(activity.lastInput);
|
|
4578
|
+
this.machine.onSessionActive();
|
|
4579
|
+
} else this.machine.onSessionDisposed();
|
|
4580
|
+
})
|
|
4581
|
+
];
|
|
4510
4582
|
return () => {
|
|
4511
4583
|
for (const dispose of disposers) dispose();
|
|
4512
4584
|
};
|
|
@@ -4776,6 +4848,40 @@ var PetService = class extends Service {
|
|
|
4776
4848
|
display: this.ledger.snapshot.display
|
|
4777
4849
|
};
|
|
4778
4850
|
}
|
|
4851
|
+
/**
|
|
4852
|
+
* The persisted skin for one entry, when the manifest still declares it: a
|
|
4853
|
+
* stale id (skin removed from the manifest, pet swapped) reads as "default"
|
|
4854
|
+
* rather than pinning a track the browser half cannot resolve.
|
|
4855
|
+
*/
|
|
4856
|
+
persistedSkin(entry) {
|
|
4857
|
+
const stored = this.ledger.petSkin(entry.id);
|
|
4858
|
+
if (stored === void 0) return void 0;
|
|
4859
|
+
return entry.frames2d?.skins?.some((skin) => skin.id === stored) === true ? stored : void 0;
|
|
4860
|
+
}
|
|
4861
|
+
/**
|
|
4862
|
+
* RPC: select the current pet's frames2d skin (`undefined` restores the
|
|
4863
|
+
* pet's default look). The choice is stored per pet, so every later state
|
|
4864
|
+
* view (reload, client restart, pet re-selection) serves it back.
|
|
4865
|
+
*/
|
|
4866
|
+
async setSkin(skin) {
|
|
4867
|
+
const entry = this.activeEntry();
|
|
4868
|
+
const declared = entry.frames2d?.skins ?? [];
|
|
4869
|
+
if (skin === void 0) {
|
|
4870
|
+
this.ledger.setPetSkin(entry.id, void 0);
|
|
4871
|
+
this.flush();
|
|
4872
|
+
return { ok: true };
|
|
4873
|
+
}
|
|
4874
|
+
if (!declared.some((candidate) => candidate.id === skin)) return {
|
|
4875
|
+
ok: false,
|
|
4876
|
+
error: "unknown-skin"
|
|
4877
|
+
};
|
|
4878
|
+
this.ledger.setPetSkin(entry.id, skin);
|
|
4879
|
+
this.flush();
|
|
4880
|
+
return {
|
|
4881
|
+
ok: true,
|
|
4882
|
+
skin
|
|
4883
|
+
};
|
|
4884
|
+
}
|
|
4779
4885
|
/** RPC: update display config (size / position). Values are clamped to whole pixels. */
|
|
4780
4886
|
async setConfig(patch) {
|
|
4781
4887
|
const next = {
|
|
@@ -4885,6 +4991,7 @@ var PetService = class extends Service {
|
|
|
4885
4991
|
gameplay = this.gameplayViewOf(state);
|
|
4886
4992
|
}
|
|
4887
4993
|
const announcement = this.announcement !== void 0 && announcementFresh(this.announcement, Date.now()) ? this.announcement : void 0;
|
|
4994
|
+
const skin = this.persistedSkin(entry);
|
|
4888
4995
|
return {
|
|
4889
4996
|
animation: snapshot.animation,
|
|
4890
4997
|
...snapshot.bubble === void 0 ? {} : { bubble: snapshot.bubble },
|
|
@@ -4901,6 +5008,7 @@ var PetService = class extends Service {
|
|
|
4901
5008
|
description: entry.description
|
|
4902
5009
|
},
|
|
4903
5010
|
name: this.petName(),
|
|
5011
|
+
...skin === void 0 ? {} : { skin },
|
|
4904
5012
|
treats: {
|
|
4905
5013
|
stocked: this.ledger.snapshot.treats.treats,
|
|
4906
5014
|
max: this.ledger.treatMax
|
|
@@ -5587,6 +5695,11 @@ function makePetRoutes(deps) {
|
|
|
5587
5695
|
if (typeof name !== "string") return Promise.reject(/* @__PURE__ */ new Error("invalid-name"));
|
|
5588
5696
|
return service.setName(name);
|
|
5589
5697
|
}),
|
|
5698
|
+
postRoute(ctx, "/api/pet/set-skin", (body) => {
|
|
5699
|
+
const skin = body.skin;
|
|
5700
|
+
if (skin !== void 0 && typeof skin !== "string") return Promise.reject(/* @__PURE__ */ new Error("invalid-skin"));
|
|
5701
|
+
return service.setSkin(skin === void 0 || skin === "" ? void 0 : skin);
|
|
5702
|
+
}),
|
|
5590
5703
|
postRoute(ctx, "/api/pet/set-pet", (body) => {
|
|
5591
5704
|
const petId = body.petId;
|
|
5592
5705
|
if (typeof petId !== "string") return Promise.reject(/* @__PURE__ */ new Error("invalid-pet"));
|
|
@@ -21,6 +21,14 @@ export interface GameplayApi {
|
|
|
21
21
|
setMode: (mode: 'work' | 'sleep' | null) => Promise<PetGameplayVerbResult>;
|
|
22
22
|
workTick: () => Promise<PetGameplayVerbResult>;
|
|
23
23
|
buy: (item: string) => Promise<PetGameplayVerbResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Persist the selected skin for the current pet (host-authoritative;
|
|
26
|
+
* `undefined` restores the pet's default look).
|
|
27
|
+
*/
|
|
28
|
+
setSkin: (skin: string | undefined) => Promise<{
|
|
29
|
+
ok: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
}>;
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* The per-pet coordination bus. The frames2d visual mount registers the
|
|
@@ -32,6 +40,13 @@ export interface GameplayBus {
|
|
|
32
40
|
setTrack?: (track?: string) => void;
|
|
33
41
|
/** Swap the pet's base idle track (skin switch); undefined restores default. */
|
|
34
42
|
setIdleTrack?: (track?: string) => void;
|
|
43
|
+
/**
|
|
44
|
+
* The base idle track the HUD wants right now, latched on the bus so a
|
|
45
|
+
* renderer that registers late (or remounts: hidden/summoned, StrictMode's
|
|
46
|
+
* double mount) still applies the restored skin instead of snapping back to
|
|
47
|
+
* the default look. Mutating it never requires a re-render.
|
|
48
|
+
*/
|
|
49
|
+
idleTrack?: string;
|
|
35
50
|
tap?: (fx: number, fy: number) => void;
|
|
36
51
|
/**
|
|
37
52
|
* Card open/close request from the chrome (the hover panel's 玩法 action):
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gameplay-hud.d.ts","sourceRoot":"","sources":["../../../src/client/gameplay-hud.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAsE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAC7G,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,gBAAgB,CAAA;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGtC,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACxD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC1E,QAAQ,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC9C,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"gameplay-hud.d.ts","sourceRoot":"","sources":["../../../src/client/gameplay-hud.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAsE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAC7G,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,gBAAgB,CAAA;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGtC,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACxD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC1E,QAAQ,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC9C,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACrD;;;OAGG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAChF;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,gFAAgF;IAChF,YAAY,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CACpC;AAYD,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,UAAU,EAAE,aAAa,CAAA;IACzB,KAAK,EAAE,gBAAgB,CAAA;IACvB,GAAG,EAAE,WAAW,CAAA;IAChB,GAAG,EAAE,WAAW,CAAA;IAChB,IAAI,EAAE,UAAU,CAAA;IAChB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;CAC/B,GAAG,YAAY,GAAG,IAAI,CAodtB"}
|
|
@@ -20,6 +20,8 @@ export function GameplayHud(props) {
|
|
|
20
20
|
const def = definition.gameplay;
|
|
21
21
|
const view = ui.snapshot?.gameplay;
|
|
22
22
|
const phase = ui.snapshot?.phase ?? 'idle';
|
|
23
|
+
// Host-persisted skin selection for this pet (undefined = default look).
|
|
24
|
+
const persistedSkin = ui.snapshot?.skin;
|
|
23
25
|
const [open, setOpen] = useState(false);
|
|
24
26
|
const [page, setPage] = useState('root');
|
|
25
27
|
// Currently selected skin id (base idle swap); undefined = default look.
|
|
@@ -247,12 +249,16 @@ export function GameplayHud(props) {
|
|
|
247
249
|
}, [definition.id, def]);
|
|
248
250
|
// Work loop: hold the work track, adjudicate one round per tick, play the
|
|
249
251
|
// result track for its hold window, then resume. Leaving the mode
|
|
250
|
-
// releases the override so the phase mapping takes over.
|
|
252
|
+
// releases the override so the phase mapping takes over. A skin that
|
|
253
|
+
// declares gameplayTracks for the work states plays its own art instead.
|
|
251
254
|
useEffect(() => {
|
|
252
255
|
const work = def?.work;
|
|
253
256
|
if (def === undefined || work === undefined || view?.mode !== 'work')
|
|
254
257
|
return undefined;
|
|
255
|
-
|
|
258
|
+
const skinGameplay = definition.frames2d?.skins?.find(skin => skin.id === skinIdRef.current)?.gameplayTracks;
|
|
259
|
+
/** The state's track, swapped for the skin's override when it declares one. */
|
|
260
|
+
const trackOf = (state) => skinGameplay?.[state] ?? state;
|
|
261
|
+
bus.setTrack?.(trackOf(work.state));
|
|
256
262
|
let resultTimer = 0;
|
|
257
263
|
const timer = window.setInterval(() => {
|
|
258
264
|
if (busyRef.current)
|
|
@@ -260,15 +266,20 @@ export function GameplayHud(props) {
|
|
|
260
266
|
busyRef.current = true;
|
|
261
267
|
void api.workTick().then((result) => {
|
|
262
268
|
busyRef.current = false;
|
|
269
|
+
// Leaving work mode while the adjudication is in flight drops the late
|
|
270
|
+
// result: writing it back would show work rewards and play the result
|
|
271
|
+
// track for a mode the user has already left (#1495).
|
|
272
|
+
if (modeRef.current !== 'work')
|
|
273
|
+
return;
|
|
263
274
|
applyResult(result);
|
|
264
275
|
if (result.ok !== true || result.outcome === undefined)
|
|
265
276
|
return;
|
|
266
|
-
const resultTrack = result.outcome === 'success' ? work.successState : work.failState;
|
|
277
|
+
const resultTrack = trackOf(result.outcome === 'success' ? work.successState : work.failState);
|
|
267
278
|
const hold = result.outcome === 'success' ? work.resultMs?.success ?? 1300 : work.resultMs?.fail ?? 1900;
|
|
268
279
|
bus.setTrack?.(resultTrack);
|
|
269
280
|
resultTimer = window.setTimeout(() => {
|
|
270
281
|
if (modeRef.current === 'work')
|
|
271
|
-
bus.setTrack?.(work.state);
|
|
282
|
+
bus.setTrack?.(trackOf(work.state));
|
|
272
283
|
}, hold);
|
|
273
284
|
}, () => { busyRef.current = false; });
|
|
274
285
|
}, work.tickMs);
|
|
@@ -277,8 +288,8 @@ export function GameplayHud(props) {
|
|
|
277
288
|
window.clearTimeout(resultTimer);
|
|
278
289
|
bus.setTrack?.(undefined);
|
|
279
290
|
};
|
|
280
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- the loop keys on the mode
|
|
281
|
-
}, [definition.id, def, view?.mode]);
|
|
291
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- the loop keys on the mode and the selected skin
|
|
292
|
+
}, [definition.id, def, view?.mode, skinId]);
|
|
282
293
|
// Sleep loop: hold the sleep track; restore is host-side (lazy settle).
|
|
283
294
|
// While a skin with a gameplayTracks.sleep override is selected, the skin's
|
|
284
295
|
// own track replaces the default sleep track (e.g. a skin-specific doze).
|
|
@@ -315,10 +326,41 @@ export function GameplayHud(props) {
|
|
|
315
326
|
const setMode = (next) => {
|
|
316
327
|
void api.setMode(next).then(applyResult, () => undefined);
|
|
317
328
|
};
|
|
329
|
+
// Skin selection is persisted host-side (per pet): re-seed the menu from
|
|
330
|
+
// every fresh state view, so a page reload or client restart keeps the last
|
|
331
|
+
// choice instead of snapping back to the default look.
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
setSkinId(persistedSkin);
|
|
334
|
+
}, [definition.id, persistedSkin]);
|
|
335
|
+
// Push the resolved base idle track into the renderer whenever the pet or
|
|
336
|
+
// the selection changes. The value is latched on the bus first: the visual
|
|
337
|
+
// may register later, or remount later (hidden/summoned), and reads the
|
|
338
|
+
// latch back on activation so a restored skin never falls back to default.
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
const skin = definition.frames2d?.skins?.find(candidate => candidate.id === skinId);
|
|
341
|
+
bus.idleTrack = skin?.idleTrack;
|
|
342
|
+
bus.setIdleTrack?.(skin?.idleTrack);
|
|
343
|
+
// persistedSkin rides the deps so the host's value also re-pushes on
|
|
344
|
+
// arrival (a renderer that mounted early still converges).
|
|
345
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one push per selection
|
|
346
|
+
}, [definition.id, skinId, persistedSkin]);
|
|
318
347
|
const skins = definition.frames2d?.skins;
|
|
348
|
+
/** The base idle track one skin id resolves to (undefined = default look). */
|
|
349
|
+
const skinTrackOf = (id) => id === undefined ? undefined : definition.frames2d?.skins?.find(candidate => candidate.id === id)?.idleTrack;
|
|
319
350
|
const selectSkin = (skin) => {
|
|
320
351
|
setSkinId(skin?.id);
|
|
321
352
|
bus.setIdleTrack?.(skin?.idleTrack);
|
|
353
|
+
// The host owns the choice: a refusal (unknown skin) restores both the
|
|
354
|
+
// menu highlight and the renderer to the value it still serves.
|
|
355
|
+
const restore = () => {
|
|
356
|
+
setSkinId(persistedSkin);
|
|
357
|
+
bus.setIdleTrack?.(skinTrackOf(persistedSkin));
|
|
358
|
+
};
|
|
359
|
+
void api.setSkin(skin?.id).then((result) => {
|
|
360
|
+
if (result.ok)
|
|
361
|
+
return;
|
|
362
|
+
restore();
|
|
363
|
+
}, restore);
|
|
322
364
|
};
|
|
323
365
|
return (_jsxs("div", { ref: hudRef, className: styles.gameplayHud, "data-dsh-pet-gameplay": definition.id, children: [floats.map(entry => (_jsx("div", { className: styles.gameplayFloat, children: entry.text }, entry.id))), mode !== null && (_jsx("div", { className: styles.gameplayModeChip, children: tr(mode === 'work' ? 'pet.gameplay.working' : 'pet.gameplay.sleeping') })), open && (_jsxs("div", { ref: cardRef, className: styles.gameplayCard, "data-page": page, children: [page === 'root' && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.gameplayBars, children: Object.entries(stats).map(([name, stat]) => {
|
|
324
366
|
const value = view.stats[name] ?? 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAiFlG,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAuBD,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAqU9C"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { createElement } from 'react';
|
|
16
16
|
import { createRoot } from 'react-dom/client';
|
|
17
17
|
import { createPetStore } from "./pet-store.js";
|
|
18
|
+
import { createWorkTickGate } from "./work-tick-gate.js";
|
|
18
19
|
import { PetDockEntry } from "./PetDockEntry.js";
|
|
19
20
|
import { defaultPetRendererRegistry } from "./renderers/registry.js";
|
|
20
21
|
import { live2dRenderer } from "./renderers/live2d.js";
|
|
@@ -47,6 +48,7 @@ const petApi = {
|
|
|
47
48
|
setConfig: (patch) => petFetch('/api/pet/set-config', patch),
|
|
48
49
|
setName: (name) => petFetch('/api/pet/set-name', { name }),
|
|
49
50
|
setPet: (petId) => petFetch('/api/pet/set-pet', { petId }),
|
|
51
|
+
setSkin: (skin) => petFetch('/api/pet/set-skin', skin === undefined ? {} : { skin }),
|
|
50
52
|
gameplayTouch: (zone) => petFetch('/api/pet/gameplay/touch', zone === undefined ? {} : { zone }),
|
|
51
53
|
gameplaySetMode: (mode) => petFetch('/api/pet/gameplay/mode', { mode }),
|
|
52
54
|
gameplayWorkTick: () => petFetch('/api/pet/gameplay/work-tick', {}),
|
|
@@ -64,16 +66,18 @@ export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote
|
|
|
64
66
|
* first-level settings section.
|
|
65
67
|
* @param ctx - client root context.
|
|
66
68
|
*/
|
|
69
|
+
/** The page-wide work-tick gate; its window follows the active pet's cadence. */
|
|
70
|
+
const workTickGate = createWorkTickGate();
|
|
67
71
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* call re-rolls, re-grants treats and re-plays the success/fail track, so
|
|
72
|
-
* the outcome appears to play several times per window. This shared marker
|
|
73
|
-
* accepts the first adjudication of a window and silently suppresses the
|
|
74
|
-
* duplicates that follow. Reset when (re-)entering work mode.
|
|
72
|
+
* The work cadence the active pet declares, when its registry entry is known.
|
|
73
|
+
* @param store - the pet store holding the host snapshot and the registry list.
|
|
74
|
+
* @returns the configured `gameplay.work.tickMs`, or undefined when unknown.
|
|
75
75
|
*/
|
|
76
|
-
|
|
76
|
+
function activeWorkTickMs(store) {
|
|
77
|
+
const state = store.getSnapshot();
|
|
78
|
+
const definition = state.pets.find((entry) => entry.id === state.snapshot?.pet.id);
|
|
79
|
+
return definition?.gameplay?.work?.tickMs;
|
|
80
|
+
}
|
|
77
81
|
export function apply(ctx) {
|
|
78
82
|
// Anonymous install heartbeat (docs/telemetry.md): one beat per browser per
|
|
79
83
|
// UTC day, package name only, silent failure.
|
|
@@ -309,19 +313,23 @@ export function apply(ctx) {
|
|
|
309
313
|
},
|
|
310
314
|
gameplay: {
|
|
311
315
|
touch: (zone) => petApi.gameplayTouch(zone),
|
|
316
|
+
setSkin: (skin) => petApi.setSkin(skin).then((result) => {
|
|
317
|
+
if (result.ok)
|
|
318
|
+
pollNow();
|
|
319
|
+
return result;
|
|
320
|
+
}, () => ({ ok: false, error: 'transport' })),
|
|
312
321
|
setMode: async (mode) => {
|
|
313
322
|
if (mode === 'work')
|
|
314
|
-
|
|
323
|
+
workTickGate.reset();
|
|
315
324
|
return petApi.gameplaySetMode(mode);
|
|
316
325
|
},
|
|
317
326
|
workTick: async () => {
|
|
318
327
|
// One adjudication per tick window, page-wide: suppress stale
|
|
319
|
-
// duplicate intervals (HMR) re-playing the result track.
|
|
320
|
-
|
|
321
|
-
if (
|
|
328
|
+
// duplicate intervals (HMR) re-playing the result track. The window
|
|
329
|
+
// is the pet's own cadence, so a shorter tickMs is not downgraded.
|
|
330
|
+
if (!workTickGate.allow(activeWorkTickMs(petStore))) {
|
|
322
331
|
return { ok: true };
|
|
323
332
|
}
|
|
324
|
-
lastWorkTickAt = now;
|
|
325
333
|
return petApi.gameplayWorkTick();
|
|
326
334
|
},
|
|
327
335
|
buy: (item) => petApi.gameplayBuy(item),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Frames2dVisualMount.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/Frames2dVisualMount.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAA+B,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAInD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAEvC,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,UAAU,EAAE,aAAa,CAAA;IACzB,KAAK,EAAE,aAAa,CAAA;IACpB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,gEAAgE;IAChE,IAAI,EAAE,UAAU,CAAA;IAChB,yEAAyE;IACzE,GAAG,CAAC,EAAE,WAAW,CAAA;IACjB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;CAC/B,GAAG,YAAY,
|
|
1
|
+
{"version":3,"file":"Frames2dVisualMount.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/Frames2dVisualMount.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAA+B,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAInD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAEvC,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,UAAU,EAAE,aAAa,CAAA;IACzB,KAAK,EAAE,aAAa,CAAA;IACpB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,gEAAgE;IAChE,IAAI,EAAE,UAAU,CAAA;IAChB,yEAAyE;IACzE,GAAG,CAAC,EAAE,WAAW,CAAA;IACjB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;CAC/B,GAAG,YAAY,CAqFf"}
|
|
@@ -52,6 +52,11 @@ export function Frames2dVisualMount(props) {
|
|
|
52
52
|
const gameplayBus = props.bus;
|
|
53
53
|
gameplayBus.setTrack = (track) => { handleRef.current?.setState(track); };
|
|
54
54
|
gameplayBus.setIdleTrack = (track) => { handleRef.current?.setIdleTrack(track); };
|
|
55
|
+
// The HUD latches the wanted base idle (skin selection, restored from the
|
|
56
|
+
// host snapshot): apply it on activation, so a late or repeated mount
|
|
57
|
+
// never repaints the pet with the default look.
|
|
58
|
+
if (gameplayBus.idleTrack !== undefined)
|
|
59
|
+
handle.setIdleTrack(gameplayBus.idleTrack);
|
|
55
60
|
cleanups.push(() => {
|
|
56
61
|
gameplayBus.setTrack = undefined;
|
|
57
62
|
gameplayBus.setIdleTrack = undefined;
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* Frame presentation has two modes picked once at mount by capability
|
|
13
13
|
* probing:
|
|
14
14
|
* - Canvas bitmap buffer (default where createImageBitmap/fetch/2D context
|
|
15
|
-
* exist):
|
|
16
|
-
* the
|
|
17
|
-
* zero DOM mutations and zero
|
|
18
|
-
* <img>.src per frame drove image
|
|
15
|
+
* exist): frames are decoded once into an ImageBitmap on demand, with a
|
|
16
|
+
* bounded look-ahead window over the playing track, and drawn onto one
|
|
17
|
+
* <canvas> - steady-state playback issues zero DOM mutations and zero
|
|
18
|
+
* re-decodes (measured hotspot: swapping <img>.src per frame drove image
|
|
19
|
+
* decode + invalidation every tick).
|
|
19
20
|
* - Classic <img> fallback (jsdom/tests or missing APIs): identical to the
|
|
20
21
|
* historical behavior - cache-warm Image elements plus guarded src swaps,
|
|
21
22
|
* so environments without modern decoding keep working unchanged.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frames2d.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/frames2d.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"frames2d.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/frames2d.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAA4B,KAAK,WAAW,EAA2B,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AACzI,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IAC3C,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACjE,wEAAwE;IACxE,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAA;CAC7B;AAED,sEAAsE;AACtE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAA;IACjB,sFAAsF;IACtF,YAAY,CAAC,EAAE,6BAA6B,EAAE,CAAA;IAC9C,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAED,4DAA4D;AAC5D,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IACzC;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IAC7C,2DAA2D;IAC3D,YAAY,IAAI,MAAM,CAAA;CACvB;AAuFD,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,iBAAiB,CAoU3D,CAAA"}
|