@@ -16,7 +16,7 @@ import {
1616 HEAD_BOB_SPEED , HEAD_BOB_AMOUNT , HEAD_BOB_THRESHOLD ,
1717 CONTROLS_HINT_FADE_DELAY
1818} from './config.js' ;
19- import { isInsideBuilding } from './city.js' ;
19+ import { isInsideBuilding , isBlockedByTree } from './city.js' ;
2020import { canPlayerMove } from './player-input.js' ;
2121
2222/** Player state */
@@ -39,8 +39,12 @@ let lookTouchId = null;
3939let lookLastX = 0 ;
4040let lookLastY = 0 ;
4141
42- /** Accumulated time for head bob (uses clock, not Date.now()) */
43- let bobTime = 0 ;
42+ /** Head bob phase (radians) */
43+ let bobPhase = 0 ;
44+
45+ /** Smoothed look (optional lag) */
46+ let viewYaw = 0 ;
47+ let viewPitch = 0 ;
4448
4549/** Controls hint visibility */
4650let controlsHintVisible = true ;
@@ -68,8 +72,9 @@ export function setupControls(renderer, camera) {
6872 rendererRef = renderer ;
6973 cameraRef = camera ;
7074
71- // Set rotation order once (not every frame)
7275 camera . rotation . order = 'YXZ' ;
76+ viewYaw = player . yaw ;
77+ viewPitch = player . pitch ;
7378
7479 // Keyboard — support both e.code (positional) and e.key (layout-aware)
7580 document . addEventListener ( 'keydown' , ( e ) => {
@@ -245,13 +250,15 @@ export function updatePlayer(delta, camera) {
245250 const newX = player . x + ( moveX * cosYaw + moveZ * sinYaw ) * speed ;
246251 const newZ = player . z + ( - moveX * sinYaw + moveZ * cosYaw ) * speed ;
247252
248- // Sliding collision detection
249- if ( ! isInsideBuilding ( newX , newZ , COLLISION_PADDING ) ) {
253+ const blocked = ( x , z ) =>
254+ isInsideBuilding ( x , z , COLLISION_PADDING ) || isBlockedByTree ( x , z ) ;
255+
256+ if ( ! blocked ( newX , newZ ) ) {
250257 player . x = newX ;
251258 player . z = newZ ;
252- } else if ( ! isInsideBuilding ( newX , player . z , COLLISION_PADDING ) ) {
259+ } else if ( ! blocked ( newX , player . z ) ) {
253260 player . x = newX ;
254- } else if ( ! isInsideBuilding ( player . x , newZ , COLLISION_PADDING ) ) {
261+ } else if ( ! blocked ( player . x , newZ ) ) {
255262 player . z = newZ ;
256263 }
257264
@@ -263,16 +270,29 @@ export function updatePlayer(delta, camera) {
263270 // Update camera position
264271 camera . position . set ( player . x , PLAYER_HEIGHT , player . z ) ;
265272
266- // Gentle head bob when moving (uses accumulated clock time, not Date.now())
267273 if ( len > HEAD_BOB_THRESHOLD ) {
268- bobTime += delta ;
269- const bobAmount = Math . sin ( bobTime / HEAD_BOB_SPEED * 0.001 ) * HEAD_BOB_AMOUNT ;
270- camera . position . y += bobAmount ;
274+ bobPhase += delta * HEAD_BOB_SPEED ;
275+ camera . position . y += Math . abs ( Math . sin ( bobPhase ) ) * HEAD_BOB_AMOUNT ;
271276 }
272277
273- // Camera rotation (order already set in setupControls)
274- camera . rotation . y = player . yaw ;
275- camera . rotation . x = player . pitch ;
278+ const lookLerp = 1 - Math . exp ( - 14 * delta ) ;
279+ viewYaw += ( player . yaw - viewYaw ) * lookLerp ;
280+ viewPitch += ( player . pitch - viewPitch ) * lookLerp ;
281+ camera . rotation . y = viewYaw ;
282+ camera . rotation . x = viewPitch ;
283+ }
284+
285+ /**
286+ * Align player look with camera (after cinematic).
287+ * @param {THREE.PerspectiveCamera } camera
288+ */
289+ export function syncPlayerLookFromCamera ( camera ) {
290+ const euler = new THREE . Euler ( 0 , 0 , 0 , 'YXZ' ) ;
291+ euler . setFromQuaternion ( camera . quaternion , 'YXZ' ) ;
292+ player . yaw = euler . y ;
293+ player . pitch = euler . x ;
294+ viewYaw = player . yaw ;
295+ viewPitch = player . pitch ;
276296}
277297
278298/**
0 commit comments