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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/assets/blue-archive/arona/spine/arona_spr.atlas.txt +1168 -0
- package/assets/blue-archive/arona/spine/arona_spr.png +0 -0
- package/assets/blue-archive/arona/spine/arona_spr.skel +0 -0
- package/assets/blue-archive/arona/voice.mp3 +0 -0
- package/assets/blue-archive/hoshino/hoshino_spr.atlas.txt +174 -0
- package/assets/blue-archive/hoshino/hoshino_spr.json +15385 -0
- package/assets/blue-archive/hoshino/hoshino_spr.png +0 -0
- package/assets/blue-archive/hoshino/hoshino_spr.skel +0 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.atlas.txt +111 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.png +0 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.skel +0 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.atlas.txt +251 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.png +0 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.skel +0 -0
- package/assets/blue-archive/hoshino/voice.mp3 +0 -0
- package/assets/blue-archive/plana/spine/plana_spr.atlas.txt +1266 -0
- package/assets/blue-archive/plana/spine/plana_spr.png +0 -0
- package/assets/blue-archive/plana/spine/plana_spr.skel +0 -0
- package/assets/blue-archive/plana/voice.mp3 +0 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.atlas.txt +97 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.png +0 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.skel +0 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.atlas.txt +174 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.png +0 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.skel +0 -0
- package/assets/blue-archive/shiroko/shiroko_spr.atlas.txt +167 -0
- package/assets/blue-archive/shiroko/shiroko_spr.json +10702 -0
- package/assets/blue-archive/shiroko/shiroko_spr.png +0 -0
- package/assets/blue-archive/shiroko/shiroko_spr.skel +0 -0
- package/assets/blue-archive/shiroko/voice.mp3 +0 -0
- package/bin/arona.mjs +43 -0
- package/package.json +37 -0
- package/pet/agents.cjs +197 -0
- package/pet/main.cjs +462 -0
- package/pet/preload.cjs +21 -0
- package/pet/renderer/fx.html +30 -0
- package/pet/renderer/fx.js +41 -0
- package/pet/renderer/gallery.html +18 -0
- package/pet/renderer/gallery.js +31 -0
- package/pet/renderer/index.html +18 -0
- package/pet/renderer/renderer.js +382 -0
- package/pet/renderer/spine_layer.js +609 -0
- package/pet/renderer/spinetest.html +24 -0
- package/pet/renderer/spinetest.js +295 -0
- package/pet/renderer/style.css +63 -0
- package/pet/tools/gallery_capture.cjs +267 -0
- package/pet/tools/gen_sway.cjs +170 -0
- package/pet/tools/meshify_region.cjs +105 -0
- package/pet/tools/skel_inspect.cjs +112 -0
- package/pet/tools/skel_to_json.cjs +410 -0
- package/pet/tools/spine_node.cjs +69 -0
- package/pet/tools/visual_test.cjs +262 -0
- package/pet/vendor/spine/spine-canvas.js +8472 -0
- package/pet/vendor/spine/spine-webgl.js +10839 -0
- package/python/__pycache__/_i18n.cpython-313.pyc +0 -0
- package/python/__pycache__/_i18n.cpython-314.pyc +0 -0
- package/python/__pycache__/computer_use.cpython-314.pyc +0 -0
- package/python/__pycache__/hotkey.cpython-313.pyc +0 -0
- package/python/__pycache__/hotkey.cpython-314.pyc +0 -0
- package/python/__pycache__/stt.cpython-313.pyc +0 -0
- package/python/__pycache__/stt.cpython-314.pyc +0 -0
- package/python/__pycache__/tts.cpython-313.pyc +0 -0
- package/python/__pycache__/tts.cpython-314.pyc +0 -0
- package/python/__pycache__/tts_stream.cpython-313.pyc +0 -0
- package/python/__pycache__/tts_stream.cpython-314.pyc +0 -0
- package/python/__pycache__/voice_clone.cpython-314.pyc +0 -0
- package/python/_i18n.py +18 -0
- package/python/computer_use.py +190 -0
- package/python/hotkey.py +196 -0
- package/python/stt.py +240 -0
- package/python/tts_say.py +177 -0
- package/python/voice_clone.py +98 -0
- package/requirements.txt +6 -0
- package/src/agent.ts +791 -0
- package/src/agent_registry.ts +109 -0
- package/src/commands.ts +641 -0
- package/src/config.ts +245 -0
- package/src/in_memory_credentials.ts +61 -0
- package/src/index.ts +78 -0
- package/src/locale.ts +131 -0
- package/src/logo.ts +120 -0
- package/src/mcp.ts +192 -0
- package/src/memory.ts +303 -0
- package/src/pet.ts +346 -0
- package/src/renderer.ts +350 -0
- package/src/repl.ts +902 -0
- package/src/setup.ts +371 -0
- package/src/skills.ts +96 -0
- package/src/slash_menu.ts +371 -0
- package/src/slash_registry.ts +53 -0
- package/src/speaker_context.ts +41 -0
- package/src/text_split.ts +79 -0
- package/src/tools/computer_use.ts +142 -0
- package/src/tools/emotion_tool.ts +55 -0
- package/src/tools/keep_silent_tool.ts +26 -0
- package/src/tools/memory_tool.ts +24 -0
- package/src/tools/skill_tools.ts +100 -0
- package/src/tools/tavily_tools.ts +431 -0
- package/src/tools/voice_tools.ts +28 -0
- package/src/tts_stream.ts +260 -0
- package/src/tui_select.ts +146 -0
- package/src/undo.ts +451 -0
- package/src/utils/python.ts +211 -0
- package/src/utils/spawn.ts +56 -0
- package/src/voice.ts +83 -0
- package/src/voice_cli.ts +198 -0
- package/src/voices.ts +135 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// ARONA 桌宠渲染层:Spine 骨骼基底 + 情绪预设(track4 数字预设动画,瞬时切换)+ 手动拖动 + 摇动彩蛋(摸头)
|
|
2
|
+
// 角色参数化:当前 Agent 配置(agents.cjs,经 ?agent= + pet:get-agent-config)决定
|
|
3
|
+
// 情绪映射(emotions)、闭眼预设集合(closedEyePresets)、禁跟随集合(noGazePresets)。
|
|
4
|
+
// 情绪模型:track4 非循环数字预设"残留末帧"持有,脸+光环瞬时切换(游戏原生语义,无过渡);
|
|
5
|
+
// track0 始终 Idle_01 循环 → 情绪期间身体继续呼吸摇摆,只有脸+光环切换。
|
|
6
|
+
// 摸头(头部摇动 ≥2 次换向触发,轻微划出窗口也算——主进程光标轮询补采样):Pat_01_A + Pat_01_M
|
|
7
|
+
// 静态姿势对 + Head_Rot 跟随光标,窗口锁死不动。
|
|
8
|
+
// dizzy(按住鼠标任意区域大幅晃动触发,仅主 Agent 窗口):600ms 内单轴摆幅极差 ≥150px 且换向 ≥2 次
|
|
9
|
+
// → track4 dizzy 预设(Arona 30 / Plana 13,闭眼晕脸);与摸头按"摆幅量级"分档(小幅频繁=摸头)。
|
|
10
|
+
// 松手后保持 DIZZY_RECOVER_MS 再自动回待机;dizzy 期间窗口不锁死(桌宠被拎着晃,跟随鼠标拖动)。
|
|
11
|
+
// 摸头/dizzy 手势级互斥(interactionDone):同一次按住内任一触发过,另一项禁用直到松开。
|
|
12
|
+
// 空闲注视:瞳孔跟随光标(spine_layer.js 内部实现),头部不歪。
|
|
13
|
+
// 眨眼:Eye_Close_01 独立轨道(track5,预设之上),2~6s 均匀随机,15% 概率双眨;闭眼预设期间禁止。
|
|
14
|
+
const spineCanvas = document.getElementById("spine");
|
|
15
|
+
|
|
16
|
+
const IS_IDLE_DEBUG = new URLSearchParams(location.search).has("idledebug");
|
|
17
|
+
|
|
18
|
+
// ---- 情绪预设映射(单一事实源:agents.cjs 当前 Agent 的 emotions 值 = 预设动画名;closedEye 集合见下)----
|
|
19
|
+
// closedEye:该预设 key 了 L/R_Eye_Cover_01/02(闭眼)→ 情绪期间禁止眨眼
|
|
20
|
+
// (Eye_Close_01 播完末帧会把 cover 置 null,闭眼预设上眨眼会闪出"睁眼")。
|
|
21
|
+
// noGaze:睁眼预设默认保留瞳孔跟随;以下预设禁跟随(如 jealous 自带固定斜视,跟随会覆盖)。
|
|
22
|
+
let closedEyePresets = new Set();
|
|
23
|
+
let noGazePresets = new Set();
|
|
24
|
+
let EMOTION_PRESET = {};
|
|
25
|
+
|
|
26
|
+
// 当前稳定态情绪名("spine" 基底时为 null);同名重复路由跳过(幂等)
|
|
27
|
+
let currentEmotion = null;
|
|
28
|
+
|
|
29
|
+
// 角色是否禁用摇动摸头(Hoshino pat.type = "none");模块加载后由 init 读取配置决定
|
|
30
|
+
let patDisabled = false;
|
|
31
|
+
|
|
32
|
+
// 是否主 Agent 窗口(main.cjs get-agent-config.isMain):dizzy 大幅晃动仅主窗口响应
|
|
33
|
+
let isMainPet = false;
|
|
34
|
+
|
|
35
|
+
// 路由入口:摸头中收到新指令先结束摸头(crossfade 回 Idle),再正常路由。
|
|
36
|
+
// 瞬时切换语义:setAnimation 直接 snap(本运行时高 track attachment 恒覆盖低 track),无过渡。
|
|
37
|
+
function routeTo(target) {
|
|
38
|
+
if (window.SpineLayer.getState().patting) window.SpineLayer.endPat();
|
|
39
|
+
if (target === "spine") {
|
|
40
|
+
if (currentEmotion === null) return;
|
|
41
|
+
currentEmotion = null;
|
|
42
|
+
// ⚠️ 摘除必须走 setEmptyAnimation(触发 mix-out SETUP 重置,脸回 setup pose);
|
|
43
|
+
// clearTrack 会残留预设 attachment 不还原(见 spine_layer.clearEmotionPreset 注释)。
|
|
44
|
+
window.SpineLayer.clearEmotionPreset();
|
|
45
|
+
// 空闲注视恢复(决策点:情绪期间 gaze/眼球跟随维持关闭,与现状一致)
|
|
46
|
+
window.SpineLayer.setGaze(true);
|
|
47
|
+
} else {
|
|
48
|
+
if (currentEmotion === target) return;
|
|
49
|
+
const m = EMOTION_PRESET[target];
|
|
50
|
+
if (!m) return;
|
|
51
|
+
currentEmotion = target;
|
|
52
|
+
// 睁眼情绪保留瞳孔跟随,闭眼 / 禁跟随预设(jealous/angry/doubt)关闭
|
|
53
|
+
window.SpineLayer.setGaze(m.gaze);
|
|
54
|
+
window.SpineLayer.setEmotionPreset(m.anim, m.closedEye, m.gaze);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---- 眨眼调度(2~6s 均匀随机,15% 概率双眨;?idledebug 缩短为 1~2s 供视觉回归截获)----
|
|
59
|
+
let blinkTimer = null;
|
|
60
|
+
|
|
61
|
+
function scheduleBlink() {
|
|
62
|
+
const delay = IS_IDLE_DEBUG
|
|
63
|
+
? 1000 + Math.random() * 1000
|
|
64
|
+
: 2000 + Math.random() * 4000;
|
|
65
|
+
blinkTimer = setTimeout(() => {
|
|
66
|
+
tryBlink();
|
|
67
|
+
scheduleBlink();
|
|
68
|
+
}, delay);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function tryBlink() {
|
|
72
|
+
// 互斥判断全在 SpineLayer.blink() 内部(摸头中/闭眼情绪预设中返回 false 即跳过本次)
|
|
73
|
+
if (!window.SpineLayer.blink()) return;
|
|
74
|
+
if (Math.random() < 0.15) {
|
|
75
|
+
// 双眨:第一次(0.133s)结束后 150~250ms 再来一次
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
window.SpineLayer.blink();
|
|
78
|
+
}, 300 + Math.random() * 150);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function showEmotion(name) {
|
|
83
|
+
if (!EMOTION_PRESET[name]) return;
|
|
84
|
+
// 外部(LLM)情绪覆盖时取消 dizzy 的延迟恢复,防定时器把新情绪拉回 idle
|
|
85
|
+
if (name !== "dizzy") cancelDizzyRecover();
|
|
86
|
+
routeTo(name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resetToBase() {
|
|
90
|
+
cancelDizzyRecover();
|
|
91
|
+
routeTo("spine");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- 拖动与摇动检测 ----
|
|
95
|
+
// 不用 -webkit-app-region: drag(否则收不到鼠标事件,无法检测摇动)
|
|
96
|
+
const SHAKE_WINDOW_MS = 600; // 采样窗口
|
|
97
|
+
const SHAKE_TURNS = 2; // 水平换向次数阈值
|
|
98
|
+
const SHAKE_DISTANCE = 60; // 累计水平位移阈值(px)
|
|
99
|
+
const PAT_TURN_PX = 20; // 单次"有效换向"所需的最小半波位移(px)——单向拖动/微抖动不计数
|
|
100
|
+
// ---- dizzy(大幅晃动)检测:仅主 Agent 窗口 ----
|
|
101
|
+
// 与摸头按摆幅量级分档:采样窗口(同 SHAKE_WINDOW_MS)内光标单轴极差 ≥ DIZZY_AMPLITUDE
|
|
102
|
+
// 且该轴换向 ≥ DIZZY_TURNS → 判"大幅晃动";小幅频繁滑动(摸头)包络远小于此,不会误抢。
|
|
103
|
+
// 用窗口内坐标极差(max−min)而非逐帧半波检测:快速甩动采样稀疏时极值点仍稳定可采。
|
|
104
|
+
const DIZZY_TURNS = 2; // 换向次数阈值(一来一回)
|
|
105
|
+
const DIZZY_AMPLITUDE = 150; // 窗口内单轴摆幅极差阈值(px)——"大幅"档位,待实测调整
|
|
106
|
+
const DIZZY_RECOVER_MS = 2000; // 松手后保持晕脸的时长,之后自动回待机
|
|
107
|
+
// 头部区域:x 比例基于左侧 320px 渲染区(spineCanvas.clientWidth),y 基于窗口高。
|
|
108
|
+
// ⚠️ x 不能按整窗 innerWidth(580 = 320 渲染 + 260 气泡)算:0.26×580≈151px 比头部实际
|
|
109
|
+
// 左缘(渲染区内 ~110px)偏右,会把头部左边缘切掉——左侧长按拖动永不触发摸头(2026-08-20 实测)。
|
|
110
|
+
// Spine 姿势实测头部在 CSS y 119~180(旧视频为 y 6%-29%,保留旧值兼容)。
|
|
111
|
+
const HEAD_BOX = { xMin: 0.26, xMax: 0.78, yMin: 0.06, yMax: 0.29 };
|
|
112
|
+
// 摸头期间"离开头部"判定缓冲(px):用户反馈 16px 太严苛,先放宽 50px、再要求更宽 → 90px;
|
|
113
|
+
// 摸头时手在头部附近大幅游移不打断;离开很远(躯干以下/窗口外)才结束 enjoy
|
|
114
|
+
const HEAD_EXIT_MARGIN = 90;
|
|
115
|
+
|
|
116
|
+
let dragging = false;
|
|
117
|
+
let shaking = false; // 摸头态(摇动触发)
|
|
118
|
+
let onHead = false; // 本次手势是否起于头部区域(摇动彩蛋只在头上划动才触发)
|
|
119
|
+
let lastX = 0;
|
|
120
|
+
let lastY = 0;
|
|
121
|
+
let samples = []; // { t, x, y }(e.screenX/Y 全局坐标,不受窗口移动影响)
|
|
122
|
+
let dragLocked = false; // 本次手势摇动触发过摸头后锁死拖动(窗口停在原地、拖尾保留),直到松开
|
|
123
|
+
let dizzy = false; // dizzy 态(按住大幅晃动触发)
|
|
124
|
+
let dizzyTimer = null; // 松手后的延迟恢复定时器
|
|
125
|
+
// 手势级互斥:本次手势(mousedown→mouseup)内摸头/dizzy 任一触发过,另一项禁用直到松开。
|
|
126
|
+
// 堵"摸头触发→离开头部区退出→继续大幅晃又触发 dizzy"(及反向)的串扰。
|
|
127
|
+
let interactionDone = false;
|
|
128
|
+
|
|
129
|
+
// 采样入列 + 600ms 窗口裁剪(mousemove 与主进程光标轮询两路共用)
|
|
130
|
+
function pushSample(x, y) {
|
|
131
|
+
const now = performance.now();
|
|
132
|
+
samples.push({ t: now, x, y });
|
|
133
|
+
while (samples.length && now - samples[0].t > SHAKE_WINDOW_MS) samples.shift();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function enterPat() {
|
|
137
|
+
interactionDone = true; // 手势级互斥:本手势内 dizzy 不再触发
|
|
138
|
+
window.SpineLayer.startPat();
|
|
139
|
+
window.petAPI.shake();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function exitPat() {
|
|
143
|
+
window.SpineLayer.endPat();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- dizzy(大幅晃动):仅主 Agent 窗口;纯本地动画,不涉及 LLM / 子 Agent ----
|
|
147
|
+
// 窗口不锁死:dizzy 状态下仍走现有拖动路径(桌宠跟随鼠标被拎着晃),松手 dragEnd 落盘位置。
|
|
148
|
+
function enterDizzy() {
|
|
149
|
+
interactionDone = true; // 手势级互斥:本手势内摸头不再触发
|
|
150
|
+
dizzy = true;
|
|
151
|
+
window.petAPI.dizzy(); // 上报(与 pet:shake 对称,主进程仅日志)
|
|
152
|
+
routeTo("dizzy"); // 幂等;闭眼预设(Arona 30 / Plana 13)自动禁眨眼/关注视
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 松手时调用:延迟 DIZZY_RECOVER_MS 自动回待机(晕一会儿缓过来)
|
|
156
|
+
function exitDizzy() {
|
|
157
|
+
if (!dizzy) return;
|
|
158
|
+
dizzy = false;
|
|
159
|
+
dizzyTimer = setTimeout(() => {
|
|
160
|
+
dizzyTimer = null;
|
|
161
|
+
// 守卫:恢复瞬间若又开始摸头,不掐断摸头
|
|
162
|
+
if (!window.SpineLayer.getState().patting) resetToBase();
|
|
163
|
+
}, DIZZY_RECOVER_MS);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 外部情绪 / 重置覆盖时取消 dizzy 的延迟恢复(防"LLM 发了新情绪后被定时器拉回 idle")
|
|
167
|
+
function cancelDizzyRecover() {
|
|
168
|
+
dizzy = false;
|
|
169
|
+
if (dizzyTimer) {
|
|
170
|
+
clearTimeout(dizzyTimer);
|
|
171
|
+
dizzyTimer = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function onMouseDown(e) {
|
|
176
|
+
dragging = true;
|
|
177
|
+
shaking = false;
|
|
178
|
+
interactionDone = false; // 新手势重置互斥标志
|
|
179
|
+
dragLocked = false;
|
|
180
|
+
const h = window.innerHeight;
|
|
181
|
+
const sw = spineCanvas.clientWidth || 320; // 渲染区宽(x 判定基准,见 HEAD_BOX 注释)
|
|
182
|
+
onHead =
|
|
183
|
+
!patDisabled &&
|
|
184
|
+
e.clientX >= sw * HEAD_BOX.xMin && e.clientX <= sw * HEAD_BOX.xMax &&
|
|
185
|
+
e.clientY >= h * HEAD_BOX.yMin && e.clientY <= h * HEAD_BOX.yMax;
|
|
186
|
+
lastX = e.screenX;
|
|
187
|
+
lastY = e.screenY;
|
|
188
|
+
samples = [{ t: performance.now(), x: e.screenX, y: e.screenY }];
|
|
189
|
+
window.petAPI.fxDown(e.screenX, e.screenY);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function onMouseMove(e) {
|
|
193
|
+
if (!dragging) return;
|
|
194
|
+
window.petAPI.fxMove(e.screenX, e.screenY);
|
|
195
|
+
const dx = e.screenX - lastX;
|
|
196
|
+
const dy = e.screenY - lastY;
|
|
197
|
+
lastX = e.screenX;
|
|
198
|
+
lastY = e.screenY;
|
|
199
|
+
|
|
200
|
+
// 摇动检测:维护采样序列(mousemove;按住期间另有主进程光标轮询补采样,见 init 的 onCursor)
|
|
201
|
+
pushSample(e.screenX, e.screenY);
|
|
202
|
+
|
|
203
|
+
// ---- dizzy 检测(大幅晃动,仅主 Agent 窗口)----
|
|
204
|
+
// 按摆幅量级与摸头分档:真大幅优先判定(头部大幅晃 → dizzy,未达大幅的头部手势落回摸头检测)。
|
|
205
|
+
// x/y 两轴独立判定、任一满足即触发(斜着甩、上下大幅晃也算)。
|
|
206
|
+
if (!dizzy && !shaking && !interactionDone && isMainPet && samples.length >= 4) {
|
|
207
|
+
let minX = Infinity, maxX = -Infinity, turnsX = 0, prevSx = 0;
|
|
208
|
+
let minY = Infinity, maxY = -Infinity, turnsY = 0, prevSy = 0;
|
|
209
|
+
for (let i = 0; i < samples.length; i++) {
|
|
210
|
+
const s = samples[i];
|
|
211
|
+
if (s.x < minX) minX = s.x;
|
|
212
|
+
if (s.x > maxX) maxX = s.x;
|
|
213
|
+
if (s.y < minY) minY = s.y;
|
|
214
|
+
if (s.y > maxY) maxY = s.y;
|
|
215
|
+
if (i > 0) {
|
|
216
|
+
const dx = s.x - samples[i - 1].x;
|
|
217
|
+
const dy = s.y - samples[i - 1].y;
|
|
218
|
+
const sx = dx > 2 ? 1 : dx < -2 ? -1 : 0;
|
|
219
|
+
if (sx !== 0) {
|
|
220
|
+
if (prevSx !== 0 && sx !== prevSx) turnsX++;
|
|
221
|
+
prevSx = sx;
|
|
222
|
+
}
|
|
223
|
+
const sy = dy > 2 ? 1 : dy < -2 ? -1 : 0;
|
|
224
|
+
if (sy !== 0) {
|
|
225
|
+
if (prevSy !== 0 && sy !== prevSy) turnsY++;
|
|
226
|
+
prevSy = sy;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if ((maxX - minX >= DIZZY_AMPLITUDE && turnsX >= DIZZY_TURNS) ||
|
|
231
|
+
(maxY - minY >= DIZZY_AMPLITUDE && turnsY >= DIZZY_TURNS)) {
|
|
232
|
+
enterDizzy();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!shaking && !dizzy && !interactionDone && onHead && samples.length >= 4) {
|
|
238
|
+
let turns = 0;
|
|
239
|
+
let travel = 0;
|
|
240
|
+
let half = 0; // 当前方向半波累计位移
|
|
241
|
+
let prevSign = 0;
|
|
242
|
+
for (let i = 1; i < samples.length; i++) {
|
|
243
|
+
const d = samples[i].x - samples[i - 1].x;
|
|
244
|
+
travel += Math.abs(d);
|
|
245
|
+
half += Math.abs(d);
|
|
246
|
+
const sign = d > 2 ? 1 : d < -2 ? -1 : 0;
|
|
247
|
+
if (sign !== 0 && prevSign !== 0 && sign !== prevSign) {
|
|
248
|
+
// 换向:仅当刚走完的这半波 ≥ PAT_TURN_PX 才算一次"有效换向"。
|
|
249
|
+
// 单向拖动/手指微抖动的半波远小于此,不计入 turns → 不会误触发摸头。
|
|
250
|
+
if (half >= PAT_TURN_PX) turns++;
|
|
251
|
+
half = 0;
|
|
252
|
+
}
|
|
253
|
+
if (sign !== 0) prevSign = sign;
|
|
254
|
+
}
|
|
255
|
+
if (turns >= SHAKE_TURNS && travel > SHAKE_DISTANCE) {
|
|
256
|
+
// 判定摇动 → 进入摸头态:锁死本次手势(不再拖动),Spine Pat 动画 + 头部跟随
|
|
257
|
+
shaking = true;
|
|
258
|
+
dragLocked = true; // 本次手势触发过摸头 → 之后不再拖动窗口
|
|
259
|
+
enterPat();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 摸头中(仍长按):光标离开头部区域(任意方向)→ 结束摸头(窗口仍锁在原地,头部平滑回正)
|
|
265
|
+
if (shaking) {
|
|
266
|
+
const h = window.innerHeight;
|
|
267
|
+
const sw = spineCanvas.clientWidth || 320; // 渲染区宽(x 判定基准,见 HEAD_BOX 注释)
|
|
268
|
+
const inHead =
|
|
269
|
+
e.clientX >= sw * HEAD_BOX.xMin - HEAD_EXIT_MARGIN &&
|
|
270
|
+
e.clientX <= sw * HEAD_BOX.xMax + HEAD_EXIT_MARGIN &&
|
|
271
|
+
e.clientY >= h * HEAD_BOX.yMin - HEAD_EXIT_MARGIN &&
|
|
272
|
+
e.clientY <= h * HEAD_BOX.yMax + HEAD_EXIT_MARGIN;
|
|
273
|
+
if (!inHead) {
|
|
274
|
+
shaking = false;
|
|
275
|
+
onHead = false; // 本次手势不再触发摇动
|
|
276
|
+
exitPat();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!shaking && !dragLocked && (dx !== 0 || dy !== 0)) {
|
|
281
|
+
window.petAPI.drag(dx, dy);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function onMouseUp() {
|
|
286
|
+
if (!dragging) return;
|
|
287
|
+
window.petAPI.fxUp();
|
|
288
|
+
dragging = false;
|
|
289
|
+
samples = [];
|
|
290
|
+
if (shaking) {
|
|
291
|
+
// 松手结束摸头:crossfade 回 Idle,头部随平滑系数回正
|
|
292
|
+
shaking = false;
|
|
293
|
+
exitPat();
|
|
294
|
+
} else {
|
|
295
|
+
window.petAPI.dragEnd();
|
|
296
|
+
}
|
|
297
|
+
if (dizzy) {
|
|
298
|
+
// 松手结束 dizzy:延迟 DIZZY_RECOVER_MS 自动回待机(晕一会儿缓过来)
|
|
299
|
+
exitDizzy();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---- 文字气泡 ----
|
|
304
|
+
const petBubble = document.getElementById("pet-bubble");
|
|
305
|
+
let bubbleTimer = null;
|
|
306
|
+
|
|
307
|
+
function showBubble(text) {
|
|
308
|
+
if (!petBubble) return;
|
|
309
|
+
petBubble.textContent = text;
|
|
310
|
+
petBubble.classList.remove("hidden");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function hideBubble() {
|
|
314
|
+
if (!petBubble) return;
|
|
315
|
+
petBubble.classList.add("hidden");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function init() {
|
|
319
|
+
// 情绪预设映射:agents.cjs(与 main 进程白名单同源)值 = 预设动画名;
|
|
320
|
+
// closedEye/noGaze 集合同样来自当前 Agent 配置
|
|
321
|
+
const cfg = await window.petAPI.getAgentConfig();
|
|
322
|
+
isMainPet = cfg.isMain === true;
|
|
323
|
+
patDisabled = cfg.pat?.type === "none";
|
|
324
|
+
closedEyePresets = new Set(cfg.closedEyePresets || []);
|
|
325
|
+
noGazePresets = new Set(cfg.noGazePresets || []);
|
|
326
|
+
for (const [name, anim] of Object.entries(cfg.emotions || {})) {
|
|
327
|
+
EMOTION_PRESET[name] = {
|
|
328
|
+
anim,
|
|
329
|
+
closedEye: closedEyePresets.has(anim),
|
|
330
|
+
gaze: !closedEyePresets.has(anim) && !noGazePresets.has(anim),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
// Spine 基底(加载失败时抛出——spine 资源是硬依赖,渲染层无降级路径;
|
|
336
|
+
// WebGL 不可用时 spine_layer 内部已自动降级 Canvas 2D,此处仅兜底输出错误)
|
|
337
|
+
await window.SpineLayer.init(spineCanvas);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
// 错误经 main.cjs console-message 转发 → 终端可见([pet:render]),不静默
|
|
340
|
+
console.error("[pet:render] SpineLayer.init 失败:", err && err.message ? err.message : err);
|
|
341
|
+
return; // 不挂交互事件,保持窗口透明
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 光标 → Spine(摸头头部跟随 / 空闲注视共用)
|
|
345
|
+
// 附带的全局坐标(gx/gy)在按住期间补晃动检测采样:光标快速甩动划出窗口时
|
|
346
|
+
// mousemove 断流(换向/位移凑不齐 → 摸头"没反应"),主进程 16ms 轮询不断流,
|
|
347
|
+
// 轻微出窗的晃动也能采到 → 轻微划出窗口仍视为摸头手势的一部分。
|
|
348
|
+
window.petAPI.onCursor((x, y, gx, gy) => {
|
|
349
|
+
window.SpineLayer.setCursor(x, y);
|
|
350
|
+
if (dragging) pushSample(gx, gy);
|
|
351
|
+
});
|
|
352
|
+
// 基底稳定态:开启空闲注视 + 眨眼调度
|
|
353
|
+
window.SpineLayer.setGaze(true);
|
|
354
|
+
|
|
355
|
+
window.petAPI.onEmotion(showEmotion);
|
|
356
|
+
window.petAPI.onReset(resetToBase);
|
|
357
|
+
|
|
358
|
+
// 文字气泡:只显示 mid/final 短消息;等到 TTS 播放结束(tts_end)再消失
|
|
359
|
+
window.petAPI.onText(({ kind, data }) => {
|
|
360
|
+
if (!petBubble) return;
|
|
361
|
+
if (kind === "tts_end") {
|
|
362
|
+
if (bubbleTimer) clearTimeout(bubbleTimer);
|
|
363
|
+
bubbleTimer = null;
|
|
364
|
+
hideBubble();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// mid / final
|
|
368
|
+
if (typeof data === "string" && data.length > 0) {
|
|
369
|
+
if (bubbleTimer) clearTimeout(bubbleTimer);
|
|
370
|
+
bubbleTimer = null;
|
|
371
|
+
showBubble(data);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
document.addEventListener("mousedown", onMouseDown);
|
|
376
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
377
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
378
|
+
|
|
379
|
+
scheduleBlink();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
init();
|