arona-agent 1.0.0

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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/assets/blue-archive/arona/spine/arona_spr.atlas.txt +1168 -0
  4. package/assets/blue-archive/arona/spine/arona_spr.png +0 -0
  5. package/assets/blue-archive/arona/spine/arona_spr.skel +0 -0
  6. package/assets/blue-archive/arona/voice.mp3 +0 -0
  7. package/assets/blue-archive/hoshino/hoshino_spr.atlas.txt +174 -0
  8. package/assets/blue-archive/hoshino/hoshino_spr.json +15385 -0
  9. package/assets/blue-archive/hoshino/hoshino_spr.png +0 -0
  10. package/assets/blue-archive/hoshino/hoshino_spr.skel +0 -0
  11. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.atlas.txt +111 -0
  12. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.png +0 -0
  13. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.skel +0 -0
  14. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.atlas.txt +251 -0
  15. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.png +0 -0
  16. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.skel +0 -0
  17. package/assets/blue-archive/hoshino/voice.mp3 +0 -0
  18. package/assets/blue-archive/plana/spine/plana_spr.atlas.txt +1266 -0
  19. package/assets/blue-archive/plana/spine/plana_spr.png +0 -0
  20. package/assets/blue-archive/plana/spine/plana_spr.skel +0 -0
  21. package/assets/blue-archive/plana/voice.mp3 +0 -0
  22. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.atlas.txt +97 -0
  23. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.png +0 -0
  24. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.skel +0 -0
  25. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.atlas.txt +174 -0
  26. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.png +0 -0
  27. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.skel +0 -0
  28. package/assets/blue-archive/shiroko/shiroko_spr.atlas.txt +167 -0
  29. package/assets/blue-archive/shiroko/shiroko_spr.json +10702 -0
  30. package/assets/blue-archive/shiroko/shiroko_spr.png +0 -0
  31. package/assets/blue-archive/shiroko/shiroko_spr.skel +0 -0
  32. package/assets/blue-archive/shiroko/voice.mp3 +0 -0
  33. package/bin/arona.mjs +43 -0
  34. package/package.json +37 -0
  35. package/pet/agents.cjs +197 -0
  36. package/pet/main.cjs +462 -0
  37. package/pet/preload.cjs +21 -0
  38. package/pet/renderer/fx.html +30 -0
  39. package/pet/renderer/fx.js +41 -0
  40. package/pet/renderer/gallery.html +18 -0
  41. package/pet/renderer/gallery.js +31 -0
  42. package/pet/renderer/index.html +18 -0
  43. package/pet/renderer/renderer.js +382 -0
  44. package/pet/renderer/spine_layer.js +609 -0
  45. package/pet/renderer/spinetest.html +24 -0
  46. package/pet/renderer/spinetest.js +295 -0
  47. package/pet/renderer/style.css +63 -0
  48. package/pet/tools/gallery_capture.cjs +267 -0
  49. package/pet/tools/gen_sway.cjs +170 -0
  50. package/pet/tools/meshify_region.cjs +105 -0
  51. package/pet/tools/skel_inspect.cjs +112 -0
  52. package/pet/tools/skel_to_json.cjs +410 -0
  53. package/pet/tools/spine_node.cjs +69 -0
  54. package/pet/tools/visual_test.cjs +262 -0
  55. package/pet/vendor/spine/spine-canvas.js +8472 -0
  56. package/pet/vendor/spine/spine-webgl.js +10839 -0
  57. package/python/__pycache__/_i18n.cpython-313.pyc +0 -0
  58. package/python/__pycache__/_i18n.cpython-314.pyc +0 -0
  59. package/python/__pycache__/computer_use.cpython-314.pyc +0 -0
  60. package/python/__pycache__/hotkey.cpython-313.pyc +0 -0
  61. package/python/__pycache__/hotkey.cpython-314.pyc +0 -0
  62. package/python/__pycache__/stt.cpython-313.pyc +0 -0
  63. package/python/__pycache__/stt.cpython-314.pyc +0 -0
  64. package/python/__pycache__/tts.cpython-313.pyc +0 -0
  65. package/python/__pycache__/tts.cpython-314.pyc +0 -0
  66. package/python/__pycache__/tts_stream.cpython-313.pyc +0 -0
  67. package/python/__pycache__/tts_stream.cpython-314.pyc +0 -0
  68. package/python/__pycache__/voice_clone.cpython-314.pyc +0 -0
  69. package/python/_i18n.py +18 -0
  70. package/python/computer_use.py +190 -0
  71. package/python/hotkey.py +196 -0
  72. package/python/stt.py +240 -0
  73. package/python/tts_say.py +177 -0
  74. package/python/voice_clone.py +98 -0
  75. package/requirements.txt +6 -0
  76. package/src/agent.ts +791 -0
  77. package/src/agent_registry.ts +109 -0
  78. package/src/commands.ts +641 -0
  79. package/src/config.ts +245 -0
  80. package/src/in_memory_credentials.ts +61 -0
  81. package/src/index.ts +78 -0
  82. package/src/locale.ts +131 -0
  83. package/src/logo.ts +120 -0
  84. package/src/mcp.ts +192 -0
  85. package/src/memory.ts +303 -0
  86. package/src/pet.ts +346 -0
  87. package/src/renderer.ts +350 -0
  88. package/src/repl.ts +902 -0
  89. package/src/setup.ts +371 -0
  90. package/src/skills.ts +96 -0
  91. package/src/slash_menu.ts +371 -0
  92. package/src/slash_registry.ts +53 -0
  93. package/src/speaker_context.ts +41 -0
  94. package/src/text_split.ts +79 -0
  95. package/src/tools/computer_use.ts +142 -0
  96. package/src/tools/emotion_tool.ts +55 -0
  97. package/src/tools/keep_silent_tool.ts +26 -0
  98. package/src/tools/memory_tool.ts +24 -0
  99. package/src/tools/skill_tools.ts +100 -0
  100. package/src/tools/tavily_tools.ts +431 -0
  101. package/src/tools/voice_tools.ts +28 -0
  102. package/src/tts_stream.ts +260 -0
  103. package/src/tui_select.ts +146 -0
  104. package/src/undo.ts +451 -0
  105. package/src/utils/python.ts +211 -0
  106. package/src/utils/spawn.ts +56 -0
  107. package/src/voice.ts +83 -0
  108. package/src/voice_cli.ts +198 -0
  109. package/src/voices.ts +135 -0
@@ -0,0 +1,410 @@
1
+ // skel(二进制) → Spine 3.8 JSON 导出器 + round-trip 深比对验证。
2
+ // 用法:node pet/tools/skel_to_json.cjs <agentId> [--verify] [--out <path>]
3
+ // 默认输出到 assets/blue-archive/<id>/<id>_spr.json(arona/plana 在 spine/ 子目录)
4
+ // --verify:导出后用 SkeletonJson 读回,与 SkeletonBinary 的 SkeletonData 深比对。
5
+ //
6
+ // 关键格式事实(源码核对 vendored spine-webgl.js):
7
+ // - binary 的 defaultSkin 恒命名 "default";JSON skins 是数组,deform timeline 以 skin 名为键。
8
+ // - deform 帧值在两个 loader 里都是"绝对值"(delta + setup);JSON 文件里的 vertices 是
9
+ // **相对 setup 的增量**(parser 会 += setup)——导出时必须减去 setup 顶点。
10
+ // - curves 按 19(float BEZIER_SIZE) 分段:[0]=类型(0 linear/1 stepped/2 bezier)。
11
+ // bezier 段不存控制点,存 t=k/10 (k=1..9) 的 9 个 (x,y) 采样——可反解控制点
12
+ // (x(t)=3(1-t)²t·cx1+3(1-t)t²·cx2+t³,两组采样解 2×2 线性方程,再全采样校验)。
13
+ // - JSON color 为 "rrggbbaa" 十六进制;binary rgba8888→float 往返无损。
14
+ "use strict";
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+ const { loadAgent, loadSkelDataFromJson } = require("./spine_node.cjs");
19
+
20
+ const BEZIER_SIZE = 19;
21
+ const TRANSFORM_MODES = ["normal", "onlyTranslation", "noRotationOrReflection", "noScale", "noScaleOrReflection"];
22
+ const BLEND_MODES = ["normal", "additive", "multiply", "screen"];
23
+
24
+ function r2(v) {
25
+ // 浮点输出精简:保留足够精度(float32 精度 ~1e-7,7 位有效数字安全)
26
+ return Number.isInteger(v) ? v : Number(v.toFixed(7));
27
+ }
28
+
29
+ function arr(a) {
30
+ return Array.from(a, r2);
31
+ }
32
+
33
+ function colorToHex(c) {
34
+ const b = (v) => Math.max(0, Math.min(255, Math.round(v * 255))).toString(16).padStart(2, "0");
35
+ return b(c.r) + b(c.g) + b(c.b) + b(c.a);
36
+ }
37
+
38
+ // ---- 贝塞尔控制点反解 ----
39
+ // seg: 采样数组(9 对 x,y,即 curves[base+1..base+18])
40
+ function recoverBezierControls(seg) {
41
+ const S = [];
42
+ for (let k = 1; k <= 9; k++) S.push({ x: seg[(k - 1) * 2], y: seg[(k - 1) * 2 + 1] });
43
+ // x(t) = a(t)*cx1 + b(t)*cx2 + t^3, t=k/10
44
+ const solve2 = (t1, t2, v1, v2) => {
45
+ const a1 = 3 * (1 - t1) * (1 - t1) * t1, b1 = 3 * (1 - t1) * t1 * t1;
46
+ const a2 = 3 * (1 - t2) * (1 - t2) * t2, b2 = 3 * (1 - t2) * t2 * t2;
47
+ const c1 = v1 - t1 * t1 * t1, c2 = v2 - t2 * t2 * t2;
48
+ const det = a1 * b2 - b1 * a2;
49
+ if (Math.abs(det) < 1e-9) return null;
50
+ return [(c1 * b2 - b1 * c2) / det, (a1 * c2 - c1 * a2) / det];
51
+ };
52
+ const cx = solve2(0.1, 0.2, S[0].x, S[1].x);
53
+ const cy = solve2(0.1, 0.2, S[0].y, S[1].y);
54
+ if (!cx || !cy) return null;
55
+ const [cx1, cx2] = cx, [cy1, cy2] = cy;
56
+ // 全采样校验(float32 误差容忍)
57
+ for (let k = 1; k <= 9; k++) {
58
+ const t = k / 10, o = 1 - t;
59
+ const ex = 3 * o * o * t * cx1 + 3 * o * t * t * cx2 + t * t * t;
60
+ const ey = 3 * o * o * t * cy1 + 3 * o * t * t * cy2 + t * t * t;
61
+ if (Math.abs(ex - S[k - 1].x) > 2e-4 || Math.abs(ey - S[k - 1].y) > 2e-4) return null;
62
+ }
63
+ return [cx1, cy1, cx2, cy2];
64
+ }
65
+
66
+ // 导出第 i 段曲线;linear 返回 {}(省略键),stepped/bBezier 附加键
67
+ function curveFields(timeline, i) {
68
+ const base = i * BEZIER_SIZE;
69
+ const type = timeline.curves[base];
70
+ if (type === 0) return {};
71
+ if (type === 1) return { curve: "stepped" };
72
+ const ctl = recoverBezierControls(timeline.curves.subarray(base + 1, base + BEZIER_SIZE));
73
+ if (!ctl) throw new Error("bezier 控制点反解失败(采样校验不过)");
74
+ return { curve: r2(ctl[0]), c2: r2(ctl[1]), c3: r2(ctl[2]), c4: r2(ctl[3]) };
75
+ }
76
+
77
+ // ---- 附件导出 ----
78
+ function exportAttachment(a) {
79
+ if (a.constructor.name === "RegionAttachment") {
80
+ const m = { type: "region", name: a.name, path: a.path, x: r2(a.x), y: r2(a.y) };
81
+ if (a.rotation) m.rotation = r2(a.rotation);
82
+ if (a.scaleX !== 1) m.scaleX = r2(a.scaleX);
83
+ if (a.scaleY !== 1) m.scaleY = r2(a.scaleY);
84
+ m.width = r2(a.width);
85
+ m.height = r2(a.height);
86
+ m.color = colorToHex(a.color);
87
+ return m;
88
+ }
89
+ if (a.constructor.name === "MeshAttachment") {
90
+ if (a.bones != null) throw new Error(`加权 mesh 不支持导出: ${a.name}`);
91
+ const m = {
92
+ type: "mesh",
93
+ name: a.name,
94
+ path: a.path,
95
+ color: colorToHex(a.color),
96
+ uvs: arr(a.regionUVs),
97
+ vertices: arr(a.vertices),
98
+ triangles: arr(a.triangles),
99
+ hull: a.hullLength / 2,
100
+ };
101
+ if (a.edges) m.edges = arr(a.edges);
102
+ if (a.width) m.width = r2(a.width);
103
+ if (a.height) m.height = r2(a.height);
104
+ return m;
105
+ }
106
+ throw new Error(`不支持的附件类型: ${a.constructor.name} (${a.name})`);
107
+ }
108
+
109
+ // ---- 动画导出 ----
110
+ function exportAnimation(anim, data) {
111
+ const out = { slots: {}, bones: {}, deform: {}, drawOrder: [] };
112
+ const skin = data.defaultSkin || data.skins[0];
113
+ const nSeg = (t) => Math.floor(t.frames.length / (t.frameVertices ? 1 : (t.constructor.ENTRIES || 1)));
114
+
115
+ for (const t of anim.timelines) {
116
+ const cn = t.constructor.name;
117
+ if (cn === "AttachmentTimeline") {
118
+ const slot = data.slots[t.slotIndex].name;
119
+ const list = (out.slots[slot] = out.slots[slot] || {});
120
+ const tl = (list.attachment = []);
121
+ for (let i = 0; i < t.frames.length; i++)
122
+ tl.push({ time: r2(t.frames[i]), name: t.attachmentNames[i] });
123
+ } else if (cn === "ColorTimeline") {
124
+ const slot = data.slots[t.slotIndex].name;
125
+ const list = (out.slots[slot] = out.slots[slot] || {});
126
+ const tl = (list.color = []);
127
+ const E = 5;
128
+ for (let i = 0; i * E < t.frames.length; i++) {
129
+ const b = i * E;
130
+ const hex = colorToHex({ r: t.frames[b + 1], g: t.frames[b + 2], b: t.frames[b + 3], a: t.frames[b + 4] });
131
+ tl.push({ time: r2(t.frames[b]), color: hex, ...curveFields(t, i) });
132
+ }
133
+ } else if (cn === "TranslateTimeline" || cn === "ScaleTimeline" || cn === "ShearTimeline") {
134
+ const bone = data.bones[t.boneIndex].name;
135
+ const kind = cn === "TranslateTimeline" ? "translate" : cn === "ScaleTimeline" ? "scale" : "shear";
136
+ const list = (out.bones[bone] = out.bones[bone] || {});
137
+ const tl = (list[kind] = []);
138
+ const E = 3, def = kind === "scale" ? 1 : 0;
139
+ for (let i = 0; i * E < t.frames.length; i++) {
140
+ const b = i * E;
141
+ const f = { time: r2(t.frames[b]) };
142
+ if (t.frames[b + 1] !== def) f.x = r2(t.frames[b + 1]);
143
+ if (t.frames[b + 2] !== def) f.y = r2(t.frames[b + 2]);
144
+ tl.push({ ...f, ...curveFields(t, i) });
145
+ }
146
+ } else if (cn === "RotateTimeline") {
147
+ const bone = data.bones[t.boneIndex].name;
148
+ const list = (out.bones[bone] = out.bones[bone] || {});
149
+ const tl = (list.rotate = []);
150
+ const E = 2;
151
+ for (let i = 0; i * E < t.frames.length; i++) {
152
+ const b = i * E;
153
+ const f = { time: r2(t.frames[b]) };
154
+ if (t.frames[b + 1] !== 0) f.angle = r2(t.frames[b + 1]);
155
+ tl.push({ ...f, ...curveFields(t, i) });
156
+ }
157
+ } else if (cn === "DeformTimeline") {
158
+ const slot = data.slots[t.slotIndex].name;
159
+ const attName = t.attachment.name;
160
+ const setup = t.attachment.vertices;
161
+ if (t.attachment.bones != null) throw new Error(`加权 deform 不支持导出: ${attName}`);
162
+ const perSkin = (out.deform[skin.name] = out.deform[skin.name] || {});
163
+ const perSlot = (perSkin[slot] = perSkin[slot] || {});
164
+ const tl = (perSlot[attName] = []);
165
+ for (let i = 0; i < t.frames.length; i++) {
166
+ const fv = t.frameVertices[i];
167
+ const isSetup = fv === setup;
168
+ const delta = new Array(fv.length);
169
+ let allZero = isSetup;
170
+ if (!isSetup) {
171
+ allZero = true;
172
+ for (let v = 0; v < fv.length; v++) {
173
+ delta[v] = fv[v] - setup[v];
174
+ if (Math.abs(delta[v]) > 1e-9) allZero = false;
175
+ }
176
+ }
177
+ tl.push({
178
+ time: r2(t.frames[i]),
179
+ ...(allZero ? { vertices: null } : { vertices: delta.map(r2) }),
180
+ ...curveFields(t, i),
181
+ });
182
+ }
183
+ } else if (cn === "DrawOrderTimeline") {
184
+ for (let i = 0; i < t.frames.length; i++) {
185
+ const perm = t.drawOrders[i]; // drawOrder[position] = originalIndex
186
+ const pos = new Array(perm.length);
187
+ for (let p = 0; p < perm.length; p++) pos[perm[p]] = p;
188
+ const offsets = [];
189
+ for (let o = 0; o < perm.length; o++)
190
+ if (pos[o] !== o) offsets.push({ slot: data.slots[o].name, offset: pos[o] - o });
191
+ out.drawOrder.push({ time: r2(t.frames[i]), offsets });
192
+ }
193
+ } else {
194
+ throw new Error(`不支持的 timeline 类型: ${cn} (${anim.name})`);
195
+ }
196
+ }
197
+ for (const k of ["slots", "bones", "deform"]) if (!Object.keys(out[k]).length) delete out[k];
198
+ if (!out.drawOrder.length) delete out.drawOrder;
199
+ return out;
200
+ }
201
+
202
+ // ---- 顶层导出 ----
203
+ function exportSkeletonData(data) {
204
+ if (data.ikConstraints.length || data.transformConstraints.length || data.pathConstraints.length)
205
+ throw new Error("存在 IK/transform/path 约束,导出器未覆盖");
206
+ if (data.events.length) throw new Error("存在事件,导出器未覆盖");
207
+
208
+ const root = {
209
+ skeleton: {
210
+ hash: data.hash,
211
+ spine: data.version,
212
+ x: r2(data.x), y: r2(data.y), width: r2(data.width), height: r2(data.height),
213
+ },
214
+ bones: data.bones.map((b) => {
215
+ const m = { name: b.name };
216
+ if (b.parent) m.parent = b.parent.name;
217
+ if (b.rotation) m.rotation = r2(b.rotation);
218
+ m.x = r2(b.x); m.y = r2(b.y);
219
+ if (b.scaleX !== 1) m.scaleX = r2(b.scaleX);
220
+ if (b.scaleY !== 1) m.scaleY = r2(b.scaleY);
221
+ if (b.shearX) m.shearX = r2(b.shearX);
222
+ if (b.shearY) m.shearY = r2(b.shearY);
223
+ if (b.length) m.length = r2(b.length);
224
+ m.transform = TRANSFORM_MODES[b.transformMode] || "normal";
225
+ return m;
226
+ }),
227
+ slots: data.slots.map((s) => {
228
+ const m = { name: s.name, bone: s.boneData.name, color: colorToHex(s.color) };
229
+ if (s.darkColor) m.dark = colorToHex(s.darkColor);
230
+ if (s.attachmentName != null) m.attachment = s.attachmentName;
231
+ if (s.blendMode) m.blend = BLEND_MODES[s.blendMode] || "normal";
232
+ return m;
233
+ }),
234
+ skins: data.skins.map((skin) => {
235
+ const attachments = {};
236
+ for (const slotIdx of Object.keys(skin.attachments)) {
237
+ const slotName = data.slots[Number(slotIdx)].name;
238
+ const per = {};
239
+ for (const name of Object.keys(skin.attachments[slotIdx])) {
240
+ const a = skin.attachments[slotIdx][name];
241
+ if (a) per[name] = exportAttachment(a);
242
+ }
243
+ attachments[slotName] = per;
244
+ }
245
+ return { name: skin.name, attachments };
246
+ }),
247
+ animations: {},
248
+ };
249
+ for (const anim of data.animations) root.animations[anim.name] = exportAnimation(anim, data);
250
+ return root;
251
+ }
252
+
253
+ // ---- 深比对(--verify) ----
254
+ function approx(a, b, eps) {
255
+ if (a === b) return true;
256
+ if (typeof a === "number" && typeof b === "number") return Math.abs(a - b) <= eps;
257
+ return false;
258
+ }
259
+
260
+ function cmpFloatArray(label, x, y, eps, errors) {
261
+ const xa = Array.from(x), ya = Array.from(y);
262
+ if (xa.length !== ya.length) { errors.push(`${label}: 长度 ${xa.length} != ${ya.length}`); return; }
263
+ for (let i = 0; i < xa.length; i++)
264
+ if (!approx(xa[i], ya[i], eps)) { errors.push(`${label}[${i}]: ${xa[i]} != ${ya[i]}`); return; }
265
+ }
266
+
267
+ function cmpCurves(label, ta, tb, errors) {
268
+ const len = Math.min(ta.curves.length, tb.curves.length);
269
+ if (ta.curves.length !== tb.curves.length) { errors.push(`${label}: curves 长度不等`); return; }
270
+ for (let i = 0; i < len; i += BEZIER_SIZE) {
271
+ const typeA = ta.curves[i], typeB = tb.curves[i];
272
+ if (typeA !== typeB) { errors.push(`${label} seg${i / BEZIER_SIZE}: 类型 ${typeA}!=${typeB}`); return; }
273
+ const eps = typeA === 2 ? 2e-3 : 1e-6;
274
+ for (let j = i; j < i + BEZIER_SIZE; j++)
275
+ if (!approx(ta.curves[j], tb.curves[j], eps)) { errors.push(`${label} seg${i / BEZIER_SIZE}[${j - i}]: ${ta.curves[j]} != ${tb.curves[j]}`); return; }
276
+ }
277
+ }
278
+
279
+ function verify(a, b) {
280
+ const errors = [];
281
+ const ctx = (s) => errors.push.bind(errors, s);
282
+
283
+ // 元数据
284
+ if (a.version !== b.version) ctx()( `version ${a.version} != ${b.version}`);
285
+ for (const k of ["x", "y", "width", "height"]) if (!approx(a[k], b[k], 1e-4)) ctx()(`skeleton.${k}`);
286
+
287
+ // 骨
288
+ if (a.bones.length !== b.bones.length) ctx()("骨数量不等");
289
+ a.bones.forEach((ba, i) => {
290
+ const bb = b.bones[i];
291
+ if (ba.name !== bb.name) return ctx()(`bone[${i}] 名 ${ba.name}!=${bb.name}`);
292
+ for (const k of ["x", "y", "rotation", "scaleX", "scaleY", "shearX", "shearY", "length"]) {
293
+ if (!approx(ba[k], bb[k], 1e-4)) ctx()(`bone ${ba.name}.${k}: ${ba[k]} != ${bb[k]}`);
294
+ }
295
+ const pa = ba.parent ? ba.parent.name : null, pb = bb.parent ? bb.parent.name : null;
296
+ if (pa !== pb) ctx()(`bone ${ba.name} parent ${pa}!=${pb}`);
297
+ });
298
+
299
+ // 槽
300
+ if (a.slots.length !== b.slots.length) ctx()("槽数量不等");
301
+ a.slots.forEach((sa, i) => {
302
+ const sb = b.slots[i];
303
+ if (sa.name !== sb.name) return ctx()(`slot[${i}] 名 ${sa.name}!=${sb.name}`);
304
+ if (sa.boneData.name !== sb.boneData.name) ctx()(`slot ${sa.name} bone 不等`);
305
+ if (colorToHex(sa.color) !== colorToHex(sb.color)) ctx()(`slot ${sa.name} color`);
306
+ if ((sa.attachmentName || null) !== (sb.attachmentName || null)) ctx()(`slot ${sa.name} setupAtt ${sa.attachmentName}!=${sb.attachmentName}`);
307
+ if (sa.blendMode !== sb.blendMode) ctx()(`slot ${sa.name} blend`);
308
+ });
309
+
310
+ // 皮肤附件
311
+ if (a.skins.length !== b.skins.length) ctx()("皮肤数量不等");
312
+ a.skins.forEach((ska, i) => {
313
+ const skb = b.skins[i];
314
+ if (ska.name !== skb.name) ctx()(`skin[${i}] 名 ${ska.name}!=${skb.name}`);
315
+ for (const slotIdx of Object.keys(ska.attachments)) {
316
+ const perA = ska.attachments[slotIdx];
317
+ const perB = skb.attachments[slotIdx] || {};
318
+ const slotName = a.slots[Number(slotIdx)].name;
319
+ const namesA = Object.keys(perA).sort(), namesB = Object.keys(perB).sort();
320
+ if (namesA.join() !== namesB.join()) { ctx()(`skin ${ska.name} slot ${slotName} 附件集不等`); continue; }
321
+ for (const n of namesA) {
322
+ const aa = perA[n], ab = perB[n];
323
+ const p = `skin[${ska.name}] ${slotName}/${n}`;
324
+ if (aa.constructor.name !== ab.constructor.name) { ctx()(`${p} 类型不等`); continue; }
325
+ if (aa.path !== ab.path) ctx()(`${p} path`);
326
+ if (colorToHex(aa.color) !== colorToHex(ab.color)) ctx()(`${p} color`);
327
+ if (aa.constructor.name === "RegionAttachment") {
328
+ for (const k of ["x", "y", "rotation", "scaleX", "scaleY", "width", "height"])
329
+ if (!approx(aa[k], ab[k], 1e-4)) ctx()(`${p}.${k}: ${aa[k]} != ${ab[k]}`);
330
+ cmpFloatArray(`${p}.offset`, aa.offset, ab.offset, 1e-4, errors);
331
+ } else {
332
+ cmpFloatArray(`${p}.vertices`, aa.vertices, ab.vertices, 1e-4, errors);
333
+ cmpFloatArray(`${p}.regionUVs`, aa.regionUVs, ab.regionUVs, 1e-6, errors);
334
+ cmpFloatArray(`${p}.uvs`, aa.uvs, ab.uvs, 1e-6, errors);
335
+ cmpFloatArray(`${p}.triangles`, aa.triangles, ab.triangles, 0, errors);
336
+ if (aa.hullLength !== ab.hullLength) ctx()(`${p} hull ${aa.hullLength}!=${ab.hullLength}`);
337
+ if ((aa.edges ? aa.edges.length : -1) !== (ab.edges ? ab.edges.length : -1)) ctx()(`${p} edges`);
338
+ }
339
+ }
340
+ }
341
+ });
342
+
343
+ // 动画(按 属性标识 分组比对,顺序无关)
344
+ const keyOf = (t, data) => `${t.constructor.name}#${t.slotIndex ?? t.boneIndex ?? ""}#${t.attachment ? t.attachment.name : ""}`;
345
+ if (a.animations.length !== b.animations.length) ctx()(`动画数量 ${a.animations.length}!=${b.animations.length}`);
346
+ const mapB = new Map(b.animations.map((x) => [x.name, x]));
347
+ for (const anA of a.animations) {
348
+ const anB = mapB.get(anA.name);
349
+ if (!anB) { ctx()(`缺动画 ${anA.name}`); continue; }
350
+ if (!approx(anA.duration, anB.duration, 1e-4)) ctx()(`${anA.name} duration`);
351
+ const gA = new Map();
352
+ for (const t of anA.timelines) { const k = keyOf(t, a); if (gA.has(k)) ctx()(`${anA.name} 重复 timeline ${k}`); gA.set(k, t); }
353
+ const gB = new Map(anB.timelines.map((t) => [keyOf(t, b), t]));
354
+ if (gA.size !== gB.size) { ctx()(`${anA.name} timeline 数 ${gA.size}!=${gB.size} (${[...gA.keys()].filter(k=>!gB.has(k)).join(",")})`); continue; }
355
+ for (const [k, ta] of gA) {
356
+ const tb = gB.get(k);
357
+ if (!tb) { ctx()(`${anA.name} 缺 timeline ${k}`); continue; }
358
+ const eps = 1e-4;
359
+ cmpFloatArray(`${anA.name}/${k}.frames`, ta.frames, tb.frames, eps, errors);
360
+ if (ta.curves && tb.curves) cmpCurves(`${anA.name}/${k}`, ta, tb, errors);
361
+ if (ta.constructor.name === "DeformTimeline") {
362
+ if (ta.frameVertices.length !== tb.frameVertices.length) { ctx()(`${anA.name}/${k} deform 帧数不等`); continue; }
363
+ for (let i = 0; i < ta.frameVertices.length; i++)
364
+ cmpFloatArray(`${anA.name}/${k}.deform[${i}]`, ta.frameVertices[i], tb.frameVertices[i], 1e-4, errors);
365
+ }
366
+ if (ta.constructor.name === "AttachmentTimeline") {
367
+ const na = ta.attachmentNames.map((x) => x ?? null).join("|");
368
+ const nb = tb.attachmentNames.map((x) => x ?? null).join("|");
369
+ if (na !== nb) ctx()(`${anA.name}/${k} attachmentNames 不等`);
370
+ }
371
+ if (ta.constructor.name === "DrawOrderTimeline") {
372
+ for (let i = 0; i < ta.drawOrders.length; i++)
373
+ if (Array.from(ta.drawOrders[i]).join(",") !== Array.from(tb.drawOrders[i]).join(","))
374
+ ctx()(`${anA.name}/${k} drawOrder[${i}] 不等`);
375
+ }
376
+ }
377
+ }
378
+ return errors;
379
+ }
380
+
381
+ // ---- main ----
382
+ function main() {
383
+ const args = process.argv.slice(2);
384
+ const id = args.find((x) => !x.startsWith("--"));
385
+ if (!id) { console.error("用法: node pet/tools/skel_to_json.cjs <agentId> [--verify] [--out <path>]"); process.exit(2); }
386
+ const verifyFlag = args.includes("--verify");
387
+ const outIdx = args.indexOf("--out");
388
+ const { skelData, base, skelFile } = loadAgent(id);
389
+
390
+ const root = exportSkeletonData(skelData);
391
+ const outPath = outIdx >= 0 ? args[outIdx + 1] : path.join(base, `${id}_spr.json`);
392
+ const json = JSON.stringify(root, null, 1);
393
+ fs.writeFileSync(outPath, json);
394
+ const sizeKb = (fs.statSync(outPath).size / 1024).toFixed(1);
395
+ console.log(`[${id}] ${skelFile} -> ${path.relative(process.cwd(), outPath)} (${sizeKb} KB)`);
396
+
397
+ if (verifyFlag) {
398
+ const { loader } = loadAgent(id);
399
+ const dataB = loadSkelDataFromJson(loader, outPath);
400
+ const errors = verify(skelData, dataB);
401
+ if (errors.length) {
402
+ console.error(`verify FAILED: ${errors.length} 处差异(前 20 条):`);
403
+ for (const e of errors.slice(0, 20)) console.error(" - " + e);
404
+ process.exit(1);
405
+ }
406
+ console.log(`[${id}] verify OK: 骨/槽/皮肤附件/动画 timeline 深比对全部一致`);
407
+ }
408
+ }
409
+
410
+ main();
@@ -0,0 +1,69 @@
1
+ // Node 端加载 vendored Spine 3.8 runtime 并解析项目 skel 资产的工具库。
2
+ // 仅供 pet/tools/ 下离线脚本使用(inspect / skel_to_json / gen_sway 等),不进渲染链路。
3
+ //
4
+ // 两个坑:
5
+ // 1) vendored spine-webgl.js 是挂全局 `spine` 的 IIFE UMD(非 CommonJS),
6
+ // require() 拿不到导出 —— 用 new Function 包一层 eval,注入沙箱 window/document。
7
+ // 2) TextureAtlas 构造时会调 textureLoader 返回对象的 setFilters/setWraps/getImage,
8
+ // 纯 Node 下没有真纹理 —— 用假 texture(宽高从 PNG IHDR 头读,保证 atlas 页尺寸正确)。
9
+ "use strict";
10
+
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+
14
+ const PET = path.join(__dirname, "..");
15
+ const ASSETS = path.join(PET, "..", "assets", "blue-archive");
16
+
17
+ const spineSrc = fs.readFileSync(
18
+ path.join(PET, "vendor", "spine", "spine-webgl.js"),
19
+ "utf8"
20
+ );
21
+
22
+ const spine = (() => {
23
+ const sandboxSpine = {};
24
+ const fn = new Function("spine", "window", "document", spineSrc + "\nreturn spine;");
25
+ return fn(sandboxSpine, {}, {});
26
+ })();
27
+
28
+ // 按角色 id 解析 { atlas, skelData, pngSize }。
29
+ // id 解析路径规则与 pet/agents.cjs 一致:arona/plana 在 spine/ 子目录,其余在角色根目录。
30
+ function loadAgent(id) {
31
+ const withSub = ["arona", "plana"].includes(id);
32
+ const base = withSub
33
+ ? path.join(ASSETS, id, "spine")
34
+ : path.join(ASSETS, id);
35
+ const atlasFile = `${id}_spr.atlas.txt`;
36
+ const skelFile = `${id}_spr.skel`;
37
+ const pngFile = `${id}_spr.png`;
38
+
39
+ const pngBuf = fs.readFileSync(path.join(base, pngFile));
40
+ const pngW = pngBuf.readUInt32BE(16);
41
+ const pngH = pngBuf.readUInt32BE(20);
42
+
43
+ const fakeTexture = () => ({
44
+ setFilters() {},
45
+ setWraps() {},
46
+ dispose() {},
47
+ getImage: () => ({ width: pngW, height: pngH }),
48
+ });
49
+
50
+ const atlasText = fs.readFileSync(path.join(base, atlasFile), "utf8");
51
+ const atlas = new spine.TextureAtlas(atlasText, fakeTexture);
52
+ const loader = new spine.AtlasAttachmentLoader(atlas);
53
+
54
+ const buf = fs.readFileSync(path.join(base, skelFile));
55
+ const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
56
+ const skelData = new spine.SkeletonBinary(loader).readSkeletonData(
57
+ new Uint8Array(ab)
58
+ );
59
+
60
+ return { id, base, atlas, skelData, atlasFile, skelFile, pngW, pngH, loader };
61
+ }
62
+
63
+ // 从 JSON 文件解析 SkeletonData(round-trip 验证用),atlasLoader 复用 loadAgent 的。
64
+ function loadSkelDataFromJson(atlasLoader, jsonPath) {
65
+ const obj = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
66
+ return new spine.SkeletonJson(atlasLoader).readSkeletonData(obj);
67
+ }
68
+
69
+ module.exports = { spine, loadAgent, loadSkelDataFromJson };