# Questwright: handleiding voor het schrijven van quests (voor AI-agents)

*Dit is een vertaling voor het leesgemak. Het formaat zelf en alle veldnamen blijven Engels; wijkt deze tekst af van het origineel, dan is de Engelse versie (`/downloads/questwright-authoring-reference.md`) leidend.*

**Lees dit volledig voordat je ook maar één `.quest.yaml` schrijft.** Het beschrijft PRECIES wat de
compiler/validator van Questwright ondersteunt. Verzin **geen** velden, emotions, shot-namen, condition kinds,
effect verbs of objective types die hier niet staan: alles wat onbekend is, wordt óf stilzwijgend weggegooid óf
als validatiefout gemeld. Twijfel je, kies dan een veld/waarde die hieronder letterlijk voorkomt.

De parser is de autoriteit (`QuestwrightYamlNormalizer.cpp`); de validator is `ValidateSchema`. Deze handleiding
is daar een afspiegeling van. Spreekt iets hier een ouder specificatiedocument tegen, dan **wint de code (deze handleiding)**.

---

## 0. Gouden regels (TL;DR)

1. Een quest is één YAML-bestand. De YAML is de **bron van waarheid**; de Studio compileert die tot assets.
   Jij schrijft YAML; je bewerkt nooit gegenereerde `.uasset`s.
2. Gebruik alleen de velden, emotions, shots, objective types, condition kinds en effect verbs die hier staan.
3. Elke waarde van `target` / `location` / `tag` / `grant_tag` is een **GameplayTag die GEREGISTREERD MOET zijn**
   (native C++-tag of `DefaultGameplayTags.ini`). Een niet-geregistreerde tag is stilzwijgend ongeldig → het doel
   telt nooit mee / de voorwaarde vuurt nooit. Dit is oorzaak nummer 1 van "mijn quest doet niets".
4. Geef elke gesproken regel, elke keuze en elk doel een stabiele `id` (kleine letters/underscore). Id's sleutelen
   de lokalisatie en de gegenereerde VO-/gezichtsassets; regels herordenen mag andere id's nooit veranderen.
5. Stap-id's krijgen automatisch de namespace van de korte naam van de quest. Verwijs in `goto`/`next` naar stappen
   met hun geschreven id (de namespace wordt voor je geregeld, maar de vorm `<Short>_<id>` aanhouden is het veiligst).
6. `schema_version` is optioneel en informatief. De parser accepteert elke waarde, en features hangen aan de
   aanwezigheid van hun blok (bijvoorbeeld `staging:`), niet aan het nummer. De huidige versie is **4**
   (3 voegde staging toe, 4 de camera marks); oudere bestanden die `2` opgeven, parsen ongewijzigd.

---

## 1. Hoe het werkt (pipeline)

```
<Quest>.quest.yaml  ──parse──►  IR  ──validate──►  (fouten worden in de Studio getoond)
                                  │
                                  ├─ Resolve Audio (VO synthetiseren per culture, per sprekerstem)
                                  ├─ Facial (lip-sync FA_<id> bakken uit de VO)
                                  ├─ Generate (DataAssets: DA_QW_Dialogue_*, DA_QW_Quest_*, String Tables)
                                  └─ Bind & Stage (de cast aan NPC's in het level koppelen)
```

Jij schrijft de YAML; "Build → RUN" in de Studio doet de rest.

---

## 2. Locatie en naamgeving van bestanden

- Zet je eigen quests in **`<Project>/Content/Quests/`** (ze verschijnen automatisch in de Studio library;
  Import is niet nodig). `Docs/Samples/` is de map met de meegeleverde demo; zet daar geen eigen quests in.
- Bestandsnaam: `<Name>.quest.yaml` (de `.quest.` is conventie; elke `.yaml`/`.yml`/`.json` in een scan-root
  wordt opgepikt). Eén quest per bestand.

---

## 3. Minimaal skelet

```yaml
quest: Region.MyQuest          # unieke id/slug (VERPLICHT)
name: "Mijn quest"
giver: Bran                    # speaker key van de NPC die hem aanbiedt
source_culture: en
schema_version: 4
steps:                         # VERPLICHT, minimaal 1
  - id: MyQuest_Offer
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Bran,   id: greet, text: "Hallo.", emotion: neutral }
        - { by: Player, id: hi,    text: "Hoi." }
      choices:
        - { id: accept, text: "Ik help je.", goto: MyQuest_Do }
      on_end: { quest: accept }
  - id: MyQuest_Do
    end: completed
```

---

## 4. Questvelden op het hoogste niveau

| Veld | Type | Verplicht | Toelichting |
|---|---|---|---|
| `quest` | string | **ja** | Unieke id/slug, bijvoorbeeld `Farmlands.NightVisitors`. Wordt overal als QuestId gebruikt. |
| `name` | string | nee | Weergegeven titel (lokaliseerbaar). |
| `summary` | string | nee | Korte omschrijving voor het journaal. |
| `giver` | string | nee | Speaker key van de questgever-NPC (bijvoorbeeld `Bran`). Stuurt de keuze van de questgever + de barkstem aan. |
| `giver_display_name` | string | nee | Vriendelijke naam in de UI; valt terug op `giver`. |
| `region_display_name` | string | nee | Label waarmee het journaal per regio groepeert. |
| `source_culture` | string | nee | Taal waarin je schrijft, bijvoorbeeld `en`. Andere talen staan in losse CSV's ernaast. |
| `schema_version` | int | nee | Optioneel/informatief; nu `4` (zie §0, regel 6). |
| `priority` | int | nee | Volgorde van aanbieden door de questgever: hoger wordt eerst aangeboden als een NPC meerdere quests heeft. Standaard 0. |
| `repeatable` | bool | nee | Bij true kan een afgeronde quest opnieuw geaccepteerd worden (een "daily"). Standaard false. |
| `available_when` | voorwaarde | nee | Voorwaarde vooraf (zie §11). Leeg = meteen beschikbaar. Stuurt ketens aan. |
| `rewards` | object | nee | Zie §15. Alleen `base.{currency,xp,items}` is aangesloten. |
| `camera` | object | nee | Beleid voor de automatische camera. Zie §16. |
| `barks` | object | nee | Sfeerregels van de questgever op een timer. Zie §14. |
| `idle_barks` | object/lijst | nee | Regels bij interactie wanneer de questgever niets te bieden heeft. Zie §14. |
| `steps` | lijst | **ja** | Het lichaam van de quest, ≥1 stap. Zie §5. |

> Er is GEEN veld `description`, `tags`, `category`, `type`, `level`, `zone`, `objectives` (doelen leven
> binnen stappen), `dialogue` of `npc` op het hoogste niveau. Verzin ze niet.

---

## 5. Stappen

Een stap is één knoop van de quest (een dialoogscène, een doel of een eindmarkering).

| Veld | Type | Toelichting |
|---|---|---|
| `id` | string | **Verplicht.** Stabiele id; krijgt automatisch de namespace `<ShortQuest>_<id>` als die er nog niet voor staat. |
| `title` | string | Optioneel intern label. |
| `scene` | object | Een dialoogscène (regels + keuzes). Zie §6. |
| `objective` | object | Een bij te houden doel (kill/collect/reach/talk/deliver). Zie §9. |
| `next` | string | Standaardovergang: id van de volgende stap (wanneer de scène geen keuzes heeft). |
| `end` | string | Eindmarkering. Niet-leeg sluit de dialoog. Gebruikelijke waarden: `completed`, `declined`, `failed`. |
| `require` | voorwaarde OF string | Toegangsvoorwaarde voor deze stap (zie §11). Bijvoorbeeld: de inleverstap afschermen tot het doel af is. |

Een stap heeft meestal ÓF een `scene` (praten) ÓF een `objective` (iets doen), plus een `next`/`end`.

---

## 6. Scènes: regels en keuzes

```yaml
scene:
  speakers: [Bran, Mira, Player]   # ALLE speaker keys die in deze scène voorkomen (2, 3 of meer)
  lines:
    - { by: Bran,   id: greet, text: "Kom hier.", emotion: worried, shot: cu }
    - { by: Mira,   id: warn,  text: "Vertrouw hem niet.", to: Player }
    - { by: Player, id: what,  text: "Wat is er?" }
  choices:
    - { id: help,   text: "Ik help je.",       goto: MyQuest_Accept }
    - { id: refuse, text: "Niet mijn probleem.", goto: MyQuest_Refuse }
  on_end: { quest: accept }      # effect dat wordt toegepast zodra de scène wordt verlaten (zie §12)
```

**Meerdere personages.** Een scène ondersteunt een willekeurig aantal sprekers; zet elke key in `speakers` en
stel bij elke regel `by` in. Voor een tweede/derde personage voeg je die key (bijvoorbeeld `Mira`) toe aan
`speakers` en gebruik je hem in `by`. Tijdens runtime bindt elke key aan een actor die al met die speaker key in
het level staat; is die er niet, dan wordt er een gespawnd vanuit de speaker map (`DT_QW_Speakers`, weggeschreven
door de Cast-fase) en naast de questgever neergezet. Gespawnde personages worden automatisch verwijderd zodra de
dialoog eindigt.

### Velden van een regel
| Veld | Type | Toelichting |
|---|---|---|
| `by` | string | **Verplicht.** Speaker key (moet in `speakers` staan). VO en gezicht spelen af op deze deelnemer. |
| `to` | string | *Optioneel.* Speaker key tot wie deze regel is **gericht**; stuurt de over-the-shoulder/close-up framing en de hoofddraai aan in scènes met 3 of meer sprekers. Laat weg bij scènes met twee sprekers (de andere partij wordt afgeleid: de vorige spreker, anders de speler). |
| `id` | string | Stabiele regel-id (kleine letters/underscore). Sterk aanbevolen, want hij sleutelt VO/gezicht/lokalisatie. |
| `text` | string | De gesproken tekst (lokaliseerbaar). |
| `emotion` | enum | Zie §7. Stuurt het additieve gezicht + de toon van de VO aan. Standaard `neutral`. |
| `shot` | enum | Zie §8. Standaard `auto` (de compiler kiest). |
| `echo_of` | string | Deze regel herhaalt de tekst van een keuze; geef de `id` van die keuze op. Deelt de lokalisatiesleutel (dupliceer geen tekstsleutels). |
| `loc` | string | Expliciete override van de lokalisatiesleutel (zelden nodig). |

### Velden van een keuze
| Veld | Type | Toelichting |
|---|---|---|
| `id` | string | Stabiele keuze-id. Sleutelt de gelokaliseerde keuzetekst. |
| `text` | string | Label van de keuze. |
| `goto` | string | **Verplicht.** Id van de doelstap. |
| `require` | tag of lijst | GameplayTag(s) die de speler moet bezitten om de keuze beschikbaar te maken. (Tags, GEEN structurele voorwaarden.) |
| `grant` | tag of lijst | GameplayTag(s) die worden toegekend zodra de keuze gemaakt wordt. |

> De volledige LineId is `<StepId>_<lineId>_<Speaker>`; zo worden de VO-assets genoemd (bijvoorbeeld
> `NightVisitors_Offer_greet_Bran`). Die schrijf je niet zelf; hij wordt afgeleid uit `by`+`id`+stap.

---

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

Geldige canonieke waarden: **`neutral`, `happy`, `sad`, `angry`, `afraid`, `surprised`**.

Geaccepteerde aliassen (allemaal toegewezen aan de canonieke stemming):
- `fear`, `scared`, `worried`, `nervous`, `anxious`  → **afraid**

Al het andere is een **validatiefout** (`invalid_emotion`) en valt terug op `neutral`. Er bestaat geen aparte
stemming `worried`, geen `confused`, `disgusted`, `excited`, `bored`, enzovoort. Kies de dichtstbijzijnde van de
zes (bijvoorbeeld excited→happy, distressed→sad/afraid). Heb je een nieuw woord nodig, dan moet het eerst als
alias in de code worden toegevoegd; schrijf geen onbekende emotion-tokens.

---

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

Geldige waarden: **`auto`, `wide`, `ots`** (aliassen `over_shoulder`/`overshoulder`), **`cu`** (aliassen
`close_up`/`closeup`), **`reaction`**, **of de id van een eigen camera mark** die in
`staging.cameras` is gedeclareerd (§8b).

Standaard is `auto`: de compiler kadreert vanzelf (een establishing wide, daarna close-ups bij opeenvolgende
regels van dezelfde spreker). Zet `shot` alleen om een specifieke regel te overschrijven. Onbekende shot-tokens
(noch een basis-shot, noch een gedeclareerde camera-id) → fout `invalid_shot`.

---

## 8a. Staging: deelnemers plaatsen en verplaatsen (`staging.marks`, `line.stage`)

Een optioneel `staging:`-blok op het hoogste niveau plaatst de deelnemers van de scène rond een **anker**
(bovenaanzicht, cm), en `line.stage` verplaatst ze tijdens de dialoog. Zonder dat blok wordt elke spreker
simpelweg met het gezicht naar zijn gesprekspartner gezet; staging is alleen nodig voor blocking met meerdere
NPC's, gespawnde figuranten en verplaatsingen.

```yaml
staging:
  anchor: DialogueMarker_Barn   # een geplaatste BP_QW_DialogueMarker; weglaten → de transform van de questgever
  settle_timeout: 4.0           # seconden die een loopverplaatsing mag duren voordat er wordt teruggevallen op teleporteren
  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 voor een verplaatsing tijdens de dialoog
```

### Velden van een mark
| Veld | Toelichting |
|---|---|
| `id` | **Verplicht**, stabiel (`[A-Za-z0-9_]+`). `line.stage.to` en camera's verwijzen met de id naar marks. Deelt ÉÉN namespace met de id's van camera marks (§8b); een botsing is een fout. |
| `actor` | Speaker key (uit `speakers`/Cast). Laat weg voor een puur waypoint. |
| `pos` | **Verplicht** `[x, y]` in cm, op het grondvlak ten opzichte van het anker. Z wordt tijdens runtime op de grond vastgezet. |
| `face` | Automatisch draaien naar `anchor` \| een speaker key \| een mark-id. **Sluit `yaw` uit.** Van het anker af kijken mag (informatie, geen fout). |
| `yaw` | Handmatige lichaamshoek in graden (in plaats van `face`). |
| `appear` | `placed` (standaard: staat er al bij het begin van de scène) \| `arrive` (spawnt buiten het zicht van de speler en loopt naar de mark; zijn eerste regel wacht op aankomst). |
| `keep` | Standaard `false`, wat betekent dat een gespawnde deelnemer despawnt zodra de dialoog eindigt; `true` laat hem staan. |
| `source` | `existing` \| `spawn`. Normaal gesproken weglaten, want dit wordt afgeleid uit de Cast (de Player en al geplaatste actors worden *verplaatst*, alle anderen worden *gespawnd*). |

### Overgangen tijdens de dialoog (`line.stage`)
```yaml
  - { by: Mira, id: step, text: "Laat me de sporen bekijken.",
      stage: [ { who: Mira, to: m2, approach: walk } ] }
```
`who` = deelnemer, `to` = een gedeclareerde mark-id, `approach` = `walk` (standaard; blokkerend: de regel wacht
op aankomst, met teleporteren als terugval na `settle_timeout`) | `teleport`. Eén mark houdt **één** deelnemer
tegelijk vast; de bezetting wordt regel voor regel bijgehouden, dus twee mensen op dezelfde mark op hetzelfde
moment is een validatiefout.

---

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

Camera's die ten opzichte van de scène op het staging-canvas staan. De herbruikbare optiek (lens/DOF/filmback)
zit in een **cameraklasse** (elke Blueprint-subklasse van `QuestwrightDialogueSceneCamera`); de mark is een
instantie: een stabiele `id` + een positie ten opzichte van het anker + een richting. Regels verwijzen via
`shot:` naar de **id van de mark**, nooit naar een klasse of naar coördinaten. De automatische grammatica kiest
nooit eigen marks; die gelden alleen als expliciete overschrijving per regel (of via `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 }

# op een regel:              shot: cam_window
# terwijl de keuzes komen:   camera: { choice_shot: cam_high }
```

Velden: `id` (verplicht, `[A-Za-z0-9_]+`, deelt ÉÉN namespace met de id's van staging marks; een botsing is een
fout), `class` (verplicht: het ingebouwde token `dynamic`, een korte BP-naam die uniek is binnen het project, of
een volledig objectpad), `pos` (verplicht `[x, y]` in cm ten opzichte van het anker), `height` (cm boven de grond
van het anker, standaard 160; camera's worden nooit op de grond vastgezet, dus in de lucht hangen mag), `look_at`
(**xor** `yaw`+`pitch`) richt op `anchor` | een speaker key | een staging-mark-id; dit wordt opgelost op het moment
van de cut, dus het volgt `stage:`-verplaatsingen tijdens de dialoog. Een handmatige `yaw` wordt gemeten vanaf de
voorkant van het anker. `fov` (in graden) overschrijft desgewenst de optiek van de klasse.

`class: dynamic` heeft **helemaal geen Blueprint** nodig: de camera wordt op het moment van de dialoog gemaakt
vanuit de native C++-basis met standaardoptiek (24 mm, geen DOF). Combineer hem met `fov:` voor een snelle eigen
hoek; maak alleen een subklasse van `QuestwrightDialogueSceneCamera` in BP wanneer je lens/DOF/filmback wilt
afstemmen.

Laadt de cameraklasse tijdens runtime niet, dan degradeert de regel met een waarschuwing naar een gebakken
terugval op `wide`, zodat de scène nooit breekt.

---

## 9. Doelen (stapveld `objective:`)

Een bij te houden doel leeft binnen een stap. **Een structurele stap (een stap die een `objective` draagt)
gebruikt een generieke sectie-id, geen inhoudelijke naam**: `Goal` voor een doel, `Deliver` voor het inleveren.
De concrete soort staat in de `type` van het doel (Kill/Collect/…), en die kun je wijzigen zonder opnieuw te
sleutelen. Geef ze nooit een handgemaakte naam ("KillWolves", "TurnIn"). De id van het doel zelf valt terug op de
sectie-id, en `next`/`goto`/`require` verwijzen ernaar. Generiek voor elke quest. (Je mag de stap-`id` ook
weglaten; dan leidt de compiler `Goal`/`Deliver` af uit het doel, maar `id: Goal` schrijven is het duidelijkst.)

```yaml
- id: Goal                                          # generieke sectie; de SOORT staat hieronder in `type`
  objective:
    type: Kill                                      # de soort doel (zie tabel); id valt terug op "Goal"
    target: QuestwrightDemo.Enemy.Wolf              # GameplayTag; MOET geregistreerd zijn (§10)
    count: 3                                        # hoeveel events er nodig zijn (>=1)
    location: QuestwrightDemo.Area.FarmOutskirts    # optionele GameplayTag; telt alleen in dit gebied
    desc: "Dood de wolven bij de boerderij"         # tekst voor journaal/tracker (lokaliseerbaar)
  next: Deliver                                     # verwijst met zijn type naar de inleverstap (Deliver)
```

(Heb je twee doelen van hetzelfde type in één quest nodig, geef ze dan verschillende expliciete id's.)

### `type` van een doel (het eventkanaal)
| Type | Betekenis | Hoe het meetelt |
|---|---|---|
| `Kill` | dood N getagde vijanden | de game meldt een Kill-event bij overlijden (`ReportObjectiveEvent`) |
| `Collect` | verzamel N getagde items | de game meldt een Collect-event bij het oppakken |
| `Reach` | bereik een getagde plek | de game meldt een Reach-event / of `CompleteObjective` vanuit een trigger |
| `TalkTo` (`talk`) | praat met iemand | **automatisch**: een gesprek beginnen met de NPC telt mee (zie hieronder) |
| `Deliver` | terugkeren naar / inleveren bij de questgever | wordt vervuld DOOR het inleveren zelf (zie de opmerking hieronder) |
| `Custom` | elk eigen event | de game meldt `Custom` + je eigen `target`-tag |

Een `TalkTo`-doel is gesleuteld op een **speaker key, niet op een GameplayTag**: schrijf de key van het personage
als target (`target: Mary`, dezelfde key als in `speakers:` en op het Cast-tabblad). Wanneer de speler een gesprek
met een NPC begint (welke afloop dan ook: aanbieden, inleveren, zelfs een idle bark), meldt de dialoogcomponent
"gepraat met `<SpeakerKey>`" voor de interagerende speler, en elk overeenkomend **actief** TalkTo-doel gaat
vooruit. Zo krijg je de volgordegarantie gratis: met de NPC praten *voordat* je de quest accepteert telt niets
mee. Opmerkingen: het vergelijken is hoofdletterongevoelig; alleen de pratende speler telt mee (nooit de hele
server); herhaalde gesprekken zijn ongevaarlijk; een `location:`-afscherming geldt NIET op dit kanaal (heb je die
nodig, gebruik dan een geregistreerde tag als target en meld het event zelf via `ReportObjectiveEvent`). De NPC
moet een dialoog-actorcomponent met die speaker key dragen; een in het level geplaatst castlid of een via Cast
gebonden actor heeft die al.

```yaml
- id: Goal
  objective:
    type: TalkTo
    target: Mary            # speaker key; geen tagregistratie nodig
    count: 1
    desc: "Praat met Mary"
```

Een `Deliver`-doel modelleert de klassieke stap "ga terugrapporteren aan de questgever". Het wordt **vervuld door
het inleveren zelf**: het blokkeert het inleveren nooit (de questgever biedt het nog steeds aan zodra de
afschermende Kill/Collect/enzovoort-doelen af zijn), en het wordt automatisch afgerond zodra de quest wordt
ingeleverd (zodat de tracker het afgevinkt toont). Zet het op de inleverstap, naast de `require`-afscherming; er
is geen event/`target` nodig (geef het een `desc` als "Vertel Bran dat de wolven zijn afgehandeld").

Alles wat buiten bovenstaande woorden valt, wordt `Custom`. Voor getelde, getagde doelen (kill/collect) gebruik je
het eventpad: de game roept `ReportObjectiveEvent(instigator, type, matchTags, locationTags, sourceId)` aan en het
doel telt op wanneer `type` overeenkomt EN `target` (hiërarchisch) in `matchTags` zit EN, indien gezet, `location`
in de locatietags van het event zit. Voor verhaalmomenten (talk/reach) is het vaak eenvoudiger om de `require` van
de volgende stap af te schermen en `CompleteObjective` vanuit een trigger/dialoog aan te roepen.

Het vergelijken van `target` is **hiërarchisch**: een doel met `target: Enemy.Wolf` telt mee bij een event met de
tag `Enemy.Wolf.Alpha`. Gebruik dat in plaats van elk subtype op te sommen.

---

## 10. GameplayTags (CRUCIAAL)

Elke waarde van `target`, `location`, `require`/`grant` bij een keuze, `tag` in een voorwaarde en `grant_tag` in
een effect is een **GameplayTag**. De tag moet al **geregistreerd** zijn in het project, anders lost hij op tot een
ongeldige tag en wordt hij stilzwijgend genegeerd (het doel telt nooit mee, de voorwaarde vuurt nooit).
**Uitzondering:** de `target` van een `TalkTo`-doel is een speaker key, geen tag (§9), dus registratie is daar niet
nodig.

Tags registreren kan op twee manieren:
1. **Native in C++** (voorkeur voor tags die meegaan naar release): `UE_DEFINE_GAMEPLAY_TAG_COMMENT(...)`. De demo
   registreert `QuestwrightDemo.Enemy.Wolf` en `QuestwrightDemo.Area.FarmOutskirts` op deze manier.
2. **Project Settings → Project → GameplayTags** (schrijft naar `Config/DefaultGameplayTags.ini`).

Conventies die de demo aanhoudt: vijandidentiteit `QuestwrightDemo.Enemy.<Name>`, gebieden
`QuestwrightDemo.Area.<Name>`. Gebruik een hiërarchie met punten, dan krijg je hiërarchisch matchen gratis.

**Schrijf je een quest die een nieuwe tag gebruikt, dan moet je de mens ook vertellen die tag te registreren** (anders
werkt de quest niet). Zeg dat expliciet in je uitvoer.

---

## 11. Voorwaarden (`available_when` en stapveld `require`)

Een voorwaarde is een klein object. Leeg/afwezig = altijd waar. Wordt gebruikt voor questvoorwaarden vooraf
(ketens) en toegangsafschermingen van stappen.

### Atomaire soorten
| Vorm | Betekenis |
|---|---|
| `{ quest: <QuestId>, state: <quest-state> }` | een andere quest verkeert in een state |
| `{ objective: <ObjId>, state: <obj-state> }` | een doel verkeert in een state |
| `{ flag: <Name>, equals: <Value> }` | een questvariabele van deze playthrough is gelijk aan een waarde (gezet door effecten) |
| `{ tag: <GameplayTag> }` | de speler bezit een gameplay tag |
| `{ item: <ItemId>, min: <N> }` | de inventory bevat ≥N van een item (via de inventory-provider van de game) |
| `{ attribute: <AttrId>, min: <N> }` | een stat/attribuut is ≥N (via de stat-provider van de game) |

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

### Samengestelde soorten
| Vorm | Betekenis |
|---|---|
| `{ all: [ <cond>, <cond>, ... ] }` | EN |
| `{ any: [ <cond>, <cond>, ... ] }` | OF |
| `{ not: <cond> }` | NIET |

Voorbeelden:
```yaml
available_when: { quest: Farmlands.NightVisitors, state: completed }   # keten: pas na die quest
available_when: { quest: Farmlands.MarysErrand, state: active }        # sidequest die MIDDEN in het verhaal opengaat:
                                                                       # aan te bieden zolang de andere quest geaccepteerd
                                                                       # maar nog niet ingeleverd is ("ga met X praten")
require: { objective: Goal, state: complete }                          # scherm het inleveren af op het doel (generieke id "Goal")
available_when:
  all:
    - { quest: Region.Intro, state: completed }
    - { not: { flag: Betrayed, equals: "true" } }
```

Alleen de keys hierboven worden herkend. Een voorwaardeobject zonder herkende key → fout `unknown_condition`.

---

## 12. Effecten (scèneveld `on_end:`)

Worden toegepast zodra een scène wordt verlaten. De structurele (aanbevolen) vorm is een map:

```yaml
on_end:
  quest: accept                 # actie op de queststate (zie hieronder)
  set: { RewardTier: Negotiated }  # questvariabelen (flags) schrijven, gelezen door `flag`-voorwaarden
  grant_tag: Region.MetBran     # gameplay tag(s) aan de speler toekennen (string of lijst)
  grant_quest: Region.FollowUp  # vervolgquest(s) accepteren; zo lopen ketens door (string of lijst)
```

### Actiewerkwoorden van `quest:`
`accept`, `advance`, `complete`, `decline`, `fail`, `turnin` (alias `turn_in`).
- `accept` accepteert deze quest. `turnin` levert hem in (beloningen + afronden). `complete` is een
  onvoorwaardelijke afronding. `decline` / `fail` sluiten hem. `advance` doet tijdens **runtime niets** (het
  stapverloop wordt aangestuurd door de dialooggraaf, niet door effecten), dus vertrouw er niet op.

Een inleverscène eindigt doorgaans met `on_end: { quest: turnin }` en `end: completed`.

Oude stringvormen (`on_end: "quest.accept"`, `on_end: ["quest.accept", "set X=Y"]`) parsen nog steeds, maar geef de
voorkeur aan de structurele map hierboven.

### Het inleveren (of elke andere questgeverscène) vertakken op een flag

Wil je dat de questgever iets anders zegt afhankelijk van een eerdere keuze, combineer dan een `set`-flag met een
stap die op die flag is afgeschermd. Zet de flag in de eerdere scène en schrijf vervolgens **twee** inleverstappen:
de specifieke (op de flag afgeschermde) **eerst**, de vangnetstap als laatste:

```yaml
  # de eerdere vertakking legt de keuze vast
  - id: Haggle
    scene: { ... }
    on_end: { quest: accept, set: { RewardTier: Negotiated } }

  # inleveren, specifieke tak; MOET vóór het vangnet komen
  - id: DeliverHaggled
    require:
      all:
        - { objective: Goal, state: complete }
        - { flag: RewardTier, equals: Negotiated }
    scene: { ... }                 # de regels die het afdingen kennen
    on_end: { quest: turnin }
    end: completed

  # inleveren, standaardtak; bereikt wanneer de flag niet gezet is
  - id: Deliver
    require: { objective: Goal, state: complete }
    objective: { type: Deliver, count: 1, desc: "..." }   # het bijgehouden doel leeft hier
    scene: { ... }
    on_end: { quest: turnin }
    end: completed
```

De routering van de questgever kiest de **eerste** inleverscène waarvan de `require` slaagt, dus zet de stappen op
volgorde van meest specifiek eerst. Alleen het vangnet heeft het bijgehouden `Deliver`-doel nodig; de afgeschermde
takken bestaan alleen uit een scène en delen die ene trackerregel. Hetzelfde patroon werkt voor elke voorwaarde op
een flag/quest/doel/tag (§11), niet alleen voor flags. Zie `Docs/Samples/NightVisitors.quest.yaml` voor een levend
voorbeeld (afdingen → een andere uitbetalingsdialoog).

---

## 13. Questketens

Een keten is niet meer dan data: quest B begint na quest A via:
- `available_when: { quest: A, state: completed }` bij B (voorwaarde vooraf), en/of
- `on_end: { ..., grant_quest: B }` bij A (het vervolg automatisch accepteren bij het afronden).

`priority` bepaalt de volgorde van meerdere quests die dezelfde questgever aanbiedt (hoger eerst). De "New Chain"-UI
van de Studio schrijft deze waarden voor `available_when`/`priority` voor je weg, maar ze met de hand schrijven is
gelijkwaardig.

De validatie vangt `chain_cycle` (A vereist B vereist A), `dangling_quest_ref` (verwijzing naar een onbekende
QuestId) en `dangling_objective_ref` (een voorwaarde verwijst naar een ontbrekende doel-id) op. Zorg dat de
quest-id's en doel-id's waarnaar verwezen wordt, bestaan.

---

## 14. Barks en idle barks

**Sfeerbarks** (`barks:`) zijn willekeurige regels die de questgever boven zijn hoofd zegt op een timer, vóór en
rond het gesprek. **Idle barks** (`idle_barks:`) worden gezegd bij interactie wanneer de questgever niets meer aan
te bieden of in te leveren heeft (bijvoorbeeld een afgeronde, niet-herhaalbare quest). Beide worden ingesproken in
de stem van de **questgever**, via dezelfde VO-/gezichtspipeline.

```yaml
barks:
  interval: [8, 16]            # seconden tussen pogingen [min, max]
  lines:
    - "Alweer huilende wolven..."
    - { text: "Na donker zet niemand nog een voet op het erf.", weight: 2 }   # weight beïnvloedt de willekeurige keuze

idle_barks:
  lines:
    - "Nu even niet, vriend. Ik heb niets meer voor je."
    - "Slaap gerust. De boerderij is veilig, dankzij jou."
```

Een barkregel is een gewone string, of `{ text, weight, loc }`. Id's worden automatisch toegekend (`Bark_NNN_<Giver>` /
`IdleBark_NNN_<Giver>`). Idle barks hebben geen `interval` (ze vuren bij interactie). Sfeerbarks van een quest
stoppen zodra de quest is afgerond.

---

## 15. Beloningen

Alleen `base` is aangesloten. `bonus` en `rep` (reputatie) worden **NIET verwerkt**, dus vertrouw er niet op.

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

Ondersteund: `currency` (int), `xp` (int), `items` (lijst van `{def, count}`). De item-`def` is de item-id van jouw
game (de game deelt het daadwerkelijke item uit; Questwright legt alleen de beloning vast).

---

## 16. Camerabeleid

```yaml
camera:
  cu_run_threshold: 2     # close-up na dit aantal opeenvolgende regels van één spreker (standaard 3)
  establishing: true      # establishing wide op de eerste regel van een scène (standaard true)
  choice_shot: wide       # shot terwijl er op de keuze van de speler wordt gewacht (wide|ots|cu of een id uit staging.cameras; standaard wide)
```

Allemaal optioneel; laat het blok weg voor verstandige standaardwaarden. Een `shot:` per regel overschrijft dit
voor die regel. `choice_shot` accepteert ook de id van een eigen camera mark (§8b): de camera waar de scène naartoe
snijdt terwijl de opties verschijnen.

---

## 17. Id's, stabiliteit en lokalisatie

- `source_culture` is de taal waarin je schrijft (bijvoorbeeld `en`). Vertalingen staan in losse CSV's ernaast
  (`Loc/<Quest>.<culture>.csv`), gesleuteld op id; onvertaalde sleutels vallen terug op de brontekst.
- **Stabiele id's doen ertoe:** lokalisatiesleutels en de namen van VO-/gezichtsassets worden afgeleid uit `by`+de
  `id` van de regel, de `id` van de keuze en de `id` van het doel. Voeg je ergens in het midden een regel toe, geef
  die dan een nieuwe `id`; hernummer de andere NIET, want dan breek je hun vertalingen/VO. Daarom hoort elke
  regel/keuze/doel een expliciete `id` te dragen.
- Echoregels (een spelerregel die de tekst van een keuze herhaalt) gebruiken `echo_of: <choiceId>`, zodat ze de
  lokalisatiesleutel van de keuze delen in plaats van een duplicaat aan te maken.

### Teksttokens (dynamische waarden zoals de naam van de speler)

Elke regel, keuze, objective-`description` of quest-`name` mag `{Token}`-placeholders bevatten. Op het moment van
tonen vervangt de runtime ze door de tekstvariabelen van de lokale speler via `FText::Format` (de
lokalisatiecorrecte manier: het token overleeft de vertaling, zodat elke taal `{PlayerName}` in haar eigen
woordvolgorde houdt). Onbekende tokens blijven ongemoeid, zodat een typefout de tekst nooit leegmaakt.

- `{PlayerName}`: de weergegeven naam van de speler. Valt terug op "Adventurer" totdat de game hem instelt.
- Eigen tokens: elke naam die via `UQuestwrightTextVarsSubsystem::SetTextVar(Key, Value)` is gezet.

```yaml
- { by: Bran, id: greet, text: "Dus jij bent {PlayerName}. Kom dichterbij.", emotion: neutral }
```

De game levert de waarden aan vanuit C++ (`UQuestwrightTextVarsSubsystem::Get(this)->SetPlayerName(...)`) of
Blueprint (*Get Game Instance Subsystem → Questwright Text Vars → Set Player Name*). De waarden zijn puur lokale
presentatie en worden nooit gerepliceerd; alleen de eigen naam van de lokale speler valt binnen bereik.

---

## 18. Validatiechecklist (zorg dat de quest 0 fouten oplevert)

De validator meldt deze punten, dus voorkom ze meteen:
- `invalid_emotion`: een emotion die niet in §7 staat (aliassen meegerekend).
- `invalid_shot`: een shot dat noch een basistoken (§8) noch een gedeclareerde `staging.cameras`-id is (§8b).
- `camera_missing_id` / `camera_missing_class` / `camera_invalid_pos`: een camera mark mist zijn
  verplichte velden (§8b).
- `duplicate_camera_id` / `camera_mark_id_collision`: camera-id's moeten uniek zijn EN mogen niet botsen met
  staging-mark-id's (één gedeelde namespace).
- `camera_lookat_angles_conflict`: `look_at` én een handmatige `yaw`/`pitch` op dezelfde camera (die sluiten elkaar uit).
- `unknown_camera_class` / `ambiguous_camera_class`: het `class`-token lost op tot nul / meerdere klassen;
  gebruik een volledig objectpad om het eenduidig te maken.
- `camera_lookat_unknown`: `look_at` benoemt geen anker/deelnemer/staging mark.
- `camera_unused` (waarschuwing): een gedeclareerde camera die geen enkele regel of `choice_shot` gebruikt.
- `mark_missing_id` / `duplicate_mark_id` / `mark_invalid_pos`: een staging mark mist zijn verplichte
  velden (§8a).
- `mark_yaw_face_conflict`: `face` én een handmatige `yaw` op dezelfde mark (die sluiten elkaar uit).
- `mark_invalid_appear` / `mark_invalid_keep` / `mark_invalid_source`: waarden buiten
  `placed|arrive` / bool / `existing|spawn`.
- `stage_missing_who` / `stage_missing_to` / `stage_invalid_approach` / `broken_stage_target` /
  `stage_who_unknown`: een misvormde `line.stage`-invoer of een verwijzing naar een ontbrekende mark/deelnemer.
- `mark_occupancy_conflict` / `staging_marks_overlap`: twee deelnemers op één mark op hetzelfde
  moment / marks die boven op elkaar staan.
- `unknown_condition`: een voorwaarde zonder herkende key (§11).
- `unknown_effect`: een effect zonder herkend werkwoord (§12).
- `unstable_line_id`: een regel zonder `id` terwijl `source_culture` gezet is (geef elke regel een id).
- `objective_no_id`: een doel zonder `id`.
- `dangling_quest_ref` / `dangling_objective_ref` / `chain_cycle`: verwijzingen naar ontbrekende quests/doelen,
  of een circulaire keten (§13).
- `dangling_goto` / `broken_goto`: een keuze/next wijst naar een stap-id die niet bestaat.
- Niet-geregistreerde GameplayTags geven geen fout maar **breken stilzwijgend** het matchen, dus controleer of elke tag geregistreerd is (§10).

---

## 19. Volledig voorbeeld met toelichting

```yaml
quest: Farmlands.NightVisitors
name: "Nachtelijke bezoekers"
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:
    - "Alweer huilende wolven... een slecht voorteken."
    - { text: "De wolven zijn brutaal geworden, de duivels.", weight: 2 }

idle_barks:
  lines:
    - "Nu even niet, vriend. Ik heb niets meer voor je."

steps:
  - id: NightVisitors_Offer
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Bran,   id: greet,  text: "Hé. Jij. Kom eens dichterbij.", emotion: angry }
        - { by: Player, id: what,   text: "Wat is er?" }
        - { by: Bran,   id: wolves, text: "Wolven. Ze komen voor mijn dieren.", emotion: sad }
      choices:
        - { id: help,   text: "Ik help je. Waar zitten de wolven?", goto: NightVisitors_Accept }
        - { id: refuse, text: "Niet mijn probleem, ouwe.",          goto: NightVisitors_Refuse }

  - id: NightVisitors_Accept
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Player, id: help_echo, text: "Ik help je. Waar zitten de wolven?", echo_of: help }
        - { by: Bran,   id: where,     text: "Achter de boerderij, aan de rand van het bos.", emotion: worried }
      on_end: { quest: accept }
    next: Goal

  - id: NightVisitors_Refuse
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Player, id: refuse_echo, text: "Niet mijn probleem, ouwe.", echo_of: refuse }
        - { by: Bran,   id: warn,        text: "Morgen staan die beesten op de weg.", emotion: angry, shot: cu }
      on_end: { quest: decline }
    end: declined

  # Doelstap: generieke sectie "Goal" (de soort staat in `type`); de id van het doel valt terug op "Goal".
  - id: Goal
    objective:
      type: Kill
      target: QuestwrightDemo.Enemy.Wolf
      count: 3
      location: QuestwrightDemo.Area.FarmOutskirts
      desc: "Dood de wolven bij de boerderij"
    next: Deliver

  # Inleveren = een Deliver-stap, afgeschermd op het Goal-doel.
  - id: Deliver
    require: { objective: Goal, state: complete }
    objective:
      type: Deliver                                   # vervuld door het inleveren; schermt het nooit af
      count: 1
      desc: "Vertel Bran dat de wolven zijn afgehandeld"
    scene:
      speakers: [Bran, Player]
      lines:
        - { by: Player, id: done,   text: "De wolven zijn afgehandeld." }
        - { by: Bran,   id: relief, text: "Mooi. Mijn dieren halen de ochtend.", emotion: happy }
      on_end: { quest: turnin }
    end: completed
```

---

## 20. WEL / NIET

**WEL**
- Houd `quest`, `steps` (≥1) en minstens één eindstap (`end:`) aan.
- Geef elke regel / keuze / doel een stabiele `id` in kleine letters.
- Gebruik alleen de emotions, shots, types, condition kinds en effect verbs uit deze handleiding.
- Gebruik GameplayTags met punten die **geregistreerd** zijn voor `target`/`location`/`tag`/`grant_tag`, en vertel de
  mens dat hij elke nieuwe tag moet registreren.
- Scherm de inleverstap af op het afgeronde doel.

**NIET**
- Verzin geen velden op het hoogste niveau (`description`, `level`, `zone`, `tags`, `category`, `npc`, `dialogue`, …).
- Gebruik geen emotions/shots/types buiten de lijsten (geen `confused`, `excited`, `disgusted`; geen `pan`, `zoom`).
- Vertrouw niet op `rewards.bonus`, `rewards.rep` of de questactie `advance`; die zijn niet aangesloten.
- Hergebruik of hernummer geen id's wanneer je content invoegt; voeg nieuwe id's toe en laat bestaande ongemoeid.
- Verwijs niet naar quest-id's / doel-id's / stap-id's die niet bestaan.
- Ga er niet van uit dat een GameplayTag werkt zonder registratie.
```
