dsh-pet 0.1.5 → 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/assets/config.jsonc +15 -10
- package/cordis.patch.yml +0 -9
- package/lib/client.js +1228 -480
- package/lib/index.js +115 -13
- package/lib/types/index.d.ts +2 -12
- package/package.json +12 -3
- package/src/client/app.ts +25 -427
- package/src/client/config.ts +88 -67
- 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 +34 -5
- package/src/client/settings.ts +633 -0
- package/src/client/types.ts +30 -10
- package/src/host/index.ts +168 -56
package/lib/client.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
//#region src/client/pickers.ts
|
|
3
3
|
const pick = (pool, exclude) => {
|
|
4
4
|
const entries = exclude ? pool.filter((n) => n !== exclude) : pool;
|
|
5
|
-
|
|
5
|
+
const src = entries.length ? entries : pool;
|
|
6
|
+
return src[Math.floor(Math.random() * src.length)];
|
|
6
7
|
};
|
|
7
8
|
const randomBetween = (min, max) => Math.floor(min + Math.random() * (max - min));
|
|
8
9
|
const pickWeightedCategory = (categories, facing) => {
|
|
@@ -18,85 +19,139 @@ const pickWeightedCategory = (categories, facing) => {
|
|
|
18
19
|
}
|
|
19
20
|
return eligible[eligible.length - 1];
|
|
20
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
|
+
};
|
|
21
40
|
|
|
22
41
|
//#endregion
|
|
23
|
-
//#region src/client/
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
margin
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
};
|
|
31
55
|
};
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/client/config.ts
|
|
32
59
|
const stripJsonc = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^\\:])\/\/.*$/gm, "$1").trim();
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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: []
|
|
41
80
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
idle: 100,
|
|
81
|
+
animationWeights: {
|
|
82
|
+
idle: 0,
|
|
45
83
|
turn: 0,
|
|
46
84
|
move: 0
|
|
47
85
|
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
});
|
|
72
114
|
}
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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;
|
|
82
135
|
}
|
|
83
136
|
return {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
clicks,
|
|
88
|
-
moves: {
|
|
89
|
-
default: mdef,
|
|
90
|
-
actions: mActs
|
|
91
|
-
},
|
|
92
|
-
categories: cats,
|
|
93
|
-
weights: {
|
|
94
|
-
idle: Number(w.idle) >= 0 ? Number(w.idle) : 10,
|
|
95
|
-
turn: Number(w.turn) >= 0 ? Number(w.turn) : 5,
|
|
96
|
-
move: Number(w.move) >= 0 ? Number(w.move) : 5
|
|
97
|
-
}
|
|
137
|
+
pets,
|
|
138
|
+
animations: a,
|
|
139
|
+
animationWeights: w
|
|
98
140
|
};
|
|
99
|
-
}
|
|
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
|
+
}
|
|
100
155
|
|
|
101
156
|
//#endregion
|
|
102
157
|
//#region src/client/constants.ts
|
|
@@ -111,442 +166,1135 @@ const HIT_BOX = {
|
|
|
111
166
|
const DRAG_THRESHOLD = 5;
|
|
112
167
|
|
|
113
168
|
//#endregion
|
|
114
|
-
//#region src/client/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
169
|
+
//#region src/client/settings.ts
|
|
170
|
+
const petBridge = {
|
|
171
|
+
current: [],
|
|
172
|
+
sync: () => {},
|
|
173
|
+
template: void 0
|
|
174
|
+
};
|
|
175
|
+
const NS = "pet.config";
|
|
176
|
+
const zh = {
|
|
177
|
+
nav: "桌宠配置",
|
|
178
|
+
intro: "管理多个桌宠:每个宠物可独立设置大小与位置(保存后即时生效)。",
|
|
179
|
+
petsLabel: "宠物列表",
|
|
180
|
+
add: "添加宠物",
|
|
181
|
+
remove: "删除",
|
|
182
|
+
confirmRemove: "确定删除宠物「{id}」吗?",
|
|
183
|
+
confirmTitle: "确认操作",
|
|
184
|
+
cancel: "取消",
|
|
185
|
+
atLeastOne: "至少保留一个宠物。",
|
|
186
|
+
emptyPets: "暂无宠物,点击「添加宠物」创建。",
|
|
187
|
+
sizeLabel: "大小(宽度 px)",
|
|
188
|
+
sizeHint: "高度自动 = 宽度 × 9/16。",
|
|
189
|
+
cornerLabel: "位置",
|
|
190
|
+
"corner.top-left": "左上角",
|
|
191
|
+
"corner.top-right": "右上角",
|
|
192
|
+
"corner.bottom-left": "左下角",
|
|
193
|
+
"corner.bottom-right": "右下角",
|
|
194
|
+
marginX: "水平偏移",
|
|
195
|
+
marginY: "垂直偏移",
|
|
196
|
+
save: "保存",
|
|
197
|
+
reset: "恢复默认",
|
|
198
|
+
confirmReset: "确定恢复默认吗?将删除整个用户配置(含自定义的动画池与播放权重)。",
|
|
199
|
+
resetHint: "「重置」会删除整个用户配置(含自定义的动画池与播放权重),不只是宠物列表。",
|
|
200
|
+
configMeta: "高级配置(文件)",
|
|
201
|
+
configMetaHint: "用户配置可覆盖宠物列表 / 动画池 / 播放权重,修改后刷新或重启生效;默认配置为完整参考。",
|
|
202
|
+
defaultConfig: "默认配置(只读,完整参考)",
|
|
203
|
+
userConfig: "用户配置(自定义覆盖)",
|
|
204
|
+
animationDir: "动画素材目录(可自定义/扩充动画)",
|
|
205
|
+
saved: "已保存,桌宠即时生效。",
|
|
206
|
+
loadError: "加载配置失败",
|
|
207
|
+
invalid: "请检查输入:大小需为正数,边距可为任意数字。",
|
|
208
|
+
busy: "保存中…"
|
|
209
|
+
};
|
|
210
|
+
const en = {
|
|
211
|
+
nav: "Pet Config",
|
|
212
|
+
intro: "Manage multiple pets: each pet has its own size and position (applies instantly after saving).",
|
|
213
|
+
petsLabel: "Pets",
|
|
214
|
+
add: "Add pet",
|
|
215
|
+
remove: "Remove",
|
|
216
|
+
confirmRemove: "Delete pet \"{id}\"?",
|
|
217
|
+
confirmTitle: "Confirm action",
|
|
218
|
+
cancel: "Cancel",
|
|
219
|
+
atLeastOne: "Keep at least one pet.",
|
|
220
|
+
emptyPets: "No pets yet — click \"Add pet\" to create one.",
|
|
221
|
+
sizeLabel: "Size (width px)",
|
|
222
|
+
sizeHint: "Height is automatic = width × 9/16.",
|
|
223
|
+
cornerLabel: "Position",
|
|
224
|
+
"corner.top-left": "Top-left",
|
|
225
|
+
"corner.top-right": "Top-right",
|
|
226
|
+
"corner.bottom-left": "Bottom-left",
|
|
227
|
+
"corner.bottom-right": "Bottom-right",
|
|
228
|
+
marginX: "Horizontal offset",
|
|
229
|
+
marginY: "Vertical offset",
|
|
230
|
+
save: "Save",
|
|
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)",
|
|
239
|
+
saved: "Saved — the pets updated instantly.",
|
|
240
|
+
loadError: "Failed to load config",
|
|
241
|
+
invalid: "Check your input: size must be positive; margins can be any number.",
|
|
242
|
+
busy: "Saving…"
|
|
243
|
+
};
|
|
244
|
+
function makePetConfigSection(rt) {
|
|
245
|
+
const { h, useState, useEffect, t } = rt;
|
|
246
|
+
const CORNERS$1 = [
|
|
247
|
+
"top-left",
|
|
248
|
+
"top-right",
|
|
249
|
+
"bottom-left",
|
|
250
|
+
"bottom-right"
|
|
251
|
+
];
|
|
252
|
+
const cornerLabel = (c) => t("corner." + c);
|
|
253
|
+
const inputStyle = {
|
|
254
|
+
boxSizing: "border-box",
|
|
255
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
256
|
+
borderRadius: "8px",
|
|
257
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
258
|
+
color: "var(--dsw-alias-label-primary)",
|
|
259
|
+
padding: "5px 10px",
|
|
260
|
+
fontSize: "13px",
|
|
261
|
+
minHeight: "28px",
|
|
262
|
+
outline: "none"
|
|
263
|
+
};
|
|
264
|
+
/** 生成一个未占用的宠物 id(pet-2、pet-3…) */
|
|
265
|
+
const nextId = (list) => {
|
|
266
|
+
let n = 2;
|
|
267
|
+
for (;; n++) {
|
|
268
|
+
const id = "pet-" + n;
|
|
269
|
+
if (!list.some((p) => p.id === id)) return id;
|
|
148
270
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
271
|
+
};
|
|
272
|
+
return function PetConfigSection() {
|
|
273
|
+
const initPets = petBridge.current;
|
|
274
|
+
const [pets, setPets] = useState(initPets.map((p) => ({
|
|
275
|
+
...p,
|
|
276
|
+
position: { ...p.position }
|
|
277
|
+
})));
|
|
278
|
+
const [selId, setSelId] = useState(initPets[0]?.id ?? "");
|
|
279
|
+
const [busy, setBusy] = useState(false);
|
|
280
|
+
const [msg, setMsg] = useState({
|
|
281
|
+
kind: "",
|
|
282
|
+
text: ""
|
|
283
|
+
});
|
|
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
|
+
}));
|
|
302
|
+
const validated = () => {
|
|
303
|
+
for (const p of pets) if (!Number.isFinite(p.size) || p.size <= 0 || !Number.isFinite(p.position.marginX) || !Number.isFinite(p.position.marginY)) {
|
|
304
|
+
setMsg({
|
|
305
|
+
kind: "err",
|
|
306
|
+
text: t("invalid")
|
|
307
|
+
});
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
return true;
|
|
311
|
+
};
|
|
312
|
+
const save = async () => {
|
|
313
|
+
const isOk = validated();
|
|
314
|
+
if (!isOk) return;
|
|
315
|
+
setBusy(true);
|
|
316
|
+
setMsg({
|
|
317
|
+
kind: "",
|
|
318
|
+
text: ""
|
|
163
319
|
});
|
|
164
|
-
|
|
165
|
-
fetch("/pet/config
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}, []);
|
|
193
|
-
const [seq, setSeq] = useState(0);
|
|
194
|
-
const rootRef = useRef(null);
|
|
195
|
-
const stageRef = useRef(null);
|
|
196
|
-
const videoARef = useRef(null);
|
|
197
|
-
const videoBRef = useRef(null);
|
|
198
|
-
const frontRef = useRef(0);
|
|
199
|
-
const pendingRef = useRef(null);
|
|
200
|
-
const genRef = useRef(0);
|
|
201
|
-
const dragRef = useRef({
|
|
202
|
-
active: false,
|
|
203
|
-
dragging: false,
|
|
204
|
-
sx: 0,
|
|
205
|
-
sy: 0,
|
|
206
|
-
offX: 0,
|
|
207
|
-
offY: 0
|
|
320
|
+
try {
|
|
321
|
+
const res = await fetch("/pet/config", {
|
|
322
|
+
method: "PUT",
|
|
323
|
+
headers: { "content-type": "application/json" },
|
|
324
|
+
body: JSON.stringify({ pets })
|
|
325
|
+
});
|
|
326
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
327
|
+
petBridge.current = pets;
|
|
328
|
+
petBridge.sync(pets);
|
|
329
|
+
setMsg({
|
|
330
|
+
kind: "ok",
|
|
331
|
+
text: t("saved")
|
|
332
|
+
});
|
|
333
|
+
} catch {
|
|
334
|
+
setMsg({
|
|
335
|
+
kind: "err",
|
|
336
|
+
text: t("loadError")
|
|
337
|
+
});
|
|
338
|
+
} finally {
|
|
339
|
+
setBusy(false);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const reset = () => setConfirm("reset");
|
|
343
|
+
const doReset = async () => {
|
|
344
|
+
setBusy(true);
|
|
345
|
+
setMsg({
|
|
346
|
+
kind: "",
|
|
347
|
+
text: ""
|
|
208
348
|
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
349
|
+
try {
|
|
350
|
+
await fetch("/pet/config", { method: "DELETE" });
|
|
351
|
+
const defRes = await fetch("/pet/config.jsonc");
|
|
352
|
+
const defs = assertClientConfig(JSON.parse(stripJsonc(await defRes.text()))).pets;
|
|
353
|
+
setPets(defs.map((p) => ({
|
|
354
|
+
...p,
|
|
355
|
+
position: { ...p.position }
|
|
356
|
+
})));
|
|
357
|
+
setSelId(defs[0]?.id ?? "");
|
|
358
|
+
petBridge.current = defs;
|
|
359
|
+
petBridge.sync(defs);
|
|
360
|
+
setMsg({
|
|
361
|
+
kind: "ok",
|
|
362
|
+
text: t("saved")
|
|
363
|
+
});
|
|
364
|
+
} catch {
|
|
365
|
+
setMsg({
|
|
366
|
+
kind: "err",
|
|
367
|
+
text: t("loadError")
|
|
368
|
+
});
|
|
369
|
+
} finally {
|
|
370
|
+
setBusy(false);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const addPet = () => {
|
|
374
|
+
const tpl = petBridge.template;
|
|
375
|
+
if (!tpl) return;
|
|
376
|
+
const id = nextId(pets);
|
|
377
|
+
setPets((list) => [...list, {
|
|
378
|
+
id,
|
|
379
|
+
size: tpl.size,
|
|
380
|
+
position: { ...tpl.position }
|
|
381
|
+
}]);
|
|
382
|
+
setSelId(id);
|
|
383
|
+
};
|
|
384
|
+
const removeSel = () => {
|
|
385
|
+
if (pets.length <= 1) {
|
|
386
|
+
setMsg({
|
|
387
|
+
kind: "err",
|
|
388
|
+
text: t("atLeastOne")
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
setConfirm("remove");
|
|
393
|
+
};
|
|
394
|
+
const doRemove = () => {
|
|
395
|
+
const list = pets.filter((p) => p.id !== selId);
|
|
396
|
+
setPets(list);
|
|
397
|
+
setSelId(list[0].id);
|
|
398
|
+
};
|
|
399
|
+
const field = (key, value, setter, width) => h("input", {
|
|
400
|
+
type: "number",
|
|
401
|
+
step: key === "size" ? "10" : "1",
|
|
402
|
+
min: key === "size" ? "120" : "",
|
|
403
|
+
value: String(value),
|
|
404
|
+
disabled: busy,
|
|
405
|
+
onChange: (e) => setter(Number(e.target.value)),
|
|
406
|
+
style: {
|
|
407
|
+
width,
|
|
408
|
+
...inputStyle
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
return h("section", {
|
|
412
|
+
style: {
|
|
413
|
+
maxWidth: "720px",
|
|
414
|
+
color: "var(--dsw-alias-label-primary)",
|
|
415
|
+
display: "flex",
|
|
416
|
+
flexDirection: "column",
|
|
417
|
+
gap: "6px"
|
|
418
|
+
},
|
|
419
|
+
children: [
|
|
420
|
+
h("h2", {
|
|
421
|
+
style: {
|
|
422
|
+
margin: 0,
|
|
423
|
+
fontSize: "16px",
|
|
424
|
+
fontWeight: 500,
|
|
425
|
+
lineHeight: "24px"
|
|
426
|
+
},
|
|
427
|
+
children: t("nav")
|
|
428
|
+
}),
|
|
429
|
+
h("p", {
|
|
430
|
+
style: {
|
|
431
|
+
margin: 0,
|
|
432
|
+
fontSize: "14px",
|
|
433
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
434
|
+
lineHeight: "22px"
|
|
435
|
+
},
|
|
436
|
+
children: t("intro")
|
|
437
|
+
}),
|
|
438
|
+
h("div", {
|
|
439
|
+
style: {
|
|
440
|
+
display: "flex",
|
|
441
|
+
gap: "8px",
|
|
442
|
+
flexWrap: "wrap",
|
|
443
|
+
alignItems: "center",
|
|
444
|
+
marginTop: "4px"
|
|
445
|
+
},
|
|
446
|
+
children: [
|
|
447
|
+
h("span", {
|
|
448
|
+
style: {
|
|
449
|
+
fontSize: "12px",
|
|
450
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
451
|
+
},
|
|
452
|
+
children: t("petsLabel")
|
|
453
|
+
}),
|
|
454
|
+
...pets.map((p) => h("button", {
|
|
455
|
+
key: p.id,
|
|
456
|
+
type: "button",
|
|
457
|
+
onClick: () => setSelId(p.id),
|
|
458
|
+
style: {
|
|
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",
|
|
461
|
+
color: "var(--dsw-alias-label-primary)",
|
|
462
|
+
borderRadius: "8px",
|
|
463
|
+
padding: "4px 12px",
|
|
464
|
+
fontSize: "13px",
|
|
465
|
+
cursor: "pointer"
|
|
466
|
+
},
|
|
467
|
+
children: p.id + " (" + p.size + "px)"
|
|
468
|
+
})),
|
|
469
|
+
h("button", {
|
|
470
|
+
type: "button",
|
|
471
|
+
onClick: addPet,
|
|
472
|
+
disabled: busy,
|
|
473
|
+
style: {
|
|
474
|
+
border: "1px dashed var(--dsw-alias-border-l2)",
|
|
475
|
+
background: "transparent",
|
|
476
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
477
|
+
borderRadius: "8px",
|
|
478
|
+
padding: "4px 12px",
|
|
479
|
+
fontSize: "13px",
|
|
480
|
+
cursor: "pointer"
|
|
481
|
+
},
|
|
482
|
+
children: "+ " + t("add")
|
|
483
|
+
})
|
|
484
|
+
]
|
|
485
|
+
}),
|
|
486
|
+
cur ? h("div", {
|
|
487
|
+
style: {
|
|
488
|
+
display: "flex",
|
|
489
|
+
gap: "16px",
|
|
490
|
+
flexWrap: "wrap",
|
|
491
|
+
marginTop: "8px",
|
|
492
|
+
padding: "12px 14px",
|
|
493
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
494
|
+
borderRadius: "12px"
|
|
495
|
+
},
|
|
496
|
+
children: [
|
|
497
|
+
h("label", {
|
|
498
|
+
style: {
|
|
499
|
+
display: "flex",
|
|
500
|
+
flexDirection: "column",
|
|
501
|
+
gap: "4px",
|
|
502
|
+
fontSize: "12px",
|
|
503
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
504
|
+
},
|
|
505
|
+
children: [
|
|
506
|
+
t("sizeLabel"),
|
|
507
|
+
field("size", cur.size, (v) => updateSel({ size: v }), "150px"),
|
|
508
|
+
h("span", {
|
|
509
|
+
style: {
|
|
510
|
+
fontSize: "11px",
|
|
511
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
512
|
+
},
|
|
513
|
+
children: t("sizeHint")
|
|
514
|
+
})
|
|
515
|
+
]
|
|
516
|
+
}),
|
|
517
|
+
h("label", {
|
|
518
|
+
style: {
|
|
519
|
+
display: "flex",
|
|
520
|
+
flexDirection: "column",
|
|
521
|
+
gap: "4px",
|
|
522
|
+
fontSize: "12px",
|
|
523
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
524
|
+
},
|
|
525
|
+
children: [t("cornerLabel"), h("select", {
|
|
526
|
+
value: cur.position.corner,
|
|
527
|
+
disabled: busy,
|
|
528
|
+
onChange: (e) => updateSel({ position: { corner: e.target.value } }),
|
|
529
|
+
style: {
|
|
530
|
+
width: "160px",
|
|
531
|
+
...inputStyle
|
|
532
|
+
},
|
|
533
|
+
children: CORNERS$1.map((c) => h("option", {
|
|
534
|
+
key: c,
|
|
535
|
+
value: c,
|
|
536
|
+
children: cornerLabel(c)
|
|
537
|
+
}))
|
|
538
|
+
})]
|
|
539
|
+
}),
|
|
540
|
+
h("label", {
|
|
541
|
+
style: {
|
|
542
|
+
display: "flex",
|
|
543
|
+
flexDirection: "column",
|
|
544
|
+
gap: "4px",
|
|
545
|
+
fontSize: "12px",
|
|
546
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
547
|
+
},
|
|
548
|
+
children: [t("marginX"), field("marginX", cur.position.marginX, (v) => updateSel({ position: { marginX: v } }), "120px")]
|
|
549
|
+
}),
|
|
550
|
+
h("label", {
|
|
551
|
+
style: {
|
|
552
|
+
display: "flex",
|
|
553
|
+
flexDirection: "column",
|
|
554
|
+
gap: "4px",
|
|
555
|
+
fontSize: "12px",
|
|
556
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
557
|
+
},
|
|
558
|
+
children: [t("marginY"), field("marginY", cur.position.marginY, (v) => updateSel({ position: { marginY: v } }), "120px")]
|
|
559
|
+
}),
|
|
560
|
+
h("button", {
|
|
561
|
+
type: "button",
|
|
562
|
+
onClick: removeSel,
|
|
563
|
+
disabled: busy,
|
|
564
|
+
title: t("remove"),
|
|
565
|
+
style: {
|
|
566
|
+
alignSelf: "flex-end",
|
|
567
|
+
border: "1px solid var(--dsw-alias-state-error-secondary)",
|
|
568
|
+
background: "transparent",
|
|
569
|
+
color: "var(--dsw-alias-state-error-primary)",
|
|
570
|
+
borderRadius: "8px",
|
|
571
|
+
padding: "4px 12px",
|
|
572
|
+
fontSize: "12px",
|
|
573
|
+
cursor: "pointer"
|
|
574
|
+
},
|
|
575
|
+
children: t("remove")
|
|
576
|
+
})
|
|
577
|
+
]
|
|
578
|
+
}) : h("p", {
|
|
579
|
+
style: {
|
|
580
|
+
margin: 0,
|
|
581
|
+
fontSize: "13px",
|
|
582
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
583
|
+
},
|
|
584
|
+
children: t("emptyPets")
|
|
585
|
+
}),
|
|
586
|
+
h("div", {
|
|
587
|
+
style: {
|
|
588
|
+
display: "flex",
|
|
589
|
+
gap: "8px",
|
|
590
|
+
alignItems: "center",
|
|
591
|
+
marginTop: "4px"
|
|
592
|
+
},
|
|
593
|
+
children: [
|
|
594
|
+
h("button", {
|
|
595
|
+
type: "button",
|
|
596
|
+
disabled: busy,
|
|
597
|
+
onClick: save,
|
|
598
|
+
style: {
|
|
599
|
+
border: "1px solid var(--dsw-alias-button-info-fill)",
|
|
600
|
+
background: "var(--dsw-alias-button-info-fill)",
|
|
601
|
+
color: "#fff",
|
|
602
|
+
borderRadius: "8px",
|
|
603
|
+
padding: "4px 14px",
|
|
604
|
+
fontSize: "12px",
|
|
605
|
+
cursor: "pointer",
|
|
606
|
+
opacity: busy ? .5 : 1
|
|
607
|
+
},
|
|
608
|
+
children: t("save")
|
|
609
|
+
}),
|
|
610
|
+
h("button", {
|
|
611
|
+
type: "button",
|
|
612
|
+
disabled: busy,
|
|
613
|
+
onClick: reset,
|
|
614
|
+
style: {
|
|
615
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
616
|
+
background: "transparent",
|
|
617
|
+
color: "var(--dsw-alias-label-primary)",
|
|
618
|
+
borderRadius: "8px",
|
|
619
|
+
padding: "4px 14px",
|
|
620
|
+
fontSize: "12px",
|
|
621
|
+
cursor: "pointer",
|
|
622
|
+
opacity: busy ? .5 : 1
|
|
623
|
+
},
|
|
624
|
+
children: t("reset")
|
|
625
|
+
}),
|
|
626
|
+
msg.text ? h("span", {
|
|
627
|
+
style: {
|
|
628
|
+
fontSize: "12px",
|
|
629
|
+
color: msg.kind === "err" ? "var(--dsw-alias-state-error-primary)" : "var(--dsw-alias-state-ok-primary)",
|
|
630
|
+
marginLeft: "4px"
|
|
631
|
+
},
|
|
632
|
+
children: msg.text
|
|
633
|
+
}) : null
|
|
634
|
+
]
|
|
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
|
|
791
|
+
]
|
|
792
|
+
});
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
//#endregion
|
|
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);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
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
|
|
848
|
+
});
|
|
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
|
|
324
883
|
};
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
rootEl.style.left = px - halfW + "px";
|
|
347
|
-
rootEl.style.top = py - halfH + "px";
|
|
348
|
-
rootEl.style.right = "auto";
|
|
349
|
-
rootEl.style.bottom = "auto";
|
|
350
|
-
}
|
|
351
|
-
if (t < duration - tailSec) moveRef.current = requestAnimationFrame(step);
|
|
352
|
-
else {
|
|
353
|
-
moveRef.current = null;
|
|
354
|
-
setCustomPos({
|
|
355
|
-
rx: targetRatio,
|
|
356
|
-
ry: startYRatio
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
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);
|
|
361
905
|
};
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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));
|
|
385
965
|
setOnce(true);
|
|
386
|
-
|
|
387
|
-
return
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const d = dragRef.current;
|
|
422
|
-
if (!d.active) return;
|
|
423
|
-
const dx = e.clientX - d.sx;
|
|
424
|
-
const dy = e.clientY - d.sy;
|
|
425
|
-
if (!d.dragging) {
|
|
426
|
-
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
427
|
-
d.dragging = true;
|
|
428
|
-
setDragging(true);
|
|
429
|
-
setOnce(true);
|
|
430
|
-
if (ANIM.drag.length) setAnim(pick(ANIM.drag));
|
|
431
|
-
}
|
|
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;
|
|
432
1001
|
const rootEl = rootRef.current;
|
|
433
1002
|
if (rootEl) {
|
|
434
|
-
|
|
435
|
-
|
|
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";
|
|
436
1013
|
rootEl.style.right = "auto";
|
|
437
1014
|
rootEl.style.bottom = "auto";
|
|
438
1015
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const handlePointerUp = (e) => {
|
|
443
|
-
const d = dragRef.current;
|
|
444
|
-
const wasDragging = d.dragging;
|
|
445
|
-
d.active = false;
|
|
446
|
-
d.dragging = false;
|
|
447
|
-
e.currentTarget.classList.remove("dragging");
|
|
448
|
-
if (wasDragging) {
|
|
449
|
-
justDraggedRef.current = true;
|
|
450
|
-
setTimeout(() => {
|
|
451
|
-
justDraggedRef.current = false;
|
|
452
|
-
}, 100);
|
|
453
|
-
setDragging(false);
|
|
1016
|
+
if (t < duration - tailSec) moveRef.current = requestAnimationFrame(step);
|
|
1017
|
+
else {
|
|
1018
|
+
moveRef.current = null;
|
|
454
1019
|
setCustomPos({
|
|
455
|
-
rx:
|
|
456
|
-
ry:
|
|
1020
|
+
rx: targetRatio,
|
|
1021
|
+
ry: startYRatio
|
|
457
1022
|
});
|
|
458
|
-
const stageEl = stageRef.current;
|
|
459
|
-
if (stageEl) stageEl.style.transform = "translateY(" + bottomPad + "px)";
|
|
460
|
-
if (ANIM.idle.length) setAnim(pick(ANIM.idle, animRef.current));
|
|
461
|
-
setOnce(false);
|
|
462
1023
|
}
|
|
463
1024
|
};
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
471
1053
|
};
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
|
491
1087
|
};
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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"
|
|
512
1153
|
};
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
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 = {};
|
|
1223
|
+
try {
|
|
1224
|
+
const r2 = await fetch("/pet/config");
|
|
1225
|
+
if (r2.ok && r2.status !== 204) user = await r2.json().catch(() => ({}));
|
|
1226
|
+
} catch {}
|
|
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
|
+
});
|
|
541
1269
|
const name = "pet";
|
|
542
|
-
const inject = ["slots"];
|
|
543
|
-
function apply(ctx
|
|
1270
|
+
const inject = ["slots", "locale"];
|
|
1271
|
+
function apply(ctx) {
|
|
1272
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1273
|
+
zh,
|
|
1274
|
+
en
|
|
1275
|
+
}), "dsh-pet: dictionaries");
|
|
1276
|
+
const t = ctx.locale.bind(NS);
|
|
544
1277
|
ctx.slots.inject("shell.overlay", function* () {
|
|
545
1278
|
yield ctx.slots.register({
|
|
546
1279
|
name: "shell.overlay",
|
|
547
1280
|
id: "pet",
|
|
548
1281
|
order: 1e3
|
|
549
|
-
}, () => h(
|
|
1282
|
+
}, () => h(PetMulti, {}));
|
|
1283
|
+
});
|
|
1284
|
+
const PetConfigSection = makePetConfigSection({
|
|
1285
|
+
h,
|
|
1286
|
+
useState,
|
|
1287
|
+
useEffect,
|
|
1288
|
+
t
|
|
1289
|
+
});
|
|
1290
|
+
ctx.slots.inject("settings.section", function* () {
|
|
1291
|
+
yield ctx.slots.register({
|
|
1292
|
+
name: "settings.section",
|
|
1293
|
+
id: "pet-config",
|
|
1294
|
+
order: 30,
|
|
1295
|
+
label: () => t("nav"),
|
|
1296
|
+
inject: () => ({ t })
|
|
1297
|
+
}, PetConfigSection);
|
|
550
1298
|
});
|
|
551
1299
|
}
|
|
552
1300
|
module.exports = {
|