1use std::{collections::HashSet, fmt};
13
14use uuid::Uuid;
15
16use super::{CombatEvent, CombatState, Combatant, LoggedEvent};
17use crate::HitPoints;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21pub enum Vitality {
22 Down,
23 Bloodied,
25 Hurt,
26 Unhurt,
27}
28
29impl Vitality {
30 pub fn of(hp: HitPoints) -> Self {
32 match hp.current {
33 0 => Vitality::Down,
34 current if u32::from(current) * 2 <= u32::from(hp.max) => Vitality::Bloodied,
35 current if current < hp.max => Vitality::Hurt,
36 _ => Vitality::Unhurt,
37 }
38 }
39
40 pub fn hit_points(self) -> HitPoints {
42 HitPoints {
43 max: 3,
44 current: self as u16,
45 temporary: 0,
46 }
47 }
48
49 pub fn from_concealed(hp: HitPoints) -> Self {
51 match hp.current {
52 0 => Vitality::Down,
53 1 => Vitality::Bloodied,
54 2 => Vitality::Hurt,
55 _ => Vitality::Unhurt,
56 }
57 }
58}
59
60impl fmt::Display for Vitality {
61 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
62 f.write_str(match self {
63 Vitality::Down => "down",
64 Vitality::Bloodied => "bloodied",
65 Vitality::Hurt => "hurt",
66 Vitality::Unhurt => "unhurt",
67 })
68 }
69}
70
71fn behind_the_screen(combatant: &Combatant) -> bool {
74 combatant.character_id.is_none()
75}
76
77fn conceal(combatant: &mut Combatant) {
78 if behind_the_screen(combatant) && !combatant.concealed {
79 combatant.hit_points = Vitality::of(combatant.hit_points).hit_points();
80 combatant.armor_class = None;
81 combatant.concealed = true;
82 }
83}
84
85pub struct Concealer {
89 hidden: HashSet<Uuid>,
90}
91
92impl Concealer {
93 pub fn new(state: &CombatState) -> Self {
95 let mut concealer = Self {
96 hidden: HashSet::new(),
97 };
98 concealer.learn(state.combatants.iter());
99 concealer
100 }
101
102 fn learn<'c>(&mut self, combatants: impl Iterator<Item = &'c Combatant>) {
103 self.hidden
104 .extend(combatants.filter(|c| behind_the_screen(c)).map(|c| c.id));
105 }
106
107 pub fn state(&self, state: &CombatState) -> CombatState {
109 let mut state = state.clone();
110 state.combatants.iter_mut().for_each(conceal);
111 state
112 }
113
114 pub fn event(&mut self, event: &LoggedEvent) -> LoggedEvent {
116 let mut event = event.clone();
117 match &mut event.event {
118 CombatEvent::CombatantAdded { combatant }
119 | CombatEvent::CombatantRestored { combatant } => {
120 self.learn(std::iter::once(&*combatant));
121 conceal(combatant);
122 }
123 CombatEvent::Damaged {
124 combatant,
125 hit_points,
126 ..
127 }
128 | CombatEvent::Healed {
129 combatant,
130 hit_points,
131 ..
132 }
133 | CombatEvent::TemporaryHitPointsSet {
134 combatant,
135 hit_points,
136 } if self.hidden.contains(combatant) => {
137 *hit_points = Vitality::of(*hit_points).hit_points();
138 }
139 _ => {}
140 }
141 event
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use chrono::Utc;
148
149 use super::*;
150
151 fn logged(seq: u64, event: CombatEvent) -> LoggedEvent {
152 LoggedEvent {
153 seq,
154 command_id: None,
155 actor: None,
156 at: Utc::now(),
157 event,
158 }
159 }
160
161 #[test]
162 fn players_see_how_hurt_monsters_look_and_everything_about_the_party() {
163 use Vitality::{Bloodied, Down, Hurt, Unhurt};
164
165 let mut goblin = Combatant::new("Goblin", 12);
166 goblin.armor_class = Some(15);
167 let mut vex = Combatant::new("Vex", 30);
168 vex.character_id = Some(Uuid::new_v4());
169 let mut state = CombatState::default();
170 state.apply(&CombatEvent::CombatantAdded {
171 combatant: vex.clone(),
172 });
173 let mut concealer = Concealer::new(&state);
174 let mut seen = concealer.state(&state);
175
176 let hit = |current| HitPoints {
177 max: 12,
178 current,
179 temporary: 0,
180 };
181 let events = [
182 CombatEvent::CombatantAdded {
183 combatant: goblin.clone(),
184 },
185 CombatEvent::Damaged {
186 combatant: goblin.id,
187 amount: 3,
188 hit_points: hit(9),
189 },
190 CombatEvent::Damaged {
191 combatant: goblin.id,
192 amount: 3,
193 hit_points: hit(6),
194 },
195 CombatEvent::Damaged {
196 combatant: vex.id,
197 amount: 4,
198 hit_points: HitPoints {
199 max: 30,
200 current: 26,
201 temporary: 0,
202 },
203 },
204 CombatEvent::CombatantRemoved {
205 combatant: goblin.id,
206 },
207 CombatEvent::CombatantRestored {
208 combatant: Combatant {
209 hit_points: hit(1),
210 ..goblin.clone()
211 },
212 },
213 CombatEvent::Damaged {
214 combatant: goblin.id,
215 amount: 1,
216 hit_points: hit(0),
217 },
218 ];
219 let mut looks = Vec::new();
220 for (seq, event) in events.into_iter().enumerate() {
221 let shown = concealer.event(&logged(seq as u64, event));
222 seen.apply(&shown.event);
223 if let Some(g) = seen.combatant(goblin.id) {
224 assert!(g.concealed && g.armor_class.is_none());
225 looks.push(g.vitality());
226 }
227 }
228 assert_eq!(looks, [Unhurt, Hurt, Bloodied, Bloodied, Bloodied, Down]);
229 assert!(seen.combatant(goblin.id).unwrap().is_down());
230 let vex_seen = seen.combatant(vex.id).unwrap();
231 assert!(!vex_seen.concealed);
232 assert_eq!(vex_seen.hit_points.current, 26);
233 }
234
235 #[test]
236 fn bloodied_is_half_or_fewer() {
237 let hp = |current, max| HitPoints {
238 max,
239 current,
240 temporary: 0,
241 };
242 assert_eq!(Vitality::of(hp(7, 7)), Vitality::Unhurt);
243 assert_eq!(Vitality::of(hp(4, 7)), Vitality::Hurt);
244 assert_eq!(Vitality::of(hp(3, 7)), Vitality::Bloodied);
245 assert_eq!(Vitality::of(hp(0, 7)), Vitality::Down);
246 for v in [
247 Vitality::Down,
248 Vitality::Bloodied,
249 Vitality::Hurt,
250 Vitality::Unhurt,
251 ] {
252 assert_eq!(Vitality::from_concealed(v.hit_points()), v);
253 }
254 }
255}