﻿# Questwright: Quest Authoring Guide (for AI agents)

**Read this fully before writing any `.quest.yaml`.** It describes EXACTLY what the Questwright
compiler/validator supports. Do **not** invent fields, emotions, shot names, condition kinds, effect
verbs, or objective types that are not listed here: anything unknown is either dropped silently or
flagged as a validation error. When in doubt, prefer a field/value that appears verbatim below.

The authoritative parser is `QuestwrightYamlNormalizer.cpp`; the validator is `ValidateSchema`. This guide
mirrors them. If something here conflicts with an older spec doc, **the code (this guide) wins**.

---

## 0. Golden rules (TL;DR)

1. A quest is a single YAML file. The YAML is the **source of truth**; the Studio compiles it into assets.
   You author YAML; you never edit generated `.uasset`s.
2. Only use the fields, emotions, shots, objective types, condition kinds and effect verbs listed here.
3. Every `target` / `location` / `tag` / `grant_tag` value is a **GameplayTag that MUST be registered**
   (native C++ tag or `DefaultGameplayTags.ini`). An unregistered tag is silently invalid → the objective
   never matches / the condition never fires. This is the #1 cause of "my quest does nothing".
4. Give every spoken line, every choice and every objective a stable `id` (lowercase/underscore). Ids key
   the localization and the generated VO/face assets; reordering lines must not change other ids.
5. Step ids are auto-namespaced with the quest's short name. Reference steps in `goto`/`next` by their
   authored id (namespacing is handled for you, but matching the `<Short>_<id>` form is safest).
6. `schema_version` is optional and informational. The parser accepts any value, and features are
   keyed by the presence of their blocks (e.g. `staging:`), not the number. Current version is **4**
   (3 added staging, 4 added camera marks); older files declaring `2` parse unchanged.

---

## 1. How it works (pipeline)

```
<Quest>.quest.yaml  ──parse──►  IR  ──validate──►  (errors shown in Studio)
                                  │
                                  ├─ Resolve Audio (synthesize VO per culture, per speaker voice)
                                  ├─ Facial (bake lip-sync FA_<id> from the VO)
                                  ├─ Generate (DataAssets: DA_QW_Dialogue_*, DA_QW_Quest_*, String Tables)
                                  └─ Bind & Stage (assign cast to NPCs in the level)
```

You write the YAML; the Studio "Build → RUN" does the rest.

---

## 2. File location & naming

- Put author quests in **`<Project>/Content/Quests/`** (they appear in the Studio library automatically;
  no Import needed). `Docs/Samples/` is the SHIPPED demo folder; don't add your own quests there.
- File name: `<Name>.quest.yaml` (the `.quest.` is conventional; any `.yaml`/`.yml`/`.json` in a scan root
  is picked up). One quest per file.

---

## 3. Minimal skeleton

```yaml
quest: Region.MyQuest          # unique id/slug (REQUIRED)
name: "My Quest"
giver: Bran                    # speaker key of the NPC who gives it
source_culture: en
schema_version: 4
steps:                         # REQUIRED, at least 1
  - id: MyQuest_Offer
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Bran,   id: greet, text: "Hello.", emotion: neutral }
        - { by: Player, id: hi,    text: "Hi." }
      choices:
        - { id: accept, text: "I'll help.", goto: MyQuest_Do }
      on_end: { quest: accept }
  - id: MyQuest_Do
    end: completed
```

---

## 4. Top-level quest fields

| Field | Type | Required | Notes |
|---|---|---|---|
| `quest` | string | **yes** | Unique id/slug, e.g. `Farmlands.NightVisitors`. Used as the QuestId everywhere. |
| `name` | string | no | Display title (localizable). |
| `summary` | string | no | Short description for the journal. |
| `giver` | string | no | Speaker key of the giver NPC (e.g. `Bran`). Drives giver selection + bark voice. |
| `giver_display_name` | string | no | Friendly name shown in UI; falls back to `giver`. |
| `region_display_name` | string | no | Journal region grouping label. |
| `source_culture` | string | no | Authoring language, e.g. `en`. Other languages live in sidecar CSVs. |
| `schema_version` | int | no | Optional/informational; current is `4` (see §0 rule 6). |
| `priority` | int | no | Giver offer ordering: higher is offered first when an NPC has several quests. Default 0. |
| `repeatable` | bool | no | If true, a Completed quest can be accepted again (a "daily"). Default false. |
| `available_when` | condition | no | Prerequisite gate (see §11). Empty = available immediately. Drives chaining. |
| `rewards` | object | no | See §15. Only `base.{currency,xp,items}` are wired. |
| `camera` | object | no | Auto-camera policy. See §16. |
| `barks` | object | no | Ambient giver lines on a timer. See §14. |
| `idle_barks` | object/list | no | Lines spoken on interact when the giver has nothing to offer. See §14. |
| `steps` | list | **yes** | The quest body, ≥1 step. See §5. |

> There is NO top-level `description`, `tags`, `category`, `type`, `level`, `zone`, `objectives` (objectives
> live inside steps), `dialogue`, or `npc` field. Don't invent them.

---

## 5. Steps

A step is one node of the quest (a dialogue scene, an objective, or a terminal marker).

| Field | Type | Notes |
|---|---|---|
| `id` | string | **Required.** Stable id; auto-namespaced to `<ShortQuest>_<id>` if not already prefixed. |
| `title` | string | Optional internal label. |
| `scene` | object | A dialogue scene (lines + choices). See §6. |
| `objective` | object | A trackable goal (kill/collect/reach/talk/deliver). See §9. |
| `next` | string | Default transition: id of the next step (when the scene has no choices). |
| `end` | string | Terminal marker. Non-empty closes the dialogue. Conventional values: `completed`, `declined`, `failed`. |
| `require` | condition OR string | Entry gate for this step (see §11). E.g. gate the turn-in step on the objective being complete. |

A step usually has EITHER a `scene` (talk) OR an `objective` (do a thing), plus a `next`/`end`.

---

## 6. Scenes: lines & choices

```yaml
scene:
  speakers: [Bran, Mira, Player]   # ALL speaker keys present in this scene (2, 3, or more)
  lines:
    - { by: Bran,   id: greet, text: "Come here.", emotion: worried, shot: cu }
    - { by: Mira,   id: warn,  text: "Don't trust him.", to: Player }
    - { by: Player, id: what,  text: "What's wrong?" }
  choices:
    - { id: help,   text: "I'll help.",      goto: MyQuest_Accept }
    - { id: refuse, text: "Not my problem.", goto: MyQuest_Refuse }
  on_end: { quest: accept }      # effect applied when the scene exits (see §12)
```

**Multiple characters.** A scene supports any number of speakers; just list every key in `speakers` and
set each line's `by`. To add a second/third character, add its key (e.g. `Mira`) to `speakers` and use it in
`by`. At runtime each key is bound to an actor already placed in the level with that speaker key; if none
is present, one is spawned from the speaker map (`DT_QW_Speakers`, written by the Cast stage) and staged
beside the giver. Spawned characters are removed automatically when the dialogue ends.

### Line fields
| Field | Type | Notes |
|---|---|---|
| `by` | string | **Required.** Speaker key (must be in `speakers`). VO/face play on this participant. |
| `to` | string | *Optional.* Speaker key this line is addressed **to**; it drives the over-the-shoulder/close-up framing and head-turn in 3+ speaker scenes. Omit for 2-way scenes (the other party is inferred: previous speaker, else the player). |
| `id` | string | Stable line id (lowercase/underscore). Strongly recommended, since it keys VO/face/loc. |
| `text` | string | The spoken text (localizable). |
| `emotion` | enum | See §7. Drives the additive face + VO tone. Default `neutral`. |
| `shot` | enum | See §8. Default `auto` (compiler chooses). |
| `echo_of` | string | This line repeats a choice's text; give the choice's `id`. Shares its loc key (don't duplicate text keys). |
| `loc` | string | Explicit loc key override (rarely needed). |

### Choice fields
| Field | Type | Notes |
|---|---|---|
| `id` | string | Stable choice id. Keys the localized choice text. |
| `text` | string | Choice label. |
| `goto` | string | **Required.** Target step id. |
| `require` | tag or list | GameplayTag(s) the player must own for the choice to be available. (Tags, NOT structural conditions.) |
| `grant` | tag or list | GameplayTag(s) granted when the choice is picked. |

> The full LineId is `<StepId>_<lineId>_<Speaker>`; that's how VO assets are named (e.g.
> `NightVisitors_Offer_greet_Bran`). You don't write that; it's derived from `by`+`id`+step.

---

## 7. Emotions (line `emotion:`)

Valid canonical values: **`neutral`, `happy`, `sad`, `angry`, `afraid`, `surprised`**.

Accepted aliases (all map to the canonical mood):
- `fear`, `scared`, `worried`, `nervous`, `anxious`  → **afraid**

Anything else is a **validation error** (`invalid_emotion`) and falls back to `neutral`. There is no
`worried` mood of its own, no `confused`, `disgusted`, `excited`, `bored`, etc. Pick the nearest of the
six (e.g. excited→happy, distressed→sad/afraid). If you need a new word, it must be added as an alias in
code first; do not author unknown emotion tokens.

---

## 8. Shots (line `shot:`)

Valid values: **`auto`, `wide`, `ots`** (aliases `over_shoulder`/`overshoulder`), **`cu`** (aliases
`close_up`/`closeup`), **`reaction`**, **or the id of a custom camera mark** declared in
`staging.cameras` (§8b).

Default is `auto`: the compiler auto-frames (establishing wide, then close-ups on consecutive lines from
one speaker). Only set `shot` to override a specific line. Unknown shot tokens (neither a base shot nor a
declared camera id) → `invalid_shot` error.

---

## 8a. Staging: participant placement & movement (`staging.marks`, `line.stage`)

An optional top-level `staging:` block places the scene's participants around an **anchor** (top-down,
cm), and `line.stage` moves them mid-dialogue. Without the block every speaker is simply placed facing
their interlocutor; staging is only needed for multi-NPC blocking, spawned extras and movement.

```yaml
staging:
  anchor: DialogueMarker_Barn   # a placed BP_QW_DialogueMarker; omit → the giver's transform
  settle_timeout: 4.0           # seconds a walk move may take before falling back to teleport
  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 }   # waypoint for a mid-dialogue move
```

### Mark fields
| Field | Notes |
|---|---|
| `id` | **Required**, stable (`[A-Za-z0-9_]+`). `line.stage.to` and cameras reference marks by id. Shares ONE namespace with camera mark ids (§8b); a collision is an error. |
| `actor` | Speaker key (from `speakers`/Cast). Omit for a pure waypoint. |
| `pos` | **Required** `[x, y]` cm, anchor-relative ground plane. Z is ground-snapped at runtime. |
| `face` | Auto-turn towards `anchor` \| a speaker key \| a mark id. **Mutually exclusive with `yaw`.** Facing away from the anchor is legal (info, not an error). |
| `yaw` | Manual body angle in degrees (instead of `face`). |
| `appear` | `placed` (the default: standing there at scene start) \| `arrive` (spawns out of the player's view and walks to the mark; its first line waits for arrival). |
| `keep` | Default `false`, meaning a spawned participant despawns when the dialogue ends; `true` keeps it. |
| `source` | `existing` \| `spawn`. Normally omit, since it is resolved from Cast (the Player and already-placed actors are *moved*, everyone else is *spawned*). |

### Mid-dialogue transitions (`line.stage`)
```yaml
  - { by: Mira, id: step, text: "Let me see the tracks.",
      stage: [ { who: Mira, to: m2, approach: walk } ] }
```
`who` = participant, `to` = a declared mark id, `approach` = `walk` (default; blocking: the line waits
for arrival, with a teleport fallback after `settle_timeout`) | `teleport`. One mark holds **one** participant
at a time; occupancy is tracked line-by-line, so two people on the same mark at the same moment is a
validation error.

---

## 8b. Custom camera marks (`staging.cameras`)

Scene-relative cameras placed on the staging canvas. The reusable optics (lens/DOF/filmback) live in a
**camera class** (any Blueprint subclass of `QuestwrightDialogueSceneCamera`); the mark is an instance:
a stable `id` + anchor-relative position + aiming. Lines reference the **mark id** via `shot:`, never a
class or coordinates. The auto grammar never picks custom marks; they are explicit per-line (or
`choice_shot`) overrides only.

```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 }

# on a line:            shot: cam_window
# while choices unfold: camera: { choice_shot: cam_high }
```

Fields: `id` (required, `[A-Za-z0-9_]+`, shares ONE namespace with staging mark ids; a collision is an
error), `class` (required: the built-in token `dynamic`, a short BP name unique across the project, or
a full object path), `pos` (required `[x, y]` cm anchor-relative), `height` (cm above the anchor ground,
default 160; cameras are never ground-snapped, so hanging in the air is legal), `look_at` (**xor**
`yaw`+`pitch`) aims at `anchor` | a speaker key | a staging mark id; it is resolved at the moment of the cut,
so it follows mid-dialogue `stage:` moves. Manual `yaw` is measured from the anchor forward. `fov`
(degrees) optionally overrides the class optics.

`class: dynamic` needs **no Blueprint at all**: the camera is created at dialogue time from the native
C++ base with default optics (24mm, no DOF). Combine it with `fov:` for a quick custom angle; subclass
`QuestwrightDialogueSceneCamera` in BP only when you want to tune lens/DOF/filmback.

If the camera class fails to load at runtime, the line degrades to a baked `wide` fallback with a
warning, so the scene never breaks.

---

## 9. Objectives (step `objective:`)

A trackable goal lives inside a step. **A structural step (one that carries an `objective`) uses a generic
section id, not a content name**: `Goal` for an objective, `Deliver` for a turn-in. The concrete kind lives in
the objective's `type` (Kill/Collect/…), which you can change without re-keying. Never hand-name them
("KillWolves", "TurnIn"). The objective's own id defaults to the section id, and `next`/`goto`/`require`
reference it. Generic for every quest. (You may also omit the step `id`, in which case the compiler derives
`Goal`/`Deliver` from the objective, but writing `id: Goal` is clearest.)

```yaml
- id: Goal                                          # generic section; the KIND is in `type` below
  objective:
    type: Kill                                      # the objective kind (see table); id defaults to "Goal"
    target: QuestwrightDemo.Enemy.Wolf              # GameplayTag; MUST be registered (§10)
    count: 3                                        # how many events to complete (>=1)
    location: QuestwrightDemo.Area.FarmOutskirts    # optional GameplayTag; only counts in this area
    desc: "Kill the wolves near the farm"           # journal/tracker text (localizable)
  next: Deliver                                     # references the turn-in (Deliver) step by its type
```

(If you need two objectives of the same type in one quest, give them distinct explicit ids.)

### Objective `type` (the event channel)
| Type | Meaning | How it's credited |
|---|---|---|
| `Kill` | kill N tagged enemies | game reports a Kill event on death (`ReportObjectiveEvent`) |
| `Collect` | gather N tagged items | game reports a Collect event on pickup |
| `Reach` | reach a tagged place | game reports a Reach event / or `CompleteObjective` from a trigger |
| `TalkTo` (`talk`) | talk to someone | **automatic**: starting a conversation with the NPC credits it (see below) |
| `Deliver` | return to / deliver to the giver | completed BY the turn-in itself (see note below) |
| `Custom` | any custom event | game reports `Custom` + your own `target` tag |

A `TalkTo` objective is keyed to a **speaker key, not a GameplayTag**: write the character's key as the
target (`target: Mary`, the same key used in `speakers:` and the Cast tab). When the player starts a
conversation with an NPC (any outcome: offer, turn-in, even an idle bark), the dialogue component reports
"talked to `<SpeakerKey>`" for the interacting player, and every matching **active** TalkTo objective
advances. That gives you the ordering guarantee for free: talking to the NPC *before* accepting the quest
credits nothing. Notes: matching is case-insensitive; only the talking player is credited (never the whole
server); repeat conversations are harmless; a `location:` gate is NOT applied on this channel (if you need
one, use a registered tag as the target and report the event yourself via `ReportObjectiveEvent`). The NPC
must carry a dialogue actor component with that speaker key; a level-placed cast member or a Cast-bound
actor already does.

```yaml
- id: Goal
  objective:
    type: TalkTo
    target: Mary            # speaker key; no tag registration needed
    count: 1
    desc: "Talk to Mary"
```

A `Deliver` objective models the classic "report back to the quest giver" step. It is **satisfied by the
turn-in act**: it never blocks turn-in (the giver still offers it once the gating Kill/Collect/etc. objectives
are done), and it is auto-completed when the quest is turned in (so the tracker shows it ticked). Put it on the
turn-in step alongside the `require` gate; no event/`target` is needed (give it a `desc` like "Tell Bran the
wolves are dealt with").

Anything other than the words above becomes `Custom`. For counted, tagged goals (kill/collect) use the
event path: the game calls `ReportObjectiveEvent(instigator, type, matchTags, locationTags, sourceId)` and the
objective increments when `type` matches AND `target` is contained in `matchTags` (hierarchically) AND, if
set, `location` is in the event's location tags. For story beats (talk/reach) it's often simpler to gate the
next step's `require` and call `CompleteObjective` from a trigger/dialogue.

`target` matching is **hierarchical**: an objective with `target: Enemy.Wolf` is credited by an event tagged
`Enemy.Wolf.Alpha`. Use this instead of listing every subtype.

---

## 10. GameplayTags (CRITICAL)

Every `target`, `location`, choice `require`/`grant`, condition `tag`, and effect `grant_tag` value is a
**GameplayTag**. The tag must already be **registered** in the project, or it resolves to an invalid tag and
is silently ignored (objective never matches, condition never fires). **Exception:** a `TalkTo` objective's
`target` is a speaker key, not a tag (§9), so no registration is needed.

Register tags one of two ways:
1. **Native C++** (preferred for shipped tags): `UE_DEFINE_GAMEPLAY_TAG_COMMENT(...)`. The demo registers
   `QuestwrightDemo.Enemy.Wolf` and `QuestwrightDemo.Area.FarmOutskirts` this way.
2. **Project Settings → Project → GameplayTags** (writes `Config/DefaultGameplayTags.ini`).

Conventions used by the demo: enemy identity `QuestwrightDemo.Enemy.<Name>`, areas
`QuestwrightDemo.Area.<Name>`. Use a dotted hierarchy so you get hierarchical matching for free.

**If you author a quest that uses a new tag, you must also tell the human to register that tag** (or the
quest won't function). State this explicitly in your output.

---

## 11. Conditions (`available_when` and step `require`)

A condition is a small object. Empty/absent = always true. Used for quest prerequisites (chaining) and step
entry gates.

### Atomic kinds
| Form | Meaning |
|---|---|
| `{ quest: <QuestId>, state: <quest-state> }` | another quest is in a state |
| `{ objective: <ObjId>, state: <obj-state> }` | an objective is in a state |
| `{ flag: <Name>, equals: <Value> }` | a per-playthrough quest variable equals a value (set by effects) |
| `{ tag: <GameplayTag> }` | the player owns a gameplay tag |
| `{ item: <ItemId>, min: <N> }` | inventory has ≥N of an item (via the game's inventory provider) |
| `{ attribute: <AttrId>, min: <N> }` | a stat/attribute is ≥N (via the game's stat provider) |

`quest-state` values: `available`, `active`, `completed` (alias `complete`), `failed`, `declined`.
`obj-state` values: `active`, `complete` (alias `completed`), `failed`, `inactive`.

### Composite kinds
| Form | Meaning |
|---|---|
| `{ all: [ <cond>, <cond>, ... ] }` | AND |
| `{ any: [ <cond>, <cond>, ... ] }` | OR |
| `{ not: <cond> }` | NOT |

Examples:
```yaml
available_when: { quest: Farmlands.NightVisitors, state: completed }   # chain: only after that quest
available_when: { quest: Farmlands.MarysErrand, state: active }        # side quest that opens MID-story: offerable
                                                                       # while the other quest is accepted but not
                                                                       # yet turned in ("go talk to X" hand-offs)
require: { objective: Goal, state: complete }                          # gate turn-in on the objective (generic id "Goal")
available_when:
  all:
    - { quest: Region.Intro, state: completed }
    - { not: { flag: Betrayed, equals: "true" } }
```

Only the keys above are recognized. A condition object with no recognized key → `unknown_condition` error.

---

## 12. Effects (scene `on_end:`)

Applied when a scene exits. Structural (recommended) form is a map:

```yaml
on_end:
  quest: accept                 # quest state action (see below)
  set: { RewardTier: Negotiated }  # write quest variables (flags), read by `flag` conditions
  grant_tag: Region.MetBran     # grant gameplay tag(s) to the player (string or list)
  grant_quest: Region.FollowUp  # accept follow-up quest(s); this is how chains advance (string or list)
```

### `quest:` action verbs
`accept`, `advance`, `complete`, `decline`, `fail`, `turnin` (alias `turn_in`).
- `accept` accepts this quest. `turnin` turns it in (rewards + complete). `complete` is an unconditional complete.
  `decline` / `fail` close it. `advance` is a **no-op at runtime** (step flow is driven by the dialogue graph,
  not effects), so don't rely on it.

A turn-in scene typically ends with `on_end: { quest: turnin }` and `end: completed`.

Legacy string forms (`on_end: "quest.accept"`, `on_end: ["quest.accept", "set X=Y"]`) still parse, but prefer
the structural map above.

### Branching the turn-in (or any giver scene) on a flag

To make the giver say something different depending on an earlier choice, combine a `set` flag with a
flag-gated step. Set the flag in the earlier scene, then author **two** turn-in steps: the specific
(flag-gated) one **first**, the catch-all last:

```yaml
  # earlier branch records the choice
  - id: Haggle
    scene: { ... }
    on_end: { quest: accept, set: { RewardTier: Negotiated } }

  # turn-in, specific branch; MUST come before the catch-all
  - id: DeliverHaggled
    require:
      all:
        - { objective: Goal, state: complete }
        - { flag: RewardTier, equals: Negotiated }
    scene: { ... }                 # the haggle-aware lines
    on_end: { quest: turnin }
    end: completed

  # turn-in, default branch; reached when the flag is unset
  - id: Deliver
    require: { objective: Goal, state: complete }
    objective: { type: Deliver, count: 1, desc: "..." }   # the tracked objective lives here
    scene: { ... }
    on_end: { quest: turnin }
    end: completed
```

The giver routing picks the **first** turn-in scene whose `require` passes, so order the steps most-specific
first. Only the catch-all needs the tracked `Deliver` objective; the gated branches are scene-only and share
that one tracker line. The same pattern works for any flag/quest/objective/tag condition (§11), not just flags.
See `Docs/Samples/NightVisitors.quest.yaml` for a live example (haggle → different pay-out dialogue).

---

## 13. Quest chains

A chain is just data: quest B starts after quest A via:
- B's `available_when: { quest: A, state: completed }` (prerequisite), and/or
- A's `on_end: { ..., grant_quest: B }` (auto-accept the follow-up on completion).

`priority` orders multiple quests offered by the same giver (higher first). The Studio "New Chain" UI writes
these `available_when`/`priority` values for you, but authoring them by hand is equivalent.

Validation catches `chain_cycle` (A requires B requires A), `dangling_quest_ref` (references an unknown
QuestId), and `dangling_objective_ref` (condition references a missing objective id). Make sure referenced
quest ids and objective ids exist.

---

## 14. Barks & idle barks

**Ambient barks** (`barks:`) are random lines the giver says over their head on a timer, before/around the
conversation. **Idle barks** (`idle_barks:`) are said on interact when the giver has nothing left to
offer/turn-in (e.g. a finished non-repeatable quest). Both are voiced in the **giver's** voice through the
same VO/face pipeline.

```yaml
barks:
  interval: [8, 16]            # seconds between attempts [min, max]
  lines:
    - "Wolves howling again..."
    - { text: "After dark, nobody sets foot in the yard.", weight: 2 }   # weight biases random pick

idle_barks:
  lines:
    - "Not now, friend. I've nothing more for you."
    - "Rest easy. The farm's safe, thanks to you."
```

A bark line is a plain string, or `{ text, weight, loc }`. Ids are auto-assigned (`Bark_NNN_<Giver>` /
`IdleBark_NNN_<Giver>`). Idle barks have no `interval` (they fire on interact). Ambient quest barks stop once
the quest is finished.

---

## 15. Rewards

Only `base` is wired. `bonus` and `rep` (reputation) are **NOT consumed**, so don't rely on them.

```yaml
rewards:
  base:
    currency: 20
    xp: 50
    items:
      - { def: SmokedMeat, count: 1 }     # def = item id, count >= 1
```

Supported: `currency` (int), `xp` (int), `items` (list of `{def, count}`). Item `def` is your game's item id
(the game grants the actual item; Questwright just records the reward).

---

## 16. Camera policy

```yaml
camera:
  cu_run_threshold: 2     # close-up after this many consecutive lines from one speaker (default 3)
  establishing: true      # establishing wide on the first line of a scene (default true)
  choice_shot: wide       # shot while waiting on the player's choice (wide|ots|cu or a staging.cameras id; default wide)
```

All optional; omit the block for sensible defaults. Per-line `shot:` overrides this for that line.
`choice_shot` also accepts a custom camera mark id (§8b): the camera the scene cuts to while the
options unfold.

---

## 17. Ids, stability & localization

- `source_culture` is the language you author in (e.g. `en`). Translations live in sidecar CSVs
  (`Loc/<Quest>.<culture>.csv`) keyed by id; untranslated keys fall back to the source text.
- **Stable ids matter:** loc keys and VO/face asset names derive from `by`+line `id`, choice `id`,
  objective `id`. If you insert a line in the middle, give it a new `id`; do NOT renumber others, or you
  break their translations/VO. This is why every line/choice/objective should carry an explicit `id`.
- Echo lines (a player line that repeats a choice's text) use `echo_of: <choiceId>` so they share the
  choice's loc key instead of creating a duplicate.

### Text tokens (dynamic values like the player's name)

Any line, choice, objective `description` or quest `name` may embed `{Token}` placeholders. At display
time the runtime substitutes them with the local player's text variables via `FText::Format` (the
loc-correct way: the token survives translation, so each language keeps `{PlayerName}` in its own word
order). Unknown tokens are left intact, so a typo never blanks the text.

- `{PlayerName}`: the player's display name. Defaults to "Adventurer" until the game sets it.
- Custom tokens: any name set through `UQuestwrightTextVarsSubsystem::SetTextVar(Key, Value)`.

```yaml
- { by: Bran, id: greet, text: "So you're {PlayerName}. Come closer.", emotion: neutral }
```

The game supplies values from C++ (`UQuestwrightTextVarsSubsystem::Get(this)->SetPlayerName(...)`) or
Blueprint (*Get Game Instance Subsystem → Questwright Text Vars → Set Player Name*). Values are local
presentation only and never replicated; only the local player's own name is in scope.

---

## 18. Validation checklist (make the quest pass with 0 errors)

The validator will flag these, so avoid them up front:
- `invalid_emotion`: emotion not in §7 (incl. aliases).
- `invalid_shot`: shot neither a base token (§8) nor a declared `staging.cameras` id (§8b).
- `camera_missing_id` / `camera_missing_class` / `camera_invalid_pos`: a camera mark lacks its
  required fields (§8b).
- `duplicate_camera_id` / `camera_mark_id_collision`: camera ids must be unique AND not collide with
  staging mark ids (one shared namespace).
- `camera_lookat_angles_conflict`: `look_at` and manual `yaw`/`pitch` on one camera (mutually exclusive).
- `unknown_camera_class` / `ambiguous_camera_class`: the `class` token resolves to zero / several
  classes; use a full object path to disambiguate.
- `camera_lookat_unknown`: `look_at` names no anchor/participant/staging mark.
- `camera_unused` (warning): a declared camera no line or `choice_shot` uses.
- `mark_missing_id` / `duplicate_mark_id` / `mark_invalid_pos`: a staging mark lacks its required
  fields (§8a).
- `mark_yaw_face_conflict`: `face` and manual `yaw` on one mark (mutually exclusive).
- `mark_invalid_appear` / `mark_invalid_keep` / `mark_invalid_source`: values outside
  `placed|arrive` / bool / `existing|spawn`.
- `stage_missing_who` / `stage_missing_to` / `stage_invalid_approach` / `broken_stage_target` /
  `stage_who_unknown`: a malformed `line.stage` entry or a reference to a missing mark/participant.
- `mark_occupancy_conflict` / `staging_marks_overlap`: two participants on one mark at the same
  moment / marks placed on top of each other.
- `unknown_condition`: a condition with no recognized key (§11).
- `unknown_effect`: an effect with no recognized verb (§12).
- `unstable_line_id`: a line without an `id` when `source_culture` is set (give every line an id).
- `objective_no_id`: an objective without an `id`.
- `dangling_quest_ref` / `dangling_objective_ref` / `chain_cycle`: references to missing quests/objectives,
  or a circular chain (§13).
- `dangling_goto` / `broken_goto`: a choice/next points at a step id that doesn't exist.
- Unregistered GameplayTags don't error but **silently break** matching, so verify every tag is registered (§10).

---

## 19. Complete annotated example

```yaml
quest: Farmlands.NightVisitors
name: "Night Visitors"
giver: Bran
giver_display_name: "Bran"
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:
    - "Wolves howling again... a bad omen."
    - { text: "The wolves have grown bold, the devils.", weight: 2 }

idle_barks:
  lines:
    - "Not now, friend. I've nothing more for you."

steps:
  - id: NightVisitors_Offer
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Bran,   id: greet,  text: "Hey. You. Come closer.", emotion: angry }
        - { by: Player, id: what,   text: "What's wrong?" }
        - { by: Bran,   id: wolves, text: "Wolves. They come for my animals.", emotion: sad }
      choices:
        - { id: help,   text: "I'll help. Where are the wolves?", goto: NightVisitors_Accept }
        - { id: refuse, text: "Not my problem, old man.",         goto: NightVisitors_Refuse }

  - id: NightVisitors_Accept
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Player, id: help_echo, text: "I'll help. Where are the wolves?", echo_of: help }
        - { by: Bran,   id: where,     text: "Behind the farm, at the woods' edge.", emotion: worried }
      on_end: { quest: accept }
    next: Goal

  - id: NightVisitors_Refuse
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Player, id: refuse_echo, text: "Not my problem, old man.", echo_of: refuse }
        - { by: Bran,   id: warn,        text: "Tomorrow those beasts hit the road.", emotion: angry, shot: cu }
      on_end: { quest: decline }
    end: declined

  # Objective step: generic section "Goal" (the kind is in `type`); objective id defaults to "Goal".
  - id: Goal
    objective:
      type: Kill
      target: QuestwrightDemo.Enemy.Wolf
      count: 3
      location: QuestwrightDemo.Area.FarmOutskirts
      desc: "Kill the wolves near the farm"
    next: Deliver

  # Turn-in = a Deliver step, gated on the Goal objective.
  - id: Deliver
    require: { objective: Goal, state: complete }
    objective:
      type: Deliver                                   # completed by the turn-in; never gates it
      count: 1
      desc: "Tell Bran the wolves are dealt with"
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Player, id: done,   text: "The wolves are dealt with." }
        - { by: Bran,   id: relief, text: "Good. My animals live to see the morning.", emotion: happy }
      on_end: { quest: turnin }
    end: completed
```

---

## 20. DO / DON'T

**DO**
- Keep `quest`, `steps` (≥1), and at least one terminal step (`end:`).
- Give every line / choice / objective a stable lowercase `id`.
- Use only the emotions, shots, types, condition kinds and effect verbs in this guide.
- Use dotted, **registered** GameplayTags for `target`/`location`/`tag`/`grant_tag`, and tell the human to
  register any new tag.
- Gate the turn-in step on the objective being complete.

**DON'T**
- Don't invent top-level fields (`description`, `level`, `zone`, `tags`, `category`, `npc`, `dialogue`, …).
- Don't use emotions/shots/types outside the lists (no `confused`, `excited`, `disgusted`; no `pan`, `zoom`).
- Don't rely on `rewards.bonus`, `rewards.rep`, or the `advance` quest action; they aren't wired.
- Don't reuse or renumber ids when inserting content; add new ids and leave existing ones untouched.
- Don't reference quest ids / objective ids / step ids that don't exist.
- Don't assume a GameplayTag works without registering it.
```
