feat: Add cheat menu with hitbox toggle and car-relative ramp spawning functionality.
This commit is contained in:
106
src/App.tsx
106
src/App.tsx
@@ -6,45 +6,87 @@ import { Car } from './Car'
|
|||||||
import { WorldBorder } from './WorldBorder'
|
import { WorldBorder } from './WorldBorder'
|
||||||
import { Mesh } from 'three'
|
import { Mesh } from 'three'
|
||||||
|
|
||||||
|
import { CheatMenu } from './CheatMenu'
|
||||||
|
import { Ramp } from './Ramp'
|
||||||
|
import { Vector3, Euler } from 'three'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [isDriving, setIsDriving] = useState(false)
|
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<Mesh>(null)
|
const carRef = useRef<Mesh>(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 (
|
return (
|
||||||
<Canvas camera={{ position: [0, 5, 10] }} gl={{ antialias: true }}>
|
<>
|
||||||
<Sky sunPosition={[100, 20, 100]} distance={5000} />
|
<Canvas camera={{ position: [0, 5, 10] }} gl={{ antialias: true }}>
|
||||||
<ambientLight intensity={0.5} />
|
<Sky sunPosition={[100, 20, 100]} distance={5000} />
|
||||||
<pointLight position={[10, 10, 10]} />
|
<ambientLight intensity={0.5} />
|
||||||
|
<pointLight position={[10, 10, 10]} />
|
||||||
|
|
||||||
{/* Ground */}
|
{/* Ground */}
|
||||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]}>
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} userData={{ isGround: true }}>
|
||||||
<planeGeometry args={[2500, 2500]} />
|
<planeGeometry args={[2500, 2500]} />
|
||||||
<meshStandardMaterial color="#55aaff" />
|
<meshStandardMaterial color="#55aaff" />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
args={[2500, 2500]}
|
args={[2500, 2500]}
|
||||||
fadeDistance={2000}
|
fadeDistance={2000}
|
||||||
sectionSize={10}
|
sectionSize={10}
|
||||||
sectionThickness={1}
|
sectionThickness={1}
|
||||||
sectionColor="#ffffff"
|
sectionColor="#ffffff"
|
||||||
cellSize={1}
|
cellSize={1}
|
||||||
cellThickness={0.5}
|
cellThickness={0.5}
|
||||||
cellColor="#aaccff"
|
cellColor="#aaccff"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<WorldBorder />
|
||||||
|
|
||||||
|
{ramps.map(ramp => (
|
||||||
|
<Ramp key={ramp.id} position={ramp.position} rotation={ramp.rotation} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Character
|
||||||
|
isDriving={isDriving}
|
||||||
|
onEnter={() => setIsDriving(true)}
|
||||||
|
carRef={carRef}
|
||||||
|
showHitboxes={showHitboxes}
|
||||||
|
/>
|
||||||
|
<Car
|
||||||
|
ref={carRef}
|
||||||
|
isDriving={isDriving}
|
||||||
|
onExit={() => setIsDriving(false)}
|
||||||
|
showHitboxes={showHitboxes}
|
||||||
|
/>
|
||||||
|
</Canvas>
|
||||||
|
<CheatMenu
|
||||||
|
showHitboxes={showHitboxes}
|
||||||
|
setShowHitboxes={setShowHitboxes}
|
||||||
|
onSpawnRamp={spawnRamp}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
<WorldBorder />
|
|
||||||
|
|
||||||
<Character
|
|
||||||
isDriving={isDriving}
|
|
||||||
onEnter={() => setIsDriving(true)}
|
|
||||||
carRef={carRef}
|
|
||||||
/>
|
|
||||||
<Car
|
|
||||||
ref={carRef}
|
|
||||||
isDriving={isDriving}
|
|
||||||
onExit={() => setIsDriving(false)}
|
|
||||||
/>
|
|
||||||
</Canvas>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
148
src/Car.tsx
148
src/Car.tsx
@@ -2,15 +2,17 @@
|
|||||||
import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react'
|
import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react'
|
||||||
import { useFrame, useThree } from '@react-three/fiber'
|
import { useFrame, useThree } from '@react-three/fiber'
|
||||||
import { Vector3, Mesh, Euler } from 'three'
|
import { Vector3, Mesh, Euler } from 'three'
|
||||||
|
import * as THREE from 'three'
|
||||||
import { useGLTF } from '@react-three/drei'
|
import { useGLTF } from '@react-three/drei'
|
||||||
|
|
||||||
interface CarProps {
|
interface CarProps {
|
||||||
isDriving: boolean
|
isDriving: boolean
|
||||||
onExit: () => void
|
onExit: () => void
|
||||||
position?: [number, number, number]
|
position?: [number, number, number]
|
||||||
|
showHitboxes?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [5, 0.5, 5] }, ref) => {
|
export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [5, 0.5, 5], showHitboxes = false }, ref) => {
|
||||||
const internalRef = useRef<Mesh>(null)
|
const internalRef = useRef<Mesh>(null)
|
||||||
useImperativeHandle(ref, () => internalRef.current!)
|
useImperativeHandle(ref, () => internalRef.current!)
|
||||||
|
|
||||||
@@ -26,6 +28,13 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
|||||||
distance: 15
|
distance: 15
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Initial position setup
|
||||||
|
useEffect(() => {
|
||||||
|
if (internalRef.current) {
|
||||||
|
internalRef.current.position.set(...position)
|
||||||
|
}
|
||||||
|
}, []) // Run only once on mount
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset camera yaw and pitch when entering vehicle
|
// Reset camera yaw and pitch when entering vehicle
|
||||||
if (isDriving) {
|
if (isDriving) {
|
||||||
@@ -77,56 +86,107 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
|||||||
useFrame((_state, delta) => {
|
useFrame((_state, delta) => {
|
||||||
if (!internalRef.current) return
|
if (!internalRef.current) return
|
||||||
|
|
||||||
if (isDriving) {
|
// Car Physics / Movement
|
||||||
// Car Physics / Movement
|
const maxSpeed = 60
|
||||||
const maxSpeed = 60
|
const acceleration = 30
|
||||||
const acceleration = 30
|
const friction = 5
|
||||||
const friction = 5
|
const turnSpeed = 2
|
||||||
const turnSpeed = 2
|
|
||||||
|
|
||||||
|
// Input handling (only when driving)
|
||||||
|
if (isDriving) {
|
||||||
if (keys.current.w) speed.current += acceleration * delta
|
if (keys.current.w) speed.current += acceleration * delta
|
||||||
if (keys.current.s) speed.current -= acceleration * delta
|
if (keys.current.s) speed.current -= acceleration * delta
|
||||||
|
}
|
||||||
|
|
||||||
// Friction
|
// Friction (always apply if not accelerating)
|
||||||
if (!keys.current.w && !keys.current.s) {
|
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 (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
|
if (Math.abs(speed.current) < 0.1) speed.current = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp speed
|
// Clamp speed
|
||||||
speed.current = Math.max(-maxSpeed / 2, Math.min(maxSpeed, speed.current))
|
speed.current = Math.max(-maxSpeed / 2, Math.min(maxSpeed, speed.current))
|
||||||
|
|
||||||
// Steering
|
// Steering (only when driving and moving)
|
||||||
if (Math.abs(speed.current) > 0.1) {
|
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.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)
|
if (keys.current.d) internalRef.current.rotation.y -= turnSpeed * delta * (speed.current > 0 ? 1 : -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move
|
// Move (always apply speed)
|
||||||
internalRef.current.translateZ(speed.current * delta)
|
internalRef.current.translateZ(speed.current * delta)
|
||||||
|
|
||||||
// Clamp position to map boundaries with rotation-aware hitbox
|
// Clamp position to map boundaries with rotation-aware hitbox
|
||||||
const mapSize = 2500
|
const mapSize = 2500
|
||||||
const halfMapSize = mapSize / 2
|
const halfMapSize = mapSize / 2
|
||||||
const carWidth = 3
|
const carWidth = 3
|
||||||
const carLength = 7
|
const carLength = 7
|
||||||
const halfWidth = carWidth / 2
|
const halfWidth = carWidth / 2
|
||||||
const halfLength = carLength / 2
|
const halfLength = carLength / 2
|
||||||
|
|
||||||
const yaw = internalRef.current.rotation.y
|
const yaw = internalRef.current.rotation.y
|
||||||
const absSin = Math.abs(Math.sin(yaw))
|
const absSin = Math.abs(Math.sin(yaw))
|
||||||
const absCos = Math.abs(Math.cos(yaw))
|
const absCos = Math.abs(Math.cos(yaw))
|
||||||
|
|
||||||
const extentX = absSin * halfLength + absCos * halfWidth
|
const extentX = absSin * halfLength + absCos * halfWidth
|
||||||
const extentZ = absCos * halfLength + absSin * halfWidth
|
const extentZ = absCos * halfLength + absSin * halfWidth
|
||||||
|
|
||||||
const boundaryX = halfMapSize - extentX
|
const boundaryX = halfMapSize - extentX
|
||||||
const boundaryZ = halfMapSize - extentZ
|
const boundaryZ = halfMapSize - extentZ
|
||||||
|
|
||||||
internalRef.current.position.x = Math.max(-boundaryX, Math.min(boundaryX, internalRef.current.position.x))
|
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.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
|
// Camera Follow
|
||||||
const { yaw: camYaw, pitch, distance } = cameraState.current
|
const { yaw: camYaw, pitch, distance } = cameraState.current
|
||||||
|
|
||||||
@@ -164,14 +224,16 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={internalRef} position={new Vector3(...position)}>
|
<group ref={internalRef}>
|
||||||
<primitive object={gltf.scene} scale={1.5} rotation={[0, 0, 0]} />
|
<primitive object={gltf.scene} scale={1.5} rotation={[0, 0, 0]} />
|
||||||
|
|
||||||
{/* Debug Hitbox */}
|
{/* Debug Hitbox */}
|
||||||
<mesh position={[0, 0.75, 0]}>
|
{showHitboxes && (
|
||||||
<boxGeometry args={[3, 1.5, 7]} />
|
<mesh position={[0, 1, 0]}>
|
||||||
<meshBasicMaterial wireframe color="yellow" />
|
<boxGeometry args={[3, 2, 7]} />
|
||||||
</mesh>
|
<meshBasicMaterial wireframe color="yellow" />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useRef, useEffect } from 'react'
|
import { useRef, useEffect } from 'react'
|
||||||
import { useFrame, useThree } from '@react-three/fiber'
|
import { useFrame, useThree } from '@react-three/fiber'
|
||||||
import { Vector3, Mesh, Euler } from 'three'
|
import { Vector3, Mesh, Euler } from 'three'
|
||||||
|
import * as THREE from 'three'
|
||||||
import { useGLTF } from '@react-three/drei'
|
import { useGLTF } from '@react-three/drei'
|
||||||
|
|
||||||
interface CharacterProps {
|
interface CharacterProps {
|
||||||
isDriving: boolean
|
isDriving: boolean
|
||||||
onEnter: () => void
|
onEnter: () => void
|
||||||
carRef: React.RefObject<Mesh | null>
|
carRef: React.RefObject<Mesh | null>
|
||||||
|
showHitboxes?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Character({ isDriving, onEnter, carRef }: CharacterProps) {
|
export function Character({ isDriving, onEnter, carRef, showHitboxes = false }: CharacterProps) {
|
||||||
const ref = useRef<Mesh>(null)
|
const ref = useRef<Mesh>(null)
|
||||||
const gltf = useGLTF('/character.glb')
|
const gltf = useGLTF('/character.glb')
|
||||||
const { camera } = useThree()
|
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.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))
|
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)
|
// Physics (Gravity & Jumping)
|
||||||
const gravity = 30
|
const gravity = 30
|
||||||
const jumpForce = 12
|
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
|
// Apply gravity
|
||||||
velocity.current.y -= gravity * delta
|
velocity.current.y -= gravity * delta
|
||||||
@@ -203,10 +252,12 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) {
|
|||||||
<primitive object={gltf.scene} rotation={[0, 0, 0]} />
|
<primitive object={gltf.scene} rotation={[0, 0, 0]} />
|
||||||
|
|
||||||
{/* Debug Hitbox */}
|
{/* Debug Hitbox */}
|
||||||
<mesh position={[0, 0.9, 0]}>
|
{showHitboxes && (
|
||||||
<cylinderGeometry args={[0.5, 0.5, 1.8, 16]} />
|
<mesh position={[0, 0.9, 0]}>
|
||||||
<meshBasicMaterial wireframe color="cyan" />
|
<cylinderGeometry args={[0.5, 0.5, 1.8, 16]} />
|
||||||
</mesh>
|
<meshBasicMaterial wireframe color="cyan" />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/CheatMenu.tsx
Normal file
58
src/CheatMenu.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
onPointerDown={(e) => 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'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ margin: '0 0 5px 0', fontSize: '16px', color: '#ff00ff' }}>CHEAT MENU</h3>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="hitbox-toggle"
|
||||||
|
checked={showHitboxes}
|
||||||
|
onChange={(e) => setShowHitboxes(e.target.checked)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
/>
|
||||||
|
<label htmlFor="hitbox-toggle" style={{ cursor: 'pointer', userSelect: 'none' }}>
|
||||||
|
Show Hitboxes
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onSpawnRamp}
|
||||||
|
style={{
|
||||||
|
background: '#444',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid #666',
|
||||||
|
padding: '5px 10px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontFamily: 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Spawn Ramp
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/Ramp.tsx
Normal file
18
src/Ramp.tsx
Normal file
@@ -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 (
|
||||||
|
<mesh position={new Vector3(...position)} rotation={new Euler(...rotation)} userData={{ isGround: true }}>
|
||||||
|
{/* A wedge shape using a box geometry rotated or a custom geometry */}
|
||||||
|
{/* For simplicity, let's use a box rotated to form a ramp */}
|
||||||
|
<boxGeometry args={[4, 1, 10]} />
|
||||||
|
<meshStandardMaterial color="orange" />
|
||||||
|
</mesh>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user