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/src/client/config.ts
CHANGED
|
@@ -1,122 +1,98 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
import type { AnimConfig, Corner } from './types';
|
|
1
|
+
// 配置层:剥注释、校验 config.jsonc。运行时(ANIM)直接使用与 jsonc 同构的 ClientConfig,
|
|
2
|
+
// 不做字段转换;缺失/非法一律视为配置错误(throw,由加载层显式报错)。
|
|
3
|
+
import type { Animations, ClientConfig, Corner, Pet, Weights } from './types';
|
|
5
4
|
|
|
6
|
-
/**
|
|
7
|
-
export const FALLBACK_IDLE = '待机呼吸休闲';
|
|
8
|
-
/** 移动默认参数(仅当 config.jsonc 未提供 moves.default 时兜底) */
|
|
9
|
-
export const DEFAULT_MOVE_PARAMS: Record<string, number> = {
|
|
10
|
-
minDist: 60,
|
|
11
|
-
maxDist: 240,
|
|
12
|
-
margin: 20,
|
|
13
|
-
leadSec: 2,
|
|
14
|
-
tailSec: 2,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/** 剥除 JSONC 注释(行注释 // 与块注释),得到纯 JSON 字符串。仅用于插件自带配置(无含 // 的 URL 值) */
|
|
5
|
+
/** 剥除 JSONC 注释(行注释 // 与块注释),得到纯 JSON 字符串 */
|
|
18
6
|
export const stripJsonc = (src: string): string =>
|
|
19
7
|
src
|
|
20
8
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
21
9
|
.replace(/(^|[^\\:])\/\/.*$/gm, '$1')
|
|
22
10
|
.trim();
|
|
23
11
|
|
|
24
|
-
/** 最小兜底配置:仅当 config.jsonc 缺失时使用,保证宠物不至于空白 */
|
|
25
|
-
export const buildMinimalAnim = (): AnimConfig => ({
|
|
26
|
-
idle: [FALLBACK_IDLE],
|
|
27
|
-
turn: [],
|
|
28
|
-
drag: [],
|
|
29
|
-
clicks: [],
|
|
30
|
-
moves: {
|
|
31
|
-
default: { ...DEFAULT_MOVE_PARAMS },
|
|
32
|
-
actions: [],
|
|
33
|
-
},
|
|
34
|
-
categories: [],
|
|
35
|
-
weights: { idle: 100, turn: 0, move: 0 },
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
/** 从 config.jsonc 解析出的对象构建运行时动作配置(缺字段/写错尽量安全兜底) */
|
|
39
|
-
export const buildAnim = (obj: unknown): AnimConfig => {
|
|
40
|
-
const a = (obj as any)?.animations;
|
|
41
|
-
const w = (obj as any)?.animationWeights ?? {};
|
|
42
|
-
const strArr = (x: unknown): string[] => (Array.isArray(x) ? x.map(String).filter(Boolean) : []);
|
|
43
|
-
const idle = strArr(a?.idle);
|
|
44
|
-
if (!idle.length) idle.push(FALLBACK_IDLE);
|
|
45
|
-
const turn = strArr(a?.turn);
|
|
46
|
-
const drag = strArr(a?.drag);
|
|
47
|
-
const clicks = strArr(a?.clicks);
|
|
48
|
-
let mdef: Record<string, number> = { ...DEFAULT_MOVE_PARAMS };
|
|
49
|
-
let mActs: { name: string; params?: Record<string, number> }[] = [];
|
|
50
|
-
if (a?.moves && typeof a.moves === 'object') {
|
|
51
|
-
if (a.moves.default && typeof a.moves.default === 'object') mdef = { ...mdef, ...a.moves.default };
|
|
52
|
-
if (Array.isArray(a.moves.actions)) {
|
|
53
|
-
const list = a.moves.actions
|
|
54
|
-
.filter((x: any) => x && x.name)
|
|
55
|
-
.map((x: any) => ({ name: String(x.name), params: x.params && typeof x.params === 'object' ? { ...x.params } : undefined }));
|
|
56
|
-
if (list.length) mActs = list;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
let cats: { id: string; weight: number; noMirror?: boolean; actions: string[] }[] = [];
|
|
60
|
-
if (Array.isArray(a?.categories)) {
|
|
61
|
-
const list = a.categories
|
|
62
|
-
.filter((c: any) => c && c.id && Array.isArray(c.actions) && c.actions.length)
|
|
63
|
-
.map((c: any) => ({ id: String(c.id), weight: Number(c.weight), noMirror: !!c.noMirror, actions: c.actions.map(String) }));
|
|
64
|
-
if (list.length) cats = list;
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
idle,
|
|
68
|
-
turn,
|
|
69
|
-
drag,
|
|
70
|
-
clicks,
|
|
71
|
-
moves: { default: mdef, actions: mActs },
|
|
72
|
-
categories: cats,
|
|
73
|
-
weights: {
|
|
74
|
-
idle: Number(w.idle) >= 0 ? Number(w.idle) : 10,
|
|
75
|
-
turn: Number(w.turn) >= 0 ? Number(w.turn) : 5,
|
|
76
|
-
move: Number(w.move) >= 0 ? Number(w.move) : 5,
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// ============================================================================
|
|
82
|
-
// 多开宠物配置解析(纯函数,不依赖 React)
|
|
83
|
-
// ============================================================================
|
|
84
12
|
/** 支持的角落白名单 */
|
|
85
13
|
export const CORNERS: Corner[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
|
14
|
+
/** corner 合法性检查用的 string 集合(Corner[] 的 includes 要求 Corner 参数,无法接收未知 string) */
|
|
15
|
+
const CORNER_SET: ReadonlySet<string> = new Set(CORNERS);
|
|
86
16
|
|
|
87
|
-
/**
|
|
88
|
-
export
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
corner: CORNERS.indexOf(pos.corner) !== -1 ? (pos.corner as Corner) : DEFAULT_PETS[0].corner,
|
|
94
|
-
marginX: Number.isFinite(Number(pos.marginX)) ? Number(pos.marginX) : DEFAULT_PETS[0].marginX,
|
|
95
|
-
marginY: Number.isFinite(Number(pos.marginY)) ? Number(pos.marginY) : DEFAULT_PETS[0].marginY,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
17
|
+
/** ClientConfig 类型占位(data-less;PetMulti 加载后由 assertClientConfig 赋真实值) */
|
|
18
|
+
export const EMPTY_CONF: ClientConfig = {
|
|
19
|
+
pets: [],
|
|
20
|
+
animations: { idle: [], turn: [], drag: [], clicks: [], moves: { default: {}, actions: [] }, categories: [] },
|
|
21
|
+
animationWeights: { idle: 0, turn: 0, move: 0 },
|
|
22
|
+
};
|
|
98
23
|
|
|
99
|
-
/**
|
|
100
|
-
export function
|
|
101
|
-
|
|
102
|
-
|
|
24
|
+
/** 校验 config.jsonc 解析结果并返回 ClientConfig;任一字段缺失/非法即视为配置错误抛出 */
|
|
25
|
+
export function assertClientConfig(raw: unknown): ClientConfig {
|
|
26
|
+
if (!raw || typeof raw !== 'object') throw new Error('dsh-pet: config 非对象');
|
|
27
|
+
// raw 是 unknown 输入(jsonc 解析产物),按 Record 读取后逐字段手工校验,字段读写无法静态定型
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
const cfg = raw as Record<string, any>;
|
|
30
|
+
|
|
31
|
+
// ---- pets ----
|
|
32
|
+
const petsArr = cfg.pets;
|
|
33
|
+
if (!Array.isArray(petsArr) || !petsArr.length) throw new Error('dsh-pet: 缺少 pets');
|
|
103
34
|
const seen = new Set<string>();
|
|
104
|
-
const
|
|
105
|
-
for (const p of
|
|
106
|
-
const id = String(p
|
|
107
|
-
if (!seen.has(id))
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
35
|
+
const pets: Pet[] = [];
|
|
36
|
+
for (const p of petsArr) {
|
|
37
|
+
const id = String(p?.id ?? '');
|
|
38
|
+
if (!id || seen.has(id)) throw new Error('dsh-pet: pet id 非法或重复「' + id + '」');
|
|
39
|
+
const size = Number(p?.size);
|
|
40
|
+
if (!Number.isFinite(size) || size <= 0) throw new Error('dsh-pet: pet「' + id + '」大小非法');
|
|
41
|
+
const corner = p?.position?.corner;
|
|
42
|
+
if (typeof corner !== 'string' || !CORNER_SET.has(corner)) throw new Error('dsh-pet: pet「' + id + '」corner 非法');
|
|
43
|
+
const marginX = Number(p?.position?.marginX);
|
|
44
|
+
const marginY = Number(p?.position?.marginY);
|
|
45
|
+
if (!Number.isFinite(marginX) || !Number.isFinite(marginY)) throw new Error('dsh-pet: pet「' + id + '」边距非法');
|
|
46
|
+
seen.add(id);
|
|
47
|
+
pets.push({ id, size, position: { corner: corner as Corner, marginX, marginY } });
|
|
111
48
|
}
|
|
112
|
-
return out.length ? out : DEFAULT_PETS.map((p) => ({ ...p }));
|
|
113
|
-
}
|
|
114
49
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
50
|
+
// ---- animations ----
|
|
51
|
+
const a = cfg.animations;
|
|
52
|
+
if (!a || typeof a !== 'object') throw new Error('dsh-pet: 缺少 animations');
|
|
53
|
+
for (const key of ['idle', 'turn', 'drag', 'clicks']) {
|
|
54
|
+
if (!Array.isArray(a[key])) throw new Error('dsh-pet: animations.' + key + ' 缺失');
|
|
120
55
|
}
|
|
56
|
+
if (
|
|
57
|
+
!a.moves ||
|
|
58
|
+
typeof a.moves !== 'object' ||
|
|
59
|
+
typeof a.moves.default !== 'object' ||
|
|
60
|
+
a.moves.default === null ||
|
|
61
|
+
!Array.isArray(a.moves.actions)
|
|
62
|
+
) {
|
|
63
|
+
throw new Error('dsh-pet: animations.moves 结构非法');
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(a.categories)) throw new Error('dsh-pet: animations.categories 缺失');
|
|
66
|
+
|
|
67
|
+
// ---- animationWeights ----
|
|
68
|
+
const w = cfg.animationWeights;
|
|
69
|
+
if (!w || typeof w !== 'object') throw new Error('dsh-pet: 缺少 animationWeights');
|
|
70
|
+
for (const key of ['idle', 'turn', 'move']) {
|
|
71
|
+
const v = Number(w[key]);
|
|
72
|
+
if (!Number.isFinite(v) || v < 0) throw new Error('dsh-pet: animationWeights.' + key + ' 非法');
|
|
73
|
+
w[key] = v;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { pets, animations: a, animationWeights: w };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 合并宠物:用户层({ pets },与 jsonc 同构)全量替换默认;无用户层回落默认 */
|
|
80
|
+
export function resolvePets(defaults: Pet[], user: { pets?: Pet[] }): Pet[] {
|
|
81
|
+
if (user && Array.isArray(user.pets)) return user.pets.length ? user.pets : defaults;
|
|
121
82
|
return defaults;
|
|
122
83
|
}
|
|
84
|
+
|
|
85
|
+
/** 用户覆盖片段(与 jsonc 同构;高级用户直接编辑 pet-config.json,缺省字段回落默认) */
|
|
86
|
+
export interface UserOverrides {
|
|
87
|
+
pets?: Pet[];
|
|
88
|
+
animations?: Animations;
|
|
89
|
+
animationWeights?: Weights;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 合并用户覆盖片段到完全体配置:pets / animations / animationWeights 有则整体替换,缺省回落默认 */
|
|
93
|
+
export function applyUserOverrides(base: ClientConfig, user: UserOverrides): ClientConfig {
|
|
94
|
+
const next: ClientConfig = { ...base, pets: resolvePets(base.pets, user) };
|
|
95
|
+
if (user.animations) next.animations = user.animations;
|
|
96
|
+
if (user.animationWeights) next.animationWeights = user.animationWeights;
|
|
97
|
+
return next;
|
|
98
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { makeFactory } from './app';
|
|
|
5
5
|
|
|
6
6
|
declare const window: {
|
|
7
7
|
__ModuleLoader__: {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- DSH 全局注入的模块系统契约(f(require) => module)
|
|
8
9
|
load(info: { id: string; factory: (require: (m: string) => any) => any }): void;
|
|
9
10
|
};
|
|
10
11
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// 移动几何:纯计算(无 DOM / ref),可独立单测。
|
|
2
|
+
// 所有坐标归一化为视口比例(ratio),px 换算由调用方(rAF 驱动 / customPos)完成。
|
|
3
|
+
import { randomBetween } from './pickers';
|
|
4
|
+
|
|
5
|
+
/** 一次移动的几何参数(比例坐标) */
|
|
6
|
+
export interface MovePlan {
|
|
7
|
+
startRatio: number;
|
|
8
|
+
startYRatio: number;
|
|
9
|
+
targetRatio: number;
|
|
10
|
+
totalRatio: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 计算一次移动的起点/终点比例坐标;目标越出视口边缘(含边距)时返回 null */
|
|
14
|
+
export const planMove = (o: {
|
|
15
|
+
cx: number;
|
|
16
|
+
cy: number;
|
|
17
|
+
W: number;
|
|
18
|
+
H: number;
|
|
19
|
+
dir: 1 | -1;
|
|
20
|
+
minDist: number;
|
|
21
|
+
maxDist: number;
|
|
22
|
+
margin: number;
|
|
23
|
+
halfW: number;
|
|
24
|
+
}): MovePlan | null => {
|
|
25
|
+
const distance = randomBetween(o.minDist, o.maxDist);
|
|
26
|
+
const target = o.cx + o.dir * distance;
|
|
27
|
+
const leftBound = o.margin + o.halfW;
|
|
28
|
+
const rightBound = o.W - o.margin - o.halfW;
|
|
29
|
+
if (target < leftBound || target > rightBound) return null;
|
|
30
|
+
return {
|
|
31
|
+
startRatio: o.cx / o.W,
|
|
32
|
+
startYRatio: o.cy / o.H,
|
|
33
|
+
targetRatio: target / o.W,
|
|
34
|
+
totalRatio: Math.abs(target - o.cx) / o.W,
|
|
35
|
+
};
|
|
36
|
+
};
|