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:
1 parent
91bb91b6ee
commit
220d9543cc
6 files changed
+180
-589
No files matched your search
+138
-93
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user