Configurable map size (64-256, non-square), one portal per 64x64 tiles, guarded treasure caves, glowing dig-mark blocks
- New Game dialog with map width/height (steps of 32), remembered in settings - map dimensions are per game; renderer chunks, minimap and camera follow them - content scales with area; grace period grows with the distance to the Hero Gate - treasure caves with rich gold, guarded by hero squads that hold their post - marked blocks: translucent yellow box with outline on all edges - savegame v2: packed run-length encoded tiles (256x256 map ~320 KB), v1 still loads - tests: savegame round trip on 96x128, 256x256 generation/simulation/save Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
925be17e82
commit
f710a19351
12 files changed
+588
-137
No files matched your search
+56
-17
@@ -159,7 +159,9 @@ pub struct Ent {
|
||||
pub face: f32,
|
||||
pub moving: bool,
|
||||
pub working: bool,
|
||||
pub guard: bool, // gate defender
|
||||
pub guard: bool, // stays at its home (gate defenders, treasure guards)
|
||||
#[serde(default)]
|
||||
pub home: Option<(f32, f32)>, // guard post
|
||||
}
|
||||
|
||||
impl Ent {
|
||||
@@ -193,10 +195,11 @@ impl Ent {
|
||||
moving: false,
|
||||
working: false,
|
||||
guard: false,
|
||||
home: None,
|
||||
}
|
||||
}
|
||||
pub fn tile(&self) -> usize {
|
||||
idx((self.x as i32).clamp(0, MW as i32 - 1), (self.y as i32).clamp(0, MH as i32 - 1))
|
||||
idx((self.x as i32).clamp(0, mw() as i32 - 1), (self.y as i32).clamp(0, mh() as i32 - 1))
|
||||
}
|
||||
pub fn atk(&self) -> f32 {
|
||||
st(self.kind).atk * (1.0 + 0.12 * (self.level - 1) as f32)
|
||||
@@ -293,8 +296,8 @@ fn sgn(v: f32) -> f32 {
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new(seed: u32) -> Game {
|
||||
let map = Map::generate(seed);
|
||||
pub fn new(seed: u32, w: usize, h: usize) -> Game {
|
||||
let map = Map::generate(seed, w, h);
|
||||
let mut g = Game {
|
||||
map,
|
||||
ents: Vec::new(),
|
||||
@@ -330,11 +333,28 @@ impl Game {
|
||||
g.spawn(EK::Worker, false, cx as f32 - 0.5, cy as f32 + k as f32 * 0.7 + 0.3, 1);
|
||||
}
|
||||
g.recount();
|
||||
for i in 0..N {
|
||||
for i in 0..n() {
|
||||
if g.map.t[i].owner == Owner::Player {
|
||||
g.place_torches(i);
|
||||
}
|
||||
}
|
||||
// heroes take longer to reach the core on big maps: longer grace period
|
||||
let (gx, gy) = g.map.gate;
|
||||
let dist = ((gx - cx).abs() + (gy - cy).abs()) as f32;
|
||||
g.wave_timer = FIRST_WAVE + ((dist - 45.0) * 1.5).clamp(0.0, 300.0);
|
||||
// treasure caves are guarded by heroes
|
||||
for k in 0..g.map.treasures.len() {
|
||||
let (tx, ty) = g.map.treasures[k];
|
||||
let squad = [EK::Knight, EK::Archer, EK::Dwarf, if k % 2 == 0 { EK::Priest } else { EK::Knight }];
|
||||
for (j, &kind) in squad.iter().enumerate() {
|
||||
let x = tx as f32 + 0.5 + (j as f32 - 1.5) * 0.6;
|
||||
let id = g.spawn(kind, true, x, ty as f32 + 0.5, 2);
|
||||
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.guard = true;
|
||||
e.home = Some((tx as f32 + 0.5, ty as f32 + 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
g.msg("msg.welcome1", vec![]);
|
||||
g.msg("msg.welcome2", vec![]);
|
||||
g.msg("msg.welcome3", vec![]);
|
||||
@@ -428,8 +448,19 @@ impl Game {
|
||||
acc
|
||||
}
|
||||
|
||||
pub fn portals_owned(&self) -> usize {
|
||||
(0..self.map.portals.len()).filter(|&k| self.map.portal_owned(k)).count()
|
||||
}
|
||||
|
||||
// gate defenders alive (not the treasure guards)
|
||||
pub fn gate_defenders_alive(&self) -> bool {
|
||||
let (gx, gy) = self.gate_center();
|
||||
self.ents.iter().any(|e| e.alive && e.guard && e.home.map_or(false, |(x, y)| (x - gx).abs() < 1.0 && (y - gy).abs() < 1.0))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn portal_owned(&self) -> bool {
|
||||
let (px, py) = self.map.portal;
|
||||
let Some(&(px, py)) = self.map.portals.first() else { return false };
|
||||
self.map.t[idx(px, py)].owner == Owner::Player
|
||||
}
|
||||
|
||||
@@ -499,10 +530,11 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
if self.portal_owned() {
|
||||
let owned = self.portals_owned();
|
||||
if owned > 0 {
|
||||
self.portal_timer -= dt;
|
||||
if self.portal_timer <= 0.0 {
|
||||
self.portal_timer = 20.0 + self.rng.f() * 14.0;
|
||||
self.portal_timer = (20.0 + self.rng.f() * 14.0) / (owned as f32).sqrt();
|
||||
self.try_portal();
|
||||
}
|
||||
}
|
||||
@@ -589,7 +621,9 @@ impl Game {
|
||||
}
|
||||
r -= w;
|
||||
}
|
||||
let (px, py) = self.map.portal;
|
||||
let owned: Vec<usize> = (0..self.map.portals.len()).filter(|&k| self.map.portal_owned(k)).collect();
|
||||
let pk = owned[(self.rng.next() as usize) % owned.len()];
|
||||
let (px, py) = self.map.portals[pk];
|
||||
self.spawn(kind, false, px as f32 + 1.5, py as f32 + 1.5, 1);
|
||||
self.fx(3, px as f32 + 1.5, py as f32 + 1.5, 0.0, 0.0, 0.8, 0xffcc44ff);
|
||||
self.sfx(Sfx::PortalArrive, px as f32 + 1.5, py as f32 + 1.5);
|
||||
@@ -670,8 +704,12 @@ impl Game {
|
||||
let x = gx as f32 + 0.5 + self.rng.f() * 2.0;
|
||||
let y = gy as f32 + 0.5 + self.rng.f() * 2.0;
|
||||
let id = self.spawn(k, true, x, y, l);
|
||||
let home = (gx as f32 + 1.5, gy as f32 + 1.5);
|
||||
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.guard = guard;
|
||||
if guard {
|
||||
e.home = Some(home);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.fx(3, gx as f32 + 1.5, gy as f32 + 1.5, 0.0, 0.0, 1.0, 0xffffeecc);
|
||||
@@ -1120,8 +1158,9 @@ impl Game {
|
||||
if e.timer >= 1.0 {
|
||||
self.unreserve(me);
|
||||
e.job = Job::None;
|
||||
if self.map.t[t].kind == Kind::Portal {
|
||||
let (px, py) = self.map.portal;
|
||||
let (tx, ty) = xy(t);
|
||||
if let (Kind::Portal, Some(pk)) = (self.map.t[t].kind, self.map.portal_at(tx, ty)) {
|
||||
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;
|
||||
@@ -1569,7 +1608,7 @@ impl Game {
|
||||
}
|
||||
}
|
||||
if let Job::Attack(id) = e.job {
|
||||
let (gx, gy) = self.gate_center();
|
||||
let (gx, gy) = e.home.unwrap_or_else(|| self.gate_center());
|
||||
let far = e.guard && (e.x - gx).powi(2) + (e.y - gy).powi(2) > 14.0 * 14.0;
|
||||
if !far && self.attack_job(e, id, dt) {
|
||||
return;
|
||||
@@ -1579,8 +1618,8 @@ impl Game {
|
||||
e.think = 1.5;
|
||||
}
|
||||
if e.guard {
|
||||
// gate defenders stay at the gate
|
||||
let (gx, gy) = self.gate_center();
|
||||
// guards stay at their post (gate or treasure cave)
|
||||
let (gx, gy) = e.home.unwrap_or_else(|| self.gate_center());
|
||||
let d2 = (e.x - gx).powi(2) + (e.y - gy).powi(2);
|
||||
if d2 > 2.5 * 2.5 && e.path.is_empty() && e.repath <= 0.0 {
|
||||
e.repath = 1.0;
|
||||
@@ -1854,9 +1893,9 @@ mod tests {
|
||||
#[test]
|
||||
fn simulate_long_game() {
|
||||
for seed in [1u32, 7, 12345] {
|
||||
let mut g = Game::new(seed);
|
||||
let mut g = Game::new(seed, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
let (px, py) = g.map.portal;
|
||||
let (px, py) = g.map.portals[0];
|
||||
// dig a way to the portal (through earth/gold, around rock)
|
||||
let start = idx(cx - 1, cy + 1);
|
||||
let goal = idx(px + 1, py + 1);
|
||||
@@ -1912,7 +1951,7 @@ mod tests {
|
||||
g.army_value()
|
||||
);
|
||||
}
|
||||
if g.portal_owned() && step % 20 == 0 && !portal_logged {
|
||||
if g.portals_owned() > 0 && step % 20 == 0 && !portal_logged {
|
||||
portal_logged = true;
|
||||
println!("seed {} portal claimed after {:.0}s", seed, g.time);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user