2021-01-20 11:20:06 +00:00
|
|
|
// Example for calculating a drop rate:
|
|
|
|
//
|
|
|
|
// On every roll an f32 between 0 and 1 is created.
|
|
|
|
// For every loot table a total range is created by the sum of the individual
|
|
|
|
// ranges per item.
|
|
|
|
//
|
|
|
|
// This range is the sum of all single ranges defined per item in a table.
|
|
|
|
// // Individual Range
|
|
|
|
// (3, "common.items.food.cheese"), // 0.0..3.0
|
|
|
|
// (3, "common.items.food.apple"), // 3.0..6.0
|
|
|
|
// (3, "common.items.food.mushroom"), // 6.0..9.0
|
|
|
|
// (1, "common.items.food.coconut"), // 9.0..10.0
|
|
|
|
// (0.05, "common.items.food.apple_mushroom_curry"), // 10.0..10.05
|
|
|
|
// (0.10, "common.items.food.apple_stick"), // 10.05..10.15
|
|
|
|
// (0.10, "common.items.food.mushroom_stick"), // 10.15..10.25
|
|
|
|
//
|
|
|
|
// The f32 is multiplied by the max. value needed to drop an item in this
|
|
|
|
// particular table. X = max. value needed = 10.15
|
|
|
|
//
|
|
|
|
// Example roll
|
|
|
|
// [Random Value 0..1] * X = Number inside the table's total range
|
|
|
|
// 0.45777 * X = 4.65
|
|
|
|
// 4.65 is in the range of 3.0..6.0 => Apple drops
|
|
|
|
//
|
|
|
|
// Example drop chance calculation
|
|
|
|
// Cheese drop rate = 3/X = 29.6%
|
|
|
|
// Coconut drop rate = 1/X = 9.85%
|
|
|
|
|
2021-03-29 04:12:11 +00:00
|
|
|
use crate::{
|
|
|
|
assets::{self, AssetExt},
|
2021-04-03 03:03:59 +00:00
|
|
|
comp::Item,
|
2021-03-29 04:12:11 +00:00
|
|
|
};
|
2020-07-03 13:49:17 +00:00
|
|
|
use rand::prelude::*;
|
2021-03-28 21:41:14 +00:00
|
|
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
2021-04-03 15:37:57 +00:00
|
|
|
use tracing::warn;
|
2020-07-03 13:49:17 +00:00
|
|
|
|
2020-08-28 01:02:17 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
2020-07-03 13:49:17 +00:00
|
|
|
pub struct Lottery<T> {
|
2020-07-03 20:23:24 +00:00
|
|
|
items: Vec<(f32, T)>,
|
2020-07-03 13:49:17 +00:00
|
|
|
total: f32,
|
|
|
|
}
|
|
|
|
|
2020-12-12 22:14:24 +00:00
|
|
|
impl<T: DeserializeOwned + Send + Sync + 'static> assets::Asset for Lottery<T> {
|
|
|
|
type Loader = assets::LoadFrom<Vec<(f32, T)>, assets::RonLoader>;
|
2020-12-13 01:09:57 +00:00
|
|
|
|
|
|
|
const EXTENSION: &'static str = "ron";
|
2020-07-03 13:49:17 +00:00
|
|
|
}
|
|
|
|
|
2020-12-12 22:14:24 +00:00
|
|
|
impl<T> From<Vec<(f32, T)>> for Lottery<T> {
|
|
|
|
fn from(mut items: Vec<(f32, T)>) -> Lottery<T> {
|
2020-07-03 13:49:17 +00:00
|
|
|
let mut total = 0.0;
|
2020-12-12 22:14:24 +00:00
|
|
|
|
|
|
|
for (rate, _) in &mut items {
|
|
|
|
total += *rate;
|
|
|
|
*rate = total - *rate;
|
|
|
|
}
|
|
|
|
|
2020-07-03 13:49:17 +00:00
|
|
|
Self { items, total }
|
|
|
|
}
|
2020-12-12 22:14:24 +00:00
|
|
|
}
|
2020-07-03 13:49:17 +00:00
|
|
|
|
2020-12-12 22:14:24 +00:00
|
|
|
impl<T> Lottery<T> {
|
2020-07-25 11:19:13 +00:00
|
|
|
pub fn choose_seeded(&self, seed: u32) -> &T {
|
|
|
|
let x = ((seed % 65536) as f32 / 65536.0) * self.total;
|
2020-07-03 20:23:24 +00:00
|
|
|
&self.items[self
|
|
|
|
.items
|
|
|
|
.binary_search_by(|(y, _)| y.partial_cmp(&x).unwrap())
|
|
|
|
.unwrap_or_else(|i| i.saturating_sub(1))]
|
|
|
|
.1
|
2020-07-03 13:49:17 +00:00
|
|
|
}
|
2020-07-05 16:49:15 +00:00
|
|
|
|
2020-08-12 14:10:12 +00:00
|
|
|
pub fn choose(&self) -> &T { self.choose_seeded(thread_rng().gen()) }
|
2020-07-25 11:19:13 +00:00
|
|
|
|
2020-07-05 16:49:15 +00:00
|
|
|
pub fn iter(&self) -> impl Iterator<Item = &(f32, T)> { self.items.iter() }
|
2021-03-22 09:39:35 +00:00
|
|
|
|
|
|
|
pub fn total(&self) -> f32 { self.total }
|
2020-07-05 16:49:15 +00:00
|
|
|
}
|
|
|
|
|
2021-03-28 21:41:14 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
2021-07-03 18:59:00 +00:00
|
|
|
pub enum LootSpec<T: AsRef<str>> {
|
2021-03-28 23:17:23 +00:00
|
|
|
/// Asset specifier
|
2021-07-03 18:59:00 +00:00
|
|
|
Item(T),
|
2021-03-28 23:17:23 +00:00
|
|
|
/// Asset specifier, lower range, upper range
|
2021-07-03 18:59:00 +00:00
|
|
|
ItemQuantity(T, u32, u32),
|
2021-03-28 23:17:23 +00:00
|
|
|
/// Loot table
|
2021-07-03 18:59:00 +00:00
|
|
|
LootTable(T),
|
2021-09-01 23:17:36 +00:00
|
|
|
/// No loot given
|
2021-09-22 17:16:10 +00:00
|
|
|
Nothing,
|
2021-03-28 21:41:14 +00:00
|
|
|
}
|
|
|
|
|
2021-07-03 18:59:00 +00:00
|
|
|
impl<T: AsRef<str>> LootSpec<T> {
|
2021-09-01 23:17:36 +00:00
|
|
|
pub fn to_item(&self) -> Option<Item> {
|
2021-03-28 21:41:14 +00:00
|
|
|
match self {
|
2021-09-22 17:16:10 +00:00
|
|
|
Self::Item(item) => Item::new_from_asset(item.as_ref()).map_or_else(
|
|
|
|
|e| {
|
2021-09-26 17:19:24 +00:00
|
|
|
warn!(?e, "error while loading item: {}", item.as_ref());
|
2021-09-22 17:16:10 +00:00
|
|
|
None
|
|
|
|
},
|
2021-09-22 21:19:15 +00:00
|
|
|
Option::Some,
|
2021-09-22 17:16:10 +00:00
|
|
|
),
|
2021-03-28 23:17:23 +00:00
|
|
|
Self::ItemQuantity(item, lower, upper) => {
|
|
|
|
let range = *lower..=*upper;
|
|
|
|
let quantity = thread_rng().gen_range(range);
|
2021-09-22 17:16:10 +00:00
|
|
|
match Item::new_from_asset(item.as_ref()) {
|
|
|
|
Ok(mut item) => {
|
|
|
|
// TODO: Handle multiple of an item that is unstackable
|
|
|
|
if item.set_amount(quantity).is_err() {
|
|
|
|
warn!("Tried to set quantity on non stackable item");
|
|
|
|
}
|
|
|
|
Some(item)
|
|
|
|
},
|
|
|
|
Err(e) => {
|
2021-09-26 17:19:24 +00:00
|
|
|
warn!(?e, "error while loading item: {}", item.as_ref());
|
2021-09-22 17:16:10 +00:00
|
|
|
None
|
|
|
|
},
|
2021-04-03 15:37:57 +00:00
|
|
|
}
|
2021-03-28 23:17:23 +00:00
|
|
|
},
|
2021-07-03 18:59:00 +00:00
|
|
|
Self::LootTable(table) => Lottery::<LootSpec<String>>::load_expect(table.as_ref())
|
2021-03-29 04:12:11 +00:00
|
|
|
.read()
|
|
|
|
.choose()
|
2021-04-03 03:03:59 +00:00
|
|
|
.to_item(),
|
2021-09-22 17:16:10 +00:00
|
|
|
Self::Nothing => None,
|
2021-03-28 21:41:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-22 02:25:14 +00:00
|
|
|
impl Default for LootSpec<String> {
|
2021-09-22 17:16:10 +00:00
|
|
|
fn default() -> Self { Self::Nothing }
|
2021-09-22 02:25:14 +00:00
|
|
|
}
|
|
|
|
|
2020-07-05 16:49:15 +00:00
|
|
|
#[cfg(test)]
|
2021-09-22 02:25:14 +00:00
|
|
|
pub mod tests {
|
2020-08-12 17:29:51 +00:00
|
|
|
use super::*;
|
2021-06-25 16:47:03 +00:00
|
|
|
use crate::{assets, comp::Item};
|
2021-04-02 01:26:35 +00:00
|
|
|
|
2021-09-22 02:25:14 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
pub fn validate_loot_spec(item: &LootSpec<String>) {
|
|
|
|
match item {
|
|
|
|
LootSpec::Item(item) => {
|
|
|
|
Item::new_from_asset_expect(item);
|
|
|
|
},
|
|
|
|
LootSpec::ItemQuantity(item, lower, upper) => {
|
|
|
|
assert!(
|
|
|
|
*lower > 0,
|
|
|
|
"Lower quantity must be more than 0. It is {}.",
|
|
|
|
lower
|
|
|
|
);
|
|
|
|
assert!(
|
|
|
|
upper >= lower,
|
|
|
|
"Upper quantity must be at least the value of lower quantity. Upper value: \
|
|
|
|
{}, low value: {}.",
|
|
|
|
upper,
|
|
|
|
lower
|
|
|
|
);
|
|
|
|
Item::new_from_asset_expect(item);
|
|
|
|
},
|
|
|
|
LootSpec::LootTable(loot_table) => {
|
|
|
|
let loot_table = Lottery::<LootSpec<String>>::load_expect_cloned(loot_table);
|
|
|
|
validate_table_contents(loot_table);
|
|
|
|
},
|
2021-09-22 17:16:10 +00:00
|
|
|
LootSpec::Nothing => {},
|
2021-04-02 01:02:36 +00:00
|
|
|
}
|
2021-09-22 02:25:14 +00:00
|
|
|
}
|
2021-04-02 01:02:36 +00:00
|
|
|
|
2021-09-22 02:25:14 +00:00
|
|
|
fn validate_table_contents(table: Lottery<LootSpec<String>>) {
|
|
|
|
for (_, item) in table.iter() {
|
|
|
|
validate_loot_spec(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_loot_tables() {
|
2021-07-03 18:59:00 +00:00
|
|
|
let loot_tables =
|
2021-07-05 15:43:30 +00:00
|
|
|
assets::read_expect_dir::<Lottery<LootSpec<String>>>("common.loot_tables", true);
|
|
|
|
for loot_table in loot_tables {
|
|
|
|
validate_table_contents(loot_table.clone());
|
2020-07-05 16:49:15 +00:00
|
|
|
}
|
|
|
|
}
|
2020-07-03 20:23:24 +00:00
|
|
|
}
|