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
+2
View File
@@ -272,6 +272,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',
+13 -1
View File
@@ -77,12 +77,24 @@ class Bullet {
/**
* Render the bullet.
* In team battle mode, bullet color is determined by the _isAlly flag
* set by the scene: ally = bright yellow, enemy = red.
* In single-player / PVE mode, owner-based colors are used.
* @param {CanvasRenderingContext2D} ctx
*/
render(ctx) {
if (!this.alive) return;
ctx.fillStyle = this.owner === 'player' ? '#FFFF00' : '#FF6600';
let fillColor;
if (this._isAlly !== undefined) {
// Team mode: use team-aware coloring
fillColor = this._isAlly ? '#FFFF00' : '#FF4444';
} else {
// Single-player / PVE fallback
fillColor = this.owner === 'player' ? '#FFFF00' : '#FF6600';
}
ctx.fillStyle = fillColor;
ctx.fillRect(
this.x - this.halfSize,
this.y - this.halfSize,
+1 -1
View File
@@ -45,7 +45,7 @@ class PlayerTank extends Tank {
// Skin colors (reserved for future use)
this._skinColors = null;
this._skinId = 'default';
this._skinId = null;
// Fire level system
this.fireLevel = FIRE_LEVEL.LV1;
+17
View File
@@ -307,6 +307,14 @@ class Tank {
ctx.scale(k, k);
drawTankSkin(ctx, this._skinId, this._skinColors, t);
ctx.restore();
// Gold border for local player identification
if (this._isLocal) {
ctx.strokeStyle = '#FFD700';
ctx.lineWidth = 2;
ctx.strokeRect(-this.halfSize, -this.halfSize, this.size, this.size);
// Also outline the barrel area
ctx.strokeRect(-this.size * 0.15 / 2, -this.halfSize - barrelExtra, this.size * 0.15, barrelExtra);
}
ctx.restore();
return;
}
@@ -344,6 +352,15 @@ class Tank {
ctx.fillRect(-hs, -hs, trackW, this.size);
ctx.fillRect(hs - trackW, -hs, trackW, this.size);
// Gold border for local player identification
if (this._isLocal) {
ctx.strokeStyle = '#FFD700';
ctx.lineWidth = 2;
ctx.strokeRect(-hs, -hs, this.size, this.size);
// Barrel outline
ctx.strokeRect(-barrelW / 2, -hs - barrelH, barrelW, barrelH);
}
ctx.restore();
}
+2
View File
@@ -202,6 +202,7 @@ class NetworkManager {
const profile = (typeof GameGlobal !== 'undefined') ? GameGlobal.playerProfile : null;
const nickname = (profile && profile.nickname) ? profile.nickname : '';
const avatarUrl = (profile && profile.avatarUrl) ? profile.avatarUrl : '';
const skinId = (GameGlobal && GameGlobal.skinManager) ? (GameGlobal.skinManager.getEquippedSkinId() || '') : '';
const message = JSON.stringify({
type,
@@ -209,6 +210,7 @@ class NetworkManager {
playerId: this._playerId,
nickname,
avatarUrl,
skinId,
roomId: this._roomId,
timestamp: Date.now(),
});
+1 -1
View File
@@ -20,7 +20,7 @@ class ShareManager {
if (wx.showShareMenu) {
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage'],
menus: ['shareAppMessage', 'shareTimeline'],
});
}
if (wx.onShareAppMessage) {
+6 -3
View File
@@ -140,10 +140,13 @@ const GameScene = {
});
this._playerTank.activateShield(3000); // spawn protection
// Apply equipped skin colors to player tank
// Apply equipped skin — only non-default skins override tank color
if (GameGlobal.skinManager) {
this._playerTank._skinColors = GameGlobal.skinManager.getCurrentSkinColors();
this._playerTank._skinId = GameGlobal.skinManager.getEquippedSkinId();
const skinId = GameGlobal.skinManager.getEquippedSkinId();
if (skinId && skinId !== 'default') {
this._playerTank._skinColors = GameGlobal.skinManager.getCurrentSkinColors();
this._playerTank._skinId = skinId;
}
}
// Safety: ensure player spawn area is clear of blocking terrain
+166 -33
View File
@@ -158,6 +158,10 @@ const TeamGameScene = {
const teamAMembers = (params && params.teamA) || [];
const teamBMembers = (params && params.teamB) || [];
console.log(`[TeamGameScene] teamA: ${JSON.stringify(teamAMembers.map(m => ({ playerId: m.playerId, isBot: m.isBot })))}`);
console.log(`[TeamGameScene] teamB: ${JSON.stringify(teamBMembers.map(m => ({ playerId: m.playerId, isBot: m.isBot })))}`);
console.log(`[TeamGameScene] myPlayerId: ${this._myPlayerId}`);
this._myTeam = teamAMembers.find(m => m.playerId === this._myPlayerId) ? 'A' : 'B';
// Create all player tanks
@@ -165,9 +169,25 @@ const TeamGameScene = {
this._createTeamPlayers(teamAMembers, 'A', this._mapData.teamASpawns);
this._createTeamPlayers(teamBMembers, 'B', this._mapData.teamBSpawns);
// Log _players for duplicate check
const playerIds = this._players.map(p => p.playerId);
const duplicates = playerIds.filter((id, idx) => playerIds.indexOf(id) !== idx);
if (duplicates.length > 0) {
console.error(`[TeamGameScene] DUPLICATE playerIds: ${JSON.stringify(duplicates)}`);
}
console.log(`[TeamGameScene] All players: ${JSON.stringify(this._players.map(p => ({ playerId: p.playerId, team: p.team, isLocal: p.isLocal, isBot: p.isBot })))}`);
// Find local player
this._localPlayer = this._players.find(p => p.isLocal);
// Check if enemy team has no human players (all bots) —
// if so, this client must run enemy bot AI locally since no remote
// client exists to act as authority
const enemyTeam = this._myTeam === 'A' ? 'B' : 'A';
this._enemyTeamAllBots = !this._players.some(
p => p.team === enemyTeam && !p.isBot && !p.isLocal
);
// Initialize stats
for (const p of this._players) {
this._stats[p.playerId] = { kills: 0, deaths: 0, assists: 0, baseDamage: 0 };
@@ -203,14 +223,9 @@ const TeamGameScene = {
tankColor = team === 'A' ? PLAYER1_COLOR : PLAYER2_COLOR;
}
} else {
// 3v3: local=gold, ally=blue, enemy=red
if (isLocal) {
tankColor = LOCAL_PLAYER_COLOR;
} else if (isMyTeam) {
tankColor = TEAM_A_COLOR;
} else {
tankColor = TEAM_B_COLOR;
}
// 3v3: Team A = blue, Team B = red
// Local player uses team color too (gold border drawn separately for identification)
tankColor = team === 'A' ? TEAM_A_COLOR : TEAM_B_COLOR;
}
let tank;
@@ -240,15 +255,28 @@ const TeamGameScene = {
tank.color = tankColor;
// Unlimited lives for 3v3
tank.lives = 999;
// Apply equipped skin (only for the LOCAL player — other players keep team color)
if (GameGlobal.skinManager && isLocal) {
tank._skinColors = GameGlobal.skinManager.getCurrentSkinColors();
tank._skinId = GameGlobal.skinManager.getEquippedSkinId();
// Apply equipped skin only non-default skins override team color
if (GameGlobal.skinManager) {
const skinId = isLocal
? GameGlobal.skinManager.getEquippedSkinId()
: (member.skinId || '');
if (skinId && skinId !== 'default') {
const skinDef = GameGlobal.skinManager.getSkin(skinId);
if (skinDef && skinDef.colors) {
tank._skinColors = skinDef.colors;
tank._skinId = skinId;
}
}
}
}
tank.activateShield(3000);
// Mark local player's tank for gold-border rendering
if (isLocal) {
tank._isLocal = true;
}
// Set initial direction based on team
if (team === 'A') {
tank.direction = DIRECTION.RIGHT;
@@ -321,9 +349,13 @@ const TeamGameScene = {
// Receive bullet fire from other players
unsubs.push(nm.on(NET_MSG.BULLET_FIRE, (data) => {
if (data.playerId && data.playerId !== this._myPlayerId) {
this._spawnRemoteBullet(data);
}
if (!data.playerId) return;
// Ignore our own bullets (local player or our team's bots) — we already
// created them locally. Only spawn remote bullets for enemy players/bots.
const shooter = this._players.find(p => p.playerId === data.playerId);
if (shooter && shooter.team === this._myTeam) return;
if (data.playerId === this._myPlayerId) return;
this._spawnRemoteBullet(data);
}));
// Receive player killed notification
@@ -470,6 +502,25 @@ const TeamGameScene = {
}
}));
// Receive terrain changes from remote client (brick/steel/base_wall destruction)
unsubs.push(nm.on(NET_MSG.TERRAIN_CHANGE, (data) => {
if (data.row !== undefined && data.col !== undefined && data.terrain !== undefined) {
const currentTerrain = this._mapManager.getTerrain(data.row, data.col);
// Only apply if the terrain still matches the original type (avoid double-apply)
// Accept EMPTY→EMPTY as no-op, but apply any real change
if (currentTerrain !== data.terrain) {
this._mapManager.setTerrain(data.row, data.col, data.terrain);
}
}
}));
// Receive bot state from remote client (enemy team bots)
unsubs.push(nm.on(NET_MSG.BOT_STATE, (data) => {
if (data.playerId) {
this._updateRemotePlayerState(data);
}
}));
this._unsubscribers = unsubs;
},
@@ -558,9 +609,16 @@ const TeamGameScene = {
} else if (!player.isBot) {
// Remote player interpolation
this._interpolateRemoteTank(player, dt);
} else {
// Bot AI using BotTank.updateAI
} else if (player.team === this._myTeam) {
// Our team's bot — we are the authority, run AI locally
this._updateBotAI(player, dt);
} else if (this._enemyTeamAllBots) {
// Enemy team's bot but enemy has no human players —
// run AI locally since no remote client exists to drive them
this._updateBotAI(player, dt);
} else {
// Enemy team's bot — interpolated from remote state (authority is on their side)
this._interpolateRemoteTank(player, dt);
}
player.tank.update(dt);
@@ -673,6 +731,9 @@ const TeamGameScene = {
hp: tank.hp,
alive: tank.alive,
});
// Also sync our team's bot states
this._sendBotStates();
},
_sendInputIfChanged() {
@@ -696,6 +757,32 @@ const TeamGameScene = {
}
},
_sendTerrainChange(row, col, newTerrain) {
if (!this._networkManager) return;
this._networkManager.send(NET_MSG.TERRAIN_CHANGE, {
row,
col,
terrain: newTerrain,
});
},
_sendBotStates() {
if (!this._networkManager) return;
// Only sync bots on OUR team (we are the authority for our team's bots)
const myBots = this._players.filter(p => p.isBot && p.team === this._myTeam);
for (const bot of myBots) {
const tank = bot.tank;
this._networkManager.send(NET_MSG.BOT_STATE, {
playerId: bot.playerId,
col: (tank.x - MAP_OFFSET_X) / TILE_SIZE,
row: (tank.y - MAP_OFFSET_Y) / TILE_SIZE,
direction: tank.direction,
alive: tank.alive,
hp: tank.hp,
});
}
},
// ============================================================
// Collision Detection
// ============================================================
@@ -738,15 +825,21 @@ const TeamGameScene = {
}
const terrain = this._mapManager.getTerrain(row, col);
const bulletOwner = this._players.find(p => p.playerId === bullet.ownerPlayerId);
// Only our team's bullets can modify terrain locally; enemy bullets are
// visual-only and their terrain changes come via TERRAIN_CHANGE messages.
const isAuthority = bulletOwner && bulletOwner.team === this._myTeam;
if (terrain === TERRAIN.BRICK) {
this._mapManager.setTerrain(row, col, TERRAIN.EMPTY);
if (isAuthority) {
this._mapManager.setTerrain(row, col, TERRAIN.EMPTY);
this._sendTerrainChange(row, col, TERRAIN.EMPTY);
}
bullet.destroy();
this._spawnExplosion(bullet.x, bullet.y, false);
} else if (terrain === TERRAIN.BASE_WALL) {
// Determine which team this base wall belongs to
const wallTeam = this._getBaseWallTeam(row, col);
const bulletOwner = this._players.find(p => p.playerId === bullet.ownerPlayerId);
// Friendly-fire immunity: own team's bullets don't damage own base walls
if (bulletOwner && wallTeam && bulletOwner.team === wallTeam) {
@@ -754,13 +847,21 @@ const TeamGameScene = {
return;
}
// Base wall has HP — use bulletHitTerrain for proper HP tracking
this._mapManager.bulletHitTerrain(row, col, bullet.canBreakSteel);
if (isAuthority) {
// Base wall has HP — use bulletHitTerrain for proper HP tracking
const prevTerrain = this._mapManager.getTerrain(row, col);
this._mapManager.bulletHitTerrain(row, col, bullet.canBreakSteel);
const newTerrain = this._mapManager.getTerrain(row, col);
if (prevTerrain !== newTerrain) {
this._sendTerrainChange(row, col, newTerrain);
}
}
bullet.destroy();
this._spawnExplosion(bullet.x, bullet.y, false);
} else if (terrain === TERRAIN.STEEL) {
if (bullet.canBreakSteel) {
if (bullet.canBreakSteel && isAuthority) {
this._mapManager.setTerrain(row, col, TERRAIN.EMPTY);
this._sendTerrainChange(row, col, TERRAIN.EMPTY);
}
bullet.destroy();
this._spawnExplosion(bullet.x, bullet.y, false);
@@ -836,8 +937,11 @@ const TeamGameScene = {
return; // ignore friendly base hit
}
// Local player or local bot reports base hits to server
if ((bulletOwner.isLocal || bulletOwner.isBot) && this._networkManager) {
// Local player or our team's bot reports base hits to server.
// Also report when enemy team is all bots (we are the authority for them).
const isLocalAuthority = bulletOwner.isLocal || (bulletOwner.isBot && bulletOwner.team === this._myTeam);
const isEnemyBotAuthority = this._enemyTeamAllBots && bulletOwner.isBot && bulletOwner.team !== this._myTeam;
if ((isLocalAuthority || isEnemyBotAuthority) && this._networkManager) {
this._networkManager.send(NET_MSG.BASE_HIT, {
targetTeam,
damage: 1,
@@ -856,6 +960,13 @@ const TeamGameScene = {
const bulletOwner = this._players.find(p => p.playerId === bullet.ownerPlayerId);
if (!bulletOwner) return;
// Only perform tank hit detection for bullets from our team.
// Enemy bullets are visual-only on our side; the enemy client is the
// authority for those hits and will send PLAYER_KILLED.
// Exception: when the enemy team has no human players (all bots),
// we run enemy bot AI locally and must also resolve their bullet hits.
if (bulletOwner.team !== this._myTeam && !this._enemyTeamAllBots) return;
for (const player of this._players) {
if (!player.tank.alive || player.isRespawning) continue;
@@ -972,6 +1083,7 @@ const TeamGameScene = {
ownerTank: tank,
});
bullet.ownerPlayerId = this._myPlayerId;
bullet._isAlly = true; // local player's bullets are always ally
tank.activeBullets++;
this._bullets.push(bullet);
GameGlobal.audioManager.playSFX('shoot');
@@ -1003,8 +1115,20 @@ const TeamGameScene = {
ownerTank: tank,
});
bullet.ownerPlayerId = player.playerId;
bullet._isAlly = player.team === this._myTeam; // bot on my team = ally
tank.activeBullets++;
this._bullets.push(bullet);
// Sync our team's bot bullets to the remote client
if (player.team === this._myTeam && this._networkManager) {
this._networkManager.send(NET_MSG.BULLET_FIRE, {
playerId: player.playerId,
col: (bullet.x - MAP_OFFSET_X) / TILE_SIZE,
row: (bullet.y - MAP_OFFSET_Y) / TILE_SIZE,
direction: bullet.direction,
canBreakSteel: false,
});
}
},
_spawnRemoteBullet(data) {
@@ -1031,6 +1155,7 @@ const TeamGameScene = {
ownerTank: player.tank,
});
bullet.ownerPlayerId = data.playerId;
bullet._isAlly = player.team === this._myTeam; // remote on my team = ally
player.tank.activeBullets++;
this._bullets.push(bullet);
},
@@ -1070,8 +1195,11 @@ const TeamGameScene = {
// Start respawn timer
this._startRespawn(victim);
// If local player or local bot killed someone, notify server
if ((killer.isLocal || killer.isBot) && this._networkManager) {
// If local player or our team's bot killed someone, notify server.
// Also notify when enemy team is all bots (we are the authority for them).
const isLocalAuthority = killer.isLocal || (killer.isBot && killer.team === this._myTeam);
const isEnemyBotAuthority = this._enemyTeamAllBots && killer.isBot && killer.team !== this._myTeam;
if ((isLocalAuthority || isEnemyBotAuthority) && this._networkManager) {
this._networkManager.send(NET_MSG.PLAYER_KILLED, {
killerId: killer.playerId,
victimId: victim.playerId,
@@ -1191,12 +1319,11 @@ const TeamGameScene = {
// Per-tank team color:
// - local player → gold
// - ally (not me) → blue
// - enemy → red
// - Team A (ally or enemy) → blue
// - Team B (ally or enemy) → red
let labelColor;
if (player.isLocal) labelColor = LOCAL_PLAYER_COLOR;
else if (player.team === this._myTeam) labelColor = TEAM_A_COLOR;
else labelColor = TEAM_B_COLOR;
else labelColor = player.team === 'A' ? TEAM_A_COLOR : TEAM_B_COLOR;
ctx.fillStyle = labelColor;
ctx.textAlign = 'center';
@@ -1289,15 +1416,21 @@ const TeamGameScene = {
if (profile && profile.nickname) raw = profile.nickname;
else raw = player.nickname || '';
} else {
// For remote players, use the server-provided nickname.
// Do NOT fall back to profile.getDisplayName() because that returns
// the LOCAL player's nickname when set, which would show the wrong
// name for every remote player.
raw = player.nickname || '';
}
if (!raw) {
if (player.isBot) {
raw = ''; // bot — we already draw the 🤖 marker, skip name
} else if (profile && typeof profile.getDisplayName === 'function') {
raw = profile.getDisplayName(player.playerId);
} else if (player.playerId && typeof player.playerId === 'string') {
// Derive a stable anonymous tag from the remote player's own ID.
const tail = player.playerId.slice(-4).toUpperCase();
raw = `Tanker_${tail}`;
} else {
raw = player.playerId || '';
raw = '';
}
}
if (!raw) return '';
+2
View File
@@ -360,6 +360,8 @@ const TeamRoomScene = {
teamB: data.teamB,
teamABaseHp: data.teamABaseHp,
teamBBaseHp: data.teamBBaseHp,
battleMode: data.battleMode || '3v3',
roomId: data.roomId || '',
myPlayerId: this._myPlayerId,
});
},