first commit

This commit is contained in:
jakciehan
2026-04-10 22:59:39 +08:00
commit cc2e7b9bb0
89 changed files with 23631 additions and 0 deletions
+939
View File
@@ -0,0 +1,939 @@
/**
* 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,
_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._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
// 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)
if (btns.goldRevive) {
const hasGold = GameGlobal.currencyManager && GameGlobal.currencyManager.hasGold(200);
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 (200)', 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) {
// Check if revive ad is available and not yet used this level
if (!this._reviveAdUsed) {
// Always show revive dialog (with ad and/or gold options)
this._showReviveAdDialog();
} else {
this._triggerGameOver();
}
}
},
/**
* 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);
},
/**
* 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 (200 gold).
* @private
*/
_onGoldRevive() {
const cm = GameGlobal.currencyManager;
if (cm && cm.spendGold(200)) {
this._showingReviveDialog = false;
this._reviveDialogButtons = null;
this._reviveAdUsed = true;
this._revivePlayer();
console.log('[GameScene] Player revived via gold (200)');
}
},
/**
* 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._reviveAdUsed = true;
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;