flatland-client-lib 0.2.21

Flatland3 remote game client library (TCP session, bots, game state)
Documentation
//! Client-side currency display from inventory stacks.

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 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))
}

const DENOM_TEMPLATES: [(u64, &str); 4] = [
    (COPPER_PER_PLATINUM, PLATINUM),
    (COPPER_PER_GOLD, GOLD),
    (COPPER_PER_SILVER, SILVER),
    (1, COPPER),
];

/// Mint or spend physical coin stacks on the client inventory mirror.
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);
}

/// Remove up to `qty` of a template from root inventory stacks.
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)
        );
    }

    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
    }
}