Weather above ground: rain puts out fires, fog shortens sight, storms strike with lightning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f98f05f0d2
commit
3e17bdf52d
27 files changed
+555
-5
No files matched your search
+240
@@ -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<usize> = (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<usize> = (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<usize> = (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::<usize>() / 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user