const COPPER: &str = "copper_coin";
const SILVER: &str = "silver_coin";
const GOLD: &str = "gold_coin";
const PLATINUM: &str = "platinum_coin";
const COPPER_PER_SILVER: u64 = 100;
const COPPER_PER_GOLD: u64 = 10_000;
const COPPER_PER_PLATINUM: u64 = 1_000_000;
pub fn copper_from_counts(counts: &std::collections::HashMap<String, u32>) -> u64 {
counts
.iter()
.map(|(id, qty)| copper_value(id).saturating_mul(*qty as u64))
.sum()
}
fn copper_value(template_id: &str) -> u64 {
match template_id {
PLATINUM => COPPER_PER_PLATINUM,
GOLD => COPPER_PER_GOLD,
SILVER => COPPER_PER_SILVER,
COPPER => 1,
_ => 0,
}
}
pub fn is_currency(template_id: &str) -> bool {
copper_value(template_id) > 0
}
pub fn format_copper(amount: u64) -> String {
let denoms = [
(COPPER_PER_PLATINUM, "pp"),
(COPPER_PER_GOLD, "gp"),
(COPPER_PER_SILVER, "sp"),
(1, "cp"),
];
let mut rest = amount;
let mut parts = Vec::new();
for (unit, label) in denoms {
if rest >= unit {
let count = rest / unit;
rest %= unit;
parts.push(format!("{count}{label}"));
}
}
if parts.is_empty() {
"0cp".into()
} else {
parts.join(" ")
}
}
pub fn currency_line(counts: &std::collections::HashMap<String, u32>) -> String {
format_copper(copper_from_counts(counts))
}
pub fn format_quest_reward(reward: &flatland_protocol::QuestRewardView) -> Option<String> {
if reward.is_empty() {
return None;
}
let mut parts = Vec::new();
if reward.coins > 0 {
parts.push(format_copper(reward.coins as u64));
}
for item in &reward.items {
let name = if !item.display_name.trim().is_empty() {
item.display_name.trim().to_string()
} else if !item.template_id.is_empty() {
item.template_id
.split('_')
.filter(|s| !s.is_empty())
.map(|s| {
let mut c = s.chars();
match c.next() {
Some(f) => format!("{}{}", f.to_uppercase(), c.as_str()),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ")
} else {
continue;
};
if item.quantity > 1 {
parts.push(format!("{name} ×{}", item.quantity));
} else {
parts.push(name);
}
}
if parts.is_empty() {
None
} else {
Some(parts.join(", "))
}
}
const DENOM_TEMPLATES: [(u64, &str); 4] = [
(COPPER_PER_PLATINUM, PLATINUM),
(COPPER_PER_GOLD, GOLD),
(COPPER_PER_SILVER, SILVER),
(1, COPPER),
];
pub fn apply_coins_delta(stacks: &mut Vec<flatland_protocol::ItemStack>, delta: i32) {
if delta == 0 {
return;
}
if delta > 0 {
add_copper_stacks(stacks, delta as u64);
} else {
spend_copper_stacks(stacks, (-delta) as u64);
}
}
fn add_copper_stacks(stacks: &mut Vec<flatland_protocol::ItemStack>, amount: u64) {
if amount == 0 {
return;
}
let mut rest = amount;
for (unit, template) in DENOM_TEMPLATES {
if rest < unit {
continue;
}
let count = (rest / unit) as u32;
rest %= unit;
if count == 0 {
continue;
}
if let Some(existing) = stacks.iter_mut().find(|s| s.template_id == template) {
existing.quantity = existing.quantity.saturating_add(count);
} else {
stacks.push(flatland_protocol::ItemStack::simple(template, count));
}
}
}
fn spend_copper_stacks(stacks: &mut Vec<flatland_protocol::ItemStack>, amount: u64) {
if amount == 0 {
return;
}
let held = stacks
.iter()
.map(|s| copper_value(&s.template_id).saturating_mul(s.quantity as u64))
.sum::<u64>();
if held < amount {
return;
}
let rest = held.saturating_sub(amount);
for (_, template) in DENOM_TEMPLATES {
drain_template_stacks(stacks, template, u32::MAX);
}
add_copper_stacks(stacks, rest);
}
pub fn drain_template_stacks(
stacks: &mut Vec<flatland_protocol::ItemStack>,
template_id: &str,
qty: u32,
) {
let mut remaining = qty;
stacks.retain_mut(|s| {
if s.template_id != template_id || remaining == 0 {
return true;
}
let take = remaining.min(s.quantity);
s.quantity = s.quantity.saturating_sub(take);
remaining = remaining.saturating_sub(take);
s.quantity > 0
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_coins_delta_mints_and_spends() {
let mut stacks = Vec::new();
add_copper_stacks(&mut stacks, 150);
assert_eq!(copper_from_counts(&counts_from_stacks(&stacks)), 150);
spend_copper_stacks(&mut stacks, 100);
assert_eq!(copper_from_counts(&counts_from_stacks(&stacks)), 50);
}
#[test]
fn drain_template_stacks_removes_quantity() {
let mut stacks = vec![
flatland_protocol::ItemStack::simple("iron_ore", 2),
flatland_protocol::ItemStack::simple("oak_log", 5),
];
drain_template_stacks(&mut stacks, "oak_log", 3);
assert_eq!(stacks.len(), 2);
assert_eq!(
stacks
.iter()
.find(|s| s.template_id == "oak_log")
.map(|s| s.quantity),
Some(2)
);
}
#[test]
fn format_quest_reward_joins_coins_and_items() {
assert!(format_quest_reward(&flatland_protocol::QuestRewardView::default()).is_none());
let line = format_quest_reward(&flatland_protocol::QuestRewardView {
coins: 25,
items: vec![flatland_protocol::QuestRewardItemView {
template_id: "wooden_sword".into(),
display_name: "Wooden sword".into(),
quantity: 1,
}],
})
.expect("non-empty reward");
assert!(line.contains("25cp"), "{line}");
assert!(line.contains("Wooden sword"), "{line}");
}
fn counts_from_stacks(
stacks: &[flatland_protocol::ItemStack],
) -> std::collections::HashMap<String, u32> {
let mut counts = std::collections::HashMap::new();
for s in stacks {
*counts.entry(s.template_id.clone()).or_insert(0) += s.quantity;
}
counts
}
}