veloren_voxygen/scene/
smoke_cycle.rs

1use rand::prelude::*;
2use rand_chacha::ChaCha8Rng;
3use vek::*;
4
5// create pseudorandom from position
6fn seed_from_pos(pos: Vec3<i32>) -> [u8; 32] {
7    [
8        pos.x as u8,
9        (pos.x >> 8) as u8,
10        (pos.x >> 16) as u8,
11        (pos.x >> 24) as u8,
12        0,
13        0,
14        0,
15        0,
16        pos.y as u8,
17        (pos.y >> 8) as u8,
18        (pos.y >> 16) as u8,
19        (pos.y >> 24) as u8,
20        0,
21        0,
22        0,
23        0,
24        pos.z as u8,
25        (pos.z >> 8) as u8,
26        (pos.z >> 16) as u8,
27        (pos.z >> 24) as u8,
28        0,
29        0,
30        0,
31        0,
32        0,
33        0,
34        0,
35        0,
36        0,
37        0,
38        0,
39        0,
40    ]
41}
42
43#[derive(Debug)]
44struct FireplaceTiming {
45    // all this assumes sunrise at 6am, sunset at 6pm
46    breakfast: f32,   // 5am to 7am
47    dinner: f32,      // 5pm to 7pm
48    daily_cycle: f32, // 30min to 2hours
49}
50
51const SMOKE_BREAKFAST_STRENGTH: f32 = 96.0;
52const SMOKE_BREAKFAST_HALF_DURATION: f32 = 45.0 * 60.0;
53const SMOKE_BREAKFAST_START: f32 = 5.0 * 60.0 * 60.0;
54const SMOKE_BREAKFAST_RANGE: f32 = 2.0 * 60.0 * 60.0;
55const SMOKE_DINNER_STRENGTH: f32 = 128.0;
56const SMOKE_DINNER_HALF_DURATION: f32 = 60.0 * 60.0;
57const SMOKE_DINNER_START: f32 = 17.0 * 60.0 * 60.0;
58const SMOKE_DINNER_RANGE: f32 = 2.0 * 60.0 * 60.0;
59const SMOKE_DAILY_CYCLE_MIN: f32 = 30.0 * 60.0;
60const SMOKE_DAILY_CYCLE_MAX: f32 = 120.0 * 60.0;
61const SMOKE_MAX_TEMPERATURE: f32 = 0.0; // temperature for nominal smoke (0..daily_var)
62const SMOKE_MAX_TEMP_VALUE: f32 = 1.0;
63const SMOKE_TEMP_MULTIPLIER: f32 = 96.0;
64const SMOKE_DAILY_VARIATION: f32 = 32.0;
65
66#[derive(Debug)]
67struct FireplaceClimate {
68    daily_strength: f32, // can be negative (offset)
69    day_start: f32,      // seconds since breakfast for daily cycle
70    day_end: f32,        // seconds before dinner on daily cycle
71}
72
73fn create_timing(rng: &mut ChaCha8Rng) -> FireplaceTiming {
74    let breakfast: f32 = SMOKE_BREAKFAST_START + rng.gen::<f32>() * SMOKE_BREAKFAST_RANGE;
75    let dinner: f32 = SMOKE_DINNER_START + rng.gen::<f32>() * SMOKE_DINNER_RANGE;
76    let daily_cycle: f32 =
77        SMOKE_DAILY_CYCLE_MIN + rng.gen::<f32>() * (SMOKE_DAILY_CYCLE_MAX - SMOKE_DAILY_CYCLE_MIN);
78    FireplaceTiming {
79        breakfast,
80        dinner,
81        daily_cycle,
82    }
83}
84
85fn create_climate(temperature: f32) -> FireplaceClimate {
86    // temp -1…1
87    let daily_strength =
88        (SMOKE_MAX_TEMPERATURE - temperature).min(SMOKE_MAX_TEMP_VALUE) * SMOKE_TEMP_MULTIPLIER;
89    // when is breakfast down to daily strength
90    // daily_strength ==
91    // SMOKE_BREAKFAST_STRENGTH*(1.0-(t-breakfast)/SMOKE_BREAKFAST_HALF_DURATION)
92    //
93    // (t-breakfast) = (1.0 -
94    // daily_strength/SMOKE_BREAKFAST_STRENGTH)*SMOKE_BREAKFAST_HALF_DURATION
95    let day_start = (SMOKE_BREAKFAST_STRENGTH - daily_strength.max(0.0))
96        * (SMOKE_BREAKFAST_HALF_DURATION / SMOKE_BREAKFAST_STRENGTH);
97    let day_end = (SMOKE_DINNER_STRENGTH - daily_strength.max(0.0))
98        * (SMOKE_DINNER_HALF_DURATION / SMOKE_DINNER_STRENGTH);
99    FireplaceClimate {
100        daily_strength,
101        day_start,
102        day_end,
103    }
104}
105
106pub type Increasing = bool;
107
108pub fn smoke_at_time(position: Vec3<i32>, temperature: f32, time_of_day: f32) -> (f32, Increasing) {
109    let mut pseudorandom = ChaCha8Rng::from_seed(seed_from_pos(position));
110    let timing = create_timing(&mut pseudorandom);
111    let climate = create_climate(temperature);
112    let after_breakfast = time_of_day - timing.breakfast;
113    if after_breakfast < -SMOKE_BREAKFAST_HALF_DURATION {
114        /* night */
115        (0.0, false)
116    } else if after_breakfast < 0.0 {
117        /* cooking breakfast */
118        (
119            (SMOKE_BREAKFAST_HALF_DURATION + after_breakfast)
120                * (SMOKE_BREAKFAST_STRENGTH / SMOKE_BREAKFAST_HALF_DURATION),
121            true,
122        )
123    } else if after_breakfast < climate.day_start {
124        /* cooling */
125        (
126            (SMOKE_BREAKFAST_HALF_DURATION - after_breakfast)
127                * (SMOKE_BREAKFAST_STRENGTH / SMOKE_BREAKFAST_HALF_DURATION),
128            false,
129        )
130    } else if time_of_day < timing.dinner - climate.day_end {
131        /* day cycle */
132        let day_phase = ((after_breakfast - climate.day_start) / timing.daily_cycle).fract();
133        if day_phase < 0.5 {
134            (
135                (climate.daily_strength + day_phase * (2.0 * SMOKE_DAILY_VARIATION)).max(0.0),
136                true,
137            )
138        } else {
139            (
140                (climate.daily_strength + (1.0 - day_phase) * (2.0 * SMOKE_DAILY_VARIATION))
141                    .max(0.0),
142                false,
143            )
144        }
145    } else if time_of_day < timing.dinner {
146        /* cooking dinner */
147        (
148            (SMOKE_DINNER_HALF_DURATION + time_of_day - timing.dinner)
149                * (SMOKE_DINNER_STRENGTH / SMOKE_DINNER_HALF_DURATION),
150            true,
151        )
152    } else {
153        /* cooling + night */
154        (
155            (SMOKE_DINNER_HALF_DURATION - time_of_day + timing.dinner).max(0.0)
156                * (SMOKE_DINNER_STRENGTH / SMOKE_DINNER_HALF_DURATION),
157            false,
158        )
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    fn test_conditions(position: Vec3<i32>, temperature: f32) {
167        print!("{} T{:.1}  ", position, temperature);
168        let mut pseudorandom = ChaCha8Rng::from_seed(seed_from_pos(position));
169        if true {
170            let timing = create_timing(&mut pseudorandom);
171            let climate = create_climate(temperature);
172            print!(
173                "B{:.1}+{:.1} D{:.1}-{:.1} C{:.0} S{:.0} ",
174                timing.breakfast / 3600.0,
175                climate.day_start / 3600.0,
176                timing.dinner / 3600.0,
177                climate.day_end / 3600.0,
178                timing.daily_cycle / 60.0,
179                climate.daily_strength
180            );
181        }
182        for i in 0..24 {
183            print!(" {}:", i);
184            for j in 0..6 {
185                let time_of_day = 60.0 * 60.0 * (i as f32) + 60.0 * 10.0 * (j as f32);
186                let res = smoke_at_time(position, temperature, time_of_day);
187                print!("{:.0}{} ", res.0, if res.1 { "^" } else { "" },);
188                assert!(res.0 >= 0.0);
189                assert!(res.0 <= SMOKE_DINNER_STRENGTH);
190            }
191        }
192        println!();
193    }
194
195    #[test]
196    fn test_smoke() {
197        test_conditions(Vec3::new(25_i32, 11, 33), -1.0);
198        test_conditions(Vec3::new(22_i32, 11, 33), -0.5);
199        test_conditions(Vec3::new(27_i32, 11, 33), 0.0);
200        test_conditions(Vec3::new(24_i32, 11, 33), 0.5);
201        test_conditions(Vec3::new(26_i32, 11, 33), 1.0);
202    }
203}