Ruby-Cogs/pokemonduel/pokemon.py
2025-04-02 22:57:51 -04:00

2666 lines
130 KiB
Python

import random
from .data import find, find_one
from .enums import Ability, DamageClass, ElementType
from .misc import NonVolatileEffect, Metronome, ExpiringEffect, ExpiringItem, HeldItem
from .move import Move
class DuelPokemon():
"""
An instance of a pokemon in a duel.
Contains extra duel-specific information.
"""
def __init__(self, **kwargs):
#ID from pfile
self.pokemon_id = kwargs['pokemon_id']
self._name = kwargs['name']
self._nickname = kwargs['nickname']
if self._nickname != "None":
self.name = f"{self._nickname} ({self._name.replace('-', ' ')})"
else:
self.name = self._name.replace("-", " ")
self.illusion__name = None
self.illusion_name = None
# Dict {pokname: List[int]}
self.base_stats = kwargs['base_stats']
self.hp = kwargs['hp']
self.attack = self.base_stats[self._name][1]
self.defense = self.base_stats[self._name][2]
self.spatk = self.base_stats[self._name][3]
self.spdef = self.base_stats[self._name][4]
self.speed = self.base_stats[self._name][5]
self.hpiv = min(31, kwargs['hpiv'])
self.atkiv = min(31, kwargs['atkiv'])
self.defiv = min(31, kwargs['defiv'])
self.spatkiv = min(31, kwargs['spatkiv'])
self.spdefiv = min(31, kwargs['spdefiv'])
self.speediv = min(31, kwargs['speediv'])
self.hpev = kwargs['hpev']
self.atkev = kwargs['atkev']
self.defev = kwargs['defev']
self.spatkev = kwargs['spatkev']
self.spdefev = kwargs['spdefev']
self.speedev = kwargs['speedev']
self.nature_stat_deltas = kwargs['nature_stat_deltas']
self.moves = kwargs['moves']
self.ability_id = kwargs['ability_id']
self.mega_ability_id = kwargs['mega_ability_id']
self.type_ids = kwargs['type_ids']
self.mega_type_ids = kwargs['mega_type_ids']
self._starting_name = self._name
self.starting_hp = self.hp
self.starting_hpiv = self.hpiv
self.starting_atkiv = self.atkiv
self.starting_defiv = self.defiv
self.starting_spatkiv = self.spatkiv
self.starting_spdefiv = self.spdefiv
self.starting_speediv = self.speediv
self.starting_hpev = self.hpev
self.starting_atkev = self.atkev
self.starting_defev = self.defev
self.starting_spatkev = self.spatkev
self.starting_spdefev = self.spdefev
self.starting_speedev = self.speedev
self.starting_moves = self.moves.copy() #shallow copy to keep the objects but not the list itself
self.starting_ability_id = self.ability_id
self.starting_type_ids = self.type_ids.copy()
#10 "weight" = 1 kg
self.starting_weight = max(1, kwargs['weight'])
self.attack_stage = 0
self.defense_stage = 0
self.spatk_stage = 0
self.spdef_stage = 0
self.speed_stage = 0
self.accuracy_stage = 0
self.evasion_stage = 0
self.level = kwargs['level']
self.shiny = kwargs['shiny']
self.radiant = kwargs['radiant']
self.skin = kwargs['skin']
self.id = kwargs['id']
self.held_item = HeldItem(kwargs['held_item'], self)
self.happiness = kwargs['happiness']
self.gender = kwargs['gender']
self.can_still_evolve = kwargs['can_still_evolve']
self.disliked_flavor = kwargs['disliked_flavor']
self.owner = None
self.active_turns = 0
self.cursed = False
self.switched = False
self.minimized = False
self.nv = NonVolatileEffect(self)
self.metronome = Metronome()
self.leech_seed = False
self.stockpile = 0
self.flinched = False
self.confusion = ExpiringEffect(0)
self.can_move = True
self.has_moved = False
#Boolean - stores whether this poke swapped in this turn, and should not have the next_turn function be called.
self.swapped_in = False
#Boolean - stores whether this poke has ever been sent in.
self.ever_sent_out = False
#Boolean - stores whether this poke should attempt to mega evolve this turn.
self.should_mega_evolve = False
# Moves
#Optional[Move] - stores the last move used by this poke
self.last_move = None
#Optional[Tuple[int, DamageClass]] - stores the damage taken and the class of that damage.
#Resets at the end of a turn.
self.last_move_damage = None
#Boolean - stores whether or not the last move used by this poke failed.
self.last_move_failed = False
#Optional[LockedMove] - stores the move this poke is currently locked into using due to it being a multi turn move.
self.locked_move = None
#Optional[Move] - stores the move this poke is currently locked into using due to a choice item.
self.choice_move = None
#ExpiringItem - stores the number of turns a specific move is disabled.
self.disable = ExpiringItem()
#ExpiringEffect - stores the number of turns this poke is taunted.
self.taunt = ExpiringEffect(0)
#ExpiringItem - stores the number of turns this poke is giving an encore of a specific move.
self.encore = ExpiringItem()
#Boolean - stores whether or not this pokemon is affected by torment.
self.torment = False
#Boolean - stores whether or not this pokemon has imprison active.
self.imprison = False
#ExpiringEffect - stores the number of turns this poke is blocked from using healing moves.
self.heal_block = ExpiringEffect(0)
#Optional[int] - stores the damage takes since bide was started.
self.bide = None
#Boolean - stores whether or not this pokemon has used focus energy.
self.focus_energy = False
#ExpiringEffect - stores the number of turns until this pokemon faints.
self.perish_song = ExpiringEffect(0)
#Boolean - stores whether or not this pokemon is inflicted by nightmare.
self.nightmare = False
#Boolean - stores whether or not this pokemon has used defense curl since entering the field.
self.defense_curl = False
#Int - stores the number of times fury cutter has been used since entering the field.
self.fury_cutter = 0
#ExpiringEffect - stores the number of turns bind is active on this poke.
self.bind = ExpiringEffect(0)
#Int - stores the remaining HP of this pokemon's substitute, or 0 if there is no substitute.
self.substitute = 0
#ExpiringEffect - stores the number of turns this pokemon is silenced for (can't use sound based moves).
self.silenced = ExpiringEffect(0)
#Boolean - stores whether or not this poke is in rage, and gains attack stages after being hit.
self.rage = False
#ExpiringItem - stores whether or not mind reader is active ON this poke, and the poke which is reading its mind.
self.mind_reader = ExpiringItem()
#Boolean - stores whether any attacker that causes this poke to faint will also faint this turn.
self.destiny_bond = False
#ExpiringEffect - stores whether destiny bond is on cooldown and cannot be executed.
self.destiny_bond_cooldown = ExpiringEffect(0)
#Boolean - stores whether or not this poke is prevented from switching out. This does not prevent moves that swap the target or user.
self.trapping = False
#Boolean - stores whether or not this poke is under the effect of ingrain. This does not prevent moves that swap the user, but does prevent target swaps.
self.ingrain = False
#Optional[DuelPokemon] - stores the pokemon that this pokemon is infatuated with.
self.infatuated = None
#Boolean - stores whether or not this poke is under the effect of aqua ring.
self.aqua_ring = False
#ExpiringEffect - stores the number of turns this pokemon is ungrounded due to magnet rise.
self.magnet_rise = ExpiringEffect(0)
#Boolean - stores whether or not this poke is in a semi-invulnerable state from dive.
self.dive = False
#Boolean - stores whether or not this poke is in a semi-invulnerable state from dig.
self.dig = False
#Boolean - stores whether or not this poke is in a semi-invulnerable state from bounce or fly.
self.fly = False
#Boolean - stores whether or not this poke is in a semi-invulnerable state from shadow force or phantom force.
self.shadow_force = False
#ExpiringEffect - stores how long this poke cannot be hit by critical hits due to lucky chant.
self.lucky_chant = ExpiringEffect(0)
#Boolean - stores whether or not this poke has been hit by smack down or thousand arrows, and is grounded.
self.grounded_by_move = False
#ExpiringEffect - stores the number of turns electric type moves have double power from this poke.
self.charge = ExpiringEffect(0)
#ExpiringEffect - stores the number of turns pokes cannot fall asleep.
self.uproar = ExpiringEffect(0)
#Boolean - stores whether or not this poke reflects status moves due to magic coat.
self.magic_coat = False
#Boolean - stores whether or not this poke has used power trick.
self.power_trick = False
#Boolean - stores whether or not this poke has used power shift.
self.power_shift = False
#ExpiringEffect - stores the number of turns until this poke falls asleep due to drowsiness.
self.yawn = ExpiringEffect(0)
#Boolean - stores whether this poke is turning normal moves to electric this turn.
self.ion_deluge = False
#Boolean - stores whether this poke's move is changed to electric type due to electrify.
self.electrify = False
#Boolean - stores whether this poke used a protection move this turn.
self.protection_used = False
#Int - stores the current chance (1/x) that a poke can use a protection move.
self.protection_chance = 1
#Boolean - stores whether this poke is protected by protect this turn.
self.protect = False
#Boolean - stores whether this poke is protected by endure this turn.
self.endure = False
#Boolean - stores whether this poke is protected by wide guard this turn.
self.wide_guard = False
#Boolean - stores whether this poke is protected by crafty shield this turn.
self.crafty_shield = False
#Boolean - stores whether this poke is protected by king shield this turn.
self.king_shield = False
#Boolean - stores whether this poke is protected by spiky shield this turn.
self.spiky_shield = False
#Boolean - stores whether this poke is protected by mat block this turn.
self.mat_block = False
#Boolean - stores whether this poke is protected by baneful bunker this turn.
self.baneful_bunker = False
#Boolean - stores whether this poke is protected by quick guard this turn.
self.quick_guard = False
#Boolean - stores whether this poke is protected by obstruct this turn.
self.obstruct = False
#Boolean - stores whether this poke is protected by silk trap this turn.
self.silk_trap = False
#Boolean - stores whether this poke is protected by burning bulwark this turn.
self.burning_bulwark = False
#ExpiringEffect - stores whether this poke will always crit due to laser focus.
self.laser_focus = ExpiringEffect(0)
#Boolean - stores whether this poke is coated with powder and will explode if it uses a fire type move.
self.powdered = False
#Boolean - stores whether this poke will steal the effect of certain self targeted moves this turn using snatch.
self.snatching = False
#ExpiringEffect - stores the number of turns this poke is ungrounded and hits to it never miss due to telekinesis.
self.telekinesis = ExpiringEffect(0)
#ExpiringEffect - stores the number of turns this poke is blocked from using held items due to embargo.
self.embargo = ExpiringEffect(0)
#Int - stores the current power of echoed voice.
self.echoed_voice_power = 40
#Boolean - stores whether echoed voice was used this turn.
self.echoed_voice_used = False
#Boolean - stores whether this poke is inflicted with a curse.
self.curse = False
#ExpiringEffect - stores whether this poke is preventing swap outs this turn.
self.fairy_lock = ExpiringEffect(0)
#Boolean - stores whether this poke remove all PP from a move that kills it this turn.
self.grudge = False
#Boolean - stores whether this poke is affected by foresight.
self.foresight = False
#Boolean - stores whether this poke is affected by miracle_eye.
self.miracle_eye = False
#Boolean - stores whether this poke is currently charging beak blast.
self.beak_blast = False
#Boolean - stores whether this poke is unable to switch out from no retreat.
self.no_retreat = False
#Boolean - stores whether this poke has taken ANY damage this turn.
self.dmg_this_turn = False
#Boolean - stores whether this poke has eaten a berry this battle.
self.ate_berry = False
#Boolean - stores whether this poke is unable to use its held item for the rest of the battle due to corrosive gas.
self.corrosive_gas = False
#Boolean - stores whether this poke has had a stat increase this turn.
self.stat_increased = False
#Boolean - stores whether this poke has had a stat decrease this turn.
self.stat_decreased = False
#Boolean - stores whether this poke has had its flying type surpressed this turn.
self.roost = False
#Boolean - stores whether this poke is affected by octolock, and has its def/spdef lowered each turn.
self.octolock = False
#Optional[Int] - stores the raw attack value this poke's attack is split with.
self.attack_split = None
#Optional[Int] - stores the raw special attack value this poke's special attack is split with.
self.spatk_split = None
#Optional[Int] - stores the raw defense value this poke's defense is split with.
self.defense_split = None
#Optional[Int] - stores the raw special defense value this poke's special defense is split with.
self.spdef_split = None
#Int - stores the number of times autotomize has been used since switching out.
self.autotomize = 0
#Boolean - stores whether or not this pokemon's critical hit ratio is increased from eating a lansat berry.
self.lansat_berry_ate = False
#Boolean - stores whether or not this pokemon's next move has increased accuracy from eating a micle berry.
self.micle_berry_ate = False
#Boolean - stores whether or not fire type moves are 2x effective on this pokemon from tar shot.
self.tar_shot = False
#ExpiringEffect - stores the number of turns this pokemon is lowering the opponent's speed at the end of every turn.
self.syrup_bomb = ExpiringEffect(0)
#Int - stores the number of times this pokemon has been hit this battle, and does NOT reset when switching out.
self.num_hits = 0
# Abilities
#Boolean - stores whether this poke's fire moves are boosted by flash fire.
self.flash_fire = False
#Int - stores the turn number relative to getting truant, % 2 == 1 -> loaf around.
self.truant_turn = 0
#Boolean - stores whether the ice face of this pokemon has been repaired.
self.ice_repaired = False
#Optional[Item] - stores the last berry ate by this pokemon.
self.last_berry = None
#ExpiringEffect - stores the number of turns until this pokemon attempts to recover & eat their last eaten berry.
self.cud_chew = ExpiringEffect(0)
#Boolean - stores whether booster_energy has been consumed this send out to activate the effects of Protosynthesis / Quark Drive.
self.booster_energy = False
#Boolean - stores whether supersweet_syrup has been consumed to lower the evasion of its opponent. NOT reset on switch out.
self.supersweet_syrup = False
def send_out(self, otherpoke, battle):
"""
Initalize a poke upon first sending it out.
otherpoke may be None, if two pokes are sent out at the same time and the first is killed in the send out process.
Returns a formatted message.
"""
self.ever_sent_out = True
# Emergency exit `remove`s the pokemon *in the middle of the turn* in a somewhat unsafe way.
# `remove` may need to be called here, but that seems like it may have side effects.
self.flinched = False
# This has to go BEFORE the send out message, and not in send_out_ability as it only
# applies on send out, not when abilities are changed, and it changes the send out msg.
illusion_options = [x for x in self.owner.party if x is not self and x.hp > 0]
if self.ability() == Ability.ILLUSION and illusion_options:
self.illusion__name = self._name
self.illusion_name = self.name
self._name = illusion_options[-1]._name
self.name = illusion_options[-1].name
if self._name == "Pikachu":
msg = f"{self.name}, I choose you!\n"
else:
msg = f"{self.owner.name} sent out {self.name}!\n"
# Any time a poke switches out, certain effects it had put on its opponent end
if otherpoke is not None:
otherpoke.trapping = False
otherpoke.octolock = False
otherpoke.bind.set_turns(0)
#Baton Pass
if self.owner.baton_pass is not None:
msg += f"{self.name} carries on the baton!\n"
self.owner.baton_pass.apply(self)
self.owner.baton_pass = None
#Shed Tail
if self.owner.next_substitute:
self.substitute = self.owner.next_substitute
self.owner.next_substitute = 0
#Entry hazards
#Special case for clearing toxic spikes, still happens even with heavy duty boots
if self.owner.toxic_spikes and self.grounded(battle) and ElementType.POISON in self.type_ids:
self.owner.toxic_spikes = 0
msg += f"{self.name} absorbed the toxic spikes!\n"
if self.held_item != "heavy-duty-boots":
#Grounded entry hazards
if self.grounded(battle):
#Spikes
if self.owner.spikes:
#1/8 -> 1/4
damage = self.starting_hp // (10 - (2 * self.owner.spikes))
msg += self.damage(damage, battle, source="spikes")
#Toxic spikes
if self.owner.toxic_spikes == 1:
msg += self.nv.apply_status("poison", battle, source="toxic spikes")
elif self.owner.toxic_spikes == 2:
msg += self.nv.apply_status("b-poison", battle, source="toxic spikes")
#Sticky web
if self.owner.sticky_web:
msg += self.append_speed(-1, source="the sticky web")
#Non-grounded entry hazards
if self.owner.stealth_rock:
effective = self.effectiveness(ElementType.ROCK, battle)
if effective:
# damage = 1/8 max hp * effectiveness
damage = self.starting_hp // (32 // (4 * effective))
msg += self.damage(int(damage), battle, source="stealth rock")
if self.hp > 0:
msg += self.send_out_ability(otherpoke, battle)
#Restoration
if self.owner.healing_wish:
used = False
if self.hp != self.starting_hp:
used = True
self.hp = self.starting_hp
if self.nv.current:
used = True
self.nv.reset()
if used:
self.owner.healing_wish = False
msg += f"{self.name} was restored by healing wish!\n"
if self.owner.lunar_dance:
used = False
if self.hp != self.starting_hp:
used = True
self.hp = self.starting_hp
if self.nv.current:
used = True
self.nv.reset()
not_at_full_pp = [move for move in self.moves if move.pp != move.starting_pp]
if not_at_full_pp:
used = True
for move in not_at_full_pp:
move.pp = move.starting_pp
if used:
self.owner.lunar_dance = False
msg += f"{self.name} was restored by lunar dance\n"
# Items
if self.held_item == "air-balloon" and not self.grounded(battle):
msg += f"{self.name} floats in the air with its air balloon!\n"
if self.held_item == "electric-seed" and battle.terrain.item == "electric":
msg += self.append_defense(1, attacker=self, source="its electric seed")
self.held_item.use()
if self.held_item == "psychic-seed" and battle.terrain.item == "psychic":
msg += self.append_spdef(1, attacker=self, source="its psychic seed")
self.held_item.use()
if self.held_item == "misty-seed" and battle.terrain.item == "misty":
msg += self.append_spdef(1, attacker=self, source="its misty seed")
self.held_item.use()
if self.held_item == "grassy-seed" and battle.terrain.item == "grassy":
msg += self.append_defense(1, attacker=self, source="its grassy seed")
self.held_item.use()
return msg
def send_out_ability(self, otherpoke, battle):
"""
Initalize this poke's ability.
otherpoke may be None.
Returns a formatted message.
"""
msg = ""
#Imposter (sus)
if self.ability() == Ability.IMPOSTER and otherpoke is not None and not otherpoke.substitute and otherpoke.illusion_name is None:
msg += f"{self.name} transformed into {otherpoke._name}!\n"
self.transform(otherpoke)
#Weather
if self.ability() == Ability.DRIZZLE:
msg += battle.weather.set("rain", self)
if self.ability() == Ability.PRIMORDIAL_SEA:
msg += battle.weather.set("h-rain", self)
if self.ability() == Ability.SAND_STREAM:
msg += battle.weather.set("sandstorm", self)
if self.ability() == Ability.SNOW_WARNING:
msg += battle.weather.set("hail", self)
if self.ability() in (Ability.DROUGHT, Ability.ORICHALCUM_PULSE):
msg += battle.weather.set("sun", self)
if self.ability() == Ability.DESOLATE_LAND:
msg += battle.weather.set("h-sun", self)
if self.ability() == Ability.DELTA_STREAM:
msg += battle.weather.set("h-wind", self)
#Terrain
if self.ability() == Ability.GRASSY_SURGE:
msg += battle.terrain.set("grassy", self)
if self.ability() == Ability.MISTY_SURGE:
msg += battle.terrain.set("misty", self)
if self.ability() in (Ability.ELECTRIC_SURGE, Ability.HADRON_ENGINE):
msg += battle.terrain.set("electric", self)
if self.ability() == Ability.PSYCHIC_SURGE:
msg += battle.terrain.set("psychic", self)
# Message only
if self.ability() == Ability.MOLD_BREAKER:
msg += f"{self.name} breaks the mold!\n"
if self.ability() == Ability.TURBOBLAZE:
msg += f"{self.name} is radiating a blazing aura!\n"
if self.ability() == Ability.TERAVOLT:
msg += f"{self.name} is radiating a bursting aura!\n"
if self.ability() == Ability.INTIMIDATE and otherpoke is not None:
if otherpoke.ability() == Ability.OBLIVIOUS:
msg += f"{otherpoke.name} is too oblivious to be intimidated!\n"
elif otherpoke.ability() == Ability.OWN_TEMPO:
msg += f"{otherpoke.name} keeps walking on its own tempo, and is not intimidated!\n"
elif otherpoke.ability() == Ability.INNER_FOCUS:
msg += f"{otherpoke.name} is too focused to be intimidated!\n"
elif otherpoke.ability() == Ability.SCRAPPY:
msg += f"{otherpoke.name} is too scrappy to be intimidated!\n"
elif otherpoke.ability() == Ability.GUARD_DOG:
msg += f"{otherpoke.name}'s guard dog keeps it from being intimidated!\n"
msg += otherpoke.append_attack(1, attacker=otherpoke, source="its guard dog")
else:
msg += otherpoke.append_attack(-1, attacker=self, source=f"{self.name}'s Intimidate")
if otherpoke.held_item == "adrenaline-orb":
msg += otherpoke.append_speed(1, attacker=otherpoke, source="its adrenaline orb")
if otherpoke.ability() == Ability.RATTLED:
msg += otherpoke.append_speed(1, attacker=otherpoke, source="its rattled")
if self.ability() == Ability.SCREEN_CLEANER:
battle.trainer1.aurora_veil.set_turns(0)
battle.trainer1.light_screen.set_turns(0)
battle.trainer1.reflect.set_turns(0)
battle.trainer2.aurora_veil.set_turns(0)
battle.trainer2.light_screen.set_turns(0)
battle.trainer2.reflect.set_turns(0)
msg += f"{self.name}'s screen cleaner removed barriers from both sides of the field!\n"
if self.ability() == Ability.INTREPID_SWORD:
msg += self.append_attack(1, attacker=self, source="its intrepid sword")
if self.ability() == Ability.DAUNTLESS_SHIELD:
msg += self.append_defense(1, attacker=self, source="its dauntless shield")
if self.ability() == Ability.TRACE and otherpoke is not None and otherpoke.ability_giveable():
self.ability_id = otherpoke.ability_id
msg += f"{self.name} traced {otherpoke.name}'s ability!\n"
msg += self.send_out_ability(otherpoke, battle)
return msg
if self.ability() == Ability.DOWNLOAD and otherpoke is not None:
if otherpoke.get_spdef(battle) > otherpoke.get_defense(battle):
msg += self.append_attack(1, attacker=self, source="its download")
else:
msg += self.append_spatk(1, attacker=self, source="its download")
if self.ability() == Ability.ANTICIPATION and otherpoke is not None:
for move in otherpoke.moves:
if move.effect == 39:
msg += f"{self.name} shuddered in anticipation!\n"
break
if self.effectiveness(move.type, battle) > 1:
msg += f"{self.name} shuddered in anticipation!\n"
break
if self.ability() == Ability.FOREWARN and otherpoke is not None:
bestmoves = []
bestpower = 0
for move in otherpoke.moves:
if move.damage_class == DamageClass.STATUS:
power = 0
if move.effect == 39:
power = 150
elif move.power is None: # Good enough
power = 80
else:
power = move.power
if power > bestpower:
bestpower = power
bestmoves = [move]
elif power == bestpower:
bestmoves.append(move)
if bestmoves:
move = random.choice(bestmoves)
msg += f"{self.name} is forewarned about {otherpoke.name}'s {move.pretty_name}!\n"
if self.ability() == Ability.FRISK and otherpoke is not None and otherpoke.held_item.has_item():
msg += f"{self.name} senses that {otherpoke.name} is holding a {otherpoke.held_item.name} using its frisk!\n"
if self.ability() == Ability.MULTITYPE:
e = None
if self.held_item == "draco-plate":
e = ElementType.DRAGON
f = "Arceus-dragon"
elif self.held_item == "dread-plate":
e = ElementType.DARK
f = "Arceus-dark"
elif self.held_item == "earth-plate":
e = ElementType.GROUND
f = "Arceus-ground"
elif self.held_item == "fist-plate":
e = ElementType.FIGHTING
f = "Arceus-fighting"
elif self.held_item == "flame-plate":
e = ElementType.FIRE
f = "Arceus-fire"
elif self.held_item == "icicle-plate":
e = ElementType.ICE
f = "Arceus-ice"
elif self.held_item == "insect-plate":
e = ElementType.BUG
f = "Arceus-bug"
elif self.held_item == "iron-plate":
e = ElementType.STEEL
f = "Arceus-steel"
elif self.held_item == "meadow-plate":
e = ElementType.GRASS
f = "Arceus-grass"
elif self.held_item == "mind-plate":
e = ElementType.PSYCHIC
f = "Arceus-psychic"
elif self.held_item == "pixie-plate":
e = ElementType.FAIRY
f = "Arceus-fairy"
elif self.held_item == "sky-plate":
e = ElementType.FLYING
f = "Arceus-flying"
elif self.held_item == "splash-plate":
e = ElementType.WATER
f = "Arceus-water"
elif self.held_item == "spooky-plate":
e = ElementType.GHOST
f = "Arceus-ghost"
elif self.held_item == "stone-plate":
e = ElementType.ROCK
f = "Arceus-rock"
elif self.held_item == "toxic-plate":
e = ElementType.POISON
f = "Arceus-poison"
elif self.held_item == "zap-plate":
e = ElementType.ELECTRIC
f = "Arceus-electric"
if e is not None and self.form(f):
self.type_ids = [e]
t = ElementType(e).name.lower()
msg += f"{self.name} transformed into a {t} type using its multitype!\n"
if self.ability() == Ability.RKS_SYSTEM and self._name == "Silvally":
e = None
if self.held_item == "dragon-memory":
if self.form("Silvally-dragon"):
e = ElementType.DRAGON
elif self.held_item == "dark-memory":
if self.form("Silvally-dark"):
e = ElementType.DARK
elif self.held_item == "ground-memory":
if self.form("Silvally-ground"):
e = ElementType.GROUND
elif self.held_item == "fighting-memory":
if self.form("Silvally-fighting"):
e = ElementType.FIGHTING
elif self.held_item == "fire-memory":
if self.form("Silvally-fire"):
e = ElementType.FIRE
elif self.held_item == "ice-memory":
if self.form("Silvally-ice"):
e = ElementType.ICE
elif self.held_item == "bug-memory":
if self.form("Silvally-bug"):
e = ElementType.BUG
elif self.held_item == "steel-memory":
if self.form("Silvally-steel"):
e = ElementType.STEEL
elif self.held_item == "grass-memory":
if self.form("Silvally-grass"):
e = ElementType.GRASS
elif self.held_item == "psychic-memory":
if self.form("Silvally-psychic"):
e = ElementType.PSYCHIC
elif self.held_item == "fairy-memory":
if self.form("Silvally-fairy"):
e = ElementType.FAIRY
elif self.held_item == "flying-memory":
if self.form("Silvally-flying"):
e = ElementType.FLYING
elif self.held_item == "water-memory":
if self.form("Silvally-water"):
e = ElementType.WATER
elif self.held_item == "ghost-memory":
if self.form("Silvally-ghost"):
e = ElementType.GHOST
elif self.held_item == "rock-memory":
if self.form("Silvally-rock"):
e = ElementType.ROCK
elif self.held_item == "poison-memory":
if self.form("Silvally-poison"):
e = ElementType.POISON
elif self.held_item == "electric-memory":
if self.form("Silvally-electric"):
e = ElementType.ELECTRIC
if e is not None:
self.type_ids = [e]
t = ElementType(e).name.lower()
msg += f"{self.name} transformed into a {t} type using its rks system!\n"
if self.ability() == Ability.TRUANT:
self.truant_turn = 0
if self.ability() == Ability.FORECAST and self._name in ("Castform", "Castform-snowy", "Castform-rainy", "Castform-sunny"):
weather = battle.weather.get()
element = None
if weather == "hail" and self._name != "Castform-snowy":
if self.form("Castform-snowy"):
element = ElementType.ICE
elif weather in ("sandstorm", "h-wind", None) and self._name != "Castform":
if self.form("Castform"):
element = ElementType.NORMAL
elif weather in ("rain", "h-rain") and self._name != "Castform-rainy":
if self.form("Castform-rainy"):
element = ElementType.WATER
elif weather in ("sun", "h-sun") and self._name != "Castform-sunny":
if self.form("Castform-sunny"):
element = ElementType.FIRE
if element is not None:
self.type_ids = [element]
t = ElementType(element).name.lower()
msg += f"{self.name} transformed into a {t} type using its forecast!\n"
if self.ability() == Ability.MIMICRY and battle.terrain.item:
terrain = battle.terrain.item
if terrain == "electric":
element = ElementType.ELECTRIC
elif terrain == "grassy":
element = ElementType.GRASS
elif terrain == "misty":
element = ElementType.FAIRY
elif terrain == "psychic":
element = ElementType.PSYCHIC
self.type_ids = [element]
t = ElementType(element).name.lower()
msg += f"{self.name} transformed into a {t} type using its mimicry!\n"
if self.ability() == Ability.WIND_RIDER and self.owner.tailwind.active():
msg += self.append_attack(1, attacker=self, source="its wind rider")
if self.ability() == Ability.SUPERSWEET_SYRUP and not self.supersweet_syrup and otherpoke is not None:
msg += otherpoke.append_evasion(-1, attacker=self, source=f"{self.name}'s supersweet syrup")
return msg
def remove(self, battle, *, fainted=False):
"""
Clean up a poke when it is removed from battle.
Returns a formatted message of anything that happened while switching out.
"""
msg = ""
if not fainted:
if self.ability() == Ability.NATURAL_CURE and self.nv.current:
msg += f"{self.name}'s {self.nv.current} was cured by its natural cure!\n"
self.nv.reset()
if self.ability() == Ability.REGENERATOR:
msg += self.heal(self.starting_hp // 3, source="its regenerator")
if self.ability() == Ability.ZERO_TO_HERO:
if self.form("Palafin-hero"):
msg += f"{self.name} is ready to be a hero!\n"
self.nv.badly_poisoned_turn = 0
self.minimized = False
self.has_moved = False
self.choice_move = None
self.last_move = None
self.should_mega_evolve = False
self.swapped_in = False
self.active_turns = 0
if self.illusion__name is not None:
self._name = self.illusion__name
self.name = self.illusion_name
self.illusion__name = None
self.illusion_name = None
if self._starting_name in ("Ditto", "Smeargle", "Mew", "Aegislash"):
self._name = self._starting_name
if self._nickname != "None":
self.name = f"{self._nickname} ({self._name.replace('-', ' ')})"
else:
self.name = self._name.replace("-", " ")
if self.owner.current_pokemon is self:
self.owner.current_pokemon = None
if battle.weather.recheck_ability_weather():
msg += "The weather cleared!\n"
self.attack = self.base_stats[self._name][1]
self.defense = self.base_stats[self._name][2]
self.spatk = self.base_stats[self._name][3]
self.spdef = self.base_stats[self._name][4]
self.speed = self.base_stats[self._name][5]
self.hpiv = self.starting_hpiv
self.atkiv = self.starting_atkiv
self.defiv = self.starting_defiv
self.spatkiv = self.starting_spatkiv
self.spdefiv = self.starting_spdefiv
self.speediv = self.starting_speediv
self.hpev = self.starting_hpev
self.atkev = self.starting_atkev
self.defev = self.starting_defev
self.spatkev = self.starting_spatkev
self.spdefev = self.starting_spdefev
self.speedev = self.starting_speedev
self.moves = self.starting_moves.copy()
self.ability_id = self.starting_ability_id
self.type_ids = self.starting_type_ids.copy()
self.attack_stage = 0
self.defense_stage = 0
self.spatk_stage = 0
self.spdef_stage = 0
self.speed_stage = 0
self.accuracy_stage = 0
self.evasion_stage = 0
self.metronome = Metronome()
self.leech_seed = False
self.stockpile = 0
self.flinched = False
self.confusion = ExpiringEffect(0)
self.last_move_damage = None
self.locked_move = None
self.bide = None
self.torment = False
self.imprison = False
self.disable = ExpiringItem()
self.taunt = ExpiringEffect(0)
self.encore = ExpiringItem()
self.heal_block = ExpiringEffect(0)
self.focus_energy = False
self.perish_song = ExpiringEffect(0)
self.nightmare = False
self.defense_curl = False
self.fury_cutter = 0
self.bind = ExpiringEffect(0)
self.substitute = 0
self.silenced = ExpiringEffect(0)
self.last_move_failed = False
self.rage = False
self.mind_reader = ExpiringItem()
self.destiny_bond = False
self.destiny_bond_cooldown = ExpiringEffect(0)
self.trapping = False
self.ingrain = False
self.infatuated = None
self.aqua_ring = False
self.magnet_rise = ExpiringEffect(0)
self.dive = False
self.dig = False
self.fly = False
self.shadow_force = False
self.lucky_chant = ExpiringEffect(0)
self.grounded_by_move = False
self.charge = ExpiringEffect(0)
self.uproar = ExpiringEffect(0)
self.magic_coat = False
self.power_trick = False
self.power_shift = False
self.yawn = ExpiringEffect(0)
self.ion_deluge = False
self.electrify = False
self.protection_used = False
self.protection_chance = 1
self.protect = False
self.endure = False
self.wide_guard = False
self.crafty_shield = False
self.king_shield = False
self.spiky_shield = False
self.mat_block = False
self.baneful_bunker = False
self.quick_guard = False
self.obstruct = False
self.silk_trap = False
self.burning_bulwark = False
self.laser_focus = ExpiringEffect(0)
self.powdered = False
self.snatching = False
self.telekinesis = ExpiringEffect(0)
self.embargo = ExpiringEffect(0)
self.echoed_voice_power = 40
self.echoed_voice_used = False
self.curse = False
self.fairy_lock = ExpiringEffect(0)
self.grudge = False
self.foresight = False
self.miracle_eye = False
self.beak_blast = False
self.no_retreat = False
self.dmg_this_turn = False
self.autotomize = 0
self.lansat_berry_ate = False
self.micle_berry_ate = False
self.flash_fire = False
self.truant_turn = 0
self.cud_chew = ExpiringEffect(0)
self.booster_energy = False
self.stat_increased = False
self.stat_decreased = False
self.roost = False
self.octolock = False
self.attack_split = None
self.spatk_split = None
self.defense_split = None
self.spdef_split = None
self.tar_shot = False
self.syrup_bomb = ExpiringEffect(0)
self.held_item.ever_had_item = self.held_item.item is not None
return msg
def next_turn(self, otherpoke, battle):
"""
Updates this pokemon for a new turn.
`otherpoke` may be None if the opponent fainted the previous turn.
Returns a formatted message.
"""
msg = ""
# This needs to be here, as swapping sets this value explicitly
self.has_moved = False
if not self.swapped_in:
self.active_turns += 1
self.last_move_damage = None
self.last_move_failed = False
self.should_mega_evolve = False
self.rage = False
self.mind_reader.next_turn()
self.charge.next_turn()
self.destiny_bond_cooldown.next_turn()
self.magic_coat = False
self.ion_deluge = False
self.electrify = False
if not self.protection_used:
self.protection_chance = 1
self.protection_used = False
self.protect = False
self.endure = False
self.wide_guard = False
self.crafty_shield = False
self.king_shield = False
self.spiky_shield = False
self.mat_block = False
self.baneful_bunker = False
self.quick_guard = False
self.obstruct = False
self.silk_trap = False
self.burning_bulwark = False
self.laser_focus.next_turn()
self.powdered = False
self.snatching = False
if not self.echoed_voice_used:
self.echoed_voice_power = 40
self.grudge = False
self.beak_blast = False
self.dmg_this_turn = False
if self.locked_move:
if self.locked_move.next_turn():
self.locked_move = None
# Just in case they never actually used the move to remove it
self.dive = False
self.dig = False
self.fly = False
self.shadow_force = False
self.fairy_lock.next_turn()
self.flinched = False
self.truant_turn += 1
self.stat_increased = False
self.stat_decreased = False
self.roost = False
self.syrup_bomb.next_turn()
msg += self.nv.next_turn(battle)
# Volatile status turn progression
prev_disab_move = self.disable.item
if self.disable.next_turn():
msg += f"{self.name}'s {prev_disab_move.pretty_name} is no longer disabled!\n"
if self.taunt.next_turn():
msg += f"{self.name}'s taunt has ended!\n"
if self.heal_block.next_turn():
msg += f"{self.name}'s heal block has ended!\n"
if self.silenced.next_turn():
msg += f"{self.name}'s voice returned!\n"
if self.magnet_rise.next_turn():
msg += f"{self.name}'s magnet rise has ended!\n"
if self.lucky_chant.next_turn():
msg += f"{self.name} is no longer shielded by lucky chant!\n"
if self.uproar.next_turn():
msg += f"{self.name} calms down!\n"
if self.telekinesis.next_turn():
msg += f"{self.name} was released from telekinesis!\n"
if self.embargo.next_turn():
msg += f"{self.name}'s embargo was lifted!\n"
if self.yawn.next_turn():
msg += self.nv.apply_status("sleep", battle, attacker=self, source="drowsiness")
if self.encore.next_turn():
msg += f"{self.name}'s encore is over!\n"
if self.perish_song.next_turn():
msg += self.faint(battle, source="perish song")
if self.encore.active() and self.encore.item.pp == 0:
self.encore.end()
msg += f"{self.name}'s encore is over!\n"
if self.cud_chew.next_turn() and self.held_item.last_used is not None and self.held_item.last_used.name.endswith("-berry"):
self.held_item.recover(self.held_item)
msg += self.held_item.eat_berry()
self.held_item.last_used = None
# Held Items
if self.held_item == "white-herb":
changed = False
if self.attack_stage < 0:
self.attack_stage = 0
changed = True
if self.defense_stage < 0:
self.defense_stage = 0
changed = True
if self.spatk_stage < 0:
self.spatk_stage = 0
changed = True
if self.spdef_stage < 0:
self.spdef_stage = 0
changed = True
if self.speed_stage < 0:
self.speed_stage = 0
changed = True
if self.accuracy_stage < 0:
self.accuracy_stage = 0
changed = True
if self.evasion_stage < 0:
self.evasion_stage = 0
changed = True
if changed:
msg += f"{self.name}'s white herb reset all negative stat stage changes.\n"
self.held_item.use()
if self.held_item == "toxic-orb":
msg += self.nv.apply_status("b-poison", battle, attacker=self, source="its toxic orb")
if self.held_item == "flame-orb":
msg += self.nv.apply_status("burn", battle, attacker=self, source="its flame orb")
if self.held_item == "leftovers":
msg += self.heal(self.starting_hp // 16, source="its leftovers")
if self.held_item == "black-sludge":
if ElementType.POISON in self.type_ids:
msg += self.heal(self.starting_hp // 16, source="its black sludge")
else:
msg += self.damage(self.starting_hp // 8, battle, source="its black sludge")
# Abilities
if self.ability() == Ability.SPEED_BOOST and not self.swapped_in:
msg += self.append_speed(1, attacker=self, source="its Speed boost")
if self.ability() == Ability.LIMBER and self.nv.paralysis():
self.nv.reset()
msg += f"{self.name}'s limber cured it of its paralysis!\n"
if self.ability() == Ability.INSOMNIA and self.nv.sleep():
self.nv.reset()
msg += f"{self.name}'s insomnia woke it up!\n"
if self.ability() == Ability.VITAL_SPIRIT and self.nv.sleep():
self.nv.reset()
msg += f"{self.name}'s vital spirit woke it up!\n"
if self.ability() == Ability.IMMUNITY and self.nv.poison():
self.nv.reset()
msg += f"{self.name}'s immunity cured it of its poison!\n"
if self.ability() == Ability.MAGMA_ARMOR and self.nv.freeze():
self.nv.reset()
msg += f"{self.name}'s magma armor cured it of thawed it!\n"
if self.ability() in (Ability.WATER_VEIL, Ability.WATER_BUBBLE) and self.nv.burn():
self.nv.reset()
ability_name = Ability(self.ability_id).pretty_name
msg += f"{self.name}'s {ability_name} cured it of its burn!\n"
if self.ability() == Ability.OWN_TEMPO and self.confusion.active():
self.confusion.set_turns(0)
msg += f"{self.name}'s tempo cured it of its confusion!\n"
if self.ability() == Ability.OBLIVIOUS:
if self.infatuated:
self.infatuated = None
msg += f"{self.name} fell out of love because of its obliviousness!\n"
if self.taunt.active():
self.taunt.set_turns(0)
msg += f"{self.name} stopped caring about being taunted because of its obliviousness!\n"
if self.ability() == Ability.RAIN_DISH and battle.weather.get() in ("rain", "h-rain"):
msg += self.heal(self.starting_hp // 16, source="its rain dish")
if self.ability() == Ability.ICE_BODY and battle.weather.get() == "hail":
msg += self.heal(self.starting_hp // 16, source="its ice body")
if self.ability() == Ability.DRY_SKIN:
if battle.weather.get() in ("rain", "h-rain"):
msg += self.heal(self.starting_hp // 8, source="its dry skin")
elif battle.weather.get() in ("sun", "h-sun"):
msg += self.damage(self.starting_hp // 8, battle, source="its dry skin")
if self.ability() == Ability.SOLAR_POWER and battle.weather.get() in ("sun", "h-sun"):
msg += self.damage(self.starting_hp // 8, battle, source="its solar power")
if self.ability() == Ability.MOODY:
stats = [
(self.attack_stage, "attack"),
(self.defense_stage, "defense"),
(self.spatk_stage, "special attack"),
(self.spdef_stage, "special defense"),
(self.speed_stage, "speed")
]
add_stats = stats.copy()
remove_stats = stats.copy()
for stat in stats:
if stat[0] == 6:
add_stats.remove(stat)
add_stat = None
if add_stats:
add_stat = random.choice(add_stats)
msg += self.append_stat(2, self, None, add_stat[1], "its moodiness")
for stat in stats:
if stat == add_stat:
remove_stats.remove(stat)
if stat[0] == -6:
remove_stats.remove(stat)
if remove_stats:
remove_stat = random.choice(remove_stats)
msg += self.append_stat(-1, self, None, remove_stat[1], "its moodiness")
if (
self.ability() == Ability.PICKUP
and not self.held_item.has_item()
and otherpoke is not None
and otherpoke.held_item.last_used is not None
):
self.held_item.recover(otherpoke.held_item)
msg += f"{self.name} picked up a {self.held_item.name}!\n"
if self.ability() == Ability.ICE_FACE and not self.ice_repaired and self._name == "Eiscue-noice" and battle.weather.get() == "hail":
if self.form("Eiscue"):
self.ice_repaired = True
msg += f"{self.name}'s ice face was restored by the hail!\n"
if self.ability() == Ability.HARVEST and self.last_berry is not None and not self.held_item.has_item():
if random.randint(0, 1):
self.held_item.item = self.last_berry
self.last_berry = None
msg += f"{self.name} harvested a {self.held_item.name}!\n"
if self.ability() == Ability.ZEN_MODE and self._name == "Darmanitan" and self.hp < self.starting_hp / 2:
if self.form("Darmanitan-zen"):
if ElementType.PSYCHIC not in self.type_ids:
self.type_ids.append(ElementType.PSYCHIC)
msg += f"{self.name} enters a zen state.\n"
if self.ability() == Ability.ZEN_MODE and self._name == "Darmanitan-galar" and self.hp < self.starting_hp / 2:
if self.form("Darmanitan-zen-galar"):
if ElementType.FIRE not in self.type_ids:
self.type_ids.append(ElementType.FIRE)
msg += f"{self.name} enters a zen state.\n"
if self.ability() == Ability.ZEN_MODE and self._name == "Darmanitan-zen" and self.hp >= self.starting_hp / 2:
if self.form("Darmanitan"):
if ElementType.PSYCHIC in self.type_ids:
self.type_ids.remove(ElementType.PSYCHIC)
msg += f"{self.name}'s zen state ends!\n"
if self.ability() == Ability.ZEN_MODE and self._name == "Darmanitan-zen-galar" and self.hp >= self.starting_hp / 2:
if self.form("Darmanitan-galar"):
if ElementType.FIRE in self.type_ids:
self.type_ids.remove(ElementType.FIRE)
msg += f"{self.name}'s zen state ends!\n"
if self.ability() == Ability.SHIELDS_DOWN and self._name == "Minior" and self.hp < self.starting_hp / 2:
if self.id % 7 == 0:
new_form = "Minior-red"
elif self.id % 7 == 1:
new_form = "Minior-orange"
elif self.id % 7 == 2:
new_form = "Minior-yellow"
elif self.id % 7 == 3:
new_form = "Minior-green"
elif self.id % 7 == 4:
new_form = "Minior-blue"
elif self.id % 7 == 5:
new_form = "Minior-indigo"
else:
new_form = "Minior-violet"
if self.form(new_form):
msg += f"{self.name}'s core was exposed!\n"
if (
self.ability() == Ability.SHIELDS_DOWN
and self._name in ("Minior-red", "Minior-orange", "Minior-yellow", "Minior-green", "Minior-blue", "Minior-indigo", "Minior-violet")
and self.hp >= self.starting_hp / 2
):
if self.form("Minior"):
msg += f"{self.name}'s shell returned!\n"
if self.ability() == Ability.SCHOOLING and self._name == "Wishiwashi-school" and self.hp < self.starting_hp / 4:
if self.form("Wishiwashi"):
msg += f"{self.name}'s school is gone!\n"
if self.ability() == Ability.SCHOOLING and self._name == "Wishiwashi" and self.hp >= self.starting_hp / 4 and self.level >= 20:
if self.form("Wishiwashi-school"):
msg += f"{self.name} schools together!\n"
if self.ability() == Ability.POWER_CONSTRUCT and self._name in ("Zygarde", "Zygarde-10") and self.hp < self.starting_hp / 2 and self.hp > 0:
if self.form("Zygarde-complete"):
msg += f"{self.name} is at full power!\n"
# Janky way to raise the current HP of this poke, as it's new form has a higher HP stat. Note, this is NOT healing.
new_hp = round((((2 * self.base_stats["Zygarde-complete"][0] + self.hpiv + (self.hpev / 4)) * self.level) / 100) + self.level + 10)
self.hp = new_hp - (self.starting_hp - self.hp)
self.starting_hp = new_hp
if self.ability() == Ability.HUNGER_SWITCH:
if self._name == "Morpeko":
self.form("Morpeko-hangry")
elif self._name == "Morpeko-hangry":
self.form("Morpeko")
if self.ability() == Ability.FLOWER_GIFT and self._name == "Cherrim" and battle.weather.get() in ("sun", "h-sun"):
self.form("Cherrim-sunshine")
if self.ability() == Ability.FLOWER_GIFT and self._name == "Cherrim-sunshine" and battle.weather.get() not in ("sun", "h-sun"):
self.form("Cherrim")
#Bad Dreams
if otherpoke is not None and otherpoke.ability() == Ability.BAD_DREAMS and self.nv.sleep():
msg += self.damage(self.starting_hp // 8, battle, source=f"{otherpoke.name}'s bad dreams")
#Leech seed
if self.leech_seed and otherpoke is not None:
damage = self.starting_hp // 8
msg += self.damage(damage, battle, attacker=otherpoke, drain_heal_ratio=1, source="leech seed")
#Curse
if self.curse:
msg += self.damage(self.starting_hp // 4, battle, source="its curse")
#Syrup bomb
if self.syrup_bomb.active() and otherpoke is not None:
msg += self.append_speed(-1, attacker=otherpoke, source="its syrup coating")
#Weather damages
if self.ability() == Ability.OVERCOAT:
pass
elif self.held_item == "safety-goggles":
pass
elif battle.weather.get() == "sandstorm":
if (
ElementType.ROCK not in self.type_ids
and ElementType.GROUND not in self.type_ids
and ElementType.STEEL not in self.type_ids
and self.ability() not in (Ability.SAND_RUSH, Ability.SAND_VEIL, Ability.SAND_FORCE)
):
msg += self.damage(self.starting_hp // 16, battle, source="the sandstorm")
elif battle.weather.get() == "hail":
if (
ElementType.ICE not in self.type_ids
and self.ability() not in (Ability.SNOW_CLOAK, Ability.ICE_BODY)
):
msg += self.damage(self.starting_hp // 16, battle, source="the hail")
#Bind
if self.bind.next_turn():
msg += f"{self.name} is no longer bound!\n"
elif self.bind.active() and otherpoke is not None:
if otherpoke.held_item == "binding-band":
msg += self.damage(self.starting_hp // 6, battle, source=f"{otherpoke.name}'s bind")
else:
msg += self.damage(self.starting_hp // 8, battle, source=f"{otherpoke.name}'s bind")
#Ingrain
if self.ingrain:
heal = self.starting_hp // 16
if self.held_item == "big_root":
heal = int(heal * 1.3)
msg += self.heal(heal, source="ingrain")
#Aqua Ring
if self.aqua_ring:
heal = self.starting_hp // 16
if self.held_item == "big_root":
heal = int(heal * 1.3)
msg += self.heal(heal, source="aqua ring")
#Octolock
if self.octolock and otherpoke is not None:
msg += self.append_defense(-1, attacker=self, source=f"{otherpoke.name}'s octolock")
msg += self.append_spdef(-1, attacker=self, source=f"{otherpoke.name}'s octolock")
#Grassy Terrain
if battle.terrain.item == "grassy" and self.grounded(battle) and not self.heal_block.active():
msg += self.heal(self.starting_hp // 16, source="grassy terrain")
#Goes at the end so everything in this func that checks it handles it correctly
self.swapped_in = False
return msg
def faint(self, battle, *, move=None, attacker=None, source: str=""):
"""
Sets a pokemon's HP to zero and cleans it up.
If a pokemon takes damage equal to its HP, use damage instead.
This method ignores focus sash and sturdy, forcing the pokemon to faint.
Returns a formatted message.
"""
msg = ""
self.hp = 0
if source:
source = f" from {source}"
msg += f"{self.name} fainted{source}!\n"
if move is not None and attacker is not None and self.destiny_bond and self.owner.has_alive_pokemon():
msg += attacker.faint(battle, source=f"{self.name}'s destiny bond")
if move is not None and attacker is not None and attacker._name == "Greninja" and attacker.ability() == Ability.BATTLE_BOND:
if attacker.form("Greninja-ash"):
msg += f"{attacker.name}'s bond with its trainer has strengthened it!\n"
if move is not None and self.grudge:
move.pp = 0
msg += f"{move.pretty_name}'s pp was depleted!\n"
if attacker is not None and attacker.ability() in (Ability.CHILLING_NEIGH, Ability.AS_ONE_ICE):
msg += attacker.append_attack(1, attacker=attacker, source="its chilling neigh")
if attacker is not None and attacker.ability() in (Ability.GRIM_NEIGH, Ability.AS_ONE_SHADOW):
msg += attacker.append_spatk(1, attacker=attacker, source="its grim neigh")
for poke in (battle.trainer1.current_pokemon, battle.trainer2.current_pokemon):
if poke is not None and poke is not self and poke.ability() == Ability.SOUL_HEART:
msg += poke.append_spatk(
1,
attacker=poke,
source="its soul heart"
)
self.owner.retaliate.set_turns(2)
self.owner.num_fainted += 1
msg += self.remove(battle, fainted=True)
return msg
def damage(self, damage, battle, *, move=None, move_type=None, attacker=None, critical=False, drain_heal_ratio=None, source: str=""):
"""
Applies a certain amount of damage to this pokemon.
Returns a formatted message.
"""
msgadd, _ = self._damage(damage, battle, move=move, move_type=move_type, attacker=attacker, critical=critical, drain_heal_ratio=drain_heal_ratio, source=source)
return msgadd
def _damage(self, damage, battle, *, move=None, move_type=None, attacker=None, critical=False, drain_heal_ratio=None, source: str=""):
"""
Applies a certain amount of damage to this pokemon.
Returns a formatted message and the amount of damage actually dealt.
"""
msg = ""
# Don't go through with an attack if the poke is already dead.
# If this is a bad idea for *some* reason, make sure to add an `attacker is self` check to INNARDS_OUT.
if self.hp <= 0:
return "", 0
previous_hp = self.hp
damage = max(1, damage)
# Magic guard
if self.ability(attacker=attacker, move=move) == Ability.MAGIC_GUARD and move is None and attacker is not self:
return f"{self.name}'s magic guard protected it from damage!\n", 0
# Substitute
if (
self.substitute
and move is not None and move.is_affected_by_substitute() and not move.is_sound_based()
and (attacker is None or attacker.ability() != Ability.INFILTRATOR)
):
# is_affected_by_substitute should be a superset of is_sound_based, but it's better to check both to be sure.
msg += f"{self.name}'s substitute took {damage} damage{source}!\n"
new_hp = max(0, self.substitute - damage)
true_damage = self.substitute - new_hp
self.substitute = new_hp
if self.substitute == 0:
msg += f"{self.name}'s substitute broke!\n"
return msg, true_damage
# Damage blocking forms / abilities
if move is not None:
if self.ability(attacker=attacker, move=move) == Ability.DISGUISE and self._name == "Mimikyu":
if self.form("Mimikyu-busted"):
msg += f"{self.name}'s disguise was busted!\n"
msg += self.damage(self.starting_hp // 8, battle, source="losing its disguise")
return msg, 0
if self.ability(attacker=attacker, move=move) == Ability.ICE_FACE and self._name == "Eiscue" and move.damage_class == DamageClass.PHYSICAL:
if self.form("Eiscue-noice"):
msg += f"{self.name}'s ice face was busted!\n"
return msg, 0
# OHKO protection
self.dmg_this_turn = True
if damage >= self.hp and move is not None:
if self.endure:
msg += f"{self.name} endured the hit!\n"
damage = self.hp - 1
elif self.hp == self.starting_hp and self.ability(attacker=attacker, move=move) == Ability.STURDY:
msg += f"{self.name} endured the hit with its Sturdy!\n"
damage = self.hp - 1
elif self.hp == self.starting_hp and self.held_item == "focus-sash":
msg += f"{self.name} held on using its focus sash!\n"
damage = self.hp - 1
self.held_item.use()
elif self.held_item == "focus-band" and not random.randrange(10):
msg += f"{self.name} held on using its focus band!\n"
damage = self.hp - 1
# Apply the damage
dropped_below_half = self.hp > self.starting_hp / 2
dropped_below_quarter = self.hp > self.starting_hp / 4
new_hp = max(0, self.hp - damage)
true_damage = self.hp - new_hp
self.hp = new_hp
dropped_below_half = dropped_below_half and self.hp <= self.starting_hp / 2
dropped_below_quarter = dropped_below_quarter and self.hp <= self.starting_hp / 4
if source:
source = f" from {source}"
msg += f"{self.name} took {damage} damage{source}!\n"
self.num_hits += 1
# Drain
if drain_heal_ratio is not None and attacker is not None:
heal = true_damage * drain_heal_ratio
if attacker.held_item == "big-root":
heal = heal * 1.3
heal = int(heal)
if self.ability() == Ability.LIQUID_OOZE:
msg += attacker.damage(heal, battle, source=f"{self.name}'s liquid ooze")
else:
if not attacker.heal_block.active():
msg += attacker.heal(heal, source=source)
if self.hp == 0:
msg += self.faint(battle, move=move, attacker=attacker)
if (
self.ability() == Ability.AFTERMATH
and attacker is not None
and attacker is not self
and attacker.ability() != Ability.DAMP
and move is not None
and move.makes_contact(attacker)
):
msg += attacker.damage(attacker.starting_hp // 4, battle, source=f"{self.name}'s aftermath")
if attacker is not None and attacker.ability() == Ability.MOXIE:
msg += attacker.append_attack(1, attacker=attacker, source="its moxie")
if attacker is not None and attacker.ability() == Ability.BEAST_BOOST:
stats = (
(attacker.get_raw_attack(), attacker.append_attack),
(attacker.get_raw_defense(), attacker.append_defense),
(attacker.get_raw_spatk(), attacker.append_spatk),
(attacker.get_raw_spdef(), attacker.append_spdef),
(attacker.get_raw_speed(), attacker.append_speed),
)
append_func = sorted(stats, reverse=True, key=lambda s: s[0])[0][1]
msg += append_func(1, attacker=attacker, source="its beast boost")
if attacker is not None and self.ability() == Ability.INNARDS_OUT:
msg += attacker.damage(previous_hp, battle, attacker=self, source=f"{self.name}'s innards out")
elif move is not None and move_type is not None:
if move_type == ElementType.FIRE and self.nv.freeze():
self.nv.reset()
msg += f"{self.name} thawed out!\n"
if move.effect in (458, 500) and self.nv.freeze():
self.nv.reset()
msg += f"{self.name} thawed out!\n"
if self.ability() == Ability.COLOR_CHANGE and move_type not in self.type_ids:
self.type_ids = [move_type]
t = ElementType(move_type).name.lower()
msg += f"{self.name} changed its color, transforming into a {t} type!\n"
if self.ability() == Ability.ANGER_POINT and critical:
msg += self.append_attack(6, attacker=self, source="its anger point")
if self.ability() == Ability.WEAK_ARMOR and move.damage_class == DamageClass.PHYSICAL and attacker is not self:
msg += self.append_defense(-1, attacker=self, source="its weak armor")
msg += self.append_speed(2, attacker=self, source="its weak armor")
if self.ability() == Ability.JUSTIFIED and move_type == ElementType.DARK:
msg += self.append_attack(1, attacker=self, source="justified")
if self.ability() == Ability.RATTLED and move_type in (ElementType.BUG, ElementType.DARK, ElementType.GHOST):
msg += self.append_speed(1, attacker=self, source="its rattled")
if self.ability() == Ability.STAMINA:
msg += self.append_defense(1, attacker=self, source="its stamina")
if self.ability() == Ability.WATER_COMPACTION and move_type == ElementType.WATER:
msg += self.append_defense(2, attacker=self, source="its water compaction")
if self.ability() == Ability.BERSERK and dropped_below_half:
msg += self.append_spatk(1, attacker=self, source="its berserk")
if self.ability() == Ability.ANGER_SHELL and dropped_below_half:
msg += self.append_attack(1, attacker=self, source="its anger shell")
msg += self.append_spatk(1, attacker=self, source="its anger shell")
msg += self.append_speed(1, attacker=self, source="its anger shell")
msg += self.append_defense(-1, attacker=self, source="its anger shell")
msg += self.append_spdef(-1, attacker=self, source="its anger shell")
if self.ability() == Ability.STEAM_ENGINE and move_type in (ElementType.FIRE, ElementType.WATER):
msg += self.append_speed(6, attacker=self, source="its steam engine")
if self.ability() == Ability.THERMAL_EXCHANGE and move_type == ElementType.FIRE:
msg += self.append_attack(1, attacker=self, source="its thermal exchange")
if self.ability() == Ability.WIND_RIDER and move.is_wind():
msg += self.append_attack(1, attacker=self, source="its wind rider")
if self.ability() == Ability.COTTON_DOWN and attacker is not None:
msg += attacker.append_speed(-1, attacker=self, source=f"{self.name}'s cotton down")
if self.ability() == Ability.SAND_SPIT:
msg += battle.weather.set("sandstorm", self)
if self.ability() == Ability.SEED_SOWER and battle.terrain.item is None:
msg += battle.terrain.set("grassy", self)
if self.ability() == Ability.ELECTROMORPHOSIS:
self.charge.set_turns(2)
msg += f"{self.name} became charged by its electromorphosis!\n"
if self.ability() == Ability.WIND_POWER and move.is_wind():
self.charge.set_turns(2)
msg += f"{self.name} became charged by its wind power!\n"
if self.ability() == Ability.TOXIC_DEBRIS and move.damage_class == DamageClass.PHYSICAL:
if attacker is not None and attacker is not self and attacker.owner.toxic_spikes < 2:
attacker.owner.toxic_spikes += 1
msg += f"Toxic spikes were scattered around the feet of {attacker.owner.name}'s team because of {self.name}'s toxic debris!\n"
if self.illusion_name is not None:
self._name = self.illusion__name
self.name = self.illusion_name
self.illusion__name = None
self.illusion_name = None
msg += f"{self.name}'s illusion broke!\n"
if self.held_item == "air-balloon":
self.held_item.remove()
msg += f"{self.name}'s air balloon popped!\n"
if move is not None:
self.last_move_damage = (max(1, damage), move.damage_class)
if self.bide is not None:
self.bide += damage
if self.rage:
msg += self.append_attack(1, attacker=self, source="its rage")
if attacker is not None:
if (
self.ability() == Ability.CURSED_BODY
and not attacker.disable.active()
and move in attacker.moves
and random.randint(1, 100) <= 30
):
if attacker.ability() == Ability.AROMA_VEIL:
msg += f"{attacker.name}'s aroma veil protects its move from being disabled!\n"
else:
attacker.disable.set(move, 4)
msg += f"{attacker.name}'s {move.pretty_name} was disabled by {self.name}'s cursed body!\n"
if attacker.ability() == Ability.MAGICIAN and not attacker.held_item.has_item() and self.held_item.can_remove():
self.held_item.transfer(attacker.held_item)
msg += f"{attacker.name} stole {attacker.held_item.name} using its magician!\n"
if attacker.ability() == Ability.TOXIC_CHAIN and random.randint(1, 100) <= 30:
msg += self.nv.apply_status("b-poison", battle, attacker=attacker, source=f"{attacker.name}'s toxic chain")
if attacker.held_item == "shell-bell":
# Shell bell does not trigger when a move is buffed by sheer force.
if attacker.ability() != Ability.SHEER_FORCE or move.effect_chance is None:
msg += attacker.heal(damage // 8, source="its shell bell")
# Retreat
if dropped_below_half and sum(x.hp > 0 for x in self.owner.party) > 1:
if self.ability() == Ability.WIMP_OUT:
msg += f"{self.name} wimped out and retreated!\n"
msg += self.remove(battle)
elif self.ability() == Ability.EMERGENCY_EXIT:
msg += f"{self.name} used the emergency exit and retreated!\n"
msg += self.remove(battle)
# Gulp Missile
if attacker is not None and self._name in ("Cramorant-gulping", "Cramorant-gorging") and self.owner.has_alive_pokemon():
prey = "pikachu" if self._name == "Cramorant-gorging" else "arrokuda"
if self.form("Cramorant"):
msg += attacker.damage(attacker.starting_hp // 4, battle, source=f"{self.name} spitting out its {prey}")
if prey == "arrokuda":
msg += attacker.append_defense(-1, attacker=self, source=f"{self.name} spitting out its {prey}")
elif prey == "pikachu":
msg += attacker.nv.apply_status("paralysis", battle, attacker=self, source=f"{self.name} spitting out its {prey}")
# Berries
if self.held_item.should_eat_berry_damage(attacker):
msg += self.held_item.eat_berry(attacker=attacker, move=move)
# Contact
if move is not None and attacker is not None and move.makes_contact(attacker):
# Affects ATTACKER
if attacker.held_item != "protective-pads":
if self.beak_blast:
msg += attacker.nv.apply_status("burn", battle, attacker=attacker, source=f"{self.name}'s charging beak blast")
if self.ability() == Ability.STATIC:
if random.randint(1, 100) <= 30:
msg += attacker.nv.apply_status("paralysis", battle, attacker=attacker, source=f"{self.name}'s static")
if self.ability() == Ability.POISON_POINT:
if random.randint(1, 100) <= 30:
msg += attacker.nv.apply_status("poison", battle, attacker=attacker, source=f"{self.name}'s poison point")
if self.ability() == Ability.FLAME_BODY:
if random.randint(1, 100) <= 30:
msg += attacker.nv.apply_status("burn", battle, attacker=attacker, source=f"{self.name}'s flame body")
if self.ability() == Ability.ROUGH_SKIN and self.owner.has_alive_pokemon():
msg += attacker.damage(attacker.starting_hp // 8, battle, source=f"{self.name}'s rough skin")
if self.ability() == Ability.IRON_BARBS and self.owner.has_alive_pokemon():
msg += attacker.damage(attacker.starting_hp // 8, battle, source=f"{self.name}'s iron barbs")
if self.ability() == Ability.EFFECT_SPORE:
if (
attacker.ability() != Ability.OVERCOAT
and ElementType.GRASS not in attacker.type_ids
and attacker.held_item != "safety-glasses"
and random.randint(1, 100) <= 30
):
status = random.choice(["paralysis", "poison", "sleep"])
msg += attacker.nv.apply_status(status, battle, attacker=attacker)
if self.ability() == Ability.CUTE_CHARM and random.randint(1, 100) <= 30:
msg += attacker.infatuate(self, source=f"{self.name}'s cute charm")
if self.ability() == Ability.MUMMY and attacker.ability() != Ability.MUMMY and attacker.ability_changeable():
attacker.ability_id = Ability.MUMMY
msg += f"{attacker.name} gained mummy from {self.name}!\n"
msg += attacker.send_out_ability(self, battle)
if self.ability() == Ability.LINGERING_AROMA and attacker.ability() != Ability.LINGERING_AROMA and attacker.ability_changeable():
attacker.ability_id = Ability.LINGERING_AROMA
msg += f"{attacker.name} gained lingering aroma from {self.name}!\n"
msg += attacker.send_out_ability(self, battle)
if self.ability() == Ability.GOOEY:
msg += attacker.append_speed(-1, attacker=self, source=f"touching {self.name}'s gooey body")
if self.ability() == Ability.TANGLING_HAIR:
msg += attacker.append_speed(-1, attacker=self, source=f"touching {self.name}'s tangled hair")
if self.held_item == "rocky-helmet" and self.owner.has_alive_pokemon():
msg += attacker.damage(attacker.starting_hp // 6, battle, source=f"{self.name}'s rocky helmet")
# Pickpocket is not included in the protective pads protection
if (
self.ability() == Ability.PICKPOCKET
and not self.held_item.has_item()
and attacker.held_item.has_item()
and attacker.held_item.can_remove()
):
if attacker.ability() == Ability.STICKY_HOLD:
msg += f"{attacker.name}'s sticky hand kept hold of its item!\n"
else:
attacker.held_item.transfer(self.held_item)
msg += f"{attacker.name}'s {self.held_item.name} was stolen!\n"
# Affects DEFENDER
if attacker.ability() == Ability.POISON_TOUCH:
if random.randint(1, 100) <= 30:
msg += self.nv.apply_status("poison", battle, attacker=attacker, move=move, source=f"{attacker.name}'s poison touch")
# Affects BOTH
if attacker.held_item != "protective-pads":
if self.ability() == Ability.PERISH_BODY and not attacker.perish_song.active():
attacker.perish_song.set_turns(4)
self.perish_song.set_turns(4)
msg += f"All pokemon will faint after 3 turns from {self.name}'s perish body!\n"
if self.ability() == Ability.WANDERING_SPIRIT and attacker.ability_changeable() and attacker.ability_giveable():
msg += f"{attacker.name} swapped abilities with {self.name} because of {self.name}'s wandering spirit!\n"
self.ability_id, attacker.ability_id = attacker.ability_id, self.ability_id
ability_name = Ability(self.ability_id).pretty_name
msg += f"{self.name} acquired {ability_name}!\n"
msg += self.send_out_ability(attacker, battle)
ability_name = Ability(attacker.ability_id).pretty_name
msg += f"{attacker.name} acquired {ability_name}!\n"
msg += attacker.send_out_ability(self, battle)
return msg, true_damage
def heal(self, health, *, source: str=""):
"""
Heals a pokemon by a certain amount.
Handles heal-affecting items and abilities, and keeping health within bounds.
Returns a formatted message.
"""
msg = ""
health = max(1, int(health))
#greater than is used to prevent zygarde's hp incresing form from losing its extra health
if self.hp >= self.starting_hp:
return msg
#safety to prevent errors from ""reviving"" pokes
if self.hp == 0:
return msg
if self.heal_block.active():
return msg
health = min(self.starting_hp - self.hp, health)
self.hp += health
if source:
source = f" from {source}"
msg += f"{self.name} healed {health} hp{source}!\n"
return msg
def confuse(self, *, attacker=None, move=None, source=""):
"""
Attempts to confuse this poke.
Returns a formatted message.
"""
if self.substitute and (move is None or move.is_affected_by_substitute()):
return ""
if self.confusion.active():
return ""
if self.ability(move=move, attacker=attacker) == Ability.OWN_TEMPO:
return ""
self.confusion.set_turns(random.randint(2, 5))
if source:
source = f" from {source}"
msg = f"{self.name} is confused{source}!\n"
if self.held_item.should_eat_berry_status(attacker):
msg += self.held_item.eat_berry(attacker=attacker, move=move)
return msg
def flinch(self, *, attacker=None, move=None, source=""):
"""
Attepts to flinch this poke.
Returns a formatted message.
"""
msg = ""
if self.substitute and (move is None or move.is_affected_by_substitute()):
return ""
if self.ability(move=move, attacker=attacker) == Ability.INNER_FOCUS:
return f"{self.name} resisted the urge to flinch with its inner focus!\n"
self.flinched = True
if source:
source = f" from {source}"
msg += f"{self.name} flinched{source}!\n"
if self.ability() == Ability.STEADFAST:
msg += self.append_speed(1, attacker=self, source="its steadfast")
return msg
def infatuate(self, attacker, *, move=None, source=""):
"""
Attepts to cause attacker to infatuate this poke.
Returns a formatted message.
"""
msg = ""
if source:
source = f" from {source}"
if "-x" in (self.gender, attacker.gender):
return ""
if self.gender == attacker.gender:
return ""
if self.ability(move=move, attacker=attacker) == Ability.OBLIVIOUS:
return f"{self.name} is too oblivious to fall in love!\n"
if self.ability(move=move, attacker=attacker) == Ability.AROMA_VEIL:
return f"{self.name}'s aroma veil protects it from being infatuated!\n"
self.infatuated = attacker
msg += f"{self.name} fell in love{source}!\n"
if self.held_item == "destiny-knot":
msg += attacker.infatuate(self, source=f"{self.name}'s destiny knot")
return msg
def calculate_raw_stat(self, base, iv, ev, nature):
"""
Helper function to calculate a raw stat using the base, IV, EV, level, and nature.
https://bulbapedia.bulbagarden.net/wiki/Stat#Determination_of_stats "In Generation III onward"
"""
return round((((2 * base + iv + (ev / 4)) * self.level) / 100) + 5) * nature
@staticmethod
def calculate_stat(stat, stat_stage, *, crop=False):
"""Calculates a stat based on that stat's stage changes."""
if crop == "bottom":
stat_stage = max(stat_stage, 0)
elif crop == "top":
stat_stage = min(stat_stage, 0)
stage_multiplier = {
-6: 2/8,
-5: 2/7,
-4: 2/6,
-3: 2/5,
-2: 2/4,
-1: 2/3,
0: 1,
1: 3/2,
2: 2,
3: 5/2,
4: 3,
5: 7/2,
6: 4,
}
return stage_multiplier[stat_stage] * stat
def get_raw_attack(self, *, check_power_trick=True, check_power_shift=True):
"""Returns the raw attack of this poke, taking into account IVs EVs and natures and forms."""
if self.power_trick and check_power_trick:
return self.get_raw_defense(check_power_trick=False, check_power_shift=check_power_shift)
if self.power_shift and check_power_shift:
return self.get_raw_defense(check_power_trick=check_power_trick, check_power_shift=False)
stat = self.calculate_raw_stat(self.attack, self.atkiv, self.atkev, self.nature_stat_deltas["Attack"])
if self.attack_split is not None:
stat = (stat + self.attack_split) // 2
return stat
def get_raw_defense(self, *, check_power_trick=True, check_power_shift=True):
"""Returns the raw attack of this poke, taking into account IVs EVs and natures and forms."""
if self.power_trick and check_power_trick:
return self.get_raw_attack(check_power_trick=False, check_power_shift=check_power_shift)
if self.power_shift and check_power_shift:
return self.get_raw_attack(check_power_trick=check_power_trick, check_power_shift=False)
stat = self.calculate_raw_stat(self.defense, self.defiv, self.defev, self.nature_stat_deltas["Defense"])
if self.defense_split is not None:
stat = (stat + self.defense_split) // 2
return stat
def get_raw_spatk(self, *, check_power_shift=True):
"""Returns the raw attack of this poke, taking into account IVs EVs and natures and forms."""
if self.power_shift and check_power_shift:
return self.get_raw_spdef(check_power_shift=False)
stat = self.calculate_raw_stat(self.spatk, self.spatkiv, self.spatkev, self.nature_stat_deltas["Special attack"])
if self.spatk_split is not None:
stat = (stat + self.spatk_split) // 2
return stat
def get_raw_spdef(self, *, check_power_shift=True):
"""Returns the raw attack of this poke, taking into account IVs EVs and natures and forms."""
if self.power_shift and check_power_shift:
return self.get_raw_spatk(check_power_shift=False)
stat = self.calculate_raw_stat(self.spdef, self.spdefiv, self.spdefev, self.nature_stat_deltas["Special defense"])
if self.spdef_split is not None:
stat = (stat + self.spdef_split) // 2
return stat
def get_raw_speed(self):
"""Returns the raw attack of this poke, taking into account IVs EVs and natures and forms."""
return self.calculate_raw_stat(self.speed, self.speediv, self.speedev, self.nature_stat_deltas["Speed"])
def get_attack(self, battle, *, critical=False, ignore_stages=False):
"""Helper method to call calculate_stat for attack."""
attack = self.get_raw_attack()
if not ignore_stages:
attack = self.calculate_stat(attack, self.attack_stage, crop="bottom" if critical else False)
if self.ability() == Ability.GUTS and self.nv.current:
attack *= 1.5
if self.ability() == Ability.SLOW_START and self.active_turns < 5:
attack *= .5
if self.ability() in (Ability.HUGE_POWER, Ability.PURE_POWER):
attack *= 2
if self.ability() == Ability.HUSTLE:
attack *= 1.5
if self.ability() == Ability.DEFEATIST and self.hp <= self.starting_hp / 2:
attack *= .5
if self.ability() == Ability.GORILLA_TACTICS:
attack *= 1.5
if self.ability() == Ability.FLOWER_GIFT and battle.weather.get() in ("sun", "h-sun"):
attack *= 1.5
if self.ability() == Ability.ORICHALCUM_PULSE and battle.weather.get() in ("sun", "h-sun"):
attack *= 4/3
if self.held_item == "choice-band":
attack *= 1.5
if self.held_item == "light-ball" and self._name == "Pikachu":
attack *= 2
if self.held_item == "thick-club" and self._name in ("Cubone", "Marowak", "Marowak-alola"):
attack *= 2
for poke in (battle.trainer1.current_pokemon, battle.trainer2.current_pokemon):
if poke is not None and poke is not self and poke.ability() == Ability.TABLETS_OF_RUIN:
attack *= .75
if all(self.get_raw_attack() >= x for x in (self.get_raw_defense(), self.get_raw_spatk(), self.get_raw_spdef(), self.get_raw_speed())):
if (
(self.ability() == Ability.PROTOSYNTHESIS and (battle.weather.get() in ("sun", "h-sun") or self.booster_energy))
or (self.ability() == Ability.QUARK_DRIVE and (battle.terrain.item == "electric" or self.booster_energy))
):
attack *= 1.3
elif self.ability() in (Ability.PROTOSYNTHESIS, Ability.QUARK_DRIVE) and self.held_item == "booster-energy":
self.held_item.use()
self.booster_energy = True
attack *= 1.3
return attack
def get_defense(self, battle, *, critical=False, ignore_stages=False, attacker=None, move=None):
"""Helper method to call calculate_stat for defense."""
if battle.wonder_room.active():
defense = self.get_raw_spdef()
else:
defense = self.get_raw_defense()
if not ignore_stages:
defense = self.calculate_stat(defense, self.defense_stage, crop="top" if critical else False)
if self.ability(attacker=attacker, move=move) == Ability.MARVEL_SCALE and self.nv.current:
defense *= 1.5
if self.ability(attacker=attacker, move=move) == Ability.FUR_COAT:
defense *= 2
if self.ability(attacker=attacker, move=move) == Ability.GRASS_PELT and battle.terrain.item == "grassy":
defense *= 1.5
if self.held_item == "eviolite" and self.can_still_evolve:
defense *= 1.5
for poke in (battle.trainer1.current_pokemon, battle.trainer2.current_pokemon):
if poke is not None and poke is not self and poke.ability() == Ability.SWORD_OF_RUIN:
defense *= .75
if (
self.get_raw_defense() > self.get_raw_attack()
and all(self.get_raw_defense() >= x for x in (self.get_raw_spatk(), self.get_raw_spdef(), self.get_raw_speed()))
):
if (
(self.ability() == Ability.PROTOSYNTHESIS and (battle.weather.get() in ("sun", "h-sun") or self.booster_energy))
or (self.ability() == Ability.QUARK_DRIVE and (battle.terrain.item == "electric" or self.booster_energy))
):
defense *= 1.3
elif self.ability() in (Ability.PROTOSYNTHESIS, Ability.QUARK_DRIVE) and self.held_item == "booster-energy":
self.held_item.use()
self.booster_energy = True
defense *= 1.3
return defense
def get_spatk(self, battle, *, critical=False, ignore_stages=False):
"""Helper method to call calculate_stat for spatk."""
spatk = self.get_raw_spatk()
if not ignore_stages:
spatk = self.calculate_stat(spatk, self.spatk_stage, crop="bottom" if critical else False)
if self.ability() == Ability.DEFEATIST and self.hp <= self.starting_hp / 2:
spatk *= .5
if self.ability() == Ability.SOLAR_POWER and battle.weather.get() in ("sun", "h-sun"):
spatk *= 1.5
if self.ability() == Ability.HADRON_ENGINE and battle.terrain.item == "grassy":
spatk *= 4/3
if self.held_item == "choice-specs":
spatk *= 1.5
if self.held_item == "deep-sea-tooth" and self._name == "Clamperl":
spatk *= 2
if self.held_item == "light-ball" and self._name == "Pikachu":
spatk *= 2
for poke in (battle.trainer1.current_pokemon, battle.trainer2.current_pokemon):
if poke is not None and poke is not self and poke.ability() == Ability.VESSEL_OF_RUIN:
spatk *= .75
if (
all(self.get_raw_spatk() >= x for x in (self.get_raw_spdef(), self.get_raw_speed()))
and all(self.get_raw_spatk() > x for x in (self.get_raw_attack(), self.get_raw_defense()))
):
if (
(self.ability() == Ability.PROTOSYNTHESIS and (battle.weather.get() in ("sun", "h-sun") or self.booster_energy))
or (self.ability() == Ability.QUARK_DRIVE and (battle.terrain.item == "electric" or self.booster_energy))
):
spatk *= 1.3
elif self.ability() in (Ability.PROTOSYNTHESIS, Ability.QUARK_DRIVE) and self.held_item == "booster-energy":
self.held_item.use()
self.booster_energy = True
spatk *= 1.3
return spatk
def get_spdef(self, battle, *, critical=False, ignore_stages=False, attacker=None, move=None):
"""Helper method to call calculate_stat for spdef."""
if battle.wonder_room.active():
spdef = self.get_raw_defense()
else:
spdef = self.get_raw_spdef()
if not ignore_stages:
spdef = self.calculate_stat(spdef, self.spdef_stage, crop="top" if critical else False)
if battle.weather.get() == "sandstorm" and ElementType.ROCK in self.type_ids:
spdef *= 1.5
if self.ability(attacker=attacker, move=move) == Ability.FLOWER_GIFT and battle.weather.get() in ("sun", "h-sun"):
spdef *= 1.5
if self.held_item == "deep-sea-scale" and self._name == "Clamperl":
spdef *= 2
if self.held_item == "assault-vest":
spdef *= 1.5
if self.held_item == "eviolite" and self.can_still_evolve:
spdef *= 1.5
for poke in (battle.trainer1.current_pokemon, battle.trainer2.current_pokemon):
if poke is not None and poke is not self and poke.ability() == Ability.BEADS_OF_RUIN:
spdef *= .75
if (
self.get_raw_spdef() >= self.get_raw_speed()
and all(self.get_raw_spdef() > x for x in (self.get_raw_attack(), self.get_raw_defense(), self.get_raw_spatk()))
):
if (
(self.ability() == Ability.PROTOSYNTHESIS and (battle.weather.get() in ("sun", "h-sun") or self.booster_energy))
or (self.ability() == Ability.QUARK_DRIVE and (battle.terrain.item == "electric" or self.booster_energy))
):
spdef *= 1.3
elif self.ability() in (Ability.PROTOSYNTHESIS, Ability.QUARK_DRIVE) and self.held_item == "booster-energy":
self.held_item.use()
self.booster_energy = True
spdef *= 1.3
return spdef
def get_speed(self, battle):
"""Helper method to call calculate_stat for speed."""
#Always active stage changes
speed = self.calculate_stat(self.get_raw_speed(), self.speed_stage)
if self.nv.paralysis() and not self.ability() == Ability.QUICK_FEET:
speed //= 2
if self.held_item == "iron-ball":
speed //= 2
if self.owner.tailwind.active():
speed *= 2
if self.ability() == Ability.SLUSH_RUSH and battle.weather.get() == "hail":
speed *= 2
if self.ability() == Ability.SAND_RUSH and battle.weather.get() == "sandstorm":
speed *= 2
if self.ability() == Ability.SWIFT_SWIM and battle.weather.get() in ("rain", "h-rain"):
speed *= 2
if self.ability() == Ability.CHLOROPHYLL and battle.weather.get() in ("sun", "h-sun"):
speed *= 2
if self.ability() == Ability.SLOW_START and self.active_turns < 5:
speed *= .5
if self.ability() == Ability.UNBURDEN and not self.held_item.has_item() and self.held_item.ever_had_item:
speed *= 2
if self.ability() == Ability.QUICK_FEET and self.nv.current:
speed *= 1.5
if self.ability() == Ability.SURGE_SURFER and battle.terrain.item == "electric":
speed *= 2
if self.held_item == "choice-scarf":
speed *= 1.5
if all(self.get_raw_speed() > x for x in (self.get_raw_attack(), self.get_raw_defense(), self.get_raw_spatk(), self.get_raw_spdef())):
if (
(self.ability() == Ability.PROTOSYNTHESIS and (battle.weather.get() in ("sun", "h-sun") or self.booster_energy))
or (self.ability() == Ability.QUARK_DRIVE and (battle.terrain.item == "electric" or self.booster_energy))
):
speed *= 1.5
elif self.ability() in (Ability.PROTOSYNTHESIS, Ability.QUARK_DRIVE) and self.held_item == "booster-energy":
self.held_item.use()
self.booster_energy = True
speed *= 1.5
return speed
def get_accuracy(self, battle):
"""Helper method to calculate accuracy stage."""
return self.accuracy_stage
def get_evasion(self, battle):
"""Helper method to calculate evasion stage."""
return self.evasion_stage
def append_attack(self, stage_change: int, *, attacker=None, move=None, source: str="", check_looping: bool=True):
"""Helper method to call append_stat for attack."""
return self.append_stat(stage_change, attacker, move, "attack", source, check_looping)
def append_defense(self, stage_change: int, *, attacker=None, move=None, source: str="", check_looping: bool=True):
"""Helper method to call append_stat for defense."""
return self.append_stat(stage_change, attacker, move, "defense", source, check_looping)
def append_spatk(self, stage_change: int, *, attacker=None, move=None, source: str="", check_looping: bool=True):
"""Helper method to call append_stat for special attack."""
return self.append_stat(stage_change, attacker, move, "special attack", source, check_looping)
def append_spdef(self, stage_change: int, *, attacker=None, move=None, source: str="", check_looping: bool=True):
"""Helper method to call append_stat for special defense."""
return self.append_stat(stage_change, attacker, move, "special defense", source, check_looping)
def append_speed(self, stage_change: int, *, attacker=None, move=None, source: str="", check_looping: bool=True):
"""Helper method to call append_stat for speed."""
return self.append_stat(stage_change, attacker, move, "speed", source, check_looping)
def append_accuracy(self, stage_change: int, *, attacker=None, move=None, source: str="", check_looping: bool=True):
"""Helper method to call append_stat for accuracy."""
return self.append_stat(stage_change, attacker, move, "accuracy", source, check_looping)
def append_evasion(self, stage_change: int, *, attacker=None, move=None, source: str="", check_looping: bool=True):
"""Helper method to call append_stat for evasion."""
return self.append_stat(stage_change, attacker, move, "evasion", source, check_looping)
def append_stat(self, stage_change: int, attacker, move, stat: str, source: str, check_looping: bool=True):
"""
Adds a stat stage change to this pokemon.
Returns a formatted string describing the stat change.
"""
msg = ""
if self.substitute and attacker is not self and attacker is not None and (move is None or move.is_affected_by_substitute()):
return ""
if source:
source = f" from {source}"
delta_msgs = {
-3: f"{self.name}'s {stat} severely fell{source}!\n",
-2: f"{self.name}'s {stat} harshly fell{source}!\n",
-1: f"{self.name}'s {stat} fell{source}!\n",
1: f"{self.name}'s {stat} rose{source}!\n",
2: f"{self.name}'s {stat} rose sharply{source}!\n",
3: f"{self.name}'s {stat} rose drastically{source}!\n",
}
delta = stage_change
if self.ability(attacker=attacker, move=move) == Ability.SIMPLE:
delta *= 2
if self.ability(attacker=attacker, move=move) == Ability.CONTRARY:
delta *= -1
if stat == "attack":
current_stage = self.attack_stage
elif stat == "defense":
current_stage = self.defense_stage
elif stat == "special attack":
current_stage = self.spatk_stage
elif stat == "special defense":
current_stage = self.spdef_stage
elif stat == "speed":
current_stage = self.speed_stage
elif stat == "accuracy":
current_stage = self.accuracy_stage
elif stat == "evasion":
current_stage = self.evasion_stage
else:
raise ValueError(f"invalid stat {stat}")
# Cap stat stages within -6 to 6
if delta < 0:
#-6 -5 -4 .. 2
# 0 -1 -2 .. -8
cap = (current_stage * -1) - 6
delta = max(delta, cap)
if delta == 0:
return f"{self.name}'s {stat} won't go any lower!\n"
else:
# 6 5 4 .. -2
# 0 1 2 .. 8
cap = (current_stage * -1) + 6
delta = min(delta, cap)
if delta == 0:
return f"{self.name}'s {stat} won't go any higher!\n"
# Prevent stat changes
if delta < 0 and attacker is not self:
if self.ability(attacker=attacker, move=move) in (Ability.CLEAR_BODY, Ability.WHITE_SMOKE, Ability.FULL_METAL_BODY):
ability_name = Ability(self.ability_id).pretty_name
return f"{self.name}'s {ability_name} prevented its {stat} from being lowered!\n"
if self.ability(attacker=attacker, move=move) == Ability.HYPER_CUTTER and stat == "attack":
return f"{self.name}'s claws stayed sharp because of its hyper cutter!\n"
if self.ability(attacker=attacker, move=move) == Ability.KEEN_EYE and stat == "accuracy":
return f"{self.name}'s aim stayed true because of its keen eye!\n"
if self.ability(attacker=attacker, move=move) == Ability.MINDS_EYE and stat == "accuracy":
return f"{self.name}'s aim stayed true because of its mind's eye!\n"
if self.ability(attacker=attacker, move=move) == Ability.BIG_PECKS and stat == "defense":
return f"{self.name}'s defense stayed strong because of its big pecks!\n"
if self.owner.mist.active() and (attacker is None or attacker.ability() != Ability.INFILTRATOR):
return f"The mist around {self.name}'s feet prevented its {stat} from being lowered!\n"
if self.ability(attacker=attacker, move=move) == Ability.FLOWER_VEIL and ElementType.GRASS in self.type_ids:
return ""
if self.ability(attacker=attacker, move=move) == Ability.MIRROR_ARMOR and attacker is not None and check_looping:
msg += f"{self.name} reflected the stat change with its mirror armor!\n"
msg += attacker.append_stat(delta, self, None, stat, "", check_looping=False)
return msg
# Remember if stats were changed for certain moves
if delta > 0:
self.stat_increased = True
elif delta < 0:
self.stat_decreased = True
if stat == "attack":
self.attack_stage += delta
elif stat == "defense":
self.defense_stage += delta
elif stat == "special attack":
self.spatk_stage += delta
elif stat == "special defense":
self.spdef_stage += delta
elif stat == "speed":
self.speed_stage += delta
elif stat == "accuracy":
self.accuracy_stage += delta
elif stat == "evasion":
self.evasion_stage += delta
else:
raise ValueError(f"invalid stat {stat}")
formatted_delta = min(max(delta, -3), 3)
msg += delta_msgs[formatted_delta]
# TODO: fix this hacky way of doing this, but probably not until multi battles...
battle = self.held_item.battle
# Effects that happen after a pokemon gains stats
if delta < 0:
if attacker is not self:
if self.ability(attacker=attacker, move=move) == Ability.DEFIANT:
msg += self.append_attack(2, attacker=self, source="its defiance")
if self.ability(attacker=attacker, move=move) == Ability.COMPETITIVE:
msg += self.append_spatk(2, attacker=self, source="its competitiveness")
if self.held_item == "eject-pack":
# This assumes that neither attacker or poke are needed if not checking traps
swaps = self.owner.valid_swaps(..., ..., check_trap=False)
if swaps:
msg += f"{self.name} is switched out by its eject pack!\n"
self.held_item.use()
msg += self.remove(battle)
# Force this pokemon to immediately return to be attacked
self.owner.mid_turn_remove = True
elif delta > 0:
for poke in (battle.trainer1.current_pokemon, battle.trainer2.current_pokemon):
if poke is not None and poke is not self and poke.ability() == Ability.OPPORTUNIST and check_looping:
msg += f"{poke.name} seizes the opportunity to boost its stat with its opportunist!\n"
msg += poke.append_stat(delta, poke, None, stat, "", check_looping=False)
return msg
def grounded(self, battle, *, attacker=None, move=None):
"""
Returns True if this pokemon is considered "grounded".
Explicit grounding applies first, then explicit ungrounding, then implicit grounding.
"""
if battle.gravity.active():
return True
if self.held_item == "iron-ball":
return True
if self.grounded_by_move:
return True
if ElementType.FLYING in self.type_ids and not self.roost:
return False
if self.ability(attacker=attacker, move=move) == Ability.LEVITATE:
return False
if self.held_item == "air-balloon":
return False
if self.magnet_rise.active():
return False
if self.telekinesis.active():
return False
return True
def transform(self, otherpoke):
"""Transforms this poke into otherpoke."""
self.choice_move = None
self._name = otherpoke._name
if self._nickname != "None":
self.name = f"{self._nickname} ({self._name.replace('-', ' ')})"
else:
self.name = self._name.replace("-", " ")
self.attack = otherpoke.attack
self.defense = otherpoke.defense
self.spatk = otherpoke.spatk
self.spdef = otherpoke.spdef
self.speed = otherpoke.speed
self.hpiv = otherpoke.hpiv
self.atkiv = otherpoke.atkiv
self.defiv = otherpoke.defiv
self.spatkiv = otherpoke.spatkiv
self.spdefiv = otherpoke.spdefiv
self.speediv = otherpoke.speediv
self.hpev = otherpoke.hpev
self.atkev = otherpoke.atkev
self.defev = otherpoke.defev
self.spatkev = otherpoke.spatkev
self.spdefev = otherpoke.spdefev
self.speedev = otherpoke.speedev
self.moves = [move.copy() for move in otherpoke.moves]
for m in self.moves:
m.pp = 5
self.ability_id = otherpoke.ability_id
self.type_ids = otherpoke.type_ids.copy()
self.attack_stage = otherpoke.attack_stage
self.defense_stage = otherpoke.defense_stage
self.spatk_stage = otherpoke.spatk_stage
self.spdef_stage = otherpoke.spdef_stage
self.speed_stage = otherpoke.speed_stage
self.accuracy_stage = otherpoke.accuracy_stage
self.evasion_stage = otherpoke.evasion_stage
def form(self, form: str):
"""
Changes this poke's form to the provided form.
This changes its name and base stats, and may affect moves, abilities, and items.
Returns True if the poke successfully reformed.
"""
if form not in self.base_stats:
return False
self._name = form
if self._nickname != "None":
self.name = f"{self._nickname} ({self._name.replace('-', ' ')})"
else:
self.name = self._name.replace("-", " ")
self.attack = self.base_stats[self._name][1]
self.defense = self.base_stats[self._name][2]
self.spatk = self.base_stats[self._name][3]
self.spdef = self.base_stats[self._name][4]
self.speed = self.base_stats[self._name][5]
self.attack_split = None
self.spatk_split = None
self.defense_split = None
self.spdef_split = None
self.autotomize = 0
return True
def effectiveness(self, attacker_type: ElementType, battle, *, attacker=None, move=None):
"""Calculates a float representing the effectiveness of `attacker_type` damage on this poke."""
if attacker_type == ElementType.TYPELESS:
return 1
effectiveness = 1
for defender_type in self.type_ids:
if defender_type == ElementType.TYPELESS:
continue
if move is not None and move.effect == 380 and defender_type == ElementType.WATER:
effectiveness *= 2
continue
if move is not None and move.effect == 373 and defender_type == ElementType.FLYING and not self.grounded(battle, attacker=attacker, move=move):
return 1 # Ignores secondary types if defender is flying type and not grounded
if self.roost and defender_type == ElementType.FLYING:
continue
if self.foresight and attacker_type in (ElementType.FIGHTING, ElementType.NORMAL) and defender_type == ElementType.GHOST:
continue
if self.miracle_eye and attacker_type == ElementType.PSYCHIC and defender_type == ElementType.DARK:
continue
if (
attacker_type in (ElementType.FIGHTING, ElementType.NORMAL)
and defender_type == ElementType.GHOST
and attacker is not None
and attacker.ability() in [Ability.SCRAPPY, Ability.MINDS_EYE]
):
continue
if attacker_type == ElementType.GROUND and defender_type == ElementType.FLYING and self.grounded(battle, attacker=attacker, move=move):
continue
e = battle.type_effectiveness[(attacker_type, defender_type)] / 100
if defender_type == ElementType.FLYING and e > 1 and move is not None and battle.weather.get() == "h-wind":
e = 1
if battle.inverse_battle:
if e < 1:
e = 2
elif e > 1:
e = 0.5
effectiveness *= e
if attacker_type == ElementType.FIRE and self.tar_shot:
effectiveness *= 2
if effectiveness >= 1 and self.hp == self.starting_hp and self.ability(attacker=attacker, move=move) == Ability.TERA_SHELL:
effectiveness = 0.5
return effectiveness
def weight(self, *, attacker=None, move=None):
"""
Returns this pokemon's current weight.
Dynamically modifies the weight based on the ability of this pokemon.
"""
cur_ability = self.ability(attacker=attacker, move=move)
cur_weight = self.starting_weight
if cur_ability == Ability.HEAVY_METAL:
cur_weight *= 2
if cur_ability == Ability.LIGHT_METAL:
cur_weight //= 2
cur_weight = max(1, cur_weight)
cur_weight -= self.autotomize * 1000
cur_weight = max(1, cur_weight)
return cur_weight
def ability(self, *, attacker=None, move=None):
"""
Returns this pokemon's current ability.
Returns None in cases where the ability is blocked or nullified.
"""
# Currently there are two categories of ability ignores, and both only apply when a move is used.
# Since this could change, the method signature is flexable. However, without both present, it
# should not consider the existing options.
if move is None or attacker is None or attacker is self:
return self.ability_id
if not self.ability_ignorable():
return self.ability_id
if move.effect in (411, 460):
return None
if attacker.ability_id in (Ability.MOLD_BREAKER, Ability.TURBOBLAZE, Ability.TERAVOLT, Ability.NEUTRALIZING_GAS):
return None
if attacker.ability_id == Ability.MYCELIUM_MIGHT and move.damage_class == DamageClass.STATUS:
return None
return self.ability_id
def ability_changeable(self):
"""Returns True if this pokemon's current ability can be changed."""
return self.ability_id not in (
Ability.MULTITYPE, Ability.STANCE_CHANGE, Ability.SCHOOLING, Ability.COMATOSE,
Ability.SHIELDS_DOWN, Ability.DISGUISE, Ability.RKS_SYSTEM, Ability.BATTLE_BOND,
Ability.POWER_CONSTRUCT, Ability.ICE_FACE, Ability.GULP_MISSILE, Ability.ZERO_TO_HERO
)
def ability_giveable(self):
"""Returns True if this pokemon's current ability can be given to another pokemon."""
return self.ability_id not in (
Ability.TRACE, Ability.FORECAST, Ability.FLOWER_GIFT, Ability.ZEN_MODE, Ability.ILLUSION,
Ability.IMPOSTER, Ability.POWER_OF_ALCHEMY, Ability.RECEIVER, Ability.DISGUISE, Ability.STANCE_CHANGE,
Ability.POWER_CONSTRUCT, Ability.ICE_FACE, Ability.HUNGER_SWITCH, Ability.GULP_MISSILE, Ability.ZERO_TO_HERO
)
def ability_ignorable(self):
"""Returns True if this pokemon's current ability can be ignored."""
return self.ability_id in (
Ability.AROMA_VEIL, Ability.BATTLE_ARMOR, Ability.BIG_PECKS, Ability.BULLETPROOF, Ability.CLEAR_BODY,
Ability.CONTRARY, Ability.DAMP, Ability.DAZZLING, Ability.DISGUISE, Ability.DRY_SKIN, Ability.FILTER,
Ability.FLASH_FIRE, Ability.FLOWER_GIFT, Ability.FLOWER_VEIL, Ability.FLUFFY, Ability.FRIEND_GUARD,
Ability.FUR_COAT, Ability.HEATPROOF, Ability.HEAVY_METAL, Ability.HYPER_CUTTER, Ability.ICE_FACE,
Ability.ICE_SCALES, Ability.IMMUNITY, Ability.INNER_FOCUS, Ability.INSOMNIA, Ability.KEEN_EYE,
Ability.LEAF_GUARD, Ability.LEVITATE, Ability.LIGHT_METAL, Ability.LIGHTNING_ROD, Ability.LIMBER,
Ability.MAGIC_BOUNCE, Ability.MAGMA_ARMOR, Ability.MARVEL_SCALE, Ability.MIRROR_ARMOR, Ability.MOTOR_DRIVE,
Ability.MULTISCALE, Ability.OBLIVIOUS, Ability.OVERCOAT, Ability.OWN_TEMPO, Ability.PASTEL_VEIL,
Ability.PUNK_ROCK, Ability.QUEENLY_MAJESTY, Ability.SAND_VEIL, Ability.SAP_SIPPER, Ability.SHELL_ARMOR,
Ability.SHIELD_DUST, Ability.SIMPLE, Ability.SNOW_CLOAK, Ability.SOLID_ROCK, Ability.SOUNDPROOF,
Ability.STICKY_HOLD, Ability.STORM_DRAIN, Ability.STURDY, Ability.SUCTION_CUPS, Ability.SWEET_VEIL,
Ability.TANGLED_FEET, Ability.TELEPATHY, Ability.THICK_FAT, Ability.UNAWARE, Ability.VITAL_SPIRIT,
Ability.VOLT_ABSORB, Ability.WATER_ABSORB, Ability.WATER_BUBBLE, Ability.WATER_VEIL, Ability.WHITE_SMOKE,
Ability.WONDER_GUARD, Ability.WONDER_SKIN, Ability.ARMOR_TAIL, Ability.EARTH_EATER, Ability.GOOD_AS_GOLD,
Ability.PURIFYING_SALT, Ability.WELL_BAKED_BODY
)
def get_assist_move(self):
"""
Returns a Move that can be used with assist, or None if none exists.
This selects a random move from the pool of moves from pokes in the user's party that are eligable.
"""
moves = []
for idx, poke in enumerate(self.owner.party):
if idx == self.owner.last_idx:
continue
for move in poke.moves:
if move.selectable_by_assist():
moves.append(move)
if not moves:
return None
return random.choice(moves)
@classmethod
async def create(cls, ctx, raw_data: dict):
"""Creates a new DuelPokemon object using the raw data provided."""
pn = raw_data["pokname"]
nick = raw_data["poknick"]
hpiv = min(31, raw_data["hpiv"])
atkiv = min(31, raw_data["atkiv"])
defiv = min(31, raw_data["defiv"])
spatkiv = min(31, raw_data["spatkiv"])
spdefiv = min(31, raw_data["spdefiv"])
speediv = min(31, raw_data["speediv"])
hpev = raw_data["hpev"]
atkev = raw_data["atkev"]
defev = raw_data["defev"]
spaev = raw_data["spatkev"]
spdev = raw_data["spdefev"]
speedev = raw_data["speedev"]
plevel = raw_data["pokelevel"]
shiny = raw_data["shiny"]
radiant = raw_data["radiant"]
skin = raw_data["skin"]
id = raw_data["id"]
hitem = raw_data["hitem"]
happiness = raw_data["happiness"]
moves = raw_data["moves"]
ab_index = raw_data["ability_index"]
nature = raw_data["nature"]
gender = raw_data["gender"]
nature = await find_one(ctx, "natures", {"identifier": nature.lower()})
dec_stat_id = nature["decreased_stat_id"]
inc_stat_id = nature["increased_stat_id"]
dec_stat = await find_one(ctx, "stat_types", {"id": dec_stat_id})
inc_stat = await find_one(ctx, "stat_types", {"id": inc_stat_id})
dec_stat = dec_stat["identifier"].capitalize().replace("-", " ")
inc_stat = inc_stat["identifier"].capitalize().replace("-", " ")
nature_stat_deltas = {"Attack": 1, "Defense": 1, "Special attack": 1, "Special defense": 1, "Speed": 1}
flavor_map = {
"Attack": "spicy",
"Defense": "sour",
"Speed": "sweet",
"Special attack": "dry",
"Special defense": "bitter",
}
disliked_flavor = ""
if dec_stat != inc_stat:
nature_stat_deltas[dec_stat] = 0.9
nature_stat_deltas[inc_stat] = 1.1
disliked_flavor = flavor_map[dec_stat]
#Deform pokes that are formed into battle forms that they should not start off in
if pn == "Mimikyu-busted":
pn = "Mimikyu"
if pn in ("Cramorant-gorging", "Cramorant-gulping"):
pn = "Cramorant"
if pn == "Eiscue-noice":
pn = "Eiscue"
if pn == "Darmanitan-zen":
pn = "Darmanitan"
if pn == "Darmanitan-zen-galar":
pn = "Darmanitan-galar"
if pn == "Aegislash-blade":
pn = "Aegislash"
if pn in ("Minior-red", "Minior-orange", "Minior-yellow", "Minior-green", "Minior-blue", "Minior-indigo", "Minior-violet"):
pn = "Minior"
if pn == "Wishiwashi" and plevel >= 20:
pn = "Wishiwashi-school"
if pn == "Wishiwashi-school" and plevel < 20:
pn = "Wishiwashi"
if pn == "Greninja-ash":
pn = "Greninja"
if pn == "Zygarde-complete":
pn = "Zygarde"
if pn == "Morpeko-hangry":
pn = "Morpeko"
if pn == "Cherrim-sunshine":
pn = "Cherrim"
if pn in ("Castform-snowy", "Castform-rainy", "Castform-sunny"):
pn = "Castform"
if pn in (
"Arceus-dragon",
"Arceus-dark",
"Arceus-ground",
"Arceus-fighting",
"Arceus-fire",
"Arceus-ice",
"Arceus-bug",
"Arceus-steel",
"Arceus-grass",
"Arceus-psychic",
"Arceus-fairy",
"Arceus-flying",
"Arceus-water",
"Arceus-ghost",
"Arceus-rock",
"Arceus-poison",
"Arceus-electric",
):
pn = "Arceus"
if pn in (
"Silvally-psychic",
"Silvally-fairy",
"Silvally-flying",
"Silvally-water",
"Silvally-ghost",
"Silvally-rock",
"Silvally-poison",
"Silvally-electric",
"Silvally-dragon",
"Silvally-dark",
"Silvally-ground",
"Silvally-fighting",
"Silvally-fire",
"Silvally-ice",
"Silvally-bug",
"Silvally-steel",
"Silvally-grass",
):
pn = "Silvally"
if pn == "Palafin-hero":
pn = "Palafin"
if pn.endswith("-mega-x") or pn.endswith("-mega-y"):
pn = pn[:-7]
if pn.endswith("-mega"):
pn = pn[:-5]
#TODO: Meloetta, Shaymin
form_info = await find_one(ctx, "forms", {"identifier": pn.lower()})
#List of type ids
type_ids = (await find_one(ctx, "ptypes", {"id": form_info["pokemon_id"]}))["types"]
#6 element list of stat values (int)
stats = (await find_one(ctx, "pokemon_stats", {"pokemon_id": form_info["pokemon_id"]}))["stats"]
pokemonHp = stats[0]
#Store the base stats for all forms of this poke
base_stats = {}
base_stats[pn] = stats
extra_forms = []
if pn == "Mimikyu":
extra_forms = ["Mimikyu-busted"]
if pn == "Cramorant":
extra_forms = ["Cramorant-gorging", "Cramorant-gulping"]
if pn == "Eiscue":
extra_forms = ["Eiscue-noice"]
if pn == "Darmanitan":
extra_forms = ["Darmanitan-zen"]
if pn == "Darmanitan-galar":
extra_forms = ["Darmanitan-zen-galar"]
if pn == "Aegislash":
extra_forms = ["Aegislash-blade"]
if pn == "Minior":
extra_forms = ["Minior-red", "Minior-orange", "Minior-yellow", "Minior-green", "Minior-blue", "Minior-indigo", "Minior-violet"]
if pn == "Wishiwashi":
extra_forms = ["Wishiwashi-school"]
if pn == "Wishiwashi-school":
extra_forms = ["Wishiwashi"]
if pn == "Greninja":
extra_forms = ["Greninja-ash"]
if pn == "Zygarde":
extra_forms = ["Zygarde-complete"]
if pn == "Zygarde-10":
extra_forms = ["Zygarde-complete"]
if pn == "Morpeko":
extra_forms = ["Morpeko-hangry"]
if pn == "Cherrim":
extra_forms = ["Cherrim-sunshine"]
if pn == "Castform":
extra_forms = ["Castform-snowy", "Castform-rainy", "Castform-sunny"]
if pn == "Arceus":
extra_forms = [
"Arceus-dragon",
"Arceus-dark",
"Arceus-ground",
"Arceus-fighting",
"Arceus-fire",
"Arceus-ice",
"Arceus-bug",
"Arceus-steel",
"Arceus-grass",
"Arceus-psychic",
"Arceus-fairy",
"Arceus-flying",
"Arceus-water",
"Arceus-ghost",
"Arceus-rock",
"Arceus-poison",
"Arceus-electric",
]
if pn == "Silvally":
extra_forms = [
"Silvally-psychic",
"Silvally-fairy",
"Silvally-flying",
"Silvally-water",
"Silvally-ghost",
"Silvally-rock",
"Silvally-poison",
"Silvally-electric",
"Silvally-dragon",
"Silvally-dark",
"Silvally-ground",
"Silvally-fighting",
"Silvally-fire",
"Silvally-ice",
"Silvally-bug",
"Silvally-steel",
"Silvally-grass",
]
if pn == "Palafin":
extra_forms = ["Palafin-hero"]
mega_form = None
mega_ability_id = None
mega_type_ids = None
if pn != "Rayquaza":
if hitem == "mega-stone":
mega_form = pn + "-mega"
elif hitem == "mega-stone-x":
mega_form = pn + "-mega-x"
elif hitem == "mega-stone-y":
mega_form = pn + "-mega-y"
else:
if "dragon-ascent" in moves:
mega_form = pn + "-mega"
if mega_form is not None:
mega_form_info = await find_one(ctx, "forms", {"identifier": mega_form.lower()})
if mega_form_info is not None:
mega_ability = await find_one(ctx, "poke_abilities", {"pokemon_id": mega_form_info["pokemon_id"]})
if mega_ability is None:
raise ValueError("mega form missing ability in `poke_abilities`")
mega_ability_id = mega_ability["ability_id"]
mega_types = (await find_one(ctx, "ptypes", {"id": mega_form_info["pokemon_id"]}))
if mega_types is None:
raise ValueError("mega form missing types in `ptypes`")
mega_type_ids = mega_types["types"]
extra_forms.append(mega_form)
for f_name in extra_forms:
f_info = await find_one(ctx, "forms", {"identifier": f_name.lower()})
f_stats = (await find_one(ctx, "pokemon_stats", {"pokemon_id": f_info["pokemon_id"]}))["stats"]
base_stats[f_name] = f_stats
#Builds a list of the possible ability ids for this poke, `ab_index` is the currently selected ability from this list
ab_ids = []
for record in await find(ctx, "poke_abilities", {"pokemon_id": form_info["pokemon_id"]}):
ab_ids.append(record["ability_id"])
try:
ab_id = ab_ids[ab_index]
#Should never happen, but better safe than sorry
except IndexError:
ab_id = ab_ids[0]
if any(
pn.endswith(suffix)
for suffix in [
"-bug","-summer","-marine","-elegant","-poison","-average","-altered","-winter","-trash","-incarnate",
"-baile","-rainy","-steel","-star","-ash","-diamond","-pop-star","-fan","-school","-therian","-pau",
"-river","-poke-ball","-kabuki","-electric","-heat","-unbound","-chill","-archipelago","-zen","-normal",
"-mega-y","-resolute","-blade","-speed","-indigo","-dusk","-sky","-west","-sun","-dandy","-solo","-high-plains",
"-la-reine","-50","-unova-cap","-burn","-mega-x","-monsoon","-primal","-red-striped","-blue-striped",
"-white-striped","-ground","-super","-yellow","-polar","-cosplay","-ultra","-heart","-snowy","-sensu",
"-eternal","-douse","-defense","-sunshine","-psychic","-modern","-natural","-tundra","-flying","-pharaoh",
"-libre","-sunny","-autumn","-10","-orange","-standard","-land","-partner","-dragon","-plant","-pirouette",
"-male","-hoenn-cap","-violet","-spring","-fighting","-sandstorm","-original-cap","-neutral","-fire",
"-fairy","-attack","-black","-shock","-shield","-shadow","-grass","-continental","-overcast","-disguised",
"-exclamation","-origin","-garden","-blue","-matron","-red-meteor","-small","-rock-star","-belle",
"-alola-cap","-green","-active","-red","-mow","-icy-snow","-debutante","-east","-midday","-jungle","-frost",
"-midnight","-rock","-fancy","-busted","-ordinary","-water","-phd","-ice","-spiky-eared","-savanna","-original",
"-ghost","-meadow","-dawn","-question","-pom-pom","-female","-kalos-cap","-confined","-sinnoh-cap","-aria",
"-dark","-ocean","-wash","-white","-mega","-sandy","-complete","-large","-crowned","-ice-rider","-shadow-rider",
"-zen-galar","-rapid-strike","-noice","-hangry",
]
):
name = pn.lower().split("-")[0]
pid = (await find_one(ctx, "forms", {"identifier": name}))["pokemon_id"]
else:
pid = form_info["pokemon_id"]
#True if any possible future evo exists
can_still_evolve = bool(await find_one(ctx, "pfile", {"evolves_from_species_id": pid}))
#Unreleased pokemon that is treated like a form in the bot, monkeypatch fix.
if pn == "Floette-eternal":
can_still_evolve = False
#This stat can (/has to) be calculated ahead of time, as it does not change between forms.
#If transform copied HP, I would probably take up drinking...
pokemonHp = round((((2 * pokemonHp + hpiv + (hpev / 4)) * plevel) / 100) + plevel + 10)
hitem = await find_one(ctx, "items", {"identifier": hitem})
if pn == "Shedinja":
pokemonHp = 1
weight = (await find_one(ctx, "forms", {"identifier": pn.lower()}))["weight"]
if weight is None:
weight = 20
object_moves = []
for move in moves:
type_override = None
if move.startswith("hidden-power-"):
element = move.split("-")[2]
move = "hidden-power"
type_override = ElementType[element.upper()]
move = await find_one(ctx, "moves", {"identifier": move})
if move is None:
move = await find_one(ctx, "moves", {"identifier": "tackle"})
else:
if type_override is not None:
move["type_id"] = type_override
object_moves.append(Move(**move))
p = cls(
pokemon_id=pid,
name=pn,
nickname=nick,
base_stats=base_stats,
hp=pokemonHp,
hpiv=hpiv,
atkiv=atkiv,
defiv=defiv,
spatkiv=spatkiv,
spdefiv=spdefiv,
speediv=speediv,
hpev=hpev,
atkev=atkev,
defev=defev,
spatkev=spaev,
spdefev=spdev,
speedev=speedev,
level=plevel,
nature_stat_deltas=nature_stat_deltas,
shiny=shiny,
radiant=radiant,
skin=skin,
type_ids=type_ids,
mega_type_ids=mega_type_ids,
id=id,
held_item=hitem,
happiness=happiness,
moves=object_moves,
ability_id=ab_id,
mega_ability_id=mega_ability_id,
weight=weight,
gender=gender,
can_still_evolve=can_still_evolve,
disliked_flavor=disliked_flavor,
)
return p
def __repr__(self):
return f"DuelPokemon(name={self._name!r}, hp={self.hp!r})"