189 lines
4.8 KiB
JavaScript
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;
|