clawmate 1.3.0 → 1.4.1

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 (43) hide show
  1. package/electron-builder.yml +1 -1
  2. package/index.js +589 -406
  3. package/main/ai-bridge.js +64 -58
  4. package/main/ai-connector.js +67 -62
  5. package/main/autostart.js +7 -7
  6. package/main/desktop-path.js +4 -4
  7. package/main/file-command-parser.js +77 -41
  8. package/main/file-ops.js +27 -27
  9. package/main/index.js +18 -16
  10. package/main/ipc-handlers.js +27 -24
  11. package/main/manifest.js +2 -2
  12. package/main/platform.js +16 -16
  13. package/main/smart-file-ops.js +64 -64
  14. package/main/store.js +1 -1
  15. package/main/telegram.js +154 -121
  16. package/main/tray.js +226 -71
  17. package/main/updater.js +13 -13
  18. package/openclaw.plugin.json +1 -1
  19. package/package.json +3 -4
  20. package/preload/preload.js +18 -18
  21. package/renderer/css/effects.css +6 -6
  22. package/renderer/css/pet.css +8 -8
  23. package/renderer/css/speech.css +5 -5
  24. package/renderer/first-run.html +15 -15
  25. package/renderer/index.html +4 -4
  26. package/renderer/js/ai-controller.js +99 -88
  27. package/renderer/js/app.js +26 -23
  28. package/renderer/js/browser-watcher.js +32 -32
  29. package/renderer/js/character.js +33 -33
  30. package/renderer/js/interactions.js +57 -14
  31. package/renderer/js/memory.js +144 -37
  32. package/renderer/js/metrics.js +141 -141
  33. package/renderer/js/mode-manager.js +59 -15
  34. package/renderer/js/pet-engine.js +236 -236
  35. package/renderer/js/speech.js +19 -19
  36. package/renderer/js/state-machine.js +23 -23
  37. package/renderer/js/time-aware.js +15 -15
  38. package/renderer/launcher.html +9 -9
  39. package/shared/constants.js +11 -11
  40. package/shared/messages.js +130 -130
  41. package/shared/personalities.js +72 -37
  42. package/skills/launch-pet/index.js +13 -13
  43. package/skills/launch-pet/skill.json +12 -23
@@ -1,72 +1,72 @@
1
1
  /**
2
- * 핵심 이동/물리 엔진 (리뉴얼)
3
- * requestAnimationFrame 기반 스텝 이동 + 점프 + 레펠 + 중력 낙하
2
+ * Core movement/physics engine (renewed)
3
+ * requestAnimationFrame based -- step movement + jump + rappel + gravity fall
4
4
  *
5
- * 이동 모드:
6
- * crawling 표면 위에서 한발자국씩 기어가는 이동
7
- * jumping 포물선 궤도 점프
8
- * falling 중력에 의한 자유 낙하
9
- * rappelling 실(thread)을 타고 진자운동하며 하강
5
+ * Movement modes:
6
+ * crawling -- step-by-step crawling on surfaces
7
+ * jumping -- parabolic trajectory jump
8
+ * falling -- gravity-based free fall
9
+ * rappelling -- pendulum swing descent on a thread
10
10
  */
11
11
  const PetEngine = (() => {
12
- // --- 물리 상수 ---
13
- const GRAVITY = 0.3; // 중력 가속도 (px/frame^2)
14
- const STEP_SIZE = 4; // 걸음 크기 (px)
15
- const JUMP_VX = 3; // 점프 수평 초기 속도
16
- const JUMP_VY = -7; // 점프 수직 초기 속도 (위로)
17
- const BOUNCE_FACTOR = 0.3; // 착지 바운스 계수
18
- const CHAR_SIZE = 64; // 캐릭터 크기 (px)
19
- const ANIM_INTERVAL = 150; // 애니메이션 프레임 전환 간격 (ms) 부드러운 전환
20
- const THREAD_SPEED = 0.8; // 레펠 하강 속도 (px/frame)
21
-
22
- // --- 위치 속도 ---
12
+ // --- Physics constants ---
13
+ const GRAVITY = 0.3; // Gravity acceleration (px/frame^2)
14
+ const STEP_SIZE = 4; // Step size (px)
15
+ const JUMP_VX = 3; // Jump horizontal initial velocity
16
+ const JUMP_VY = -7; // Jump vertical initial velocity (upward)
17
+ const BOUNCE_FACTOR = 0.3; // Landing bounce factor
18
+ const CHAR_SIZE = 64; // Character size (px)
19
+ const ANIM_INTERVAL = 150; // Animation frame transition interval (ms) -- smooth transition
20
+ const THREAD_SPEED = 0.8; // Rappel descent speed (px/frame)
21
+
22
+ // --- Position and velocity ---
23
23
  let x = 0, y = 0;
24
- let vx = 0, vy = 0; // 현재 속도 벡터
25
-
26
- // --- 표면/방향 ---
27
- let edge = 'bottom'; // 현재 부착된 가장자리 (bottom, left, right, top, surface)
28
- let direction = 1; // 이동 방향: 1=오른쪽/아래, -1=왼쪽/위
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 전환 시작 시각
24
+ let vx = 0, vy = 0; // Current velocity vector
25
+
26
+ // --- Surface/direction ---
27
+ let edge = 'bottom'; // Currently attached edge (bottom, left, right, top, surface)
28
+ let direction = 1; // Movement direction: 1=right/down, -1=left/up
29
+ let flipX = false; // Character horizontal flip
30
+ let prevFlipX = false; // Previous frame flipX (for transition detection)
31
+ let flipTransition = 0; // flipX transition progress (0~1, 1 = complete)
32
+ const FLIP_DURATION = 120; // flipX transition duration (ms)
33
+ let flipStartTime = 0; // flipX transition start time
34
34
  let screenW, screenH;
35
35
 
36
- // --- 엔진 상태 ---
36
+ // --- Engine state ---
37
37
  let running = false;
38
38
  let petContainer = null;
39
39
  let speedMultiplier = 1.0;
40
40
  let animFrame = 0;
41
41
  let lastAnimTime = 0;
42
- let animFrameChanged = false; // 애니메이션 프레임 전환 플래그 (이동과 동기화)
42
+ let animFrameChanged = false; // Animation frame change flag (synced with movement)
43
43
 
44
- // --- 이동 모드 ---
44
+ // --- Movement mode ---
45
45
  let movementMode = 'crawling'; // crawling | jumping | falling | rappelling
46
- let onSurface = true; // 표면 위에 있는지 여부
46
+ let onSurface = true; // Whether on a surface
47
47
 
48
- // --- 스텝 시스템 (애니메이션 프레임 동기화) ---
48
+ // --- Step system (animation frame sync) ---
49
49
 
50
- // --- 레펠(thread) 시스템 ---
51
- // 부착점에서 실을 내려 진자운동하며 하강
50
+ // --- Rappel (thread) system ---
51
+ // Pendulum swing descent from attachment point
52
52
  let thread = null; // { attachX, attachY, length, angle, swingVel } | null
53
53
 
54
- // --- 윈도우 표면 목록 ---
55
- // 외부 윈도우의 타이틀바 등을 추가 표면으로 등록
54
+ // --- Window surface list ---
55
+ // Register external window title bars etc. as additional surfaces
56
56
  let windowSurfaces = []; // [{ id, x, y, width, height }]
57
57
 
58
- // --- 착지 현재 올라가 있는 표면 참조 ---
58
+ // --- Reference to current surface when landed ---
59
59
  let currentSurface = null;
60
60
 
61
61
  /**
62
- * 초기화: 컨테이너 설정 화면 하단 중앙 배치
62
+ * Initialization: set up container and place at bottom-center of screen
63
63
  */
64
64
  function init(container) {
65
65
  petContainer = container;
66
66
  screenW = window.innerWidth;
67
67
  screenH = window.innerHeight;
68
68
 
69
- // 화면 하단 중앙에서 시작
69
+ // Start at bottom-center of screen
70
70
  x = (screenW - CHAR_SIZE) / 2;
71
71
  y = screenH - CHAR_SIZE;
72
72
  edge = 'bottom';
@@ -76,7 +76,7 @@ const PetEngine = (() => {
76
76
  currentSurface = null;
77
77
  updateVisual();
78
78
 
79
- // 화면 크기 변경 대응
79
+ // Handle window resize
80
80
  window.addEventListener('resize', () => {
81
81
  screenW = window.innerWidth;
82
82
  screenH = window.innerHeight;
@@ -86,14 +86,14 @@ const PetEngine = (() => {
86
86
  }
87
87
 
88
88
  /**
89
- * 속도 배율 설정 (성격/진화 단계에 따른 속도 조절)
89
+ * Set speed multiplier (adjusts speed based on personality/evolution stage)
90
90
  */
91
91
  function setSpeedMultiplier(mult) {
92
92
  speedMultiplier = mult;
93
93
  }
94
94
 
95
95
  /**
96
- * 화면 경계 내로 위치 제한
96
+ * Clamp position within screen bounds
97
97
  */
98
98
  function clampPosition() {
99
99
  x = Math.max(0, Math.min(x, screenW - CHAR_SIZE));
@@ -101,24 +101,24 @@ const PetEngine = (() => {
101
101
  }
102
102
 
103
103
  // ===================================
104
- // 시각적 업데이트
104
+ // Visual update
105
105
  // ===================================
106
106
 
107
107
  /**
108
- * 컨테이너 위치 회전/반전 업데이트
109
- * 가장자리별로 캐릭터가 올바른 방향을 향하도록 transform 적용
108
+ * Update container position and rotation/flip
109
+ * Apply transform so character faces correct direction per edge
110
110
  *
111
- * EDGE_OFFSET: 스프라이트 테두리의 픽셀(4px) 보정하여
112
- * 다리가 실제로 벽면/바닥/천장에 밀착하도록 렌더링 위치 조정
111
+ * EDGE_OFFSET: Compensates for empty pixels (4px) at sprite border
112
+ * so legs render flush against wall/floor/ceiling
113
113
  */
114
114
  const EDGE_OFFSET = 4;
115
115
 
116
116
  function updateVisual() {
117
117
  if (!petContainer) return;
118
118
 
119
- // --- flipX 부드러운 전환 처리 ---
119
+ // --- Smooth flipX transition handling ---
120
120
  if (flipX !== prevFlipX) {
121
- // 방향이 바뀌었으면 전환 시작
121
+ // Start transition when direction changed
122
122
  flipStartTime = Date.now();
123
123
  flipTransition = 0;
124
124
  prevFlipX = flipX;
@@ -128,7 +128,7 @@ const PetEngine = (() => {
128
128
  flipTransition = Math.min(1, elapsed / FLIP_DURATION);
129
129
  }
130
130
 
131
- // 전환 중일 때 CSS transition 적용
131
+ // Apply CSS transition during flip
132
132
  if (flipTransition < 1) {
133
133
  petContainer.style.transition = `transform ${FLIP_DURATION}ms ease-in-out`;
134
134
  } else {
@@ -138,21 +138,21 @@ const PetEngine = (() => {
138
138
  let renderX = x;
139
139
  let renderY = y;
140
140
 
141
- // 표면에 붙어있을 때만 오프셋 적용 (공중 상태에서는 불필요)
141
+ // Apply offset only when attached to surface (not needed in mid-air)
142
142
  if (onSurface && movementMode === 'crawling') {
143
143
  switch (edge) {
144
144
  case 'bottom':
145
145
  case 'surface':
146
- renderY += EDGE_OFFSET; // 바닥: 다리를 아래로 밀착
146
+ renderY += EDGE_OFFSET; // Floor: press legs down
147
147
  break;
148
148
  case 'top':
149
- renderY -= EDGE_OFFSET; // 천장: 다리를 위로 밀착
149
+ renderY -= EDGE_OFFSET; // Ceiling: press legs up
150
150
  break;
151
151
  case 'left':
152
- renderX -= EDGE_OFFSET; // 왼쪽 벽: 다리를 왼쪽으로 밀착
152
+ renderX -= EDGE_OFFSET; // Left wall: press legs left
153
153
  break;
154
154
  case 'right':
155
- renderX += EDGE_OFFSET; // 오른쪽 벽: 다리를 오른쪽으로 밀착
155
+ renderX += EDGE_OFFSET; // Right wall: press legs right
156
156
  break;
157
157
  }
158
158
  }
@@ -163,41 +163,41 @@ const PetEngine = (() => {
163
163
  let transform = '';
164
164
 
165
165
  if (movementMode === 'rappelling' || movementMode === 'jumping' || movementMode === 'falling') {
166
- // 공중 상태: 바닥 기준 기본 자세 (회전 없음)
166
+ // Mid-air: default floor-based pose (no rotation)
167
167
  if (flipX) transform = 'scaleX(-1)';
168
168
  } else if (edge === 'left') {
169
- // 왼쪽 벽: 다리가 왼쪽 가장자리를 향하도록 반시계 회전
169
+ // Left wall: rotate counter-clockwise so legs face left edge
170
170
  transform = 'rotate(-90deg)';
171
171
  if (flipX) transform += ' scaleX(-1)';
172
172
  } else if (edge === 'right') {
173
- // 오른쪽 벽: 다리가 오른쪽 가장자리를 향하도록 시계 회전
173
+ // Right wall: rotate clockwise so legs face right edge
174
174
  transform = 'rotate(90deg)';
175
175
  if (flipX) transform += ' scaleX(-1)';
176
176
  } else if (edge === 'top') {
177
- // 천장: 다리가 위를 향하도록 상하 반전
177
+ // Ceiling: flip vertically so legs face upward
178
178
  transform = 'scaleY(-1)';
179
179
  if (flipX) transform += ' scaleX(-1)';
180
180
  } else {
181
- // 바닥/표면: 기본 자세
181
+ // Floor/surface: default pose
182
182
  if (flipX) transform = 'scaleX(-1)';
183
183
  }
184
184
 
185
185
  petContainer.style.transform = transform || 'none';
186
186
 
187
- // 레펠 스레드 시각화 갱신
187
+ // Update rappel thread visualization
188
188
  updateThreadVisual();
189
189
  }
190
190
 
191
191
  /**
192
- * SVG 라인으로 레펠 실(thread) 시각화
193
- * thread 없으면 숨기고, 있으면 부착점 캐릭터 상단 연결
192
+ * Visualize rappel thread as SVG line
193
+ * Hide when no thread; when active, connect attachment point to character top
194
194
  */
195
195
  function updateThreadVisual() {
196
196
  const line = document.getElementById('thread-line');
197
197
  if (!line) return;
198
198
 
199
199
  if (!thread) {
200
- // 실이 없으면 숨김
200
+ // Hide when no thread
201
201
  line.setAttribute('x1', '0');
202
202
  line.setAttribute('y1', '0');
203
203
  line.setAttribute('x2', '0');
@@ -206,7 +206,7 @@ const PetEngine = (() => {
206
206
  return;
207
207
  }
208
208
 
209
- // 부착점에서 캐릭터 상단 중앙까지 표시
209
+ // Draw thread from attachment point to character top-center
210
210
  line.style.display = 'block';
211
211
  line.setAttribute('x1', thread.attachX);
212
212
  line.setAttribute('y1', thread.attachY);
@@ -217,56 +217,56 @@ const PetEngine = (() => {
217
217
  }
218
218
 
219
219
  // ===================================
220
- // 스텝 기반 이동 (뚝뚝 끊어 걷기)
220
+ // Step-based movement (choppy walking)
221
221
  // ===================================
222
222
 
223
223
  /**
224
- * 스텝 이동: 애니메이션 프레임 전환 시에만 걸음 이동
225
- * 다리 움직임(프레임 변화) 실제 위치 이동이 1:1 동기화되어
226
- * 다리가 움직일 때만 몸이 이동하는 자연스러운 보행 구현
224
+ * Step movement: move one step only when animation frame transitions
225
+ * Leg motion (frame change) and actual position movement are 1:1 synced
226
+ * so the body only moves when legs move, creating natural walking
227
227
  *
228
- * @param {number} stepScale - 스텝 크기 배율 (0.6 = 들고 느리게, 1.0 = 기본)
228
+ * @param {number} stepScale - Step size multiplier (0.6 = slow with load, 1.0 = default)
229
229
  */
230
230
  function stepMove(stepScale) {
231
- // 애니메이션 프레임이 바뀌지 않았으면 이동
231
+ // Do not move if animation frame has not changed
232
232
  if (!animFrameChanged) return;
233
233
 
234
- // 걸음 전진
234
+ // Advance one step
235
235
  const stepDist = STEP_SIZE * stepScale * speedMultiplier;
236
236
 
237
237
  if (edge === 'bottom' || edge === 'top' || edge === 'surface') {
238
- // 수평 이동 (바닥, 천장, 윈도우 표면)
238
+ // Horizontal movement (floor, ceiling, window surface)
239
239
  x += stepDist * direction;
240
240
  flipX = direction < 0;
241
241
  } else if (edge === 'left') {
242
- // 왼쪽 벽: y 이동 (direction=1이면 아래로, -1이면 위로)
242
+ // Left wall: y-axis movement (direction=1 goes down, -1 goes up)
243
243
  y += stepDist * direction;
244
244
  } else if (edge === 'right') {
245
- // 오른쪽 벽: y 이동
245
+ // Right wall: y-axis movement
246
246
  y += stepDist * direction;
247
247
  }
248
248
  }
249
249
 
250
250
  // ===================================
251
- // 윈도우 표면 탐지
251
+ // Window surface detection
252
252
  // ===================================
253
253
 
254
254
  /**
255
- * 주어진 위치 아래에 있는 윈도우 표면을 찾음
256
- * 캐릭터가 수평 범위 안에 있고, 표면 상단 근처에 있을 착지 가능
255
+ * Find window surface below a given position
256
+ * Landable when character is within horizontal range and near surface top
257
257
  *
258
- * @param {number} px - 캐릭터 x 좌표
259
- * @param {number} py - 캐릭터 하단 y 좌표 (y + CHAR_SIZE)
260
- * @returns {object|null} 착지 가능한 표면 또는 null
258
+ * @param {number} px - Character x coordinate
259
+ * @param {number} py - Character bottom y coordinate (y + CHAR_SIZE)
260
+ * @returns {object|null} Landable surface or null
261
261
  */
262
262
  function findSurfaceBelow(px, py) {
263
263
  let closest = null;
264
264
  let closestDist = Infinity;
265
265
 
266
266
  for (const s of windowSurfaces) {
267
- // 수평 범위 확인: 캐릭터가 표면 위에 겹치는지
267
+ // Check horizontal range: whether character overlaps surface
268
268
  if (px + CHAR_SIZE > s.x && px < s.x + s.width) {
269
- // 표면 상단에 근접했는지 (위에서 떨어지는 )
269
+ // Check if near surface top (falling from above)
270
270
  if (py >= s.y && py <= s.y + 10) {
271
271
  const dist = Math.abs(py - s.y);
272
272
  if (dist < closestDist) {
@@ -280,8 +280,8 @@ const PetEngine = (() => {
280
280
  }
281
281
 
282
282
  /**
283
- * 외부에서 윈도우 표면 목록 등록
284
- * (예: 다른 창의 타이틀바를 걸어다닐 있는 표면으로 설정)
283
+ * Register window surface list from external source
284
+ * (e.g., register title bars of other windows as walkable surfaces)
285
285
  *
286
286
  * @param {Array} surfaces - [{ id, x, y, width, height }]
287
287
  */
@@ -290,28 +290,28 @@ const PetEngine = (() => {
290
290
  }
291
291
 
292
292
  // ===================================
293
- // 물리 상태별 이동 처리
293
+ // Physics state-based movement handling
294
294
  // ===================================
295
295
 
296
296
  /**
297
- * 메인 이동 로직: movementMode에 따라 물리 연산 수행
297
+ * Main movement logic: perform physics calculations based on movementMode
298
298
  *
299
- * @param {string} state - StateMachine 현재 상태 (walking, idle )
299
+ * @param {string} state - Current StateMachine state (walking, idle, etc.)
300
300
  */
301
301
  function moveForState(state) {
302
302
  switch (movementMode) {
303
303
 
304
- // --- 포물선 점프 ---
304
+ // --- Parabolic jump ---
305
305
  case 'jumping':
306
- vy += GRAVITY; // 중력 적용
306
+ vy += GRAVITY; // Apply gravity
307
307
  x += vx;
308
308
  y += vy;
309
309
  flipX = vx < 0;
310
310
 
311
- // 바닥 착지 감지
311
+ // Floor landing detection
312
312
  if (y >= screenH - CHAR_SIZE) {
313
313
  y = screenH - CHAR_SIZE;
314
- // 바운스 효과: 약간 튕김
314
+ // Bounce effect: slight rebound
315
315
  if (Math.abs(vy) > 2) {
316
316
  vy = -vy * BOUNCE_FACTOR;
317
317
  } else {
@@ -324,7 +324,7 @@ const PetEngine = (() => {
324
324
  }
325
325
  }
326
326
 
327
- // 윈도우 표면 착지 감지 (아래로 떨어지는 중일 때만)
327
+ // Window surface landing detection (only while falling downward)
328
328
  if (vy > 0 && movementMode === 'jumping') {
329
329
  const landSurface = findSurfaceBelow(x, y + CHAR_SIZE);
330
330
  if (landSurface) {
@@ -338,7 +338,7 @@ const PetEngine = (() => {
338
338
  }
339
339
  }
340
340
 
341
- // 벽/천장 충돌 해당 가장자리에 붙음
341
+ // Wall/ceiling collision -> attach to that edge
342
342
  if (x <= 0 && movementMode === 'jumping') {
343
343
  x = 0;
344
344
  edge = 'left';
@@ -347,7 +347,7 @@ const PetEngine = (() => {
347
347
  currentSurface = null;
348
348
  vx = 0;
349
349
  vy = 0;
350
- direction = 1; // 아래쪽 방향
350
+ direction = 1; // Downward direction
351
351
  }
352
352
  if (x >= screenW - CHAR_SIZE && movementMode === 'jumping') {
353
353
  x = screenW - CHAR_SIZE;
@@ -367,16 +367,16 @@ const PetEngine = (() => {
367
367
  currentSurface = null;
368
368
  vx = 0;
369
369
  vy = 0;
370
- direction = 1; // 오른쪽 방향
370
+ direction = 1; // Rightward direction
371
371
  }
372
372
  break;
373
373
 
374
- // --- 자유 낙하 (중력) ---
374
+ // --- Free fall (gravity) ---
375
375
  case 'falling':
376
376
  vy += GRAVITY;
377
377
  y += vy;
378
378
 
379
- // 윈도우 표면 착지 감지
379
+ // Window surface landing detection
380
380
  if (vy > 0) {
381
381
  const fallSurface = findSurfaceBelow(x, y + CHAR_SIZE);
382
382
  if (fallSurface) {
@@ -389,7 +389,7 @@ const PetEngine = (() => {
389
389
  }
390
390
  }
391
391
 
392
- // 바닥 착지
392
+ // Floor landing
393
393
  if (y >= screenH - CHAR_SIZE) {
394
394
  y = screenH - CHAR_SIZE;
395
395
  edge = 'bottom';
@@ -400,23 +400,23 @@ const PetEngine = (() => {
400
400
  }
401
401
  break;
402
402
 
403
- // --- 레펠: 실을 타고 진자운동하며 하강 ---
403
+ // --- Rappel: pendulum swing descent on thread ---
404
404
  case 'rappelling':
405
405
  if (thread) {
406
- // 길이 증가 하강
406
+ // Increase thread length -> descend
407
407
  thread.length += THREAD_SPEED * speedMultiplier;
408
408
 
409
- // 진자 흔들림 물리
409
+ // Pendulum swing physics
410
410
  thread.swingVel += Math.sin(thread.angle) * 0.01;
411
- thread.swingVel *= 0.98; // 감쇠
411
+ thread.swingVel *= 0.98; // Damping
412
412
 
413
413
  thread.angle += thread.swingVel;
414
414
 
415
- // 부착점 기준 진자 위치 계산
415
+ // Calculate pendulum position from attachment point
416
416
  x = thread.attachX + Math.sin(thread.angle) * thread.length - CHAR_SIZE / 2;
417
417
  y = thread.attachY + Math.cos(thread.angle) * thread.length;
418
418
 
419
- // 좌우 화면 경계 반사
419
+ // Bounce off left/right screen edges
420
420
  if (x <= 0) {
421
421
  x = 0;
422
422
  thread.swingVel = Math.abs(thread.swingVel) * 0.5;
@@ -426,7 +426,7 @@ const PetEngine = (() => {
426
426
  thread.swingVel = -Math.abs(thread.swingVel) * 0.5;
427
427
  }
428
428
 
429
- // 윈도우 표면 착지 감지
429
+ // Window surface landing detection
430
430
  const rappelSurface = findSurfaceBelow(x, y + CHAR_SIZE);
431
431
  if (rappelSurface) {
432
432
  y = rappelSurface.y - CHAR_SIZE;
@@ -437,7 +437,7 @@ const PetEngine = (() => {
437
437
  currentSurface = rappelSurface;
438
438
  }
439
439
 
440
- // 바닥 도달
440
+ // Reached floor
441
441
  if (y >= screenH - CHAR_SIZE) {
442
442
  y = screenH - CHAR_SIZE;
443
443
  thread = null;
@@ -449,7 +449,7 @@ const PetEngine = (() => {
449
449
  }
450
450
  break;
451
451
 
452
- // --- 표면 기어가기 (스텝 기반) ---
452
+ // --- Surface crawling (step-based) ---
453
453
  case 'crawling':
454
454
  default:
455
455
  switch (state) {
@@ -457,17 +457,17 @@ const PetEngine = (() => {
457
457
  case 'ceiling_walk':
458
458
  stepMove(1.0);
459
459
 
460
- // 수평 이동 경계 처리
460
+ // Boundary handling for horizontal movement
461
461
  if (edge === 'bottom' || edge === 'top') {
462
462
  if (x <= 0) { x = 0; direction = 1; }
463
463
  if (x >= screenW - CHAR_SIZE) { x = screenW - CHAR_SIZE; direction = -1; }
464
464
  }
465
465
 
466
- // 윈도우 표면 이동 가장자리에서 떨어짐
466
+ // Fall off edge when moving on window surface
467
467
  if (edge === 'surface' && currentSurface) {
468
468
  if (x <= currentSurface.x - CHAR_SIZE / 2 ||
469
469
  x >= currentSurface.x + currentSurface.width - CHAR_SIZE / 2) {
470
- // 표면 가장자리에서 떨어짐 낙하 모드
470
+ // Fell off surface edge -> falling mode
471
471
  movementMode = 'falling';
472
472
  onSurface = false;
473
473
  currentSurface = null;
@@ -479,7 +479,7 @@ const PetEngine = (() => {
479
479
 
480
480
  case 'climbing_up':
481
481
  if (edge === 'bottom' || edge === 'surface') {
482
- // 바닥/표면에서 벽으로 전환
482
+ // Transition from floor/surface to wall
483
483
  if (direction > 0) {
484
484
  x = screenW - CHAR_SIZE;
485
485
  edge = 'right';
@@ -488,33 +488,33 @@ const PetEngine = (() => {
488
488
  edge = 'left';
489
489
  }
490
490
  currentSurface = null;
491
- direction = -1; // 벽에서 위쪽 방향
491
+ direction = -1; // Upward direction on wall
492
492
  }
493
493
 
494
- // 벽에서 위로 기어오름: y 감소
494
+ // Climbing up wall: y decreases
495
495
  if (edge === 'left' || edge === 'right') {
496
496
  stepMove(0.7);
497
- // stepMove direction(-1) 적용하므로 y 감소함
497
+ // stepMove applies direction(-1) so y decreases
498
498
  }
499
499
 
500
- // 천장 도달
500
+ // Reached ceiling
501
501
  if (y <= 0) {
502
502
  y = 0;
503
503
  edge = 'top';
504
- direction = 1; // 천장에서 오른쪽으로 이동
504
+ direction = 1; // Move rightward on ceiling
505
505
  }
506
506
  break;
507
507
 
508
508
  case 'climbing_down':
509
- // 벽에서 아래로 기어내려감: y 증가
509
+ // Climbing down wall: y increases
510
510
  if (edge === 'left' || edge === 'right') {
511
- // direction 1(아래) 설정해서 stepMove
511
+ // Set direction to 1 (down) for stepMove
512
512
  const prevDir = direction;
513
513
  direction = 1;
514
514
  stepMove(0.7);
515
515
  direction = prevDir;
516
516
  } else if (edge === 'top') {
517
- // 천장에서 벽으로 내려가기 시작
517
+ // Start descending from ceiling to wall
518
518
  if (x < screenW / 2) {
519
519
  x = 0;
520
520
  edge = 'left';
@@ -522,19 +522,19 @@ const PetEngine = (() => {
522
522
  x = screenW - CHAR_SIZE;
523
523
  edge = 'right';
524
524
  }
525
- direction = 1; // 아래 방향
525
+ direction = 1; // Downward direction
526
526
  }
527
527
 
528
- // 바닥 도달
528
+ // Reached floor
529
529
  if (y >= screenH - CHAR_SIZE) {
530
530
  y = screenH - CHAR_SIZE;
531
531
  edge = 'bottom';
532
- direction = Math.random() < 0.5 ? 1 : -1; // 랜덤 방향
532
+ direction = Math.random() < 0.5 ? 1 : -1; // Random direction
533
533
  }
534
534
  break;
535
535
 
536
536
  case 'scared':
537
- // 도망: 스텝 스킵하고 빠르게 연속 이동
537
+ // Flee: skip steps, fast continuous movement
538
538
  if (edge === 'bottom' || edge === 'top' || edge === 'surface') {
539
539
  x += STEP_SIZE * 2.5 * direction * speedMultiplier;
540
540
  flipX = direction < 0;
@@ -544,7 +544,7 @@ const PetEngine = (() => {
544
544
  break;
545
545
 
546
546
  case 'carrying':
547
- // 들고 느리게 이동
547
+ // Slow movement while carrying
548
548
  stepMove(0.6);
549
549
  if (edge === 'bottom' || edge === 'top' || edge === 'surface') {
550
550
  if (x <= 0) { x = 0; direction = 1; }
@@ -553,7 +553,7 @@ const PetEngine = (() => {
553
553
  break;
554
554
 
555
555
  case 'excited':
556
- // 작은 점프 효과 (제자리에서 통통 )
556
+ // Small jump effect (bouncing in place)
557
557
  if (typeof StateMachine !== 'undefined') {
558
558
  const elapsed = StateMachine.getElapsed();
559
559
  const jumpOffset = Math.sin(elapsed / 150) * 8;
@@ -565,22 +565,22 @@ const PetEngine = (() => {
565
565
  }
566
566
  break;
567
567
 
568
- // 점프 상태 (StateMachine에서 전이된 물리 상태)
568
+ // Jumping state (physics state transitioned from StateMachine)
569
569
  case 'jumping':
570
- // movementMode jumping이 아니면 시작
570
+ // Initiate if movementMode is not yet jumping
571
571
  if (movementMode === 'crawling') {
572
572
  _initiateRandomJump();
573
573
  }
574
574
  break;
575
575
 
576
- // 레펠 중 상태
576
+ // Rappelling state
577
577
  case 'rappelling':
578
578
  if (movementMode === 'crawling') {
579
579
  startRappel();
580
580
  }
581
581
  break;
582
582
 
583
- // 낙하 중 상태
583
+ // Falling state
584
584
  case 'falling':
585
585
  if (movementMode === 'crawling') {
586
586
  movementMode = 'falling';
@@ -589,7 +589,7 @@ const PetEngine = (() => {
589
589
  }
590
590
  break;
591
591
 
592
- // 커스텀 이동 패턴 실행
592
+ // Custom movement pattern in progress
593
593
  case 'custom':
594
594
  if (activeCustomMovement) {
595
595
  updateCustomMovement(now - (updateCustomMovement._lastTime || now));
@@ -601,7 +601,7 @@ const PetEngine = (() => {
601
601
  case 'sleeping':
602
602
  case 'interacting':
603
603
  case 'playing':
604
- // 정지 또는 미세한 흔들림 (이동 없음)
604
+ // Stationary or subtle sway (no movement)
605
605
  break;
606
606
  }
607
607
  break;
@@ -612,26 +612,26 @@ const PetEngine = (() => {
612
612
  }
613
613
 
614
614
  /**
615
- * StateMachine이 jumping 상태로 전이했을 랜덤 목표로 점프
616
- * 현재 위치 기준 화면 중앙 방향 또는 랜덤 위치로 도약
615
+ * Random jump when StateMachine transitions to jumping state
616
+ * Leap toward screen center or random position from current location
617
617
  */
618
618
  function _initiateRandomJump() {
619
- // 화면 중앙 근처의 랜덤 목표 지점
619
+ // Random target point near screen center
620
620
  const targetX = screenW * 0.2 + Math.random() * screenW * 0.6;
621
621
  const targetY = screenH * 0.3 + Math.random() * screenH * 0.4;
622
622
  jumpTo(targetX, targetY);
623
623
  }
624
624
 
625
625
  // ===================================
626
- // 점프 명령
626
+ // Jump commands
627
627
  // ===================================
628
628
 
629
629
  /**
630
- * 목표 지점을 향해 포물선 점프 시작
631
- * 초기 속도(vx, vy) 계산하여 포물선 궤도 생성
630
+ * Start parabolic jump toward target point
631
+ * Calculate initial velocity (vx, vy) to create parabolic trajectory
632
632
  *
633
- * @param {number} targetX - 목표 x 좌표
634
- * @param {number} targetY - 목표 y 좌표
633
+ * @param {number} targetX - Target x coordinate
634
+ * @param {number} targetY - Target y coordinate
635
635
  */
636
636
  function jumpTo(targetX, targetY) {
637
637
  if (movementMode !== 'crawling') return;
@@ -640,14 +640,14 @@ const PetEngine = (() => {
640
640
  const dy = targetY - y;
641
641
  const dist = Math.hypot(dx, dy);
642
642
 
643
- // 비행 시간 추정 (거리 기반)
643
+ // Estimate flight time (distance-based)
644
644
  const time = Math.max(20, dist / (JUMP_VX * 2 + 2));
645
645
 
646
- // 포물선 초기 속도 계산
646
+ // Calculate parabolic initial velocity
647
647
  vx = dx / time;
648
648
  vy = (dy / time) - (GRAVITY * time) / 2;
649
649
 
650
- // vx, vy 범위 제한 (너무 빠르지 않게)
650
+ // Clamp vx, vy range (prevent excessive speed)
651
651
  const maxV = 8;
652
652
  vx = Math.max(-maxV, Math.min(maxV, vx));
653
653
  vy = Math.max(-12, Math.min(maxV, vy));
@@ -662,29 +662,29 @@ const PetEngine = (() => {
662
662
  }
663
663
 
664
664
  // ===================================
665
- // 레펠(Thread) 시스템
665
+ // Rappel (Thread) system
666
666
  // ===================================
667
667
 
668
668
  /**
669
- * 레펠 시작: 천장이나 벽에서 실을 내려 하강
670
- * 부착점을 현재 위치에 설정하고 진자운동 시작
669
+ * Start rappel: descend by lowering thread from ceiling or wall
670
+ * Set attachment point at current position and begin pendulum swing
671
671
  */
672
672
  function startRappel() {
673
- // 천장, 왼쪽 벽, 오른쪽 벽에서만 레펠 가능
673
+ // Rappel only possible from ceiling, left wall, or right wall
674
674
  if (edge !== 'top' && edge !== 'left' && edge !== 'right') return;
675
675
 
676
676
  let attachX, attachY;
677
677
 
678
678
  if (edge === 'top') {
679
- // 천장에서 레펠: 현재 위치 바로 위에 부착
679
+ // Rappel from ceiling: attach directly above current position
680
680
  attachX = x + CHAR_SIZE / 2;
681
681
  attachY = 0;
682
682
  } else if (edge === 'left') {
683
- // 왼쪽 벽에서 레펠: 벽의 현재 y 위치에 부착
683
+ // Rappel from left wall: attach at current y position on wall
684
684
  attachX = 0;
685
685
  attachY = y;
686
686
  } else {
687
- // 오른쪽 벽에서 레펠
687
+ // Rappel from right wall
688
688
  attachX = screenW;
689
689
  attachY = y;
690
690
  }
@@ -692,9 +692,9 @@ const PetEngine = (() => {
692
692
  thread = {
693
693
  attachX: attachX,
694
694
  attachY: attachY,
695
- length: CHAR_SIZE, // 초기 길이
696
- angle: 0, // 진자 각도 (라디안)
697
- swingVel: (Math.random() - 0.5) * 0.05, // 초기 흔들림 속도
695
+ length: CHAR_SIZE, // Initial thread length
696
+ angle: 0, // Pendulum angle (radians)
697
+ swingVel: (Math.random() - 0.5) * 0.05, // Initial swing velocity
698
698
  };
699
699
 
700
700
  movementMode = 'rappelling';
@@ -707,7 +707,7 @@ const PetEngine = (() => {
707
707
  }
708
708
 
709
709
  /**
710
- * 레펠 해제: 실을 놓아 자유 낙하 전환
710
+ * Release rappel: let go of thread to transition to free fall
711
711
  */
712
712
  function releaseThread() {
713
713
  if (!thread) return;
@@ -722,41 +722,41 @@ const PetEngine = (() => {
722
722
  }
723
723
 
724
724
  /**
725
- * 화면 중앙으로 이동
726
- * 천장에서는 레펠로 하강, 외에는 점프
725
+ * Move to screen center
726
+ * Descend via rappel from ceiling, otherwise jump
727
727
  */
728
728
  function moveToCenter() {
729
729
  const cx = (screenW - CHAR_SIZE) / 2;
730
730
  const cy = (screenH - CHAR_SIZE) / 2;
731
731
 
732
732
  if (edge === 'top') {
733
- // 천장에서는 레펠로 내려감
733
+ // From ceiling, descend via rappel
734
734
  startRappel();
735
735
  } else {
736
- // 바닥/벽에서는 중앙으로 점프
736
+ // From floor/wall, jump to center
737
737
  jumpTo(cx, cy);
738
738
  }
739
739
  }
740
740
 
741
741
  // ===================================
742
- // 애니메이션 프레임 갱신
742
+ // Animation frame update
743
743
  // ===================================
744
744
 
745
745
  /**
746
- * 현재 상태에 맞는 애니메이션 프레임 렌더링
747
- * 공중 상태일 때는 기존 프레임셋을 재활용
746
+ * Render animation frame matching current state
747
+ * Reuse existing frameset when in mid-air
748
748
  *
749
- * @param {string} state - StateMachine 상태
750
- * @param {number} timestamp - requestAnimationFrame 타임스탬프
749
+ * @param {string} state - StateMachine state
750
+ * @param {number} timestamp - requestAnimationFrame timestamp
751
751
  */
752
752
  function updateAnimation(state, timestamp) {
753
753
  if (timestamp - lastAnimTime > ANIM_INTERVAL) {
754
754
  animFrame++;
755
755
  lastAnimTime = timestamp;
756
- animFrameChanged = true; // 이동 시스템에 프레임 전환 알림
756
+ animFrameChanged = true; // Notify movement system of frame transition
757
757
  }
758
758
 
759
- // 이동 모드에 따라 적절한 프레임셋으로 매핑
759
+ // Map to appropriate frameset based on movement mode
760
760
  let effectiveState = state;
761
761
  if (movementMode === 'jumping') effectiveState = 'jumping';
762
762
  if (movementMode === 'falling') effectiveState = 'falling';
@@ -768,11 +768,11 @@ const PetEngine = (() => {
768
768
  }
769
769
 
770
770
  // ===================================
771
- // 위치/상태 접근자
771
+ // Position/state accessors
772
772
  // ===================================
773
773
 
774
774
  /**
775
- * 현재 위치 상태 정보 반환
775
+ * Return current position and state info
776
776
  * @returns {{ x, y, edge, direction, flipX, movementMode, onSurface, thread }}
777
777
  */
778
778
  function getPosition() {
@@ -784,7 +784,7 @@ const PetEngine = (() => {
784
784
  }
785
785
 
786
786
  /**
787
- * 위치 직접 설정 (드래그 )
787
+ * Set position directly (for drag, etc.)
788
788
  */
789
789
  function setPosition(nx, ny) {
790
790
  x = nx;
@@ -803,8 +803,8 @@ const PetEngine = (() => {
803
803
  }
804
804
 
805
805
  /**
806
- * 가장 가까운 가장자리로 즉시 이동 (드래그 )
807
- * 모든 물리 상태를 초기화하고 표면에 붙음
806
+ * Snap to nearest edge instantly (after drag)
807
+ * Reset all physics state and attach to surface
808
808
  */
809
809
  function snapToNearestEdge() {
810
810
  const distBottom = screenH - CHAR_SIZE - y;
@@ -827,7 +827,7 @@ const PetEngine = (() => {
827
827
  edge = 'right';
828
828
  }
829
829
 
830
- // 물리 상태 완전 초기화
830
+ // Full physics state reset
831
831
  movementMode = 'crawling';
832
832
  onSurface = true;
833
833
  currentSurface = null;
@@ -839,8 +839,8 @@ const PetEngine = (() => {
839
839
  }
840
840
 
841
841
  /**
842
- * 자유 낙하 시작 (화면 중앙 근처에서 놓았을 )
843
- * 중력에 의해 바닥 또는 가장 가까운 표면으로 떨어짐
842
+ * Start free fall (when released near screen center)
843
+ * Fall to floor or nearest surface by gravity
844
844
  */
845
845
  function startFalling() {
846
846
  movementMode = 'falling';
@@ -856,7 +856,7 @@ const PetEngine = (() => {
856
856
  }
857
857
 
858
858
  /**
859
- * 레펠 스레드 정보 반환
859
+ * Return rappel thread info
860
860
  * @returns {object|null}
861
861
  */
862
862
  function getThread() {
@@ -864,36 +864,36 @@ const PetEngine = (() => {
864
864
  }
865
865
 
866
866
  // ===================================
867
- // 커스텀 이동 패턴 레지스트리
867
+ // Custom movement pattern registry
868
868
  // ===================================
869
869
 
870
- // 등록된 커스텀 이동 패턴 저장소
871
- // 핸들러: { init(params), update(deltaTime), isComplete(), cleanup() }
870
+ // Registered custom movement pattern store
871
+ // Each handler: { init(params), update(deltaTime), isComplete(), cleanup() }
872
872
  let customMovements = {};
873
- let activeCustomMovement = null; // 현재 실행 중인 커스텀 이동 { name, handler, state }
873
+ let activeCustomMovement = null; // Currently active custom movement { name, handler, state }
874
874
 
875
875
  /**
876
- * 커스텀 이동 패턴 등록
877
- * @param {string} name - 패턴 이름 (예: 'zigzag', 'patrol')
876
+ * Register custom movement pattern
877
+ * @param {string} name - Pattern name (e.g., 'zigzag', 'patrol')
878
878
  * @param {object} handler - { init, update, isComplete, cleanup }
879
879
  */
880
880
  function registerMovement(name, handler) {
881
881
  if (!handler || typeof handler.update !== 'function') {
882
- console.error(`[PetEngine] 이동 패턴 '${name}' 등록 실패: update 함수 필수`);
882
+ console.error(`[PetEngine] Failed to register movement pattern '${name}': update function required`);
883
883
  return false;
884
884
  }
885
- // 기본 메서드 보완
885
+ // Fill in default methods
886
886
  handler.init = handler.init || (() => {});
887
887
  handler.isComplete = handler.isComplete || (() => false);
888
888
  handler.cleanup = handler.cleanup || (() => {});
889
889
  customMovements[name] = handler;
890
- console.log(`[PetEngine] 커스텀 이동 패턴 등록: ${name}`);
890
+ console.log(`[PetEngine] Custom movement pattern registered: ${name}`);
891
891
  return true;
892
892
  }
893
893
 
894
894
  /**
895
- * 등록된 커스텀 이동 패턴 제거
896
- * @param {string} name - 패턴 이름
895
+ * Remove registered custom movement pattern
896
+ * @param {string} name - Pattern name
897
897
  */
898
898
  function unregisterMovement(name) {
899
899
  if (activeCustomMovement && activeCustomMovement.name === name) {
@@ -903,24 +903,24 @@ const PetEngine = (() => {
903
903
  }
904
904
 
905
905
  /**
906
- * 커스텀 이동 패턴 실행
907
- * @param {string} name - 등록된 패턴 이름
908
- * @param {object} params - 패턴 초기화 파라미터
909
- * @returns {boolean} 실행 성공 여부
906
+ * Execute custom movement pattern
907
+ * @param {string} name - Registered pattern name
908
+ * @param {object} params - Pattern initialization parameters
909
+ * @returns {boolean} Whether execution succeeded
910
910
  */
911
911
  function executeCustomMovement(name, params = {}) {
912
912
  const handler = customMovements[name];
913
913
  if (!handler) {
914
- console.warn(`[PetEngine] 미등록 이동 패턴: ${name}`);
914
+ console.warn(`[PetEngine] Unregistered movement pattern: ${name}`);
915
915
  return false;
916
916
  }
917
917
 
918
- // 기존 커스텀 이동이 있으면 정리
918
+ // Clean up existing custom movement if any
919
919
  if (activeCustomMovement) {
920
920
  activeCustomMovement.handler.cleanup();
921
921
  }
922
922
 
923
- // 패턴 초기화 현재 위치/화면 정보 전달
923
+ // Pass current position/screen info on pattern initialization
924
924
  const context = {
925
925
  x, y, screenW, screenH,
926
926
  charSize: CHAR_SIZE,
@@ -930,18 +930,18 @@ const PetEngine = (() => {
930
930
  const state = handler.init(Object.assign({}, params, context)) || {};
931
931
  activeCustomMovement = { name, handler, state };
932
932
 
933
- // CUSTOM 상태로 전환
933
+ // Transition to CUSTOM state
934
934
  if (typeof StateMachine !== 'undefined') {
935
935
  StateMachine.forceState('custom');
936
936
  }
937
937
 
938
- console.log(`[PetEngine] 커스텀 이동 실행: ${name}`);
938
+ console.log(`[PetEngine] Executing custom movement: ${name}`);
939
939
  return true;
940
940
  }
941
941
 
942
942
  /**
943
- * 커스텀 이동 프레임 갱신
944
- * @param {number} deltaTime - 프레임 경과 시간 (ms)
943
+ * Update custom movement every frame
944
+ * @param {number} deltaTime - Elapsed time between frames (ms)
945
945
  */
946
946
  function updateCustomMovement(deltaTime) {
947
947
  if (!activeCustomMovement) return;
@@ -958,17 +958,17 @@ const PetEngine = (() => {
958
958
 
959
959
  handler.update(deltaTime, state, context);
960
960
 
961
- // 핸들러가 setPos로 위치를 설정했을 있음
961
+ // Handler may have set position via setPos
962
962
  clampPosition();
963
963
 
964
- // 완료 확인
964
+ // Check completion
965
965
  if (handler.isComplete(state)) {
966
966
  stopCustomMovement();
967
967
  }
968
968
  }
969
969
 
970
970
  /**
971
- * 현재 커스텀 이동 강제 중지 IDLE 복귀
971
+ * Force stop current custom movement -> return to IDLE
972
972
  */
973
973
  function stopCustomMovement() {
974
974
  if (!activeCustomMovement) return;
@@ -981,24 +981,24 @@ const PetEngine = (() => {
981
981
  }
982
982
 
983
983
  /**
984
- * 등록된 커스텀 이동 패턴 목록 반환
984
+ * Return list of registered custom movement patterns
985
985
  */
986
986
  function getRegisteredMovements() {
987
987
  return Object.keys(customMovements);
988
988
  }
989
989
 
990
- // --- 사전 등록 이동 패턴 ---
990
+ // --- Pre-registered movement patterns ---
991
991
 
992
- // 지그재그: 대각선 방향 교대로 이동
992
+ // Zigzag: alternating diagonal movement
993
993
  registerMovement('zigzag', {
994
994
  init(params) {
995
995
  return {
996
- amplitude: params.amplitude || 40, // 좌우 진폭 (px)
997
- speed: params.speed || 2, // 전진 속도
998
- segmentLength: params.segmentLength || 60, // 구간 길이
996
+ amplitude: params.amplitude || 40, // Horizontal amplitude (px)
997
+ speed: params.speed || 2, // Forward speed
998
+ segmentLength: params.segmentLength || 60, // Segment length
999
999
  traveled: 0,
1000
- totalDistance: params.distance || 300, // 이동 거리
1001
- zigDir: 1, // 지그재그 방향
1000
+ totalDistance: params.distance || 300, // Total travel distance
1001
+ zigDir: 1, // Zigzag direction
1002
1002
  startX: params.x,
1003
1003
  startY: params.y,
1004
1004
  };
@@ -1007,9 +1007,9 @@ const PetEngine = (() => {
1007
1007
  const step = state.speed * (dt / 16);
1008
1008
  state.traveled += step;
1009
1009
 
1010
- // 수평 전진
1010
+ // Horizontal advance
1011
1011
  const moveX = step * (ctx.direction || 1);
1012
- // 수직 지그재그
1012
+ // Vertical zigzag
1013
1013
  const segProgress = (state.traveled % state.segmentLength) / state.segmentLength;
1014
1014
  if (segProgress < 0.05) state.zigDir *= -1;
1015
1015
  const moveY = state.zigDir * step * 0.7;
@@ -1023,14 +1023,14 @@ const PetEngine = (() => {
1023
1023
  cleanup() {},
1024
1024
  });
1025
1025
 
1026
- // 순찰: 지점 사이를 왕복
1026
+ // Patrol: round-trip between two points
1027
1027
  registerMovement('patrol', {
1028
1028
  init(params) {
1029
1029
  return {
1030
1030
  pointA: { x: params.pointAX || 100, y: params.pointAY || params.y },
1031
1031
  pointB: { x: params.pointBX || params.screenW - 164, y: params.pointBY || params.y },
1032
1032
  speed: params.speed || 1.5,
1033
- laps: params.laps || 3, // 왕복 횟수
1033
+ laps: params.laps || 3, // Number of round trips
1034
1034
  currentLap: 0,
1035
1035
  targetIdx: 0, // 0=A, 1=B
1036
1036
  };
@@ -1042,7 +1042,7 @@ const PetEngine = (() => {
1042
1042
  const dist = Math.hypot(dx, dy);
1043
1043
 
1044
1044
  if (dist < 5) {
1045
- // 목표 도달 방향 전환
1045
+ // Reached target -> reverse direction
1046
1046
  state.targetIdx = 1 - state.targetIdx;
1047
1047
  if (state.targetIdx === 0) state.currentLap++;
1048
1048
  return;
@@ -1059,14 +1059,14 @@ const PetEngine = (() => {
1059
1059
  cleanup() {},
1060
1060
  });
1061
1061
 
1062
- // 원형 회전: 중심점 기준 회전
1062
+ // Circular rotation: revolve around center point
1063
1063
  registerMovement('circle', {
1064
1064
  init(params) {
1065
1065
  return {
1066
1066
  centerX: params.centerX || params.x,
1067
1067
  centerY: params.centerY || params.y - 50,
1068
1068
  radius: params.radius || 50,
1069
- speed: params.speed || 0.03, // 각속도 (rad/frame)
1069
+ speed: params.speed || 0.03, // Angular velocity (rad/frame)
1070
1070
  angle: 0,
1071
1071
  totalAngle: params.revolutions ? params.revolutions * Math.PI * 2 : Math.PI * 4,
1072
1072
  traveled: 0,
@@ -1088,12 +1088,12 @@ const PetEngine = (() => {
1088
1088
  cleanup() {},
1089
1089
  });
1090
1090
 
1091
- // 떨기: 빠르게 좌우 진동
1091
+ // Shake: fast horizontal vibration
1092
1092
  registerMovement('shake', {
1093
1093
  init(params) {
1094
1094
  return {
1095
- intensity: params.intensity || 4, // 떨림 강도 (px)
1096
- duration: params.duration || 800, // 지속 시간 (ms)
1095
+ intensity: params.intensity || 4, // Shake intensity (px)
1096
+ duration: params.duration || 800, // Duration (ms)
1097
1097
  elapsed: 0,
1098
1098
  originX: params.x,
1099
1099
  originY: params.y,
@@ -1104,7 +1104,7 @@ const PetEngine = (() => {
1104
1104
  state.elapsed += dt;
1105
1105
  state.phase += dt * 0.05;
1106
1106
 
1107
- // 감쇠하는 사인파 진동
1107
+ // Damped sinusoidal vibration
1108
1108
  const decay = 1 - (state.elapsed / state.duration);
1109
1109
  const offsetX = Math.sin(state.phase) * state.intensity * decay;
1110
1110
  ctx.setPos(state.originX + offsetX, state.originY);
@@ -1115,7 +1115,7 @@ const PetEngine = (() => {
1115
1115
  cleanup() {},
1116
1116
  });
1117
1117
 
1118
- // 댄스: 여러 동작을 연속 수행 (점프 + 회전 + 떨기 조합)
1118
+ // Dance: sequential combo of moves (jump + spin + shake)
1119
1119
  registerMovement('dance', {
1120
1120
  init(params) {
1121
1121
  return {
@@ -1132,18 +1132,18 @@ const PetEngine = (() => {
1132
1132
 
1133
1133
  const t = state.elapsed / state.duration;
1134
1134
 
1135
- // 구간별 다른 동작
1135
+ // Different moves per phase
1136
1136
  if (t < 0.25) {
1137
- // 구간 1: 좌우 스윙
1137
+ // Phase 1: left-right swing
1138
1138
  const swingX = Math.sin(state.phase * 8) * 20;
1139
1139
  ctx.setPos(state.originX + swingX, state.originY);
1140
1140
  ctx.setFlip(swingX < 0);
1141
1141
  } else if (t < 0.5) {
1142
- // 구간 2: 상하 바운스
1142
+ // Phase 2: up-down bounce
1143
1143
  const bounceY = Math.abs(Math.sin(state.phase * 6)) * -30;
1144
1144
  ctx.setPos(state.originX, state.originY + bounceY);
1145
1145
  } else if (t < 0.75) {
1146
- // 구간 3: 작은
1146
+ // Phase 3: small circle
1147
1147
  const angle = state.phase * 10;
1148
1148
  ctx.setPos(
1149
1149
  state.originX + Math.cos(angle) * 15,
@@ -1151,7 +1151,7 @@ const PetEngine = (() => {
1151
1151
  );
1152
1152
  ctx.setFlip(Math.cos(angle) < 0);
1153
1153
  } else {
1154
- // 구간 4: 빠른 좌우 떨기 (피니시)
1154
+ // Phase 4: fast horizontal shake (finish)
1155
1155
  const shake = Math.sin(state.phase * 20) * 6 * (1 - t);
1156
1156
  ctx.setPos(state.originX + shake, state.originY);
1157
1157
  }
@@ -1163,15 +1163,15 @@ const PetEngine = (() => {
1163
1163
  });
1164
1164
 
1165
1165
  // ===================================
1166
- // 메인 루프
1166
+ // Main loop
1167
1167
  // ===================================
1168
1168
 
1169
1169
  let frameId = null;
1170
1170
 
1171
1171
  /**
1172
- * 엔진 시작: requestAnimationFrame 루프 가동
1172
+ * Start engine: begin requestAnimationFrame loop
1173
1173
  */
1174
- let lastLoopTimestamp = 0; // 이전 루프 타임스탬프 (deltaTime 계산용)
1174
+ let lastLoopTimestamp = 0; // Previous loop timestamp (for deltaTime calculation)
1175
1175
 
1176
1176
  function start() {
1177
1177
  if (running) return;
@@ -1182,16 +1182,16 @@ const PetEngine = (() => {
1182
1182
  function loop(timestamp) {
1183
1183
  if (!running) return;
1184
1184
 
1185
- // 커스텀 이동용 deltaTime 계산
1185
+ // Calculate deltaTime for custom movement
1186
1186
  const deltaTime = timestamp - lastLoopTimestamp;
1187
1187
  lastLoopTimestamp = timestamp;
1188
1188
 
1189
1189
  const state = StateMachine.update();
1190
1190
 
1191
- // 애니메이션을 먼저 갱신 animFrameChanged 플래그 설정
1191
+ // Update animation first -> set animFrameChanged flag
1192
1192
  updateAnimation(state, timestamp);
1193
1193
 
1194
- // 커스텀 이동이 활성 상태이면 전용 업데이트 실행
1194
+ // Run dedicated update if custom movement is active
1195
1195
  if (activeCustomMovement && state === 'custom') {
1196
1196
  updateCustomMovement(deltaTime);
1197
1197
  clampPosition();
@@ -1200,7 +1200,7 @@ const PetEngine = (() => {
1200
1200
  moveForState(state);
1201
1201
  }
1202
1202
 
1203
- // 프레임 전환 플래그 리셋 (다음 프레임까지 대기)
1203
+ // Reset frame transition flag (wait until next frame)
1204
1204
  animFrameChanged = false;
1205
1205
  frameId = requestAnimationFrame(loop);
1206
1206
  }
@@ -1208,23 +1208,23 @@ const PetEngine = (() => {
1208
1208
  }
1209
1209
 
1210
1210
  /**
1211
- * 엔진 정지
1211
+ * Stop engine
1212
1212
  */
1213
1213
  function stop() {
1214
1214
  running = false;
1215
1215
  if (frameId) cancelAnimationFrame(frameId);
1216
1216
  }
1217
1217
 
1218
- // --- 공개 API ---
1218
+ // --- Public API ---
1219
1219
  return {
1220
1220
  init, start, stop,
1221
1221
  getPosition, setPosition, setEdge, setDirection,
1222
1222
  snapToNearestEdge, setSpeedMultiplier,
1223
1223
  moveForState, updateAnimation,
1224
- // 물리 기반 이동
1224
+ // Physics-based movement
1225
1225
  jumpTo, startRappel, releaseThread, moveToCenter,
1226
1226
  setSurfaces, getThread, startFalling,
1227
- // 커스텀 이동 패턴 시스템
1227
+ // Custom movement pattern system
1228
1228
  registerMovement, unregisterMovement,
1229
1229
  executeCustomMovement, stopCustomMovement,
1230
1230
  getRegisteredMovements,