orbit/js/combat/Bullet.js

91 lines
3.2 KiB
JavaScript

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;
}
}