From 1ec749218917904ccb2de99278b7d132a96ff7ba Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Wed, 4 Nov 2026 09:57:02 +0100 Subject: [PATCH] Multiplayer step B: every player order is a command (dig, build, cast, hand, orders) and the simulation runs in fixed 50 ms steps, so single player and a match take the same path --- docs/MULTIPLAYER.md | 27 +++- src/commands.rs | 275 ++++++++++++++++++++++++++++++++++ src/fog.rs | 2 +- src/game.rs | 354 ++++++++++++++++++++++++++++++++++++-------- src/main.rs | 62 ++++---- src/orders.rs | 4 +- src/rooms.rs | 6 +- src/traps.rs | 6 +- 8 files changed, 635 insertions(+), 101 deletions(-) create mode 100644 src/commands.rs diff --git a/docs/MULTIPLAYER.md b/docs/MULTIPLAYER.md index 4263ebe..6c6a645 100644 --- a/docs/MULTIPLAYER.md +++ b/docs/MULTIPLAYER.md @@ -29,13 +29,34 @@ The server does not simulate; it relays: Only the commands travel over the network, so bandwidth is tiny and no game state has to be serialised per turn. Fog of war, particles and the camera are local and not part of the shared state. +## What is there already + +- **Per-master state** (`game.rs`): every master has his own gold, mana, hand, research, payday, flag, + core... `Game::switch_to(side)` puts one master in the active fields, the simulation walks through all + masters step by step. A single player game has no stored masters and is unaffected. +- **Commands** (`commands.rs`): every order a player gives is an `Act` (`Dig`, `Room`, `Tower`, `Trap`, + `Cast`, `Flag`, `Pickup`, `Drop`, `Slap`, `Attack`, `Guard`, `CancelOrders`, ...) wrapped in a + `Command { side, seq, act }`. Creature targets are *ids*, so they mean the same on every client. + The tools in `main.rs` only queue commands now - nothing touches the world directly. +- **Steps**: the simulation runs in fixed steps of `STEP` = 50 ms (`Game::update` feeds real time into + them, `Game::apply_turn(cmds, steps)` is the lockstep entry point). Commands are applied at step + boundaries, so single player and a match run exactly the same path. Particles, effects and the camera + keep running in real time: they are local and not part of the shared state. +- **Who speaks**: messages, hints and interface sounds belong to the master who is being processed + (`Game::speaking`); what another master orders never shows up on my screen. +- Savegames, tests and the standalone file are unchanged: `cargo test --release` covers the command path + (including "the same actions on both paths build the same world"). + ## Steps | # | Step | Status | |---|---|---| -| A | Simulation per master: gold, mana, research, rooms, hand, flag, traps, payday... per side; up to four masters (red, green, blue, yellow) | planned | -| B | All player actions as commands (`Command` + `Game::apply`), single player uses the same path; fixed time step | planned | -| C | Server: lobby and match relay over WebSocket (axum), turns, pause on disconnect, AI takeover on leave, chat | planned | +| A | Simulation per master: gold, mana, research, rooms, hand, flag, traps, payday... per side; up to four masters (red, green, blue, yellow) | built | +| B | All player actions as commands (`Command` + `Game::apply`), single player uses the same path; fixed time step | built | +| C | Server: lobby and match relay over WebSocket (axum), turns, pause on disconnect, AI takeover on leave, chat | next | | D | Client: *Multiplayer* menu (lobby, create, join, ready, teams), WebSocket bridge in `dc_plugin.js`, chat, gold sending | planned | | E | Alliances and coop rules, AI masters for free and abandoned slots | planned | | F | Coop campaign: 20 missions with story (EN/DE) | planned | + +Still open on the way to C: player owners (today side 1 is the humans' and 2/3 the rivals' - four +player sides need their own `Owner`s), the turn/turn-boundary client loop, and the state checksum. diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..6c23cb4 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,275 @@ +// Multiplayer step B: everything a master orders becomes a Command. The simulation only changes +// through Game::apply_turn (commands, then a fixed number of steps), so a single player game and a +// 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::spells::Spell; +use crate::traps::TrapKind; +use serde::{Deserialize, Serialize}; + +// a rectangle of tiles, as dragged with the dig, room, sell, bridge and steel tools +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Rect { + pub x0: i32, + pub y0: i32, + pub x1: i32, + pub y1: i32, +} + +impl Rect { + pub fn new(x0: i32, y0: i32, x1: i32, y1: i32) -> Rect { + Rect { x0, y0, x1, y1 } + } +} + +// one ordered action of a master; entity targets are ids, so they mean the same on every client +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub enum Act { + Dig(Rect), + Room { + room: u8, + r: Rect, + }, + Sell(Rect), + Bridge(Rect), + Steel(Rect), + Tower { + kind: TowerKind, + x: i32, + y: i32, + }, + Trap { + kind: TrapKind, + x: i32, + y: i32, + }, + Cast { + spell: Spell, + x: f32, + y: f32, + }, + Lightning { + x: f32, + y: f32, + }, + Heal { + x: f32, + y: f32, + }, + Summon { + x: f32, + y: f32, + }, + Flag { + x: i32, + y: i32, + }, + Pickup { + ent: u32, + }, + Drop { + x: f32, + y: f32, + }, + Slap { + ent: u32, + }, + Attack { + ent: Option, + x: f32, + y: f32, + }, + Guard { + x: i32, + y: i32, + }, + CancelOrders, +} + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub struct Command { + pub side: u8, // the master who gave the order + pub seq: u32, // order within that master's commands (the server numbers them) + pub act: Act, +} + +impl Game { + // the local master orders something: queued until the next step boundary + pub fn queue(&mut self, act: Act) { + if self.state != 0 || self.paused || self.demo.is_some() || !self.is_master(self.local_side) { + return; + } + let seq = self.cmd_seq; + self.cmd_seq = self.cmd_seq.wrapping_add(1); + self.pending.push(Command { side: self.local_side, seq, act }); + } + + // one command of one master: that master's state is switched in, the action runs, the local + // master's state is switched back. Messages, hints and sounds only come from the local master. + pub fn apply(&mut self, c: &Command) { + if !self.is_master(c.side) { + return; + } + self.switch_to(c.side); + self.run_act(c.act); + let local = self.local_side; + self.switch_to(local); + } + + // a turn: the commands of all masters, then a fixed number of simulation steps + pub fn apply_turn(&mut self, cmds: &[Command], steps: usize) { + for c in cmds { + self.apply(c); + } + for _ in 0..steps { + self.step_fixed(); + } + } + + fn run_act(&mut self, act: Act) { + match act { + Act::Dig(r) => { + let on = !self.map.get(r.x0, r.y0).map_or(true, |t| t.marked); + self.mark_rect(r.x0, r.y0, r.x1, r.y1, on); + } + Act::Room { room, r } => self.build_rect(room as usize, r.x0, r.y0, r.x1, r.y1), + Act::Sell(r) => self.sell_rect(r.x0, r.y0, r.x1, r.y1), + Act::Bridge(r) => self.build_bridges(r.x0, r.y0, r.x1, r.y1), + Act::Steel(r) => self.build_steel(r.x0, r.y0, r.x1, r.y1), + Act::Tower { kind, x, y } => self.build_tower(kind, x, y), + Act::Trap { kind, x, y } => self.place_trap(kind, x, y), + Act::Cast { spell, x, y } => self.cast(spell, x, y), + Act::Lightning { x, y } => self.lightning(x, y), + Act::Heal { x, y } => self.heal(x, y), + Act::Summon { x, y } => self.summon(x, y), + Act::Flag { x, y } => self.set_flag(x, y), + Act::Pickup { ent } => { + let j = self.find(ent); + self.pickup(j); + } + Act::Drop { x, y } => { + self.drop(x, y); + } + Act::Slap { ent } => { + let j = self.find(ent); + self.slap(j); + } + Act::Attack { ent, x, y } => { + let j = ent.and_then(|id| self.find(id)); + self.order_attack(j, x, y); + } + Act::Guard { x, y } => self.order_guard(x, y), + Act::CancelOrders => self.cancel_orders(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::game::{EK, Job, STEP}; + use crate::map::idx; + + fn game(seed: u32) -> Game { + let mut g = Game::new(seed, 64, 64); + g.init_fog(); + g + } + + #[test] + fn a_queued_command_is_applied_at_the_next_step() { + let mut g = game(7); + let (cx, cy) = g.map.core; + 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"); + g.update(STEP); + assert!(g.map.t[idx(x, y)].marked, "applied at the step boundary"); + assert!(g.pending.is_empty()); + } + + #[test] + fn a_command_of_another_master_leaves_the_local_one_alone() { + let mut g = game(8); + let (cx, cy) = g.map.core; + g.add_master(2, (cx + 20, cy + 20)); + let (gold, hand) = (g.gold, g.hand.len()); + let (msgs, events) = (g.msgs.len(), g.events.len()); + // an order far away, given by another master, reaches his state only + g.apply(&Command { side: 2, seq: 0, act: Act::Attack { ent: None, x: (cx + 21) as f32, y: (cy + 21) as f32 } }); + assert_eq!(g.gold, gold, "his gold, not mine"); + assert_eq!(g.hand.len(), hand); + assert_eq!(g.msgs.len(), msgs, "he hears his own order, not I"); + assert_eq!(g.events.len(), events, "and it makes no sound here"); + assert_eq!(g.me_side, g.local_side, "I am back in the saddle"); + assert_eq!(g.map.core, (cx, cy), "my heart is still mine"); + // while it was in the saddle, that master had his own heart and his own ground + g.switch_to(2); + assert_eq!(g.map.core, (cx + 20, cy + 20)); + assert_eq!(g.me, crate::map::Owner::of_side(2)); + g.switch_to(0); + // the same order from the local master is heard here + g.apply(&Command { side: 0, seq: 1, act: Act::Attack { ent: None, x: (cx + 21) as f32, y: (cy + 21) as f32 } }); + assert!(g.msgs.len() > msgs, "my own orders speak to me"); + assert!(g.master_sides().contains(&2)); + } + + #[test] + fn unknown_sides_cannot_order_anything() { + let mut g = game(9); + let (cx, cy) = g.map.core; + let (x, y) = (cx + 6, cy); + g.apply(&Command { side: 3, seq: 0, act: Act::Dig(Rect::new(x, y, x, y)) }); + g.update(STEP); + assert!(!g.map.t[idx(x, y)].marked); + } + + #[test] + fn the_same_actions_on_both_paths_build_the_same_world() { + let dig = Rect::new(20, 20, 22, 22); + let room = Act::Room { room: 0, r: Rect::new(30, 30, 31, 31) }; + let drop_at = (21.4f32, 22.6f32); + + // one game driven like a single player session (queue + update) + let mut a = game(11); + let ogre = a.spawn(EK::Ogre, false, 21.5, 21.5, 3); + a.queue(Act::Dig(dig)); + a.queue(room); + a.queue(Act::Pickup { ent: ogre }); + a.queue(Act::Drop { x: drop_at.0, y: drop_at.1 }); + for _ in 0..20 { + a.update(0.05); + } + + // the same game driven like a match (one turn with all commands, then the same steps) + let mut b = game(11); + assert_eq!(b.spawn(EK::Ogre, false, 21.5, 21.5, 3), ogre); + let turn = vec![ + Command { side: 0, seq: 0, act: Act::Dig(dig) }, + Command { side: 0, seq: 1, act: room }, + Command { side: 0, seq: 2, act: Act::Pickup { ent: ogre } }, + Command { side: 0, seq: 3, act: Act::Drop { x: drop_at.0, y: drop_at.1 } }, + ]; + b.apply_turn(&turn, 20); + + assert_eq!(a.time, b.time, "the same number of fixed steps"); + assert_eq!(a.sim_steps, b.sim_steps); + assert_eq!(a.gold, b.gold); + assert_eq!(a.hand, b.hand); + assert!(a.map.t.iter().zip(b.map.t.iter()).all(|(x, y)| x.kind == y.kind && x.owner == y.owner && x.marked == y.marked && x.hp == y.hp && x.gold == y.gold && x.reserved == y.reserved), "the maps diverged"); + assert_eq!(fingerprint(&a), fingerprint(&b), "the creatures diverged"); + } + + // what the simulation has to agree on for two clients of one match + fn fingerprint(g: &Game) -> Vec<(u32, EK, bool, u8, f32, f32, f32, Job)> { + g.ents.iter().map(|e| (e.id, e.kind, e.alive, e.side, e.x, e.y, e.hp, e.job)).collect() + } + + #[test] + fn commands_survive_a_savegame_round_trip() { + let c = Command { side: 1, seq: 4, act: Act::Attack { ent: Some(9), x: 3.5, y: 4.5 } }; + let json = serde_json::to_string(&c).unwrap(); + assert_eq!(serde_json::from_str::(&json).unwrap(), c); + } +} diff --git a/src/fog.rs b/src/fog.rs index 622787d..efea7b7 100644 --- a/src/fog.rs +++ b/src/fog.rs @@ -80,7 +80,7 @@ impl Game { let (w, h) = (mw() as i32, mh() as i32); // own ground and everything around it for i in 0..n { - if self.map.t[i].owner == Owner::Player { + if self.map.t[i].owner == self.me { let (x, y) = xy(i); for dy in -1..=1 { for dx in -1..=1 { diff --git a/src/game.rs b/src/game.rs index 76775a9..33bbbea 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,5 +1,6 @@ // Game logic: units, AI, economy, hero waves. +use crate::commands::Command; use crate::map::*; use serde::{Deserialize, Serialize}; @@ -359,7 +360,7 @@ impl Ent { } // the player's creatures (not imps, heroes or the rivals' units) pub fn creature(&self) -> bool { - !self.hero && self.side == 0 && self.kind != EK::Worker && !self.chained + !self.hero && self.side == me_side() && self.kind != EK::Worker && !self.chained } // 0 player, 1 humans, 2 and 3 rival masters pub fn side(&self) -> u8 { @@ -426,6 +427,50 @@ pub struct Fx { pub style: u8, // projectiles: arrow, cannonball, fireball, ... (see particles.rs) } +thread_local! { + // the master whose turn it is in the simulation (see Game::switch_to) + static ME_SIDE: std::cell::Cell = std::cell::Cell::new(0); +} + +pub fn me_side() -> u8 { + ME_SIDE.with(|s| s.get()) +} + +fn player_owner() -> Owner { + Owner::Player +} + +// The state of a dungeon master that is not being processed right now. The Game's own fields +// (gold, mana, hand, ...) always belong to the master whose side is `me_side`; switching swaps +// them with the stored ones. A single player game has no stored masters at all. +#[derive(Clone, Debug, Default)] +pub struct MasterState { + pub side: u8, + pub owner: Option, + pub core: (i32, i32), + pub core_hp: f32, + pub gold: f32, + pub mana: f32, + pub payday: f32, + pub payday_warned: bool, + pub hand: Vec, + pub flag: Option, + pub reserve: f32, + pub incoming: Vec<(u8, f32, Option)>, + pub training_bill: f32, + pub counts: [usize; N_ROOMS], + pub springs: usize, + pub known: Vec, + pub researchable: Vec, + pub research: f32, + pub forge_work: f32, + pub rune_free: u32, + pub rune_final: u32, + pub rune_next: u32, + pub portal_timer: f32, + pub punch_ready: f32, +} + #[derive(Serialize, Deserialize)] pub struct Game { pub map: Map, @@ -532,6 +577,22 @@ pub struct Game { pub rune_next: u32, // sold rune forge towers: back as a chest in the next mission #[serde(default)] pub freed: u32, // chained creatures freed // tiles next to fighting heroes: knight ground there cannot be claimed + #[serde(skip, default = "player_owner")] + pub me: Owner, // owner of the ground of the master being processed + #[serde(skip)] + pub me_side: u8, // that master's side (0 = the first player) + #[serde(skip)] + pub local_side: u8, // the master who plays at this computer + #[serde(skip)] + pub masters: Vec, // the other masters (multiplayer) + #[serde(skip)] + pub pending: Vec, // orders of this master, applied at the next step boundary + #[serde(skip)] + pub cmd_seq: u32, // number of the next command queued here + #[serde(skip)] + pub sim_steps: u64, // fixed simulation steps done so far (the lockstep clock) + #[serde(skip)] + step_acc: f32, // real time not yet paid for with a step #[serde(skip)] pub hints: bool, // show hint messages (setting) #[serde(skip, default = "hint_times")] @@ -542,6 +603,12 @@ fn hint_times() -> [f32; 32] { [-1000.0; 32] } +// every simulation step has the same length: on every client of a match the same commands are +// applied and then the same number of steps runs, so the worlds stay identical +pub const STEP: f32 = 0.05; +// one frame never runs away into the future (a lagging tab catches up at most this much) +const MAX_STEPS_PER_FRAME: usize = 4; + // torch on a wall face: wall tile + direction (index into DIR4) towards the open side #[derive(Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Torch { @@ -699,6 +766,14 @@ impl Game { entry_history: Vec::new(), planned_entries: Vec::new(), hints: true, + me: Owner::Player, + me_side: 0, + local_side: 0, + masters: Vec::new(), + pending: Vec::new(), + cmd_seq: 0, + sim_steps: 0, + step_acc: 0.0, hint_t: hint_times(), reserve: 0.0, incoming: Vec::new(), @@ -758,7 +833,7 @@ impl Game { let map = &self.map; self.torches.retain(|t| map.t[t.wall].solid()); for i in 0..n() { - if self.map.t[i].owner == Owner::Player { + if self.map.t[i].owner == self.me { self.place_torches(i); } } @@ -800,7 +875,16 @@ impl Game { self.mission.as_ref().map_or(self.gate_on, |m| m.heart) } + // messages, hints and sounds are meant for the master who plays at this computer: while + // another master is being simulated (his economy, his orders) nothing of that reaches the screen + fn speaking(&self) -> bool { + self.me_side == self.local_side + } + pub fn msg(&mut self, key: &'static str, args: Vec) { + if !self.speaking() { + return; // a foreign master ordered something: only he hears about it + } self.msgs.push(Msg { key, args }); if self.msgs.len() > 50 { self.msgs.remove(0); @@ -808,12 +892,18 @@ impl Game { } pub fn sfx(&mut self, sfx: Sfx, x: f32, y: f32) { + if !self.speaking() { + return; + } if self.events.len() < 64 { self.events.push(SfxEvent { sfx, pos: Some((x, y)) }); } } pub fn sfx_ui(&mut self, sfx: Sfx) { + if !self.speaking() { + return; + } if self.events.len() < 64 { self.events.push(SfxEvent { sfx, pos: None }); } @@ -862,6 +952,9 @@ impl Game { } pub(crate) fn hint(&mut self, slot: usize, key: &'static str) { + if !self.speaking() { + return; // another master ordered something: he keeps his hints for himself + } if self.hints && self.time - self.hint_t[slot] > 30.0 { self.hint_t[slot] = self.time; self.msg(key, vec![]); @@ -870,9 +963,9 @@ impl Game { pub fn recount(&mut self) { let mut c = [0usize; N_ROOMS]; - self.springs = self.map.t.iter().filter(|t| t.kind == Kind::ManaSpring && t.owner == Owner::Player).count(); + self.springs = self.map.t.iter().filter(|t| t.kind == Kind::ManaSpring && t.owner == self.me).count(); for t in &self.map.t { - if t.owner == Owner::Player { + if t.owner == self.me { if let Some(r) = room_index(t.kind) { c[r] += 1; } @@ -923,7 +1016,7 @@ impl Game { let mut stored = 0.0; for i in 0..self.map.t.len() { let t = &mut self.map.t[i]; - if t.kind == Kind::Treasury && t.owner == Owner::Player { + if t.kind == Kind::Treasury && t.owner == self.me { tiles.push(i); stored += t.gold as f32; } else if t.gold != 0 && t.vein().is_none() && t.kind != Kind::Treasury { @@ -1010,7 +1103,7 @@ impl Game { #[allow(dead_code)] pub fn portal_owned(&self) -> bool { let Some(&(px, py)) = self.map.portals.first() else { return false }; - self.map.t[idx(px, py)].owner == Owner::Player + self.map.t[idx(px, py)].owner == self.me } pub fn creature_count(&self) -> usize { @@ -1083,11 +1176,15 @@ impl Game { // ---------------------------------------------------------------- update pub fn update(&mut self, dt: f32) { - let dt = dt.min(0.1); - if self.state == 0 && !self.paused { - self.step(dt); + self.step_acc = (self.step_acc + dt).min(STEP * MAX_STEPS_PER_FRAME as f32); + let mut steps = 0usize; + while self.step_acc >= STEP { + self.step_acc -= STEP; + steps += 1; } - self.sync_treasury(); + let cmds = std::mem::take(&mut self.pending); + self.apply_turn(&cmds, steps); + // what only the camera cares about runs in real time: it is not part of the shared state self.update_particles(dt); for f in self.fx.iter_mut() { f.t += dt; @@ -1095,11 +1192,134 @@ impl Game { self.fx.retain(|f| f.t < f.max); } + // one fixed step: the world of every master, then the shared bookkeeping + pub(crate) fn step_fixed(&mut self) { + self.sim_steps += 1; + if self.state == 0 && !self.paused { + self.step(STEP); + } + for side in self.master_sides() { + self.switch_to(side); + self.sync_treasury(); + } + let local = self.local_side; + self.switch_to(local); + } + + // ---------------------------------------------------------------- masters + + pub fn master_sides(&self) -> Vec { + let mut v: Vec = std::iter::once(self.me_side).chain(self.masters.iter().map(|m| m.side)).collect(); + v.sort_unstable(); + v + } + + pub fn is_master(&self, side: u8) -> bool { + side == self.me_side || self.masters.iter().any(|m| m.side == side) + } + + fn take_active(&mut self) -> MasterState { + MasterState { + side: self.me_side, + owner: Some(self.me), + core: self.map.core, + core_hp: self.core_hp, + gold: self.gold, + mana: self.mana, + payday: self.payday, + payday_warned: self.payday_warned, + hand: std::mem::take(&mut self.hand), + flag: self.flag, + reserve: self.reserve, + incoming: std::mem::take(&mut self.incoming), + training_bill: self.training_bill, + counts: self.counts, + springs: self.springs, + known: std::mem::take(&mut self.known), + researchable: std::mem::take(&mut self.researchable), + research: self.research, + forge_work: self.forge_work, + rune_free: self.rune_free, + rune_final: self.rune_final, + rune_next: self.rune_next, + portal_timer: self.portal_timer, + punch_ready: self.punch_ready, + } + } + + fn put_active(&mut self, m: MasterState) { + self.me_side = m.side; + self.me = m.owner.unwrap_or(Owner::of_side(m.side)); + ME_SIDE.with(|s| s.set(m.side)); + self.map.core = m.core; + self.core_hp = m.core_hp; + self.gold = m.gold; + self.mana = m.mana; + self.payday = m.payday; + self.payday_warned = m.payday_warned; + self.hand = m.hand; + self.flag = m.flag; + self.reserve = m.reserve; + self.incoming = m.incoming; + self.training_bill = m.training_bill; + self.counts = m.counts; + self.springs = m.springs; + self.known = m.known; + self.researchable = m.researchable; + self.research = m.research; + self.forge_work = m.forge_work; + self.rune_free = m.rune_free; + self.rune_final = m.rune_final; + self.rune_next = m.rune_next; + self.portal_timer = m.portal_timer; + self.punch_ready = m.punch_ready; + } + + // make `side` the master being processed (no-op for sides that are no masters) + pub fn switch_to(&mut self, side: u8) { + if side == self.me_side { + return; + } + let Some(k) = self.masters.iter().position(|m| m.side == side) else { return }; + let target = self.masters.swap_remove(k); + let cur = self.take_active(); + self.masters.push(cur); + self.put_active(target); + } + + // a new master on `side` with its core at (cx, cy) (top-left of the 3x3 core). Used by the + // multiplayer lobby (and its tests); single player games have no stored masters. + #[allow(dead_code)] + pub fn add_master(&mut self, side: u8, core: (i32, i32)) { + let mut m = self.take_active(); + let me = std::mem::replace(&mut m, MasterState::default()); + // the copy keeps the economy settings; the new one starts fresh + let fresh = MasterState { + side, + owner: Some(Owner::of_side(side)), + core, + core_hp: CORE_HP, + gold: 1500.0, + mana: 100.0, + payday: PAYDAY, + known: me.known.clone(), + researchable: me.researchable.clone(), + portal_timer: 10.0, + ..MasterState::default() + }; + self.put_active(me); + self.masters.push(fresh); + } + pub(crate) fn update_units(&mut self, dt: f32) { for i in 0..self.ents.len() { if !self.ents[i].alive || self.ents[i].held { continue; } + let side = self.ents[i].side; + if !self.ents[i].hero && side != self.me_side && !self.masters.is_empty() { + self.switch_to(side); + } let mut e = std::mem::replace(&mut self.ents[i], Ent::dummy()); let (x0, y0) = (e.x, e.y); self.update_ent(&mut e, dt); @@ -1121,14 +1341,23 @@ impl Game { return; } self.time += dt; - self.recount(); - self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * dt).min(self.mana_max()); - self.core_hp = (self.core_hp + 0.5 * dt).min(CORE_HP); for t in self.map.t.iter_mut() { if t.kind == Kind::Farm { t.food = (t.food + dt / 18.0).min(3.0); } } + for side in self.master_sides() { + self.switch_to(side); + self.economy(dt); + } + self.after_economy(dt); + } + + // a master's household: mana, core, portals, payday + fn economy(&mut self, dt: f32) { + self.recount(); + self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * dt).min(self.mana_max()); + self.core_hp = (self.core_hp + 0.5 * dt).min(CORE_HP); let owned = self.portals_owned(); if owned > 0 { @@ -1156,7 +1385,18 @@ impl Game { self.payday_warned = false; self.do_payday(); } + // every two seconds: corners between reinforced walls are closed + if (self.time / 2.0).floor() != ((self.time - dt) / 2.0).floor() { + self.reinforce_corners(); + } + self.update_forge(dt); + self.update_research(); + } + fn after_economy(&mut self, dt: f32) { + // from here on it is about the local master and the humans: that side is switched in first + let local = self.local_side; + self.switch_to(local); if self.waves_on() { self.wave_tick(dt); } @@ -1164,23 +1404,18 @@ impl Game { self.gate_hp = (self.gate_hp + 8.0 * dt).min(GATE_HP); } - // every two seconds: corners between reinforced walls are closed - if (self.time / 2.0).floor() != ((self.time - dt) / 2.0).floor() { - self.reinforce_corners(); - } self.update_contested(); self.update_units(dt); + self.switch_to(local); // update_units worked master by master self.update_towers(dt); - self.update_forge(dt); - self.update_research(); self.update_reveals(dt); self.update_graves(dt); - self.update_fog(dt); if !self.rivals.is_empty() { self.update_rivals(dt); } self.update_dwarves(dt); self.check_end(); + self.update_fog(dt); } fn check_end(&mut self) { @@ -1313,7 +1548,7 @@ impl Game { let (px, py) = self.map.portals[0]; for y in 0..3 { for x in 0..3 { - self.map.t[idx(px + x, py + y)].owner = Owner::Player; + self.map.t[idx(px + x, py + y)].owner = self.me; } } self.try_portal(); @@ -1455,7 +1690,7 @@ impl Game { let cur = e.tile(); let goal = |i: usize| { let t = &self.map.t[i]; - (t.kind == Kind::Treasury && t.owner == Owner::Player && t.gold > 0) || (self.counts[0] == 0 && self.map.adj_core(i)) + (t.kind == Kind::Treasury && t.owner == self.me && t.gold > 0) || (self.counts[0] == 0 && self.map.adj_core(i)) }; let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), goal, true) } else { self.map.search(cur, self.map.walk_cost(), goal, true) }; match found { @@ -1760,7 +1995,7 @@ impl Game { // waits in chains until the player's imps claim its cave e.moving = false; let (x, y) = xy(e.tile()); - let reached = std::iter::once((0, 0)).chain(DIR8.iter().copied()).any(|(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |t| t.owner == Owner::Player)); + let reached = std::iter::once((0, 0)).chain(DIR8.iter().copied()).any(|(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |t| t.owner == self.me)); if reached { e.chained = false; e.settled = false; @@ -2314,7 +2549,7 @@ impl Game { if t.kind != Kind::Earth || t.owner == Owner::Hero { continue; } - let own = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == Owner::Player && n.walkable())); + let own = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == self.me && n.walkable())); if !own { continue; } @@ -2354,7 +2589,7 @@ impl Game { continue; } any_river = true; - let own = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == Owner::Player && n.walkable())); + let own = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == self.me && n.walkable())); if own { add.push(idx(x, y)); } @@ -2374,7 +2609,7 @@ impl Game { self.gold -= cost; let t = &mut self.map.t[i]; t.kind = if lava { Kind::LavaBridge } else { Kind::Bridge }; - t.owner = Owner::Player; + t.owner = self.me; built += 1; progress = true; let (x, y) = xy(i); @@ -2401,7 +2636,7 @@ impl Game { pub fn reinforce_corners(&mut self) { let (w, h) = (mw() as i32, mh() as i32); let wall = |m: &Map, x: i32, y: i32| m.get(x, y).map_or(false, |n| n.kind == Kind::Earth && n.reinforced && n.owner != Owner::Hero); - let floor = |m: &Map, x: i32, y: i32| m.get(x, y).map_or(false, |n| n.owner == Owner::Player && n.walkable()); + let floor = |m: &Map, x: i32, y: i32| m.get(x, y).map_or(false, |n| n.owner == self.me && n.walkable()); let mut add = Vec::new(); for y in 1..h - 1 { for x in 1..w - 1 { @@ -2514,7 +2749,7 @@ impl Game { continue; } if let Some(t) = self.map.get(cx + dx, cy + dy) { - if t.bridge() && t.owner == Owner::Player { + if t.bridge() && t.owner == self.me { return Some(idx(cx + dx, cy + dy)); } } @@ -2527,7 +2762,7 @@ impl Game { // a knight walks next to the bridge tile and tears it down fn tear_bridge(&mut self, e: &mut Ent, i: usize, dt: f32) -> bool { let t = self.map.t[i]; - if !t.bridge() || t.owner != Owner::Player { + if !t.bridge() || t.owner != self.me { return false; } let (bx, by) = xy(i); @@ -2596,7 +2831,7 @@ impl Game { // an earth wall next to own floor that can be reinforced from tile i fn reinforce_neighbor(&self, i: usize, me: u32) -> Option { - if self.map.t[i].owner != Owner::Player { + if self.map.t[i].owner != self.me { return None; } let (x, y) = xy(i); @@ -2612,14 +2847,13 @@ impl Game { fn claimable(&self, i: usize, me: u32) -> bool { let t = &self.map.t[i]; - let ok = match t.owner { - Owner::None => matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Treasury | Kind::ManaSpring | Kind::RuneChest), - // knight ground (also vaults and shard chambers), but not while heroes fight there - // knight and rival ground (also vaults and shard chambers), but not while enemies fight there - Owner::Hero | Owner::Rival1 | Owner::Rival2 => { - matches!(t.kind, Kind::Floor | Kind::Treasury | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Lair | Kind::Training) && !self.contested.get(i).copied().unwrap_or(false) - } - Owner::Player => false, + let ok = if t.owner == self.me { + false + } else if t.owner == Owner::None { + matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Treasury | Kind::ManaSpring | Kind::RuneChest) + } else { + // knight and other masters' ground (also vaults and shard chambers), but not while enemies fight there + matches!(t.kind, Kind::Floor | Kind::Treasury | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Lair | Kind::Training) && !self.contested.get(i).copied().unwrap_or(false) }; if !ok { return false; @@ -2628,12 +2862,12 @@ impl Game { return false; } let (x, y) = xy(i); - DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == Owner::Player)) + DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == self.me)) } fn is_deposit(&self, i: usize) -> bool { let t = &self.map.t[i]; - (t.kind == Kind::Treasury && t.owner == Owner::Player) || (self.counts[0] == 0 && self.map.adj_core(i)) + (t.kind == Kind::Treasury && t.owner == self.me) || (self.counts[0] == 0 && self.map.adj_core(i)) } fn start_deliver(&mut self, e: &mut Ent) -> bool { @@ -2683,7 +2917,7 @@ impl Game { for _ in 0..6 { let nx = x + self.rng.range(-radius, radius); let ny = y + self.rng.range(-radius, radius); - if self.map.walk(nx, ny) && self.map.t[idx(nx, ny)].owner == Owner::Player { + if self.map.walk(nx, ny) && self.map.t[idx(nx, ny)].owner == self.me { if let Some(p) = self.map.path_to(e.tile(), idx(nx, ny)) { if p.len() as i32 <= radius * 2 + 2 { e.path = p; @@ -2840,13 +3074,13 @@ impl Game { let (px, py) = self.map.portals[pk]; for y in 0..3 { for x in 0..3 { - self.map.t[idx(px + x, py + y)].owner = Owner::Player; + self.map.t[idx(px + x, py + y)].owner = self.me; } } self.msg("msg.portal_claimed", vec![]); self.fx(7, px as f32 + 1.5, py as f32 + 1.5, 0.0, 0.0, 1.0, 0xffff66cc); } else { - self.map.t[t].owner = Owner::Player; + self.map.t[t].owner = self.me; } self.place_torches(t); let (x, y) = xy(t); @@ -2982,7 +3216,7 @@ impl Game { self.map.walk_cost(), |i| { let t = &self.map.t[i]; - t.kind == kind && t.owner == Owner::Player && (t.reserved == 0 || t.reserved == me) && (!need_food || t.food >= 1.0) + t.kind == kind && t.owner == self.me && (t.reserved == 0 || t.reserved == me) && (!need_food || t.food >= 1.0) }, true, ) @@ -3213,7 +3447,7 @@ impl Game { Job::Guard(t) => { // a guard post, or a guard order in any room e.timer -= dt; - if cur != t || !self.map.t[t].walkable() || self.map.t[t].owner != Owner::Player || e.timer <= 0.0 { + if cur != t || !self.map.t[t].walkable() || self.map.t[t].owner != self.me || e.timer <= 0.0 { self.unreserve(me); e.job = Job::None; } @@ -3979,11 +4213,11 @@ impl Game { let mut cands = Vec::new(); for i in 0..self.map.t.len() { let t = &self.map.t[i]; - if t.owner != Owner::Player || !t.walkable() { + if t.owner != self.me || !t.walkable() { continue; } let (x, y) = xy(i); - let border = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner != Owner::Player)); + let border = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner != self.me)); if !border { continue; } @@ -4061,7 +4295,7 @@ impl Game { } let i = idx(x, y); let t = self.map.t[i]; - if t.kind == Kind::Floor && t.owner == Owner::Player { + if t.kind == Kind::Floor && t.owner == self.me { if self.gold < cost { broke = true; continue; @@ -4124,7 +4358,7 @@ impl Game { refund += STEEL_COST[1] as f32 * 0.5; continue; } - if t.owner == Owner::Player && t.bridge() { + if t.owner == self.me && t.bridge() { let lava = t.kind == Kind::LavaBridge; t.kind = if lava { Kind::Lava } else { Kind::Water }; t.owner = Owner::None; @@ -4134,7 +4368,7 @@ impl Game { continue; } let t = &mut self.map.t[i]; - if t.owner == Owner::Player { + if t.owner == self.me { if let Some(r) = room_index(t.kind) { t.kind = Kind::Floor; t.bed = 0; @@ -4197,7 +4431,7 @@ impl Game { return; } let t = self.map.t[i]; - if t.kind != Kind::Floor || t.owner != Owner::Player { + if t.kind != Kind::Floor || t.owner != self.me { self.hint(16, "hint.tower_where"); self.sfx_ui(Sfx::Error); return; @@ -4626,7 +4860,7 @@ impl Game { } fn on_own_ground(&self, x: f32, y: f32) -> bool { - self.map.get(x as i32, y as i32).map_or(false, |t| t.owner == Owner::Player) + self.map.get(x as i32, y as i32).map_or(false, |t| t.owner == self.me) } // Left click with the hand: own units anywhere, unconscious heroes only on own ground @@ -4685,7 +4919,7 @@ impl Game { } let i = idx(x, y); let t = self.map.t[i]; - if !t.walkable() || t.owner != Owner::Player { + if !t.walkable() || t.owner != self.me { self.hint(9, "hint.drop_where"); self.sfx_ui(Sfx::Error); return false; @@ -4829,7 +5063,7 @@ impl Game { pub fn summon(&mut self, wx: f32, wy: f32) { let cost = self.summon_cost(); let (x, y) = (wx as i32, wy as i32); - if !self.map.walk(x, y) || self.map.t[idx(x, y)].owner != Owner::Player { + if !self.map.walk(x, y) || self.map.t[idx(x, y)].owner != self.me { self.hint(13, "hint.summon_where"); return; } @@ -5137,23 +5371,23 @@ mod tests { #[test] fn treasury_shows_its_treasures() { let mut g = Game::new(3, 64, 64); - g.update(0.01); + g.update(STEP); // the starting gold lies in the treasury ring assert!((treasury_total(&g) - g.gold).abs() < 2.0); assert_eq!(g.reserve, 0.0); // rubies fill their own tiles g.add_res(1200.0, 2); - g.update(0.01); + g.update(STEP); let rubies: i32 = g.map.t.iter().filter(|t| t.kind == Kind::Treasury && t.owner == Owner::Player && t.res == 2).map(|t| t.gold).sum(); assert_eq!(rubies, 1200); assert!(g.map.t.iter().all(|t| t.gold <= TREASURY_TILE as i32 || t.vein().is_some())); // spending empties tiles (randomly), the total always matches the gold counter g.gold -= 1700.0; - g.update(0.01); + g.update(STEP); assert!((treasury_total(&g) - g.gold).abs() < 2.0); // more than the treasury holds: the core keeps the rest g.gold = g.gold_cap(); - g.update(0.01); + g.update(STEP); assert!((treasury_total(&g) - g.gold).abs() < 2.0); // (tiles partly filled with rubies take no gold, so the core may hold a little more) let gem_room: f32 = g.map.t.iter().filter(|t| t.kind == Kind::Treasury && t.owner == Owner::Player && t.res != 0).map(|t| TREASURY_TILE - t.gold as f32).sum(); @@ -5174,12 +5408,12 @@ mod tests { } } g.recount(); - g.update(0.01); + g.update(STEP); let far = |g: &Game| -> i32 { (cy - 9..=cy - 7).flat_map(|y| (cx + 10..=cx + 12).map(move |x| (x, y))).map(|(x, y)| g.map.t[idx(x, y)].gold).sum() }; let before = far(&g); let at = idx(cx + 11, cy - 8); g.add_res_at(900.0, 2, Some(at)); - g.update(0.01); + g.update(STEP); assert_eq!(far(&g) - before, 900, "the rubies lie where they were delivered"); } diff --git a/src/main.rs b/src/main.rs index 0f3b329..5d2db7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod audio; mod campaign; mod cloud; +mod commands; mod demo; mod dwarves; mod fog; @@ -35,10 +36,11 @@ mod wilds; mod xm; use cloud::{Cloud, CloudEvent}; +use commands::Act; use game::{Game, Msg}; use gfx::Gfx; use macroquad::prelude::*; -use map::{idx, inb, mh, mw}; +use map::{inb, mh, mw}; use menu::{Menu, MenuAction, Page}; use render3d::{Cam, Overlay, R3d}; use settings::{bindable_keys, Action, Settings}; @@ -110,27 +112,32 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32, (Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge | Tool::Steel, Some(t)) => inp.ldown = Some(t), (Tool::Hand, _) => { let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false); + let Some(j) = j else { return }; + let (id, hero, dwarf) = (app.game.ents[j].id, app.game.ents[j].hero, app.game.ents[j].dwarf()); + if dwarf { + return; + } if slap { - let kind = app.game.slap(j); - if kind > 0 { - ui.hand_anim = Some((kind, now)); - } + // the punch/backhand animation at the pointer starts at once, the order follows at the next step + ui.hand_anim = Some((if hero { 2 } else { 1 }, now)); + app.game.queue(Act::Slap { ent: id }); } else { - app.game.pickup(j); + app.game.queue(Act::Pickup { ent: id }); } } - (Tool::Flag, Some((x, z))) => app.game.set_flag(x, z), - (Tool::Lightning, _) => app.game.lightning(wx, wy), - (Tool::Heal, _) => app.game.heal(wx, wy), - (Tool::Summon, _) => app.game.summon(wx, wy), - (Tool::Tower(k), Some((x, z))) => app.game.build_tower(k, x, z), - (Tool::Trap(k), Some((x, z))) => app.game.place_trap(k, x, z), - (Tool::Spell(sp), _) => app.game.cast(sp, wx, wy), + (Tool::Flag, Some((x, z))) => app.game.queue(Act::Flag { x, y: z }), + (Tool::Lightning, _) => app.game.queue(Act::Lightning { x: wx, y: wy }), + (Tool::Heal, _) => app.game.queue(Act::Heal { x: wx, y: wy }), + (Tool::Summon, _) => app.game.queue(Act::Summon { x: wx, y: wy }), + (Tool::Tower(k), Some((x, z))) => app.game.queue(Act::Tower { kind: k, x, y: z }), + (Tool::Trap(k), Some((x, z))) => app.game.queue(Act::Trap { kind: k, x, y: z }), + (Tool::Spell(sp), _) => app.game.queue(Act::Cast { spell: sp, x: wx, y: wy }), (Tool::Order(0), _) => { let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false); - app.game.order_attack(j, wx, wy); + let ent = j.filter(|&j| j < app.game.ents.len()).map(|j| app.game.ents[j].id); + app.game.queue(Act::Attack { ent, x: wx, y: wy }); } - (Tool::Order(_), Some((x, z))) => app.game.order_guard(x, z), + (Tool::Order(_), Some((x, z))) => app.game.queue(Act::Guard { x, y: z }), _ => {} } } @@ -139,15 +146,13 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32, fn finish_rect(app: &mut App, tool: Tool, start: (i32, i32), mx: f32, my: f32) { let Some((ex, ey)) = tool_tile(&app.cam, &app.game.map, tool, mx, my) else { return }; let (sx, sy) = start; + let r = commands::Rect::new(sx, sy, ex, ey); match tool { - Tool::Dig => { - let on = !app.game.map.t[idx(sx, sy)].marked; - app.game.mark_rect(sx, sy, ex, ey, on); - } - Tool::Room(r) => app.game.build_rect(r, sx, sy, ex, ey), - Tool::Sell => app.game.sell_rect(sx, sy, ex, ey), - Tool::Bridge => app.game.build_bridges(sx, sy, ex, ey), - Tool::Steel => app.game.build_steel(sx, sy, ex, ey), + Tool::Dig => app.game.queue(Act::Dig(r)), + Tool::Room(room) => app.game.queue(Act::Room { room: room as u8, r }), + Tool::Sell => app.game.queue(Act::Sell(r)), + Tool::Bridge => app.game.queue(Act::Bridge(r)), + Tool::Steel => app.game.queue(Act::Steel(r)), _ => {} } } @@ -731,9 +736,9 @@ async fn main() { if inp.ldown.is_some() { inp.ldown = None; } else if app.game.hand.is_empty() && matches!(ui.tool, Tool::Order(_)) { - app.game.cancel_orders(); + app.game.queue(Act::CancelOrders); } else { - app.game.drop(wx, wy); + app.game.queue(Act::Drop { x: wx, y: wy }); } } inp.rdown = None; @@ -929,7 +934,7 @@ async fn main() { inp.ldown = None; if ui.tool == Tool::Hand && !app.game.hand.is_empty() { let gp = app.cam.ground_point(p.x / d, p.y / d); - app.game.drop(gp.x, gp.y); + app.game.queue(Act::Drop { x: gp.x, y: gp.y }); } else { inp.inspect = Some((p.x / d, p.y / d, now)); } @@ -983,9 +988,8 @@ async fn main() { app.game.events.retain(|ev| ev.pos.is_none()); } if playing { - for _ in 0..(app.settings.game_speed.round().max(1.0) as usize) { - app.game.update(dt); - } + // the world runs in fixed steps: the speed setting only feeds more time into them + app.game.update(dt * app.settings.game_speed.max(1.0)); // autosave: snapshot for leaving the tab + regular autosave if app.game.state == 0 { cache_t += dt; diff --git a/src/orders.rs b/src/orders.rs index df20b54..08277e5 100644 --- a/src/orders.rs +++ b/src/orders.rs @@ -66,7 +66,7 @@ impl Game { } let i = idx(x, y); let kind = self.map.t[i].kind; - if self.map.t[i].owner != Owner::Player || room_index(kind).is_none() && kind != Kind::Floor { + if self.map.t[i].owner != self.me || room_index(kind).is_none() && kind != Kind::Floor { self.hint(31, "hint.order_guard"); self.sfx_ui(Sfx::Error); return; @@ -77,7 +77,7 @@ impl Game { for dx in -3..=3 { if let Some(t) = self.map.get(x + dx, y + dy) { let j = idx(x + dx, y + dy); - if t.kind == kind && t.owner == Owner::Player && t.reserved == 0 && !self.ents.iter().any(|e| e.job == Job::Guard(j)) { + if t.kind == kind && t.owner == self.me && t.reserved == 0 && !self.ents.iter().any(|e| e.job == Job::Guard(j)) { spots.push(j); } } diff --git a/src/rooms.rs b/src/rooms.rs index 6410e08..13b544f 100644 --- a/src/rooms.rs +++ b/src/rooms.rs @@ -36,7 +36,7 @@ impl Game { // ---- prison pub(crate) fn free_cell(&self, i: usize) -> bool { let t = &self.map.t[i]; - matches!(t.kind, Kind::Torture | Kind::Prison) && t.owner == Owner::Player && t.bed == 0 + matches!(t.kind, Kind::Torture | Kind::Prison) && t.owner == self.me && t.bed == 0 } // ---- graveyard: a fallen unit is buried in a free grave @@ -50,7 +50,7 @@ impl Game { .t .iter() .enumerate() - .filter(|(i, t)| t.kind == Kind::Graveyard && t.owner == Owner::Player && !taken.contains(i)) + .filter(|(i, t)| t.kind == Kind::Graveyard && t.owner == self.me && !taken.contains(i)) .min_by_key(|(i, _)| { let (tx, ty) = xy(*i); ((tx as f32 - x).powi(2) + (ty as f32 - y).powi(2)) as i32 @@ -96,7 +96,7 @@ impl Game { // ---- guard post: a free post for a creature pub(crate) fn free_post(&self, i: usize, me: u32) -> bool { let t = &self.map.t[i]; - t.kind == Kind::GuardPost && t.owner == Owner::Player && (t.reserved == 0 || t.reserved == me) + t.kind == Kind::GuardPost && t.owner == self.me && (t.reserved == 0 || t.reserved == me) } // ---- arena: sparring with a partner nearby; fast experience, some bruises diff --git a/src/traps.rs b/src/traps.rs index 0e105b7..a88750c 100644 --- a/src/traps.rs +++ b/src/traps.rs @@ -166,7 +166,7 @@ impl Game { return; } let t = self.map.t[i]; - if t.kind != Kind::Floor || t.owner != Owner::Player { + if t.kind != Kind::Floor || t.owner != self.me { self.hint(25, "hint.trap_where"); self.sfx_ui(Sfx::Error); return; @@ -201,7 +201,7 @@ impl Game { self.traps[k].progress += work; if self.traps[k].progress >= self.traps[k].kind.work() { // finished: a crate on a forge tile - let spot = self.map.t.iter().enumerate().filter(|(_, t)| t.kind == Kind::Forge && t.owner == Owner::Player).map(|(i, _)| i).nth(self.traps[k].id as usize % self.forge_tiles().max(1)); + let spot = self.map.t.iter().enumerate().filter(|(_, t)| t.kind == Kind::Forge && t.owner == self.me).map(|(i, _)| i).nth(self.traps[k].id as usize % self.forge_tiles().max(1)); match spot { Some(s) => { let t = &mut self.traps[k]; @@ -219,7 +219,7 @@ impl Game { } // orders on lost ground are dropped let map = &self.map; - self.traps.retain(|t| t.stage == ARMED || map.t[t.tile].owner == Owner::Player); + self.traps.retain(|t| t.stage == ARMED || map.t[t.tile].owner == self.me); for t in self.traps.iter_mut() { t.fired += dt; t.cd -= dt;