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;
|
||||
}
|
||||
}
|
||||
|
||||
+30
-30
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user