| 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 |
| |
| |
| 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 |
| |
| |
| 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 = 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 |
| |
| |
| acc_stage = attacker.stat_stages.get('accuracy', 0) |
| eva_stage = defender.stat_stages.get('evasion', 0) |
| |
| |
| def get_multiplier(stage): |
| if stage > 0: |
| return (3 + stage) / 3 |
| elif stage < 0: |
| return 3 / (3 - stage) |
| return 1.0 |
| |
| |
| |
| combined_stage = max(-6, min(6, acc_stage - eva_stage)) |
| stage_multiplier = get_multiplier(combined_stage) |
| |
| effective_accuracy = base_accuracy * stage_multiplier |
| |
| |
| if hasattr(attacker, 'ability'): |
| |
| if attacker.ability.id == 'victorystar': |
| effective_accuracy *= 1.1 |
| |
| elif attacker.ability.id == 'compoundeyes': |
| effective_accuracy *= 1.3 |
| |
| if hasattr(defender, 'ability'): |
| |
| if defender.ability.id == 'tangledfeet' and 'confusion' in defender.volatile_statuses: |
| effective_accuracy *= 0.66 |
| |
| |
| |
| 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 [] |
| |
| |
| 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': |
| |
| 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 |
|
|
| |
| 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: |
| |
| 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) |
| |
| |
| healing_messages = [] |
| if attacking_pokemon: |
| healing_messages = self._apply_healing_effects(attacking_pokemon, defending_pokemon, 0) |
| |
| |
| stat_modification_messages = [] |
| if self.stat_modifications: |
| stat_modification_messages = self._apply_stat_modifications(attacking_pokemon, defending_pokemon) |
| |
| |
| 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 |
| |
| |
| if self.is_multihit_move: |
| return self._use_multihit_move(attacking_pokemon, defending_pokemon) |
| |
| |
| base_damage = self.power |
| effectiveness_message = "" |
| |
| |
| 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 self.is_status_move or self.power == 0: |
| return 0, 0, f"{attacking_pokemon.name} used {self.name}!", "", weather_to_set |
| |
| |
| if attacking_pokemon and defending_pokemon: |
| |
| 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 |
|
|
| |
| move_type = self.type.lower() |
| |
| |
| 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()] |
| |
| |
| effectiveness = 1.0 |
| |
| effectiveness = 1.0 |
| |
| for target_type in defending_types: |
| eff = data_loader.get_type_effectiveness(move_type, target_type) |
| |
| 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) |
| |
| |
| 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 |
| |
| |
| defending_types_lower = [t.lower() for t in defending_pokemon.types] |
| |
| |
| if self.use_target_offensive: |
| |
| 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: |
| attack_stat = self._get_damage_stat(attacking_pokemon, 'special_attack', is_critical, 'attacker') |
| attack_name = 'Special Attack' |
| |
| |
| 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') |
| |
| if weather == 'hail' and 'ice' in defending_types_lower: |
| defense_stat = int(defense_stat * 1.5) |
| defense_name = 'Defense' |
| else: |
| defense_stat = self._get_damage_stat(defending_pokemon, 'special_defense', is_critical, 'defender') |
| |
| 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: |
| |
| 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) |
| |
| |
| 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': |
| 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!" |
| |
| |
| 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))) |
| |
| |
| 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 |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| status_messages = [] |
| if self.is_status_move and defending_pokemon: |
| status_messages = self._apply_status_effects(attacking_pokemon, defending_pokemon) |
| |
| |
| 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 |
| |
| |
| hit_count = self._determine_hit_count() |
| |
| |
| move_type = self.type.lower() |
| |
| |
| 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()] |
| |
| |
| 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) |
| |
| 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 |
| |
| |
| if self.category == 'physical': |
| attack_stat_name = 'attack' |
| defense_stat_name = 'defense' |
| else: |
| attack_stat_name = 'special_attack' |
| defense_stat_name = 'special_defense' |
| |
| |
| 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): |
| |
| 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') |
| |
| |
| 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) |
| |
| |
| 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) |
| |
| |
| if is_critical: |
| hit_damage = int(hit_damage * 1.5) |
| |
| |
| 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) |
| |
| |
| 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 |
| |
| |
| if defending_pokemon.substitute_hp <= 0: |
| |
| |
| |
| pass |
| else: |
| total_poke_damage += hit_damage |
| |
| total_damage = total_poke_damage + total_sub_damage |
| |
| |
| effectiveness_message = "" |
| if effectiveness < 1: |
| effectiveness_message = "It's not very effective..." |
| elif effectiveness > 1: |
| effectiveness_message = "It's super effective!" |
| |
| |
| hit_message = f"Hit {hit_count} time{'s' if hit_count != 1 else ''}!" |
| |
| |
| combined_message = f"{hit_message} {effectiveness_message}".strip() |
| |
| |
| 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) |
| |
| |
| 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 [] |
| |
| |
| hit_count = self._determine_hit_count() |
| |
| |
| move_type = self.type.lower() |
| |
| |
| 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()] |
| |
| |
| 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 [] |
| |
| |
| if self.category == 'physical': |
| attack_stat_name = 'attack' |
| defense_stat_name = 'defense' |
| else: |
| attack_stat_name = 'special_attack' |
| defense_stat_name = 'special_defense' |
| |
| |
| hits = [] |
| level = getattr(attacking_pokemon, 'level', 100) |
| |
| for hit_num in range(1, hit_count + 1): |
| |
| 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') |
| |
| |
| damage = ((2 * level / 5 + 2) * self.power * attack_stat / defense_stat) / 50 + 2 |
| damage = int(damage) |
| |
| |
| damage = int(damage * effectiveness) |
| |
| |
| 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) |
| |
| |
| 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': |
| |
| move = cls(name=data['name']) |
| |
| |
| move.pp = data.get('pp', move.pp) |
| move.max_pp = data.get('max_pp', move.max_pp) |
| |
| |
| 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'] |
| |
| |
| 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'] |
| |
| |
| if 'stat_modifications' in data: |
| move.stat_modifications = data['stat_modifications'] |
| if 'targets_self' in data: |
| move.targets_self = data['targets_self'] |
| |
| return move |
|
|