Manual AI seats for matches: host toggles empty seats, co-op allies fight heroes

This commit is contained in:
Richard Zink committed 2026-11-05 06:38:38 +01:00
1 parent 3492eacd59
commit ee7b76fa6a
10 files changed
+308 -43

No files matched your search

+35 -6
View File
@@ -1308,12 +1308,15 @@ impl Game {
side == self.me_side || self.masters.iter().any(|m| m.side == side)
}
fn is_hostile_master(&self, side: u8) -> bool {
self.coop && Owner::MASTER_SIDES.contains(&self.me_side) && Owner::MASTER_SIDES.contains(&side)
pub(crate) fn is_coop_ally_side(&self, side: u8) -> bool {
self.coop
&& Owner::MASTER_SIDES.contains(&self.local_side)
&& Owner::MASTER_SIDES.contains(&side)
&& (side == self.local_side || self.is_master(side) || self.ai_sides.iter().any(|&s| s == side))
}
pub(crate) fn is_hostile(&self, e: &Ent) -> bool {
e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != self.me_side && !self.is_hostile_master(e.side())
e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != self.me_side && !self.is_coop_ally_side(e.side())
}
pub fn is_rival_side(&self, side: u8) -> bool {
@@ -1420,7 +1423,10 @@ impl Game {
#[allow(dead_code)] // used by the tests; the *Multiplayer* menu (step D) will call it
pub fn start_masters(&mut self, sides: &[u8], local: u8) -> bool {
let sides: Vec<u8> = sides.iter().copied().filter(|s| Owner::MASTER_SIDES.contains(s)).collect();
if sides.len() < 2 || !sides.contains(&0) || !sides.contains(&local) {
if sides.is_empty() || !sides.contains(&0) || !sides.contains(&local) {
return false;
}
if sides.len() < 2 && !self.coop {
return false;
}
let (w, h) = (mw() as i32, mh() as i32);
@@ -2362,8 +2368,8 @@ impl Game {
let mut bd = radius * radius;
for o in &self.ents {
// every other side is an enemy; co-op master sides fight the heroes, not each other
let coop_allies = self.coop && Owner::MASTER_SIDES.contains(&e.side()) && Owner::MASTER_SIDES.contains(&o.side());
if !o.alive || o.held || o.side() == e.side() || coop_allies || o.captive != 0 || o.chained || o.invis > 0.0 {
let coop_ally = self.is_coop_ally_side(o.side());
if !o.alive || o.held || o.side() == e.side() || coop_ally || o.captive != 0 || o.chained || o.invis > 0.0 {
continue;
}
let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2);
@@ -6295,5 +6301,28 @@ mod tests {
assert!(seen);
assert!(g.ents.iter().filter(|e| e.kind == EK::Chainbreaker).count() <= 1);
}
#[test]
fn a_coop_ai_seat_fights_beside_the_host_not_against_him() {
let mut g = Game::new(11, 96, 128);
g.init_fog();
g.coop = true;
assert!(g.start_masters(&[0, 2, 3], 2), "a co-op match may keep an empty side as AI");
g.sync_ai_sides(&[3], 0);
assert!(g.is_coop_ally_side(3), "side 3 is an allied master seat, not a rival");
g.ents.clear();
let (cx, cy) = g.map.core;
let local = g.spawn(EK::Kobold, false, cx as f32 + 3.5, cy as f32 + 3.5, 1);
let ally = g.spawn(EK::Kobold, false, cx as f32 + 4.5, cy as f32 + 3.5, 1);
if let Some(e) = g.ents.iter_mut().find(|e| e.id == ally) {
e.side = 3;
}
let local = g.ents.iter().find(|e| e.id == local).unwrap().clone();
let ally = g.ents.iter().find(|e| e.id == ally).unwrap().clone();
assert!(!g.is_hostile(&ally));
assert_eq!(g.nearest_enemy(&local, 5.0), None, "the allied AI creature is no target for the local master");
}
}