@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/types/service.d.ts
CHANGED
|
@@ -117,6 +117,12 @@ export interface PetStateView {
|
|
|
117
117
|
};
|
|
118
118
|
/** The selected pet's display name (user rename or manifest default). */
|
|
119
119
|
name: string;
|
|
120
|
+
/**
|
|
121
|
+
* The selected frames2d skin id for this pet (absent = the pet's default
|
|
122
|
+
* look). Persisted host-side, so a page reload or client restart restores
|
|
123
|
+
* the user's last choice instead of falling back to the default look.
|
|
124
|
+
*/
|
|
125
|
+
skin?: string;
|
|
120
126
|
/** Treat (小鱼干) stock snapshot. */
|
|
121
127
|
treats: {
|
|
122
128
|
/** Stocked treats now. */
|
|
@@ -308,6 +314,24 @@ export declare class PetService extends Service {
|
|
|
308
314
|
ok: true;
|
|
309
315
|
display: PetDisplayConfig;
|
|
310
316
|
}>;
|
|
317
|
+
/**
|
|
318
|
+
* The persisted skin for one entry, when the manifest still declares it: a
|
|
319
|
+
* stale id (skin removed from the manifest, pet swapped) reads as "default"
|
|
320
|
+
* rather than pinning a track the browser half cannot resolve.
|
|
321
|
+
*/
|
|
322
|
+
private persistedSkin;
|
|
323
|
+
/**
|
|
324
|
+
* RPC: select the current pet's frames2d skin (`undefined` restores the
|
|
325
|
+
* pet's default look). The choice is stored per pet, so every later state
|
|
326
|
+
* view (reload, client restart, pet re-selection) serves it back.
|
|
327
|
+
*/
|
|
328
|
+
setSkin(skin: string | undefined): Promise<{
|
|
329
|
+
ok: true;
|
|
330
|
+
skin?: string;
|
|
331
|
+
} | {
|
|
332
|
+
ok: false;
|
|
333
|
+
error: string;
|
|
334
|
+
}>;
|
|
311
335
|
/** RPC: update display config (size / position). Values are clamped to whole pixels. */
|
|
312
336
|
setConfig(patch: Partial<PetDisplayConfig>): Promise<{
|
|
313
337
|
ok: true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
|
package/lib/types/service.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { Service } from '@deepseek-ai/cordis';
|
|
15
15
|
import { announcementFresh, parseAnnouncement } from "./announce.js";
|
|
16
|
-
import { emptyProjectionRuntime, isActivityPhase, projectOfficialEvent, } from "./event-projection.js";
|
|
16
|
+
import { emptyProjectionRuntime, isActivityPhase, projectAssistantStreamFrame, projectOfficialEvent, } from "./event-projection.js";
|
|
17
17
|
import { PetLedger } from "./ledger.js";
|
|
18
18
|
import { DEFAULT_PET_NAME, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, PET_NAME_MAX_LENGTH, loadPetPersist, petHomeDir, savePetPersist, } from "./persist.js";
|
|
19
19
|
import { DEFAULT_DECORATION_ID, decorationView, loadPetRegistry, petEntryView, petPackageRoot, } from "./registry.js";
|
|
@@ -236,6 +236,16 @@ export class PetService extends Service {
|
|
|
236
236
|
this.rewardTurn(String(session.id), transition.completedTurn);
|
|
237
237
|
}
|
|
238
238
|
}),
|
|
239
|
+
this.ctx.on('agent/assistant-stream', ({ agent, frame }) => {
|
|
240
|
+
const session = agent.session;
|
|
241
|
+
const runtime = this.activityOf(session).runtime;
|
|
242
|
+
const transition = projectAssistantStreamFrame(frame, runtime);
|
|
243
|
+
if (transition === undefined)
|
|
244
|
+
return;
|
|
245
|
+
runtime.officialEventsSeen = true;
|
|
246
|
+
this.officialEventSessions.add(session);
|
|
247
|
+
this.applyActivity(session, transition.input, transition.whisper);
|
|
248
|
+
}),
|
|
239
249
|
this.ctx.on('session/disposed', (session) => {
|
|
240
250
|
this.ledger.forgetSession(String(session.id));
|
|
241
251
|
this.officialEventSessions.delete(session);
|
|
@@ -492,6 +502,36 @@ export class PetService extends Service {
|
|
|
492
502
|
this.syncSettingsFromPet();
|
|
493
503
|
return { ok: true, display: this.ledger.snapshot.display };
|
|
494
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* The persisted skin for one entry, when the manifest still declares it: a
|
|
507
|
+
* stale id (skin removed from the manifest, pet swapped) reads as "default"
|
|
508
|
+
* rather than pinning a track the browser half cannot resolve.
|
|
509
|
+
*/
|
|
510
|
+
persistedSkin(entry) {
|
|
511
|
+
const stored = this.ledger.petSkin(entry.id);
|
|
512
|
+
if (stored === undefined)
|
|
513
|
+
return undefined;
|
|
514
|
+
return entry.frames2d?.skins?.some(skin => skin.id === stored) === true ? stored : undefined;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* RPC: select the current pet's frames2d skin (`undefined` restores the
|
|
518
|
+
* pet's default look). The choice is stored per pet, so every later state
|
|
519
|
+
* view (reload, client restart, pet re-selection) serves it back.
|
|
520
|
+
*/
|
|
521
|
+
async setSkin(skin) {
|
|
522
|
+
const entry = this.activeEntry();
|
|
523
|
+
const declared = entry.frames2d?.skins ?? [];
|
|
524
|
+
if (skin === undefined) {
|
|
525
|
+
this.ledger.setPetSkin(entry.id, undefined);
|
|
526
|
+
this.flush();
|
|
527
|
+
return { ok: true };
|
|
528
|
+
}
|
|
529
|
+
if (!declared.some(candidate => candidate.id === skin))
|
|
530
|
+
return { ok: false, error: 'unknown-skin' };
|
|
531
|
+
this.ledger.setPetSkin(entry.id, skin);
|
|
532
|
+
this.flush();
|
|
533
|
+
return { ok: true, skin };
|
|
534
|
+
}
|
|
495
535
|
/** RPC: update display config (size / position). Values are clamped to whole pixels. */
|
|
496
536
|
async setConfig(patch) {
|
|
497
537
|
const next = { ...this.ledger.snapshot.display, ...patch };
|
|
@@ -626,6 +666,7 @@ export class PetService extends Service {
|
|
|
626
666
|
const announcement = this.announcement !== undefined && announcementFresh(this.announcement, Date.now())
|
|
627
667
|
? this.announcement
|
|
628
668
|
: undefined;
|
|
669
|
+
const skin = this.persistedSkin(entry);
|
|
629
670
|
return {
|
|
630
671
|
animation: snapshot.animation,
|
|
631
672
|
...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
|
|
@@ -642,6 +683,7 @@ export class PetService extends Service {
|
|
|
642
683
|
description: entry.description,
|
|
643
684
|
},
|
|
644
685
|
name: this.petName(),
|
|
686
|
+
...(skin === undefined ? {} : { skin }),
|
|
645
687
|
treats: {
|
|
646
688
|
stocked: this.ledger.snapshot.treats.treats,
|
|
647
689
|
max: this.ledger.treatMax,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
3
|
"description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.21",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"license": "Apache-2.0",
|
|
42
42
|
"dsh": {
|
|
43
43
|
"engines": {
|
|
44
|
-
"dsh": ">=0.1.
|
|
44
|
+
"dsh": ">=0.1.5-rc.1"
|
|
45
45
|
},
|
|
46
46
|
"bundle": {
|
|
47
47
|
"patch": "./cordis.patch.yml"
|
|
@@ -66,18 +66,18 @@
|
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
69
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.
|
|
70
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.
|
|
71
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.
|
|
72
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
73
|
-
"@deepseek-ai/dsh-client-store": "^0.1.
|
|
74
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
78
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
79
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
80
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
69
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.5-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.5-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
|
|
73
|
+
"@deepseek-ai/dsh-client-store": "^0.1.5-rc.1",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.1",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
|
|
78
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
|
|
79
|
+
"@deepseek-ai/dsh-session": "^0.1.5-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
81
81
|
"@testing-library/dom": "^10.4.1",
|
|
82
82
|
"@testing-library/react": "^16.3.2",
|
|
83
83
|
"@types/node": "^22.20.0",
|
|
@@ -110,7 +110,7 @@ function petDefinition(): PetDefinition {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function snapshot(view: PetGameplayStateView): PetStateView {
|
|
113
|
+
function snapshot(view: PetGameplayStateView, skin?: string): PetStateView {
|
|
114
114
|
return {
|
|
115
115
|
animation: 'idle',
|
|
116
116
|
phase: 'idle',
|
|
@@ -121,6 +121,7 @@ function snapshot(view: PetGameplayStateView): PetStateView {
|
|
|
121
121
|
pet: { id: 'miku', displayName: 'Miku', description: '' },
|
|
122
122
|
name: 'Miku',
|
|
123
123
|
treats: { stocked: 0, max: 5 },
|
|
124
|
+
...(skin === undefined ? {} : { skin }),
|
|
124
125
|
gameplay: view,
|
|
125
126
|
}
|
|
126
127
|
}
|
|
@@ -128,7 +129,7 @@ function snapshot(view: PetGameplayStateView): PetStateView {
|
|
|
128
129
|
interface Harness {
|
|
129
130
|
store: PetStoreInstance
|
|
130
131
|
bus: GameplayBus
|
|
131
|
-
api: GameplayApi & { touch: ReturnType<typeof vi.fn>; setMode: ReturnType<typeof vi.fn>; workTick: ReturnType<typeof vi.fn>; buy: ReturnType<typeof vi.fn> }
|
|
132
|
+
api: GameplayApi & { touch: ReturnType<typeof vi.fn>; setMode: ReturnType<typeof vi.fn>; workTick: ReturnType<typeof vi.fn>; buy: ReturnType<typeof vi.fn>; setSkin: ReturnType<typeof vi.fn> }
|
|
132
133
|
setTrack: ReturnType<typeof vi.fn>
|
|
133
134
|
drag: ReturnType<typeof createDragStream>
|
|
134
135
|
setView: (view: PetGameplayStateView) => void
|
|
@@ -147,6 +148,7 @@ function harness(view: PetGameplayStateView = gameplayView()): Harness {
|
|
|
147
148
|
setMode: vi.fn(async (mode: 'work' | 'sleep' | null) => ok({ view: gameplayView({ mode }) })),
|
|
148
149
|
workTick: vi.fn(async () => ok({ outcome: 'success' as const, view: gameplayView({ mode: 'work' }) })),
|
|
149
150
|
buy: vi.fn(async () => ok({ view: gameplayView() })),
|
|
151
|
+
setSkin: vi.fn(async () => ({ ok: true })),
|
|
150
152
|
}
|
|
151
153
|
const setView = (next: PetGameplayStateView): void => store.actions.setSnapshot(snapshot(next))
|
|
152
154
|
render(<GameplayHud definition={petDefinition()} store={store} api={api} bus={bus} drag={drag} t={t} />)
|
|
@@ -284,6 +286,7 @@ describe('GameplayHud', () => {
|
|
|
284
286
|
const api = {
|
|
285
287
|
touch: vi.fn(async () => ({ ok: true, view: gameplayView() })),
|
|
286
288
|
setMode: vi.fn(), workTick: vi.fn(), buy: vi.fn(),
|
|
289
|
+
setSkin: vi.fn(async () => ({ ok: true })),
|
|
287
290
|
} as unknown as Harness['api']
|
|
288
291
|
const setTrack = vi.fn()
|
|
289
292
|
const bus: GameplayBus = { setTrack }
|
|
@@ -316,6 +319,7 @@ describe('GameplayHud', () => {
|
|
|
316
319
|
const api = {
|
|
317
320
|
touch: vi.fn(async () => ({ ok: true, view: gameplayView() })),
|
|
318
321
|
setMode: vi.fn(), workTick: vi.fn(), buy: vi.fn(),
|
|
322
|
+
setSkin: vi.fn(async () => ({ ok: true })),
|
|
319
323
|
} as unknown as Harness['api']
|
|
320
324
|
const setTrack = vi.fn()
|
|
321
325
|
const bus: GameplayBus = { setTrack }
|
|
@@ -335,6 +339,101 @@ describe('GameplayHud', () => {
|
|
|
335
339
|
expect(api.touch).not.toHaveBeenCalledWith('head')
|
|
336
340
|
})
|
|
337
341
|
|
|
342
|
+
it('restores the host-persisted skin and pushes its idle track on mount', () => {
|
|
343
|
+
// Reload / client restart: the state view carries the last choice, so the
|
|
344
|
+
// renderer repaints the selected skin without any interaction.
|
|
345
|
+
const def = petDefinition()
|
|
346
|
+
def.frames2d!.skins = [
|
|
347
|
+
{ id: 'lanhainishang', label: '蓝海霓裳', idleTrack: 'lanhainishang-idle' },
|
|
348
|
+
]
|
|
349
|
+
const store = createPetStore().create()
|
|
350
|
+
store.actions.setSnapshot(snapshot(gameplayView(), 'lanhainishang'))
|
|
351
|
+
const setSkin = vi.fn(async () => ({ ok: true }))
|
|
352
|
+
const api = { touch: vi.fn(), setMode: vi.fn(), workTick: vi.fn(), buy: vi.fn(), setSkin } as unknown as Harness['api']
|
|
353
|
+
const setIdleTrack = vi.fn()
|
|
354
|
+
const bus: GameplayBus = { setTrack: vi.fn(), setIdleTrack }
|
|
355
|
+
render(<GameplayHud definition={def} store={store} api={api} bus={bus} drag={createDragStream()} t={t} />)
|
|
356
|
+
expect(setIdleTrack).toHaveBeenLastCalledWith('lanhainishang-idle')
|
|
357
|
+
// Latched on the bus too, so a later (re)mount re-applies it.
|
|
358
|
+
expect(bus.idleTrack).toBe('lanhainishang-idle')
|
|
359
|
+
// Restoring is read-only: mounting never writes the choice back.
|
|
360
|
+
expect(setSkin).not.toHaveBeenCalled()
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('persists the skin selection through the host API', async () => {
|
|
364
|
+
const def = petDefinition()
|
|
365
|
+
def.frames2d!.skins = [
|
|
366
|
+
{ id: 'lanhainishang', label: '蓝海霓裳', idleTrack: 'lanhainishang-idle' },
|
|
367
|
+
]
|
|
368
|
+
const store = createPetStore().create()
|
|
369
|
+
store.actions.setSnapshot(snapshot(gameplayView()))
|
|
370
|
+
const setSkin = vi.fn(async () => ({ ok: true }))
|
|
371
|
+
const api = { touch: vi.fn(), setMode: vi.fn(), workTick: vi.fn(), buy: vi.fn(), setSkin } as unknown as Harness['api']
|
|
372
|
+
const setIdleTrack = vi.fn()
|
|
373
|
+
const bus: GameplayBus = { setTrack: vi.fn(), setIdleTrack }
|
|
374
|
+
render(<GameplayHud definition={def} store={store} api={api} bus={bus} drag={createDragStream()} t={t} />)
|
|
375
|
+
await act(async () => {
|
|
376
|
+
bus.openCard?.()
|
|
377
|
+
})
|
|
378
|
+
fireEvent.click(screen.getByText('皮肤'))
|
|
379
|
+
await act(async () => {
|
|
380
|
+
fireEvent.click(screen.getByText('蓝海霓裳'))
|
|
381
|
+
})
|
|
382
|
+
expect(setIdleTrack).toHaveBeenLastCalledWith('lanhainishang-idle')
|
|
383
|
+
expect(setSkin).toHaveBeenCalledWith('lanhainishang')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('clears the persisted skin when the default look is picked', async () => {
|
|
387
|
+
const def = petDefinition()
|
|
388
|
+
def.frames2d!.skins = [
|
|
389
|
+
{ id: 'lanhainishang', label: '蓝海霓裳', idleTrack: 'lanhainishang-idle' },
|
|
390
|
+
]
|
|
391
|
+
const store = createPetStore().create()
|
|
392
|
+
store.actions.setSnapshot(snapshot(gameplayView(), 'lanhainishang'))
|
|
393
|
+
const setSkin = vi.fn(async () => ({ ok: true }))
|
|
394
|
+
const api = { touch: vi.fn(), setMode: vi.fn(), workTick: vi.fn(), buy: vi.fn(), setSkin } as unknown as Harness['api']
|
|
395
|
+
const setIdleTrack = vi.fn()
|
|
396
|
+
const bus: GameplayBus = { setTrack: vi.fn(), setIdleTrack }
|
|
397
|
+
render(<GameplayHud definition={def} store={store} api={api} bus={bus} drag={createDragStream()} t={t} />)
|
|
398
|
+
await act(async () => {
|
|
399
|
+
bus.openCard?.()
|
|
400
|
+
})
|
|
401
|
+
fireEvent.click(screen.getByText('皮肤'))
|
|
402
|
+
await act(async () => {
|
|
403
|
+
fireEvent.click(screen.getByText('默认'))
|
|
404
|
+
})
|
|
405
|
+
expect(setSkin).toHaveBeenCalledWith(undefined)
|
|
406
|
+
expect(setIdleTrack).toHaveBeenLastCalledWith(undefined)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('falls back to the served skin when the host rejects the choice', async () => {
|
|
410
|
+
const def = petDefinition()
|
|
411
|
+
def.frames2d!.skins = [
|
|
412
|
+
{ id: 'lanhainishang', label: '蓝海霓裳', idleTrack: 'lanhainishang-idle' },
|
|
413
|
+
]
|
|
414
|
+
const store = createPetStore().create()
|
|
415
|
+
store.actions.setSnapshot(snapshot(gameplayView()))
|
|
416
|
+
const setSkin = vi.fn(async () => ({ ok: false, error: 'unknown-skin' }))
|
|
417
|
+
const api = { touch: vi.fn(), setMode: vi.fn(), workTick: vi.fn(), buy: vi.fn(), setSkin } as unknown as Harness['api']
|
|
418
|
+
const setIdleTrack = vi.fn()
|
|
419
|
+
const bus: GameplayBus = { setTrack: vi.fn(), setIdleTrack }
|
|
420
|
+
render(<GameplayHud definition={def} store={store} api={api} bus={bus} drag={createDragStream()} t={t} />)
|
|
421
|
+
await act(async () => {
|
|
422
|
+
bus.openCard?.()
|
|
423
|
+
})
|
|
424
|
+
fireEvent.click(screen.getByText('皮肤'))
|
|
425
|
+
await act(async () => {
|
|
426
|
+
fireEvent.click(screen.getByText('蓝海霓裳'))
|
|
427
|
+
// The rejected write settles on a later microtask; flush it so the
|
|
428
|
+
// rollback render reaches the bus before the assertion.
|
|
429
|
+
await Promise.resolve()
|
|
430
|
+
await Promise.resolve()
|
|
431
|
+
})
|
|
432
|
+
expect(setSkin).toHaveBeenCalledWith('lanhainishang')
|
|
433
|
+
// The rejected optimistic swap rolls back to what the host still serves.
|
|
434
|
+
expect(setIdleTrack).toHaveBeenLastCalledWith(undefined)
|
|
435
|
+
})
|
|
436
|
+
|
|
338
437
|
it('runs the idle director: weighted act rolls on the interval', async () => {
|
|
339
438
|
const h = harness()
|
|
340
439
|
await act(async () => {
|
|
@@ -372,6 +471,70 @@ describe('GameplayHud', () => {
|
|
|
372
471
|
expect(h.store.getSnapshot().snapshot?.gameplay?.mode).toBe('work')
|
|
373
472
|
})
|
|
374
473
|
|
|
474
|
+
it('drops a late work adjudication once the mode has been left', async () => {
|
|
475
|
+
// #1495: the tick RPC can still be in flight when the user exits work mode;
|
|
476
|
+
// its result must not write the work view back or play the result track.
|
|
477
|
+
const h = harness(gameplayView({ mode: 'work' }))
|
|
478
|
+
let resolveTick: ((result: PetGameplayVerbResult) => void) | undefined
|
|
479
|
+
h.api.workTick.mockImplementationOnce(() => new Promise<PetGameplayVerbResult>((resolve) => { resolveTick = resolve }))
|
|
480
|
+
await act(async () => {
|
|
481
|
+
vi.advanceTimersByTime(10_000)
|
|
482
|
+
})
|
|
483
|
+
expect(h.api.workTick).toHaveBeenCalledTimes(1)
|
|
484
|
+
await act(async () => {
|
|
485
|
+
h.setView(gameplayView({ mode: null }))
|
|
486
|
+
})
|
|
487
|
+
await act(async () => {
|
|
488
|
+
resolveTick?.({ ok: true, outcome: 'success', view: gameplayView({ mode: 'work' }) })
|
|
489
|
+
await Promise.resolve()
|
|
490
|
+
})
|
|
491
|
+
expect(h.store.getSnapshot().snapshot?.gameplay?.mode).toBeNull()
|
|
492
|
+
expect(h.setTrack).not.toHaveBeenCalledWith('success')
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('drives the skin work loop and its skin result animation on the shared 10s rule', async () => {
|
|
496
|
+
// A skin supplies its own work / result art, but the adjudication rule
|
|
497
|
+
// (10s tick, 50% roll, result hold) stays the pet-level gameplay.work rule.
|
|
498
|
+
const def = petDefinition()
|
|
499
|
+
def.frames2d!.tracks['skin-work'] = { frames: ['/pet/miku/skin-work_1.webp'], durations: [200], loop: true }
|
|
500
|
+
def.frames2d!.tracks['skin-success'] = { frames: ['/pet/miku/skin-ok_1.webp'], durations: [200], loop: false, fallback: 'skin-work' }
|
|
501
|
+
def.frames2d!.skins = [
|
|
502
|
+
{
|
|
503
|
+
id: 'skin', label: 'Skin', idleTrack: 'idle',
|
|
504
|
+
// The override key is the manifest's successState ('success' in this fixture).
|
|
505
|
+
gameplayTracks: { work: 'skin-work', success: 'skin-success' },
|
|
506
|
+
},
|
|
507
|
+
]
|
|
508
|
+
const store = createPetStore().create()
|
|
509
|
+
store.actions.setSnapshot(snapshot(gameplayView({ mode: 'work' }), 'skin'))
|
|
510
|
+
const setTrack = vi.fn()
|
|
511
|
+
const bus: GameplayBus = { setTrack }
|
|
512
|
+
const api = {
|
|
513
|
+
touch: vi.fn(), setMode: vi.fn(),
|
|
514
|
+
workTick: vi.fn(async () => ({ ok: true, outcome: 'success' as const, view: gameplayView({ mode: 'work' }) })),
|
|
515
|
+
buy: vi.fn(), setSkin: vi.fn(async () => ({ ok: true })),
|
|
516
|
+
} as unknown as Harness['api']
|
|
517
|
+
render(<GameplayHud definition={def} store={store} api={api} bus={bus} drag={createDragStream()} t={t} />)
|
|
518
|
+
// The mode hold uses the skin's own work loop, not the manifest track.
|
|
519
|
+
expect(setTrack).toHaveBeenCalledWith('skin-work')
|
|
520
|
+
await act(async () => {
|
|
521
|
+
vi.advanceTimersByTime(10_000)
|
|
522
|
+
})
|
|
523
|
+
// One adjudication per 10s window, then the skin's own result animation.
|
|
524
|
+
expect(api.workTick).toHaveBeenCalledTimes(1)
|
|
525
|
+
expect(setTrack).toHaveBeenCalledWith('skin-success')
|
|
526
|
+
await act(async () => {
|
|
527
|
+
vi.advanceTimersByTime(1300)
|
|
528
|
+
})
|
|
529
|
+
// The result holds for resultMs, then the skin work loop resumes ...
|
|
530
|
+
expect(setTrack).toHaveBeenLastCalledWith('skin-work')
|
|
531
|
+
await act(async () => {
|
|
532
|
+
vi.advanceTimersByTime(10_000)
|
|
533
|
+
})
|
|
534
|
+
// ... and the next 10s round adjudicates again.
|
|
535
|
+
expect(api.workTick).toHaveBeenCalledTimes(2)
|
|
536
|
+
})
|
|
537
|
+
|
|
375
538
|
it('holds the sleep track and wakes on drag', async () => {
|
|
376
539
|
const h = harness(gameplayView({ mode: 'sleep' }))
|
|
377
540
|
expect(h.setTrack).toHaveBeenCalledWith('sleep')
|
|
@@ -25,6 +25,11 @@ export interface GameplayApi {
|
|
|
25
25
|
setMode: (mode: 'work' | 'sleep' | null) => Promise<PetGameplayVerbResult>
|
|
26
26
|
workTick: () => Promise<PetGameplayVerbResult>
|
|
27
27
|
buy: (item: string) => Promise<PetGameplayVerbResult>
|
|
28
|
+
/**
|
|
29
|
+
* Persist the selected skin for the current pet (host-authoritative;
|
|
30
|
+
* `undefined` restores the pet's default look).
|
|
31
|
+
*/
|
|
32
|
+
setSkin: (skin: string | undefined) => Promise<{ ok: boolean; error?: string }>
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -37,6 +42,13 @@ export interface GameplayBus {
|
|
|
37
42
|
setTrack?: (track?: string) => void
|
|
38
43
|
/** Swap the pet's base idle track (skin switch); undefined restores default. */
|
|
39
44
|
setIdleTrack?: (track?: string) => void
|
|
45
|
+
/**
|
|
46
|
+
* The base idle track the HUD wants right now, latched on the bus so a
|
|
47
|
+
* renderer that registers late (or remounts: hidden/summoned, StrictMode's
|
|
48
|
+
* double mount) still applies the restored skin instead of snapping back to
|
|
49
|
+
* the default look. Mutating it never requires a re-render.
|
|
50
|
+
*/
|
|
51
|
+
idleTrack?: string
|
|
40
52
|
tap?: (fx: number, fy: number) => void
|
|
41
53
|
/**
|
|
42
54
|
* Card open/close request from the chrome (the hover panel's 玩法 action):
|
|
@@ -70,6 +82,8 @@ export function GameplayHud(props: {
|
|
|
70
82
|
const def = definition.gameplay
|
|
71
83
|
const view = ui.snapshot?.gameplay
|
|
72
84
|
const phase = ui.snapshot?.phase ?? 'idle'
|
|
85
|
+
// Host-persisted skin selection for this pet (undefined = default look).
|
|
86
|
+
const persistedSkin = ui.snapshot?.skin
|
|
73
87
|
|
|
74
88
|
const [open, setOpen] = useState(false)
|
|
75
89
|
const [page, setPage] = useState<HudPage>('root')
|
|
@@ -284,24 +298,32 @@ export function GameplayHud(props: {
|
|
|
284
298
|
|
|
285
299
|
// Work loop: hold the work track, adjudicate one round per tick, play the
|
|
286
300
|
// result track for its hold window, then resume. Leaving the mode
|
|
287
|
-
// releases the override so the phase mapping takes over.
|
|
301
|
+
// releases the override so the phase mapping takes over. A skin that
|
|
302
|
+
// declares gameplayTracks for the work states plays its own art instead.
|
|
288
303
|
useEffect(() => {
|
|
289
304
|
const work = def?.work
|
|
290
305
|
if (def === undefined || work === undefined || view?.mode !== 'work') return undefined
|
|
291
|
-
|
|
306
|
+
const skinGameplay = definition.frames2d?.skins?.find(skin => skin.id === skinIdRef.current)?.gameplayTracks
|
|
307
|
+
/** The state's track, swapped for the skin's override when it declares one. */
|
|
308
|
+
const trackOf = (state: string): string => skinGameplay?.[state] ?? state
|
|
309
|
+
bus.setTrack?.(trackOf(work.state))
|
|
292
310
|
let resultTimer = 0
|
|
293
311
|
const timer = window.setInterval(() => {
|
|
294
312
|
if (busyRef.current) return
|
|
295
313
|
busyRef.current = true
|
|
296
314
|
void api.workTick().then((result) => {
|
|
297
315
|
busyRef.current = false
|
|
316
|
+
// Leaving work mode while the adjudication is in flight drops the late
|
|
317
|
+
// result: writing it back would show work rewards and play the result
|
|
318
|
+
// track for a mode the user has already left (#1495).
|
|
319
|
+
if (modeRef.current !== 'work') return
|
|
298
320
|
applyResult(result)
|
|
299
321
|
if (result.ok !== true || result.outcome === undefined) return
|
|
300
|
-
const resultTrack = result.outcome === 'success' ? work.successState : work.failState
|
|
322
|
+
const resultTrack = trackOf(result.outcome === 'success' ? work.successState : work.failState)
|
|
301
323
|
const hold = result.outcome === 'success' ? work.resultMs?.success ?? 1300 : work.resultMs?.fail ?? 1900
|
|
302
324
|
bus.setTrack?.(resultTrack)
|
|
303
325
|
resultTimer = window.setTimeout(() => {
|
|
304
|
-
if (modeRef.current === 'work') bus.setTrack?.(work.state)
|
|
326
|
+
if (modeRef.current === 'work') bus.setTrack?.(trackOf(work.state))
|
|
305
327
|
}, hold)
|
|
306
328
|
}, () => { busyRef.current = false })
|
|
307
329
|
}, work.tickMs)
|
|
@@ -310,8 +332,8 @@ export function GameplayHud(props: {
|
|
|
310
332
|
window.clearTimeout(resultTimer)
|
|
311
333
|
bus.setTrack?.(undefined)
|
|
312
334
|
}
|
|
313
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- the loop keys on the mode
|
|
314
|
-
}, [definition.id, def, view?.mode])
|
|
335
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- the loop keys on the mode and the selected skin
|
|
336
|
+
}, [definition.id, def, view?.mode, skinId])
|
|
315
337
|
|
|
316
338
|
// Sleep loop: hold the sleep track; restore is host-side (lazy settle).
|
|
317
339
|
// While a skin with a gameplayTracks.sleep override is selected, the skin's
|
|
@@ -352,10 +374,44 @@ export function GameplayHud(props: {
|
|
|
352
374
|
void api.setMode(next).then(applyResult, () => undefined)
|
|
353
375
|
}
|
|
354
376
|
|
|
377
|
+
// Skin selection is persisted host-side (per pet): re-seed the menu from
|
|
378
|
+
// every fresh state view, so a page reload or client restart keeps the last
|
|
379
|
+
// choice instead of snapping back to the default look.
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
setSkinId(persistedSkin)
|
|
382
|
+
}, [definition.id, persistedSkin])
|
|
383
|
+
|
|
384
|
+
// Push the resolved base idle track into the renderer whenever the pet or
|
|
385
|
+
// the selection changes. The value is latched on the bus first: the visual
|
|
386
|
+
// may register later, or remount later (hidden/summoned), and reads the
|
|
387
|
+
// latch back on activation so a restored skin never falls back to default.
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
const skin = definition.frames2d?.skins?.find(candidate => candidate.id === skinId)
|
|
390
|
+
bus.idleTrack = skin?.idleTrack
|
|
391
|
+
bus.setIdleTrack?.(skin?.idleTrack)
|
|
392
|
+
// persistedSkin rides the deps so the host's value also re-pushes on
|
|
393
|
+
// arrival (a renderer that mounted early still converges).
|
|
394
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one push per selection
|
|
395
|
+
}, [definition.id, skinId, persistedSkin])
|
|
396
|
+
|
|
355
397
|
const skins = definition.frames2d?.skins
|
|
398
|
+
/** The base idle track one skin id resolves to (undefined = default look). */
|
|
399
|
+
const skinTrackOf = (id: string | undefined): string | undefined =>
|
|
400
|
+
id === undefined ? undefined : definition.frames2d?.skins?.find(candidate => candidate.id === id)?.idleTrack
|
|
401
|
+
|
|
356
402
|
const selectSkin = (skin: PetSkinDefinition | undefined): void => {
|
|
357
403
|
setSkinId(skin?.id)
|
|
358
404
|
bus.setIdleTrack?.(skin?.idleTrack)
|
|
405
|
+
// The host owns the choice: a refusal (unknown skin) restores both the
|
|
406
|
+
// menu highlight and the renderer to the value it still serves.
|
|
407
|
+
const restore = (): void => {
|
|
408
|
+
setSkinId(persistedSkin)
|
|
409
|
+
bus.setIdleTrack?.(skinTrackOf(persistedSkin))
|
|
410
|
+
}
|
|
411
|
+
void api.setSkin(skin?.id).then((result) => {
|
|
412
|
+
if (result.ok) return
|
|
413
|
+
restore()
|
|
414
|
+
}, restore)
|
|
359
415
|
}
|
|
360
416
|
|
|
361
417
|
return (
|
package/src/client/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type { PetDefinition } from '../registry.ts'
|
|
|
32
32
|
import { createElement } from 'react'
|
|
33
33
|
import { createRoot } from 'react-dom/client'
|
|
34
34
|
import { createPetStore, type PetStoreInstance } from './pet-store.ts'
|
|
35
|
+
import { createWorkTickGate } from './work-tick-gate.ts'
|
|
35
36
|
import { PetDockEntry, type PetInjected } from './PetDockEntry.tsx'
|
|
36
37
|
import { defaultPetRendererRegistry } from './renderers/registry.ts'
|
|
37
38
|
import { live2dRenderer } from './renderers/live2d.ts'
|
|
@@ -51,6 +52,7 @@ interface PetHttpApi {
|
|
|
51
52
|
setConfig(patch: Partial<PetDisplayConfig>): Promise<{ ok: true; display: PetDisplayConfig }>
|
|
52
53
|
setName(name: string): Promise<{ ok: true; name: string } | { ok: false; error: string }>
|
|
53
54
|
setPet(petId: string): Promise<{ ok: true; petId: string } | { ok: false; error: string }>
|
|
55
|
+
setSkin(skin?: string): Promise<{ ok: boolean; error?: string; skin?: string }>
|
|
54
56
|
gameplayTouch(zone?: string): Promise<PetGameplayVerbResult>
|
|
55
57
|
gameplaySetMode(mode: 'work' | 'sleep' | null): Promise<PetGameplayVerbResult>
|
|
56
58
|
gameplayWorkTick(): Promise<PetGameplayVerbResult>
|
|
@@ -82,6 +84,7 @@ const petApi: PetHttpApi = {
|
|
|
82
84
|
setConfig: (patch) => petFetch('/api/pet/set-config', patch),
|
|
83
85
|
setName: (name) => petFetch('/api/pet/set-name', { name }),
|
|
84
86
|
setPet: (petId) => petFetch('/api/pet/set-pet', { petId }),
|
|
87
|
+
setSkin: (skin) => petFetch('/api/pet/set-skin', skin === undefined ? {} : { skin }),
|
|
85
88
|
gameplayTouch: (zone) => petFetch('/api/pet/gameplay/touch', zone === undefined ? {} : { zone }),
|
|
86
89
|
gameplaySetMode: (mode) => petFetch('/api/pet/gameplay/mode', { mode }),
|
|
87
90
|
gameplayWorkTick: () => petFetch('/api/pet/gameplay/work-tick', {}),
|
|
@@ -123,16 +126,19 @@ declare module '@deepseek-ai/cordis' {
|
|
|
123
126
|
* @param ctx - client root context.
|
|
124
127
|
*/
|
|
125
128
|
|
|
129
|
+
/** The page-wide work-tick gate; its window follows the active pet's cadence. */
|
|
130
|
+
const workTickGate = createWorkTickGate()
|
|
131
|
+
|
|
126
132
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* call re-rolls, re-grants treats and re-plays the success/fail track, so
|
|
131
|
-
* the outcome appears to play several times per window. This shared marker
|
|
132
|
-
* accepts the first adjudication of a window and silently suppresses the
|
|
133
|
-
* duplicates that follow. Reset when (re-)entering work mode.
|
|
133
|
+
* The work cadence the active pet declares, when its registry entry is known.
|
|
134
|
+
* @param store - the pet store holding the host snapshot and the registry list.
|
|
135
|
+
* @returns the configured `gameplay.work.tickMs`, or undefined when unknown.
|
|
134
136
|
*/
|
|
135
|
-
|
|
137
|
+
function activeWorkTickMs(store: PetStoreInstance): number | undefined {
|
|
138
|
+
const state = store.getSnapshot()
|
|
139
|
+
const definition = state.pets.find((entry) => entry.id === state.snapshot?.pet.id)
|
|
140
|
+
return definition?.gameplay?.work?.tickMs
|
|
141
|
+
}
|
|
136
142
|
|
|
137
143
|
export function apply(ctx: ClientContext): void {
|
|
138
144
|
// Anonymous install heartbeat (docs/telemetry.md): one beat per browser per
|
|
@@ -371,18 +377,22 @@ export function apply(ctx: ClientContext): void {
|
|
|
371
377
|
},
|
|
372
378
|
gameplay: {
|
|
373
379
|
touch: (zone) => petApi.gameplayTouch(zone),
|
|
380
|
+
setSkin: (skin) => petApi.setSkin(skin).then((result) => {
|
|
381
|
+
if (result.ok) pollNow()
|
|
382
|
+
return result
|
|
383
|
+
}, () => ({ ok: false, error: 'transport' })),
|
|
384
|
+
|
|
374
385
|
setMode: async (mode) => {
|
|
375
|
-
if (mode === 'work')
|
|
386
|
+
if (mode === 'work') workTickGate.reset()
|
|
376
387
|
return petApi.gameplaySetMode(mode)
|
|
377
388
|
},
|
|
378
389
|
workTick: async () => {
|
|
379
390
|
// One adjudication per tick window, page-wide: suppress stale
|
|
380
|
-
// duplicate intervals (HMR) re-playing the result track.
|
|
381
|
-
|
|
382
|
-
if (
|
|
391
|
+
// duplicate intervals (HMR) re-playing the result track. The window
|
|
392
|
+
// is the pet's own cadence, so a shorter tickMs is not downgraded.
|
|
393
|
+
if (!workTickGate.allow(activeWorkTickMs(petStore))) {
|
|
383
394
|
return { ok: true } as PetGameplayVerbResult
|
|
384
395
|
}
|
|
385
|
-
lastWorkTickAt = now
|
|
386
396
|
return petApi.gameplayWorkTick()
|
|
387
397
|
},
|
|
388
398
|
buy: (item) => petApi.gameplayBuy(item),
|