Debug demo map: four allied masters with every room and creature against an endless hero gate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f70992cfb
commit
210f17773a
23 files changed
+536
-3
No files matched your search
+1
-1
@@ -153,7 +153,7 @@ impl Tracker {
|
||||
|
||||
// called every frame; returns achievements unlocked just now
|
||||
pub fn update(&mut self, g: &Game, ctx: &Context, dt: f32, now: u64) -> Vec<String> {
|
||||
if g.demo.is_some() || ctx.editor_test {
|
||||
if g.demo.is_some() || g.demo_map.is_some() || ctx.editor_test {
|
||||
self.last_time = 0.0;
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
+471
@@ -0,0 +1,471 @@
|
||||
// The demo map of the debug menu: all four dungeon masters (the player and three AI partners, all
|
||||
// allied) each have a walled dungeon with every room kind and every dungeon creature kind. Their
|
||||
// dungeons lie in the four corners around the humans' gate in the middle; the attack flag goes up
|
||||
// on the gate now and then and all four armies march on it together, in between the creatures do
|
||||
// their work in the rooms. The gate never falls and keeps sending heroes of every kind, the soul
|
||||
// cores never fall, the treasuries never run dry. Meant to watch every animation; nothing of it
|
||||
// counts for achievements or overwrites the autosave (see main.rs, achievements.rs).
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
use crate::render3d::Cam;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const W: usize = 74;
|
||||
const H: usize = 74;
|
||||
const PLAZA: (i32, i32) = (37, 37); // middle of the humans' plaza (the gate stands on it)
|
||||
const PLAZA_R: f32 = 8.5;
|
||||
const HALF: i32 = 10; // a dungeon's floor reaches this far from the middle of its soul core
|
||||
// the masters and the middle of their dungeons: player north-west, then clockwise
|
||||
pub const MASTERS: [(u8, (i32, i32)); 4] = [(0, (13, 13)), (2, (60, 13)), (3, (60, 60)), (4, (13, 60))];
|
||||
// every creature kind a dungeon can have (imps come with every master)
|
||||
pub const CREATURES: [EK; 17] = [
|
||||
EK::Kobold,
|
||||
EK::Spider,
|
||||
EK::Mage,
|
||||
EK::Ogre,
|
||||
EK::Jailer,
|
||||
EK::Priestess,
|
||||
EK::Chainbreaker,
|
||||
EK::Bat,
|
||||
EK::Gargoyle,
|
||||
EK::Owl,
|
||||
EK::Wyrm,
|
||||
EK::Cogling,
|
||||
EK::Revenant,
|
||||
EK::CaveBear,
|
||||
EK::Beetle,
|
||||
EK::Ram,
|
||||
EK::Catapult,
|
||||
];
|
||||
// the gate sends them in this order, over and over
|
||||
pub const HEROES: [EK; 14] = [
|
||||
EK::Knight,
|
||||
EK::Archer,
|
||||
EK::Priest,
|
||||
EK::Dwarf,
|
||||
EK::Fairy,
|
||||
EK::Thief,
|
||||
EK::BattleMage,
|
||||
EK::Monk,
|
||||
EK::Paladin,
|
||||
EK::Ranger,
|
||||
EK::Giant,
|
||||
EK::Inquisitor,
|
||||
EK::Ram,
|
||||
EK::Catapult,
|
||||
];
|
||||
const HERO_EVERY: f32 = 1.2; // seconds between two heroes at the gate
|
||||
const MAX_HEROES: usize = 30; // fighting heroes on the map at most
|
||||
const RESTOCK_EVERY: f32 = 15.0; // seconds between two looks for creature kinds that died out
|
||||
const ATTACK_FOR: f32 = 75.0; // the flag stays on the gate this long ...
|
||||
const HOME_FOR: f32 = 40.0; // ... then the creatures are free for their rooms this long
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct DemoMap {
|
||||
pub hero_t: f32,
|
||||
pub next_hero: u32,
|
||||
pub heroes_sent: u32,
|
||||
pub restock_t: f32,
|
||||
pub phase_t: f32, // seconds until the flag goes up (or down)
|
||||
pub attacking: bool,
|
||||
}
|
||||
|
||||
// the 3x3 room slots of a dungeon around its core, one tile of corridor between them (5 x 5
|
||||
// slots, the middle one is the core): top-left corners
|
||||
fn slots(c: (i32, i32)) -> Vec<(i32, i32)> {
|
||||
let mut v = Vec::new();
|
||||
for ky in -2..=2 {
|
||||
for kx in -2..=2 {
|
||||
if kx != 0 || ky != 0 {
|
||||
v.push((c.0 - 1 + 4 * kx, c.1 - 1 + 4 * ky));
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn in_square(c: (i32, i32), x: i32, y: i32, r: i32) -> bool {
|
||||
(x - c.0).abs() <= r && (y - c.1).abs() <= r
|
||||
}
|
||||
|
||||
// the spot of the attack flag: on the plaza right before the gate
|
||||
fn rally_tile() -> usize {
|
||||
idx(PLAZA.0, PLAZA.1 + 3)
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new_demo_map(seed: u32) -> (Game, Cam) {
|
||||
let opts = GenOpts { portals: Some(0), treasures: Some(0), gate: false, core_y: None };
|
||||
let mut map = Map::generate_with(seed, W, H, &opts);
|
||||
let (wi, hi) = (W as i32, H as i32);
|
||||
// solid earth everywhere, with a few gold veins between the dungeons
|
||||
for y in 2..hi - 2 {
|
||||
for x in 2..wi - 2 {
|
||||
let t = &mut map.t[idx(x, y)];
|
||||
let var = t.var;
|
||||
*t = Tile::new(Kind::Earth);
|
||||
t.var = var;
|
||||
t.hp = 6.0;
|
||||
let h = ((x * 73 + y * 151) as u32).wrapping_mul(2654435761) >> 26;
|
||||
let between = MASTERS.iter().all(|&(_, c)| !in_square(c, x, y, HALF + 1)) && ((x - PLAZA.0).pow(2) + (y - PLAZA.1).pow(2)) as f32 > (PLAZA_R + 2.0).powi(2);
|
||||
if between && h % 17 == 0 {
|
||||
t.kind = Kind::Gold;
|
||||
t.gold = 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
// the humans' plaza with the gate in the middle
|
||||
for y in 2..hi - 2 {
|
||||
for x in 2..wi - 2 {
|
||||
if (((x - PLAZA.0).pow(2) + (y - PLAZA.1).pow(2)) as f32) <= PLAZA_R * PLAZA_R {
|
||||
let t = &mut map.t[idx(x, y)];
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = Owner::Hero;
|
||||
t.gold = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
let gate = (PLAZA.0 - 1, PLAZA.1 - 1);
|
||||
for y in gate.1..gate.1 + 3 {
|
||||
for x in gate.0..gate.0 + 3 {
|
||||
let t = &mut map.t[idx(x, y)];
|
||||
t.kind = Kind::Gate;
|
||||
t.owner = Owner::Hero;
|
||||
}
|
||||
}
|
||||
map.gate = gate;
|
||||
// the dungeons: a square of own floor in a reinforced wall, the core in the middle, the
|
||||
// rooms around it
|
||||
map.portals.clear();
|
||||
map.treasures.clear();
|
||||
for &(side, c) in MASTERS.iter() {
|
||||
let owner = Owner::of_side(side);
|
||||
for y in c.1 - HALF - 1..=c.1 + HALF + 1 {
|
||||
for x in c.0 - HALF - 1..=c.0 + HALF + 1 {
|
||||
let t = &mut map.t[idx(x, y)];
|
||||
t.owner = owner;
|
||||
t.gold = 0;
|
||||
if in_square(c, x, y, HALF) {
|
||||
t.kind = Kind::Floor;
|
||||
} else {
|
||||
t.kind = Kind::Earth;
|
||||
t.reinforced = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for y in c.1 - 1..=c.1 + 1 {
|
||||
for x in c.0 - 1..=c.0 + 1 {
|
||||
map.t[idx(x, y)].kind = Kind::Core;
|
||||
}
|
||||
}
|
||||
// every room kind, more beds and food, the portal on the far side; slots left over
|
||||
// stay plain floor for the master's own plans
|
||||
let mut rooms: Vec<Kind> = ROOM_KINDS.to_vec();
|
||||
rooms.extend([Kind::Lair, Kind::Lair, Kind::Farm, Kind::Aerie]);
|
||||
let mut list = slots(c);
|
||||
// the slots nearest the plaza first: the busy rooms are what the camera sees
|
||||
list.sort_by_key(|&(x, y)| (x + 1 - PLAZA.0).pow(2) + (y + 1 - PLAZA.1).pow(2));
|
||||
let portal = list.pop().unwrap();
|
||||
for y in portal.1..portal.1 + 3 {
|
||||
for x in portal.0..portal.0 + 3 {
|
||||
map.t[idx(x, y)].kind = Kind::Portal;
|
||||
}
|
||||
}
|
||||
map.portals.push(portal);
|
||||
for (&(sx, sy), &kind) in list.iter().zip(rooms.iter()) {
|
||||
for y in sy..sy + 3 {
|
||||
for x in sx..sx + 3 {
|
||||
map.t[idx(x, y)].kind = kind;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// corridors from every dungeon to the plaza (through the wall)
|
||||
for &(side, c) in MASTERS.iter() {
|
||||
let owner = Owner::of_side(side);
|
||||
let (dx, dy) = ((PLAZA.0 - c.0) as f32, (PLAZA.1 - c.1) as f32);
|
||||
let n = (dx.abs().max(dy.abs()) * 2.0) as i32;
|
||||
for k in 0..=n {
|
||||
let (px, py) = (c.0 as f32 + 0.5 + dx * k as f32 / n as f32, c.1 as f32 + 0.5 + dy * k as f32 / n as f32);
|
||||
for y in (py - 1.5) as i32..=(py + 1.5) as i32 {
|
||||
for x in (px - 1.5) as i32..=(px + 1.5) as i32 {
|
||||
if (x as f32 + 0.5 - px).powi(2) + (y as f32 + 0.5 - py).powi(2) > 1.6 * 1.6 || x < 2 || y < 2 || x >= wi - 2 || y >= hi - 2 {
|
||||
continue;
|
||||
}
|
||||
let t = &mut map.t[idx(x, y)];
|
||||
if in_square(c, x, y, HALF) || t.owner == Owner::Hero {
|
||||
continue; // the dungeon's rooms and the plaza stay as they are
|
||||
}
|
||||
let wall = in_square(c, x, y, HALF + 1);
|
||||
t.kind = Kind::Floor;
|
||||
t.reinforced = false;
|
||||
t.gold = 0;
|
||||
t.owner = if wall { owner } else { Owner::None };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let player = MASTERS[0].1;
|
||||
map.core = (player.0 - 1, player.1 - 1);
|
||||
let mut g = Game::from_map(map, seed);
|
||||
// from_map put a treasury ring around the player's core, on the corridors between the
|
||||
// rooms: back to floor (his treasury is one of the rooms, like everybody's)
|
||||
for y in player.1 - 2..=player.1 + 2 {
|
||||
for x in player.0 - 2..=player.0 + 2 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
if t.kind == Kind::Treasury && ((x - player.0).abs() == 2 || (y - player.1).abs() == 2) {
|
||||
t.kind = Kind::Floor;
|
||||
}
|
||||
}
|
||||
}
|
||||
g.demo_map = Some(DemoMap { hero_t: 3.0, next_hero: 0, heroes_sent: 0, restock_t: RESTOCK_EVERY, phase_t: 15.0, attacking: false });
|
||||
g.gate_on = true;
|
||||
g.gate_hp = GATE_HP;
|
||||
g.wave_timer = f32::MAX;
|
||||
g.coop = true;
|
||||
g.ai_sides = vec![2, 3, 4];
|
||||
// the three AI partners: real masters with the brain of ai.rs (hard: they think fast)
|
||||
for &(side, c) in MASTERS.iter().skip(1) {
|
||||
let core = (c.0 - 1, c.1 - 1);
|
||||
g.rivals.push(crate::rival::Rival {
|
||||
side,
|
||||
core,
|
||||
hp: crate::rival::RIVAL_CORE_HP,
|
||||
alive: true,
|
||||
radius: HALF + 1,
|
||||
grow_t: 20.0,
|
||||
spawn_t: 5.0,
|
||||
attack_t: 0.0,
|
||||
difficulty: 2,
|
||||
power: 0.0,
|
||||
gift_t: crate::rival::PARTNER_GIFT_EVERY,
|
||||
think_t: 2.0,
|
||||
attacking: false,
|
||||
army0: 0.0,
|
||||
focus: None,
|
||||
defending: false,
|
||||
});
|
||||
g.adopt_master(side, core, 2, true);
|
||||
}
|
||||
// every creature kind for every master, gold in the treasuries
|
||||
for &(side, _) in MASTERS.iter() {
|
||||
g.switch_to(side);
|
||||
g.recount();
|
||||
for &kind in CREATURES.iter() {
|
||||
g.demo_map_creature(side, kind);
|
||||
}
|
||||
let fill = g.gold_cap() * 0.7 - g.gold;
|
||||
g.add_gold(fill.max(0.0));
|
||||
g.mana = g.mana_max();
|
||||
g.wood = g.wood.max(200.0);
|
||||
}
|
||||
g.switch_to(0);
|
||||
g.place_all_torches();
|
||||
g.msgs.clear();
|
||||
// the camera on the plaza, where the four armies meet
|
||||
let mut cam = Cam::new(PLAZA.0 as f32 + 0.5, PLAZA.1 as f32 + 0.5);
|
||||
cam.zoom = 30.0;
|
||||
(g, cam)
|
||||
}
|
||||
|
||||
// a creature of `kind` for the master on `side` (the master being processed), somewhere on
|
||||
// the floor of his dungeon
|
||||
fn demo_map_creature(&mut self, side: u8, kind: EK) {
|
||||
let Some(&(_, c)) = MASTERS.iter().find(|m| m.0 == side) else { return };
|
||||
let owner = Owner::of_side(side);
|
||||
let spots: Vec<usize> = (0..self.map.t.len())
|
||||
.filter(|&i| {
|
||||
let (x, y) = xy(i);
|
||||
let t = &self.map.t[i];
|
||||
in_square(c, x, y, HALF) && t.owner == owner && t.walkable() && t.kind != Kind::Portal
|
||||
})
|
||||
.collect();
|
||||
if spots.is_empty() {
|
||||
return;
|
||||
}
|
||||
let i = spots[self.rng.next() as usize % spots.len()];
|
||||
let (x, y) = xy(i);
|
||||
let level = 2 + self.rng.next() % 5;
|
||||
let id = self.spawn(kind, false, x as f32 + 0.5, y as f32 + 0.5, level);
|
||||
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.side = side;
|
||||
e.settled = true;
|
||||
e.happy = 1.0;
|
||||
if crate::siege::siege(kind) {
|
||||
e.home = Some((x as f32 + 0.5, y as f32 + 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the demo map's rules, once per step from the first master's side: the gate and the cores
|
||||
// never fall, gold never runs out, heroes keep coming, the flag goes up and down
|
||||
pub(crate) fn demo_map_tick(&mut self, dt: f32) {
|
||||
let Some(mut d) = self.demo_map.take() else { return };
|
||||
let back = self.me_side;
|
||||
self.gate_hp = GATE_HP;
|
||||
self.gate_stage = 0;
|
||||
self.wave_timer = f32::MAX;
|
||||
self.warned = true;
|
||||
self.out_sides.clear();
|
||||
// the attack flag of the player (the partners follow it, see ai.rs)
|
||||
d.phase_t -= dt;
|
||||
if d.phase_t <= 0.0 {
|
||||
d.attacking = !d.attacking;
|
||||
d.phase_t = if d.attacking { ATTACK_FOR } else { HOME_FOR };
|
||||
}
|
||||
d.restock_t -= dt;
|
||||
let restock = d.restock_t <= 0.0;
|
||||
if restock {
|
||||
d.restock_t = RESTOCK_EVERY;
|
||||
}
|
||||
for &(side, _) in MASTERS.iter() {
|
||||
self.switch_to(side);
|
||||
self.core_hp = CORE_HP;
|
||||
if self.gold < self.gold_cap() * 0.4 {
|
||||
let fill = self.gold_cap() * 0.7 - self.gold;
|
||||
self.add_gold(fill.max(0.0));
|
||||
}
|
||||
if side == 0 {
|
||||
self.flag = if d.attacking { Some(rally_tile()) } else { None };
|
||||
}
|
||||
// kinds that died out (or left) come back
|
||||
if restock {
|
||||
for &kind in CREATURES.iter() {
|
||||
if !self.ents.iter().any(|e| e.alive && !e.hero && e.side == side && e.kind == kind) {
|
||||
self.demo_map_creature(side, kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for r in self.rivals.iter_mut() {
|
||||
r.alive = true;
|
||||
r.hp = crate::rival::RIVAL_CORE_HP;
|
||||
}
|
||||
self.switch_to(back);
|
||||
// heroes of every kind from the gate: half of them guard the plaza, the others march on
|
||||
// the dungeons
|
||||
d.hero_t -= dt;
|
||||
let fighting = self.ents.iter().filter(|e| e.alive && e.active_hero()).count();
|
||||
if d.hero_t <= 0.0 && fighting < MAX_HEROES {
|
||||
d.hero_t = HERO_EVERY;
|
||||
let n = d.next_hero;
|
||||
d.next_hero += 1;
|
||||
d.heroes_sent += 1;
|
||||
let kind = HEROES[n as usize % HEROES.len()];
|
||||
let level = 3 + (n / HEROES.len() as u32) % 6;
|
||||
let (gx, gy) = self.map.gate;
|
||||
let x = gx as f32 + 0.5 + self.rng.f() * 2.0;
|
||||
let y = gy as f32 + 3.2 + self.rng.f() * 0.6;
|
||||
let id = self.spawn(kind, true, x, y, level);
|
||||
let guard = n % 2 == 0 && !crate::siege::siege(kind);
|
||||
let a = self.rng.f() * std::f32::consts::TAU;
|
||||
let r = 2.5 + self.rng.f() * 3.0;
|
||||
let home = (PLAZA.0 as f32 + 0.5 + a.cos() * r, PLAZA.1 as f32 + 0.5 + a.sin() * r);
|
||||
let target = [0u8, 2, 3, 4][(n / 2) as usize % 4];
|
||||
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
|
||||
if guard {
|
||||
e.guard = true;
|
||||
e.home = Some(home);
|
||||
} else if target != 0 {
|
||||
e.target = Some(target);
|
||||
}
|
||||
}
|
||||
self.fx(3, gx as f32 + 1.5, gy as f32 + 1.5, 0.0, 0.0, 0.6, 0xffffeecc);
|
||||
}
|
||||
self.demo_map = Some(d);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn kinds_of(g: &Game, side: u8) -> Vec<EK> {
|
||||
g.ents.iter().filter(|e| e.alive && !e.hero && e.side == side).map(|e| e.kind).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_allied_masters_with_every_room_and_creature() {
|
||||
let (mut g, _) = Game::new_demo_map(5);
|
||||
for y in 0..H as i32 {
|
||||
let row: String = (0..W as i32)
|
||||
.map(|x| {
|
||||
let t = g.map.t[idx(x, y)];
|
||||
match t.kind {
|
||||
Kind::Gate => 'G',
|
||||
Kind::Core => 'C',
|
||||
Kind::Portal => 'P',
|
||||
Kind::Gold => '$',
|
||||
Kind::Floor => if t.owner == Owner::None { '.' } else { ',' },
|
||||
k if room_index(k).is_some() => (b'a' + room_index(k).unwrap() as u8) as char,
|
||||
_ if t.solid() => if t.reinforced { '#' } else { ' ' },
|
||||
_ => '?',
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
println!("{}", row);
|
||||
}
|
||||
assert_eq!(g.master_sides(), vec![0, 2, 3, 4]);
|
||||
for &(a, _) in MASTERS.iter() {
|
||||
for &(b, _) in MASTERS.iter() {
|
||||
assert!(g.allied(a, b), "{a} and {b} are allies");
|
||||
}
|
||||
assert!(!g.allied(a, 1), "the heroes are the enemy");
|
||||
}
|
||||
for &(side, _) in MASTERS.iter() {
|
||||
g.switch_to(side);
|
||||
g.recount();
|
||||
for (r, &k) in ROOM_KINDS.iter().enumerate() {
|
||||
assert!(g.counts[r] >= 9, "side {side} has a {:?}", k);
|
||||
}
|
||||
assert!(g.portals_owned() > 0, "side {side} has a portal");
|
||||
assert!(g.gold > 1000.0, "side {side} has gold");
|
||||
let have = kinds_of(&g, side);
|
||||
for k in CREATURES.iter().chain([EK::Worker].iter()) {
|
||||
assert!(have.contains(k), "side {side} has a {:?}", k);
|
||||
}
|
||||
}
|
||||
g.switch_to(0);
|
||||
assert!(g.has_gate() && g.gate_hp > 0.0);
|
||||
assert!(g.is_partner(2) && g.is_partner(3) && g.is_partner(4));
|
||||
// every dungeon reaches the gate
|
||||
let gate = rally_tile();
|
||||
for &(_, c) in MASTERS.iter() {
|
||||
assert!(g.map.path_to(idx(c.0 + 2, c.1 + 2), gate).is_some(), "the way from {:?} to the gate", c);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_gate_never_falls_and_keeps_sending_heroes() {
|
||||
let (mut g, _) = Game::new_demo_map(5);
|
||||
let mut kinds = Vec::new();
|
||||
let mut rallied = false;
|
||||
for k in 0..(20 * 120) {
|
||||
g.update(0.05);
|
||||
if k % (20 * 20) == 0 {
|
||||
let c: Vec<usize> = MASTERS.iter().map(|m| g.ents.iter().filter(|e| e.alive && !e.hero && e.side == m.0).count()).collect();
|
||||
let h = g.ents.iter().filter(|e| e.alive && e.active_hero()).count();
|
||||
let rally = g.ents.iter().filter(|e| e.alive && e.job == Job::Rally).count();
|
||||
println!("{:>3} s: creatures {:?}, heroes {}, rallying {}, gate {}", k / 20, c, h, rally, g.gate_hp);
|
||||
}
|
||||
for e in g.ents.iter().filter(|e| e.alive && e.hero) {
|
||||
if !kinds.contains(&e.kind) {
|
||||
kinds.push(e.kind);
|
||||
}
|
||||
}
|
||||
rallied |= g.ents.iter().any(|e| e.alive && !e.hero && e.side != 0 && e.job == Job::Rally);
|
||||
assert_eq!(g.state, 0, "the demo map never ends");
|
||||
}
|
||||
let d = g.demo_map.as_ref().unwrap();
|
||||
assert!(d.heroes_sent >= 20, "heroes keep coming: {}", d.heroes_sent);
|
||||
assert!(kinds.len() >= 12, "every kind of hero: {:?}", kinds);
|
||||
assert!(g.gate_hp > 0.0 && g.has_gate());
|
||||
assert_eq!(g.master_sides(), vec![0, 2, 3, 4]);
|
||||
assert!(g.rivals.iter().all(|r| r.alive), "no master falls");
|
||||
assert!(rallied, "the partners' creatures follow the flag to the gate");
|
||||
for &(side, _) in MASTERS.iter() {
|
||||
assert!(g.core_hp_of(side) > 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,6 +652,8 @@ pub struct Game {
|
||||
#[serde(skip)]
|
||||
pub demo: Option<crate::demo::Demo>, // the endless bridge battle behind the title menu
|
||||
#[serde(default)]
|
||||
pub demo_map: Option<crate::demo_map::DemoMap>, // the debug demo map: four allied masters against an endless gate
|
||||
#[serde(default)]
|
||||
pub dwarf_caves: Vec<crate::dwarves::DwarfCave>,
|
||||
#[serde(default)]
|
||||
pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges
|
||||
@@ -1001,6 +1003,7 @@ impl Game {
|
||||
springs: 0,
|
||||
freed: 0,
|
||||
demo: None,
|
||||
demo_map: None,
|
||||
traps: Vec::new(),
|
||||
rollers: Vec::new(),
|
||||
clouds: Vec::new(),
|
||||
@@ -1843,6 +1846,9 @@ impl Game {
|
||||
self.update_wood(dt);
|
||||
self.update_events(dt);
|
||||
self.update_traders(dt);
|
||||
if self.demo_map.is_some() {
|
||||
self.demo_map_tick(dt);
|
||||
}
|
||||
// fallen hearts of a match are shared state: every client marks them in the same step
|
||||
self.update_fallen_masters();
|
||||
// the end of the game and the fog are about the master at this computer
|
||||
|
||||
+26
-2
@@ -12,6 +12,7 @@ mod campaign_doc;
|
||||
mod cloud;
|
||||
mod commands;
|
||||
mod demo;
|
||||
mod demo_map;
|
||||
mod editor;
|
||||
mod events;
|
||||
mod dwarves;
|
||||
@@ -1660,6 +1661,28 @@ app.pending_briefing = briefing_of(&menu.campaign_doc, i);
|
||||
ui.show_help = false;
|
||||
autosave_t = 0.0;
|
||||
}
|
||||
MenuAction::DemoMap => {
|
||||
// debug: four allied masters with every room and creature against an
|
||||
// endless gate (never autosaved, counts for no achievement)
|
||||
if app.match_mode || app.game.in_match() {
|
||||
app.leave_match();
|
||||
}
|
||||
app.seed = next_seed(app.seed);
|
||||
(app.game, app.cam) = Game::new_demo_map(app.seed);
|
||||
apply_game_settings(&mut app.game, &app.settings);
|
||||
app.coop = false;
|
||||
app.recorded = false;
|
||||
app.tutorial = false;
|
||||
app.custom_campaign = None;
|
||||
app.custom_progress = None;
|
||||
app.custom_mission_index = None;
|
||||
app.custom_next_index = None;
|
||||
menu.page = None;
|
||||
r3d.invalidate();
|
||||
ui.log.clear();
|
||||
ui.show_help = false;
|
||||
autosave_t = 0.0;
|
||||
}
|
||||
MenuAction::OpenRepo => platform::open_url(menu::REPO_URL),
|
||||
MenuAction::OpenClaudeCode => platform::open_url(menu::CLAUDE_CODE_URL),
|
||||
MenuAction::OpenOpenCode => platform::open_url(menu::OPENCODE_URL),
|
||||
@@ -1700,7 +1723,7 @@ app.pending_briefing = briefing_of(&menu.campaign_doc, i);
|
||||
app.tutorial = false;
|
||||
if app.match_mode || app.game.in_match() {
|
||||
app.leave_match();
|
||||
} else if app.game.state == 0 {
|
||||
} else if app.game.state == 0 && app.game.demo_map.is_none() {
|
||||
platform::storage_set(save::AUTOSAVE_KEY, &app.autosave_json());
|
||||
menu.has_autosave = true;
|
||||
}
|
||||
@@ -1980,7 +2003,8 @@ app.pending_briefing = briefing_of(&campaign, i);
|
||||
// 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 && !app.match_mode {
|
||||
// (the debug demo map never replaces the autosave)
|
||||
if app.game.state == 0 && !app.match_mode && app.game.demo_map.is_none() {
|
||||
cache_t += dt;
|
||||
autosave_t += dt;
|
||||
if cache_t >= AUTOSAVE_CACHE_EVERY {
|
||||
|
||||
+14
@@ -252,6 +252,7 @@ enum Btn {
|
||||
Start,
|
||||
Key(usize),
|
||||
ResetKeys,
|
||||
DemoMap, // debug tab: start the demo map
|
||||
SaveNew,
|
||||
Slot(usize), // load / overwrite
|
||||
Delete(usize),
|
||||
@@ -512,6 +513,7 @@ pub enum MenuAction {
|
||||
OpenRepo,
|
||||
OpenClaudeCode,
|
||||
Tutorial, // start the tutorial mission
|
||||
DemoMap, // debug: the demo map with all four masters against an endless gate
|
||||
BackToEditor,
|
||||
OpenOpenCode,
|
||||
Login(String, String),
|
||||
@@ -854,6 +856,10 @@ impl Menu {
|
||||
self.page = Some(Page::KeyCapture { action: a, from_title });
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::DemoMap) => {
|
||||
self.page = None;
|
||||
MenuAction::DemoMap
|
||||
}
|
||||
Some(Btn::ResetKeys) => {
|
||||
settings.reset_keys();
|
||||
MenuAction::SettingsChanged
|
||||
@@ -1523,6 +1529,14 @@ impl Menu {
|
||||
self.buttons.push((right, Btn::Change(i, 1)));
|
||||
self.buttons.push((mid, Btn::Change(i, 1)));
|
||||
}
|
||||
if tab == 4 {
|
||||
// the demo map: all four masters with every room and creature against an endless gate
|
||||
let label = tr("set.debug_demo_map");
|
||||
let rw = gfx.measure(&label, S()) + CW() * 4.0;
|
||||
let rr = Rect::new(x + cols as f32 * CW() - rw - CW() * 2.0, y + (rows - 4) as f32 * CH(), rw, CH() * 2.0);
|
||||
button(gfx, rr, &label, rr.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((rr, Btn::DemoMap));
|
||||
}
|
||||
}
|
||||
let bw = gfx.measure(&tr("menu.back"), S()) + CW() * 6.0;
|
||||
let br = Rect::new(x + CW() * 2.0, y + (rows - 4) as f32 * CH(), bw, CH() * 2.0);
|
||||
|
||||
Reference in new issue
Block a user