211 lines
4.6 KiB
JavaScript
211 lines
4.6 KiB
JavaScript
/**
|
|
* PlayerTank.js
|
|
* Player-controlled tank with fire level, lives, shield, and respawn logic.
|
|
*/
|
|
|
|
const Tank = require('./Tank');
|
|
const {
|
|
TANK_TYPE,
|
|
TANK_CONFIG,
|
|
FIRE_LEVEL,
|
|
MAX_BULLETS_BY_LEVEL,
|
|
DIRECTION,
|
|
DEFAULT_LIVES,
|
|
SHIELD_DURATION,
|
|
INVINCIBLE_BLINK_INTERVAL,
|
|
TILE_SIZE,
|
|
MAP_OFFSET_X,
|
|
MAP_OFFSET_Y,
|
|
} = require('../base/GameGlobal');
|
|
|
|
class PlayerTank extends Tank {
|
|
/**
|
|
* @param {object} params
|
|
* @param {number} params.col - Spawn grid column.
|
|
* @param {number} params.row - Spawn grid row.
|
|
*/
|
|
constructor(params) {
|
|
const cfg = TANK_CONFIG[TANK_TYPE.PLAYER];
|
|
const spawnX = MAP_OFFSET_X + params.col * TILE_SIZE + TILE_SIZE / 2;
|
|
const spawnY = MAP_OFFSET_Y + params.row * TILE_SIZE + TILE_SIZE / 2;
|
|
|
|
super({
|
|
x: spawnX,
|
|
y: spawnY,
|
|
speed: cfg.speed,
|
|
hp: cfg.hp,
|
|
color: cfg.color,
|
|
size: cfg.size,
|
|
direction: DIRECTION.UP,
|
|
});
|
|
|
|
this.type = TANK_TYPE.PLAYER;
|
|
this.spawnCol = params.col;
|
|
this.spawnRow = params.row;
|
|
|
|
// Skin colors (reserved for future use)
|
|
this._skinColors = null;
|
|
this._skinId = 'default';
|
|
|
|
// Fire level system
|
|
this.fireLevel = FIRE_LEVEL.LV1;
|
|
|
|
// Lives
|
|
this.lives = DEFAULT_LIVES;
|
|
|
|
// Shield (invincibility)
|
|
this._shieldTimer = 0; // ms remaining
|
|
this._shieldBlink = false;
|
|
this._blinkTimer = 0;
|
|
|
|
// Active bullets count (managed externally)
|
|
this.activeBullets = 0;
|
|
|
|
// Respawn invincibility (short shield on spawn)
|
|
this._respawnShieldDuration = 3000; // 3 seconds on respawn
|
|
}
|
|
|
|
/**
|
|
* Update player tank state.
|
|
* @param {number} dt - Delta time in seconds.
|
|
*/
|
|
update(dt) {
|
|
if (!this.alive) return;
|
|
|
|
// Shield timer
|
|
if (this._shieldTimer > 0) {
|
|
this._shieldTimer -= dt * 1000;
|
|
this._blinkTimer += dt * 1000;
|
|
|
|
if (this._blinkTimer >= INVINCIBLE_BLINK_INTERVAL) {
|
|
this._blinkTimer = 0;
|
|
this._shieldBlink = !this._shieldBlink;
|
|
}
|
|
|
|
if (this._shieldTimer <= 0) {
|
|
this._shieldTimer = 0;
|
|
this._shieldBlink = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Override takeDamage to check shield.
|
|
* @param {number} amount
|
|
* @returns {boolean} Whether destroyed.
|
|
*/
|
|
takeDamage(amount = 1) {
|
|
if (this._shieldTimer > 0) return false; // invincible
|
|
return super.takeDamage(amount);
|
|
}
|
|
|
|
/**
|
|
* Handle player death: lose a life and respawn, or game over.
|
|
* @returns {boolean} True if player has lives remaining and respawned.
|
|
*/
|
|
die() {
|
|
this.alive = false;
|
|
this.lives--;
|
|
|
|
if (this.lives > 0) {
|
|
this.respawn();
|
|
return true;
|
|
}
|
|
return false; // game over
|
|
}
|
|
|
|
/**
|
|
* Respawn at the spawn point with temporary invincibility.
|
|
*/
|
|
respawn() {
|
|
this.x = MAP_OFFSET_X + this.spawnCol * TILE_SIZE + TILE_SIZE / 2;
|
|
this.y = MAP_OFFSET_Y + this.spawnRow * TILE_SIZE + TILE_SIZE / 2;
|
|
this.direction = DIRECTION.UP;
|
|
this.hp = 1;
|
|
this.alive = true;
|
|
this.visible = true;
|
|
this.fireLevel = FIRE_LEVEL.LV1;
|
|
this.activeBullets = 0;
|
|
|
|
// Temporary shield on respawn
|
|
this.activateShield(this._respawnShieldDuration);
|
|
}
|
|
|
|
/**
|
|
* Activate shield (invincibility).
|
|
* @param {number} duration - Duration in ms.
|
|
*/
|
|
activateShield(duration) {
|
|
this._shieldTimer = duration;
|
|
this._blinkTimer = 0;
|
|
this._shieldBlink = false;
|
|
}
|
|
|
|
/**
|
|
* Upgrade fire level.
|
|
*/
|
|
upgradeFireLevel() {
|
|
if (this.fireLevel < FIRE_LEVEL.LV3) {
|
|
this.fireLevel++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a life.
|
|
*/
|
|
addLife() {
|
|
this.lives++;
|
|
}
|
|
|
|
/**
|
|
* Check if the player can fire (based on active bullets and fire level).
|
|
* @returns {boolean}
|
|
*/
|
|
canFire() {
|
|
return this.alive && this.activeBullets < MAX_BULLETS_BY_LEVEL[this.fireLevel];
|
|
}
|
|
|
|
/**
|
|
* Whether the bullet should break steel.
|
|
* @returns {boolean}
|
|
*/
|
|
canBreakSteel() {
|
|
return this.fireLevel >= FIRE_LEVEL.LV3;
|
|
}
|
|
|
|
/**
|
|
* Render player tank with shield effect.
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
*/
|
|
render(ctx) {
|
|
if (!this.alive) return;
|
|
|
|
// Call base render
|
|
super.render(ctx);
|
|
|
|
// Draw shield effect
|
|
if (this._shieldTimer > 0 && this._shieldBlink) {
|
|
ctx.save();
|
|
ctx.strokeStyle = '#00FFFF';
|
|
ctx.lineWidth = 2;
|
|
ctx.globalAlpha = 0.6;
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, this.halfSize + 4, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
/** Whether the player is currently shielded. */
|
|
get isShielded() {
|
|
return this._shieldTimer > 0;
|
|
}
|
|
|
|
/** Get max bullets allowed on screen. */
|
|
get maxBullets() {
|
|
return MAX_BULLETS_BY_LEVEL[this.fireLevel];
|
|
}
|
|
}
|
|
|
|
module.exports = PlayerTank;
|