diff --git a/bun.lock b/bun.lock index afa8d81..69db34c 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ }, "devDependencies": { "bun-types": "latest", + "typescript": "^5.8.2", }, }, }, @@ -20,6 +21,8 @@ "three": ["three@0.161.0", "", {}, "sha512-LC28VFtjbOyEu5b93K0bNRLw1rQlMJ85lilKsYj6dgTu+7i17W+JCCEbvrpmNHF1F3NAUqDSWq50UD7w9H2xQw=="], + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], } } diff --git a/package.json b/package.json index 502624b..e6a4c4e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "three": "^0.161.0" }, "devDependencies": { - "bun-types": "latest" + "bun-types": "latest", + "typescript": "^5.8.2" }, "type": "module" } diff --git a/public/models/cowboy.glb b/public/models/cowboy.glb new file mode 100644 index 0000000..723ce53 Binary files /dev/null and b/public/models/cowboy.glb differ diff --git a/public/models/houses.glb b/public/models/houses.glb new file mode 100644 index 0000000..a2e9408 Binary files /dev/null and b/public/models/houses.glb differ diff --git a/public/models/human.fbx b/public/models/human.fbx new file mode 100644 index 0000000..2d4303f Binary files /dev/null and b/public/models/human.fbx differ diff --git a/public/models/samurai.glb b/public/models/samurai.glb new file mode 100644 index 0000000..d54d9b4 Binary files /dev/null and b/public/models/samurai.glb differ diff --git a/src/client/game.js b/src/client/game.js index 2dae2bf..90c6d8a 100644 --- a/src/client/game.js +++ b/src/client/game.js @@ -4,8 +4,8 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Game state and networking let socket; let gameState = { - players: {}, - localPlayerId: null + players: {}, + localPlayerId: null, }; // Three.js setup @@ -19,225 +19,260 @@ let frameCount = 0; // Initialize the game function init() { - initThreeJS(); - initWebSocket(); - animate(); + initThreeJS(); + initWebSocket(); + animate(); } // Set up Three.js scene function initThreeJS() { - // Create scene - scene = new THREE.Scene(); - scene.background = new THREE.Color(0x000000); - - // Create camera - camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); - camera.position.set(0, 10, 20); - - // Create renderer - renderer = new THREE.WebGLRenderer({ antialias: true }); - renderer.setSize(window.innerWidth, window.innerHeight); - renderer.setPixelRatio(window.devicePixelRatio); - document.body.appendChild(renderer.domElement); - - // Add orbit controls - controls = new OrbitControls(camera, renderer.domElement); - controls.enableDamping = true; - controls.dampingFactor = 0.05; - - // Add ambient light - const ambientLight = new THREE.AmbientLight(0x404040); - scene.add(ambientLight); - - // Add directional light - const directionalLight = new THREE.DirectionalLight(0xffffff, 1); - directionalLight.position.set(1, 1, 1); - scene.add(directionalLight); - - // Add a grid helper - const gridHelper = new THREE.GridHelper(50, 50); - scene.add(gridHelper); - - // Handle window resize - window.addEventListener('resize', onWindowResize); + // Create scene + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x0a1033); // Dark blue night sky color + + // Add stars to the night sky + const starsGeometry = new THREE.BufferGeometry(); + const starsCount = 1000; + const starsPositions = new Float32Array(starsCount * 3); + + for (let i = 0; i < starsCount * 3; i += 3) { + starsPositions[i] = (Math.random() - 0.5) * 1000; + starsPositions[i + 1] = (Math.random() - 0.5) * 1000; + starsPositions[i + 2] = (Math.random() - 0.5) * 1000; + } + + starsGeometry.setAttribute('position', new THREE.BufferAttribute(starsPositions, 3)); + const starsMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 0.5 }); + const starField = new THREE.Points(starsGeometry, starsMaterial); + scene.add(starField); + + // Create camera + camera = new THREE.PerspectiveCamera( + 75, + window.innerWidth / window.innerHeight, + 0.1, + 1000 + ); + camera.position.set(0, 10, 20); + + // Create renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + document.body.appendChild(renderer.domElement); + + // Add orbit controls + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.05; + + // Add ambient light + const ambientLight = new THREE.AmbientLight(0x404040); + scene.add(ambientLight); + + // Add directional light + const directionalLight = new THREE.DirectionalLight(0xffffff, 1); + directionalLight.position.set(1, 1, 1); + scene.add(directionalLight); + + // Add a grid helper + const gridHelper = new THREE.GridHelper(50, 50); + scene.add(gridHelper); + + // Handle window resize + window.addEventListener('resize', onWindowResize); } // Connect to WebSocket server function initWebSocket() { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws`; - - socket = new WebSocket(wsUrl); - - socket.onopen = () => { - console.log('Connected to server'); - document.getElementById('connection-status').textContent = 'Connected'; - document.getElementById('connection-status').classList.remove('disconnected'); - document.getElementById('connection-status').classList.add('connected'); - }; - - socket.onclose = () => { - console.log('Disconnected from server'); - document.getElementById('connection-status').textContent = 'Disconnected'; - document.getElementById('connection-status').classList.remove('connected'); - document.getElementById('connection-status').classList.add('disconnected'); - - // Try to reconnect after a delay - setTimeout(initWebSocket, 3000); - }; - - socket.onerror = (error) => { - console.error('WebSocket error:', error); - }; - - socket.onmessage = (event) => { - const message = JSON.parse(event.data); - - switch (message.type) { - case 'init': - handleInitMessage(message); - break; - case 'gameState': - handleGameStateMessage(message); - break; - case 'playerJoined': - handlePlayerJoinedMessage(message); - break; - case 'playerLeft': - handlePlayerLeftMessage(message); - break; - default: - console.log('Unknown message type:', message.type); - } - }; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws`; + + socket = new WebSocket(wsUrl); + + socket.onopen = () => { + console.log('Connected to server'); + document.getElementById('connection-status').textContent = 'Connected'; + document + .getElementById('connection-status') + .classList.remove('disconnected'); + document.getElementById('connection-status').classList.add('connected'); + }; + + socket.onclose = () => { + console.log('Disconnected from server'); + document.getElementById('connection-status').textContent = 'Disconnected'; + document.getElementById('connection-status').classList.remove('connected'); + document.getElementById('connection-status').classList.add('disconnected'); + + // Try to reconnect after a delay + setTimeout(initWebSocket, 3000); + }; + + socket.onerror = (error) => { + console.error('WebSocket error:', error); + }; + + socket.onmessage = (event) => { + const message = JSON.parse(event.data); + + switch (message.type) { + case 'init': + handleInitMessage(message); + break; + case 'gameState': + handleGameStateMessage(message); + break; + case 'playerJoined': + handlePlayerJoinedMessage(message); + break; + case 'playerLeft': + handlePlayerLeftMessage(message); + break; + default: + console.log('Unknown message type:', message.type); + } + }; } // Handle initial connection message function handleInitMessage(message) { - gameState.localPlayerId = message.playerId; - gameState.players = message.players; - - // Create meshes for all existing players - Object.keys(gameState.players).forEach(playerId => { - createPlayerMesh(playerId); - }); - - // Update player count - document.getElementById('player-count').textContent = Object.keys(gameState.players).length; + gameState.localPlayerId = message.playerId; + gameState.players = message.players; + + // Create meshes for all existing players + Object.keys(gameState.players).forEach((playerId) => { + createPlayerMesh(playerId); + }); + + // Update player count + document.getElementById('player-count').textContent = Object.keys( + gameState.players + ).length; } // Handle game state update message function handleGameStateMessage(message) { - // Update game state with new player positions - Object.keys(message.players).forEach(playerId => { - if (gameState.players[playerId]) { - gameState.players[playerId] = message.players[playerId]; - } - }); + // Update game state with new player positions + Object.keys(message.players).forEach((playerId) => { + if (gameState.players[playerId]) { + gameState.players[playerId] = message.players[playerId]; + } + }); } // Handle new player joined message function handlePlayerJoinedMessage(message) { - const { playerId, player } = message; - gameState.players[playerId] = player; - createPlayerMesh(playerId); - - // Update player count - document.getElementById('player-count').textContent = Object.keys(gameState.players).length; + const { playerId, player } = message; + gameState.players[playerId] = player; + createPlayerMesh(playerId); + + // Update player count + document.getElementById('player-count').textContent = Object.keys( + gameState.players + ).length; } // Handle player left message function handlePlayerLeftMessage(message) { - const { playerId } = message; - - // Remove player from game state - delete gameState.players[playerId]; - - // Remove player mesh from scene - if (playerMeshes[playerId]) { - scene.remove(playerMeshes[playerId]); - delete playerMeshes[playerId]; - } - - // Update player count - document.getElementById('player-count').textContent = Object.keys(gameState.players).length; + const { playerId } = message; + + // Remove player from game state + delete gameState.players[playerId]; + + // Remove player mesh from scene + if (playerMeshes[playerId]) { + scene.remove(playerMeshes[playerId]); + delete playerMeshes[playerId]; + } + + // Update player count + document.getElementById('player-count').textContent = Object.keys( + gameState.players + ).length; } // Create a mesh for a player function createPlayerMesh(playerId) { - const player = gameState.players[playerId]; - const isLocalPlayer = playerId === gameState.localPlayerId; - - // Create a different colored cube based on team - const color = player.team === 'red' ? 0xff0000 : 0x0000ff; - const geometry = new THREE.BoxGeometry(1, 1, 1); - const material = new THREE.MeshLambertMaterial({ color }); - - const mesh = new THREE.Mesh(geometry, material); - mesh.position.set(player.position.x, player.position.y, player.position.z); - - // Add wireframe for local player to distinguish it - if (isLocalPlayer) { - const wireframe = new THREE.LineSegments( - new THREE.EdgesGeometry(geometry), - new THREE.LineBasicMaterial({ color: 0xffffff }) - ); - mesh.add(wireframe); - } - - scene.add(mesh); - playerMeshes[playerId] = mesh; + const player = gameState.players[playerId]; + const isLocalPlayer = playerId === gameState.localPlayerId; + + // Create a different colored cube based on team + const color = player.team === 'red' ? 0xff0000 : 0x0000ff; + const geometry = new THREE.BoxGeometry(1, 1, 1); + const material = new THREE.MeshLambertMaterial({ color }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set(player.position.x, player.position.y, player.position.z); + + // Add wireframe for local player to distinguish it + if (isLocalPlayer) { + const wireframe = new THREE.LineSegments( + new THREE.EdgesGeometry(geometry), + new THREE.LineBasicMaterial({ color: 0xffffff }) + ); + mesh.add(wireframe); + } + + scene.add(mesh); + playerMeshes[playerId] = mesh; } // Send player input to server function sendPlayerInput(input) { - if (socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ - type: 'playerInput', - input - })); - } + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send( + JSON.stringify({ + type: 'playerInput', + input, + }) + ); + } } // Handle window resize function onWindowResize() { - camera.aspect = window.innerWidth / window.innerHeight; - camera.updateProjectionMatrix(); - renderer.setSize(window.innerWidth, window.innerHeight); + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); } // Animation loop function animate() { - requestAnimationFrame(animate); - - // Update controls - controls.update(); - - // Update player meshes based on game state - Object.keys(gameState.players).forEach(playerId => { - const player = gameState.players[playerId]; - const mesh = playerMeshes[playerId]; - - if (mesh) { - mesh.position.set(player.position.x, player.position.y, player.position.z); - mesh.rotation.y = player.rotation || 0; - } - }); - - // Render scene - renderer.render(scene, camera); - - // Update FPS counter - frameCount++; - const now = performance.now(); - const elapsed = now - lastFrameTime; - - if (elapsed >= 1000) { - const fps = Math.round((frameCount * 1000) / elapsed); - fpsCounter.textContent = fps; - frameCount = 0; - lastFrameTime = now; + requestAnimationFrame(animate); + + // Update controls + controls.update(); + + // Update player meshes based on game state + Object.keys(gameState.players).forEach((playerId) => { + const player = gameState.players[playerId]; + const mesh = playerMeshes[playerId]; + + if (mesh) { + mesh.position.set( + player.position.x, + player.position.y, + player.position.z + ); + mesh.rotation.y = player.rotation || 0; } + }); + + // Render scene + renderer.render(scene, camera); + + // Update FPS counter + frameCount++; + const now = performance.now(); + const elapsed = now - lastFrameTime; + + if (elapsed >= 1000) { + const fps = Math.round((frameCount * 1000) / elapsed); + fpsCounter.textContent = fps; + frameCount = 0; + lastFrameTime = now; + } } // Initialize the game when the page loads @@ -246,35 +281,35 @@ window.addEventListener('load', init); // Set up keyboard controls const keys = {}; window.addEventListener('keydown', (e) => { - // Store the lowercase version of the key for consistency - const key = e.key.toLowerCase(); - keys[key] = true; - - // Send input to server - const input = { - forward: keys['w'] || keys['arrowup'] || false, - backward: keys['s'] || keys['arrowdown'] || false, - left: keys['a'] || keys['arrowleft'] || false, - right: keys['d'] || keys['arrowright'] || false, - jump: keys[' '] || false - }; - - sendPlayerInput(input); + // Store the lowercase version of the key for consistency + const key = e.key.toLowerCase(); + keys[key] = true; + + // Send input to server + const input = { + forward: keys['w'] || keys['arrowup'] || false, + backward: keys['s'] || keys['arrowdown'] || false, + left: keys['a'] || keys['arrowleft'] || false, + right: keys['d'] || keys['arrowright'] || false, + jump: keys[' '] || false, + }; + + sendPlayerInput(input); }); window.addEventListener('keyup', (e) => { - // Store the lowercase version of the key for consistency - const key = e.key.toLowerCase(); - keys[key] = false; - - // Send input to server - const input = { - forward: keys['w'] || keys['arrowup'] || false, - backward: keys['s'] || keys['arrowdown'] || false, - left: keys['a'] || keys['arrowleft'] || false, - right: keys['d'] || keys['arrowright'] || false, - jump: keys[' '] || false - }; - - sendPlayerInput(input); + // Store the lowercase version of the key for consistency + const key = e.key.toLowerCase(); + keys[key] = false; + + // Send input to server + const input = { + forward: keys['w'] || keys['arrowup'] || false, + backward: keys['s'] || keys['arrowdown'] || false, + left: keys['a'] || keys['arrowleft'] || false, + right: keys['d'] || keys['arrowright'] || false, + jump: keys[' '] || false, + }; + + sendPlayerInput(input); }); diff --git a/src/server/index.js b/src/server/index.js index 9d76213..6a61e24 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -20,6 +20,9 @@ const clients = new Map(); const TICK_RATE = 60; // Updates per second const PLAYER_SPEED = 0.1; const TEAMS = ['red', 'blue']; +const GRAVITY = 0.01; +const JUMP_FORCE = 0.3; +const GROUND_LEVEL = 0.5; // Get port from environment variable or use default const PORT = process.env.PORT || 3000; @@ -71,6 +74,8 @@ const server = serve({ team, position: { x: 0, y: 0.5, z: 0 }, rotation: 0, + velocity: { x: 0, y: 0, z: 0 }, + isGrounded: true, input: { forward: false, backward: false, @@ -210,9 +215,28 @@ setInterval(() => { dz *= PLAYER_SPEED; } + // Apply jumping if player is on the ground and jump input is active + if (input.jump && player.isGrounded) { + player.velocity.y = JUMP_FORCE; + player.isGrounded = false; + } + + // Apply gravity + if (!player.isGrounded) { + player.velocity.y -= GRAVITY * deltaTime; + } + // Update position player.position.x += dx * deltaTime; player.position.z += dz * deltaTime; + player.position.y += player.velocity.y * deltaTime; + + // Check if player has landed + if (player.position.y <= GROUND_LEVEL) { + player.position.y = GROUND_LEVEL; + player.velocity.y = 0; + player.isGrounded = true; + } // Update rotation if moving if (dx !== 0 || dz !== 0) { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5d19805 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "types": [ + "bun-types" + ] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file