scroll.server/crates/game-service/src/rewards.rs
WiseDev ca8e41c2c5 roll chest loot from the csv instead of a fixed config
RandomSpells / DifferentSpells / RareChance / EpicChance / MinGold / MaxGold all
come off the chest row now, gold is scaled by the player's arena. magic chest is
30 cards over 8 cards with 1 epic and 6 rares, like the client says it is.

arena chest rows inherit everything through BaseChest, so Free_Arena1 reads Free.
2026-08-23 09:30:38 +03:00

110 lines
3.6 KiB
Rust

use logic::data::{RARITY_COMMON, RARITY_EPIC, RARITY_RARE};
use logic::model::LogicSpell;
use logic::{LogicDataRef, LogicReward};
use rand::seq::SliceRandom;
use rand::Rng;
use crate::catalog::Catalog;
pub struct RewardRoller {
pool: Vec<LogicDataRef>,
}
impl RewardRoller {
pub fn new(catalog: &Catalog, fallback: Vec<LogicDataRef>) -> Self {
let pool = match catalog.card_pool() {
pool if pool.is_empty() => fallback,
pool => pool,
};
Self { pool }
}
pub fn is_empty(&self) -> bool {
self.pool.is_empty()
}
fn by_rarity(&self, rarity: &str) -> Vec<LogicDataRef> {
self.pool
.iter()
.filter(|card| {
card.as_spell()
.map(|spell| spell.rarity() == rarity)
.unwrap_or(false)
})
.cloned()
.collect()
}
pub fn roll(&self, chest: &LogicDataRef, arena: &LogicDataRef) -> LogicReward {
let Some(data) = chest.as_treasure_chest() else {
return LogicReward::default();
};
let mut rng = rand::thread_rng();
let scale = |value: i32| match arena.as_arena() {
Some(arena) => arena.scaled_chest_reward(value),
None => value,
};
let gold = {
let low = scale(data.min_gold());
let high = scale(data.max_gold());
if high > low {
rng.gen_range(low..=high)
} else {
low
}
};
let diamonds = {
let low = data.min_diamonds();
let high = data.max_diamonds();
if high > low {
rng.gen_range(low..=high)
} else {
low
}
};
let copies = data.random_spells().max(0);
let distinct = data.different_spells().max(1) as usize;
let mut picked: Vec<LogicDataRef> = Vec::new();
let guaranteed = data.guaranteed_spell();
if !guaranteed.is_none() {
picked.push(guaranteed);
}
for (rarity, count) in [
(RARITY_EPIC, data.guaranteed_epics()),
(RARITY_RARE, data.guaranteed_rares()),
] {
let mut candidates = self.by_rarity(rarity);
candidates.retain(|card| !picked.contains(card));
candidates.shuffle(&mut rng);
for card in candidates.into_iter().take(count.max(0) as usize) {
if picked.len() >= distinct {
break;
}
picked.push(card);
}
}
if picked.len() < distinct {
let mut candidates = self.by_rarity(RARITY_COMMON);
candidates.retain(|card| !picked.contains(card));
candidates.shuffle(&mut rng);
for card in candidates {
if picked.len() >= distinct {
break;
}
picked.push(card);
}
}
if picked.is_empty() {
return LogicReward::new(Vec::new(), gold, diamonds);
}
picked.sort_by_key(|card| card.global_id().map(|id| (id.class_id, id.instance_id)));
let share = copies / picked.len() as i32;
let mut remainder = copies % picked.len() as i32;
let spells = picked
.into_iter()
.map(|card| {
let mut count = share;
if remainder > 0 {
count += 1;
remainder -= 1;
}
LogicSpell::card(card, 0, count.max(1))
})
.collect();
LogicReward::new(spells, gold, diamonds)
}
}