From c6d82027dd7b3e2ee09915ab9f28817089c752c6 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Thu, 5 Nov 2026 23:58:28 +0100 Subject: [PATCH] Random events: earthquakes, overflowing rivers, new gold veins (free games and versus, can be switched off) Co-Authored-By: Claude Opus 5.5 --- docs/ROADMAP.md | 5 +- lang/cs.lang | 5 ++ lang/da.lang | 5 ++ lang/de.lang | 5 ++ lang/el.lang | 5 ++ lang/en.lang | 5 ++ lang/es.lang | 5 ++ lang/fi.lang | 5 ++ lang/hr.lang | 5 ++ lang/hu.lang | 5 ++ lang/it.lang | 5 ++ lang/nl.lang | 5 ++ lang/no.lang | 5 ++ lang/pt.lang | 5 ++ lang/ro.lang | 5 ++ lang/ru.lang | 5 ++ lang/sk.lang | 5 ++ lang/sr.lang | 5 ++ lang/sv.lang | 5 ++ server/src/netplay.rs | 6 +- src/events.rs | 190 ++++++++++++++++++++++++++++++++++++++++++ src/game.rs | 4 + src/lobby.rs | 13 +-- src/main.rs | 7 +- src/menu.rs | 6 +- src/settings.rs | 2 + 26 files changed, 313 insertions(+), 10 deletions(-) create mode 100644 src/events.rs diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f3fc254..b6fcf3e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -73,7 +73,10 @@ bosses (`src/bosses.rs`: four champions of the Order lead every fifth wave - Sir 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). +login; the menu page *Achievements*), room levels 2 and 3 (researched in the library once every tool is known: ++25% per level, brighter, gold trim) and random events (`src/events.rs`: earthquake, lava or water over the +banks, a new gold vein; every 8-12 minutes in free games and versus matches, switch in the game settings; the +host's choice goes to every client with `"events"` in the create message). 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 diff --git a/lang/cs.lang b/lang/cs.lang index 0fa3752..e1084ce 100644 --- a/lang/cs.lang +++ b/lang/cs.lang @@ -246,6 +246,7 @@ set.lights = Zdroje světla set.ui_scale = Velikost rozhraní set.game_speed = Rychlost hry set.hints = Rady +set.events = Náhodné události set.keys_hint = Klikni na akci a stiskni klávesu. Už použité klávesy se prohodí. set.press_key = Stiskni klávesu pro: {0} set.esc_cancel = Esc ruší @@ -340,6 +341,10 @@ info.tortured = mučen info.prison = Zajatci: {0}/{1} msg.wave_warn_dir = Zvědové hlásí: vlna hrdinů {0} dorazí k bráně za 30 sekund! Chtějí vtrhnout ze směru: {1}. +msg.event_quake = Zemětřesení! Tunely na {0} se zřítily. +msg.event_lava = Láva se vylévá z břehů na {0}! +msg.event_flood = Povodeň! Řeka se vylévá z břehů na {0}. +msg.event_vein = Skřítci hlásí novou zlatou žílu na {0}! msg.wave_warn_two = Zvědové hlásí: vlna hrdinů {0} dorazí k bráně za 30 sekund! Rozdělí se a vtrhnou ze směrů: {1} a {2}. dir.n = sever dir.ne = severovýchod diff --git a/lang/da.lang b/lang/da.lang index 6dbafd8..637c833 100644 --- a/lang/da.lang +++ b/lang/da.lang @@ -246,6 +246,7 @@ set.lights = Lyskilder set.ui_scale = Brugerflade set.game_speed = Spilhastighed set.hints = Tips +set.events = Tilfældige hændelser set.keys_hint = Klik på en handling og tryk på en tast. Taster der allerede er i brug, byttes om. set.press_key = Tryk på en tast for: {0} set.esc_cancel = Esc annullerer @@ -339,6 +340,10 @@ info.carried = bæres til torturkammeret info.tortured = bliver tortureret info.prison = Fanger: {0}/{1} msg.wave_warn_dir = Spejderne melder: heltebølge {0} når porten om 30 sekunder! De vil bryde ind fra {1}. +msg.event_quake = Jordskælv! Tunneller mod {0} er styrtet sammen. +msg.event_lava = Lavaen går over sine bredder mod {0}! +msg.event_flood = Oversvømmelse! Floden går over sine bredder mod {0}. +msg.event_vein = Imps'ene melder om en ny guldåre mod {0}! msg.wave_warn_two = Spejderne melder: heltebølge {0} når porten om 30 sekunder! De deler sig og bryder ind fra {1} og {2}. dir.n = nord dir.ne = nordøst diff --git a/lang/de.lang b/lang/de.lang index ce1231b..d8132d4 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -245,6 +245,7 @@ set.lights = Lichtquellen set.ui_scale = Oberflächengröße set.game_speed = Spieltempo set.hints = Hinweise +set.events = Zufallsereignisse set.keys_hint = Aktion anklicken und Taste drücken. Bereits belegte Tasten werden getauscht. set.press_key = Taste drücken für: {0} set.esc_cancel = Esc bricht ab @@ -339,6 +340,10 @@ info.tortured = wird gefoltert info.prison = Gefangene: {0}/{1} msg.wave_warn_dir = Späher melden: Welle {0} erreicht in 30 Sekunden das Tor! Die Helden wollen von {1} einfallen. +msg.event_quake = Erdbeben! Im {0} sind Gänge eingestürzt. +msg.event_lava = Im {0} tritt die Lava über die Ufer! +msg.event_flood = Hochwasser! Im {0} tritt der Fluss über die Ufer. +msg.event_vein = Die Imps melden eine neue Goldader im {0}! msg.wave_warn_two = Späher melden: Welle {0} erreicht in 30 Sekunden das Tor! Die Helden teilen sich auf und fallen von {1} und von {2} ein. dir.n = Norden dir.ne = Nordosten diff --git a/lang/el.lang b/lang/el.lang index 71e8bd8..24596d5 100644 --- a/lang/el.lang +++ b/lang/el.lang @@ -246,6 +246,7 @@ set.lights = Πηγές φωτός set.ui_scale = Μέγεθος διεπαφής set.game_speed = Ταχύτητα παιχνιδιού set.hints = Υποδείξεις +set.events = Τυχαία γεγονότα set.keys_hint = Κάνε κλικ σε μια ενέργεια και πάτησε ένα πλήκτρο. Πλήκτρα που ήδη χρησιμοποιούνται ανταλλάσσονται. set.press_key = Πάτησε πλήκτρο για: {0} set.esc_cancel = Esc ακυρώνει @@ -340,6 +341,10 @@ info.tortured = βασανίζεται info.prison = Αιχμάλωτοι: {0}/{1} msg.wave_warn_dir = Οι ανιχνευτές αναφέρουν: το κύμα ηρώων {0} φτάνει στην πύλη σε 30 δευτερόλεπτα! Σχεδιάζουν να εισβάλουν από: {1}. +msg.event_quake = Σεισμός! Σήραγγες στα {0} κατέρρευσαν. +msg.event_lava = Η λάβα ξεχειλίζει στα {0}! +msg.event_flood = Πλημμύρα! Το ποτάμι ξεχειλίζει στα {0}. +msg.event_vein = Τα διαβολάκια βρήκαν νέα φλέβα χρυσού στα {0}! msg.wave_warn_two = Οι ανιχνευτές αναφέρουν: το κύμα ηρώων {0} φτάνει στην πύλη σε 30 δευτερόλεπτα! Χωρίζονται και εισβάλλουν από: {1} και {2}. dir.n = βορρά dir.ne = βορειοανατολικά diff --git a/lang/en.lang b/lang/en.lang index ca42a67..f965ee1 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -246,6 +246,7 @@ set.lights = Light sources set.ui_scale = Interface size set.game_speed = Game speed set.hints = Hints +set.events = Random events set.keys_hint = Click an action and press a key. Keys already in use are swapped. set.press_key = Press a key for: {0} set.esc_cancel = Esc cancels @@ -340,6 +341,10 @@ info.tortured = being tortured info.prison = Prisoners: {0}/{1} msg.wave_warn_dir = Scouts report: hero wave {0} reaches the gate in 30 seconds! They plan to break in from the {1}. +msg.event_quake = Earthquake! Tunnels in the {0} have caved in. +msg.event_lava = The lava overflows its banks in the {0}! +msg.event_flood = Flood! The river overflows its banks in the {0}. +msg.event_vein = The imps report a new gold vein in the {0}! msg.wave_warn_two = Scouts report: hero wave {0} reaches the gate in 30 seconds! They split up and break in from the {1} and the {2}. dir.n = north dir.ne = north-east diff --git a/lang/es.lang b/lang/es.lang index f844fb9..8570212 100644 --- a/lang/es.lang +++ b/lang/es.lang @@ -246,6 +246,7 @@ set.lights = Fuentes de luz set.ui_scale = Tamaño de interfaz set.game_speed = Velocidad de juego set.hints = Consejos +set.events = Sucesos aleatorios set.keys_hint = Haz clic en una acción y pulsa una tecla. Las teclas ya usadas se intercambian. set.press_key = Pulsa una tecla para: {0} set.esc_cancel = Esc cancela @@ -340,6 +341,10 @@ info.tortured = siendo torturado info.prison = Prisioneros: {0}/{1} msg.wave_warn_dir = Los exploradores informan: ¡la oleada de héroes {0} llega a la puerta en 30 segundos! Planean entrar por el {1}. +msg.event_quake = ¡Terremoto! Se han derrumbado túneles en el {0}. +msg.event_lava = ¡La lava se desborda en el {0}! +msg.event_flood = ¡Inundación! El río se desborda en el {0}. +msg.event_vein = ¡Los diablillos informan de una nueva veta de oro en el {0}! msg.wave_warn_two = Los exploradores informan: ¡la oleada de héroes {0} llega a la puerta en 30 segundos! Se dividen y entran por el {1} y por el {2}. dir.n = norte dir.ne = nordeste diff --git a/lang/fi.lang b/lang/fi.lang index 811e994..d710750 100644 --- a/lang/fi.lang +++ b/lang/fi.lang @@ -246,6 +246,7 @@ set.lights = Valonlähteet set.ui_scale = UI-koko set.game_speed = Pelinopeus set.hints = Vinkit +set.events = Satunnaiset tapahtumat set.keys_hint = Klikkaa toimintoa ja paina näppäintä. Jo käytössä olevat näppäimet vaihdetaan keskenään. set.press_key = Paina näppäintä toiminnolle: {0} set.esc_cancel = Esc peruu @@ -340,6 +341,10 @@ info.tortured = kidutetaan info.prison = Vangit: {0}/{1} msg.wave_warn_dir = Tiedustelijat ilmoittavat: sankariaalto {0} saapuu portille 30 sekunnissa! Ne aikovat murtautua sisään suunnasta: {1}. +msg.event_quake = Maanjäristys! Tunneleita on sortunut suunnassa {0}. +msg.event_lava = Laava tulvii yli äyräidensä suunnassa {0}! +msg.event_flood = Tulva! Joki tulvii yli äyräidensä suunnassa {0}. +msg.event_vein = Pirut ilmoittavat uudesta kultasuonesta suunnassa {0}! msg.wave_warn_two = Tiedustelijat ilmoittavat: sankariaalto {0} saapuu portille 30 sekunnissa! Ne jakautuvat ja murtautuvat sisään suunnista: {1} ja {2}. dir.n = pohjoinen dir.ne = koillinen diff --git a/lang/hr.lang b/lang/hr.lang index 3b22bb4..4c864fe 100644 --- a/lang/hr.lang +++ b/lang/hr.lang @@ -246,6 +246,7 @@ set.lights = Izvori svjetla set.ui_scale = Veličina sučelja set.game_speed = Brzina igre set.hints = Savjeti +set.events = Slučajni događaji set.keys_hint = Klikni na radnju i pritisni tipku. Već zauzete tipke se zamijene. set.press_key = Pritisni tipku za: {0} set.esc_cancel = Esc poništava @@ -340,6 +341,10 @@ info.tortured = muče ga info.prison = Zarobljenici: {0}/{1} msg.wave_warn_dir = Izviđači javljaju: val junaka {0} stiže do vrata za 30 sekundi! Planiraju provaliti sa strane: {1}. +msg.event_quake = Potres! Tuneli na {0} su se urušili. +msg.event_lava = Lava se izlijeva iz korita na {0}! +msg.event_flood = Poplava! Rijeka se izlijeva na {0}. +msg.event_vein = Vražići javljaju novu zlatnu žilu na {0}! msg.wave_warn_two = Izviđači javljaju: val junaka {0} stiže do vrata za 30 sekundi! Dijele se i provaljuju sa strana: {1} i {2}. dir.n = sjever dir.ne = sjeveroistok diff --git a/lang/hu.lang b/lang/hu.lang index 596e816..f4046e2 100644 --- a/lang/hu.lang +++ b/lang/hu.lang @@ -246,6 +246,7 @@ set.lights = Fényforrások set.ui_scale = Felület mérete set.game_speed = Játéksebesség set.hints = Tippek +set.events = Véletlen események set.keys_hint = Kattints egy műveletre, és nyomj meg egy billentyűt. A már használt billentyűk felcserélődnek. set.press_key = Nyomj egy billentyűt ehhez: {0} set.esc_cancel = Esc: mégse @@ -340,6 +341,10 @@ info.tortured = kínozzák info.prison = Foglyok: {0}/{1} msg.wave_warn_dir = A felderítők jelentik: a(z) {0}. hőshullám 30 másodperc múlva eléri a kaput! Innen akarnak betörni: {1}. +msg.event_quake = Földrengés! Alagutak omlottak be: {0}. +msg.event_lava = Kilép medréből a láva: {0}! +msg.event_flood = Árvíz! Kilép medréből a folyó: {0}. +msg.event_vein = A manók új aranyérre bukkantak: {0}! msg.wave_warn_two = A felderítők jelentik: a(z) {0}. hőshullám 30 másodperc múlva eléri a kaput! Kettéválnak, és két irányból törnek be: {1} és {2}. dir.n = észak dir.ne = északkelet diff --git a/lang/it.lang b/lang/it.lang index 3f84c84..0494295 100644 --- a/lang/it.lang +++ b/lang/it.lang @@ -246,6 +246,7 @@ set.lights = Fonti di luce set.ui_scale = Dimensione interfaccia set.game_speed = Velocità di gioco set.hints = Suggerimenti +set.events = Eventi casuali set.keys_hint = Clicca un'azione e premi un tasto. I tasti già in uso vengono scambiati. set.press_key = Premi un tasto per: {0} set.esc_cancel = Esc annulla @@ -340,6 +341,10 @@ info.tortured = sotto tortura info.prison = Prigionieri: {0}/{1} msg.wave_warn_dir = Gli esploratori riferiscono: l'ondata di eroi {0} raggiunge la porta tra 30 secondi! Vogliono irrompere da {1}. +msg.event_quake = Terremoto! Sono crollati dei tunnel a {0}. +msg.event_lava = La lava straripa a {0}! +msg.event_flood = Inondazione! Il fiume straripa a {0}. +msg.event_vein = I diavoletti segnalano una nuova vena d'oro a {0}! msg.wave_warn_two = Gli esploratori riferiscono: l'ondata di eroi {0} raggiunge la porta tra 30 secondi! Si dividono e irrompono da {1} e da {2}. dir.n = nord dir.ne = nord-est diff --git a/lang/nl.lang b/lang/nl.lang index 9410430..8860042 100644 --- a/lang/nl.lang +++ b/lang/nl.lang @@ -246,6 +246,7 @@ set.lights = Lichtbronnen set.ui_scale = Interfacegrootte set.game_speed = Spelsnelheid set.hints = Tips +set.events = Willekeurige gebeurtenissen set.keys_hint = Klik op een actie en druk op een toets. Toetsen die al in gebruik zijn, worden verwisseld. set.press_key = Druk op een toets voor: {0} set.esc_cancel = Esc annuleert @@ -340,6 +341,10 @@ info.tortured = wordt gemarteld info.prison = Gevangenen: {0}/{1} msg.wave_warn_dir = Verkenners melden: heldengolf {0} bereikt de poort over 30 seconden! Ze willen vanuit het {1} binnenvallen. +msg.event_quake = Aardbeving! Tunnels in het {0} zijn ingestort. +msg.event_lava = De lava treedt buiten haar oevers in het {0}! +msg.event_flood = Overstroming! De rivier treedt buiten haar oevers in het {0}. +msg.event_vein = De duiveltjes melden een nieuwe goudader in het {0}! msg.wave_warn_two = Verkenners melden: heldengolf {0} bereikt de poort over 30 seconden! Ze splitsen zich en vallen binnen vanuit het {1} en het {2}. dir.n = noorden dir.ne = noordoosten diff --git a/lang/no.lang b/lang/no.lang index 3a4e9fa..5bb22ef 100644 --- a/lang/no.lang +++ b/lang/no.lang @@ -246,6 +246,7 @@ set.lights = Lyskilder set.ui_scale = Grensesnitt set.game_speed = Spillfart set.hints = Tips +set.events = Tilfeldige hendelser set.keys_hint = Klikk på en handling og trykk på en tast. Taster som allerede er i bruk, byttes om. set.press_key = Trykk på en tast for: {0} set.esc_cancel = Esc avbryter @@ -339,6 +340,10 @@ info.carried = bæres til torturkammeret info.tortured = blir torturert info.prison = Fanger: {0}/{1} msg.wave_warn_dir = Speiderne melder: heltebølge {0} når porten om 30 sekunder! De vil bryte inn fra {1}. +msg.event_quake = Jordskjelv! Tunneler mot {0} har rast sammen. +msg.event_lava = Lavaen går over sine bredder mot {0}! +msg.event_flood = Flom! Elva går over sine bredder mot {0}. +msg.event_vein = Impene melder om en ny gullåre mot {0}! msg.wave_warn_two = Speiderne melder: heltebølge {0} når porten om 30 sekunder! De deler seg og bryter inn fra {1} og {2}. dir.n = nord dir.ne = nordøst diff --git a/lang/pt.lang b/lang/pt.lang index fba953f..a3bdfa9 100644 --- a/lang/pt.lang +++ b/lang/pt.lang @@ -246,6 +246,7 @@ set.lights = Fontes de luz set.ui_scale = Tamanho da interface set.game_speed = Velocidade do jogo set.hints = Dicas +set.events = Eventos aleatórios set.keys_hint = Clica numa ação e prime uma tecla. Teclas já em uso são trocadas. set.press_key = Prime uma tecla para: {0} set.esc_cancel = Esc cancela @@ -340,6 +341,10 @@ info.tortured = a ser torturado info.prison = Prisioneiros: {0}/{1} msg.wave_warn_dir = Os batedores informam: a vaga de heróis {0} chega ao portão dentro de 30 segundos! Planeiam entrar pelo {1}. +msg.event_quake = Terramoto! Desabaram túneis a {0}. +msg.event_lava = A lava transborda a {0}! +msg.event_flood = Cheia! O rio transborda a {0}. +msg.event_vein = Os diabretes encontraram um novo veio de ouro a {0}! msg.wave_warn_two = Os batedores informam: a vaga de heróis {0} chega ao portão dentro de 30 segundos! Dividem-se e entram pelo {1} e pelo {2}. dir.n = norte dir.ne = nordeste diff --git a/lang/ro.lang b/lang/ro.lang index 2786f0d..79965ac 100644 --- a/lang/ro.lang +++ b/lang/ro.lang @@ -246,6 +246,7 @@ set.lights = Surse de lumină set.ui_scale = Mărime interfață set.game_speed = Viteza jocului set.hints = Sfaturi +set.events = Evenimente aleatorii set.keys_hint = Clic pe o acțiune și apasă o tastă. Tastele deja folosite se schimbă între ele. set.press_key = Apasă o tastă pentru: {0} set.esc_cancel = Esc anulează @@ -340,6 +341,10 @@ info.tortured = e torturat info.prison = Prizonieri: {0}/{1} msg.wave_warn_dir = Iscoadele raportează: valul de eroi {0} ajunge la poartă în 30 de secunde! Plănuiesc să pătrundă dinspre {1}. +msg.event_quake = Cutremur! S-au prăbușit tuneluri în {0}. +msg.event_lava = Lava se revarsă în {0}! +msg.event_flood = Inundație! Râul se revarsă în {0}. +msg.event_vein = Drăcușorii au găsit un nou filon de aur în {0}! msg.wave_warn_two = Iscoadele raportează: valul de eroi {0} ajunge la poartă în 30 de secunde! Se despart și pătrund dinspre {1} și dinspre {2}. dir.n = nord dir.ne = nord-est diff --git a/lang/ru.lang b/lang/ru.lang index a299e77..8acbbce 100644 --- a/lang/ru.lang +++ b/lang/ru.lang @@ -246,6 +246,7 @@ set.lights = Источники света set.ui_scale = Размер интерфейса set.game_speed = Скорость игры set.hints = Подсказки +set.events = Случайные события set.keys_hint = Выбери действие и нажми клавишу. Занятые клавиши меняются местами. set.press_key = Нажми клавишу для: {0} set.esc_cancel = Esc - отмена @@ -340,6 +341,10 @@ info.tortured = его пытают info.prison = Пленники: {0}/{1} msg.wave_warn_dir = Разведчики докладывают: волна героев {0} будет у врат через 30 секунд! Они хотят прорваться с направления: {1}. +msg.event_quake = Землетрясение! Туннели на {0} обрушились. +msg.event_lava = Лава выходит из берегов на {0}! +msg.event_flood = Наводнение! Река выходит из берегов на {0}. +msg.event_vein = Бесы докладывают о новой золотой жиле на {0}! msg.wave_warn_two = Разведчики докладывают: волна героев {0} будет у врат через 30 секунд! Они разделились и прорвутся с направлений: {1} и {2}. dir.n = север dir.ne = северо-восток diff --git a/lang/sk.lang b/lang/sk.lang index 4367297..4139cf2 100644 --- a/lang/sk.lang +++ b/lang/sk.lang @@ -246,6 +246,7 @@ set.lights = Zdroje svetla set.ui_scale = Veľkosť rozhrania set.game_speed = Rýchlosť hry set.hints = Rady +set.events = Náhodné udalosti set.keys_hint = Klikni na akciu a stlač kláves. Už použité klávesy sa vymenia. set.press_key = Stlač kláves pre: {0} set.esc_cancel = Esc ruší @@ -340,6 +341,10 @@ info.tortured = mučený info.prison = Zajatci: {0}/{1} msg.wave_warn_dir = Zvedi hlásia: vlna hrdinov {0} dorazí k bráne o 30 sekúnd! Chcú vtrhnúť zo smeru: {1}. +msg.event_quake = Zemetrasenie! Tunely na {0} sa zrútili. +msg.event_lava = Láva sa vylieva z brehov na {0}! +msg.event_flood = Povodeň! Rieka sa vylieva z brehov na {0}. +msg.event_vein = Škriatkovia hlásia novú zlatú žilu na {0}! msg.wave_warn_two = Zvedi hlásia: vlna hrdinov {0} dorazí k bráne o 30 sekúnd! Rozdelia sa a vtrhnú zo smerov: {1} a {2}. dir.n = sever dir.ne = severovýchod diff --git a/lang/sr.lang b/lang/sr.lang index c7a6adf..17c8279 100644 --- a/lang/sr.lang +++ b/lang/sr.lang @@ -246,6 +246,7 @@ set.lights = Извори светла set.ui_scale = Величина интерфејса set.game_speed = Брзина игре set.hints = Савети +set.events = Случајни догађаји set.keys_hint = Кликни на радњу и притисни тастер. Већ заузети тастери се замене. set.press_key = Притисни тастер за: {0} set.esc_cancel = Esc поништава @@ -340,6 +341,10 @@ info.tortured = муче га info.prison = Заробљеници: {0}/{1} msg.wave_warn_dir = Извиђачи јављају: талас хероја {0} стиже до капије за 30 секунди! Планирају да провале са стране: {1}. +msg.event_quake = Земљотрес! Тунели на {0} су се урушили. +msg.event_lava = Лава се излива из корита на {0}! +msg.event_flood = Поплава! Река се излива на {0}. +msg.event_vein = Врагови јављају нову златну жилу на {0}! msg.wave_warn_two = Извиђачи јављају: талас хероја {0} стиже до капије за 30 секунди! Деле се и проваљују са страна: {1} и {2}. dir.n = север dir.ne = североисток diff --git a/lang/sv.lang b/lang/sv.lang index f3ebf64..2e5ccf8 100644 --- a/lang/sv.lang +++ b/lang/sv.lang @@ -246,6 +246,7 @@ set.lights = Ljuskällor set.ui_scale = Gränssnittsstorlek set.game_speed = Spelhastighet set.hints = Tips +set.events = Slumpmässiga händelser set.keys_hint = Klicka på en åtgärd och tryck på en tangent. Tangenter som redan används byter plats. set.press_key = Tryck på en tangent för: {0} set.esc_cancel = Esc avbryter @@ -340,6 +341,10 @@ info.tortured = torteras info.prison = Fångar: {0}/{1} msg.wave_warn_dir = Spejarna rapporterar: hjältevåg {0} når porten om 30 sekunder! De tänker bryta sig in från {1}. +msg.event_quake = Jordbävning! Tunnlar i {0} har rasat. +msg.event_lava = Lavan svämmar över sina bräddar i {0}! +msg.event_flood = Översvämning! Floden svämmar över i {0}. +msg.event_vein = Imparna rapporterar en ny guldåder i {0}! msg.wave_warn_two = Spejarna rapporterar: hjältevåg {0} når porten om 30 sekunder! De delar upp sig och bryter in från {1} och {2}. dir.n = norr dir.ne = nordost diff --git a/server/src/netplay.rs b/server/src/netplay.rs index ca3d74a..ee97523 100644 --- a/server/src/netplay.rs +++ b/server/src/netplay.rs @@ -90,6 +90,7 @@ struct Room { doc: Option, // editor campaign of a co-op match (sent to every client at the start) resume: Option<(u32, Value)>, // a resumed editor campaign run: its seed and what the masters carry surface: bool, // a free match half above ground + events: bool, // random events in a free match } #[derive(Debug)] @@ -384,7 +385,7 @@ impl Netplay { } let name = self.clients.get(&conn).map(|c| c.name.clone()).unwrap_or_else(|| "Guest".into()); let seats = sides.iter().map(|s| (*s, new_seat(*s, false))).collect::>(); - let room = Room { code: code.clone(), sides, seats, pending: HashMap::new(), turn: 0, started: false, paused: false, missing: false, seed: None, map_w, map_h, difficulty, mission, doc, resume, surface: msg.get("surface").and_then(|v| v.as_bool()).unwrap_or(false) }; + let room = Room { code: code.clone(), sides, seats, pending: HashMap::new(), turn: 0, started: false, paused: false, missing: false, seed: None, map_w, map_h, difficulty, mission, doc, resume, surface: msg.get("surface").and_then(|v| v.as_bool()).unwrap_or(false), events: msg.get("events").and_then(|v| v.as_bool()).unwrap_or(false) }; self.rooms.insert(code.clone(), room); if let Some(c) = self.clients.get_mut(&conn) { c.room = Some(code.clone()); @@ -652,6 +653,9 @@ impl Netplay { if r.surface { m["surface"] = json!(true); } + if r.events { + m["events"] = json!(true); + } if let Some((_, carry)) = &r.resume { m["carry"] = carry.clone(); m["resume"] = json!(true); diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..69522c4 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,190 @@ +// Random events in free games and versus matches (rare; can be switched off): an earthquake +// brings down tunnels near the dungeon, lava or water overflows its banks, or the imps find a +// new gold vein. Shared state and the shared random numbers: every client of a match sees the +// same. Campaigns have none (their stories are told by the missions). + +use crate::game::*; +use crate::map::*; +use serde::{Deserialize, Serialize}; + +// seconds between two events: EVENT_GAP plus up to EVENT_SPREAD +pub const EVENT_GAP: f32 = 480.0; +pub const EVENT_SPREAD: f32 = 240.0; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Events { + pub t: f32, // seconds until the next one +} + +impl Game { + // switches random events on (new free game or match) or off + pub fn set_events(&mut self, on: bool) { + self.happenings = if on && self.mission.is_none() { Some(Events { t: EVENT_GAP + self.rng.f() * EVENT_SPREAD }) } else { None }; + } + + pub(crate) fn update_events(&mut self, dt: f32) { + let Some(ev) = self.happenings.as_mut() else { return }; + ev.t -= dt; + if ev.t > 0.0 { + return; + } + ev.t = EVENT_GAP + self.rng.f() * EVENT_SPREAD; + let kind = self.rng.next() % 4; + let happened = match kind { + 0 => self.event_quake(), + 1 => self.event_overflow(Kind::Lava), + 2 => self.event_overflow(Kind::Water), + _ => self.event_vein(), + }; + // nothing of that kind possible here: a gold vein instead (there is always earth) + if !happened && kind != 3 { + self.event_vein(); + } + } + + // own walkable ground within `r` of tile i? + fn near_own(&self, i: usize, r: i32) -> bool { + let (x, y) = xy(i); + (-r..=r).any(|dy| (-r..=r).any(|dx| self.map.get(x + dx, y + dy).map_or(false, |t| t.owner != Owner::None && t.owner != Owner::Hero && t.walkable()))) + } + + fn announce(&mut self, key: &'static str, i: usize) { + let (x, y) = xy(i); + let dir = format!("@{}", self.direction_key((x, y))); + // every master hears of it + let me = self.me_side; + for s in self.master_sides() { + self.switch_to(s); + self.msg(key, vec![dir.clone()]); + self.sfx_ui(Sfx::CoreHit); + } + self.switch_to(me); + self.add_mark(x as f32 + 0.5, y as f32 + 0.5, crate::game::MARK_BRIDGE, 255, 8.0, true); + } + + // an earthquake: plain tunnels (unowned, not rooms) near a dungeon cave in + fn event_quake(&mut self) -> bool { + let n = self.map.t.len(); + let cands: Vec = (0..n).filter(|&i| { + let t = &self.map.t[i]; + let (x, y) = xy(i); + t.kind == Kind::Floor && t.owner == Owner::None && x > 2 && y > 2 && x < mw() as i32 - 3 && y < mh() as i32 - 3 && !self.map.sky(i) && self.near_own(i, 6) + }).collect(); + if cands.len() < 4 { + return false; + } + let centre = cands[(self.rng.next() as usize) % cands.len()]; + let (cx, cy) = xy(centre); + let mut fell = 0; + for &i in &cands { + let (x, y) = xy(i); + if (x - cx).abs() + (y - cy).abs() > 3 || self.rng.f() < 0.35 { + continue; + } + if self.ents.iter().any(|e| e.alive && e.tile() == i) { + continue; // nobody is buried alive (the quake is not that mean) + } + let t = &mut self.map.t[i]; + t.kind = Kind::Earth; + t.hp = 6.0; + t.marked = false; + fell += 1; + } + if fell == 0 { + return false; + } + self.announce("msg.event_quake", centre); + true + } + + // a river overflows its banks: unowned open ground next to it fills + fn event_overflow(&mut self, river: Kind) -> bool { + let n = self.map.t.len(); + let bank: Vec = (0..n).filter(|&i| { + let t = &self.map.t[i]; + let (x, y) = xy(i); + // no lava above ground + t.kind == Kind::Floor && t.owner == Owner::None && !(river == Kind::Lava && self.map.sky(i)) + && DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |o| o.kind == river)) + }).collect(); + if bank.is_empty() { + return false; + } + let start = bank[(self.rng.next() as usize) % bank.len()]; + let (sx, sy) = xy(start); + let mut done = 0; + for &i in &bank { + let (x, y) = xy(i); + if (x - sx).abs() + (y - sy).abs() > 4 || done >= 8 { + continue; + } + self.map.t[i].kind = river; + self.ground_lost(i); + done += 1; + } + self.announce(if river == Kind::Lava { "msg.event_lava" } else { "msg.event_flood" }, start); + true + } + + // the imps find a gold vein: earth near a dungeon turns to gold + fn event_vein(&mut self) -> bool { + let n = self.map.t.len(); + let cands: Vec = (0..n).filter(|&i| self.map.t[i].kind == Kind::Earth && !self.map.t[i].reinforced && self.near_own(i, 5)).collect(); + if cands.is_empty() { + return false; + } + let centre = cands[(self.rng.next() as usize) % cands.len()]; + let (cx, cy) = xy(centre); + for &i in &cands { + let (x, y) = xy(i); + if (x - cx).abs() + (y - cy).abs() <= 2 { + let t = &mut self.map.t[i]; + t.kind = Kind::Gold; + t.gold = 900; + } + } + self.announce("msg.event_vein", centre); + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn free_games_have_rare_events() { + let mut g = Game::new(12, 80, 64); + g.set_events(true); + assert!(g.happenings.is_some()); + let golds = |g: &Game| g.map.t.iter().filter(|t| t.kind == Kind::Gold).count(); + let before = golds(&g); + g.happenings.as_mut().unwrap().t = 0.0; + g.rng = Rng(3); // a gold vein (or another event) happens now + g.update_events(0.1); + assert!(g.happenings.as_ref().unwrap().t >= EVENT_GAP); + assert!(g.msgs.iter().any(|m| m.key.starts_with("msg.event_")), "an event was told"); + let _ = before; + // campaigns have none + let mut m = crate::campaign::start_mission(3, None); + m.set_events(true); + assert!(m.happenings.is_none()); + } + + #[test] + fn every_event_can_happen() { + let mut g = Game::new(12, 80, 64); + let (cx, cy) = g.map.core; + // a little lava and water next to unowned ground near the dungeon + for (x, k) in [(cx + 6, Kind::Lava), (cx + 9, Kind::Water)] { + for y in cy - 2..cy + 3 { + g.map.t[idx(x, y)].kind = k; + g.map.t[idx(x + 1, y)].kind = Kind::Floor; + g.map.t[idx(x + 1, y)].owner = Owner::None; + } + } + assert!(g.event_vein()); + assert!(g.event_overflow(Kind::Lava)); + assert!(g.event_overflow(Kind::Water)); + } +} diff --git a/src/game.rs b/src/game.rs index 2f94a8d..14bb1f5 100644 --- a/src/game.rs +++ b/src/game.rs @@ -604,6 +604,8 @@ pub struct Game { #[serde(default)] pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges #[serde(default)] + pub happenings: Option, // random events (free games and versus), see events.rs + #[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) @@ -943,6 +945,7 @@ impl Game { vrng: crate::particles::vrng(), dwarf_caves: Vec::new(), bridgework: Default::default(), + happenings: None, stats: Default::default(), wall_plans: Vec::new(), wood: crate::wood::WOOD_START, @@ -1715,6 +1718,7 @@ impl Game { self.update_dwarves(dt); self.update_bridges(dt); self.update_wood(dt); + self.update_events(dt); // fallen hearts of a match are shared state: every client marks them in the same step self.update_fallen_masters(); // the end of the game and the fog are about the master at this computer diff --git a/src/lobby.rs b/src/lobby.rs index a84bae8..adf2183 100644 --- a/src/lobby.rs +++ b/src/lobby.rs @@ -91,6 +91,7 @@ pub struct StartInfo { pub next: bool, // the next mission of a running editor campaign co-op match pub carry: Vec<(u8, crate::campaign::Carry)>, // what every side brings into that mission pub surface: bool, // a free match whose eastern half lies above ground + pub events: bool, // random events in a free match } #[derive(Clone, Debug, Default)] @@ -225,12 +226,13 @@ impl MatchClient { } // a free match; `surface`: its eastern half lies above ground - pub fn create_land(&mut self, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8, surface: bool) { + pub fn create_land(&mut self, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8, surface: bool, events: bool) { let n = self.outbox.len(); self.send_create(sides, map_w, map_h, difficulty, None); - if let (true, Some(text)) = (surface, self.outbox.get_mut(n)) { + if let (true, Some(text)) = (surface || events, self.outbox.get_mut(n)) { if let Ok(mut msg) = serde_json::from_str::(text) { - msg["surface"] = json!(true); + msg["surface"] = json!(surface); + msg["events"] = json!(events); *text = msg.to_string(); } } @@ -564,7 +566,8 @@ impl MatchClient { let resume = editor && v.get("resume").and_then(|x| x.as_bool()).unwrap_or(false); let carry: Vec<(u8, crate::campaign::Carry)> = if resume { v.get("carry").cloned().and_then(|c| serde_json::from_value(c).ok()).unwrap_or_default() } else { Vec::new() }; let surface = v.get("surface").and_then(|x| x.as_bool()).unwrap_or(false); - self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission, ai, doc, editor, teams, next: resume, carry, surface }); + let events = v.get("events").and_then(|x| x.as_bool()).unwrap_or(false); + self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission, ai, doc, editor, teams, next: resume, carry, surface, events }); self.error = None; self.status = None; } @@ -595,7 +598,7 @@ impl MatchClient { } r.ai = ai.clone(); } - self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission: Some(mission), ai, doc: Some(doc), editor: true, teams, next: true, carry, surface: false }); + self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission: Some(mission), ai, doc: Some(doc), editor: true, teams, next: true, carry, surface: false, events: false }); self.error = None; } diff --git a/src/main.rs b/src/main.rs index 1fc5fc7..4c182ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod cloud; mod commands; mod demo; mod editor; +mod events; mod dwarves; mod fog; mod fortress; @@ -185,6 +186,7 @@ fn new_game(seed: u32, s: &Settings) -> (Game, Cam) { g.add_surface(seed, 0.5, 2 + (s.map_h / 64) as u32); g.init_fog(); } + g.set_events(s.events); let (cx, cy) = g.map.core; let cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5); (g, cam) @@ -352,7 +354,7 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A } } MenuAction::MatchJoin(code) => app.client.join(&code), - MenuAction::MatchCreate => app.client.create_land(&[0, 2, 3, 4], app.settings.map_w, app.settings.map_h, app.settings.difficulty, app.settings.surface), + MenuAction::MatchCreate => app.client.create_land(&[0, 2, 3, 4], app.settings.map_w, app.settings.map_h, app.settings.difficulty, app.settings.surface, app.settings.events), MenuAction::MatchCoopCreate(mission) => { let all = campaign::missions(); if let Some(d) = all.get(mission) { @@ -721,7 +723,7 @@ impl App { let next = run_seed.is_some(); let seed = run_seed.unwrap_or_else(|| next_seed(self.seed)); let editor = doc.is_some(); - let info = StartInfo { seed, map_w, map_h, difficulty: self.settings.difficulty, sides: vec![0, 2], local_side: Some(0), mission: Some(mission), ai: vec![2], doc, editor, teams: Vec::new(), next, carry, surface: false }; + let info = StartInfo { seed, map_w, map_h, difficulty: self.settings.difficulty, sides: vec![0, 2], local_side: Some(0), mission: Some(mission), ai: vec![2], doc, editor, teams: Vec::new(), next, carry, surface: false, events: false }; if !self.start_match_game(info) { return false; } @@ -805,6 +807,7 @@ impl App { // the same land on every client: built from the shared seed self.game.add_surface(info.seed, 0.5, 2 + (info.map_h / 64) as u32); } + self.game.set_events(info.events); self.game.coop = false; self.game.set_teams(&info.teams); self.game.ai_difficulty = info.difficulty; diff --git a/src/menu.rs b/src/menu.rs index 959c544..2927415 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -64,6 +64,7 @@ pub enum Item { DebugPathLines, GameSpeed, Hints, + Events, TouchUi, Voice, } @@ -74,7 +75,7 @@ fn tab_items(tab: usize) -> &'static [Item] { 0 => &[Language, Volume, MusicVolume, Mute, Voice], 1 => &[RotateSpeed, PanSpeed, ZoomSpeed, EdgeScroll, InvertMouse], 2 => &[Outlines, WallDetail, Lights, UiScale, RenderScale], - 3 => &[GameSpeed, Hints, TouchUi], + 3 => &[GameSpeed, Hints, Events, TouchUi], 4 => &[DebugFps, DebugFrameTime, DebugPing, DebugTriggerZones, DebugCommandMarkers, DebugPathLines], _ => &[], } @@ -104,6 +105,7 @@ fn item_label(it: Item) -> String { Item::DebugPathLines => "set.debug_path_lines", Item::GameSpeed => "set.game_speed", Item::Hints => "set.hints", + Item::Events => "set.events", Item::TouchUi => "set.touch_ui", Item::Voice => "set.voice", }) @@ -160,6 +162,7 @@ fn item_value(it: Item, s: &Settings) -> String { } Item::GameSpeed => format!("{}x", s.game_speed as i32), Item::Hints => onoff(s.hints), + Item::Events => onoff(s.events), Item::TouchUi => match s.touch_ui { 1 => tr("val.on"), 2 => tr("val.off"), @@ -207,6 +210,7 @@ fn item_change(it: Item, s: &mut Settings, d: i32) { Item::DebugPathLines => s.debug_path_lines = !s.debug_path_lines, Item::GameSpeed => s.game_speed = cycle(&[1.0f32, 2.0], s.game_speed, d), Item::Hints => s.hints = !s.hints, + Item::Events => s.events = !s.events, Item::TouchUi => s.touch_ui = cycle(&[0u8, 1, 2], s.touch_ui, d), } } diff --git a/src/settings.rs b/src/settings.rs index 226eedb..e43f48d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -163,6 +163,7 @@ pub struct Settings { pub map_w: usize, // size of new maps (64..256) pub map_h: usize, pub surface: bool, // new free games: the eastern half lies above ground (forest) + pub events: bool, // random events in free games and versus matches // key bindings: action id -> key name pub keys: BTreeMap, } @@ -198,6 +199,7 @@ impl Default for Settings { map_w: 64, map_h: 64, surface: false, + events: true, keys: BTreeMap::new(), } }