@linxin666/dsh-pet 0.2.6 → 0.2.8
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/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 +62 -58
- package/lib/types/access.js +1 -1
- package/lib/types/affinity.d.ts +18 -18
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +1 -1
- package/lib/types/chatter.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +2 -2
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +2 -2
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +27 -21
- package/lib/types/client/PluginSettingsCard.d.ts +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +56 -9
- package/lib/types/client/locales.d.ts +100 -100
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/renderers/PetRendererSwitch.js +2 -2
- package/lib/types/client/renderers/live2d/Live2dVisualMount.js +2 -2
- package/lib/types/client/renderers/live2d.js +2 -2
- package/lib/types/client/renderers/registry.d.ts.map +1 -1
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +1 -1
- 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/contracts/status-decoration.d.ts +1 -1
- package/lib/types/decoration.d.ts +1 -1
- package/lib/types/decoration.d.ts.map +1 -1
- package/lib/types/decoration.js +1 -1
- package/lib/types/event-projection.js +1 -1
- package/lib/types/index.js +13 -13
- package/lib/types/invariant.js +2 -2
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +3 -3
- package/lib/types/manifest-v2.d.ts +1 -1
- package/lib/types/manifest-v2.d.ts.map +1 -1
- package/lib/types/model3.js +1 -1
- package/lib/types/persist.js +3 -3
- package/lib/types/registry.d.ts +5 -1
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +22 -18
- package/lib/types/remarks.d.ts.map +1 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +4 -4
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +7 -7
- package/lib/types/state.d.ts.map +1 -1
- package/lib/types/voice-pack.d.ts +3 -3
- package/lib/types/voice-pack.d.ts.map +1 -1
- package/lib/types/voice-pack.js +1 -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
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The global pet entry container opts into the L2 semantic attributes
|
|
4
4
|
* (issue #506): the apply body mounts [data-dsh-pet-root] with
|
|
5
|
-
* data-dsh-plugin="pet" so skins can target the pet subtree.
|
|
5
|
+
* data-dsh-plugin="pet" so skins can target the pet subtree. The same
|
|
6
|
+
* tests pin the fiber-lifecycle contract (issue #785): a hot-reloaded or
|
|
7
|
+
* re-injected bundle instance must never leave the previous React root,
|
|
8
|
+
* container, or settings subscription behind, so the page always holds
|
|
9
|
+
* exactly one [data-dsh-pet-root].
|
|
6
10
|
*/
|
|
7
11
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
8
12
|
// The npm SDK's client half is a closure-factory bundle for the GUI's
|
|
@@ -51,12 +55,24 @@ beforeAll(() => {
|
|
|
51
55
|
document.documentElement.lang = 'zh'
|
|
52
56
|
})
|
|
53
57
|
|
|
58
|
+
/** A client root context with observable fiber disposal. */
|
|
59
|
+
interface FakeClientLifecycle {
|
|
60
|
+
ctx: ClientContext
|
|
61
|
+
dispose(): void
|
|
62
|
+
settingsListenerCount(): number
|
|
63
|
+
emitSettings(): void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const activeLifecycles: FakeClientLifecycle[] = []
|
|
67
|
+
|
|
54
68
|
afterEach(() => {
|
|
69
|
+
for (const lifecycle of activeLifecycles.splice(0).reverse()) lifecycle.dispose()
|
|
55
70
|
document.body.replaceChildren()
|
|
56
71
|
})
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
function fakeContext(): FakeClientLifecycle {
|
|
74
|
+
const disposers: (() => void)[] = []
|
|
75
|
+
const settingsListeners = new Set<() => void>()
|
|
60
76
|
const scope = {
|
|
61
77
|
getSnapshot: () => ({
|
|
62
78
|
status: 'ready',
|
|
@@ -67,29 +83,120 @@ function fakeContext(): ClientContext {
|
|
|
67
83
|
revision: 1,
|
|
68
84
|
mode: 'host',
|
|
69
85
|
}),
|
|
70
|
-
subscribe: () =>
|
|
86
|
+
subscribe: (listener: () => void) => {
|
|
87
|
+
settingsListeners.add(listener)
|
|
88
|
+
return () => { settingsListeners.delete(listener) }
|
|
89
|
+
},
|
|
71
90
|
}
|
|
72
|
-
|
|
73
|
-
effect: (fn: () => unknown) => {
|
|
91
|
+
const ctx = {
|
|
92
|
+
effect: (fn: () => unknown, _label?: string) => {
|
|
74
93
|
const dispose = fn()
|
|
75
|
-
|
|
94
|
+
if (typeof dispose !== 'function') return () => {}
|
|
95
|
+
const cleanup = dispose as () => void
|
|
96
|
+
disposers.push(cleanup)
|
|
97
|
+
return cleanup
|
|
76
98
|
},
|
|
77
99
|
locale: { register: () => () => {} },
|
|
78
100
|
get: () => undefined,
|
|
79
101
|
settingsScope: { bind: () => scope },
|
|
80
102
|
slots: {
|
|
81
|
-
|
|
103
|
+
// Cordis runs the factory when the slot mounts and its returned
|
|
104
|
+
// disposer when the fiber disposes; mirror that so slot content
|
|
105
|
+
// (settings card subscription) is cleaned with the fiber.
|
|
106
|
+
inject: (_name: string, callback: () => () => void) => {
|
|
107
|
+
const dispose = callback()
|
|
108
|
+
if (typeof dispose === 'function') disposers.push(dispose)
|
|
109
|
+
return dispose
|
|
110
|
+
},
|
|
82
111
|
register: () => () => {},
|
|
83
112
|
},
|
|
84
113
|
sessions: undefined,
|
|
85
114
|
} as unknown as ClientContext
|
|
115
|
+
let disposed = false
|
|
116
|
+
const lifecycle: FakeClientLifecycle = {
|
|
117
|
+
ctx,
|
|
118
|
+
dispose: () => {
|
|
119
|
+
if (disposed) return
|
|
120
|
+
disposed = true
|
|
121
|
+
for (const dispose of disposers.splice(0).reverse()) dispose()
|
|
122
|
+
},
|
|
123
|
+
settingsListenerCount: () => settingsListeners.size,
|
|
124
|
+
emitSettings: () => {
|
|
125
|
+
for (const listener of settingsListeners) listener()
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
activeLifecycles.push(lifecycle)
|
|
129
|
+
return lifecycle
|
|
86
130
|
}
|
|
87
131
|
|
|
88
|
-
describe('pet client apply
|
|
89
|
-
it('mounts the pet root container with data-dsh-plugin
|
|
90
|
-
apply(fakeContext())
|
|
132
|
+
describe('pet client apply', () => {
|
|
133
|
+
it('mounts the pet root container with the L2 data-dsh-plugin attribute (#506)', () => {
|
|
134
|
+
apply(fakeContext().ctx)
|
|
91
135
|
const root = document.body.querySelector('[data-dsh-pet-root]')
|
|
92
136
|
expect(root).not.toBeNull()
|
|
93
137
|
expect(root!.getAttribute('data-dsh-plugin')).toBe('pet')
|
|
94
138
|
})
|
|
139
|
+
|
|
140
|
+
it('keeps one global pet root when two client factories overlap (#785)', () => {
|
|
141
|
+
const first = fakeContext()
|
|
142
|
+
apply(first.ctx)
|
|
143
|
+
const firstContainer = document.body.querySelector('[data-dsh-pet-root]')
|
|
144
|
+
expect(firstContainer).not.toBeNull()
|
|
145
|
+
|
|
146
|
+
// A rebuilt bundle re-applies while the first fiber is still draining.
|
|
147
|
+
const second = fakeContext()
|
|
148
|
+
apply(second.ctx)
|
|
149
|
+
|
|
150
|
+
const roots = document.body.querySelectorAll('[data-dsh-pet-root]')
|
|
151
|
+
expect(roots).toHaveLength(1)
|
|
152
|
+
expect(roots[0]).not.toBe(firstContainer)
|
|
153
|
+
expect(firstContainer!.isConnected).toBe(false)
|
|
154
|
+
|
|
155
|
+
// The first instance must not resurrect its own root.
|
|
156
|
+
first.emitSettings()
|
|
157
|
+
expect(document.body.querySelectorAll('[data-dsh-pet-root]')).toHaveLength(1)
|
|
158
|
+
|
|
159
|
+
// The first fiber draining later stays a no-op.
|
|
160
|
+
first.dispose()
|
|
161
|
+
expect(document.body.querySelectorAll('[data-dsh-pet-root]')).toHaveLength(1)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('tears down root, container, and settings subscription on fiber disposal (#785)', () => {
|
|
165
|
+
const lifecycle = fakeContext()
|
|
166
|
+
apply(lifecycle.ctx)
|
|
167
|
+
expect(document.body.querySelectorAll('[data-dsh-pet-root]')).toHaveLength(1)
|
|
168
|
+
// The settings card controller subscribes to the same scope as the UI
|
|
169
|
+
// sync, so at least the sync listener is present while the fiber lives.
|
|
170
|
+
expect(lifecycle.settingsListenerCount()).toBeGreaterThan(0)
|
|
171
|
+
|
|
172
|
+
lifecycle.dispose()
|
|
173
|
+
|
|
174
|
+
expect(document.body.querySelectorAll('[data-dsh-pet-root]')).toHaveLength(0)
|
|
175
|
+
expect(lifecycle.settingsListenerCount()).toBe(0)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('re-applies cleanly after disposal so a hot reload keeps one pet (#785)', () => {
|
|
179
|
+
const first = fakeContext()
|
|
180
|
+
apply(first.ctx)
|
|
181
|
+
first.dispose()
|
|
182
|
+
expect(document.body.querySelectorAll('[data-dsh-pet-root]')).toHaveLength(0)
|
|
183
|
+
|
|
184
|
+
const second = fakeContext()
|
|
185
|
+
apply(second.ctx)
|
|
186
|
+
expect(document.body.querySelectorAll('[data-dsh-pet-root]')).toHaveLength(1)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('sweeps stale containers left behind by instances without a teardown slot (#785)', () => {
|
|
190
|
+
// A container from a bundle build that predates the teardown registry:
|
|
191
|
+
// nothing registered a teardown, so only the mount-path sweep can clear it.
|
|
192
|
+
const stale = document.createElement('div')
|
|
193
|
+
stale.dataset.dshPetRoot = ''
|
|
194
|
+
document.body.appendChild(stale)
|
|
195
|
+
|
|
196
|
+
apply(fakeContext().ctx)
|
|
197
|
+
|
|
198
|
+
const roots = document.body.querySelectorAll('[data-dsh-pet-root]')
|
|
199
|
+
expect(roots).toHaveLength(1)
|
|
200
|
+
expect(stale.isConnected).toBe(false)
|
|
201
|
+
})
|
|
95
202
|
})
|
package/src/client/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { createPetStore, type PetStoreInstance } from './pet-store.ts'
|
|
|
28
28
|
import { PetDockEntry, type PetInjected } from './PetDockEntry.tsx'
|
|
29
29
|
import { defaultPetRendererRegistry } from './renderers/registry.ts'
|
|
30
30
|
import { live2dRenderer } from './renderers/live2d.ts'
|
|
31
|
+
import { registerPetUiTeardown, takeoverPetUiTeardown } from './ui-teardown.ts'
|
|
31
32
|
import { PetSettingsSection, PetSettingsCardController, type PetSettings } from './PetSettingsCard.tsx'
|
|
32
33
|
import { NS, en, zh, t } from './locales.ts'
|
|
33
34
|
|
|
@@ -139,9 +140,22 @@ export function apply(ctx: ClientContext): void {
|
|
|
139
140
|
|
|
140
141
|
// The global pet entry, its store, and the poll loop live while the plugin
|
|
141
142
|
// is enabled; toggling the setting off hides the pet and stops polling.
|
|
143
|
+
// 'uiDead' marks a terminal teardown (takeover by a later bundle instance
|
|
144
|
+
// or fiber disposal): a taken-over or disposed instance must never remount
|
|
145
|
+
// from a late settings callback (issue #785).
|
|
142
146
|
let disposeUi: (() => void) | undefined
|
|
147
|
+
let clearUiTeardown: (() => void) | undefined
|
|
148
|
+
let uiDead = false
|
|
149
|
+
const killUi = (): void => {
|
|
150
|
+
if (uiDead) return
|
|
151
|
+
uiDead = true
|
|
152
|
+
clearUiTeardown?.()
|
|
153
|
+
clearUiTeardown = undefined
|
|
154
|
+
disposeUi?.()
|
|
155
|
+
disposeUi = undefined
|
|
156
|
+
}
|
|
143
157
|
const syncUi = (): void => {
|
|
144
|
-
if (enabled() && disposeUi === undefined) {
|
|
158
|
+
if (!uiDead && enabled() && disposeUi === undefined) {
|
|
145
159
|
// ONE store instance for the whole app, owned by this apply body. The
|
|
146
160
|
// pet is host-global (state/display/interactions are /api/pet/*
|
|
147
161
|
// endpoints with no session dimension), so the slot system's per-session
|
|
@@ -296,6 +310,17 @@ export function apply(ctx: ClientContext): void {
|
|
|
296
310
|
// The entry therefore mounts straight onto document.body via a single
|
|
297
311
|
// React root for the page lifetime: PetSprite portals itself to body
|
|
298
312
|
// when visible, and the hidden-state summon button is fixed-positioned.
|
|
313
|
+
//
|
|
314
|
+
// Cross-instance single-mount guard (issue #785): take over the
|
|
315
|
+
// page-global slot first — the previous bundle instance's fiber may
|
|
316
|
+
// still be draining during a client reload, so unmount its React root
|
|
317
|
+
// and remove its container — then sweep containers left behind by
|
|
318
|
+
// instances that predate the teardown registry, so this mount is the
|
|
319
|
+
// page's only [data-dsh-pet-root].
|
|
320
|
+
takeoverPetUiTeardown()
|
|
321
|
+
for (const stale of Array.from(document.querySelectorAll('div[data-dsh-pet-root]'))) {
|
|
322
|
+
stale.remove()
|
|
323
|
+
}
|
|
299
324
|
const container = document.createElement('div')
|
|
300
325
|
container.dataset.dshPetRoot = ''
|
|
301
326
|
container.dataset.dshPlugin = 'pet'
|
|
@@ -303,17 +328,40 @@ export function apply(ctx: ClientContext): void {
|
|
|
303
328
|
const petRoot = createRoot(container)
|
|
304
329
|
petRoot.render(createElement(PetDockEntry, { ...injected(), t }))
|
|
305
330
|
|
|
331
|
+
let uiGone = false
|
|
306
332
|
disposeUi = () => {
|
|
333
|
+
if (uiGone) return
|
|
334
|
+
uiGone = true
|
|
335
|
+
clearUiTeardown?.()
|
|
336
|
+
clearUiTeardown = undefined
|
|
307
337
|
petRoot.unmount()
|
|
308
338
|
container.remove()
|
|
309
339
|
disposePoll()
|
|
310
340
|
disposeUi = undefined
|
|
311
341
|
}
|
|
312
|
-
|
|
342
|
+
// The slot teardown is the takeover hook a later apply body runs; it
|
|
343
|
+
// marks this instance terminal so a late settings callback from the
|
|
344
|
+
// still-draining instance cannot remount a second pet.
|
|
345
|
+
clearUiTeardown = registerPetUiTeardown(() => {
|
|
346
|
+
uiDead = true
|
|
347
|
+
disposeUi?.()
|
|
348
|
+
})
|
|
349
|
+
} else if (!uiDead && !enabled() && disposeUi !== undefined) {
|
|
313
350
|
disposeUi()
|
|
314
351
|
disposeUi = undefined
|
|
315
352
|
}
|
|
316
353
|
}
|
|
317
|
-
|
|
354
|
+
// The settings subscription and the pet UI lifetime follow the fiber
|
|
355
|
+
// (issue #785): disposal drops the subscription and tears the UI down
|
|
356
|
+
// (terminal), so a hot-reloaded or re-injected bundle never leaves the
|
|
357
|
+
// previous React root, container, or poll loop behind on document.body.
|
|
358
|
+
const unsubscribeSettings = settingsScope.subscribe(syncUi)
|
|
359
|
+
ctx.effect(
|
|
360
|
+
() => () => {
|
|
361
|
+
unsubscribeSettings()
|
|
362
|
+
killUi()
|
|
363
|
+
},
|
|
364
|
+
'pet: client lifecycle',
|
|
365
|
+
)
|
|
318
366
|
syncUi()
|
|
319
367
|
}
|
|
@@ -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. */
|