/** * Bullet.js * Bullet entity that travels in a straight line and interacts with terrain/tanks. */ const { BULLET_SPEED, BULLET_SIZE, DIRECTION, DIR_VECTORS, MAP_OFFSET_X, MAP_OFFSET_Y, MAP_WIDTH, MAP_HEIGHT, } = require('../base/GameGlobal'); class Bullet { constructor() { this.x = 0; this.y = 0; this.direction = DIRECTION.UP; this.speed = BULLET_SPEED; this.size = BULLET_SIZE; this.halfSize = BULLET_SIZE / 2; this.alive = false; this.canBreakSteel = false; /** @type {'player'|'enemy'} */ this.owner = 'player'; /** @type {object|null} Reference to the tank that fired this bullet */ this.ownerTank = null; } /** * Initialize/reset the bullet for reuse from object pool. * @param {object} config * @param {number} config.x * @param {number} config.y * @param {number} config.direction * @param {string} config.owner - 'player' or 'enemy' * @param {boolean} [config.canBreakSteel] * @param {object} [config.ownerTank] */ init(config) { this.x = config.x; this.y = config.y; this.direction = config.direction; this.owner = config.owner || 'player'; this.canBreakSteel = config.canBreakSteel || false; this.ownerTank = config.ownerTank || null; this.alive = true; } /** * Update bullet position. * @param {number} dt - Delta time in seconds. */ update(dt) { if (!this.alive) return; const vec = DIR_VECTORS[this.direction]; const moveAmount = this.speed * dt * 60; this.x += vec.dx * moveAmount; this.y += vec.dy * moveAmount; // Check map boundaries if ( this.x < MAP_OFFSET_X || this.y < MAP_OFFSET_Y || this.x > MAP_OFFSET_X + MAP_WIDTH || this.y > MAP_OFFSET_Y + MAP_HEIGHT ) { this.destroy(); } } /** * Render the bullet. * In team battle mode, bullet color is determined by the _isAlly flag * set by the scene: ally = bright yellow, enemy = red. * In single-player / PVE mode, owner-based colors are used. * @param {CanvasRenderingContext2D} ctx */ render(ctx) { if (!this.alive) return; let fillColor; if (this._isAlly !== undefined) { // Team mode: use team-aware coloring fillColor = this._isAlly ? '#FFFF00' : '#FF4444'; } else { // Single-player / PVE fallback fillColor = this.owner === 'player' ? '#FFFF00' : '#FF6600'; } ctx.fillStyle = fillColor; ctx.fillRect( this.x - this.halfSize, this.y - this.halfSize, this.size, this.size ); } /** * Get bounding box. * @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, }; } /** * Destroy the bullet (mark for recycling). */ destroy() { this.alive = false; // Decrement owner's active bullet count if (this.ownerTank && typeof this.ownerTank.activeBullets === 'number') { this.ownerTank.activeBullets = Math.max(0, this.ownerTank.activeBullets - 1); } } } module.exports = Bullet;