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}