From db417b3af850d9f96712cdb5020d26fcd796d14a Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Mon, 2 Nov 2026 22:16:41 +0100 Subject: [PATCH] =?UTF-8?q?Renderer,=20Oberfl=C3=A4che,=20Eingabe,=20Sound?= =?UTF-8?q?-Modul,=20Simulationstest=20und=20Docker-Setup=20(Port=2018093)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 2 + Cargo.lock | 404 +++++++++++++++++++++++++++++++++ Dockerfile | 23 ++ build.sh | 6 - docker-compose.yml | 8 + nginx.conf | 10 + src/game.rs | 84 ++++++- src/main.rs | 275 ++++++++++++++++++++++ src/map.rs | 11 +- src/render.rs | 455 +++++++++++++++++++++++++++++++++++++ src/sound.rs | 98 ++++++++ src/ui.rs | 553 +++++++++++++++++++++++++++++++++++++++++++++ www/index.html | 20 ++ 13 files changed, 1930 insertions(+), 19 deletions(-) create mode 100644 .dockerignore create mode 100644 Cargo.lock create mode 100644 Dockerfile delete mode 100755 build.sh create mode 100644 docker-compose.yml create mode 100644 nginx.conf create mode 100644 src/main.rs create mode 100644 src/render.rs create mode 100644 src/sound.rs create mode 100644 src/ui.rs create mode 100644 www/index.html diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c7f83c2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +target +.git diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..130a8d7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,404 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "audir-sles" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea47348666a8edb7ad80cbee3940eb2bccf70df0e6ce09009abe1a836cb779f5" + +[[package]] +name = "audrey" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b92a84e89497e3cd25d3672cd5d1c288abaac02c18ff21283f17d118b889b8" +dependencies = [ + "dasp_frame", + "dasp_sample", + "hound", + "lewton", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "crc32fast" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f97d3416d5337d15c4b0d78c8c91df922ca143f57ec20e2d1c188333047ef04" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dasp_frame" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "dungeoncraft" +version = "0.1.0" +dependencies = [ + "macroquad", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fontdue" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e796cb6894e6418daacea84b28dd4f98d3b32c74d5bb452ba1ecfbf5ad8e84ca" +dependencies = [ + "hashbrown", + "ttf-parser", +] + +[[package]] +name = "glam" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png", +] + +[[package]] +name = "lewton" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d542c1a317036c45c2aa1cf10cc9d403ca91eb2d333ef1a4917e5cb10628bd0" +dependencies = [ + "byteorder", + "ogg", + "smallvec", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "macroquad" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cbd54d99d4d3ac59deed7ee4927bbbf4eb33a55635530af3be9395a55746090" +dependencies = [ + "fontdue", + "glam", + "image", + "macroquad_macro", + "miniquad", + "quad-rand", + "quad-snd", +] + +[[package]] +name = "macroquad_macro" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b1d96218903768c1ce078b657c0d5965465c95a60d2682fd97443c9d2483dd" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "miniquad" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbaea978d741f3b60ced666b06a2a1e4a2bc81cb8b9dd29f4a40965e1f59e65" +dependencies = [ + "libc", + "ndk-sys", + "objc-rs", + "winapi", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "ndk-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc-rs" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "ogg" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e571c3517af9e1729d4c63571a27edd660ade0667973bfc74a67c660c2b651" +dependencies = [ + "byteorder", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "quad-alsa-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66c2f04a6946293477973d85adc251d502da51c57b08cd9c997f0cfd8dcd4b5" +dependencies = [ + "libc", +] + +[[package]] +name = "quad-rand" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" + +[[package]] +name = "quad-snd" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba0c4943fc67147fbe9d1eb731fb9e678bfc9d926507eebbbfe0103e154e5b0" +dependencies = [ + "audir-sles", + "audrey", + "libc", + "quad-alsa-sys", + "winapi", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6f2907f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# --- Build: Rust -> WASM +FROM rust:1-slim AS build +RUN rustup target add wasm32-unknown-unknown +WORKDIR /src +COPY Cargo.toml Cargo.lock ./ +COPY .cargo .cargo +# Abhängigkeiten vorab bauen (Docker-Cache) +RUN mkdir src && echo "fn main() {}" > src/main.rs \ + && cargo build --release --target wasm32-unknown-unknown \ + && rm -rf src +COPY assets assets +COPY src src +RUN touch src/main.rs && cargo build --release --target wasm32-unknown-unknown \ + && mkdir /out \ + && cp target/wasm32-unknown-unknown/release/dungeoncraft.wasm /out/ \ + && cp "$CARGO_HOME"/registry/src/*/macroquad-*/js/mq_js_bundle.js /out/ + +# --- Auslieferung: nginx +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY www/ /usr/share/nginx/html/ +COPY --from=build /out/ /usr/share/nginx/html/ +EXPOSE 80 diff --git a/build.sh b/build.sh deleted file mode 100755 index 0c0c0ca..0000000 --- a/build.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -e -cd "$(dirname "$0")" -cargo build --release --target wasm32-unknown-unknown -cp target/wasm32-unknown-unknown/release/unterreich.wasm www/unterreich.wasm -echo "Fertig. Starten mit: python3 -m http.server 8080 -d www -> http://localhost:8080" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dff2b2e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + dungeoncraft: + build: . + image: dungeoncraft:latest + container_name: dungeoncraft + ports: + - "18093:80" + restart: unless-stopped diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..20ea7ba --- /dev/null +++ b/nginx.conf @@ -0,0 +1,10 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + # Testversion: immer frisch laden + add_header Cache-Control "no-cache"; + location / { + try_files $uri $uri/ =404; + } +} diff --git a/src/game.rs b/src/game.rs index 6926a6e..27852af 100644 --- a/src/game.rs +++ b/src/game.rs @@ -380,9 +380,6 @@ impl Game { pub fn worker_count(&self) -> usize { self.ents.iter().filter(|e| e.alive && !e.hero && e.kind == EK::Worker).count() } - pub fn hero_count(&self) -> usize { - self.ents.iter().filter(|e| e.alive && e.hero).count() - } pub fn summon_cost(&self) -> f32 { 80.0 + 20.0 * (self.worker_count() as f32 - 4.0).max(0.0) } @@ -1027,7 +1024,7 @@ impl Game { let fxx = (e.x + tx as f32 + 0.5) * 0.5; let fxy = (e.y + ty as f32 + 0.5) * 0.5; let tl = &mut self.map.t[t]; - let mut done = false; + let done; if tl.kind == Kind::Gold { let take = tl.gold.min(25); tl.gold -= take; @@ -1802,3 +1799,82 @@ pub fn job_name(e: &Ent) -> &'static str { } } } + +#[cfg(test)] +mod tests { + use super::*; + + // Simuliert ein Spiel mit einfacher "KI-Spielerin" und prüft, dass nichts abstürzt. + #[test] + fn simulate_long_game() { + for seed in [1u32, 7, 12345] { + let mut g = Game::new(seed); + let (cx, cy) = g.map.core; + let (px, py) = g.map.portal; + // Weg zum Portal graben (durch Erde/Gold, um Fels herum) + let start = idx(cx - 1, cy + 1); + let goal = idx(px + 1, py + 1); + let path = g + .map + .search(start, |i| if g.map.t[i].kind == Kind::Rock { None } else { Some(1) }, |i| i == goal, false) + .expect("Portal erreichbar"); + for i in path { + if g.map.t[i].diggable() { + g.map.t[i].marked = true; + } + } + g.mark_rect(cx - 6, cy - 6, cx + 9, cy + 9, true); + let (ox, oy) = (cx - 3, cy - 3); + g.build_rect(1, ox, oy, ox + 2, oy + 1); // Quartier 3x2 + g.build_rect(2, ox + 5, oy, ox + 7, oy + 1); // Pilzgarten 3x2 + let dt = 0.05; + let mut built = false; + let mut portal_logged = false; + for step in 0..(40 * 60 * 20) { + g.update(dt); + g.events.clear(); + g.msgs.clear(); + if step % 200 == 0 && !built && g.gold > 1900.0 { + g.build_rect(0, ox, oy + 6, ox + 1, oy + 8); // Schatzkammer + g.build_rect(3, ox + 6, oy + 6, ox + 8, oy + 8); // Training + g.build_rect(1, ox, oy + 2, ox + 2, oy + 2); // Quartier erweitern + built = true; + } + // Bot nutzt den Blitz gegen Helden am Kern + if step % 20 == 0 && g.mana >= 50.0 { + let target = g.ents.iter().find(|e| e.alive && e.hero && (e.x - cx as f32).abs() < 4.0 && (e.y - cy as f32).abs() < 4.0).map(|e| (e.x, e.y)); + if let Some((x, y)) = target { + g.lightning(x, y); + } + } + if step == 20 * 60 * 20 { + let (gx, gy) = g.map.gate; + g.set_flag(gx + 1, gy + 1); + } + if step % 2400 == 0 { + println!( + "seed {} t={:>5.0}s gold={:>5.0} creatures={} workers={} wave={} core={:.0} gate={:.0} heroes={} army={:.0}", + seed, + g.time, + g.gold, + g.creature_count(), + g.worker_count(), + g.wave, + g.core_hp, + g.gate_hp, + g.ents.iter().filter(|e| e.alive && e.hero).count(), + g.army_value() + ); + } + if g.portal_owned() && step % 20 == 0 && !portal_logged { + portal_logged = true; + println!("seed {} Portal erobert nach {:.0}s", seed, g.time); + } + if g.state != 0 { + println!("seed {} Ende nach {:.0}s, state={}", seed, g.time, g.state); + break; + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a559932 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,275 @@ +// Dungeoncraft – Dungeon-Aufbau & Verteidigung im Browser (Rust + macroquad, WASM). + +mod game; +mod gfx; +mod map; +mod render; +mod sound; +mod ui; + +use game::Game; +use gfx::Gfx; +use macroquad::prelude::*; +use map::{idx, inb, MH, MW}; +use render::{draw_world, Overlay, View, ZOOMS}; +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() } +} + +struct Input { + ldown: Option<(i32, i32)>, // Start des Auswahlrechtecks + rdown: Option<(f32, f32)>, // Startpunkt rechte Maustaste + rlast: (f32, f32), + rdrag: bool, + minimap_drag: bool, +} + +fn new_game(seed: u32) -> (Game, View) { + 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) +} + +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; +} + +#[macroquad::main(conf)] +async fn main() { + let gfx = Gfx::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 ui = Ui::new(); + let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, minimap_drag: false }; + + loop { + let dt = get_frame_time(); + 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); + + // ---------------- Tastatur + for (i, k) in [ + KeyCode::Key1, + KeyCode::Key2, + KeyCode::Key3, + KeyCode::Key4, + KeyCode::Key5, + KeyCode::Key6, + KeyCode::Key7, + KeyCode::Key8, + KeyCode::Key9, + KeyCode::Key0, + KeyCode::Q, + KeyCode::E, + ] + .iter() + .enumerate() + { + if is_key_pressed(*k) { + ui.tool = TOOLS[i]; + inp.ldown = None; + } + } + if is_key_pressed(KeyCode::H) || (is_key_pressed(KeyCode::Escape) && ui.show_help) { + ui.show_help = !ui.show_help; + } + if is_key_pressed(KeyCode::Escape) { + inp.ldown = None; + } + if (is_key_pressed(KeyCode::P) || is_key_pressed(KeyCode::Space)) && game.state == 0 { + game.paused = !game.paused; + } + if is_key_pressed(KeyCode::M) { + sounds.muted = !sounds.muted; + } + if is_key_pressed(KeyCode::Equal) || is_key_pressed(KeyCode::KpAdd) { + sounds.change_volume(0.1); + } + if is_key_pressed(KeyCode::Minus) || is_key_pressed(KeyCode::KpSubtract) { + sounds.change_volume(-0.1); + } + if is_key_pressed(KeyCode::N) && game.state != 0 { + seed = seed.wrapping_mul(1103515245).wrapping_add(12345) | 1; + (game, view) = new_game(seed); + ui.show_help = false; + } + + // ---------------- Kamera + let pan = 900.0 * dt / view.zoom; + if is_key_down(KeyCode::W) || is_key_down(KeyCode::Up) { + view.cam_y -= pan; + } + if is_key_down(KeyCode::S) || is_key_down(KeyCode::Down) { + view.cam_y += pan; + } + if is_key_down(KeyCode::A) || is_key_down(KeyCode::Left) { + view.cam_x -= pan; + } + if is_key_down(KeyCode::D) || is_key_down(KeyCode::Right) { + view.cam_x += pan; + } + // 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; + } else if mx > screen_width() - edge { + view.cam_x += pan; + } + if my < edge { + view.cam_y -= pan; + } else if my > screen_height() - edge { + view.cam_y += pan; + } + } + // Zoom + 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; + } + } + // Rechte Maustaste: ziehen = verschieben, klicken = absetzen + if is_mouse_button_pressed(MouseButton::Right) { + inp.rdown = Some((mx, my)); + inp.rlast = (mx, my); + inp.rdrag = false; + } + if let Some((sx, sy)) = inp.rdown { + if is_mouse_button_down(MouseButton::Right) { + if (mx - sx).abs() + (my - sy).abs() > 5.0 { + inp.rdrag = true; + } + if inp.rdrag { + view.cam_x -= (mx - inp.rlast.0) / view.zoom; + view.cam_y -= (my - inp.rlast.1) / view.zoom; + } + inp.rlast = (mx, my); + } + } + if is_mouse_button_released(MouseButton::Right) { + if !inp.rdrag && !over_ui && game.state == 0 { + if inp.ldown.is_some() { + inp.ldown = None; + } else { + game.drop(wx, wy); + } + } + inp.rdown = None; + } + + // ---------------- Linke Maustaste + if is_mouse_button_pressed(MouseButton::Left) { + match ui.click(mx, my, &game) { + UiAction::SelectTool(t) => { + ui.tool = t; + game.sfx_ui(game::Sfx::Click); + } + UiAction::MinimapJump(x, y) => { + center_on(&mut view, x, 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); + 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); + } + 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), + } + } + } + } + } + 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); + } 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); + } + Tool::Room(r) => game.build_rect(r, sx, sy, ex, ey), + Tool::Sell => game.sell_rect(sx, sy, ex, ey), + _ => {} + } + } + } + clamp_cam(&mut view); + + // ---------------- Update + game.update(dt); + for ev in game.events.drain(..) { + sounds.play(ev, &view, 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 { + Tool::Dig => (Color::new(1.0, 0.85, 0.3, 1.0), None), + Tool::Hand => (Color::new(0.9, 0.75, 0.6, 1.0), None), + Tool::Room(_) => (Color::new(0.4, 0.85, 1.0, 1.0), None), + Tool::Sell => (Color::new(1.0, 0.5, 0.3, 1.0), None), + Tool::Flag => (Color::new(1.0, 0.3, 0.3, 1.0), None), + Tool::Lightning => (Color::new(1.0, 1.0, 0.6, 1.0), Some((wx, wy, 1.6))), + Tool::Heal => (Color::new(0.4, 1.0, 0.5, 1.0), Some((wx, wy, 2.55))), + Tool::Summon => (Color::new(0.8, 0.5, 1.0, 1.0), None), + }; + let ov = Overlay { + hover: if over_ui || !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, + radius: if over_ui { None } else { radius }, + }; + 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); + + next_frame().await + } +} diff --git a/src/map.rs b/src/map.rs index fc5dc57..73c757d 100644 --- a/src/map.rs +++ b/src/map.rs @@ -59,9 +59,6 @@ impl Tile { Kind::Floor | Kind::Treasury | Kind::Lair | Kind::Farm | Kind::Training | Kind::Library | Kind::Portal | Kind::Gate ) } - pub fn is_room(&self) -> bool { - matches!(self.kind, Kind::Treasury | Kind::Lair | Kind::Farm | Kind::Training | Kind::Library) - } } pub fn idx(x: i32, y: i32) -> usize { @@ -148,7 +145,7 @@ impl Map { let portal = (rng.range(24, 30), if core.1 > 31 { rng.range(12, 18) } else { rng.range(44, 50) }); let gate = (rng.range(51, 55), rng.range(10, 50)); - let mut blob = |t: &mut Vec, rng: &mut Rng, cx: i32, cy: i32, r: i32, k: Kind| { + let blob = |t: &mut Vec, rng: &mut Rng, cx: i32, cy: i32, r: i32, k: Kind| { for y in cy - r..=cy + r { for x in cx - r..=cx + r { if !inb(x, y) || x < 2 || y < 2 || x >= MW as i32 - 2 || y >= MH as i32 - 2 { @@ -187,7 +184,7 @@ impl Map { blob(&mut t, &mut rng, cx, cy, 1, Kind::Gold); } - let mut room = |t: &mut Vec, x0: i32, y0: i32, x1: i32, y1: i32, owner: Owner| { + let room = |t: &mut Vec, x0: i32, y0: i32, x1: i32, y1: i32, owner: Owner| { for y in y0..=y1 { for x in x0..=x1 { if inb(x, y) && x >= 2 && y >= 2 && x < MW as i32 - 2 && y < MH as i32 - 2 { @@ -327,8 +324,4 @@ impl Map { pub fn path_to(&self, start: usize, target: usize) -> Option> { self.search(start, self.walk_cost(), |i| i == target, true) } - - pub fn count(&self, k: Kind) -> usize { - self.t.iter().filter(|t| t.kind == k && t.owner == Owner::Player).count() - } } diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..0225263 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,455 @@ +// 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/sound.rs b/src/sound.rs new file mode 100644 index 0000000..30f0159 --- /dev/null +++ b/src/sound.rs @@ -0,0 +1,98 @@ +// Sound-Wiedergabe. Die Spiellogik meldet Ereignisse (Sfx), hier werden sie abgespielt. +// Klänge werden später generiert und per `SoundBank::set` (WAV-Bytes) registriert. +// 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::*; + +pub struct SoundBank { + sounds: Vec>, + last: Vec, + pub volume: f32, + pub muted: bool, +} + +impl SoundBank { + pub fn new() -> SoundBank { + SoundBank { sounds: vec![None; SFX_ALL.len()], last: vec![-1.0; SFX_ALL.len()], volume: 0.7, muted: false } + } + + // Klang für ein Ereignis registrieren (WAV- oder OGG-Bytes) + #[allow(dead_code)] + pub async fn set(&mut self, sfx: Sfx, bytes: &[u8]) -> bool { + match load_sound_from_bytes(bytes).await { + Ok(s) => { + self.sounds[sfx.index()] = Some(s); + true + } + Err(_) => false, + } + } + + // Hier werden später die prozedural erzeugten Klänge registriert. + pub async fn generate_all(&mut self) {} + + pub fn change_volume(&mut self, d: f32) { + self.volume = (self.volume + d).clamp(0.0, 1.0); + } + + // Mindestabstand zwischen zwei gleichen Klängen (gegen Klangbrei) + fn min_gap(sfx: Sfx) -> f64 { + match sfx { + Sfx::Dig | Sfx::GoldDig | Sfx::MeleeHit | Sfx::Shoot | Sfx::GateHit | Sfx::CoreHit => 0.08, + Sfx::Claim | Sfx::Deposit | Sfx::DigDone => 0.12, + _ => 0.03, + } + } + + pub fn play(&mut self, ev: SfxEvent, view: &View, now: f64) { + if self.muted || self.volume <= 0.0 { + return; + } + let i = ev.sfx.index(); + let Some(sound) = &self.sounds[i] else { return }; + if now - self.last[i] < Self::min_gap(ev.sfx) { + return; + } + // 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 f = 1.0 - ((d - hear * 0.5) / hear).clamp(0.0, 1.0); + if f <= 0.05 { + return; + } + vol *= f; + } + self.last[i] = now; + play_sound(sound, PlaySoundParams { looped: false, volume: vol }); + } +} + +// Hilfsfunktion für die spätere Generierung: Samples (-1..1, mono) -> WAV (16 bit PCM) +#[allow(dead_code)] +pub fn wav_from_samples(samples: &[f32], rate: u32) -> Vec { + let data_len = (samples.len() * 2) as u32; + let mut out = Vec::with_capacity(44 + data_len as usize); + out.extend_from_slice(b"RIFF"); + out.extend_from_slice(&(36 + data_len).to_le_bytes()); + out.extend_from_slice(b"WAVEfmt "); + out.extend_from_slice(&16u32.to_le_bytes()); + out.extend_from_slice(&1u16.to_le_bytes()); // PCM + out.extend_from_slice(&1u16.to_le_bytes()); // mono + out.extend_from_slice(&rate.to_le_bytes()); + out.extend_from_slice(&(rate * 2).to_le_bytes()); + out.extend_from_slice(&2u16.to_le_bytes()); + out.extend_from_slice(&16u16.to_le_bytes()); + out.extend_from_slice(b"data"); + out.extend_from_slice(&data_len.to_le_bytes()); + for &s in samples { + let v = (s.clamp(-1.0, 1.0) * 32767.0) as i16; + out.extend_from_slice(&v.to_le_bytes()); + } + out +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..6c43ed5 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,553 @@ +// Benutzeroberfläche im Canvas: CP437-Fenster mit DINOBYTE-Schrift. + +use crate::game::*; +use crate::gfx::*; +use crate::map::*; +use crate::render::{minimap_color, View}; +use crate::sound::SoundBank; +use macroquad::prelude::*; + +pub const S: f32 = 2.0; // UI-Skalierung der Schrift +pub const CW: f32 = GW * S; // Zeichenbreite in px +pub const CH: f32 = GH * S; // Zeichenhöhe in px + +pub const FRAME: Color = Color::new(0.62, 0.52, 0.36, 1.0); +pub const FRAME_HI: Color = Color::new(0.95, 0.8, 0.4, 1.0); +pub const BG: Color = Color::new(0.06, 0.04, 0.05, 0.92); +pub const TXT: Color = Color::new(0.9, 0.86, 0.78, 1.0); +pub const DIM: Color = Color::new(0.6, 0.56, 0.5, 1.0); +pub const GOLD: Color = Color::new(1.0, 0.84, 0.25, 1.0); +pub const MANA: Color = Color::new(0.7, 0.45, 1.0, 1.0); +pub const RED: Color = Color::new(0.95, 0.3, 0.3, 1.0); +pub const GREEN: Color = Color::new(0.45, 0.9, 0.45, 1.0); +pub const BLUE: Color = Color::new(0.5, 0.75, 1.0, 1.0); + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Tool { + Dig, + Hand, + Room(usize), + Sell, + Flag, + Lightning, + Heal, + Summon, +} + +pub const TOOLS: [Tool; 12] = [ + Tool::Dig, + Tool::Hand, + Tool::Room(0), + Tool::Room(1), + Tool::Room(2), + Tool::Room(3), + Tool::Room(4), + Tool::Sell, + Tool::Flag, + Tool::Lightning, + Tool::Heal, + Tool::Summon, +]; + +pub const TOOL_KEYS: [&str; 12] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "Q", "E"]; + +pub struct ToolInfo { + pub label: &'static str, + pub name: &'static str, + pub desc: &'static str, + pub icon: u32, +} + +pub fn tool_info(t: Tool) -> ToolInfo { + let i = |label, name, desc, icon| ToolInfo { label, name, desc, icon }; + match t { + Tool::Dig => i("Graben", "Graben", "Erde und Gold markieren. Ziehen für Rechteck; auf markiertem Feld beginnen hebt Markierung auf.", I_PICK), + Tool::Hand => i("Hand", "Hand", "Eigene Einheit aufnehmen (max. 8). Rechtsklick setzt sie auf eigenem Gebiet ab.", I_HAND), + Tool::Room(0) => i("Schatz", "Schatzkammer", "Lagert Gold: +500 Kapazität pro Feld.", T_TREASURY), + Tool::Room(1) => i("Quartier", "Quartier", "Ein Schlafplatz pro Feld. Bestimmt, wie viele Kreaturen kommen.", T_LAIR), + Tool::Room(2) => i("Pilze", "Pilzgarten", "Nahrung für Kreaturen. Ab 6 Feldern kommen Höhlenspinnen.", T_FARM), + Tool::Room(3) => i("Training", "Trainingshalle", "Kreaturen steigen Stufen auf (kostet Gold). Ab 9 Feldern kommen Steinoger.", T_TRAINING), + Tool::Room(4) => i("Bücher", "Bibliothek", "Kreaturen erzeugen Mana. Ab 6 Feldern kommen Schattenmagier.", T_LIBRARY), + Tool::Sell => i("Verkauf", "Verkaufen", "Räume zurück in Boden verwandeln (50 % Erstattung).", I_SELL), + Tool::Flag => i("Flagge", "Angriffsflagge", "Kampfbereite Kreaturen sammeln sich hier. Nahe am Heldentor greifen sie es an. Erneut klicken entfernt sie.", I_FLAG), + Tool::Lightning => i("Blitz", "Blitz", "55 Schaden an Helden im Umkreis.", I_BOLT), + Tool::Heal => i("Heilung", "Heilung", "Heilt eigene Einheiten im Umkreis um 50 %.", I_HEAL), + Tool::Summon => i("Wicht", "Grubenwicht beschwören", "Erschafft einen neuen Grubenwicht auf eigenem Gebiet.", I_SUMMON), + Tool::Room(_) => i("?", "?", "", 0), + } +} + +pub fn tool_cost(t: Tool, g: &Game) -> String { + match t { + Tool::Room(r) => format!("{} Gold/Feld", ROOM_COST[r]), + Tool::Lightning => "50 Mana".into(), + Tool::Heal => "60 Mana".into(), + Tool::Summon => format!("{} Mana", g.summon_cost() as i32), + _ => String::new(), + } +} + +pub enum UiAction { + None, + SelectTool(Tool), + MinimapJump(f32, f32), + CloseHelp, + NewGame, + Consumed, +} + +pub struct Ui { + pub tool: Tool, + pub show_help: bool, + pub log: Vec<(String, f64)>, + pub rects: Vec, + minimap_img: Image, + minimap_tex: Texture2D, + minimap_t: f64, + minimap_rect: Rect, + tool_rects: Vec, + end_button: Rect, +} + +fn wrap(s: &str, cols: usize) -> Vec { + let mut out = Vec::new(); + let mut line = String::new(); + for word in s.split(' ') { + let len = line.chars().count(); + if len > 0 && len + 1 + word.chars().count() > cols { + out.push(std::mem::take(&mut line)); + } + if !line.is_empty() { + line.push(' '); + } + line.push_str(word); + } + if !line.is_empty() { + out.push(line); + } + out +} + +fn bar(x: f32, y: f32, w: f32, h: f32, frac: f32, col: Color) { + draw_rectangle(x - 1.0, y - 1.0, w + 2.0, h + 2.0, Color::new(0.0, 0.0, 0.0, 1.0)); + draw_rectangle(x, y, w, h, Color::new(col.r * 0.25, col.g * 0.25, col.b * 0.25, 1.0)); + draw_rectangle(x, y, w * frac.clamp(0.0, 1.0), h, col); + draw_rectangle(x, y, w * frac.clamp(0.0, 1.0), (h * 0.3).max(1.0), Color::new(1.0, 1.0, 1.0, 0.25)); +} + +fn mmss(t: f32) -> String { + let t = t.max(0.0) as i32; + format!("{}:{:02}", t / 60, t % 60) +} + +impl Ui { + pub fn new() -> Ui { + let img = Image::gen_image_color(MW as u16, MH as u16, BLACK); + let tex = Texture2D::from_image(&img); + tex.set_filter(FilterMode::Nearest); + Ui { + tool: Tool::Dig, + show_help: true, + log: Vec::new(), + rects: Vec::new(), + minimap_img: img, + minimap_tex: tex, + minimap_t: -1.0, + minimap_rect: Rect::new(0.0, 0.0, 0.0, 0.0), + tool_rects: Vec::new(), + end_button: Rect::new(0.0, 0.0, 0.0, 0.0), + } + } + + pub fn over_ui(&self, x: f32, y: f32) -> bool { + self.show_help || self.rects.iter().any(|r| r.contains(vec2(x, y))) + } + + pub fn minimap_contains(&self, x: f32, y: f32) -> bool { + self.minimap_rect.contains(vec2(x, y)) + } + + pub fn minimap_world(&self, x: f32, y: f32) -> (f32, f32) { + let r = self.minimap_rect; + ((x - r.x) / r.w * MW as f32, (y - r.y) / r.h * MH as f32) + } + + pub fn click(&mut self, x: f32, y: f32, g: &Game) -> UiAction { + let p = vec2(x, y); + if g.state != 0 { + if self.end_button.contains(p) { + return UiAction::NewGame; + } + return UiAction::Consumed; + } + if self.show_help { + return UiAction::CloseHelp; + } + for (i, r) in self.tool_rects.iter().enumerate() { + if r.contains(p) { + return UiAction::SelectTool(TOOLS[i]); + } + } + if self.minimap_rect.contains(p) { + let (wx, wy) = self.minimap_world(x, y); + return UiAction::MinimapJump(wx, wy); + } + if self.over_ui(x, y) { + return UiAction::Consumed; + } + UiAction::None + } + + pub fn draw(&mut self, g: &Game, gfx: &Gfx, view: &View, snd: &SoundBank, info: &[(String, Color)], now: f64) { + self.rects.clear(); + let sw = screen_width(); + let sh = screen_height(); + let (mx, my) = mouse_position(); + + // ---------------- obere Leiste + let cols = (sw / CW).ceil() as i32; + gfx.window(0.0, 0.0, cols, 3, S, false, FRAME, BG); + self.rects.push(Rect::new(0.0, 0.0, sw, 3.0 * CH)); + let y = CH; + let mut x = CW * 2.0; + let seg = |label: &str, lc: Color, value: &str, vc: Color, x: &mut f32| { + gfx.text(label, *x, y, S, lc); + *x += gfx.measure(label, S) + CW; + gfx.text(value, *x, y, S, vc); + *x += gfx.measure(value, S) + CW * 2.0; + }; + seg("Gold", GOLD, &format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut x); + gfx.text("Mana", x, y, S, MANA); + x += gfx.measure("Mana", S) + CW * 0.5; + bar(x, y + 3.0, 70.0, 10.0, g.mana / MANA_MAX, MANA); + x += 70.0 + CW * 0.5; + gfx.text(&format!("{}", g.mana as i32), x, y, S, TXT); + x += CW * 5.0; + gfx.text("Kern", x, y, S, RED); + x += gfx.measure("Kern", S) + CW * 0.5; + bar(x, y + 3.0, 70.0, 10.0, g.core_hp / CORE_HP, RED); + x += 70.0 + CW * 1.5; + gfx.text("Tor", x, y, S, BLUE); + x += gfx.measure("Tor", S) + CW * 0.5; + bar(x, y + 3.0, 70.0, 10.0, g.gate_hp / GATE_HP, BLUE); + x += 70.0 + CW * 1.5; + let cap = g.counts[1].min(30); + seg("Kreaturen", TXT, &format!("{}/{}", g.creature_count(), cap), TXT, &mut x); + seg("Wichte", TXT, &format!("{}", g.worker_count()), TXT, &mut x); + let wave_col = if g.wave_timer < 30.0 { RED } else { TXT }; + seg(&format!("Welle {}", g.wave + 1), wave_col, &mmss(g.wave_timer), wave_col, &mut x); + seg("Armee", TXT, &format!("{}", g.army_value() as i32), TXT, &mut x); + let vol = if snd.muted { "aus".to_string() } else { format!("{}%", (snd.volume * 100.0).round() as i32) }; + seg("♪", DIM, &vol, DIM, &mut x); + + // ---------------- Werkzeugleiste + let tb_rows = 5; + let tb_y = sh - tb_rows as f32 * CH; + gfx.window(0.0, tb_y, cols, tb_rows, S, false, FRAME, BG); + self.rects.push(Rect::new(0.0, tb_y, sw, tb_rows as f32 * CH)); + self.tool_rects.clear(); + let bw = 80.0; + let mut hover_tool = None; + for (i, &t) in TOOLS.iter().enumerate() { + let bx = CW + i as f32 * (bw + 4.0); + let by = tb_y + CH * 0.75; + let r = Rect::new(bx, by, bw, CH * 3.5); + self.tool_rects.push(r); + let hovered = r.contains(vec2(mx, my)); + if hovered { + hover_tool = Some(i); + } + let sel = self.tool == t; + draw_rectangle(r.x, r.y, r.w, r.h, if sel { Color::new(0.35, 0.22, 0.1, 1.0) } else if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) }); + draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if sel { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) }); + let ti = tool_info(t); + gfx.draw_cell(ti.icon, bx + (bw - 32.0) * 0.5, by + 4.0, 32.0, 32.0, WHITE); + gfx.text(TOOL_KEYS[i], bx + 3.0, by + 3.0, 1.0, if sel { FRAME_HI } else { DIM }); + let lw = gfx.measure(ti.label, 1.0); + gfx.text(ti.label, bx + (bw - lw) * 0.5, by + 42.0, 1.0, if sel { FRAME_HI } else { TXT }); + } + // Hand + let hx = CW + TOOLS.len() as f32 * (bw + 4.0) + CW; + if hx + 120.0 < sw - 200.0 { + gfx.text("Hand", hx, tb_y + CH, S, TXT); + for (k, id) in g.hand.iter().enumerate() { + if let Some(e) = g.ents.iter().find(|e| e.id == *id) { + let sx = hx + (k % 4) as f32 * 26.0; + let sy = tb_y + CH * 2.2 + (k / 4) as f32 * 24.0; + draw_texture_ex( + &gfx.sprites, + sx, + sy, + WHITE, + DrawTextureParams { dest_size: Some(vec2(20.0, 20.0)), source: Some(Gfx::sprite_rect(e.kind, false)), ..Default::default() }, + ); + } + } + if g.hand.is_empty() { + gfx.text("leer", hx, tb_y + CH * 2.5, 1.0, DIM); + } + } + + // ---------------- Minikarte + let mm_cols = 16; + let mm_rows = 12; + let mm_w = mm_cols as f32 * CW; + let mm_h = mm_rows as f32 * CH; + let mm_x = sw - mm_w; + let mm_y = tb_y - mm_h; + gfx.window_titled(mm_x, mm_y, mm_cols, mm_rows, S, false, FRAME, BG, "Karte", FRAME_HI); + self.rects.push(Rect::new(mm_x, mm_y, mm_w, mm_h)); + if now - self.minimap_t > 0.25 { + self.minimap_t = now; + for (i, t) in g.map.t.iter().enumerate() { + let (x, y) = xy(i); + let mut c = minimap_color(t); + if t.marked { + c = [255, 230, 90, 255]; + } + self.minimap_img.set_pixel(x as u32, y as u32, Color::from_rgba(c[0], c[1], c[2], c[3])); + } + self.minimap_tex.update(&self.minimap_img); + } + let map_px = (mm_w - CW * 2.0).min(mm_h - CH * 2.0); + let mrx = mm_x + (mm_w - map_px) * 0.5; + let mry = mm_y + CH + (mm_h - CH * 2.0 - map_px) * 0.5; + self.minimap_rect = Rect::new(mrx, mry, map_px, map_px); + draw_texture_ex(&self.minimap_tex, mrx, mry, WHITE, DrawTextureParams { dest_size: Some(vec2(map_px, map_px)), ..Default::default() }); + let ms = map_px / MW as f32; + for e in &g.ents { + if e.alive && !e.held { + let c = if e.hero { Color::new(0.6, 0.85, 1.0, 1.0) } else if e.kind == EK::Worker { Color::new(1.0, 0.6, 0.3, 1.0) } else { Color::new(1.0, 0.25, 0.25, 1.0) }; + draw_rectangle(mrx + e.x * ms - 1.0, mry + e.y * ms - 1.0, 3.0, 3.0, c); + } + } + if let Some(f) = g.flag { + 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); + + // ---------------- Infofeld + if !info.is_empty() { + let icols = 30; + let mut lines: Vec<(String, Color)> = Vec::new(); + for (s, c) in info.iter().skip(1) { + for l in wrap(s, icols as usize - 4) { + lines.push((l, *c)); + } + } + let irows = lines.len() as i32 + 2; + let ix = sw - icols as f32 * CW; + let iy = 3.0 * CH; + gfx.window_titled(ix, iy, icols, irows, S, false, FRAME, BG, &info[0].0, info[0].1); + for (k, (l, c)) in lines.iter().enumerate() { + gfx.text(l, ix + CW * 2.0, iy + CH * (k as f32 + 1.0), S, *c); + } + } + + // ---------------- Meldungen + let recent: Vec<&(String, f64)> = self.log.iter().filter(|(_, t)| now - t < 14.0).collect(); + if !recent.is_empty() { + let mcols = (((sw - mm_w) / CW) as i32 - 2).clamp(20, 70); + let mut lines: Vec<(String, f32)> = Vec::new(); + for (s, t) in recent.iter().rev().take(5).rev() { + let a = (1.0 - ((now - t) as f32 - 11.0) / 3.0).clamp(0.2, 1.0); + for l in wrap(s, mcols as usize - 4) { + lines.push((l, a)); + } + } + let mrows = lines.len() as i32 + 2; + let my0 = tb_y - mrows as f32 * CH; + gfx.window(0.0, my0, mcols, mrows, S, false, FRAME, Color::new(0.06, 0.04, 0.05, 0.75)); + self.rects.push(Rect::new(0.0, my0, mcols as f32 * CW, mrows as f32 * CH)); + for (k, (l, a)) in lines.iter().enumerate() { + gfx.text(l, CW * 2.0, my0 + CH * (k as f32 + 1.0), S, Color::new(TXT.r, TXT.g, TXT.b, *a)); + } + } + if self.log.len() > 100 { + self.log.drain(0..50); + } + + // ---------------- Tooltip + if let Some(i) = hover_tool { + let t = TOOLS[i]; + let ti = tool_info(t); + let tcols = 34; + let mut lines: Vec<(String, Color)> = wrap(ti.desc, tcols - 4).into_iter().map(|l| (l, TXT)).collect(); + let cost = tool_cost(t, g); + if !cost.is_empty() { + lines.push((format!("Kosten: {}", cost), GOLD)); + } + lines.push((format!("Taste: {}", TOOL_KEYS[i]), DIM)); + let rows = lines.len() as i32 + 2; + let tx = (mx - tcols as f32 * CW * 0.5).clamp(0.0, sw - tcols as f32 * CW); + let ty = tb_y - rows as f32 * CH - 4.0; + gfx.window_titled(tx, ty, tcols as i32, rows, S, false, FRAME_HI, BG, ti.name, FRAME_HI); + for (k, (l, c)) in lines.iter().enumerate() { + gfx.text(l, tx + CW * 2.0, ty + CH * (k as f32 + 1.0), S, *c); + } + } + + // ---------------- Dialoge + if g.state != 0 { + self.draw_end(g, gfx); + } else if self.show_help { + self.draw_help(gfx); + } else if g.paused { + let w = 24; + let x = (sw - w as f32 * CW) * 0.5; + let y = sh * 0.35; + gfx.window(x, y, w, 5, S, true, FRAME_HI, BG); + let t = "PAUSE"; + gfx.text(t, (sw - gfx.measure(t, 3.0)) * 0.5, y + CH, 3.0, FRAME_HI); + let t2 = "P: weiter"; + gfx.text(t2, (sw - gfx.measure(t2, S)) * 0.5, y + CH * 3.0, S, DIM); + } + } + + fn draw_help(&mut self, gfx: &Gfx) { + let lines: &[(&str, Color)] = &[ + ("Ziel: Zerstöre das Heldentor und schütze deinen Seelenkern.", GOLD), + ("", TXT), + ("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), + ("Mausrad Zoomen", TXT), + ("1-9, 0, Q, E Werkzeug wählen", TXT), + ("P / Leertaste Pause M Ton an/aus +/- Lautstärke", TXT), + ("H Diese Hilfe", TXT), + ("", TXT), + ("1. Grabe zum Portal und lass es von Grubenwichten erobern.", TXT), + ("2. Baue Quartier, Pilzgarten und Schatzkammer.", TXT), + ("3. Trainiere deine Kreaturen, zahle ihren Lohn pünktlich.", TXT), + ("4. Wehre die Heldenwellen ab. Sie passen sich deiner Armee an.", TXT), + ("5. Setze die Angriffsflagge nahe ans Tor und greife an!", TXT), + ("", TXT), + ("Schrift: DINOBYTE von mby (CC BY 4.0)", DIM), + ("", TXT), + ("Klick zum Starten", FRAME_HI), + ]; + let cols = 74; + let rows = lines.len() as i32 + 4; + let sw = screen_width(); + let sh = screen_height(); + let x = ((sw - cols as f32 * CW) * 0.5).max(0.0); + let y = ((sh - rows as f32 * CH) * 0.5).max(0.0); + gfx.window_titled(x, y, cols, rows, S, true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), "DUNGEONCRAFT", FRAME_HI); + for (k, (l, c)) in lines.iter().enumerate() { + gfx.text(l, x + CW * 3.0, y + CH * (k as f32 + 2.0), S, *c); + } + self.rects.push(Rect::new(0.0, 0.0, sw, sh)); + } + + fn draw_end(&mut self, g: &Game, gfx: &Gfx) { + let sw = screen_width(); + let sh = screen_height(); + draw_rectangle(0.0, 0.0, sw, sh, Color::new(0.0, 0.0, 0.0, 0.5)); + let win = g.state == 1; + let cols = 44; + let rows = 11; + let x = (sw - cols as f32 * CW) * 0.5; + let y = (sh - rows as f32 * CH) * 0.5; + let col = if win { GOLD } else { RED }; + gfx.window(x, y, cols, rows, S, true, col, Color::new(0.05, 0.03, 0.04, 0.97)); + let title = if win { "SIEG!" } else { "NIEDERLAGE" }; + gfx.text(title, (sw - gfx.measure(title, 4.0)) * 0.5, y + CH * 1.5, 4.0, col); + let sub = if win { "Das Heldentor ist gefallen." } else { "Dein Seelenkern ist zerbrochen." }; + gfx.text(sub, (sw - gfx.measure(sub, S)) * 0.5, y + CH * 4.5, S, TXT); + let st = format!("Spielzeit {} Wellen {} Armee {}", mmss(g.time), g.wave, g.army_value() as i32); + gfx.text(&st, (sw - gfx.measure(&st, S)) * 0.5, y + CH * 6.0, S, DIM); + let b = " Neues Spiel (N) "; + let bw = gfx.measure(b, S) + CW * 2.0; + let bx = (sw - bw) * 0.5; + let by = y + CH * 7.75; + self.end_button = Rect::new(bx, by, bw, CH * 2.0); + let (mx, my) = mouse_position(); + let hov = self.end_button.contains(vec2(mx, my)); + draw_rectangle(bx, by, bw, CH * 2.0, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) }); + draw_rectangle_lines(bx, by, bw, CH * 2.0, 2.0, col); + gfx.text(b, bx + CW, by + CH * 0.5, S, TXT); + self.rects.push(Rect::new(0.0, 0.0, sw, sh)); + } +} + +// 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)> { + let mut out = Vec::new(); + if let Some(j) = g.ent_at(wx, wy, false) { + 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 })); + out.push((format!("HP {}/{}", e.hp.max(0.0) as i32, e.max_hp as i32), GREEN)); + out.push((job_name(e).to_string(), TXT)); + if e.hero { + out.push((if e.guard { "Torwächter".into() } else { "Eindringling".into() }, BLUE)); + } else if e.creature() { + let pct = |v: f32| (v.clamp(0.0, 1.0) * 100.0).round() as i32; + out.push((format!("Energie {}% Hunger {}%", pct(e.energy), pct(e.hunger / 2.0)), TXT)); + let mood = if e.happy > 0.6 { + ("zufrieden", GREEN) + } else if e.happy > 0.25 { + ("mürrisch", GOLD) + } else { + ("wütend", RED) + }; + out.push((format!("Laune: {}", mood.0), mood.1)); + out.push((format!("Lohn: {} Gold", (s.wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)) as i32), GOLD)); + } else if e.carry > 0 { + out.push((format!("Trägt {} Gold", e.carry), GOLD)); + } + return out; + } + let (x, y) = (wx.floor() as i32, wy.floor() as i32); + let Some(t) = g.map.get(x, y) else { return out }; + let owner = match t.owner { + Owner::Player => "erobert", + Owner::Hero => "Heldengebiet", + Owner::None => "neutral", + }; + match t.kind { + Kind::Rock => out.push(("Fels".into(), DIM)), + Kind::Earth => out.push(("Erde".into(), FRAME_HI)), + Kind::Gold => { + out.push(("Goldader".into(), GOLD)); + out.push((format!("{} Gold", t.gold), GOLD)); + } + Kind::Floor => { + out.push(("Boden".into(), TXT)); + out.push((owner.into(), DIM)); + } + Kind::Core => { + out.push(("Seelenkern".into(), RED)); + out.push((format!("HP {}/{}", g.core_hp as i32, CORE_HP as i32), RED)); + } + Kind::Portal => { + out.push(("Portal".into(), MANA)); + out.push((if g.portal_owned() { "erobert".into() } else { "neutral: Wichte können es erobern".into() }, DIM)); + } + Kind::Gate => { + out.push(("Heldentor".into(), BLUE)); + out.push((format!("HP {}/{}", g.gate_hp as i32, GATE_HP as i32), BLUE)); + } + k => { + if let Some(r) = room_index(k) { + out.push((ROOM_NAMES[r].into(), FRAME_HI)); + match k { + Kind::Farm => out.push((format!("{} Pilze", t.food as i32), GREEN)), + Kind::Lair => out.push((if t.bed != 0 { "Schlafplatz belegt".into() } else { "Schlafplatz frei".into() }, DIM)), + _ => {} + } + } + } + } + if t.kind == Kind::Rock { + out.push(("unzerstörbar".into(), DIM)); + } + if t.marked { + out.push(("zum Graben markiert".into(), GOLD)); + } + out +} diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..2109efb --- /dev/null +++ b/www/index.html @@ -0,0 +1,20 @@ + + + + + Dungeoncraft + + + + + + + + +