import {
Container,
Graphics,
IsoDepthLayer,
Sprite,
SpriteAnimator,
Texture,
worldToTile,
worldToScreen,
type Entity,
type FxLayer,
type Vec2
} from '@rpg/engine';
import type { CombatWorld } from './CombatWorld';
import type { EnemyKindId } from '../../data/enemies';
/**
* Пиксельные вьюхи боя: спрайты сгустков, снаряды, вспышки урона, растворение
* трупов, частицы ударов. Данные живут в CombatWorld (ECS) — здесь только отображение.
*/
interface EnemyView {
root: Container;
sprite: Sprite;
animator: SpriteAnimator;
/** Растворение запущено (труп тает через FxFade, не вручную). */
dissolving?: boolean;
}
export class CombatViews {
private views = new Map<Entity, EnemyView>();
private projectileViews = new Map<Entity, Graphics>();
constructor(
private combat: CombatWorld,
private actors: IsoDepthLayer,
private enemyTextures: Map<EnemyKindId, [Texture, Texture]>,
private fxRoot: Container,
/** Слой эффектов (FxLayer сцены): тик и зачистка — сами. */
private fx: FxLayer
) {}
/** Спавн-вспышка пепла (взрыв частиц). */
hitBurst(pos: Vec2): void {
this.fx.burst(10, { x: pos.x, y: pos.y - 6 }, {
color: 0x52525c,
rate: 0,
lifetime: [0.2, 0.45],
radialSpeed: [30, 70],
size: 2,
drag: 2.5,
scaleOverLife: 'shrink',
seed: (pos.x * 31 + pos.y) | 0
}, this.fxRoot);
}
/** Смерть: заметный выброс пепла. */
deathBurst(pos: Vec2): void {
this.fx.burst(16, { x: pos.x, y: pos.y - 8 }, {
colors: [0x6a6a74, 0x8a8a96],
rate: 0,
lifetime: [0.3, 0.6],
radialSpeed: [40, 90],
size: 2,
drag: 2,
spin: [-4, 4],
scaleOverLife: 'shrink',
seed: (pos.x * 17 + pos.y) | 0
}, this.fxRoot);
}
/** Кольцо резонанса — расходящаяся окружность (пресет, тикается слоем). */
resonanceRing(pos: Vec2): void {
this.fx.ring(
{ color: 0xd99a32, from: 8, to: 96, duration: 0.4, width: 1 },
{ x: pos.x, y: pos.y - 4 },
{ parent: this.fxRoot }
);
}
/** Синхронизация вьюх с 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,
animator: new SpriteAnimator(sprite, { walk: { frames, fps: 4 } }, 'walk')
});
}
}
// Обновление/смерть
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) {
// Растворение трупа: один fade на переход alive→dead (keep —
// вьюху уничтожает сцена по !isAlive, без двойного destroy).
if (!view.dissolving) {
view.dissolving = true;
this.fx.fade(view.root, { duration: 0.5, keep: true });
}
view.sprite.tint = 0x555560;
continue;
}
// Мигание неактивных: спящий сгусток почти невидим в пепле
view.root.alpha = en.brain.asleep ? 0.45 : 1;
// Покадровая анимация (4 Гц) — только когда активен; спящим pause.
if (en.brain.asleep) view.animator.pause();
else {
view.animator.resume();
view.animator.play('walk');
}
view.animator.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);
}
}
}
/** Вспышка конкретного врага (вызывает CombatWorld при попадании). */
flashEnemy(e: Entity): void {
const view = this.views.get(e);
if (view) this.fx.flash(view.sprite, 0xf2b45a, 0.15);
}
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();
}
}