Skip to main content

subtr_actor/stats/calculators/
player_vertical_state.rs

1use super::*;
2
3pub const PLAYER_GROUND_Z_THRESHOLD: f32 = 20.0;
4pub const PLAYER_HIGH_AIR_Z_THRESHOLD: f32 = 642.775 + BALL_RADIUS_Z;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum PlayerVerticalBand {
8    Ground,
9    LowAir,
10    HighAir,
11}
12
13pub const ALL_PLAYER_VERTICAL_BANDS: [PlayerVerticalBand; 3] = [
14    PlayerVerticalBand::Ground,
15    PlayerVerticalBand::LowAir,
16    PlayerVerticalBand::HighAir,
17];
18
19impl PlayerVerticalBand {
20    pub fn from_height(height: f32) -> Self {
21        if height <= PLAYER_GROUND_Z_THRESHOLD {
22            Self::Ground
23        } else if height >= PLAYER_HIGH_AIR_Z_THRESHOLD {
24            Self::HighAir
25        } else {
26            Self::LowAir
27        }
28    }
29
30    pub fn as_label(self) -> StatLabel {
31        let value = match self {
32            Self::Ground => "ground",
33            Self::LowAir => "low_air",
34            Self::HighAir => "high_air",
35        };
36        StatLabel::new("height_band", value)
37    }
38
39    pub fn is_grounded(self) -> bool {
40        matches!(self, Self::Ground)
41    }
42
43    pub fn is_airborne(self) -> bool {
44        !self.is_grounded()
45    }
46
47    pub fn is_high_air(self) -> bool {
48        matches!(self, Self::HighAir)
49    }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct PlayerVerticalSample {
54    pub height: f32,
55    pub band: PlayerVerticalBand,
56}
57
58impl PlayerVerticalSample {
59    pub fn from_height(height: f32) -> Self {
60        Self {
61            height,
62            band: PlayerVerticalBand::from_height(height),
63        }
64    }
65}
66
67#[derive(Debug, Clone, Default)]
68pub struct PlayerVerticalState {
69    pub players: HashMap<PlayerId, PlayerVerticalSample>,
70}
71
72impl PlayerVerticalState {
73    pub fn sample(&self, player_id: &PlayerId) -> Option<&PlayerVerticalSample> {
74        self.players.get(player_id)
75    }
76
77    pub fn band_for_player(&self, player_id: &PlayerId) -> Option<PlayerVerticalBand> {
78        self.sample(player_id).map(|sample| sample.band)
79    }
80
81    pub fn is_grounded(&self, player_id: &PlayerId) -> bool {
82        self.band_for_player(player_id)
83            .is_some_and(PlayerVerticalBand::is_grounded)
84    }
85}
86
87#[derive(Default)]
88pub struct PlayerVerticalStateCalculator;
89
90impl PlayerVerticalStateCalculator {
91    pub fn new() -> Self {
92        Self
93    }
94
95    pub fn update(&mut self, players: &PlayerFrameState) -> PlayerVerticalState {
96        let players = players
97            .players
98            .iter()
99            .filter_map(|player| {
100                let height = player.position()?.z;
101                Some((
102                    player.player_id.clone(),
103                    PlayerVerticalSample::from_height(height),
104                ))
105            })
106            .collect();
107
108        PlayerVerticalState { players }
109    }
110}