clawmate 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/electron-builder.yml +23 -0
- package/index.js +341 -0
- package/main/ai-bridge.js +261 -0
- package/main/ai-connector.js +169 -0
- package/main/autostart.js +42 -0
- package/main/desktop-path.js +33 -0
- package/main/file-ops.js +146 -0
- package/main/index.js +128 -0
- package/main/ipc-handlers.js +104 -0
- package/main/manifest.js +70 -0
- package/main/platform.js +33 -0
- package/main/store.js +45 -0
- package/main/tray.js +125 -0
- package/openclaw.plugin.json +15 -0
- package/package.json +30 -0
- package/preload/preload.js +57 -0
- package/renderer/css/effects.css +87 -0
- package/renderer/css/launcher.css +127 -0
- package/renderer/css/pet.css +109 -0
- package/renderer/css/speech.css +72 -0
- package/renderer/first-run.html +90 -0
- package/renderer/index.html +62 -0
- package/renderer/js/ai-controller.js +206 -0
- package/renderer/js/app.js +107 -0
- package/renderer/js/character.js +396 -0
- package/renderer/js/interactions.js +164 -0
- package/renderer/js/memory.js +322 -0
- package/renderer/js/mode-manager.js +82 -0
- package/renderer/js/pet-engine.js +228 -0
- package/renderer/js/speech.js +116 -0
- package/renderer/js/state-machine.js +175 -0
- package/renderer/js/time-aware.js +93 -0
- package/renderer/launcher.html +58 -0
- package/shared/constants.js +80 -0
- package/shared/messages.js +103 -0
- package/shared/personalities.js +106 -0
- package/skills/launch-pet/index.js +64 -0
- package/skills/launch-pet/skill.json +26 -0
package/main/platform.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const platform = os.platform();
|
|
6
|
+
|
|
7
|
+
function getDesktopPath() {
|
|
8
|
+
if (platform === 'win32') {
|
|
9
|
+
try {
|
|
10
|
+
const result = execSync(
|
|
11
|
+
'powershell -Command "[Environment]::GetFolderPath(\'Desktop\')"',
|
|
12
|
+
{ encoding: 'utf-8' }
|
|
13
|
+
).trim();
|
|
14
|
+
if (result) return result;
|
|
15
|
+
} catch {}
|
|
16
|
+
return path.join(os.homedir(), 'Desktop');
|
|
17
|
+
}
|
|
18
|
+
return path.join(os.homedir(), 'Desktop');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getTrayIconExt() {
|
|
22
|
+
return platform === 'win32' ? '.ico' : '.png';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isWindows() {
|
|
26
|
+
return platform === 'win32';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isMac() {
|
|
30
|
+
return platform === 'darwin';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { getDesktopPath, getTrayIconExt, isWindows, isMac, platform };
|
package/main/store.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { app } = require('electron');
|
|
4
|
+
|
|
5
|
+
class Store {
|
|
6
|
+
constructor(name, defaults = {}) {
|
|
7
|
+
const userDataPath = app.getPath('userData');
|
|
8
|
+
this.path = path.join(userDataPath, `${name}.json`);
|
|
9
|
+
this.data = { ...defaults };
|
|
10
|
+
this._load();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_load() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs.readFileSync(this.path, 'utf-8');
|
|
16
|
+
this.data = { ...this.data, ...JSON.parse(raw) };
|
|
17
|
+
} catch {
|
|
18
|
+
// 파일 없으면 기본값 사용
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_save() {
|
|
23
|
+
try {
|
|
24
|
+
fs.mkdirSync(path.dirname(this.path), { recursive: true });
|
|
25
|
+
fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2));
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('Store save error:', err);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get(key) {
|
|
32
|
+
return this.data[key];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
set(key, value) {
|
|
36
|
+
this.data[key] = value;
|
|
37
|
+
this._save();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getAll() {
|
|
41
|
+
return { ...this.data };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = Store;
|
package/main/tray.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const { Tray, Menu, nativeImage, app } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const Store = require('./store');
|
|
4
|
+
const { undoAllMoves, getFileManifest } = require('./file-ops');
|
|
5
|
+
const { isAutoStartEnabled, toggleAutoStart } = require('./autostart');
|
|
6
|
+
|
|
7
|
+
let tray = null;
|
|
8
|
+
let aiBridge = null;
|
|
9
|
+
|
|
10
|
+
function setupTray(mainWindow, bridge) {
|
|
11
|
+
aiBridge = bridge;
|
|
12
|
+
const store = new Store('clawmate-config', { mode: 'pet' });
|
|
13
|
+
|
|
14
|
+
// 트레이 아이콘 생성
|
|
15
|
+
let icon;
|
|
16
|
+
try {
|
|
17
|
+
const iconPath = path.join(__dirname, '..', 'assets', 'icons', 'tray-pet.png');
|
|
18
|
+
icon = nativeImage.createFromPath(iconPath);
|
|
19
|
+
if (icon.isEmpty()) throw new Error('no icon');
|
|
20
|
+
} catch {
|
|
21
|
+
// 16x16 빨간색 아이콘 폴백
|
|
22
|
+
const size = 16;
|
|
23
|
+
const buffer = Buffer.alloc(size * size * 4);
|
|
24
|
+
for (let i = 0; i < size * size; i++) {
|
|
25
|
+
buffer[i * 4 + 0] = 0xff;
|
|
26
|
+
buffer[i * 4 + 1] = 0x4f;
|
|
27
|
+
buffer[i * 4 + 2] = 0x40;
|
|
28
|
+
buffer[i * 4 + 3] = 0xff;
|
|
29
|
+
}
|
|
30
|
+
icon = nativeImage.createFromBuffer(buffer, { width: size, height: size });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
tray = new Tray(icon);
|
|
34
|
+
tray.setToolTip('ClawMate - 데스크톱 펫');
|
|
35
|
+
|
|
36
|
+
function buildMenu() {
|
|
37
|
+
const mode = store.get('mode') || 'pet';
|
|
38
|
+
const fileInteraction = store.get('fileInteraction') !== false;
|
|
39
|
+
const aiConnected = aiBridge ? aiBridge.isConnected() : false;
|
|
40
|
+
const autoStart = isAutoStartEnabled();
|
|
41
|
+
|
|
42
|
+
return Menu.buildFromTemplate([
|
|
43
|
+
{
|
|
44
|
+
label: `ClawMate (${mode === 'pet' ? 'Clawby' : 'OpenClaw'})`,
|
|
45
|
+
enabled: false,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: aiConnected ? 'AI: 연결됨' : 'AI: 자율 모드 (대기 중)',
|
|
49
|
+
enabled: false,
|
|
50
|
+
},
|
|
51
|
+
{ type: 'separator' },
|
|
52
|
+
{
|
|
53
|
+
label: 'Pet 모드 (Clawby)',
|
|
54
|
+
type: 'radio',
|
|
55
|
+
checked: mode === 'pet',
|
|
56
|
+
click: () => {
|
|
57
|
+
store.set('mode', 'pet');
|
|
58
|
+
if (mainWindow) mainWindow.webContents.send('mode-changed', 'pet');
|
|
59
|
+
buildAndSet();
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: 'Incarnation 모드 (OpenClaw)',
|
|
64
|
+
type: 'radio',
|
|
65
|
+
checked: mode === 'incarnation',
|
|
66
|
+
click: () => {
|
|
67
|
+
store.set('mode', 'incarnation');
|
|
68
|
+
if (mainWindow) mainWindow.webContents.send('mode-changed', 'incarnation');
|
|
69
|
+
buildAndSet();
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{ type: 'separator' },
|
|
73
|
+
{
|
|
74
|
+
label: '파일 상호작용',
|
|
75
|
+
type: 'checkbox',
|
|
76
|
+
checked: fileInteraction,
|
|
77
|
+
click: (item) => {
|
|
78
|
+
store.set('fileInteraction', item.checked);
|
|
79
|
+
if (mainWindow) mainWindow.webContents.send('config-changed', store.getAll());
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
label: '컴퓨터 시작 시 자동 실행',
|
|
84
|
+
type: 'checkbox',
|
|
85
|
+
checked: autoStart,
|
|
86
|
+
click: () => {
|
|
87
|
+
toggleAutoStart();
|
|
88
|
+
buildAndSet();
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{ type: 'separator' },
|
|
92
|
+
{
|
|
93
|
+
label: '파일 이동 되돌리기',
|
|
94
|
+
click: async () => {
|
|
95
|
+
const manifest = await getFileManifest();
|
|
96
|
+
const pending = manifest.filter(m => !m.restored);
|
|
97
|
+
if (pending.length === 0) return;
|
|
98
|
+
await undoAllMoves();
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{ type: 'separator' },
|
|
102
|
+
{
|
|
103
|
+
label: '종료',
|
|
104
|
+
click: () => {
|
|
105
|
+
app.quit();
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildAndSet() {
|
|
112
|
+
tray.setContextMenu(buildMenu());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// AI 연결 상태 변경 시 메뉴 업데이트
|
|
116
|
+
if (aiBridge) {
|
|
117
|
+
aiBridge.on('connected', () => buildAndSet());
|
|
118
|
+
aiBridge.on('disconnected', () => buildAndSet());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
buildAndSet();
|
|
122
|
+
return tray;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { setupTray };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "clawmate",
|
|
3
|
+
"name": "ClawMate",
|
|
4
|
+
"description": "OpenClaw 데스크톱 펫 - 화면 위의 살아있는 Claw",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"skills": ["skills/launch-pet"],
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"mode": { "enum": ["pet", "incarnation"], "default": "pet" },
|
|
11
|
+
"fileInteraction": { "type": "boolean", "default": true },
|
|
12
|
+
"soundEnabled": { "type": "boolean", "default": false }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawmate",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenClaw 데스크톱 펫 - AI가 조종하는 화면 위의 살아있는 Claw",
|
|
5
|
+
"main": "main/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clawmate": "./skills/launch-pet/index.js"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/boqum/clawmate.git"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "electron .",
|
|
15
|
+
"build": "electron-builder",
|
|
16
|
+
"build:win": "electron-builder --win",
|
|
17
|
+
"build:mac": "electron-builder --mac"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["openclaw", "desktop-pet", "electron", "clawmate", "ai-pet"],
|
|
20
|
+
"author": "OpenClaw",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"homepage": "https://github.com/boqum/clawmate",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"ws": "^8.18.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"electron": "^33.0.0",
|
|
28
|
+
"electron-builder": "^25.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('clawmate', {
|
|
4
|
+
// 클릭 통과 제어
|
|
5
|
+
setClickThrough: (ignore) => ipcRenderer.send('set-click-through', ignore),
|
|
6
|
+
|
|
7
|
+
// 파일 작업
|
|
8
|
+
getDesktopFiles: () => ipcRenderer.invoke('get-desktop-files'),
|
|
9
|
+
moveFile: (fileName, newPosition) => ipcRenderer.invoke('move-file', fileName, newPosition),
|
|
10
|
+
undoFileMove: (moveId) => ipcRenderer.invoke('undo-file-move', moveId),
|
|
11
|
+
undoAllMoves: () => ipcRenderer.invoke('undo-all-moves'),
|
|
12
|
+
getFileManifest: () => ipcRenderer.invoke('get-file-manifest'),
|
|
13
|
+
|
|
14
|
+
// 모드 전환
|
|
15
|
+
getMode: () => ipcRenderer.invoke('get-mode'),
|
|
16
|
+
setMode: (mode) => ipcRenderer.invoke('set-mode', mode),
|
|
17
|
+
|
|
18
|
+
// 설정
|
|
19
|
+
getConfig: () => ipcRenderer.invoke('get-config'),
|
|
20
|
+
setConfig: (key, value) => ipcRenderer.invoke('set-config', key, value),
|
|
21
|
+
|
|
22
|
+
// 메모리 (사용자 상호작용 기억)
|
|
23
|
+
getMemory: () => ipcRenderer.invoke('get-memory'),
|
|
24
|
+
saveMemory: (data) => ipcRenderer.invoke('save-memory', data),
|
|
25
|
+
|
|
26
|
+
// 창 정보
|
|
27
|
+
getScreenSize: () => ipcRenderer.invoke('get-screen-size'),
|
|
28
|
+
|
|
29
|
+
// 이벤트 수신
|
|
30
|
+
onModeChanged: (callback) => {
|
|
31
|
+
ipcRenderer.on('mode-changed', (_, mode) => callback(mode));
|
|
32
|
+
},
|
|
33
|
+
onConfigChanged: (callback) => {
|
|
34
|
+
ipcRenderer.on('config-changed', (_, config) => callback(config));
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// === OpenClaw AI 통신 ===
|
|
38
|
+
|
|
39
|
+
// AI 명령 수신 (OpenClaw → 펫)
|
|
40
|
+
onAICommand: (callback) => {
|
|
41
|
+
ipcRenderer.on('ai-command', (_, command) => callback(command));
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// AI 연결/해제 이벤트
|
|
45
|
+
onAIConnected: (callback) => {
|
|
46
|
+
ipcRenderer.on('ai-connected', () => callback());
|
|
47
|
+
},
|
|
48
|
+
onAIDisconnected: (callback) => {
|
|
49
|
+
ipcRenderer.on('ai-disconnected', () => callback());
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// 사용자 이벤트를 OpenClaw에 전달 (펫 → OpenClaw)
|
|
53
|
+
reportToAI: (event, data) => ipcRenderer.send('report-to-ai', event, data),
|
|
54
|
+
|
|
55
|
+
// AI 연결 상태 확인
|
|
56
|
+
isAIConnected: () => ipcRenderer.invoke('is-ai-connected'),
|
|
57
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/* 파티클 및 전환 효과 */
|
|
2
|
+
|
|
3
|
+
/* 모드 전환 파티클 */
|
|
4
|
+
.mode-transition-particle {
|
|
5
|
+
position: absolute;
|
|
6
|
+
width: 4px;
|
|
7
|
+
height: 4px;
|
|
8
|
+
border-radius: 50%;
|
|
9
|
+
animation: particle-burst 0.8s ease-out forwards;
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@keyframes particle-burst {
|
|
14
|
+
0% {
|
|
15
|
+
opacity: 1;
|
|
16
|
+
transform: translate(0, 0) scale(1);
|
|
17
|
+
}
|
|
18
|
+
100% {
|
|
19
|
+
opacity: 0;
|
|
20
|
+
transform: translate(var(--px), var(--py)) scale(0);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* 진화 글로우 링 */
|
|
25
|
+
.evolve-ring {
|
|
26
|
+
position: absolute;
|
|
27
|
+
border-radius: 50%;
|
|
28
|
+
border: 2px solid;
|
|
29
|
+
animation: ring-expand 1s ease-out forwards;
|
|
30
|
+
pointer-events: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@keyframes ring-expand {
|
|
34
|
+
0% {
|
|
35
|
+
opacity: 1;
|
|
36
|
+
transform: scale(0.5);
|
|
37
|
+
}
|
|
38
|
+
100% {
|
|
39
|
+
opacity: 0;
|
|
40
|
+
transform: scale(3);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* 파일 픽업 이펙트 */
|
|
45
|
+
.file-pickup-effect {
|
|
46
|
+
position: absolute;
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
color: #ff4f40;
|
|
49
|
+
font-weight: bold;
|
|
50
|
+
animation: pickup-float 1s ease-out forwards;
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
user-select: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@keyframes pickup-float {
|
|
56
|
+
0% { opacity: 1; transform: translateY(0); }
|
|
57
|
+
100% { opacity: 0; transform: translateY(-30px); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* 꼬리 흔들기 (idle 상태 추가 효과) */
|
|
61
|
+
.tail-wag {
|
|
62
|
+
animation: wag 0.5s ease-in-out infinite alternate;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@keyframes wag {
|
|
66
|
+
0% { transform: rotate(-5deg); }
|
|
67
|
+
100% { transform: rotate(5deg); }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* 진화 단계 전환 — 밝은 플래시 */
|
|
71
|
+
.evolve-flash {
|
|
72
|
+
position: fixed;
|
|
73
|
+
top: 0;
|
|
74
|
+
left: 0;
|
|
75
|
+
width: 100%;
|
|
76
|
+
height: 100%;
|
|
77
|
+
background: white;
|
|
78
|
+
animation: flash-in-out 0.6s ease forwards;
|
|
79
|
+
pointer-events: none;
|
|
80
|
+
z-index: 9999;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes flash-in-out {
|
|
84
|
+
0% { opacity: 0; }
|
|
85
|
+
30% { opacity: 0.3; }
|
|
86
|
+
100% { opacity: 0; }
|
|
87
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
* {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 0;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
|
|
9
|
+
background: transparent;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
user-select: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.launcher {
|
|
15
|
+
width: 360px;
|
|
16
|
+
padding: 24px;
|
|
17
|
+
background: #ffffff;
|
|
18
|
+
border-radius: 16px;
|
|
19
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.launcher-header {
|
|
23
|
+
text-align: center;
|
|
24
|
+
margin-bottom: 20px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.logo {
|
|
28
|
+
font-size: 36px;
|
|
29
|
+
margin-bottom: 8px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.launcher-header h1 {
|
|
33
|
+
font-size: 22px;
|
|
34
|
+
color: #1a1a2e;
|
|
35
|
+
font-weight: 700;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.subtitle {
|
|
39
|
+
color: #888;
|
|
40
|
+
font-size: 13px;
|
|
41
|
+
margin-top: 4px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.launcher-body {
|
|
45
|
+
display: flex;
|
|
46
|
+
gap: 12px;
|
|
47
|
+
margin-bottom: 20px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.mode-card {
|
|
51
|
+
flex: 1;
|
|
52
|
+
padding: 16px 12px;
|
|
53
|
+
border: 2px solid #e0e0e0;
|
|
54
|
+
border-radius: 12px;
|
|
55
|
+
text-align: center;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
transition: all 0.2s ease;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.mode-card:hover {
|
|
61
|
+
border-color: #ccc;
|
|
62
|
+
background: #fafafa;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.mode-card.selected {
|
|
66
|
+
border-color: #ff4f40;
|
|
67
|
+
background: #fff5f5;
|
|
68
|
+
box-shadow: 0 0 0 3px rgba(255, 79, 64, 0.15);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#mode-incarnation.selected {
|
|
72
|
+
border-color: #00BFA5;
|
|
73
|
+
background: #f0fffd;
|
|
74
|
+
box-shadow: 0 0 0 3px rgba(0, 191, 165, 0.15);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.mode-icon {
|
|
78
|
+
font-size: 32px;
|
|
79
|
+
margin-bottom: 8px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.mode-card h2 {
|
|
83
|
+
font-size: 15px;
|
|
84
|
+
color: #1a1a2e;
|
|
85
|
+
margin-bottom: 2px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.mode-card p {
|
|
89
|
+
font-size: 11px;
|
|
90
|
+
color: #888;
|
|
91
|
+
margin-bottom: 6px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.mode-desc {
|
|
95
|
+
font-size: 11px;
|
|
96
|
+
color: #666;
|
|
97
|
+
line-height: 1.4;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.start-btn {
|
|
101
|
+
width: 100%;
|
|
102
|
+
padding: 12px;
|
|
103
|
+
border: none;
|
|
104
|
+
border-radius: 10px;
|
|
105
|
+
background: #ff4f40;
|
|
106
|
+
color: white;
|
|
107
|
+
font-size: 15px;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
transition: all 0.2s ease;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.start-btn:disabled {
|
|
114
|
+
background: #ddd;
|
|
115
|
+
color: #999;
|
|
116
|
+
cursor: not-allowed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.start-btn:not(:disabled):hover {
|
|
120
|
+
background: #e63e30;
|
|
121
|
+
transform: translateY(-1px);
|
|
122
|
+
box-shadow: 0 4px 12px rgba(255, 79, 64, 0.3);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.start-btn:not(:disabled):active {
|
|
126
|
+
transform: translateY(0);
|
|
127
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* 펫 컨테이너 */
|
|
2
|
+
#pet-container {
|
|
3
|
+
position: absolute;
|
|
4
|
+
width: 64px;
|
|
5
|
+
height: 64px;
|
|
6
|
+
transition: transform 0.1s ease;
|
|
7
|
+
image-rendering: pixelated;
|
|
8
|
+
z-index: 1000;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#pet-container canvas {
|
|
12
|
+
display: block;
|
|
13
|
+
image-rendering: pixelated;
|
|
14
|
+
image-rendering: crisp-edges;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* 수면 Z 이펙트 */
|
|
18
|
+
.sleep-z {
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: -20px;
|
|
21
|
+
right: -10px;
|
|
22
|
+
font-size: 14px;
|
|
23
|
+
color: #888;
|
|
24
|
+
animation: float-z 2s ease-in-out infinite;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
user-select: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.sleep-z:nth-child(2) {
|
|
30
|
+
top: -35px;
|
|
31
|
+
right: -5px;
|
|
32
|
+
font-size: 18px;
|
|
33
|
+
animation-delay: 0.5s;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sleep-z:nth-child(3) {
|
|
37
|
+
top: -50px;
|
|
38
|
+
right: 0px;
|
|
39
|
+
font-size: 22px;
|
|
40
|
+
animation-delay: 1s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@keyframes float-z {
|
|
44
|
+
0% { opacity: 0; transform: translateY(0) translateX(0); }
|
|
45
|
+
30% { opacity: 1; }
|
|
46
|
+
100% { opacity: 0; transform: translateY(-20px) translateX(10px); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* 흥분 별 이펙트 */
|
|
50
|
+
.star-effect {
|
|
51
|
+
position: absolute;
|
|
52
|
+
width: 8px;
|
|
53
|
+
height: 8px;
|
|
54
|
+
background: #FFD700;
|
|
55
|
+
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
|
|
56
|
+
animation: star-pop 0.6s ease-out forwards;
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@keyframes star-pop {
|
|
61
|
+
0% { opacity: 1; transform: scale(0); }
|
|
62
|
+
50% { opacity: 1; transform: scale(1.5); }
|
|
63
|
+
100% { opacity: 0; transform: scale(0.5) translateY(-20px); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* 하트 이펙트 (상호작용) */
|
|
67
|
+
.heart-effect {
|
|
68
|
+
position: absolute;
|
|
69
|
+
font-size: 16px;
|
|
70
|
+
animation: heart-float 1s ease-out forwards;
|
|
71
|
+
pointer-events: none;
|
|
72
|
+
user-select: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@keyframes heart-float {
|
|
76
|
+
0% { opacity: 1; transform: translateY(0) scale(0.5); }
|
|
77
|
+
100% { opacity: 0; transform: translateY(-40px) scale(1.2); }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* 진화 반짝임 이펙트 */
|
|
81
|
+
.evolve-sparkle {
|
|
82
|
+
position: absolute;
|
|
83
|
+
width: 6px;
|
|
84
|
+
height: 6px;
|
|
85
|
+
border-radius: 50%;
|
|
86
|
+
animation: sparkle-pop 0.8s ease-out forwards;
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@keyframes sparkle-pop {
|
|
91
|
+
0% { opacity: 1; transform: scale(0) rotate(0deg); }
|
|
92
|
+
50% { opacity: 1; transform: scale(1.5) rotate(180deg); }
|
|
93
|
+
100% { opacity: 0; transform: scale(0) rotate(360deg); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* 드래그 중 상태 */
|
|
97
|
+
#pet-container.dragging {
|
|
98
|
+
cursor: grabbing;
|
|
99
|
+
filter: brightness(1.1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* 모드별 글로우 효과 */
|
|
103
|
+
#pet-container.mode-incarnation canvas {
|
|
104
|
+
filter: drop-shadow(0 0 4px rgba(0, 191, 165, 0.6));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#pet-container.mode-pet canvas {
|
|
108
|
+
filter: drop-shadow(0 0 2px rgba(255, 79, 64, 0.3));
|
|
109
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* 말풍선 공통 */
|
|
2
|
+
.speech-bubble {
|
|
3
|
+
position: absolute;
|
|
4
|
+
padding: 8px 14px;
|
|
5
|
+
font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
|
|
6
|
+
font-size: 13px;
|
|
7
|
+
line-height: 1.4;
|
|
8
|
+
white-space: nowrap;
|
|
9
|
+
pointer-events: none;
|
|
10
|
+
z-index: 2000;
|
|
11
|
+
animation: speech-appear 0.2s ease-out;
|
|
12
|
+
max-width: 220px;
|
|
13
|
+
white-space: normal;
|
|
14
|
+
word-break: keep-all;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.speech-bubble::after {
|
|
18
|
+
content: '';
|
|
19
|
+
position: absolute;
|
|
20
|
+
bottom: -8px;
|
|
21
|
+
left: 30px;
|
|
22
|
+
width: 0;
|
|
23
|
+
height: 0;
|
|
24
|
+
border-left: 8px solid transparent;
|
|
25
|
+
border-right: 8px solid transparent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Pet 모드 말풍선 — 둥글고 빨간 테두리 */
|
|
29
|
+
.speech-pet {
|
|
30
|
+
background: #fff5f5;
|
|
31
|
+
border: 2px solid #ff4f40;
|
|
32
|
+
border-radius: 16px;
|
|
33
|
+
color: #333;
|
|
34
|
+
box-shadow: 0 2px 8px rgba(255, 79, 64, 0.15);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.speech-pet::after {
|
|
38
|
+
border-top: 8px solid #ff4f40;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Incarnation 모드 말풍선 — 각지고 틸 발광 */
|
|
42
|
+
.speech-incarnation {
|
|
43
|
+
background: #f0fffd;
|
|
44
|
+
border: 2px solid #00BFA5;
|
|
45
|
+
border-radius: 4px;
|
|
46
|
+
color: #1a1a2e;
|
|
47
|
+
box-shadow: 0 0 12px rgba(0, 191, 165, 0.3);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.speech-incarnation::after {
|
|
51
|
+
border-top: 8px solid #00BFA5;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* 타자기 커서 효과 */
|
|
55
|
+
.speech-text {
|
|
56
|
+
display: inline;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* 페이드 아웃 */
|
|
60
|
+
.speech-fade {
|
|
61
|
+
animation: speech-disappear 0.5s ease-in forwards;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@keyframes speech-appear {
|
|
65
|
+
0% { opacity: 0; transform: translateY(5px) scale(0.9); }
|
|
66
|
+
100% { opacity: 1; transform: translateY(0) scale(1); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@keyframes speech-disappear {
|
|
70
|
+
0% { opacity: 1; transform: scale(1); }
|
|
71
|
+
100% { opacity: 0; transform: scale(0.9) translateY(-5px); }
|
|
72
|
+
}
|