diff --git a/src/combat.rs b/src/combat.rs index 0887c84..5d10c64 100644 --- a/src/combat.rs +++ b/src/combat.rs @@ -1,6 +1,6 @@ use rand::Rng; -use crate::entities::{Agent, AgentType, Henchman, HenchmanAbility, Minion, Player}; +use crate::entities::{Agent, AgentType, Henchman, HenchmanAbility, Minion, MinionClass, Player}; use crate::map::GameMap; use crate::tiles::TileType; @@ -86,12 +86,14 @@ impl CombatSystem { // Recalculate path agents[i].reset_moves(); - let avoid: std::collections::HashSet<(i32, i32)> = agents + let mut avoid: std::collections::HashSet<(i32, i32)> = agents .iter() .enumerate() .filter(|&(j, _)| j != i && agents[j].entity.alive) .map(|(_, a)| (a.entity.x, a.entity.y)) .collect(); + // Avoid known trap positions (agents that have seen traps trigger) + avoid.extend(agents[i].known_traps.iter()); agents[i].calculate_path(game_map, target, &avoid); // Move along path @@ -250,6 +252,9 @@ impl CombatSystem { } } + // Share trap knowledge among surviving agents + Agent::share_trap_knowledge(agents); + false } @@ -301,6 +306,63 @@ impl CombatSystem { } } Some(j) => { + // Class-specific behavior + match minions[i].class { + MinionClass::Worker => { + // Workers flee from agents — move away from nearest agent + if nearest_dist <= 3 { + let dx = minions[i].entity.x - agents[j].entity.x; + let dy = minions[i].entity.y - agents[j].entity.y; + let flee_x = minions[i].entity.x + dx.signum(); + let flee_y = minions[i].entity.y + dy.signum(); + if game_map.is_walkable(flee_x, flee_y) { + minions[i].entity.x = flee_x; + minions[i].entity.y = flee_y; + } + continue; + } + // Workers too far from agents just return home + if (minions[i].entity.x, minions[i].entity.y) != (minions[i].home_x, minions[i].home_y) + && let Some(path) = game_map.find_path( + (minions[i].entity.x, minions[i].entity.y), + (minions[i].home_x, minions[i].home_y), + &std::collections::HashSet::new(), + ) + && path.len() >= 2 { + minions[i].entity.x = path[1].0; + minions[i].entity.y = path[1].1; + } + continue; + } + MinionClass::Scientist => { + // Scientists flee from agents and return to lab + if nearest_dist <= 4 { + let dx = minions[i].entity.x - agents[j].entity.x; + let dy = minions[i].entity.y - agents[j].entity.y; + let flee_x = minions[i].entity.x + dx.signum(); + let flee_y = minions[i].entity.y + dy.signum(); + if game_map.is_walkable(flee_x, flee_y) { + minions[i].entity.x = flee_x; + minions[i].entity.y = flee_y; + } + continue; + } + // Return to home (lab) + if (minions[i].entity.x, minions[i].entity.y) != (minions[i].home_x, minions[i].home_y) + && let Some(path) = game_map.find_path( + (minions[i].entity.x, minions[i].entity.y), + (minions[i].home_x, minions[i].home_y), + &std::collections::HashSet::new(), + ) + && path.len() >= 2 { + minions[i].entity.x = path[1].0; + minions[i].entity.y = path[1].1; + } + continue; + } + _ => {} // Guards and Technicians fight normally + } + if nearest_dist == 1 { let dmg = minions[i].entity.damage; agents[j].entity.take_damage(dmg); diff --git a/src/entities/agent.rs b/src/entities/agent.rs index 90c8e18..9af4731 100644 --- a/src/entities/agent.rs +++ b/src/entities/agent.rs @@ -101,6 +101,8 @@ pub struct Agent { pub gold_value: i32, pub heat_increase: i32, pub has_stolen: bool, + /// Positions of traps this agent has seen trigger — they'll avoid these + pub known_traps: std::collections::HashSet<(i32, i32)>, } impl Agent { @@ -149,6 +151,7 @@ impl Agent { gold_value, heat_increase, has_stolen: false, + known_traps: std::collections::HashSet::new(), } } @@ -209,15 +212,22 @@ impl Agent { // Agents with trap detection can avoid traps 50% of the time if self.can_detect_traps && rng.random_range(0.0..1.0) < 0.5 { messages.push(format!("{} detects and avoids the {}!", self.entity.name, tile.name())); + // Remember this trap location for future pathing + self.known_traps.insert((self.entity.x, self.entity.y)); return messages; } let bypasses_resist = tile == TileType::TrapLaser; if !bypasses_resist && rng.random::() < self.trap_resist { messages.push(format!("{} resists the {}!", self.entity.name, tile.name())); + // Still remember the trap location + self.known_traps.insert((self.entity.x, self.entity.y)); return messages; } + // Remember this trap location + self.known_traps.insert((self.entity.x, self.entity.y)); + let dmg = tile.trap_damage() + trap_damage_bonus; self.entity.take_damage(dmg); messages.push(format!( @@ -256,6 +266,24 @@ impl Agent { messages } + /// Share trap knowledge with nearby agents (within 6 tiles). + /// Called after an agent triggers or detects a trap. + pub fn share_trap_knowledge(agents: &mut [Agent]) { + // Collect all known traps from agents that have seen traps + let all_known: std::collections::HashSet<(i32, i32)> = agents.iter() + .filter(|a| a.entity.alive) + .flat_map(|a| a.known_traps.iter()) + .copied() + .collect(); + + // Share with all alive agents + for agent in agents.iter_mut() { + if agent.entity.alive { + agent.known_traps = all_known.clone(); + } + } + } + pub fn get_splash_info(&self, game_map: &GameMap) -> Option<(i32, (i32, i32))> { let tile = game_map.get_tile(self.entity.x, self.entity.y); if tile == TileType::TrapFire && game_map.powered.contains(&(self.entity.x, self.entity.y)) { @@ -465,4 +493,47 @@ mod tests { let high = generate_agent_wave(1, 50, &entries); assert!(high.len() >= low.len(), "Higher heat should produce at least as many agents"); } + + #[test] + fn new_agent_has_no_known_traps() { + let agent = Agent::new(5, 5, "investigator", 1); + assert!(agent.known_traps.is_empty()); + } + + #[test] + fn trap_knowledge_is_shared() { + let mut agents = vec![ + Agent::new(5, 5, "investigator", 1), + Agent::new(10, 10, "investigator", 1), + ]; + // First agent learns about a trap + agents[0].known_traps.insert((20, 20)); + // Second agent learns about a different trap + agents[1].known_traps.insert((30, 30)); + + Agent::share_trap_knowledge(&mut agents); + + // Both agents should now know about both traps + assert!(agents[0].known_traps.contains(&(20, 20))); + assert!(agents[0].known_traps.contains(&(30, 30))); + assert!(agents[1].known_traps.contains(&(20, 20))); + assert!(agents[1].known_traps.contains(&(30, 30))); + } + + #[test] + fn dead_agents_dont_share_traps() { + let mut agents = vec![ + Agent::new(5, 5, "investigator", 1), + Agent::new(10, 10, "investigator", 1), + ]; + agents[0].entity.alive = false; + agents[0].known_traps.insert((20, 20)); + agents[1].known_traps.insert((30, 30)); + + Agent::share_trap_knowledge(&mut agents); + + // Dead agent's knowledge shouldn't be shared + assert!(!agents[1].known_traps.contains(&(20, 20))); + assert!(agents[1].known_traps.contains(&(30, 30))); + } }