github-actions
Deploy to Hugging Face Spaces
6c7a453
from ..utils.data_loader import data_loader
from ..systems.damage_engine import smogon_damage_for_move
from typing import Optional, Tuple, Dict, Any, Union, List
import random
import re
class Move:
def __init__(self, name: str, move_data: Optional[Dict[str, Any]] = None):
self.name = name
self.data = move_data if move_data else data_loader.get_move(name)
self.id = self.data.get('id', name.lower().replace(' ', '')) if self.data else name.lower()
if not self.data:
self.power = 0
self.pp = 10
self.max_pp = 10
self.type = 'normal'
self.accuracy = 100
self.category = 'physical'
self.priority = 0
self.is_status_move = True
self.priority_counter_conditions = None
self.is_priority_counter = False
else:
self.power = self.data.get('basePower', 0)
self.pp = self.data.get('pp', 10)
self.max_pp = self.pp
self.type = self.data.get('type', 'normal').lower()
self.accuracy = self.data.get('accuracy', 100)
self.category = self.data.get('category', 'physical').lower()
self.priority = self.data.get('priority', 0)
self.is_status_move = self.category == 'status' or self.power == 0
self.priority_counter_conditions = self.data.get('priority_counter_conditions')
self.is_priority_counter = self.data.get('is_priority_counter', False)
self.is_healing_move, self.heal_amount, self.drain_ratio = self._parse_healing_data()
self.is_multihit_move, self.multihit_data = self._parse_multihit_data()
self.stat_modifications, self.targets_self = self._parse_stat_modifications()
self.is_recoil_move, self.recoil_ratio = self._parse_recoil_data()
data_dict = self.data or {}
self.fixed_damage = data_dict.get('damage')
self.self_switch = data_dict.get('selfSwitch')
self.volatile_status = data_dict.get('volatileStatus')
self.stalling_move = data_dict.get('stallingMove', False)
self.target = data_dict.get('target', 'normal')
self.flags = data_dict.get('flags', {})
self.ohko = data_dict.get('ohko', False)
self.defensive_category = data_dict.get('defensiveCategory')
self.use_target_offensive = data_dict.get('useTargetOffensive', False)
self.effectiveness = 1.0
self.damage_source = 'python'
self.last_damage_range = None
self.last_damage_description = None
def _parse_chain_modify(self, logic_str: str) -> float:
"""Extract multiplier from chainModify([num1, num2]) or chainModify(float)."""
if not logic_str or "chainModify" not in logic_str:
return 1.0
# Look for [num1, num2] pattern
match = re.search(r'chainModify\(\[?([\d., ]+)\]?\)', logic_str)
if match:
parts = [p.strip() for p in match.group(1).split(',')]
if len(parts) >= 2:
try:
return float(parts[0]) / float(parts[1])
except (ValueError, ZeroDivisionError):
return 1.0
else:
try:
return float(parts[0])
except ValueError:
return 1.0
# Look for modify(stat, mult) pattern
match = re.search(r'this\.modify\([^,]+,\s*([\d.]+)\)', logic_str)
if match:
try:
return float(match.group(1))
except ValueError:
return 1.0
return 1.0
def _check_condition(self, logic_str: str, pokemon, opponent, move=None) -> bool:
"""Check if conditions in logic_str are met."""
if not logic_str:
return True
if "target.getItem()" in logic_str or "target.item" in logic_str:
return True
if "hp <= pokemon.maxhp / 4" in logic_str:
if pokemon.current_hp <= pokemon.max_hp / 4:
return True
return False
return True
def _get_damage_stat(self, pokemon, stat_name: str, is_critical: bool, role: str) -> int:
base = pokemon.base_stats.get(stat_name, 10)
level = getattr(pokemon, 'level', 100)
mapping = {'attack': 'atk', 'defense': 'def', 'special_attack': 'spa', 'special_defense': 'spd'}
key = mapping.get(stat_name, stat_name)
iv = pokemon.ivs.get(key, 31)
ev = pokemon.evs.get(key, 0)
stat = int(((2 * base + iv + int(ev / 4)) * level) / 100) + 5
natures = {
'Adamant': ('attack', 'special_attack'), 'Bold': ('defense', 'attack'),
'Brave': ('attack', 'speed'), 'Calm': ('special_defense', 'attack'),
'Careful': ('special_defense', 'special_attack'), 'Gentle': ('special_defense', 'defense'),
'Hasty': ('speed', 'defense'), 'Impish': ('defense', 'special_attack'),
'Jolly': ('speed', 'special_attack'), 'Lax': ('defense', 'special_defense'),
'Lonely': ('attack', 'defense'), 'Mild': ('special_attack', 'defense'),
'Modest': ('special_attack', 'attack'), 'Naive': ('speed', 'special_defense'),
'Naughty': ('attack', 'special_defense'), 'Quiet': ('special_attack', 'speed'),
'Rash': ('special_attack', 'special_defense'), 'Relaxed': ('defense', 'speed'),
'Sassy': ('special_defense', 'speed'), 'Timid': ('speed', 'attack')
}
plus, minus = natures.get(pokemon.nature, (None, None))
if plus == stat_name:
stat = int(stat * 1.1)
elif minus == stat_name:
stat = int(stat * 0.9)
stage = pokemon.stat_stages.get(stat_name, 0)
if is_critical and role == 'attacker' and stage < 0:
stage = 0
elif is_critical and role == 'defender' and stage > 0:
stage = 0
stat = int(stat * pokemon.get_stat_stage_multiplier(stage))
if hasattr(pokemon, 'ability'):
stat = pokemon.ability.modify_stat(pokemon, stat_name, stat)
if pokemon.item_obj:
stat = pokemon.item_obj.modify_stat(pokemon, stat_name, stat)
return max(1, stat)
def _get_critical_hit(self, defending_pokemon=None) -> bool:
if defending_pokemon and hasattr(defending_pokemon, 'ability') and defending_pokemon.ability.id in ['battlearmor', 'shellarmor']:
return False
crit_ratio = self.data.get('critRatio', 1)
will_crit = self.data.get('willCrit', False)
crit_chances = {1: 1/16, 2: 1/8, 3: 1/2, 4: 1.0}
crit_chance = crit_chances.get(crit_ratio, 1.0) if crit_ratio in crit_chances else (1.0 if crit_ratio > 4 else 1/16)
return will_crit or random.random() < crit_chance
def _get_burn_multiplier(self, attacking_pokemon) -> float:
if self.category != 'physical':
return 1.0
if self.id == 'facade':
return 1.0
if not attacking_pokemon or not getattr(attacking_pokemon, 'major_status', None) == 'burn':
return 1.0
if hasattr(attacking_pokemon, 'ability') and attacking_pokemon.ability.id == 'guts':
return 1.0
return 0.5
def _parse_healing_data(self) -> Tuple[bool, Optional[List[int]], Optional[List[int]]]:
if not self.data:
return False, None, None
is_healing = False
heal_amount = None
drain_ratio = None
if 'heal' in self.data and isinstance(self.data['heal'], list):
is_healing = True
heal_amount = self.data['heal']
if 'drain' in self.data and isinstance(self.data['drain'], list):
is_healing = True
drain_ratio = self.data['drain']
if 'flags' in self.data and isinstance(self.data['flags'], dict):
if self.data['flags'].get('heal') == 1:
is_healing = True
return is_healing, heal_amount, drain_ratio
def _parse_multihit_data(self) -> Tuple[bool, Optional[Union[int, List[int]]]]:
if not self.data:
return False, None
if 'multihit' in self.data:
multihit_data = self.data['multihit']
if isinstance(multihit_data, int):
return True, multihit_data
elif isinstance(multihit_data, list) and len(multihit_data) == 2:
return True, multihit_data
elif multihit_data is not None:
return False, None
return False, None
def _apply_boosts(self, pokemon, boosts: Dict[str, int]) -> List[str]:
if not boosts or not pokemon:
return []
stat_name_mapping = {
'atk': 'attack',
'def': 'defense',
'spa': 'special_attack',
'spd': 'special_defense',
'spe': 'speed',
'accuracy': 'accuracy',
'evasion': 'evasion'
}
messages = []
for stat_abbrev, stages in boosts.items():
full_stat_name = stat_name_mapping.get(stat_abbrev, stat_abbrev)
if hasattr(pokemon, 'modify_stat_stage'):
msg = pokemon.modify_stat_stage(full_stat_name, stages)
if msg:
messages.append(msg)
return messages
def _parse_stat_modifications(self) -> Tuple[Optional[Dict[str, int]], bool]:
if not self.data:
return None, False
stat_modifications = self.data.get('boosts')
targets_self = False
if 'target' in self.data:
target = self.data['target']
if target in ['self']:
targets_self = True
return stat_modifications, targets_self
def _parse_recoil_data(self) -> Tuple[bool, Optional[List[int]]]:
if not self.data:
return False, None
if 'recoil' in self.data and isinstance(self.data['recoil'], list):
recoil_data = self.data['recoil']
if len(recoil_data) == 2 and all(isinstance(x, (int, float)) for x in recoil_data):
return True, recoil_data
else:
return False, None
return False, None
def _determine_hit_count(self) -> int:
if not self.is_multihit_move or not self.multihit_data:
return 1
if isinstance(self.multihit_data, int):
return self.multihit_data
elif isinstance(self.multihit_data, list) and len(self.multihit_data) == 2:
min_hits, max_hits = self.multihit_data
if max_hits == 5:
rand = random.randint(1, 100)
if rand <= 35:
return 2
elif rand <= 70:
return 3
elif rand <= 85:
return 4
else:
return 5
else:
return random.randint(min_hits, max_hits)
return 1
def is_priority_counter_move(self) -> bool:
return self.is_priority_counter
def can_counter_move(self, target_move: 'Move') -> bool:
if not self.is_priority_counter:
return False
if not target_move:
return False
from ..systems.priority_system import SuckerPunchHandler
if self.name.lower() == 'sucker punch':
handler = SuckerPunchHandler()
return handler.check_success_condition(target_move)
if self.priority_counter_conditions:
counters = self.priority_counter_conditions.get('counters', [])
fails_against = self.priority_counter_conditions.get('fails_against', [])
target_category = target_move.category.lower()
if target_category in counters:
return True
if target_category in fails_against:
return False
return not target_move.is_status_move
def get_priority_counter_failure_message(self) -> str:
if not self.is_priority_counter:
return ""
from ..systems.priority_system import SuckerPunchHandler
if self.name.lower() == 'sucker punch':
handler = SuckerPunchHandler()
return handler.get_failure_message()
if self.priority_counter_conditions:
return self.priority_counter_conditions.get('failure_message', "But it failed!")
return "But it failed!"
def validate_move_category_for_counter(self, target_move: 'Move') -> Tuple[bool, str]:
if not self.is_priority_counter:
return True, ""
if not target_move:
return False, self.get_priority_counter_failure_message()
from ..systems.priority_system import SuckerPunchHandler
if self.name.lower() == 'sucker punch':
handler = SuckerPunchHandler()
return handler.validate_target_move_category(target_move)
can_counter = self.can_counter_move(target_move)
if can_counter:
return True, f"{self.name} intercepted {target_move.name}!"
else:
return False, self.get_priority_counter_failure_message()
def get_effective_priority_against_move(self, target_move: Optional['Move']) -> int:
if not self.is_priority_counter or not target_move:
return self.priority
from ..systems.priority_system import SuckerPunchHandler
if self.name.lower() == 'sucker punch':
handler = SuckerPunchHandler()
return handler.get_effective_priority(target_move)
if self.can_counter_move(target_move):
if self.priority_counter_conditions:
return self.priority_counter_conditions.get('priority_when_successful', self.priority)
return self.priority
def _check_accuracy(self, attacker, defender) -> bool:
if self.accuracy is True:
return True
if not isinstance(self.accuracy, (int, float)):
return True
# Base accuracy from the move
base_accuracy = self.accuracy
if self.ohko and attacker and defender:
level_delta = getattr(attacker, 'level', 100) - getattr(defender, 'level', 100)
if level_delta < 0:
return False
base_accuracy = 30 + level_delta
# Accuracy/Evasion stage multipliers
acc_stage = attacker.stat_stages.get('accuracy', 0)
eva_stage = defender.stat_stages.get('evasion', 0)
# Formula for acc/eva stage multiplier: (3 + stage) / 3 if stage > 0, 3 / (3 - stage) if stage < 0
def get_multiplier(stage):
if stage > 0:
return (3 + stage) / 3
elif stage < 0:
return 3 / (3 - stage)
return 1.0
# Effective accuracy = base_accuracy * (acc_multiplier / eva_multiplier)
# Simplified: combine stages
combined_stage = max(-6, min(6, acc_stage - eva_stage))
stage_multiplier = get_multiplier(combined_stage)
effective_accuracy = base_accuracy * stage_multiplier
# Ability modifiers
if hasattr(attacker, 'ability'):
# Victory Star (simplified: +10% to all moves)
if attacker.ability.id == 'victorystar':
effective_accuracy *= 1.1
# Compound Eyes (+30%)
elif attacker.ability.id == 'compoundeyes':
effective_accuracy *= 1.3
if hasattr(defender, 'ability'):
# Tangled Feet: 1.5x evasion (0.66x accuracy) when confused
if defender.ability.id == 'tangledfeet' and 'confusion' in defender.volatile_statuses:
effective_accuracy *= 0.66
# Sand Veil / Snow Cloak in weather
# (Need weather access here, but for now we'll skip or use a simple check)
return random.randint(1, 100) <= effective_accuracy
def _apply_effect_block(self, target, effect_block: Dict[str, Any], chance_override: Optional[int] = None, user=None) -> List[str]:
if not effect_block or not target:
return []
if hasattr(target, 'substitute_hp') and target.substitute_hp > 0 and (user is None or target != user):
return []
chance = chance_override if chance_override is not None else effect_block.get('chance', 100)
if random.randint(1, 100) > chance:
return []
messages = []
if 'status' in effect_block:
status_type = effect_block['status']
msg = target.apply_status_effect(status_type)
if msg:
messages.append(msg)
if 'volatileStatus' in effect_block:
v_status = effect_block['volatileStatus']
if hasattr(target, 'apply_volatile_status'):
msg = target.apply_volatile_status(v_status)
if msg:
messages.append(msg)
if 'boosts' in effect_block:
boost_msgs = self._apply_boosts(target, effect_block['boosts'])
messages.extend(boost_msgs)
if 'self' in effect_block and user:
self_msgs = self._apply_effect_block(user, effect_block['self'], user=user)
messages.extend(self_msgs)
return messages
def _apply_secondary_effects(self, user, target) -> List[str]:
messages = []
multiplier = 2.0 if hasattr(user, 'has_ability') and user.has_ability('serenegrace') else 1.0
if 'secondary' in self.data and self.data['secondary']:
chance = self.data['secondary'].get('chance', 100) * multiplier
msgs = self._apply_effect_block(target, self.data['secondary'], chance_override=chance, user=user)
messages.extend(msgs)
if 'secondaries' in self.data and self.data['secondaries']:
for effect in self.data['secondaries']:
chance = effect.get('chance', 100) * multiplier
msgs = self._apply_effect_block(target, effect, chance_override=chance, user=user)
messages.extend(msgs)
return messages
def _apply_self_effects(self, user, target) -> List[str]:
messages = []
if 'self' in self.data and self.data['self']:
msgs = self._apply_effect_block(user, self.data['self'], user=user)
messages.extend(msgs)
if self.stat_modifications and self.targets_self:
boost_msgs = self._apply_boosts(user, self.stat_modifications)
messages.extend(boost_msgs)
return messages
def _apply_status_effects(self, user, target) -> List[str]:
if not self.is_status_move or not self.data:
return []
# Determine actual target for status effects
effective_target = user if self.target == 'self' else target
if not effective_target:
return []
messages = []
if effective_target.substitute_hp > 0 and effective_target != user:
bypasses = self.flags.get('sound') or (hasattr(user, 'ability') and user.ability.id == 'infiltrator')
if not bypasses:
return [f"{effective_target.name} is behind a substitute!"]
if 'status' in self.data:
msg = effective_target.apply_status_effect(self.data['status'])
if msg:
messages.append(msg)
if 'volatileStatus' in self.data:
if hasattr(effective_target, 'apply_volatile_status'):
msg = effective_target.apply_volatile_status(self.data['volatileStatus'])
if msg:
if self.data['volatileStatus'] == 'protect':
# Protect is handled specially in the game loop for announcements
pass
else:
messages.append(msg)
secondary_msgs = self._apply_secondary_effects(user, target)
messages.extend(secondary_msgs)
return messages
def _calculate_heal_amount(self, user, damage_dealt: int = 0) -> int:
try:
if not user or not hasattr(user, 'max_hp'):
return 0
if damage_dealt < 0:
damage_dealt = 0
if not self.is_healing_move:
return 0
if self.heal_amount is not None:
try:
if not isinstance(self.heal_amount, list) or len(self.heal_amount) != 2:
return 0
numerator, denominator = self.heal_amount
heal_fraction = numerator / denominator
heal_amount = int(user.max_hp * heal_fraction)
if heal_fraction > 0 and heal_amount == 0:
heal_amount = 1
return max(0, heal_amount)
except (TypeError, ValueError, ZeroDivisionError):
return 0
elif self.drain_ratio is not None:
try:
if not isinstance(self.drain_ratio, list) or len(self.drain_ratio) != 2:
return 0
numerator, denominator = self.drain_ratio
if damage_dealt <= 0:
return 0
drain_fraction = numerator / denominator
heal_amount = int(damage_dealt * drain_fraction)
if drain_fraction > 0 and damage_dealt > 0 and heal_amount == 0:
heal_amount = 1
return max(0, heal_amount)
except (TypeError, ValueError, ZeroDivisionError):
return 0
else:
return 0
except Exception:
return 0
def _apply_healing_effects(self, user, target, damage_dealt: int = 0) -> List[str]:
messages = []
try:
if not user or not hasattr(user, 'current_hp') or not hasattr(user, 'max_hp'):
return messages
if not self.is_healing_move:
return messages
heal_amount = self._calculate_heal_amount(user, damage_dealt)
if heal_amount <= 0:
if self.drain_ratio is not None and damage_dealt <= 0:
return messages
if self.heal_amount is not None:
if user.current_hp >= user.max_hp:
messages.append(f"{user.name} is already at full health!")
return messages
return messages
hp_before = user.current_hp
user.heal(heal_amount)
actual_heal = user.current_hp - hp_before
if actual_heal <= 0:
messages.append(f"{user.name} is already at full health!")
else:
if self.heal_amount is not None:
messages.append(f"{user.name} recovered {actual_heal} HP!")
elif self.drain_ratio is not None:
if actual_heal > 0:
messages.append(f"{user.name} recovered {actual_heal} HP!")
messages.append(f"{target.name if target else 'The target'}'s energy was drained!")
else:
messages.append(f"{target.name if target else 'The target'}'s energy was drained!")
else:
messages.append(f"{user.name} recovered {actual_heal} HP!")
except Exception:
pass
return messages
def _calculate_recoil_damage(self, user, damage_dealt: int) -> int:
try:
if not user or not hasattr(user, 'max_hp') or damage_dealt <= 0 or not self.is_recoil_move or not self.recoil_ratio:
return 0
numerator, denominator = self.recoil_ratio
recoil_fraction = numerator / denominator
recoil_damage = int(damage_dealt * recoil_fraction)
if recoil_fraction > 0 and damage_dealt > 0 and recoil_damage == 0:
recoil_damage = 1
return max(0, recoil_damage)
except Exception:
return 0
def _apply_recoil_effects(self, user, damage_dealt: int) -> List[str]:
messages = []
try:
if not user or not hasattr(user, 'current_hp') or not hasattr(user, 'max_hp') or not self.is_recoil_move:
return messages
recoil_damage = self._calculate_recoil_damage(user, damage_dealt)
if recoil_damage <= 0:
return messages
user.take_damage(recoil_damage)
messages.append(f"{user.name} is hurt by recoil!")
except Exception:
pass
return messages
def _handle_special_moves(self, attacking_pokemon, defending_pokemon):
move_name_lower = self.name.lower()
if move_name_lower == 'belly drum':
return self._handle_belly_drum(attacking_pokemon)
return None
def _handle_belly_drum(self, user):
if user.stat_stages.get('attack', 0) >= 6:
return 0, f"{user.name} used {self.name}!", f"{user.name}'s Attack won't go any higher!"
hp_cost = user.max_hp // 2
if user.current_hp <= hp_cost:
return 0, f"{user.name} used {self.name}!", f"But it failed!"
user.current_hp -= hp_cost
user.stat_stages['attack'] = 6
user._recalculate_stats()
move_desc = data_loader.get_move_description(self.name)
if move_desc and 'boost' in move_desc:
boost_message = move_desc['boost'].replace('[POKEMON]', user.name)
else:
boost_message = f"{user.name} cut its own HP and maximized its Attack!"
return 0, f"{user.name} used {self.name}!", boost_message
def _apply_stat_modifications(self, user, target) -> List[str]:
if not self.stat_modifications:
return []
modification_target = user if self.target == 'self' else target
if not modification_target:
return []
return self._apply_boosts(modification_target, self.stat_modifications)
def use_move(self, attacking_pokemon=None, defending_pokemon=None, weather='none', field=None) -> Tuple[int, int, str, Optional[str], Optional[str]]:
self.damage_source = 'python'
self.last_damage_range = None
self.last_damage_description = None
if self.stalling_move:
success_rate = 1.0 / (3.0 ** attacking_pokemon.consecutive_stalling_moves)
if random.random() >= success_rate:
attacking_pokemon.consecutive_stalling_moves = 0
return 0, 0, "", f"But it failed!", None
attacking_pokemon.consecutive_stalling_moves += 1
else:
attacking_pokemon.consecutive_stalling_moves = 0
# Special handling for Substitute
if self.id == 'substitute':
if attacking_pokemon.substitute_hp > 0:
return 0, 0, "", "But it failed!", None
cost = attacking_pokemon.max_hp // 4
if attacking_pokemon.current_hp > cost:
attacking_pokemon.current_hp -= cost
attacking_pokemon.substitute_hp = cost
attacking_pokemon.apply_volatile_status('substitute')
return 0, 0, "", f"{attacking_pokemon.name} put in a substitute!", None
else:
return 0, 0, "", "But it failed!", None
if self.is_status_move or self.power == 0:
# Check for special moves first (like Belly Drum)
special_move_result = self._handle_special_moves(attacking_pokemon, defending_pokemon)
if special_move_result:
damage, usage_msg, status_msg = special_move_result
return damage, 0, usage_msg, status_msg, None
status_messages = []
if defending_pokemon:
status_messages = self._apply_status_effects(attacking_pokemon, defending_pokemon)
# Apply healing effects for status moves (like Recover)
healing_messages = []
if attacking_pokemon:
healing_messages = self._apply_healing_effects(attacking_pokemon, defending_pokemon, 0)
# Apply stat modifications for status moves
stat_modification_messages = []
if self.stat_modifications:
stat_modification_messages = self._apply_stat_modifications(attacking_pokemon, defending_pokemon)
# Combine status, healing, and stat modification messages
all_messages = status_messages + healing_messages + stat_modification_messages
combined_message = " ".join(all_messages) if all_messages else None
return 0, 0, "", combined_message, None
# Handle multi-hit moves
if self.is_multihit_move:
return self._use_multihit_move(attacking_pokemon, defending_pokemon)
# If we get here, it's a single-hit damaging move
base_damage = self.power
effectiveness_message = ""
# Check for weather-setting moves
weather_to_set = None
WEATHER_MOVES = {
'raindance': 'raindance', 'sunnyday': 'sunnyday',
'sandstorm': 'sandstorm', 'hail': 'hail', 'snowscape': 'hail'
}
if self.id in WEATHER_MOVES:
weather_to_set = WEATHER_MOVES[self.id]
# If it's a status move, it shouldn't have any damage calculation
if self.is_status_move or self.power == 0:
return 0, 0, f"{attacking_pokemon.name} used {self.name}!", "", weather_to_set
# Apply type effectiveness if both Pokemon are provided
if attacking_pokemon and defending_pokemon:
# Check for ability immunities (e.g. Levitate)
if hasattr(defending_pokemon, 'ability'):
if defending_pokemon.ability.is_immune(self.type, self.category):
return 0, 0, f"It had no effect on {defending_pokemon.name}!", None, weather_to_set
# Get the move type in lowercase for comparison
move_type = self.type.lower()
# Get defending Pokémon's types, handling both single and dual types
if hasattr(defending_pokemon, 'types') and isinstance(defending_pokemon.types, list) and defending_pokemon.types:
defending_types = [t.lower() if isinstance(t, str) else str(t).lower() for t in defending_pokemon.types]
else:
# Fallback for backward compatibility
defending_types = [getattr(defending_pokemon, 'type', 'normal').lower()]
# Calculate effectiveness against each of the target's types
effectiveness = 1.0
# Calculate effectiveness against each of the target's types
effectiveness = 1.0
for target_type in defending_types:
eff = data_loader.get_type_effectiveness(move_type, target_type)
# Delta Stream: Super-effective moves against Flying become neutral (1x)
if weather == 'deltastream' and target_type == 'flying' and eff > 1:
eff = 1.0
effectiveness *= eff
effectiveness = round(effectiveness, 2)
self.effectiveness = effectiveness
is_critical = self._get_critical_hit(defending_pokemon)
# Primal Weather Suppression
if weather == 'primordialsea' and move_type == 'fire':
return 0, 0, "The Fire-type move fizzled out in the heavy rain!", None, weather_to_set
if weather == 'desolateland' and move_type == 'water':
return 0, 0, "The Water-type move evaporated in the harsh sunlight!", None, weather_to_set
# Determine attack and defense stats based on move category
defending_types_lower = [t.lower() for t in defending_pokemon.types]
# Determine which offensive stat to use
if self.use_target_offensive:
# Foul Play: Use target's attack stat
attack_stat = self._get_damage_stat(defending_pokemon, 'attack', is_critical, 'attacker')
attack_name = "Target's Attack"
elif self.category == 'physical':
attack_stat = self._get_damage_stat(attacking_pokemon, 'attack', is_critical, 'attacker')
attack_name = 'Attack'
else: # 'special'
attack_stat = self._get_damage_stat(attacking_pokemon, 'special_attack', is_critical, 'attacker')
attack_name = 'Special Attack'
# Determine which defensive stat to use
effective_defensive_category = self.defensive_category.lower() if self.defensive_category else self.category
if effective_defensive_category == 'physical':
defense_stat = self._get_damage_stat(defending_pokemon, 'defense', is_critical, 'defender')
# Gen 9 Snow (replaces Hail): +50% Defense for Ice types
if weather == 'hail' and 'ice' in defending_types_lower:
defense_stat = int(defense_stat * 1.5)
defense_name = 'Defense'
else: # 'special'
defense_stat = self._get_damage_stat(defending_pokemon, 'special_defense', is_critical, 'defender')
# Sandstorm: +50% Special Defense for Rock types
if weather == 'sandstorm' and 'rock' in defending_types_lower:
defense_stat = int(defense_stat * 1.5)
defense_name = 'Special Defense'
level = getattr(attacking_pokemon, 'level', 100)
move_name_lower = self.name.lower()
smogon_result = smogon_damage_for_move(
attacking_pokemon,
defending_pokemon,
self,
field or {'weather': weather}
)
if self.fixed_damage:
if isinstance(self.fixed_damage, int):
damage = self.fixed_damage
elif self.fixed_damage == 'level':
damage = level
else:
damage = 40
if effectiveness == 0:
effectiveness_message = "It had no effect..."
return 0, 0, effectiveness_message, None, weather_to_set
base_damage = damage
elif smogon_result:
base_damage = int(smogon_result["selected_damage"])
self.damage_source = 'smogon'
self.last_damage_range = [int(smogon_result["min"]), int(smogon_result["max"])]
self.last_damage_description = smogon_result.get("description")
if effectiveness == 0:
effectiveness_message = "It had no effect..."
return 0, 0, effectiveness_message, None, weather_to_set
elif effectiveness < 1:
effectiveness_message = "It's not very effective..."
elif effectiveness > 1:
effectiveness_message = "It's super effective!"
else:
# Check for special move logic in JSON generically
actual_base_power = base_damage
for hook in ["onBasePower", "onModifyMove"]:
logic = self.data.get(hook)
if not logic: continue
if self._check_condition(logic, attacking_pokemon, defending_pokemon, self):
multiplier = self._parse_chain_modify(logic)
actual_base_power = int(actual_base_power * multiplier)
damage = ((2 * level / 5 + 2) * actual_base_power * attack_stat / defense_stat) / 50 + 2
damage = int(damage)
damage = int(damage * effectiveness)
# Apply weather multipliers
if weather in ['raindance', 'primordialsea']:
if move_type == 'water':
damage = int(damage * 1.5)
elif move_type == 'fire':
damage = int(damage * 0.5)
elif weather in ['sunnyday', 'desolateland']:
if move_type == 'fire':
damage = int(damage * 1.5)
elif move_type == 'water':
damage = int(damage * 0.5)
elif weather == 'hail': # Gen 9 Snow
if move_type == 'ice':
damage = int(damage * 1.5)
if effectiveness == 0:
effectiveness_message = "It had no effect..."
return 0, 0, effectiveness_message, None, weather_to_set
elif effectiveness < 1:
effectiveness_message = "It's not very effective..."
elif effectiveness > 1:
effectiveness_message = "It's super effective!"
# Apply STAB (Same Type Attack Bonus) - 1.5x (or 2x with Adaptability)
if hasattr(attacking_pokemon, 'types') and attacking_pokemon.types:
attacker_types = [t.lower() if isinstance(t, str) else str(t).lower() for t in attacking_pokemon.types]
if move_type in attacker_types:
stab_multiplier = 1.5
if hasattr(attacking_pokemon, 'ability'):
stab_multiplier = attacking_pokemon.ability.get_stab_multiplier()
damage = int(damage * stab_multiplier)
if is_critical:
damage = int(damage * 1.5)
if effectiveness_message:
effectiveness_message = "A critical hit! " + effectiveness_message
else:
effectiveness_message = "A critical hit!"
damage = int(damage * random.randint(85, 100) / 100)
damage = max(1, int(damage * self._get_burn_multiplier(attacking_pokemon)))
# Apply ability damage modifiers (e.g. Blaze, Technician)
if hasattr(attacking_pokemon, 'ability'):
damage = attacking_pokemon.ability.modify_damage_dealt(attacking_pokemon, defending_pokemon, self, damage)
base_damage = damage
has_substitute = getattr(defending_pokemon, 'substitute_hp', 0) > 0
bypasses_substitute = self.flags.get('sound') or (hasattr(attacking_pokemon, 'ability') and attacking_pokemon.ability.id == 'infiltrator')
if base_damage > 0 and has_substitute and not bypasses_substitute:
substitute_damage = min(base_damage, defending_pokemon.substitute_hp)
defending_pokemon.substitute_hp -= substitute_damage
return 0, substitute_damage, effectiveness_message, None, weather_to_set
# Apply status effects after damage calculation
# Apply secondary effects, self effects, healing, and recoil
# Apply legacy status effects for status moves
status_messages = []
if self.is_status_move and defending_pokemon:
status_messages = self._apply_status_effects(attacking_pokemon, defending_pokemon)
# Combine all messages
all_messages = status_messages
combined_message = " ".join(all_messages) if all_messages else None
return base_damage, 0, effectiveness_message, combined_message, weather_to_set
def _use_multihit_move(self, attacking_pokemon, defending_pokemon) -> Tuple[int, int, str, Optional[str]]:
"""Handle multi-hit moves like Pin Missile, Rock Blast, etc."""
if not attacking_pokemon or not defending_pokemon:
return 0, 0, f"{attacking_pokemon.name} used {self.name}!", None, None
# Determine number of hits
hit_count = self._determine_hit_count()
# Get the move type in lowercase for comparison
move_type = self.type.lower()
# Get defending Pokémon's types, handling both single and dual types
if hasattr(defending_pokemon, 'types') and isinstance(defending_pokemon.types, list) and defending_pokemon.types:
defending_types = [t.lower() if isinstance(t, str) else str(t).lower() for t in defending_pokemon.types]
else:
# Fallback for backward compatibility
defending_types = [getattr(defending_pokemon, 'type', 'normal').lower()]
# Calculate effectiveness against each of the target's types
effectiveness = 1.0
effectiveness_breakdown = {}
for target_type in defending_types:
type_effectiveness = data_loader.get_type_effectiveness(move_type, target_type)
effectiveness_breakdown[target_type] = type_effectiveness
for t in defending_types:
eff = data_loader.get_type_effectiveness(move_type, t)
# Delta Stream: Super-effective moves against Flying become neutral (1x)
if weather == 'deltastream' and t == 'flying' and eff > 1:
eff = 1.0
effectiveness *= eff
effectiveness = round(effectiveness, 2)
self.effectiveness = effectiveness
if effectiveness == 0:
return 0, 0, "It had no effect...", None, None
# Determine attack and defense stats based on move category
if self.category == 'physical':
attack_stat_name = 'attack'
defense_stat_name = 'defense'
else: # 'special'
attack_stat_name = 'special_attack'
defense_stat_name = 'special_defense'
# Calculate damage for each hit, accounting for Substitute
total_poke_damage = 0
total_sub_damage = 0
level = getattr(attacking_pokemon, 'level', 100)
has_substitute = getattr(defending_pokemon, 'substitute_hp', 0) > 0
bypasses_substitute = self.flags.get('sound') or (hasattr(attacking_pokemon, 'ability') and attacking_pokemon.ability.id == 'infiltrator')
for hit_num in range(1, hit_count + 1):
# Check accuracy for each hit
if not self._check_accuracy(attacking_pokemon, defending_pokemon):
accuracy = self.accuracy
continue
is_critical = self._get_critical_hit(defending_pokemon)
attack_stat = self._get_damage_stat(attacking_pokemon, attack_stat_name, is_critical, 'attacker')
defense_stat = self._get_damage_stat(defending_pokemon, defense_stat_name, is_critical, 'defender')
# Calculate base damage for this hit
hit_damage = ((2 * level / 5 + 2) * self.power * attack_stat / defense_stat) / 50 + 2
hit_damage = int(hit_damage)
hit_damage = int(hit_damage * effectiveness)
# STAB
if hasattr(attacking_pokemon, 'types') and attacking_pokemon.types:
attacker_types = [t.lower() if isinstance(t, str) else str(t).lower() for t in attacking_pokemon.types]
if move_type in attacker_types:
stab_multiplier = attacking_pokemon.ability.get_stab_multiplier() if hasattr(attacking_pokemon, 'ability') else 1.5
hit_damage = int(hit_damage * stab_multiplier)
# Critical
if is_critical:
hit_damage = int(hit_damage * 1.5)
# Variation
hit_damage = int(hit_damage * random.randint(85, 100) / 100)
hit_damage = max(1, int(hit_damage * self._get_burn_multiplier(attacking_pokemon)))
if hasattr(attacking_pokemon, 'ability'):
hit_damage = attacking_pokemon.ability.modify_damage_dealt(attacking_pokemon, defending_pokemon, self, hit_damage)
# Apply to substitute if active
if has_substitute and not bypasses_substitute and defending_pokemon.substitute_hp > 0:
absorbed = min(hit_damage, defending_pokemon.substitute_hp)
defending_pokemon.substitute_hp -= absorbed
total_sub_damage += absorbed
# Check if broken mid-hit
if defending_pokemon.substitute_hp <= 0:
# In Gen 5+, remaining hits strike the Pokemon
# But the remainder of THIS hit is usually lost?
# Let's say subsequent hits will strike the poke.
pass
else:
total_poke_damage += hit_damage
total_damage = total_poke_damage + total_sub_damage
# Generate effectiveness message
effectiveness_message = ""
if effectiveness < 1:
effectiveness_message = "It's not very effective..."
elif effectiveness > 1:
effectiveness_message = "It's super effective!"
# Generate hit count message
hit_message = f"Hit {hit_count} time{'s' if hit_count != 1 else ''}!"
# Combine messages
combined_message = f"{hit_message} {effectiveness_message}".strip()
# Apply secondary effects, self effects, healing, and recoil
secondary_messages = []
self_messages = []
healing_messages = []
recoil_messages = []
if defending_pokemon:
secondary_messages = self._apply_secondary_effects(attacking_pokemon, defending_pokemon)
if attacking_pokemon:
self_messages = self._apply_self_effects(attacking_pokemon, defending_pokemon)
healing_messages = self._apply_healing_effects(attacking_pokemon, defending_pokemon, total_damage)
recoil_messages = self._apply_recoil_effects(attacking_pokemon, total_damage)
# Combine all messages
all_messages = secondary_messages + self_messages + healing_messages + recoil_messages
if all_messages:
combined_message = f"{combined_message} {' '.join(all_messages)}".strip()
return total_poke_damage, total_sub_damage, combined_message, None, None
def get_multihit_hits(self, attacking_pokemon, defending_pokemon) -> List[Dict[str, Any]]:
"""Get individual hit information for multi-hit moves (for progressive damage display)"""
if not self.is_multihit_move or not attacking_pokemon or not defending_pokemon:
return []
# Determine number of hits
hit_count = self._determine_hit_count()
# Get the move type in lowercase for comparison
move_type = self.type.lower()
# Get defending Pokémon's types
if hasattr(defending_pokemon, 'types') and isinstance(defending_pokemon.types, list) and defending_pokemon.types:
defending_types = [t.lower() if isinstance(t, str) else str(t).lower() for t in defending_pokemon.types]
else:
defending_types = [getattr(defending_pokemon, 'type', 'normal').lower()]
# Calculate effectiveness
effectiveness = 1.0
for target_type in defending_types:
type_effectiveness = data_loader.get_type_effectiveness(move_type, target_type)
effectiveness *= type_effectiveness
effectiveness = round(effectiveness, 2)
self.effectiveness = effectiveness
if effectiveness == 0:
return []
# Determine attack and defense stats
if self.category == 'physical':
attack_stat_name = 'attack'
defense_stat_name = 'defense'
else:
attack_stat_name = 'special_attack'
defense_stat_name = 'special_defense'
# Calculate each hit
hits = []
level = getattr(attacking_pokemon, 'level', 100)
for hit_num in range(1, hit_count + 1):
# Check accuracy for each hit
if not self._check_accuracy(attacking_pokemon, defending_pokemon):
hits.append({
'hit_number': hit_num,
'damage': 0,
'missed': True,
'critical': False
})
continue
is_critical = self._get_critical_hit(defending_pokemon)
attack_stat = self._get_damage_stat(attacking_pokemon, attack_stat_name, is_critical, 'attacker')
defense_stat = self._get_damage_stat(defending_pokemon, defense_stat_name, is_critical, 'defender')
# Calculate base damage for this hit
damage = ((2 * level / 5 + 2) * self.power * attack_stat / defense_stat) / 50 + 2
damage = int(damage)
# Apply effectiveness
damage = int(damage * effectiveness)
# Apply STAB
if hasattr(attacking_pokemon, 'types') and attacking_pokemon.types:
attacker_types = [t.lower() if isinstance(t, str) else str(t).lower() for t in attacking_pokemon.types]
if move_type in attacker_types:
stab_multiplier = attacking_pokemon.ability.get_stab_multiplier() if hasattr(attacking_pokemon, 'ability') else 1.5
damage = int(damage * stab_multiplier)
if is_critical:
damage = int(damage * 1.5)
# Apply random damage variation
damage = int(damage * random.randint(85, 100) / 100)
damage = max(1, int(damage * self._get_burn_multiplier(attacking_pokemon)))
hits.append({
'hit_number': hit_num,
'damage': damage,
'missed': False,
'critical': is_critical
})
return hits
def to_dict(self) -> Dict[str, Any]:
return {
'name': self.name,
'power': self.power,
'pp': self.pp,
'max_pp': self.max_pp,
'type': self.type,
'accuracy': self.accuracy,
'category': self.category,
'priority': self.priority,
'is_status_move': self.is_status_move,
'is_healing_move': self.is_healing_move,
'heal_amount': self.heal_amount,
'drain_ratio': self.drain_ratio,
'is_multihit_move': self.is_multihit_move,
'multihit_data': self.multihit_data,
'stat_modifications': self.stat_modifications,
'targets_self': self.targets_self
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Move':
# Create move using the new data-driven constructor
move = cls(name=data['name'])
# Override fields that might have been changed/saved
move.pp = data.get('pp', move.pp)
move.max_pp = data.get('max_pp', move.max_pp)
# Restore healing data if available
if 'is_healing_move' in data:
move.is_healing_move = data['is_healing_move']
if 'heal_amount' in data:
move.heal_amount = data['heal_amount']
if 'drain_ratio' in data:
move.drain_ratio = data['drain_ratio']
# Restore multi-hit data if available
if 'is_multihit_move' in data:
move.is_multihit_move = data['is_multihit_move']
if 'multihit_data' in data:
move.multihit_data = data['multihit_data']
# Restore stat modification data if available
if 'stat_modifications' in data:
move.stat_modifications = data['stat_modifications']
if 'targets_self' in data:
move.targets_self = data['targets_self']
return move