From 97625cdd4a5aa58b8b039eee37d2916dcf0d18b9 Mon Sep 17 00:00:00 2001 From: Cross Joy <87638792+CrossJoy@users.noreply.github.com> Date: Wed, 14 Dec 2022 20:49:23 +0800 Subject: [PATCH] Add files via upload --- plugins/utilities/practice_tools.py | 2227 +++++++++++++++++++++++++++ 1 file changed, 2227 insertions(+) create mode 100644 plugins/utilities/practice_tools.py diff --git a/plugins/utilities/practice_tools.py b/plugins/utilities/practice_tools.py new file mode 100644 index 0000000..803b0f9 --- /dev/null +++ b/plugins/utilities/practice_tools.py @@ -0,0 +1,2227 @@ +"""Practice Tools Mod: V1.0 +Made by Cross Joy""" + +# If anyone who want to help me on giving suggestion/ fix bugs/ creating PR, +# Can visit my github https://github.com/CrossJoy/Bombsquad-Modding + +# You can contact me through discord: +# My Discord Id: Cross Joy#0721 +# My BS Discord Server: https://discord.gg/JyBY6haARJ + +# Some support will be much appreciated. :') +# Support link: https://www.buymeacoffee.com/CrossJoy + + +# ---------------------------------------------------------------------------- +# Powerful and comprehensive tools for practice purpose. + +# Features: +# - Spawn any bot anywhere. +# - Can spawn power up by your own. +# - Bomb radius visualizer. (Thx Mikirog for some of the codes :D ) +# - Bomb Countdown. +# and many more + +# Go explore the tools yourself.:) + +# Practice tabs can be access through party window. +# Coop and local multiplayer compatible. +# Work on any 1.7+ ver. + +# FAQ: +# Can I use it to practice with friends? +# - Yes, but you are the only one can access the practice window. + +# Does it work when I join a public server? +# - Not possible. + +# Can I use it during Coop game? +# - Yes, it works fine. +# ---------------------------------------------------------------------------- + +from __future__ import annotations + +import random +import weakref +from enum import Enum +from typing import TYPE_CHECKING +import ba, _ba +import ba.internal +import bastd +from bastd.actor.powerupbox import PowerupBox +from bastd.actor.spaz import Spaz +from bastd.actor.spazbot import (SpazBotSet, SpazBot, BrawlerBot, TriggerBot, + ChargerBot, StickyBot, ExplodeyBot, BouncyBot, + BomberBotPro, BrawlerBotPro, TriggerBotPro, + ChargerBotPro, BomberBotProShielded, + BrawlerBotProShielded, TriggerBotProShielded, + ChargerBotProShielded) +from bastd.mainmenu import MainMenuSession +from bastd.ui.party import PartyWindow as OriginalPartyWindow +from ba import app, Plugin +from bastd.ui import popup +from bastd.actor.bomb import Bomb +import math + +from bastd.ui.tabs import TabRow + +if TYPE_CHECKING: + from typing import Any, Sequence, Callable, Optional + +try: + if ba.app.config.get("bombCountdown") is None: + ba.app.config["bombCountdown"] = False + else: + ba.app.config.get("bombCountdown") +except: + ba.app.config["bombCountdown"] = False + +try: + if ba.app.config.get("bombRadiusVisual") is None: + ba.app.config["bombRadiusVisual"] = False + else: + ba.app.config.get("bombRadiusVisual") +except: + ba.app.config["bombRadiusVisual"] = False + +try: + if ba.app.config.get("stopBots") is None: + ba.app.config["stopBots"] = False + else: + ba.app.config.get("stopBots") +except: + ba.app.config["stopBots"] = False + +try: + if ba.app.config.get("stopBots") is None: + ba.app.config["stopBots"] = False + else: + ba.app.config.get("stopBots") +except: + ba.app.config["stopBots"] = False + +try: + if ba.app.config.get("immortalDummy") is None: + ba.app.config["immortalDummy"] = False + else: + ba.app.config.get("immortalDummy") +except: + ba.app.config["immortalDummy"] = False + +try: + if ba.app.config.get("invincible") is None: + ba.app.config["invincible"] = False + else: + ba.app.config.get("invincible") +except: + ba.app.config["invincible"] = False + +_ba.set_party_icon_always_visible(True) + + +def is_game_version_lower_than(version): + """ + Returns a boolean value indicating whether the current game + version is lower than the passed version. Useful for addressing + any breaking changes within game versions. + """ + game_version = tuple(map(int, ba.app.version.split("."))) + version = tuple(map(int, version.split("."))) + return game_version < version + + +if is_game_version_lower_than("1.7.7"): + ba_internal = _ba +else: + ba_internal = ba.internal + + +class PartyWindow(ba.Window): + _redefine_methods = ['__init__'] + + def __init__(self, *args, **kwargs): + getattr(self, '__init___old')(*args, **kwargs) + + self.bg_color = (.5, .5, .5) + + self._edit_movements_button = ba.buttonwidget( + parent=self._root_widget, + scale=0.7, + position=(360, self._height - 47), + # (self._width - 80, self._height - 47) + size=(100, 50), + label='Practice', + autoselect=True, + button_type='square', + on_activate_call=ba.Call(doTestButton, self), + color=self.bg_color, + iconscale=1.2) + + +def redefine(obj: object, name: str, new: callable, + new_name: str = None) -> None: + if not new_name: + new_name = name + '_old' + if hasattr(obj, name): + setattr(obj, new_name, getattr(obj, name)) + setattr(obj, name, new) + + +def redefine_class(original_cls: object, cls: object) -> None: + for method in cls._redefine_methods: + redefine(original_cls, method, getattr(cls, method)) + + +def main(plugin: Plugin) -> None: + print(f'Plugins Tools v{plugin.__version__}') + app.practice_tool = plugin + redefine_class(OriginalPartyWindow, PartyWindow) + + +# ba_meta require api 7 +# ba_meta export plugin +class Practice(Plugin): + __version__ = '1.0' + + def on_app_running(self) -> None: + """Plugin start point.""" + + if app.build_number < 20427: + ba.screenmessage( + 'ok', + color=(.8, .1, .1)) + raise RuntimeError( + 'sad') + + return main(self) + + def new_bomb_init(func): + def setting(*args, **kwargs): + func(*args, **kwargs) + + bomb_type = args[0].bomb_type + fuse_bomb = ('land_mine', 'tnt', 'impact') + + if ba.app.config.get("bombRadiusVisual"): + args[0].radius_visualizer = ba.newnode('locator', + owner=args[0].node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circle', + 'color': (1, 0, 0), + 'opacity': 0.05, + 'draw_beauty': False, + 'additive': False + }) + args[0].node.connectattr('position', args[0].radius_visualizer, + 'position') + + ba.animate_array(args[0].radius_visualizer, 'size', 1, { + 0.0: [0.0], + 0.2: [args[0].blast_radius * 2.2], + 0.25: [args[0].blast_radius * 2.0] + }) + + args[0].radius_visualizer_circle = ba.newnode( + 'locator', + owner=args[ + 0].node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circleOutline', + 'size': [ + args[ + 0].blast_radius * 2.0], + # Here's that bomb's blast radius value again! + 'color': ( + 1, 1, 0), + 'draw_beauty': False, + 'additive': True + }) + args[0].node.connectattr('position', + args[0].radius_visualizer_circle, + 'position') + + ba.animate( + args[0].radius_visualizer_circle, 'opacity', { + 0: 0.0, + 0.4: 0.1 + }) + + if bomb_type == 'tnt': + args[0].fatal = ba.newnode('locator', + owner=args[0].node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circle', + 'color': ( + 0.7, 0, 0), + 'opacity': 0.10, + 'draw_beauty': False, + 'additive': False + }) + args[0].node.connectattr('position', + args[0].fatal, + 'position') + + ba.animate_array(args[0].fatal, 'size', 1, { + 0.0: [0.0], + 0.2: [args[0].blast_radius * 2.2 * 0.7], + 0.25: [args[0].blast_radius * 2.0 * 0.7] + }) + + if ba.app.config.get( + "bombCountdown") and bomb_type not in fuse_bomb: + color = (1.0, 1.0, 0.0) + count_bomb(*args, count='3', color=color) + color = (1.0, 0.5, 0.0) + ba.timer(1, ba.Call(count_bomb, *args, count='2', color=color)) + color = (1.0, 0.15, 0.15) + ba.timer(2, ba.Call(count_bomb, *args, count='1', color=color)) + + return setting + + bastd.actor.bomb.Bomb.__init__ = new_bomb_init( + bastd.actor.bomb.Bomb.__init__) + + +Spaz._pm2_spz_old = Spaz.__init__ + + +def _init_spaz_(self, *args, **kwargs): + self._pm2_spz_old(*args, **kwargs) + self.bot_radius = ba.newnode('locator', + owner=self.node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circle', + 'color': (0, 0, 1), + 'opacity': 0.0, + 'draw_beauty': False, + 'additive': False + }) + self.node.connectattr('position', + self.bot_radius, + 'position') + + self.radius_visualizer_circle = ba.newnode( + 'locator', + owner=self.node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circleOutline', + 'size': [(self.hitpoints_max - self.hitpoints) * 0.0048], + # Here's that bomb's blast radius value again! + 'color': (0, 1, 1), + 'draw_beauty': False, + 'additive': True + }) + + self.node.connectattr('position', self.radius_visualizer_circle, + 'position') + + self.curse_visualizer = ba.newnode('locator', + owner=self.node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circle', + 'color': (1, 0, 0), + 'size': (0.0, 0.0, 0.0), + 'opacity': 0.05, + 'draw_beauty': False, + 'additive': False + }) + self.node.connectattr('position', self.curse_visualizer, + 'position') + + self.curse_visualizer_circle = ba.newnode( + 'locator', + owner=self.node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circleOutline', + 'size': [3 * 2.0], + # Here's that bomb's blast radius value again! + 'color': ( + 1, 1, 0), + 'opacity': 0.0, + 'draw_beauty': False, + 'additive': True + }) + self.node.connectattr('position', + self.curse_visualizer_circle, + 'position') + + self.curse_visualizer_fatal = ba.newnode('locator', + owner=self.node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circle', + 'color': ( + 0.7, 0, 0), + 'size': (0.0, 0.0, 0.0), + 'opacity': 0.10, + 'draw_beauty': False, + 'additive': False + }) + self.node.connectattr('position', + self.curse_visualizer_fatal, + 'position') + + def invincible() -> None: + for i in _ba.get_foreground_host_activity().players: + try: + if i.node: + if ba.app.config.get("invincible"): + i.actor.node.invincible = True + else: + i.actor.node.invincible = False + except: + pass + + ba.timer(1.001, ba.Call(invincible)) + + +Spaz.__init__ = _init_spaz_ + +Spaz.super_curse = Spaz.curse + + +def new_cursed(self): + self.super_curse() + if ba.app.config.get("bombRadiusVisual"): + + ba.animate_array(self.curse_visualizer, 'size', 1, { + 0.0: [0.0], + 0.2: [3 * 2.2], + 0.5: [3 * 2.0], + 5.0: [3 * 2.0], + 5.1: [0.0], + }) + + ba.animate( + self.curse_visualizer_circle, 'opacity', { + 0: 0.0, + 0.4: 0.1, + 5.0: 0.1, + 5.1: 0.0, + }) + + ba.animate_array(self.curse_visualizer_fatal, 'size', 1, { + 0.0: [0.0], + 0.2: [2.2], + 0.5: [2.0], + 5.0: [2.0], + 5.1: [0.0], + }) + + +Spaz.curse = new_cursed + +Spaz.super_handlemessage = Spaz.handlemessage + + +def bot_handlemessage(self, msg: Any): + + + if isinstance(msg, ba.PowerupMessage): + if msg.poweruptype == 'health': + if ba.app.config.get("bombRadiusVisual"): + if self._cursed: + ba.animate_array(self.curse_visualizer, 'size', 1, { + 0.0: [3 * 2.0], + 0.2: [0.0], + }) + + ba.animate( + self.curse_visualizer_circle, 'opacity', { + 0.0: 0.1, + 0.2: 0.0, + }) + + ba.animate_array(self.curse_visualizer_fatal, 'size', 1, { + 0.0: [2.0], + 0.2: [0.0], + }) + + ba.animate_array(self.bot_radius, 'size', 1, { + 0.0: [0], + 0.25: [0] + }) + ba.animate(self.bot_radius, 'opacity', { + 0.0: 0.00, + 0.25: 0.0 + }) + + ba.animate_array(self.radius_visualizer_circle, 'size', 1, { + 0.0: [0], + 0.25: [0] + }) + + ba.animate( + self.radius_visualizer_circle, 'opacity', { + 0.0: 0.00, + 0.25: 0.0 + }) + + self.super_handlemessage(msg) + + if isinstance(msg, ba.HitMessage): + if self.hitpoints <= 0: + ba.animate(self.bot_radius, 'opacity', { + 0.0: 0.00 + }) + ba.animate( + self.radius_visualizer_circle, 'opacity', { + 0.0: 0.00 + }) + elif ba.app.config.get('bombRadiusVisual'): + + ba.animate_array(self.bot_radius, 'size', 1, { + 0.0: [(self.hitpoints_max - self.hitpoints) * 0.0048], + 0.25: [(self.hitpoints_max - self.hitpoints) * 0.0048] + }) + ba.animate(self.bot_radius, 'opacity', { + 0.0: 0.00, + 0.25: 0.05 + }) + + ba.animate_array(self.radius_visualizer_circle, 'size', 1, { + 0.0: [(self.hitpoints_max - self.hitpoints) * 0.0048], + 0.25: [(self.hitpoints_max - self.hitpoints) * 0.0048] + }) + + ba.animate( + self.radius_visualizer_circle, 'opacity', { + 0.0: 0.00, + 0.25: 0.1 + }) + + + +Spaz.handlemessage = bot_handlemessage + + +def count_bomb(*args, count, color): + text = ba.newnode('math', owner=args[0].node, + attrs={'input1': (0, 0.7, 0), + 'operation': 'add'}) + args[0].node.connectattr('position', text, 'input2') + args[0].spaztext = ba.newnode('text', + owner=args[0].node, + attrs={ + 'text': count, + 'in_world': True, + 'color': color, + 'shadow': 1.0, + 'flatness': 1.0, + 'scale': 0.012, + 'h_align': 'center', + }) + + args[0].node.connectattr('position', args[0].spaztext, + 'position') + ba.animate(args[0].spaztext, 'scale', + {0: 0, 0.3: 0.03, 0.5: 0.025, 0.8: 0.025, 1.0: 0.0}) + + +def doTestButton(self): + if isinstance(_ba.get_foreground_host_session(), MainMenuSession): + ba.screenmessage('Join any map to start using it.', color=(.8, .8, .1)) + return + + if ba.app.config.get("disablePractice"): + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.Call(PracticeWindow()) + else: + ba.screenmessage('Only works on local games.', color=(.8, .8, .1)) + + +# --------------------------------------------------------------- + + +class NewBotSet(SpazBotSet): + + def __init__(self): + super().__init__() + + def _update(self) -> None: + try: + with ba.Context(_ba.get_foreground_host_activity()): + # Update one of our bot lists each time through. + # First off, remove no-longer-existing bots from the list. + try: + bot_list = self._bot_lists[self._bot_update_list] = ([ + b for b in self._bot_lists[self._bot_update_list] if b + ]) + except Exception: + bot_list = [] + ba.print_exception('Error updating bot list: ' + + str(self._bot_lists[ + self._bot_update_list])) + self._bot_update_list = (self._bot_update_list + + 1) % self._bot_list_count + + # Update our list of player points for the bots to use. + player_pts = [] + for player in ba.getactivity().players: + assert isinstance(player, ba.Player) + try: + # TODO: could use abstracted player.position here so we + # don't have to assume their actor type, but we have no + # abstracted velocity as of yet. + if player.is_alive(): + assert isinstance(player.actor, Spaz) + assert player.actor.node + player_pts.append( + (ba.Vec3(player.actor.node.position), + ba.Vec3( + player.actor.node.velocity))) + except Exception: + ba.print_exception('Error on bot-set _update.') + + for bot in bot_list: + if not ba.app.config.get('stopBots'): + bot.set_player_points(player_pts) + bot.update_ai() + + ba.app.config["disablePractice"] = True + except: + ba.app.config["disablePractice"] = False + + def clear(self) -> None: + """Immediately clear out any bots in the set.""" + with ba.Context(_ba.get_foreground_host_activity()): + # Don't do this if the activity is shutting down or dead. + activity = ba.getactivity(doraise=False) + if activity is None or activity.expired: + return + + for i, bot_list in enumerate(self._bot_lists): + for bot in bot_list: + bot.handlemessage(ba.DieMessage(immediate=True)) + self._bot_lists[i] = [] + + def spawn_bot( + self, + bot_type: type[SpazBot], + pos: Sequence[float], + spawn_time: float = 3.0, + on_spawn_call: Callable[[SpazBot], Any] | None = None) -> None: + """Spawn a bot from this set.""" + from bastd.actor import spawner + spawner.Spawner(pt=pos, + spawn_time=spawn_time, + send_spawn_message=False, + spawn_callback=ba.Call(self._spawn_bot, bot_type, pos, + on_spawn_call)) + self._spawning_count += 1 + + def _spawn_bot(self, bot_type: type[SpazBot], pos: Sequence[float], + on_spawn_call: Callable[[SpazBot], Any] | None) -> None: + spaz = bot_type().autoretain() + ba.playsound(ba.getsound('spawn'), position=pos) + assert spaz.node + spaz.node.handlemessage('flash') + spaz.node.is_area_of_interest = False + spaz.handlemessage(ba.StandMessage(pos, random.uniform(0, 360))) + self.add_bot(spaz) + self._spawning_count -= 1 + if on_spawn_call is not None: + on_spawn_call(spaz) + + +class DummyBotSet(NewBotSet): + + def _update(self) -> None: + + try: + with ba.Context(_ba.get_foreground_host_activity()): + # Update one of our bot lists each time through. + # First off, remove no-longer-existing bots from the list. + try: + bot_list = self._bot_lists[self._bot_update_list] = ([ + b for b in self._bot_lists[self._bot_update_list] if b + ]) + except Exception: + ba.print_exception('Error updating bot list: ' + + str(self._bot_lists[ + self._bot_update_list])) + self._bot_update_list = (self._bot_update_list + + 1) % self._bot_list_count + + + except: + pass + + +class DummyBot(SpazBot): + character = 'Bones' + + def __init__(self): + super().__init__() + if ba.app.config.get('immortalDummy'): + ba.timer(0.2, self.immortal, + repeat=True) + + def immortal(self): + self.hitpoints = self.hitpoints_max = 10000 + ba.emitfx( + position=self.node.position, + count=20, + emit_type='fairydust') + + +# ------------------------------------------------------------------- + +class PracticeTab: + """Defines a tab for use in the gather UI.""" + + def __init__(self, window: PracticeWindow) -> None: + self._window = weakref.ref(window) + + @property + def window(self) -> PracticeWindow: + """The GatherWindow that this tab belongs to.""" + window = self._window() + if window is None: + raise ba.NotFoundError("PracticeTab's window no longer exists.") + return window + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + scroll_widget: ba.Widget, + extra_x: float, + ) -> ba.Widget: + """Called when the tab becomes the active one. + + The tab should create and return a container widget covering the + specified region. + """ + raise RuntimeError('Should not get here.') + + def on_deactivate(self) -> None: + """Called when the tab will no longer be the active one.""" + + def save_state(self) -> None: + """Called when the parent window is saving state.""" + + def restore_state(self) -> None: + """Called when the parent window is restoring state.""" + + +def _check_value_change(setting: int, widget: ba.Widget, + value: str) -> None: + ba.textwidget(edit=widget, + text=ba.Lstr(resource='onText') if value else ba.Lstr( + resource='offText')) + + if setting == 0: + if value: + ba.app.config["stopBots"] = True + else: + ba.app.config["stopBots"] = False + elif setting == 1: + if value: + ba.app.config["immortalDummy"] = True + else: + ba.app.config["immortalDummy"] = False + + +class BotsPracticeTab(PracticeTab): + """The about tab in the practice UI""" + + def __init__(self, window: PracticeWindow, + bot1=DummyBotSet(), bot2=NewBotSet()) -> None: + super().__init__(window) + self._container: ba.Widget | None = None + self.count = 1 + self.radius = 0 + self.radius_array = (['Small', 'Medium', 'Big']) + self.parent_widget = None + self.bot1 = bot1 + self.bot2 = bot2 + self.activity = _ba.get_foreground_host_activity() + self.image_array = ( + ['bonesIcon', 'neoSpazIcon', 'kronkIcon', + 'zoeIcon', 'ninjaIcon', 'melIcon', 'jackIcon', 'bunnyIcon', + 'neoSpazIcon', 'kronkIcon', 'zoeIcon', 'ninjaIcon', + 'neoSpazIcon', 'kronkIcon', 'zoeIcon', 'ninjaIcon']) + self.bot_array_name = ( + ['Dummy', 'Bomber', 'Bruiser', + 'Trigger', 'Charger', 'Sticky', + 'Explodey', 'Bouncy', 'Pro Bomber', + 'Pro Brawler', 'Pro Trigger', 'Pro Charger', + 'S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger']) + + self.setting_name = (['Stop Bots', 'Immortal Dummy']) + self.config = (['stopBots', 'immortalDummy']) + + self.bot_array = ( + [DummyBot, SpazBot, BrawlerBot, TriggerBot, + ChargerBot, StickyBot, ExplodeyBot, BouncyBot, + BomberBotPro, BrawlerBotPro, TriggerBotPro, ChargerBotPro, + BomberBotProShielded, BrawlerBotProShielded, + TriggerBotProShielded, ChargerBotProShielded]) + + self._icon_index = self.bot_array_name.index('Dummy') + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + scroll_widget: ba.Widget, + extra_x: float, + ) -> ba.Widget: + + b_size_2 = 100 + spacing_h = -50 + mask_texture = ba.gettexture('characterIconMask') + spacing_v = 60 + + self.parent_widget = parent_widget + + self._scroll_width = region_width * 0.8 + self._scroll_height = region_height * 0.6 + self._scroll_position = ((region_width - self._scroll_width) * 0.5, + (region_height - self._scroll_height) * 0.5) + + self._sub_width = self._scroll_width + self._sub_height = 200 + + self.container_h = 600 + bots_height = self.container_h - 50 + + self._subcontainer = ba.containerwidget( + parent=scroll_widget, + size=(self._sub_width, self.container_h), + background=False, + selection_loops_to_parent=True) + + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, + bots_height), + size=(0, 0), + color=(1.0, 1.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text='Spawn Bot', + maxwidth=200) + + self._bot_button = bot = ba.buttonwidget( + parent=self._subcontainer, + autoselect=True, + position=(self._sub_width * 0.5 - b_size_2 * 0.5, + bots_height + spacing_h * 3), + on_activate_call=self._bot_window, + size=(b_size_2, b_size_2), + label='', + color=(1, 1, 1), + tint_texture=(ba.gettexture( + self.image_array[self._icon_index] + 'ColorMask')), + tint_color=(0.6, 0.6, 0.6), + tint2_color=(0.1, 0.3, 0.1), + texture=ba.gettexture(self.image_array[self._icon_index]), + mask_texture=mask_texture) + + ba.textwidget( + parent=self._subcontainer, + h_align='center', + v_align='center', + position=(self._sub_width * 0.5, + bots_height + spacing_h * 4 + 10), + size=(0, 0), + draw_controller=bot, + text='Bot Type', + scale=1.0, + color=ba.app.ui.title_color, + maxwidth=130) + + ba.textwidget(parent=self._subcontainer, + position=( + self._sub_width * 0.005, + bots_height + + spacing_h * 7), + size=(100, 30), + text='Count', + h_align='left', + color=(0.8, 0.8, 0.8), + v_align='center', + maxwidth=200) + self.count_text = txt = ba.textwidget(parent=self._subcontainer, + position=( + self._sub_width * 0.85 - spacing_v * 2, + bots_height + + spacing_h * 7), + size=(0, 28), + text=str(self.count), + editable=False, + color=(0.6, 1.0, 0.6), + maxwidth=150, + h_align='center', + v_align='center', + padding=2) + self.button_bot_left = btn1 = ba.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.85 - spacing_v - 14, + bots_height + + spacing_h * 7), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=self.decrease_count, + repeat=True) + self.button_bot_right = btn2 = ba.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.85 - 14, + bots_height + + spacing_h * 7), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=self.increase_count, + repeat=True) + + ba.textwidget(parent=self._subcontainer, + position=( + self._sub_width * 0.005, + bots_height + + spacing_h * 8), + size=(100, 30), + text='Spawn Radius', + h_align='left', + color=(0.8, 0.8, 0.8), + v_align='center', + maxwidth=200) + + self.radius_text = txt = ba.textwidget(parent=self._subcontainer, + position=( + self._sub_width * 0.85 - spacing_v * 2, + bots_height + + spacing_h * 8), + size=(0, 28), + text=self.radius_array[ + self.radius], + editable=False, + color=(0.6, 1.0, 0.6), + maxwidth=50, + h_align='center', + v_align='center', + padding=2) + self.button_bot_left = btn1 = ba.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.85 - spacing_v - 14, + bots_height + + spacing_h * 8), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=self.decrease_radius, + repeat=True) + self.button_bot_right = btn2 = ba.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.85 - 14, + bots_height + + spacing_h * 8), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=self.increase_radius, + repeat=True) + + self.button = ba.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.25 - 40, + bots_height + + spacing_h * 6), + size=(80, 50), + autoselect=True, + button_type='square', + label='Spawn', + on_activate_call=self.do_spawn_bot) + + self.button = ba.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.75 - 40, + bots_height + + spacing_h * 6), + size=(80, 50), + autoselect=True, + button_type='square', + color=(1, 0.2, 0.2), + label='Clear', + on_activate_call=self.clear_bot) + + i = 0 + for name in self.setting_name: + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.005, + bots_height + spacing_h * (9 + i)), + size=(100, 30), + text=name, + h_align='left', + color=(0.8, 0.8, 0.8), + v_align='center', + maxwidth=200) + value = ba.app.config.get(self.config[i]) + txt2 = ba.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.8 - spacing_v / 2, + bots_height + + spacing_h * (9 + i)), + size=(0, 28), + text=ba.Lstr(resource='onText') if value else ba.Lstr( + resource='offText'), + editable=False, + color=(0.6, 1.0, 0.6), + maxwidth=50, + h_align='right', + v_align='center', + padding=2) + ba.checkboxwidget(parent=self._subcontainer, + text='', + position=(self._sub_width * 0.8 - 15, + bots_height + + spacing_h * (9 + i)), + size=(30, 30), + autoselect=False, + textcolor=(0.8, 0.8, 0.8), + value=value, + on_value_change_call=ba.Call( + _check_value_change, + i, txt2)) + i += 1 + + return self._subcontainer + + def _bot_window(self) -> None: + BotPicker( + parent=self.parent_widget, + delegate=self) + + def increase_count(self): + if self.count < 10: + self.count += 1 + + ba.textwidget(edit=self.count_text, + text=str(self.count)) + + def decrease_count(self): + if self.count > 1: + self.count -= 1 + + ba.textwidget(edit=self.count_text, + text=str(self.count)) + + def increase_radius(self): + if self.radius < 2: + self.radius += 1 + + ba.textwidget(edit=self.radius_text, + text=self.radius_array[self.radius]) + + def decrease_radius(self): + if self.radius > 0: + self.radius -= 1 + + ba.textwidget(edit=self.radius_text, + text=self.radius_array[self.radius]) + + def clear_bot(self): + self.bot1.clear() + self.bot2.clear() + + def do_spawn_bot(self, clid: int = -1) -> None: + with ba.Context(self.activity): + for i in _ba.get_foreground_host_activity().players: + if i.sessionplayer.inputdevice.client_id == clid: + if i.node: + bot_type = self._icon_index + for a in range(self.count): + x = (random.randrange + (-10, 10) / 10) * math.pow(self.radius + 1, 2) + z = (random.randrange + (-10, 10) / 10) * math.pow(self.radius + 1, 2) + pos = (i.node.position[0] + x, + i.node.position[1], + i.node.position[2] + z) + if bot_type == 0: + self.bot1.spawn_bot(self.bot_array[0], + pos=pos, + spawn_time=1.0) + else: + self.bot2.spawn_bot(self.bot_array[bot_type], + pos=pos, + spawn_time=1.0) + break + + def on_bots_picker_pick(self, character: str) -> None: + """A bots has been selected by the picker.""" + if not self.parent_widget: + return + + # The player could have bought a new one while the picker was u + self._icon_index = self.bot_array_name.index( + character) if character in self.bot_array_name else 0 + self._update_character() + + def _update_character(self, change: int = 0) -> None: + if self._bot_button: + if self.bot_array_name[self._icon_index] in ( + 'Pro Bomber', 'Pro Brawler', + 'Pro Trigger', 'Pro Charger', + 'S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger'): + tint1 = (1.0, 0.2, 0.1) + tint2 = (0.6, 0.1, 0.05) + elif self.bot_array_name[self._icon_index] in 'Bouncy': + tint1 = (1, 1, 1) + tint2 = (1.0, 0.5, 0.5) + else: + tint1 = (0.6, 0.6, 0.6) + tint2 = (0.1, 0.3, 0.1) + + if self.bot_array_name[self._icon_index] in ( + 'S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger'): + color = (1.3, 1.2, 3.0) + else: + color = (1.0, 1.0, 1.0) + + ba.buttonwidget( + edit=self._bot_button, + texture=ba.gettexture(self.image_array[self._icon_index]), + tint_texture=(ba.gettexture( + self.image_array[self._icon_index] + 'ColorMask')), + color=color, + tint_color=tint1, + tint2_color=tint2) + + +class PowerUpPracticeTab(PracticeTab): + """The about tab in the practice UI""" + + def __init__(self, window: PracticeWindow) -> None: + super().__init__(window) + self._container: ba.Widget | None = None + self.count = 1 + self.parent_widget = None + self.activity = _ba.get_foreground_host_activity() + + self.power_list = (['Bomb', 'Curse', 'Health', 'IceBombs', + 'ImpactBombs', 'LandMines', 'Punch', + 'Shield', 'StickyBombs']) + + self.power_list_type_name = ( + ['Tripple Bombs', 'Curse', 'Health', 'Ice Bombs', + 'Impact Bombs', 'Land Mines', 'Punch', + 'Shield', 'Sticky Bombs']) + + self.power_list_type = ( + ['triple_bombs', 'curse', 'health', 'ice_bombs', + 'impact_bombs', 'land_mines', 'punch', + 'shield', 'sticky_bombs']) + self._icon_index = self.power_list_type_name.index('Tripple Bombs') + + self.setting_name = (['Bomb Countdown', 'Bomb Radius Visualizer']) + self.config = (['bombCountdown', 'bombRadiusVisual']) + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + scroll_widget: ba.Widget, + extra_x: float, + ) -> ba.Widget: + + b_size_2 = 100 + spacing_h = -50 + spacing_v = 60 + + self.parent_widget = parent_widget + + self._scroll_width = region_width * 0.8 + self._scroll_height = region_height * 0.6 + self._scroll_position = ((region_width - self._scroll_width) * 0.5, + (region_height - self._scroll_height) * 0.5) + + self._sub_width = self._scroll_width + self._sub_height = 200 + + self.container_h = 450 + power_height = self.container_h - 50 + + self._subcontainer = ba.containerwidget( + parent=scroll_widget, + size=(self._sub_width, self.container_h), + background=False, + selection_loops_to_parent=True) + + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, + power_height), + size=(0, 0), + color=(1.0, 1.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text='Spawn Power Up', + maxwidth=200) + + self._power_button = bot = ba.buttonwidget( + parent=self._subcontainer, + autoselect=True, + position=(self._sub_width * 0.5 - b_size_2 * 0.5, + power_height + spacing_h * 3), + on_activate_call=self._power_window, + size=(b_size_2, b_size_2), + label='', + color=(1, 1, 1), + texture=ba.gettexture('powerup' + + self.power_list[ + self._icon_index])) + + ba.textwidget( + parent=self._subcontainer, + h_align='center', + v_align='center', + position=(self._sub_width * 0.5, + power_height + spacing_h * 4 + 10), + size=(0, 0), + draw_controller=bot, + text='Power Up Type', + scale=1.0, + color=ba.app.ui.title_color, + maxwidth=300) + + self.button = ba.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.25 - 40, + power_height + + spacing_h * 6), + size=(80, 50), + autoselect=True, + button_type='square', + label='Spawn', + on_activate_call=self.get_powerup) + + self.button = ba.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.75 - 40, + power_height + + spacing_h * 6), + size=(80, 50), + autoselect=True, + button_type='square', + color=(1, 0.2, 0.2), + label='Debuff', + on_activate_call=self.debuff) + + i = 0 + for name in self.setting_name: + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.005, + power_height + spacing_h * (7 + i)), + size=(100, 30), + text=name, + h_align='left', + color=(0.8, 0.8, 0.8), + v_align='center', + maxwidth=200) + value = ba.app.config.get(self.config[i]) + txt2 = ba.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.8 - spacing_v / 2, + power_height + + spacing_h * (7 + i)), + size=(0, 28), + text=ba.Lstr(resource='onText') if value else ba.Lstr( + resource='offText'), + editable=False, + color=(0.6, 1.0, 0.6), + maxwidth=400, + h_align='right', + v_align='center', + padding=2) + ba.checkboxwidget(parent=self._subcontainer, + text='', + position=(self._sub_width * 0.8 - 15, + power_height + + spacing_h * (7 + i)), + size=(30, 30), + autoselect=False, + textcolor=(0.8, 0.8, 0.8), + value=value, + on_value_change_call=ba.Call( + self._check_value_change, + i, txt2)) + i += 1 + + return self._subcontainer + + def debuff(self): + with ba.Context(_ba.get_foreground_host_activity()): + for i in _ba.get_foreground_host_activity().players: + Spaz._gloves_wear_off(i.actor) + Spaz._multi_bomb_wear_off(i.actor) + Spaz._bomb_wear_off(i.actor) + i.actor.shield_hitpoints = 1 + + def get_powerup(self, clid: int = -1) -> None: + with ba.Context(_ba.get_foreground_host_activity()): + for i in _ba.get_foreground_host_activity().players: + if i.sessionplayer.inputdevice.client_id == clid: + if i.node: + x = (random.choice([-7, 7]) / 10) + z = (random.choice([-7, 7]) / 10) + pos = (i.node.position[0] + x, + i.node.position[1], + i.node.position[2] + z) + PowerupBox(position=pos, + poweruptype= + self.power_list_type + [self._icon_index]).autoretain() + + def _power_window(self) -> None: + PowerPicker( + parent=self.parent_widget, + delegate=self) + + def on_power_picker_pick(self, power: str) -> None: + """A power up has been selected by the picker.""" + if not self.parent_widget: + return + + # The player could have bought a new one while the picker was u + self._icon_index = self.power_list.index( + power) if power in self.power_list else 0 + self._update_power() + + def _update_power(self, change: int = 0) -> None: + if self._power_button: + ba.buttonwidget( + edit=self._power_button, + texture=(ba.gettexture('powerup' + + self.power_list[ + self._icon_index]))) + + def _check_value_change(self, setting: int, widget: ba.Widget, + value: str) -> None: + ba.textwidget(edit=widget, + text=ba.Lstr(resource='onText') if value else ba.Lstr( + resource='offText')) + + with ba.Context(self.activity): + if setting == 0: + if value: + ba.app.config["bombCountdown"] = True + else: + ba.app.config["bombCountdown"] = False + elif setting == 1: + if value: + ba.app.config["bombRadiusVisual"] = True + else: + ba.app.config["bombRadiusVisual"] = False + + +class OthersPracticeTab(PracticeTab): + """The about tab in the practice UI""" + + def __init__(self, window: PracticeWindow) -> None: + super().__init__(window) + self._container: ba.Widget | None = None + self.count = 1 + self.parent_widget = None + self.activity = _ba.get_foreground_host_activity() + self.setting_name = (['Pause On Window', 'Invincible']) + self.config = (['pause', 'invincible']) + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + scroll_widget: ba.Widget, + extra_x: float, + ) -> ba.Widget: + spacing_v = 60 + spacing_h = -50 + + self.parent_widget = parent_widget + + self._scroll_width = region_width * 0.8 + self._scroll_height = region_height * 0.6 + self._scroll_position = ((region_width - self._scroll_width) * 0.5, + (region_height - self._scroll_height) * 0.5) + + self._sub_width = self._scroll_width + + self.container_h = 300 + other_height = self.container_h - 50 + + self._subcontainer = ba.containerwidget( + parent=scroll_widget, + size=(self._sub_width, self.container_h), + background=False, + selection_loops_to_parent=True) + + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, + other_height), + size=(0, 0), + color=(1.0, 1.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text='Others', + maxwidth=200) + + i = 0 + for name in self.setting_name: + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.005, + other_height + spacing_h * (2 + i)), + size=(100, 30), + text=name, + h_align='left', + color=(0.8, 0.8, 0.8), + v_align='center', + maxwidth=200) + value = ba.app.config.get(self.config[i]) + txt2 = ba.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.8 - spacing_v / 2, + other_height + + spacing_h * (2 + i)), + size=(0, 28), + text=ba.Lstr(resource='onText') if value else ba.Lstr( + resource='offText'), + editable=False, + color=(0.6, 1.0, 0.6), + maxwidth=400, + h_align='right', + v_align='center', + padding=2) + ba.checkboxwidget(parent=self._subcontainer, + text='', + position=(self._sub_width * 0.8 - 15, + other_height + + spacing_h * (2 + i)), + size=(30, 30), + autoselect=False, + textcolor=(0.8, 0.8, 0.8), + value=value, + on_value_change_call=ba.Call( + self._check_value_change, + i, txt2)) + i += 1 + + return self._subcontainer + + def _check_value_change(self, setting: int, widget: ba.Widget, + value: str) -> None: + ba.textwidget(edit=widget, + text=ba.Lstr(resource='onText') if value else ba.Lstr( + resource='offText')) + + with ba.Context(self.activity): + if setting == 0: + if value: + ba.app.config["pause"] = True + self.activity.globalsnode.paused = True + else: + ba.app.config["pause"] = False + self.activity.globalsnode.paused = False + elif setting == 1: + if value: + ba.app.config["invincible"] = True + else: + ba.app.config["invincible"] = False + for i in _ba.get_foreground_host_activity().players: + try: + if i.node: + if ba.app.config.get("invincible"): + i.actor.node.invincible = True + else: + i.actor.node.invincible = False + except: + pass + + +class PracticeWindow(ba.Window): + class TabID(Enum): + """Our available tab types.""" + + BOTS = 'bots' + POWERUP = 'power up' + OTHERS = 'others' + + def __del__(self): + _ba.set_party_icon_always_visible(True) + self.activity.globalsnode.paused = False + + def __init__(self, + transition: Optional[str] = 'in_right'): + + self.activity = _ba.get_foreground_host_activity() + _ba.set_party_icon_always_visible(False) + if ba.app.config.get("pause"): + self.activity.globalsnode.paused = True + uiscale = ba.app.ui.uiscale + self.pick = 0 + self._width = 500 + + self._height = (578 if uiscale is ba.UIScale.SMALL else + 670 if uiscale is ba.UIScale.MEDIUM else 800) + extra_x = 100 if uiscale is ba.UIScale.SMALL else 0 + self.extra_x = extra_x + + self._transitioning_out = False + + b_size_2 = 100 + + spacing_h = -50 + spacing = -450 + spacing_v = 60 + self.container_h = 500 + v = self._height - 115.0 + v -= spacing_v * 3.0 + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=(1.3 if uiscale is ba.UIScale.SMALL else + 0.97 if uiscale is ba.UIScale.MEDIUM else 0.8), + stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else ( + 240, 0) if uiscale is ba.UIScale.MEDIUM else (330, 20))) + + self._sub_height = 200 + + self._scroll_width = self._width * 0.8 + self._scroll_height = self._height * 0.6 + self._scroll_position = ((self._width - self._scroll_width) * 0.5, + (self._height - self._scroll_height) * 0.5) + + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + size=(self._scroll_width, + self._scroll_height), + color=(0.55, 0.55, 0.55), + highlight=False, + position=self._scroll_position) + + ba.containerwidget(edit=self._scrollwidget, + claims_left_right=True) + + # --------------------------------------------------------- + + x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 + + self._current_tab: PracticeWindow.TabID | None = None + extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 + self._r = 'gatherWindow' + + tabdefs: list[tuple[PracticeWindow.TabID, str]] = [ + (self.TabID.BOTS, 'Bots') + ] + if ba_internal.get_v1_account_misc_read_val( + 'enablePublicParties', True + ): + tabdefs.append( + ( + self.TabID.POWERUP, + 'Power Ups') + ) + tabdefs.append( + (self.TabID.OTHERS, 'Others') + ) + + condensed = uiscale is not ba.UIScale.LARGE + t_offs_y = ( + 0 if not condensed else 25 if uiscale is ba.UIScale.MEDIUM else 17 + ) + + tab_buffer_h = (320 if condensed else 250) + 2 * x_offs + + self._sub_width = self._width * 0.8 + + # On small UI, push our tabs up closer to the top of the screen to + # save a bit of space. + tabs_top_extra = 42 if condensed else 0 + self._tab_row = TabRow( + self._root_widget, + tabdefs, + pos=( + self._width * 0.5 - self._sub_width * 0.5, + self._height * 0.79), + size=(self._sub_width, 50), + on_select_call=ba.WeakCall(self._set_tab), + ) + + # Now instantiate handlers for these tabs. + tabtypes: dict[PracticeWindow.TabID, type[PracticeTab]] = { + self.TabID.BOTS: BotsPracticeTab, + self.TabID.POWERUP: PowerUpPracticeTab, + self.TabID.OTHERS: OthersPracticeTab, + } + self._tabs: dict[PracticeWindow.TabID, PracticeTab] = {} + for tab_id in self._tab_row.tabs: + tabtype = tabtypes.get(tab_id) + if tabtype is not None: + self._tabs[tab_id] = tabtype(self) + + if ba.app.ui.use_toolbars: + ba.widget( + edit=self._tab_row.tabs[tabdefs[-1][0]].button, + right_widget=ba_internal.get_special_widget('party_button'), + ) + if uiscale is ba.UIScale.SMALL: + ba.widget( + edit=self._tab_row.tabs[tabdefs[0][0]].button, + left_widget=ba_internal.get_special_widget('back_button'), + ) + + # ----------------------------------------------------------- + + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(self._width * 0.15 - 30, + self._height * 0.95 - 30), + size=(60, 60), + scale=1.1, + label=ba.charstr(ba.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self.close) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, + self._height * 0.95), + size=(0, 0), + color=ba.app.ui.title_color, + scale=1.5, + h_align='center', + v_align='center', + text='Practice Tools', + maxwidth=400) + + self.info_button = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(self._width * 0.8 - 30, + self._height * 0.15 - 30), + on_activate_call=self._info_window, + size=(60, 60), + label='') + + ba.imagewidget( + parent=self._root_widget, + position=(self._width * 0.8 - 25, + self._height * 0.15 - 25), + size=(50, 50), + draw_controller=self.info_button, + texture=ba.gettexture('achievementEmpty'), + color=(1.0, 1.0, 1.0)) + + self._tab_container: ba.Widget | None = None + + self._restore_state() + + # # ------------------------------------------------------- + + def _info_window(self): + InfoWindow( + parent=self._root_widget) + + def _button(self) -> None: + ba.buttonwidget(edit=None, + color=(0.2, 0.4, 0.8)) + + def close(self) -> None: + """Close the window.""" + ba.containerwidget(edit=self._root_widget, transition='out_right') + + def _set_tab(self, tab_id: TabID) -> None: + if self._current_tab is tab_id: + return + prev_tab_id = self._current_tab + self._current_tab = tab_id + + # We wanna preserve our current tab between runs. + cfg = ba.app.config + cfg['Practice Tab'] = tab_id.value + cfg.commit() + + # Update tab colors based on which is selected. + self._tab_row.update_appearance(tab_id) + + if prev_tab_id is not None: + prev_tab = self._tabs.get(prev_tab_id) + if prev_tab is not None: + prev_tab.on_deactivate() + + # Clear up prev container if it hasn't been done. + if self._tab_container: + self._tab_container.delete() + + tab = self._tabs.get(tab_id) + if tab is not None: + self._tab_container = tab.on_activate( + self._root_widget, + self._tab_row.tabs[tab_id].button, + self._width, + self._height, + self._scrollwidget, + self.extra_x, + ) + return + + def _restore_state(self) -> None: + from efro.util import enum_by_value + + try: + for tab in self._tabs.values(): + tab.restore_state() + + sel: ba.Widget | None + winstate = ba.app.ui.window_states.get(type(self), {}) + sel_name = winstate.get('sel_name', None) + assert isinstance(sel_name, (str, type(None))) + current_tab = self.TabID.BOTS + gather_tab_val = ba.app.config.get('Practice Tab') + try: + stored_tab = enum_by_value(self.TabID, gather_tab_val) + if stored_tab in self._tab_row.tabs: + current_tab = stored_tab + except ValueError: + pass + self._set_tab(current_tab) + if sel_name == 'Back': + sel = self._back_button + elif sel_name == 'TabContainer': + sel = self._tab_container + elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): + try: + sel_tab_id = enum_by_value( + self.TabID, sel_name.split(':')[-1] + ) + except ValueError: + sel_tab_id = self.TabID.BOTS + sel = self._tab_row.tabs[sel_tab_id].button + else: + sel = self._tab_row.tabs[current_tab].button + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception('Error restoring gather-win state.') + + +org_begin = ba._activity.Activity.on_begin + + +def new_begin(self): + """Runs when game is began.""" + _ba.set_party_icon_always_visible(True) + + +ba._activity.Activity.on_begin = new_begin + + +class BotPicker(popup.PopupWindow): + """Popup window for selecting bots to spwan.""" + + def __init__(self, + parent: ba.Widget, + position: tuple[float, float] = (0.0, 0.0), + delegate: Any = None, + scale: float | None = None, + offset: tuple[float, float] = (0.0, 0.0), + selected_character: str | None = None): + del parent # unused here + uiscale = ba.app.ui.uiscale + if scale is None: + scale = (1.85 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + + self._delegate = delegate + self._transitioning_out = False + + count = 16 + + columns = 3 + rows = int(math.ceil(float(count) / columns)) + + button_width = 100 + button_height = 100 + button_buffer_h = 10 + button_buffer_v = 15 + + self._width = (10 + columns * (button_width + 2 * button_buffer_h) * + (1.0 / 0.95) * (1.0 / 0.8)) + self._height = self._width * (0.8 + if uiscale is ba.UIScale.SMALL else 1.06) + + self._scroll_width = self._width * 0.8 + self._scroll_height = self._height * 0.8 + self._scroll_position = ((self._width - self._scroll_width) * 0.5, + (self._height - self._scroll_height) * 0.5) + + # creates our _root_widget + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=(0.5, 0.5, 0.5), + offset=offset, + focus_position=self._scroll_position, + focus_size=(self._scroll_width, + self._scroll_height)) + + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + size=(self._scroll_width, + self._scroll_height), + color=(0.55, 0.55, 0.55), + highlight=False, + position=self._scroll_position) + + ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 5 + rows * (button_height + + 2 * button_buffer_v) + 100 + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False) + + mask_texture = ba.gettexture('characterIconMask') + + bot_list = (['bones', 'neoSpaz', 'kronk', 'zoe', + 'ninja', 'mel', 'jack', 'bunny', + 'neoSpaz', 'kronk', 'zoe', + 'ninja', + 'neoSpaz', 'kronk', 'zoe', + 'ninja']) + bot_list_type = (['Dummy', 'Bomber', 'Brawler', 'Trigger', + 'Charger', 'Sticky', 'Explodey', 'Bouncy', + 'Pro Bomber', 'Pro Brawler', 'Pro Trigger', + 'Pro Charger', 'S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger']) + + index = 0 + for y in range(rows): + for x in range(columns): + + if bot_list_type[index] in ('Pro Bomber', 'Pro Brawler', + 'Pro Trigger', 'Pro Charger', + 'S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger'): + tint1 = (1.0, 0.2, 0.1) + tint2 = (0.6, 0.1, 0.05) + elif bot_list_type[index] in 'Bouncy': + tint1 = (1, 1, 1) + tint2 = (1.0, 0.5, 0.5) + else: + tint1 = (0.6, 0.6, 0.6) + tint2 = (0.1, 0.3, 0.1) + + if bot_list_type[index] in ('S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger'): + color = (1.3, 1.2, 3.0) + else: + color = (1.0, 1.0, 1.0) + pos = (x * (button_width + 2 * button_buffer_h) + + button_buffer_h, self._sub_height - (y + 1) * + (button_height + 2 * button_buffer_v) + 12) + btn = ba.buttonwidget( + parent=self._subcontainer, + button_type='square', + position=(pos[0], + pos[1]), + size=(button_width, button_height), + autoselect=True, + texture=ba.gettexture(bot_list[index] + 'Icon'), + tint_texture=ba.gettexture( + bot_list[index] + 'IconColorMask'), + mask_texture=mask_texture, + label='', + color=color, + tint_color=tint1, + tint2_color=tint2, + on_activate_call=ba.Call(self._select_character, + character=bot_list_type[index])) + ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + + name = bot_list_type[index] + ba.textwidget(parent=self._subcontainer, + text=name, + position=(pos[0] + button_width * 0.5, + pos[1] - 12), + size=(0, 0), + scale=0.5, + maxwidth=button_width, + draw_controller=btn, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8, 0.8)) + + index += 1 + if index >= len(bot_list): + break + if index >= len(bot_list): + break + + def _select_character(self, character: str) -> None: + if self._delegate is not None: + self._delegate.on_bots_picker_pick(character) + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() + + +class PowerPicker(popup.PopupWindow): + """Popup window for selecting power up.""" + + def __init__(self, + parent: ba.Widget, + position: tuple[float, float] = (0.0, 0.0), + delegate: Any = None, + scale: float | None = None, + offset: tuple[float, float] = (0.0, 0.0), + selected_character: str | None = None): + del parent # unused here + + if scale is None: + uiscale = ba.app.ui.uiscale + scale = (1.85 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + + self._delegate = delegate + self._transitioning_out = False + + count = 7 + + columns = 3 + rows = int(math.ceil(float(count) / columns)) + + button_width = 100 + button_height = 100 + button_buffer_h = 10 + button_buffer_v = 15 + + self._width = (10 + columns * (button_width + 2 * button_buffer_h) * + (1.0 / 0.95) * (1.0 / 0.8)) + self._height = self._width * (0.8 + if uiscale is ba.UIScale.SMALL else 1.06) + + self._scroll_width = self._width * 0.8 + self._scroll_height = self._height * 0.8 + self._scroll_position = ((self._width - self._scroll_width) * 0.5, + (self._height - self._scroll_height) * 0.5) + + # creates our _root_widget + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=(0.5, 0.5, 0.5), + offset=offset, + focus_position=self._scroll_position, + focus_size=(self._scroll_width, + self._scroll_height)) + + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + size=(self._scroll_width, + self._scroll_height), + color=(0.55, 0.55, 0.55), + highlight=False, + position=self._scroll_position) + + ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 5 + rows * (button_height + + 2 * button_buffer_v) + 100 + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False) + + power_list = (['Bomb', 'Curse', 'Health', 'IceBombs', + 'ImpactBombs', 'LandMines', 'Punch', + 'Shield', 'StickyBombs']) + + power_list_type = (['Tripple Bomb', 'Curse', 'Health', 'Ice Bombs', + 'Impact Bombs', 'Land Mines', 'Punch', + 'Shield', 'Sticky Bombs']) + + index = 0 + for y in range(rows): + for x in range(columns): + pos = (x * (button_width + 2 * button_buffer_h) + + button_buffer_h, self._sub_height - (y + 1) * + (button_height + 2 * button_buffer_v) + 12) + btn = ba.buttonwidget( + parent=self._subcontainer, + button_type='square', + position=(pos[0], + pos[1]), + size=(button_width, button_height), + autoselect=True, + texture=ba.gettexture('powerup' + power_list[index]), + label='', + color=(1, 1, 1), + on_activate_call=ba.Call(self._select_power, + power=power_list[index])) + ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + + name = power_list_type[index] + ba.textwidget(parent=self._subcontainer, + text=name, + position=(pos[0] + button_width * 0.5, + pos[1] - 12), + size=(0, 0), + scale=0.5, + maxwidth=button_width, + draw_controller=btn, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8, 0.8)) + + index += 1 + if index >= len(power_list): + break + if index >= len(power_list): + break + + def _select_power(self, power: str) -> None: + if self._delegate is not None: + self._delegate.on_power_picker_pick(power) + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() + + +class InfoWindow(popup.PopupWindow): + """Popup window for Infos.""" + + def __init__(self, + parent: ba.Widget, + position: tuple[float, float] = (0.0, 0.0), + delegate: Any = None, + scale: float | None = None, + offset: tuple[float, float] = (0.0, 0.0), + selected_character: str | None = None): + del parent # unused here + + if scale is None: + uiscale = ba.app.ui.uiscale + scale = (1.85 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + + self._delegate = delegate + self._transitioning_out = False + + self._width = 600 + self._height = self._width * (0.6 + if uiscale is ba.UIScale.SMALL else 0.795) + + # creates our _root_widget + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=(0.5, 0.5, 0.5), + offset=offset, + focus_size=(self._width, + self._height)) + + ba.textwidget(parent=self.root_widget, + position=(self._width * 0.5, + self._height * 0.9), + size=(0, 0), + color=ba.app.ui.title_color, + scale=1.3, + h_align='center', + v_align='center', + text='About', + maxwidth=200) + + text = ('Practice Tools Mod\n' + 'Made By Cross Joy\n' + 'version 1.0\n' + '\n' + 'Thx to\n' + 'Mikirog for the Bomb radius visualizer mod.\n' + ) + + lines = text.splitlines() + line_height = 16 + scale_t = 0.56 + + voffs = 0 + i = 0 + for line in lines: + i += 1 + if i <= 3: + color = (1.0, 1.0, 1.0, 1.0) + else: + color = (0.4, 1.0, 1.4, 1.0) + + ba.textwidget( + parent=self.root_widget, + padding=4, + color=color, + scale=scale_t, + flatness=1.0, + size=(0, 0), + position=(self._width * 0.5, self._height * 0.8 + voffs), + h_align='center', + v_align='top', + text=line) + voffs -= line_height + + text_spacing = 70 + + self.button_discord = ba.buttonwidget( + parent=self.root_widget, + position=( + self._width * 0.25 - 40, self._height * 0.2 - 40), + size=(80, 80), + autoselect=True, + button_type='square', + color=(0.447, 0.537, 0.854), + label='', + on_activate_call=self._discord) + ba.imagewidget( + parent=self.root_widget, + position=(self._width * 0.25 - 25, + self._height * 0.2 - 25), + size=(50, 50), + draw_controller=self.button_discord, + texture=ba.gettexture('discordLogo'), + color=(5, 5, 5)) + ba.textwidget( + parent=self.root_widget, + position=(self._width * 0.25, + self._height * 0.2 + text_spacing), + size=(0, 0), + scale=0.75, + draw_controller=self.button_discord, + text='Join us. :D', + h_align='center', + v_align='center', + maxwidth=150, + color=(0.447, 0.537, 0.854)) + + self.button_github = ba.buttonwidget( + parent=self.root_widget, + position=( + self._width * 0.5 - 40, self._height * 0.2 - 40), + size=(80, 80), + autoselect=True, + button_type='square', + color=(0.129, 0.122, 0.122), + label='', + on_activate_call=self._github) + ba.imagewidget( + parent=self.root_widget, + position=(self._width * 0.5 - 25, + self._height * 0.2 - 25), + size=(50, 50), + draw_controller=self.button_github, + texture=ba.gettexture('githubLogo'), + color=(1, 1, 1)) + ba.textwidget( + parent=self.root_widget, + position=(self._width * 0.5, + self._height * 0.2 + text_spacing), + size=(0, 0), + scale=0.75, + draw_controller=self.button_github, + text='Found Bugs?', + h_align='center', + v_align='center', + maxwidth=150, + color=(0.129, 0.122, 0.122)) + + self.button_support = ba.buttonwidget( + parent=self.root_widget, + position=( + self._width * 0.75 - 40, self._height * 0.2 - 40), + size=(80, 80), + autoselect=True, + button_type='square', + color=(0.83, 0.69, 0.21), + label='', + on_activate_call=self._support) + ba.imagewidget( + parent=self.root_widget, + position=(self._width * 0.75 - 25, + self._height * 0.2 - 25), + size=(50, 50), + draw_controller=self.button_support, + texture=ba.gettexture('heart'), + color=(1, 1, 1)) + ba.textwidget( + parent=self.root_widget, + position=(self._width * 0.75, + self._height * 0.2 + text_spacing), + size=(0, 0), + scale=0.75, + draw_controller=self.button_support, + text='Support uwu.', + h_align='center', + v_align='center', + maxwidth=150, + color=(0.83, 0.69, 0.21)) + + def _discord(self): + ba.open_url('https://discord.gg/JyBY6haARJ') + + def _github(self): + ba.open_url('https://github.com/CrossJoy/Bombsquad-Modding') + + def _support(self): + ba.open_url('https://www.buymeacoffee.com/CrossJoy') + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out()