Statistics with a chart at the end of a game, 64 achievements kept locally and on the server

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 23:36:19 +01:00
1 parent 193b199875
commit 9931ca9b54
37 files changed
+1833 -4

No files matched your search

+4
View File
@@ -96,6 +96,10 @@ of your forge to break a fortress in the forest.
<br><i>Left: the world up there. Too bright. Right: a drawbridge up, a drawbridge down, and lava that thinks it is a floor.</i>
</p>
**🏆 Champions and trophies.** Every fifth wave a champion of the Order leads the knights - Sir Aldric, Dame
Seraphine, Brother Halvard or Captain Rothgar, each with a trick of his own and loot for whoever brings him down.
After every game a chart shows how it went, and 64 achievements wait - some of them secret, all of them petty.
**👑 Other masters want your realm, too.** Mossgrim the Mouldy (green, smells like a wet cellar) and Lady Nightwell
(blue, writes threatening letters on scented paper) dig their own dungeons in free play and in the campaign.
And the humans attack *everybody*. Let them bite each other first.
+8
View File
@@ -67,6 +67,14 @@ camera's surroundings lie above ground (4 s crossfade).
## 4. Additions
Status: creature abilities (personality.rs: an ability per creature at level 3-7, used by itself) and
creature relationships (likes, hates, brawls) existed already. Done in this block so far:
bosses (`src/bosses.rs`: four champions of the Order lead every fifth wave - Sir Aldric's shield wall,
Dame Seraphine's radiant burst, Brother Halvard's call to arms, Captain Rothgar's charge - with a health bar
and loot), statistics (`src/stats.rs`: counters and a chart at the end of a game) and achievements
(`src/achievements.rs`: 64 of them, kept in the browser and merged with `/api/profile` on the server after a
login; the menu page *Achievements*). Room levels 2 and 3 will be researched in the library (per room kind).
1. **Random events** – earthquake (tunnels cave in), lava eruption, flood, gold vein found. Rare
(every 8–12 minutes), free play and multiplayer only, can be switched off in the menu; in
campaigns only through new script blocks.
Binary file not shown.

After

Width:  |  Height:  |  Size: 769 KiB

+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Porazte Mossgrima a všechny jeho tvory.
coop.m20.boss = Mossgrim už není. Mech zase doroste. On ne.
coop.m20.t1 = Ten drak na východě je Mossgrim. Ano, nechal se trochu jít.
coop.m20.t2 = Každý poražený tvor je o jednoho méně, kdo bude tenhle příběh vyprávět špatně.
# achievements and statistics
menu.achievements = Úspěchy
ach.count = Odemčeno {0} z {1} úspěchů · {2} minut hraní
ach.hidden = Tajemství. Křivoprst nic neprozradí.
msg.achievement = Úspěch odemčen: {0}!
end.stats2 = Hrdinové {0} · Ztracení tvorové {1} · Vykopaná pole {2} · Zlato {3} · Dřevo {4}
ach.heroes.name = Postrach hrdinů {0}
ach.heroes.desc = Poraz {0} hrdinů.
ach.bosses.name = Lamač šampionů {0}
ach.bosses.desc = Svrhni {0} šampionů řádu.
ach.dug.name = Hlubinný kopáč {0}
ach.dug.desc = Vykopej {0} polí.
ach.gold.name = Hromaditel {0}
ach.gold.desc = Vydělej {0} zlata.
ach.wood.name = Pán dřeva {0}
ach.wood.desc = Pokácej {0} dřeva.
ach.walls.name = Zedník {0}
ach.walls.desc = Postav {0} zdí.
ach.rooms.name = Architekt stínů {0}
ach.rooms.desc = Postav {0} polí místností.
ach.spells.name = Temný mág {0}
ach.spells.desc = Sešli {0} kouzel.
ach.slaps.name = Pevná ruka {0}
ach.slaps.desc = Dej svým tvorům {0} pohlavků. Milují to. Asi.
ach.traps.name = Pastičkář {0}
ach.traps.desc = Objednej {0} pastí a dveří v kovárně.
ach.towers.name = Pán věží {0}
ach.towers.desc = Postav {0} věží.
ach.drowned.name = Kurz plavání {0}
ach.drowned.desc = Nech {0} hrdinů spadnout do vody nebo lávy.
ach.burnt.name = Žhář mostů {0}
ach.burnt.desc = Spal {0} mostních prken.
ach.siege.name = Mistr obléhání {0}
ach.siege.desc = Postav {0} obléhacích strojů.
ach.captured.name = Žalářník {0}
ach.captured.desc = Zajmi {0} hrdinů.
ach.drawbridge.name = Nahoru s ním {0}
ach.drawbridge.desc = Vytáhni padací mosty {0}krát.
ach.wins.name = Dobyvatel {0}
ach.wins.desc = Vyhraj {0} volných her.
ach.missions.name = Hledač střepů {0}
ach.missions.desc = Vyhraj {0} misí kampaně.
ach.coop.name = Člen ligy {0}
ach.coop.desc = Vyhraj {0} kooperativních misí.
ach.minutes.name = Noční sova {0}
ach.minutes.desc = Hraj {0} minut.
ach.flawless.name = Ani škrábnutí
ach.flawless.desc = Vyhraj, aniž bys ztratil jediného tvora.
ach.speedrun.name = Než čaj vystydne
ach.speedrun.desc = Vyhraj do patnácti minut.
ach.campaign.name = Jediný mistr
ach.campaign.desc = Vyhraj poslední misi kampaně.
ach.surface.name = Spálení od slunce
ach.surface.desc = Vyhraj hru, která sahá až na povrch.
ach.defeat.name = Čaj a soucit
ach.defeat.desc = Prohraj hru. Křivoprst přistaví vodu na čaj.
ach.legend.name = Živoucí legenda
ach.legend.desc = Tvor dosáhne úrovně deset.
ach.wyrm.name = Zde jsou draci
ach.wyrm.desc = Žhavý drak se přidá k tvé kobce.
ach.fullhand.name = Plné ruce práce
ach.fullhand.desc = Drž v ruce najednou osm jednotek.
+63
View File
@@ -1551,3 +1551,66 @@ coop.m20.goal = Besejr Mossgrim og alle hans væsner.
coop.m20.boss = Mossgrim er ikke mere. Mosset gror ud igen. Det gør han ikke.
coop.m20.t1 = Den lindorm i øst er Mossgrim. Ja, han har sluppet sig selv lidt løs.
coop.m20.t2 = Hvert væsen, I besejrer, er ét mindre, der vil fortælle denne historie forkert.
# achievements and statistics
menu.achievements = Præstationer
ach.count = {0} af {1} præstationer låst op · {2} minutter spillet
ach.hidden = En hemmelighed. Krumfinger siger ingenting.
msg.achievement = Præstation låst op: {0}!
end.stats2 = Helte {0} · Væsner tabt {1} · Felter gravet {2} · Guld {3} · Træ {4}
ach.heroes.name = Heltenes Svøbe {0}
ach.heroes.desc = Besejr {0} helte.
ach.bosses.name = Kæmpeknuser {0}
ach.bosses.desc = Fæld {0} af ordenens kæmper.
ach.dug.name = Dybgraver {0}
ach.dug.desc = Grav {0} felter ud.
ach.gold.name = Skattesamler {0}
ach.gold.desc = Tjen {0} guld.
ach.wood.name = Tømmerfyrste {0}
ach.wood.desc = Fæld {0} træ.
ach.walls.name = Murer {0}
ach.walls.desc = Rejs {0} mure.
ach.rooms.name = Skyggernes Arkitekt {0}
ach.rooms.desc = Byg {0} rumfelter.
ach.spells.name = Mørk Troldmand {0}
ach.spells.desc = Kast {0} besværgelser.
ach.slaps.name = Fast Hånd {0}
ach.slaps.desc = Giv dine væsner en lussing {0} gange. De elsker det. Formentlig.
ach.traps.name = Fældesmed {0}
ach.traps.desc = Bestil {0} fælder og døre i smedjen.
ach.towers.name = Tårnherre {0}
ach.towers.desc = Byg {0} tårne.
ach.drowned.name = Svømmeundervisning {0}
ach.drowned.desc = Lad {0} helte falde i vand eller lava.
ach.burnt.name = Brobrænder {0}
ach.burnt.desc = Brænd {0} broplanker ned.
ach.siege.name = Belejringsmester {0}
ach.siege.desc = Byg {0} belejringsmaskiner.
ach.captured.name = Fangevogter {0}
ach.captured.desc = Tag {0} helte til fange.
ach.drawbridge.name = Op med Den {0}
ach.drawbridge.desc = Hejs vindebroer {0} gange.
ach.wins.name = Erobrer {0}
ach.wins.desc = Vind {0} frie spil.
ach.missions.name = Splintjæger {0}
ach.missions.desc = Vind {0} kampagnemissioner.
ach.coop.name = Forbundsfælle {0}
ach.coop.desc = Vind {0} co-op-missioner.
ach.minutes.name = Natteravn {0}
ach.minutes.desc = Spil i {0} minutter.
ach.flawless.name = Ikke en Skramme
ach.flawless.desc = Vind uden at miste et eneste væsen.
ach.speedrun.name = Før Teen Bliver Kold
ach.speedrun.desc = Vind inden for femten minutter.
ach.campaign.name = Den Ene Mester
ach.campaign.desc = Vind kampagnens sidste mission.
ach.surface.name = Solskoldning
ach.surface.desc = Vind et spil, der når op til overfladen.
ach.defeat.name = Te og Medfølelse
ach.defeat.desc = Tab et spil. Krumfinger sætter kedlen over.
ach.legend.name = Levende Legende
ach.legend.desc = Et væsen når niveau ti.
ach.wyrm.name = Her Er Drager
ach.wyrm.desc = En glødelindorm slutter sig til dit dungeon.
ach.fullhand.name = Hænderne Fulde
ach.fullhand.desc = Hold otte enheder i hånden på én gang.
+63
View File
@@ -1551,3 +1551,66 @@ coop.m20.goal = Besiegt Mossgrim und alle seine Kreaturen.
coop.m20.boss = Mossgrim ist nicht mehr. Das Moos wächst nach. Er nicht.
coop.m20.t1 = Der Wurm im Osten ist Mossgrim. Ja, er hat sich gehen lassen.
coop.m20.t2 = Jede Kreatur, die ihr besiegt, kann diese Geschichte nicht mehr falsch erzählen.
# achievements and statistics
menu.achievements = Erfolge
ach.count = {0} von {1} Erfolgen freigeschaltet · {2} Minuten gespielt
ach.hidden = Ein Geheimnis. Crookfinger verrät nichts.
msg.achievement = Erfolg freigeschaltet: {0}!
end.stats2 = Helden {0} · Kreaturen verloren {1} · Kacheln gegraben {2} · Gold {3} · Holz {4}
ach.heroes.name = Heldenschreck {0}
ach.heroes.desc = Besiege {0} Helden.
ach.bosses.name = Championbrecher {0}
ach.bosses.desc = Bring {0} Champions des Ordens zu Fall.
ach.dug.name = Tiefgräber {0}
ach.dug.desc = Grabe {0} Kacheln aus.
ach.gold.name = Hortmeister {0}
ach.gold.desc = Verdiene {0} Gold.
ach.wood.name = Holzfürst {0}
ach.wood.desc = Fälle {0} Holz.
ach.walls.name = Maurer {0}
ach.walls.desc = Richte {0} Wände auf.
ach.rooms.name = Architekt der Schatten {0}
ach.rooms.desc = Baue {0} Raumkacheln.
ach.spells.name = Dunkler Zauberer {0}
ach.spells.desc = Wirke {0} Zauber.
ach.slaps.name = Strenge Hand {0}
ach.slaps.desc = Gib deinen Kreaturen {0} Klapse. Sie lieben es. Vermutlich.
ach.traps.name = Fallenschmied {0}
ach.traps.desc = Gib {0} Fallen und Türen in der Schmiede in Auftrag.
ach.towers.name = Turmherr {0}
ach.towers.desc = Baue {0} Türme.
ach.drowned.name = Schwimmkurs {0}
ach.drowned.desc = Lass {0} Helden in Wasser oder Lava stürzen.
ach.burnt.name = Brückenbrenner {0}
ach.burnt.desc = Brenne {0} Brückenplanken nieder.
ach.siege.name = Belagerungsmeister {0}
ach.siege.desc = Baue {0} Belagerungsgeräte.
ach.captured.name = Kerkermeister {0}
ach.captured.desc = Nimm {0} Helden gefangen.
ach.drawbridge.name = Hoch damit {0}
ach.drawbridge.desc = Ziehe {0}-mal Zugbrücken hoch.
ach.wins.name = Eroberer {0}
ach.wins.desc = Gewinne {0} freie Spiele.
ach.missions.name = Splittersucher {0}
ach.missions.desc = Gewinne {0} Kampagnenmissionen.
ach.coop.name = Bundesmitglied {0}
ach.coop.desc = Gewinne {0} Koop-Missionen.
ach.minutes.name = Nachteule {0}
ach.minutes.desc = Spiele {0} Minuten lang.
ach.flawless.name = Kein Kratzer
ach.flawless.desc = Gewinne, ohne eine einzige Kreatur zu verlieren.
ach.speedrun.name = Bevor der Tee kalt wird
ach.speedrun.desc = Gewinne innerhalb von fünfzehn Minuten.
ach.campaign.name = Der eine Meister
ach.campaign.desc = Gewinne die letzte Mission der Kampagne.
ach.surface.name = Sonnenbrand
ach.surface.desc = Gewinne ein Spiel, das bis an die Oberfläche reicht.
ach.defeat.name = Tee und Mitgefühl
ach.defeat.desc = Verliere ein Spiel. Crookfinger setzt Wasser auf.
ach.legend.name = Lebende Legende
ach.legend.desc = Eine Kreatur erreicht Stufe zehn.
ach.wyrm.name = Hier seien Drachen
ach.wyrm.desc = Ein Glutwurm schließt sich deinem Kerker an.
ach.fullhand.name = Alle Hände voll
ach.fullhand.desc = Halte acht Einheiten gleichzeitig in der Hand.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Νικήστε τον Μόσγκριμ και όλα του τα
coop.m20.boss = Ο Μόσγκριμ δεν υπάρχει πια. Τα βρύα θα ξαναφυτρώσουν. Αυτός όχι.
coop.m20.t1 = Εκείνος ο δράκος στα ανατολικά είναι ο Μόσγκριμ. Ναι, παραμελήθηκε.
coop.m20.t2 = Κάθε πλάσμα που νικάτε είναι ένα λιγότερο που θα πει αυτή την ιστορία λάθος.
# achievements and statistics
menu.achievements = Επιτεύγματα
ach.count = {0} από {1} επιτεύγματα ξεκλειδώθηκαν · {2} λεπτά παιχνιδιού
ach.hidden = Μυστικό. Ο Στραβοδάχτυλος δεν λέει τίποτα.
msg.achievement = Επίτευγμα ξεκλειδώθηκε: {0}!
end.stats2 = Ήρωες {0} · Χαμένα πλάσματα {1} · Σκαμμένα πλακίδια {2} · Χρυσός {3} · Ξύλο {4}
ach.heroes.name = Μάστιγα ηρώων {0}
ach.heroes.desc = Νίκησε {0} ήρωες.
ach.bosses.name = Συντρίπτης πρωταθλητών {0}
ach.bosses.desc = Ρίξε {0} πρωταθλητές του Τάγματος.
ach.dug.name = Βαθύς σκαφτιάς {0}
ach.dug.desc = Σκάψε {0} πλακίδια.
ach.gold.name = Θησαυριστής {0}
ach.gold.desc = Κέρδισε {0} χρυσό.
ach.wood.name = Άρχοντας του ξύλου {0}
ach.wood.desc = Κόψε {0} ξύλο.
ach.walls.name = Χτίστης {0}
ach.walls.desc = Ύψωσε {0} τοίχους.
ach.rooms.name = Αρχιτέκτονας των σκιών {0}
ach.rooms.desc = Χτίσε {0} πλακίδια δωματίων.
ach.spells.name = Σκοτεινός μάγος {0}
ach.spells.desc = Ρίξε {0} ξόρκια.
ach.slaps.name = Σταθερό χέρι {0}
ach.slaps.desc = Χαστούκισε τα πλάσματά σου {0} φορές. Τους αρέσει. Μάλλον.
ach.traps.name = Παγιδοποιός {0}
ach.traps.desc = Παράγγειλε {0} παγίδες και πόρτες στο σιδηρουργείο.
ach.towers.name = Άρχοντας των πύργων {0}
ach.towers.desc = Χτίσε {0} πύργους.
ach.drowned.name = Μαθήματα κολύμβησης {0}
ach.drowned.desc = Ρίξε {0} ήρωες σε νερό ή λάβα.
ach.burnt.name = Καψογέφυρος {0}
ach.burnt.desc = Κάψε {0} σανίδες γεφυρών.
ach.siege.name = Πολιορκητής {0}
ach.siege.desc = Χτίσε {0} πολιορκητικές μηχανές.
ach.captured.name = Δεσμοφύλακας {0}
ach.captured.desc = Αιχμαλώτισε {0} ήρωες.
ach.drawbridge.name = Και πάνω! {0}
ach.drawbridge.desc = Σήκωσε κινητές γέφυρες {0} φορές.
ach.wins.name = Κατακτητής {0}
ach.wins.desc = Κέρδισε {0} ελεύθερα παιχνίδια.
ach.missions.name = Κυνηγός θραυσμάτων {0}
ach.missions.desc = Κέρδισε {0} αποστολές εκστρατείας.
ach.coop.name = Μέλος της λίγκας {0}
ach.coop.desc = Κέρδισε {0} συνεργατικές αποστολές.
ach.minutes.name = Νυχτοπούλι {0}
ach.minutes.desc = Παίξε για {0} λεπτά.
ach.flawless.name = Ούτε γρατζουνιά
ach.flawless.desc = Νίκησε χωρίς να χάσεις ούτε ένα πλάσμα.
ach.speedrun.name = Πριν κρυώσει το τσάι
ach.speedrun.desc = Νίκησε μέσα σε δεκαπέντε λεπτά.
ach.campaign.name = Ο Ένας Άρχοντας
ach.campaign.desc = Κέρδισε την τελευταία αποστολή της εκστρατείας.
ach.surface.name = Ηλιοκαμένος
ach.surface.desc = Κέρδισε ένα παιχνίδι που φτάνει ως την επιφάνεια.
ach.defeat.name = Τσάι και συμπόνια
ach.defeat.desc = Χάσε ένα παιχνίδι. Ο Στραβοδάχτυλος βάζει νερό να βράσει.
ach.legend.name = Ζωντανός θρύλος
ach.legend.desc = Ένα πλάσμα φτάνει στο επίπεδο δέκα.
ach.wyrm.name = Εδώ υπάρχουν δράκοι
ach.wyrm.desc = Ένας δράκος της θράκας μπαίνει στο μπουντρούμι σου.
ach.fullhand.name = Γεμάτα χέρια
ach.fullhand.desc = Κράτα οκτώ μονάδες στο χέρι σου ταυτόχρονα.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Defeat Mossgrim and all his creatures.
coop.m20.boss = Mossgrim is no more. The moss will grow back. He will not.
coop.m20.t1 = That wyrm in the east is Mossgrim. Yes, he let himself go.
coop.m20.t2 = Every creature you defeat is one less that will tell this story wrong.
# achievements and statistics
menu.achievements = Achievements
ach.count = {0} of {1} achievements unlocked · {2} minutes played
ach.hidden = A secret. Crookfinger is not telling.
msg.achievement = Achievement unlocked: {0}!
end.stats2 = Heroes {0} · Creatures lost {1} · Tiles dug {2} · Gold {3} · Wood {4}
ach.heroes.name = Hero Bane {0}
ach.heroes.desc = Defeat {0} heroes.
ach.bosses.name = Champion Breaker {0}
ach.bosses.desc = Bring down {0} champions of the Order.
ach.dug.name = Deep Digger {0}
ach.dug.desc = Dig out {0} tiles.
ach.gold.name = Hoarder {0}
ach.gold.desc = Earn {0} gold.
ach.wood.name = Lumberlord {0}
ach.wood.desc = Fell {0} wood.
ach.walls.name = Mason {0}
ach.walls.desc = Raise {0} walls.
ach.rooms.name = Architect of Shadows {0}
ach.rooms.desc = Build {0} room tiles.
ach.spells.name = Dark Caster {0}
ach.spells.desc = Cast {0} spells.
ach.slaps.name = Firm Hand {0}
ach.slaps.desc = Slap your creatures {0} times. They love it. Probably.
ach.traps.name = Trapsmith {0}
ach.traps.desc = Order {0} traps and doors in the forge.
ach.towers.name = Tower Lord {0}
ach.towers.desc = Build {0} towers.
ach.drowned.name = Swimming Lessons {0}
ach.drowned.desc = Let {0} heroes fall into water or lava.
ach.burnt.name = Bridge Burner {0}
ach.burnt.desc = Burn down {0} bridge planks.
ach.siege.name = Siege Master {0}
ach.siege.desc = Build {0} siege engines.
ach.captured.name = Warden {0}
ach.captured.desc = Capture {0} heroes.
ach.drawbridge.name = Up She Goes {0}
ach.drawbridge.desc = Raise drawbridges {0} times.
ach.wins.name = Conqueror {0}
ach.wins.desc = Win {0} free games.
ach.missions.name = Shard Seeker {0}
ach.missions.desc = Win {0} campaign missions.
ach.coop.name = League Member {0}
ach.coop.desc = Win {0} co-op missions.
ach.minutes.name = Night Owl {0}
ach.minutes.desc = Play for {0} minutes.
ach.flawless.name = Not a Scratch
ach.flawless.desc = Win without losing a single creature.
ach.speedrun.name = Before the Tea Gets Cold
ach.speedrun.desc = Win within fifteen minutes.
ach.campaign.name = The One Master
ach.campaign.desc = Win the last mission of the campaign.
ach.surface.name = Sunburn
ach.surface.desc = Win a game that reaches the surface.
ach.defeat.name = Tea and Sympathy
ach.defeat.desc = Lose a game. Crookfinger puts the kettle on.
ach.legend.name = Living Legend
ach.legend.desc = A creature reaches level ten.
ach.wyrm.name = Here Be Dragons
ach.wyrm.desc = An ember wyrm joins your dungeon.
ach.fullhand.name = Hands Full
ach.fullhand.desc = Hold eight units in your hand at once.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Derrotad a Mossgrim y a todas sus criaturas.
coop.m20.boss = Mossgrim ya no existe. El musgo volverá a crecer. Él no.
coop.m20.t1 = Ese dragón del este es Mossgrim. Sí, se ha descuidado bastante.
coop.m20.t2 = Cada criatura que derrotáis es una menos que contará mal esta historia.
# achievements and statistics
menu.achievements = Logros
ach.count = {0} de {1} logros desbloqueados · {2} minutos jugados
ach.hidden = Un secreto. Dedotuerto no suelta prenda.
msg.achievement = Logro desbloqueado: ¡{0}!
end.stats2 = Héroes {0} · Criaturas perdidas {1} · Casillas excavadas {2} · Oro {3} · Madera {4}
ach.heroes.name = Azote de héroes {0}
ach.heroes.desc = Derrota a {0} héroes.
ach.bosses.name = Rompecampeones {0}
ach.bosses.desc = Derriba a {0} campeones de la Orden.
ach.dug.name = Excavador profundo {0}
ach.dug.desc = Excava {0} casillas.
ach.gold.name = Acaparador {0}
ach.gold.desc = Gana {0} de oro.
ach.wood.name = Señor de la madera {0}
ach.wood.desc = Tala {0} de madera.
ach.walls.name = Albañil {0}
ach.walls.desc = Levanta {0} muros.
ach.rooms.name = Arquitecto de las sombras {0}
ach.rooms.desc = Construye {0} casillas de sala.
ach.spells.name = Hechicero oscuro {0}
ach.spells.desc = Lanza {0} hechizos.
ach.slaps.name = Mano firme {0}
ach.slaps.desc = Abofetea a tus criaturas {0} veces. Les encanta. Seguramente.
ach.traps.name = Forjatrampas {0}
ach.traps.desc = Encarga {0} trampas y puertas en la forja.
ach.towers.name = Señor de las torres {0}
ach.towers.desc = Construye {0} torres.
ach.drowned.name = Clases de natación {0}
ach.drowned.desc = Haz que {0} héroes caigan al agua o a la lava.
ach.burnt.name = Quemapuentes {0}
ach.burnt.desc = Quema {0} tablones de puente.
ach.siege.name = Maestro de asedio {0}
ach.siege.desc = Construye {0} máquinas de asedio.
ach.captured.name = Carcelero {0}
ach.captured.desc = Captura a {0} héroes.
ach.drawbridge.name = ¡Arriba con él! {0}
ach.drawbridge.desc = Alza puentes levadizos {0} veces.
ach.wins.name = Conquistador {0}
ach.wins.desc = Gana {0} partidas libres.
ach.missions.name = Buscafragmentos {0}
ach.missions.desc = Gana {0} misiones de campaña.
ach.coop.name = Miembro de la liga {0}
ach.coop.desc = Gana {0} misiones cooperativas.
ach.minutes.name = Ave nocturna {0}
ach.minutes.desc = Juega durante {0} minutos.
ach.flawless.name = Ni un rasguño
ach.flawless.desc = Gana sin perder una sola criatura.
ach.speedrun.name = Antes de que se enfríe el té
ach.speedrun.desc = Gana en menos de quince minutos.
ach.campaign.name = El Único Amo
ach.campaign.desc = Gana la última misión de la campaña.
ach.surface.name = Quemadura solar
ach.surface.desc = Gana una partida que llegue hasta la superficie.
ach.defeat.name = Té y compasión
ach.defeat.desc = Pierde una partida. Dedotuerto pone el agua a hervir.
ach.legend.name = Leyenda viviente
ach.legend.desc = Una criatura alcanza el nivel diez.
ach.wyrm.name = Aquí hay dragones
ach.wyrm.desc = Un dragón de brasas se une a tu mazmorra.
ach.fullhand.name = Manos llenas
ach.fullhand.desc = Sostén ocho unidades en la mano a la vez.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Kukistakaa Mossgrim ja kaikki hänen olentonsa.
coop.m20.boss = Mossgrimia ei enää ole. Sammal kasvaa takaisin. Hän ei.
coop.m20.t1 = Tuo lohikäärme idässä on Mossgrim. Niin, hän päästi itsensä rapistumaan.
coop.m20.t2 = Jokainen kukistamanne olento on yksi vähemmän kertomassa tätä tarinaa väärin.
# achievements and statistics
menu.achievements = Saavutukset
ach.count = {0}/{1} saavutusta avattu · {2} minuuttia pelattu
ach.hidden = Salaisuus. Crookfinger ei kerro.
msg.achievement = Saavutus avattu: {0}!
end.stats2 = Sankarit {0} · Menetetyt olennot {1} · Kaivetut ruudut {2} · Kulta {3} · Puu {4}
ach.heroes.name = Sankarikauhu {0}
ach.heroes.desc = Kukista {0} sankaria.
ach.bosses.name = Mestarinmurskaaja {0}
ach.bosses.desc = Kaada {0} ritarikunnan mestaria.
ach.dug.name = Syväkaivaja {0}
ach.dug.desc = Kaiva {0} ruutua.
ach.gold.name = Aarteenkahmija {0}
ach.gold.desc = Ansaitse {0} kultaa.
ach.wood.name = Puuruhtinas {0}
ach.wood.desc = Kaada {0} puuta.
ach.walls.name = Muurari {0}
ach.walls.desc = Pystytä {0} seinää.
ach.rooms.name = Varjojen arkkitehti {0}
ach.rooms.desc = Rakenna {0} huoneruutua.
ach.spells.name = Pimeä loitsija {0}
ach.spells.desc = Loitsi {0} loitsua.
ach.slaps.name = Luja käsi {0}
ach.slaps.desc = Läimäytä olentojasi {0} kertaa. Ne rakastavat sitä. Luultavasti.
ach.traps.name = Ansaseppä {0}
ach.traps.desc = Tilaa pajasta {0} ansaa ja ovea.
ach.towers.name = Tornin herra {0}
ach.towers.desc = Rakenna {0} tornia.
ach.drowned.name = Uimakoulu {0}
ach.drowned.desc = Anna {0} sankarin pudota veteen tai laavaan.
ach.burnt.name = Siltojenpolttaja {0}
ach.burnt.desc = Polta {0} siltalankkua.
ach.siege.name = Piiritysmestari {0}
ach.siege.desc = Rakenna {0} piiritysvälinettä.
ach.captured.name = Vanginvartija {0}
ach.captured.desc = Vangitse {0} sankaria.
ach.drawbridge.name = Ylös vaan {0}
ach.drawbridge.desc = Nosta laskusiltoja {0} kertaa.
ach.wins.name = Valloittaja {0}
ach.wins.desc = Voita {0} vapaata peliä.
ach.missions.name = Sirpaleenetsijä {0}
ach.missions.desc = Voita {0} kampanjatehtävää.
ach.coop.name = Liiton jäsen {0}
ach.coop.desc = Voita {0} yhteistyötehtävää.
ach.minutes.name = Yökyöpeli {0}
ach.minutes.desc = Pelaa {0} minuuttia.
ach.flawless.name = Ei naarmuakaan
ach.flawless.desc = Voita menettämättä yhtäkään olentoa.
ach.speedrun.name = Ennen kuin tee jäähtyy
ach.speedrun.desc = Voita viidessätoista minuutissa.
ach.campaign.name = Se yksi mestari
ach.campaign.desc = Voita kampanjan viimeinen tehtävä.
ach.surface.name = Auringonpolttama
ach.surface.desc = Voita peli, joka ulottuu maan pinnalle.
ach.defeat.name = Teetä ja sympatiaa
ach.defeat.desc = Häviä peli. Crookfinger laittaa teeveden kiehumaan.
ach.legend.name = Elävä legenda
ach.legend.desc = Olento saavuttaa tason kymmenen.
ach.wyrm.name = Täällä on lohikäärmeitä
ach.wyrm.desc = Hehkulohikäärme liittyy luolastoosi.
ach.fullhand.name = Kädet täynnä
ach.fullhand.desc = Pidä kahdeksaa yksikköä kädessäsi yhtä aikaa.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Porazite Mossgrima i sva njegova stvorenja.
coop.m20.boss = Mossgrima više nema. Mahovina će ponovo izrasti. On neće.
coop.m20.t1 = Onaj zmaj na istoku je Mossgrim. Da, zapustio se.
coop.m20.t2 = Svako stvorenje koje porazite jedno je manje koje će ovu priču krivo ispričati.
# achievements and statistics
menu.achievements = Postignuća
ach.count = Otključano {0} od {1} postignuća · {2} minuta igre
ach.hidden = Tajna. Krivoprst ne govori.
msg.achievement = Postignuće otključano: {0}!
end.stats2 = Junaci {0} · Izgubljena stvorenja {1} · Iskopana polja {2} · Zlato {3} · Drvo {4}
ach.heroes.name = Strah junaka {0}
ach.heroes.desc = Porazi {0} junaka.
ach.bosses.name = Lomitelj prvaka {0}
ach.bosses.desc = Obori {0} prvaka reda.
ach.dug.name = Duboki kopač {0}
ach.dug.desc = Iskopaj {0} polja.
ach.gold.name = Gomilatelj {0}
ach.gold.desc = Zaradi {0} zlata.
ach.wood.name = Gospodar drva {0}
ach.wood.desc = Posijeci {0} drva.
ach.walls.name = Zidar {0}
ach.walls.desc = Podigni {0} zidova.
ach.rooms.name = Arhitekt sjena {0}
ach.rooms.desc = Izgradi {0} polja soba.
ach.spells.name = Mračni čarobnjak {0}
ach.spells.desc = Baci {0} čarolija.
ach.slaps.name = Čvrsta ruka {0}
ach.slaps.desc = Pljusni svoja stvorenja {0} puta. Obožavaju to. Valjda.
ach.traps.name = Kovač zamki {0}
ach.traps.desc = Naruči {0} zamki i vrata u kovačnici.
ach.towers.name = Gospodar tornjeva {0}
ach.towers.desc = Izgradi {0} tornjeva.
ach.drowned.name = Tečaj plivanja {0}
ach.drowned.desc = Pusti {0} junaka da padnu u vodu ili lavu.
ach.burnt.name = Palikuća mostova {0}
ach.burnt.desc = Spali {0} dasaka mosta.
ach.siege.name = Majstor opsade {0}
ach.siege.desc = Izgradi {0} opsadnih sprava.
ach.captured.name = Tamničar {0}
ach.captured.desc = Zarobi {0} junaka.
ach.drawbridge.name = Gore s njim {0}
ach.drawbridge.desc = Podigni pokretne mostove {0} puta.
ach.wins.name = Osvajač {0}
ach.wins.desc = Pobijedi u {0} slobodnih igara.
ach.missions.name = Tragač za krhotinama {0}
ach.missions.desc = Pobijedi u {0} misija kampanje.
ach.coop.name = Član saveza {0}
ach.coop.desc = Pobijedi u {0} co-op misija.
ach.minutes.name = Noćna ptica {0}
ach.minutes.desc = Igraj {0} minuta.
ach.flawless.name = Ni ogrebotine
ach.flawless.desc = Pobijedi bez gubitka ijednog stvorenja.
ach.speedrun.name = Prije nego se čaj ohladi
ach.speedrun.desc = Pobijedi unutar petnaest minuta.
ach.campaign.name = Jedini gospodar
ach.campaign.desc = Pobijedi u posljednjoj misiji kampanje.
ach.surface.name = Opeklina od sunca
ach.surface.desc = Pobijedi u igri koja seže do površine.
ach.defeat.name = Čaj i suosjećanje
ach.defeat.desc = Izgubi igru. Krivoprst stavlja vodu za čaj.
ach.legend.name = Živa legenda
ach.legend.desc = Stvorenje dostiže razinu deset.
ach.wyrm.name = Ovdje su zmajevi
ach.wyrm.desc = Žarni zmaj pridružuje se tvojoj tamnici.
ach.fullhand.name = Pune ruke posla
ach.fullhand.desc = Drži osam jedinica u ruci odjednom.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Győzd le Mossgrimot és minden lényét.
coop.m20.boss = Mossgrim nincs többé. A moha visszanő. Ő nem.
coop.m20.t1 = Az a keleti sárkány Mossgrim. Igen, elhanyagolta magát.
coop.m20.t2 = Minden legyőzött lény eggyel kevesebb, aki rosszul meséli majd el ezt a történetet.
# achievements and statistics
menu.achievements = Teljesítmények
ach.count = {0}/{1} teljesítmény feloldva · {2} perc játékidő
ach.hidden = Titok. Crookfinger nem árulja el.
msg.achievement = Teljesítmény feloldva: {0}!
end.stats2 = Hősök {0} · Elvesztett lények {1} · Kiásott mezők {2} · Arany {3} · Fa {4}
ach.heroes.name = Hősök réme {0}
ach.heroes.desc = Győzz le {0} hőst.
ach.bosses.name = Bajnoktörő {0}
ach.bosses.desc = Dönts le {0} bajnokot a rendből.
ach.dug.name = Mélyásó {0}
ach.dug.desc = Áss ki {0} mezőt.
ach.gold.name = Kincsgyűjtő {0}
ach.gold.desc = Szerezz {0} aranyat.
ach.wood.name = Faúr {0}
ach.wood.desc = Vágj ki {0} fát.
ach.walls.name = Kőműves {0}
ach.walls.desc = Emelj {0} falat.
ach.rooms.name = Az árnyak építésze {0}
ach.rooms.desc = Építs {0} teremmezőt.
ach.spells.name = Sötét varázsló {0}
ach.spells.desc = Mondj el {0} varázslatot.
ach.slaps.name = Kemény kéz {0}
ach.slaps.desc = Pofozd fel a lényeidet {0} alkalommal. Imádják. Valószínűleg.
ach.traps.name = Csapdakovács {0}
ach.traps.desc = Rendelj {0} csapdát és ajtót a kovácsműhelyben.
ach.towers.name = Toronyúr {0}
ach.towers.desc = Építs {0} tornyot.
ach.drowned.name = Úszólecke {0}
ach.drowned.desc = Hagyd, hogy {0} hős vízbe vagy lávába zuhanjon.
ach.burnt.name = Hídégető {0}
ach.burnt.desc = Égess fel {0} hídpallót.
ach.siege.name = Ostrommester {0}
ach.siege.desc = Építs {0} ostromgépet.
ach.captured.name = Börtönőr {0}
ach.captured.desc = Ejts foglyul {0} hőst.
ach.drawbridge.name = Fel vele {0}
ach.drawbridge.desc = Húzz fel felvonóhidat {0} alkalommal.
ach.wins.name = Hódító {0}
ach.wins.desc = Nyerj meg {0} szabad játékot.
ach.missions.name = Szilánkkereső {0}
ach.missions.desc = Teljesíts {0} hadjárat-küldetést.
ach.coop.name = Szövetségi tag {0}
ach.coop.desc = Teljesíts {0} koop küldetést.
ach.minutes.name = Éjjeli bagoly {0}
ach.minutes.desc = Játssz {0} percet.
ach.flawless.name = Egy karcolás sem
ach.flawless.desc = Nyerj úgy, hogy egyetlen lényt sem veszítesz.
ach.speedrun.name = Mielőtt kihűl a tea
ach.speedrun.desc = Nyerj tizenöt percen belül.
ach.campaign.name = Az egyetlen mester
ach.campaign.desc = Nyerd meg a hadjárat utolsó küldetését.
ach.surface.name = Leégés
ach.surface.desc = Nyerj meg egy játékot, amely a felszínig ér.
ach.defeat.name = Tea és együttérzés
ach.defeat.desc = Veszíts el egy játékot. Crookfinger felteszi a teavizet.
ach.legend.name = Élő legenda
ach.legend.desc = Egy lény eléri a tizedik szintet.
ach.wyrm.name = Itt sárkányok laknak
ach.wyrm.desc = Egy parázssárkány csatlakozik a kazamatádhoz.
ach.fullhand.name = Tele a kéz
ach.fullhand.desc = Tarts egyszerre nyolc egységet a kezedben.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Sconfiggete Mossgrim e tutte le sue creature.
coop.m20.boss = Mossgrim non c'è più. Il muschio ricrescerà. Lui no.
coop.m20.t1 = Quel drago a est è Mossgrim. Sì, si è lasciato andare.
coop.m20.t2 = Ogni creatura che sconfiggete è una in meno che racconterà male questa storia.
# achievements and statistics
menu.achievements = Obiettivi
ach.count = {0} di {1} obiettivi sbloccati · {2} minuti giocati
ach.hidden = Un segreto. Ditastorte non parla.
msg.achievement = Obiettivo sbloccato: {0}!
end.stats2 = Eroi {0} · Creature perse {1} · Caselle scavate {2} · Oro {3} · Legno {4}
ach.heroes.name = Flagello degli eroi {0}
ach.heroes.desc = Sconfiggi {0} eroi.
ach.bosses.name = Spezzacampioni {0}
ach.bosses.desc = Abbatti {0} campioni dell'Ordine.
ach.dug.name = Scavatore profondo {0}
ach.dug.desc = Scava {0} caselle.
ach.gold.name = Accumulatore {0}
ach.gold.desc = Guadagna {0} oro.
ach.wood.name = Signore del legno {0}
ach.wood.desc = Abbatti {0} legno.
ach.walls.name = Muratore {0}
ach.walls.desc = Alza {0} muri.
ach.rooms.name = Architetto delle ombre {0}
ach.rooms.desc = Costruisci {0} caselle di stanza.
ach.spells.name = Incantatore oscuro {0}
ach.spells.desc = Lancia {0} magie.
ach.slaps.name = Mano ferma {0}
ach.slaps.desc = Dai {0} schiaffi alle tue creature. Lo adorano. Probabilmente.
ach.traps.name = Fabbro di trappole {0}
ach.traps.desc = Ordina {0} trappole e porte alla fucina.
ach.towers.name = Signore delle torri {0}
ach.towers.desc = Costruisci {0} torri.
ach.drowned.name = Lezioni di nuoto {0}
ach.drowned.desc = Fai cadere {0} eroi in acqua o nella lava.
ach.burnt.name = Bruciaponti {0}
ach.burnt.desc = Brucia {0} assi di ponte.
ach.siege.name = Maestro d'assedio {0}
ach.siege.desc = Costruisci {0} macchine d'assedio.
ach.captured.name = Carceriere {0}
ach.captured.desc = Cattura {0} eroi.
ach.drawbridge.name = Su che si va! {0}
ach.drawbridge.desc = Alza i ponti levatoi {0} volte.
ach.wins.name = Conquistatore {0}
ach.wins.desc = Vinci {0} partite libere.
ach.missions.name = Cercaframmenti {0}
ach.missions.desc = Vinci {0} missioni della campagna.
ach.coop.name = Membro della lega {0}
ach.coop.desc = Vinci {0} missioni co-op.
ach.minutes.name = Nottambulo {0}
ach.minutes.desc = Gioca per {0} minuti.
ach.flawless.name = Neanche un graffio
ach.flawless.desc = Vinci senza perdere nemmeno una creatura.
ach.speedrun.name = Prima che il tè si raffreddi
ach.speedrun.desc = Vinci entro quindici minuti.
ach.campaign.name = L'Unico Maestro
ach.campaign.desc = Vinci l'ultima missione della campagna.
ach.surface.name = Scottatura
ach.surface.desc = Vinci una partita che arriva fino in superficie.
ach.defeat.name = Tè e compassione
ach.defeat.desc = Perdi una partita. Ditastorte mette su il bollitore.
ach.legend.name = Leggenda vivente
ach.legend.desc = Una creatura raggiunge il livello dieci.
ach.wyrm.name = Hic sunt dracones
ach.wyrm.desc = Un drago di brace si unisce al tuo dungeon.
ach.fullhand.name = Mani piene
ach.fullhand.desc = Tieni in mano otto unità contemporaneamente.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Versla Mossgrim en al zijn wezens.
coop.m20.boss = Mossgrim is niet meer. Het mos groeit wel terug. Hij niet.
coop.m20.t1 = Die draak in het oosten is Mossgrim. Ja, hij heeft zich laten gaan.
coop.m20.t2 = Elk wezen dat jullie verslaan, is er een minder dat dit verhaal verkeerd vertelt.
# achievements and statistics
menu.achievements = Prestaties
ach.count = {0} van {1} prestaties ontgrendeld · {2} minuten gespeeld
ach.hidden = Een geheim. Krommevinger zegt niks.
msg.achievement = Prestatie ontgrendeld: {0}!
end.stats2 = Helden {0} · Wezens verloren {1} · Vakken gegraven {2} · Goud {3} · Hout {4}
ach.heroes.name = Heldenplaag {0}
ach.heroes.desc = Versla {0} helden.
ach.bosses.name = Kampioenbreker {0}
ach.bosses.desc = Breng {0} kampioenen van de Orde ten val.
ach.dug.name = Diepgraver {0}
ach.dug.desc = Graaf {0} vakken uit.
ach.gold.name = Schatgraaier {0}
ach.gold.desc = Verdien {0} goud.
ach.wood.name = Houthakkersvorst {0}
ach.wood.desc = Kap {0} hout.
ach.walls.name = Metselaar {0}
ach.walls.desc = Richt {0} muren op.
ach.rooms.name = Architect der Schaduwen {0}
ach.rooms.desc = Bouw {0} kamervakken.
ach.spells.name = Duistere Magiër {0}
ach.spells.desc = Spreek {0} spreuken uit.
ach.slaps.name = Strenge Hand {0}
ach.slaps.desc = Geef je wezens {0} keer een mep. Ze vinden het heerlijk. Waarschijnlijk.
ach.traps.name = Vallensmid {0}
ach.traps.desc = Bestel {0} vallen en deuren in de smederij.
ach.towers.name = Torenheer {0}
ach.towers.desc = Bouw {0} torens.
ach.drowned.name = Zwemles {0}
ach.drowned.desc = Laat {0} helden in water of lava vallen.
ach.burnt.name = Bruggenbrander {0}
ach.burnt.desc = Brand {0} brugplanken af.
ach.siege.name = Belegeringsmeester {0}
ach.siege.desc = Bouw {0} belegeringswerktuigen.
ach.captured.name = Cipier {0}
ach.captured.desc = Neem {0} helden gevangen.
ach.drawbridge.name = Hup, omhoog {0}
ach.drawbridge.desc = Haal {0} keer ophaalbruggen op.
ach.wins.name = Veroveraar {0}
ach.wins.desc = Win {0} vrije spellen.
ach.missions.name = Scherfzoeker {0}
ach.missions.desc = Win {0} veldtochtmissies.
ach.coop.name = Bondgenoot {0}
ach.coop.desc = Win {0} co-opmissies.
ach.minutes.name = Nachtuil {0}
ach.minutes.desc = Speel {0} minuten lang.
ach.flawless.name = Geen Schrammetje
ach.flawless.desc = Win zonder één enkel wezen te verliezen.
ach.speedrun.name = Voor de Thee Koud Is
ach.speedrun.desc = Win binnen vijftien minuten.
ach.campaign.name = De Ene Meester
ach.campaign.desc = Win de laatste missie van de veldtocht.
ach.surface.name = Zonnebrand
ach.surface.desc = Win een spel dat tot aan de oppervlakte reikt.
ach.defeat.name = Thee en Medeleven
ach.defeat.desc = Verlies een spel. Krommevinger zet water op.
ach.legend.name = Levende Legende
ach.legend.desc = Een wezen bereikt niveau tien.
ach.wyrm.name = Hier Zijn Draken
ach.wyrm.desc = Een gloeddraak sluit zich aan bij je kerker.
ach.fullhand.name = Handen Vol
ach.fullhand.desc = Houd acht units tegelijk in je hand.
+63
View File
@@ -1551,3 +1551,66 @@ coop.m20.goal = Beseir Mossgrim og alle skapningene hans.
coop.m20.boss = Mossgrim er ikke mer. Mosen gror tilbake. Det gjør ikke han.
coop.m20.t1 = Den lindormen i øst er Mossgrim. Ja, han har sluppet seg litt løs.
coop.m20.t2 = Hver skapning dere beseirer, er én mindre som kommer til å fortelle denne historien feil.
# achievements and statistics
menu.achievements = Prestasjoner
ach.count = {0} av {1} prestasjoner låst opp · {2} minutter spilt
ach.hidden = En hemmelighet. Krokfinger sier ingenting.
msg.achievement = Prestasjon låst opp: {0}!
end.stats2 = Helter {0} · Skapninger tapt {1} · Ruter gravd {2} · Gull {3} · Tre {4}
ach.heroes.name = Heltenes Svøpe {0}
ach.heroes.desc = Beseire {0} helter.
ach.bosses.name = Forkjemperknuser {0}
ach.bosses.desc = Felle {0} av ordenens forkjempere.
ach.dug.name = Dypgraver {0}
ach.dug.desc = Grav ut {0} ruter.
ach.gold.name = Skattesamler {0}
ach.gold.desc = Tjen {0} gull.
ach.wood.name = Tømmerfyrste {0}
ach.wood.desc = Felle {0} tre.
ach.walls.name = Murer {0}
ach.walls.desc = Reis {0} murer.
ach.rooms.name = Skyggenes Arkitekt {0}
ach.rooms.desc = Bygg {0} romruter.
ach.spells.name = Mørk Trollmann {0}
ach.spells.desc = Kast {0} trollformler.
ach.slaps.name = Fast Hånd {0}
ach.slaps.desc = Gi skapningene dine en ørefik {0} ganger. De elsker det. Sannsynligvis.
ach.traps.name = Fellesmed {0}
ach.traps.desc = Bestill {0} feller og dører i smia.
ach.towers.name = Tårnherre {0}
ach.towers.desc = Bygg {0} tårn.
ach.drowned.name = Svømmeopplæring {0}
ach.drowned.desc = La {0} helter falle i vann eller lava.
ach.burnt.name = Brobrenner {0}
ach.burnt.desc = Brenn ned {0} broplanker.
ach.siege.name = Beleiringsmester {0}
ach.siege.desc = Bygg {0} beleiringsmaskiner.
ach.captured.name = Fangevokter {0}
ach.captured.desc = Ta {0} helter til fange.
ach.drawbridge.name = Opp med Den {0}
ach.drawbridge.desc = Heis vindebroer {0} ganger.
ach.wins.name = Erobrer {0}
ach.wins.desc = Vinn {0} frie spill.
ach.missions.name = Skårjeger {0}
ach.missions.desc = Vinn {0} kampanjeoppdrag.
ach.coop.name = Forbundsfelle {0}
ach.coop.desc = Vinn {0} co-op-oppdrag.
ach.minutes.name = Nattugle {0}
ach.minutes.desc = Spill i {0} minutter.
ach.flawless.name = Ikke en Skramme
ach.flawless.desc = Vinn uten å miste en eneste skapning.
ach.speedrun.name = Før Teen Blir Kald
ach.speedrun.desc = Vinn innen femten minutter.
ach.campaign.name = Den Ene Mesteren
ach.campaign.desc = Vinn kampanjens siste oppdrag.
ach.surface.name = Solbrent
ach.surface.desc = Vinn et spill som når opp til overflaten.
ach.defeat.name = Te og Medfølelse
ach.defeat.desc = Tap et spill. Krokfinger setter på kjelen.
ach.legend.name = Levende Legende
ach.legend.desc = En skapning når nivå ti.
ach.wyrm.name = Her Være Drager
ach.wyrm.desc = En glødlindorm slutter seg til dungeonet ditt.
ach.fullhand.name = Hendene Fulle
ach.fullhand.desc = Hold åtte enheter i hånden samtidig.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Derrotem o Mossgrim e todas as suas criaturas.
coop.m20.boss = O Mossgrim já não existe. O musgo volta a crescer. Ele não.
coop.m20.t1 = Aquela serpe a este é o Mossgrim. Sim, ele desleixou-se.
coop.m20.t2 = Cada criatura que derrotarem é menos uma para contar mal esta história.
# achievements and statistics
menu.achievements = Conquistas
ach.count = {0} de {1} conquistas desbloqueadas · {2} minutos jogados
ach.hidden = Um segredo. O Dedotorto não conta.
msg.achievement = Conquista desbloqueada: {0}!
end.stats2 = Heróis {0} · Criaturas perdidas {1} · Casas escavadas {2} · Ouro {3} · Madeira {4}
ach.heroes.name = Flagelo dos Heróis {0}
ach.heroes.desc = Derrota {0} heróis.
ach.bosses.name = Quebra-Campeões {0}
ach.bosses.desc = Derruba {0} campeões da Ordem.
ach.dug.name = Escavador Profundo {0}
ach.dug.desc = Escava {0} casas.
ach.gold.name = Açambarcador {0}
ach.gold.desc = Ganha {0} de ouro.
ach.wood.name = Senhor da Lenha {0}
ach.wood.desc = Abate {0} de madeira.
ach.walls.name = Pedreiro {0}
ach.walls.desc = Ergue {0} paredes.
ach.rooms.name = Arquiteto das Sombras {0}
ach.rooms.desc = Constrói {0} casas de salas.
ach.spells.name = Feiticeiro Sombrio {0}
ach.spells.desc = Lança {0} feitiços.
ach.slaps.name = Mão Firme {0}
ach.slaps.desc = Esbofeteia as tuas criaturas {0} vezes. Elas adoram. Provavelmente.
ach.traps.name = Armadilheiro {0}
ach.traps.desc = Encomenda {0} armadilhas e portas na forja.
ach.towers.name = Senhor das Torres {0}
ach.towers.desc = Constrói {0} torres.
ach.drowned.name = Aulas de Natação {0}
ach.drowned.desc = Deixa {0} heróis cair na água ou na lava.
ach.burnt.name = Incendiário de Pontes {0}
ach.burnt.desc = Queima {0} tábuas de ponte.
ach.siege.name = Mestre de Cerco {0}
ach.siege.desc = Constrói {0} máquinas de cerco.
ach.captured.name = Carcereiro {0}
ach.captured.desc = Captura {0} heróis.
ach.drawbridge.name = Para Cima! {0}
ach.drawbridge.desc = Levanta pontes levadiças {0} vezes.
ach.wins.name = Conquistador {0}
ach.wins.desc = Vence {0} jogos livres.
ach.missions.name = Caçador de Fragmentos {0}
ach.missions.desc = Vence {0} missões da campanha.
ach.coop.name = Membro da Liga {0}
ach.coop.desc = Vence {0} missões cooperativas.
ach.minutes.name = Ave Noturna {0}
ach.minutes.desc = Joga durante {0} minutos.
ach.flawless.name = Nem um Arranhão
ach.flawless.desc = Vence sem perder uma única criatura.
ach.speedrun.name = Antes que o Chá Arrefeça
ach.speedrun.desc = Vence em menos de quinze minutos.
ach.campaign.name = O Mestre Único
ach.campaign.desc = Vence a última missão da campanha.
ach.surface.name = Escaldão
ach.surface.desc = Vence um jogo que chega à superfície.
ach.defeat.name = Chá e Compaixão
ach.defeat.desc = Perde um jogo. O Dedotorto põe a chaleira ao lume.
ach.legend.name = Lenda Viva
ach.legend.desc = Uma criatura atinge o nível dez.
ach.wyrm.name = Aqui Há Dragões
ach.wyrm.desc = Uma serpe de brasa junta-se à tua masmorra.
ach.fullhand.name = Mãos Cheias
ach.fullhand.desc = Segura oito unidades na mão ao mesmo tempo.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Învingeți-l pe Mossgrim și toate creaturile lui.
coop.m20.boss = Mossgrim nu mai e. Mușchiul va crește la loc. El nu.
coop.m20.t1 = Balaurul de la est e Mossgrim. Da, s-a lăsat cam pe tânjală.
coop.m20.t2 = Fiecare creatură pe care o învingeți e una mai puțin care va povesti greșit această istorie.
# achievements and statistics
menu.achievements = Realizări
ach.count = {0} din {1} realizări deblocate · {2} minute jucate
ach.hidden = Un secret. Crookfinger nu spune nimic.
msg.achievement = Realizare deblocată: {0}!
end.stats2 = Eroi {0} · Creaturi pierdute {1} · Dale săpate {2} · Aur {3} · Lemn {4}
ach.heroes.name = Groaza eroilor {0}
ach.heroes.desc = Învinge {0} eroi.
ach.bosses.name = Zdrobitor de campioni {0}
ach.bosses.desc = Doboară {0} campioni ai Ordinului.
ach.dug.name = Săpător adânc {0}
ach.dug.desc = Sapă {0} dale.
ach.gold.name = Strângător de comori {0}
ach.gold.desc = Câștigă {0} aur.
ach.wood.name = Stăpânul lemnului {0}
ach.wood.desc = Taie {0} lemn.
ach.walls.name = Zidar {0}
ach.walls.desc = Ridică {0} ziduri.
ach.rooms.name = Arhitectul umbrelor {0}
ach.rooms.desc = Construiește {0} dale de cameră.
ach.spells.name = Vrăjitor întunecat {0}
ach.spells.desc = Aruncă {0} vrăji.
ach.slaps.name = Mână fermă {0}
ach.slaps.desc = Dă-le creaturilor tale {0} palme. Le place. Probabil.
ach.traps.name = Făurar de capcane {0}
ach.traps.desc = Comandă {0} capcane și uși la forjă.
ach.towers.name = Stăpânul turnurilor {0}
ach.towers.desc = Construiește {0} turnuri.
ach.drowned.name = Lecții de înot {0}
ach.drowned.desc = Lasă {0} eroi să cadă în apă sau în lavă.
ach.burnt.name = Incendiator de poduri {0}
ach.burnt.desc = Arde {0} scânduri de pod.
ach.siege.name = Maestru al asediului {0}
ach.siege.desc = Construiește {0} mașinării de asediu.
ach.captured.name = Temnicer {0}
ach.captured.desc = Capturează {0} eroi.
ach.drawbridge.name = Sus cu el {0}
ach.drawbridge.desc = Ridică podurile mobile de {0} ori.
ach.wins.name = Cuceritor {0}
ach.wins.desc = Câștigă {0} jocuri libere.
ach.missions.name = Căutător de cioburi {0}
ach.missions.desc = Câștigă {0} misiuni de campanie.
ach.coop.name = Membru al Ligii {0}
ach.coop.desc = Câștigă {0} misiuni co-op.
ach.minutes.name = Bufniță de noapte {0}
ach.minutes.desc = Joacă {0} minute.
ach.flawless.name = Nicio zgârietură
ach.flawless.desc = Câștigă fără să pierzi nicio creatură.
ach.speedrun.name = Până nu se răcește ceaiul
ach.speedrun.desc = Câștigă în cel mult cincisprezece minute.
ach.campaign.name = Singurul Stăpân
ach.campaign.desc = Câștigă ultima misiune a campaniei.
ach.surface.name = Arsură de soare
ach.surface.desc = Câștigă un joc care ajunge la suprafață.
ach.defeat.name = Ceai și compasiune
ach.defeat.desc = Pierde un joc. Crookfinger pune de un ceai.
ach.legend.name = Legendă vie
ach.legend.desc = O creatură ajunge la nivelul zece.
ach.wyrm.name = Aici sunt dragoni
ach.wyrm.desc = Un balaur de jar se alătură temniței tale.
ach.fullhand.name = Mâinile pline
ach.fullhand.desc = Ține opt unități în mână deodată.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Победите Моссгрима и всех его суще
coop.m20.boss = Моссгрима больше нет. Мох отрастёт. Он - нет.
coop.m20.t1 = Этот змий на востоке - Моссгрим. Да, он себя запустил.
coop.m20.t2 = Каждое побеждённое существо - на одного меньше, кто перескажет эту историю неправильно.
# achievements and statistics
menu.achievements = Достижения
ach.count = Открыто достижений: {0} из {1} · Сыграно минут: {2}
ach.hidden = Секрет. Кривопал не расскажет.
msg.achievement = Достижение открыто: {0}!
end.stats2 = Герои {0} · Потеряно существ {1} · Выкопано клеток {2} · Золото {3} · Дерево {4}
ach.heroes.name = Гроза героев {0}
ach.heroes.desc = Победи героев: {0}.
ach.bosses.name = Сокрушитель чемпионов {0}
ach.bosses.desc = Повергни чемпионов ордена: {0}.
ach.dug.name = Глубокий копатель {0}
ach.dug.desc = Выкопай клеток: {0}.
ach.gold.name = Скопидом {0}
ach.gold.desc = Заработай {0} золота.
ach.wood.name = Лесной барон {0}
ach.wood.desc = Сруби {0} дерева.
ach.walls.name = Каменщик {0}
ach.walls.desc = Возведи стен: {0}.
ach.rooms.name = Зодчий теней {0}
ach.rooms.desc = Построй клеток комнат: {0}.
ach.spells.name = Тёмный чародей {0}
ach.spells.desc = Сотвори чар: {0}.
ach.slaps.name = Твёрдая рука {0}
ach.slaps.desc = Отшлёпай своих существ {0} раз. Им нравится. Наверное.
ach.traps.name = Мастер ловушек {0}
ach.traps.desc = Закажи в кузнице ловушек и дверей: {0}.
ach.towers.name = Владыка башен {0}
ach.towers.desc = Построй башен: {0}.
ach.drowned.name = Уроки плавания {0}
ach.drowned.desc = Пусть героев упадёт в воду или лаву: {0}.
ach.burnt.name = Поджигатель мостов {0}
ach.burnt.desc = Сожги досок мостов: {0}.
ach.siege.name = Мастер осады {0}
ach.siege.desc = Построй осадных машин: {0}.
ach.captured.name = Тюремщик {0}
ach.captured.desc = Возьми героев в плен: {0}.
ach.drawbridge.name = Поднять мост! {0}
ach.drawbridge.desc = Подними подъёмные мосты {0} раз.
ach.wins.name = Завоеватель {0}
ach.wins.desc = Выиграй свободных игр: {0}.
ach.missions.name = Искатель осколков {0}
ach.missions.desc = Выиграй миссий кампании: {0}.
ach.coop.name = Член лиги {0}
ach.coop.desc = Выиграй кооп-миссий: {0}.
ach.minutes.name = Полуночник {0}
ach.minutes.desc = Сыграй {0} минут.
ach.flawless.name = Ни царапины
ach.flawless.desc = Победи, не потеряв ни одного существа.
ach.speedrun.name = Пока чай не остыл
ach.speedrun.desc = Победи за пятнадцать минут.
ach.campaign.name = Единый Властелин
ach.campaign.desc = Выиграй последнюю миссию кампании.
ach.surface.name = Солнечный ожог
ach.surface.desc = Выиграй игру, которая доходит до поверхности.
ach.defeat.name = Чай и сочувствие
ach.defeat.desc = Проиграй игру. Кривопал ставит чайник.
ach.legend.name = Живая легенда
ach.legend.desc = Существо достигает десятого уровня.
ach.wyrm.name = Здесь водятся драконы
ach.wyrm.desc = Огненный змий присоединяется к твоему подземелью.
ach.fullhand.name = Полные руки
ach.fullhand.desc = Держи в руке восемь существ одновременно.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Porazte Mossgrima a všetky jeho tvory.
coop.m20.boss = Mossgrim už nie je. Mach zase dorastie. On nie.
coop.m20.t1 = Ten šarkan na východe je Mossgrim. Áno, trochu sa zanedbal.
coop.m20.t2 = Každý porazený tvor je o jedného menej, kto bude tento príbeh rozprávať zle.
# achievements and statistics
menu.achievements = Úspechy
ach.count = Odomknuté {0} z {1} úspechov · {2} minút hrania
ach.hidden = Tajomstvo. Krivoprst nič neprezradí.
msg.achievement = Úspech odomknutý: {0}!
end.stats2 = Hrdinovia {0} · Stratené tvory {1} · Vykopané polia {2} · Zlato {3} · Drevo {4}
ach.heroes.name = Postrach hrdinov {0}
ach.heroes.desc = Poraz {0} hrdinov.
ach.bosses.name = Lamač šampiónov {0}
ach.bosses.desc = Zvrhni {0} šampiónov rádu.
ach.dug.name = Hlbinný kopáč {0}
ach.dug.desc = Vykop {0} polí.
ach.gold.name = Hromadič {0}
ach.gold.desc = Zarob {0} zlata.
ach.wood.name = Drevorubačský pán {0}
ach.wood.desc = Vyrúbaj {0} dreva.
ach.walls.name = Murár {0}
ach.walls.desc = Postav {0} múrov.
ach.rooms.name = Architekt tieňov {0}
ach.rooms.desc = Postav {0} polí miestností.
ach.spells.name = Temný mág {0}
ach.spells.desc = Zošli {0} kúziel.
ach.slaps.name = Pevná ruka {0}
ach.slaps.desc = Daj svojim tvorom {0} záuch. Milujú to. Asi.
ach.traps.name = Pascový kováč {0}
ach.traps.desc = Objednaj {0} pascí a dverí vo vyhni.
ach.towers.name = Pán veží {0}
ach.towers.desc = Postav {0} veží.
ach.drowned.name = Kurz plávania {0}
ach.drowned.desc = Nechaj {0} hrdinov spadnúť do vody alebo lávy.
ach.burnt.name = Podpaľač mostov {0}
ach.burnt.desc = Spáľ {0} mostových dosiek.
ach.siege.name = Majster obliehania {0}
ach.siege.desc = Postav {0} obliehacích strojov.
ach.captured.name = Žalárnik {0}
ach.captured.desc = Zajmi {0} hrdinov.
ach.drawbridge.name = Hore s ním {0}
ach.drawbridge.desc = Vytiahni padacie mosty {0}-krát.
ach.wins.name = Dobyvateľ {0}
ach.wins.desc = Vyhraj {0} voľných hier.
ach.missions.name = Hľadač črepov {0}
ach.missions.desc = Vyhraj {0} misií kampane.
ach.coop.name = Člen ligy {0}
ach.coop.desc = Vyhraj {0} kooperatívnych misií.
ach.minutes.name = Nočná sova {0}
ach.minutes.desc = Hraj {0} minút.
ach.flawless.name = Ani škrabanec
ach.flawless.desc = Vyhraj bez straty jediného tvora.
ach.speedrun.name = Kým čaj nevychladne
ach.speedrun.desc = Vyhraj do pätnástich minút.
ach.campaign.name = Jediný majster
ach.campaign.desc = Vyhraj poslednú misiu kampane.
ach.surface.name = Úpal
ach.surface.desc = Vyhraj hru, ktorá siaha až na povrch.
ach.defeat.name = Čaj a súcit
ach.defeat.desc = Prehraj hru. Krivoprst pristaví vodu na čaj.
ach.legend.name = Živá legenda
ach.legend.desc = Tvor dosiahne úroveň desať.
ach.wyrm.name = Tu sú draci
ach.wyrm.desc = Žeravý šarkan sa pridá k tvojej kobke.
ach.fullhand.name = Plné ruky práce
ach.fullhand.desc = Drž v ruke naraz osem jednotiek.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Поразите Мосгрима и сва његова ств
coop.m20.boss = Мосгрима више нема. Маховина ће поново израсти. Он неће.
coop.m20.t1 = Онај змај на истоку је Мосгрим. Да, запустио се.
coop.m20.t2 = Свако створење које поразите једно је мање које ће ову причу погрешно испричати.
# achievements and statistics
menu.achievements = Достигнућа
ach.count = Откључано {0} од {1} достигнућа · {2} минута игре
ach.hidden = Тајна. Кривопрст не говори.
msg.achievement = Достигнуће откључано: {0}!
end.stats2 = Јунаци {0} · Изгубљена створења {1} · Ископана поља {2} · Злато {3} · Дрво {4}
ach.heroes.name = Страх јунака {0}
ach.heroes.desc = Порази {0} јунака.
ach.bosses.name = Ломитељ првака {0}
ach.bosses.desc = Обори {0} првака реда.
ach.dug.name = Дубоки копач {0}
ach.dug.desc = Ископај {0} поља.
ach.gold.name = Гомилач {0}
ach.gold.desc = Заради {0} злата.
ach.wood.name = Господар дрва {0}
ach.wood.desc = Посеци {0} дрва.
ach.walls.name = Зидар {0}
ach.walls.desc = Подигни {0} зидова.
ach.rooms.name = Архитекта сенки {0}
ach.rooms.desc = Изгради {0} поља соба.
ach.spells.name = Мрачни чаробњак {0}
ach.spells.desc = Баци {0} чини.
ach.slaps.name = Чврста рука {0}
ach.slaps.desc = Ошамари своја створења {0} пута. Обожавају то. Ваљда.
ach.traps.name = Ковач замки {0}
ach.traps.desc = Наручи {0} замки и врата у ковачници.
ach.towers.name = Господар кула {0}
ach.towers.desc = Изгради {0} кула.
ach.drowned.name = Курс пливања {0}
ach.drowned.desc = Пусти {0} јунака да падну у воду или лаву.
ach.burnt.name = Палитељ мостова {0}
ach.burnt.desc = Спали {0} дасака моста.
ach.siege.name = Мајстор опсаде {0}
ach.siege.desc = Изгради {0} опсадних справа.
ach.captured.name = Тамничар {0}
ach.captured.desc = Зароби {0} јунака.
ach.drawbridge.name = Горе с њим {0}
ach.drawbridge.desc = Подигни покретне мостове {0} пута.
ach.wins.name = Освајач {0}
ach.wins.desc = Победи у {0} слободних игара.
ach.missions.name = Трагач за крхотинама {0}
ach.missions.desc = Победи у {0} мисија кампање.
ach.coop.name = Члан савеза {0}
ach.coop.desc = Победи у {0} ко-оп мисија.
ach.minutes.name = Ноћна птица {0}
ach.minutes.desc = Играј {0} минута.
ach.flawless.name = Ни огреботине
ach.flawless.desc = Победи без губитка иједног створења.
ach.speedrun.name = Пре него што се чај охлади
ach.speedrun.desc = Победи у року од петнаест минута.
ach.campaign.name = Једини господар
ach.campaign.desc = Победи у последњој мисији кампање.
ach.surface.name = Опекотина од сунца
ach.surface.desc = Победи у игри која сеже до површине.
ach.defeat.name = Чај и саосећање
ach.defeat.desc = Изгуби игру. Кривопрст ставља воду за чај.
ach.legend.name = Жива легенда
ach.legend.desc = Створење достиже ниво десет.
ach.wyrm.name = Овде су змајеви
ach.wyrm.desc = Жарки змај се придружује твојој тамници.
ach.fullhand.name = Пуне руке посла
ach.fullhand.desc = Држи осам јединица у руци одједном.
+63
View File
@@ -1552,3 +1552,66 @@ coop.m20.goal = Besegra Mossgrim och alla hans varelser.
coop.m20.boss = Mossgrim finns inte mer. Mossan växer tillbaka. Det gör inte han.
coop.m20.t1 = Den där lindormen i öster är Mossgrim. Ja, han har släppt taget om sig själv.
coop.m20.t2 = Varje varelse ni besegrar är en mindre som berättar den här historien fel.
# achievements and statistics
menu.achievements = Prestationer
ach.count = {0} av {1} prestationer upplåsta · {2} minuter spelade
ach.hidden = En hemlighet. Crookfinger säger ingenting.
msg.achievement = Prestation upplåst: {0}!
end.stats2 = Hjältar {0} · Förlorade varelser {1} · Grävda rutor {2} · Guld {3} · Trä {4}
ach.heroes.name = Hjälteskräck {0}
ach.heroes.desc = Besegra {0} hjältar.
ach.bosses.name = Kämpebrytare {0}
ach.bosses.desc = Fäll {0} av ordens kämpar.
ach.dug.name = Djupgrävare {0}
ach.dug.desc = Gräv ut {0} rutor.
ach.gold.name = Skattsamlare {0}
ach.gold.desc = Tjäna {0} guld.
ach.wood.name = Timmerfurste {0}
ach.wood.desc = Fäll {0} trä.
ach.walls.name = Murare {0}
ach.walls.desc = Res {0} väggar.
ach.rooms.name = Skuggornas arkitekt {0}
ach.rooms.desc = Bygg {0} rumsrutor.
ach.spells.name = Mörk besvärjare {0}
ach.spells.desc = Kasta {0} trollformler.
ach.slaps.name = Fast hand {0}
ach.slaps.desc = Ge dina varelser {0} örfilar. De älskar det. Troligen.
ach.traps.name = Fällsmed {0}
ach.traps.desc = Beställ {0} fällor och dörrar i smedjan.
ach.towers.name = Tornherre {0}
ach.towers.desc = Bygg {0} torn.
ach.drowned.name = Simskola {0}
ach.drowned.desc = Låt {0} hjältar falla i vatten eller lava.
ach.burnt.name = Brobrännare {0}
ach.burnt.desc = Bränn ner {0} broplankor.
ach.siege.name = Belägringsmästare {0}
ach.siege.desc = Bygg {0} belägringsmaskiner.
ach.captured.name = Fångvaktare {0}
ach.captured.desc = Ta {0} hjältar till fånga.
ach.drawbridge.name = Upp med den {0}
ach.drawbridge.desc = Hissa upp vindbryggor {0} gånger.
ach.wins.name = Erövrare {0}
ach.wins.desc = Vinn {0} fria spel.
ach.missions.name = Skärvsökare {0}
ach.missions.desc = Vinn {0} kampanjuppdrag.
ach.coop.name = Förbundsmedlem {0}
ach.coop.desc = Vinn {0} co-op-uppdrag.
ach.minutes.name = Nattuggla {0}
ach.minutes.desc = Spela i {0} minuter.
ach.flawless.name = Inte en skråma
ach.flawless.desc = Vinn utan att förlora en enda varelse.
ach.speedrun.name = Innan teet kallnar
ach.speedrun.desc = Vinn inom femton minuter.
ach.campaign.name = Den ende mästaren
ach.campaign.desc = Vinn kampanjens sista uppdrag.
ach.surface.name = Solbränna
ach.surface.desc = Vinn ett spel som når upp till ytan.
ach.defeat.name = Te och medkänsla
ach.defeat.desc = Förlora ett spel. Crookfinger sätter på tevattnet.
ach.legend.name = Levande legend
ach.legend.desc = En varelse når nivå tio.
ach.wyrm.name = Här finns drakar
ach.wyrm.desc = En glödlindorm ansluter sig till din håla.
ach.fullhand.name = Fullt upp
ach.fullhand.desc = Håll åtta enheter i handen samtidigt.
+38
View File
@@ -562,6 +562,43 @@ async fn list_campaigns(State(st): State<AppState>, headers: HeaderMap) -> Respo
Json(campaign_infos(&st.campaign_dir(&u.name)).await).into_response()
}
// the player's profile: lifetime statistics and achievements (an opaque JSON object; the client
// merges it with what it kept locally)
const MAX_PROFILE: usize = 64 * 1024;
async fn get_profile(State(st): State<AppState>, headers: HeaderMap) -> Response {
let u = match st.auth(&headers).await {
Ok(u) => u,
Err(r) => return r,
};
match tokio::fs::read(st.user_dir(&u.name).join("profile.json")).await {
Ok(b) => ([("content-type", "application/json")], b).into_response(),
Err(_) => ([("content-type", "application/json")], b"{}".to_vec()).into_response(),
}
}
async fn put_profile(State(st): State<AppState>, headers: HeaderMap, body: Bytes) -> Response {
let u = match st.auth(&headers).await {
Ok(u) => u,
Err(r) => return r,
};
if body.len() > MAX_PROFILE {
return err(StatusCode::PAYLOAD_TOO_LARGE, "profile too large");
}
if limited(&st.writes, WRITES_PER_MINUTE).await {
return err(StatusCode::TOO_MANY_REQUESTS, "too many requests");
}
if !serde_json::from_slice::<serde_json::Value>(&body).map_or(false, |v| v.is_object()) {
return err(StatusCode::BAD_REQUEST, "not a JSON object");
}
let dir = st.user_dir(&u.name);
let _ = tokio::fs::create_dir_all(&dir).await;
match tokio::fs::write(dir.join("profile.json"), &body).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(_) => err(StatusCode::INTERNAL_SERVER_ERROR, "write failed"),
}
}
async fn get_campaign(State(st): State<AppState>, headers: HeaderMap, Path(id): Path<String>) -> Response {
let u = match st.auth(&headers).await {
Ok(u) => u,
@@ -754,6 +791,7 @@ fn router(state: AppState) -> Router {
.route("/api/users", get(list_users).post(create_user))
.route("/api/users/{name}", delete(delete_user))
.route("/api/netplay/ws", get(netplay::netplay_ws))
.route("/api/profile", get(get_profile).put(put_profile))
.route("/api/saves", get(list).post(create))
.route("/api/saves/{id}", get(get_save).put(overwrite).delete(delete_save))
.route("/api/campaigns", get(list_campaigns).post(create_campaign).layer(campaign_limit()))
+310
View File
@@ -0,0 +1,310 @@
// Achievements: a lifetime profile of the player (totals of every game's statistics, wins,
// minutes played) and 64 achievements - tiers of counters (bronze, silver, gold, ...) and a few
// special, partly hidden ones. Kept in the browser; after a login merged with the server's copy
// (the larger counter wins, unlocked stays unlocked) and stored there too.
use crate::game::*;
use crate::stats as k;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const PROFILE_KEY: &str = "dc.profile";
// counters of the profile: the game statistics (stats.rs) and a few of its own
pub const STAT_NAMES: [&str; k::COUNTERS] = [
"heroes", "bosses", "lost", "dug", "wood", "walls", "rooms", "spells", "slaps", "traps", "towers", "drowned", "burnt", "siege", "captured", "gold", "drawbridge",
];
// tiered achievements: id (= counter), thresholds, icon cell
pub const TIERS: [(&str, &[f64], u32); 20] = [
("heroes", &[1.0, 50.0, 250.0, 1000.0], crate::gfx::I_ARROW),
("bosses", &[1.0, 5.0, 20.0], crate::gfx::I_RUNE),
("dug", &[100.0, 1000.0, 10000.0], crate::gfx::I_PICK),
("gold", &[10000.0, 100000.0, 1000000.0], crate::gfx::I_SELL),
("wood", &[50.0, 500.0, 2000.0], crate::gfx::I_BRIDGE),
("walls", &[10.0, 100.0], crate::gfx::I_WALL),
("rooms", &[50.0, 500.0, 2000.0], crate::gfx::I_STEEL),
("spells", &[10.0, 100.0, 500.0], crate::gfx::I_BOLT),
("slaps", &[10.0, 100.0, 1000.0], crate::gfx::I_HAND),
("traps", &[5.0, 50.0, 200.0], crate::gfx::I_TRAPS),
("towers", &[5.0, 50.0], crate::gfx::I_CANNON),
("drowned", &[1.0, 25.0, 100.0], crate::gfx::I_FREEZE),
("burnt", &[5.0, 50.0], crate::gfx::I_DRAWBRIDGE),
("siege", &[1.0, 10.0], crate::gfx::I_CATAPULT),
("captured", &[1.0, 25.0, 100.0], crate::gfx::I_SUMMON),
("drawbridge", &[1.0, 50.0], crate::gfx::I_DRAWBRIDGE),
("wins", &[1.0, 10.0, 50.0], crate::gfx::I_FLAG),
("missions", &[1.0, 10.0, 30.0], crate::gfx::I_MAGIC),
("coop", &[1.0, 10.0, 20.0], crate::gfx::I_HEAL),
("minutes", &[60.0, 600.0, 3000.0], crate::gfx::I_SPELLS),
];
// special achievements: id, hidden until unlocked, icon cell
pub const SPECIAL: [(&str, bool, u32); 8] = [
("flawless", false, crate::gfx::I_HEAL),
("speedrun", false, crate::gfx::I_BOLT),
("campaign", false, crate::gfx::I_RUNE),
("surface", false, crate::gfx::I_WALL),
("defeat", true, crate::gfx::I_SELL),
("legend", false, crate::gfx::I_ARROW),
("wyrm", true, crate::gfx::I_SUMMON),
("fullhand", true, crate::gfx::I_HAND),
];
// number of achievements in all
#[cfg(test)]
pub fn count() -> usize {
TIERS.iter().map(|t| t.1.len()).sum::<usize>() + SPECIAL.len()
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Profile {
#[serde(default)]
pub totals: BTreeMap<String, f64>,
#[serde(default)]
pub unlocked: BTreeMap<String, u64>, // achievement id ("heroes.2", "flawless") -> unix time
}
impl Profile {
pub fn total(&self, id: &str) -> f64 {
self.totals.get(id).copied().unwrap_or(0.0)
}
// the larger counter wins, unlocked stays unlocked (with the earlier date)
pub fn merge(&mut self, other: &Profile) {
for (k, v) in &other.totals {
let e = self.totals.entry(k.clone()).or_insert(0.0);
*e = e.max(*v);
}
for (k, t) in &other.unlocked {
let e = self.unlocked.entry(k.clone()).or_insert(*t);
*e = (*e).min(*t);
}
}
// unlocks what the totals have reached; returns the new ones
pub fn check_tiers(&mut self, now: u64) -> Vec<String> {
let mut new = Vec::new();
for (id, steps, _) in TIERS.iter() {
let v = self.total(id);
for (t, &need) in steps.iter().enumerate() {
let key = format!("{}.{}", id, t + 1);
if v >= need && !self.unlocked.contains_key(&key) {
self.unlocked.insert(key.clone(), now);
new.push(key);
}
}
}
new
}
pub fn unlock(&mut self, id: &str, now: u64) -> bool {
if self.unlocked.contains_key(id) {
return false;
}
self.unlocked.insert(id.to_string(), now);
true
}
}
// what the tracker needs to know about the running game besides the game itself
pub struct Context {
pub coop: bool,
pub editor_test: bool, // test play from the editor counts nothing
}
// Follows the running game: adds what happened to the profile every few seconds and unlocks
pub struct Tracker {
pub profile: Profile,
flushed: Vec<f32>,
last_time: f32,
ended: bool,
t: f32,
pub dirty: bool, // changed since the last store
pub upload: bool, // something new for the server
}
impl Tracker {
pub fn new(profile: Profile) -> Tracker {
Tracker { profile, flushed: vec![0.0; k::COUNTERS], last_time: 0.0, ended: false, t: 0.0, dirty: false, upload: false }
}
// a different game started (new, loaded): nothing of it has been counted yet
fn restart(&mut self, g: &Game) {
self.flushed = (0..k::COUNTERS).map(|i| g.stats.get(i)).collect();
self.ended = g.state != 0;
}
// called every frame; returns achievements unlocked just now
pub fn update(&mut self, g: &Game, ctx: &Context, dt: f32, now: u64) -> Vec<String> {
if g.demo.is_some() || ctx.editor_test {
self.last_time = 0.0;
return Vec::new();
}
// a loaded or new game: its old counters are not counted again
if g.time + 0.5 < self.last_time || self.last_time == 0.0 {
self.restart(g);
}
self.last_time = g.time.max(0.01);
self.t += dt;
let ended_now = g.state != 0 && !self.ended;
if self.t < 5.0 && !ended_now {
return Vec::new();
}
let played = self.t;
self.t = 0.0;
let mut new = Vec::new();
// the counters of the game
for i in 0..k::COUNTERS {
let cur = g.stats.get(i);
let d = cur - self.flushed[i];
if d > 0.0 {
*self.profile.totals.entry(STAT_NAMES[i].to_string()).or_insert(0.0) += d as f64;
self.flushed[i] = cur;
self.dirty = true;
}
}
if g.state == 0 {
*self.profile.totals.entry("minutes".into()).or_insert(0.0) += (played / 60.0) as f64;
self.dirty = true;
}
// the special ones that can happen any time
if g.ents.iter().any(|e| e.alive && e.creature() && e.level >= 10) && self.profile.unlock("legend", now) {
new.push("legend".to_string());
}
if g.ents.iter().any(|e| e.alive && e.creature() && e.kind == EK::Wyrm) && self.profile.unlock("wyrm", now) {
new.push("wyrm".to_string());
}
if g.hand.len() >= 8 && self.profile.unlock("fullhand", now) {
new.push("fullhand".to_string());
}
// the end of the game
if ended_now {
self.ended = true;
if g.state == 1 {
let key = if ctx.coop {
"coop"
} else if g.mission.is_some() {
"missions"
} else {
"wins"
};
*self.profile.totals.entry(key.into()).or_insert(0.0) += 1.0;
if g.stats.get(k::LOST) == 0.0 && self.profile.unlock("flawless", now) {
new.push("flawless".into());
}
if g.time < 15.0 * 60.0 && self.profile.unlock("speedrun", now) {
new.push("speedrun".into());
}
let last = crate::campaign::missions().len().saturating_sub(1);
if !ctx.coop && g.mission.as_ref().map_or(false, |m| m.index == last) && self.profile.unlock("campaign", now) {
new.push("campaign".into());
}
if !g.map.sky.is_empty() && self.profile.unlock("surface", now) {
new.push("surface".into());
}
} else if self.profile.unlock("defeat", now) {
new.push("defeat".into());
}
self.dirty = true;
}
new.extend(self.profile.check_tiers(now));
if !new.is_empty() {
self.dirty = true;
self.upload = true;
}
new
}
}
// the name and description of an achievement ("heroes.2" or "flawless") in the current language
pub fn texts(id: &str) -> (String, String) {
use crate::i18n::{tr, trf};
match id.split_once('.') {
Some((cat, tier)) => {
let t: usize = tier.parse().unwrap_or(1);
let need = TIERS.iter().find(|x| x.0 == cat).and_then(|x| x.1.get(t - 1)).copied().unwrap_or(0.0);
let roman = ["I", "II", "III", "IV", "V"][(t - 1).min(4)];
(trf(&format!("ach.{}.name", cat), &[roman.to_string()]), trf(&format!("ach.{}.desc", cat), &[fmt_count(need)]))
}
None => (tr(&format!("ach.{}.name", id)), tr(&format!("ach.{}.desc", id))),
}
}
pub fn fmt_count(v: f64) -> String {
let n = v.round() as i64;
let s = n.to_string();
let mut out = String::new();
for (i, c) in s.chars().enumerate() {
if i > 0 && (s.len() - i) % 3 == 0 {
out.push('.');
}
out.push(c);
}
out
}
// every achievement in display order: (id, hidden, icon, tier 0..)
pub fn all() -> Vec<(String, bool, u32, usize)> {
let mut v = Vec::new();
for (id, steps, icon) in TIERS.iter() {
for t in 0..steps.len() {
v.push((format!("{}.{}", id, t + 1), false, *icon, t));
}
}
for (id, hidden, icon) in SPECIAL.iter() {
v.push((id.to_string(), *hidden, *icon, 9));
}
v
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn more_than_fifty_achievements() {
assert!(count() >= 50, "{}", count());
assert_eq!(all().len(), count());
}
#[test]
fn the_tracker_counts_a_game_and_unlocks() {
let mut g = Game::new(3, 64, 64);
let mut t = Tracker::new(Profile::default());
let ctx = Context { coop: false, editor_test: false };
g.time = 1.0;
t.update(&g, &ctx, 0.1, 1);
g.count(k::HEROES, 3.0);
g.count(k::DUG, 150.0);
g.time = 10.0;
let new = t.update(&g, &ctx, 6.0, 2);
assert!(new.contains(&"heroes.1".to_string()) && new.contains(&"dug.1".to_string()), "{:?}", new);
assert_eq!(t.profile.total("heroes"), 3.0);
// the same counters are not counted twice
g.time = 20.0;
t.update(&g, &ctx, 6.0, 3);
assert_eq!(t.profile.total("heroes"), 3.0);
// a won free game without losses
g.state = 1;
let new = t.update(&g, &ctx, 0.1, 4);
assert!(new.contains(&"wins.1".to_string()) && new.contains(&"flawless".to_string()), "{:?}", new);
}
#[test]
fn profiles_merge() {
let mut a = Profile::default();
a.totals.insert("heroes".into(), 10.0);
a.unlocked.insert("heroes.1".into(), 5);
let mut b = Profile::default();
b.totals.insert("heroes".into(), 7.0);
b.totals.insert("dug".into(), 100.0);
b.unlocked.insert("heroes.1".into(), 3);
b.unlocked.insert("dug.1".into(), 4);
a.merge(&b);
assert_eq!(a.total("heroes"), 10.0);
assert_eq!(a.total("dug"), 100.0);
assert_eq!(a.unlocked["heroes.1"], 3);
assert!(a.unlocked.contains_key("dug.1"));
}
}
+1
View File
@@ -110,6 +110,7 @@ impl Game {
return;
}
self.ents[j].boss = 0;
self.count_local(crate::stats::BOSSES);
let got = self.add_gold(BOSS_GOLD);
self.rune_free += 1;
let (x, y) = (self.ents[j].x, self.ents[j].y);
+2
View File
@@ -128,6 +128,7 @@ impl Game {
}
if t >= BURN_TIME {
self.bridgework.burning.swap_remove(k);
self.count_local(crate::stats::BURNT);
self.bridge_falls(i);
continue;
}
@@ -262,6 +263,7 @@ impl Game {
t.res = (t.res & DB_LAVA) | state;
}
if raise {
self.count(crate::stats::DRAWBRIDGE, 1.0);
// whoever stands on it jumps to the bank (or falls)
for &g in &group {
self.ground_lost(g);
+20
View File
@@ -80,6 +80,8 @@ enum Req {
CampaignPublish,
PublicList,
PublicLoad,
ProfileLoad,
ProfileSave,
}
// the logged-in account
@@ -124,6 +126,7 @@ pub struct Cloud {
pub public_listed: bool,
pub campaign_error: Option<&'static str>, // language key of the last failed campaign request
loaded_campaign: Option<String>,
pub profile_loaded: Option<String>, // the profile from the server, waiting to be merged
saved_campaign: Option<CampaignInfo>,
pending: Option<(u32, Req)>,
save_name: String,
@@ -176,6 +179,7 @@ impl Cloud {
public_listed: false,
campaign_error: None,
loaded_campaign: None,
profile_loaded: None,
saved_campaign: None,
pending: Some((id, Req::Health)),
save_name: String::new(),
@@ -330,6 +334,13 @@ impl Cloud {
CloudEvent::Error(error_key(res.status))
}
}
Req::ProfileLoad => {
if ok {
self.profile_loaded = Some(String::from_utf8_lossy(&res.body).into_owned());
}
CloudEvent::None
}
Req::ProfileSave => CloudEvent::None,
Req::CreateUser | Req::DeleteUser => {
if ok {
self.load_users();
@@ -470,6 +481,15 @@ impl Cloud {
}
// JSON text of a campaign loaded by load_campaign() or load_public_campaign()
// the profile (statistics and achievements) of the logged-in player
pub fn load_profile(&mut self) -> bool {
self.session.is_some() && self.start("GET", "profile", &[], Req::ProfileLoad)
}
pub fn save_profile(&mut self, json: &str) -> bool {
self.session.is_some() && self.start("PUT", "profile", json.as_bytes(), Req::ProfileSave)
}
pub fn take_loaded_campaign(&mut self) -> Option<String> {
self.loaded_campaign.take()
}
+30
View File
@@ -304,6 +304,11 @@ pub struct Ent {
pub disp_y: f32,
}
// units of the neutral dwarves count for nobody
fn e_is_dwarf_side(side: u8) -> bool {
side == crate::dwarves::DWARF_SIDE
}
fn yes() -> bool {
true
}
@@ -596,6 +601,8 @@ pub struct Game {
#[serde(default)]
pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges
#[serde(default)]
pub stats: crate::stats::GameStats, // statistics of the master at this computer (see stats.rs)
#[serde(default)]
pub wall_plans: Vec<(usize, u8)>, // walls to raise: tile, master side (see walls.rs)
#[serde(default)]
pub wood: f32, // the second resource (see wood.rs)
@@ -920,6 +927,7 @@ impl Game {
vrng: crate::particles::vrng(),
dwarf_caves: Vec::new(),
bridgework: Default::default(),
stats: Default::default(),
wall_plans: Vec::new(),
wood: crate::wood::WOOD_START,
woodwork: Default::default(),
@@ -1137,6 +1145,7 @@ impl Game {
self.gold += acc;
if acc > 0.0 {
self.incoming.push((res, acc, at));
self.count(crate::stats::GOLD, acc);
}
acc
}
@@ -1384,6 +1393,7 @@ impl Game {
}
let local = self.local_side;
self.switch_to(local);
self.sample_stats(STEP);
}
// ---------------------------------------------------------------- masters
@@ -2494,6 +2504,7 @@ impl Game {
t.hp = 0.0;
t.gold = 0;
let (x, y) = xy(i);
self.count(crate::stats::DUG, 1.0);
self.fx(3, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.6, 0xff304a66);
self.sfx(Sfx::DigDone, x as f32 + 0.5, y as f32 + 0.5);
}
@@ -2759,6 +2770,8 @@ impl Game {
e.patrol.clear();
let b = st(kind).bounty as f32;
let got = self.add_gold(b);
self.count_local(crate::stats::HEROES);
self.count_local(crate::stats::CAPTURED);
self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff);
self.sfx(Sfx::Capture, x, y);
self.msg("msg.hero_captured", vec![format!("@{}", st(kind).name), (got as i32).to_string()]);
@@ -2780,6 +2793,15 @@ impl Game {
self.bury(x, y);
}
let s = st(kind);
if hero {
self.count_local(crate::stats::HEROES);
} else if side == self.local_side && !e_is_dwarf_side(side) {
let me = self.me_side;
let local = self.local_side;
self.switch_to(local);
self.count(crate::stats::LOST, 1.0);
self.switch_to(me);
}
if hero {
let b = s.bounty as f32;
let got = self.add_gold(b);
@@ -2866,6 +2888,9 @@ impl Game {
if !e.alive || e.held || e.tile() != i || flies(e.kind) {
continue;
}
if e.hero {
self.count_local(crate::stats::DROWNED);
}
if lava {
self.burn(j);
continue;
@@ -4778,6 +4803,7 @@ impl Game {
if built == 0 && !broke {
self.hint(7, "hint.build_where");
}
self.count(crate::stats::ROOMS, built as f32);
self.recount();
}
@@ -4923,6 +4949,7 @@ impl Game {
let tower = Tower { id, kind, tile: i, level: 1, hp: s.hp, cd: 1.0, aim: 0.0, flash: 0.0, hero: false, rune_final };
let (px, py) = tower.pos();
self.towers.push(tower);
self.count(crate::stats::TOWERS, 1.0);
// units standing on the tile step aside
for e in self.ents.iter_mut() {
if e.alive && e.tile() == i {
@@ -5460,6 +5487,7 @@ impl Game {
self.fx(2, x, y, 0.0, 0.0, 0.3, 0xffffffff);
self.sfx(Sfx::Slap, x, y);
self.script_event(crate::mscript::ScriptEvent::Slap);
self.count(crate::stats::SLAPS, 1.0);
return 1;
}
if self.ents[j].captive != 0 {
@@ -5498,6 +5526,7 @@ impl Game {
return;
}
self.mana -= COST;
self.count(crate::stats::SPELLS, 1.0);
self.sfx(Sfx::Lightning, wx, wy);
self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa);
self.ignite_at(wx, wy);
@@ -5524,6 +5553,7 @@ impl Game {
return;
}
self.mana -= COST;
self.count(crate::stats::SPELLS, 1.0);
self.sfx(Sfx::Heal, wx, wy);
let mut fxs = Vec::new();
for e in self.ents.iter_mut() {
+49
View File
@@ -1,5 +1,6 @@
// DungeonCraft – dungeon building & defence in the browser (Rust + macroquad, WASM).
mod achievements;
mod audio;
mod campaign;
mod campaign_doc;
@@ -41,6 +42,7 @@ mod traps;
mod sound;
mod surface;
mod spells;
mod stats;
mod touch;
mod ui;
mod voice;
@@ -847,6 +849,12 @@ async fn main() {
let mut r3d = R3d::new();
let mut sounds = SoundBank::new();
let mut music = MusicDirector::new();
// lifetime statistics and achievements (browser storage, merged with the server after a login)
let stored_profile: achievements::Profile = platform::storage_get(achievements::PROFILE_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default();
let mut tracker = achievements::Tracker::new(stored_profile);
let mut want_profile = false; // fetch the server's profile when the client is free
let mut profile_store_t = 0.0f32;
let mut profile_upload_t = 0.0f32;
let mut ui = Ui::new();
let mut menu = Menu::new();
let mut cloud = Cloud::new(); // savegame service; files + browser autosave when unreachable
@@ -992,6 +1000,7 @@ async fn main() {
}
}
CloudEvent::LoggedIn => {
want_profile = true;
if menu.page == Some(Page::Login) {
menu.page = Some(Page::Title);
}
@@ -1939,8 +1948,47 @@ app.pending_briefing = briefing_of(&campaign, i);
| Some(Page::Account)
| Some(Page::Bestiary { from_title: true, .. })
| Some(Page::Match { from_title: true })
| Some(Page::Achievements { from_title: true })
);
music.update(&app.game, on_title, (app.cam.rx, app.cam.rz));
// ---------------- achievements: count the game, unlock, store and share the profile
{
let ctx = achievements::Context { coop: app.coop, editor_test: app.editor_testing };
let unix = miniquad::date::now() as u64;
let counting = !on_title && (playing || app.game.state != 0);
for id in if counting { tracker.update(&app.game, &ctx, dt, unix) } else { Vec::new() } {
let (name, _) = achievements::texts(&id);
app.game.msgs.push(game::Msg { key: "msg.achievement", args: vec![name] });
audio::play_sfx(game::Sfx::Victory.index(), 0.8, 0.0);
}
if let Some(json) = cloud.profile_loaded.take() {
if let Ok(p) = serde_json::from_str::<achievements::Profile>(&json) {
tracker.profile.merge(&p);
tracker.profile.check_tiers(unix);
tracker.dirty = true;
tracker.upload = tracker.profile != p;
}
}
if want_profile && cloud.session.is_some() && cloud.load_profile() {
want_profile = false;
}
profile_store_t += dt;
profile_upload_t += dt;
if tracker.dirty && (profile_store_t > 10.0 || tracker.upload) {
profile_store_t = 0.0;
tracker.dirty = false;
platform::storage_set(achievements::PROFILE_KEY, &serde_json::to_string(&tracker.profile).unwrap_or_default());
}
if tracker.upload && cloud.session.is_some() && profile_upload_t > 30.0 && !want_profile {
if cloud.save_profile(&serde_json::to_string(&tracker.profile).unwrap_or_default()) {
tracker.upload = false;
profile_upload_t = 0.0;
}
}
if matches!(menu.page, Some(Page::Achievements { .. })) {
menu.profile = tracker.profile.clone();
}
}
for m in app.game.msgs.drain(..) {
if app.settings.voice && voice::speaks(m.key) {
voice.say(&i18n::trf(m.key, &m.args), now, 12);
@@ -2039,6 +2087,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
| Some(Page::Account)
| Some(Page::Bestiary { from_title: true, .. })
| Some(Page::Match { from_title: true })
| Some(Page::Achievements { from_title: true })
);
if !title_screen {
let info = if over_ui || (touch && inp.inspect.is_none()) {
+16 -1
View File
@@ -14,6 +14,7 @@ use crate::settings::{all_actions, key_name, Action, Settings};
use crate::ui::{grid_center, grid_center_y, mouse, sh, sw, tool_info, wrap, side_color, CH, CW, DIM, FRAME, FRAME_HI, GOLD, S, SMALL, TITLE, TOOLS, TXT};
use macroquad::prelude::*;
mod achievements;
mod bestiary;
pub mod flags;
@@ -33,6 +34,7 @@ pub enum Page {
Account, // password, logout, users (admin)
Match { from_title: bool }, // multiplayer lobby and match status
Bestiary { tab: usize, sel: usize, level: u32, from_title: bool }, // units, buildings, ranks
Achievements { from_title: bool },
}
pub const TABS: [&str; 6] = ["set.tab.sound", "set.tab.camera", "set.tab.graphics", "set.tab.game", "set.tab.debug", "set.tab.keys"];
@@ -294,6 +296,7 @@ enum Btn {
MatchLeave,
MatchChat,
Bestiary,
Achievements,
BestTab(usize),
BestSel(i32),
BestPick(usize),
@@ -441,6 +444,7 @@ pub struct Menu {
confirm: Option<Btn>, // overwrite / delete waiting for a second click
scroll: usize,
rows_visible: usize,
pub profile: crate::achievements::Profile, // shown on the achievements page
buttons: Vec<(Rect, Btn)>,
lang_open: Option<Rect>, // the language dropdown is open below/above this field
lang_scroll: usize, // first language shown in the open dropdown
@@ -505,6 +509,7 @@ impl Menu {
lang_rows: 0,
cur_lang: String::new(),
rows_visible: 1,
profile: Default::default(),
buttons: Vec::new(),
}
}
@@ -530,6 +535,7 @@ impl Menu {
Some(Page::Match { from_title }) => Some(if from_title { Page::Title } else { Page::Pause }),
Some(Page::Bestiary { from_title: true, .. }) => Some(Page::Title),
Some(Page::Bestiary { from_title: false, .. }) => Some(Page::Pause),
Some(Page::Achievements { from_title }) => Some(if from_title { Page::Title } else { Page::Pause }),
Some(Page::Briefing { ingame: true, .. }) => Some(Page::Pause),
Some(Page::Briefing { ingame: false, .. }) => Some(Page::Campaign),
Some(Page::CustomBriefing) => None,
@@ -916,6 +922,11 @@ impl Menu {
}
MenuAction::Consumed
}
Some(Btn::Achievements) => {
self.page = Some(Page::Achievements { from_title });
self.scroll = 0;
MenuAction::Consumed
}
Some(Btn::Bestiary) => {
self.open_bestiary(from_title);
MenuAction::Consumed
@@ -954,6 +965,7 @@ impl Menu {
Page::Briefing { mission, ingame } => self.draw_briefing(gfx, mission, ingame),
Page::CustomBriefing => self.draw_custom_briefing(gfx),
Page::Bestiary { tab, sel, level, .. } => self.draw_bestiary(gfx, tab, sel, level),
Page::Achievements { .. } => self.draw_achievements(gfx),
Page::KeyCapture { action, .. } => {
self.draw_settings(gfx, settings, 5);
self.buttons.clear();
@@ -1098,6 +1110,7 @@ impl Menu {
}
items.push((Btn::Standalone, tr("menu.standalone")));
items.push((Btn::Bestiary, tr("menu.bestiary")));
items.push((Btn::Achievements, tr("menu.achievements")));
items.push((Btn::Help, tr("menu.help")));
let w = 26.0 * CW();
let y = ty + GH * big + CH() * 4.0;
@@ -1215,6 +1228,7 @@ impl Menu {
items.extend([
(Btn::Settings, tr("menu.settings")),
(Btn::Bestiary, tr("menu.bestiary")),
(Btn::Achievements, tr("menu.achievements")),
(Btn::Help, tr("menu.help")),
(Btn::MainMenu, tr("net.leave")),
]);
@@ -1225,6 +1239,7 @@ impl Menu {
(Btn::Match, tr("menu.multiplayer")),
(Btn::Settings, tr("menu.settings")),
(Btn::Bestiary, tr("menu.bestiary")),
(Btn::Achievements, tr("menu.achievements")),
(Btn::Help, tr("menu.help")),
(Btn::MainMenu, tr("menu.main")),
]);
@@ -1780,7 +1795,7 @@ impl Menu {
pub fn scrollable(&self) -> bool {
matches!(
self.page,
Some(Page::Saves { .. }) | Some(Page::Campaign) | Some(Page::CampaignEditor) | Some(Page::Briefing { .. }) | Some(Page::CustomBriefing) | Some(Page::Settings { tab: 5, .. }) | Some(Page::Match { .. }) | Some(Page::Account)
Some(Page::Saves { .. }) | Some(Page::Campaign) | Some(Page::CampaignEditor) | Some(Page::Briefing { .. }) | Some(Page::CustomBriefing) | Some(Page::Settings { tab: 5, .. }) | Some(Page::Match { .. }) | Some(Page::Account) | Some(Page::Achievements { .. })
)
}
+70
View File
@@ -0,0 +1,70 @@
// The achievements page: every achievement with its badge (bronze, silver, gold, ... in the
// colour of its tier), name, description and the progress towards it. Hidden ones show "???"
// until they are unlocked.
use super::*;
use crate::achievements::{all, texts, TIERS};
const TIER_COL: [(f32, f32, f32); 5] = [(0.72, 0.45, 0.25), (0.75, 0.78, 0.85), (0.95, 0.78, 0.25), (0.55, 0.9, 1.0), (0.85, 0.5, 1.0)];
impl Menu {
pub(super) fn draw_achievements(&mut self, gfx: &Gfx) {
let sw = sw();
let sh = sh();
let cols = 70.min((sw / CW()) as i32 - 2);
let rows = 40.min((sh / CH()) as i32 - 2);
let x = grid_center(sw, cols as f32 * CW()).max(0.0);
let y = grid_center_y(sh, rows as f32 * CH()).max(0.0);
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &tr("menu.achievements"), FRAME_HI);
let list = all();
let done = list.iter().filter(|a| self.profile.unlocked.contains_key(&a.0)).count();
let head = trf("ach.count", &[done.to_string(), list.len().to_string(), crate::achievements::fmt_count(self.profile.total("minutes"))]);
gfx.text(&head, x + CW() * 3.0, y + CH() * 1.6, S(), GOLD);
let top = y + CH() * 3.5;
let bottom = y + (rows - 4) as f32 * CH();
let row_h = CH() * 3.2;
self.rows_visible = (((bottom - top) / row_h).floor() as usize).max(1);
self.scroll = self.scroll.min(list.len().saturating_sub(self.rows_visible));
let ix = x + CW() * 3.0;
let w = (cols - 6) as f32 * CW();
let maxc = ((w - CW() * 6.0) / (GW * SMALL())) as usize;
for (k, (id, hidden, icon, tier)) in list.iter().enumerate().skip(self.scroll).take(self.rows_visible) {
let ry = top + (k - self.scroll) as f32 * row_h;
let got = self.profile.unlocked.contains_key(id);
// the badge: a frame in the tier's colour around the icon (dark while locked)
let (r, g, b) = TIER_COL[(*tier).min(4)];
let bc = if got { Color::new(r, g, b, 1.0) } else { Color::new(0.25, 0.22, 0.22, 1.0) };
let pic = CH() * 2.6;
draw_rectangle(ix, ry, pic, pic, Color::new(0.08, 0.05, 0.05, 1.0));
draw_rectangle_lines(ix, ry, pic, pic, 3.0, bc);
let tint = if got { WHITE } else { Color::new(0.3, 0.3, 0.3, 1.0) };
draw_texture_ex(&gfx.atlas, ix + 4.0, ry + 4.0, tint, DrawTextureParams { source: Some(crate::gfx::Gfx::cell(*icon)), dest_size: Some(vec2(pic - 8.0, pic - 8.0)), ..Default::default() });
let tx = ix + pic + CW();
let (name, desc) = if *hidden && !got { ("???".to_string(), tr("ach.hidden")) } else { texts(id) };
gfx.text(&name, tx, ry + CH() * 0.1, S(), if got { GOLD } else { TXT });
let d: String = desc.chars().take(maxc).collect();
gfx.text(&d, tx, ry + CH() * 1.3, SMALL(), DIM);
// progress towards a tier
if let Some((cat, t)) = id.split_once('.') {
if !got {
let need = TIERS.iter().find(|x| x.0 == cat).and_then(|x| x.1.get(t.parse::<usize>().unwrap_or(1) - 1)).copied().unwrap_or(1.0);
let have = self.profile.total(cat);
let bar_x = x + (cols - 20) as f32 * CW();
crate::ui::ascii_bar(gfx, bar_x, ry + CH() * 0.1, 10, (have / need).min(1.0) as f32, FRAME_HI, SMALL());
let p = format!("{}/{}", crate::achievements::fmt_count(have.floor()), crate::achievements::fmt_count(need));
gfx.text(&p, bar_x, ry + CH() * 1.3, SMALL(), DIM);
}
}
}
// scrolling and back
let by = y + (rows - 3) as f32 * CH();
let (mx, my) = mouse();
let bw = 14.0 * CW();
let items = [(Btn::Scroll(-1), "▲".to_string()), (Btn::Scroll(1), "▼".to_string()), (Btn::Back, tr("menu.back"))];
for (k, (b, label)) in items.iter().enumerate() {
let r = Rect::new(ix + k as f32 * (bw + CW()), by, bw, CH() * 2.0);
button(gfx, r, label, r.contains(vec2(mx, my)), FRAME);
self.buttons.push((r, *b));
}
}
}
+1
View File
@@ -48,6 +48,7 @@ impl Game {
return;
}
self.gold -= gold;
self.count(crate::stats::SIEGE, 1.0);
let id = self.spawn(kind, false, x as f32 + 0.5, y as f32 + 0.5, 1);
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
e.side = self.me_side;
+1
View File
@@ -263,6 +263,7 @@ impl Game {
}
}
self.mana -= cost;
self.count(crate::stats::SPELLS, 1.0);
}
// running spell effects on a unit (called every frame)
+108
View File
@@ -0,0 +1,108 @@
// Statistics of a game for the master at this computer: counters (heroes slain, tiles dug, wood
// felled, ...) and a sample of gold, creatures and army strength every few seconds for the charts
// at the end. They are local (not part of the shared match state) and feed the achievements.
use crate::game::*;
use serde::{Deserialize, Serialize};
// counters (index into GameStats::c)
pub const HEROES: usize = 0; // heroes slain or captured
pub const BOSSES: usize = 1; // champions of the Order brought down
pub const LOST: usize = 2; // own creatures lost
pub const DUG: usize = 3; // tiles dug out
pub const WOOD: usize = 4; // wood felled
pub const WALLS: usize = 5; // walls raised
pub const ROOMS: usize = 6; // room tiles built
pub const SPELLS: usize = 7; // spells cast (incl. lightning and healing)
pub const SLAPS: usize = 8; // creatures slapped
pub const TRAPS: usize = 9; // traps and doors ordered
pub const TOWERS: usize = 10; // towers built
pub const DROWNED: usize = 11; // heroes who fell into water or lava
pub const BURNT: usize = 12; // bridge tiles that burnt down
pub const SIEGE: usize = 13; // siege engines built
pub const CAPTURED: usize = 14; // heroes knocked out and carried off
pub const GOLD: usize = 15; // gold earned (treasure delivered, bounties, loot)
pub const DRAWBRIDGE: usize = 16; // drawbridges raised
pub const COUNTERS: usize = 17;
// one sample every SAMPLE_EVERY seconds: time, gold, creatures, army
pub const SAMPLE_EVERY: f32 = 10.0;
const MAX_SAMPLES: usize = 720;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct GameStats {
#[serde(default)]
pub c: Vec<f32>,
#[serde(default)]
pub samples: Vec<(f32, f32, u16, f32)>,
#[serde(default)]
pub sample_t: f32,
}
impl GameStats {
pub fn get(&self, k: usize) -> f32 {
self.c.get(k).copied().unwrap_or(0.0)
}
}
impl Game {
// counts for the master at this computer (another master's deeds count on his computer)
pub(crate) fn count(&mut self, k: usize, n: f32) {
if self.me_side != self.local_side || self.demo.is_some() {
return;
}
if self.stats.c.len() < COUNTERS {
self.stats.c.resize(COUNTERS, 0.0);
}
self.stats.c[k] += n;
}
// a hero fell or was captured: the master at this computer counts it in single player and
// co-op (the heroes are everybody's enemy there)
pub(crate) fn count_local(&mut self, k: usize) {
let me = self.me_side;
let local = self.local_side;
self.switch_to(local);
self.count(k, 1.0);
self.switch_to(me);
}
// every few seconds: how the local master stands
pub(crate) fn sample_stats(&mut self, dt: f32) {
if self.demo.is_some() || self.state != 0 {
return;
}
self.stats.sample_t -= dt;
if self.stats.sample_t > 0.0 {
return;
}
self.stats.sample_t = SAMPLE_EVERY;
let s = (self.time, self.gold, self.creature_count() as u16, self.army_value());
self.stats.samples.push(s);
if self.stats.samples.len() > MAX_SAMPLES {
// keep the whole game in view: every second sample goes
let v: Vec<_> = self.stats.samples.iter().copied().step_by(2).collect();
self.stats.samples = v;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_game_keeps_its_statistics() {
let mut g = Game::new(3, 64, 64);
for _ in 0..(35 * 20) {
g.update(0.05);
}
assert!(g.stats.samples.len() >= 3, "{} samples", g.stats.samples.len());
g.count(DUG, 2.0);
assert_eq!(g.stats.get(DUG), 2.0);
// saved with the game
let json = serde_json::to_string(&g.stats).unwrap();
let back: GameStats = serde_json::from_str(&json).unwrap();
assert_eq!(back.get(DUG), 2.0);
}
}
+1
View File
@@ -186,6 +186,7 @@ impl Game {
}
let id = self.next_id;
self.next_id += 1;
self.count(crate::stats::TRAPS, 1.0);
self.traps.push(Trap { id, kind, tile: i, stage: ORDERED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: kind.hp(), locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false, sprung: false });
self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.4, 0xff40d0ff);
self.sfx_ui(Sfx::FlagSet);
+38 -3
View File
@@ -1334,7 +1334,10 @@ impl Ui {
}
lines.extend(wrap(para, tw));
}
let rows = 9 + lines.len() as i32;
// the chart of the game (gold, creatures, army) when there are a few samples
let chart = g.stats.samples.len() >= 3 && sh > CH() * (22.0 + lines.len() as f32);
let chart_rows = if chart { 10 } else { 0 };
let rows = 9 + lines.len() as i32 + chart_rows;
let x = grid_center(sw, cols as f32 * CW());
let y = grid_center_y(sh, rows as f32 * CH()).max(0.0);
let col = if win { GOLD } else { RED };
@@ -1347,6 +1350,38 @@ impl Ui {
}
let st = trf("end.stats", &[mmss(g.time), g.wave.to_string(), (g.army_value() as i32).to_string()]);
gfx.text(&st, grid_center(sw, gfx.measure(&st, S())), y + CH() * (4.5 + lines.len() as f32), S(), DIM);
if chart {
use crate::stats as k;
let cy = y + CH() * (6.0 + lines.len() as f32);
let c = &g.stats;
let line2 = trf("end.stats2", &[(c.get(k::HEROES) as i32).to_string(), (c.get(k::LOST) as i32).to_string(), (c.get(k::DUG) as i32).to_string(), (c.get(k::GOLD) as i32).to_string(), (c.get(k::WOOD) as i32).to_string()]);
gfx.text(&line2, grid_center(sw, gfx.measure(&line2, SMALL())), cy, SMALL(), DIM);
// three lines, each scaled to its own maximum
let (px, py, pw, ph) = (x + CW() * 3.0, cy + CH() * 1.2, (cols - 6) as f32 * CW(), CH() * 6.0);
draw_rectangle(px, py, pw, ph, Color::new(0.0, 0.0, 0.0, 0.35));
draw_rectangle_lines(px, py, pw, ph, 1.0, Color::new(0.4, 0.3, 0.2, 1.0));
let n = c.samples.len();
let t_end = c.samples[n - 1].0.max(1.0);
let series: [(Box<dyn Fn(&(f32, f32, u16, f32)) -> f32>, Color); 3] = [(Box::new(|s| s.1), GOLD), (Box::new(|s| s.2 as f32), GREEN), (Box::new(|s| s.3), RED)];
for (f, col) in series.iter() {
let max = c.samples.iter().map(|s| f(s)).fold(1.0f32, f32::max);
for w in c.samples.windows(2) {
let (a, b) = (&w[0], &w[1]);
let p = |s: &(f32, f32, u16, f32)| (px + pw * (s.0 / t_end), py + ph - ph * 0.92 * (f(s) / max));
let (x0, y0) = p(a);
let (x1, y1) = p(b);
draw_line(x0, y0, x1, y1, 2.0 * K(), *col);
}
}
let legend = format!("{} {} {}", tr("hud.gold"), tr("hud.creatures"), tr("hud.army"));
let lx = px + CW();
gfx.text(&tr("hud.gold"), lx, py + ph + CH() * 0.3, SMALL(), GOLD);
let w1 = gfx.measure(&format!("{} ", tr("hud.gold")), SMALL());
gfx.text(&tr("hud.creatures"), lx + w1, py + ph + CH() * 0.3, SMALL(), GREEN);
let w2 = gfx.measure(&format!("{} {} ", tr("hud.gold"), tr("hud.creatures")), SMALL());
gfx.text(&tr("hud.army"), lx + w2, py + ph + CH() * 0.3, SMALL(), RED);
let _ = legend;
}
// buttons: free game: new game; mission: next / again and back to the campaign
let labels: Vec<(String, u8)> = match (mission, self.coop_end) {
// an editor campaign in co-op: the host goes on for everybody, the others wait or leave
@@ -1359,10 +1394,10 @@ impl Ui {
if let Some((host, true, sent)) = self.coop_end {
if !host || sent {
let t = tr("net.coop_wait_host");
gfx.text(&t, grid_center(sw, gfx.measure(&t, SMALL())), y + CH() * (5.3 + lines.len() as f32), SMALL(), GOLD);
gfx.text(&t, grid_center(sw, gfx.measure(&t, SMALL())), y + CH() * (5.3 + lines.len() as f32 + chart_rows as f32), SMALL(), GOLD);
}
}
let by = y + CH() * (6.0 + lines.len() as f32);
let by = y + CH() * (6.0 + lines.len() as f32 + chart_rows as f32);
let widths: Vec<f32> = labels.iter().map(|(l, _)| gfx.measure(l, S()) + CW() * 4.0).collect();
let total: f32 = widths.iter().sum::<f32>() + CW() * 2.0 * (labels.len() as f32 - 1.0);
let mut bx = grid_center(sw, total);
+1
View File
@@ -119,6 +119,7 @@ impl Game {
tl.bed = 0;
tl.food = 0.0;
self.wall_plans.retain(|p| p.0 != w);
self.count(crate::stats::WALLS, 1.0);
self.fx(8, wx as f32 + 0.5, wy as f32 + 0.5, 0.0, 0.0, 0.5, 0xff4040e0);
self.sfx(Sfx::Reinforce, wx as f32 + 0.5, wy as f32 + 0.5);
false
+1
View File
@@ -79,6 +79,7 @@ impl Game {
let kind = self.map.t[i].kind;
let wood = if kind == Kind::Tree { WOOD_TREE } else { WOOD_MUSHROOM };
self.wood += wood;
self.count(crate::stats::WOOD, wood);
if kind == Kind::Mushroom {
self.woodwork.regrow.push((i, REGROW));
}