@linxin666/dsh-pet 0.3.3 → 0.3.5
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 +30 -13
- package/README.zh.md +30 -13
- 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/contracts/voice-pack-v1.schema.json +23 -28
- package/lib/client.js +781 -40
- package/lib/client.js.map +1 -1
- package/lib/index.js +1567 -482
- package/lib/types/chatter.d.ts +68 -46
- package/lib/types/chatter.d.ts.map +1 -1
- package/lib/types/chatter.js +243 -301
- 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 +49 -45
- 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 +38 -9
- 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/event-projection.d.ts +9 -4
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +46 -14
- 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 +30 -3
- package/lib/types/service.d.ts +63 -11
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +208 -19
- package/lib/types/voice-pack.d.ts +6 -7
- package/lib/types/voice-pack.d.ts.map +1 -1
- package/lib/types/voice-pack.js +44 -47
- package/package.json +5 -2
- package/src/chatter.test.ts +68 -38
- package/src/chatter.ts +269 -316
- package/src/client/PetDockEntry.test.tsx +6 -0
- package/src/client/PetDockEntry.tsx +32 -1
- package/src/client/PetSprite.test.tsx +68 -21
- package/src/client/PetSprite.tsx +63 -45
- 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.test.tsx +10 -1
- package/src/client/index.ts +46 -11
- 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/event-projection.ts +57 -16
- 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 +28 -4
- package/src/service.ts +240 -29
- package/src/voice-pack.test.ts +48 -25
- package/src/voice-pack.ts +50 -47
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). */
|
|
@@ -111,8 +114,8 @@ export class PetService extends Service {
|
|
|
111
114
|
return this.enabled;
|
|
112
115
|
}
|
|
113
116
|
/** RPC: current pet state snapshot. */
|
|
114
|
-
async state() {
|
|
115
|
-
return this.view();
|
|
117
|
+
async state(currentSessionId) {
|
|
118
|
+
return this.view(currentSessionId);
|
|
116
119
|
}
|
|
117
120
|
/** Current persisted display config (read-only view). */
|
|
118
121
|
display() {
|
|
@@ -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 });
|
|
@@ -376,15 +547,17 @@ export class PetService extends Service {
|
|
|
376
547
|
if (this.ledger.rewardLegacyTurn(Date.now()))
|
|
377
548
|
this.flush();
|
|
378
549
|
}
|
|
379
|
-
view() {
|
|
550
|
+
view(currentSessionId) {
|
|
380
551
|
const snapshot = this.machine.render();
|
|
381
552
|
const entry = this.activeEntry();
|
|
382
|
-
// One bubble per concurrently active TOP-LEVEL session
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
553
|
+
// One bubble per concurrently active TOP-LEVEL session. The GUI's current
|
|
554
|
+
// session leads the stack when reported (the browser half passes its
|
|
555
|
+
// session list's 'current'); everything else keeps the most recent
|
|
556
|
+
// meaningful event order. Subagent children render no bubble of their own
|
|
557
|
+
// (their activity already shows through the spawning conversation's
|
|
558
|
+
// bubble/display, and the bubble buttons navigate to GUI sessions, which
|
|
559
|
+
// subagents are not). Sessions whose own machine has settled (no bubble
|
|
560
|
+
// copy) drop out, so a finished turn does not leave a stale bubble behind.
|
|
388
561
|
const sessions = [];
|
|
389
562
|
for (const [session, activity] of [...this.sessionActivity.entries()].reverse()) {
|
|
390
563
|
if (sessions.length >= MAX_SESSION_BUBBLES)
|
|
@@ -394,23 +567,39 @@ export class PetService extends Service {
|
|
|
394
567
|
const perSession = activity.machine.render();
|
|
395
568
|
if (perSession.bubble === undefined)
|
|
396
569
|
continue;
|
|
570
|
+
// Each session's whisper rides its own bubble while fresh; an expired
|
|
571
|
+
// whisper simply stops appearing (the client's 2 s poll drops it).
|
|
572
|
+
const whisper = activity.whisper;
|
|
573
|
+
const freshWhisper = whisper !== undefined && Date.now() - whisper.at < WHISPER_TTL_MS
|
|
574
|
+
? whisper.text
|
|
575
|
+
: undefined;
|
|
397
576
|
sessions.push({
|
|
398
577
|
sessionId: String(session.id),
|
|
399
578
|
animation: perSession.animation,
|
|
400
579
|
bubble: perSession.bubble,
|
|
401
580
|
phase: perSession.phase,
|
|
581
|
+
...(freshWhisper === undefined ? {} : { whisper: freshWhisper }),
|
|
402
582
|
});
|
|
403
583
|
}
|
|
404
|
-
// The
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
584
|
+
// The browser half reports the session the user is currently on; that
|
|
585
|
+
// session's bubble leads the stack (its whisper then speaks on the
|
|
586
|
+
// collapsed single bubble). An unreported or absent session keeps the
|
|
587
|
+
// legacy most-recent-first order.
|
|
588
|
+
if (currentSessionId !== undefined) {
|
|
589
|
+
const index = sessions.findIndex(session => session.sessionId === currentSessionId);
|
|
590
|
+
if (index > 0)
|
|
591
|
+
sessions.unshift(sessions.splice(index, 1)[0]);
|
|
592
|
+
}
|
|
413
593
|
const decoration = this.activeDecoration();
|
|
594
|
+
// Gameplay view: a read-only projection. The settle runs on a copy, so
|
|
595
|
+
// polling never writes pet.json (verbs settle and persist).
|
|
596
|
+
const gameplayDef = this.gameplayDef();
|
|
597
|
+
let gameplay;
|
|
598
|
+
if (gameplayDef !== undefined) {
|
|
599
|
+
const { state } = this.gameplayState(gameplayDef, Date.now());
|
|
600
|
+
settleGameplay(state, gameplayDef, Date.now(), { sessionActive: snapshot.sessionActive });
|
|
601
|
+
gameplay = this.gameplayViewOf(state);
|
|
602
|
+
}
|
|
414
603
|
// Read-only: the ledger settles on economic events only, never on a read,
|
|
415
604
|
// so polling the state cannot trigger pet.json writes.
|
|
416
605
|
return {
|
|
@@ -419,7 +608,6 @@ export class PetService extends Service {
|
|
|
419
608
|
phase: snapshot.phase,
|
|
420
609
|
sessionActive: snapshot.sessionActive,
|
|
421
610
|
sessions,
|
|
422
|
-
...(freshWhisper === undefined ? {} : { whisper: freshWhisper }),
|
|
423
611
|
...(decoration === undefined ? {} : { decoration }),
|
|
424
612
|
affinity: this.ledger.affinityView(Date.now()),
|
|
425
613
|
display: { ...this.ledger.snapshot.display },
|
|
@@ -433,6 +621,7 @@ export class PetService extends Service {
|
|
|
433
621
|
stocked: this.ledger.snapshot.treats.treats,
|
|
434
622
|
max: this.ledger.treatMax,
|
|
435
623
|
},
|
|
624
|
+
...(gameplay === undefined ? {} : { gameplay }),
|
|
436
625
|
};
|
|
437
626
|
}
|
|
438
627
|
flush() {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* TypeScript mode: keep it erasable-syntax-only.
|
|
28
28
|
* @module @linxin666/dsh-pet/voice-pack
|
|
29
29
|
*/
|
|
30
|
-
import { type VoicePackOverrides, type
|
|
30
|
+
import { type VoicePackOverrides, type WhisperCategory, type WhisperResult } from './chatter.ts';
|
|
31
31
|
/** Schema version this module normalizes (optional field; missing = 1). */
|
|
32
32
|
export declare const VOICE_PACK_V1: 1;
|
|
33
33
|
/** Hover-panel action buttons a pack can show or hide (canonical order). */
|
|
@@ -58,12 +58,9 @@ export interface VoicePack {
|
|
|
58
58
|
/** Hard caps shared by every pool slot (mirrors the remarks discipline). */
|
|
59
59
|
export declare const VOICE_POOL_LINES_MAX = 64;
|
|
60
60
|
export declare const VOICE_LINE_MAX = 160;
|
|
61
|
-
export declare const VOICE_KEYWORDS_PER_RULE_MAX = 16;
|
|
62
|
-
export declare const VOICE_KEYWORD_MAX = 40;
|
|
63
|
-
export declare const VOICE_RULES_MAX = 32;
|
|
64
61
|
export declare const VOICE_LABEL_MAX = 40;
|
|
65
62
|
export declare const VOICE_STAT_MAX = 80;
|
|
66
|
-
type PoolKind = 'status' | 'tools' | 'toolRemaining' | '
|
|
63
|
+
type PoolKind = 'status' | 'tools' | 'toolRemaining' | 'whisperCategory' | 'whisperResult' | 'label' | 'stat';
|
|
67
64
|
/**
|
|
68
65
|
* Normalize one pool slot. Accepts a single line or an array; non-string
|
|
69
66
|
* entries warn and drop, empty lines drop silently, lines over the length
|
|
@@ -73,8 +70,10 @@ type PoolKind = 'status' | 'tools' | 'toolRemaining' | 'whisperGeneric' | 'whisp
|
|
|
73
70
|
* undefined (the slot keeps the built-in pool).
|
|
74
71
|
*/
|
|
75
72
|
export declare function normalizePool(raw: unknown, kind: PoolKind, onWarning?: (message: string) => void): string[] | undefined;
|
|
76
|
-
/** Normalize
|
|
77
|
-
export declare function
|
|
73
|
+
/** Normalize whisper category pools; a key replaces that category's built-in pool (an explicit empty pool mutes it). */
|
|
74
|
+
export declare function normalizeWhisperCategories(raw: unknown, onWarning?: (message: string) => void): Partial<Record<WhisperCategory, readonly string[]>> | undefined;
|
|
75
|
+
/** Normalize whisper outcome pools; a key replaces that outcome's built-in pool (an explicit empty pool mutes it). */
|
|
76
|
+
export declare function normalizeWhisperResults(raw: unknown, onWarning?: (message: string) => void): Partial<Record<WhisperResult, readonly string[]>> | undefined;
|
|
78
77
|
/** Normalize the panel block (labels / stats / actions; warn-and-drop). */
|
|
79
78
|
export declare function normalizePanel(raw: unknown, onWarning?: (message: string) => void): PetPanelView | undefined;
|
|
80
79
|
/** Voice-pack top-level fields ('$schema' mirrors the schema twin; drift-locked in tests). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-pack.d.ts","sourceRoot":"","sources":["../../src/voice-pack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"voice-pack.d.ts","sourceRoot":"","sources":["../../src/voice-pack.ts"],"names":[],"mappings":"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"}
|
package/lib/types/voice-pack.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* TypeScript mode: keep it erasable-syntax-only.
|
|
28
28
|
* @module @linxin666/dsh-pet/voice-pack
|
|
29
29
|
*/
|
|
30
|
-
import { STATUS_SCENES, TOOL_CATEGORIES, } from "./chatter.js";
|
|
30
|
+
import { STATUS_SCENES, TOOL_CATEGORIES, WHISPER_CATEGORIES, WHISPER_RESULTS, } from "./chatter.js";
|
|
31
31
|
/** Schema version this module normalizes (optional field; missing = 1). */
|
|
32
32
|
export const VOICE_PACK_V1 = 1;
|
|
33
33
|
/** Hover-panel action buttons a pack can show or hide (canonical order). */
|
|
@@ -39,9 +39,6 @@ export const PANEL_STAT_KEYS = ['rank', 'treats', 'points'];
|
|
|
39
39
|
/** Hard caps shared by every pool slot (mirrors the remarks discipline). */
|
|
40
40
|
export const VOICE_POOL_LINES_MAX = 64;
|
|
41
41
|
export const VOICE_LINE_MAX = 160;
|
|
42
|
-
export const VOICE_KEYWORDS_PER_RULE_MAX = 16;
|
|
43
|
-
export const VOICE_KEYWORD_MAX = 40;
|
|
44
|
-
export const VOICE_RULES_MAX = 32;
|
|
45
42
|
export const VOICE_LABEL_MAX = 40;
|
|
46
43
|
export const VOICE_STAT_MAX = 80;
|
|
47
44
|
/** Any '{token}' placeholder (no nesting, no newlines). */
|
|
@@ -112,51 +109,45 @@ export function normalizePool(raw, kind, onWarning = () => { }) {
|
|
|
112
109
|
}
|
|
113
110
|
return pool;
|
|
114
111
|
}
|
|
115
|
-
/** Normalize
|
|
116
|
-
export function
|
|
112
|
+
/** Normalize whisper category pools; a key replaces that category's built-in pool (an explicit empty pool mutes it). */
|
|
113
|
+
export function normalizeWhisperCategories(raw, onWarning = () => { }) {
|
|
117
114
|
if (raw === undefined)
|
|
118
115
|
return undefined;
|
|
119
|
-
if (!
|
|
120
|
-
onWarning('whispers.
|
|
116
|
+
if (!isRecord(raw)) {
|
|
117
|
+
onWarning('whispers.categories must be an object');
|
|
121
118
|
return undefined;
|
|
122
119
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for (const item of raw.slice(0, VOICE_RULES_MAX)) {
|
|
128
|
-
if (!isRecord(item)) {
|
|
129
|
-
onWarning('whisper rule must be an object');
|
|
120
|
+
const pools = {};
|
|
121
|
+
for (const key of Object.keys(raw)) {
|
|
122
|
+
if (!WHISPER_CATEGORIES.includes(key)) {
|
|
123
|
+
onWarning('unknown whisper category ' + key + ' ignored');
|
|
130
124
|
continue;
|
|
131
125
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const pool = normalizePool(item.pool, 'whisperRule', onWarning);
|
|
153
|
-
if (keywords.length === 0 || pool === undefined || pool.length === 0) {
|
|
154
|
-
onWarning('whisper rule dropped (needs keywords and a non-empty pool)');
|
|
126
|
+
const pool = normalizePool(raw[key], 'whisperCategory', onWarning);
|
|
127
|
+
if (pool !== undefined)
|
|
128
|
+
pools[key] = pool;
|
|
129
|
+
}
|
|
130
|
+
return Object.keys(pools).length > 0 ? pools : undefined;
|
|
131
|
+
}
|
|
132
|
+
/** Normalize whisper outcome pools; a key replaces that outcome's built-in pool (an explicit empty pool mutes it). */
|
|
133
|
+
export function normalizeWhisperResults(raw, onWarning = () => { }) {
|
|
134
|
+
if (raw === undefined)
|
|
135
|
+
return undefined;
|
|
136
|
+
if (!isRecord(raw)) {
|
|
137
|
+
onWarning('whispers.results must be an object');
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const pools = {};
|
|
141
|
+
for (const key of Object.keys(raw)) {
|
|
142
|
+
if (!WHISPER_RESULTS.includes(key)) {
|
|
143
|
+
onWarning('unknown whisper result ' + key + ' ignored');
|
|
155
144
|
continue;
|
|
156
145
|
}
|
|
157
|
-
|
|
146
|
+
const pool = normalizePool(raw[key], 'whisperResult', onWarning);
|
|
147
|
+
if (pool !== undefined)
|
|
148
|
+
pools[key] = pool;
|
|
158
149
|
}
|
|
159
|
-
return
|
|
150
|
+
return Object.keys(pools).length > 0 ? pools : undefined;
|
|
160
151
|
}
|
|
161
152
|
/** Normalize the panel block (labels / stats / actions; warn-and-drop). */
|
|
162
153
|
export function normalizePanel(raw, onWarning = () => { }) {
|
|
@@ -236,7 +227,7 @@ export function normalizePanel(raw, onWarning = () => { }) {
|
|
|
236
227
|
/** Voice-pack top-level fields ('$schema' mirrors the schema twin; drift-locked in tests). */
|
|
237
228
|
export const VOICE_PACK_KEYS = new Set(['$schema', 'voicePackVersion', 'status', 'tools', 'toolRemaining', 'whispers', 'panel']);
|
|
238
229
|
/** Allowed whisper-section fields (drift-locked in tests). */
|
|
239
|
-
export const WHISPER_KEYS = new Set(['
|
|
230
|
+
export const WHISPER_KEYS = new Set(['categories', 'results']);
|
|
240
231
|
/**
|
|
241
232
|
* Normalize one raw voice.json document into a VoicePack, or undefined when
|
|
242
233
|
* the file cannot serve as a pack at all (non-object root — structure is
|
|
@@ -307,15 +298,21 @@ export function normalizeVoicePack(raw, onWarning = () => { }) {
|
|
|
307
298
|
}
|
|
308
299
|
else {
|
|
309
300
|
for (const key of Object.keys(whispersRaw)) {
|
|
310
|
-
if (
|
|
301
|
+
if (key === 'generic' || key === 'rules') {
|
|
302
|
+
// pet M6: keyword and ambient whisper channels are gone; legacy
|
|
303
|
+
// fields are ignored with a warning, never mapped to the new shape.
|
|
304
|
+
onWarning('whispers.' + key + ' is no longer supported and was ignored');
|
|
305
|
+
}
|
|
306
|
+
else if (!WHISPER_KEYS.has(key)) {
|
|
311
307
|
onWarning('unknown whispers field ' + key + ' ignored');
|
|
308
|
+
}
|
|
312
309
|
}
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
310
|
+
const categories = normalizeWhisperCategories(whispersRaw.categories, onWarning);
|
|
311
|
+
const results = normalizeWhisperResults(whispersRaw.results, onWarning);
|
|
312
|
+
if (categories !== undefined || results !== undefined) {
|
|
316
313
|
overrides.whispers = {
|
|
317
|
-
...(
|
|
318
|
-
...(
|
|
314
|
+
...(categories === undefined ? {} : { categories }),
|
|
315
|
+
...(results === undefined ? {} : { results }),
|
|
319
316
|
};
|
|
320
317
|
}
|
|
321
318
|
}
|
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.5",
|
|
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",
|
package/src/chatter.test.ts
CHANGED
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
TOOL_POOLS,
|
|
12
12
|
toolArgHint,
|
|
13
13
|
toolCategory,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
WHISPER_CATEGORY_POOLS,
|
|
15
|
+
WHISPER_RESULT_POOLS,
|
|
16
|
+
looksLikeTestTool,
|
|
16
17
|
WhisperEngine,
|
|
17
18
|
type VoicePackOverrides,
|
|
18
19
|
} from './chatter.ts'
|
|
@@ -127,30 +128,65 @@ describe('toolArgHint', () => {
|
|
|
127
128
|
})
|
|
128
129
|
|
|
129
130
|
describe('WhisperEngine', () => {
|
|
130
|
-
it('
|
|
131
|
+
it('speaks a category line for the current situation, then respects the cooldown', () => {
|
|
131
132
|
const engine = new WhisperEngine()
|
|
132
|
-
expect(engine.feed('
|
|
133
|
+
expect(engine.feed('thinking', 0)).toBe(WHISPER_CATEGORY_POOLS.thinking[0])
|
|
134
|
+
expect(engine.feed('writing', 1)).toBeUndefined()
|
|
133
135
|
})
|
|
134
136
|
|
|
135
|
-
it('
|
|
137
|
+
it('rotates within each category and keeps the category cursors apart', () => {
|
|
136
138
|
const engine = new WhisperEngine()
|
|
137
|
-
expect(engine.feed('
|
|
138
|
-
expect(engine.feed('
|
|
139
|
-
expect(engine.feed('
|
|
139
|
+
expect(engine.feed('thinking', 0)).toBe(WHISPER_CATEGORY_POOLS.thinking[0])
|
|
140
|
+
expect(engine.feed('writing', 9001)).toBe(WHISPER_CATEGORY_POOLS.writing[0])
|
|
141
|
+
expect(engine.feed('thinking', 18002)).toBe(WHISPER_CATEGORY_POOLS.thinking[1])
|
|
140
142
|
})
|
|
141
143
|
|
|
142
|
-
it('
|
|
143
|
-
const engine = new WhisperEngine(
|
|
144
|
-
expect(engine.feed('
|
|
145
|
-
expect(engine.feed('bbbb', 1)).toBe(WHISPER_GENERIC_POOL[0])
|
|
146
|
-
// The counter resets after speaking: another budget must accumulate.
|
|
147
|
-
expect(engine.feed('cccc', 2)).toBeUndefined()
|
|
148
|
-
expect(engine.feed('dddddddddd', 3)).toBe(WHISPER_GENERIC_POOL[1])
|
|
144
|
+
it('stays quiet on an explicitly muted category', () => {
|
|
145
|
+
const engine = new WhisperEngine(() => ({ whispers: { categories: { thinking: [] } } }))
|
|
146
|
+
expect(engine.feed('thinking', 0)).toBeUndefined()
|
|
149
147
|
})
|
|
150
148
|
|
|
151
|
-
it('
|
|
152
|
-
const engine = new WhisperEngine(
|
|
153
|
-
expect(engine.
|
|
149
|
+
it('wakes outcome whispers on their own shorter cooldown and rotates per outcome', () => {
|
|
150
|
+
const engine = new WhisperEngine()
|
|
151
|
+
expect(engine.result('pass', 0)).toBe(WHISPER_RESULT_POOLS.pass[0])
|
|
152
|
+
expect(engine.result('fail', 1000)).toBeUndefined()
|
|
153
|
+
expect(engine.result('fail', 5001)).toBe(WHISPER_RESULT_POOLS.fail[0])
|
|
154
|
+
expect(engine.result('pass', 10002)).toBe(WHISPER_RESULT_POOLS.pass[1])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('paces category and outcome whispers against one shared clock', () => {
|
|
158
|
+
const engine = new WhisperEngine()
|
|
159
|
+
expect(engine.feed('writing', 0)).toBe(WHISPER_CATEGORY_POOLS.writing[0])
|
|
160
|
+
// An outcome right after a category reply must wait the outcome cooldown.
|
|
161
|
+
expect(engine.result('done', 4999)).toBeUndefined()
|
|
162
|
+
expect(engine.result('done', 5000)).toBe(WHISPER_RESULT_POOLS.done[0])
|
|
163
|
+
// After an outcome, the category must wait the full category cooldown
|
|
164
|
+
// measured from the outcome whisper (5000), so 14000 is the next slot.
|
|
165
|
+
expect(engine.feed('thinking', 5001)).toBeUndefined()
|
|
166
|
+
expect(engine.feed('thinking', 14000)).toBe(WHISPER_CATEGORY_POOLS.thinking[0])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('mutes an outcome with an explicit empty pool', () => {
|
|
170
|
+
const engine = new WhisperEngine(() => ({ whispers: { results: { fail: [] } } }))
|
|
171
|
+
expect(engine.result('fail', 0)).toBeUndefined()
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('looksLikeTestTool', () => {
|
|
176
|
+
it('recognizes test tools by name and by shell command', () => {
|
|
177
|
+
expect(looksLikeTestTool('run_tests', undefined)).toBe(true)
|
|
178
|
+
expect(looksLikeTestTool('run_code', '{"command":"npm test"}')).toBe(true)
|
|
179
|
+
expect(looksLikeTestTool('run_code', '{"command":"pnpm run test -- --run"}')).toBe(true)
|
|
180
|
+
expect(looksLikeTestTool('bash', '{"command":"pytest -q"}')).toBe(true)
|
|
181
|
+
expect(looksLikeTestTool('bash', '{"command":"go test ./..."}')).toBe(true)
|
|
182
|
+
expect(looksLikeTestTool('run_code', '{"code":"import pytest"}')).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('does not misfire on ordinary tools or mentions in generic code', () => {
|
|
186
|
+
expect(looksLikeTestTool('bash', '{"command":"node server.js"}')).toBe(false)
|
|
187
|
+
expect(looksLikeTestTool('grep', '{"pattern":"test"}')).toBe(false)
|
|
188
|
+
expect(looksLikeTestTool('read', '{"file_path":"/repo/test/fixture.txt"}')).toBe(false)
|
|
189
|
+
expect(looksLikeTestTool('run_code', '{"code":"server.listen(8080)"}')).toBe(false)
|
|
154
190
|
})
|
|
155
191
|
})
|
|
156
192
|
|
|
@@ -165,8 +201,8 @@ describe('voice-pack overrides (pet-center M4)', () => {
|
|
|
165
201
|
},
|
|
166
202
|
toolRemaining: ['后台还有 {n} 个'],
|
|
167
203
|
whispers: {
|
|
168
|
-
|
|
169
|
-
|
|
204
|
+
categories: { thinking: ['自定义思考', '自定义思考二'] },
|
|
205
|
+
results: { pass: ['自定义全绿'] },
|
|
170
206
|
},
|
|
171
207
|
})
|
|
172
208
|
|
|
@@ -214,28 +250,22 @@ describe('voice-pack overrides (pet-center M4)', () => {
|
|
|
214
250
|
expect(voice.scene('done', 5000)).toBe('换声了')
|
|
215
251
|
})
|
|
216
252
|
|
|
217
|
-
it('replaces
|
|
253
|
+
it('replaces whisper category pools per key', () => {
|
|
218
254
|
const engine = new WhisperEngine(PACK)
|
|
219
|
-
expect(engine.feed('
|
|
220
|
-
|
|
221
|
-
expect(engine.feed('
|
|
255
|
+
expect(engine.feed('thinking', 0)).toBe('自定义思考')
|
|
256
|
+
expect(engine.feed('writing', 9000)).toBe(WHISPER_CATEGORY_POOLS.writing[0])
|
|
257
|
+
expect(engine.feed('thinking', 18000)).toBe('自定义思考二')
|
|
222
258
|
})
|
|
223
259
|
|
|
224
|
-
it('replaces
|
|
225
|
-
const engine = new WhisperEngine(PACK
|
|
226
|
-
expect(engine.
|
|
227
|
-
expect(engine.
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('mutes ambient whispers when the generic pool is explicitly empty', () => {
|
|
231
|
-
const engine = new WhisperEngine(() => ({ whispers: { generic: [] } }), 0, 3)
|
|
232
|
-
expect(engine.feed('aaaa', 0)).toBeUndefined()
|
|
233
|
-
expect(engine.feed('bbbb', 1)).toBeUndefined()
|
|
260
|
+
it('replaces whisper outcome pools per key', () => {
|
|
261
|
+
const engine = new WhisperEngine(PACK)
|
|
262
|
+
expect(engine.result('pass', 0)).toBe('自定义全绿')
|
|
263
|
+
expect(engine.result('fail', 5001)).toBe(WHISPER_RESULT_POOLS.fail[0])
|
|
234
264
|
})
|
|
235
265
|
|
|
236
|
-
it('
|
|
237
|
-
const engine = new WhisperEngine(() => ({ whispers: {
|
|
238
|
-
|
|
239
|
-
expect(engine.
|
|
266
|
+
it('mutes a whisper channel with an explicit empty override', () => {
|
|
267
|
+
const engine = new WhisperEngine(() => ({ whispers: { categories: { thinking: [] }, results: { done: [] } } }))
|
|
268
|
+
expect(engine.feed('thinking', 0)).toBeUndefined()
|
|
269
|
+
expect(engine.result('done', 0)).toBeUndefined()
|
|
240
270
|
})
|
|
241
271
|
})
|