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:
1 parent
a101f6ddc4
commit
fbf6f223fc
18 files changed
+1072
-362
No files matched your search
+123
-115
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user