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