fix: 3v3 team match wrong problems

This commit is contained in:
jakciehan
2026-05-16 09:59:54 +08:00
parent 9359139186
commit 7d17325be6
30 changed files with 441 additions and 535 deletions
+87 -31
View File
@@ -146,6 +146,8 @@ const NET_MSG = {
PLAYER_RESPAWN: 'player_respawn',
TEAM_GAME_START: 'team_game_start',
TEAM_GAME_OVER: 'team_game_over',
TERRAIN_CHANGE: 'terrain_change',
BOT_STATE: 'bot_state',
RECONNECT: 'reconnect',
RECONNECT_OK: 'reconnect_ok',
PLAYER_DISCONNECT: 'player_disconnect',
@@ -230,6 +232,7 @@ class PlayerInfo {
this.playerId = playerId;
this.nickname = '';
this.avatarUrl = '';
this.skinId = ''; // equipped tank skin id
this.roomId = null;
this.teamId = null;
this.isAlive = true;
@@ -249,7 +252,7 @@ class TeamRoom {
* @param {string} [battleMode='3v3'] - Battle mode ('1v1' or '3v3')
* @param {string} [leaderNickname=''] - Display nickname of the leader
*/
constructor(id, leaderWs, leaderId, battleMode = '3v3', leaderNickname = '', leaderAvatarUrl = '') {
constructor(id, leaderWs, leaderId, battleMode = '3v3', leaderNickname = '', leaderAvatarUrl = '', leaderSkinId = '') {
this.id = id;
this.state = 'forming'; // forming | matching | playing | finished
this.createdAt = Date.now();
@@ -260,8 +263,8 @@ class TeamRoom {
this.teamSize = config.teamSize;
this.fillWithBotsEnabled = config.fillWithBots;
// Team A members: { ws, playerId, nickname, avatarUrl, ready, isBot, disconnectedAt }
this.teamA = [{ ws: leaderWs, playerId: leaderId, nickname: leaderNickname || '', avatarUrl: leaderAvatarUrl || '', ready: true, isBot: false, disconnectedAt: null }];
// Team A members: { ws, playerId, nickname, avatarUrl, skinId, ready, isBot, disconnectedAt }
this.teamA = [{ ws: leaderWs, playerId: leaderId, nickname: leaderNickname || '', avatarUrl: leaderAvatarUrl || '', skinId: leaderSkinId || '', ready: true, isBot: false, disconnectedAt: null }];
// Team B members
this.teamB = [];
this.leaderId = leaderId;
@@ -329,15 +332,19 @@ class TeamRoom {
}
/** Add a player to team A */
addToTeamA(ws, playerId, nickname = '', avatarUrl = '') {
addToTeamA(ws, playerId, nickname = '', avatarUrl = '', skinId = '') {
if (this.teamA.length >= this.teamSize) return false;
this.teamA.push({ ws, playerId, nickname, avatarUrl, ready: false, isBot: false, disconnectedAt: null });
// Prevent duplicate playerId across both teams
if (this.teamA.some(m => m.playerId === playerId) || this.teamB.some(m => m.playerId === playerId)) return false;
this.teamA.push({ ws, playerId, nickname, avatarUrl, skinId, ready: false, isBot: false, disconnectedAt: null });
return true;
}
addToTeamB(ws, playerId, nickname = '', avatarUrl = '') {
addToTeamB(ws, playerId, nickname = '', avatarUrl = '', skinId = '') {
if (this.teamB.length >= this.teamSize) return false;
this.teamB.push({ ws, playerId, nickname, avatarUrl, ready: false, isBot: false, disconnectedAt: null });
// Prevent duplicate playerId across both teams
if (this.teamA.some(m => m.playerId === playerId) || this.teamB.some(m => m.playerId === playerId)) return false;
this.teamB.push({ ws, playerId, nickname, avatarUrl, skinId, ready: false, isBot: false, disconnectedAt: null });
return true;
}
/** Remove a player from the team room */
@@ -435,6 +442,7 @@ class TeamRoom {
playerId: m.playerId,
nickname: m.nickname || '',
avatarUrl: m.avatarUrl || '',
skinId: m.skinId || '',
ready: m.ready,
isBot: m.isBot,
isLeader: m.playerId === this.leaderId,
@@ -444,6 +452,7 @@ class TeamRoom {
playerId: m.playerId,
nickname: m.nickname || '',
avatarUrl: m.avatarUrl || '',
skinId: m.skinId || '',
ready: m.ready,
isBot: m.isBot,
connected: m.isBot || (m.ws && m.ws.readyState === 1),
@@ -551,7 +560,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', playerInfo.nickname || '', playerInfo.avatarUrl || '');
const teamRoom = new TeamRoom(roomCode, ws, playerInfo.playerId, '1v1', playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
teamRooms.set(roomCode, teamRoom);
playerInfo.teamId = roomCode;
@@ -600,7 +609,7 @@ function handleJoinRoom(ws, data) {
}
// Join as team B
teamRoom.addToTeamB(ws, playerInfo.playerId, playerInfo.nickname || '', playerInfo.avatarUrl || '');
teamRoom.addToTeamB(ws, playerInfo.playerId, playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
playerInfo.teamId = roomId;
console.log(`[Server] Player ${playerInfo.playerId} joined 1v1 room ${roomId} (TeamRoom)`);
@@ -670,7 +679,7 @@ function handleCreateTeam(ws, data) {
}
const teamId = generateTeamId();
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '', playerInfo.avatarUrl || '');
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
teamRooms.set(teamId, teamRoom);
playerInfo.teamId = teamId;
@@ -694,7 +703,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, '3v3', playerInfo.nickname || '', playerInfo.avatarUrl || '');
teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
teamRooms.set(teamId, teamRoom);
playerInfo.teamId = teamId;
sendMessage(ws, NET_MSG.TEAM_STATE, teamRoom.getTeamState());
@@ -716,7 +725,7 @@ function handleJoinTeam(ws, data) {
handleLeaveTeam(ws, {});
}
teamRoom.addToTeamA(ws, playerInfo.playerId, playerInfo.nickname || '', playerInfo.avatarUrl || '');
teamRoom.addToTeamA(ws, playerInfo.playerId, playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
playerInfo.teamId = teamId;
console.log(`[Server] Player ${playerInfo.playerId} joined team ${teamId}`);
@@ -874,8 +883,10 @@ function handleMatchStart(ws, data) {
teamRoom.state = 'matching';
teamRoom.matchStartTime = Date.now();
// Add to match pool
teamMatchPool.push(teamRoom);
// Add to match pool (dedup guard)
if (!teamMatchPool.includes(teamRoom)) {
teamMatchPool.push(teamRoom);
}
console.log(`[Server] Team ${teamRoom.id} entered matching pool`);
@@ -917,14 +928,16 @@ function handleSoloMatch(ws, data) {
// Create a solo team room for this player
const teamId = generateTeamId();
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '', playerInfo.avatarUrl || '');
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
teamRoom.state = 'matching';
teamRoom.matchStartTime = Date.now();
teamRooms.set(teamId, teamRoom);
playerInfo.teamId = teamId;
// Add to solo match pool
soloMatchPool.push(ws);
// Add to solo match pool (dedup guard)
if (!soloMatchPool.includes(ws)) {
soloMatchPool.push(ws);
}
console.log(`[Server] Player ${playerInfo.playerId} entered solo match pool (team ${teamId})`);
@@ -988,9 +1001,11 @@ function tryMatchTeams() {
const teamA_room = teamMatchPool.shift();
const teamB_room = teamMatchPool.shift();
// Merge team B members into team A room as opponents
for (const member of teamB_room.teamA) {
teamA_room.addToTeamB(member.ws, member.playerId, member.nickname || '', member.avatarUrl || '');
// Merge team B room members into team A room as opponents
// Both teamA and teamB of teamB_room should be moved
const allBMembers = [...teamB_room.teamA, ...teamB_room.teamB];
for (const member of allBMembers) {
teamA_room.addToTeamB(member.ws, member.playerId, member.nickname || '', member.avatarUrl || '', member.skinId || '');
if (member.ws) {
const info = players.get(member.ws);
if (info) info.teamId = teamA_room.id;
@@ -1019,8 +1034,15 @@ function tryMatchTeams() {
});
if (availableSolos.length >= 2) {
// Take up to 10 solo players and form a game
const gamePlayers = availableSolos.splice(0, Math.min(10, availableSolos.length));
// Deduplicate by playerId — same player should not appear twice
const seenIds = new Set();
const gamePlayersRaw = availableSolos.splice(0, Math.min(10, availableSolos.length));
const gamePlayers = gamePlayersRaw.filter(ws => {
const info = players.get(ws);
if (!info || seenIds.has(info.playerId)) return false;
seenIds.add(info.playerId);
return true;
});
// Remove from solo pool
for (const ws of gamePlayers) {
@@ -1053,11 +1075,17 @@ function tryMatchTeams() {
info.teamId = gameRoom.id;
// Alternate: odd index -> team A, even index -> team B
if (i % 2 === 1 && !gameRoom.isTeamAFull()) {
gameRoom.addToTeamA(ws, info.playerId, info.nickname || '', info.avatarUrl || '');
// Alternate teams: first remaining player → teamB, next → teamA, etc.
// (player at index 0 is already in teamA as the room creator)
const isTeamBSlot = (i % 2 === 1);
if (isTeamBSlot && !gameRoom.isTeamBFull()) {
gameRoom.addToTeamB(ws, info.playerId, info.nickname || '', info.avatarUrl || '', info.skinId || '');
} else if (!isTeamBSlot && !gameRoom.isTeamAFull()) {
gameRoom.addToTeamA(ws, info.playerId, info.nickname || '', info.avatarUrl || '', info.skinId || '');
} else if (!gameRoom.isTeamBFull()) {
gameRoom.addToTeamB(ws, info.playerId, info.nickname || '', info.avatarUrl || '', info.skinId || '');
} else {
gameRoom.addToTeamB(ws, info.playerId, info.nickname || '', info.avatarUrl || '');
gameRoom.addToTeamA(ws, info.playerId, info.nickname || '', info.avatarUrl || '', info.skinId || '');
}
}
@@ -1088,8 +1116,8 @@ function startTeamGame(teamRoom) {
const gameData = {
mapId: teamRoom.mapId,
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 })),
teamA: teamRoom.teamA.map(m => ({ playerId: m.playerId, nickname: m.nickname || '', isBot: m.isBot, skinId: m.skinId || '' })),
teamB: teamRoom.teamB.map(m => ({ playerId: m.playerId, nickname: m.nickname || '', isBot: m.isBot, skinId: m.skinId || '' })),
teamABaseHp: teamRoom.teamABaseHp,
teamBBaseHp: teamRoom.teamBBaseHp,
battleMode: teamRoom.battleMode,
@@ -1097,6 +1125,8 @@ function startTeamGame(teamRoom) {
};
console.log(`[Server] ${teamRoom.battleMode} game starting in room ${teamRoom.id}`);
console.log(`[Server] teamA: ${JSON.stringify(gameData.teamA.map(m => ({ playerId: m.playerId, isBot: m.isBot })))}`);
console.log(`[Server] teamB: ${JSON.stringify(gameData.teamB.map(m => ({ playerId: m.playerId, isBot: m.isBot })))}`);
// For 1v1, use GAME_START message (client RoomScene listens for it)
// For 3v3, use TEAM_GAME_START message (client TeamRoomScene listens for it)
@@ -1408,7 +1438,7 @@ function handleMessage(ws, rawData) {
return;
}
const { type, data, playerId, nickname, avatarUrl } = msg;
const { type, data, playerId, nickname, avatarUrl, skinId } = msg;
// Update player info
const playerInfo = players.get(ws);
@@ -1451,6 +1481,20 @@ function handleMessage(ws, rawData) {
}
}
}
// Refresh skinId on every message (equipped skin may change mid-session).
if (typeof skinId === 'string' && skinId && playerInfo.skinId !== skinId) {
playerInfo.skinId = skinId;
if (playerInfo.teamId) {
const tr = teamRooms.get(playerInfo.teamId);
if (tr) {
const member = tr.getMemberByWs(ws);
if (member && member.skinId !== skinId) {
member.skinId = skinId;
tr.broadcast(NET_MSG.TEAM_STATE, tr.getTeamState());
}
}
}
}
}
switch (type) {
@@ -1469,15 +1513,27 @@ function handleMessage(ws, rawData) {
// Relay gameplay messages
case NET_MSG.PLAYER_INPUT:
// Messages where playerId is the sender themselves — override to prevent spoofing
case NET_MSG.PLAYER_INPUT:
if (playerInfo && playerInfo.teamId) {
relayToTeamRoom(ws, type, { ...data, playerId: playerInfo.playerId });
} else if (playerInfo && playerInfo.roomId) {
relayToOpponent(ws, type, data || {});
}
break;
// Messages where playerId/victimId/killerId refer to specific entities (bots, etc.)
// — must NOT be overwritten with the sender's playerId
case NET_MSG.PLAYER_STATE:
case NET_MSG.BULLET_FIRE:
case NET_MSG.BULLET_HIT:
case NET_MSG.PLAYER_HIT:
case NET_MSG.PLAYER_KILLED:
case NET_MSG.GAME_OVER:
// All modes now use teamRoom relay
case NET_MSG.TERRAIN_CHANGE:
case NET_MSG.BOT_STATE:
if (playerInfo && playerInfo.teamId) {
relayToTeamRoom(ws, type, { ...data, playerId: playerInfo.playerId });
relayToTeamRoom(ws, type, data || {});
} else if (playerInfo && playerInfo.roomId) {
relayToOpponent(ws, type, data || {});
}