use super::{
BattleAction, BattleProvider, BattleRng, BattleState, BattlerRef, BattlerState as Battler,
EffectResult, MoveGate, OrderKey,
};
use std::fmt;
pub enum TurnEvent<P: BattleProvider + ?Sized> {
MoveUsed {
who: BattlerRef,
move_: P::Move,
},
ActionPrevented {
who: BattlerRef,
reason: EffectResult,
},
Missed {
who: BattlerRef,
target: BattlerRef,
},
Damage {
who: BattlerRef,
target: BattlerRef,
amount: u16,
critical: bool,
effectiveness: f32,
},
Faint {
who: BattlerRef,
},
Switched {
side: u8,
to_slot: usize,
},
Residual {
result: EffectResult,
},
Effect {
result: EffectResult,
},
}
impl<P: BattleProvider + ?Sized> fmt::Debug for TurnEvent<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TurnEvent::MoveUsed { who, move_ } => f
.debug_struct("MoveUsed")
.field("who", who)
.field("move_", move_)
.finish(),
TurnEvent::ActionPrevented { who, reason } => f
.debug_struct("ActionPrevented")
.field("who", who)
.field("reason", reason)
.finish(),
TurnEvent::Missed { who, target } => f
.debug_struct("Missed")
.field("who", who)
.field("target", target)
.finish(),
TurnEvent::Damage {
who,
target,
amount,
critical,
effectiveness,
} => f
.debug_struct("Damage")
.field("who", who)
.field("target", target)
.field("amount", amount)
.field("critical", critical)
.field("effectiveness", effectiveness)
.finish(),
TurnEvent::Faint { who } => f.debug_struct("Faint").field("who", who).finish(),
TurnEvent::Switched { side, to_slot } => f
.debug_struct("Switched")
.field("side", side)
.field("to_slot", to_slot)
.finish(),
TurnEvent::Residual { result } => {
f.debug_struct("Residual").field("result", result).finish()
}
TurnEvent::Effect { result } => {
f.debug_struct("Effect").field("result", result).finish()
}
}
}
}
impl<P: BattleProvider + ?Sized> PartialEq for TurnEvent<P>
where
P::Move: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
use TurnEvent::*;
match (self, other) {
(MoveUsed { who: a, move_: ma }, MoveUsed { who: b, move_: mb }) => a == b && ma == mb,
(ActionPrevented { who: a, reason: ra }, ActionPrevented { who: b, reason: rb }) => {
a == b && ra == rb
}
(Missed { who: a, target: ta }, Missed { who: b, target: tb }) => a == b && ta == tb,
(
Damage {
who: a,
target: ta,
amount: am,
critical: ca,
effectiveness: ea,
},
Damage {
who: b,
target: tb,
amount: bm,
critical: cb,
effectiveness: eb,
},
) => a == b && ta == tb && am == bm && ca == cb && ea == eb,
(Faint { who: a }, Faint { who: b }) => a == b,
(Switched { side: a, to_slot: ta }, Switched { side: b, to_slot: tb }) => {
a == b && ta == tb
}
(Residual { result: a }, Residual { result: b }) => a == b,
(Effect { result: a }, Effect { result: b }) => a == b,
_ => false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BattleEnd {
PlayerWin,
PlayerLoss,
Fled,
Caught,
}
pub struct TurnOutcome<P: BattleProvider + ?Sized> {
pub events: Vec<TurnEvent<P>>,
pub battle_over: Option<BattleEnd>,
}
impl<P: BattleProvider + ?Sized> fmt::Debug for TurnOutcome<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TurnOutcome")
.field("events", &self.events)
.field("battle_over", &self.battle_over)
.finish()
}
}
pub struct BattleDriver;
impl BattleDriver {
pub fn execute_turn<P: BattleProvider>(
provider: &P,
state: &mut BattleState<P>,
actions: [BattleAction<P>; 2],
rng: &mut dyn BattleRng,
) -> TurnOutcome<P> {
let mut events: Vec<TurnEvent<P>> = Vec::new();
let [player_action, opponent_action] = actions;
let actors = [
(BattlerRef::PLAYER, player_action),
(BattlerRef::OPPONENT, opponent_action),
];
let mut keyed: Vec<(OrderKey, usize)> = actors
.iter()
.enumerate()
.map(|(idx, (who, action))| (provider.turn_order_key(state, *who, action, rng), idx))
.collect();
keyed.sort_by(|a, b| a.0.cmp(&b.0));
for (_key, idx) in keyed {
let (who, action) = &actors[idx];
if Self::side_all_fainted(state, who.side) {
continue;
}
Self::resolve_action(provider, state, *who, action, rng, &mut events);
if let Some(battle_over) = Self::detect_end(state) {
return TurnOutcome {
events,
battle_over: Some(battle_over),
};
}
}
for result in provider.end_of_turn(state, rng) {
events.push(TurnEvent::Residual { result });
}
Self::push_faints(state, &mut events);
state.turn_count = state.turn_count.saturating_add(1);
let battle_over = Self::detect_end(state);
TurnOutcome {
events,
battle_over,
}
}
fn resolve_action<P: BattleProvider>(
provider: &P,
state: &mut BattleState<P>,
who: BattlerRef,
action: &BattleAction<P>,
rng: &mut dyn BattleRng,
events: &mut Vec<TurnEvent<P>>,
) {
let gate = provider.before_move(state, who, action, rng);
let effective_action: BattleAction<P> = match gate {
MoveGate::Acts => action.clone(),
MoveGate::Prevented(reason) => {
events.push(TurnEvent::ActionPrevented { who, reason });
return;
}
MoveGate::ForcedAction(forced) => forced,
};
match effective_action {
BattleAction::Fight { move_ } => {
Self::resolve_fight(provider, state, who, move_, rng, events);
}
BattleAction::Switch { to_slot } => {
if Self::apply_switch(state, who.side, to_slot) {
events.push(TurnEvent::Switched {
side: who.side,
to_slot,
});
}
}
BattleAction::UseItem { .. } | BattleAction::Run | BattleAction::Nothing => {}
}
}
fn resolve_fight<P: BattleProvider>(
provider: &P,
state: &mut BattleState<P>,
who: BattlerRef,
move_: P::Move,
rng: &mut dyn BattleRng,
events: &mut Vec<TurnEvent<P>>,
) {
let target = Self::opposing(who);
events.push(TurnEvent::MoveUsed {
who,
move_: move_.clone(),
});
if !provider.accuracy_check(state, who, target, &move_, rng) {
events.push(TurnEvent::Missed { who, target });
return;
}
let critical = provider.roll_critical(state, who, target, &move_, rng);
let random = rng.next_u8();
let (Some(attacker), Some(defender)) = (
Self::battler(state, who).cloned(),
Self::battler(state, target).cloned(),
) else {
return;
};
let dmg = provider.calculate_damage(&move_, &attacker, &defender, random, critical);
if dmg.is_miss {
events.push(TurnEvent::Missed { who, target });
return;
}
if let Some(def_mut) = Self::battler_mut(state, target) {
def_mut.take_damage(dmg.damage);
}
events.push(TurnEvent::Damage {
who,
target,
amount: dmg.damage,
critical,
effectiveness: dmg.effectiveness,
});
if let Some(def) = Self::battler(state, target) {
if def.hp == 0 {
events.push(TurnEvent::Faint { who: target });
}
}
}
pub fn apply_switch<P: BattleProvider>(
state: &mut BattleState<P>,
side: u8,
to_slot: usize,
) -> bool {
let party = match side {
0 => &mut state.player_battlers,
_ => &mut state.opponent_battlers,
};
if to_slot == 0 || to_slot >= party.len() || party[to_slot].hp == 0 {
return false;
}
party.swap(0, to_slot);
true
}
fn opposing(who: BattlerRef) -> BattlerRef {
BattlerRef::new(if who.side == 0 { 1 } else { 0 }, who.slot)
}
fn battler<P: BattleProvider>(
state: &BattleState<P>,
who: BattlerRef,
) -> Option<&Battler<P>> {
let party = if who.side == 0 {
&state.player_battlers
} else {
&state.opponent_battlers
};
party.get(who.slot as usize)
}
fn battler_mut<P: BattleProvider>(
state: &mut BattleState<P>,
who: BattlerRef,
) -> Option<&mut Battler<P>> {
let party = if who.side == 0 {
&mut state.player_battlers
} else {
&mut state.opponent_battlers
};
party.get_mut(who.slot as usize)
}
fn side_all_fainted<P: BattleProvider>(state: &BattleState<P>, side: u8) -> bool {
let party = if side == 0 {
&state.player_battlers
} else {
&state.opponent_battlers
};
party.is_empty() || party.iter().all(|b| b.hp == 0)
}
fn push_faints<P: BattleProvider>(state: &BattleState<P>, events: &mut Vec<TurnEvent<P>>) {
for who in [BattlerRef::PLAYER, BattlerRef::OPPONENT] {
if let Some(b) = Self::battler(state, who) {
if b.hp == 0 {
let already = events
.iter()
.any(|e| matches!(e, TurnEvent::Faint { who: w } if *w == who));
if !already {
events.push(TurnEvent::Faint { who });
}
}
}
}
}
fn detect_end<P: BattleProvider>(state: &BattleState<P>) -> Option<BattleEnd> {
let player_wiped =
!state.player_battlers.is_empty() && state.player_battlers.iter().all(|b| b.hp == 0);
let opponent_wiped = !state.opponent_battlers.is_empty()
&& state.opponent_battlers.iter().all(|b| b.hp == 0);
match (player_wiped, opponent_wiped) {
(_, true) => Some(BattleEnd::PlayerWin),
(true, false) => Some(BattleEnd::PlayerLoss),
(false, false) => None,
}
}
}