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:
1 parent
d6bcce3a76
commit
1ec7492189
8 files changed
+634
-100
No files matched your search
+24
-3
@@ -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.
|
||||
+275
@@ -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<u32>,
|
||||
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::<Command>(&json).unwrap(), c);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+293
-59
@@ -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 => {
|
||||
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)
|
||||
}
|
||||
Owner::Player => 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");
|
||||
}
|
||||
|
||||
|
||||
+33
-29
@@ -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;
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+3
-3
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user