Co-op campaign matches and command markers: mission relay, allied masters, temporary flags

This commit is contained in:
Richard Zink committed 2026-11-04 20:07:06 +01:00
1 parent dc22e27184
commit 5928acf7f6
12 files changed
+433 -27

No files matched your search

+43 -5
View File
@@ -6,7 +6,7 @@
//
// Lobby messages (JSON, one object per WebSocket frame):
// {"t":"rooms"} -> list of open matches
// {"t":"create","sides":[0,2,3,4]?,"map_w":96?,"map_h":128?,"difficulty":1?}
// {"t":"create","sides":[0,2,3,4]?,"map_w":96?,"map_h":128?,"difficulty":1?,"mission":12?}
// {"t":"join","code":"ABC123"}
// {"t":"take_side","side":2}
// {"t":"ready","ready":true}
@@ -41,6 +41,7 @@ const CHAT_MAX: usize = 80;
const MAX_PENDING: usize = 200;
const MAX_ROOMS: usize = 64;
const MASTER_SIDES: [u8; 4] = [0, 2, 3, 4];
const MAX_MISSIONS: usize = 30; // keep this in step with the campaign mission count in the client
#[derive(Debug, Default)]
pub struct Netplay {
@@ -72,6 +73,7 @@ struct Room {
map_w: usize,
map_h: usize,
difficulty: u8,
mission: Option<usize>,
}
#[derive(Debug)]
@@ -136,6 +138,13 @@ fn sides_of(v: Option<&Value>) -> Vec<u8> {
out
}
fn mission_of(v: Option<&Value>) -> Option<usize> {
match v {
None | Some(Value::Null) => None,
Some(x) => x.as_u64().map(|m| m as usize),
}
}
pub async fn netplay_ws(ws: WebSocketUpgrade, Query(q): Query<WsQuery>, State(st): State<AppState>) -> Response {
ws.on_upgrade(move |socket| handle_socket(socket, st, q))
}
@@ -279,9 +288,14 @@ impl Netplay {
let map_w = msg.get("map_w").and_then(|v| v.as_u64()).unwrap_or(96).clamp(72, 256) as usize;
let map_h = msg.get("map_h").and_then(|v| v.as_u64()).unwrap_or(128).clamp(72, 256) as usize;
let difficulty = msg.get("difficulty").and_then(|v| v.as_u64()).unwrap_or(1).clamp(0, 2) as u8;
let mission = mission_of(msg.get("mission"));
if mission.map_or(false, |m| m >= MAX_MISSIONS) {
self.error(conn, "unknown campaign mission");
return None;
}
let name = self.clients.get(&conn).map(|c| c.name.clone()).unwrap_or_else(|| "Guest".into());
let seats = sides.iter().map(|s| (*s, Seat { conn: None, name: String::new(), ready: false })).collect::<HashMap<_, _>>();
let room = Room { code: code.clone(), sides, seats, pending: HashMap::new(), turn: 0, started: false, paused: false, missing: false, seed: None, map_w, map_h, difficulty };
let room = Room { code: code.clone(), sides, seats, pending: HashMap::new(), turn: 0, started: false, paused: false, missing: false, seed: None, map_w, map_h, difficulty, mission };
self.rooms.insert(code.clone(), room);
if let Some(c) = self.clients.get_mut(&conn) {
c.room = Some(code.clone());
@@ -422,7 +436,7 @@ impl Netplay {
r.turn = 0;
}
let msg = match self.rooms.get(&code) {
Some(r) => json!({"t":"start","seed":r.seed.unwrap_or(0),"map_w":r.map_w,"map_h":r.map_h,"difficulty":r.difficulty,"sides":r.sides}),
Some(r) => json!({"t":"start","seed":r.seed.unwrap_or(0),"map_w":r.map_w,"map_h":r.map_h,"difficulty":r.difficulty,"sides":r.sides,"mission":r.mission}),
None => return,
};
self.room_msg(conn, &code);
@@ -648,7 +662,8 @@ impl Netplay {
"map_w":r.map_w,
"map_h":r.map_h,
"difficulty":r.difficulty,
"seed":r.seed
"seed":r.seed,
"mission":r.mission
}))
}
@@ -658,7 +673,7 @@ impl Netplay {
.values()
.map(|r| {
let players = r.seats.values().filter(|s| s.conn.is_some()).count();
json!({"code":r.code,"players":players,"sides":r.sides,"started":r.started,"paused":r.paused})
json!({"code":r.code,"players":players,"sides":r.sides,"started":r.started,"paused":r.paused,"mission":r.mission})
})
.collect();
self.send(conn, json!({"t":"rooms","rooms":rooms}));
@@ -720,6 +735,29 @@ mod tests {
assert_eq!(sum["sum"].as_u64(), Some(12345678901234567890u64));
}
#[test]
fn a_room_remembers_a_coop_campaign_mission() {
let mut net = Netplay::new();
let (tx, mut rx) = mpsc::unbounded_channel();
let red = net.connect(tx, Some("red".into()), None);
let rooms = net.handle(red, json!({"t":"create","sides":[0,2],"mission":2}));
let code = rooms[0].clone();
let room = until(&mut rx, "room");
assert_eq!(room["mission"].as_u64(), Some(2));
let (tx2, mut rx2) = mpsc::unbounded_channel();
let green = net.connect(tx2, Some("green".into()), None);
net.handle(green, json!({"t":"join","code":&code}));
net.handle(red, json!({"t":"ready"}));
net.handle(green, json!({"t":"ready"}));
net.handle(red, json!({"t":"start"}));
let start_red = until(&mut rx, "start");
let start_green = until(&mut rx2, "start");
assert_eq!(start_red["mission"], start_green["mission"]);
assert_eq!(start_red["mission"].as_u64(), Some(2));
}
#[test]
fn a_non_host_cannot_start_a_match() {
let mut net = Netplay::new();