| from typing import Dict, List, Any, Optional, Tuple |
| import json |
| import os |
| import re |
|
|
| LOGIC_PATH = os.path.join(os.path.dirname(__file__), "../../data/datasets/abilities_logic.json") |
| METADATA_PATH = os.path.join(os.path.dirname(__file__), "../../data/datasets/abilities.json") |
|
|
| def load_abilities_config(): |
| config = {} |
| |
| if os.path.exists(LOGIC_PATH): |
| with open(LOGIC_PATH, 'r') as f: |
| config = json.load(f) |
| |
| if os.path.exists(METADATA_PATH): |
| with open(METADATA_PATH, 'r') as f: |
| metadata = json.load(f) |
| for id, data in metadata.items(): |
| if id in config: |
| config[id]["desc"] = data.get("desc", config[id].get("desc", "")) |
| config[id]["shortDesc"] = data.get("shortDesc", config[id].get("shortDesc", "")) |
| else: |
| config[id] = { |
| "id": id, |
| "name": data.get("name", id), |
| "desc": data.get("desc", ""), |
| "shortDesc": data.get("shortDesc", "") |
| } |
| |
| if "levitate" in config: |
| config["levitate"]["immunities"] = {"types": ["ground"]} |
| |
| return config |
|
|
| ABILITIES_CONFIG = load_abilities_config() |
|
|
| class Ability: |
| def __init__(self, name: str): |
| self.id = name.lower().replace(" ", "").replace("-", "").replace("'", "").replace("(", "").replace(")", "") |
| self.config = ABILITIES_CONFIG.get(self.id, {}) |
| self.name = self.config.get("name", name) |
| self.description = self.config.get("desc", "No effect.") |
| |
| self.state = {} |
| if self.id == 'slowstart': |
| self.state['counter'] = 5 |
| elif self.id == 'truant': |
| self.state['skip'] = False |
| |
| 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 |
| |
| curr_hp = getattr(pokemon, 'current_hp', getattr(pokemon, 'max_hp', 100)) |
| max_hp = getattr(pokemon, 'max_hp', 100) |
| |
| |
| if "hp <= pokemon.maxhp / 3" in logic_str or "hp <= attacker.maxhp / 3" in logic_str: |
| if curr_hp <= max_hp / 3: |
| return True |
| return False |
| |
| if "hp <= pokemon.maxhp / 2" in logic_str: |
| if curr_hp <= max_hp / 2: |
| return True |
| return False |
|
|
| |
| if "pokemon.status" in logic_str or "attacker.status" in logic_str: |
| if pokemon.major_status: |
| return True |
| return False |
| |
| |
| match = re.search(r"move\.type === '([^']+)'", logic_str) |
| if match: |
| target_type = match.group(1).lower() |
| if move and move.type.lower() == target_type: |
| return True |
| return False |
|
|
| |
| match = re.search(r"isTerrain\('([^']+)'\)", logic_str) |
| if match: |
| target_terrain = match.group(1).lower() |
| |
| return False |
| |
| |
| if "power <= 60" in logic_str or "basePowerAfterMultiplier <= 60" in logic_str: |
| if move and 0 < move.power <= 60: |
| return True |
| return False |
| |
| |
| match = re.search(r"power <= (\d+)", logic_str) |
| if match: |
| threshold = int(match.group(1)) |
| if move and 0 < move.power <= threshold: |
| return True |
| return False |
|
|
| |
| if "this.effectState.counter" in logic_str: |
| if self.state.get('counter', 0) > 0: |
| return True |
| return False |
|
|
| |
| match = re.search(r"move\.flags\[['\"](\w+)['\"]\]", logic_str) |
| if match: |
| flag = match.group(1).lower() |
| if move and hasattr(move, 'flags') and move.flags.get(flag): |
| return True |
| return False |
|
|
| |
| |
| |
| if "(" in logic_str or "{" in logic_str or "===" in logic_str: |
| return False |
| |
| return True |
|
|
| def _extract_popups(self, logic_str: str, pokemon) -> List[Dict[str, Any]]: |
| """Detect and extract battle log/popup events from Showdown-style logic strings.""" |
| if not logic_str: return [] |
| results = [] |
| is_p = hasattr(pokemon, "is_player") and pokemon.is_player |
| |
| |
| patterns = [ |
| (r"this\.add\('-ability',\s*[^,]+,\s*'([^']+)'\)", "{user}'s {ability} activated!"), |
| (r"this\.add\('cant',\s*[^,]+,\s*'ability:\s*([^']+)'\)", "{user} is loafing around!"), |
| (r"this\.add\('-activate',\s*[^,]+,\s*'ability:\s*([^']+)'\)", "{user}'s {ability} activated!"), |
| (r"this\.add\('-immune',\s*[^,]+,\s*'ability:\s*([^']+)'\)", "{user} is immune thanks to its {ability}!"), |
| (r"this\.add\('-fail',\s*[^,]+,\s*'ability:\s*([^']+)'\)", "{user}'s {ability} failed!"), |
| (r"this\.add\('-block',\s*[^,]+,\s*'ability:\s*([^']+)'\)", "{user}'s {ability} blocked the effect!"), |
| ] |
| |
| for pattern, template in patterns: |
| matches = re.findall(pattern, logic_str) |
| for ability_name in matches: |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": template.format(user=pokemon.get_display_name(), ability=ability_name), |
| "is_player": is_p |
| }) |
| |
| return results |
|
|
| def modify_stat(self, pokemon, stat_name: str, value: int) -> int: |
| """Applies stat multipliers based on the ability.""" |
| |
| condition = self.config.get("condition") |
| if condition == "has_status": |
| if not pokemon.major_status: |
| return value |
| |
| stat_modifiers = self.config.get("stat_modifiers", {}) |
| multiplier = stat_modifiers.get(stat_name, 1.0) |
| |
| if multiplier != 1.0: |
| value = int(value * multiplier) |
| |
| |
| stat_map = { |
| 'attack': 'onModifyAtk', |
| 'defense': 'onModifyDef', |
| 'special_attack': 'onModifySpA', |
| 'special_defense': 'onModifySpD', |
| 'speed': 'onModifySpe' |
| } |
| |
| hook = stat_map.get(stat_name) |
| if hook and hook in self.config: |
| logic = self.config[hook] |
| |
| |
| if self.id == 'slowstart' and self.state.get('counter', 0) > 0: |
| multiplier = 0.5 |
| value = int(value * multiplier) |
| elif self._check_condition(logic, pokemon, None): |
| multiplier = self._parse_chain_modify(logic) |
| value = int(value * multiplier) |
| |
| return value |
|
|
| def _parse_boost_amounts(self, logic_str: str) -> Dict[str, int]: |
| """Extract boost amounts from ability logic strings.""" |
| boosts = {} |
| |
| if not logic_str: |
| return boosts |
| |
| |
| match = re.search(r"this\.boost\(\{\s*([^}]+)\s*\}", logic_str) |
| if match: |
| boost_str = match.group(1) |
| |
| stat_pairs = re.findall(r"(\w+):\s*(-?\d+)", boost_str) |
| for stat_abbr, value in stat_pairs: |
| stat_name = self._abbr_to_stat_name(stat_abbr) |
| if stat_name: |
| boosts[stat_name] = int(value) |
| |
| return boosts |
| |
| def _abbr_to_stat_name(self, abbr: str) -> Optional[str]: |
| """Convert stat abbreviations to full names.""" |
| abbr_map = { |
| 'hp': 'hp', |
| 'atk': 'attack', |
| 'def': 'defense', |
| 'spa': 'special_attack', |
| 'spd': 'special_defense', |
| 'spe': 'speed' |
| } |
| return abbr_map.get(abbr.lower()) |
|
|
| def on_switch_in(self, pokemon, opponent) -> List[Dict[str, Any]]: |
| results = [] |
| is_p = hasattr(pokemon, "is_player") and pokemon.is_player |
| |
| |
| MECHANICS = { |
| "download": None, |
| } |
| |
| if self.id in MECHANICS: |
| if self.id == "download": |
| if hasattr(opponent, "defense") and hasattr(opponent, "special_defense"): |
| stat = "attack" if opponent.defense < opponent.special_defense else "special_attack" |
| msg = pokemon.modify_stat_stage(stat, 1) |
| if msg: results.append({"type": "ability", "ability_name": self.name, "pokemon_name": pokemon.get_display_name(), "message": f"{pokemon.get_display_name()}'s Download boosted its {stat.replace('_', ' ').title()}!", "is_player": is_p}) |
| else: |
| |
| pass |
| |
| |
| if self.id == 'slowstart' and self.state.get('counter', 0) > 0: |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()} can't get it going!", |
| "is_player": is_p |
| }) |
| elif self.id == 'defeatist': |
| is_active = pokemon.current_hp <= pokemon.max_hp / 2 |
| if is_active and not self.state.get('activated'): |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s Defeatist activated! Its Attack and Sp. Atk were halved!", |
| "is_player": is_p |
| }) |
| self.state['activated'] = True |
| elif not is_active: |
| self.state['activated'] = False |
|
|
| |
| for hook in ["onStart", "onSwitchIn"]: |
| logic = self.config.get(hook) |
| if not logic: continue |
| |
| |
| results.extend(self._extract_popups(logic, pokemon)) |
| |
| |
| if "setWeather" in logic or "setTerrain" in logic: |
| res = {"type": "ability", "ability_name": self.name, "pokemon_name": pokemon.get_display_name(), "message": f"{pokemon.get_display_name()}'s {self.name} changed the field!", "is_player": is_p} |
| |
| |
| w_match = re.search(r"setWeather\('([^']+)'\)", logic) |
| t_match = re.search(r"setTerrain\('([^']+)'\)", logic) |
| if w_match: res["set_weather"] = w_match.group(1).lower() |
| if t_match: res["set_terrain"] = t_match.group(1).lower() |
| |
| results.append(res) |
| |
| |
| boosts = self._parse_boost_amounts(logic) |
| if boosts: |
| target = opponent if "target" in logic else pokemon |
| for s_name, stages in boosts.items(): |
| if hasattr(target, "modify_stat_stage"): |
| msg = target.modify_stat_stage(s_name, stages) |
| if msg: results.append({"type": "ability", "ability_name": self.name, "pokemon_name": pokemon.get_display_name(), "message": msg if isinstance(msg, str) else f"{pokemon.get_display_name()}'s {self.name} activated!", "is_player": is_p}) |
|
|
| |
| for effect in self.config.get("on_switch_in", []): |
| target = opponent if effect.get("target") == "opponent" else pokemon |
| if effect.get("action") == "boost": |
| for s_name, stages in effect.get("stats", {}).items(): |
| if hasattr(target, "modify_stat_stage"): |
| msg = target.modify_stat_stage(s_name, stages) |
| if msg: |
| f_msg = effect.get("message", msg).format(user=pokemon.get_display_name(), target=target.get_display_name()) |
| results.append({"type": "ability", "ability_name": self.name, "pokemon_name": pokemon.get_display_name(), "message": f_msg, "is_player": is_p}) |
| |
| return results |
|
|
| def on_turn_end(self, pokemon, opponent) -> List[Dict[str, Any]]: |
| """Trigger end-of-turn effects (e.g. Speed Boost).""" |
| results = [] |
| |
| |
| if self.id == "speedboost": |
| msg = pokemon.modify_stat_stage("speed", 1) |
| if msg: |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s Speed Boost increased its Speed!", |
| "is_player": hasattr(pokemon, "is_player") and pokemon.is_player |
| }) |
| elif self.id == "losteye": |
| msg = pokemon.modify_stat_stage("accuracy", -1) |
| if msg: |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s Lost Eye lowered its Accuracy!", |
| "is_player": hasattr(pokemon, "is_player") and pokemon.is_player |
| }) |
| elif self.id == "powerspotboost": |
| msg = pokemon.modify_stat_stage("special_attack", 1) |
| if msg: |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s ability boosted its Special Attack!", |
| "is_player": hasattr(pokemon, "is_player") and pokemon.is_player |
| }) |
| elif self.id == "contrariness": |
| |
| msg = pokemon.modify_stat_stage("attack", 1) |
| if msg: |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s Contrariness flipped the stat changes!", |
| "is_player": hasattr(pokemon, "is_player") and pokemon.is_player |
| }) |
| |
| |
| residual_logic = self.config.get("onResidual") |
| if residual_logic: |
| if "damage" in residual_logic.lower(): |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s {self.name} activated!", |
| "is_player": hasattr(pokemon, "is_player") and pokemon.is_player |
| }) |
| |
| |
| if self.id == 'slowstart' and self.state.get('counter', 0) > 0: |
| self.state['counter'] -= 1 |
| if self.state['counter'] == 0: |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()} finally got its act together!", |
| "is_player": hasattr(pokemon, "is_player") and pokemon.is_player |
| }) |
| |
| return results |
|
|
| def on_faint(self, pokemon, opponent) -> List[Dict[str, Any]]: |
| """Trigger effects when the Pokemon faints (e.g. Aftermath).""" |
| results = [] |
| if self.id == "aftermath": |
| damage = opponent.max_hp // 4 |
| opponent.current_hp = max(0, opponent.current_hp - damage) |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s Aftermath hurt {opponent.get_display_name()}!", |
| "is_player": hasattr(pokemon, "is_player") and pokemon.is_player |
| }) |
| return results |
|
|
| def on_source_after_faint(self, pokemon, opponent) -> List[Dict[str, Any]]: |
| """Trigger effects when this Pokemon faints an opponent (e.g. Moxie).""" |
| results = [] |
| |
| |
| logic = self.config.get("onSourceAfterFaint") |
| if logic: |
| |
| if "getBestStat" in logic: |
| if hasattr(pokemon, "get_best_stat"): |
| best_stat_abbr = pokemon.get_best_stat(True, True) |
| |
| stat_map = {'atk': 'attack', 'def': 'defense', 'spa': 'special_attack', 'spd': 'special_defense', 'spe': 'speed'} |
| stat_name = stat_map.get(best_stat_abbr, best_stat_abbr) |
| |
| if hasattr(pokemon, "modify_stat_stage"): |
| msg = pokemon.modify_stat_stage(stat_name, 1) |
| if msg: |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s {self.name} boosted its {stat_name}!", |
| "is_player": hasattr(pokemon, "is_player") and pokemon.is_player |
| }) |
| return results |
|
|
| boosts = self._parse_boost_amounts(logic) |
| if boosts: |
| for stat_name, stages in boosts.items(): |
| |
| |
| amount = stages if stages != 0 else 1 |
| if hasattr(pokemon, "modify_stat_stage"): |
| msg = pokemon.modify_stat_stage(stat_name, amount) |
| if msg: |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s {self.name} boosted its {stat_name}!", |
| "is_player": hasattr(pokemon, "is_player") and pokemon.is_player |
| }) |
| |
| return results |
|
|
| def on_any_faint(self, pokemon) -> List[Dict[str, Any]]: |
| """Trigger effects when any Pokemon faints (e.g. Soul-Heart).""" |
| results = [] |
| |
| logic = self.config.get("onAnyFaint") |
| if logic: |
| boosts = self._parse_boost_amounts(logic) |
| if boosts: |
| for stat_name, stages in boosts.items(): |
| if hasattr(pokemon, "modify_stat_stage"): |
| msg = pokemon.modify_stat_stage(stat_name, stages) |
| if msg: |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s {self.name} boosted its {stat_name}!", |
| "is_player": hasattr(pokemon, "is_player") and pokemon.is_player |
| }) |
| |
| return results |
|
|
| def on_stat_drop(self, pokemon, stat_name: str) -> List[Dict[str, Any]]: |
| """Trigger effects when a stat is lowered (e.g. Defiant).""" |
| results = [] |
| |
| if self.id == "defiant": |
| |
| if hasattr(pokemon, "modify_stat_stage"): |
| pokemon.modify_stat_stage("attack", 2) |
| elif self.id == "competitive": |
| |
| if hasattr(pokemon, "modify_stat_stage"): |
| pokemon.modify_stat_stage("special_attack", 2) |
| |
| return results |
|
|
| def modify_damage_taken(self, pokemon, opponent, move, damage: int) -> int: |
| """Modifies damage taken by the Pokemon.""" |
| final_damage = damage |
| |
| |
| if self.id == "filter" or self.id == "solidrock": |
| |
| if hasattr(move, 'effectiveness'): |
| if move.effectiveness > 1: |
| final_damage = int(final_damage * 0.75) |
| elif self.id == "thickfat": |
| |
| if hasattr(move, 'type') and move.type.lower() in ['fire', 'ice']: |
| final_damage = int(final_damage * 0.5) |
| elif self.id == "waterabsorb" or self.id == "dryskin": |
| |
| if hasattr(move, 'type') and move.type.lower() == 'water': |
| return 0 |
| if hasattr(move, 'type') and move.type.lower() == 'fire': |
| final_damage = int(final_damage * 1.25) |
| elif self.id == "heatproof": |
| if hasattr(move, 'type') and move.type.lower() == 'fire': |
| final_damage = int(final_damage * 0.5) |
| elif self.id == "flashfire": |
| |
| if hasattr(move, 'type') and move.type.lower() == 'fire': |
| return 0 |
| elif self.id == "voltabsorb": |
| |
| if hasattr(move, 'type') and move.type.lower() == 'electric': |
| return 0 |
| elif self.id == "sapsipper": |
| |
| if hasattr(move, 'type') and move.type.lower() == 'grass': |
| return 0 |
| elif self.id == "furcoat": |
| |
| if hasattr(move, 'category') and move.category == 'physical': |
| final_damage = int(final_damage * 0.5) |
| elif self.id == "marvelscale": |
| |
| if pokemon.major_status: |
| final_damage = int(final_damage * 0.5) |
| elif self.id == "unaware": |
| |
| final_damage = int(final_damage * 0.8) |
| elif self.id == "regenerator": |
| |
| pass |
| |
| |
| on_damage = self.config.get("onDamage") |
| if on_damage: |
| if "damage * 0.5" in on_damage or "chainModify(0.5)" in on_damage: |
| final_damage = int(final_damage * 0.5) |
| elif "damage * 0.75" in on_damage or "chainModify(0.75)" in on_damage: |
| final_damage = int(final_damage * 0.75) |
| |
| return final_damage |
|
|
| def modify_damage_dealt(self, pokemon, opponent, move, damage: int) -> int: |
| """Modifies damage dealt by the Pokemon.""" |
| final_damage = damage |
| |
| |
| if self.id == "technician": |
| |
| if hasattr(move, 'power') and 0 < move.power <= 60: |
| final_damage = int(final_damage * 1.5) |
| elif self.id == "adaptability": |
| |
| pass |
| elif self.id == "sheerforce": |
| |
| if hasattr(move, 'secondary') and move.secondary: |
| final_damage = int(final_damage * 1.3125) |
| elif self.id == "hugepower" or self.id == "purplepower": |
| |
| pass |
| elif self.id == "ironbarbs": |
| |
| pass |
| elif self.id == "roughskin": |
| |
| pass |
| elif self.id == "effectspore": |
| |
| pass |
| elif self.id == "sandstream": |
| |
| pass |
| elif self.id == "swordofruin": |
| |
| final_damage = int(final_damage * 0.8) |
| elif self.id == "beadsofruin": |
| |
| final_damage = int(final_damage * 0.8) |
| elif self.id == "tabletsofruin": |
| |
| final_damage = int(final_damage * 0.8) |
| elif self.id == "vesselofruin": |
| |
| final_damage = int(final_damage * 0.8) |
| |
| |
| modifiers = self.config.get("damage_modifiers", []) |
| for mod in modifiers: |
| condition = mod.get("condition") |
| multiplier = mod.get("multiplier", 1.0) |
| |
| if condition == "hp_threshold": |
| threshold = mod.get("threshold", 0.33) |
| required_type = mod.get("move_type") |
| if pokemon.current_hp / pokemon.max_hp <= threshold: |
| if not required_type or move.type.lower() == required_type.lower(): |
| final_damage = int(final_damage * multiplier) |
| elif condition == "base_power_below": |
| threshold = mod.get("threshold", 60) |
| if move.power <= threshold and move.power > 0: |
| final_damage = int(final_damage * multiplier) |
| |
| |
| for hook in ["onBasePower", "onModifyAtk", "onModifySpA"]: |
| |
| if hook == "onModifyAtk" and move.category != 'physical': |
| continue |
| if hook == "onModifySpA" and move.category != 'special': |
| continue |
| |
| logic = self.config.get(hook) |
| if not logic: continue |
| |
| if self._check_condition(logic, pokemon, opponent, move): |
| multiplier = self._parse_chain_modify(logic) |
| final_damage = int(final_damage * multiplier) |
| |
| return final_damage |
|
|
| def is_immune(self, move_type: str, move_category: str) -> bool: |
| """Checks if the ability provides immunity to a certain move type/category.""" |
| immunities = self.config.get("immunities", {}) |
| |
| |
| if move_type.lower() in [t.lower() for t in immunities.get("types", [])]: |
| return True |
| |
| |
| type_immunities = { |
| 'levitate': ['ground'], |
| 'waterabsorb': ['water'], |
| 'voltabsorb': ['electric'], |
| 'dryskin': ['water'], |
| 'flashfire': ['fire'], |
| 'sapsipper': ['grass'], |
| 'lightningrod': ['electric'], |
| 'motordrive': ['electric'], |
| 'immunity': ['poison'], |
| 'wonderguard': [], |
| 'goodasgold': ['item-based'], |
| } |
| |
| if self.id in type_immunities: |
| if move_type.lower() in type_immunities[self.id]: |
| return True |
| |
| |
| on_try_hit = self.config.get("onTryHit") |
| if on_try_hit: |
| |
| if f"move.type === '{move_type.capitalize()}'" in on_try_hit or f"move.type === '{move_type.lower()}'" in on_try_hit: |
| if "return null" in on_try_hit or "return false" in on_try_hit: |
| return True |
|
|
| return False |
|
|
| def get_type_change(self, move) -> Optional[str]: |
| """Returns the type a move is changed to by this ability (e.g. Aerilate).""" |
| type_changes = { |
| 'aerilate': 'flying', |
| 'pixilate': 'fairy', |
| 'refrigerate': 'ice', |
| 'iondeluge': 'electric', |
| 'normalize': 'normal', |
| } |
| |
| if self.id in type_changes and hasattr(move, 'type') and move.type.lower() == 'normal': |
| return type_changes[self.id] |
| |
| return None |
|
|
| def get_weather_boost(self, move_type: str, weather: Optional[str]) -> float: |
| """Returns damage multiplier based on weather and this ability.""" |
| if not weather: |
| return 1.0 |
| |
| weather_boosts = { |
| 'drizzle': {'water': 1.5, 'fire': 0.5}, |
| 'drought': {'fire': 1.5, 'water': 0.5}, |
| 'sandstream': {'rock': 1.5, 'steel': 1.5, 'ground': 1.5, 'fire': 0.5}, |
| 'snowwarning': {'ice': 1.5, 'fire': 0.5}, |
| 'hail': {'ice': 1.5}, |
| } |
| |
| if weather in weather_boosts: |
| return weather_boosts[weather].get(move_type.lower(), 1.0) |
| |
| return 1.0 |
|
|
| def get_stab_multiplier(self) -> float: |
| """Returns the STAB multiplier (usually 1.5, Adaptability makes it 2.0).""" |
| |
| if "on_modify_stab" in self.config: |
| return self.config["on_modify_stab"] |
| |
| |
| stab_multipliers = { |
| 'adaptability': 2.0, |
| } |
| |
| if self.id in stab_multipliers: |
| return stab_multipliers[self.id] |
| |
| |
| on_modify_stab = self.config.get("onModifySTAB") |
| if on_modify_stab: |
| if "return 2.25" in on_modify_stab: return 2.25 |
| if "return 2" in on_modify_stab: return 2.0 |
| |
| return 1.5 |
|
|
| def get_secondary_multiplier(self) -> float: |
| """Returns the multiplier for secondary effect chances.""" |
| |
| if "secondary_multiplier" in self.config: |
| return self.config["secondary_multiplier"] |
| |
| |
| secondary_multipliers = { |
| 'serenegrace': 2.0, |
| 'sheerforce': 1.3, |
| } |
| |
| if self.id in secondary_multipliers: |
| return secondary_multipliers[self.id] |
| |
| |
| on_modify_move = self.config.get("onModifyMove") |
| if on_modify_move: |
| if "* 2" in on_modify_move or "*= 2" in on_modify_move: return 2.0 |
| if "* 3" in on_modify_move or "*= 3" in on_modify_move: return 3.0 |
| |
| return 1.0 |
|
|
| def get_accuracy_modifier(self) -> float: |
| """Returns the accuracy multiplier for moves used by this ability.""" |
| accuracy_modifiers = { |
| 'compoundeyes': 1.3, |
| 'victorystar': 1.1, |
| 'keeneye': 1.0, |
| } |
| |
| if self.id in accuracy_modifiers: |
| return accuracy_modifiers[self.id] |
| |
| return 1.0 |
|
|
| def get_ability_summary(self) -> Dict[str, Any]: |
| """Returns a summary of what this ability does.""" |
| summary = { |
| 'id': self.id, |
| 'name': self.name, |
| 'description': self.description, |
| 'rating': self.config.get('rating', 0), |
| } |
| |
| |
| hooks = list(self.config.keys()) |
| |
| if any(h in hooks for h in ['onStart', 'onSwitchIn', 'drizzle', 'drought']): |
| summary['type'] = 'Stat/Weather Setter' |
| elif any(h in hooks for h in ['onBasePower', 'onModifyAtk', 'onModifySpA']): |
| summary['type'] = 'Damage Modifier' |
| elif any(h in hooks for h in ['onDamage', 'onTryHit']): |
| summary['type'] = 'Defensive' |
| elif any(h in hooks for h in ['onResidual']): |
| summary['type'] = 'End-of-turn' |
| elif any(h in hooks for h in ['onTryBoost']): |
| summary['type'] = 'Stat Protection' |
| else: |
| summary['type'] = 'Special Effect' |
| |
| return summary |
|
|
| def get_priority_modification(self) -> float: |
| """Returns priority modification (e.g. Stall).""" |
| return self.config.get('onFractionalPriority', 0.0) |
|
|
| def can_use_move(self, pokemon) -> Tuple[bool, str]: |
| """Checks if the ability allows the move (e.g. Truant).""" |
| if self.id == 'truant': |
| if self.state.get('skip'): |
| self.state['skip'] = False |
| return False, f"{pokemon.get_display_name()} is loafing around!" |
| self.state['skip'] = True |
| return True, "" |
|
|
| def on_damage(self, pokemon, damage: int) -> List[Dict[str, Any]]: |
| """Trigger effects when taking damage (e.g. Defeatist).""" |
| results = [] |
| is_p = hasattr(pokemon, "is_player") and pokemon.is_player |
| |
| if self.id == 'defeatist': |
| |
| old_hp = pokemon.current_hp + damage |
| is_active = pokemon.current_hp <= pokemon.max_hp / 2 |
| |
| if old_hp > pokemon.max_hp / 2 and is_active: |
| if not self.state.get('activated'): |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()}'s {self.name} activated! Its Attack and Sp. Atk were halved!", |
| "is_player": is_p |
| }) |
| self.state['activated'] = True |
| elif self.id == 'sturdy': |
| |
| |
| if pokemon.current_hp == 1 and damage >= 1: |
| |
| results.append({ |
| "type": "ability", |
| "ability_name": self.name, |
| "pokemon_name": pokemon.get_display_name(), |
| "message": f"{pokemon.get_display_name()} endured the hit with Sturdy!", |
| "is_player": is_p |
| }) |
| |
| return results |
|
|
| def can_use_item(self) -> bool: |
| """Checks if the ability allows using held items (e.g. Klutz).""" |
| if self.id == 'klutz': |
| return False |
| return True |
|
|
| def create_ability(name: str) -> Ability: |
| """Helper to create an ability instance.""" |
| if not name: |
| return Ability("noability") |
| return Ability(name) |
|
|