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
+585
-134
No files matched your search
@@ -18,6 +18,7 @@ An original dungeon management game (genre: dungeon building & defence) that run
|
|||||||
- Textures: **procedural pixel art** (tile atlas)
|
- Textures: **procedural pixel art** (tile atlas)
|
||||||
- Units: **pixel sprites as billboards** (always facing the camera)
|
- Units: **pixel sprites as billboards** (always facing the camera)
|
||||||
- Lighting: **dark + torches** – dark base level; claimed areas and light sources (torches, core, portal, gate) are bright, flickering light
|
- Lighting: **dark + torches** – dark base level; claimed areas and light sources (torches, core, portal, gate) are bright, flickering light
|
||||||
|
- **Dig marking**: a marked block glows as a translucent yellow box with a yellow outline around all edges
|
||||||
- **Tile outlines**: subtle dark lines around every tile (floors, wall tops, vertical wall edges), following the irregular surface; toggle with G (later in the graphics settings)
|
- **Tile outlines**: subtle dark lines around every tile (floors, wall tops, vertical wall edges), following the irregular surface; toggle with G (later in the graphics settings)
|
||||||
- The UI is a 2D layer on top, including a minimap
|
- The UI is a 2D layer on top, including a minimap
|
||||||
|
|
||||||
@@ -43,6 +44,13 @@ An original dungeon management game (genre: dungeon building & defence) that run
|
|||||||
- Music changes with **soft crossfades** (2.5 s): title → build → battle when heroes invade → storm music while attacking the gate → jingles
|
- Music changes with **soft crossfades** (2.5 s): title → build → battle when heroes invade → storm music while attacking the gate → jingles
|
||||||
- Sound effects are **synthesised chip sounds** (`src/sfx.rs`), positioned by distance and stereo panning
|
- Sound effects are **synthesised chip sounds** (`src/sfx.rs`), positioned by distance and stereo panning
|
||||||
|
|
||||||
|
## Map size
|
||||||
|
- Chosen in the **New Game dialog**: width and height separately, **64–256 tiles** in steps of 32 (remembered in the settings)
|
||||||
|
- Content scales with the area: rock, gold veins and caves; the Hero Gate lies on the far side, the **grace period grows with the distance** to the gate
|
||||||
|
- **One portal per 64×64 tiles**; every claimed portal brings creatures (more portals = faster arrivals, still limited by the lair)
|
||||||
|
- Bigger maps get **guarded treasure caves** (one less than the number of portals): very rich gold veins, defended by a hero squad that stays at its post
|
||||||
|
- Savegames store the tiles packed (run-length encoded, base64), so even 256×256 maps stay small (~300 KB); version 1 savegames still load
|
||||||
|
|
||||||
## Game content
|
## Game content
|
||||||
- **Digging**: mark earth/gold → Pit Imps dig, bring gold home and claim ground
|
- **Digging**: mark earth/gold → Pit Imps dig, bring gold home and claim ground
|
||||||
- **Rooms**: treasury, lair, mushroom garden, training hall, library
|
- **Rooms**: treasury, lair, mushroom garden, training hall, library
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Dig out your underground realm, claim the portal to attract creatures, keep them
|
|||||||
- 3D isometric view with a freely rotatable and tiltable camera
|
- 3D isometric view with a freely rotatable and tiltable camera
|
||||||
- Procedural pixel art, irregular cave walls, flickering torch light
|
- Procedural pixel art, irregular cave walls, flickering torch light
|
||||||
- CP437 text-mode style interface with the DINOBYTE bitmap font
|
- CP437 text-mode style interface with the DINOBYTE bitmap font
|
||||||
|
- Map sizes from 64×64 to 256×256 tiles (one portal per 64×64 tiles, guarded treasure caves on bigger maps)
|
||||||
- English and German (language files in `lang/`, easy to extend)
|
- English and German (language files in `lang/`, easy to extend)
|
||||||
|
|
||||||
See [DESIGN.md](DESIGN.md) for all design decisions and [CREDITS.md](CREDITS.md) for third-party assets.
|
See [DESIGN.md](DESIGN.md) for all design decisions and [CREDITS.md](CREDITS.md) for third-party assets.
|
||||||
|
|||||||
@@ -266,3 +266,9 @@ save.loaded = Spiel geladen.
|
|||||||
save.err_invalid = Diese Datei ist kein gültiger DungeonCraft-Spielstand.
|
save.err_invalid = Diese Datei ist kein gültiger DungeonCraft-Spielstand.
|
||||||
save.err_version = Dieser Spielstand stammt aus einer neueren DungeonCraft-Version.
|
save.err_version = Dieser Spielstand stammt aus einer neueren DungeonCraft-Version.
|
||||||
save.choose = Spielstand-Datei auswählen ...
|
save.choose = Spielstand-Datei auswählen ...
|
||||||
|
|
||||||
|
# ---- Neues Spiel
|
||||||
|
newgame.width = Kartenbreite
|
||||||
|
newgame.height = Kartenhöhe
|
||||||
|
newgame.start = Starten
|
||||||
|
newgame.info = {0} Portal(e), {1} bewachte Schatzhöhle(n)
|
||||||
@@ -267,3 +267,9 @@ save.loaded = Game loaded.
|
|||||||
save.err_invalid = This file is not a valid DungeonCraft savegame.
|
save.err_invalid = This file is not a valid DungeonCraft savegame.
|
||||||
save.err_version = This savegame comes from a newer version of DungeonCraft.
|
save.err_version = This savegame comes from a newer version of DungeonCraft.
|
||||||
save.choose = Choose a savegame file ...
|
save.choose = Choose a savegame file ...
|
||||||
|
|
||||||
|
# ---- new game
|
||||||
|
newgame.width = Map width
|
||||||
|
newgame.height = Map height
|
||||||
|
newgame.start = Start
|
||||||
|
newgame.info = {0} portal(s), {1} guarded treasure cave(s)
|
||||||
+56
-17
@@ -159,7 +159,9 @@ pub struct Ent {
|
|||||||
pub face: f32,
|
pub face: f32,
|
||||||
pub moving: bool,
|
pub moving: bool,
|
||||||
pub working: 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 {
|
impl Ent {
|
||||||
@@ -193,10 +195,11 @@ impl Ent {
|
|||||||
moving: false,
|
moving: false,
|
||||||
working: false,
|
working: false,
|
||||||
guard: false,
|
guard: false,
|
||||||
|
home: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn tile(&self) -> usize {
|
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 {
|
pub fn atk(&self) -> f32 {
|
||||||
st(self.kind).atk * (1.0 + 0.12 * (self.level - 1) as 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 {
|
impl Game {
|
||||||
pub fn new(seed: u32) -> Game {
|
pub fn new(seed: u32, w: usize, h: usize) -> Game {
|
||||||
let map = Map::generate(seed);
|
let map = Map::generate(seed, w, h);
|
||||||
let mut g = Game {
|
let mut g = Game {
|
||||||
map,
|
map,
|
||||||
ents: Vec::new(),
|
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.spawn(EK::Worker, false, cx as f32 - 0.5, cy as f32 + k as f32 * 0.7 + 0.3, 1);
|
||||||
}
|
}
|
||||||
g.recount();
|
g.recount();
|
||||||
for i in 0..N {
|
for i in 0..n() {
|
||||||
if g.map.t[i].owner == Owner::Player {
|
if g.map.t[i].owner == Owner::Player {
|
||||||
g.place_torches(i);
|
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.welcome1", vec![]);
|
||||||
g.msg("msg.welcome2", vec![]);
|
g.msg("msg.welcome2", vec![]);
|
||||||
g.msg("msg.welcome3", vec![]);
|
g.msg("msg.welcome3", vec![]);
|
||||||
@@ -428,8 +448,19 @@ impl Game {
|
|||||||
acc
|
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 {
|
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
|
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;
|
self.portal_timer -= dt;
|
||||||
if self.portal_timer <= 0.0 {
|
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();
|
self.try_portal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -589,7 +621,9 @@ impl Game {
|
|||||||
}
|
}
|
||||||
r -= w;
|
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.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.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);
|
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 x = gx as f32 + 0.5 + self.rng.f() * 2.0;
|
||||||
let y = gy 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 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) {
|
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
|
||||||
e.guard = guard;
|
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);
|
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 {
|
if e.timer >= 1.0 {
|
||||||
self.unreserve(me);
|
self.unreserve(me);
|
||||||
e.job = Job::None;
|
e.job = Job::None;
|
||||||
if self.map.t[t].kind == Kind::Portal {
|
let (tx, ty) = xy(t);
|
||||||
let (px, py) = self.map.portal;
|
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 y in 0..3 {
|
||||||
for x 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 = Owner::Player;
|
||||||
@@ -1569,7 +1608,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Job::Attack(id) = e.job {
|
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;
|
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) {
|
if !far && self.attack_job(e, id, dt) {
|
||||||
return;
|
return;
|
||||||
@@ -1579,8 +1618,8 @@ impl Game {
|
|||||||
e.think = 1.5;
|
e.think = 1.5;
|
||||||
}
|
}
|
||||||
if e.guard {
|
if e.guard {
|
||||||
// gate defenders stay at the gate
|
// guards stay at their post (gate or treasure cave)
|
||||||
let (gx, gy) = self.gate_center();
|
let (gx, gy) = e.home.unwrap_or_else(|| self.gate_center());
|
||||||
let d2 = (e.x - gx).powi(2) + (e.y - gy).powi(2);
|
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 {
|
if d2 > 2.5 * 2.5 && e.path.is_empty() && e.repath <= 0.0 {
|
||||||
e.repath = 1.0;
|
e.repath = 1.0;
|
||||||
@@ -1854,9 +1893,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn simulate_long_game() {
|
fn simulate_long_game() {
|
||||||
for seed in [1u32, 7, 12345] {
|
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 (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)
|
// dig a way to the portal (through earth/gold, around rock)
|
||||||
let start = idx(cx - 1, cy + 1);
|
let start = idx(cx - 1, cy + 1);
|
||||||
let goal = idx(px + 1, py + 1);
|
let goal = idx(px + 1, py + 1);
|
||||||
@@ -1912,7 +1951,7 @@ mod tests {
|
|||||||
g.army_value()
|
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;
|
portal_logged = true;
|
||||||
println!("seed {} portal claimed after {:.0}s", seed, g.time);
|
println!("seed {} portal claimed after {:.0}s", seed, g.time);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -18,7 +18,7 @@ mod xm;
|
|||||||
use game::{Game, Msg};
|
use game::{Game, Msg};
|
||||||
use gfx::Gfx;
|
use gfx::Gfx;
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
use map::{idx, inb, MH, MW};
|
use map::{idx, inb, mh, mw};
|
||||||
use menu::{Menu, MenuAction, Page};
|
use menu::{Menu, MenuAction, Page};
|
||||||
use render3d::{Cam, Overlay, R3d};
|
use render3d::{Cam, Overlay, R3d};
|
||||||
use settings::{bindable_keys, Action, Settings};
|
use settings::{bindable_keys, Action, Settings};
|
||||||
@@ -53,8 +53,8 @@ struct Input {
|
|||||||
minimap_drag: bool,
|
minimap_drag: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_game(seed: u32) -> (Game, Cam) {
|
fn new_game(seed: u32, s: &Settings) -> (Game, Cam) {
|
||||||
let g = Game::new(seed);
|
let g = Game::new(seed, s.map_w, s.map_h);
|
||||||
let (cx, cy) = g.map.core;
|
let (cx, cy) = g.map.core;
|
||||||
let cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
|
let cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
|
||||||
(g, cam)
|
(g, cam)
|
||||||
@@ -82,7 +82,7 @@ impl MusicDirector {
|
|||||||
fn update(&mut self, g: &Game, title: bool) {
|
fn update(&mut self, g: &Game, title: bool) {
|
||||||
use audio::Track;
|
use audio::Track;
|
||||||
const BUILD: [Track; 3] = [Track::Build1, Track::Build2, Track::Build3];
|
const BUILD: [Track; 3] = [Track::Build1, Track::Build2, Track::Build3];
|
||||||
let gate_fight = g.time - g.gate_hit_t < 12.0 || g.ents.iter().any(|e| e.alive && e.guard);
|
let gate_fight = g.time - g.gate_hit_t < 12.0 || g.gate_defenders_alive();
|
||||||
let invasion = g.ents.iter().any(|e| e.alive && e.hero && !e.guard);
|
let invasion = g.ents.iter().any(|e| e.alive && e.hero && !e.guard);
|
||||||
let want = if title {
|
let want = if title {
|
||||||
Track::Title
|
Track::Title
|
||||||
@@ -193,7 +193,7 @@ async fn main() {
|
|||||||
menu.has_autosave = platform::storage_get(save::AUTOSAVE_KEY).is_some();
|
menu.has_autosave = platform::storage_get(save::AUTOSAVE_KEY).is_some();
|
||||||
|
|
||||||
let seed = (miniquad::date::now() * 1000.0) as u32 | 1;
|
let seed = (miniquad::date::now() * 1000.0) as u32 | 1;
|
||||||
let (game, cam) = new_game(seed);
|
let (game, cam) = new_game(seed, &settings);
|
||||||
let mut app = App { game, cam, settings, seed, status: None };
|
let mut app = App { game, cam, settings, seed, status: None };
|
||||||
app.game.hints = app.settings.hints;
|
app.game.hints = app.settings.hints;
|
||||||
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false };
|
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false };
|
||||||
@@ -279,7 +279,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
if is_key_pressed(KeyCode::N) && app.game.state != 0 {
|
if is_key_pressed(KeyCode::N) && app.game.state != 0 {
|
||||||
app.seed = next_seed(app.seed);
|
app.seed = next_seed(app.seed);
|
||||||
(app.game, app.cam) = new_game(app.seed);
|
(app.game, app.cam) = new_game(app.seed, &app.settings);
|
||||||
app.game.hints = app.settings.hints;
|
app.game.hints = app.settings.hints;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,7 +416,7 @@ async fn main() {
|
|||||||
MenuAction::Resume => {}
|
MenuAction::Resume => {}
|
||||||
MenuAction::NewGame => {
|
MenuAction::NewGame => {
|
||||||
app.seed = next_seed(app.seed);
|
app.seed = next_seed(app.seed);
|
||||||
(app.game, app.cam) = new_game(app.seed);
|
(app.game, app.cam) = new_game(app.seed, &app.settings);
|
||||||
app.game.hints = app.settings.hints;
|
app.game.hints = app.settings.hints;
|
||||||
menu.page = None;
|
menu.page = None;
|
||||||
ui.log.clear();
|
ui.log.clear();
|
||||||
@@ -460,7 +460,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
UiAction::NewGame => {
|
UiAction::NewGame => {
|
||||||
app.seed = next_seed(app.seed);
|
app.seed = next_seed(app.seed);
|
||||||
(app.game, app.cam) = new_game(app.seed);
|
(app.game, app.cam) = new_game(app.seed, &app.settings);
|
||||||
app.game.hints = app.settings.hints;
|
app.game.hints = app.settings.hints;
|
||||||
}
|
}
|
||||||
UiAction::CloseHelp | UiAction::SelectLanguage(_) | UiAction::Consumed => {}
|
UiAction::CloseHelp | UiAction::SelectLanguage(_) | UiAction::Consumed => {}
|
||||||
@@ -507,8 +507,8 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.cam.tx = app.cam.tx.clamp(0.0, MW as f32);
|
app.cam.tx = app.cam.tx.clamp(0.0, mw() as f32);
|
||||||
app.cam.tz = app.cam.tz.clamp(0.0, MH as f32);
|
app.cam.tz = app.cam.tz.clamp(0.0, mh() as f32);
|
||||||
|
|
||||||
if settings_changed {
|
if settings_changed {
|
||||||
apply_settings(&app.settings, &mut r3d);
|
apply_settings(&app.settings, &mut r3d);
|
||||||
@@ -571,7 +571,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
|
|||||||
};
|
};
|
||||||
let ov = Overlay {
|
let ov = Overlay {
|
||||||
hover: if over_ui { None } else { hover_tile.filter(|&(x, z)| inb(x, z)) },
|
hover: if over_ui { None } else { hover_tile.filter(|&(x, z)| inb(x, z)) },
|
||||||
rect: inp.ldown.and_then(|(sx, sy)| hover_tile.map(|(ex, ey)| (sx, sy, ex.clamp(0, MW as i32 - 1), ey.clamp(0, MH as i32 - 1)))),
|
rect: inp.ldown.and_then(|(sx, sy)| hover_tile.map(|(ex, ey)| (sx, sy, ex.clamp(0, mw() as i32 - 1), ey.clamp(0, mh() as i32 - 1)))),
|
||||||
color,
|
color,
|
||||||
radius: if over_ui { None } else { radius },
|
radius: if over_ui { None } else { radius },
|
||||||
high: ui.tool == Tool::Dig,
|
high: ui.tool == Tool::Dig,
|
||||||
|
|||||||
+304
-55
@@ -1,12 +1,36 @@
|
|||||||
// Map, tiles, generation and path finding.
|
// Map, tiles, generation and path finding.
|
||||||
|
//
|
||||||
|
// The map size is chosen per game (64..256 tiles per side). Only one map is active at a time;
|
||||||
|
// its dimensions are kept in a thread-local so the many small helpers (idx, xy, inb) stay simple.
|
||||||
|
// Map::generate and loading a savegame (Map::activate) set them.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use std::cell::Cell;
|
||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::BinaryHeap;
|
use std::collections::BinaryHeap;
|
||||||
|
|
||||||
pub const MW: usize = 64;
|
pub const MIN_SIZE: usize = 64;
|
||||||
pub const MH: usize = 64;
|
pub const MAX_SIZE: usize = 256;
|
||||||
pub const N: usize = MW * MH;
|
|
||||||
|
thread_local! {
|
||||||
|
static DIMS: Cell<(usize, usize)> = Cell::new((64, 64));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_dims(w: usize, h: usize) {
|
||||||
|
DIMS.with(|d| d.set((w, h)));
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn mw() -> usize {
|
||||||
|
DIMS.with(|d| d.get().0)
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn mh() -> usize {
|
||||||
|
DIMS.with(|d| d.get().1)
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn n() -> usize {
|
||||||
|
mw() * mh()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
pub enum Kind {
|
pub enum Kind {
|
||||||
@@ -24,6 +48,21 @@ pub enum Kind {
|
|||||||
Gate,
|
Gate,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const KINDS: [Kind; 12] = [
|
||||||
|
Kind::Rock,
|
||||||
|
Kind::Earth,
|
||||||
|
Kind::Gold,
|
||||||
|
Kind::Floor,
|
||||||
|
Kind::Treasury,
|
||||||
|
Kind::Lair,
|
||||||
|
Kind::Farm,
|
||||||
|
Kind::Training,
|
||||||
|
Kind::Library,
|
||||||
|
Kind::Core,
|
||||||
|
Kind::Portal,
|
||||||
|
Kind::Gate,
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
pub enum Owner {
|
pub enum Owner {
|
||||||
None,
|
None,
|
||||||
@@ -31,8 +70,10 @@ pub enum Owner {
|
|||||||
Hero,
|
Hero,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short serde names keep savegames small (the map has 4096 tiles)
|
const OWNERS: [Owner; 3] = [Owner::None, Owner::Player, Owner::Hero];
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
|
||||||
|
// Short serde names (only used by version 1 savegames, newer ones use the packed codec)
|
||||||
|
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Tile {
|
pub struct Tile {
|
||||||
#[serde(rename = "k")]
|
#[serde(rename = "k")]
|
||||||
pub kind: Kind,
|
pub kind: Kind,
|
||||||
@@ -73,13 +114,14 @@ impl Tile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn idx(x: i32, y: i32) -> usize {
|
pub fn idx(x: i32, y: i32) -> usize {
|
||||||
y as usize * MW + x as usize
|
y as usize * mw() + x as usize
|
||||||
}
|
}
|
||||||
pub fn xy(i: usize) -> (i32, i32) {
|
pub fn xy(i: usize) -> (i32, i32) {
|
||||||
((i % MW) as i32, (i / MW) as i32)
|
let w = mw();
|
||||||
|
((i % w) as i32, (i / w) as i32)
|
||||||
}
|
}
|
||||||
pub fn inb(x: i32, y: i32) -> bool {
|
pub fn inb(x: i32, y: i32) -> bool {
|
||||||
x >= 0 && y >= 0 && (x as usize) < MW && (y as usize) < MH
|
x >= 0 && y >= 0 && (x as usize) < mw() && (y as usize) < mh()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DIR4: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
|
pub const DIR4: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
|
||||||
@@ -109,13 +151,37 @@ impl Rng {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Map {
|
pub struct Map {
|
||||||
|
#[serde(default = "default_size")]
|
||||||
|
pub w: usize,
|
||||||
|
#[serde(default = "default_size")]
|
||||||
|
pub h: usize,
|
||||||
|
#[serde(serialize_with = "tiles_ser", deserialize_with = "tiles_de")]
|
||||||
pub t: Vec<Tile>,
|
pub t: Vec<Tile>,
|
||||||
pub core: (i32, i32), // top-left corner of the core (3x3)
|
pub core: (i32, i32), // top-left corner of the core (3x3)
|
||||||
pub portal: (i32, i32),
|
#[serde(default)]
|
||||||
|
pub portals: Vec<(i32, i32)>, // top-left corners of the portals (3x3)
|
||||||
|
#[serde(default, rename = "portal", skip_serializing)]
|
||||||
|
legacy_portal: Option<(i32, i32)>, // version 1 savegames had a single portal
|
||||||
pub gate: (i32, i32),
|
pub gate: (i32, i32),
|
||||||
|
#[serde(default)]
|
||||||
|
pub treasures: Vec<(i32, i32)>, // centres of the treasure caves
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_size() -> usize {
|
||||||
|
64
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Map {
|
impl Map {
|
||||||
|
// Make this map's dimensions the active ones (after loading a savegame)
|
||||||
|
pub fn activate(&mut self) {
|
||||||
|
set_dims(self.w, self.h);
|
||||||
|
if self.portals.is_empty() {
|
||||||
|
if let Some(p) = self.legacy_portal.take() {
|
||||||
|
self.portals.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self, x: i32, y: i32) -> Option<&Tile> {
|
pub fn get(&self, x: i32, y: i32) -> Option<&Tile> {
|
||||||
if inb(x, y) {
|
if inb(x, y) {
|
||||||
Some(&self.t[idx(x, y)])
|
Some(&self.t[idx(x, y)])
|
||||||
@@ -128,10 +194,23 @@ impl Map {
|
|||||||
inb(x, y) && self.t[idx(x, y)].walkable()
|
inb(x, y) && self.t[idx(x, y)].walkable()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate(seed: u32) -> Map {
|
// Index of the portal containing tile (x, y)
|
||||||
|
pub fn portal_at(&self, x: i32, y: i32) -> Option<usize> {
|
||||||
|
self.portals.iter().position(|&(px, py)| x >= px && x < px + 3 && y >= py && y < py + 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn portal_owned(&self, k: usize) -> bool {
|
||||||
|
let (px, py) = self.portals[k];
|
||||||
|
self.t[idx(px, py)].owner == Owner::Player
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate(seed: u32, w: usize, h: usize) -> Map {
|
||||||
|
let w = w.clamp(MIN_SIZE, MAX_SIZE);
|
||||||
|
let h = h.clamp(MIN_SIZE, MAX_SIZE);
|
||||||
|
set_dims(w, h);
|
||||||
let mut s = seed;
|
let mut s = seed;
|
||||||
loop {
|
loop {
|
||||||
let m = Map::try_generate(s);
|
let m = Map::try_generate(s, w, h);
|
||||||
// make sure heroes can reach the core
|
// make sure heroes can reach the core
|
||||||
if m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() {
|
if m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() {
|
||||||
return m;
|
return m;
|
||||||
@@ -140,68 +219,99 @@ impl Map {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_generate(seed: u32) -> Map {
|
fn try_generate(seed: u32, w: usize, h: usize) -> Map {
|
||||||
|
let (wi, hi) = (w as i32, h as i32);
|
||||||
let mut rng = Rng(seed | 1);
|
let mut rng = Rng(seed | 1);
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
rng.next();
|
rng.next();
|
||||||
}
|
}
|
||||||
let mut t = vec![Tile::new(Kind::Earth); N];
|
// content scales with the area; one portal per 64x64 tiles
|
||||||
|
let area = (w * h) as f32 / 4096.0;
|
||||||
|
let scale = |n: f32| (n * area).round() as i32;
|
||||||
|
let n_portals = (area.round() as usize).max(1);
|
||||||
|
let n_treasures = n_portals - 1;
|
||||||
|
|
||||||
|
let mut t = vec![Tile::new(Kind::Earth); w * h];
|
||||||
for (i, tile) in t.iter_mut().enumerate() {
|
for (i, tile) in t.iter_mut().enumerate() {
|
||||||
let (x, y) = xy(i);
|
let (x, y) = ((i % w) as i32, (i / w) as i32);
|
||||||
tile.var = (rng.next() % 4) as u8;
|
tile.var = (rng.next() % 4) as u8;
|
||||||
tile.hp = 6.0;
|
tile.hp = 6.0;
|
||||||
if x < 2 || y < 2 || x >= MW as i32 - 2 || y >= MH as i32 - 2 {
|
if x < 2 || y < 2 || x >= wi - 2 || y >= hi - 2 {
|
||||||
tile.kind = Kind::Rock;
|
tile.kind = Kind::Rock;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let core = (11, rng.range(26, 35));
|
let inner = |x: i32, y: i32| x >= 2 && y >= 2 && x < wi - 2 && y < hi - 2;
|
||||||
let portal = (rng.range(24, 30), if core.1 > 31 { rng.range(12, 18) } else { rng.range(44, 50) });
|
let core = (11, hi / 2 + rng.range(-5, 3));
|
||||||
let gate = (rng.range(51, 55), rng.range(10, 50));
|
let gate = (wi - 13 + rng.range(-2, 2), rng.range(10, hi - 14));
|
||||||
|
|
||||||
let blob = |t: &mut Vec<Tile>, rng: &mut Rng, cx: i32, cy: i32, r: i32, k: Kind| {
|
// places features far apart: best of several random candidates
|
||||||
|
let mut taken: Vec<(i32, i32)> = vec![(core.0 + 1, core.1 + 1), (gate.0 + 1, gate.1 + 1)];
|
||||||
|
let place = |rng: &mut Rng, taken: &mut Vec<(i32, i32)>, margin: i32| -> (i32, i32) {
|
||||||
|
let mut best = (margin, margin);
|
||||||
|
let mut bd = -1;
|
||||||
|
for _ in 0..40 {
|
||||||
|
let p = (rng.range(margin, wi - margin - 3), rng.range(margin, hi - margin - 3));
|
||||||
|
let d = taken.iter().map(|q| (p.0 - q.0).pow(2) + (p.1 - q.1).pow(2)).min().unwrap_or(0);
|
||||||
|
if d > bd {
|
||||||
|
bd = d;
|
||||||
|
best = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
taken.push(best);
|
||||||
|
best
|
||||||
|
};
|
||||||
|
// the first portal is close to the start
|
||||||
|
let first = (core.0 + rng.range(13, 19), (core.1 + if rng.f() < 0.5 { rng.range(-14, -10) } else { rng.range(10, 14) }).clamp(5, hi - 9));
|
||||||
|
taken.push(first);
|
||||||
|
let mut portals = vec![first];
|
||||||
|
for _ in 1..n_portals {
|
||||||
|
portals.push(place(&mut rng, &mut taken, 6));
|
||||||
|
}
|
||||||
|
let mut treasures = Vec::new();
|
||||||
|
for _ in 0..n_treasures {
|
||||||
|
let p = place(&mut rng, &mut taken, 7);
|
||||||
|
treasures.push((p.0 + 1, p.1 + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
let blob = |t: &mut Vec<Tile>, rng: &mut Rng, cx: i32, cy: i32, r: i32, k: Kind, gold: (i32, i32)| {
|
||||||
for y in cy - r..=cy + r {
|
for y in cy - r..=cy + r {
|
||||||
for x in cx - r..=cx + r {
|
for x in cx - r..=cx + r {
|
||||||
if !inb(x, y) || x < 2 || y < 2 || x >= MW as i32 - 2 || y >= MH as i32 - 2 {
|
if !inner(x, y) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let d = ((x - cx) * (x - cx) + (y - cy) * (y - cy)) as f32;
|
let d = ((x - cx) * (x - cx) + (y - cy) * (y - cy)) as f32;
|
||||||
if d <= (r as f32 + 0.5).powi(2) - rng.f() * r as f32 {
|
if d <= (r as f32 + 0.5).powi(2) - rng.f() * r as f32 {
|
||||||
let tl = &mut t[idx(x, y)];
|
let tl = &mut t[y as usize * w + x as usize];
|
||||||
tl.kind = k;
|
tl.kind = k;
|
||||||
if k == Kind::Gold {
|
if k == Kind::Gold {
|
||||||
tl.gold = rng.range(250, 550);
|
tl.gold = rng.range(gold.0, gold.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
for _ in 0..scale(22.0) {
|
||||||
// rock clusters
|
let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5));
|
||||||
for _ in 0..22 {
|
|
||||||
let cx = rng.range(4, MW as i32 - 5);
|
|
||||||
let cy = rng.range(4, MH as i32 - 5);
|
|
||||||
let r = rng.range(1, 3);
|
let r = rng.range(1, 3);
|
||||||
blob(&mut t, &mut rng, cx, cy, r, Kind::Rock);
|
blob(&mut t, &mut rng, cx, cy, r, Kind::Rock, (0, 0));
|
||||||
}
|
}
|
||||||
// gold veins
|
for _ in 0..scale(16.0) {
|
||||||
for _ in 0..16 {
|
let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5));
|
||||||
let cx = rng.range(4, MW as i32 - 5);
|
|
||||||
let cy = rng.range(4, MH as i32 - 5);
|
|
||||||
let r = rng.range(1, 2);
|
let r = rng.range(1, 2);
|
||||||
blob(&mut t, &mut rng, cx, cy, r, Kind::Gold);
|
blob(&mut t, &mut rng, cx, cy, r, Kind::Gold, (250, 550));
|
||||||
}
|
}
|
||||||
// guaranteed gold near the start
|
// guaranteed gold near the start
|
||||||
for k in 0..3 {
|
for k in 0..3 {
|
||||||
let cx = core.0 + rng.range(-3, 9);
|
let cx = core.0 + rng.range(-3, 9);
|
||||||
let cy = core.1 + if k % 2 == 0 { rng.range(-9, -6) } else { rng.range(7, 9) };
|
let cy = core.1 + if k % 2 == 0 { rng.range(-9, -6) } else { rng.range(7, 9) };
|
||||||
blob(&mut t, &mut rng, cx, cy, 1, Kind::Gold);
|
blob(&mut t, &mut rng, cx, cy, 1, Kind::Gold, (250, 550));
|
||||||
}
|
}
|
||||||
|
|
||||||
let room = |t: &mut Vec<Tile>, x0: i32, y0: i32, x1: i32, y1: i32, owner: Owner| {
|
let room = |t: &mut Vec<Tile>, x0: i32, y0: i32, x1: i32, y1: i32, owner: Owner| {
|
||||||
for y in y0..=y1 {
|
for y in y0..=y1 {
|
||||||
for x in x0..=x1 {
|
for x in x0..=x1 {
|
||||||
if inb(x, y) && x >= 2 && y >= 2 && x < MW as i32 - 2 && y < MH as i32 - 2 {
|
if inner(x, y) {
|
||||||
let tl = &mut t[idx(x, y)];
|
let tl = &mut t[y as usize * w + x as usize];
|
||||||
tl.kind = Kind::Floor;
|
tl.kind = Kind::Floor;
|
||||||
tl.owner = owner;
|
tl.owner = owner;
|
||||||
tl.gold = 0;
|
tl.gold = 0;
|
||||||
@@ -209,38 +319,40 @@ impl Map {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// small natural caves
|
// small natural caves
|
||||||
for _ in 0..4 {
|
for _ in 0..scale(4.0) {
|
||||||
let cx = rng.range(20, 48);
|
let (cx, cy) = (rng.range(20, wi - 16), rng.range(6, hi - 6));
|
||||||
let cy = rng.range(6, 58);
|
let (cw, ch) = (rng.range(1, 2), rng.range(1, 2));
|
||||||
let w = rng.range(1, 2);
|
room(&mut t, cx - cw, cy - ch, cx + cw, cy + ch, Owner::None);
|
||||||
let h = rng.range(1, 2);
|
|
||||||
room(&mut t, cx - w, cy - h, cx + w, cy + h, Owner::None);
|
|
||||||
}
|
}
|
||||||
|
// treasure caves: a cave surrounded by very rich gold veins (guarded by heroes, see Game::new)
|
||||||
// starting area
|
for &(cx, cy) in &treasures {
|
||||||
|
blob(&mut t, &mut rng, cx, cy, 4, Kind::Gold, (1000, 1600));
|
||||||
|
room(&mut t, cx - 2, cy - 2, cx + 2, cy + 2, Owner::None);
|
||||||
|
}
|
||||||
|
// starting area and core
|
||||||
room(&mut t, core.0 - 3, core.1 - 3, core.0 + 5, core.1 + 5, Owner::Player);
|
room(&mut t, core.0 - 3, core.1 - 3, core.0 + 5, core.1 + 5, Owner::Player);
|
||||||
// core 3x3
|
|
||||||
for y in 0..3 {
|
for y in 0..3 {
|
||||||
for x in 0..3 {
|
for x in 0..3 {
|
||||||
let tl = &mut t[idx(core.0 + x, core.1 + y)];
|
let tl = &mut t[(core.1 + y) as usize * w + (core.0 + x) as usize];
|
||||||
tl.kind = Kind::Core;
|
tl.kind = Kind::Core;
|
||||||
tl.owner = Owner::Player;
|
tl.owner = Owner::Player;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// portal cave
|
// portal caves
|
||||||
room(&mut t, portal.0 - 2, portal.1 - 2, portal.0 + 4, portal.1 + 4, Owner::None);
|
for &(px, py) in &portals {
|
||||||
|
room(&mut t, px - 2, py - 2, px + 4, py + 4, Owner::None);
|
||||||
for y in 0..3 {
|
for y in 0..3 {
|
||||||
for x in 0..3 {
|
for x in 0..3 {
|
||||||
t[idx(portal.0 + x, portal.1 + y)].kind = Kind::Portal;
|
t[(py + y) as usize * w + (px + x) as usize].kind = Kind::Portal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// hero gate
|
// hero gate
|
||||||
room(&mut t, gate.0 - 2, gate.1 - 2, gate.0 + 4, gate.1 + 4, Owner::Hero);
|
room(&mut t, gate.0 - 2, gate.1 - 2, gate.0 + 4, gate.1 + 4, Owner::Hero);
|
||||||
for y in 0..3 {
|
for y in 0..3 {
|
||||||
for x in 0..3 {
|
for x in 0..3 {
|
||||||
let tl = &mut t[idx(gate.0 + x, gate.1 + y)];
|
let tl = &mut t[(gate.1 + y) as usize * w + (gate.0 + x) as usize];
|
||||||
tl.kind = Kind::Gate;
|
tl.kind = Kind::Gate;
|
||||||
tl.owner = Owner::Hero;
|
tl.owner = Owner::Hero;
|
||||||
}
|
}
|
||||||
@@ -250,7 +362,7 @@ impl Map {
|
|||||||
tl.hp = 6.0;
|
tl.hp = 6.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Map { t, core, portal, gate }
|
Map { w, h, t, core, portals, legacy_portal: None, gate, treasures }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic Dijkstra search.
|
// Generic Dijkstra search.
|
||||||
@@ -261,8 +373,8 @@ impl Map {
|
|||||||
C: Fn(usize) -> Option<u32>,
|
C: Fn(usize) -> Option<u32>,
|
||||||
G: Fn(usize) -> bool,
|
G: Fn(usize) -> bool,
|
||||||
{
|
{
|
||||||
let mut dist = vec![u32::MAX; N];
|
let mut dist = vec![u32::MAX; self.t.len()];
|
||||||
let mut prev = vec![usize::MAX; N];
|
let mut prev = vec![usize::MAX; self.t.len()];
|
||||||
let mut heap = BinaryHeap::new();
|
let mut heap = BinaryHeap::new();
|
||||||
dist[start] = 0;
|
dist[start] = 0;
|
||||||
heap.push(Reverse((0u32, start)));
|
heap.push(Reverse((0u32, start)));
|
||||||
@@ -338,3 +450,140 @@ impl Map {
|
|||||||
self.search(start, self.walk_cost(), |i| i == target, true)
|
self.search(start, self.walk_cost(), |i| i == target, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ compact tile codec
|
||||||
|
//
|
||||||
|
// Savegame version 2 stores the tiles as one base64 string: the 2-bit texture variants,
|
||||||
|
// then run-length encoded tile records (kind, owner, marked, hp, gold, food, reserved, bed).
|
||||||
|
// A 256x256 map shrinks from ~4.5 MB of JSON to a few hundred KB.
|
||||||
|
|
||||||
|
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
|
||||||
|
fn b64_encode(data: &[u8]) -> String {
|
||||||
|
let mut out = String::with_capacity(data.len() * 4 / 3 + 4);
|
||||||
|
for chunk in data.chunks(3) {
|
||||||
|
let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
|
||||||
|
let v = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
|
||||||
|
for k in 0..4 {
|
||||||
|
if k <= chunk.len() {
|
||||||
|
out.push(B64[(v >> (18 - 6 * k) & 63) as usize] as char);
|
||||||
|
} else {
|
||||||
|
out.push('=');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn b64_decode(s: &str) -> Option<Vec<u8>> {
|
||||||
|
let mut val = [255u8; 256];
|
||||||
|
for (i, &c) in B64.iter().enumerate() {
|
||||||
|
val[c as usize] = i as u8;
|
||||||
|
}
|
||||||
|
let bytes: Vec<u8> = s.bytes().filter(|&c| c != b'=').collect();
|
||||||
|
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
|
||||||
|
for chunk in bytes.chunks(4) {
|
||||||
|
let mut v = 0u32;
|
||||||
|
for (k, &c) in chunk.iter().enumerate() {
|
||||||
|
let d = val[c as usize];
|
||||||
|
if d == 255 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
v |= (d as u32) << (18 - 6 * k);
|
||||||
|
}
|
||||||
|
let n = chunk.len() * 6 / 8;
|
||||||
|
for k in 0..n {
|
||||||
|
out.push((v >> (16 - 8 * k)) as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
const REC: usize = 23;
|
||||||
|
|
||||||
|
fn tile_record(t: &Tile) -> [u8; REC] {
|
||||||
|
let mut r = [0u8; REC];
|
||||||
|
r[0] = KINDS.iter().position(|&k| k == t.kind).unwrap_or(0) as u8;
|
||||||
|
r[1] = OWNERS.iter().position(|&o| o == t.owner).unwrap_or(0) as u8;
|
||||||
|
r[2] = t.marked as u8;
|
||||||
|
r[3..7].copy_from_slice(&t.hp.to_le_bytes());
|
||||||
|
r[7..11].copy_from_slice(&t.gold.to_le_bytes());
|
||||||
|
r[11..15].copy_from_slice(&t.food.to_le_bytes());
|
||||||
|
r[15..19].copy_from_slice(&t.reserved.to_le_bytes());
|
||||||
|
r[19..23].copy_from_slice(&t.bed.to_le_bytes());
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tiles_ser<S: Serializer>(tiles: &[Tile], s: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(&(tiles.len() as u32).to_le_bytes());
|
||||||
|
// texture variants, 4 per byte
|
||||||
|
for chunk in tiles.chunks(4) {
|
||||||
|
let mut b = 0u8;
|
||||||
|
for (k, t) in chunk.iter().enumerate() {
|
||||||
|
b |= (t.var & 3) << (k * 2);
|
||||||
|
}
|
||||||
|
out.push(b);
|
||||||
|
}
|
||||||
|
// runs of identical records
|
||||||
|
let mut i = 0;
|
||||||
|
while i < tiles.len() {
|
||||||
|
let rec = tile_record(&tiles[i]);
|
||||||
|
let mut run = 1;
|
||||||
|
while i + run < tiles.len() && run < 65535 && tile_record(&tiles[i + run]) == rec {
|
||||||
|
run += 1;
|
||||||
|
}
|
||||||
|
out.extend_from_slice(&(run as u16).to_le_bytes());
|
||||||
|
out.extend_from_slice(&rec);
|
||||||
|
i += run;
|
||||||
|
}
|
||||||
|
s.serialize_str(&format!("v2:{}", b64_encode(&out)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum TilesIn {
|
||||||
|
Packed(String),
|
||||||
|
Plain(Vec<Tile>), // version 1 savegames
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tiles_de<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Tile>, D::Error> {
|
||||||
|
use serde::de::Error;
|
||||||
|
match TilesIn::deserialize(d)? {
|
||||||
|
TilesIn::Plain(v) => Ok(v),
|
||||||
|
TilesIn::Packed(s) => {
|
||||||
|
let data = s.strip_prefix("v2:").and_then(b64_decode).ok_or_else(|| D::Error::custom("bad tile data"))?;
|
||||||
|
let rd32 = |o: usize| -> Option<[u8; 4]> { data.get(o..o + 4).map(|s| [s[0], s[1], s[2], s[3]]) };
|
||||||
|
let count = u32::from_le_bytes(rd32(0).ok_or_else(|| D::Error::custom("bad tile data"))?) as usize;
|
||||||
|
if count > MAX_SIZE * MAX_SIZE {
|
||||||
|
return Err(D::Error::custom("map too large"));
|
||||||
|
}
|
||||||
|
let mut tiles = Vec::with_capacity(count);
|
||||||
|
let mut o = 4 + (count + 3) / 4;
|
||||||
|
while tiles.len() < count {
|
||||||
|
let run = data.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).ok_or_else(|| D::Error::custom("bad tile data"))? as usize;
|
||||||
|
let r = data.get(o + 2..o + 2 + REC).ok_or_else(|| D::Error::custom("bad tile data"))?;
|
||||||
|
let f = |a: usize| [r[a], r[a + 1], r[a + 2], r[a + 3]];
|
||||||
|
let t = Tile {
|
||||||
|
kind: *KINDS.get(r[0] as usize).unwrap_or(&Kind::Rock),
|
||||||
|
owner: *OWNERS.get(r[1] as usize).unwrap_or(&Owner::None),
|
||||||
|
marked: r[2] != 0,
|
||||||
|
hp: f32::from_le_bytes(f(3)),
|
||||||
|
gold: i32::from_le_bytes(f(7)),
|
||||||
|
food: f32::from_le_bytes(f(11)),
|
||||||
|
reserved: u32::from_le_bytes(f(15)),
|
||||||
|
bed: u32::from_le_bytes(f(19)),
|
||||||
|
var: 0,
|
||||||
|
};
|
||||||
|
for _ in 0..run.min(count - tiles.len()) {
|
||||||
|
tiles.push(t);
|
||||||
|
}
|
||||||
|
o += 2 + REC;
|
||||||
|
}
|
||||||
|
for (i, t) in tiles.iter_mut().enumerate() {
|
||||||
|
t.var = (data[4 + i / 4] >> ((i % 4) * 2)) & 3;
|
||||||
|
}
|
||||||
|
Ok(tiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
-2
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use crate::gfx::*;
|
use crate::gfx::*;
|
||||||
use crate::i18n::{languages, tr, trf};
|
use crate::i18n::{languages, tr, trf};
|
||||||
|
use crate::map::{MAX_SIZE, MIN_SIZE};
|
||||||
use crate::settings::{all_actions, key_name, Action, Settings};
|
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 crate::ui::{tool_info, CH, CW, DIM, FRAME, FRAME_HI, GOLD, S, TOOLS, TXT};
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
@@ -12,6 +13,7 @@ pub enum Page {
|
|||||||
Title,
|
Title,
|
||||||
Pause,
|
Pause,
|
||||||
Settings { tab: usize, from_title: bool },
|
Settings { tab: usize, from_title: bool },
|
||||||
|
NewGame,
|
||||||
KeyCapture { action: usize, from_title: bool },
|
KeyCapture { action: usize, from_title: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +152,9 @@ enum Btn {
|
|||||||
Back,
|
Back,
|
||||||
Tab(usize),
|
Tab(usize),
|
||||||
Change(usize, i32),
|
Change(usize, i32),
|
||||||
|
MapW(i32),
|
||||||
|
MapH(i32),
|
||||||
|
Start,
|
||||||
Key(usize),
|
Key(usize),
|
||||||
ResetKeys,
|
ResetKeys,
|
||||||
}
|
}
|
||||||
@@ -159,7 +164,7 @@ pub enum MenuAction {
|
|||||||
Consumed,
|
Consumed,
|
||||||
Continue, // load the autosave
|
Continue, // load the autosave
|
||||||
Resume,
|
Resume,
|
||||||
NewGame,
|
NewGame, // start a new game with the chosen map size
|
||||||
Load,
|
Load,
|
||||||
Save,
|
Save,
|
||||||
Help,
|
Help,
|
||||||
@@ -195,6 +200,7 @@ impl Menu {
|
|||||||
Some(Page::KeyCapture { from_title, .. }) => Some(Page::Settings { tab: 4, from_title }),
|
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: true, .. }) => Some(Page::Title),
|
||||||
Some(Page::Settings { from_title: false, .. }) => Some(Page::Pause),
|
Some(Page::Settings { from_title: false, .. }) => Some(Page::Pause),
|
||||||
|
Some(Page::NewGame) => Some(Page::Title),
|
||||||
Some(Page::Pause) => None,
|
Some(Page::Pause) => None,
|
||||||
Some(Page::Title) => Some(Page::Title),
|
Some(Page::Title) => Some(Page::Title),
|
||||||
None => Some(Page::Pause),
|
None => Some(Page::Pause),
|
||||||
@@ -227,7 +233,19 @@ impl Menu {
|
|||||||
self.page = None;
|
self.page = None;
|
||||||
MenuAction::Resume
|
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::Load) => MenuAction::Load,
|
||||||
Some(Btn::Save) => MenuAction::Save,
|
Some(Btn::Save) => MenuAction::Save,
|
||||||
Some(Btn::Help) => MenuAction::Help,
|
Some(Btn::Help) => MenuAction::Help,
|
||||||
@@ -273,6 +291,7 @@ impl Menu {
|
|||||||
match page {
|
match page {
|
||||||
Page::Title => self.draw_title(gfx),
|
Page::Title => self.draw_title(gfx),
|
||||||
Page::Pause => self.draw_pause(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::Settings { tab, .. } => self.draw_settings(gfx, settings, tab),
|
||||||
Page::KeyCapture { action, .. } => {
|
Page::KeyCapture { action, .. } => {
|
||||||
self.draw_settings(gfx, settings, 4);
|
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);
|
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) {
|
fn draw_pause(&mut self, gfx: &Gfx) {
|
||||||
let sw = screen_width();
|
let sw = screen_width();
|
||||||
let sh = screen_height();
|
let sh = screen_height();
|
||||||
|
|||||||
+88
-37
@@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
pub const WALL_H: f32 = 0.95;
|
pub const WALL_H: f32 = 0.95;
|
||||||
const CHUNK: i32 = 16;
|
const CHUNK: i32 = 16;
|
||||||
const NCH: i32 = MW as i32 / CHUNK;
|
|
||||||
pub const MAX_LIGHTS: usize = 24;
|
pub const MAX_LIGHTS: usize = 24;
|
||||||
const ATLAS_W: f32 = 256.0;
|
const ATLAS_W: f32 = 256.0;
|
||||||
const ATLAS_H: f32 = 80.0;
|
const ATLAS_H: f32 = 80.0;
|
||||||
@@ -350,7 +349,7 @@ impl MB {
|
|||||||
|
|
||||||
// Ambient light field: own territory is lit, foreign areas are dim
|
// Ambient light field: own territory is lit, foreign areas are dim
|
||||||
fn ambient_field(map: &Map) -> Vec<f32> {
|
fn ambient_field(map: &Map) -> Vec<f32> {
|
||||||
let mut f = vec![0.0f32; N];
|
let mut f = vec![0.0f32; n()];
|
||||||
for (i, t) in map.t.iter().enumerate() {
|
for (i, t) in map.t.iter().enumerate() {
|
||||||
f[i] = match (t.solid(), t.owner) {
|
f[i] = match (t.solid(), t.owner) {
|
||||||
(false, Owner::Player) => 1.0,
|
(false, Owner::Player) => 1.0,
|
||||||
@@ -363,8 +362,8 @@ fn ambient_field(map: &Map) -> Vec<f32> {
|
|||||||
}
|
}
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
let src = f.clone();
|
let src = f.clone();
|
||||||
for y in 0..MH as i32 {
|
for y in 0..mh() as i32 {
|
||||||
for x in 0..MW as i32 {
|
for x in 0..mw() as i32 {
|
||||||
let mut s = 0.0;
|
let mut s = 0.0;
|
||||||
let mut n = 0.0;
|
let mut n = 0.0;
|
||||||
for dy in -1..=1 {
|
for dy in -1..=1 {
|
||||||
@@ -383,11 +382,11 @@ fn ambient_field(map: &Map) -> Vec<f32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn amb_at(f: &[f32], x: f32, z: f32) -> f32 {
|
fn amb_at(f: &[f32], x: f32, z: f32) -> f32 {
|
||||||
let gx = (x - 0.5).clamp(0.0, MW as f32 - 1.001);
|
let gx = (x - 0.5).clamp(0.0, mw() as f32 - 1.001);
|
||||||
let gz = (z - 0.5).clamp(0.0, MH as f32 - 1.001);
|
let gz = (z - 0.5).clamp(0.0, mh() as f32 - 1.001);
|
||||||
let (x0, z0) = (gx.floor() as i32, gz.floor() as i32);
|
let (x0, z0) = (gx.floor() as i32, gz.floor() as i32);
|
||||||
let (fx, fz) = (gx - x0 as f32, gz - z0 as f32);
|
let (fx, fz) = (gx - x0 as f32, gz - z0 as f32);
|
||||||
let g = |x: i32, z: i32| f[idx(x.min(MW as i32 - 1), z.min(MH as i32 - 1))];
|
let g = |x: i32, z: i32| f[idx(x.min(mw() as i32 - 1), z.min(mh() as i32 - 1))];
|
||||||
let a = g(x0, z0) * (1.0 - fx) + g(x0 + 1, z0) * fx;
|
let a = g(x0, z0) * (1.0 - fx) + g(x0 + 1, z0) * fx;
|
||||||
let b = g(x0, z0 + 1) * (1.0 - fx) + g(x0 + 1, z0 + 1) * fx;
|
let b = g(x0, z0 + 1) * (1.0 - fx) + g(x0 + 1, z0 + 1) * fx;
|
||||||
0.15 + 0.45 * (a * (1.0 - fz) + b * fz)
|
0.15 + 0.45 * (a * (1.0 - fz) + b * fz)
|
||||||
@@ -412,13 +411,48 @@ fn edge_pts(a: Vec3, b: Vec3, n: usize) -> Vec<Vec3> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: &Texture2D, wall_sub: usize) -> (Option<Mesh>, Option<Mesh>, Option<Mesh>) {
|
const MARK_FILL: [u8; 4] = [255, 205, 50, 70];
|
||||||
|
const MARK_LINE: [u8; 4] = [255, 220, 70, 235];
|
||||||
|
|
||||||
|
// Glowing yellow box around a block that is marked for digging
|
||||||
|
fn mark_block(mk: &mut MB, map: &Map, x: i32, z: i32) {
|
||||||
|
let (x0, z0) = (x as f32 - 0.06, z as f32 - 0.06);
|
||||||
|
let (x1, z1) = (x as f32 + 1.06, z as f32 + 1.06);
|
||||||
|
let top = WALL_H + 0.16;
|
||||||
|
let c = [x0, z0, x1, z1];
|
||||||
|
let p = |xx: f32, y: f32, zz: f32| vec3(xx, y, zz);
|
||||||
|
// top face
|
||||||
|
mk.quad([p(c[0], top, c[1]), p(c[2], top, c[1]), p(c[2], top, c[3]), p(c[0], top, c[3])], [Vec2::ZERO; 4], [MARK_FILL; 4], [Vec3::Y; 4]);
|
||||||
|
let corners = [p(c[0], 0.0, c[1]), p(c[2], 0.0, c[1]), p(c[2], 0.0, c[3]), p(c[0], 0.0, c[3])];
|
||||||
|
// side faces towards open neighbours, outline on all edges
|
||||||
|
for k in 0..4 {
|
||||||
|
let a = corners[k];
|
||||||
|
let b = corners[(k + 1) % 4];
|
||||||
|
let (dx, dz) = DIR4[k];
|
||||||
|
let up = vec3(0.0, top, 0.0);
|
||||||
|
let out = vec3(dx as f32, 0.0, dz as f32);
|
||||||
|
if !solid_at(map, x + dx, z + dz) {
|
||||||
|
mk.quad([a, b, b + up, a + up], [Vec2::ZERO; 4], [MARK_FILL; 4], [out; 4]);
|
||||||
|
mk.strip(a + vec3(0.0, 0.02, 0.0), b + vec3(0.0, 0.02, 0.0), out, 0.05, MARK_LINE);
|
||||||
|
}
|
||||||
|
mk.strip(a + up, b + up, Vec3::Y, 0.06, MARK_LINE);
|
||||||
|
// vertical edge at this corner: down to the floor only where a side is open,
|
||||||
|
// otherwise just the part that sticks out above the neighbouring walls
|
||||||
|
let (pdx, pdz) = DIR4[(k + 3) % 4];
|
||||||
|
let open_here = !solid_at(map, x + dx, z + dz) || !solid_at(map, x + pdx, z + pdz);
|
||||||
|
let bottom = if open_here { 0.0 } else { WALL_H - 0.05 };
|
||||||
|
mk.strip(a + vec3(0.0, bottom, 0.0), a + up, out, 0.05, MARK_LINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: &Texture2D, wall_sub: usize) -> [Option<Mesh>; 4] {
|
||||||
let mut mb = MB::new();
|
let mut mb = MB::new();
|
||||||
let mut ov = MB::new();
|
let ov = MB::new(); // textured overlays (currently unused)
|
||||||
let mut ln = MB::new();
|
let mut ln = MB::new();
|
||||||
|
let mut mk = MB::new();
|
||||||
let ld = LIGHT_DIR.normalize();
|
let ld = LIGHT_DIR.normalize();
|
||||||
for z in cz * CHUNK..(cz + 1) * CHUNK {
|
for z in cz * CHUNK..((cz + 1) * CHUNK).min(mh() as i32) {
|
||||||
for x in cx * CHUNK..(cx + 1) * CHUNK {
|
for x in cx * CHUNK..((cx + 1) * CHUNK).min(mw() as i32) {
|
||||||
let t = map.t[idx(x, z)];
|
let t = map.t[idx(x, z)];
|
||||||
let tex = Gfx::tile_tex(t.kind, t.owner == Owner::Player, t.owner == Owner::Hero, t.var);
|
let tex = Gfx::tile_tex(t.kind, t.owner == Owner::Player, t.owner == Owner::Hero, t.var);
|
||||||
let (ua, ub) = cell_uv(tex);
|
let (ua, ub) = cell_uv(tex);
|
||||||
@@ -510,14 +544,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
|
|||||||
ln.polyline(&edge_pts(tr, br, n), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
ln.polyline(&edge_pts(tr, br, n), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
||||||
}
|
}
|
||||||
if t.marked {
|
if t.marked {
|
||||||
let (a, b) = cell_uv(T_MARK);
|
mark_block(&mut mk, map, x, z);
|
||||||
let y = WALL_H + 0.12;
|
|
||||||
ov.quad(
|
|
||||||
[vec3(xf, y, zf), vec3(xf + 1.0, y, zf), vec3(xf + 1.0, y, zf + 1.0), vec3(xf, y, zf + 1.0)],
|
|
||||||
[vec2(a.x, a.y), vec2(b.x, a.y), vec2(b.x, b.y), vec2(a.x, b.y)],
|
|
||||||
[[255, 255, 255, 230]; 4],
|
|
||||||
[Vec3::Y; 4],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
||||||
if solid_at(map, x + dx, z + dz) {
|
if solid_at(map, x + dx, z + dz) {
|
||||||
@@ -550,7 +577,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(mb.into_mesh(atlas), ov.into_mesh(atlas), ln.into_plain_mesh())
|
[mb.into_mesh(atlas), ov.into_mesh(atlas), ln.into_plain_mesh(), mk.into_plain_mesh()]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subdivided, displaced face. pos(u, v) gives the undisplaced point (u, v in 0..1).
|
// Subdivided, displaced face. pos(u, v) gives the undisplaced point (u, v in 0..1).
|
||||||
@@ -601,6 +628,7 @@ struct Chunk {
|
|||||||
mesh: Option<Mesh>,
|
mesh: Option<Mesh>,
|
||||||
overlay: Option<Mesh>,
|
overlay: Option<Mesh>,
|
||||||
lines: Option<Mesh>,
|
lines: Option<Mesh>,
|
||||||
|
marks: Option<Mesh>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Overlay {
|
pub struct Overlay {
|
||||||
@@ -615,6 +643,8 @@ pub struct R3d {
|
|||||||
lit: Material,
|
lit: Material,
|
||||||
glow: Material,
|
glow: Material,
|
||||||
chunks: Vec<Chunk>,
|
chunks: Vec<Chunk>,
|
||||||
|
ncx: i32, // chunks per row / column for the current map size
|
||||||
|
ncz: i32,
|
||||||
pub outlines: bool, // tile outlines on/off
|
pub outlines: bool, // tile outlines on/off
|
||||||
wall_sub: usize, // wall face subdivisions (detail)
|
wall_sub: usize, // wall face subdivisions (detail)
|
||||||
pub max_lights: usize,
|
pub max_lights: usize,
|
||||||
@@ -647,8 +677,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 {
|
|||||||
|
|
||||||
impl R3d {
|
impl R3d {
|
||||||
pub fn new() -> R3d {
|
pub fn new() -> R3d {
|
||||||
let chunks = (0..NCH * NCH).map(|_| Chunk { sig: 0, mesh: None, overlay: None, lines: None }).collect();
|
R3d { lit: lit_material(), glow: glow_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS }
|
||||||
R3d { lit: lit_material(), glow: glow_material(), chunks, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changing the wall detail rebuilds all chunks
|
// Changing the wall detail rebuilds all chunks
|
||||||
@@ -665,20 +694,37 @@ impl R3d {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_chunks(&mut self, g: &Game, gfx: &Gfx) {
|
// (Re)creates the chunk grid when the map size changes
|
||||||
|
fn ensure_chunks(&mut self) {
|
||||||
|
let ncx = (mw() as i32 + CHUNK - 1) / CHUNK;
|
||||||
|
let ncz = (mh() as i32 + CHUNK - 1) / CHUNK;
|
||||||
|
if ncx != self.ncx || ncz != self.ncz {
|
||||||
|
self.ncx = ncx;
|
||||||
|
self.ncz = ncz;
|
||||||
|
self.chunks = (0..ncx * ncz).map(|_| Chunk { sig: 0, mesh: None, overlay: None, lines: None, marks: None }).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuilds visible chunks whose content changed (off-screen chunks are updated when they appear)
|
||||||
|
fn update_chunks(&mut self, g: &Game, gfx: &Gfx, m: &Mat4) {
|
||||||
|
self.ensure_chunks();
|
||||||
let fill = (g.gold / g.gold_cap()).clamp(0.0, 1.0);
|
let fill = (g.gold / g.gold_cap()).clamp(0.0, 1.0);
|
||||||
let bucket = (fill * 6.0) as u8;
|
let bucket = (fill * 6.0) as u8;
|
||||||
let mut amb: Option<Vec<f32>> = None;
|
let mut amb: Option<Vec<f32>> = None;
|
||||||
for cz in 0..NCH {
|
for cz in 0..self.ncz {
|
||||||
for cx in 0..NCH {
|
for cx in 0..self.ncx {
|
||||||
|
if !Self::chunk_visible(m, cx, cz) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let sig = chunk_sig(&g.map, cx, cz, bucket);
|
let sig = chunk_sig(&g.map, cx, cz, bucket);
|
||||||
let ch = &mut self.chunks[(cz * NCH + cx) as usize];
|
let ch = &mut self.chunks[(cz * self.ncx + cx) as usize];
|
||||||
if ch.sig != sig {
|
if ch.sig != sig {
|
||||||
let a = amb.get_or_insert_with(|| ambient_field(&g.map));
|
let a = amb.get_or_insert_with(|| ambient_field(&g.map));
|
||||||
let (m, o, l) = build_chunk(&g.map, a, cx, cz, bucket as f32 / 6.0, &gfx.atlas, self.wall_sub);
|
let [m, o, l, k] = build_chunk(&g.map, a, cx, cz, bucket as f32 / 6.0, &gfx.atlas, self.wall_sub);
|
||||||
ch.mesh = m;
|
ch.mesh = m;
|
||||||
ch.overlay = o;
|
ch.overlay = o;
|
||||||
ch.lines = l;
|
ch.lines = l;
|
||||||
|
ch.marks = k;
|
||||||
ch.sig = sig;
|
ch.sig = sig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -718,9 +764,10 @@ impl R3d {
|
|||||||
let (cx, cy) = g.map.core;
|
let (cx, cy) = g.map.core;
|
||||||
let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0);
|
let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0);
|
||||||
push(vec3(cx as f32 + 1.5, 1.2, cy as f32 + 1.5), 7.0, vec3(1.0, 0.25, 0.2), 1.1 + 0.2 * (t * 3.0).sin() * (1.5 - hp), 100.0);
|
push(vec3(cx as f32 + 1.5, 1.2, cy as f32 + 1.5), 7.0, vec3(1.0, 0.25, 0.2), 1.1 + 0.2 * (t * 3.0).sin() * (1.5 - hp), 100.0);
|
||||||
let (px, py) = g.map.portal;
|
for (k, &(px, py)) in g.map.portals.iter().enumerate() {
|
||||||
let owned = g.portal_owned();
|
let owned = g.map.portal_owned(k);
|
||||||
push(vec3(px as f32 + 1.5, 0.8, py as f32 + 1.5), 6.0, vec3(0.7, 0.3, 1.0), if owned { 1.0 } else { 0.35 }, 50.0);
|
push(vec3(px as f32 + 1.5, 0.8, py as f32 + 1.5), 6.0, vec3(0.7, 0.3, 1.0), if owned { 1.0 } else { 0.35 }, 20.0);
|
||||||
|
}
|
||||||
if g.gate_hp > 0.0 {
|
if g.gate_hp > 0.0 {
|
||||||
let (gx, gz) = g.gate_center();
|
let (gx, gz) = g.gate_center();
|
||||||
push(vec3(gx, 1.2, gz), 7.0, vec3(0.75, 0.85, 1.0), 1.0, 50.0);
|
push(vec3(gx, 1.2, gz), 7.0, vec3(0.75, 0.85, 1.0), 1.0, 50.0);
|
||||||
@@ -765,8 +812,8 @@ impl R3d {
|
|||||||
|
|
||||||
pub fn draw(&mut self, g: &Game, gfx: &Gfx, cam: &Cam, time: f64, ov: &Overlay) {
|
pub fn draw(&mut self, g: &Game, gfx: &Gfx, cam: &Cam, time: f64, ov: &Overlay) {
|
||||||
let t = time as f32;
|
let t = time as f32;
|
||||||
self.update_chunks(g, gfx);
|
|
||||||
let m = cam.matrix();
|
let m = cam.matrix();
|
||||||
|
self.update_chunks(g, gfx, &m);
|
||||||
EYE.with(|e| e.set(cam.eye_dir()));
|
EYE.with(|e| e.set(cam.eye_dir()));
|
||||||
set_camera(&cam.camera());
|
set_camera(&cam.camera());
|
||||||
|
|
||||||
@@ -777,12 +824,12 @@ impl R3d {
|
|||||||
self.lit.set_uniform_array("LightCol", &lc[..]);
|
self.lit.set_uniform_array("LightCol", &lc[..]);
|
||||||
self.lit.set_uniform("LightCount", count);
|
self.lit.set_uniform("LightCount", count);
|
||||||
gl_use_material(&self.lit);
|
gl_use_material(&self.lit);
|
||||||
for cz in 0..NCH {
|
for cz in 0..self.ncz {
|
||||||
for cx in 0..NCH {
|
for cx in 0..self.ncx {
|
||||||
if !Self::chunk_visible(&m, cx, cz) {
|
if !Self::chunk_visible(&m, cx, cz) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(mesh) = &self.chunks[(cz * NCH + cx) as usize].mesh {
|
if let Some(mesh) = &self.chunks[(cz * self.ncx + cx) as usize].mesh {
|
||||||
draw_mesh(mesh);
|
draw_mesh(mesh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -810,6 +857,9 @@ impl R3d {
|
|||||||
if let Some(o) = &c.overlay {
|
if let Some(o) = &c.overlay {
|
||||||
draw_mesh(o);
|
draw_mesh(o);
|
||||||
}
|
}
|
||||||
|
if let Some(k) = &c.marks {
|
||||||
|
draw_mesh(k);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (k, tr) in g.torches.iter().enumerate() {
|
for (k, tr) in g.torches.iter().enumerate() {
|
||||||
let (x, z) = tr.pos();
|
let (x, z) = tr.pos();
|
||||||
@@ -1008,9 +1058,9 @@ fn draw_landmarks_glow(g: &Game, t: f32) {
|
|||||||
draw_sphere(c, 0.75 * pulse, None, Color::new(0.4 + 0.5 * hp, 0.05, 0.08, 1.0));
|
draw_sphere(c, 0.75 * pulse, None, Color::new(0.4 + 0.5 * hp, 0.05, 0.08, 1.0));
|
||||||
draw_sphere(c, 0.95 * pulse, None, Color::new(1.0, 0.2, 0.15, 0.25));
|
draw_sphere(c, 0.95 * pulse, None, Color::new(1.0, 0.2, 0.15, 0.25));
|
||||||
draw_sphere(c + vec3(-0.2, 0.25, 0.2), 0.18, None, Color::new(1.0, 0.85, 0.7, 0.8));
|
draw_sphere(c + vec3(-0.2, 0.25, 0.2), 0.18, None, Color::new(1.0, 0.85, 0.7, 0.8));
|
||||||
// portal
|
// portals
|
||||||
let (px, py) = g.map.portal;
|
for (pk, &(px, py)) in g.map.portals.iter().enumerate() {
|
||||||
let owned = g.portal_owned();
|
let owned = g.map.portal_owned(pk);
|
||||||
let pc = vec3(px as f32 + 1.5, 0.03, py as f32 + 1.5);
|
let pc = vec3(px as f32 + 1.5, 0.03, py as f32 + 1.5);
|
||||||
let arm = if owned { Color::new(0.8, 0.4, 1.0, 1.0) } else { Color::new(0.45, 0.42, 0.5, 1.0) };
|
let arm = if owned { Color::new(0.8, 0.4, 1.0, 1.0) } else { Color::new(0.45, 0.42, 0.5, 1.0) };
|
||||||
disc(pc, 1.35, if owned { Color::new(0.2, 0.05, 0.3, 0.9) } else { Color::new(0.1, 0.1, 0.12, 0.9) }, 32);
|
disc(pc, 1.35, if owned { Color::new(0.2, 0.05, 0.3, 0.9) } else { Color::new(0.1, 0.1, 0.12, 0.9) }, 32);
|
||||||
@@ -1024,6 +1074,7 @@ fn draw_landmarks_glow(g: &Game, t: f32) {
|
|||||||
draw_cube(p, Vec3::splat(0.05 + 0.06 * (1.0 - f)), None, Color::new(arm.r, arm.g, arm.b, 0.4 + 0.6 * (1.0 - f)));
|
draw_cube(p, Vec3::splat(0.05 + 0.06 * (1.0 - f)), None, Color::new(arm.r, arm.g, arm.b, 0.4 + 0.6 * (1.0 - f)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// gate light
|
// gate light
|
||||||
if g.gate_hp > 0.0 {
|
if g.gate_hp > 0.0 {
|
||||||
let (gx, gz) = g.gate_center();
|
let (gx, gz) = g.gate_center();
|
||||||
|
|||||||
+26
-2
@@ -7,7 +7,7 @@ use crate::settings::Settings;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub const FORMAT: &str = "dungeoncraft-save";
|
pub const FORMAT: &str = "dungeoncraft-save";
|
||||||
pub const VERSION: u32 = 1;
|
pub const VERSION: u32 = 2; // 2: variable map size, several portals, packed tiles
|
||||||
pub const AUTOSAVE_KEY: &str = "dc.autosave";
|
pub const AUTOSAVE_KEY: &str = "dc.autosave";
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -48,6 +48,7 @@ pub fn from_bytes(data: &[u8]) -> Result<Loaded, &'static str> {
|
|||||||
return Err("save.err_version");
|
return Err("save.err_version");
|
||||||
}
|
}
|
||||||
let mut game = s.game;
|
let mut game = s.game;
|
||||||
|
game.map.activate();
|
||||||
game.recount();
|
game.recount();
|
||||||
Ok(Loaded { game, camera: s.camera, settings: s.settings })
|
Ok(Loaded { game, camera: s.camera, settings: s.settings })
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn save_roundtrip() {
|
fn save_roundtrip() {
|
||||||
let mut g = Game::new(42);
|
let mut g = Game::new(42, 96, 128);
|
||||||
let (cx, cy) = g.map.core;
|
let (cx, cy) = g.map.core;
|
||||||
g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true);
|
g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true);
|
||||||
for _ in 0..3000 {
|
for _ in 0..3000 {
|
||||||
@@ -76,6 +77,9 @@ mod tests {
|
|||||||
assert!(l.game.map.t.iter().zip(g.map.t.iter()).all(|(a, b)| a.kind == b.kind && a.owner == b.owner && a.marked == b.marked));
|
assert!(l.game.map.t.iter().zip(g.map.t.iter()).all(|(a, b)| a.kind == b.kind && a.owner == b.owner && a.marked == b.marked));
|
||||||
assert_eq!(l.settings.unwrap().language, "de");
|
assert_eq!(l.settings.unwrap().language, "de");
|
||||||
assert_eq!(l.camera.unwrap().tx, 10.0);
|
assert_eq!(l.camera.unwrap().tx, 10.0);
|
||||||
|
assert_eq!((l.game.map.w, l.game.map.h), (96, 128));
|
||||||
|
assert_eq!(l.game.map.portals.len(), g.map.portals.len());
|
||||||
|
assert!(l.game.map.t.iter().zip(g.map.t.iter()).all(|(a, b)| a.var == b.var && a.hp == b.hp && a.food == b.food && a.gold == b.gold));
|
||||||
// the loaded game keeps running identically
|
// the loaded game keeps running identically
|
||||||
let mut a = l.game;
|
let mut a = l.game;
|
||||||
let mut b = g;
|
let mut b = g;
|
||||||
@@ -87,4 +91,24 @@ mod tests {
|
|||||||
assert_eq!(a.ents.len(), b.ents.len());
|
assert_eq!(a.ents.len(), b.ents.len());
|
||||||
assert!(from_bytes(b"{\"format\":\"x\"}").is_err());
|
assert!(from_bytes(b"{\"format\":\"x\"}").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn huge_map() {
|
||||||
|
let t0 = std::time::Instant::now();
|
||||||
|
let mut g = Game::new(7, 256, 256);
|
||||||
|
let gen = t0.elapsed();
|
||||||
|
assert_eq!(g.map.portals.len(), 16);
|
||||||
|
assert_eq!(g.map.treasures.len(), 15);
|
||||||
|
let (cx, cy) = g.map.core;
|
||||||
|
g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true);
|
||||||
|
let t1 = std::time::Instant::now();
|
||||||
|
for _ in 0..(120 * 20) {
|
||||||
|
g.update(0.05);
|
||||||
|
}
|
||||||
|
let sim = t1.elapsed();
|
||||||
|
let json = to_json(&g, &Cam::new(0.0, 0.0), &Settings::default());
|
||||||
|
println!("256x256: generate {:?}, 2 min simulated in {:?}, savegame {} KB", gen, sim, json.len() / 1024);
|
||||||
|
let l = from_bytes(json.as_bytes()).expect("load");
|
||||||
|
assert_eq!(l.game.map.t.len(), 256 * 256);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -145,6 +145,8 @@ pub struct Settings {
|
|||||||
// game
|
// game
|
||||||
pub game_speed: f32, // 1 or 2
|
pub game_speed: f32, // 1 or 2
|
||||||
pub hints: bool,
|
pub hints: bool,
|
||||||
|
pub map_w: usize, // size of new maps (64..256)
|
||||||
|
pub map_h: usize,
|
||||||
// key bindings: action id -> key name
|
// key bindings: action id -> key name
|
||||||
pub keys: BTreeMap<String, String>,
|
pub keys: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
@@ -166,6 +168,8 @@ impl Default for Settings {
|
|||||||
ui_scale: 2,
|
ui_scale: 2,
|
||||||
game_speed: 1.0,
|
game_speed: 1.0,
|
||||||
hints: true,
|
hints: true,
|
||||||
|
map_w: 64,
|
||||||
|
map_h: 64,
|
||||||
keys: BTreeMap::new(),
|
keys: BTreeMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ fn mmss(t: f32) -> String {
|
|||||||
|
|
||||||
impl Ui {
|
impl Ui {
|
||||||
pub fn new() -> Ui {
|
pub fn new() -> Ui {
|
||||||
let img = Image::gen_image_color(MW as u16, MH as u16, BLACK);
|
let img = Image::gen_image_color(64, 64, BLACK);
|
||||||
let tex = Texture2D::from_image(&img);
|
let tex = Texture2D::from_image(&img);
|
||||||
tex.set_filter(FilterMode::Nearest);
|
tex.set_filter(FilterMode::Nearest);
|
||||||
Ui {
|
Ui {
|
||||||
@@ -199,7 +199,7 @@ impl Ui {
|
|||||||
|
|
||||||
pub fn minimap_world(&self, x: f32, y: f32) -> (f32, f32) {
|
pub fn minimap_world(&self, x: f32, y: f32) -> (f32, f32) {
|
||||||
let r = self.minimap_rect;
|
let r = self.minimap_rect;
|
||||||
((x - r.x) / r.w * MW as f32, (y - r.y) / r.h * MH as f32)
|
((x - r.x) / r.w * mw() as f32, (y - r.y) / r.h * mh() as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn click(&mut self, x: f32, y: f32, g: &Game) -> UiAction {
|
pub fn click(&mut self, x: f32, y: f32, g: &Game) -> UiAction {
|
||||||
@@ -344,6 +344,13 @@ impl Ui {
|
|||||||
let mm_y = tb_y - mm_h;
|
let mm_y = tb_y - mm_h;
|
||||||
gfx.window_titled(mm_x, mm_y, mm_cols, mm_rows, S(), false, FRAME, BG, &tr("ui.map"), FRAME_HI);
|
gfx.window_titled(mm_x, mm_y, mm_cols, mm_rows, S(), false, FRAME, BG, &tr("ui.map"), FRAME_HI);
|
||||||
self.rects.push(Rect::new(mm_x, mm_y, mm_w, mm_h));
|
self.rects.push(Rect::new(mm_x, mm_y, mm_w, mm_h));
|
||||||
|
// the minimap image follows the map size
|
||||||
|
if self.minimap_img.width as usize != mw() || self.minimap_img.height as usize != mh() {
|
||||||
|
self.minimap_img = Image::gen_image_color(mw() as u16, mh() as u16, BLACK);
|
||||||
|
self.minimap_tex = Texture2D::from_image(&self.minimap_img);
|
||||||
|
self.minimap_tex.set_filter(FilterMode::Nearest);
|
||||||
|
self.minimap_t = -1.0;
|
||||||
|
}
|
||||||
if now - self.minimap_t > 0.25 {
|
if now - self.minimap_t > 0.25 {
|
||||||
self.minimap_t = now;
|
self.minimap_t = now;
|
||||||
for (i, t) in g.map.t.iter().enumerate() {
|
for (i, t) in g.map.t.iter().enumerate() {
|
||||||
@@ -356,12 +363,14 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
self.minimap_tex.update(&self.minimap_img);
|
self.minimap_tex.update(&self.minimap_img);
|
||||||
}
|
}
|
||||||
let map_px = (mm_w - CW() * 2.0).min(mm_h - CH() * 2.0);
|
// keep the map's aspect ratio inside the window
|
||||||
let mrx = mm_x + (mm_w - map_px) * 0.5;
|
let avail = (mm_w - CW() * 2.0).min(mm_h - CH() * 2.0);
|
||||||
let mry = mm_y + CH() + (mm_h - CH() * 2.0 - map_px) * 0.5;
|
let ms = avail / mw().max(mh()) as f32;
|
||||||
self.minimap_rect = Rect::new(mrx, mry, map_px, map_px);
|
let (map_w, map_h) = (mw() as f32 * ms, mh() as f32 * ms);
|
||||||
draw_texture_ex(&self.minimap_tex, mrx, mry, WHITE, DrawTextureParams { dest_size: Some(vec2(map_px, map_px)), ..Default::default() });
|
let mrx = mm_x + (mm_w - map_w) * 0.5;
|
||||||
let ms = map_px / MW as f32;
|
let mry = mm_y + CH() + (mm_h - CH() * 2.0 - map_h) * 0.5;
|
||||||
|
self.minimap_rect = Rect::new(mrx, mry, map_w, map_h);
|
||||||
|
draw_texture_ex(&self.minimap_tex, mrx, mry, WHITE, DrawTextureParams { dest_size: Some(vec2(map_w, map_h)), ..Default::default() });
|
||||||
for e in &g.ents {
|
for e in &g.ents {
|
||||||
if e.alive && !e.held {
|
if e.alive && !e.held {
|
||||||
let c = if e.hero { Color::new(0.6, 0.85, 1.0, 1.0) } else if e.kind == EK::Worker { Color::new(1.0, 0.6, 0.3, 1.0) } else { Color::new(1.0, 0.25, 0.25, 1.0) };
|
let c = if e.hero { Color::new(0.6, 0.85, 1.0, 1.0) } else if e.kind == EK::Worker { Color::new(1.0, 0.6, 0.3, 1.0) } else { Color::new(1.0, 0.25, 0.25, 1.0) };
|
||||||
@@ -377,7 +386,7 @@ impl Ui {
|
|||||||
for k in 0..4 {
|
for k in 0..4 {
|
||||||
let a = corners[k];
|
let a = corners[k];
|
||||||
let b = corners[(k + 1) % 4];
|
let b = corners[(k + 1) % 4];
|
||||||
let clampp = |p: Vec2| vec2(mrx + p.x.clamp(0.0, MW as f32) * ms, mry + p.y.clamp(0.0, MH as f32) * ms);
|
let clampp = |p: Vec2| vec2(mrx + p.x.clamp(0.0, mw() as f32) * ms, mry + p.y.clamp(0.0, mh() as f32) * ms);
|
||||||
let (pa, pb) = (clampp(a), clampp(b));
|
let (pa, pb) = (clampp(a), clampp(b));
|
||||||
draw_line(pa.x, pa.y, pb.x, pb.y, 1.5, WHITE);
|
draw_line(pa.x, pa.y, pb.x, pb.y, 1.5, WHITE);
|
||||||
}
|
}
|
||||||
@@ -623,7 +632,7 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
|
|||||||
}
|
}
|
||||||
Kind::Portal => {
|
Kind::Portal => {
|
||||||
out.push((tr("tile.portal"), MANA));
|
out.push((tr("tile.portal"), MANA));
|
||||||
out.push((tr(if g.portal_owned() { "portal.claimed" } else { "portal.neutral" }), DIM));
|
out.push((tr(if t.owner == Owner::Player { "portal.claimed" } else { "portal.neutral" }), DIM));
|
||||||
}
|
}
|
||||||
Kind::Gate => {
|
Kind::Gate => {
|
||||||
out.push((tr("tile.gate"), BLUE));
|
out.push((tr("tile.gate"), BLUE));
|
||||||
|
|||||||
Reference in new issue
Block a user