Title screen, pause menu, settings dialog and savegames

- title screen (continue/new/load/settings/help) and Esc pause menu
- settings: language & sound, camera, graphics, game, rebindable keys;
  stored in localStorage and in every savegame (always applied on load)
- savegames as JSON (.dcsave): download, load via file picker or drag & drop
- autosave to localStorage every 2 minutes and when the tab is hidden/closed
- configurable wall detail, light count, interface size, game speed, hints
- audio behind a cargo feature so native tests run (cargo test --no-default-features)
- savegame round-trip test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-02 23:08:33 +01:00
1 parent fbf6f223fc
commit fb7dab3683
17 files changed
+1742 -345

No files matched your search

Generated
+98
View File
@@ -106,6 +106,8 @@ name = "dungeoncraft"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"macroquad", "macroquad",
"serde",
"serde_json",
] ]
[[package]] [[package]]
@@ -186,6 +188,12 @@ dependencies = [
"png", "png",
] ]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "lewton" name = "lewton"
version = "0.9.4" version = "0.9.4"
@@ -245,6 +253,12 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]] [[package]]
name = "miniquad" name = "miniquad"
version = "0.4.11" version = "0.4.11"
@@ -323,6 +337,15 @@ dependencies = [
"miniz_oxide 0.8.9", "miniz_oxide 0.8.9",
] ]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "quad-alsa-sys" name = "quad-alsa-sys"
version = "0.3.2" version = "0.3.2"
@@ -351,6 +374,58 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.10" version = "0.3.10"
@@ -366,6 +441,17 @@ dependencies = [
"maybe-uninit", "maybe-uninit",
] ]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.25.1" version = "0.25.1"
@@ -375,6 +461,12 @@ dependencies = [
"core_maths", "core_maths",
] ]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -402,3 +494,9 @@ name = "zlib-rs"
version = "0.6.8" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+8 -1
View File
@@ -4,7 +4,14 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
macroquad = { version = "0.4", features = ["audio"] } macroquad = { version = "0.4" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[features]
default = ["audio"]
# macroquad audio (needs libasound natively); disable for native tests: cargo test --no-default-features
audio = ["macroquad/audio"]
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
+7 -2
View File
@@ -29,9 +29,14 @@ An original dungeon management game (genre: dungeon building & defence) that run
- Spells: 0 lightning, X heal, V summon imp - Spells: 0 lightning, X heal, V summon imp
- Torches: placed automatically when ground is claimed; foreign areas are dimly visible - Torches: placed automatically when ground is claimed; foreign areas are dimly visible
## Menus, savegames, settings
- **Title screen** (Continue, New Game, Load, Settings, Help) and **pause menu** on Esc (Resume, Save, Load, Settings, Help, Main Menu)
- Savegames as **JSON** (`*.dcsave`, format tag + version, ~300 KB): **save = download**, **load = file picker or drag & drop** onto the game window
- **Autosave** in the browser (localStorage) every 2 minutes and when the tab is hidden/closed; "Continue" on the title screen
- Settings: language + sound, camera (rotation/zoom speed, edge scrolling, invert mouse), graphics (tile outlines, wall detail, light sources, interface size), game (speed 1×/2×, hints), **key bindings** (conflicts swap keys; Esc and arrow keys are fixed)
- Settings are stored in the browser **and** in every savegame; loading a savegame **always applies** its settings
## Planned ## Planned
- Menu: save (download `*.dcsave`), load (file picker + drag & drop), autosave in the browser (localStorage)
- Settings: language + sound, camera, graphics, game, key bindings – stored in the browser **and** in the savegame
- Music: own **XM/MOD replayer in Rust**, shared mixer for music + sound effects (streamed via a JS audio plugin, replaces macroquad audio) - Music: own **XM/MOD replayer in Rust**, shared mixer for music + sound effects (streamed via a JS audio plugin, replaces macroquad audio)
- Soundtrack (composed as `.xm`, editable in MilkyTracker): title/menu, several calm build tracks, battle, victory/defeat jingles; **the title melody is a recognisable leitmotif in every piece** - Soundtrack (composed as `.xm`, editable in MilkyTracker): title/menu, several calm build tracks, battle, victory/defeat jingles; **the title melody is a recognisable leitmotif in every piece**
+12 -1
View File
@@ -34,12 +34,17 @@ python3 -m http.server 8080 -d www # open http://localhost:8080
### Testing the game logic ### Testing the game logic
The game logic (`src/game.rs`, `src/map.rs`) does not depend on macroquad and contains a headless simulation test. The game logic (`src/game.rs`, `src/map.rs`) does not depend on macroquad. The tests (a headless simulation of whole games and a savegame round trip) run natively without the audio feature:
```sh
cargo test --release --no-default-features
```
## Controls ## Controls
| Input | Action | | Input | Action |
|---|---| |---|---|
| Esc | Pause menu (save, load, settings) |
| Left click / drag | Use the active tool (dig and rooms: drag a rectangle) | | Left click / drag | Use the active tool (dig and rooms: drag a rectangle) |
| Right click | Drop a unit from your hand | | Right click | Drop a unit from your hand |
| Right drag, WASD, arrows, screen edge | Move the camera | | Right drag, WASD, arrows, screen edge | Move the camera |
@@ -51,6 +56,12 @@ The game logic (`src/game.rs`, `src/map.rs`) does not depend on macroquad and co
| G | Tile outlines on/off | | G | Tile outlines on/off |
| H | Help | | H | Help |
All keys except Esc and the arrow keys can be changed in Settings → Keys.
## Savegames
Use *Save Game* in the pause menu to download a `.dcsave` file (JSON). Load it again via *Load Game* or by dropping the file onto the game window. The game also autosaves into the browser every two minutes and when the tab is closed; *Continue* on the title screen resumes from there.
## Adding a language ## Adding a language
1. Copy `lang/en.lang` to `lang/<code>.lang` and translate the texts (placeholders `{0}`, `{1}` stay as they are). 1. Copy `lang/en.lang` to `lang/<code>.lang` and translate the texts (placeholders `{0}`, `{1}` stay as they are).
+69
View File
@@ -197,3 +197,72 @@ tile.marked = zum Graben markiert
# ---- Sprachauswahl # ---- Sprachauswahl
langsel.title = Sprache langsel.title = Sprache
langsel.hint = Klicke eine Sprache an. Du kannst sie später in den Einstellungen ändern. langsel.hint = Klicke eine Sprache an. Du kannst sie später in den Einstellungen ändern.
# ---- Menüs
menu.subtitle = Graben. Bauen. Verteidigen. Erobern.
menu.continue = Fortsetzen
menu.new = Neues Spiel
menu.load = Spiel laden
menu.save = Spiel speichern
menu.settings = Einstellungen
menu.help = Hilfe
menu.resume = Weiterspielen
menu.main = Hauptmenü
menu.back = Zurück
menu.paused = Pause
menu.drop_hint = Tipp: Du kannst eine .dcsave-Datei auch einfach aufs Spielfenster ziehen, um sie zu laden.
# ---- Einstellungen
set.tab.sound = Sprache & Ton
set.tab.camera = Kamera
set.tab.graphics = Grafik
set.tab.game = Spiel
set.tab.keys = Tasten
set.language = Sprache
set.volume = Effekt-Lautstärke
set.music = Musik-Lautstärke
set.mute = Stumm
set.rotate_speed = Drehgeschwindigkeit
set.zoom_speed = Zoomgeschwindigkeit
set.edge_scroll = Bildschirmrand-Scrollen
set.invert = Mausdrehung umkehren
set.outlines = Kachel-Umrisse
set.wall_detail = Wanddetail
set.lights = Lichtquellen
set.ui_scale = Oberflächengröße
set.game_speed = Spieltempo
set.hints = Hinweise
set.keys_hint = Aktion anklicken und Taste drücken. Bereits belegte Tasten werden getauscht.
set.press_key = Taste drücken für: {0}
set.esc_cancel = Esc bricht ab
set.reset_keys = Tasten zurücksetzen
val.on = An
val.off = Aus
val.low = Niedrig
val.medium = Mittel
val.high = Hoch
val.normal = Normal
val.large = Groß
# ---- Namen der Tastenbelegung
key.cam_forward = Kamera vor
key.cam_back = Kamera zurück
key.cam_left = Kamera links
key.cam_right = Kamera rechts
key.rotate_left = Links drehen
key.rotate_right = Rechts drehen
key.tilt_up = Neigen hoch
key.tilt_down = Neigen runter
key.pause = Pause
key.mute = Ton an/aus
key.volume_up = Lauter
key.volume_down = Leiser
key.outlines = Kachel-Umrisse
key.help = Hilfe
# ---- Spielstände
save.saved = Spiel gespeichert: {0}
save.loaded = Spiel geladen.
save.err_invalid = Diese Datei ist kein gültiger DungeonCraft-Spielstand.
save.err_version = Dieser Spielstand stammt aus einer neueren DungeonCraft-Version.
save.choose = Spielstand-Datei auswählen ...
+69
View File
@@ -198,3 +198,72 @@ tile.marked = marked for digging
# ---- language selection # ---- language selection
langsel.title = Language langsel.title = Language
langsel.hint = Click a language. You can change it later in the settings. langsel.hint = Click a language. You can change it later in the settings.
# ---- menus
menu.subtitle = Dig. Build. Defend. Conquer.
menu.continue = Continue
menu.new = New Game
menu.load = Load Game
menu.save = Save Game
menu.settings = Settings
menu.help = Help
menu.resume = Resume
menu.main = Main Menu
menu.back = Back
menu.paused = Paused
menu.drop_hint = Tip: you can also drop a .dcsave file onto the game window to load it.
# ---- settings
set.tab.sound = Language & Sound
set.tab.camera = Camera
set.tab.graphics = Graphics
set.tab.game = Game
set.tab.keys = Keys
set.language = Language
set.volume = Sound volume
set.music = Music volume
set.mute = Mute
set.rotate_speed = Rotation speed
set.zoom_speed = Zoom speed
set.edge_scroll = Screen edge scrolling
set.invert = Invert mouse rotation
set.outlines = Tile outlines
set.wall_detail = Wall detail
set.lights = Light sources
set.ui_scale = Interface size
set.game_speed = Game speed
set.hints = Hints
set.keys_hint = Click an action and press a key. Keys already in use are swapped.
set.press_key = Press a key for: {0}
set.esc_cancel = Esc cancels
set.reset_keys = Reset keys
val.on = On
val.off = Off
val.low = Low
val.medium = Medium
val.high = High
val.normal = Normal
val.large = Large
# ---- key binding names
key.cam_forward = Camera forward
key.cam_back = Camera back
key.cam_left = Camera left
key.cam_right = Camera right
key.rotate_left = Rotate left
key.rotate_right = Rotate right
key.tilt_up = Tilt up
key.tilt_down = Tilt down
key.pause = Pause
key.mute = Sound on/off
key.volume_up = Volume up
key.volume_down = Volume down
key.outlines = Tile outlines
key.help = Help
# ---- savegames
save.saved = Game saved: {0}
save.loaded = Game loaded.
save.err_invalid = This file is not a valid DungeonCraft savegame.
save.err_version = This savegame comes from a newer version of DungeonCraft.
save.choose = Choose a savegame file ...
+13 -5
View File
@@ -1,6 +1,7 @@
// Game logic: units, AI, economy, hero waves. // Game logic: units, AI, economy, hero waves.
use crate::map::*; use crate::map::*;
use serde::{Deserialize, Serialize};
pub const CORE_HP: f32 = 2000.0; pub const CORE_HP: f32 = 2000.0;
pub const GATE_HP: f32 = 3000.0; pub const GATE_HP: f32 = 3000.0;
@@ -71,7 +72,7 @@ pub struct SfxEvent {
pub const MANA_MAX: f32 = 400.0; pub const MANA_MAX: f32 = 400.0;
pub const CARRY_MAX: i32 = 300; pub const CARRY_MAX: i32 = 300;
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum EK { pub enum EK {
Worker, Worker,
Kobold, Kobold,
@@ -112,7 +113,7 @@ pub fn st(k: EK) -> St {
} }
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Job { pub enum Job {
None, None,
Dig(usize), Dig(usize),
@@ -129,7 +130,7 @@ pub enum Job {
Rally, Rally,
} }
#[derive(Clone)] #[derive(Clone, Serialize, Deserialize)]
pub struct Ent { pub struct Ent {
pub id: u32, pub id: u32,
pub kind: EK, pub kind: EK,
@@ -217,6 +218,7 @@ pub struct Fx {
pub col: u32, pub col: u32,
} }
#[derive(Serialize, Deserialize)]
pub struct Game { pub struct Game {
pub map: Map, pub map: Map,
pub ents: Vec<Ent>, pub ents: Vec<Ent>,
@@ -231,7 +233,9 @@ pub struct Game {
pub warned: bool, pub warned: bool,
pub portal_timer: f32, pub portal_timer: f32,
pub payday: f32, pub payday: f32,
#[serde(skip)]
pub fx: Vec<Fx>, pub fx: Vec<Fx>,
#[serde(skip)]
pub msgs: Vec<Msg>, pub msgs: Vec<Msg>,
pub state: u8, // 0 running, 1 victory, 2 defeat pub state: u8, // 0 running, 1 victory, 2 defeat
pub paused: bool, pub paused: bool,
@@ -241,14 +245,17 @@ pub struct Game {
pub gate_hit_t: f32, pub gate_hit_t: f32,
pub gate_stage: u32, // defender thresholds triggered (0..3) pub gate_stage: u32, // defender thresholds triggered (0..3)
pub flag: Option<usize>, pub flag: Option<usize>,
#[serde(skip)]
pub events: Vec<SfxEvent>, pub events: Vec<SfxEvent>,
pub last_wave_value: f32, pub last_wave_value: f32,
pub torches: Vec<Torch>, pub torches: Vec<Torch>,
#[serde(skip)]
pub hints: bool, // show hint messages (setting)
hint_t: [f32; 16], hint_t: [f32; 16],
} }
// torch on a wall face: wall tile + direction (index into DIR4) towards the open side // torch on a wall face: wall tile + direction (index into DIR4) towards the open side
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Torch { pub struct Torch {
pub wall: usize, pub wall: usize,
pub dir: usize, pub dir: usize,
@@ -315,6 +322,7 @@ impl Game {
events: Vec::new(), events: Vec::new(),
last_wave_value: 0.0, last_wave_value: 0.0,
torches: Vec::new(), torches: Vec::new(),
hints: true,
hint_t: [-1000.0; 16], hint_t: [-1000.0; 16],
}; };
let (cx, cy) = g.map.core; let (cx, cy) = g.map.core;
@@ -391,7 +399,7 @@ impl Game {
} }
fn hint(&mut self, slot: usize, key: &'static str) { fn hint(&mut self, slot: usize, key: &'static str) {
if self.time - self.hint_t[slot] > 30.0 { if self.hints && self.time - self.hint_t[slot] > 30.0 {
self.hint_t[slot] = self.time; self.hint_t[slot] = self.time;
self.msg(key, vec![]); self.msg(key, vec![]);
} }
+452 -222
View File
@@ -4,19 +4,28 @@ mod game;
mod gfx; mod gfx;
mod i18n; mod i18n;
mod map; mod map;
mod menu;
mod platform; mod platform;
mod render3d; mod render3d;
mod save;
mod settings;
mod sound; mod sound;
mod ui; mod ui;
use game::Game; 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 render3d::{Cam, Overlay, R3d}; use render3d::{Cam, Overlay, R3d};
use settings::{bindable_keys, Action, Settings};
use sound::SoundBank; use sound::SoundBank;
use ui::{hover_info, Tool, Ui, UiAction, TOOLS}; use ui::{hover_info, Tool, Ui, UiAction, TOOLS};
const SETTINGS_KEY: &str = "dc.settings";
const AUTOSAVE_EVERY: f32 = 120.0; // seconds of play between autosaves
const AUTOSAVE_CACHE_EVERY: f32 = 15.0; // snapshot kept in JS, written when the tab is left
fn conf() -> macroquad::conf::Conf { fn conf() -> macroquad::conf::Conf {
macroquad::conf::Conf { macroquad::conf::Conf {
miniquad_conf: miniquad::conf::Conf { miniquad_conf: miniquad::conf::Conf {
@@ -52,215 +61,389 @@ fn next_seed(seed: u32) -> u32 {
seed.wrapping_mul(1103515245).wrapping_add(12345) | 1 seed.wrapping_mul(1103515245).wrapping_add(12345) | 1
} }
fn apply_settings(s: &Settings, r3d: &mut R3d, sounds: &mut SoundBank) {
i18n::set_language(&s.language);
ui::set_ui_scale(s.ui_scale as f32);
r3d.outlines = s.outlines;
r3d.set_wall_detail(s.wall_subdivisions());
r3d.max_lights = s.max_lights as usize;
sounds.volume = s.volume;
sounds.muted = s.muted;
}
fn store_settings(s: &Settings) {
platform::storage_set(SETTINGS_KEY, &s.to_json());
}
// "dungeoncraft_2026-11-02_2145.dcsave" (UTC)
fn save_file_name() -> String {
let secs = miniquad::date::now() as i64;
let days = secs.div_euclid(86400);
let rem = secs.rem_euclid(86400);
// civil date from days since 1970-01-01 (Howard Hinnant's algorithm)
let z = days + 719468;
let era = z.div_euclid(146097);
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };
format!("dungeoncraft_{:04}-{:02}-{:02}_{:02}{:02}.dcsave", y, m, d, rem / 3600, rem % 3600 / 60)
}
struct App {
game: Game,
cam: Cam,
settings: Settings,
seed: u32,
status: Option<(String, f64)>,
}
impl App {
fn set_status(&mut self, text: String, now: f64) {
self.status = Some((text, now));
}
// Load a savegame (settings in the savegame are always applied)
fn load_bytes(&mut self, bytes: &[u8], ui: &mut Ui, menu: &mut Menu, r3d: &mut R3d, sounds: &mut SoundBank, now: f64) {
match save::from_bytes(bytes) {
Ok(l) => {
self.game = l.game;
if let Some(c) = l.camera {
self.cam = c;
}
if let Some(s) = l.settings {
self.settings = s;
apply_settings(&self.settings, r3d, sounds);
store_settings(&self.settings);
}
self.game.hints = self.settings.hints;
r3d.invalidate();
menu.page = None;
ui.show_help = false;
ui.log.clear();
ui.log.push((Msg { key: "save.loaded", args: vec![] }, now));
self.status = None;
}
Err(key) => self.set_status(i18n::tr(key), now),
}
}
fn autosave_json(&self) -> String {
save::to_json(&self.game, &self.cam, &self.settings)
}
}
#[macroquad::main(conf)] #[macroquad::main(conf)]
async fn main() { async fn main() {
let gfx = Gfx::new(); let gfx = Gfx::new();
let mut r3d = R3d::new(); let mut r3d = R3d::new();
let mut sounds = SoundBank::new(); let mut sounds = SoundBank::new();
sounds.generate_all().await; sounds.generate_all().await;
let mut seed = (miniquad::date::now() * 1000.0) as u32 | 1;
let (mut game, mut cam) = new_game(seed);
let mut ui = Ui::new(); let mut ui = Ui::new();
// language: stored choice, otherwise ask on first start let mut menu = Menu::new();
match platform::storage_get("dc.lang") {
Some(code) if i18n::set_language(&code) => {} // settings: stored in the browser; on the very first start the player picks a language
_ => ui.show_langsel = true, let stored = platform::storage_get(SETTINGS_KEY).and_then(|s| Settings::from_json(&s));
let mut settings = stored.clone().unwrap_or_default();
if stored.is_none() {
match platform::storage_get("dc.lang") {
Some(code) => settings.language = code,
None => ui.show_langsel = true,
}
} }
apply_settings(&settings, &mut r3d, &mut sounds);
ui.show_help = false;
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);
let mut app = App { game, cam, settings, seed, status: None };
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 };
let mut autosave_t = 0.0f32;
let mut cache_t = 0.0f32;
loop { loop {
let dt = get_frame_time().min(0.1); let dt = get_frame_time().min(0.1);
let now = get_time(); let now = get_time();
let (mx, my) = mouse_position(); let (mx, my) = mouse_position();
let mut settings_changed = false;
// ---------------- savegame files: file picker result or dropped onto the window
let mut incoming: Option<Vec<u8>> = platform::take_file();
for f in get_dropped_files() {
if let Some(b) = f.bytes {
incoming = Some(b);
}
}
if let Some(bytes) = incoming {
app.load_bytes(&bytes, &mut ui, &mut menu, &mut r3d, &mut sounds, now);
}
// ---------------- key capture for the key binding dialog
if let Some(action) = menu.capturing() {
if is_key_pressed(KeyCode::Escape) {
menu.finish_capture();
} else if let Some(k) = get_last_key_pressed() {
if bindable_keys().any(|b| b == k) {
app.settings.bind(action, k);
apply_settings(&app.settings, &mut r3d, &mut sounds);
store_settings(&app.settings);
menu.finish_capture();
}
}
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &sounds, &inp, now);
next_frame().await;
continue;
}
let dialog = ui.show_langsel || ui.show_help;
let in_menu = menu.open();
let playing = !in_menu && !dialog;
let over_ui = ui.over_ui(mx, my); let over_ui = ui.over_ui(mx, my);
let picked = cam.pick_tile(&game.map, mx, my); let picked = app.cam.pick_tile(&app.game.map, mx, my);
let hover_tile = picked.map(|(x, z, _)| (x, z)); let hover_tile = picked.map(|(x, z, _)| (x, z));
let ground = cam.ground_point(mx, my); let ground = app.cam.ground_point(mx, my);
let (wx, wy) = (ground.x, ground.y); let (wx, wy) = (ground.x, ground.y);
let keys = app.settings.clone();
let key = |a: Action| keys.key(a);
// ---------------- keyboard // ---------------- Esc: dialogs, menus, pause menu
let tool_keys = [
KeyCode::Key1,
KeyCode::Key2,
KeyCode::Key3,
KeyCode::Key4,
KeyCode::Key5,
KeyCode::Key6,
KeyCode::Key7,
KeyCode::Key8,
KeyCode::Key9,
KeyCode::Key0,
KeyCode::X,
KeyCode::V,
];
for (i, k) in tool_keys.iter().enumerate() {
if is_key_pressed(*k) {
ui.tool = TOOLS[i];
inp.ldown = None;
}
}
if is_key_pressed(KeyCode::H) || (is_key_pressed(KeyCode::Escape) && ui.show_help) {
ui.show_help = !ui.show_help;
}
if is_key_pressed(KeyCode::Escape) { if is_key_pressed(KeyCode::Escape) {
inp.ldown = None; if ui.show_langsel {
} } else if ui.show_help {
if (is_key_pressed(KeyCode::P) || is_key_pressed(KeyCode::Space)) && game.state == 0 { ui.show_help = false;
game.paused = !game.paused; } else if in_menu {
} menu.back();
if is_key_pressed(KeyCode::G) { } else if inp.ldown.is_some() {
r3d.outlines = !r3d.outlines; inp.ldown = None;
} } else {
if is_key_pressed(KeyCode::M) { menu.page = Some(Page::Pause);
sounds.muted = !sounds.muted; }
}
if is_key_pressed(KeyCode::Equal) || is_key_pressed(KeyCode::KpAdd) {
sounds.change_volume(0.1);
}
if is_key_pressed(KeyCode::Minus) || is_key_pressed(KeyCode::KpSubtract) {
sounds.change_volume(-0.1);
}
if is_key_pressed(KeyCode::N) && game.state != 0 {
seed = next_seed(seed);
(game, cam) = new_game(seed);
ui.show_help = false;
} }
// ---------------- camera: pan // ---------------- keyboard (only while playing)
let (right, fwd) = cam.ground_axes(); if playing {
let pan = cam.zoom * 0.9 * dt; for i in 0..TOOLS.len() {
let move_cam = |dx: f32, dz: f32, cam: &mut Cam| { if is_key_pressed(key(Action::Tool(i))) {
let d = right * dx + fwd * dz; ui.tool = TOOLS[i];
cam.tx += d.x;
cam.tz += d.y;
};
if is_key_down(KeyCode::W) || is_key_down(KeyCode::Up) {
move_cam(0.0, pan, &mut cam);
}
if is_key_down(KeyCode::S) || is_key_down(KeyCode::Down) {
move_cam(0.0, -pan, &mut cam);
}
if is_key_down(KeyCode::A) || is_key_down(KeyCode::Left) {
move_cam(-pan, 0.0, &mut cam);
}
if is_key_down(KeyCode::D) || is_key_down(KeyCode::Right) {
move_cam(pan, 0.0, &mut cam);
}
let edge = 6.0;
if !ui.show_help && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
if mx < edge {
move_cam(-pan, 0.0, &mut cam);
} else if mx > screen_width() - edge {
move_cam(pan, 0.0, &mut cam);
}
if my < edge {
move_cam(0.0, pan, &mut cam);
} else if my > screen_height() - edge {
move_cam(0.0, -pan, &mut cam);
}
}
// ---------------- camera: rotate / tilt
let rot_speed = 1.8 * dt;
if is_key_down(KeyCode::Q) {
cam.yaw += rot_speed;
}
if is_key_down(KeyCode::E) {
cam.yaw -= rot_speed;
}
if is_key_down(KeyCode::R) {
cam.pitch += rot_speed * 0.6;
}
if is_key_down(KeyCode::F) {
cam.pitch -= rot_speed * 0.6;
}
if is_mouse_button_down(MouseButton::Middle) {
if let Some((lx, ly)) = inp.mlast {
cam.yaw -= (mx - lx) * 0.008;
cam.pitch += (my - ly) * 0.005;
}
inp.mlast = Some((mx, my));
} else {
inp.mlast = None;
}
cam.pitch = cam.pitch.clamp(0.35, 1.45);
// ---------------- camera: zoom around the mouse
let (_, wheel) = mouse_wheel();
if wheel != 0.0 && !over_ui {
let before = cam.ground_point(mx, my);
cam.zoom = (cam.zoom * if wheel > 0.0 { 0.88 } else { 1.0 / 0.88 }).clamp(6.0, 45.0);
let after = cam.ground_point(mx, my);
cam.tx += before.x - after.x;
cam.tz += before.y - after.y;
}
// ---------------- right button: drag = pan, click = drop
if is_mouse_button_pressed(MouseButton::Right) {
inp.rdown = Some((mx, my));
inp.rlast = (mx, my);
inp.rdrag = false;
}
if let Some((sx, sy)) = inp.rdown {
if is_mouse_button_down(MouseButton::Right) {
if (mx - sx).abs() + (my - sy).abs() > 5.0 {
inp.rdrag = true;
}
if inp.rdrag {
let a = cam.ground_point(inp.rlast.0, inp.rlast.1);
let b = cam.ground_point(mx, my);
cam.tx -= b.x - a.x;
cam.tz -= b.y - a.y;
}
inp.rlast = (mx, my);
}
}
if is_mouse_button_released(MouseButton::Right) {
if !inp.rdrag && !over_ui && game.state == 0 {
if inp.ldown.is_some() {
inp.ldown = None; inp.ldown = None;
} else {
game.drop(wx, wy);
} }
} }
inp.rdown = None; if is_key_pressed(key(Action::Help)) {
ui.show_help = true;
}
if is_key_pressed(key(Action::Pause)) && app.game.state == 0 {
app.game.paused = !app.game.paused;
}
if is_key_pressed(key(Action::Outlines)) {
app.settings.outlines = !app.settings.outlines;
settings_changed = true;
}
if is_key_pressed(KeyCode::N) && app.game.state != 0 {
app.seed = next_seed(app.seed);
(app.game, app.cam) = new_game(app.seed);
app.game.hints = app.settings.hints;
}
}
if !dialog {
if is_key_pressed(key(Action::Mute)) {
app.settings.muted = !app.settings.muted;
settings_changed = true;
}
if is_key_pressed(key(Action::VolumeUp)) || is_key_pressed(KeyCode::KpAdd) {
app.settings.volume = (app.settings.volume + 0.1).min(1.0);
settings_changed = true;
}
if is_key_pressed(key(Action::VolumeDown)) || is_key_pressed(KeyCode::KpSubtract) {
app.settings.volume = (app.settings.volume - 0.1).max(0.0);
settings_changed = true;
}
}
// ---------------- camera
if playing {
let cam = &mut app.cam;
let (right, fwd) = cam.ground_axes();
let pan = cam.zoom * 0.9 * dt;
let move_cam = |dx: f32, dz: f32, cam: &mut Cam| {
let d = right * dx + fwd * dz;
cam.tx += d.x;
cam.tz += d.y;
};
if is_key_down(key(Action::CamForward)) || is_key_down(KeyCode::Up) {
move_cam(0.0, pan, cam);
}
if is_key_down(key(Action::CamBack)) || is_key_down(KeyCode::Down) {
move_cam(0.0, -pan, cam);
}
if is_key_down(key(Action::CamLeft)) || is_key_down(KeyCode::Left) {
move_cam(-pan, 0.0, cam);
}
if is_key_down(key(Action::CamRight)) || is_key_down(KeyCode::Right) {
move_cam(pan, 0.0, cam);
}
let edge = 6.0;
if keys.edge_scroll && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
if mx < edge {
move_cam(-pan, 0.0, cam);
} else if mx > screen_width() - edge {
move_cam(pan, 0.0, cam);
}
if my < edge {
move_cam(0.0, pan, cam);
} else if my > screen_height() - edge {
move_cam(0.0, -pan, cam);
}
}
let rot = 1.8 * dt * keys.rotate_speed;
if is_key_down(key(Action::RotateLeft)) {
cam.yaw += rot;
}
if is_key_down(key(Action::RotateRight)) {
cam.yaw -= rot;
}
if is_key_down(key(Action::TiltUp)) {
cam.pitch += rot * 0.6;
}
if is_key_down(key(Action::TiltDown)) {
cam.pitch -= rot * 0.6;
}
if is_mouse_button_down(MouseButton::Middle) {
if let Some((lx, ly)) = inp.mlast {
let inv = if keys.invert_mouse { -1.0 } else { 1.0 };
cam.yaw -= (mx - lx) * 0.008 * keys.rotate_speed * inv;
cam.pitch += (my - ly) * 0.005 * keys.rotate_speed * inv;
}
inp.mlast = Some((mx, my));
} else {
inp.mlast = None;
}
cam.pitch = cam.pitch.clamp(0.35, 1.45);
let (_, wheel) = mouse_wheel();
if wheel != 0.0 && !over_ui {
let before = cam.ground_point(mx, my);
let f = 0.88f32.powf(keys.zoom_speed);
cam.zoom = (cam.zoom * if wheel > 0.0 { f } else { 1.0 / f }).clamp(6.0, 45.0);
let after = cam.ground_point(mx, my);
cam.tx += before.x - after.x;
cam.tz += before.y - after.y;
}
// right button: drag = pan, click = drop
if is_mouse_button_pressed(MouseButton::Right) {
inp.rdown = Some((mx, my));
inp.rlast = (mx, my);
inp.rdrag = false;
}
if let Some((sx, sy)) = inp.rdown {
if is_mouse_button_down(MouseButton::Right) {
if (mx - sx).abs() + (my - sy).abs() > 5.0 {
inp.rdrag = true;
}
if inp.rdrag {
let a = cam.ground_point(inp.rlast.0, inp.rlast.1);
let b = cam.ground_point(mx, my);
cam.tx -= b.x - a.x;
cam.tz -= b.y - a.y;
}
inp.rlast = (mx, my);
}
}
if is_mouse_button_released(MouseButton::Right) {
if !inp.rdrag && !over_ui && app.game.state == 0 {
if inp.ldown.is_some() {
inp.ldown = None;
} else {
app.game.drop(wx, wy);
}
}
inp.rdown = None;
}
} }
// ---------------- left button // ---------------- left button
if is_mouse_button_pressed(MouseButton::Left) { if is_mouse_button_pressed(MouseButton::Left) {
match ui.click(mx, my, &game) { if dialog {
UiAction::SelectTool(t) => { match ui.click(mx, my, &app.game) {
ui.tool = t; UiAction::SelectLanguage(code) => {
game.sfx_ui(game::Sfx::Click); app.settings.language = code;
settings_changed = true;
ui.show_langsel = false;
}
UiAction::CloseHelp => ui.show_help = false,
_ => {}
} }
UiAction::MinimapJump(x, y) => { } else if in_menu {
cam.tx = x; match menu.click(mx, my, &mut app.settings) {
cam.tz = y; MenuAction::SettingsChanged => settings_changed = true,
inp.minimap_drag = true; MenuAction::Resume => {}
MenuAction::NewGame => {
app.seed = next_seed(app.seed);
(app.game, app.cam) = new_game(app.seed);
app.game.hints = app.settings.hints;
menu.page = None;
ui.log.clear();
ui.show_help = true;
autosave_t = 0.0;
}
MenuAction::Continue => {
if let Some(s) = platform::storage_get(save::AUTOSAVE_KEY) {
app.load_bytes(s.as_bytes(), &mut ui, &mut menu, &mut r3d, &mut sounds, now);
}
}
MenuAction::Load => {
platform::open_file();
app.set_status(i18n::tr("save.choose"), now);
}
MenuAction::Save => {
let name = save_file_name();
platform::download(&name, app.autosave_json().as_bytes());
app.set_status(i18n::trf("save.saved", &[name]), now);
}
MenuAction::Help => ui.show_help = true,
MenuAction::MainMenu => {
if app.game.state == 0 {
platform::storage_set(save::AUTOSAVE_KEY, &app.autosave_json());
menu.has_autosave = true;
}
menu.page = Some(Page::Title);
}
MenuAction::None | MenuAction::Consumed => {}
} }
UiAction::CloseHelp => ui.show_help = false, } else {
UiAction::NewGame => { match ui.click(mx, my, &app.game) {
seed = next_seed(seed); UiAction::SelectTool(t) => {
(game, cam) = new_game(seed); ui.tool = t;
ui.show_help = false; app.game.sfx_ui(game::Sfx::Click);
} }
UiAction::SelectLanguage(code) => { UiAction::MinimapJump(x, y) => {
i18n::set_language(&code); app.cam.tx = x;
platform::storage_set("dc.lang", &code); app.cam.tz = y;
ui.show_langsel = false; inp.minimap_drag = true;
game.sfx_ui(game::Sfx::Click); }
} UiAction::NewGame => {
UiAction::Consumed => {} app.seed = next_seed(app.seed);
UiAction::None => { (app.game, app.cam) = new_game(app.seed);
if game.state == 0 { app.game.hints = app.settings.hints;
match (ui.tool, hover_tile) { }
(Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t), UiAction::CloseHelp | UiAction::SelectLanguage(_) | UiAction::Consumed => {}
(Tool::Hand, _) => { UiAction::None => {
let j = r3d.pick_ent(&game, &cam, mx, my, true); if app.game.state == 0 {
game.pickup(j); match (ui.tool, hover_tile) {
(Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t),
(Tool::Hand, _) => {
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, true);
app.game.pickup(j);
}
(Tool::Flag, Some((x, z))) => app.game.set_flag(x, z),
(Tool::Lightning, _) => app.game.lightning(wx, wy),
(Tool::Heal, _) => app.game.heal(wx, wy),
(Tool::Summon, _) => app.game.summon(wx, wy),
_ => {}
} }
(Tool::Flag, Some((x, z))) => game.set_flag(x, z),
(Tool::Lightning, _) => game.lightning(wx, wy),
(Tool::Heal, _) => game.heal(wx, wy),
(Tool::Summon, _) => game.summon(wx, wy),
_ => {}
} }
} }
} }
@@ -269,68 +452,115 @@ async fn main() {
if inp.minimap_drag { if inp.minimap_drag {
if is_mouse_button_down(MouseButton::Left) && ui.minimap_contains(mx, my) { if is_mouse_button_down(MouseButton::Left) && ui.minimap_contains(mx, my) {
let (x, y) = ui.minimap_world(mx, my); let (x, y) = ui.minimap_world(mx, my);
cam.tx = x; app.cam.tx = x;
cam.tz = y; app.cam.tz = y;
} else if !is_mouse_button_down(MouseButton::Left) { } else if !is_mouse_button_down(MouseButton::Left) {
inp.minimap_drag = false; inp.minimap_drag = false;
} }
} }
if is_mouse_button_released(MouseButton::Left) { if is_mouse_button_released(MouseButton::Left) {
if let Some((sx, sy)) = inp.ldown.take() { if let Some((sx, sy)) = inp.ldown.take() {
if let Some((ex, ey)) = hover_tile { if let (true, Some((ex, ey))) = (playing, hover_tile) {
match ui.tool { match ui.tool {
Tool::Dig => { Tool::Dig => {
let on = !game.map.t[idx(sx, sy)].marked; let on = !app.game.map.t[idx(sx, sy)].marked;
game.mark_rect(sx, sy, ex, ey, on); app.game.mark_rect(sx, sy, ex, ey, on);
} }
Tool::Room(r) => game.build_rect(r, sx, sy, ex, ey), Tool::Room(r) => app.game.build_rect(r, sx, sy, ex, ey),
Tool::Sell => game.sell_rect(sx, sy, ex, ey), Tool::Sell => app.game.sell_rect(sx, sy, ex, ey),
_ => {} _ => {}
} }
} }
} }
} }
cam.tx = cam.tx.clamp(0.0, MW as f32); app.cam.tx = app.cam.tx.clamp(0.0, MW as f32);
cam.tz = cam.tz.clamp(0.0, MH as f32); app.cam.tz = app.cam.tz.clamp(0.0, MH as f32);
// ---------------- update if settings_changed {
game.update(dt); apply_settings(&app.settings, &mut r3d, &mut sounds);
let hear = cam.zoom * 1.2; store_settings(&app.settings);
for ev in game.events.drain(..) { app.game.hints = app.settings.hints;
sounds.play(ev, vec2(cam.tx, cam.tz), hear, now);
} }
for m in game.msgs.drain(..) {
// ---------------- update (the world stands still while menus or dialogs are open)
if playing {
for _ in 0..(app.settings.game_speed.round().max(1.0) as usize) {
app.game.update(dt);
}
// autosave: snapshot for leaving the tab + regular autosave
if app.game.state == 0 {
cache_t += dt;
autosave_t += dt;
if cache_t >= AUTOSAVE_CACHE_EVERY {
cache_t = 0.0;
platform::cache_on_leave(save::AUTOSAVE_KEY, &app.autosave_json());
}
if autosave_t >= AUTOSAVE_EVERY {
autosave_t = 0.0;
platform::storage_set(save::AUTOSAVE_KEY, &app.autosave_json());
menu.has_autosave = true;
}
}
}
let hear = app.cam.zoom * 1.2;
for ev in app.game.events.drain(..) {
sounds.play(ev, vec2(app.cam.tx, app.cam.tz), hear, now);
}
for m in app.game.msgs.drain(..) {
ui.log.push((m, now)); ui.log.push((m, now));
} }
// ---------------- draw draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &sounds, &inp, now);
clear_background(Color::from_rgba(4, 3, 5, 255));
let (color, radius) = match ui.tool {
Tool::Dig => (Color::new(1.0, 0.85, 0.3, 1.0), None),
Tool::Hand => (Color::new(0.9, 0.75, 0.6, 1.0), None),
Tool::Room(_) => (Color::new(0.4, 0.85, 1.0, 1.0), None),
Tool::Sell => (Color::new(1.0, 0.5, 0.3, 1.0), None),
Tool::Flag => (Color::new(1.0, 0.3, 0.3, 1.0), None),
Tool::Lightning => (Color::new(1.0, 1.0, 0.6, 1.0), Some((wx, wy, 1.6))),
Tool::Heal => (Color::new(0.4, 1.0, 0.5, 1.0), Some((wx, wy, 2.55))),
Tool::Summon => (Color::new(0.8, 0.5, 1.0, 1.0), None),
};
let ov = Overlay {
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)))),
color,
radius: if over_ui { None } else { radius },
high: ui.tool == Tool::Dig,
};
r3d.draw(&game, &gfx, &cam, now, &ov);
let info = if over_ui {
Vec::new()
} else {
let ent = r3d.pick_ent(&game, &cam, mx, my, false);
hover_info(&game, ent, hover_tile)
};
ui.draw(&game, &gfx, &cam, &sounds, &info, now);
next_frame().await next_frame().await
} }
} }
#[allow(clippy::too_many_arguments)]
fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut Menu, sounds: &SoundBank, inp: &Input, now: f64) {
let (mx, my) = mouse_position();
let blocked = menu.open() || ui.show_help || ui.show_langsel;
let over_ui = blocked || ui.over_ui(mx, my);
let hover_tile = app.cam.pick_tile(&app.game.map, mx, my).map(|(x, z, _)| (x, z));
let ground = app.cam.ground_point(mx, my);
clear_background(Color::from_rgba(4, 3, 5, 255));
let (color, radius) = match ui.tool {
Tool::Dig => (Color::new(1.0, 0.85, 0.3, 1.0), None),
Tool::Hand => (Color::new(0.9, 0.75, 0.6, 1.0), None),
Tool::Room(_) => (Color::new(0.4, 0.85, 1.0, 1.0), None),
Tool::Sell => (Color::new(1.0, 0.5, 0.3, 1.0), None),
Tool::Flag => (Color::new(1.0, 0.3, 0.3, 1.0), None),
Tool::Lightning => (Color::new(1.0, 1.0, 0.6, 1.0), Some((ground.x, ground.y, 1.6))),
Tool::Heal => (Color::new(0.4, 1.0, 0.5, 1.0), Some((ground.x, ground.y, 2.55))),
Tool::Summon => (Color::new(0.8, 0.5, 1.0, 1.0), None),
};
let ov = Overlay {
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)))),
color,
radius: if over_ui { None } else { radius },
high: ui.tool == Tool::Dig,
};
r3d.draw(&app.game, gfx, &app.cam, now, &ov);
let title_screen = matches!(
menu.page,
Some(Page::Title) | Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. })
);
if !title_screen {
let info = if over_ui {
Vec::new()
} else {
let ent = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
hover_info(&app.game, ent, hover_tile)
};
ui.draw(&app.game, gfx, &app.cam, sounds, &app.settings, &info, now);
} else {
ui.rects.clear();
}
if app.status.as_ref().map_or(false, |(_, t)| now - t > 4.0) {
app.status = None;
}
menu.draw(gfx, &app.settings, app.status.as_ref().map(|(s, _)| s.as_str()));
if !menu.open() || ui.show_help || ui.show_langsel {
ui.draw_dialogs(&app.game, gfx);
}
}
+21 -8
View File
@@ -1,5 +1,6 @@
// Map, tiles, generation and path finding. // Map, tiles, generation and path finding.
use serde::{Deserialize, Serialize};
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::BinaryHeap; use std::collections::BinaryHeap;
@@ -7,7 +8,7 @@ pub const MW: usize = 64;
pub const MH: usize = 64; pub const MH: usize = 64;
pub const N: usize = MW * MH; pub const N: usize = MW * MH;
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Kind { pub enum Kind {
Rock, Rock,
Earth, Earth,
@@ -23,24 +24,34 @@ pub enum Kind {
Gate, Gate,
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Owner { pub enum Owner {
None, None,
Player, Player,
Hero, Hero,
} }
#[derive(Clone, Copy)] // Short serde names keep savegames small (the map has 4096 tiles)
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct Tile { pub struct Tile {
#[serde(rename = "k")]
pub kind: Kind, pub kind: Kind,
#[serde(rename = "o")]
pub owner: Owner, pub owner: Owner,
pub hp: f32, // dig resistance #[serde(rename = "h")]
pub gold: i32, // gold left (gold vein) pub hp: f32, // dig resistance
#[serde(rename = "g")]
pub gold: i32, // gold left (gold vein)
#[serde(rename = "m")]
pub marked: bool, // marked for digging pub marked: bool, // marked for digging
#[serde(rename = "r")]
pub reserved: u32, // entity id + 1 working on this tile pub reserved: u32, // entity id + 1 working on this tile
pub food: f32, // mushrooms in the mushroom garden #[serde(rename = "f")]
pub bed: u32, // entity id + 1 sleeping here pub food: f32, // mushrooms in the mushroom garden
pub var: u8, // texture variant #[serde(rename = "b")]
pub bed: u32, // entity id + 1 sleeping here
#[serde(rename = "v")]
pub var: u8, // texture variant
} }
impl Tile { impl Tile {
@@ -74,6 +85,7 @@ pub fn inb(x: i32, y: i32) -> bool {
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)];
pub const DIR8: [(i32, i32); 8] = [(0, -1), (1, 0), (0, 1), (-1, 0), (1, -1), (1, 1), (-1, 1), (-1, -1)]; pub const DIR8: [(i32, i32); 8] = [(0, -1), (1, 0), (0, 1), (-1, 0), (1, -1), (1, 1), (-1, 1), (-1, -1)];
#[derive(Serialize, Deserialize)]
pub struct Rng(pub u32); pub struct Rng(pub u32);
impl Rng { impl Rng {
pub fn next(&mut self) -> u32 { pub fn next(&mut self) -> u32 {
@@ -95,6 +107,7 @@ impl Rng {
} }
} }
#[derive(Serialize, Deserialize)]
pub struct Map { pub struct Map {
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)
+434
View File
@@ -0,0 +1,434 @@
// Menus: title screen, pause menu and the settings dialog (tabs, values, key bindings).
// Drawn as CP437 windows on top of the (paused) game world.
use crate::gfx::*;
use crate::i18n::{languages, tr, trf};
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::*;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Page {
Title,
Pause,
Settings { tab: usize, from_title: bool },
KeyCapture { action: usize, from_title: bool },
}
pub const TABS: [&str; 5] = ["set.tab.sound", "set.tab.camera", "set.tab.graphics", "set.tab.game", "set.tab.keys"];
// One adjustable value in the settings dialog
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Item {
Language,
Volume,
MusicVolume,
Mute,
RotateSpeed,
ZoomSpeed,
EdgeScroll,
InvertMouse,
Outlines,
WallDetail,
Lights,
UiScale,
GameSpeed,
Hints,
}
fn tab_items(tab: usize) -> &'static [Item] {
use Item::*;
match tab {
0 => &[Language, Volume, MusicVolume, Mute],
1 => &[RotateSpeed, ZoomSpeed, EdgeScroll, InvertMouse],
2 => &[Outlines, WallDetail, Lights, UiScale],
3 => &[GameSpeed, Hints],
_ => &[],
}
}
fn item_label(it: Item) -> String {
tr(match it {
Item::Language => "set.language",
Item::Volume => "set.volume",
Item::MusicVolume => "set.music",
Item::Mute => "set.mute",
Item::RotateSpeed => "set.rotate_speed",
Item::ZoomSpeed => "set.zoom_speed",
Item::EdgeScroll => "set.edge_scroll",
Item::InvertMouse => "set.invert",
Item::Outlines => "set.outlines",
Item::WallDetail => "set.wall_detail",
Item::Lights => "set.lights",
Item::UiScale => "set.ui_scale",
Item::GameSpeed => "set.game_speed",
Item::Hints => "set.hints",
})
}
fn onoff(b: bool) -> String {
tr(if b { "val.on" } else { "val.off" })
}
fn pct(v: f32) -> String {
format!("{}%", (v * 100.0).round() as i32)
}
fn item_value(it: Item, s: &Settings) -> String {
match it {
Item::Language => languages().into_iter().find(|(c, _)| *c == s.language).map(|(_, n)| n).unwrap_or_else(|| s.language.clone()),
Item::Volume => pct(s.volume),
Item::MusicVolume => pct(s.music_volume),
Item::Mute => onoff(s.muted),
Item::RotateSpeed => pct(s.rotate_speed),
Item::ZoomSpeed => pct(s.zoom_speed),
Item::EdgeScroll => onoff(s.edge_scroll),
Item::InvertMouse => onoff(s.invert_mouse),
Item::Outlines => onoff(s.outlines),
Item::WallDetail => tr(match s.wall_detail {
1 => "val.low",
3 => "val.high",
_ => "val.medium",
}),
Item::Lights => s.max_lights.to_string(),
Item::UiScale => tr(if s.ui_scale >= 3 { "val.large" } else { "val.normal" }),
Item::GameSpeed => format!("{}x", s.game_speed as i32),
Item::Hints => onoff(s.hints),
}
}
fn cycle<T: PartialEq + Copy>(opts: &[T], cur: T, d: i32) -> T {
let i = opts.iter().position(|&o| o == cur).unwrap_or(0) as i32;
opts[(i + d).rem_euclid(opts.len() as i32) as usize]
}
fn step(v: f32, d: i32, lo: f32, hi: f32, st: f32) -> f32 {
((v + d as f32 * st) * 100.0).round().clamp(lo * 100.0, hi * 100.0) / 100.0
}
// Change a value; d = -1 / +1 (toggles flip either way)
fn item_change(it: Item, s: &mut Settings, d: i32) {
match it {
Item::Language => {
let codes: Vec<String> = languages().into_iter().map(|(c, _)| c).collect();
let i = codes.iter().position(|c| *c == s.language).unwrap_or(0) as i32;
s.language = codes[(i + d).rem_euclid(codes.len() as i32) as usize].clone();
}
Item::Volume => s.volume = step(s.volume, d, 0.0, 1.0, 0.1),
Item::MusicVolume => s.music_volume = step(s.music_volume, d, 0.0, 1.0, 0.1),
Item::Mute => s.muted = !s.muted,
Item::RotateSpeed => s.rotate_speed = step(s.rotate_speed, d, 0.25, 2.5, 0.25),
Item::ZoomSpeed => s.zoom_speed = step(s.zoom_speed, d, 0.25, 2.5, 0.25),
Item::EdgeScroll => s.edge_scroll = !s.edge_scroll,
Item::InvertMouse => s.invert_mouse = !s.invert_mouse,
Item::Outlines => s.outlines = !s.outlines,
Item::WallDetail => s.wall_detail = cycle(&[1u8, 2, 3], s.wall_detail, d),
Item::Lights => s.max_lights = cycle(&[8u8, 16, 24], s.max_lights, d),
Item::UiScale => s.ui_scale = cycle(&[2u8, 3], s.ui_scale, d),
Item::GameSpeed => s.game_speed = cycle(&[1.0f32, 2.0], s.game_speed, d),
Item::Hints => s.hints = !s.hints,
}
}
pub fn action_label(a: Action) -> String {
match a {
Action::Tool(i) => tool_info(TOOLS[i]).name,
other => tr(&format!("key.{}", other.id())),
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum Btn {
Continue,
Resume,
New,
Load,
Save,
Settings,
Help,
MainMenu,
Back,
Tab(usize),
Change(usize, i32),
Key(usize),
ResetKeys,
}
pub enum MenuAction {
None,
Consumed,
Continue, // load the autosave
Resume,
NewGame,
Load,
Save,
Help,
MainMenu,
SettingsChanged,
}
pub struct Menu {
pub page: Option<Page>,
pub has_autosave: bool,
buttons: Vec<(Rect, Btn)>,
}
fn button(gfx: &Gfx, r: Rect, label: &str, hot: bool, accent: Color) {
draw_rectangle(r.x, r.y, r.w, r.h, if hot { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.13, 0.09, 0.08, 1.0) });
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hot { FRAME_HI } else { accent });
let s = S();
gfx.text(label, r.x + (r.w - gfx.measure(label, s)) * 0.5, r.y + (r.h - GH * s) * 0.5, s, if hot { FRAME_HI } else { TXT });
}
impl Menu {
pub fn new() -> Menu {
Menu { page: Some(Page::Title), has_autosave: false, buttons: Vec::new() }
}
pub fn open(&self) -> bool {
self.page.is_some()
}
// Esc: go one level back
pub fn back(&mut self) {
self.page = match self.page {
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::Pause) => None,
Some(Page::Title) => Some(Page::Title),
None => Some(Page::Pause),
};
}
pub fn capturing(&self) -> Option<Action> {
match self.page {
Some(Page::KeyCapture { action, .. }) => all_actions().get(action).copied(),
_ => None,
}
}
pub fn finish_capture(&mut self) {
if let Some(Page::KeyCapture { from_title, .. }) = self.page {
self.page = Some(Page::Settings { tab: 4, from_title });
}
}
pub fn click(&mut self, x: f32, y: f32, settings: &mut Settings) -> MenuAction {
if self.page.is_none() {
return MenuAction::None;
}
let hit = self.buttons.iter().find(|(r, _)| r.contains(vec2(x, y))).map(|(_, b)| *b);
let from_title = matches!(self.page, Some(Page::Title) | Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. }));
match hit {
None => MenuAction::Consumed,
Some(Btn::Continue) => MenuAction::Continue,
Some(Btn::Resume) => {
self.page = None;
MenuAction::Resume
}
Some(Btn::New) => MenuAction::NewGame,
Some(Btn::Load) => MenuAction::Load,
Some(Btn::Save) => MenuAction::Save,
Some(Btn::Help) => MenuAction::Help,
Some(Btn::MainMenu) => MenuAction::MainMenu,
Some(Btn::Settings) => {
self.page = Some(Page::Settings { tab: 0, from_title });
MenuAction::Consumed
}
Some(Btn::Back) => {
self.back();
MenuAction::Consumed
}
Some(Btn::Tab(t)) => {
self.page = Some(Page::Settings { tab: t, from_title });
MenuAction::Consumed
}
Some(Btn::Change(i, d)) => {
if let Some(Page::Settings { tab, .. }) = self.page {
if let Some(&it) = tab_items(tab).get(i) {
item_change(it, settings, d);
return MenuAction::SettingsChanged;
}
}
MenuAction::Consumed
}
Some(Btn::Key(a)) => {
self.page = Some(Page::KeyCapture { action: a, from_title });
MenuAction::Consumed
}
Some(Btn::ResetKeys) => {
settings.reset_keys();
MenuAction::SettingsChanged
}
}
}
pub fn draw(&mut self, gfx: &Gfx, settings: &Settings, status: Option<&str>) {
self.buttons.clear();
let Some(page) = self.page else { return };
let sw = screen_width();
let sh = screen_height();
draw_rectangle(0.0, 0.0, sw, sh, Color::new(0.0, 0.0, 0.0, if page == Page::Title { 0.55 } else { 0.45 }));
match page {
Page::Title => self.draw_title(gfx),
Page::Pause => self.draw_pause(gfx),
Page::Settings { tab, .. } => self.draw_settings(gfx, settings, tab),
Page::KeyCapture { action, .. } => {
self.draw_settings(gfx, settings, 4);
self.buttons.clear();
let a = all_actions()[action];
let text = trf("set.press_key", &[action_label(a)]);
let cols = (text.chars().count() as i32 + 8).max(36);
let x = (sw - cols as f32 * CW()) * 0.5;
let y = sh * 0.4;
gfx.window(x, y, cols, 5, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.98));
gfx.text(&text, x + CW() * 4.0, y + CH() * 1.5, S(), TXT);
let esc = tr("set.esc_cancel");
gfx.text(&esc, x + CW() * 4.0, y + CH() * 3.0, S(), DIM);
}
}
if let Some(st) = status {
let w = gfx.measure(st, S()) + CW() * 4.0;
let x = (sw - w) * 0.5;
let y = sh - CH() * 9.0;
draw_rectangle(x, y, w, CH() * 2.0, Color::new(0.05, 0.03, 0.04, 0.95));
draw_rectangle_lines(x, y, w, CH() * 2.0, 2.0, GOLD);
gfx.text(st, x + CW() * 2.0, y + CH() * 0.5, S(), GOLD);
}
}
fn column(&mut self, gfx: &Gfx, x: f32, y: f32, w: f32, items: &[(Btn, String)]) {
let (mx, my) = mouse_position();
for (k, (b, label)) in items.iter().enumerate() {
let r = Rect::new(x, y + k as f32 * CH() * 3.0, w, CH() * 2.2);
button(gfx, r, label, r.contains(vec2(mx, my)), FRAME);
self.buttons.push((r, *b));
}
}
fn draw_title(&mut self, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();
let big = (S() * 4.0).min((sw / (GW * 14.0)).floor()).max(S());
let title = "DungeonCraft";
let ty = sh * 0.12;
gfx.text(title, (sw - gfx.measure(title, big)) * 0.5 + big, ty + big, big, Color::new(0.0, 0.0, 0.0, 0.8));
gfx.text(title, (sw - gfx.measure(title, big)) * 0.5, ty, big, FRAME_HI);
let sub = tr("menu.subtitle");
gfx.text(&sub, (sw - gfx.measure(&sub, S())) * 0.5, ty + GH * big + CH(), S(), TXT);
let mut items = Vec::new();
if self.has_autosave {
items.push((Btn::Continue, tr("menu.continue")));
}
items.push((Btn::New, tr("menu.new")));
items.push((Btn::Load, tr("menu.load")));
items.push((Btn::Settings, tr("menu.settings")));
items.push((Btn::Help, tr("menu.help")));
let w = 26.0 * CW();
let y = ty + GH * big + CH() * 4.0;
self.column(gfx, (sw - w) * 0.5, y, w, &items);
let hint = tr("menu.drop_hint");
gfx.text(&hint, (sw - gfx.measure(&hint, (S() / 2.0).max(1.0))) * 0.5, sh - CH() * 7.5, (S() / 2.0).max(1.0), DIM);
let credit = tr("help.font");
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);
}
fn draw_pause(&mut self, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();
let items = vec![
(Btn::Resume, tr("menu.resume")),
(Btn::Save, tr("menu.save")),
(Btn::Load, tr("menu.load")),
(Btn::Settings, tr("menu.settings")),
(Btn::Help, tr("menu.help")),
(Btn::MainMenu, tr("menu.main")),
];
let cols = 32;
let rows = items.len() as i32 * 3 + 4;
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.paused"), FRAME_HI);
let w = (cols - 6) as f32 * CW();
self.column(gfx, x + CW() * 3.0, y + CH() * 2.0, w, &items);
}
fn draw_settings(&mut self, gfx: &Gfx, s: &Settings, tab: usize) {
let sw = screen_width();
let sh = screen_height();
let (mx, my) = mouse_position();
let cols = ((sw / CW()) as i32 - 2).min(66);
let rows = ((sh / CH()) as i32 - 2).min(28);
let x = ((sw - cols as f32 * CW()) * 0.5).max(0.0);
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.settings"), FRAME_HI);
// tabs
let mut tx = x + CW() * 2.0;
let ty = y + CH() * 1.5;
for (i, key) in TABS.iter().enumerate() {
let label = tr(key);
let w = gfx.measure(&label, S()) + CW() * 2.0;
let r = Rect::new(tx, ty, w, CH() * 2.0);
let hot = r.contains(vec2(mx, my));
draw_rectangle(r.x, r.y, r.w, r.h, if i == tab { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if i == tab || hot { FRAME_HI } else { FRAME });
gfx.text(&label, r.x + CW(), r.y + CH() * 0.5, S(), if i == tab { FRAME_HI } else { TXT });
self.buttons.push((r, Btn::Tab(i)));
tx += w + CW();
}
let top = ty + CH() * 3.5;
if tab == 4 {
// key bindings in two columns
let acts = all_actions();
let half = (acts.len() + 1) / 2;
let colw = (cols as f32 - 6.0) * CW() * 0.5;
let rowh = CH() * 1.35;
for (k, a) in acts.iter().enumerate() {
let c = k / half;
let r = k % half;
let bx = x + CW() * 2.0 + c as f32 * (colw + CW() * 2.0);
let by = top + r as f32 * rowh;
let rect = Rect::new(bx, by - 2.0, colw, rowh - 2.0);
let hot = rect.contains(vec2(mx, my));
if hot {
draw_rectangle(rect.x, rect.y, rect.w, rect.h, Color::new(0.25, 0.16, 0.08, 1.0));
}
let key = format!("[{}]", key_name(s.key(*a)));
let maxc = ((colw - gfx.measure(&key, S()) - CW()) / CW()).max(4.0) as usize;
let label: String = action_label(*a).chars().take(maxc).collect();
gfx.text(&label, bx + CW() * 0.5, by, S(), TXT);
gfx.text(&key, bx + colw - gfx.measure(&key, S()) - CW() * 0.5, by, S(), GOLD);
self.buttons.push((rect, Btn::Key(k)));
}
let hint = tr("set.keys_hint");
gfx.text(&hint, x + CW() * 2.0, top + half as f32 * rowh + CH() * 0.5, (S() / 2.0).max(1.0), DIM);
let rw = gfx.measure(&tr("set.reset_keys"), S()) + CW() * 4.0;
let rr = Rect::new(x + cols as f32 * CW() - rw - CW() * 2.0, y + (rows - 4) as f32 * CH(), rw, CH() * 2.2);
button(gfx, rr, &tr("set.reset_keys"), rr.contains(vec2(mx, my)), FRAME);
self.buttons.push((rr, Btn::ResetKeys));
} else {
for (i, &it) in tab_items(tab).iter().enumerate() {
let by = top + i as f32 * CH() * 2.5;
gfx.text(&item_label(it), x + CW() * 3.0, by + CH() * 0.4, S(), TXT);
// value with < > buttons
let vw = 18.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 val = item_value(it, s);
gfx.text(&val, vx + (vw - gfx.measure(&val, S())) * 0.5, by + CH() * 0.4, S(), GOLD);
let mid = Rect::new(vx + CW() * 2.5, by, vw - CW() * 5.0, CH() * 1.8);
self.buttons.push((left, Btn::Change(i, -1)));
self.buttons.push((right, Btn::Change(i, 1)));
self.buttons.push((mid, Btn::Change(i, 1)));
}
}
let bw = gfx.measure(&tr("menu.back"), S()) + CW() * 6.0;
let br = Rect::new(x + CW() * 2.0, y + (rows - 4) as f32 * CH(), bw, CH() * 2.2);
button(gfx, br, &tr("menu.back"), br.contains(vec2(mx, my)), FRAME);
self.buttons.push((br, Btn::Back));
}
}
+43 -1
View File
@@ -1,5 +1,6 @@
// Browser integration via our own miniquad JS plugin (www/dc_plugin.js): // Browser integration via our own miniquad JS plugin (www/dc_plugin.js):
// persistent key/value storage (localStorage). Native builds use an in-memory fallback. // persistent key/value storage (localStorage), a cache that is written to storage when the
// page is hidden or closed, file download and file upload. Native builds use simple fallbacks.
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
mod imp { mod imp {
@@ -7,6 +8,11 @@ mod imp {
fn dc_storage_get(key: *const u8, key_len: u32) -> i32; fn dc_storage_get(key: *const u8, key_len: u32) -> i32;
fn dc_storage_take(dst: *mut u8); fn dc_storage_take(dst: *mut u8);
fn dc_storage_set(key: *const u8, key_len: u32, val: *const u8, val_len: u32); fn dc_storage_set(key: *const u8, key_len: u32, val: *const u8, val_len: u32);
fn dc_cache_set(key: *const u8, key_len: u32, val: *const u8, val_len: u32);
fn dc_download(name: *const u8, name_len: u32, data: *const u8, data_len: u32);
fn dc_open_file();
fn dc_file_poll() -> i32;
fn dc_file_take(dst: *mut u8);
} }
pub fn storage_get(key: &str) -> Option<String> { pub fn storage_get(key: &str) -> Option<String> {
@@ -24,6 +30,32 @@ mod imp {
pub fn storage_set(key: &str, val: &str) { pub fn storage_set(key: &str, val: &str) {
unsafe { dc_storage_set(key.as_ptr(), key.len() as u32, val.as_ptr(), val.len() as u32) } unsafe { dc_storage_set(key.as_ptr(), key.len() as u32, val.as_ptr(), val.len() as u32) }
} }
// value is written to storage by JS when the page gets hidden or closed
pub fn cache_on_leave(key: &str, val: &str) {
unsafe { dc_cache_set(key.as_ptr(), key.len() as u32, val.as_ptr(), val.len() as u32) }
}
pub fn download(name: &str, data: &[u8]) {
unsafe { dc_download(name.as_ptr(), name.len() as u32, data.as_ptr(), data.len() as u32) }
}
// opens the browser file picker; poll with take_file()
pub fn open_file() {
unsafe { dc_open_file() }
}
pub fn take_file() -> Option<Vec<u8>> {
unsafe {
let n = dc_file_poll();
if n < 0 {
return None;
}
let mut buf = vec![0u8; n as usize];
dc_file_take(buf.as_mut_ptr());
Some(buf)
}
}
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@@ -39,6 +71,16 @@ mod imp {
pub fn storage_set(key: &str, val: &str) { pub fn storage_set(key: &str, val: &str) {
STORE.with(|s| s.borrow_mut().insert(key.to_string(), val.to_string())); STORE.with(|s| s.borrow_mut().insert(key.to_string(), val.to_string()));
} }
pub fn cache_on_leave(key: &str, val: &str) {
storage_set(key, val);
}
pub fn download(name: &str, data: &[u8]) {
let _ = std::fs::write(name, data);
}
pub fn open_file() {}
pub fn take_file() -> Option<Vec<u8>> {
None
}
} }
pub use imp::*; pub use imp::*;
+23 -6
View File
@@ -8,6 +8,7 @@ use crate::map::*;
use macroquad::miniquad::{BlendFactor, BlendState, BlendValue, Comparison, Equation, PipelineParams, UniformDesc, UniformType}; use macroquad::miniquad::{BlendFactor, BlendState, BlendValue, Comparison, Equation, PipelineParams, UniformDesc, UniformType};
use macroquad::models::{Mesh, Vertex}; use macroquad::models::{Mesh, Vertex};
use macroquad::prelude::*; use macroquad::prelude::*;
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;
@@ -15,11 +16,11 @@ 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;
const WALL_SUB: usize = 3; // subdivisions per wall face edge
const DISP_AMP: f32 = 0.11; // wall surface irregularity const DISP_AMP: f32 = 0.11; // wall surface irregularity
// ------------------------------------------------------------------ camera // ------------------------------------------------------------------ camera
#[derive(Clone, Serialize, Deserialize)]
pub struct Cam { pub struct Cam {
pub tx: f32, // look-at target on the ground (world x) pub tx: f32, // look-at target on the ground (world x)
pub tz: f32, // look-at target on the ground (world z = map y) pub tz: f32, // look-at target on the ground (world z = map y)
@@ -411,7 +412,7 @@ 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) -> (Option<Mesh>, Option<Mesh>, Option<Mesh>) { 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>) {
let mut mb = MB::new(); let mut mb = MB::new();
let mut ov = MB::new(); let mut ov = MB::new();
let mut ln = MB::new(); let mut ln = MB::new();
@@ -485,7 +486,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
[dark; 4], [dark; 4],
[Vec3::Y; 4], [Vec3::Y; 4],
); );
let n = WALL_SUB; let n = wall_sub;
let top_amb = amb_at(amb, xf + 0.5, zf + 0.5) * 0.95 + 0.1; let top_amb = amb_at(amb, xf + 0.5, zf + 0.5) * 0.95 + 0.1;
build_grid( build_grid(
&mut mb, &mut mb,
@@ -615,6 +616,8 @@ pub struct R3d {
glow: Material, glow: Material,
chunks: Vec<Chunk>, chunks: Vec<Chunk>,
pub outlines: bool, // tile outlines on/off pub outlines: bool, // tile outlines on/off
wall_sub: usize, // wall face subdivisions (detail)
pub max_lights: usize,
} }
fn rgba(u: u32) -> Color { fn rgba(u: u32) -> Color {
@@ -645,7 +648,21 @@ 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(); let chunks = (0..NCH * NCH).map(|_| Chunk { sig: 0, mesh: None, overlay: None, lines: None }).collect();
R3d { lit: lit_material(), glow: glow_material(), chunks, outlines: true } R3d { lit: lit_material(), glow: glow_material(), chunks, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS }
}
// Changing the wall detail rebuilds all chunks
pub fn set_wall_detail(&mut self, sub: usize) {
if sub != self.wall_sub {
self.wall_sub = sub;
self.invalidate();
}
}
pub fn invalidate(&mut self) {
for c in self.chunks.iter_mut() {
c.sig = 0;
}
} }
fn update_chunks(&mut self, g: &Game, gfx: &Gfx) { fn update_chunks(&mut self, g: &Game, gfx: &Gfx) {
@@ -658,7 +675,7 @@ impl R3d {
let ch = &mut self.chunks[(cz * NCH + cx) as usize]; let ch = &mut self.chunks[(cz * NCH + 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); let (m, o, l) = 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;
@@ -714,7 +731,7 @@ impl R3d {
} }
} }
l.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); l.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
l.truncate(MAX_LIGHTS); l.truncate(self.max_lights.min(MAX_LIGHTS));
let mut pos = vec![Vec4::ZERO; MAX_LIGHTS]; let mut pos = vec![Vec4::ZERO; MAX_LIGHTS];
let mut colr = vec![Vec4::ZERO; MAX_LIGHTS]; let mut colr = vec![Vec4::ZERO; MAX_LIGHTS];
for (k, (_, p, c)) in l.iter().enumerate() { for (k, (_, p, c)) in l.iter().enumerate() {
+90
View File
@@ -0,0 +1,90 @@
// Savegames: JSON with format tag and version, containing the game state, the camera
// and the player's settings (settings are always applied when a savegame is loaded).
use crate::game::Game;
use crate::render3d::Cam;
use crate::settings::Settings;
use serde::{Deserialize, Serialize};
pub const FORMAT: &str = "dungeoncraft-save";
pub const VERSION: u32 = 1;
pub const AUTOSAVE_KEY: &str = "dc.autosave";
#[derive(Serialize)]
struct SaveOut<'a> {
format: &'a str,
version: u32,
game: &'a Game,
camera: &'a Cam,
settings: &'a Settings,
}
#[derive(Deserialize)]
struct SaveIn {
format: String,
version: u32,
game: Game,
camera: Option<Cam>,
settings: Option<Settings>,
}
pub struct Loaded {
pub game: Game,
pub camera: Option<Cam>,
pub settings: Option<Settings>,
}
pub fn to_json(game: &Game, camera: &Cam, settings: &Settings) -> String {
serde_json::to_string(&SaveOut { format: FORMAT, version: VERSION, game, camera, settings }).unwrap_or_default()
}
// Err contains a language key describing the problem
pub fn from_bytes(data: &[u8]) -> Result<Loaded, &'static str> {
let s: SaveIn = serde_json::from_slice(data).map_err(|_| "save.err_invalid")?;
if s.format != FORMAT {
return Err("save.err_invalid");
}
if s.version > VERSION {
return Err("save.err_version");
}
let mut game = s.game;
game.recount();
Ok(Loaded { game, camera: s.camera, settings: s.settings })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn save_roundtrip() {
let mut g = Game::new(42);
let (cx, cy) = g.map.core;
g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true);
for _ in 0..3000 {
g.update(0.05);
}
let cam = Cam::new(10.0, 20.0);
let mut settings = Settings::default();
settings.language = "de".into();
let json = to_json(&g, &cam, &settings);
println!("savegame size: {} KB", json.len() / 1024);
let l = from_bytes(json.as_bytes()).expect("load");
assert_eq!(l.game.time, g.time);
assert_eq!(l.game.ents.len(), g.ents.len());
assert_eq!(l.game.gold, g.gold);
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.camera.unwrap().tx, 10.0);
// the loaded game keeps running identically
let mut a = l.game;
let mut b = g;
for _ in 0..2000 {
a.update(0.05);
b.update(0.05);
}
assert_eq!(a.gold, b.gold);
assert_eq!(a.ents.len(), b.ents.len());
assert!(from_bytes(b"{\"format\":\"x\"}").is_err());
}
}
+209
View File
@@ -0,0 +1,209 @@
// Player settings: stored in the browser (localStorage "dc.settings") and in every savegame.
use macroquad::prelude::KeyCode;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
// Rebindable keyboard actions. Esc (menu) and the arrow keys (camera) are fixed.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Action {
Tool(usize), // 0..12, same order as ui::TOOLS
CamForward,
CamBack,
CamLeft,
CamRight,
RotateLeft,
RotateRight,
TiltUp,
TiltDown,
Pause,
Mute,
VolumeUp,
VolumeDown,
Outlines,
Help,
}
pub fn all_actions() -> Vec<Action> {
let mut v: Vec<Action> = (0..12).map(Action::Tool).collect();
v.extend_from_slice(&[
Action::CamForward,
Action::CamBack,
Action::CamLeft,
Action::CamRight,
Action::RotateLeft,
Action::RotateRight,
Action::TiltUp,
Action::TiltDown,
Action::Pause,
Action::Mute,
Action::VolumeUp,
Action::VolumeDown,
Action::Outlines,
Action::Help,
]);
v
}
impl Action {
// stable id used in settings files and as language key suffix (key.<id>)
pub fn id(self) -> String {
match self {
Action::Tool(i) => format!("tool{}", i),
Action::CamForward => "cam_forward".into(),
Action::CamBack => "cam_back".into(),
Action::CamLeft => "cam_left".into(),
Action::CamRight => "cam_right".into(),
Action::RotateLeft => "rotate_left".into(),
Action::RotateRight => "rotate_right".into(),
Action::TiltUp => "tilt_up".into(),
Action::TiltDown => "tilt_down".into(),
Action::Pause => "pause".into(),
Action::Mute => "mute".into(),
Action::VolumeUp => "volume_up".into(),
Action::VolumeDown => "volume_down".into(),
Action::Outlines => "outlines".into(),
Action::Help => "help".into(),
}
}
fn default_key(self) -> KeyCode {
use KeyCode::*;
match self {
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V][i.min(11)],
Action::CamForward => W,
Action::CamBack => S,
Action::CamLeft => A,
Action::CamRight => D,
Action::RotateLeft => Q,
Action::RotateRight => E,
Action::TiltUp => R,
Action::TiltDown => F,
Action::Pause => P,
Action::Mute => M,
Action::VolumeUp => Equal,
Action::VolumeDown => Minus,
Action::Outlines => G,
Action::Help => H,
}
}
}
// Keys that can be bound, with their display names
const KEY_NAMES: &[(KeyCode, &str)] = &[
(KeyCode::A, "A"), (KeyCode::B, "B"), (KeyCode::C, "C"), (KeyCode::D, "D"), (KeyCode::E, "E"), (KeyCode::F, "F"),
(KeyCode::G, "G"), (KeyCode::H, "H"), (KeyCode::I, "I"), (KeyCode::J, "J"), (KeyCode::K, "K"), (KeyCode::L, "L"),
(KeyCode::M, "M"), (KeyCode::N, "N"), (KeyCode::O, "O"), (KeyCode::P, "P"), (KeyCode::Q, "Q"), (KeyCode::R, "R"),
(KeyCode::S, "S"), (KeyCode::T, "T"), (KeyCode::U, "U"), (KeyCode::V, "V"), (KeyCode::W, "W"), (KeyCode::X, "X"),
(KeyCode::Y, "Y"), (KeyCode::Z, "Z"),
(KeyCode::Key0, "0"), (KeyCode::Key1, "1"), (KeyCode::Key2, "2"), (KeyCode::Key3, "3"), (KeyCode::Key4, "4"),
(KeyCode::Key5, "5"), (KeyCode::Key6, "6"), (KeyCode::Key7, "7"), (KeyCode::Key8, "8"), (KeyCode::Key9, "9"),
(KeyCode::Space, "Space"), (KeyCode::Minus, "-"), (KeyCode::Equal, "="), (KeyCode::Comma, ","), (KeyCode::Period, "."),
(KeyCode::Slash, "/"), (KeyCode::Semicolon, ";"), (KeyCode::Apostrophe, "'"), (KeyCode::LeftBracket, "["),
(KeyCode::RightBracket, "]"), (KeyCode::Backslash, "\\"), (KeyCode::Tab, "Tab"), (KeyCode::Backspace, "Backspace"),
(KeyCode::Enter, "Enter"), (KeyCode::Insert, "Insert"), (KeyCode::Delete, "Delete"), (KeyCode::Home, "Home"),
(KeyCode::End, "End"), (KeyCode::PageUp, "PageUp"), (KeyCode::PageDown, "PageDown"),
(KeyCode::F1, "F1"), (KeyCode::F2, "F2"), (KeyCode::F3, "F3"), (KeyCode::F4, "F4"), (KeyCode::F6, "F6"),
(KeyCode::F7, "F7"), (KeyCode::F8, "F8"), (KeyCode::F9, "F9"), (KeyCode::F10, "F10"),
(KeyCode::Kp0, "Num0"), (KeyCode::Kp1, "Num1"), (KeyCode::Kp2, "Num2"), (KeyCode::Kp3, "Num3"), (KeyCode::Kp4, "Num4"),
(KeyCode::Kp5, "Num5"), (KeyCode::Kp6, "Num6"), (KeyCode::Kp7, "Num7"), (KeyCode::Kp8, "Num8"), (KeyCode::Kp9, "Num9"),
(KeyCode::KpAdd, "Num+"), (KeyCode::KpSubtract, "Num-"), (KeyCode::KpMultiply, "Num*"), (KeyCode::KpDivide, "Num/"),
(KeyCode::LeftShift, "Shift"), (KeyCode::LeftControl, "Ctrl"), (KeyCode::LeftAlt, "Alt"),
];
pub fn key_name(k: KeyCode) -> String {
KEY_NAMES.iter().find(|(c, _)| *c == k).map(|(_, n)| n.to_string()).unwrap_or_else(|| "?".into())
}
fn key_from_name(n: &str) -> Option<KeyCode> {
KEY_NAMES.iter().find(|(_, name)| *name == n).map(|(c, _)| *c)
}
// All bindable keys (for the key capture dialog)
pub fn bindable_keys() -> impl Iterator<Item = KeyCode> {
KEY_NAMES.iter().map(|(k, _)| *k)
}
#[derive(Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct Settings {
// language & sound
pub language: String,
pub volume: f32,
pub music_volume: f32,
pub muted: bool,
// camera
pub rotate_speed: f32,
pub zoom_speed: f32,
pub edge_scroll: bool,
pub invert_mouse: bool,
// graphics
pub outlines: bool,
pub wall_detail: u8, // 1 low, 2 medium, 3 high
pub max_lights: u8, // 8, 16 or 24
pub ui_scale: u8, // font scale 2 (normal) or 3 (large)
// game
pub game_speed: f32, // 1 or 2
pub hints: bool,
// key bindings: action id -> key name
pub keys: BTreeMap<String, String>,
}
impl Default for Settings {
fn default() -> Settings {
Settings {
language: "en".into(),
volume: 0.7,
music_volume: 0.6,
muted: false,
rotate_speed: 1.0,
zoom_speed: 1.0,
edge_scroll: true,
invert_mouse: false,
outlines: true,
wall_detail: 2,
max_lights: 24,
ui_scale: 2,
game_speed: 1.0,
hints: true,
keys: BTreeMap::new(),
}
}
}
impl Settings {
pub fn key(&self, a: Action) -> KeyCode {
self.keys.get(&a.id()).and_then(|n| key_from_name(n)).unwrap_or_else(|| a.default_key())
}
// Bind a key; if another action already uses it, the two actions swap keys
pub fn bind(&mut self, a: Action, k: KeyCode) {
let old = self.key(a);
for other in all_actions() {
if other != a && self.key(other) == k {
self.keys.insert(other.id(), key_name(old));
}
}
self.keys.insert(a.id(), key_name(k));
}
pub fn reset_keys(&mut self) {
self.keys.clear();
}
pub fn wall_subdivisions(&self) -> usize {
match self.wall_detail {
1 => 2,
3 => 4,
_ => 3,
}
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
pub fn from_json(s: &str) -> Option<Settings> {
serde_json::from_str(s).ok()
}
}
+18 -4
View File
@@ -3,7 +3,25 @@
// Events without a registered sound stay silent. // Events without a registered sound stay silent.
use crate::game::{Sfx, SfxEvent, SFX_ALL}; use crate::game::{Sfx, SfxEvent, SFX_ALL};
#[cfg(feature = "audio")]
use macroquad::audio::{load_sound_from_bytes, play_sound, PlaySoundParams, Sound}; use macroquad::audio::{load_sound_from_bytes, play_sound, PlaySoundParams, Sound};
#[cfg(not(feature = "audio"))]
use stub::*;
// Silent stand-in when the "audio" feature is off (native tests)
#[cfg(not(feature = "audio"))]
mod stub {
#[derive(Clone)]
pub struct Sound;
pub struct PlaySoundParams {
pub looped: bool,
pub volume: f32,
}
pub async fn load_sound_from_bytes(_: &[u8]) -> Result<Sound, ()> {
Err(())
}
pub fn play_sound(_: &Sound, _: PlaySoundParams) {}
}
use macroquad::prelude::*; use macroquad::prelude::*;
pub struct SoundBank { pub struct SoundBank {
@@ -33,10 +51,6 @@ impl SoundBank {
// procedurally generated sounds will be registered here // procedurally generated sounds will be registered here
pub async fn generate_all(&mut self) {} pub async fn generate_all(&mut self) {}
pub fn change_volume(&mut self, d: f32) {
self.volume = (self.volume + d).clamp(0.0, 1.0);
}
// minimum gap between two identical sounds (avoids a wall of noise) // minimum gap between two identical sounds (avoids a wall of noise)
fn min_gap(sfx: Sfx) -> f64 { fn min_gap(sfx: Sfx) -> f64 {
match sfx { match sfx {
+122 -90
View File
@@ -6,12 +6,34 @@ use crate::gfx::*;
use crate::i18n::{languages, tr, trf}; use crate::i18n::{languages, tr, trf};
use crate::map::*; use crate::map::*;
use crate::render3d::{minimap_color, Cam}; use crate::render3d::{minimap_color, Cam};
use crate::settings::{key_name, Action, Settings};
use crate::sound::SoundBank; use crate::sound::SoundBank;
use macroquad::prelude::*; use macroquad::prelude::*;
pub const S: f32 = 2.0; // UI font scale // UI font scale (setting: 2 = normal, 3 = large); character cell size follows it
pub const CW: f32 = GW * S; // character width in px thread_local! {
pub const CH: f32 = GH * S; // character height in px static UI_SCALE: std::cell::Cell<f32> = std::cell::Cell::new(2.0);
}
pub fn set_ui_scale(s: f32) {
UI_SCALE.with(|c| c.set(s));
}
#[allow(non_snake_case)]
pub fn S() -> f32 {
UI_SCALE.with(|c| c.get())
}
#[allow(non_snake_case)]
pub fn CW() -> f32 {
GW * S()
}
#[allow(non_snake_case)]
pub fn CH() -> f32 {
GH * S()
}
// factor relative to the normal scale, for pixel sizes such as buttons and icons
#[allow(non_snake_case)]
pub fn K() -> f32 {
S() / 2.0
}
pub const FRAME: Color = Color::new(0.62, 0.52, 0.36, 1.0); pub const FRAME: Color = Color::new(0.62, 0.52, 0.36, 1.0);
pub const FRAME_HI: Color = Color::new(0.95, 0.8, 0.4, 1.0); pub const FRAME_HI: Color = Color::new(0.95, 0.8, 0.4, 1.0);
@@ -51,7 +73,6 @@ pub const TOOLS: [Tool; 12] = [
Tool::Summon, Tool::Summon,
]; ];
pub const TOOL_KEYS: [&str; 12] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "X", "V"];
pub struct ToolInfo { pub struct ToolInfo {
pub label: String, pub label: String,
@@ -216,42 +237,43 @@ impl Ui {
UiAction::None UiAction::None
} }
pub fn draw(&mut self, g: &Game, gfx: &Gfx, cam: &Cam, snd: &SoundBank, info: &[(String, Color)], now: f64) { pub fn draw(&mut self, g: &Game, gfx: &Gfx, cam: &Cam, snd: &SoundBank, settings: &Settings, info: &[(String, Color)], now: f64) {
self.rects.clear(); self.rects.clear();
let keys: Vec<String> = (0..TOOLS.len()).map(|i| key_name(settings.key(Action::Tool(i)))).collect();
let sw = screen_width(); let sw = screen_width();
let sh = screen_height(); let sh = screen_height();
let (mx, my) = mouse_position(); let (mx, my) = mouse_position();
// ---------------- top bar // ---------------- top bar
let cols = (sw / CW).ceil() as i32; let cols = (sw / CW()).ceil() as i32;
gfx.window(0.0, 0.0, cols, 3, S, false, FRAME, BG); gfx.window(0.0, 0.0, cols, 3, S(), false, FRAME, BG);
self.rects.push(Rect::new(0.0, 0.0, sw, 3.0 * CH)); self.rects.push(Rect::new(0.0, 0.0, sw, 3.0 * CH()));
let y = CH; let y = CH();
let mut x = CW * 2.0; let mut x = CW() * 2.0;
let seg = |label: &str, lc: Color, value: &str, vc: Color, x: &mut f32| { let seg = |label: &str, lc: Color, value: &str, vc: Color, x: &mut f32| {
gfx.text(label, *x, y, S, lc); gfx.text(label, *x, y, S(), lc);
*x += gfx.measure(label, S) + CW; *x += gfx.measure(label, S()) + CW();
gfx.text(value, *x, y, S, vc); gfx.text(value, *x, y, S(), vc);
*x += gfx.measure(value, S) + CW * 2.0; *x += gfx.measure(value, S()) + CW() * 2.0;
}; };
seg(&tr("hud.gold"), GOLD, &format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut x); seg(&tr("hud.gold"), GOLD, &format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut x);
let mana = tr("hud.mana"); let mana = tr("hud.mana");
gfx.text(&mana, x, y, S, MANA); gfx.text(&mana, x, y, S(), MANA);
x += gfx.measure(&mana, S) + CW * 0.5; x += gfx.measure(&mana, S()) + CW() * 0.5;
bar(x, y + 3.0, 70.0, 10.0, g.mana / MANA_MAX, MANA); bar(x, y + 3.0, 70.0, 10.0, g.mana / MANA_MAX, MANA);
x += 70.0 + CW * 0.5; x += 70.0 + CW() * 0.5;
gfx.text(&format!("{}", g.mana as i32), x, y, S, TXT); gfx.text(&format!("{}", g.mana as i32), x, y, S(), TXT);
x += CW * 5.0; x += CW() * 5.0;
let core = tr("hud.core"); let core = tr("hud.core");
gfx.text(&core, x, y, S, RED); gfx.text(&core, x, y, S(), RED);
x += gfx.measure(&core, S) + CW * 0.5; x += gfx.measure(&core, S()) + CW() * 0.5;
bar(x, y + 3.0, 70.0, 10.0, g.core_hp / CORE_HP, RED); bar(x, y + 3.0, 70.0, 10.0, g.core_hp / CORE_HP, RED);
x += 70.0 + CW * 1.5; x += 70.0 + CW() * 1.5;
let gate = tr("hud.gate"); let gate = tr("hud.gate");
gfx.text(&gate, x, y, S, BLUE); gfx.text(&gate, x, y, S(), BLUE);
x += gfx.measure(&gate, S) + CW * 0.5; x += gfx.measure(&gate, S()) + CW() * 0.5;
bar(x, y + 3.0, 70.0, 10.0, g.gate_hp / GATE_HP, BLUE); bar(x, y + 3.0, 70.0, 10.0, g.gate_hp / GATE_HP, BLUE);
x += 70.0 + CW * 1.5; x += 70.0 + CW() * 1.5;
let cap = g.counts[1].min(30); let cap = g.counts[1].min(30);
seg(&tr("hud.creatures"), TXT, &format!("{}/{}", g.creature_count(), cap), TXT, &mut x); seg(&tr("hud.creatures"), TXT, &format!("{}/{}", g.creature_count(), cap), TXT, &mut x);
seg(&tr("hud.workers"), TXT, &format!("{}", g.worker_count()), TXT, &mut x); seg(&tr("hud.workers"), TXT, &format!("{}", g.worker_count()), TXT, &mut x);
@@ -259,20 +281,23 @@ impl Ui {
seg(&trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col, &mmss(g.wave_timer), wave_col, &mut x); seg(&trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col, &mmss(g.wave_timer), wave_col, &mut x);
seg(&tr("hud.army"), TXT, &format!("{}", g.army_value() as i32), TXT, &mut x); seg(&tr("hud.army"), TXT, &format!("{}", g.army_value() as i32), TXT, &mut x);
let vol = if snd.muted { tr("hud.off") } else { format!("{}%", (snd.volume * 100.0).round() as i32) }; let vol = if snd.muted { tr("hud.off") } else { format!("{}%", (snd.volume * 100.0).round() as i32) };
seg("♪", DIM, &vol, DIM, &mut x); // volume only if it still fits into the bar
if x + gfx.measure(&format!("♪ {}", vol), S()) < sw - CW() * 2.0 {
seg("♪", DIM, &vol, DIM, &mut x);
}
// ---------------- toolbar // ---------------- toolbar
let tb_rows = 5; let tb_rows = 5;
let tb_y = sh - tb_rows as f32 * CH; let tb_y = sh - tb_rows as f32 * CH();
gfx.window(0.0, tb_y, cols, tb_rows, S, false, FRAME, BG); gfx.window(0.0, tb_y, cols, tb_rows, S(), false, FRAME, BG);
self.rects.push(Rect::new(0.0, tb_y, sw, tb_rows as f32 * CH)); self.rects.push(Rect::new(0.0, tb_y, sw, tb_rows as f32 * CH()));
self.tool_rects.clear(); self.tool_rects.clear();
let bw = 80.0; let bw = 80.0 * K();
let mut hover_tool = None; let mut hover_tool = None;
for (i, &t) in TOOLS.iter().enumerate() { for (i, &t) in TOOLS.iter().enumerate() {
let bx = CW + i as f32 * (bw + 4.0); let bx = CW() + i as f32 * (bw + 4.0 * K());
let by = tb_y + CH * 0.75; let by = tb_y + CH() * 0.75;
let r = Rect::new(bx, by, bw, CH * 3.5); let r = Rect::new(bx, by, bw, CH() * 3.5);
self.tool_rects.push(r); self.tool_rects.push(r);
let hovered = r.contains(vec2(mx, my)); let hovered = r.contains(vec2(mx, my));
if hovered { if hovered {
@@ -282,19 +307,21 @@ impl Ui {
draw_rectangle(r.x, r.y, r.w, r.h, if sel { Color::new(0.35, 0.22, 0.1, 1.0) } else if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) }); draw_rectangle(r.x, r.y, r.w, r.h, if sel { Color::new(0.35, 0.22, 0.1, 1.0) } else if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if sel { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) }); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if sel { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) });
let ti = tool_info(t); let ti = tool_info(t);
gfx.draw_cell(ti.icon, bx + (bw - 32.0) * 0.5, by + 4.0, 32.0, 32.0, WHITE); let icon = 32.0 * K();
gfx.text(TOOL_KEYS[i], bx + 3.0, by + 3.0, 1.0, if sel { FRAME_HI } else { DIM }); let small = (S() / 2.0).floor().max(1.0);
let lw = gfx.measure(&ti.label, 1.0); gfx.draw_cell(ti.icon, bx + (bw - icon) * 0.5, by + 4.0 * K(), icon, icon, WHITE);
gfx.text(&ti.label, bx + (bw - lw) * 0.5, by + 42.0, 1.0, if sel { FRAME_HI } else { TXT }); gfx.text(&keys[i], bx + 3.0, by + 3.0, small, if sel { FRAME_HI } else { DIM });
let lw = gfx.measure(&ti.label, small);
gfx.text(&ti.label, bx + (bw - lw) * 0.5, by + 42.0 * K(), small, if sel { FRAME_HI } else { TXT });
} }
// hand contents // hand contents
let hx = CW + TOOLS.len() as f32 * (bw + 4.0) + CW; let hx = CW() + TOOLS.len() as f32 * (bw + 4.0 * K()) + CW();
if hx + 120.0 < sw - 200.0 { if hx + 120.0 < sw - 200.0 {
gfx.text(&tr("hud.hand"), hx, tb_y + CH, S, TXT); gfx.text(&tr("hud.hand"), hx, tb_y + CH(), S(), TXT);
for (k, id) in g.hand.iter().enumerate() { for (k, id) in g.hand.iter().enumerate() {
if let Some(e) = g.ents.iter().find(|e| e.id == *id) { if let Some(e) = g.ents.iter().find(|e| e.id == *id) {
let sx = hx + (k % 4) as f32 * 26.0; let sx = hx + (k % 4) as f32 * 26.0;
let sy = tb_y + CH * 2.2 + (k / 4) as f32 * 24.0; let sy = tb_y + CH() * 2.2 + (k / 4) as f32 * 24.0;
draw_texture_ex( draw_texture_ex(
&gfx.sprites, &gfx.sprites,
sx, sx,
@@ -305,18 +332,18 @@ impl Ui {
} }
} }
if g.hand.is_empty() { if g.hand.is_empty() {
gfx.text(&tr("hud.empty"), hx, tb_y + CH * 2.5, 1.0, DIM); gfx.text(&tr("hud.empty"), hx, tb_y + CH() * 2.5, 1.0, DIM);
} }
} }
// ---------------- minimap // ---------------- minimap
let mm_cols = 16; let mm_cols = 16;
let mm_rows = 12; let mm_rows = 12;
let mm_w = mm_cols as f32 * CW; let mm_w = mm_cols as f32 * CW();
let mm_h = mm_rows as f32 * CH; let mm_h = mm_rows as f32 * CH();
let mm_x = sw - mm_w; let mm_x = sw - mm_w;
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));
if now - self.minimap_t > 0.25 { if now - self.minimap_t > 0.25 {
self.minimap_t = now; self.minimap_t = now;
@@ -330,9 +357,9 @@ 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); let map_px = (mm_w - CW() * 2.0).min(mm_h - CH() * 2.0);
let mrx = mm_x + (mm_w - map_px) * 0.5; let mrx = mm_x + (mm_w - map_px) * 0.5;
let mry = mm_y + CH + (mm_h - CH * 2.0 - map_px) * 0.5; let mry = mm_y + CH() + (mm_h - CH() * 2.0 - map_px) * 0.5;
self.minimap_rect = Rect::new(mrx, mry, map_px, map_px); self.minimap_rect = Rect::new(mrx, mry, map_px, map_px);
draw_texture_ex(&self.minimap_tex, mrx, mry, WHITE, DrawTextureParams { dest_size: Some(vec2(map_px, map_px)), ..Default::default() }); draw_texture_ex(&self.minimap_tex, mrx, mry, WHITE, DrawTextureParams { dest_size: Some(vec2(map_px, map_px)), ..Default::default() });
let ms = map_px / MW as f32; let ms = map_px / MW as f32;
@@ -366,18 +393,18 @@ impl Ui {
} }
} }
let irows = lines.len() as i32 + 2; let irows = lines.len() as i32 + 2;
let ix = sw - icols as f32 * CW; let ix = sw - icols as f32 * CW();
let iy = 3.0 * CH; let iy = 3.0 * CH();
gfx.window_titled(ix, iy, icols, irows, S, false, FRAME, BG, &info[0].0, info[0].1); gfx.window_titled(ix, iy, icols, irows, S(), false, FRAME, BG, &info[0].0, info[0].1);
for (k, (l, c)) in lines.iter().enumerate() { for (k, (l, c)) in lines.iter().enumerate() {
gfx.text(l, ix + CW * 2.0, iy + CH * (k as f32 + 1.0), S, *c); gfx.text(l, ix + CW() * 2.0, iy + CH() * (k as f32 + 1.0), S(), *c);
} }
} }
// ---------------- messages (translated at display time, so a language switch applies at once) // ---------------- messages (translated at display time, so a language switch applies at once)
let recent: Vec<&(Msg, f64)> = self.log.iter().filter(|(_, t)| now - t < 14.0).collect(); let recent: Vec<&(Msg, f64)> = self.log.iter().filter(|(_, t)| now - t < 14.0).collect();
if !recent.is_empty() { if !recent.is_empty() {
let mcols = (((sw - mm_w) / CW) as i32 - 2).clamp(20, 70); let mcols = (((sw - mm_w) / CW()) as i32 - 2).clamp(20, 70);
let mut lines: Vec<(String, f32)> = Vec::new(); let mut lines: Vec<(String, f32)> = Vec::new();
for (m, t) in recent.iter().rev().take(5).rev() { for (m, t) in recent.iter().rev().take(5).rev() {
let a = (1.0 - ((now - t) as f32 - 11.0) / 3.0).clamp(0.2, 1.0); let a = (1.0 - ((now - t) as f32 - 11.0) / 3.0).clamp(0.2, 1.0);
@@ -386,11 +413,11 @@ impl Ui {
} }
} }
let mrows = lines.len() as i32 + 2; let mrows = lines.len() as i32 + 2;
let my0 = tb_y - mrows as f32 * CH; let my0 = tb_y - mrows as f32 * CH();
gfx.window(0.0, my0, mcols, mrows, S, false, FRAME, Color::new(0.06, 0.04, 0.05, 0.75)); gfx.window(0.0, my0, mcols, mrows, S(), false, FRAME, Color::new(0.06, 0.04, 0.05, 0.75));
self.rects.push(Rect::new(0.0, my0, mcols as f32 * CW, mrows as f32 * CH)); self.rects.push(Rect::new(0.0, my0, mcols as f32 * CW(), mrows as f32 * CH()));
for (k, (l, a)) in lines.iter().enumerate() { for (k, (l, a)) in lines.iter().enumerate() {
gfx.text(l, CW * 2.0, my0 + CH * (k as f32 + 1.0), S, Color::new(TXT.r, TXT.g, TXT.b, *a)); gfx.text(l, CW() * 2.0, my0 + CH() * (k as f32 + 1.0), S(), Color::new(TXT.r, TXT.g, TXT.b, *a));
} }
} }
if self.log.len() > 100 { if self.log.len() > 100 {
@@ -407,17 +434,22 @@ impl Ui {
if !cost.is_empty() { if !cost.is_empty() {
lines.push((trf("ui.cost", &[cost]), GOLD)); lines.push((trf("ui.cost", &[cost]), GOLD));
} }
lines.push((trf("ui.key", &[TOOL_KEYS[i].to_string()]), DIM)); lines.push((trf("ui.key", &[keys[i].clone()]), DIM));
let rows = lines.len() as i32 + 2; let rows = lines.len() as i32 + 2;
let tx = (mx - tcols as f32 * CW * 0.5).clamp(0.0, sw - tcols as f32 * CW); let tx = (mx - tcols as f32 * CW() * 0.5).clamp(0.0, sw - tcols as f32 * CW());
let ty = tb_y - rows as f32 * CH - 4.0; let ty = tb_y - rows as f32 * CH() - 4.0;
gfx.window_titled(tx, ty, tcols as i32, rows, S, false, FRAME_HI, BG, &ti.name, FRAME_HI); gfx.window_titled(tx, ty, tcols as i32, rows, S(), false, FRAME_HI, BG, &ti.name, FRAME_HI);
for (k, (l, c)) in lines.iter().enumerate() { for (k, (l, c)) in lines.iter().enumerate() {
gfx.text(l, tx + CW * 2.0, ty + CH * (k as f32 + 1.0), S, *c); gfx.text(l, tx + CW() * 2.0, ty + CH() * (k as f32 + 1.0), S(), *c);
} }
} }
// ---------------- dialogs }
// Dialogs on top of everything (help, language selection, end screen, pause)
pub fn draw_dialogs(&mut self, g: &Game, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();
if self.show_langsel { if self.show_langsel {
self.draw_langsel(gfx); self.draw_langsel(gfx);
} else if g.state != 0 { } else if g.state != 0 {
@@ -426,13 +458,13 @@ impl Ui {
self.draw_help(gfx); self.draw_help(gfx);
} else if g.paused { } else if g.paused {
let w = 24; let w = 24;
let x = (sw - w as f32 * CW) * 0.5; let x = (sw - w as f32 * CW()) * 0.5;
let y = sh * 0.35; let y = sh * 0.35;
gfx.window(x, y, w, 5, S, true, FRAME_HI, BG); gfx.window(x, y, w, 5, S(), true, FRAME_HI, BG);
let t = tr("ui.pause"); let t = tr("ui.pause");
gfx.text(&t, (sw - gfx.measure(&t, 3.0)) * 0.5, y + CH, 3.0, FRAME_HI); gfx.text(&t, (sw - gfx.measure(&t, 3.0)) * 0.5, y + CH(), 3.0, FRAME_HI);
let t2 = tr("ui.pause_hint"); let t2 = tr("ui.pause_hint");
gfx.text(&t2, (sw - gfx.measure(&t2, S)) * 0.5, y + CH * 3.0, S, DIM); gfx.text(&t2, (sw - gfx.measure(&t2, S())) * 0.5, y + CH() * 3.0, S(), DIM);
} }
} }
@@ -464,11 +496,11 @@ impl Ui {
let rows = lines.len() as i32 + 4; let rows = lines.len() as i32 + 4;
let sw = screen_width(); let sw = screen_width();
let sh = screen_height(); let sh = screen_height();
let x = ((sw - cols as f32 * CW) * 0.5).max(0.0); let x = ((sw - cols as f32 * CW()) * 0.5).max(0.0);
let y = ((sh - rows as f32 * CH) * 0.5).max(0.0); 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), "DungeonCraft", FRAME_HI); gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), "DungeonCraft", FRAME_HI);
for (k, (l, c)) in lines.iter().enumerate() { for (k, (l, c)) in lines.iter().enumerate() {
gfx.text(l, x + CW * 3.0, y + CH * (k as f32 + 2.0), S, *c); gfx.text(l, x + CW() * 3.0, y + CH() * (k as f32 + 2.0), S(), *c);
} }
self.rects.push(Rect::new(0.0, 0.0, sw, sh)); self.rects.push(Rect::new(0.0, 0.0, sw, sh));
} }
@@ -481,28 +513,28 @@ impl Ui {
let langs = languages(); let langs = languages();
let cols = 50; let cols = 50;
let rows = 6 + langs.len() as i32 * 3; let rows = 6 + langs.len() as i32 * 3;
let x = ((sw - cols as f32 * CW) * 0.5).max(0.0); let x = ((sw - cols as f32 * CW()) * 0.5).max(0.0);
let y = ((sh - rows as f32 * CH) * 0.5).max(0.0); let y = ((sh - rows as f32 * CH()) * 0.5).max(0.0);
// the title shows all language names, since no language is chosen yet // the title shows all language names, since no language is chosen yet
let title = langs.iter().map(|(c, _)| crate::i18n::tr_in(c, "langsel.title")).collect::<Vec<_>>().join(" / "); let title = langs.iter().map(|(c, _)| crate::i18n::tr_in(c, "langsel.title")).collect::<Vec<_>>().join(" / ");
gfx.window_titled(x, y, cols, rows, S, true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &title, FRAME_HI); gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &title, FRAME_HI);
self.lang_rects.clear(); self.lang_rects.clear();
let (mx, my) = mouse_position(); let (mx, my) = mouse_position();
for (k, (code, name)) in langs.iter().enumerate() { for (k, (code, name)) in langs.iter().enumerate() {
let bw = 24.0 * CW; let bw = 24.0 * CW();
let bx = (sw - bw) * 0.5; let bx = (sw - bw) * 0.5;
let by = y + CH * (2.0 + k as f32 * 3.0); let by = y + CH() * (2.0 + k as f32 * 3.0);
let r = Rect::new(bx, by, bw, CH * 2.0); let r = Rect::new(bx, by, bw, CH() * 2.0);
let hov = r.contains(vec2(mx, my)); let hov = r.contains(vec2(mx, my));
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) }); draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hov { FRAME_HI } else { FRAME }); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hov { FRAME_HI } else { FRAME });
gfx.text(name, bx + (bw - gfx.measure(name, S)) * 0.5, by + CH * 0.5, S, TXT); gfx.text(name, bx + (bw - gfx.measure(name, S())) * 0.5, by + CH() * 0.5, S(), TXT);
self.lang_rects.push((r, code.clone())); self.lang_rects.push((r, code.clone()));
} }
let hint_y = y + CH * (2.0 + langs.len() as f32 * 3.0 + 0.5); let hint_y = y + CH() * (2.0 + langs.len() as f32 * 3.0 + 0.5);
for (k, (c, _)) in langs.iter().enumerate() { for (k, (c, _)) in langs.iter().enumerate() {
let h = crate::i18n::tr_in(c, "langsel.hint"); let h = crate::i18n::tr_in(c, "langsel.hint");
gfx.text(&h, x + CW * 2.0, hint_y + CH * k as f32, 1.0, DIM); gfx.text(&h, x + CW() * 2.0, hint_y + CH() * k as f32, 1.0, DIM);
} }
self.rects.push(Rect::new(0.0, 0.0, sw, sh)); self.rects.push(Rect::new(0.0, 0.0, sw, sh));
} }
@@ -514,27 +546,27 @@ impl Ui {
let win = g.state == 1; let win = g.state == 1;
let cols = 44; let cols = 44;
let rows = 11; let rows = 11;
let x = (sw - cols as f32 * CW) * 0.5; let x = (sw - cols as f32 * CW()) * 0.5;
let y = (sh - rows as f32 * CH) * 0.5; let y = (sh - rows as f32 * CH()) * 0.5;
let col = if win { GOLD } else { RED }; let col = if win { GOLD } else { RED };
gfx.window(x, y, cols, rows, S, true, col, Color::new(0.05, 0.03, 0.04, 0.97)); gfx.window(x, y, cols, rows, S(), true, col, Color::new(0.05, 0.03, 0.04, 0.97));
let title = tr(if win { "end.victory" } else { "end.defeat" }); let title = tr(if win { "end.victory" } else { "end.defeat" });
gfx.text(&title, (sw - gfx.measure(&title, 4.0)) * 0.5, y + CH * 1.5, 4.0, col); gfx.text(&title, (sw - gfx.measure(&title, 4.0)) * 0.5, y + CH() * 1.5, 4.0, col);
let sub = tr(if win { "end.victory_sub" } else { "end.defeat_sub" }); let sub = tr(if win { "end.victory_sub" } else { "end.defeat_sub" });
gfx.text(&sub, (sw - gfx.measure(&sub, S)) * 0.5, y + CH * 4.5, S, TXT); gfx.text(&sub, (sw - gfx.measure(&sub, S())) * 0.5, y + CH() * 4.5, S(), TXT);
let st = trf("end.stats", &[mmss(g.time), g.wave.to_string(), (g.army_value() as i32).to_string()]); let st = trf("end.stats", &[mmss(g.time), g.wave.to_string(), (g.army_value() as i32).to_string()]);
gfx.text(&st, (sw - gfx.measure(&st, S)) * 0.5, y + CH * 6.0, S, DIM); gfx.text(&st, (sw - gfx.measure(&st, S())) * 0.5, y + CH() * 6.0, S(), DIM);
let b = format!(" {} ", tr("end.new")); let b = format!(" {} ", tr("end.new"));
let b = b.as_str(); let b = b.as_str();
let bw = gfx.measure(b, S) + CW * 2.0; let bw = gfx.measure(b, S()) + CW() * 2.0;
let bx = (sw - bw) * 0.5; let bx = (sw - bw) * 0.5;
let by = y + CH * 7.75; let by = y + CH() * 7.75;
self.end_button = Rect::new(bx, by, bw, CH * 2.0); self.end_button = Rect::new(bx, by, bw, CH() * 2.0);
let (mx, my) = mouse_position(); let (mx, my) = mouse_position();
let hov = self.end_button.contains(vec2(mx, my)); let hov = self.end_button.contains(vec2(mx, my));
draw_rectangle(bx, by, bw, CH * 2.0, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) }); draw_rectangle(bx, by, bw, CH() * 2.0, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
draw_rectangle_lines(bx, by, bw, CH * 2.0, 2.0, col); draw_rectangle_lines(bx, by, bw, CH() * 2.0, 2.0, col);
gfx.text(b, bx + CW, by + CH * 0.5, S, TXT); gfx.text(b, bx + CW(), by + CH() * 0.5, S(), TXT);
self.rects.push(Rect::new(0.0, 0.0, sw, sh)); self.rects.push(Rect::new(0.0, 0.0, sw, sh));
} }
} }
+54 -5
View File
@@ -1,7 +1,9 @@
// DungeonCraft browser plugin for miniquad: localStorage access for the Rust code. // DungeonCraft browser plugin for miniquad: storage, file download/upload, save-on-leave.
(function () { (function () {
"use strict"; "use strict";
var pending = null; var pending = null; // value handed over by dc_storage_get
var file = null; // bytes of a file chosen via the file picker
var cache = {}; // key -> string, persisted to localStorage when the page is hidden/closed
function bytes(ptr, len) { function bytes(ptr, len) {
return new Uint8Array(wasm_memory.buffer, ptr, len); return new Uint8Array(wasm_memory.buffer, ptr, len);
@@ -9,23 +11,70 @@
function str(ptr, len) { function str(ptr, len) {
return new TextDecoder().decode(bytes(ptr, len)); return new TextDecoder().decode(bytes(ptr, len));
} }
function flushCache() {
for (var k in cache) {
try { window.localStorage.setItem(k, cache[k]); } catch (e) { }
}
}
window.addEventListener("pagehide", flushCache);
document.addEventListener("visibilitychange", function () {
if (document.visibilityState === "hidden") flushCache();
});
// hidden <input type=file> for loading savegames
var input = document.createElement("input");
input.type = "file";
input.accept = ".dcsave,.json,application/json";
input.style.display = "none";
input.addEventListener("change", function () {
var f = input.files && input.files[0];
if (!f) return;
f.arrayBuffer().then(function (buf) { file = new Uint8Array(buf); });
input.value = "";
});
document.addEventListener("DOMContentLoaded", function () { document.body.appendChild(input); });
function register(importObject) { function register(importObject) {
importObject.env.dc_storage_get = function (kp, kl) { var env = importObject.env;
env.dc_storage_get = function (kp, kl) {
var v = null; var v = null;
try { v = window.localStorage.getItem(str(kp, kl)); } catch (e) { } try { v = window.localStorage.getItem(str(kp, kl)); } catch (e) { }
if (v === null) { pending = null; return -1; } if (v === null) { pending = null; return -1; }
pending = new TextEncoder().encode(v); pending = new TextEncoder().encode(v);
return pending.length; return pending.length;
}; };
importObject.env.dc_storage_take = function (ptr) { env.dc_storage_take = function (ptr) {
if (pending === null) return; if (pending === null) return;
bytes(ptr, pending.length).set(pending); bytes(ptr, pending.length).set(pending);
pending = null; pending = null;
}; };
importObject.env.dc_storage_set = function (kp, kl, vp, vl) { env.dc_storage_set = function (kp, kl, vp, vl) {
try { window.localStorage.setItem(str(kp, kl), str(vp, vl)); } catch (e) { } try { window.localStorage.setItem(str(kp, kl), str(vp, vl)); } catch (e) { }
}; };
env.dc_cache_set = function (kp, kl, vp, vl) {
cache[str(kp, kl)] = str(vp, vl);
};
env.dc_download = function (np, nl, dp, dl) {
var blob = new Blob([bytes(dp, dl).slice()], { type: "application/json" });
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = str(np, nl);
document.body.appendChild(a);
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
};
env.dc_open_file = function () {
file = null;
input.click();
};
env.dc_file_poll = function () {
return file === null ? -1 : file.length;
};
env.dc_file_take = function (ptr) {
if (file === null) return;
bytes(ptr, file.length).set(file);
file = null;
};
} }
miniquad_add_plugin({ register_plugin: register, version: 1, name: "dungeoncraft" }); miniquad_add_plugin({ register_plugin: register, version: 1, name: "dungeoncraft" });