chore: adjust player tank's size

This commit is contained in:
jakciehan
2026-05-02 13:50:52 +08:00
parent 0e321bcea6
commit 38294c040c
35 changed files with 5767 additions and 348 deletions
+96 -22
View File
@@ -2,18 +2,48 @@
* Tank War PVP Server
* WebSocket server for online 1v1 multiplayer.
* Handles room management, message relay, and basic game state authority.
*
* Deployment note:
* - /health → HTTP health check (used by K8s livenessProbe/readinessProbe)
* - /tankwar/ws → WebSocket upgrade path (exposed publicly via Nginx)
* Both share the same HTTP server on PORT.
*/
const { WebSocketServer } = require('ws');
const http = require('http');
// ============================================================
// Configuration
// ============================================================
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '0.0.0.0';
const WS_PATH = process.env.WS_PATH || '/tankwar/ws';
const HEARTBEAT_INTERVAL = 10000; // ms
const ROOM_TIMEOUT = 300000; // 5 minutes room idle timeout
// ============================================================
// HTTP Health Check Server
// ============================================================
const healthServer = http.createServer((req, res) => {
if (req.url === '/health' || req.url === '/tankwar/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'healthy',
timestamp: new Date().toISOString(),
activeConnections: players.size,
activeRooms: rooms.size,
activeTeamRooms: teamRooms.size
}));
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
});
healthServer.listen(PORT, HOST, () => {
console.log(`[Health Server] Running on ${HOST}:${PORT}`);
});
// ============================================================
// Message Types (must match client NET_MSG)
// ============================================================
@@ -135,6 +165,7 @@ class PlayerInfo {
constructor(ws, playerId) {
this.ws = ws;
this.playerId = playerId;
this.nickname = '';
this.roomId = null;
this.teamId = null;
this.isAlive = true;
@@ -152,8 +183,9 @@ class TeamRoom {
* @param {WebSocket} leaderWs - WebSocket of the team leader
* @param {string} leaderId - Player id of the leader
* @param {string} [battleMode='3v3'] - Battle mode ('1v1' or '3v3')
* @param {string} [leaderNickname=''] - Display nickname of the leader
*/
constructor(id, leaderWs, leaderId, battleMode = '3v3') {
constructor(id, leaderWs, leaderId, battleMode = '3v3', leaderNickname = '') {
this.id = id;
this.state = 'forming'; // forming | matching | playing | finished
this.createdAt = Date.now();
@@ -164,8 +196,8 @@ class TeamRoom {
this.teamSize = config.teamSize;
this.fillWithBotsEnabled = config.fillWithBots;
// Team A members: { ws, playerId, ready, isBot, disconnectedAt }
this.teamA = [{ ws: leaderWs, playerId: leaderId, ready: true, isBot: false, disconnectedAt: null }];
// Team A members: { ws, playerId, nickname, ready, isBot, disconnectedAt }
this.teamA = [{ ws: leaderWs, playerId: leaderId, nickname: leaderNickname || '', ready: true, isBot: false, disconnectedAt: null }];
// Team B members
this.teamB = [];
this.leaderId = leaderId;
@@ -233,16 +265,16 @@ class TeamRoom {
}
/** Add a player to team A */
addToTeamA(ws, playerId) {
addToTeamA(ws, playerId, nickname = '') {
if (this.isTeamAFull()) return false;
this.teamA.push({ ws, playerId, ready: false, isBot: false, disconnectedAt: null });
this.teamA.push({ ws, playerId, nickname, ready: false, isBot: false, disconnectedAt: null });
return true;
}
/** Add a player to team B */
addToTeamB(ws, playerId) {
addToTeamB(ws, playerId, nickname = '') {
if (this.teamB.length >= this.teamSize) return false;
this.teamB.push({ ws, playerId, ready: false, isBot: false, disconnectedAt: null });
this.teamB.push({ ws, playerId, nickname, ready: false, isBot: false, disconnectedAt: null });
return true;
}
@@ -260,6 +292,7 @@ class TeamRoom {
this.teamA.push({
ws: null,
playerId: `bot_a_${botCounter}_${this.id}`,
nickname: '',
ready: true,
isBot: true,
disconnectedAt: null,
@@ -270,6 +303,7 @@ class TeamRoom {
this.teamB.push({
ws: null,
playerId: `bot_b_${botCounter}_${this.id}`,
nickname: '',
ready: true,
isBot: true,
disconnectedAt: null,
@@ -335,6 +369,7 @@ class TeamRoom {
teamSize: this.teamSize,
teamA: this.teamA.map(m => ({
playerId: m.playerId,
nickname: m.nickname || '',
ready: m.ready,
isBot: m.isBot,
isLeader: m.playerId === this.leaderId,
@@ -342,6 +377,7 @@ class TeamRoom {
})),
teamB: this.teamB.map(m => ({
playerId: m.playerId,
nickname: m.nickname || '',
ready: m.ready,
isBot: m.isBot,
connected: m.isBot || (m.ws && m.ws.readyState === 1),
@@ -449,7 +485,7 @@ function handleCreateRoom(ws, data) {
const roomCode = generateRoomCode();
// Create a TeamRoom in 1v1 mode instead of a legacy Room
const teamRoom = new TeamRoom(roomCode, ws, playerInfo.playerId, '1v1');
const teamRoom = new TeamRoom(roomCode, ws, playerInfo.playerId, '1v1', playerInfo.nickname || '');
teamRooms.set(roomCode, teamRoom);
playerInfo.teamId = roomCode;
@@ -498,7 +534,7 @@ function handleJoinRoom(ws, data) {
}
// Join as team B
teamRoom.addToTeamB(ws, playerInfo.playerId);
teamRoom.addToTeamB(ws, playerInfo.playerId, playerInfo.nickname || '');
playerInfo.teamId = roomId;
console.log(`[Server] Player ${playerInfo.playerId} joined 1v1 room ${roomId} (TeamRoom)`);
@@ -568,7 +604,7 @@ function handleCreateTeam(ws, data) {
}
const teamId = generateTeamId();
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId);
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '');
teamRooms.set(teamId, teamRoom);
playerInfo.teamId = teamId;
@@ -592,7 +628,7 @@ function handleJoinTeam(ws, data) {
// Team was cleaned up (e.g. leader disconnected during dev-tool reload).
// Auto-create a new room with the same ID so the invite link still works.
console.log(`[Server] Team ${teamId} not found, auto-creating for ${playerInfo.playerId}`);
teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId);
teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '');
teamRooms.set(teamId, teamRoom);
playerInfo.teamId = teamId;
sendMessage(ws, NET_MSG.TEAM_STATE, teamRoom.getTeamState());
@@ -614,7 +650,7 @@ function handleJoinTeam(ws, data) {
handleLeaveTeam(ws, {});
}
teamRoom.addToTeamA(ws, playerInfo.playerId);
teamRoom.addToTeamA(ws, playerInfo.playerId, playerInfo.nickname || '');
playerInfo.teamId = teamId;
console.log(`[Server] Player ${playerInfo.playerId} joined team ${teamId}`);
@@ -815,7 +851,7 @@ function handleSoloMatch(ws, data) {
// Create a solo team room for this player
const teamId = generateTeamId();
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId);
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '');
teamRoom.state = 'matching';
teamRoom.matchStartTime = Date.now();
teamRooms.set(teamId, teamRoom);
@@ -888,7 +924,7 @@ function tryMatchTeams() {
// Merge team B members into team A room as opponents
for (const member of teamB_room.teamA) {
teamA_room.addToTeamB(member.ws, member.playerId);
teamA_room.addToTeamB(member.ws, member.playerId, member.nickname || '');
if (member.ws) {
const info = players.get(member.ws);
if (info) info.teamId = teamA_room.id;
@@ -953,9 +989,9 @@ function tryMatchTeams() {
// Alternate: odd index -> team A, even index -> team B
if (i % 2 === 1 && !gameRoom.isTeamAFull()) {
gameRoom.addToTeamA(ws, info.playerId);
gameRoom.addToTeamA(ws, info.playerId, info.nickname || '');
} else {
gameRoom.addToTeamB(ws, info.playerId);
gameRoom.addToTeamB(ws, info.playerId, info.nickname || '');
}
}
@@ -986,8 +1022,8 @@ function startTeamGame(teamRoom) {
const gameData = {
mapId: teamRoom.mapId,
teamA: teamRoom.teamA.map(m => ({ playerId: m.playerId, isBot: m.isBot })),
teamB: teamRoom.teamB.map(m => ({ playerId: m.playerId, isBot: m.isBot })),
teamA: teamRoom.teamA.map(m => ({ playerId: m.playerId, nickname: m.nickname || '', isBot: m.isBot })),
teamB: teamRoom.teamB.map(m => ({ playerId: m.playerId, nickname: m.nickname || '', isBot: m.isBot })),
teamABaseHp: teamRoom.teamABaseHp,
teamBBaseHp: teamRoom.teamBBaseHp,
battleMode: teamRoom.battleMode,
@@ -1306,7 +1342,7 @@ function handleMessage(ws, rawData) {
return;
}
const { type, data, playerId } = msg;
const { type, data, playerId, nickname } = msg;
// Update player info
const playerInfo = players.get(ws);
@@ -1315,6 +1351,26 @@ function handleMessage(ws, rawData) {
if (playerId && !playerInfo.playerId) {
playerInfo.playerId = playerId;
}
// Refresh nickname on every message (it may be granted mid-session).
if (typeof nickname === 'string' && nickname) {
if (playerInfo.nickname !== nickname) {
playerInfo.nickname = nickname;
// Also propagate into any active team room member entry.
if (playerInfo.teamId) {
const tr = teamRooms.get(playerInfo.teamId);
if (tr) {
const member = tr.getMemberByWs(ws);
if (member && member.nickname !== nickname) {
member.nickname = nickname;
// Broadcast regardless of room state (forming / matching / playing)
// so that peers always render the latest display name — in 3v3 a
// player may only tap the UserInfoButton AFTER the match starts.
tr.broadcast(NET_MSG.TEAM_STATE, tr.getTeamState());
}
}
}
}
}
}
switch (type) {
@@ -1538,9 +1594,26 @@ setInterval(() => {
}, 300000); // Every 5 minutes
// ============================================================
// WebSocket Server
// WebSocket Server (noServer mode, shares HTTP server with health check)
// ============================================================
const wss = new WebSocketServer({ host: HOST, port: PORT });
// Use noServer mode so the WS upgrade only fires on the configured path.
// This lets /health stay as plain HTTP on the same port.
const wss = new WebSocketServer({ noServer: true });
healthServer.on('upgrade', (req, socket, head) => {
// Only upgrade on the configured WebSocket path; reject any other path.
// We compare by pathname so query strings are tolerated.
const pathname = (req.url || '').split('?')[0];
if (pathname !== WS_PATH) {
console.warn(`[Server] Rejected WebSocket upgrade on unexpected path: ${req.url}`);
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
wss.on('connection', (ws, req) => {
const ip = req.socket.remoteAddress;
@@ -1604,4 +1677,5 @@ setInterval(() => {
// Startup
// ============================================================
console.log(`[Tank War Server] Running on ${HOST}:${PORT}`);
console.log(`[Tank War Server] WebSocket URL: ws://${HOST}:${PORT}`);
console.log(`[Tank War Server] WebSocket path: ${WS_PATH}`);
console.log(`[Tank War Server] Health check paths: /health, /tankwar/health`);