use super::types::Combatant;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DamageResult {
pub actual_damage: i32,
pub is_dead: bool,
pub was_blocked: bool,
}
#[derive(Debug, Clone, issun_macros::Service)]
#[service(name = "combat_service")]
pub struct CombatService {
min_damage: i32,
}
impl CombatService {
pub fn new() -> Self {
Self { min_damage: 1 }
}
pub fn with_min_damage(min_damage: i32) -> Self {
Self { min_damage }
}
pub fn apply_damage<C: Combatant + ?Sized>(
&self,
target: &mut C,
base_damage: i32,
defense: Option<i32>,
) -> DamageResult {
let actual_damage = self.calculate_damage(base_damage, defense);
let was_blocked = defense.is_some() && actual_damage < base_damage;
target.take_damage(actual_damage);
DamageResult {
actual_damage,
is_dead: !target.is_alive(),
was_blocked,
}
}
pub fn calculate_damage(&self, base_damage: i32, defense: Option<i32>) -> i32 {
if let Some(def) = defense {
(base_damage - def).max(self.min_damage)
} else {
base_damage
}
}
pub fn calculate_attack_damage<C: Combatant + ?Sized>(
&self,
attacker: &C,
multiplier: f32,
) -> i32 {
(attacker.attack() as f32 * multiplier) as i32
}
pub fn transfer_hp<C1: Combatant + ?Sized, C2: Combatant + ?Sized>(
&self,
from: &mut C1,
_to: &mut C2,
amount: i32,
) -> i32 {
let actual = amount.min(from.hp());
from.take_damage(actual);
actual
}
pub fn apply_attack<A: Combatant + ?Sized, D: Combatant + ?Sized>(
&self,
attacker: &A,
defender: &mut D,
multiplier: f32,
) -> DamageResult {
let base_damage = self.calculate_attack_damage(attacker, multiplier);
let defense = defender.defense();
self.apply_damage(defender, base_damage, defense)
}
}
impl Default for CombatService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockCombatant {
name: String,
hp: i32,
max_hp: i32,
attack: i32,
defense: Option<i32>,
}
impl Combatant for MockCombatant {
fn name(&self) -> &str {
&self.name
}
fn hp(&self) -> i32 {
self.hp
}
fn max_hp(&self) -> i32 {
self.max_hp
}
fn attack(&self) -> i32 {
self.attack
}
fn defense(&self) -> Option<i32> {
self.defense
}
fn take_damage(&mut self, damage: i32) {
self.hp = (self.hp - damage).max(0);
}
}
#[test]
fn test_calculate_damage_no_defense() {
let service = CombatService::new();
assert_eq!(service.calculate_damage(50, None), 50);
assert_eq!(service.calculate_damage(100, None), 100);
}
#[test]
fn test_calculate_damage_with_defense() {
let service = CombatService::new();
assert_eq!(service.calculate_damage(50, Some(10)), 40);
assert_eq!(service.calculate_damage(50, Some(30)), 20);
}
#[test]
fn test_calculate_damage_minimum() {
let service = CombatService::new();
assert_eq!(service.calculate_damage(10, Some(50)), 1);
assert_eq!(service.calculate_damage(5, Some(100)), 1);
}
#[test]
fn test_apply_damage() {
let service = CombatService::new();
let mut target = MockCombatant {
name: "Target".to_string(),
hp: 100,
max_hp: 100,
attack: 10,
defense: Some(5),
};
let result = service.apply_damage(&mut target, 30, Some(5));
assert_eq!(result.actual_damage, 25);
assert!(result.was_blocked);
assert!(!result.is_dead);
assert_eq!(target.hp, 75);
}
#[test]
fn test_apply_damage_lethal() {
let service = CombatService::new();
let mut target = MockCombatant {
name: "Target".to_string(),
hp: 20,
max_hp: 100,
attack: 10,
defense: None,
};
let result = service.apply_damage(&mut target, 50, None);
assert_eq!(result.actual_damage, 50);
assert!(!result.was_blocked);
assert!(result.is_dead);
assert_eq!(target.hp, 0);
}
#[test]
fn test_calculate_attack_damage() {
let service = CombatService::new();
let attacker = MockCombatant {
name: "Attacker".to_string(),
hp: 100,
max_hp: 100,
attack: 50,
defense: None,
};
assert_eq!(service.calculate_attack_damage(&attacker, 1.0), 50);
assert_eq!(service.calculate_attack_damage(&attacker, 1.5), 75);
assert_eq!(service.calculate_attack_damage(&attacker, 2.0), 100);
}
#[test]
fn test_apply_attack() {
let service = CombatService::new();
let attacker = MockCombatant {
name: "Attacker".to_string(),
hp: 100,
max_hp: 100,
attack: 50,
defense: None,
};
let mut defender = MockCombatant {
name: "Defender".to_string(),
hp: 100,
max_hp: 100,
attack: 30,
defense: Some(10),
};
let result = service.apply_attack(&attacker, &mut defender, 1.0);
assert_eq!(result.actual_damage, 40);
assert!(result.was_blocked);
assert_eq!(defender.hp, 60);
}
#[test]
fn test_transfer_hp() {
let service = CombatService::new();
let mut source = MockCombatant {
name: "Source".to_string(),
hp: 50,
max_hp: 100,
attack: 10,
defense: None,
};
let mut target = MockCombatant {
name: "Target".to_string(),
hp: 30,
max_hp: 100,
attack: 10,
defense: None,
};
let transferred = service.transfer_hp(&mut source, &mut target, 20);
assert_eq!(transferred, 20);
assert_eq!(source.hp, 30);
}
#[test]
fn test_transfer_hp_capped() {
let service = CombatService::new();
let mut source = MockCombatant {
name: "Source".to_string(),
hp: 10,
max_hp: 100,
attack: 10,
defense: None,
};
let mut target = MockCombatant {
name: "Target".to_string(),
hp: 30,
max_hp: 100,
attack: 10,
defense: None,
};
let transferred = service.transfer_hp(&mut source, &mut target, 50);
assert_eq!(transferred, 10);
assert_eq!(source.hp, 0);
}
}