// Orders: "attack" on an enemy unit or tower sends the fighting creatures nearby at it; on open // ground it sets an attack marker (a sword) - the ready creatures share out over all swords and // fight where they stand. "Guard" sets a guard marker (a shield) that three creatures hold. Up to // eight of each; a right click on a marker removes it (elsewhere: the guards stand down). use crate::game::*; use crate::map::*; const ORDER_RADIUS: f32 = 18.0; const GUARDS: usize = 3; pub const MAX_MARKS: usize = 8; fn ready(e: &Ent) -> bool { e.alive && !e.held && e.creature() && !matches!(e.job, Job::Leave | Job::Register | Job::MakeBed(_)) && e.hp > e.max_hp * 0.25 } impl Game { // attack: the enemy (unit, or tower) at (wx, wy) pub fn order_attack(&mut self, picked: Option, wx: f32, wy: f32) { let unit = picked.filter(|&j| j < self.ents.len() && self.is_hostile(&self.ents[j])).or_else(|| { self.ents.iter().position(|e| self.is_hostile(e) && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 1.5 * 1.5) }); let tower = if unit.is_none() { self.towers.iter().find(|t| t.hero && (t.pos().0 - wx).powi(2) + (t.pos().1 - wy).powi(2) < 1.2 * 1.2).map(|t| (t.id, t.pos())) } else { None }; let (tx, ty) = match (unit, tower) { (Some(j), _) => (self.ents[j].x, self.ents[j].y), (None, Some((_, p))) => p, _ => { // open ground: an attack marker self.place_mark(wx.floor() as i32, wy.floor() as i32, true); return; } }; let mut n = 0; for e in self.ents.iter_mut().filter(|e| ready(e) && (e.x - tx).powi(2) + (e.y - ty).powi(2) < ORDER_RADIUS * ORDER_RADIUS) { e.job = match (unit, tower) { (Some(_), _) => Job::Attack(0), (None, Some((id, _))) => Job::AttackTower(id), _ => continue, }; e.path.clear(); e.repath = 0.0; n += 1; } if let Some(j) = unit { let id = self.ents[j].id; for e in self.ents.iter_mut() { if e.job == Job::Attack(0) { e.job = Job::Attack(id); } } } if n == 0 { self.hint(31, "hint.order_nobody"); return; } self.fx(7, tx, ty, 0.0, 0.0, 0.6, 0xff3040ff); self.sfx_ui(Sfx::FlagSet); self.msg("msg.order_attack", vec![n.to_string()]); } // guard: a guard marker at (x, y) (three creatures hold it, see update_marks) pub fn order_guard(&mut self, x: i32, y: i32) { self.place_mark(x, y, false); } // a sword (attack) or shield (guard) marker on tile (x, y) pub fn place_mark(&mut self, x: i32, y: i32, attack: bool) { if !inb(x, y) { return; } let i = idx(x, y); let t = self.map.t[i]; // a guard marker stands on own ground, an attack marker anywhere one can walk let ok = if attack { t.walkable() || t.bridge() } else { t.owner == self.me && t.walkable() }; if !ok { self.hint(31, if attack { "hint.order_attack" } else { "hint.order_guard" }); self.sfx_ui(Sfx::Error); return; } let marks = if attack { &self.attack_marks } else { &self.guard_marks }; if marks.iter().any(|&m| { let (mx, my) = xy(m); (mx - x).abs() <= 1 && (my - y).abs() <= 1 }) { return; // there is one already } if marks.len() >= MAX_MARKS { self.hint(31, "hint.marks_full"); self.sfx_ui(Sfx::Error); return; } if attack { self.attack_marks.push(i); } else { self.guard_marks.push(i); } self.marks_t = 0.0; // creatures are sent at once self.fx(7, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.6, if attack { 0xff3040ff } else { 0xff40d0ff }); self.sfx_ui(Sfx::FlagSet); self.msg(if attack { "msg.mark_attack" } else { "msg.mark_guard" }, vec![]); } // a right click with an order tool: the marker there goes (its creatures are free again); // without a marker there the guards stand down pub fn unmark(&mut self, wx: f32, wy: f32) { let near = |m: &usize| { let (mx, my) = xy(*m); (mx as f32 + 0.5 - wx).powi(2) + (my as f32 + 0.5 - wy).powi(2) < 1.6 * 1.6 }; if let Some(k) = self.attack_marks.iter().position(near) { let m = self.attack_marks.remove(k); for e in self.ents.iter_mut().filter(|e| e.job == Job::March(m)) { e.job = Job::None; e.path.clear(); } } else if let Some(k) = self.guard_marks.iter().position(near) { let m = self.guard_marks.remove(k); for j in 0..self.ents.len() { if let Job::Guard(spot) = self.ents[j].job { if guard_of(m, spot) && self.ents[j].creature() { let me = self.ents[j].id + 1; self.unreserve(me); self.ents[j].job = Job::None; } } } } else { self.cancel_orders(); return; } self.msg("msg.mark_removed", vec![]); self.sfx_ui(Sfx::Click); } // every second: three creatures for every shield, the ready ones shared out over the swords pub(crate) fn update_marks(&mut self, dt: f32) { self.marks_t -= dt; if self.marks_t > 0.0 || (self.guard_marks.is_empty() && self.attack_marks.is_empty()) { return; } self.marks_t = 1.0; // markers on ground that is gone (dug in, lost) go let me = self.me; let map = &self.map; self.guard_marks.retain(|&m| map.t[m].owner == me && map.t[m].walkable()); self.attack_marks.retain(|&m| map.t[m].walkable() || map.t[m].bridge()); // shields for m in self.guard_marks.clone() { let have = self.ents.iter().filter(|e| e.alive && e.creature() && matches!(e.job, Job::Guard(s) if guard_of(m, s))).count(); for _ in have..GUARDS { if !self.send_guard(m) { break; } } } // swords: every ready creature that is free goes to the nearest sword with room if self.attack_marks.is_empty() { return; } let free: Vec = (0..self.ents.len()) .filter(|&j| { let e = &self.ents[j]; ready(e) && e.hp > e.max_hp * 0.5 && e.energy > 0.15 && matches!(e.job, Job::None | Job::Wander | Job::Train(_) | Job::Study(_) | Job::March(_)) }) .collect(); let cap = free.len().div_ceil(self.attack_marks.len()).max(1); let mut count: Vec = self.attack_marks.iter().map(|&m| self.ents.iter().filter(|e| e.alive && e.job == Job::March(m)).count()).collect(); for j in free { if matches!(self.ents[j].job, Job::March(_)) { continue; } let (ex, ey) = (self.ents[j].x, self.ents[j].y); let best = (0..self.attack_marks.len()).filter(|&k| count[k] < cap).min_by(|&a, &b| { let d = |k: usize| { let (mx, my) = xy(self.attack_marks[k]); (mx as f32 + 0.5 - ex).powi(2) + (my as f32 + 0.5 - ey).powi(2) }; d(a).partial_cmp(&d(b)).unwrap_or(std::cmp::Ordering::Equal) }); let Some(k) = best else { break }; count[k] += 1; let mark = self.attack_marks[k]; let me = self.ents[j].id + 1; self.unreserve(me); let e = &mut self.ents[j]; e.job = Job::March(mark); e.path.clear(); e.repath = 0.0; } } // the nearest free creature takes a free spot next to shield m fn send_guard(&mut self, m: usize) -> bool { let (x, y) = xy(m); let spot = (-2..=2).flat_map(|dy| (-2..=2).map(move |dx| (dx, dy))).filter(|&(dx, dy)| inb(x + dx, y + dy)).map(|(dx, dy)| idx(x + dx, y + dy)).filter(|&j| { let t = &self.map.t[j]; t.walkable() && t.owner == self.me && t.reserved == 0 && !self.ents.iter().any(|e| e.job == Job::Guard(j)) }).min_by_key(|&j| { let (sx, sy) = xy(j); (sx - x).pow(2) + (sy - y).pow(2) }); let Some(spot) = spot else { return false }; let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5); let cand = (0..self.ents.len()).filter(|&j| ready(&self.ents[j]) && !matches!(self.ents[j].job, Job::Attack(_) | Job::Guard(_) | Job::March(_) | Job::Rally)).min_by(|&a, &b| { let d = |j: usize| (self.ents[j].x - fx).powi(2) + (self.ents[j].y - fy).powi(2); d(a).partial_cmp(&d(b)).unwrap_or(std::cmp::Ordering::Equal) }); let Some(j) = cand else { return false }; let Some(p) = self.map.creature_path(flies(self.ents[j].kind), self.ents[j].tile(), spot) else { return false }; let me = self.ents[j].id + 1; self.unreserve(me); self.map.t[spot].reserved = me; let e = &mut self.ents[j]; e.job = Job::Guard(spot); e.path = p; e.timer = 1.0e9; // until the marker goes true } // the old one-off guard order in a room (kept for scripts and tests) #[allow(dead_code)] pub fn order_guard_room(&mut self, x: i32, y: i32) { if !inb(x, y) { return; } let i = idx(x, y); let kind = self.map.t[i].kind; if self.map.t[i].owner != self.me || room_index(kind).is_none() && kind != Kind::Floor { self.hint(31, "hint.order_guard"); self.sfx_ui(Sfx::Error); return; } // the spots: tiles of the same room (or floor) around the click let mut spots: Vec = Vec::new(); for dy in -3..=3 { for dx in -3..=3 { if let Some(t) = self.map.get(x + dx, y + dy) { let j = idx(x + dx, y + dy); if t.kind == kind && t.owner == self.me && t.reserved == 0 && !self.ents.iter().any(|e| e.job == Job::Guard(j)) { spots.push(j); } } } } spots.sort_by_key(|&j| { let (sx, sy) = xy(j); (sx - x).pow(2) + (sy - y).pow(2) }); let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5); let mut cands: Vec = (0..self.ents.len()).filter(|&j| ready(&self.ents[j]) && !matches!(self.ents[j].job, Job::Attack(_) | Job::Guard(_))).collect(); cands.sort_by(|&a, &b| { let da = (self.ents[a].x - fx).powi(2) + (self.ents[a].y - fy).powi(2); let db = (self.ents[b].x - fx).powi(2) + (self.ents[b].y - fy).powi(2); da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) }); let mut n = 0; for (&j, &spot) in cands.iter().zip(spots.iter()).take(GUARDS) { let cur = self.ents[j].tile(); let Some(p) = self.map.creature_path(flies(self.ents[j].kind), cur, spot) else { continue }; let me = self.ents[j].id + 1; self.unreserve(me); self.map.t[spot].reserved = me; let e = &mut self.ents[j]; e.job = Job::Guard(spot); e.path = p; e.timer = 120.0; n += 1; } if n == 0 { self.hint(31, "hint.order_nobody"); return; } self.fx(8, fx, fy, 0.0, 0.0, 0.6, 0xff40d0ff); self.sfx_ui(Sfx::FlagSet); self.msg("msg.order_guard", vec![n.to_string()]); } // right click with an order tool: everybody stands down pub fn cancel_orders(&mut self) { let mut n = 0; for k in 0..self.ents.len() { if matches!(self.ents[k].job, Job::Guard(_)) && self.ents[k].creature() { let me = self.ents[k].id + 1; self.unreserve(me); self.ents[k].job = Job::None; n += 1; } } if n > 0 { self.msg("msg.orders_cancelled", vec![]); self.sfx_ui(Sfx::Click); } } } // is guard spot s next to shield m? fn guard_of(m: usize, s: usize) -> bool { let (mx, my) = xy(m); let (sx, sy) = xy(s); (mx - sx).abs() <= 2 && (my - sy).abs() <= 2 } #[cfg(test)] mod tests { use super::*; #[test] fn creatures_follow_attack_and_guard_orders() { let mut g = Game::new(14, 64, 64); let (cx, cy) = g.map.core; let ids: Vec = (0..4).map(|k| g.spawn(EK::Ogre, false, cx as f32 - 0.5, cy as f32 + 0.5 + k as f32 * 0.3, 2)).collect(); let knight = g.spawn(EK::Knight, true, cx as f32 + 2.5, cy as f32 - 1.5, 1); let j = g.find(knight).unwrap(); g.order_attack(Some(j), 0.0, 0.0); assert!(ids.iter().all(|&id| g.ents[g.find(id).unwrap()].job == Job::Attack(knight))); // guard the treasury ring around the core for &id in &ids { let j = g.find(id).unwrap(); g.ents[j].job = Job::None; } g.order_guard_room(cx - 1, cy - 1); let guards = g.ents.iter().filter(|e| matches!(e.job, Job::Guard(_))).count(); assert_eq!(guards, 3); g.cancel_orders(); assert_eq!(g.ents.iter().filter(|e| matches!(e.job, Job::Guard(_))).count(), 0); } #[test] fn shields_are_held_and_swords_share_the_creatures() { let mut g = Game::new(14, 64, 64); let (cx, cy) = g.map.core; for y in cy - 6..=cy + 8 { for x in cx - 6..=cx + 10 { let t = &mut g.map.t[idx(x, y)]; if t.kind != Kind::Core { t.kind = Kind::Floor; t.owner = Owner::Player; } } } let ids: Vec = (0..6).map(|k| g.spawn(EK::Ogre, false, cx as f32 - 2.5, cy as f32 + 0.5 + k as f32 * 0.3, 2)).collect(); for &id in &ids { let j = g.find(id).unwrap(); g.ents[j].settled = true; g.ents[j].job = Job::None; } g.order_guard(cx + 5, cy + 5); g.update_marks(1.0); let guards = g.ents.iter().filter(|e| matches!(e.job, Job::Guard(_))).count(); assert_eq!(guards, 3, "three hold the shield"); g.place_mark(cx + 8, cy - 4, true); g.place_mark(cx - 4, cy - 5, true); g.update_marks(1.0); let a = g.ents.iter().filter(|e| e.job == Job::March(g.attack_marks[0])).count(); let b = g.ents.iter().filter(|e| e.job == Job::March(g.attack_marks[1])).count(); assert!(a + b == 3 && a.abs_diff(b) <= 1, "{} and {}", a, b); // a right click on a sword frees its creatures let (sx, sy) = xy(g.attack_marks[0]); g.unmark(sx as f32 + 0.5, sy as f32 + 0.5); assert_eq!(g.attack_marks.len(), 1); assert!(!g.ents.iter().any(|e| e.job == Job::March(idx(sx, sy)))); } }