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}