/** * GameScene.js * Main battle scene - orchestrates map, tanks, bullets, power-ups, and HUD. * This is the core gameplay scene that integrates all sub-systems. */ const { SCREEN_WIDTH, SCREEN_HEIGHT, COLORS, SCENE, GAME_MODE, MAP_OFFSET_X, MAP_OFFSET_Y, MAP_WIDTH, MAP_HEIGHT, TILE_SIZE, GRID_COLS, GRID_ROWS, DIRECTION, DIR_VECTORS, BULLET_SPEED, FIRE_LEVEL, POWERUP_TYPE, FREEZE_DURATION, SHIELD_DURATION, SHOVEL_DURATION, TANK_TYPE, TERRAIN, } = require('../base/GameGlobal'); const ObjectPool = require('../base/ObjectPool'); const MapManager = require('../managers/MapManager'); const CollisionManager = require('../managers/CollisionManager'); const SpawnManager = require('../managers/SpawnManager'); const PlayerTank = require('../entities/PlayerTank'); const Bullet = require('../entities/Bullet'); const Explosion = require('../entities/Explosion'); const PowerUp = require('../entities/PowerUp'); const Joystick = require('../ui/Joystick'); const FireButton = require('../ui/FireButton'); const { getLevelData } = require('../data/LevelData'); const { t } = require('../i18n/I18n'); const GameScene = { _mode: GAME_MODE.CLASSIC, _level: 1, _initialized: false, _gameOver: false, _victory: false, _paused: false, // Sub-systems _mapManager: null, _collisionManager: null, _spawnManager: null, _playerTank: null, _joystick: null, _fireButton: null, // Entity lists _enemies: [], _bullets: [], _explosions: [], _powerUps: [], // Object pools _bulletPool: null, _explosionPool: null, // Game stats _stats: null, _levelStartTime: 0, _freezeTimer: 0, // Game over delay _gameOverDelay: 0, _gameOverDelayDuration: 2, // seconds // Revive ad state _reviveAdUsed: false, _reviveCount: 0, // Track revive count for escalating cost _showingReviveDialog: false, _reviveDialogButtons: null, // Buff manager reference _buffManager: null, enter(params) { this._mode = (params && params.mode) || GAME_MODE.CLASSIC; this._level = (params && params.level) || 1; this._gameOver = false; this._victory = false; this._paused = false; this._freezeTimer = 0; this._gameOverDelay = 0; this._cachedBasePos = null; this._reviveAdUsed = false; this._reviveCount = 0; this._showingReviveDialog = false; this._reviveDialogButtons = null; // Initialize buff manager reference this._buffManager = GameGlobal.buffManager || null; // Initialize stats this._stats = { kills: { normal: 0, fast: 0, armor: 0, boss: 0 }, totalKills: 0, score: 0, timeElapsed: 0, baseAlive: true, }; // Initialize object pools this._bulletPool = new ObjectPool(() => new Bullet(), null, 20); this._explosionPool = new ObjectPool(() => new Explosion(), null, 10); // Initialize entity lists this._enemies = []; this._bullets = []; this._explosions = []; this._powerUps = []; // Initialize map this._mapManager = new MapManager(); const levelData = getLevelData(this._level); this._mapManager.loadGrid(levelData.grid); // Initialize spawn manager this._spawnManager = new SpawnManager(); this._spawnManager.init(levelData); // Initialize player this._playerTank = new PlayerTank({ col: levelData.playerSpawn.col, row: levelData.playerSpawn.row, }); this._playerTank.activateShield(3000); // spawn protection // Apply equipped skin colors to player tank if (GameGlobal.skinManager) { this._playerTank._skinColors = GameGlobal.skinManager.getCurrentSkinColors(); this._playerTank._skinId = GameGlobal.skinManager.getEquippedSkinId(); } // Safety: ensure player spawn area is clear of blocking terrain this._clearSpawnArea(levelData.playerSpawn.col, levelData.playerSpawn.row); // Initialize collision manager this._collisionManager = new CollisionManager({ mapManager: this._mapManager, onExplosion: (x, y, isBig) => this._spawnExplosion(x, y, isBig), eventBus: GameGlobal.eventBus, }); // Initialize controls this._joystick = new Joystick(); this._fireButton = new FireButton(); this._fireButton.onFire(() => this._playerFire()); // Event listeners this._setupEvents(); this._levelStartTime = Date.now(); this._initialized = true; // Activate pre-game buffs if any were purchased if (this._buffManager) { this._buffManager.activateBuffs(this._playerTank); } // Preload rewarded video ad for revive/double reward scenes if (GameGlobal.adManager) { GameGlobal.adManager.preloadRewardedVideo(); } console.log(`[GameScene] Level ${this._level} started. Mode: ${this._mode}`); }, exit() { this._initialized = false; this._cleanupEvents(); this._bullets = []; this._enemies = []; this._explosions = []; this._powerUps = []; // Clear buffs at end of round if (this._buffManager) { this._buffManager.clearBuffs(); } }, _setupEvents() { const eb = GameGlobal.eventBus; this._onEnemyDestroyed = (data) => this._handleEnemyDestroyed(data); this._onPlayerDestroyed = () => this._handlePlayerDestroyed(); this._onBaseDestroyed = () => this._handleBaseDestroyed(); eb.on('enemy:destroyed', this._onEnemyDestroyed); eb.on('player:destroyed', this._onPlayerDestroyed); eb.on('base:destroyed', this._onBaseDestroyed); }, _cleanupEvents() { const eb = GameGlobal.eventBus; eb.off('enemy:destroyed', this._onEnemyDestroyed); eb.off('player:destroyed', this._onPlayerDestroyed); eb.off('base:destroyed', this._onBaseDestroyed); }, // ============================================================ // Update // ============================================================ update(dt) { if (!this._initialized || this._paused) return; // Game over delay (show explosion before transitioning) if (this._gameOver || this._victory) { this._gameOverDelay += dt; // Still update explosions during delay this._updateExplosions(dt); if (this._gameOverDelay >= this._gameOverDelayDuration) { this._transitionToResult(); } return; } this._stats.timeElapsed += dt; // Update map (shovel timer etc.) this._mapManager.update(dt); // Update freeze timer if (this._freezeTimer > 0) { this._freezeTimer -= dt * 1000; if (this._freezeTimer < 0) this._freezeTimer = 0; } // Player movement if (this._playerTank.alive && this._joystick.active && this._joystick.direction >= 0) { this._playerTank.move(this._joystick.direction, dt, this._mapManager); } this._playerTank.update(dt); // Update buff timers if (this._buffManager) { this._buffManager.update(dt, this._playerTank); } // Spawn enemies (pause spawning while freeze is active) if (this._freezeTimer <= 0) { const newEnemy = this._spawnManager.update(dt, this._enemies); if (newEnemy) { this._enemies.push(newEnemy); } } // Update enemies — find base position from the grid const basePos = this._findBasePos(); for (const enemy of this._enemies) { if (this._freezeTimer > 0) { enemy.frozen = true; } else { enemy.frozen = false; } enemy.update(dt, this._mapManager, basePos, (tank) => this._enemyFire(tank)); } // Update bullets (freeze enemy bullets while freeze is active) for (const bullet of this._bullets) { if (this._freezeTimer > 0 && bullet.owner === 'enemy') { continue; // enemy bullets are frozen } bullet.update(dt); } // Update power-ups for (const pu of this._powerUps) { pu.update(dt); } // Collision detection this._collisionManager.update({ player: this._playerTank, enemies: this._enemies, bullets: this._bullets, }); // Check power-up pickup this._checkPowerUpPickup(); // Update explosions this._updateExplosions(dt); // Cleanup dead entities this._cleanup(); // Check victory condition this._checkVictory(); }, // ============================================================ // Render // ============================================================ render(ctx) { if (!this._initialized) return; // Draw game area background ctx.fillStyle = '#111111'; ctx.fillRect(MAP_OFFSET_X, MAP_OFFSET_Y, MAP_WIDTH, MAP_HEIGHT); // Render map (terrain layer) this._mapManager.render(ctx); // Render power-ups for (const pu of this._powerUps) { pu.render(ctx); } // Render player this._playerTank.render(ctx); // Render enemies for (const enemy of this._enemies) { enemy.render(ctx); } // Render forest overlay (on top of tanks) 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); // Render pause overlay if (this._paused && !this._showingReviveDialog) { this._renderPauseOverlay(ctx); } // Render revive ad dialog if (this._showingReviveDialog) { this._renderReviveDialog(ctx); } // Game over text if (this._gameOver) { this._renderGameOverText(ctx, t('game.gameOver')); } else if (this._victory) { this._renderGameOverText(ctx, t('game.stageClear')); } }, // ============================================================ // HUD Rendering // ============================================================ _renderHUD(ctx) { // In landscape mode, HUD is rendered on the sides of the map const leftX = MAP_OFFSET_X - 8; // right edge of left panel const rightX = MAP_OFFSET_X + MAP_WIDTH + 8; // left edge of right panel const topY = MAP_OFFSET_Y + 10; // === Left side panel === ctx.fillStyle = COLORS.HUD_TEXT; ctx.font = 'bold 12px Arial'; ctx.textAlign = 'right'; ctx.textBaseline = 'top'; // Level info ctx.fillText(t('game.level', { level: this._level }), leftX, topY); // Player lives ctx.fillStyle = '#FFD700'; ctx.font = '11px Arial'; ctx.fillText(t('game.hp', { count: this._playerTank.lives }), leftX, topY + 20); // Fire level ctx.fillText(t('game.fireLevel', { level: this._playerTank.fireLevel }), leftX, topY + 38); // === Right side panel === ctx.textAlign = 'left'; ctx.textBaseline = 'top'; // Remaining enemies ctx.fillStyle = COLORS.HUD_TEXT; ctx.font = 'bold 12px Arial'; const aliveEnemies = this._enemies.filter((e) => e.alive).length; const remaining = this._spawnManager.remainingToSpawn + aliveEnemies; ctx.fillText(t('game.enemies', { count: remaining }), rightX, topY); // Score ctx.fillStyle = '#FFD700'; ctx.font = '11px Arial'; ctx.fillText(t('game.score', { score: this._stats.score }), rightX, topY + 20); }, _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); }, _renderGameOverText(ctx, text) { ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); ctx.fillStyle = text === t('game.gameOver') ? '#FF0000' : '#00FF00'; ctx.font = 'bold 32px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(text, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2); }, /** * Render the revive dialog overlay with dual options (ad + gold). * @private */ _renderReviveDialog(ctx) { const cx = SCREEN_WIDTH / 2; const cy = SCREEN_HEIGHT / 2; // Dim background ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); // Dialog box const boxW = 300; const boxH = 180; ctx.fillStyle = 'rgba(30,30,30,0.95)'; ctx.strokeStyle = '#FFD700'; ctx.lineWidth = 2; ctx.fillRect(cx - boxW / 2, cy - boxH / 2, boxW, boxH); ctx.strokeRect(cx - boxW / 2, cy - boxH / 2, boxW, boxH); // Title text ctx.fillStyle = '#FFFFFF'; ctx.font = 'bold 18px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(t('ad.reviveTitle') || 'Revive Chance', cx, cy - 55); // Subtitle ctx.fillStyle = '#AAAAAA'; ctx.font = '12px Arial'; ctx.fillText(t('ad.reviveDesc') || 'Choose how to revive and continue', cx, cy - 35); const btns = this._reviveDialogButtons; if (btns) { // Watch Ad button (green) - only show if ad is available if (btns.watchAd) { ctx.fillStyle = '#4CAF50'; ctx.fillRect(btns.watchAd.x, btns.watchAd.y, btns.watchAd.w, btns.watchAd.h); ctx.fillStyle = '#FFFFFF'; ctx.font = 'bold 13px Arial'; ctx.fillText(t('ad.watchAd') || '📺 Watch Ad (Free)', btns.watchAd.x + btns.watchAd.w / 2, btns.watchAd.y + btns.watchAd.h / 2); } // Gold Revive button (orange) - show escalating cost if (btns.goldRevive) { const reviveCost = this._getReviveCost(); const hasGold = GameGlobal.currencyManager && GameGlobal.currencyManager.hasGold(reviveCost); ctx.fillStyle = hasGold ? '#FF9800' : '#555555'; ctx.fillRect(btns.goldRevive.x, btns.goldRevive.y, btns.goldRevive.w, btns.goldRevive.h); ctx.fillStyle = '#FFFFFF'; ctx.font = 'bold 13px Arial'; ctx.fillText(`🪙 ${t('ad.goldRevive') || 'Gold Revive'} (${reviveCost})`, btns.goldRevive.x + btns.goldRevive.w / 2, btns.goldRevive.y + btns.goldRevive.h / 2); } // Give Up button (gray) ctx.fillStyle = '#666666'; ctx.fillRect(btns.giveUp.x, btns.giveUp.y, btns.giveUp.w, btns.giveUp.h); ctx.fillStyle = '#FFFFFF'; ctx.font = 'bold 13px Arial'; ctx.fillText(t('ad.giveUp') || 'Give Up', btns.giveUp.x + btns.giveUp.w / 2, btns.giveUp.y + btns.giveUp.h / 2); } }, // ============================================================ // Game Logic // ============================================================ _playerFire() { if (!this._playerTank.alive || !this._playerTank.canFire()) return; if (this._gameOver || this._victory || this._paused) return; const tank = this._playerTank; 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: 'player', canBreakSteel: tank.canBreakSteel(), ownerTank: tank, }); tank.activeBullets++; this._bullets.push(bullet); GameGlobal.audioManager.playSFX('shoot'); }, _enemyFire(enemyTank) { if (!enemyTank.alive || !enemyTank.canFire()) return; const vec = DIR_VECTORS[enemyTank.direction]; const bullet = this._bulletPool.get(); bullet.init({ x: enemyTank.x + vec.dx * enemyTank.halfSize, y: enemyTank.y + vec.dy * enemyTank.halfSize, direction: enemyTank.direction, owner: 'enemy', canBreakSteel: false, ownerTank: enemyTank, }); enemyTank.activeBullets++; this._bullets.push(bullet); GameGlobal.audioManager.playSFX('shoot'); }, _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'); }, /** * Clear any blocking terrain at the player spawn area. * Ensures the tank won't be stuck inside walls on spawn. * @private */ _clearSpawnArea(col, row) { const terrain = this._mapManager.getTerrain(row, col); if (terrain !== TERRAIN.EMPTY && terrain !== TERRAIN.FOREST) { this._mapManager.setTerrain(row, col, TERRAIN.EMPTY); } }, /** * Find the base (eagle) position from the map grid. * Scans for TERRAIN.BASE and returns its pixel center. * Result is cached after first call per level. * @private * @returns {{x: number, y: number}} */ _findBasePos() { if (this._cachedBasePos) return this._cachedBasePos; // Scan grid for BASE terrain for (let r = GRID_ROWS - 1; r >= 0; r--) { for (let c = 0; c < GRID_COLS; c++) { if (this._mapManager.getTerrain(r, c) === TERRAIN.BASE) { this._cachedBasePos = { x: MAP_OFFSET_X + c * TILE_SIZE + TILE_SIZE / 2, y: MAP_OFFSET_Y + r * TILE_SIZE + TILE_SIZE / 2, }; return this._cachedBasePos; } } } // Fallback: center-bottom this._cachedBasePos = { x: MAP_OFFSET_X + Math.floor(GRID_COLS / 2) * TILE_SIZE + TILE_SIZE / 2, y: MAP_OFFSET_Y + (GRID_ROWS - 1) * TILE_SIZE + TILE_SIZE / 2, }; return this._cachedBasePos; }, _updateExplosions(dt) { for (const exp of this._explosions) { exp.update(dt); } }, _checkPowerUpPickup() { if (!this._playerTank.alive) return; const pb = this._playerTank.getBounds(); for (const pu of this._powerUps) { if (!pu.alive) continue; const pub = pu.getBounds(); if ( pb.x < pub.x + pub.w && pb.x + pb.w > pub.x && pb.y < pub.y + pub.h && pb.y + pb.h > pub.y ) { this._applyPowerUp(pu); pu.alive = false; GameGlobal.audioManager.playSFX('powerup'); } } }, _applyPowerUp(powerUp) { switch (powerUp.type) { case POWERUP_TYPE.STAR: this._playerTank.upgradeFireLevel(); break; case POWERUP_TYPE.CLOCK: this._freezeTimer = FREEZE_DURATION; break; case POWERUP_TYPE.BOMB: // Destroy all on-screen enemies for (const enemy of this._enemies) { if (enemy.alive) { enemy.alive = false; this._spawnExplosion(enemy.x, enemy.y, true); this._recordKill(enemy); } } break; case POWERUP_TYPE.HELMET: this._playerTank.activateShield(SHIELD_DURATION); break; case POWERUP_TYPE.SHOVEL: this._mapManager.activateShovel(SHOVEL_DURATION); break; case POWERUP_TYPE.TANK: this._playerTank.addLife(); break; } }, _handleEnemyDestroyed(data) { const enemy = data.enemy; this._recordKill(enemy); // Spawn power-up if this enemy was marked if (enemy.hasPowerUp) { const type = PowerUp.randomType(this._level); const pos = PowerUp.randomPosition(this._mapManager); const pu = new PowerUp(type, pos.x, pos.y); this._powerUps.push(pu); } }, _recordKill(enemy) { this._stats.totalKills++; this._stats.score += enemy.score || 100; switch (enemy.type) { case TANK_TYPE.ENEMY_NORMAL: this._stats.kills.normal++; break; case TANK_TYPE.ENEMY_FAST: this._stats.kills.fast++; break; case TANK_TYPE.ENEMY_ARMOR: this._stats.kills.armor++; break; case TANK_TYPE.ENEMY_BOSS: this._stats.kills.boss++; break; } }, _handlePlayerDestroyed() { // Check if buff shield can absorb the hit if (this._buffManager && this._buffManager.consumeShield(this._playerTank)) { // Shield absorbed the damage, player survives this._playerTank.hp = 1; this._playerTank.alive = true; return; } const hasLives = this._playerTank.die(); if (!hasLives) { // Always show revive dialog (escalating cost each time) this._showReviveAdDialog(); } }, /** * Check if revive ad can be shown. * @returns {boolean} * @private */ _canShowReviveAd() { if (!GameGlobal.adManager) return false; const AdManager = require('../managers/AdManager'); return GameGlobal.adManager.canShowScene(AdManager.AD_SCENE.REVIVE); }, /** * Get the current revive gold cost based on revive count (escalating). * 1st revive: 200, 2nd: 400, 3rd+: 800 * @returns {number} * @private */ _getReviveCost() { if (this._reviveCount === 0) return 200; if (this._reviveCount === 1) return 400; return 800; }, /** * Show the revive dialog overlay with dual options. * Pauses the game and presents watch-ad / gold-revive / give-up options. * @private */ _showReviveAdDialog() { this._showingReviveDialog = true; this._paused = true; const cx = SCREEN_WIDTH / 2; const cy = SCREEN_HEIGHT / 2; const btnW = 220; const btnH = 36; // Check if ad is available const canShowAd = this._canShowReviveAd(); const buttons = { giveUp: { x: cx - btnW / 2, y: cy + 50, w: btnW, h: btnH }, }; if (canShowAd) { buttons.watchAd = { x: cx - btnW / 2, y: cy - 20, w: btnW, h: btnH }; buttons.goldRevive = { x: cx - btnW / 2, y: cy + 15, w: btnW, h: btnH }; buttons.giveUp = { x: cx - btnW / 2, y: cy + 50, w: btnW, h: btnH }; } else { // No ad available, only show gold revive and give up buttons.goldRevive = { x: cx - btnW / 2, y: cy - 5, w: btnW, h: btnH }; buttons.giveUp = { x: cx - btnW / 2, y: cy + 35, w: btnW, h: btnH }; } this._reviveDialogButtons = buttons; }, /** * Handle the player choosing to revive with gold (escalating cost). * @private */ _onGoldRevive() { const cost = this._getReviveCost(); const cm = GameGlobal.currencyManager; if (cm && cm.spendGold(cost)) { this._showingReviveDialog = false; this._reviveDialogButtons = null; this._reviveCount++; this._revivePlayer(); console.log(`[GameScene] Player revived via gold (${cost}), revive #${this._reviveCount}`); } }, /** * Handle the player choosing to watch the revive ad. * @private */ _onReviveAdWatch() { const AdManager = require('../managers/AdManager'); GameGlobal.adManager.showRewardedVideoForScene( AdManager.AD_SCENE.REVIVE, (completed) => { this._showingReviveDialog = false; this._reviveDialogButtons = null; if (completed) { this._reviveCount++; this._revivePlayer(); } else { this._triggerGameOver(); } } ); }, /** * Handle the player choosing to give up (skip revive ad). * @private */ _onReviveAdGiveUp() { this._showingReviveDialog = false; this._reviveDialogButtons = null; this._triggerGameOver(); }, /** * Revive the player: restore 1 life, keep fire level, respawn at start. * @private */ _revivePlayer() { this._paused = false; this._playerTank.addLife(); // Respawn: reset position, alive=true, hp=1, shield protection this._playerTank.respawn(); console.log('[GameScene] Player revived'); }, /** * Trigger the game over state. * @private */ _triggerGameOver() { this._paused = false; this._gameOver = true; this._stats.baseAlive = !this._mapManager.baseDestroyed; GameGlobal.audioManager.playSFX('gameover'); }, _handleBaseDestroyed() { this._gameOver = true; this._stats.baseAlive = false; GameGlobal.audioManager.playSFX('gameover'); }, _checkVictory() { if (this._gameOver || this._victory) return; const allSpawned = this._spawnManager.allSpawned; const allDead = this._enemies.every((e) => !e.alive); if (allSpawned && allDead) { this._victory = true; this._stats.baseAlive = !this._mapManager.baseDestroyed; GameGlobal.audioManager.playSFX('victory'); // Time bonus const timeBonus = Math.max(0, 300 - Math.floor(this._stats.timeElapsed)) * 10; this._stats.score += timeBonus; // Base alive bonus if (this._stats.baseAlive) { this._stats.score += 1000; } } }, _cleanup() { // Recycle dead bullets 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); } } // Recycle dead explosions 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); } } // Remove dead power-ups this._powerUps = this._powerUps.filter((pu) => pu.alive); // Remove dead enemies (keep for counting) // Don't remove - they're needed for allDead check }, _transitionToResult() { const sm = GameGlobal.sceneManager; if (!sm._scenes.has(SCENE.RESULT)) { const ResultScene = require('./ResultScene'); sm.register(SCENE.RESULT, ResultScene); } sm.switchTo(SCENE.RESULT, { level: this._level, mode: this._mode, victory: this._victory, stats: this._stats, }); }, // ============================================================ // Touch Handling // ============================================================ handleTouch(eventType, e) { if (this._gameOver || this._victory) return; // Handle revive dialog touches if (this._showingReviveDialog && eventType === 'touchstart') { const touches = e.touches; for (let i = 0; i < touches.length; i++) { const tx = touches[i].clientX; const ty = touches[i].clientY; const btns = this._reviveDialogButtons; if (btns) { // Watch Ad button if (btns.watchAd && tx >= btns.watchAd.x && tx <= btns.watchAd.x + btns.watchAd.w && ty >= btns.watchAd.y && ty <= btns.watchAd.y + btns.watchAd.h) { this._onReviveAdWatch(); return; } // Gold Revive button if (btns.goldRevive && tx >= btns.goldRevive.x && tx <= btns.goldRevive.x + btns.goldRevive.w && ty >= btns.goldRevive.y && ty <= btns.goldRevive.y + btns.goldRevive.h) { this._onGoldRevive(); return; } // Give Up button if (tx >= btns.giveUp.x && tx <= btns.giveUp.x + btns.giveUp.w && ty >= btns.giveUp.y && ty <= btns.giveUp.y + btns.giveUp.h) { this._onReviveAdGiveUp(); return; } } } return; } // Handle pause toggle if (this._paused) { if (eventType === 'touchstart') { this._paused = false; } return; } // Distribute touches to controls const touches = eventType === 'touchend' ? e.changedTouches : e.touches; for (let i = 0; i < touches.length; i++) { const touch = touches[i]; // Try joystick first if (this._joystick.handleTouch(eventType, touch)) continue; // Then fire button if (this._fireButton.handleTouch(eventType, touch)) continue; // Pause button area (top-right corner) if (eventType === 'touchstart') { if (touch.clientX > SCREEN_WIDTH - 50 && touch.clientY < MAP_OFFSET_Y) { this._paused = true; } } } }, }; module.exports = GameScene;