Files
vectorvoid/server.js
2026-02-05 22:50:20 +02:00

837 lines
32 KiB
JavaScript
Executable File

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: "*", // Allow connection from any IP or Domain
methods: ["GET", "POST"]
}
});
//const io = socketIo(server);
app.use(express.static('public'));
const gameRooms = {};
const WIDTH = 1600;
const HEIGHT = 1200;
const gravity = 0.02;
const maxSpeed = 3; // Reduced max speed
// --- MAP GENERATOR FACTORY ---
// Returns a unique set of terrain data for a specific room
function generateCaveLevel() {
const roofPoints = [];
const floorPoints = [];
const leftWallPoints = [];
const rightWallPoints = [];
const islands = [];
const step = 40;
const roofInitialHeight = HEIGHT / 8;
const floorInitialHeight = HEIGHT * 7 / 8;
const verticalDrift = 30;
const horizontalDrift = 15;
const islandCount = 15;
const minGap = 500;
let y_roof = roofInitialHeight;
let y_floor = floorInitialHeight;
// Horizontal terrain generation
for (let x = 0; x <= WIDTH + step; x += step) {
y_roof += (Math.random() - 0.5) * verticalDrift;
y_floor += (Math.random() - 0.5) * verticalDrift;
y_roof = Math.max(20, Math.min(y_roof, HEIGHT - minGap));
y_floor = Math.min(HEIGHT - 20, Math.max(y_floor, y_roof + minGap));
roofPoints.push({ x, y: y_roof });
floorPoints.push({ x, y: y_floor });
}
// Vertical wall generation
let x_left = 50;
let x_right = WIDTH - 50;
for (let y = 0; y <= HEIGHT + step; y += step) {
x_left += (Math.random() - 0.5) * horizontalDrift;
x_right += (Math.random() - 0.5) * horizontalDrift;
x_left = Math.max(20, Math.min(x_left, 80));
x_right = Math.min(WIDTH - 20, Math.max(x_right, WIDTH - 80));
leftWallPoints.push({ x: x_left, y });
rightWallPoints.push({ x: x_right, y });
}
// Island generation
for (let i = 0; i < islandCount; i++) {
const islandCenterX = Math.random() * (WIDTH - 600) + 300;
// Helper functions are defined below, but hoisted, so this works
const roofAtX = getTerrainHeight(islandCenterX, roofPoints);
const floorAtX = getTerrainHeight(islandCenterX, floorPoints);
const islandCenterY = Math.random() * (floorAtX - roofAtX - 400) + (roofAtX + 200);
const islandPoints = [];
const islandSegments = Math.floor(Math.random() * 12) + 10;
const minRadius = 50, maxRadius = 120;
for (let j = 0; j < islandSegments; j++) {
const angle = (j / islandSegments) * Math.PI * 2;
const radius = Math.random() * (maxRadius - minRadius) + minRadius;
const x = islandCenterX + Math.cos(angle) * radius;
const y = islandCenterY + Math.sin(angle) * radius;
islandPoints.push({ x, y });
}
islands.push(islandPoints);
}
// Create Landing Zone
const landingZoneWidth = 100;
const lzStartIndex = Math.floor(Math.random() * (floorPoints.length - (landingZoneWidth / step) - 10)) + 5; // avoid edges
const lzEndIndex = lzStartIndex + Math.round(landingZoneWidth / step);
let landingZone = null;
if (lzEndIndex < floorPoints.length) {
const avgY = floorPoints.slice(lzStartIndex, lzEndIndex).reduce((sum, p) => sum + p.y, 0) / (lzEndIndex - lzStartIndex);
for(let i = lzStartIndex; i < lzEndIndex; i++) {
floorPoints[i].y = avgY; // Flatten the floor segment
}
landingZone = {
x: floorPoints[lzStartIndex].x,
y: avgY,
width: floorPoints[lzEndIndex].x - floorPoints[lzStartIndex].x,
height: 40 // For visualization on client
};
}
return { roofPoints, floorPoints, leftWallPoints, rightWallPoints, islands, landingZone };
}
// --- HELPER FUNCTIONS ---
const PLAYER_EMOJIS = ['🚀', '👾', '🤖', '👽', '⭐', '💫', '💥', '✨', '☄️', '🪐'];
function makeId(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function getTerrainHeight(x, points) {
if (!points || points.length === 0) return 0;
if (x <= points[0].x) return points[0].y;
if (x >= points[points.length - 1].x) return points[points.length - 1].y;
let p1, p2;
for (let i = 0; i < points.length - 1; i++) {
if (x >= points[i].x && x < points[i+1].x) {
p1 = points[i];
p2 = points[i+1];
break;
}
}
if (!p1) return points[0].y;
const t = (x - p1.x) / (p2.x - p1.x);
return p1.y + t * (p2.y - p1.y);
}
function getTerrainWidth(y, points) {
if (!points || points.length === 0) return 0;
if (y <= points[0].y) return points[0].x;
if (y >= points[points.length - 1].y) return points[points.length - 1].x;
let p1, p2;
for (let i = 0; i < points.length - 1; i++) {
if (y >= points[i].y && y < points[i+1].y) {
p1 = points[i];
p2 = points[i+1];
break;
}
}
if (!p1) return points[0].x;
const t = (y - p1.y) / (p2.y - p1.y);
return p1.x + t * (p2.x - p1.x);
}
function isPointInPolygon(point, polygon) {
let isInside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x, yi = polygon[i].y;
const xj = polygon[j].x, yj = polygon[j].y;
const intersect = ((yi > point.y) !== (yj > point.y))
&& (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi);
if (intersect) isInside = !isInside;
}
return isInside;
}
function getSafeSpawnPoint(mapData) {
const { roofPoints, floorPoints, leftWallPoints, rightWallPoints, islands } = mapData;
const shipRadius = 20; // Slightly larger than actual radius for safety
let newX, newY, isSafe;
let attempts = 0;
do {
attempts++;
newX = Math.random() * (WIDTH - 200) + 100;
newY = Math.random() * (HEIGHT / 2) + (HEIGHT / 4);
isSafe = true;
if (newY - shipRadius < getTerrainHeight(newX, roofPoints) ||
newY + shipRadius > getTerrainHeight(newX, floorPoints) ||
newX - shipRadius < getTerrainWidth(newY, leftWallPoints) ||
newX + shipRadius > getTerrainWidth(newY, rightWallPoints)) {
isSafe = false;
}
if(isSafe) {
for (const island of islands) {
if (isPointInPolygon({x: newX, y: newY}, island)) {
isSafe = false;
break;
}
}
}
// Fallback if stuck
if (attempts > 50) {
return { x: WIDTH/2, y: HEIGHT/2 };
}
} while (!isSafe);
return { x: newX, y: newY };
}
// --- ROOM LIST HELPER ---
function getRoomList() {
const rooms = [];
for (const code in gameRooms) {
rooms.push({
code: code,
count: Object.keys(gameRooms[code].players).length
});
}
return rooms;
}
function broadcastRoomList() {
io.emit('roomListUpdate', getRoomList());
}
// --- SOCKET IO LOGIC ---
io.on('connection', (socket) => {
console.log('A user connected:', socket.id);
socket.emit('roomListUpdate', getRoomList()); // Send list immediately on connect
socket.on('createGame', (data) => {
let roomCode = makeId(4);
while (gameRooms[roomCode]) {
roomCode = makeId(4);
}
// Generate Unique Map for this room
const levelData = generateCaveLevel();
gameRooms[roomCode] = {
players: {},
pellets: [],
mines: [],
pickups: [], // Init pickups
missiles: [], // Init missiles
map: levelData, // Store map in the room object
wallCollisionsEnabled: data.enableWallCollisions, // Store setting
winScore: data.winScore || 10 // Store win score
};
socket.join(roomCode);
socket.roomCode = roomCode;
const spawn = getSafeSpawnPoint(levelData);
const player = {
x: spawn.x,
y: spawn.y,
rotation: 0, velocityX: 0, velocityY: 0, id: socket.id,
score: 0, isDead: false, respawnTimer: 0,
name: data.playerName,
isLanded: false,
secondaryType: 'mine', // Default weapon
secondaryAmmo: 3, // Default ammo
emoji: PLAYER_EMOJIS[Math.floor(Math.random() * PLAYER_EMOJIS.length)] // Assign random emoji
};
gameRooms[roomCode].players[socket.id] = player;
socket.emit('gameStarted', roomCode);
// Send THIS room's map data
socket.emit('currentPlayers', {
players: gameRooms[roomCode].players,
roof: levelData.roofPoints,
floor: levelData.floorPoints,
islands: levelData.islands,
landingZone: levelData.landingZone,
leftWall: levelData.leftWallPoints,
rightWall: levelData.rightWallPoints,
mapWidth: WIDTH, mapHeight: HEIGHT,
winScore: gameRooms[roomCode].winScore
});
broadcastRoomList(); // Update everyone
});
socket.on('joinRoom', (data) => {
const roomCode = data.roomCode;
if (!gameRooms[roomCode]) {
socket.emit('unknownGame');
return;
}
socket.join(roomCode);
socket.roomCode = roomCode;
const room = gameRooms[roomCode]; // Reference the specific room
const spawn = getSafeSpawnPoint(room.map);
const player = {
x: spawn.x,
y: spawn.y,
rotation: 0, velocityX: 0, velocityY: 0, id: socket.id,
score: 0, isDead: false, respawnTimer: 0,
name: data.playerName,
isLanded: false,
secondaryType: 'mine', // Default weapon
secondaryAmmo: 3, // Default ammo
emoji: PLAYER_EMOJIS[Math.floor(Math.random() * PLAYER_EMOJIS.length)] // Assign random emoji
};
room.players[socket.id] = player;
socket.emit('gameStarted', roomCode);
// Send THIS room's map data
socket.emit('currentPlayers', {
players: room.players,
roof: room.map.roofPoints,
floor: room.map.floorPoints,
islands: room.map.islands,
landingZone: room.map.landingZone,
leftWall: room.map.leftWallPoints,
rightWall: room.map.rightWallPoints,
mapWidth: WIDTH, mapHeight: HEIGHT,
winScore: room.winScore
});
socket.to(roomCode).emit('newPlayer', player);
broadcastRoomList(); // Update everyone
});
socket.on('playerMovement', (movementData) => {
const roomCode = socket.roomCode;
if (roomCode && gameRooms[roomCode] && gameRooms[roomCode].players[socket.id]) {
const player = gameRooms[roomCode].players[socket.id];
if(player.isDead) return;
if (player.isLanded) {
// LANDED STATE:
// Only listen for UP to take off
if (movementData.up) {
player.isLanded = false;
player.y -= 2; // CRITICAL: Nudge up so we don't immediately re-land
player.velocityY = -2; // Give initial pop
}
// Ignore left/right while landed to prevent spinning on the ground
return;
}
// AIRBORNE STATE:
if (movementData.isTouch) {
const touchRotationSpeed = 0.04; // Slower rotation for touch
if (movementData.left) player.rotation -= touchRotationSpeed;
if (movementData.right) player.rotation += touchRotationSpeed;
} else {
// Keyboard or other input
if (movementData.left) player.rotation -= 0.1;
if (movementData.right) player.rotation += 0.1;
}
if (movementData.up) {
const thrust = 0.1; // Reduced thrust
player.velocityX += Math.cos(player.rotation) * thrust;
player.velocityY += Math.sin(player.rotation) * thrust;
}
}
});
socket.on('shoot', () => {
const roomCode = socket.roomCode;
if (roomCode && gameRooms[roomCode] && gameRooms[roomCode].players[socket.id]) {
const player = gameRooms[roomCode].players[socket.id];
if(player.isDead) return;
const pelletSpeed = 7;
gameRooms[roomCode].pellets.push({
x: player.x + Math.cos(player.rotation) * 18,
y: player.y + Math.sin(player.rotation) * 18,
velocityX: Math.cos(player.rotation) * pelletSpeed + player.velocityX,
velocityY: Math.sin(player.rotation) * pelletSpeed + player.velocityY,
ownerId: player.id,
});
}
});
socket.on('fireSecondary', () => {
const roomCode = socket.roomCode;
if (roomCode && gameRooms[roomCode] && gameRooms[roomCode].players[socket.id]) {
const player = gameRooms[roomCode].players[socket.id];
if(player.isDead || player.secondaryAmmo <= 0) return;
if (player.secondaryType === 'mine') {
player.secondaryAmmo--;
gameRooms[roomCode].mines.push({
x: player.x,
y: player.y,
ownerId: player.id,
age: 0
});
} else if (player.secondaryType === 'missile') {
player.secondaryAmmo--;
const speed = 10;
gameRooms[roomCode].missiles.push({
x: player.x + Math.cos(player.rotation) * 20,
y: player.y + Math.sin(player.rotation) * 20,
velocityX: Math.cos(player.rotation) * speed + player.velocityX,
velocityY: Math.sin(player.rotation) * speed + player.velocityY,
rotation: player.rotation,
ownerId: player.id,
age: 0
});
}
}
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
const roomCode = socket.roomCode;
if (roomCode && gameRooms[roomCode]) {
delete gameRooms[roomCode].players[socket.id];
io.to(roomCode).emit('playerDisconnected', socket.id);
if (Object.keys(gameRooms[roomCode].players).length === 0) {
console.log(`Room ${roomCode} is empty, deleting.`);
delete gameRooms[roomCode];
}
broadcastRoomList(); // Update everyone
}
});
});
// --- GAME LOOP ---
function updateGameState() {
for (const roomCode in gameRooms) {
const room = gameRooms[roomCode];
// CRITICAL: Destructure this room's specific map data
// All physics checks below now use these local variables instead of globals
const { roofPoints, floorPoints, leftWallPoints, rightWallPoints, islands } = room.map;
// --- UPDATE PICKUPS ---
if (!room.pickups) room.pickups = [];
// Spawn chance: 0.5% per frame if less than 3 pickups
if (room.pickups.length < 3 && Math.random() < 0.005) {
let px, py, valid = false;
let attempts = 0;
while (!valid && attempts < 10) {
attempts++;
px = Math.random() * (WIDTH - 200) + 100;
py = Math.random() * (HEIGHT - 200) + 100;
const rY = getTerrainHeight(px, roofPoints);
const fY = getTerrainHeight(px, floorPoints);
const lX = getTerrainWidth(py, leftWallPoints);
const rX = getTerrainWidth(py, rightWallPoints);
// Check boundaries
if (py > rY + 30 && py < fY - 30 && px > lX + 30 && px < rX - 30) {
valid = true;
// Check islands
for (const island of islands) {
if (isPointInPolygon({x: px, y: py}, island)) {
valid = false;
break;
}
}
}
}
if (valid) {
room.pickups.push({
x: px,
y: py,
type: 'missile',
id: Math.random().toString(36).substring(7)
});
}
}
for (let i = room.pickups.length - 1; i >= 0; i--) {
const pickup = room.pickups[i];
// Check collision with players
for (const pid in room.players) {
const p = room.players[pid];
if (p.isDead) continue;
const dist = Math.sqrt((p.x - pickup.x)**2 + (p.y - pickup.y)**2);
if (dist < 30) { // Pickup radius
// Award Weapon
if (pickup.type === 'missile') {
p.secondaryType = 'missile';
p.secondaryAmmo = 2; // Give 2 missiles
}
io.to(roomCode).emit('pickupCollected', { x: pickup.x, y: pickup.y, playerId: p.id, type: pickup.type });
room.pickups.splice(i, 1);
break; // Pickup consumed
}
}
}
// --- UPDATE MISSILES ---
if (!room.missiles) room.missiles = [];
for (let i = room.missiles.length - 1; i >= 0; i--) {
const missile = room.missiles[i];
missile.x += missile.velocityX;
missile.y += missile.velocityY;
missile.age++;
if (missile.age > 120) { // 2 seconds life
room.missiles.splice(i, 1);
continue;
}
// Check Terrain Collision
const roofY = getTerrainHeight(missile.x, roofPoints);
const floorY = getTerrainHeight(missile.x, floorPoints);
const leftX = getTerrainWidth(missile.y, leftWallPoints);
const rightX = getTerrainWidth(missile.y, rightWallPoints);
let hit = false;
if (missile.y < roofY || missile.y > floorY || missile.x < leftX || missile.x > rightX) hit = true;
else {
for (const island of islands) {
if (isPointInPolygon(missile, island)) { hit = true; break; }
}
}
if (!hit) {
// Check Player Collision
for (const pid in room.players) {
const p = room.players[pid];
if (p.isDead) continue;
if (missile.ownerId === p.id && missile.age < 10) continue; // Safety
const dist = Math.sqrt((p.x - missile.x)**2 + (p.y - missile.y)**2);
if (dist < 20) {
hit = true;
io.to(roomCode).emit('playerDestroyed', { x: p.x, y: p.y, playerId: p.id });
p.isDead = true;
p.isLanded = false;
p.respawnTimer = 180;
if (missile.ownerId !== p.id) {
const killer = room.players[missile.ownerId];
if (killer) killer.score++;
}
break;
}
}
}
if (hit) {
io.to(roomCode).emit('pelletImpact', { x: missile.x, y: missile.y });
room.missiles.splice(i, 1);
}
}
// 0. Update Mines
if (!room.mines) room.mines = []; // Safety init if missing
for (let i = room.mines.length - 1; i >= 0; i--) {
const mine = room.mines[i];
mine.age++;
// Self-destroy after 10 seconds (600 frames)
if (mine.age > 600) {
io.to(roomCode).emit('pelletImpact', { x: mine.x, y: mine.y }); // Show explosion
room.mines.splice(i, 1);
continue;
}
// Check collision with all players
let hit = false;
for (const pid in room.players) {
const p = room.players[pid];
if (p.isDead) continue;
// Safety: Don't kill owner immediately
if (mine.ownerId === p.id && mine.age < 60) continue;
const dx = p.x - mine.x;
const dy = p.y - mine.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 20) { // Mine radius
hit = true;
io.to(roomCode).emit('playerDestroyed', { x: p.x, y: p.y, playerId: p.id });
p.isDead = true;
p.isLanded = false;
p.respawnTimer = 180;
// Award point to mine owner (even if they killed themselves?)
// Let's say suicide doesn't give points, but kills you.
if (mine.ownerId !== p.id) {
const killer = room.players[mine.ownerId];
if (killer) killer.score++;
}
}
}
if (hit) {
io.to(roomCode).emit('pelletImpact', { x: mine.x, y: mine.y }); // Use impact effect for mine too
room.mines.splice(i, 1);
}
}
// 1. Update pellets
for (let i = room.pellets.length - 1; i >= 0; i--) {
const pellet = room.pellets[i];
pellet.x += pellet.velocityX;
pellet.y += pellet.velocityY;
// Check collisions using local map data
const roofY = getTerrainHeight(pellet.x, roofPoints);
const floorY = getTerrainHeight(pellet.x, floorPoints);
const leftX = getTerrainWidth(pellet.y, leftWallPoints);
const rightX = getTerrainWidth(pellet.y, rightWallPoints);
let collided = false;
if (pellet.y < roofY || pellet.y > floorY || pellet.x < leftX || pellet.x > rightX) {
collided = true;
} else {
for (const island of islands) {
if (isPointInPolygon(pellet, island)) {
collided = true;
break;
}
}
}
if (collided) {
io.to(roomCode).emit('pelletImpact', { x: pellet.x, y: pellet.y });
room.pellets.splice(i, 1);
}
}
// 2. Update players
for (const playerId in room.players) {
const player = room.players[playerId];
const shipRadius = 15;
// --- DEAD STATE ---
if (player.isDead) {
player.respawnTimer--;
if (player.respawnTimer <= 0) {
const spawn = getSafeSpawnPoint(room.map);
player.isDead = false;
player.isLanded = false; // Ensure they respawn airborne
player.x = spawn.x;
player.y = spawn.y;
player.velocityX = 0;
player.velocityY = 0;
}
continue;
}
// --- LANDED STATE ---
if (player.isLanded) {
// Force physics to stop.
// This prevents "jitter" where gravity pulls it down and collision snaps it up.
player.velocityX *= 0.9; // Friction to stop sliding
player.velocityY = 0;
player.rotation = -Math.PI / 2; // Force upright visual (pointing Up)
// Check if they are still over the landing zone (in case they slid off)
const lz = room.map.landingZone;
if (lz) {
// Hard snap to surface to prevent sinking
player.y = (lz.y - lz.height) - shipRadius;
// If they slid off the edge, wake them up
if (player.x < lz.x || player.x > lz.x + lz.width) {
player.isLanded = false;
}
}
// Skip the rest of physics, but still check for bullet collisions later
}
// --- AIRBORNE STATE ---
else {
// Apply inputs and physics
player.velocityY += gravity;
player.x += player.velocityX;
player.y += player.velocityY;
const speed = Math.sqrt(player.velocityX ** 2 + player.velocityY ** 2);
if (speed > maxSpeed) {
player.velocityX = (player.velocityX / speed) * maxSpeed;
player.velocityY = (player.velocityY / speed) * maxSpeed;
}
}
// 3. Check for collisions and deaths (Using local map data)
const roofY = getTerrainHeight(player.x, roofPoints);
const floorY = getTerrainHeight(player.x, floorPoints);
const leftX = getTerrainWidth(player.y, leftWallPoints);
const rightX = getTerrainWidth(player.y, rightWallPoints);
let hasDied = false;
// Collision Flags
let hitFloor = false, hitRoof = false, hitLeft = false, hitRight = false, hitIsland = false;
// Only check terrain collision if NOT landed (or if we slid off)
if (!player.isLanded) {
if (player.y + shipRadius > floorY) hitFloor = true;
else if (player.y - shipRadius < roofY) hitRoof = true;
else if (player.x - shipRadius < leftX) hitLeft = true;
else if (player.x + shipRadius > rightX) hitRight = true;
else {
for (const island of islands) {
if (isPointInPolygon(player, island)) { hitIsland = true; break; }
}
}
if (hitFloor || hitRoof || hitLeft || hitRight || hitIsland) {
// LANDING LOGIC (Always check first)
let landed = false;
if (hitFloor) {
const lz = room.map.landingZone;
const speed = Math.sqrt(player.velocityX ** 2 + player.velocityY ** 2);
const isOverLZ = lz && player.x > lz.x && player.x < lz.x + lz.width;
const isUpright = Math.sin(player.rotation) < -0.9;
if (isOverLZ && speed < 2.0 && isUpright) {
landed = true;
player.isLanded = true;
player.secondaryType = 'mine'; // Reset to default
player.secondaryAmmo = 3; // Restock
player.velocityX = 0;
player.velocityY = 0;
player.rotation = 0;
player.y = (lz.y - lz.height) - shipRadius; // Snap perfectly to top
}
}
if (!landed) {
if (room.wallCollisionsEnabled) {
hasDied = true;
} else {
// SAFE MODE: Resolve Collision (Bounce)
const bounceDamp = 0.5;
if (hitFloor) {
player.y = floorY - shipRadius;
player.velocityY = -Math.abs(player.velocityY) * bounceDamp;
} else if (hitRoof) {
player.y = roofY + shipRadius;
player.velocityY = Math.abs(player.velocityY) * bounceDamp;
} else if (hitLeft) {
player.x = leftX + shipRadius;
player.velocityX = Math.abs(player.velocityX) * bounceDamp;
} else if (hitRight) {
player.x = rightX - shipRadius;
player.velocityX = -Math.abs(player.velocityX) * bounceDamp;
} else if (hitIsland) {
// Simple reverse for islands
player.x -= player.velocityX * 1.5;
player.y -= player.velocityY * 1.5;
player.velocityX *= -bounceDamp;
player.velocityY *= -bounceDamp;
}
// Add impact sound/effect even if safe
if (Math.abs(player.velocityX) > 1 || Math.abs(player.velocityY) > 1) {
io.to(roomCode).emit('pelletImpact', { x: player.x, y: player.y });
}
}
}
}
}
if (hasDied) {
io.to(roomCode).emit('playerDestroyed', { x: player.x, y: player.y, playerId: player.id });
player.isDead = true;
player.isLanded = false;
player.respawnTimer = 180;
}
// Pellet-player collision (unchanged)
for (let i = room.pellets.length - 1; i >= 0; i--) {
const pellet = room.pellets[i];
if (pellet.ownerId !== playerId && !player.isDead) {
const distance = Math.sqrt((player.x - pellet.x) ** 2 + (player.y - pellet.y) ** 2);
if (distance < shipRadius) {
io.to(roomCode).emit('playerDestroyed', { x: player.x, y: player.y, playerId: player.id });
player.isDead = true;
player.isLanded = false;
player.respawnTimer = 180;
const killer = room.players[pellet.ownerId];
if (killer) killer.score++;
room.pellets.splice(i, 1);
break;
}
}
}
}
// 4. Check for win condition
let winnerId = null;
for (const playerId in room.players) {
if (room.players[playerId].score >= room.winScore) {
winnerId = playerId;
break;
}
}
if (winnerId) {
io.to(roomCode).emit('gameOver', winnerId);
for (const playerId in room.players) room.players[playerId].score = 0;
}
// 5. Emit state (Pellets and Scores - Map data is static and sent on join)
// We create a lightweight room object to send, we don't need to resend the map every frame
io.to(roomCode).emit('gameState', {
players: room.players,
pellets: room.pellets,
mines: room.mines,
pickups: room.pickups,
missiles: room.missiles
});
}
}
setInterval(updateGameState, 1000 / 60);
const PORT = process.env.PORT || 3001;
// UPDATED: Added '0.0.0.0' as the second argument
server.listen(PORT, '0.0.0.0', () => {
console.log(`Server listening on port ${PORT}`);
console.log(`Access locally via: http://localhost:${PORT}`);
console.log(`Access via LAN via: https://192.168.1.64:${PORT}`); // (Or whatever your IP is)
});