@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/src/gameplay.ts
CHANGED
|
@@ -572,6 +572,10 @@ export interface PetGameplayState {
|
|
|
572
572
|
mode: 'work' | 'sleep' | null
|
|
573
573
|
/** Epoch ms of the last lazy settle. */
|
|
574
574
|
settledAt: number
|
|
575
|
+
/** Accumulated remainder ms towards the next passive income tick. */
|
|
576
|
+
incomeCarryMs?: number
|
|
577
|
+
/** Accumulated remainder ms towards the next sleep restore tick. */
|
|
578
|
+
restoreCarryMs?: number
|
|
575
579
|
}
|
|
576
580
|
|
|
577
581
|
/** Fresh state for one pet: stats at their initial (default max), no currency. */
|
|
@@ -624,7 +628,10 @@ export function settleGameplay(
|
|
|
624
628
|
}
|
|
625
629
|
}
|
|
626
630
|
if (manifest.passiveIncome !== undefined) {
|
|
627
|
-
const
|
|
631
|
+
const incomeElapsed = elapsedMs + (state.incomeCarryMs ?? 0)
|
|
632
|
+
const interval = manifest.passiveIncome.intervalMs
|
|
633
|
+
const ticks = Math.floor(incomeElapsed / interval)
|
|
634
|
+
state.incomeCarryMs = incomeElapsed % interval
|
|
628
635
|
if (ticks > 0) {
|
|
629
636
|
const currency = manifest.passiveIncome.currency
|
|
630
637
|
state.currencies[currency] = (state.currencies[currency] ?? 0) + ticks * manifest.passiveIncome.amount
|
|
@@ -632,12 +639,17 @@ export function settleGameplay(
|
|
|
632
639
|
}
|
|
633
640
|
}
|
|
634
641
|
if (state.mode === 'sleep' && manifest.sleep !== undefined) {
|
|
635
|
-
const
|
|
642
|
+
const restoreElapsed = elapsedMs + (state.restoreCarryMs ?? 0)
|
|
643
|
+
const interval = manifest.sleep.restore.intervalMs
|
|
644
|
+
const ticks = Math.floor(restoreElapsed / interval)
|
|
645
|
+
state.restoreCarryMs = restoreElapsed % interval
|
|
636
646
|
if (ticks > 0) {
|
|
637
647
|
const stat = manifest.sleep.restore.stat
|
|
638
648
|
state.stats[stat] = (state.stats[stat] ?? 0) + ticks * manifest.sleep.restore.amount
|
|
639
649
|
changed = true
|
|
640
650
|
}
|
|
651
|
+
} else {
|
|
652
|
+
state.restoreCarryMs = 0
|
|
641
653
|
}
|
|
642
654
|
state.settledAt = now
|
|
643
655
|
clampGameplay(state, manifest)
|
package/src/ledger.test.ts
CHANGED
|
@@ -143,6 +143,25 @@ describe('PetLedger', () => {
|
|
|
143
143
|
expect(ledger.takeDirty()).toBe(true)
|
|
144
144
|
})
|
|
145
145
|
|
|
146
|
+
it('stores and clears the per-pet skin selection', () => {
|
|
147
|
+
const ledger = new PetLedger(emptyPersist())
|
|
148
|
+
expect(ledger.petSkin('jyn')).toBeUndefined()
|
|
149
|
+
ledger.setPetSkin('jyn', 'bingjing-gongzhu')
|
|
150
|
+
expect(ledger.petSkin('jyn')).toBe('bingjing-gongzhu')
|
|
151
|
+
expect(ledger.snapshot.skins).toEqual({ jyn: 'bingjing-gongzhu' })
|
|
152
|
+
expect(ledger.takeDirty()).toBe(true)
|
|
153
|
+
// Same value again: nothing changed, so no extra persist.
|
|
154
|
+
ledger.setPetSkin('jyn', 'bingjing-gongzhu')
|
|
155
|
+
expect(ledger.takeDirty()).toBe(false)
|
|
156
|
+
// undefined clears the entry back to the pet's default look.
|
|
157
|
+
ledger.setPetSkin('jyn', undefined)
|
|
158
|
+
expect(ledger.petSkin('jyn')).toBeUndefined()
|
|
159
|
+
expect(ledger.snapshot.skins).toEqual({})
|
|
160
|
+
expect(ledger.takeDirty()).toBe(true)
|
|
161
|
+
ledger.setPetSkin('jyn', undefined)
|
|
162
|
+
expect(ledger.takeDirty()).toBe(false)
|
|
163
|
+
})
|
|
164
|
+
|
|
146
165
|
it('exposes the treat stock cap and display/pet/name setters', () => {
|
|
147
166
|
const ledger = new PetLedger(emptyPersist())
|
|
148
167
|
expect(ledger.treatMax).toBe(defaultTreatConfig.maxTreats)
|
package/src/ledger.ts
CHANGED
|
@@ -124,6 +124,30 @@ export class PetLedger {
|
|
|
124
124
|
this.dirty = true
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Select one pet's frames2d skin; `undefined` clears the choice back to the
|
|
129
|
+
* pet's default look. Manifest validation stays a caller concern, exactly
|
|
130
|
+
* like setPetName's length check.
|
|
131
|
+
*/
|
|
132
|
+
setPetSkin(petId: string, skinId: string | undefined): void {
|
|
133
|
+
const skins = this.current.skins
|
|
134
|
+
if (skinId === undefined) {
|
|
135
|
+
if (skins[petId] === undefined) return
|
|
136
|
+
const next = { ...skins }
|
|
137
|
+
delete next[petId]
|
|
138
|
+
this.current = { ...this.current, skins: next }
|
|
139
|
+
} else {
|
|
140
|
+
if (skins[petId] === skinId) return
|
|
141
|
+
this.current = { ...this.current, skins: { ...skins, [petId]: skinId } }
|
|
142
|
+
}
|
|
143
|
+
this.dirty = true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The persisted skin id for one pet (undefined = the pet's default look). */
|
|
147
|
+
petSkin(petId: string): string | undefined {
|
|
148
|
+
return this.current.skins[petId]
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
/**
|
|
128
152
|
* Swap the reaction pools to another pet's custom remarks (called on pet
|
|
129
153
|
* selection). Slots the pet does not declare fall back to voice packs or built-ins.
|
package/src/persist.test.ts
CHANGED
|
@@ -49,6 +49,7 @@ describe('loadPetPersist', () => {
|
|
|
49
49
|
const data = {
|
|
50
50
|
petId: 'otter',
|
|
51
51
|
names: { otter: '泡泡', 'whale-girl': '鲸鱼娘' },
|
|
52
|
+
skins: { otter: 'lanhainishang' },
|
|
52
53
|
affinity: { ...emptyAffinity(), points: 42, pets: 3, feeds: 1, turns: 10 },
|
|
53
54
|
treats: { ...emptyTreatLedger(), treats: 7, lastTreatGrantAt: 1234, turnsAtLastTreatGrant: 9 },
|
|
54
55
|
display: { visible: false, size: 200, right: 10, bottom: 40 },
|
|
@@ -111,6 +112,18 @@ describe('loadPetPersist', () => {
|
|
|
111
112
|
}
|
|
112
113
|
})
|
|
113
114
|
|
|
115
|
+
it('sanitizes the per-pet skin map', () => {
|
|
116
|
+
const dir = tempDir()
|
|
117
|
+
try {
|
|
118
|
+
writeFileSync(join(dir, 'pet.json'), JSON.stringify({
|
|
119
|
+
skins: { otter: ' lanhainishang ', blank: ' ', numeric: 7, '': 'x' },
|
|
120
|
+
}), 'utf8')
|
|
121
|
+
expect(loadPetPersist(dir).skins).toEqual({ otter: 'lanhainishang' })
|
|
122
|
+
} finally {
|
|
123
|
+
rmSync(dir, { recursive: true, force: true })
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
114
127
|
it('clamps out-of-range and non-numeric fields', () => {
|
|
115
128
|
const dir = tempDir()
|
|
116
129
|
try {
|
package/src/persist.ts
CHANGED
|
@@ -48,6 +48,12 @@ export interface PetPersist {
|
|
|
48
48
|
* to its manifest displayName, so only user renames are stored here.
|
|
49
49
|
*/
|
|
50
50
|
names: Record<string, string>
|
|
51
|
+
/**
|
|
52
|
+
* Per-pet selected frames2d skin id (keyed by pet id). Skin ids are manifest
|
|
53
|
+
* data, so a stale entry (skin renamed or removed, pet swapped) is ignored
|
|
54
|
+
* when the state view is built instead of pinning an unresolvable track.
|
|
55
|
+
*/
|
|
56
|
+
skins: Record<string, string>
|
|
51
57
|
affinity: AffinityState
|
|
52
58
|
/** Treat (小鱼干) stock ledger. */
|
|
53
59
|
treats: TreatLedger
|
|
@@ -63,6 +69,7 @@ export function emptyPersist(): PetPersist {
|
|
|
63
69
|
return {
|
|
64
70
|
petId: DEFAULT_PET_ID,
|
|
65
71
|
names: {},
|
|
72
|
+
skins: {},
|
|
66
73
|
affinity: emptyAffinity(),
|
|
67
74
|
treats: emptyTreatLedger(),
|
|
68
75
|
display: { ...defaultDisplayConfig },
|
|
@@ -100,6 +107,19 @@ function loadPetNames(parsed: PetPersistDocument): Record<string, string> {
|
|
|
100
107
|
return names
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
/** Sanitize the per-pet skin selection map (string keys, non-empty trimmed values). */
|
|
111
|
+
function loadPetSkins(parsed: PetPersistDocument): Record<string, string> {
|
|
112
|
+
const skins: Record<string, string> = {}
|
|
113
|
+
if (typeof parsed.skins !== 'object' || parsed.skins === null) return skins
|
|
114
|
+
for (const [id, value] of Object.entries(parsed.skins as Record<string, unknown>)) {
|
|
115
|
+
if (id === '' || typeof value !== 'string') continue
|
|
116
|
+
const skin = value.trim()
|
|
117
|
+
if (skin === '') continue
|
|
118
|
+
skins[id] = skin
|
|
119
|
+
}
|
|
120
|
+
return skins
|
|
121
|
+
}
|
|
122
|
+
|
|
103
123
|
/** Clamp one count/score into [0, max]. */
|
|
104
124
|
function clamp(value: number, max: number): number {
|
|
105
125
|
return Math.min(max, Math.max(0, value))
|
|
@@ -130,12 +150,19 @@ function loadGameplay(parsed: PetPersistDocument): Record<string, PetGameplaySta
|
|
|
130
150
|
currencies[key] = Math.min(GAMEPLAY_LOAD_CURRENCY_CAP, Math.max(0, Math.floor(value)))
|
|
131
151
|
}
|
|
132
152
|
}
|
|
133
|
-
|
|
153
|
+
const item: PetGameplayState = {
|
|
134
154
|
stats,
|
|
135
155
|
currencies,
|
|
136
156
|
mode: record.mode === 'work' || record.mode === 'sleep' ? record.mode : null,
|
|
137
157
|
settledAt: clamp(finiteNum(record.settledAt, 0), Number.MAX_SAFE_INTEGER),
|
|
138
158
|
}
|
|
159
|
+
if (typeof record.incomeCarryMs === 'number' && Number.isFinite(record.incomeCarryMs)) {
|
|
160
|
+
item.incomeCarryMs = Math.max(0, record.incomeCarryMs)
|
|
161
|
+
}
|
|
162
|
+
if (typeof record.restoreCarryMs === 'number' && Number.isFinite(record.restoreCarryMs)) {
|
|
163
|
+
item.restoreCarryMs = Math.max(0, record.restoreCarryMs)
|
|
164
|
+
}
|
|
165
|
+
result[petId] = item
|
|
139
166
|
}
|
|
140
167
|
return result
|
|
141
168
|
}
|
|
@@ -185,6 +212,7 @@ export function loadPetPersist(dir: string = petHomeDir()): PetPersist {
|
|
|
185
212
|
return {
|
|
186
213
|
petId,
|
|
187
214
|
names,
|
|
215
|
+
skins: loadPetSkins(parsed),
|
|
188
216
|
affinity,
|
|
189
217
|
treats,
|
|
190
218
|
display,
|
package/src/routes.ts
CHANGED
|
@@ -603,6 +603,11 @@ export function makePetRoutes(deps: { service: PetService; ctx: Context; assetCa
|
|
|
603
603
|
if (typeof name !== 'string') return Promise.reject(new Error('invalid-name'))
|
|
604
604
|
return service.setName(name)
|
|
605
605
|
}),
|
|
606
|
+
postRoute(ctx, PET_API_PREFIX + '/set-skin', (body) => {
|
|
607
|
+
const skin = body.skin
|
|
608
|
+
if (skin !== undefined && typeof skin !== 'string') return Promise.reject(new Error('invalid-skin'))
|
|
609
|
+
return service.setSkin(skin === undefined || skin === '' ? undefined : skin)
|
|
610
|
+
}),
|
|
606
611
|
postRoute(ctx, PET_API_PREFIX + '/set-pet', (body) => {
|
|
607
612
|
const petId = body.petId
|
|
608
613
|
if (typeof petId !== 'string') return Promise.reject(new Error('invalid-pet'))
|
package/src/service.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
16
|
+
import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent'
|
|
16
17
|
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
17
18
|
import type { AffinityConfig, PetAffinityView, PetInteraction } from './affinity.ts'
|
|
18
19
|
import { announcementFresh, parseAnnouncement, type PetAnnouncement } from './announce.ts'
|
|
@@ -20,6 +21,7 @@ import type { TreatConfig } from './treats.ts'
|
|
|
20
21
|
import {
|
|
21
22
|
emptyProjectionRuntime,
|
|
22
23
|
isActivityPhase,
|
|
24
|
+
projectAssistantStreamFrame,
|
|
23
25
|
projectOfficialEvent,
|
|
24
26
|
type ActivityStatusEventLike,
|
|
25
27
|
type ProjectionRuntime,
|
|
@@ -173,6 +175,12 @@ export interface PetStateView {
|
|
|
173
175
|
}
|
|
174
176
|
/** The selected pet's display name (user rename or manifest default). */
|
|
175
177
|
name: string
|
|
178
|
+
/**
|
|
179
|
+
* The selected frames2d skin id for this pet (absent = the pet's default
|
|
180
|
+
* look). Persisted host-side, so a page reload or client restart restores
|
|
181
|
+
* the user's last choice instead of falling back to the default look.
|
|
182
|
+
*/
|
|
183
|
+
skin?: string
|
|
176
184
|
/** Treat (小鱼干) stock snapshot. */
|
|
177
185
|
treats: {
|
|
178
186
|
/** Stocked treats now. */
|
|
@@ -466,6 +474,15 @@ export class PetService extends Service {
|
|
|
466
474
|
this.rewardTurn(String(session.id), transition.completedTurn)
|
|
467
475
|
}
|
|
468
476
|
}),
|
|
477
|
+
this.ctx.on('agent/assistant-stream', ({ agent, frame }: { agent: { session: Session }; frame: AssistantStreamFrame }) => {
|
|
478
|
+
const session = agent.session
|
|
479
|
+
const runtime = this.activityOf(session).runtime
|
|
480
|
+
const transition = projectAssistantStreamFrame(frame, runtime)
|
|
481
|
+
if (transition === undefined) return
|
|
482
|
+
runtime.officialEventsSeen = true
|
|
483
|
+
this.officialEventSessions.add(session)
|
|
484
|
+
this.applyActivity(session, transition.input, transition.whisper)
|
|
485
|
+
}),
|
|
469
486
|
this.ctx.on('session/disposed', (session: Session) => {
|
|
470
487
|
this.ledger.forgetSession(String(session.id))
|
|
471
488
|
this.officialEventSessions.delete(session)
|
|
@@ -709,6 +726,36 @@ export class PetService extends Service {
|
|
|
709
726
|
return { ok: true, display: this.ledger.snapshot.display }
|
|
710
727
|
}
|
|
711
728
|
|
|
729
|
+
/**
|
|
730
|
+
* The persisted skin for one entry, when the manifest still declares it: a
|
|
731
|
+
* stale id (skin removed from the manifest, pet swapped) reads as "default"
|
|
732
|
+
* rather than pinning a track the browser half cannot resolve.
|
|
733
|
+
*/
|
|
734
|
+
private persistedSkin(entry: NonNullable<PetRegistry['entries'][number]>): string | undefined {
|
|
735
|
+
const stored = this.ledger.petSkin(entry.id)
|
|
736
|
+
if (stored === undefined) return undefined
|
|
737
|
+
return entry.frames2d?.skins?.some(skin => skin.id === stored) === true ? stored : undefined
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* RPC: select the current pet's frames2d skin (`undefined` restores the
|
|
742
|
+
* pet's default look). The choice is stored per pet, so every later state
|
|
743
|
+
* view (reload, client restart, pet re-selection) serves it back.
|
|
744
|
+
*/
|
|
745
|
+
async setSkin(skin: string | undefined): Promise<{ ok: true; skin?: string } | { ok: false; error: string }> {
|
|
746
|
+
const entry = this.activeEntry()
|
|
747
|
+
const declared = entry.frames2d?.skins ?? []
|
|
748
|
+
if (skin === undefined) {
|
|
749
|
+
this.ledger.setPetSkin(entry.id, undefined)
|
|
750
|
+
this.flush()
|
|
751
|
+
return { ok: true }
|
|
752
|
+
}
|
|
753
|
+
if (!declared.some(candidate => candidate.id === skin)) return { ok: false, error: 'unknown-skin' }
|
|
754
|
+
this.ledger.setPetSkin(entry.id, skin)
|
|
755
|
+
this.flush()
|
|
756
|
+
return { ok: true, skin }
|
|
757
|
+
}
|
|
758
|
+
|
|
712
759
|
/** RPC: update display config (size / position). Values are clamped to whole pixels. */
|
|
713
760
|
async setConfig(patch: Partial<PetDisplayConfig>): Promise<{ ok: true; display: PetDisplayConfig }> {
|
|
714
761
|
const next = { ...this.ledger.snapshot.display, ...patch }
|
|
@@ -839,6 +886,7 @@ export class PetService extends Service {
|
|
|
839
886
|
const announcement = this.announcement !== undefined && announcementFresh(this.announcement, Date.now())
|
|
840
887
|
? this.announcement
|
|
841
888
|
: undefined
|
|
889
|
+
const skin = this.persistedSkin(entry)
|
|
842
890
|
return {
|
|
843
891
|
animation: snapshot.animation,
|
|
844
892
|
...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
|
|
@@ -855,6 +903,7 @@ export class PetService extends Service {
|
|
|
855
903
|
description: entry.description,
|
|
856
904
|
},
|
|
857
905
|
name: this.petName(),
|
|
906
|
+
...(skin === undefined ? {} : { skin }),
|
|
858
907
|
treats: {
|
|
859
908
|
stocked: this.ledger.snapshot.treats.treats,
|
|
860
909
|
max: this.ledger.treatMax,
|