@pheem49/mint 1.5.0 → 1.5.2

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 (101) hide show
  1. package/README.md +35 -1
  2. package/main.js +28 -14
  3. package/mint-cli-logic.js +3 -119
  4. package/mint-cli.js +201 -500
  5. package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  6. package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +40 -0
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json +15 -0
  9. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
  10. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
  11. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
  12. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +15 -0
  13. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
  14. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
  15. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
  16. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
  17. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
  18. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
  19. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
  20. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
  21. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
  22. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
  23. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
  24. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +23 -0
  25. package/package.json +40 -17
  26. package/src/AI_Brain/Gemini_API.js +147 -46
  27. package/src/AI_Brain/autonomous_brain.js +2 -1
  28. package/src/AI_Brain/memory_store.js +299 -3
  29. package/src/AI_Brain/proactive_engine.js +12 -2
  30. package/src/Automation_Layer/browser_automation.js +26 -24
  31. package/src/CLI/approval_handler.js +42 -0
  32. package/src/CLI/chat_router.js +18 -6
  33. package/src/CLI/chat_ui.js +583 -52
  34. package/src/CLI/cli_colors.js +32 -0
  35. package/src/CLI/cli_formatters.js +89 -0
  36. package/src/CLI/code_agent.js +369 -71
  37. package/src/CLI/image_input.js +90 -0
  38. package/src/CLI/intent_detectors.js +181 -0
  39. package/src/CLI/interactive_chat.js +479 -0
  40. package/src/CLI/list_features.js +3 -0
  41. package/src/CLI/onboarding.js +72 -15
  42. package/src/CLI/repo_summarizer.js +282 -0
  43. package/src/CLI/semantic_code_search.js +312 -0
  44. package/src/CLI/skill_manager.js +41 -0
  45. package/src/CLI/slash_command_handler.js +418 -0
  46. package/src/CLI/symbol_indexer.js +231 -0
  47. package/src/CLI/updater.js +6 -4
  48. package/src/Channels/discord_bridge.js +11 -13
  49. package/src/Channels/line_bridge.js +10 -10
  50. package/src/Channels/slack_bridge.js +7 -12
  51. package/src/Channels/telegram_bridge.js +6 -14
  52. package/src/Channels/whatsapp_bridge.js +11 -9
  53. package/src/System/action_executor.js +59 -10
  54. package/src/System/chat_history_manager.js +20 -12
  55. package/src/System/config_manager.js +31 -1
  56. package/src/System/granular_automation.js +122 -53
  57. package/src/System/optional_require.js +23 -0
  58. package/src/System/proactive_loop.js +19 -3
  59. package/src/System/safety_manager.js +108 -0
  60. package/src/System/sandbox_runner.js +182 -0
  61. package/src/System/system_automation.js +127 -81
  62. package/src/System/system_info.js +70 -0
  63. package/src/System/tool_registry.js +280 -0
  64. package/src/System/window_manager.js +4 -2
  65. package/src/UI/live2d_manager.js +566 -0
  66. package/src/UI/renderer.js +339 -21
  67. package/src/UI/settings.css +655 -420
  68. package/src/UI/settings.html +478 -432
  69. package/src/UI/settings.js +10 -8
  70. package/src/UI/styles.css +516 -31
  71. package/.codex +0 -0
  72. package/docs/assets/Agent_Mint.png +0 -0
  73. package/docs/assets/CLI_Screen.png +0 -0
  74. package/docs/assets/Settings.png +0 -0
  75. package/docs/assets/icon.png +0 -0
  76. package/docs/guide.html +0 -632
  77. package/docs/index.html +0 -133
  78. package/docs/style.css +0 -579
  79. package/index.html +0 -16
  80. package/src/UI/index.html +0 -126
  81. package/tech_news.txt +0 -3
  82. package/test_knowledge.txt +0 -3
  83. package/tests/action_executor_safety.test.js +0 -67
  84. package/tests/agent_orchestrator.test.js +0 -41
  85. package/tests/chat_router.test.js +0 -42
  86. package/tests/code_agent.test.js +0 -69
  87. package/tests/config_manager.test.js +0 -141
  88. package/tests/docker.test.js +0 -46
  89. package/tests/file_operations.test.js +0 -57
  90. package/tests/gmail.test.js +0 -135
  91. package/tests/gmail_auth.test.js +0 -129
  92. package/tests/google_calendar.test.js +0 -113
  93. package/tests/google_tts_urls.test.js +0 -24
  94. package/tests/memory_store.test.js +0 -185
  95. package/tests/notion.test.js +0 -121
  96. package/tests/provider_routing.test.js +0 -83
  97. package/tests/safety_manager.test.js +0 -40
  98. package/tests/spotify.test.js +0 -201
  99. package/tests/system_monitor.test.js +0 -37
  100. package/tests/updater.test.js +0 -32
  101. package/tests/workspace_manager.test.js +0 -56
@@ -0,0 +1,566 @@
1
+ /**
2
+ * Live2DManager - Encapsulates Live2D model loading, fitting, and lip-sync logic.
3
+ */
4
+ window.Live2DManager = {
5
+ app: null,
6
+ model: null,
7
+ resizeObserver: null,
8
+ lipSyncInterval: null,
9
+ expIndex: 0,
10
+ interactionEnabled: true,
11
+ interactionStorageKey: 'mint-model-interaction-enabled',
12
+ accessoryStorageKey: 'mint-live2d-accessories',
13
+ activeAccessories: {},
14
+ accessoryOrder: ['glasses', 'pen', 'cat'],
15
+ accessoryParams: {
16
+ glasses: { paramId: 'Param96', label: 'Glasses' },
17
+ pen: { paramId: 'Param68', label: 'Pen' },
18
+ cat: { paramId: 'Param54', label: 'Cat Filter' }
19
+ },
20
+ pointerTrackingEnabled: true,
21
+ pointerTrackingFrame: null,
22
+ pointerTracking: {
23
+ targetX: 0,
24
+ targetY: 0,
25
+ currentX: 0,
26
+ currentY: 0,
27
+ lastMoveAt: 0
28
+ },
29
+ pointerTrackingConfig: {
30
+ focusX: 0.35,
31
+ focusY: 0.35,
32
+ rangeX: 0.35,
33
+ rangeY: 0.35,
34
+ smoothing: 0.18
35
+ },
36
+ baseModelPosition: null,
37
+ lastInteractionAt: 0,
38
+ expressionToastTimeout: null,
39
+ expressionParamIds: [
40
+ 'Param54',
41
+ 'Param55',
42
+ 'Param68',
43
+ 'Param76',
44
+ 'Param91',
45
+ 'Param93',
46
+ 'Param94',
47
+ 'Param96',
48
+ 'ParamAngleY',
49
+ 'ParamAngleZ',
50
+ 'ParamEyeBallX',
51
+ 'ParamEyeBallY',
52
+ 'ParamMouthForm',
53
+ 'ParamMouthOpenY'
54
+ ],
55
+ expressionNames: [
56
+ { id: null, label: 'Normal' },
57
+ { id: 'Apron', label: 'Apron' },
58
+ { id: 'Dazed', label: 'Dazed' },
59
+ { id: 'Photo', label: 'Photo' },
60
+ { id: 'Glasses', label: 'Glasses' },
61
+ { id: 'Pen', label: 'Writing' },
62
+ { id: 'Click', label: 'Blush' },
63
+ { id: 'CatFilter', label: 'Cat Ears' },
64
+ { id: 'DazedEyes', label: 'Dazed Eyes' }
65
+ ],
66
+
67
+ async loadModel(mountEl, statusEl, shellEl) {
68
+ this.statusEl = statusEl; // Store for later use
69
+ this.interactionEnabled = this.getSavedInteractionEnabled();
70
+ this.activeAccessories = this.getSavedAccessories();
71
+ if (!mountEl) return;
72
+ if (statusEl) {
73
+ statusEl.classList.remove('is-error');
74
+ statusEl.innerHTML = `
75
+ <div class="loader-dots">
76
+ <span></span><span></span><span></span>
77
+ </div>
78
+ <div style="font-size: 0.7rem; opacity: 0.8; letter-spacing: 0.05em;">SYNCHRONIZING MINT...</div>
79
+ `;
80
+ }
81
+ if (!window.PIXI || !window.PIXI.live2d) {
82
+ const message = 'Live2D runtime is not available.';
83
+ console.error(message);
84
+ if (statusEl) {
85
+ statusEl.classList.add('is-error');
86
+ statusEl.textContent = message;
87
+ }
88
+ return;
89
+ }
90
+
91
+ try {
92
+ window.PIXI.live2d.Live2DModel.registerTicker(window.PIXI.Ticker);
93
+
94
+ this.app = new window.PIXI.Application({
95
+ autoDensity: true,
96
+ antialias: true,
97
+ backgroundAlpha: 0,
98
+ resizeTo: mountEl,
99
+ resolution: window.devicePixelRatio || 1
100
+ });
101
+
102
+ mountEl.prepend(this.app.view);
103
+
104
+ const modelUrl = new URL('../../models/Shiroko_Model/Shiroko/Shiroko_Core/%E9%9D%A2%E9%A5%BC0.model3.json', window.location.href).href;
105
+ this.model = await window.PIXI.live2d.Live2DModel.from(modelUrl, {
106
+ autoInteract: false
107
+ });
108
+ this.expressionToastEl = document.getElementById('expression-toast');
109
+
110
+ this.model.anchor.set(0.5, 0.5);
111
+ this.app.stage.addChild(this.model);
112
+
113
+ // -- Interaction Setup --
114
+ this.setInteractionEnabled(this.interactionEnabled);
115
+ this.setupPointerTracking(mountEl);
116
+ this.applyAccessories();
117
+
118
+ // Tap Interaction. This model does not define Cubism HitAreas, so use
119
+ // normalized model coordinates to provide stable region reactions.
120
+ this.model.on('pointertap', (e) => this.handleModelTap(e));
121
+ this.model.on('hit', (hitAreaNames) => {
122
+ console.log(`[Live2D] Runtime hit detected: ${hitAreaNames}`);
123
+ });
124
+
125
+ const fitModel = () => {
126
+ if (!this.model || !mountEl) return;
127
+ const mountWidth = mountEl.clientWidth || 460;
128
+ const mountHeight = mountEl.clientHeight || 620;
129
+ this.app.renderer.resize(mountWidth, mountHeight);
130
+
131
+ const internal = this.model.internalModel || {};
132
+ const modelWidth = internal.width || internal.originalWidth || this.model.width || 1;
133
+ const modelHeight = internal.height || internal.originalHeight || this.model.height || 1;
134
+ const widthScale = mountWidth / Math.max(modelWidth, 1);
135
+ const heightScale = mountHeight / Math.max(modelHeight, 1);
136
+
137
+ // Reduced zoom to 2.0 as requested
138
+ const scale = Math.min(widthScale, heightScale) * 1.85;
139
+
140
+ this.model.scale.set(scale);
141
+ // Adjusted Y offset to 1.0 as requested
142
+ this.baseModelPosition = {
143
+ x: mountWidth / 2,
144
+ y: mountHeight / 2 + mountHeight * 0.55
145
+ };
146
+ this.applyModelFollowOffset();
147
+ };
148
+
149
+ requestAnimationFrame(() => {
150
+ fitModel();
151
+ requestAnimationFrame(fitModel);
152
+ });
153
+ this.resizeObserver = new ResizeObserver(fitModel);
154
+ this.resizeObserver.observe(mountEl);
155
+
156
+ shellEl?.classList.add('is-live2d-ready');
157
+ if (statusEl) statusEl.textContent = '';
158
+ this.model.motion('Idle', 0).catch(() => {});
159
+ } catch (error) {
160
+ console.error('Failed to load Live2D model:', error);
161
+ shellEl?.classList.remove('is-live2d-ready');
162
+ if (statusEl) {
163
+ statusEl.classList.add('is-error');
164
+ statusEl.textContent = `Live2D failed: ${error && error.message ? error.message : String(error)}`;
165
+ }
166
+ }
167
+ },
168
+
169
+ showStatus(text, duration = 2000) {
170
+ if (!this.statusEl) return;
171
+ this.statusEl.textContent = text;
172
+ this.statusEl.style.opacity = '1';
173
+
174
+ if (this.statusTimeout) clearTimeout(this.statusTimeout);
175
+ this.statusTimeout = setTimeout(() => {
176
+ this.statusEl.style.opacity = '0';
177
+ setTimeout(() => {
178
+ if (this.statusEl.style.opacity === '0') this.statusEl.textContent = '';
179
+ }, 500);
180
+ }, duration);
181
+ },
182
+
183
+ handleModelTap(event) {
184
+ if (!this.model || !this.interactionEnabled) return;
185
+
186
+ const now = Date.now();
187
+ if (now - this.lastInteractionAt < 3000) return;
188
+
189
+ const region = this.getInteractionRegion(event);
190
+ if (!region) return;
191
+ this.lastInteractionAt = now;
192
+ const expressionId = region.expression || 'Click';
193
+
194
+ console.log(`[Live2D] Interaction: ${region.id}`, region);
195
+ this.applyExpression(expressionId);
196
+ this.showStatus(region.label, 2500);
197
+
198
+ window.dispatchEvent(new CustomEvent('live2d-model-interaction', {
199
+ detail: {
200
+ region: region.id,
201
+ label: region.label,
202
+ prompt: region.prompt
203
+ }
204
+ }));
205
+
206
+ setTimeout(() => {
207
+ const currentIdx = this.expIndex === 0 ? 0 : this.expIndex;
208
+ const prevExp = this.expressionNames[currentIdx]?.id;
209
+ this.applyExpression(prevExp);
210
+ }, 2000);
211
+ },
212
+
213
+ getInteractionRegion(event) {
214
+ try {
215
+ const point = this.getPointerViewportPoint(event);
216
+ if (!point) return null;
217
+ const { x, y } = point;
218
+
219
+ if (this.isPointInZone(x, y, 0.38, 0.40, 0.24, 0.115)) {
220
+ return {
221
+ id: 'face',
222
+ label: 'Cat Ears',
223
+ expression: 'CatFilter',
224
+ prompt: 'The user poked Mint model on the cheek. Reply briefly, shyly or with a light tease. Use the same language as the user’s recent conversation; do not switch to Thai unless the user has been speaking Thai.'
225
+ };
226
+ }
227
+
228
+ if (this.isPointInZone(x, y, 0.34, 0.255, 0.32, 0.15)) {
229
+ return {
230
+ id: 'head',
231
+ label: 'Head Pat',
232
+ expression: 'Dazed',
233
+ prompt: 'The user patted Mint model on the head. Reply briefly in a cute, slightly shy way. Use the same language as the user’s recent conversation; do not switch to Thai unless the user has been speaking Thai.'
234
+ };
235
+ }
236
+
237
+ const isLeftHand = this.isPointInZone(x, y, 0.22, 0.68, 0.20, 0.16);
238
+ const isRightHand = this.isPointInZone(x, y, 0.61, 0.68, 0.19, 0.16);
239
+ if (isLeftHand || isRightHand) {
240
+ return {
241
+ id: isLeftHand ? 'left-hand' : 'right-hand',
242
+ label: 'Hand Tap',
243
+ expression: 'Pen',
244
+ prompt: 'The user tapped Mint model’s hand. Reply briefly as if ready to help or take a request. Use the same language as the user’s recent conversation; do not switch to Thai unless the user has been speaking Thai.'
245
+ };
246
+ }
247
+
248
+ if (this.isPointInZone(x, y, 0.38, 0.77, 0.30, 0.23)) {
249
+ return {
250
+ id: 'lower-body',
251
+ label: 'Careful',
252
+ expression: 'Photo',
253
+ prompt: 'The user touched the lower body area of Mint model. Reply briefly in a shy, playful way, similar to “hehe~ what are you playing at, that makes me blush,” then gently invite the user back to chatting or work. Use the same language as the user’s recent conversation; do not switch to Thai unless the user has been speaking Thai.'
254
+ };
255
+ }
256
+
257
+ if (this.isPointInZone(x, y, 0.37, 0.555, 0.29, 0.14)) {
258
+ return {
259
+ id: 'body',
260
+ label: 'Shoulder Tap',
261
+ expression: 'Click',
262
+ prompt: 'The user tapped Mint model’s body or shoulder. Reply briefly as if turning toward the user and asking what they need help with. Use the same language as the user’s recent conversation; do not switch to Thai unless the user has been speaking Thai.'
263
+ };
264
+ }
265
+
266
+ return null;
267
+ } catch (error) {
268
+ console.error('[Live2D] Failed to resolve interaction region:', error);
269
+ return null;
270
+ }
271
+ },
272
+
273
+ getPointerViewportPoint(event) {
274
+ const originalEvent = event?.data?.originalEvent;
275
+ const rect = this.app?.view?.getBoundingClientRect?.();
276
+ if (originalEvent && rect) {
277
+ return {
278
+ x: (originalEvent.clientX - rect.left) / Math.max(rect.width, 1),
279
+ y: (originalEvent.clientY - rect.top) / Math.max(rect.height, 1)
280
+ };
281
+ }
282
+
283
+ const globalPoint = event?.data?.global;
284
+ const screen = this.app?.screen;
285
+ if (!globalPoint || !screen) return null;
286
+ return {
287
+ x: globalPoint.x / Math.max(screen.width, 1),
288
+ y: globalPoint.y / Math.max(screen.height, 1)
289
+ };
290
+ },
291
+
292
+ isPointInZone(x, y, left, top, width, height) {
293
+ return x >= left && x <= left + width && y >= top && y <= top + height;
294
+ },
295
+
296
+ cycleExpression() {
297
+ if (!this.model) return;
298
+ this.expIndex = (this.expIndex + 1) % this.expressionNames.length;
299
+ const nextExp = this.expressionNames[this.expIndex];
300
+
301
+ console.log(`[Live2D] Triggering expression: ${nextExp.id} (${nextExp.label})`);
302
+ this.applyExpression(nextExp.id);
303
+
304
+ this.showStatus(nextExp.label);
305
+ this.showExpressionToast(`Expression: ${nextExp.label}`);
306
+ },
307
+
308
+ setInteractionEnabled(isEnabled, persist = false) {
309
+ this.interactionEnabled = Boolean(isEnabled);
310
+ if (persist) {
311
+ this.saveInteractionEnabled(this.interactionEnabled);
312
+ }
313
+ if (!this.model) return;
314
+
315
+ this.model.interactive = this.interactionEnabled;
316
+ this.model.buttonMode = this.interactionEnabled;
317
+ },
318
+
319
+ getSavedInteractionEnabled() {
320
+ try {
321
+ return localStorage.getItem(this.interactionStorageKey) !== 'false';
322
+ } catch (_) {
323
+ return true;
324
+ }
325
+ },
326
+
327
+ saveInteractionEnabled(isEnabled) {
328
+ try {
329
+ localStorage.setItem(this.interactionStorageKey, String(Boolean(isEnabled)));
330
+ } catch (_) {}
331
+ },
332
+
333
+ setupPointerTracking(mountEl) {
334
+ if (!mountEl) return;
335
+
336
+ window.addEventListener('mousemove', (event) => this.updatePointerTrackingTarget(event, mountEl));
337
+
338
+ if (!this.pointerTrackingFrame) {
339
+ this.pointerTrackingFrame = () => this.updatePointerTracking();
340
+ this.app?.ticker?.add(this.pointerTrackingFrame);
341
+ }
342
+ },
343
+
344
+ updatePointerTrackingTarget(event, mountEl) {
345
+ if (!this.pointerTrackingEnabled || !mountEl) return;
346
+
347
+ const rect = {
348
+ left: 0,
349
+ top: 0,
350
+ width: window.innerWidth || mountEl.getBoundingClientRect().width,
351
+ height: window.innerHeight || mountEl.getBoundingClientRect().height
352
+ };
353
+ const config = this.pointerTrackingConfig;
354
+ const centerX = rect.left + rect.width * config.focusX;
355
+ const centerY = rect.top + rect.height * config.focusY;
356
+ const rangeX = Math.max(rect.width * config.rangeX, 1);
357
+ const rangeY = Math.max(rect.height * config.rangeY, 1);
358
+
359
+ this.pointerTracking.targetX = this.clamp((event.clientX - centerX) / rangeX, -1, 1);
360
+ this.pointerTracking.targetY = this.clamp((event.clientY - centerY) / rangeY, -1, 1);
361
+ this.pointerTracking.lastMoveAt = performance.now();
362
+ },
363
+
364
+ resetPointerTrackingTarget() {
365
+ this.pointerTracking.targetX = 0;
366
+ this.pointerTracking.targetY = 0;
367
+ },
368
+
369
+ updatePointerTracking() {
370
+ if (!this.model || !this.pointerTrackingEnabled) return;
371
+
372
+ const tracking = this.pointerTracking;
373
+ const smoothing = this.pointerTrackingConfig.smoothing;
374
+ tracking.currentX += (tracking.targetX - tracking.currentX) * smoothing;
375
+ tracking.currentY += (tracking.targetY - tracking.currentY) * smoothing;
376
+
377
+ const x = tracking.currentX;
378
+ const y = tracking.currentY;
379
+ const core = this.model?.internalModel?.coreModel;
380
+ if (!core) return;
381
+
382
+ this.setLive2DParam(core, 'ParamAngleX', x * 18);
383
+ this.setLive2DParam(core, 'ParamAngleY', -y * 14);
384
+ this.setLive2DParam(core, 'ParamAngleZ', -x * 5);
385
+ this.setLive2DParam(core, 'ParamEyeBallX', x * 1.45);
386
+ this.setLive2DParam(core, 'ParamEyeBallY', -y * 1.35);
387
+ this.setLive2DParam(core, 'Param49', x * 7);
388
+ this.setLive2DParam(core, 'Param51', -y * 5);
389
+ this.setLive2DParam(core, 'Param50', -x * 3);
390
+ this.applyModelFollowOffset();
391
+ },
392
+
393
+ applyModelFollowOffset() {
394
+ if (!this.model || !this.baseModelPosition) return;
395
+
396
+ const x = this.pointerTracking.currentX || 0;
397
+ const y = this.pointerTracking.currentY || 0;
398
+ this.model.position.set(
399
+ this.baseModelPosition.x + x * 22,
400
+ this.baseModelPosition.y + y * 16
401
+ );
402
+ },
403
+
404
+ setLive2DParam(core, id, value) {
405
+ try {
406
+ core.setParameterValueById(id, value);
407
+ } catch (_) {}
408
+ },
409
+
410
+ clamp(value, min, max) {
411
+ return Math.max(min, Math.min(max, value));
412
+ },
413
+
414
+ showExpressionToast(text, duration = 1600) {
415
+ const toast = this.expressionToastEl || document.getElementById('expression-toast');
416
+ if (!toast) return;
417
+
418
+ this.expressionToastEl = toast;
419
+ toast.textContent = text;
420
+ toast.classList.add('is-visible');
421
+
422
+ if (this.expressionToastTimeout) clearTimeout(this.expressionToastTimeout);
423
+ this.expressionToastTimeout = setTimeout(() => {
424
+ toast.classList.remove('is-visible');
425
+ }, duration);
426
+ },
427
+
428
+ applyExpression(expressionId) {
429
+ if (!this.model) return;
430
+
431
+ this.resetExpressionParams();
432
+ if (!expressionId) {
433
+ this.clearExpressionState();
434
+ return;
435
+ }
436
+
437
+ try {
438
+ this.model.expression(expressionId);
439
+ requestAnimationFrame(() => this.applyAccessories());
440
+ } catch (error) {
441
+ console.error(`[Live2D] Failed to apply expression: ${expressionId}`, error);
442
+ }
443
+ },
444
+
445
+ resetExpressionParams() {
446
+ const core = this.model?.internalModel?.coreModel;
447
+ if (!core) return;
448
+
449
+ this.expressionParamIds.forEach(id => {
450
+ try { core.setParameterValueById(id, 0); } catch (_) {}
451
+ });
452
+ },
453
+
454
+ clearExpressionState() {
455
+ const expressionManager =
456
+ this.model?.internalModel?.motionManager?.expressionManager ||
457
+ this.model?.internalModel?.expressionManager;
458
+
459
+ try {
460
+ if (expressionManager?.defaultExpression) {
461
+ expressionManager.currentExpression = expressionManager.defaultExpression;
462
+ expressionManager.reserveExpressionIndex = -1;
463
+ expressionManager.resetExpression();
464
+ } else {
465
+ this.model.expression(null);
466
+ }
467
+ } catch (error) {
468
+ console.error('[Live2D] Failed to clear expression state:', error);
469
+ }
470
+
471
+ this.resetExpressionParams();
472
+ requestAnimationFrame(() => {
473
+ this.resetExpressionParams();
474
+ this.applyAccessories();
475
+ });
476
+ },
477
+
478
+ setAccessory(accessoryId, isEnabled, persist = false) {
479
+ if (!this.accessoryParams[accessoryId]) return;
480
+
481
+ this.activeAccessories[accessoryId] = Boolean(isEnabled);
482
+ if (persist) {
483
+ this.saveAccessories();
484
+ }
485
+ this.applyAccessory(accessoryId);
486
+ },
487
+
488
+ setExclusiveAccessory(accessoryId, persist = false) {
489
+ const nextAccessoryId = this.accessoryParams[accessoryId] ? accessoryId : null;
490
+ Object.keys(this.accessoryParams).forEach(id => {
491
+ this.activeAccessories[id] = id === nextAccessoryId;
492
+ });
493
+ if (persist) {
494
+ this.saveAccessories();
495
+ }
496
+ this.applyAccessories();
497
+ return nextAccessoryId;
498
+ },
499
+
500
+ getActiveAccessoryId() {
501
+ return this.accessoryOrder.find(id => this.activeAccessories[id]) || null;
502
+ },
503
+
504
+ applyAccessories() {
505
+ Object.keys(this.accessoryParams).forEach(accessoryId => {
506
+ this.applyAccessory(accessoryId);
507
+ });
508
+ },
509
+
510
+ applyAccessory(accessoryId) {
511
+ const accessory = this.accessoryParams[accessoryId];
512
+ const core = this.model?.internalModel?.coreModel;
513
+ if (!accessory || !core) return;
514
+
515
+ this.setLive2DParam(core, accessory.paramId, this.activeAccessories[accessoryId] ? 1 : 0);
516
+ },
517
+
518
+ getSavedAccessories() {
519
+ try {
520
+ return JSON.parse(localStorage.getItem(this.accessoryStorageKey) || '{}') || {};
521
+ } catch (_) {
522
+ return {};
523
+ }
524
+ },
525
+
526
+ saveAccessories() {
527
+ try {
528
+ localStorage.setItem(this.accessoryStorageKey, JSON.stringify(this.activeAccessories));
529
+ } catch (_) {}
530
+ },
531
+
532
+ startLipSync() {
533
+ if (!this.model || this.lipSyncInterval) return;
534
+
535
+ this.model.motion('Speak', 0).catch(() => {});
536
+
537
+ this.lipSyncInterval = setInterval(() => {
538
+ if (!this.model) return;
539
+ const value = Math.random() * 0.8;
540
+ if (this.model.internalModel && this.model.internalModel.coreModel) {
541
+ const core = this.model.internalModel.coreModel;
542
+ const mouthIds = ['ParamMouthOpenY', 'ParamMouthOpen', 'PARAM_MOUTH_OPEN_Y'];
543
+ mouthIds.forEach(id => {
544
+ try { core.setParameterValueById(id, value); } catch(e) {}
545
+ });
546
+ }
547
+ }, 80);
548
+ },
549
+
550
+ stopLipSync() {
551
+ if (this.lipSyncInterval) {
552
+ clearInterval(this.lipSyncInterval);
553
+ this.lipSyncInterval = null;
554
+ }
555
+ if (this.model) {
556
+ if (this.model.internalModel && this.model.internalModel.coreModel) {
557
+ const core = this.model.internalModel.coreModel;
558
+ const mouthIds = ['ParamMouthOpenY', 'ParamMouthOpen', 'PARAM_MOUTH_OPEN_Y'];
559
+ mouthIds.forEach(id => {
560
+ try { core.setParameterValueById(id, 0); } catch(e) {}
561
+ });
562
+ }
563
+ this.model.motion('Idle', 0).catch(() => {});
564
+ }
565
+ }
566
+ };