Pokemon-Battle-Sim / src /systems /ability_system.py
github-actions
Deploy to Hugging Face Spaces
6c7a453
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.")
# State for specific abilities
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
# 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
curr_hp = getattr(pokemon, 'current_hp', getattr(pokemon, 'max_hp', 100))
max_hp = getattr(pokemon, 'max_hp', 100)
# Pattern: pokemon.hp <= pokemon.maxhp / 3
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
# Pattern: pokemon.status
if "pokemon.status" in logic_str or "attacker.status" in logic_str:
if pokemon.major_status:
return True
return False
# Pattern: move.type === 'Fire'
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
# Pattern: this.field.isTerrain('electricterrain')
match = re.search(r"isTerrain\('([^']+)'\)", logic_str)
if match:
target_terrain = match.group(1).lower()
# Simple check for now - would need terrain system in game.py
return False # Fallback until terrain is implemented
# Handle basePowerAfterMultiplier <= 60 or move.power <= 60
if "power <= 60" in logic_str or "basePowerAfterMultiplier <= 60" in logic_str:
if move and 0 < move.power <= 60:
return True
return False
# Generic power check
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
# Pattern: this.effectState.counter
if "this.effectState.counter" in logic_str:
if self.state.get('counter', 0) > 0:
return True
return False
# Pattern: move.flags['pulse']
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 no obvious condition, assume it applies only if it's a simple logic string
# If it contains complex JS patterns we don't recognize, it's safer to return False
# than to always return True (which was causing over-damage)
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
# Pattern mappings for common Showdown log calls
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."""
# 1. Check direct config (legacy/simple)
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)
# 2. Check dynamic logic from JSON
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]
# Special case for Slow Start
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
# Pattern: this.boost({ atk: 2, spa: 1 }, target, ...)
match = re.search(r"this\.boost\(\{\s*([^}]+)\s*\}", logic_str)
if match:
boost_str = match.group(1)
# Extract individual stats: atk: 1, def: -1, etc.
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
# Priority handlers for common switch-in mechanics that need complex logic
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:
# Fallback for any remaining mechanics
pass
# Special switch-in popups for bad abilities
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
# Process dynamic hooks (onStart, onSwitchIn)
for hook in ["onStart", "onSwitchIn"]:
logic = self.config.get(hook)
if not logic: continue
# Extract generic popups from JSON logic (e.g. Turboblaze)
results.extend(self._extract_popups(logic, pokemon))
# Weather/Terrain
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}
# Extract the weather/terrain name if possible
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
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})
# Process effect list
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 = []
# Common turn-end abilities
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":
# Flips stat changes
msg = pokemon.modify_stat_stage("attack", 1) # Placeholder - actual logic would flip boosts
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
})
# Parse onResidual for passive damage/healing
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
})
# Slow Start counter
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 = []
# Generic parsing for boost patterns in onSourceAfterFaint hook
logic = self.config.get("onSourceAfterFaint")
if logic:
# Check for bestStat call (Beast Boost)
if "getBestStat" in logic:
if hasattr(pokemon, "get_best_stat"):
best_stat_abbr = pokemon.get_best_stat(True, True)
# Convert to full name for modify_stat_stage
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():
# In onSourceAfterFaint, 'length' is usually used for boost amount (usually 1)
# We'll use the parsed value or default to 1
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":
# Defiant: +2 Atk when any stat is lowered
if hasattr(pokemon, "modify_stat_stage"):
pokemon.modify_stat_stage("attack", 2)
elif self.id == "competitive":
# Competitive: +2 SpA when any stat is lowered
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
# Abilities that reduce damage
if self.id == "filter" or self.id == "solidrock":
# Reduces super-effective damage to 1/4x (6/8 = 0.75)
if hasattr(move, 'effectiveness'):
if move.effectiveness > 1:
final_damage = int(final_damage * 0.75)
elif self.id == "thickfat":
# Reduces Fire and Ice type moves by 50%
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":
# Heals from Water type moves instead of taking damage
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":
# Absorbs Fire type moves
if hasattr(move, 'type') and move.type.lower() == 'fire':
return 0
elif self.id == "voltabsorb":
# Absorbs Electric type moves
if hasattr(move, 'type') and move.type.lower() == 'electric':
return 0
elif self.id == "sapsipper":
# Absorbs Grass type moves
if hasattr(move, 'type') and move.type.lower() == 'grass':
return 0
elif self.id == "furcoat":
# Halves physical damage
if hasattr(move, 'category') and move.category == 'physical':
final_damage = int(final_damage * 0.5)
elif self.id == "marvelscale":
# Reduces all damage to 50% when having status
if pokemon.major_status:
final_damage = int(final_damage * 0.5)
elif self.id == "unaware":
# Ignores opponent stat boosts (reduce damage)
final_damage = int(final_damage * 0.8) # Simplified
elif self.id == "regenerator":
# Heals 1/3 HP per turn (handled elsewhere)
pass
# Generic damage reduction from onDamage hooks
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
# Hardcoded abilities with damage modifiers
if self.id == "technician":
# 1.5x damage for moves with 60 or less base power
if hasattr(move, 'power') and 0 < move.power <= 60:
final_damage = int(final_damage * 1.5)
elif self.id == "adaptability":
# 2.25x STAB instead of 1.5x (handled in STAB calculation)
pass
elif self.id == "sheerforce":
# 1.3125x (1.3x boost) for moves with secondary effects
if hasattr(move, 'secondary') and move.secondary:
final_damage = int(final_damage * 1.3125)
elif self.id == "hugepower" or self.id == "purplepower":
# Doubles attack (handled in modify_stat)
pass
elif self.id == "ironbarbs":
# Reflects 1/8 damage back (handled separately)
pass
elif self.id == "roughskin":
# Reflects 1/8 damage back (handled separately)
pass
elif self.id == "effectspore":
# 30% chance to cause status on contact (handled separately)
pass
elif self.id == "sandstream":
# Weakens water moves (handled with weather)
pass
elif self.id == "swordofruin":
# Reduces opponent Special Defense (handled separately)
final_damage = int(final_damage * 0.8) # Simplified
elif self.id == "beadsofruin":
# Reduces opponent Special Defense (handled separately)
final_damage = int(final_damage * 0.8) # Simplified
elif self.id == "tabletsofruin":
# Reduces opponent Special Defense (handled separately)
final_damage = int(final_damage * 0.8) # Simplified
elif self.id == "vesselofruin":
# Reduces opponent Special Defense (handled separately)
final_damage = int(final_damage * 0.8) # Simplified
# 1. Check direct modifiers list (legacy/simple)
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)
# 2. Check raw logic for multipliers (Technician, Aerilate, etc.)
for hook in ["onBasePower", "onModifyAtk", "onModifySpA"]:
# Only apply Atk/SpA hooks if they match the move category
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", {})
# 1. Check direct immunities (e.g. Levitate)
if move_type.lower() in [t.lower() for t in immunities.get("types", [])]:
return True
# 2. Hardcoded type immunities
type_immunities = {
'levitate': ['ground'],
'waterabsorb': ['water'],
'voltabsorb': ['electric'],
'dryskin': ['water'],
'flashfire': ['fire'],
'sapsipper': ['grass'],
'lightningrod': ['electric'],
'motordrive': ['electric'],
'immunity': ['poison'],
'wonderguard': [], # Only takes super-effective damage
'goodasgold': ['item-based'],
}
if self.id in type_immunities:
if move_type.lower() in type_immunities[self.id]:
return True
# 3. Check raw logic for immunity (onTryHit)
on_try_hit = self.config.get("onTryHit")
if on_try_hit:
# Check if this move type is mentioned as being blocked
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)."""
# 1. Check direct config
if "on_modify_stab" in self.config:
return self.config["on_modify_stab"]
# Hardcoded STAB multipliers
stab_multipliers = {
'adaptability': 2.0, # Adaptability changes STAB from 1.5x to 2x.
}
if self.id in stab_multipliers:
return stab_multipliers[self.id]
# 2. Check raw logic for Adaptability pattern
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."""
# 1. Check direct config
if "secondary_multiplier" in self.config:
return self.config["secondary_multiplier"]
# Hardcoded secondary multipliers
secondary_multipliers = {
'serenegrace': 2.0, # 2x secondary chance
'sheerforce': 1.3, # 1.3x damage but removes secondary effects
}
if self.id in secondary_multipliers:
return secondary_multipliers[self.id]
# 2. Check raw logic for Serene Grace pattern
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, # 1.3x accuracy
'victorystar': 1.1, # 1.1x accuracy
'keeneye': 1.0, # Prevents accuracy lowering
}
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),
}
# Determine ability category
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':
# Check if we just dropped below 50%
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 the pokemon survived with 1 HP and the damage was essentially its entire HP
# We check if it's currently at 1 and the damage dealt was prev_hp - 1
if pokemon.current_hp == 1 and damage >= 1:
# This is a bit of a heuristic but works since Sturdy only triggers at max HP
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)