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