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

+61 -1
View File
@@ -3,7 +3,7 @@
// network match run exactly the same path. Commands are applied at step boundaries on every client,
// in the order the server confirmed them.
use crate::game::{Game, TowerKind};
use crate::game::{Game, TowerKind, MARK_ATTACK, MARK_BRIDGE, MARK_CANCEL, MARK_DIG, MARK_DROP, MARK_FLAG, MARK_GUARD, MARK_HEAL, MARK_LIGHTNING, MARK_PICKUP, MARK_ROOM, MARK_SELL, MARK_SLAP, MARK_SPELL, MARK_STEEL, MARK_SUMMON, MARK_TOWER, MARK_TRAP};
use crate::spells::Spell;
use crate::traps::TrapKind;
use serde::{Deserialize, Serialize};
@@ -103,6 +103,7 @@ impl Game {
let seq = self.cmd_seq;
self.cmd_seq = self.cmd_seq.wrapping_add(1);
self.pending.push(Command { side: self.local_side, seq, act });
self.mark_command(act, self.local_side);
}
// one command of one master: that master's state is switched in, the action runs, the local
@@ -113,6 +114,7 @@ impl Game {
}
self.switch_to(c.side);
self.run_act(c.act);
self.mark_command(c.act, c.side);
let local = self.local_side;
self.switch_to(local);
}
@@ -127,6 +129,39 @@ impl Game {
}
}
fn mark_command(&mut self, act: Act, side: u8) {
let ((x, y), kind, dur) = match act {
Act::Dig(r) => (rect_center(r), MARK_DIG, 4.0),
Act::Room { r, .. } => (rect_center(r), MARK_ROOM, 5.0),
Act::Sell(r) => (rect_center(r), MARK_SELL, 4.0),
Act::Bridge(r) => (rect_center(r), MARK_BRIDGE, 4.0),
Act::Steel(r) => (rect_center(r), MARK_STEEL, 4.0),
Act::Tower { x, y, .. } => (point(x as f32, y as f32), MARK_TOWER, 6.0),
Act::Trap { x, y, .. } => (point(x as f32, y as f32), MARK_TRAP, 6.0),
Act::Cast { x, y, .. } => (point(x, y), MARK_SPELL, 5.0),
Act::Lightning { x, y } => (point(x, y), MARK_LIGHTNING, 5.0),
Act::Heal { x, y } => (point(x, y), MARK_HEAL, 5.0),
Act::Summon { x, y } => (point(x, y), MARK_SUMMON, 6.0),
Act::Flag { x, y } => (point(x as f32, y as f32), MARK_FLAG, 4.0),
Act::Pickup { ent } => match self.find(ent) {
Some(j) => (point(self.ents[j].x as f32, self.ents[j].y as f32), MARK_PICKUP, 3.5),
None => (point(self.map.core.0 as f32, self.map.core.1 as f32), MARK_PICKUP, 3.5),
},
Act::Drop { x, y } => (point(x, y), MARK_DROP, 3.5),
Act::Slap { ent } => match self.find(ent) {
Some(j) => (point(self.ents[j].x as f32, self.ents[j].y as f32), MARK_SLAP, 4.0),
None => (point(self.map.core.0 as f32, self.map.core.1 as f32), MARK_SLAP, 4.0),
},
Act::Attack { ent, x, y } => match ent.and_then(|id| self.find(id)) {
Some(j) => (point(self.ents[j].x as f32, self.ents[j].y as f32), MARK_ATTACK, 10.0),
None => (point(x, y), MARK_ATTACK, 10.0),
},
Act::Guard { x, y } => (point(x as f32, y as f32), MARK_GUARD, 10.0),
Act::CancelOrders => (point(self.map.core.0 as f32, self.map.core.1 as f32), MARK_CANCEL, 4.0),
};
self.add_mark(x, y, kind, side, dur);
}
fn run_act(&mut self, act: Act) {
match act {
Act::Dig(r) => {
@@ -165,6 +200,17 @@ impl Game {
}
}
fn rect_center(r: Rect) -> (f32, f32) {
(
(r.x0 as f32 + r.x1 as f32 + 1.0) * 0.5,
(r.y0 as f32 + r.y1 as f32 + 1.0) * 0.5,
)
}
fn point(x: f32, y: f32) -> (f32, f32) {
(x.floor() + 0.5, y.floor() + 0.5)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -184,11 +230,25 @@ mod tests {
let (x, y) = (cx + 6, cy); // outside the treasury ring: rock that can be marked
g.queue(Act::Dig(Rect::new(x, y, x + 1, y + 1)));
assert!(!g.map.t[idx(x, y)].marked, "queued, not applied yet");
assert!(g.marks.iter().any(|m| m.kind == MARK_DIG), "queued command has a marker");
g.update(STEP);
assert!(g.map.t[idx(x, y)].marked, "applied at the step boundary");
assert!(g.pending.is_empty());
}
#[test]
fn commands_leave_visible_markers() {
let mut g = game(12);
let (cx, cy) = g.map.core;
g.queue(Act::Dig(Rect::new(cx + 6, cy, cx + 7, cy + 1)));
g.queue(Act::Guard { x: cx, y: cy });
g.queue(Act::Attack { ent: None, x: (cx + 20) as f32, y: (cy + 20) as f32 });
g.update(STEP);
assert!(g.marks.iter().any(|m| m.kind == MARK_DIG && m.side == 0), "dig marker");
assert!(g.marks.iter().any(|m| m.kind == MARK_GUARD && m.side == 0), "guard marker");
assert!(g.marks.iter().any(|m| m.kind == MARK_ATTACK && m.side == 0), "attack marker");
}
#[test]
fn a_command_of_another_master_leaves_the_local_one_alone() {
let mut g = game(8);