/**
* Квест «Три цветка» (перенос v1 FlowerQuest на воксельный мир): сбор
* лунных колокольчиков у прудов, посадка поляны у тропы Звенца с
* пересевом на соседние тайлы. Изменения тайлов — правки вокселя цветка
* + журнал правок (переживает пересборку мира при переходе областей).
*/
import type { VoxelWorld } from '@rpg/engine';
import { GROUND_Y, UNIT, type AreaLayout } from './areas';
import { QUEST_FLOWERS, QUEST_PLANT_TILES, QUEST_TOWER_GREEN, QUEST_TOWER_TILE } from './data/quests';
import { VARS } from './data/ids';
import type { GameState } from './state';
/** Тайл раскладки v1: цветок / трава / пепел. */
const T_BELLFLOWER = 5, T_GRASS = 0, T_ASH = 4;
/** Слот вокселя цветка (B1 — тёплая жизнь). */
const FLOWER = 9;
/** Воксель цветка тайла (центр, как stampBellflower в areas.ts). */
function flowerVoxel(tx: number, ty: number): [number, number, number] {
return [tx * UNIT + 1, GROUND_Y, ty * UNIT + 1];
}
/** Правки цветков области: тайл → стоит ли цветок (переживает пересборку). */
export type FlowerEdits = Map<string, Map<number, boolean>>;
/** Тайлы раскладки области (RLE → плоский массив). */
function tilesOf(l: AreaLayout): number[] {
const out: number[] = [];
for (const [id, run] of l.tiles) for (let k = 0; k < run; k++) out.push(id);
while (out.length < l.width * l.height) out.push(T_ASH);
return out;
}
/** Тайтл цветка: тайл раскладки — сборный и правка не снята. */
export class FlowerQuest {
/** Правки по областям: areaId → (индекс тайла → стоит ли цветок). */
readonly edits: FlowerEdits = new Map();
/** Кэш декодированных тайлов текущей раскладки (isFlower зовётся ~25 раз/тик). */
private tilesCache: { layout: AreaLayout; flat: number[] } | null = null;
constructor(
private state: GameState,
/** Взять воксельный мир текущей области (он пересоздаётся на переходах). */
private world: () => VoxelWorld,
private layout: () => AreaLayout,
private areaId: () => string,
private showToast: (text: string) => void,
/** Собранный цветок — в сумку (v1: inventory.add('bellflower')). */
private addItem: (id: string) => void = () => {},
/** Правка тайла поля (v1 setTileTracked): свет колокольчиков следует за полем. */
private onFlowerTile?: (tx: number, ty: number, present: boolean) => void,
) {}
/** Тайлы раскладки с кэшем (кэш живёт, пока жива ссылка на раскладку). */
private flatTiles(l: AreaLayout): number[] {
if (this.tilesCache?.layout !== l) this.tilesCache = { layout: l, flat: tilesOf(l) };
return this.tilesCache.flat;
}
/** Цветок стоит на тайле? (раскладка + правки) */
isFlower(tx: number, ty: number): boolean {
const l = this.layout();
if (tx < 0 || ty < 0 || tx >= l.width || ty >= l.height) return false;
const idx = ty * l.width + tx;
const edit = this.edits.get(this.areaId())?.get(idx);
if (edit !== undefined) return edit;
return this.flatTiles(l)[idx] === T_BELLFLOWER;
}
/** Записать правку тайла (стоит/не стоит цветок). */
private setFlower(tx: number, ty: number, present: boolean): void {
let per = this.edits.get(this.areaId());
if (!per) this.edits.set(this.areaId(), (per = new Map()));
per.set(ty * this.layout().width + tx, present);
const [x, y, z] = flowerVoxel(tx, ty);
this.world().set(x, y, z, present ? FLOWER : 0);
this.onFlowerTile?.(tx, ty, present);
}
/** Восстановить цветки области после пересборки мира. */
restore(): void {
const per = this.edits.get(this.areaId());
if (!per) return;
for (const [idx, present] of per) {
const [x, y, z] = flowerVoxel(idx % this.layout().width, Math.floor(idx / this.layout().width));
this.world().set(x, y, z, present ? FLOWER : 0);
}
}
/** Сбор лунного колокольчика: цветок — в сумку, прогресс — в vars. */
collect(tx: number, ty: number): void {
this.setFlower(tx, ty, false);
this.addItem('bellflower'); // v1: предмет виден в сумке до сдачи
this.state.setVar(VARS.flowers, this.state.getNumber(VARS.flowers) + 1);
const n = this.state.getNumber(VARS.flowers);
this.showToast(`Лунный колокольчик (${Math.min(n, QUEST_FLOWERS)}/${QUEST_FLOWERS})`);
}
/** Посадка: поляна у тропы гудит и расползается на соседние тайлы. */
plantFlowers(): void {
const left = this.state.getNumber(VARS.flowers) - QUEST_FLOWERS;
this.state.setVar(VARS.flowers, Math.max(0, left));
for (const [tx, ty] of QUEST_PLANT_TILES) this.setFlower(tx, ty, true);
// Пересев: поляна расползается на соседние тайлы (якорь «надежда растёт»).
const l = this.layout();
const tiles = this.flatTiles(l);
for (const [tx, ty] of QUEST_PLANT_TILES) {
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]] as const) {
const x = tx + dx, y = ty + dy;
const t = tiles[y * l.width + x];
if ((t === T_GRASS || t === T_ASH) && !this.isFlower(x, y)) this.setFlower(x, y, true);
}
}
this.showToast('Цветы в земле. Поляна гудит.');
}
/** Зелень у подножия башни: визуальный финал акта 1. */
raiseTowerGreen(): void {
const l = this.layout();
const tiles = this.flatTiles(l);
for (const [tx, ty] of QUEST_TOWER_GREEN) {
const t = tiles[ty * l.width + tx];
if ((t === T_GRASS || t === T_ASH) && !this.isFlower(tx, ty)) this.setFlower(tx, ty, true);
}
}
/** Тайл башни Звенца (камера кат-сцены сдачи идёт сюда). */
get towerTile(): { x: number; y: number } {
return QUEST_TOWER_TILE;
}
}