use std::time::Instant; use crate::ansi; use crate::game::{CombatAction, GameState}; pub struct CombatRoundResult { pub output: String, pub npc_died: bool, pub player_died: bool, pub xp_gained: i32, pub fled: bool, } pub fn resolve_combat_tick( player_id: usize, state: &mut GameState, ) -> Option { let (npc_id, action, was_defending) = { let conn = state.players.get(&player_id)?; let combat = conn.combat.as_ref()?; let action = combat.action.clone().unwrap_or(CombatAction::Attack); (combat.npc_id.clone(), action, combat.defending) }; let npc_template = state.world.get_npc(&npc_id)?.clone(); let npc_combat = npc_template.combat.as_ref()?; let instance = state.npc_instances.get(&npc_id)?; if !instance.alive { if let Some(conn) = state.players.get_mut(&player_id) { conn.combat = None; } return Some(CombatRoundResult { output: format!( " {} {} is already dead. Combat ended.\r\n", ansi::color(ansi::DIM, "--"), npc_template.name, ), npc_died: false, player_died: false, xp_gained: 0, fled: false, }); } let npc_hp_before = instance.hp; let conn = state.players.get(&player_id)?; let p_atk = conn.player.effective_attack(&state.world); let p_def = conn.player.effective_defense(&state.world); let _ = conn; let mut out = String::new(); let mut npc_died = false; let mut player_died = false; let mut xp_gained = 0; let mut fled = false; match action { CombatAction::Attack => { let roll = state.rng.next_range(1, 6); let player_dmg = (p_atk - npc_combat.defense / 2 + roll).max(1); let new_npc_hp = (npc_hp_before - player_dmg).max(0); out.push_str(&format!( " {} You strike {} for {} damage!\r\n", ansi::color(ansi::YELLOW, ">>"), ansi::color(ansi::RED, &npc_template.name), ansi::bold(&player_dmg.to_string()), )); if new_npc_hp <= 0 { let player_name = state.players.get(&player_id).map(|c| c.player.name.clone()).unwrap_or_else(|| "Unknown".into()); log::info!("Combat: Player '{}' (ID {}) killed NPC '{}' ({})", player_name, player_id, npc_template.name, npc_id); if let Some(inst) = state.npc_instances.get_mut(&npc_id) { inst.alive = false; inst.hp = 0; inst.death_time = Some(Instant::now()); } npc_died = true; xp_gained = npc_combat.xp_reward; let gold_gained = npc_template.gold; let silver_gained = npc_template.silver; let copper_gained = npc_template.copper; out.push_str(&format!( " {} {} collapses! You gain {} XP and {}g {}s {}c.\r\n", ansi::color(ansi::GREEN, "**"), ansi::color(ansi::RED, &npc_template.name), ansi::bold(&xp_gained.to_string()), gold_gained, silver_gained, copper_gained )); if let Some(conn) = state.players.get_mut(&player_id) { conn.combat = None; conn.player.stats.xp += xp_gained; conn.player.gold += gold_gained; conn.player.silver += silver_gained; conn.player.copper += copper_gained; } } else { if let Some(inst) = state.npc_instances.get_mut(&npc_id) { inst.hp = new_npc_hp; } out.push_str(&format!( " {} {} HP: {}/{}\r\n", ansi::color(ansi::DIM, " "), npc_template.name, new_npc_hp, npc_combat.max_hp, )); } } CombatAction::Defend => { if let Some(conn) = state.players.get_mut(&player_id) { if let Some(ref mut combat) = conn.combat { combat.defending = true; } } out.push_str(&format!( " {} You brace yourself and raise your guard.\r\n", ansi::color(ansi::CYAN, "[]"), )); } CombatAction::Flee => { let flee_chance = 40 + (p_def / 2).min(30); let roll = state.rng.next_range(1, 100); if roll <= flee_chance { if let Some(conn) = state.players.get_mut(&player_id) { conn.combat = None; } fled = true; out.push_str(&format!( " {} You disengage and flee from combat!\r\n", ansi::color(ansi::GREEN, "<<"), )); } else { out.push_str(&format!( " {} You try to flee but {} blocks your escape!\r\n", ansi::color(ansi::RED, "!!"), ansi::color(ansi::RED, &npc_template.name), )); } } CombatAction::Cast(ref spell_id) => { let spell = state.world.get_spell(spell_id).cloned(); if let Some(spell) = spell { if let Some(conn) = state.players.get_mut(&player_id) { if spell.cost_mana > conn.player.stats.mana { out.push_str(&format!( " {} Not enough mana for {}!\r\n", ansi::color(ansi::RED, "!!"), spell.name, )); } else if spell.cost_endurance > conn.player.stats.endurance { out.push_str(&format!( " {} Not enough endurance for {}!\r\n", ansi::color(ansi::RED, "!!"), spell.name, )); } else { conn.player.stats.mana -= spell.cost_mana; conn.player.stats.endurance -= spell.cost_endurance; if spell.cooldown_ticks > 0 { conn.player.cooldowns.insert(spell_id.clone(), spell.cooldown_ticks); } if spell.spell_type == "heal" { let old_hp = conn.player.stats.hp; conn.player.stats.hp = (conn.player.stats.hp + spell.heal).min(conn.player.stats.max_hp); let healed = conn.player.stats.hp - old_hp; out.push_str(&format!( " {} You cast {}! Restored {} HP.\r\n", ansi::color(ansi::GREEN, "++"), ansi::color(ansi::CYAN, &spell.name), ansi::bold(&healed.to_string()), )); } else { // Offensive spell let spell_dmg = spell.damage + state.rng.next_range(1, 4); let new_npc_hp = (npc_hp_before - spell_dmg).max(0); out.push_str(&format!( " {} You cast {} on {} for {} {} damage!\r\n", ansi::color(ansi::MAGENTA, "**"), ansi::color(ansi::CYAN, &spell.name), ansi::color(ansi::RED, &npc_template.name), ansi::bold(&spell_dmg.to_string()), spell.damage_type, )); if new_npc_hp <= 0 { if let Some(inst) = state.npc_instances.get_mut(&npc_id) { inst.alive = false; inst.hp = 0; inst.death_time = Some(Instant::now()); } npc_died = true; xp_gained = npc_combat.xp_reward; out.push_str(&format!( " {} {} collapses! You gain {} XP.\r\n", ansi::color(ansi::GREEN, "**"), ansi::color(ansi::RED, &npc_template.name), ansi::bold(&xp_gained.to_string()), )); conn.combat = None; conn.player.stats.xp += xp_gained; } else { if let Some(inst) = state.npc_instances.get_mut(&npc_id) { inst.hp = new_npc_hp; } out.push_str(&format!( " {} {} HP: {}/{}\r\n", ansi::color(ansi::DIM, " "), npc_template.name, new_npc_hp, npc_combat.max_hp, )); } } // Apply status effect from spell if let Some(ref eff) = spell.effect { if spell.spell_type == "offensive" { // Could apply effect to NPC in future, for now just note it } else { let pname = conn.player.name.clone(); state.db.save_effect(&pname, eff, spell.effect_duration, spell.effect_magnitude); } } } } } else { out.push_str(&format!( " {} Spell not found.\r\n", ansi::color(ansi::RED, "!!"), )); } } CombatAction::UseItem(idx) => { if let Some(conn) = state.players.get_mut(&player_id) { if idx < conn.player.inventory.len() { let obj = &conn.player.inventory[idx]; if obj.kind.as_deref() == Some("consumable") { let heal = obj.stats.heal_amount.unwrap_or(0); let name = obj.name.clone(); conn.player.inventory.remove(idx); let old_hp = conn.player.stats.hp; conn.player.stats.hp = (conn.player.stats.hp + heal).min(conn.player.stats.max_hp); let healed = conn.player.stats.hp - old_hp; out.push_str(&format!( " {} You use the {}. Restored {} HP.\r\n", ansi::color(ansi::GREEN, "++"), ansi::color(ansi::CYAN, &name), ansi::bold(&healed.to_string()), )); } else { out.push_str(&format!( " {} You can't use that in combat.\r\n", ansi::color(ansi::RED, "!!"), )); } } else { out.push_str(&format!( " {} Item not found in inventory.\r\n", ansi::color(ansi::RED, "!!"), )); } } } } // Clear the queued action if let Some(conn) = state.players.get_mut(&player_id) { if let Some(ref mut combat) = conn.combat { combat.action = None; } } // NPC counter-attack (if player is still in combat and NPC is alive) let still_in_combat = state .players .get(&player_id) .map(|c| c.combat.is_some()) .unwrap_or(false); let npc_alive = state .npc_instances .get(&npc_id) .map(|i| i.alive) .unwrap_or(false); if still_in_combat && npc_alive && !fled { let is_defending = state .players .get(&player_id) .and_then(|c| c.combat.as_ref()) .map(|c| c.defending) .unwrap_or(was_defending); let defense_mult = if is_defending { 2.0 } else { 1.0 }; let effective_def = (p_def as f32 * defense_mult) as i32; let npc_roll = state.rng.next_range(1, 6); let npc_dmg = (npc_combat.attack - effective_def / 2 + npc_roll).max(1); if let Some(conn) = state.players.get_mut(&player_id) { conn.player.stats.hp = (conn.player.stats.hp - npc_dmg).max(0); let hp = conn.player.stats.hp; let max_hp = conn.player.stats.max_hp; out.push_str(&format!( " {} {} strikes you for {} damage!{}\r\n", ansi::color(ansi::RED, "<<"), ansi::color(ansi::RED, &npc_template.name), ansi::bold(&npc_dmg.to_string()), if is_defending { " (blocked some)" } else { "" }, )); let hp_color = if hp * 3 < max_hp { ansi::RED } else if hp * 3 < max_hp * 2 { ansi::YELLOW } else { ansi::GREEN }; out.push_str(&format!( " {} Your HP: {}{}/{}{}\r\n", ansi::color(ansi::DIM, " "), hp_color, hp, max_hp, ansi::RESET, )); if hp <= 0 { player_died = true; conn.combat = None; } // Reset defending after the round if let Some(ref mut combat) = conn.combat { combat.defending = false; } } } Some(CombatRoundResult { output: out, npc_died, player_died, xp_gained, fled, }) } pub fn player_death_respawn(player_id: usize, state: &mut GameState) -> String { let spawn_room = state.spawn_room().to_string(); let player_name = state .players .get(&player_id) .map(|c| c.player.name.clone()) .unwrap_or_else(|| "Unknown".into()); log::info!("Combat: Player '{}' (ID {}) died and respawned at {}", player_name, player_id, spawn_room); if let Some(conn) = state.players.get_mut(&player_id) { conn.player.stats.hp = conn.player.stats.max_hp; conn.player.room_id = spawn_room; conn.combat = None; } // Clear status effects on death state.db.clear_effects(&player_name); format!( "\r\n{}\r\n{}\r\n{}\r\n{}\r\n", ansi::color(ansi::RED, " ╔═══════════════════════════╗"), ansi::color(ansi::RED, " ║ YOU HAVE DIED! ║"), ansi::color(ansi::RED, " ╚═══════════════════════════╝"), ansi::system_msg("You awaken at the town square, fully healed."), ) }