@linxin666/dsh-pet 0.2.7 → 0.2.9
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/LICENSE +202 -29
- package/README.i18n.yaml +2 -2
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/assets/whale/pet.json +54 -54
- package/assets/whale-refined/pet.json +96 -1
- package/lib/client.js +84 -14
- package/lib/client.js.map +1 -1
- package/lib/index.js +80 -59
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +23 -17
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +50 -3
- package/lib/types/client/ui-teardown.d.ts +29 -0
- package/lib/types/client/ui-teardown.d.ts.map +1 -0
- package/lib/types/client/ui-teardown.js +41 -0
- package/lib/types/registry.d.ts +5 -1
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +14 -10
- package/lib/types/service.d.ts +8 -0
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +19 -1
- package/package.json +12 -9
- package/src/client/PetSprite.test.tsx +168 -26
- package/src/client/PetSprite.tsx +21 -15
- package/src/client/index.test.tsx +118 -11
- package/src/client/index.ts +51 -3
- package/src/client/ui-teardown.ts +47 -0
- package/src/registry.test.ts +14 -0
- package/src/registry.ts +14 -10
- package/src/service.ts +19 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-bundle-instance teardown slot for the page-global pet root
|
|
3
|
+
* (issue #785).
|
|
4
|
+
*
|
|
5
|
+
* A client bundle swap (HMR rebuilt frame, plugin update, duplicate
|
|
6
|
+
* injection) runs a new apply body while the previous instance's fiber
|
|
7
|
+
* may still be draining. Module state does not survive the swap, so a
|
|
8
|
+
* closure guard only sees one apply body and the previous instance's
|
|
9
|
+
* container keeps sitting on document.body: the page shows two pets
|
|
10
|
+
* until a full refresh. The slot rides globalThis (which does survive)
|
|
11
|
+
* so a re-apply can find the previous instance and unmount its React
|
|
12
|
+
* root cleanly before mounting its own; the previous fiber's later
|
|
13
|
+
* disposal stays a no-op through the idempotent teardowns.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const SLOT = Symbol.for('dsh-pet.client-ui-teardown')
|
|
17
|
+
|
|
18
|
+
interface TeardownSlot {
|
|
19
|
+
[SLOT]?: (() => void) | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Claim the page-global pet UI slot with the current instance's teardown
|
|
24
|
+
* (React root unmount + container removal + poll stop).
|
|
25
|
+
* @param teardown - what a later instance runs to take the slot over.
|
|
26
|
+
* @returns a disposer that clears the slot when the current instance's
|
|
27
|
+
* UI is torn down (settings toggle, takeover, or fiber disposal).
|
|
28
|
+
*/
|
|
29
|
+
export function registerPetUiTeardown(teardown: () => void): () => void {
|
|
30
|
+
const slot = globalThis as TeardownSlot
|
|
31
|
+
slot[SLOT] = teardown
|
|
32
|
+
return () => {
|
|
33
|
+
if (slot[SLOT] === teardown) slot[SLOT] = undefined
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run the previous instance's teardown if one is registered, so the
|
|
39
|
+
* re-applying instance becomes the sole owner of the page-global pet
|
|
40
|
+
* root. No-op when the previous fiber already tore down cleanly.
|
|
41
|
+
*/
|
|
42
|
+
export function takeoverPetUiTeardown(): void {
|
|
43
|
+
const slot = globalThis as TeardownSlot
|
|
44
|
+
const teardown = slot[SLOT]
|
|
45
|
+
slot[SLOT] = undefined
|
|
46
|
+
teardown?.()
|
|
47
|
+
}
|
package/src/registry.test.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { join } from 'node:path'
|
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_FRAME_COUNTS,
|
|
7
7
|
DEFAULT_PET_CELL,
|
|
8
|
+
DEFAULT_TRACK_PATTERNS,
|
|
9
|
+
PET_ROW_ORDER,
|
|
8
10
|
PET_SCAN_JSON_CAP,
|
|
9
11
|
codexPetsDir,
|
|
10
12
|
loadPetRegistry,
|
|
@@ -168,6 +170,18 @@ describe('loadPetRegistry', () => {
|
|
|
168
170
|
readFileSync(petAtlasFile(registry.byId('whale-girl-refined')!)),
|
|
169
171
|
)).toBe(false)
|
|
170
172
|
expect(registry.defaultEntry().id).toBe('whale-girl')
|
|
173
|
+
// Both built-in whales and every override-free pet share the one slow
|
|
174
|
+
// global rhythm (user request: all pets were too fast at the legacy
|
|
175
|
+
// hatch-pet contract pace).
|
|
176
|
+
for (const track of PET_ROW_ORDER) {
|
|
177
|
+
expect(registry.byId('whale-girl')!.tracks[track].durations)
|
|
178
|
+
.toEqual(DEFAULT_TRACK_PATTERNS[track].durations)
|
|
179
|
+
expect(registry.byId('whale-girl-refined')!.tracks[track].durations)
|
|
180
|
+
.toEqual(DEFAULT_TRACK_PATTERNS[track].durations)
|
|
181
|
+
}
|
|
182
|
+
expect(DEFAULT_TRACK_PATTERNS.idle.durations[0]!).toBeGreaterThanOrEqual(500)
|
|
183
|
+
expect(DEFAULT_TRACK_PATTERNS['running-right'].durations[0]!).toBeGreaterThanOrEqual(300)
|
|
184
|
+
expect(DEFAULT_TRACK_PATTERNS.waving.durations[0]!).toBeGreaterThanOrEqual(450)
|
|
171
185
|
})
|
|
172
186
|
|
|
173
187
|
it('scans built-in assets, the custom pets dir, and composed extras with precedence', () => {
|
package/src/registry.ts
CHANGED
|
@@ -114,21 +114,25 @@ export interface PetTrackDef {
|
|
|
114
114
|
fallback?: PetAnimation
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Default per-track rhythm — the shared slow baseline every sprite2d pet
|
|
119
|
+
* plays unless its manifest overrides a track (user request: all pets were
|
|
120
|
+
* too fast at the legacy hatch-pet contract pace).
|
|
121
|
+
*/
|
|
118
122
|
export const DEFAULT_TRACK_PATTERNS: Record<PetAnimation, {
|
|
119
123
|
durations: number[]
|
|
120
124
|
loop: boolean
|
|
121
125
|
fallback?: PetAnimation
|
|
122
126
|
}> = {
|
|
123
|
-
idle: { durations: [
|
|
124
|
-
'running-right': { durations: [
|
|
125
|
-
'running-left': { durations: [
|
|
126
|
-
waving: { durations: [
|
|
127
|
-
jumping: { durations: [
|
|
128
|
-
failed: { durations: [
|
|
129
|
-
waiting: { durations: [
|
|
130
|
-
running: { durations: [
|
|
131
|
-
review: { durations: [
|
|
127
|
+
idle: { durations: [500, 500, 600, 500, 500, 600], loop: true },
|
|
128
|
+
'running-right': { durations: [300, 300, 300, 300, 300, 300, 300, 400], loop: true },
|
|
129
|
+
'running-left': { durations: [300, 300, 300, 300, 300, 300, 300, 400], loop: true },
|
|
130
|
+
waving: { durations: [450, 450, 450, 450], loop: true },
|
|
131
|
+
jumping: { durations: [400, 400, 400, 450, 450], loop: false, fallback: 'idle' },
|
|
132
|
+
failed: { durations: [550, 550, 550, 600, 650, 700, 550, 550], loop: false, fallback: 'idle' },
|
|
133
|
+
waiting: { durations: [550, 550, 600, 550, 550, 600], loop: true },
|
|
134
|
+
running: { durations: [330, 330, 330, 330, 330, 400], loop: true },
|
|
135
|
+
review: { durations: [650, 650, 650, 650, 650, 650], loop: true },
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
/** Manifest shape a pet directory (or 'PetConfig.pets' entry) declares. */
|
package/src/service.ts
CHANGED
|
@@ -234,6 +234,12 @@ export class PetService extends Service {
|
|
|
234
234
|
* disposed sessions are removed by the 'session/disposed' listener.
|
|
235
235
|
*/
|
|
236
236
|
private readonly sessionActivity = new Map<Session, SessionActivity>()
|
|
237
|
+
/**
|
|
238
|
+
* Sessions whose reward source is the official event stream. This metadata
|
|
239
|
+
* outlives transient visual resets so a derived legacy `done` cannot reward
|
|
240
|
+
* the same turn again after the pet is disabled and re-enabled.
|
|
241
|
+
*/
|
|
242
|
+
private readonly officialEventSessions = new WeakSet<Session>()
|
|
237
243
|
|
|
238
244
|
constructor(ctx: Context, config: PetConfig = {}) {
|
|
239
245
|
super(ctx, 'pet')
|
|
@@ -356,6 +362,7 @@ export class PetService extends Service {
|
|
|
356
362
|
setEnabled(enabled: boolean): void {
|
|
357
363
|
this.enabled = enabled
|
|
358
364
|
this.syncActivity()
|
|
365
|
+
if (!enabled) this.resetActivity()
|
|
359
366
|
}
|
|
360
367
|
|
|
361
368
|
private syncActivity(): void {
|
|
@@ -391,6 +398,7 @@ export class PetService extends Service {
|
|
|
391
398
|
const transition = projectOfficialEvent(event, runtime)
|
|
392
399
|
if (transition === undefined) return
|
|
393
400
|
runtime.officialEventsSeen = true
|
|
401
|
+
this.officialEventSessions.add(session)
|
|
394
402
|
this.applyActivity(session, transition.input, transition.whisper)
|
|
395
403
|
if (transition.completedTurn !== undefined) {
|
|
396
404
|
this.rewardTurn(String(session.id), transition.completedTurn)
|
|
@@ -398,6 +406,7 @@ export class PetService extends Service {
|
|
|
398
406
|
}),
|
|
399
407
|
this.ctx.on('session/disposed', (session: Session) => {
|
|
400
408
|
this.ledger.forgetSession(String(session.id))
|
|
409
|
+
this.officialEventSessions.delete(session)
|
|
401
410
|
this.sessionActivity.delete(session)
|
|
402
411
|
if (session !== this.displaySession) return
|
|
403
412
|
// The display session is gone: fall back to the most recent
|
|
@@ -418,12 +427,21 @@ export class PetService extends Service {
|
|
|
418
427
|
})()
|
|
419
428
|
}
|
|
420
429
|
|
|
430
|
+
/** Drop transient activity because terminal events missed while disabled cannot be replayed safely. */
|
|
431
|
+
private resetActivity(): void {
|
|
432
|
+
this.displaySession = undefined
|
|
433
|
+
this.sessionActivity.clear()
|
|
434
|
+
this.machine.onSessionDisposed()
|
|
435
|
+
}
|
|
436
|
+
|
|
421
437
|
/** Return the per-session activity record, creating it on first sight. */
|
|
422
438
|
private activityOf(session: Session): SessionActivity {
|
|
423
439
|
let activity = this.sessionActivity.get(session)
|
|
424
440
|
if (activity === undefined) {
|
|
441
|
+
const runtime = emptyProjectionRuntime(this.voicePools())
|
|
442
|
+
runtime.officialEventsSeen = this.officialEventSessions.has(session)
|
|
425
443
|
activity = {
|
|
426
|
-
runtime
|
|
444
|
+
runtime,
|
|
427
445
|
machine: new PetStateMachine(this.stateConfig),
|
|
428
446
|
}
|
|
429
447
|
this.sessionActivity.set(session, activity)
|