Newer
Older
rpg / apps / game / src / systems / Shadows.ts
/**
 * Тени мира: блобы под актёрами (герой, NPC, враги) и направленные тени
 * tall-объектов карты. Положение считает чистая математика sunShadow/
 * blobPlacement по часам суток (сцена передаёт SunShadow каждый тик —
 * тени ползут за солнцем плавно). Актёрский блоб — первый ребёнок вьюхи
 * (ноги в (0,0)), статические тени — отдельный ground-слой под актёрами.
 */
import { Container, Sprite, Texture, blobPlacement, applyBlobShadow, makeShadowTexture, type Renderer, type SunShadow } from '@rpg/engine';

/** Актёрский блоб: вьюха + «толщина» объекта в px виртуального экрана. */
interface ActorShadow {
    view: Container;
    radiusPx: number;
    heightPx: number;
    sprite: Sprite;
}

/** Статическая тень (tall-тайл): база уже в координатах мира-экрана. */
interface StaticShadow {
    sprite: Sprite;
    base: { x: number; y: number };
    radiusPx: number;
    heightPx: number;
}

/** Узкий интерфейс для боевых вьюх — без связности с полной системой. */
export interface ShadowAttach {
    attachActor(view: Container, radiusPx: number, heightPx: number): void;
}

export class GameShadows implements ShadowAttach {
    private readonly tex: Texture;
    /** Ground-слой: добавить в world между картой и актёрами. */
    readonly ground = new Container();
    private readonly actorShadows: ActorShadow[] = [];
    private readonly statics: StaticShadow[] = [];

    constructor(renderer: Renderer) {
        this.tex = makeShadowTexture(renderer);
        this.ground.eventMode = 'none';
    }

    /** Блоб под актёром: первым ребёнком — спрайты рисуются поверх тени. */
    attachActor(view: Container, radiusPx: number, heightPx: number): void {
        const sprite = new Sprite(this.tex);
        sprite.anchor.set(0.5, 0.5);
        view.addChildAt(sprite, 0);
        this.actorShadows.push({ view, radiusPx, heightPx, sprite });
    }

    /** Направленная тень tall-объекта: база — центр основания (центр ромба). */
    addStatic(base: { x: number; y: number }, radiusPx: number, heightPx: number): void {
        const sprite = new Sprite(this.tex);
        sprite.anchor.set(0.5, 0.5);
        this.ground.addChild(sprite);
        this.statics.push({ sprite, base, radiusPx, heightPx });
    }

    /** Пересчёт всех теней по солнцу (null — ночь: погасить). */
    update(sun: SunShadow | null): void {
        // Вьюхи погибших врагов уничтожаются CombatViews — блобы вычищаем сами.
        for (let i = this.actorShadows.length - 1; i >= 0; i--) {
            if (this.actorShadows[i]!.sprite.destroyed) {
                this.actorShadows.splice(i, 1);
                continue;
            }
            const rec = this.actorShadows[i]!;
            this.place(rec.sprite, { x: 0, y: 0 }, rec, sun);
        }
        for (const rec of this.statics) this.place(rec.sprite, rec.base, rec, sun);
    }

    private place(
        sprite: Sprite,
        base: { x: number; y: number },
        rec: { radiusPx: number; heightPx: number },
        sun: SunShadow | null
    ): void {
        if (!sun) {
            sprite.alpha = 0;
            return;
        }
        applyBlobShadow(sprite, base, blobPlacement(rec.radiusPx, rec.heightPx, sun));
    }

    destroy(): void {
        this.ground.destroy({ children: true });
        this.tex.destroy();
    }
}