/** * PowerUp.js * Power-up item entity with type, position, timer, and blink animation. */ const { POWERUP_TYPE, POWERUP_DURATION, TILE_SIZE, MAP_OFFSET_X, MAP_OFFSET_Y, MAP_WIDTH, MAP_HEIGHT, GRID_COLS, GRID_ROWS, } = require('../base/GameGlobal'); /** Power-up visual config */ const POWERUP_VISUALS = { [POWERUP_TYPE.STAR]: { emoji: '⭐', color: '#FFD700', label: 'STAR' }, [POWERUP_TYPE.CLOCK]: { emoji: '🕒', color: '#87CEEB', label: 'CLOCK' }, [POWERUP_TYPE.BOMB]: { emoji: '💣', color: '#FF4500', label: 'BOMB' }, [POWERUP_TYPE.HELMET]: { emoji: '🛡️', color: '#00CED1', label: 'SHIELD' }, [POWERUP_TYPE.SHOVEL]: { emoji: '🏠', color: '#8B4513', label: 'SHOVEL' }, [POWERUP_TYPE.TANK]: { emoji: '+1', color: '#32CD32', label: 'LIFE' }, }; class PowerUp { /** * @param {string} type - POWERUP_TYPE value. * @param {number} x - Pixel X. * @param {number} y - Pixel Y. */ constructor(type, x, y) { this.type = type; this.x = x; this.y = y; this.alive = true; this.size = TILE_SIZE * 0.9; this.halfSize = this.size / 2; this._timer = 0; this._duration = POWERUP_DURATION; this._blinkStart = POWERUP_DURATION * 0.7; // start blinking at 70% of duration this._visible = true; this._blinkTimer = 0; } /** * Update power-up timer and blink. * @param {number} dt - Delta time in seconds. */ update(dt) { if (!this.alive) return; this._timer += dt * 1000; // Blink when about to expire if (this._timer >= this._blinkStart) { this._blinkTimer += dt * 1000; if (this._blinkTimer >= 150) { this._blinkTimer = 0; this._visible = !this._visible; } } // Expire if (this._timer >= this._duration) { this.alive = false; } } /** * Render the power-up. * @param {CanvasRenderingContext2D} ctx */ render(ctx) { if (!this.alive || !this._visible) return; const visual = POWERUP_VISUALS[this.type]; const x = this.x - this.halfSize; const y = this.y - this.halfSize; // Background glow ctx.save(); ctx.globalAlpha = 0.3; ctx.fillStyle = visual.color; ctx.beginPath(); ctx.arc(this.x, this.y, this.halfSize + 4, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // Background box ctx.fillStyle = '#000000'; ctx.globalAlpha = 0.7; ctx.fillRect(x, y, this.size, this.size); ctx.globalAlpha = 1; // Border ctx.strokeStyle = visual.color; ctx.lineWidth = 2; ctx.strokeRect(x, y, this.size, this.size); // Icon/text ctx.fillStyle = visual.color; ctx.font = `bold ${Math.floor(this.size * 0.5)}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; if (this.type === POWERUP_TYPE.TANK) { ctx.fillText('+1', this.x, this.y); } else { ctx.font = `${Math.floor(this.size * 0.6)}px Arial`; ctx.fillText(visual.emoji, this.x, this.y); } } /** * Get bounding box for collision. * @returns {{x: number, y: number, w: number, h: number}} */ getBounds() { return { x: this.x - this.halfSize, y: this.y - this.halfSize, w: this.size, h: this.size, }; } /** * Generate a random position on the map (avoiding terrain). * @param {MapManager} mapManager * @returns {{x: number, y: number}} */ static randomPosition(mapManager) { let attempts = 0; while (attempts < 50) { const col = Math.floor(Math.random() * GRID_COLS); const row = Math.floor(Math.random() * (GRID_ROWS - 2)) + 1; // avoid top/bottom rows const terrain = mapManager.getTerrain(row, col); // Only place on empty tiles if (terrain === 0) { // TERRAIN.EMPTY return { x: MAP_OFFSET_X + col * TILE_SIZE + TILE_SIZE / 2, y: MAP_OFFSET_Y + row * TILE_SIZE + TILE_SIZE / 2, }; } attempts++; } // Fallback: center of map return { x: MAP_OFFSET_X + MAP_WIDTH / 2, y: MAP_OFFSET_Y + MAP_HEIGHT / 2, }; } /** * Get a random power-up type based on level-adjusted probabilities. * @param {number} levelNum - Current level number. * @returns {string} POWERUP_TYPE value. */ static randomType(levelNum) { // Base probabilities (weights) const weights = { [POWERUP_TYPE.STAR]: Math.max(10, 30 - levelNum), // decreases with level [POWERUP_TYPE.CLOCK]: 15, [POWERUP_TYPE.BOMB]: 10, [POWERUP_TYPE.HELMET]: 15, [POWERUP_TYPE.SHOVEL]: 10, [POWERUP_TYPE.TANK]: 10, }; const totalWeight = Object.values(weights).reduce((a, b) => a + b, 0); let rand = Math.random() * totalWeight; for (const [type, weight] of Object.entries(weights)) { rand -= weight; if (rand <= 0) return type; } return POWERUP_TYPE.STAR; // fallback } } module.exports = PowerUp;