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,170 @@
1
+ // 衣摆微动生成器:向角色的 <id>_spr.json 的 Idle_01 注入衣摆 DeformTimeline。
2
+ // 用法:node pet/tools/gen_sway.cjs shiroko [--amp 20] [--keys 12]
3
+ // 模式(SWAY_TARGETS[].main.mode):
4
+ // band = y 区间带内摆动(Shiroko 百褶裙:腿/手臂钉死,只动裙摆带;带内越低越自由)
5
+ // bottom= mesh 底部向上渐变(旧逻辑,仅适用于底缘就是自由端的 mesh)
6
+ // 原理(对未加权 mesh,附件顶点在骨本地空间):
7
+ // - dx = amp·w·sin(2πt/T + φ(x)),φ 随 x 错开制造布料波浪
8
+ // - dy = amp·dyRatio·w·sin(4πt/T)(微弱垂直起伏)
9
+ // - 关键帧 k·T/12(13 帧),首末帧数值相同 → 与 Idle_01 无缝循环
10
+ // 注意:JSON deform 的 vertices 语义是"相对 setup 的增量"(loader 会加回 setup)。
11
+ // ⚠️ 不要给 tiltSlots 里的 slot(00_default/Hair_Cover/eyeclose)加 deform:
12
+ // computeWorldVertices 在 deform 存在时整体替换 setup 顶点 → 摸头 tilt 顶点修改失效。
13
+ // 幂等:重复执行直接覆盖同名 deform timeline,不会叠加。
14
+ "use strict";
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+ const { loadAgent } = require("./spine_node.cjs");
19
+
20
+ // 每角色摆动部位配置。
21
+ // Shiroko 实测(2026-08-18 目视+顶点勘察):黑色百褶裙 y≈950~1260(±535 两外角在 y 1235~1241,
22
+ // 中部裙片垂到 y 982~1132);腿 y<600(8 顶点)、手臂衔接 y>1300 —— 全部钉死。
23
+ const SWAY_TARGETS = {
24
+ shiroko: {
25
+ idle: "Idle_01",
26
+ main: {
27
+ slot: "siroko2", att: "siroko", mode: "band",
28
+ bandMin: 950, bandMax: 1260, ampScale: 1, dyRatio: 0.35,
29
+ },
30
+ },
31
+ // Hoshino 身体先经 meshify_region.cjs 网格化(6×10,region y 253~2250)。
32
+ // band 1100~1500 ≈ 黑百褶裙带(背部工具包 y>1500 不动、腿 y<1100 不动);可用 --band=mn:mx 调。
33
+ hoshino: {
34
+ idle: "Idle_01",
35
+ main: {
36
+ slot: "hoshino", att: "hoshino", mode: "band",
37
+ bandMin: 1100, bandMax: 1500, ampScale: 1, dyRatio: 0.35,
38
+ },
39
+ },
40
+ };
41
+
42
+ function smoothstep(x) {
43
+ const t = Math.max(0, Math.min(1, x));
44
+ return t * t * (3 - 2 * t);
45
+ }
46
+
47
+ function r2(v) {
48
+ return Number.isInteger(v) ? v : Number(v.toFixed(4));
49
+ }
50
+
51
+ function genForTarget(meshAtt, tgt, amp, keys, T) {
52
+ const verts = meshAtt.vertices;
53
+ const n = verts.length / 2;
54
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
55
+ for (let i = 0; i < verts.length; i += 2) {
56
+ if (verts[i] < minX) minX = verts[i];
57
+ if (verts[i] > maxX) maxX = verts[i];
58
+ if (verts[i + 1] < minY) minY = verts[i + 1];
59
+ if (verts[i + 1] > maxY) maxY = verts[i + 1];
60
+ }
61
+ const spanX = Math.max(1e-6, maxX - minX);
62
+ const spanY = Math.max(1e-6, maxY - minY);
63
+ const mode = tgt.mode || "bottom";
64
+ const bandMin = tgt.bandMin ?? 0;
65
+ const bandMax = tgt.bandMax ?? 0;
66
+ const ampV = amp * (tgt.dyRatio ?? 0.25);
67
+ // 顶点权重(band 模式:带外 0;带内基础 0.35 + 越低(自由端)越强)
68
+ const weight = (vy) => {
69
+ if (mode === "band") {
70
+ if (vy < bandMin || vy > bandMax) return 0;
71
+ return 0.35 + 0.65 * smoothstep((bandMax - vy) / Math.max(1e-6, bandMax - bandMin));
72
+ }
73
+ // bottom:mesh 底(自由端)最大,上 45% 钉死
74
+ const hNorm = (maxY - vy) / spanY;
75
+ return smoothstep((hNorm - 0.45) / 0.55);
76
+ };
77
+ const frames = [];
78
+ for (let k = 0; k <= keys; k++) {
79
+ const t = (k / keys) * T;
80
+ const delta = new Array(verts.length).fill(0);
81
+ for (let v = 0; v < n; v++) {
82
+ const vx = verts[v * 2], vy = verts[v * 2 + 1];
83
+ const w = weight(vy);
84
+ if (w <= 0) continue;
85
+ const nx = (vx - minX) / spanX; // 0~1 左→右
86
+ const phase = (nx - 0.5) * (Math.PI / 1.5); // 横向波浪相位差
87
+ delta[v * 2] = amp * w * Math.sin((2 * Math.PI * t) / T + phase);
88
+ delta[v * 2 + 1] = ampV * w * Math.sin((4 * Math.PI * t) / T);
89
+ }
90
+ frames.push({ time: r2(t), vertices: delta.map(r2) });
91
+ }
92
+ // 首末帧严格相同(无缝循环)
93
+ frames[keys].vertices = frames[0].vertices.slice();
94
+ return frames;
95
+ }
96
+
97
+ function main() {
98
+ const args = process.argv.slice(2);
99
+ const id = args.find((x) => !x.startsWith("--"));
100
+ if (!id || !SWAY_TARGETS[id]) {
101
+ console.error("用法: node pet/tools/gen_sway.cjs <shiroko|hoshino> [--amp 4] [--keys 12] [--band=mn:mx]");
102
+ process.exit(2);
103
+ }
104
+ const cfg = SWAY_TARGETS[id];
105
+ const opt = (name, dflt) => {
106
+ const i = args.indexOf(`--${name}`);
107
+ return i >= 0 && args[i + 1] !== undefined && !Number.isNaN(Number(args[i + 1]))
108
+ ? Number(args[i + 1]) : dflt;
109
+ };
110
+ const amp = opt("amp", 4);
111
+ const keys = opt("keys", 12);
112
+
113
+ const { skelData, base } = loadAgent(id);
114
+ const idle = skelData.findAnimation(cfg.idle);
115
+ if (!idle) throw new Error(`找不到动画 ${cfg.idle}`);
116
+ const T = idle.duration;
117
+
118
+ // 几何来源 = json 资产里的 mesh vertices(Hoshino 身体经 meshify_region 网格化后 skel 二进制里
119
+ // 没有 mesh;Shiroko 的数值与 skel 一致,统一从 json 读最简单)
120
+ const jsonPath = path.join(base, `${id}_spr.json`);
121
+ const root = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
122
+ const anim = (root.animations[cfg.idle] ??= {});
123
+ const deform = (anim.deform ??= {});
124
+ const perSkin = (deform["default"] ??= {});
125
+ const jsonSkin = (root.skins.find((s) => s.name === "default") || root.skins[0]).attachments;
126
+
127
+ for (let tgt of [cfg.main]) {
128
+ tgt = { ...tgt }; // CLI band 覆盖不污染静态配置
129
+ const bandArg = args.find((a) => a.startsWith("--band="));
130
+ if (bandArg && tgt.mode === "band") {
131
+ const [mn, mx] = bandArg.slice(7).split(":").map(Number);
132
+ if (isFinite(mn) && isFinite(mx)) tgt = { ...tgt, bandMin: mn, bandMax: mx };
133
+ }
134
+ const attMap = jsonSkin[tgt.slot] && jsonSkin[tgt.slot][tgt.att];
135
+ if (!attMap || attMap.type !== "mesh")
136
+ throw new Error(`${tgt.slot}/${tgt.att} 在 json 里不是 mesh(region 需先跑 meshify_region.cjs)`);
137
+ const att = { vertices: attMap.vertices }; // genForTarget 只用 vertices
138
+ const frames = genForTarget(att, tgt, amp * (tgt.ampScale ?? 1), keys, T);
139
+ const perSlot = (perSkin[tgt.slot] ??= {});
140
+ perSlot[tgt.att] = frames;
141
+ // 报告参与摆动的顶点与权重
142
+ const nv = att.vertices.length / 2;
143
+ const parts = [];
144
+ for (let i = 0; i < nv; i++) {
145
+ const vy = att.vertices[i * 2 + 1];
146
+ const vx = att.vertices[i * 2];
147
+ let w = 0;
148
+ if (tgt.mode === "band" && vy >= tgt.bandMin && vy <= tgt.bandMax)
149
+ w = 0.35 + 0.65 * smoothstep((tgt.bandMax - vy) / (tgt.bandMax - tgt.bandMin));
150
+ else if (!tgt.mode) {
151
+ let mnY = Infinity, mxY = -Infinity;
152
+ for (let j = 0; j < nv; j++) {
153
+ const y = att.vertices[j * 2 + 1];
154
+ if (y < mnY) mnY = y;
155
+ if (y > mxY) mxY = y;
156
+ }
157
+ w = smoothstep(((mxY - vy) / Math.max(1e-6, mxY - mnY) - 0.45) / 0.55);
158
+ }
159
+ if (w > 0.01) parts.push(`(x=${vx.toFixed(0)},y=${vy.toFixed(0)},w=${w.toFixed(2)})`);
160
+ }
161
+ console.log(`[${id}] ${cfg.idle}.deform.${tgt.slot}/${tgt.att}: ${frames.length} 帧, T=${T.toFixed(3)}s, amp=${(amp * (tgt.ampScale ?? 1)).toFixed(1)}, dyRatio=${tgt.dyRatio ?? 0.25}, band=${tgt.mode === "band" ? tgt.bandMin + "~" + tgt.bandMax : "n/a"}`);
162
+ console.log(` 摆动顶点(${parts.length}/${nv}): ${parts.join(" ")}`);
163
+ }
164
+
165
+ fs.writeFileSync(jsonPath, JSON.stringify(root, null, 1));
166
+ const sizeKb = (fs.statSync(jsonPath).size / 1024).toFixed(1);
167
+ console.log(`[${id}] 已写回 ${path.relative(process.cwd(), jsonPath)} (${sizeKb} KB)`);
168
+ }
169
+
170
+ main();
@@ -0,0 +1,105 @@
1
+ // region→mesh 网格化:把 json 资产里的 RegionAttachment 替换为显式网格 MeshAttachment,
2
+ // 使其可被 gen_sway 注入 deform(region 无顶点不可形变)。
3
+ // 用法:node pet/tools/meshify_region.cjs <agentId> <slot> <att> [--cols 6] [--rows 10]
4
+ // 前置:先跑 skel_to_json.cjs 导出 json(本脚本只改 json,不动 skel)。
5
+ // 几何:网格覆盖 region 的 local 四边形(x±w/2, y±h/2,仅支持 rotation=0);
6
+ // regionUVs = region 本地归一化坐标(0~1),updateUVs 自动映射页纹理(rotate region 亦然)。
7
+ // 幂等:重复执行覆盖同一 attachment。
8
+ "use strict";
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { loadAgent, spine } = require("./spine_node.cjs");
13
+
14
+ function main() {
15
+ const args = process.argv.slice(2).filter((x) => !x.startsWith("--"));
16
+ const opt = (name, dflt) => {
17
+ const i = process.argv.indexOf(`--${name}`);
18
+ const v = i >= 0 ? process.argv[i + 1] : undefined;
19
+ return v !== undefined && !Number.isNaN(Number(v)) ? Number(v) : dflt;
20
+ };
21
+ const [id, slotName, attName] = args;
22
+ if (!id || !slotName || !attName) {
23
+ console.error("用法: node pet/tools/meshify_region.cjs <agentId> <slot> <att> [--cols 6] [--rows 10]");
24
+ process.exit(2);
25
+ }
26
+ const cols = opt("cols", 6);
27
+ const rows = opt("rows", 10);
28
+
29
+ const { skelData, base } = loadAgent(id);
30
+ const skin = skelData.defaultSkin || skelData.skins[0];
31
+ const sIdx = skelData.slots.findIndex((s) => s.name === slotName);
32
+ if (sIdx < 0) throw new Error(`slot 不存在: ${slotName}`);
33
+ const att = skin.getAttachment(sIdx, attName);
34
+ if (!att || att.constructor.name !== "RegionAttachment")
35
+ throw new Error(`${slotName}/${attName} 不是 region attachment`);
36
+ if (Math.abs(att.rotation) > 1e-6)
37
+ throw new Error(`region rotation=${att.rotation} ≠ 0,网格化未覆盖旋转补偿`);
38
+
39
+ const x0 = att.x - att.width / 2, x1 = att.x + att.width / 2;
40
+ const y0 = att.y - att.height / 2, y1 = att.y + att.height / 2;
41
+
42
+ // 网格顶点(行优先)+ regionUVs(region 本地归一化)
43
+ // ⚠️ degrees=90(atlas rotate:true)region 的 regionUVs = (fx, 1-fy),非旋转 = (fx, fy)。
44
+ // 推导(对照 RegionAttachment.setRegion rotate 分支的真实角点映射 LL=(u2,v2)/UL=(u,v2)/UR=(u,v)/LR=(u2,v)
45
+ // ——注意源码里 uvs[0]/[1] 是最后赋值的,勿按阅读顺序记——
46
+ // 与 MeshAttachment.updateUVs case 90(pageU=u+uv.y·W'、pageV=v+(1-uv.x)·H')联立解得。
47
+ const rotated = !!(att.region && att.region.rotate);
48
+ const vertices = [];
49
+ const uvs = [];
50
+ for (let j = 0; j < rows; j++) {
51
+ for (let i = 0; i < cols; i++) {
52
+ const fx = cols === 1 ? 0.5 : i / (cols - 1);
53
+ const fy = rows === 1 ? 0.5 : j / (rows - 1);
54
+ vertices.push(+(x0 + fx * (x1 - x0)).toFixed(4), +(y0 + fy * (y1 - y0)).toFixed(4));
55
+ if (rotated) uvs.push(+fx.toFixed(6), +(1 - fy).toFixed(6));
56
+ else uvs.push(+fx.toFixed(6), +fy.toFixed(6));
57
+ }
58
+ }
59
+ // 三角形(每格 2 个,绕向一致)
60
+ const triangles = [];
61
+ for (let j = 0; j < rows - 1; j++) {
62
+ for (let i = 0; i < cols - 1; i++) {
63
+ const a = j * cols + i, b = j * cols + i + 1;
64
+ const c = (j + 1) * cols + i, d = (j + 1) * cols + i + 1;
65
+ triangles.push(a, b, c, b, d, c);
66
+ }
67
+ }
68
+ const hull = 2 * cols + 2 * rows - 4; // 周界顶点数
69
+
70
+ // 替换 json 中的 attachment(保留 name/path/color/width/height)
71
+ const jsonPath = path.join(base, `${id}_spr.json`);
72
+ const root = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
73
+ const jsonSkin = (root.skins.find((s) => s.name === "default") || root.skins[0]).attachments;
74
+ const old = jsonSkin[slotName] && jsonSkin[slotName][attName];
75
+ if (!old || old.type !== "region")
76
+ throw new Error(`json 里 ${slotName}/${attName} 不是 region(已是 mesh 则无需重复网格化)`);
77
+ jsonSkin[slotName][attName] = {
78
+ type: "mesh",
79
+ name: old.name,
80
+ path: old.path,
81
+ color: old.color,
82
+ width: old.width,
83
+ height: old.height,
84
+ uvs,
85
+ vertices,
86
+ triangles,
87
+ hull,
88
+ };
89
+ fs.writeFileSync(jsonPath, JSON.stringify(root, null, 1));
90
+
91
+ // sanity:SkeletonJson 读回,mesh updateUVs 成功且顶点/UV 数量匹配
92
+ const { loader } = loadAgent(id);
93
+ const dataB = new spine.SkeletonJson(loader).readSkeletonData(root);
94
+ const skinB = dataB.defaultSkin;
95
+ const attB = skinB.getAttachment(sIdx, attName);
96
+ if (attB.constructor.name !== "MeshAttachment") throw new Error("读回后不是 mesh");
97
+ if (attB.vertices.length !== vertices.length || attB.uvs.length !== uvs.length)
98
+ throw new Error("读回顶点/UV 数量不匹配");
99
+ const sizeKb = (fs.statSync(jsonPath).size / 1024).toFixed(1);
100
+ console.log(`[${id}] ${slotName}/${attName}: region → ${cols}×${rows} mesh(${cols * rows} 顶点 / ${triangles.length / 3} 三角形 / hull=${hull})`);
101
+ console.log(` bounds=(${x0.toFixed(1)},${y0.toFixed(1)})~(${x1.toFixed(1)},${y1.toFixed(1)}),读回 sanity OK`);
102
+ console.log(`[${id}] 已写回 ${path.relative(process.cwd(), jsonPath)} (${sizeKb} KB)`);
103
+ }
104
+
105
+ main();
@@ -0,0 +1,112 @@
1
+ // skel 结构勘察工具:dump 骨/槽/附件/动画 timeline + mesh 顶点 bounds + atlas region +
2
+ // Idle_01 关键帧数值。用于判定衣摆 mesh、为 gen_sway 定参数。
3
+ // 用法:node pet/tools/skel_inspect.cjs [agentId ...] (缺省 shiroko hoshino)
4
+ "use strict";
5
+
6
+ const { loadAgent } = require("./spine_node.cjs");
7
+
8
+ function fmt(n) {
9
+ if (typeof n !== "number" || !isFinite(n)) return String(n);
10
+ return Math.abs(n) < 1e-6 ? "0" : n.toFixed(2).replace(/\.?0+$/, "");
11
+ }
12
+
13
+ function attachmentInfo(a) {
14
+ const type = a.constructor.name;
15
+ if (type === "MeshAttachment") {
16
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
17
+ for (let i = 0; i < a.vertices.length; i += 2) {
18
+ if (a.vertices[i] < minX) minX = a.vertices[i];
19
+ if (a.vertices[i + 1] < minY) minY = a.vertices[i + 1];
20
+ if (a.vertices[i] > maxX) maxX = a.vertices[i];
21
+ if (a.vertices[i + 1] > maxY) maxY = a.vertices[i + 1];
22
+ }
23
+ return {
24
+ type,
25
+ verts: a.vertices.length / 2,
26
+ tris: a.triangles.length / 3,
27
+ hull: a.hull,
28
+ minX, minY, maxX, maxY,
29
+ w: maxX - minX,
30
+ h: maxY - minY,
31
+ cx: (minX + maxX) / 2,
32
+ cy: (minY + maxY) / 2,
33
+ };
34
+ }
35
+ return { type, x: a.x, y: a.y, w: a.width, h: a.height, rot: a.rotation };
36
+ }
37
+
38
+ function dump(id) {
39
+ const { skelData, atlas } = loadAgent(id);
40
+ const slotName = (i) => skelData.slots[i] ? skelData.slots[i].name : `#${i}`;
41
+
42
+ console.log(`\n===== ${id} =====`);
43
+ console.log(`skeleton meta: version=${skelData.version} hash=${skelData.hash} x=${skelData.x} y=${skelData.y} w=${skelData.width} h=${skelData.height}`);
44
+
45
+ console.log("\n-- bones --");
46
+ for (const b of skelData.bones) {
47
+ console.log(
48
+ ` ${b.name}${b.parent ? " <- " + b.parent.name : ""} x=${fmt(b.x)} y=${fmt(b.y)} rot=${fmt(b.rotation)} sx=${fmt(b.scaleX)} sy=${fmt(b.scaleY)}`
49
+ );
50
+ }
51
+
52
+ console.log("\n-- slots --");
53
+ skelData.slots.forEach((s, i) => {
54
+ console.log(` #${i} ${s.name} [bone=${s.boneData.name}] blend=${s.blendMode} setupAtt=${s.attachmentName}`);
55
+ });
56
+
57
+ console.log("\n-- attachments (skin) --");
58
+ const skin = skelData.defaultSkin || Object.values(skelData.skins)[0];
59
+ for (const slotIdx of Object.keys(skin.attachments)) {
60
+ const perSlot = skin.attachments[slotIdx];
61
+ for (const name of Object.keys(perSlot)) {
62
+ const a = perSlot[name];
63
+ if (!a) { console.log(` [${slotName(slotIdx)}] "${name}" -> null(卸下)`); continue; }
64
+ const info = attachmentInfo(a);
65
+ if (info.type === "MeshAttachment") {
66
+ console.log(
67
+ ` [${slotName(slotIdx)}] "${name}" mesh verts=${info.verts} tris=${info.tris} hull=${info.hull} bounds=(${fmt(info.minX)},${fmt(info.minY)})~(${fmt(info.maxX)},${fmt(info.maxY)}) w=${fmt(info.w)} h=${fmt(info.h)} c=(${fmt(info.cx)},${fmt(info.cy)})`
68
+ );
69
+ } else {
70
+ console.log(
71
+ ` [${slotName(slotIdx)}] "${name}" region x=${fmt(info.x)} y=${fmt(info.y)} ${fmt(info.w)}x${fmt(info.h)} rot=${fmt(info.rot)}`
72
+ );
73
+ }
74
+ }
75
+ }
76
+
77
+ console.log("\n-- atlas regions --");
78
+ for (const r of atlas.regions) {
79
+ console.log(` ${r.name} ${r.width}x${r.height} rotate=${r.rotate} xy=(${r.x},${r.y})`);
80
+ }
81
+
82
+ console.log("\n-- animations --");
83
+ for (const anim of skelData.animations) {
84
+ console.log(` * ${anim.name} dur=${anim.duration.toFixed(3)}s timelines=${anim.timelines.length}`);
85
+ for (const t of anim.timelines) {
86
+ const cn = t.constructor.name;
87
+ let target = "";
88
+ if (t.boneIndex !== undefined) target = `bone=${slotName2bone(skelData, t.boneIndex)}`;
89
+ else if (t.slotIndex !== undefined) target = `slot=${slotName(t.slotIndex)}`;
90
+ // 关键帧数值(仅 translate / deform 展开前几帧)
91
+ if (cn === "TranslateTimeline" && anim.name === "Idle_01") {
92
+ const frames = [];
93
+ for (let f = 0; f < t.frames.length; f += 3) {
94
+ frames.push(`(${t.frames[f].toFixed(2)},${t.frames[f + 1].toFixed(2)},${t.frames[f + 2].toFixed(2)})`);
95
+ }
96
+ console.log(` ${cn} ${target} frames(t,x,y): ${frames.join(" ")}`);
97
+ } else if (cn === "DeformTimeline" && anim.name === "Idle_01") {
98
+ console.log(` ${cn} ${target} frames=${t.frames.length} att=${t.attachment ? t.attachment.name : "?"}`);
99
+ } else {
100
+ console.log(` ${cn} ${target} frames=${t.frames.length / (t.frameVertices ? 1 : 1)}`);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ function slotName2bone(skelData, boneIndex) {
107
+ const b = skelData.bones[boneIndex];
108
+ return b ? b.name : `#${boneIndex}`;
109
+ }
110
+
111
+ const ids = process.argv.slice(2);
112
+ (ids.length ? ids : ["shiroko", "hoshino"]).forEach(dump);