veloren_common/
lottery.rs

1// Example for calculating a drop rate:
2//
3// On every roll an f32 between 0 and 1 is created.
4// For every loot table a total range is created by the sum of the individual
5// ranges per item.
6//
7// This range is the sum of all single ranges defined per item in a table.
8//                                                   // Individual Range
9// (3, "common.items.food.cheese"),                  // 0.0..3.0
10// (3, "common.items.food.apple"),                   // 3.0..6.0
11// (3, "common.items.food.mushroom"),                // 6.0..9.0
12// (1, "common.items.food.coconut"),                 // 9.0..10.0
13// (0.05, "common.items.food.apple_mushroom_curry"), // 10.0..10.05
14// (0.10, "common.items.food.apple_stick"),          // 10.05..10.15
15// (0.10, "common.items.food.mushroom_stick"),       // 10.15..10.25
16//
17// The f32 is multiplied by the max. value needed to drop an item in this
18// particular table. X = max. value needed = 10.15
19//
20// Example roll
21// [Random Value 0..1] * X = Number inside the table's total range
22// 0.45777 * X = 4.65
23// 4.65 is in the range of 3.0..6.0 => Apple drops
24//
25// Example drop chance calculation
26// Cheese drop rate = 3/X = 29.6%
27// Coconut drop rate = 1/X = 9.85%
28
29use std::hash::Hash;
30
31use crate::{
32    assets::{self, AssetExt},
33    comp::{Item, inventory::item},
34};
35use rand::prelude::*;
36use serde::{Deserialize, Serialize, de::DeserializeOwned};
37use tracing::warn;
38
39#[derive(Clone, Debug, PartialEq, Deserialize)]
40pub struct Lottery<T> {
41    items: Vec<(f32, T)>,
42    total: f32,
43}
44
45impl<T: DeserializeOwned + Send + Sync + 'static> assets::Asset for Lottery<T> {
46    type Loader = assets::LoadFrom<Vec<(f32, T)>, assets::RonLoader>;
47
48    const EXTENSION: &'static str = "ron";
49}
50
51impl<T> From<Vec<(f32, T)>> for Lottery<T> {
52    fn from(mut items: Vec<(f32, T)>) -> Lottery<T> {
53        let mut total = 0.0;
54
55        for (rate, _) in &mut items {
56            total += *rate;
57            *rate = total - *rate;
58        }
59
60        Self { items, total }
61    }
62}
63
64impl<T> Lottery<T> {
65    pub fn choose_seeded(&self, seed: u32) -> &T {
66        let x = ((seed % 65536) as f32 / 65536.0) * self.total;
67        &self.items[self
68            .items
69            .binary_search_by(|(y, _)| y.partial_cmp(&x).unwrap())
70            .unwrap_or_else(|i| i.saturating_sub(1))]
71        .1
72    }
73
74    pub fn choose(&self) -> &T { self.choose_seeded(thread_rng().gen()) }
75
76    pub fn iter(&self) -> impl Iterator<Item = &(f32, T)> { self.items.iter() }
77
78    pub fn total(&self) -> f32 { self.total }
79}
80
81/// Try to distribute stacked items fairly between weighted participants.
82pub fn distribute_many<T: Copy + Eq + Hash, I>(
83    participants: impl IntoIterator<Item = (f32, T)>,
84    rng: &mut impl Rng,
85    items: &[I],
86    mut get_amount: impl FnMut(&I) -> u32,
87    mut exec_item: impl FnMut(&I, T, u32),
88) {
89    struct Participant<T> {
90        // weight / total
91        weight: f32,
92        sorted_weight: f32,
93        data: T,
94        recieved_count: u32,
95        current_recieved_count: u32,
96    }
97
98    impl<T> Participant<T> {
99        fn give(&mut self, amount: u32) {
100            self.current_recieved_count += amount;
101            self.recieved_count += amount;
102        }
103    }
104
105    // Nothing to distribute, we can return early.
106    if items.is_empty() {
107        return;
108    }
109
110    let mut total_weight = 0.0;
111
112    let mut participants = participants
113        .into_iter()
114        .map(|(weight, participant)| Participant {
115            weight,
116            sorted_weight: {
117                total_weight += weight;
118                total_weight - weight
119            },
120            data: participant,
121            recieved_count: 0,
122            current_recieved_count: 0,
123        })
124        .collect::<Vec<_>>();
125
126    let total_item_amount = items.iter().map(&mut get_amount).sum::<u32>();
127
128    let mut current_total_weight = total_weight;
129
130    for item in items.iter() {
131        let amount = get_amount(item);
132        let mut distributed = 0;
133
134        let Some(mut give) = participants
135            .iter()
136            .map(|participant| {
137                (total_item_amount as f32 * participant.weight / total_weight).ceil() as u32
138                    - participant.recieved_count
139            })
140            .min()
141        else {
142            tracing::error!("Tried to distribute items to no participants.");
143            return;
144        };
145
146        while distributed < amount {
147            // Can't give more than amount, and don't give more than the average between all
148            // to keep things well distributed.
149            let max_give = (amount / participants.len() as u32).clamp(1, amount - distributed);
150            give = give.clamp(1, max_give);
151            let x = rng.gen_range(0.0..=current_total_weight);
152
153            let index = participants
154                .binary_search_by(|item| item.sorted_weight.partial_cmp(&x).unwrap())
155                .unwrap_or_else(|i| i.saturating_sub(1));
156
157            let participant_count = participants.len();
158
159            let Some(winner) = participants.get_mut(index) else {
160                tracing::error!("Tried to distribute items to no participants.");
161                return;
162            };
163
164            winner.give(give);
165            distributed += give;
166
167            // If a participant has received enough, remove it.
168            if participant_count > 1
169                && winner.recieved_count as f32 / total_item_amount as f32
170                    >= winner.weight / total_weight
171            {
172                current_total_weight = index
173                    .checked_sub(1)
174                    .and_then(|i| Some(participants.get(i)?.sorted_weight))
175                    .unwrap_or(0.0);
176                let winner = participants.swap_remove(index);
177                exec_item(item, winner.data, winner.current_recieved_count);
178
179                // Keep participant weights correct so that we can binary search it.
180                for participant in &mut participants[index..] {
181                    current_total_weight += participant.weight;
182                    participant.sorted_weight = current_total_weight - participant.weight;
183                }
184
185                // Update max item give amount.
186                give = participants
187                    .iter()
188                    .map(|participant| {
189                        (total_item_amount as f32 * participant.weight / total_weight).ceil() as u32
190                            - participant.recieved_count
191                    })
192                    .min()
193                    .unwrap_or(0);
194            } else {
195                give = give.min(
196                    (total_item_amount as f32 * winner.weight / total_weight).ceil() as u32
197                        - winner.recieved_count,
198                );
199            }
200        }
201        for participant in participants.iter_mut() {
202            if participant.current_recieved_count != 0 {
203                exec_item(item, participant.data, participant.current_recieved_count);
204                participant.current_recieved_count = 0;
205            }
206        }
207    }
208}
209
210#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
211#[rustfmt::skip] // breaks doc comments
212pub enum LootSpec<T: AsRef<str>> {
213    /// Asset specifier
214    Item(T),
215    /// Loot table
216    LootTable(T),
217    /// No loot given
218    Nothing,
219    /// Random modular weapon that matches requested restrictions
220    ModularWeapon {
221        tool: item::tool::ToolKind,
222        material: item::Material,
223        hands: Option<item::tool::Hands>,
224    },
225    /// Random primary modular weapon component that matches requested
226    /// restrictions
227    ModularWeaponPrimaryComponent {
228        tool: item::tool::ToolKind,
229        material: item::Material,
230        hands: Option<item::tool::Hands>,
231    },
232    /// Dropping variable number of items at random from respective Category
233    ///
234    /// # Examples:
235    /// ```text
236    /// MultiDrop(Item("common.items.utility.coins"), 100, 250)
237    /// ```
238    /// Will drop 100-250 coins (250 coins is also possible).
239    /// ```text
240    /// MultiDrop(LootTable("common.loot_tables.food.prepared"), 1, 4)
241    /// ```
242    /// Will drop random item from food.prepared loot table one to four times.
243    /// Each time the dice is thrown again, so items might get duplicated or
244    /// not.
245    MultiDrop(Box<LootSpec<T>>, u32, u32),
246    /// Each category is evaluated, often used to have guaranteed quest item
247    /// and random reward.
248    ///
249    /// # Examples:
250    /// ```text
251    /// All([
252    ///     Item("common.items.keys.bone_key"),
253    ///     MultiDrop(
254    ///         Item("common.items.crafting_ing.mineral.gem.sapphire"),
255    ///         0, 1,
256    ///     ),
257    /// ])
258    /// ```
259    /// Will always drop bone key, 1-2 furs, and may drop or not drop one
260    /// sapphire.
261    ///
262    /// ```text
263    /// All([
264    ///     Item("common.items.armor.cultist.necklace"),
265    ///     MultiDrop(Item("common.items.armor.cultist.ring"), 2, 2),
266    /// ])
267    /// ```
268    /// Will always drop cultist necklace and two cultist rings.
269    All(Vec<LootSpec<T>>),
270    /// Like a `LootTable` but inline, most useful with `All([])`.
271    ///
272    /// # Examples:
273    /// ```text
274    /// All([
275    ///     Item("common.items.keys.terracotta_key_door"),
276    ///
277    ///     Lottery([
278    ///         // Weapons
279    ///         (3.0, LootTable("common.loot_tables.weapons.tier-5")),
280    ///         // Armor
281    ///         (3.0, LootTable("common.loot_tables.armor.tier-5")),
282    ///         // Misc
283    ///         (0.25, Item("common.items.tool.instruments.steeltonguedrum")),
284    ///     ]),
285    /// ])
286    /// ```
287    /// Will always drop a terracotta key, and ONE of items defined in a lottery:
288    /// * one random tier-5 weapon
289    /// * one random tier-5 armour piece
290    /// * Steeldrum
291    Lottery(Vec<(f32, LootSpec<T>)>),
292}
293
294impl<T: AsRef<str>> LootSpec<T> {
295    fn to_items_inner(
296        &self,
297        rng: &mut rand::rngs::ThreadRng,
298        amount: u32,
299        items: &mut Vec<(u32, Item)>,
300    ) {
301        let convert_item = |item: &T| {
302            Item::new_from_asset(item.as_ref()).map_or_else(
303                |e| {
304                    warn!(?e, "error while loading item: {}", item.as_ref());
305                    None
306                },
307                Some,
308            )
309        };
310        let mut push_item = |mut item: Item, count: u32| {
311            let count = item.amount().saturating_mul(count);
312            item.set_amount(1).expect("1 is always a valid amount.");
313            let hash = item.item_hash();
314            match items.binary_search_by_key(&hash, |(_, item)| item.item_hash()) {
315                Ok(i) => {
316                    // Since item hash can collide with other items, we search nearby items with the
317                    // same hash.
318                    // NOTE: The `ParitalEq` implementation for `Item` doesn't compare some data
319                    // like durability, or wether slots contain anything. Although since these are
320                    // Newly loaded items we don't care about comparing those for deduplication
321                    // here.
322                    let has_same_hash = |i: &usize| items[*i].1.item_hash() == hash;
323                    if let Some(i) = (i..items.len())
324                        .take_while(has_same_hash)
325                        .chain((0..i).rev().take_while(has_same_hash))
326                        .find(|i| items[*i].1 == item)
327                    {
328                        // We saturate at 4 billion items, could use u64 instead if this isn't
329                        // desirable.
330                        items[i].0 = items[i].0.saturating_add(count);
331                    } else {
332                        items.insert(i, (count, item));
333                    }
334                },
335                Err(i) => items.insert(i, (count, item)),
336            }
337        };
338
339        match self {
340            Self::Item(item) => {
341                if let Some(item) = convert_item(item) {
342                    push_item(item, amount);
343                }
344            },
345            Self::LootTable(table) => {
346                let loot_spec = Lottery::<LootSpec<String>>::load_expect(table.as_ref()).read();
347                for _ in 0..amount {
348                    loot_spec.choose().to_items_inner(rng, 1, items)
349                }
350            },
351            Self::Lottery(table) => {
352                let lottery = Lottery::from(
353                    table
354                        .iter()
355                        .map(|(weight, spec)| (*weight, spec))
356                        .collect::<Vec<_>>(),
357                );
358
359                for _ in 0..amount {
360                    lottery.choose().to_items_inner(rng, 1, items)
361                }
362            },
363            Self::Nothing => {},
364            Self::ModularWeapon {
365                tool,
366                material,
367                hands,
368            } => {
369                for _ in 0..amount {
370                    match item::modular::random_weapon(*tool, *material, *hands, rng) {
371                        Ok(item) => push_item(item, 1),
372                        Err(e) => {
373                            warn!(
374                                ?e,
375                                "error while creating modular weapon. Toolkind: {:?}, Material: \
376                                 {:?}, Hands: {:?}",
377                                tool,
378                                material,
379                                hands,
380                            );
381                        },
382                    }
383                }
384            },
385            Self::ModularWeaponPrimaryComponent {
386                tool,
387                material,
388                hands,
389            } => {
390                for _ in 0..amount {
391                    match item::modular::random_weapon(*tool, *material, *hands, rng) {
392                        Ok(item) => push_item(item, 1),
393                        Err(e) => {
394                            warn!(
395                                ?e,
396                                "error while creating modular weapon primary component. Toolkind: \
397                                 {:?}, Material: {:?}, Hands: {:?}",
398                                tool,
399                                material,
400                                hands,
401                            );
402                        },
403                    }
404                }
405            },
406            Self::MultiDrop(loot_spec, lower, upper) => {
407                let sub_amount = rng.gen_range(*lower..=*upper);
408                // We saturate at 4 billion items, could use u64 instead if this isn't
409                // desirable.
410                loot_spec.to_items_inner(rng, sub_amount.saturating_mul(amount), items);
411            },
412            Self::All(loot_specs) => {
413                for loot_spec in loot_specs {
414                    loot_spec.to_items_inner(rng, amount, items);
415                }
416            },
417        }
418    }
419
420    pub fn to_items(&self) -> Option<Vec<(u32, Item)>> {
421        let mut items = Vec::new();
422        self.to_items_inner(&mut thread_rng(), 1, &mut items);
423
424        if !items.is_empty() {
425            items.sort_unstable_by_key(|(amount, _)| *amount);
426
427            Some(items)
428        } else {
429            None
430        }
431    }
432}
433
434impl Default for LootSpec<String> {
435    fn default() -> Self { Self::Nothing }
436}
437
438#[cfg(test)]
439pub mod tests {
440    use std::borrow::Borrow;
441
442    use super::*;
443    use crate::{assets, comp::Item};
444    use assets::AssetExt;
445
446    #[cfg(test)]
447    pub fn validate_loot_spec(item: &LootSpec<String>) {
448        let mut rng = thread_rng();
449        match item {
450            LootSpec::Item(item) => {
451                Item::new_from_asset_expect(item);
452            },
453            LootSpec::LootTable(loot_table) => {
454                let loot_table = Lottery::<LootSpec<String>>::load_expect(loot_table).read();
455                validate_table_contents(&loot_table);
456            },
457            LootSpec::Nothing => {},
458            LootSpec::ModularWeapon {
459                tool,
460                material,
461                hands,
462            } => {
463                item::modular::random_weapon(*tool, *material, *hands, &mut rng).unwrap_or_else(
464                    |_| {
465                        panic!(
466                            "Failed to synthesize a modular {tool:?} made of {material:?} that \
467                             had a hand restriction of {hands:?}."
468                        )
469                    },
470                );
471            },
472            LootSpec::ModularWeaponPrimaryComponent {
473                tool,
474                material,
475                hands,
476            } => {
477                item::modular::random_weapon_primary_component(*tool, *material, *hands, &mut rng)
478                    .unwrap_or_else(|_| {
479                        panic!(
480                            "Failed to synthesize a modular weapon primary component: {tool:?} \
481                             made of {material:?} that had a hand restriction of {hands:?}."
482                        )
483                    });
484            },
485            LootSpec::MultiDrop(loot_spec, lower, upper) => {
486                assert!(
487                    upper >= lower,
488                    "Upper quantity must be at least the value of lower quantity. Upper value: \
489                     {}, low value: {}.",
490                    upper,
491                    lower
492                );
493                validate_loot_spec(loot_spec);
494            },
495            LootSpec::All(loot_specs) => {
496                for loot_spec in loot_specs {
497                    validate_loot_spec(loot_spec);
498                }
499            },
500            LootSpec::Lottery(table) => {
501                let lottery = Lottery::from(
502                    table
503                        .iter()
504                        .map(|(weight, spec)| (*weight, spec))
505                        .collect::<Vec<_>>(),
506                );
507
508                validate_table_contents(&lottery);
509            },
510        }
511    }
512
513    fn validate_table_contents<T: Borrow<LootSpec<String>>>(table: &Lottery<T>) {
514        for (_, item) in table.iter() {
515            validate_loot_spec(item.borrow());
516        }
517    }
518
519    #[test]
520    fn test_loot_tables() {
521        let loot_tables = assets::load_rec_dir::<Lottery<LootSpec<String>>>("common.loot_tables")
522            .expect("load loot_tables");
523        for loot_table in loot_tables.read().ids() {
524            let loot_table = Lottery::<LootSpec<String>>::load_expect(loot_table);
525            validate_table_contents(&loot_table.read());
526        }
527    }
528
529    #[test]
530    fn test_distribute_many() {
531        let mut rng = thread_rng();
532
533        // Known successful case
534        for _ in 0..10 {
535            distribute_many(
536                vec![(0.4f32, "a"), (0.4, "b"), (0.2, "c")],
537                &mut rng,
538                &[("item", 10)],
539                |(_, m)| *m,
540                |_item, winner, count| match winner {
541                    "a" | "b" => assert_eq!(count, 4),
542                    "c" => assert_eq!(count, 2),
543                    _ => unreachable!(),
544                },
545            );
546        }
547    }
548}