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

491 lines
22 KiB
Python

import asyncio
import random
from .buttons import SwapPromptView
from .data import generate_main_battle_message, generate_text_battle_message, find
from .enums import Ability, DamageClass
from .misc import ExpiringEffect, Weather, Terrain
class Battle():
"""
Represents a battle between two trainers and their pokemon.
This object holds all necessary information for a battle & runs the battle.
"""
def __init__(self, ctx, channel, trainer1, trainer2, *, inverse_battle=False):
self.ctx = ctx
self.channel = channel
self.trainer1 = trainer1
for poke in trainer1.party:
poke.held_item.battle = self
self.trainer2 = trainer2
for poke in trainer2.party:
poke.held_item.battle = self
self.bg_num = random.randint(1, 4)
self.trick_room = ExpiringEffect(0)
self.magic_room = ExpiringEffect(0)
self.wonder_room = ExpiringEffect(0)
self.gravity = ExpiringEffect(0)
self.weather = Weather(self)
self.terrain = Terrain(self)
self.plasma_fists = False
self.turn = 0
self.last_move_effect = None
self.metronome_moves_raw = []
#(AttackerType, DefenderType): Effectiveness
self.type_effectiveness = {}
self.inverse_battle = inverse_battle
self.msg = ""
async def run(self):
"""Runs the duel."""
self.msg = ""
# Moves which are immune to metronome
immune_ids = [
68, 102, 119, 144, 165, 166, 168, 173, 182, 194, 197, 203, 214, 243, 264, 266,
267, 270, 271, 274, 289, 343, 364, 382, 383, 415, 448, 469, 476, 495, 501, 511,
516, 546, 547, 548, 553, 554, 555, 557, 561, 562, 578, 588, 591, 592, 593, 596,
606, 607, 614, 615, 617, 621, 661, 671, 689, 690, 704, 705, 712, 720, 721, 722
]
# Moves which are not coded in the bot
uncoded_ids = [
266, 270, 476, 495, 502, 511, 597, 602, 603, 607, 622, 623, 624, 625, 626, 627,
628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643,
644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 671,
695, 696, 697, 698, 699, 700, 701, 702, 703, 719, 723, 724, 725, 726, 727, 728,
811, 10001, 10002, 10003, 10004, 10005, 10006, 10007, 10008, 10009, 10010, 10011,
10012, 10013, 10014, 10015, 10016, 10017, 10018
]
ignored_ids = list(set(immune_ids) | set(uncoded_ids))
self.metronome_moves_raw = await find(self.ctx, "moves", {"id": {"$nin": ignored_ids}})
for te in await find(self.ctx, "type_effectiveness", {}):
self.type_effectiveness[(te["damage_type_id"], te["target_type_id"])] = te["damage_factor"]
#This calculation only uses the primative speed attr as the pokes have not been fully initiaized yet.
if self.trainer1.current_pokemon.get_raw_speed() > self.trainer2.current_pokemon.get_raw_speed():
self.msg += self.trainer1.current_pokemon.send_out(self.trainer2.current_pokemon, self)
self.msg += self.trainer2.current_pokemon.send_out(self.trainer1.current_pokemon, self)
else:
self.msg += self.trainer2.current_pokemon.send_out(self.trainer1.current_pokemon, self)
self.msg += self.trainer1.current_pokemon.send_out(self.trainer2.current_pokemon, self)
await self.send_msg()
winner = None
while True:
# Swap pokes for any users w/o an active poke
while self.trainer1.current_pokemon is None or self.trainer2.current_pokemon is None:
swapped1 = False
swapped2 = False
if self.trainer1.current_pokemon is None:
swapped1 = True
winner = await self.run_swap(self.trainer1, self.trainer2)
if winner:
break
if self.trainer2.current_pokemon is None:
swapped2 = True
winner = await self.run_swap(self.trainer2, self.trainer1)
if winner:
break
# Send out the pokes that were just swapped to
if swapped1 and swapped2:
if self.trainer1.current_pokemon.get_raw_speed() > self.trainer2.current_pokemon.get_raw_speed():
self.msg += self.trainer1.current_pokemon.send_out(self.trainer2.current_pokemon, self)
if not self.trainer1.has_alive_pokemon():
self.msg += f"{self.trainer2.name} wins!\n"
winner = self.trainer2
break
self.msg += self.trainer2.current_pokemon.send_out(self.trainer1.current_pokemon, self)
if not self.trainer2.has_alive_pokemon():
self.msg += f"{self.trainer1.name} wins!\n"
winner = self.trainer1
break
else:
self.msg += self.trainer2.current_pokemon.send_out(self.trainer1.current_pokemon, self)
if not self.trainer2.has_alive_pokemon():
self.msg += f"{self.trainer1.name} wins!\n"
winner = self.trainer1
break
self.msg += self.trainer1.current_pokemon.send_out(self.trainer2.current_pokemon, self)
if not self.trainer1.has_alive_pokemon():
self.msg += f"{self.trainer2.name} wins!\n"
winner = self.trainer2
break
elif swapped1:
self.msg += self.trainer1.current_pokemon.send_out(self.trainer2.current_pokemon, self)
if not self.trainer1.has_alive_pokemon():
self.msg += f"{self.trainer2.name} wins!\n"
winner = self.trainer2
break
elif swapped2:
self.msg += self.trainer2.current_pokemon.send_out(self.trainer1.current_pokemon, self)
if not self.trainer2.has_alive_pokemon():
self.msg += f"{self.trainer1.name} wins!\n"
winner = self.trainer1
break
# Handle breaking out of the main game loop when a winner happens in the poke select loop
if winner:
break
# Get trainer actions
await self.send_msg()
self.trainer1.event.clear()
self.trainer2.event.clear()
if not self.trainer1.is_human():
self.trainer1.move(self.trainer2.current_pokemon, self)
if not self.trainer2.is_human():
self.trainer2.move(self.trainer1.current_pokemon, self)
battle_view = await generate_main_battle_message(self)
await self.trainer1.event.wait()
await self.trainer2.event.wait()
battle_view.stop()
# Check for forfeits
if self.trainer1.selected_action is None and self.trainer2.selected_action is None:
await self.channel.send("Both players forfeited...")
return #TODO: ???
if self.trainer1.selected_action is None:
self.msg += f"{self.trainer1.name} forfeited, {self.trainer2.name} wins!\n"
winner = self.trainer2
break
if self.trainer2.selected_action is None:
self.msg += f"{self.trainer2.name} forfeited, {self.trainer1.name} wins!\n"
winner = self.trainer1
break
# Run setup for both pokemon
t1, t2 = self.who_first()
if t1.current_pokemon is not None and t2.current_pokemon is not None:
if not isinstance(t1.selected_action, int):
self.msg += t1.selected_action.setup(t1.current_pokemon, t2.current_pokemon, self)
if not t1.has_alive_pokemon():
self.msg += f"{t2.name} wins!\n"
winner = t2
break
if not t2.has_alive_pokemon():
self.msg += f"{t1.name} wins!\n"
winner = t1
break
if t1.current_pokemon is not None and t2.current_pokemon is not None:
if not isinstance(t2.selected_action, int):
self.msg += t2.selected_action.setup(t2.current_pokemon, t1.current_pokemon, self)
if not t2.has_alive_pokemon():
self.msg += f"{t1.name} wins!\n"
winner = t1
break
if not t1.has_alive_pokemon():
self.msg += f"{t2.name} wins!\n"
winner = t2
break
# Run moves for both pokemon
# Trainer 1's move
ran_megas = False
if not isinstance(t1.selected_action, int):
self.handle_megas(t1, t2)
ran_megas = True
if t1.current_pokemon is not None and t2.current_pokemon is not None:
if isinstance(t1.selected_action, int):
self.msg += t1.current_pokemon.remove(self)
t1.switch_poke(t1.selected_action, mid_turn=True)
self.msg += t1.current_pokemon.send_out(t2.current_pokemon, self)
if t1.current_pokemon is not None:
t1.current_pokemon.has_moved = True
else:
self.msg += t1.selected_action.use(t1.current_pokemon, t2.current_pokemon, self)
if not t1.has_alive_pokemon():
self.msg += f"{t2.name} wins!\n"
winner = t2
break
if not t2.has_alive_pokemon():
self.msg += f"{t1.name} wins!\n"
winner = t1
break
# Pokes who die do NOT get attacked, but pokes who retreat *do*
if t1.mid_turn_remove:
winner = await self.run_swap(t1, t2, mid_turn=True)
if winner:
break
# EDGE CASE - Moves that DO NOT target the opponent (and swapping) SHOULD run
# even if there is no other poke on the field. Right now everything is hardcoded
# to require two pokes to work, on a rewrite the `use` function should be the
# one handling the job of checking if the attacked poke is `None` before using a
# move that targets opponents.
if (
t1.current_pokemon is None and t2.current_pokemon is not None
and (isinstance(t2.selected_action, int) or not t2.selected_action.targets_opponent())
):
winner = await self.run_swap(t1, t2, mid_turn=True)
if winner:
break
self.msg += "\n"
# Trainer 2's move
if not ran_megas and not isinstance(t2.selected_action, int):
self.handle_megas(t1, t2)
ran_megas = True
if t1.current_pokemon is not None and t2.current_pokemon is not None:
if isinstance(t2.selected_action, int):
self.msg += t2.current_pokemon.remove(self)
t2.switch_poke(t2.selected_action, mid_turn=True)
self.msg += t2.current_pokemon.send_out(t1.current_pokemon, self)
if t2.current_pokemon is not None:
t2.current_pokemon.has_moved = True
else:
self.msg += t2.selected_action.use(t2.current_pokemon, t1.current_pokemon, self)
if not t2.has_alive_pokemon():
self.msg += f"{t1.name} wins!\n"
winner = t1
break
if not t1.has_alive_pokemon():
self.msg += f"{t2.name} wins!\n"
winner = t2
break
self.msg += "\n"
if t2.mid_turn_remove:
# This DOES need to be here, otherwise end of turn effects aren't handled right
winner = await self.run_swap(t2, t1, mid_turn=True)
if winner:
break
if not t2.has_alive_pokemon():
self.msg += f"{t1.name} wins!\n"
winner = t1
break
if not t1.has_alive_pokemon():
self.msg += f"{t2.name} wins!\n"
winner = t2
break
if not ran_megas:
self.handle_megas(t1, t2)
#Progress turns
self.turn += 1
self.plasma_fists = False
if self.weather.next_turn():
self.msg += "The weather cleared!\n"
if self.terrain.next_turn():
self.msg += "The terrain cleared!\n"
self.last_move_effect = None
t1, t2 = self.who_first(False)
self.msg += t1.next_turn(self)
if t1.current_pokemon is not None:
self.msg += t1.current_pokemon.next_turn(t2.current_pokemon, self)
if not t1.has_alive_pokemon():
self.msg += f"{t2.name} wins!\n"
winner = t2
break
if not t2.has_alive_pokemon():
self.msg += f"{t1.name} wins!\n"
winner = t1
break
self.msg += t2.next_turn(self)
if t2.current_pokemon is not None:
self.msg += t2.current_pokemon.next_turn(t1.current_pokemon, self)
if not t2.has_alive_pokemon():
self.msg += f"{t1.name} wins!\n"
winner = t1
break
if not t1.has_alive_pokemon():
self.msg += f"{t2.name} wins!\n"
winner = t2
break
if self.trick_room.next_turn():
self.msg += "The Dimensions returned back to normal!\n"
if self.gravity.next_turn():
self.msg += "Gravity returns to normal!\n"
if self.magic_room.next_turn():
self.msg += "The room returns to normal, and held items regain their effect!\n"
if self.wonder_room.next_turn():
self.msg += "The room returns to normal, and stats swap back to what they were before!\n"
#The game is over, and we broke out before sending, send the remaining cache
await self.send_msg()
return winner
def who_first(self, check_move=True):
"""
Determines which move should go.
Returns the two trainers and their moves, in the order they should go.
"""
T1FIRST = (self.trainer1, self.trainer2)
T2FIRST = (self.trainer2, self.trainer1)
if self.trainer1.current_pokemon is None or self.trainer2.current_pokemon is None:
return T1FIRST
speed1 = self.trainer1.current_pokemon.get_speed(self)
speed2 = self.trainer2.current_pokemon.get_speed(self)
#Pokes that are switching go before pokes making other moves
if check_move:
if isinstance(self.trainer1.selected_action, int) and isinstance(self.trainer2.selected_action, int):
if self.trainer1.current_pokemon.get_raw_speed() > self.trainer2.current_pokemon.get_raw_speed():
return T1FIRST
return T2FIRST
if isinstance(self.trainer1.selected_action, int):
return T1FIRST
if isinstance(self.trainer2.selected_action, int):
return T2FIRST
#Priority brackets & abilities
if check_move:
prio1 = self.trainer1.selected_action.get_priority(self.trainer1.current_pokemon, self.trainer2.current_pokemon, self)
prio2 = self.trainer2.selected_action.get_priority(self.trainer2.current_pokemon, self.trainer1.current_pokemon, self)
if prio1 > prio2:
return T1FIRST
if prio2 > prio1:
return T2FIRST
t1_quick = False
t2_quick = False
# Quick draw/claw
if (
self.trainer1.current_pokemon.ability() == Ability.QUICK_DRAW
and self.trainer1.selected_action.damage_class != DamageClass.STATUS
and random.randint(1, 100) <= 30
):
t1_quick = True
if (
self.trainer2.current_pokemon.ability() == Ability.QUICK_DRAW
and self.trainer2.selected_action.damage_class != DamageClass.STATUS
and random.randint(1, 100) <= 30
):
t2_quick = True
if (
self.trainer1.current_pokemon.held_item == "quick-claw"
and random.randint(1, 100) <= 20
):
t1_quick = True
if (
self.trainer2.current_pokemon.held_item == "quick-claw"
and random.randint(1, 100) <= 20
):
t2_quick = True
#if both pokemon activate a quick, priority bracket proceeds as normal
if t1_quick and not t2_quick:
return T1FIRST
if t2_quick and not t1_quick:
return T2FIRST
# Move last in prio bracket
t1_slow = False
t2_slow = False
if self.trainer1.current_pokemon.ability() == Ability.STALL:
t1_slow = True
if (
self.trainer1.current_pokemon.ability() == Ability.MYCELIUM_MIGHT
and self.trainer1.selected_action.damage_class == DamageClass.STATUS
):
t1_slow = True
if self.trainer2.current_pokemon.ability() == Ability.STALL:
t2_slow = True
if (
self.trainer2.current_pokemon.ability() == Ability.MYCELIUM_MIGHT
and self.trainer2.selected_action.damage_class == DamageClass.STATUS
):
t2_slow = True
if t1_slow and t2_slow:
if speed1 == speed2:
return random.choice([T1FIRST, T2FIRST])
if speed1 > speed2:
return T2FIRST
return T1FIRST
if t1_slow:
return T2FIRST
if t2_slow:
return T1FIRST
#Equal speed
if speed1 == speed2:
return random.choice([T1FIRST, T2FIRST])
#Trick room
if self.trick_room.active():
if speed1 > speed2:
return T2FIRST
return T1FIRST
#Default handling
if speed1 > speed2:
return T1FIRST
return T2FIRST
async def send_msg(self):
"""
Send the msg in a boilerplate embed.
Handles the message being too long.
"""
await generate_text_battle_message(self)
async def run_swap(self, swapper, othertrainer, *, mid_turn=False):
"""
Called when swapper does not have a pokemon selected, and needs a new one.
Prompts the swapper to pick a pokemon.
If mid_turn is set to True, the pokemon is being swapped in the middle of a turn (NOT at the start of a turn).
Returns None if the trainer swapped, and the trainer that won if they did not.
"""
await self.send_msg()
swapper.event.clear()
if swapper.is_human():
swap_view = SwapPromptView(swapper, othertrainer, self, mid_turn=mid_turn)
await self.channel.send(
f"{swapper.name}, pick a pokemon to swap to!",
view=swap_view
)
else:
swapper.swap(othertrainer, self, mid_turn=mid_turn)
try:
await swapper.event.wait()
except asyncio.TimeoutError:
self.msg += f"{swapper.name} did not select a poke, {othertrainer.name} wins!\n"
return othertrainer
if swapper.is_human():
swap_view.stop()
if swapper.current_pokemon is None:
self.msg += f"{swapper.name} did not select a poke, {othertrainer.name} wins!\n"
return othertrainer
if mid_turn:
self.msg += swapper.current_pokemon.send_out(othertrainer.current_pokemon, self)
if swapper.current_pokemon is not None:
swapper.current_pokemon.has_moved = True
return None
def handle_megas(self, t1, t2):
"""Handle mega evolving pokemon who mega evolve this turn."""
for at, dt in ((t1, t2), (t2, t1)):
if at.current_pokemon is not None and at.current_pokemon.should_mega_evolve:
# Bit of a hack, since it is in its mega form and dashes are removed from `name`, it will show as "<poke> mega evolved!".
if (at.current_pokemon.held_item == "mega-stone" or at.current_pokemon._name == "Rayquaza") and at.current_pokemon.form(at.current_pokemon._name + "-mega"):
self.msg += f"{at.current_pokemon.name} evolved!\n"
elif at.current_pokemon.held_item == "mega-stone-x" and at.current_pokemon.form(at.current_pokemon._name + "-mega-x"):
self.msg += f"{at.current_pokemon.name} evolved!\n"
elif at.current_pokemon.held_item == "mega-stone-y" and at.current_pokemon.form(at.current_pokemon._name + "-mega-y"):
self.msg += f"{at.current_pokemon.name} evolved!\n"
else:
raise ValueError("expected to mega evolve but no valid mega condition")
at.current_pokemon.ability_id = at.current_pokemon.mega_ability_id
at.current_pokemon.starting_ability_id = at.current_pokemon.mega_ability_id
at.current_pokemon.type_ids = at.current_pokemon.mega_type_ids.copy()
at.current_pokemon.starting_type_ids = at.current_pokemon.mega_type_ids.copy()
self.msg += at.current_pokemon.send_out_ability(dt.current_pokemon, self)
at.has_mega_evolved = True
def __repr__(self):
return f"Battle(trainer1={self.trainer1!r}, trainer2={self.trainer2!r})"