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/README.en.md +116 -100
- package/README.md +116 -101
- package/cordis.patch.yml +0 -9
- package/lib/client.js +836 -625
- package/lib/index.js +27 -17
- package/lib/types/index.d.ts +2 -12
- package/package.json +12 -3
- package/src/client/app.ts +13 -451
- package/src/client/config.ts +81 -105
- package/src/client/index.ts +1 -0
- package/src/client/motion.ts +36 -0
- package/src/client/pet.ts +482 -0
- package/src/client/pickers.ts +31 -4
- package/src/client/settings.ts +441 -133
- package/src/client/types.ts +30 -10
- package/src/host/index.ts +126 -99
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:
|
|
34
|
-
sync:
|
|
35
|
-
template:
|
|
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
|
|
132
|
-
const [pets, setPets] = useState(initPets.map((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
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
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
|
|
308
|
+
return false;
|
|
151
309
|
}
|
|
152
|
-
return
|
|
310
|
+
return true;
|
|
153
311
|
};
|
|
154
312
|
const save = async () => {
|
|
155
|
-
const
|
|
156
|
-
if (!
|
|
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
|
|
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 =
|
|
178
|
-
petBridge.sync
|
|
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 =
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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
|
-
|
|
223
|
-
const np = {
|
|
377
|
+
setPets((list) => [...list, {
|
|
224
378
|
id,
|
|
225
379
|
size: tpl.size,
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
|
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 ===
|
|
306
|
-
background: p.id ===
|
|
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
|
-
|
|
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",
|
|
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:
|
|
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",
|
|
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",
|
|
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/
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
866
|
-
return
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
914
|
-
|
|
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
|
-
|
|
919
|
-
|
|
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:
|
|
935
|
-
ry:
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
|
1028
|
-
if (
|
|
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
|
-
|
|
1050
|
-
alive
|
|
1051
|
-
petBridge.
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
|
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* () {
|