first commit
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// 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;
|
||||
Reference in New Issue
Block a user