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

-16
View File
@@ -1683,22 +1683,6 @@ impl Game {
} }
} }
pub fn ent_at(&self, wx: f32, wy: f32, player_only: bool) -> Option<usize> {
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<usize>) -> bool { pub fn pickup(&mut self, target: Option<usize>) -> bool {
if self.hand.len() >= 8 { if self.hand.len() >= 8 {
self.hint(8, "Deine Hand ist voll (max. 8). Rechtsklick lässt Einheiten fallen."); self.hint(8, "Deine Hand ist voll (max. 8). Rechtsklick lässt Einheiten fallen.");
+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 game;
mod gfx; mod gfx;
mod map; mod map;
mod render; mod render3d;
mod sound; mod sound;
mod ui; mod ui;
@@ -11,62 +11,68 @@ use game::Game;
use gfx::Gfx; use gfx::Gfx;
use macroquad::prelude::*; use macroquad::prelude::*;
use map::{idx, inb, MH, MW}; use map::{idx, inb, MH, MW};
use render::{draw_world, Overlay, View, ZOOMS}; use render3d::{Cam, Overlay, R3d};
use sound::SoundBank; use sound::SoundBank;
use ui::{hover_info, Tool, Ui, UiAction, TOOLS}; use ui::{hover_info, Tool, Ui, UiAction, TOOLS};
fn conf() -> Conf { fn conf() -> macroquad::conf::Conf {
Conf { window_title: "DungeonCraft".into(), window_width: 1280, window_height: 800, high_dpi: false, ..Default::default() } 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 { struct Input {
ldown: Option<(i32, i32)>, // Start des Auswahlrechtecks ldown: Option<(i32, i32)>, // start of the selection rectangle
rdown: Option<(f32, f32)>, // Startpunkt rechte Maustaste rdown: Option<(f32, f32)>, // right button press position
rlast: (f32, f32), rlast: (f32, f32),
rdrag: bool, rdrag: bool,
mlast: Option<(f32, f32)>, // middle button drag
minimap_drag: bool, minimap_drag: bool,
} }
fn new_game(seed: u32) -> (Game, View) { fn new_game(seed: u32) -> (Game, Cam) {
let g = Game::new(seed); let g = Game::new(seed);
let (cx, cy) = g.map.core; let (cx, cy) = g.map.core;
let zoom = 32.0; let cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
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, cam)
(g, view)
} }
fn clamp_cam(v: &mut View) { fn next_seed(seed: u32) -> u32 {
let vw = screen_width() / v.zoom; seed.wrapping_mul(1103515245).wrapping_add(12345) | 1
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;
} }
#[macroquad::main(conf)] #[macroquad::main(conf)]
async fn main() { async fn main() {
let gfx = Gfx::new(); let gfx = Gfx::new();
let mut r3d = R3d::new();
let mut sounds = SoundBank::new(); let mut sounds = SoundBank::new();
sounds.generate_all().await; sounds.generate_all().await;
let mut seed = (miniquad::date::now() * 1000.0) as u32 | 1; let mut 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 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 { loop {
let dt = get_frame_time(); let dt = get_frame_time().min(0.1);
let now = get_time(); let now = get_time();
let (mx, my) = mouse_position(); let (mx, my) = mouse_position();
let over_ui = ui.over_ui(mx, my); let over_ui = ui.over_ui(mx, my);
let (wx, wy) = view.world(mx, my); let picked = cam.pick_tile(&game.map, mx, my);
let hover_tile = (wx.floor() as i32, wy.floor() as i32); let hover_tile = picked.map(|(x, z, _)| (x, z));
let ground = cam.ground_point(mx, my);
let (wx, wy) = (ground.x, ground.y);
// ---------------- Tastatur // ---------------- keyboard
for (i, k) in [ let tool_keys = [
KeyCode::Key1, KeyCode::Key1,
KeyCode::Key2, KeyCode::Key2,
KeyCode::Key3, KeyCode::Key3,
@@ -77,12 +83,10 @@ async fn main() {
KeyCode::Key8, KeyCode::Key8,
KeyCode::Key9, KeyCode::Key9,
KeyCode::Key0, KeyCode::Key0,
KeyCode::Q, KeyCode::X,
KeyCode::E, KeyCode::V,
] ];
.iter() for (i, k) in tool_keys.iter().enumerate() {
.enumerate()
{
if is_key_pressed(*k) { if is_key_pressed(*k) {
ui.tool = TOOLS[i]; ui.tool = TOOLS[i];
inp.ldown = None; inp.ldown = None;
@@ -107,51 +111,78 @@ async fn main() {
sounds.change_volume(-0.1); sounds.change_volume(-0.1);
} }
if is_key_pressed(KeyCode::N) && game.state != 0 { if is_key_pressed(KeyCode::N) && game.state != 0 {
seed = seed.wrapping_mul(1103515245).wrapping_add(12345) | 1; seed = next_seed(seed);
(game, view) = new_game(seed); (game, cam) = new_game(seed);
ui.show_help = false; ui.show_help = false;
} }
// ---------------- Kamera // ---------------- camera: pan
let pan = 900.0 * dt / view.zoom; 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) { 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) { 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) { 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) { 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; let edge = 6.0;
if !ui.show_help && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() { if !ui.show_help && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
if mx < edge { if mx < edge {
view.cam_x -= pan; move_cam(-pan, 0.0, &mut cam);
} else if mx > screen_width() - edge { } else if mx > screen_width() - edge {
view.cam_x += pan; move_cam(pan, 0.0, &mut cam);
} }
if my < edge { if my < edge {
view.cam_y -= pan; move_cam(0.0, pan, &mut cam);
} else if my > screen_height() - edge { } 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(); let (_, wheel) = mouse_wheel();
if wheel != 0.0 && !over_ui { if wheel != 0.0 && !over_ui {
let zi = ZOOMS.iter().position(|&z| z >= view.zoom).unwrap_or(2) as i32; let before = cam.ground_point(mx, my);
let ni = (zi + if wheel > 0.0 { 1 } else { -1 }).clamp(0, ZOOMS.len() as i32 - 1) as usize; cam.zoom = (cam.zoom * if wheel > 0.0 { 0.88 } else { 1.0 / 0.88 }).clamp(6.0, 45.0);
if ZOOMS[ni] != view.zoom { let after = cam.ground_point(mx, my);
view.zoom = ZOOMS[ni]; cam.tx += before.x - after.x;
view.cam_x = wx - mx / view.zoom; cam.tz += before.y - after.y;
view.cam_y = wy - my / view.zoom;
}
} }
// Rechte Maustaste: ziehen = verschieben, klicken = absetzen // ---------------- right button: drag = pan, click = drop
if is_mouse_button_pressed(MouseButton::Right) { if is_mouse_button_pressed(MouseButton::Right) {
inp.rdown = Some((mx, my)); inp.rdown = Some((mx, my));
inp.rlast = (mx, my); inp.rlast = (mx, my);
@@ -163,8 +194,10 @@ async fn main() {
inp.rdrag = true; inp.rdrag = true;
} }
if inp.rdrag { if inp.rdrag {
view.cam_x -= (mx - inp.rlast.0) / view.zoom; let a = cam.ground_point(inp.rlast.0, inp.rlast.1);
view.cam_y -= (my - inp.rlast.1) / view.zoom; let b = cam.ground_point(mx, my);
cam.tx -= b.x - a.x;
cam.tz -= b.y - a.y;
} }
inp.rlast = (mx, my); inp.rlast = (mx, my);
} }
@@ -180,7 +213,7 @@ async fn main() {
inp.rdown = None; inp.rdown = None;
} }
// ---------------- Linke Maustaste // ---------------- left button
if is_mouse_button_pressed(MouseButton::Left) { if is_mouse_button_pressed(MouseButton::Left) {
match ui.click(mx, my, &game) { match ui.click(mx, my, &game) {
UiAction::SelectTool(t) => { UiAction::SelectTool(t) => {
@@ -188,27 +221,30 @@ async fn main() {
game.sfx_ui(game::Sfx::Click); game.sfx_ui(game::Sfx::Click);
} }
UiAction::MinimapJump(x, y) => { UiAction::MinimapJump(x, y) => {
center_on(&mut view, x, y); cam.tx = x;
cam.tz = y;
inp.minimap_drag = true; inp.minimap_drag = true;
} }
UiAction::CloseHelp => ui.show_help = false, UiAction::CloseHelp => ui.show_help = false,
UiAction::NewGame => { UiAction::NewGame => {
seed = seed.wrapping_mul(1103515245).wrapping_add(12345) | 1; seed = next_seed(seed);
(game, view) = new_game(seed); (game, cam) = new_game(seed);
ui.show_help = false; ui.show_help = false;
} }
UiAction::Consumed => {} UiAction::Consumed => {}
UiAction::None => { UiAction::None => {
if game.state == 0 && inb(hover_tile.0, hover_tile.1) { if game.state == 0 {
match ui.tool { match (ui.tool, hover_tile) {
Tool::Dig | Tool::Room(_) | Tool::Sell => inp.ldown = Some(hover_tile), (Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t),
Tool::Hand => { (Tool::Hand, _) => {
game.pickup(wx, wy); 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::Flag, Some((x, z))) => game.set_flag(x, z),
Tool::Lightning => game.lightning(wx, wy), (Tool::Lightning, _) => game.lightning(wx, wy),
Tool::Heal => game.heal(wx, wy), (Tool::Heal, _) => game.heal(wx, wy),
Tool::Summon => game.summon(wx, wy), (Tool::Summon, _) => game.summon(wx, wy),
_ => {}
} }
} }
} }
@@ -217,39 +253,43 @@ async fn main() {
if inp.minimap_drag { if inp.minimap_drag {
if is_mouse_button_down(MouseButton::Left) && ui.minimap_contains(mx, my) { if is_mouse_button_down(MouseButton::Left) && ui.minimap_contains(mx, my) {
let (x, y) = ui.minimap_world(mx, my); let (x, y) = ui.minimap_world(mx, my);
center_on(&mut view, x, y); cam.tx = x;
cam.tz = y;
} else if !is_mouse_button_down(MouseButton::Left) { } else if !is_mouse_button_down(MouseButton::Left) {
inp.minimap_drag = false; inp.minimap_drag = false;
} }
} }
if is_mouse_button_released(MouseButton::Left) { if is_mouse_button_released(MouseButton::Left) {
if let Some((sx, sy)) = inp.ldown.take() { if let Some((sx, sy)) = inp.ldown.take() {
let (ex, ey) = (hover_tile.0.clamp(0, MW as i32 - 1), hover_tile.1.clamp(0, MH as i32 - 1)); if let Some((ex, ey)) = hover_tile {
match ui.tool { match ui.tool {
Tool::Dig => { Tool::Dig => {
let on = !game.map.t[idx(sx, sy)].marked; let on = !game.map.t[idx(sx, sy)].marked;
game.mark_rect(sx, sy, ex, ey, on); 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); game.update(dt);
let hear = cam.zoom * 1.2;
for ev in game.events.drain(..) { 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(..) { for m in game.msgs.drain(..) {
ui.log.push((m, now)); ui.log.push((m, now));
} }
// ---------------- Zeichnen // ---------------- draw
clear_background(Color::from_rgba(8, 6, 8, 255)); clear_background(Color::from_rgba(4, 3, 5, 255));
let (hover_color, radius) = match ui.tool { let (color, radius) = match ui.tool {
Tool::Dig => (Color::new(1.0, 0.85, 0.3, 1.0), None), 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::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::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), Tool::Summon => (Color::new(0.8, 0.5, 1.0, 1.0), None),
}; };
let ov = Overlay { let ov = Overlay {
hover: if over_ui || !inb(hover_tile.0, hover_tile.1) { None } else { Some(hover_tile) }, hover: if over_ui { None } else { hover_tile.filter(|&(x, z)| inb(x, z)) },
hover_color, 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)))),
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))), color,
rect_color: hover_color,
radius: if over_ui { None } else { radius }, radius: if over_ui { None } else { radius },
high: ui.tool == Tool::Dig,
}; };
draw_world(&game, &gfx, &view, now, &ov); r3d.draw(&game, &gfx, &cam, now, &ov);
let info = if over_ui { Vec::new() } else { hover_info(&game, wx, wy) }; let info = if over_ui {
ui.draw(&game, &gfx, &view, &sounds, &info, now); Vec::new()
} else {
let ent = r3d.pick_ent(&game, &cam, mx, my, false);
hover_info(&game, ent, hover_tile)
};
ui.draw(&game, &gfx, &cam, &sounds, &info, now);
next_frame().await next_frame().await
} }
-455
View File
@@ -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<usize> = (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],
}
}
+22 -9
View File
@@ -446,7 +446,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
[Vec3::Y; 4], [Vec3::Y; 4],
); );
let n = WALL_SUB; let n = WALL_SUB;
let top_amb = amb_at(amb, xf + 0.5, zf + 0.5) * 0.85 + 0.05; let top_amb = amb_at(amb, xf + 0.5, zf + 0.5) * 0.95 + 0.1;
build_grid( build_grid(
&mut mb, &mut mb,
|u, v| vec3(xf + u, WALL_H, zf + v), |u, v| vec3(xf + u, WALL_H, zf + v),
@@ -690,6 +690,7 @@ impl R3d {
let t = time as f32; let t = time as f32;
self.update_chunks(g, gfx); self.update_chunks(g, gfx);
let m = cam.matrix(); let m = cam.matrix();
EYE.with(|e| e.set(cam.eye_dir()));
set_camera(&cam.camera()); set_camera(&cam.camera());
// ---- lit geometry // ---- lit geometry
@@ -855,6 +856,20 @@ impl R3d {
// ------------------------------------------------------------------ landmarks, fx, overlay // ------------------------------------------------------------------ landmarks, fx, overlay
// Camera direction for camera-facing line quads (set once per frame)
thread_local! {
static EYE: std::cell::Cell<Vec3> = 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) { fn disc(c: Vec3, r: f32, color: Color, seg: usize) {
let mut v = Vec::with_capacity(seg + 1); 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]; 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 { for k in 0..seg {
let a0 = k as f32 / seg as f32 * std::f32::consts::TAU; 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; 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)); draw_sphere(gc, 0.8, None, Color::new(0.7, 0.85, 1.0, 0.2));
for k in 0..8 { for k in 0..8 {
let a = t * 0.6 + k as f32 * 0.785; 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 to = vec3(f.x2, 0.45, f.y2);
let head = from.lerp(to, p); let head = from.lerp(to, p);
let tail = from.lerp(to, (p - 0.35).max(0.0)); 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); draw_sphere(head, 0.08, None, c);
} }
1 => { 1 => {
@@ -995,7 +1010,7 @@ fn draw_fx(f: &Fx, t: f32) {
let y = 7.0 - 7.0 * k as f32 / 9.0; 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 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); 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; 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); 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; let r = 0.1 + 0.3 * p;
for k in 0..6 { for k in 0..6 {
let ang = k as f32 * 1.047; 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))), 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) { 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)]; let p = [vec3(x0, y, z0), vec3(x1, y, z0), vec3(x1, y, z1), vec3(x0, y, z1)];
for k in 0..4 { for k in 0..4 {
draw_line_3d(p[k], p[(k + 1) % 4], c); line3(p[k], p[(k + 1) % 4], 0.06, 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);
} }
} }
+3 -5
View File
@@ -3,7 +3,6 @@
// Ohne registrierten Klang bleibt ein Ereignis stumm. // Ohne registrierten Klang bleibt ein Ereignis stumm.
use crate::game::{Sfx, SfxEvent, SFX_ALL}; use crate::game::{Sfx, SfxEvent, SFX_ALL};
use crate::render::View;
use macroquad::audio::{load_sound_from_bytes, play_sound, PlaySoundParams, Sound}; use macroquad::audio::{load_sound_from_bytes, play_sound, PlaySoundParams, Sound};
use macroquad::prelude::*; 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 { if self.muted || self.volume <= 0.0 {
return; return;
} }
@@ -59,9 +59,7 @@ impl SoundBank {
// Lautstärke nach Entfernung zur Bildschirmmitte // Lautstärke nach Entfernung zur Bildschirmmitte
let mut vol = self.volume; let mut vol = self.volume;
if let Some((wx, wy)) = ev.pos { if let Some((wx, wy)) = ev.pos {
let (cx, cy) = view.world(screen_width() * 0.5, screen_height() * 0.5); let d = ((wx - listener.x).powi(2) + (wy - listener.y).powi(2)).sqrt();
let d = ((wx - cx).powi(2) + (wy - cy).powi(2)).sqrt();
let hear = screen_width().max(screen_height()) / view.zoom * 0.6;
let f = 1.0 - ((d - hear * 0.5) / hear).clamp(0.0, 1.0); let f = 1.0 - ((d - hear * 0.5) / hear).clamp(0.0, 1.0);
if f <= 0.05 { if f <= 0.05 {
return; return;
+17 -11
View File
@@ -3,7 +3,7 @@
use crate::game::*; use crate::game::*;
use crate::gfx::*; use crate::gfx::*;
use crate::map::*; use crate::map::*;
use crate::render::{minimap_color, View}; use crate::render3d::{minimap_color, Cam};
use crate::sound::SoundBank; use crate::sound::SoundBank;
use macroquad::prelude::*; use macroquad::prelude::*;
@@ -49,7 +49,7 @@ pub const TOOLS: [Tool; 12] = [
Tool::Summon, Tool::Summon,
]; ];
pub const TOOL_KEYS: [&str; 12] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "Q", "E"]; pub const TOOL_KEYS: [&str; 12] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "X", "V"];
pub struct ToolInfo { pub struct ToolInfo {
pub label: &'static str, pub label: &'static str,
@@ -198,7 +198,7 @@ impl Ui {
UiAction::None 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(); self.rects.clear();
let sw = screen_width(); let sw = screen_width();
let sh = screen_height(); let sh = screen_height();
@@ -325,10 +325,15 @@ impl Ui {
let (fx, fy) = xy(f); 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); draw_rectangle(mrx + fx as f32 * ms - 2.0, mry + fy as f32 * ms - 2.0, 5.0, 5.0, RED);
} }
// Kamerarahmen // camera footprint on the ground
let vw = sw / view.zoom * ms; 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)];
let vh = sh / view.zoom * ms; for k in 0..4 {
draw_rectangle_lines(mrx + view.cam_x * ms, mry + view.cam_y * ms, vw, vh, 1.5, WHITE); 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 // ---------------- Infofeld
if !info.is_empty() { if !info.is_empty() {
@@ -415,8 +420,9 @@ impl Ui {
("Linksklick Werkzeug benutzen (Graben/Räume: Rechteck ziehen)", TXT), ("Linksklick Werkzeug benutzen (Graben/Räume: Rechteck ziehen)", TXT),
("Rechtsklick Einheit aus der Hand absetzen", TXT), ("Rechtsklick Einheit aus der Hand absetzen", TXT),
("Rechts ziehen Karte verschieben (auch WASD/Pfeile, Bildrand)", 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), ("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), ("P / Leertaste Pause M Ton an/aus +/- Lautstärke", TXT),
("H Diese Hilfe", TXT), ("H Diese Hilfe", TXT),
("", TXT), ("", TXT),
@@ -475,9 +481,9 @@ impl Ui {
} }
// Infotext für Einheit oder Kachel unter dem Mauszeiger (erste Zeile = Titel) // 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<usize>, tile: Option<(i32, i32)>) -> Vec<(String, Color)> {
let mut out = Vec::new(); 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 e = &g.ents[j];
let s = st(e.kind); let s = st(e.kind);
out.push((format!("{} (Stufe {})", s.name, e.level), if e.hero { BLUE } else { FRAME_HI })); 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; 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 Some(t) = g.map.get(x, y) else { return out };
let owner = match t.owner { let owner = match t.owner {
Owner::Player => "erobert", Owner::Player => "erobert",