3D isometric renderer: chunked meshes with noise-displaced walls, lit shader with flickering torches, billboard sprites, rotatable/tiltable ortho camera, ray picking

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-02 22:42:10 +01:00
1 parent 91bb91b6ee
commit 220d9543cc
6 files changed
+180 -589

No files matched your search

+138 -93
View File
@@ -1,9 +1,9 @@
// DungeonCraft – Dungeon-Aufbau & Verteidigung im Browser (Rust + macroquad, WASM).
// DungeonCraft – dungeon building & defence in the browser (Rust + macroquad, WASM).
mod game;
mod gfx;
mod map;
mod render;
mod render3d;
mod sound;
mod ui;
@@ -11,62 +11,68 @@ use game::Game;
use gfx::Gfx;
use macroquad::prelude::*;
use map::{idx, inb, MH, MW};
use render::{draw_world, Overlay, View, ZOOMS};
use render3d::{Cam, Overlay, R3d};
use sound::SoundBank;
use ui::{hover_info, Tool, Ui, UiAction, TOOLS};
fn conf() -> Conf {
Conf { window_title: "DungeonCraft".into(), window_width: 1280, window_height: 800, high_dpi: false, ..Default::default() }
fn conf() -> macroquad::conf::Conf {
macroquad::conf::Conf {
miniquad_conf: miniquad::conf::Conf {
window_title: "DungeonCraft".into(),
window_width: 1280,
window_height: 800,
high_dpi: false,
..Default::default()
},
draw_call_vertex_capacity: 40000,
draw_call_index_capacity: 60000,
..Default::default()
}
}
struct Input {
ldown: Option<(i32, i32)>, // Start des Auswahlrechtecks
rdown: Option<(f32, f32)>, // Startpunkt rechte Maustaste
ldown: Option<(i32, i32)>, // start of the selection rectangle
rdown: Option<(f32, f32)>, // right button press position
rlast: (f32, f32),
rdrag: bool,
mlast: Option<(f32, f32)>, // middle button drag
minimap_drag: bool,
}
fn new_game(seed: u32) -> (Game, View) {
fn new_game(seed: u32) -> (Game, Cam) {
let g = Game::new(seed);
let (cx, cy) = g.map.core;
let zoom = 32.0;
let view = View { cam_x: cx as f32 + 1.5 - screen_width() / zoom * 0.5, cam_y: cy as f32 + 1.5 - screen_height() / zoom * 0.5, zoom };
(g, view)
let cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
(g, cam)
}
fn clamp_cam(v: &mut View) {
let vw = screen_width() / v.zoom;
let vh = screen_height() / v.zoom;
v.cam_x = v.cam_x.clamp(-vw * 0.5, MW as f32 - vw * 0.5);
v.cam_y = v.cam_y.clamp(-vh * 0.5, MH as f32 - vh * 0.5);
}
fn center_on(v: &mut View, wx: f32, wy: f32) {
v.cam_x = wx - screen_width() / v.zoom * 0.5;
v.cam_y = wy - screen_height() / v.zoom * 0.5;
fn next_seed(seed: u32) -> u32 {
seed.wrapping_mul(1103515245).wrapping_add(12345) | 1
}
#[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 view) = new_game(seed);
let (mut game, mut cam) = new_game(seed);
let mut ui = Ui::new();
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, minimap_drag: false };
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false };
loop {
let dt = get_frame_time();
let dt = get_frame_time().min(0.1);
let now = get_time();
let (mx, my) = mouse_position();
let over_ui = ui.over_ui(mx, my);
let (wx, wy) = view.world(mx, my);
let hover_tile = (wx.floor() as i32, wy.floor() as i32);
let picked = cam.pick_tile(&game.map, mx, my);
let hover_tile = picked.map(|(x, z, _)| (x, z));
let ground = cam.ground_point(mx, my);
let (wx, wy) = (ground.x, ground.y);
// ---------------- Tastatur
for (i, k) in [
// ---------------- keyboard
let tool_keys = [
KeyCode::Key1,
KeyCode::Key2,
KeyCode::Key3,
@@ -77,12 +83,10 @@ async fn main() {
KeyCode::Key8,
KeyCode::Key9,
KeyCode::Key0,
KeyCode::Q,
KeyCode::E,
]
.iter()
.enumerate()
{
KeyCode::X,
KeyCode::V,
];
for (i, k) in tool_keys.iter().enumerate() {
if is_key_pressed(*k) {
ui.tool = TOOLS[i];
inp.ldown = None;
@@ -107,51 +111,78 @@ async fn main() {
sounds.change_volume(-0.1);
}
if is_key_pressed(KeyCode::N) && game.state != 0 {
seed = seed.wrapping_mul(1103515245).wrapping_add(12345) | 1;
(game, view) = new_game(seed);
seed = next_seed(seed);
(game, cam) = new_game(seed);
ui.show_help = false;
}
// ---------------- Kamera
let pan = 900.0 * dt / view.zoom;
// ---------------- 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) {
view.cam_y -= pan;
move_cam(0.0, pan, &mut cam);
}
if is_key_down(KeyCode::S) || is_key_down(KeyCode::Down) {
view.cam_y += pan;
move_cam(0.0, -pan, &mut cam);
}
if is_key_down(KeyCode::A) || is_key_down(KeyCode::Left) {
view.cam_x -= pan;
move_cam(-pan, 0.0, &mut cam);
}
if is_key_down(KeyCode::D) || is_key_down(KeyCode::Right) {
view.cam_x += pan;
move_cam(pan, 0.0, &mut cam);
}
// Bildschirmrand
let edge = 6.0;
if !ui.show_help && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
if mx < edge {
view.cam_x -= pan;
move_cam(-pan, 0.0, &mut cam);
} else if mx > screen_width() - edge {
view.cam_x += pan;
move_cam(pan, 0.0, &mut cam);
}
if my < edge {
view.cam_y -= pan;
move_cam(0.0, pan, &mut cam);
} else if my > screen_height() - edge {
view.cam_y += pan;
move_cam(0.0, -pan, &mut cam);
}
}
// Zoom
// ---------------- 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 zi = ZOOMS.iter().position(|&z| z >= view.zoom).unwrap_or(2) as i32;
let ni = (zi + if wheel > 0.0 { 1 } else { -1 }).clamp(0, ZOOMS.len() as i32 - 1) as usize;
if ZOOMS[ni] != view.zoom {
view.zoom = ZOOMS[ni];
view.cam_x = wx - mx / view.zoom;
view.cam_y = wy - my / view.zoom;
}
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;
}
// Rechte Maustaste: ziehen = verschieben, klicken = absetzen
// ---------------- right button: drag = pan, click = drop
if is_mouse_button_pressed(MouseButton::Right) {
inp.rdown = Some((mx, my));
inp.rlast = (mx, my);
@@ -163,8 +194,10 @@ async fn main() {
inp.rdrag = true;
}
if inp.rdrag {
view.cam_x -= (mx - inp.rlast.0) / view.zoom;
view.cam_y -= (my - inp.rlast.1) / view.zoom;
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);
}
@@ -180,7 +213,7 @@ async fn main() {
inp.rdown = None;
}
// ---------------- Linke Maustaste
// ---------------- left button
if is_mouse_button_pressed(MouseButton::Left) {
match ui.click(mx, my, &game) {
UiAction::SelectTool(t) => {
@@ -188,27 +221,30 @@ async fn main() {
game.sfx_ui(game::Sfx::Click);
}
UiAction::MinimapJump(x, y) => {
center_on(&mut view, x, y);
cam.tx = x;
cam.tz = y;
inp.minimap_drag = true;
}
UiAction::CloseHelp => ui.show_help = false,
UiAction::NewGame => {
seed = seed.wrapping_mul(1103515245).wrapping_add(12345) | 1;
(game, view) = new_game(seed);
seed = next_seed(seed);
(game, cam) = new_game(seed);
ui.show_help = false;
}
UiAction::Consumed => {}
UiAction::None => {
if game.state == 0 && inb(hover_tile.0, hover_tile.1) {
match ui.tool {
Tool::Dig | Tool::Room(_) | Tool::Sell => inp.ldown = Some(hover_tile),
Tool::Hand => {
game.pickup(wx, wy);
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);
}
Tool::Flag => game.set_flag(hover_tile.0, hover_tile.1),
Tool::Lightning => game.lightning(wx, wy),
Tool::Heal => game.heal(wx, wy),
Tool::Summon => 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),
_ => {}
}
}
}
@@ -217,39 +253,43 @@ 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);
center_on(&mut view, x, y);
cam.tx = x;
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() {
let (ex, ey) = (hover_tile.0.clamp(0, MW as i32 - 1), hover_tile.1.clamp(0, MH as i32 - 1));
match ui.tool {
Tool::Dig => {
let on = !game.map.t[idx(sx, sy)].marked;
game.mark_rect(sx, sy, ex, ey, on);
if let Some((ex, ey)) = hover_tile {
match ui.tool {
Tool::Dig => {
let on = !game.map.t[idx(sx, sy)].marked;
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) => game.build_rect(r, sx, sy, ex, ey),
Tool::Sell => game.sell_rect(sx, sy, ex, ey),
_ => {}
}
}
}
clamp_cam(&mut view);
cam.tx = cam.tx.clamp(0.0, MW as f32);
cam.tz = cam.tz.clamp(0.0, MH as f32);
// ---------------- Update
// ---------------- update
game.update(dt);
let hear = cam.zoom * 1.2;
for ev in game.events.drain(..) {
sounds.play(ev, &view, now);
sounds.play(ev, vec2(cam.tx, cam.tz), hear, now);
}
for m in game.msgs.drain(..) {
ui.log.push((m, now));
}
// ---------------- Zeichnen
clear_background(Color::from_rgba(8, 6, 8, 255));
let (hover_color, radius) = match ui.tool {
// ---------------- 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),
@@ -260,15 +300,20 @@ async fn main() {
Tool::Summon => (Color::new(0.8, 0.5, 1.0, 1.0), None),
};
let ov = Overlay {
hover: if over_ui || !inb(hover_tile.0, hover_tile.1) { None } else { Some(hover_tile) },
hover_color,
rect: inp.ldown.map(|(sx, sy)| (sx, sy, hover_tile.0.clamp(0, MW as i32 - 1), hover_tile.1.clamp(0, MH as i32 - 1))),
rect_color: hover_color,
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,
};
draw_world(&game, &gfx, &view, now, &ov);
let info = if over_ui { Vec::new() } else { hover_info(&game, wx, wy) };
ui.draw(&game, &gfx, &view, &sounds, &info, now);
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
}