@linxin666/dsh-pet 0.1.17 → 0.1.19
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 +12 -5
- package/README.zh.md +12 -5
- package/lib/client.js +134 -78
- package/lib/client.js.map +1 -1
- package/lib/index.js +100 -30
- package/lib/invariant.js +2 -2
- package/lib/state-Diiq8vjk.js +394 -0
- package/lib/types/affinity.d.ts +31 -3
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +21 -7
- package/lib/types/client/PetSettingsCard.d.ts +16 -5
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +14 -1
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +8 -2
- package/lib/types/client/PluginSettingsCard.d.ts +11 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +11 -5
- package/lib/types/client/index.d.ts +3 -22
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +19 -12
- package/lib/types/client/locales.js +2 -2
- package/lib/types/client/settings-form.d.ts +8 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +15 -1
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +3 -1
- package/lib/types/index.d.ts +4 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +2 -1
- package/lib/types/ledger.d.ts +16 -1
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +29 -4
- package/lib/types/registry.d.ts +10 -0
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +3 -0
- package/lib/types/remarks.d.ts +56 -0
- package/lib/types/remarks.d.ts.map +1 -0
- package/lib/types/remarks.js +157 -0
- package/lib/types/service.d.ts +40 -3
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +90 -18
- package/lib/types/state.js +5 -5
- package/lib/types/treats.d.ts +3 -0
- package/lib/types/treats.d.ts.map +1 -1
- package/lib/types/treats.js +5 -2
- package/package.json +1 -1
- package/src/affinity.test.ts +19 -0
- package/src/affinity.ts +22 -7
- package/src/client/PetSettingsCard.tsx +35 -6
- package/src/client/PetSprite.test.tsx +33 -0
- package/src/client/PetSprite.tsx +16 -1
- package/src/client/PluginSettingsCard.tsx +50 -47
- package/src/client/index.ts +20 -30
- package/src/client/locales.ts +2 -2
- package/src/client/pet.module.css +19 -0
- package/src/client/settings-card.module.css +9 -0
- package/src/client/settings-form.ts +15 -1
- package/src/client/settings-section.module.css +5 -0
- package/src/event-projection.ts +3 -1
- package/src/index.ts +12 -1
- package/src/ledger.test.ts +40 -9
- package/src/ledger.ts +33 -4
- package/src/persist.test.ts +1 -1
- package/src/registry.test.ts +27 -0
- package/src/registry.ts +12 -0
- package/src/remarks.test.ts +89 -0
- package/src/remarks.ts +177 -0
- package/src/service.ts +116 -18
- package/src/state.test.ts +9 -0
- package/src/state.ts +5 -5
- package/src/treats.test.ts +13 -11
- package/src/treats.ts +5 -2
- package/lib/state-CFyJv0sQ.js +0 -213
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remarks.d.ts","sourceRoot":"","sources":["../../src/remarks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,aAAa,GAAG,MAAM,GAAG,cAAc,GAAG,UAAU,CAAA;AAErF,4CAA4C;AAC5C,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAM7C,CAAA;AAED,6EAA6E;AAC7E,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;AAE9D,yEAAyE;AACzE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC,CAAA;AAE/E,2EAA2E;AAC3E,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,sCAAsC;AACtC,eAAO,MAAM,gBAAgB,KAAK,CAAA;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC,CA6D3E,CAAA;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAEtD;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,EACZ,SAAS,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAe,GAC9C,UAAU,GAAG,SAAS,CAyBxB;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgC;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;gBAEjD,SAAS,CAAC,EAAE,UAAU;IAQlC,qEAAqE;IACrE,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE;IAIzC,gEAAgE;IAChE,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM;CAM/B"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pet remark (reaction copy) library — pure. Reaction bubbles the pet speaks
|
|
3
|
+
* on interaction events come from two layers:
|
|
4
|
+
* - the built-in default library below (a generous pool per event kind);
|
|
5
|
+
* - per-pet custom lines declared in a manifest's 'remarks' block, which
|
|
6
|
+
* community pet contributions (PRs) use to give their pet its own voice.
|
|
7
|
+
* A custom slot replaces the built-in pool for that slot only; the other
|
|
8
|
+
* slots keep the built-in lines. Picks cycle round-robin within a pool so
|
|
9
|
+
* repeated interactions stay varied while tests stay deterministic (no
|
|
10
|
+
* randomness, no clock).
|
|
11
|
+
* @module @linxin666/dsh-pet/remarks
|
|
12
|
+
*/
|
|
13
|
+
/** Every remark slot, in a stable order. */
|
|
14
|
+
export const REMARK_KINDS = [
|
|
15
|
+
'pet',
|
|
16
|
+
'petCooldown',
|
|
17
|
+
'feed',
|
|
18
|
+
'feedCooldown',
|
|
19
|
+
'noTreats',
|
|
20
|
+
];
|
|
21
|
+
/** Longest accepted reaction line (characters, trimmed before slicing). */
|
|
22
|
+
export const REMARK_LINE_MAX = 120;
|
|
23
|
+
/** Longest accepted pool per slot. */
|
|
24
|
+
export const REMARK_LINES_MAX = 64;
|
|
25
|
+
/**
|
|
26
|
+
* Built-in default remark library. Every pool is plain zh copy in the
|
|
27
|
+
* whale-girl voice; the first line of each pool is the legacy reaction the
|
|
28
|
+
* plugin has always spoken, so existing installs and tests keep their
|
|
29
|
+
* wording while the pool adds variety. No emoji characters anywhere (the
|
|
30
|
+
* repo bans them); ~ is the whale-girl's signature.
|
|
31
|
+
*/
|
|
32
|
+
export const BUILTIN_REMARKS = {
|
|
33
|
+
pet: [
|
|
34
|
+
'咕噜咕噜~被摸摸好舒服!',
|
|
35
|
+
'再摸摸这里,痒痒的~',
|
|
36
|
+
'头顶温度刚刚好,安心~',
|
|
37
|
+
'被摸到耳朵啦,扑通扑通!',
|
|
38
|
+
'你的手掌好温暖,舍不得你走~',
|
|
39
|
+
'呼噜呼噜~就靠在这里不走了!',
|
|
40
|
+
'今天的摸头也收货成功!',
|
|
41
|
+
'蹭蹭你的手心,这是回礼~',
|
|
42
|
+
'多摸摸我,亲密度会涨哦!',
|
|
43
|
+
'闭眼享受中,请勿打扰~',
|
|
44
|
+
],
|
|
45
|
+
petCooldown: [
|
|
46
|
+
'摸过头啦,让鲸鱼娘歇口气~',
|
|
47
|
+
'羽毛都快被摸秃啦,缓一缓~',
|
|
48
|
+
'呼……先让我喘口气嘛!',
|
|
49
|
+
'再摸就要睡着了哦~',
|
|
50
|
+
'稍微休息一下,待会儿再摸~',
|
|
51
|
+
'头顶要冒烟啦,停一停!',
|
|
52
|
+
'我知道你喜欢我,但也要节制呀~',
|
|
53
|
+
'歇一歇,摸摸的手感会更好哦~',
|
|
54
|
+
'咕……等我回个蓝~',
|
|
55
|
+
'让我先消化一下刚才的爱!',
|
|
56
|
+
],
|
|
57
|
+
feed: [
|
|
58
|
+
'呜哇!小鱼干好好吃!',
|
|
59
|
+
'咔嚓咔嚓,美味到尾巴打结~',
|
|
60
|
+
'这条小鱼干是刚晒好的,好香!',
|
|
61
|
+
'谢谢你,胃里暖暖的~',
|
|
62
|
+
'囤粮 +1,今天也有好好被爱!',
|
|
63
|
+
'好吃到想转圈圈~',
|
|
64
|
+
'小鱼干最好吃了,再来亿条!',
|
|
65
|
+
'饱餐一顿,马上满血复活~',
|
|
66
|
+
'这个味道,是幸福的味道!',
|
|
67
|
+
'吃完了还不忘舔舔爪子~',
|
|
68
|
+
],
|
|
69
|
+
feedCooldown: [
|
|
70
|
+
'吃饱啦,晚点再喂~',
|
|
71
|
+
'肚子圆滚滚的,装不下啦~',
|
|
72
|
+
'再喂就要变成球啦!',
|
|
73
|
+
'让我慢慢消化这份心意~',
|
|
74
|
+
'小鱼干的香气还没散呢~',
|
|
75
|
+
'呼……满足得动不了了~',
|
|
76
|
+
'先散步一圈再吃下一顿!',
|
|
77
|
+
'肚皮已经鼓鼓的啦~',
|
|
78
|
+
'好吃是好吃,可也得节制呀~',
|
|
79
|
+
'等我饿了会告诉你哦~',
|
|
80
|
+
],
|
|
81
|
+
noTreats: [
|
|
82
|
+
'没有小鱼干了,多陪我工作一会儿吧~',
|
|
83
|
+
'粮仓空空,陪我完成几轮任务就会有小鱼干啦~',
|
|
84
|
+
'小鱼干在路上啦,先一起加油工作!',
|
|
85
|
+
'嘴巴寂寞了……快去完成一轮任务!',
|
|
86
|
+
'陪我多工作一会儿,鱼干自动到账~',
|
|
87
|
+
'现在喂我也只会饿着肚子说谢谢哦~',
|
|
88
|
+
'粮仓见底啦,用几轮任务换一条鱼干吧~',
|
|
89
|
+
'饿着肚子等你完成下一轮任务~',
|
|
90
|
+
'小鱼干藏在你的工作里,去找找看!',
|
|
91
|
+
'先工作后干饭,我们的约定哦~',
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
/** The legacy first line of one kind (direct callers' fallback copy). */
|
|
95
|
+
export function builtinRemark(kind) {
|
|
96
|
+
return BUILTIN_REMARKS[kind][0];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Normalize a manifest 'remarks' block into per-kind line pools. Unknown
|
|
100
|
+
* slots and non-string entries are skipped with a warning; empty pools are
|
|
101
|
+
* dropped so the built-in library takes the slot. Returns undefined when no
|
|
102
|
+
* usable slot remains.
|
|
103
|
+
*/
|
|
104
|
+
export function normalizePetRemarks(raw, onWarning = () => { }) {
|
|
105
|
+
if (raw === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
108
|
+
onWarning('remarks must be an object with pet/petCooldown/feed/feedCooldown/noTreats slots');
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const remarks = {};
|
|
112
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
113
|
+
if (!REMARK_KINDS.includes(key)) {
|
|
114
|
+
onWarning('unknown remarks slot ' + JSON.stringify(key));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const lines = (Array.isArray(value) ? value : [value])
|
|
118
|
+
.filter((line) => typeof line === 'string')
|
|
119
|
+
.map(line => line.trim())
|
|
120
|
+
.filter(line => line !== '')
|
|
121
|
+
.slice(0, REMARK_LINES_MAX)
|
|
122
|
+
.map(line => line.slice(0, REMARK_LINE_MAX));
|
|
123
|
+
if (lines.length === 0) {
|
|
124
|
+
onWarning('remarks slot ' + key + ' carries no usable lines');
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
remarks[key] = lines;
|
|
128
|
+
}
|
|
129
|
+
return Object.keys(remarks).length === 0 ? undefined : remarks;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Round-robin reaction picker over the effective pools (per-pet custom lines
|
|
133
|
+
* override the built-in pool per slot). Counters are per slot, so each slot
|
|
134
|
+
* cycles its own list independently and picks stay deterministic for tests.
|
|
135
|
+
*/
|
|
136
|
+
export class RemarkPicker {
|
|
137
|
+
counters = new Map();
|
|
138
|
+
pools;
|
|
139
|
+
constructor(overrides) {
|
|
140
|
+
this.pools = {};
|
|
141
|
+
for (const kind of REMARK_KINDS) {
|
|
142
|
+
const custom = overrides?.[kind];
|
|
143
|
+
this.pools[kind] = custom !== undefined && custom.length > 0 ? custom : BUILTIN_REMARKS[kind];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** The effective pool for one slot (custom override or built-in). */
|
|
147
|
+
pool(kind) {
|
|
148
|
+
return this.pools[kind];
|
|
149
|
+
}
|
|
150
|
+
/** The next line for one slot (round-robin within its pool). */
|
|
151
|
+
pick(kind) {
|
|
152
|
+
const pool = this.pools[kind];
|
|
153
|
+
const index = (this.counters.get(kind) ?? 0) % pool.length;
|
|
154
|
+
this.counters.set(kind, index + 1);
|
|
155
|
+
return pool[index];
|
|
156
|
+
}
|
|
157
|
+
}
|
package/lib/types/service.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* persistence through `persist`. The API gateway maps these methods onto
|
|
6
6
|
* `pet.state` / `pet.pets` / `pet.interact` / `pet.setVisible` /
|
|
7
7
|
* `pet.setConfig` / `pet.setName` / `pet.setPet` for browser consumers.
|
|
8
|
+
*
|
|
9
|
+
* Concurrent sessions each keep their own machine: the sprite animation
|
|
10
|
+
* follows the most recent meaningful event (the display session) while the
|
|
11
|
+
* state view carries one bubble per active session.
|
|
8
12
|
* @module @linxin666/dsh-pet/service
|
|
9
13
|
*/
|
|
10
14
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
@@ -54,12 +58,35 @@ export interface PetSettingsSection {
|
|
|
54
58
|
}
|
|
55
59
|
/** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
|
|
56
60
|
export declare const PET_SETTINGS_NAMESPACE = "pet";
|
|
61
|
+
/**
|
|
62
|
+
* One active session as the pet displays it. Sessions run in parallel, so
|
|
63
|
+
* each gets its own bubble while the sprite itself follows the most recent
|
|
64
|
+
* meaningful event (the display session).
|
|
65
|
+
*/
|
|
66
|
+
export interface PetSessionView {
|
|
67
|
+
/** Session identity (stringified for the wire; never exposed as a key). */
|
|
68
|
+
sessionId: string;
|
|
69
|
+
/** The animation this session's activity maps onto. */
|
|
70
|
+
animation: PetStateSnapshot['animation'];
|
|
71
|
+
/** This session's status bubble copy. */
|
|
72
|
+
bubble: string;
|
|
73
|
+
/** This session's raw activity phase. */
|
|
74
|
+
phase: PetStateSnapshot['phase'];
|
|
75
|
+
}
|
|
76
|
+
/** Hard cap on simultaneously displayed session bubbles (most recent first). */
|
|
77
|
+
export declare const MAX_SESSION_BUBBLES = 12;
|
|
57
78
|
/** Snapshot returned by `pet.state`. */
|
|
58
79
|
export interface PetStateView {
|
|
59
80
|
animation: PetStateSnapshot['animation'];
|
|
60
81
|
bubble?: string;
|
|
61
82
|
phase: PetStateSnapshot['phase'];
|
|
62
83
|
sessionActive: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Per-session bubbles for every concurrently active session, most recent
|
|
86
|
+
* first; optional so older hosts without the multi-session view stay
|
|
87
|
+
* consumable. The single 'bubble' above mirrors the display session.
|
|
88
|
+
*/
|
|
89
|
+
sessions?: PetSessionView[];
|
|
63
90
|
/** Affinity ledger snapshot. */
|
|
64
91
|
affinity: PetAffinityView;
|
|
65
92
|
/** Display configuration. */
|
|
@@ -99,6 +126,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
99
126
|
export declare class PetService extends Service {
|
|
100
127
|
static inject: string[];
|
|
101
128
|
private readonly machine;
|
|
129
|
+
private readonly stateConfig;
|
|
102
130
|
private readonly ledger;
|
|
103
131
|
private readonly registry;
|
|
104
132
|
private readonly persistDir;
|
|
@@ -106,6 +134,11 @@ export declare class PetService extends Service {
|
|
|
106
134
|
private disposeActivity;
|
|
107
135
|
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
108
136
|
private displaySession;
|
|
137
|
+
/**
|
|
138
|
+
* Per-session activity, most recent last (Map insertion order). Bounded by
|
|
139
|
+
* MAX_SESSION_BUBBLES so a burst of sessions cannot grow it without bound;
|
|
140
|
+
* disposed sessions are removed by the 'session/disposed' listener.
|
|
141
|
+
*/
|
|
109
142
|
private readonly sessionActivity;
|
|
110
143
|
constructor(ctx: Context, config?: PetConfig);
|
|
111
144
|
/** Whether the pet service consumes session activity while enabled. */
|
|
@@ -135,9 +168,13 @@ export declare class PetService extends Service {
|
|
|
135
168
|
/** Start or stop the session-activity listeners that drive the pet. */
|
|
136
169
|
setEnabled(enabled: boolean): void;
|
|
137
170
|
private syncActivity;
|
|
138
|
-
/** Return the
|
|
139
|
-
private
|
|
140
|
-
/**
|
|
171
|
+
/** Return the per-session activity record, creating it on first sight. */
|
|
172
|
+
private activityOf;
|
|
173
|
+
/**
|
|
174
|
+
* Commit one activity: the session's own machine renders its bubble, and
|
|
175
|
+
* the session becomes the host-global display session (most recent
|
|
176
|
+
* meaningful event wins the sprite animation).
|
|
177
|
+
*/
|
|
141
178
|
private applyActivity;
|
|
142
179
|
/** RPC: pet or feed the pet. */
|
|
143
180
|
interact(kind: PetInteraction): Promise<PetInteractResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
|
package/lib/types/service.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* persistence through `persist`. The API gateway maps these methods onto
|
|
6
6
|
* `pet.state` / `pet.pets` / `pet.interact` / `pet.setVisible` /
|
|
7
7
|
* `pet.setConfig` / `pet.setName` / `pet.setPet` for browser consumers.
|
|
8
|
+
*
|
|
9
|
+
* Concurrent sessions each keep their own machine: the sprite animation
|
|
10
|
+
* follows the most recent meaningful event (the display session) while the
|
|
11
|
+
* state view carries one bubble per active session.
|
|
8
12
|
* @module @linxin666/dsh-pet/service
|
|
9
13
|
*/
|
|
10
14
|
import { Service } from '@deepseek-ai/cordis';
|
|
@@ -15,6 +19,8 @@ import { loadPetRegistry, petEntryView, petPackageRoot, } from "./registry.js";
|
|
|
15
19
|
import { defaultPetStateConfig, PetStateMachine, } from "./state.js";
|
|
16
20
|
/** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
|
|
17
21
|
export const PET_SETTINGS_NAMESPACE = 'pet';
|
|
22
|
+
/** Hard cap on simultaneously displayed session bubbles (most recent first). */
|
|
23
|
+
export const MAX_SESSION_BUBBLES = 12;
|
|
18
24
|
/**
|
|
19
25
|
* Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
|
|
20
26
|
* written until an economic event or interaction arrives; event listeners
|
|
@@ -24,6 +30,7 @@ export const PET_SETTINGS_NAMESPACE = 'pet';
|
|
|
24
30
|
export class PetService extends Service {
|
|
25
31
|
static inject = [];
|
|
26
32
|
machine;
|
|
33
|
+
stateConfig;
|
|
27
34
|
ledger;
|
|
28
35
|
registry;
|
|
29
36
|
persistDir;
|
|
@@ -31,7 +38,12 @@ export class PetService extends Service {
|
|
|
31
38
|
disposeActivity;
|
|
32
39
|
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
33
40
|
displaySession;
|
|
34
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Per-session activity, most recent last (Map insertion order). Bounded by
|
|
43
|
+
* MAX_SESSION_BUBBLES so a burst of sessions cannot grow it without bound;
|
|
44
|
+
* disposed sessions are removed by the 'session/disposed' listener.
|
|
45
|
+
*/
|
|
46
|
+
sessionActivity = new Map();
|
|
35
47
|
constructor(ctx, config = {}) {
|
|
36
48
|
super(ctx, 'pet');
|
|
37
49
|
this.persistDir = config.persistDir ?? petHomeDir();
|
|
@@ -49,12 +61,15 @@ export class PetService extends Service {
|
|
|
49
61
|
// copied pet.json): fall back to the registry default.
|
|
50
62
|
persist = { ...persist, petId: this.registry.defaultEntry().id };
|
|
51
63
|
}
|
|
52
|
-
const
|
|
64
|
+
const selected = this.registry.byId(persist.petId) ?? this.registry.defaultEntry();
|
|
65
|
+
const ledgerConfig = {
|
|
66
|
+
affinity: config.affinity,
|
|
67
|
+
treats: config.treats,
|
|
68
|
+
remarks: selected.remarks,
|
|
69
|
+
};
|
|
53
70
|
this.ledger = new PetLedger(persist, ledgerConfig);
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
...(config.state ?? {}),
|
|
57
|
-
});
|
|
71
|
+
this.stateConfig = { ...defaultPetStateConfig, ...(config.state ?? {}) };
|
|
72
|
+
this.machine = new PetStateMachine(this.stateConfig);
|
|
58
73
|
this.enabled = config.enabled ?? true;
|
|
59
74
|
this.syncActivity();
|
|
60
75
|
}
|
|
@@ -99,6 +114,7 @@ export class PetService extends Service {
|
|
|
99
114
|
if (entry === undefined)
|
|
100
115
|
return { ok: false, error: 'unknown-pet' };
|
|
101
116
|
this.ledger.setPetId(entry.id);
|
|
117
|
+
this.ledger.setRemarks(entry.remarks);
|
|
102
118
|
this.flush();
|
|
103
119
|
this.syncSettingsFromPet();
|
|
104
120
|
return { ok: true, petId: entry.id };
|
|
@@ -118,7 +134,7 @@ export class PetService extends Service {
|
|
|
118
134
|
this.disposeActivity = (() => {
|
|
119
135
|
const disposers = [
|
|
120
136
|
this.ctx.on('session/event', (session, event) => {
|
|
121
|
-
const runtime = this.
|
|
137
|
+
const runtime = this.activityOf(session).runtime;
|
|
122
138
|
// `activity/status` is an optional compatibility input. It is not
|
|
123
139
|
// declared as a durable event type by this package because current
|
|
124
140
|
// Harness installations publish the official session vocabulary.
|
|
@@ -149,27 +165,63 @@ export class PetService extends Service {
|
|
|
149
165
|
}
|
|
150
166
|
}),
|
|
151
167
|
this.ctx.on('session/disposed', (session) => {
|
|
168
|
+
this.ledger.forgetSession(String(session.id));
|
|
169
|
+
this.sessionActivity.delete(session);
|
|
152
170
|
if (session !== this.displaySession)
|
|
153
171
|
return;
|
|
172
|
+
// The display session is gone: fall back to the most recent
|
|
173
|
+
// remaining session's last input, or settle to idle when none.
|
|
154
174
|
this.displaySession = undefined;
|
|
155
|
-
this.
|
|
175
|
+
const remaining = [...this.sessionActivity.entries()].at(-1);
|
|
176
|
+
if (remaining !== undefined) {
|
|
177
|
+
const [nextSession, activity] = remaining;
|
|
178
|
+
this.displaySession = nextSession;
|
|
179
|
+
if (activity.lastInput !== undefined)
|
|
180
|
+
this.machine.onActivityStatus(activity.lastInput);
|
|
181
|
+
this.machine.onSessionActive();
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
this.machine.onSessionDisposed();
|
|
185
|
+
}
|
|
156
186
|
}),
|
|
157
187
|
];
|
|
158
188
|
return () => { for (const dispose of disposers)
|
|
159
189
|
dispose(); };
|
|
160
190
|
})();
|
|
161
191
|
}
|
|
162
|
-
/** Return the
|
|
163
|
-
|
|
164
|
-
let
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
192
|
+
/** Return the per-session activity record, creating it on first sight. */
|
|
193
|
+
activityOf(session) {
|
|
194
|
+
let activity = this.sessionActivity.get(session);
|
|
195
|
+
if (activity === undefined) {
|
|
196
|
+
activity = {
|
|
197
|
+
runtime: emptyProjectionRuntime(),
|
|
198
|
+
machine: new PetStateMachine(this.stateConfig),
|
|
199
|
+
};
|
|
200
|
+
this.sessionActivity.set(session, activity);
|
|
168
201
|
}
|
|
169
|
-
return
|
|
202
|
+
return activity;
|
|
170
203
|
}
|
|
171
|
-
/**
|
|
204
|
+
/**
|
|
205
|
+
* Commit one activity: the session's own machine renders its bubble, and
|
|
206
|
+
* the session becomes the host-global display session (most recent
|
|
207
|
+
* meaningful event wins the sprite animation).
|
|
208
|
+
*/
|
|
172
209
|
applyActivity(session, input) {
|
|
210
|
+
const activity = this.activityOf(session);
|
|
211
|
+
activity.lastInput = input;
|
|
212
|
+
activity.machine.onActivityStatus(input);
|
|
213
|
+
activity.machine.onSessionActive();
|
|
214
|
+
// Move to the tail so map order reads most-recent-last, then trim the
|
|
215
|
+
// oldest session states beyond the bubble cap. The display session is
|
|
216
|
+
// reassigned below, so trimming its stale predecessor is safe.
|
|
217
|
+
this.sessionActivity.delete(session);
|
|
218
|
+
this.sessionActivity.set(session, activity);
|
|
219
|
+
while (this.sessionActivity.size > MAX_SESSION_BUBBLES) {
|
|
220
|
+
const oldest = this.sessionActivity.keys().next().value;
|
|
221
|
+
if (oldest === undefined)
|
|
222
|
+
break;
|
|
223
|
+
this.sessionActivity.delete(oldest);
|
|
224
|
+
}
|
|
173
225
|
this.displaySession = session;
|
|
174
226
|
this.machine.onActivityStatus(input);
|
|
175
227
|
this.machine.onSessionActive();
|
|
@@ -218,8 +270,10 @@ export class PetService extends Service {
|
|
|
218
270
|
* @param section - the resolved settings section.
|
|
219
271
|
*/
|
|
220
272
|
applySettingsSection(section) {
|
|
221
|
-
|
|
222
|
-
|
|
273
|
+
const selected = typeof section.petId === 'string' ? this.registry.byId(section.petId) : undefined;
|
|
274
|
+
if (selected !== undefined) {
|
|
275
|
+
this.ledger.setPetId(selected.id);
|
|
276
|
+
this.ledger.setRemarks(selected.remarks);
|
|
223
277
|
}
|
|
224
278
|
else if (section.petId !== undefined) {
|
|
225
279
|
// The stored selection names a pet the registry no longer has: keep the
|
|
@@ -263,6 +317,23 @@ export class PetService extends Service {
|
|
|
263
317
|
view() {
|
|
264
318
|
const snapshot = this.machine.render();
|
|
265
319
|
const entry = this.activeEntry();
|
|
320
|
+
// One bubble per concurrently active session, most recent first.
|
|
321
|
+
// Sessions whose own machine has settled (no bubble copy) drop out, so a
|
|
322
|
+
// finished turn does not leave a stale bubble behind.
|
|
323
|
+
const sessions = [];
|
|
324
|
+
for (const [session, activity] of [...this.sessionActivity.entries()].reverse()) {
|
|
325
|
+
if (sessions.length >= MAX_SESSION_BUBBLES)
|
|
326
|
+
break;
|
|
327
|
+
const perSession = activity.machine.render();
|
|
328
|
+
if (perSession.bubble === undefined)
|
|
329
|
+
continue;
|
|
330
|
+
sessions.push({
|
|
331
|
+
sessionId: String(session.id),
|
|
332
|
+
animation: perSession.animation,
|
|
333
|
+
bubble: perSession.bubble,
|
|
334
|
+
phase: perSession.phase,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
266
337
|
// Read-only: the ledger settles on economic events only, never on a read,
|
|
267
338
|
// so polling the state cannot trigger pet.json writes.
|
|
268
339
|
return {
|
|
@@ -270,6 +341,7 @@ export class PetService extends Service {
|
|
|
270
341
|
...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
|
|
271
342
|
phase: snapshot.phase,
|
|
272
343
|
sessionActive: snapshot.sessionActive,
|
|
344
|
+
sessions,
|
|
273
345
|
affinity: this.ledger.affinityView(Date.now()),
|
|
274
346
|
display: { ...this.ledger.snapshot.display },
|
|
275
347
|
pet: {
|
package/lib/types/state.js
CHANGED
|
@@ -94,11 +94,11 @@ export class PetStateMachine {
|
|
|
94
94
|
animation = 'idle';
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
// Settled sessions never bubble: idle (e.g. an aborted/stopped turn)
|
|
98
|
+
// and done past its celebration window both fall silent.
|
|
99
|
+
const settled = this.phase === 'idle'
|
|
100
|
+
|| (this.phase === 'done' && this.doneAt !== undefined && nowMs - this.doneAt >= this.config.celebrateMs);
|
|
101
|
+
const bubble = settled ? undefined : this.phrase ?? this.line;
|
|
102
102
|
return {
|
|
103
103
|
animation,
|
|
104
104
|
...(bubble === undefined ? {} : { bubble }),
|
package/lib/types/treats.d.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* two sources, both tied to companionship:
|
|
4
4
|
* - work output: every N completed turns grant one treat;
|
|
5
5
|
* - time output: every T minutes of wall-clock time grant one treat.
|
|
6
|
+
* Difficulty tuned deliberately high (10x the original cadence): treats are
|
|
7
|
+
* a rare delicacy the pet earns through sustained companionship, not a
|
|
8
|
+
* routine drop.
|
|
6
9
|
* Feeding consumes one treat. Settlement is lazy: it runs whenever the host
|
|
7
10
|
* serves a state snapshot or an interaction, so there is no timer and no
|
|
8
11
|
* drift — elapsed periods are computed from the persisted last-grant marks.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"treats.d.ts","sourceRoot":"","sources":["../../src/treats.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"treats.d.ts","sourceRoot":"","sources":["../../src/treats.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,4BAA4B;AAC5B,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,aAAa,EAAE,MAAM,CAAA;IACrB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,kBAAkB,EAAE,WAIhC,CAAA;AAED,mDAAmD;AACnD,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,iGAAiG;IACjG,gBAAgB,EAAE,MAAM,CAAA;IACxB,8EAA8E;IAC9E,qBAAqB,EAAE,MAAM,CAAA;CAC9B;AAED,wBAAgB,gBAAgB,IAAI,WAAW,CAE9C;AAED,sCAAsC;AACtC,MAAM,WAAW,eAAe;IAC9B,2CAA2C;IAC3C,MAAM,EAAE,WAAW,CAAA;IACnB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAA;CACf;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,WAAgC,GACvC,eAAe,CA6BjB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,WAAW,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAGnD"}
|
package/lib/types/treats.js
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
* two sources, both tied to companionship:
|
|
4
4
|
* - work output: every N completed turns grant one treat;
|
|
5
5
|
* - time output: every T minutes of wall-clock time grant one treat.
|
|
6
|
+
* Difficulty tuned deliberately high (10x the original cadence): treats are
|
|
7
|
+
* a rare delicacy the pet earns through sustained companionship, not a
|
|
8
|
+
* routine drop.
|
|
6
9
|
* Feeding consumes one treat. Settlement is lazy: it runs whenever the host
|
|
7
10
|
* serves a state snapshot or an interaction, so there is no timer and no
|
|
8
11
|
* drift — elapsed periods are computed from the persisted last-grant marks.
|
|
9
12
|
* @module @linxin666/dsh-pet/treats
|
|
10
13
|
*/
|
|
11
14
|
export const defaultTreatConfig = {
|
|
12
|
-
turnsPerTreat:
|
|
13
|
-
timeTreatMs:
|
|
15
|
+
turnsPerTreat: 30,
|
|
16
|
+
timeTreatMs: 300 * 60_000,
|
|
14
17
|
maxTreats: 20,
|
|
15
18
|
};
|
|
16
19
|
export function emptyTreatLedger() {
|
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.1.
|
|
4
|
+
"version": "0.1.19",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
package/src/affinity.test.ts
CHANGED
|
@@ -52,6 +52,14 @@ describe('applyInteraction', () => {
|
|
|
52
52
|
const state = { ...emptyAffinity(), points: AFFINITY_MAX - 1 }
|
|
53
53
|
const outcome = applyInteraction(state, 'pet', 1_000_000)
|
|
54
54
|
expect(outcome.affinity.points).toBe(AFFINITY_MAX)
|
|
55
|
+
expect(outcome.affinity.points).toBe(999_999_999)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('keeps the legacy first lines as the default reactions', () => {
|
|
59
|
+
const outcome = applyInteraction(emptyAffinity(), 'pet', 1_000_000)
|
|
60
|
+
expect(outcome.reaction).toBe('咕噜咕噜~被摸摸好舒服!')
|
|
61
|
+
const feed = applyInteraction(emptyAffinity(), 'feed', 1_000_000)
|
|
62
|
+
expect(feed.reaction).toBe('呜哇!小鱼干好好吃!')
|
|
55
63
|
})
|
|
56
64
|
})
|
|
57
65
|
|
|
@@ -71,4 +79,15 @@ describe('rankOf', () => {
|
|
|
71
79
|
expect(rankOf(AFFINITY_MAX).name).toBe(AFFINITY_RANKS[AFFINITY_RANKS.length - 1]!.name)
|
|
72
80
|
expect(rankOf(-1).name).toBe(AFFINITY_RANKS[0]!.name)
|
|
73
81
|
})
|
|
82
|
+
|
|
83
|
+
it('extends past the removed 100 cap with higher tiers', () => {
|
|
84
|
+
// The original four tiers keep their thresholds; the ladder now reaches
|
|
85
|
+
// into the 999,999,999 range instead of freezing at 80 points.
|
|
86
|
+
expect(rankOf(80).name).toBe('深海羁绊')
|
|
87
|
+
expect(rankOf(200).name).toBe('心有灵犀')
|
|
88
|
+
expect(rankOf(100_000).name).toBe('鲸生共渡')
|
|
89
|
+
expect(rankOf(1_000_000).name).toBe('鲸生共渡')
|
|
90
|
+
expect(AFFINITY_RANKS[AFFINITY_RANKS.length - 1]!.min).toBe(100_000)
|
|
91
|
+
expect(AFFINITY_MAX).toBe(999_999_999)
|
|
92
|
+
})
|
|
74
93
|
})
|
package/src/affinity.ts
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
* work together and care for it: every completed turn earns a small reward,
|
|
4
4
|
* petting earns a tiny one (cooldown-gated), feeding earns the most.
|
|
5
5
|
* Persistence lives in the service; this module only computes transitions.
|
|
6
|
+
* Reaction copy resolves from the shared remark library (first line of each
|
|
7
|
+
* built-in pool); the ledger layers per-pet custom remarks on top.
|
|
6
8
|
* @module @linxin666/dsh-pet/affinity
|
|
7
9
|
*/
|
|
8
10
|
|
|
11
|
+
import { builtinRemark } from './remarks.ts'
|
|
12
|
+
|
|
9
13
|
/** One interaction the user can perform on the pet. */
|
|
10
14
|
export type PetInteraction = 'pet' | 'feed'
|
|
11
15
|
|
|
@@ -25,16 +29,27 @@ export interface AffinityState {
|
|
|
25
29
|
turns: number
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Affinity points cap. Historically 100; removed so long-term companions
|
|
34
|
+
* keep growing — the default limit is now the full 999,999,999 range.
|
|
35
|
+
*/
|
|
36
|
+
export const AFFINITY_MAX = 999_999_999
|
|
29
37
|
|
|
30
38
|
/** Affinity ranks by points; the pet visibly grows with its rank.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
39
|
+
* The original four tiers (0/25/50/80) are unchanged; higher tiers reach
|
|
40
|
+
* into the extended cap so the ladder stays meaningful for veteran
|
|
41
|
+
* companions. Marker glyphs are plain ASCII (the repo bans all emoji
|
|
42
|
+
* characters); they read as a growing star trail alongside the rank name. */
|
|
33
43
|
export const AFFINITY_RANKS = [
|
|
34
44
|
{ min: 0, name: '幼鲸', emoji: '*' },
|
|
35
45
|
{ min: 25, name: '伙伴', emoji: '**' },
|
|
36
46
|
{ min: 50, name: '挚友', emoji: '***' },
|
|
37
47
|
{ min: 80, name: '深海羁绊', emoji: '****' },
|
|
48
|
+
{ min: 200, name: '心有灵犀', emoji: '*****' },
|
|
49
|
+
{ min: 500, name: '传说羁绊', emoji: '******' },
|
|
50
|
+
{ min: 2_000, name: '神话羁绊', emoji: '*******' },
|
|
51
|
+
{ min: 10_000, name: '永恒之契', emoji: '********' },
|
|
52
|
+
{ min: 100_000, name: '鲸生共渡', emoji: '*********' },
|
|
38
53
|
] as const
|
|
39
54
|
|
|
40
55
|
/** Interaction tuning (all in points / ms). */
|
|
@@ -136,7 +151,7 @@ export function applyInteraction(
|
|
|
136
151
|
const next = { ...state }
|
|
137
152
|
if (kind === 'pet') {
|
|
138
153
|
if (state.lastPetAt !== 0 && nowMs - state.lastPetAt < config.petCooldownMs) {
|
|
139
|
-
return { affinity: state, delta: 0, reaction: '
|
|
154
|
+
return { affinity: state, delta: 0, reaction: builtinRemark('petCooldown'), accepted: false }
|
|
140
155
|
}
|
|
141
156
|
next.lastPetAt = nowMs
|
|
142
157
|
next.pets += 1
|
|
@@ -144,13 +159,13 @@ export function applyInteraction(
|
|
|
144
159
|
return {
|
|
145
160
|
affinity: next,
|
|
146
161
|
delta: config.petReward,
|
|
147
|
-
reaction: '
|
|
162
|
+
reaction: builtinRemark('pet'),
|
|
148
163
|
accepted: true,
|
|
149
164
|
}
|
|
150
165
|
}
|
|
151
166
|
if (kind === 'feed') {
|
|
152
167
|
if (state.lastFeedAt !== 0 && nowMs - state.lastFeedAt < config.feedCooldownMs) {
|
|
153
|
-
return { affinity: state, delta: 0, reaction: '
|
|
168
|
+
return { affinity: state, delta: 0, reaction: builtinRemark('feedCooldown'), accepted: false }
|
|
154
169
|
}
|
|
155
170
|
next.lastFeedAt = nowMs
|
|
156
171
|
next.feeds += 1
|
|
@@ -158,7 +173,7 @@ export function applyInteraction(
|
|
|
158
173
|
return {
|
|
159
174
|
affinity: next,
|
|
160
175
|
delta: config.feedReward,
|
|
161
|
-
reaction: '
|
|
176
|
+
reaction: builtinRemark('feed'),
|
|
162
177
|
accepted: true,
|
|
163
178
|
}
|
|
164
179
|
}
|