# Questwright: руководство по написанию квестов (для ИИ-агентов)

*Перевод для удобства чтения. Формат и все имена полей остаются английскими; при расхождении верна английская версия: `/downloads/questwright-authoring-reference.md`.*

**Прочитайте это полностью, прежде чем писать хоть один `.quest.yaml`.** Здесь описано РОВНО то, что
поддерживают компилятор и валидатор Questwright. Не выдумывайте поля, эмоции, имена кадров, виды условий,
глаголы эффектов и типы целей, которых нет в этом списке: всё незнакомое либо молча отбрасывается, либо
помечается как ошибка валидации. В сомнительном случае берите поле или значение, встречающееся ниже дословно.

Авторитетный парсер — `QuestwrightYamlNormalizer.cpp`; валидатор — `ValidateSchema`. Это руководство
повторяет их. Если что-то здесь противоречит более старой спецификации, **побеждает код (то есть это
руководство)**.

---

## 0. Золотые правила (кратко)

1. Квест — это один YAML-файл. YAML — **источник истины**; Studio компилирует его в ассеты.
   Вы пишете YAML и никогда не правите сгенерированные `.uasset`.
2. Используйте только те поля, эмоции, кадры, типы целей, виды условий и глаголы эффектов, что перечислены здесь.
3. Каждое значение `target` / `location` / `tag` / `grant_tag` — это **GameplayTag, который ОБЯЗАН быть
   зарегистрирован** (нативный тег C++ или `DefaultGameplayTags.ini`). Незарегистрированный тег молча
   недействителен → цель никогда не засчитывается, условие никогда не срабатывает. Это причина №1 жалоб
   «мой квест ничего не делает».
4. Каждой произносимой реплике, каждому варианту ответа и каждой цели давайте стабильный `id` (строчные буквы
   и подчёркивания). По идентификаторам ключуются локализация и сгенерированные ассеты VO и лицевой анимации;
   перестановка реплик не должна менять чужие идентификаторы.
5. Идентификаторы шагов автоматически получают префикс короткого имени квеста. Ссылайтесь на шаги в `goto`/`next`
   по авторскому идентификатору (префикс проставляется за вас, но форма `<Short>_<id>` безопаснее всего).
6. `schema_version` необязателен и носит информационный характер. Парсер принимает любое значение, а функции
   включаются по наличию своих блоков (например, `staging:`), а не по номеру. Текущая версия — **4**
   (в 3 появилась постановка, в 4 — камерные метки); старые файлы с объявленной `2` разбираются без изменений.

---

## 1. Как это работает (конвейер)

```
<Quest>.quest.yaml  ──разбор──►  IR  ──проверка──►  (ошибки видны в Studio)
                                   │
                                   ├─ Resolve Audio (синтез VO для каждой культуры, голосом каждого спикера)
                                   ├─ Facial (запекание липсинка FA_<id> из VO)
                                   ├─ Generate (DataAssets: DA_QW_Dialogue_*, DA_QW_Quest_*, String Tables)
                                   └─ Bind & Stage (привязка каста к NPC на уровне)
```

Вы пишете YAML; всё остальное делает «Build → RUN» в Studio.

---

## 2. Расположение и именование файлов

- Авторские квесты кладите в **`<Project>/Content/Quests/`** (они появляются в Studio library автоматически,
  Import не нужен). `Docs/Samples/` — папка ПОСТАВЛЯЕМОГО демо; свои квесты туда не добавляйте.
- Имя файла: `<Name>.quest.yaml` (часть `.quest.` — соглашение; подхватывается любой `.yaml`/`.yml`/`.json`
  в корне сканирования). Один квест на файл.

---

## 3. Минимальный каркас

```yaml
quest: Region.MyQuest          # уникальный id/слаг (ОБЯЗАТЕЛЬНО)
name: "Мой квест"
giver: Bran                    # ключ спикера того NPC, который выдаёт квест
source_culture: en
schema_version: 4
steps:                         # ОБЯЗАТЕЛЬНО, минимум 1
  - id: MyQuest_Offer
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Bran,   id: greet, text: "Здравствуй.", emotion: neutral }
        - { by: Player, id: hi,    text: "Привет." }
      choices:
        - { id: accept, text: "Я помогу.", goto: MyQuest_Do }
      on_end: { quest: accept }
  - id: MyQuest_Do
    end: completed
```

---

## 4. Поля квеста верхнего уровня

| Поле | Тип | Обязательно | Примечания |
|---|---|---|---|
| `quest` | строка | **да** | Уникальный id/слаг, например `Farmlands.NightVisitors`. Везде используется как QuestId. |
| `name` | строка | нет | Отображаемый заголовок (локализуемый). |
| `summary` | строка | нет | Краткое описание для журнала. |
| `giver` | строка | нет | Ключ спикера NPC-квестодателя (например, `Bran`). Определяет выбор квестодателя и голос реплик-выкриков. |
| `giver_display_name` | строка | нет | Понятное имя, показываемое в интерфейсе; по умолчанию берётся `giver`. |
| `region_display_name` | строка | нет | Подпись группировки по регионам в журнале. |
| `source_culture` | строка | нет | Язык написания, например `en`. Другие языки живут в сопутствующих CSV. |
| `schema_version` | целое | нет | Необязательно, информационно; сейчас `4` (см. §0, правило 6). |
| `priority` | целое | нет | Порядок предложения у квестодателя: чем выше, тем раньше предлагается, если у NPC несколько квестов. По умолчанию 0. |
| `repeatable` | логическое | нет | Если true, завершённый квест можно принять снова («дейлик»). По умолчанию false. |
| `available_when` | условие | нет | Затвор предусловия (см. §11). Пусто = доступен сразу. Обеспечивает цепочки. |
| `rewards` | объект | нет | См. §15. Работают только `base.{currency,xp,items}`. |
| `camera` | объект | нет | Политика автокамеры. См. §16. |
| `barks` | объект | нет | Фоновые реплики квестодателя по таймеру. См. §14. |
| `idle_barks` | объект/список | нет | Реплики при взаимодействии, когда квестодателю нечего предложить. См. §14. |
| `steps` | список | **да** | Тело квеста, ≥1 шага. См. §5. |

> Полей `description`, `tags`, `category`, `type`, `level`, `zone`, `objectives` (цели живут внутри шагов),
> `dialogue` и `npc` на верхнем уровне НЕТ. Не выдумывайте их.

---

## 5. Шаги

Шаг — это один узел квеста (диалоговая сцена, цель или терминальный маркер).

| Поле | Тип | Примечания |
|---|---|---|
| `id` | строка | **Обязательно.** Стабильный идентификатор; автоматически превращается в `<ShortQuest>_<id>`, если префикса ещё нет. |
| `title` | строка | Необязательная внутренняя подпись. |
| `scene` | объект | Диалоговая сцена (реплики и варианты ответа). См. §6. |
| `objective` | объект | Отслеживаемая цель (убить/собрать/дойти/поговорить/доставить). См. §9. |
| `next` | строка | Переход по умолчанию: идентификатор следующего шага (когда в сцене нет вариантов ответа). |
| `end` | строка | Терминальный маркер. Непустое значение закрывает диалог. Принятые значения: `completed`, `declined`, `failed`. |
| `require` | условие ИЛИ строка | Затвор входа в этот шаг (см. §11). Например, закрыть шаг сдачи квеста до выполнения цели. |

Обычно у шага есть ЛИБО `scene` (разговор), ЛИБО `objective` (сделать дело), плюс `next`/`end`.

---

## 6. Сцены: реплики и варианты ответа

```yaml
scene:
  speakers: [Bran, Mira, Player]   # ВСЕ ключи спикеров, присутствующих в этой сцене (2, 3 и больше)
  lines:
    - { by: Bran,   id: greet, text: "Подойди сюда.", emotion: worried, shot: cu }
    - { by: Mira,   id: warn,  text: "Не верь ему.", to: Player }
    - { by: Player, id: what,  text: "Что случилось?" }
  choices:
    - { id: help,   text: "Я помогу.",      goto: MyQuest_Accept }
    - { id: refuse, text: "Это не моя беда.", goto: MyQuest_Refuse }
  on_end: { quest: accept }      # эффект, применяемый при выходе из сцены (см. §12)
```

**Несколько персонажей.** Сцена поддерживает любое число спикеров; просто перечислите каждый ключ в `speakers`
и проставьте `by` у каждой реплики. Чтобы добавить второго или третьего персонажа, добавьте его ключ (например,
`Mira`) в `speakers` и используйте в `by`. В рантайме каждый ключ привязывается к актёру, уже размещённому на
уровне с этим ключом спикера; если такого нет, персонаж спавнится из карты спикеров (`DT_QW_Speakers`, её пишет
стадия Cast) и ставится рядом с квестодателем. Заспавненные персонажи убираются автоматически по окончании диалога.

### Поля реплики
| Поле | Тип | Примечания |
|---|---|---|
| `by` | строка | **Обязательно.** Ключ спикера (должен быть в `speakers`). На этом участнике проигрываются VO и лицевая анимация. |
| `to` | строка | *Необязательно.* Ключ спикера, **к которому** обращена реплика; определяет кадр через плечо / крупный план и поворот головы в сценах с 3+ спикерами. В сценах на двоих опускается (вторая сторона выводится: предыдущий говорящий, иначе игрок). |
| `id` | строка | Стабильный идентификатор реплики (строчные буквы и подчёркивания). Настоятельно рекомендуется, так как по нему ключуются VO, лицевая анимация и локализация. |
| `text` | строка | Произносимый текст (локализуемый). |
| `emotion` | перечисление | См. §7. Задаёт аддитивную мимику и тон VO. По умолчанию `neutral`. |
| `shot` | перечисление | См. §8. По умолчанию `auto` (выбирает компилятор). |
| `echo_of` | строка | Эта реплика повторяет текст варианта ответа; укажите `id` этого варианта. Разделяет его ключ локализации (не дублируйте текстовые ключи). |
| `loc` | строка | Явное переопределение ключа локализации (нужно редко). |

### Поля варианта ответа
| Поле | Тип | Примечания |
|---|---|---|
| `id` | строка | Стабильный идентификатор варианта. По нему ключуется локализованный текст варианта. |
| `text` | строка | Подпись варианта. |
| `goto` | строка | **Обязательно.** Идентификатор целевого шага. |
| `require` | тег или список | GameplayTag(и), которыми должен владеть игрок, чтобы вариант был доступен. (Именно теги, НЕ структурные условия.) |
| `grant` | тег или список | GameplayTag(и), выдаваемые при выборе этого варианта. |

> Полный LineId имеет вид `<StepId>_<lineId>_<Speaker>`; так называются ассеты VO (например,
> `NightVisitors_Offer_greet_Bran`). Вы его не пишете — он выводится из `by` + `id` + шага.

---

## 7. Эмоции (`emotion:` у реплики)

Допустимые канонические значения: **`neutral`, `happy`, `sad`, `angry`, `afraid`, `surprised`**.

Принимаемые псевдонимы (все сводятся к каноническому настроению):
- `fear`, `scared`, `worried`, `nervous`, `anxious`  → **afraid**

Всё остальное — **ошибка валидации** (`invalid_emotion`), значение откатывается к `neutral`. Отдельного
настроения `worried` нет, как и `confused`, `disgusted`, `excited`, `bored` и прочих. Берите ближайшее из
шести (например, excited→happy, distressed→sad/afraid). Если нужно новое слово, его сперва добавляют
псевдонимом в код; не пишите неизвестные токены эмоций.

---

## 8. Кадры (`shot:` у реплики)

Допустимые значения: **`auto`, `wide`, `ots`** (псевдонимы `over_shoulder`/`overshoulder`), **`cu`** (псевдонимы
`close_up`/`closeup`), **`reaction`**, **либо идентификатор пользовательской камерной метки**, объявленной в
`staging.cameras` (§8b).

По умолчанию — `auto`: компилятор кадрирует сам (общий план для завязки, затем крупные планы на последовательных
репликах одного спикера). Задавайте `shot` только чтобы переопределить конкретную реплику. Неизвестные токены
кадра (ни базовый кадр, ни объявленная камера) → ошибка `invalid_shot`.

---

## 8a. Постановка: расстановка и перемещение участников (`staging.marks`, `line.stage`)

Необязательный блок `staging:` верхнего уровня расставляет участников сцены вокруг **якоря** (вид сверху, см),
а `line.stage` перемещает их по ходу диалога. Без этого блока каждый спикер просто ставится лицом к собеседнику;
постановка нужна только для мизансцен с несколькими NPC, заспавненной массовки и перемещений.

```yaml
staging:
  anchor: DialogueMarker_Barn   # размещённый BP_QW_DialogueMarker; опустить → трансформ квестодателя
  settle_timeout: 4.0           # секунд на переход пешком, после чего откат к телепорту
  marks:
    - { id: giver, actor: Bran,   pos: [-100, -30], face: hero,   keep: true, source: existing }
    - { id: hero,  actor: Player, pos: [-100,  60], face: giver,  source: existing }
    - { id: m1,    actor: Mira,   pos: [ 140,  30], face: anchor, appear: arrive }
    - { id: m2,    actor: Mira,   pos: [  40, -80], face: anchor }   # путевая точка для перехода по ходу диалога
```

### Поля метки
| Поле | Примечания |
|---|---|
| `id` | **Обязательно**, стабильный (`[A-Za-z0-9_]+`). `line.stage.to` и камеры ссылаются на метки по идентификатору. Делит ОДНО пространство имён с идентификаторами камерных меток (§8b); столкновение — ошибка. |
| `actor` | Ключ спикера (из `speakers`/Cast). Опустите для чистой путевой точки. |
| `pos` | **Обязательно** `[x, y]` в см, плоскость земли относительно якоря. Z прижимается к земле в рантайме. |
| `face` | Автоповорот к `anchor` \| ключу спикера \| идентификатору метки. **Взаимоисключающе с `yaw`.** Стоять спиной к якорю допустимо (информационное сообщение, не ошибка). |
| `yaw` | Ручной угол корпуса в градусах (вместо `face`). |
| `appear` | `placed` (по умолчанию: стоит здесь с начала сцены) \| `arrive` (спавнится вне поля зрения игрока и приходит к метке пешком; его первая реплика ждёт прибытия). |
| `keep` | По умолчанию `false`: заспавненный участник исчезает по окончании диалога; `true` оставляет его. |
| `source` | `existing` \| `spawn`. Обычно опускается, так как выводится из Cast (игрок и уже размещённые актёры *перемещаются*, все остальные *спавнятся*). |

### Переходы по ходу диалога (`line.stage`)
```yaml
  - { by: Mira, id: step, text: "Дай-ка я посмотрю на следы.",
      stage: [ { who: Mira, to: m2, approach: walk } ] }
```
`who` = участник, `to` = идентификатор объявленной метки, `approach` = `walk` (по умолчанию; блокирующий:
реплика ждёт прибытия, с откатом к телепорту по истечении `settle_timeout`) | `teleport`. Одна метка держит
**одного** участника за раз; занятость отслеживается построчно, поэтому двое на одной метке в один момент —
ошибка валидации.

---

## 8b. Пользовательские камерные метки (`staging.cameras`)

Камеры относительно сцены, расставленные на том же полотне постановки. Переиспользуемая оптика (объектив,
глубина резкости, фильмбэк) живёт в **классе камеры** (любой Blueprint-подкласс `QuestwrightDialogueSceneCamera`),
а метка — это экземпляр: стабильный `id` + позиция относительно якоря + наведение. Реплики ссылаются на
**идентификатор метки** через `shot:`, никогда на класс или координаты. Автоматическая грамматика никогда не
выбирает пользовательские метки — они только явные переопределения на реплику (или через `choice_shot`).

```yaml
staging:
  anchor: DialogueMarker_Barn
  marks:
    - { id: giver, actor: Bran, pos: [-100, -30], face: hero }
  cameras:
    - { id: cam_quick,  class: dynamic,         pos: [200, 0],    height: 200, look_at: anchor }
    - { id: cam_high,   class: BP_QW_Cam_Wide,  pos: [220, -180], height: 340, look_at: anchor }
    - { id: cam_window, class: BP_MyCam_Voyeur, pos: [-320, 90],  height: 150, look_at: giver, fov: 28 }
    - { id: cam_dutch,  class: BP_MyCam_Dutch,  pos: [140, 200],  height: 120, yaw: -135, pitch: -8 }

# в реплике:              shot: cam_window
# пока разворачиваются варианты ответа: camera: { choice_shot: cam_high }
```

Поля: `id` (обязательно, `[A-Za-z0-9_]+`, делит ОДНО пространство имён с идентификаторами меток постановки;
столкновение — ошибка), `class` (обязательно: встроенный токен `dynamic`, короткое имя BP, уникальное в рамках
проекта, или полный путь объекта), `pos` (обязательно `[x, y]` в см относительно якоря), `height` (см над землёй
якоря, по умолчанию 160; камеры никогда не прижимаются к земле, так что висеть в воздухе допустимо), `look_at`
(**xor** `yaw`+`pitch`) наводит на `anchor` | ключ спикера | идентификатор метки постановки; наведение вычисляется
в момент склейки, поэтому следует за переходами `stage:` по ходу диалога. Ручной `yaw` отсчитывается от направления
«вперёд» у якоря. `fov` (в градусах) при желании переопределяет оптику класса.

Классу `class: dynamic` **не нужен Blueprint вообще**: камера создаётся во время диалога из нативной базы C++ с
оптикой по умолчанию (24 мм, без глубины резкости). Сочетайте её с `fov:` для быстрого нестандартного ракурса, а
наследовать `QuestwrightDialogueSceneCamera` в BP имеет смысл только ради тонкой настройки объектива, глубины
резкости и фильмбэка.

Если класс камеры не удаётся загрузить в рантайме, реплика деградирует до запечённого запасного `wide` с
предупреждением, так что сцена никогда не ломается.

---

## 9. Цели (`objective:` у шага)

Отслеживаемая цель живёт внутри шага. **Структурный шаг (тот, что несёт `objective`) использует обобщённый
идентификатор секции, а не содержательное имя**: `Goal` для цели, `Deliver` для сдачи квеста. Конкретный вид
живёт в поле `type` цели (Kill/Collect/…), и его можно менять, не меняя идентификаторов. Никогда не именуйте их вручную
(«KillWolves», «TurnIn»). Собственный идентификатор цели по умолчанию равен идентификатору секции, и на него
ссылаются `next`/`goto`/`require`. Обобщённо для любого квеста. (Можно и опустить `id` шага — тогда компилятор
выведет `Goal`/`Deliver` из цели, — но написать `id: Goal` яснее всего.)

```yaml
- id: Goal                                          # обобщённая секция; ВИД указан в `type` ниже
  objective:
    type: Kill                                      # вид цели (см. таблицу); id по умолчанию — "Goal"
    target: QuestwrightDemo.Enemy.Wolf              # GameplayTag; ОБЯЗАН быть зарегистрирован (§10)
    count: 3                                        # сколько событий нужно для выполнения (>=1)
    location: QuestwrightDemo.Area.FarmOutskirts    # необязательный GameplayTag; засчитывается только в этой области
    desc: "Убить волков у фермы"                    # текст журнала/трекера (локализуемый)
  next: Deliver                                     # ссылается на шаг сдачи (Deliver) по его типу
```

(Если в одном квесте нужны две цели одного типа, задайте им разные явные идентификаторы.)

### `type` цели (канал событий)
| Тип | Смысл | Как засчитывается |
|---|---|---|
| `Kill` | убить N помеченных врагов | игра сообщает о событии Kill при смерти (`ReportObjectiveEvent`) |
| `Collect` | собрать N помеченных предметов | игра сообщает о событии Collect при подборе |
| `Reach` | добраться до помеченного места | игра сообщает о событии Reach либо вызывает `CompleteObjective` из триггера |
| `TalkTo` (`talk`) | поговорить с кем-то | **автоматически**: начало разговора с NPC засчитывает цель (см. ниже) |
| `Deliver` | вернуться к квестодателю / доставить ему | выполняется САМОЙ сдачей квеста (см. примечание ниже) |
| `Custom` | любое пользовательское событие | игра сообщает `Custom` плюс ваш собственный тег `target` |

Цель `TalkTo` ключуется по **ключу спикера, а не по GameplayTag**: пишите в качестве цели ключ персонажа
(`target: Mary` — тот же ключ, что в `speakers:` и на вкладке Cast). Когда игрок начинает разговор с NPC (с любым
исходом: предложение квеста, сдача, даже праздная реплика-выкрик), компонент диалога сообщает «поговорил с
`<SpeakerKey>`» для взаимодействующего игрока, и каждая подходящая **активная** цель TalkTo продвигается. Так вы
бесплатно получаете гарантию порядка: разговор с NPC *до* принятия квеста ничего не засчитывает. Замечания:
сравнение регистронезависимо; засчитывается только говорящему игроку (никогда не всему серверу); повторные
разговоры безвредны; затвор `location:` на этом канале НЕ применяется (если он нужен, используйте
зарегистрированный тег в качестве цели и сообщайте о событии сами через `ReportObjectiveEvent`). NPC обязан нести
компонент диалогового актёра с этим ключом спикера; у размещённого на уровне члена каста или актёра, привязанного
через Cast, он уже есть.

```yaml
- id: Goal
  objective:
    type: TalkTo
    target: Mary            # ключ спикера; регистрация тега не нужна
    count: 1
    desc: "Поговорить с Мэри"
```

Цель `Deliver` моделирует классический шаг «доложи квестодателю». Она **удовлетворяется самим актом сдачи**: она
никогда не блокирует сдачу (квестодатель всё равно предложит её, как только выполнены запирающие цели
Kill/Collect и прочие) и автоматически выполняется при сдаче квеста (чтобы в трекере стояла галочка). Ставьте её
на шаг сдачи рядом с затвором `require`; ни события, ни `target` не нужно (дайте ей `desc` вроде «Сказать Брану,
что с волками покончено»).

Всё, что не совпадает со словами выше, становится `Custom`. Для счётных целей с тегами (kill/collect) используйте
путь событий: игра вызывает `ReportObjectiveEvent(instigator, type, matchTags, locationTags, sourceId)`, и цель
инкрементируется, когда совпадает `type` И `target` содержится в `matchTags` (иерархически) И, если задано,
`location` есть в тегах местоположения события. Для сюжетных моментов (talk/reach) часто проще запереть `require`
следующего шага и вызвать `CompleteObjective` из триггера или диалога.

Сопоставление `target` **иерархическое**: цель с `target: Enemy.Wolf` засчитывается событием с тегом
`Enemy.Wolf.Alpha`. Пользуйтесь этим вместо перечисления всех подтипов.

---

## 10. GameplayTags (КРИТИЧНО)

Каждое значение `target`, `location`, `require`/`grant` у варианта ответа, `tag` у условия и `grant_tag` у
эффекта — это **GameplayTag**. Тег обязан быть уже **зарегистрирован** в проекте, иначе он превращается в
недействительный тег и молча игнорируется (цель никогда не совпадает, условие никогда не срабатывает).
**Исключение:** `target` у цели `TalkTo` — это ключ спикера, а не тег (§9), так что регистрация не нужна.

Зарегистрировать теги можно двумя способами:
1. **Нативно в C++** (предпочтительно для поставляемых тегов): `UE_DEFINE_GAMEPLAY_TAG_COMMENT(...)`. Демо
   регистрирует `QuestwrightDemo.Enemy.Wolf` и `QuestwrightDemo.Area.FarmOutskirts` именно так.
2. **Project Settings → Project → GameplayTags** (пишет `Config/DefaultGameplayTags.ini`).

Соглашения, принятые в демо: идентичность врага `QuestwrightDemo.Enemy.<Name>`, области
`QuestwrightDemo.Area.<Name>`. Используйте иерархию через точки — иерархическое сопоставление достанется даром.

**Если вы пишете квест, использующий новый тег, вы обязаны также сказать человеку зарегистрировать этот тег**
(иначе квест не заработает). Заявляйте это в своём ответе явно.

---

## 11. Условия (`available_when` и `require` у шага)

Условие — небольшой объект. Пустое или отсутствующее = всегда истинно. Используется для предусловий квеста
(цепочки) и для затворов входа в шаг.

### Атомарные виды
| Форма | Смысл |
|---|---|
| `{ quest: <QuestId>, state: <quest-state> }` | другой квест находится в состоянии |
| `{ objective: <ObjId>, state: <obj-state> }` | цель находится в состоянии |
| `{ flag: <Name>, equals: <Value> }` | переменная квеста в рамках прохождения равна значению (ставится эффектами) |
| `{ tag: <GameplayTag> }` | игрок владеет геймплейным тегом |
| `{ item: <ItemId>, min: <N> }` | в инвентаре ≥N предметов (через поставщика инвентаря игры) |
| `{ attribute: <AttrId>, min: <N> }` | характеристика/атрибут ≥N (через поставщика статов игры) |

Значения `quest-state`: `available`, `active`, `completed` (псевдоним `complete`), `failed`, `declined`.
Значения `obj-state`: `active`, `complete` (псевдоним `completed`), `failed`, `inactive`.

### Составные виды
| Форма | Смысл |
|---|---|
| `{ all: [ <cond>, <cond>, ... ] }` | И |
| `{ any: [ <cond>, <cond>, ... ] }` | ИЛИ |
| `{ not: <cond> }` | НЕ |

Примеры:
```yaml
available_when: { quest: Farmlands.NightVisitors, state: completed }   # цепочка: только после того квеста
available_when: { quest: Farmlands.MarysErrand, state: active }        # побочный квест, открывающийся ПО ХОДУ сюжета:
                                                                       # предлагается, пока другой квест принят, но ещё
                                                                       # не сдан (передачи вида «сходи поговори с X»)
require: { objective: Goal, state: complete }                          # запереть сдачу на цели (обобщённый id "Goal")
available_when:
  all:
    - { quest: Region.Intro, state: completed }
    - { not: { flag: Betrayed, equals: "true" } }
```

Распознаются только перечисленные выше ключи. Объект условия без распознанного ключа → ошибка
`unknown_condition`.

---

## 12. Эффекты (`on_end:` у сцены)

Применяются при выходе из сцены. Структурная (рекомендуемая) форма — словарь:

```yaml
on_end:
  quest: accept                 # действие над состоянием квеста (см. ниже)
  set: { RewardTier: Negotiated }  # запись переменных квеста (флагов), читаемых условиями `flag`
  grant_tag: Region.MetBran     # выдать игроку геймплейный тег(и) (строка или список)
  grant_quest: Region.FollowUp  # принять квест(ы)-продолжение; так продвигаются цепочки (строка или список)
```

### Глаголы действия `quest:`
`accept`, `advance`, `complete`, `decline`, `fail`, `turnin` (псевдоним `turn_in`).
- `accept` принимает этот квест. `turnin` сдаёт его (награды + завершение). `complete` — безусловное завершение.
  `decline` / `fail` закрывают его. `advance` — **пустая операция в рантайме** (поток шагов ведёт граф диалога,
  а не эффекты), так что не полагайтесь на него.

Сцена сдачи обычно заканчивается на `on_end: { quest: turnin }` и `end: completed`.

Устаревшие строковые формы (`on_end: "quest.accept"`, `on_end: ["quest.accept", "set X=Y"]`) всё ещё разбираются,
но предпочтительнее структурный словарь выше.

### Ветвление сдачи (или любой сцены квестодателя) по флагу

Чтобы квестодатель говорил разное в зависимости от более раннего выбора, сочетайте `set` с флагом и шаг,
запертый по флагу. Поставьте флаг в ранней сцене, а затем напишите **два** шага сдачи: сначала частный
(запертый флагом), а универсальный — последним:

```yaml
  # ранняя ветка фиксирует выбор
  - id: Haggle
    scene: { ... }
    on_end: { quest: accept, set: { RewardTier: Negotiated } }

  # сдача, частная ветка; ОБЯЗАНА идти раньше универсальной
  - id: DeliverHaggled
    require:
      all:
        - { objective: Goal, state: complete }
        - { flag: RewardTier, equals: Negotiated }
    scene: { ... }                 # реплики с учётом торга
    on_end: { quest: turnin }
    end: completed

  # сдача, ветка по умолчанию; сюда попадаем, когда флаг не установлен
  - id: Deliver
    require: { objective: Goal, state: complete }
    objective: { type: Deliver, count: 1, desc: "..." }   # отслеживаемая цель живёт здесь
    scene: { ... }
    on_end: { quest: turnin }
    end: completed
```

Маршрутизация квестодателя выбирает **первую** сцену сдачи, чей `require` проходит, поэтому располагайте шаги от
самого частного к общему. Отслеживаемая цель `Deliver` нужна только универсальной ветке; запертые ветки состоят
из одной сцены и делят эту единственную строчку трекера. Тот же приём работает для любого условия по
флагу/квесту/цели/тегу (§11), не только по флагам. Живой пример — `Docs/Samples/NightVisitors.quest.yaml`
(торг → другой диалог при выплате).

---

## 13. Цепочки квестов

Цепочка — это просто данные: квест B начинается после квеста A через:
- `available_when: { quest: A, state: completed }` у B (предусловие) и/или
- `on_end: { ..., grant_quest: B }` у A (автопринятие продолжения по завершении).

`priority` упорядочивает несколько квестов, предлагаемых одним квестодателем (выше — первым). Интерфейс
«New Chain» в Studio пишет эти значения `available_when`/`priority` за вас, но написать их вручную — то же самое.

Валидация ловит `chain_cycle` (A требует B, B требует A), `dangling_quest_ref` (ссылка на неизвестный QuestId) и
`dangling_objective_ref` (условие ссылается на отсутствующий идентификатор цели). Убедитесь, что упоминаемые
идентификаторы квестов и целей существуют.

---

## 14. Реплики-выкрики (barks) и праздные выкрики

**Фоновые реплики-выкрики** (`barks:`) — случайные фразы, которые квестодатель произносит у себя над головой по
таймеру, до и вокруг разговора. **Праздные выкрики** (`idle_barks:`) произносятся при взаимодействии, когда
квестодателю больше нечего предложить или принять (например, завершённый неповторяемый квест). И те и другие
озвучиваются голосом **квестодателя** через тот же конвейер VO и лицевой анимации.

```yaml
barks:
  interval: [8, 16]            # секунд между попытками [min, max]
  lines:
    - "Опять волки воют..."
    - { text: "После заката во двор никто и носа не сунет.", weight: 2 }   # weight смещает случайный выбор

idle_barks:
  lines:
    - "Не сейчас, друг. Больше мне нечего тебе дать."
    - "Спи спокойно. Ферма в безопасности, благодаря тебе."
```

Реплика-выкрик — это обычная строка либо `{ text, weight, loc }`. Идентификаторы назначаются автоматически
(`Bark_NNN_<Giver>` / `IdleBark_NNN_<Giver>`). У праздных выкриков нет `interval` (они срабатывают на
взаимодействие). Фоновые квестовые выкрики прекращаются, как только квест завершён.

---

## 15. Награды

Работает только `base`. `bonus` и `rep` (репутация) **НЕ потребляются**, так что не полагайтесь на них.

```yaml
rewards:
  base:
    currency: 20
    xp: 50
    items:
      - { def: SmokedMeat, count: 1 }     # def = идентификатор предмета, count >= 1
```

Поддерживаются: `currency` (целое), `xp` (целое), `items` (список `{def, count}`). `def` предмета — это
идентификатор предмета в вашей игре (сам предмет выдаёт игра; Questwright лишь фиксирует награду).

---

## 16. Политика камеры

```yaml
camera:
  cu_run_threshold: 2     # крупный план после стольких подряд идущих реплик одного спикера (по умолчанию 3)
  establishing: true      # общий план для завязки на первой реплике сцены (по умолчанию true)
  choice_shot: wide       # кадр во время ожидания выбора игрока (wide|ots|cu или id из staging.cameras; по умолчанию wide)
```

Всё необязательно; опустите блок ради разумных значений по умолчанию. Заданный у реплики `shot:` переопределяет
это для неё. `choice_shot` также принимает идентификатор пользовательской камерной метки (§8b): это камера,
на которую сцена переключается, пока разворачиваются варианты ответа.

---

## 17. Идентификаторы, стабильность и локализация

- `source_culture` — язык, на котором вы пишете (например, `en`). Переводы живут в сопутствующих CSV
  (`Loc/<Quest>.<culture>.csv`) с ключами по идентификаторам; непереведённые ключи откатываются к исходному тексту.
- **Стабильные идентификаторы важны:** ключи локализации и имена ассетов VO и лицевой анимации выводятся из `by`
  и `id` реплики, `id` варианта ответа, `id` цели. Если вы вставляете реплику в середину, дайте ей новый `id`;
  НЕ перенумеровывайте остальные, иначе сломаете их переводы и озвучку. Именно поэтому каждая реплика, вариант
  ответа и цель должны нести явный `id`.
- Эхо-реплики (реплика игрока, повторяющая текст варианта ответа) используют `echo_of: <choiceId>`, чтобы делить
  ключ локализации варианта, а не создавать дубликат.

### Текстовые токены (динамические значения вроде имени игрока)

Любая реплика, вариант ответа, `description` цели или `name` квеста могут содержать подстановки `{Token}`. В
момент показа рантайм подставляет вместо них текстовые переменные локального игрока через `FText::Format`
(корректный для локализации способ: токен переживает перевод, и каждый язык держит `{PlayerName}` в своём
порядке слов). Неизвестные токены остаются как есть, так что опечатка никогда не обнуляет текст.

- `{PlayerName}`: отображаемое имя игрока. По умолчанию «Adventurer», пока игра не задаст своё.
- Пользовательские токены: любое имя, заданное через `UQuestwrightTextVarsSubsystem::SetTextVar(Key, Value)`.

```yaml
- { by: Bran, id: greet, text: "Так вот ты какой, {PlayerName}. Подойди ближе.", emotion: neutral }
```

Значения игра поставляет из C++ (`UQuestwrightTextVarsSubsystem::Get(this)->SetPlayerName(...)`) или из
Blueprint (*Get Game Instance Subsystem → Questwright Text Vars → Set Player Name*). Значения существуют только
для локального отображения и никогда не реплицируются; в области видимости только собственное имя локального игрока.

---

## 18. Чек-лист валидации (чтобы квест прошёл с 0 ошибок)

Валидатор пометит это, так что избегайте заранее:
- `invalid_emotion`: эмоция не из §7 (включая псевдонимы).
- `invalid_shot`: кадр не является ни базовым токеном (§8), ни объявленным идентификатором из `staging.cameras` (§8b).
- `camera_missing_id` / `camera_missing_class` / `camera_invalid_pos`: у камерной метки нет обязательных
  полей (§8b).
- `duplicate_camera_id` / `camera_mark_id_collision`: идентификаторы камер должны быть уникальны И не сталкиваться
  с идентификаторами меток постановки (общее пространство имён).
- `camera_lookat_angles_conflict`: у одной камеры одновременно `look_at` и ручные `yaw`/`pitch` (взаимоисключающи).
- `unknown_camera_class` / `ambiguous_camera_class`: токен `class` разрешается в ноль / несколько
  классов; используйте полный путь объекта, чтобы снять неоднозначность.
- `camera_lookat_unknown`: `look_at` не называет ни якорь, ни участника, ни метку постановки.
- `camera_unused` (предупреждение): объявленная камера, которую не использует ни одна реплика и ни один `choice_shot`.
- `mark_missing_id` / `duplicate_mark_id` / `mark_invalid_pos`: у метки постановки нет обязательных
  полей (§8a).
- `mark_yaw_face_conflict`: у одной метки одновременно `face` и ручной `yaw` (взаимоисключающи).
- `mark_invalid_appear` / `mark_invalid_keep` / `mark_invalid_source`: значения вне
  `placed|arrive` / логического / `existing|spawn`.
- `stage_missing_who` / `stage_missing_to` / `stage_invalid_approach` / `broken_stage_target` /
  `stage_who_unknown`: некорректная запись `line.stage` или ссылка на отсутствующую метку либо участника.
- `mark_occupancy_conflict` / `staging_marks_overlap`: двое участников на одной метке в один момент /
  метки поставлены друг на друга.
- `unknown_condition`: условие без распознанного ключа (§11).
- `unknown_effect`: эффект без распознанного глагола (§12).
- `unstable_line_id`: реплика без `id`, когда задан `source_culture` (давайте `id` каждой реплике).
- `objective_no_id`: цель без `id`.
- `dangling_quest_ref` / `dangling_objective_ref` / `chain_cycle`: ссылки на отсутствующие квесты и цели
  либо круговая цепочка (§13).
- `dangling_goto` / `broken_goto`: вариант ответа или `next` указывает на несуществующий идентификатор шага.
- Незарегистрированные GameplayTags не дают ошибки, но **молча ломают** сопоставление, поэтому проверяйте
  регистрацию каждого тега (§10).

---

## 19. Полный пример с комментариями

```yaml
quest: Farmlands.NightVisitors
name: "Ночные гости"
giver: Bran
giver_display_name: "Бран"
source_culture: en
schema_version: 4
priority: 10
rewards:
  base: { currency: 20, items: [ { def: SmokedMeat, count: 1 } ] }
camera: { cu_run_threshold: 2, establishing: true, choice_shot: wide }

barks:
  interval: [8, 16]
  lines:
    - "Опять волки воют... дурная примета."
    - { text: "Обнаглели волки, окаянные.", weight: 2 }

idle_barks:
  lines:
    - "Не сейчас, друг. Больше мне нечего тебе дать."

steps:
  - id: NightVisitors_Offer
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Bran,   id: greet,  text: "Эй. Ты. Подойди-ка ближе.", emotion: angry }
        - { by: Player, id: what,   text: "Что случилось?" }
        - { by: Bran,   id: wolves, text: "Волки. Ходят за моей скотиной.", emotion: sad }
      choices:
        - { id: help,   text: "Помогу. Где эти волки?", goto: NightVisitors_Accept }
        - { id: refuse, text: "Не моя забота, старик.", goto: NightVisitors_Refuse }

  - id: NightVisitors_Accept
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Player, id: help_echo, text: "Помогу. Где эти волки?", echo_of: help }
        - { by: Bran,   id: where,     text: "За фермой, у самой кромки леса.", emotion: worried }
      on_end: { quest: accept }
    next: Goal

  - id: NightVisitors_Refuse
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Player, id: refuse_echo, text: "Не моя забота, старик.", echo_of: refuse }
        - { by: Bran,   id: warn,        text: "Завтра эти твари выйдут на дорогу.", emotion: angry, shot: cu }
      on_end: { quest: decline }
    end: declined

  # Шаг цели: обобщённая секция "Goal" (вид указан в `type`); id цели по умолчанию — "Goal".
  - id: Goal
    objective:
      type: Kill
      target: QuestwrightDemo.Enemy.Wolf
      count: 3
      location: QuestwrightDemo.Area.FarmOutskirts
      desc: "Убить волков у фермы"
    next: Deliver

  # Сдача = шаг Deliver, запертый на цели Goal.
  - id: Deliver
    require: { objective: Goal, state: complete }
    objective:
      type: Deliver                                   # выполняется самой сдачей; никогда её не запирает
      count: 1
      desc: "Сказать Брану, что с волками покончено"
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Player, id: done,   text: "С волками покончено." }
        - { by: Bran,   id: relief, text: "Хорошо. Моя скотина доживёт до утра.", emotion: happy }
      on_end: { quest: turnin }
    end: completed
```

---

## 20. НАДО / НЕ НАДО

**НАДО**
- Держать `quest`, `steps` (≥1) и хотя бы один терминальный шаг (`end:`).
- Давать каждой реплике / варианту ответа / цели стабильный `id` строчными буквами.
- Использовать только те эмоции, кадры, типы, виды условий и глаголы эффектов, что есть в этом руководстве.
- Использовать иерархические через точку, **зарегистрированные** GameplayTags для `target`/`location`/`tag`/`grant_tag`
  и говорить человеку зарегистрировать каждый новый тег.
- Запирать шаг сдачи на выполнении цели.

**НЕ НАДО**
- Не выдумывайте поля верхнего уровня (`description`, `level`, `zone`, `tags`, `category`, `npc`, `dialogue`, …).
- Не используйте эмоции/кадры/типы вне списков (никаких `confused`, `excited`, `disgusted`; никаких `pan`, `zoom`).
- Не полагайтесь на `rewards.bonus`, `rewards.rep` и действие квеста `advance` — они не подключены.
- Не переиспользуйте и не перенумеровывайте идентификаторы при вставке контента; добавляйте новые, а
  существующие не трогайте.
- Не ссылайтесь на несуществующие идентификаторы квестов, целей и шагов.
- Не считайте, что GameplayTag работает без регистрации.
```
