Newer
Older
rpg / apps / game / src / systems / combat / CombatViews.ts
import {
    Container,
    Graphics,
    IsoDepthLayer,
    ParticleEmitter,
    Sprite,
    SpriteFlash,
    Texture,
    screenToIsoExact,
    DEFAULT_ISO,
    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 } =>
            screenToIsoExact(p.x, p.y, map.data.width, map.data.height, DEFAULT_ISO) ?? { 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);
                root.position.set(en.pos.x, en.pos.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)!;
            view.root.position.set(en.pos.x, en.pos.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);
            }
            pv.position.set(pr.pos.x, pr.pos.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 = [];
    }
}