'use client' import { ExampleLayoutProps } from '../example-layout' const instanceId = Math.random().toString(36).substring(2, 9) const appJsCode = `import { useEffect, useState, useRef } from 'react'; import './styles.css'; import { createClient } from '@supabase/supabase-js'; import { Canvas, useFrame } from '@react-three/fiber'; import { Physics, useBox, usePlane } from '@react-three/cannon'; import { OrbitControls } from '@react-three/drei'; import { Vector3, MathUtils } from 'three'; // Initialize Supabase client const supabaseUrl = '${process.env.NEXT_PUBLIC_EXAMPLES_SUPABASE_URL}'; const supabaseKey = '${process.env.NEXT_PUBLIC_EXAMPLES_SUPABASE_ANON_KEY}'; const supabase = createClient(supabaseUrl, supabaseKey); // Channel name - using a unique ID to ensure both instances connect to the same channel const CHANNEL = 'platformer-3d-example-${instanceId}'; // Game constants const MOVE_SPEED = 5; const GROUND_MOVE_SPEED = 10; // Increased movement speed on ground const JUMP_FORCE = 8; // Strong jump force const PLAYER_SIZE = [1, 1, 1]; // Width, height, depth const GRAVITY = -12; // Stronger gravity for more responsive falling const BROADCAST_THROTTLE = 50; // ms between broadcasts (20 updates per second) const INTERPOLATION_SPEED = 15; // Higher values = faster interpolation const PROJECTILE_SPEED = 12; // Reduced speed for better collision detection const PROJECTILE_SIZE = 0.2; // Smaller projectile size const SHOOT_COOLDOWN = 500; // Cooldown between shots in ms const COLLISION_OFFSET = 0.05; // Small offset to prevent clipping const GROUND_CHECK_THRESHOLD = 0.3; // Threshold for ground detection // Character colors - vibrant neon colors const COLORS = [ '#ff00ff', // Neon pink '#00ffff', // Neon cyan '#ffff00', // Neon yellow '#00ff00', // Neon green '#ff9900', // Neon orange '#ff0099' // Neon magenta ]; // Spawn positions - safe places to teleport to const SPAWN_POSITIONS = [ [0, 1, 0], // Above ground [-3, 2, 0], // Above left platform [3, 3, 0], // Above right platform [0, 4, 0] // Above center platform ]; export default function App() { const [username, setUsername] = useState(''); const [isConnected, setIsConnected] = useState(false); const [players, setPlayers] = useState([]); const [localPlayer, setLocalPlayer] = useState(null); const [projectiles, setProjectiles] = useState([]); const [gameOverMessage, setGameOverMessage] = useState(''); const userId = useRef(Math.random().toString(36).substring(2, 15)); const channelRef = useRef(null); const keysPressed = useRef({}); const lastShootTime = useRef(0); const canShoot = useRef(true); const isGameOver = useRef(false); // Use a ref to track the current localPlayer state to avoid closure issues const localPlayerRef = useRef(null); // Keep the ref in sync with the state useEffect(() => { localPlayerRef.current = localPlayer; }, [localPlayer]); // Initialize game useEffect(() => { // Generate a random username const adjectives = ['Happy', 'Clever', 'Brave', 'Bright', 'Kind']; const nouns = ['Panda', 'Tiger', 'Eagle', 'Dolphin', 'Fox']; const randomName = \`\${adjectives[Math.floor(Math.random() * adjectives.length)]}\${ nouns[Math.floor(Math.random() * nouns.length)] }\${Math.floor(Math.random() * 100)}\`; setUsername(randomName); // Assign a random color const playerColor = COLORS[Math.floor(Math.random() * COLORS.length)]; // Create initial player state const initialPlayerState = { id: userId.current, username: randomName, position: [Math.random() * 8 - 4, 5, 0], // Higher Y position (5 instead of 3) color: playerColor, lastUpdated: Date.now(), isAlive: true, direction: [1, 0, 0] // Default direction (facing right) }; setLocalPlayer(initialPlayerState); localPlayerRef.current = initialPlayerState; // Also set the ref directly // Set up Supabase channel const channel = supabase.channel(CHANNEL, { config: { presence: { key: userId.current, }, }, }); channelRef.current = channel; // Handle presence for player list channel.on('presence', { event: 'sync' }, () => { const state = channel.presenceState(); const presenceList = []; // Convert presence state to array Object.keys(state).forEach(key => { const presences = state[key]; presenceList.push(...presences); }); // Update players list, but keep local player's state setPlayers(prevPlayers => { const updatedPlayers = presenceList.map(presence => { // If this is our local player, use our local state if (presence.id === userId.current) { return localPlayerRef.current || presence; } // For other players, find their previous state or use presence data const existingPlayer = prevPlayers.find(p => p.id === presence.id); if (existingPlayer) { // If player already exists, keep their interpolation data return { ...existingPlayer, // Update any new properties from presence ...presence, // But keep position and interpolation data position: existingPlayer.position, currentPosition: existingPlayer.currentPosition, targetPosition: existingPlayer.targetPosition, interpolating: existingPlayer.interpolating }; } else { // For new players, initialize interpolation data return { ...presence, // Add interpolation properties currentPosition: [...presence.position], targetPosition: [...presence.position], interpolating: false, isAlive: presence.isAlive !== false, // Default to alive if not specified direction: presence.direction || [1, 0, 0] // Default direction }; } }); return updatedPlayers; }); }); // Handle player position updates channel.on('broadcast', { event: 'player_update' }, (payload) => { const updatedPlayer = payload.payload; // Don't update our own player (we manage that locally) if (updatedPlayer.id === userId.current) return; // Update the player in our list setPlayers(prevPlayers => { return prevPlayers.map(player => { if (player.id === updatedPlayer.id) { // Only update if the incoming data is newer if (!player.lastUpdated || updatedPlayer.lastUpdated > player.lastUpdated) { // Calculate distance between current and new position const currentPos = player.position || [0, 0, 0]; const newPos = updatedPlayer.position; const distance = Math.sqrt( Math.pow(newPos[0] - currentPos[0], 2) + Math.pow(newPos[1] - currentPos[1], 2) + Math.pow(newPos[2] - currentPos[2], 2) ); // For large position changes (teleports), skip interpolation const skipInterpolation = distance > 10; return { ...player, // Update direction direction: updatedPlayer.direction || player.direction, // Update alive status isAlive: updatedPlayer.isAlive !== undefined ? updatedPlayer.isAlive : player.isAlive, // If skipping interpolation, directly set position ...(skipInterpolation ? { position: [...newPos], currentPosition: [...newPos], targetPosition: [...newPos], interpolating: false } : { // Keep the current position for interpolation currentPosition: player.currentPosition || player.position, // Set the new target position targetPosition: newPos, // Update the canonical position position: player.position, // Mark as interpolating interpolating: true }), // Update timestamp lastUpdated: updatedPlayer.lastUpdated }; } } return player; }); }); }); // Handle projectile creation channel.on('broadcast', { event: 'projectile_fired' }, (payload) => { const projectileData = payload.payload; // Don't process our own projectiles (we already added them) if (projectileData.ownerId === userId.current) return; // Add the projectile to our list setProjectiles(prev => { return [ ...prev, { ...projectileData, position: [...projectileData.position], createdAt: Date.now() } ]; }); }); // Handle player hit events channel.on('broadcast', { event: 'player_hit' }, (payload) => { const hitData = payload.payload; // If we were hit if (hitData.playerId === userId.current) { // Show game over message setGameOverMessage(\`You were eliminated by \${hitData.shooterName}!\`); isGameOver.current = true; // Update our local player state setLocalPlayer(prev => { const updated = { ...prev, isAlive: false }; localPlayerRef.current = updated; // Update ref too return updated; }); // After 3 seconds, respawn setTimeout(() => { respawnPlayer(); }, 3000); } // Update the hit player in our list setPlayers(prevPlayers => { return prevPlayers.map(player => { if (player.id === hitData.playerId) { return { ...player, isAlive: false }; } return player; }); }); }); // Subscribe to channel channel.subscribe(async (status) => { if (status === 'SUBSCRIBED') { // Track presence await channel.track(initialPlayerState); setIsConnected(true); } }); // Set up keyboard event listeners window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); // Clean up return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); if (channel) { channel.unsubscribe(); } }; }, []); // Broadcast player position when it changes useEffect(() => { if (!localPlayer || !channelRef.current || !isConnected) return; // Broadcast our updated position channelRef.current.send({ type: 'broadcast', event: 'player_update', payload: localPlayer }); // Update our presence data channelRef.current.track(localPlayer); }, [localPlayer, isConnected]); // Periodic position broadcast to ensure consistency useEffect(() => { if (!localPlayer || !channelRef.current || !isConnected) return; // Broadcast position every 2 seconds regardless of movement const interval = setInterval(() => { // Update timestamp for the broadcast const updatedPlayer = { ...localPlayer, lastUpdated: Date.now() }; // Broadcast position channelRef.current.send({ type: 'broadcast', event: 'player_update', payload: updatedPlayer }); // Update presence data channelRef.current.track(updatedPlayer); }, 2000); return () => clearInterval(interval); }, [localPlayer, isConnected]); // Handle key down events const handleKeyDown = (e) => { // Prevent default behavior for arrow keys and WASD to disable scrolling if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyS', 'KeyA', 'KeyD'].includes(e.code)) { e.preventDefault(); } // Simply update the keysPressed object keysPressed.current[e.code] = true; // Shoot when Enter is pressed if (e.code === 'Enter' || e.code === 'NumpadEnter') { if (canShoot.current && !isGameOver.current && localPlayerRef.current?.isAlive !== false) { shootProjectile(); } } }; // Handle key up events const handleKeyUp = (e) => { keysPressed.current[e.code] = false; }; // Function to shoot a projectile const shootProjectile = () => { // Use the ref instead of the state const player = localPlayerRef.current; if (!player) { return; } const now = Date.now(); // Check cooldown if (now - lastShootTime.current < SHOOT_COOLDOWN) { return; } lastShootTime.current = now; canShoot.current = false; // Reset cooldown after delay setTimeout(() => { canShoot.current = true; }, SHOOT_COOLDOWN); // Create projectile data with better positioning const projectileData = { id: \`\${userId.current}-\${now}\`, ownerId: userId.current, ownerName: player.username, position: [ player.position[0] + player.direction[0] * 1.0, // Increased offset from player player.position[1] + 0.5, // Slightly above center player.position[2] + player.direction[2] * 1.0 ], direction: [...player.direction], color: player.color, createdAt: now }; try { // Add to local projectiles setProjectiles(prev => { const newProjectiles = [...prev, projectileData]; return newProjectiles; }); // Broadcast projectile creation channelRef.current.send({ type: 'broadcast', event: 'projectile_fired', payload: projectileData }); } catch (error) { console.error('Error creating projectile:', error); } }; // Function to handle player hit const handlePlayerHit = (playerId, shooterId, shooterName) => { // Don't process hits for dead players const hitPlayer = players.find(p => p.id === playerId); if (!hitPlayer || hitPlayer.isAlive === false) return; // Broadcast hit channelRef.current.send({ type: 'broadcast', event: 'player_hit', payload: { playerId, shooterId, shooterName } }); // If we hit someone else if (playerId !== userId.current && shooterId === userId.current) { // Show elimination message setGameOverMessage(\`You eliminated \${hitPlayer.username}!\`); // Clear message after 3 seconds setTimeout(() => { setGameOverMessage(''); }, 3000); } }; // Function to respawn player const respawnPlayer = () => { if (!localPlayerRef.current) return; // Get a random spawn position const spawnPos = SPAWN_POSITIONS[Math.floor(Math.random() * SPAWN_POSITIONS.length)]; // Update local player position and state setLocalPlayer(prev => { if (!prev) return prev; const updated = { ...prev, position: [...spawnPos], isAlive: true, lastUpdated: Date.now() }; // Update ref too localPlayerRef.current = updated; return updated; }); // Clear game over message setGameOverMessage(''); isGameOver.current = false; }; return (