import {
Container,
Graphics,
IsoDepthLayer,
ParticleEmitter,
Sprite,
SpriteFlash,
Texture,
worldToTile,
worldToScreen,
type Entity,
type Vec2
} from '@rpg/engine';
import type { CombatWorld } from './CombatWorld';
import type { EnemyKindId } from '../../data/enemies';
/**
* Пиксельные вьюхи боя: спрайты сгустков, снаряды, вспышки урона, растворение
* трупов, частицы ударов. Данные живут в CombatWorld (ECS) — здесь только отображение.
*/
interface EnemyView {
root: Container;
sprite: Sprite;
flash: SpriteFlash;
frames: [Texture, Texture];
animT: number;
animI: number;
}
export class CombatViews {
private views = new Map<Entity, EnemyView>();
private projectileViews = new Map<Entity, Graphics>();
private rings: { g: Graphics; t: number }[] = [];
constructor(
private combat: CombatWorld,
private actors: IsoDepthLayer,
private enemyTextures: Map<EnemyKindId, [Texture, Texture]>,
private fxRoot: Container
) {}
/** Спавн-вспышка пепла (взрыв частиц). */
hitBurst(pos: Vec2): void {
const fx = ParticleEmitter.oneShot(10, {
color: 0x52525c,
rate: 0,
lifetime: [0.2, 0.45],
velocity: { x: [0, 0], y: [0, 0] },
radialSpeed: [30, 70],
size: 2,
seed: (pos.x * 31 + pos.y) | 0
});
fx.position.set(pos.x, pos.y - 6);
this.fxRoot.addChild(fx);
}
/** Смерть: заметный выброс пепла. */
deathBurst(pos: Vec2): void {
const fx = ParticleEmitter.oneShot(16, {
color: 0x6a6a74,
rate: 0,
lifetime: [0.3, 0.6],
velocity: { x: [0, 0], y: [0, 0] },
radialSpeed: [40, 90],
size: 2,
seed: (pos.x * 17 + pos.y) | 0
});
fx.position.set(pos.x, pos.y - 8);
this.fxRoot.addChild(fx);
}
/** Кольцо резонанса — расходящаяся окружность (тикает в sync). */
resonanceRing(pos: Vec2): void {
const g = new Graphics();
g.position.set(pos.x, pos.y - 4);
this.fxRoot.addChild(g);
this.rings.push({ g, t: 0 });
}
/** Синхронизация вьюх с ECS-миром. Вызывать каждый тик после combat.update. */
sync(dt: number): void {
const map = this.combat.deps.map;
const tileOf = (p: Vec2): { x: number; y: number } =>
worldToTile(p.x, p.y, map.data.width, map.data.height) ?? { x: 0, y: 0 };
// Спавн новых
for (const [e, en] of this.combat.enemies) {
if (!this.views.has(e)) {
const frames = this.enemyTextures.get(en.kind.id)!;
const root = new Container();
const sprite = new Sprite(frames[0]);
sprite.anchor.set(0.5, 1);
root.addChild(sprite);
const sp = worldToScreen(en.pos.x, en.pos.y);
root.position.set(sp.x, sp.y);
const t = tileOf(en.pos);
this.actors.add(root, t.x, t.y);
this.views.set(e, {
root,
sprite,
flash: new SpriteFlash(sprite, 0xf2b45a, 0.15),
frames,
animT: 0,
animI: 0
});
}
}
// Обновление/смерть
for (const [e, en] of this.combat.enemies) {
const view = this.views.get(e)!;
const sp = worldToScreen(en.pos.x, en.pos.y);
view.root.position.set(sp.x, sp.y);
const t = tileOf(en.pos);
this.actors.setDepth(view.root, t.x, t.y);
if (en.brain.dead) {
// Растворение трупа
view.root.alpha = Math.max(0, en.corpseTimer / 0.5);
view.sprite.tint = 0x555560;
continue;
}
// Мигание неактивных: спящий сгусток почти невидим в пепле
view.root.alpha = en.brain.asleep ? 0.45 : 1;
// Покадровая анимация (два кадра, 4 Гц, только когда активен)
if (!en.brain.asleep) {
view.animT += dt;
if (view.animT >= 0.25) {
view.animT -= 0.25;
view.animI = 1 - view.animI;
view.sprite.texture = view.frames[view.animI];
}
}
// Вспышка урона: короткая подсветка в момент hp снижения — через flash.start
// (запускает CombatWorld через damageEnemy -> событие, здесь упрощённо по alpha)
view.flash.update(dt);
}
// Удаление уничтоженных сущностей
for (const [e, view] of this.views) {
if (!this.combat.world.isAlive(e)) {
this.actors.removeChild(view.root);
view.root.destroy({ children: true });
this.views.delete(e);
}
}
// Снаряды
for (const [e, pr] of this.combat.projectiles) {
let pv = this.projectileViews.get(e);
if (!pv) {
pv = new Graphics();
pv.circle(0, 0, 2).fill(0x86868f);
this.fxRoot.addChild(pv);
this.projectileViews.set(e, pv);
}
const sp = worldToScreen(pr.pos.x, pr.pos.y);
pv.position.set(sp.x, sp.y);
}
for (const [e, pv] of this.projectileViews) {
if (!this.combat.world.isAlive(e)) {
pv.destroy();
this.projectileViews.delete(e);
}
}
// Кольца резонанса
for (const ring of this.rings) {
ring.t += dt;
const k = Math.min(1, ring.t / 0.4);
ring.g.clear();
ring.g.circle(0, 0, 96 * k).stroke({ color: 0xd99a32, width: 1, alpha: 1 - k });
if (k >= 1) {
ring.g.destroy();
}
}
this.rings = this.rings.filter((r) => r.t < 0.4);
}
/** Вспышка конкретного врага (вызывает CombatWorld при попадании). */
flashEnemy(e: Entity): void {
this.views.get(e)?.flash.start();
}
exit(): void {
for (const [, view] of this.views) {
this.actors.removeChild(view.root);
view.root.destroy({ children: true });
}
this.views.clear();
for (const [, pv] of this.projectileViews) pv.destroy();
this.projectileViews.clear();
for (const r of this.rings) r.g.destroy();
this.rings = [];
}
}