diff --git a/docs/plan.md b/docs/plan.md index 577cb00..9d66cae 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -638,8 +638,27 @@ Уроки: экранная ось (1,0) — диагональ мира (триггер срезается углом — чистые оси комбинированным стиком); штампы шире тайла запирают проходы — контент карты строго в тайл 3×3; solidAt — колонка над землёй (y≥2). -- **Трек 16 — NPC и квестовый каркас**: NPC-дефы (появление по флагам), - перенос 9 диалоговых графов v1, флаги/вары, тосты; гейт. +- **Трек 16 — NPC и квестовый каркас** ✅ (2026-09-10, коммит): контент v1 + 1:1 — `src/data/ids.ts` (FLAGS 17/VARS/usedFlag), `src/data/items.ts` + (7 предметов), `src/data/npcs.ts` (Ирвин/Мила статично в Звенце, Клинт — + NPC_PLACEMENTS по флагу act2_hook), 9 диалоговых графов v1 в + `src/data/dialogues/*.json` + реестр с validateDialogue при старте, + `src/data/quests.ts` (стадии с doneFlag/ready/progressKey, + questDialogueFor/questLog/questTracker), `src/state.ts` (GameState — + DialogueState движка, сериализуема под сейв). В main.ts: NPC-манекены + (Character speed 0) + Interactable-точки (radius 5), выбор графа — + квест-стадия → repeat → dialogueFirst; в диалоге мир замер; onEffect — + giveItem/takeItem в Inventory, toast/custom → тосты; HUD: трекер/сумка/ + тосты; E = диалог/дальше, цифры = выбор варианта. Мост: npcInfo/talkTo/ + dialogueInfo/dialoguePick/dialogueAdvance/dialogueAbort/setState/stateInfo. + Гейт `tools/checks/npc.mjs` — 27 проверок (раскладка NPC, появление Клинта + по флагу, выбор графа по стадиям включая сдачи flowers/tones ≥3, выбор + ответа, сумка, флаги, трекер, motes_note по whenVar, мир замер, панель). + Уроки: после сдачи квеста активная стадия (npc trader) перекрывает + dialogueFirst этого NPC графом эпилога — порядок знакомств в сценарии + гейта существенен; подстроки имён с падежами («Мила» в «Милой») в + проверках текста ненадёжны. Экспортирован `evalConditions` из движка + (ready-условия стадий — те же предикаты, что в графах). - **Трек 17 — интерактив, пикапы, квест «Три цветка»**: INTERACTABLES (сундуки, записка, колодцы, прилавок, bell_rope), сбор цветов/мотов, FlowerQuest (взять → собрать в прудах → посадить → сдать с кат-сценой), diff --git a/v2/CLAUDE.md b/v2/CLAUDE.md index d896e53..e1cdc53 100644 --- a/v2/CLAUDE.md +++ b/v2/CLAUDE.md @@ -50,6 +50,7 @@ node apps/demo/tools/checks/combat.mjs # скриншот-гейт боя (/tmp/v2_combat_*.png) node apps/demo/tools/checks/audio.mjs # гейт аудио (DEV-шпион, /tmp/v2_audio_*.png) node apps/game/tools/checks/areas.mjs # гейт областей игры (/tmp/v2_areas_*.png) +node apps/game/tools/checks/npc.mjs # гейт NPC/квестов игры (/tmp/v2_npc_*.png) npx vite --port 5299 # демка (в apps/demo), http://localhost:5299 npx vite --port 5315 # игра «Пепельные луга» (в apps/game) ``` @@ -330,6 +331,34 @@ (луга→Звенец, луга⇄пруды) с фейдом и запоминанием возврата, люк→зал→return по стеку, click-переход «E» (подсказка у двери, вдали тишина), интерьер рисуется. + Трек 16 «NPC и квестовый каркас»: `src/data/ids.ts` (реестры FLAGS/VARS/ + usedFlag — флаги сюжета только через них), `src/data/items.ts` (7 предметов, + itemName), `src/data/npcs.ts` (NpcDef: статичные Ирвин/Мила в Звенце + + NPC_PLACEMENTS — Клинт в роще по флагу act2_hook; npcsForArea), 9 диалоговых + графов v1 в `src/data/dialogues/*.json` + реестр `src/data/dialogues.ts` + (validateDialogue при старте — битый граф падает сразу), `src/data/quests.ts` + (квест = стадии с doneFlag/ready/progressKey; questDialogueFor — выбор графа + по активной стадии; questLog/questTracker) и `src/state.ts` (GameState — + флаги/вары, DialogueState движка, сериализуема). В main.ts: NPC — манекены + (Character speed 0) с точками Interactable (radius 5), выбор графа — + квест-стадия → repeat по flagKey → dialogueFirst; в диалоге мир замер + (герой стоит, переходы/подсказки не тикаются); onEffect исполняет игру + (giveItem/takeItem → Inventory, toast/custom → тосты); HUD — трекер `#quest` + (первая активная стадия), сумка `#inv` (по inv.onChange), тосты `#toasts`. + Клавиши: E = диалог/дальше/переход, цифры 1-9 = выбор варианта. Мост: + `npcInfo` (кто стоит, tile, dialogue, near)/`talkTo(id?)`/`dialogueInfo` + (узел/выборы/path/result/флаги/вары/трекер/сумка)/`dialoguePick`/ + `dialogueAdvance`/`dialogueAbort`/`setState({vars,flags})` (гейт стадий)/ + `stateInfo`. Гейт `tools/checks/npc.mjs` (27 проверок): раскладка NPC v1, + появление Клинта только с act2_hook, выбор графа по стадиям (первый → + repeat → сдача при flowers/tones ≥3 → эпилоги), выбор варианта ответа, + giveItem → сумка, флаги/вары, трекер с прогрессом, motes_note по whenVar, + мир замер в диалоге, панель диалога на скриншоте. + Грабли трека 16: **порядок знакомств существенен** — после сдачи цветов + активная стадия «bells» (npc trader) перекрывает trader_first графом + trader_after: сдача квеста закрывает окно первого диалога этого NPC; + **«Мила» ≠ «Милой»** — подстроки имён с падежными окончаниями в проверках + трекера ненадёжны, сверять по устойчивому куску текста стадии. ## Известные грабли (уже собранные) diff --git a/v2/apps/game/index.html b/v2/apps/game/index.html index d7a12eb..cacd7fa 100644 --- a/v2/apps/game/index.html +++ b/v2/apps/game/index.html @@ -35,12 +35,94 @@ opacity: 0; pointer-events: none; } + /* Трекер квестов: первая активная стадия, левый верхний угол */ + #quest { + position: fixed; + top: 12px; + left: 12px; + padding: 8px 12px; + background: rgba(23, 23, 29, 0.85); + color: #d99a32; + font: 15px/1.5 monospace; + border: 1px solid #44454e; + border-radius: 4px; + white-space: pre; + } + /* Сумка: под трекером */ + #inv { + position: fixed; + top: 56px; + left: 12px; + padding: 8px 12px; + background: rgba(23, 23, 29, 0.85); + color: #c9c9d2; + font: 15px/1.5 monospace; + border: 1px solid #44454e; + border-radius: 4px; + white-space: pre; + } + #inv .title { color: #626c4c; } + #inv .item { color: #d99a32; } + /* Тосты: столбик всплывашек по центру сверху */ + #toasts { + position: fixed; + top: 52px; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + gap: 4px; + align-items: center; + pointer-events: none; + } + #toasts div { + padding: 5px 12px; + background: rgba(23, 23, 29, 0.85); + color: #d99a32; + font: 15px monospace; + border: 1px solid #44454e; + border-radius: 4px; + white-space: nowrap; + } + /* Диалог: панель внизу экрана, реплика + варианты (кнопки) */ + #dialog { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + width: 640px; + max-width: 92vw; + padding: 12px 16px; + background: rgba(23, 23, 29, 0.92); + color: #c9c9d2; + font: 16px/1.5 monospace; + border: 1px solid #44454e; + border-radius: 6px; + } + #dialog .speaker { color: #d99a32; font-weight: bold; } + #dialog .choices { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; } + #dialog .choices button { + text-align: left; + padding: 4px 10px; + background: #23232c; + color: #c9c9d2; + border: 1px solid #44454e; + border-radius: 3px; + font: inherit; + cursor: pointer; + } + #dialog .choices button:hover { border-color: #d99a32; color: #fff; } + #dialog .continue { margin-top: 8px; color: #626c4c; font-size: 13px; }
+ + + +