delvewright_dsl/difficulty.rs
1//! The world difficulty a campaign ships at, and the bodies it stages.
2//!
3//! A campaign may declare `world.difficulty` (`easy` / `normal` / `hard`;
4//! `peaceful` is refused, `DW0468`). Absent, the engine **derives** it, and the
5//! derivation never picks a difficulty that removes a body the campaign stages:
6//! peaceful is the only difficulty that removes any body (see
7//! [`crate::registry::peaceful_despawn`]), so the derivation is `easy` when the
8//! campaign fields any wave or stages any body of a type peaceful discards, and
9//! `peaceful` otherwise. One function, read by the emitter (`server.properties`),
10//! the combat arithmetic and the `DW0469` advisory alike.
11
12use crate::envelope::Campaign;
13use crate::{BodyRef, Verb, WorldDifficulty};
14
15/// One body a campaign can put into the world, with the entity type it ships
16/// as.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct StagedBody {
19 /// What declares it: `wave-mob`, `npc`, `actor`, `actor-twin` (the
20 /// real-AI body an `unleash-actor` summons) or `volley-projectile`.
21 pub what: &'static str,
22 /// The declaring object's id (the wave, NPC or actor id; for a volley, the
23 /// JSON pointer of the `volley` effect).
24 pub id: String,
25 /// The entity type the body ships as.
26 pub entity: String,
27}
28
29/// Every body the campaign stages whose entity type the author chose, in a
30/// fixed order: wave mobs, NPCs, actors (the puppet as it ships — a skinned one
31/// is a mannequin — then the twin when the actor is unleashed), volley
32/// projectiles.
33pub fn staged_bodies(c: &Campaign) -> Vec<StagedBody> {
34 let q = &c.quests.content;
35 let mut out = Vec::new();
36 for w in &q.waves {
37 for m in &w.mobs {
38 out.push(StagedBody {
39 what: "wave-mob",
40 id: w.id.as_str().to_string(),
41 entity: m.entity.clone(),
42 });
43 }
44 }
45 for n in &c.npcs.content.npcs {
46 out.push(StagedBody {
47 what: "npc",
48 id: n.id.as_str().to_string(),
49 entity: BodyRef::Npc(n).worn_entity().to_string(),
50 });
51 }
52 let unleashed = crate::fight::unleashed_actors(c);
53 for a in &q.actors {
54 out.push(StagedBody {
55 what: "actor",
56 id: a.id.as_str().to_string(),
57 entity: BodyRef::Actor(a).worn_entity().to_string(),
58 });
59 if unleashed.contains(a.id.as_str()) {
60 out.push(StagedBody {
61 what: "actor-twin",
62 id: a.id.as_str().to_string(),
63 entity: a.entity.clone(),
64 });
65 }
66 }
67 crate::for_each_campaign_effect(c, &mut |path, _, eff| {
68 if let Verb::Volley { projectile, .. } = &eff.verb {
69 out.push(StagedBody {
70 what: "volley-projectile",
71 id: path.to_string(),
72 entity: projectile
73 .as_deref()
74 .unwrap_or(crate::DEFAULT_VOLLEY_PROJECTILE)
75 .to_string(),
76 });
77 }
78 });
79 out
80}
81
82/// The staged bodies the pinned game would discard on peaceful.
83pub fn bodies_removed_on_peaceful(c: &Campaign) -> Vec<StagedBody> {
84 staged_bodies(c)
85 .into_iter()
86 .filter(|b| crate::registry::removed_on_peaceful(&b.entity))
87 .collect()
88}
89
90/// The difficulty the engine derives for a campaign that declares none: the
91/// lowest that keeps every staged body and every declared fight. `easy` when
92/// the campaign fields any wave or stages a body peaceful discards, else
93/// `peaceful`.
94pub fn derived_difficulty(c: &Campaign) -> WorldDifficulty {
95 if !c.quests.content.waves.is_empty() || !bodies_removed_on_peaceful(c).is_empty() {
96 WorldDifficulty::Easy
97 } else {
98 WorldDifficulty::Peaceful
99 }
100}
101
102/// The difficulty the delve ships at: the declared one, else
103/// [`derived_difficulty`].
104pub fn effective_difficulty(c: &Campaign) -> WorldDifficulty {
105 c.world
106 .content
107 .difficulty
108 .unwrap_or_else(|| derived_difficulty(c))
109}