Spaces:
Sleeping
Sleeping
| const Player = require('./Player'); | |
| const Bot = require('./Bot'); | |
| const PickupManager = require('./PickupManager'); | |
| const { | |
| TICK_RATE, | |
| SCORE_PER_PICKUP, | |
| ORBIT_SPEED, | |
| ORBIT_RADIUS_BASE, | |
| ORBIT_RADIUS_STEP, | |
| ORBIT_ITEM_RADIUS, | |
| MAP_WIDTH, | |
| MAP_HEIGHT, | |
| } = require('../../../shared/constants'); | |
| class Room { | |
| constructor(io) { | |
| this.io = io; | |
| this.players = new Map(); | |
| this.pickups = new PickupManager(); | |
| this.orbitClock = 0; | |
| this.lastHit = null; | |
| this.tickIntervalMs = 1000 / TICK_RATE; | |
| // Spawn initial bots | |
| this.spawnBots(); | |
| } | |
| addPlayer(socketId, name) { | |
| // Determine the skinId with the least usage to avoid duplicates | |
| const skinCounts = [0, 0, 0, 0]; | |
| for (const p of this.players.values()) { | |
| if (p.skinId >= 0 && p.skinId < 4) { | |
| skinCounts[p.skinId]++; | |
| } | |
| } | |
| const minCount = Math.min(...skinCounts); | |
| const candidates = []; | |
| for (let i = 0; i < 4; i++) { | |
| if (skinCounts[i] === minCount) { | |
| candidates.push(i); | |
| } | |
| } | |
| const chosenSkinId = candidates[Math.floor(Math.random() * candidates.length)]; | |
| const player = new Player(socketId, name, chosenSkinId); | |
| this.players.set(socketId, player); | |
| return player; | |
| } | |
| removePlayer(socketId) { | |
| this.players.delete(socketId); | |
| } | |
| spawnBots() { | |
| const activeBots = Array.from(this.players.values()).filter(p => p.isBot); | |
| const botsNeeded = 5 - activeBots.length; | |
| if (botsNeeded <= 0) return; | |
| const botNames = [ | |
| 'AlphaBot', 'BetaBot', 'DeltaBot', 'ShadowSpinner', 'NeonBlade', | |
| 'OrbitKing', 'ZeroGravity', 'StarChaser', 'SonicBlade', 'CyberSpinner' | |
| ]; | |
| for (let i = 0; i < botsNeeded; i++) { | |
| const botId = `bot_${Math.random().toString(36).substr(2, 9)}`; | |
| const name = `[BOT] ${botNames[Math.floor(Math.random() * botNames.length)]}`; | |
| const skinCounts = [0, 0, 0, 0]; | |
| for (const p of this.players.values()) { | |
| if (p.skinId >= 0 && p.skinId < 4) { | |
| skinCounts[p.skinId]++; | |
| } | |
| } | |
| const minCount = Math.min(...skinCounts); | |
| const candidates = []; | |
| for (let s = 0; s < 4; s++) { | |
| if (skinCounts[s] === minCount) { | |
| candidates.push(s); | |
| } | |
| } | |
| const chosenSkinId = candidates[Math.floor(Math.random() * candidates.length)]; | |
| const bot = new Bot(botId, name, chosenSkinId); | |
| bot.addOrbit(); | |
| this.players.set(botId, bot); | |
| } | |
| } | |
| // Remove bots that exceeded their lifetime to reduce server load | |
| cleanupBots() { | |
| const now = Date.now(); | |
| const toRemove = []; | |
| for (const p of this.players.values()) { | |
| if (p.isBot && p.spawnTime && p.lifetime) { | |
| if (now - p.spawnTime >= p.lifetime) { | |
| toRemove.push(p.id); | |
| } | |
| } | |
| } | |
| for (const id of toRemove) { | |
| this.removePlayer(id); | |
| console.log(`Removed bot ${id} after lifetime expired`); | |
| } | |
| } | |
| handleInput(socketId, x, y, boost) { | |
| const player = this.players.get(socketId); | |
| if (!player) return; | |
| if (typeof x !== 'number' || typeof y !== 'number') return; | |
| player.setInput(x, y); | |
| player.setBoost(!!boost); | |
| } | |
| checkCollisions() { | |
| for (const player of this.players.values()) { | |
| for (const pickup of this.pickups.list()) { | |
| const dx = pickup.x - player.x; | |
| const dy = pickup.y - player.y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < pickup.radius + player.radius) { | |
| this.pickups.remove(pickup.id); | |
| player.score += SCORE_PER_PICKUP; | |
| player.addOrbit(pickup.type || 'normal'); | |
| } | |
| } | |
| } | |
| this.pickups.fill(); | |
| } | |
| checkOrbitCollisions() { | |
| const orbits = []; | |
| for (const player of this.players.values()) { | |
| const total = player.orbits.length; | |
| player.orbits.forEach((orbit, index) => { | |
| const angleOffset = (index / total) * Math.PI * 2; | |
| const angle = angleOffset + this.orbitClock; | |
| const radius = ORBIT_RADIUS_BASE + orbit.ring * ORBIT_RADIUS_STEP; | |
| orbits.push({ | |
| player, | |
| orbit, | |
| x: player.x + Math.cos(angle) * radius, | |
| y: player.y + Math.sin(angle) * radius, | |
| radius: ORBIT_ITEM_RADIUS, | |
| }); | |
| }); | |
| } | |
| for (let i = 0; i < orbits.length; i++) { | |
| for (let j = i + 1; j < orbits.length; j++) { | |
| const a = orbits[i]; | |
| const b = orbits[j]; | |
| if (a.player.id === b.player.id) continue; | |
| if (!a.player.orbits.includes(a.orbit) || !b.player.orbits.includes(b.orbit)) { | |
| continue; | |
| } | |
| const dx = a.x - b.x; | |
| const dy = a.y - b.y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < a.radius + b.radius) { | |
| a.orbit.hp -= 1; | |
| b.orbit.hp -= 1; | |
| if (a.orbit.hp <= 0) { | |
| a.player.removeOrbit(a.orbit); | |
| } | |
| if (b.orbit.hp <= 0) { | |
| b.player.removeOrbit(b.orbit); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| checkPlayerHits() { | |
| const orbits = []; | |
| for (const player of this.players.values()) { | |
| const total = player.orbits.length; | |
| player.orbits.forEach((orbit, index) => { | |
| const angleOffset = (index / total) * Math.PI * 2; | |
| const angle = angleOffset + this.orbitClock; | |
| const radius = ORBIT_RADIUS_BASE + orbit.ring * ORBIT_RADIUS_STEP; | |
| orbits.push({ | |
| player, | |
| orbit, | |
| x: player.x + Math.cos(angle) * radius, | |
| y: player.y + Math.sin(angle) * radius, | |
| radius: ORBIT_ITEM_RADIUS, | |
| }); | |
| }); | |
| } | |
| for (const orbit of orbits) { | |
| for (const target of this.players.values()) { | |
| if (target.id === orbit.player.id) continue; | |
| if (!orbit.player.orbits.includes(orbit.orbit)) continue; | |
| const dx = orbit.x - target.x; | |
| const dy = orbit.y - target.y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (target.protectionTicks > 0) { | |
| continue; | |
| } | |
| if (dist < orbit.radius + target.radius) { | |
| orbit.player.removeOrbit(orbit.orbit); | |
| orbit.player.kills += 1; | |
| this.io.to(target.id).emit('gameOver', { | |
| score: target.score, | |
| kills: target.kills, | |
| killedBy: orbit.player.name, | |
| x: target.x, | |
| y: target.y, | |
| skinId: target.skinId | |
| }); | |
| // Drop target's orbits as pickups on the ground around target | |
| target.orbits.forEach((orb) => { | |
| const angle = Math.random() * Math.PI * 2; | |
| const dist = 30 + Math.random() * 50; | |
| const px = Math.min(Math.max(target.x + Math.cos(angle) * dist, 0), MAP_WIDTH); | |
| const py = Math.min(Math.max(target.y + Math.sin(angle) * dist, 0), MAP_HEIGHT); | |
| this.pickups.spawnAt(px, py); | |
| }); | |
| this.lastHit = { | |
| targetId: target.id, | |
| attackerId: orbit.player.id, | |
| attackerName: orbit.player.name, | |
| }; | |
| this.removePlayer(target.id); | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| tick() { | |
| this.orbitClock += ORBIT_SPEED; | |
| for (const player of this.players.values()) { | |
| if (player.isBot) { | |
| player.updateAI(this); | |
| } | |
| player.update(); | |
| } | |
| this.checkCollisions(); | |
| this.checkOrbitCollisions(); | |
| this.checkPlayerHits(); | |
| this.cleanupBots(); | |
| this.spawnBots(); | |
| this.broadcastState(); | |
| } | |
| broadcastState() { | |
| const state = { | |
| players: Array.from(this.players.values()).map(p => p.toJSON()), | |
| pickups: this.pickups.list(), | |
| orbitClock: this.orbitClock, | |
| lastHit: this.lastHit, | |
| }; | |
| this.io.emit('state', state); | |
| this.lastHit = null; | |
| } | |
| start() { | |
| this.interval = setInterval(() => this.tick(), this.tickIntervalMs); | |
| } | |
| stop() { | |
| clearInterval(this.interval); | |
| } | |
| } | |
| module.exports = Room; | |