overlord_event_system/mechanics/
content.rs

1//! Native `content` script-module dispatchers plus the per-ability
2//! `ability_info(level)` composition for the `content_raw`-style ability map.
3//!
4//! These are `pub` Rust fns that the native script ports
5//! (`behaviors::description_values`, `behaviors::expression`'s
6//! `chest_item_choose`) call. The balance primitives they use
7//! (`balance::ability_damage_from_id`, `balance::effect_duration`, consts
8//! `AOE_COEF`/`OT_COEF`/`HEAL_COEF`) live in [`crate::mechanics::balance`].
9
10use configs::game_config::GameConfig;
11use essences::items::ItemTemplate;
12use uuid::Uuid;
13
14use crate::game_config_helpers::GameConfigLookup;
15use crate::mechanics::balance;
16use crate::mechanics::content_lookups::ContentLookups;
17
18/// The typed result of the per-ability `ability_info(level)` closures. Each
19/// struct holds every key that appears across the 18 bespoke ability infos, with
20/// `None` for keys a given ability does not set.
21///
22/// `PartialEq + Serialize` so it can be diffed/logged if ever surfaced directly.
23/// The downstream `description_values` fn reads individual fields, computing
24/// `floor(ability_info.<field> * 100)`.
25#[derive(Debug, Clone, PartialEq, serde::Serialize)]
26pub struct AbilityInfo {
27    /// `damage` — instant damage component. Present on all damage abilities.
28    pub damage: Option<f64>,
29    /// `dot` — damage-over-time component.
30    pub dot: Option<f64>,
31    /// `hot` — heal-over-time component.
32    pub hot: Option<f64>,
33    /// `effect_duration` — buff/debuff duration (seconds), from
34    /// `balance::effect_duration`.
35    pub effect_duration: Option<f64>,
36    /// `duration` — integer tick/second duration literal.
37    pub duration: Option<i64>,
38    /// `crit_chance_bonus` — added crit chance (fraction).
39    pub crit_chance_bonus: Option<f64>,
40    /// `vampiric` — life-steal fraction.
41    pub vampiric: Option<f64>,
42    /// `projectiles` — projectile count (integer).
43    pub projectiles: Option<i64>,
44}
45
46impl AbilityInfo {
47    /// All-`None` base; closures set the keys they produce.
48    fn empty() -> Self {
49        AbilityInfo {
50            damage: None,
51            dot: None,
52            hot: None,
53            effect_duration: None,
54            duration: None,
55            crit_chance_bonus: None,
56            vampiric: None,
57            projectiles: None,
58        }
59    }
60}
61
62/// Native port of the `content` module's `get_ability_info(id, level)`:
63/// dispatch to the per-ability `ability_info(level)` closure baked into the
64/// ability map. Returns `None` for ability ids that have no `ability_info`
65///
66/// Faithfully mirrors each of the 18 closures'
67/// arithmetic; the per-ability constants are ported verbatim from
68pub fn ability_info(
69    config: &GameConfig,
70    lookups: &ContentLookups,
71    ability_id: Uuid,
72    level: i64,
73) -> anyhow::Result<AbilityInfo> {
74    // `balance::ability_damage(uuid(id), level)` in every closure.
75    let dmg = |id: Uuid| -> anyhow::Result<f64> {
76        balance::ability_damage_from_id(config, lookups, id, level)
77            .map_err(|e| anyhow::anyhow!("balance::ability_damage({id}): {e}"))
78    };
79
80    let id_str = ability_id.to_string();
81    let mut info = AbilityInfo::empty();
82
83    match id_str.as_str() {
84        // damage: power
85        "0194d64e-20f2-75e5-89c8-4cb812672485" => {
86            info.damage = Some(dmg(ability_id)?);
87        }
88        // DOT split: damage = base*K*INSTANT, dot = base*K*DOT_PART
89        "01958172-9e65-7061-9d15-56b2c33cc13e" => {
90            const DOT_PART: f64 = 0.5;
91            const INSTANT: f64 = 1.0 - DOT_PART;
92            let base_dmg = dmg(ability_id)?;
93            let k = INSTANT + DOT_PART * balance::OT_COEF;
94            info.damage = Some(base_dmg * k * INSTANT);
95            info.dot = Some(base_dmg * k * DOT_PART);
96        }
97        // crit_chance_bonus + de-rated damage
98        "019584aa-5bde-7ac2-8850-076dafdc4603" => {
99            const CRIT_CHANCE_BONUS: f64 = 0.3;
100            let overall = dmg(ability_id)?;
101            let raw = overall / (1.0 + CRIT_CHANCE_BONUS);
102            info.crit_chance_bonus = Some(CRIT_CHANCE_BONUS);
103            info.damage = Some(raw);
104        }
105        // AoE-scaled damage
106        "019584f4-2c99-72bf-bcd3-bfc02fb33977" => {
107            info.damage = Some(dmg(ability_id)? * balance::AOE_COEF);
108        }
109        // PROJECTILES split
110        "019589e6-f9dd-7b22-8d39-5350e95aaf69" => {
111            const PROJECTILES: i64 = 3;
112            info.projectiles = Some(PROJECTILES);
113            info.damage = Some(dmg(ability_id)? / (PROJECTILES as f64));
114        }
115        // vampiric, damage de-rated by VAMPIRIC/HEAL_COEF
116        "019589f7-f4a3-701c-bc0f-f60d980ae250" => {
117            const VAMPIRIC: f64 = 0.3;
118            info.damage = Some(dmg(ability_id)? / (1.0 + VAMPIRIC / balance::HEAL_COEF));
119            info.vampiric = Some(VAMPIRIC);
120        }
121        // empower budget → effect_duration + damage
122        "01958a30-8f18-745f-928a-75028cb3ee99" => {
123            const EMPOWER_BUDGET: f64 = 0.3;
124            let raw_power = dmg(ability_id)?;
125            info.effect_duration = Some(balance::effect_duration(raw_power * EMPOWER_BUDGET));
126            info.damage = Some(raw_power * (1.0 - EMPOWER_BUDGET));
127        }
128        // hot only. Blessing of Life: a HoT, so it gets HEAL_COEF (like all
129        // healing) — but NOT also OT_COEF. The old `* HEAL_COEF * OT_COEF`
130        // double-counted (×1.2×1.2 = ×1.44), giving this one ability a unique
131        // 44% bonus that no peer heal/HoT had (vs e.g. vampiric touch), making it
132        // imbalanced. A HoT's over-time nature is already its identity; it should
133        // not also be paid the over-time coefficient on top of the heal one.
134        "01958ed0-d45f-7cad-b086-8f11962d3859" => {
135            info.hot = Some(dmg(ability_id)? * balance::HEAL_COEF);
136        }
137        // vulnerability budget → effect_duration + damage
138        "01958ef2-dff5-76dd-89f1-d9c2707b2ffc" => {
139            const VULNERABILITY_BUDGET: f64 = 0.3;
140            let raw_power = dmg(ability_id)?;
141            info.effect_duration = Some(balance::effect_duration(raw_power * VULNERABILITY_BUDGET));
142            info.damage = Some(raw_power * (1.0 - VULNERABILITY_BUDGET));
143        }
144        // OT-scaled damage + integer duration
145        "01958efd-77f9-7dec-8444-c9d759549225" => {
146            let damage = dmg(ability_id)?;
147            info.damage = Some(damage * balance::OT_COEF);
148            info.duration = Some(5);
149        }
150        // plain damage abilities
151        "019a0245-ff0c-7964-b345-ded525c71e74"
152        | "019a0246-5aaf-7c01-9a1a-3969e04129ec"
153        | "019a0246-cf87-73b0-b701-f8788cf9cc08"
154        | "019bff40-af44-75b7-940c-6074097a2925"
155        | "019c00a4-38c9-7859-a642-fd82c25ef285"
156        | "019cc464-e752-71c1-a9dd-8fda9f212801"
157        | "019cc465-14b8-7dbc-9799-4691b91805d3"
158        | "019cc465-2f63-7b54-8ddc-fcbcb483fe81" => {
159            info.damage = Some(dmg(ability_id)?);
160        }
161        other => {
162            anyhow::bail!("content::ability_info: no ability_info closure for ability id {other}");
163        }
164    }
165
166    Ok(info)
167}
168
169/// Native port of `content::get_item_by_code(code)`: find the item template
170/// whose `next_mimic_item_code` equals `code`, via
171/// [`ContentLookups::item_id_by_mimic_code`] (built at init from
172/// `ItemTemplate::next_mimic_item_code`). Returns the matching config
173/// returns the `content_raw` item map (the chest script then reads `item.id`).
174pub fn get_item_by_code<'a>(
175    config: &'a GameConfig,
176    lookups: &ContentLookups,
177    code: i64,
178) -> Option<&'a ItemTemplate> {
179    let id = lookups.item_id_by_mimic_code.get(&code).copied()?;
180    config.item_template(id)
181}
182
183/// One entry of `content_raw::inventory_levels`, mirroring the fields the
184/// `chest_item_choose_script` reads (`from_chapter_level`, `item_types`). This
185/// is exactly [`essences::item_case::InventoryLevel`].
186pub type InventoryLevel = essences::item_case::InventoryLevel;
187
188/// Native port of `content::get_inventory_levels()`: the configured inventory
189/// levels (`from_chapter_level`, `item_types`). Borrowed from config rather than
190/// cloned; callers sort/filter a local view.
191pub fn get_inventory_levels(config: &GameConfig) -> &[InventoryLevel] {
192    &config.inventory_levels
193}