diff --git a/src/App.tsx b/src/App.tsx index 9aa3909..d6b6b60 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,9 @@ import { useState, useRef } from 'react' import { Canvas } from '@react-three/fiber' -import { Sky } from '@react-three/drei' +import { Sky, Grid } from '@react-three/drei' import { Character } from './Character' import { Car } from './Car' +import { WorldBorder } from './WorldBorder' import { Mesh } from 'three' export default function App() { @@ -10,17 +11,30 @@ export default function App() { const carRef = useRef(null) return ( - - + + {/* Ground */} - - + + + + + + setIsDriving(true)} diff --git a/src/Car.tsx b/src/Car.tsx index 21af818..c64ce55 100644 --- a/src/Car.tsx +++ b/src/Car.tsx @@ -1,3 +1,4 @@ + import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react' import { useFrame, useThree } from '@react-three/fiber' import { Vector3, Mesh, Euler } from 'three' @@ -13,7 +14,7 @@ export const Car = forwardRef(({ isDriving, onExit, position = [ const internalRef = useRef(null) useImperativeHandle(ref, () => internalRef.current!) - const { scene } = useGLTF('/car.glb') + const gltf = useGLTF('/car.glb') const { camera } = useThree() const keys = useRef({ w: false, a: false, s: false, d: false, f: false }) const speed = useRef(0) @@ -36,8 +37,8 @@ export const Car = forwardRef(({ isDriving, onExit, position = [ useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const key = e.key.toLowerCase() - if (keys.current.hasOwnProperty(key)) { - (keys.current as any)[key] = true + if (Object.prototype.hasOwnProperty.call(keys.current, key)) { + (keys.current as Record)[key] = true } if (e.key.toLowerCase() === 'f') { if (isDriving) { @@ -47,8 +48,8 @@ export const Car = forwardRef(({ isDriving, onExit, position = [ } const handleKeyUp = (e: KeyboardEvent) => { const key = e.key.toLowerCase() - if (keys.current.hasOwnProperty(key)) { - (keys.current as any)[key] = false + if (Object.prototype.hasOwnProperty.call(keys.current, key)) { + (keys.current as Record)[key] = false } } @@ -105,8 +106,29 @@ export const Car = forwardRef(({ isDriving, onExit, position = [ // Move internalRef.current.translateZ(speed.current * delta) + // Clamp position to map boundaries with rotation-aware hitbox + const mapSize = 2500 + const halfMapSize = mapSize / 2 + const carWidth = 3 + const carLength = 7 + const halfWidth = carWidth / 2 + const halfLength = carLength / 2 + + const yaw = internalRef.current.rotation.y + const absSin = Math.abs(Math.sin(yaw)) + const absCos = Math.abs(Math.cos(yaw)) + + const extentX = absSin * halfLength + absCos * halfWidth + const extentZ = absCos * halfLength + absSin * halfWidth + + const boundaryX = halfMapSize - extentX + const boundaryZ = halfMapSize - extentZ + + internalRef.current.position.x = Math.max(-boundaryX, Math.min(boundaryX, internalRef.current.position.x)) + internalRef.current.position.z = Math.max(-boundaryZ, Math.min(boundaryZ, internalRef.current.position.z)) + // Camera Follow - const { yaw, pitch, distance } = cameraState.current + const { yaw: camYaw, pitch, distance } = cameraState.current // Target position (above car) const targetPos = internalRef.current.position.clone().add(new Vector3(0, 2, 0)) @@ -114,7 +136,7 @@ export const Car = forwardRef(({ isDriving, onExit, position = [ // Calculate max camera position (relative to target) const maxCamPos = new Vector3(0, 0, distance) // Apply rotation: pitch, then yaw + car rotation + 180deg (to look from behind) - maxCamPos.applyEuler(new Euler(pitch, yaw + internalRef.current.rotation.y + Math.PI, 0, 'YXZ')) + maxCamPos.applyEuler(new Euler(pitch, camYaw + internalRef.current.rotation.y + Math.PI, 0, 'YXZ')) // Check for ground collision const minCameraHeight = 0.5 @@ -132,7 +154,7 @@ export const Car = forwardRef(({ isDriving, onExit, position = [ } const actualCamPos = new Vector3(0, 0, actualDistance) - actualCamPos.applyEuler(new Euler(pitch, yaw + internalRef.current.rotation.y + Math.PI, 0, 'YXZ')) + actualCamPos.applyEuler(new Euler(pitch, camYaw + internalRef.current.rotation.y + Math.PI, 0, 'YXZ')) // Lerp to the collided position // Increased lerp speed from 0.1 to 0.5 to reduce lag @@ -142,8 +164,14 @@ export const Car = forwardRef(({ isDriving, onExit, position = [ }) return ( - - - + + + + {/* Debug Hitbox */} + + + + + ) }) diff --git a/src/Character.tsx b/src/Character.tsx index f034164..aa785a5 100644 --- a/src/Character.tsx +++ b/src/Character.tsx @@ -11,7 +11,7 @@ interface CharacterProps { export function Character({ isDriving, onEnter, carRef }: CharacterProps) { const ref = useRef(null) - const { scene } = useGLTF('/character.glb') + const gltf = useGLTF('/character.glb') const { camera } = useThree() const keys = useRef({ w: false, a: false, s: false, d: false, shift: false, space: false }) const velocity = useRef(new Vector3(0, 0, 0)) @@ -26,8 +26,8 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const key = e.key.toLowerCase() - if (keys.current.hasOwnProperty(key)) { - (keys.current as any)[key] = true + if (Object.prototype.hasOwnProperty.call(keys.current, key)) { + (keys.current as Record)[key] = true } if (e.key === 'Shift') keys.current.shift = true if (e.key === ' ') keys.current.space = true @@ -43,8 +43,8 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) { } const handleKeyUp = (e: KeyboardEvent) => { const key = e.key.toLowerCase() - if (keys.current.hasOwnProperty(key)) { - (keys.current as any)[key] = false + if (Object.prototype.hasOwnProperty.call(keys.current, key)) { + (keys.current as Record)[key] = false } if (e.key === 'Shift') keys.current.shift = false if (e.key === ' ') keys.current.space = false @@ -100,7 +100,7 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) { ref.current.position.y = 1 // Ensure above ground velocity.current.set(0, 0, 0) } - }, [isDriving]) + }, [isDriving, carRef]) useFrame((_state, delta) => { if (!ref.current || isDriving) return @@ -128,6 +128,15 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) { ref.current.lookAt(ref.current.position.clone().add(direction)) } + // Clamp position to map boundaries with radius + const mapSize = 2500 + const halfMapSize = mapSize / 2 + const radius = 0.5 + const boundary = halfMapSize - radius + + ref.current.position.x = Math.max(-boundary, Math.min(boundary, ref.current.position.x)) + ref.current.position.z = Math.max(-boundary, Math.min(boundary, ref.current.position.z)) + // Physics (Gravity & Jumping) const gravity = 30 const jumpForce = 12 @@ -190,8 +199,14 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) { }) return ( - - - + + + + {/* Debug Hitbox */} + + + + + ) } diff --git a/src/WorldBorder.tsx b/src/WorldBorder.tsx new file mode 100644 index 0000000..2bd80b1 --- /dev/null +++ b/src/WorldBorder.tsx @@ -0,0 +1,103 @@ +import { useRef, useMemo } from 'react' +import { useFrame, useThree } from '@react-three/fiber' +import { ShaderMaterial, Vector3, DoubleSide } from 'three' + +export function WorldBorder() { + const materialRef = useRef(null) + const { camera } = useThree() + + const uniforms = useMemo( + () => ({ + uPlayerPos: { value: new Vector3() }, + uVisibleDistance: { value: 200.0 }, + }), + [] + ) + + useFrame(() => { + if (materialRef.current) { + materialRef.current.uniforms.uPlayerPos.value.copy(camera.position) + } + }) + + const vertexShader = ` + varying vec3 vWorldPos; +void main() { + vec4 worldPosition = modelMatrix * vec4(position, 1.0); + vWorldPos = worldPosition.xyz; + gl_Position = projectionMatrix * viewMatrix * worldPosition; +} +` + + const fragmentShader = ` + uniform vec3 uPlayerPos; + uniform float uVisibleDistance; + varying vec3 vWorldPos; + +void main() { + // Calculate distance from player to this fragment + float distToPlayer = distance(uPlayerPos, vWorldPos); + + // Fade based on distance + float alpha = 1.0 - smoothstep(0.0, uVisibleDistance, distToPlayer); + + if (alpha <= 0.0) discard; + + gl_FragColor = vec4(1.0, 0.0, 0.0, alpha * 0.8); +} +` + + const mapSize = 2500 + const halfSize = mapSize / 2 + const wallHeight = 100 + + return ( + + {/* North Wall (-Z) */} + + + + + {/* South Wall (+Z) */} + + + + + {/* East Wall (+X) */} + + + + + {/* West Wall (-X) */} + + + + + + ) +}