1use uuid::Uuid;
21
22use super::{CombatCommand, CombatError, CombatState, Combatant};
23
24pub fn player_may(
28 state: &CombatState,
29 command: &CombatCommand,
30 own: impl Fn(&Combatant) -> bool,
31) -> Result<(), CombatError> {
32 let mine = |ids: &mut dyn Iterator<Item = &Uuid>| -> Result<(), CombatError> {
33 for id in ids {
34 if let Some(combatant) = state.combatant(*id)
35 && !own(combatant)
36 {
37 return Err(CombatError::NotYours(combatant.name.clone()));
38 }
39 }
40 Ok(())
41 };
42 match command {
43 CombatCommand::Damage { combatant, .. }
44 | CombatCommand::Heal { combatant, .. }
45 | CombatCommand::SetTemporaryHitPoints { combatant, .. }
46 | CombatCommand::UseFeature { combatant, .. } => mine(&mut std::iter::once(combatant)),
47 CombatCommand::CastSpell {
50 caster,
51 concentration: Some(_),
52 ..
53 } => mine(&mut std::iter::once(caster)),
54 CombatCommand::CastSpell {
55 caster, targets, ..
56 } => mine(&mut std::iter::once(caster).chain(targets)),
57 CombatCommand::AddEffect { effect } => match effect.source {
58 Some(source) if effect.concentration => mine(&mut std::iter::once(&source)),
59 _ => mine(&mut effect.source.iter().chain(&effect.targets)),
60 },
61 CombatCommand::RemoveEffect { effect } => match state.effect(*effect) {
62 Some(effect) => match effect.source {
63 Some(source) if effect.concentration => mine(&mut std::iter::once(&source)),
64 _ => mine(&mut effect.source.iter().chain(&effect.targets)),
65 },
66 None => Ok(()),
67 },
68 CombatCommand::NextTurn => match state.current_turn.and_then(|id| state.combatant(id)) {
69 Some(current) if own(current) => Ok(()),
70 _ => Err(CombatError::NotYourTurn),
71 },
72 CombatCommand::Undo { .. } => Ok(()),
74 CombatCommand::AddCombatant { .. }
75 | CombatCommand::AddMonsters { .. }
76 | CombatCommand::RemoveCombatant { .. }
77 | CombatCommand::RollInitiative { .. }
78 | CombatCommand::RollInitiativeForAll
79 | CombatCommand::SetInitiative { .. }
80 | CombatCommand::Start
81 | CombatCommand::Pause
82 | CombatCommand::Resume
83 | CombatCommand::End => Err(CombatError::DmOnly),
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::combat::{CombatEvent, NewEffect};
91
92 fn fight() -> (CombatState, [Uuid; 3]) {
95 let mut state = CombatState::default();
96 let mut ids = [Uuid::nil(); 3];
97 for (i, name) in ["Vex", "Grog", "Goblin"].into_iter().enumerate() {
98 let mut combatant = Combatant::new(name, 10);
99 combatant.initiative = Some(20 - i16::try_from(i).unwrap());
100 ids[i] = combatant.id;
101 state.apply(&CombatEvent::CombatantAdded { combatant });
102 }
103 state.apply(&CombatEvent::Started);
104 state.apply(&CombatEvent::TurnStarted { combatant: ids[0] });
105 (state, ids)
106 }
107
108 #[expect(
109 clippy::needless_pass_by_value,
110 reason = "tests read better passing commands by value"
111 )]
112 fn vex_may(state: &CombatState, command: CombatCommand) -> Result<(), CombatError> {
113 player_may(state, &command, |c| c.name == "Vex")
114 }
115
116 #[test]
117 fn players_change_only_their_own_combatants() {
118 let (state, [vex, grog, goblin]) = fight();
119 let damage = |combatant| CombatCommand::Damage {
120 combatant,
121 amount: 3,
122 };
123 assert_eq!(vex_may(&state, damage(vex)), Ok(()));
124 assert_eq!(
125 vex_may(&state, damage(goblin)),
126 Err(CombatError::NotYours("Goblin".into()))
127 );
128 let cast = |targets| CombatCommand::CastSpell {
129 caster: vex,
130 spell: "Cure Wounds".into(),
131 slot: None,
132 targets,
133 concentration: None,
134 };
135 assert_eq!(vex_may(&state, cast(vec![vex])), Ok(()));
136 assert_eq!(
137 vex_may(&state, cast(vec![vex, grog])),
138 Err(CombatError::NotYours("Grog".into()))
139 );
140 let effect = |source, targets| CombatCommand::AddEffect {
141 effect: NewEffect {
142 id: Uuid::new_v4(),
143 name: "Blessed".into(),
144 source,
145 targets,
146 concentration: false,
147 rounds: None,
148 },
149 };
150 assert_eq!(vex_may(&state, effect(None, vec![vex])), Ok(()));
151 assert!(vex_may(&state, effect(Some(vex), vec![goblin])).is_err());
152 assert!(vex_may(&state, effect(Some(grog), vec![vex])).is_err());
153 }
154
155 #[test]
156 fn players_concentrate_on_anyone_but_only_for_themselves() {
157 let (mut state, [vex, grog, goblin]) = fight();
158 let concentrate = |source, targets| CombatCommand::AddEffect {
159 effect: NewEffect {
160 id: Uuid::new_v4(),
161 name: "Hunter's Mark".into(),
162 source,
163 targets,
164 concentration: true,
165 rounds: None,
166 },
167 };
168 assert_eq!(
169 vex_may(&state, concentrate(Some(vex), vec![goblin])),
170 Ok(())
171 );
172 assert!(vex_may(&state, concentrate(Some(grog), vec![goblin])).is_err());
173 assert!(vex_may(&state, concentrate(None, vec![goblin])).is_err());
174 let bless = |concentration| CombatCommand::CastSpell {
175 caster: vex,
176 spell: "Bless".into(),
177 slot: None,
178 targets: vec![vex, grog],
179 concentration,
180 };
181 assert_eq!(vex_may(&state, bless(Some(Uuid::new_v4()))), Ok(()));
182 assert_eq!(
183 vex_may(&state, bless(None)),
184 Err(CombatError::NotYours("Grog".into()))
185 );
186
187 let mark = |id, source| CombatEvent::EffectAdded {
189 effect: crate::combat::Effect {
190 id,
191 name: "Mark".into(),
192 source: Some(source),
193 targets: vec![goblin],
194 concentration: true,
195 expires: None,
196 },
197 };
198 let (hers, his) = (Uuid::new_v4(), Uuid::new_v4());
199 state.apply(&mark(hers, vex));
200 state.apply(&mark(his, grog));
201 let drop = |effect| CombatCommand::RemoveEffect { effect };
202 assert_eq!(vex_may(&state, drop(hers)), Ok(()));
203 assert!(vex_may(&state, drop(his)).is_err());
204 }
205
206 #[test]
207 fn players_end_only_their_own_turn_and_the_dm_runs_the_rest() {
208 let (state, [vex, ..]) = fight();
209 assert_eq!(vex_may(&state, CombatCommand::NextTurn), Ok(()));
210 let grogs_turn = |c: &Combatant| c.name == "Grog";
211 assert_eq!(
212 player_may(&state, &CombatCommand::NextTurn, grogs_turn),
213 Err(CombatError::NotYourTurn)
214 );
215 for command in [
216 CombatCommand::Start,
217 CombatCommand::End,
218 CombatCommand::RemoveCombatant { combatant: vex },
219 CombatCommand::RollInitiative {
220 combatant: vex,
221 roll: None,
222 },
223 ] {
224 assert_eq!(vex_may(&state, command), Err(CombatError::DmOnly));
225 }
226 assert_eq!(
227 vex_may(&state, CombatCommand::Undo { command: None }),
228 Ok(())
229 );
230 }
231}