use alloc::{
borrow::ToOwned,
boxed::Box,
collections::BTreeMap,
format,
string::{
String,
ToString,
},
vec::Vec,
};
use core::marker::PhantomData;
use anyhow::Result;
use battler_data::{
Boost,
BoostOrderIterator,
BoostTable,
BoostTableEntries,
CopyVolatileType,
DefaultTrueBool,
Fraction,
Id,
Identifiable,
ItemFlag,
MoveCategory,
MoveData,
MoveFlag,
MoveSource,
MoveTarget,
MultihitType,
RecoilBase,
SecondaryEffectData,
SelfDestructType,
Stat,
StatTable,
SwitchType,
Type,
ZCrystalData,
ZCrystalSource,
};
use battler_prng::rand_util;
use hashbrown::{
HashMap,
HashSet,
hash_map::Entry,
};
use crate::{
battle::{
AbilitySlot,
Action,
ActiveMoveContext,
ActiveTargetContext,
ApplyingEffectContext,
BattleQueue,
CalculateStatContext,
Context,
CoreBattle,
EffectContext,
EffectiveAbility,
EventResult,
EventResultOutput,
ExperienceAction,
FieldEffectContext,
LevelUpAction,
Mon,
MonContext,
MonExitType,
MonHandle,
MonSpecialFormeChangeType,
MoveHandle,
MoveOutcome,
MoveOutcomeOnTarget,
Player,
PlayerContext,
RecalculateStatsHpPolicy,
ReceivedAttackEntry,
Side,
SideEffectContext,
SwitchEventsAction,
core_battle_effects,
core_battle_logs,
modify_32,
mon_states,
},
common::UnsafelyDetachBorrowMut,
effect::{
AppliedEffectHandle,
AppliedEffectLocation,
EffectHandle,
LinkedEffectsManager,
NonExistentEffect,
fxlang::{
self,
EffectState,
},
},
error::{
WrapOptionError,
general_error,
integer_overflow_error,
},
moves::SecondaryEffect,
try_event,
};
#[derive(Clone, Debug)]
struct HitTargetState {
handle: MonHandle,
outcome: MoveOutcomeOnTarget,
}
impl HitTargetState {
pub fn new(handle: MonHandle, outcome: MoveOutcomeOnTarget) -> Self {
Self { handle, outcome }
}
}
fn hit_targets_state_from_targets<I>(targets: I) -> Vec<HitTargetState>
where
I: IntoIterator<Item = MonHandle>,
{
targets
.into_iter()
.map(|handle| HitTargetState::new(handle, MoveOutcomeOnTarget::Unknown))
.collect()
}
fn switch_out_internal(
context: &mut MonContext,
being_replaced_immediately: bool,
run_switch_out_events: bool,
copy_volatile_type: Option<CopyVolatileType>,
) -> Result<bool> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if context.mon().active && !context.mon().switch_state.being_called_back {
if context.mon().hp > 0 {
context.mon_mut().switch_state.being_called_back = true;
if run_switch_out_events {
if !context.mon().switch_state.skip_before_switch_out {
core_battle_effects::run_event::<MonContext, ()>(
context,
fxlang::BattleEvent::BeforeSwitchOut,
);
}
context.mon_mut().switch_state.skip_before_switch_out = true;
if context.mon().hp == 0 {
return Ok(false);
}
core_battle_effects::run_event::<MonContext, ()>(
context,
fxlang::BattleEvent::SwitchOut,
);
}
if !being_replaced_immediately {
core_battle_logs::switch_out(context, copy_volatile_type)?;
}
core_battle_effects::run_event::<MonContext, ()>(
context,
fxlang::BattleEvent::AfterSwitchOut,
);
end_ability(
&mut context.applying_effect_context(
EffectHandle::Condition(Id::from_known("switchout")),
None,
None,
)?,
true,
)?;
core_battle_effects::run_event::<MonContext, ()>(context, fxlang::BattleEvent::Exit);
}
Mon::switch_out(context)?;
}
if copy_volatile_type.is_none() {
Mon::clear_volatile(context, being_replaced_immediately)?;
}
Ok(true)
}
pub fn switch_out(
context: &mut MonContext,
copy_volatile_type: Option<CopyVolatileType>,
) -> Result<bool> {
if !Player::can_switch(context.as_player_context()) {
return Ok(false);
}
if !context.mon().active || context.mon().hp == 0 {
return Ok(false);
}
if context.mon().switch_state.being_called_back
|| context.mon().volatile_state.switched_out_this_turn
{
return Ok(false);
}
if context.mon().switch_state.needs_switch.is_none() {
context.mon_mut().switch_state.needs_switch = Some(SwitchType::Normal);
}
switch_out_internal(context, false, true, copy_volatile_type)
}
pub fn switch_in(
context: &mut MonContext,
position: usize,
mut switch_type: Option<SwitchType>,
is_drag: bool,
) -> Result<bool> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if context.mon().active {
return Ok(false);
}
let active_len = context.player().total_active_positions();
if position >= active_len {
return Err(general_error(format!(
"invalid switch position {position} / {active_len}",
)));
}
let previous_team_position = context.mon().effective_team_position;
let mon_handle = context.mon_handle();
let mut copied_volatiles = None;
if let Some(previous_mon) = context.player().active_or_exited_mon_handle(position) {
let mut context = context.as_battle_context_mut().mon_context(previous_mon)?;
if context.mon().hp > 0 {
if let Some(previous_mon_switch_type) = context.mon().switch_state.needs_switch {
switch_type = Some(previous_mon_switch_type);
}
if let Some(SwitchType::CopyVolatile(copy_volatile_type)) = switch_type {
copied_volatiles = Some((
previous_mon,
copy_volatiles_for_switch(
&mut context.as_battle_context_mut().mon_context(mon_handle)?,
previous_mon,
copy_volatile_type,
)?,
));
}
if !switch_out_internal(&mut context, true, true, None)? {
return Ok(false);
}
context.mon_mut().effective_team_position = previous_team_position;
}
}
context.mon_mut().switch_state.switching_in = true;
core_battle_effects::run_event::<MonContext, ()>(context, fxlang::BattleEvent::SwitchingIn);
Mon::switch_in(context, position)?;
core_battle_logs::switch(context, is_drag)?;
if let Some((previous_mon, copied)) = copied_volatiles {
context.mon_mut().volatile_state.boosts = copied.boosts;
copy_volatiles(
&mut context.as_battle_context_mut().applying_effect_context(
EffectHandle::Condition(Id::from_known("switchout")),
Some(previous_mon),
mon_handle,
None,
)?,
copied.volatiles,
)?;
}
if is_drag {
run_switch_in_events(context)?;
} else {
let mon_handle = context.mon_handle();
BattleQueue::insert_action_into_sorted_position(
context.as_battle_context_mut(),
Action::SwitchEvents(SwitchEventsAction::new(mon_handle)),
)?;
}
Ok(true)
}
pub fn run_before_switch_in_events(context: &mut MonContext) -> Result<()> {
core_battle_effects::run_event::<_, ()>(context, fxlang::BattleEvent::BeforeSwitchIn);
Ok(())
}
pub fn run_switch_in_events(context: &mut MonContext) -> Result<()> {
core_battle_effects::run_event::<_, ()>(context, fxlang::BattleEvent::SwitchIn);
if !context.mon().active || context.mon().hp == 0 {
return Ok(());
}
start_ability(
&mut context.applying_effect_context(
EffectHandle::Condition(Id::from_known("switchin")),
None,
None,
)?,
true,
)?;
start_item(
&mut context.applying_effect_context(
EffectHandle::Condition(Id::from_known("switchin")),
None,
None,
)?,
true,
)?;
Ok(())
}
struct CopiedVolatilesForSwitch {
boosts: BoostTable,
volatiles: HashMap<Id, EffectState>,
}
fn copy_volatiles_for_switch(
context: &mut MonContext,
source: MonHandle,
copy_volatile_type: CopyVolatileType,
) -> Result<CopiedVolatilesForSwitch> {
Mon::clear_volatile(context, true)?;
let mut source_context = context.as_battle_context_mut().mon_context(source)?;
let boosts = source_context.mon().volatile_state.boosts.clone();
let mut volatiles = HashMap::default();
for (volatile, state) in source_context.mon().volatile_state.volatiles.clone() {
if CoreBattle::get_parsed_effect_by_id(source_context.as_battle_context_mut(), &volatile)?
.is_some_and(|condition| condition.condition().no_copy)
|| (copy_volatile_type == CopyVolatileType::SubstituteOnly && &volatile != "substitute")
{
continue;
}
volatiles.insert(volatile, state);
}
Mon::clear_volatile(&mut source_context, true)?;
Ok(CopiedVolatilesForSwitch { boosts, volatiles })
}
fn copy_volatiles(
context: &mut ApplyingEffectContext,
volatiles: HashMap<Id, EffectState>,
) -> Result<()> {
for (volatile, mut state) in volatiles {
state.set_target(context.target_handle());
context
.target_mut()
.volatile_state
.volatiles
.insert(volatile.clone(), state);
let effect_handle = context
.battle_mut()
.get_effect_handle_by_id(&volatile)?
.clone();
if !*core_battle_effects::run_effect_event_with_options::<_, _, DefaultTrueBool>(
context,
fxlang::BattleEvent::CopyVolatile,
(),
core_battle_effects::RunEffectEventOptions {
effects: Vec::from_iter([AppliedEffectHandle::new(
effect_handle,
AppliedEffectLocation::MonVolatile(context.target_handle()),
)]),
},
) {
context
.target_mut()
.volatile_state
.volatiles
.remove(&volatile);
}
}
Ok(())
}
pub fn do_move_by_id(
context: &mut MonContext,
move_id: &Id,
target_location: Option<isize>,
original_target: Option<MonHandle>,
) -> Result<()> {
let mon_handle = context.mon_handle();
let active_move_handle = CoreBattle::register_active_move_by_id(
context.as_battle_context_mut(),
move_id,
mon_handle,
)?;
do_move(
context,
active_move_handle,
target_location,
original_target,
)
}
pub fn do_move_action(
context: &mut MonContext,
active_move_handle: MoveHandle,
target_location: Option<isize>,
original_target: Option<MonHandle>,
z_move: bool,
) -> Result<()> {
if z_move {
add_volatile(
&mut context.applying_effect_context(
EffectHandle::Condition(Id::from_known("playerchoice")),
None,
None,
)?,
&Id::from_known("zpower"),
false,
None,
)?;
}
do_move(
context,
active_move_handle,
target_location,
original_target,
)?;
if z_move {
context.player_mut().can_z_move = false;
}
Ok(())
}
fn do_move(
context: &mut MonContext,
active_move_handle: MoveHandle,
target_location: Option<isize>,
original_target: Option<MonHandle>,
) -> Result<()> {
do_move_internal(
context,
active_move_handle,
target_location,
original_target,
)?;
context.mon_mut().clear_active_move();
Ok(())
}
fn upgrade_move(context: &mut ActiveMoveContext) -> Result<MoveHandle> {
if let Some(upgraded_move_handle) =
core_battle_effects::run_event_with_options::<_, _, Option<MoveHandle>>(
&mut context.user_applying_effect_context(None)?,
fxlang::BattleEvent::UpgradeMove,
(),
core_battle_effects::RunEventOptions {
return_first_value: true,
..Default::default()
},
)
{
return Ok(upgraded_move_handle);
}
Ok(context.active_move_handle())
}
fn do_move_internal(
context: &mut MonContext,
active_move_handle: MoveHandle,
target_location: Option<isize>,
original_target: Option<MonHandle>,
) -> Result<()> {
if !context.mon().active || context.mon().hp == 0 {
return Ok(());
}
context.mon_mut().active_move_actions += 1;
let selected_move_id = context
.as_battle_context()
.active_move(active_move_handle)?
.id()
.clone();
let mut active_move_handle =
upgrade_move(&mut context.active_move_context(active_move_handle)?)?;
let move_id = context
.as_battle_context()
.active_move(active_move_handle)?
.id()
.clone();
let mon_handle = context.mon_handle();
let mut target = CoreBattle::get_target(
context.as_battle_context_mut(),
mon_handle,
&move_id,
target_location,
original_target,
)?;
if context
.as_battle_context()
.active_move(active_move_handle)?
.upgraded
.is_none()
{
if let Some(change_move) = core_battle_effects::run_event_with_input::<_, _, Option<String>>(
context,
fxlang::BattleEvent::OverrideMove,
move_id.to_string(),
) && &change_move != move_id.as_ref()
{
active_move_handle = CoreBattle::register_active_move_by_id(
context.as_battle_context_mut(),
&Id::from(change_move),
mon_handle,
)?;
let mon_handle = context.mon_handle();
let move_target = context
.as_battle_context()
.active_move(active_move_handle)?
.data
.target;
target = CoreBattle::random_target(
context.as_battle_context_mut(),
mon_handle,
move_target,
)?;
}
}
context.mon_mut().set_active_move(active_move_handle, false);
let mut context = context.active_move_context(active_move_handle)?;
let locked_move_before = context.mon().next_turn_state.locked_move.clone();
if locked_move_before.is_none()
&& !context.mon_mut().check_pp(&selected_move_id, 1)
&& !selected_move_id.eq("struggle")
{
core_battle_logs::cant(
context.as_mon_context_mut(),
EffectHandle::NonExistent(NonExistentEffect::new(Id::from_known("nopp"))),
None,
)?;
return Ok(());
}
if locked_move_before.is_none() {
context.mon_mut().volatile_state.last_move_selected = Some(selected_move_id.clone());
context.mon_mut().volatile_state.last_move_target_location = target_location;
}
let context = context.as_mon_context_mut();
let result = use_active_move(
context,
active_move_handle,
target,
None,
UseActiveMoveOptions::default(),
)?;
context.mon_mut().volatile_state.move_this_turn_outcome = match (
context.mon_mut().volatile_state.move_this_turn_outcome,
result.outcome(),
) {
(
left @ Some(MoveOutcome::Succeeded | MoveOutcome::Skipped),
MoveOutcome::Skipped | MoveOutcome::Failed,
) => left,
_ => Some(result.outcome()),
};
let this_move_is_the_last_selected = context
.mon()
.volatile_state
.last_move_selected
.as_ref()
.is_some_and(|last_move| last_move == &selected_move_id);
if result.used() && this_move_is_the_last_selected {
let set_last_move = core_battle_effects::run_event::<_, DefaultTrueBool>(
context,
fxlang::BattleEvent::SetLastMove,
);
if *set_last_move {
context.mon_mut().volatile_state.last_move = Some(active_move_handle);
}
}
if result.used() && this_move_is_the_last_selected && locked_move_before.is_none()
|| context
.as_battle_context()
.active_move(active_move_handle)?
.data
.flags
.contains(&MoveFlag::Charge)
{
let deduction = core_battle_effects::run_event_with_relay::<_, u8>(
&mut context
.active_move_context(active_move_handle)?
.user_applying_effect_context(target)?,
fxlang::BattleEvent::DeductPp,
1,
);
if deduction > 0 {
context.mon_mut().deduct_pp(&selected_move_id, deduction);
}
}
Ok(())
}
pub fn use_move(
context: &mut MonContext,
move_id: &Id,
target: Option<MonHandle>,
source_effect: Option<&EffectHandle>,
external: bool,
) -> Result<bool> {
let mon_handle = context.mon_handle();
let active_move_handle = CoreBattle::register_active_move_by_id(
context.as_battle_context_mut(),
move_id,
mon_handle,
)?;
let active_move_handle = match source_effect {
Some(EffectHandle::ActiveMove(source_move_handle, _)) => {
if context
.as_battle_context()
.active_move(*source_move_handle)?
.upgraded
.is_some()
{
upgrade_move(&mut context.active_move_context(active_move_handle)?)?
} else {
active_move_handle
}
}
_ => active_move_handle,
};
use_active_move(
context,
active_move_handle,
target,
source_effect,
UseActiveMoveOptions {
external,
directly_used: true,
..Default::default()
},
)
.map(|result| result.outcome().succeeded())
}
#[derive(Debug, Clone)]
pub struct UseActiveMoveOptions {
pub external: bool,
pub directly_used: bool,
pub preventable: Option<bool>,
}
impl UseActiveMoveOptions {
pub fn preventable(&self) -> bool {
self.preventable
.unwrap_or_else(|| !self.external && self.directly_used)
}
}
impl Default for UseActiveMoveOptions {
fn default() -> Self {
Self {
external: false,
directly_used: true,
preventable: None,
}
}
}
#[derive(Debug, Clone)]
pub struct UseActiveMoveResult {
used: bool,
outcome: MoveOutcome,
}
impl UseActiveMoveResult {
pub fn new_used(outcome: MoveOutcome) -> Self {
Self {
used: true,
outcome,
}
}
pub fn new_unused(outcome: MoveOutcome) -> Self {
Self {
used: false,
outcome,
}
}
pub fn used(&self) -> bool {
self.used
}
pub fn outcome(&self) -> MoveOutcome {
self.outcome
}
}
pub fn run_in_using_move_state<F, T>(context: &mut MonContext, f: F) -> T
where
F: FnOnce(&mut MonContext) -> T,
{
let not_using_move_at_start = !context.mon().volatile_state.using_move;
if not_using_move_at_start {
context.mon_mut().volatile_state.using_move = true;
core_battle_effects::run_event::<_, ()>(context, fxlang::BattleEvent::StartUsingMove);
}
let result = f(context);
if not_using_move_at_start {
core_battle_effects::run_event::<_, ()>(context, fxlang::BattleEvent::StopUsingMove);
context.mon_mut().volatile_state.using_move = false;
}
result
}
pub fn use_active_move(
context: &mut MonContext,
active_move_handle: MoveHandle,
target: Option<MonHandle>,
source_effect: Option<&EffectHandle>,
options: UseActiveMoveOptions,
) -> Result<UseActiveMoveResult> {
run_in_using_move_state(context, |context| {
use_active_move_with_using_move_state(
context,
active_move_handle,
target,
source_effect,
options,
)
})
}
fn use_active_move_with_using_move_state(
context: &mut MonContext,
active_move_handle: MoveHandle,
target: Option<MonHandle>,
source_effect: Option<&EffectHandle>,
options: UseActiveMoveOptions,
) -> Result<UseActiveMoveResult> {
if options.directly_used && (!context.mon().active || context.mon().hp == 0) {
return Ok(UseActiveMoveResult::new_unused(MoveOutcome::Failed));
}
if options.directly_used {
context.mon_mut().volatile_state.move_this_turn_outcome = None;
context
.mon_mut()
.set_active_move(active_move_handle, options.external);
}
let mut context = context.active_move_context(active_move_handle)?;
let mut effect_state = context.active_move().effect_state.clone();
effect_state.initialize(context.as_battle_context_mut(), source_effect, target, None)?;
context.active_move_mut().effect_state = effect_state;
context.active_move_mut().external = options.external;
match source_effect {
Some(EffectHandle::ActiveMove(source_move_handle, _)) => {
let priority = context
.as_battle_context()
.active_move(*source_move_handle)?
.priority;
context.active_move_mut().priority = priority;
}
_ => (),
}
if options.preventable() {
if let Some(locked_move) = context.mon().next_turn_state.locked_move.clone()
&& context.active_move().id() != locked_move.as_str()
{
let effect = context
.battle_mut()
.get_effect_handle_by_id(&Id::from(locked_move.as_str()))?
.clone();
core_battle_logs::cant(context.as_mon_context_mut(), effect, None)?;
return Ok(UseActiveMoveResult::new_unused(MoveOutcome::Skipped));
}
let before_move_result = core_battle_effects::run_event::<_, EventResult>(
&mut context.user_applying_effect_context(None)?,
fxlang::BattleEvent::BeforeMove,
)
.and_then(|| {
core_battle_effects::run_active_move_event::<EventResult>(
&mut context,
fxlang::BattleEvent::BeforeMove,
core_battle_effects::MoveTargetForEvent::User,
)
});
if !before_move_result.advance() {
core_battle_effects::run_active_move_event::<()>(
&mut context,
fxlang::BattleEvent::MoveAborted,
core_battle_effects::MoveTargetForEvent::None,
);
core_battle_effects::run_event::<_, ()>(
&mut context.user_applying_effect_context(None)?,
fxlang::BattleEvent::MoveAborted,
);
return Ok(UseActiveMoveResult::new_unused(before_move_result.into()));
}
}
if options.directly_used {
context.mon_mut().volatile_state.last_move_used = Some(context.active_move_handle());
}
let target = core_battle_effects::run_event_with_options::<_, _, Option<MonHandle>>(
&mut context.user_applying_effect_context(target)?,
fxlang::BattleEvent::ModifyTarget,
target,
core_battle_effects::RunEventOptions {
return_first_value: true,
..Default::default()
},
)
.or(target);
let outcome = use_active_move_internal(&mut context, target, options.directly_used)?;
if outcome.succeeded() {
context
.battle_mut()
.set_last_successful_move(Some(active_move_handle));
}
core_battle_effects::run_active_move_event_with_input::<_, ()>(
&mut context,
fxlang::BattleEvent::AfterMove,
core_battle_effects::MoveTargetForEvent::User,
outcome.succeeded(),
);
core_battle_effects::run_event_with_input::<_, _, ()>(
&mut context.user_applying_effect_context(target)?,
fxlang::BattleEvent::AfterMove,
outcome.succeeded(),
);
context.battle_mut().set_last_move(Some(active_move_handle));
Ok(UseActiveMoveResult::new_used(outcome))
}
fn effective_move_type(
context: &mut MonContext,
effect: &EffectHandle,
target: Option<MonHandle>,
source_effect: Option<&EffectHandle>,
use_cache: bool,
) -> Result<Type> {
let stable_effect = effect.stable_effect_handle(context.as_battle_context())?;
let typ = if let Some(id) = stable_effect.try_id()
&& let Ok(mov) = context.battle().dex.moves.get_by_id(id)
{
if use_cache
&& let Some(slot) = context
.mon()
.volatile_state
.effect_cache
.move_slot_effects
.get(id)
&& let Some(typ) = slot.typ
{
return Ok(typ);
}
let typ = mov.data.primary_type;
if typ == Type::None {
return Ok(typ);
}
typ
} else {
return Err(general_error(format!(
"{} is not a move",
stable_effect
.try_id()
.map(|id| id.as_ref())
.unwrap_or_else(|| "[unidentified effect]")
)));
};
let mut context =
context.applying_effect_context(stable_effect, target, source_effect.cloned())?;
let typ = core_battle_effects::run_effect_event_with_input::<_, _, Option<Type>>(
&mut context,
fxlang::BattleEvent::ModifyMoveType,
typ,
)
.unwrap_or(typ);
let typ = core_battle_effects::run_event_with_relay::<_, Type>(
&mut context,
fxlang::BattleEvent::ModifyMoveType,
typ,
);
if use_cache {
let id = context.effect().id().clone();
context
.target_mut()
.volatile_state
.effect_cache
.move_slot_effects
.entry(id)
.or_default()
.typ = Some(typ);
}
Ok(typ)
}
pub fn move_type_for_display(context: &mut MonContext, mov: &Id) -> Result<Type> {
effective_move_type(
context,
&EffectHandle::InactiveMove(mov.clone()),
None,
None,
true,
)
}
pub fn modify_move_type(context: &mut ActiveMoveContext, target: Option<MonHandle>) -> Result<()> {
let effect = context.effect_handle();
let source_effect = context.source_effect_handle();
let typ = effective_move_type(
context.as_mon_context_mut(),
&effect,
target,
source_effect.as_ref(),
false,
)
.unwrap_or(context.active_move().data.primary_type);
context.active_move_mut().data.primary_type = typ;
Ok(())
}
fn use_active_move_internal(
context: &mut ActiveMoveContext,
target: Option<MonHandle>,
directly_used: bool,
) -> Result<MoveOutcome> {
modify_move_type(context, target)?;
core_battle_effects::run_active_move_event::<()>(
context,
fxlang::BattleEvent::UseMove,
core_battle_effects::MoveTargetForEvent::UserWithTarget(target),
);
core_battle_effects::run_event::<_, ()>(
&mut context.user_applying_effect_context(target)?,
fxlang::BattleEvent::UseMove,
);
let targets = get_move_targets(context, target)?;
context.active_move_mut().original_targets = targets.clone();
let target = if context.active_move().data.target.has_single_target() {
targets.first().cloned()
} else if !context.active_move().data.target.affects_mons_directly() {
None
} else {
target
};
core_battle_logs::use_move(context, target, !directly_used)?;
core_battle_effects::run_event::<_, ()>(
&mut context.user_applying_effect_context(target)?,
fxlang::BattleEvent::PreMoveEffect,
);
if context.active_move().data.target.requires_target() && target.is_none() {
core_battle_logs::last_move_had_no_target(context);
core_battle_logs::fail(context.as_mon_context_mut(), None, None)?;
return Ok(MoveOutcome::Skipped);
}
let try_move_result = core_battle_effects::run_active_move_event::<EventResult>(
context,
fxlang::BattleEvent::TryMove,
core_battle_effects::MoveTargetForEvent::UserWithTarget(target),
)
.and_then_try(|| {
Ok(core_battle_effects::run_event::<_, EventResult>(
&mut context.user_applying_effect_context(target)?,
fxlang::BattleEvent::TryMove,
))
})?;
if !try_move_result.advance() {
handle_move_event_result(context, try_move_result)?;
return Ok(try_move_result.into());
}
core_battle_effects::run_active_move_event::<()>(
context,
fxlang::BattleEvent::UseMoveMessage,
core_battle_effects::MoveTargetForEvent::User,
);
if context.active_move().data.self_destruct == Some(SelfDestructType::Always) {
let mon_handle = context.mon_handle();
let effect_handle = context.effect_handle();
faint(
context.as_mon_context_mut(),
Some(mon_handle),
Some(&effect_handle),
)?;
}
let result = if !context.active_move().data.target.affects_mons_directly() {
try_indirect_move(context, &targets)?
} else {
if targets.is_empty() {
core_battle_logs::last_move_had_no_target(context);
core_battle_logs::fail(context.as_mon_context_mut(), None, None)?;
return Ok(MoveOutcome::Skipped);
}
try_direct_move(context, &targets, target)?
};
if !result.advance() {
handle_move_event_result(context, result)?;
}
let outcome = MoveOutcome::from(result);
if context.mon().hp == 0 {
let mon_handle = context.mon_handle();
let effect_handle = context.effect_handle();
faint(
context.as_mon_context_mut(),
Some(mon_handle),
Some(&effect_handle),
)?;
}
if outcome.failed() {
core_battle_effects::run_active_move_event::<()>(
context,
fxlang::BattleEvent::MoveFailed,
core_battle_effects::MoveTargetForEvent::User,
);
}
if !context.active_move().ignore_all_secondary_effects && context.mon().active {
core_battle_effects::run_active_move_event_with_input::<_, ()>(
context,
fxlang::BattleEvent::AfterMoveSecondaryEffectsUser,
core_battle_effects::MoveTargetForEvent::UserWithTarget(target),
targets.clone(),
);
core_battle_effects::run_event_with_input::<_, _, ()>(
&mut context.user_applying_effect_context(target)?,
fxlang::BattleEvent::AfterMoveSecondaryEffectsUser,
targets,
);
}
Ok(outcome)
}
pub fn faint(
context: &mut MonContext,
source: Option<MonHandle>,
effect: Option<&EffectHandle>,
) -> Result<()> {
Mon::faint(context, source, effect)
}
pub fn get_move_targets(
context: &mut ActiveMoveContext,
selected_target: Option<MonHandle>,
) -> Result<Vec<MonHandle>> {
let mut targets = Vec::new();
match context.active_move().data.target {
MoveTarget::All | MoveTarget::Field => {
targets.extend(
context
.battle()
.active_mon_handles_on_side(context.side().index),
);
targets.extend(
context
.battle()
.active_mon_handles_on_side(context.foe_side().index),
);
}
MoveTarget::FoeSide => {
targets.extend(
context
.battle()
.active_mon_handles_on_side(context.foe_side().index),
);
}
MoveTarget::AllySide | MoveTarget::Allies => {
targets.extend(
context
.battle()
.active_mon_handles_on_side(context.side().index),
);
}
MoveTarget::AlliesExceptUser => {
targets.extend(Mon::active_allies(context.as_mon_context_mut()));
}
MoveTarget::AllyTeam => {
targets.extend(
context
.battle()
.all_mon_handles_on_side(context.side().index),
);
}
MoveTarget::AllAdjacent => {
targets.extend(Mon::adjacent_allies(context.as_mon_context_mut())?);
targets.extend(Mon::adjacent_foes(context.as_mon_context_mut())?);
}
MoveTarget::AllAdjacentAllies => {
targets.extend(Mon::adjacent_allies(context.as_mon_context_mut())?);
}
MoveTarget::AllAdjacentFoes => {
targets.extend(Mon::adjacent_foes(context.as_mon_context_mut())?);
}
MoveTarget::User => {
targets.push(context.mon_handle());
}
_ => {
let mut target = match selected_target {
Some(target) => {
let mon = context.mon_handle();
let target_context = context.target_mon_context(target)?;
if !target_context.mon().active
&& !target_context
.mon()
.is_ally(target_context.as_battle_context().mon(mon)?)
{
None
} else {
Some(target)
}
}
None => None,
};
if target.is_none()
&& !context
.active_move()
.data
.advanced_targeting
.no_random_target
{
let mon = context.mon_handle();
let move_target = context.active_move().data.target;
target =
CoreBattle::random_target(context.as_battle_context_mut(), mon, move_target)?;
}
let mut target = match target {
Some(target) => target,
None => return Ok(Vec::new()),
};
let mut redirected = false;
if context.battle().max_side_length() > 1
&& !context.active_move().data.advanced_targeting.tracks_target
{
if let Some(redirected_target) =
core_battle_effects::run_event_with_options::<_, _, Option<MonHandle>>(
&mut context.user_applying_effect_context(Some(target))?,
fxlang::BattleEvent::RedirectTarget,
target,
core_battle_effects::RunEventOptions {
return_first_value: true,
..Default::default()
},
)
{
target = redirected_target;
redirected = true;
}
}
if !redirected && context.active_move().data.advanced_targeting.smart_target {
let mut allies = Vec::default();
for ally in Mon::adjacent_allies(&mut context.target_mon_context(target)?)?
.collect::<Vec<_>>()
{
if ally == context.mon_handle()
|| !Mon::is_adjacent(context.as_mon_context_mut(), ally)?
|| context.target_mon_context(ally)?.mon().hp == 0
{
continue;
}
allies.push(ally);
}
if allies.is_empty() || context.target_mon_context(target)?.mon().hp > 0 {
targets.push(target);
}
targets.extend(allies);
} else {
targets.push(target);
}
}
}
let targets = targets
.into_iter()
.filter(|target| {
context
.as_battle_context()
.mon(*target)
.is_ok_and(|target| target.hp > 0)
})
.collect();
Ok(targets)
}
fn handle_move_event_result(context: &mut ActiveMoveContext, result: EventResult) -> Result<()> {
if context.is_self() || context.is_secondary() {
return Ok(());
}
if result.do_not_animate() {
core_battle_logs::do_not_animate_last_move(context);
}
if result.report_failure() && !context.active_move().fail_reported {
context.active_move_mut().fail_reported = true;
core_battle_logs::fail(context.as_mon_context_mut(), None, None)?;
}
Ok(())
}
fn fold_target_hits_into_event_result<I>(targets: I) -> EventResult
where
I: IntoIterator<Item = MoveOutcomeOnTarget>,
{
targets
.into_iter()
.fold(MoveOutcomeOnTarget::Unknown, |acc, outcome| {
acc.combine(outcome)
})
.into()
}
fn run_try_use_move_events(
context: &mut ActiveMoveContext,
selected_target: Option<MonHandle>,
) -> Result<EventResult> {
core_battle_effects::run_active_move_event::<EventResult>(
context,
fxlang::BattleEvent::TryUseMove,
core_battle_effects::MoveTargetForEvent::UserWithTarget(selected_target),
)
.and_then(|| {
core_battle_effects::run_active_move_event::<EventResult>(
context,
fxlang::BattleEvent::PrepareHit,
core_battle_effects::MoveTargetForEvent::UserWithTarget(selected_target),
)
})
.and_then_try(|| {
Ok(core_battle_effects::run_event::<_, EventResult>(
&mut context.user_applying_effect_context(selected_target)?,
fxlang::BattleEvent::PrepareHit,
))
})
}
fn try_indirect_move(
context: &mut ActiveMoveContext,
targets: &[MonHandle],
) -> Result<EventResult> {
try_event!(run_try_use_move_events(context, None)?, Ok);
let move_target = context.active_move().data.target;
let try_move_result = match move_target {
MoveTarget::Field => core_battle_effects::run_event::<_, EventResult>(
&mut context.field_effect_context()?,
fxlang::BattleEvent::TryHitField,
),
MoveTarget::AllySide | MoveTarget::AllyTeam => {
core_battle_effects::run_event::<_, EventResult>(
&mut context.side_effect_context(context.side().index)?,
fxlang::BattleEvent::TryHitSide,
)
}
MoveTarget::FoeSide => core_battle_effects::run_event::<_, EventResult>(
&mut context.side_effect_context(context.foe_side().index)?,
fxlang::BattleEvent::TryHitSide,
),
_ => {
return Err(general_error(format!(
"move against target {move_target} applied indirectly, but it should directly hit target mons"
)));
}
};
try_event!(try_move_result, Ok);
move_hit_determine_success(context, &targets[0..1])
}
fn prepare_direct_move_against_targets(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<()> {
for step in [
direct_move_step::check_targets_invulnerability,
direct_move_step::check_try_hit_event,
direct_move_step::check_type_immunity,
direct_move_step::check_general_immunity,
direct_move_step::handle_accuracy,
] {
step(context, targets)?;
}
Ok(())
}
pub fn prepare_direct_move(
context: &mut ActiveMoveContext,
targets: &[MonHandle],
) -> Result<Vec<MonHandle>> {
let mut targets = hit_targets_state_from_targets(targets.iter().cloned());
prepare_direct_move_against_targets(context, &mut targets)?;
Ok(targets
.into_iter()
.filter_map(|target| {
if target.outcome.advance() {
Some(target.handle)
} else {
None
}
})
.collect())
}
fn try_direct_move(
context: &mut ActiveMoveContext,
targets: &[MonHandle],
selected_target: Option<MonHandle>,
) -> Result<EventResult> {
if targets.len() > 1 && !context.active_move().data.advanced_targeting.smart_target {
context.active_move_mut().spread_hit = true;
}
try_event!(run_try_use_move_events(context, selected_target)?, Ok);
let targets = move_hit_loop(context, targets)?;
Ok(fold_target_hits_into_event_result(
targets.into_iter().map(|target| target.outcome),
))
}
fn move_hit_loop_internal(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
hits: u8,
) -> Result<HashMap<MonHandle, MoveOutcomeOnTarget>> {
let mut target_hit_results = HashMap::new();
for hit in 0..hits {
if targets.is_empty() || targets.iter().all(|target| !target.outcome.advance()) {
break;
}
if targets.iter().all(|target| {
context
.target_context(target.handle)
.map(|context| context.target_mon().hp)
.unwrap_or(0)
== 0
}) {
break;
}
context.active_move_mut().hit = hit + 1;
if context.active_move().hit > 1 {
let target = if context.active_move().data.target.has_single_target() {
targets.first().map(|target| target.handle)
} else {
None
};
core_battle_logs::use_move(context, target, true)?;
}
prepare_direct_move_against_targets(context, targets)?;
let hit_targets = targets
.iter()
.filter_map(|target| target.outcome.advance().then_some(target.handle))
.collect::<Vec<_>>();
if hit_targets.is_empty() {
context.active_move_mut().hit -= 1;
break;
}
let hit_targets = if context.active_move().data.advanced_targeting.smart_target {
let target = *hit_targets.get(hit as usize % hit_targets.len()).unwrap();
core_battle_logs::last_move_target(context, target)?;
Vec::from_iter([target])
} else {
hit_targets
};
let mut hit_targets_state =
hit_targets_state_from_targets(hit_targets.iter().map(|target_handle| *target_handle));
let result = move_hit(context, &mut hit_targets_state)?;
if !result.advance() {
handle_move_event_result(context, result)?;
}
for (i, hit_target) in hit_targets.iter().enumerate() {
let new_outcome = hit_targets_state
.get(i)
.wrap_expectation_with_format(format_args!(
"expected target hit state at index {i}"
))?
.outcome;
let target = targets
.iter_mut()
.find(|target| target.handle == *hit_target)
.wrap_expectation_with_format(format_args!(
"expected target corresponding to index {i}"
))?;
target.outcome = new_outcome;
}
if hit_targets_state
.iter()
.all(|target| target.outcome.failed())
{
context.active_move_mut().hit -= 1;
break;
}
let hit_targets = hit_targets_state
.iter()
.filter_map(|target| {
if target.outcome.hit() {
Some(target.handle)
} else {
None
}
})
.collect::<Vec<_>>();
if context.active_move().spread_hit && !hit_targets.is_empty() {
core_battle_logs::last_move_spread_targets(context, hit_targets)?;
}
context.active_move_mut().total_damage += hit_targets_state
.iter()
.map(|target| target.outcome.damage() as u64)
.sum::<u64>();
for state in hit_targets_state.iter() {
context.active_move_mut().hit_data_mut(state.handle).damage =
state.outcome.damage() as u64;
if state.outcome.damage() > 0
&& !context
.active_move()
.damaged_targets
.contains(&state.handle)
{
context.active_move_mut().damaged_targets.push(state.handle);
}
match target_hit_results.entry(state.handle) {
Entry::Vacant(entry) => {
entry.insert(state.outcome);
}
Entry::Occupied(mut entry) => *entry.get_mut() = entry.get().combine(state.outcome),
}
}
CoreBattle::update(context.as_battle_context_mut())?;
if !context.mon().active || context.mon().hp == 0 {
break;
}
}
Ok(target_hit_results)
}
fn move_hit_loop(
context: &mut ActiveMoveContext,
targets: &[MonHandle],
) -> Result<Vec<HitTargetState>> {
context.active_move_mut().total_damage = 0;
let hits = match context.active_move().data.multihit {
None => 1,
Some(MultihitType::Static(hits)) => hits,
Some(MultihitType::Range(min, max)) => {
if min == 2 && max == 5 {
*rand_util::sample_slice(
context.battle_mut().prng.as_mut(),
&[2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5],
)
.wrap_expectation("expected sample_slice to produce a value")?
} else {
rand_util::range(context.battle_mut().prng.as_mut(), min as u64, max as u64) as u8
}
}
};
context.active_move_mut().target_original_hps = targets
.iter()
.map(|target| {
Ok((
*target,
context
.as_battle_context_mut()
.mon_context(*target)?
.mon()
.hp,
))
})
.collect::<Result<HashMap<_, _>>>()?;
let mut targets = hit_targets_state_from_targets(targets.iter().cloned());
let target_hit_results = move_hit_loop_internal(context, targets.as_mut_slice(), hits)?;
let targets = targets;
for target in targets.iter() {
let mut context = context.target_context(target.handle)?;
if context.active_move().data.ohko_type.is_some() && context.target_mon().hp == 0 {
core_battle_logs::ohko(&mut context.target_mon_context()?)?;
}
}
CoreBattle::faint_messages(context.as_battle_context_mut())?;
let hits = context.active_move().hit;
if context.active_move().data.multihit.is_some() && hits > 0 {
core_battle_logs::hit_count(context.as_battle_context_mut(), hits)?;
}
apply_recoil_damage(context, context.active_move().total_damage)?;
for (target, result) in target_hit_results {
let source = context.mon_handle();
if target != source && result.hit() {
let turn = context.battle().turn();
let source_side = context.side().index;
let source_position = Mon::position_on_side_or_previous(context.as_mon_context())
.wrap_expectation("expected attacker to have a position")?;
let mut context = context.target_mon_context(target)?;
context.mon_mut().times_attacked += 1;
context.mon_mut().volatile_state.times_attacked += 1;
context
.mon_mut()
.volatile_state
.received_attacks
.push(ReceivedAttackEntry {
source,
source_side,
source_position,
damage: result.damage(),
turn,
});
}
}
CoreBattle::update(context.as_battle_context_mut())?;
if !context.active_move().ignore_all_secondary_effects {
for target in targets.iter().filter(|target| target.outcome.advance()) {
core_battle_effects::run_active_move_event::<()>(
context,
fxlang::BattleEvent::AfterMoveSecondaryEffects,
core_battle_effects::MoveTargetForEvent::Mon(target.handle),
);
core_battle_effects::run_event::<_, ()>(
&mut context.applying_effect_context_for_target(target.handle)?,
fxlang::BattleEvent::AfterMoveSecondaryEffects,
);
}
for target in &targets {
let total_damage = context.active_move().total_damage(target.handle);
let original_hp = *context
.active_move()
.target_original_hps
.get(&target.handle)
.wrap_expectation("expected target original hp")?;
core_battle_effects::run_active_move_event_with_input::<_, ()>(
context,
fxlang::BattleEvent::AfterMoveSecondaryEffectsDamage,
core_battle_effects::MoveTargetForEvent::Mon(target.handle),
[total_damage.into(), original_hp.into()],
);
core_battle_effects::run_event_with_input::<_, _, ()>(
&mut context.applying_effect_context_for_target(target.handle)?,
fxlang::BattleEvent::AfterMoveSecondaryEffectsDamage,
[total_damage.into(), original_hp.into()],
);
}
}
Ok(targets)
}
fn move_hit_determine_success(
context: &mut ActiveMoveContext,
targets: &[MonHandle],
) -> Result<EventResult> {
let mut targets = hit_targets_state_from_targets(targets.iter().cloned());
try_event!(move_hit(context, &mut targets)?, Ok);
Ok(fold_target_hits_into_event_result(
targets.into_iter().map(|target| target.outcome),
))
}
fn move_hit(
context: &mut ActiveMoveContext,
hit_targets_state: &mut [HitTargetState],
) -> Result<EventResult> {
hit_targets(context, hit_targets_state)
}
fn hit_targets(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<EventResult> {
let move_target = context.active_move().data.target.clone();
let try_move_result = match move_target {
MoveTarget::Field => core_battle_effects::run_active_move_event::<EventResult>(
context,
fxlang::BattleEvent::TryHitField,
core_battle_effects::MoveTargetForEvent::None,
),
MoveTarget::AllySide | MoveTarget::AllyTeam => {
core_battle_effects::run_active_move_event::<EventResult>(
context,
fxlang::BattleEvent::TryHitSide,
core_battle_effects::MoveTargetForEvent::Side(context.side().index),
)
}
MoveTarget::FoeSide => core_battle_effects::run_active_move_event::<EventResult>(
context,
fxlang::BattleEvent::TryHitSide,
core_battle_effects::MoveTargetForEvent::Side(context.foe_side().index),
),
_ => {
let mut result: Option<EventResult> = None;
for target in targets.iter_mut() {
if !target.outcome.hit() {
continue;
}
let target_result = core_battle_effects::run_active_move_event::<EventResult>(
context,
fxlang::BattleEvent::TryHit,
core_battle_effects::MoveTargetForEvent::Mon(target.handle),
);
if !target_result.advance() {
target.outcome = target_result.into();
}
result = match result {
Some(result) => Some(result.combine(target_result)),
None => Some(target_result),
};
}
result.unwrap_or(EventResult::Stop)
}
};
try_event!(try_move_result, Ok);
if !context.is_secondary() && !context.is_self() && move_target.affects_mons_directly() {
try_primary_hit(context, targets)?;
}
for target in targets.as_mut() {
if target.outcome == MoveOutcomeOnTarget::Damage(0) {
target.outcome = MoveOutcomeOnTarget::HitSubstitute;
}
}
calculate_spread_damage(context, targets)?;
let mon_handle = context.mon_handle();
apply_spread_damage(&mut context.effect_context()?, Some(mon_handle), targets)?;
try_event!(apply_move_effects(context, targets)?, Ok);
if !context.is_self() {
apply_user_effect(context, targets)?;
}
if !context.is_secondary() && !context.is_self() {
apply_secondary_effects(context, targets)?;
}
force_switch_from_move(context, targets)?;
for target in targets.iter().filter(|target| target.outcome.damage() > 0) {
core_battle_effects::run_event_with_input::<_, _, ()>(
&mut context.applying_effect_context_for_target(target.handle)?,
fxlang::BattleEvent::DamagingHit,
target.outcome.damage(),
);
core_battle_effects::run_active_move_event::<()>(
context,
fxlang::BattleEvent::AfterHit,
core_battle_effects::MoveTargetForEvent::Mon(target.handle),
);
}
Ok(EventResult::Advance)
}
fn try_primary_hit(context: &mut ActiveMoveContext, targets: &mut [HitTargetState]) -> Result<()> {
for target in targets {
if !target.outcome.hit() {
continue;
}
target.outcome = core_battle_effects::run_event::<_, MoveOutcomeOnTarget>(
&mut context.applying_effect_context_for_target(target.handle)?,
fxlang::BattleEvent::TryPrimaryHit,
);
}
Ok(())
}
fn calculate_spread_damage(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<()> {
for target in targets {
if !target.outcome.advance() {
continue;
}
if context.is_secondary() || context.is_self() {
continue;
}
let mut context = context.target_context(target.handle)?;
target.outcome = match calculate_damage(&mut context)? {
Some(damage) if damage > 0 => MoveOutcomeOnTarget::Damage(damage),
Some(_) => MoveOutcomeOnTarget::Unknown,
None => MoveOutcomeOnTarget::EventResult(EventResult::Fail),
}
}
Ok(())
}
pub fn calculate_base_damage(level: u32, base_power: u32, attack: u32, defense: u32) -> u32 {
let base_damage = 2 * level / 5 + 2;
let base_damage = base_damage * base_power * attack;
let base_damage = base_damage / defense;
let base_damage = base_damage / 50;
base_damage
}
pub fn calculate_damage(context: &mut ActiveTargetContext) -> Result<Option<u16>> {
let target_mon_handle = context.target_mon_handle();
let move_type = context.active_move().data.primary_type;
let ignore_immunity = ignore_type_immunity(context)?;
if !ignore_immunity && check_type_immunity(&mut context.target_mon_context()?, move_type)? {
return Ok(None);
}
if context.active_move().data.ohko_type.is_some() {
return Ok(Some(context.target_mon().max_hp));
}
let target_handle = context.target_mon_handle();
if let Some(damage) = core_battle_effects::run_active_move_event::<Option<u16>>(
context.as_active_move_context_mut(),
fxlang::BattleEvent::MoveDamage,
core_battle_effects::MoveTargetForEvent::Mon(target_handle),
) {
return Ok(Some(damage));
}
if let Some(damage) = context.active_move().data.damage {
return Ok(Some(damage));
}
let mut base_power = context.active_move().data.base_power;
if let Some(dynamic_base_power) = core_battle_effects::run_active_move_event::<Option<u32>>(
context.as_active_move_context_mut(),
fxlang::BattleEvent::MoveBasePower,
core_battle_effects::MoveTargetForEvent::Mon(target_handle),
) {
base_power = dynamic_base_power;
}
let base_power = core_battle_effects::run_event_with_relay::<_, u32>(
&mut context.applying_effect_context()?,
fxlang::BattleEvent::BasePower,
base_power,
);
if base_power == 0 {
return Ok(Some(0));
}
let crit_ratio = context.active_move().data.crit_ratio.unwrap_or(0);
let crit_ratio = core_battle_effects::run_event_with_relay::<_, u8>(
&mut context.user_applying_effect_context()?,
fxlang::BattleEvent::ModifyCritRatio,
crit_ratio,
);
let crit_ratio = crit_ratio.max(0).min(4);
let crit_chance = if crit_ratio > 0 {
let crit_chance = [24, 8, 2, 1][(crit_ratio - 1) as usize];
let crit_chance = Fraction::new(1, crit_chance);
let crit_chance = core_battle_effects::run_event_with_relay::<_, Fraction<u32>>(
&mut context.user_applying_effect_context()?,
fxlang::BattleEvent::ModifyCritChance,
crit_chance,
);
if crit_chance < 0 {
0
} else {
crit_chance.min(1u32.into()).inverse().floor()
}
} else {
0
};
context
.active_move_mut()
.hit_data_mut(target_mon_handle)
.crit = context.active_move().data.will_crit
|| (crit_chance > 0
&& rand_util::chance(context.battle_mut().prng.as_mut(), 1, crit_chance as u64));
if context
.active_move_mut()
.hit_data_mut(target_mon_handle)
.crit
{
if !*core_battle_effects::run_event::<_, DefaultTrueBool>(
&mut context.applying_effect_context()?,
fxlang::BattleEvent::CriticalHit,
) {
context
.active_move_mut()
.hit_data_mut(target_mon_handle)
.crit = false;
}
}
let level = context.mon().level;
let move_category = context.active_move().data.category.clone();
let is_physical = move_category == MoveCategory::Physical;
let attack_stat = context
.active_move()
.data
.override_offensive_stat
.unwrap_or(if is_physical { Stat::Atk } else { Stat::SpAtk });
let defense_stat = context
.active_move()
.data
.override_defensive_stat
.unwrap_or(if is_physical { Stat::Def } else { Stat::SpDef });
let mut attack_boosts = context
.attacker_context()?
.mon()
.volatile_state
.boosts
.get(attack_stat.try_into()?);
let mut defense_boosts = context
.defender_context()?
.mon()
.volatile_state
.boosts
.get(defense_stat.try_into()?);
let ignore_offensive = context.active_move().data.ignore_offensive
|| (context
.active_move_mut()
.hit_data_mut(target_mon_handle)
.crit
&& attack_boosts < 0);
let ignore_defensive = context.active_move().data.ignore_defensive
|| (context
.active_move_mut()
.hit_data_mut(target_mon_handle)
.crit
&& defense_boosts > 0);
if ignore_offensive {
attack_boosts = 0;
}
if ignore_defensive {
defense_boosts = 0;
}
let effect_handle = context.effect_handle();
let move_user = context.mon_handle();
let move_target = context.target_mon_handle();
let attack = Mon::calculate_stat(
&mut context.attacker_context()?,
attack_stat,
attack_boosts,
Some(move_user),
Some(CalculateStatContext {
effect: effect_handle.clone(),
source: move_target,
}),
)?;
let defense = Mon::calculate_stat(
&mut context.defender_context()?,
defense_stat,
defense_boosts,
Some(move_target),
Some(CalculateStatContext {
effect: effect_handle,
source: move_user,
}),
)?;
let base_damage =
calculate_base_damage(level as u32, base_power, attack as u32, defense as u32);
modify_damage(context, base_damage).map(|val| Some(val))
}
pub fn type_effectiveness(context: &mut ApplyingEffectContext) -> Result<i8> {
if context
.effect()
.move_effect()
.map(|mov| mov.data.primary_type == Type::None)
.unwrap_or(false)
{
return Ok(0);
}
let offense = match context.effect().move_effect() {
Some(mov) => mov.data.primary_type,
None => return Ok(0),
};
let modifier = core_battle_effects::run_event_with_relay::<_, i8>(
context,
fxlang::BattleEvent::ForceEffectiveness,
0,
);
let modifier = if modifier == 0 {
let target_handle = context.target_handle();
let mut total = 0;
for (i, defense) in mon_states::effective_types(&mut context.target_context()?)
.into_iter()
.enumerate()
{
let modifier = context.battle().check_type_effectiveness(offense, defense);
let modifier = match context.active_move_context()? {
Some(mut context) => {
core_battle_effects::run_active_move_event_with_input::<_, Option<i8>>(
&mut context,
fxlang::BattleEvent::Effectiveness,
core_battle_effects::MoveTargetForEvent::Mon(target_handle),
[modifier.into(), defense.into(), (i as u64).into()],
)
.unwrap_or(modifier)
}
None => modifier,
};
let modifier = core_battle_effects::run_event_with_input::<_, _, i8>(
context,
fxlang::BattleEvent::Effectiveness,
[modifier.into(), defense.into(), (i as u64).into()],
);
total += modifier;
}
total
} else {
modifier
};
let modifier = core_battle_effects::run_event_with_relay::<_, i8>(
context,
fxlang::BattleEvent::ModifyEffectiveness,
modifier,
);
Ok(modifier)
}
pub fn type_modifier(context: &mut ApplyingEffectContext) -> Result<i8> {
let type_modifier = type_effectiveness(context)?;
Ok(type_modifier.max(-6).min(6))
}
fn modify_damage(context: &mut ActiveTargetContext, mut base_damage: u32) -> Result<u16> {
base_damage += 2;
if context.active_move().spread_hit {
let spread_modifier = Fraction::new(3, 4);
base_damage = modify_32(base_damage, spread_modifier);
}
base_damage = core_battle_effects::run_event_with_relay::<_, u32>(
&mut context.user_applying_effect_context()?,
fxlang::BattleEvent::WeatherModifyDamage,
base_damage,
);
let target_mon_handle = context.target_mon_handle();
let crit = context
.active_move_mut()
.hit_data_mut(target_mon_handle)
.crit;
if crit {
let crit_modifier = Fraction::new(3, 2);
base_damage = modify_32(base_damage, crit_modifier);
}
base_damage = context.battle_mut().randomize_base_damage(base_damage);
let move_type = context.active_move().data.primary_type;
let stab = context.active_move().data.force_stab
|| (context.active_move().data.primary_type != Type::None
&& (mon_states::has_type(context.as_mon_context_mut(), move_type)
|| mon_states::has_type_before_forced_types(
context.as_mon_context_mut(),
move_type,
)));
if stab {
let stab_modifier = Fraction::new(3, 2);
let stab_modifier = core_battle_effects::run_event_with_relay::<_, Fraction<u32>>(
&mut context.user_applying_effect_context()?,
fxlang::BattleEvent::ModifyStab,
stab_modifier,
);
base_damage = modify_32(base_damage, stab_modifier);
}
let type_modifier = type_modifier(&mut context.applying_effect_context()?)?;
context
.active_move_mut()
.hit_data_mut(target_mon_handle)
.type_modifier = type_modifier;
if type_modifier > 0 {
core_battle_logs::super_effective(&mut context.target_mon_context()?)?;
for _ in 0..type_modifier {
base_damage *= 2;
}
} else if type_modifier < 0 {
core_battle_logs::resisted(&mut context.target_mon_context()?)?;
for _ in 0..-type_modifier {
base_damage /= 2;
}
}
if crit {
core_battle_logs::critical_hit(&mut context.target_mon_context()?)?;
}
base_damage = core_battle_effects::run_event_with_relay::<_, u32>(
&mut context.user_applying_effect_context()?,
fxlang::BattleEvent::ModifyDamage,
base_damage,
);
let base_damage = base_damage as u16;
let base_damage = base_damage.max(1);
Ok(base_damage)
}
fn calculate_recoil_damage(context: &ActiveMoveContext, damage_dealt: u64) -> u16 {
let recoil = match &context.active_move().data.recoil {
Some(recoil) => recoil,
None => return 0,
};
let base = match recoil.base {
RecoilBase::Damage => damage_dealt,
RecoilBase::UserMaxHp => context.as_mon_context().mon().max_hp as u64,
RecoilBase::UserBaseMaxHp => context.as_mon_context().mon().base_max_hp as u64,
};
((recoil.percent.convert() * base).round().max(1)).min(u16::MAX as u64) as u16
}
pub fn apply_recoil_damage(context: &mut ActiveMoveContext, damage_dealt: u64) -> Result<u16> {
let recoil = match &context.active_move().data.recoil {
Some(recoil) => recoil,
None => return Ok(0),
};
let recoil_damage = calculate_recoil_damage(context, damage_dealt);
if recoil_damage == 0 {
return Ok(0);
}
if recoil.struggle {
let mon_handle = context.mon_handle();
direct_damage(
&mut context.as_battle_context_mut().applying_effect_context(
EffectHandle::Condition(Id::from_known("strugglerecoil")),
Some(mon_handle),
mon_handle,
None,
)?,
recoil_damage,
true,
)
} else {
damage(
&mut context
.user_applying_effect_context(None)?
.forward_applying_effect_context(EffectHandle::Condition(Id::from_known(
"recoil",
)))?,
recoil_damage,
)
}
}
pub fn check_type_immunity(context: &mut MonContext, typ: Type) -> Result<bool> {
Mon::is_immune(context, typ)
}
pub fn check_move_immunity(context: &mut ActiveMoveContext, target: MonHandle) -> Result<bool> {
let move_type = context.active_move().data.primary_type;
let ignore_immunity = ignore_type_immunity(&mut context.target_context(target)?)?;
let mut target_context = context.target_mon_context(target)?;
let immune = !ignore_immunity && check_type_immunity(&mut target_context, move_type)?;
Ok(immune)
}
mod direct_move_step {
use core::ops::Mul;
use anyhow::Result;
use battler_data::{
Accuracy,
BoostTable,
DefaultTrueBool,
Fraction,
MoveCategory,
MoveTarget,
OhkoType,
};
use battler_prng::rand_util;
use super::HitTargetState;
use crate::{
battle::{
ActiveMoveContext,
ActiveTargetContext,
EventResult,
MoveOutcomeOnTarget,
core_battle_actions,
core_battle_effects,
core_battle_logs,
mon_states,
},
effect::fxlang,
};
#[allow(unused)]
pub type DirectMoveStep = fn(&mut ActiveMoveContext, &mut [HitTargetState]) -> Result<()>;
fn should_report_failure(
context: &mut ActiveMoveContext,
targets: &[HitTargetState],
index: usize,
) -> bool {
if !context.active_move().data.advanced_targeting.smart_target {
return true;
}
if targets
.iter()
.enumerate()
.any(|(i, target)| i != index && !target.outcome.failed())
{
return false;
}
true
}
pub fn check_targets_invulnerability(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<()> {
for i in 0..targets.len() {
let report_failure = should_report_failure(context, targets, i);
let target = targets.get_mut(i).unwrap();
if !target.outcome.advance() {
continue;
}
if !*core_battle_effects::run_event_with_options::<_, _, DefaultTrueBool>(
&mut context.applying_effect_context_for_target(target.handle)?,
fxlang::BattleEvent::Invulnerability,
(),
core_battle_effects::RunEventOptions {
return_first_value: true,
..Default::default()
},
) {
target.outcome = MoveOutcomeOnTarget::EventResult(EventResult::StopFail);
if report_failure {
core_battle_logs::miss(&mut context.target_mon_context(target.handle)?)?;
}
}
}
Ok(())
}
pub fn check_try_hit_event(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<()> {
for i in 0..targets.len() {
let report_failure = should_report_failure(context, targets, i);
let target = targets.get_mut(i).unwrap();
if !target.outcome.advance() {
continue;
}
let result = match context.active_move().force_try_hit_result {
Some(result) => result,
None => core_battle_effects::run_event_with_input::<_, _, EventResult>(
&mut context.applying_effect_context_for_target(target.handle)?,
fxlang::BattleEvent::TryHit,
report_failure,
),
};
if !result.advance() {
target.outcome = result.into();
}
}
Ok(())
}
pub fn check_type_immunity(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<()> {
for i in 0..targets.len() {
let report_failure = should_report_failure(context, targets, i);
let target = targets.get_mut(i).unwrap();
if !target.outcome.advance() {
continue;
}
if core_battle_actions::check_move_immunity(context, target.handle)? {
target.outcome = MoveOutcomeOnTarget::EventResult(EventResult::StopFail);
if report_failure {
core_battle_logs::immune(
&mut context.target_mon_context(target.handle)?,
None,
)?;
}
}
}
Ok(())
}
pub fn check_general_immunity(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<()> {
for i in 0..targets.len() {
let report_failure = should_report_failure(context, targets, i);
let target = targets.get_mut(i).unwrap();
if !target.outcome.advance() {
continue;
}
let immune = !*core_battle_effects::run_active_move_event::<DefaultTrueBool>(
context,
fxlang::BattleEvent::TryImmunity,
core_battle_effects::MoveTargetForEvent::Mon(target.handle),
) || core_battle_actions::check_immunity(
&mut context.applying_effect_context_for_target(target.handle)?,
)?;
if immune {
target.outcome = MoveOutcomeOnTarget::EventResult(EventResult::StopFail);
if report_failure {
core_battle_logs::immune(
&mut context.target_mon_context(target.handle)?,
None,
)?;
}
}
}
Ok(())
}
pub fn handle_accuracy(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<()> {
if !context.active_move().data.multiaccuracy && context.active_move().hit > 1 {
return Ok(());
}
for i in 0..targets.len() {
let report_failure = should_report_failure(context, targets, i);
let target = targets.get_mut(i).unwrap();
if !target.outcome.advance() {
continue;
}
let mut context = context.target_context(target.handle)?;
if !accuracy_check(&mut context, !report_failure)? {
target.outcome = MoveOutcomeOnTarget::EventResult(EventResult::StopFail);
}
}
Ok(())
}
fn accuracy_check(context: &mut ActiveTargetContext, silent: bool) -> Result<bool> {
let mut accuracy = context.active_move().data.accuracy;
if let Some(ohko) = context.active_move().data.ohko_type.clone() {
if !mon_states::is_semi_invulnerable(&mut context.target_mon_context()?) {
let mut immune = context.mon().level < context.target_mon().level;
if let OhkoType::Type(typ) = ohko {
if mon_states::has_type(&mut context.target_mon_context()?, typ) {
immune = true;
}
}
if immune {
if !silent {
core_battle_logs::immune(&mut context.target_mon_context()?, None)?;
}
return Ok(false);
}
if let Accuracy::Chance(accuracy) = &mut accuracy {
if context.mon().level >= context.target_mon().level {
let user_has_ohko_type = match ohko {
OhkoType::Always => true,
OhkoType::Type(typ) => {
mon_states::has_type(context.as_mon_context_mut(), typ)
}
};
if user_has_ohko_type {
*accuracy += context.mon().level - context.target_mon().level;
}
}
}
}
} else {
if let Accuracy::Chance(accuracy) = &mut accuracy {
*accuracy = core_battle_effects::run_event_with_relay::<_, u8>(
&mut context.applying_effect_context()?,
fxlang::BattleEvent::ModifyAccuracy,
*accuracy,
);
let mut boost = 0;
if !context.active_move().data.ignore_accuracy {
let boosts = context.mon().volatile_state.boosts.clone();
let boosts = core_battle_effects::run_event_with_relay::<_, BoostTable>(
context.as_mon_context_mut(),
fxlang::BattleEvent::ModifyBoosts,
boosts,
);
boost = boosts.acc.max(-6).min(6);
}
if !context.active_move().data.ignore_evasion {
let boosts = context.target_mon().volatile_state.boosts.clone();
let boosts = core_battle_effects::run_event_with_relay::<_, BoostTable>(
&mut context.target_mon_context()?,
fxlang::BattleEvent::ModifyBoosts,
boosts,
);
boost = (boost - boosts.eva).max(-6).min(6);
}
let multiplier = if boost > 0 {
Fraction::new((3 + boost) as u16, 3)
} else {
Fraction::new(3, (3 - boost) as u16)
};
*accuracy = multiplier.mul(*accuracy as u16).floor() as u8;
}
}
if context.active_move().data.target == MoveTarget::User
&& context.active_move().data.category == MoveCategory::Status
&& !mon_states::is_semi_invulnerable(&mut context.target_mon_context()?)
{
accuracy = Accuracy::Exempt;
} else if !*core_battle_effects::run_event_with_options::<_, _, DefaultTrueBool>(
&mut context.applying_effect_context()?,
fxlang::BattleEvent::AccuracyExempt,
(),
core_battle_effects::RunEventOptions {
return_first_value: true,
..Default::default()
},
) {
accuracy = Accuracy::Exempt;
}
let hit = match accuracy {
Accuracy::Chance(accuracy) => {
rand_util::chance(context.battle_mut().prng.as_mut(), accuracy as u64, 100)
}
_ => true,
};
if !hit {
if !silent {
core_battle_logs::miss(&mut context.target_mon_context()?)?;
}
core_battle_effects::run_event::<_, ()>(
&mut context.applying_effect_context()?,
fxlang::BattleEvent::AccuracyCheckFailed,
);
}
Ok(hit)
}
}
pub fn direct_damage(
context: &mut ApplyingEffectContext,
damage: u16,
run_after_damage: bool,
) -> Result<u16> {
if context.target().hp == 0 || damage == 0 {
return Ok(0);
}
let damage = damage.max(1);
let source = context.source_handle();
let effect = context.effect_handle().clone();
let damage = Mon::damage(
&mut context.target_context()?,
damage,
source,
Some(&effect),
)?;
core_battle_logs::damage(&mut context.target_context()?, Some(effect), source)?;
if run_after_damage {
core_battle_effects::run_event_with_input::<_, _, ()>(
context,
fxlang::BattleEvent::AfterDamage,
damage,
);
}
Ok(damage)
}
pub fn damage(context: &mut ApplyingEffectContext, damage: u16) -> Result<u16> {
let target = context.target_handle();
let source = context.source_handle();
let mut targets = [HitTargetState::new(
target,
MoveOutcomeOnTarget::Damage(damage),
)];
apply_spread_damage(context.as_effect_context_mut(), source, &mut targets)?;
Ok(targets
.get(0)
.wrap_expectation("expected target result to exist after applying spread damage")?
.outcome
.damage())
}
fn apply_spread_damage(
context: &mut EffectContext,
source: Option<MonHandle>,
targets: &mut [HitTargetState],
) -> Result<()> {
for target in targets {
if !target.outcome.advance() {
continue;
}
let mut context = context.applying_effect_context(source, target.handle)?;
let damage = match &mut target.outcome {
MoveOutcomeOnTarget::Damage(damage) => damage,
_ => continue,
};
if context.target().hp == 0 {
target.outcome = MoveOutcomeOnTarget::Damage(0);
continue;
}
if !context.target().active {
target.outcome = MoveOutcomeOnTarget::EventResult(EventResult::Fail);
continue;
}
if context.effect().id() != &Id::from("strugglerecoil") {
if check_immunity(&mut context)? {
*damage = 0;
continue;
}
*damage = core_battle_effects::run_event_with_relay::<_, u16>(
&mut context,
fxlang::BattleEvent::Damage,
*damage,
);
if *damage == 0 {
continue;
}
}
let source_handle = context.source_handle();
let effect_handle = context.effect_handle().clone();
*damage = Mon::damage(
&mut context.target_context()?,
*damage,
source_handle,
Some(&effect_handle),
)?;
context.target_mut().volatile_state.damaged_this_turn = true;
core_battle_logs::damage(
&mut context.target_context()?,
Some(effect_handle),
source_handle,
)?;
apply_drain(&mut context, *damage)?;
core_battle_effects::run_event_with_input::<_, _, ()>(
&mut context,
fxlang::BattleEvent::AfterDamage,
*damage,
);
}
Ok(())
}
pub fn apply_drain(context: &mut ApplyingEffectContext, damage: u16) -> Result<u16> {
let mut healed = 0;
if let Some(Some(drain_percent)) = context
.effect()
.move_effect()
.map(|active_move| active_move.data.drain_percent)
{
let target_handle = context.target_handle();
if let Some(mut context) = context.source_context()? {
let amount = drain_percent * damage;
let amount = amount.round();
healed = heal(
&mut context.applying_effect_context(
EffectHandle::Condition(Id::from_known("drain")),
Some(target_handle),
None,
)?,
amount,
false,
)?;
}
}
Ok(healed)
}
fn apply_heal(context: &mut ApplyingEffectContext, damage: u16) -> Result<u16> {
if damage == 0 {
return Ok(0);
}
let healed = context.target_mut().heal(damage)?;
if healed > 0 {
core_battle_logs::heal(context)?;
core_battle_effects::run_event_with_input::<_, _, ()>(
context,
fxlang::BattleEvent::AfterHeal,
damage,
);
}
Ok(healed)
}
fn force_healing_effect(context: &ApplyingEffectContext) -> bool {
context
.source_effect_handle()
.map(|effect_handle| effect_handle.try_id() == Some(&Id::from_known("playeruseditem")))
.unwrap_or(false)
}
pub fn heal(
context: &mut ApplyingEffectContext,
damage: u16,
is_primary_move_effect: bool,
) -> Result<u16> {
let healed = heal_internal(context, damage)?;
if healed == 0 && is_primary_move_effect {
core_battle_logs::fail_heal(&mut context.target_context()?)?;
}
Ok(healed)
}
fn heal_internal(context: &mut ApplyingEffectContext, damage: u16) -> Result<u16> {
if damage == 0 || context.target().hp == 0 || context.target().hp > context.target().max_hp {
return Ok(0);
}
let damage = if force_healing_effect(context) {
damage
} else {
if !mon_states::can_heal(&mut context.target_context()?) {
return Ok(0);
}
core_battle_effects::run_event_with_relay::<_, u16>(
context,
fxlang::BattleEvent::TryHeal,
damage,
)
};
apply_heal(context, damage)
}
pub fn drag_in(context: &mut PlayerContext, position: usize) -> Result<EventResult> {
let old = context
.player()
.active_mon_handle(position)
.wrap_expectation("nothing to drag out")?;
let mut old_context = context.mon_context(old)?;
if old_context.mon().hp == 0 {
return Ok(EventResult::Fail);
}
try_event!(
core_battle_effects::run_event::<_, EventResult>(
&mut old_context,
fxlang::BattleEvent::DragOut,
),
Ok
);
let player = context.player().index;
let mon = CoreBattle::random_switchable(context.as_battle_context_mut(), player)?;
let mut context = match mon {
None => return Ok(EventResult::Fail),
Some(mon) => context.mon_context(mon)?,
};
if context.mon().active {
return Ok(EventResult::Fail);
}
let switch_type = context.mon().switch_state.force_switch.unwrap_or_default();
switch_in(&mut context, position, Some(switch_type), true)?;
Ok(EventResult::Advance)
}
fn apply_move_effects(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<EventResult> {
let is_secondary = context.is_secondary();
let is_self = context.is_self();
for target in targets.iter_mut() {
if !target.outcome.advance() {
continue;
}
let mut target_context = context.target_context(target.handle)?;
let source_handle = target_context.mon_handle();
let effect_handle = target_context.as_active_move_context().effect_handle();
let mut hit_effect_outcome = MoveOutcomeOnTarget::Unknown;
if let Some(hit_effect) = target_context.hit_effect().cloned() {
if target.outcome.advance() {
if target_context.target_mon().active {
if let Some(boosts) = hit_effect.boosts {
let outcome = boost(
&mut target_context.applying_effect_context()?,
boosts,
is_secondary,
is_self,
)?;
hit_effect_outcome = hit_effect_outcome.combine(outcome.into());
}
if let Some(heal_percent) = hit_effect.heal_percent {
let damage = heal_percent * target_context.mon().base_max_hp;
let damage = damage.round();
let healed = heal(
&mut target_context.applying_effect_context()?,
damage,
!is_secondary && !is_self,
)? > 0;
let outcome = if healed {
MoveOutcomeOnTarget::EventResult(EventResult::Advance)
} else {
MoveOutcomeOnTarget::EventResult(EventResult::Stop)
};
hit_effect_outcome = hit_effect_outcome.combine(outcome);
}
}
if let Some(status) = hit_effect.status {
let set_status = try_set_status(
&mut target_context.applying_effect_context()?,
&Id::from(status),
!is_secondary && !is_self,
)?;
hit_effect_outcome = hit_effect_outcome.combine(set_status.into());
}
if let Some(volatile_status) = hit_effect.volatile_status {
let added_volatile = add_volatile(
&mut target_context.applying_effect_context()?,
&Id::from(volatile_status),
!is_secondary && !is_self,
None,
)?;
hit_effect_outcome = hit_effect_outcome.combine(added_volatile.into());
}
if let Some(side_condition) = hit_effect.side_condition {
let added_side_condition = add_side_condition(
&mut target_context
.applying_effect_context()?
.side_effect_context()?,
&Id::from(side_condition),
None,
)?;
hit_effect_outcome = hit_effect_outcome.combine(added_side_condition.into());
}
if let Some(slot_condition) = hit_effect.slot_condition {
let slot = Mon::position_on_side(&target_context.target_mon_context()?)
.wrap_expectation("expected target to be active")?;
let added_slot_condition = add_slot_condition(
&mut target_context
.applying_effect_context()?
.side_effect_context()?,
slot,
&Id::from(slot_condition),
None,
)?;
hit_effect_outcome = hit_effect_outcome.combine(added_slot_condition.into());
}
if let Some(weather) = hit_effect.weather {
let set_weather_success = set_weather(
&mut target_context
.applying_effect_context()?
.field_effect_context()?,
&Id::from(weather),
)?;
hit_effect_outcome = hit_effect_outcome.combine(set_weather_success.into());
}
if let Some(terrain) = hit_effect.terrain {
let set_terrain_success = set_terrain(
&mut target_context
.applying_effect_context()?
.field_effect_context()?,
&Id::from(terrain),
)?;
hit_effect_outcome = hit_effect_outcome.combine(set_terrain_success.into());
}
if let Some(pseudo_weather) = hit_effect.pseudo_weather {
let add_pseudo_weather_success = add_pseudo_weather(
&mut target_context
.applying_effect_context()?
.field_effect_context()?,
&Id::from(pseudo_weather),
None,
)?;
hit_effect_outcome =
hit_effect_outcome.combine(add_pseudo_weather_success.into());
}
if hit_effect.force_switch {
let outcome = if Player::can_switch(
target_context.target_mon_context()?.as_player_context(),
) {
MoveOutcomeOnTarget::EventResult(EventResult::Advance)
} else {
MoveOutcomeOnTarget::EventResult(EventResult::Fail)
};
hit_effect_outcome = hit_effect_outcome.combine(outcome);
}
}
}
let move_target = target_context.active_move().data.target;
let target_handle = target_context.target_mon_handle();
if target_context.is_self() {
if let Some(hit_result) =
core_battle_effects::run_active_move_event::<Option<EventResult>>(
target_context.as_active_move_context_mut(),
fxlang::BattleEvent::HitUser,
core_battle_effects::MoveTargetForEvent::Mon(target_handle),
)
{
hit_effect_outcome = hit_effect_outcome.combine(hit_result.into());
}
} else {
match move_target {
MoveTarget::Field => {
if let Some(hit_result) =
core_battle_effects::run_active_move_event::<Option<EventResult>>(
target_context.as_active_move_context_mut(),
fxlang::BattleEvent::HitField,
core_battle_effects::MoveTargetForEvent::Field,
)
{
hit_effect_outcome = hit_effect_outcome.combine(hit_result.into());
}
}
MoveTarget::AllySide | MoveTarget::AllyTeam => {
let side = target_context.side().index;
if let Some(hit_result) =
core_battle_effects::run_active_move_event::<Option<EventResult>>(
target_context.as_active_move_context_mut(),
fxlang::BattleEvent::HitSide,
core_battle_effects::MoveTargetForEvent::Side(side),
)
{
hit_effect_outcome = hit_effect_outcome.combine(hit_result.into());
}
}
MoveTarget::FoeSide => {
let side = target_context.foe_side().index;
if let Some(hit_result) =
core_battle_effects::run_active_move_event::<Option<EventResult>>(
target_context.as_active_move_context_mut(),
fxlang::BattleEvent::HitSide,
core_battle_effects::MoveTargetForEvent::Side(side),
)
{
hit_effect_outcome = hit_effect_outcome.combine(hit_result.into());
}
}
_ => {
if let Some(hit_result) =
core_battle_effects::run_active_move_event::<Option<EventResult>>(
target_context.as_active_move_context_mut(),
fxlang::BattleEvent::Hit,
core_battle_effects::MoveTargetForEvent::Mon(target_handle),
)
{
hit_effect_outcome = hit_effect_outcome.combine(hit_result.into());
}
if !target_context.is_secondary() {
if let Some(hit_result) =
core_battle_effects::run_event::<_, Option<EventResult>>(
&mut target_context.applying_effect_context()?,
fxlang::BattleEvent::Hit,
)
{
hit_effect_outcome = hit_effect_outcome.combine(hit_result.into());
}
}
}
}
}
if !is_secondary && !is_self {
if let Some(self_destruct_type) = &context.active_move().data.self_destruct {
if self_destruct_type == &SelfDestructType::IfHit {
if target.outcome.hit() {
faint(
context.as_mon_context_mut(),
Some(source_handle),
Some(&effect_handle),
)?;
}
}
}
if let Some(switch_type) = context.active_move().data.user_switch
&& (!switch_type.if_hit() || (target.outcome.hit() && hit_effect_outcome.hit()))
{
let outcome = if Player::can_switch(context.as_player_context()) {
MoveOutcomeOnTarget::EventResult(EventResult::Advance)
} else {
MoveOutcomeOnTarget::EventResult(EventResult::Fail)
};
hit_effect_outcome = hit_effect_outcome.combine(outcome);
}
}
target.outcome = target.outcome.combine(hit_effect_outcome);
}
let result = fold_target_hits_into_event_result(targets.iter().map(|target| target.outcome));
if result.advance() && context.active_move().data.user_switch.is_some() && context.mon().hp > 0
{
context.mon_mut().switch_state.needs_switch = context.active_move().data.user_switch;
}
Ok(result)
}
pub fn can_boost(context: &mut MonContext, boosts: BoostTable) -> Result<bool> {
if !context.mon().active
|| context.mon().hp == 0
|| Side::mons_left(context.as_side_context_mut())? == 0
{
return Ok(false);
}
let boosts = core_battle_effects::run_event_with_relay::<_, BoostTable>(
context,
fxlang::BattleEvent::ChangeBoosts,
boosts,
);
Ok(Mon::cap_boosts(context, boosts).non_zero_iter().count() > 0)
}
pub fn boost(
context: &mut ApplyingEffectContext,
original_boosts: BoostTable,
is_secondary: bool,
is_self: bool,
) -> Result<EventResult> {
if !context.target().active
|| context.target().hp == 0
|| Side::mons_left(context.target_context()?.as_side_context_mut())? == 0
{
return Ok(EventResult::Fail);
}
if original_boosts.non_zero_iter().count() == 0 {
return Ok(EventResult::Stop);
}
let original_boosts = core_battle_effects::run_event_with_relay::<_, BoostTable>(
&mut context.target_context()?,
fxlang::BattleEvent::ChangeBoosts,
original_boosts.clone(),
);
let capped_boosts = Mon::cap_boosts(&mut context.target_context()?, original_boosts.clone());
let boosts = core_battle_effects::run_event_with_relay::<_, BoostTable>(
context,
fxlang::BattleEvent::TryBoost,
capped_boosts.clone(),
);
let mut success = false;
for (boost, value) in BoostTableEntries::new(&boosts) {
let original_delta = original_boosts.get(boost);
let user_intended = original_delta != 0;
let capped = original_delta != 0 && capped_boosts.get(boost) == 0;
let suppressed = value == 0;
let delta = Mon::boost_stat(&mut context.target_context()?, boost, value);
success = success || delta != 0;
if delta != 0 || capped || (!is_secondary && !is_self && user_intended && !suppressed) {
core_battle_logs::boost(context, boost, delta, original_delta)?;
}
core_battle_effects::run_event_with_input::<_, _, ()>(
context,
fxlang::BattleEvent::AfterEachBoost,
[boost.into(), original_delta.into()],
);
}
core_battle_effects::run_event_with_input::<_, _, ()>(
context,
fxlang::BattleEvent::AfterBoost,
boosts.clone(),
);
if success {
if boosts.values().any(|val| val > 0) {
context.target_mut().volatile_state.stats_raised_this_turn = true;
}
if boosts.values().any(|val| val < 0) {
context.target_mut().volatile_state.stats_lowered_this_turn = true;
}
Ok(EventResult::Advance)
} else {
Ok(EventResult::Fail)
}
}
pub fn swap_boosts(context: &mut ApplyingEffectContext, boosts: &[Boost]) -> Result<()> {
if !context.has_source() {
return Err(general_error("swapping boosts requires a source mon"));
}
let mut target_boosts = BoostTable::default();
let mut source_boosts = BoostTable::default();
let iter: Box<dyn Iterator<Item = Boost>> = if boosts.is_empty() {
Box::new(BoostOrderIterator::new())
} else {
Box::new(boosts.into_iter().cloned())
};
for boost in iter {
target_boosts.set(boost, context.target().volatile_state.boosts.get(boost));
source_boosts.set(
boost,
context
.source()
.wrap_expectation("expected source")?
.volatile_state
.boosts
.get(boost),
);
}
Mon::set_boosts(&mut context.target_context()?, &source_boosts);
Mon::set_boosts(
&mut context
.source_context()?
.wrap_expectation("expected source")?,
&target_boosts,
);
core_battle_logs::swap_boosts(context, boosts)?;
Ok(())
}
pub fn invert_boosts(context: &mut ApplyingEffectContext) -> Result<EventResult> {
if context
.target()
.volatile_state
.boosts
.values()
.all(|val| val == 0)
{
return Ok(EventResult::Fail);
}
let mut boosts = BoostTable::default();
for (boost, val) in context.target().volatile_state.boosts.iter() {
boosts.set(boost, -val);
}
Mon::set_boosts(&mut context.target_context()?, &boosts);
core_battle_logs::invert_boosts(context)?;
Ok(EventResult::Advance)
}
fn apply_user_effect(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<()> {
let mon_handle = context.mon_handle();
let mut context = context.hit_self_active_move_context();
if context.hit_effect().is_none() {
return Ok(());
}
for target in targets {
if !target.outcome.hit() {
continue;
}
if !context.is_secondary() {
let apply = if let Some(chance) = context.active_move().data.user_effect_chance {
rand_util::chance(
context.battle_mut().prng.as_mut(),
chance.numerator() as u64,
chance.denominator() as u64,
)
} else {
true
};
if apply {
if context.active_move().primary_user_effect_applied {
if let Some(hit_effect) = context.hit_effect_mut() {
hit_effect.boosts = None;
}
}
move_hit(
&mut context,
&mut hit_targets_state_from_targets([mon_handle]),
)?;
context.active_move_mut().primary_user_effect_applied = true;
}
} else {
move_hit(
&mut context,
&mut hit_targets_state_from_targets([mon_handle]),
)?;
}
}
Ok(())
}
fn apply_secondary_effects(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<()> {
if context.active_move().data.secondary_effects.is_empty() {
return Ok(());
}
for target in targets {
if !target.outcome.advance() {
continue;
}
let secondary_effects = context.active_move().data.secondary_effects.clone();
let secondary_effects =
core_battle_effects::run_event_with_relay::<_, Vec<SecondaryEffectData>>(
&mut context.applying_effect_context_for_target(target.handle)?,
fxlang::BattleEvent::ModifySecondaryEffects,
secondary_effects,
);
let secondary_effects = secondary_effects
.into_iter()
.map(|secondary_effect| SecondaryEffect::new(secondary_effect))
.collect();
context
.active_move_mut()
.save_secondary_effects(target.handle, secondary_effects)?;
for (i, chance) in context
.active_move()
.secondary_effect_chances(target.handle)
.collect::<Vec<_>>()
{
let secondary_roll = match chance {
Some(chance) => rand_util::chance(
context.battle_mut().prng.as_mut(),
chance.numerator() as u64,
chance.denominator() as u64,
),
None => true,
};
if secondary_roll {
let mut context = context.secondary_active_move_context(target.handle, i);
let mut targets = hit_targets_state_from_targets([target.handle]);
for target in &mut targets {
if target.outcome.advance() {
target.outcome = MoveOutcomeOnTarget::Unknown;
}
}
move_hit(&mut context, &mut targets)?;
}
}
}
Ok(())
}
pub fn force_switch(context: &mut ApplyingEffectContext) -> Result<EventResult> {
let mut context = context.target_context()?;
if !context.mon().active
|| context.mon().hp == 0
|| !Player::can_switch(context.as_player_context())
{
return Ok(EventResult::Fail);
}
try_event!(
core_battle_effects::run_event::<_, EventResult>(
&mut context,
fxlang::BattleEvent::DragOut,
),
Ok
);
context.mon_mut().switch_state.force_switch = Some(SwitchType::Normal);
Ok(EventResult::Advance)
}
fn force_switch_from_move(
context: &mut ActiveMoveContext,
targets: &mut [HitTargetState],
) -> Result<()> {
if !context
.hit_effect()
.is_some_and(|hit_effect| hit_effect.force_switch)
{
return Ok(());
}
for target in targets {
let mut context = context.target_context(target.handle)?;
if !target.outcome.advance() || context.mon().hp == 0 {
continue;
}
force_switch(&mut context.applying_effect_context()?)?;
}
Ok(())
}
pub fn try_set_status(
context: &mut ApplyingEffectContext,
status: &Id,
is_primary_move_effect: bool,
) -> Result<EventResult> {
struct Callbacks {
is_primary_move_effect: bool,
previous_status: Option<Id>,
previous_status_state: EffectState,
}
impl<'context, 'battle, 'data>
StartConditionCallbacks<
'context,
'battle,
'data,
ApplyingEffectContext<'_, '_, 'battle, 'data>,
> for Callbacks
where
'data: 'battle,
{
fn pre_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<EventResult> {
if context.context.target().hp == 0 || context.context.target().status.is_some() {
return Ok(EventResult::Fail);
}
Ok(EventResult::Advance)
}
fn exists(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<bool> {
Ok(false)
}
fn pre_apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<EventResult> {
if check_immunity(
&mut context
.context
.forward_applying_effect_context(context.effect_handle.clone())?,
)? {
if self.is_primary_move_effect {
core_battle_logs::immune(&mut context.context.target_context()?, None)?;
}
return Ok(EventResult::StopFail);
}
try_event!(
core_battle_effects::run_event_with_input::<_, _, EventResult>(
context.context,
fxlang::BattleEvent::SetStatus,
context.effect_handle.clone(),
),
Ok
);
Ok(EventResult::Advance)
}
fn apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
effect_state: fxlang::EffectState,
) -> Result<()> {
context.context.target_mut().status = Some(context.effect_id.clone());
context.context.target_mut().status_state = effect_state;
Ok(())
}
fn rollback(
self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
context.context.target_mut().status = self.previous_status;
context.context.target_mut().status_state = self.previous_status_state;
Ok(())
}
fn post_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
core_battle_effects::run_event_with_input::<_, _, ()>(
context.context,
fxlang::BattleEvent::AfterSetStatus,
context.effect_handle.clone(),
);
Ok(())
}
fn end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
cure_status(context.context, false, false)?;
Ok(())
}
}
let callbacks = Callbacks {
is_primary_move_effect,
previous_status: context.target().status.clone(),
previous_status_state: context.target().status_state.clone(),
};
let mon_handle = context.target_handle();
start_condition(
context,
status,
AppliedEffectLocation::MonStatus(mon_handle),
None,
callbacks,
)
}
fn ignore_type_immunity(context: &mut ActiveTargetContext) -> Result<bool> {
let ignore_immunity = context.active_move().data.primary_type == Type::None
|| context.active_move().data.category == MoveCategory::Status;
let target = context.target_mon_handle();
let ignore_immunity = core_battle_effects::run_active_move_event::<Option<bool>>(
context.as_active_move_context_mut(),
fxlang::BattleEvent::IgnoreImmunity,
core_battle_effects::MoveTargetForEvent::Mon(target),
)
.unwrap_or(ignore_immunity);
let ignore_immunity = if !ignore_immunity {
core_battle_effects::run_event_with_options::<_, _, Option<bool>>(
&mut context.applying_effect_context()?,
fxlang::BattleEvent::IgnoreImmunity,
(),
core_battle_effects::RunEventOptions {
return_first_value: true,
..Default::default()
},
)
.unwrap_or(ignore_immunity)
} else {
ignore_immunity
};
Ok(ignore_immunity)
}
pub fn check_immunity(context: &mut ApplyingEffectContext) -> Result<bool> {
if !context.target().active || context.target().hp == 0 {
return Ok(true);
}
Ok(!*core_battle_effects::run_event::<_, DefaultTrueBool>(
context,
fxlang::BattleEvent::Immunity,
))
}
pub fn clear_status(context: &mut ApplyingEffectContext) -> Result<EventResult> {
cure_status(context, true, false)
}
pub fn cure_status(
context: &mut ApplyingEffectContext,
silent: bool,
log_effect: bool,
) -> Result<EventResult> {
struct Callbacks {
silent: bool,
log_effect: bool,
}
impl<'context, 'battle, 'data>
EndConditionCallbacks<
'context,
'battle,
'data,
ApplyingEffectContext<'_, '_, 'battle, 'data>,
> for Callbacks
{
fn identify(&mut self, context: &mut &'context mut ApplyingEffectContext) -> Option<Id> {
context.target().status.clone()
}
fn pre_end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<EventResult> {
if context.context.target().hp == 0 {
return Ok(EventResult::Fail);
}
if !force_healing_effect(context.context)
&& !*core_battle_effects::run_event::<_, DefaultTrueBool>(
context.context,
fxlang::BattleEvent::CureStatus,
)
{
return Ok(EventResult::Fail);
}
Ok(EventResult::Advance)
}
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
if !self.silent {
core_battle_logs::cure_status(
context.context,
&context.effect_id,
self.log_effect,
)?;
}
Ok(())
}
fn remove(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
context.context.target_mut().status = None;
context.context.target_mut().status_state = fxlang::EffectState::default();
Ok(())
}
fn post_end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
core_battle_effects::run_event::<_, ()>(
context.context,
fxlang::BattleEvent::AfterCureStatus,
);
Ok(())
}
}
let callbacks = Callbacks { silent, log_effect };
let mon_handle = context.target_handle();
end_condition(
context,
None,
AppliedEffectLocation::MonStatus(mon_handle),
false,
callbacks,
)
}
pub fn add_volatile(
context: &mut ApplyingEffectContext,
volatile: &Id,
is_primary_move_effect: bool,
link_to: Option<&AppliedEffectHandle>,
) -> Result<EventResult> {
struct Callbacks {
is_primary_move_effect: bool,
}
impl<'context, 'battle, 'data>
StartConditionCallbacks<
'context,
'battle,
'data,
ApplyingEffectContext<'_, '_, 'battle, 'data>,
> for Callbacks
where
'data: 'battle,
{
fn pre_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<EventResult> {
if !context.context.target().active || context.context.target().hp == 0 {
return Ok(EventResult::Fail);
}
Ok(EventResult::Advance)
}
fn exists(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<bool> {
Ok(context
.context
.target()
.volatile_state
.volatiles
.contains_key(&context.effect_id))
}
fn pre_apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<EventResult> {
if check_immunity(
&mut context
.context
.forward_applying_effect_context(context.effect_handle.clone())?,
)? {
if self.is_primary_move_effect {
core_battle_logs::immune(&mut context.context.target_context()?, None)?;
}
return Ok(EventResult::StopFail);
}
try_event!(
core_battle_effects::run_event_with_input::<_, _, EventResult>(
context.context,
fxlang::BattleEvent::AddVolatile,
context.effect_handle.clone(),
),
Ok
);
Ok(EventResult::Advance)
}
fn apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
effect_state: fxlang::EffectState,
) -> Result<()> {
context
.context
.target_mut()
.volatile_state
.volatiles
.insert(context.effect_id.clone(), effect_state);
Ok(())
}
fn rollback(
self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
context
.context
.target_mut()
.volatile_state
.volatiles
.remove(&context.effect_id);
Ok(())
}
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
core_battle_logs::add_volatile(context.context, &context.effect_id)?;
Ok(())
}
fn post_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
core_battle_effects::run_event_with_input::<_, _, ()>(
context.context,
fxlang::BattleEvent::AfterAddVolatile,
context.effect_handle.clone(),
);
Ok(())
}
fn end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
remove_volatile(context.context, &context.effect_id, false)?;
Ok(())
}
}
let callbacks = Callbacks {
is_primary_move_effect,
};
let mon_handle = context.target_handle();
start_condition(
context,
volatile,
AppliedEffectLocation::MonVolatile(mon_handle),
link_to,
callbacks,
)
}
pub fn remove_volatile(
context: &mut ApplyingEffectContext,
volatile: &Id,
no_events: bool,
) -> Result<EventResult> {
struct Callbacks {}
impl<'context, 'battle, 'data>
EndConditionCallbacks<
'context,
'battle,
'data,
ApplyingEffectContext<'_, '_, 'battle, 'data>,
> for Callbacks
{
fn pre_end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<EventResult> {
if !context.context.target().active || context.context.target().hp == 0 {
return Ok(EventResult::Fail);
}
Ok(EventResult::Advance)
}
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
core_battle_logs::remove_volatile(context.context, &context.effect_id)?;
Ok(())
}
fn remove(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
context
.context
.target_mut()
.volatile_state
.volatiles
.remove(&context.effect_id);
Ok(())
}
fn post_end(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, ApplyingEffectContext>,
) -> Result<()> {
Ok(())
}
}
let callbacks = Callbacks {};
let mon_handle = context.target_handle();
end_condition(
context,
Some(volatile),
AppliedEffectLocation::MonVolatile(mon_handle),
no_events,
callbacks,
)
}
pub fn trap_mon(context: &mut MonContext) -> Result<()> {
let effect_handle = context
.battle_mut()
.get_effect_handle_by_id(&Id::from_known("trapped"))?
.clone();
if check_immunity(&mut context.applying_effect_context(effect_handle, None, None)?)? {
return Ok(());
}
context.mon_mut().next_turn_state.trapped = true;
Ok(())
}
pub fn calculate_confusion_damage(context: &mut MonContext, base_power: u32) -> Result<u16> {
let attack_stat = Stat::Atk;
let defense_stat = Stat::Def;
let attack_boosts = context
.mon()
.volatile_state
.boosts
.get(attack_stat.try_into()?);
let defense_boosts = context
.mon()
.volatile_state
.boosts
.get(defense_stat.try_into()?);
let attack = Mon::calculate_stat(context, attack_stat, attack_boosts, None, None)?;
let defense = Mon::calculate_stat(context, defense_stat, defense_boosts, None, None)?;
let level = context.mon().level as u32;
let base_damage = 2 * level / 5 + 2;
let base_damage = base_damage * base_power;
let base_damage = base_damage * attack as u32;
let base_damage = base_damage / defense as u32;
let base_damage = base_damage / 50;
let base_damage = base_damage + 2;
let base_damage = context.battle_mut().randomize_base_damage(base_damage);
Ok((base_damage as u16).max(1))
}
pub fn add_side_condition(
context: &mut SideEffectContext,
condition: &Id,
link_to: Option<&AppliedEffectHandle>,
) -> Result<EventResult> {
struct Callbacks {}
impl<'context, 'battle, 'data>
StartConditionCallbacks<'context, 'battle, 'data, SideEffectContext<'_, '_, 'battle, 'data>>
for Callbacks
where
'data: 'battle,
{
fn pre_start(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<EventResult> {
Ok(EventResult::Advance)
}
fn exists(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<bool> {
Ok(context
.context
.side()
.conditions
.contains_key(&context.effect_id))
}
fn pre_apply(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<EventResult> {
Ok(EventResult::Advance)
}
fn apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
effect_state: fxlang::EffectState,
) -> Result<()> {
context
.context
.side_mut()
.conditions
.insert(context.effect_id.clone(), effect_state);
Ok(())
}
fn rollback(
self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
context
.context
.side_mut()
.conditions
.remove(&context.effect_id);
Ok(())
}
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
core_battle_logs::add_side_condition(context.context, &context.effect_id)?;
Ok(())
}
fn post_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
core_battle_effects::run_event_with_input::<_, _, ()>(
context.context,
fxlang::BattleEvent::SideConditionStart,
context.effect_handle.clone(),
);
Ok(())
}
fn end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
remove_side_condition(context.context, &context.effect_id)?;
Ok(())
}
}
let callbacks = Callbacks {};
let side = context.side().index;
start_condition(
context,
condition,
AppliedEffectLocation::SideCondition(side),
link_to,
callbacks,
)
}
pub fn remove_side_condition(
context: &mut SideEffectContext,
condition: &Id,
) -> Result<EventResult> {
struct Callbacks {}
impl<'context, 'battle, 'data>
EndConditionCallbacks<'context, 'battle, 'data, SideEffectContext<'_, '_, 'battle, 'data>>
for Callbacks
{
fn pre_end(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<EventResult> {
Ok(EventResult::Advance)
}
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
core_battle_logs::remove_side_condition(context.context, &context.effect_id)?;
Ok(())
}
fn remove(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
context
.context
.side_mut()
.conditions
.remove(&context.effect_id);
Ok(())
}
fn post_end(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
Ok(())
}
}
let callbacks = Callbacks {};
let side = context.side().index;
end_condition(
context,
Some(condition),
AppliedEffectLocation::SideCondition(side),
false,
callbacks,
)
}
pub fn set_types(context: &mut ApplyingEffectContext, mut types: Vec<Type>) -> Result<EventResult> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active {
return Ok(EventResult::Fail);
}
if types.contains(&Type::Stellar) {
return Ok(EventResult::Fail);
}
if mon_states::effective_types(&mut context.target_context()?) == types {
return Ok(EventResult::Fail);
}
try_event!(
core_battle_effects::run_event_with_input::<ApplyingEffectContext, _, EventResult>(
context,
fxlang::BattleEvent::SetTypes,
fxlang::Value::List(types.iter().map(|typ| fxlang::Value::Type(*typ)).collect()),
),
Ok
);
if types.is_empty() {
types = Vec::from_iter([Type::None]);
}
Mon::set_types(&mut context.target_context()?, types)?;
let types = context.target().volatile_state.types.clone();
core_battle_logs::type_change(context, &types)?;
Ok(EventResult::Advance)
}
pub fn reset_types(context: &mut ApplyingEffectContext) -> Result<EventResult> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active {
return Ok(EventResult::Fail);
}
let species = context
.battle()
.dex
.species
.get_by_id(&context.target().base_species)?;
let base_types = species.data.types_iter().collect::<Vec<_>>();
let current_types = mon_states::effective_types_no_added_type(&mut context.target_context()?);
if base_types == current_types {
return Ok(EventResult::Fail);
}
Mon::set_types(&mut context.target_context()?, base_types)?;
core_battle_logs::reset_type_change(context)?;
Ok(EventResult::Advance)
}
pub fn add_type(context: &mut ApplyingEffectContext, typ: Type) -> Result<EventResult> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active || Mon::has_type(&mut context.target_context()?, typ) {
return Ok(EventResult::Fail);
}
try_event!(
core_battle_effects::run_event_with_input::<ApplyingEffectContext, _, EventResult>(
context,
fxlang::BattleEvent::AddType,
typ,
),
Ok
);
context.target_mut().volatile_state.added_type = Some(typ);
let source = context.source_handle();
let source_effect = context.source_effect_handle().cloned();
let mut context = context.target_context()?;
core_battle_logs::added_type(&mut context, typ, source_effect, source)?;
Ok(EventResult::Advance)
}
fn calculate_exp_gain(context: &mut MonContext, fainted_mon_handle: MonHandle) -> Result<u32> {
let mon_handle = context.mon_handle();
let mon_level = context.mon().level as u32;
let mon_friendship = context.mon().friendship;
let fainted_mon = context.as_battle_context_mut().mon(fainted_mon_handle)?;
let species = fainted_mon.base_species.clone();
let target_level = fainted_mon.level as u32;
let participated = fainted_mon
.volatile_state
.foes_fought_while_active
.contains(&mon_handle);
let base_exp_yield = context
.battle()
.dex
.species
.get_by_id(&species)?
.data
.base_exp_yield;
let exp = ((base_exp_yield as u32) * (target_level as u32)) / 5;
let exp = if participated { exp } else { exp / 2 };
let dynamic_scaling = Fraction::new(2 * target_level + 10, target_level + mon_level + 10);
let dynamic_scaling = dynamic_scaling * dynamic_scaling * dynamic_scaling.sqrt();
let exp = dynamic_scaling * exp;
let exp = exp.floor() + 1;
let exp = if mon_friendship > 220 {
exp * 6 / 5
} else {
exp
};
let exp = if context.mon().different_original_trainer {
exp * 3 / 2
} else {
exp
};
let exp = core_battle_effects::run_event_with_relay::<_, u32>(
context,
fxlang::BattleEvent::ModifyExperience,
exp,
);
let exp = Fraction::from(exp)
* context
.player()
.player_options
.experience
.custom_modifier
.unwrap_or(Fraction::from(1u32));
let exp = exp.floor();
Ok(exp)
}
pub fn level_up(context: &mut MonContext, target_level: u8) -> Result<()> {
let old_level = context.mon().level;
context.mon_mut().level = target_level;
Mon::recalculate_stats(context, RecalculateStatsHpPolicy::DoNotUpdate)?;
Mon::recalculate_base_stats(
context,
RecalculateStatsHpPolicy::KeepHealthRatio { silent: false },
)?;
core_battle_logs::level_up(context)?;
for level in old_level..target_level {
Mon::increase_friendship(context, [3, 2, 1]);
learn_moves_at_level(context, level + 1)?;
}
return Ok(());
}
fn learn_moves_at_level(context: &mut MonContext, level: u8) -> Result<()> {
let mut learnable_moves_at_level = context
.battle()
.dex
.species
.get_by_id(&context.mon().volatile_state.species)?
.data
.learnset
.iter()
.filter_map(|(id, methods)| {
methods
.contains(&MoveSource::Level(level))
.then_some(Id::from(id.as_str()))
})
.collect::<Vec<_>>();
learnable_moves_at_level.sort();
let max_move_count = context.battle().format.rules.numeric_rules.max_move_count as usize;
let current_move_count = context.mon().base_move_slots.len();
let instant_learn_count = (current_move_count < max_move_count)
.then(|| (max_move_count - current_move_count).min(learnable_moves_at_level.len()))
.unwrap_or(0);
let (instant_learn, learn_request) = learnable_moves_at_level.split_at(instant_learn_count);
for (move_slot_index, move_id) in instant_learn.iter().enumerate() {
Mon::learn_move(
context,
move_id,
context.mon().base_move_slots.len() + move_slot_index,
)?;
}
context
.mon_mut()
.learnable_moves
.extend(learn_request.iter().cloned());
Ok(())
}
pub fn gain_experience(context: &mut MonContext, exp: u32) -> Result<()> {
if context.mon().level == 100 {
return Ok(());
}
core_battle_logs::experience(context, exp)?;
context.mon_mut().experience += exp;
let leveling_rate = context
.battle()
.dex
.species
.get_by_id(&context.mon().volatile_state.species)?
.data
.leveling_rate;
let new_level = leveling_rate.level_from_exp(context.mon().experience);
if new_level > context.mon().level {
let mon_handle = context.mon_handle();
if !context.mon().active {
BattleQueue::insert_action_into_sorted_position(
context.as_battle_context_mut(),
Action::LevelUp(LevelUpAction {
mon: mon_handle,
level: Some(new_level),
}),
)?;
} else {
for _ in context.mon().level..new_level {
BattleQueue::insert_action_into_sorted_position(
context.as_battle_context_mut(),
Action::LevelUp(LevelUpAction {
mon: mon_handle,
level: None,
}),
)?;
}
}
}
Ok(())
}
pub fn give_out_experience(context: &mut Context, fainted_mon_handle: MonHandle) -> Result<()> {
let foe_side = context.mon_context(fainted_mon_handle)?.foe_side().index;
for foe_handle in context
.battle()
.all_mon_handles_on_side(foe_side)
.collect::<Vec<_>>()
{
let mut context = context.mon_context(foe_handle)?;
if !context.player().player_type.gains_experience() || context.mon().exited.is_some() {
continue;
}
let participated = context
.as_battle_context()
.mon(fainted_mon_handle)?
.volatile_state
.foes_fought_while_active
.contains(&foe_handle);
let participation_modifier = if participated {
Fraction::from(1u32)
} else {
match &context.player().player_options.experience.share {
Some(share) => share.modifier,
None => Fraction::from(0u32),
}
};
if participation_modifier == 0 {
return Ok(());
}
let species = context
.as_battle_context()
.mon(fainted_mon_handle)?
.base_species
.clone();
let ev_yield = context
.battle()
.dex
.species
.get_by_id(&species)?
.data
.ev_yield
.clone();
let ev_yield = core_battle_effects::run_event_with_relay::<_, StatTable>(
&mut context,
fxlang::BattleEvent::ModifyEvYield,
ev_yield,
);
for (stat, value) in ev_yield.entries() {
let current_ev_sum = context.mon().evs.sum();
let ev_limit = context.battle().format.rules.numeric_rules.ev_limit;
let new_ev_value = context.mon().evs.get(stat)
+ (current_ev_sum < ev_limit)
.then(|| value.min((ev_limit - current_ev_sum) as u16))
.unwrap_or(0);
context.mon_mut().evs.set(stat, new_ev_value);
}
Mon::recalculate_stats(&mut context, RecalculateStatsHpPolicy::DoNotUpdate)?;
let exp = calculate_exp_gain(&mut context, fainted_mon_handle)?;
let exp = Fraction::from(exp) * participation_modifier;
let exp = exp.floor();
let mon_handle = context.mon_handle();
match context
.battle_mut()
.queue
.find_action_mut(|action| match action {
Action::Experience(action) => action.mon == mon_handle,
_ => false,
}) {
Some(Action::Experience(action)) => {
action.exp += exp;
}
_ => {
let player_index = context.player().index;
let mon_index = context.mon().team_position;
let active = context.mon().active;
BattleQueue::insert_action_into_sorted_position(
context.as_battle_context_mut(),
Action::Experience(ExperienceAction {
mon: foe_handle,
player_index,
mon_index,
active,
exp,
}),
)?;
}
}
}
Ok(())
}
fn leave_battle(context: &mut PlayerContext) -> Result<()> {
context.player_mut().escaped = true;
for mon in context
.player()
.active_mon_handles()
.cloned()
.collect::<Vec<_>>()
{
switch_out_internal(
&mut context.as_battle_context_mut().mon_context(mon)?,
false,
false,
None,
)?;
}
Ok(())
}
pub fn try_escape(context: &mut MonContext, force: bool) -> Result<EventResult> {
if context.player().escaped {
return Ok(EventResult::Advance);
}
context.player_mut().escape_attempts += 1;
let escaped = Player::can_escape(context.as_player_context());
let mut escaped = escaped && force || Mon::can_escape(context)?;
let force = force
|| !*core_battle_effects::run_event::<_, DefaultTrueBool>(
context,
fxlang::BattleEvent::ForceEscape,
);
if escaped && !context.player().player_type.wild() && !force {
let speed = context.mon().volatile_state.speed;
let mut foe_speed = 0;
let mut foe_count = 0;
for foe in context
.battle()
.active_mon_handles_on_side(context.foe_side().index)
{
foe_speed += context.as_battle_context().mon(foe)?.volatile_state.speed;
foe_count += 1;
}
let foe_speed = foe_speed / foe_count;
let odds = Fraction::from(speed * 32);
let odds = odds / Fraction::new(foe_speed, 4);
let odds = odds.floor() + 30 * context.player().escape_attempts as u32;
escaped = rand_util::chance(context.battle_mut().prng.as_mut(), odds as u64, 256);
}
if !escaped {
core_battle_logs::cannot_escape(context.as_player_context_mut())?;
return Ok(EventResult::Fail);
}
leave_battle(context.as_player_context_mut())?;
core_battle_logs::escaped(context.as_player_context_mut())?;
Ok(EventResult::Advance)
}
pub fn forfeit(context: &mut PlayerContext) -> Result<()> {
leave_battle(context)?;
core_battle_logs::forfeited(context)?;
Ok(())
}
pub fn set_weather(context: &mut FieldEffectContext, weather: &Id) -> Result<EventResult> {
struct Callbacks {
previous_weather: Option<Id>,
previous_weather_state: EffectState,
}
impl<'context, 'battle, 'data>
StartConditionCallbacks<
'context,
'battle,
'data,
FieldEffectContext<'_, '_, 'battle, 'data>,
> for Callbacks
where
'data: 'battle,
{
fn pre_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<EventResult> {
if context
.context
.battle()
.field
.weather
.as_ref()
.is_some_and(|existing| existing == &context.effect_id)
{
return Ok(EventResult::Fail);
}
Ok(EventResult::Advance)
}
fn exists(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<bool> {
Ok(false)
}
fn pre_apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<EventResult> {
Ok(
core_battle_effects::run_event_with_input::<_, _, EventResult>(
context.context,
fxlang::BattleEvent::SetWeather,
context.effect_handle.clone(),
),
)
}
fn apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
effect_state: fxlang::EffectState,
) -> Result<()> {
context.context.battle_mut().field.weather = Some(context.effect_id.clone());
context.context.battle_mut().field.weather_state = effect_state;
Ok(())
}
fn rollback(
self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
context.context.battle_mut().field.weather = self.previous_weather;
context.context.battle_mut().field.weather_state = self.previous_weather_state;
Ok(())
}
fn post_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
core_battle_effects::run_event_for_each_active_mon_with_effect(
context.context.as_effect_context_mut(),
fxlang::BattleEvent::WeatherChange,
)?;
Ok(())
}
fn end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
clear_weather(context.context)?;
Ok(())
}
}
let callbacks = Callbacks {
previous_weather: context.battle().field.weather.clone(),
previous_weather_state: context.battle().field.weather_state.clone(),
};
start_condition(
context,
weather,
AppliedEffectLocation::Weather,
None,
callbacks,
)
}
pub fn clear_weather(context: &mut FieldEffectContext) -> Result<EventResult> {
struct Callbacks {}
impl<'context, 'battle, 'data>
EndConditionCallbacks<'context, 'battle, 'data, FieldEffectContext<'_, '_, 'battle, 'data>>
for Callbacks
{
fn identify(&mut self, context: &mut &'context mut FieldEffectContext) -> Option<Id> {
context.battle().field.weather.clone()
}
fn pre_end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<EventResult> {
Ok(core_battle_effects::run_event::<_, EventResult>(
context.context,
fxlang::BattleEvent::ClearWeather,
))
}
fn reset(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
set_default_weather(context.context.as_battle_context_mut())?;
Ok(())
}
fn remove(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
context.context.battle_mut().field.weather = None;
context.context.battle_mut().field.weather_state = fxlang::EffectState::default();
Ok(())
}
fn post_end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
core_battle_effects::run_event_for_each_active_mon_with_effect(
context.context.as_effect_context_mut(),
fxlang::BattleEvent::WeatherChange,
)?;
Ok(())
}
}
let callbacks = Callbacks {};
end_condition(
context,
None,
AppliedEffectLocation::Weather,
false,
callbacks,
)
}
pub fn set_default_weather(context: &mut Context) -> Result<()> {
if let Some(default_weather) = context.battle().field.default_weather.clone() {
set_weather(
&mut context.field_effect_context(
EffectHandle::Condition(Id::from_known("start")),
None,
None,
)?,
&default_weather,
)?;
}
Ok(())
}
pub fn set_terrain(context: &mut FieldEffectContext, terrain: &Id) -> Result<EventResult> {
struct Callbacks {
previous_terrain: Option<Id>,
previous_terrain_state: EffectState,
}
impl<'context, 'battle, 'data>
StartConditionCallbacks<
'context,
'battle,
'data,
FieldEffectContext<'_, '_, 'battle, 'data>,
> for Callbacks
where
'data: 'battle,
{
fn pre_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<EventResult> {
if context
.context
.battle()
.field
.terrain
.as_ref()
.is_some_and(|existing| existing == &context.effect_id)
{
return Ok(EventResult::Fail);
}
Ok(EventResult::Advance)
}
fn exists(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<bool> {
Ok(false)
}
fn pre_apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<EventResult> {
Ok(
core_battle_effects::run_event_with_input::<_, _, EventResult>(
context.context,
fxlang::BattleEvent::SetTerrain,
context.effect_handle.clone(),
),
)
}
fn apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
effect_state: fxlang::EffectState,
) -> Result<()> {
context.context.battle_mut().field.terrain = Some(context.effect_id.clone());
context.context.battle_mut().field.terrain_state = effect_state;
Ok(())
}
fn rollback(
self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
context.context.battle_mut().field.terrain = self.previous_terrain;
context.context.battle_mut().field.terrain_state = self.previous_terrain_state;
Ok(())
}
fn post_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
core_battle_effects::run_event_for_each_active_mon_with_effect(
context.context.as_effect_context_mut(),
fxlang::BattleEvent::TerrainChange,
)?;
Ok(())
}
fn end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
clear_terrain(context.context)?;
Ok(())
}
}
let callbacks = Callbacks {
previous_terrain: context.battle().field.terrain.clone(),
previous_terrain_state: context.battle().field.terrain_state.clone(),
};
start_condition(
context,
terrain,
AppliedEffectLocation::Terrain,
None,
callbacks,
)
}
pub fn clear_terrain(context: &mut FieldEffectContext) -> Result<EventResult> {
struct Callbacks {}
impl<'context, 'battle, 'data>
EndConditionCallbacks<'context, 'battle, 'data, FieldEffectContext<'_, '_, 'battle, 'data>>
for Callbacks
{
fn identify(&mut self, context: &mut &'context mut FieldEffectContext) -> Option<Id> {
context.battle().field.terrain.clone()
}
fn pre_end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<EventResult> {
Ok(core_battle_effects::run_event::<_, EventResult>(
context.context,
fxlang::BattleEvent::ClearTerrain,
))
}
fn reset(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
set_default_terrain(context.context.as_battle_context_mut())?;
Ok(())
}
fn remove(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
context.context.battle_mut().field.terrain = None;
context.context.battle_mut().field.terrain_state = fxlang::EffectState::default();
Ok(())
}
fn post_end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
core_battle_effects::run_event_for_each_active_mon_with_effect(
context.context.as_effect_context_mut(),
fxlang::BattleEvent::TerrainChange,
)?;
Ok(())
}
}
let callbacks = Callbacks {};
end_condition(
context,
None,
AppliedEffectLocation::Terrain,
false,
callbacks,
)
}
pub fn set_default_terrain(context: &mut Context) -> Result<()> {
if let Some(default_terrain) = context.battle().field.default_terrain.clone() {
set_terrain(
&mut context.field_effect_context(
EffectHandle::Condition(Id::from_known("start")),
None,
None,
)?,
&default_terrain,
)?;
}
Ok(())
}
pub fn add_pseudo_weather(
context: &mut FieldEffectContext,
pseudo_weather: &Id,
link_to: Option<&AppliedEffectHandle>,
) -> Result<EventResult> {
struct Callbacks {}
impl<'context, 'battle, 'data>
StartConditionCallbacks<
'context,
'battle,
'data,
FieldEffectContext<'_, '_, 'battle, 'data>,
> for Callbacks
where
'data: 'battle,
{
fn pre_start(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<EventResult> {
Ok(EventResult::Advance)
}
fn exists(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<bool> {
Ok(context
.context
.battle()
.field
.pseudo_weathers
.contains_key(&context.effect_id))
}
fn pre_apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<EventResult> {
Ok(
core_battle_effects::run_event_with_input::<_, _, EventResult>(
context.context,
fxlang::BattleEvent::AddPseudoWeather,
context.effect_handle.clone(),
),
)
}
fn apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
effect_state: fxlang::EffectState,
) -> Result<()> {
context
.context
.battle_mut()
.field
.pseudo_weathers
.insert(context.effect_id.clone(), effect_state);
Ok(())
}
fn rollback(
self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
context
.context
.battle_mut()
.field
.pseudo_weathers
.remove(&context.effect_id);
Ok(())
}
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
core_battle_logs::add_pseudo_weather(context.context, &context.effect_id)?;
Ok(())
}
fn post_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
core_battle_effects::run_event_with_input::<_, _, ()>(
context.context,
fxlang::BattleEvent::AfterAddPseudoWeather,
context.effect_handle.clone(),
);
Ok(())
}
fn end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
remove_pseudo_weather(context.context, &context.effect_id)?;
Ok(())
}
}
let callbacks = Callbacks {};
start_condition(
context,
pseudo_weather,
AppliedEffectLocation::PseudoWeather,
link_to,
callbacks,
)
}
pub fn remove_pseudo_weather(
context: &mut FieldEffectContext,
pseudo_weather: &Id,
) -> Result<EventResult> {
struct Callbacks {}
impl<'context, 'battle, 'data>
EndConditionCallbacks<'context, 'battle, 'data, FieldEffectContext<'_, '_, 'battle, 'data>>
for Callbacks
{
fn pre_end(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<EventResult> {
Ok(EventResult::Advance)
}
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
core_battle_logs::remove_pseudo_weather(context.context, &context.effect_id)?;
Ok(())
}
fn remove(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
context
.context
.battle_mut()
.field
.pseudo_weathers
.remove(&context.effect_id);
Ok(())
}
fn post_end(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, FieldEffectContext>,
) -> Result<()> {
Ok(())
}
}
let callbacks = Callbacks {};
end_condition(
context,
Some(pseudo_weather),
AppliedEffectLocation::PseudoWeather,
false,
callbacks,
)
}
pub fn end_ability(context: &mut ApplyingEffectContext, silent: bool) -> Result<()> {
if !context.target().active || context.target().hp == 0 {
return Ok(());
}
end_ability_even_if_exiting(context, silent)
}
pub fn end_ability_even_if_exiting(
context: &mut ApplyingEffectContext,
silent: bool,
) -> Result<()> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context
.target()
.volatile_state
.ability_slot
.effect_state
.started()
&& !context
.target()
.volatile_state
.ability_slot
.effect_state
.starting()
{
return Ok(());
}
if !silent {
core_battle_logs::ability_end(context)?;
}
core_battle_effects::run_ability_event::<ApplyingEffectContext, _, ()>(
context,
fxlang::BattleEvent::End,
(),
);
Ok(())
}
pub fn start_ability(context: &mut ApplyingEffectContext, silent: bool) -> Result<()> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active
|| context.target().hp == 0
|| context
.target()
.volatile_state
.ability_slot
.effect_state
.started()
{
return Ok(());
}
if !silent {
core_battle_logs::ability(context)?;
}
core_battle_effects::run_ability_event::<ApplyingEffectContext, _, ()>(
context,
fxlang::BattleEvent::BeforeStart,
(),
);
core_battle_effects::run_ability_event::<ApplyingEffectContext, _, ()>(
context,
fxlang::BattleEvent::Start,
(),
);
Ok(())
}
pub fn set_ability(
context: &mut ApplyingEffectContext,
ability: &Id,
dry_run: bool,
force: bool,
silent: bool,
) -> Result<EventResult> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active || context.target().hp == 0 {
return Ok(EventResult::Fail);
}
if !force && &context.target().volatile_state.ability_slot.ability.id == ability {
return Ok(EventResult::Fail);
}
if context.battle().dex.abilities.get_by_id(ability).is_err() {
return Ok(EventResult::Fail);
}
let ability_handle = context
.battle_mut()
.get_effect_handle_by_id(&ability)?
.clone()
.non_condition_handle()
.wrap_expectation("expected ability to have non-condition handle")?;
if !force {
try_event!(
core_battle_effects::run_event_with_input::<ApplyingEffectContext, _, EventResult>(
context,
fxlang::BattleEvent::SetAbility,
ability_handle.clone(),
),
Ok
);
}
if dry_run {
return Ok(EventResult::Advance);
}
end_ability(context, silent)?;
let ability = context.battle().dex.abilities.get_by_id(ability)?;
let effect_handle = context.effect_handle().clone();
let target_handle = context.target_handle();
let source_handle = context.source_handle();
context.target_mut().volatile_state.ability_slot = AbilitySlot {
ability: EffectiveAbility {
id: ability.id().clone(),
sub_abilities: Vec::default(),
},
effect_state: fxlang::EffectState::initial_effect_state(
context.as_battle_context_mut(),
Some(&effect_handle),
Some(target_handle),
source_handle,
)?,
};
start_ability(context, silent)?;
core_battle_effects::run_event_with_input::<ApplyingEffectContext, _, ()>(
context,
fxlang::BattleEvent::AfterSetAbility,
ability_handle,
);
Ok(EventResult::Advance)
}
pub fn add_sub_ability(context: &mut ApplyingEffectContext, ability: &Id) -> Result<EventResult> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active || context.target().hp == 0 {
return Ok(EventResult::Fail);
}
if context.battle().dex.abilities.get_by_id(ability).is_err() {
return Ok(EventResult::Fail);
}
context
.target_mut()
.volatile_state
.ability_slot
.ability
.sub_abilities
.push(ability.clone());
Ok(EventResult::Advance)
}
pub fn clear_sub_abilities(context: &mut ApplyingEffectContext) -> Result<EventResult> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active || context.target().hp == 0 {
return Ok(EventResult::Fail);
}
context
.target_mut()
.volatile_state
.ability_slot
.ability
.sub_abilities
.clear();
Ok(EventResult::Advance)
}
pub fn add_slot_condition(
context: &mut SideEffectContext,
slot: usize,
condition: &Id,
link_to: Option<&AppliedEffectHandle>,
) -> Result<EventResult> {
struct Callbacks {
slot: usize,
}
impl<'context, 'battle, 'data>
StartConditionCallbacks<'context, 'battle, 'data, SideEffectContext<'_, '_, 'battle, 'data>>
for Callbacks
where
'data: 'battle,
{
fn event_input(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> fxlang::VariableInput {
fxlang::VariableInput::from_iter([(self.slot as u64).into()])
}
fn pre_start(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<EventResult> {
Ok(EventResult::Advance)
}
fn exists(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<bool> {
Ok(context
.context
.side()
.slot_conditions
.get(&self.slot)
.is_some_and(|conditions| conditions.contains_key(&context.effect_id)))
}
fn pre_apply(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<EventResult> {
Ok(EventResult::Advance)
}
fn apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
effect_state: fxlang::EffectState,
) -> Result<()> {
context
.context
.side_mut()
.slot_conditions
.entry(self.slot)
.or_default()
.insert(context.effect_id.clone(), effect_state);
Ok(())
}
fn rollback(
self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
context
.context
.side_mut()
.slot_conditions
.get_mut(&self.slot)
.wrap_expectation("expected side slot condition state to exist")?
.remove(&context.effect_id);
Ok(())
}
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
core_battle_logs::add_slot_condition(context.context, self.slot, &context.effect_id)?;
Ok(())
}
fn post_start(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
Ok(())
}
fn end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
remove_slot_condition(context.context, self.slot, &context.effect_id)?;
Ok(())
}
}
let callbacks = Callbacks { slot };
let side = context.side().index;
start_condition(
context,
condition,
AppliedEffectLocation::SlotCondition(side, slot),
link_to,
callbacks,
)
}
pub fn remove_slot_condition(
context: &mut SideEffectContext,
slot: usize,
condition: &Id,
) -> Result<EventResult> {
struct Callbacks {
slot: usize,
}
impl<'context, 'battle, 'data>
EndConditionCallbacks<'context, 'battle, 'data, SideEffectContext<'_, '_, 'battle, 'data>>
for Callbacks
{
fn event_input(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> fxlang::VariableInput {
fxlang::VariableInput::from_iter([(self.slot as u64).into()])
}
fn pre_end(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<EventResult> {
Ok(EventResult::Advance)
}
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
core_battle_logs::remove_slot_condition(
context.context,
self.slot,
&context.effect_id,
)?;
Ok(())
}
fn remove(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
context
.context
.side_mut()
.slot_conditions
.entry(self.slot)
.or_default()
.remove(&context.effect_id);
Ok(())
}
fn post_end(
&mut self,
_: &mut ConditionContext<'context, 'battle, 'data, SideEffectContext>,
) -> Result<()> {
Ok(())
}
}
let callbacks = Callbacks { slot };
let side = context.side().index;
end_condition(
context,
Some(condition),
AppliedEffectLocation::SlotCondition(side, slot),
false,
callbacks,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EndItemType {
End,
Take,
Use,
Eat,
Discard,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EndItemLog {
Log,
LogSilent,
TrueSilent,
}
fn end_item_internal(
context: &mut ApplyingEffectContext,
end_item_type: EndItemType,
end_item_log: EndItemLog,
) -> Result<()> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active || context.target().hp == 0 {
return Ok(());
}
if end_item_log != EndItemLog::TrueSilent {
let no_source = match end_item_type {
EndItemType::Eat | EndItemType::Use => true,
_ => false,
};
let eat = match end_item_type {
EndItemType::Eat => true,
_ => false,
};
core_battle_logs::item_end(
context,
no_source,
end_item_log == EndItemLog::LogSilent,
eat,
)?;
}
if context.target().item.as_ref().is_none_or(|_| {
!context.target().volatile_state.item_state.started()
&& !context.target().volatile_state.item_state.starting()
}) {
return Ok(());
}
let event = match end_item_type {
EndItemType::End | EndItemType::Take | EndItemType::Discard => {
Some(fxlang::BattleEvent::End)
}
EndItemType::Use => Some(fxlang::BattleEvent::Use),
EndItemType::Eat => Some(fxlang::BattleEvent::Eat),
};
if let Some(event) = event {
core_battle_effects::run_item_event::<ApplyingEffectContext, _, ()>(context, event, ());
}
Ok(())
}
pub fn end_item(context: &mut ApplyingEffectContext, silent: bool) -> Result<()> {
end_item_internal(
context,
EndItemType::End,
if silent {
EndItemLog::TrueSilent
} else {
EndItemLog::Log
},
)
}
pub fn start_item(context: &mut ApplyingEffectContext, silent: bool) -> Result<()> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active
|| context.target().hp == 0
|| context
.target()
.item
.as_ref()
.is_none_or(|_| context.target().volatile_state.item_state.started())
{
return Ok(());
}
if !silent {
core_battle_logs::item(context)?;
}
core_battle_effects::run_item_event::<ApplyingEffectContext, _, ()>(
context,
fxlang::BattleEvent::BeforeStart,
(),
);
core_battle_effects::run_item_event::<ApplyingEffectContext, _, ()>(
context,
fxlang::BattleEvent::Start,
(),
);
Ok(())
}
pub fn set_item(
context: &mut ApplyingEffectContext,
item: &Id,
dry_run: bool,
) -> Result<EventResult> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active || context.target().hp == 0 {
return Ok(EventResult::Fail);
}
if !dry_run && context.target().item.is_some() {
return Ok(EventResult::Fail);
}
if !dry_run {
end_item_internal(context, EndItemType::End, EndItemLog::TrueSilent)?;
}
let item_id = context.battle().dex.items.get_by_id(item)?.id().clone();
let item_handle = context
.battle_mut()
.get_effect_handle_by_id(&item_id)?
.clone()
.non_condition_handle()
.wrap_expectation("expected item to have non-condition handle")?;
try_event!(
core_battle_effects::run_event_with_input::<ApplyingEffectContext, _, EventResult>(
context,
fxlang::BattleEvent::SetItem,
item_handle.clone(),
),
Ok
);
if dry_run {
return Ok(EventResult::Advance);
}
let effect_handle = context.effect_handle().clone();
let target_handle = context.target_handle();
let source_handle = context.source_handle();
context.target_mut().item = Some(item_id.clone());
context.target_mut().volatile_state.item_state = fxlang::EffectState::initial_effect_state(
context.as_battle_context_mut(),
Some(&effect_handle),
Some(target_handle),
source_handle,
)?;
start_item(context, false)?;
core_battle_effects::run_event_with_input::<ApplyingEffectContext, _, DefaultTrueBool>(
context,
fxlang::BattleEvent::AfterSetItem,
item_handle,
);
Ok(EventResult::Advance)
}
pub fn take_item(
context: &mut ApplyingEffectContext,
dry_run: bool,
silent: bool,
) -> Result<EventResultOutput<Id>> {
if !context.target().active {
return EventResultOutput::no_output(EventResult::Fail);
}
let item_id = match context.target().item.clone() {
Some(item) => item,
None => return EventResultOutput::no_output(EventResult::Fail),
};
let item_handle = context
.battle_mut()
.get_effect_handle_by_id(&item_id)?
.clone()
.non_condition_handle()
.wrap_expectation("expected item to have non-condition handle")?;
let take_item_result = core_battle_effects::run_effect_event_with_input::<_, _, EventResult>(
&mut context.forward_applying_effect_context(item_handle.clone())?,
fxlang::BattleEvent::TakeItem,
item_handle.clone(),
)
.and_then(|| {
core_battle_effects::run_event_with_input::<_, _, EventResult>(
context,
fxlang::BattleEvent::TakeItem,
item_handle.clone(),
)
});
if !take_item_result.advance() {
return EventResultOutput::no_output(take_item_result);
}
if dry_run {
return Ok(EventResultOutput::with_output(item_id));
}
end_item_internal(
context,
EndItemType::Take,
if silent {
EndItemLog::LogSilent
} else {
EndItemLog::Log
},
)?;
context.target_mut().item = None;
context.target_mut().volatile_state.item_state = fxlang::EffectState::default();
core_battle_effects::run_event_with_input::<_, _, ()>(
context,
fxlang::BattleEvent::AfterTakeItem,
item_handle,
);
Ok(EventResultOutput::with_output(item_id))
}
fn after_use_item(context: &mut ApplyingEffectContext, item: Id) -> Result<bool> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
let item_handle = context
.battle_mut()
.get_effect_handle_by_id(&item)?
.clone()
.non_condition_handle()
.wrap_expectation("expected item to have non-condition handle")?;
context.target_mut().volatile_state.item_used_this_turn = true;
context.target_mut().volatile_state.last_item = Some(item);
context.target_mut().item = None;
context.target_mut().volatile_state.item_state = fxlang::EffectState::default();
core_battle_effects::run_event_with_input::<ApplyingEffectContext, _, ()>(
context,
fxlang::BattleEvent::AfterUseItem,
item_handle,
);
Ok(true)
}
pub fn eat_item(context: &mut ApplyingEffectContext, force: bool) -> Result<EventResult> {
if !context.target().active || context.target().hp == 0 {
return Ok(EventResult::Fail);
}
let item_id = match context.target().item.clone() {
Some(item) => item,
None => return Ok(EventResult::Fail),
};
let item_handle = context
.battle_mut()
.get_effect_handle_by_id(&item_id)?
.clone()
.non_condition_handle()
.wrap_expectation("expected item to have non-condition handle")?;
if !force {
try_event!(
core_battle_effects::run_event_with_input::<_, _, EventResult>(
context,
fxlang::BattleEvent::TryEatItem,
item_handle.clone(),
),
Ok
);
}
end_item_internal(context, EndItemType::Eat, EndItemLog::Log)?;
core_battle_effects::run_event_with_input::<_, _, ()>(
context,
fxlang::BattleEvent::EatItem,
item_handle,
);
context.target_mut().ate_item = true;
after_use_item(context, item_id).map(EventResult::from)
}
pub fn eat_given_item(context: &mut ApplyingEffectContext, item: &Id) -> Result<EventResult> {
if context.target().hp == 0 {
return Ok(EventResult::Fail);
}
core_battle_effects::run_effect_event::<_, ()>(
&mut context.forward_applying_effect_context(EffectHandle::Item(item.clone()))?,
fxlang::BattleEvent::Eat,
);
let item_handle = context
.battle_mut()
.get_effect_handle_by_id(item)?
.clone()
.non_condition_handle()
.wrap_expectation("expected item to have non-condition handle")?;
core_battle_effects::run_event_with_input::<_, _, ()>(
context,
fxlang::BattleEvent::EatItem,
item_handle,
);
context.target_mut().ate_item = true;
Ok(EventResult::Advance)
}
pub fn use_item(context: &mut ApplyingEffectContext) -> Result<EventResult> {
if !context.target().active || context.target().hp == 0 {
return Ok(EventResult::Fail);
}
let item_id = match context.target().item.clone() {
Some(item) => item,
None => return Ok(EventResult::Fail),
};
let item_handle = context
.battle_mut()
.get_effect_handle_by_id(&item_id)?
.clone()
.non_condition_handle()
.wrap_expectation("expected item to have non-condition handle")?;
try_event!(
core_battle_effects::run_event_with_input::<_, _, EventResult>(
context,
fxlang::BattleEvent::TryUseItem,
item_handle,
),
Ok
);
end_item_internal(context, EndItemType::Use, EndItemLog::Log)?;
after_use_item(context, item_id).map(EventResult::from)
}
pub fn use_given_item(context: &mut ApplyingEffectContext, item: &Id) -> Result<EventResult> {
if context.target().hp == 0 {
return Ok(EventResult::Fail);
}
core_battle_effects::run_effect_event::<_, ()>(
&mut context.forward_applying_effect_context(EffectHandle::Item(item.clone()))?,
fxlang::BattleEvent::Use,
);
Ok(EventResult::Advance)
}
pub fn discard_item(context: &mut ApplyingEffectContext, silent: bool) -> Result<bool> {
if !context.target().active || context.target().hp == 0 {
return Ok(false);
}
let item_id = match context.target().item.clone() {
Some(item) => item,
None => return Ok(false),
};
end_item_internal(
context,
EndItemType::Discard,
if silent {
EndItemLog::TrueSilent
} else {
EndItemLog::LogSilent
},
)?;
after_use_item(context, item_id)
}
pub struct PlayerUseItemInput {
pub move_slot: Option<Id>,
}
impl PlayerUseItemInput {
pub fn input_for_fxlang_callback(&self) -> fxlang::Value {
let mut input = HashMap::default();
if let Some(move_slot) = &self.move_slot {
input.insert(
"move".to_owned(),
fxlang::Value::String(move_slot.to_string()),
);
}
fxlang::Value::Object(input)
}
}
pub fn player_use_item(
context: &mut MonContext,
item: &Id,
target: Option<isize>,
input: PlayerUseItemInput,
) -> Result<bool> {
if !context
.battle()
.format
.rules
.has_rule(&Id::from_known("bagitems"))
{
return Ok(false);
}
let target = CoreBattle::get_item_target(context, item, target)?;
core_battle_logs::use_item(context.as_player_context_mut(), item, target)?;
if !player_use_item_internal(context, item, target, input)? {
core_battle_logs::last_item_had_no_target(context.as_battle_context_mut());
return Ok(false);
}
Ok(true)
}
pub fn player_use_item_internal(
context: &mut MonContext,
item: &Id,
target: Option<MonHandle>,
input: PlayerUseItemInput,
) -> Result<bool> {
let item = context.battle().dex.items.get_by_id(item)?;
let item_id = item.id().clone();
let item_target = item.data.target;
let item_is_ball = item.data.flags.contains(&ItemFlag::Ball);
match item_target {
Some(item_target) => {
if target.is_none() && item_target.requires_target() {
core_battle_logs::last_item_had_no_target(context.as_battle_context_mut());
core_battle_logs::fail_use_item(context.as_player_context_mut(), &item_id, None)?;
return Ok(false);
}
}
None => {
core_battle_logs::fail_use_item(
context.as_player_context_mut(),
&item_id,
Some(EffectHandle::NonExistent(NonExistentEffect::new(
Id::from_known("unusable"),
))),
)?;
return Ok(false);
}
}
if !Player::use_item_from_bag(context.as_player_context_mut(), &item_id, false) {
core_battle_logs::fail_use_item(
context.as_player_context_mut(),
&item_id,
Some(EffectHandle::NonExistent(NonExistentEffect::new(
Id::from_known("unavailable"),
))),
)?;
}
match target {
Some(target) => core_battle_effects::run_effect_event_with_input::<_, _, ()>(
&mut context.as_battle_context_mut().applying_effect_context(
EffectHandle::Item(item_id.clone()),
None,
target,
Some(EffectHandle::Condition(Id::from_known("playeruseditem"))),
)?,
fxlang::BattleEvent::PlayerUse,
input.input_for_fxlang_callback(),
),
None => (),
}
if let Some(target) = target {
if item_is_ball {
try_catch(context, target, &item_id)?;
}
}
Ok(true)
}
pub fn revive(context: &mut ApplyingEffectContext, hp: u16) -> Result<u16> {
if context.target().exited != Some(MonExitType::Fainted) {
return Ok(0);
}
let effect = context.effect_handle().clone();
let source = context.source_handle();
core_battle_logs::revive(&mut context.target_context()?, Some(effect), source)?;
let hp = Mon::revive(&mut context.target_context()?, hp)?;
Ok(hp)
}
pub fn deduct_pp(context: &mut ApplyingEffectContext, move_id: &Id, amount: u8) -> Result<u8> {
let delta = context.target_mut().deduct_pp(move_id, amount);
if delta != 0 {
core_battle_logs::deduct_pp(context, move_id, delta)?;
}
Ok(delta)
}
pub fn restore_pp(
context: &mut ApplyingEffectContext,
move_id: &Id,
amount: u8,
silent: bool,
) -> Result<u8> {
let amount = if force_healing_effect(context) {
amount
} else {
core_battle_effects::run_event_with_relay::<_, u8>(
context,
fxlang::BattleEvent::RestorePp,
amount,
)
};
let delta = context.target_mut().restore_pp(move_id, amount);
if delta != 0 && !silent {
core_battle_logs::restore_pp(context, move_id, delta)?;
}
Ok(delta)
}
pub fn set_pp(context: &mut ApplyingEffectContext, move_id: &Id, pp: u8) -> Result<u8> {
let pp = context.target_mut().set_pp(move_id, pp);
core_battle_logs::set_pp(context, move_id, pp)?;
Ok(pp)
}
pub fn clear_boosts(context: &mut ApplyingEffectContext, silent: bool) -> Result<()> {
if !context.target().active {
return Ok(());
}
context.target_mut().clear_boosts();
if !silent {
core_battle_logs::clear_boosts(context)?;
}
Ok(())
}
pub fn clear_negative_boosts(context: &mut ApplyingEffectContext) -> Result<()> {
if !context.target().active {
return Ok(());
}
context.target_mut().volatile_state.boosts = context
.target()
.volatile_state
.boosts
.non_zero_iter()
.filter(|(_, val)| *val > 0)
.collect();
core_battle_logs::clear_negative_boosts(context)?;
Ok(())
}
pub fn clear_positive_boosts(context: &mut ApplyingEffectContext) -> Result<()> {
if !context.target().active {
return Ok(());
}
context.target_mut().volatile_state.boosts = context
.target()
.volatile_state
.boosts
.non_zero_iter()
.filter(|(_, val)| *val < 0)
.collect();
core_battle_logs::clear_positive_boosts(context)?;
Ok(())
}
pub fn try_catch(context: &mut MonContext, target: MonHandle, item: &Id) -> Result<bool> {
let item = context.battle().dex.items.get_by_id(item)?;
if !item.data.flags.contains(&ItemFlag::Ball) {
return Ok(false);
}
let item_id = item.id().clone();
let player_type = context
.as_battle_context_mut()
.mon_context(target)?
.player()
.player_type;
if !player_type.catchable() {
core_battle_logs::uncatchable(context.as_player_context_mut(), target, player_type.wild())?;
Player::put_item_in_bag(context.as_player_context_mut(), item_id);
return Ok(false);
}
let source = context.mon_handle();
let catch_rate = calculate_modified_catch_rate(
&mut context.as_battle_context_mut().applying_effect_context(
EffectHandle::Item(item_id.clone()),
Some(source),
target,
None,
)?,
)?;
let shake_probability = calculate_shake_probability(catch_rate)?;
if context.battle().engine_options.log_catch_rate {
context.battle_mut().log(crate::battle_log_entry!(
"catchrate",
("catchrate", format!("{catch_rate}/1044480")),
("shakeprobability", format!("{shake_probability}/65536")),
));
}
let critical = check_critical_capture(context.as_player_context_mut(), shake_probability);
let total_shakes = if critical { 1 } else { 4 };
let mut shakes = 0;
for _ in 0..total_shakes {
let rand = rand_util::range(context.battle_mut().prng.as_mut(), 0, 65536);
if rand < shake_probability {
shakes += 1;
} else {
break;
}
}
if shakes != total_shakes {
core_battle_logs::catch_failed(
context.as_player_context_mut(),
target,
&item_id,
shakes,
critical,
)?;
let item = EffectHandle::Item(item_id.clone());
core_battle_effects::run_effect_event::<_, ()>(
&mut context.applying_effect_context(item.clone(), None, None)?,
fxlang::BattleEvent::CatchFailed,
);
core_battle_effects::run_event_with_input::<_, _, ()>(
context,
fxlang::BattleEvent::CatchFailed,
item,
);
return Ok(false);
}
let player = context.player().index;
Mon::catch(
&mut context.as_battle_context_mut().mon_context(target)?,
player,
item_id,
shakes,
critical,
)?;
Ok(true)
}
fn calculate_modified_catch_rate(context: &mut ApplyingEffectContext) -> Result<u64> {
let max_hp_factor = Fraction::from(3 * context.target().max_hp as u64);
let hp_factor = max_hp_factor - 2 * context.target().hp as u64;
let hp_factor = hp_factor / max_hp_factor;
let a = hp_factor * 4096;
let catch_rate = context
.battle()
.dex
.species
.get_by_id(&context.target().base_species)?
.data
.catch_rate;
let catch_rate = core_battle_effects::run_event_with_relay::<_, u64>(
context,
fxlang::BattleEvent::ModifySpeciesCatchRate,
catch_rate as u64,
);
let catch_rate = catch_rate.min(255);
let a = a * catch_rate;
let a = core_battle_effects::run_event_with_relay::<_, u64>(
context,
fxlang::BattleEvent::ModifyCatchRate,
a.floor(),
);
let a = a.min(1044480);
Ok(a)
}
fn calculate_shake_probability(catch_rate: u64) -> Result<u64> {
const MULTIPLIER_FOR_PRECISION: u128 = 4096;
const MAX_CATCH_RATE: u128 = 1044480;
let b = Fraction::new(
catch_rate as u128 * MULTIPLIER_FOR_PRECISION,
MAX_CATCH_RATE * MULTIPLIER_FOR_PRECISION,
);
let b = b
.pow(Fraction::new(3i32, 16i32))
.map_err(integer_overflow_error)?;
let b = b * 65536;
b.floor().try_into().map_err(integer_overflow_error)
}
fn check_critical_capture(context: &mut PlayerContext, catch_rate: u64) -> bool {
let mons_caught = context.player().player_options.mons_caught;
let multiplier = if mons_caught > 600 {
5
} else if mons_caught > 450 {
4
} else if mons_caught > 300 {
3
} else if mons_caught > 150 {
2
} else if mons_caught > 30 {
1
} else {
0
};
let c = Fraction::new(multiplier, 2) * catch_rate;
let c = c / 6;
let c = c.floor();
let rand = rand_util::range(context.battle_mut().prng.as_mut(), 0, 65536);
rand < c
}
pub fn end_battle(context: &mut Context) -> Result<()> {
core_battle_effects::run_event_for_each_active_mon(context, fxlang::BattleEvent::EndBattle)?;
for mon in context
.battle()
.all_active_mon_handles()
.collect::<Vec<_>>()
{
Mon::recalculate_base_stats(
&mut context.mon_context(mon)?,
RecalculateStatsHpPolicy::KeepHealthRatio { silent: true },
)?;
}
Ok(())
}
pub struct MegaEvolution {
pub source_effect: EffectHandle,
pub species: Id,
}
pub fn can_mega_evolve(context: &mut MonContext) -> Result<Option<MegaEvolution>> {
if !context.player().can_mega_evolve || !context.mon().active {
return Ok(None);
}
let species = context
.battle()
.dex
.species
.get_by_id(&context.mon().base_species)?;
let forme = species
.data
.formes
.iter()
.map(|forme| Id::from(forme.as_ref()))
.find(|forme| {
context
.battle()
.dex
.species
.get_by_id(&forme)
.is_ok_and(|forme| forme.data.mega())
});
if let Some(forme) = forme {
let forme = context.battle().dex.species.get_by_id(&forme)?;
let required_moves = forme
.data
.required_moves
.iter()
.map(|move_name| Id::from(move_name.as_ref()))
.collect::<HashSet<_>>();
if !required_moves.is_empty() {
let base_moves = context
.mon()
.base_move_slots
.iter()
.map(|move_slot| move_slot.id.clone())
.collect();
let required_move_known = required_moves.intersection(&base_moves).next();
return Ok(
required_move_known.map(|required_move_known| MegaEvolution {
source_effect: EffectHandle::InactiveMove(required_move_known.clone()),
species: forme.id().clone(),
}),
);
}
}
let item = match context.mon().item.clone() {
Some(item) => item,
None => return Ok(None),
};
let item = context.battle().dex.items.get_by_id(&item)?;
let mega_evolution_data = match item.data.special_data.mega_evolution.clone() {
Some(data) => data,
None => return Ok(None),
};
if Id::from(mega_evolution_data.from) != *species.id() {
return Ok(None);
}
return Ok(Some(MegaEvolution {
source_effect: EffectHandle::Item(item.id().clone()),
species: Id::from(mega_evolution_data.into),
}));
}
pub struct UltraBurst {
pub source_effect: EffectHandle,
pub species: Id,
}
pub fn can_ultra_burst(context: &mut MonContext) -> Result<Option<UltraBurst>> {
if !context.player().can_ultra_burst || !context.mon().active {
return Ok(None);
}
let species = context
.battle()
.dex
.species
.get_by_id(&context.mon().base_species)?;
let item = match context.mon().item.clone() {
Some(item) => item,
None => return Ok(None),
};
let item = context.battle().dex.items.get_by_id(&item)?;
let ultra_burst_data = match item.data.special_data.ultra_burst.clone() {
Some(data) => data,
None => return Ok(None),
};
if !ultra_burst_data
.from
.iter()
.any(|from| Id::from(from.as_str()) == *species.id())
{
return Ok(None);
}
return Ok(Some(UltraBurst {
source_effect: EffectHandle::Item(item.id().clone()),
species: Id::from(ultra_burst_data.into),
}));
}
pub fn copy_boosts(
context: &mut ApplyingEffectContext,
target: MonHandle,
silent: bool,
) -> Result<EventResult> {
let mut target_context = context.as_battle_context_mut().mon_context(target)?;
if !target_context.mon().active {
return Ok(EventResult::Fail);
}
let boosts = target_context.mon().volatile_state.boosts.clone();
let mut volatiles: HashMap<Id, EffectState> = HashMap::default();
for volatile in target_context
.mon()
.volatile_state
.volatiles
.keys()
.cloned()
.collect::<Vec<_>>()
{
if CoreBattle::get_parsed_effect_by_id(target_context.as_battle_context_mut(), &volatile)?
.is_none_or(|condition| !condition.condition().copy_with_boosts)
{
continue;
}
let state = target_context
.mon()
.volatile_state
.volatiles
.get(&volatile)
.cloned()
.wrap_expectation("expected volatile status state to exist after lookup")?;
volatiles.insert(volatile, state);
}
context.target_mut().volatile_state.boosts = boosts;
if !silent {
core_battle_logs::copy_boosts(context, target)?;
}
for volatile in context
.target()
.volatile_state
.volatiles
.keys()
.cloned()
.collect::<Vec<_>>()
{
if CoreBattle::get_parsed_effect_by_id(context.as_battle_context_mut(), &volatile)?
.is_none_or(|condition| !condition.condition().copy_with_boosts)
{
continue;
}
remove_volatile(context, &volatile, false)?;
}
copy_volatiles(context, volatiles)?;
Ok(EventResult::Advance)
}
pub fn transform_into(
context: &mut ApplyingEffectContext,
target: MonHandle,
) -> Result<EventResult> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if !context.target().active
|| context.target().volatile_state.transformed
|| context.target().volatile_state.illusion.is_some()
{
return Ok(EventResult::Fail);
}
let target_context = context.as_battle_context_mut().mon_context(target)?;
if !target_context.mon().active
|| target_context.mon().volatile_state.transformed
|| target_context.mon().volatile_state.illusion.is_some()
{
return Ok(EventResult::Fail);
}
let weight = target_context.mon().volatile_state.weight;
let types = target_context.mon().volatile_state.types.clone();
let stats = target_context
.mon()
.volatile_state
.base_stored_stats
.clone();
let ability_id = target_context
.mon()
.volatile_state
.ability_slot
.ability
.id
.clone();
let mut move_slots = target_context.mon().volatile_state.move_slots.clone();
for move_slot in &mut move_slots {
move_slot.pp = move_slot.max_pp.min(5);
move_slot.max_pp = move_slot.max_pp.min(5);
move_slot.disabled = false;
move_slot.used = false;
move_slot.simulated = true;
}
let times_attacked = target_context.mon().volatile_state.times_attacked;
let species = target_context.mon().volatile_state.species.clone();
context.target_mut().volatile_state.transformed = true;
Mon::set_species(
&mut context.target_context()?,
&Id::from(species),
RecalculateStatsHpPolicy::DoNotUpdate,
)?;
context.target_mut().volatile_state.weight = weight;
Mon::set_types(&mut context.target_context()?, types)?;
Mon::set_stats(
&mut context.target_context()?,
stats,
false,
RecalculateStatsHpPolicy::DoNotUpdate,
)?;
set_ability(context, &ability_id, false, true, true)?;
context.target_mut().volatile_state.move_slots = move_slots;
context.target_mut().volatile_state.times_attacked = times_attacked;
core_battle_logs::transform(context, target)?;
try_event!(copy_boosts(context, target, true)?, Ok);
Ok(EventResult::Advance)
}
pub fn set_illusion(context: &mut ApplyingEffectContext, target: MonHandle) -> Result<EventResult> {
if context.target_handle() == target || context.target().volatile_state.illusion.is_some() {
return Ok(EventResult::Fail);
}
let illusion = Mon::physical_details(&context.as_battle_context_mut().mon_context(target)?)?;
context.target_mut().volatile_state.illusion = Some(illusion);
Ok(EventResult::Advance)
}
pub fn end_illusion(context: &mut ApplyingEffectContext) -> Result<EventResult> {
if context.target().volatile_state.illusion.is_none() {
return Ok(EventResult::Fail);
}
context.target_mut().volatile_state.illusion = None;
core_battle_logs::replace(&mut context.target_context()?)?;
Ok(EventResult::Advance)
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum FormeChangeType {
Temporary,
Permanent { revert_on_exit: bool },
MegaEvolution,
PrimalReversion,
UltraBurst,
Gigantamax,
}
impl FormeChangeType {
pub fn permanent(&self) -> bool {
match self {
Self::Temporary | Self::Gigantamax => false,
Self::Permanent { .. }
| Self::MegaEvolution
| Self::PrimalReversion
| Self::UltraBurst => true,
}
}
pub fn set_ability_even_if_unchanged(&self) -> bool {
match self {
Self::Temporary | Self::Permanent { .. } | Self::Gigantamax => false,
Self::MegaEvolution | Self::PrimalReversion | Self::UltraBurst => true,
}
}
}
pub fn forme_change(
context: &mut ApplyingEffectContext,
species: &Id,
forme_change_type: FormeChangeType,
) -> Result<EventResult> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
let old_ability = context.target().base_ability.clone();
if !Mon::set_species(
&mut context.target_context()?,
species,
RecalculateStatsHpPolicy::KeepDamageTaken {
silent: forme_change_type.permanent(),
},
)? {
return Ok(EventResult::Fail);
}
if forme_change_type.permanent() {
let mut context = context.target_context()?;
core_battle_logs::species_change(&mut context)?;
let ability = context
.battle()
.dex
.species
.get_by_id(&species)?
.data
.abilities
.first()
.map(|ability| Id::from(ability.as_ref()))
.unwrap_or(Id::from_known("noability"));
Mon::set_base_species(&mut context, species, &ability)?;
}
match forme_change_type {
FormeChangeType::Temporary | FormeChangeType::Permanent { .. } => {
core_battle_logs::forme_change(context)?;
}
FormeChangeType::MegaEvolution => {
core_battle_logs::mega_evolution(context)?;
}
FormeChangeType::PrimalReversion => {
core_battle_logs::primal_reversion(context)?;
}
FormeChangeType::UltraBurst => {
core_battle_logs::ultra_burst(context)?;
}
FormeChangeType::Gigantamax => {
core_battle_logs::gigantamax(context)?;
}
}
let new_ability = context.target().base_ability.clone();
if (old_ability != new_ability || forme_change_type.set_ability_even_if_unchanged())
&& context.target().hp != 0
{
set_ability(context, &new_ability, false, true, true)?;
}
if let FormeChangeType::Permanent {
revert_on_exit: true,
..
} = forme_change_type
{
context.target_mut().revert_forme_change_on_exit = true;
}
Ok(EventResult::Advance)
}
pub fn revert_on_exit(context: &mut ApplyingEffectContext) -> Result<()> {
let needs_mechanic_revert =
context.target().dynamaxed || context.target().terastallized.is_some();
let needs_forme_change_revert = match context.target().special_forme_change_type {
Some(
MonSpecialFormeChangeType::MegaEvolution
| MonSpecialFormeChangeType::PrimalReversion
| MonSpecialFormeChangeType::UltraBurst
| MonSpecialFormeChangeType::Gigantamax,
) => true,
None => context.target().revert_forme_change_on_exit,
};
let needs_revert = needs_mechanic_revert || needs_forme_change_revert;
if !needs_revert {
return Ok(());
}
revert(
context,
RecalculateStatsHpPolicy::KeepDamageTaken { silent: false },
)
}
fn revert(context: &mut ApplyingEffectContext, hp_policy: RecalculateStatsHpPolicy) -> Result<()> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
let revert_forme = context.target().special_forme_change_type.is_some()
|| context.target().revert_forme_change_on_exit;
let base_species_update = match context.target().special_forme_change_type {
Some(special_forme_change_type) => match special_forme_change_type {
MonSpecialFormeChangeType::MegaEvolution
| MonSpecialFormeChangeType::PrimalReversion
| MonSpecialFormeChangeType::UltraBurst => true,
MonSpecialFormeChangeType::Gigantamax => false,
},
None => context.target().revert_forme_change_on_exit,
};
if revert_forme {
let species = context.target().original_base_species.clone();
let ability = context.target().original_base_ability.clone();
{
let mut context = context.target_context()?;
Mon::set_species(&mut context, &species, hp_policy)?;
if base_species_update {
core_battle_logs::species_change(&mut context)?;
Mon::set_base_species(&mut context, &species, &ability)?;
}
}
match context.target().special_forme_change_type {
Some(MonSpecialFormeChangeType::MegaEvolution) => {
core_battle_logs::revert_mega_evolution(context)?;
}
Some(MonSpecialFormeChangeType::PrimalReversion) => {
core_battle_logs::revert_primal_reversion(context)?;
}
Some(MonSpecialFormeChangeType::UltraBurst) => {
core_battle_logs::revert_ultra_burst(context)?;
}
Some(MonSpecialFormeChangeType::Gigantamax) => {
core_battle_logs::revert_gigantamax(context)?;
}
None => {
core_battle_logs::forme_change(context)?;
}
}
context.target_mut().special_forme_change_type = None;
context.target_mut().revert_forme_change_on_exit = false;
}
if context.target().dynamaxed {
end_dynamax(context)?;
}
if context.target().terastallized.is_some() {
end_terastallization(context)?;
}
Ok(())
}
pub fn mega_evolve(context: &mut MonContext) -> Result<EventResult> {
let mega_evolution = match can_mega_evolve(context)? {
Some(mega_evolution) => mega_evolution,
None => return Ok(EventResult::Fail),
};
let target = context.mon_handle();
try_event!(
forme_change(
&mut context.as_battle_context_mut().applying_effect_context(
mega_evolution.source_effect,
None,
target,
None,
)?,
&mega_evolution.species,
FormeChangeType::MegaEvolution,
)?,
Ok
);
context.mon_mut().special_forme_change_type = Some(MonSpecialFormeChangeType::MegaEvolution);
core_battle_effects::run_event::<_, ()>(context, fxlang::BattleEvent::AfterMegaEvolution);
context.player_mut().can_mega_evolve = false;
Ok(EventResult::Advance)
}
pub fn primal_reversion(context: &mut ApplyingEffectContext, species: &Id) -> Result<EventResult> {
if !context
.battle()
.format
.rules
.has_rule(&Id::from_known("primalreversion"))
{
return Ok(EventResult::Fail);
}
try_event!(
forme_change(context, species, FormeChangeType::PrimalReversion)?,
Ok
);
context.target_mut().special_forme_change_type =
Some(MonSpecialFormeChangeType::PrimalReversion);
Ok(EventResult::Advance)
}
pub fn ultra_burst(context: &mut MonContext) -> Result<EventResult> {
let ultra_burst = match can_ultra_burst(context)? {
Some(ultra_burst) => ultra_burst,
None => return Ok(EventResult::Fail),
};
let target = context.mon_handle();
try_event!(
forme_change(
&mut context.as_battle_context_mut().applying_effect_context(
ultra_burst.source_effect,
None,
target,
None,
)?,
&ultra_burst.species,
FormeChangeType::UltraBurst,
)?,
Ok
);
context.mon_mut().special_forme_change_type = Some(MonSpecialFormeChangeType::UltraBurst);
context.player_mut().can_ultra_burst = false;
Ok(EventResult::Advance)
}
pub fn can_dynamax(context: &mut MonContext) -> Result<Option<Id>> {
if !context.player().can_dynamax || !context.mon().active || !Mon::can_dynamax(context)? {
return Ok(None);
}
if context.mon().gigantamax_factor {
let species = context
.battle()
.dex
.species
.get_by_id(&context.mon().base_species)?;
let forme = species
.data
.formes
.iter()
.map(|forme| Id::from(forme.as_ref()))
.find(|forme| {
context
.battle()
.dex
.species
.get_by_id(&forme)
.is_ok_and(|forme| forme.data.gigantamax())
});
if let Some(forme) = forme {
return Ok(Some(forme));
}
}
Ok(Some(context.mon().volatile_state.species.clone()))
}
pub fn dynamax(context: &mut MonContext) -> Result<EventResult> {
let forme = match can_dynamax(context)? {
Some(forme) => forme,
None => return Ok(EventResult::Fail),
};
let target = context.mon_handle();
let mut context = context.as_battle_context_mut().applying_effect_context(
EffectHandle::Condition(Id::from_known("dynamax")),
None,
target,
None,
)?;
try_event!(
core_battle_effects::run_event::<_, EventResult>(
&mut context.target_context()?,
fxlang::BattleEvent::BeforeDynamax,
),
Ok
);
if forme != context.target().volatile_state.species {
if forme_change(&mut context, &forme, FormeChangeType::Gigantamax)?.advance() {
context.target_mut().special_forme_change_type =
Some(MonSpecialFormeChangeType::Gigantamax);
}
}
try_event!(
add_volatile(&mut context, &Id::from_known("dynamax"), false, None)?,
Ok
);
core_battle_logs::dynamax(&mut context)?;
context.target_mut().dynamaxed = true;
Mon::update_max_hp(
&mut context.target_context()?,
RecalculateStatsHpPolicy::KeepHealthRatio { silent: false },
)?;
context.target_context()?.player_mut().can_dynamax = false;
Ok(EventResult::Advance)
}
pub fn end_dynamax(context: &mut ApplyingEffectContext) -> Result<()> {
if !context.target().dynamaxed {
return Ok(());
}
context.target_mut().dynamaxed = false;
if context.target().special_forme_change_type == Some(MonSpecialFormeChangeType::Gigantamax) {
revert(context, RecalculateStatsHpPolicy::DoNotUpdate)?;
}
core_battle_logs::revert_dynamax(context)?;
remove_volatile(context, &Id::from_known("dynamax"), false)?;
Mon::update_max_hp(
&mut context.target_context()?,
RecalculateStatsHpPolicy::KeepHealthRatioCeiling,
)?;
Ok(())
}
pub fn max_move_by_id(context: &mut MonContext, move_id: &Id) -> Result<Option<Id>> {
let mov = context.battle().dex.moves.get_by_id(&move_id)?;
let move_data = mov.data.clone();
max_move(context, &move_data)
}
pub fn max_move(context: &mut MonContext, move_data: &MoveData) -> Result<Option<Id>> {
if move_data.category == MoveCategory::Status {
return Ok(Some(Id::from_known("maxguard")));
}
let mut species = context.mon().volatile_state.species.clone();
if !context.mon().dynamaxed
&& let Some(dynamax_forme) = can_dynamax(context)?
{
species = dynamax_forme.clone();
}
let species = context.battle().dex.species.get_by_id(&species)?;
if let Some(gigantamax_move) = &species.data.gigantamax_move {
if let Ok(gigantamax_move) = context
.battle()
.dex
.moves
.get_by_id(&Id::from(gigantamax_move.as_str()))
&& gigantamax_move.data.primary_type == move_data.primary_type
{
return Ok(Some(gigantamax_move.id().clone()));
}
}
if move_data.max_move.is_none() {
return Ok(None);
}
let id = match move_data.primary_type {
Type::Flying => "maxairstream",
Type::Dark => "maxdarkness",
Type::Fire => "maxflare",
Type::Bug => "maxflutterby",
Type::Water => "maxgeyser",
Type::Ice => "maxhailstorm",
Type::Fighting => "maxknuckle",
Type::Electric => "maxlightning",
Type::Psychic => "maxmindstorm",
Type::Poison => "maxooze",
Type::Grass => "maxovergrowth",
Type::Ghost => "maxphantasm",
Type::Ground => "maxquake",
Type::Rock => "maxrockfall",
Type::Fairy => "maxstarfall",
Type::Steel => "maxsteelspike",
Type::Normal | Type::None | Type::Stellar => "maxstrike",
Type::Dragon => "maxwyrmwind",
};
match context.battle().dex.moves.get_by_id(&Id::from_known(id)) {
Ok(max_move) => Ok(Some(max_move.id().clone())),
Err(_) => Ok(None),
}
}
pub fn can_terastallize(context: &mut MonContext) -> Result<Option<Type>> {
if !context.player().can_terastallize || !context.mon().active {
return Ok(None);
}
let tera_type = mon_states::effective_tera_type(context);
if tera_type == Type::None {
return Ok(None);
}
Ok(Some(tera_type))
}
pub fn terastallize(context: &mut MonContext) -> Result<EventResult> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
let tera_type = match can_terastallize(context)? {
Some(tera_type) => tera_type,
None => return Ok(EventResult::Fail),
};
let target = context.mon_handle();
let mut context = context.as_battle_context_mut().applying_effect_context(
EffectHandle::Condition(Id::from_known("terastallization")),
None,
target,
None,
)?;
try_event!(
core_battle_effects::run_event::<_, EventResult>(
&mut context.target_context()?,
fxlang::BattleEvent::BeforeTerastallization,
),
Ok
);
core_battle_logs::terastallize(&mut context, tera_type)?;
context.target_mut().terastallized = Some(tera_type);
context.target_mut().terastallization_state = fxlang::EffectState::initial_effect_state(
context.as_battle_context_mut(),
None,
Some(target),
None,
)?;
core_battle_effects::run_event::<_, ()>(
&mut context.target_context()?,
fxlang::BattleEvent::AfterTerastallization,
);
context.target_context()?.player_mut().can_terastallize = false;
Ok(EventResult::Advance)
}
pub fn end_terastallization(context: &mut ApplyingEffectContext) -> Result<()> {
let context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
if context.target().terastallized.is_none() {
return Ok(());
}
context.target_mut().terastallized = None;
context.target_mut().terastallization_state = fxlang::EffectState::default();
core_battle_logs::revert_terastallization(context)?;
Ok(())
}
pub fn swap_position(
context: &mut ApplyingEffectContext,
new_position: usize,
allow_empty_target: bool,
) -> Result<EventResult> {
if !context.target().active || context.target().hp == 0 {
return Ok(EventResult::Fail);
}
let ((source, old_position), (target, new_position)) = {
let context = context.target_context()?;
if new_position > context.player().total_active_positions() {
return Ok(EventResult::Fail);
}
let source = context.mon_handle();
let target = context.player().active_mon_handle(new_position);
if target.is_none() && !allow_empty_target {
return Ok(EventResult::Fail);
}
let old_position = match context.mon().active_position {
Some(old_position) => old_position,
None => return Ok(EventResult::Fail),
};
if new_position == old_position {
return Ok(EventResult::Fail);
}
((source, old_position), (target, new_position))
};
core_battle_logs::swap(context, new_position + 1)?;
{
let mut context = context.target_context()?;
context
.player_mut()
.set_active_position(new_position, Some(source))?;
context
.player_mut()
.set_active_position(old_position, target)?;
context.mon_mut().active_position = Some(new_position);
context.mon_mut().effective_team_position = new_position;
if let Some(target) = target {
let mut context = context.as_player_context_mut().mon_context(target)?;
context.mon_mut().active_position = Some(old_position);
context.mon_mut().effective_team_position = old_position;
}
}
let effect_handle = context.effect_handle().clone();
let source_effect_handle = context.source_effect_handle().cloned();
if let Some(target) = target {
core_battle_effects::run_event::<_, ()>(
&mut context.as_battle_context_mut().applying_effect_context(
effect_handle.clone(),
Some(source),
target,
source_effect_handle.clone(),
)?,
fxlang::BattleEvent::Swap,
);
}
core_battle_effects::run_event::<_, ()>(
&mut context.as_battle_context_mut().applying_effect_context(
effect_handle,
target,
source,
source_effect_handle,
)?,
fxlang::BattleEvent::Swap,
);
Ok(EventResult::Advance)
}
pub fn swap_player_position(context: &mut PlayerContext, new_position: usize) -> Result<()> {
core_battle_logs::swap_player(context, new_position)?;
let current_position = context.player().position;
let target = Side::player_position_to_index(context.as_side_context(), new_position);
if let Some(target) = target {
context
.as_battle_context_mut()
.player_context(target)?
.player_mut()
.position = current_position;
}
context.player_mut().position = new_position;
Ok(())
}
pub fn can_z_move(context: &mut MonContext) -> Result<Vec<Option<Id>>> {
let z_crystal = match usable_z_crystal(context)? {
Some(z_crystal) => z_crystal,
None => return Ok(Vec::default()),
};
let mut z_moves = Vec::default();
let mut no_usable_moves = true;
for move_slot in context.mon().volatile_state.move_slots.clone() {
if !move_slot.disabled {
no_usable_moves = false;
}
let mov = context.battle().dex.moves.get(&move_slot.name)?;
z_moves.push(z_move(context, mov.id(), &mov.data, &z_crystal, false)?);
}
if z_moves.iter().all(|z_move| z_move.is_none()) || no_usable_moves {
return Ok(Vec::default());
}
Ok(z_moves)
}
pub fn z_move_by_id(context: &mut MonContext, move_id: &Id) -> Result<Option<Id>> {
let mov = context.battle().dex.moves.get_by_id(&move_id)?;
let move_data = mov.data.clone();
z_move_by_move_data(context, move_id, &move_data, false)
}
pub fn z_move_by_move_data(
context: &mut MonContext,
move_id: &Id,
move_data: &MoveData,
external: bool,
) -> Result<Option<Id>> {
let z_crystal = match usable_z_crystal(context)? {
Some(z_crystal) => z_crystal,
None => return Ok(None),
};
z_move(context, move_id, move_data, &z_crystal, external)
}
fn z_move(
context: &MonContext,
move_id: &Id,
move_data: &MoveData,
z_crystal: &ZCrystalData,
external: bool,
) -> Result<Option<Id>> {
if !external {
let index = match context.mon().move_slot_index(move_id) {
Some(index) => index,
None => return Ok(None),
};
let move_slot = context
.mon()
.volatile_state
.move_slots
.get(index)
.wrap_expectation_with_format(format_args!("expected move slot at index {index}"))?;
if move_slot.pp == 0 {
return Ok(None);
}
}
let z_move = match &z_crystal.source {
Some(ZCrystalSource::Move(from)) if Id::from(from.as_str()) == *move_id => {
Some(Id::from(z_crystal.into.as_str()))
}
Some(ZCrystalSource::Type(move_type)) if move_data.primary_type == *move_type => {
if move_data.category == MoveCategory::Status {
Some(move_id.clone())
} else {
Some(Id::from(z_crystal.into.as_str()))
}
}
Some(ZCrystalSource::Type(move_type)) if external => {
if move_data.category == MoveCategory::Status {
Some(move_id.clone())
} else {
let id = match move_data.primary_type {
Type::Flying => "supersonicskystrike",
Type::Dark => "blackholeeclipse",
Type::Fire => "infernooverdrive",
Type::Bug => "savagespinout",
Type::Water => "hydrovortex",
Type::Ice => "subzeroslammer",
Type::Fighting => "alloutpummeling",
Type::Electric => "gigavolthavoc",
Type::Psychic => "shatteredpsyche",
Type::Poison => "aciddownpour",
Type::Grass => "bloomdoom",
Type::Ghost => "neverendingnightmare",
Type::Ground => "tectonicrage",
Type::Rock => "continentalcrush",
Type::Fairy => "twinkletackle",
Type::Steel => "corkscrewcrash",
Type::Normal | Type::None | Type::Stellar => "breakneckblitz",
Type::Dragon => "devastatingdrake",
};
Some(Id::from(id))
}
}
_ => None,
};
Ok(z_move)
}
fn usable_z_crystal(context: &mut MonContext) -> Result<Option<ZCrystalData>> {
if !context.player().can_z_move || !context.mon().active {
return Ok(None);
}
let species = context
.battle()
.dex
.species
.get_by_id(&context.mon().volatile_state.species)?;
let item = match context.mon().item.clone() {
Some(item) => item,
None => return Ok(None),
};
let item = context.battle().dex.items.get_by_id(&item)?;
let z_crystal = match item.data.special_data.z_crystal.clone() {
Some(z_crystal) => z_crystal,
None => return Ok(None),
};
if !z_crystal.users.is_empty()
&& z_crystal
.users
.iter()
.all(|user| Id::from(user.as_str()) != *species.id())
{
return Ok(None);
}
Ok(Some(z_crystal))
}
struct ConditionContext<'context, 'battle, 'data, Context> {
context: &'context mut Context,
effect_id: Id,
effect_handle: EffectHandle,
_phantom: (PhantomData<&'battle ()>, PhantomData<&'data ()>),
}
trait StartConditionCallbacks<'context, 'battle, 'data, Context>
where
'data: 'battle,
Context: core_battle_effects::EventContext<'battle, 'data>,
{
#[allow(unused_variables)]
fn event_input(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> fxlang::VariableInput {
fxlang::VariableInput::default()
}
fn pre_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<EventResult>;
fn exists(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<bool>;
fn pre_apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<EventResult>;
fn apply(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
effect_state: fxlang::EffectState,
) -> Result<()>;
fn rollback(
self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<()>;
#[allow(unused_variables)]
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<()> {
Ok(())
}
fn post_start(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<()>;
fn end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<()>;
}
fn start_condition<'context, 'battle, 'data, Context, Callbacks>(
context: &'context mut Context,
id: &Id,
location: AppliedEffectLocation,
link_to: Option<&AppliedEffectHandle>,
mut callbacks: Callbacks,
) -> Result<EventResult>
where
'data: 'battle,
Context: core_battle_effects::EventContext<'battle, 'data>,
Callbacks: StartConditionCallbacks<'context, 'battle, 'data, Context>,
{
let effect_handle = context
.as_battle_context_mut()
.battle_mut()
.get_effect_handle_by_id(id)?
.clone();
let effect_id = effect_handle
.try_id()
.wrap_expectation("condition must have an id")?
.clone();
let applied_effect_handle = AppliedEffectHandle::new(effect_handle.clone(), location);
let target_handle = context.target();
let source_handle = context.source();
let (before_start_event, start_event, restart_event, modify_duration_event) =
if location.mon_handle().is_some() {
(
Some(fxlang::BattleEvent::BeforeStart),
fxlang::BattleEvent::Start,
fxlang::BattleEvent::Restart,
fxlang::BattleEvent::ModifyDuration,
)
} else if location.side_index().is_some() {
(
None,
fxlang::BattleEvent::SideStart,
fxlang::BattleEvent::SideRestart,
fxlang::BattleEvent::ModifySideDuration,
)
} else if location.slot_index().is_some() {
(
None,
fxlang::BattleEvent::SlotStart,
fxlang::BattleEvent::SlotRestart,
fxlang::BattleEvent::ModifySlotDuration,
)
} else if location.field() {
(
None,
fxlang::BattleEvent::FieldStart,
fxlang::BattleEvent::FieldRestart,
fxlang::BattleEvent::ModifyFieldDuration,
)
} else {
return Err(general_error("condition applied to an undefined location"));
};
let scoped_context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
let context: &mut Context = &mut *scoped_context;
let context: &'context mut Context = unsafe { context.unsafely_detach_borrow_mut() };
let mut context = ConditionContext {
context,
effect_id,
effect_handle,
_phantom: (PhantomData, PhantomData),
};
let event_input = callbacks.event_input(&mut context);
try_event!(callbacks.pre_start(&mut context)?, Ok);
let mut restarted = false;
if callbacks.exists(&mut context)? {
try_event!(
core_battle_effects::run_effect_event_with_options::<_, _, Option<EventResult>>(
context.context,
restart_event,
event_input.clone(),
core_battle_effects::RunEffectEventOptions {
effects: Vec::from_iter([applied_effect_handle.clone()]),
},
)
.unwrap_or(EventResult::Fail),
Ok
);
restarted = true;
}
if !restarted {
try_event!(callbacks.pre_apply(&mut context)?, Ok);
let effect = context.context.effect();
let mut effect_state = fxlang::EffectState::initial_effect_state(
context.context.as_battle_context_mut(),
effect.as_ref(),
target_handle,
source_handle,
)?;
let mut condition_duration = None;
if let Some(condition) = CoreBattle::get_parsed_effect_by_handle(
context.context.as_battle_context_mut(),
&context.effect_handle,
)? && let Some(duration) = condition.condition().duration
{
condition_duration = Some(duration);
}
if let Some(duration) = core_battle_effects::run_effect_event_with_options::<_, _, Option<u8>>(
context.context,
fxlang::BattleEvent::Duration,
(),
core_battle_effects::RunEffectEventOptions {
effects: Vec::from_iter([AppliedEffectHandle::new(
context.effect_handle.clone(),
AppliedEffectLocation::None,
)]),
},
) {
condition_duration = Some(duration);
}
if let Some(duration) = condition_duration {
let input = fxlang::VariableInput::from_iter(
core::iter::once(duration.into())
.chain(event_input.clone().into_iter())
.chain(core::iter::once(context.effect_handle.clone().into())),
);
let duration = core_battle_effects::run_event_with_input::<_, _, Option<u8>>(
context.context,
modify_duration_event,
input,
)
.unwrap_or(duration);
effect_state.set_duration(duration);
}
callbacks.apply(&mut context, effect_state)?;
let result = if let Some(before_start_event) = before_start_event {
core_battle_effects::run_effect_event_with_options::<_, _, EventResult>(
context.context,
before_start_event,
event_input.clone(),
core_battle_effects::RunEffectEventOptions {
effects: Vec::from_iter([applied_effect_handle.clone()]),
},
)
} else {
EventResult::Advance
};
let result = result.and_then(|| {
core_battle_effects::run_effect_event_with_options::<_, _, EventResult>(
context.context,
start_event,
event_input.clone(),
core_battle_effects::RunEffectEventOptions {
effects: Vec::from_iter([applied_effect_handle.clone()]),
},
)
});
if !result.advance() {
callbacks.rollback(&mut context)?;
return Ok(result);
}
}
if let Some(link_to) = link_to {
LinkedEffectsManager::link(
context.context.as_battle_context_mut(),
link_to,
&applied_effect_handle,
)?;
}
if !restarted {
callbacks.log(&mut context)?;
callbacks.post_start(&mut context)?;
}
if let Some(effect_state_connector) = applied_effect_handle.effect_state_connector()
&& effect_state_connector.exists(context.context.as_battle_context_mut())?
&& effect_state_connector
.get_mut(context.context.as_battle_context_mut())?
.duration()
.is_some_and(|duration| duration == 0)
{
callbacks.end(&mut context)?;
}
Ok(EventResult::Advance)
}
trait EndConditionCallbacks<'context, 'battle, 'data, Context>
where
'data: 'battle,
Context: core_battle_effects::EventContext<'battle, 'data>,
{
#[allow(unused_variables)]
fn identify(&mut self, context: &mut &'context mut Context) -> Option<Id> {
None
}
#[allow(unused_variables)]
fn event_input(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> fxlang::VariableInput {
fxlang::VariableInput::default()
}
fn pre_end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<EventResult>;
#[allow(unused_variables)]
fn reset(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<()> {
Ok(())
}
#[allow(unused_variables)]
fn log(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<()> {
Ok(())
}
fn remove(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<()>;
fn post_end(
&mut self,
context: &mut ConditionContext<'context, 'battle, 'data, Context>,
) -> Result<()>;
}
fn end_condition<'context, 'battle, 'data, Context, Callbacks>(
mut context: &'context mut Context,
id: Option<&Id>,
location: AppliedEffectLocation,
no_events: bool,
mut callbacks: Callbacks,
) -> Result<EventResult>
where
'data: 'battle,
Context: core_battle_effects::EventContext<'battle, 'data>,
Callbacks: EndConditionCallbacks<'context, 'battle, 'data, Context>,
{
let id = match id {
Some(id) => id.clone(),
None => match callbacks.identify(&mut context) {
Some(id) => id,
None => return Ok(EventResult::Fail),
},
};
let effect_handle = context
.as_battle_context_mut()
.battle_mut()
.get_effect_handle_by_id(&id)?
.clone();
let effect_id = effect_handle
.try_id()
.wrap_expectation("condition must have an id")?
.clone();
let applied_effect_handle = AppliedEffectHandle::new(effect_handle.clone(), location);
let (try_end_event, end_event) = if location.mon_handle().is_some() {
(Some(fxlang::BattleEvent::TryEnd), fxlang::BattleEvent::End)
} else if location.side_index().is_some() {
(None, fxlang::BattleEvent::SideEnd)
} else if location.slot_index().is_some() {
(None, fxlang::BattleEvent::SlotEnd)
} else if location.field() {
(None, fxlang::BattleEvent::FieldEnd)
} else {
return Err(general_error(
"condition is ending on an undefined location",
));
};
let scoped_context = &mut scopeguard::guard(context, |context| {
CoreBattle::invalidate_effect_caches(context.as_battle_context_mut()).ok();
});
let context: &mut Context = &mut *scoped_context;
let context: &'context mut Context = unsafe { context.unsafely_detach_borrow_mut() };
let mut context = ConditionContext {
context,
effect_id,
effect_handle,
_phantom: (PhantomData, PhantomData),
};
let event_input = callbacks.event_input(&mut context);
if let Some(try_end_event) = try_end_event
&& !no_events
{
try_event!(
core_battle_effects::run_effect_event_with_options::<_, _, EventResult>(
context.context,
try_end_event,
event_input.clone(),
core_battle_effects::RunEffectEventOptions {
effects: Vec::from_iter([applied_effect_handle.clone()]),
},
),
Ok
);
}
try_event!(callbacks.pre_end(&mut context)?, Ok);
if let Some(effect_state_connector) = applied_effect_handle.effect_state_connector()
&& let exists = effect_state_connector.exists(context.context.as_battle_context_mut())?
&& (!exists
|| effect_state_connector
.get_mut(context.context.as_battle_context_mut())?
.ending())
{
return Ok(EventResult::Fail);
}
if !no_events {
core_battle_effects::run_effect_event_with_options::<_, _, ()>(
context.context,
end_event,
event_input,
core_battle_effects::RunEffectEventOptions {
effects: Vec::from_iter([applied_effect_handle.clone()]),
},
);
}
callbacks.log(&mut context)?;
if let Some(effect) = context.context.effect() {
let source_effect = context.context.source_effect();
LinkedEffectsManager::remove(
&mut context
.context
.as_battle_context_mut()
.effect_context(effect, source_effect)?,
applied_effect_handle.effect_handle.clone(),
applied_effect_handle.location,
)?;
}
callbacks.remove(&mut context)?;
callbacks.reset(&mut context)?;
if !no_events {
callbacks.post_end(&mut context)?;
}
Ok(EventResult::Advance)
}
pub fn swap_side_conditions(
context: &mut SideEffectContext,
source_side: usize,
conditions: HashSet<Id>,
) -> Result<EventResult> {
let success;
let mut target_side_conditions: BTreeMap<Id, EffectState> = BTreeMap::default();
let mut source_side_conditions: BTreeMap<Id, EffectState> = BTreeMap::default();
for condition in &conditions {
if let Some(state) = context.side_mut().conditions.remove(condition) {
target_side_conditions.insert(condition.clone(), state);
}
}
{
let mut context = context.change_target_context(source_side)?;
for condition in &conditions {
if let Some(state) = context.side_mut().conditions.remove(condition) {
source_side_conditions.insert(condition.clone(), state);
}
}
success = !target_side_conditions.is_empty() || !source_side_conditions.is_empty();
}
if success {
core_battle_logs::swap_side_conditions(context, source_side)?;
}
{
let target_side = context.side().index;
let mut context = context.change_target_context(source_side)?;
for (condition, state) in target_side_conditions {
core_battle_logs::swap_side_condition(&mut context, &condition, target_side)?;
context.side_mut().conditions.insert(condition, state);
}
}
for (condition, state) in source_side_conditions {
core_battle_logs::swap_side_condition(context, &condition, source_side)?;
context.side_mut().conditions.insert(condition, state);
}
Ok(EventResult::from(success))
}
pub fn select_into_position(context: &mut ApplyingEffectContext, position: usize) -> Result<()> {
let selected = context.target_handle();
let active_mon = context
.target_context()?
.player()
.active_or_exited_mon_handle(position)
.wrap_expectation("expected selected mon to correspond to an active mon")?;
let mut context = context.as_battle_context_mut().mon_context(active_mon)?;
core_battle_effects::run_event_with_input::<_, _, ()>(
&mut context,
fxlang::BattleEvent::Select,
selected,
);
context.mon_mut().volatile_state.select = None;
Ok(())
}