From a569aff7a8630dfa8fa79a3ca44d39a0e6361b69 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Mon, 2 Nov 2026 21:49:10 +0100 Subject: [PATCH] =?UTF-8?q?Projektger=C3=BCst,=20Karte/Wegfindung=20und=20?= =?UTF-8?q?Spiellogik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .gitignore | 2 + Cargo.toml | 14 + build.sh | 6 + src/game.rs | 1436 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/map.rs | 334 ++++++++++++ 5 files changed, 1792 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100755 build.sh create mode 100644 src/game.rs create mode 100644 src/map.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..407892e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock.bak diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9aae1b7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "unterreich" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +path = "src/lib.rs" + +[profile.release] +opt-level = 3 +lto = true +panic = "abort" +codegen-units = 1 diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..0c0c0ca --- /dev/null +++ b/build.sh @@ -0,0 +1,6 @@ +#!/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/src/game.rs b/src/game.rs new file mode 100644 index 0000000..9a685c1 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,1436 @@ +// Spiellogik: Einheiten, KI, Wirtschaft, Wellen. + +use crate::map::*; + +pub const WAVES: u32 = 6; +pub const CORE_HP: f32 = 2000.0; +pub const PAYDAY: f32 = 150.0; +pub const FIRST_WAVE: f32 = 300.0; +pub const WAVE_GAP: f32 = 210.0; +pub const MANA_MAX: f32 = 400.0; +pub const CARRY_MAX: i32 = 300; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum EK { + Worker, + Kobold, + Spider, + Mage, + Ogre, + Knight, + Archer, + Priest, + Dwarf, + Paladin, +} + +pub struct St { + pub name: &'static str, + pub hp: f32, + pub atk: f32, + pub range: f32, + pub speed: f32, + pub interval: f32, + pub wage: i32, + pub bounty: i32, +} + +pub fn st(k: EK) -> St { + let s = |name, hp, atk, range, speed, interval, wage, bounty| St { name, hp, atk, range, speed, interval, wage, bounty }; + match k { + EK::Worker => s("Grubenwicht", 45.0, 3.0, 1.0, 3.2, 1.0, 0, 0), + EK::Kobold => s("Kobold", 75.0, 8.0, 1.0, 2.6, 0.9, 40, 0), + EK::Spider => s("Höhlenspinne", 95.0, 10.0, 1.0, 2.9, 1.0, 60, 0), + EK::Mage => s("Schattenmagier", 60.0, 13.0, 4.5, 2.0, 1.4, 110, 0), + EK::Ogre => s("Steinoger", 230.0, 20.0, 1.0, 1.8, 1.4, 150, 0), + EK::Knight => s("Ritter", 140.0, 11.0, 1.0, 1.9, 1.1, 0, 120), + EK::Archer => s("Bogenschützin", 75.0, 9.0, 4.5, 2.1, 1.3, 0, 100), + EK::Priest => s("Priester", 80.0, 5.0, 3.5, 1.9, 1.5, 0, 120), + EK::Dwarf => s("Zwergenhauer", 110.0, 9.0, 1.0, 2.0, 1.0, 0, 100), + EK::Paladin => s("Lichtpaladin", 700.0, 26.0, 1.0, 1.7, 1.2, 0, 1000), + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Job { + None, + Dig(usize), + Claim(usize), + Deliver, + Wander, + Sleep(usize), + Eat(usize), + Train(usize), + Study(usize), + Attack(u32), + AttackCore, + Leave, +} + +#[derive(Clone)] +pub struct Ent { + pub id: u32, + pub kind: EK, + pub hero: bool, + pub x: f32, + pub y: f32, + pub hp: f32, + pub max_hp: f32, + pub level: u32, + pub xp: f32, + pub job: Job, + pub path: Vec, + pub timer: f32, + pub cd: f32, + pub think: f32, + pub repath: f32, + pub carry: i32, + pub hunger: f32, + pub energy: f32, + pub happy: f32, + pub bed: Option, + pub alive: bool, + pub held: bool, + pub anim: f32, + pub flash: f32, + pub face: f32, + pub moving: bool, + pub working: bool, +} + +impl Ent { + fn dummy() -> Ent { + Ent { + id: 0, + kind: EK::Worker, + hero: false, + x: 0.0, + y: 0.0, + hp: 0.0, + max_hp: 1.0, + level: 1, + xp: 0.0, + job: Job::None, + path: Vec::new(), + timer: 0.0, + cd: 0.0, + think: 0.0, + repath: 0.0, + carry: 0, + hunger: 0.0, + energy: 1.0, + happy: 1.0, + bed: None, + alive: false, + held: false, + anim: 0.0, + flash: 0.0, + face: 1.0, + moving: false, + working: false, + } + } + pub fn tile(&self) -> usize { + idx((self.x as i32).clamp(0, MW as i32 - 1), (self.y as i32).clamp(0, MH as i32 - 1)) + } + pub fn atk(&self) -> f32 { + st(self.kind).atk * (1.0 + 0.12 * (self.level - 1) as f32) + } + pub fn creature(&self) -> bool { + !self.hero && self.kind != EK::Worker + } +} + +#[derive(Clone, Copy)] +pub struct Fx { + pub kind: u8, // 0 Geschoss, 1 Blitz, 2 Treffer, 3 Rauch, 4 Heilung, 5 Gold, 6 Staub, 7 Stufenaufstieg, 8 Einnahme + pub x: f32, + pub y: f32, + pub x2: f32, + pub y2: f32, + pub t: f32, + pub max: f32, + pub col: u32, +} + +pub struct Game { + pub map: Map, + pub ents: Vec, + pub next_id: u32, + pub rng: Rng, + pub gold: f32, + pub mana: f32, + pub core_hp: f32, + pub time: f32, + pub wave: u32, + pub wave_timer: f32, + pub warned: bool, + pub portal_timer: f32, + pub payday: f32, + pub fx: Vec, + pub msgs: Vec, + pub state: u8, // 0 läuft, 1 Sieg, 2 Niederlage + pub paused: bool, + pub hand: Vec, + pub counts: [usize; 5], // Schatzkammer, Quartier, Pilzgarten, Trainingshalle, Bibliothek + hint_t: [f32; 16], +} + +pub const ROOM_KINDS: [Kind; 5] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library]; +pub const ROOM_COST: [i32; 5] = [60, 80, 100, 140, 160]; +pub const ROOM_NAMES: [&str; 5] = ["Schatzkammer", "Quartier", "Pilzgarten", "Trainingshalle", "Bibliothek"]; + +pub fn room_index(k: Kind) -> Option { + ROOM_KINDS.iter().position(|&r| r == k) +} + +fn adj4(a: usize, b: usize) -> bool { + let (ax, ay) = xy(a); + let (bx, by) = xy(b); + (ax - bx).abs() + (ay - by).abs() == 1 +} + +fn sgn(v: f32) -> f32 { + if v < 0.0 { + -1.0 + } else { + 1.0 + } +} + +impl Game { + pub fn new(seed: u32) -> Game { + let map = Map::generate(seed); + let mut g = Game { + map, + ents: Vec::new(), + next_id: 1, + rng: Rng(seed.wrapping_mul(2654435761) | 1), + gold: 1500.0, + mana: 100.0, + core_hp: CORE_HP, + time: 0.0, + wave: 0, + wave_timer: FIRST_WAVE, + warned: false, + portal_timer: 10.0, + payday: PAYDAY, + fx: Vec::new(), + msgs: Vec::new(), + state: 0, + paused: false, + hand: Vec::new(), + counts: [0; 5], + hint_t: [-1000.0; 16], + }; + let (cx, cy) = g.map.core; + for k in 0..4 { + g.spawn(EK::Worker, false, cx as f32 - 0.5, cy as f32 + k as f32 * 0.7 + 0.3, 1); + } + g.recount(); + g.msg("Willkommen, Herrscher des Unterreichs! Beschütze deinen Seelenkern.".into()); + g.msg("Markiere Erde und Goldadern mit dem Werkzeug »Graben« (Taste 1). Deine Grubenwichte erledigen den Rest.".into()); + g.msg("Grabe dich zum Portal durch und erobere es. Kreaturen brauchen ein Quartier und einen Pilzgarten.".into()); + g + } + + pub fn msg(&mut self, s: String) { + self.msgs.push(s); + if self.msgs.len() > 50 { + self.msgs.remove(0); + } + } + + fn hint(&mut self, key: usize, s: &str) { + if self.time - self.hint_t[key] > 30.0 { + self.hint_t[key] = self.time; + self.msg(s.into()); + } + } + + pub fn recount(&mut self) { + let mut c = [0usize; 5]; + for t in &self.map.t { + if t.owner == Owner::Player { + if let Some(r) = room_index(t.kind) { + c[r] += 1; + } + } + } + self.counts = c; + } + + pub fn gold_cap(&self) -> f32 { + 2000.0 + 500.0 * self.counts[0] as f32 + } + + pub fn add_gold(&mut self, amount: f32) -> f32 { + let cap = self.gold_cap(); + let acc = amount.min((cap - self.gold).max(0.0)); + self.gold += acc; + acc + } + + pub fn portal_owned(&self) -> bool { + let (px, py) = self.map.portal; + self.map.t[idx(px, py)].owner == Owner::Player + } + + pub fn creature_count(&self) -> usize { + self.ents.iter().filter(|e| e.alive && e.creature()).count() + } + 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) + } + + pub fn spawn(&mut self, kind: EK, hero: bool, x: f32, y: f32, level: u32) -> u32 { + let s = st(kind); + let id = self.next_id; + self.next_id += 1; + let mut e = Ent::dummy(); + e.id = id; + e.kind = kind; + e.hero = hero; + e.x = x; + e.y = y; + e.level = level; + e.max_hp = s.hp * (1.0 + 0.15 * (level - 1) as f32); + e.hp = e.max_hp; + e.alive = true; + e.think = self.rng.f(); + e.energy = 0.7 + self.rng.f() * 0.3; + e.hunger = self.rng.f() * 0.3; + e.anim = self.rng.f() * 10.0; + self.ents.push(e); + id + } + + pub fn fx(&mut self, kind: u8, x: f32, y: f32, x2: f32, y2: f32, max: f32, col: u32) { + if self.fx.len() < 400 { + self.fx.push(Fx { kind, x, y, x2, y2, t: 0.0, max, col }); + } + } + + pub fn find(&self, id: u32) -> Option { + self.ents.iter().position(|e| e.id == id && e.alive && !e.held) + } + + // ---------------------------------------------------------------- Update + + pub fn update(&mut self, dt: f32) { + let dt = dt.min(0.1); + if self.state == 0 && !self.paused { + self.step(dt); + } + for f in self.fx.iter_mut() { + f.t += dt; + } + self.fx.retain(|f| f.t < f.max); + } + + fn step(&mut self, dt: f32) { + self.time += dt; + self.recount(); + self.mana = (self.mana + 1.0 * dt).min(MANA_MAX); + self.core_hp = (self.core_hp + 0.5 * dt).min(CORE_HP); + for t in self.map.t.iter_mut() { + if t.kind == Kind::Farm { + t.food = (t.food + dt / 18.0).min(3.0); + } + } + + if self.portal_owned() { + self.portal_timer -= dt; + if self.portal_timer <= 0.0 { + self.portal_timer = 20.0 + self.rng.f() * 14.0; + self.try_portal(); + } + } + + self.payday -= dt; + if self.payday <= 0.0 { + self.payday = PAYDAY; + self.do_payday(); + } + + if self.wave < WAVES { + self.wave_timer -= dt; + if !self.warned && self.wave_timer < 30.0 { + self.warned = true; + self.msg(format!("Späher melden: Welle {} der Helden erreicht in 30 Sekunden das Tor!", self.wave + 1)); + } + if self.wave_timer <= 0.0 { + self.spawn_wave(); + self.wave += 1; + self.wave_timer = WAVE_GAP; + self.warned = false; + } + } + + for i in 0..self.ents.len() { + if !self.ents[i].alive || self.ents[i].held { + continue; + } + let mut e = std::mem::replace(&mut self.ents[i], Ent::dummy()); + self.update_ent(&mut e, dt); + self.ents[i] = e; + } + self.ents.retain(|e| e.alive); + + if self.core_hp <= 0.0 { + self.core_hp = 0.0; + self.state = 2; + self.msg("Dein Seelenkern ist zerbrochen. Das Unterreich ist verloren.".into()); + } else if self.wave >= WAVES && self.hero_count() == 0 { + self.state = 1; + self.msg("Alle Heldenwellen sind vernichtet. Das Unterreich gehört dir!".into()); + } + } + + fn try_portal(&mut self) { + let creatures = self.creature_count(); + let lair = self.counts[1]; + let farm = self.counts[2]; + let cap = lair.min(30); + if lair == 0 { + self.hint(0, "Das Portal ist offen, aber ohne Quartier kommen keine Kreaturen."); + return; + } + if farm == 0 { + self.hint(1, "Kreaturen wollen essen: Baue einen Pilzgarten."); + return; + } + if creatures >= cap { + self.hint(2, "Alle Quartierplätze sind belegt. Vergrößere dein Quartier."); + return; + } + let mut opts: Vec<(EK, u32)> = vec![(EK::Kobold, 4)]; + if farm >= 6 { + opts.push((EK::Spider, 3)); + } + if self.counts[4] >= 6 { + opts.push((EK::Mage, 3)); + } + if self.counts[3] >= 9 { + opts.push((EK::Ogre, 2)); + } + let total: u32 = opts.iter().map(|o| o.1).sum(); + let mut r = self.rng.next() % total; + let mut kind = EK::Kobold; + for (k, w) in opts { + if r < w { + kind = k; + break; + } + r -= w; + } + let (px, py) = self.map.portal; + self.spawn(kind, false, px as f32 + 1.5, py as f32 + 1.5, 1); + self.fx(3, px as f32 + 1.5, py as f32 + 1.5, 0.0, 0.0, 0.8, 0xffcc44ff); + self.msg(format!("Ein {} ist durch das Portal gekommen.", st(kind).name)); + } + + fn do_payday(&mut self) { + let mut paid = 0; + let mut unpaid = 0; + let mut total = 0.0; + for i in 0..self.ents.len() { + if !self.ents[i].alive || !self.ents[i].creature() { + continue; + } + let e = &self.ents[i]; + let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32); + if self.gold >= w { + self.gold -= w; + total += w; + paid += 1; + let e = &mut self.ents[i]; + e.happy = (e.happy + 0.2).min(1.0); + } else { + unpaid += 1; + self.ents[i].happy -= 0.45; + } + } + if paid + unpaid > 0 { + if unpaid > 0 { + self.msg(format!("Zahltag! {} Kreaturen bezahlt ({} Gold), {} gingen leer aus und sind wütend!", paid, total as i32, unpaid)); + } else { + self.msg(format!("Zahltag! {} Kreaturen erhielten {} Gold.", paid, total as i32)); + } + } + } + + fn spawn_wave(&mut self) { + use EK::*; + let w = self.wave + 1; + let list: Vec = match w { + 1 => vec![Knight, Knight, Archer], + 2 => vec![Knight, Knight, Archer, Archer, Dwarf], + 3 => vec![Knight, Knight, Knight, Archer, Archer, Priest, Dwarf], + 4 => vec![Knight, Knight, Knight, Knight, Archer, Archer, Archer, Priest, Dwarf, Dwarf], + 5 => vec![Knight, Knight, Knight, Knight, Knight, Archer, Archer, Archer, Archer, Priest, Priest, Dwarf, Dwarf], + _ => vec![Paladin, Knight, Knight, Knight, Knight, Archer, Archer, Archer, Archer, Priest, Priest, Dwarf], + }; + let (gx, gy) = self.map.gate; + let n = list.len(); + for k in list { + let x = gx as f32 + 0.5 + self.rng.f() * 2.0; + let y = gy as f32 + 0.5 + self.rng.f() * 2.0; + self.spawn(k, true, x, y, w); + } + self.fx(3, gx as f32 + 1.5, gy as f32 + 1.5, 0.0, 0.0, 1.0, 0xffffeecc); + if w == WAVES { + self.msg(format!("Letzte Welle: Der Lichtpaladin führt {} Helden in dein Reich!", n)); + } else { + self.msg(format!("Welle {}: {} Helden dringen durch das Tor!", w, n)); + } + } + + fn update_ent(&mut self, e: &mut Ent, dt: f32) { + e.anim += dt; + e.flash = (e.flash - dt).max(0.0); + e.cd -= dt; + e.think -= dt; + e.repath -= dt; + e.moving = false; + e.working = false; + let s = st(e.kind); + while e.level < 10 && e.xp >= 100.0 * e.level as f32 { + e.xp -= 100.0 * e.level as f32; + e.level += 1; + let old = e.max_hp; + e.max_hp = s.hp * (1.0 + 0.15 * (e.level - 1) as f32); + e.hp += e.max_hp - old; + self.fx(7, e.x, e.y, 0.0, 0.0, 0.8, 0xff44ddff); + if !e.hero && e.level >= 3 { + self.msg(format!("{} erreicht Stufe {}.", s.name, e.level)); + } + } + if e.hero { + self.hero_ai(e, dt); + } else if e.kind == EK::Worker { + self.worker_ai(e, dt); + } else { + self.creature_ai(e, dt); + } + } + + // Bewegt eine Einheit entlang ihres Pfads. true = noch unterwegs. + fn step_path(&mut self, e: &mut Ent, dt: f32) -> bool { + if e.path.is_empty() { + return false; + } + let next = e.path[0]; + let tile = self.map.t[next]; + if !tile.walkable() { + if e.hero && tile.diggable() { + // Helden graben sich durch + e.working = true; + let (tx, ty) = xy(next); + e.face = sgn(tx as f32 + 0.5 - e.x); + e.timer += dt; + if e.timer >= 0.5 { + e.timer = 0.0; + let pow = match e.kind { + EK::Dwarf => 3.0, + EK::Paladin => 2.0, + _ => 1.0, + }; + self.fx(6, tx as f32 + 0.5, ty as f32 + 0.5, 0.0, 0.0, 0.4, 0xff3a5a7a); + let t = &mut self.map.t[next]; + t.hp -= pow; + if t.hp <= 0.0 { + self.dig_out(next); + } + } + return true; + } + e.path.clear(); + return false; + } + let (tx, ty) = xy(next); + let (cx, cy) = (tx as f32 + 0.5, ty as f32 + 0.5); + let dx = cx - e.x; + let dy = cy - e.y; + let d = (dx * dx + dy * dy).sqrt(); + let mut sp = st(e.kind).speed * dt; + if !e.hero && e.creature() && e.hunger > 2.0 { + sp *= 0.8; + } + if d <= sp { + e.x = cx; + e.y = cy; + e.path.remove(0); + } else { + e.x += dx / d * sp; + e.y += dy / d * sp; + } + if dx.abs() > 0.01 { + e.face = sgn(dx); + } + e.moving = true; + true + } + + pub fn dig_out(&mut self, i: usize) { + let t = &mut self.map.t[i]; + t.kind = Kind::Floor; + t.owner = Owner::None; + t.marked = false; + t.reserved = 0; + t.hp = 0.0; + t.gold = 0; + let (x, y) = xy(i); + self.fx(3, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.6, 0xff304a66); + } + + fn unreserve(&mut self, me: u32) { + for t in self.map.t.iter_mut() { + if t.reserved == me { + t.reserved = 0; + } + } + } + + fn release_all(&mut self, id: u32) { + let me = id + 1; + for t in self.map.t.iter_mut() { + if t.reserved == me { + t.reserved = 0; + } + if t.bed == me { + t.bed = 0; + } + } + } + + fn los(&self, x0: f32, y0: f32, x1: f32, y1: f32) -> bool { + let dx = x1 - x0; + let dy = y1 - y0; + let d = (dx * dx + dy * dy).sqrt(); + let n = (d * 4.0) as i32 + 1; + for k in 1..n { + let f = k as f32 / n as f32; + let x = (x0 + dx * f) as i32; + let y = (y0 + dy * f) as i32; + if let Some(t) = self.map.get(x, y) { + if t.solid() || t.kind == Kind::Core { + return false; + } + } + } + true + } + + fn nearest_enemy(&self, e: &Ent, radius: f32) -> Option { + let mut best = None; + let mut bd = radius * radius; + for o in &self.ents { + if !o.alive || o.held || o.hero == e.hero { + continue; + } + let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2); + if d < bd { + bd = d; + best = Some(o.id); + } + } + best + } + + // Kampf gegen eine bestimmte Einheit. false = Ziel ungültig/unerreichbar. + fn attack_job(&mut self, e: &mut Ent, id: u32, dt: f32) -> bool { + let Some(j) = self.find(id) else { return false }; + let (tx, ty) = (self.ents[j].x, self.ents[j].y); + let d = ((tx - e.x).powi(2) + (ty - e.y).powi(2)).sqrt(); + let s = st(e.kind); + if d > 12.0 { + return false; + } + if d <= s.range + 0.45 && (s.range < 1.5 || self.los(e.x, e.y, tx, ty)) { + e.path.clear(); + e.face = sgn(tx - e.x); + e.working = true; + if e.cd <= 0.0 { + e.cd = s.interval; + self.hit(e, j); + } + return true; + } + if e.path.is_empty() || e.repath <= 0.0 { + e.repath = 0.6; + let tt = idx(tx as i32, ty as i32); + match self.map.path_to(e.tile(), tt) { + Some(p) => e.path = p, + None => return false, + } + } + if !self.step_path(e, dt) && e.tile() == idx(tx as i32, ty as i32) { + // gleiches Feld: direkt annähern + let dx = tx - e.x; + let dy = ty - e.y; + let l = (dx * dx + dy * dy).sqrt().max(0.001); + let sp = s.speed * dt; + e.x += dx / l * sp.min(l); + e.y += dy / l * sp.min(l); + } + true + } + + fn hit(&mut self, e: &mut Ent, j: usize) { + let dmg = e.atk() * (0.85 + self.rng.f() * 0.3); + let (tx, ty) = (self.ents[j].x, self.ents[j].y); + let s = st(e.kind); + if s.range > 1.5 { + let col = match e.kind { + EK::Mage => 0xffff44cc, + EK::Priest => 0xff88ffff, + _ => 0xff70b0d0, + }; + self.fx(0, e.x, e.y - 0.3, tx, ty - 0.3, 0.25, col); + } else { + self.fx(2, tx, ty - 0.2, 0.0, 0.0, 0.25, 0xffffffff); + } + e.xp += dmg * 0.6; + let my_id = e.id; + let t = &mut self.ents[j]; + t.hp -= dmg; + t.flash = 0.15; + if t.creature() && !matches!(t.job, Job::Attack(_)) { + t.job = Job::Attack(my_id); + t.path.clear(); + } + if t.hp <= 0.0 { + self.kill(j); + } + } + + fn kill(&mut self, j: usize) { + let e = &mut self.ents[j]; + e.alive = false; + let (id, kind, hero, x, y) = (e.id, e.kind, e.hero, e.x, e.y); + self.release_all(id); + self.fx(3, x, y, 0.0, 0.0, 0.7, 0xff505070); + let s = st(kind); + if hero { + let b = s.bounty as f32; + let got = self.add_gold(b); + self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff); + self.msg(format!("{} besiegt! Beute: {} Gold.", s.name, got as i32)); + } else { + self.msg(format!("Dein {} ist gefallen.", s.name)); + } + } + + // ---------------------------------------------------------------- Grubenwichte + + fn dig_neighbor(&self, i: usize, me: u32) -> Option { + let (x, y) = xy(i); + let mut found = None; + for &(dx, dy) in &DIR4 { + if let Some(t) = self.map.get(x + dx, y + dy) { + if t.marked && t.diggable() { + if t.reserved == me { + return Some(idx(x + dx, y + dy)); + } + if t.reserved == 0 && found.is_none() { + found = Some(idx(x + dx, y + dy)); + } + } + } + } + found + } + + fn claimable(&self, i: usize, me: u32) -> bool { + let t = &self.map.t[i]; + if t.owner != Owner::None || !matches!(t.kind, Kind::Floor | Kind::Portal) { + return false; + } + if t.reserved != 0 && t.reserved != me { + return false; + } + let (x, y) = xy(i); + DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == Owner::Player)) + } + + fn is_deposit(&self, i: usize) -> bool { + let t = &self.map.t[i]; + (t.kind == Kind::Treasury && t.owner == Owner::Player) || (self.counts[0] == 0 && self.map.adj_core(i)) + } + + fn start_deliver(&mut self, e: &mut Ent) -> bool { + let cur = e.tile(); + if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.is_deposit(i), true) { + e.path = p; + e.job = Job::Deliver; + return true; + } + false + } + + fn wander(&mut self, e: &mut Ent, radius: i32) { + let (x, y) = xy(e.tile()); + for _ in 0..6 { + let nx = x + self.rng.range(-radius, radius); + let ny = y + self.rng.range(-radius, radius); + if self.map.walk(nx, ny) && self.map.t[idx(nx, ny)].owner == Owner::Player { + if let Some(p) = self.map.path_to(e.tile(), idx(nx, ny)) { + if p.len() as i32 <= radius * 2 + 2 { + e.path = p; + break; + } + } + } + } + e.job = Job::Wander; + e.timer = 1.0 + self.rng.f() * 2.0; + } + + fn worker_ai(&mut self, e: &mut Ent, dt: f32) { + if self.step_path(e, dt) { + return; + } + let me = e.id + 1; + let cur = e.tile(); + match e.job { + Job::Dig(t) => { + let tile = self.map.t[t]; + if !tile.marked || !tile.diggable() || !adj4(cur, t) { + self.unreserve(me); + e.job = Job::None; + return; + } + let (tx, ty) = xy(t); + e.face = sgn(tx as f32 + 0.5 - e.x); + e.working = true; + e.timer += dt; + if e.timer >= 0.45 { + e.timer = 0.0; + 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; + if tl.kind == Kind::Gold { + let take = tl.gold.min(25); + tl.gold -= take; + e.carry += take; + done = tl.gold <= 0; + self.fx(5, fxx, fxy, 0.0, 0.0, 0.5, 0xff30d0ff); + } else { + tl.hp -= 1.0; + done = tl.hp <= 0.0; + self.fx(6, fxx, fxy, 0.0, 0.0, 0.4, 0xff3a5a7a); + } + if done { + self.dig_out(t); + e.job = Job::None; + } + if e.carry >= CARRY_MAX { + self.unreserve(me); + e.job = Job::None; + self.start_deliver(e); + } + } + } + Job::Claim(t) => { + if cur != t || !self.claimable(t, me) { + self.unreserve(me); + e.job = Job::None; + return; + } + e.working = true; + e.timer += dt; + if e.timer >= 1.0 { + self.unreserve(me); + e.job = Job::None; + if self.map.t[t].kind == Kind::Portal { + let (px, py) = self.map.portal; + for y in 0..3 { + for x in 0..3 { + self.map.t[idx(px + x, py + y)].owner = Owner::Player; + } + } + self.msg("Das Portal gehört nun dir! Kreaturen werden davon angelockt.".into()); + self.fx(7, px as f32 + 1.5, py as f32 + 1.5, 0.0, 0.0, 1.0, 0xffff66cc); + } else { + self.map.t[t].owner = Owner::Player; + } + let (x, y) = xy(t); + self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xff4040e0); + } + } + Job::Deliver => { + if self.is_deposit(cur) { + let amount = e.carry as f32; + let got = self.add_gold(amount); + if got < amount { + self.hint(3, "Deine Schatzkammern sind voll! Baue mehr Schatzkammer-Felder."); + } + if got > 0.0 { + self.fx(5, e.x, e.y, 0.0, 0.0, 0.8, 0xff30d0ff); + } + e.carry = 0; + e.job = Job::None; + } else if !self.start_deliver(e) { + e.job = Job::None; + e.think = 1.0; + } + } + Job::Wander => { + e.timer -= dt; + if e.timer <= 0.0 { + e.job = Job::None; + } + } + Job::None => { + if e.think > 0.0 { + return; + } + e.think = 0.3 + self.rng.f() * 0.2; + e.timer = 0.0; + if e.carry >= CARRY_MAX && self.start_deliver(e) { + return; + } + if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.dig_neighbor(i, me).is_some(), true) { + let end = p.last().copied().unwrap_or(cur); + if let Some(t) = self.dig_neighbor(end, me) { + self.map.t[t].reserved = me; + e.path = p; + e.job = Job::Dig(t); + return; + } + } + if e.carry > 0 && self.start_deliver(e) { + return; + } + if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.claimable(i, me), true) { + let end = p.last().copied().unwrap_or(cur); + self.map.t[end].reserved = me; + e.path = p; + e.job = Job::Claim(end); + return; + } + e.think = 1.0; + self.wander(e, 3); + } + _ => e.job = Job::None, + } + } + + // ---------------------------------------------------------------- Kreaturen + + fn find_room_tile(&self, cur: usize, kind: Kind, me: u32, need_food: bool) -> Option> { + self.map.search( + cur, + self.map.walk_cost(), + |i| { + let t = &self.map.t[i]; + t.kind == kind && t.owner == Owner::Player && (t.reserved == 0 || t.reserved == me) && (!need_food || t.food >= 1.0) + }, + true, + ) + } + + fn creature_ai(&mut self, e: &mut Ent, dt: f32) { + let me = e.id + 1; + e.hunger += dt / 75.0; + if !matches!(e.job, Job::Sleep(_)) { + e.energy -= dt / 110.0; + } + if e.hunger > 1.8 { + e.happy -= dt * 0.004; + } + if e.think <= 0.0 { + e.think = 0.4 + self.rng.f() * 0.3; + if !matches!(e.job, Job::Attack(_) | Job::Leave) { + if let Some(id) = self.nearest_enemy(e, 7.0) { + self.unreserve(me); + e.job = Job::Attack(id); + e.path.clear(); + e.repath = 0.0; + } + } + } + if let Job::Attack(id) = e.job { + if !self.attack_job(e, id, dt) { + e.job = Job::None; + e.path.clear(); + } + return; + } + if self.step_path(e, dt) { + return; + } + let cur = e.tile(); + match e.job { + Job::Sleep(_) => { + e.energy += dt / 14.0; + e.hp = (e.hp + e.max_hp * dt / 25.0).min(e.max_hp); + if e.energy >= 1.0 { + e.energy = 1.0; + e.job = Job::None; + } + } + Job::Eat(t) => { + if cur != t { + self.unreserve(me); + e.job = Job::None; + return; + } + e.working = true; + e.timer += dt; + if e.timer >= 2.0 { + let tl = &mut self.map.t[t]; + if tl.food >= 1.0 { + tl.food -= 1.0; + e.hunger = 0.0; + e.hp = (e.hp + e.max_hp * 0.2).min(e.max_hp); + } + self.unreserve(me); + e.job = Job::None; + } + } + Job::Train(t) => { + if cur != t || self.map.t[t].kind != Kind::Training || self.gold < 1.0 { + self.unreserve(me); + e.job = Job::None; + return; + } + e.working = true; + self.gold -= 1.5 * dt; + e.xp += 7.0 * dt; + e.timer -= dt; + if e.timer <= 0.0 { + self.unreserve(me); + e.job = Job::None; + } + } + Job::Study(t) => { + if cur != t || self.map.t[t].kind != Kind::Library { + self.unreserve(me); + e.job = Job::None; + return; + } + e.working = true; + let rate = if e.kind == EK::Mage { 1.2 } else { 0.5 }; + self.mana = (self.mana + rate * dt).min(MANA_MAX); + e.xp += 3.0 * dt; + e.timer -= dt; + if e.timer <= 0.0 { + self.unreserve(me); + e.job = Job::None; + } + } + Job::Wander => { + e.timer -= dt; + if e.timer <= 0.0 { + e.job = Job::None; + } + } + Job::Leave => { + if self.map.t[cur].kind == Kind::Portal { + e.alive = false; + self.release_all(e.id); + self.fx(3, e.x, e.y, 0.0, 0.0, 0.8, 0xffcc44ff); + } else { + self.start_leave(e); + } + } + Job::None => self.creature_choose(e), + _ => e.job = Job::None, + } + } + + fn start_leave(&mut self, e: &mut Ent) { + let cur = e.tile(); + e.job = Job::Leave; + if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.map.t[i].kind == Kind::Portal, true) { + e.path = p; + } else { + e.alive = false; + self.release_all(e.id); + self.fx(3, e.x, e.y, 0.0, 0.0, 0.8, 0xff505070); + } + } + + fn creature_choose(&mut self, e: &mut Ent) { + let me = e.id + 1; + let cur = e.tile(); + let name = st(e.kind).name; + if e.happy <= 0.0 { + self.msg(format!("Ein unzufriedener {} verlässt dein Reich!", name)); + self.start_leave(e); + return; + } + if e.energy < 0.25 { + if let Some(b) = e.bed { + let t = &self.map.t[b]; + if t.kind != Kind::Lair || t.bed != me { + e.bed = None; + } + } + if e.bed.is_none() { + let found = self.map.search( + cur, + self.map.walk_cost(), + |i| { + let t = &self.map.t[i]; + t.kind == Kind::Lair && t.bed == 0 + }, + true, + ); + if let Some(p) = found { + let b = p.last().copied().unwrap_or(cur); + self.map.t[b].bed = me; + e.bed = Some(b); + } + } + if let Some(b) = e.bed { + if let Some(p) = self.map.path_to(cur, b) { + e.path = p; + e.job = Job::Sleep(b); + return; + } + } + e.happy -= 0.1; + self.hint(4, "Kreaturen müssen auf dem nackten Boden schlafen. Vergrößere dein Quartier!"); + e.job = Job::Sleep(cur); + return; + } + if e.hunger > 1.0 { + if let Some(p) = self.find_room_tile(cur, Kind::Farm, me, true) { + let t = p.last().copied().unwrap_or(cur); + self.map.t[t].reserved = me; + e.path = p; + e.job = Job::Eat(t); + e.timer = 0.0; + return; + } + if e.hunger > 1.8 { + self.hint(5, "Deine Kreaturen hungern! Vergrößere den Pilzgarten."); + } + } + let r = self.rng.f(); + if (e.kind == EK::Mage && r < 0.8) || r < 0.2 { + if let Some(p) = self.find_room_tile(cur, Kind::Library, me, false) { + let t = p.last().copied().unwrap_or(cur); + self.map.t[t].reserved = me; + e.path = p; + e.job = Job::Study(t); + e.timer = 8.0 + self.rng.f() * 6.0; + return; + } + } + if r < 0.8 && self.gold >= 5.0 { + if let Some(p) = self.find_room_tile(cur, Kind::Training, me, false) { + let t = p.last().copied().unwrap_or(cur); + self.map.t[t].reserved = me; + e.path = p; + e.job = Job::Train(t); + e.timer = 8.0 + self.rng.f() * 6.0; + return; + } + } + self.wander(e, 4); + } + + // ---------------------------------------------------------------- Helden + + fn hero_ai(&mut self, e: &mut Ent, dt: f32) { + if e.think <= 0.0 { + e.think = 0.5; + if e.kind == EK::Priest && e.cd <= 0.0 { + let mut best: Option = None; + let mut worst = 0.95; + for (j, o) in self.ents.iter().enumerate() { + if o.alive && o.hero { + let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2); + let r = o.hp / o.max_hp; + if d < 16.0 && r < worst { + worst = r; + best = Some(j); + } + } + } + if e.hp / e.max_hp < worst { + e.hp = (e.hp + 25.0).min(e.max_hp); + self.fx(4, e.x, e.y, 0.0, 0.0, 0.8, 0xff60ff80); + e.cd = 2.5; + } else if let Some(j) = best { + let o = &mut self.ents[j]; + o.hp = (o.hp + 25.0).min(o.max_hp); + let (ox, oy) = (o.x, o.y); + self.fx(4, ox, oy, 0.0, 0.0, 0.8, 0xff60ff80); + e.cd = 2.5; + } + } + if !matches!(e.job, Job::Attack(_)) { + if let Some(id) = self.nearest_enemy(e, 6.5) { + e.job = Job::Attack(id); + e.path.clear(); + e.repath = 0.0; + } + } + } + if let Job::Attack(id) = e.job { + if self.attack_job(e, id, dt) { + return; + } + e.job = Job::AttackCore; + e.path.clear(); + e.think = 1.5; + } + let cur = e.tile(); + if e.path.is_empty() && self.map.adj_core(cur) { + e.working = true; + let (cx, cy) = self.map.core; + e.face = sgn(cx as f32 + 1.5 - e.x); + if e.cd <= 0.0 { + e.cd = st(e.kind).interval; + self.core_hp -= e.atk(); + let fx = (e.x + cx as f32 + 1.5) * 0.5; + let fy = (e.y + cy as f32 + 1.5) * 0.5; + self.fx(2, fx, fy, 0.0, 0.0, 0.3, 0xff4040ff); + self.hint(6, "Dein Seelenkern wird angegriffen!"); + } + return; + } + if e.path.is_empty() && e.repath <= 0.0 { + e.repath = 1.0; + if let Some(p) = self.map.hero_path_to_core(cur) { + e.path = p; + } + } + self.step_path(e, dt); + } + + // ---------------------------------------------------------------- Spieleraktionen + + pub fn mark_rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, on: bool) { + for y in y0.min(y1)..=y0.max(y1) { + for x in x0.min(x1)..=x0.max(x1) { + if inb(x, y) { + let t = &mut self.map.t[idx(x, y)]; + if t.diggable() { + t.marked = on; + } + } + } + } + } + + pub fn build_rect(&mut self, room: usize, x0: i32, y0: i32, x1: i32, y1: i32) { + let kind = ROOM_KINDS[room]; + let cost = ROOM_COST[room] as f32; + let mut built = 0; + let mut broke = false; + for y in y0.min(y1)..=y0.max(y1) { + for x in x0.min(x1)..=x0.max(x1) { + if !inb(x, y) { + continue; + } + let i = idx(x, y); + let t = self.map.t[i]; + if t.kind == Kind::Floor && t.owner == Owner::Player { + if self.gold < cost { + broke = true; + continue; + } + self.gold -= cost; + let t = &mut self.map.t[i]; + t.kind = kind; + t.food = 0.0; + t.bed = 0; + built += 1; + self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xff40d0ff); + } + } + } + if broke { + self.msg("Nicht genug Gold!".into()); + } + if built == 0 && !broke { + self.hint(7, "Räume können nur auf eroberten, leeren Bodenfeldern gebaut werden."); + } + self.recount(); + } + + pub fn sell_rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) { + let mut refund = 0.0; + for y in y0.min(y1)..=y0.max(y1) { + for x in x0.min(x1)..=x0.max(x1) { + if !inb(x, y) { + continue; + } + let t = &mut self.map.t[idx(x, y)]; + if t.owner == Owner::Player { + if let Some(r) = room_index(t.kind) { + t.kind = Kind::Floor; + t.bed = 0; + t.food = 0.0; + refund += ROOM_COST[r] as f32 * 0.5; + } + } + } + } + self.recount(); + if refund > 0.0 { + let got = self.add_gold(refund); + self.msg(format!("Räume verkauft: +{} Gold.", got as i32)); + } + } + + pub fn ent_at(&self, wx: f32, wy: f32, player_only: bool) -> Option { + let mut best = None; + let mut bd = 0.45f32; + for (j, e) in self.ents.iter().enumerate() { + if !e.alive || e.held || (player_only && e.hero) { + continue; + } + let d = (e.x - wx).powi(2) + (e.y - 0.25 - wy).powi(2); + if d < bd { + bd = d; + best = Some(j); + } + } + best + } + + pub fn pickup(&mut self, wx: f32, wy: f32) -> bool { + if self.hand.len() >= 8 { + self.hint(8, "Deine Hand ist voll (max. 8). Rechtsklick lässt Einheiten fallen."); + return false; + } + if let Some(j) = self.ent_at(wx, wy, true) { + let id = self.ents[j].id; + let me = id + 1; + self.unreserve(me); + let e = &mut self.ents[j]; + e.held = true; + e.job = Job::None; + e.path.clear(); + self.hand.push(id); + return true; + } + false + } + + pub fn drop(&mut self, wx: f32, wy: f32) -> bool { + let Some(&id) = self.hand.last() else { return false }; + let (x, y) = (wx as i32, wy as i32); + if !inb(x, y) { + return false; + } + let t = self.map.t[idx(x, y)]; + if !t.walkable() || t.owner != Owner::Player { + self.hint(9, "Einheiten können nur auf eigenem Gebiet abgesetzt werden."); + return false; + } + self.hand.pop(); + if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { + e.held = false; + e.x = wx; + e.y = wy; + e.job = Job::None; + e.think = 0.0; + e.path.clear(); + } + self.fx(3, wx, wy, 0.0, 0.0, 0.4, 0xff707090); + true + } + + pub fn lightning(&mut self, wx: f32, wy: f32) { + const COST: f32 = 50.0; + if self.mana < COST { + self.msg("Nicht genug Mana für den Blitz.".into()); + return; + } + self.mana -= COST; + self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa); + let mut kills = Vec::new(); + for (j, e) in self.ents.iter_mut().enumerate() { + if e.alive && !e.held && e.hero && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 2.6 { + e.hp -= 55.0; + e.flash = 0.2; + if e.hp <= 0.0 { + kills.push(j); + } + } + } + for j in kills { + self.kill(j); + } + } + + pub fn heal(&mut self, wx: f32, wy: f32) { + const COST: f32 = 60.0; + if self.mana < COST { + self.msg("Nicht genug Mana für die Heilung.".into()); + return; + } + self.mana -= COST; + let mut fxs = Vec::new(); + for e in self.ents.iter_mut() { + if e.alive && !e.held && !e.hero && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 6.5 { + e.hp = (e.hp + e.max_hp * 0.5).min(e.max_hp); + fxs.push((e.x, e.y)); + } + } + self.fx(7, wx, wy, 0.0, 0.0, 0.6, 0xff60ff80); + for (x, y) in fxs { + self.fx(4, x, y, 0.0, 0.0, 0.9, 0xff60ff80); + } + } + + pub fn summon(&mut self, wx: f32, wy: f32) { + let cost = self.summon_cost(); + let (x, y) = (wx as i32, wy as i32); + if !self.map.walk(x, y) || self.map.t[idx(x, y)].owner != Owner::Player { + self.hint(9, "Grubenwichte können nur auf eigenem Gebiet beschworen werden."); + return; + } + if self.mana < cost { + self.msg(format!("Nicht genug Mana ({} benötigt).", cost as i32)); + return; + } + self.mana -= cost; + self.spawn(EK::Worker, false, wx, wy, 1); + self.fx(7, wx, wy, 0.0, 0.0, 0.6, 0xffff66cc); + } +} + +pub fn job_name(e: &Ent) -> &'static str { + match e.job { + Job::None => "Überlegt", + Job::Dig(_) => "Gräbt", + Job::Claim(_) => "Erobert Boden", + Job::Deliver => "Bringt Gold zur Schatzkammer", + Job::Wander => "Streift umher", + Job::Sleep(_) => { + if e.moving { + "Geht schlafen" + } else { + "Schläft" + } + } + Job::Eat(_) => "Isst", + Job::Train(_) => "Trainiert", + Job::Study(_) => "Studiert", + Job::Attack(_) => "Kämpft", + Job::AttackCore => { + if e.working { + "Greift den Seelenkern an" + } else { + "Marschiert zum Seelenkern" + } + } + Job::Leave => "Verlässt das Reich", + } +} diff --git a/src/map.rs b/src/map.rs new file mode 100644 index 0000000..fc5dc57 --- /dev/null +++ b/src/map.rs @@ -0,0 +1,334 @@ +// Karte, Kacheln, Generierung und Wegfindung. + +use std::cmp::Reverse; +use std::collections::BinaryHeap; + +pub const MW: usize = 64; +pub const MH: usize = 64; +pub const N: usize = MW * MH; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Kind { + Rock, + Earth, + Gold, + Floor, + Treasury, + Lair, + Farm, + Training, + Library, + Core, + Portal, + Gate, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Owner { + None, + Player, + Hero, +} + +#[derive(Clone, Copy)] +pub struct Tile { + pub kind: Kind, + pub owner: Owner, + pub hp: f32, // Grab-Widerstand + pub gold: i32, // Goldvorrat (Goldader) + pub marked: bool, // zum Graben markiert + pub reserved: u32, // Entity-ID + 1, die diese Kachel bearbeitet + pub food: f32, // Pilze im Pilzgarten + pub bed: u32, // Entity-ID + 1, die hier schläft + pub var: u8, // Texturvariante +} + +impl Tile { + pub fn new(kind: Kind) -> Tile { + Tile { kind, owner: Owner::None, hp: 0.0, gold: 0, marked: false, reserved: 0, food: 0.0, bed: 0, var: 0 } + } + pub fn solid(&self) -> bool { + matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold) + } + pub fn diggable(&self) -> bool { + matches!(self.kind, Kind::Earth | Kind::Gold) + } + pub fn walkable(&self) -> bool { + matches!( + self.kind, + 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 { + y as usize * MW + x as usize +} +pub fn xy(i: usize) -> (i32, i32) { + ((i % MW) as i32, (i / MW) as i32) +} +pub fn inb(x: i32, y: i32) -> bool { + x >= 0 && y >= 0 && (x as usize) < MW && (y as usize) < MH +} + +pub const DIR4: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)]; +pub const DIR8: [(i32, i32); 8] = [(0, -1), (1, 0), (0, 1), (-1, 0), (1, -1), (1, 1), (-1, 1), (-1, -1)]; + +pub struct Rng(pub u32); +impl Rng { + pub fn next(&mut self) -> u32 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + self.0 = x; + x + } + pub fn f(&mut self) -> f32 { + (self.next() >> 8) as f32 / (1u32 << 24) as f32 + } + pub fn range(&mut self, a: i32, b: i32) -> i32 { + if b <= a { + return a; + } + a + (self.next() % (b - a + 1) as u32) as i32 + } +} + +pub struct Map { + pub t: Vec, + pub core: (i32, i32), // linke obere Ecke des Kerns (3x3) + pub portal: (i32, i32), + pub gate: (i32, i32), +} + +impl Map { + pub fn get(&self, x: i32, y: i32) -> Option<&Tile> { + if inb(x, y) { + Some(&self.t[idx(x, y)]) + } else { + None + } + } + + pub fn walk(&self, x: i32, y: i32) -> bool { + inb(x, y) && self.t[idx(x, y)].walkable() + } + + pub fn generate(seed: u32) -> Map { + let mut s = seed; + loop { + let m = Map::try_generate(s); + // Sicherstellen, dass Helden den Kern erreichen können + if m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() { + return m; + } + s = s.wrapping_mul(747796405).wrapping_add(2891336453) | 1; + } + } + + fn try_generate(seed: u32) -> Map { + let mut rng = Rng(seed | 1); + for _ in 0..10 { + rng.next(); + } + let mut t = vec![Tile::new(Kind::Earth); N]; + for (i, tile) in t.iter_mut().enumerate() { + let (x, y) = xy(i); + tile.var = (rng.next() % 4) as u8; + tile.hp = 6.0; + if x < 2 || y < 2 || x >= MW as i32 - 2 || y >= MH as i32 - 2 { + tile.kind = Kind::Rock; + } + } + let core = (11, rng.range(26, 35)); + 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| { + 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 { + continue; + } + let d = ((x - cx) * (x - cx) + (y - cy) * (y - cy)) as f32; + if d <= (r as f32 + 0.5).powi(2) - rng.f() * r as f32 { + let tl = &mut t[idx(x, y)]; + tl.kind = k; + if k == Kind::Gold { + tl.gold = rng.range(250, 550); + } + } + } + } + }; + + // Felsbrocken + for _ in 0..22 { + let cx = rng.range(4, MW as i32 - 5); + let cy = rng.range(4, MH as i32 - 5); + let r = rng.range(1, 3); + blob(&mut t, &mut rng, cx, cy, r, Kind::Rock); + } + // Goldadern + for _ in 0..16 { + let cx = rng.range(4, MW as i32 - 5); + let cy = rng.range(4, MH as i32 - 5); + let r = rng.range(1, 2); + blob(&mut t, &mut rng, cx, cy, r, Kind::Gold); + } + // Garantiertes Gold in Startnähe + for k in 0..3 { + let cx = core.0 + rng.range(-3, 9); + let cy = core.1 + if k % 2 == 0 { rng.range(-9, -6) } else { rng.range(7, 9) }; + 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| { + 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 { + let tl = &mut t[idx(x, y)]; + tl.kind = Kind::Floor; + tl.owner = owner; + tl.gold = 0; + } + } + } + }; + + // Kleine natürliche Höhlen + for _ in 0..4 { + let cx = rng.range(20, 48); + let cy = rng.range(6, 58); + let w = rng.range(1, 2); + let h = rng.range(1, 2); + room(&mut t, cx - w, cy - h, cx + w, cy + h, Owner::None); + } + + // Startbereich + room(&mut t, core.0 - 3, core.1 - 3, core.0 + 5, core.1 + 5, Owner::Player); + // Kern 3x3 + for y in 0..3 { + for x in 0..3 { + let tl = &mut t[idx(core.0 + x, core.1 + y)]; + tl.kind = Kind::Core; + tl.owner = Owner::Player; + } + } + // Portalhöhle + room(&mut t, portal.0 - 2, portal.1 - 2, portal.0 + 4, portal.1 + 4, Owner::None); + for y in 0..3 { + for x in 0..3 { + t[idx(portal.0 + x, portal.1 + y)].kind = Kind::Portal; + } + } + // Heldentor + room(&mut t, gate.0 - 2, gate.1 - 2, gate.0 + 4, gate.1 + 4, Owner::Hero); + for y in 0..3 { + for x in 0..3 { + let tl = &mut t[idx(gate.0 + x, gate.1 + y)]; + tl.kind = Kind::Gate; + tl.owner = Owner::Hero; + } + } + for tl in t.iter_mut() { + if tl.kind == Kind::Gold { + tl.hp = 6.0; + } + } + Map { t, core, portal, gate } + } + + // Allgemeine Dijkstra-Suche. + // cost(i) -> None = unpassierbar, Some(c) = Kosten zum Betreten. + // goal(i) -> true = Ziel erreicht. + pub fn search(&self, start: usize, cost: C, goal: G, diag: bool) -> Option> + where + C: Fn(usize) -> Option, + G: Fn(usize) -> bool, + { + let mut dist = vec![u32::MAX; N]; + let mut prev = vec![usize::MAX; N]; + let mut heap = BinaryHeap::new(); + dist[start] = 0; + heap.push(Reverse((0u32, start))); + while let Some(Reverse((d, i))) = heap.pop() { + if d > dist[i] { + continue; + } + if goal(i) { + let mut path = Vec::new(); + let mut c = i; + while c != start { + path.push(c); + c = prev[c]; + } + path.reverse(); + return Some(path); + } + let (x, y) = xy(i); + let dirs: &[(i32, i32)] = if diag { &DIR8 } else { &DIR4 }; + for (k, &(dx, dy)) in dirs.iter().enumerate() { + let nx = x + dx; + let ny = y + dy; + if !inb(nx, ny) { + continue; + } + let ni = idx(nx, ny); + let Some(c) = cost(ni) else { continue }; + let mut step = c * 10; + if k >= 4 { + // Diagonal nur, wenn beide Nachbarn begehbar sind (keine Ecken schneiden) + if c > 1 || !self.walk(x + dx, y) || !self.walk(x, y + dy) { + continue; + } + step = 14; + } + let nd = d + step; + if nd < dist[ni] { + dist[ni] = nd; + prev[ni] = i; + heap.push(Reverse((nd, ni))); + } + } + } + None + } + + pub fn walk_cost(&self) -> impl Fn(usize) -> Option + '_ { + move |i| if self.t[i].walkable() { Some(1) } else { None } + } + + pub fn hero_cost(&self) -> impl Fn(usize) -> Option + '_ { + move |i| { + let tl = &self.t[i]; + match tl.kind { + Kind::Earth => Some(7), + Kind::Gold => Some(12), + Kind::Rock | Kind::Core => None, + _ => Some(1), + } + } + } + + pub fn adj_core(&self, i: usize) -> bool { + let (x, y) = xy(i); + DIR4.iter().any(|&(dx, dy)| self.get(x + dx, y + dy).map_or(false, |t| t.kind == Kind::Core)) + } + + pub fn hero_path_to_core(&self, start: usize) -> Option> { + self.search(start, self.hero_cost(), |i| self.adj_core(i), true) + } + + 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() + } +}