From 3e17bdf52d3f9a4f64bbc91c883f804c4a04904b Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Fri, 6 Nov 2026 12:11:19 +0100 Subject: [PATCH] Weather above ground: rain puts out fires, fog shortens sight, storms strike with lightning Co-Authored-By: Claude Opus 5.5 --- docs/ROADMAP.md | 5 +- lang/cs.lang | 11 +++ lang/da.lang | 11 +++ lang/de.lang | 11 +++ lang/el.lang | 11 +++ lang/en.lang | 11 +++ lang/es.lang | 11 +++ lang/fi.lang | 11 +++ lang/hr.lang | 11 +++ lang/hu.lang | 11 +++ lang/it.lang | 11 +++ lang/nl.lang | 11 +++ lang/no.lang | 11 +++ lang/pt.lang | 11 +++ lang/ro.lang | 11 +++ lang/ru.lang | 11 +++ lang/sk.lang | 11 +++ lang/sr.lang | 11 +++ lang/sv.lang | 11 +++ src/bridges.rs | 3 +- src/fog.rs | 2 +- src/game.rs | 4 + src/main.rs | 1 + src/render3d.rs | 81 +++++++++++++++- src/scenes.rs | 19 ++++ src/ui.rs | 7 ++ src/weather.rs | 240 ++++++++++++++++++++++++++++++++++++++++++++++++ 27 files changed, 555 insertions(+), 5 deletions(-) create mode 100644 src/weather.rs diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b088ba6..a7f0e78 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -134,5 +134,8 @@ grid), guard and attack markers (shields and swords), savegames with the game st level), Warlord (creatures +5% damage, -5% damage taken), Warlock (mana +15%, spells -6%), Merchant (wages -6%, treasure +6%); 5 levels each. The "Master" button (number of free points, pulsing) opens the window. AI masters spend their points themselves; per game, carried over in the campaign and co-op runs. -5. **Weather above ground** - rain (puts out fire), fog (less sight), storms (lightning in trees and units). +5. **Weather above ground** (done, v202611-00066, weather.rs) - about 60% fair, 20% rain, 12% fog, 8% storm, + minutes each with soft 20 s changes, only on maps with land above ground. Rain (and storms) put out burning + bridges and keep fire from catching; fog halves the sight of units outside; storms strike units (40 damage) + or trees with lightning every 6-20 s. Dimmed daylight, rain streaks, fog veils, flashes; a HUD label. 6. **Named creatures** - names per kind and language, epithets at higher levels, memorial stones. diff --git a/lang/cs.lang b/lang/cs.lang index 3e346c3..ca3a00b 100644 --- a/lang/cs.lang +++ b/lang/cs.lang @@ -1712,3 +1712,14 @@ talent.none = Zatím nenaučeno. talent.then = Další úroveň: {0} msg.talent_point = Nový bod talentu! Použij ho v okně pána. msg.talent_learned = {0}: úroveň {1} naučena. + +# weather above ground +weather.fair = Jasno +weather.rain = Déšť +weather.fog = Mlha +weather.storm = Bouře +msg.weather_clear = Na povrchu se vyjasňuje. +msg.weather_rain = Na povrchu začíná pršet - oheň nechytne. +msg.weather_fog = Nad krajinou se valí mlha - venku každý vidí méně. +msg.weather_storm = Na povrchu se blíží bouře! Blesky zasahují jednotky a stromy. +msg.lightning_unit = Zásah bleskem: {0}! diff --git a/lang/da.lang b/lang/da.lang index 5bd93f7..0832021 100644 --- a/lang/da.lang +++ b/lang/da.lang @@ -1711,3 +1711,14 @@ talent.none = Endnu ikke lært. talent.then = Næste niveau: {0} msg.talent_point = Et nyt talentpoint! Brug det i mestervinduet. msg.talent_learned = {0} niveau {1} lært. + +# weather above ground +weather.fair = Klart +weather.rain = Regn +weather.fog = Tåge +weather.storm = Storm +msg.weather_clear = Over jorden klarer det op. +msg.weather_rain = Over jorden begynder det at regne - ild fænger ikke. +msg.weather_fog = Tåge driver ind over landet - udenfor ser alle mindre. +msg.weather_storm = Over jorden trækker en storm op! Lynet rammer enheder og træer. +msg.lightning_unit = Ramt af lynet: {0}! diff --git a/lang/de.lang b/lang/de.lang index 30069d1..cc185a8 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -1711,3 +1711,14 @@ talent.none = Noch nicht gelernt. talent.then = Nächste Stufe: {0} msg.talent_point = Ein neuer Talentpunkt! Verteile ihn im Meister-Fenster. msg.talent_learned = {0} Stufe {1} gelernt. + +# weather above ground +weather.fair = Heiter +weather.rain = Regen +weather.fog = Nebel +weather.storm = Sturm +msg.weather_clear = Über der Erde klart es auf. +msg.weather_rain = Über der Erde beginnt es zu regnen - Feuer fängt nicht. +msg.weather_fog = Nebel zieht über das Land - draußen sieht jeder weniger. +msg.weather_storm = Über der Erde zieht ein Sturm auf! Blitze treffen Einheiten und Bäume. +msg.lightning_unit = Vom Blitz getroffen: {0}! diff --git a/lang/el.lang b/lang/el.lang index 0cf00c1..a289114 100644 --- a/lang/el.lang +++ b/lang/el.lang @@ -1712,3 +1712,14 @@ talent.none = Δεν έχει μαθευτεί ακόμη. talent.then = Επόμενο επίπεδο: {0} msg.talent_point = Νέος πόντος ταλέντου! Ξόδεψέ τον στο παράθυρο του άρχοντα. msg.talent_learned = {0}: μαθεύτηκε το επίπεδο {1}. + +# weather above ground +weather.fair = Αίθριο +weather.rain = Βροχή +weather.fog = Ομίχλη +weather.storm = Καταιγίδα +msg.weather_clear = Στην επιφάνεια ο ουρανός καθαρίζει. +msg.weather_rain = Στην επιφάνεια αρχίζει να βρέχει - η φωτιά δεν πιάνει. +msg.weather_fog = Η ομίχλη απλώνεται στη γη - έξω όλοι βλέπουν λιγότερο. +msg.weather_storm = Στην επιφάνεια πλησιάζει καταιγίδα! Οι κεραυνοί χτυπούν μονάδες και δέντρα. +msg.lightning_unit = Χτυπήθηκε από κεραυνό: {0}! diff --git a/lang/en.lang b/lang/en.lang index 9c9c616..3704a32 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -1712,3 +1712,14 @@ talent.none = Not learned yet. talent.then = Next level: {0} msg.talent_point = A new talent point! Spend it in the Master window. msg.talent_learned = {0} level {1} learned. + +# weather above ground +weather.fair = Fair +weather.rain = Rain +weather.fog = Fog +weather.storm = Storm +msg.weather_clear = The sky clears up above ground. +msg.weather_rain = It starts to rain above ground - fire will not catch. +msg.weather_fog = Fog rolls over the land - everyone outside sees less. +msg.weather_storm = A storm is brewing above ground! Lightning strikes units and trees. +msg.lightning_unit = Lightning struck: {0}! diff --git a/lang/es.lang b/lang/es.lang index 6ff7010..3826170 100644 --- a/lang/es.lang +++ b/lang/es.lang @@ -1712,3 +1712,14 @@ talent.none = Aún no aprendido. talent.then = Siguiente nivel: {0} msg.talent_point = ¡Un nuevo punto de talento! Gástalo en la ventana del maestro. msg.talent_learned = {0}: nivel {1} aprendido. + +# weather above ground +weather.fair = Despejado +weather.rain = Lluvia +weather.fog = Niebla +weather.storm = Tormenta +msg.weather_clear = El cielo se despeja en la superficie. +msg.weather_rain = Empieza a llover en la superficie: el fuego no prende. +msg.weather_fog = La niebla cubre la tierra: todos ven menos al aire libre. +msg.weather_storm = ¡Se avecina una tormenta en la superficie! Los rayos alcanzan unidades y árboles. +msg.lightning_unit = ¡Un rayo alcanza a {0}! diff --git a/lang/fi.lang b/lang/fi.lang index 6825ab9..831bb9c 100644 --- a/lang/fi.lang +++ b/lang/fi.lang @@ -1712,3 +1712,14 @@ talent.none = Ei vielä opittu. talent.then = Seuraava taso: {0} msg.talent_point = Uusi kykypiste! Käytä se mestarin ikkunassa. msg.talent_learned = {0}: taso {1} opittu. + +# weather above ground +weather.fair = Selkeää +weather.rain = Sade +weather.fog = Sumu +weather.storm = Myrsky +msg.weather_clear = Maan päällä taivas selkenee. +msg.weather_rain = Maan päällä alkaa sataa - tuli ei syty. +msg.weather_fog = Sumu leviää maan ylle - ulkona kaikki näkevät vähemmän. +msg.weather_storm = Maan päällä nousee myrsky! Salamat iskevät yksiköihin ja puihin. +msg.lightning_unit = Salama iski: {0}! diff --git a/lang/hr.lang b/lang/hr.lang index 07cf619..aee89cf 100644 --- a/lang/hr.lang +++ b/lang/hr.lang @@ -1712,3 +1712,14 @@ talent.none = Još nije naučeno. talent.then = Sljedeća razina: {0} msg.talent_point = Novi bod talenta! Potroši ga u prozoru gospodara. msg.talent_learned = {0}: naučena razina {1}. + +# weather above ground +weather.fair = Vedro +weather.rain = Kiša +weather.fog = Magla +weather.storm = Oluja +msg.weather_clear = Na površini se razvedrava. +msg.weather_rain = Na površini počinje kiša - vatra se ne hvata. +msg.weather_fog = Magla se spušta nad zemlju - vani svi vide manje. +msg.weather_storm = Na površini se sprema oluja! Munje pogađaju jedinice i stabla. +msg.lightning_unit = Munja je pogodila: {0}! diff --git a/lang/hu.lang b/lang/hu.lang index 06dbfc7..48fffe4 100644 --- a/lang/hu.lang +++ b/lang/hu.lang @@ -1712,3 +1712,14 @@ talent.none = Még nincs megtanulva. talent.then = Következő szint: {0} msg.talent_point = Új tehetségpont! Költsd el a mester ablakában. msg.talent_learned = {0}: {1}. szint megtanulva. + +# weather above ground +weather.fair = Derült +weather.rain = Eső +weather.fog = Köd +weather.storm = Vihar +msg.weather_clear = A felszínen kitisztul az ég. +msg.weather_rain = A felszínen esni kezd - a tűz nem kap lángra. +msg.weather_fog = Köd ereszkedik a tájra - odakint mindenki kevesebbet lát. +msg.weather_storm = A felszínen vihar készül! Villámok csapnak egységekbe és fákba. +msg.lightning_unit = Villám sújtotta: {0}! diff --git a/lang/it.lang b/lang/it.lang index ceba863..d53f7bb 100644 --- a/lang/it.lang +++ b/lang/it.lang @@ -1712,3 +1712,14 @@ talent.none = Non ancora appreso. talent.then = Livello successivo: {0} msg.talent_point = Un nuovo punto talento! Spendilo nella finestra del maestro. msg.talent_learned = {0}: livello {1} appreso. + +# weather above ground +weather.fair = Sereno +weather.rain = Pioggia +weather.fog = Nebbia +weather.storm = Tempesta +msg.weather_clear = In superficie il cielo si rasserena. +msg.weather_rain = In superficie inizia a piovere: il fuoco non attecchisce. +msg.weather_fog = La nebbia avvolge la terra: all'aperto tutti vedono meno. +msg.weather_storm = In superficie si avvicina una tempesta! I fulmini colpiscono unità e alberi. +msg.lightning_unit = Un fulmine colpisce {0}! diff --git a/lang/nl.lang b/lang/nl.lang index 76b312b..a7d7015 100644 --- a/lang/nl.lang +++ b/lang/nl.lang @@ -1712,3 +1712,14 @@ talent.none = Nog niet geleerd. talent.then = Volgend niveau: {0} msg.talent_point = Een nieuw talentpunt! Besteed het in het meestervenster. msg.talent_learned = {0} niveau {1} geleerd. + +# weather above ground +weather.fair = Helder +weather.rain = Regen +weather.fog = Mist +weather.storm = Storm +msg.weather_clear = Bovengronds klaart het op. +msg.weather_rain = Bovengronds begint het te regenen - vuur vat geen vlam. +msg.weather_fog = Mist trekt over het land - buiten ziet iedereen minder. +msg.weather_storm = Bovengronds steekt een storm op! Bliksem treft units en bomen. +msg.lightning_unit = Door de bliksem getroffen: {0}! diff --git a/lang/no.lang b/lang/no.lang index e53ac1b..837ecb6 100644 --- a/lang/no.lang +++ b/lang/no.lang @@ -1711,3 +1711,14 @@ talent.none = Ikke lært ennå. talent.then = Neste nivå: {0} msg.talent_point = Et nytt talentpoeng! Bruk det i mestervinduet. msg.talent_learned = {0} nivå {1} lært. + +# weather above ground +weather.fair = Klart +weather.rain = Regn +weather.fog = Tåke +weather.storm = Storm +msg.weather_clear = Over jorden klarner det opp. +msg.weather_rain = Over jorden begynner det å regne - ild tar ikke fyr. +msg.weather_fog = Tåke driver inn over landet - ute ser alle mindre. +msg.weather_storm = Over jorden trekker et uvær opp! Lynet treffer enheter og trær. +msg.lightning_unit = Truffet av lynet: {0}! diff --git a/lang/pt.lang b/lang/pt.lang index c56240c..fe80e1a 100644 --- a/lang/pt.lang +++ b/lang/pt.lang @@ -1712,3 +1712,14 @@ talent.none = Ainda não aprendido. talent.then = Próximo nível: {0} msg.talent_point = Um novo ponto de talento! Gasta-o na janela do mestre. msg.talent_learned = {0}: nível {1} aprendido. + +# weather above ground +weather.fair = Limpo +weather.rain = Chuva +weather.fog = Nevoeiro +weather.storm = Temporal +msg.weather_clear = O céu limpa à superfície. +msg.weather_rain = Começa a chover à superfície - o fogo não pega. +msg.weather_fog = O nevoeiro cobre a terra - lá fora todos veem menos. +msg.weather_storm = Aproxima-se uma tempestade à superfície! Os raios atingem unidades e árvores. +msg.lightning_unit = Um raio atinge {0}! diff --git a/lang/ro.lang b/lang/ro.lang index 7e655ea..9e18656 100644 --- a/lang/ro.lang +++ b/lang/ro.lang @@ -1712,3 +1712,14 @@ talent.none = Încă neînvățat. talent.then = Nivelul următor: {0} msg.talent_point = Un nou punct de talent! Folosește-l în fereastra stăpânului. msg.talent_learned = {0}: nivelul {1} învățat. + +# weather above ground +weather.fair = Senin +weather.rain = Ploaie +weather.fog = Ceață +weather.storm = Furtună +msg.weather_clear = La suprafață cerul se înseninează. +msg.weather_rain = La suprafață începe să plouă - focul nu se aprinde. +msg.weather_fog = Ceața se lasă peste ținut - afară toată lumea vede mai puțin. +msg.weather_storm = La suprafață se pregătește o furtună! Fulgerele lovesc unități și copaci. +msg.lightning_unit = Fulgerul a lovit: {0}! diff --git a/lang/ru.lang b/lang/ru.lang index f35c735..f2e4f60 100644 --- a/lang/ru.lang +++ b/lang/ru.lang @@ -1712,3 +1712,14 @@ talent.none = Ещё не изучено. talent.then = Следующий уровень: {0} msg.talent_point = Новое очко таланта! Потрать его в окне мастера. msg.talent_learned = {0}: изучен уровень {1}. + +# weather above ground +weather.fair = Ясно +weather.rain = Дождь +weather.fog = Туман +weather.storm = Гроза +msg.weather_clear = Наверху проясняется. +msg.weather_rain = Наверху начинается дождь - огонь не разгорается. +msg.weather_fog = Туман стелется над землёй - снаружи все видят хуже. +msg.weather_storm = Наверху надвигается гроза! Молнии бьют по бойцам и деревьям. +msg.lightning_unit = Удар молнии: {0}! diff --git a/lang/sk.lang b/lang/sk.lang index 5443e11..d1083ed 100644 --- a/lang/sk.lang +++ b/lang/sk.lang @@ -1712,3 +1712,14 @@ talent.none = Zatiaľ nenaučené. talent.then = Ďalšia úroveň: {0} msg.talent_point = Nový bod talentu! Použi ho v okne pána. msg.talent_learned = {0}: úroveň {1} naučená. + +# weather above ground +weather.fair = Jasno +weather.rain = Dážď +weather.fog = Hmla +weather.storm = Búrka +msg.weather_clear = Na povrchu sa vyjasňuje. +msg.weather_rain = Na povrchu začína pršať - oheň sa nechytí. +msg.weather_fog = Nad krajinou sa valí hmla - vonku každý vidí menej. +msg.weather_storm = Na povrchu sa blíži búrka! Blesky zasahujú jednotky a stromy. +msg.lightning_unit = Zásah bleskom: {0}! diff --git a/lang/sr.lang b/lang/sr.lang index 93c18f2..016842c 100644 --- a/lang/sr.lang +++ b/lang/sr.lang @@ -1712,3 +1712,14 @@ talent.none = Још није научено. talent.then = Следећи ниво: {0} msg.talent_point = Нови бод талента! Потроши га у прозору господара. msg.talent_learned = {0}: научен ниво {1}. + +# weather above ground +weather.fair = Ведро +weather.rain = Киша +weather.fog = Магла +weather.storm = Олуја +msg.weather_clear = На површини се разведрава. +msg.weather_rain = На површини почиње киша - ватра се не хвата. +msg.weather_fog = Магла се спушта над земљу - напољу сви виде мање. +msg.weather_storm = На површини се спрема олуја! Муње погађају јединице и стабла. +msg.lightning_unit = Муња је погодила: {0}! diff --git a/lang/sv.lang b/lang/sv.lang index b4f3bef..8098358 100644 --- a/lang/sv.lang +++ b/lang/sv.lang @@ -1712,3 +1712,14 @@ talent.none = Inte inlärd än. talent.then = Nästa nivå: {0} msg.talent_point = En ny talangpoäng! Använd den i mästarfönstret. msg.talent_learned = {0} nivå {1} inlärd. + +# weather above ground +weather.fair = Klart +weather.rain = Regn +weather.fog = Dimma +weather.storm = Storm +msg.weather_clear = Ovan jord klarnar det upp. +msg.weather_rain = Ovan jord börjar det regna - eld tar inte fäste. +msg.weather_fog = Dimma drar in över landet - ute ser alla sämre. +msg.weather_storm = Ovan jord drar en storm in! Blixten slår ner i enheter och träd. +msg.lightning_unit = Blixten träffade {0}! diff --git a/src/bridges.rs b/src/bridges.rs index e09bb0f..b750882 100644 --- a/src/bridges.rs +++ b/src/bridges.rs @@ -56,7 +56,8 @@ impl Game { return; } let i = idx(tx, ty); - if !self.wooden_bridge(i) || self.burning(i) { + // nothing catches fire in the rain + if !self.wooden_bridge(i) || self.burning(i) || self.rain_at(i) { return; } self.bridgework.burning.push((i, 0.0)); diff --git a/src/fog.rs b/src/fog.rs index 1c74c11..ea16892 100644 --- a/src/fog.rs +++ b/src/fog.rs @@ -137,7 +137,7 @@ impl Game { } } // what the creatures and towers see along open ground (walls stop the view) - let mut eyes: Vec<(usize, i32)> = self.ents.iter().filter(|e| e.alive && !e.held && !e.hero && (e.side == self.me_side || self.allied(self.me_side, e.side)) && !e.chained).map(|e| (e.tile(), SIGHT)).collect(); + let mut eyes: Vec<(usize, i32)> = self.ents.iter().filter(|e| e.alive && !e.held && !e.hero && (e.side == self.me_side || self.allied(self.me_side, e.side)) && !e.chained).map(|e| (e.tile(), self.sight_at(e.tile(), SIGHT))).collect(); let solo = self.masters.is_empty(); eyes.extend(self.towers.iter().filter(|t| !t.hero && (solo || allies.contains(&self.map.t[t.tile].owner))).map(|t| (t.tile, t.range() as i32 + 1))); let mut stamp = vec![0u32; n]; diff --git a/src/game.rs b/src/game.rs index 6034082..4994d27 100644 --- a/src/game.rs +++ b/src/game.rs @@ -636,6 +636,8 @@ pub struct Game { #[serde(default)] pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges #[serde(default)] + pub weather: crate::weather::Weather, // above ground (weather.rs) + #[serde(default)] pub trading: crate::traders::Trading, // the visiting trader (traders.rs) #[serde(default)] pub happenings: Option, // random events (free games and versus), see events.rs @@ -991,6 +993,7 @@ impl Game { vrng: crate::particles::vrng(), dwarf_caves: Vec::new(), bridgework: Default::default(), + weather: Default::default(), trading: Default::default(), happenings: None, snares: Vec::new(), @@ -1805,6 +1808,7 @@ impl Game { self.update_rivals(dt); } self.update_dwarves(dt); + self.update_weather(dt); self.update_bridges(dt); self.update_wood(dt); self.update_events(dt); diff --git a/src/main.rs b/src/main.rs index d99913a..0c002a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ mod research; mod rival; mod rogues; mod talents; +mod weather; mod render3d; mod rooms; mod save; diff --git a/src/render3d.rs b/src/render3d.rs index ad70d6b..ea5595e 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -512,6 +512,11 @@ fn room_level_at(map: &Map, t: &Tile) -> u8 { } // Ambient light field: own territory is lit, foreign areas are dim +thread_local! { + // daylight above ground (bad weather dims it), in tenths + static SKY_LIGHT: std::cell::Cell = const { std::cell::Cell::new(16) }; +} + fn ambient_field(map: &Map) -> Vec { let mut f = vec![0.0f32; n()]; for (i, t) in map.t.iter().enumerate() { @@ -522,7 +527,7 @@ fn ambient_field(map: &Map) -> Vec { }; // daylight above ground (cliffs and trees too) if map.sky(i) { - f[i] = 1.6; + f[i] = SKY_LIGHT.with(|s| s.get()) as f32 / 10.0; } // rooms of a higher level shine brighter let lv = room_level_at(map, t); @@ -1279,6 +1284,14 @@ impl R3d { fn update_chunks(&mut self, g: &Game, gfx: &Gfx, m: &Mat4) { self.ensure_chunks(); let side = g.me.side().min(7); + // the weather dims the daylight: the chunks above ground are lit again in steps + let sky = ((1.6 * (1.0 - g.weather.gloom())) * 10.0).round() as u32; + if SKY_LIGHT.with(|s| s.get()) != sky { + SKY_LIGHT.with(|s| s.set(sky)); + for ch in self.chunks.iter_mut() { + ch.sig = 0; + } + } if view_side() != side { // another master looks: the marks change colour VIEW_SIDE.with(|v| v.set(side)); @@ -1609,6 +1622,7 @@ impl R3d { } draw_traps_glow(g, t); draw_parts_glow(g); + draw_weather(g, cam, t); gl_use_default_material(); crate::ui::begin_ui(); if let (Some((_, _, k)), Some((rt, tw, th))) = (pix, self.target.as_ref()) { @@ -1629,7 +1643,7 @@ impl R3d { let (ix, iz) = (x as i32, z as i32); let own = g.map.get(ix, iz).map_or(false, |t| matches!(t.owner, Owner::Player | Owner::Rival1 | Owner::Rival2 | Owner::Rival3)); if inb(ix, iz) && g.map.sky(idx(ix, iz)) { - 0.95 + 0.95 * (1.0 - 0.6 * g.weather.gloom()) } else if own { 0.75 } else { @@ -2489,6 +2503,69 @@ fn draw_landmarks_solid(g: &Game, t: f32) { let _ = t; } +// rain streaks and fog veils over the land above ground near the camera; a storm's lightning +// lights the sky up for a moment +fn draw_weather(g: &Game, cam: &Cam, t: f32) { + let w = &g.weather; + if w.level <= 0.01 || g.map.sky.is_empty() { + return; + } + let (cx, cz) = (cam.rx as i32, cam.rz as i32); + let reach = (cam.view_h() * 1.2) as i32 + 4; + let sky = |x: i32, z: i32| inb(x, z) && g.map.sky(idx(x, z)); + // fog: veils at two heights, one plane per run of open sky tiles in a row + let fog = w.fog(); + if fog > 0.01 { + for (h, a) in [(0.7f32, 0.22f32), (1.6, 0.16)] { + let col = Color::new(0.75, 0.78, 0.8, a * fog); + for z in cz - reach..=cz + reach { + let mut x = cx - reach; + while x <= cx + reach { + if !sky(x, z) { + x += 1; + continue; + } + let x0 = x; + while x <= cx + reach && sky(x, z) { + x += 1; + } + let len = (x - x0) as f32; + let drift = (t * 0.2 + z as f32 * 0.7).sin() * 0.15; + draw_plane(vec3(x0 as f32 + len * 0.5 + drift, h, z as f32 + 0.5), vec2(len * 0.5, 0.5), None, col); + } + } + } + } + // rain: slanted streaks falling over the open land + let rain = w.rain(); + if rain > 0.01 { + let n = (1100.0 * rain) as u32; + let col = Color::new(0.75, 0.82, 0.95, 0.8); + let span = (2 * reach + 1) as u32; + for k in 0..n { + let h = k.wrapping_mul(2654435761) ^ 0x9e37_79b9; + let x = cx - reach + (h % span) as i32; + let z = cz - reach + ((h >> 11) % span) as i32; + if !sky(x, z) { + continue; + } + let fx = x as f32 + ((h >> 3) % 100) as f32 / 100.0; + let fz = z as f32 + ((h >> 17) % 100) as f32 / 100.0; + let fall = (t * 1.6 + (h % 1000) as f32 / 1000.0).fract(); + let y = 3.0 * (1.0 - fall); + line3(vec3(fx, y + 0.5, fz - 0.08), vec3(fx + 0.05, y, fz + 0.08), 0.035, col); + } + } + // the flash of a lightning bolt + let since = g.time - w.flash; + if w.flash > 0.0 && (0.0..0.25).contains(&since) { + let a = 0.35 * (1.0 - since / 0.25); + for z in (cz - reach..=cz + reach).step_by(4) { + draw_plane(vec3(cx as f32, 0.05, z as f32 + 2.0), vec2(reach as f32, 2.0), None, Color::new(0.9, 0.92, 1.0, a)); + } + } +} + // growth stage of a giant mushroom: 0 a sprout .. 4 fully grown (felling only then) fn mushroom_stage(t: &Tile) -> u32 { if t.grown() { diff --git a/src/scenes.rs b/src/scenes.rs index c2d7b05..5c93361 100644 --- a/src/scenes.rs +++ b/src/scenes.rs @@ -264,3 +264,22 @@ fn export_rogues_scene() { g.snares.push(idx(cx + 7, cy + 3)); save("rogues", &mut g, cx as f32 + 6.5, cy as f32 + 4.5, 8.0, 0.0, false); } + +// weather above ground: rain and fog over the land +#[test] +#[ignore] +fn export_weather_scenes() { + std::fs::create_dir_all("/tmp/scenes").unwrap(); + for (name, kind) in [("weather-rain", crate::weather::RAIN), ("weather-fog", crate::weather::FOG), ("weather-storm", crate::weather::STORM)] { + let mut g = Game::new(7, 96, 96); + g.add_surface(7, 0.5, 3); + g.weather = crate::weather::Weather { kind, shown: kind, level: 1.0, left: 900.0, ..Default::default() }; + // the camera on the land above ground, near the edge of the dungeon side + let i = (0..g.map.t.len()).filter(|&i| g.map.sky(i)).min_by_key(|&i| { + let (x, y) = xy(i); + (x - 52).abs() + (y - 48).abs() + }).unwrap(); + let (x, y) = xy(i); + save(name, &mut g, x as f32 + 4.5, y as f32 + 0.5, 18.0, 0.0, false); + } +} diff --git a/src/ui.rs b/src/ui.rs index 83be387..3764e31 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -809,6 +809,13 @@ impl Ui { }; segs.insert(3, vec![txt(tr(key), col), Part::Gap, Part::Bar(rv.hp / crate::rival::RIVAL_CORE_HP, col)]); } + // the weather above ground (maps with land above ground only) + if g.has_sky() { + let w = &g.weather; + let k = if w.level > 0.05 { w.shown } else { crate::weather::FAIR }; + let col = [GOLD, Color::new(0.55, 0.7, 1.0, 1.0), Color::new(0.75, 0.78, 0.8, 1.0), Color::new(0.75, 0.6, 1.0, 1.0)][k as usize]; + segs.push(vec![txt(tr(crate::weather::NAMES[k as usize]), col)]); + } if g.waves_on() { segs.push(vec![txt(trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col), Part::Gap, txt(mmss(g.wave_timer), TXT)]); } diff --git a/src/weather.rs b/src/weather.rs new file mode 100644 index 0000000..b1ff90a --- /dev/null +++ b/src/weather.rs @@ -0,0 +1,240 @@ +// Weather above ground: mostly fair, now and then rain, fog or a storm, each for a few minutes +// with soft changes. Rain puts out burning bridges and keeps fire from catching; fog shortens the +// sight of everyone outside; a storm brings both rain and lightning that strikes units and trees. +// Only maps with land above ground have weather. It runs in the shared simulation (same on every +// computer of a match). + +use crate::game::*; +use crate::map::*; +use serde::{Deserialize, Serialize}; + +pub const FAIR: u8 = 0; +pub const RAIN: u8 = 1; +pub const FOG: u8 = 2; +pub const STORM: u8 = 3; +pub const NAMES: [&str; 4] = ["weather.fair", "weather.rain", "weather.fog", "weather.storm"]; + +// seconds for a change to fade in or out +const FADE: f32 = 20.0; +// damage of a lightning strike +pub const BOLT_DAMAGE: f32 = 40.0; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Weather { + pub kind: u8, // the weather that is coming or here + pub shown: u8, // the weather that is felt now (changes when the old one has faded out) + pub level: f32, // how strong it is: 0 none .. 1 full + pub left: f32, // seconds until the next change + pub strike_t: f32, // seconds until the next lightning (storm) + pub flash: f32, // game time of the last lightning (the sky lights up) +} + +// the weather that comes next: about 60% fair, 20% rain, 12% fog, 8% storm +fn pick(r: f32) -> u8 { + if r < 0.6 { + FAIR + } else if r < 0.8 { + RAIN + } else if r < 0.92 { + FOG + } else { + STORM + } +} + +impl Weather { + // strength of an effect: rain falls in rain and storms, fog in fog (a little in storms) + pub fn rain(&self) -> f32 { + if matches!(self.shown, RAIN | STORM) { self.level } else { 0.0 } + } + pub fn fog(&self) -> f32 { + match self.shown { + FOG => self.level, + STORM => self.level * 0.4, + _ => 0.0, + } + } + // how much the daylight is dimmed (0 .. 1) + pub fn gloom(&self) -> f32 { + self.level + * match self.shown { + RAIN => 0.3, + FOG => 0.2, + STORM => 0.55, + _ => 0.0, + } + } +} + +impl Game { + pub fn has_sky(&self) -> bool { + self.map.sky.iter().any(|&s| s) + } + + // it rains on tile i right now (enough to put out fire) + pub fn rain_at(&self, i: usize) -> bool { + self.weather.rain() > 0.4 && self.map.sky(i) + } + + // sight of a unit standing on tile i (fog outside shortens it) + pub fn sight_at(&self, i: usize, base: i32) -> i32 { + if !self.map.sky(i) { + return base; + } + ((base as f32) * (1.0 - 0.5 * self.weather.fog())).round().max(2.0) as i32 + } + + pub(crate) fn update_weather(&mut self, dt: f32) { + if self.map.sky.is_empty() { + return; + } + let w = &mut self.weather; + w.left -= dt; + if w.left <= 0.0 { + // after bad weather it clears up more often + let r = self.rng.f(); + let next = if w.kind != FAIR && r < 0.5 { FAIR } else { pick(self.rng.f()) }; + let w = &mut self.weather; + w.kind = next; + w.left = if next == FAIR { 180.0 + self.rng.f() * 240.0 } else { 90.0 + self.rng.f() * 150.0 }; + let w = &mut self.weather; + if w.kind != w.shown || w.level < 0.99 { + let key = ["msg.weather_clear", "msg.weather_rain", "msg.weather_fog", "msg.weather_storm"][next as usize]; + if next != FAIR || w.shown != FAIR { + self.tell_local(key, vec![], None); + } + } + } + // soft changes: the old weather fades out before the new one fades in + let w = &mut self.weather; + if w.shown != w.kind { + w.level -= dt / FADE; + if w.level <= 0.0 { + w.level = 0.0; + w.shown = w.kind; + } + } else if w.kind != FAIR { + w.level = (w.level + dt / FADE).min(1.0); + } else { + w.level = (w.level - dt / FADE).max(0.0); + } + // rain puts out the burning bridges outside + if self.weather.rain() > 0.4 && !self.bridgework.burning.is_empty() { + let map = &self.map; + self.bridgework.burning.retain(|b| !map.sky(b.0)); + } + // lightning in a storm + if self.weather.shown == STORM && self.weather.level > 0.6 { + self.weather.strike_t -= dt; + if self.weather.strike_t <= 0.0 { + self.weather.strike_t = 6.0 + self.rng.f() * 14.0; + self.lightning_strike(); + } + } + } + + // a bolt from the sky: a unit outside (now and then) or a tree, else bare land + pub(crate) fn lightning_strike(&mut self) { + let outside: Vec = (0..self.ents.len()).filter(|&j| { + let e = &self.ents[j]; + e.alive && !e.held && e.captive == 0 && self.map.sky(e.tile()) + }).collect(); + let r = self.rng.f(); + let (x, y) = if r < 0.35 && !outside.is_empty() { + let j = outside[(self.rng.next() as usize) % outside.len()]; + let e = &mut self.ents[j]; + e.hp -= BOLT_DAMAGE; + e.flash = 0.3; + let (x, y, side, kind, hero, dead) = (e.x, e.y, e.side, e.kind, e.hero, e.hp <= 0.0); + if !hero && side == self.local_side { + self.tell_local("msg.lightning_unit", vec![format!("@{}", st(kind).name)], None); + } + if dead { + self.kill(j); + } + (x, y) + } else { + let trees: Vec = (0..self.map.t.len()).filter(|&i| self.map.t[i].kind == Kind::Tree && self.map.sky(i)).collect(); + if let Some(&i) = trees.get((self.rng.next() as usize) % trees.len().max(1)) { + // the tree is split and burns down + let t = &mut self.map.t[i]; + t.kind = Kind::Floor; + t.owner = Owner::None; + t.marked = false; + t.hp = 0.0; + let (tx, ty) = xy(i); + self.fx(3, tx as f32 + 0.5, ty as f32 + 0.5, 0.0, 0.0, 1.2, 0xff304050); + (tx as f32 + 0.5, ty as f32 + 0.5) + } else { + let sky: Vec = (0..self.map.t.len()).filter(|&i| self.map.sky(i)).collect(); + let Some(&i) = sky.get((self.rng.next() as usize) % sky.len().max(1)) else { return }; + let (tx, ty) = xy(i); + (tx as f32 + 0.5, ty as f32 + 0.5) + } + }; + self.weather.flash = self.time; + self.fx(1, x, y, 0.0, 0.0, 0.5, 0xffffeeaa); + self.sfx(Sfx::Lightning, x, y); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn surface_game() -> Game { + let mut g = Game::new(7, 96, 96); + g.add_surface(7, 0.5, 3); + assert!(g.has_sky()); + g + } + + #[test] + fn the_weather_changes_and_is_mostly_fair() { + let mut g = surface_game(); + let mut seen = [0usize; 4]; + for _ in 0..(4 * 60 * 60) { + g.update_weather(1.0); + seen[g.weather.shown as usize] += 1; + } + assert!(seen[FAIR as usize] > seen.iter().sum::() / 3, "{:?}", seen); + assert!(seen.iter().filter(|&&n| n > 0).count() >= 3, "several kinds of weather: {:?}", seen); + // no weather underground only + let mut u = Game::new(7, 96, 96); + u.weather.left = 0.0; + u.update_weather(1.0); + assert_eq!(u.weather.level, 0.0); + } + + #[test] + fn rain_puts_out_fires_and_fog_shortens_sight() { + let mut g = surface_game(); + let i = (0..g.map.t.len()).find(|&i| g.map.sky(i)).unwrap(); + g.bridgework.burning.push((i, 0.0)); + g.weather = Weather { kind: RAIN, shown: RAIN, level: 1.0, left: 100.0, ..Default::default() }; + g.update_weather(0.1); + assert!(g.bridgework.burning.is_empty(), "rain puts the fire out"); + assert!(g.rain_at(i)); + g.weather = Weather { kind: FOG, shown: FOG, level: 1.0, left: 100.0, ..Default::default() }; + assert!(g.sight_at(i, 6) < 6); + let under = (0..g.map.t.len()).find(|&i| !g.map.sky(i)).unwrap(); + assert_eq!(g.sight_at(under, 6), 6, "underground nothing changes"); + } + + #[test] + fn storms_strike_with_lightning() { + let mut g = surface_game(); + let trees0 = g.map.t.iter().filter(|t| t.kind == Kind::Tree).count(); + let i = (0..g.map.t.len()).find(|&i| g.map.sky(i) && g.map.t[i].walkable()).unwrap(); + let (x, y) = xy(i); + let id = g.spawn(EK::Knight, true, x as f32 + 0.5, y as f32 + 0.5, 1); + g.weather = Weather { kind: STORM, shown: STORM, level: 1.0, left: 1000.0, ..Default::default() }; + for _ in 0..30 { + g.lightning_strike(); + } + let trees = g.map.t.iter().filter(|t| t.kind == Kind::Tree).count(); + let hurt = g.ents.iter().find(|e| e.id == id).map_or(true, |e| e.hp < e.max_hp); + assert!(trees < trees0 || hurt, "lightning hit something"); + assert!(g.weather.flash > 0.0 || g.time == 0.0); + } +}