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}