1use 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
81pub 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: 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 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 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 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 for participant in &mut participants[index..] {
181 current_total_weight += participant.weight;
182 participant.sorted_weight = current_total_weight - participant.weight;
183 }
184
185 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)]
211pub enum LootSpec<T: AsRef<str>> {
212 Item(T),
214 LootTable(T),
216 Nothing,
218 ModularWeapon {
220 tool: item::tool::ToolKind,
221 material: item::Material,
222 hands: Option<item::tool::Hands>,
223 },
224 ModularWeaponPrimaryComponent {
225 tool: item::tool::ToolKind,
226 material: item::Material,
227 hands: Option<item::tool::Hands>,
228 },
229 MultiDrop(Box<LootSpec<T>>, u32, u32),
232 All(Vec<LootSpec<T>>),
235 Lottery(Vec<(f32, LootSpec<T>)>),
237}
238
239impl<T: AsRef<str>> LootSpec<T> {
240 fn to_items_inner(
241 &self,
242 rng: &mut rand::rngs::ThreadRng,
243 amount: u32,
244 items: &mut Vec<(u32, Item)>,
245 ) {
246 let convert_item = |item: &T| {
247 Item::new_from_asset(item.as_ref()).map_or_else(
248 |e| {
249 warn!(?e, "error while loading item: {}", item.as_ref());
250 None
251 },
252 Some,
253 )
254 };
255 let mut push_item = |mut item: Item, count: u32| {
256 let count = item.amount().saturating_mul(count);
257 item.set_amount(1).expect("1 is always a valid amount.");
258 let hash = item.item_hash();
259 match items.binary_search_by_key(&hash, |(_, item)| item.item_hash()) {
260 Ok(i) => {
261 let has_same_hash = |i: &usize| items[*i].1.item_hash() == hash;
268 if let Some(i) = (i..items.len())
269 .take_while(has_same_hash)
270 .chain((0..i).rev().take_while(has_same_hash))
271 .find(|i| items[*i].1 == item)
272 {
273 items[i].0 = items[i].0.saturating_add(count);
276 } else {
277 items.insert(i, (count, item));
278 }
279 },
280 Err(i) => items.insert(i, (count, item)),
281 }
282 };
283
284 match self {
285 Self::Item(item) => {
286 if let Some(item) = convert_item(item) {
287 push_item(item, amount);
288 }
289 },
290 Self::LootTable(table) => {
291 let loot_spec = Lottery::<LootSpec<String>>::load_expect(table.as_ref()).read();
292 for _ in 0..amount {
293 loot_spec.choose().to_items_inner(rng, 1, items)
294 }
295 },
296 Self::Lottery(table) => {
297 let lottery = Lottery::from(
298 table
299 .iter()
300 .map(|(weight, spec)| (*weight, spec))
301 .collect::<Vec<_>>(),
302 );
303
304 for _ in 0..amount {
305 lottery.choose().to_items_inner(rng, 1, items)
306 }
307 },
308 Self::Nothing => {},
309 Self::ModularWeapon {
310 tool,
311 material,
312 hands,
313 } => {
314 for _ in 0..amount {
315 match item::modular::random_weapon(*tool, *material, *hands, rng) {
316 Ok(item) => push_item(item, 1),
317 Err(e) => {
318 warn!(
319 ?e,
320 "error while creating modular weapon. Toolkind: {:?}, Material: \
321 {:?}, Hands: {:?}",
322 tool,
323 material,
324 hands,
325 );
326 },
327 }
328 }
329 },
330 Self::ModularWeaponPrimaryComponent {
331 tool,
332 material,
333 hands,
334 } => {
335 for _ in 0..amount {
336 match item::modular::random_weapon(*tool, *material, *hands, rng) {
337 Ok(item) => push_item(item, 1),
338 Err(e) => {
339 warn!(
340 ?e,
341 "error while creating modular weapon primary component. Toolkind: \
342 {:?}, Material: {:?}, Hands: {:?}",
343 tool,
344 material,
345 hands,
346 );
347 },
348 }
349 }
350 },
351 Self::MultiDrop(loot_spec, lower, upper) => {
352 let sub_amount = rng.gen_range(*lower..=*upper);
353 loot_spec.to_items_inner(rng, sub_amount.saturating_mul(amount), items);
356 },
357 Self::All(loot_specs) => {
358 for loot_spec in loot_specs {
359 loot_spec.to_items_inner(rng, amount, items);
360 }
361 },
362 }
363 }
364
365 pub fn to_items(&self) -> Option<Vec<(u32, Item)>> {
366 let mut items = Vec::new();
367 self.to_items_inner(&mut thread_rng(), 1, &mut items);
368
369 if !items.is_empty() {
370 items.sort_unstable_by_key(|(amount, _)| *amount);
371
372 Some(items)
373 } else {
374 None
375 }
376 }
377}
378
379impl Default for LootSpec<String> {
380 fn default() -> Self { Self::Nothing }
381}
382
383#[cfg(test)]
384pub mod tests {
385 use std::borrow::Borrow;
386
387 use super::*;
388 use crate::{assets, comp::Item};
389 use assets::AssetExt;
390
391 #[cfg(test)]
392 pub fn validate_loot_spec(item: &LootSpec<String>) {
393 let mut rng = thread_rng();
394 match item {
395 LootSpec::Item(item) => {
396 Item::new_from_asset_expect(item);
397 },
398 LootSpec::LootTable(loot_table) => {
399 let loot_table = Lottery::<LootSpec<String>>::load_expect(loot_table).read();
400 validate_table_contents(&loot_table);
401 },
402 LootSpec::Nothing => {},
403 LootSpec::ModularWeapon {
404 tool,
405 material,
406 hands,
407 } => {
408 item::modular::random_weapon(*tool, *material, *hands, &mut rng).unwrap_or_else(
409 |_| {
410 panic!(
411 "Failed to synthesize a modular {tool:?} made of {material:?} that \
412 had a hand restriction of {hands:?}."
413 )
414 },
415 );
416 },
417 LootSpec::ModularWeaponPrimaryComponent {
418 tool,
419 material,
420 hands,
421 } => {
422 item::modular::random_weapon_primary_component(*tool, *material, *hands, &mut rng)
423 .unwrap_or_else(|_| {
424 panic!(
425 "Failed to synthesize a modular weapon primary component: {tool:?} \
426 made of {material:?} that had a hand restriction of {hands:?}."
427 )
428 });
429 },
430 LootSpec::MultiDrop(loot_spec, lower, upper) => {
431 assert!(
432 upper >= lower,
433 "Upper quantity must be at least the value of lower quantity. Upper value: \
434 {}, low value: {}.",
435 upper,
436 lower
437 );
438 validate_loot_spec(loot_spec);
439 },
440 LootSpec::All(loot_specs) => {
441 for loot_spec in loot_specs {
442 validate_loot_spec(loot_spec);
443 }
444 },
445 LootSpec::Lottery(table) => {
446 let lottery = Lottery::from(
447 table
448 .iter()
449 .map(|(weight, spec)| (*weight, spec))
450 .collect::<Vec<_>>(),
451 );
452
453 validate_table_contents(&lottery);
454 },
455 }
456 }
457
458 fn validate_table_contents<T: Borrow<LootSpec<String>>>(table: &Lottery<T>) {
459 for (_, item) in table.iter() {
460 validate_loot_spec(item.borrow());
461 }
462 }
463
464 #[test]
465 fn test_loot_tables() {
466 let loot_tables = assets::load_rec_dir::<Lottery<LootSpec<String>>>("common.loot_tables")
467 .expect("load loot_tables");
468 for loot_table in loot_tables.read().ids() {
469 let loot_table = Lottery::<LootSpec<String>>::load_expect(loot_table);
470 validate_table_contents(&loot_table.read());
471 }
472 }
473
474 #[test]
475 fn test_distribute_many() {
476 let mut rng = thread_rng();
477
478 for _ in 0..10 {
480 distribute_many(
481 vec![(0.4f32, "a"), (0.4, "b"), (0.2, "c")],
482 &mut rng,
483 &[("item", 10)],
484 |(_, m)| *m,
485 |_item, winner, count| match winner {
486 "a" | "b" => assert_eq!(count, 4),
487 "c" => assert_eq!(count, 2),
488 _ => unreachable!(),
489 },
490 );
491 }
492 }
493}