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

+2 -2
View File
@@ -1,4 +1,4 @@
# quad-snd (macroquad-Audio) importiert JS-Funktionen aus mq_js_bundle.js. # quad-snd (macroquad audio) imports JS functions from mq_js_bundle.js, and our own
# Neuere Rust-Versionen verlangen dafür explizit --allow-undefined. # plugin (www/dc_plugin.js) provides more. Newer Rust versions need --allow-undefined for that.
[target.wasm32-unknown-unknown] [target.wasm32-unknown-unknown]
rustflags = ["-C", "link-arg=--allow-undefined"] rustflags = ["-C", "link-arg=--allow-undefined"]
+6 -6
View File
@@ -1,9 +1,9 @@
# Credits # Credits
## Schrift: DINOBYTE bitmap font ## Font: DINOBYTE bitmap font
- Autor: **mby** – https://mby.itch.io/dinobyte - Author: **mby** – https://mby.itch.io/dinobyte
- Lizenz: **Creative Commons Attribution 4.0 International (CC BY 4.0)** – https://creativecommons.org/licenses/by/4.0/ - License: **Creative Commons Attribution 4.0 International (CC BY 4.0)** – https://creativecommons.org/licenses/by/4.0/
- Datei: `assets/dbyte_1x.png` (unverändert). Im Spiel wird sie ganzzahlig skaliert und eingefärbt dargestellt. - File: `assets/dbyte_1x.png` (unmodified). In the game it is scaled by integer factors and tinted. The swapped glyphs ö/ò are corrected in the character mapping, not in the file.
## Alles andere ## Everything else
Code, prozedurale Grafiken und Spieldesign: DungeonCraft-Projekt. Code, procedural graphics and game design: the DungeonCraft project.
+55 -54
View File
@@ -1,64 +1,65 @@
# DungeonCraft – Design-Entscheidungen # DungeonCraft – Design Decisions
Eigenständiges Dungeon-Management-Spiel (Genre: Dungeon-Aufbau & Verteidigung), läuft als WASM im Browser. An original dungeon management game (genre: dungeon building & defence) that runs as WASM in the browser.
## Technik ## Technology
- Rust + **macroquad** (WebGL), Ziel `wasm32-unknown-unknown` - Rust + **macroquad** (WebGL), target `wasm32-unknown-unknown`
- Oberfläche komplett **im Canvas** - User interface drawn entirely **in the canvas**
- Schrift: **DINOBYTE** Bitmap-Font (CP437, 6×8 px, CC BY 4.0, siehe CREDITS.md) für alle Texte, ganzzahlig skaliert - Font: **DINOBYTE** bitmap font (CP437, 6×8 px, CC BY 4.0, see CREDITS.md) for all texts, integer scaled
- Alle UI-Fenster mit **CP437-Rahmen** (einfach für Panels, doppelt für Dialoge); Balken (HP/Mana/Tor) als Pixel-Balken - All UI windows use **CP437 frames** (single lines for panels, double lines for dialogs); bars (HP/mana/gate) are pixel bars
- Sound: Event-System + macroquad-Audio vorbereitet (Lautstärke, Stumm-Taste M); Klänge werden später generiert - Sound: event system + macroquad audio prepared (volume, mute key M); sounds will be generated later
- Languages: all in-game texts live in **language files** (`lang/*.lang`, key = text); English is the reference/fallback, German included, more can be added. The language is chosen on first start and stored in the browser.
- Repository language: **English** (code, comments, docs, commit messages)
## Grafik ## Graphics
- **3D**, **echt isometrisch** (orthografische Kamera) - **3D**, **true isometric** (orthographic camera)
- Kamera **frei 360° drehbar + neigbar** - Camera **freely rotatable 360° and tiltable**
- Wände mit **unregelmäßiger Oberfläche** (Rauschverformung der Wandflächen, nahtlos zwischen Nachbarn) - Walls with an **irregular surface** (noise displacement of the wall faces, seamless between neighbours)
- Texturen: **prozedurale Pixelart** (Kachel-Atlas) - Textures: **procedural pixel art** (tile atlas)
- Figuren: **Pixel-Sprites als Billboards** (immer zur Kamera gedreht) - Units: **pixel sprites as billboards** (always facing the camera)
- Licht: **Dunkel + Fackeln** – Grunddunkelheit, eroberte Bereiche und Lichtquellen (Fackeln, Kern, Portal, Tor) hell, flackerndes Licht - Lighting: **dark + torches** – dark base level; claimed areas and light sources (torches, core, portal, gate) are bright, flickering light
- **Kachel-Outlines**: dezente dunkle Linien um jedes Feld (Böden, Wandoberseiten, senkrechte Wandkanten), folgen der unregelmäßigen Oberfläche; abschaltbar (Taste G, später Grafik-Einstellungen) - **Tile outlines**: subtle dark lines around every tile (floors, wall tops, vertical wall edges), following the irregular surface; toggle with G (later in the graphics settings)
- UI bleibt 2D-Ebene darüber, Minikarte - The UI is a 2D layer on top, including a minimap
## Steuerung ## Controls
- Linksklick: aktives Werkzeug benutzen (Rechteck aufziehen für Graben/Räume) - Left click: use the active tool (drag a rectangle for digging/rooms)
- Rechtsklick: Einheit aus der Hand absetzen - Right click: drop a unit from the hand
- Kamera: WASD/Pfeiltasten, rechte Maus ziehen, Mausrad-Zoom, Bildschirmrand - Camera: WASD/arrow keys, right mouse drag, mouse wheel zoom, screen edge
- Drehen/Neigen: mittlere Maustaste ziehen, Q/E drehen, R/F neigen - Rotate/tilt: middle mouse drag, Q/E rotate, R/F tilt
- Zauber: 0 Blitz, X Heilung, V Wicht beschwören - Spells: 0 lightning, X heal, V summon imp
- Fackeln: automatisch beim Erobern; fremde Gebiete schummrig sichtbar - Torches: placed automatically when ground is claimed; foreign areas are dimly visible
## Geplant (nach 3D) ## Planned
- Repo komplett auf Englisch; Spieltexte in separaten Sprachdateien (EN + DE, erweiterbar), Sprache im Spiel umschaltbar - Menu: save (download `*.dcsave`), load (file picker + drag & drop), autosave in the browser (localStorage)
- Menü: Speichern (Download *.dcsave), Laden (Dateiauswahl + Drag & Drop), Autosave im Browser (localStorage) - Settings: language + sound, camera, graphics, game, key bindings – stored in the browser **and** in the savegame
- Einstellungen: Sprache + Ton, Kamera, Grafik, Spiel, Tastenbelegung – gespeichert im Browser **und** im Savegame - Music: own **XM/MOD replayer in Rust**, shared mixer for music + sound effects (streamed via a JS audio plugin, replaces macroquad audio)
- Musik: eigener **XM/MOD-Replayer in Rust**, gemeinsamer Mixer für Musik + Effekte (Streaming über JS-Audio-Plugin, ersetzt macroquad-Audio) - Soundtrack (composed as `.xm`, editable in MilkyTracker): title/menu, several calm build tracks, battle, victory/defeat jingles; **the title melody is a recognisable leitmotif in every piece**
- Soundtrack (als .xm komponiert, in MilkyTracker bearbeitbar): Titel/Menü, mehrere ruhige Aufbau-Tracks, Kampf, Sieg/Niederlage-Jingles; **Titelmelodie als Leitmotiv in allen Stücken erkennbar**
## Spielinhalt (volles Konzept) ## Game content
- **Graben**: Erde/Gold markieren → Grubenwichte graben, bringen Gold heim, erobern Boden - **Digging**: mark earth/gold → Pit Imps dig, bring gold home and claim ground
- **Räume**: Schatzkammer, Quartier, Pilzgarten, Trainingshalle, Bibliothek - **Rooms**: treasury, lair, mushroom garden, training hall, library
- **Portal** erobern → Kreaturen: Kobold, Höhlenspinne, Schattenmagier, Steinoger (abhängig von Räumen) - Claim the **portal** → creatures: kobold, cave spider, shadow mage, stone ogre (depending on rooms)
- **Bedürfnisse**: Schlaf, Essen, Lohn (Zahltag); unzufriedene Kreaturen gehen - **Needs**: sleep, food, wages (payday); unhappy creatures leave
- **Hand**: eigene Einheiten aufnehmen/absetzen (max. 8) - **Hand**: pick up / drop own units (max. 8)
- **Zauber** (Mana): Blitz, Heilung, Grubenwicht beschwören - **Spells** (mana): lightning, heal, summon Pit Imp
- **Helden**: Ritter, Bogenschützin, Priester, Zwergenhauer, Lichtpaladin (graben sich zum Seelenkern durch) - **Heroes**: knight, archer, priest, dwarf miner, paladin of light (they dig their way to the Soul Core)
## Ziel ## Goal
- **Sieg**: Heldentor zerstören - **Victory**: destroy the Hero Gate
- **Niederlage**: Seelenkern zerstört - **Defeat**: the Soul Core is destroyed
## Torangriff ## Attacking the gate
- **Angriffsflagge**: Spieler setzt eine Sammelflagge; freie Kreaturen marschieren hin und greifen alles in der Nähe an (auch das Tor) - **Attack flag**: the player places a rally flag; free creatures march there and attack everything nearby (including the gate)
- **Tor**: ca. 3000 HP, regeneriert langsam, wenn es nicht angegriffen wird - **Gate**: about 3000 HP, regenerates slowly when not under attack
- **Verteidiger** erscheinen bei HP-Schwellen 75 % / 50 % / 25 % (letzter Trupp mit Boss), herausfordernd - **Defenders** appear at HP thresholds 75% / 50% / 25% (the last squad with a boss), challenging
## Heldenwellen ## Hero waves
- **Endlos**, solange das Tor steht - **Endless** while the gate stands
- Stärke richtet sich nach dem **Armeewert** (Summe HP × Angriff aller Kreaturen inkl. Stufen) - Strength follows the **army value** (sum of HP × damage of all creatures incl. levels)
- Wellenstärke **mitwachsend 60 % → 100 %** des Armeewerts, mit etwas Zufall - Wave strength **grows from 60% to 100%** of the army value, with some randomness
- **Schonfrist** 5 Minuten, danach **Mindestwelle** (z. B. 2 Ritter) auch bei schwacher Armee - **Grace period** of 5 minutes, then a **minimum wave** (e.g. 2 knights) even with a weak army
- Ziel: immer schaffbar, aber herausfordernd - Goal: always beatable, but challenging
## Betrieb / Test ## Operations / testing
- Build im Docker (Multi-Stage: Rust → nginx:alpine) - Build in Docker (multi-stage: Rust → nginx:alpine)
- Testversion auf Port **18093**, später zusätzlich Subdomain über Nginx Proxy Manager - Test build on port **18093**, later additionally a subdomain via Nginx Proxy Manager
+3 -2
View File
@@ -4,18 +4,19 @@ RUN rustup target add wasm32-unknown-unknown
WORKDIR /src WORKDIR /src
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
COPY .cargo .cargo COPY .cargo .cargo
# Abhängigkeiten vorab bauen (Docker-Cache) # build dependencies first (Docker layer cache)
RUN mkdir src && echo "fn main() {}" > src/main.rs \ RUN mkdir src && echo "fn main() {}" > src/main.rs \
&& cargo build --release --target wasm32-unknown-unknown \ && cargo build --release --target wasm32-unknown-unknown \
&& rm -rf src && rm -rf src
COPY assets assets COPY assets assets
COPY lang lang
COPY src src COPY src src
RUN touch src/main.rs && cargo build --release --target wasm32-unknown-unknown \ RUN touch src/main.rs && cargo build --release --target wasm32-unknown-unknown \
&& mkdir /out \ && mkdir /out \
&& cp target/wasm32-unknown-unknown/release/dungeoncraft.wasm /out/ \ && cp target/wasm32-unknown-unknown/release/dungeoncraft.wasm /out/ \
&& cp "$CARGO_HOME"/registry/src/*/macroquad-*/js/mq_js_bundle.js /out/ && cp "$CARGO_HOME"/registry/src/*/macroquad-*/js/mq_js_bundle.js /out/
# --- Auslieferung: nginx # --- serve: nginx
FROM nginx:alpine FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY www/ /usr/share/nginx/html/ COPY www/ /usr/share/nginx/html/
+58
View File
@@ -0,0 +1,58 @@
# DungeonCraft
An original dungeon building & defence game that runs in the browser (Rust + [macroquad](https://github.com/not-fl3/macroquad), compiled to WebAssembly).
Dig out your underground realm, claim the portal to attract creatures, keep them fed, rested and paid, fend off endless hero waves that adapt to your army – and finally destroy the Hero Gate.
- 3D isometric view with a freely rotatable and tiltable camera
- Procedural pixel art, irregular cave walls, flickering torch light
- CP437 text-mode style interface with the DINOBYTE bitmap font
- English and German (language files in `lang/`, easy to extend)
See [DESIGN.md](DESIGN.md) for all design decisions and [CREDITS.md](CREDITS.md) for third-party assets.
## Running
The easiest way is Docker; the image builds the WASM file and serves it with nginx:
```sh
docker compose up -d --build
# open http://localhost:18093
```
### Building without Docker
Requirements: Rust with the `wasm32-unknown-unknown` target.
```sh
rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown
cp target/wasm32-unknown-unknown/release/dungeoncraft.wasm www/
cp ~/.cargo/registry/src/*/macroquad-*/js/mq_js_bundle.js www/
python3 -m http.server 8080 -d www # open http://localhost:8080
```
### Testing the game logic
The game logic (`src/game.rs`, `src/map.rs`) does not depend on macroquad and contains a headless simulation test.
## Controls
| Input | Action |
|---|---|
| Left click / drag | Use the active tool (dig and rooms: drag a rectangle) |
| Right click | Drop a unit from your hand |
| Right drag, WASD, arrows, screen edge | Move the camera |
| Middle drag, Q/E, R/F | Rotate / tilt the camera |
| Mouse wheel | Zoom |
| 1–9, 0, X, V | Select a tool or spell |
| P / Space | Pause |
| M, +/- | Sound on/off, volume |
| G | Tile outlines on/off |
| H | Help |
## Adding a language
1. Copy `lang/en.lang` to `lang/<code>.lang` and translate the texts (placeholders `{0}`, `{1}` stay as they are).
2. Add the file to `SOURCES` in `src/i18n.rs`.
3. Note: the DINOBYTE font only covers the CP437 character set.
+199
View File
@@ -0,0 +1,199 @@
# DungeonCraft Sprachdatei: Deutsch
# Format: key = Text Platzhalter: {0}, {1}, ... Zeilen mit # sind Kommentare.
lang.name = Deutsch
# ---- Einheiten
unit.worker = Grubenwicht
unit.kobold = Kobold
unit.spider = Höhlenspinne
unit.mage = Schattenmagier
unit.ogre = Steinoger
unit.knight = Ritter
unit.archer = Bogenschützin
unit.priest = Priester
unit.dwarf = Zwergenhauer
unit.paladin = Lichtpaladin
# ---- Räume
room.treasury = Schatzkammer
room.lair = Quartier
room.farm = Pilzgarten
room.training = Trainingshalle
room.library = Bibliothek
# ---- Tätigkeiten
job.none = Überlegt
job.dig = Gräbt
job.claim = Erobert Boden
job.deliver = Bringt Gold zur Schatzkammer
job.wander = Streift umher
job.sleep_go = Geht schlafen
job.sleep = Schläft
job.eat = Isst
job.train = Trainiert
job.study = Studiert
job.fight = Kämpft
job.core_attack = Greift den Seelenkern an
job.core_march = Marschiert zum Seelenkern
job.leave = Verlässt das Reich
job.gate_attack = Greift das Heldentor an
job.rally = Folgt der Angriffsflagge
# ---- Meldungen
msg.welcome1 = Willkommen, Herrscher des Unterreichs! Beschütze deinen Seelenkern und zerstöre das Heldentor.
msg.welcome2 = Markiere Erde und Goldadern mit dem Werkzeug »Graben« (Taste 1). Deine Grubenwichte erledigen den Rest.
msg.welcome3 = Grabe dich zum Portal durch und erobere es. Kreaturen brauchen ein Quartier und einen Pilzgarten.
msg.welcome4 = Mit der Angriffsflagge (Taste 9) schickst du deine Kreaturen in den Kampf, auch gegen das Heldentor.
msg.wave_warn = Späher melden: Welle {0} der Helden erreicht in 30 Sekunden das Tor!
msg.defeat = Dein Seelenkern ist zerbrochen. Das Unterreich ist verloren.
msg.victory = Das Heldentor ist zerstört. Das Unterreich gehört dir!
msg.creature_arrived = Ein {0} ist durch das Portal gekommen.
msg.payday_partial = Zahltag! {0} Kreaturen bezahlt ({1} Gold), {2} gingen leer aus und sind wütend!
msg.payday = Zahltag! {0} Kreaturen erhielten {1} Gold.
msg.wave_boss = Welle {0}: Ein Lichtpaladin führt {1} Helden in dein Reich!
msg.wave = Welle {0}: {1} Helden dringen durch das Tor!
msg.gate_boss = Das Tor wankt! Der Torwächter erscheint mit {0} Verteidigern!
msg.gate_defenders = Das Tor ruft {0} Verteidiger herbei!
msg.level_up = {0} erreicht Stufe {1}.
msg.hero_slain = {0} besiegt! Beute: {1} Gold.
msg.creature_died = Dein {0} ist gefallen.
msg.portal_claimed = Das Portal gehört nun dir! Kreaturen werden davon angelockt.
msg.flag_removed = Angriffsflagge entfernt. Deine Kreaturen kehren zurück.
msg.flag_set = Angriffsflagge gesetzt! Kampfbereite Kreaturen sammeln sich dort.
msg.creature_leaves = Ein unzufriedener {0} verlässt dein Reich!
msg.no_gold = Nicht genug Gold!
msg.sold = Räume verkauft: +{0} Gold.
msg.no_mana_bolt = Nicht genug Mana für den Blitz.
msg.no_mana_heal = Nicht genug Mana für die Heilung.
msg.no_mana_cost = Nicht genug Mana ({0} benötigt).
# ---- Hinweise (höchstens alle 30 Sekunden)
hint.portal_no_lair = Das Portal ist offen, aber ohne Quartier kommen keine Kreaturen.
hint.no_farm = Kreaturen wollen essen: Baue einen Pilzgarten.
hint.lair_full = Alle Quartierplätze sind belegt. Vergrößere dein Quartier.
hint.gate_attack = Deine Kreaturen greifen das Heldentor an!
hint.treasury_full = Deine Schatzkammern sind voll! Baue mehr Schatzkammer-Felder.
hint.flag_unreachable = Die Angriffsflagge ist für deine Kreaturen nicht erreichbar.
hint.flag_ground = Die Flagge muss auf begehbarem Boden stehen.
hint.no_beds = Kreaturen müssen auf dem nackten Boden schlafen. Vergrößere dein Quartier!
hint.hungry = Deine Kreaturen hungern! Vergrößere den Pilzgarten.
hint.core_attacked = Dein Seelenkern wird angegriffen!
hint.build_where = Räume können nur auf eroberten, leeren Bodenfeldern gebaut werden.
hint.hand_full = Deine Hand ist voll (max. 8). Rechtsklick lässt Einheiten fallen.
hint.drop_where = Einheiten können nur auf eigenem Gebiet abgesetzt werden.
hint.summon_where = Grubenwichte können nur auf eigenem Gebiet beschworen werden.
# ---- Werkzeuge
tool.dig.label = Graben
tool.dig.name = Graben
tool.dig.desc = Erde und Gold markieren. Ziehen für Rechteck; auf markiertem Feld beginnen hebt Markierung auf.
tool.hand.label = Hand
tool.hand.name = Hand
tool.hand.desc = Eigene Einheit aufnehmen (max. 8). Rechtsklick setzt sie auf eigenem Gebiet ab.
tool.treasury.label = Schatz
tool.treasury.desc = Lagert Gold: +500 Kapazität pro Feld.
tool.lair.label = Quartier
tool.lair.desc = Ein Schlafplatz pro Feld. Bestimmt, wie viele Kreaturen kommen.
tool.farm.label = Pilze
tool.farm.desc = Nahrung für Kreaturen. Ab 6 Feldern kommen Höhlenspinnen.
tool.training.label = Training
tool.training.desc = Kreaturen steigen Stufen auf (kostet Gold). Ab 9 Feldern kommen Steinoger.
tool.library.label = Bücher
tool.library.desc = Kreaturen erzeugen Mana. Ab 6 Feldern kommen Schattenmagier.
tool.sell.label = Verkauf
tool.sell.name = Verkaufen
tool.sell.desc = Räume zurück in Boden verwandeln (50 % Erstattung).
tool.flag.label = Flagge
tool.flag.name = Angriffsflagge
tool.flag.desc = Kampfbereite Kreaturen sammeln sich hier. Nahe am Heldentor greifen sie es an. Erneut klicken entfernt sie.
tool.lightning.label = Blitz
tool.lightning.name = Blitz
tool.lightning.desc = 55 Schaden an Helden im Umkreis.
tool.heal.label = Heilung
tool.heal.name = Heilung
tool.heal.desc = Heilt eigene Einheiten im Umkreis um 50 %.
tool.summon.label = Wicht
tool.summon.name = Grubenwicht beschwören
tool.summon.desc = Erschafft einen neuen Grubenwicht auf eigenem Gebiet.
cost.gold_tile = {0} Gold/Feld
cost.mana = {0} Mana
ui.cost = Kosten: {0}
ui.key = Taste: {0}
# ---- Anzeige
hud.gold = Gold
hud.mana = Mana
hud.core = Kern
hud.gate = Tor
hud.creatures = Kreaturen
hud.workers = Wichte
hud.wave = Welle {0}
hud.army = Armee
hud.off = aus
hud.hand = Hand
hud.empty = leer
ui.map = Karte
ui.pause = PAUSE
ui.pause_hint = P: weiter
# ---- Hilfe
help.goal = Ziel: Zerstöre das Heldentor und schütze deinen Seelenkern.
help.k_left = Linksklick Werkzeug benutzen (Graben/Räume: Rechteck ziehen)
help.k_right = Rechtsklick Einheit aus der Hand absetzen
help.k_pan = Rechts ziehen Karte verschieben (auch WASD/Pfeile, Bildrand)
help.k_rot = Mitte ziehen Kamera drehen und neigen (auch Q/E, R/F)
help.k_zoom = Mausrad Zoomen
help.k_tools = 1-9, 0, X, V Werkzeug wählen
help.k_misc = P / Leertaste Pause M Ton an/aus +/- Lautstärke
help.k_misc2 = G Kachel-Umrisse an/aus H Diese Hilfe
help.s1 = 1. Grabe zum Portal und lass es von Grubenwichten erobern.
help.s2 = 2. Baue Quartier, Pilzgarten und Schatzkammer.
help.s3 = 3. Trainiere deine Kreaturen, zahle ihren Lohn pünktlich.
help.s4 = 4. Wehre die Heldenwellen ab. Sie passen sich deiner Armee an.
help.s5 = 5. Setze die Angriffsflagge nahe ans Tor und greife an!
help.font = Schrift: DINOBYTE von mby (CC BY 4.0)
help.start = Klick zum Starten
# ---- Spielende
end.victory = SIEG!
end.defeat = NIEDERLAGE
end.victory_sub = Das Heldentor ist gefallen.
end.defeat_sub = Dein Seelenkern ist zerbrochen.
end.stats = Spielzeit {0} Wellen {1} Armee {2}
end.new = Neues Spiel (N)
# ---- Infofeld
info.level = {0} (Stufe {1})
info.hp = HP {0}/{1}
info.guard = Torwächter
info.intruder = Eindringling
info.needs = Energie {0}% Hunger {1}%
info.mood = Laune: {0}
mood.happy = zufrieden
mood.grumpy = mürrisch
mood.angry = wütend
info.wage = Lohn: {0} Gold
info.carries = Trägt {0} Gold
tile.rock = Fels
tile.indestructible = unzerstörbar
tile.earth = Erde
tile.gold = Goldader
tile.gold_amount = {0} Gold
tile.floor = Boden
owner.player = erobert
owner.hero = Heldengebiet
owner.none = neutral
tile.core = Seelenkern
tile.portal = Portal
portal.claimed = erobert
portal.neutral = neutral: Wichte können es erobern
tile.gate = Heldentor
farm.food = {0} Pilze
lair.taken = Schlafplatz belegt
lair.free = Schlafplatz frei
tile.marked = zum Graben markiert
# ---- Sprachauswahl
langsel.title = Sprache
langsel.hint = Klicke eine Sprache an. Du kannst sie später in den Einstellungen ändern.
+200
View File
@@ -0,0 +1,200 @@
# DungeonCraft language file: English (reference language)
# Format: key = text Placeholders: {0}, {1}, ... Lines starting with # are comments.
# To add a language: copy this file, translate the texts and register it in src/i18n.rs.
lang.name = English
# ---- units
unit.worker = Pit Imp
unit.kobold = Kobold
unit.spider = Cave Spider
unit.mage = Shadow Mage
unit.ogre = Stone Ogre
unit.knight = Knight
unit.archer = Archer
unit.priest = Priest
unit.dwarf = Dwarf Miner
unit.paladin = Paladin of Light
# ---- rooms
room.treasury = Treasury
room.lair = Lair
room.farm = Mushroom Garden
room.training = Training Hall
room.library = Library
# ---- jobs
job.none = Thinking
job.dig = Digging
job.claim = Claiming ground
job.deliver = Carrying gold to the treasury
job.wander = Wandering
job.sleep_go = Going to sleep
job.sleep = Sleeping
job.eat = Eating
job.train = Training
job.study = Studying
job.fight = Fighting
job.core_attack = Attacking the Soul Core
job.core_march = Marching to the Soul Core
job.leave = Leaving the realm
job.gate_attack = Attacking the Hero Gate
job.rally = Following the attack flag
# ---- messages
msg.welcome1 = Welcome, ruler of the underworld! Protect your Soul Core and destroy the Hero Gate.
msg.welcome2 = Mark earth and gold veins with the Dig tool (key 1). Your Pit Imps do the rest.
msg.welcome3 = Dig through to the portal and claim it. Creatures need a lair and a mushroom garden.
msg.welcome4 = Use the attack flag (key 9) to send your creatures into battle, even against the Hero Gate.
msg.wave_warn = Scouts report: hero wave {0} reaches the gate in 30 seconds!
msg.defeat = Your Soul Core has shattered. The underworld is lost.
msg.victory = The Hero Gate is destroyed. The underworld is yours!
msg.creature_arrived = A {0} came through the portal.
msg.payday_partial = Payday! {0} creatures paid ({1} gold), {2} got nothing and are angry!
msg.payday = Payday! {0} creatures received {1} gold.
msg.wave_boss = Wave {0}: a Paladin of Light leads {1} heroes into your realm!
msg.wave = Wave {0}: {1} heroes break through the gate!
msg.gate_boss = The gate is crumbling! The Gate Warden appears with {0} defenders!
msg.gate_defenders = The gate summons {0} defenders!
msg.level_up = {0} reaches level {1}.
msg.hero_slain = {0} defeated! Loot: {1} gold.
msg.creature_died = Your {0} has fallen.
msg.portal_claimed = The portal is yours! It attracts creatures.
msg.flag_removed = Attack flag removed. Your creatures return.
msg.flag_set = Attack flag set! Battle-ready creatures gather there.
msg.creature_leaves = An unhappy {0} leaves your realm!
msg.no_gold = Not enough gold!
msg.sold = Rooms sold: +{0} gold.
msg.no_mana_bolt = Not enough mana for Lightning.
msg.no_mana_heal = Not enough mana for Heal.
msg.no_mana_cost = Not enough mana ({0} needed).
# ---- hints (shown at most every 30 seconds)
hint.portal_no_lair = The portal is open, but without a lair no creatures will come.
hint.no_farm = Creatures want to eat: build a mushroom garden.
hint.lair_full = All beds in the lair are taken. Enlarge your lair.
hint.gate_attack = Your creatures are attacking the Hero Gate!
hint.treasury_full = Your treasuries are full! Build more treasury tiles.
hint.flag_unreachable = Your creatures cannot reach the attack flag.
hint.flag_ground = The flag must be placed on walkable ground.
hint.no_beds = Creatures have to sleep on bare ground. Enlarge your lair!
hint.hungry = Your creatures are starving! Enlarge the mushroom garden.
hint.core_attacked = Your Soul Core is under attack!
hint.build_where = Rooms can only be built on claimed, empty floor tiles.
hint.hand_full = Your hand is full (max. 8). Right-click drops units.
hint.drop_where = Units can only be dropped on your own territory.
hint.summon_where = Pit Imps can only be summoned on your own territory.
# ---- tools (label = short text under the button)
tool.dig.label = Dig
tool.dig.name = Dig
tool.dig.desc = Mark earth and gold. Drag for a rectangle; starting on a marked tile removes the marks.
tool.hand.label = Hand
tool.hand.name = Hand
tool.hand.desc = Pick up one of your units (max. 8). Right-click drops it on your own territory.
tool.treasury.label = Treasury
tool.treasury.desc = Stores gold: +500 capacity per tile.
tool.lair.label = Lair
tool.lair.desc = One bed per tile. Determines how many creatures come.
tool.farm.label = Mushrooms
tool.farm.desc = Food for creatures. With 6 tiles or more, cave spiders come.
tool.training.label = Training
tool.training.desc = Creatures gain levels (costs gold). With 9 tiles or more, stone ogres come.
tool.library.label = Library
tool.library.desc = Creatures generate mana. With 6 tiles or more, shadow mages come.
tool.sell.label = Sell
tool.sell.name = Sell
tool.sell.desc = Turn rooms back into floor (50% refund).
tool.flag.label = Flag
tool.flag.name = Attack Flag
tool.flag.desc = Battle-ready creatures gather here. Near the Hero Gate they attack it. Click again to remove it.
tool.lightning.label = Lightning
tool.lightning.name = Lightning
tool.lightning.desc = 55 damage to heroes in the area.
tool.heal.label = Heal
tool.heal.name = Heal
tool.heal.desc = Heals your units in the area by 50%.
tool.summon.label = Imp
tool.summon.name = Summon Pit Imp
tool.summon.desc = Creates a new Pit Imp on your own territory.
cost.gold_tile = {0} gold/tile
cost.mana = {0} mana
ui.cost = Cost: {0}
ui.key = Key: {0}
# ---- HUD
hud.gold = Gold
hud.mana = Mana
hud.core = Core
hud.gate = Gate
hud.creatures = Creatures
hud.workers = Imps
hud.wave = Wave {0}
hud.army = Army
hud.off = off
hud.hand = Hand
hud.empty = empty
ui.map = Map
ui.pause = PAUSE
ui.pause_hint = P: continue
# ---- help
help.goal = Goal: destroy the Hero Gate and protect your Soul Core.
help.k_left = Left click Use tool (dig/rooms: drag a rectangle)
help.k_right = Right click Drop a unit from your hand
help.k_pan = Right drag Move the map (also WASD/arrows, screen edge)
help.k_rot = Middle drag Rotate and tilt the camera (also Q/E, R/F)
help.k_zoom = Mouse wheel Zoom
help.k_tools = 1-9, 0, X, V Select tool
help.k_misc = P / Space Pause M Sound on/off +/- Volume
help.k_misc2 = G Tile outlines on/off H This help
help.s1 = 1. Dig to the portal and let your Pit Imps claim it.
help.s2 = 2. Build a lair, a mushroom garden and a treasury.
help.s3 = 3. Train your creatures and pay their wages on time.
help.s4 = 4. Fend off the hero waves. They adapt to your army.
help.s5 = 5. Place the attack flag near the gate and attack!
help.font = Font: DINOBYTE by mby (CC BY 4.0)
help.start = Click to start
# ---- end screen
end.victory = VICTORY!
end.defeat = DEFEAT
end.victory_sub = The Hero Gate has fallen.
end.defeat_sub = Your Soul Core has shattered.
end.stats = Time {0} Waves {1} Army {2}
end.new = New Game (N)
# ---- info panel
info.level = {0} (level {1})
info.hp = HP {0}/{1}
info.guard = Gate Warden
info.intruder = Intruder
info.needs = Energy {0}% Hunger {1}%
info.mood = Mood: {0}
mood.happy = content
mood.grumpy = grumpy
mood.angry = angry
info.wage = Wage: {0} gold
info.carries = Carries {0} gold
tile.rock = Rock
tile.indestructible = indestructible
tile.earth = Earth
tile.gold = Gold Vein
tile.gold_amount = {0} gold
tile.floor = Floor
owner.player = claimed
owner.hero = hero territory
owner.none = neutral
tile.core = Soul Core
tile.portal = Portal
portal.claimed = claimed
portal.neutral = neutral: imps can claim it
tile.gate = Hero Gate
farm.food = {0} mushrooms
lair.taken = Bed taken
lair.free = Bed free
tile.marked = marked for digging
# ---- language selection
langsel.title = Language
langsel.hint = Click a language. You can change it later in the settings.
+1 -1
View File
@@ -2,7 +2,7 @@ server {
listen 80; listen 80;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
# Testversion: immer frisch laden # test build: always reload fresh files
add_header Cache-Control "no-cache"; add_header Cache-Control "no-cache";
location / { location / {
try_files $uri $uri/ =404; try_files $uri $uri/ =404;
+123 -115
View File
@@ -1,4 +1,4 @@
// Spiellogik: Einheiten, KI, Wirtschaft, Wellen. // Game logic: units, AI, economy, hero waves.
use crate::map::*; use crate::map::*;
@@ -9,7 +9,7 @@ pub const FIRST_WAVE: f32 = 300.0;
pub const WAVE_GAP: f32 = 180.0; pub const WAVE_GAP: f32 = 180.0;
pub const MAX_SQUAD: usize = 18; 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)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Sfx { pub enum Sfx {
Dig, 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)] #[derive(Clone, Copy)]
pub struct SfxEvent { pub struct SfxEvent {
pub sfx: Sfx, 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 MANA_MAX: f32 = 400.0;
pub const CARRY_MAX: i32 = 300; pub const CARRY_MAX: i32 = 300;
@@ -92,16 +99,16 @@ pub struct St {
pub fn st(k: EK) -> 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 }; let s = |name, hp, atk, range, speed, interval, wage, bounty| St { name, hp, atk, range, speed, interval, wage, bounty };
match k { match k {
EK::Worker => s("Grubenwicht", 45.0, 3.0, 1.0, 3.2, 1.0, 0, 0), EK::Worker => s("unit.worker", 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::Kobold => s("unit.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::Spider => s("unit.spider", 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::Mage => s("unit.mage", 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::Ogre => s("unit.ogre", 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::Knight => s("unit.knight", 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::Archer => s("unit.archer", 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::Priest => s("unit.priest", 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::Dwarf => s("unit.dwarf", 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::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 face: f32,
pub moving: bool, pub moving: bool,
pub working: bool, pub working: bool,
pub guard: bool, // Torverteidiger pub guard: bool, // gate defender
} }
impl Ent { impl Ent {
@@ -200,7 +207,7 @@ impl Ent {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct Fx { 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 x: f32,
pub y: f32, pub y: f32,
pub x2: f32, pub x2: f32,
@@ -225,14 +232,14 @@ pub struct Game {
pub portal_timer: f32, pub portal_timer: f32,
pub payday: f32, pub payday: f32,
pub fx: Vec<Fx>, pub fx: Vec<Fx>,
pub msgs: Vec<String>, pub msgs: Vec<Msg>,
pub state: u8, // 0 läuft, 1 Sieg, 2 Niederlage pub state: u8, // 0 running, 1 victory, 2 defeat
pub paused: bool, pub paused: bool,
pub hand: Vec<u32>, 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_hp: f32,
pub gate_hit_t: 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 flag: Option<usize>,
pub events: Vec<SfxEvent>, pub events: Vec<SfxEvent>,
pub last_wave_value: f32, pub last_wave_value: f32,
@@ -240,7 +247,7 @@ pub struct Game {
hint_t: [f32; 16], 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)] #[derive(Clone, Copy, PartialEq)]
pub struct Torch { pub struct Torch {
pub wall: usize, pub wall: usize,
@@ -248,7 +255,7 @@ pub struct Torch {
} }
impl 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) { pub fn pos(&self) -> (f32, f32) {
let (x, y) = xy(self.wall); let (x, y) = xy(self.wall);
let (dx, dy) = DIR4[self.dir]; 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_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_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> { pub fn room_index(k: Kind) -> Option<usize> {
ROOM_KINDS.iter().position(|&r| r == k) ROOM_KINDS.iter().position(|&r| r == k)
@@ -320,15 +327,15 @@ impl Game {
g.place_torches(i); g.place_torches(i);
} }
} }
g.msg("Willkommen, Herrscher des Unterreichs! Beschütze deinen Seelenkern und zerstöre das Heldentor.".into()); g.msg("msg.welcome1", vec![]);
g.msg("Markiere Erde und Goldadern mit dem Werkzeug »Graben« (Taste 1). Deine Grubenwichte erledigen den Rest.".into()); g.msg("msg.welcome2", vec![]);
g.msg("Grabe dich zum Portal durch und erobere es. Kreaturen brauchen ein Quartier und einen Pilzgarten.".into()); g.msg("msg.welcome3", vec![]);
g.msg("Mit der Angriffsflagge (Taste 9) schickst du deine Kreaturen in den Kampf, auch gegen das Heldentor.".into()); g.msg("msg.welcome4", vec![]);
g g
} }
pub fn msg(&mut self, s: String) { pub fn msg(&mut self, key: &'static str, args: Vec<String>) {
self.msgs.push(s); self.msgs.push(Msg { key, args });
if self.msgs.len() > 50 { if self.msgs.len() > 50 {
self.msgs.remove(0); 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) (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 { pub fn army_value(&self) -> f32 {
let (mut h, mut d) = (0.0, 0.0); let (mut h, mut d) = (0.0, 0.0);
for e in &self.ents { for e in &self.ents {
@@ -362,7 +369,7 @@ impl Game {
(h * d).sqrt() (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) { pub fn place_torches(&mut self, i: usize) {
let (x, y) = xy(i); let (x, y) = xy(i);
for (d, &(dx, dy)) in DIR4.iter().enumerate() { 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() { if !inb(wx, wy) || !self.map.t[idx(wx, wy)].solid() {
continue; 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 t = Torch { wall: idx(wx, wy), dir: (d + 2) % 4 };
let (px, pz) = t.pos(); let (px, pz) = t.pos();
let near = self.torches.iter().any(|o| { let near = self.torches.iter().any(|o| {
@@ -383,10 +390,10 @@ impl Game {
} }
} }
fn hint(&mut self, key: usize, s: &str) { fn hint(&mut self, slot: usize, key: &'static str) {
if self.time - self.hint_t[key] > 30.0 { if self.time - self.hint_t[slot] > 30.0 {
self.hint_t[key] = self.time; self.hint_t[slot] = self.time;
self.msg(s.into()); self.msg(key, vec![]);
} }
} }
@@ -460,7 +467,7 @@ impl Game {
self.ents.iter().position(|e| e.id == id && e.alive && !e.held) self.ents.iter().position(|e| e.id == id && e.alive && !e.held)
} }
// ---------------------------------------------------------------- Update // ---------------------------------------------------------------- update
pub fn update(&mut self, dt: f32) { pub fn update(&mut self, dt: f32) {
let dt = dt.min(0.1); let dt = dt.min(0.1);
@@ -501,7 +508,7 @@ impl Game {
self.wave_timer -= dt; self.wave_timer -= dt;
if !self.warned && self.wave_timer < 30.0 { if !self.warned && self.wave_timer < 30.0 {
self.warned = true; 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); self.sfx_ui(Sfx::WaveAlarm);
} }
if self.wave_timer <= 0.0 { if self.wave_timer <= 0.0 {
@@ -527,12 +534,12 @@ impl Game {
if self.core_hp <= 0.0 { if self.core_hp <= 0.0 {
self.core_hp = 0.0; self.core_hp = 0.0;
self.state = 2; self.state = 2;
self.msg("Dein Seelenkern ist zerbrochen. Das Unterreich ist verloren.".into()); self.msg("msg.defeat", vec![]);
self.sfx_ui(Sfx::Defeat); self.sfx_ui(Sfx::Defeat);
} else if self.gate_hp <= 0.0 { } else if self.gate_hp <= 0.0 {
self.gate_hp = 0.0; self.gate_hp = 0.0;
self.state = 1; 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); self.sfx_ui(Sfx::Victory);
} }
} }
@@ -543,15 +550,15 @@ impl Game {
let farm = self.counts[2]; let farm = self.counts[2];
let cap = lair.min(30); let cap = lair.min(30);
if lair == 0 { if lair == 0 {
self.hint(0, "Das Portal ist offen, aber ohne Quartier kommen keine Kreaturen."); self.hint(0, "hint.portal_no_lair");
return; return;
} }
if farm == 0 { if farm == 0 {
self.hint(1, "Kreaturen wollen essen: Baue einen Pilzgarten."); self.hint(1, "hint.no_farm");
return; return;
} }
if creatures >= cap { if creatures >= cap {
self.hint(2, "Alle Quartierplätze sind belegt. Vergrößere dein Quartier."); self.hint(2, "hint.lair_full");
return; return;
} }
let mut opts: Vec<(EK, u32)> = vec![(EK::Kobold, 4)]; 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.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.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.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) { fn do_payday(&mut self) {
@@ -605,14 +612,14 @@ impl Game {
if paid + unpaid > 0 { if paid + unpaid > 0 {
self.sfx_ui(Sfx::Payday); self.sfx_ui(Sfx::Payday);
if unpaid > 0 { 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 { } 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 { fn squad_value(list: &[(EK, u32)]) -> f32 {
let (mut h, mut d) = (0.0, 0.0); let (mut h, mut d) = (0.0, 0.0);
for &(k, l) in list { for &(k, l) in list {
@@ -623,7 +630,7 @@ impl Game {
(h * d).sqrt() (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)> { 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 total: u32 = pool.iter().map(|p| p.1).sum();
let mut list = Vec::new(); 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); 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) { fn spawn_wave(&mut self) {
use EK::*; use EK::*;
let w = self.wave + 1; let w = self.wave + 1;
@@ -683,13 +690,13 @@ impl Game {
self.spawn_squad(&list, false); self.spawn_squad(&list, false);
self.sfx_ui(Sfx::WaveStart); self.sfx_ui(Sfx::WaveStart);
if boss.is_some() { 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 { } 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) { fn check_gate_stage(&mut self) {
use EK::*; use EK::*;
let frac = self.gate_hp / GATE_HP; let frac = self.gate_hp / GATE_HP;
@@ -707,9 +714,9 @@ impl Game {
self.spawn_squad(&list, true); self.spawn_squad(&list, true);
self.sfx_ui(Sfx::GateDefenders); self.sfx_ui(Sfx::GateDefenders);
if st_i == 3 { 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 { } 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.fx(2, fx, fy, 0.0, 0.0, 0.25, 0xffffffff);
} }
self.sfx(Sfx::GateHit, gx, gy); 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(); self.check_gate_stage();
} }
@@ -755,7 +762,7 @@ impl Game {
self.sfx(Sfx::LevelUp, e.x, e.y); self.sfx(Sfx::LevelUp, e.x, e.y);
} }
if !e.hero && e.level >= 3 { 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 { 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 { fn step_path(&mut self, e: &mut Ent, dt: f32) -> bool {
if e.path.is_empty() { if e.path.is_empty() {
return false; return false;
@@ -776,7 +783,7 @@ impl Game {
let tile = self.map.t[next]; let tile = self.map.t[next];
if !tile.walkable() { if !tile.walkable() {
if e.hero && tile.diggable() { if e.hero && tile.diggable() {
// Helden graben sich durch // heroes dig their way through
e.working = true; e.working = true;
let (tx, ty) = xy(next); let (tx, ty) = xy(next);
e.face = sgn(tx as f32 + 0.5 - e.x); e.face = sgn(tx as f32 + 0.5 - e.x);
@@ -893,7 +900,7 @@ impl Game {
best 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 { fn attack_job(&mut self, e: &mut Ent, id: u32, dt: f32) -> bool {
let Some(j) = self.find(id) else { return false }; let Some(j) = self.find(id) else { return false };
let (tx, ty) = (self.ents[j].x, self.ents[j].y); 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) { 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 dx = tx - e.x;
let dy = ty - e.y; let dy = ty - e.y;
let l = (dx * dx + dy * dy).sqrt().max(0.001); let l = (dx * dx + dy * dy).sqrt().max(0.001);
@@ -974,14 +981,14 @@ impl Game {
let got = self.add_gold(b); let got = self.add_gold(b);
self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff); self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff);
self.sfx(Sfx::HeroDeath, x, y); 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 { } else {
self.sfx(Sfx::Death, x, y); 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> { fn dig_neighbor(&self, i: usize, me: u32) -> Option<usize> {
let (x, y) = xy(i); let (x, y) = xy(i);
@@ -1112,7 +1119,7 @@ impl Game {
self.map.t[idx(px + x, py + y)].owner = Owner::Player; 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); self.fx(7, px as f32 + 1.5, py as f32 + 1.5, 0.0, 0.0, 1.0, 0xffff66cc);
} else { } else {
self.map.t[t].owner = Owner::Player; self.map.t[t].owner = Owner::Player;
@@ -1128,7 +1135,7 @@ impl Game {
let amount = e.carry as f32; let amount = e.carry as f32;
let got = self.add_gold(amount); let got = self.add_gold(amount);
if got < 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 { if got > 0.0 {
self.fx(5, e.x, e.y, 0.0, 0.0, 0.8, 0xff30d0ff); 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>> { fn find_room_tile(&self, cur: usize, kind: Kind, me: u32, need_food: bool) -> Option<Vec<usize>> {
self.map.search( 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 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) { fn rally(&mut self, e: &mut Ent, dt: f32) {
let Some(f) = self.flag else { let Some(f) = self.flag else {
e.job = Job::None; e.job = Job::None;
@@ -1335,7 +1342,7 @@ impl Game {
return; return;
}; };
if e.hp < e.max_hp * 0.3 || e.energy < 0.1 || e.hunger > 2.2 { 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.job = Job::None;
e.path.clear(); e.path.clear();
return; return;
@@ -1364,14 +1371,14 @@ impl Game {
}; };
let d = ((e.x - tx).powi(2) + (e.y - ty).powi(2)).sqrt(); let d = ((e.x - tx).powi(2) + (e.y - ty).powi(2)).sqrt();
if d <= near && e.path.is_empty() { 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 { if e.path.is_empty() || e.repath <= 0.0 {
e.repath = 1.5; e.repath = 1.5;
match self.map.path_to(e.tile(), target) { match self.map.path_to(e.tile(), target) {
Some(p) => e.path = p, Some(p) => e.path = p,
None => { None => {
self.hint(11, "Die Angriffsflagge ist für deine Kreaturen nicht erreichbar."); self.hint(11, "hint.flag_unreachable");
e.job = Job::Wander; e.job = Job::Wander;
e.timer = 3.0; e.timer = 3.0;
return; return;
@@ -1388,7 +1395,7 @@ impl Game {
let i = idx(x, y); let i = idx(x, y);
if self.flag == Some(i) { if self.flag == Some(i) {
self.flag = None; self.flag = None;
self.msg("Angriffsflagge entfernt. Deine Kreaturen kehren zurück.".into()); self.msg("msg.flag_removed", vec![]);
self.sfx_ui(Sfx::Click); self.sfx_ui(Sfx::Click);
for e in self.ents.iter_mut() { for e in self.ents.iter_mut() {
if e.alive && e.creature() && e.job == Job::Rally { if e.alive && e.creature() && e.job == Job::Rally {
@@ -1399,13 +1406,13 @@ impl Game {
return; return;
} }
if !self.map.t[i].walkable() { 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); self.sfx_ui(Sfx::Error);
return; return;
} }
self.flag = Some(i); self.flag = Some(i);
self.sfx(Sfx::FlagSet, x as f32 + 0.5, y as f32 + 0.5); 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(); let mut ids = Vec::new();
for e in self.ents.iter() { 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) { 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 cur = e.tile();
let name = st(e.kind).name; let name = st(e.kind).name;
if e.happy <= 0.0 { 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); self.start_leave(e);
return; return;
} }
@@ -1461,7 +1468,7 @@ impl Game {
} }
} }
e.happy -= 0.1; 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); e.job = Job::Sleep(cur);
return; return;
} }
@@ -1475,7 +1482,7 @@ impl Game {
return; return;
} }
if e.hunger > 1.8 { 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) { if self.wants_rally(e) {
@@ -1485,7 +1492,7 @@ impl Game {
return; return;
} }
if e.hp < e.max_hp * 0.5 && e.energy >= 0.25 && self.counts[1] > 0 { 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); e.energy = e.energy.min(0.2);
self.creature_choose(e); self.creature_choose(e);
return; return;
@@ -1514,7 +1521,7 @@ impl Game {
self.wander(e, 4); self.wander(e, 4);
} }
// ---------------------------------------------------------------- Helden // ---------------------------------------------------------------- heroes
fn hero_ai(&mut self, e: &mut Ent, dt: f32) { fn hero_ai(&mut self, e: &mut Ent, dt: f32) {
if e.think <= 0.0 { if e.think <= 0.0 {
@@ -1564,7 +1571,7 @@ impl Game {
e.think = 1.5; e.think = 1.5;
} }
if e.guard { if e.guard {
// Torwächter bleiben am Tor // gate defenders stay at the gate
let (gx, gy) = self.gate_center(); let (gx, gy) = self.gate_center();
let d2 = (e.x - gx).powi(2) + (e.y - gy).powi(2); 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 { 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; let fy = (e.y + cy as f32 + 1.5) * 0.5;
self.fx(2, fx, fy, 0.0, 0.0, 0.3, 0xff4040ff); self.fx(2, fx, fy, 0.0, 0.0, 0.3, 0xff4040ff);
self.sfx(Sfx::CoreHit, fx, fy); self.sfx(Sfx::CoreHit, fx, fy);
self.hint(6, "Dein Seelenkern wird angegriffen!"); self.hint(6, "hint.core_attacked");
} }
return; return;
} }
@@ -1602,7 +1609,7 @@ impl Game {
self.step_path(e, dt); self.step_path(e, dt);
} }
// ---------------------------------------------------------------- Spieleraktionen // ---------------------------------------------------------------- player actions
pub fn mark_rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, on: bool) { pub fn mark_rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, on: bool) {
for y in y0.min(y1)..=y0.max(y1) { for y in y0.min(y1)..=y0.max(y1) {
@@ -1648,11 +1655,11 @@ impl Game {
} }
} }
if broke { if broke {
self.msg("Nicht genug Gold!".into()); self.msg("msg.no_gold", vec![]);
self.sfx_ui(Sfx::Error); self.sfx_ui(Sfx::Error);
} }
if built == 0 && !broke { 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(); self.recount();
} }
@@ -1679,13 +1686,13 @@ impl Game {
if refund > 0.0 { if refund > 0.0 {
let got = self.add_gold(refund); let got = self.add_gold(refund);
self.sfx_ui(Sfx::Sell); 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 { pub fn pickup(&mut self, target: Option<usize>) -> bool {
if self.hand.len() >= 8 { 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; 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) { 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)]; let t = self.map.t[idx(x, y)];
if !t.walkable() || t.owner != Owner::Player { 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; return false;
} }
self.hand.pop(); self.hand.pop();
@@ -1731,7 +1738,7 @@ impl Game {
pub fn lightning(&mut self, wx: f32, wy: f32) { pub fn lightning(&mut self, wx: f32, wy: f32) {
const COST: f32 = 50.0; const COST: f32 = 50.0;
if self.mana < COST { 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); self.sfx_ui(Sfx::Error);
return; return;
} }
@@ -1756,7 +1763,7 @@ impl Game {
pub fn heal(&mut self, wx: f32, wy: f32) { pub fn heal(&mut self, wx: f32, wy: f32) {
const COST: f32 = 60.0; const COST: f32 = 60.0;
if self.mana < COST { 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); self.sfx_ui(Sfx::Error);
return; return;
} }
@@ -1779,11 +1786,11 @@ impl Game {
let cost = self.summon_cost(); let cost = self.summon_cost();
let (x, y) = (wx as i32, wy as i32); let (x, y) = (wx as i32, wy as i32);
if !self.map.walk(x, y) || self.map.t[idx(x, y)].owner != Owner::Player { 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; return;
} }
if self.mana < cost { 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); self.sfx_ui(Sfx::Error);
return; return;
} }
@@ -1794,37 +1801,38 @@ impl Game {
} }
} }
// Language key describing what a unit is doing
pub fn job_name(e: &Ent) -> &'static str { pub fn job_name(e: &Ent) -> &'static str {
match e.job { match e.job {
Job::None => "Überlegt", Job::None => "job.none",
Job::Dig(_) => "Gräbt", Job::Dig(_) => "job.dig",
Job::Claim(_) => "Erobert Boden", Job::Claim(_) => "job.claim",
Job::Deliver => "Bringt Gold zur Schatzkammer", Job::Deliver => "job.deliver",
Job::Wander => "Streift umher", Job::Wander => "job.wander",
Job::Sleep(_) => { Job::Sleep(_) => {
if e.moving { if e.moving {
"Geht schlafen" "job.sleep_go"
} else { } else {
"Schläft" "job.sleep"
} }
} }
Job::Eat(_) => "Isst", Job::Eat(_) => "job.eat",
Job::Train(_) => "Trainiert", Job::Train(_) => "job.train",
Job::Study(_) => "Studiert", Job::Study(_) => "job.study",
Job::Attack(_) => "Kämpft", Job::Attack(_) => "job.fight",
Job::AttackCore => { Job::AttackCore => {
if e.working { if e.working {
"Greift den Seelenkern an" "job.core_attack"
} else { } else {
"Marschiert zum Seelenkern" "job.core_march"
} }
} }
Job::Leave => "Verlässt das Reich", Job::Leave => "job.leave",
Job::Rally => { Job::Rally => {
if e.working { if e.working {
"Greift das Heldentor an" "job.gate_attack"
} else { } else {
"Folgt der Angriffsflagge" "job.rally"
} }
} }
} }
@@ -1834,14 +1842,14 @@ pub fn job_name(e: &Ent) -> &'static str {
mod tests { mod tests {
use super::*; 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] #[test]
fn simulate_long_game() { fn simulate_long_game() {
for seed in [1u32, 7, 12345] { for seed in [1u32, 7, 12345] {
let mut g = Game::new(seed); let mut g = Game::new(seed);
let (cx, cy) = g.map.core; let (cx, cy) = g.map.core;
let (px, py) = g.map.portal; 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 start = idx(cx - 1, cy + 1);
let goal = idx(px + 1, py + 1); let goal = idx(px + 1, py + 1);
let path = g let path = g
@@ -1855,8 +1863,8 @@ mod tests {
} }
g.mark_rect(cx - 6, cy - 6, cx + 9, cy + 9, true); g.mark_rect(cx - 6, cy - 6, cx + 9, cy + 9, true);
let (ox, oy) = (cx - 3, cy - 3); let (ox, oy) = (cx - 3, cy - 3);
g.build_rect(1, ox, oy, ox + 2, oy + 1); // Quartier 3x2 g.build_rect(1, ox, oy, ox + 2, oy + 1); // lair 3x2
g.build_rect(2, ox + 5, oy, ox + 7, oy + 1); // Pilzgarten 3x2 g.build_rect(2, ox + 5, oy, ox + 7, oy + 1); // farm 3x2
let dt = 0.05; let dt = 0.05;
let mut built = false; let mut built = false;
let mut portal_logged = false; let mut portal_logged = false;
@@ -1865,12 +1873,12 @@ mod tests {
g.events.clear(); g.events.clear();
g.msgs.clear(); g.msgs.clear();
if step % 200 == 0 && !built && g.gold > 1900.0 { if step % 200 == 0 && !built && g.gold > 1900.0 {
g.build_rect(0, ox, oy + 6, ox + 1, oy + 8); // Schatzkammer 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 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); // Quartier erweitern g.build_rect(1, ox, oy + 2, ox + 2, oy + 2); // enlarge the lair
built = true; 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 { 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)); 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 { if let Some((x, y)) = target {
@@ -1898,10 +1906,10 @@ mod tests {
} }
if g.portal_owned() && step % 20 == 0 && !portal_logged { if g.portal_owned() && step % 20 == 0 && !portal_logged {
portal_logged = true; 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 { 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; 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::game::EK;
use crate::map::Kind; use crate::map::Kind;
@@ -8,7 +8,7 @@ pub const CELL: f32 = 16.0;
const ATLAS_COLS: u32 = 16; const ATLAS_COLS: u32 = 16;
const ATLAS_ROWS: u32 = 5; const ATLAS_ROWS: u32 = 5;
// Atlas-Indizes (jeweils 4 Varianten) // atlas indices (4 variants each)
pub const T_ROCK: u32 = 0; pub const T_ROCK: u32 = 0;
pub const T_EARTH: u32 = 4; pub const T_EARTH: u32 = 4;
pub const T_GOLD: u32 = 8; 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_COREFLOOR: u32 = 44;
pub const T_PORTALFLOOR: u32 = 48; pub const T_PORTALFLOOR: u32 = 48;
pub const T_GATEFLOOR: u32 = 52; pub const T_GATEFLOOR: u32 = 52;
// Einzelzellen // single cells
pub const T_FACE_EARTH: u32 = 56; pub const T_FACE_EARTH: u32 = 56;
pub const T_FACE_ROCK: u32 = 57; pub const T_FACE_ROCK: u32 = 57;
pub const T_FACE_GOLD: u32 = 58; pub const T_FACE_GOLD: u32 = 58;
@@ -44,19 +44,19 @@ pub const I_SUMMON: u32 = 71;
pub struct Gfx { pub struct Gfx {
pub atlas: Texture2D, pub atlas: Texture2D,
pub sprites: 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 GW: f32 = 6.0; // glyph width
pub const GH: f32 = 8.0; // Glyphhöhe pub const GH: f32 = 8.0; // glyph height
const CP437_LOW: &str = " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼"; const CP437_LOW: &str = " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼";
const CP437_HIGH: &str = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}"; const CP437_HIGH: &str = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}";
pub fn cp437(ch: char) -> u8 { pub fn cp437(ch: char) -> u8 {
// In DINOBYTE sind ö (0x94) und ò (0x95) vertauscht // DINOBYTE has ö (0x94) and ò (0x95) swapped
match ch { match ch {
'ö' => return 0x95, 'ö' => return 0x95,
'ò' => return 0x94, 'ò' => 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_SINGLE: [u8; 6] = [0xDA, 0xBF, 0xC0, 0xD9, 0xC4, 0xB3];
pub const BOX_DOUBLE: [u8; 6] = [0xC9, 0xBB, 0xC8, 0xBC, 0xCD, 0xBA]; 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 { fn stones(x: i32, y: i32, v: u32, base: (i32, i32, i32), grout: (i32, i32, i32), var: i32) -> Color {
let row = y / 8; let row = y / 8;
let off = if row % 2 == 1 { 4 } else { 0 }; 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 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; 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 { let edge = if y % 8 == 1 || sx % 8 == 1 {
10 10
} else if y % 8 == 7 || sx % 8 == 7 { } 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 { 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)) }; 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 { for v in 0..4u32 {
// Fels // rock
cv.cell(T_ROCK + v, |x, y| { cv.cell(T_ROCK + v, |x, y| {
let blk = rnd(x / 4 + (y / 4) % 2, y / 4, v + 21) * 20.0 - 10.0; 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; 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)) Some(shade((80, 80, 92), (blk + fine) as i32))
} }
}); });
// Erde // earth
cv.cell(T_EARTH + v, |x, y| Some(earth_px(x, y, v))); cv.cell(T_EARTH + v, |x, y| Some(earth_px(x, y, v)));
// Gold // gold
cv.cell(T_GOLD + v, |x, y| { cv.cell(T_GOLD + v, |x, y| {
let mut best = 99.0f32; let mut best = 99.0f32;
for k in 0..6 { 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_FLOOR + v, |x, y| Some(stones(x, y, v, (64, 56, 50), (34, 30, 28), 10)));
cv.cell(T_CLAIM + v, |x, y| { cv.cell(T_CLAIM + v, |x, y| {
let col = stones(x, y, v + 10, (78, 44, 58), (36, 20, 28), 10); 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 { if (x % 8 == 4 && y % 8 == 4) && hash(x / 8, y / 8, v) % 2 == 0 {
Some(c(150, 60, 80)) Some(c(150, 60, 80))
} else { } else {
@@ -268,7 +268,7 @@ fn gen_atlas() -> Image {
} }
}); });
} }
// Wandfronten (Pseudo-3D) // wall faces
cv.cell(T_FACE_EARTH, |x, y| { cv.cell(T_FACE_EARTH, |x, y| {
let streak = (hash(x, 0, 70) % 3) as i32 * 5; 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)) 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)) 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| { cv.cell(T_MARK, |x, y| {
if (x + y) % 6 < 2 { if (x + y) % 6 < 2 {
Some(Color::from_rgba(255, 220, 60, 150)) Some(Color::from_rgba(255, 220, 60, 150))
@@ -294,7 +294,7 @@ fn gen_atlas() -> Image {
None None
} }
}); });
// Bett // bed
cv.cell(D_BED, |x, y| { cv.cell(D_BED, |x, y| {
let dx = (x as f32 - 7.5) / 6.5; let dx = (x as f32 - 7.5) / 6.5;
let dy = (y as f32 - 8.0) / 5.5; let dy = (y as f32 - 8.0) / 5.5;
@@ -307,7 +307,7 @@ fn gen_atlas() -> Image {
None None
} }
}); });
// Pilze // mushrooms
cv.cell(D_MUSHROOM, |x, y| { cv.cell(D_MUSHROOM, |x, y| {
let caps = [(4, 6, 3), (11, 5, 2), (8, 11, 3)]; let caps = [(4, 6, 3), (11, 5, 2), (8, 11, 3)];
for &(cx, cy, r) in &caps { for &(cx, cy, r) in &caps {
@@ -322,7 +322,7 @@ fn gen_atlas() -> Image {
} }
None None
}); });
// Trainingspuppe // training dummy
cv.cell(D_DUMMY, |x, y| { cv.cell(D_DUMMY, |x, y| {
if (x == 7 || x == 8) && y >= 4 { if (x == 7 || x == 8) && y >= 4 {
Some(c(90, 60, 36)) Some(c(90, 60, 36))
@@ -336,7 +336,7 @@ fn gen_atlas() -> Image {
None None
} }
}); });
// Goldhaufen // gold pile
cv.cell(D_COINS, |x, y| { cv.cell(D_COINS, |x, y| {
let dx = (x as f32 - 7.5) / 7.0; let dx = (x as f32 - 7.5) / 7.0;
let dy = (y as f32 - 11.0) / 5.0; let dy = (y as f32 - 11.0) / 5.0;
@@ -348,7 +348,7 @@ fn gen_atlas() -> Image {
None None
} }
}); });
// Bücherregal // bookshelf
cv.cell(D_SHELF, |x, y| { cv.cell(D_SHELF, |x, y| {
if y > 9 || x == 0 || x == 15 { if y > 9 || x == 0 || x == 15 {
return if y <= 10 { Some(c(60, 36, 22)) } else { None }; return if y <= 10 { Some(c(60, 36, 22)) } else { None };
@@ -365,7 +365,7 @@ fn gen_atlas() -> Image {
_ => c(100, 60, 120), _ => c(100, 60, 120),
}) })
}); });
// Werkzeug-Icons // tool icons
cv.cell(I_PICK, |x, y| { cv.cell(I_PICK, |x, y| {
let handle = (x + y == 16 || x + y == 17) && x >= 3; 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; 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 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 { fn gen_sprites() -> Image {
let n = SPRITE_KINDS.len() as u32; 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)); 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); atlas.set_filter(FilterMode::Nearest);
let sprites = Texture2D::from_image(&gen_sprites()); let sprites = Texture2D::from_image(&gen_sprites());
sprites.set_filter(FilterMode::Nearest); 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"); 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() { for px in img.get_image_data_mut().iter_mut() {
let on = px[3] > 128 && px[0] > 128; 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) { 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); let src = Rect::new((code % 16) as f32 * GW, (code / 16) as f32 * GH, GW, GH);
draw_texture_ex( 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) { pub fn text(&self, s: &str, x: f32, y: f32, scale: f32, color: Color) {
let mut cx = x; let mut cx = x;
for ch in s.chars() { for ch in s.chars() {
@@ -708,13 +708,13 @@ impl Gfx {
s.chars().count() as f32 * GW * scale 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) { 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 + scale, y + scale, scale, Color::new(0.0, 0.0, 0.0, color.a * 0.85));
self.text(s, x, y, scale, color); 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) { 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 cw = GW * scale;
let ch = GH * 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) { 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); self.window(x, y, cols, rows, scale, double, frame, bg);
if !title.is_empty() { 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 game;
mod gfx; mod gfx;
mod i18n;
mod map; mod map;
mod platform;
mod render3d; mod render3d;
mod sound; mod sound;
mod ui; mod ui;
@@ -59,6 +61,11 @@ async fn main() {
let mut seed = (miniquad::date::now() * 1000.0) as u32 | 1; let mut seed = (miniquad::date::now() * 1000.0) as u32 | 1;
let (mut game, mut cam) = new_game(seed); let (mut game, mut cam) = new_game(seed);
let mut ui = Ui::new(); 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 }; let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false };
loop { loop {
@@ -234,6 +241,12 @@ async fn main() {
(game, cam) = new_game(seed); (game, cam) = new_game(seed);
ui.show_help = false; 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::Consumed => {}
UiAction::None => { UiAction::None => {
if game.state == 0 { 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::cmp::Reverse;
use std::collections::BinaryHeap; use std::collections::BinaryHeap;
@@ -34,13 +34,13 @@ pub enum Owner {
pub struct Tile { pub struct Tile {
pub kind: Kind, pub kind: Kind,
pub owner: Owner, pub owner: Owner,
pub hp: f32, // Grab-Widerstand pub hp: f32, // dig resistance
pub gold: i32, // Goldvorrat (Goldader) pub gold: i32, // gold left (gold vein)
pub marked: bool, // zum Graben markiert pub marked: bool, // marked for digging
pub reserved: u32, // Entity-ID + 1, die diese Kachel bearbeitet pub reserved: u32, // entity id + 1 working on this tile
pub food: f32, // Pilze im Pilzgarten pub food: f32, // mushrooms in the mushroom garden
pub bed: u32, // Entity-ID + 1, die hier schläft pub bed: u32, // entity id + 1 sleeping here
pub var: u8, // Texturvariante pub var: u8, // texture variant
} }
impl Tile { impl Tile {
@@ -97,7 +97,7 @@ impl Rng {
pub struct Map { pub struct Map {
pub t: Vec<Tile>, 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 portal: (i32, i32),
pub gate: (i32, i32), pub gate: (i32, i32),
} }
@@ -119,7 +119,7 @@ impl Map {
let mut s = seed; let mut s = seed;
loop { loop {
let m = Map::try_generate(s); 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() { if m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() {
return m; return m;
} }
@@ -163,21 +163,21 @@ impl Map {
} }
}; };
// Felsbrocken // rock clusters
for _ in 0..22 { for _ in 0..22 {
let cx = rng.range(4, MW as i32 - 5); let cx = rng.range(4, MW as i32 - 5);
let cy = rng.range(4, MH as i32 - 5); let cy = rng.range(4, MH as i32 - 5);
let r = rng.range(1, 3); let r = rng.range(1, 3);
blob(&mut t, &mut rng, cx, cy, r, Kind::Rock); blob(&mut t, &mut rng, cx, cy, r, Kind::Rock);
} }
// Goldadern // gold veins
for _ in 0..16 { for _ in 0..16 {
let cx = rng.range(4, MW as i32 - 5); let cx = rng.range(4, MW as i32 - 5);
let cy = rng.range(4, MH as i32 - 5); let cy = rng.range(4, MH as i32 - 5);
let r = rng.range(1, 2); let r = rng.range(1, 2);
blob(&mut t, &mut rng, cx, cy, r, Kind::Gold); 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 { for k in 0..3 {
let cx = core.0 + rng.range(-3, 9); 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) }; 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 { for _ in 0..4 {
let cx = rng.range(20, 48); let cx = rng.range(20, 48);
let cy = rng.range(6, 58); 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); 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); 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 y in 0..3 {
for x in 0..3 { for x in 0..3 {
let tl = &mut t[idx(core.0 + x, core.1 + y)]; let tl = &mut t[idx(core.0 + x, core.1 + y)];
@@ -216,14 +216,14 @@ impl Map {
tl.owner = Owner::Player; 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); room(&mut t, portal.0 - 2, portal.1 - 2, portal.0 + 4, portal.1 + 4, Owner::None);
for y in 0..3 { for y in 0..3 {
for x in 0..3 { for x in 0..3 {
t[idx(portal.0 + x, portal.1 + y)].kind = Kind::Portal; 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); room(&mut t, gate.0 - 2, gate.1 - 2, gate.0 + 4, gate.1 + 4, Owner::Hero);
for y in 0..3 { for y in 0..3 {
for x in 0..3 { for x in 0..3 {
@@ -240,9 +240,9 @@ impl Map {
Map { t, core, portal, gate } Map { t, core, portal, gate }
} }
// Allgemeine Dijkstra-Suche. // Generic Dijkstra search.
// cost(i) -> None = unpassierbar, Some(c) = Kosten zum Betreten. // cost(i) -> None = impassable, Some(c) = cost of entering.
// goal(i) -> true = Ziel erreicht. // goal(i) -> true = goal reached.
pub fn search<C, G>(&self, start: usize, cost: C, goal: G, diag: bool) -> Option<Vec<usize>> pub fn search<C, G>(&self, start: usize, cost: C, goal: G, diag: bool) -> Option<Vec<usize>>
where where
C: Fn(usize) -> Option<u32>, C: Fn(usize) -> Option<u32>,
@@ -279,7 +279,7 @@ impl Map {
let Some(c) = cost(ni) else { continue }; let Some(c) = cost(ni) else { continue };
let mut step = c * 10; let mut step = c * 10;
if k >= 4 { 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) { if c > 1 || !self.walk(x + dx, y) || !self.walk(x, y + dy) {
continue; 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. // Sound playback. The game logic reports events (Sfx), they are played here.
// Klänge werden später generiert und per `SoundBank::set` (WAV-Bytes) registriert. // Sounds will be generated later and registered via `SoundBank::set` (WAV bytes).
// Ohne registrierten Klang bleibt ein Ereignis stumm. // Events without a registered sound stay silent.
use crate::game::{Sfx, SfxEvent, SFX_ALL}; use crate::game::{Sfx, SfxEvent, SFX_ALL};
use macroquad::audio::{load_sound_from_bytes, play_sound, PlaySoundParams, Sound}; 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 } 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)] #[allow(dead_code)]
pub async fn set(&mut self, sfx: Sfx, bytes: &[u8]) -> bool { pub async fn set(&mut self, sfx: Sfx, bytes: &[u8]) -> bool {
match load_sound_from_bytes(bytes).await { 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 async fn generate_all(&mut self) {}
pub fn change_volume(&mut self, d: f32) { pub fn change_volume(&mut self, d: f32) {
self.volume = (self.volume + d).clamp(0.0, 1.0); 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 { fn min_gap(sfx: Sfx) -> f64 {
match sfx { match sfx {
Sfx::Dig | Sfx::GoldDig | Sfx::MeleeHit | Sfx::Shoot | Sfx::GateHit | Sfx::CoreHit => 0.08, 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) { if now - self.last[i] < Self::min_gap(ev.sfx) {
return; return;
} }
// Lautstärke nach Entfernung zur Bildschirmmitte // volume by distance to the listener
let mut vol = self.volume; let mut vol = self.volume;
if let Some((wx, wy)) = ev.pos { if let Some((wx, wy)) = ev.pos {
let d = ((wx - listener.x).powi(2) + (wy - listener.y).powi(2)).sqrt(); 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)] #[allow(dead_code)]
pub fn wav_from_samples(samples: &[f32], rate: u32) -> Vec<u8> { pub fn wav_from_samples(samples: &[f32], rate: u32) -> Vec<u8> {
let data_len = (samples.len() * 2) as u32; 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::game::*;
use crate::gfx::*; use crate::gfx::*;
use crate::i18n::{languages, tr, trf};
use crate::map::*; use crate::map::*;
use crate::render3d::{minimap_color, Cam}; use crate::render3d::{minimap_color, Cam};
use crate::sound::SoundBank; use crate::sound::SoundBank;
use macroquad::prelude::*; use macroquad::prelude::*;
pub const S: f32 = 2.0; // UI-Skalierung der Schrift pub const S: f32 = 2.0; // UI font scale
pub const CW: f32 = GW * S; // Zeichenbreite in px pub const CW: f32 = GW * S; // character width in px
pub const CH: f32 = GH * S; // Zeichenhöhe 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: 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); 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 const TOOL_KEYS: [&str; 12] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "X", "V"];
pub struct ToolInfo { pub struct ToolInfo {
pub label: &'static str, pub label: String,
pub name: &'static str, pub name: String,
pub desc: &'static str, pub desc: String,
pub icon: u32, pub icon: u32,
} }
pub fn tool_info(t: Tool) -> ToolInfo { pub fn tool_info(t: Tool) -> ToolInfo {
let i = |label, name, desc, icon| ToolInfo { label, name, desc, icon }; let (id, icon, room) = match t {
match t { Tool::Dig => ("dig", I_PICK, None),
Tool::Dig => i("Graben", "Graben", "Erde und Gold markieren. Ziehen für Rechteck; auf markiertem Feld beginnen hebt Markierung auf.", I_PICK), Tool::Hand => ("hand", I_HAND, None),
Tool::Hand => i("Hand", "Hand", "Eigene Einheit aufnehmen (max. 8). Rechtsklick setzt sie auf eigenem Gebiet ab.", I_HAND), Tool::Room(0) => ("treasury", T_TREASURY, Some(0)),
Tool::Room(0) => i("Schatz", "Schatzkammer", "Lagert Gold: +500 Kapazität pro Feld.", T_TREASURY), Tool::Room(1) => ("lair", T_LAIR, Some(1)),
Tool::Room(1) => i("Quartier", "Quartier", "Ein Schlafplatz pro Feld. Bestimmt, wie viele Kreaturen kommen.", T_LAIR), Tool::Room(2) => ("farm", T_FARM, Some(2)),
Tool::Room(2) => i("Pilze", "Pilzgarten", "Nahrung für Kreaturen. Ab 6 Feldern kommen Höhlenspinnen.", T_FARM), Tool::Room(3) => ("training", T_TRAINING, Some(3)),
Tool::Room(3) => i("Training", "Trainingshalle", "Kreaturen steigen Stufen auf (kostet Gold). Ab 9 Feldern kommen Steinoger.", T_TRAINING), Tool::Room(_) => ("library", T_LIBRARY, Some(4)),
Tool::Room(4) => i("Bücher", "Bibliothek", "Kreaturen erzeugen Mana. Ab 6 Feldern kommen Schattenmagier.", T_LIBRARY), Tool::Sell => ("sell", I_SELL, None),
Tool::Sell => i("Verkauf", "Verkaufen", "Räume zurück in Boden verwandeln (50 % Erstattung).", I_SELL), Tool::Flag => ("flag", I_FLAG, None),
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 => ("lightning", I_BOLT, None),
Tool::Lightning => i("Blitz", "Blitz", "55 Schaden an Helden im Umkreis.", I_BOLT), Tool::Heal => ("heal", I_HEAL, None),
Tool::Heal => i("Heilung", "Heilung", "Heilt eigene Einheiten im Umkreis um 50 %.", I_HEAL), Tool::Summon => ("summon", I_SUMMON, None),
Tool::Summon => i("Wicht", "Grubenwicht beschwören", "Erschafft einen neuen Grubenwicht auf eigenem Gebiet.", I_SUMMON), };
Tool::Room(_) => i("?", "?", "", 0), 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 { pub fn tool_cost(t: Tool, g: &Game) -> String {
match t { match t {
Tool::Room(r) => format!("{} Gold/Feld", ROOM_COST[r]), Tool::Room(r) => trf("cost.gold_tile", &[ROOM_COST[r].to_string()]),
Tool::Lightning => "50 Mana".into(), Tool::Lightning => trf("cost.mana", &["50".into()]),
Tool::Heal => "60 Mana".into(), Tool::Heal => trf("cost.mana", &["60".into()]),
Tool::Summon => format!("{} Mana", g.summon_cost() as i32), Tool::Summon => trf("cost.mana", &[(g.summon_cost() as i32).to_string()]),
_ => String::new(), _ => String::new(),
} }
} }
@@ -93,13 +98,15 @@ pub enum UiAction {
MinimapJump(f32, f32), MinimapJump(f32, f32),
CloseHelp, CloseHelp,
NewGame, NewGame,
SelectLanguage(String),
Consumed, Consumed,
} }
pub struct Ui { pub struct Ui {
pub tool: Tool, pub tool: Tool,
pub show_help: bool, pub show_help: bool,
pub log: Vec<(String, f64)>, pub show_langsel: bool,
pub log: Vec<(Msg, f64)>,
pub rects: Vec<Rect>, pub rects: Vec<Rect>,
minimap_img: Image, minimap_img: Image,
minimap_tex: Texture2D, minimap_tex: Texture2D,
@@ -107,6 +114,7 @@ pub struct Ui {
minimap_rect: Rect, minimap_rect: Rect,
tool_rects: Vec<Rect>, tool_rects: Vec<Rect>,
end_button: Rect, end_button: Rect,
lang_rects: Vec<(Rect, String)>,
} }
fn wrap(s: &str, cols: usize) -> Vec<String> { fn wrap(s: &str, cols: usize) -> Vec<String> {
@@ -148,6 +156,7 @@ impl Ui {
Ui { Ui {
tool: Tool::Dig, tool: Tool::Dig,
show_help: true, show_help: true,
show_langsel: false,
log: Vec::new(), log: Vec::new(),
rects: Vec::new(), rects: Vec::new(),
minimap_img: img, minimap_img: img,
@@ -156,11 +165,12 @@ impl Ui {
minimap_rect: Rect::new(0.0, 0.0, 0.0, 0.0), minimap_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
tool_rects: Vec::new(), tool_rects: Vec::new(),
end_button: Rect::new(0.0, 0.0, 0.0, 0.0), 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 { 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 { 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 { pub fn click(&mut self, x: f32, y: f32, g: &Game) -> UiAction {
let p = vec2(x, y); 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 g.state != 0 {
if self.end_button.contains(p) { if self.end_button.contains(p) {
return UiAction::NewGame; return UiAction::NewGame;
@@ -204,7 +222,7 @@ impl Ui {
let sh = screen_height(); let sh = screen_height();
let (mx, my) = mouse_position(); let (mx, my) = mouse_position();
// ---------------- obere Leiste // ---------------- top bar
let cols = (sw / CW).ceil() as i32; let cols = (sw / CW).ceil() as i32;
gfx.window(0.0, 0.0, cols, 3, S, false, FRAME, BG); 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)); 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); gfx.text(value, *x, y, S, vc);
*x += gfx.measure(value, S) + CW * 2.0; *x += gfx.measure(value, S) + CW * 2.0;
}; };
seg("Gold", GOLD, &format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut x); seg(&tr("hud.gold"), GOLD, &format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut x);
gfx.text("Mana", x, y, S, MANA); let mana = tr("hud.mana");
x += gfx.measure("Mana", S) + CW * 0.5; 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); bar(x, y + 3.0, 70.0, 10.0, g.mana / MANA_MAX, MANA);
x += 70.0 + CW * 0.5; x += 70.0 + CW * 0.5;
gfx.text(&format!("{}", g.mana as i32), x, y, S, TXT); gfx.text(&format!("{}", g.mana as i32), x, y, S, TXT);
x += CW * 5.0; x += CW * 5.0;
gfx.text("Kern", x, y, S, RED); let core = tr("hud.core");
x += gfx.measure("Kern", S) + CW * 0.5; 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); bar(x, y + 3.0, 70.0, 10.0, g.core_hp / CORE_HP, RED);
x += 70.0 + CW * 1.5; x += 70.0 + CW * 1.5;
gfx.text("Tor", x, y, S, BLUE); let gate = tr("hud.gate");
x += gfx.measure("Tor", S) + CW * 0.5; 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); bar(x, y + 3.0, 70.0, 10.0, g.gate_hp / GATE_HP, BLUE);
x += 70.0 + CW * 1.5; x += 70.0 + CW * 1.5;
let cap = g.counts[1].min(30); let cap = g.counts[1].min(30);
seg("Kreaturen", TXT, &format!("{}/{}", g.creature_count(), cap), TXT, &mut x); seg(&tr("hud.creatures"), TXT, &format!("{}/{}", g.creature_count(), cap), TXT, &mut x);
seg("Wichte", TXT, &format!("{}", g.worker_count()), 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 }; 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(&trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col, &mmss(g.wave_timer), wave_col, &mut x);
seg("Armee", TXT, &format!("{}", g.army_value() as i32), TXT, &mut x); seg(&tr("hud.army"), 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) }; let vol = if snd.muted { tr("hud.off") } else { format!("{}%", (snd.volume * 100.0).round() as i32) };
seg("♪", DIM, &vol, DIM, &mut x); seg("♪", DIM, &vol, DIM, &mut x);
// ---------------- Werkzeugleiste // ---------------- toolbar
let tb_rows = 5; let tb_rows = 5;
let tb_y = sh - tb_rows as f32 * CH; let tb_y = sh - tb_rows as f32 * CH;
gfx.window(0.0, tb_y, cols, tb_rows, S, false, FRAME, BG); gfx.window(0.0, tb_y, cols, tb_rows, S, false, FRAME, BG);
@@ -263,13 +284,13 @@ impl Ui {
let ti = tool_info(t); 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.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 }); 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); 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 }); 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; let hx = CW + TOOLS.len() as f32 * (bw + 4.0) + CW;
if hx + 120.0 < sw - 200.0 { 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() { for (k, id) in g.hand.iter().enumerate() {
if let Some(e) = g.ents.iter().find(|e| e.id == *id) { if let Some(e) = g.ents.iter().find(|e| e.id == *id) {
let sx = hx + (k % 4) as f32 * 26.0; let sx = hx + (k % 4) as f32 * 26.0;
@@ -284,18 +305,18 @@ impl Ui {
} }
} }
if g.hand.is_empty() { 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_cols = 16;
let mm_rows = 12; let mm_rows = 12;
let mm_w = mm_cols as f32 * CW; let mm_w = mm_cols as f32 * CW;
let mm_h = mm_rows as f32 * CH; let mm_h = mm_rows as f32 * CH;
let mm_x = sw - mm_w; let mm_x = sw - mm_w;
let mm_y = tb_y - mm_h; 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)); self.rects.push(Rect::new(mm_x, mm_y, mm_w, mm_h));
if now - self.minimap_t > 0.25 { if now - self.minimap_t > 0.25 {
self.minimap_t = now; self.minimap_t = now;
@@ -335,7 +356,7 @@ impl Ui {
draw_line(pa.x, pa.y, pb.x, pb.y, 1.5, WHITE); draw_line(pa.x, pa.y, pb.x, pb.y, 1.5, WHITE);
} }
// ---------------- Infofeld // ---------------- info panel
if !info.is_empty() { if !info.is_empty() {
let icols = 30; let icols = 30;
let mut lines: Vec<(String, Color)> = Vec::new(); let mut lines: Vec<(String, Color)> = Vec::new();
@@ -353,14 +374,14 @@ impl Ui {
} }
} }
// ---------------- Meldungen // ---------------- messages (translated at display time, so a language switch applies at once)
let recent: Vec<&(String, f64)> = self.log.iter().filter(|(_, t)| now - t < 14.0).collect(); let recent: Vec<&(Msg, f64)> = self.log.iter().filter(|(_, t)| now - t < 14.0).collect();
if !recent.is_empty() { if !recent.is_empty() {
let mcols = (((sw - mm_w) / CW) as i32 - 2).clamp(20, 70); let mcols = (((sw - mm_w) / CW) as i32 - 2).clamp(20, 70);
let mut lines: Vec<(String, f32)> = Vec::new(); 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); 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)); lines.push((l, a));
} }
} }
@@ -381,23 +402,25 @@ impl Ui {
let t = TOOLS[i]; let t = TOOLS[i];
let ti = tool_info(t); let ti = tool_info(t);
let tcols = 34; 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); let cost = tool_cost(t, g);
if !cost.is_empty() { 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 rows = lines.len() as i32 + 2;
let tx = (mx - tcols as f32 * CW * 0.5).clamp(0.0, sw - tcols as f32 * CW); 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; 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() { for (k, (l, c)) in lines.iter().enumerate() {
gfx.text(l, tx + CW * 2.0, ty + CH * (k as f32 + 1.0), S, *c); gfx.text(l, tx + CW * 2.0, ty + CH * (k as f32 + 1.0), S, *c);
} }
} }
// ---------------- Dialoge // ---------------- dialogs
if g.state != 0 { if self.show_langsel {
self.draw_langsel(gfx);
} else if g.state != 0 {
self.draw_end(g, gfx); self.draw_end(g, gfx);
} else if self.show_help { } else if self.show_help {
self.draw_help(gfx); self.draw_help(gfx);
@@ -406,36 +429,37 @@ impl Ui {
let x = (sw - w as f32 * CW) * 0.5; let x = (sw - w as f32 * CW) * 0.5;
let y = sh * 0.35; let y = sh * 0.35;
gfx.window(x, y, w, 5, S, true, FRAME_HI, BG); gfx.window(x, y, w, 5, S, true, FRAME_HI, BG);
let t = "PAUSE"; let t = tr("ui.pause");
gfx.text(t, (sw - gfx.measure(t, 3.0)) * 0.5, y + CH, 3.0, FRAME_HI); gfx.text(&t, (sw - gfx.measure(&t, 3.0)) * 0.5, y + CH, 3.0, FRAME_HI);
let t2 = "P: weiter"; let t2 = tr("ui.pause_hint");
gfx.text(t2, (sw - gfx.measure(t2, S)) * 0.5, y + CH * 3.0, S, DIM); gfx.text(&t2, (sw - gfx.measure(&t2, S)) * 0.5, y + CH * 3.0, S, DIM);
} }
} }
fn draw_help(&mut self, gfx: &Gfx) { fn draw_help(&mut self, gfx: &Gfx) {
let lines: &[(&str, Color)] = &[ let keys: &[(&str, Color)] = &[
("Ziel: Zerstöre das Heldentor und schütze deinen Seelenkern.", GOLD), ("help.goal", GOLD),
("", TXT), ("", TXT),
("Linksklick Werkzeug benutzen (Graben/Räume: Rechteck ziehen)", TXT), ("help.k_left", TXT),
("Rechtsklick Einheit aus der Hand absetzen", TXT), ("help.k_right", TXT),
("Rechts ziehen Karte verschieben (auch WASD/Pfeile, Bildrand)", TXT), ("help.k_pan", TXT),
("Mitte ziehen Kamera drehen und neigen (auch Q/E, R/F)", TXT), ("help.k_rot", TXT),
("Mausrad Zoomen", TXT), ("help.k_zoom", TXT),
("1-9, 0, X, V Werkzeug wählen", TXT), ("help.k_tools", TXT),
("P / Leertaste Pause M Ton an/aus +/- Lautstärke", TXT), ("help.k_misc", TXT),
("G Kachel-Umrisse an/aus H Diese Hilfe", TXT), ("help.k_misc2", TXT),
("", TXT), ("", TXT),
("1. Grabe zum Portal und lass es von Grubenwichten erobern.", TXT), ("help.s1", TXT),
("2. Baue Quartier, Pilzgarten und Schatzkammer.", TXT), ("help.s2", TXT),
("3. Trainiere deine Kreaturen, zahle ihren Lohn pünktlich.", TXT), ("help.s3", TXT),
("4. Wehre die Heldenwellen ab. Sie passen sich deiner Armee an.", TXT), ("help.s4", TXT),
("5. Setze die Angriffsflagge nahe ans Tor und greife an!", TXT), ("help.s5", TXT),
("", TXT), ("", TXT),
("Schrift: DINOBYTE von mby (CC BY 4.0)", DIM), ("help.font", DIM),
("", TXT), ("", 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 cols = 74;
let rows = lines.len() as i32 + 4; let rows = lines.len() as i32 + 4;
let sw = screen_width(); let sw = screen_width();
@@ -449,6 +473,40 @@ impl Ui {
self.rects.push(Rect::new(0.0, 0.0, sw, sh)); 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) { fn draw_end(&mut self, g: &Game, gfx: &Gfx) {
let sw = screen_width(); let sw = screen_width();
let sh = screen_height(); let sh = screen_height();
@@ -460,13 +518,14 @@ impl Ui {
let y = (sh - rows as f32 * CH) * 0.5; let y = (sh - rows as f32 * CH) * 0.5;
let col = if win { GOLD } else { RED }; 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)); 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" }; 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); 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." }; 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); 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 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); 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 bw = gfx.measure(b, S) + CW * 2.0;
let bx = (sw - bw) * 0.5; let bx = (sw - bw) * 0.5;
let by = y + CH * 7.75; 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)> { pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec<(String, Color)> {
let mut out = Vec::new(); let mut out = Vec::new();
if let Some(j) = ent { if let Some(j) = ent {
let e = &g.ents[j]; let e = &g.ents[j];
let s = st(e.kind); let s = st(e.kind);
out.push((format!("{} (Stufe {})", s.name, e.level), if e.hero { BLUE } else { FRAME_HI })); out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), 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((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));
out.push((job_name(e).to_string(), TXT)); out.push((tr(job_name(e)), TXT));
if e.hero { 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() { } else if e.creature() {
let pct = |v: f32| (v.clamp(0.0, 1.0) * 100.0).round() as i32; let pct = |v: f32| ((v.clamp(0.0, 1.0) * 100.0).round() as i32).to_string();
out.push((format!("Energie {}% Hunger {}%", pct(e.energy), pct(e.hunger / 2.0)), TXT)); out.push((trf("info.needs", &[pct(e.energy), pct(e.hunger / 2.0)]), TXT));
let mood = if e.happy > 0.6 { let mood = if e.happy > 0.6 {
("zufrieden", GREEN) ("mood.happy", GREEN)
} else if e.happy > 0.25 { } else if e.happy > 0.25 {
("mürrisch", GOLD) ("mood.grumpy", GOLD)
} else { } else {
("wütend", RED) ("mood.angry", RED)
}; };
out.push((format!("Laune: {}", mood.0), mood.1)); out.push((trf("info.mood", &[tr(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)); 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 { } 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; return out;
} }
let Some((x, y)) = tile else { return out }; let Some((x, y)) = tile else { return out };
let Some(t) = g.map.get(x, y) else { return out }; let Some(t) = g.map.get(x, y) else { return out };
let owner = match t.owner { let owner = match t.owner {
Owner::Player => "erobert", Owner::Player => "owner.player",
Owner::Hero => "Heldengebiet", Owner::Hero => "owner.hero",
Owner::None => "neutral", Owner::None => "owner.none",
}; };
match t.kind { match t.kind {
Kind::Rock => out.push(("Fels".into(), DIM)), Kind::Rock => out.push((tr("tile.rock"), DIM)),
Kind::Earth => out.push(("Erde".into(), FRAME_HI)), Kind::Earth => out.push((tr("tile.earth"), FRAME_HI)),
Kind::Gold => { Kind::Gold => {
out.push(("Goldader".into(), GOLD)); out.push((tr("tile.gold"), GOLD));
out.push((format!("{} Gold", t.gold), GOLD)); out.push((trf("tile.gold_amount", &[t.gold.to_string()]), GOLD));
} }
Kind::Floor => { Kind::Floor => {
out.push(("Boden".into(), TXT)); out.push((tr("tile.floor"), TXT));
out.push((owner.into(), DIM)); out.push((tr(owner), DIM));
} }
Kind::Core => { Kind::Core => {
out.push(("Seelenkern".into(), RED)); out.push((tr("tile.core"), RED));
out.push((format!("HP {}/{}", g.core_hp as i32, CORE_HP as i32), RED)); out.push((trf("info.hp", &[(g.core_hp as i32).to_string(), (CORE_HP as i32).to_string()]), RED));
} }
Kind::Portal => { Kind::Portal => {
out.push(("Portal".into(), MANA)); out.push((tr("tile.portal"), MANA));
out.push((if g.portal_owned() { "erobert".into() } else { "neutral: Wichte können es erobern".into() }, DIM)); out.push((tr(if g.portal_owned() { "portal.claimed" } else { "portal.neutral" }), DIM));
} }
Kind::Gate => { Kind::Gate => {
out.push(("Heldentor".into(), BLUE)); out.push((tr("tile.gate"), BLUE));
out.push((format!("HP {}/{}", g.gate_hp as i32, GATE_HP as i32), BLUE)); out.push((trf("info.hp", &[(g.gate_hp as i32).to_string(), (GATE_HP as i32).to_string()]), BLUE));
} }
k => { k => {
if let Some(r) = room_index(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 { match k {
Kind::Farm => out.push((format!("{} Pilze", t.food as i32), GREEN)), Kind::Farm => out.push((trf("farm.food", &[(t.food as i32).to_string()]), GREEN)),
Kind::Lair => out.push((if t.bed != 0 { "Schlafplatz belegt".into() } else { "Schlafplatz frei".into() }, DIM)), Kind::Lair => out.push((tr(if t.bed != 0 { "lair.taken" } else { "lair.free" }), DIM)),
_ => {} _ => {}
} }
} }
} }
} }
if t.kind == Kind::Rock { if t.kind == Kind::Rock {
out.push(("unzerstörbar".into(), DIM)); out.push((tr("tile.indestructible"), DIM));
} }
if t.marked { if t.marked {
out.push(("zum Graben markiert".into(), GOLD)); out.push((tr("tile.marked"), GOLD));
} }
out out
} }
+32
View File
@@ -0,0 +1,32 @@
// DungeonCraft browser plugin for miniquad: localStorage access for the Rust code.
(function () {
"use strict";
var pending = null;
function bytes(ptr, len) {
return new Uint8Array(wasm_memory.buffer, ptr, len);
}
function str(ptr, len) {
return new TextDecoder().decode(bytes(ptr, len));
}
function register(importObject) {
importObject.env.dc_storage_get = function (kp, kl) {
var v = null;
try { v = window.localStorage.getItem(str(kp, kl)); } catch (e) { }
if (v === null) { pending = null; return -1; }
pending = new TextEncoder().encode(v);
return pending.length;
};
importObject.env.dc_storage_take = function (ptr) {
if (pending === null) return;
bytes(ptr, pending.length).set(pending);
pending = null;
};
importObject.env.dc_storage_set = function (kp, kl, vp, vl) {
try { window.localStorage.setItem(str(kp, kl), str(vp, vl)); } catch (e) { }
};
}
miniquad_add_plugin({ register_plugin: register, version: 1, name: "dungeoncraft" });
})();
+2 -1
View File
@@ -11,8 +11,9 @@
</head> </head>
<body> <body>
<canvas id="glcanvas" tabindex="1" oncontextmenu="return false"></canvas> <canvas id="glcanvas" tabindex="1" oncontextmenu="return false"></canvas>
<script>var register_plugin; /* vom quad_net-Plugin im Bundle erwartet */</script> <script>var register_plugin; /* expected by the quad_net plugin in the bundle */</script>
<script src="mq_js_bundle.js"></script> <script src="mq_js_bundle.js"></script>
<script src="dc_plugin.js"></script>
<script> <script>
document.getElementById("glcanvas").focus(); document.getElementById("glcanvas").focus();
load("dungeoncraft.wasm"); load("dungeoncraft.wasm");