@@ -140,6 +140,7 @@ export function createDoug(scene, gradientMap) { |
| 140 | 140 | position: new THREE.Vector3(0, 0, 0), |
| 141 | 141 | targetPosition: new THREE.Vector3(0, 0, 0), |
| 142 | 142 | rotation: 0, |
| 143 | + targetRotation: 0, |
| 143 | 144 | mode: 'idle', // 'idle', 'waiting', 'swimming' |
| 144 | 145 | waitTimer: 0, |
| 145 | 146 | waitDuration: 0, |
@@ -152,6 +153,16 @@ export function createDoug(scene, gradientMap) { |
| 152 | 153 | // Movement speeds |
| 153 | 154 | const idleSpeed = 0.5 |
| 154 | 155 | const swimSpeed = 1.2 |
| 156 | + const turnSpeed = 4 // radians per second |
| 157 | + |
| 158 | + // Helper to lerp angles properly (handles wraparound) |
| 159 | + function lerpAngle(from, to, t) { |
| 160 | + let diff = to - from |
| 161 | + // Normalize to -PI to PI |
| 162 | + while (diff > Math.PI) diff -= Math.PI * 2 |
| 163 | + while (diff < -Math.PI) diff += Math.PI * 2 |
| 164 | + return from + diff * t |
| 165 | + } |
| 155 | 166 | |
| 156 | 167 | function pickIdleTarget(pond) { |
| 157 | 168 | const angle = Math.random() * Math.PI * 2 |
@@ -206,19 +217,32 @@ export function createDoug(scene, gradientMap) { |
| 206 | 217 | const dist = Math.sqrt(dx * dx + dz * dz) |
| 207 | 218 | |
| 208 | 219 | if (dist > 0.1) { |
| 209 | | - const speed = state.mode === 'swimming' ? swimSpeed : idleSpeed |
| 210 | | - const moveAmount = Math.min(speed * delta, dist) |
| 211 | | - const moveX = (dx / dist) * moveAmount |
| 212 | | - const moveZ = (dz / dist) * moveAmount |
| 220 | + // Calculate desired rotation to face target |
| 221 | + state.targetRotation = Math.atan2(dx, dz) |
| 222 | + |
| 223 | + // Smoothly turn toward target direction |
| 224 | + state.rotation = lerpAngle(state.rotation, state.targetRotation, turnSpeed * delta) |
| 225 | + |
| 226 | + // Check if we're facing roughly the right direction (within ~30 degrees) |
| 227 | + let angleDiff = Math.abs(state.targetRotation - state.rotation) |
| 228 | + while (angleDiff > Math.PI) angleDiff -= Math.PI * 2 |
| 229 | + angleDiff = Math.abs(angleDiff) |
| 213 | 230 | |
| 214 | | - state.position.x += moveX |
| 215 | | - state.position.z += moveZ |
| 231 | + // Only move forward if facing the right way |
| 232 | + if (angleDiff < 0.5) { |
| 233 | + const speed = state.mode === 'swimming' ? swimSpeed : idleSpeed |
| 234 | + const moveAmount = Math.min(speed * delta, dist) |
| 216 | 235 | |
| 217 | | - // Face movement direction |
| 218 | | - state.rotation = Math.atan2(dz, dx) |
| 236 | + // Move in the direction Doug is FACING (not toward target directly) |
| 237 | + const moveX = Math.sin(state.rotation) * moveAmount |
| 238 | + const moveZ = Math.cos(state.rotation) * moveAmount |
| 219 | 239 | |
| 220 | | - // Wobble animation while moving |
| 221 | | - state.wobble += delta * 8 |
| 240 | + state.position.x += moveX |
| 241 | + state.position.z += moveZ |
| 242 | + |
| 243 | + // Wobble animation while moving |
| 244 | + state.wobble += delta * 8 |
| 245 | + } |
| 222 | 246 | } else { |
| 223 | 247 | // Arrived |
| 224 | 248 | if (state.mode === 'swimming') { |
@@ -254,7 +278,7 @@ export function createDoug(scene, gradientMap) { |
| 254 | 278 | group.position.y = Math.sin(elapsed * 2) * 0.03 |
| 255 | 279 | |
| 256 | 280 | // Rotation (face direction of movement) |
| 257 | | - group.rotation.y = -state.rotation + Math.PI / 2 |
| 281 | + group.rotation.y = state.rotation |
| 258 | 282 | |
| 259 | 283 | // Body wobble while swimming |
| 260 | 284 | const wobbleAmount = Math.sin(state.wobble) * 0.08 |