@pheem49/mint 1.5.1 → 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.
- package/README.md +8 -0
- package/mint-cli.js +148 -921
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
- package/package.json +18 -20
- package/src/AI_Brain/proactive_engine.js +12 -2
- package/src/Automation_Layer/browser_automation.js +26 -24
- package/src/CLI/approval_handler.js +42 -0
- package/src/CLI/chat_ui.js +192 -7
- package/src/CLI/cli_colors.js +32 -0
- package/src/CLI/cli_formatters.js +89 -0
- package/src/CLI/code_agent.js +166 -57
- package/src/CLI/intent_detectors.js +181 -0
- package/src/CLI/interactive_chat.js +479 -0
- package/src/CLI/list_features.js +3 -0
- package/src/CLI/repo_summarizer.js +282 -0
- package/src/CLI/semantic_code_search.js +312 -0
- package/src/CLI/skill_manager.js +41 -0
- package/src/CLI/slash_command_handler.js +418 -0
- package/src/CLI/symbol_indexer.js +231 -0
- package/src/Channels/discord_bridge.js +11 -13
- package/src/Channels/line_bridge.js +10 -10
- package/src/Channels/slack_bridge.js +7 -12
- package/src/Channels/telegram_bridge.js +6 -14
- package/src/Channels/whatsapp_bridge.js +11 -9
- package/src/System/chat_history_manager.js +20 -12
- package/src/System/optional_require.js +23 -0
- package/src/UI/live2d_manager.js +211 -13
- package/src/UI/renderer.js +163 -3
- package/src/UI/settings.css +655 -420
- package/src/UI/settings.html +478 -432
- package/src/UI/settings.js +10 -8
- package/src/UI/styles.css +89 -25
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireOptional } = require('../System/optional_require');
|
|
2
4
|
const { handleChat } = require('../AI_Brain/Gemini_API');
|
|
3
5
|
|
|
4
6
|
class DiscordBridge {
|
|
5
7
|
constructor(token) {
|
|
6
8
|
this.token = token;
|
|
9
|
+
const { Client, GatewayIntentBits, Partials } = requireOptional(
|
|
10
|
+
'discord.js',
|
|
11
|
+
'npm install discord.js'
|
|
12
|
+
);
|
|
7
13
|
this.client = new Client({
|
|
8
14
|
intents: [
|
|
9
15
|
GatewayIntentBits.Guilds,
|
|
@@ -21,30 +27,22 @@ class DiscordBridge {
|
|
|
21
27
|
});
|
|
22
28
|
|
|
23
29
|
this.client.on('messageCreate', async (message) => {
|
|
24
|
-
// Ignore bot messages
|
|
25
30
|
if (message.author.bot) return;
|
|
26
|
-
|
|
27
|
-
// Handle DMs or Mentions
|
|
28
31
|
const isDM = !message.guild;
|
|
29
32
|
const isMentioned = message.mentions.has(this.client.user);
|
|
30
33
|
|
|
31
34
|
if (isDM || isMentioned) {
|
|
32
35
|
try {
|
|
33
|
-
// Clean up the message if it's a mention
|
|
34
36
|
let cleanContent = message.content;
|
|
35
37
|
if (isMentioned) {
|
|
36
|
-
cleanContent = message.content
|
|
38
|
+
cleanContent = message.content
|
|
39
|
+
.replace(`<@!${this.client.user.id}>`, '')
|
|
40
|
+
.replace(`<@${this.client.user.id}>`, '')
|
|
41
|
+
.trim();
|
|
37
42
|
}
|
|
38
|
-
|
|
39
43
|
if (!cleanContent) return;
|
|
40
|
-
|
|
41
|
-
// Show typing indicator
|
|
42
44
|
await message.channel.sendTyping();
|
|
43
|
-
|
|
44
|
-
// Send to Mint AI Brain
|
|
45
45
|
const result = await handleChat(cleanContent);
|
|
46
|
-
|
|
47
|
-
// Reply to user
|
|
48
46
|
if (result && result.response) {
|
|
49
47
|
await message.reply(result.response);
|
|
50
48
|
}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireOptional } = require('../System/optional_require');
|
|
3
4
|
const { handleChat } = require('../AI_Brain/Gemini_API');
|
|
4
5
|
|
|
5
6
|
class LineBridge {
|
|
6
7
|
constructor(credentials) {
|
|
8
|
+
this._line = requireOptional('@line/bot-sdk', 'npm install @line/bot-sdk express');
|
|
9
|
+
this._express = requireOptional('express', 'npm install @line/bot-sdk express');
|
|
7
10
|
this.config = {
|
|
8
11
|
channelAccessToken: credentials.accessToken,
|
|
9
12
|
channelSecret: credentials.secret,
|
|
10
13
|
};
|
|
11
|
-
this.port
|
|
12
|
-
this.client = new
|
|
14
|
+
this.port = credentials.port || 3000;
|
|
15
|
+
this.client = new this._line.messagingApi.MessagingApiClient({
|
|
13
16
|
channelAccessToken: credentials.accessToken
|
|
14
17
|
});
|
|
15
|
-
this.app
|
|
18
|
+
this.app = this._express();
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
async connect() {
|
|
19
|
-
this.app.post('/callback',
|
|
22
|
+
this.app.post('/callback', this._line.middleware(this.config), (req, res) => {
|
|
20
23
|
Promise
|
|
21
24
|
.all(req.body.events.map(event => this.handleEvent(event)))
|
|
22
25
|
.then((result) => res.json(result))
|
|
@@ -36,7 +39,6 @@ class LineBridge {
|
|
|
36
39
|
if (event.type !== 'message' || event.message.type !== 'text') {
|
|
37
40
|
return Promise.resolve(null);
|
|
38
41
|
}
|
|
39
|
-
|
|
40
42
|
try {
|
|
41
43
|
const result = await handleChat(event.message.text);
|
|
42
44
|
if (result && result.response) {
|
|
@@ -51,9 +53,7 @@ class LineBridge {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
async disconnect() {
|
|
54
|
-
if (this.server)
|
|
55
|
-
this.server.close();
|
|
56
|
-
}
|
|
56
|
+
if (this.server) this.server.close();
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireOptional } = require('../System/optional_require');
|
|
2
4
|
const { handleChat } = require('../AI_Brain/Gemini_API');
|
|
3
5
|
|
|
4
6
|
class SlackBridge {
|
|
5
7
|
constructor(credentials) {
|
|
8
|
+
const { App } = requireOptional('@slack/bolt', 'npm install @slack/bolt');
|
|
6
9
|
this.app = new App({
|
|
7
10
|
token: credentials.botToken,
|
|
8
11
|
appToken: credentials.appToken,
|
|
@@ -15,24 +18,18 @@ class SlackBridge {
|
|
|
15
18
|
try {
|
|
16
19
|
const text = event.text.replace(/<@.*?>/g, '').trim();
|
|
17
20
|
if (!text) return;
|
|
18
|
-
|
|
19
21
|
const result = await handleChat(text);
|
|
20
|
-
if (result && result.response)
|
|
21
|
-
await say(result.response);
|
|
22
|
-
}
|
|
22
|
+
if (result && result.response) await say(result.response);
|
|
23
23
|
} catch (err) {
|
|
24
24
|
console.error('[Slack Bridge] Error processing app_mention:', err);
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
this.app.event('message', async ({ event, say }) => {
|
|
29
|
-
// Only respond in DMs
|
|
30
29
|
if (event.channel_type === 'im') {
|
|
31
30
|
try {
|
|
32
31
|
const result = await handleChat(event.text);
|
|
33
|
-
if (result && result.response)
|
|
34
|
-
await say(result.response);
|
|
35
|
-
}
|
|
32
|
+
if (result && result.response) await say(result.response);
|
|
36
33
|
} catch (err) {
|
|
37
34
|
console.error('[Slack Bridge] Error processing message:', err);
|
|
38
35
|
}
|
|
@@ -44,9 +41,7 @@ class SlackBridge {
|
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
async disconnect() {
|
|
47
|
-
if (this.app)
|
|
48
|
-
await this.app.stop();
|
|
49
|
-
}
|
|
44
|
+
if (this.app) await this.app.stop();
|
|
50
45
|
}
|
|
51
46
|
}
|
|
52
47
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireOptional } = require('../System/optional_require');
|
|
2
4
|
const { handleChat } = require('../AI_Brain/Gemini_API');
|
|
3
5
|
|
|
4
6
|
class TelegramBridge {
|
|
5
7
|
constructor(token) {
|
|
6
8
|
this.token = token;
|
|
9
|
+
const { Telegraf } = requireOptional('telegraf', 'npm install telegraf');
|
|
7
10
|
this.bot = new Telegraf(token);
|
|
8
11
|
}
|
|
9
12
|
|
|
@@ -12,19 +15,11 @@ class TelegramBridge {
|
|
|
12
15
|
|
|
13
16
|
this.bot.on('text', async (ctx) => {
|
|
14
17
|
try {
|
|
15
|
-
// Show typing status
|
|
16
18
|
await ctx.sendChatAction('typing');
|
|
17
|
-
|
|
18
19
|
const message = ctx.message.text;
|
|
19
20
|
if (!message) return;
|
|
20
|
-
|
|
21
|
-
// Send to Mint AI Brain
|
|
22
21
|
const result = await handleChat(message);
|
|
23
|
-
|
|
24
|
-
// Reply to user
|
|
25
|
-
if (result && result.response) {
|
|
26
|
-
await ctx.reply(result.response);
|
|
27
|
-
}
|
|
22
|
+
if (result && result.response) await ctx.reply(result.response);
|
|
28
23
|
} catch (err) {
|
|
29
24
|
console.error('[Telegram Bridge] Error processing message:', err);
|
|
30
25
|
await ctx.reply('ขออภัยค่ะ เกิดข้อผิดพลาดบางอย่างในการประมวลผลข้อความ');
|
|
@@ -34,15 +29,12 @@ class TelegramBridge {
|
|
|
34
29
|
this.bot.launch();
|
|
35
30
|
console.log('[Telegram Bridge] Bot started!');
|
|
36
31
|
|
|
37
|
-
// Enable graceful stop
|
|
38
32
|
process.once('SIGINT', () => this.bot.stop('SIGINT'));
|
|
39
33
|
process.once('SIGTERM', () => this.bot.stop('SIGTERM'));
|
|
40
34
|
}
|
|
41
35
|
|
|
42
36
|
async disconnect() {
|
|
43
|
-
if (this.bot)
|
|
44
|
-
await this.bot.stop();
|
|
45
|
-
}
|
|
37
|
+
if (this.bot) await this.bot.stop();
|
|
46
38
|
}
|
|
47
39
|
}
|
|
48
40
|
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireOptional } = require('../System/optional_require');
|
|
3
4
|
const { handleChat } = require('../AI_Brain/Gemini_API');
|
|
4
5
|
|
|
5
6
|
class WhatsappBridge {
|
|
6
7
|
constructor() {
|
|
8
|
+
// Dynamic require — only loads if user has installed whatsapp-web.js
|
|
9
|
+
const { Client, LocalAuth } = requireOptional(
|
|
10
|
+
'whatsapp-web.js',
|
|
11
|
+
'npm install whatsapp-web.js qrcode-terminal'
|
|
12
|
+
);
|
|
13
|
+
this._qrcode = requireOptional('qrcode-terminal', 'npm install qrcode-terminal');
|
|
7
14
|
this.client = new Client({
|
|
8
15
|
authStrategy: new LocalAuth({
|
|
9
16
|
dataPath: require('path').join(require('os').homedir(), '.config', 'mint', 'whatsapp-session')
|
|
@@ -17,7 +24,7 @@ class WhatsappBridge {
|
|
|
17
24
|
async connect() {
|
|
18
25
|
this.client.on('qr', (qr) => {
|
|
19
26
|
console.log('[WhatsApp Bridge] Scan this QR code to login:');
|
|
20
|
-
|
|
27
|
+
this._qrcode.generate(qr, { small: true });
|
|
21
28
|
});
|
|
22
29
|
|
|
23
30
|
this.client.on('ready', () => {
|
|
@@ -26,13 +33,8 @@ class WhatsappBridge {
|
|
|
26
33
|
|
|
27
34
|
this.client.on('message', async (msg) => {
|
|
28
35
|
try {
|
|
29
|
-
// Ignore messages from groups unless mentioned (simple implementation)
|
|
30
36
|
const chat = await msg.getChat();
|
|
31
|
-
if (chat.isGroup)
|
|
32
|
-
// For groups, we could add a mention check here if desired
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
37
|
+
if (chat.isGroup) return;
|
|
36
38
|
const result = await handleChat(msg.body);
|
|
37
39
|
if (result && result.response) {
|
|
38
40
|
await msg.reply(result.response);
|
|
@@ -19,21 +19,29 @@ if (!fs.existsSync(CONFIG_DIR)) {
|
|
|
19
19
|
|
|
20
20
|
const CHAT_HISTORY_PATH = path.join(CONFIG_DIR, 'mint-chat-history.json');
|
|
21
21
|
|
|
22
|
-
// Migration Logic: Consolidate from
|
|
22
|
+
// Migration Logic: Consolidate from various legacy locations to ~/.config/mint/
|
|
23
23
|
if (!fs.existsSync(CHAT_HISTORY_PATH)) {
|
|
24
24
|
const electronUserData = app && app.getPath ? path.join(app.getPath('userData'), 'mint-chat-history.json') : null;
|
|
25
|
-
const
|
|
25
|
+
const legacyDotMint = path.join(MINT_DIR, 'mint-chat-history.json');
|
|
26
|
+
// Legacy: file was written to the project root (CWD) before v1.5.2
|
|
27
|
+
const legacyProjectRoot = path.join(process.cwd(), 'mint-chat-history.json');
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const candidates = [
|
|
30
|
+
electronUserData,
|
|
31
|
+
legacyDotMint,
|
|
32
|
+
legacyProjectRoot
|
|
33
|
+
].filter(Boolean);
|
|
34
|
+
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
if (candidate !== CHAT_HISTORY_PATH && fs.existsSync(candidate)) {
|
|
37
|
+
try {
|
|
38
|
+
fs.copyFileSync(candidate, CHAT_HISTORY_PATH);
|
|
39
|
+
console.log(`[History] Migrated chat history from ${candidate}`);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('[History] Migration failed:', e);
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper: ลอง require package แบบ dynamic
|
|
5
|
+
* ถ้าหาไม่เจอให้ throw Error พร้อม install guide
|
|
6
|
+
*/
|
|
7
|
+
function requireOptional(pkg, installHint) {
|
|
8
|
+
try {
|
|
9
|
+
return require(pkg);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
if (e.code === 'MODULE_NOT_FOUND') {
|
|
12
|
+
const hint = installHint || `npm install ${pkg}`;
|
|
13
|
+
throw new Error(
|
|
14
|
+
`[Mint] Optional package "${pkg}" is not installed.\n` +
|
|
15
|
+
`To use this feature, run: ${hint}\n` +
|
|
16
|
+
`(This package is not bundled by default to keep Mint lightweight.)`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { requireOptional };
|
package/src/UI/live2d_manager.js
CHANGED
|
@@ -7,6 +7,33 @@ window.Live2DManager = {
|
|
|
7
7
|
resizeObserver: null,
|
|
8
8
|
lipSyncInterval: null,
|
|
9
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,
|
|
10
37
|
lastInteractionAt: 0,
|
|
11
38
|
expressionToastTimeout: null,
|
|
12
39
|
expressionParamIds: [
|
|
@@ -39,6 +66,8 @@ window.Live2DManager = {
|
|
|
39
66
|
|
|
40
67
|
async loadModel(mountEl, statusEl, shellEl) {
|
|
41
68
|
this.statusEl = statusEl; // Store for later use
|
|
69
|
+
this.interactionEnabled = this.getSavedInteractionEnabled();
|
|
70
|
+
this.activeAccessories = this.getSavedAccessories();
|
|
42
71
|
if (!mountEl) return;
|
|
43
72
|
if (statusEl) {
|
|
44
73
|
statusEl.classList.remove('is-error');
|
|
@@ -74,7 +103,7 @@ window.Live2DManager = {
|
|
|
74
103
|
|
|
75
104
|
const modelUrl = new URL('../../models/Shiroko_Model/Shiroko/Shiroko_Core/%E9%9D%A2%E9%A5%BC0.model3.json', window.location.href).href;
|
|
76
105
|
this.model = await window.PIXI.live2d.Live2DModel.from(modelUrl, {
|
|
77
|
-
autoInteract:
|
|
106
|
+
autoInteract: false
|
|
78
107
|
});
|
|
79
108
|
this.expressionToastEl = document.getElementById('expression-toast');
|
|
80
109
|
|
|
@@ -82,8 +111,9 @@ window.Live2DManager = {
|
|
|
82
111
|
this.app.stage.addChild(this.model);
|
|
83
112
|
|
|
84
113
|
// -- Interaction Setup --
|
|
85
|
-
this.
|
|
86
|
-
this.
|
|
114
|
+
this.setInteractionEnabled(this.interactionEnabled);
|
|
115
|
+
this.setupPointerTracking(mountEl);
|
|
116
|
+
this.applyAccessories();
|
|
87
117
|
|
|
88
118
|
// Tap Interaction. This model does not define Cubism HitAreas, so use
|
|
89
119
|
// normalized model coordinates to provide stable region reactions.
|
|
@@ -105,11 +135,15 @@ window.Live2DManager = {
|
|
|
105
135
|
const heightScale = mountHeight / Math.max(modelHeight, 1);
|
|
106
136
|
|
|
107
137
|
// Reduced zoom to 2.0 as requested
|
|
108
|
-
const scale = Math.min(widthScale, heightScale) * 1.
|
|
138
|
+
const scale = Math.min(widthScale, heightScale) * 1.85;
|
|
109
139
|
|
|
110
140
|
this.model.scale.set(scale);
|
|
111
141
|
// Adjusted Y offset to 1.0 as requested
|
|
112
|
-
this.
|
|
142
|
+
this.baseModelPosition = {
|
|
143
|
+
x: mountWidth / 2,
|
|
144
|
+
y: mountHeight / 2 + mountHeight * 0.55
|
|
145
|
+
};
|
|
146
|
+
this.applyModelFollowOffset();
|
|
113
147
|
};
|
|
114
148
|
|
|
115
149
|
requestAnimationFrame(() => {
|
|
@@ -147,7 +181,7 @@ window.Live2DManager = {
|
|
|
147
181
|
},
|
|
148
182
|
|
|
149
183
|
handleModelTap(event) {
|
|
150
|
-
if (!this.model) return;
|
|
184
|
+
if (!this.model || !this.interactionEnabled) return;
|
|
151
185
|
|
|
152
186
|
const now = Date.now();
|
|
153
187
|
if (now - this.lastInteractionAt < 3000) return;
|
|
@@ -182,7 +216,7 @@ window.Live2DManager = {
|
|
|
182
216
|
if (!point) return null;
|
|
183
217
|
const { x, y } = point;
|
|
184
218
|
|
|
185
|
-
if (this.isPointInZone(x, y, 0.
|
|
219
|
+
if (this.isPointInZone(x, y, 0.38, 0.40, 0.24, 0.115)) {
|
|
186
220
|
return {
|
|
187
221
|
id: 'face',
|
|
188
222
|
label: 'Cat Ears',
|
|
@@ -191,7 +225,7 @@ window.Live2DManager = {
|
|
|
191
225
|
};
|
|
192
226
|
}
|
|
193
227
|
|
|
194
|
-
if (this.isPointInZone(x, y, 0.
|
|
228
|
+
if (this.isPointInZone(x, y, 0.34, 0.255, 0.32, 0.15)) {
|
|
195
229
|
return {
|
|
196
230
|
id: 'head',
|
|
197
231
|
label: 'Head Pat',
|
|
@@ -200,8 +234,8 @@ window.Live2DManager = {
|
|
|
200
234
|
};
|
|
201
235
|
}
|
|
202
236
|
|
|
203
|
-
const isLeftHand = this.isPointInZone(x, y, 0.
|
|
204
|
-
const isRightHand = this.isPointInZone(x, y, 0.
|
|
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);
|
|
205
239
|
if (isLeftHand || isRightHand) {
|
|
206
240
|
return {
|
|
207
241
|
id: isLeftHand ? 'left-hand' : 'right-hand',
|
|
@@ -211,7 +245,7 @@ window.Live2DManager = {
|
|
|
211
245
|
};
|
|
212
246
|
}
|
|
213
247
|
|
|
214
|
-
if (this.isPointInZone(x, y, 0.
|
|
248
|
+
if (this.isPointInZone(x, y, 0.38, 0.77, 0.30, 0.23)) {
|
|
215
249
|
return {
|
|
216
250
|
id: 'lower-body',
|
|
217
251
|
label: 'Careful',
|
|
@@ -220,7 +254,7 @@ window.Live2DManager = {
|
|
|
220
254
|
};
|
|
221
255
|
}
|
|
222
256
|
|
|
223
|
-
if (this.isPointInZone(x, y, 0.
|
|
257
|
+
if (this.isPointInZone(x, y, 0.37, 0.555, 0.29, 0.14)) {
|
|
224
258
|
return {
|
|
225
259
|
id: 'body',
|
|
226
260
|
label: 'Shoulder Tap',
|
|
@@ -271,6 +305,112 @@ window.Live2DManager = {
|
|
|
271
305
|
this.showExpressionToast(`Expression: ${nextExp.label}`);
|
|
272
306
|
},
|
|
273
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
|
+
|
|
274
414
|
showExpressionToast(text, duration = 1600) {
|
|
275
415
|
const toast = this.expressionToastEl || document.getElementById('expression-toast');
|
|
276
416
|
if (!toast) return;
|
|
@@ -296,6 +436,7 @@ window.Live2DManager = {
|
|
|
296
436
|
|
|
297
437
|
try {
|
|
298
438
|
this.model.expression(expressionId);
|
|
439
|
+
requestAnimationFrame(() => this.applyAccessories());
|
|
299
440
|
} catch (error) {
|
|
300
441
|
console.error(`[Live2D] Failed to apply expression: ${expressionId}`, error);
|
|
301
442
|
}
|
|
@@ -328,7 +469,64 @@ window.Live2DManager = {
|
|
|
328
469
|
}
|
|
329
470
|
|
|
330
471
|
this.resetExpressionParams();
|
|
331
|
-
requestAnimationFrame(() =>
|
|
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 (_) {}
|
|
332
530
|
},
|
|
333
531
|
|
|
334
532
|
startLipSync() {
|