spinner-io / server /src /game /Room.js
Phuc Long Nguyen
Init project from github
f676ff2
Raw
History Blame Contribute Delete
7.95 kB
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;