From 12a4e777f787765c8e0c5d3cbbb2469062a29112 Mon Sep 17 00:00:00 2001 From: KaseToatz1337 <112391293+Raeven69@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:01:25 +0100 Subject: [PATCH] feat: Add cheat menu with hitbox toggle and car-relative ramp spawning functionality. --- src/App.tsx | 106 +++++++++++++++++++++++---------- src/Car.tsx | 148 ++++++++++++++++++++++++++++++++-------------- src/Character.tsx | 63 ++++++++++++++++++-- src/CheatMenu.tsx | 58 ++++++++++++++++++ src/Ramp.tsx | 18 ++++++ 5 files changed, 312 insertions(+), 81 deletions(-) create mode 100644 src/CheatMenu.tsx create mode 100644 src/Ramp.tsx diff --git a/src/App.tsx b/src/App.tsx index d6b6b60..f1c2d01 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,45 +6,87 @@ import { Car } from './Car' import { WorldBorder } from './WorldBorder' import { Mesh } from 'three' +import { CheatMenu } from './CheatMenu' +import { Ramp } from './Ramp' +import { Vector3, Euler } from 'three' + export default function App() { const [isDriving, setIsDriving] = useState(false) + const [showHitboxes, setShowHitboxes] = useState(false) + const [ramps, setRamps] = useState<{ id: number; position: [number, number, number]; rotation: [number, number, number] }[]>([]) const carRef = useRef(null) + const spawnRamp = () => { + if (carRef.current) { + const carPos = carRef.current.position + const carRot = carRef.current.rotation + + // Calculate position in front of car + const forward = new Vector3(0, 0, 1).applyEuler(carRot) + const spawnPos = carPos.clone().add(forward.multiplyScalar(10)) + + // Rotation: same as car but pitched up slightly? Or just flat? + // Let's make it flat but rotated to face the car's direction + // Actually, a ramp usually goes UP. If we use a box, we need to pitch it. + // Let's pitch it up by 15 degrees (approx 0.26 rad) + const spawnRot: [number, number, number] = [0.26, carRot.y, 0] + + setRamps(prev => [...prev, { + id: Date.now(), + position: [spawnPos.x, 0.5, spawnPos.z], // Ensure it's on ground + rotation: spawnRot + }]) + } + } + return ( - - - - + <> + + + + - {/* Ground */} - - - - + {/* Ground */} + + + + - + + + + {ramps.map(ramp => ( + + ))} + + setIsDriving(true)} + carRef={carRef} + showHitboxes={showHitboxes} + /> + setIsDriving(false)} + showHitboxes={showHitboxes} + /> + + - - - - setIsDriving(true)} - carRef={carRef} - /> - setIsDriving(false)} - /> - + ) } diff --git a/src/Car.tsx b/src/Car.tsx index c64ce55..a200a47 100644 --- a/src/Car.tsx +++ b/src/Car.tsx @@ -2,15 +2,17 @@ import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react' import { useFrame, useThree } from '@react-three/fiber' import { Vector3, Mesh, Euler } from 'three' +import * as THREE from 'three' import { useGLTF } from '@react-three/drei' interface CarProps { isDriving: boolean onExit: () => void position?: [number, number, number] + showHitboxes?: boolean } -export const Car = forwardRef(({ isDriving, onExit, position = [5, 0.5, 5] }, ref) => { +export const Car = forwardRef(({ isDriving, onExit, position = [5, 0.5, 5], showHitboxes = false }, ref) => { const internalRef = useRef(null) useImperativeHandle(ref, () => internalRef.current!) @@ -26,6 +28,13 @@ export const Car = forwardRef(({ isDriving, onExit, position = [ distance: 15 }) + // Initial position setup + useEffect(() => { + if (internalRef.current) { + internalRef.current.position.set(...position) + } + }, []) // Run only once on mount + useEffect(() => { // Reset camera yaw and pitch when entering vehicle if (isDriving) { @@ -77,56 +86,107 @@ export const Car = forwardRef(({ isDriving, onExit, position = [ useFrame((_state, delta) => { if (!internalRef.current) return - if (isDriving) { - // Car Physics / Movement - const maxSpeed = 60 - const acceleration = 30 - const friction = 5 - const turnSpeed = 2 + // Car Physics / Movement + const maxSpeed = 60 + const acceleration = 30 + const friction = 5 + const turnSpeed = 2 + // Input handling (only when driving) + if (isDriving) { if (keys.current.w) speed.current += acceleration * delta if (keys.current.s) speed.current -= acceleration * delta + } - // Friction - if (!keys.current.w && !keys.current.s) { - if (speed.current > 0) speed.current -= friction * delta - if (speed.current < 0) speed.current += friction * delta - if (Math.abs(speed.current) < 0.1) speed.current = 0 - } + // Friction (always apply if not accelerating) + if (!isDriving || (!keys.current.w && !keys.current.s)) { + if (speed.current > 0) speed.current -= friction * delta + if (speed.current < 0) speed.current += friction * delta + if (Math.abs(speed.current) < 0.1) speed.current = 0 + } - // Clamp speed - speed.current = Math.max(-maxSpeed / 2, Math.min(maxSpeed, speed.current)) + // Clamp speed + speed.current = Math.max(-maxSpeed / 2, Math.min(maxSpeed, speed.current)) - // Steering - if (Math.abs(speed.current) > 0.1) { - if (keys.current.a) internalRef.current.rotation.y += turnSpeed * delta * (speed.current > 0 ? 1 : -1) - if (keys.current.d) internalRef.current.rotation.y -= turnSpeed * delta * (speed.current > 0 ? 1 : -1) - } + // Steering (only when driving and moving) + if (isDriving && Math.abs(speed.current) > 0.1) { + if (keys.current.a) internalRef.current.rotation.y += turnSpeed * delta * (speed.current > 0 ? 1 : -1) + if (keys.current.d) internalRef.current.rotation.y -= turnSpeed * delta * (speed.current > 0 ? 1 : -1) + } - // Move - internalRef.current.translateZ(speed.current * delta) + // Move (always apply speed) + 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 + // 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 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 extentX = absSin * halfLength + absCos * halfWidth + const extentZ = absCos * halfLength + absSin * halfWidth - const boundaryX = halfMapSize - extentX - const boundaryZ = halfMapSize - extentZ + 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)) + 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)) + // Ground Collision & Pitch + // Cast rays from front and back to determine pitch and height + const carPos = internalRef.current.position + const carRot = internalRef.current.rotation + + const forwardDir = new Vector3(0, 0, 1).applyEuler(carRot) + const frontOffset = forwardDir.clone().multiplyScalar(2.5) // Half length approx + const backOffset = forwardDir.clone().multiplyScalar(-2.5) + + const rayOriginFront = carPos.clone().add(frontOffset).add(new Vector3(0, 5, 0)) + const rayOriginBack = carPos.clone().add(backOffset).add(new Vector3(0, 5, 0)) + const rayDir = new Vector3(0, -1, 0) + + const raycasterFront = new THREE.Raycaster(rayOriginFront, rayDir, 0, 10) + const raycasterBack = new THREE.Raycaster(rayOriginBack, rayDir, 0, 10) + + const intersectsFront = raycasterFront.intersectObjects(_state.scene.children, true) + const intersectsBack = raycasterBack.intersectObjects(_state.scene.children, true) + + const groundHitFront = intersectsFront.find(hit => hit.object.userData.isGround) + const groundHitBack = intersectsBack.find(hit => hit.object.userData.isGround) + + let frontY = 0.5 + let backY = 0.5 + + if (groundHitFront) frontY = groundHitFront.point.y + 0.5 // Car half height + if (groundHitBack) backY = groundHitBack.point.y + 0.5 + + // Average height + const targetY = (frontY + backY) / 2 + + // Apply gravity if in air (simple approach: just lerp to targetY) + // For ramps, we want to snap to it + internalRef.current.position.y = THREE.MathUtils.lerp(internalRef.current.position.y, targetY, 0.2) + + // Calculate pitch + // atan2(deltaY, length) + const deltaY = frontY - backY + const dist = 5 // Distance between ray points + const targetPitch = Math.atan2(deltaY, dist) + + // Smoothly interpolate pitch + // Note: We need to preserve the yaw, so we can't just set rotation + // But rotation.x is pitch? No, it depends on order. Default is XYZ. + // If we rotate Y (yaw), then X is pitch. + internalRef.current.rotation.x = THREE.MathUtils.lerp(internalRef.current.rotation.x, targetPitch, 0.2) + + + if (isDriving) { // Camera Follow const { yaw: camYaw, pitch, distance } = cameraState.current @@ -164,14 +224,16 @@ export const Car = forwardRef(({ isDriving, onExit, position = [ }) return ( - + {/* Debug Hitbox */} - - - - + {showHitboxes && ( + + + + + )} ) }) diff --git a/src/Character.tsx b/src/Character.tsx index aa785a5..d58f3f7 100644 --- a/src/Character.tsx +++ b/src/Character.tsx @@ -1,15 +1,17 @@ import { useRef, useEffect } from 'react' import { useFrame, useThree } from '@react-three/fiber' import { Vector3, Mesh, Euler } from 'three' +import * as THREE from 'three' import { useGLTF } from '@react-three/drei' interface CharacterProps { isDriving: boolean onEnter: () => void carRef: React.RefObject + showHitboxes?: boolean } -export function Character({ isDriving, onEnter, carRef }: CharacterProps) { +export function Character({ isDriving, onEnter, carRef, showHitboxes = false }: CharacterProps) { const ref = useRef(null) const gltf = useGLTF('/character.glb') const { camera } = useThree() @@ -137,10 +139,57 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) { 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)) + // Car Collision + if (carRef.current) { + const carPos = carRef.current.position + const charPos = ref.current.position + + // Simple box collision approximation + // Car is roughly 3x7, rotated + // We'll just use a distance check for simplicity first, or a rotated box check if needed + // Let's use a simple radius check that is slightly larger than the car + // Actually, let's do a proper local coordinate check + const localCharPos = charPos.clone().sub(carPos).applyEuler(new Euler(-carRef.current.rotation.x, -carRef.current.rotation.y, -carRef.current.rotation.z)) + + const carHalfWidth = 1.5 + 0.5 // + char radius + const carHalfLength = 3.5 + 0.5 // + char radius + + if (Math.abs(localCharPos.x) < carHalfWidth && Math.abs(localCharPos.z) < carHalfLength) { + // Collision detected + // Push out along the axis of least penetration + const overlapX = carHalfWidth - Math.abs(localCharPos.x) + const overlapZ = carHalfLength - Math.abs(localCharPos.z) + + if (overlapX < overlapZ) { + localCharPos.x = Math.sign(localCharPos.x) * carHalfWidth + } else { + localCharPos.z = Math.sign(localCharPos.z) * carHalfLength + } + + // Convert back to world + const newWorldPos = localCharPos.applyEuler(carRef.current.rotation).add(carPos) + ref.current.position.x = newWorldPos.x + ref.current.position.z = newWorldPos.z + } + } + // Physics (Gravity & Jumping) const gravity = 30 const jumpForce = 12 - const groundY = 0.5 + + // Raycast for ground + const rayOrigin = ref.current.position.clone().add(new Vector3(0, 2, 0)) + const rayDir = new Vector3(0, -1, 0) + const raycaster = new THREE.Raycaster(rayOrigin, rayDir, 0, 10) + + // Filter objects with userData.isGround + const intersects = raycaster.intersectObjects(_state.scene.children, true) + const groundHit = intersects.find(hit => hit.object.userData.isGround) + + let groundY = 0.5 // Default ground + if (groundHit) { + groundY = groundHit.point.y + 0.5 // Character half-height + } // Apply gravity velocity.current.y -= gravity * delta @@ -203,10 +252,12 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) { {/* Debug Hitbox */} - - - - + {showHitboxes && ( + + + + + )} ) } diff --git a/src/CheatMenu.tsx b/src/CheatMenu.tsx new file mode 100644 index 0000000..5bae587 --- /dev/null +++ b/src/CheatMenu.tsx @@ -0,0 +1,58 @@ +import React from 'react' + +interface CheatMenuProps { + showHitboxes: boolean + setShowHitboxes: (show: boolean) => void + onSpawnRamp: () => void +} + +export function CheatMenu({ showHitboxes, setShowHitboxes, onSpawnRamp }: CheatMenuProps) { + return ( +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + style={{ + position: 'absolute', + top: '20px', + right: '20px', + background: 'rgba(0, 0, 0, 0.7)', + padding: '15px', + borderRadius: '8px', + color: 'white', + fontFamily: 'monospace', + zIndex: 1000, + border: '1px solid #444', + display: 'flex', + flexDirection: 'column', + gap: '10px' + }}> +

CHEAT MENU

+
+ setShowHitboxes(e.target.checked)} + style={{ cursor: 'pointer' }} + /> + +
+ +
+ ) +} diff --git a/src/Ramp.tsx b/src/Ramp.tsx new file mode 100644 index 0000000..49f3f53 --- /dev/null +++ b/src/Ramp.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { Vector3, Euler } from 'three' + +interface RampProps { + position: [number, number, number] + rotation: [number, number, number] +} + +export function Ramp({ position, rotation }: RampProps) { + return ( + + {/* A wedge shape using a box geometry rotated or a custom geometry */} + {/* For simplicity, let's use a box rotated to form a ramp */} + + + + ) +}