@linxin666/dsh-pet 0.3.16 → 0.3.18
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 +16 -1
- package/README.zh.md +16 -1
- package/assets/blue-throated-bee-eater/pet.json +204 -0
- package/assets/blue-throated-bee-eater/previews/failed.gif +0 -0
- package/assets/blue-throated-bee-eater/previews/idle.gif +0 -0
- package/assets/blue-throated-bee-eater/previews/jumping.gif +0 -0
- package/assets/blue-throated-bee-eater/previews/review.gif +0 -0
- package/assets/blue-throated-bee-eater/previews/running-left.gif +0 -0
- package/assets/blue-throated-bee-eater/previews/running-right.gif +0 -0
- package/assets/blue-throated-bee-eater/previews/running.gif +0 -0
- package/assets/blue-throated-bee-eater/previews/waiting.gif +0 -0
- package/assets/blue-throated-bee-eater/previews/waving.gif +0 -0
- package/assets/blue-throated-bee-eater/spritesheet.webp +0 -0
- package/assets/blue-throated-bee-eater/voice.json +13 -0
- package/contracts/pet-manifest-v2.schema.json +84 -0
- package/lib/client.js +176 -27
- package/lib/client.js.map +1 -1
- package/lib/index.js +170 -6
- package/lib/types/client/gameplay-hud.d.ts +2 -0
- package/lib/types/client/gameplay-hud.d.ts.map +1 -1
- package/lib/types/client/gameplay-hud.js +65 -12
- package/lib/types/client/index.d.ts +0 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +25 -2
- package/lib/types/client/locales.d.ts +7 -3
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +7 -3
- package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -1
- package/lib/types/client/renderers/Frames2dVisualMount.js +5 -1
- package/lib/types/client/renderers/frames2d.d.ts +25 -0
- package/lib/types/client/renderers/frames2d.d.ts.map +1 -1
- package/lib/types/client/renderers/frames2d.js +84 -7
- package/lib/types/manifest-v2.d.ts +45 -0
- package/lib/types/manifest-v2.d.ts.map +1 -1
- package/lib/types/manifest-v2.js +139 -1
- package/lib/types/persist.d.ts +1 -1
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +1 -1
- package/lib/types/registry.d.ts +22 -0
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +45 -1
- package/package.json +2 -1
- package/src/client/gameplay-hud.test.tsx +64 -0
- package/src/client/gameplay-hud.tsx +109 -21
- package/src/client/index.ts +26 -2
- package/src/client/locales.ts +7 -3
- package/src/client/pet.module.css +31 -1
- package/src/client/renderers/Frames2dVisualMount.tsx +5 -1
- package/src/client/renderers/frames2d.test.ts +43 -0
- package/src/client/renderers/frames2d.ts +100 -7
- package/src/manifest-v2.ts +167 -1
- package/src/persist.ts +1 -1
- package/src/registry.test.ts +19 -5
- package/src/registry.ts +66 -1
package/src/manifest-v2.ts
CHANGED
|
@@ -100,6 +100,43 @@ export interface PetManifestFrames2d {
|
|
|
100
100
|
tracks: Record<string, PetManifestFrames2dTrack>
|
|
101
101
|
/** ActivityPhase -> track name; idle is required, unmapped phases fall back to it. */
|
|
102
102
|
phases: Partial<Record<ActivityPhase, string>> & { idle: string }
|
|
103
|
+
/**
|
|
104
|
+
* Optional selectable skins: a skin swaps the base idle target (the idle
|
|
105
|
+
* phase, unmapped phases, and every fallback back to idle) to its own
|
|
106
|
+
* looping idle track. Gameplay tracks (shy/work/sleep...) are not replaced.
|
|
107
|
+
*/
|
|
108
|
+
skins?: PetManifestSkin[]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One selectable frames2d skin. */
|
|
112
|
+
export interface PetManifestSkin {
|
|
113
|
+
id: string
|
|
114
|
+
label: string
|
|
115
|
+
/** A declared, looping track that becomes the base idle while selected. */
|
|
116
|
+
idleTrack: string
|
|
117
|
+
/**
|
|
118
|
+
* Optional click-triggered actions exclusive to this skin: while the skin
|
|
119
|
+
* is selected, a tap rolls each action by probability (in order) and plays
|
|
120
|
+
* its track once if hit. Misses fall through to the regular touch zones.
|
|
121
|
+
*/
|
|
122
|
+
clickActions?: PetManifestSkinClickAction[]
|
|
123
|
+
/**
|
|
124
|
+
* Optional gameplay-track overrides: gameplay state name (e.g. 'sleep' or
|
|
125
|
+
* 'work') -> a declared track the skin swaps in while selected. Lets a skin
|
|
126
|
+
* restyle a gameplay loop (e.g. its own sleep animation) without touching
|
|
127
|
+
* the default pet's tracks.
|
|
128
|
+
*/
|
|
129
|
+
gameplayTracks?: Record<string, string>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** One probability-rolled tap action a skin may declare. */
|
|
133
|
+
export interface PetManifestSkinClickAction {
|
|
134
|
+
/** A declared track played once on hit (usually non-looping). */
|
|
135
|
+
track: string
|
|
136
|
+
/** Hit probability for this action (0..1). */
|
|
137
|
+
probability: number
|
|
138
|
+
/** Optional lines spoken when the action fires. */
|
|
139
|
+
phrases?: string[]
|
|
103
140
|
}
|
|
104
141
|
|
|
105
142
|
/** Normalized v2 manifest the registry consumes. */
|
|
@@ -153,9 +190,35 @@ export const KNOWN_SPRITE2D = new Set(['spritesheetPath', 'cell', 'columns', 'at
|
|
|
153
190
|
/** live2d block field allow-list (drift-locked to the schema file). */
|
|
154
191
|
export const KNOWN_LIVE2D = new Set(['model', 'scale', 'translate', 'motions', 'expressions', 'hitAreas', 'lipSync'])
|
|
155
192
|
/** frames2d block field allow-list (drift-locked to the schema file). */
|
|
156
|
-
export const KNOWN_FRAMES2D = new Set(['dir', 'defaultFrameMs', 'tracks', 'phases'])
|
|
193
|
+
export const KNOWN_FRAMES2D = new Set(['dir', 'defaultFrameMs', 'tracks', 'phases', 'skins'])
|
|
157
194
|
/** frames2d track field allow-list (drift-locked to the schema file). */
|
|
158
195
|
export const KNOWN_FRAMES2D_TRACK = new Set(['frames', 'frameMs', 'loop', 'fallback'])
|
|
196
|
+
/** frames2d skin entry field allow-list (drift-locked to the schema file). */
|
|
197
|
+
export const KNOWN_SKIN = new Set(['id', 'label', 'idleTrack', 'clickActions', 'gameplayTracks'])
|
|
198
|
+
/** frames2d skin click-action field allow-list (drift-locked to the schema file). */
|
|
199
|
+
export const KNOWN_SKIN_CLICK = new Set(['track', 'probability', 'phrases'])
|
|
200
|
+
/** Max selectable skins a frames2d manifest may declare. */
|
|
201
|
+
export const FRAMES2D_MAX_SKINS = 16
|
|
202
|
+
/** Max click actions one skin may declare. */
|
|
203
|
+
export const FRAMES2D_MAX_SKIN_CLICKS = 8
|
|
204
|
+
/** Max gameplay-state overrides one skin may declare. */
|
|
205
|
+
export const FRAMES2D_MAX_SKIN_GAMEPLAY = 8
|
|
206
|
+
/** Max spoken lines one skin click action may declare. */
|
|
207
|
+
const SKIN_CLICK_MAX_PHRASES = 5
|
|
208
|
+
/** Max length of one spoken line. */
|
|
209
|
+
const SKIN_CLICK_PHRASE_MAX_LENGTH = 120
|
|
210
|
+
|
|
211
|
+
/** Validate a skin click action's optional phrase pool (same spirit as gameplay phrases). */
|
|
212
|
+
function parseSkinPhrases(raw: unknown, field: string, diag: Diagnostics): string[] | undefined {
|
|
213
|
+
if (raw === undefined) return undefined
|
|
214
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > SKIN_CLICK_MAX_PHRASES
|
|
215
|
+
|| raw.some(line => typeof line !== 'string' || line.trim() === '' || line.length > SKIN_CLICK_PHRASE_MAX_LENGTH)) {
|
|
216
|
+
diag.error(field + ' must be 1..' + SKIN_CLICK_MAX_PHRASES + ' non-empty lines of at most '
|
|
217
|
+
+ SKIN_CLICK_PHRASE_MAX_LENGTH + ' chars')
|
|
218
|
+
return undefined
|
|
219
|
+
}
|
|
220
|
+
return raw as string[]
|
|
221
|
+
}
|
|
159
222
|
|
|
160
223
|
class Diagnostics {
|
|
161
224
|
readonly list: PetManifestDiagnostic[] = []
|
|
@@ -451,6 +514,109 @@ function parseFrames2dBlock(raw: unknown, diag: Diagnostics): PetManifestFrames2
|
|
|
451
514
|
diag.error('frames2d.tracks.' + name + '.fallback references unknown track ' + JSON.stringify(track.fallback))
|
|
452
515
|
}
|
|
453
516
|
}
|
|
517
|
+
// Optional skins: each selects a declared looping idle track as the base
|
|
518
|
+
// idle while active (id/label are UI-facing, idleTrack is structure).
|
|
519
|
+
if (raw.skins !== undefined) {
|
|
520
|
+
if (!Array.isArray(raw.skins) || raw.skins.length === 0 || raw.skins.length > FRAMES2D_MAX_SKINS) {
|
|
521
|
+
diag.error('frames2d.skins must be an array of 1..' + FRAMES2D_MAX_SKINS + ' skins')
|
|
522
|
+
} else {
|
|
523
|
+
const skins: PetManifestSkin[] = []
|
|
524
|
+
const seen = new Set<string>()
|
|
525
|
+
for (const [index, entry] of raw.skins.entries()) {
|
|
526
|
+
const field = 'frames2d.skins[' + index + ']'
|
|
527
|
+
if (!isRecord(entry)) {
|
|
528
|
+
diag.error(field + ' must be an object')
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
const extra = unknownKeys(entry, KNOWN_SKIN)
|
|
532
|
+
if (extra.length > 0) diag.error(field + ': unknown field(s) ' + extra.map(k => JSON.stringify(k)).join(', '))
|
|
533
|
+
const id = typeof entry.id === 'string' ? entry.id.trim() : ''
|
|
534
|
+
if (id === '' || id.length > 24 || !FRAMES2D_TRACK_NAME.test(id)) {
|
|
535
|
+
diag.error(field + '.id must be a lowercase kebab id of at most 24 chars')
|
|
536
|
+
continue
|
|
537
|
+
}
|
|
538
|
+
if (seen.has(id)) {
|
|
539
|
+
diag.error(field + ': duplicate skin id ' + JSON.stringify(id))
|
|
540
|
+
continue
|
|
541
|
+
}
|
|
542
|
+
seen.add(id)
|
|
543
|
+
const label = typeof entry.label === 'string' ? entry.label.trim() : ''
|
|
544
|
+
if (label === '' || label.length > 40) {
|
|
545
|
+
diag.error(field + '.label must be a non-empty string of at most 40 chars')
|
|
546
|
+
continue
|
|
547
|
+
}
|
|
548
|
+
const idleTrack = typeof entry.idleTrack === 'string' ? entry.idleTrack : ''
|
|
549
|
+
if (!validTrackName(idleTrack) || tracks[idleTrack] === undefined) {
|
|
550
|
+
diag.error(field + '.idleTrack must name a declared frames2d track')
|
|
551
|
+
continue
|
|
552
|
+
}
|
|
553
|
+
// Optional click actions: each names a declared track and a hit
|
|
554
|
+
// probability; phrases reuse the gameplay phrase constraints.
|
|
555
|
+
let clickActions: PetManifestSkinClickAction[] | undefined
|
|
556
|
+
if (entry.clickActions !== undefined) {
|
|
557
|
+
if (!Array.isArray(entry.clickActions) || entry.clickActions.length === 0 || entry.clickActions.length > FRAMES2D_MAX_SKIN_CLICKS) {
|
|
558
|
+
diag.error(field + '.clickActions must be an array of 1..' + FRAMES2D_MAX_SKIN_CLICKS + ' actions')
|
|
559
|
+
} else {
|
|
560
|
+
const resolved: PetManifestSkinClickAction[] = []
|
|
561
|
+
for (const [cIndex, action] of entry.clickActions.entries()) {
|
|
562
|
+
const cField = field + '.clickActions[' + cIndex + ']'
|
|
563
|
+
if (!isRecord(action)) {
|
|
564
|
+
diag.error(cField + ' must be an object')
|
|
565
|
+
continue
|
|
566
|
+
}
|
|
567
|
+
const cExtra = unknownKeys(action, KNOWN_SKIN_CLICK)
|
|
568
|
+
if (cExtra.length > 0) diag.error(cField + ': unknown field(s) ' + cExtra.map(k => JSON.stringify(k)).join(', '))
|
|
569
|
+
const trackName = typeof action.track === 'string' ? action.track : ''
|
|
570
|
+
if (!validTrackName(trackName) || tracks[trackName] === undefined) {
|
|
571
|
+
diag.error(cField + '.track must name a declared frames2d track')
|
|
572
|
+
continue
|
|
573
|
+
}
|
|
574
|
+
if (typeof action.probability !== 'number' || !Number.isFinite(action.probability)
|
|
575
|
+
|| action.probability <= 0 || action.probability > 1) {
|
|
576
|
+
diag.error(cField + '.probability must be a number in (0, 1]')
|
|
577
|
+
continue
|
|
578
|
+
}
|
|
579
|
+
const phrases = parseSkinPhrases(action.phrases, cField + '.phrases', diag)
|
|
580
|
+
resolved.push({ track: trackName, probability: action.probability, ...(phrases === undefined ? {} : { phrases }) })
|
|
581
|
+
}
|
|
582
|
+
if (resolved.length > 0) clickActions = resolved
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Optional gameplay-track overrides: gameplay state name -> declared
|
|
586
|
+
// track swapped in while this skin is selected (e.g. a skin variant of
|
|
587
|
+
// the sleep loop). State names mirror gameplay blocks (sleep/work...);
|
|
588
|
+
// values must reference declared tracks.
|
|
589
|
+
let gameplayTracks: Record<string, string> | undefined
|
|
590
|
+
if (entry.gameplayTracks !== undefined) {
|
|
591
|
+
if (!isRecord(entry.gameplayTracks)) {
|
|
592
|
+
diag.error(field + '.gameplayTracks must be an object mapping gameplay state names to tracks')
|
|
593
|
+
} else {
|
|
594
|
+
const states = Object.keys(entry.gameplayTracks)
|
|
595
|
+
if (states.length === 0 || states.length > FRAMES2D_MAX_SKIN_GAMEPLAY) {
|
|
596
|
+
diag.error(field + '.gameplayTracks must map 1..' + FRAMES2D_MAX_SKIN_GAMEPLAY + ' gameplay states')
|
|
597
|
+
} else {
|
|
598
|
+
const resolvedTracks: Record<string, string> = {}
|
|
599
|
+
for (const state of states) {
|
|
600
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(state)) {
|
|
601
|
+
diag.error(field + '.gameplayTracks key ' + JSON.stringify(state) + ' must be a lowercase kebab state name')
|
|
602
|
+
continue
|
|
603
|
+
}
|
|
604
|
+
const gTrack = entry.gameplayTracks[state as keyof typeof entry.gameplayTracks]
|
|
605
|
+
if (typeof gTrack !== 'string' || !validTrackName(gTrack) || tracks[gTrack] === undefined) {
|
|
606
|
+
diag.error(field + '.gameplayTracks[' + JSON.stringify(state) + '] must name a declared frames2d track')
|
|
607
|
+
continue
|
|
608
|
+
}
|
|
609
|
+
resolvedTracks[state] = gTrack
|
|
610
|
+
}
|
|
611
|
+
if (Object.keys(resolvedTracks).length > 0) gameplayTracks = resolvedTracks
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
skins.push({ id, label, idleTrack, ...(clickActions === undefined ? {} : { clickActions }), ...(gameplayTracks === undefined ? {} : { gameplayTracks }) })
|
|
616
|
+
}
|
|
617
|
+
if (skins.length > 0) block.skins = skins
|
|
618
|
+
}
|
|
619
|
+
}
|
|
454
620
|
return diag.hasErrors ? undefined : block
|
|
455
621
|
}
|
|
456
622
|
|
package/src/persist.ts
CHANGED
|
@@ -36,7 +36,7 @@ export const defaultDisplayConfig: PetDisplayConfig = {
|
|
|
36
36
|
|
|
37
37
|
/** Display value bounds (shared by load-time validation and setConfig). */
|
|
38
38
|
export const DISPLAY_SIZE_MIN = 32
|
|
39
|
-
export const DISPLAY_SIZE_MAX =
|
|
39
|
+
export const DISPLAY_SIZE_MAX = 1024
|
|
40
40
|
export const DISPLAY_INSET_MAX = 10_000
|
|
41
41
|
|
|
42
42
|
/** Everything persisted for the pet. */
|
package/src/registry.test.ts
CHANGED
|
@@ -314,18 +314,32 @@ describe('loadPetRegistry', () => {
|
|
|
314
314
|
dshPetsDir: '',
|
|
315
315
|
})
|
|
316
316
|
|
|
317
|
-
// The repo checkout also resolves miku (frames2d gameplay pet)
|
|
318
|
-
// starry-doll (community sprite2d pet) from
|
|
319
|
-
// whitelist excludes them (Workshop delivery), so
|
|
320
|
-
//
|
|
321
|
-
// $DSH_HOME/pets.
|
|
317
|
+
// The repo checkout also resolves miku (frames2d gameplay pet), jyn
|
|
318
|
+
// (frames2d gameplay pet) and starry-doll (community sprite2d pet) from
|
|
319
|
+
// assets/; the npm files whitelist excludes them (Workshop delivery), so
|
|
320
|
+
// npm installs see the atlas pets until a Workshop install lands them
|
|
321
|
+
// under $DSH_HOME/pets. blue-throated-bee-eater ships bundled alongside
|
|
322
|
+
// the other atlas pets.
|
|
322
323
|
expect(registry.entries.map(entry => entry.id)).toEqual([
|
|
324
|
+
'blue-throated-bee-eater',
|
|
325
|
+
'jyn',
|
|
323
326
|
'miku',
|
|
324
327
|
'ouo-neko',
|
|
325
328
|
'starry-doll',
|
|
326
329
|
'whale-girl',
|
|
327
330
|
'whale-girl-refined',
|
|
328
331
|
])
|
|
332
|
+
expect(registry.byId('blue-throated-bee-eater')).toMatchObject({
|
|
333
|
+
displayName: '蓝喉蜂虎',
|
|
334
|
+
atlasRows: 9,
|
|
335
|
+
columns: 8,
|
|
336
|
+
rows: [6, 8, 8, 4, 5, 8, 6, 6, 6],
|
|
337
|
+
})
|
|
338
|
+
expect(existsSync(petAtlasFile(registry.byId('blue-throated-bee-eater')!))).toBe(true)
|
|
339
|
+
expect(registry.byId('jyn')).toMatchObject({
|
|
340
|
+
displayName: '女仆鲸鱼娘',
|
|
341
|
+
renderer: 'frames2d',
|
|
342
|
+
})
|
|
329
343
|
expect(registry.byId('ouo-neko')).toMatchObject({
|
|
330
344
|
displayName: 'OUO Neko',
|
|
331
345
|
atlasRows: 11,
|
package/src/registry.ts
CHANGED
|
@@ -215,6 +215,30 @@ export interface PetFrames2dDefinition {
|
|
|
215
215
|
tracks: Record<string, PetFrames2dTrackView>
|
|
216
216
|
/** ActivityPhase -> track id; unmapped phases fall back to idle. */
|
|
217
217
|
phases: Partial<Record<ActivityPhase, string>> & { idle: string }
|
|
218
|
+
/** Selectable skins; each swaps the base idle target to its idleTrack. */
|
|
219
|
+
skins?: PetSkinDefinition[]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** One selectable frames2d skin as served to the browser half. */
|
|
223
|
+
export interface PetSkinDefinition {
|
|
224
|
+
id: string
|
|
225
|
+
label: string
|
|
226
|
+
/** A declared looping track that becomes the base idle while selected. */
|
|
227
|
+
idleTrack: string
|
|
228
|
+
/** Click actions exclusive to this skin (roll by probability; miss → touch zones). */
|
|
229
|
+
clickActions?: PetSkinClickActionDefinition[]
|
|
230
|
+
/** Gameplay-state overrides (state -> track) swapped in while selected. */
|
|
231
|
+
gameplayTracks?: Record<string, string>
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** One probability-rolled tap action a skin may declare. */
|
|
235
|
+
export interface PetSkinClickActionDefinition {
|
|
236
|
+
/** A declared track played once on hit. */
|
|
237
|
+
track: string
|
|
238
|
+
/** Hit probability (0..1). */
|
|
239
|
+
probability: number
|
|
240
|
+
/** Optional lines spoken when the action fires. */
|
|
241
|
+
phrases?: string[]
|
|
218
242
|
}
|
|
219
243
|
|
|
220
244
|
/** A normalized pet as served to the browser half. */
|
|
@@ -719,6 +743,47 @@ function resolveFrames2dEntry(
|
|
|
719
743
|
phases[phase as ActivityPhase] = idleTrack
|
|
720
744
|
}
|
|
721
745
|
}
|
|
746
|
+
// Skins: keep only entries whose idleTrack survived track resolution, and
|
|
747
|
+
// drop click actions whose track did not (the skin itself stays usable).
|
|
748
|
+
let skins: PetSkinDefinition[] | undefined
|
|
749
|
+
if (block.skins !== undefined) {
|
|
750
|
+
const resolved: PetSkinDefinition[] = []
|
|
751
|
+
for (const skin of block.skins) {
|
|
752
|
+
if (tracks[skin.idleTrack] === undefined) {
|
|
753
|
+
record('warning', 'pet ' + manifest.id + ': frames2d skin ' + JSON.stringify(skin.id) + ' idleTrack dropped; skipping')
|
|
754
|
+
continue
|
|
755
|
+
}
|
|
756
|
+
let clickActions: PetSkinClickActionDefinition[] | undefined
|
|
757
|
+
if (skin.clickActions !== undefined) {
|
|
758
|
+
const kept: PetSkinClickActionDefinition[] = []
|
|
759
|
+
for (const action of skin.clickActions) {
|
|
760
|
+
if (tracks[action.track] === undefined) {
|
|
761
|
+
record('warning', 'pet ' + manifest.id + ': frames2d skin ' + JSON.stringify(skin.id)
|
|
762
|
+
+ ' click action track ' + JSON.stringify(action.track) + ' dropped')
|
|
763
|
+
continue
|
|
764
|
+
}
|
|
765
|
+
kept.push(action)
|
|
766
|
+
}
|
|
767
|
+
if (kept.length > 0) clickActions = kept
|
|
768
|
+
}
|
|
769
|
+
// Gameplay track overrides: drop entries whose track did not survive.
|
|
770
|
+
let gameplayTracks: Record<string, string> | undefined
|
|
771
|
+
if (skin.gameplayTracks !== undefined) {
|
|
772
|
+
const kept: Record<string, string> = {}
|
|
773
|
+
for (const [state, gTrack] of Object.entries(skin.gameplayTracks)) {
|
|
774
|
+
if (tracks[gTrack] === undefined) {
|
|
775
|
+
record('warning', 'pet ' + manifest.id + ': frames2d skin ' + JSON.stringify(skin.id)
|
|
776
|
+
+ ' gameplay track override ' + JSON.stringify(state) + ' -> ' + JSON.stringify(gTrack) + ' dropped')
|
|
777
|
+
continue
|
|
778
|
+
}
|
|
779
|
+
kept[state] = gTrack
|
|
780
|
+
}
|
|
781
|
+
if (Object.keys(kept).length > 0) gameplayTracks = kept
|
|
782
|
+
}
|
|
783
|
+
resolved.push({ ...skin, ...(clickActions === undefined ? {} : { clickActions }), ...(gameplayTracks === undefined ? {} : { gameplayTracks }) })
|
|
784
|
+
}
|
|
785
|
+
if (resolved.length > 0) skins = resolved
|
|
786
|
+
}
|
|
722
787
|
const remarks = normalizePetRemarks(manifest.remarks, message => record('warning', 'pet ' + manifest.id + ': ' + message))
|
|
723
788
|
// Gameplay layer: shop item icons are manifest-relative frame paths —
|
|
724
789
|
// verify them, promote them to browser URLs, and add them to the servable
|
|
@@ -756,7 +821,7 @@ function resolveFrames2dEntry(
|
|
|
756
821
|
displayName: manifest.displayName,
|
|
757
822
|
description: manifest.description ?? '',
|
|
758
823
|
renderer: 'frames2d' as const,
|
|
759
|
-
frames2d: { tracks, phases },
|
|
824
|
+
frames2d: { tracks, phases, ...(skins === undefined ? {} : { skins }) },
|
|
760
825
|
...(gameplay === undefined ? {} : { gameplay }),
|
|
761
826
|
cell,
|
|
762
827
|
columns: DEFAULT_PET_COLUMNS,
|