overlord_event_system/mechanics/
util.rs

1//!
2//! (`build_module` / `register_array_extensions`) is gone — there is no engine
3//! to register into. What remains here are the **pure, typed** versions of the
4//! random helpers the `util` module used to expose to scripts:
5//! `rand_round`, `get_random_element`, `rand_weight`, and `drain_random`.
6//!
7//! count and index/weight arithmetic) but operate on native typed collections
8//! script ports call the typed equivalents directly: `rand_round` delegates to
9//! [`crate::mechanics::balance::rand_round_f64`], and the
10//! drain/weight draws are reproduced by `opponent_generation`, `loop_tasks`,
11//! and `currencies` so their RNG sequences stay identical.
12
13use event_system::script::random::GameRng;
14
15use crate::mechanics::balance;
16
17/// Stochastic round of a non-integer value. Delegates to the shared native
18/// helper so every `rand_round` caller draws RNG identically.
19pub fn rand_round(value: f64, random: &GameRng) -> i64 {
20    balance::rand_round_f64(value, random)
21}
22
23/// Pick a uniformly-random element from `slice`, returning `None` for an empty
24/// `floor(random_f64() * len)`, clamped to `len - 1`.
25pub fn get_random_element<'a, T>(slice: &'a [T], random: &GameRng) -> Option<&'a T> {
26    if slice.is_empty() {
27        return None;
28    }
29    let len = slice.len() as f64;
30    let idx = (random.random_f64() * len).floor() as usize;
31    let idx = idx.min(slice.len() - 1);
32    slice.get(idx)
33}
34
35/// Remove and return a uniformly-random element from `vec`, returning `None`
36/// `floor(random_f64() * len)`, clamped, then `remove(idx)`.
37pub fn drain_random<T>(vec: &mut Vec<T>, random: &GameRng) -> Option<T> {
38    if vec.is_empty() {
39        return None;
40    }
41    let len = vec.len() as f64;
42    let idx = (random.random_f64() * len).floor() as usize;
43    let idx = idx.min(vec.len() - 1);
44    Some(vec.remove(idx))
45}
46
47/// `rand_weight`: sum the weights, draw `random_f64() * sum_weight` once, then
48/// walk the slice subtracting each weight until it goes non-positive. Returns
49/// `None` when the slice is empty or all weights sum to `<= 0.0` (matching the
50/// old `Dynamic::UNIT` return). The last element is returned as the fallback if
51/// floating-point drift leaves the accumulator positive at the end.
52pub fn rand_weight<'a, T>(random: &GameRng, weighted: &'a [(f64, T)]) -> Option<&'a T> {
53    let sum_weight: f64 = weighted.iter().map(|(w, _)| *w).sum();
54    if sum_weight <= 0.0 {
55        return None;
56    }
57    let mut weight = random.random_f64() * sum_weight;
58    for (w, value) in weighted {
59        weight -= *w;
60        if weight <= 0.0 {
61            return Some(value);
62        }
63    }
64    weighted.last().map(|(_, value)| value)
65}