import React, { useRef, useState, useEffect } from 'react';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { Physics, useBox, usePlane, useSphere } from '@react-three/cannon';
import { Sky, Text, OrbitControls } from '@react-three/drei';
import { Vector3 } from 'three';
// Game component
export default function Game() {
const [score, setScore] = useState(0);
return (
Fugitive Chase
Score: {score}
Controls: WASD to move, Space to jump
);
}
// Ground component
function Ground() {
const [ref] = usePlane(() => ({
rotation: [-Math.PI / 2, 0, 0],
position: [0, 0, 0],
type: 'Static'
}));
return (
);
}
// Arch component
function Arch({ position }) {
const [baseRef] = useBox(() => ({
args: [10, 1, 5],
position: [position[0], position[1] + 0.5, position[2]],
type: 'Static'
}));
const [leftPillarRef] = useBox(() => ({
args: [1, 5, 5],
position: [position[0] - 4, position[1] + 3, position[2]],
type: 'Static'
}));
const [rightPillarRef] = useBox(() => ({
args: [1, 5, 5],
position: [position[0] + 4, position[1] + 3, position[2]],
type: 'Static'
}));
const [topRef] = useBox(() => ({
args: [9, 1, 5],
position: [position[0], position[1] + 6, position[2]],
type: 'Static'
}));
// Trigger zone for scoring
const [triggerRef] = useBox(() => ({
args: [7, 5, 5],
position: [position[0], position[1] + 3, position[2]],
isTrigger: true,
type: 'Static'
}));
return (
{/* Base */}
{/* Left pillar */}
{/* Right pillar */}
{/* Top */}
{/* Invisible trigger zone */}
{/* Sign */}
Ow Shitz
);
}
// Player component
function Player({ setScore }) {
const [ref, api] = useSphere(() => ({
mass: 1,
position: [0, 1, 0],
args: [0.5]
}));
const velocity = useRef([0, 0, 0]);
const position = useRef([0, 1, 0]);
// Store followers
const [followers, setFollowers] = useState([]);
// Track which fugitives are following
useEffect(() => {
const unsubscribe = api.position.subscribe(p => {
position.current = p;
});
return unsubscribe;
}, [api.position]);
useEffect(() => {
const unsubscribe = api.velocity.subscribe(v => {
velocity.current = v;
});
return unsubscribe;
}, [api.velocity]);
// Handle keyboard controls
const { camera } = useThree();
useFrame(() => {
// Update camera to follow player
camera.position.x = position.current[0];
camera.position.y = position.current[1] + 5;
camera.position.z = position.current[2] + 10;
// Look at player
camera.lookAt(position.current[0], position.current[1], position.current[2]);
});
useEffect(() => {
const handleKeyDown = (e) => {
const speed = 5;
// Movement controls
if (e.code === 'KeyW') {
api.velocity.set(velocity.current[0], velocity.current[1], -speed);
}
if (e.code === 'KeyS') {
api.velocity.set(velocity.current[0], velocity.current[1], speed);
}
if (e.code === 'KeyA') {
api.velocity.set(-speed, velocity.current[1], velocity.current[2]);
}
if (e.code === 'KeyD') {
api.velocity.set(speed, velocity.current[1], velocity.current[2]);
}
if (e.code === 'Space') {
api.velocity.set(velocity.current[0], 10, velocity.current[2]);
}
};
const handleKeyUp = (e) => {
if (['KeyW', 'KeyS'].includes(e.code)) {
api.velocity.set(velocity.current[0], velocity.current[1], 0);
}
if (['KeyA', 'KeyD'].includes(e.code)) {
api.velocity.set(0, velocity.current[1], velocity.current[2]);
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, [api]);
// Check for collisions with fugitives and arch
useEffect(() => {
// This function will be called by fugitives when they collide with player
window.playerCollideWithFugitive = (fugitiveId) => {
if (!followers.includes(fugitiveId)) {
setFollowers(prev => [...prev, fugitiveId]);
}
};
// This function will be called when a follower enters the arch
window.fugitiveEntersArch = (fugitiveId) => {
if (followers.includes(fugitiveId)) {
setFollowers(prev => prev.filter(id => id !== fugitiveId));
setScore(prev => prev + 1);
// Notify the fugitive to disappear
window.fugitiveDisappear(fugitiveId);
}
};
return () => {
window.playerCollideWithFugitive = null;
window.fugitiveEntersArch = null;
};
}, [followers, setScore]);
return (
);
}
// Fugitives component
function Fugitives({ count = 5 }) {
return (
<>
{Array.from({ length: count }).map((_, i) => (
))}
>
);
}
// Individual Fugitive component
function Fugitive({ id }) {
const [ref, api] = useSphere(() => ({
mass: 1,
position: [
Math.random() * 20 - 10,
1,
Math.random() * 20 - 10
],
args: [0.5]
}));
const position = useRef([0, 1, 0]);
const [isFollowing, setIsFollowing] = useState(false);
const [isVisible, setIsVisible] = useState(true);
const playerPosition = useRef([0, 0, 0]);
const moveDirection = useRef(new Vector3(
Math.random() * 2 - 1,
0,
Math.random() * 2 - 1
).normalize());
// Subscribe to position updates
useEffect(() => {
const unsubscribe = api.position.subscribe(p => {
position.current = p;
// Check if in arch trigger zone
if (p[2] < -17 && p[2] > -23 && p[0] > -3.5 && p[0] < 3.5 && p[1] > 0 && p[1] < 6) {
if (isFollowing && window.fugitiveEntersArch) {
window.fugitiveEntersArch(id);
}
}
});
return unsubscribe;
}, [api.position, id, isFollowing]);
// Handle collision with player
useEffect(() => {
// Check distance to player (simplified collision detection)
const checkPlayerCollision = () => {
if (!isFollowing && isVisible) {
const dx = position.current[0] - playerPosition.current[0];
const dy = position.current[1] - playerPosition.current[1];
const dz = position.current[2] - playerPosition.current[2];
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (distance < 1.5) {
setIsFollowing(true);
if (window.playerCollideWithFugitive) {
window.playerCollideWithFugitive(id);
}
}
}
};
const interval = setInterval(checkPlayerCollision, 100);
return () => clearInterval(interval);
}, [id, isFollowing, isVisible]);
// Make fugitive disappear when it enters the arch
useEffect(() => {
window.fugitiveDisappear = (fugitiveId) => {
if (fugitiveId === id) {
setIsVisible(false);
// Respawn after a delay
setTimeout(() => {
api.position.set(
Math.random() * 20 - 10,
1,
Math.random() * 20 - 10
);
setIsFollowing(false);
setIsVisible(true);
}, 3000);
}
};
return () => {
if (window.fugitiveDisappear === fugitiveDisappear) {
window.fugitiveDisappear = null;
}
};
}, [api, id]);
// Movement behavior
useFrame(() => {
if (!isVisible) return;
if (isFollowing) {
// Follow the player
const direction = new Vector3(
playerPosition.current[0] - position.current[0],
0,
playerPosition.current[2] - position.current[2]
).normalize().multiplyScalar(3);
api.velocity.set(direction.x, 0, direction.z);
} else {
// Random movement
if (Math.random() < 0.02) {
moveDirection.current = new Vector3(
Math.random() * 2 - 1,
0,
Math.random() * 2 - 1
).normalize();
}
api.velocity.set(moveDirection.current.x * 2, 0, moveDirection.current.z * 2);
// Avoid going out of bounds
if (
position.current[0] > 40 ||
position.current[0] < -40 ||
position.current[2] > 40 ||
position.current[2] < -40
) {
moveDirection.current.negate();
}
}
});
// Track player position
useEffect(() => {
const trackPlayer = () => {
if (window.player && window.player.position) {
playerPosition.current = window.player.position;
}
};
const interval = setInterval(trackPlayer, 100);
return () => clearInterval(interval);
}, []);
// Expose position to window for player to access
useEffect(() => {
if (!window.fugitives) window.fugitives = {};
window.fugitives[id] = { position: position.current, isFollowing };
return () => {
if (window.fugitives && window.fugitives[id]) {
delete window.fugitives[id];
}
};
}, [id, isFollowing]);
// Expose player position to window
useEffect(() => {
if (!window.player) window.player = { position: [0, 0, 0] };
const updatePlayerPosition = () => {
if (document.querySelector('canvas')) {
window.player.position = position.current;
}
};
const interval = setInterval(updatePlayerPosition, 100);
return () => clearInterval(interval);
}, []);
return isVisible ? (
) : null;
}