diff --git a/README.md b/README.md index a937f5d..e133012 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ of your forge to break a fortress in the forest.
Left: the world up there. Too bright. Right: a drawbridge up, a drawbridge down, and lava that thinks it is a floor.

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