use core::{f32::consts::PI, fmt::Display};
use log::{error, trace};
#[cfg(not(feature = "std"))]
use num_traits::Float;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{
Atmosim,
config::{ONE_ATMOSPHERE, R},
gases::GasMixture,
reactions::ATMOS_TICKRATE,
};
const HARD_TICK_LIMIT: usize = 15000;
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct GasContainerPrototype {
pub volume: f32,
pub max_integrity: i8,
pub normal_pressure: f32,
pub logic: GasContainerLogic,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum GasContainerLogic {
Modern {
container: ContainerType,
release_area: f32,
safety_pressure: f32,
overpressure: f32,
explosion_slope: f32,
explosion_max_intensity: f32,
},
TankOld {
fragment_pressure: f32,
rupture_pressure: f32,
leak_pressure: f32,
fragment_scale: f32,
},
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ContainerType {
Tank,
Canister,
}
impl GasContainerPrototype {
#[must_use]
pub(crate) fn default_tank() -> Self {
Self {
volume: 5.,
max_integrity: 5,
normal_pressure: ONE_ATMOSPHERE * 10.,
logic: GasContainerLogic::Modern {
container: ContainerType::Tank,
release_area: 0.0001,
safety_pressure: ONE_ATMOSPHERE * 15.,
overpressure: ONE_ATMOSPHERE * 20.,
explosion_max_intensity: 20.,
explosion_slope: 1.,
},
}
}
#[must_use]
pub(crate) fn default_canister() -> Self {
Self {
volume: 1500.,
max_integrity: 25,
normal_pressure: 4500.,
logic: GasContainerLogic::Modern {
container: ContainerType::Canister,
release_area: 0.05,
safety_pressure: 5066.25,
overpressure: 15198.75,
explosion_max_intensity: 100.,
explosion_slope: 1.,
},
}
}
#[must_use]
pub(crate) fn old_default_tank() -> Self {
Self {
max_integrity: 3,
logic: GasContainerLogic::TankOld {
fragment_pressure: 50. * ONE_ATMOSPHERE,
rupture_pressure: 40. * ONE_ATMOSPHERE,
leak_pressure: 30. * ONE_ATMOSPHERE,
fragment_scale: 2.25 * ONE_ATMOSPHERE,
},
..Self::default_tank()
}
}
}
impl Atmosim {
#[must_use]
pub(crate) fn container(&self) -> GasContainerPrototype {
self.game.container
}
}
impl Default for GasContainerPrototype {
fn default() -> Self {
Self::default_tank()
}
}
#[derive(Debug)]
pub struct GasContainer<'a> {
pub(crate) mix: GasMixture<'a>,
state: GasContainerState,
integrity: i8,
lifetime: usize,
valve_open: bool,
}
impl Display for GasContainer<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.mix.fmt(f)?;
writeln!(f, ", P = {} kPa", self.pressure())?;
write!(f, "State: {:?}", self.state)
}
}
impl<'a> GasContainer<'a> {
#[must_use]
pub fn new(mix: GasMixture<'a>) -> Self {
Self {
mix,
state: GasContainerState::Idle,
integrity: mix.engine.game.container.max_integrity,
lifetime: 0,
valve_open: false,
}
}
#[must_use]
pub fn proto(&self) -> GasContainerPrototype {
self.mix.engine.container()
}
#[must_use]
pub fn mixture(&'_ self) -> &'_ GasMixture<'_> {
&self.mix
}
#[must_use]
pub fn volume(&self) -> f32 {
self.proto().volume
}
#[must_use]
pub fn ticks(&self) -> usize {
self.lifetime
}
#[must_use]
pub fn state(&self) -> GasContainerState {
self.state
}
#[must_use]
pub fn normalized(mut self) -> Self {
let ratio = self.proto().normal_pressure / self.pressure(); self.mix.modify_moles_each(|mole| mole * ratio);
self
}
#[must_use]
pub fn pressure(&self) -> f32 {
self.mix.total_moles() * R * self.mix.temperature() / self.volume()
}
pub fn step(&mut self) -> GasContainerState {
self.lifetime += 1;
match self.proto().logic {
GasContainerLogic::Modern { .. } => {
if self.check_status() {
self.device_updated();
}
}
GasContainerLogic::TankOld { .. } => {
if self.mix.react() {
self.state = GasContainerState::Reacting;
} else {
self.state = GasContainerState::Idle;
}
self.check_status();
}
}
trace!(
"Container stepped, currently contains {:?} at {}K ({} kPa), integrity = {}.",
self.mix.moles(),
self.mix.temperature(),
self.pressure(),
self.integrity
);
self.state
}
pub fn step_n(&mut self, n: usize) {
for _ in 0..n {
if self.step() != GasContainerState::Reacting {
break;
}
}
}
pub fn step_all(&mut self) {
self.step_n(HARD_TICK_LIMIT);
if self.state() == GasContainerState::Reacting {
error!("Gas mixture {self} went into an infinite loop.");
}
}
pub(crate) fn get_over_pressure(&self, other: Option<GasContainer>) -> f32 {
(self.pressure()
- if let Some(container) = other {
container.pressure()
} else {
0.
})
* self.proto().volume
}
fn check_status(&mut self) -> bool {
match self.proto().logic {
GasContainerLogic::Modern {
safety_pressure,
overpressure,
explosion_slope,
explosion_max_intensity,
..
} => {
let pressure = self.pressure();
if pressure > overpressure * f32::from(self.integrity + 1) {
self.state = GasContainerState::Exploded {
radius: intensity_to_radius(
self.get_over_pressure(None).sqrt(),
explosion_slope,
explosion_max_intensity,
),
};
return false;
}
if pressure > overpressure {
self.integrity -= 1;
} else if self.integrity < self.proto().max_integrity {
self.integrity += 1;
}
if pressure > safety_pressure {
self.valve_open = true;
}
true
}
GasContainerLogic::TankOld {
rupture_pressure,
fragment_pressure,
leak_pressure,
fragment_scale,
} => {
match self.pressure() {
p if p > fragment_pressure => {
for _ in 0..3 {
self.mix.react(); }
self.state = GasContainerState::Exploded {
radius: ((self.pressure() - fragment_pressure) / fragment_scale).sqrt(),
}
}
p if p > rupture_pressure && self.integrity <= 0 => {
self.state = GasContainerState::Ruptured;
}
p if p > leak_pressure && self.integrity <= 0 => {
self.mix.remove_ratio(0.25);
}
p if p > leak_pressure || p > rupture_pressure => self.integrity -= 1,
_ if self.integrity < 3 => self.integrity += 1, _ => (),
}
false }
}
}
fn device_updated(&mut self) {
match self.proto().logic {
GasContainerLogic::Modern {
container: ContainerType::Tank,
..
} => {
if self.valve_open {
self.release_gas();
}
self.state = if self.mix.react() {
GasContainerState::Reacting
} else {
GasContainerState::Idle
};
}
GasContainerLogic::Modern {
container: ContainerType::Canister,
safety_pressure,
..
} => {
self.state = if self.mix.react() {
GasContainerState::Reacting
} else {
GasContainerState::Idle
};
if self.valve_open {
self.release_gas();
}
if self.pressure() < safety_pressure {
self.valve_open = false;
}
}
GasContainerLogic::TankOld { .. } => {
unreachable!("Old tanks shouldn't call device_updated.")
}
}
}
fn release_gas(&mut self) {
if let GasContainerLogic::Modern {
release_area,
safety_pressure,
..
} = self.proto().logic
{
let pressure = self.pressure();
let environment_air_pressure = 0.;
let delta_p = pressure - environment_air_pressure;
assert!(delta_p.is_sign_positive());
if pressure >= safety_pressure {
let volume = ATMOS_TICKRATE
* release_area
* (2. * delta_p * self.volume() / self.mix.get_mass()).sqrt();
let moles_needed = delta_p * volume / (R * self.mix.temperature());
self.mix.remove(moles_needed);
}
}
}
}
fn intensity_to_radius(total_intensity: f32, slope: f32, max_intensity: f32) -> f32 {
let r0 = max_intensity / slope;
let v0 = slope * PI / 3. * r0.powi(3);
if total_intensity < v0 {
(3. * total_intensity / (slope * PI)).cbrt()
} else {
r0 * ((12. * total_intensity / v0 - 3.).sqrt() / 6. + 0.5)
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum GasContainerState {
Reacting,
Idle,
Exploded {
radius: f32,
},
Ruptured,
}