clawmate 1.1.0 → 1.2.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 +178 -2
- package/main/ai-bridge.js +36 -0
- package/main/ai-connector.js +37 -0
- package/main/index.js +14 -0
- package/main/ipc-handlers.js +6 -0
- package/main/platform.js +172 -2
- package/package.json +1 -1
- package/preload/preload.js +3 -0
- package/renderer/css/pet.css +11 -0
- package/renderer/index.html +4 -0
- package/renderer/js/ai-controller.js +39 -1
- package/renderer/js/character.js +3 -0
- package/renderer/js/interactions.js +24 -3
- package/renderer/js/pet-engine.js +748 -98
- package/renderer/js/speech.js +52 -5
- package/renderer/js/state-machine.js +24 -2
|
@@ -1,182 +1,777 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 핵심
|
|
3
|
-
* requestAnimationFrame 기반 이동
|
|
2
|
+
* 핵심 이동/물리 엔진 (리뉴얼)
|
|
3
|
+
* requestAnimationFrame 기반 — 스텝 이동 + 점프 + 레펠 + 중력 낙하
|
|
4
|
+
*
|
|
5
|
+
* 이동 모드:
|
|
6
|
+
* crawling — 표면 위에서 한발자국씩 기어가는 이동
|
|
7
|
+
* jumping — 포물선 궤도 점프
|
|
8
|
+
* falling — 중력에 의한 자유 낙하
|
|
9
|
+
* rappelling — 실(thread)을 타고 진자운동하며 하강
|
|
4
10
|
*/
|
|
5
11
|
const PetEngine = (() => {
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
12
|
+
// --- 물리 상수 ---
|
|
13
|
+
const GRAVITY = 0.3; // 중력 가속도 (px/frame^2)
|
|
14
|
+
const STEP_SIZE = 4; // 한 걸음 크기 (px)
|
|
15
|
+
const STEP_PAUSE = 80; // 걸음 사이 멈춤 시간 (ms)
|
|
16
|
+
const JUMP_VX = 3; // 점프 수평 초기 속도
|
|
17
|
+
const JUMP_VY = -7; // 점프 수직 초기 속도 (위로)
|
|
18
|
+
const BOUNCE_FACTOR = 0.3; // 착지 바운스 계수
|
|
19
|
+
const CHAR_SIZE = 64; // 캐릭터 크기 (px)
|
|
20
|
+
const ANIM_INTERVAL = 200; // 애니메이션 프레임 전환 간격 (ms)
|
|
21
|
+
const THREAD_SPEED = 0.8; // 레펠 하강 속도 (px/frame)
|
|
10
22
|
|
|
23
|
+
// --- 위치 및 속도 ---
|
|
11
24
|
let x = 0, y = 0;
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
let
|
|
16
|
-
let
|
|
25
|
+
let vx = 0, vy = 0; // 현재 속도 벡터
|
|
26
|
+
|
|
27
|
+
// --- 표면/방향 ---
|
|
28
|
+
let edge = 'bottom'; // 현재 부착된 가장자리 (bottom, left, right, top, surface)
|
|
29
|
+
let direction = 1; // 이동 방향: 1=오른쪽/아래, -1=왼쪽/위
|
|
30
|
+
let flipX = false; // 캐릭터 좌우 반전 여부
|
|
31
|
+
let screenW, screenH;
|
|
32
|
+
|
|
33
|
+
// --- 엔진 상태 ---
|
|
17
34
|
let running = false;
|
|
35
|
+
let petContainer = null;
|
|
36
|
+
let speedMultiplier = 1.0;
|
|
18
37
|
let animFrame = 0;
|
|
19
38
|
let lastAnimTime = 0;
|
|
20
|
-
let speedMultiplier = 1.0;
|
|
21
|
-
let petContainer = null;
|
|
22
39
|
|
|
40
|
+
// --- 이동 모드 ---
|
|
41
|
+
let movementMode = 'crawling'; // crawling | jumping | falling | rappelling
|
|
42
|
+
let onSurface = true; // 표면 위에 있는지 여부
|
|
43
|
+
|
|
44
|
+
// --- 스텝 시스템 (뚝뚝 끊어 걷기) ---
|
|
45
|
+
let stepPhase = 'move'; // 'move' 또는 'pause'
|
|
46
|
+
let lastStepTime = 0;
|
|
47
|
+
|
|
48
|
+
// --- 레펠(thread) 시스템 ---
|
|
49
|
+
// 부착점에서 실을 내려 진자운동하며 하강
|
|
50
|
+
let thread = null; // { attachX, attachY, length, angle, swingVel } | null
|
|
51
|
+
|
|
52
|
+
// --- 윈도우 표면 목록 ---
|
|
53
|
+
// 외부 윈도우의 타이틀바 등을 추가 표면으로 등록
|
|
54
|
+
let windowSurfaces = []; // [{ id, x, y, width, height }]
|
|
55
|
+
|
|
56
|
+
// --- 착지 시 현재 올라가 있는 표면 참조 ---
|
|
57
|
+
let currentSurface = null;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 초기화: 컨테이너 설정 및 화면 하단 중앙 배치
|
|
61
|
+
*/
|
|
23
62
|
function init(container) {
|
|
24
63
|
petContainer = container;
|
|
25
|
-
// 화면 하단 중앙에서 시작
|
|
26
64
|
screenW = window.innerWidth;
|
|
27
65
|
screenH = window.innerHeight;
|
|
66
|
+
|
|
67
|
+
// 화면 하단 중앙에서 시작
|
|
28
68
|
x = (screenW - CHAR_SIZE) / 2;
|
|
29
69
|
y = screenH - CHAR_SIZE;
|
|
30
70
|
edge = 'bottom';
|
|
31
71
|
direction = 1;
|
|
32
|
-
|
|
72
|
+
movementMode = 'crawling';
|
|
73
|
+
onSurface = true;
|
|
74
|
+
currentSurface = null;
|
|
75
|
+
updateVisual();
|
|
33
76
|
|
|
77
|
+
// 화면 크기 변경 대응
|
|
34
78
|
window.addEventListener('resize', () => {
|
|
35
79
|
screenW = window.innerWidth;
|
|
36
80
|
screenH = window.innerHeight;
|
|
37
81
|
clampPosition();
|
|
38
|
-
|
|
82
|
+
updateVisual();
|
|
39
83
|
});
|
|
40
84
|
}
|
|
41
85
|
|
|
86
|
+
/**
|
|
87
|
+
* 속도 배율 설정 (성격/진화 단계에 따른 속도 조절)
|
|
88
|
+
*/
|
|
42
89
|
function setSpeedMultiplier(mult) {
|
|
43
90
|
speedMultiplier = mult;
|
|
44
91
|
}
|
|
45
92
|
|
|
93
|
+
/**
|
|
94
|
+
* 화면 경계 내로 위치 제한
|
|
95
|
+
*/
|
|
46
96
|
function clampPosition() {
|
|
47
97
|
x = Math.max(0, Math.min(x, screenW - CHAR_SIZE));
|
|
48
98
|
y = Math.max(0, Math.min(y, screenH - CHAR_SIZE));
|
|
49
99
|
}
|
|
50
100
|
|
|
51
|
-
|
|
101
|
+
// ===================================
|
|
102
|
+
// 시각적 업데이트
|
|
103
|
+
// ===================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 컨테이너 위치 및 회전/반전 업데이트
|
|
107
|
+
* 가장자리별로 캐릭터가 올바른 방향을 향하도록 transform 적용
|
|
108
|
+
*
|
|
109
|
+
* EDGE_OFFSET: 스프라이트 테두리의 빈 픽셀(4px)을 보정하여
|
|
110
|
+
* 다리가 실제로 벽면/바닥/천장에 밀착하도록 렌더링 위치 조정
|
|
111
|
+
*/
|
|
112
|
+
const EDGE_OFFSET = 4;
|
|
113
|
+
|
|
114
|
+
function updateVisual() {
|
|
52
115
|
if (!petContainer) return;
|
|
53
|
-
petContainer.style.left = x + 'px';
|
|
54
|
-
petContainer.style.top = y + 'px';
|
|
55
116
|
|
|
56
|
-
|
|
117
|
+
let renderX = x;
|
|
118
|
+
let renderY = y;
|
|
119
|
+
|
|
120
|
+
// 표면에 붙어있을 때만 오프셋 적용 (공중 상태에서는 불필요)
|
|
121
|
+
if (onSurface && movementMode === 'crawling') {
|
|
122
|
+
switch (edge) {
|
|
123
|
+
case 'bottom':
|
|
124
|
+
case 'surface':
|
|
125
|
+
renderY += EDGE_OFFSET; // 바닥: 다리를 아래로 밀착
|
|
126
|
+
break;
|
|
127
|
+
case 'top':
|
|
128
|
+
renderY -= EDGE_OFFSET; // 천장: 다리를 위로 밀착
|
|
129
|
+
break;
|
|
130
|
+
case 'left':
|
|
131
|
+
renderX -= EDGE_OFFSET; // 왼쪽 벽: 다리를 왼쪽으로 밀착
|
|
132
|
+
break;
|
|
133
|
+
case 'right':
|
|
134
|
+
renderX += EDGE_OFFSET; // 오른쪽 벽: 다리를 오른쪽으로 밀착
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
petContainer.style.left = renderX + 'px';
|
|
140
|
+
petContainer.style.top = renderY + 'px';
|
|
141
|
+
|
|
57
142
|
let transform = '';
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (flipX) transform
|
|
143
|
+
|
|
144
|
+
if (movementMode === 'rappelling' || movementMode === 'jumping' || movementMode === 'falling') {
|
|
145
|
+
// 공중 상태: 바닥 기준 기본 자세 (회전 없음)
|
|
146
|
+
if (flipX) transform = 'scaleX(-1)';
|
|
147
|
+
} else if (edge === 'left') {
|
|
148
|
+
// 왼쪽 벽: 다리가 왼쪽 가장자리를 향하도록 반시계 회전
|
|
149
|
+
transform = 'rotate(-90deg)';
|
|
150
|
+
if (flipX) transform += ' scaleX(-1)';
|
|
62
151
|
} else if (edge === 'right') {
|
|
63
|
-
// 오른쪽 벽: 다리가 오른쪽
|
|
64
|
-
transform
|
|
65
|
-
if (flipX) transform += 'scaleX(-1)
|
|
152
|
+
// 오른쪽 벽: 다리가 오른쪽 가장자리를 향하도록 시계 회전
|
|
153
|
+
transform = 'rotate(90deg)';
|
|
154
|
+
if (flipX) transform += ' scaleX(-1)';
|
|
66
155
|
} else if (edge === 'top') {
|
|
67
|
-
// 천장: 다리가
|
|
68
|
-
transform
|
|
69
|
-
if (flipX) transform += 'scaleX(-1)
|
|
156
|
+
// 천장: 다리가 위를 향하도록 상하 반전
|
|
157
|
+
transform = 'scaleY(-1)';
|
|
158
|
+
if (flipX) transform += ' scaleX(-1)';
|
|
70
159
|
} else {
|
|
71
|
-
//
|
|
72
|
-
if (flipX) transform
|
|
160
|
+
// 바닥/표면: 기본 자세
|
|
161
|
+
if (flipX) transform = 'scaleX(-1)';
|
|
73
162
|
}
|
|
74
|
-
|
|
163
|
+
|
|
164
|
+
petContainer.style.transform = transform || 'none';
|
|
165
|
+
|
|
166
|
+
// 레펠 스레드 시각화 갱신
|
|
167
|
+
updateThreadVisual();
|
|
75
168
|
}
|
|
76
169
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
170
|
+
/**
|
|
171
|
+
* SVG 라인으로 레펠 실(thread) 시각화
|
|
172
|
+
* thread가 없으면 숨기고, 있으면 부착점 → 캐릭터 상단 연결
|
|
173
|
+
*/
|
|
174
|
+
function updateThreadVisual() {
|
|
175
|
+
const line = document.getElementById('thread-line');
|
|
176
|
+
if (!line) return;
|
|
177
|
+
|
|
178
|
+
if (!thread) {
|
|
179
|
+
// 실이 없으면 숨김
|
|
180
|
+
line.setAttribute('x1', '0');
|
|
181
|
+
line.setAttribute('y1', '0');
|
|
182
|
+
line.setAttribute('x2', '0');
|
|
183
|
+
line.setAttribute('y2', '0');
|
|
184
|
+
line.style.display = 'none';
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 부착점에서 캐릭터 상단 중앙까지 실 표시
|
|
189
|
+
line.style.display = 'block';
|
|
190
|
+
line.setAttribute('x1', thread.attachX);
|
|
191
|
+
line.setAttribute('y1', thread.attachY);
|
|
192
|
+
line.setAttribute('x2', x + CHAR_SIZE / 2);
|
|
193
|
+
line.setAttribute('y2', y);
|
|
194
|
+
line.setAttribute('stroke', '#888');
|
|
195
|
+
line.setAttribute('stroke-width', '1');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ===================================
|
|
199
|
+
// 스텝 기반 이동 (뚝뚝 끊어 걷기)
|
|
200
|
+
// ===================================
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 스텝 이동: STEP_SIZE만큼 이동 → STEP_PAUSE만큼 멈춤 반복
|
|
204
|
+
* 한 발자국씩 끊어서 기어가는 느낌을 줌
|
|
205
|
+
*
|
|
206
|
+
* @param {number} stepScale - 스텝 크기 배율 (0.6 = 짐 들고 느리게, 1.0 = 기본)
|
|
207
|
+
* @param {number} now - 현재 시각 (Date.now())
|
|
208
|
+
*/
|
|
209
|
+
function stepMove(stepScale, now) {
|
|
210
|
+
// 멈춤 단계: 아직 대기 시간이 남았으면 아무것도 안 함
|
|
211
|
+
if (stepPhase === 'pause') {
|
|
212
|
+
if (now - lastStepTime >= STEP_PAUSE) {
|
|
213
|
+
stepPhase = 'move';
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 이동 단계: 한 걸음 전진
|
|
219
|
+
const stepDist = STEP_SIZE * stepScale * speedMultiplier;
|
|
220
|
+
|
|
221
|
+
if (edge === 'bottom' || edge === 'top' || edge === 'surface') {
|
|
222
|
+
// 수평 이동 (바닥, 천장, 윈도우 표면)
|
|
223
|
+
x += stepDist * direction;
|
|
224
|
+
flipX = direction < 0;
|
|
225
|
+
} else if (edge === 'left') {
|
|
226
|
+
// 왼쪽 벽: y축 이동 (direction=1이면 아래로, -1이면 위로)
|
|
227
|
+
y += stepDist * direction;
|
|
228
|
+
} else if (edge === 'right') {
|
|
229
|
+
// 오른쪽 벽: y축 이동
|
|
230
|
+
y += stepDist * direction;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 한 걸음 완료, 멈춤 단계로 전환
|
|
234
|
+
stepPhase = 'pause';
|
|
235
|
+
lastStepTime = now;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ===================================
|
|
239
|
+
// 윈도우 표면 탐지
|
|
240
|
+
// ===================================
|
|
91
241
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
242
|
+
/**
|
|
243
|
+
* 주어진 위치 아래에 있는 윈도우 표면을 찾음
|
|
244
|
+
* 캐릭터가 수평 범위 안에 있고, 표면 상단 근처에 있을 때 착지 가능
|
|
245
|
+
*
|
|
246
|
+
* @param {number} px - 캐릭터 x 좌표
|
|
247
|
+
* @param {number} py - 캐릭터 하단 y 좌표 (y + CHAR_SIZE)
|
|
248
|
+
* @returns {object|null} 착지 가능한 표면 또는 null
|
|
249
|
+
*/
|
|
250
|
+
function findSurfaceBelow(px, py) {
|
|
251
|
+
let closest = null;
|
|
252
|
+
let closestDist = Infinity;
|
|
253
|
+
|
|
254
|
+
for (const s of windowSurfaces) {
|
|
255
|
+
// 수평 범위 확인: 캐릭터가 표면 위에 겹치는지
|
|
256
|
+
if (px + CHAR_SIZE > s.x && px < s.x + s.width) {
|
|
257
|
+
// 표면 상단에 근접했는지 (위에서 떨어지는 중)
|
|
258
|
+
if (py >= s.y && py <= s.y + 10) {
|
|
259
|
+
const dist = Math.abs(py - s.y);
|
|
260
|
+
if (dist < closestDist) {
|
|
261
|
+
closestDist = dist;
|
|
262
|
+
closest = s;
|
|
263
|
+
}
|
|
98
264
|
}
|
|
99
|
-
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return closest;
|
|
268
|
+
}
|
|
100
269
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
270
|
+
/**
|
|
271
|
+
* 외부에서 윈도우 표면 목록 등록
|
|
272
|
+
* (예: 다른 창의 타이틀바를 걸어다닐 수 있는 표면으로 설정)
|
|
273
|
+
*
|
|
274
|
+
* @param {Array} surfaces - [{ id, x, y, width, height }]
|
|
275
|
+
*/
|
|
276
|
+
function setSurfaces(surfaces) {
|
|
277
|
+
windowSurfaces = surfaces || [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ===================================
|
|
281
|
+
// 물리 상태별 이동 처리
|
|
282
|
+
// ===================================
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 메인 이동 로직: movementMode에 따라 물리 연산 수행
|
|
286
|
+
*
|
|
287
|
+
* @param {string} state - StateMachine의 현재 상태 (walking, idle 등)
|
|
288
|
+
*/
|
|
289
|
+
function moveForState(state) {
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
|
|
292
|
+
switch (movementMode) {
|
|
293
|
+
|
|
294
|
+
// --- 포물선 점프 ---
|
|
295
|
+
case 'jumping':
|
|
296
|
+
vy += GRAVITY; // 중력 적용
|
|
297
|
+
x += vx;
|
|
298
|
+
y += vy;
|
|
299
|
+
flipX = vx < 0;
|
|
300
|
+
|
|
301
|
+
// 바닥 착지 감지
|
|
302
|
+
if (y >= screenH - CHAR_SIZE) {
|
|
303
|
+
y = screenH - CHAR_SIZE;
|
|
304
|
+
// 바운스 효과: 약간 튕김
|
|
305
|
+
if (Math.abs(vy) > 2) {
|
|
306
|
+
vy = -vy * BOUNCE_FACTOR;
|
|
107
307
|
} else {
|
|
108
|
-
|
|
109
|
-
|
|
308
|
+
edge = 'bottom';
|
|
309
|
+
movementMode = 'crawling';
|
|
310
|
+
onSurface = true;
|
|
311
|
+
currentSurface = null;
|
|
312
|
+
vx = 0;
|
|
313
|
+
vy = 0;
|
|
110
314
|
}
|
|
111
315
|
}
|
|
112
|
-
|
|
113
|
-
|
|
316
|
+
|
|
317
|
+
// 윈도우 표면 착지 감지 (아래로 떨어지는 중일 때만)
|
|
318
|
+
if (vy > 0 && movementMode === 'jumping') {
|
|
319
|
+
const landSurface = findSurfaceBelow(x, y + CHAR_SIZE);
|
|
320
|
+
if (landSurface) {
|
|
321
|
+
y = landSurface.y - CHAR_SIZE;
|
|
322
|
+
edge = 'surface';
|
|
323
|
+
movementMode = 'crawling';
|
|
324
|
+
onSurface = true;
|
|
325
|
+
currentSurface = landSurface;
|
|
326
|
+
vx = 0;
|
|
327
|
+
vy = 0;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 벽/천장 충돌 → 해당 가장자리에 붙음
|
|
332
|
+
if (x <= 0 && movementMode === 'jumping') {
|
|
333
|
+
x = 0;
|
|
334
|
+
edge = 'left';
|
|
335
|
+
movementMode = 'crawling';
|
|
336
|
+
onSurface = true;
|
|
337
|
+
currentSurface = null;
|
|
338
|
+
vx = 0;
|
|
339
|
+
vy = 0;
|
|
340
|
+
direction = 1; // 아래쪽 방향
|
|
341
|
+
}
|
|
342
|
+
if (x >= screenW - CHAR_SIZE && movementMode === 'jumping') {
|
|
343
|
+
x = screenW - CHAR_SIZE;
|
|
344
|
+
edge = 'right';
|
|
345
|
+
movementMode = 'crawling';
|
|
346
|
+
onSurface = true;
|
|
347
|
+
currentSurface = null;
|
|
348
|
+
vx = 0;
|
|
349
|
+
vy = 0;
|
|
350
|
+
direction = 1;
|
|
351
|
+
}
|
|
352
|
+
if (y <= 0 && movementMode === 'jumping') {
|
|
114
353
|
y = 0;
|
|
115
354
|
edge = 'top';
|
|
355
|
+
movementMode = 'crawling';
|
|
356
|
+
onSurface = true;
|
|
357
|
+
currentSurface = null;
|
|
358
|
+
vx = 0;
|
|
359
|
+
vy = 0;
|
|
360
|
+
direction = 1; // 오른쪽 방향
|
|
116
361
|
}
|
|
117
362
|
break;
|
|
118
363
|
|
|
119
|
-
|
|
120
|
-
|
|
364
|
+
// --- 자유 낙하 (중력) ---
|
|
365
|
+
case 'falling':
|
|
366
|
+
vy += GRAVITY;
|
|
367
|
+
y += vy;
|
|
368
|
+
|
|
369
|
+
// 윈도우 표면 착지 감지
|
|
370
|
+
if (vy > 0) {
|
|
371
|
+
const fallSurface = findSurfaceBelow(x, y + CHAR_SIZE);
|
|
372
|
+
if (fallSurface) {
|
|
373
|
+
y = fallSurface.y - CHAR_SIZE;
|
|
374
|
+
edge = 'surface';
|
|
375
|
+
movementMode = 'crawling';
|
|
376
|
+
onSurface = true;
|
|
377
|
+
currentSurface = fallSurface;
|
|
378
|
+
vy = 0;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 바닥 착지
|
|
121
383
|
if (y >= screenH - CHAR_SIZE) {
|
|
122
384
|
y = screenH - CHAR_SIZE;
|
|
123
385
|
edge = 'bottom';
|
|
386
|
+
movementMode = 'crawling';
|
|
387
|
+
onSurface = true;
|
|
388
|
+
currentSurface = null;
|
|
389
|
+
vy = 0;
|
|
124
390
|
}
|
|
125
391
|
break;
|
|
126
392
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (x >= screenW - CHAR_SIZE) { x = screenW - CHAR_SIZE; direction = -1; }
|
|
133
|
-
break;
|
|
393
|
+
// --- 레펠: 실을 타고 진자운동하며 하강 ---
|
|
394
|
+
case 'rappelling':
|
|
395
|
+
if (thread) {
|
|
396
|
+
// 실 길이 증가 → 하강
|
|
397
|
+
thread.length += THREAD_SPEED * speedMultiplier;
|
|
134
398
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
399
|
+
// 진자 흔들림 물리
|
|
400
|
+
thread.swingVel += Math.sin(thread.angle) * 0.01;
|
|
401
|
+
thread.swingVel *= 0.98; // 감쇠
|
|
402
|
+
|
|
403
|
+
thread.angle += thread.swingVel;
|
|
404
|
+
|
|
405
|
+
// 부착점 기준 진자 위치 계산
|
|
406
|
+
x = thread.attachX + Math.sin(thread.angle) * thread.length - CHAR_SIZE / 2;
|
|
407
|
+
y = thread.attachY + Math.cos(thread.angle) * thread.length;
|
|
141
408
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
409
|
+
// 좌우 화면 경계 반사
|
|
410
|
+
if (x <= 0) {
|
|
411
|
+
x = 0;
|
|
412
|
+
thread.swingVel = Math.abs(thread.swingVel) * 0.5;
|
|
413
|
+
}
|
|
414
|
+
if (x >= screenW - CHAR_SIZE) {
|
|
415
|
+
x = screenW - CHAR_SIZE;
|
|
416
|
+
thread.swingVel = -Math.abs(thread.swingVel) * 0.5;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 윈도우 표면 착지 감지
|
|
420
|
+
const rappelSurface = findSurfaceBelow(x, y + CHAR_SIZE);
|
|
421
|
+
if (rappelSurface) {
|
|
422
|
+
y = rappelSurface.y - CHAR_SIZE;
|
|
423
|
+
thread = null;
|
|
424
|
+
edge = 'surface';
|
|
425
|
+
movementMode = 'crawling';
|
|
426
|
+
onSurface = true;
|
|
427
|
+
currentSurface = rappelSurface;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 바닥 도달
|
|
431
|
+
if (y >= screenH - CHAR_SIZE) {
|
|
432
|
+
y = screenH - CHAR_SIZE;
|
|
433
|
+
thread = null;
|
|
434
|
+
edge = 'bottom';
|
|
435
|
+
movementMode = 'crawling';
|
|
436
|
+
onSurface = true;
|
|
437
|
+
currentSurface = null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
147
440
|
break;
|
|
148
441
|
|
|
149
|
-
|
|
150
|
-
case '
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
442
|
+
// --- 표면 위 기어가기 (스텝 기반) ---
|
|
443
|
+
case 'crawling':
|
|
444
|
+
default:
|
|
445
|
+
switch (state) {
|
|
446
|
+
case 'walking':
|
|
447
|
+
case 'ceiling_walk':
|
|
448
|
+
stepMove(1.0, now);
|
|
449
|
+
|
|
450
|
+
// 수평 이동 시 경계 처리
|
|
451
|
+
if (edge === 'bottom' || edge === 'top') {
|
|
452
|
+
if (x <= 0) { x = 0; direction = 1; }
|
|
453
|
+
if (x >= screenW - CHAR_SIZE) { x = screenW - CHAR_SIZE; direction = -1; }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 윈도우 표면 위 이동 시 가장자리에서 떨어짐
|
|
457
|
+
if (edge === 'surface' && currentSurface) {
|
|
458
|
+
if (x <= currentSurface.x - CHAR_SIZE / 2 ||
|
|
459
|
+
x >= currentSurface.x + currentSurface.width - CHAR_SIZE / 2) {
|
|
460
|
+
// 표면 가장자리에서 떨어짐 → 낙하 모드
|
|
461
|
+
movementMode = 'falling';
|
|
462
|
+
onSurface = false;
|
|
463
|
+
currentSurface = null;
|
|
464
|
+
vy = 0;
|
|
465
|
+
StateMachine.forceState('falling');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
|
|
470
|
+
case 'climbing_up':
|
|
471
|
+
if (edge === 'bottom' || edge === 'surface') {
|
|
472
|
+
// 바닥/표면에서 벽으로 전환
|
|
473
|
+
if (direction > 0) {
|
|
474
|
+
x = screenW - CHAR_SIZE;
|
|
475
|
+
edge = 'right';
|
|
476
|
+
} else {
|
|
477
|
+
x = 0;
|
|
478
|
+
edge = 'left';
|
|
479
|
+
}
|
|
480
|
+
currentSurface = null;
|
|
481
|
+
direction = -1; // 벽에서 위쪽 방향
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 벽에서 위로 기어오름: y 감소
|
|
485
|
+
if (edge === 'left' || edge === 'right') {
|
|
486
|
+
stepMove(0.7, now);
|
|
487
|
+
// stepMove가 direction(-1)을 적용하므로 y가 감소함
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 천장 도달
|
|
491
|
+
if (y <= 0) {
|
|
492
|
+
y = 0;
|
|
493
|
+
edge = 'top';
|
|
494
|
+
direction = 1; // 천장에서 오른쪽으로 이동
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
|
|
498
|
+
case 'climbing_down':
|
|
499
|
+
// 벽에서 아래로 기어내려감: y 증가
|
|
500
|
+
if (edge === 'left' || edge === 'right') {
|
|
501
|
+
// direction을 1(아래)로 설정해서 stepMove
|
|
502
|
+
const prevDir = direction;
|
|
503
|
+
direction = 1;
|
|
504
|
+
stepMove(0.7, now);
|
|
505
|
+
direction = prevDir;
|
|
506
|
+
} else if (edge === 'top') {
|
|
507
|
+
// 천장에서 벽으로 내려가기 시작
|
|
508
|
+
if (x < screenW / 2) {
|
|
509
|
+
x = 0;
|
|
510
|
+
edge = 'left';
|
|
511
|
+
} else {
|
|
512
|
+
x = screenW - CHAR_SIZE;
|
|
513
|
+
edge = 'right';
|
|
514
|
+
}
|
|
515
|
+
direction = 1; // 아래 방향
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 바닥 도달
|
|
519
|
+
if (y >= screenH - CHAR_SIZE) {
|
|
520
|
+
y = screenH - CHAR_SIZE;
|
|
521
|
+
edge = 'bottom';
|
|
522
|
+
direction = Math.random() < 0.5 ? 1 : -1; // 랜덤 방향
|
|
523
|
+
}
|
|
524
|
+
break;
|
|
525
|
+
|
|
526
|
+
case 'scared':
|
|
527
|
+
// 도망: 스텝 스킵하고 빠르게 연속 이동
|
|
528
|
+
if (edge === 'bottom' || edge === 'top' || edge === 'surface') {
|
|
529
|
+
x += STEP_SIZE * 2.5 * direction * speedMultiplier;
|
|
530
|
+
flipX = direction < 0;
|
|
531
|
+
if (x <= 0) { x = 0; direction = 1; }
|
|
532
|
+
if (x >= screenW - CHAR_SIZE) { x = screenW - CHAR_SIZE; direction = -1; }
|
|
533
|
+
}
|
|
534
|
+
break;
|
|
535
|
+
|
|
536
|
+
case 'carrying':
|
|
537
|
+
// 짐 들고 느리게 이동
|
|
538
|
+
stepMove(0.6, now);
|
|
539
|
+
if (edge === 'bottom' || edge === 'top' || edge === 'surface') {
|
|
540
|
+
if (x <= 0) { x = 0; direction = 1; }
|
|
541
|
+
if (x >= screenW - CHAR_SIZE) { x = screenW - CHAR_SIZE; direction = -1; }
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
|
|
545
|
+
case 'excited':
|
|
546
|
+
// 작은 점프 효과 (제자리에서 통통 뜀)
|
|
547
|
+
if (typeof StateMachine !== 'undefined') {
|
|
548
|
+
const elapsed = StateMachine.getElapsed();
|
|
549
|
+
const jumpOffset = Math.sin(elapsed / 150) * 8;
|
|
550
|
+
if (edge === 'bottom') {
|
|
551
|
+
y = (screenH - CHAR_SIZE) + jumpOffset;
|
|
552
|
+
} else if (edge === 'surface' && currentSurface) {
|
|
553
|
+
y = (currentSurface.y - CHAR_SIZE) + jumpOffset;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
|
|
558
|
+
// 점프 중 상태 (StateMachine에서 전이된 물리 상태)
|
|
559
|
+
case 'jumping':
|
|
560
|
+
// movementMode가 jumping이 아니면 시작
|
|
561
|
+
if (movementMode === 'crawling') {
|
|
562
|
+
_initiateRandomJump();
|
|
563
|
+
}
|
|
564
|
+
break;
|
|
565
|
+
|
|
566
|
+
// 레펠 중 상태
|
|
567
|
+
case 'rappelling':
|
|
568
|
+
if (movementMode === 'crawling') {
|
|
569
|
+
startRappel();
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
|
|
573
|
+
// 낙하 중 상태
|
|
574
|
+
case 'falling':
|
|
575
|
+
if (movementMode === 'crawling') {
|
|
576
|
+
movementMode = 'falling';
|
|
577
|
+
onSurface = false;
|
|
578
|
+
vy = 0;
|
|
579
|
+
}
|
|
580
|
+
break;
|
|
581
|
+
|
|
582
|
+
case 'idle':
|
|
583
|
+
case 'sleeping':
|
|
584
|
+
case 'interacting':
|
|
585
|
+
case 'playing':
|
|
586
|
+
// 정지 또는 미세한 흔들림 (이동 없음)
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
154
589
|
break;
|
|
155
590
|
}
|
|
156
591
|
|
|
157
592
|
clampPosition();
|
|
158
|
-
|
|
593
|
+
updateVisual();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* StateMachine이 jumping 상태로 전이했을 때 랜덤 목표로 점프
|
|
598
|
+
* 현재 위치 기준 화면 중앙 방향 또는 랜덤 위치로 도약
|
|
599
|
+
*/
|
|
600
|
+
function _initiateRandomJump() {
|
|
601
|
+
// 화면 중앙 근처의 랜덤 목표 지점
|
|
602
|
+
const targetX = screenW * 0.2 + Math.random() * screenW * 0.6;
|
|
603
|
+
const targetY = screenH * 0.3 + Math.random() * screenH * 0.4;
|
|
604
|
+
jumpTo(targetX, targetY);
|
|
159
605
|
}
|
|
160
606
|
|
|
607
|
+
// ===================================
|
|
608
|
+
// 점프 명령
|
|
609
|
+
// ===================================
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 목표 지점을 향해 포물선 점프 시작
|
|
613
|
+
* 초기 속도(vx, vy)를 계산하여 포물선 궤도 생성
|
|
614
|
+
*
|
|
615
|
+
* @param {number} targetX - 목표 x 좌표
|
|
616
|
+
* @param {number} targetY - 목표 y 좌표
|
|
617
|
+
*/
|
|
618
|
+
function jumpTo(targetX, targetY) {
|
|
619
|
+
if (movementMode !== 'crawling') return;
|
|
620
|
+
|
|
621
|
+
const dx = targetX - x;
|
|
622
|
+
const dy = targetY - y;
|
|
623
|
+
const dist = Math.hypot(dx, dy);
|
|
624
|
+
|
|
625
|
+
// 비행 시간 추정 (거리 기반)
|
|
626
|
+
const time = Math.max(20, dist / (JUMP_VX * 2 + 2));
|
|
627
|
+
|
|
628
|
+
// 포물선 초기 속도 계산
|
|
629
|
+
vx = dx / time;
|
|
630
|
+
vy = (dy / time) - (GRAVITY * time) / 2;
|
|
631
|
+
|
|
632
|
+
// vx, vy 범위 제한 (너무 빠르지 않게)
|
|
633
|
+
const maxV = 8;
|
|
634
|
+
vx = Math.max(-maxV, Math.min(maxV, vx));
|
|
635
|
+
vy = Math.max(-12, Math.min(maxV, vy));
|
|
636
|
+
|
|
637
|
+
movementMode = 'jumping';
|
|
638
|
+
onSurface = false;
|
|
639
|
+
currentSurface = null;
|
|
640
|
+
|
|
641
|
+
if (typeof StateMachine !== 'undefined') {
|
|
642
|
+
StateMachine.forceState('jumping');
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ===================================
|
|
647
|
+
// 레펠(Thread) 시스템
|
|
648
|
+
// ===================================
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* 레펠 시작: 천장이나 벽에서 실을 내려 하강
|
|
652
|
+
* 부착점을 현재 위치에 설정하고 진자운동 시작
|
|
653
|
+
*/
|
|
654
|
+
function startRappel() {
|
|
655
|
+
// 천장, 왼쪽 벽, 오른쪽 벽에서만 레펠 가능
|
|
656
|
+
if (edge !== 'top' && edge !== 'left' && edge !== 'right') return;
|
|
657
|
+
|
|
658
|
+
let attachX, attachY;
|
|
659
|
+
|
|
660
|
+
if (edge === 'top') {
|
|
661
|
+
// 천장에서 레펠: 현재 위치 바로 위에 부착
|
|
662
|
+
attachX = x + CHAR_SIZE / 2;
|
|
663
|
+
attachY = 0;
|
|
664
|
+
} else if (edge === 'left') {
|
|
665
|
+
// 왼쪽 벽에서 레펠: 벽의 현재 y 위치에 부착
|
|
666
|
+
attachX = 0;
|
|
667
|
+
attachY = y;
|
|
668
|
+
} else {
|
|
669
|
+
// 오른쪽 벽에서 레펠
|
|
670
|
+
attachX = screenW;
|
|
671
|
+
attachY = y;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
thread = {
|
|
675
|
+
attachX: attachX,
|
|
676
|
+
attachY: attachY,
|
|
677
|
+
length: CHAR_SIZE, // 초기 실 길이
|
|
678
|
+
angle: 0, // 진자 각도 (라디안)
|
|
679
|
+
swingVel: (Math.random() - 0.5) * 0.05, // 초기 흔들림 속도
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
movementMode = 'rappelling';
|
|
683
|
+
onSurface = false;
|
|
684
|
+
currentSurface = null;
|
|
685
|
+
|
|
686
|
+
if (typeof StateMachine !== 'undefined') {
|
|
687
|
+
StateMachine.forceState('rappelling');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* 레펠 해제: 실을 놓아 자유 낙하 전환
|
|
693
|
+
*/
|
|
694
|
+
function releaseThread() {
|
|
695
|
+
if (!thread) return;
|
|
696
|
+
thread = null;
|
|
697
|
+
vy = 0;
|
|
698
|
+
movementMode = 'falling';
|
|
699
|
+
onSurface = false;
|
|
700
|
+
|
|
701
|
+
if (typeof StateMachine !== 'undefined') {
|
|
702
|
+
StateMachine.forceState('falling');
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* 화면 중앙으로 이동
|
|
708
|
+
* 천장에서는 레펠로 하강, 그 외에는 점프
|
|
709
|
+
*/
|
|
710
|
+
function moveToCenter() {
|
|
711
|
+
const cx = (screenW - CHAR_SIZE) / 2;
|
|
712
|
+
const cy = (screenH - CHAR_SIZE) / 2;
|
|
713
|
+
|
|
714
|
+
if (edge === 'top') {
|
|
715
|
+
// 천장에서는 레펠로 내려감
|
|
716
|
+
startRappel();
|
|
717
|
+
} else {
|
|
718
|
+
// 바닥/벽에서는 중앙으로 점프
|
|
719
|
+
jumpTo(cx, cy);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ===================================
|
|
724
|
+
// 애니메이션 프레임 갱신
|
|
725
|
+
// ===================================
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* 현재 상태에 맞는 애니메이션 프레임 렌더링
|
|
729
|
+
* 공중 상태일 때는 기존 프레임셋을 재활용
|
|
730
|
+
*
|
|
731
|
+
* @param {string} state - StateMachine 상태
|
|
732
|
+
* @param {number} timestamp - requestAnimationFrame 타임스탬프
|
|
733
|
+
*/
|
|
161
734
|
function updateAnimation(state, timestamp) {
|
|
162
735
|
if (timestamp - lastAnimTime > ANIM_INTERVAL) {
|
|
163
736
|
animFrame++;
|
|
164
737
|
lastAnimTime = timestamp;
|
|
165
738
|
}
|
|
166
|
-
|
|
739
|
+
|
|
740
|
+
// 이동 모드에 따라 적절한 프레임셋으로 매핑
|
|
741
|
+
let effectiveState = state;
|
|
742
|
+
if (movementMode === 'jumping') effectiveState = 'jumping';
|
|
743
|
+
if (movementMode === 'falling') effectiveState = 'falling';
|
|
744
|
+
if (movementMode === 'rappelling') effectiveState = 'rappelling';
|
|
745
|
+
|
|
746
|
+
const frameCount = Character.getFrameCount(effectiveState);
|
|
167
747
|
const currentFrame = animFrame % frameCount;
|
|
168
|
-
Character.renderFrame(
|
|
748
|
+
Character.renderFrame(effectiveState, currentFrame, flipX);
|
|
169
749
|
}
|
|
170
750
|
|
|
751
|
+
// ===================================
|
|
752
|
+
// 위치/상태 접근자
|
|
753
|
+
// ===================================
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* 현재 위치 및 상태 정보 반환
|
|
757
|
+
* @returns {{ x, y, edge, direction, flipX, movementMode, onSurface, thread }}
|
|
758
|
+
*/
|
|
171
759
|
function getPosition() {
|
|
172
|
-
return {
|
|
760
|
+
return {
|
|
761
|
+
x, y, edge, direction, flipX,
|
|
762
|
+
movementMode, onSurface,
|
|
763
|
+
thread: thread ? true : false,
|
|
764
|
+
};
|
|
173
765
|
}
|
|
174
766
|
|
|
767
|
+
/**
|
|
768
|
+
* 위치 직접 설정 (드래그 등)
|
|
769
|
+
*/
|
|
175
770
|
function setPosition(nx, ny) {
|
|
176
771
|
x = nx;
|
|
177
772
|
y = ny;
|
|
178
773
|
clampPosition();
|
|
179
|
-
|
|
774
|
+
updateVisual();
|
|
180
775
|
}
|
|
181
776
|
|
|
182
777
|
function setEdge(newEdge) {
|
|
@@ -188,7 +783,10 @@ const PetEngine = (() => {
|
|
|
188
783
|
flipX = dir < 0;
|
|
189
784
|
}
|
|
190
785
|
|
|
191
|
-
|
|
786
|
+
/**
|
|
787
|
+
* 가장 가까운 가장자리로 즉시 이동 (드래그 후)
|
|
788
|
+
* 모든 물리 상태를 초기화하고 표면에 붙음
|
|
789
|
+
*/
|
|
192
790
|
function snapToNearestEdge() {
|
|
193
791
|
const distBottom = screenH - CHAR_SIZE - y;
|
|
194
792
|
const distTop = y;
|
|
@@ -209,15 +807,57 @@ const PetEngine = (() => {
|
|
|
209
807
|
x = screenW - CHAR_SIZE;
|
|
210
808
|
edge = 'right';
|
|
211
809
|
}
|
|
212
|
-
|
|
810
|
+
|
|
811
|
+
// 물리 상태 완전 초기화
|
|
812
|
+
movementMode = 'crawling';
|
|
813
|
+
onSurface = true;
|
|
814
|
+
currentSurface = null;
|
|
815
|
+
vx = 0;
|
|
816
|
+
vy = 0;
|
|
817
|
+
thread = null;
|
|
818
|
+
|
|
819
|
+
updateVisual();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* 자유 낙하 시작 (화면 중앙 근처에서 놓았을 때)
|
|
824
|
+
* 중력에 의해 바닥 또는 가장 가까운 표면으로 떨어짐
|
|
825
|
+
*/
|
|
826
|
+
function startFalling() {
|
|
827
|
+
movementMode = 'falling';
|
|
828
|
+
onSurface = false;
|
|
829
|
+
currentSurface = null;
|
|
830
|
+
vx = 0;
|
|
831
|
+
vy = 0;
|
|
832
|
+
thread = null;
|
|
833
|
+
|
|
834
|
+
if (typeof StateMachine !== 'undefined') {
|
|
835
|
+
StateMachine.forceState('falling');
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* 레펠 스레드 정보 반환
|
|
841
|
+
* @returns {object|null}
|
|
842
|
+
*/
|
|
843
|
+
function getThread() {
|
|
844
|
+
return thread;
|
|
213
845
|
}
|
|
214
846
|
|
|
847
|
+
// ===================================
|
|
848
|
+
// 메인 루프
|
|
849
|
+
// ===================================
|
|
850
|
+
|
|
215
851
|
let frameId = null;
|
|
216
852
|
|
|
853
|
+
/**
|
|
854
|
+
* 엔진 시작: requestAnimationFrame 루프 가동
|
|
855
|
+
*/
|
|
217
856
|
function start() {
|
|
218
857
|
if (running) return;
|
|
219
858
|
running = true;
|
|
220
859
|
lastAnimTime = performance.now();
|
|
860
|
+
lastStepTime = Date.now();
|
|
221
861
|
|
|
222
862
|
function loop(timestamp) {
|
|
223
863
|
if (!running) return;
|
|
@@ -229,13 +869,23 @@ const PetEngine = (() => {
|
|
|
229
869
|
frameId = requestAnimationFrame(loop);
|
|
230
870
|
}
|
|
231
871
|
|
|
872
|
+
/**
|
|
873
|
+
* 엔진 정지
|
|
874
|
+
*/
|
|
232
875
|
function stop() {
|
|
233
876
|
running = false;
|
|
234
877
|
if (frameId) cancelAnimationFrame(frameId);
|
|
235
878
|
}
|
|
236
879
|
|
|
880
|
+
// --- 공개 API ---
|
|
237
881
|
return {
|
|
238
|
-
init, start, stop,
|
|
239
|
-
|
|
882
|
+
init, start, stop,
|
|
883
|
+
getPosition, setPosition, setEdge, setDirection,
|
|
884
|
+
snapToNearestEdge, setSpeedMultiplier,
|
|
885
|
+
moveForState, updateAnimation,
|
|
886
|
+
// 새 API: 물리 기반 이동
|
|
887
|
+
jumpTo, startRappel, releaseThread, moveToCenter,
|
|
888
|
+
setSurfaces, getThread, startFalling,
|
|
889
|
+
CHAR_SIZE,
|
|
240
890
|
};
|
|
241
891
|
})();
|