Title menu background: an endless battle at a bridge over an underground river
- dungeon creatures against knights, balanced reinforcements for the weaker side - towers on both banks; heavy units go for them, destroyed towers are rebuilt after 25 s - the camera slowly circles the bridge; battle sounds stay muted; the demo is never saved Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d06ce2edb5
commit
f359671dac
3 files changed
+351
-16
No files matched your search
+312
@@ -0,0 +1,312 @@
|
||||
// The endless battle behind the title menu: the dungeon's creatures and the knights fight at
|
||||
// a bridge over an underground river. Both sides get balanced reinforcements, destroyed towers
|
||||
// are rebuilt after a while. Nothing here is ever saved.
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
use crate::render3d::Cam;
|
||||
|
||||
const W: usize = 72;
|
||||
const H: usize = 48;
|
||||
const RIVER_X: i32 = 34; // first water column (four tiles wide)
|
||||
const BRIDGE_Y: i32 = 23; // first bridge row (three tiles wide)
|
||||
const PER_SIDE: usize = 9; // at most this many units per side
|
||||
const REBUILD: f32 = 25.0; // seconds until a destroyed tower stands again
|
||||
|
||||
pub struct Demo {
|
||||
spawn_t: f32,
|
||||
towers: Vec<(i32, i32, TowerKind, bool, f32)>, // slot: position, kind, knight tower, time since lost
|
||||
rng: Rng,
|
||||
}
|
||||
|
||||
const CREATURES: [(EK, u32); 9] = [(EK::Kobold, 4), (EK::Spider, 3), (EK::Mage, 2), (EK::Ogre, 2), (EK::Jailer, 1), (EK::Priestess, 1), (EK::Bat, 2), (EK::Gargoyle, 1), (EK::Owl, 1)];
|
||||
const KNIGHTS: [(EK, u32); 5] = [(EK::Knight, 5), (EK::Archer, 3), (EK::Priest, 2), (EK::Dwarf, 2), (EK::Paladin, 1)];
|
||||
|
||||
fn pick(rng: &mut Rng, pool: &[(EK, u32)]) -> EK {
|
||||
let total: u32 = pool.iter().map(|p| p.1).sum();
|
||||
let mut r = rng.next() % total;
|
||||
for &(k, w) in pool {
|
||||
if r < w {
|
||||
return k;
|
||||
}
|
||||
r -= w;
|
||||
}
|
||||
pool[0].0
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new_demo(seed: u32) -> (Game, Cam) {
|
||||
let opts = GenOpts { portals: Some(0), treasures: Some(0), gate: false, core_y: Some(H as i32 / 2 - 1) };
|
||||
let map = Map::generate_with(seed, W, H, &opts);
|
||||
let mut g = Game::from_map(map, seed);
|
||||
g.ents.clear();
|
||||
let mut rng = Rng(seed | 1);
|
||||
// a wide cave on both banks: the dungeon's ground on the left, knight ground on the right
|
||||
for y in 3..H as i32 - 3 {
|
||||
for x in 3..W as i32 - 3 {
|
||||
let cy = (y - H as i32 / 2) as f32 / (H as f32 * 0.42);
|
||||
let cx = (x - W as i32 / 2) as f32 / (W as f32 * 0.47);
|
||||
let edge = 1.0 + (rng.f() - 0.5) * 0.12;
|
||||
if cx * cx + cy * cy > edge {
|
||||
continue;
|
||||
}
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
if matches!(t.kind, Kind::Core | Kind::Treasury) {
|
||||
continue;
|
||||
}
|
||||
t.kind = Kind::Floor;
|
||||
t.gold = 0;
|
||||
t.owner = if x < RIVER_X { Owner::Player } else if x >= RIVER_X + 4 { Owner::Hero } else { Owner::None };
|
||||
}
|
||||
}
|
||||
// the river from top to bottom, and the bridge across
|
||||
for y in 2..H as i32 - 2 {
|
||||
for x in RIVER_X..RIVER_X + 4 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.owner = Owner::None;
|
||||
t.gold = 0;
|
||||
t.kind = if (BRIDGE_Y..BRIDGE_Y + 3).contains(&y) { Kind::Bridge } else { Kind::Water };
|
||||
}
|
||||
}
|
||||
for y in BRIDGE_Y..BRIDGE_Y + 3 {
|
||||
for x in RIVER_X..RIVER_X + 4 {
|
||||
g.map.t[idx(x, y)].owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
// a few rooms on the dungeon side for the look of it
|
||||
for (x0, y0, x1, y1, k) in [(8, 13, 13, 17, Kind::Lair), (8, 30, 13, 34, Kind::Training), (17, 12, 21, 15, Kind::Farm)] {
|
||||
for y in y0..=y1 {
|
||||
for x in x0..=x1 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
if t.kind == Kind::Floor && t.owner == Owner::Player {
|
||||
t.kind = k;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let towers = vec![
|
||||
(RIVER_X - 4, BRIDGE_Y - 3, TowerKind::Arrow, false, 0.0),
|
||||
(RIVER_X - 4, BRIDGE_Y + 5, TowerKind::Cannon, false, 0.0),
|
||||
(RIVER_X - 8, BRIDGE_Y + 1, TowerKind::Magic, false, 0.0),
|
||||
(RIVER_X + 7, BRIDGE_Y - 3, TowerKind::Arrow, true, 0.0),
|
||||
(RIVER_X + 7, BRIDGE_Y + 5, TowerKind::Cannon, true, 0.0),
|
||||
(RIVER_X + 11, BRIDGE_Y + 1, TowerKind::Arrow, true, 0.0),
|
||||
];
|
||||
g.demo = Some(Demo { spawn_t: 0.0, towers, rng: Rng(seed.wrapping_mul(747796405) | 1) });
|
||||
g.gate_hp = 0.0;
|
||||
g.gate_on = false;
|
||||
g.mana = MANA_MAX;
|
||||
g.place_all_towers_of_demo();
|
||||
g.place_all_torches();
|
||||
// start with a fight already going on
|
||||
for _ in 0..6 {
|
||||
g.demo_spawn(true);
|
||||
g.demo_spawn(false);
|
||||
}
|
||||
let mut cam = Cam::new(RIVER_X as f32 + 2.0, BRIDGE_Y as f32 + 1.5);
|
||||
cam.zoom = 16.0;
|
||||
(g, cam)
|
||||
}
|
||||
|
||||
fn place_all_towers_of_demo(&mut self) {
|
||||
let Some(d) = self.demo.as_ref() else { return };
|
||||
let slots: Vec<(i32, i32, TowerKind, bool)> = d.towers.iter().map(|s| (s.0, s.1, s.2, s.3)).collect();
|
||||
for (x, y, kind, hero) in slots {
|
||||
self.demo_tower(x, y, kind, hero);
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_tower(&mut self, x: i32, y: i32, kind: TowerKind, hero: bool) {
|
||||
let i = idx(x, y);
|
||||
if self.towers.iter().any(|t| t.tile == i) {
|
||||
return;
|
||||
}
|
||||
let s = tower_stats(kind);
|
||||
let t = &mut self.map.t[i];
|
||||
t.kind = Kind::Tower;
|
||||
t.owner = if hero { Owner::Hero } else { Owner::Player };
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
let level = 1 + (self.demo.as_mut().map_or(0, |d| d.rng.next() % 2));
|
||||
let mut tw = Tower { id, kind, tile: i, level, hp: s.hp, cd: 1.0, aim: 0.0, flash: 0.0, hero, rune_final: false };
|
||||
tw.hp = tw.max_hp();
|
||||
self.towers.push(tw);
|
||||
self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.6, 0xff40d0ff);
|
||||
}
|
||||
|
||||
// a new unit at the far end of its side
|
||||
fn demo_spawn(&mut self, creature: bool) {
|
||||
let Some(d) = self.demo.as_mut() else { return };
|
||||
let kind = pick(&mut d.rng, if creature { &CREATURES } else { &KNIGHTS });
|
||||
let level = 2 + d.rng.next() % 6;
|
||||
let y = BRIDGE_Y as f32 + 1.5 + (d.rng.f() - 0.5) * 14.0;
|
||||
let x = if creature { 9.0 + d.rng.f() * 6.0 } else { W as f32 - 15.0 + d.rng.f() * 6.0 };
|
||||
if !self.map.walk(x as i32, y as i32) {
|
||||
return;
|
||||
}
|
||||
let id = self.spawn(kind, !creature, x, y, level);
|
||||
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.happy = 1.0;
|
||||
e.settled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// the demo instead of the normal game: units, towers, reinforcements, rebuilding
|
||||
pub fn demo_step(&mut self, dt: f32) {
|
||||
self.time += dt;
|
||||
self.core_hp = CORE_HP;
|
||||
self.mana = MANA_MAX;
|
||||
self.update_units(dt);
|
||||
self.update_towers(dt);
|
||||
let Some(mut d) = self.demo.take() else { return };
|
||||
// reinforcements: the weaker side gets the next unit
|
||||
d.spawn_t -= dt;
|
||||
let mut want = None;
|
||||
if d.spawn_t <= 0.0 {
|
||||
d.spawn_t = 1.0;
|
||||
let side = |hero: bool| -> (usize, f32) {
|
||||
let list: Vec<(EK, u32)> = self.ents.iter().filter(|e| e.alive && e.hero == hero && e.captive == 0).map(|e| (e.kind, e.level)).collect();
|
||||
(list.len(), Game::squad_value(&list))
|
||||
};
|
||||
let (nc, vc) = side(false);
|
||||
let (nk, vk) = side(true);
|
||||
if nc < PER_SIDE && (vc <= vk || nk >= PER_SIDE) {
|
||||
want = Some(true);
|
||||
} else if nk < PER_SIDE {
|
||||
want = Some(false);
|
||||
}
|
||||
}
|
||||
// destroyed towers come back after a while
|
||||
let mut rebuild = Vec::new();
|
||||
for s in d.towers.iter_mut() {
|
||||
let i = idx(s.0, s.1);
|
||||
if self.towers.iter().any(|t| t.tile == i) {
|
||||
s.4 = 0.0;
|
||||
} else {
|
||||
s.4 += dt;
|
||||
if s.4 >= REBUILD {
|
||||
s.4 = 0.0;
|
||||
rebuild.push((s.0, s.1, s.2, s.3));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.demo = Some(d);
|
||||
if let Some(creature) = want {
|
||||
self.demo_spawn(creature);
|
||||
}
|
||||
for (x, y, kind, hero) in rebuild {
|
||||
// nobody may stand on the spot
|
||||
let blocked = self.ents.iter().any(|e| e.alive && e.tile() == idx(x, y));
|
||||
if !blocked {
|
||||
self.demo_tower(x, y, kind, hero);
|
||||
}
|
||||
}
|
||||
// the battle never ends
|
||||
self.state = 0;
|
||||
self.msgs.clear();
|
||||
}
|
||||
|
||||
// demo units: fight what is close, tear down enemy towers, otherwise push over the bridge
|
||||
pub fn demo_ai(&mut self, e: &mut Ent, dt: f32) {
|
||||
if e.captive != 0 {
|
||||
e.alive = false;
|
||||
return;
|
||||
}
|
||||
if e.think <= 0.0 {
|
||||
e.think = 0.5;
|
||||
// the heavy ones go for the towers first
|
||||
let breaker = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Dwarf | EK::Gargoyle);
|
||||
if !matches!(e.job, Job::Attack(_) | Job::AttackTower(_)) {
|
||||
let enemy = if breaker { None } else { self.nearest_enemy(e, 7.0) };
|
||||
if let Some(id) = enemy {
|
||||
e.job = Job::Attack(id);
|
||||
e.path.clear();
|
||||
e.repath = 0.0;
|
||||
} else {
|
||||
let (x, y) = (e.x, e.y);
|
||||
let tower = self.towers.iter().filter(|t| t.hero != e.hero).min_by(|a, b| {
|
||||
let da = (a.pos().0 - x).powi(2) + (a.pos().1 - y).powi(2);
|
||||
let db = (b.pos().0 - x).powi(2) + (b.pos().1 - y).powi(2);
|
||||
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
if let Some(t) = tower {
|
||||
let (tx, ty) = t.pos();
|
||||
if (tx - x).powi(2) + (ty - y).powi(2) < 8.0 * 8.0 {
|
||||
e.job = Job::AttackTower(t.id);
|
||||
e.path.clear();
|
||||
e.repath = 0.0;
|
||||
}
|
||||
}
|
||||
if breaker && e.job == Job::None {
|
||||
if let Some(id) = self.nearest_enemy(e, 7.0) {
|
||||
e.job = Job::Attack(id);
|
||||
e.path.clear();
|
||||
e.repath = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match e.job {
|
||||
Job::Attack(id) => {
|
||||
if self.attack_job(e, id, dt) {
|
||||
return;
|
||||
}
|
||||
e.job = Job::None;
|
||||
}
|
||||
Job::AttackTower(id) => {
|
||||
if self.attack_tower(e, id, dt) {
|
||||
return;
|
||||
}
|
||||
e.job = Job::None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// march towards the far bank
|
||||
if e.path.is_empty() && e.repath <= 0.0 {
|
||||
e.repath = 2.0;
|
||||
let d = self.demo.as_mut();
|
||||
let r = d.map_or(0.5, |d| d.rng.f());
|
||||
let tx = if e.hero { 12 + (r * 8.0) as i32 } else { W as i32 - 20 + (r * 8.0) as i32 };
|
||||
let ty = BRIDGE_Y + 1 + ((r * 97.0).fract() * 12.0) as i32 - 6;
|
||||
if self.map.walk(tx, ty) {
|
||||
let p = if flies(e.kind) { self.map.fly_path_to(e.tile(), idx(tx, ty)) } else { self.map.path_to(e.tile(), idx(tx, ty)) };
|
||||
if let Some(p) = p {
|
||||
e.path = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.step_path(e, dt);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_battle_goes_on() {
|
||||
let (mut g, _) = Game::new_demo(7);
|
||||
let mut kills = 0;
|
||||
let mut rebuilt = false;
|
||||
let mut lost = false;
|
||||
for k in 0..(20 * 240) {
|
||||
let before = g.ents.len();
|
||||
let towers = g.towers.len();
|
||||
g.update(0.05);
|
||||
kills += before.saturating_sub(g.ents.len());
|
||||
lost |= g.towers.len() < 6;
|
||||
rebuilt |= lost && g.towers.len() > towers;
|
||||
if k % (20 * 30) == 0 {
|
||||
let c = g.ents.iter().filter(|e| e.alive && !e.hero).count();
|
||||
let h = g.ents.iter().filter(|e| e.alive && e.hero).count();
|
||||
println!("{:>3} s: {} creatures, {} knights, {} towers, {} fallen", k / 20, c, h, g.towers.len(), kills);
|
||||
}
|
||||
}
|
||||
assert_eq!(g.state, 0);
|
||||
assert!(kills > 10, "they fight: {} fallen", kills);
|
||||
assert!(g.ents.iter().any(|e| e.alive && e.hero) && g.ents.iter().any(|e| e.alive && !e.hero), "both sides are still there");
|
||||
println!("towers lost {}, rebuilt {}", lost, rebuilt);
|
||||
assert!(rebuilt, "lost towers are rebuilt");
|
||||
}
|
||||
}
|
||||
+27
-14
@@ -445,6 +445,8 @@ pub struct Game {
|
||||
pub gate_on: bool, // free game: the humans' gate exists
|
||||
#[serde(skip)]
|
||||
pub springs: usize, // claimed mana springs (recounted)
|
||||
#[serde(skip)]
|
||||
pub demo: Option<crate::demo::Demo>, // the endless bridge battle behind the title menu
|
||||
#[serde(default)]
|
||||
pub dwarf_caves: Vec<crate::dwarves::DwarfCave>,
|
||||
#[serde(default)]
|
||||
@@ -603,6 +605,7 @@ impl Game {
|
||||
gate_on: true,
|
||||
springs: 0,
|
||||
freed: 0,
|
||||
demo: None,
|
||||
dwarf_caves: Vec::new(),
|
||||
dwarf_t: 30.0,
|
||||
rune_free: 0,
|
||||
@@ -964,7 +967,25 @@ impl Game {
|
||||
self.fx.retain(|f| f.t < f.max);
|
||||
}
|
||||
|
||||
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 mut e = std::mem::replace(&mut self.ents[i], Ent::dummy());
|
||||
let (x0, y0) = (e.x, e.y);
|
||||
self.update_ent(&mut e, dt);
|
||||
self.turn(&mut e, x0, y0);
|
||||
self.ents[i] = e;
|
||||
}
|
||||
self.ents.retain(|e| e.alive);
|
||||
}
|
||||
|
||||
fn step(&mut self, dt: f32) {
|
||||
if self.demo.is_some() {
|
||||
self.demo_step(dt);
|
||||
return;
|
||||
}
|
||||
self.time += dt;
|
||||
self.recount();
|
||||
self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * dt).min(self.mana_max());
|
||||
@@ -1014,17 +1035,7 @@ impl Game {
|
||||
self.reinforce_corners();
|
||||
}
|
||||
self.update_contested();
|
||||
for i in 0..self.ents.len() {
|
||||
if !self.ents[i].alive || self.ents[i].held {
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
self.turn(&mut e, x0, y0);
|
||||
self.ents[i] = e;
|
||||
}
|
||||
self.ents.retain(|e| e.alive);
|
||||
self.update_units(dt);
|
||||
self.update_towers(dt);
|
||||
if !self.rivals.is_empty() {
|
||||
self.update_rivals(dt);
|
||||
@@ -1611,6 +1622,8 @@ impl Game {
|
||||
self.sfx(Sfx::Convert, e.x, e.y);
|
||||
self.msg("msg.creature_freed", vec![format!("@{}", st(e.kind).name)]);
|
||||
}
|
||||
} else if self.demo.is_some() {
|
||||
self.demo_ai(e, dt);
|
||||
} else if e.hero && e.captive != 0 {
|
||||
self.captive_update(e, dt);
|
||||
} else if e.rival() {
|
||||
@@ -1934,7 +1947,7 @@ impl Game {
|
||||
cap.saturating_sub(used)
|
||||
}
|
||||
|
||||
fn kill(&mut self, j: usize) {
|
||||
pub(crate) fn kill(&mut self, j: usize) {
|
||||
let e = &self.ents[j];
|
||||
if e.hero && e.captive == 0 && self.prison_space() > 0 {
|
||||
// knocked out instead of killed: imps carry the hero to the torture chamber
|
||||
@@ -4064,7 +4077,7 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_towers(&mut self, dt: f32) {
|
||||
pub(crate) fn update_towers(&mut self, dt: f32) {
|
||||
for k in 0..self.towers.len() {
|
||||
if k >= self.towers.len() {
|
||||
break;
|
||||
@@ -4227,7 +4240,7 @@ impl Game {
|
||||
}
|
||||
|
||||
// a hero attacks a tower; false = tower gone or unreachable
|
||||
fn attack_tower(&mut self, e: &mut Ent, id: u32, dt: f32) -> bool {
|
||||
pub(crate) fn attack_tower(&mut self, e: &mut Ent, id: u32, dt: f32) -> bool {
|
||||
let Some(k) = self.towers.iter().position(|t| t.id == id) else { return false };
|
||||
let (tx, ty) = self.towers[k].pos();
|
||||
let hit_h = self.towers[k].top() * 0.6;
|
||||
|
||||
+12
-2
@@ -3,6 +3,7 @@
|
||||
mod audio;
|
||||
mod campaign;
|
||||
mod cloud;
|
||||
mod demo;
|
||||
mod dwarves;
|
||||
mod fortress;
|
||||
mod game;
|
||||
@@ -374,7 +375,8 @@ async fn main() {
|
||||
menu.has_autosave = platform::storage_get(save::AUTOSAVE_KEY).is_some();
|
||||
|
||||
let seed = (miniquad::date::now() * 1000.0) as u32 | 1;
|
||||
let (game, cam) = new_game(seed, &settings);
|
||||
// behind the title menu: the endless bridge battle
|
||||
let (game, cam) = Game::new_demo(seed);
|
||||
let mut app = App { game, cam, settings, seed, status: None, recorded: false };
|
||||
menu.progress = platform::storage_get(PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default();
|
||||
app.game.hints = app.settings.hints;
|
||||
@@ -814,6 +816,8 @@ async fn main() {
|
||||
menu.has_autosave = true;
|
||||
}
|
||||
menu.page = Some(Page::Title);
|
||||
app.seed = next_seed(app.seed);
|
||||
(app.game, app.cam) = Game::new_demo(app.seed);
|
||||
}
|
||||
MenuAction::None | MenuAction::Consumed => {}
|
||||
}
|
||||
@@ -948,7 +952,13 @@ async fn main() {
|
||||
app.game.hints = app.settings.hints;
|
||||
}
|
||||
|
||||
// ---------------- update (the world stands still while menus or dialogs are open)
|
||||
// ---------------- update (the world stands still while menus or dialogs are open;
|
||||
// the bridge battle behind the title menu goes on, the camera slowly circles it)
|
||||
if app.game.demo.is_some() && !playing {
|
||||
app.game.update(dt);
|
||||
app.cam.yaw += dt * 0.03;
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user