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;
}
}
+30 -30
View File
@@ -1,4 +1,4 @@
// Prozedural erzeugte Pixelart: Kachel-Atlas und Figuren-Sprites.
// Procedurally generated pixel art: tile atlas, unit sprites and the bitmap font.
use crate::game::EK;
use crate::map::Kind;
@@ -8,7 +8,7 @@ pub const CELL: f32 = 16.0;
const ATLAS_COLS: u32 = 16;
const ATLAS_ROWS: u32 = 5;
// Atlas-Indizes (jeweils 4 Varianten)
// atlas indices (4 variants each)
pub const T_ROCK: u32 = 0;
pub const T_EARTH: u32 = 4;
pub const T_GOLD: u32 = 8;
@@ -23,7 +23,7 @@ pub const T_LIBRARY: u32 = 40;
pub const T_COREFLOOR: u32 = 44;
pub const T_PORTALFLOOR: u32 = 48;
pub const T_GATEFLOOR: u32 = 52;
// Einzelzellen
// single cells
pub const T_FACE_EARTH: u32 = 56;
pub const T_FACE_ROCK: u32 = 57;
pub const T_FACE_GOLD: u32 = 58;
@@ -44,19 +44,19 @@ pub const I_SUMMON: u32 = 71;
pub struct Gfx {
pub atlas: Texture2D,
pub sprites: Texture2D,
pub font: Texture2D, // DINOBYTE, CP437, 16x16 Zeichen à 6x8 px
pub font: Texture2D, // DINOBYTE, CP437, 16x16 glyphs of 6x8 px
}
// ------------------------------------------------------------------ Bitmap-Schrift (CP437)
// ------------------------------------------------------------------ bitmap font (CP437)
pub const GW: f32 = 6.0; // Glyphbreite
pub const GH: f32 = 8.0; // Glyphhöhe
pub const GW: f32 = 6.0; // glyph width
pub const GH: f32 = 8.0; // glyph height
const CP437_LOW: &str = " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼";
const CP437_HIGH: &str = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}";
pub fn cp437(ch: char) -> u8 {
// In DINOBYTE sind ö (0x94) und ò (0x95) vertauscht
// DINOBYTE has ö (0x94) and ò (0x95) swapped
match ch {
'ö' => return 0x95,
'ò' => return 0x94,
@@ -88,7 +88,7 @@ pub fn cp437(ch: char) -> u8 {
}
}
// Rahmenzeichen: [oben-links, oben-rechts, unten-links, unten-rechts, waagrecht, senkrecht]
// frame glyphs: [top-left, top-right, bottom-left, bottom-right, horizontal, vertical]
pub const BOX_SINGLE: [u8; 6] = [0xDA, 0xBF, 0xC0, 0xD9, 0xC4, 0xB3];
pub const BOX_DOUBLE: [u8; 6] = [0xC9, 0xBB, 0xC8, 0xBC, 0xCD, 0xBA];
@@ -126,7 +126,7 @@ impl Canvas {
}
}
// Pflasterstein-Muster mit Fugen
// flagstone pattern with grout
fn stones(x: i32, y: i32, v: u32, base: (i32, i32, i32), grout: (i32, i32, i32), var: i32) -> Color {
let row = y / 8;
let off = if row % 2 == 1 { 4 } else { 0 };
@@ -136,7 +136,7 @@ fn stones(x: i32, y: i32, v: u32, base: (i32, i32, i32), grout: (i32, i32, i32),
}
let stone = hash((x + off) / 8, row, v + 3) % 5;
let d = (rnd(x, y, v) * var as f32) as i32 - var / 2 + stone as i32 * 3 - 6;
// Lichtkante oben/links, Schatten unten/rechts
// highlight top/left, shadow bottom/right
let edge = if y % 8 == 1 || sx % 8 == 1 {
10
} else if y % 8 == 7 || sx % 8 == 7 {
@@ -163,7 +163,7 @@ fn earth_px(x: i32, y: i32, v: u32) -> Color {
fn gen_atlas() -> Image {
let mut cv = Canvas { img: Image::gen_image_color((ATLAS_COLS * 16) as u16, (ATLAS_ROWS * 16) as u16, Color::new(0.0, 0.0, 0.0, 0.0)) };
for v in 0..4u32 {
// Fels
// rock
cv.cell(T_ROCK + v, |x, y| {
let blk = rnd(x / 4 + (y / 4) % 2, y / 4, v + 21) * 20.0 - 10.0;
let fine = rnd(x, y, v + 5) * 10.0 - 5.0;
@@ -174,9 +174,9 @@ fn gen_atlas() -> Image {
Some(shade((80, 80, 92), (blk + fine) as i32))
}
});
// Erde
// earth
cv.cell(T_EARTH + v, |x, y| Some(earth_px(x, y, v)));
// Gold
// gold
cv.cell(T_GOLD + v, |x, y| {
let mut best = 99.0f32;
for k in 0..6 {
@@ -198,7 +198,7 @@ fn gen_atlas() -> Image {
cv.cell(T_FLOOR + v, |x, y| Some(stones(x, y, v, (64, 56, 50), (34, 30, 28), 10)));
cv.cell(T_CLAIM + v, |x, y| {
let col = stones(x, y, v + 10, (78, 44, 58), (36, 20, 28), 10);
// kleines Hoheitszeichen in der Mitte jedes zweiten Steins
// small emblem in the middle of every other stone
if (x % 8 == 4 && y % 8 == 4) && hash(x / 8, y / 8, v) % 2 == 0 {
Some(c(150, 60, 80))
} else {
@@ -268,7 +268,7 @@ fn gen_atlas() -> Image {
}
});
}
// Wandfronten (Pseudo-3D)
// wall faces
cv.cell(T_FACE_EARTH, |x, y| {
let streak = (hash(x, 0, 70) % 3) as i32 * 5;
Some(shade((62, 42, 28), (rnd(x, y, 71) * 10.0) as i32 - streak - y / 2))
@@ -284,7 +284,7 @@ fn gen_atlas() -> Image {
Some(shade((62, 42, 28), (rnd(x, y, 75) * 10.0) as i32 - y / 2))
}
});
// Grab-Markierung: diagonale Streifen
// dig marker: diagonal stripes
cv.cell(T_MARK, |x, y| {
if (x + y) % 6 < 2 {
Some(Color::from_rgba(255, 220, 60, 150))
@@ -294,7 +294,7 @@ fn gen_atlas() -> Image {
None
}
});
// Bett
// bed
cv.cell(D_BED, |x, y| {
let dx = (x as f32 - 7.5) / 6.5;
let dy = (y as f32 - 8.0) / 5.5;
@@ -307,7 +307,7 @@ fn gen_atlas() -> Image {
None
}
});
// Pilze
// mushrooms
cv.cell(D_MUSHROOM, |x, y| {
let caps = [(4, 6, 3), (11, 5, 2), (8, 11, 3)];
for &(cx, cy, r) in &caps {
@@ -322,7 +322,7 @@ fn gen_atlas() -> Image {
}
None
});
// Trainingspuppe
// training dummy
cv.cell(D_DUMMY, |x, y| {
if (x == 7 || x == 8) && y >= 4 {
Some(c(90, 60, 36))
@@ -336,7 +336,7 @@ fn gen_atlas() -> Image {
None
}
});
// Goldhaufen
// gold pile
cv.cell(D_COINS, |x, y| {
let dx = (x as f32 - 7.5) / 7.0;
let dy = (y as f32 - 11.0) / 5.0;
@@ -348,7 +348,7 @@ fn gen_atlas() -> Image {
None
}
});
// Bücherregal
// bookshelf
cv.cell(D_SHELF, |x, y| {
if y > 9 || x == 0 || x == 15 {
return if y <= 10 { Some(c(60, 36, 22)) } else { None };
@@ -365,7 +365,7 @@ fn gen_atlas() -> Image {
_ => c(100, 60, 120),
})
});
// Werkzeug-Icons
// tool icons
cv.cell(I_PICK, |x, y| {
let handle = (x + y == 16 || x + y == 17) && x >= 3;
let head = (y == 3 || y == 4) && (2..=13).contains(&x) && (x - 8).abs() + (y - 2) * 2 < 9;
@@ -599,7 +599,7 @@ pub fn sprite_index(k: EK) -> u32 {
SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32
}
// Zeile 0: normale Sprites, Zeile 1: weiße Silhouetten (Treffer-Blitz)
// row 0: normal sprites, row 1: white silhouettes (hit flash)
fn gen_sprites() -> Image {
let n = SPRITE_KINDS.len() as u32;
let mut img = Image::gen_image_color((n * SPR_CELL) as u16, (2 * SPR_CELL) as u16, Color::new(0.0, 0.0, 0.0, 0.0));
@@ -625,7 +625,7 @@ impl Gfx {
atlas.set_filter(FilterMode::Nearest);
let sprites = Texture2D::from_image(&gen_sprites());
sprites.set_filter(FilterMode::Nearest);
// Schrift laden; Hintergrund transparent machen, Glyphen weiß (zum Einfärben)
// load the font; transparent background, white glyphs (for tinting)
let mut img = Image::from_file_with_format(include_bytes!("../assets/dbyte_1x.png"), Some(ImageFormat::Png)).expect("Schrift");
for px in img.get_image_data_mut().iter_mut() {
let on = px[3] > 128 && px[0] > 128;
@@ -681,7 +681,7 @@ impl Gfx {
);
}
// Einzelnes CP437-Zeichen zeichnen (x, y = linke obere Ecke)
// draw a single CP437 glyph (x, y = top-left corner)
pub fn glyph(&self, code: u8, x: f32, y: f32, scale: f32, color: Color) {
let src = Rect::new((code % 16) as f32 * GW, (code / 16) as f32 * GH, GW, GH);
draw_texture_ex(
@@ -693,7 +693,7 @@ impl Gfx {
);
}
// Text zeichnen (x, y = linke obere Ecke); scale sollte ganzzahlig sein
// draw text (x, y = top-left corner); scale should be an integer
pub fn text(&self, s: &str, x: f32, y: f32, scale: f32, color: Color) {
let mut cx = x;
for ch in s.chars() {
@@ -708,13 +708,13 @@ impl Gfx {
s.chars().count() as f32 * GW * scale
}
// Text mit dunklem Schatten
// text with a dark shadow
pub fn text_sh(&self, s: &str, x: f32, y: f32, scale: f32, color: Color) {
self.text(s, x + scale, y + scale, scale, Color::new(0.0, 0.0, 0.0, color.a * 0.85));
self.text(s, x, y, scale, color);
}
// Fenster mit CP437-Rahmen; Größe in Zeichen (cols x rows inkl. Rahmen)
// window with a CP437 frame; size in characters (cols x rows incl. frame)
pub fn window(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color) {
let cw = GW * scale;
let ch = GH * scale;
@@ -740,7 +740,7 @@ impl Gfx {
}
}
// Fenster mit Titel in der oberen Rahmenlinie
// window with a title in the top frame line
pub fn window_titled(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color, title: &str, tcol: Color) {
self.window(x, y, cols, rows, scale, double, frame, bg);
if !title.is_empty() {
+87
View File
@@ -0,0 +1,87 @@
// Internationalisation: in-game texts live in lang/*.lang files (key = text).
// English is the reference language and the fallback for missing keys.
// To add a language, create lang/<code>.lang and add it to SOURCES.
use std::cell::RefCell;
use std::collections::HashMap;
const SOURCES: &[(&str, &str)] = &[("en", include_str!("../lang/en.lang")), ("de", include_str!("../lang/de.lang"))];
struct I18n {
langs: Vec<(String, HashMap<String, String>)>,
current: usize,
}
fn parse(src: &str) -> HashMap<String, String> {
let mut m = HashMap::new();
for line in src.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((k, v)) = line.split_once('=') {
m.insert(k.trim().to_string(), v.trim().to_string());
}
}
m
}
thread_local! {
static I18N: RefCell<I18n> = RefCell::new(I18n {
langs: SOURCES.iter().map(|(code, src)| (code.to_string(), parse(src))).collect(),
current: 0,
});
}
// Available languages: (code, display name in that language)
pub fn languages() -> Vec<(String, String)> {
I18N.with(|i| {
i.borrow().langs.iter().map(|(c, m)| (c.clone(), m.get("lang.name").cloned().unwrap_or_else(|| c.clone()))).collect()
})
}
pub fn set_language(code: &str) -> bool {
I18N.with(|i| {
let mut i = i.borrow_mut();
if let Some(k) = i.langs.iter().position(|(c, _)| c == code) {
i.current = k;
true
} else {
false
}
})
}
#[allow(dead_code)] // used by the settings menu
pub fn language() -> String {
I18N.with(|i| {
let i = i.borrow();
i.langs[i.current].0.clone()
})
}
// Translate a key (fallback: English, then the key itself)
pub fn tr(key: &str) -> String {
I18N.with(|i| {
let i = i.borrow();
i.langs[i.current].1.get(key).or_else(|| i.langs[0].1.get(key)).cloned().unwrap_or_else(|| key.to_string())
})
}
// Translate with arguments. Arguments starting with '@' are keys and get translated too.
pub fn trf(key: &str, args: &[String]) -> String {
let mut s = tr(key);
for (n, a) in args.iter().enumerate() {
let v = if let Some(k) = a.strip_prefix('@') { tr(k) } else { a.clone() };
s = s.replace(&format!("{{{}}}", n), &v);
}
s
}
// Translate a key in a specific language (used by the language selection dialog)
pub fn tr_in(code: &str, key: &str) -> String {
I18N.with(|i| {
let i = i.borrow();
i.langs.iter().find(|(c, _)| c == code).and_then(|(_, m)| m.get(key)).cloned().unwrap_or_else(|| key.to_string())
})
}
+13
View File
@@ -2,7 +2,9 @@
mod game;
mod gfx;
mod i18n;
mod map;
mod platform;
mod render3d;
mod sound;
mod ui;
@@ -59,6 +61,11 @@ async fn main() {
let mut seed = (miniquad::date::now() * 1000.0) as u32 | 1;
let (mut game, mut cam) = new_game(seed);
let mut ui = Ui::new();
// language: stored choice, otherwise ask on first start
match platform::storage_get("dc.lang") {
Some(code) if i18n::set_language(&code) => {}
_ => ui.show_langsel = true,
}
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false };
loop {
@@ -234,6 +241,12 @@ async fn main() {
(game, cam) = new_game(seed);
ui.show_help = false;
}
UiAction::SelectLanguage(code) => {
i18n::set_language(&code);
platform::storage_set("dc.lang", &code);
ui.show_langsel = false;
game.sfx_ui(game::Sfx::Click);
}
UiAction::Consumed => {}
UiAction::None => {
if game.state == 0 {
+22 -22
View File
@@ -1,4 +1,4 @@
// Karte, Kacheln, Generierung und Wegfindung.
// Map, tiles, generation and path finding.
use std::cmp::Reverse;
use std::collections::BinaryHeap;
@@ -34,13 +34,13 @@ pub enum Owner {
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
pub hp: f32, // dig resistance
pub gold: i32, // gold left (gold vein)
pub marked: bool, // marked for digging
pub reserved: u32, // entity id + 1 working on this tile
pub food: f32, // mushrooms in the mushroom garden
pub bed: u32, // entity id + 1 sleeping here
pub var: u8, // texture variant
}
impl Tile {
@@ -97,7 +97,7 @@ impl Rng {
pub struct Map {
pub t: Vec<Tile>,
pub core: (i32, i32), // linke obere Ecke des Kerns (3x3)
pub core: (i32, i32), // top-left corner of the core (3x3)
pub portal: (i32, i32),
pub gate: (i32, i32),
}
@@ -119,7 +119,7 @@ impl Map {
let mut s = seed;
loop {
let m = Map::try_generate(s);
// Sicherstellen, dass Helden den Kern erreichen können
// make sure heroes can reach the core
if m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() {
return m;
}
@@ -163,21 +163,21 @@ impl Map {
}
};
// Felsbrocken
// rock clusters
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
// gold veins
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
// guaranteed gold near the start
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) };
@@ -197,7 +197,7 @@ impl Map {
}
};
// Kleine natürliche Höhlen
// small natural caves
for _ in 0..4 {
let cx = rng.range(20, 48);
let cy = rng.range(6, 58);
@@ -206,9 +206,9 @@ impl Map {
room(&mut t, cx - w, cy - h, cx + w, cy + h, Owner::None);
}
// Startbereich
// starting area
room(&mut t, core.0 - 3, core.1 - 3, core.0 + 5, core.1 + 5, Owner::Player);
// Kern 3x3
// core 3x3
for y in 0..3 {
for x in 0..3 {
let tl = &mut t[idx(core.0 + x, core.1 + y)];
@@ -216,14 +216,14 @@ impl Map {
tl.owner = Owner::Player;
}
}
// Portalhöhle
// portal cave
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
// hero gate
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 {
@@ -240,9 +240,9 @@ impl Map {
Map { t, core, portal, gate }
}
// Allgemeine Dijkstra-Suche.
// cost(i) -> None = unpassierbar, Some(c) = Kosten zum Betreten.
// goal(i) -> true = Ziel erreicht.
// Generic Dijkstra search.
// cost(i) -> None = impassable, Some(c) = cost of entering.
// goal(i) -> true = goal reached.
pub fn search<C, G>(&self, start: usize, cost: C, goal: G, diag: bool) -> Option<Vec<usize>>
where
C: Fn(usize) -> Option<u32>,
@@ -279,7 +279,7 @@ impl Map {
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)
// diagonal only if both neighbours are walkable (no corner cutting)
if c > 1 || !self.walk(x + dx, y) || !self.walk(x, y + dy) {
continue;
}
+50
View File
@@ -0,0 +1,50 @@
// Browser integration via our own miniquad JS plugin (www/dc_plugin.js):
// persistent key/value storage (localStorage). Native builds use an in-memory fallback.
#[cfg(target_arch = "wasm32")]
mod imp {
extern "C" {
fn dc_storage_get(key: *const u8, key_len: u32) -> i32;
fn dc_storage_take(dst: *mut u8);
fn dc_storage_set(key: *const u8, key_len: u32, val: *const u8, val_len: u32);
}
pub fn storage_get(key: &str) -> Option<String> {
unsafe {
let n = dc_storage_get(key.as_ptr(), key.len() as u32);
if n < 0 {
return None;
}
let mut buf = vec![0u8; n as usize];
dc_storage_take(buf.as_mut_ptr());
String::from_utf8(buf).ok()
}
}
pub fn storage_set(key: &str, val: &str) {
unsafe { dc_storage_set(key.as_ptr(), key.len() as u32, val.as_ptr(), val.len() as u32) }
}
}
#[cfg(not(target_arch = "wasm32"))]
mod imp {
use std::cell::RefCell;
use std::collections::HashMap;
thread_local! {
static STORE: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
}
pub fn storage_get(key: &str) -> Option<String> {
STORE.with(|s| s.borrow().get(key).cloned())
}
pub fn storage_set(key: &str, val: &str) {
STORE.with(|s| s.borrow_mut().insert(key.to_string(), val.to_string()));
}
}
pub use imp::*;
// Lets the JS plugin loader know this crate uses the "dungeoncraft" plugin
#[no_mangle]
pub extern "C" fn dungeoncraft_crate_version() -> u32 {
1
}
+8 -8
View File
@@ -1,6 +1,6 @@
// Sound-Wiedergabe. Die Spiellogik meldet Ereignisse (Sfx), hier werden sie abgespielt.
// Klänge werden später generiert und per `SoundBank::set` (WAV-Bytes) registriert.
// Ohne registrierten Klang bleibt ein Ereignis stumm.
// Sound playback. The game logic reports events (Sfx), they are played here.
// Sounds will be generated later and registered via `SoundBank::set` (WAV bytes).
// Events without a registered sound stay silent.
use crate::game::{Sfx, SfxEvent, SFX_ALL};
use macroquad::audio::{load_sound_from_bytes, play_sound, PlaySoundParams, Sound};
@@ -18,7 +18,7 @@ impl SoundBank {
SoundBank { sounds: vec![None; SFX_ALL.len()], last: vec![-1.0; SFX_ALL.len()], volume: 0.7, muted: false }
}
// Klang für ein Ereignis registrieren (WAV- oder OGG-Bytes)
// register the sound for an event (WAV or OGG bytes)
#[allow(dead_code)]
pub async fn set(&mut self, sfx: Sfx, bytes: &[u8]) -> bool {
match load_sound_from_bytes(bytes).await {
@@ -30,14 +30,14 @@ impl SoundBank {
}
}
// Hier werden später die prozedural erzeugten Klänge registriert.
// procedurally generated sounds will be registered here
pub async fn generate_all(&mut self) {}
pub fn change_volume(&mut self, d: f32) {
self.volume = (self.volume + d).clamp(0.0, 1.0);
}
// Mindestabstand zwischen zwei gleichen Klängen (gegen Klangbrei)
// minimum gap between two identical sounds (avoids a wall of noise)
fn min_gap(sfx: Sfx) -> f64 {
match sfx {
Sfx::Dig | Sfx::GoldDig | Sfx::MeleeHit | Sfx::Shoot | Sfx::GateHit | Sfx::CoreHit => 0.08,
@@ -56,7 +56,7 @@ impl SoundBank {
if now - self.last[i] < Self::min_gap(ev.sfx) {
return;
}
// Lautstärke nach Entfernung zur Bildschirmmitte
// volume by distance to the listener
let mut vol = self.volume;
if let Some((wx, wy)) = ev.pos {
let d = ((wx - listener.x).powi(2) + (wy - listener.y).powi(2)).sqrt();
@@ -71,7 +71,7 @@ impl SoundBank {
}
}
// Hilfsfunktion für die spätere Generierung: Samples (-1..1, mono) -> WAV (16 bit PCM)
// helper for sound generation: samples (-1..1, mono) -> WAV (16 bit PCM)
#[allow(dead_code)]
pub fn wav_from_samples(samples: &[f32], rate: u32) -> Vec<u8> {
let data_len = (samples.len() * 2) as u32;
+181 -121
View File
@@ -1,15 +1,17 @@
// Benutzeroberfläche im Canvas: CP437-Fenster mit DINOBYTE-Schrift.
// In-canvas user interface: CP437 windows with the DINOBYTE bitmap font.
// All texts come from the language files (see i18n.rs).
use crate::game::*;
use crate::gfx::*;
use crate::i18n::{languages, tr, trf};
use crate::map::*;
use crate::render3d::{minimap_color, Cam};
use crate::sound::SoundBank;
use macroquad::prelude::*;
pub const S: f32 = 2.0; // UI-Skalierung der Schrift
pub const CW: f32 = GW * S; // Zeichenbreite in px
pub const CH: f32 = GH * S; // Zeichenhöhe in px
pub const S: f32 = 2.0; // UI font scale
pub const CW: f32 = GW * S; // character width in px
pub const CH: f32 = GH * S; // character height in px
pub const FRAME: Color = Color::new(0.62, 0.52, 0.36, 1.0);
pub const FRAME_HI: Color = Color::new(0.95, 0.8, 0.4, 1.0);
@@ -52,37 +54,40 @@ pub const TOOLS: [Tool; 12] = [
pub const TOOL_KEYS: [&str; 12] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "X", "V"];
pub struct ToolInfo {
pub label: &'static str,
pub name: &'static str,
pub desc: &'static str,
pub label: String,
pub name: String,
pub desc: String,
pub icon: u32,
}
pub fn tool_info(t: Tool) -> ToolInfo {
let i = |label, name, desc, icon| ToolInfo { label, name, desc, icon };
match t {
Tool::Dig => i("Graben", "Graben", "Erde und Gold markieren. Ziehen für Rechteck; auf markiertem Feld beginnen hebt Markierung auf.", I_PICK),
Tool::Hand => i("Hand", "Hand", "Eigene Einheit aufnehmen (max. 8). Rechtsklick setzt sie auf eigenem Gebiet ab.", I_HAND),
Tool::Room(0) => i("Schatz", "Schatzkammer", "Lagert Gold: +500 Kapazität pro Feld.", T_TREASURY),
Tool::Room(1) => i("Quartier", "Quartier", "Ein Schlafplatz pro Feld. Bestimmt, wie viele Kreaturen kommen.", T_LAIR),
Tool::Room(2) => i("Pilze", "Pilzgarten", "Nahrung für Kreaturen. Ab 6 Feldern kommen Höhlenspinnen.", T_FARM),
Tool::Room(3) => i("Training", "Trainingshalle", "Kreaturen steigen Stufen auf (kostet Gold). Ab 9 Feldern kommen Steinoger.", T_TRAINING),
Tool::Room(4) => i("Bücher", "Bibliothek", "Kreaturen erzeugen Mana. Ab 6 Feldern kommen Schattenmagier.", T_LIBRARY),
Tool::Sell => i("Verkauf", "Verkaufen", "Räume zurück in Boden verwandeln (50 % Erstattung).", I_SELL),
Tool::Flag => i("Flagge", "Angriffsflagge", "Kampfbereite Kreaturen sammeln sich hier. Nahe am Heldentor greifen sie es an. Erneut klicken entfernt sie.", I_FLAG),
Tool::Lightning => i("Blitz", "Blitz", "55 Schaden an Helden im Umkreis.", I_BOLT),
Tool::Heal => i("Heilung", "Heilung", "Heilt eigene Einheiten im Umkreis um 50 %.", I_HEAL),
Tool::Summon => i("Wicht", "Grubenwicht beschwören", "Erschafft einen neuen Grubenwicht auf eigenem Gebiet.", I_SUMMON),
Tool::Room(_) => i("?", "?", "", 0),
}
let (id, icon, room) = match t {
Tool::Dig => ("dig", I_PICK, None),
Tool::Hand => ("hand", I_HAND, None),
Tool::Room(0) => ("treasury", T_TREASURY, Some(0)),
Tool::Room(1) => ("lair", T_LAIR, Some(1)),
Tool::Room(2) => ("farm", T_FARM, Some(2)),
Tool::Room(3) => ("training", T_TRAINING, Some(3)),
Tool::Room(_) => ("library", T_LIBRARY, Some(4)),
Tool::Sell => ("sell", I_SELL, None),
Tool::Flag => ("flag", I_FLAG, None),
Tool::Lightning => ("lightning", I_BOLT, None),
Tool::Heal => ("heal", I_HEAL, None),
Tool::Summon => ("summon", I_SUMMON, None),
};
let name = match room {
Some(r) => tr(ROOM_NAMES[r]),
None => tr(&format!("tool.{}.name", id)),
};
ToolInfo { label: tr(&format!("tool.{}.label", id)), name, desc: tr(&format!("tool.{}.desc", id)), icon }
}
pub fn tool_cost(t: Tool, g: &Game) -> String {
match t {
Tool::Room(r) => format!("{} Gold/Feld", ROOM_COST[r]),
Tool::Lightning => "50 Mana".into(),
Tool::Heal => "60 Mana".into(),
Tool::Summon => format!("{} Mana", g.summon_cost() as i32),
Tool::Room(r) => trf("cost.gold_tile", &[ROOM_COST[r].to_string()]),
Tool::Lightning => trf("cost.mana", &["50".into()]),
Tool::Heal => trf("cost.mana", &["60".into()]),
Tool::Summon => trf("cost.mana", &[(g.summon_cost() as i32).to_string()]),
_ => String::new(),
}
}
@@ -93,13 +98,15 @@ pub enum UiAction {
MinimapJump(f32, f32),
CloseHelp,
NewGame,
SelectLanguage(String),
Consumed,
}
pub struct Ui {
pub tool: Tool,
pub show_help: bool,
pub log: Vec<(String, f64)>,
pub show_langsel: bool,
pub log: Vec<(Msg, f64)>,
pub rects: Vec<Rect>,
minimap_img: Image,
minimap_tex: Texture2D,
@@ -107,6 +114,7 @@ pub struct Ui {
minimap_rect: Rect,
tool_rects: Vec<Rect>,
end_button: Rect,
lang_rects: Vec<(Rect, String)>,
}
fn wrap(s: &str, cols: usize) -> Vec<String> {
@@ -148,6 +156,7 @@ impl Ui {
Ui {
tool: Tool::Dig,
show_help: true,
show_langsel: false,
log: Vec::new(),
rects: Vec::new(),
minimap_img: img,
@@ -156,11 +165,12 @@ impl Ui {
minimap_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
tool_rects: Vec::new(),
end_button: Rect::new(0.0, 0.0, 0.0, 0.0),
lang_rects: Vec::new(),
}
}
pub fn over_ui(&self, x: f32, y: f32) -> bool {
self.show_help || self.rects.iter().any(|r| r.contains(vec2(x, y)))
self.show_help || self.show_langsel || self.rects.iter().any(|r| r.contains(vec2(x, y)))
}
pub fn minimap_contains(&self, x: f32, y: f32) -> bool {
@@ -174,6 +184,14 @@ impl Ui {
pub fn click(&mut self, x: f32, y: f32, g: &Game) -> UiAction {
let p = vec2(x, y);
if self.show_langsel {
for (r, code) in &self.lang_rects {
if r.contains(p) {
return UiAction::SelectLanguage(code.clone());
}
}
return UiAction::Consumed;
}
if g.state != 0 {
if self.end_button.contains(p) {
return UiAction::NewGame;
@@ -204,7 +222,7 @@ impl Ui {
let sh = screen_height();
let (mx, my) = mouse_position();
// ---------------- obere Leiste
// ---------------- top bar
let cols = (sw / CW).ceil() as i32;
gfx.window(0.0, 0.0, cols, 3, S, false, FRAME, BG);
self.rects.push(Rect::new(0.0, 0.0, sw, 3.0 * CH));
@@ -216,31 +234,34 @@ impl Ui {
gfx.text(value, *x, y, S, vc);
*x += gfx.measure(value, S) + CW * 2.0;
};
seg("Gold", GOLD, &format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut x);
gfx.text("Mana", x, y, S, MANA);
x += gfx.measure("Mana", S) + CW * 0.5;
seg(&tr("hud.gold"), GOLD, &format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut x);
let mana = tr("hud.mana");
gfx.text(&mana, x, y, S, MANA);
x += gfx.measure(&mana, S) + CW * 0.5;
bar(x, y + 3.0, 70.0, 10.0, g.mana / MANA_MAX, MANA);
x += 70.0 + CW * 0.5;
gfx.text(&format!("{}", g.mana as i32), x, y, S, TXT);
x += CW * 5.0;
gfx.text("Kern", x, y, S, RED);
x += gfx.measure("Kern", S) + CW * 0.5;
let core = tr("hud.core");
gfx.text(&core, x, y, S, RED);
x += gfx.measure(&core, S) + CW * 0.5;
bar(x, y + 3.0, 70.0, 10.0, g.core_hp / CORE_HP, RED);
x += 70.0 + CW * 1.5;
gfx.text("Tor", x, y, S, BLUE);
x += gfx.measure("Tor", S) + CW * 0.5;
let gate = tr("hud.gate");
gfx.text(&gate, x, y, S, BLUE);
x += gfx.measure(&gate, S) + CW * 0.5;
bar(x, y + 3.0, 70.0, 10.0, g.gate_hp / GATE_HP, BLUE);
x += 70.0 + CW * 1.5;
let cap = g.counts[1].min(30);
seg("Kreaturen", TXT, &format!("{}/{}", g.creature_count(), cap), TXT, &mut x);
seg("Wichte", TXT, &format!("{}", g.worker_count()), TXT, &mut x);
seg(&tr("hud.creatures"), TXT, &format!("{}/{}", g.creature_count(), cap), TXT, &mut x);
seg(&tr("hud.workers"), TXT, &format!("{}", g.worker_count()), TXT, &mut x);
let wave_col = if g.wave_timer < 30.0 { RED } else { TXT };
seg(&format!("Welle {}", g.wave + 1), wave_col, &mmss(g.wave_timer), wave_col, &mut x);
seg("Armee", TXT, &format!("{}", g.army_value() as i32), TXT, &mut x);
let vol = if snd.muted { "aus".to_string() } else { format!("{}%", (snd.volume * 100.0).round() as i32) };
seg(&trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col, &mmss(g.wave_timer), wave_col, &mut x);
seg(&tr("hud.army"), TXT, &format!("{}", g.army_value() as i32), TXT, &mut x);
let vol = if snd.muted { tr("hud.off") } else { format!("{}%", (snd.volume * 100.0).round() as i32) };
seg("♪", DIM, &vol, DIM, &mut x);
// ---------------- Werkzeugleiste
// ---------------- toolbar
let tb_rows = 5;
let tb_y = sh - tb_rows as f32 * CH;
gfx.window(0.0, tb_y, cols, tb_rows, S, false, FRAME, BG);
@@ -263,13 +284,13 @@ impl Ui {
let ti = tool_info(t);
gfx.draw_cell(ti.icon, bx + (bw - 32.0) * 0.5, by + 4.0, 32.0, 32.0, WHITE);
gfx.text(TOOL_KEYS[i], bx + 3.0, by + 3.0, 1.0, if sel { FRAME_HI } else { DIM });
let lw = gfx.measure(ti.label, 1.0);
gfx.text(ti.label, bx + (bw - lw) * 0.5, by + 42.0, 1.0, if sel { FRAME_HI } else { TXT });
let lw = gfx.measure(&ti.label, 1.0);
gfx.text(&ti.label, bx + (bw - lw) * 0.5, by + 42.0, 1.0, if sel { FRAME_HI } else { TXT });
}
// Hand
// hand contents
let hx = CW + TOOLS.len() as f32 * (bw + 4.0) + CW;
if hx + 120.0 < sw - 200.0 {
gfx.text("Hand", hx, tb_y + CH, S, TXT);
gfx.text(&tr("hud.hand"), hx, tb_y + CH, S, TXT);
for (k, id) in g.hand.iter().enumerate() {
if let Some(e) = g.ents.iter().find(|e| e.id == *id) {
let sx = hx + (k % 4) as f32 * 26.0;
@@ -284,18 +305,18 @@ impl Ui {
}
}
if g.hand.is_empty() {
gfx.text("leer", hx, tb_y + CH * 2.5, 1.0, DIM);
gfx.text(&tr("hud.empty"), hx, tb_y + CH * 2.5, 1.0, DIM);
}
}
// ---------------- Minikarte
// ---------------- minimap
let mm_cols = 16;
let mm_rows = 12;
let mm_w = mm_cols as f32 * CW;
let mm_h = mm_rows as f32 * CH;
let mm_x = sw - mm_w;
let mm_y = tb_y - mm_h;
gfx.window_titled(mm_x, mm_y, mm_cols, mm_rows, S, false, FRAME, BG, "Karte", FRAME_HI);
gfx.window_titled(mm_x, mm_y, mm_cols, mm_rows, S, false, FRAME, BG, &tr("ui.map"), FRAME_HI);
self.rects.push(Rect::new(mm_x, mm_y, mm_w, mm_h));
if now - self.minimap_t > 0.25 {
self.minimap_t = now;
@@ -335,7 +356,7 @@ impl Ui {
draw_line(pa.x, pa.y, pb.x, pb.y, 1.5, WHITE);
}
// ---------------- Infofeld
// ---------------- info panel
if !info.is_empty() {
let icols = 30;
let mut lines: Vec<(String, Color)> = Vec::new();
@@ -353,14 +374,14 @@ impl Ui {
}
}
// ---------------- Meldungen
let recent: Vec<&(String, f64)> = self.log.iter().filter(|(_, t)| now - t < 14.0).collect();
// ---------------- messages (translated at display time, so a language switch applies at once)
let recent: Vec<&(Msg, f64)> = self.log.iter().filter(|(_, t)| now - t < 14.0).collect();
if !recent.is_empty() {
let mcols = (((sw - mm_w) / CW) as i32 - 2).clamp(20, 70);
let mut lines: Vec<(String, f32)> = Vec::new();
for (s, t) in recent.iter().rev().take(5).rev() {
for (m, t) in recent.iter().rev().take(5).rev() {
let a = (1.0 - ((now - t) as f32 - 11.0) / 3.0).clamp(0.2, 1.0);
for l in wrap(s, mcols as usize - 4) {
for l in wrap(&trf(m.key, &m.args), mcols as usize - 4) {
lines.push((l, a));
}
}
@@ -381,23 +402,25 @@ impl Ui {
let t = TOOLS[i];
let ti = tool_info(t);
let tcols = 34;
let mut lines: Vec<(String, Color)> = wrap(ti.desc, tcols - 4).into_iter().map(|l| (l, TXT)).collect();
let mut lines: Vec<(String, Color)> = wrap(&ti.desc, tcols - 4).into_iter().map(|l| (l, TXT)).collect();
let cost = tool_cost(t, g);
if !cost.is_empty() {
lines.push((format!("Kosten: {}", cost), GOLD));
lines.push((trf("ui.cost", &[cost]), GOLD));
}
lines.push((format!("Taste: {}", TOOL_KEYS[i]), DIM));
lines.push((trf("ui.key", &[TOOL_KEYS[i].to_string()]), DIM));
let rows = lines.len() as i32 + 2;
let tx = (mx - tcols as f32 * CW * 0.5).clamp(0.0, sw - tcols as f32 * CW);
let ty = tb_y - rows as f32 * CH - 4.0;
gfx.window_titled(tx, ty, tcols as i32, rows, S, false, FRAME_HI, BG, ti.name, FRAME_HI);
gfx.window_titled(tx, ty, tcols as i32, rows, S, false, FRAME_HI, BG, &ti.name, FRAME_HI);
for (k, (l, c)) in lines.iter().enumerate() {
gfx.text(l, tx + CW * 2.0, ty + CH * (k as f32 + 1.0), S, *c);
}
}
// ---------------- Dialoge
if g.state != 0 {
// ---------------- dialogs
if self.show_langsel {
self.draw_langsel(gfx);
} else if g.state != 0 {
self.draw_end(g, gfx);
} else if self.show_help {
self.draw_help(gfx);
@@ -406,36 +429,37 @@ impl Ui {
let x = (sw - w as f32 * CW) * 0.5;
let y = sh * 0.35;
gfx.window(x, y, w, 5, S, true, FRAME_HI, BG);
let t = "PAUSE";
gfx.text(t, (sw - gfx.measure(t, 3.0)) * 0.5, y + CH, 3.0, FRAME_HI);
let t2 = "P: weiter";
gfx.text(t2, (sw - gfx.measure(t2, S)) * 0.5, y + CH * 3.0, S, DIM);
let t = tr("ui.pause");
gfx.text(&t, (sw - gfx.measure(&t, 3.0)) * 0.5, y + CH, 3.0, FRAME_HI);
let t2 = tr("ui.pause_hint");
gfx.text(&t2, (sw - gfx.measure(&t2, S)) * 0.5, y + CH * 3.0, S, DIM);
}
}
fn draw_help(&mut self, gfx: &Gfx) {
let lines: &[(&str, Color)] = &[
("Ziel: Zerstöre das Heldentor und schütze deinen Seelenkern.", GOLD),
let keys: &[(&str, Color)] = &[
("help.goal", GOLD),
("", TXT),
("Linksklick Werkzeug benutzen (Graben/Räume: Rechteck ziehen)", TXT),
("Rechtsklick Einheit aus der Hand absetzen", TXT),
("Rechts ziehen Karte verschieben (auch WASD/Pfeile, Bildrand)", TXT),
("Mitte ziehen Kamera drehen und neigen (auch Q/E, R/F)", TXT),
("Mausrad Zoomen", TXT),
("1-9, 0, X, V Werkzeug wählen", TXT),
("P / Leertaste Pause M Ton an/aus +/- Lautstärke", TXT),
("G Kachel-Umrisse an/aus H Diese Hilfe", TXT),
("help.k_left", TXT),
("help.k_right", TXT),
("help.k_pan", TXT),
("help.k_rot", TXT),
("help.k_zoom", TXT),
("help.k_tools", TXT),
("help.k_misc", TXT),
("help.k_misc2", TXT),
("", TXT),
("1. Grabe zum Portal und lass es von Grubenwichten erobern.", TXT),
("2. Baue Quartier, Pilzgarten und Schatzkammer.", TXT),
("3. Trainiere deine Kreaturen, zahle ihren Lohn pünktlich.", TXT),
("4. Wehre die Heldenwellen ab. Sie passen sich deiner Armee an.", TXT),
("5. Setze die Angriffsflagge nahe ans Tor und greife an!", TXT),
("help.s1", TXT),
("help.s2", TXT),
("help.s3", TXT),
("help.s4", TXT),
("help.s5", TXT),
("", TXT),
("Schrift: DINOBYTE von mby (CC BY 4.0)", DIM),
("help.font", DIM),
("", TXT),
("Klick zum Starten", FRAME_HI),
("help.start", FRAME_HI),
];
let lines: Vec<(String, Color)> = keys.iter().map(|(k, c)| (if k.is_empty() { String::new() } else { tr(k) }, *c)).collect();
let cols = 74;
let rows = lines.len() as i32 + 4;
let sw = screen_width();
@@ -449,6 +473,40 @@ impl Ui {
self.rects.push(Rect::new(0.0, 0.0, sw, sh));
}
// First start: choose the language (names shown in their own language)
fn draw_langsel(&mut self, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();
draw_rectangle(0.0, 0.0, sw, sh, Color::new(0.0, 0.0, 0.0, 0.6));
let langs = languages();
let cols = 50;
let rows = 6 + langs.len() as i32 * 3;
let x = ((sw - cols as f32 * CW) * 0.5).max(0.0);
let y = ((sh - rows as f32 * CH) * 0.5).max(0.0);
// the title shows all language names, since no language is chosen yet
let title = langs.iter().map(|(c, _)| crate::i18n::tr_in(c, "langsel.title")).collect::<Vec<_>>().join(" / ");
gfx.window_titled(x, y, cols, rows, S, true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &title, FRAME_HI);
self.lang_rects.clear();
let (mx, my) = mouse_position();
for (k, (code, name)) in langs.iter().enumerate() {
let bw = 24.0 * CW;
let bx = (sw - bw) * 0.5;
let by = y + CH * (2.0 + k as f32 * 3.0);
let r = Rect::new(bx, by, bw, CH * 2.0);
let hov = r.contains(vec2(mx, my));
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hov { FRAME_HI } else { FRAME });
gfx.text(name, bx + (bw - gfx.measure(name, S)) * 0.5, by + CH * 0.5, S, TXT);
self.lang_rects.push((r, code.clone()));
}
let hint_y = y + CH * (2.0 + langs.len() as f32 * 3.0 + 0.5);
for (k, (c, _)) in langs.iter().enumerate() {
let h = crate::i18n::tr_in(c, "langsel.hint");
gfx.text(&h, x + CW * 2.0, hint_y + CH * k as f32, 1.0, DIM);
}
self.rects.push(Rect::new(0.0, 0.0, sw, sh));
}
fn draw_end(&mut self, g: &Game, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();
@@ -460,13 +518,14 @@ impl Ui {
let y = (sh - rows as f32 * CH) * 0.5;
let col = if win { GOLD } else { RED };
gfx.window(x, y, cols, rows, S, true, col, Color::new(0.05, 0.03, 0.04, 0.97));
let title = if win { "SIEG!" } else { "NIEDERLAGE" };
gfx.text(title, (sw - gfx.measure(title, 4.0)) * 0.5, y + CH * 1.5, 4.0, col);
let sub = if win { "Das Heldentor ist gefallen." } else { "Dein Seelenkern ist zerbrochen." };
gfx.text(sub, (sw - gfx.measure(sub, S)) * 0.5, y + CH * 4.5, S, TXT);
let st = format!("Spielzeit {} Wellen {} Armee {}", mmss(g.time), g.wave, g.army_value() as i32);
let title = tr(if win { "end.victory" } else { "end.defeat" });
gfx.text(&title, (sw - gfx.measure(&title, 4.0)) * 0.5, y + CH * 1.5, 4.0, col);
let sub = tr(if win { "end.victory_sub" } else { "end.defeat_sub" });
gfx.text(&sub, (sw - gfx.measure(&sub, S)) * 0.5, y + CH * 4.5, S, TXT);
let st = trf("end.stats", &[mmss(g.time), g.wave.to_string(), (g.army_value() as i32).to_string()]);
gfx.text(&st, (sw - gfx.measure(&st, S)) * 0.5, y + CH * 6.0, S, DIM);
let b = " Neues Spiel (N) ";
let b = format!(" {} ", tr("end.new"));
let b = b.as_str();
let bw = gfx.measure(b, S) + CW * 2.0;
let bx = (sw - bw) * 0.5;
let by = y + CH * 7.75;
@@ -480,80 +539,81 @@ impl Ui {
}
}
// Infotext für Einheit oder Kachel unter dem Mauszeiger (erste Zeile = Titel)
// Info text for the unit or tile under the mouse (first line = title)
pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec<(String, Color)> {
let mut out = Vec::new();
if let Some(j) = ent {
let e = &g.ents[j];
let s = st(e.kind);
out.push((format!("{} (Stufe {})", s.name, e.level), if e.hero { BLUE } else { FRAME_HI }));
out.push((format!("HP {}/{}", e.hp.max(0.0) as i32, e.max_hp as i32), GREEN));
out.push((job_name(e).to_string(), TXT));
out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), if e.hero { BLUE } else { FRAME_HI }));
out.push((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));
out.push((tr(job_name(e)), TXT));
if e.hero {
out.push((if e.guard { "Torwächter".into() } else { "Eindringling".into() }, BLUE));
out.push((tr(if e.guard { "info.guard" } else { "info.intruder" }), BLUE));
} else if e.creature() {
let pct = |v: f32| (v.clamp(0.0, 1.0) * 100.0).round() as i32;
out.push((format!("Energie {}% Hunger {}%", pct(e.energy), pct(e.hunger / 2.0)), TXT));
let pct = |v: f32| ((v.clamp(0.0, 1.0) * 100.0).round() as i32).to_string();
out.push((trf("info.needs", &[pct(e.energy), pct(e.hunger / 2.0)]), TXT));
let mood = if e.happy > 0.6 {
("zufrieden", GREEN)
("mood.happy", GREEN)
} else if e.happy > 0.25 {
("mürrisch", GOLD)
("mood.grumpy", GOLD)
} else {
("wütend", RED)
("mood.angry", RED)
};
out.push((format!("Laune: {}", mood.0), mood.1));
out.push((format!("Lohn: {} Gold", (s.wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)) as i32), GOLD));
out.push((trf("info.mood", &[tr(mood.0)]), mood.1));
let wage = (s.wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)) as i32;
out.push((trf("info.wage", &[wage.to_string()]), GOLD));
} else if e.carry > 0 {
out.push((format!("Trägt {} Gold", e.carry), GOLD));
out.push((trf("info.carries", &[e.carry.to_string()]), GOLD));
}
return out;
}
let Some((x, y)) = tile else { return out };
let Some(t) = g.map.get(x, y) else { return out };
let owner = match t.owner {
Owner::Player => "erobert",
Owner::Hero => "Heldengebiet",
Owner::None => "neutral",
Owner::Player => "owner.player",
Owner::Hero => "owner.hero",
Owner::None => "owner.none",
};
match t.kind {
Kind::Rock => out.push(("Fels".into(), DIM)),
Kind::Earth => out.push(("Erde".into(), FRAME_HI)),
Kind::Rock => out.push((tr("tile.rock"), DIM)),
Kind::Earth => out.push((tr("tile.earth"), FRAME_HI)),
Kind::Gold => {
out.push(("Goldader".into(), GOLD));
out.push((format!("{} Gold", t.gold), GOLD));
out.push((tr("tile.gold"), GOLD));
out.push((trf("tile.gold_amount", &[t.gold.to_string()]), GOLD));
}
Kind::Floor => {
out.push(("Boden".into(), TXT));
out.push((owner.into(), DIM));
out.push((tr("tile.floor"), TXT));
out.push((tr(owner), DIM));
}
Kind::Core => {
out.push(("Seelenkern".into(), RED));
out.push((format!("HP {}/{}", g.core_hp as i32, CORE_HP as i32), RED));
out.push((tr("tile.core"), RED));
out.push((trf("info.hp", &[(g.core_hp as i32).to_string(), (CORE_HP as i32).to_string()]), RED));
}
Kind::Portal => {
out.push(("Portal".into(), MANA));
out.push((if g.portal_owned() { "erobert".into() } else { "neutral: Wichte können es erobern".into() }, DIM));
out.push((tr("tile.portal"), MANA));
out.push((tr(if g.portal_owned() { "portal.claimed" } else { "portal.neutral" }), DIM));
}
Kind::Gate => {
out.push(("Heldentor".into(), BLUE));
out.push((format!("HP {}/{}", g.gate_hp as i32, GATE_HP as i32), BLUE));
out.push((tr("tile.gate"), BLUE));
out.push((trf("info.hp", &[(g.gate_hp as i32).to_string(), (GATE_HP as i32).to_string()]), BLUE));
}
k => {
if let Some(r) = room_index(k) {
out.push((ROOM_NAMES[r].into(), FRAME_HI));
out.push((tr(ROOM_NAMES[r]), FRAME_HI));
match k {
Kind::Farm => out.push((format!("{} Pilze", t.food as i32), GREEN)),
Kind::Lair => out.push((if t.bed != 0 { "Schlafplatz belegt".into() } else { "Schlafplatz frei".into() }, DIM)),
Kind::Farm => out.push((trf("farm.food", &[(t.food as i32).to_string()]), GREEN)),
Kind::Lair => out.push((tr(if t.bed != 0 { "lair.taken" } else { "lair.free" }), DIM)),
_ => {}
}
}
}
}
if t.kind == Kind::Rock {
out.push(("unzerstörbar".into(), DIM));
out.push((tr("tile.indestructible"), DIM));
}
if t.marked {
out.push(("zum Graben markiert".into(), GOLD));
out.push((tr("tile.marked"), GOLD));
}
out
}