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:
1 parent
fbf6f223fc
commit
fb7dab3683
17 files changed
+1742
-345
No files matched your search
+13
-5
@@ -1,6 +1,7 @@
|
||||
// Game logic: units, AI, economy, hero waves.
|
||||
|
||||
use crate::map::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const CORE_HP: f32 = 2000.0;
|
||||
pub const GATE_HP: f32 = 3000.0;
|
||||
@@ -71,7 +72,7 @@ pub struct SfxEvent {
|
||||
pub const MANA_MAX: f32 = 400.0;
|
||||
pub const CARRY_MAX: i32 = 300;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum EK {
|
||||
Worker,
|
||||
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 {
|
||||
None,
|
||||
Dig(usize),
|
||||
@@ -129,7 +130,7 @@ pub enum Job {
|
||||
Rally,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Ent {
|
||||
pub id: u32,
|
||||
pub kind: EK,
|
||||
@@ -217,6 +218,7 @@ pub struct Fx {
|
||||
pub col: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Game {
|
||||
pub map: Map,
|
||||
pub ents: Vec<Ent>,
|
||||
@@ -231,7 +233,9 @@ pub struct Game {
|
||||
pub warned: bool,
|
||||
pub portal_timer: f32,
|
||||
pub payday: f32,
|
||||
#[serde(skip)]
|
||||
pub fx: Vec<Fx>,
|
||||
#[serde(skip)]
|
||||
pub msgs: Vec<Msg>,
|
||||
pub state: u8, // 0 running, 1 victory, 2 defeat
|
||||
pub paused: bool,
|
||||
@@ -241,14 +245,17 @@ pub struct Game {
|
||||
pub gate_hit_t: f32,
|
||||
pub gate_stage: u32, // defender thresholds triggered (0..3)
|
||||
pub flag: Option<usize>,
|
||||
#[serde(skip)]
|
||||
pub events: Vec<SfxEvent>,
|
||||
pub last_wave_value: f32,
|
||||
pub torches: Vec<Torch>,
|
||||
#[serde(skip)]
|
||||
pub hints: bool, // show hint messages (setting)
|
||||
hint_t: [f32; 16],
|
||||
}
|
||||
|
||||
// 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 wall: usize,
|
||||
pub dir: usize,
|
||||
@@ -315,6 +322,7 @@ impl Game {
|
||||
events: Vec::new(),
|
||||
last_wave_value: 0.0,
|
||||
torches: Vec::new(),
|
||||
hints: true,
|
||||
hint_t: [-1000.0; 16],
|
||||
};
|
||||
let (cx, cy) = g.map.core;
|
||||
@@ -391,7 +399,7 @@ impl Game {
|
||||
}
|
||||
|
||||
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.msg(key, vec![]);
|
||||
}
|
||||
|
||||
+452
-222
@@ -4,19 +4,28 @@ mod game;
|
||||
mod gfx;
|
||||
mod i18n;
|
||||
mod map;
|
||||
mod menu;
|
||||
mod platform;
|
||||
mod render3d;
|
||||
mod save;
|
||||
mod settings;
|
||||
mod sound;
|
||||
mod ui;
|
||||
|
||||
use game::Game;
|
||||
use game::{Game, Msg};
|
||||
use gfx::Gfx;
|
||||
use macroquad::prelude::*;
|
||||
use map::{idx, inb, MH, MW};
|
||||
use menu::{Menu, MenuAction, Page};
|
||||
use render3d::{Cam, Overlay, R3d};
|
||||
use settings::{bindable_keys, Action, Settings};
|
||||
use sound::SoundBank;
|
||||
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 {
|
||||
macroquad::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
|
||||
}
|
||||
|
||||
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)]
|
||||
async fn main() {
|
||||
let gfx = Gfx::new();
|
||||
let mut r3d = R3d::new();
|
||||
let mut sounds = SoundBank::new();
|
||||
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();
|
||||
// language: stored choice, otherwise ask on first start
|
||||
match platform::storage_get("dc.lang") {
|
||||
Some(code) if i18n::set_language(&code) => {}
|
||||
_ => ui.show_langsel = true,
|
||||
let mut menu = Menu::new();
|
||||
|
||||
// settings: stored in the browser; on the very first start the player picks a language
|
||||
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 autosave_t = 0.0f32;
|
||||
let mut cache_t = 0.0f32;
|
||||
|
||||
loop {
|
||||
let dt = get_frame_time().min(0.1);
|
||||
let now = get_time();
|
||||
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 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 ground = cam.ground_point(mx, my);
|
||||
let ground = app.cam.ground_point(mx, my);
|
||||
let (wx, wy) = (ground.x, ground.y);
|
||||
let keys = app.settings.clone();
|
||||
let key = |a: Action| keys.key(a);
|
||||
|
||||
// ---------------- keyboard
|
||||
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;
|
||||
}
|
||||
// ---------------- Esc: dialogs, menus, pause menu
|
||||
if is_key_pressed(KeyCode::Escape) {
|
||||
inp.ldown = None;
|
||||
}
|
||||
if (is_key_pressed(KeyCode::P) || is_key_pressed(KeyCode::Space)) && game.state == 0 {
|
||||
game.paused = !game.paused;
|
||||
}
|
||||
if is_key_pressed(KeyCode::G) {
|
||||
r3d.outlines = !r3d.outlines;
|
||||
}
|
||||
if is_key_pressed(KeyCode::M) {
|
||||
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;
|
||||
if ui.show_langsel {
|
||||
} else if ui.show_help {
|
||||
ui.show_help = false;
|
||||
} else if in_menu {
|
||||
menu.back();
|
||||
} else if inp.ldown.is_some() {
|
||||
inp.ldown = None;
|
||||
} else {
|
||||
menu.page = Some(Page::Pause);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- camera: pan
|
||||
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(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() {
|
||||
// ---------------- keyboard (only while playing)
|
||||
if playing {
|
||||
for i in 0..TOOLS.len() {
|
||||
if is_key_pressed(key(Action::Tool(i))) {
|
||||
ui.tool = TOOLS[i];
|
||||
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
|
||||
if is_mouse_button_pressed(MouseButton::Left) {
|
||||
match ui.click(mx, my, &game) {
|
||||
UiAction::SelectTool(t) => {
|
||||
ui.tool = t;
|
||||
game.sfx_ui(game::Sfx::Click);
|
||||
if dialog {
|
||||
match ui.click(mx, my, &app.game) {
|
||||
UiAction::SelectLanguage(code) => {
|
||||
app.settings.language = code;
|
||||
settings_changed = true;
|
||||
ui.show_langsel = false;
|
||||
}
|
||||
UiAction::CloseHelp => ui.show_help = false,
|
||||
_ => {}
|
||||
}
|
||||
UiAction::MinimapJump(x, y) => {
|
||||
cam.tx = x;
|
||||
cam.tz = y;
|
||||
inp.minimap_drag = true;
|
||||
} else if in_menu {
|
||||
match menu.click(mx, my, &mut app.settings) {
|
||||
MenuAction::SettingsChanged => settings_changed = 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,
|
||||
UiAction::NewGame => {
|
||||
seed = next_seed(seed);
|
||||
(game, cam) = new_game(seed);
|
||||
ui.show_help = false;
|
||||
}
|
||||
UiAction::SelectLanguage(code) => {
|
||||
i18n::set_language(&code);
|
||||
platform::storage_set("dc.lang", &code);
|
||||
ui.show_langsel = false;
|
||||
game.sfx_ui(game::Sfx::Click);
|
||||
}
|
||||
UiAction::Consumed => {}
|
||||
UiAction::None => {
|
||||
if game.state == 0 {
|
||||
match (ui.tool, hover_tile) {
|
||||
(Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t),
|
||||
(Tool::Hand, _) => {
|
||||
let j = r3d.pick_ent(&game, &cam, mx, my, true);
|
||||
game.pickup(j);
|
||||
} else {
|
||||
match ui.click(mx, my, &app.game) {
|
||||
UiAction::SelectTool(t) => {
|
||||
ui.tool = t;
|
||||
app.game.sfx_ui(game::Sfx::Click);
|
||||
}
|
||||
UiAction::MinimapJump(x, y) => {
|
||||
app.cam.tx = x;
|
||||
app.cam.tz = y;
|
||||
inp.minimap_drag = true;
|
||||
}
|
||||
UiAction::NewGame => {
|
||||
app.seed = next_seed(app.seed);
|
||||
(app.game, app.cam) = new_game(app.seed);
|
||||
app.game.hints = app.settings.hints;
|
||||
}
|
||||
UiAction::CloseHelp | UiAction::SelectLanguage(_) | UiAction::Consumed => {}
|
||||
UiAction::None => {
|
||||
if app.game.state == 0 {
|
||||
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 is_mouse_button_down(MouseButton::Left) && ui.minimap_contains(mx, my) {
|
||||
let (x, y) = ui.minimap_world(mx, my);
|
||||
cam.tx = x;
|
||||
cam.tz = y;
|
||||
app.cam.tx = x;
|
||||
app.cam.tz = y;
|
||||
} else if !is_mouse_button_down(MouseButton::Left) {
|
||||
inp.minimap_drag = false;
|
||||
}
|
||||
}
|
||||
if is_mouse_button_released(MouseButton::Left) {
|
||||
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 {
|
||||
Tool::Dig => {
|
||||
let on = !game.map.t[idx(sx, sy)].marked;
|
||||
game.mark_rect(sx, sy, ex, ey, on);
|
||||
let on = !app.game.map.t[idx(sx, sy)].marked;
|
||||
app.game.mark_rect(sx, sy, ex, ey, on);
|
||||
}
|
||||
Tool::Room(r) => game.build_rect(r, sx, sy, ex, ey),
|
||||
Tool::Sell => game.sell_rect(sx, sy, ex, ey),
|
||||
Tool::Room(r) => app.game.build_rect(r, sx, sy, ex, ey),
|
||||
Tool::Sell => app.game.sell_rect(sx, sy, ex, ey),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cam.tx = cam.tx.clamp(0.0, MW as f32);
|
||||
cam.tz = cam.tz.clamp(0.0, MH as f32);
|
||||
app.cam.tx = app.cam.tx.clamp(0.0, MW as f32);
|
||||
app.cam.tz = app.cam.tz.clamp(0.0, MH as f32);
|
||||
|
||||
// ---------------- update
|
||||
game.update(dt);
|
||||
let hear = cam.zoom * 1.2;
|
||||
for ev in game.events.drain(..) {
|
||||
sounds.play(ev, vec2(cam.tx, cam.tz), hear, now);
|
||||
if settings_changed {
|
||||
apply_settings(&app.settings, &mut r3d, &mut sounds);
|
||||
store_settings(&app.settings);
|
||||
app.game.hints = app.settings.hints;
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
// ---------------- draw
|
||||
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);
|
||||
|
||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &sounds, &inp, now);
|
||||
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
@@ -1,5 +1,6 @@
|
||||
// Map, tiles, generation and path finding.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BinaryHeap;
|
||||
|
||||
@@ -7,7 +8,7 @@ pub const MW: usize = 64;
|
||||
pub const MH: usize = 64;
|
||||
pub const N: usize = MW * MH;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum Kind {
|
||||
Rock,
|
||||
Earth,
|
||||
@@ -23,24 +24,34 @@ pub enum Kind {
|
||||
Gate,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum Owner {
|
||||
None,
|
||||
Player,
|
||||
Hero,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
// Short serde names keep savegames small (the map has 4096 tiles)
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct Tile {
|
||||
#[serde(rename = "k")]
|
||||
pub kind: Kind,
|
||||
#[serde(rename = "o")]
|
||||
pub owner: Owner,
|
||||
pub hp: f32, // dig resistance
|
||||
pub gold: i32, // gold left (gold vein)
|
||||
#[serde(rename = "h")]
|
||||
pub hp: f32, // dig resistance
|
||||
#[serde(rename = "g")]
|
||||
pub gold: i32, // gold left (gold vein)
|
||||
#[serde(rename = "m")]
|
||||
pub marked: bool, // marked for digging
|
||||
#[serde(rename = "r")]
|
||||
pub reserved: u32, // entity id + 1 working on this tile
|
||||
pub food: f32, // mushrooms in the mushroom garden
|
||||
pub bed: u32, // entity id + 1 sleeping here
|
||||
pub var: u8, // texture variant
|
||||
#[serde(rename = "f")]
|
||||
pub food: f32, // mushrooms in the mushroom garden
|
||||
#[serde(rename = "b")]
|
||||
pub bed: u32, // entity id + 1 sleeping here
|
||||
#[serde(rename = "v")]
|
||||
pub var: u8, // texture variant
|
||||
}
|
||||
|
||||
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 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);
|
||||
impl Rng {
|
||||
pub fn next(&mut self) -> u32 {
|
||||
@@ -95,6 +107,7 @@ impl Rng {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Map {
|
||||
pub t: Vec<Tile>,
|
||||
pub core: (i32, i32), // top-left corner of the core (3x3)
|
||||
|
||||
+434
@@ -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
@@ -1,5 +1,6 @@
|
||||
// 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")]
|
||||
mod imp {
|
||||
@@ -7,6 +8,11 @@ mod imp {
|
||||
fn dc_storage_get(key: *const u8, key_len: u32) -> i32;
|
||||
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_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> {
|
||||
@@ -24,6 +30,32 @@ mod imp {
|
||||
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) }
|
||||
}
|
||||
|
||||
// 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"))]
|
||||
@@ -39,6 +71,16 @@ mod imp {
|
||||
pub fn storage_set(key: &str, val: &str) {
|
||||
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::*;
|
||||
|
||||
+23
-6
@@ -8,6 +8,7 @@ use crate::map::*;
|
||||
use macroquad::miniquad::{BlendFactor, BlendState, BlendValue, Comparison, Equation, PipelineParams, UniformDesc, UniformType};
|
||||
use macroquad::models::{Mesh, Vertex};
|
||||
use macroquad::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const WALL_H: f32 = 0.95;
|
||||
const CHUNK: i32 = 16;
|
||||
@@ -15,11 +16,11 @@ const NCH: i32 = MW as i32 / CHUNK;
|
||||
pub const MAX_LIGHTS: usize = 24;
|
||||
const ATLAS_W: f32 = 256.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
|
||||
|
||||
// ------------------------------------------------------------------ camera
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Cam {
|
||||
pub tx: f32, // look-at target on the ground (world x)
|
||||
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()
|
||||
}
|
||||
|
||||
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 ov = 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],
|
||||
[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;
|
||||
build_grid(
|
||||
&mut mb,
|
||||
@@ -615,6 +616,8 @@ pub struct R3d {
|
||||
glow: Material,
|
||||
chunks: Vec<Chunk>,
|
||||
pub outlines: bool, // tile outlines on/off
|
||||
wall_sub: usize, // wall face subdivisions (detail)
|
||||
pub max_lights: usize,
|
||||
}
|
||||
|
||||
fn rgba(u: u32) -> Color {
|
||||
@@ -645,7 +648,21 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 {
|
||||
impl R3d {
|
||||
pub fn new() -> R3d {
|
||||
let chunks = (0..NCH * NCH).map(|_| Chunk { sig: 0, mesh: None, overlay: None, lines: None }).collect();
|
||||
R3d { lit: lit_material(), glow: glow_material(), chunks, 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) {
|
||||
@@ -658,7 +675,7 @@ impl R3d {
|
||||
let ch = &mut self.chunks[(cz * NCH + cx) as usize];
|
||||
if ch.sig != sig {
|
||||
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.overlay = o;
|
||||
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.truncate(MAX_LIGHTS);
|
||||
l.truncate(self.max_lights.min(MAX_LIGHTS));
|
||||
let mut pos = vec![Vec4::ZERO; MAX_LIGHTS];
|
||||
let mut colr = vec![Vec4::ZERO; MAX_LIGHTS];
|
||||
for (k, (_, p, c)) in l.iter().enumerate() {
|
||||
|
||||
+90
@@ -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
@@ -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
@@ -3,7 +3,25 @@
|
||||
// Events without a registered sound stay silent.
|
||||
|
||||
use crate::game::{Sfx, SfxEvent, SFX_ALL};
|
||||
#[cfg(feature = "audio")]
|
||||
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::*;
|
||||
|
||||
pub struct SoundBank {
|
||||
@@ -33,10 +51,6 @@ impl SoundBank {
|
||||
// procedurally generated sounds will be registered here
|
||||
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)
|
||||
fn min_gap(sfx: Sfx) -> f64 {
|
||||
match sfx {
|
||||
|
||||
@@ -6,12 +6,34 @@ use crate::gfx::*;
|
||||
use crate::i18n::{languages, tr, trf};
|
||||
use crate::map::*;
|
||||
use crate::render3d::{minimap_color, Cam};
|
||||
use crate::settings::{key_name, Action, Settings};
|
||||
use crate::sound::SoundBank;
|
||||
use macroquad::prelude::*;
|
||||
|
||||
pub const S: f32 = 2.0; // UI font scale
|
||||
pub const CW: f32 = GW * S; // character width in px
|
||||
pub const CH: f32 = GH * S; // character height in px
|
||||
// UI font scale (setting: 2 = normal, 3 = large); character cell size follows it
|
||||
thread_local! {
|
||||
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_HI: Color = Color::new(0.95, 0.8, 0.4, 1.0);
|
||||
@@ -51,7 +73,6 @@ pub const TOOLS: [Tool; 12] = [
|
||||
Tool::Summon,
|
||||
];
|
||||
|
||||
pub const TOOL_KEYS: [&str; 12] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "X", "V"];
|
||||
|
||||
pub struct ToolInfo {
|
||||
pub label: String,
|
||||
@@ -216,42 +237,43 @@ impl Ui {
|
||||
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();
|
||||
let keys: Vec<String> = (0..TOOLS.len()).map(|i| key_name(settings.key(Action::Tool(i)))).collect();
|
||||
let sw = screen_width();
|
||||
let sh = screen_height();
|
||||
let (mx, my) = mouse_position();
|
||||
|
||||
// ---------------- top bar
|
||||
let cols = (sw / CW).ceil() as i32;
|
||||
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));
|
||||
let y = CH;
|
||||
let mut x = CW * 2.0;
|
||||
let cols = (sw / CW()).ceil() as i32;
|
||||
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()));
|
||||
let y = CH();
|
||||
let mut x = CW() * 2.0;
|
||||
let seg = |label: &str, lc: Color, value: &str, vc: Color, x: &mut f32| {
|
||||
gfx.text(label, *x, y, S, lc);
|
||||
*x += gfx.measure(label, S) + CW;
|
||||
gfx.text(value, *x, y, S, vc);
|
||||
*x += gfx.measure(value, S) + CW * 2.0;
|
||||
gfx.text(label, *x, y, S(), lc);
|
||||
*x += gfx.measure(label, S()) + CW();
|
||||
gfx.text(value, *x, y, S(), vc);
|
||||
*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);
|
||||
let mana = tr("hud.mana");
|
||||
gfx.text(&mana, x, y, S, MANA);
|
||||
x += gfx.measure(&mana, S) + CW * 0.5;
|
||||
gfx.text(&mana, x, y, S(), MANA);
|
||||
x += gfx.measure(&mana, S()) + CW() * 0.5;
|
||||
bar(x, y + 3.0, 70.0, 10.0, g.mana / MANA_MAX, MANA);
|
||||
x += 70.0 + CW * 0.5;
|
||||
gfx.text(&format!("{}", g.mana as i32), x, y, S, TXT);
|
||||
x += CW * 5.0;
|
||||
x += 70.0 + CW() * 0.5;
|
||||
gfx.text(&format!("{}", g.mana as i32), x, y, S(), TXT);
|
||||
x += CW() * 5.0;
|
||||
let core = tr("hud.core");
|
||||
gfx.text(&core, x, y, S, RED);
|
||||
x += gfx.measure(&core, S) + CW * 0.5;
|
||||
gfx.text(&core, x, y, S(), RED);
|
||||
x += gfx.measure(&core, S()) + CW() * 0.5;
|
||||
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");
|
||||
gfx.text(&gate, x, y, S, BLUE);
|
||||
x += gfx.measure(&gate, S) + CW * 0.5;
|
||||
gfx.text(&gate, x, y, S(), BLUE);
|
||||
x += gfx.measure(&gate, S()) + CW() * 0.5;
|
||||
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);
|
||||
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);
|
||||
@@ -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(&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) };
|
||||
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
|
||||
let tb_rows = 5;
|
||||
let tb_y = sh - tb_rows as f32 * CH;
|
||||
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));
|
||||
let tb_y = sh - tb_rows as f32 * CH();
|
||||
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.tool_rects.clear();
|
||||
let bw = 80.0;
|
||||
let bw = 80.0 * K();
|
||||
let mut hover_tool = None;
|
||||
for (i, &t) in TOOLS.iter().enumerate() {
|
||||
let bx = CW + i as f32 * (bw + 4.0);
|
||||
let by = tb_y + CH * 0.75;
|
||||
let r = Rect::new(bx, by, bw, CH * 3.5);
|
||||
let bx = CW() + i as f32 * (bw + 4.0 * K());
|
||||
let by = tb_y + CH() * 0.75;
|
||||
let r = Rect::new(bx, by, bw, CH() * 3.5);
|
||||
self.tool_rects.push(r);
|
||||
let hovered = r.contains(vec2(mx, my));
|
||||
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_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);
|
||||
gfx.draw_cell(ti.icon, bx + (bw - 32.0) * 0.5, by + 4.0, 32.0, 32.0, WHITE);
|
||||
gfx.text(TOOL_KEYS[i], bx + 3.0, by + 3.0, 1.0, if sel { FRAME_HI } else { DIM });
|
||||
let lw = gfx.measure(&ti.label, 1.0);
|
||||
gfx.text(&ti.label, bx + (bw - lw) * 0.5, by + 42.0, 1.0, if sel { FRAME_HI } else { TXT });
|
||||
let icon = 32.0 * K();
|
||||
let small = (S() / 2.0).floor().max(1.0);
|
||||
gfx.draw_cell(ti.icon, bx + (bw - icon) * 0.5, by + 4.0 * K(), icon, icon, WHITE);
|
||||
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
|
||||
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 {
|
||||
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() {
|
||||
if let Some(e) = g.ents.iter().find(|e| e.id == *id) {
|
||||
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(
|
||||
&gfx.sprites,
|
||||
sx,
|
||||
@@ -305,18 +332,18 @@ impl Ui {
|
||||
}
|
||||
}
|
||||
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
|
||||
let mm_cols = 16;
|
||||
let mm_rows = 12;
|
||||
let mm_w = mm_cols as f32 * CW;
|
||||
let mm_h = mm_rows as f32 * CH;
|
||||
let mm_w = mm_cols as f32 * CW();
|
||||
let mm_h = mm_rows as f32 * CH();
|
||||
let mm_x = sw - mm_w;
|
||||
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));
|
||||
if now - self.minimap_t > 0.25 {
|
||||
self.minimap_t = now;
|
||||
@@ -330,9 +357,9 @@ impl Ui {
|
||||
}
|
||||
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 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);
|
||||
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;
|
||||
@@ -366,18 +393,18 @@ impl Ui {
|
||||
}
|
||||
}
|
||||
let irows = lines.len() as i32 + 2;
|
||||
let ix = sw - icols as f32 * CW;
|
||||
let iy = 3.0 * CH;
|
||||
gfx.window_titled(ix, iy, icols, irows, S, false, FRAME, BG, &info[0].0, info[0].1);
|
||||
let ix = sw - icols as f32 * CW();
|
||||
let iy = 3.0 * CH();
|
||||
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() {
|
||||
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)
|
||||
let recent: Vec<&(Msg, f64)> = self.log.iter().filter(|(_, t)| now - t < 14.0).collect();
|
||||
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();
|
||||
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);
|
||||
@@ -386,11 +413,11 @@ impl Ui {
|
||||
}
|
||||
}
|
||||
let mrows = lines.len() as i32 + 2;
|
||||
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));
|
||||
self.rects.push(Rect::new(0.0, my0, mcols as f32 * CW, 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));
|
||||
self.rects.push(Rect::new(0.0, my0, mcols as f32 * CW(), mrows as f32 * CH()));
|
||||
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 {
|
||||
@@ -407,17 +434,22 @@ impl Ui {
|
||||
if !cost.is_empty() {
|
||||
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 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;
|
||||
gfx.window_titled(tx, ty, tcols as i32, rows, S, false, FRAME_HI, BG, &ti.name, FRAME_HI);
|
||||
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;
|
||||
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() {
|
||||
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 {
|
||||
self.draw_langsel(gfx);
|
||||
} else if g.state != 0 {
|
||||
@@ -426,13 +458,13 @@ impl Ui {
|
||||
self.draw_help(gfx);
|
||||
} else if g.paused {
|
||||
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;
|
||||
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");
|
||||
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");
|
||||
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 sw = screen_width();
|
||||
let sh = screen_height();
|
||||
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), "DungeonCraft", FRAME_HI);
|
||||
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), "DungeonCraft", FRAME_HI);
|
||||
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));
|
||||
}
|
||||
@@ -481,28 +513,28 @@ impl Ui {
|
||||
let langs = languages();
|
||||
let cols = 50;
|
||||
let rows = 6 + langs.len() as i32 * 3;
|
||||
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 x = ((sw - cols as f32 * CW()) * 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
|
||||
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();
|
||||
let (mx, my) = mouse_position();
|
||||
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 by = y + CH * (2.0 + k as f32 * 3.0);
|
||||
let r = Rect::new(bx, by, bw, CH * 2.0);
|
||||
let by = y + CH() * (2.0 + k as f32 * 3.0);
|
||||
let r = Rect::new(bx, by, bw, CH() * 2.0);
|
||||
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_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()));
|
||||
}
|
||||
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() {
|
||||
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));
|
||||
}
|
||||
@@ -514,27 +546,27 @@ impl Ui {
|
||||
let win = g.state == 1;
|
||||
let cols = 44;
|
||||
let rows = 11;
|
||||
let x = (sw - cols as f32 * CW) * 0.5;
|
||||
let y = (sh - rows as f32 * CH) * 0.5;
|
||||
let x = (sw - cols as f32 * CW()) * 0.5;
|
||||
let y = (sh - rows as f32 * CH()) * 0.5;
|
||||
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" });
|
||||
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" });
|
||||
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()]);
|
||||
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 = 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 by = y + CH * 7.75;
|
||||
self.end_button = Rect::new(bx, by, bw, CH * 2.0);
|
||||
let by = y + CH() * 7.75;
|
||||
self.end_button = Rect::new(bx, by, bw, CH() * 2.0);
|
||||
let (mx, my) = mouse_position();
|
||||
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_lines(bx, by, bw, CH * 2.0, 2.0, col);
|
||||
gfx.text(b, bx + CW, by + CH * 0.5, S, TXT);
|
||||
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);
|
||||
gfx.text(b, bx + CW(), by + CH() * 0.5, S(), TXT);
|
||||
self.rects.push(Rect::new(0.0, 0.0, sw, sh));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user