subtr_actor/stats/calculators/
ball_third.rs1use super::*;
2
3const DEFAULT_BALL_THIRD_BOUNDARY_Y: f32 = FIELD_ZONE_BOUNDARY_Y;
4
5#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9#[allow(clippy::enum_variant_names)]
10pub(crate) enum BallThirdLabel {
11 TeamZeroThird,
12 #[default]
13 NeutralThird,
14 TeamOneThird,
15}
16
17impl BallThirdLabel {
18 pub(crate) fn from_ball(ball: &BallSample) -> Self {
19 Self::from_y(ball.position().y, DEFAULT_BALL_THIRD_BOUNDARY_Y)
20 }
21
22 pub(crate) fn from_y(ball_y: f32, boundary_y: f32) -> Self {
23 if ball_y < -boundary_y {
24 Self::TeamZeroThird
25 } else if ball_y > boundary_y {
26 Self::TeamOneThird
27 } else {
28 Self::NeutralThird
29 }
30 }
31
32 pub(crate) fn as_label_value(self) -> &'static str {
33 match self {
34 Self::TeamZeroThird => "team_zero_third",
35 Self::NeutralThird => "neutral_third",
36 Self::TeamOneThird => "team_one_third",
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
42#[ts(export)]
43pub struct BallThirdEvent {
44 pub time: f32,
45 pub frame: usize,
46 pub end_time: f32,
47 pub end_frame: usize,
48 pub active: bool,
49 pub duration: f32,
50 pub field_third: String,
51}
52
53impl BallThirdEvent {
54 fn absorb_duration(&mut self, frame: &FrameInfo, duration: f32) {
55 self.end_time = frame.time;
56 self.end_frame = frame.frame_number;
57 self.duration += duration;
58 }
59}
60
61#[derive(Debug, Clone, PartialEq)]
62pub struct BallThirdCalculatorConfig {
63 pub boundary_y: f32,
64}
65
66impl Default for BallThirdCalculatorConfig {
67 fn default() -> Self {
68 Self {
69 boundary_y: DEFAULT_BALL_THIRD_BOUNDARY_Y,
70 }
71 }
72}
73
74#[derive(Debug, Clone, Default, PartialEq)]
75pub struct BallThirdCalculator {
76 config: BallThirdCalculatorConfig,
77 events: EventStream<BallThirdEvent>,
78 last_emitted_event_state: Option<BallThirdEventState>,
79 pending_event: Option<PendingBallThirdEvent>,
80}
81
82#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
83struct BallThirdEventState {
84 active: bool,
85 field_third: BallThirdLabel,
86}
87
88#[derive(Debug, Clone, PartialEq)]
89struct PendingBallThirdEvent {
90 state: BallThirdEventState,
91 event: BallThirdEvent,
92}
93
94impl BallThirdCalculator {
95 pub fn new() -> Self {
96 Self::with_config(BallThirdCalculatorConfig::default())
97 }
98
99 pub fn with_config(config: BallThirdCalculatorConfig) -> Self {
100 Self {
101 config,
102 ..Self::default()
103 }
104 }
105
106 pub fn events(&self) -> &[BallThirdEvent] {
107 self.events.all()
108 }
109
110 pub fn new_events(&self) -> &[BallThirdEvent] {
111 self.events.new_events()
112 }
113
114 pub fn projected_events(&self) -> Vec<BallThirdEvent> {
115 let mut events = self.events.all().to_vec();
116 if let Some(pending) = &self.pending_event {
117 events.push(pending.event.clone());
118 }
119 events
120 }
121
122 pub fn flush_pending_event(&mut self) {
123 let Some(pending) = self.pending_event.take() else {
124 return;
125 };
126 self.events.push(pending.event);
127 }
128
129 pub fn current_event(&self) -> Option<&BallThirdEvent> {
132 self.pending_event
133 .as_ref()
134 .map(|pending| &pending.event)
135 .or_else(|| self.events.all().last())
136 }
137
138 pub fn config(&self) -> &BallThirdCalculatorConfig {
139 &self.config
140 }
141
142 fn emit_event_if_changed(
143 &mut self,
144 frame: &FrameInfo,
145 active: bool,
146 duration: f32,
147 field_third: BallThirdLabel,
148 ) {
149 let event_state = BallThirdEventState {
150 active,
151 field_third,
152 };
153 if self.last_emitted_event_state == Some(event_state) && duration == 0.0 {
154 return;
155 }
156 let event = BallThirdEvent {
157 time: frame.time,
158 frame: frame.frame_number,
159 end_time: frame.time,
160 end_frame: frame.frame_number,
161 active,
162 duration,
163 field_third: field_third.as_label_value().to_owned(),
164 };
165 self.record_event(event_state, frame, event);
166 self.last_emitted_event_state = Some(event_state);
167 }
168
169 fn record_event(
170 &mut self,
171 state: BallThirdEventState,
172 frame: &FrameInfo,
173 event: BallThirdEvent,
174 ) {
175 let Some(pending) = self.pending_event.as_mut() else {
176 self.pending_event = Some(PendingBallThirdEvent { state, event });
177 return;
178 };
179
180 if pending.state == state {
181 pending.event.absorb_duration(frame, event.duration);
182 } else {
183 let previous = self
184 .pending_event
185 .replace(PendingBallThirdEvent { state, event });
186 let Some(previous) = previous else {
187 return;
188 };
189 self.events.push(previous.event);
190 }
191 }
192
193 pub fn update(
194 &mut self,
195 frame: &FrameInfo,
196 ball: &BallFrameState,
197 live_play_state: &LivePlayState,
198 ) -> SubtrActorResult<()> {
199 self.events.begin_update();
200 if !live_play_state.is_live_play {
201 self.emit_event_if_changed(frame, false, 0.0, BallThirdLabel::NeutralThird);
202 return Ok(());
203 }
204 if let Some(ball) = ball.sample() {
205 let third = BallThirdLabel::from_y(ball.position().y, self.config.boundary_y);
206 self.emit_event_if_changed(frame, true, frame.dt, third);
207 } else {
208 self.emit_event_if_changed(frame, false, 0.0, BallThirdLabel::NeutralThird);
209 }
210 Ok(())
211 }
212}