feat(combat): Update combat functions to integrate collectible yield system with new rewards during combat

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-16 22:02:36 -07:00
parent 5ef36b0cd4
commit 61a49f3fed
2 changed files with 115 additions and 0 deletions

View file

@ -2,6 +2,7 @@ pub mod bonuses;
pub mod keywords;
pub mod loot;
pub mod promotions;
pub mod requirements;
pub mod resolver;
pub mod siege;
pub mod wilds;
@ -20,6 +21,9 @@ pub use resolver::{
CombatOutcome, CombatParams, CombatResolver, CombatResult, CombatType, UnitAttributes,
UnitStats,
};
pub use requirements::{
check_strategic_reqs, credit_resources, debit_resources, MissingResource,
};
pub use siege::{melee_wall_penalty, siege_city_bonus, split_ranged_damage_vs_city};
pub use wilds::wild_combat_stats;

View file

@ -0,0 +1,111 @@
use std::collections::BTreeMap;
/// A required strategic resource was not available in the empire ledger.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MissingResource(pub String);
impl std::fmt::Display for MissingResource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "missing strategic resource: {}", self.0)
}
}
/// Returns `Ok(())` if every required resource has at least one unit in the
/// ledger, otherwise `Err(MissingResource)` for the first missing entry.
///
/// `unit_reqs` is the `requires_resource` list from unit JSON (may be empty).
/// `ledger` maps resource ID → stockpile count (decremented on build,
/// credited on unit death — see `debit_resource` / `credit_resource`).
pub fn check_strategic_reqs(
unit_reqs: &[String],
ledger: &BTreeMap<String, u32>,
) -> Result<(), MissingResource> {
for req in unit_reqs {
let count = ledger.get(req.as_str()).copied().unwrap_or(0);
if count == 0 {
return Err(MissingResource(req.clone()));
}
}
Ok(())
}
/// Deduct one unit of each required resource from the ledger on successful
/// build. Saturates at zero (cannot go negative).
pub fn debit_resources(unit_reqs: &[String], ledger: &mut BTreeMap<String, u32>) {
for req in unit_reqs {
let entry = ledger.entry(req.clone()).or_insert(0);
*entry = entry.saturating_sub(1);
}
}
/// Return one unit of each required resource to the ledger when the unit dies.
pub fn credit_resources(unit_reqs: &[String], ledger: &mut BTreeMap<String, u32>) {
for req in unit_reqs {
*ledger.entry(req.clone()).or_insert(0) += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ledger(pairs: &[(&str, u32)]) -> BTreeMap<String, u32> {
pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect()
}
#[test]
fn no_reqs_always_passes() {
assert!(check_strategic_reqs(&[], &BTreeMap::new()).is_ok());
}
#[test]
fn missing_resource_returns_err() {
let reqs = vec!["iron_ore".to_string()];
let result = check_strategic_reqs(&reqs, &BTreeMap::new());
assert_eq!(result, Err(MissingResource("iron_ore".to_string())));
}
#[test]
fn present_resource_returns_ok() {
let reqs = vec!["iron_ore".to_string()];
let ld = ledger(&[("iron_ore", 2)]);
assert!(check_strategic_reqs(&reqs, &ld).is_ok());
}
#[test]
fn debit_decrements_ledger() {
let reqs = vec!["iron_ore".to_string()];
let mut ld = ledger(&[("iron_ore", 3)]);
debit_resources(&reqs, &mut ld);
assert_eq!(ld["iron_ore"], 2);
}
#[test]
fn debit_saturates_at_zero() {
let reqs = vec!["iron_ore".to_string()];
let mut ld = ledger(&[("iron_ore", 0)]);
debit_resources(&reqs, &mut ld);
assert_eq!(ld["iron_ore"], 0);
}
#[test]
fn golden_build_then_kill_restores_iron() {
let reqs = vec!["iron_ore".to_string()];
let mut ld = ledger(&[("iron_ore", 1)]);
// Build cavalry: check then debit
assert!(check_strategic_reqs(&reqs, &ld).is_ok());
debit_resources(&reqs, &mut ld);
assert_eq!(ld["iron_ore"], 0);
// Unit is now alive, ledger at 0 — another build blocked
assert!(check_strategic_reqs(&reqs, &ld).is_err());
// Unit dies: credit returns the resource
credit_resources(&reqs, &mut ld);
assert_eq!(ld["iron_ore"], 1);
// Can build again
assert!(check_strategic_reqs(&reqs, &ld).is_ok());
}
}