clawmate 1.2.0 → 1.3.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/index.js +749 -5
- package/main/ai-bridge.js +54 -0
- package/main/ai-connector.js +78 -0
- package/main/file-command-parser.js +324 -0
- package/main/index.js +12 -0
- package/main/ipc-handlers.js +102 -0
- package/main/platform.js +48 -1
- package/main/smart-file-ops.js +373 -0
- package/main/telegram.js +560 -0
- package/main/tray.js +132 -19
- package/package.json +2 -1
- package/preload/preload.js +16 -0
- package/renderer/index.html +2 -0
- package/renderer/js/ai-controller.js +294 -0
- package/renderer/js/app.js +10 -0
- package/renderer/js/browser-watcher.js +172 -0
- package/renderer/js/character.js +119 -22
- package/renderer/js/metrics.js +607 -0
- package/renderer/js/pet-engine.js +372 -30
- package/renderer/js/state-machine.js +7 -0
- package/shared/messages.js +110 -0
|
@@ -12,12 +12,11 @@ const PetEngine = (() => {
|
|
|
12
12
|
// --- 물리 상수 ---
|
|
13
13
|
const GRAVITY = 0.3; // 중력 가속도 (px/frame^2)
|
|
14
14
|
const STEP_SIZE = 4; // 한 걸음 크기 (px)
|
|
15
|
-
const STEP_PAUSE = 80; // 걸음 사이 멈춤 시간 (ms)
|
|
16
15
|
const JUMP_VX = 3; // 점프 수평 초기 속도
|
|
17
16
|
const JUMP_VY = -7; // 점프 수직 초기 속도 (위로)
|
|
18
17
|
const BOUNCE_FACTOR = 0.3; // 착지 바운스 계수
|
|
19
18
|
const CHAR_SIZE = 64; // 캐릭터 크기 (px)
|
|
20
|
-
const ANIM_INTERVAL =
|
|
19
|
+
const ANIM_INTERVAL = 150; // 애니메이션 프레임 전환 간격 (ms) — 부드러운 전환
|
|
21
20
|
const THREAD_SPEED = 0.8; // 레펠 하강 속도 (px/frame)
|
|
22
21
|
|
|
23
22
|
// --- 위치 및 속도 ---
|
|
@@ -28,6 +27,10 @@ const PetEngine = (() => {
|
|
|
28
27
|
let edge = 'bottom'; // 현재 부착된 가장자리 (bottom, left, right, top, surface)
|
|
29
28
|
let direction = 1; // 이동 방향: 1=오른쪽/아래, -1=왼쪽/위
|
|
30
29
|
let flipX = false; // 캐릭터 좌우 반전 여부
|
|
30
|
+
let prevFlipX = false; // 이전 프레임 flipX (전환 감지용)
|
|
31
|
+
let flipTransition = 0; // flipX 전환 진행도 (0~1, 1이면 완료)
|
|
32
|
+
const FLIP_DURATION = 120; // flipX 전환 시간 (ms)
|
|
33
|
+
let flipStartTime = 0; // flipX 전환 시작 시각
|
|
31
34
|
let screenW, screenH;
|
|
32
35
|
|
|
33
36
|
// --- 엔진 상태 ---
|
|
@@ -36,14 +39,13 @@ const PetEngine = (() => {
|
|
|
36
39
|
let speedMultiplier = 1.0;
|
|
37
40
|
let animFrame = 0;
|
|
38
41
|
let lastAnimTime = 0;
|
|
42
|
+
let animFrameChanged = false; // 애니메이션 프레임 전환 플래그 (이동과 동기화)
|
|
39
43
|
|
|
40
44
|
// --- 이동 모드 ---
|
|
41
45
|
let movementMode = 'crawling'; // crawling | jumping | falling | rappelling
|
|
42
46
|
let onSurface = true; // 표면 위에 있는지 여부
|
|
43
47
|
|
|
44
|
-
// --- 스텝 시스템 (
|
|
45
|
-
let stepPhase = 'move'; // 'move' 또는 'pause'
|
|
46
|
-
let lastStepTime = 0;
|
|
48
|
+
// --- 스텝 시스템 (애니메이션 프레임 동기화) ---
|
|
47
49
|
|
|
48
50
|
// --- 레펠(thread) 시스템 ---
|
|
49
51
|
// 부착점에서 실을 내려 진자운동하며 하강
|
|
@@ -114,6 +116,25 @@ const PetEngine = (() => {
|
|
|
114
116
|
function updateVisual() {
|
|
115
117
|
if (!petContainer) return;
|
|
116
118
|
|
|
119
|
+
// --- flipX 부드러운 전환 처리 ---
|
|
120
|
+
if (flipX !== prevFlipX) {
|
|
121
|
+
// 방향이 바뀌었으면 전환 시작
|
|
122
|
+
flipStartTime = Date.now();
|
|
123
|
+
flipTransition = 0;
|
|
124
|
+
prevFlipX = flipX;
|
|
125
|
+
}
|
|
126
|
+
if (flipTransition < 1) {
|
|
127
|
+
const elapsed = Date.now() - flipStartTime;
|
|
128
|
+
flipTransition = Math.min(1, elapsed / FLIP_DURATION);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 전환 중일 때 CSS transition 적용
|
|
132
|
+
if (flipTransition < 1) {
|
|
133
|
+
petContainer.style.transition = `transform ${FLIP_DURATION}ms ease-in-out`;
|
|
134
|
+
} else {
|
|
135
|
+
petContainer.style.transition = 'none';
|
|
136
|
+
}
|
|
137
|
+
|
|
117
138
|
let renderX = x;
|
|
118
139
|
let renderY = y;
|
|
119
140
|
|
|
@@ -200,22 +221,17 @@ const PetEngine = (() => {
|
|
|
200
221
|
// ===================================
|
|
201
222
|
|
|
202
223
|
/**
|
|
203
|
-
* 스텝 이동:
|
|
204
|
-
*
|
|
224
|
+
* 스텝 이동: 애니메이션 프레임 전환 시에만 한 걸음 이동
|
|
225
|
+
* 다리 움직임(프레임 변화)과 실제 위치 이동이 1:1 동기화되어
|
|
226
|
+
* 다리가 움직일 때만 몸이 이동하는 자연스러운 보행 구현
|
|
205
227
|
*
|
|
206
228
|
* @param {number} stepScale - 스텝 크기 배율 (0.6 = 짐 들고 느리게, 1.0 = 기본)
|
|
207
|
-
* @param {number} now - 현재 시각 (Date.now())
|
|
208
229
|
*/
|
|
209
|
-
function stepMove(stepScale
|
|
210
|
-
//
|
|
211
|
-
if (
|
|
212
|
-
if (now - lastStepTime >= STEP_PAUSE) {
|
|
213
|
-
stepPhase = 'move';
|
|
214
|
-
}
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
230
|
+
function stepMove(stepScale) {
|
|
231
|
+
// 애니메이션 프레임이 바뀌지 않았으면 이동 안 함
|
|
232
|
+
if (!animFrameChanged) return;
|
|
217
233
|
|
|
218
|
-
//
|
|
234
|
+
// 한 걸음 전진
|
|
219
235
|
const stepDist = STEP_SIZE * stepScale * speedMultiplier;
|
|
220
236
|
|
|
221
237
|
if (edge === 'bottom' || edge === 'top' || edge === 'surface') {
|
|
@@ -229,10 +245,6 @@ const PetEngine = (() => {
|
|
|
229
245
|
// 오른쪽 벽: y축 이동
|
|
230
246
|
y += stepDist * direction;
|
|
231
247
|
}
|
|
232
|
-
|
|
233
|
-
// 한 걸음 완료, 멈춤 단계로 전환
|
|
234
|
-
stepPhase = 'pause';
|
|
235
|
-
lastStepTime = now;
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
// ===================================
|
|
@@ -287,8 +299,6 @@ const PetEngine = (() => {
|
|
|
287
299
|
* @param {string} state - StateMachine의 현재 상태 (walking, idle 등)
|
|
288
300
|
*/
|
|
289
301
|
function moveForState(state) {
|
|
290
|
-
const now = Date.now();
|
|
291
|
-
|
|
292
302
|
switch (movementMode) {
|
|
293
303
|
|
|
294
304
|
// --- 포물선 점프 ---
|
|
@@ -445,7 +455,7 @@ const PetEngine = (() => {
|
|
|
445
455
|
switch (state) {
|
|
446
456
|
case 'walking':
|
|
447
457
|
case 'ceiling_walk':
|
|
448
|
-
stepMove(1.0
|
|
458
|
+
stepMove(1.0);
|
|
449
459
|
|
|
450
460
|
// 수평 이동 시 경계 처리
|
|
451
461
|
if (edge === 'bottom' || edge === 'top') {
|
|
@@ -483,7 +493,7 @@ const PetEngine = (() => {
|
|
|
483
493
|
|
|
484
494
|
// 벽에서 위로 기어오름: y 감소
|
|
485
495
|
if (edge === 'left' || edge === 'right') {
|
|
486
|
-
stepMove(0.7
|
|
496
|
+
stepMove(0.7);
|
|
487
497
|
// stepMove가 direction(-1)을 적용하므로 y가 감소함
|
|
488
498
|
}
|
|
489
499
|
|
|
@@ -501,7 +511,7 @@ const PetEngine = (() => {
|
|
|
501
511
|
// direction을 1(아래)로 설정해서 stepMove
|
|
502
512
|
const prevDir = direction;
|
|
503
513
|
direction = 1;
|
|
504
|
-
stepMove(0.7
|
|
514
|
+
stepMove(0.7);
|
|
505
515
|
direction = prevDir;
|
|
506
516
|
} else if (edge === 'top') {
|
|
507
517
|
// 천장에서 벽으로 내려가기 시작
|
|
@@ -535,7 +545,7 @@ const PetEngine = (() => {
|
|
|
535
545
|
|
|
536
546
|
case 'carrying':
|
|
537
547
|
// 짐 들고 느리게 이동
|
|
538
|
-
stepMove(0.6
|
|
548
|
+
stepMove(0.6);
|
|
539
549
|
if (edge === 'bottom' || edge === 'top' || edge === 'surface') {
|
|
540
550
|
if (x <= 0) { x = 0; direction = 1; }
|
|
541
551
|
if (x >= screenW - CHAR_SIZE) { x = screenW - CHAR_SIZE; direction = -1; }
|
|
@@ -579,6 +589,14 @@ const PetEngine = (() => {
|
|
|
579
589
|
}
|
|
580
590
|
break;
|
|
581
591
|
|
|
592
|
+
// 커스텀 이동 패턴 실행 중
|
|
593
|
+
case 'custom':
|
|
594
|
+
if (activeCustomMovement) {
|
|
595
|
+
updateCustomMovement(now - (updateCustomMovement._lastTime || now));
|
|
596
|
+
updateCustomMovement._lastTime = now;
|
|
597
|
+
}
|
|
598
|
+
break;
|
|
599
|
+
|
|
582
600
|
case 'idle':
|
|
583
601
|
case 'sleeping':
|
|
584
602
|
case 'interacting':
|
|
@@ -735,6 +753,7 @@ const PetEngine = (() => {
|
|
|
735
753
|
if (timestamp - lastAnimTime > ANIM_INTERVAL) {
|
|
736
754
|
animFrame++;
|
|
737
755
|
lastAnimTime = timestamp;
|
|
756
|
+
animFrameChanged = true; // 이동 시스템에 프레임 전환 알림
|
|
738
757
|
}
|
|
739
758
|
|
|
740
759
|
// 이동 모드에 따라 적절한 프레임셋으로 매핑
|
|
@@ -844,6 +863,305 @@ const PetEngine = (() => {
|
|
|
844
863
|
return thread;
|
|
845
864
|
}
|
|
846
865
|
|
|
866
|
+
// ===================================
|
|
867
|
+
// 커스텀 이동 패턴 레지스트리
|
|
868
|
+
// ===================================
|
|
869
|
+
|
|
870
|
+
// 등록된 커스텀 이동 패턴 저장소
|
|
871
|
+
// 각 핸들러: { init(params), update(deltaTime), isComplete(), cleanup() }
|
|
872
|
+
let customMovements = {};
|
|
873
|
+
let activeCustomMovement = null; // 현재 실행 중인 커스텀 이동 { name, handler, state }
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* 커스텀 이동 패턴 등록
|
|
877
|
+
* @param {string} name - 패턴 이름 (예: 'zigzag', 'patrol')
|
|
878
|
+
* @param {object} handler - { init, update, isComplete, cleanup }
|
|
879
|
+
*/
|
|
880
|
+
function registerMovement(name, handler) {
|
|
881
|
+
if (!handler || typeof handler.update !== 'function') {
|
|
882
|
+
console.error(`[PetEngine] 이동 패턴 '${name}' 등록 실패: update 함수 필수`);
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
// 기본 메서드 보완
|
|
886
|
+
handler.init = handler.init || (() => {});
|
|
887
|
+
handler.isComplete = handler.isComplete || (() => false);
|
|
888
|
+
handler.cleanup = handler.cleanup || (() => {});
|
|
889
|
+
customMovements[name] = handler;
|
|
890
|
+
console.log(`[PetEngine] 커스텀 이동 패턴 등록: ${name}`);
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* 등록된 커스텀 이동 패턴 제거
|
|
896
|
+
* @param {string} name - 패턴 이름
|
|
897
|
+
*/
|
|
898
|
+
function unregisterMovement(name) {
|
|
899
|
+
if (activeCustomMovement && activeCustomMovement.name === name) {
|
|
900
|
+
stopCustomMovement();
|
|
901
|
+
}
|
|
902
|
+
delete customMovements[name];
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* 커스텀 이동 패턴 실행
|
|
907
|
+
* @param {string} name - 등록된 패턴 이름
|
|
908
|
+
* @param {object} params - 패턴 초기화 파라미터
|
|
909
|
+
* @returns {boolean} 실행 성공 여부
|
|
910
|
+
*/
|
|
911
|
+
function executeCustomMovement(name, params = {}) {
|
|
912
|
+
const handler = customMovements[name];
|
|
913
|
+
if (!handler) {
|
|
914
|
+
console.warn(`[PetEngine] 미등록 이동 패턴: ${name}`);
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// 기존 커스텀 이동이 있으면 정리
|
|
919
|
+
if (activeCustomMovement) {
|
|
920
|
+
activeCustomMovement.handler.cleanup();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// 패턴 초기화 시 현재 위치/화면 정보 전달
|
|
924
|
+
const context = {
|
|
925
|
+
x, y, screenW, screenH,
|
|
926
|
+
charSize: CHAR_SIZE,
|
|
927
|
+
direction, edge, flipX,
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const state = handler.init(Object.assign({}, params, context)) || {};
|
|
931
|
+
activeCustomMovement = { name, handler, state };
|
|
932
|
+
|
|
933
|
+
// CUSTOM 상태로 전환
|
|
934
|
+
if (typeof StateMachine !== 'undefined') {
|
|
935
|
+
StateMachine.forceState('custom');
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
console.log(`[PetEngine] 커스텀 이동 실행: ${name}`);
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* 커스텀 이동 매 프레임 갱신
|
|
944
|
+
* @param {number} deltaTime - 프레임 간 경과 시간 (ms)
|
|
945
|
+
*/
|
|
946
|
+
function updateCustomMovement(deltaTime) {
|
|
947
|
+
if (!activeCustomMovement) return;
|
|
948
|
+
|
|
949
|
+
const { handler, state } = activeCustomMovement;
|
|
950
|
+
const context = {
|
|
951
|
+
x, y, screenW, screenH,
|
|
952
|
+
charSize: CHAR_SIZE,
|
|
953
|
+
direction, edge, flipX,
|
|
954
|
+
setPos: (nx, ny) => { x = nx; y = ny; },
|
|
955
|
+
setFlip: (f) => { flipX = f; },
|
|
956
|
+
setDir: (d) => { direction = d; },
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
handler.update(deltaTime, state, context);
|
|
960
|
+
|
|
961
|
+
// 핸들러가 setPos로 위치를 설정했을 수 있음
|
|
962
|
+
clampPosition();
|
|
963
|
+
|
|
964
|
+
// 완료 확인
|
|
965
|
+
if (handler.isComplete(state)) {
|
|
966
|
+
stopCustomMovement();
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* 현재 커스텀 이동 강제 중지 → IDLE 복귀
|
|
972
|
+
*/
|
|
973
|
+
function stopCustomMovement() {
|
|
974
|
+
if (!activeCustomMovement) return;
|
|
975
|
+
activeCustomMovement.handler.cleanup(activeCustomMovement.state);
|
|
976
|
+
activeCustomMovement = null;
|
|
977
|
+
|
|
978
|
+
if (typeof StateMachine !== 'undefined') {
|
|
979
|
+
StateMachine.forceState('idle');
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* 등록된 커스텀 이동 패턴 목록 반환
|
|
985
|
+
*/
|
|
986
|
+
function getRegisteredMovements() {
|
|
987
|
+
return Object.keys(customMovements);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// --- 사전 등록 이동 패턴 ---
|
|
991
|
+
|
|
992
|
+
// 지그재그: 대각선 방향 교대로 이동
|
|
993
|
+
registerMovement('zigzag', {
|
|
994
|
+
init(params) {
|
|
995
|
+
return {
|
|
996
|
+
amplitude: params.amplitude || 40, // 좌우 진폭 (px)
|
|
997
|
+
speed: params.speed || 2, // 전진 속도
|
|
998
|
+
segmentLength: params.segmentLength || 60, // 한 구간 길이
|
|
999
|
+
traveled: 0,
|
|
1000
|
+
totalDistance: params.distance || 300, // 총 이동 거리
|
|
1001
|
+
zigDir: 1, // 지그재그 방향
|
|
1002
|
+
startX: params.x,
|
|
1003
|
+
startY: params.y,
|
|
1004
|
+
};
|
|
1005
|
+
},
|
|
1006
|
+
update(dt, state, ctx) {
|
|
1007
|
+
const step = state.speed * (dt / 16);
|
|
1008
|
+
state.traveled += step;
|
|
1009
|
+
|
|
1010
|
+
// 수평 전진
|
|
1011
|
+
const moveX = step * (ctx.direction || 1);
|
|
1012
|
+
// 수직 지그재그
|
|
1013
|
+
const segProgress = (state.traveled % state.segmentLength) / state.segmentLength;
|
|
1014
|
+
if (segProgress < 0.05) state.zigDir *= -1;
|
|
1015
|
+
const moveY = state.zigDir * step * 0.7;
|
|
1016
|
+
|
|
1017
|
+
ctx.setPos(ctx.x + moveX, ctx.y + moveY);
|
|
1018
|
+
ctx.setFlip(moveX < 0);
|
|
1019
|
+
},
|
|
1020
|
+
isComplete(state) {
|
|
1021
|
+
return state.traveled >= state.totalDistance;
|
|
1022
|
+
},
|
|
1023
|
+
cleanup() {},
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
// 순찰: 두 지점 사이를 왕복
|
|
1027
|
+
registerMovement('patrol', {
|
|
1028
|
+
init(params) {
|
|
1029
|
+
return {
|
|
1030
|
+
pointA: { x: params.pointAX || 100, y: params.pointAY || params.y },
|
|
1031
|
+
pointB: { x: params.pointBX || params.screenW - 164, y: params.pointBY || params.y },
|
|
1032
|
+
speed: params.speed || 1.5,
|
|
1033
|
+
laps: params.laps || 3, // 왕복 횟수
|
|
1034
|
+
currentLap: 0,
|
|
1035
|
+
targetIdx: 0, // 0=A, 1=B
|
|
1036
|
+
};
|
|
1037
|
+
},
|
|
1038
|
+
update(dt, state, ctx) {
|
|
1039
|
+
const target = state.targetIdx === 0 ? state.pointA : state.pointB;
|
|
1040
|
+
const dx = target.x - ctx.x;
|
|
1041
|
+
const dy = target.y - ctx.y;
|
|
1042
|
+
const dist = Math.hypot(dx, dy);
|
|
1043
|
+
|
|
1044
|
+
if (dist < 5) {
|
|
1045
|
+
// 목표 도달 → 방향 전환
|
|
1046
|
+
state.targetIdx = 1 - state.targetIdx;
|
|
1047
|
+
if (state.targetIdx === 0) state.currentLap++;
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const step = state.speed * (dt / 16);
|
|
1052
|
+
const ratio = step / dist;
|
|
1053
|
+
ctx.setPos(ctx.x + dx * ratio, ctx.y + dy * ratio);
|
|
1054
|
+
ctx.setFlip(dx < 0);
|
|
1055
|
+
},
|
|
1056
|
+
isComplete(state) {
|
|
1057
|
+
return state.currentLap >= state.laps;
|
|
1058
|
+
},
|
|
1059
|
+
cleanup() {},
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// 원형 회전: 중심점 기준 회전
|
|
1063
|
+
registerMovement('circle', {
|
|
1064
|
+
init(params) {
|
|
1065
|
+
return {
|
|
1066
|
+
centerX: params.centerX || params.x,
|
|
1067
|
+
centerY: params.centerY || params.y - 50,
|
|
1068
|
+
radius: params.radius || 50,
|
|
1069
|
+
speed: params.speed || 0.03, // 각속도 (rad/frame)
|
|
1070
|
+
angle: 0,
|
|
1071
|
+
totalAngle: params.revolutions ? params.revolutions * Math.PI * 2 : Math.PI * 4,
|
|
1072
|
+
traveled: 0,
|
|
1073
|
+
};
|
|
1074
|
+
},
|
|
1075
|
+
update(dt, state, ctx) {
|
|
1076
|
+
const step = state.speed * (dt / 16);
|
|
1077
|
+
state.angle += step;
|
|
1078
|
+
state.traveled += Math.abs(step);
|
|
1079
|
+
|
|
1080
|
+
const nx = state.centerX + Math.cos(state.angle) * state.radius;
|
|
1081
|
+
const ny = state.centerY + Math.sin(state.angle) * state.radius;
|
|
1082
|
+
ctx.setPos(nx, ny);
|
|
1083
|
+
ctx.setFlip(Math.sin(state.angle) < 0);
|
|
1084
|
+
},
|
|
1085
|
+
isComplete(state) {
|
|
1086
|
+
return state.traveled >= state.totalAngle;
|
|
1087
|
+
},
|
|
1088
|
+
cleanup() {},
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
// 떨기: 빠르게 좌우 진동
|
|
1092
|
+
registerMovement('shake', {
|
|
1093
|
+
init(params) {
|
|
1094
|
+
return {
|
|
1095
|
+
intensity: params.intensity || 4, // 떨림 강도 (px)
|
|
1096
|
+
duration: params.duration || 800, // 지속 시간 (ms)
|
|
1097
|
+
elapsed: 0,
|
|
1098
|
+
originX: params.x,
|
|
1099
|
+
originY: params.y,
|
|
1100
|
+
phase: 0,
|
|
1101
|
+
};
|
|
1102
|
+
},
|
|
1103
|
+
update(dt, state, ctx) {
|
|
1104
|
+
state.elapsed += dt;
|
|
1105
|
+
state.phase += dt * 0.05;
|
|
1106
|
+
|
|
1107
|
+
// 감쇠하는 사인파 진동
|
|
1108
|
+
const decay = 1 - (state.elapsed / state.duration);
|
|
1109
|
+
const offsetX = Math.sin(state.phase) * state.intensity * decay;
|
|
1110
|
+
ctx.setPos(state.originX + offsetX, state.originY);
|
|
1111
|
+
},
|
|
1112
|
+
isComplete(state) {
|
|
1113
|
+
return state.elapsed >= state.duration;
|
|
1114
|
+
},
|
|
1115
|
+
cleanup() {},
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// 댄스: 여러 동작을 연속 수행 (점프 + 회전 + 떨기 조합)
|
|
1119
|
+
registerMovement('dance', {
|
|
1120
|
+
init(params) {
|
|
1121
|
+
return {
|
|
1122
|
+
duration: params.duration || 3000,
|
|
1123
|
+
elapsed: 0,
|
|
1124
|
+
originX: params.x,
|
|
1125
|
+
originY: params.y,
|
|
1126
|
+
phase: 0,
|
|
1127
|
+
};
|
|
1128
|
+
},
|
|
1129
|
+
update(dt, state, ctx) {
|
|
1130
|
+
state.elapsed += dt;
|
|
1131
|
+
state.phase += dt * 0.004;
|
|
1132
|
+
|
|
1133
|
+
const t = state.elapsed / state.duration;
|
|
1134
|
+
|
|
1135
|
+
// 구간별 다른 동작
|
|
1136
|
+
if (t < 0.25) {
|
|
1137
|
+
// 구간 1: 좌우 스윙
|
|
1138
|
+
const swingX = Math.sin(state.phase * 8) * 20;
|
|
1139
|
+
ctx.setPos(state.originX + swingX, state.originY);
|
|
1140
|
+
ctx.setFlip(swingX < 0);
|
|
1141
|
+
} else if (t < 0.5) {
|
|
1142
|
+
// 구간 2: 상하 바운스
|
|
1143
|
+
const bounceY = Math.abs(Math.sin(state.phase * 6)) * -30;
|
|
1144
|
+
ctx.setPos(state.originX, state.originY + bounceY);
|
|
1145
|
+
} else if (t < 0.75) {
|
|
1146
|
+
// 구간 3: 작은 원
|
|
1147
|
+
const angle = state.phase * 10;
|
|
1148
|
+
ctx.setPos(
|
|
1149
|
+
state.originX + Math.cos(angle) * 15,
|
|
1150
|
+
state.originY + Math.sin(angle) * 15
|
|
1151
|
+
);
|
|
1152
|
+
ctx.setFlip(Math.cos(angle) < 0);
|
|
1153
|
+
} else {
|
|
1154
|
+
// 구간 4: 빠른 좌우 떨기 (피니시)
|
|
1155
|
+
const shake = Math.sin(state.phase * 20) * 6 * (1 - t);
|
|
1156
|
+
ctx.setPos(state.originX + shake, state.originY);
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
isComplete(state) {
|
|
1160
|
+
return state.elapsed >= state.duration;
|
|
1161
|
+
},
|
|
1162
|
+
cleanup() {},
|
|
1163
|
+
});
|
|
1164
|
+
|
|
847
1165
|
// ===================================
|
|
848
1166
|
// 메인 루프
|
|
849
1167
|
// ===================================
|
|
@@ -853,17 +1171,37 @@ const PetEngine = (() => {
|
|
|
853
1171
|
/**
|
|
854
1172
|
* 엔진 시작: requestAnimationFrame 루프 가동
|
|
855
1173
|
*/
|
|
1174
|
+
let lastLoopTimestamp = 0; // 이전 루프 타임스탬프 (deltaTime 계산용)
|
|
1175
|
+
|
|
856
1176
|
function start() {
|
|
857
1177
|
if (running) return;
|
|
858
1178
|
running = true;
|
|
859
1179
|
lastAnimTime = performance.now();
|
|
860
|
-
|
|
1180
|
+
lastLoopTimestamp = performance.now();
|
|
861
1181
|
|
|
862
1182
|
function loop(timestamp) {
|
|
863
1183
|
if (!running) return;
|
|
1184
|
+
|
|
1185
|
+
// 커스텀 이동용 deltaTime 계산
|
|
1186
|
+
const deltaTime = timestamp - lastLoopTimestamp;
|
|
1187
|
+
lastLoopTimestamp = timestamp;
|
|
1188
|
+
|
|
864
1189
|
const state = StateMachine.update();
|
|
865
|
-
|
|
1190
|
+
|
|
1191
|
+
// 애니메이션을 먼저 갱신 → animFrameChanged 플래그 설정
|
|
866
1192
|
updateAnimation(state, timestamp);
|
|
1193
|
+
|
|
1194
|
+
// 커스텀 이동이 활성 상태이면 전용 업데이트 실행
|
|
1195
|
+
if (activeCustomMovement && state === 'custom') {
|
|
1196
|
+
updateCustomMovement(deltaTime);
|
|
1197
|
+
clampPosition();
|
|
1198
|
+
updateVisual();
|
|
1199
|
+
} else {
|
|
1200
|
+
moveForState(state);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// 프레임 전환 플래그 리셋 (다음 프레임까지 대기)
|
|
1204
|
+
animFrameChanged = false;
|
|
867
1205
|
frameId = requestAnimationFrame(loop);
|
|
868
1206
|
}
|
|
869
1207
|
frameId = requestAnimationFrame(loop);
|
|
@@ -883,9 +1221,13 @@ const PetEngine = (() => {
|
|
|
883
1221
|
getPosition, setPosition, setEdge, setDirection,
|
|
884
1222
|
snapToNearestEdge, setSpeedMultiplier,
|
|
885
1223
|
moveForState, updateAnimation,
|
|
886
|
-
//
|
|
1224
|
+
// 물리 기반 이동
|
|
887
1225
|
jumpTo, startRappel, releaseThread, moveToCenter,
|
|
888
1226
|
setSurfaces, getThread, startFalling,
|
|
1227
|
+
// 커스텀 이동 패턴 시스템
|
|
1228
|
+
registerMovement, unregisterMovement,
|
|
1229
|
+
executeCustomMovement, stopCustomMovement,
|
|
1230
|
+
getRegisteredMovements,
|
|
889
1231
|
CHAR_SIZE,
|
|
890
1232
|
};
|
|
891
1233
|
})();
|
|
@@ -23,6 +23,7 @@ const StateMachine = (() => {
|
|
|
23
23
|
JUMPING: 'jumping', // 포물선 점프 중 (물리 엔진이 제어)
|
|
24
24
|
RAPPELLING: 'rappelling', // 실(thread)을 타고 하강 중
|
|
25
25
|
FALLING: 'falling', // 중력에 의한 자유 낙하 중
|
|
26
|
+
CUSTOM: 'custom', // 커스텀 이동 패턴 실행 중 (Movement Registry)
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
// 각 상태의 최소/최대 지속 시간(ms)
|
|
@@ -41,6 +42,7 @@ const StateMachine = (() => {
|
|
|
41
42
|
[STATES.JUMPING]: { min: 500, max: 2000 }, // 점프 비행 시간
|
|
42
43
|
[STATES.RAPPELLING]: { min: 2000, max: 8000 }, // 레펠 하강 시간
|
|
43
44
|
[STATES.FALLING]: { min: 200, max: 1000 }, // 낙하 시간
|
|
45
|
+
[STATES.CUSTOM]: { min: 500, max: 30000 }, // 커스텀 이동 (패턴에 따라 가변)
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
let currentState = STATES.IDLE;
|
|
@@ -115,6 +117,11 @@ const StateMachine = (() => {
|
|
|
115
117
|
[STATES.FALLING]: [
|
|
116
118
|
{ state: STATES.IDLE, weight: 1.0 },
|
|
117
119
|
],
|
|
120
|
+
// 커스텀 이동 완료 후: idle 또는 walking
|
|
121
|
+
[STATES.CUSTOM]: [
|
|
122
|
+
{ state: STATES.IDLE, weight: 0.6 },
|
|
123
|
+
{ state: STATES.WALKING, weight: 0.4 },
|
|
124
|
+
],
|
|
118
125
|
};
|
|
119
126
|
|
|
120
127
|
function setPersonality(p) {
|
package/shared/messages.js
CHANGED
|
@@ -85,6 +85,116 @@ const MESSAGES = {
|
|
|
85
85
|
'오늘 날씨 어때?',
|
|
86
86
|
],
|
|
87
87
|
|
|
88
|
+
// 브라우저 감시 코멘트 (키워드 → 코멘트 배열)
|
|
89
|
+
browsing: {
|
|
90
|
+
// 쇼핑
|
|
91
|
+
shopping: {
|
|
92
|
+
keywords: ['쿠팡', 'coupang', '11번가', 'gmarket', 'g마켓', '옥션', 'auction', '위메프', '티몬', 'amazon', '아마존', '알리', 'aliexpress', '무신사', '올리브영', 'oliveyoung', '네이버 쇼핑', 'shopping'],
|
|
93
|
+
comments: [
|
|
94
|
+
'또 쇼핑해? 지갑이 울고 있어...',
|
|
95
|
+
'뭐 사려고? 나도 보여줘!',
|
|
96
|
+
'충동구매 주의보!',
|
|
97
|
+
'이번엔 뭘 질러?',
|
|
98
|
+
'장바구니 비울 생각은 없어?',
|
|
99
|
+
'택배 언제 와? 기다려진다!',
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
// 유튜브/동영상
|
|
103
|
+
video: {
|
|
104
|
+
keywords: ['youtube', '유튜브', 'twitch', '트위치', 'netflix', '넷플릭스', 'disney+', '디즈니', 'wavve', '웨이브', 'tving', '티빙', 'watcha', '왓챠'],
|
|
105
|
+
comments: [
|
|
106
|
+
'뭐 보는 거야? 나도 같이 볼래!',
|
|
107
|
+
'또 영상 보고 있구나~',
|
|
108
|
+
'이거 재미있어?',
|
|
109
|
+
'자막 켜줘, 나도 보게!',
|
|
110
|
+
'한 편만 더... 그러다 새벽이다!',
|
|
111
|
+
'구독 눌렀어?',
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
// SNS
|
|
115
|
+
sns: {
|
|
116
|
+
keywords: ['instagram', '인스타', 'twitter', 'x.com', '트위터', 'facebook', '페이스북', 'threads', '쓰레드', 'tiktok', '틱톡', 'reddit'],
|
|
117
|
+
comments: [
|
|
118
|
+
'SNS 또 보는 거야? 현실에 나도 있는데!',
|
|
119
|
+
'좋아요 누르고 있지?',
|
|
120
|
+
'무한 스크롤 주의!',
|
|
121
|
+
'댓글 달지 마, 싸움 나!',
|
|
122
|
+
'나 사진도 찍어줘~',
|
|
123
|
+
'피드 보느라 나 무시하지 마!',
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
// 뉴스
|
|
127
|
+
news: {
|
|
128
|
+
keywords: ['뉴스', 'news', 'naver.com', '다음', 'daum', '한겨레', '조선일보', '중앙일보', 'bbc', 'cnn', '연합뉴스'],
|
|
129
|
+
comments: [
|
|
130
|
+
'세상에 무슨 일이야?',
|
|
131
|
+
'뉴스 읽고 있구나... 오늘 뭔 일 있어?',
|
|
132
|
+
'나쁜 뉴스만 보지 마, 기분 나빠져!',
|
|
133
|
+
'좋은 소식 있으면 알려줘!',
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
// 개발/프로그래밍
|
|
137
|
+
dev: {
|
|
138
|
+
keywords: ['github', 'gitlab', 'stackoverflow', 'stack overflow', 'vscode', 'codepen', 'npm', 'developer', '개발', 'documentation', 'docs', 'api'],
|
|
139
|
+
comments: [
|
|
140
|
+
'코딩하고 있구나! 멋져!',
|
|
141
|
+
'에러 나면 나한테 말해, 위로해줄게!',
|
|
142
|
+
'버그 잡고 있어? 화이팅!',
|
|
143
|
+
'Stack Overflow 복붙은 개발의 기본이지!',
|
|
144
|
+
'커밋은 자주 해야 해!',
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
// 검색
|
|
148
|
+
search: {
|
|
149
|
+
keywords: ['google.com/search', '구글', 'google', 'naver.com/search', '네이버 검색', 'bing', '검색'],
|
|
150
|
+
comments: [
|
|
151
|
+
'뭐 찾고 있어?',
|
|
152
|
+
'검색하면 다 나와~',
|
|
153
|
+
'나한테 물어보지 왜 검색해!',
|
|
154
|
+
'궁금한 게 있어?',
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
// 게임
|
|
158
|
+
game: {
|
|
159
|
+
keywords: ['steam', 'epic games', '리그 오브', 'league of', 'valorant', '발로란트', '오버워치', 'overwatch', 'minecraft', '마인크래프트', 'roblox', '게임'],
|
|
160
|
+
comments: [
|
|
161
|
+
'게임하고 있구나! 나도 끼워줘!',
|
|
162
|
+
'이겨야 해! 파이팅!',
|
|
163
|
+
'한 판만 더... 라고 했잖아!',
|
|
164
|
+
'게임 중독 주의보~',
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
// 음악
|
|
168
|
+
music: {
|
|
169
|
+
keywords: ['spotify', '스포티파이', 'melon', '멜론', 'genie', '지니', 'bugs', '벅스', 'apple music', 'soundcloud'],
|
|
170
|
+
comments: [
|
|
171
|
+
'뭐 듣고 있어? 좋은 거야?',
|
|
172
|
+
'나도 들려줘~',
|
|
173
|
+
'노래 취향 좋다!',
|
|
174
|
+
'이 노래 좋아? 나도!',
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
// 메일
|
|
178
|
+
mail: {
|
|
179
|
+
keywords: ['gmail', 'outlook', 'mail', '메일', 'naver mail', '네이버 메일'],
|
|
180
|
+
comments: [
|
|
181
|
+
'메일 확인 중이구나~',
|
|
182
|
+
'중요한 메일 있어?',
|
|
183
|
+
'스팸 조심해!',
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
// 일반 브라우저 감지 (다른 카테고리에 해당 안 될 때)
|
|
187
|
+
general: {
|
|
188
|
+
keywords: ['chrome', 'edge', 'firefox', 'safari', 'brave', 'whale', '웨일'],
|
|
189
|
+
comments: [
|
|
190
|
+
'인터넷 서핑 중이구나~',
|
|
191
|
+
'재미있는 거 있으면 알려줘!',
|
|
192
|
+
'탭 좀 정리해... 너무 많아!',
|
|
193
|
+
'와이파이 잘 돼?',
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
|
|
88
198
|
// 진화 관련 메시지
|
|
89
199
|
evolution: {
|
|
90
200
|
stage_1: '뭔가 변하는 느낌이야...!',
|