Initial commit
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
130
.gitignore
vendored
Normal file
130
.gitignore
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
1061
package-lock.json
generated
Executable file
1061
package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
16
package.json
Executable file
16
package.json
Executable file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "cawings",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
}
|
||||
1166
public/index.html
Executable file
1166
public/index.html
Executable file
File diff suppressed because it is too large
Load Diff
837
server.js
Executable file
837
server.js
Executable file
@@ -0,0 +1,837 @@
|
||||
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)
|
||||
});
|
||||
Reference in New Issue
Block a user