Newer
Older
rpg / apps / game / src / systems / quest / FlowerQuest.ts
import { CutsceneRunner, IsometricTileMap, tileToWorld, worldToScreen, type Vec2 } from '@rpg/engine';
import { Game } from '../../Game';
import { VARS } from '../../data/ids';
import { TILES } from '../../data/map';
import { QUEST_FLOWERS, QUEST_PLANT_TILES, QUEST_TOWER_GREEN, QUEST_TOWER_TILE } from '../../data/quests';
import { playSfx } from '../../data/sfxSpecs';

/**
 * Квест «Три цветка» (docs/world.md, акт 1): сбор лунных колокольчиков,
 * посадка поляны у тропы и кат-сцена сдачи (колокол башни, зелень).
 * Изменения тайлов идут через map.setTile с поддержанием индекса
 * layerTilePos — свет по тайлам-источникам остаётся живым.
 */
export class FlowerQuest {
    constructor(
        private game: Game,
        private map: IsometricTileMap,
        /** Позиции тайлов-источников по id (свет/слои): меняем при setTile. */
        private layerTilePos: Map<number, Vec2[]>,
        /** Экранный центр ромба тайла (с Origin карты). */
        private tileCenter: (tx: number, ty: number) => { x: number; y: number },
        /** Всплеск пеплинок в точке (CombatViews.hitBurst). */
        private burst: (s: { x: number; y: number }) => void,
        private showToast: (text: string) => void,
        private cutscene: CutsceneRunner
    ) {}

    /** id тайла карты. */
    tileId(x: number, y: number): number {
        return this.map.data.tiles[y * this.map.data.width + x];
    }

    /** Смена тайла с поддержанием индекса layerTilePos (свет по тайлам живой). */
    setTileTracked(x: number, y: number, id: number): void {
        const prev = this.tileId(x, y);
        this.map.setTile(x, y, id);
        const from = this.layerTilePos.get(prev);
        if (from) {
            const i = from.findIndex((p) => p.x === x && p.y === y);
            if (i >= 0) from.splice(i, 1);
            if (from.length === 0) this.layerTilePos.delete(prev);
        }
        const list = this.layerTilePos.get(id) ?? [];
        list.push({ x, y });
        this.layerTilePos.set(id, list);
    }

    /** Сбор лунного колокольчика: тайл зеленеет, цветок — в сумку, прогресс — в vars. */
    collectFlower(x: number, y: number): void {
        this.setTileTracked(x, y, TILES.GRASS);
        this.game.inventory.add('bellflower');
        const n = this.game.state.getNumber(VARS.flowers) + 1;
        this.game.state.setVar(VARS.flowers, n);
        this.game.engine.events.emit('quest:flower', { n });
        playSfx(this.game.audio, 'sfx/bell_hit', 0.6);
        this.burst(this.tileCenter(x, y));
        this.showToast(`Лунный колокольчик (${Math.min(n, QUEST_FLOWERS)}/${QUEST_FLOWERS})`);
    }

    /** Посадка цветов у тропы: поляна гудит колокольчиками и разрастается. */
    plantFlowers(): void {
        const left = this.game.state.getNumber(VARS.flowers) - QUEST_FLOWERS;
        this.game.state.setVar(VARS.flowers, Math.max(0, left));
        for (const [tx, ty] of QUEST_PLANT_TILES) {
            this.setTileTracked(tx, ty, TILES.BELLFLOWER);
            this.burst(this.tileCenter(tx, ty));
        }
        // Пересев: поляна расползается на соседние тайлы (якорь «надежда растёт»).
        for (const [tx, ty] of QUEST_PLANT_TILES) {
            for (const [dx, dy] of [
                [1, 0],
                [-1, 0],
                [0, 1],
                [0, -1]
            ]) {
                const id = this.tileId(tx + dx, ty + dy);
                if (id === TILES.GRASS || id === TILES.ASH) this.setTileTracked(tx + dx, ty + dy, TILES.BELLFLOWER);
            }
        }
        void this.game.audio.play('sfx/bell_low', 0.8);
        this.showToast('Цветы в земле. Поляна гудит.');
    }

    /** Кат-сцена сдачи: посадка, камера к башне, удар колокола, зелень — финал акта 1. */
    startHandInCutscene(): void {
        // Башня Звенца: тайл в данных, камере нужны юниты.
        const tower = tileToWorld(QUEST_TOWER_TILE.x, QUEST_TOWER_TILE.y);
        const s = worldToScreen(tower.x, tower.y);
        this.cutscene.play([
            // Пересев: поляна гудит, герой стоит.
            { kind: 'call', fn: () => this.plantFlowers(), seconds: 0.8 },
            // Камера уходит к башне.
            { kind: 'cameraMove', x: tower.x, y: tower.y, seconds: 1.2, camera: this.game.camera },
            // Удар колокола: низкий звон, тряска, кольцо резонанса.
            {
                kind: 'burst',
                fn: () => {
                    void this.game.audio.play('sfx/bell_low', 1);
                    this.game.camera.addShake(2.5, 0.4);
                    this.burst(s);
                },
                seconds: 1
            },
            // Зелень поднимается у подножия башни (визуальный финал акта).
            {
                kind: 'burst',
                fn: () => {
                    for (const [tx, ty] of QUEST_TOWER_GREEN) {
                        const id = this.tileId(tx, ty);
                        if (id === TILES.GRASS || id === TILES.ASH) {
                            this.setTileTracked(tx, ty, TILES.BELLFLOWER);
                            this.burst(this.tileCenter(tx, ty));
                        }
                    }
                },
                seconds: 1.2
            },
            // Тост-финал акта 1.
            { kind: 'call', fn: () => this.showToast('Пепел отступил у тропы. Машина дышит тише... но дышит.') }
        ]);
    }
}