Skip to main content

flatland_client_lib/
currency.rs

1//! Client-side currency display from inventory stacks.
2
3const COPPER: &str = "copper_coin";
4const SILVER: &str = "silver_coin";
5const GOLD: &str = "gold_coin";
6const PLATINUM: &str = "platinum_coin";
7
8const COPPER_PER_SILVER: u64 = 100;
9const COPPER_PER_GOLD: u64 = 10_000;
10const COPPER_PER_PLATINUM: u64 = 1_000_000;
11
12pub fn copper_from_counts(counts: &std::collections::HashMap<String, u32>) -> u64 {
13    counts
14        .iter()
15        .map(|(id, qty)| copper_value(id).saturating_mul(*qty as u64))
16        .sum()
17}
18
19fn copper_value(template_id: &str) -> u64 {
20    match template_id {
21        PLATINUM => COPPER_PER_PLATINUM,
22        GOLD => COPPER_PER_GOLD,
23        SILVER => COPPER_PER_SILVER,
24        COPPER => 1,
25        _ => 0,
26    }
27}
28
29pub fn format_copper(amount: u64) -> String {
30    let denoms = [
31        (COPPER_PER_PLATINUM, "pp"),
32        (COPPER_PER_GOLD, "gp"),
33        (COPPER_PER_SILVER, "sp"),
34        (1, "cp"),
35    ];
36    let mut rest = amount;
37    let mut parts = Vec::new();
38    for (unit, label) in denoms {
39        if rest >= unit {
40            let count = rest / unit;
41            rest %= unit;
42            parts.push(format!("{count}{label}"));
43        }
44    }
45    if parts.is_empty() {
46        "0cp".into()
47    } else {
48        parts.join(" ")
49    }
50}
51
52pub fn currency_line(counts: &std::collections::HashMap<String, u32>) -> String {
53    format_copper(copper_from_counts(counts))
54}
55
56const DENOM_TEMPLATES: [(u64, &str); 4] = [
57    (COPPER_PER_PLATINUM, PLATINUM),
58    (COPPER_PER_GOLD, GOLD),
59    (COPPER_PER_SILVER, SILVER),
60    (1, COPPER),
61];
62
63/// Mint or spend physical coin stacks on the client inventory mirror.
64pub fn apply_coins_delta(stacks: &mut Vec<flatland_protocol::ItemStack>, delta: i32) {
65    if delta == 0 {
66        return;
67    }
68    if delta > 0 {
69        add_copper_stacks(stacks, delta as u64);
70    } else {
71        spend_copper_stacks(stacks, (-delta) as u64);
72    }
73}
74
75fn add_copper_stacks(stacks: &mut Vec<flatland_protocol::ItemStack>, amount: u64) {
76    if amount == 0 {
77        return;
78    }
79    let mut rest = amount;
80    for (unit, template) in DENOM_TEMPLATES {
81        if rest < unit {
82            continue;
83        }
84        let count = (rest / unit) as u32;
85        rest %= unit;
86        if count == 0 {
87            continue;
88        }
89        if let Some(existing) = stacks.iter_mut().find(|s| s.template_id == template) {
90            existing.quantity = existing.quantity.saturating_add(count);
91        } else {
92            stacks.push(flatland_protocol::ItemStack::simple(template, count));
93        }
94    }
95}
96
97fn spend_copper_stacks(stacks: &mut Vec<flatland_protocol::ItemStack>, amount: u64) {
98    if amount == 0 {
99        return;
100    }
101    let held = stacks
102        .iter()
103        .map(|s| copper_value(&s.template_id).saturating_mul(s.quantity as u64))
104        .sum::<u64>();
105    if held < amount {
106        return;
107    }
108    let rest = held.saturating_sub(amount);
109    for (_, template) in DENOM_TEMPLATES {
110        drain_template_stacks(stacks, template, u32::MAX);
111    }
112    add_copper_stacks(stacks, rest);
113}
114
115/// Remove up to `qty` of a template from root inventory stacks.
116pub fn drain_template_stacks(
117    stacks: &mut Vec<flatland_protocol::ItemStack>,
118    template_id: &str,
119    qty: u32,
120) {
121    let mut remaining = qty;
122    stacks.retain_mut(|s| {
123        if s.template_id != template_id || remaining == 0 {
124            return true;
125        }
126        let take = remaining.min(s.quantity);
127        s.quantity = s.quantity.saturating_sub(take);
128        remaining = remaining.saturating_sub(take);
129        s.quantity > 0
130    });
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn apply_coins_delta_mints_and_spends() {
139        let mut stacks = Vec::new();
140        add_copper_stacks(&mut stacks, 150);
141        assert_eq!(copper_from_counts(&counts_from_stacks(&stacks)), 150);
142        spend_copper_stacks(&mut stacks, 100);
143        assert_eq!(copper_from_counts(&counts_from_stacks(&stacks)), 50);
144    }
145
146    #[test]
147    fn drain_template_stacks_removes_quantity() {
148        let mut stacks = vec![
149            flatland_protocol::ItemStack::simple("iron_ore", 2),
150            flatland_protocol::ItemStack::simple("oak_log", 5),
151        ];
152        drain_template_stacks(&mut stacks, "oak_log", 3);
153        assert_eq!(stacks.len(), 2);
154        assert_eq!(
155            stacks
156                .iter()
157                .find(|s| s.template_id == "oak_log")
158                .map(|s| s.quantity),
159            Some(2)
160        );
161    }
162
163    fn counts_from_stacks(
164        stacks: &[flatland_protocol::ItemStack],
165    ) -> std::collections::HashMap<String, u32> {
166        let mut counts = std::collections::HashMap::new();
167        for s in stacks {
168            *counts.entry(s.template_id.clone()).or_insert(0) += s.quantity;
169        }
170        counts
171    }
172}