Switch repo to English, add i18n with EN/DE language files and first-start language selection

- in-game texts moved to lang/en.lang and lang/de.lang (key = text, placeholders)
- game logic emits message keys + args, translated at display time
- localStorage access via own miniquad JS plugin (www/dc_plugin.js)
- code comments, DESIGN.md, CREDITS.md translated; README.md added

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-02 22:53:03 +01:00
1 parent a101f6ddc4
commit fbf6f223fc
18 files changed
+1072 -362

No files matched your search

+123 -115
View File
@@ -1,4 +1,4 @@
// Spiellogik: Einheiten, KI, Wirtschaft, Wellen.
// Game logic: units, AI, economy, hero waves.
use crate::map::*;
@@ -9,7 +9,7 @@ pub const FIRST_WAVE: f32 = 300.0;
pub const WAVE_GAP: f32 = 180.0;
pub const MAX_SQUAD: usize = 18;
// Sound-Ereignisse. Die Spiellogik meldet sie, das Sound-Modul spielt sie ab.
// Sound events. The game logic reports them, the sound module plays them.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Sfx {
Dig,
@@ -56,10 +56,17 @@ impl Sfx {
}
}
// A game message: language key + arguments (arguments starting with '@' are keys too)
#[derive(Clone, Debug)]
pub struct Msg {
pub key: &'static str,
pub args: Vec<String>,
}
#[derive(Clone, Copy)]
pub struct SfxEvent {
pub sfx: Sfx,
pub pos: Option<(f32, f32)>, // Weltposition (für Lautstärke nach Entfernung), None = UI
pub pos: Option<(f32, f32)>, // world position (for distance-based volume), None = UI
}
pub const MANA_MAX: f32 = 400.0;
pub const CARRY_MAX: i32 = 300;
@@ -92,16 +99,16 @@ pub struct St {
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),
EK::Worker => s("unit.worker", 45.0, 3.0, 1.0, 3.2, 1.0, 0, 0),
EK::Kobold => s("unit.kobold", 75.0, 8.0, 1.0, 2.6, 0.9, 40, 0),
EK::Spider => s("unit.spider", 95.0, 10.0, 1.0, 2.9, 1.0, 60, 0),
EK::Mage => s("unit.mage", 60.0, 13.0, 4.5, 2.0, 1.4, 110, 0),
EK::Ogre => s("unit.ogre", 230.0, 20.0, 1.0, 1.8, 1.4, 150, 0),
EK::Knight => s("unit.knight", 140.0, 11.0, 1.0, 1.9, 1.1, 0, 120),
EK::Archer => s("unit.archer", 75.0, 9.0, 4.5, 2.1, 1.3, 0, 100),
EK::Priest => s("unit.priest", 80.0, 5.0, 3.5, 1.9, 1.5, 0, 120),
EK::Dwarf => s("unit.dwarf", 110.0, 9.0, 1.0, 2.0, 1.0, 0, 100),
EK::Paladin => s("unit.paladin", 700.0, 26.0, 1.0, 1.7, 1.2, 0, 1000),
}
}
@@ -151,7 +158,7 @@ pub struct Ent {
pub face: f32,
pub moving: bool,
pub working: bool,
pub guard: bool, // Torverteidiger
pub guard: bool, // gate defender
}
impl Ent {
@@ -200,7 +207,7 @@ impl Ent {
#[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 kind: u8, // 0 projectile, 1 lightning, 2 hit, 3 smoke, 4 heal, 5 gold, 6 dust, 7 level up, 8 claim
pub x: f32,
pub y: f32,
pub x2: f32,
@@ -225,14 +232,14 @@ pub struct Game {
pub portal_timer: f32,
pub payday: f32,
pub fx: Vec<Fx>,
pub msgs: Vec<String>,
pub state: u8, // 0 läuft, 1 Sieg, 2 Niederlage
pub msgs: Vec<Msg>,
pub state: u8, // 0 running, 1 victory, 2 defeat
pub paused: bool,
pub hand: Vec<u32>,
pub counts: [usize; 5], // Schatzkammer, Quartier, Pilzgarten, Trainingshalle, Bibliothek
pub counts: [usize; 5], // treasury, lair, farm, training hall, library
pub gate_hp: f32,
pub gate_hit_t: f32,
pub gate_stage: u32, // ausgelöste Verteidiger-Schwellen (0..3)
pub gate_stage: u32, // defender thresholds triggered (0..3)
pub flag: Option<usize>,
pub events: Vec<SfxEvent>,
pub last_wave_value: f32,
@@ -240,7 +247,7 @@ pub struct Game {
hint_t: [f32; 16],
}
// Fackel an einer Wandseite: Wandkachel + Richtung (Index in DIR4) zur offenen Seite
// torch on a wall face: wall tile + direction (index into DIR4) towards the open side
#[derive(Clone, Copy, PartialEq)]
pub struct Torch {
pub wall: usize,
@@ -248,7 +255,7 @@ pub struct Torch {
}
impl Torch {
// Weltposition der Flamme (x, z) auf Bodenebene
// world position of the flame (x, z) on the ground plane
pub fn pos(&self) -> (f32, f32) {
let (x, y) = xy(self.wall);
let (dx, dy) = DIR4[self.dir];
@@ -258,7 +265,7 @@ impl Torch {
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 const ROOM_NAMES: [&str; 5] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library"];
pub fn room_index(k: Kind) -> Option<usize> {
ROOM_KINDS.iter().position(|&r| r == k)
@@ -320,15 +327,15 @@ impl Game {
g.place_torches(i);
}
}
g.msg("Willkommen, Herrscher des Unterreichs! Beschütze deinen Seelenkern und zerstöre das Heldentor.".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.msg("Mit der Angriffsflagge (Taste 9) schickst du deine Kreaturen in den Kampf, auch gegen das Heldentor.".into());
g.msg("msg.welcome1", vec![]);
g.msg("msg.welcome2", vec![]);
g.msg("msg.welcome3", vec![]);
g.msg("msg.welcome4", vec![]);
g
}
pub fn msg(&mut self, s: String) {
self.msgs.push(s);
pub fn msg(&mut self, key: &'static str, args: Vec<String>) {
self.msgs.push(Msg { key, args });
if self.msgs.len() > 50 {
self.msgs.remove(0);
}
@@ -350,7 +357,7 @@ impl Game {
(self.map.gate.0 as f32 + 1.5, self.map.gate.1 as f32 + 1.5)
}
// Armeewert: sqrt(Summe HP * Summe Schaden/s) aller eigenen Kreaturen.
// Army value: sqrt(sum of HP * sum of damage/s) of all own creatures.
pub fn army_value(&self) -> f32 {
let (mut h, mut d) = (0.0, 0.0);
for e in &self.ents {
@@ -362,7 +369,7 @@ impl Game {
(h * d).sqrt()
}
// Wichte hängen Fackeln an Wände neben erobertem Boden (Mindestabstand 3,5 Felder)
// imps hang torches on walls next to claimed floor (min. distance 3.5 tiles)
pub fn place_torches(&mut self, i: usize) {
let (x, y) = xy(i);
for (d, &(dx, dy)) in DIR4.iter().enumerate() {
@@ -370,7 +377,7 @@ impl Game {
if !inb(wx, wy) || !self.map.t[idx(wx, wy)].solid() {
continue;
}
// Richtung von der Wand zum Boden ist die Gegenrichtung
// the direction from the wall to the floor is the opposite one
let t = Torch { wall: idx(wx, wy), dir: (d + 2) % 4 };
let (px, pz) = t.pos();
let near = self.torches.iter().any(|o| {
@@ -383,10 +390,10 @@ impl Game {
}
}
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());
fn hint(&mut self, slot: usize, key: &'static str) {
if self.time - self.hint_t[slot] > 30.0 {
self.hint_t[slot] = self.time;
self.msg(key, vec![]);
}
}
@@ -460,7 +467,7 @@ impl Game {
self.ents.iter().position(|e| e.id == id && e.alive && !e.held)
}
// ---------------------------------------------------------------- Update
// ---------------------------------------------------------------- update
pub fn update(&mut self, dt: f32) {
let dt = dt.min(0.1);
@@ -501,7 +508,7 @@ impl Game {
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));
self.msg("msg.wave_warn", vec![(self.wave + 1).to_string()]);
self.sfx_ui(Sfx::WaveAlarm);
}
if self.wave_timer <= 0.0 {
@@ -527,12 +534,12 @@ impl Game {
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());
self.msg("msg.defeat", vec![]);
self.sfx_ui(Sfx::Defeat);
} else if self.gate_hp <= 0.0 {
self.gate_hp = 0.0;
self.state = 1;
self.msg("Das Heldentor ist zerstört. Das Unterreich gehört dir!".into());
self.msg("msg.victory", vec![]);
self.sfx_ui(Sfx::Victory);
}
}
@@ -543,15 +550,15 @@ impl Game {
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.");
self.hint(0, "hint.portal_no_lair");
return;
}
if farm == 0 {
self.hint(1, "Kreaturen wollen essen: Baue einen Pilzgarten.");
self.hint(1, "hint.no_farm");
return;
}
if creatures >= cap {
self.hint(2, "Alle Quartierplätze sind belegt. Vergrößere dein Quartier.");
self.hint(2, "hint.lair_full");
return;
}
let mut opts: Vec<(EK, u32)> = vec![(EK::Kobold, 4)];
@@ -578,7 +585,7 @@ impl Game {
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.sfx(Sfx::PortalArrive, px as f32 + 1.5, py as f32 + 1.5);
self.msg(format!("Ein {} ist durch das Portal gekommen.", st(kind).name));
self.msg("msg.creature_arrived", vec![format!("@{}", st(kind).name)]);
}
fn do_payday(&mut self) {
@@ -605,14 +612,14 @@ impl Game {
if paid + unpaid > 0 {
self.sfx_ui(Sfx::Payday);
if unpaid > 0 {
self.msg(format!("Zahltag! {} Kreaturen bezahlt ({} Gold), {} gingen leer aus und sind wütend!", paid, total as i32, unpaid));
self.msg("msg.payday_partial", vec![paid.to_string(), (total as i32).to_string(), unpaid.to_string()]);
} else {
self.msg(format!("Zahltag! {} Kreaturen erhielten {} Gold.", paid, total as i32));
self.msg("msg.payday", vec![paid.to_string(), (total as i32).to_string()]);
}
}
}
// Stärke eines Trupps (gleiche Formel wie der Armeewert)
// strength of a squad (same formula as the army value)
fn squad_value(list: &[(EK, u32)]) -> f32 {
let (mut h, mut d) = (0.0, 0.0);
for &(k, l) in list {
@@ -623,7 +630,7 @@ impl Game {
(h * d).sqrt()
}
// Stellt einen Heldentrupp mit mindestens der Zielstärke zusammen.
// builds a hero squad with at least the target strength
fn build_squad(&mut self, target: f32, base_level: u32, pool: &[(EK, u32)], boss: Option<EK>, min: usize) -> Vec<(EK, u32)> {
let total: u32 = pool.iter().map(|p| p.1).sum();
let mut list = Vec::new();
@@ -662,7 +669,7 @@ impl Game {
self.fx(3, gx as f32 + 1.5, gy as f32 + 1.5, 0.0, 0.0, 1.0, 0xffffeecc);
}
// Endlose Wellen: Stärke 60 % -> 100 % des Armeewerts, mit etwas Zufall, mindestens 2 Ritter.
// Endless waves: strength 60% -> 100% of the army value, with some randomness, at least 2 knights.
fn spawn_wave(&mut self) {
use EK::*;
let w = self.wave + 1;
@@ -683,13 +690,13 @@ impl Game {
self.spawn_squad(&list, false);
self.sfx_ui(Sfx::WaveStart);
if boss.is_some() {
self.msg(format!("Welle {}: Ein Lichtpaladin führt {} Helden in dein Reich!", w, list.len()));
self.msg("msg.wave_boss", vec![w.to_string(), list.len().to_string()]);
} else {
self.msg(format!("Welle {}: {} Helden dringen durch das Tor!", w, list.len()));
self.msg("msg.wave", vec![w.to_string(), list.len().to_string()]);
}
}
// Verteidiger bei 75 / 50 / 25 % Tor-HP
// defenders at 75 / 50 / 25 % gate HP
fn check_gate_stage(&mut self) {
use EK::*;
let frac = self.gate_hp / GATE_HP;
@@ -707,9 +714,9 @@ impl Game {
self.spawn_squad(&list, true);
self.sfx_ui(Sfx::GateDefenders);
if st_i == 3 {
self.msg(format!("Das Tor wankt! Der Torwächter erscheint mit {} Verteidigern!", list.len() - 1));
self.msg("msg.gate_boss", vec![(list.len() - 1).to_string()]);
} else {
self.msg(format!("Das Tor ruft {} Verteidiger herbei!", list.len()));
self.msg("msg.gate_defenders", vec![list.len().to_string()]);
}
}
}
@@ -731,7 +738,7 @@ impl Game {
self.fx(2, fx, fy, 0.0, 0.0, 0.25, 0xffffffff);
}
self.sfx(Sfx::GateHit, gx, gy);
self.hint(10, "Deine Kreaturen greifen das Heldentor an!");
self.hint(10, "hint.gate_attack");
self.check_gate_stage();
}
@@ -755,7 +762,7 @@ impl Game {
self.sfx(Sfx::LevelUp, e.x, e.y);
}
if !e.hero && e.level >= 3 {
self.msg(format!("{} erreicht Stufe {}.", s.name, e.level));
self.msg("msg.level_up", vec![format!("@{}", s.name), e.level.to_string()]);
}
}
if e.hero {
@@ -767,7 +774,7 @@ impl Game {
}
}
// Bewegt eine Einheit entlang ihres Pfads. true = noch unterwegs.
// moves a unit along its path; true = still moving
fn step_path(&mut self, e: &mut Ent, dt: f32) -> bool {
if e.path.is_empty() {
return false;
@@ -776,7 +783,7 @@ impl Game {
let tile = self.map.t[next];
if !tile.walkable() {
if e.hero && tile.diggable() {
// Helden graben sich durch
// heroes dig their way through
e.working = true;
let (tx, ty) = xy(next);
e.face = sgn(tx as f32 + 0.5 - e.x);
@@ -893,7 +900,7 @@ impl Game {
best
}
// Kampf gegen eine bestimmte Einheit. false = Ziel ungültig/unerreichbar.
// fight a specific unit; false = target invalid/unreachable
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);
@@ -921,7 +928,7 @@ impl Game {
}
}
if !self.step_path(e, dt) && e.tile() == idx(tx as i32, ty as i32) {
// gleiches Feld: direkt annähern
// same tile: approach directly
let dx = tx - e.x;
let dy = ty - e.y;
let l = (dx * dx + dy * dy).sqrt().max(0.001);
@@ -974,14 +981,14 @@ impl Game {
let got = self.add_gold(b);
self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff);
self.sfx(Sfx::HeroDeath, x, y);
self.msg(format!("{} besiegt! Beute: {} Gold.", s.name, got as i32));
self.msg("msg.hero_slain", vec![format!("@{}", s.name), (got as i32).to_string()]);
} else {
self.sfx(Sfx::Death, x, y);
self.msg(format!("Dein {} ist gefallen.", s.name));
self.msg("msg.creature_died", vec![format!("@{}", s.name)]);
}
}
// ---------------------------------------------------------------- Grubenwichte
// ---------------------------------------------------------------- pit imps (workers)
fn dig_neighbor(&self, i: usize, me: u32) -> Option<usize> {
let (x, y) = xy(i);
@@ -1112,7 +1119,7 @@ impl Game {
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.msg("msg.portal_claimed", vec![]);
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;
@@ -1128,7 +1135,7 @@ impl Game {
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.");
self.hint(3, "hint.treasury_full");
}
if got > 0.0 {
self.fx(5, e.x, e.y, 0.0, 0.0, 0.8, 0xff30d0ff);
@@ -1182,7 +1189,7 @@ impl Game {
}
}
// ---------------------------------------------------------------- Kreaturen
// ---------------------------------------------------------------- creatures
fn find_room_tile(&self, cur: usize, kind: Kind, me: u32, need_food: bool) -> Option<Vec<usize>> {
self.map.search(
@@ -1327,7 +1334,7 @@ impl Game {
self.flag.is_some() && e.hp > e.max_hp * 0.5 && e.energy > 0.15 && e.hunger < 1.8 && e.happy > 0.0
}
// Angriffsflagge: zur Flagge marschieren; liegt das Tor in der Nähe, wird es angegriffen.
// Attack flag: march to the flag; if the gate is nearby, attack it.
fn rally(&mut self, e: &mut Ent, dt: f32) {
let Some(f) = self.flag else {
e.job = Job::None;
@@ -1335,7 +1342,7 @@ impl Game {
return;
};
if e.hp < e.max_hp * 0.3 || e.energy < 0.1 || e.hunger > 2.2 {
// erschöpft oder schwer verletzt: zurückziehen
// exhausted or badly hurt: retreat
e.job = Job::None;
e.path.clear();
return;
@@ -1364,14 +1371,14 @@ impl Game {
};
let d = ((e.x - tx).powi(2) + (e.y - ty).powi(2)).sqrt();
if d <= near && e.path.is_empty() {
return; // wartet an der Flagge
return; // wait at the flag
}
if e.path.is_empty() || e.repath <= 0.0 {
e.repath = 1.5;
match self.map.path_to(e.tile(), target) {
Some(p) => e.path = p,
None => {
self.hint(11, "Die Angriffsflagge ist für deine Kreaturen nicht erreichbar.");
self.hint(11, "hint.flag_unreachable");
e.job = Job::Wander;
e.timer = 3.0;
return;
@@ -1388,7 +1395,7 @@ impl Game {
let i = idx(x, y);
if self.flag == Some(i) {
self.flag = None;
self.msg("Angriffsflagge entfernt. Deine Kreaturen kehren zurück.".into());
self.msg("msg.flag_removed", vec![]);
self.sfx_ui(Sfx::Click);
for e in self.ents.iter_mut() {
if e.alive && e.creature() && e.job == Job::Rally {
@@ -1399,13 +1406,13 @@ impl Game {
return;
}
if !self.map.t[i].walkable() {
self.hint(12, "Die Flagge muss auf begehbarem Boden stehen.");
self.hint(12, "hint.flag_ground");
self.sfx_ui(Sfx::Error);
return;
}
self.flag = Some(i);
self.sfx(Sfx::FlagSet, x as f32 + 0.5, y as f32 + 0.5);
self.msg("Angriffsflagge gesetzt! Kampfbereite Kreaturen sammeln sich dort.".into());
self.msg("msg.flag_set", vec![]);
let mut ids = Vec::new();
for e in self.ents.iter() {
if e.alive && !e.held && e.creature() && matches!(e.job, Job::None | Job::Wander | Job::Train(_) | Job::Study(_) | Job::Rally) {
@@ -1426,7 +1433,7 @@ impl Game {
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.msg("msg.creature_leaves", vec![format!("@{}", name)]);
self.start_leave(e);
return;
}
@@ -1461,7 +1468,7 @@ impl Game {
}
}
e.happy -= 0.1;
self.hint(4, "Kreaturen müssen auf dem nackten Boden schlafen. Vergrößere dein Quartier!");
self.hint(4, "hint.no_beds");
e.job = Job::Sleep(cur);
return;
}
@@ -1475,7 +1482,7 @@ impl Game {
return;
}
if e.hunger > 1.8 {
self.hint(5, "Deine Kreaturen hungern! Vergrößere den Pilzgarten.");
self.hint(5, "hint.hungry");
}
}
if self.wants_rally(e) {
@@ -1485,7 +1492,7 @@ impl Game {
return;
}
if e.hp < e.max_hp * 0.5 && e.energy >= 0.25 && self.counts[1] > 0 {
// verletzt: im Quartier ausruhen
// hurt: rest in the lair
e.energy = e.energy.min(0.2);
self.creature_choose(e);
return;
@@ -1514,7 +1521,7 @@ impl Game {
self.wander(e, 4);
}
// ---------------------------------------------------------------- Helden
// ---------------------------------------------------------------- heroes
fn hero_ai(&mut self, e: &mut Ent, dt: f32) {
if e.think <= 0.0 {
@@ -1564,7 +1571,7 @@ impl Game {
e.think = 1.5;
}
if e.guard {
// Torwächter bleiben am Tor
// gate defenders stay at the gate
let (gx, gy) = self.gate_center();
let d2 = (e.x - gx).powi(2) + (e.y - gy).powi(2);
if d2 > 2.5 * 2.5 && e.path.is_empty() && e.repath <= 0.0 {
@@ -1589,7 +1596,7 @@ impl Game {
let fy = (e.y + cy as f32 + 1.5) * 0.5;
self.fx(2, fx, fy, 0.0, 0.0, 0.3, 0xff4040ff);
self.sfx(Sfx::CoreHit, fx, fy);
self.hint(6, "Dein Seelenkern wird angegriffen!");
self.hint(6, "hint.core_attacked");
}
return;
}
@@ -1602,7 +1609,7 @@ impl Game {
self.step_path(e, dt);
}
// ---------------------------------------------------------------- Spieleraktionen
// ---------------------------------------------------------------- player actions
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) {
@@ -1648,11 +1655,11 @@ impl Game {
}
}
if broke {
self.msg("Nicht genug Gold!".into());
self.msg("msg.no_gold", vec![]);
self.sfx_ui(Sfx::Error);
}
if built == 0 && !broke {
self.hint(7, "Räume können nur auf eroberten, leeren Bodenfeldern gebaut werden.");
self.hint(7, "hint.build_where");
}
self.recount();
}
@@ -1679,13 +1686,13 @@ impl Game {
if refund > 0.0 {
let got = self.add_gold(refund);
self.sfx_ui(Sfx::Sell);
self.msg(format!("Räume verkauft: +{} Gold.", got as i32));
self.msg("msg.sold", vec![(got as i32).to_string()]);
}
}
pub fn pickup(&mut self, target: Option<usize>) -> bool {
if self.hand.len() >= 8 {
self.hint(8, "Deine Hand ist voll (max. 8). Rechtsklick lässt Einheiten fallen.");
self.hint(8, "hint.hand_full");
return false;
}
if let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held && !self.ents[j].hero) {
@@ -1711,7 +1718,7 @@ impl Game {
}
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.");
self.hint(9, "hint.drop_where");
return false;
}
self.hand.pop();
@@ -1731,7 +1738,7 @@ impl Game {
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());
self.msg("msg.no_mana_bolt", vec![]);
self.sfx_ui(Sfx::Error);
return;
}
@@ -1756,7 +1763,7 @@ impl Game {
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());
self.msg("msg.no_mana_heal", vec![]);
self.sfx_ui(Sfx::Error);
return;
}
@@ -1779,11 +1786,11 @@ impl Game {
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.");
self.hint(13, "hint.summon_where");
return;
}
if self.mana < cost {
self.msg(format!("Nicht genug Mana ({} benötigt).", cost as i32));
self.msg("msg.no_mana_cost", vec![(cost as i32).to_string()]);
self.sfx_ui(Sfx::Error);
return;
}
@@ -1794,37 +1801,38 @@ impl Game {
}
}
// Language key describing what a unit is doing
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::None => "job.none",
Job::Dig(_) => "job.dig",
Job::Claim(_) => "job.claim",
Job::Deliver => "job.deliver",
Job::Wander => "job.wander",
Job::Sleep(_) => {
if e.moving {
"Geht schlafen"
"job.sleep_go"
} else {
"Schläft"
"job.sleep"
}
}
Job::Eat(_) => "Isst",
Job::Train(_) => "Trainiert",
Job::Study(_) => "Studiert",
Job::Attack(_) => "Kämpft",
Job::Eat(_) => "job.eat",
Job::Train(_) => "job.train",
Job::Study(_) => "job.study",
Job::Attack(_) => "job.fight",
Job::AttackCore => {
if e.working {
"Greift den Seelenkern an"
"job.core_attack"
} else {
"Marschiert zum Seelenkern"
"job.core_march"
}
}
Job::Leave => "Verlässt das Reich",
Job::Leave => "job.leave",
Job::Rally => {
if e.working {
"Greift das Heldentor an"
"job.gate_attack"
} else {
"Folgt der Angriffsflagge"
"job.rally"
}
}
}
@@ -1834,14 +1842,14 @@ pub fn job_name(e: &Ent) -> &'static str {
mod tests {
use super::*;
// Simuliert ein Spiel mit einfacher "KI-Spielerin" und prüft, dass nichts abstürzt.
// Simulates games with a simple bot player and checks that nothing crashes.
#[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)
// dig a way to the portal (through earth/gold, around rock)
let start = idx(cx - 1, cy + 1);
let goal = idx(px + 1, py + 1);
let path = g
@@ -1855,8 +1863,8 @@ mod tests {
}
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
g.build_rect(1, ox, oy, ox + 2, oy + 1); // lair 3x2
g.build_rect(2, ox + 5, oy, ox + 7, oy + 1); // farm 3x2
let dt = 0.05;
let mut built = false;
let mut portal_logged = false;
@@ -1865,12 +1873,12 @@ mod tests {
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
g.build_rect(0, ox, oy + 6, ox + 1, oy + 8); // treasury
g.build_rect(3, ox + 6, oy + 6, ox + 8, oy + 8); // training hall
g.build_rect(1, ox, oy + 2, ox + 2, oy + 2); // enlarge the lair
built = true;
}
// Bot nutzt den Blitz gegen Helden am Kern
// the bot uses lightning against heroes at the core
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 {
@@ -1898,10 +1906,10 @@ mod tests {
}
if g.portal_owned() && step % 20 == 0 && !portal_logged {
portal_logged = true;
println!("seed {} Portal erobert nach {:.0}s", seed, g.time);
println!("seed {} portal claimed after {:.0}s", seed, g.time);
}
if g.state != 0 {
println!("seed {} Ende nach {:.0}s, state={}", seed, g.time, g.state);
println!("seed {} game over after {:.0}s, state={}", seed, g.time, g.state);
break;
}
}