import Phaser from '../vendor/phaser.js'; import { toColor } from '../utils/Color.js'; import { canvasTexture } from '../utils/Textures.js'; const TEX_KEY = '__combat_bullet'; const TEX_SIZE = 32; /** A small, bright, soft dot — white, so a tint reads as its color. */ function drawSoftDot(ctx, size) { const c = size / 2; const g = ctx.createRadialGradient(c, c, 0, c, c, c); g.addColorStop(0.0, 'rgba(255,255,255,1.0)'); g.addColorStop(0.25, 'rgba(255,255,255,0.9)'); g.addColorStop(0.55, 'rgba(255,255,255,0.32)'); g.addColorStop(1.0, 'rgba(255,255,255,0.0)'); ctx.fillStyle = g; ctx.fillRect(0, 0, size, size); } /** * A combat tracer: a glowing dot that flies straight, lives for a while, * and carries the damage of the weapon that fired it. The scene (not the * bullet) decides what it hits — the sandbox runs its collisions by hand * (circle checks, like the main game's keep-out constraints) because * this build's collider API is fiddly; a bullet just reports its radius * (this.radius, world px) and owner. * * Tuned per weapon from data/combat.json (player.weapon / enemy.weapon): * speed, damage, life, color, size. */ export class Bullet extends Phaser.Physics.Arcade.Sprite { static TEXTURE_KEY = TEX_KEY; /** Ensure the shared soft-dot texture exists (canvas-generated). */ static ensureTexture(scene) { if (scene.textures.exists(TEX_KEY)) return TEX_KEY; canvasTexture(scene, TEX_KEY, TEX_SIZE, TEX_SIZE, (ctx, w, h) => drawSoftDot(ctx, Math.min(w, h))); return TEX_KEY; } /** * @param {object} scene the combat scene * @param {object} o { x, y, angle, speed, damage, life, color, size, owner } * angle rad (flight direction), speed px/s, life seconds, * owner 'player' | 'enemy' (whose bullets fly whose way). */ constructor(scene, o) { Bullet.ensureTexture(scene); super(scene, o.x, o.y, TEX_KEY); scene.add.existing(this); scene.physics.add.existing(this); this.owner = o.owner; this.damage = Math.max(0, o.damage ?? 10); this.life = Math.max(0, o.life ?? 1.2); this.dead = false; // consumed (a hit or the end of life) — consume() is one-shot const s = Math.max(0.05, (o.size ?? 16) / TEX_SIZE); this.setScale(s); this.setTint(toColor(o.color, 0xffffff)); this.setBlendMode(Phaser.BlendModes.ADD); // World-space radius for the scene's circle checks (~ a third of the glow). this.radius = TEX_SIZE * 0.3125 * s; // 10 px in texture space, at scale const body = this.body; body.setCircle(10); // texture-space radius (auto-scales with the sprite) body.setAllowGravity(false); body.setVelocity(Math.cos(o.angle) * (o.speed ?? 600), Math.sin(o.angle) * (o.speed ?? 600)); } /** Age the bullet (ms). Fizzles past its life. */ update(deltaMs) { this.life -= deltaMs / 1000; if (this.life <= 0) this.consume(); } /** * Consume the bullet (a hit, or the end of its life). One-shot: only * the FIRST caller destroys it — the collision pass and the aging pass * can both land in the same frame. * @returns {boolean} true when THIS call consumed it */ consume() { if (this.dead) return false; this.dead = true; this.destroy(); return true; } }