From 220d9543cc91c4d1f83617c68d65950cb0aa47b0 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Mon, 2 Nov 2026 22:42:10 +0100 Subject: [PATCH] 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 --- src/game.rs | 16 -- src/main.rs | 231 ++++++++++++++---------- src/render.rs | 455 ------------------------------------------------ src/render3d.rs | 31 +++- src/sound.rs | 8 +- src/ui.rs | 28 +-- 6 files changed, 180 insertions(+), 589 deletions(-) delete mode 100644 src/render.rs diff --git a/src/game.rs b/src/game.rs index aa8a62c..be1981f 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1683,22 +1683,6 @@ impl Game { } } - pub fn ent_at(&self, wx: f32, wy: f32, player_only: bool) -> Option { - let mut best = None; - let mut bd = 0.45f32; - for (j, e) in self.ents.iter().enumerate() { - if !e.alive || e.held || (player_only && e.hero) { - continue; - } - let d = (e.x - wx).powi(2) + (e.y - 0.25 - wy).powi(2); - if d < bd { - bd = d; - best = Some(j); - } - } - best - } - pub fn pickup(&mut self, target: Option) -> bool { if self.hand.len() >= 8 { self.hint(8, "Deine Hand ist voll (max. 8). Rechtsklick lässt Einheiten fallen."); diff --git a/src/main.rs b/src/main.rs index e17eaeb..c4c6445 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 } diff --git a/src/render.rs b/src/render.rs deleted file mode 100644 index 0225263..0000000 --- a/src/render.rs +++ /dev/null @@ -1,455 +0,0 @@ -// Zeichnet die Spielwelt: Kacheln, Wände, Räume, Kern/Portal/Tor, Einheiten, Effekte. - -use crate::game::*; -use crate::gfx::*; -use crate::map::*; -use macroquad::prelude::*; - -pub const ZOOMS: [f32; 6] = [16.0, 24.0, 32.0, 40.0, 48.0, 64.0]; - -pub struct View { - pub cam_x: f32, // Weltkoordinate (Kacheln) am linken Bildschirmrand - pub cam_y: f32, - pub zoom: f32, // Pixel pro Kachel -} - -impl View { - pub fn sx(&self, wx: f32) -> f32 { - ((wx - self.cam_x) * self.zoom).floor() - } - pub fn sy(&self, wy: f32) -> f32 { - ((wy - self.cam_y) * self.zoom).floor() - } - pub fn world(&self, sx: f32, sy: f32) -> (f32, f32) { - (sx / self.zoom + self.cam_x, sy / self.zoom + self.cam_y) - } -} - -// Overlay-Infos aus der Eingabe (Hover, Auswahlrechteck, Zauberradius) -pub struct Overlay { - pub hover: Option<(i32, i32)>, - pub hover_color: Color, - pub rect: Option<(i32, i32, i32, i32)>, - pub rect_color: Color, - pub radius: Option<(f32, f32, f32)>, // Weltpos + Radius -} - -pub fn rgba(u: u32) -> Color { - Color::from_rgba(u as u8, (u >> 8) as u8, (u >> 16) as u8, (u >> 24) as u8) -} - -fn with_a(c: Color, a: f32) -> Color { - Color::new(c.r, c.g, c.b, a) -} - -fn open(t: Option<&Tile>) -> bool { - match t { - Some(t) => !t.solid() && t.kind != Kind::Core, - None => false, - } -} - -fn solid(t: Option<&Tile>) -> bool { - t.map_or(true, |t| t.solid()) -} - -pub fn draw_world(g: &Game, gfx: &Gfx, v: &View, time: f64, ov: &Overlay) { - let z = v.zoom; - let t = time as f32; - let x0 = (v.cam_x.floor() as i32 - 1).max(0); - let y0 = (v.cam_y.floor() as i32 - 1).max(0); - let x1 = ((v.cam_x + screen_width() / z).ceil() as i32 + 1).min(MW as i32 - 1); - let y1 = ((v.cam_y + screen_height() / z).ceil() as i32 + 1).min(MH as i32 - 1); - let m = &g.map; - let gold_fill = (g.gold / g.gold_cap()).clamp(0.0, 1.0); - - // --- Kacheln - for y in y0..=y1 { - for x in x0..=x1 { - let tile = &m.t[idx(x, y)]; - let px = v.sx(x as f32); - let py = v.sy(y as f32); - let tex = Gfx::tile_tex(tile.kind, tile.owner == Owner::Player, tile.owner == Owner::Hero, tile.var); - gfx.draw_cell(tex, px, py, z, z, WHITE); - - if tile.solid() { - // Wandfront, wenn unten offen - if open(m.get(x, y + 1)) { - let face = match tile.kind { - Kind::Rock => T_FACE_ROCK, - Kind::Gold => T_FACE_GOLD, - _ => T_FACE_EARTH, - }; - let fh = (z * 0.3).floor(); - gfx.draw_cell(face, px, py + z - fh, z, fh, WHITE); - draw_rectangle(px, py + z - fh, z, (z / 16.0).max(1.0), Color::new(0.0, 0.0, 0.0, 0.35)); - } - if open(m.get(x, y - 1)) { - draw_rectangle(px, py, z, (z / 16.0).max(1.0), Color::new(1.0, 1.0, 1.0, 0.14)); - } - if open(m.get(x - 1, y)) { - draw_rectangle(px, py, (z / 16.0).max(1.0), z, Color::new(0.0, 0.0, 0.0, 0.3)); - } - if open(m.get(x + 1, y)) { - draw_rectangle(px + z - (z / 16.0).max(1.0), py, (z / 16.0).max(1.0), z, Color::new(0.0, 0.0, 0.0, 0.3)); - } - if tile.marked { - let a = 0.75 + 0.25 * (t * 4.0).sin(); - gfx.draw_cell(T_MARK, px, py, z, z, with_a(WHITE, a)); - } - } else { - // Schatten von angrenzenden Wänden - if solid(m.get(x, y - 1)) { - draw_rectangle(px, py, z, z * 0.28, Color::new(0.0, 0.0, 0.0, 0.22)); - draw_rectangle(px, py, z, z * 0.12, Color::new(0.0, 0.0, 0.0, 0.25)); - } - if solid(m.get(x - 1, y)) { - draw_rectangle(px, py, z * 0.14, z, Color::new(0.0, 0.0, 0.0, 0.22)); - } - if solid(m.get(x + 1, y)) { - draw_rectangle(px + z * 0.9, py, z * 0.1, z, Color::new(0.0, 0.0, 0.0, 0.18)); - } - // Raumdeko - match tile.kind { - Kind::Treasury if gold_fill > 0.02 => { - let s = z * (0.35 + 0.6 * gold_fill); - gfx.draw_cell(D_COINS, px + (z - s) * 0.5, py + (z - s) * 0.7, s, s, WHITE); - } - Kind::Lair => { - gfx.draw_cell(D_BED, px + z * 0.05, py + z * 0.05, z * 0.9, z * 0.9, WHITE); - } - Kind::Farm => { - let pos = [(0.05, 0.1), (0.45, 0.05), (0.25, 0.45)]; - for (k, &(ox, oy)) in pos.iter().enumerate() { - if tile.food >= k as f32 + 1.0 { - gfx.draw_cell(D_MUSHROOM, px + z * ox, py + z * oy, z * 0.55, z * 0.55, WHITE); - } - } - } - Kind::Training if (x + y) % 2 == 0 => { - gfx.draw_cell(D_DUMMY, px + z * 0.1, py, z * 0.8, z * 0.8, WHITE); - } - Kind::Library if y % 2 == 0 => { - gfx.draw_cell(D_SHELF, px, py, z, z, WHITE); - } - _ => {} - } - } - } - } - - draw_core(g, v, t); - draw_portal(g, v, t); - draw_gate(g, gfx, v, t); - - // --- Angriffsflagge - if let Some(f) = g.flag { - let (fx, fy) = xy(f); - let bx = v.sx(fx as f32 + 0.35); - let by = v.sy(fy as f32 + 0.9); - let top = v.sy(fy as f32 + 0.05); - draw_line(bx, by, bx, top, (z / 12.0).max(2.0), Color::from_rgba(200, 200, 200, 255)); - let wave = (t * 6.0).sin() * z * 0.06; - let p1 = vec2(bx, top); - let p2 = vec2(bx + z * 0.55, top + z * 0.15 + wave); - let p3 = vec2(bx, top + z * 0.35); - draw_triangle(p1, p2, p3, Color::from_rgba(210, 40, 40, 255)); - draw_triangle_lines(p1, p2, p3, 1.0, Color::from_rgba(90, 10, 10, 255)); - } - - // --- Einheiten (nach y sortiert) - let mut order: Vec = (0..g.ents.len()) - .filter(|&i| { - let e = &g.ents[i]; - e.alive && !e.held && e.x >= x0 as f32 - 1.0 && e.x <= x1 as f32 + 2.0 && e.y >= y0 as f32 - 1.0 && e.y <= y1 as f32 + 2.0 - }) - .collect(); - order.sort_by(|&a, &b| g.ents[a].y.partial_cmp(&g.ents[b].y).unwrap_or(std::cmp::Ordering::Equal)); - for i in order { - draw_ent(&g.ents[i], gfx, v, t); - } - - // --- Effekte - for f in &g.fx { - draw_fx(f, v, t); - } - - // --- Overlay - if let Some((x0, y0, x1, y1)) = ov.rect { - let (ax, bx) = (x0.min(x1), x0.max(x1)); - let (ay, by) = (y0.min(y1), y0.max(y1)); - let px = v.sx(ax as f32); - let py = v.sy(ay as f32); - let w = (bx - ax + 1) as f32 * z; - let h = (by - ay + 1) as f32 * z; - draw_rectangle(px, py, w, h, with_a(ov.rect_color, 0.18)); - draw_rectangle_lines(px, py, w, h, 2.0, ov.rect_color); - } else if let Some((hx, hy)) = ov.hover { - draw_rectangle_lines(v.sx(hx as f32), v.sy(hy as f32), z, z, 2.0, ov.hover_color); - } - if let Some((wx, wy, r)) = ov.radius { - draw_circle_lines(v.sx(wx), v.sy(wy), r * z, 2.0, with_a(ov.hover_color, 0.8)); - } -} - -fn draw_core(g: &Game, v: &View, t: f32) { - let (cx, cy) = g.map.core; - let x = v.sx(cx as f32 + 1.5); - let y = v.sy(cy as f32 + 1.5); - let z = v.zoom; - let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0); - let pulse = 1.0 + (t * (2.0 + (1.0 - hp) * 6.0)).sin() * 0.05; - let r = z * 1.15 * pulse; - // Sockel - draw_circle(x, y + z * 0.15, z * 1.35, Color::from_rgba(30, 10, 14, 255)); - draw_circle_lines(x, y + z * 0.15, z * 1.35, (z / 10.0).max(2.0), Color::from_rgba(90, 40, 40, 255)); - for k in 0..6 { - let f = k as f32 / 6.0; - let rr = r * (1.0 - f * 0.8); - let col = Color::new(0.35 + 0.65 * f * hp + 0.2, 0.05 + 0.5 * f * f * hp, 0.08 + 0.1 * f, 0.9); - draw_circle(x, y - f * z * 0.15, rr, col); - } - // Glanzpunkt - draw_circle(x - r * 0.3, y - r * 0.4, r * 0.15, Color::new(1.0, 0.9, 0.8, 0.6)); - // Risse bei Schaden - if hp < 0.6 { - let n = ((1.0 - hp) * 8.0) as i32; - for k in 0..n { - let a = k as f32 * 2.39; - let l = r * (0.5 + 0.4 * ((k * 7 % 5) as f32 / 5.0)); - draw_line(x, y, x + a.cos() * l, y + a.sin() * l, 2.0, Color::new(0.1, 0.0, 0.0, 0.8)); - } - } -} - -fn draw_portal(g: &Game, v: &View, t: f32) { - let (px, py) = g.map.portal; - let x = v.sx(px as f32 + 1.5); - let y = v.sy(py as f32 + 1.5); - let z = v.zoom; - let owned = g.portal_owned(); - let (base, arm) = if owned { - (Color::from_rgba(40, 10, 60, 255), Color::from_rgba(200, 90, 255, 255)) - } else { - (Color::from_rgba(30, 28, 36, 255), Color::from_rgba(110, 100, 120, 255)) - }; - draw_circle(x, y, z * 1.35, base); - let speed = if owned { 1.6 } else { 0.3 }; - for a in 0..3 { - for k in 0..28 { - let f = k as f32 / 28.0; - let ang = t * speed + a as f32 * 2.094 + f * 4.0; - let r = f * z * 1.3; - draw_circle(x + ang.cos() * r, y + ang.sin() * r, z * (0.04 + 0.07 * f), with_a(arm, 0.3 + 0.7 * (1.0 - f))); - } - } - draw_circle_lines(x, y, z * 1.35, (z / 10.0).max(2.0), with_a(arm, 0.7)); -} - -fn draw_gate(g: &Game, gfx: &Gfx, v: &View, t: f32) { - let (gx, gy) = g.gate_center(); - let x = v.sx(gx); - let y = v.sy(gy); - let z = v.zoom; - let hp = (g.gate_hp / GATE_HP).clamp(0.0, 1.0); - if hp <= 0.0 { - for k in 0..12 { - let a = k as f32 * 1.7; - draw_circle(x + a.cos() * z * 0.9, y + a.sin() * z * 0.7, z * 0.2, Color::from_rgba(120, 120, 130, 255)); - } - return; - } - draw_circle(x, y, z * 1.4, Color::from_rgba(150, 160, 190, 255)); - draw_circle(x, y, z * 1.15, Color::from_rgba(30, 40, 70, 255)); - // Lichtstrahlen - for k in 0..10 { - let a = t * 0.5 + k as f32 * 0.628; - let r0 = z * 0.25; - let r1 = z * (0.8 + 0.25 * ((t * 2.0 + k as f32).sin())); - draw_line(x + a.cos() * r0, y + a.sin() * r0, x + a.cos() * r1, y + a.sin() * r1, (z / 12.0).max(2.0), Color::new(0.8, 0.9, 1.0, 0.5 * hp + 0.2)); - } - draw_circle(x, y, z * (0.35 + 0.05 * (t * 3.0).sin()), Color::new(0.95, 0.97, 1.0, 0.9)); - draw_circle_lines(x, y, z * 1.4, (z / 10.0).max(2.0), Color::from_rgba(230, 210, 120, 255)); - // HP-Balken - if hp < 1.0 { - let w = z * 3.0; - let bx = x - w * 0.5; - let by = y - z * 1.9; - draw_rectangle(bx - 1.0, by - 1.0, w + 2.0, 8.0, BLACK); - draw_rectangle(bx, by, w * hp, 6.0, Color::from_rgba(120, 180, 255, 255)); - gfx.text_sh("Heldentor", bx, by - 10.0, 1.0, WHITE); - } -} - -fn draw_ent(e: &Ent, gfx: &Gfx, v: &View, t: f32) { - let z = v.zoom; - let big = matches!(e.kind, EK::Ogre | EK::Paladin); - let sc = z / 16.0 * if big { 1.25 } else { 1.0 }; - let w = 10.0 * sc; - let h = 10.0 * sc; - let cx = v.sx(e.x); - let foot = v.sy(e.y + 0.32); - let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving; - let bob = if e.moving { -((e.anim * 12.0).sin().abs()) * sc * 1.2 } else { 0.0 }; - let x = cx - w * 0.5; - let y = foot - h + bob; - - // Schatten + Teamring - draw_ellipse(cx, foot, w * 0.42, w * 0.16, 0.0, Color::new(0.0, 0.0, 0.0, 0.35)); - let ring = if e.guard { - Color::new(1.0, 0.85, 0.3, 0.8) - } else if e.hero { - Color::new(0.5, 0.8, 1.0, 0.7) - } else { - Color::new(0.9, 0.25, 0.3, 0.6) - }; - draw_ellipse_lines(cx, foot, w * 0.45, w * 0.18, 0.0, 1.5, ring); - - let rot = if e.working && !sleeping { (t * 14.0 + e.anim).sin() * 0.15 } else { 0.0 }; - let tint = if sleeping { Color::new(0.55, 0.55, 0.7, 1.0) } else { WHITE }; - let params = |white: bool| DrawTextureParams { - dest_size: Some(vec2(w, h)), - source: Some(Gfx::sprite_rect(e.kind, white)), - flip_x: e.face < 0.0, - rotation: rot, - ..Default::default() - }; - draw_texture_ex(&gfx.sprites, x, y, tint, params(false)); - if e.flash > 0.0 { - draw_texture_ex(&gfx.sprites, x, y, Color::new(1.0, 1.0, 1.0, (e.flash / 0.15).min(1.0)), params(true)); - } - if sleeping { - let k = (t * 1.5 + e.anim) % 1.0; - gfx.glyph(b'z', cx + w * 0.3, y - k * z * 0.4, (z / 32.0).max(1.0).round(), Color::new(0.8, 0.85, 1.0, 1.0 - k)); - } - if e.carry > 0 { - let r = (sc * 1.5).max(2.0); - draw_circle(cx + w * 0.45, foot - h * 0.35, r, Color::from_rgba(250, 210, 60, 255)); - draw_circle_lines(cx + w * 0.45, foot - h * 0.35, r, 1.0, Color::from_rgba(120, 80, 10, 255)); - } - - // HP-Balken - if e.hp < e.max_hp || e.hero { - let bw = w.max(12.0); - let bx = cx - bw * 0.5; - let by = y - 5.0; - let f = (e.hp / e.max_hp).clamp(0.0, 1.0); - let col = if e.hero { - Color::from_rgba(120, 180, 255, 255) - } else if f > 0.5 { - Color::from_rgba(90, 220, 90, 255) - } else if f > 0.25 { - Color::from_rgba(240, 200, 60, 255) - } else { - Color::from_rgba(230, 60, 50, 255) - }; - draw_rectangle(bx - 1.0, by - 1.0, bw + 2.0, 4.0, Color::new(0.0, 0.0, 0.0, 0.8)); - draw_rectangle(bx, by, bw * f, 2.0, col); - } - if e.level > 1 && z >= 24.0 { - gfx.text_sh(&e.level.to_string(), cx - w * 0.5 - 7.0, y - 7.0, 1.0, Color::from_rgba(255, 220, 90, 255)); - } -} - -fn draw_fx(f: &Fx, v: &View, t: f32) { - let z = v.zoom; - let p = (f.t / f.max).clamp(0.0, 1.0); - let col = rgba(f.col); - let x = v.sx(f.x); - let y = v.sy(f.y); - match f.kind { - 0 => { - let x2 = v.sx(f.x2); - let y2 = v.sy(f.y2); - let hx = x + (x2 - x) * p; - let hy = y + (y2 - y) * p; - let q = (p - 0.35).max(0.0); - let tx = x + (x2 - x) * q; - let ty = y + (y2 - y) * q; - draw_line(tx, ty, hx, hy, (z / 12.0).max(2.0), with_a(col, 0.6)); - draw_circle(hx, hy, (z * 0.1).max(2.0), col); - } - 1 => { - let a = 1.0 - p; - let mut px = x; - let mut py = y - z * 7.0; - let steps = 9; - for k in 1..=steps { - let ny = y - z * 7.0 + z * 7.0 * k as f32 / steps as f32; - let jitter = if k == steps { 0.0 } else { ((t * 50.0 + k as f32 * 13.7).sin()) * z * 0.6 }; - let nx = x + jitter; - draw_line(px, py, nx, ny, (z / 8.0).max(2.0), Color::new(1.0, 1.0, 0.85, a)); - px = nx; - py = ny; - } - draw_circle(x, y, z * 1.3 * (0.5 + p), Color::new(1.0, 1.0, 0.7, 0.35 * a)); - } - 2 => { - let r = z * (0.1 + 0.3 * p); - for k in 0..4 { - let a = k as f32 * 1.571 + 0.785; - draw_line(x + a.cos() * r * 0.4, y + a.sin() * r * 0.4, x + a.cos() * r, y + a.sin() * r, 2.0, with_a(col, 1.0 - p)); - } - } - 3 => { - draw_circle(x, y, z * (0.2 + 0.6 * p), with_a(col, 0.45 * (1.0 - p))); - } - 4 => { - for k in 0..3 { - let ox = (k as f32 - 1.0) * z * 0.25; - let oy = -p * z * 0.8 - k as f32 * z * 0.1; - let s = (z * 0.08).max(1.5); - let c = with_a(col, 1.0 - p); - draw_rectangle(x + ox - s * 1.5, y + oy - s * 0.5, s * 3.0, s, c); - draw_rectangle(x + ox - s * 0.5, y + oy - s * 1.5, s, s * 3.0, c); - } - } - 5 => { - for k in 0..4 { - let a = k as f32 * 1.9 + f.x * 3.0; - let ox = a.cos() * z * 0.3 * p; - let oy = -p * z * 0.7 + a.sin() * z * 0.1; - let s = (z * 0.09).max(2.0); - draw_rectangle(x + ox, y + oy, s, s, with_a(col, 1.0 - p)); - } - } - 6 => { - for k in 0..4 { - let a = k as f32 * 1.7 + f.y * 5.0; - let ox = a.cos() * z * 0.35 * p; - let oy = a.sin().abs() * z * 0.2 * p + p * z * 0.2; - let s = (z * 0.08).max(2.0); - draw_rectangle(x + ox, y + oy, s, s, with_a(col, 1.0 - p)); - } - } - 7 => { - draw_circle_lines(x, y, z * (0.3 + 1.2 * p), 2.0, with_a(col, 1.0 - p)); - } - 8 => { - draw_rectangle(x - z * 0.5, y - z * 0.5, z, z, with_a(col, 0.45 * (1.0 - p))); - } - _ => {} - } -} - -// Minikarten-Farbe einer Kachel -pub fn minimap_color(tile: &Tile) -> [u8; 4] { - match tile.kind { - Kind::Rock => [40, 40, 48, 255], - Kind::Earth => [86, 60, 40, 255], - Kind::Gold => [220, 180, 50, 255], - Kind::Floor => match tile.owner { - Owner::Player => [140, 60, 80, 255], - Owner::Hero => [170, 175, 190, 255], - Owner::None => [70, 64, 60, 255], - }, - Kind::Treasury => [200, 160, 60, 255], - Kind::Lair => [150, 110, 60, 255], - Kind::Farm => [70, 140, 60, 255], - Kind::Training => [160, 100, 60, 255], - Kind::Library => [150, 40, 60, 255], - Kind::Core => [255, 60, 60, 255], - Kind::Portal => [190, 90, 240, 255], - Kind::Gate => [230, 230, 255, 255], - } -} diff --git a/src/render3d.rs b/src/render3d.rs index e2edafe..20c7f97 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -446,7 +446,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: [Vec3::Y; 4], ); let n = WALL_SUB; - let top_amb = amb_at(amb, xf + 0.5, zf + 0.5) * 0.85 + 0.05; + let top_amb = amb_at(amb, xf + 0.5, zf + 0.5) * 0.95 + 0.1; build_grid( &mut mb, |u, v| vec3(xf + u, WALL_H, zf + v), @@ -690,6 +690,7 @@ impl R3d { let t = time as f32; self.update_chunks(g, gfx); let m = cam.matrix(); + EYE.with(|e| e.set(cam.eye_dir())); set_camera(&cam.camera()); // ---- lit geometry @@ -855,6 +856,20 @@ impl R3d { // ------------------------------------------------------------------ landmarks, fx, overlay +// Camera direction for camera-facing line quads (set once per frame) +thread_local! { + static EYE: std::cell::Cell = std::cell::Cell::new(Vec3::Y); +} + +// Thick 3D line as a camera-facing quad (GL lines don't work with custom materials) +fn line3(a: Vec3, b: Vec3, w: f32, color: Color) { + let eye = EYE.with(|e| e.get()); + let side = (b - a).cross(eye).normalize_or_zero() * (w * 0.5); + let c = [(color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, (color.a * 255.0) as u8]; + let v = |p: Vec3| Vertex { position: p, uv: Vec2::ZERO, color: c, normal: Vec4::Y }; + draw_mesh(&Mesh { vertices: vec![v(a - side), v(a + side), v(b + side), v(b - side)], indices: vec![0, 1, 2, 0, 2, 3], texture: None }); +} + fn disc(c: Vec3, r: f32, color: Color, seg: usize) { let mut v = Vec::with_capacity(seg + 1); let cc = [(color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, (color.a * 255.0) as u8]; @@ -875,7 +890,7 @@ fn ring(c: Vec3, r: f32, color: Color) { for k in 0..seg { let a0 = k as f32 / seg as f32 * std::f32::consts::TAU; let a1 = (k + 1) as f32 / seg as f32 * std::f32::consts::TAU; - draw_line_3d(c + vec3(a0.cos() * r, 0.0, a0.sin() * r), c + vec3(a1.cos() * r, 0.0, a1.sin() * r), color); + line3(c + vec3(a0.cos() * r, 0.0, a0.sin() * r), c + vec3(a1.cos() * r, 0.0, a1.sin() * r), 0.035, color); } } @@ -935,7 +950,7 @@ fn draw_landmarks_glow(g: &Game, t: f32) { draw_sphere(gc, 0.8, None, Color::new(0.7, 0.85, 1.0, 0.2)); for k in 0..8 { let a = t * 0.6 + k as f32 * 0.785; - draw_line_3d(gc, gc + vec3(a.cos() * 1.0, a.sin() * 0.9, 0.0), Color::new(0.85, 0.92, 1.0, 0.6)); + line3(gc, gc + vec3(a.cos() * 1.0, a.sin() * 0.9, 0.0), 0.05, Color::new(0.85, 0.92, 1.0, 0.6)); } } } @@ -986,7 +1001,7 @@ fn draw_fx(f: &Fx, t: f32) { let to = vec3(f.x2, 0.45, f.y2); let head = from.lerp(to, p); let tail = from.lerp(to, (p - 0.35).max(0.0)); - draw_line_3d(tail, head, a(0.8)); + line3(tail, head, 0.06, a(0.8)); draw_sphere(head, 0.08, None, c); } 1 => { @@ -995,7 +1010,7 @@ fn draw_fx(f: &Fx, t: f32) { let y = 7.0 - 7.0 * k as f32 / 9.0; let j = if k == 9 { 0.0 } else { (t * 50.0 + k as f32 * 13.7).sin() * 0.5 }; let next = vec3(f.x + j, y, f.y + j * 0.5); - draw_line_3d(prev, next, Color::new(1.0, 1.0, 0.85, 1.0 - p)); + line3(prev, next, 0.12, Color::new(1.0, 1.0, 0.85, 1.0 - p)); prev = next; } disc(vec3(f.x, 0.05, f.y), 1.6 * (0.5 + p), Color::new(1.0, 1.0, 0.7, 0.35 * (1.0 - p)), 24); @@ -1004,7 +1019,7 @@ fn draw_fx(f: &Fx, t: f32) { let r = 0.1 + 0.3 * p; for k in 0..6 { let ang = k as f32 * 1.047; - draw_line_3d(at, at + vec3(ang.cos() * r, (k as f32 - 2.5) * 0.06, ang.sin() * r), a(1.0 - p)); + line3(at, at + vec3(ang.cos() * r, (k as f32 - 2.5) * 0.06, ang.sin() * r), 0.04, a(1.0 - p)); } } 3 => draw_sphere(vec3(f.x, 0.3, f.y), 0.2 + 0.5 * p, None, a(0.4 * (1.0 - p))), @@ -1038,9 +1053,7 @@ fn draw_fx(f: &Fx, t: f32) { fn outline(x0: f32, z0: f32, x1: f32, z1: f32, y: f32, c: Color) { let p = [vec3(x0, y, z0), vec3(x1, y, z0), vec3(x1, y, z1), vec3(x0, y, z1)]; for k in 0..4 { - draw_line_3d(p[k], p[(k + 1) % 4], c); - // double line for visibility - draw_line_3d(p[k] + vec3(0.0, 0.015, 0.0), p[(k + 1) % 4] + vec3(0.0, 0.015, 0.0), c); + line3(p[k], p[(k + 1) % 4], 0.06, c); } } diff --git a/src/sound.rs b/src/sound.rs index 30f0159..b6c4bff 100644 --- a/src/sound.rs +++ b/src/sound.rs @@ -3,7 +3,6 @@ // Ohne registrierten Klang bleibt ein Ereignis stumm. use crate::game::{Sfx, SfxEvent, SFX_ALL}; -use crate::render::View; use macroquad::audio::{load_sound_from_bytes, play_sound, PlaySoundParams, Sound}; use macroquad::prelude::*; @@ -47,7 +46,8 @@ impl SoundBank { } } - pub fn play(&mut self, ev: SfxEvent, view: &View, now: f64) { + // listener: camera target on the ground; hear: audible radius in tiles + pub fn play(&mut self, ev: SfxEvent, listener: Vec2, hear: f32, now: f64) { if self.muted || self.volume <= 0.0 { return; } @@ -59,9 +59,7 @@ impl SoundBank { // Lautstärke nach Entfernung zur Bildschirmmitte let mut vol = self.volume; if let Some((wx, wy)) = ev.pos { - let (cx, cy) = view.world(screen_width() * 0.5, screen_height() * 0.5); - let d = ((wx - cx).powi(2) + (wy - cy).powi(2)).sqrt(); - let hear = screen_width().max(screen_height()) / view.zoom * 0.6; + let d = ((wx - listener.x).powi(2) + (wy - listener.y).powi(2)).sqrt(); let f = 1.0 - ((d - hear * 0.5) / hear).clamp(0.0, 1.0); if f <= 0.05 { return; diff --git a/src/ui.rs b/src/ui.rs index e9cd95f..44eed49 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,7 +3,7 @@ use crate::game::*; use crate::gfx::*; use crate::map::*; -use crate::render::{minimap_color, View}; +use crate::render3d::{minimap_color, Cam}; use crate::sound::SoundBank; use macroquad::prelude::*; @@ -49,7 +49,7 @@ pub const TOOLS: [Tool; 12] = [ Tool::Summon, ]; -pub const TOOL_KEYS: [&str; 12] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "Q", "E"]; +pub const TOOL_KEYS: [&str; 12] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "X", "V"]; pub struct ToolInfo { pub label: &'static str, @@ -198,7 +198,7 @@ impl Ui { UiAction::None } - pub fn draw(&mut self, g: &Game, gfx: &Gfx, view: &View, snd: &SoundBank, info: &[(String, Color)], now: f64) { + pub fn draw(&mut self, g: &Game, gfx: &Gfx, cam: &Cam, snd: &SoundBank, info: &[(String, Color)], now: f64) { self.rects.clear(); let sw = screen_width(); let sh = screen_height(); @@ -325,10 +325,15 @@ impl Ui { let (fx, fy) = xy(f); draw_rectangle(mrx + fx as f32 * ms - 2.0, mry + fy as f32 * ms - 2.0, 5.0, 5.0, RED); } - // Kamerarahmen - let vw = sw / view.zoom * ms; - let vh = sh / view.zoom * ms; - draw_rectangle_lines(mrx + view.cam_x * ms, mry + view.cam_y * ms, vw, vh, 1.5, WHITE); + // camera footprint on the ground + let corners = [cam.ground_point(0.0, 0.0), cam.ground_point(sw, 0.0), cam.ground_point(sw, sh), cam.ground_point(0.0, sh)]; + for k in 0..4 { + let a = corners[k]; + let b = corners[(k + 1) % 4]; + let clampp = |p: Vec2| vec2(mrx + p.x.clamp(0.0, MW as f32) * ms, mry + p.y.clamp(0.0, MH as f32) * ms); + let (pa, pb) = (clampp(a), clampp(b)); + draw_line(pa.x, pa.y, pb.x, pb.y, 1.5, WHITE); + } // ---------------- Infofeld if !info.is_empty() { @@ -415,8 +420,9 @@ impl Ui { ("Linksklick Werkzeug benutzen (Graben/Räume: Rechteck ziehen)", TXT), ("Rechtsklick Einheit aus der Hand absetzen", TXT), ("Rechts ziehen Karte verschieben (auch WASD/Pfeile, Bildrand)", TXT), + ("Mitte ziehen Kamera drehen und neigen (auch Q/E, R/F)", TXT), ("Mausrad Zoomen", TXT), - ("1-9, 0, Q, E Werkzeug wählen", TXT), + ("1-9, 0, X, V Werkzeug wählen", TXT), ("P / Leertaste Pause M Ton an/aus +/- Lautstärke", TXT), ("H Diese Hilfe", TXT), ("", TXT), @@ -475,9 +481,9 @@ impl Ui { } // Infotext für Einheit oder Kachel unter dem Mauszeiger (erste Zeile = Titel) -pub fn hover_info(g: &Game, wx: f32, wy: f32) -> Vec<(String, Color)> { +pub fn hover_info(g: &Game, ent: Option, tile: Option<(i32, i32)>) -> Vec<(String, Color)> { let mut out = Vec::new(); - if let Some(j) = g.ent_at(wx, wy, false) { + if let Some(j) = ent { let e = &g.ents[j]; let s = st(e.kind); out.push((format!("{} (Stufe {})", s.name, e.level), if e.hero { BLUE } else { FRAME_HI })); @@ -502,7 +508,7 @@ pub fn hover_info(g: &Game, wx: f32, wy: f32) -> Vec<(String, Color)> { } return out; } - let (x, y) = (wx.floor() as i32, wy.floor() as i32); + let Some((x, y)) = tile else { return out }; let Some(t) = g.map.get(x, y) else { return out }; let owner = match t.owner { Owner::Player => "erobert",