@linxin666/dsh-pet 0.3.24 → 0.4.2
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 +4 -3
- package/README.zh.md +4 -3
- package/icon.svg +3 -0
- package/lib/client.js +522 -194
- package/lib/client.js.map +1 -1
- package/lib/index.js +706 -422
- package/lib/live2d-vendor.js +96 -96
- package/lib/live2d-vendor.js.map +1 -1
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +1 -1
- package/lib/types/client/PetSettingsCard.d.ts +20 -9
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +116 -5
- package/lib/types/client/PetSprite.d.ts +6 -0
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +95 -0
- package/lib/types/client/PluginSettingsCard.d.ts +4 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/gameplay-hud.d.ts +10 -1
- package/lib/types/client/gameplay-hud.d.ts.map +1 -1
- package/lib/types/client/gameplay-hud.js +137 -23
- package/lib/types/client/index.d.ts +8 -5
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +61 -27
- package/lib/types/client/main-session.d.ts +33 -0
- package/lib/types/client/main-session.d.ts.map +1 -0
- package/lib/types/client/main-session.js +34 -0
- package/lib/types/client/settings-form.d.ts +17 -17
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +30 -27
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +5 -3
- package/lib/types/gameplay.d.ts +79 -5
- package/lib/types/gameplay.d.ts.map +1 -1
- package/lib/types/gameplay.js +212 -17
- package/lib/types/http.d.ts +18 -1
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/http.js +22 -1
- package/lib/types/index.d.ts +100 -29
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +113 -64
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +3 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +3 -1
- package/lib/types/service.d.ts +8 -3
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +26 -4
- package/package.json +46 -26
- package/src/client/PetDockEntry.tsx +1 -0
- package/src/client/PetSettingsCard.test.tsx +103 -0
- package/src/client/PetSettingsCard.tsx +129 -26
- package/src/client/PetSprite.tsx +96 -0
- package/src/client/PluginSettingsCard.tsx +7 -2
- package/src/client/gameplay-hud.test.tsx +106 -8
- package/src/client/gameplay-hud.tsx +143 -28
- package/src/client/index.test.tsx +37 -4
- package/src/client/index.ts +74 -34
- package/src/client/main-session.ts +43 -0
- package/src/client/settings-form.ts +34 -31
- package/src/event-projection.ts +5 -3
- package/src/gameplay.test.ts +138 -1
- package/src/gameplay.ts +253 -16
- package/src/http.ts +23 -1
- package/src/index.test.ts +107 -11
- package/src/index.ts +162 -73
- package/src/persist.ts +3 -1
- package/src/registry.test.ts +3 -2
- package/src/routes.ts +3 -1
- package/src/service.ts +27 -5
package/src/index.ts
CHANGED
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
* @module @linxin666/dsh-pet
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { Context } from '@deepseek-ai/cordis'
|
|
15
|
-
|
|
14
|
+
import { Context, type Volatile } from '@deepseek-ai/cordis'
|
|
15
|
+
// Type-only: pulls the settings service's Context merge (ctx.settings).
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-settings'
|
|
16
17
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
17
|
-
import z from 'schemastery'
|
|
18
|
-
import { PetService,
|
|
18
|
+
import z from '@deepseek-ai/schemastery'
|
|
19
|
+
import { PetService, type PetConfig, type PetSettingsSection } from './service.ts'
|
|
19
20
|
import { makePetRoutes } from './routes.ts'
|
|
20
21
|
import { loadPetRegistry, petPackageRoot } from './registry.ts'
|
|
21
|
-
import { BUBBLE_SCALE_MAX, BUBBLE_SCALE_MIN, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN } from './persist.ts'
|
|
22
|
+
import { BUBBLE_SCALE_MAX, BUBBLE_SCALE_MIN, DEFAULT_PET_ID, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN } from './persist.ts'
|
|
22
23
|
import { mountOnce } from './mount-once.ts'
|
|
23
24
|
|
|
24
25
|
export { PetService, MAX_SESSION_BUBBLES } from './service.ts'
|
|
@@ -119,54 +120,137 @@ export const name = 'pet'
|
|
|
119
120
|
export const inject = ['webServer']
|
|
120
121
|
|
|
121
122
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* a removed pet cannot invalidate the section (a strict union would refuse
|
|
126
|
-
* the whole registration). The settings card renders the actual registry
|
|
127
|
-
* choices itself from '/api/pet/pets'.
|
|
123
|
+
* Defaults of the fields the pet's settings page edits. They are the schema
|
|
124
|
+
* defaults of the pet row's own config, i.e. what a field the profile entry
|
|
125
|
+
* never set resolves to.
|
|
128
126
|
*/
|
|
129
|
-
export
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
export const PET_FORM_DEFAULTS = {
|
|
128
|
+
visible: true,
|
|
129
|
+
size: 160,
|
|
130
|
+
right: 24,
|
|
131
|
+
bottom: 20,
|
|
132
|
+
bubbleScale: 1,
|
|
133
|
+
petId: DEFAULT_PET_ID,
|
|
134
|
+
enabled: true,
|
|
135
|
+
decorationEnabled: true,
|
|
136
|
+
} as const
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* One settings field as the config carries it. The Host commits an edit into
|
|
140
|
+
* the running config through a live reference rather than remounting the row,
|
|
141
|
+
* so a field usually arrives as that reference; a plain value appears when the
|
|
142
|
+
* plugin runs outside a Loader (tests, direct mounts).
|
|
143
|
+
*/
|
|
144
|
+
export type LiveField<T> = Volatile<T> | T
|
|
145
|
+
|
|
146
|
+
/** The pet's settings fields, as a profile entry's config declares them. */
|
|
147
|
+
export interface PetFormConfig {
|
|
148
|
+
/** Master switch for the plugin (browser half + host routes). */
|
|
149
|
+
enabled?: LiveField<boolean>
|
|
150
|
+
/** Status-decoration master switch (pet-center M5, #567); defaults to on. */
|
|
151
|
+
decorationEnabled?: LiveField<boolean>
|
|
152
|
+
/** Master switch for the pet surface. */
|
|
153
|
+
visible?: LiveField<boolean>
|
|
154
|
+
/** Scale of the rendered pet in px (sprite cell height). */
|
|
155
|
+
size?: LiveField<number>
|
|
156
|
+
/** Horizontal inset from the viewport right edge, px. */
|
|
157
|
+
right?: LiveField<number>
|
|
158
|
+
/** Vertical inset from the viewport bottom edge, px. */
|
|
159
|
+
bottom?: LiveField<number>
|
|
160
|
+
/** Bubble typography multiplier on the automatic size following (#1549). */
|
|
161
|
+
bubbleScale?: LiveField<number>
|
|
162
|
+
/** Selected pet id (a registry entry; the service clamps stale values). */
|
|
163
|
+
petId?: LiveField<string | undefined>
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Plugin configuration. Under the 0.1.7 settings model a plugin's own Cordis
|
|
168
|
+
* Config IS its settings page: the Host derives one form per profile entry
|
|
169
|
+
* from this schema, so the fields the pet card edits live here with the
|
|
170
|
+
* defaults the card inherits, next to the tuning block a profile may still
|
|
171
|
+
* declare.
|
|
172
|
+
*
|
|
173
|
+
* Every page field is `volatile()` on purpose. The Host serves exactly the
|
|
174
|
+
* volatile fields of a Config and refuses writes to any other path, and a
|
|
175
|
+
* volatile field is the one it can commit into the RUNNING config: the pet
|
|
176
|
+
* reads the edited value from the live reference instead of being remounted
|
|
177
|
+
* for each edit (see `syncSettings` in `apply`). petId stays a plain string
|
|
178
|
+
* because the service clamps the value against the registry, so a stored
|
|
179
|
+
* selection naming a removed pet cannot invalidate the entry.
|
|
180
|
+
*/
|
|
181
|
+
export const Config = z.object({
|
|
182
|
+
visible: z.boolean().default(PET_FORM_DEFAULTS.visible).volatile(),
|
|
183
|
+
size: z.number().step(1).min(DISPLAY_SIZE_MIN).max(DISPLAY_SIZE_MAX).default(PET_FORM_DEFAULTS.size).volatile(),
|
|
184
|
+
right: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(PET_FORM_DEFAULTS.right).volatile(),
|
|
185
|
+
bottom: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(PET_FORM_DEFAULTS.bottom).volatile(),
|
|
186
|
+
bubbleScale: z.number().step(0.05).min(BUBBLE_SCALE_MIN).max(BUBBLE_SCALE_MAX).default(PET_FORM_DEFAULTS.bubbleScale).volatile(),
|
|
187
|
+
// An absent profile choice must leave the selection persisted in pet.json
|
|
188
|
+
// intact across restarts (aggregate rows have no served Host pet form).
|
|
189
|
+
petId: z.string().volatile(),
|
|
190
|
+
enabled: z.boolean().default(PET_FORM_DEFAULTS.enabled).volatile(),
|
|
191
|
+
decorationEnabled: z.boolean().default(PET_FORM_DEFAULTS.decorationEnabled).volatile(),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Read one live config field.
|
|
196
|
+
* @param field - the field's live reference, plain value, or nothing at all.
|
|
197
|
+
* @param fallback - value used when the field is absent.
|
|
198
|
+
* @returns the current field value.
|
|
199
|
+
*/
|
|
200
|
+
function readLive<T>(field: LiveField<T> | undefined, fallback: T): T {
|
|
201
|
+
if (field === undefined) return fallback
|
|
202
|
+
const ref = field as { get?: () => T | undefined }
|
|
203
|
+
return typeof ref.get === 'function' ? ref.get() ?? fallback : field as T
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The settings section the pet runs with: the effective values of the row's
|
|
208
|
+
* own config. `fallbackPetId` covers a mount whose config names no pet at all
|
|
209
|
+
* (a direct mount outside a Loader) — under a Loader the schema default is
|
|
210
|
+
* always present, so the persisted selection stands whenever the config
|
|
211
|
+
* carries it.
|
|
212
|
+
* @param config - the effective config of the pet row.
|
|
213
|
+
* @param fallbackPetId - pet id to use when the config names none.
|
|
214
|
+
* @returns the resolved settings section.
|
|
215
|
+
*/
|
|
216
|
+
export function petSettingsSection(config: PetFormConfig, fallbackPetId: string): PetSettingsSection {
|
|
217
|
+
return {
|
|
218
|
+
visible: readLive(config.visible, PET_FORM_DEFAULTS.visible),
|
|
219
|
+
size: readLive(config.size, PET_FORM_DEFAULTS.size),
|
|
220
|
+
right: readLive(config.right, PET_FORM_DEFAULTS.right),
|
|
221
|
+
bottom: readLive(config.bottom, PET_FORM_DEFAULTS.bottom),
|
|
222
|
+
bubbleScale: readLive(config.bubbleScale, PET_FORM_DEFAULTS.bubbleScale),
|
|
223
|
+
petId: readLive(config.petId, fallbackPetId),
|
|
224
|
+
enabled: readLive(config.enabled, PET_FORM_DEFAULTS.enabled),
|
|
225
|
+
decorationEnabled: readLive(config.decorationEnabled, PET_FORM_DEFAULTS.decorationEnabled),
|
|
226
|
+
}
|
|
140
227
|
}
|
|
141
228
|
|
|
142
229
|
/** Register the pet service and its API + asset routes on the context. */
|
|
143
230
|
export const apply = mountOnce('@linxin666/dsh-pet', applyImpl)
|
|
144
231
|
|
|
145
|
-
|
|
232
|
+
/** Plugin config: the tuning block a profile may declare plus the pet's settings fields. */
|
|
233
|
+
export type PetPluginConfig = Omit<PetConfig, 'enabled' | 'decorationEnabled'> & PetFormConfig
|
|
234
|
+
|
|
235
|
+
function applyImpl(ctx: Context, config: PetPluginConfig = {}): void {
|
|
146
236
|
const registry = config.registry
|
|
147
237
|
?? loadPetRegistry({
|
|
148
238
|
packageRoot: petPackageRoot(import.meta.url),
|
|
149
239
|
...(config.pets === undefined ? {} : { extra: config.pets }),
|
|
150
240
|
})
|
|
151
|
-
const service = new PetService(ctx, {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
right: service.display().right,
|
|
165
|
-
bottom: service.display().bottom,
|
|
166
|
-
petId: service.selectedPetId(),
|
|
167
|
-
enabled: config.enabled ?? true,
|
|
168
|
-
decorationEnabled: config.decorationEnabled ?? true,
|
|
169
|
-
}
|
|
241
|
+
const service = new PetService(ctx, {
|
|
242
|
+
...config,
|
|
243
|
+
enabled: readLive(config.enabled, PET_FORM_DEFAULTS.enabled),
|
|
244
|
+
decorationEnabled: readLive(config.decorationEnabled, PET_FORM_DEFAULTS.decorationEnabled),
|
|
245
|
+
registry,
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
// The effective settings ARE this row's own config: the Host serves one form
|
|
249
|
+
// per profile entry from `Config` above, and every edit is committed into the
|
|
250
|
+
// running config, so the plugin re-reads the section here instead of holding
|
|
251
|
+
// a separate settings document (0.1.6 registered one through
|
|
252
|
+
// settings.installSection/register and re-resolved it on every change).
|
|
253
|
+
const current = (): PetSettingsSection => petSettingsSection(config, service.selectedPetId())
|
|
170
254
|
// The browser half talks to the pet through same-origin JSON endpoints and
|
|
171
255
|
// loads each pet's atlas from the registry's own media route (RPC domains
|
|
172
256
|
// are platform-registered, so the pet serves its own API — the same
|
|
@@ -190,38 +274,43 @@ function applyImpl(ctx: Context, config: PetConfig = {}): void {
|
|
|
190
274
|
disposeRoutes = undefined
|
|
191
275
|
}
|
|
192
276
|
}
|
|
277
|
+
// Apply the row's config to the running service: pet.json mirrors the live
|
|
278
|
+
// display the drag / hide / summon interactions write (see
|
|
279
|
+
// syncSettingsFromPet), so a section the settings surface never touched
|
|
280
|
+
// resolves back to exactly what the pet already shows (the composition
|
|
281
|
+
// 'base' of the pre-0.1.7 section).
|
|
282
|
+
const syncSettings = (): void => {
|
|
283
|
+
const section = current()
|
|
284
|
+
service.applySettingsSection(section)
|
|
285
|
+
service.setEnabled(section.enabled ?? true)
|
|
286
|
+
syncRoutes()
|
|
287
|
+
}
|
|
288
|
+
// A settings edit lands in this row's live config reference and is announced
|
|
289
|
+
// here; the entry is NOT remounted for it, so the pet applies the new section
|
|
290
|
+
// itself (the pre-0.1.7 host ran the section's onChange hook for this).
|
|
291
|
+
ctx.on('loader/volatile-update', () => { syncSettings() })
|
|
193
292
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
{
|
|
203
|
-
setSource: (source) => { current = source },
|
|
204
|
-
onChange: () => {
|
|
205
|
-
const section = current()
|
|
206
|
-
service.applySettingsSection(section)
|
|
207
|
-
service.setEnabled(section.enabled ?? true)
|
|
208
|
-
syncRoutes()
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
)
|
|
212
|
-
} else if (typeof settingsCtx.settings?.register === 'function') {
|
|
213
|
-
const scope = settingsCtx.settings.register(PET_SETTINGS_NAMESPACE as SettingsNamespace, schema, { base })
|
|
214
|
-
current = () => scope?.get?.() ?? base
|
|
215
|
-
scope?.watch?.(() => {
|
|
216
|
-
const section = current()
|
|
217
|
-
service.applySettingsSection(section)
|
|
218
|
-
service.setEnabled(section.enabled ?? true)
|
|
219
|
-
syncRoutes()
|
|
220
|
-
})
|
|
293
|
+
// The pet ships its own settings card (the browser half's
|
|
294
|
+
// 'settings.section' page), so the Host must not also generate a page for
|
|
295
|
+
// this entry from Config.
|
|
296
|
+
settingsCtx.effect(() => {
|
|
297
|
+
try {
|
|
298
|
+
return settingsCtx.settings.configure({ auto: false }, ctx.fiber)
|
|
299
|
+
} catch {
|
|
300
|
+
return () => {}
|
|
221
301
|
}
|
|
222
|
-
}
|
|
223
|
-
// Defensive fallback against settings registration differences
|
|
224
|
-
}
|
|
302
|
+
}, 'pet: settings page policy')
|
|
225
303
|
})
|
|
226
|
-
|
|
304
|
+
syncSettings()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
declare module '@deepseek-ai/cordis' {
|
|
308
|
+
interface Events {
|
|
309
|
+
/**
|
|
310
|
+
* Volatile config values were committed into the running fiber without a
|
|
311
|
+
* remount; dispatched to the owning fiber only. Declared by the Loader and
|
|
312
|
+
* restated here because this package carries no dependency on its types.
|
|
313
|
+
*/
|
|
314
|
+
'loader/volatile-update'(paths: readonly (readonly string[])[]): void
|
|
315
|
+
}
|
|
227
316
|
}
|
package/src/persist.ts
CHANGED
|
@@ -197,7 +197,9 @@ function loadGameplay(parsed: PetPersistDocument): Record<string, PetGameplaySta
|
|
|
197
197
|
const item: PetGameplayState = {
|
|
198
198
|
stats,
|
|
199
199
|
currencies,
|
|
200
|
-
|
|
200
|
+
// Any non-empty id survives the round trip; whether it is still declared
|
|
201
|
+
// is settled against the manifest when the mode is read back.
|
|
202
|
+
mode: typeof record.mode === 'string' && record.mode.length > 0 && record.mode.length <= 24 ? record.mode : null,
|
|
201
203
|
settledAt: clamp(finiteNum(record.settledAt, 0), Number.MAX_SAFE_INTEGER),
|
|
202
204
|
}
|
|
203
205
|
if (typeof record.incomeCarryMs === 'number' && Number.isFinite(record.incomeCarryMs)) {
|
package/src/registry.test.ts
CHANGED
|
@@ -314,14 +314,15 @@ describe('loadPetRegistry', () => {
|
|
|
314
314
|
dshPetsDir: '',
|
|
315
315
|
})
|
|
316
316
|
|
|
317
|
-
// The repo checkout also resolves miku (frames2d gameplay
|
|
318
|
-
// (frames2d gameplay pet) and starry-doll (community sprite2d pet) from
|
|
317
|
+
// The repo checkout also resolves doro and miku (frames2d gameplay pets),
|
|
318
|
+
// jyn (frames2d gameplay pet) and starry-doll (community sprite2d pet) from
|
|
319
319
|
// assets/; the npm files whitelist excludes them (Workshop delivery), so
|
|
320
320
|
// npm installs see the atlas pets until a Workshop install lands them
|
|
321
321
|
// under $DSH_HOME/pets. blue-throated-bee-eater ships bundled alongside
|
|
322
322
|
// the other atlas pets.
|
|
323
323
|
expect(registry.entries.map(entry => entry.id)).toEqual([
|
|
324
324
|
'blue-throated-bee-eater',
|
|
325
|
+
'doro',
|
|
325
326
|
'jyn',
|
|
326
327
|
'miku',
|
|
327
328
|
'ouo-neko',
|
package/src/routes.ts
CHANGED
|
@@ -623,7 +623,9 @@ export function makePetRoutes(deps: { service: PetService; ctx: Context; assetCa
|
|
|
623
623
|
}),
|
|
624
624
|
postRoute(ctx, PET_API_PREFIX + '/gameplay/mode', (body) => {
|
|
625
625
|
const mode = body.mode
|
|
626
|
-
|
|
626
|
+
// The id is checked against the pet's own mode set in the service (work,
|
|
627
|
+
// sleep, or any declared 'modes' entry).
|
|
628
|
+
if (mode !== null && typeof mode !== 'string') return Promise.reject(new Error('invalid-mode'))
|
|
627
629
|
return service.gameplaySetMode(mode)
|
|
628
630
|
}),
|
|
629
631
|
postRoute(ctx, PET_API_PREFIX + '/gameplay/work-tick', () => service.gameplayWorkTick()),
|
package/src/service.ts
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
applyGameplayEffects,
|
|
66
66
|
drawLotteryTier,
|
|
67
67
|
initialGameplayState,
|
|
68
|
+
isDeclaredMode,
|
|
68
69
|
rollTouchBranch,
|
|
69
70
|
rollWorkOutcome,
|
|
70
71
|
settleGameplay,
|
|
@@ -219,7 +220,8 @@ export type PetInteractResult = LedgerInteractionResult
|
|
|
219
220
|
export interface PetGameplayStateView {
|
|
220
221
|
/** Stat values rounded for display. */
|
|
221
222
|
stats: Record<string, number>
|
|
222
|
-
mode
|
|
223
|
+
/** Active mode id ('work' | 'sleep' | a declared extra mode) or null. */
|
|
224
|
+
mode: string | null
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
/** Result of the gameplay verbs (touch / setMode / workTick / buy). */
|
|
@@ -585,7 +587,12 @@ export class PetService extends Service {
|
|
|
585
587
|
petId,
|
|
586
588
|
state: stored === undefined
|
|
587
589
|
? initialGameplayState(def, now)
|
|
588
|
-
|
|
590
|
+
// Spread first so the settle remainders (restoreCarryMs / incomeCarryMs)
|
|
591
|
+
// survive a verb. Enumerating the fields dropped them, so every verb
|
|
592
|
+
// floored the elapsed time and threw the remainder away -- which starves
|
|
593
|
+
// any interval longer than the gap between verbs (30 s sleep restore,
|
|
594
|
+
// 30 min passive income).
|
|
595
|
+
: { ...stored, stats: { ...stored.stats }, currencies: { ...stored.currencies } },
|
|
589
596
|
}
|
|
590
597
|
}
|
|
591
598
|
|
|
@@ -593,7 +600,14 @@ export class PetService extends Service {
|
|
|
593
600
|
private gameplayViewOf(state: PetGameplayState): PetGameplayStateView {
|
|
594
601
|
const stats: Record<string, number> = {}
|
|
595
602
|
for (const [name, value] of Object.entries(state.stats)) stats[name] = Math.round(value)
|
|
596
|
-
|
|
603
|
+
// A persisted mode the manifest no longer declares (a pet.json edit after
|
|
604
|
+
// the pet was parked in it) reads as "no mode". Otherwise the client would
|
|
605
|
+
// latch a mode it cannot resolve: the mode chip prints a raw i18n key, and
|
|
606
|
+
// the client's roam roll — suppressed while any mode is active — would
|
|
607
|
+
// never fire again.
|
|
608
|
+
const def = this.gameplayDef()
|
|
609
|
+
const mode = state.mode !== null && def !== undefined && !isDeclaredMode(def, state.mode) ? null : state.mode
|
|
610
|
+
return { stats, mode }
|
|
597
611
|
}
|
|
598
612
|
|
|
599
613
|
/**
|
|
@@ -657,15 +671,23 @@ export class PetService extends Service {
|
|
|
657
671
|
}
|
|
658
672
|
}
|
|
659
673
|
|
|
660
|
-
/**
|
|
661
|
-
|
|
674
|
+
/**
|
|
675
|
+
* RPC: enter or leave a gameplay mode (null clears it). Every mode the
|
|
676
|
+
* manifest declares is accepted: 'work', 'sleep', or one of the extra
|
|
677
|
+
* 'modes' entries (a bath, a play session…).
|
|
678
|
+
*/
|
|
679
|
+
async gameplaySetMode(mode: string | null): Promise<PetGameplayVerbResult> {
|
|
662
680
|
const def = this.gameplayDef()
|
|
663
681
|
if (def === undefined) return { ok: false, error: 'no-gameplay' }
|
|
664
682
|
if (mode === 'work' && def.work === undefined) return { ok: false, error: 'no-work' }
|
|
665
683
|
if (mode === 'sleep' && def.sleep === undefined) return { ok: false, error: 'no-sleep' }
|
|
684
|
+
if (mode !== null && !isDeclaredMode(def, mode)) return { ok: false, error: 'unknown-mode' }
|
|
666
685
|
const now = Date.now()
|
|
667
686
|
const { petId, state } = this.gameplayState(def, now)
|
|
668
687
|
settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive })
|
|
688
|
+
// Switching modes restarts the restore cadence: a leftover carry from the
|
|
689
|
+
// previous mode must not advance the new mode's first tick.
|
|
690
|
+
if (state.mode !== mode) state.restoreCarryMs = 0
|
|
669
691
|
state.mode = mode
|
|
670
692
|
this.drainGameplayTreats(state)
|
|
671
693
|
this.commitGameplay(petId, state)
|