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> <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> </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 **👑 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. (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. 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 ## 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 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 (every 8–12 minutes), free play and multiplayer only, can be switched off in the menu; in
campaigns only through new script blocks. 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.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.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ě. 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.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.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. 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.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.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. 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.boss = Ο Μόσγκριμ δεν υπάρχει πια. Τα βρύα θα ξαναφυτρώσουν. Αυτός όχι.
coop.m20.t1 = Εκείνος ο δράκος στα ανατολικά είναι ο Μόσγκριμ. Ναι, παραμελήθηκε. coop.m20.t1 = Εκείνος ο δράκος στα ανατολικά είναι ο Μόσγκριμ. Ναι, παραμελήθηκε.
coop.m20.t2 = Κάθε πλάσμα που νικάτε είναι ένα λιγότερο που θα πει αυτή την ιστορία λάθος. 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.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.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. 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.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.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. 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.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.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. 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.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.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. 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.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.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. 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.boss = Mossgrim non c'è più. Il muschio ricrescerà. Lui no.
coop.m20.t1 = Quel drago a est è Mossgrim. Sì, si è lasciato andare. 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. 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.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.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. 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.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.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. 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.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.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. 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.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.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. 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.boss = Моссгрима больше нет. Мох отрастёт. Он - нет.
coop.m20.t1 = Этот змий на востоке - Моссгрим. Да, он себя запустил. coop.m20.t1 = Этот змий на востоке - Моссгрим. Да, он себя запустил.
coop.m20.t2 = Каждое побеждённое существо - на одного меньше, кто перескажет эту историю неправильно. 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.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.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. 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.boss = Мосгрима више нема. Маховина ће поново израсти. Он неће.
coop.m20.t1 = Онај змај на истоку је Мосгрим. Да, запустио се. coop.m20.t1 = Онај змај на истоку је Мосгрим. Да, запустио се.
coop.m20.t2 = Свако створење које поразите једно је мање које ће ову причу погрешно испричати. 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.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.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. 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() 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 { async fn get_campaign(State(st): State<AppState>, headers: HeaderMap, Path(id): Path<String>) -> Response {
let u = match st.auth(&headers).await { let u = match st.auth(&headers).await {
Ok(u) => u, Ok(u) => u,
@@ -754,6 +791,7 @@ fn router(state: AppState) -> Router {
.route("/api/users", get(list_users).post(create_user)) .route("/api/users", get(list_users).post(create_user))
.route("/api/users/{name}", delete(delete_user)) .route("/api/users/{name}", delete(delete_user))
.route("/api/netplay/ws", get(netplay::netplay_ws)) .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", get(list).post(create))
.route("/api/saves/{id}", get(get_save).put(overwrite).delete(delete_save)) .route("/api/saves/{id}", get(get_save).put(overwrite).delete(delete_save))
.route("/api/campaigns", get(list_campaigns).post(create_campaign).layer(campaign_limit())) .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; return;
} }
self.ents[j].boss = 0; self.ents[j].boss = 0;
self.count_local(crate::stats::BOSSES);
let got = self.add_gold(BOSS_GOLD); let got = self.add_gold(BOSS_GOLD);
self.rune_free += 1; self.rune_free += 1;
let (x, y) = (self.ents[j].x, self.ents[j].y); let (x, y) = (self.ents[j].x, self.ents[j].y);
+2
View File
@@ -128,6 +128,7 @@ impl Game {
} }
if t >= BURN_TIME { if t >= BURN_TIME {
self.bridgework.burning.swap_remove(k); self.bridgework.burning.swap_remove(k);
self.count_local(crate::stats::BURNT);
self.bridge_falls(i); self.bridge_falls(i);
continue; continue;
} }
@@ -262,6 +263,7 @@ impl Game {
t.res = (t.res & DB_LAVA) | state; t.res = (t.res & DB_LAVA) | state;
} }
if raise { if raise {
self.count(crate::stats::DRAWBRIDGE, 1.0);
// whoever stands on it jumps to the bank (or falls) // whoever stands on it jumps to the bank (or falls)
for &g in &group { for &g in &group {
self.ground_lost(g); self.ground_lost(g);
+20
View File
@@ -80,6 +80,8 @@ enum Req {
CampaignPublish, CampaignPublish,
PublicList, PublicList,
PublicLoad, PublicLoad,
ProfileLoad,
ProfileSave,
} }
// the logged-in account // the logged-in account
@@ -124,6 +126,7 @@ pub struct Cloud {
pub public_listed: bool, pub public_listed: bool,
pub campaign_error: Option<&'static str>, // language key of the last failed campaign request pub campaign_error: Option<&'static str>, // language key of the last failed campaign request
loaded_campaign: Option<String>, loaded_campaign: Option<String>,
pub profile_loaded: Option<String>, // the profile from the server, waiting to be merged
saved_campaign: Option<CampaignInfo>, saved_campaign: Option<CampaignInfo>,
pending: Option<(u32, Req)>, pending: Option<(u32, Req)>,
save_name: String, save_name: String,
@@ -176,6 +179,7 @@ impl Cloud {
public_listed: false, public_listed: false,
campaign_error: None, campaign_error: None,
loaded_campaign: None, loaded_campaign: None,
profile_loaded: None,
saved_campaign: None, saved_campaign: None,
pending: Some((id, Req::Health)), pending: Some((id, Req::Health)),
save_name: String::new(), save_name: String::new(),
@@ -330,6 +334,13 @@ impl Cloud {
CloudEvent::Error(error_key(res.status)) 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 => { Req::CreateUser | Req::DeleteUser => {
if ok { if ok {
self.load_users(); self.load_users();
@@ -470,6 +481,15 @@ impl Cloud {
} }
// JSON text of a campaign loaded by load_campaign() or load_public_campaign() // 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> { pub fn take_loaded_campaign(&mut self) -> Option<String> {
self.loaded_campaign.take() self.loaded_campaign.take()
} }
+30
View File
@@ -304,6 +304,11 @@ pub struct Ent {
pub disp_y: f32, 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 { fn yes() -> bool {
true true
} }
@@ -596,6 +601,8 @@ pub struct Game {
#[serde(default)] #[serde(default)]
pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges
#[serde(default)] #[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) pub wall_plans: Vec<(usize, u8)>, // walls to raise: tile, master side (see walls.rs)
#[serde(default)] #[serde(default)]
pub wood: f32, // the second resource (see wood.rs) pub wood: f32, // the second resource (see wood.rs)
@@ -920,6 +927,7 @@ impl Game {
vrng: crate::particles::vrng(), vrng: crate::particles::vrng(),
dwarf_caves: Vec::new(), dwarf_caves: Vec::new(),
bridgework: Default::default(), bridgework: Default::default(),
stats: Default::default(),
wall_plans: Vec::new(), wall_plans: Vec::new(),
wood: crate::wood::WOOD_START, wood: crate::wood::WOOD_START,
woodwork: Default::default(), woodwork: Default::default(),
@@ -1137,6 +1145,7 @@ impl Game {
self.gold += acc; self.gold += acc;
if acc > 0.0 { if acc > 0.0 {
self.incoming.push((res, acc, at)); self.incoming.push((res, acc, at));
self.count(crate::stats::GOLD, acc);
} }
acc acc
} }
@@ -1384,6 +1393,7 @@ impl Game {
} }
let local = self.local_side; let local = self.local_side;
self.switch_to(local); self.switch_to(local);
self.sample_stats(STEP);
} }
// ---------------------------------------------------------------- masters // ---------------------------------------------------------------- masters
@@ -2494,6 +2504,7 @@ impl Game {
t.hp = 0.0; t.hp = 0.0;
t.gold = 0; t.gold = 0;
let (x, y) = xy(i); 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.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); self.sfx(Sfx::DigDone, x as f32 + 0.5, y as f32 + 0.5);
} }
@@ -2759,6 +2770,8 @@ impl Game {
e.patrol.clear(); e.patrol.clear();
let b = st(kind).bounty as f32; let b = st(kind).bounty as f32;
let got = self.add_gold(b); 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.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff);
self.sfx(Sfx::Capture, x, y); self.sfx(Sfx::Capture, x, y);
self.msg("msg.hero_captured", vec![format!("@{}", st(kind).name), (got as i32).to_string()]); 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); self.bury(x, y);
} }
let s = st(kind); 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 { if hero {
let b = s.bounty as f32; let b = s.bounty as f32;
let got = self.add_gold(b); let got = self.add_gold(b);
@@ -2866,6 +2888,9 @@ impl Game {
if !e.alive || e.held || e.tile() != i || flies(e.kind) { if !e.alive || e.held || e.tile() != i || flies(e.kind) {
continue; continue;
} }
if e.hero {
self.count_local(crate::stats::DROWNED);
}
if lava { if lava {
self.burn(j); self.burn(j);
continue; continue;
@@ -4778,6 +4803,7 @@ impl Game {
if built == 0 && !broke { if built == 0 && !broke {
self.hint(7, "hint.build_where"); self.hint(7, "hint.build_where");
} }
self.count(crate::stats::ROOMS, built as f32);
self.recount(); 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 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(); let (px, py) = tower.pos();
self.towers.push(tower); self.towers.push(tower);
self.count(crate::stats::TOWERS, 1.0);
// units standing on the tile step aside // units standing on the tile step aside
for e in self.ents.iter_mut() { for e in self.ents.iter_mut() {
if e.alive && e.tile() == i { 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.fx(2, x, y, 0.0, 0.0, 0.3, 0xffffffff);
self.sfx(Sfx::Slap, x, y); self.sfx(Sfx::Slap, x, y);
self.script_event(crate::mscript::ScriptEvent::Slap); self.script_event(crate::mscript::ScriptEvent::Slap);
self.count(crate::stats::SLAPS, 1.0);
return 1; return 1;
} }
if self.ents[j].captive != 0 { if self.ents[j].captive != 0 {
@@ -5498,6 +5526,7 @@ impl Game {
return; return;
} }
self.mana -= COST; self.mana -= COST;
self.count(crate::stats::SPELLS, 1.0);
self.sfx(Sfx::Lightning, wx, wy); self.sfx(Sfx::Lightning, wx, wy);
self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa); self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa);
self.ignite_at(wx, wy); self.ignite_at(wx, wy);
@@ -5524,6 +5553,7 @@ impl Game {
return; return;
} }
self.mana -= COST; self.mana -= COST;
self.count(crate::stats::SPELLS, 1.0);
self.sfx(Sfx::Heal, wx, wy); self.sfx(Sfx::Heal, wx, wy);
let mut fxs = Vec::new(); let mut fxs = Vec::new();
for e in self.ents.iter_mut() { for e in self.ents.iter_mut() {
+49
View File
@@ -1,5 +1,6 @@
// DungeonCraft – dungeon building & defence in the browser (Rust + macroquad, WASM). // DungeonCraft – dungeon building & defence in the browser (Rust + macroquad, WASM).
mod achievements;
mod audio; mod audio;
mod campaign; mod campaign;
mod campaign_doc; mod campaign_doc;
@@ -41,6 +42,7 @@ mod traps;
mod sound; mod sound;
mod surface; mod surface;
mod spells; mod spells;
mod stats;
mod touch; mod touch;
mod ui; mod ui;
mod voice; mod voice;
@@ -847,6 +849,12 @@ async fn main() {
let mut r3d = R3d::new(); let mut r3d = R3d::new();
let mut sounds = SoundBank::new(); let mut sounds = SoundBank::new();
let mut music = MusicDirector::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 ui = Ui::new();
let mut menu = Menu::new(); let mut menu = Menu::new();
let mut cloud = Cloud::new(); // savegame service; files + browser autosave when unreachable let mut cloud = Cloud::new(); // savegame service; files + browser autosave when unreachable
@@ -992,6 +1000,7 @@ async fn main() {
} }
} }
CloudEvent::LoggedIn => { CloudEvent::LoggedIn => {
want_profile = true;
if menu.page == Some(Page::Login) { if menu.page == Some(Page::Login) {
menu.page = Some(Page::Title); menu.page = Some(Page::Title);
} }
@@ -1939,8 +1948,47 @@ app.pending_briefing = briefing_of(&campaign, i);
| Some(Page::Account) | Some(Page::Account)
| Some(Page::Bestiary { from_title: true, .. }) | Some(Page::Bestiary { from_title: true, .. })
| Some(Page::Match { 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)); 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(..) { for m in app.game.msgs.drain(..) {
if app.settings.voice && voice::speaks(m.key) { if app.settings.voice && voice::speaks(m.key) {
voice.say(&i18n::trf(m.key, &m.args), now, 12); 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::Account)
| Some(Page::Bestiary { from_title: true, .. }) | Some(Page::Bestiary { from_title: true, .. })
| Some(Page::Match { from_title: true }) | Some(Page::Match { from_title: true })
| Some(Page::Achievements { from_title: true })
); );
if !title_screen { if !title_screen {
let info = if over_ui || (touch && inp.inspect.is_none()) { 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 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::*; use macroquad::prelude::*;
mod achievements;
mod bestiary; mod bestiary;
pub mod flags; pub mod flags;
@@ -33,6 +34,7 @@ pub enum Page {
Account, // password, logout, users (admin) Account, // password, logout, users (admin)
Match { from_title: bool }, // multiplayer lobby and match status Match { from_title: bool }, // multiplayer lobby and match status
Bestiary { tab: usize, sel: usize, level: u32, from_title: bool }, // units, buildings, ranks 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"]; 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, MatchLeave,
MatchChat, MatchChat,
Bestiary, Bestiary,
Achievements,
BestTab(usize), BestTab(usize),
BestSel(i32), BestSel(i32),
BestPick(usize), BestPick(usize),
@@ -441,6 +444,7 @@ pub struct Menu {
confirm: Option<Btn>, // overwrite / delete waiting for a second click confirm: Option<Btn>, // overwrite / delete waiting for a second click
scroll: usize, scroll: usize,
rows_visible: usize, rows_visible: usize,
pub profile: crate::achievements::Profile, // shown on the achievements page
buttons: Vec<(Rect, Btn)>, buttons: Vec<(Rect, Btn)>,
lang_open: Option<Rect>, // the language dropdown is open below/above this field lang_open: Option<Rect>, // the language dropdown is open below/above this field
lang_scroll: usize, // first language shown in the open dropdown lang_scroll: usize, // first language shown in the open dropdown
@@ -505,6 +509,7 @@ impl Menu {
lang_rows: 0, lang_rows: 0,
cur_lang: String::new(), cur_lang: String::new(),
rows_visible: 1, rows_visible: 1,
profile: Default::default(),
buttons: Vec::new(), 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::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: true, .. }) => Some(Page::Title),
Some(Page::Bestiary { from_title: false, .. }) => Some(Page::Pause), 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: true, .. }) => Some(Page::Pause),
Some(Page::Briefing { ingame: false, .. }) => Some(Page::Campaign), Some(Page::Briefing { ingame: false, .. }) => Some(Page::Campaign),
Some(Page::CustomBriefing) => None, Some(Page::CustomBriefing) => None,
@@ -916,6 +922,11 @@ impl Menu {
} }
MenuAction::Consumed MenuAction::Consumed
} }
Some(Btn::Achievements) => {
self.page = Some(Page::Achievements { from_title });
self.scroll = 0;
MenuAction::Consumed
}
Some(Btn::Bestiary) => { Some(Btn::Bestiary) => {
self.open_bestiary(from_title); self.open_bestiary(from_title);
MenuAction::Consumed MenuAction::Consumed
@@ -954,6 +965,7 @@ impl Menu {
Page::Briefing { mission, ingame } => self.draw_briefing(gfx, mission, ingame), Page::Briefing { mission, ingame } => self.draw_briefing(gfx, mission, ingame),
Page::CustomBriefing => self.draw_custom_briefing(gfx), Page::CustomBriefing => self.draw_custom_briefing(gfx),
Page::Bestiary { tab, sel, level, .. } => self.draw_bestiary(gfx, tab, sel, level), Page::Bestiary { tab, sel, level, .. } => self.draw_bestiary(gfx, tab, sel, level),
Page::Achievements { .. } => self.draw_achievements(gfx),
Page::KeyCapture { action, .. } => { Page::KeyCapture { action, .. } => {
self.draw_settings(gfx, settings, 5); self.draw_settings(gfx, settings, 5);
self.buttons.clear(); self.buttons.clear();
@@ -1098,6 +1110,7 @@ impl Menu {
} }
items.push((Btn::Standalone, tr("menu.standalone"))); items.push((Btn::Standalone, tr("menu.standalone")));
items.push((Btn::Bestiary, tr("menu.bestiary"))); items.push((Btn::Bestiary, tr("menu.bestiary")));
items.push((Btn::Achievements, tr("menu.achievements")));
items.push((Btn::Help, tr("menu.help"))); items.push((Btn::Help, tr("menu.help")));
let w = 26.0 * CW(); let w = 26.0 * CW();
let y = ty + GH * big + CH() * 4.0; let y = ty + GH * big + CH() * 4.0;
@@ -1215,6 +1228,7 @@ impl Menu {
items.extend([ items.extend([
(Btn::Settings, tr("menu.settings")), (Btn::Settings, tr("menu.settings")),
(Btn::Bestiary, tr("menu.bestiary")), (Btn::Bestiary, tr("menu.bestiary")),
(Btn::Achievements, tr("menu.achievements")),
(Btn::Help, tr("menu.help")), (Btn::Help, tr("menu.help")),
(Btn::MainMenu, tr("net.leave")), (Btn::MainMenu, tr("net.leave")),
]); ]);
@@ -1225,6 +1239,7 @@ impl Menu {
(Btn::Match, tr("menu.multiplayer")), (Btn::Match, tr("menu.multiplayer")),
(Btn::Settings, tr("menu.settings")), (Btn::Settings, tr("menu.settings")),
(Btn::Bestiary, tr("menu.bestiary")), (Btn::Bestiary, tr("menu.bestiary")),
(Btn::Achievements, tr("menu.achievements")),
(Btn::Help, tr("menu.help")), (Btn::Help, tr("menu.help")),
(Btn::MainMenu, tr("menu.main")), (Btn::MainMenu, tr("menu.main")),
]); ]);
@@ -1780,7 +1795,7 @@ impl Menu {
pub fn scrollable(&self) -> bool { pub fn scrollable(&self) -> bool {
matches!( matches!(
self.page, 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; return;
} }
self.gold -= gold; 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); 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) { if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
e.side = self.me_side; e.side = self.me_side;
+1
View File
@@ -263,6 +263,7 @@ impl Game {
} }
} }
self.mana -= cost; self.mana -= cost;
self.count(crate::stats::SPELLS, 1.0);
} }
// running spell effects on a unit (called every frame) // 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; let id = self.next_id;
self.next_id += 1; 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.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.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.4, 0xff40d0ff);
self.sfx_ui(Sfx::FlagSet); self.sfx_ui(Sfx::FlagSet);
+38 -3
View File
@@ -1334,7 +1334,10 @@ impl Ui {
} }
lines.extend(wrap(para, tw)); 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 x = grid_center(sw, cols as f32 * CW());
let y = grid_center_y(sh, rows as f32 * CH()).max(0.0); let y = grid_center_y(sh, rows as f32 * CH()).max(0.0);
let col = if win { GOLD } else { RED }; 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()]); 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); 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 // buttons: free game: new game; mission: next / again and back to the campaign
let labels: Vec<(String, u8)> = match (mission, self.coop_end) { 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 // 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 let Some((host, true, sent)) = self.coop_end {
if !host || sent { if !host || sent {
let t = tr("net.coop_wait_host"); 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 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 total: f32 = widths.iter().sum::<f32>() + CW() * 2.0 * (labels.len() as f32 - 1.0);
let mut bx = grid_center(sw, total); let mut bx = grid_center(sw, total);
+1
View File
@@ -119,6 +119,7 @@ impl Game {
tl.bed = 0; tl.bed = 0;
tl.food = 0.0; tl.food = 0.0;
self.wall_plans.retain(|p| p.0 != w); 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.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); self.sfx(Sfx::Reinforce, wx as f32 + 0.5, wy as f32 + 0.5);
false false
+1
View File
@@ -79,6 +79,7 @@ impl Game {
let kind = self.map.t[i].kind; let kind = self.map.t[i].kind;
let wood = if kind == Kind::Tree { WOOD_TREE } else { WOOD_MUSHROOM }; let wood = if kind == Kind::Tree { WOOD_TREE } else { WOOD_MUSHROOM };
self.wood += wood; self.wood += wood;
self.count(crate::stats::WOOD, wood);
if kind == Kind::Mushroom { if kind == Kind::Mushroom {
self.woodwork.regrow.push((i, REGROW)); self.woodwork.regrow.push((i, REGROW));
} }