import type { GameState, Inventory } from '@rpg/engine';
import { itemName } from './items';
/**
* Квестовая система (контент по docs/world.md).
* Квест — декларативный набор стадий; состояние живёт во флагах/варах
* GameState (ставятся графами диалогов), статус выводится из состояния.
* Сцена спрашивает только «какой диалог у NPC» и «что делать после».
*/
/** Квест «Три цветка» (docs/world.md, акт 1): собрать и посадить колокольчики. */
export const QUEST_FLOWERS = 3;
/** Одна стадия квеста. */
export interface QuestStage {
/** Чей диалог ведёт стадию (id из NpcDef). */
npc: string;
/** Строка журнала, пока стадия активна. */
text: string;
/** Прогресс N/M по var-ключу (если стадия считается в штуках). */
progressKey?: string;
goal?: number;
/** Диалог стадии: взятие (первая) или сдача (промежуточная). */
dialogue?: string;
/** Готовность диалога (по умолчанию — сразу, как стадия активна). */
ready?: (state: GameState) => boolean;
/** Флаг, которым граф диалога завершает стадию. Без него стадия — «эпилог». */
doneFlag?: string;
/** Побочный эффект сцены после диалога стадии (см. applyQuestEffect). */
effect?: string;
}
export interface QuestDef {
id: string;
title: string;
/** Стадии по порядку; активная — первая незавершённая. */
stages: QuestStage[];
}
export const QUESTS: QuestDef[] = [
{
id: 'bells',
title: 'Три цветка',
stages: [
// Взятие: Ирвин рассказывает про поляны у прудов.
{
npc: 'elder',
text: 'Слышать гул на закате — пепел дышит у прудов.',
dialogue: 'elder_first',
doneFlag: 'quest_bells_taken'
},
// Сбор: активная строка журнала с прогрессом; сдача — Ирвину.
{
npc: 'elder',
text: 'Собрать лунные колокольчики у Серых прудов',
progressKey: 'flowers',
goal: QUEST_FLOWERS,
dialogue: 'elder_hand_in',
ready: (state) => state.getNumber('flowers') >= QUEST_FLOWERS,
doneFlag: 'quest_bells_done',
effect: 'plant_flowers'
},
// Эпилог: крючок акта 1 — разговор с Милой после посадки.
{
npc: 'trader',
text: 'Цветы посажены у тропы. Поляна гудит.',
dialogue: 'trader_after'
}
]
}
];
/** Квест взят: стоит doneFlag первой стадии. */
export function questTaken(state: GameState, quest: QuestDef): boolean {
const first = quest.stages[0]!;
return first.doneFlag !== undefined && state.hasFlag(first.doneFlag);
}
/** Квест завершён: все завершающие флаги стоят. */
export function questDone(state: GameState, quest: QuestDef): boolean {
return quest.stages.every((s) => s.doneFlag === undefined || state.hasFlag(s.doneFlag));
}
/** Активная стадия: первая незавершённая (эпилог без 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 с готовым диалогом.
* null — обычный repeat-диалог.
*/
export function questDialogueFor(state: GameState, npcId: string): string | null {
for (const quest of QUESTS) {
const stage = activeStage(state, quest);
if (!stage || stage.npc !== npcId || !stage.dialogue) continue;
if (stage.ready && !stage.ready(state)) continue;
return stage.dialogue;
}
return null;
}
/** Побочные эффекты внеквестовых диалогов (знакомства). */
const EXTRA_EFFECTS: Record<string, string> = {
trader_first: 'give_cloth' // Мила дарит полотно при знакомстве
};
/** Побочный эффект по завершённому диалогу (из стадий реестра и знакомств). */
export function questEffectFor(dialogueId: string): string | null {
for (const quest of QUESTS) {
for (const stage of quest.stages) {
if (stage.dialogue === dialogueId && stage.effect) return stage.effect;
}
}
return EXTRA_EFFECTS[dialogueId] ?? 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;
}
/** Строки сумки из инвентаря: имя + ×N (у одиночных — без множителя). */
export function inventoryItems(inventory: Inventory): string[] {
const items = inventory.all.map((slot) =>
slot.count > 1 ? `${itemName(slot.id)} ×${slot.count}` : itemName(slot.id)
);
if (items.length === 0) items.push('— пусто —');
return items;
}