853 lines
36 KiB
Python
853 lines
36 KiB
Python
import random
|
|
from .enums import Ability, ElementType
|
|
|
|
|
|
class ExpiringEffect():
|
|
"""
|
|
Some effect that has a specific amount of time it is active.
|
|
|
|
turns_to_expire can be None, in which case this effect never expires.
|
|
"""
|
|
def __init__(self, turns_to_expire: int):
|
|
self._remaining_turns = turns_to_expire
|
|
|
|
def active(self):
|
|
"""Returns True if this effect is still active, False otherwise."""
|
|
if self._remaining_turns is None:
|
|
return True
|
|
return bool(self._remaining_turns)
|
|
|
|
def next_turn(self):
|
|
"""
|
|
Progresses this effect for a turn.
|
|
|
|
Returns True if the effect just ended.
|
|
"""
|
|
if self._remaining_turns is None:
|
|
return False
|
|
if self.active():
|
|
self._remaining_turns -= 1
|
|
return not self.active()
|
|
return False
|
|
|
|
def set_turns(self, turns_to_expire):
|
|
"""Set the amount of turns until this effect expires."""
|
|
self._remaining_turns = turns_to_expire
|
|
|
|
|
|
class Weather(ExpiringEffect):
|
|
"""
|
|
The current weather of the battlefield.
|
|
|
|
Options:
|
|
-hail
|
|
-sandstorm
|
|
-h-rain
|
|
-rain
|
|
-h-sun
|
|
-sun
|
|
-h-wind
|
|
"""
|
|
def __init__(self, battle):
|
|
super().__init__(0)
|
|
self._weather_type = ""
|
|
self.battle = battle
|
|
|
|
def _expire_weather(self):
|
|
"""Clear the current weather and update Castform forms."""
|
|
self._weather_type = ""
|
|
for poke in (self.battle.trainer1.current_pokemon, self.battle.trainer2.current_pokemon):
|
|
if poke is None:
|
|
continue
|
|
# Forecast
|
|
if poke.ability() == Ability.FORECAST and poke._name in ("Castform-snowy", "Castform-rainy", "Castform-sunny"):
|
|
if poke.form("Castform"):
|
|
poke.type_ids = [ElementType.NORMAL]
|
|
|
|
def next_turn(self):
|
|
"""Progresses the weather a turn."""
|
|
if super().next_turn():
|
|
self._expire_weather()
|
|
return True
|
|
return False
|
|
|
|
def recheck_ability_weather(self):
|
|
"""Checks if strong weather effects from a pokemon with a weather ability need to be removed."""
|
|
maintain_weather = False
|
|
for poke in (self.battle.trainer1.current_pokemon, self.battle.trainer2.current_pokemon):
|
|
if poke is None:
|
|
continue
|
|
if self._weather_type == "h-wind" and poke.ability() == Ability.DELTA_STREAM:
|
|
maintain_weather = True
|
|
if self._weather_type == "h-sun" and poke.ability() == Ability.DESOLATE_LAND:
|
|
maintain_weather = True
|
|
if self._weather_type == "h-rain" and poke.ability() == Ability.PRIMORDIAL_SEA:
|
|
maintain_weather = True
|
|
|
|
if self._weather_type in ("h-wind", "h-sun", "h-rain") and not maintain_weather:
|
|
self._expire_weather()
|
|
return True
|
|
return False
|
|
|
|
def get(self):
|
|
"""Get the current weather type."""
|
|
for poke in (self.battle.trainer1.current_pokemon, self.battle.trainer2.current_pokemon):
|
|
if poke is None:
|
|
continue
|
|
if poke.ability() in (Ability.CLOUD_NINE, Ability.AIR_LOCK):
|
|
return ""
|
|
return self._weather_type
|
|
|
|
def set(self, weather: str, pokemon):
|
|
"""
|
|
Set the weather, lasting a certain number of turns.
|
|
|
|
Returns a formatted message indicating any weather change.
|
|
"""
|
|
msg = ""
|
|
turns = None
|
|
element = None
|
|
castform = None
|
|
if self._weather_type == weather:
|
|
return ""
|
|
if weather == "hail":
|
|
if self._weather_type in ("h-rain", "h-sun", "h-wind"):
|
|
return ""
|
|
if pokemon.held_item == "icy-rock":
|
|
turns = 8
|
|
else:
|
|
turns = 5
|
|
msg += "It starts to hail!\n"
|
|
element = ElementType.ICE
|
|
castform = "Castform-snowy"
|
|
elif weather == "sandstorm":
|
|
if self._weather_type in ("h-rain", "h-sun", "h-wind"):
|
|
return ""
|
|
if pokemon.held_item == "smooth-rock":
|
|
turns = 8
|
|
else:
|
|
turns = 5
|
|
msg += "A sandstorm is brewing up!\n"
|
|
element = ElementType.NORMAL
|
|
castform = "Castform"
|
|
elif weather == "rain":
|
|
if self._weather_type in ("h-rain", "h-sun", "h-wind"):
|
|
return ""
|
|
if pokemon.held_item == "damp-rock":
|
|
turns = 8
|
|
else:
|
|
turns = 5
|
|
msg += "It starts to rain!\n"
|
|
element = ElementType.WATER
|
|
castform = "Castform-rainy"
|
|
elif weather == "sun":
|
|
if self._weather_type in ("h-rain", "h-sun", "h-wind"):
|
|
return ""
|
|
if pokemon.held_item == "heat-rock":
|
|
turns = 8
|
|
else:
|
|
turns = 5
|
|
msg += "The sunlight is strong!\n"
|
|
element = ElementType.FIRE
|
|
castform = "Castform-sunny"
|
|
elif weather == "h-rain":
|
|
msg += "Heavy rain begins to fall!\n"
|
|
element = ElementType.WATER
|
|
castform = "Castform-rainy"
|
|
elif weather == "h-sun":
|
|
msg += "The sunlight is extremely harsh!\n"
|
|
element = ElementType.FIRE
|
|
castform = "Castform-sunny"
|
|
elif weather == "h-wind":
|
|
msg += "The winds are extremely strong!\n"
|
|
element = ElementType.NORMAL
|
|
castform = "Castform"
|
|
else:
|
|
raise ValueError("unexpected weather")
|
|
|
|
# Forecast
|
|
t = ElementType(element).name.lower()
|
|
for poke in (self.battle.trainer1.current_pokemon, self.battle.trainer2.current_pokemon):
|
|
if poke is None:
|
|
continue
|
|
if poke.ability() == Ability.FORECAST and poke._name != castform:
|
|
if poke.form(castform):
|
|
poke.type_ids = [element]
|
|
msg += f"{poke.name} transformed into a {t} type using its forecast!\n"
|
|
|
|
self._weather_type = weather
|
|
self._remaining_turns = turns
|
|
return msg
|
|
|
|
|
|
class LockedMove(ExpiringEffect):
|
|
"""A multi-turn move that a pokemon is locked into."""
|
|
def __init__(self, move, turns_to_expire: int):
|
|
super().__init__(turns_to_expire)
|
|
self.move = move
|
|
self.turn = 0
|
|
|
|
def next_turn(self):
|
|
"""Progresses the move a turn."""
|
|
expired = super().next_turn()
|
|
self.turn += 1
|
|
return expired
|
|
|
|
def is_last_turn(self):
|
|
"""Returns True if this is the last turn this move will be used."""
|
|
return self._remaining_turns == 1
|
|
|
|
|
|
class ExpiringItem(ExpiringEffect):
|
|
"""An expiration timer with some data."""
|
|
def __init__(self):
|
|
super().__init__(0)
|
|
self.item = None
|
|
|
|
def next_turn(self):
|
|
"""Progresses the effect a turn."""
|
|
expired = super().next_turn()
|
|
if expired:
|
|
self.item = None
|
|
return expired
|
|
|
|
def set(self, item, turns: int):
|
|
"""Set the item and turns until expiration."""
|
|
self.item = item
|
|
self._remaining_turns = turns
|
|
|
|
def end(self):
|
|
"""Ends this expiring item."""
|
|
self.item = None
|
|
self._remaining_turns = 0
|
|
|
|
|
|
class Terrain(ExpiringItem):
|
|
"""The terrain of the battle"""
|
|
def __init__(self, battle):
|
|
super().__init__()
|
|
self.battle = battle
|
|
|
|
def next_turn(self):
|
|
"""Progresses the effect a turn."""
|
|
expired = super().next_turn()
|
|
if expired:
|
|
self.end()
|
|
return expired
|
|
|
|
def set(self, item, attacker):
|
|
"""
|
|
Set the terrain and turns until expiration.
|
|
|
|
Returns a formatted string.
|
|
"""
|
|
if item == self.item:
|
|
return f"There's already a {item} terrain!\n"
|
|
turns = 8 if attacker.held_item == "terrain-extender" else 5
|
|
super().set(item, turns)
|
|
msg = f"{attacker.name} creates a{'n' if item == 'electric' else ''} {item} terrain!\n"
|
|
# Mimicry
|
|
element = None
|
|
if item == "electric":
|
|
element = ElementType.ELECTRIC
|
|
elif item == "grassy":
|
|
element = ElementType.GRASS
|
|
elif item == "misty":
|
|
element = ElementType.FAIRY
|
|
elif item == "psychic":
|
|
element = ElementType.PSYCHIC
|
|
for poke in (self.battle.trainer1.current_pokemon, self.battle.trainer2.current_pokemon):
|
|
if poke is None:
|
|
continue
|
|
if poke.ability() == Ability.MIMICRY:
|
|
poke.type_ids = [element]
|
|
t = ElementType(element).name.lower()
|
|
msg += f"{poke.name} became a {t} type using its mimicry!\n"
|
|
if poke.held_item == "electric-seed" and item == "electric":
|
|
msg += poke.append_defense(1, attacker=poke, source="its electric seed")
|
|
poke.held_item.use()
|
|
if poke.held_item == "psychic-seed" and item == "psychic":
|
|
msg += poke.append_spdef(1, attacker=poke, source="its psychic seed")
|
|
poke.held_item.use()
|
|
if poke.held_item == "misty-seed" and item == "misty":
|
|
msg += poke.append_spdef(1, attacker=poke, source="its misty seed")
|
|
poke.held_item.use()
|
|
if poke.held_item == "grassy-seed" and item == "grassy":
|
|
msg += poke.append_defense(1, attacker=poke, source="its grassy seed")
|
|
poke.held_item.use()
|
|
return msg
|
|
|
|
def end(self):
|
|
"""Ends the terrain."""
|
|
super().end()
|
|
# Mimicry
|
|
for poke in (self.battle.trainer1.current_pokemon, self.battle.trainer2.current_pokemon):
|
|
if poke is None:
|
|
continue
|
|
if poke.ability() == Ability.MIMICRY:
|
|
poke.type_ids = poke.starting_type_ids.copy()
|
|
|
|
|
|
class ExpiringWish(ExpiringEffect):
|
|
"""Stores the HP and when to heal for the move Wish."""
|
|
def __init__(self):
|
|
super().__init__(0)
|
|
self.hp = None
|
|
|
|
def next_turn(self):
|
|
"""Progresses the effect a turn."""
|
|
expired = super().next_turn()
|
|
hp = 0
|
|
if expired:
|
|
hp = self.hp
|
|
self.hp = None
|
|
return hp
|
|
|
|
def set(self, hp):
|
|
"""Set the move and turns until expiration."""
|
|
self.hp = hp
|
|
self._remaining_turns = 2
|
|
|
|
|
|
class NonVolatileEffect():
|
|
"""The current non volatile effect status."""
|
|
def __init__(self, pokemon):
|
|
self.current = ""
|
|
self.pokemon = pokemon
|
|
self.sleep_timer = ExpiringEffect(0)
|
|
self.badly_poisoned_turn = 0
|
|
|
|
def next_turn(self, battle):
|
|
"""
|
|
Progresses this status by a turn.
|
|
|
|
Returns a formatted string if a status wore off.
|
|
"""
|
|
if not self.current:
|
|
return ""
|
|
if self.current == "b-poison":
|
|
self.badly_poisoned_turn += 1
|
|
if self.pokemon.ability() == Ability.HYDRATION and battle.weather.get() in ("rain", "h-rain"):
|
|
removed = self.current
|
|
self.reset()
|
|
return f"{self.pokemon.name}'s hydration cured its {removed}!\n"
|
|
if self.pokemon.ability() == Ability.SHED_SKIN and not random.randint(0, 2):
|
|
removed = self.current
|
|
self.reset()
|
|
return f"{self.pokemon.name}'s shed skin cured its {removed}!\n"
|
|
# The poke still has a status effect, apply damage
|
|
if self.current == "burn":
|
|
damage = max(1, self.pokemon.starting_hp // 16)
|
|
if self.pokemon.ability() == Ability.HEATPROOF:
|
|
damage //= 2
|
|
return self.pokemon.damage(damage, battle, source="its burn")
|
|
if self.current == "b-poison":
|
|
if self.pokemon.ability() == Ability.POISON_HEAL:
|
|
return self.pokemon.heal(self.pokemon.starting_hp // 8, source="its poison heal")
|
|
damage = max(1, (self.pokemon.starting_hp // 16) * min(15, self.badly_poisoned_turn))
|
|
return self.pokemon.damage(damage, battle, source="its bad poison")
|
|
if self.current == "poison":
|
|
if self.pokemon.ability() == Ability.POISON_HEAL:
|
|
return self.pokemon.heal(self.pokemon.starting_hp // 8, source="its poison heal")
|
|
damage = max(1, self.pokemon.starting_hp // 8)
|
|
return self.pokemon.damage(damage, battle, source="its poison")
|
|
if self.current == "sleep" and self.pokemon.nightmare:
|
|
return self.pokemon.damage(self.pokemon.starting_hp // 4, battle, source="its nightmare")
|
|
return ""
|
|
|
|
def burn(self):
|
|
"""Returns True if the pokemon is burned."""
|
|
return self.current == "burn"
|
|
|
|
def sleep(self):
|
|
"""Returns True if the pokemon is asleep."""
|
|
if self.pokemon.ability() == Ability.COMATOSE:
|
|
return True
|
|
return self.current == "sleep"
|
|
|
|
def poison(self):
|
|
"""Returns True if the pokemon is poisoned."""
|
|
return self.current in ("poison", "b-poison")
|
|
|
|
def paralysis(self):
|
|
"""Returns True if the pokemon is paralyzed."""
|
|
return self.current == "paralysis"
|
|
|
|
def freeze(self):
|
|
"""Returns True if the pokemon is frozen."""
|
|
return self.current == "freeze"
|
|
|
|
def apply_status(self, status, battle, *, attacker=None, move=None, turns=None, force=False, source: str=""):
|
|
"""
|
|
Apply a non volatile status to a pokemon.
|
|
|
|
Returns a formatted message.
|
|
"""
|
|
msg = ""
|
|
if source:
|
|
source = f" from {source}"
|
|
if self.current and not force:
|
|
return f"{self.pokemon.name} already has a status, it can't get {status} too!\n"
|
|
if self.pokemon.ability(attacker=attacker, move=move) == Ability.COMATOSE:
|
|
return f"{self.pokemon.name} already has a status, it can't get {status} too!\n"
|
|
if self.pokemon.ability(attacker=attacker, move=move) == Ability.PURIFYING_SALT:
|
|
return f"{self.pokemon.name}'s purifying salt protects it from being inflicted with {status}!\n"
|
|
if self.pokemon.ability(attacker=attacker, move=move) == Ability.LEAF_GUARD and battle.weather.get() in ("sun", "h-sun"):
|
|
return f"{self.pokemon.name}'s leaf guard protects it from being inflicted with {status}!\n"
|
|
if self.pokemon.substitute and attacker is not self.pokemon and (move is None or move.is_affected_by_substitute()):
|
|
return f"{self.pokemon.name}'s substitute protects it from being inflicted with {status}!\n"
|
|
if self.pokemon.owner.safeguard.active() and attacker is not self.pokemon and (attacker is None or attacker.ability() != Ability.INFILTRATOR):
|
|
return f"{self.pokemon.name}'s safeguard protects it from being inflicted with {status}!\n"
|
|
if self.pokemon.grounded(battle, attacker=attacker, move=move) and battle.terrain.item == "misty":
|
|
return f"The misty terrain protects {self.pokemon.name} from being inflicted with {status}!\n"
|
|
if self.pokemon.ability(attacker=attacker, move=move) == Ability.FLOWER_VEIL and ElementType.GRASS in self.pokemon.type_ids:
|
|
return f"{self.pokemon.name}'s flower veil protects it from being inflicted with {status}!\n"
|
|
if self.pokemon._name == "Minior":
|
|
return "Minior's hard shell protects it from status effects!\n"
|
|
if status == "burn":
|
|
if ElementType.FIRE in self.pokemon.type_ids:
|
|
return f"{self.pokemon.name} is a fire type and can't be burned!\n"
|
|
if self.pokemon.ability(attacker=attacker, move=move) in (Ability.WATER_VEIL, Ability.WATER_BUBBLE):
|
|
ability_name = Ability(self.pokemon.ability_id).pretty_name
|
|
return f"{self.pokemon.name}'s {ability_name} prevents it from getting burned!\n"
|
|
self.current = status
|
|
msg += f"{self.pokemon.name} was burned{source}!\n"
|
|
if status == "sleep":
|
|
if self.pokemon.ability(attacker=attacker, move=move) in (Ability.INSOMNIA, Ability.VITAL_SPIRIT, Ability.SWEET_VEIL):
|
|
ability_name = Ability(self.pokemon.ability_id).pretty_name
|
|
return f"{self.pokemon.name}'s {ability_name} keeps it awake!\n"
|
|
if self.pokemon.grounded(battle, attacker=attacker, move=move) and battle.terrain.item == "electric":
|
|
return f"The terrain is too electric for {self.pokemon.name} to fall asleep!\n"
|
|
if battle.trainer1.current_pokemon and battle.trainer1.current_pokemon.uproar.active():
|
|
return f"An uproar keeps {self.pokemon.name} from falling asleep!\n"
|
|
if battle.trainer2.current_pokemon and battle.trainer2.current_pokemon.uproar.active():
|
|
return f"An uproar keeps {self.pokemon.name} from falling asleep!\n"
|
|
if turns is None:
|
|
turns = random.randint(2, 4)
|
|
if self.pokemon.ability(attacker=attacker, move=move) == Ability.EARLY_BIRD:
|
|
turns //= 2
|
|
self.current = status
|
|
self.sleep_timer.set_turns(turns)
|
|
msg += f"{self.pokemon.name} fell asleep{source}!\n"
|
|
if status in ("poison", "b-poison"):
|
|
if attacker is None or attacker.ability() != Ability.CORROSION:
|
|
if ElementType.STEEL in self.pokemon.type_ids:
|
|
return f"{self.pokemon.name} is a steel type and can't be poisoned!\n"
|
|
if ElementType.POISON in self.pokemon.type_ids:
|
|
return f"{self.pokemon.name} is a poison type and can't be poisoned!\n"
|
|
if self.pokemon.ability(attacker=attacker, move=move) in (Ability.IMMUNITY, Ability.PASTEL_VEIL):
|
|
ability_name = Ability(self.pokemon.ability_id).pretty_name
|
|
return f"{self.pokemon.name}'s {ability_name} keeps it from being poisoned!\n"
|
|
self.current = status
|
|
bad = " badly" if status == "b-poison" else ""
|
|
msg += f"{self.pokemon.name} was{bad} poisoned{source}!\n"
|
|
|
|
if move is not None and attacker is not None and attacker.ability() == Ability.POISON_PUPPETEER:
|
|
msg += self.pokemon.confuse(attacker=attacker, source=f"{attacker.name}'s poison puppeteer")
|
|
if status == "paralysis":
|
|
if ElementType.ELECTRIC in self.pokemon.type_ids:
|
|
return f"{self.pokemon.name} is an electric type and can't be paralyzed!\n"
|
|
if self.pokemon.ability(attacker=attacker, move=move) == Ability.LIMBER:
|
|
return f"{self.pokemon.name}'s limber keeps it from being paralyzed!\n"
|
|
self.current = status
|
|
msg += f"{self.pokemon.name} was paralyzed{source}!\n"
|
|
if status == "freeze":
|
|
if ElementType.ICE in self.pokemon.type_ids:
|
|
return f"{self.pokemon.name} is an ice type and can't be frozen!\n"
|
|
if self.pokemon.ability(attacker=attacker, move=move) == Ability.MAGMA_ARMOR:
|
|
return f"{self.pokemon.name}'s magma armor keeps it from being frozen!\n"
|
|
if battle.weather.get() in ("sun", "h-sun"):
|
|
return f"It's too sunny to freeze {self.pokemon.name}!\n"
|
|
self.current = status
|
|
msg += f"{self.pokemon.name} was frozen solid{source}!\n"
|
|
|
|
if self.pokemon.ability(attacker=attacker, move=move) == Ability.SYNCHRONIZE and attacker is not None:
|
|
msg += attacker.nv.apply_status(status, battle, attacker=self.pokemon, source=f"{self.pokemon.name}'s synchronize")
|
|
|
|
if self.pokemon.held_item.should_eat_berry_status(attacker):
|
|
msg += self.pokemon.held_item.eat_berry(attacker=attacker, move=move)
|
|
|
|
return msg
|
|
|
|
def reset(self):
|
|
"""Remove a non volatile status from a pokemon."""
|
|
self.current = ""
|
|
self.badly_poisoned_turn = 0
|
|
self.sleep_timer.set_turns(0)
|
|
self.pokemon.nightmare = False
|
|
|
|
|
|
class Metronome():
|
|
"""Holds recent move status for the held item metronome."""
|
|
def __init__(self):
|
|
self.move = ""
|
|
self.count = 0
|
|
|
|
def reset(self):
|
|
"""A move failed or a non-move action was done."""
|
|
self.move = ""
|
|
self.count = 0
|
|
|
|
def use(self, movename):
|
|
"""Updates the metronome based on a used move."""
|
|
if self.move == movename:
|
|
self.count += 1
|
|
else:
|
|
self.move = movename
|
|
self.count = 1
|
|
|
|
def get_buff(self, movename):
|
|
"""Get the buff multiplier for this metronome."""
|
|
if self.move != movename:
|
|
return 1
|
|
return min(2, 1 + (.2 * self.count))
|
|
|
|
|
|
class Item():
|
|
"""Stores information about an item."""
|
|
def __init__(self, item_data):
|
|
self.name = item_data["identifier"]
|
|
self.id = item_data["id"]
|
|
self.power = item_data["fling_power"]
|
|
self.effect = item_data["fling_effect_id"]
|
|
|
|
class HeldItem():
|
|
"""Stores information about the current held item for a particualar poke."""
|
|
def __init__(self, item_data, owner):
|
|
if item_data is None:
|
|
self.item = None
|
|
else:
|
|
self.item = Item(item_data)
|
|
self.owner = owner
|
|
self.battle = None
|
|
self.last_used = None
|
|
self.ever_had_item = self.item is not None
|
|
|
|
def get(self):
|
|
"""Get the current held item identifier."""
|
|
if self.item is None:
|
|
return None
|
|
if not self.can_remove():
|
|
return self.item.name
|
|
if self.owner.embargo.active():
|
|
return None
|
|
if self.battle and self.battle.magic_room.active():
|
|
return None
|
|
if self.owner.ability() == Ability.KLUTZ:
|
|
return None
|
|
if self.owner.corrosive_gas:
|
|
return None
|
|
return self.item.name
|
|
|
|
def has_item(self):
|
|
"""Helper method to prevent attempting to acquire a new item if the poke already has one."""
|
|
return self.item is not None
|
|
|
|
def can_remove(self):
|
|
"""Returns a boolean indicating whether this held item can be removed."""
|
|
return self.name not in (
|
|
# Plates
|
|
"draco-plate", "dread-plate", "earth-plate", "fist-plate", "flame-plate", "icicle-plate",
|
|
"insect-plate", "iron-plate", "meadow-plate", "mind-plate", "pixie-plate", "sky-plate",
|
|
"splash-plate", "spooky-plate", "stone-plate", "toxic-plate", "zap-plate",
|
|
# Memories
|
|
"dragon-memory", "dark-memory", "ground-memory", "fighting-memory", "fire-memory",
|
|
"ice-memory", "bug-memory", "steel-memory", "grass-memory", "psychic-memory",
|
|
"fairy-memory", "flying-memory", "water-memory", "ghost-memory", "rock-memory",
|
|
"poison-memory", "electric-memory",
|
|
# Misc
|
|
"primal-orb", "griseous-orb", "blue-orb", "red-orb", "rusty-sword", "rusty-shield",
|
|
# Mega Stones
|
|
"mega-stone", "mega-stone-x", "mega-stone-y",
|
|
)
|
|
|
|
def is_berry(self, *, only_active=True):
|
|
"""
|
|
Returns a boolean indicating whether this held item is a berry.
|
|
|
|
The optional param only_active determines if this method should only return True if the berry is active and usable.
|
|
"""
|
|
if only_active:
|
|
return self.get() is not None and self.get().endswith("-berry")
|
|
return self.name is not None and self.name.endswith("-berry")
|
|
|
|
def remove(self):
|
|
"""Remove this held item, setting it to None."""
|
|
if not self.can_remove():
|
|
raise ValueError(f"{self.name} cannot be removed.")
|
|
self.item = None
|
|
|
|
def use(self):
|
|
"""Uses this item, setting it to None but also recording that it was used."""
|
|
if not self.can_remove():
|
|
raise ValueError(f"{self.name} cannot be removed.")
|
|
self.last_used = self.item
|
|
self.owner.choice_move = None
|
|
self.remove()
|
|
|
|
def transfer(self, other):
|
|
"""Transfer the data of this held item to other, and clear this item."""
|
|
if not self.can_remove():
|
|
raise ValueError(f"{self.name} cannot be removed.")
|
|
if not other.can_remove():
|
|
raise ValueError(f"{other.name} cannot be removed.")
|
|
other.item = self.item
|
|
self.remove()
|
|
|
|
def swap(self, other):
|
|
"""Swap the date between this held item and other."""
|
|
if not self.can_remove():
|
|
raise ValueError(f"{self.name} cannot be removed.")
|
|
if not other.can_remove():
|
|
raise ValueError(f"{other.name} cannot be removed.")
|
|
self.item, other.item = other.item, self.item
|
|
self.owner.choice_move = None
|
|
other.owner.choice_move = None
|
|
self.ever_had_item = self.ever_had_item or self.item is not None
|
|
|
|
def recover(self, other):
|
|
"""Recover & claim the last_used item from other."""
|
|
self.item = other.last_used
|
|
other.last_used = None
|
|
self.ever_had_item = self.ever_had_item or self.item is not None
|
|
|
|
def _should_eat_berry_util(self, otherpoke=None):
|
|
"""Util for all the things that are shared between the different kinds of berry."""
|
|
if self.owner.hp == 0:
|
|
return False
|
|
if otherpoke is not None and otherpoke.ability() in (Ability.UNNERVE, Ability.AS_ONE_SHADOW, Ability.AS_ONE_ICE): #TODO: idk make this check better...
|
|
return False
|
|
if not self.is_berry():
|
|
return False
|
|
return True
|
|
|
|
def should_eat_berry_damage(self, otherpoke=None):
|
|
"""Returns True if the pokemon meets the criteria to eat its held berry after being damaged."""
|
|
if not self._should_eat_berry_util(otherpoke):
|
|
return False
|
|
if self.owner.hp <= self.owner.starting_hp / 4:
|
|
if self in (
|
|
# HP berries
|
|
"figy-berry", "wiki-berry", "mago-berry", "aguav-berry", "iapapa-berry",
|
|
# Stat berries
|
|
"apicot-berry", "ganlon-berry", "lansat-berry", "liechi-berry", "micle-berry", "petaya-berry", "salac-berry", "starf-berry",
|
|
):
|
|
return True
|
|
if self.owner.hp <= self.owner.starting_hp / 2:
|
|
if self.owner.ability() == Ability.GLUTTONY:
|
|
return True
|
|
if self == "sitrus-berry":
|
|
return True
|
|
return False
|
|
|
|
def should_eat_berry_status(self, otherpoke=None):
|
|
"""Returns True if the pokemon meets the criteria to eat its held berry after getting a status."""
|
|
if not self._should_eat_berry_util(otherpoke):
|
|
return False
|
|
if self in ("aspear-berry", "lum-berry") and self.owner.nv.freeze():
|
|
return True
|
|
if self in ("cheri-berry", "lum-berry") and self.owner.nv.paralysis():
|
|
return True
|
|
if self in ("chesto-berry", "lum-berry") and self.owner.nv.sleep():
|
|
return True
|
|
if self in ("pecha-berry", "lum-berry") and self.owner.nv.poison():
|
|
return True
|
|
if self in ("rawst-berry", "lum-berry") and self.owner.nv.burn():
|
|
return True
|
|
if self in ("persim-berry", "lum-berry") and self.owner.confusion.active():
|
|
return True
|
|
return False
|
|
|
|
def should_eat_berry(self, otherpoke=None):
|
|
"""Returns True if the pokemon meets the criteria to eat its held berry."""
|
|
return self.should_eat_berry_damage(otherpoke) or self.should_eat_berry_status(otherpoke)
|
|
|
|
def eat_berry(self, *, consumer=None, attacker=None, move=None):
|
|
"""
|
|
Eat this held item berry.
|
|
|
|
Returns a formatted message.
|
|
"""
|
|
msg = ""
|
|
if not self.is_berry():
|
|
return ""
|
|
if consumer is None:
|
|
consumer = self.owner
|
|
else:
|
|
msg += f"{consumer.name} eats {self.owner.name}'s berry!\n"
|
|
|
|
# 2x or 1x
|
|
ripe = int(consumer.ability(attacker=attacker, move=move) == Ability.RIPEN) + 1
|
|
flavor = None
|
|
|
|
if self == "sitrus-berry":
|
|
msg += consumer.heal((ripe * consumer.starting_hp) // 4, source="eating its berry")
|
|
elif self == "figy-berry":
|
|
msg += consumer.heal((ripe * consumer.starting_hp) // 3, source="eating its berry")
|
|
flavor = "spicy"
|
|
elif self == "wiki-berry":
|
|
msg += consumer.heal((ripe * consumer.starting_hp) // 3, source="eating its berry")
|
|
flavor = "dry"
|
|
elif self == "mago-berry":
|
|
msg += consumer.heal((ripe * consumer.starting_hp) // 3, source="eating its berry")
|
|
flavor = "sweet"
|
|
elif self == "aguav-berry":
|
|
msg += consumer.heal((ripe * consumer.starting_hp) // 3, source="eating its berry")
|
|
flavor = "bitter"
|
|
elif self == "iapapa-berry":
|
|
msg += consumer.heal((ripe * consumer.starting_hp) // 3, source="eating its berry")
|
|
flavor = "sour"
|
|
elif self == "apicot-berry":
|
|
msg += consumer.append_spdef(ripe * 1, attacker=attacker, move=move, source="eating its berry")
|
|
elif self == "ganlon-berry":
|
|
msg += consumer.append_defense(ripe * 1, attacker=attacker, move=move, source="eating its berry")
|
|
elif self == "lansat-berry":
|
|
consumer.lansat_berry_ate = True
|
|
msg += f"{consumer.name} is powered up by eating its berry.\n"
|
|
elif self == "liechi-berry":
|
|
msg += consumer.append_attack(ripe * 1, attacker=attacker, move=move, source="eating its berry")
|
|
elif self == "micle-berry":
|
|
consumer.micle_berry_ate = True
|
|
msg += f"{consumer.name} is powered up by eating its berry.\n"
|
|
elif self == "petaya-berry":
|
|
msg += consumer.append_spatk(ripe * 1, attacker=attacker, move=move, source="eating its berry")
|
|
elif self == "salac-berry":
|
|
msg += consumer.append_speed(ripe * 1, attacker=attacker, move=move, source="eating its berry")
|
|
elif self == "starf-berry":
|
|
funcs = [
|
|
consumer.append_attack,
|
|
consumer.append_defense,
|
|
consumer.append_spatk,
|
|
consumer.append_spdef,
|
|
consumer.append_speed,
|
|
]
|
|
func = random.choice(funcs)
|
|
msg += func(ripe * 2, attacker=attacker, move=move, source="eating its berry")
|
|
elif self == "aspear-berry":
|
|
if consumer.nv.freeze():
|
|
consumer.nv.reset()
|
|
msg += f"{consumer.name} is no longer frozen after eating its berry!\n"
|
|
else:
|
|
msg += f"{consumer.name}'s berry had no effect!\n"
|
|
elif self == "cheri-berry":
|
|
if consumer.nv.paralysis():
|
|
consumer.nv.reset()
|
|
msg += f"{consumer.name} is no longer paralyzed after eating its berry!\n"
|
|
else:
|
|
msg += f"{consumer.name}'s berry had no effect!\n"
|
|
elif self == "chesto-berry":
|
|
if consumer.nv.sleep():
|
|
consumer.nv.reset()
|
|
msg += f"{consumer.name} woke up after eating its berry!\n"
|
|
else:
|
|
msg += f"{consumer.name}'s berry had no effect!\n"
|
|
elif self == "pecha-berry":
|
|
if consumer.nv.poison():
|
|
consumer.nv.reset()
|
|
msg += f"{consumer.name} is no longer poisoned after eating its berry!\n"
|
|
else:
|
|
msg += f"{consumer.name}'s berry had no effect!\n"
|
|
elif self == "rawst-berry":
|
|
if consumer.nv.burn():
|
|
consumer.nv.reset()
|
|
msg += f"{consumer.name} is no longer burned after eating its berry!\n"
|
|
else:
|
|
msg += f"{consumer.name}'s berry had no effect!\n"
|
|
elif self == "persim-berry":
|
|
if consumer.confusion.active():
|
|
consumer.confusion.set_turns(0)
|
|
msg += f"{consumer.name} is no longer confused after eating its berry!\n"
|
|
else:
|
|
msg += f"{consumer.name}'s berry had no effect!\n"
|
|
elif self == "lum-berry":
|
|
consumer.nv.reset()
|
|
consumer.confusion.set_turns(0)
|
|
msg += f"{consumer.name}'s statuses were cleared from eating its berry!\n"
|
|
|
|
if flavor is not None and consumer.disliked_flavor == flavor:
|
|
msg += consumer.confuse(attacker=attacker, move=move, source="disliking its berry's flavor")
|
|
if consumer.ability(attacker=attacker, move=move) == Ability.CHEEK_POUCH:
|
|
msg += consumer.heal(consumer.starting_hp // 3, source="its cheek pouch")
|
|
|
|
consumer.last_berry = self.item
|
|
consumer.ate_berry = True
|
|
# TODO: right now HeldItem does not support `recover`ing/setting from anything other than another HeldItem object.
|
|
# this should probably be modified to be an `ExpiringItem` w/ that item for cases where `last_item` gets reset.
|
|
if consumer.ability(attacker=attacker, move=move) == Ability.CUD_CHEW:
|
|
consumer.cud_chew.set_turns(2)
|
|
if consumer is self.owner:
|
|
self.use()
|
|
else:
|
|
self.remove()
|
|
|
|
return msg
|
|
|
|
def __eq__(self, other):
|
|
return self.get() == other
|
|
|
|
def __getattr__(self, attr):
|
|
if attr not in ("name", "power", "id", "effect"):
|
|
raise AttributeError(f"{attr} is not an attribute of {self.__class__.__name__}.")
|
|
if self.item is None:
|
|
return None
|
|
if attr == "name":
|
|
return self.item.name
|
|
if attr == "power":
|
|
return self.item.power
|
|
if attr == "id":
|
|
return self.item.id
|
|
if attr == "effect":
|
|
return self.item.effect
|
|
raise AttributeError(f"{attr} is not an attribute of {self.__class__.__name__}.")
|
|
|
|
class BatonPass():
|
|
"""Stores the necessary data from a pokemon to baton pass to another pokemon."""
|
|
def __init__(self, poke):
|
|
self.attack_stage = poke.attack_stage
|
|
self.defense_stage = poke.defense_stage
|
|
self.spatk_stage = poke.spatk_stage
|
|
self.spdef_stage = poke.spdef_stage
|
|
self.speed_stage = poke.speed_stage
|
|
self.evasion_stage = poke.evasion_stage
|
|
self.accuracy_stage = poke.accuracy_stage
|
|
self.confusion = poke.confusion
|
|
self.focus_energy = poke.focus_energy
|
|
self.mind_reader = poke.mind_reader
|
|
self.leech_seed = poke.leech_seed
|
|
self.curse = poke.curse
|
|
self.substitute = poke.substitute
|
|
self.ingrain = poke.ingrain
|
|
self.power_trick = poke.power_trick
|
|
self.power_shift = poke.power_shift
|
|
self.heal_block = poke.heal_block
|
|
self.embargo = poke.embargo
|
|
self.perish_song = poke.perish_song
|
|
self.magnet_rise = poke.magnet_rise
|
|
self.aqua_ring = poke.aqua_ring
|
|
self.telekinesis = poke.telekinesis
|
|
|
|
def apply(self, poke):
|
|
"""Push this objects data to a poke."""
|
|
if poke.ability() != Ability.CURIOUS_MEDICINE:
|
|
poke.attack_stage = self.attack_stage
|
|
poke.defense_stage = self.defense_stage
|
|
poke.spatk_stage = self.spatk_stage
|
|
poke.spdef_stage = self.spdef_stage
|
|
poke.speed_stage = self.speed_stage
|
|
poke.evasion_stage = self.evasion_stage
|
|
poke.accuracy_stage = self.accuracy_stage
|
|
poke.confusion = self.confusion
|
|
poke.focus_energy = self.focus_energy
|
|
poke.mind_reader = self.mind_reader
|
|
poke.leech_seed = self.leech_seed
|
|
poke.curse = self.curse
|
|
poke.substitute = self.substitute
|
|
poke.ingrain = self.ingrain
|
|
poke.power_trick = self.power_trick
|
|
poke.power_shift = self.power_shift
|
|
poke.heal_block = self.heal_block
|
|
poke.embargo = self.embargo
|
|
poke.perish_song = self.perish_song
|
|
poke.magnet_rise = self.magnet_rise
|
|
poke.aqua_ring = self.aqua_ring
|
|
poke.telekinesis = self.telekinesis
|