Guard and attack markers (shields and swords, up to eight each, right click removes); the world and the interface are copied opaque so glows keep their brightness
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a1a53273cd
commit
c9dafb1606
24 files changed
+470
-46
No files matched your search
+212
-6
@@ -1,11 +1,14 @@
|
||||
// Orders from the hand: "attack" sends the fighting creatures nearby at an enemy unit or tower,
|
||||
// "guard" posts a few creatures in a room. Right click with an order tool cancels the orders.
|
||||
// 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
|
||||
@@ -26,8 +29,8 @@ impl Game {
|
||||
(Some(j), _) => (self.ents[j].x, self.ents[j].y),
|
||||
(None, Some((_, p))) => p,
|
||||
_ => {
|
||||
self.hint(31, "hint.order_attack");
|
||||
self.sfx_ui(Sfx::Error);
|
||||
// open ground: an attack marker
|
||||
self.place_mark(wx.floor() as i32, wy.floor() as i32, true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -59,8 +62,168 @@ impl Game {
|
||||
self.msg("msg.order_attack", vec![n.to_string()]);
|
||||
}
|
||||
|
||||
// guard: a few creatures take positions in the room at (x, y)
|
||||
// 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<usize> = (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<usize> = 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;
|
||||
}
|
||||
@@ -134,6 +297,13 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
// 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::*;
|
||||
@@ -152,10 +322,46 @@ mod tests {
|
||||
let j = g.find(id).unwrap();
|
||||
g.ents[j].job = Job::None;
|
||||
}
|
||||
g.order_guard(cx - 1, cy - 1);
|
||||
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<u32> = (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))));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user