91 lines
3.2 KiB
JavaScript
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;
|
|
}
|
|
}
|