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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 06:34:09 +01:00
1 parent 925be17e82
commit f710a19351
12 files changed
+588 -137

No files matched your search

+56 -2
View File
@@ -3,6 +3,7 @@
use crate::gfx::*;
use crate::i18n::{languages, tr, trf};
use crate::map::{MAX_SIZE, MIN_SIZE};
use crate::settings::{all_actions, key_name, Action, Settings};
use crate::ui::{tool_info, CH, CW, DIM, FRAME, FRAME_HI, GOLD, S, TOOLS, TXT};
use macroquad::prelude::*;
@@ -12,6 +13,7 @@ pub enum Page {
Title,
Pause,
Settings { tab: usize, from_title: bool },
NewGame,
KeyCapture { action: usize, from_title: bool },
}
@@ -150,6 +152,9 @@ enum Btn {
Back,
Tab(usize),
Change(usize, i32),
MapW(i32),
MapH(i32),
Start,
Key(usize),
ResetKeys,
}
@@ -159,7 +164,7 @@ pub enum MenuAction {
Consumed,
Continue, // load the autosave
Resume,
NewGame,
NewGame, // start a new game with the chosen map size
Load,
Save,
Help,
@@ -195,6 +200,7 @@ impl Menu {
Some(Page::KeyCapture { from_title, .. }) => Some(Page::Settings { tab: 4, from_title }),
Some(Page::Settings { from_title: true, .. }) => Some(Page::Title),
Some(Page::Settings { from_title: false, .. }) => Some(Page::Pause),
Some(Page::NewGame) => Some(Page::Title),
Some(Page::Pause) => None,
Some(Page::Title) => Some(Page::Title),
None => Some(Page::Pause),
@@ -227,7 +233,19 @@ impl Menu {
self.page = None;
MenuAction::Resume
}
Some(Btn::New) => MenuAction::NewGame,
Some(Btn::New) => {
self.page = Some(Page::NewGame);
MenuAction::Consumed
}
Some(Btn::Start) => MenuAction::NewGame,
Some(Btn::MapW(d)) => {
settings.map_w = (settings.map_w as i32 + d * 32).clamp(MIN_SIZE as i32, MAX_SIZE as i32) as usize;
MenuAction::SettingsChanged
}
Some(Btn::MapH(d)) => {
settings.map_h = (settings.map_h as i32 + d * 32).clamp(MIN_SIZE as i32, MAX_SIZE as i32) as usize;
MenuAction::SettingsChanged
}
Some(Btn::Load) => MenuAction::Load,
Some(Btn::Save) => MenuAction::Save,
Some(Btn::Help) => MenuAction::Help,
@@ -273,6 +291,7 @@ impl Menu {
match page {
Page::Title => self.draw_title(gfx),
Page::Pause => self.draw_pause(gfx),
Page::NewGame => self.draw_new_game(gfx, settings),
Page::Settings { tab, .. } => self.draw_settings(gfx, settings, tab),
Page::KeyCapture { action, .. } => {
self.draw_settings(gfx, settings, 4);
@@ -334,6 +353,41 @@ impl Menu {
gfx.text(&credit, (sw - gfx.measure(&credit, (S() / 2.0).max(1.0))) * 0.5, sh - CH() * 6.5, (S() / 2.0).max(1.0), DIM);
}
// New game: choose the map size (width and height separately)
fn draw_new_game(&mut self, gfx: &Gfx, s: &Settings) {
let sw = screen_width();
let sh = screen_height();
let (mx, my) = mouse_position();
let cols = 44;
let rows = 17;
let x = (sw - cols as f32 * CW()) * 0.5;
let y = ((sh - rows as f32 * CH()) * 0.5).max(0.0);
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &tr("menu.new"), FRAME_HI);
let lines = [("newgame.width", s.map_w, 0), ("newgame.height", s.map_h, 1)];
for (k, &(key, val, which)) in lines.iter().enumerate() {
let by = y + CH() * (2.0 + k as f32 * 2.5);
gfx.text(&tr(key), x + CW() * 3.0, by + CH() * 0.4, S(), TXT);
let vw = 14.0 * CW();
let vx = x + cols as f32 * CW() - vw - CW() * 3.0;
let left = Rect::new(vx, by, CW() * 2.5, CH() * 1.8);
let right = Rect::new(vx + vw - CW() * 2.5, by, CW() * 2.5, CH() * 1.8);
button(gfx, left, "<", left.contains(vec2(mx, my)), FRAME);
button(gfx, right, ">", right.contains(vec2(mx, my)), FRAME);
let v = val.to_string();
gfx.text(&v, vx + (vw - gfx.measure(&v, S())) * 0.5, by + CH() * 0.4, S(), GOLD);
let (l, r) = if which == 0 { (Btn::MapW(-1), Btn::MapW(1)) } else { (Btn::MapH(-1), Btn::MapH(1)) };
self.buttons.push((left, l));
self.buttons.push((right, r));
}
// what the size means for the game
let area = (s.map_w * s.map_h) as f32 / 4096.0;
let portals = (area.round() as usize).max(1);
let info = trf("newgame.info", &[portals.to_string(), (portals - 1).to_string()]);
gfx.text(&info, x + CW() * 3.0, y + CH() * 7.5, S(), DIM);
let w = (cols - 6) as f32 * CW();
self.column(gfx, x + CW() * 3.0, y + CH() * 9.5, w, &[(Btn::Start, tr("newgame.start")), (Btn::Back, tr("menu.back"))]);
}
fn draw_pause(&mut self, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();