@linxin666/dsh-pet 0.3.3 → 0.3.4
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 +17 -3
- package/README.zh.md +17 -3
- package/assets/ouo-neko/pet.json +31 -0
- package/assets/ouo-neko/previews/failed.gif +0 -0
- package/assets/ouo-neko/previews/idle.gif +0 -0
- package/assets/ouo-neko/previews/jumping.gif +0 -0
- package/assets/ouo-neko/previews/review.gif +0 -0
- package/assets/ouo-neko/previews/running-left.gif +0 -0
- package/assets/ouo-neko/previews/running-right.gif +0 -0
- package/assets/ouo-neko/previews/running.gif +0 -0
- package/assets/ouo-neko/previews/waiting.gif +0 -0
- package/assets/ouo-neko/previews/waving.gif +0 -0
- package/assets/ouo-neko/spritesheet.webp +0 -0
- package/contracts/pet-manifest-v2.schema.json +102 -1
- package/lib/client.js +759 -26
- package/lib/client.js.map +1 -1
- package/lib/index.js +1224 -22
- package/lib/types/client/PetDockEntry.d.ts +3 -0
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +18 -2
- package/lib/types/client/PetSprite.d.ts +23 -0
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +34 -29
- package/lib/types/client/PluginSettingsCard.d.ts +2 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/drag-stream.d.ts +19 -0
- package/lib/types/client/drag-stream.d.ts.map +1 -0
- package/lib/types/client/drag-stream.js +26 -0
- package/lib/types/client/gameplay-hud.d.ts +50 -0
- package/lib/types/client/gameplay-hud.d.ts.map +1 -0
- package/lib/types/client/gameplay-hud.js +274 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +12 -0
- package/lib/types/client/locales.d.ts +34 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -0
- package/lib/types/client/pet-store.d.ts +7 -1
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/renderers/Frames2dVisualMount.d.ts +29 -0
- package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -0
- package/lib/types/client/renderers/Frames2dVisualMount.js +83 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts +6 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -1
- package/lib/types/client/renderers/PetRendererSwitch.js +14 -1
- package/lib/types/client/renderers/frames2d.d.ts +36 -0
- package/lib/types/client/renderers/frames2d.d.ts.map +1 -0
- package/lib/types/client/renderers/frames2d.js +196 -0
- package/lib/types/defaults.d.ts +6 -0
- package/lib/types/defaults.d.ts.map +1 -0
- package/lib/types/defaults.js +5 -0
- package/lib/types/dsh-home.d.ts.map +1 -1
- package/lib/types/dsh-home.js +9 -3
- package/lib/types/gameplay.d.ts +182 -0
- package/lib/types/gameplay.d.ts.map +1 -0
- package/lib/types/gameplay.js +618 -0
- package/lib/types/ledger.d.ts +14 -0
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +30 -0
- package/lib/types/manifest-v2.d.ts +38 -1
- package/lib/types/manifest-v2.d.ts.map +1 -1
- package/lib/types/manifest-v2.js +182 -2
- package/lib/types/persist.d.ts +4 -4
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +41 -4
- package/lib/types/registry.d.ts +29 -0
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +170 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +22 -0
- package/lib/types/service.d.ts +56 -0
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +181 -0
- package/package.json +5 -2
- package/src/client/PetDockEntry.test.tsx +6 -0
- package/src/client/PetDockEntry.tsx +32 -1
- package/src/client/PetSprite.test.tsx +36 -6
- package/src/client/PetSprite.tsx +42 -21
- package/src/client/PluginSettingsCard.tsx +4 -2
- package/src/client/drag-stream.ts +35 -0
- package/src/client/gameplay-hud.test.tsx +338 -0
- package/src/client/gameplay-hud.tsx +403 -0
- package/src/client/index.ts +17 -1
- package/src/client/locales.ts +36 -0
- package/src/client/pet-store.ts +10 -1
- package/src/client/pet.module.css +192 -29
- package/src/client/renderers/Frames2dVisualMount.tsx +111 -0
- package/src/client/renderers/PetRendererSwitch.tsx +28 -1
- package/src/client/renderers/frames2d.test.ts +137 -0
- package/src/client/renderers/frames2d.ts +216 -0
- package/src/client/settings-card.module.css +5 -0
- package/src/defaults.ts +7 -0
- package/src/dsh-home.ts +9 -3
- package/src/gameplay.test.ts +239 -0
- package/src/gameplay.ts +692 -0
- package/src/ledger.test.ts +16 -0
- package/src/ledger.ts +31 -0
- package/src/manifest-v2.test.ts +130 -1
- package/src/manifest-v2.ts +181 -2
- package/src/persist.test.ts +3 -0
- package/src/persist.ts +43 -6
- package/src/registry.test.ts +168 -1
- package/src/registry.ts +199 -1
- package/src/routes.ts +19 -0
- package/src/service.ts +208 -0
package/lib/types/registry.js
CHANGED
|
@@ -38,6 +38,7 @@ import { PET_DECORATION_API_VERSION } from "./contracts/status-decoration.js";
|
|
|
38
38
|
import { dshHome } from "./dsh-home.js";
|
|
39
39
|
import { parsePetManifest } from "./manifest-v2.js";
|
|
40
40
|
import { collectModel3References } from "./model3.js";
|
|
41
|
+
import { DEFAULT_PET_ID } from "./defaults.js";
|
|
41
42
|
/** Fixed row order of the 9-state animation contract. */
|
|
42
43
|
export const PET_ROW_ORDER = [
|
|
43
44
|
'idle',
|
|
@@ -367,6 +368,169 @@ function resolveLive2dEntry(manifest, dir, options) {
|
|
|
367
368
|
...(remarks === undefined ? {} : { remarks }),
|
|
368
369
|
};
|
|
369
370
|
}
|
|
371
|
+
/** Filename-encoded frame duration tail ('<base>_<index>_<ms>.webp'). */
|
|
372
|
+
const FRAMES2D_FILENAME_MS = /_(\d+)\.[^.]+$/;
|
|
373
|
+
/** Trailing frame index, allowing an optional '_<ms>' duration tail after it. */
|
|
374
|
+
const FRAMES2D_FRAME_INDEX = /(\d+)(?:_\d+)?\.[^.]+$/;
|
|
375
|
+
/** Default per-frame duration when neither frameMs nor the filename encodes one. */
|
|
376
|
+
const FRAMES2D_DEFAULT_FRAME_MS = 200;
|
|
377
|
+
/** Image extensions a frames2d track directory may list. */
|
|
378
|
+
const FRAMES2D_IMAGE_EXTENSIONS = new Set(['.webp', '.png', '.gif', '.jpg', '.jpeg']);
|
|
379
|
+
/**
|
|
380
|
+
* Resolve a validated frames2d manifest into a renderable entry. Track frame
|
|
381
|
+
* lists come from the manifest's explicit list or from listing
|
|
382
|
+
* '<dir>/<track>/' in filename order; durations resolve frameMs[i] >
|
|
383
|
+
* filename-encoded '_<ms>' tail > defaultFrameMs. Missing frames warn and
|
|
384
|
+
* skip (the live2d closure discipline); a track left with zero frames is
|
|
385
|
+
* dropped, and if the idle-mapped track ends up empty the entry is rejected
|
|
386
|
+
* fail-closed. The sprite fields carry contract defaults: the chrome sizes
|
|
387
|
+
* frames2d pets off 'display.size', not the atlas.
|
|
388
|
+
*/
|
|
389
|
+
function resolveFrames2dEntry(manifest, dir, options) {
|
|
390
|
+
const assetPrefix = options.assetPrefix ?? '/pet';
|
|
391
|
+
const record = (level, message) => {
|
|
392
|
+
options.diagnostics?.push({ level, source: dir, message });
|
|
393
|
+
options.warnings?.push(message);
|
|
394
|
+
};
|
|
395
|
+
const block = manifest.frames2d;
|
|
396
|
+
if (block === undefined) {
|
|
397
|
+
record('error', 'pet ' + manifest.id + ': renderer frames2d requires a frames2d block');
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
const root = block.dir ?? '.';
|
|
401
|
+
const defaultMs = block.defaultFrameMs ?? FRAMES2D_DEFAULT_FRAME_MS;
|
|
402
|
+
const idleTrack = block.phases.idle;
|
|
403
|
+
const tracks = {};
|
|
404
|
+
const servable = [];
|
|
405
|
+
const firstFrameRel = {};
|
|
406
|
+
for (const [name, track] of Object.entries(block.tracks)) {
|
|
407
|
+
const trackDir = root === '.' ? name : root + '/' + name;
|
|
408
|
+
let relFrames = [];
|
|
409
|
+
if (track.frames !== undefined) {
|
|
410
|
+
for (const frame of track.frames) {
|
|
411
|
+
const rel = trackDir + '/' + frame;
|
|
412
|
+
if (!existsSync(join(dir, rel))) {
|
|
413
|
+
record('warning', 'pet ' + manifest.id + ': frames2d frame missing: ' + rel);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
relFrames.push(rel);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
let files = [];
|
|
421
|
+
try {
|
|
422
|
+
files = readdirSync(join(dir, trackDir)).filter(file => {
|
|
423
|
+
if (file.startsWith('.'))
|
|
424
|
+
return false;
|
|
425
|
+
const dot = file.lastIndexOf('.');
|
|
426
|
+
return dot > 0 && FRAMES2D_IMAGE_EXTENSIONS.has(file.slice(dot).toLowerCase());
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
files = [];
|
|
431
|
+
}
|
|
432
|
+
// Natural frame order: the trailing index (before any _<ms> duration
|
|
433
|
+
// suffix) sorts numerically, so 'eat10' comes after 'eat9' — plain
|
|
434
|
+
// lexicographic order would break any track with 10+ frames.
|
|
435
|
+
files.sort((a, b) => {
|
|
436
|
+
const ia = FRAMES2D_FRAME_INDEX.exec(a)?.[1];
|
|
437
|
+
const ib = FRAMES2D_FRAME_INDEX.exec(b)?.[1];
|
|
438
|
+
if (ia !== undefined && ib !== undefined && ia !== ib)
|
|
439
|
+
return Number(ia) - Number(ib);
|
|
440
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
441
|
+
});
|
|
442
|
+
relFrames = files.map(file => trackDir + '/' + file);
|
|
443
|
+
}
|
|
444
|
+
if (relFrames.length === 0) {
|
|
445
|
+
record('warning', 'pet ' + manifest.id + ': frames2d track ' + JSON.stringify(name) + ' has no frames on disk; dropped');
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const durations = relFrames.map((rel, index) => {
|
|
449
|
+
if (track.frameMs !== undefined)
|
|
450
|
+
return track.frameMs[index] ?? defaultMs;
|
|
451
|
+
const match = FRAMES2D_FILENAME_MS.exec(rel);
|
|
452
|
+
if (match !== null) {
|
|
453
|
+
const ms = Number(match[1]);
|
|
454
|
+
if (Number.isInteger(ms) && ms >= 16 && ms <= 5000)
|
|
455
|
+
return ms;
|
|
456
|
+
}
|
|
457
|
+
return defaultMs;
|
|
458
|
+
});
|
|
459
|
+
const loop = track.loop ?? true;
|
|
460
|
+
const view = {
|
|
461
|
+
frames: relFrames.map(rel => assetUrl(assetPrefix, manifest.id, rel)),
|
|
462
|
+
durations,
|
|
463
|
+
loop,
|
|
464
|
+
...(loop ? {} : { fallback: track.fallback ?? idleTrack }),
|
|
465
|
+
};
|
|
466
|
+
tracks[name] = view;
|
|
467
|
+
firstFrameRel[name] = relFrames[0];
|
|
468
|
+
servable.push(...relFrames);
|
|
469
|
+
}
|
|
470
|
+
if (tracks[idleTrack] === undefined) {
|
|
471
|
+
record('error', 'pet ' + manifest.id + ': frames2d idle track ' + JSON.stringify(idleTrack) + ' has no frames on disk');
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
const phases = { ...block.phases };
|
|
475
|
+
for (const [phase, target] of Object.entries(phases)) {
|
|
476
|
+
if (target !== undefined && tracks[target] === undefined) {
|
|
477
|
+
record('warning', 'pet ' + manifest.id + ': frames2d phase ' + phase + ' maps to dropped track ' + JSON.stringify(target) + '; using idle');
|
|
478
|
+
phases[phase] = idleTrack;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const remarks = normalizePetRemarks(manifest.remarks, message => record('warning', 'pet ' + manifest.id + ': ' + message));
|
|
482
|
+
// Gameplay layer: shop item icons are manifest-relative frame paths —
|
|
483
|
+
// verify them, promote them to browser URLs, and add them to the servable
|
|
484
|
+
// allow-list so the asset route can serve them.
|
|
485
|
+
let gameplay;
|
|
486
|
+
if (manifest.gameplay !== undefined) {
|
|
487
|
+
gameplay = manifest.gameplay;
|
|
488
|
+
if (gameplay.shop !== undefined) {
|
|
489
|
+
const items = [];
|
|
490
|
+
for (const item of gameplay.shop.items) {
|
|
491
|
+
if (item.image === undefined) {
|
|
492
|
+
items.push(item);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (!existsSync(join(dir, item.image))) {
|
|
496
|
+
record('warning', 'pet ' + manifest.id + ': gameplay shop item ' + item.id + ' image missing: ' + item.image);
|
|
497
|
+
const { image, ...rest } = item;
|
|
498
|
+
items.push(rest);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
servable.push(item.image);
|
|
502
|
+
items.push({ ...item, image: assetUrl(assetPrefix, manifest.id, item.image) });
|
|
503
|
+
}
|
|
504
|
+
gameplay = { ...gameplay, shop: { ...gameplay.shop, items } };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const flatTracks = buildTracks(DEFAULT_FRAME_COUNTS, DEFAULT_PET_COLUMNS, {}, message => record('warning', 'pet ' + manifest.id + ': ' + message));
|
|
508
|
+
if (flatTracks === undefined)
|
|
509
|
+
return undefined;
|
|
510
|
+
// The render box comes from the first idle frame when decodable.
|
|
511
|
+
const firstAbs = join(dir, firstFrameRel[idleTrack]);
|
|
512
|
+
const dims = existsSync(firstAbs) ? readImageDimensions(firstAbs) : undefined;
|
|
513
|
+
const cell = dims !== undefined && dims.width >= 1 && dims.height >= 1 ? dims : { ...DEFAULT_PET_CELL };
|
|
514
|
+
return {
|
|
515
|
+
id: manifest.id,
|
|
516
|
+
displayName: manifest.displayName,
|
|
517
|
+
description: manifest.description ?? '',
|
|
518
|
+
renderer: 'frames2d',
|
|
519
|
+
frames2d: { tracks, phases },
|
|
520
|
+
...(gameplay === undefined ? {} : { gameplay }),
|
|
521
|
+
cell,
|
|
522
|
+
columns: DEFAULT_PET_COLUMNS,
|
|
523
|
+
rows: [...DEFAULT_FRAME_COUNTS],
|
|
524
|
+
atlasRows: DEFAULT_PET_ROW_COUNT,
|
|
525
|
+
tracks: flatTracks,
|
|
526
|
+
atlasUrl: tracks[idleTrack].frames[0],
|
|
527
|
+
manifestUrl: assetUrl(assetPrefix, manifest.id, 'pet.json'),
|
|
528
|
+
dir,
|
|
529
|
+
spritesheetPath: firstFrameRel[idleTrack],
|
|
530
|
+
servable,
|
|
531
|
+
...(remarks === undefined ? {} : { remarks }),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
370
534
|
/** Scan one directory of pet folders; entries come back in name order. */
|
|
371
535
|
function scanPetDir(dir, options) {
|
|
372
536
|
if (!existsSync(dir))
|
|
@@ -399,6 +563,9 @@ function scanPetDir(dir, options) {
|
|
|
399
563
|
if (verdict.manifest.renderer === 'live2d') {
|
|
400
564
|
entry = resolveLive2dEntry(verdict.manifest, entryDir, options);
|
|
401
565
|
}
|
|
566
|
+
else if (verdict.manifest.renderer === 'frames2d') {
|
|
567
|
+
entry = resolveFrames2dEntry(verdict.manifest, entryDir, options);
|
|
568
|
+
}
|
|
402
569
|
else {
|
|
403
570
|
const legacy = flattenV2Sprite2d(verdict.manifest);
|
|
404
571
|
if (legacy === undefined) {
|
|
@@ -695,7 +862,7 @@ export function loadPetRegistry(options) {
|
|
|
695
862
|
warnings,
|
|
696
863
|
diagnostics,
|
|
697
864
|
byId: (id) => byId.get(id),
|
|
698
|
-
defaultEntry: () => entries.find(entry => builtinIds.has(entry.id)) ?? entries[0],
|
|
865
|
+
defaultEntry: () => entries.find(entry => (entry.id === DEFAULT_PET_ID && builtinIds.has(entry.id))) ?? entries.find(entry => builtinIds.has(entry.id)) ?? entries[0],
|
|
699
866
|
...(globalVoice === undefined ? {} : { globalVoice }),
|
|
700
867
|
decorations,
|
|
701
868
|
decorationById: (id) => decorationById.get(id),
|
|
@@ -733,6 +900,8 @@ export function petEntryView(entry, globalVoice) {
|
|
|
733
900
|
description: entry.description,
|
|
734
901
|
renderer: entry.renderer,
|
|
735
902
|
...(entry.live2d === undefined ? {} : { live2d: entry.live2d }),
|
|
903
|
+
...(entry.frames2d === undefined ? {} : { frames2d: entry.frames2d }),
|
|
904
|
+
...(entry.gameplay === undefined ? {} : { gameplay: entry.gameplay }),
|
|
736
905
|
cell: entry.cell,
|
|
737
906
|
columns: entry.columns,
|
|
738
907
|
rows: entry.rows,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAO9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;;IAEzB,iDAAiD;;IAEjD,8EAA8E;;CAEtE,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAWD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUrF;AAwND,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA8B,CAAA;AAe7D,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwMD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAO9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;;IAEzB,iDAAiD;;IAEjD,8EAA8E;;CAEtE,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAWD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUrF;AAwND,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA8B,CAAA;AAe7D,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwMD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CA2EjI;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
|
package/lib/types/routes.js
CHANGED
|
@@ -539,6 +539,28 @@ export function makePetRoutes(deps) {
|
|
|
539
539
|
return Promise.reject(new Error('invalid-pet'));
|
|
540
540
|
return service.setPetId(petId);
|
|
541
541
|
}),
|
|
542
|
+
// Gameplay verbs (miku-pet generalization): touch rolls a named zone's
|
|
543
|
+
// branch; the omitted-zone form is the plain-click boost during a touch
|
|
544
|
+
// animation. Mode/tick/buy drive the work, sleep and shop loops.
|
|
545
|
+
postRoute(ctx, PET_API_PREFIX + '/gameplay/touch', (body) => {
|
|
546
|
+
const zone = body.zone;
|
|
547
|
+
if (zone !== undefined && typeof zone !== 'string')
|
|
548
|
+
return Promise.reject(new Error('invalid-zone'));
|
|
549
|
+
return service.gameplayTouch(zone);
|
|
550
|
+
}),
|
|
551
|
+
postRoute(ctx, PET_API_PREFIX + '/gameplay/mode', (body) => {
|
|
552
|
+
const mode = body.mode;
|
|
553
|
+
if (mode !== null && mode !== 'work' && mode !== 'sleep')
|
|
554
|
+
return Promise.reject(new Error('invalid-mode'));
|
|
555
|
+
return service.gameplaySetMode(mode);
|
|
556
|
+
}),
|
|
557
|
+
postRoute(ctx, PET_API_PREFIX + '/gameplay/work-tick', () => service.gameplayWorkTick()),
|
|
558
|
+
postRoute(ctx, PET_API_PREFIX + '/gameplay/buy', (body) => {
|
|
559
|
+
const item = body.item;
|
|
560
|
+
if (typeof item !== 'string')
|
|
561
|
+
return Promise.reject(new Error('invalid-item'));
|
|
562
|
+
return service.gameplayBuy(item);
|
|
563
|
+
}),
|
|
542
564
|
];
|
|
543
565
|
const assetRoute = {
|
|
544
566
|
kind: 'prefix',
|
package/lib/types/service.d.ts
CHANGED
|
@@ -133,9 +133,39 @@ export interface PetStateView {
|
|
|
133
133
|
* browser half renders no ornament.
|
|
134
134
|
*/
|
|
135
135
|
decoration?: DecorationView;
|
|
136
|
+
/**
|
|
137
|
+
* The selected pet's gameplay view (miku-pet generalization), present
|
|
138
|
+
* when the pet declares a gameplay block. Read-only projection: polling
|
|
139
|
+
* settles the view in memory but never writes pet.json.
|
|
140
|
+
*/
|
|
141
|
+
gameplay?: PetGameplayStateView;
|
|
136
142
|
}
|
|
137
143
|
/** Result of `pet.interact`. */
|
|
138
144
|
export type PetInteractResult = LedgerInteractionResult;
|
|
145
|
+
/** The gameplay slice of the state view (dynamic state only; defs ride the pet definition). */
|
|
146
|
+
export interface PetGameplayStateView {
|
|
147
|
+
/** Stat values rounded for display. */
|
|
148
|
+
stats: Record<string, number>;
|
|
149
|
+
mode: 'work' | 'sleep' | null;
|
|
150
|
+
}
|
|
151
|
+
/** Result of the gameplay verbs (touch / setMode / workTick / buy). */
|
|
152
|
+
export interface PetGameplayVerbResult {
|
|
153
|
+
ok: boolean;
|
|
154
|
+
error?: string;
|
|
155
|
+
/** Touch: whether a branch hit, plus its presentation. */
|
|
156
|
+
hit?: boolean;
|
|
157
|
+
state?: string;
|
|
158
|
+
stateMs?: number;
|
|
159
|
+
phrase?: string;
|
|
160
|
+
/** Work tick outcome. */
|
|
161
|
+
outcome?: 'success' | 'fail';
|
|
162
|
+
/** Shop purchase: the drawn prize, when the item was a lottery. */
|
|
163
|
+
prize?: {
|
|
164
|
+
amount: number;
|
|
165
|
+
currency: string;
|
|
166
|
+
};
|
|
167
|
+
view?: PetGameplayStateView;
|
|
168
|
+
}
|
|
139
169
|
declare module '@deepseek-ai/cordis' {
|
|
140
170
|
interface Context {
|
|
141
171
|
pet: PetService;
|
|
@@ -233,6 +263,32 @@ export declare class PetService extends Service {
|
|
|
233
263
|
private applyActivity;
|
|
234
264
|
/** RPC: pet or feed the pet. */
|
|
235
265
|
interact(kind: PetInteraction): Promise<PetInteractResult>;
|
|
266
|
+
/** The active pet's gameplay block, if it declares one. */
|
|
267
|
+
private gameplayDef;
|
|
268
|
+
/** The persisted (or fresh) gameplay state of the selected pet. */
|
|
269
|
+
private gameplayState;
|
|
270
|
+
/** Display view of one gameplay state (rounded stats; treats ride the shared treat ledger). */
|
|
271
|
+
private gameplayViewOf;
|
|
272
|
+
/**
|
|
273
|
+
* Move gameplay 'treats' currency (the unified post-wallet currency) from
|
|
274
|
+
* the engine's settle work area into the shared treat ledger, capped by
|
|
275
|
+
* the stock cap. The engine keeps its generic currency record for settle
|
|
276
|
+
* math; this drain is the only bridge to the wallet-free economy.
|
|
277
|
+
*/
|
|
278
|
+
private drainGameplayTreats;
|
|
279
|
+
/** Persist the mutated gameplay state of one verb call. */
|
|
280
|
+
private commitGameplay;
|
|
281
|
+
/**
|
|
282
|
+
* RPC: a touch on the pet. 'zone' names a touch zone (roll a branch);
|
|
283
|
+
* omitted means a plain click while a touch animation holds (clickBoost).
|
|
284
|
+
*/
|
|
285
|
+
gameplayTouch(zone?: string): Promise<PetGameplayVerbResult>;
|
|
286
|
+
/** RPC: enter or leave a gameplay mode ('work' | 'sleep' | null). */
|
|
287
|
+
gameplaySetMode(mode: 'work' | 'sleep' | null): Promise<PetGameplayVerbResult>;
|
|
288
|
+
/** RPC: one work-round adjudication (only while the work mode holds). */
|
|
289
|
+
gameplayWorkTick(): Promise<PetGameplayVerbResult>;
|
|
290
|
+
/** RPC: buy one shop item (effects, currency swap, or a lottery draw). */
|
|
291
|
+
gameplayBuy(itemId: string): Promise<PetGameplayVerbResult>;
|
|
236
292
|
/** RPC: show or hide the pet. */
|
|
237
293
|
setVisible(visible: boolean): Promise<{
|
|
238
294
|
ok: true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAQ9C,OAAO,EAAgC,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACxF,OAAO,EASL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAA;AACrB,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC3B,MAAM,eAAe,CAAA;AAGtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AACtE,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
|
package/lib/types/service.js
CHANGED
|
@@ -19,6 +19,9 @@ import { DEFAULT_DECORATION_ID, decorationView, loadPetRegistry, petEntryView, p
|
|
|
19
19
|
import { WHISPER_TTL_MS } from "./chatter.js";
|
|
20
20
|
import { mergeVoicePacks } from "./voice-pack.js";
|
|
21
21
|
import { defaultPetStateConfig, PetStateMachine, } from "./state.js";
|
|
22
|
+
import { applyGameplayEffects, drawLotteryTier, initialGameplayState, rollTouchBranch, rollWorkOutcome, settleGameplay, } from "./gameplay.js";
|
|
23
|
+
/** The unified gameplay currency: the shared treat (小鱼干) ledger. */
|
|
24
|
+
const GAMEPLAY_TREATS_CURRENCY = 'treats';
|
|
22
25
|
/** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
|
|
23
26
|
export const PET_SETTINGS_NAMESPACE = 'pet';
|
|
24
27
|
/** Hard cap on simultaneously displayed session bubbles (most recent first). */
|
|
@@ -295,6 +298,174 @@ export class PetService extends Service {
|
|
|
295
298
|
this.flush();
|
|
296
299
|
return result;
|
|
297
300
|
}
|
|
301
|
+
/* ---------------------------------------------------------------- *
|
|
302
|
+
* Gameplay verbs (miku-pet generalization). The manifest block lives
|
|
303
|
+
* on the registry entry; dynamic state persists per pet id. Verbs
|
|
304
|
+
* settle and persist; the state view projects without writing.
|
|
305
|
+
* ---------------------------------------------------------------- */
|
|
306
|
+
/** The active pet's gameplay block, if it declares one. */
|
|
307
|
+
gameplayDef() {
|
|
308
|
+
return this.activeEntry().gameplay;
|
|
309
|
+
}
|
|
310
|
+
/** The persisted (or fresh) gameplay state of the selected pet. */
|
|
311
|
+
gameplayState(def, now) {
|
|
312
|
+
const petId = this.selectedPetId();
|
|
313
|
+
const stored = this.ledger.snapshot.gameplay[petId];
|
|
314
|
+
return {
|
|
315
|
+
petId,
|
|
316
|
+
state: stored === undefined
|
|
317
|
+
? initialGameplayState(def, now)
|
|
318
|
+
: { stats: { ...stored.stats }, currencies: { ...stored.currencies }, mode: stored.mode, settledAt: stored.settledAt },
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/** Display view of one gameplay state (rounded stats; treats ride the shared treat ledger). */
|
|
322
|
+
gameplayViewOf(state) {
|
|
323
|
+
const stats = {};
|
|
324
|
+
for (const [name, value] of Object.entries(state.stats))
|
|
325
|
+
stats[name] = Math.round(value);
|
|
326
|
+
return { stats, mode: state.mode };
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Move gameplay 'treats' currency (the unified post-wallet currency) from
|
|
330
|
+
* the engine's settle work area into the shared treat ledger, capped by
|
|
331
|
+
* the stock cap. The engine keeps its generic currency record for settle
|
|
332
|
+
* math; this drain is the only bridge to the wallet-free economy.
|
|
333
|
+
*/
|
|
334
|
+
drainGameplayTreats(state) {
|
|
335
|
+
const pending = Math.floor(state.currencies[GAMEPLAY_TREATS_CURRENCY] ?? 0);
|
|
336
|
+
delete state.currencies[GAMEPLAY_TREATS_CURRENCY];
|
|
337
|
+
if (pending > 0)
|
|
338
|
+
this.ledger.grantTreats(pending);
|
|
339
|
+
}
|
|
340
|
+
/** Persist the mutated gameplay state of one verb call. */
|
|
341
|
+
commitGameplay(petId, state) {
|
|
342
|
+
this.ledger.setGameplay(petId, state);
|
|
343
|
+
if (this.ledger.takeDirty())
|
|
344
|
+
this.flush();
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* RPC: a touch on the pet. 'zone' names a touch zone (roll a branch);
|
|
348
|
+
* omitted means a plain click while a touch animation holds (clickBoost).
|
|
349
|
+
*/
|
|
350
|
+
async gameplayTouch(zone) {
|
|
351
|
+
const def = this.gameplayDef();
|
|
352
|
+
if (def === undefined)
|
|
353
|
+
return { ok: false, error: 'no-gameplay' };
|
|
354
|
+
const now = Date.now();
|
|
355
|
+
const { petId, state } = this.gameplayState(def, now);
|
|
356
|
+
settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive });
|
|
357
|
+
if (zone === undefined) {
|
|
358
|
+
const boost = def.touch?.clickBoost;
|
|
359
|
+
if (boost === undefined)
|
|
360
|
+
return { ok: false, error: 'no-touch' };
|
|
361
|
+
const amount = boost.min + Math.floor(Math.random() * (boost.max - boost.min + 1));
|
|
362
|
+
if (amount > 0)
|
|
363
|
+
applyGameplayEffects(state, def, [{ stat: boost.stat, amount }]);
|
|
364
|
+
this.drainGameplayTreats(state);
|
|
365
|
+
this.commitGameplay(petId, state);
|
|
366
|
+
return { ok: true, hit: false, view: this.gameplayViewOf(state) };
|
|
367
|
+
}
|
|
368
|
+
const target = def.touch?.zones.find(entry => entry.name === zone);
|
|
369
|
+
if (target === undefined)
|
|
370
|
+
return { ok: false, error: 'unknown-zone' };
|
|
371
|
+
const branch = rollTouchBranch(target, Math.random);
|
|
372
|
+
if (branch === undefined) {
|
|
373
|
+
this.drainGameplayTreats(state);
|
|
374
|
+
this.commitGameplay(petId, state);
|
|
375
|
+
return { ok: true, hit: false, view: this.gameplayViewOf(state) };
|
|
376
|
+
}
|
|
377
|
+
if (branch.effects !== undefined)
|
|
378
|
+
applyGameplayEffects(state, def, branch.effects);
|
|
379
|
+
const phrase = branch.phrases !== undefined && branch.phrases.length > 0
|
|
380
|
+
? branch.phrases[Math.floor(Math.random() * branch.phrases.length)]
|
|
381
|
+
: undefined;
|
|
382
|
+
this.drainGameplayTreats(state);
|
|
383
|
+
this.commitGameplay(petId, state);
|
|
384
|
+
return {
|
|
385
|
+
ok: true,
|
|
386
|
+
hit: true,
|
|
387
|
+
...(branch.state === undefined ? {} : { state: branch.state }),
|
|
388
|
+
...(branch.stateMs === undefined ? {} : { stateMs: branch.stateMs }),
|
|
389
|
+
...(phrase === undefined ? {} : { phrase }),
|
|
390
|
+
view: this.gameplayViewOf(state),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/** RPC: enter or leave a gameplay mode ('work' | 'sleep' | null). */
|
|
394
|
+
async gameplaySetMode(mode) {
|
|
395
|
+
const def = this.gameplayDef();
|
|
396
|
+
if (def === undefined)
|
|
397
|
+
return { ok: false, error: 'no-gameplay' };
|
|
398
|
+
if (mode === 'work' && def.work === undefined)
|
|
399
|
+
return { ok: false, error: 'no-work' };
|
|
400
|
+
if (mode === 'sleep' && def.sleep === undefined)
|
|
401
|
+
return { ok: false, error: 'no-sleep' };
|
|
402
|
+
const now = Date.now();
|
|
403
|
+
const { petId, state } = this.gameplayState(def, now);
|
|
404
|
+
settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive });
|
|
405
|
+
state.mode = mode;
|
|
406
|
+
this.drainGameplayTreats(state);
|
|
407
|
+
this.commitGameplay(petId, state);
|
|
408
|
+
return { ok: true, view: this.gameplayViewOf(state) };
|
|
409
|
+
}
|
|
410
|
+
/** RPC: one work-round adjudication (only while the work mode holds). */
|
|
411
|
+
async gameplayWorkTick() {
|
|
412
|
+
const def = this.gameplayDef();
|
|
413
|
+
if (def?.work === undefined)
|
|
414
|
+
return { ok: false, error: 'no-work' };
|
|
415
|
+
const now = Date.now();
|
|
416
|
+
const { petId, state } = this.gameplayState(def, now);
|
|
417
|
+
if (state.mode !== 'work')
|
|
418
|
+
return { ok: false, error: 'not-working' };
|
|
419
|
+
settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive });
|
|
420
|
+
const outcome = rollWorkOutcome(def.work, Math.random);
|
|
421
|
+
const effects = outcome === 'success' ? def.work.success?.effects : def.work.fail?.effects;
|
|
422
|
+
if (effects !== undefined)
|
|
423
|
+
applyGameplayEffects(state, def, effects);
|
|
424
|
+
this.drainGameplayTreats(state);
|
|
425
|
+
this.commitGameplay(petId, state);
|
|
426
|
+
return { ok: true, outcome, view: this.gameplayViewOf(state) };
|
|
427
|
+
}
|
|
428
|
+
/** RPC: buy one shop item (effects, currency swap, or a lottery draw). */
|
|
429
|
+
async gameplayBuy(itemId) {
|
|
430
|
+
const def = this.gameplayDef();
|
|
431
|
+
if (def === undefined)
|
|
432
|
+
return { ok: false, error: 'no-gameplay' };
|
|
433
|
+
const item = def.shop?.items.find(entry => entry.id === itemId);
|
|
434
|
+
if (item === undefined)
|
|
435
|
+
return { ok: false, error: 'unknown-item' };
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
const { petId, state } = this.gameplayState(def, now);
|
|
438
|
+
settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive });
|
|
439
|
+
// The unified currency is the shared treat ledger (wallet removed): shop
|
|
440
|
+
// prices and prizes ride the same balance the panel shows and feeding
|
|
441
|
+
// consumes, capped by the stock cap.
|
|
442
|
+
const treats = item.currency === GAMEPLAY_TREATS_CURRENCY;
|
|
443
|
+
const balance = treats ? this.ledger.snapshot.treats.treats : (state.currencies[item.currency] ?? 0);
|
|
444
|
+
if (balance < item.price) {
|
|
445
|
+
return { ok: false, error: 'insufficient-funds', view: this.gameplayViewOf(state) };
|
|
446
|
+
}
|
|
447
|
+
if (treats)
|
|
448
|
+
this.ledger.spendTreats(item.price);
|
|
449
|
+
else
|
|
450
|
+
state.currencies[item.currency] = balance - item.price;
|
|
451
|
+
if (item.effects !== undefined)
|
|
452
|
+
applyGameplayEffects(state, def, item.effects);
|
|
453
|
+
let prize;
|
|
454
|
+
if (item.lottery !== undefined) {
|
|
455
|
+
if (item.lottery.effects !== undefined)
|
|
456
|
+
applyGameplayEffects(state, def, item.lottery.effects);
|
|
457
|
+
const tier = drawLotteryTier(item.lottery, Math.random);
|
|
458
|
+
const prizeCurrency = tier.currency ?? item.lottery.currency ?? item.currency;
|
|
459
|
+
if (prizeCurrency === GAMEPLAY_TREATS_CURRENCY)
|
|
460
|
+
this.ledger.grantTreats(tier.prize);
|
|
461
|
+
else
|
|
462
|
+
state.currencies[prizeCurrency] = (state.currencies[prizeCurrency] ?? 0) + tier.prize;
|
|
463
|
+
prize = { amount: tier.prize, currency: prizeCurrency };
|
|
464
|
+
}
|
|
465
|
+
this.drainGameplayTreats(state);
|
|
466
|
+
this.commitGameplay(petId, state);
|
|
467
|
+
return { ok: true, ...(prize === undefined ? {} : { prize }), view: this.gameplayViewOf(state) };
|
|
468
|
+
}
|
|
298
469
|
/** RPC: show or hide the pet. */
|
|
299
470
|
async setVisible(visible) {
|
|
300
471
|
this.ledger.setDisplay({ ...this.ledger.snapshot.display, visible });
|
|
@@ -411,6 +582,15 @@ export class PetService extends Service {
|
|
|
411
582
|
? whisper.text
|
|
412
583
|
: undefined;
|
|
413
584
|
const decoration = this.activeDecoration();
|
|
585
|
+
// Gameplay view: a read-only projection. The settle runs on a copy, so
|
|
586
|
+
// polling never writes pet.json (verbs settle and persist).
|
|
587
|
+
const gameplayDef = this.gameplayDef();
|
|
588
|
+
let gameplay;
|
|
589
|
+
if (gameplayDef !== undefined) {
|
|
590
|
+
const { state } = this.gameplayState(gameplayDef, Date.now());
|
|
591
|
+
settleGameplay(state, gameplayDef, Date.now(), { sessionActive: snapshot.sessionActive });
|
|
592
|
+
gameplay = this.gameplayViewOf(state);
|
|
593
|
+
}
|
|
414
594
|
// Read-only: the ledger settles on economic events only, never on a read,
|
|
415
595
|
// so polling the state cannot trigger pet.json writes.
|
|
416
596
|
return {
|
|
@@ -433,6 +613,7 @@ export class PetService extends Service {
|
|
|
433
613
|
stocked: this.ledger.snapshot.treats.treats,
|
|
434
614
|
max: this.ledger.treatMax,
|
|
435
615
|
},
|
|
616
|
+
...(gameplay === undefined ? {} : { gameplay }),
|
|
436
617
|
};
|
|
437
618
|
}
|
|
438
619
|
flush() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
3
|
"description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
"lib/**/*.d.ts.map",
|
|
32
32
|
"src",
|
|
33
33
|
"cordis.patch.yml",
|
|
34
|
-
"assets",
|
|
34
|
+
"assets/whale",
|
|
35
|
+
"assets/whale-refined",
|
|
36
|
+
"assets/ouo-neko",
|
|
37
|
+
"assets/decorations",
|
|
35
38
|
"contracts"
|
|
36
39
|
],
|
|
37
40
|
"license": "Apache-2.0",
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* @module @linxin666/dsh-pet/client/PetDockEntry
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { useEffect, useSyncExternalStore, type ReactElement } from 'react'
|
|
14
|
+
import { useEffect, useRef, useSyncExternalStore, type ReactElement } from 'react'
|
|
15
15
|
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
|
16
16
|
import type { PetDisplayConfig } from '../persist.ts'
|
|
17
17
|
import type { PetStoreInstance } from './pet-store.ts'
|
|
18
18
|
import { PetSprite } from './PetSprite.tsx'
|
|
19
19
|
import { PetRendererSwitch } from './renderers/PetRendererSwitch.tsx'
|
|
20
|
+
import { createDragStream, type DragStream } from './drag-stream.ts'
|
|
21
|
+
import { GameplayHud, type GameplayApi, type GameplayBus } from './gameplay-hud.tsx'
|
|
20
22
|
import { NS } from './locales.ts'
|
|
21
23
|
import styles from './pet.module.css'
|
|
22
24
|
|
|
@@ -42,6 +44,8 @@ export interface PetInjected {
|
|
|
42
44
|
openSession: (sessionId: string) => void
|
|
43
45
|
/** Clear the reaction bubble. */
|
|
44
46
|
feedbackDone: () => void
|
|
47
|
+
/** Gameplay verb API (miku-pet generalization); wired but unused for pets without a gameplay block. */
|
|
48
|
+
gameplay: GameplayApi
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
/** Composed props of the global pet entry (locale + injected; no slot runtime share). */
|
|
@@ -70,6 +74,15 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
70
74
|
ensure()
|
|
71
75
|
}, [ensure])
|
|
72
76
|
|
|
77
|
+
// Per-pet gameplay wiring: the coordination bus (HUD taps <-> chrome,
|
|
78
|
+
// HUD track overrides <-> frames2d mount) and the shared drag stream.
|
|
79
|
+
const auxRef = useRef<{ id: string; bus: GameplayBus; drag: DragStream } | null>(null)
|
|
80
|
+
if (definition !== null && (auxRef.current === null || auxRef.current.id !== definition.id)) {
|
|
81
|
+
auxRef.current = { id: definition.id, bus: {}, drag: createDragStream() }
|
|
82
|
+
}
|
|
83
|
+
const aux = auxRef.current
|
|
84
|
+
const gameplay = definition?.gameplay
|
|
85
|
+
|
|
73
86
|
if (visible) {
|
|
74
87
|
return (
|
|
75
88
|
<span data-pet-dock data-testid="pet-dock">
|
|
@@ -80,6 +93,7 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
80
93
|
definition={definition}
|
|
81
94
|
phase={snapshot?.phase ?? 'idle'}
|
|
82
95
|
onPet={props.pet}
|
|
96
|
+
{...(aux === null ? {} : { drag: aux.drag, bus: aux.bus })}
|
|
83
97
|
t={props.t}
|
|
84
98
|
>
|
|
85
99
|
<PetSprite
|
|
@@ -94,6 +108,23 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
94
108
|
onRename={props.rename}
|
|
95
109
|
onOpenSession={props.openSession}
|
|
96
110
|
onFeedbackDone={props.feedbackDone}
|
|
111
|
+
dragDisabled={snapshot.gameplay?.mode === 'work'}
|
|
112
|
+
{...(gameplay === undefined || aux === null
|
|
113
|
+
? {}
|
|
114
|
+
: {
|
|
115
|
+
onGameplayTap: (fx: number, fy: number) => aux.bus.tap?.(fx, fy),
|
|
116
|
+
onGameplayMenu: () => aux.bus.openCard?.(),
|
|
117
|
+
hud: (
|
|
118
|
+
<GameplayHud
|
|
119
|
+
definition={definition}
|
|
120
|
+
store={store}
|
|
121
|
+
api={props.gameplay}
|
|
122
|
+
bus={aux.bus}
|
|
123
|
+
drag={aux.drag}
|
|
124
|
+
t={props.t}
|
|
125
|
+
/>
|
|
126
|
+
),
|
|
127
|
+
})}
|
|
97
128
|
t={props.t}
|
|
98
129
|
/>
|
|
99
130
|
</PetRendererSwitch>
|