Files
2026-04-10 22:59:39 +08:00

189 lines
4.8 KiB
JavaScript

/**
* 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;