Renderer, Oberfläche, Eingabe, Sound-Modul, Simulationstest und Docker-Setup (Port 18093)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
14e481cffb
commit
db417b3af8
13 files changed
+1930
-19
No files matched your search
+80
-4
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+275
@@ -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
|
||||
}
|
||||
}
|
||||
+2
-9
@@ -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<Tile>, rng: &mut Rng, cx: i32, cy: i32, r: i32, k: Kind| {
|
||||
let blob = |t: &mut Vec<Tile>, 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<Tile>, x0: i32, y0: i32, x1: i32, y1: i32, owner: Owner| {
|
||||
let room = |t: &mut Vec<Tile>, 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<Vec<usize>> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
+455
@@ -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<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],
|
||||
}
|
||||
}
|
||||
@@ -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<Option<Sound>>,
|
||||
last: Vec<f64>,
|
||||
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<u8> {
|
||||
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
|
||||
}
|
||||
@@ -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<Rect>,
|
||||
minimap_img: Image,
|
||||
minimap_tex: Texture2D,
|
||||
minimap_t: f64,
|
||||
minimap_rect: Rect,
|
||||
tool_rects: Vec<Rect>,
|
||||
end_button: Rect,
|
||||
}
|
||||
|
||||
fn wrap(s: &str, cols: usize) -> Vec<String> {
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user