import { evalConditions, type DialogueConditions, type DialogueWorld, type GameState } from '@rpg/engine';
import { FLAGS, VARS, type FlagId, type VarId } from './ids';
/**
* Квестовая система (контент по docs/world.md).
* Квест — декларативный набор стадий; состояние живёт во флагах/варах
* GameState (ставятся графами диалогов), статус выводится из состояния.
* Готовность стадии — те же условия, что и в графах диалогов (данные, не
* замыкания — валидатор и dry-run их видят). Сцена спрашивает только
* «какой диалог у NPC».
*/
/** Квест «Три цветка» (docs/world.md, акт 1): собрать и посадить колокольчики. */
export const QUEST_FLOWERS = 3;
/** Тайлы посадки поляны у тропы (Звенец): collectFlowers расползается от них. */
export const QUEST_PLANT_TILES: readonly (readonly [number, number])[] = [
[15, 13],
[16, 14],
[15, 15]
];
/** Башня Звенца: тайл (камера кат-сцены сдачи идёт сюда). */
export const QUEST_TOWER_TILE = { x: 14, y: 7 };
/** Зелень у подножия башни: визуальный финал акта 1. */
export const QUEST_TOWER_GREEN: readonly (readonly [number, number])[] = [
[13, 8],
[15, 8],
[14, 9]
];
/** Одна стадия квеста. */
export interface QuestStage {
/** Чей диалог ведёт стадию (id из NpcDef). */
npc: string;
/** Строка журнала, пока стадия активна. */
text: string;
/** Прогресс N/M по var-ключу (если стадия считается в штуках). */
progressKey?: VarId;
goal?: number;
/** Диалог стадии: взятие (первая) или сдача (промежуточная). */
dialogue?: string;
/** Готовность диалога — условия как в графах (по умолчанию сразу). */
ready?: DialogueConditions;
/** Флаг, которым граф диалога завершает стадию. Без него стадия — «эпилог». */
doneFlag?: FlagId;
}
export interface QuestDef {
id: string;
title: string;
/** Стадии по порядку; активная — первая незавершённая. */
stages: QuestStage[];
}
export const QUESTS: QuestDef[] = [
{
id: 'bells',
title: 'Три цветка',
stages: [
// Взятие: Ирвин рассказывает про поляны у прудов.
{
npc: 'elder',
text: 'Слышать гул на закате — пепел дышит у прудов.',
dialogue: 'elder_first',
doneFlag: FLAGS.quest_bells_taken
},
// Сбор: активная строка журнала с прогрессом; сдача — Ирвину.
{
npc: 'elder',
text: 'Собрать лунные колокольчики у Серых прудов',
progressKey: VARS.flowers,
goal: QUEST_FLOWERS,
dialogue: 'elder_hand_in',
ready: { whenVars: [{ key: VARS.flowers, op: 'ge', value: QUEST_FLOWERS }] },
doneFlag: FLAGS.quest_bells_done
},
// Эпилог: крючок акта 1 — разговор с Милой после посадки.
{
npc: 'trader',
text: 'Цветы посажены у тропы. Поляна гудит.',
dialogue: 'trader_after'
}
]
}
];
/** Активная стадия: первая незавершённая (эпилог без doneFlag — тоже стадия). */
export function activeStage(state: GameState, quest: QuestDef): QuestStage | null {
for (const stage of quest.stages) {
if (stage.doneFlag !== undefined && state.hasFlag(stage.doneFlag)) continue;
return stage;
}
return null;
}
/**
* Диалог NPC по квест-стадиям: активная стадия этого NPC с готовым диалогом.
* world — предикаты мира (сумка) для hasItem-условий. null — repeat-диалог.
*/
export function questDialogueFor(state: GameState, npcId: string, world?: DialogueWorld): string | null {
for (const quest of QUESTS) {
const stage = activeStage(state, quest);
if (!stage || stage.npc !== npcId || !stage.dialogue) continue;
if (stage.ready && !evalConditions(stage.ready, state, world)) continue;
return stage.dialogue;
}
return null;
}
/** Одна строка журнала квестов. */
export interface QuestEntry {
done: boolean;
text: string;
}
/** Журнал: стадии квестов с момента взятия (первая служебная стадия не показывается). */
export function questLog(state: GameState): QuestEntry[] {
const entries: QuestEntry[] = [];
for (const quest of QUESTS) {
let reached = true;
quest.stages.forEach((stage, i) => {
// Эпилог (без doneFlag) достигнут — значит, завершён.
const done = stage.doneFlag === undefined || state.hasFlag(stage.doneFlag);
if (i > 0 && reached) {
const progress =
stage.progressKey !== undefined && stage.goal !== undefined
? ` (${Math.min(state.getNumber(stage.progressKey), stage.goal)}/${stage.goal})`
: '';
entries.push({ done, text: `${stage.text}${progress}` });
}
if (!done) reached = false;
});
}
return entries;
}