Skip to main content

delvewright_dsl/
onkill.rs

1//! **A kill pays** (spec-0074): what happens each time a body of a fight is
2//! killed.
3//!
4//! `on_kill` is an optional property of `waves[]` and `actors[]` — one type on
5//! both. Its `effects` run once per body a player is **credited** with killing
6//! (vanilla's `minecraft:player_killed_entity`), as that player, so a
7//! `player`-scoped datum pays the killer and a `party`-scoped one pays the party
8//! once. It is effect root **R9** ([`crate::EffectRootKind::OnKill`]).
9//!
10//! `fires` is the creator's judgement on whether a body that comes back pays
11//! again. It is required exactly where the fight comes back after the party has
12//! met it (`DW0915`) and refused as inert where it does not (`DW0914`); both
13//! refusals read one compiler-side predicate (`plan::fight_comes_back`), which
14//! needs the campaign's rest points.
15//!
16//! The types ([`crate::stages::OnKill`], [`crate::stages::KillFires`]) are
17//! stage-5 surface and live with the rest of it in [`crate::stages`]; this
18//! module holds the two refusals that need only the documents:
19//!
20//! * `DW0100` — an empty `effects` list (the exported schema's `minItems: 1`,
21//!   which serde does not enforce);
22//! * `DW0913` — a bundle on a body no player can be credited with killing: a
23//!   wave no beat spawns ([`crate::fight::wave_area`] is `None`), or an actor no
24//!   `unleash-actor` names that is not `vulnerable`
25//!   ([`crate::fight::unleashed_actors`]).
26
27use crate::diagnostic::{Diagnostic, codes};
28use crate::envelope::Campaign;
29use crate::fight::{Fight, fights, unleashed_actors, wave_area};
30
31/// The document-tier `on_kill` refusals (spec-0074 §8.1): `DW0100` for an empty
32/// `effects` list and `DW0913` for a bundle no credited kill can ever reach. A
33/// campaign that declares no bundle gets nothing.
34pub fn on_kill_checks(c: &Campaign, d: &mut Vec<Diagnostic>) {
35    let unleashed = unleashed_actors(c);
36    for (path, fight) in fights(c) {
37        let Some(ok) = fight.on_kill() else {
38            continue;
39        };
40        if ok.effects.is_empty() {
41            d.push(Diagnostic::error(
42                codes::SCHEMA,
43                "quests",
44                format!("{path}/on_kill/effects"),
45                format!(
46                    "`on_kill` on {} `{}` has no effects. The schema requires at least one \
47                     (`minItems: 1`): a bundle that does nothing is not a bundle. Add the effect \
48                     each kill should have, or remove `on_kill`.",
49                    fight.word(),
50                    fight.id()
51                ),
52            ));
53        }
54        let unreachable = match fight {
55            Fight::Wave(w) => wave_area(c, w.id.as_str()).is_none().then(|| {
56                format!(
57                    "no beat spawns wave `{}` — no `spawn-wave` names it where the engine can \
58                     seat it, so it has no bodies and no kill can be credited. Spawn it from a \
59                     beat, or remove the bundle.",
60                    w.id
61                )
62            }),
63            Fight::Actor(a) => (!a.vulnerable && !unleashed.contains(a.id.as_str())).then(|| {
64                format!(
65                    "actor `{}` is never unleashed and is not `vulnerable`, so its body is \
66                     `Invulnerable` for the whole delve and no player can be credited with \
67                     killing it. Unleash it with an `unleash-actor` beat, mark it \
68                     `vulnerable: true`, or remove the bundle.",
69                    a.id
70                )
71            }),
72        };
73        if let Some(why) = unreachable {
74            d.push(Diagnostic::error(
75                codes::ON_KILL_UNREACHABLE,
76                "quests",
77                format!("{path}/on_kill"),
78                format!(
79                    "`on_kill` on {} `{}` can never fire: {why}",
80                    fight.word(),
81                    fight.id()
82                ),
83            ));
84        }
85    }
86}