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

This commit is contained in:
Richard Zink committed 2026-11-04 09:57:02 +01:00
1 parent d6bcce3a76
commit 1ec7492189
8 files changed
+635 -101

No files matched your search

+294 -60
View File
@@ -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<u8> = 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<Owner>,
pub core: (i32, i32),
pub core_hp: f32,
pub gold: f32,
pub mana: f32,
pub payday: f32,
pub payday_warned: bool,
pub hand: Vec<u32>,
pub flag: Option<usize>,
pub reserve: f32,
pub incoming: Vec<(u8, f32, Option<usize>)>,
pub training_bill: f32,
pub counts: [usize; N_ROOMS],
pub springs: usize,
pub known: Vec<bool>,
pub researchable: Vec<bool>,
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<MasterState>, // the other masters (multiplayer)
#[serde(skip)]
pub pending: Vec<Command>, // 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<String>) {
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<u8> {
let mut v: Vec<u8> = 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<usize> {
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");
}