dsh-pet 0.1.6 → 0.1.7

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/lib/client.js CHANGED
@@ -19,20 +19,158 @@ const pickWeightedCategory = (categories, facing) => {
19
19
  }
20
20
  return eligible[eligible.length - 1];
21
21
  };
22
+ const rollKind = (roll, w) => {
23
+ const topEnd = (w.idle + w.turn + w.move) / 100;
24
+ if (roll < w.idle / 100) return "idle";
25
+ if (roll < (w.idle + w.turn) / 100) return "turn";
26
+ if (roll < topEnd) return "move";
27
+ return "action";
28
+ };
29
+ const pickCategoryAction = (categories, idlePool, facing, current) => {
30
+ const cat = pickWeightedCategory(categories, facing);
31
+ if (!cat) return {
32
+ id: "FALLBACK",
33
+ name: pick(idlePool, current)
34
+ };
35
+ return {
36
+ id: cat.id,
37
+ name: pick(cat.actions, current)
38
+ };
39
+ };
40
+
41
+ //#endregion
42
+ //#region src/client/motion.ts
43
+ const planMove = (o) => {
44
+ const distance = randomBetween(o.minDist, o.maxDist);
45
+ const target = o.cx + o.dir * distance;
46
+ const leftBound = o.margin + o.halfW;
47
+ const rightBound = o.W - o.margin - o.halfW;
48
+ if (target < leftBound || target > rightBound) return null;
49
+ return {
50
+ startRatio: o.cx / o.W,
51
+ startYRatio: o.cy / o.H,
52
+ targetRatio: target / o.W,
53
+ totalRatio: Math.abs(target - o.cx) / o.W
54
+ };
55
+ };
56
+
57
+ //#endregion
58
+ //#region src/client/config.ts
59
+ const stripJsonc = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^\\:])\/\/.*$/gm, "$1").trim();
60
+ const CORNERS = [
61
+ "top-left",
62
+ "top-right",
63
+ "bottom-left",
64
+ "bottom-right"
65
+ ];
66
+ /** corner 合法性检查用的 string 集合(Corner[] 的 includes 要求 Corner 参数,无法接收未知 string) */
67
+ const CORNER_SET = new Set(CORNERS);
68
+ const EMPTY_CONF = {
69
+ pets: [],
70
+ animations: {
71
+ idle: [],
72
+ turn: [],
73
+ drag: [],
74
+ clicks: [],
75
+ moves: {
76
+ default: {},
77
+ actions: []
78
+ },
79
+ categories: []
80
+ },
81
+ animationWeights: {
82
+ idle: 0,
83
+ turn: 0,
84
+ move: 0
85
+ }
86
+ };
87
+ function assertClientConfig(raw) {
88
+ if (!raw || typeof raw !== "object") throw new Error("dsh-pet: config 非对象");
89
+ const cfg = raw;
90
+ const petsArr = cfg.pets;
91
+ if (!Array.isArray(petsArr) || !petsArr.length) throw new Error("dsh-pet: 缺少 pets");
92
+ const seen = new Set();
93
+ const pets = [];
94
+ for (const p of petsArr) {
95
+ const id = String(p?.id ?? "");
96
+ if (!id || seen.has(id)) throw new Error("dsh-pet: pet id 非法或重复「" + id + "」");
97
+ const size = Number(p?.size);
98
+ if (!Number.isFinite(size) || size <= 0) throw new Error("dsh-pet: pet「" + id + "」大小非法");
99
+ const corner = p?.position?.corner;
100
+ if (typeof corner !== "string" || !CORNER_SET.has(corner)) throw new Error("dsh-pet: pet「" + id + "」corner 非法");
101
+ const marginX = Number(p?.position?.marginX);
102
+ const marginY = Number(p?.position?.marginY);
103
+ if (!Number.isFinite(marginX) || !Number.isFinite(marginY)) throw new Error("dsh-pet: pet「" + id + "」边距非法");
104
+ seen.add(id);
105
+ pets.push({
106
+ id,
107
+ size,
108
+ position: {
109
+ corner,
110
+ marginX,
111
+ marginY
112
+ }
113
+ });
114
+ }
115
+ const a = cfg.animations;
116
+ if (!a || typeof a !== "object") throw new Error("dsh-pet: 缺少 animations");
117
+ for (const key of [
118
+ "idle",
119
+ "turn",
120
+ "drag",
121
+ "clicks"
122
+ ]) if (!Array.isArray(a[key])) throw new Error("dsh-pet: animations." + key + " 缺失");
123
+ if (!a.moves || typeof a.moves !== "object" || typeof a.moves.default !== "object" || a.moves.default === null || !Array.isArray(a.moves.actions)) throw new Error("dsh-pet: animations.moves 结构非法");
124
+ if (!Array.isArray(a.categories)) throw new Error("dsh-pet: animations.categories 缺失");
125
+ const w = cfg.animationWeights;
126
+ if (!w || typeof w !== "object") throw new Error("dsh-pet: 缺少 animationWeights");
127
+ for (const key of [
128
+ "idle",
129
+ "turn",
130
+ "move"
131
+ ]) {
132
+ const v = Number(w[key]);
133
+ if (!Number.isFinite(v) || v < 0) throw new Error("dsh-pet: animationWeights." + key + " 非法");
134
+ w[key] = v;
135
+ }
136
+ return {
137
+ pets,
138
+ animations: a,
139
+ animationWeights: w
140
+ };
141
+ }
142
+ function resolvePets(defaults, user) {
143
+ if (user && Array.isArray(user.pets)) return user.pets.length ? user.pets : defaults;
144
+ return defaults;
145
+ }
146
+ function applyUserOverrides(base, user) {
147
+ const next = {
148
+ ...base,
149
+ pets: resolvePets(base.pets, user)
150
+ };
151
+ if (user.animations) next.animations = user.animations;
152
+ if (user.animationWeights) next.animationWeights = user.animationWeights;
153
+ return next;
154
+ }
155
+
156
+ //#endregion
157
+ //#region src/client/constants.ts
158
+ const CANVAS_H = 360;
159
+ const FEET_Y = 330;
160
+ const HIT_BOX = {
161
+ x0: 200,
162
+ y0: 50,
163
+ x1: 440,
164
+ y1: 335
165
+ };
166
+ const DRAG_THRESHOLD = 5;
22
167
 
23
168
  //#endregion
24
169
  //#region src/client/settings.ts
25
- const DEFAULT_PETS = [{
26
- id: "main",
27
- size: 462,
28
- corner: "top-right",
29
- marginX: 24,
30
- marginY: 100
31
- }];
32
170
  const petBridge = {
33
- current: null,
34
- sync: null,
35
- template: null
171
+ current: [],
172
+ sync: () => {},
173
+ template: void 0
36
174
  };
37
175
  const NS = "pet.config";
38
176
  const zh = {
@@ -41,6 +179,9 @@ const zh = {
41
179
  petsLabel: "宠物列表",
42
180
  add: "添加宠物",
43
181
  remove: "删除",
182
+ confirmRemove: "确定删除宠物「{id}」吗?",
183
+ confirmTitle: "确认操作",
184
+ cancel: "取消",
44
185
  atLeastOne: "至少保留一个宠物。",
45
186
  emptyPets: "暂无宠物,点击「添加宠物」创建。",
46
187
  sizeLabel: "大小(宽度 px)",
@@ -54,6 +195,13 @@ const zh = {
54
195
  marginY: "垂直偏移",
55
196
  save: "保存",
56
197
  reset: "恢复默认",
198
+ confirmReset: "确定恢复默认吗?将删除整个用户配置(含自定义的动画池与播放权重)。",
199
+ resetHint: "「重置」会删除整个用户配置(含自定义的动画池与播放权重),不只是宠物列表。",
200
+ configMeta: "高级配置(文件)",
201
+ configMetaHint: "用户配置可覆盖宠物列表 / 动画池 / 播放权重,修改后刷新或重启生效;默认配置为完整参考。",
202
+ defaultConfig: "默认配置(只读,完整参考)",
203
+ userConfig: "用户配置(自定义覆盖)",
204
+ animationDir: "动画素材目录(可自定义/扩充动画)",
57
205
  saved: "已保存,桌宠即时生效。",
58
206
  loadError: "加载配置失败",
59
207
  invalid: "请检查输入:大小需为正数,边距可为任意数字。",
@@ -65,6 +213,9 @@ const en = {
65
213
  petsLabel: "Pets",
66
214
  add: "Add pet",
67
215
  remove: "Remove",
216
+ confirmRemove: "Delete pet \"{id}\"?",
217
+ confirmTitle: "Confirm action",
218
+ cancel: "Cancel",
68
219
  atLeastOne: "Keep at least one pet.",
69
220
  emptyPets: "No pets yet — click \"Add pet\" to create one.",
70
221
  sizeLabel: "Size (width px)",
@@ -78,13 +229,20 @@ const en = {
78
229
  marginY: "Vertical offset",
79
230
  save: "Save",
80
231
  reset: "Reset to default",
232
+ confirmReset: "Reset to default? This deletes the whole user config (including custom animation pools & weights).",
233
+ resetHint: "\"Reset\" deletes the whole user config (including custom animation pools & weights), not just the pet list.",
234
+ configMeta: "Advanced (files)",
235
+ configMetaHint: "User config may override pets / animation pools / weights — refresh or restart to apply. The default config is the complete reference.",
236
+ defaultConfig: "Default config (read-only, complete reference)",
237
+ userConfig: "User config (custom overrides)",
238
+ animationDir: "Animation assets dir (add/customize animations here)",
81
239
  saved: "Saved — the pets updated instantly.",
82
240
  loadError: "Failed to load config",
83
241
  invalid: "Check your input: size must be positive; margins can be any number.",
84
242
  busy: "Saving…"
85
243
  };
86
244
  function makePetConfigSection(rt) {
87
- const { h, useState, t } = rt;
245
+ const { h, useState, useEffect, t } = rt;
88
246
  const CORNERS$1 = [
89
247
  "top-left",
90
248
  "top-right",
@@ -103,7 +261,6 @@ function makePetConfigSection(rt) {
103
261
  minHeight: "28px",
104
262
  outline: "none"
105
263
  };
106
- const stripJsonc$1 = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^\\:])\/\/.*$/gm, "$1").trim();
107
264
  /** 生成一个未占用的宠物 id(pet-2、pet-3…) */
108
265
  const nextId = (list) => {
109
266
  let n = 2;
@@ -112,48 +269,49 @@ function makePetConfigSection(rt) {
112
269
  if (!list.some((p) => p.id === id)) return id;
113
270
  }
114
271
  };
115
- const normalizePet$1 = (p) => {
116
- const pos = p && p.position && typeof p.position === "object" ? p.position : {};
117
- return {
118
- id: String(p && p.id ? p.id : "main"),
119
- size: Number(p && p.size) > 0 ? Number(p.size) : DEFAULT_PETS[0].size,
120
- corner: CORNERS$1.includes(pos.corner) ? pos.corner : DEFAULT_PETS[0].corner,
121
- marginX: Number.isFinite(Number(pos.marginX)) ? Number(pos.marginX) : DEFAULT_PETS[0].marginX,
122
- marginY: Number.isFinite(Number(pos.marginY)) ? Number(pos.marginY) : DEFAULT_PETS[0].marginY
123
- };
124
- };
125
- /** 从 config.jsonc 对象提取默认宠物列表:必须为 pets 数组;缺失/为空回落代码兜底 DEFAULT_PETS */
126
- const defaultsFrom = (obj) => {
127
- const arr = obj && Array.isArray(obj.pets) ? obj.pets.filter((p) => p && p.id).map(normalizePet$1) : [];
128
- return arr.length ? arr : DEFAULT_PETS.map((p) => ({ ...p }));
129
- };
130
272
  return function PetConfigSection() {
131
- const initPets = petBridge.current ?? DEFAULT_PETS;
132
- const [pets, setPets] = useState(initPets.map((p) => ({ ...p })));
273
+ const initPets = petBridge.current;
274
+ const [pets, setPets] = useState(initPets.map((p) => ({
275
+ ...p,
276
+ position: { ...p.position }
277
+ })));
133
278
  const [selId, setSelId] = useState(initPets[0]?.id ?? "");
134
279
  const [busy, setBusy] = useState(false);
135
280
  const [msg, setMsg] = useState({
136
281
  kind: "",
137
282
  text: ""
138
283
  });
139
- const sel = pets.find((p) => p.id === selId) ?? pets[0] ?? null;
140
- const updateSel = (patch) => setPets((list) => list.map((p) => p.id === (sel?.id ?? "") ? {
141
- ...p,
142
- ...patch
143
- } : p));
284
+ const [confirm, setConfirm] = useState(null);
285
+ const [paths, setPaths] = useState(null);
286
+ useEffect(() => {
287
+ fetch("/pet/config/meta").then((r) => r.ok ? r.json() : null).then((p) => setPaths(p)).catch(() => console.warn("[dsh-pet] 读取配置文件路径失败"));
288
+ }, []);
289
+ const cur = pets.find((p) => p.id === selId) ?? null;
290
+ const updateSel = (patch) => setPets((list) => list.map((p) => {
291
+ if (p.id !== selId) return p;
292
+ const { position: posPatch,...rest } = patch;
293
+ return {
294
+ ...p,
295
+ ...rest,
296
+ position: posPatch ? {
297
+ ...p.position,
298
+ ...posPatch
299
+ } : p.position
300
+ };
301
+ }));
144
302
  const validated = () => {
145
- for (const p of pets) if (!Number.isFinite(p.size) || p.size <= 0 || !Number.isFinite(p.marginX) || !Number.isFinite(p.marginY)) {
303
+ for (const p of pets) if (!Number.isFinite(p.size) || p.size <= 0 || !Number.isFinite(p.position.marginX) || !Number.isFinite(p.position.marginY)) {
146
304
  setMsg({
147
305
  kind: "err",
148
306
  text: t("invalid")
149
307
  });
150
- return null;
308
+ return false;
151
309
  }
152
- return pets;
310
+ return true;
153
311
  };
154
312
  const save = async () => {
155
- const list = validated();
156
- if (!list) return;
313
+ const isOk = validated();
314
+ if (!isOk) return;
157
315
  setBusy(true);
158
316
  setMsg({
159
317
  kind: "",
@@ -163,19 +321,11 @@ function makePetConfigSection(rt) {
163
321
  const res = await fetch("/pet/config", {
164
322
  method: "PUT",
165
323
  headers: { "content-type": "application/json" },
166
- body: JSON.stringify({ pets: list.map((p) => ({
167
- id: p.id,
168
- size: p.size,
169
- position: {
170
- corner: p.corner,
171
- marginX: p.marginX,
172
- marginY: p.marginY
173
- }
174
- })) })
324
+ body: JSON.stringify({ pets })
175
325
  });
176
326
  if (!res.ok) throw new Error("HTTP " + res.status);
177
- petBridge.current = list;
178
- petBridge.sync?.(list);
327
+ petBridge.current = pets;
328
+ petBridge.sync(pets);
179
329
  setMsg({
180
330
  kind: "ok",
181
331
  text: t("saved")
@@ -189,7 +339,8 @@ function makePetConfigSection(rt) {
189
339
  setBusy(false);
190
340
  }
191
341
  };
192
- const reset = async () => {
342
+ const reset = () => setConfirm("reset");
343
+ const doReset = async () => {
193
344
  setBusy(true);
194
345
  setMsg({
195
346
  kind: "",
@@ -198,12 +349,14 @@ function makePetConfigSection(rt) {
198
349
  try {
199
350
  await fetch("/pet/config", { method: "DELETE" });
200
351
  const defRes = await fetch("/pet/config.jsonc");
201
- const obj = JSON.parse(stripJsonc$1(await defRes.text()));
202
- const defs = defaultsFrom(obj);
203
- setPets(defs.map((p) => ({ ...p })));
352
+ const defs = assertClientConfig(JSON.parse(stripJsonc(await defRes.text()))).pets;
353
+ setPets(defs.map((p) => ({
354
+ ...p,
355
+ position: { ...p.position }
356
+ })));
204
357
  setSelId(defs[0]?.id ?? "");
205
358
  petBridge.current = defs;
206
- petBridge.sync?.(defs);
359
+ petBridge.sync(defs);
207
360
  setMsg({
208
361
  kind: "ok",
209
362
  text: t("saved")
@@ -218,16 +371,14 @@ function makePetConfigSection(rt) {
218
371
  }
219
372
  };
220
373
  const addPet = () => {
374
+ const tpl = petBridge.template;
375
+ if (!tpl) return;
221
376
  const id = nextId(pets);
222
- const tpl = petBridge.template ?? DEFAULT_PETS[0];
223
- const np = {
377
+ setPets((list) => [...list, {
224
378
  id,
225
379
  size: tpl.size,
226
- corner: tpl.corner,
227
- marginX: tpl.marginX,
228
- marginY: tpl.marginY
229
- };
230
- setPets((list) => [...list, np]);
380
+ position: { ...tpl.position }
381
+ }]);
231
382
  setSelId(id);
232
383
  };
233
384
  const removeSel = () => {
@@ -238,7 +389,10 @@ function makePetConfigSection(rt) {
238
389
  });
239
390
  return;
240
391
  }
241
- const list = pets.filter((p) => p.id !== (sel?.id ?? ""));
392
+ setConfirm("remove");
393
+ };
394
+ const doRemove = () => {
395
+ const list = pets.filter((p) => p.id !== selId);
242
396
  setPets(list);
243
397
  setSelId(list[0].id);
244
398
  };
@@ -247,7 +401,7 @@ function makePetConfigSection(rt) {
247
401
  step: key === "size" ? "10" : "1",
248
402
  min: key === "size" ? "120" : "",
249
403
  value: String(value),
250
- disabled: busy || !sel,
404
+ disabled: busy,
251
405
  onChange: (e) => setter(Number(e.target.value)),
252
406
  style: {
253
407
  width,
@@ -302,8 +456,8 @@ function makePetConfigSection(rt) {
302
456
  type: "button",
303
457
  onClick: () => setSelId(p.id),
304
458
  style: {
305
- border: "1px solid " + (p.id === (sel?.id ?? "") ? "var(--dsw-alias-state-business-primary)" : "var(--dsw-alias-border-l2)"),
306
- background: p.id === (sel?.id ?? "") ? "var(--dsw-alias-interactive-bg-active)" : "transparent",
459
+ border: "1px solid " + (p.id === selId ? "var(--dsw-alias-state-business-primary)" : "var(--dsw-alias-border-l2)"),
460
+ background: p.id === selId ? "var(--dsw-alias-interactive-bg-active)" : "transparent",
307
461
  color: "var(--dsw-alias-label-primary)",
308
462
  borderRadius: "8px",
309
463
  padding: "4px 12px",
@@ -329,7 +483,7 @@ function makePetConfigSection(rt) {
329
483
  })
330
484
  ]
331
485
  }),
332
- sel ? h("div", {
486
+ cur ? h("div", {
333
487
  style: {
334
488
  display: "flex",
335
489
  gap: "16px",
@@ -350,7 +504,7 @@ function makePetConfigSection(rt) {
350
504
  },
351
505
  children: [
352
506
  t("sizeLabel"),
353
- field("size", sel.size, (v) => updateSel({ size: v }), "150px"),
507
+ field("size", cur.size, (v) => updateSel({ size: v }), "150px"),
354
508
  h("span", {
355
509
  style: {
356
510
  fontSize: "11px",
@@ -369,9 +523,9 @@ function makePetConfigSection(rt) {
369
523
  color: "var(--dsw-alias-label-secondary)"
370
524
  },
371
525
  children: [t("cornerLabel"), h("select", {
372
- value: sel.corner,
526
+ value: cur.position.corner,
373
527
  disabled: busy,
374
- onChange: (e) => updateSel({ corner: e.target.value }),
528
+ onChange: (e) => updateSel({ position: { corner: e.target.value } }),
375
529
  style: {
376
530
  width: "160px",
377
531
  ...inputStyle
@@ -391,7 +545,7 @@ function makePetConfigSection(rt) {
391
545
  fontSize: "12px",
392
546
  color: "var(--dsw-alias-label-secondary)"
393
547
  },
394
- children: [t("marginX"), field("marginX", sel.marginX, (v) => updateSel({ marginX: v }), "120px")]
548
+ children: [t("marginX"), field("marginX", cur.position.marginX, (v) => updateSel({ position: { marginX: v } }), "120px")]
395
549
  }),
396
550
  h("label", {
397
551
  style: {
@@ -401,7 +555,7 @@ function makePetConfigSection(rt) {
401
555
  fontSize: "12px",
402
556
  color: "var(--dsw-alias-label-secondary)"
403
557
  },
404
- children: [t("marginY"), field("marginY", sel.marginY, (v) => updateSel({ marginY: v }), "120px")]
558
+ children: [t("marginY"), field("marginY", cur.position.marginY, (v) => updateSel({ position: { marginY: v } }), "120px")]
405
559
  }),
406
560
  h("button", {
407
561
  type: "button",
@@ -478,587 +632,643 @@ function makePetConfigSection(rt) {
478
632
  children: msg.text
479
633
  }) : null
480
634
  ]
481
- })
635
+ }),
636
+ h("p", {
637
+ style: {
638
+ margin: 0,
639
+ fontSize: "11px",
640
+ color: "var(--dsw-alias-label-tertiary)",
641
+ lineHeight: "16px"
642
+ },
643
+ children: t("resetHint")
644
+ }),
645
+ paths ? h("div", {
646
+ style: {
647
+ marginTop: "12px",
648
+ padding: "10px 14px",
649
+ border: "1px solid var(--dsw-alias-border-l2)",
650
+ borderRadius: "12px",
651
+ display: "flex",
652
+ flexDirection: "column",
653
+ gap: "6px",
654
+ fontSize: "12px",
655
+ color: "var(--dsw-alias-label-secondary)"
656
+ },
657
+ children: [
658
+ h("div", {
659
+ style: {
660
+ fontSize: "12px",
661
+ color: "var(--dsw-alias-label-primary)",
662
+ fontWeight: 500
663
+ },
664
+ children: t("configMeta")
665
+ }),
666
+ h("div", {
667
+ style: {
668
+ fontSize: "12px",
669
+ lineHeight: "20px"
670
+ },
671
+ children: t("configMetaHint")
672
+ }),
673
+ h("div", {
674
+ style: {
675
+ fontSize: "12px",
676
+ lineHeight: "18px",
677
+ wordBreak: "break-all"
678
+ },
679
+ children: t("defaultConfig") + ":" + paths.default
680
+ }),
681
+ h("div", {
682
+ style: {
683
+ fontSize: "12px",
684
+ lineHeight: "18px",
685
+ wordBreak: "break-all"
686
+ },
687
+ children: t("userConfig") + ":" + paths.user
688
+ }),
689
+ h("div", {
690
+ style: {
691
+ fontSize: "12px",
692
+ lineHeight: "18px",
693
+ wordBreak: "break-all"
694
+ },
695
+ children: t("animationDir") + ":" + paths.animations
696
+ })
697
+ ]
698
+ }) : null,
699
+ confirm ? h("div", {
700
+ style: {
701
+ position: "fixed",
702
+ inset: 0,
703
+ zIndex: 2147483647,
704
+ display: "flex",
705
+ alignItems: "center",
706
+ justifyContent: "center",
707
+ background: "rgba(0, 0, 0, 0.45)"
708
+ },
709
+ onClick: () => setConfirm(null),
710
+ children: h("div", {
711
+ style: {
712
+ width: "340px",
713
+ maxWidth: "calc(100vw - 40px)",
714
+ background: "var(--dsw-alias-bg-layer-1)",
715
+ border: "1px solid var(--dsw-alias-border-l2)",
716
+ borderRadius: "12px",
717
+ padding: "16px 18px",
718
+ boxShadow: "0 8px 30px rgba(0, 0, 0, 0.35)",
719
+ display: "flex",
720
+ flexDirection: "column",
721
+ gap: "12px"
722
+ },
723
+ onClick: (e) => e.stopPropagation(),
724
+ children: [
725
+ h("div", {
726
+ style: {
727
+ fontSize: "14px",
728
+ fontWeight: 500,
729
+ color: "var(--dsw-alias-label-primary)"
730
+ },
731
+ children: t("confirmTitle")
732
+ }),
733
+ h("div", {
734
+ style: {
735
+ fontSize: "13px",
736
+ lineHeight: "20px",
737
+ color: "var(--dsw-alias-label-secondary)"
738
+ },
739
+ children: confirm === "remove" ? t("confirmRemove").replace("{id}", selId) : t("confirmReset")
740
+ }),
741
+ h("div", {
742
+ style: {
743
+ display: "flex",
744
+ gap: "8px",
745
+ justifyContent: "flex-end"
746
+ },
747
+ children: [h("button", {
748
+ type: "button",
749
+ onClick: () => setConfirm(null),
750
+ style: {
751
+ border: "1px solid var(--dsw-alias-border-l2)",
752
+ background: "transparent",
753
+ color: "var(--dsw-alias-label-primary)",
754
+ borderRadius: "8px",
755
+ padding: "4px 14px",
756
+ fontSize: "12px",
757
+ cursor: "pointer"
758
+ },
759
+ children: t("cancel")
760
+ }), h("button", {
761
+ type: "button",
762
+ onClick: () => {
763
+ const k = confirm;
764
+ setConfirm(null);
765
+ if (k === "remove") doRemove();
766
+ else doReset();
767
+ },
768
+ style: confirm === "remove" ? {
769
+ border: "1px solid var(--dsw-alias-state-error-secondary)",
770
+ background: "transparent",
771
+ color: "var(--dsw-alias-state-error-primary)",
772
+ borderRadius: "8px",
773
+ padding: "4px 14px",
774
+ fontSize: "12px",
775
+ cursor: "pointer"
776
+ } : {
777
+ border: "1px solid var(--dsw-alias-button-info-fill)",
778
+ background: "var(--dsw-alias-button-info-fill)",
779
+ color: "#fff",
780
+ borderRadius: "8px",
781
+ padding: "4px 14px",
782
+ fontSize: "12px",
783
+ cursor: "pointer"
784
+ },
785
+ children: confirm === "remove" ? t("remove") : t("reset")
786
+ })]
787
+ })
788
+ ]
789
+ })
790
+ }) : null
482
791
  ]
483
792
  });
484
793
  };
485
794
  }
486
795
 
487
796
  //#endregion
488
- //#region src/client/config.ts
489
- const FALLBACK_IDLE = "待机呼吸休闲";
490
- const DEFAULT_MOVE_PARAMS = {
491
- minDist: 60,
492
- maxDist: 240,
493
- margin: 20,
494
- leadSec: 2,
495
- tailSec: 2
496
- };
497
- const stripJsonc = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^\\:])\/\/.*$/gm, "$1").trim();
498
- const buildMinimalAnim = () => ({
499
- idle: [FALLBACK_IDLE],
500
- turn: [],
501
- drag: [],
502
- clicks: [],
503
- moves: {
504
- default: { ...DEFAULT_MOVE_PARAMS },
505
- actions: []
506
- },
507
- categories: [],
508
- weights: {
509
- idle: 100,
510
- turn: 0,
511
- move: 0
512
- }
513
- });
514
- const buildAnim = (obj) => {
515
- const a = obj?.animations;
516
- const w = obj?.animationWeights ?? {};
517
- const strArr = (x) => Array.isArray(x) ? x.map(String).filter(Boolean) : [];
518
- const idle = strArr(a?.idle);
519
- if (!idle.length) idle.push(FALLBACK_IDLE);
520
- const turn = strArr(a?.turn);
521
- const drag = strArr(a?.drag);
522
- const clicks = strArr(a?.clicks);
523
- let mdef = { ...DEFAULT_MOVE_PARAMS };
524
- let mActs = [];
525
- if (a?.moves && typeof a.moves === "object") {
526
- if (a.moves.default && typeof a.moves.default === "object") mdef = {
527
- ...mdef,
528
- ...a.moves.default
529
- };
530
- if (Array.isArray(a.moves.actions)) {
531
- const list = a.moves.actions.filter((x) => x && x.name).map((x) => ({
532
- name: String(x.name),
533
- params: x.params && typeof x.params === "object" ? { ...x.params } : void 0
534
- }));
535
- if (list.length) mActs = list;
536
- }
797
+ //#region src/client/pet.ts
798
+ /** 运行时配置(PetMulti 加载后赋值;PetCard 只读) */
799
+ let config = EMPTY_CONF;
800
+ /** 内联 CSS —— 注入一次(官方插件标准做法) */
801
+ const css = [
802
+ ".dsh-pet-root{position:fixed;z-index:40;pointer-events:none;user-select:none}",
803
+ ".dsh-pet-root[data-corner=\"bottom-right\"]{right:var(--dsh-pet-mx,24px);bottom:var(--dsh-pet-my,0)}",
804
+ ".dsh-pet-root[data-corner=\"bottom-left\"]{left:var(--dsh-pet-mx,24px);bottom:var(--dsh-pet-my,0)}",
805
+ ".dsh-pet-root[data-corner=\"top-right\"]{right:var(--dsh-pet-mx,24px);top:var(--dsh-pet-my,0)}",
806
+ ".dsh-pet-root[data-corner=\"top-left\"]{left:var(--dsh-pet-mx,24px);top:var(--dsh-pet-my,0)}",
807
+ ".dsh-pet-stage{position:relative;width:var(--dsh-pet-size,462px);height:calc(var(--dsh-pet-size,462px)*9/16);pointer-events:none}",
808
+ ".dsh-pet-video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;pointer-events:none;opacity:0;transition:opacity .18s ease;transform-origin:center}",
809
+ ".dsh-pet-video.is-front{opacity:1}",
810
+ ".dsh-pet-hit{position:absolute;pointer-events:auto;cursor:default;z-index:1}",
811
+ ".dsh-pet-hit.dragging{cursor:grabbing}",
812
+ "@media (prefers-reduced-motion: reduce){.dsh-pet-video{transition:none}}"
813
+ ].join("\n");
814
+ const cssTag = "dsh-pet/style.css";
815
+ function injectCss() {
816
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + cssTag + "\"]") === null) {
817
+ const tag = document.createElement("style");
818
+ tag.dataset.plugin = "dsh-pet";
819
+ tag.dataset.pluginCss = cssTag;
820
+ tag.textContent = css;
821
+ document.head.appendChild(tag);
537
822
  }
538
- let cats = [];
539
- if (Array.isArray(a?.categories)) {
540
- const list = a.categories.filter((c) => c && c.id && Array.isArray(c.actions) && c.actions.length).map((c) => ({
541
- id: String(c.id),
542
- weight: Number(c.weight),
543
- noMirror: !!c.noMirror,
544
- actions: c.actions.map(String)
545
- }));
546
- if (list.length) cats = list;
547
- }
548
- return {
549
- idle,
550
- turn,
551
- drag,
552
- clicks,
553
- moves: {
554
- default: mdef,
555
- actions: mActs
556
- },
557
- categories: cats,
558
- weights: {
559
- idle: Number(w.idle) >= 0 ? Number(w.idle) : 10,
560
- turn: Number(w.turn) >= 0 ? Number(w.turn) : 5,
561
- move: Number(w.move) >= 0 ? Number(w.move) : 5
562
- }
563
- };
564
- };
565
- const CORNERS = [
566
- "top-left",
567
- "top-right",
568
- "bottom-left",
569
- "bottom-right"
570
- ];
571
- function normalizePet(p) {
572
- const pos = p && p.position && typeof p.position === "object" ? p.position : {};
573
- return {
574
- id: String(p && p.id ? p.id : "main"),
575
- size: Number(p && p.size) > 0 ? Number(p.size) : DEFAULT_PETS[0].size,
576
- corner: CORNERS.indexOf(pos.corner) !== -1 ? pos.corner : DEFAULT_PETS[0].corner,
577
- marginX: Number.isFinite(Number(pos.marginX)) ? Number(pos.marginX) : DEFAULT_PETS[0].marginX,
578
- marginY: Number.isFinite(Number(pos.marginY)) ? Number(pos.marginY) : DEFAULT_PETS[0].marginY
579
- };
580
- }
581
- function extractDefaultPets(obj) {
582
- const arr = obj && Array.isArray(obj.pets) ? obj.pets.filter((p) => p && p.id) : [];
583
- if (!arr.length) return DEFAULT_PETS.map((p) => ({ ...p }));
584
- const seen = new Set();
585
- const out = [];
586
- for (const p of arr) {
587
- const id = String(p.id);
588
- if (!seen.has(id)) {
589
- seen.add(id);
590
- out.push(normalizePet(p));
591
- }
592
- }
593
- return out.length ? out : DEFAULT_PETS.map((p) => ({ ...p }));
594
- }
595
- function resolvePets(defaults, user) {
596
- if (user && Array.isArray(user.pets)) {
597
- const list = user.pets.filter((p) => p && p.id).map(normalizePet);
598
- return list.length ? list : defaults;
599
- }
600
- return defaults;
601
823
  }
602
-
603
- //#endregion
604
- //#region src/client/constants.ts
605
- const CANVAS_H = 360;
606
- const FEET_Y = 330;
607
- const HIT_BOX = {
608
- x0: 200,
609
- y0: 50,
610
- x1: 440,
611
- y1: 335
612
- };
613
- const DRAG_THRESHOLD = 5;
614
-
615
- //#endregion
616
- //#region src/client/app.ts
617
- function makeFactory() {
618
- return (require) => {
619
- var module = { exports: {} };
620
- const react = require("react");
621
- const { useEffect, useRef, useState } = react;
622
- const { jsx: h } = require("react/jsx-runtime");
623
- const css = [
624
- ".dsh-pet-root{position:fixed;z-index:40;pointer-events:none;user-select:none}",
625
- ".dsh-pet-root[data-corner=\"bottom-right\"]{right:var(--dsh-pet-mx,24px);bottom:var(--dsh-pet-my,0)}",
626
- ".dsh-pet-root[data-corner=\"bottom-left\"]{left:var(--dsh-pet-mx,24px);bottom:var(--dsh-pet-my,0)}",
627
- ".dsh-pet-root[data-corner=\"top-right\"]{right:var(--dsh-pet-mx,24px);top:var(--dsh-pet-my,0)}",
628
- ".dsh-pet-root[data-corner=\"top-left\"]{left:var(--dsh-pet-mx,24px);top:var(--dsh-pet-my,0)}",
629
- ".dsh-pet-stage{position:relative;width:var(--dsh-pet-size,462px);height:calc(var(--dsh-pet-size,462px)*9/16);pointer-events:none}",
630
- ".dsh-pet-video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;pointer-events:none;opacity:0;transition:opacity .18s ease;transform-origin:center}",
631
- ".dsh-pet-video.is-front{opacity:1}",
632
- ".dsh-pet-hit{position:absolute;pointer-events:auto;cursor:default;z-index:1}",
633
- ".dsh-pet-hit.dragging{cursor:grabbing}",
634
- "@media (prefers-reduced-motion: reduce){.dsh-pet-video{transition:none}}"
635
- ].join("\n");
636
- const cssTag = "dsh-pet/style.css";
637
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + cssTag + "\"]") === null) {
638
- const tag = document.createElement("style");
639
- tag.dataset.plugin = "dsh-pet";
640
- tag.dataset.pluginCss = cssTag;
641
- tag.textContent = css;
642
- document.head.appendChild(tag);
643
- }
644
- let ANIM = buildMinimalAnim();
645
- function PetCard({ cfg }) {
646
- const [size, setSize] = useState(cfg.size);
647
- const halfW = size / 2;
648
- const halfH = size * 9 / 16 / 2;
649
- const [anim, setAnim] = useState(ANIM.idle[0] ?? "");
650
- const [once, setOnce] = useState(true);
651
- const [facing, setFacing] = useState("left");
652
- const [dragging, setDragging] = useState(false);
653
- const [customPos, setCustomPos] = useState(null);
654
- const [corner, setCorner] = useState(cfg.corner);
655
- const [margin, setMargin] = useState({
656
- x: cfg.marginX,
657
- y: cfg.marginY
658
- });
659
- useEffect(() => {
660
- setSize(cfg.size);
661
- setCorner(cfg.corner);
662
- setMargin({
663
- x: cfg.marginX,
664
- y: cfg.marginY
665
- });
666
- }, [
667
- cfg.size,
668
- cfg.corner,
669
- cfg.marginX,
670
- cfg.marginY
671
- ]);
672
- const [seq, setSeq] = useState(0);
673
- const rootRef = useRef(null);
674
- const stageRef = useRef(null);
675
- const videoARef = useRef(null);
676
- const videoBRef = useRef(null);
677
- const frontRef = useRef(0);
678
- const pendingRef = useRef(null);
679
- const genRef = useRef(0);
680
- const dragRef = useRef({
681
- active: false,
682
- dragging: false,
683
- sx: 0,
684
- sy: 0,
685
- offX: 0,
686
- offY: 0
824
+ function makePetUI(rt) {
825
+ const { h, useState, useEffect, useRef } = rt;
826
+ injectCss();
827
+ /** 单个宠物实例(配置由容器 PetMulti 传入) */
828
+ function PetCard({ cfg }) {
829
+ const [size, setSize] = useState(cfg.size);
830
+ const halfW = size / 2;
831
+ const halfH = size * 9 / 16 / 2;
832
+ const [anim, setAnim] = useState(config.animations.idle[0] ?? "");
833
+ const [once, setOnce] = useState(true);
834
+ const [facing, setFacing] = useState("left");
835
+ const [dragging, setDragging] = useState(false);
836
+ const [customPos, setCustomPos] = useState(null);
837
+ const [corner, setCorner] = useState(cfg.position.corner);
838
+ const [margin, setMargin] = useState({
839
+ x: cfg.position.marginX,
840
+ y: cfg.position.marginY
841
+ });
842
+ useEffect(() => {
843
+ setSize(cfg.size);
844
+ setCorner(cfg.position.corner);
845
+ setMargin({
846
+ x: cfg.position.marginX,
847
+ y: cfg.position.marginY
687
848
  });
688
- const justDraggedRef = useRef(false);
689
- const animRef = useRef(anim);
690
- animRef.current = anim;
691
- const switchTo = (next, nextOnce) => {
692
- if (!next) return;
693
- const pending = pendingRef.current;
694
- if (pending && pending.anim === next && pending.once === nextOnce) return;
695
- const gen = ++genRef.current;
696
- pendingRef.current = {
697
- anim: next,
698
- once: nextOnce,
699
- gen
700
- };
701
- const target = frontRef.current === 0 ? videoBRef : videoARef;
702
- const el = target.current;
703
- if (!el) return;
704
- el.src = "/pet/thumb/" + encodeURIComponent(next) + ".webm";
705
- el.loop = !nextOnce;
706
- el.muted = true;
707
- el.autoplay = true;
708
- el.playsInline = true;
709
- el.onended = nextOnce ? handleEnded : null;
710
- el.load();
711
- const onReady = () => {
712
- el.removeEventListener("loadeddata", onReady);
713
- if (pendingRef.current?.gen !== gen) return;
714
- const old = frontRef.current === 0 ? videoARef : videoBRef;
715
- el.classList.add("is-front");
716
- if (old.current && old.current !== el) old.current.classList.remove("is-front");
717
- frontRef.current = frontRef.current === 0 ? 1 : 0;
718
- pendingRef.current = null;
719
- el.style.transform = facingRef.current === "right" ? "scaleX(-1)" : "";
720
- el.play().catch(() => {});
721
- if (pendingMoveRef.current) startMoveDrive(el);
722
- };
723
- el.addEventListener("loadeddata", onReady);
724
- if (el.readyState >= 2) onReady();
725
- };
726
- useEffect(() => {
727
- switchTo(anim, once);
728
- }, [
729
- anim,
730
- once,
731
- seq
732
- ]);
733
- useEffect(() => () => stopMove(), []);
734
- useEffect(() => {
735
- const onResize = () => setCustomPos((prev) => prev ? { ...prev } : prev);
736
- window.addEventListener("resize", onResize);
737
- return () => window.removeEventListener("resize", onResize);
738
- }, []);
739
- const pickNext = () => {
740
- const roll = Math.random();
741
- const wI = ANIM.weights.idle;
742
- const wT = ANIM.weights.turn;
743
- const wM = ANIM.weights.move;
744
- const topEnd = (wI + wT + wM) / 100;
745
- let kind = "";
746
- let next = "";
747
- if (roll < wI / 100) {
748
- kind = "IDLE";
749
- next = pick(ANIM.idle, animRef.current);
750
- setAnim(next);
751
- } else if (roll < (wI + wT) / 100) {
752
- kind = "TURN";
753
- next = pick(ANIM.turn, animRef.current);
754
- setAnim(next);
755
- } else if (roll < topEnd) if (!tryMove()) {
756
- const cat = pickWeightedCategory(ANIM.categories, facingRef.current);
757
- kind = cat ? cat.id : "FALLBACK";
758
- next = cat ? pick(cat.actions, animRef.current) : pick(ANIM.idle, animRef.current);
759
- setAnim(next);
760
- } else {
761
- kind = "MOVES";
762
- next = "移动(池内随机)";
763
- }
764
- else {
765
- const cat = pickWeightedCategory(ANIM.categories, facingRef.current);
766
- kind = cat ? cat.id : "FALLBACK";
767
- next = cat ? pick(cat.actions, animRef.current) : pick(ANIM.idle, animRef.current);
768
- setAnim(next);
769
- }
770
- console.log("[dsh-pet] roll=" + roll.toFixed(4) + " -> [" + kind + "] " + next);
771
- setOnce(true);
772
- setSeq((s) => s + 1);
773
- };
774
- const handleEnded = () => {
775
- if (dragRef.current.active) return;
776
- if (ANIM.turn.includes(animRef.current)) setFacing((f) => f === "left" ? "right" : "left");
777
- if (ANIM.drag.includes(animRef.current) || ANIM.clicks.includes(animRef.current)) {
778
- if (ANIM.idle.length) setAnim(pick(ANIM.idle, animRef.current));
779
- setOnce(true);
780
- setSeq((s) => s + 1);
781
- return;
782
- }
783
- pickNext();
784
- };
785
- const moveRef = useRef(null);
786
- const moveTokenRef = useRef(0);
787
- const pendingMoveRef = useRef(null);
788
- const customPosRef = useRef(customPos);
789
- customPosRef.current = customPos;
790
- const currentCenterX = () => {
791
- const cp = customPosRef.current;
792
- if (cp) return cp.rx * window.innerWidth;
793
- const rootEl = rootRef.current;
794
- if (rootEl) return rootEl.getBoundingClientRect().left + halfW;
795
- return window.innerWidth - 24 - halfW;
796
- };
797
- const currentCenterY = () => {
798
- const cp = customPosRef.current;
799
- if (cp) return cp.ry * window.innerHeight;
800
- const rootEl = rootRef.current;
801
- if (rootEl) return rootEl.getBoundingClientRect().top + halfH;
802
- return window.innerHeight - 20 - halfH;
849
+ }, [
850
+ cfg.size,
851
+ cfg.position.corner,
852
+ cfg.position.marginX,
853
+ cfg.position.marginY
854
+ ]);
855
+ const [seq, setSeq] = useState(0);
856
+ const rootRef = useRef(null);
857
+ const stageRef = useRef(null);
858
+ const videoARef = useRef(null);
859
+ const videoBRef = useRef(null);
860
+ const frontRef = useRef(0);
861
+ const pendingRef = useRef(null);
862
+ const genRef = useRef(0);
863
+ const dragRef = useRef({
864
+ active: false,
865
+ dragging: false,
866
+ sx: 0,
867
+ sy: 0,
868
+ offX: 0,
869
+ offY: 0
870
+ });
871
+ const justDraggedRef = useRef(false);
872
+ const animRef = useRef(anim);
873
+ animRef.current = anim;
874
+ const switchTo = (next, nextOnce) => {
875
+ if (!next) return;
876
+ const pending = pendingRef.current;
877
+ if (pending && pending.anim === next && pending.once === nextOnce) return;
878
+ const gen = ++genRef.current;
879
+ pendingRef.current = {
880
+ anim: next,
881
+ once: nextOnce,
882
+ gen
803
883
  };
804
- const startMoveDrive = (el) => {
805
- const pm = pendingMoveRef.current;
806
- if (!pm || moveRef.current !== null) return;
807
- pendingMoveRef.current = null;
808
- const { startRatio, startYRatio, targetRatio, dir, totalRatio, leadSec, tailSec } = pm;
809
- const duration = Number.isFinite(el.duration) && el.duration > 0 ? el.duration : 10.09;
810
- const travelWindow = Math.max(.1, duration - leadSec - tailSec);
811
- const token = ++moveTokenRef.current;
812
- const step = () => {
813
- if (moveTokenRef.current !== token) return;
814
- const t = el.currentTime || 0;
815
- const rootEl = rootRef.current;
816
- if (rootEl) {
817
- const W = window.innerWidth;
818
- const H = window.innerHeight;
819
- let ratioX;
820
- if (t <= leadSec) ratioX = startRatio;
821
- else if (t >= duration - tailSec) ratioX = targetRatio;
822
- else ratioX = startRatio + dir * totalRatio * ((t - leadSec) / travelWindow);
823
- const px = ratioX * W;
824
- const py = startYRatio * H;
825
- rootEl.style.left = px - halfW + "px";
826
- rootEl.style.top = py - halfH + "px";
827
- rootEl.style.right = "auto";
828
- rootEl.style.bottom = "auto";
829
- }
830
- if (t < duration - tailSec) moveRef.current = requestAnimationFrame(step);
831
- else {
832
- moveRef.current = null;
833
- setCustomPos({
834
- rx: targetRatio,
835
- ry: startYRatio
836
- });
837
- }
838
- };
839
- moveRef.current = requestAnimationFrame(step);
884
+ const target = frontRef.current === 0 ? videoBRef : videoARef;
885
+ const el = target.current;
886
+ if (!el) return;
887
+ el.src = "/pet/thumb/" + encodeURIComponent(next) + ".webm";
888
+ el.loop = !nextOnce;
889
+ el.muted = true;
890
+ el.autoplay = true;
891
+ el.playsInline = true;
892
+ el.onended = nextOnce ? handleEnded : null;
893
+ el.load();
894
+ const onReady = () => {
895
+ el.removeEventListener("loadeddata", onReady);
896
+ if (pendingRef.current?.gen !== gen) return;
897
+ const old = frontRef.current === 0 ? videoARef : videoBRef;
898
+ el.classList.add("is-front");
899
+ if (old.current && old.current !== el) old.current.classList.remove("is-front");
900
+ frontRef.current = frontRef.current === 0 ? 1 : 0;
901
+ pendingRef.current = null;
902
+ el.style.transform = facingRef.current === "right" ? "scaleX(-1)" : "";
903
+ el.play().catch(() => {});
904
+ if (pendingMoveRef.current) startMoveDrive(el);
840
905
  };
841
- const tryMove = () => {
842
- if (moveRef.current !== null || pendingMoveRef.current) return true;
843
- const actions = ANIM.moves.actions;
844
- if (!actions.length) return false;
845
- const chosen = actions[Math.floor(Math.random() * actions.length)];
846
- const mp = Object.assign({}, ANIM.moves.default, chosen.params || {});
847
- const dir = facingRef.current === "right" !== ANIM.turn.includes(animRef.current) ? 1 : -1;
848
- const W = window.innerWidth;
849
- const cx = currentCenterX();
850
- const distance = randomBetween(mp.minDist, mp.maxDist);
851
- const target = cx + dir * distance;
852
- const leftBound = mp.margin + halfW;
853
- const rightBound = W - mp.margin - halfW;
854
- if (target < leftBound || target > rightBound) return false;
855
- pendingMoveRef.current = {
856
- startRatio: cx / W,
857
- startYRatio: currentCenterY() / window.innerHeight,
858
- targetRatio: target / W,
859
- dir,
860
- totalRatio: Math.abs(target - cx) / W,
861
- leadSec: mp.leadSec,
862
- tailSec: mp.tailSec
863
- };
906
+ el.addEventListener("loadeddata", onReady);
907
+ if (el.readyState >= 2) onReady();
908
+ };
909
+ useEffect(() => {
910
+ switchTo(anim, once);
911
+ }, [
912
+ anim,
913
+ once,
914
+ seq
915
+ ]);
916
+ useEffect(() => () => stopMove(), []);
917
+ useEffect(() => {
918
+ const onResize = () => setCustomPos((prev) => prev ? { ...prev } : prev);
919
+ window.addEventListener("resize", onResize);
920
+ return () => window.removeEventListener("resize", onResize);
921
+ }, []);
922
+ const pickNext = () => {
923
+ const { animations, animationWeights } = config;
924
+ const roll = Math.random();
925
+ const k = rollKind(roll, animationWeights);
926
+ let kind;
927
+ let next;
928
+ if (k === "idle") {
929
+ kind = "IDLE";
930
+ next = pick(animations.idle, animRef.current);
931
+ setAnim(next);
932
+ } else if (k === "turn") {
933
+ kind = "TURN";
934
+ next = pick(animations.turn, animRef.current);
935
+ setAnim(next);
936
+ } else if (k === "move") if (!tryMove()) {
937
+ const act = pickCategoryAction(animations.categories, animations.idle, facingRef.current, animRef.current);
938
+ kind = act.id;
939
+ next = act.name;
940
+ setAnim(next);
941
+ } else {
942
+ kind = "MOVES";
943
+ next = "移动(池内随机)";
944
+ }
945
+ else {
946
+ const act = pickCategoryAction(animations.categories, animations.idle, facingRef.current, animRef.current);
947
+ kind = act.id;
948
+ next = act.name;
949
+ setAnim(next);
950
+ }
951
+ console.log("[dsh-pet] " + new Date().toTimeString().slice(0, 8) + " pet=" + cfg.id + " facing=" + facingRef.current + " roll=" + roll.toFixed(4) + " -> [" + kind + "] " + next);
952
+ setOnce(true);
953
+ setSeq((s) => s + 1);
954
+ };
955
+ const handleEnded = () => {
956
+ const { animations } = config;
957
+ if (dragRef.current.active) return;
958
+ if (animations.turn.includes(animRef.current)) {
959
+ const next = facing === "left" ? "right" : "left";
960
+ setFacing(next);
961
+ facingRef.current = next;
962
+ }
963
+ if (animations.drag.includes(animRef.current) || animations.clicks.includes(animRef.current)) {
964
+ if (animations.idle.length) setAnim(pick(animations.idle, animRef.current));
864
965
  setOnce(true);
865
- setAnim(chosen.name);
866
- return true;
867
- };
868
- const stopMove = () => {
869
- pendingMoveRef.current = null;
870
- moveTokenRef.current++;
871
- if (moveRef.current !== null) {
872
- cancelAnimationFrame(moveRef.current);
873
- moveRef.current = null;
874
- }
875
- };
876
- const facingRef = useRef(facing);
877
- facingRef.current = facing;
878
- const handlePointerDown = (e) => {
879
- e.currentTarget.classList.add("dragging");
880
- stopMove();
881
- e.currentTarget.setPointerCapture(e.pointerId);
882
- const rootEl = rootRef.current;
883
- let offX = 0;
884
- let offY = 0;
885
- if (rootEl) {
886
- const rr = rootEl.getBoundingClientRect();
887
- offX = e.clientX - (rr.left + rr.width / 2);
888
- offY = e.clientY - (rr.top + rr.height / 2);
889
- }
890
- dragRef.current = {
891
- active: true,
892
- dragging: false,
893
- sx: e.clientX,
894
- sy: e.clientY,
895
- offX,
896
- offY
897
- };
898
- };
899
- const handlePointerMove = (e) => {
900
- const d = dragRef.current;
901
- if (!d.active) return;
902
- const dx = e.clientX - d.sx;
903
- const dy = e.clientY - d.sy;
904
- if (!d.dragging) {
905
- if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
906
- d.dragging = true;
907
- setDragging(true);
908
- setOnce(true);
909
- if (ANIM.drag.length) setAnim(pick(ANIM.drag));
910
- }
966
+ setSeq((s) => s + 1);
967
+ return;
968
+ }
969
+ pickNext();
970
+ };
971
+ const moveRef = useRef(null);
972
+ const moveTokenRef = useRef(0);
973
+ const pendingMoveRef = useRef(null);
974
+ const customPosRef = useRef(customPos);
975
+ customPosRef.current = customPos;
976
+ const currentCenterX = () => {
977
+ const cp = customPosRef.current;
978
+ if (cp) return cp.rx * window.innerWidth;
979
+ const rootEl = rootRef.current;
980
+ if (rootEl) return rootEl.getBoundingClientRect().left + halfW;
981
+ return window.innerWidth - 24 - halfW;
982
+ };
983
+ const currentCenterY = () => {
984
+ const cp = customPosRef.current;
985
+ if (cp) return cp.ry * window.innerHeight;
986
+ const rootEl = rootRef.current;
987
+ if (rootEl) return rootEl.getBoundingClientRect().top + halfH;
988
+ return window.innerHeight - 20 - halfH;
989
+ };
990
+ const startMoveDrive = (el) => {
991
+ const pm = pendingMoveRef.current;
992
+ if (!pm || moveRef.current !== null) return;
993
+ pendingMoveRef.current = null;
994
+ const { startRatio, startYRatio, targetRatio, dir, totalRatio, leadSec, tailSec } = pm;
995
+ const duration = Number.isFinite(el.duration) && el.duration > 0 ? el.duration : 10.09;
996
+ const travelWindow = Math.max(.1, duration - leadSec - tailSec);
997
+ const token = ++moveTokenRef.current;
998
+ const step = () => {
999
+ if (moveTokenRef.current !== token) return;
1000
+ const t = el.currentTime || 0;
911
1001
  const rootEl = rootRef.current;
912
1002
  if (rootEl) {
913
- rootEl.style.left = e.clientX - d.offX - halfW + "px";
914
- rootEl.style.top = e.clientY - d.offY - halfH + "px";
1003
+ const W = window.innerWidth;
1004
+ const H = window.innerHeight;
1005
+ let ratioX;
1006
+ if (t <= leadSec) ratioX = startRatio;
1007
+ else if (t >= duration - tailSec) ratioX = targetRatio;
1008
+ else ratioX = startRatio + dir * totalRatio * ((t - leadSec) / travelWindow);
1009
+ const px = ratioX * W;
1010
+ const py = startYRatio * H;
1011
+ rootEl.style.left = px - halfW + "px";
1012
+ rootEl.style.top = py - halfH + "px";
915
1013
  rootEl.style.right = "auto";
916
1014
  rootEl.style.bottom = "auto";
917
1015
  }
918
- const stageEl = stageRef.current;
919
- if (stageEl) stageEl.style.transform = "none";
920
- };
921
- const handlePointerUp = (e) => {
922
- const d = dragRef.current;
923
- const wasDragging = d.dragging;
924
- d.active = false;
925
- d.dragging = false;
926
- e.currentTarget.classList.remove("dragging");
927
- if (wasDragging) {
928
- justDraggedRef.current = true;
929
- setTimeout(() => {
930
- justDraggedRef.current = false;
931
- }, 100);
932
- setDragging(false);
1016
+ if (t < duration - tailSec) moveRef.current = requestAnimationFrame(step);
1017
+ else {
1018
+ moveRef.current = null;
933
1019
  setCustomPos({
934
- rx: (e.clientX - d.offX) / window.innerWidth,
935
- ry: (e.clientY - d.offY) / window.innerHeight
1020
+ rx: targetRatio,
1021
+ ry: startYRatio
936
1022
  });
937
- const stageEl = stageRef.current;
938
- if (stageEl) stageEl.style.transform = "translateY(" + bottomPad + "px)";
939
- if (ANIM.idle.length) setAnim(pick(ANIM.idle, animRef.current));
940
- setOnce(false);
941
1023
  }
942
1024
  };
943
- const handleClick = (_e) => {
944
- const d = dragRef.current;
945
- if (d.active || d.dragging || justDraggedRef.current) return;
946
- if (once && !ANIM.idle.includes(animRef.current)) return;
947
- stopMove();
948
- setOnce(true);
949
- if (ANIM.clicks.length) setAnim(pick(ANIM.clicks));
1025
+ moveRef.current = requestAnimationFrame(step);
1026
+ };
1027
+ const tryMove = () => {
1028
+ if (moveRef.current !== null || pendingMoveRef.current) return true;
1029
+ const moves = config.animations.moves;
1030
+ const actions = moves.actions;
1031
+ if (!actions.length) return false;
1032
+ const chosen = actions[Math.floor(Math.random() * actions.length)];
1033
+ const mp = Object.assign({}, moves.default, chosen.params || {});
1034
+ const dir = facingRef.current === "right" !== config.animations.turn.includes(animRef.current) ? 1 : -1;
1035
+ const W = window.innerWidth;
1036
+ const plan = planMove({
1037
+ cx: currentCenterX(),
1038
+ cy: currentCenterY(),
1039
+ W,
1040
+ H: window.innerHeight,
1041
+ dir,
1042
+ minDist: mp.minDist,
1043
+ maxDist: mp.maxDist,
1044
+ margin: mp.margin,
1045
+ halfW
1046
+ });
1047
+ if (!plan) return false;
1048
+ pendingMoveRef.current = {
1049
+ ...plan,
1050
+ dir,
1051
+ leadSec: mp.leadSec,
1052
+ tailSec: mp.tailSec
950
1053
  };
951
- const bottomPad = size * (9 / 16) * (CANVAS_H - FEET_Y) / CANVAS_H;
952
- const stageStyle = dragging ? { transform: "none" } : { transform: "translateY(" + bottomPad + "px)" };
953
- const rootStyle = customPos ? (() => {
954
- const rx = customPos.rx;
955
- const ry = customPos.ry;
956
- const left = Math.min(Math.max(rx * window.innerWidth - halfW, 0), window.innerWidth - size);
957
- const top = Math.min(Math.max(ry * window.innerHeight - halfH, 0), window.innerHeight - size * 9 / 16);
958
- return {
959
- left: left + "px",
960
- top: top + "px",
961
- right: "auto",
962
- bottom: "auto"
963
- };
964
- })() : {};
965
- const commonVideoProps = {
966
- muted: true,
967
- playsInline: true,
968
- autoPlay: true,
969
- title: "dsh-pet"
1054
+ setOnce(true);
1055
+ setAnim(chosen.name);
1056
+ return true;
1057
+ };
1058
+ const stopMove = () => {
1059
+ pendingMoveRef.current = null;
1060
+ moveTokenRef.current++;
1061
+ if (moveRef.current !== null) {
1062
+ cancelAnimationFrame(moveRef.current);
1063
+ moveRef.current = null;
1064
+ }
1065
+ };
1066
+ const facingRef = useRef(facing);
1067
+ facingRef.current = facing;
1068
+ const handlePointerDown = (e) => {
1069
+ e.currentTarget.classList.add("dragging");
1070
+ stopMove();
1071
+ e.currentTarget.setPointerCapture(e.pointerId);
1072
+ const rootEl = rootRef.current;
1073
+ let offX = 0;
1074
+ let offY = 0;
1075
+ if (rootEl) {
1076
+ const rr = rootEl.getBoundingClientRect();
1077
+ offX = e.clientX - (rr.left + rr.width / 2);
1078
+ offY = e.clientY - (rr.top + rr.height / 2);
1079
+ }
1080
+ dragRef.current = {
1081
+ active: true,
1082
+ dragging: false,
1083
+ sx: e.clientX,
1084
+ sy: e.clientY,
1085
+ offX,
1086
+ offY
970
1087
  };
971
- const hitProps = {
972
- className: "dsh-pet-hit",
973
- style: {
974
- left: HIT_BOX.x0 / 640 * 100 + "%",
975
- top: HIT_BOX.y0 / 360 * 100 + "%",
976
- width: (HIT_BOX.x1 - HIT_BOX.x0) / 640 * 100 + "%",
977
- height: (HIT_BOX.y1 - HIT_BOX.y0) / 360 * 100 + "%"
978
- },
979
- onMouseEnter: (e) => {
980
- if (!dragRef.current.active) e.currentTarget.style.cursor = "grab";
981
- },
982
- onMouseLeave: (e) => {
983
- if (!dragRef.current.active) e.currentTarget.style.cursor = "default";
984
- },
985
- onClick: handleClick,
986
- onPointerDown: handlePointerDown,
987
- onPointerMove: handlePointerMove,
988
- onPointerUp: handlePointerUp,
989
- onPointerCancel: handlePointerUp,
990
- title: "dsh-pet"
1088
+ };
1089
+ const handlePointerMove = (e) => {
1090
+ const d = dragRef.current;
1091
+ if (!d.active) return;
1092
+ const dx = e.clientX - d.sx;
1093
+ const dy = e.clientY - d.sy;
1094
+ if (!d.dragging) {
1095
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
1096
+ d.dragging = true;
1097
+ setDragging(true);
1098
+ setOnce(true);
1099
+ if (config.animations.drag.length) setAnim(pick(config.animations.drag));
1100
+ }
1101
+ const rootEl = rootRef.current;
1102
+ if (rootEl) {
1103
+ rootEl.style.left = e.clientX - d.offX - halfW + "px";
1104
+ rootEl.style.top = e.clientY - d.offY - halfH + "px";
1105
+ rootEl.style.right = "auto";
1106
+ rootEl.style.bottom = "auto";
1107
+ }
1108
+ const stageEl = stageRef.current;
1109
+ if (stageEl) stageEl.style.transform = "none";
1110
+ };
1111
+ const handlePointerUp = (e) => {
1112
+ const d = dragRef.current;
1113
+ const wasDragging = d.dragging;
1114
+ d.active = false;
1115
+ d.dragging = false;
1116
+ e.currentTarget.classList.remove("dragging");
1117
+ if (wasDragging) {
1118
+ justDraggedRef.current = true;
1119
+ setTimeout(() => {
1120
+ justDraggedRef.current = false;
1121
+ }, 100);
1122
+ setDragging(false);
1123
+ setCustomPos({
1124
+ rx: (e.clientX - d.offX) / window.innerWidth,
1125
+ ry: (e.clientY - d.offY) / window.innerHeight
1126
+ });
1127
+ const stageEl = stageRef.current;
1128
+ if (stageEl) stageEl.style.transform = "translateY(" + bottomPad + "px)";
1129
+ if (config.animations.idle.length) setAnim(pick(config.animations.idle, animRef.current));
1130
+ setOnce(false);
1131
+ }
1132
+ };
1133
+ const handleClick = () => {
1134
+ const d = dragRef.current;
1135
+ if (d.active || d.dragging || justDraggedRef.current) return;
1136
+ if (once && !config.animations.idle.includes(animRef.current)) return;
1137
+ stopMove();
1138
+ setOnce(true);
1139
+ if (config.animations.clicks.length) setAnim(pick(config.animations.clicks));
1140
+ };
1141
+ const bottomPad = size * (9 / 16) * (CANVAS_H - FEET_Y) / CANVAS_H;
1142
+ const stageStyle = dragging ? { transform: "none" } : { transform: "translateY(" + bottomPad + "px)" };
1143
+ const rootStyle = customPos ? (() => {
1144
+ const rx = customPos.rx;
1145
+ const ry = customPos.ry;
1146
+ const left = Math.min(Math.max(rx * window.innerWidth - halfW, 0), window.innerWidth - size);
1147
+ const top = Math.min(Math.max(ry * window.innerHeight - halfH, 0), window.innerHeight - size * 9 / 16);
1148
+ return {
1149
+ left: left + "px",
1150
+ top: top + "px",
1151
+ right: "auto",
1152
+ bottom: "auto"
991
1153
  };
992
- return h("div", {
993
- ref: rootRef,
994
- className: "dsh-pet-root",
995
- "data-corner": corner,
996
- "data-facing": facing,
997
- style: Object.assign({
998
- "--dsh-pet-size": size + "px",
999
- "--dsh-pet-mx": margin.x + "px",
1000
- "--dsh-pet-my": margin.y + "px"
1001
- }, rootStyle),
1002
- children: h("div", {
1003
- ref: stageRef,
1004
- className: "dsh-pet-stage",
1005
- style: stageStyle,
1006
- children: [
1007
- h("video", Object.assign({}, commonVideoProps, {
1008
- ref: videoARef,
1009
- className: "dsh-pet-video is-front"
1010
- })),
1011
- h("video", Object.assign({}, commonVideoProps, {
1012
- ref: videoBRef,
1013
- className: "dsh-pet-video"
1014
- })),
1015
- h("div", hitProps)
1016
- ]
1017
- })
1018
- });
1019
- }
1020
- function PetMulti() {
1021
- const [pets, setPets] = useState([]);
1022
- const [ready, setReady] = useState(false);
1023
- useEffect(() => {
1024
- let alive = true;
1025
- (async () => {
1154
+ })() : {};
1155
+ const commonVideoProps = {
1156
+ muted: true,
1157
+ playsInline: true,
1158
+ autoPlay: true,
1159
+ title: "dsh-pet"
1160
+ };
1161
+ const hitProps = {
1162
+ className: "dsh-pet-hit",
1163
+ style: {
1164
+ left: HIT_BOX.x0 / 640 * 100 + "%",
1165
+ top: HIT_BOX.y0 / 360 * 100 + "%",
1166
+ width: (HIT_BOX.x1 - HIT_BOX.x0) / 640 * 100 + "%",
1167
+ height: (HIT_BOX.y1 - HIT_BOX.y0) / 360 * 100 + "%"
1168
+ },
1169
+ onMouseEnter: (e) => {
1170
+ if (!dragRef.current.active) e.currentTarget.style.cursor = "grab";
1171
+ },
1172
+ onMouseLeave: (e) => {
1173
+ if (!dragRef.current.active) e.currentTarget.style.cursor = "default";
1174
+ },
1175
+ onClick: handleClick,
1176
+ onPointerDown: handlePointerDown,
1177
+ onPointerMove: handlePointerMove,
1178
+ onPointerUp: handlePointerUp,
1179
+ onPointerCancel: handlePointerUp,
1180
+ title: "dsh-pet"
1181
+ };
1182
+ return h("div", {
1183
+ ref: rootRef,
1184
+ className: "dsh-pet-root",
1185
+ "data-corner": corner,
1186
+ "data-facing": facing,
1187
+ style: Object.assign({
1188
+ "--dsh-pet-size": size + "px",
1189
+ "--dsh-pet-mx": margin.x + "px",
1190
+ "--dsh-pet-my": margin.y + "px"
1191
+ }, rootStyle),
1192
+ children: h("div", {
1193
+ ref: stageRef,
1194
+ className: "dsh-pet-stage",
1195
+ style: stageStyle,
1196
+ children: [
1197
+ h("video", Object.assign({}, commonVideoProps, {
1198
+ ref: videoARef,
1199
+ className: "dsh-pet-video is-front"
1200
+ })),
1201
+ h("video", Object.assign({}, commonVideoProps, {
1202
+ ref: videoBRef,
1203
+ className: "dsh-pet-video"
1204
+ })),
1205
+ h("div", hitProps)
1206
+ ]
1207
+ })
1208
+ });
1209
+ }
1210
+ /** 多开容器:拉取配置 → 合并默认+用户层 pets → 渲染多个 PetCard */
1211
+ function PetMulti() {
1212
+ const [pets, setPets] = useState([]);
1213
+ const [ready, setReady] = useState(false);
1214
+ useEffect(() => {
1215
+ let alive = true;
1216
+ (async () => {
1217
+ try {
1218
+ const r1 = await fetch("/pet/config.jsonc");
1219
+ if (!r1.ok) throw new Error("config.jsonc HTTP " + r1.status);
1220
+ config = assertClientConfig(JSON.parse(stripJsonc(await r1.text())));
1221
+ const defaults = config.pets;
1222
+ let user = {};
1026
1223
  try {
1027
- const r1 = await fetch("/pet/config.jsonc");
1028
- if (!r1.ok) return;
1029
- const obj = JSON.parse(stripJsonc(await r1.text()));
1030
- ANIM = buildAnim(obj);
1031
- const defaults = extractDefaultPets(obj);
1032
- let user = {};
1033
- try {
1034
- const r2 = await fetch("/pet/config");
1035
- if (r2.ok && r2.status !== 204) user = await r2.json().catch(() => ({}));
1036
- } catch {}
1037
- const merged = resolvePets(defaults, user);
1038
- if (!alive) return;
1039
- petBridge.current = merged;
1040
- petBridge.template = defaults.length ? defaults[0] : null;
1041
- petBridge.sync = (list) => {
1042
- setPets(list);
1043
- petBridge.current = list;
1044
- };
1045
- setPets(merged);
1046
- setReady(true);
1224
+ const r2 = await fetch("/pet/config");
1225
+ if (r2.ok && r2.status !== 204) user = await r2.json().catch(() => ({}));
1047
1226
  } catch {}
1048
- })();
1049
- return () => {
1050
- alive = false;
1051
- petBridge.sync = null;
1052
- };
1053
- }, []);
1054
- return ready ? pets.map((p) => h(PetCard, {
1055
- key: p.id,
1056
- cfg: p
1057
- })) : null;
1058
- }
1227
+ config = applyUserOverrides(config, user);
1228
+ const merged = config.pets;
1229
+ if (!alive) return;
1230
+ petBridge.current = merged;
1231
+ petBridge.template = defaults.length ? defaults[0] : void 0;
1232
+ petBridge.sync = (list) => {
1233
+ setPets(list);
1234
+ petBridge.current = list;
1235
+ };
1236
+ setPets(merged);
1237
+ setReady(true);
1238
+ } catch (e) {
1239
+ console.error("[dsh-pet] 配置加载失败", e);
1240
+ }
1241
+ })();
1242
+ return () => {
1243
+ alive = false;
1244
+ petBridge.sync = () => {};
1245
+ };
1246
+ }, []);
1247
+ return ready ? pets.map((p) => h(PetCard, {
1248
+ key: p.id,
1249
+ cfg: p
1250
+ })) : null;
1251
+ }
1252
+ return PetMulti;
1253
+ }
1254
+
1255
+ //#endregion
1256
+ //#region src/client/app.ts
1257
+ function makeFactory() {
1258
+ return (require) => {
1259
+ const module = { exports: {} };
1260
+ const react = require("react");
1261
+ const { useEffect, useRef, useState } = react;
1262
+ const { jsx: h } = require("react/jsx-runtime");
1263
+ const PetMulti = makePetUI({
1264
+ h,
1265
+ useState,
1266
+ useEffect,
1267
+ useRef
1268
+ });
1059
1269
  const name = "pet";
1060
1270
  const inject = ["slots", "locale"];
1061
- function apply(ctx, _config) {
1271
+ function apply(ctx) {
1062
1272
  ctx.effect(() => ctx.locale.register(NS, {
1063
1273
  zh,
1064
1274
  en
@@ -1074,6 +1284,7 @@ function makeFactory() {
1074
1284
  const PetConfigSection = makePetConfigSection({
1075
1285
  h,
1076
1286
  useState,
1287
+ useEffect,
1077
1288
  t
1078
1289
  });
1079
1290
  ctx.slots.inject("settings.section", function* () {