Skip to main content

delvewright_dsl/quest/
objective.rs

1//! A quest objective: what the party does to advance a quest.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::serde_fields::{is_false, is_zero};
7use crate::{
8    AnchorId, FlagId, Guidance, Happening, NpcId, ObjectiveId, Prop, StateCompare, Visibility,
9    WaveId,
10};
11
12/// A quest objective.
13///
14/// Every variant may carry `requires_flags`:
15/// flag-gated activation, satisfied only once each referenced flag has been set
16/// by a `set-flag` effect.
17#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
18#[serde(tag = "type", rename_all = "kebab-case", deny_unknown_fields)]
19pub enum Objective {
20    /// Completed by a dialogue option's `complete-objective` effect.
21    TalkTo {
22        /// Objective id.
23        id: ObjectiveId,
24        /// Short player-facing objective name (v0.3, optional).
25        #[serde(default, skip_serializing_if = "Option::is_none")]
26        title: Option<String>,
27        /// One-line location/direction hint (v0.3, optional).
28        #[serde(default, skip_serializing_if = "Option::is_none")]
29        hint: Option<String>,
30        /// Whether this objective is **announced** — the `New objective` line, the
31        /// hint's line, the cue sound and the `Objective complete` line (spec-0093).
32        /// Absent = the campaign's [`Guidance::announcements`]. An objective with
33        /// no `title` is never announced whatever this says; `shown` on one is
34        /// `DW0961`. A hint on an unannounced objective is `DW0862`.
35        #[serde(default, skip_serializing_if = "Option::is_none")]
36        announcement: Option<Visibility>,
37        /// The NPC to talk to.
38        npc: NpcId,
39        /// Prerequisite objectives (intra-quest ordering).
40        #[serde(default, skip_serializing_if = "Vec::is_empty")]
41        after: Vec<ObjectiveId>,
42        /// Flags that must be set before this objective activates (v0.3).
43        #[serde(default, skip_serializing_if = "Vec::is_empty")]
44        requires_flags: Vec<FlagId>,
45        /// Negative flag gate (DSL v0.6): the
46        /// objective is suppressed (cannot activate or complete) while ANY listed
47        /// flag is set for the player — the dual of `requires_flags`.
48        #[serde(default, skip_serializing_if = "Vec::is_empty")]
49        forbids_flags: Vec<FlagId>,
50        /// Numeric gate terms (DSL v0.10, spec-0031): every listed comparison
51        /// must hold for this gate to be open. The third field of the one gate,
52        /// carried by every gate consumer — never by the verb that first wanted
53        /// it. Default empty, so a pre-0.10 campaign is byte-identical.
54        #[serde(default, skip_serializing_if = "Vec::is_empty")]
55        requires_state: Vec<StateCompare>,
56        /// Bot stealth hint (DSL v0.4): mark this leg as one the critical-path
57        /// bot should traverse sneaking (sprint disabled). Emitted into
58        /// `critical-path.json` as `sneak: true` on the step. Purely a harness
59        /// hint; no datapack effect.
60        #[serde(default, skip_serializing_if = "is_false")]
61        stealth: bool,
62        /// What this objective does to the story (DSL v0.8, spec-0025; required
63        /// at 0.8.0, `DW0481`).
64        #[serde(default, skip_serializing_if = "Option::is_none")]
65        happening: Option<Happening>,
66    },
67    /// Completed by reaching an anchor once prerequisites are met.
68    ReachAnchor {
69        /// Objective id.
70        id: ObjectiveId,
71        /// Short player-facing objective name (v0.3, optional).
72        #[serde(default, skip_serializing_if = "Option::is_none")]
73        title: Option<String>,
74        /// One-line location/direction hint (v0.3, optional).
75        #[serde(default, skip_serializing_if = "Option::is_none")]
76        hint: Option<String>,
77        /// Whether this objective is **announced** — the `New objective` line, the
78        /// hint's line, the cue sound and the `Objective complete` line (spec-0093).
79        /// Absent = the campaign's [`Guidance::announcements`]. An objective with
80        /// no `title` is never announced whatever this says; `shown` on one is
81        /// `DW0961`. A hint on an unannounced objective is `DW0862`.
82        #[serde(default, skip_serializing_if = "Option::is_none")]
83        announcement: Option<Visibility>,
84        /// The anchor to reach.
85        anchor: AnchorId,
86        /// Whether the glowing end-rod marker is summoned at the anchor when this
87        /// objective activates (spec-0093). Absent = the campaign's
88        /// [`Guidance::markers`]. The completion volume is adjudicated either way.
89        #[serde(default, skip_serializing_if = "Option::is_none")]
90        marker: Option<Visibility>,
91        /// Completion radius (blocks).
92        radius: u32,
93        /// Prerequisite objectives (intra-quest ordering).
94        #[serde(default, skip_serializing_if = "Vec::is_empty")]
95        after: Vec<ObjectiveId>,
96        /// Flags that must be set before this objective activates (v0.3).
97        #[serde(default, skip_serializing_if = "Vec::is_empty")]
98        requires_flags: Vec<FlagId>,
99        /// Negative flag gate (DSL v0.6): the
100        /// objective is suppressed (cannot activate or complete) while ANY listed
101        /// flag is set for the player — the dual of `requires_flags`.
102        #[serde(default, skip_serializing_if = "Vec::is_empty")]
103        forbids_flags: Vec<FlagId>,
104        /// Numeric gate terms (DSL v0.10, spec-0031): every listed comparison
105        /// must hold for this gate to be open. The third field of the one gate,
106        /// carried by every gate consumer — never by the verb that first wanted
107        /// it. Default empty, so a pre-0.10 campaign is byte-identical.
108        #[serde(default, skip_serializing_if = "Vec::is_empty")]
109        requires_state: Vec<StateCompare>,
110        /// Bot stealth hint (DSL v0.4): mark this leg as one the critical-path
111        /// bot should traverse sneaking (sprint disabled). Emitted into
112        /// `critical-path.json` as `sneak: true` on the step. Purely a harness
113        /// hint; no datapack effect.
114        #[serde(default, skip_serializing_if = "is_false")]
115        stealth: bool,
116        /// What this objective does to the story (DSL v0.8, spec-0025; required
117        /// at 0.8.0, `DW0481`).
118        #[serde(default, skip_serializing_if = "Option::is_none")]
119        happening: Option<Happening>,
120    },
121    /// Completed when the referenced wave is fully slain (v0.3).
122    Kill {
123        /// Objective id.
124        id: ObjectiveId,
125        /// Short player-facing objective name (v0.3, optional).
126        #[serde(default, skip_serializing_if = "Option::is_none")]
127        title: Option<String>,
128        /// One-line location/direction hint (v0.3, optional).
129        #[serde(default, skip_serializing_if = "Option::is_none")]
130        hint: Option<String>,
131        /// Whether this objective is **announced** — the `New objective` line, the
132        /// hint's line, the cue sound and the `Objective complete` line (spec-0093).
133        /// Absent = the campaign's [`Guidance::announcements`]. An objective with
134        /// no `title` is never announced whatever this says; `shown` on one is
135        /// `DW0961`. A hint on an unannounced objective is `DW0862`.
136        #[serde(default, skip_serializing_if = "Option::is_none")]
137        announcement: Option<Visibility>,
138        /// The wave (stage-5 `waves` ref) whose mobs must be slain.
139        wave: WaveId,
140        /// Prerequisite objectives.
141        #[serde(default, skip_serializing_if = "Vec::is_empty")]
142        after: Vec<ObjectiveId>,
143        /// Flags that must be set before this objective activates (v0.3).
144        #[serde(default, skip_serializing_if = "Vec::is_empty")]
145        requires_flags: Vec<FlagId>,
146        /// Negative flag gate (DSL v0.6): the
147        /// objective is suppressed (cannot activate or complete) while ANY listed
148        /// flag is set for the player — the dual of `requires_flags`.
149        #[serde(default, skip_serializing_if = "Vec::is_empty")]
150        forbids_flags: Vec<FlagId>,
151        /// Numeric gate terms (DSL v0.10, spec-0031): every listed comparison
152        /// must hold for this gate to be open. The third field of the one gate,
153        /// carried by every gate consumer — never by the verb that first wanted
154        /// it. Default empty, so a pre-0.10 campaign is byte-identical.
155        #[serde(default, skip_serializing_if = "Vec::is_empty")]
156        requires_state: Vec<StateCompare>,
157        /// Bot stealth hint (DSL v0.4): mark this leg as one the critical-path
158        /// bot should traverse sneaking (sprint disabled). Emitted into
159        /// `critical-path.json` as `sneak: true` on the step. Purely a harness
160        /// hint; no datapack effect.
161        #[serde(default, skip_serializing_if = "is_false")]
162        stealth: bool,
163        /// What this objective does to the story (DSL v0.8, spec-0025; required
164        /// at 0.8.0, `DW0481`).
165        #[serde(default, skip_serializing_if = "Option::is_none")]
166        happening: Option<Happening>,
167    },
168    /// Completed when `count` of `item` have been collected (v0.3).
169    ///
170    /// The items are provided in a container: the compiler's own chest at
171    /// `anchor` by default, or — since DSL v0.8 — the prefab's existing
172    /// chest/barrel at [`Objective::Collect::container`], optionally carrying an
173    /// [`Objective::Collect::item_name`] and padded to read full with
174    /// [`Objective::Collect::fill_count`].
175    Collect {
176        /// Objective id.
177        id: ObjectiveId,
178        /// Short player-facing objective name (v0.3, optional).
179        #[serde(default, skip_serializing_if = "Option::is_none")]
180        title: Option<String>,
181        /// One-line location/direction hint (v0.3, optional).
182        #[serde(default, skip_serializing_if = "Option::is_none")]
183        hint: Option<String>,
184        /// Whether this objective is **announced** — the `New objective` line, the
185        /// hint's line, the cue sound and the `Objective complete` line (spec-0093).
186        /// Absent = the campaign's [`Guidance::announcements`]. An objective with
187        /// no `title` is never announced whatever this says; `shown` on one is
188        /// `DW0961`. A hint on an unannounced objective is `DW0862`.
189        #[serde(default, skip_serializing_if = "Option::is_none")]
190        announcement: Option<Visibility>,
191        /// Vanilla item id to collect (validated against the registry).
192        item: String,
193        /// How many are required.
194        count: u32,
195        /// The anchor items are provided at (chest / pickup).
196        anchor: AnchorId,
197        /// **Adopt the container the prefab already placed**: the anchor whose
198        /// assembled-world cell holds
199        /// a `chest` / `trapped_chest` / `barrel` this collect fills instead of
200        /// conjuring its own chest at [`Objective::Collect::anchor`].
201        ///
202        /// Same division of labour a `loot` entry and a trap's dispenser already
203        /// keep with the prefab: furniture belongs in the piece. A beach camp's
204        /// barrel is scenery the player has been walking past since minute one —
205        /// having the compiler `setblock` a *second*, floating chest beside it to
206        /// hold the quest item is exactly the downstream workaround the no-hack
207        /// rule forbids. A `container` whose cell holds no container is a build
208        /// error (`DW0438`), never a silent fill into a wall.
209        ///
210        /// The critical-path step's position follows the container (the bot opens
211        /// *this* block), and no chest is placed at `anchor` when it is set.
212        #[serde(default, skip_serializing_if = "Option::is_none")]
213        container: Option<AnchorId>,
214        /// **The item comes off a body, not out of a box**: the wave whose
215        /// declared `drops[]` yield
216        /// this objective's item. No container is placed — not the compiler's own
217        /// chest at `anchor`, not a prefab one — and `container` is therefore
218        /// mutually exclusive with it (`DW0100`-adjacent; `DW0492`).
219        ///
220        /// This is what makes "kill the boss → pick up its key → open the door"
221        /// a *proved* chain rather than an authoring intention. The compiler
222        /// requires (a) that the named wave really declares an `{item}` drop of
223        /// this item (`DW0492`), and (b) that a `kill` objective for that wave
224        /// precedes this collect in the objective graph (`DW0493`). The existing
225        /// flow machinery then carries the ordering the rest of the way: the
226        /// door's `requires_flags` hangs off this collect exactly as it would off
227        /// a chest one.
228        ///
229        /// **Waves only.** An actor's death is not observable by any objective —
230        /// there is no vanilla-side signal the flow machinery could consume — so
231        /// an actor-gated collect would be an unprovable claim, and per the
232        /// no-hack doctrine it is excluded rather than approximated. An actor may
233        /// still declare `drops[]`; those drops just cannot gate a quest.
234        #[serde(default, skip_serializing_if = "Option::is_none")]
235        dropped_by: Option<WaveId>,
236        /// Display name for the collected item (DSL v0.8), emitted as the vanilla `custom_name` item component.
237        ///
238        /// A quest item is a *named thing* in the story ("Cheese", "Tide
239        /// Ledger"), and a player who opens the barrel must read that name — an
240        /// unnamed `minecraft:pumpkin_pie` says nothing about what the quest asked
241        /// for. Player-visible, so it enters the l10n string inventory
242        /// (`obj.<quest>.<obj>.item_name`) and translates like any other line.
243        ///
244        /// Naming changes nothing about adjudication: the completion advancement
245        /// and the per-tick held check both match on the ITEM ID, which a named
246        /// stack still carries.
247        #[serde(default, skip_serializing_if = "Option::is_none")]
248        item_name: Option<String>,
249        /// Padding stacks that make the container **read full**. Default `0` =
250        /// the single required
251        /// stack and nothing else.
252        ///
253        /// A barrel of cheese that opens on one lonely wheel reads as a bug, and
254        /// vanilla's notion of "full" is *occupied slots*, not stack size — so
255        /// this counts SLOTS: the objective's own stack lands in `container.0` and
256        /// each padding stack repeats it in `container.1`, `container.2`, … Slot
257        /// assignment is positional and total, the same determinism story `loot`
258        /// tells (ADR-0006): no RNG, no loot tables, nothing to reseed.
259        ///
260        /// The padding is the same item, so taking the whole barrel still
261        /// completes the objective and never over- or under-counts it.
262        #[serde(default, skip_serializing_if = "is_zero")]
263        fill_count: u32,
264        /// Prerequisite objectives.
265        #[serde(default, skip_serializing_if = "Vec::is_empty")]
266        after: Vec<ObjectiveId>,
267        /// Flags that must be set before this objective activates (v0.3).
268        #[serde(default, skip_serializing_if = "Vec::is_empty")]
269        requires_flags: Vec<FlagId>,
270        /// Negative flag gate (DSL v0.6): the
271        /// objective is suppressed (cannot activate or complete) while ANY listed
272        /// flag is set for the player — the dual of `requires_flags`.
273        #[serde(default, skip_serializing_if = "Vec::is_empty")]
274        forbids_flags: Vec<FlagId>,
275        /// Numeric gate terms (DSL v0.10, spec-0031): every listed comparison
276        /// must hold for this gate to be open. The third field of the one gate,
277        /// carried by every gate consumer — never by the verb that first wanted
278        /// it. Default empty, so a pre-0.10 campaign is byte-identical.
279        #[serde(default, skip_serializing_if = "Vec::is_empty")]
280        requires_state: Vec<StateCompare>,
281        /// Bot stealth hint (DSL v0.4): mark this leg as one the critical-path
282        /// bot should traverse sneaking (sprint disabled). Emitted into
283        /// `critical-path.json` as `sneak: true` on the step. Purely a harness
284        /// hint; no datapack effect.
285        #[serde(default, skip_serializing_if = "is_false")]
286        stealth: bool,
287        /// What this objective does to the story (DSL v0.8, spec-0025; required
288        /// at 0.8.0, `DW0481`).
289        #[serde(default, skip_serializing_if = "Option::is_none")]
290        happening: Option<Happening>,
291    },
292    /// Completed by interacting with an entity at `anchor`; if `requires_item` is
293    /// set, the item must be **held in the main hand** (v0.3; held semantics since
294    /// DSL v0.7 — see [`Objective::Interact::requires_item`]).
295    Interact {
296        /// Objective id.
297        id: ObjectiveId,
298        /// Short player-facing objective name (v0.3, optional).
299        #[serde(default, skip_serializing_if = "Option::is_none")]
300        title: Option<String>,
301        /// One-line location/direction hint (v0.3, optional).
302        #[serde(default, skip_serializing_if = "Option::is_none")]
303        hint: Option<String>,
304        /// Whether this objective is **announced** — the `New objective` line, the
305        /// hint's line, the cue sound and the `Objective complete` line (spec-0093).
306        /// Absent = the campaign's [`Guidance::announcements`]. An objective with
307        /// no `title` is never announced whatever this says; `shown` on one is
308        /// `DW0961`. A hint on an unannounced objective is `DW0862`.
309        #[serde(default, skip_serializing_if = "Option::is_none")]
310        announcement: Option<Visibility>,
311        /// The anchor the interaction entity stands at.
312        anchor: AnchorId,
313        /// Whether the glowing lantern marker is summoned beside the hitbox when
314        /// this objective activates (spec-0093). Absent = the campaign's
315        /// [`Guidance::markers`]. The `minecraft:interaction` hitbox is summoned
316        /// either way — it is what the player presses. Meaningless beside a
317        /// `prop`, which never had a marker: declaring both is `DW0962`.
318        #[serde(default, skip_serializing_if = "Option::is_none")]
319        marker: Option<Visibility>,
320        /// Item the player must be **holding in the main hand** for the
321        /// interaction to complete (optional).
322        ///
323        /// Held, not merely possessed: presenting the
324        /// item IS the action — a player who right-clicks a sleeping giant with a
325        /// sharpened stake buried in their backpack has not stabbed anything.
326        /// Before this ruling the gate read the whole inventory, which made every
327        /// `requires_item` interaction fire the moment the item was picked up
328        /// anywhere, whatever the player was actually doing with their hands.
329        #[serde(default, skip_serializing_if = "Option::is_none")]
330        requires_item: Option<String>,
331        /// Diegetic feedback for a click that arrives without the required item in
332        /// hand (DSL v0.7): narrated to that player in
333        /// chat instead of the silence the gate used to answer with. Requires
334        /// `requires_item` (`DW0437`).
335        ///
336        /// Only fires while the objective is genuinely open — same activation gate
337        /// as the affordance itself — so a finished or not-yet-active interaction
338        /// stays quiet.
339        #[serde(default, skip_serializing_if = "Option::is_none")]
340        missing_item_hint: Option<String>,
341        /// Prop block that IS the interaction affordance (DSL v0.4, spec-0008
342        /// §2): the compiler `setblock`s it at the anchor on activation (exactly
343        /// as `collect` uses a real chest). Omitted = the glowing-lantern
344        /// hologram marker (the v0.3 fallback).
345        #[serde(default, skip_serializing_if = "Option::is_none")]
346        prop: Option<Prop>,
347        /// Prerequisite objectives.
348        #[serde(default, skip_serializing_if = "Vec::is_empty")]
349        after: Vec<ObjectiveId>,
350        /// Flags that must be set before this objective activates (v0.3).
351        #[serde(default, skip_serializing_if = "Vec::is_empty")]
352        requires_flags: Vec<FlagId>,
353        /// Negative flag gate (DSL v0.6): the
354        /// objective is suppressed (cannot activate or complete) while ANY listed
355        /// flag is set for the player — the dual of `requires_flags`.
356        #[serde(default, skip_serializing_if = "Vec::is_empty")]
357        forbids_flags: Vec<FlagId>,
358        /// Numeric gate terms (DSL v0.10, spec-0031): every listed comparison
359        /// must hold for this gate to be open. The third field of the one gate,
360        /// carried by every gate consumer — never by the verb that first wanted
361        /// it. Default empty, so a pre-0.10 campaign is byte-identical.
362        #[serde(default, skip_serializing_if = "Vec::is_empty")]
363        requires_state: Vec<StateCompare>,
364        /// Bot stealth hint (DSL v0.4): mark this leg as one the critical-path
365        /// bot should traverse sneaking (sprint disabled). Emitted into
366        /// `critical-path.json` as `sneak: true` on the step. Purely a harness
367        /// hint; no datapack effect.
368        #[serde(default, skip_serializing_if = "is_false")]
369        stealth: bool,
370        /// What this objective does to the story (DSL v0.8, spec-0025; required
371        /// at 0.8.0, `DW0481`).
372        #[serde(default, skip_serializing_if = "Option::is_none")]
373        happening: Option<Happening>,
374    },
375}
376
377impl Objective {
378    /// This objective's id.
379    pub fn id(&self) -> &ObjectiveId {
380        match self {
381            Objective::TalkTo { id, .. }
382            | Objective::ReachAnchor { id, .. }
383            | Objective::Kill { id, .. }
384            | Objective::Collect { id, .. }
385            | Objective::Interact { id, .. } => id,
386        }
387    }
388
389    /// This objective's prerequisites.
390    pub fn after(&self) -> &[ObjectiveId] {
391        match self {
392            Objective::TalkTo { after, .. }
393            | Objective::ReachAnchor { after, .. }
394            | Objective::Kill { after, .. }
395            | Objective::Collect { after, .. }
396            | Objective::Interact { after, .. } => after,
397        }
398    }
399
400    /// The short player-facing objective title (v0.3, optional).
401    pub fn title(&self) -> Option<&str> {
402        match self {
403            Objective::TalkTo { title, .. }
404            | Objective::ReachAnchor { title, .. }
405            | Objective::Kill { title, .. }
406            | Objective::Collect { title, .. }
407            | Objective::Interact { title, .. } => title.as_deref(),
408        }
409    }
410
411    /// The one-line location/direction hint (v0.3, optional).
412    pub fn hint(&self) -> Option<&str> {
413        match self {
414            Objective::TalkTo { hint, .. }
415            | Objective::ReachAnchor { hint, .. }
416            | Objective::Kill { hint, .. }
417            | Objective::Collect { hint, .. }
418            | Objective::Interact { hint, .. } => hint.as_deref(),
419        }
420    }
421
422    /// Mutable access to the optional player-facing title (i18n localization).
423    pub fn title_mut(&mut self) -> &mut Option<String> {
424        match self {
425            Objective::TalkTo { title, .. }
426            | Objective::ReachAnchor { title, .. }
427            | Objective::Kill { title, .. }
428            | Objective::Collect { title, .. }
429            | Objective::Interact { title, .. } => title,
430        }
431    }
432
433    /// Mutable access to the optional one-line hint (i18n localization).
434    pub fn hint_mut(&mut self) -> &mut Option<String> {
435        match self {
436            Objective::TalkTo { hint, .. }
437            | Objective::ReachAnchor { hint, .. }
438            | Objective::Kill { hint, .. }
439            | Objective::Collect { hint, .. }
440            | Objective::Interact { hint, .. } => hint,
441        }
442    }
443
444    /// The objective's own `announcement`, when it states one (spec-0093).
445    pub fn announcement(&self) -> Option<Visibility> {
446        match self {
447            Objective::TalkTo { announcement, .. }
448            | Objective::ReachAnchor { announcement, .. }
449            | Objective::Kill { announcement, .. }
450            | Objective::Collect { announcement, .. }
451            | Objective::Interact { announcement, .. } => *announcement,
452        }
453    }
454
455    /// The objective's own `marker`, when it is a kind that has one and states
456    /// it (spec-0093). `None` for a `talk-to`, `kill` or `collect`, which carry no
457    /// such field, and for an `interact` or `reach-anchor` that leaves it absent.
458    pub fn marker(&self) -> Option<Visibility> {
459        match self {
460            Objective::ReachAnchor { marker, .. } | Objective::Interact { marker, .. } => *marker,
461            Objective::TalkTo { .. } | Objective::Kill { .. } | Objective::Collect { .. } => None,
462        }
463    }
464
465    /// Whether this objective's kind summons a wayfinding marker at all: a
466    /// `reach-anchor` (its end rod) or an `interact` with no `prop` (its lantern).
467    /// A `collect` places its chest, a `talk-to` has a body, a `kill` has bodies,
468    /// and an `interact` with a `prop` has the prop.
469    pub fn summons_marker(&self) -> bool {
470        match self {
471            Objective::ReachAnchor { .. } => true,
472            Objective::Interact { prop, .. } => prop.is_none(),
473            Objective::TalkTo { .. } | Objective::Kill { .. } | Objective::Collect { .. } => false,
474        }
475    }
476
477    /// **Is this objective marked** (spec-0093): its kind summons a marker and
478    /// its resolved visibility — its own `marker`, else the campaign's
479    /// [`Guidance::markers`] — is `shown`.
480    pub fn marker_shown(&self, guidance: &Guidance) -> bool {
481        self.summons_marker() && self.marker().unwrap_or(guidance.markers).is_shown()
482    }
483
484    /// **Is this objective announced** (spec-0093): it has a `title` and its
485    /// resolved visibility — its own `announcement`, else the campaign's
486    /// [`Guidance::announcements`] — is `shown`. The emitter prints the
487    /// activation and completion lines for exactly these objectives, so every
488    /// rule about what the party is told reads this and nothing else.
489    pub fn announced(&self, guidance: &Guidance) -> bool {
490        self.title().is_some_and(|t| !t.trim().is_empty())
491            && self
492                .announcement()
493                .unwrap_or(guidance.announcements)
494                .is_shown()
495    }
496
497    /// The flags that must be set before this objective activates (v0.3).
498    pub fn requires_flags(&self) -> &[FlagId] {
499        match self {
500            Objective::TalkTo { requires_flags, .. }
501            | Objective::ReachAnchor { requires_flags, .. }
502            | Objective::Kill { requires_flags, .. }
503            | Objective::Collect { requires_flags, .. }
504            | Objective::Interact { requires_flags, .. } => requires_flags,
505        }
506    }
507
508    /// The negative flag gate (DSL v0.6): flags whose being set **suppresses**
509    /// this objective. The dual of [`Objective::requires_flags`].
510    pub fn forbids_flags(&self) -> &[FlagId] {
511        match self {
512            Objective::TalkTo { forbids_flags, .. }
513            | Objective::ReachAnchor { forbids_flags, .. }
514            | Objective::Kill { forbids_flags, .. }
515            | Objective::Collect { forbids_flags, .. }
516            | Objective::Interact { forbids_flags, .. } => forbids_flags,
517        }
518    }
519
520    /// The numeric gate terms (DSL v0.10, spec-0031): comparisons that must hold
521    /// before this objective activates. See [`StateCompare`].
522    pub fn requires_state(&self) -> &[StateCompare] {
523        match self {
524            Objective::TalkTo { requires_state, .. }
525            | Objective::ReachAnchor { requires_state, .. }
526            | Objective::Kill { requires_state, .. }
527            | Objective::Collect { requires_state, .. }
528            | Objective::Interact { requires_state, .. } => requires_state,
529        }
530    }
531
532    /// What this objective does to the story (DSL v0.8, spec-0025).
533    pub fn happening(&self) -> Option<&Happening> {
534        match self {
535            Objective::TalkTo { happening, .. }
536            | Objective::ReachAnchor { happening, .. }
537            | Objective::Kill { happening, .. }
538            | Objective::Collect { happening, .. }
539            | Objective::Interact { happening, .. } => happening.as_ref(),
540        }
541    }
542
543    /// The bot stealth hint (DSL v0.4): traverse this leg sneaking.
544    pub fn stealth(&self) -> bool {
545        match self {
546            Objective::TalkTo { stealth, .. }
547            | Objective::ReachAnchor { stealth, .. }
548            | Objective::Kill { stealth, .. }
549            | Objective::Collect { stealth, .. }
550            | Objective::Interact { stealth, .. } => *stealth,
551        }
552    }
553
554    /// The container this objective ADOPTS (DSL v0.8), if it is a `collect` that
555    /// declares one: the anchor whose prefab-placed chest/barrel it fills instead
556    /// of conjuring its own chest. `None` on every other objective and on a
557    /// `collect` that keeps the compiler-placed chest.
558    pub fn collect_container(&self) -> Option<&AnchorId> {
559        match self {
560            Objective::Collect { container, .. } => container.as_ref(),
561            _ => None,
562        }
563    }
564
565    /// The wave whose declared drops provide this objective's item (DSL v0.9),
566    /// if it is a `collect` that declares one. `None` on every other objective
567    /// and on a `collect` fed by a container.
568    pub fn collect_dropped_by(&self) -> Option<&WaveId> {
569        match self {
570            Objective::Collect { dropped_by, .. } => dropped_by.as_ref(),
571            _ => None,
572        }
573    }
574
575    /// The padding-stack count of a `collect` (DSL v0.8); `0` for every other
576    /// objective and for a `collect` that fills the single required stack only.
577    pub fn collect_fill_count(&self) -> u32 {
578        match self {
579            Objective::Collect { fill_count, .. } => *fill_count,
580            _ => 0,
581        }
582    }
583
584    /// The `interact` prop block (DSL v0.4), if this is an `interact` with a prop.
585    pub fn prop(&self) -> Option<&Prop> {
586        match self {
587            Objective::Interact { prop, .. } => prop.as_ref(),
588            _ => None,
589        }
590    }
591
592    /// The kebab type tag.
593    pub fn kind(&self) -> &'static str {
594        match self {
595            Objective::TalkTo { .. } => "talk-to",
596            Objective::ReachAnchor { .. } => "reach-anchor",
597            Objective::Kill { .. } => "kill",
598            Objective::Collect { .. } => "collect",
599            Objective::Interact { .. } => "interact",
600        }
601    }
602
603    /// The v0.3 verb name if this objective is one of the verbs introduced in
604    /// DSL v0.3 (`kill`/`collect`/`interact`). These validate in v0.3 campaigns
605    ///.
606    pub fn v03_verb(&self) -> Option<&'static str> {
607        match self {
608            Objective::Kill { .. } => Some("kill"),
609            Objective::Collect { .. } => Some("collect"),
610            Objective::Interact { .. } => Some("interact"),
611            Objective::TalkTo { .. } | Objective::ReachAnchor { .. } => None,
612        }
613    }
614}