overlord_event_system/behaviors/
items.rs

1//! Native functions for the per-attribute item value calculation slots —
2//! `cases.rs::try_finalize_item` via `run_expression::<i64>` with scope
3//! `Item` / `Random` / `AttributesQuantity`).
4//!
5//! Each fn computes one attribute's value, referenced from the attribute's
6//! `calculation_behavior`. The production `Random` is *fresh entropy*
7//! (`GameRng::from_entropy` in `cases.rs`).
8//!
9//! All formula primitives live in [`crate::mechanics::balance`]. Each fn
10//! maps to exactly one of the 16 attributes (keyed by attribute uuid):
11//!
12//! | uuid suffix    | attribute      | native fn               |
13//! |----------------|----------------|-------------------------|
14//! | a3d34c967544   | Health         | `attr_health`           |
15//! | a3d484fc9321   | Armor          | `attr_armor`            |
16//! | a3d5ae6a6d85   | Damage         | `attr_damage`           |
17//! | a3d6986afce1   | Crit_Chance    | `attr_crit_chance`      |
18//! | d17688fe8796   | Crit_Damage    | `attr_crit_damage`      |
19//! | b7a9f4076158   | Evasion        | `attr_evasion`          |
20//! | 9bc89817cd0c   | Speed          | `attr_speed`            |
21//! | 58be88146385   | HP_Regen       | `attr_hp_regen`         |
22//! | 0533377e501c   | Multi_Cast     | `attr_multi_cast`       |
23//! | b5389ac4560b   | Counter_Attack | `attr_counter_attack`   |
24//! | 68dc4f9806b8   | Damage_Received| `attr_zero` (empty)     |
25//! | 5041af05d473   | Bravery        | `attr_bravery`          |
26//! | cb869d6d3f95   | Guile          | `attr_guile`            |
27//! | 4469382ee36c   | Block          | `attr_block`            |
28//! | 9bfc229665c3   | Bonus_Health   | `attr_zero` (`0`)       |
29//! | f5461b96d9ff   | Bonus_damage   | `attr_zero` (`0`)       |
30
31use configs::game_config::GameConfig;
32use essences::items::{Item, ItemType};
33use event_system::script::random::GameRng;
34use uuid::Uuid;
35
36use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
37use crate::mechanics::balance;
38use crate::mechanics::content_lookups::ContentLookups;
39
40/// Inputs available to an item-attribute native fn — mirrors the three `const`s
41/// the `cases.rs::try_finalize_item` caller pushes (`Item`, `Random`,
42/// `AttributesQuantity`), plus the config / content lookups the `balance`
43/// primitives need.
44pub struct ItemAttributeCtx<'a> {
45    pub item: &'a Item,
46    pub attributes_quantity: i64,
47    pub random: &'a GameRng,
48    pub config: &'a GameConfig,
49    pub lookups: &'a ContentLookups,
50}
51
52/// Signature of an item-attribute native fn. Returns the attribute value
53/// (`i64`), matching `run_expression::<i64>` (the value is later narrowed to
54pub type ItemAttributeFn = fn(&ItemAttributeCtx) -> anyhow::Result<i64>;
55
56/// `balance::eff_item(Item)`.
57fn eff_item(ctx: &ItemAttributeCtx) -> f64 {
58    balance::eff_item_with_config(
59        ctx.config,
60        ctx.lookups,
61        ctx.item.item_template_id,
62        ctx.item.level as f64,
63    )
64}
65
66/// `balance::attr_spread(Random, Item)`.
67fn attr_spread(ctx: &ItemAttributeCtx) -> f64 {
68    balance::attr_spread_for_item(
69        ctx.config,
70        ctx.lookups,
71        ctx.random,
72        ctx.item.item_template_id,
73        ctx.item.level as f64,
74    )
75}
76
77/// `balance::aux_attr_eff(eff, Random, Item)`.
78fn aux_attr_eff(ctx: &ItemAttributeCtx, base_eff: f64) -> f64 {
79    balance::aux_attr_eff_for_item(
80        ctx.config,
81        ctx.lookups,
82        base_eff,
83        ctx.random,
84        ctx.item.item_template_id,
85        ctx.item.level as f64,
86    )
87}
88
89/// Health (`...a3d34c967544`):
90pub fn attr_health(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
91    let aux_attrs = (ctx.attributes_quantity - balance::MAIN_ATTRS_QUANTITY) as f64;
92    let base_attrs_impact = 1.0 - aux_attrs * balance::AUX_ATTR_IMPACT;
93    let base_attr_impact = base_attrs_impact * 0.5;
94    let eff = eff_item(ctx);
95    let rand_mod = attr_spread(ctx);
96    let attr_eff = (eff * rand_mod).powf(base_attr_impact);
97    let hp_k = balance::hp_k_for_level(ctx.item.level as f64);
98    Ok((balance::BASE_HP * attr_eff * hp_k / 10.0).floor() as i64)
99}
100
101/// Armor (`...a3d484fc9321`):
102pub fn attr_armor(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
103    let dmg_increase = balance::eff_spell_by_level(ctx.item.level as f64);
104    let spread = attr_spread(ctx);
105    let dr = (1.0 - 1.0 / (dmg_increase * spread)).max(0.03) * 10000.0;
106    Ok((dr / 10.0).floor() as i64)
107}
108
109/// Damage (`...a3d5ae6a6d85`):
110pub fn attr_damage(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
111    let aux_attrs = (ctx.attributes_quantity - balance::MAIN_ATTRS_QUANTITY) as f64;
112    let base_attrs_impact = 1.0 - aux_attrs * balance::AUX_ATTR_IMPACT;
113    let base_attr_impact = base_attrs_impact * 0.5;
114    let eff = eff_item(ctx);
115    let rand_mod = attr_spread(ctx);
116    // == `(eff ** base_attr_impact) * rand_mod`.
117    let attr_eff = eff.powf(base_attr_impact) * rand_mod;
118    Ok((balance::BASE_ATTACK * attr_eff / 10.0).floor() as i64)
119}
120
121/// Crit_Chance (`...a3d6986afce1`):
122pub fn attr_crit_chance(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
123    let eff = eff_item(ctx);
124    let attr_eff = aux_attr_eff(ctx, eff).powi(2);
125    let crit_chance = (-1.0 + (8.0 * attr_eff - 7.0).powf(0.5)) / 4.0;
126    Ok((crit_chance * 10000.0 / 10.0).floor() as i64)
127}
128
129/// Crit_Damage (`...d17688fe8796`):
130pub fn attr_crit_damage(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
131    let eff = eff_item(ctx);
132    let attr_eff = aux_attr_eff(ctx, eff).powi(2);
133    let crit_mod = (-1.0 + (8.0 * attr_eff - 7.0).powf(0.5)) / 2.0;
134    Ok((crit_mod * 10000.0 / 10.0).floor() as i64)
135}
136
137/// Evasion (`...b7a9f4076158`):
138pub fn attr_evasion(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
139    let eff = eff_item(ctx);
140    let attr_eff = aux_attr_eff(ctx, eff);
141    let evasion_chance = 1.0 - 1.0 / attr_eff;
142    Ok((evasion_chance * 10000.0 / 10.0).floor() as i64)
143}
144
145/// Speed (`...9bc89817cd0c`):
146pub fn attr_speed(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
147    let eff = eff_item(ctx);
148    let attr_eff = aux_attr_eff(ctx, eff);
149    Ok(((attr_eff - 1.0) * 10000.0 / 10.0).floor() as i64)
150}
151
152/// HP_Regen (`...58be88146385`):
153pub fn attr_hp_regen(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
154    let aux_attrs = (ctx.attributes_quantity - balance::MAIN_ATTRS_QUANTITY) as f64;
155    let eff = eff_item(ctx);
156    let base_attr_eff = eff.powf(1.0 - aux_attrs * balance::AUX_ATTR_IMPACT);
157    let hp_eff = base_attr_eff.powf(0.5);
158    let hp = hp_eff * balance::BASE_HP;
159    let attr_eff = aux_attr_eff(ctx, eff);
160    let hp_per_sec = hp * (attr_eff - 1.0) / (balance::FIGHT_DURATION * attr_eff);
161    Ok((hp_per_sec / 10.0).floor() as i64)
162}
163
164/// Multi_Cast (`...0533377e501c`):
165pub fn attr_multi_cast(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
166    let eff = eff_item(ctx);
167    let attr_eff = aux_attr_eff(ctx, eff);
168    Ok(((attr_eff - 1.0) * 10000.0 / 10.0).floor() as i64)
169}
170
171/// Counter_Attack (`...b5389ac4560b`):
172pub fn attr_counter_attack(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
173    let eff = eff_item(ctx);
174    let attr_eff = aux_attr_eff(ctx, eff);
175    let dmg_increase = balance::eff_spell_by_level(ctx.item.level as f64);
176    let dps = dmg_increase * balance::SPELL_QUANTITY as f64;
177    let overall_damage = dps * balance::FIGHT_DURATION;
178    let added_damage = overall_damage * (attr_eff - 1.0);
179    let attacks_per_fight = balance::FIGHT_DURATION * balance::ATTACKS_PER_SEC;
180    let p = added_damage / attacks_per_fight / balance::COUNTERATTACK_POWER;
181    Ok((p * 10000.0 / 10.0).floor() as i64)
182}
183
184/// Bravery (`...5041af05d473`):
185pub fn attr_bravery(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
186    let eff = eff_item(ctx);
187    let attr_eff = aux_attr_eff(ctx, eff);
188    let p = balance::bravery_p_from_eff(attr_eff);
189    Ok((p * 10000.0 / 10.0).floor() as i64)
190}
191
192/// Guile (`...cb869d6d3f95`):
193pub fn attr_guile(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
194    let eff = eff_item(ctx);
195    let attr_eff = aux_attr_eff(ctx, eff);
196    let p = balance::deceit_p_from_eff(attr_eff);
197    Ok((p * 10000.0 / 10.0).floor() as i64)
198}
199
200/// Block (`...4469382ee36c`):
201pub fn attr_block(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
202    let eff = eff_item(ctx);
203    // f64. Mirror with `f64::min(_, 2.0)`.
204    let attr_eff = aux_attr_eff(ctx, eff).min(2.0);
205    let block_chance = 2.0 - 2.0 / attr_eff;
206    Ok((block_chance * 10000.0 / 10.0).floor() as i64)
207}
208
209/// Trivial slots: Damage_Received (empty script), Bonus_Health (`0`),
210/// `0` as `i64`.
211pub fn attr_zero(_ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
212    Ok(0)
213}
214
215/// Register this category's native fns into the registry (Expression category —
216/// scalar `i64` slots, same as `item_experience`).
217pub fn register(registry: &mut BehaviorRegistry) {
218    let fns: &[(&str, &str, &str, ItemAttributeFn)] = &[
219        (
220            "attr_health",
221            "Атрибут: здоровье",
222            "Порт calculation_behavior атрибута Health.",
223            attr_health,
224        ),
225        (
226            "attr_armor",
227            "Атрибут: броня",
228            "Порт calculation_behavior атрибута Armor.",
229            attr_armor,
230        ),
231        (
232            "attr_damage",
233            "Атрибут: урон",
234            "Порт calculation_behavior атрибута Damage.",
235            attr_damage,
236        ),
237        (
238            "attr_crit_chance",
239            "Атрибут: шанс крита",
240            "Порт calculation_behavior атрибута Crit_Chance.",
241            attr_crit_chance,
242        ),
243        (
244            "attr_crit_damage",
245            "Атрибут: крит. урон",
246            "Порт calculation_behavior атрибута Crit_Damage.",
247            attr_crit_damage,
248        ),
249        (
250            "attr_evasion",
251            "Атрибут: уклонение",
252            "Порт calculation_behavior атрибута Evasion.",
253            attr_evasion,
254        ),
255        (
256            "attr_speed",
257            "Атрибут: скорость",
258            "Порт calculation_behavior атрибута Speed.",
259            attr_speed,
260        ),
261        (
262            "attr_hp_regen",
263            "Атрибут: реген HP",
264            "Порт calculation_behavior атрибута HP_Regen.",
265            attr_hp_regen,
266        ),
267        (
268            "attr_multi_cast",
269            "Атрибут: мультикаст",
270            "Порт calculation_behavior атрибута Multi_Cast.",
271            attr_multi_cast,
272        ),
273        (
274            "attr_counter_attack",
275            "Атрибут: контратака",
276            "Порт calculation_behavior атрибута Counter_Attack.",
277            attr_counter_attack,
278        ),
279        (
280            "attr_bravery",
281            "Атрибут: храбрость",
282            "Порт calculation_behavior атрибута Bravery.",
283            attr_bravery,
284        ),
285        (
286            "attr_guile",
287            "Атрибут: коварство",
288            "Порт calculation_behavior атрибута Guile.",
289            attr_guile,
290        ),
291        (
292            "attr_block",
293            "Атрибут: блок",
294            "Порт calculation_behavior атрибута Block.",
295            attr_block,
296        ),
297        (
298            "attr_zero",
299            "Атрибут: ноль",
300            "Возвращает 0 (порт пустых / `0` calculation_behavior: \
301             Damage_Received, Bonus_Health, Bonus_damage).",
302            attr_zero,
303        ),
304    ];
305    for (name, title, description, f) in fns {
306        registry.register_item_attribute(
307            BehaviorMeta {
308                name: name.to_string(),
309                category: BehaviorKind::ItemAttribute,
310                title: title.to_string(),
311                description: description.to_string(),
312            },
313            *f,
314        );
315    }
316}
317
318/// TODO: source from config. Per-slot fallback item template for the chest
319/// item-choose path (slot name → item template uuid).
320const ITEMS_FOR_SLOTS: &[(&str, &str)] = &[
321    ("Boots", "0194d64e-216d-7059-863f-26f67c64267b"),
322    ("Torso", "0194d64e-216d-7059-863f-26f7e69b8f4a"),
323    ("Head", "0194d64e-216d-7059-863f-26f85eb1e25f"),
324    ("Gloves", "0194d64e-216d-7059-863f-26f9ab123d46"),
325    ("Ring", "0194d64e-216d-7059-863f-26fa1fc8410b"),
326    ("Waist", "0194d64e-216d-7059-863f-26fb000bf4e9"),
327    ("Legs", "0194d64e-216d-7059-863f-26fc9c33e67e"),
328    ("Shoulders", "0194d64e-216d-7059-863f-26fddb278b04"),
329    ("Neck", "0194d64e-216d-7059-863f-26fec3786536"),
330    ("Weapon", "0194d64e-216d-7059-863f-26ff77d309a3"),
331];
332
333/// Inputs for the item sell-price / experience calculations (code-dispatched
334/// from item finalization).
335pub struct ItemPriceCtx<'a> {
336    pub item: &'a Item,
337    pub config: &'a GameConfig,
338    pub lookups: &'a ContentLookups,
339}
340
341/// Sell price of an item, from its effectiveness `eff` via
342/// [`balance::sell_price`] (Phase 2: sub-linear in `eff` so gold income grows
343/// ~linearly with item level — model's linear-income assumption — letting the
344/// geometric chest-upgrade cost bind instead of being swamped by surplus; early
345/// low-`eff` sales stay ≈ the legacy `eff·100`).
346pub fn item_price(
347    ctx: &ItemPriceCtx,
348) -> anyhow::Result<Vec<event_system::script::types::ESCurrencyUnit>> {
349    let eff = crate::mechanics::balance::eff_item_with_config(
350        ctx.config,
351        ctx.lookups,
352        ctx.item.item_template_id,
353        ctx.item.level as f64,
354    );
355    let price = crate::mechanics::balance::sell_price(eff);
356    Ok(vec![event_system::script::types::ESCurrencyUnit {
357        currency_id: Uuid::from_u128(0x0194d64e_2386_7020_8b01_d6b3d5424506),
358        amount: price,
359    }])
360}
361
362/// Inputs for the item-experience calculation (code-dispatched).
363pub struct ItemExperienceCtx<'a> {
364    pub item: &'a Item,
365    pub config: &'a GameConfig,
366    pub lookups: &'a ContentLookups,
367}
368
369/// Item experience: `floor(balance::eff_item * 100)`.
370pub fn item_experience_eff_item(ctx: &ItemExperienceCtx) -> anyhow::Result<i64> {
371    let eff = crate::mechanics::balance::eff_item_with_config(
372        ctx.config,
373        ctx.lookups,
374        ctx.item.item_template_id,
375        ctx.item.level as f64,
376    );
377    Ok((eff * 100.0).floor() as i64)
378}
379
380/// Inputs for the chest item-choose override (code-dispatched).
381pub struct ChestItemChooseCtx<'a> {
382    pub character: &'a essences::character_state::CharacterState,
383    pub config: &'a GameConfig,
384    pub lookups: &'a ContentLookups,
385}
386
387/// Chest item override: the mimic item by `next_mimic_item_code`, else the
388/// per-slot fallback for the first unequipped slot of the current inventory
389/// level, else `None` (roll by weights).
390pub fn chest_item_choose(ctx: &ChestItemChooseCtx) -> anyhow::Result<Option<Uuid>> {
391    use crate::mechanics::content;
392
393    let character = &ctx.character.character;
394
395    // Branch 1: explicit mimic custom item code (absent or 0 → no override).
396    if let Some(&code) = character.custom_values.0.get("next_mimic_item_code")
397        && code != 0
398        && let Some(item) = content::get_item_by_code(ctx.config, ctx.lookups, code)
399    {
400        return Ok(Some(item.id));
401    }
402
403    // Branch 2: per-slot fallback by inventory level (highest
404    // `from_chapter_level <= current_chapter_level`).
405    let mut levels: Vec<&content::InventoryLevel> =
406        content::get_inventory_levels(ctx.config).iter().collect();
407    levels.sort_by_key(|l| std::cmp::Reverse(l.from_chapter_level));
408    let Some(current_level) = levels
409        .into_iter()
410        .find(|l| l.from_chapter_level <= character.current_chapter_level)
411    else {
412        return Ok(None);
413    };
414
415    // Artifact is never granted via chests — exclude it from the slot list so
416    // the per-slot fallback never returns an Artifact template.
417    let mut slots: Vec<String> = current_level
418        .item_types
419        .iter()
420        .filter(|t| **t != ItemType::Artifact)
421        .map(|t| t.to_string())
422        .collect();
423    for item in &ctx.character.inventory {
424        if item.is_equipped {
425            let equipped = item.item_type.to_string();
426            slots.retain(|s| *s != equipped);
427        }
428    }
429
430    if let Some(selected_slot) = slots.first() {
431        // Prefer the hardcoded per-slot fallback item (production parity). If
432        // it is not present in the loaded config (e.g. a synthetic test
433        // config), fall back to any config item of that slot type.
434        if let Some((_, uuid_str)) = ITEMS_FOR_SLOTS.iter().find(|(s, _)| s == selected_slot) {
435            let selected = Uuid::parse_str(uuid_str)
436                .map_err(|e| anyhow::anyhow!("ITEMS_FOR_SLOTS bad uuid {uuid_str:?}: {e}"))?;
437            if ctx.config.items.iter().any(|i| i.id == selected) {
438                return Ok(Some(selected));
439            }
440        }
441        if let Some(item) = ctx
442            .config
443            .items
444            .iter()
445            .find(|i| i.item_type.to_string() == *selected_slot)
446        {
447            return Ok(Some(item.id));
448        }
449    }
450
451    Ok(None)
452}