Manual AI seats for matches: host toggles empty seats, co-op allies fight heroes
This commit is contained in:
1 parent
3492eacd59
commit
ee7b76fa6a
10 files changed
+308
-43
No files matched your search
+35
-6
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+49
-2
@@ -29,6 +29,7 @@ pub struct SeatUi {
|
||||
pub name: String,
|
||||
pub ready: bool,
|
||||
pub connected: bool,
|
||||
pub ai: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -51,6 +52,7 @@ pub struct MatchUi {
|
||||
pub is_host: bool,
|
||||
pub sides: Vec<u8>,
|
||||
pub seats: Vec<SeatUi>,
|
||||
pub ai: Vec<u8>,
|
||||
pub rooms: Vec<RoomUi>,
|
||||
pub started: bool,
|
||||
pub paused: bool,
|
||||
@@ -76,6 +78,7 @@ pub struct StartInfo {
|
||||
pub sides: Vec<u8>,
|
||||
pub local_side: Option<u8>,
|
||||
pub mission: Option<usize>,
|
||||
pub ai: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -84,6 +87,7 @@ struct SeatInfo {
|
||||
name: String,
|
||||
ready: bool,
|
||||
connected: bool,
|
||||
ai: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -211,6 +215,13 @@ impl MatchClient {
|
||||
self.outbox.push_back(json!({"t":"take_side","side":side}).to_string());
|
||||
}
|
||||
|
||||
pub fn set_ai(&mut self, side: u8, ai: bool) {
|
||||
if !self.connected {
|
||||
return;
|
||||
}
|
||||
self.outbox.push_back(json!({"t":"set_ai","side":side,"ai":ai}).to_string());
|
||||
}
|
||||
|
||||
pub fn ready(&mut self, ready: bool) {
|
||||
if !self.connected {
|
||||
return;
|
||||
@@ -394,6 +405,12 @@ impl MatchClient {
|
||||
let difficulty = v.get("difficulty").and_then(|x| x.as_u64()).unwrap_or(1) as u8;
|
||||
let sides = array_bytes(v.get("sides"));
|
||||
let mission = v.get("mission").and_then(|x| x.as_u64()).map(|x| x as usize);
|
||||
let mut ai = array_bytes(v.get("ai"));
|
||||
if ai.is_empty() {
|
||||
if let Some(r) = self.room.as_ref() {
|
||||
ai = r.ai.clone();
|
||||
}
|
||||
}
|
||||
let local_side = self.room.as_ref().and_then(|r| r.seats.iter().find(|s| s.name == self.name && s.connected).map(|s| s.side));
|
||||
self.local_side = local_side;
|
||||
if let Some(r) = self.room.as_mut() {
|
||||
@@ -404,8 +421,9 @@ impl MatchClient {
|
||||
if !sides.is_empty() {
|
||||
r.sides = sides.clone();
|
||||
}
|
||||
r.ai = ai.clone();
|
||||
}
|
||||
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission });
|
||||
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission, ai });
|
||||
self.error = None;
|
||||
self.status = None;
|
||||
}
|
||||
@@ -449,7 +467,8 @@ impl MatchClient {
|
||||
ready,
|
||||
is_host,
|
||||
sides: room.sides.clone(),
|
||||
seats: room.seats.iter().map(|s| SeatUi { side: s.side, name: s.name.clone(), ready: s.ready, connected: s.connected }).collect(),
|
||||
ai: room.ai.clone(),
|
||||
seats: room.seats.iter().map(|s| SeatUi { side: s.side, name: s.name.clone(), ready: s.ready, connected: s.connected, ai: s.ai }).collect(),
|
||||
rooms: self.rooms.clone(),
|
||||
started: room.started,
|
||||
paused: room.paused,
|
||||
@@ -477,6 +496,7 @@ fn seats_from(v: Option<&Value>) -> Vec<SeatInfo> {
|
||||
name: s.get("name").and_then(|x| x.as_str()).unwrap_or("").to_string(),
|
||||
ready: s.get("ready").and_then(|x| x.as_bool()).unwrap_or(false),
|
||||
connected: s.get("connected").and_then(|x| x.as_bool()).unwrap_or(false),
|
||||
ai: s.get("ai").and_then(|x| x.as_bool()).unwrap_or(false),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
@@ -542,4 +562,31 @@ mod tests {
|
||||
let chat = client.take_new_chat();
|
||||
assert_eq!(chat[0], (Some(0), "red".to_string(), "hello".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_bridge_holds_ai_seats_through_the_lobby() {
|
||||
let mut client = MatchClient::new();
|
||||
client.on_connect("red");
|
||||
pop(&mut client); // rooms
|
||||
|
||||
client.create(&[0, 2, 3, 4], 96, 128, 1);
|
||||
pop(&mut client); // create
|
||||
|
||||
client.set_ai(3, true);
|
||||
let req = pop(&mut client);
|
||||
assert_eq!(req["t"].as_str(), Some("set_ai"));
|
||||
assert_eq!(req["side"].as_u64(), Some(3));
|
||||
assert_eq!(req["ai"].as_bool(), Some(true));
|
||||
|
||||
client.feed(r#"{"t":"room","code":"ABC123","sides":[0,2,3,4],"seats":[{"side":0,"name":"red","ready":false,"connected":true,"ai":false},{"side":2,"name":"","ready":false,"connected":false,"ai":false},{"side":3,"name":"","ready":false,"connected":false,"ai":true},{"side":4,"name":"","ready":false,"connected":false,"ai":false}],"ai":[3],"turn":0,"started":false,"paused":false,"missing":false}"#);
|
||||
let ui = client.ui();
|
||||
assert_eq!(ui.ai, vec![3u8]);
|
||||
assert!(ui.seats.iter().any(|s| s.side == 3 && s.ai));
|
||||
assert!(ui.seats.iter().any(|s| s.side == 2 && !s.connected && !s.ai));
|
||||
|
||||
client.feed(r#"{"t":"start","seed":7,"map_w":96,"map_h":128,"difficulty":1,"sides":[0,3],"ai":[3]}"#);
|
||||
let start = client.take_start().expect("start info with ai seats");
|
||||
assert_eq!(start.sides, vec![0u8, 3u8]);
|
||||
assert_eq!(start.ai, vec![3u8]);
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -248,6 +248,7 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
|
||||
}
|
||||
}
|
||||
MenuAction::MatchTakeSide(side) => app.client.take_side(side),
|
||||
MenuAction::MatchSetAi(side, ai) => app.client.set_ai(side, ai),
|
||||
MenuAction::MatchReady(ready) => app.client.ready(ready),
|
||||
MenuAction::MatchStart => app.client.start(),
|
||||
MenuAction::MatchResume => app.client.resume(),
|
||||
@@ -518,6 +519,7 @@ impl App {
|
||||
return false;
|
||||
}
|
||||
game.ai_difficulty = info.difficulty;
|
||||
game.sync_ai_sides(&info.ai, info.difficulty);
|
||||
game.hints = self.settings.hints;
|
||||
self.game = game;
|
||||
let (cx, cy) = self.game.map.core;
|
||||
@@ -537,6 +539,7 @@ impl App {
|
||||
}
|
||||
self.game.coop = false;
|
||||
self.game.ai_difficulty = info.difficulty;
|
||||
self.game.sync_ai_sides(&info.ai, info.difficulty);
|
||||
self.game.hints = self.settings.hints;
|
||||
self.game.init_fog();
|
||||
let (cx, cy) = self.game.map.core;
|
||||
@@ -552,11 +555,8 @@ impl App {
|
||||
return;
|
||||
}
|
||||
for (_, cmds, ai) in self.client.take_turns() {
|
||||
if self.coop {
|
||||
self.game.ai_sides.clear();
|
||||
} else {
|
||||
self.game.sync_ai_sides(&ai, self.settings.difficulty);
|
||||
}
|
||||
let difficulty = self.game.ai_difficulty;
|
||||
self.game.sync_ai_sides(&ai, difficulty);
|
||||
self.game.receive_turn(&cmds);
|
||||
}
|
||||
for (_, turn, sum) in self.client.take_checks() {
|
||||
@@ -1098,6 +1098,7 @@ async fn main() {
|
||||
| MenuAction::MatchCreate
|
||||
| MenuAction::MatchCoopCreate(..)
|
||||
| MenuAction::MatchTakeSide(..)
|
||||
| MenuAction::MatchSetAi(..)
|
||||
| MenuAction::MatchReady(..)
|
||||
| MenuAction::MatchStart
|
||||
| MenuAction::MatchResume
|
||||
|
||||
+38
-7
@@ -235,6 +235,7 @@ enum Btn {
|
||||
MatchCoopNext,
|
||||
MatchCoopCreate,
|
||||
MatchTake(usize),
|
||||
MatchAi(usize),
|
||||
MatchReady,
|
||||
MatchStart,
|
||||
MatchResume,
|
||||
@@ -312,6 +313,7 @@ pub enum MenuAction {
|
||||
MatchCreate,
|
||||
MatchCoopCreate(usize),
|
||||
MatchTakeSide(u8),
|
||||
MatchSetAi(u8, bool),
|
||||
MatchReady(bool),
|
||||
MatchStart,
|
||||
MatchResume,
|
||||
@@ -654,6 +656,10 @@ impl Menu {
|
||||
Some(s) => MenuAction::MatchTakeSide(s.side),
|
||||
None => MenuAction::Consumed,
|
||||
},
|
||||
Some(Btn::MatchAi(i)) => match self.match_ui.seats.get(i) {
|
||||
Some(s) if s.side != 0 => MenuAction::MatchSetAi(s.side, !s.ai),
|
||||
_ => MenuAction::Consumed,
|
||||
},
|
||||
Some(Btn::Campaign) => {
|
||||
self.page = Some(Page::Campaign);
|
||||
self.scroll = 0;
|
||||
@@ -1462,7 +1468,12 @@ impl Menu {
|
||||
yy += CH() * 1.4;
|
||||
let seat_cols = (cols - 6) as usize;
|
||||
for (k, s) in ui.seats.iter().take(4).enumerate() {
|
||||
let name = if Some(s.side) == ui.local_side { format!("{} ({})", s.name, tr("net.you")) } else { s.name.clone() };
|
||||
let is_local = Some(s.side) == ui.local_side;
|
||||
let name = if is_local {
|
||||
format!("{} ({})", s.name, tr("net.you"))
|
||||
} else {
|
||||
s.name.clone()
|
||||
};
|
||||
let side_label = match s.side {
|
||||
0 => tr("net.side0"),
|
||||
2 => tr("net.side2"),
|
||||
@@ -1470,13 +1481,33 @@ impl Menu {
|
||||
4 => tr("net.side4"),
|
||||
_ => s.side.to_string(),
|
||||
};
|
||||
let line = format!("{} {} {} {} {}", side_label, name, if s.connected { "●" } else { "○" }, if s.ready { "✓" } else { "—" }, if s.side == 0 { tr("net.host") } else { String::new() });
|
||||
gfx.text(&line, ix, yy, SMALL(), if s.connected { TXT } else { DIM });
|
||||
let status = if s.connected { "●" } else if s.ai { "AI" } else { "○" };
|
||||
let ready = if s.ready || s.ai { "✓" } else { "—" };
|
||||
let line = format!("{} {} {} {} {}", side_label, name, status, ready, if s.side == 0 { tr("net.host") } else { String::new() });
|
||||
gfx.text(&line, ix, yy, SMALL(), if s.connected || s.ai { TXT } else { DIM });
|
||||
if !ui.started && ui.local_side.is_some() && !s.connected {
|
||||
let r = Rect::new(ix + 34.0 * CW(), yy, 12.0 * CW(), CH() * 2.0);
|
||||
let label = if Some(s.side) == ui.local_side { tr("net.you") } else { tr("net.join") };
|
||||
button(gfx, r, &label, r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, Btn::MatchTake(k)));
|
||||
if ui.is_host && s.side != 0 {
|
||||
if s.ai {
|
||||
let r = Rect::new(ix + 34.0 * CW(), yy, 12.0 * CW(), CH() * 2.0);
|
||||
let label = tr("net.ai_off");
|
||||
button(gfx, r, &label, r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, Btn::MatchAi(k)));
|
||||
} else {
|
||||
let join_label = if is_local { tr("net.you") } else { tr("net.join") };
|
||||
let r = Rect::new(ix + 22.0 * CW(), yy, 12.0 * CW(), CH() * 2.0);
|
||||
button(gfx, r, &join_label, r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, Btn::MatchTake(k)));
|
||||
let ai_label = tr("net.ai");
|
||||
let r = Rect::new(ix + 36.0 * CW(), yy, 12.0 * CW(), CH() * 2.0);
|
||||
button(gfx, r, &ai_label, r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, Btn::MatchAi(k)));
|
||||
}
|
||||
} else if !s.ai {
|
||||
let r = Rect::new(ix + 34.0 * CW(), yy, 12.0 * CW(), CH() * 2.0);
|
||||
let label = if is_local { tr("net.you") } else { tr("net.join") };
|
||||
button(gfx, r, &label, r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, Btn::MatchTake(k)));
|
||||
}
|
||||
}
|
||||
yy += CH() * 2.0;
|
||||
}
|
||||
|
||||
+17
-7
@@ -196,16 +196,26 @@ impl Game {
|
||||
// attack: about two thirds of the creatures march on an enemy
|
||||
if self.rivals[k].attack_t <= 0.0 {
|
||||
self.rivals[k].attack_t = p.attack * (0.8 + self.rng.f() * 0.4);
|
||||
let mut targets: Vec<(u8, u32)> = vec![(0, 3)];
|
||||
for o in &self.rivals {
|
||||
if o.alive && o.side != side {
|
||||
targets.push((o.side, 1));
|
||||
let mut targets: Vec<(u8, u32)> = Vec::new();
|
||||
if self.coop {
|
||||
if self.gate_on || self.ents.iter().any(|e| e.alive && e.hero) {
|
||||
targets.push((1, 3));
|
||||
}
|
||||
} else {
|
||||
targets.push((0, 3));
|
||||
for o in &self.rivals {
|
||||
if o.alive && o.side != side && !self.is_coop_ally_side(o.side) {
|
||||
targets.push((o.side, 1));
|
||||
}
|
||||
}
|
||||
if self.has_gate() && self.gate_hp > 0.0 {
|
||||
targets.push((1, 1));
|
||||
}
|
||||
}
|
||||
if self.has_gate() && self.gate_hp > 0.0 {
|
||||
targets.push((1, 1));
|
||||
}
|
||||
let total: u32 = targets.iter().map(|t| t.1).sum();
|
||||
if total == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut r = self.rng.next() % total;
|
||||
let mut target = 0;
|
||||
for (s, w) in targets {
|
||||
|
||||
Reference in new issue
Block a user