-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
199 lines (172 loc) · 13.6 KB
/
server.js
File metadata and controls
199 lines (172 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
const WebSocket = require('ws');
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;
// Enable CORS
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
// Serve static files
app.use(express.static('.'));
// Create HTTP server
const server = app.listen(port, () => console.log(`Server running on port ${port}`));
// Create WebSocket server
const wss = new WebSocket.Server({ server, perMessageDeflate: false, clientTracking: true });
console.log('WebSocket server created');
// --- CONSTANTS ---
const MAX_WEAPON_RANGE = 80; const WEAPON_COOLDOWN = 125;
const ISLAND_BASE_SIZE = 5;
const RESPAWN_TIME = 5000; const SPAWN_RADIUS = 75; const ISLAND_SPAWN_BUFFER = 15;
const PING_INTERVAL = 20000; const CLIENT_TIMEOUT = 45000;
const LARGE_ISLAND_PROBABILITY = 0.10; // 10% chance for huge islands
const LARGE_ISLAND_SIZE_MULTIPLIER = 20; // Make large islands MUCH bigger
// Game state
const gameState = { players: new Map(), world: { islands: [], oceanSize: 2000, worldBounds: { minX: -1000, maxX: 1000, minZ: -1000, maxZ: 1000 } } };
// --- Helper Functions ---
function isPointInsideIsland(x, z, islands) {
for (const island of islands) {
const dx = island.x - x;
const dz = island.z - z;
const distSq = dx * dx + dz * dz;
const islandRadius = Math.max(island.size * island.scaleX, island.size * island.scaleZ);
const safeRadius = islandRadius + ISLAND_SPAWN_BUFFER;
if (distSq < safeRadius * safeRadius) {
return true; // Point is too close or inside the island buffer zone
}
}
return false;
}
function getRandomSpawnPoint() {
let spawnX, spawnZ;
let attempts = 0;
const maxAttempts = 20; // Prevent infinite loops
do {
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * SPAWN_RADIUS; // Random distance within radius
spawnX = Math.cos(angle) * distance;
spawnZ = Math.sin(angle) * distance;
attempts++;
if (attempts > maxAttempts) {
console.warn("Could not find safe spawn point after max attempts, defaulting to center.");
return { x: 0, y: 0, z: 0 }; // Fallback to center
}
} while (isPointInsideIsland(spawnX, spawnZ, gameState.world.islands)); // Keep trying if inside an island
console.log(`Generated spawn point: (${spawnX.toFixed(1)}, ${spawnZ.toFixed(1)}) after ${attempts} attempts.`);
return { x: spawnX, y: 0, z: spawnZ };
}
// Generate random islands (MODIFIED for large islands & placement)
function generateIslands() {
const islands = [];
const baseSize = ISLAND_BASE_SIZE;
const numIslands = 12; // Reduced slightly to make space for huge islands
const worldSize = gameState.world.oceanSize;
const safeZone = SPAWN_RADIUS + 30; // Increased safe zone around center spawn
console.log(`Generating islands...`);
for (let i = 0; i < numIslands; i++) {
let x, z, size, scaleX, scaleZ, rotation, isLarge;
let islandAttempts = 0;
const maxIslandAttempts = 100; // Allow more attempts for placement
let validPosition = false;
while (!validPosition && islandAttempts < maxIslandAttempts) {
islandAttempts++;
const angle = Math.random() * Math.PI * 2;
// Ensure distance allows for potentially large islands without hitting edge immediately
const maxPossibleRadius = baseSize * LARGE_ISLAND_SIZE_MULTIPLIER * 1.6; // Estimate max radius
const distance = safeZone + Math.random() * (worldSize / 2 - safeZone - maxPossibleRadius);
x = Math.cos(angle) * distance;
z = Math.sin(angle) * distance;
isLarge = Math.random() < LARGE_ISLAND_PROBABILITY;
if (isLarge) {
size = (baseSize * LARGE_ISLAND_SIZE_MULTIPLIER * 0.9) + (Math.random() * baseSize * LARGE_ISLAND_SIZE_MULTIPLIER * 0.2); // Huge size range
console.log(` -> Attempting LARGE island, size: ${size.toFixed(1)}`);
} else {
// Regular islands slightly smaller maybe?
size = baseSize + Math.random() * (baseSize * 5 - baseSize); // Regular size up to 5x base
}
scaleX = 0.7 + Math.random() * 0.6; // More variation?
scaleZ = 0.7 + Math.random() * 0.6;
rotation = Math.random() * Math.PI * 2;
// Collision check radius - ensure it's large enough for huge islands
const checkRadius = size * Math.max(scaleX, scaleZ);
// Increase minimum distance significantly
const minDistance = checkRadius * (isLarge ? 1.8 : 1.5);
let overlapping = false;
for (const existingIsland of islands) {
const dx = existingIsland.x - x;
const dz = existingIsland.z - z;
const dist = Math.sqrt(dx * dx + dz * dz);
const existingCheckRadius = existingIsland.size * Math.max(existingIsland.scaleX, existingIsland.scaleZ);
// Use larger buffer, especially if one is large
const bufferMultiplier = (isLarge || existingIsland.isLarge) ? 1.8 : 1.5;
const combinedMinDist = (checkRadius + existingCheckRadius) * bufferMultiplier;
if (dist < combinedMinDist) {
overlapping = true;
break;
}
}
if (!overlapping) {
islands.push({ x, z, size, scaleX, scaleZ, rotation, isLarge });
validPosition = true;
console.log(` Placed ${isLarge ? 'LARGE' : 'Regular'} island ${i+1} at (${x.toFixed(0)}, ${z.toFixed(0)}) Size: ${size.toFixed(1)}`);
}
}
if (!validPosition) { console.warn("Could not place an island after max attempts."); i--;} // Try again for this index if placement failed
}
console.log(`Generated ${islands.length} islands.`);
return islands;
}
gameState.world.islands = generateIslands();
// --- Refactored Player Cleanup Logic ---
function handlePlayerCleanup(playerId, reason = 'Unknown') {
const player = gameState.players.get(playerId); if (!player) return; console.log(`[Cleanup] Removing player ${playerId}. Reason: ${reason}.`); const deleted = gameState.players.delete(playerId); if (deleted) { console.log(`[Cleanup] Player ${playerId} removed from gameState. Total players: ${gameState.players.size}`); broadcast({ type: 'playerLeft', playerId: playerId }); } else { console.warn(`[Cleanup] Attempted to remove player ${playerId}, but they were not found in the map.`); }
}
// --- WebSocket Connection Handling ---
wss.on('connection', (ws, req) => {
const remoteAddr = req.socket.remoteAddress || req.headers['x-forwarded-for']; console.log('New client connected from:', remoteAddr); const playerId = Date.now().toString() + Math.random().toString(36).substring(2, 7); ws.playerId = playerId; const initialPosition = getRandomSpawnPoint();
const playerData = { id: playerId, position: initialPosition, rotation: 0, speed: 0, health: 100, lastUpdate: Date.now(), lastShotTime: 0 }; gameState.players.set(playerId, playerData); console.log(`Player ${playerId} joined. Spawned at (${initialPosition.x.toFixed(1)}, ${initialPosition.z.toFixed(1)}). Total players: ${gameState.players.size}`);
const initData = { type: 'init', playerId: playerId, gameState: { players: Array.from(gameState.players.values()), world: gameState.world } }; console.log(`[Server Init] Sending init data to ${playerId}. Players included: ${initData.gameState.players.map(p => p.id)}`); safeSend(ws, initData);
broadcast({ type: 'playerJoined', player: playerData }, ws);
ws.on('message', (message) => {
const player = gameState.players.get(playerId); if (!player) return; player.lastUpdate = Date.now();
try { const data = JSON.parse(message); switch (data.type) { case 'updatePosition': if (isValidPosition(data.position)) updatePlayerPosition(playerId, data.position, player); else console.warn(`Invalid position from ${playerId}`); break; case 'updateRotation': updatePlayerRotation(playerId, data.rotation, player); break; case 'updateSpeed': updatePlayerSpeed(playerId, data.speed, player); break; case 'playerHit': handlePlayerHit(player, data); break; default: console.log(`Unknown message type from ${playerId}: ${data.type}`); } } catch (error) { console.error(`Failed to process message from ${playerId}:`, message.toString(), error); }
});
ws.on('pong', () => { const player = gameState.players.get(playerId); if (player) player.lastUpdate = Date.now(); });
ws.on('close', (code, reason) => { handlePlayerCleanup(playerId, `WebSocket closed (Code: ${code}, Reason: ${reason || 'None'})`); });
ws.on('error', (error) => { handlePlayerCleanup(playerId, `WebSocket error (${error.message})`); ws.terminate(); });
});
// --- Heartbeat and Timeout Interval ---
const interval = setInterval(() => {
const now = Date.now(); wss.clients.forEach(client => { const player = gameState.players.get(client.playerId); if (!player) { console.warn(`[Interval] Client ${client.playerId} connected but not in gameState. Terminating.`); client.terminate(); return; } if (now - player.lastUpdate > CLIENT_TIMEOUT) { console.log(`[Interval] Player ${client.playerId} timed out. Terminating.`); client.terminate(); handlePlayerCleanup(client.playerId, 'Client Activity Timeout'); } else { if (client.readyState === WebSocket.OPEN) client.ping(); } });
}, PING_INTERVAL);
wss.on('close', () => clearInterval(interval));
// --- Helper Functions (Existing) ---
function safeSend(ws, data) { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(data)); }
function isValidPosition(position) { if (!position || typeof position.x !== 'number' || typeof position.z !== 'number') return false; const bounds = gameState.world.worldBounds; return position.x >= bounds.minX && position.x <= bounds.maxX && position.z >= bounds.minZ && position.z <= bounds.maxZ; }
function updatePlayerPosition(playerId, position, player) { if (player) { player.position = position; broadcast({ type: 'playerMoved', playerId: playerId, position: position }, null, true); } }
function updatePlayerRotation(playerId, rotation, player) { if (player && typeof rotation === 'number') { player.rotation = rotation; broadcast({ type: 'playerRotated', playerId: playerId, rotation: rotation }, null, true); } }
function updatePlayerSpeed(playerId, speed, player) { if (player && typeof speed === 'number') { if (Math.abs(player.speed - speed) > 0.05 || speed === 0 || player.speed === 0) { player.speed = speed; broadcast({ type: 'playerSpeedChanged', playerId: playerId, speed: speed }, null, true); } else { player.speed = speed; } } }
// SERVER HIT HANDLING LOGIC (Audio Flag Removed)
function handlePlayerHit(shooterPlayer, data) {
const { targetId, damage = 10, position } = data; const shooterId = shooterPlayer.id; if (!targetId || !position) return; const targetPlayer = gameState.players.get(targetId); if (!targetPlayer || targetPlayer.health <= 0 || shooterId === targetId) return;
const now = Date.now(); if (now - shooterPlayer.lastShotTime < WEAPON_COOLDOWN) return; const dx = shooterPlayer.position.x - targetPlayer.position.x; const dz = shooterPlayer.position.z - targetPlayer.position.z; const distanceSq = dx * dx + dz * dz; const rangeSq = MAX_WEAPON_RANGE * MAX_WEAPON_RANGE; if (distanceSq > rangeSq) return;
shooterPlayer.lastShotTime = now; const oldHealth = targetPlayer.health; targetPlayer.health = Math.max(0, oldHealth - damage); targetPlayer.lastUpdate = Date.now(); console.log(`Player ${targetId} health changed: ${oldHealth} -> ${targetPlayer.health}`);
let targetWs = null; for (const client of wss.clients) { if (client.playerId === targetId) { targetWs = client; break; } } if (targetWs) safeSend(targetWs, { type: 'updateHealth', health: targetPlayer.health, oldHealth: oldHealth, damage: damage, source: 'hit' }); else console.warn(`Could not find WebSocket for target ${targetId} to send health update.`);
// Broadcast visual effect ONLY
broadcast({ type: 'playerHitEffect', targetId: targetId, shooterId: shooterId, position: position });
// Check defeat & respawn
if (targetPlayer.health <= 0 && oldHealth > 0) {
console.log(`Player ${targetId} defeated by ${shooterId}!`); broadcast({ type: 'playerDefeated', playerId: targetId, killerId: shooterId });
setTimeout(() => { const playerToRespawn = gameState.players.get(targetId); if (playerToRespawn) { playerToRespawn.health = 100; playerToRespawn.position = getRandomSpawnPoint(); playerToRespawn.rotation = 0; playerToRespawn.speed = 0; playerToRespawn.lastShotTime = 0; playerToRespawn.lastUpdate = Date.now(); console.log(`Player ${targetId} respawned.`); broadcast({ type: 'playerRespawned', player: playerToRespawn }); let respawnedWs = null; for (const client of wss.clients) { if (client.playerId === targetId) { respawnedWs = client; break; } } if (respawnedWs) safeSend(respawnedWs, { type: 'updateHealth', health: playerToRespawn.health, oldHealth: 0, damage: 0, source: 'respawn' }); } }, RESPAWN_TIME);
}
}
// Broadcast data
function broadcast(data, excludeWs = null, isFrequent = false) {
// Optional reduced logging
// if (!isFrequent) { console.log(`Broadcasting: ${data.type}`) }
const message = JSON.stringify(data);
wss.clients.forEach(client => { if (client !== excludeWs && client.playerId && client.readyState === WebSocket.OPEN) client.send(message); });
}
console.log('Server setup complete. Waiting for connections...');