@linxin666/dsh-pet 0.3.20 → 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/lib/client.js +127 -26
- package/lib/client.js.map +1 -1
- package/lib/index.js +99 -4
- 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 +27 -18
- 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/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 +32 -0
- package/package.json +1 -1
- 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 +38 -6
- package/src/client/renderers/frames2d.ts +27 -19
- package/src/client/work-tick-gate.test.ts +53 -0
- package/src/client/work-tick-gate.ts +63 -0
- 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 +38 -0
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
|
@@ -175,6 +175,12 @@ export interface PetStateView {
|
|
|
175
175
|
}
|
|
176
176
|
/** The selected pet's display name (user rename or manifest default). */
|
|
177
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
|
|
178
184
|
/** Treat (小鱼干) stock snapshot. */
|
|
179
185
|
treats: {
|
|
180
186
|
/** Stocked treats now. */
|
|
@@ -720,6 +726,36 @@ export class PetService extends Service {
|
|
|
720
726
|
return { ok: true, display: this.ledger.snapshot.display }
|
|
721
727
|
}
|
|
722
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
|
+
|
|
723
759
|
/** RPC: update display config (size / position). Values are clamped to whole pixels. */
|
|
724
760
|
async setConfig(patch: Partial<PetDisplayConfig>): Promise<{ ok: true; display: PetDisplayConfig }> {
|
|
725
761
|
const next = { ...this.ledger.snapshot.display, ...patch }
|
|
@@ -850,6 +886,7 @@ export class PetService extends Service {
|
|
|
850
886
|
const announcement = this.announcement !== undefined && announcementFresh(this.announcement, Date.now())
|
|
851
887
|
? this.announcement
|
|
852
888
|
: undefined
|
|
889
|
+
const skin = this.persistedSkin(entry)
|
|
853
890
|
return {
|
|
854
891
|
animation: snapshot.animation,
|
|
855
892
|
...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
|
|
@@ -866,6 +903,7 @@ export class PetService extends Service {
|
|
|
866
903
|
description: entry.description,
|
|
867
904
|
},
|
|
868
905
|
name: this.petName(),
|
|
906
|
+
...(skin === undefined ? {} : { skin }),
|
|
869
907
|
treats: {
|
|
870
908
|
stocked: this.ledger.snapshot.treats.treats,
|
|
871
909
|
max: this.ledger.treatMax,
|