/**
 * TeamGameScene.js
 * Unified battle scene for online multiplayer (supports 1v1 PVP and 3v3 Team modes).
 * Configurable via battleMode parameter ('1v1' or '3v3').
 * Base HP system, unlimited respawns, network-synced input, team-based win/lose logic.
 */

const {
  SCREEN_WIDTH,
  SCREEN_HEIGHT,
  COLORS,
  SCENE,
  MAP_OFFSET_X,
  MAP_OFFSET_Y,
  MAP_WIDTH,
  MAP_HEIGHT,
  TILE_SIZE,
  DIRECTION,
  DIR_VECTORS,
  TERRAIN,
  NET_MSG,
  TEAM_RESPAWN_DELAY,
  TEAM_BASE_HP,
  TEAM_SIZE,
  TANK_CONFIG,
  TANK_TYPE,
  PVP_BASE_HP,
  PVP_RESPAWN_DELAY,
  BATTLE_CONFIG,
} = require('../base/GameGlobal');
const { t } = require('../i18n/I18n');

const ObjectPool = require('../base/ObjectPool');
const MapManager = require('../managers/MapManager');
const PlayerTank = require('../entities/PlayerTank');
const BotTank = require('../entities/BotTank');
const Bullet = require('../entities/Bullet');
const Explosion = require('../entities/Explosion');
const Joystick = require('../ui/Joystick');
const FireButton = require('../ui/FireButton');
const { getTeamMap, getPvpMap } = require('../data/LevelData');

// Team colors
const TEAM_A_COLOR = '#4A90D9'; // blue
const TEAM_B_COLOR = '#E94560'; // red
const LOCAL_PLAYER_COLOR = '#FFD700'; // gold highlight

// 1v1 PVP player colors
const PLAYER1_COLOR = '#FFD700'; // gold
const PLAYER2_COLOR = '#00BFFF'; // deep sky blue

const TeamGameScene = {
  _initialized: false,
  _gameOver: false,
  _paused: false,
  _winner: '', // '' | 'A' | 'B'
  _winReason: '',

  // Reconnection state
  _isDisconnected: false,
  _reconnectTimer: 0,
  _reconnectAttempts: 0,
  _maxReconnectAttempts: 5,
  _reconnectInterval: 3, // seconds between attempts

  // Network
  _networkManager: null,
  _myPlayerId: '',
  _myTeam: '', // 'A' or 'B'
  _teamId: '',
  _unsubscribers: [],

  // Map
  _mapManager: null,
  _mapData: null,

  // Controls
  _joystick: null,
  _fireButton: null,

  // Players: { playerId, tank, isBot, team, isLocal, spawnPoint, respawnTimer }
  _players: [],
  _localPlayer: null,

  // Entity lists
  _bullets: [],
  _explosions: [],

  // Object pools
  _bulletPool: null,
  _explosionPool: null,

  // Battle config (1v1 or 3v3)
  _battleMode: '3v3', // '1v1' or '3v3'
  _battleConfig: null,

  // Game state
  _elapsedTime: 0, // seconds since game started (count up)
  _teamABaseHp: TEAM_BASE_HP,
  _teamBBaseHp: TEAM_BASE_HP,
  _gameOverDelay: 0,
  _gameOverDelayDuration: 3,

  // Stats
  _stats: {}, // { playerId: { kills, deaths, assists, baseDamage } }

  // Sync
  _syncTimer: 0,
  _syncInterval: 0.05,
  _lastSentInput: null,

  // Remote state targets for interpolation
  _remoteTargets: {}, // { playerId: { x, y, direction } }

  enter(params) {
    this._teamId = (params && params.teamId) || '';
    this._myPlayerId = (params && params.myPlayerId) || '';
    this._gameOver = false;
    this._paused = false;
    this._winner = '';
    this._winReason = '';
    this._gameOverDelay = 0;
    this._elapsedTime = 0;
    this._syncTimer = 0;
    this._lastSentInput = null;
    this._stats = {};
    this._remoteTargets = {};
    this._isDisconnected = false;
    this._reconnectTimer = 0;
    this._reconnectAttempts = 0;

    // Determine battle mode from params
    this._battleMode = (params && params.battleMode) || '3v3';
    this._battleConfig = BATTLE_CONFIG[this._battleMode] || BATTLE_CONFIG['3v3'];

    const baseHp = (params && params.teamABaseHp) || this._battleConfig.baseHp;
    this._teamABaseHp = baseHp;
    this._teamBBaseHp = (params && params.teamBBaseHp) || baseHp;

    this._networkManager = GameGlobal.networkManager;

    // Initialize object pools
    this._bulletPool = new ObjectPool(() => new Bullet(), null, 40);
    this._explosionPool = new ObjectPool(() => new Explosion(), null, 20);
    this._bullets = [];
    this._explosions = [];

    // Load map based on battle mode
    if (this._battleConfig.mapPool === 'pvp') {
      this._mapData = getPvpMap(params && params.mapId, params && params.roomId);
    } else {
      this._mapData = getTeamMap(params && params.mapId);
    }
    this._mapManager = new MapManager();
    this._mapManager.loadGrid(this._mapData.grid);

    // Determine which team the local player is on
    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
    this._players = [];
    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 };
    }

    // Initialize controls
    this._joystick = new Joystick();
    this._fireButton = new FireButton();
    this._fireButton.onFire(() => this._localFire());

    // Setup network events
    this._setupNetworkEvents();

    this._initialized = true;
    console.log(`[TeamGameScene] Started (${this._battleMode}). Team: ${this._teamId}, Player: ${this._myPlayerId}, MyTeam: ${this._myTeam}`);
  },

  _createTeamPlayers(members, team, spawnPoints) {
    for (let i = 0; i < members.length; i++) {
      const member = members[i];
      const spawn = spawnPoints[i % spawnPoints.length];
      const isLocal = member.playerId === this._myPlayerId;
      const isMyTeam = team === this._myTeam;
      const isBot = member.isBot || false;

      // Determine color based on battle mode
      let tankColor;
      if (this._battleMode === '1v1') {
        // 1v1: use P1/P2 colors based on team
        if (isLocal) {
          tankColor = team === 'A' ? PLAYER1_COLOR : PLAYER2_COLOR;
        } else {
          tankColor = team === 'A' ? PLAYER1_COLOR : PLAYER2_COLOR;
        }
      } else {
        // 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;
      if (isBot) {
        // Use BotTank for AI-controlled players
        tank = new BotTank({
          col: spawn.col,
          row: spawn.row,
          team,
          playerId: member.playerId,
          color: tankColor,
        });
        // Set target base: bots attack the enemy base
        const enemyBase = team === 'A' ? this._mapData.teamBBase : this._mapData.teamABase;
        if (enemyBase && enemyBase.length > 0) {
          tank.setTargetBase({
            x: MAP_OFFSET_X + enemyBase[0].col * TILE_SIZE + TILE_SIZE / 2,
            y: MAP_OFFSET_Y + enemyBase[0].row * TILE_SIZE + TILE_SIZE / 2,
          });
        }
      } else {
        // Use PlayerTank for human players
        tank = new PlayerTank({
          col: spawn.col,
          row: spawn.row,
        });
        tank.color = tankColor;
        // Unlimited lives for 3v3
        tank.lives = 999;
        // 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;
      } else {
        tank.direction = DIRECTION.LEFT;
      }

      // Clear spawn area
      this._clearSpawnArea(spawn.col, spawn.row);

      const playerData = {
        playerId: member.playerId,
        nickname: member.nickname || '',
        tank,
        isBot,
        team,
        isLocal,
        spawnPoint: spawn,
        respawnTimer: 0,
        isRespawning: false,
      };

      this._players.push(playerData);

      // Initialize remote target
      if (!isLocal) {
        this._remoteTargets[member.playerId] = {
          x: tank.x,
          y: tank.y,
          direction: tank.direction,
        };
      }
    }
  },

  exit() {
    this._initialized = false;
    this._cleanupNetworkEvents();
    this._bullets = [];
    this._explosions = [];
    this._players = [];
  },

  _setupNetworkEvents() {
    this._cleanupNetworkEvents();
    const nm = this._networkManager;
    if (!nm) return;

    const unsubs = [];

    // Receive player state sync from other players
    unsubs.push(nm.on(NET_MSG.PLAYER_STATE, (data) => {
      if (data.playerId && data.playerId !== this._myPlayerId) {
        this._updateRemotePlayerState(data);
      }
    }));

    // Receive player input from other players
    unsubs.push(nm.on(NET_MSG.PLAYER_INPUT, (data) => {
      if (data.playerId && data.playerId !== this._myPlayerId) {
        const player = this._players.find(p => p.playerId === data.playerId);
        if (player) {
          player._remoteInput = {
            direction: data.direction !== undefined ? data.direction : -1,
            moving: data.moving || false,
          };
        }
      }
    }));

    // Receive bullet fire from other players
    unsubs.push(nm.on(NET_MSG.BULLET_FIRE, (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
    unsubs.push(nm.on(NET_MSG.PLAYER_KILLED, (data) => {
      if (data.victimId) {
        const victim = this._players.find(p => p.playerId === data.victimId);
        if (victim && victim.tank.alive) {
          victim.tank.alive = false;
          this._spawnExplosion(victim.tank.x, victim.tank.y, true);
          this._startRespawn(victim);
        }
        // Update stats
        if (data.killerId && this._stats[data.killerId]) {
          this._stats[data.killerId].kills++;
        }
        if (this._stats[data.victimId]) {
          this._stats[data.victimId].deaths++;
        }
      }
    }));

    // Receive player respawn
    unsubs.push(nm.on(NET_MSG.PLAYER_RESPAWN, (data) => {
      if (data.playerId && data.playerId !== this._myPlayerId) {
        const player = this._players.find(p => p.playerId === data.playerId);
        if (player) {
          this._respawnPlayer(player);
        }
      }
    }));

    // Receive base hit
    unsubs.push(nm.on(NET_MSG.BASE_HIT, (data) => {
      if (data.teamABaseHp !== undefined) this._teamABaseHp = data.teamABaseHp;
      if (data.teamBBaseHp !== undefined) this._teamBBaseHp = data.teamBBaseHp;

      // Check if base destroyed (immediate client-side feedback)
      if (!this._gameOver) {
        if (this._teamABaseHp <= 0) {
          this._winner = 'B';
          this._winReason = 'base_destroyed';
          this._gameOver = true;
          GameGlobal.audioManager.playSFX(this._myTeam === 'B' ? 'victory' : 'gameover');
        } else if (this._teamBBaseHp <= 0) {
          this._winner = 'A';
          this._winReason = 'base_destroyed';
          this._gameOver = true;
          GameGlobal.audioManager.playSFX(this._myTeam === 'A' ? 'victory' : 'gameover');
        }
      }
    }));

    // Receive base destroyed / game over
    unsubs.push(nm.on(NET_MSG.BASE_DESTROYED, (data) => {
      if (data.team === 'A') {
        this._teamABaseHp = 0;
        this._winner = 'B';
      } else {
        this._teamBBaseHp = 0;
        this._winner = 'A';
      }
      this._winReason = 'base_destroyed';
      this._gameOver = true;
    }));

    unsubs.push(nm.on(NET_MSG.TEAM_GAME_OVER, (data) => {
      this._winner = data.winner || '';
      this._winReason = data.reason || 'base_destroyed';
      this._teamABaseHp = data.teamABaseHp;
      this._teamBBaseHp = data.teamBBaseHp;
      this._gameOver = true;
    }));

    // Receive game over (1v1 mode uses GAME_OVER instead of TEAM_GAME_OVER)
    unsubs.push(nm.on(NET_MSG.GAME_OVER, (data) => {
      this._winner = data.winner || '';
      this._winReason = data.reason || 'base_destroyed';
      if (data.teamABaseHp !== undefined) this._teamABaseHp = data.teamABaseHp;
      if (data.teamBBaseHp !== undefined) this._teamBBaseHp = data.teamBBaseHp;
      this._gameOver = true;
    }));

    // Opponent left (1v1 mode)
    unsubs.push(nm.on(NET_MSG.OPPONENT_LEFT, () => {
      if (!this._gameOver) {
        this._winner = this._myTeam;
        this._winReason = 'disconnected';
        this._gameOver = true;
      }
    }));

    // Player disconnect
    unsubs.push(nm.on(NET_MSG.PLAYER_DISCONNECT, (data) => {
      console.log(`[TeamGameScene] Player disconnected: ${data.playerId}`);
    }));

    // Bot takeover
    unsubs.push(nm.on(NET_MSG.BOT_TAKEOVER, (data) => {
      const player = this._players.find(p => p.playerId === data.playerId);
      if (player) {
        player.isBot = true;
        console.log(`[TeamGameScene] Bot takeover: ${data.playerId}`);
      }
    }));

    unsubs.push(nm.on(NET_MSG.RECONNECT_OK, (data) => {
      console.log('[TeamGameScene] Reconnected successfully');
      this._isDisconnected = false;
      this._reconnectAttempts = 0;
      // Restore game state from server
      if (data.teamABaseHp !== undefined) this._teamABaseHp = data.teamABaseHp;
      if (data.teamBBaseHp !== undefined) this._teamBBaseHp = data.teamBBaseHp;
    }));

    unsubs.push(nm.on('disconnected', () => {
      if (!this._gameOver) {
        this._isDisconnected = true;
        this._reconnectTimer = 0;
        this._reconnectAttempts = 0;
        console.log('[TeamGameScene] Disconnected, attempting reconnect...');
      }
    }));

    // Receive live team roster updates — keeps every tank's overhead label in
    // sync with the real WeChat nickname, which may be granted AFTER the match
    // has already started (via MenuScene's UserInfoButton).
    unsubs.push(nm.on(NET_MSG.TEAM_STATE, (data) => {
      if (!data) return;
      const rosterA = Array.isArray(data.teamA) ? data.teamA : [];
      const rosterB = Array.isArray(data.teamB) ? data.teamB : [];
      const byId = Object.create(null);
      for (const m of rosterA) if (m && m.playerId) byId[m.playerId] = m.nickname || '';
      for (const m of rosterB) if (m && m.playerId) byId[m.playerId] = m.nickname || '';
      let changed = false;
      for (const p of this._players) {
        const nn = byId[p.playerId];
        if (nn && p.nickname !== nn) {
          p.nickname = nn;
          changed = true;
        }
      }
      if (changed) {
        console.log('[TeamGameScene] Roster nicknames refreshed.');
      }
    }));

    // 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;
  },

  _cleanupNetworkEvents() {
    for (const unsub of this._unsubscribers) {
      if (typeof unsub === 'function') unsub();
    }
    this._unsubscribers = [];
  },

  _updateRemotePlayerState(data) {
    const target = this._remoteTargets[data.playerId];
    if (target) {
      // Convert normalized grid coords back to local pixel coords
      if (data.col !== undefined && data.row !== undefined) {
        target.x = MAP_OFFSET_X + data.col * TILE_SIZE;
        target.y = MAP_OFFSET_Y + data.row * TILE_SIZE;
      } else {
        target.x = data.x;
        target.y = data.y;
      }
      target.direction = data.direction;
    }
    const player = this._players.find(p => p.playerId === data.playerId);
    if (player) {
      if (data.hp !== undefined) player.tank.hp = data.hp;
      if (data.alive !== undefined) player.tank.alive = data.alive;
    }
  },

  // ============================================================
  // Update
  // ============================================================
  update(dt) {
    if (!this._initialized || this._paused) return;

    // Handle reconnection
    if (this._isDisconnected) {
      this._reconnectTimer += dt;
      if (this._reconnectTimer >= this._reconnectInterval) {
        this._reconnectTimer = 0;
        this._attemptReconnect();
      }
      return; // Pause game updates while disconnected
    }

    // Game over delay
    if (this._gameOver) {
      this._gameOverDelay += dt;
      this._updateExplosions(dt);
      if (this._gameOverDelay >= this._gameOverDelayDuration) {
        this._transitionToResult();
      }
      return;
    }

    // Elapsed time (count up, for display only)
    this._elapsedTime += dt;

    // Update map
    this._mapManager.update(dt);

    // Update all players
    for (const player of this._players) {
      if (player.isRespawning) {
        player.respawnTimer -= dt * 1000;
        if (player.respawnTimer <= 0 && (player.isLocal || player.isBot)) {
          this._respawnPlayer(player);
          // Notify server
          if (this._networkManager) {
            this._networkManager.send(NET_MSG.PLAYER_RESPAWN, {
              playerId: player.playerId,
            });
          }
        }
        continue;
      }

      if (!player.tank.alive) continue;

      if (player.isLocal) {
        // Local player movement
        if (this._joystick.active && this._joystick.direction >= 0) {
          player.tank.move(this._joystick.direction, dt, this._mapManager);
        }
      } else if (!player.isBot) {
        // Remote player interpolation
        this._interpolateRemoteTank(player, dt);
      } 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);
    }

    // Update bullets
    for (const bullet of this._bullets) {
      bullet.update(dt);
    }

    // Collision detection
    this._checkCollisions();

    // Update explosions
    this._updateExplosions(dt);

    // Cleanup
    this._cleanup();

    // Send local state periodically
    this._syncTimer += dt;
    if (this._syncTimer >= this._syncInterval) {
      this._syncTimer = 0;
      this._sendLocalState();
    }

    // Send input changes
    this._sendInputIfChanged();
  },

  _interpolateRemoteTank(player, dt) {
    const target = this._remoteTargets[player.playerId];
    if (!target) return;

    const tank = player.tank;
    tank.direction = target.direction;

    const dx = target.x - tank.x;
    const dy = target.y - tank.y;
    const dist = Math.abs(dx) + Math.abs(dy);

    if (dist > TILE_SIZE * 3) {
      // Too far away (e.g. respawn / teleport) — snap directly
      tank.x = target.x;
      tank.y = target.y;
    } else if (dist > 0.5) {
      // Interpolate towards target position.
      // No local terrain collision check — the remote position is authoritative
      // from the opponent's client. Local terrain may differ (bullets destroy
      // bricks independently on each client), so blocking here would cause
      // the remote tank to get stuck at the wrong position.
      // No input prediction — it causes accumulated drift because Tank.move()
      // has grid-snapping and terrain-sliding that differ between clients.
      const lerpSpeed = Math.min(20 * dt, 1); // cap at 1 to avoid overshoot

      tank.x += dx * lerpSpeed;
      tank.y += dy * lerpSpeed;
    } else {
      // Close enough — snap to target
      tank.x = target.x;
      tank.y = target.y;
    }
  },

  _updateBotAI(player, dt) {
    const tank = player.tank;

    // Use BotTank's built-in AI if available
    if (typeof tank.updateAI === 'function') {
      tank.updateAI(dt, this._mapManager, (bot) => {
        this._botFire(player);
      });
      return;
    }

    // Fallback: simple bot AI for non-BotTank instances
    if (!player._botTimer) player._botTimer = 0;
    if (!player._botShootTimer) player._botShootTimer = 0;
    if (!player._botDirection) player._botDirection = Math.floor(Math.random() * 4);

    player._botTimer += dt;
    player._botShootTimer += dt;

    // Change direction every 1-3 seconds
    if (player._botTimer > 1 + Math.random() * 2) {
      player._botTimer = 0;
      player._botDirection = Math.floor(Math.random() * 4);
    }

    // Move
    tank.move(player._botDirection, dt, this._mapManager);

    // Shoot every 1-2 seconds
    if (player._botShootTimer > 1 + Math.random()) {
      player._botShootTimer = 0;
      this._botFire(player);
    }
  },

  _sendLocalState() {
    if (!this._networkManager || !this._localPlayer) return;
    const tank = this._localPlayer.tank;
    // Send normalized grid coordinates instead of pixel coordinates
    // so that different screen sizes produce the same grid position.
    this._networkManager.send(NET_MSG.PLAYER_STATE, {
      playerId: this._myPlayerId,
      col: (tank.x - MAP_OFFSET_X) / TILE_SIZE,
      row: (tank.y - MAP_OFFSET_Y) / TILE_SIZE,
      direction: tank.direction,
      hp: tank.hp,
      alive: tank.alive,
    });

    // Also sync our team's bot states
    this._sendBotStates();
  },

  _sendInputIfChanged() {
    if (!this._networkManager) return;

    const currentInput = {
      direction: this._joystick.direction,
      moving: this._joystick.active && this._joystick.direction >= 0,
    };

    if (
      !this._lastSentInput ||
      this._lastSentInput.direction !== currentInput.direction ||
      this._lastSentInput.moving !== currentInput.moving
    ) {
      this._lastSentInput = { ...currentInput };
      this._networkManager.send(NET_MSG.PLAYER_INPUT, {
        playerId: this._myPlayerId,
        ...currentInput,
      });
    }
  },

  _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
  // ============================================================
  _checkCollisions() {
    const aliveBullets = this._bullets.filter(b => b.alive);

    // Bullet vs terrain (including bases)
    for (const bullet of aliveBullets) {
      if (!bullet.alive) continue;
      this._checkBulletTerrain(bullet);
    }

    // Bullet vs tanks (team-aware)
    for (const bullet of aliveBullets) {
      if (!bullet.alive) continue;
      this._checkBulletVsTanks(bullet);
    }

    // Bullet vs bullet
    this._checkBulletVsBullet(aliveBullets);

    // Tank vs tank (push apart)
    const alivePlayers = this._players.filter(p => p.tank.alive && !p.isRespawning);
    for (let i = 0; i < alivePlayers.length; i++) {
      for (let j = i + 1; j < alivePlayers.length; j++) {
        if (alivePlayers[i].tank.collidesWith(alivePlayers[j].tank)) {
          this._separateTanks(alivePlayers[i].tank, alivePlayers[j].tank);
        }
      }
    }
  },

  _checkBulletTerrain(bullet) {
    const { row, col } = this._mapManager.pixelToGrid(bullet.x, bullet.y);

    if (row < 0 || row >= this._mapData.grid.length || col < 0 || col >= this._mapData.grid[0].length) {
      bullet.destroy();
      this._spawnExplosion(bullet.x, bullet.y, false);
      return;
    }

    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) {
      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);

      // Friendly-fire immunity: own team's bullets don't damage own base walls
      if (bulletOwner && wallTeam && bulletOwner.team === wallTeam) {
        bullet.destroy();
        return;
      }

      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 && 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) {
      // Base hit! Determine which team's base
      bullet.destroy();
      this._spawnExplosion(bullet.x, bullet.y, true);
      this._handleBaseHit(bullet, row, col);
    }
  },

  /**
   * Determine which team a BASE_WALL tile belongs to based on proximity to team bases.
   * @param {number} row
   * @param {number} col
   * @returns {string} 'A', 'B', or '' if unknown
   */
  _getBaseWallTeam(row, col) {
    // Check proximity to each team's base positions
    let minDistA = Infinity;
    let minDistB = Infinity;

    if (this._mapData.teamABase) {
      for (const base of this._mapData.teamABase) {
        const dist = Math.abs(row - base.row) + Math.abs(col - base.col);
        if (dist < minDistA) minDistA = dist;
      }
    }
    if (this._mapData.teamBBase) {
      for (const base of this._mapData.teamBBase) {
        const dist = Math.abs(row - base.row) + Math.abs(col - base.col);
        if (dist < minDistB) minDistB = dist;
      }
    }

    if (minDistA < minDistB) return 'A';
    if (minDistB < minDistA) return 'B';
    // Fallback: use column position (left = A, right = B)
    return col < this._mapData.grid[0].length / 2 ? 'A' : 'B';
  },

  _handleBaseHit(bullet, row, col) {
    // Determine which team's base was hit based on position
    const bulletOwner = this._players.find(p => p.playerId === bullet.ownerPlayerId);
    if (!bulletOwner) return;

    // Check if the base belongs to team A or team B
    let targetTeam = '';
    if (this._mapData.teamABase) {
      for (const base of this._mapData.teamABase) {
        if (base.row === row && base.col === col) {
          targetTeam = 'A';
          break;
        }
      }
    }
    if (!targetTeam && this._mapData.teamBBase) {
      for (const base of this._mapData.teamBBase) {
        if (base.row === row && base.col === col) {
          targetTeam = 'B';
          break;
        }
      }
    }

    // If we can't determine by exact position, use column position
    if (!targetTeam) {
      targetTeam = col < this._mapData.grid[0].length / 2 ? 'A' : 'B';
    }

    // Friendly-fire immunity: bullets do not damage their own team's base
    if (bulletOwner.team === targetTeam) {
      return; // ignore friendly base hit
    }

    // 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,
        attackerId: bulletOwner.playerId,
      });

      // Update local stats
      if (this._stats[bulletOwner.playerId]) {
        this._stats[bulletOwner.playerId].baseDamage++;
      }
    }
  },

  _checkBulletVsTanks(bullet) {
    const bb = bullet.getBounds();
    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;

      // Friendly fire protection: don't hit teammates
      if (player.team === bulletOwner.team) continue;

      const tb = player.tank.getBounds();
      if (this._rectsOverlap(bb, tb)) {
        const destroyed = player.tank.takeDamage(1);
        bullet.destroy();

        if (destroyed) {
          this._spawnExplosion(player.tank.x, player.tank.y, true);
          this._handlePlayerDeath(player, bulletOwner);
        } else {
          this._spawnExplosion(bullet.x, bullet.y, false);
        }
        return;
      }
    }
  },

  _checkBulletVsBullet(bullets) {
    for (let i = 0; i < bullets.length; i++) {
      if (!bullets[i].alive) continue;
      for (let j = i + 1; j < bullets.length; j++) {
        if (!bullets[j].alive) continue;

        // Only cancel bullets from different teams
        const ownerI = this._players.find(p => p.playerId === bullets[i].ownerPlayerId);
        const ownerJ = this._players.find(p => p.playerId === bullets[j].ownerPlayerId);
        if (ownerI && ownerJ && ownerI.team === ownerJ.team) continue;

        const a = bullets[i].getBounds();
        const b = bullets[j].getBounds();
        if (this._rectsOverlap(a, b)) {
          const mx = (bullets[i].x + bullets[j].x) / 2;
          const my = (bullets[i].y + bullets[j].y) / 2;
          bullets[i].destroy();
          bullets[j].destroy();
          this._spawnExplosion(mx, my, false);
        }
      }
    }
  },

  _separateTanks(tankA, tankB) {
    const a = tankA.getBounds();
    const b = tankB.getBounds();
    const overlapX = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x);
    const overlapY = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y);

    if (overlapX <= 0 || overlapY <= 0) return;

    // Save original positions in case push causes terrain collision
    const origAX = tankA.x, origAY = tankA.y;
    const origBX = tankB.x, origBY = tankB.y;

    if (overlapX < overlapY) {
      const sign = tankA.x < tankB.x ? -1 : 1;
      const push = overlapX / 2;
      tankA.x += sign * push;
      tankB.x -= sign * push;
    } else {
      const sign = tankA.y < tankB.y ? -1 : 1;
      const push = overlapY / 2;
      tankA.y += sign * push;
      tankB.y -= sign * push;
    }

    // Validate pushed positions against terrain; revert if stuck in wall
    if (this._mapManager) {
      const leftA = tankA.x - tankA.colliderHalfSize;
      const topA = tankA.y - tankA.colliderHalfSize;
      if (this._mapManager.rectCollidesWithTerrain(leftA, topA, tankA.colliderSize, tankA.colliderSize)) {
        tankA.x = origAX;
        tankA.y = origAY;
      }
      const leftB = tankB.x - tankB.colliderHalfSize;
      const topB = tankB.y - tankB.colliderHalfSize;
      if (this._mapManager.rectCollidesWithTerrain(leftB, topB, tankB.colliderSize, tankB.colliderSize)) {
        tankB.x = origBX;
        tankB.y = origBY;
      }
    }
  },

  _rectsOverlap(a, b) {
    return (
      a.x < b.x + b.w &&
      a.x + a.w > b.x &&
      a.y < b.y + b.h &&
      a.y + a.h > b.y
    );
  },

  // ============================================================
  // Game Logic
  // ============================================================
  _localFire() {
    if (!this._localPlayer || !this._localPlayer.tank.alive) return;
    if (!this._localPlayer.tank.canFire()) return;
    if (this._gameOver || this._paused) return;

    const tank = this._localPlayer.tank;
    const vec = DIR_VECTORS[tank.direction];
    const bullet = this._bulletPool.get();
    bullet.init({
      x: tank.x + vec.dx * tank.halfSize,
      y: tank.y + vec.dy * tank.halfSize,
      direction: tank.direction,
      owner: 'local',
      canBreakSteel: tank.canBreakSteel(),
      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');

    // Send to network — use normalized grid coordinates
    if (this._networkManager) {
      this._networkManager.send(NET_MSG.BULLET_FIRE, {
        playerId: this._myPlayerId,
        col: (bullet.x - MAP_OFFSET_X) / TILE_SIZE,
        row: (bullet.y - MAP_OFFSET_Y) / TILE_SIZE,
        direction: bullet.direction,
        canBreakSteel: bullet.canBreakSteel,
      });
    }
  },

  _botFire(player) {
    if (!player.tank.alive || !player.tank.canFire()) return;

    const tank = player.tank;
    const vec = DIR_VECTORS[tank.direction];
    const bullet = this._bulletPool.get();
    bullet.init({
      x: tank.x + vec.dx * tank.halfSize,
      y: tank.y + vec.dy * tank.halfSize,
      direction: tank.direction,
      owner: 'bot',
      canBreakSteel: false,
      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) {
    const player = this._players.find(p => p.playerId === data.playerId);
    if (!player) return;

    // Convert normalized grid coords back to local pixel coords
    let bx, by;
    if (data.col !== undefined && data.row !== undefined) {
      bx = MAP_OFFSET_X + data.col * TILE_SIZE;
      by = MAP_OFFSET_Y + data.row * TILE_SIZE;
    } else {
      bx = data.x;
      by = data.y;
    }

    const bullet = this._bulletPool.get();
    bullet.init({
      x: bx,
      y: by,
      direction: data.direction,
      owner: 'remote',
      canBreakSteel: data.canBreakSteel || false,
      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);
  },

  _spawnExplosion(x, y, isBig) {
    const exp = this._explosionPool.get();
    exp.init(x, y, isBig);
    this._explosions.push(exp);
    GameGlobal.audioManager.playSFX(isBig ? 'explosion_big' : 'explosion_small');
  },

  _clearSpawnArea(col, row) {
    // Clear a 3x3 area around the spawn point to ensure the tank
    // (whose size is ~0.85 tiles) won't overlap adjacent blocking tiles
    for (let dr = -1; dr <= 1; dr++) {
      for (let dc = -1; dc <= 1; dc++) {
        const r = row + dr;
        const c = col + dc;
        const terrain = this._mapManager.getTerrain(r, c);
        // Clear any tank-blocking terrain except BASE and RIVER
        if (terrain === TERRAIN.BRICK || terrain === TERRAIN.STEEL || terrain === TERRAIN.BASE_WALL) {
          this._mapManager.setTerrain(r, c, TERRAIN.EMPTY);
        }
      }
    }
  },

  _handlePlayerDeath(victim, killer) {
    // Update stats
    if (this._stats[killer.playerId]) {
      this._stats[killer.playerId].kills++;
    }
    if (this._stats[victim.playerId]) {
      this._stats[victim.playerId].deaths++;
    }

    // Start respawn timer
    this._startRespawn(victim);

    // 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,
      });
    }

    // Play sound
    if (victim.isLocal) {
      GameGlobal.audioManager.playSFX('gameover');
    }
  },

  _startRespawn(player) {
    player.isRespawning = true;
    player.respawnTimer = this._battleConfig ? this._battleConfig.respawnDelay : TEAM_RESPAWN_DELAY;
    player.tank.alive = false;
  },

  _respawnPlayer(player) {
    player.isRespawning = false;
    player.respawnTimer = 0;
    player.tank.alive = true;
    player.tank.hp = 1;

    // Respawn at team spawn point
    const spawn = player.spawnPoint;
    // Clear spawn area to prevent getting stuck in rebuilt terrain
    this._clearSpawnArea(spawn.col, spawn.row);
    player.tank.x = MAP_OFFSET_X + spawn.col * TILE_SIZE + TILE_SIZE / 2;
    player.tank.y = MAP_OFFSET_Y + spawn.row * TILE_SIZE + TILE_SIZE / 2;
    player.tank.activateShield(3000);

    // Update remote target
    if (!player.isLocal) {
      this._remoteTargets[player.playerId] = {
        x: player.tank.x,
        y: player.tank.y,
        direction: player.tank.direction,
      };
    }
  },

  _updateExplosions(dt) {
    for (const exp of this._explosions) {
      exp.update(dt);
    }
  },

  _cleanup() {
    for (let i = this._bullets.length - 1; i >= 0; i--) {
      if (!this._bullets[i].alive) {
        this._bulletPool.put(this._bullets[i]);
        this._bullets.splice(i, 1);
      }
    }
    for (let i = this._explosions.length - 1; i >= 0; i--) {
      if (!this._explosions[i].alive) {
        this._explosionPool.put(this._explosions[i]);
        this._explosions.splice(i, 1);
      }
    }
  },

  _transitionToResult() {
    const sm = GameGlobal.sceneManager;
    if (!sm._scenes.has(SCENE.TEAM_RESULT)) {
      const TeamResultScene = require('./TeamResultScene');
      sm.register(SCENE.TEAM_RESULT, TeamResultScene);
    }

    const didWin = this._winner === this._myTeam;

    sm.switchTo(SCENE.TEAM_RESULT, {
      winner: this._winner,
      winReason: this._winReason,
      myTeam: this._myTeam,
      didWin,
      isDraw: false, // No draw in base-destruction mode
      teamABaseHp: this._teamABaseHp,
      teamBBaseHp: this._teamBBaseHp,
      stats: this._stats,
      players: this._players.map(p => ({
        playerId: p.playerId,
        nickname: p.nickname || '',
        team: p.team,
        isBot: p.isBot,
        isLocal: p.isLocal,
      })),
      elapsedTime: Math.floor(this._elapsedTime),
      teamId: this._teamId,
      battleMode: this._battleMode,
    });
  },

  // ============================================================
  // Render
  // ============================================================
  render(ctx) {
    if (!this._initialized) return;

    // Game area background
    ctx.fillStyle = '#111111';
    ctx.fillRect(MAP_OFFSET_X, MAP_OFFSET_Y, MAP_WIDTH, MAP_HEIGHT);

    // Render map
    this._mapManager.render(ctx);

    // Render all tanks
    for (const player of this._players) {
      if (player.tank.alive && !player.isRespawning) {
        player.tank.render(ctx);

        // Name & team indicator above the tank
        const tx = player.tank.x;
        const labelY = player.tank.y - player.tank.halfSize - 4;
        const nameY = labelY - 10;

        // Per-tank team color:
        // - local player   → gold
        // - Team A (ally or enemy) → blue
        // - Team B (ally or enemy) → red
        let labelColor;
        if (player.isLocal) labelColor = LOCAL_PLAYER_COLOR;
        else labelColor = player.team === 'A' ? TEAM_A_COLOR : TEAM_B_COLOR;

        ctx.fillStyle = labelColor;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';

        // Arrow / bot tag
        ctx.font = 'bold 8px Arial';
        let marker;
        if (player.isLocal) marker = '★';
        else if (player.isBot) marker = '🤖';
        else marker = (player.team === this._myTeam) ? '▲' : '▼';
        ctx.fillText(marker, tx, labelY);

        // Nickname (truncated to 4 Chinese-equivalent chars)
        const name = this._getTankLabel(player);
        if (name) {
          ctx.font = 'bold 9px Arial';
          // Outline for readability on busy backgrounds
          ctx.lineWidth = 3;
          ctx.strokeStyle = 'rgba(0,0,0,0.7)';
          ctx.strokeText(name, tx, nameY);
          ctx.fillText(name, tx, nameY);
        }
      }
    }

    // Render forest overlay
    this._mapManager.renderForestOverlay(ctx);

    // Render bullets
    for (const bullet of this._bullets) {
      bullet.render(ctx);
    }

    // Render explosions
    for (const exp of this._explosions) {
      exp.render(ctx);
    }

    // Render HUD
    this._renderHUD(ctx);

    // Render controls
    this._joystick.render(ctx);
    this._fireButton.render(ctx);

    // Respawn overlay
    if (this._localPlayer && this._localPlayer.isRespawning) {
      this._renderRespawnOverlay(ctx);
    }

    // Pause overlay
    if (this._paused) {
      this._renderPauseOverlay(ctx);
    }

    // Game over overlay
    if (this._gameOver) {
      this._renderGameOverOverlay(ctx);
    }

    // Disconnection overlay
    if (this._isDisconnected) {
      this._renderDisconnectOverlay(ctx);
    }
  },

  _getTeamStats(team) {
    let kills = 0, deaths = 0;
    for (const p of this._players) {
      if (p.team === team && this._stats[p.playerId]) {
        kills += this._stats[p.playerId].kills;
        deaths += this._stats[p.playerId].deaths;
      }
    }
    return { kills, deaths };
  },

  /**
   * Compute a short label (≤ 4 Chinese-equivalent chars) to draw above a tank.
   * Uses real WeChat nickname if available, otherwise a stable fallback.
   * @private
   */
  _getTankLabel(player) {
    if (!player) return '';
    const profile = GameGlobal.playerProfile;
    let raw = '';
    if (player.isLocal) {
      // For local player prefer the freshest profile nickname if granted.
      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 (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 = '';
      }
    }
    if (!raw) return '';
    if (profile && typeof profile.truncate === 'function') {
      return profile.truncate(raw, 4);
    }
    return raw.length > 8 ? raw.substring(0, 8) + '..' : raw;
  },

  _renderHUD(ctx) {
    const hudY = 4;

    // Team A base HP (left)
    const barWidth = 80;
    const barHeight = 12;
    const barY = hudY + 2;
    const timeGap = 30; // half-width reserved for the timer in the center

    // Team A label + HP bar
    const teamALabel = this._battleMode === '1v1' ? 'P1' : t('team.teamA');
    const teamBLabel = this._battleMode === '1v1' ? 'P2' : t('team.teamB');
    ctx.fillStyle = this._battleMode === '1v1' ? PLAYER1_COLOR : TEAM_A_COLOR;
    ctx.font = 'bold 10px Arial';
    ctx.textAlign = 'right';
    ctx.textBaseline = 'top';
    ctx.fillText(teamALabel, SCREEN_WIDTH / 2 - timeGap - barWidth - 5, barY);

    // Team A HP bar background
    const barAX = SCREEN_WIDTH / 2 - timeGap;
    ctx.fillStyle = '#333333';
    ctx.fillRect(barAX, barY, -barWidth, barHeight);

    // Team A HP bar fill
    const maxBaseHp = this._battleConfig ? this._battleConfig.baseHp : TEAM_BASE_HP;
    const hpRatioA = this._teamABaseHp / maxBaseHp;
    const teamADisplayColor = this._battleMode === '1v1' ? PLAYER1_COLOR : TEAM_A_COLOR;
    const teamBDisplayColor = this._battleMode === '1v1' ? PLAYER2_COLOR : TEAM_B_COLOR;
    ctx.fillStyle = hpRatioA > 0.3 ? teamADisplayColor : '#FF4444';
    ctx.fillRect(barAX, barY, -barWidth * hpRatioA, barHeight);

    // Team A HP text
    ctx.fillStyle = '#FFFFFF';
    ctx.font = 'bold 9px Arial';
    ctx.textAlign = 'center';
    ctx.fillText(`${this._teamABaseHp}`, barAX - barWidth / 2, barY + barHeight / 2 + 1);

    // Elapsed time (center, count up)
    const minutes = Math.floor(this._elapsedTime / 60);
    const seconds = Math.floor(this._elapsedTime % 60);
    const timeStr = `${minutes}:${seconds.toString().padStart(2, '0')}`;

    ctx.fillStyle = '#FFFFFF';
    ctx.font = 'bold 16px Arial';
    ctx.textAlign = 'center';
    ctx.fillText(timeStr, SCREEN_WIDTH / 2, hudY);

    // Team B label + HP bar
    const barBStart = SCREEN_WIDTH / 2 + timeGap;
    ctx.fillStyle = this._battleMode === '1v1' ? PLAYER2_COLOR : TEAM_B_COLOR;
    ctx.font = 'bold 10px Arial';
    ctx.textAlign = 'left';
    ctx.fillText(teamBLabel, barBStart + barWidth + 5, barY);

    // Team B HP bar background
    ctx.fillStyle = '#333333';
    ctx.fillRect(barBStart, barY, barWidth, barHeight);

    // Team B HP bar fill
    const hpRatioB = this._teamBBaseHp / maxBaseHp;
    ctx.fillStyle = hpRatioB > 0.3 ? teamBDisplayColor : '#FF4444';
    ctx.fillRect(barBStart, barY, barWidth * hpRatioB, barHeight);

    // Team B HP text
    ctx.fillStyle = '#FFFFFF';
    ctx.font = 'bold 9px Arial';
    ctx.textAlign = 'center';
    ctx.fillText(`${this._teamBBaseHp}`, barBStart + barWidth / 2, barY + barHeight / 2 + 1);

    // My team indicator
    if (this._battleMode === '1v1') {
      const mySlot = this._myTeam === 'A' ? 1 : 2;
      ctx.fillStyle = this._myTeam === 'A' ? PLAYER1_COLOR : PLAYER2_COLOR;
      ctx.font = '9px Arial';
      ctx.textAlign = 'center';
      ctx.fillText(t('pvp.playerLabel', { slot: mySlot }), SCREEN_WIDTH / 2, hudY + 20);
    } else {
      ctx.fillStyle = this._myTeam === 'A' ? TEAM_A_COLOR : TEAM_B_COLOR;
      ctx.font = '9px Arial';
      ctx.textAlign = 'center';
      ctx.fillText(t('team.myTeam', { team: this._myTeam }), SCREEN_WIDTH / 2, hudY + 20);
    }

    // Team kill/death totals
    const myTeamStats = this._getTeamStats(this._myTeam);
    const enemyTeam = this._myTeam === 'A' ? 'B' : 'A';
    const enemyTeamStats = this._getTeamStats(enemyTeam);

    ctx.fillStyle = '#AAAAAA';
    ctx.font = '10px Arial';
    ctx.textAlign = 'left';
    ctx.fillText(t('team.killDeath', { kills: myTeamStats.kills, deaths: myTeamStats.deaths }), 10, hudY);

    ctx.textAlign = 'right';
    ctx.fillText(t('team.killDeath', { kills: enemyTeamStats.kills, deaths: enemyTeamStats.deaths }), SCREEN_WIDTH - 10, hudY);

    // Latency
    if (this._networkManager) {
      ctx.fillStyle = '#666666';
      ctx.font = '9px Arial';
      ctx.textAlign = 'right';
      ctx.fillText(`${this._networkManager.latency || 0}ms`, SCREEN_WIDTH - 10, hudY + 14);
    }
  },

  _renderRespawnOverlay(ctx) {
    const remaining = Math.ceil(this._localPlayer.respawnTimer / 1000);
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.fillRect(SCREEN_WIDTH / 2 - 60, SCREEN_HEIGHT / 2 - 25, 120, 50);

    ctx.fillStyle = '#FFFFFF';
    ctx.font = 'bold 14px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(t('team.respawn', { seconds: remaining }), SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
  },

  _renderPauseOverlay(ctx) {
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

    ctx.fillStyle = '#FFFFFF';
    ctx.font = 'bold 28px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(t('common.paused'), SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 40);

    ctx.font = '16px Arial';
    ctx.fillText(t('common.tapContinue'), SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 10);
  },

  _renderGameOverOverlay(ctx) {
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

    let text, color;
    if (this._winner === this._myTeam) {
      text = t('team.victory');
      color = '#00FF00';
    } else {
      text = t('team.defeat');
      color = '#FF0000';
    }

    ctx.fillStyle = color;
    ctx.font = 'bold 36px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(text, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 10);

    // Base HP summary
    ctx.fillStyle = '#FFFFFF';
    ctx.font = '14px Arial';
    if (this._battleMode === '1v1') {
      ctx.fillText(
        t('pvp.baseHpSummary', { hp1: this._teamABaseHp, hp2: this._teamBBaseHp }),
        SCREEN_WIDTH / 2,
        SCREEN_HEIGHT / 2 + 25
      );
    } else {
      ctx.fillText(
        t('team.baseHpSummary', { hpA: this._teamABaseHp, hpB: this._teamBBaseHp }),
        SCREEN_WIDTH / 2,
        SCREEN_HEIGHT / 2 + 25
      );
    }
  },

  _renderDisconnectOverlay(ctx) {
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

    ctx.fillStyle = '#FF6347';
    ctx.font = 'bold 20px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(t('team.disconnectTitle'), SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 30);

    const dots = '.'.repeat(Math.floor(this._reconnectTimer * 3) % 4);
    ctx.fillStyle = '#FFFFFF';
    ctx.font = '14px Arial';
    ctx.fillText(
      t('team.reconnecting', { dots, attempts: this._reconnectAttempts, max: this._maxReconnectAttempts }),
      SCREEN_WIDTH / 2,
      SCREEN_HEIGHT / 2 + 5
    );

    ctx.fillStyle = '#AAAAAA';
    ctx.font = '12px Arial';
    ctx.fillText(
      t('team.reconnectHint'),
      SCREEN_WIDTH / 2,
      SCREEN_HEIGHT / 2 + 30
    );
  },

  async _attemptReconnect() {
    this._reconnectAttempts++;
    console.log(`[TeamGameScene] Reconnect attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts}`);

    if (this._reconnectAttempts > this._maxReconnectAttempts) {
      // Give up reconnecting — treat as defeat
      this._isDisconnected = false;
      this._winner = this._myTeam === 'A' ? 'B' : 'A';
      this._winReason = 'disconnected';
      this._gameOver = true;
      return;
    }

    const nm = this._networkManager;
    if (!nm) return;

    try {
      // Try to reconnect to server
      const { SERVER_URL } = require('../base/GameGlobal');
      const ok = await nm.connect(SERVER_URL);
      if (ok) {
        // Send reconnect message
        nm.send(NET_MSG.RECONNECT, {
          teamId: this._teamId,
          playerId: this._myPlayerId,
        });
      }
    } catch (e) {
      console.error('[TeamGameScene] Reconnect failed:', e);
    }
  },

  // ============================================================
  // Touch Handling
  // ============================================================
  handleTouch(eventType, e) {
    if (this._gameOver) return;

    if (this._paused) {
      if (eventType === 'touchstart') {
        this._paused = false;
      }
      return;
    }

    const touches = eventType === 'touchend' ? e.changedTouches : e.touches;
    for (let i = 0; i < touches.length; i++) {
      const touch = touches[i];
      if (this._joystick.handleTouch(eventType, touch)) continue;
      if (this._fireButton.handleTouch(eventType, touch)) continue;

      // Pause button area
      if (eventType === 'touchstart') {
        if (touch.clientX > SCREEN_WIDTH - 50 && touch.clientY < MAP_OFFSET_Y) {
          this._paused = true;
        }
      }
    }
  },
};

module.exports = TeamGameScene;
