diff --git a/CHANGELOG.md b/CHANGELOG.md index 45cca48..9390c7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Plugin Manager (dd-mm-yyyy) +### 1.1.10 (12-06-2026) + +- Fix for older bs versions using `EXPORT_CLASS_NAME_SHORTCUTS` import + +### 1.1.9 (11-06-2026) + +- Fix for bs 1.7.63 + ### 1.1.8 (23-02-2026) - Fix for bs 1.7.61 build no 22714 diff --git a/index.json b/index.json index f9bc733..120a1ba 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,18 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { + "1.1.10": { + "api_version": 9, + "commit_sha": "a1baa5f", + "released_on": "12-06-2026", + "md5sum": "ee4f7e3e88c2982bc8290e204b10b050" + }, + "1.1.9": { + "api_version": 9, + "commit_sha": "3adfe38", + "released_on": "11-06-2026", + "md5sum": "d7e990ea4e5c530f780e617c9bdafb42" + }, "1.1.8": { "api_version": 9, "commit_sha": "8222081", diff --git a/plugin_manager.py b/plugin_manager.py index fce6017..8d3300d 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -2,7 +2,6 @@ import babase import bauiv1 as bui from bauiv1lib import popup, confirm -from babase._meta import EXPORT_CLASS_NAME_SHORTCUTS from bauiv1lib.settings.allsettings import AllSettingsWindow import urllib.request @@ -26,7 +25,7 @@ from datetime import datetime # Modules used for overriding AllSettingsWindow import logging -PLUGIN_MANAGER_VERSION = "1.1.8" +PLUGIN_MANAGER_VERSION = "1.1.10" REPOSITORY_URL = "https://github.com/bombsquad-community/plugin-manager" # Current tag can be changed to "staging" or any other branch in # plugin manager repo for testing purpose. @@ -82,7 +81,7 @@ REGEXP = { "plugin_entry_points": re.compile( bytes( "(ba_meta export (plugin|{})\n+class )(.*)\\(".format( - _regexp_friendly_class_name_shortcut(EXPORT_CLASS_NAME_SHORTCUTS["plugin"]), + _regexp_friendly_class_name_shortcut("babase.Plugin"), ), "utf-8" ), diff --git a/plugins/minigames.json b/plugins/minigames.json index 969670f..932471c 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1663,6 +1663,61 @@ "md5sum": "33e6b93420cf6115e992aca534d76bfd" } } + }, + "volley_punch": { + "description": "Have fun with your friends in this new volley game !", + "external_url": "https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", + "authors": [ + { + "name": "Learn.py", + "email": "phillipians36@gmail.com", + "discord": "Learning .py" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "a6d11fd", + "released_on": "02-06-2026", + "md5sum": "a2d73ebe4cb84b723242c187ad5b5fbb" + }, + "1.0.0": { + "api_version": 9, + "commit_sha": "576a9c0", + "released_on": "01-06-2026", + "md5sum": "be998b599896ed0e8dd5f2545fae6173" + } + } + }, + "vanishing_tiles": { + "description": "Tiles disappear gradually. Survive each round to continue. Last player standing wins!", + "external_url": "https://discord.gg/sQGDsztQcy", + "authors": [ + { + "name": "Mr.Paradox", + "email": "", + "discord": "gaurangbroyo" + }, + { + "name": "senchx", + "email": "", + "discord": "senchx0" + } + ], + "versions": { + "2.1.1": { + "api_version": 9, + "commit_sha": "5d7d222", + "released_on": "11-06-2026", + "md5sum": "d71b9615872ba3655a83166dd211ecd2" + }, + "2.1.0": { + "api_version": 9, + "commit_sha": "0749053", + "released_on": "10-06-2026", + "md5sum": "152e2e149b2686619c44aa7880a7e460" + } + } } } } \ No newline at end of file diff --git a/plugins/minigames/vanishing_tiles.py b/plugins/minigames/vanishing_tiles.py new file mode 100644 index 0000000..5a75efe --- /dev/null +++ b/plugins/minigames/vanishing_tiles.py @@ -0,0 +1,414 @@ +# ba_meta require api 9 +from __future__ import annotations +from bascenev1lib.gameutils import SharedObjects +import bascenev1 as bs +import babase +import random +from typing import Optional, List, Dict, Any + +# Vanishing Tiles - BombSquad Minigame +# Tiles disappear one by one. Survive all rounds to win. +# Last player/team standing takes the crown. + +plugman = dict( + plugin_name="vanishing_tiles", + description="Tiles disappear gradually. Survive each round to continue. Last player standing wins!", + external_url="https://discord.gg/sQGDsztQcy", + authors=[ + {"name": "Mr.Paradox", "email": "", "discord": "gaurangbroyo"}, + {"name": "senchx", "email": "", "discord": "senchx0"}, + ], + version="2.1.1", +) + + +class Player(bs.Player['Team']): + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class Team(bs.Team[Player]): + pass + + +# ba_meta export bascenev1.GameActivity +class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): + + name = 'Vanishing Tiles' + description = 'Tiles disappear one by one. Be the last one standing!' + scoreconfig = bs.ScoreConfig(label='Survived', scoretype=bs.ScoreType.MILLISECONDS) + announce_player_deaths = True + + @classmethod + def get_available_settings(cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + return [ + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Show Credits', default=True), + ] + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, (bs.FreeForAllSession, bs.DualTeamSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Vanishing Tiles Arena'] + + def __init__(self, settings: dict) -> None: + super().__init__(settings) + self._epic_mode: bool = bool(settings.get('Epic Mode', False)) + self._show_credits: bool = bool(settings.get('Show Credits', True)) + self.default_music = bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + + self._round: int = 1 + self._removing: bool = False + self._game_start_time: Optional[float] = None + + self._tile_nodes: Dict[int, bs.Node] = {} + self._region_nodes: Dict[int, bs.Node] = {} + self._present_tile_ids: set[int] = set() + + self._remove_speed: float = 1.8 + + self._collide_mat = bs.Material() + self._collide_mat.add_actions(actions=(('modify_part_collision', 'collide', True),)) + self._no_collide_mat = bs.Material() + self._no_collide_mat.add_actions(actions=(('modify_part_collision', 'collide', False),)) + + self._default_tex = bs.gettexture('powerupHealth') + self._warn_tex = bs.gettexture('powerupCurse') + self._final_tex = bs.gettexture('powerupPunch') + + self._hud_round: Optional[bs.Node] = None + self._hud_players: Optional[bs.Node] = None + self._hud_tiles: Optional[bs.Node] = None + self._credit_node: Optional[bs.Node] = None + + if self._epic_mode: + self.slow_motion = True + + def spawn_player(self, player: Player) -> Any: + if isinstance(self.session, bs.FreeForAllSession): + spaz = self.spawn_player_spaz(player, position=VanishingTilesMapDefs.points['spawn1']) + else: + spaz = self.spawn_player_spaz(player) + spaz.connect_controls_to_player(enable_punch=False, enable_pickup=False, enable_bomb=False) + spaz.set_bomb_count(0) + return spaz + + def on_begin(self) -> None: + super().on_begin() + self._game_start_time = bs.time() + + if self._show_credits: + self._credit_node = bs.newnode('text', attrs={ + 'text': 'Made by Mr.Paradox', + 'scale': 0.7, 'position': (0, 8), 'shadow': 0.8, 'flatness': 1.0, + 'color': (1.0, 0.3, 0.8, 1.0), 'h_align': 'center', 'v_attach': 'bottom', + }) + bs.animate_array(self._credit_node, 'color', 4, { + 0.0: [1.0, 0.3, 0.8, 1.0], 1.0: [0.3, 0.8, 1.0, 1.0], + 2.0: [0.4, 1.0, 0.4, 1.0], 3.0: [1.0, 1.0, 0.2, 1.0], + 4.0: [1.0, 0.3, 0.8, 1.0], + }, loop=True) + + self._build_hud() + self._cleanup_tiles() + self._spawn_all_tiles() + self._show_round_banner() + + if not isinstance(self.session, bs.FreeForAllSession): + if not all(len(t.players) >= 1 for t in self.teams): + bs.broadcastmessage('Not enough players - draw!', color=(1, 1, 0)) + bs.timer(1.0, lambda: self.end(bs.GameResults())) + return + + bs.timer(1.0, self._check_initial_state) + bs.timer(3.0, self._start_removal) + + def _build_hud(self) -> None: + for attr in ('_hud_round', '_hud_players', '_hud_tiles'): + node = getattr(self, attr, None) + if node is not None and node.exists(): + node.delete() + setattr(self, attr, None) + + self._hud_round = bs.newnode('text', attrs={ + 'text': '', 'scale': 0.85, 'position': (0, -40), 'maxwidth': 300, + 'h_align': 'center', 'v_align': 'center', 'v_attach': 'top', 'h_attach': 'center', + 'shadow': 1.0, 'flatness': 1.0, 'color': (1, 1, 1, 1), 'in_world': False, + }) + self._hud_players = bs.newnode('text', attrs={ + 'text': '', 'scale': 0.8, 'position': (0, -58), 'maxwidth': 300, + 'h_align': 'center', 'v_align': 'center', 'v_attach': 'top', 'h_attach': 'center', + 'shadow': 1.0, 'flatness': 1.0, 'color': (1, 0.8, 0.2, 1), 'in_world': False, + }) + self._hud_tiles = bs.newnode('text', attrs={ + 'text': '', 'scale': 0.8, 'position': (0, -74), 'maxwidth': 300, + 'h_align': 'center', 'v_align': 'center', 'v_attach': 'top', 'h_attach': 'center', + 'shadow': 1.0, 'flatness': 1.0, 'color': (0.5, 1.0, 0.5, 1), 'in_world': False, + }) + self._refresh_hud() + + def _refresh_hud(self) -> None: + alive = len([p for p in self.players if p.is_alive()]) + tiles = len(self._present_tile_ids) + urgent = tiles <= 4 + + if self._hud_round: + self._hud_round.text = f'Round {self._round}' + if self._hud_players: + self._hud_players.text = f'Players alive: {alive}' + if self._hud_tiles: + self._hud_tiles.color = (1.0, 0.3, 0.3, 1) if urgent else (0.5, 1.0, 0.5, 1) + self._hud_tiles.text = f'Tiles left: {tiles}' + + def _show_round_banner(self) -> None: + node = bs.newnode('text', attrs={ + 'text': f'Round {self._round}', 'scale': 1.3, 'position': (0, 60), + 'shadow': 1.2, 'flatness': 0.7, 'color': (1, 1, 0, 1), + 'h_align': 'center', 'v_attach': 'center', 'in_world': False, + }) + bs.animate(node, 'scale', {0: 0.0, 0.15: 1.3, 2.0: 1.3, 2.5: 0.0}) + bs.timer(2.6, node.delete) + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + player.death_time = bs.time() + bs.broadcastmessage( + f'{player.getname()} joined! Round {self._round} - {len(self._present_tile_ids)} tiles left.', + color=(0.5, 1.0, 1.0), transient=True, + ) + return + self.spawn_player(player) + + def _check_initial_state(self) -> None: + if not [p for p in self.players if p.is_alive()]: + bs.timer(1.0, self._check_end_game) + + def _spawn_all_tiles(self) -> None: + positions = [ + (4.5, 2, -9), (4.5, 2, -6), (4.5, 2, -3), (4.5, 2, 0), + (1.5, 2, -9), (1.5, 2, -6), (1.5, 2, -3), (1.5, 2, 0), + (-1.5, 2, -9), (-1.5, 2, -6), (-1.5, 2, -3), (-1.5, 2, 0), + (-4.5, 2, -9), (-4.5, 2, -6), (-4.5, 2, -3), (-4.5, 2, 0), + ] + model = bs.getmesh('buttonSquareOpaque') + shared = SharedObjects.get() + for i, pos in enumerate(positions): + tile = bs.newnode('prop', attrs={ + 'body': 'puck', 'position': pos, 'mesh': model, + 'mesh_scale': 3.73, 'body_scale': 3.73, 'gravity_scale': 0.0, + 'color_texture': self._default_tex, 'reflection': 'soft', + 'materials': [self._no_collide_mat], + }) + region = bs.newnode('region', attrs={ + 'position': pos, 'scale': (3.5, 0.1, 3.5), 'type': 'box', + 'materials': [self._collide_mat, shared.footing_material], + }) + self._tile_nodes[i] = tile + self._region_nodes[i] = region + self._present_tile_ids.add(i) + self._refresh_hud() + + def _start_removal(self) -> None: + if not self._removing: + self._removing = True + self._remove_next_tile() + + def _make_final_tile(self) -> None: + try: + tile_id = list(self._present_tile_ids)[0] + tile = self._tile_nodes.get(tile_id) + if tile and tile.exists(): + tile.color_texture = self._final_tex + except Exception: + pass + + self.slow_motion = True + bs.broadcastmessage('LAST TILE!', color=(1, 0.4, 0.1)) + for p in self.players: + if p.is_alive() and p.actor and p.actor.exists(): + p.actor.node.handlemessage(bs.CelebrateMessage(3.0)) + self._refresh_hud() + bs.timer(3.0, self._round_end_check) + + def _remove_next_tile(self) -> None: + if len(self._present_tile_ids) <= 1: + self._make_final_tile() + return + + if len(self._present_tile_ids) <= 4: + bs.getsound('shieldDown').play(0.4) + + tile_id = random.choice(list(self._present_tile_ids)) + self._remove_tile(tile_id) + bs.timer(self._remove_speed, self._remove_next_tile) + + def _remove_tile(self, tile_id: int) -> None: + tile = self._tile_nodes.get(tile_id) + region = self._region_nodes.get(tile_id) + if tile is None or not tile.exists(): + self._present_tile_ids.discard(tile_id) + return + + tile.color_texture = self._warn_tex + + def vanish() -> None: + if tile.exists(): + bs.emitfx(position=tile.position, count=20, scale=0.9, + spread=0.5, chunk_type='spark') + tile.delete() + if region and region.exists(): + region.delete() + self._present_tile_ids.discard(tile_id) + self._refresh_hud() + + bs.timer(1.0, vanish) + + def _round_end_check(self) -> None: + self.slow_motion = self._epic_mode + + if not isinstance(self.session, bs.FreeForAllSession): + living_teams = [t for t in self.teams if any(p.is_alive() for p in t.players)] + if len(living_teams) <= 1: + self._removing = False + self.end_game() + return + else: + if len([p for p in self.players if p.is_alive()]) <= 1: + self._removing = False + self.end_game() + return + + self._remove_speed = max(0.3, self._remove_speed * 0.75) + self._round += 1 + survivors = [p for p in self.players if p.is_alive()] + bs.broadcastmessage(f'Round {self._round}! Speed increasing!', color=(0.5, 1.0, 1.0)) + bs.timer(1.5, lambda s=survivors: self._next_round(s)) + + def _next_round(self, survivors: List[Player]) -> None: + safety = bs.newnode('region', attrs={ + 'position': (0, 1.5, -5), 'scale': (20, 0.2, 20), 'type': 'box', + 'materials': [SharedObjects.get().footing_material], + }) + self._cleanup_tiles() + self._spawn_all_tiles() + self._show_round_banner() + self._refresh_hud() + + for p in survivors: + if p.actor and p.actor.exists(): + p.actor.handlemessage(bs.StandMessage(VanishingTilesMapDefs.points['spawn1'])) + + bs.timer(2.5, safety.delete) + self._removing = False + bs.timer(3.0, self._start_removal) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) + msg.getplayer(Player).death_time = bs.time() + self._refresh_hud() + bs.timer(0.5, self._check_end_game) + return None + return super().handlemessage(msg) + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + bs.timer(0.2, self._check_team_empty) + + def _check_team_empty(self) -> None: + if self.has_ended(): + return + if isinstance(self.session, bs.FreeForAllSession): + return + for team in self.teams: + if len(team.players) == 0: + bs.broadcastmessage('A team has no players - game over!', color=(1, 1, 0)) + bs.timer(1.0, self.end_game) + return + + def _check_end_game(self) -> None: + if not self.has_begun(): + return + if not isinstance(self.session, bs.FreeForAllSession): + if len([t for t in self.teams if any(p.is_alive() for p in t.players)]) <= 1: + self.end_game() + else: + if len([p for p in self.players if p.is_alive()]) <= 1: + self.end_game() + + def end_game(self) -> None: + if self.has_ended(): + return + cur_time = bs.time() + start = self._game_start_time or cur_time + results = bs.GameResults() + for team in self.teams: + longest = 0.0 + for p in team.players: + death = p.death_time or (cur_time + 1) + longest = max(longest, death - start) + results.set_team_score(team, int(longest * 1000)) + + for attr in ('_hud_round', '_hud_players', '_hud_tiles'): + node = getattr(self, attr, None) + if node is not None and node.exists(): + node.delete() + + if self._credit_node and self._credit_node.exists(): + self._credit_node.delete() + + self.end(results=results) + + def _cleanup_tiles(self) -> None: + for n in list(self._tile_nodes.values()): + if n.exists(): + n.delete() + for n in list(self._region_nodes.values()): + if n.exists(): + n.delete() + self._tile_nodes.clear() + self._region_nodes.clear() + self._present_tile_ids.clear() + + +class VanishingTilesMapDefs: + points = {'spawn1': (0, 3, -5)} + boxes = { + 'area_of_interest_bounds': (0, 4, -5, 0, 0, 0, 16, 8, 16), + 'map_bounds': (0, 4, -5, 0, 0, 0, 30, 14, 30), + } + + +class VanishingTilesMap(bs.Map): + defs = VanishingTilesMapDefs() + name = 'Vanishing Tiles Arena' + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'powerupHealth' + + @classmethod + def on_preload(cls) -> Any: + return {'bgtex': bs.gettexture('menuBG'), 'bgmesh': bs.getmesh('thePadBG')} + + def __init__(self) -> None: + super().__init__() + self.node = bs.newnode('terrain', attrs={ + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, + 'background': True, 'color_texture': self.preloaddata['bgtex'], + }) + + +try: + bs._map.register_map(VanishingTilesMap) +except Exception: + pass + + +# ba_meta export babase.Plugin +class Main(babase.Plugin): + pass diff --git a/plugins/minigames/volley_punch.py b/plugins/minigames/volley_punch.py new file mode 100644 index 0000000..f5260e4 --- /dev/null +++ b/plugins/minigames/volley_punch.py @@ -0,0 +1,1293 @@ +# VolleyPunch ? ig i'll name it like that + +# Made by Learn.py +# but this was def inspired by some freaky guy + + +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import _babase +import math +import random +import bascenev1 as bs +import bauiv1 as bui +import math +import time +import babase +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.actor.bomb import BombFactory +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +plugman = dict( + plugin_name="volley_punch", + description="Have fun with your friends in this new volley game !", + external_url="https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", + authors=[ + {"name": "Learn.py", "email": "phillipians36@gmail.com", "discord": "Learning .py"}, + ], + version="1.1.0", +) + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +class Puck(bs.Actor): + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.05, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + + self.touches = 0 + assert activity is not None + assert isinstance(activity, VolleyBallGame) + pmats = [shared.object_material, activity.puck_material] + + # so this is the volleyball we gonna mesh with today + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': activity.puck_mesh, + 'color_texture': activity.puck_tex, + 'body': 'sphere', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.3, + 'mesh_scale': activity.ball_size, + 'body_scale': 1.07, + 'gravity_scale': 1, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + # copied that stuff from hockey game ngl + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class VolleyBallGame(bs.TeamGameActivity[Player, Team]): + name = 'VolleyPunch' + description = 'Punch the Volleyball !' + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.FloatSetting( + 'Ball size (m)', + min_value=0.15, + default=0.25, + max_value=0.35, + increment=0.05, + ), + bs.FloatChoiceSetting( + 'Ball Average Speed', + choices=[ + ('Slower', 0.9899), + ('Normal', 1.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', False), + bs.BoolSetting('Enable Credits', True), + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = bs.getsound('cheer') + self._chant_sound = bs.getsound('crowdChant') + self.one_sound = bs.getsound('announceOne') + self.two_sound = bs.getsound('announceTwo') + self.three_sound = bs.getsound('announceThree') # unused.. yet + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self.puck_mesh = bs.getmesh('shield') + self.puck_tex = bs.gettexture('ouyaUButton') + self._puck_sound = bs.getsound('metalHit') + self.puck_material = bs.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 4))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.puck_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._puck_sound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), + ('modify_part_collision', 'physical', False))) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', bs.DieMessage()))) + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + + self._fake_wall_material = bs.Material() # i'll fix this. trust + + self._net_wall_material = bs.Material() + self._net_material = bs.Material() + self._net_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._net_material.add_actions( + conditions=('they_have_material', shared.object_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True), + ('modify_part_collision', + 'friction', -10), + ('call', 'at_connect', self.slow_down))) + + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[bs.NodeActor]] = None + self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + self.credit_text = bool(settings['Enable Credits']) # i did the freaku credit thing + self._epic_mode = bool(settings['Epic Mode']) + + # the ball is too fast for the players and the map is soo huge its a good poc - brostos + self.ball_size = float(settings['Ball size (m)']) + self.ball_speed = float(settings['Ball Average Speed']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + self.s: list = [] + self.w: list = [] + self.n: list = [] + self.last_interaction_time = -9999 + self.smashed = -9999 + self.interactions_timer = None + self.curve = True + self.difficulty = 0.5 + self.smasher = None + self.commentary_text = None + self._text_task = None # Référence pour annuler l'animation en cours + self._clear_timer = None # Référence pour le timer de disparition + self.touches_indicator = None + self.guide = None + self.curving = False + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Score a goal.' + return 'Score ${ARG1} goals.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'score a goal' + return 'score ${ARG1} goals', self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + self._puck_spawn_pos = (-4, 1, 0) if random.random() < 0.5 else (4, 1, 0) + self._spawn_puck() + + # Set up the two score regions. + self._score_regions = [] + self.commentary_text = bs.newnode( + 'text', + attrs=dict( + text='', + scale=0.012, + position=(0, 3.1, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + self.touches_indicator = bs.newnode( + 'text', + attrs=dict( + text='', + scale=0.025, + position=(0, 3, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + self.guide = bs.newnode( + 'text', + attrs=dict( + text='PUNCH TO TAP THE BALL \nPICKUP TO CUFF \nBOMB TO GLIDE / DIVE \nJUMP UNDER A MID AIR BALL WITH THE BAR FILLED TO SMASH \nTHE ULT BAR FILLS AS YOU PLAY', + scale=0.005, + position=(0, 2.7, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + bs.animate_array(self.guide, 'color', 4, {0: (1, 1, 1, 0.5), 1: ( + 1, 1, 1, 1), 2: (1, 1, 1, 0.5)}, loop=True) + self.interactions_timer = bs.Timer( + 1 / 120, self.interactions, repeat=True) # time, function, repeat + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (9, 0, 0), + 'scale': (17.3, 0.002, 12), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (-9, 0, 0), + 'scale': (17.3, 0.002, 20), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._update_scoreboard() + self._chant_sound.play() + if self.credit_text: + t = bs.newnode('text', + attrs={'text': "Created by Learn.py", + 'scale': 0.7, + 'position': (0, 0), + 'shadow': 0.5, + 'flatness': 1.2, + 'color': (1, 1, 1), + 'h_align': 'center', + 'v_attach': 'bottom'}) + + # Create the net and regions. + mat = bs.Material() # spam this whenever u want a material THEN add some properties + mat.add_actions(actions=('modify_part_collision', + 'collide', False)) + net = bs.newnode( + 'prop', + attrs={ + 'body': 'puck', + 'body_scale': 0, + 'position': (0, 0, 0), + 'mesh_scale': 1, + 'mesh': bs.getmesh('volleyball_net'), + 'color_texture': bs.gettexture('black'), + 'reflection': 'soft', + 'reflection_scale': [1.5], + 'sticky': False, + 'shadow_size': 0.2, + 'gravity_scale': 0, + 'materials': [mat], + }, + ) + + self.s.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 2.4, 0), 'scale': ( + 0.8, 6, 20), 'type': 'box', 'materials': (self._fake_wall_material, )}))) + + self.w.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( + 0.6, 10, 20), 'type': 'box', 'materials': (self._net_wall_material, )}))) + self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( + 0.6, 2.75, 20), 'type': 'box', 'materials': (self._net_material, )}))) + self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, -5.9), 'scale': ( + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) + self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 5.9), 'scale': ( + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) + + def slow_down(self): + if not self._puck: + return + puck_node = self._puck.node + mult = 0.88 + v = puck_node.velocity + puck_node.velocity = (v[0] * mult, v[1] * mult, v[2] * mult) + + def write(self, text, color): + """types""" + if hasattr(self, '_timers'): + for t in self._timers: + t = None + self._timers = [] + + if not text: + return + + self.commentary_text.color = color + total_chars = len(text) + + interval = 0.9 / max(total_chars, 1) + + for i in range(1, total_chars + 1): + def show(index=i): + if self.commentary_text.exists(): + self.commentary_text.text = text[:index] + + self._timers.append(bs.Timer(interval * i, show, repeat=False)) + + def start_clear(): + for i in range(total_chars, -1, -1): + def hide(index=i): + if self.commentary_text.exists(): + self.commentary_text.text = text[:index] + + self._timers.append( + bs.Timer(0.9 + 3.0 + (0.015 * (total_chars - i)), hide, repeat=False)) + + start_clear() + + def vignette(self, final_color, longer): + """Aesthetic reasons.""" + glb = bs.getactivity().globalsnode + base_color = (0.57, 0.57, 0.57) + bs.animate_array(glb, 'vignette_outer', 3, {0: base_color, 0.1: ( + final_color[0] / 1.2, final_color[1] / 1.2, final_color[2] / 1.2), (1 if longer else 0.5): base_color}) + + def interactions(self): + """Some personalized stuff and my deepest secrets. + I don't know why, but I feel SoK may like this.""" + if self._puck is None: + return + + vel = self._puck.node.velocity + # apply the mult + self._puck.node.velocity = ( + vel[0], + vel[1] * self.ball_speed, + vel[2] + ) + + green = bs.gettexture('ouyaOButton') + yellow = bs.gettexture('ouyaYButton') + red = bs.gettexture('ouyaAButton') + blue = bs.gettexture('ouyaUButton') + + # If it passes the middle.. green. + puck_node = self._puck.node + pos = puck_node.position + x_diff = abs(pos[0]) + + if x_diff < 0.25 or self.curve: + puck_node.color_texture = blue + self._puck.touches = 0 + + if pos[1] < 2.6 and (bs.time() - self.last_interaction_time > 0.5): + mult = 0.935 + v = puck_node.velocity + puck_node.velocity = (v[0] * mult, v[1] * mult, v[2] * mult) + + self.save_text = random.choice([ + "dived to the rescue!", + "kept the ball alive!", + "flew in to save the day!", + "is too fast for the floor!", + "bounced it back just in time!", + "kept the dream alive!", + "barely missed the ground!", + "saved the rally!", + "made a clutch save!", + "refused to let it drop!", + "pulled a superman save!", + "made a desperate save!", + "proved the floor is not an option!", + "landed a miracle touch!", + "saved the point!", + "pulled off a heroic dive!", + "stretched to the limit!", + "barely kept it up!", + "showed reflexes of a cat!", + "denied the floor!", + "made a clutch save!", + "is staying in the game!", + "pulled an epic recovery!", + "reacted in the nick of time!", + "won't let the ball drop!", + "is pure hustle!", + "made an impossible save!", + "is defying gravity!", + "made sure the ground stays empty!", + "saved it!", + "pulled off last-second magic!", + "is floating to save it!", + "is too clutch to fail!", + "got the ball back in the air!", + "is denying the dirt!", + "saved it by a hair!", + "is fighting for every ball!", + "is pure energy!", + "never gives up!", + "keeps it in play!", + "saved it barely!", + "kept the floor clean!", + "is pure determination!", + "pulled a miraculous save!", + "denied the game over!", + "is still alive!", + "is keeping the rally going!", + "made an out-of-reach save!", + "made a ninja-like save!", + "bounced back from trouble!"]) + self.hit_text = random.choice([ + "sent it over!", + "cleared the net!", + "powered it across!", + "nailed a deep shot!", + "bounced it into their zone!", + "launched an attack!", + "sent a rocket over!", + "is dominating the net!", + "found the open spot!", + "placed it perfectly!", + "smashed it back!", + "is applying the pressure!", + "sent them scrambling!", + "over the net it goes!", + "hit a beauty!", + "is controlling the court!", + "forced a tough return!", + "keeps the pressure high!", + "landed a sweet strike!", + "sent it deep!", + "is playing aggressively!", + "hit a laser beam!", + "made a solid connection!", + "is taking charge!", + "sent a tricky shot over!", + "is firing back!", + "put it right on target!", + "sent the ball flying!", + "beat the defense!", + "is crushing it!", + "nailed that serve return!", + "showed off some power!", + "sent a high arc over!", + "is making moves!", + "kept them on their toes!", + "delivered a powerful hit!", + "is pushing the pace!", + "found the gap!", + "sent a curveball over!", + "is looking sharp!", + "hit it with precision!", + "made them work for it!", + "is owning the net!", + "sent a heavy strike!", + "is dictating the play!", + "put it out of reach!", + "landed the perfect hit!", + "is on fire!", + "sent a swift strike!", + "is forcing errors!" + ]) + self.control_text = random.choice([ + "is showing off sweet control!", + "has the ball on a string!", + "handles it with grace!", + "shows off some nice touch!", + "keeps it close and tight!", + "is demonstrating perfect poise!", + "has ice in their veins!", + "is keeping it under control!", + "shows a velvet touch!", + "is masterfully handling the ball!", + "keeps the ball glued to them!", + "is displaying total composure!", + "makes it look so easy!", + "is controlling the pace!", + "shows off pure finesse!", + "has the ball perfectly placed!", + "is staying cool under pressure!", + "has incredible ball handling!", + "is maneuvering with ease!", + "keeps the ball within reach!", + "shows off some fancy footwork!", + "is keeping the ball calm!", + "has the softest touch!", + "is dictating the rhythm!", + "keeps it balanced and steady!", + "is showing high-level control!", + "has the situation handled!", + "is playing with surgical precision!", + "keeps the ball in their zone!", + "shows off expert handling!", + "is keeping it smooth!", + "has the ball right where they want it!", + "demonstrates a gentle touch!", + "is keeping the game slow!", + "shows off calm concentration!", + "has everything under control!", + "is moving with grace!", + "keeps the ball softly hovering!", + "shows off true talent!", + "is maintaining perfect flow!", + "has a magical touch!", + "is playing it smart!", + "keeps the ball very close!", + "shows off professional control!", + "is handling the pressure well!", + "has a very steady hand!", + "is keeping it precise!", + "shows off masterful technique!", + "is keeping the ball in check!", + "has the perfect feel for it!" + ]) + if self.touches_indicator is not None and self.touches_indicator.exists(): + self.touches_indicator.position = (self._puck.node.position[0], self._puck.node.position[1] + 0.35, + self._puck.node.position[2]) + else: + self.touches_indicator = bs.newnode( + 'text', + attrs=dict( + text='', + scale=0.025, + position=(0, 3, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + + everything = bs.getnodes() + everyone = [] + closest_distance = 9999 + closest_player = None + + # i won't explain what "curve" is... look by yourself + if not self.curve and not self._puck.scored: + if bs.time() < self.smashed: + random_pos = (random.uniform( + 0.1, 0.7) + self._puck.node.position[0], self._puck.node.position[1], random.uniform(0.1, 0.7) + self._puck.node.position[2]) + e = bs.newnode( + 'explosion', + attrs={ + 'position': random_pos, + 'color': self.smasher.color, + 'radius': random.uniform(0.1, 0.7), + 'big': False, + } + ) + bs.timer(1, e.delete) + self.smasher.getdelegate(object).percentage /= 1.1 + else: + ball_tex = self._puck.node.color_texture + + texture_to_name = { + green: 'green', + yellow: 'yellow', + red: 'red', + blue: 'blue' + } + + colors = { + 'green': (0.6, 1, 0.6), + 'yellow': (1, 0.3, 0), + 'red': (1, 0, 0), + 'blue': (0.6, 0.6, 1) + } + + color_name = texture_to_name.get(ball_tex) + color = colors.get(color_name) + + e = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': color, + 'radius': 0.1, + 'big': False, + } + ) + indicator_color = (*color, 0.5) + bs.timer(1, e.delete) + self.touches_indicator.color = indicator_color + self.touches_indicator.text = str(3 - self._puck.touches) + + for human in everything: # doesn't make much sense, but I hope u understand I want players nodes. + if human.getnodetype() == "spaz": + everyone.append(human) + + for player_node in everyone: + if not player_node.exists(): # why wouldn't u exist at first + continue + + # not wanting to do that Euclidean thingy + distance = math.dist(player_node.position, self._puck.node.position) + player_node.hold_node = None + player_node.getdelegate(object).impact_scale = 0 + if distance < closest_distance: + closest_player = player_node # him, the closest one + closest_distance = distance # his distance with the volleyball + + good: bool = (self._puck.touches < 3 and + bs.time() - self.last_interaction_time > 0.4 and + not self._puck.scored) + # bs.time() is the current bombsquad time :) bs for bombsquad. + + delegate = closest_player.getdelegate(object) + now = bs.time() + can_jump = (delegate.last_jump_time_ms > + delegate._jump_cooldown and closest_player.position[1] < 1) + + if closest_player.jump_pressed and can_jump: + ppos = closest_player.position + delegate.last_jump_time_ms = int(bs.time() * 1000) + closest_player.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + 0, + 1, + 0, + 50, # small force + ) + + punched = now < delegate.punched + pickup = now < delegate.pickup + bombed = now < delegate.bombed + curved = now < delegate.curved + + # SO if broski is the nearest AND broski is near enough... + if closest_player and closest_distance < 4 and good: + player = spaz = delegate + ball = self._puck.node # ball node + you = closest_player.position # already a node + in_air = (you[1] > 0.8) + # Ball goes where you look but always tries to go to the other side. (tell me if u guys want it to go totally where you're looking at) + if closest_distance < (1.35 if not in_air else 1.6) and (closest_player.punch_pressed or punched): + self.difficulty += 0.6 / 75 + e = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': closest_player.color, + 'radius': 0.8, + 'big': False, + } + ) + bs.timer(1, e.delete) + self.last_interaction_time = bs.time() + self._puck.touches += 1 + self.curve = False + if self._puck.touches == 2: + spaz.percentage = min(spaz.percentage + 8, 100) + ball.color_texture = yellow + self.two_sound.play() + elif self._puck.touches == 3: + spaz.percentage = min(spaz.percentage + 12, 100) + ball.color_texture = red + self.one_sound.play() + else: + ball.color_texture = green + # This is for the sound, you probably didn't know that! + bs.getsound("impactHard").play() + + sight = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward) + ).normalized() * (12 if delegate.percentage < 95 else 30) # Sight factor + speed = 6 if delegate.percentage < 95 else 20 # bar full + + if curved: + start_pos = ball.position + end_pos = (sight[0] * 2, -1, -sight[2]) + duration = 1.1 + fps = 144 + total_steps = int(duration * fps) + curved_time = bs.time() + 0.15 + self.curving = True + + z_offset = -4.0 if sight[0] < 0 else 4.0 + + def move_to_point(step): + if not ball.exists(): + return + if ball.position[1] < 1 and bs.time() > curved_time: + return + if not self.curving: + return + + t = step / total_steps + + x = start_pos[0] + (end_pos[0] - start_pos[0]) * t + y = start_pos[1] + (end_pos[1] - start_pos[1]) * t + z = start_pos[2] + (end_pos[2] - start_pos[2]) * t + + y += math.sin(t * math.pi) * 3.0 + + # Ajout de la courbe latérale + z += math.sin(t * math.pi) * z_offset + + # --- LIMITE SUR L'AXE Z --- + # On force z à rester entre -5 et 5 + z = max(-5.0, min(5.0, z)) + # -------------------------- + + ball.position = (x, y, z) + + if step < total_steps: + bs.timer(1 / fps, lambda: move_to_point(step + 1)) + + move_to_point(0) + else: + # normal + self.curving = False + if in_air and delegate.percentage > 95: + if closest_player.position[0] < 0: + vel = (12 * 1.6, 0, sight[2]) * speed + else: + vel = (-12 * 1.6, 0, sight[2]) * speed + else: + if closest_player.position[0] < 0: + vel = (4.8 * 1.6, 0, sight[2]) * speed + else: + vel = (-4.8 * 1.6, 0, sight[2]) * speed + + # smth + dist_from_middle = math.dist(you, (0, 2, 0)) + height = (max(dist_from_middle * 2.75, 7) * 0.8) + if in_air and delegate.percentage > 95: + height = -4 * 2 / dist_from_middle + + ball.velocity = (vel[0], height, vel[2]) + if in_air and delegate.percentage > 95: + audio = random.choice(['explosion01', 'explosion02', + 'explosion03', 'explosion04', 'explosion05']) + bs.getsound(audio).play() + self.smashed = bs.time() + 1 + self.smasher = closest_player + self.vignette(closest_player.color, False) + delegate.on_punch_press() + delegate.on_punch_release() + self.write( + f'{player.name.upper()} SMASHED THE BALL !' if random.random() < 0.5 else f'{player.name} hit the ball with all his might !', closest_player.color) + else: + self.write( + f'{player.name} {self.hit_text}', closest_player.color) + + # prettiness here + ppos = you + f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() + closest_player.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + f[0], + f[1], + f[2], + 400, # small force + ) + # Ball goes straight up in the air. idk what it is called. + if closest_distance < 1.35 and (closest_player.pickup_pressed or pickup): + self.write( + f'{player.name} {self.control_text}', closest_player.color) + y_diff = abs(self._puck.node.position[1] - closest_player.position[1]) + + if y_diff < 1: + self.difficulty += 0.6 / 75 + self.last_interaction_time = bs.time() + self.curve = False + + self._puck.touches += 1 + + ball = self._puck.node + if self._puck.touches == 2: + spaz.percentage = min(spaz.percentage + 4, 100) + ball.color_texture = yellow + self.two_sound.play() + elif self._puck.touches == 3: + spaz.percentage = min(spaz.percentage + 6, 100) + ball.color_texture = red + self.one_sound.play() + else: + ball.color_texture = green + + closest_player.handlemessage('celebrate', 400) + bs.getsound("impactMedium").play() + e = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': closest_player.color, + 'radius': 0.5, + 'big': True, + } + ) + bs.timer(1, e.delete) + + direction = (bs.Vec3(you) - + bs.Vec3(closest_player.position_forward)).normalized() + ball.velocity = (-you[0] * 0.04, 12.8, direction[2] * 0.1) + if closest_player.bomb_pressed and not delegate.gliding or bombed and not delegate.gliding: # gliding.. variant coming soon + self.write( + f'{player.name} {self.save_text}', closest_player.color) + delegate.gliding = True + ppos = you + f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() + distance = math.dist(ball.position, you) + closest_player.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + f[0], + 0, + f[2], + 1000 - (500/distance), # tiny force + ) + closest_player.handlemessage('knockout', 100) + print(1) + spaz.pickup = now + 0.8 + + bs.getsound("impactHard").play(volume=0.5) + e = bs.newnode( + 'explosion', + attrs={ + 'position': you, + 'color': (0, 0.3, 0.5), + 'radius': 1.1, + 'big': False, + } + ) + bs.timer(1, e.delete) + bs.timer(0.6, lambda: setattr(delegate, 'gliding', False)) + + # variant soon + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = bs.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + puck.last_players_to_touch[player.team.id] = player + + def _kill_puck(self) -> None: + self._puck = None + self.curve = True + + def _handle_score(self) -> None: + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again + # to not be getting a corrupted score. + if self._puck.scored: + return + E = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': (0, 0, 0), + 'radius': .1, + 'big': True, + } + ) + bs.timer(1, E.delete) + bs.animate(self.touches_indicator, 'scale', {0: 0, 2.65: 0, 3: 0.025}) + self.touches_indicator.text = '0' + if self.smashed > 0: + self._puck.node.sticky = True + self.smashed = -9999 + + region = bs.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + team.score += 1 + + # Puck Spawn + if team.id == 0: # left side scored + self._puck_spawn_pos = (5, 0.7, 0) + elif team.id == 1: # right side scored + self._puck_spawn_pos = (-5, 0.7, 0) + else: # what the heck + self._puck_spawn_pos = (0, 0.7, 0) + + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) # yay we scored + player.actor.percentage = min(player.actor.percentage + 15, 100) + # dont mind this ;( + scorch = bs.newnode( + 'scorch', + attrs={ + 'position': self._puck.node.position, + 'size': 1.4, + 'color': player.actor.node.color, + 'big': True, + }, + ) + bs.animate(scorch, 'presence', {3.000: 1, 7.000: 0}) + bs.timer(7, scorch.delete) + self.vignette(player.actor.node.color, longer=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + self._cheer_sound.play() + + self._puck.scored = True + + # Kill the puck (it'll respawn itself shortly). + # Animation ! 0 sec = 0.25 of size in our case + bs.animate(self._puck.node, 'mesh_scale', {0: 0.25, 0.4: 0}) + bs.timer(0.7, self._kill_puck) + + bs.cameraflash(duration=7.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.interactions_timer = None + self.end(results=results) + + def on_transition_in(self) -> None: + super().on_transition_in() + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + spaz.bomb_count = 0 # No bombs for yall bahaha + # Now we want to make that your punches are useless too + # But we will need the button to work so let's put the damage to 0 and some custom stuff + spaz._punch_power_scale = 0 + spaz.impact_scale = 0 + spaz.check_timer = 0 + + spaz.pickup = 0 + spaz.punched = 0 + spaz.bombed = 0 + spaz.curved = 0 + + spaz.can_curve = False + + spaz.last_angle = 0 + spaz.total_rotation = 0.0 + spaz.rotation_direction = 0 # 0: neutre, 1: clockwise, -1: not clockwise + + spaz.gliding = False + spaz.smashed = False + spaz.percentage = 0 + spaz._jump_cooldown = 2000 + spaz.name = spaz.node.name + spaz.bar = bs.newnode( + 'shield', + owner=spaz.node, + attrs={ + 'position': spaz.node.position, + 'radius': .01, + 'color': (0, 0, 0), + } + ) + spaz.node.connectattr('position', spaz.bar, 'position') + spaz.bar.always_show_health_bar = True + spaz.node.name = '' # bahaha but you'll sspazuseful + + def check(): + if spaz.node.exists(): + # Controls part + now = bs.time() + + if spaz.node.punch_pressed: + spaz.punched = now + 0.25 + elif spaz.node.pickup_pressed: + spaz.pickup = now + 0.40 + elif spaz.node.bomb_pressed: + spaz.bombed = now + 0.20 + elif spaz.node.jump_pressed and spaz.percentage > 95: + spaz.punched = now + 0.50 + + if spaz.percentage > 95: + spaz.node.name = 'SMASH' + else: + spaz.node.name = '' + + spaz.bar.hurt = min(1.0 - float(spaz.percentage) / 100, 1) + + # Curved ball part + + pos = spaz.node.position + fwd = spaz.node.position_forward + + dx = fwd[0] - pos[0] + dz = fwd[2] - pos[2] + + length = math.sqrt(dx * dx + dz * dz) + if length > 0.0001: + dx /= length + dz /= length + + spaz.angle = math.degrees(math.atan2(-dx, -dz)) # trust + + # checks if u turn ! Not in the game yet ! + def check_270_rotation(): + now = bs.time() + current_angle = spaz.angle + + delta = current_angle - spaz.last_angle + if delta > 180: + delta -= 360 + if delta < -180: + delta += 360 + + current_dir = 1 if delta > 0 else -1 + + if spaz.rotation_direction != 0 and current_dir != spaz.rotation_direction: + spaz.total_rotation = 0 + + spaz.rotation_direction = current_dir + + spaz.total_rotation += abs(delta) + + if spaz.total_rotation >= 250: + spaz.curved = now + 0.40 + spaz.total_rotation = 0 + + spaz.last_angle = current_angle + + spaz.check_timer1 = bs.Timer(1/30, check, repeat=True) + # spaz.check_timer2 = bs.Timer(1/30, check_270_rotation, repeat=True) + + # spaz.node.hold_node = spaz.node ... nevermind + + # By the way when you have a node but want his player just do + # player = node.getdelegate(object) + # i dont know why you'll need it but here it is + + return spaz + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + # Because if we don't.. they won't like our game ;( + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + bs.timer(2, self._spawn_puck) # 2 secs to breathe bahahaha + else: + super().handlemessage(msg) + + def _spawn_puck(self) -> None: + self._swipsound.play() + self._whistle_sound.play() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) + self.timer = bs.Timer(1 / 144, self.lock_it, repeat=True) # 144 ?.. yes + + def lock_it(self): + if self._puck is None: + return + if self.curve: + self._puck.node.position = self._puck_spawn_pos + else: + self._puck.node.shadow_size = min(self._puck.node.position[1] * 0.3, 2) + + +# ba_meta export babase.Plugin +class justlearn(babase.Plugin): + def __init__(self): + self.installer = ModInstaller() + self.installer.run_full_install() + + +class ModInstaller: + def __init__(self) -> None: + self.python_user = _babase.env()["python_directory_user"] + self.files = self.python_user + '/VolleyBallMap/' + self.app_dir = _babase.env()["python_directory_app"] + '/' + self.data_dir = self.app_dir + '../' + self.meshes_dir = self.data_dir + 'meshes/' + self.platform = _babase.app.classic.platform + + def run_full_install(self) -> None: + import os + import urllib.request + import shutil + import babase + + target_path = os.path.join(self.files, 'volleyball_net.bob') + destination_path = os.path.join(self.meshes_dir, 'volleyball_net.bob') + + if os.path.exists(destination_path): + return + + def _do_install(): + try: + if not os.path.exists(self.files): + os.makedirs(self.files) + + url = "https://raw.githubusercontent.com/Scriptz1/Learn.py-Installer/main/volleyball_net.bob" + urllib.request.urlretrieve(url, target_path) + shutil.copy(target_path, destination_path) + + bui.screenmessage("Installation worked !", color=(0, 1, 0.3)) + bui.getsound('ding').play() + + except Exception as e: + print(f"DEBUG ERROR: {e}") + bui.screenmessage("Installation Failed!", color=(1, 0, 0)) + bui.getsound('kronk2').play() + + bs.apptimer(2.5, _do_install) diff --git a/plugins/utilities.json b/plugins/utilities.json index 0c62bac..d9e3cd5 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -1054,6 +1054,18 @@ } ], "versions": { + "2.1.7": { + "api_version": 9, + "commit_sha": "c0527f3", + "released_on": "05-04-2026", + "md5sum": "442889926c17a0ffc70b924a808d8132" + }, + "2.1.6": { + "api_version": 9, + "commit_sha": "3aa666f", + "released_on": "05-04-2026", + "md5sum": "d2b66f98b5a24aa14eb6b1ed9de2d5dc" + }, "2.1.5": { "api_version": 9, "commit_sha": "a36e3c2", @@ -2375,6 +2387,12 @@ } ], "versions": { + "1.0.1": { + "api_version": 9, + "commit_sha": "023b733", + "released_on": "01-05-2026", + "md5sum": "045eb589b2969398f7fcb171216303d1" + }, "1.0.0": { "api_version": 9, "commit_sha": "f38a7ac", @@ -2384,7 +2402,7 @@ } }, "custom_hits": { - "description": "when someone hited rainbow text whit effect will appear", + "description": "when someone hited text whit random effect will appear", "external_url": "", "authors": [ { @@ -2394,6 +2412,12 @@ } ], "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "023b733", + "released_on": "01-05-2026", + "md5sum": "bc39a552ab7f40a71ce2813f7860a62f" + }, "1.0.1": { "api_version": 9, "commit_sha": "b48a4bd", @@ -2426,6 +2450,139 @@ "md5sum": "d8002dfc93be74d71f8e6e4aacfe6fd1" } } + }, + "better_camera_shake": { + "description": "This plugin makes the camera only shake when an explosion hits a player character, begone excessive camera shaking!", + "external_url": "", + "authors": [ + { + "name": "DinoWattz", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "e67d891", + "released_on": "23-02-2026", + "md5sum": "552b0ff0492bdab6cf69c1bc2d679e0a" + } + } + }, + "chat_bubbles": { + "description": "Adds whatever the players say above their character, includes a few other features too!", + "external_url": "", + "authors": [ + { + "name": "DinoWattz", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "e67d891", + "released_on": "23-02-2026", + "md5sum": "307175b18b80671a7984862bcf09f1ce" + } + } + }, + "enhanced_effects": { + "description": "Explosions affect character colors, slow-mo tnt, fair colored shields, a trippy background screen and maybe more in the future!", + "external_url": "", + "authors": [ + { + "name": "DinoWattz", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "e67d891", + "released_on": "23-02-2026", + "md5sum": "63c665905dd4619d3f99ca8977f8fbf3" + } + } + }, + "sleep_on_afk": { + "description": "Staying idle for 40 seconds will make your character fall asleep, they need rest too..", + "external_url": "", + "authors": [ + { + "name": "DinoWattz", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "e67d891", + "released_on": "23-02-2026", + "md5sum": "5de7a474c3c6b21e8ea1e1abff3c07a1" + } + } + }, + "rank_system": { + "description": "ranks system for servers or for local players", + "external_url": "", + "authors": [ + { + "name": "ATD", + "email": "anasdhaoidi001@gmail.com", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "023b733", + "released_on": "01-05-2026", + "md5sum": "487c0520dc833f15942941e0dbc82f1e" + } + } + }, + "glowing_profiles": { + "description": "This plugin gives your profile glowlight, just like on some servers, but only offline", + "external_url": "https://m.youtube.com/watch?v=Jb_dKz99rhY", + "authors": [ + { + "name": "andrejkuroglo8", + "email": "andrejkuroglo8@gmail.com", + "discord": "andrewku" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "6a494a4", + "released_on": "10-05-2026", + "md5sum": "6346cd7146baf41eef8926dced23fdfe" + } + } + }, + "fluffyplaylisteditor": { + "description": "A simple not-so advanced Playlist Editor", + "external_url": "https://discord.com/channels/1001896771347304639/1483463979450896445", + "authors": [ + { + "name": "FluffyPal", + "email": "", + "discord": "fluffypal" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "99cf837", + "released_on": "08-06-2026", + "md5sum": "3ead338c768fbdffe022aadcbdc8c1d8" + } + } } } } \ No newline at end of file diff --git a/plugins/utilities/advanced_party_window.py b/plugins/utilities/advanced_party_window.py index 6bdfd2e..aca17e7 100644 --- a/plugins/utilities/advanced_party_window.py +++ b/plugins/utilities/advanced_party_window.py @@ -19,41 +19,51 @@ https://bombsquad-community.web.app/mods ''' +import threading +from _thread import start_new_thread +import urllib.parse +import urllib.request +from typing import TYPE_CHECKING, cast +import _babase +import bascenev1 as bs +import bauiv1 as bui +import babase +import time +import math +from dataclasses import dataclass +from bauiv1lib.confirm import ConfirmWindow +from bauiv1lib.colorpicker import ColorPickerExact +from typing import List, Sequence, Optional, Dict, Any, Union +import bauiv1lib.party as bascenev1lib_party +import ssl +import datetime +import base64 +from babase._general import Call +from bauiv1lib.popup import PopupMenuWindow, PopupWindow +from bauiv1lib.account import viewer +import os +import urllib +import copy +import shutil +import sys +import re +import json +import codecs +import traceback +plugman = dict( + plugin_name="advanced_party_window", + description="Advanced your party window with lots of feature", + external_url="https://www.youtube.com/watch?v=QrES1jQGXF0", + authors=[ + {"name": "Mr.Smoothy", "email": "", "discord": "mr.smoothy"}, + ], + version="2.1.7", +) + # added advanced ID revealer # live ping # Made by Mr.Smoothy - Plasma Boson -import traceback -import codecs -import json -import re -import sys -import shutil -import copy -import urllib -import os -from bauiv1lib.account import viewer -from bauiv1lib.popup import PopupMenuWindow, PopupWindow -from babase._general import Call -import base64 -import datetime -import ssl -import bauiv1lib.party as bascenev1lib_party -from typing import List, Sequence, Optional, Dict, Any, Union -from bauiv1lib.colorpicker import ColorPickerExact -from bauiv1lib.confirm import ConfirmWindow -from dataclasses import dataclass -import math -import time -import babase -import bauiv1 as bui -import bascenev1 as bs -import _babase -from typing import TYPE_CHECKING, cast -import urllib.request -import urllib.parse -from _thread import start_new_thread -import threading version_str = "7" BCSSERVER = 'mods.ballistica.workers.dev' @@ -461,6 +471,7 @@ class ModifiedPartyWindow(bascenev1lib_party.PartyWindow): color=(0.55, 0.73, 0.25), icon=bui.gettexture('menuButton'), iconscale=1.2) + self._menu_popup: PopupMenuWindow | None = None info = bs.get_connection_to_host_info_2() if info != None: @@ -864,7 +875,7 @@ class ModifiedPartyWindow(bascenev1lib_party.PartyWindow): choices.append("hostInfo_Debug") DisChoices.append(_getTransText("Debug_for_Host_Info", isBaLstr=True)) - PopupMenuWindow( + self._menu_popup = PopupMenuWindow( position=self._menu_button.get_screen_space_center(), scale=_get_popup_window_scale(), choices=choices, @@ -891,12 +902,12 @@ class ModifiedPartyWindow(bascenev1lib_party.PartyWindow): except: babase.print_exception() - PopupMenuWindow(position=widget.get_screen_space_center(), - scale=_get_popup_window_scale(), - choices=choices, - choices_display=choices_display, - current_choice="@ this guy", - delegate=self) + self._menu_popup = PopupMenuWindow(position=widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=choices, + choices_display=choices_display, + current_choice="@ this guy", + delegate=self) self._popup_party_member_client_id = client_id self._popup_party_member_is_host = is_host self._popup_type = "partyMemberPress" diff --git a/plugins/utilities/better_camera_shake.py b/plugins/utilities/better_camera_shake.py new file mode 100644 index 0000000..d176f46 --- /dev/null +++ b/plugins/utilities/better_camera_shake.py @@ -0,0 +1,388 @@ +# ba_meta require api 9 +from __future__ import annotations + +import babase +import bascenev1 as bs +import bascenev1lib.actor.bomb as bomb + +import random +from typing import TYPE_CHECKING + +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.bomb import BombFactory, ExplodeHitMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz + +if TYPE_CHECKING: + from typing import Any, Sequence + +plugman = dict( + plugin_name="better_camera_shake", + description="This plugin makes the camera only shake when an explosion hits a player character, begone excessive camera shaking!", + external_url="", + authors=[ + {"name": "DinoWattz", "email": "", "discord": ""} + ], + version="1.0.0", +) + +# Camera shake only on player impact + + +def new__init__( + self, + *, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + blast_radius: float = 2.0, + blast_type: str = 'normal', + source_player: bs.Player | None = None, + hit_type: str = 'explosion', + hit_subtype: str = 'normal', +): + """Instantiate with given values.""" + + # bah; get off my lawn! + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + + super(type(self), self).__init__() + + shared = SharedObjects.get() + factory = BombFactory.get() + + self.blast_type = blast_type + self._source_player = source_player + self.hit_type = hit_type + self.hit_subtype = hit_subtype + self.radius = blast_radius + + # Set our position a bit lower so we throw more things upward. + rmats = (factory.blast_material, shared.attack_material) + self.node = bs.newnode( + 'region', + delegate=self, + attrs={ + 'position': (position[0], position[1] - 0.1, position[2]), + 'scale': (self.radius, self.radius, self.radius), + 'type': 'sphere', + 'materials': rmats, + }, + ) + + bs.timer(0.05, self.node.delete) + + # Throw in an explosion and flash. + evel = (velocity[0], max(-1.0, velocity[1]), velocity[2]) + explosion = bs.newnode( + 'explosion', + attrs={ + 'position': position, + 'velocity': evel, + 'radius': self.radius, + 'big': (self.blast_type == 'tnt'), + }, + ) + if self.blast_type == 'ice': + explosion.color = (0, 0.05, 0.4) + + bs.timer(1.0, explosion.delete) + + if self.blast_type != 'ice': + bs.emitfx( + position=position, + velocity=velocity, + count=int(1.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='thin_smoke', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=int(4.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='ice' if self.blast_type == 'ice' else 'smoke', + ) + bs.emitfx( + position=position, + emit_type='distortion', + spread=1.0 if self.blast_type == 'tnt' else 2.0, + ) + + # And emit some shrapnel. + if self.blast_type == 'ice': + + def emit() -> None: + bs.emitfx( + position=position, + velocity=velocity, + count=30, + spread=2.0, + scale=0.4, + chunk_type='ice', + emit_type='stickers', + ) + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + elif self.blast_type == 'sticky': + + def emit() -> None: + bs.emitfx( + position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + spread=0.7, + chunk_type='slime', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.5, + spread=0.7, + chunk_type='slime', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=15, + scale=0.6, + chunk_type='slime', + emit_type='stickers', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=20, + scale=0.7, + chunk_type='spark', + emit_type='stickers', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=int(6.0 + random.random() * 12), + scale=0.8, + spread=1.5, + chunk_type='spark', + ) + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + elif self.blast_type == 'impact': + + def emit() -> None: + bs.emitfx( + position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.8, + chunk_type='metal', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.4, + chunk_type='metal', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=20, + scale=0.7, + chunk_type='spark', + emit_type='stickers', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=int(8.0 + random.random() * 15), + scale=0.8, + spread=1.5, + chunk_type='spark', + ) + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + else: # Regular or land mine bomb shrapnel. + + def emit() -> None: + if self.blast_type != 'tnt': + bs.emitfx( + position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + chunk_type='rock', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.5, + chunk_type='rock', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=30, + scale=1.0 if self.blast_type == 'tnt' else 0.7, + chunk_type='spark', + emit_type='stickers', + ) + bs.emitfx( + position=position, + velocity=velocity, + count=int(18.0 + random.random() * 20), + scale=1.0 if self.blast_type == 'tnt' else 0.8, + spread=1.5, + chunk_type='spark', + ) + + # TNT throws splintery chunks. + if self.blast_type == 'tnt': + + def emit_splinters() -> None: + bs.emitfx( + position=position, + velocity=velocity, + count=int(20.0 + random.random() * 25), + scale=0.8, + spread=1.0, + chunk_type='splinter', + ) + + bs.timer(0.01, emit_splinters) + + # Every now and then do a sparky one. + if self.blast_type == 'tnt' or random.random() < 0.1: + + def emit_extra_sparks() -> None: + bs.emitfx( + position=position, + velocity=velocity, + count=int(10.0 + random.random() * 20), + scale=0.8, + spread=1.5, + chunk_type='spark', + ) + + bs.timer(0.02, emit_extra_sparks) + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + lcolor = (0.6, 0.6, 1.0) if self.blast_type == 'ice' else (1, 0.3, 0.1) + light = bs.newnode( + 'light', + attrs={ + 'position': position, + 'volume_intensity_scale': 10.0, + 'color': lcolor, + }, + ) + + scl = random.uniform(0.6, 0.9) + scorch_radius = light_radius = self.radius + if self.blast_type == 'tnt': + light_radius *= 1.4 + scorch_radius *= 1.15 + scl *= 3.0 + + iscale = 1.6 + bs.animate( + light, + 'intensity', + { + 0: 2.0 * iscale, + scl * 0.02: 0.1 * iscale, + scl * 0.025: 0.2 * iscale, + scl * 0.05: 17.0 * iscale, + scl * 0.06: 5.0 * iscale, + scl * 0.08: 4.0 * iscale, + scl * 0.2: 0.6 * iscale, + scl * 2.0: 0.00 * iscale, + scl * 3.0: 0.0, + }, + ) + bs.animate( + light, + 'radius', + { + 0: light_radius * 0.2, + scl * 0.05: light_radius * 0.55, + scl * 0.1: light_radius * 0.3, + scl * 0.3: light_radius * 0.15, + scl * 1.0: light_radius * 0.05, + }, + ) + bs.timer(scl * 3.0, light.delete) + + # Make a scorch that fades over time. + scorch = bs.newnode( + 'scorch', + attrs={ + 'position': position, + 'size': scorch_radius * 0.5, + 'big': (self.blast_type == 'tnt'), + }, + ) + if self.blast_type == 'ice': + scorch.color = (1, 1, 1.5) + + bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) + bs.timer(13.0, scorch.delete) + + if self.blast_type == 'ice': + factory.hiss_sound.play(position=light.position) + + lpos = light.position + factory.random_explode_sound().play(position=lpos) + factory.debris_fall_sound.play(position=lpos) + + # ↓ We don't need any of this, we're fully trained professionals. + # bs.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) + + # TNT is more epic. + if self.blast_type == 'tnt': + factory.random_explode_sound().play(position=lpos) + + def _extra_boom() -> None: + factory.random_explode_sound().play(position=lpos) + + bs.timer(0.25, _extra_boom) + + def _extra_debris_sound() -> None: + factory.debris_fall_sound.play(position=lpos) + factory.wood_debris_fall_sound.play(position=lpos) + + bs.timer(0.4, _extra_debris_sound) + + +bomb.Blast.__init__ = new__init__ + +org_handlemessage = bomb.Blast.handlemessage + + +def new_handlemessage(self, msg: Any, *args, **kwargs) -> Any: + org_handlemessage(self, msg, *args, **kwargs) + + if isinstance(msg, ExplodeHitMessage): + node = bs.getcollision().opposingnode + delegate = node.getdelegate(PlayerSpaz) + has_used_camerashake = getattr(self, 'has_used_camerashake', False) + if not has_used_camerashake and (delegate and not delegate.shield) and not node.invincible: + self.has_used_camerashake = True + bs.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) + + +bomb.Blast.handlemessage = new_handlemessage + +# ba_meta export babase.Plugin + + +class Plugin(babase.Plugin): + pass diff --git a/plugins/utilities/chat_bubbles.py b/plugins/utilities/chat_bubbles.py new file mode 100644 index 0000000..af7eb31 --- /dev/null +++ b/plugins/utilities/chat_bubbles.py @@ -0,0 +1,599 @@ +# ba_meta require api 9 +# This plugin only works with BombSquad 1.7.49+ +from __future__ import annotations + +from typing import TYPE_CHECKING, override + +import babase +import bascenev1 as bs +import random +import string +import unicodedata + +if TYPE_CHECKING: + from typing import Any + +plugman = dict( + plugin_name="chat_bubbles", + description="Adds whatever the players say above their character, includes a few other features too!", + external_url="", + authors=[ + {"name": "DinoWattz", "email": "", "discord": ""} + ], + version="1.0.0", +) + +# Prints chat messages to the console as "[Character's Name] Player 1/Player 2: Hello World!" +PRINT_CHAT_MESSAGES = True +BUBBLE_DURATION: float = 4.7 # 4.7 matches the default onscreen message duration +SPEAK_VOLUME = 1.5 +SHOUT_VOLUME = 3.0 + +CELEBRATE_MESSAGE = [(bs.CelebrateMessage, {"duration": 1.0})] + +# Easter egg triggers, sounds, and messages flag +# If adding new ones, make sure to test them in-game +EASTER_EGGS = [ + { + "triggers": { + "hi", "hello", "hey", "hiya", "yo", "sup", "heya", "howdy", + "greetings", "salutations", "ahoy", "ahoyhoy", "bonjour", + "hola", "ciao", "ola", "oi", "eae", "salve", + "bye", "goodbye", "see ya", "see you", "cya", + "adios", "chau", "chao", "tchau", + "adeus", "ate mais", "falou", "flw" + }, + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"aaa"}, + "sound": "spazFall01", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"aaaa"}, + "sound": "zoeFall01", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"aaaaa"}, + "sound": "kronkFall", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"potato"}, + "sound": "kronk2", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"dau", "tau", "d'oh", "doh"}, + "sound": "kronk3", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"drau", "trau", "d'roh", "droh"}, + "sound": "bunny2", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"hahaha", "hahahaha", + "hahahah", "hahahahah", "lol", + "nahahah", "kakaka", "kakakaka", "kkk", "kkkk", + "jajaja", "jajajaja", "jajajajaa"}, + "sound": "mel05", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"ha", "hah", "ja", "ka"}, + "sound": "mel06", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"merry christmas", "merry krismas", "merry xmas", "feliz natal", "feliz navidad"}, + "sound": "santa02", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"hohoho", "ho ho ho", "ho-ho-ho"}, + "sound": "santa05", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"gg", "good game", "que pro", "q pro"}, + "sound": "achievement", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"boo", "bad game", "que noob", "q noob"}, + "sound": "boo", + "messages": CELEBRATE_MESSAGE, + }, + # ↓ Node messages are like this ↓ + { + "triggers": {"0", "zero"}, + "sound": "boxingBell", + "node_messages": [("celebrate_l", 1000)], + }, + { + "triggers": {"1", "one"}, + "sound": "announceOne", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"2", "two"}, + "sound": "announceTwo", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"3", "three"}, + "sound": "announceThree", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"4", "four"}, + "sound": "announceFour", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"5", "five"}, + "sound": "announceFive", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"6", "six"}, + "sound": "announceSix", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"7", "seven"}, + "sound": "announceSeven", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"8", "eight"}, + "sound": "announceEight", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"9", "nine"}, + "sound": "announceNine", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"10", "ten"}, + "sound": "announceTen", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"xd"}, + "sound": "santaDeath", + "node_messages": [("knockout", 100)], + }, + # ↓ Easter eggs without actor/node messages ↓ + { + "triggers": {"huh", "huhh", "huhhh", "huhhhh", "humph", "hmph", "hum", "humm"}, + "sound": "agent1", + }, + { + "triggers": {"hoh", "hohh", "hohhh", "hohhhh", "ohh"}, + "sound": "agent3", + }, + { + "triggers": {"eff"}, + "sound": "zoeEff", + }, + { + "triggers": {"ow", "oww", "oow"}, + "sound": "zoeOw", + }, + { + "triggers": {"pipipi", "bibibi"}, + "sound": "mel07", + }, + { + "triggers": {"oh yeah", "oh yea", "oohh yeah", "oohh yea"}, + "sound": "yeah", + }, + { + "triggers": {"woo"}, + "sound": "woo2", + }, + { + "triggers": {"wooo"}, + "sound": "woo", + }, + { + "triggers": {"woo yeah", "woo yea"}, + "sound": "woo3", + }, + { + "triggers": {"ooh"}, + "sound": "ooh", + }, + { + "triggers": {"wow"}, + "sound": "wow", + }, + { + "triggers": {"gasp"}, + "sound": "gasp", + }, + { + "triggers": {"ahh", "aah"}, + "sound": "aww", + }, + { + "triggers": {"nice"}, + "sound": "nice", + }, + # ↑ Default easter egg list ends here ↑ +] + +# Chat bubble functionality + + +def create_bubble(player: bs.SessionPlayer, text: str, name: str): + msg = normalize_message(text) + is_caps = len(text) > 1 and text.isupper() + handled = False + bubble_created = False + + activityplayer: bs.Player | None = getattr(player, 'activityplayer', None) + + def _fallback_logic(): + nonlocal handled, activityplayer + color = (1, 1, 0.4) + image_icon = {'texture': bs.gettexture("cuteSpaz"), 'tint_texture': bs.gettexture( + "cuteSpaz"), 'tint_color': (1.0, 1.0, 1.0), 'tint2_color': (1.0, 1.0, 1.0)} + + if activityplayer: + normal_highlight = bs.normalized_color( + bs.safecolor(player.highlight, target_intensity=1.15)) + saturated_highlight = bs.normalized_color( + bs.safecolor(player.highlight, target_intensity=0.75)) + color = saturated_highlight if is_caps else normal_highlight + image_icon = activityplayer.get_icon() + + bs.broadcastmessage(f"{name}: {text}", + color=color, + top=True, + image=image_icon) + + for egg in EASTER_EGGS: + if msg in egg["triggers"]: + if egg.get("sound"): + sound = bs.getsound(egg["sound"]) + sound.play(SHOUT_VOLUME if is_caps else SPEAK_VOLUME) + handled = True + break + if not handled and activityplayer: + assert bs.app.classic is not None + handled = True + character = activityplayer.character + char = bs.app.classic.spaz_appearances[character] + if is_caps: + attack_sounds = [bs.getsound(s) for s in char.attack_sounds] + if attack_sounds and isinstance(attack_sounds, (list, tuple)) and len(attack_sounds) > 0: + sound = random.choice(attack_sounds) + sound.play(SHOUT_VOLUME) + else: + pickup_sounds = [bs.getsound(s) for s in char.pickup_sounds] + if pickup_sounds and isinstance(pickup_sounds, (list, tuple)) and len(pickup_sounds) > 0: + sound = random.choice(pickup_sounds) + sound.play(SPEAK_VOLUME) + + if activityplayer is None: + # No valid activityplayer: play sound if possible (lobby/non-in-game) + _fallback_logic() + return + + bubbles = activityplayer.customdata.setdefault('chat_bubbles', []) + actor_list = [] + + # Support for both actor and ghost_actor (from the Ghost Players plugin) + if getattr(activityplayer, 'actor', None): + actor_list.append(activityplayer.actor) + if hasattr(activityplayer, 'customdata') and activityplayer.customdata.get('ghost_actor'): + actor_list.append(activityplayer.customdata['ghost_actor']) + + if actor_list: + for actor in actor_list: + if actor and actor.is_alive() and actor.node: + assert actor is not None + char_node = actor.node + activity = actor.getactivity() + if activity is None or getattr(activity, 'expired', False): + continue + + with activity.context: + # Calculate scale based on text length + base_scale = 0.015 + min_scale = 0.009 + scale = max(base_scale - 0.0003 * max(len(text) - 20, 0), min_scale) + y_offset = 1.2 + 0.25 * len(bubbles) + + mnode = bs.newnode('math', owner=char_node, attrs={ + 'input1': (0, y_offset, 0), + 'operation': 'add' + }) + char_node.connectattr('torso_position', mnode, 'input2') + + normal_highlight = bs.normalized_color( + bs.safecolor(player.highlight, target_intensity=1.15)) + saturated_highlight = bs.normalized_color( + bs.safecolor(player.highlight, target_intensity=0.75)) + bubble_color = saturated_highlight if is_caps else normal_highlight + + textnode = bs.newnode('text', + owner=char_node, + attrs={ + 'text': text, + 'color': bubble_color, + 'shadow': 0.5, + 'flatness': 0.5, + 'scale': scale, + 'h_align': 'center', + 'v_align': 'bottom', + 'in_world': True, + 'opacity': 1.0, + }) + + mnode.connectattr('output', textnode, 'position') + + focus = Plugin.Focus(owner=textnode).autoretain() + textnode.connectattr('position', focus.node, 'position') + + # Adjust bubble duration for slow-motion activities + bubble_duration = BUBBLE_DURATION + if hasattr(activity, 'slow_motion') and activity.slow_motion: + bubble_duration = max(0.5, bubble_duration / 3) + + # Play sound for all triggers (easter eggs and normal) + def _play_chat_sound(actor, sound, volume): + if actor.is_alive(): + actor._safe_play_sound(sound, volume) + else: + sound.play(volume) + + # Easter egg sound/message + for egg in EASTER_EGGS: + if msg in egg['triggers']: + # Sound selection logic + sound = None + if egg.get('sound'): + sound = bs.getsound(egg['sound']) + else: + # Default to character sounds if no specific sound is set + if is_caps: + attack_sounds = getattr(char_node, 'attack_sounds', None) + if attack_sounds and isinstance(attack_sounds, (list, tuple)) and len(attack_sounds) > 0: + sound = random.choice(attack_sounds) + else: + pickup_sounds = getattr(char_node, 'pickup_sounds', None) + if pickup_sounds and isinstance(pickup_sounds, (list, tuple)) and len(pickup_sounds) > 0: + sound = random.choice(pickup_sounds) + + if sound: + _play_chat_sound( + actor, sound, SHOUT_VOLUME if is_caps else SPEAK_VOLUME) + + # Animation logic + if egg.get('messages') and actor.is_alive() and hasattr(actor, 'handlemessage'): + for msg_class, msg_kwargs in egg.get('messages', []): + msg_kwargs = msg_kwargs or {} + actor.handlemessage(msg_class(**msg_kwargs)) + + if egg.get('node_messages') and actor.is_alive() and actor.node.exists(): + for node_args in egg.get('node_messages', []): + actor.node.handlemessage(*node_args) + + handled = True + break + + if not handled: + if is_caps: + attack_sounds = getattr(char_node, 'attack_sounds', None) + if attack_sounds and isinstance(attack_sounds, (list, tuple)) and len(attack_sounds) > 0: + sound = random.choice(attack_sounds) + _play_chat_sound(actor, sound, SHOUT_VOLUME) + if actor.is_alive() and hasattr(actor, 'handlemessage'): + actor.handlemessage(bs.CelebrateMessage(duration=1.0)) + else: + pickup_sounds = getattr(char_node, 'pickup_sounds', None) + if pickup_sounds and isinstance(pickup_sounds, (list, tuple)) and len(pickup_sounds) > 0: + sound = random.choice(pickup_sounds) + _play_chat_sound(actor, sound, SPEAK_VOLUME) + + bubbles.append((textnode, mnode)) + bubble_created = True + while len(bubbles) > 3: + old_textnode, old_mnode = bubbles.pop(0) + if old_textnode is not None and hasattr(old_textnode, 'exists') and old_textnode.exists(): + old_textnode.delete() + if old_mnode is not None and hasattr(old_mnode, 'exists') and old_mnode.exists(): + old_mnode.delete() + update_bubble_offsets(bubbles) + + fade_time = max(0.1, bubble_duration - 0.5) + bs.animate(textnode, 'opacity', {fade_time: 1.0, bubble_duration: 0.0}) + bs.timer(bubble_duration, textnode.delete) + bs.timer(bubble_duration, mnode.delete) + + def cleanup(): + if hasattr(activityplayer, 'customdata'): + if 'chat_bubbles' in activityplayer.customdata: + activityplayer.customdata['chat_bubbles'] = [ + b for b in activityplayer.customdata['chat_bubbles'] if b[0] != textnode + ] + bs.timer(bubble_duration, cleanup) + + if not bubble_created: + # No valid actor found: play sound if possible + _fallback_logic() + return + + +def update_bubble_offsets(bubbles): + for i, (tnode, mnode) in enumerate(reversed(bubbles)): + y_offset = 1.2 + 0.25 * i + if mnode is not None and hasattr(mnode, 'exists') and mnode.exists(): + mnode.input1 = (0, y_offset, 0) + +# Tools + + +def normalize_message(text): + msg = text.strip().lower() + msg = msg.translate(str.maketrans('', '', string.punctuation)) + msg = ''.join( + c for c in unicodedata.normalize('NFD', msg) + if unicodedata.category(c) != 'Mn' + ) + + return msg + +# ba_meta export babase.Plugin + + +class Plugin(babase.Plugin): + def on_app_running(self) -> None: + # Hook into chat message filtering + # We're delaying until the app runs and using a timer so it's more likely to work with other chat plugins (please don't make a chat plugin/filter that is delayed like this). + bs.apptimer(0.001, bs.WeakCallStrict(self.apply_patch)) + + def apply_patch(self): + import bascenev1._hooks as _hooks + + _org_filter_chat_message = _hooks.filter_chat_message + + def _chat_bubbles_hook(msg: str, client_id: int, *args, **kwargs) -> str | None: + org_msg = _org_filter_chat_message(msg, client_id, *args, **kwargs) + + if org_msg is None: + # The original message has been ignored, so we should ignore it as well + return org_msg + + # Chat bubble sorcery + try: + roster = bs.get_game_roster() + session = bs.get_foreground_host_session() + if session is None: + # Probably couldn't find a session because we are a client + return org_msg + sessionplayers = session.sessionplayers + + player_list = [] + matched = False + dead_bubble = False + character: str | None = None + + with session.context: + for player in sessionplayers: + if player.inputdevice.client_id == client_id and player.in_game: + player_list.append(player) + continue + if player_list: + # Get combined name for multiple players on same client + name = '' + created_bubble = False + + for player in player_list: + if name == '': + name = player.getname() + else: + name = name + '/' + player.getname() + + # Create chat bubble + for player in player_list: + activityplayer: bs.Player = player.activityplayer + # We have a session player but not an activity player + if not activityplayer: + if not dead_bubble: + create_bubble(player, org_msg, name) + created_bubble = dead_bubble = True + continue + # We have a dead activity player + if not activityplayer.is_alive() and not dead_bubble: + create_bubble(player, org_msg, name) + created_bubble = dead_bubble = True + character = activityplayer.character if character is None else character + # elif we have an alive activity player + elif activityplayer.is_alive(): + create_bubble(player, org_msg, name) + created_bubble = True + character = activityplayer.character if character is None else character + + if created_bubble: + matched = True + if PRINT_CHAT_MESSAGES: + print(f"[{character}] {name}: {org_msg}") + else: + bs.logging.warning( + "Failed to create a chat bubble, using non-session player method now.") + # Non-session players (in lobby or just spectating, this only works for servers) + if not matched and not dead_bubble: + for client in roster: + if client['client_id'] == client_id: + name = client.get('display_string') + create_bubble(None, org_msg, name) + + matched = True + if PRINT_CHAT_MESSAGES: + print(f"[{character}] {name}: {org_msg}") + break + except Exception as e: + bs.logging.exception(f"Error in chat bubble hook: {e}") + + return org_msg + + _hooks.filter_chat_message = _chat_bubbles_hook + bs.reload_hooks() + + print("[ChatBubbles] Plugin initialized.") + + class Focus(bs.Actor): + def __init__( + self, + *, + owner: bs.Node | None = None, + ): + super().__init__() + + self._focusnode_material = bs.Material() + self._focusnode_material.add_actions( + actions=('modify_node_collision', 'collide', False), + ) + + if getattr(owner, 'owner', None): + assert owner is not None + area_of_interest = False if getattr( + owner.owner, 'is_area_of_interest', False) else True + else: + area_of_interest = False if getattr(owner, 'is_area_of_interest', False) else True + + # I'm tired of this area_of_interest nonsense, it never works right + # area_of_interest = False + # actually it might work now that we don't spawn bubbles for dead players. + + self.node = bs.newnode( + 'prop', + delegate=self, + owner=owner, + attrs={ + 'body': 'sphere', + 'body_scale': 0.0, + 'shadow_size': 0.0, + 'gravity_scale': 0.0, + 'is_area_of_interest': area_of_interest, + 'materials': (self._focusnode_material,), + }, + ) + + @override + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + else: + super().handlemessage(msg) diff --git a/plugins/utilities/custom_hits.py b/plugins/utilities/custom_hits.py index afa7730..e6d3e78 100644 --- a/plugins/utilities/custom_hits.py +++ b/plugins/utilities/custom_hits.py @@ -1,7 +1,6 @@ # ba_meta require api 9 from __future__ import annotations - from typing import TYPE_CHECKING import babase @@ -14,136 +13,81 @@ if TYPE_CHECKING: plugman = dict( plugin_name="custom_hits", - description="when someone hited rainbow text whit effect will appear", + description="when someone hited text whit random effect will appear", external_url="", authors=[ {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, ], - version="1.0.1", + version="1.1.0", ) +# ___________________________________________________ -#################################### - -first_damage = (u'\ue042TRY AGAIN?\ue042', (0, 1, 0), '6') -second_damage = (u'\ue048GOOD!\ue048', (0, 1, 1), '2') -third_damage = (u'\ue041NICE!\ue041', (0.8, 0.4, 1), '3') -fourth_damage = (u'\ue049UPPS\ue049', (1, 1, 0), '4') -five_damage = (u'\ue043DEATH!\ue043', (1, 0, 0), '5') +first_damage = (u'NAH NOOB?', (1, 1, 1), '6') +second_damage = (u'GOOD!', (0, 1, 0), '2') +third_damage = (u'DAMN!', (0, 0, 0), '3') +fourth_damage = (u'SRY NOOB', (1, 1, 0), '4') +five_damage = (u'FAH!!!', (1, 0, 0), '5') def custom_effects(pos: float, effect: str = None) -> None: if effect == '1': - bs.emitfx( - position=pos, - count=3, - scale=0.1, - spread=0.1, - chunk_type='rock') + bs.emitfx(position=pos, count=3, scale=0.1, spread=0.1, chunk_type='rock') elif effect == '2': - bs.emitfx( - position=pos, - count=70, - scale=2, - spread=0.8, - chunk_type='spark') + bs.emitfx(position=pos, count=70, scale=2, spread=0.8, chunk_type='spark') elif effect == '3': - bs.emitfx( - position=pos, - count=6, - scale=0.3, - spread=0.4, - chunk_type='splinter') + bs.emitfx(position=pos, count=6, scale=0.3, spread=0.4, chunk_type='splinter') elif effect == '4': - bs.emitfx( - position=pos, - count=50, - scale=3.0, - spread=0.9, - chunk_type='ice') + bs.emitfx(position=pos, count=50, scale=3.0, spread=0.9, chunk_type='ice') elif effect == '5': - bs.emitfx( - position=pos, - count=80, - scale=3.0, - spread=1.5, - chunk_type='metal') + bs.emitfx(position=pos, count=80, scale=3.0, spread=1.5, chunk_type='metal') elif effect == '6': - bs.emitfx( - position=pos, - count=30, - scale=1.2, - spread=0.8, - chunk_type='slime') + bs.emitfx(position=pos, count=30, scale=1.2, spread=0.8, chunk_type='slime') + + +def on_punched(self, damage: int) -> None: + pos = self.node.position + + def custom_text(msg: str, color: float) -> None: + text = bs.newnode('text', attrs={ + 'text': msg, + 'color': color, + 'in_world': True, + 'h_align': 'center', + 'shadow': 0.5, + 'flatness': 1.0, + }) + bs.animate_array(text, 'position', 3, { + 0.0: (pos[0], pos[1] + 1.2, pos[2]), + 2.0: (pos[0], pos[1] + 1.7, pos[2]), + }) + bs.animate(text, 'opacity', {0.8: 1.0, 2.0: 0.0}) + bs.animate(text, 'scale', {0: 0, 0.1: 0.017, 0.15: 0.014, 2.0: 0.016}) + bs.timer(2.0, text.delete) + + if damage < 200: + custom_text(first_damage[0], first_damage[1]) + custom_effects(pos, first_damage[2]) + elif damage < 500: + custom_text(second_damage[0], second_damage[1]) + custom_effects(pos, second_damage[2]) + elif damage < 800: + custom_text(third_damage[0], third_damage[1]) + custom_effects(pos, third_damage[2]) + elif damage < 1000: + custom_text(fourth_damage[0], fourth_damage[1]) + custom_effects(pos, fourth_damage[2]) + else: + custom_text(five_damage[0], five_damage[1]) + custom_effects(pos, five_damage[2]) -#################################### # ba_meta export babase.Plugin - - -class CustomHitsPlugin(babase.Plugin): - - def on_punched(self, damage: int) -> None: - pos = self.node.position - - def custom_text(msg: str, color: float) -> None: - text = bs.newnode( - 'text', - attrs={ - 'text': msg, - 'color': color, - 'in_world': True, - 'h_align': 'center', - 'shadow': 0.5, - 'flatness': 1.0}) - bs.animate_array(text, 'position', 3, { - 0.0: (pos[0], pos[1] + 1.2, pos[2]), - 2.0: (pos[0], pos[1] + 1.7, pos[2]) - }) - bs.animate(text, 'opacity', { - 0.8: 1.0, - 2.0: 0.0 - }) - bs.animate(text, 'scale', { - 0: 0, - 0.1: 0.017, - 0.15: 0.014, - 2.0: 0.016 - }) - bs.animate_array(text, 'color', 3, { - 0.0: (0, 0, 0), # Black - 0.2: (2.55, 2.55, 2.55), # White - 0.4: (2, 0, 0), # Red - 0.6: (0, 2.55, 0), # Lime - 0.8: (0, 0, 2.55), # Blue - 1.0: (2.55, 2.55, 0), # Yellow - 1.2: (0, 2.55, 2.55), # Cyan / Aqua - 1.4: (2.55, 0, 2.55), # Magenta / Fuchsia - 1.6: (1.92, 1.92, 1.92), # Silver - 1.8: (1.28, 1.28, 1.28), # Gray - 2.0: (1.28, 0, 0), # Maroon - 2.2: (1.28, 1.28, 0), # Olive - 2.4: (0, 1.28, 0), # Green - 2.6: (1.28, 0, 1.28), # Purple - 2.8: (0, 1.28, 1.28), # Teal - 3.0: (0, 0, 1.28), # Navy - 3.2: (1.5, 0.5, 0), # Orange - 3.4: (1.8, 0.5, 1.6), # Pink - 3.6: (0.5, 0.5, 0.5), # Gray - }, loop=True) - bs.timer(2.0, text.delete) - if damage < 200: - custom_text(first_damage[0], first_damage[1]) - custom_effects(pos, first_damage[2]) - elif damage < 500: - custom_text(second_damage[0], second_damage[1]) - custom_effects(pos, second_damage[2]) - elif damage < 800: - custom_text(third_damage[0], third_damage[1]) - custom_effects(pos, third_damage[2]) - elif damage < 1000: - custom_text(fourth_damage[0], fourth_damage[1]) - custom_effects(pos, fourth_damage[2]) - else: - custom_text(five_damage[0], five_damage[1]) - custom_effects(pos, five_damage[2]) +class byATD(babase.Plugin): + """تفعيل ATD Hits — يُستدعى من bootstraping().""" Spaz.on_punched = on_punched + try: + from bascenev1lib.actor.spazbot import SpazBot + SpazBot.on_punched = on_punched + except Exception: + pass + bs.broadcastmessage(u' || WELCOME || ', color=(0, 1, 0)) diff --git a/plugins/utilities/enhanced_effects.py b/plugins/utilities/enhanced_effects.py new file mode 100644 index 0000000..d78f561 --- /dev/null +++ b/plugins/utilities/enhanced_effects.py @@ -0,0 +1,305 @@ +# ba_meta require api 9 +from __future__ import annotations + +from typing import TYPE_CHECKING + +import random +import weakref +import babase +import bascenev1 as bs + +from bascenev1lib.actor.background import Background + +if TYPE_CHECKING: + from typing import Any + +plugman = dict( + plugin_name="enhanced_effects", + description="Explosions affect character colors, slow-mo tnt, fair colored shields, a trippy background screen and maybe more in the future!", + external_url="", + authors=[ + {"name": "DinoWattz", "email": "", "discord": ""} + ], + version="1.0.0", +) + +# Transparent Background (kinda hacky, works best on pc with high/higher quality 'visuals' setting) +BACKGROUND_OPACITY = 0.5 + + +def __modified_background_init( + self, + fade_time: float = 0.5, + start_faded: bool = False, + show_logo: bool = False, +): + super(type(self), self).__init__() + self._dying = False + self.fade_time = fade_time + # We're special in that we create our node in the session + # scene instead of the activity scene. + # This way we can overlap multiple activities for fades + # and whatnot. + session = bs.getsession() + self._session = weakref.ref(session) + with session.context: + self.node = bs.newnode( + 'image', + delegate=self, + attrs={ + 'fill_screen': True, + 'texture': bs.gettexture('bg'), + 'tilt_translate': -0.3, + 'has_alpha_channel': False, + 'color': (1, 1, 1), + 'opacity': BACKGROUND_OPACITY, + }, + ) + if not start_faded: + bs.animate( + self.node, + 'opacity', + {0.0: 0.0, self.fade_time: BACKGROUND_OPACITY}, + loop=False, + ) + if show_logo: + logo_texture = bs.gettexture('logo') + logo_mesh = bs.getmesh('logo') + logo_mesh_transparent = bs.getmesh('logoTransparent') + self.logo = bs.newnode( + 'image', + owner=self.node, + attrs={ + 'texture': logo_texture, + 'mesh_opaque': logo_mesh, + 'mesh_transparent': logo_mesh_transparent, + 'scale': (0.7, 0.7), + 'vr_depth': -250, + 'color': (0.15, 0.15, 0.15), + 'position': (0, 0), + 'tilt_translate': -0.05, + 'absolute_scale': False, + }, + ) + self.node.connectattr('opacity', self.logo, 'opacity') + # add jitter/pulse for a stop-motion-y look unless we're in VR + # in which case stillness is better + if not bs.app.env.vr: + self.cmb = bs.newnode( + 'combine', owner=self.node, attrs={'size': 2} + ) + for attr in ['input0', 'input1']: + bs.animate( + self.cmb, + attr, + {0.0: 0.693, 0.05: 0.7, 0.5: 0.693}, + loop=True, + ) + self.cmb.connectattr('output', self.logo, 'scale') + cmb = bs.newnode( + 'combine', owner=self.node, attrs={'size': 2} + ) + cmb.connectattr('output', self.logo, 'position') + # Gen some random keys for that stop-motion-y look. + keys = {} + timeval = 0.0 + for _i in range(10): + keys[timeval] = (random.random() - 0.5) * 0.0015 + timeval += random.random() * 0.1 + bs.animate(cmb, 'input0', keys, loop=True) + keys = {} + timeval = 0.0 + for _i in range(10): + keys[timeval] = (random.random() - 0.5) * 0.0015 + 0.05 + timeval += random.random() * 0.1 + bs.animate(cmb, 'input1', keys, loop=True) + + +def _modified_background_die(self, immediate: bool = False) -> None: + session = self._session() + if session is None and self.node: + # If session is gone, our node should be too, + # since it was part of the session's scene. + # Let's make sure that's the case. + # (since otherwise we have no way to kill it) + bs.logging.exception( + 'got None session on Background _die' + ' (and node still exists!)' + ) + elif session is not None: + with session.context: + if not self._dying and self.node: + self._dying = True + if immediate: + self.node.delete() + else: + bs.animate( + self.node, + 'opacity', + {0.0: self.node.opacity, self.fade_time: 0.0}, + loop=False, + ) + bs.timer(self.fade_time + 0.1, self.node.delete) + + +Background.__init__ = __modified_background_init +Background._die = _modified_background_die + +# Tools + + +def blend_toward( + rgb: tuple[float, float, float], + target: tuple[float, float, float], + amount: float = 1.0, + perceptual: bool = False +) -> tuple[float, ...]: + """ + Blend 'rgb' toward 'target' by 'amount' (0..1). + + If perceptual=True, blend in linear-light space for smoother, more + natural-looking fades. + """ + def clamp01(x: float) -> float: + return max(0.0, min(1.0, x)) + + def srgb_to_linear(c: float) -> float: + return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 + + def linear_to_srgb(c: float) -> float: + return 12.92 * c if c <= 0.0031308 else 1.055 * (c ** (1 / 2.4)) - 0.055 + + a = clamp01(amount) + + if not perceptual: + return tuple(clamp01(c + (t - c) * a) for c, t in zip(rgb, target)) + + # Gamma-aware blend + rgb_lin = [srgb_to_linear(clamp01(c)) for c in rgb] + target_lin = [srgb_to_linear(clamp01(c)) for c in target] + blended_lin = [ + c + (t - c) * a for c, t in zip(rgb_lin, target_lin) + ] + return tuple(clamp01(linear_to_srgb(c)) for c in blended_lin) + +# ba_meta export babase.Plugin + + +class Plugin(babase.Plugin): + def on_app_running(self) -> None: + from bascenev1lib.actor.bomb import Blast, ExplodeHitMessage + from bascenev1lib.actor.spaz import Spaz + from bascenev1lib.actor.spazbot import SpazBot + from bascenev1lib.actor.playerspaz import PlayerSpaz + + # Colored Shields + org_equip_shields = Spaz.equip_shields + + def equip_shields(self, decay: bool = False) -> None: + org_equip_shields(self, decay) + if self.shield is not None: + safe_highlight = bs.safecolor( + getattr(self, '_original_highlight', self.node.highlight), target_intensity=0.75) + self.shield.color = bs.normalized_color(safe_highlight) + + Spaz.equip_shields = equip_shields + + # Slow Motion TNT Explosion + org_blast_init = Blast.__init__ + + def new_init(self: Blast, blast_type='normal', hit_type='explosion', *args, **kwargs) -> None: + org_blast_init(self, blast_type=blast_type, hit_type=hit_type, *args, **kwargs) + + if blast_type == 'tnt' and hit_type == 'explosion': + bs.camerashake(3.0) + activity = bs.getactivity() + gnode = activity.globalsnode + + # Pause + gnode.paused = True + self._pause_timer = bs.DisplayTimer( + 0.01, bs.CallPartial(setattr, gnode, 'paused', True), True) + + delay = 0.06 + + bs.displaytimer(delay, bs.CallPartial(setattr, self, '_pause_timer', None)) + bs.displaytimer(delay, bs.CallPartial(setattr, gnode, 'paused', False)) + + # Slow Motion + gnode.slow_motion = True + self._slow_motion_timer = bs.DisplayTimer( + 0.01, bs.CallPartial(setattr, gnode, 'slow_motion', True), True) + + delay += 0.14 + + bs.displaytimer(delay, bs.CallPartial(setattr, self, '_slow_motion_timer', None)) + bs.displaytimer(delay, bs.CallPartial( + setattr, gnode, 'slow_motion', activity.slow_motion)) + + Blast.__init__ = new_init + + # Explosion Spaz Coloring + org_handlemessage = Blast.handlemessage + + def new_handlemessage(self: Blast, msg: Any, *args, **kwargs) -> Any: + org_handlemessage(self, msg, *args, **kwargs) + + if isinstance(msg, ExplodeHitMessage): + node = bs.getcollision().opposingnode + delegate = node.getdelegate(PlayerSpaz) or node.getdelegate(SpazBot) + if (delegate and not delegate.shield) and not node.invincible: + if not hasattr(delegate, '_original_color'): + delegate._original_color = node.color + delegate._original_highlight = node.highlight + + original_color = delegate._original_color + original_highlight = delegate._original_highlight + + blend_time = 1.0 + start_time = 6.0 + end_time = 7.0 + + bs.emitfx( + position=node.position, + velocity=node.velocity, + count=int(4.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='ice' if self.blast_type == 'ice' else 'smoke', + ) + + explosion_intensity = 0.70 if not self.blast_type == 'tnt' else 0.96 + + if self.blast_type == 'ice': + explosion_intensity = 0.91 + explosion_color = (0.07, 0.6, 1.5) + elif self.blast_type == 'sticky': + explosion_intensity = 0.91 + explosion_color = (0.07, 0.9, 0.07) + blend_time = 1.0 + start_time = 1.2 + end_time = 3.0 + else: + explosion_color = (0.07, 0.03, 0.0) + + blend_color = blend_toward(node.color, explosion_color, + explosion_intensity, True) + blend_highlight = blend_toward( + node.highlight, explosion_color, explosion_intensity-0.03, True) + + bs.animate_array(node, 'color', 3, { + blend_time: blend_color, + start_time: blend_color, + end_time: original_color + }) + bs.animate_array(node, 'highlight', 3, { + blend_time: blend_highlight, + start_time: blend_highlight, + end_time: original_highlight + }) + + delegate._color_timer = bs.Timer( + end_time, bs.CallPartial(delattr, delegate, '_original_color')) + delegate._highlight_timer = bs.Timer( + end_time, bs.CallPartial(delattr, delegate, '_original_highlight')) + + Blast.handlemessage = new_handlemessage diff --git a/plugins/utilities/fluffyplaylisteditor.py b/plugins/utilities/fluffyplaylisteditor.py new file mode 100644 index 0000000..6f81fc8 --- /dev/null +++ b/plugins/utilities/fluffyplaylisteditor.py @@ -0,0 +1,2122 @@ +# ba_meta require api 9 + +from bascenev1._playlist import filter_playlist # , PlaylistType +from random import choice # , randint, randrange +from bascenev1 import ( + get_filtered_map_name, + get_map_class, + get_map_display_string, +) +from typing import Callable, Sequence, Literal, TypedDict, Any, cast +from bauiv1lib.confirm import ConfirmWindow +from bauiv1 import ( + # Window utils + Window, MainWindowState, + MainWindow, + UIScale, + + # Widget utils + Widget, widget, + containerwidget, + scrollwidget, + columnwidget, + buttonwidget as original_buttonwidget, + textwidget, + imagewidget, + checkboxwidget, + get_special_widget, + # Other + gettexture, + Mesh, Texture, Lstr, +) +from babase import ( + get_virtual_screen_size +) +from bauiv1lib.playlist.editcontroller import PlaylistEditController +import bauiv1lib.playlist.editcontroller +from bauiv1lib.playlist.editgame import PlaylistEditGameWindow +import bauiv1lib.playlist.editgame +from bauiv1lib.playlist.edit import PlaylistEditWindow +import bauiv1lib.playlist.edit +from bauiv1lib.playlist import PlaylistTypeVars +from babase import app, Plugin +import babase +import bascenev1 as bs +import bauiv1 as bui +from copy import deepcopy +import logging +plugman = dict( + plugin_name="fluffyplaylisteditor", + description="A simple not-so advanced Playlist Editor", + external_url="https://discord.com/channels/1001896771347304639/1483463979450896445", + authors=[ + {"name": "FluffyPal", "email": "", "discord": "fluffypal"} + ], + version="1.1.0", +) + +# Started: 12 Mar 2026 at night from scratch +# - Learning workflow +# - configuring packages +# - PlaylistEditWindow UI Polishing (I HATE UI POLISHING) + +# Continued: 13 Mar 2026 +# - PlaylistEditGameWindow UI Polishing (AAAA.. oh, found a bug) +# - Implemented GameEdit configs reset and restore +# - Implemented GameEdit map quick navigation buttons +# - Lazy TypedDict PlaylistType + +# Continued: 17 Mar 2026 +# - PlaylistEditController logics overwrite +# - Implemented PlaylistEdit duplicate game +# - Refactor filter_playlist +# - Implemented logics to show invalid games/maps in PlaylistEdit +# - Refine GameEdit reset and restore + +# Continued: 7 June 2026 +# - Fixed map select not recorded on MapSelect +# - Added batch game adding within supported maps + + +# ======= PLAYLIST PARENT =======# + +# ======= PLAYLIST BORWSER =======# +# Main Playlist Browser Window For Starting The Playlist +# Then goes to PlaylistCustomizeBrowserWindow if pressing "customize..." +# from bauiv1lib.playlist.browser import PlaylistBrowserWindow + +# import bauiv1lib.playlist.customizebrowser +# from bauiv1lib.playlist.customizebrowser import PlaylistCustomizeBrowserWindow + +# ======= PLAYLIST ADDING/EDITING =======# +# Main packages to modify +# - Game edit window + +# from bauiv1lib.playlist.addgame import PlaylistAddGameWindow + +# - Playlist games edit window + +# Window for adding registered games to the playlist and then goes to PlaylistEditController for editing +# The UI that shows games GameActivities names on the left and desc on the right +# And "Get More Games..." on the bottom +# After selecting the game, we goes to PlaylistEditGameWindow for configuring the game Settings and Map + +# - Game map select window +# import bauiv1lib.playlist.mapselect +# from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow + + +# ======= PLAYLIST PLAYING =======# +# This is used when we wanna "play" the playlist +# from bauiv1lib.play import PlayWindow, PlaylistSelectContext + +# from bauiv1lib.playoptions import PlayOptionsWindow + + +# ======= UI Packages =======# + +# ======= Type Hints =======# +# from enum import Enum + + +class UntotalOldTypedPlaylistDict(TypedDict, total=False): + map: str + level: str + resolved_type: type[bs.GameActivity] | None + is_unowned_map: bool + is_unowned_game: bool + is_map_invalid: bool + is_game_invalid: bool + + +class OldTypedPlaylistDict(TypedDict): + map: str + level: str + + +class TypedPlaylistDict(UntotalOldTypedPlaylistDict): + settings: OldTypedPlaylistDict + type: str + + +TypedPlaylistType = list[TypedPlaylistDict] + +# -------------------------- UI TOOLS --------------------------# + + +def buttonwidget( + *, + edit: Widget | None = None, + parent: Widget | None = None, + id: str | None = None, + size: Sequence[float] | None = None, + position: Sequence[float] | None = None, + on_activate_call: Callable | None = None, + label: str | Lstr | None = None, + color: Sequence[float] | None = None, + down_widget: Widget | None = None, + up_widget: Widget | None = None, + left_widget: Widget | None = None, + right_widget: Widget | None = None, + # texture: Texture | None = None, + text_scale: float | None = None, + textcolor: Sequence[float] | None = None, + enable_sound: bool | None = None, + mesh_transparent: Mesh | None = None, + mesh_opaque: Mesh | None = None, + repeat: bool | None = None, + scale: float | None = None, + transition_delay: float | None = None, + on_select_call: Callable | None = None, + button_type: str | None = None, + extra_touch_border_scale: float | None = None, + selectable: bool | None = None, + show_buffer_top: float | None = None, + icon: Texture | None = None, + iconscale: float | None = None, + icon_tint: float | None = None, + icon_color: Sequence[float] | None = None, + autoselect: bool | None = None, + mask_texture: Texture | None = None, + tint_texture: Texture | None = None, + tint_color: Sequence[float] | None = None, + tint2_color: Sequence[float] | None = None, + text_flatness: float | None = None, + text_res_scale: float | None = None, + text_literal: bool | None = None, + opacity: float | None = None, + better_bg_fit: bool | None = None, +) -> Widget: + return original_buttonwidget( + edit=edit, + parent=parent, + id=id, + size=size, + position=position, + on_activate_call=on_activate_call, + label=label, + color=color, + down_widget=down_widget, + up_widget=up_widget, + left_widget=left_widget, + right_widget=right_widget, + texture=gettexture('white'), + text_scale=text_scale, + textcolor=textcolor, + enable_sound=enable_sound, + mesh_transparent=mesh_transparent, + mesh_opaque=mesh_opaque, + repeat=repeat, + scale=scale, + transition_delay=transition_delay, + on_select_call=on_select_call, + button_type=button_type, + extra_touch_border_scale=extra_touch_border_scale, + selectable=selectable, + show_buffer_top=show_buffer_top, + icon=icon, + iconscale=iconscale, + icon_tint=icon_tint, + icon_color=icon_color, + autoselect=autoselect, + mask_texture=mask_texture, + tint_texture=tint_texture, + tint_color=tint_color, + tint2_color=tint2_color, + text_flatness=text_flatness, + text_res_scale=text_res_scale, + text_literal=text_literal, + opacity=opacity, + better_bg_fit=better_bg_fit + ) + + +def create_container(size: tuple[float, float], **kwargs) -> Widget: + """Create the main window with overlay.""" + # Container + p = containerwidget( + # parent=get_special_widget('overlay_stack'), + background=False, + # transition='in_scale', + size=size, + **kwargs + ) + x, y = get_virtual_screen_size() + + # Background + imagewidget( + parent=p, + texture=gettexture('white'), + size=(x*2, y*2), + position=(-x*0.5, 0-475), + opacity=0.55, + color=(0, 0, 0) + ) + + # Main Container View + border_size_range = 1.0075 + b = imagewidget( + texture=gettexture('white'), + parent=p, + size=(size[0]*border_size_range, size[1]*border_size_range), + tilt_scale=6, + color=tuple(col*1.5 for col in color_theme.get_color('primary')) + ) + f = imagewidget( # Foreground + texture=gettexture('white'), + parent=p, + size=size, + color=color_theme.get_color('bg') + ) + + border_size_offset = border_size_range-(border_size_range//border_size_range.real) + imagewidget( + edit=b, + position=tuple(pos*-border_size_offset for pos in f.center) + ) + + return p + + +def create_confirm(callback: Callable, text: str) -> Callable: + return lambda: ConfirmWindow( + text=f"{text}?", + color=color_theme.get_color('primary'), # pyright: ignore[reportArgumentType] + action=callback + ) + + +_SOUND_VOL = 1.2 + + +def play_error_sound(): + bui.getsound('error').play(_SOUND_VOL) + + +def play_guncocking_sound(): + bui.getsound('gunCocking').play(_SOUND_VOL) + + +def play_ding_sound(): + bui.getsound('ding').play(_SOUND_VOL) + + +def play_powerdown_sound(): + bui.getsound('powerdown01').play(_SOUND_VOL) + + +def play_shield_down_sound(): + bui.getsound('shieldDown').play(_SOUND_VOL) + + +def play_shield_up_sound(): + bui.getsound('shieldUp').play(_SOUND_VOL) + + +def play_deek_sound(): + bui.getsound(choice(['deek', 'deek2'])).play(_SOUND_VOL) + + +def play_click_sound(): + bui.getsound('click01').play(_SOUND_VOL) + + +class ScreenmessageColors: + RED = (1, 0, 0) + GREEN = (0, 1, 0) + BS_GREEN = (0.6, 1, 0.6) + BLUE = (0, 0, 1) + ORANGE = (1, 0.5, 0) + YELLOW = (1, 1, 0) + PURPLE = (0.6, 0, 1) + CYAN = (0, 1, 1) + MAGENTA = (1, 0, 1) + WHITE = (1, 1, 1) + BLACK = (0, 0, 0) + GRAY = (0.5, 0.5, 0.5) + PINK = (1, 0.4, 0.7) + + +_COLOR_TYPE = Literal[ + 'bg', 'primary', 'secondary', 'tertiary', 'unknown' +] + + +class ColorTheme: + main_color: Sequence[float] = (0.8, 0.8, 0.8) + colors: dict[_COLOR_TYPE, Sequence[float]] = { + 'bg': tuple(col*0.1 for col in main_color), + 'primary': main_color, + 'secondary': tuple(col*0.2 for col in main_color), + 'tertiary': tuple(col*0.4 for col in main_color), + 'unknown': (main_color[0]*1.2, main_color[1], main_color[2]) + } + + def get_color(self, color: _COLOR_TYPE) -> Sequence[float]: + return self.colors[color] + + +color_theme = ColorTheme() + + +# -------------------------- MAIN --------------------------# +class FluffyPlaylistEditController(PlaylistEditController): + def __init__( + self, + sessiontype: type[bs.Session], + from_window: MainWindow, + *, + existing_playlist_name: str | None = None, + playlist: TypedPlaylistType | None = None, + playlist_name: str | None = None, + ): + appconfig = bui.app.config + + # Since we may be showing our map list momentarily, + # lets go ahead and preload all map preview textures. + if app.classic is not None: + app.classic.preload_map_preview_media() + + self._sessiontype = sessiontype + + self._editing_game = False + self._editing_game_type: type[bs.GameActivity] | None = None + self._pvars = PlaylistTypeVars(sessiontype) + self._existing_playlist_name = existing_playlist_name + self._config_name_full = self._pvars.config_name + ' Playlists' + + self._is_batch_add = False + self._pre_game_add_state: MainWindowState | None = None + self._pre_game_edit_state: MainWindowState | None = None + + # Make sure config exists. + if self._config_name_full not in appconfig: + appconfig[self._config_name_full] = {} + + self._selected_index = 0 + if existing_playlist_name: + self._name = existing_playlist_name + + # Filter out invalid games. + self._playlist = new_filter_playlist( + appconfig[self._pvars.config_name + ' Playlists'][ + existing_playlist_name + ], + sessiontype=sessiontype, + remove_unowned=False, + # add_resolved_type=True, # HACK: This would cause json error as we save a Python class which aren't serializable + name=existing_playlist_name, + print_exc=False + ) + self._edit_ui_selection = None + else: + if playlist is not None: + self._playlist = playlist + else: + self._playlist = [] + if playlist_name is not None: + self._name = playlist_name + else: + # Find a good unused name. + i = 1 + while True: + self._name = ( + self._pvars.default_new_list_name.evaluate() + + ((' ' + str(i)) if i > 1 else '') + ) + if ( + self._name + not in appconfig[self._pvars.config_name + ' Playlists'] + ): + break + i += 1 + + # Also we want it to start with 'add' highlighted since its empty + # and that's all they can do. + self._edit_ui_selection = 'add_button' + + editwindow = from_window.main_window_replace( + lambda: FluffyPlaylistEditWindow(editcontroller=self) + ) + assert editwindow is not None + + # Once we've set our start window, store the back state. We'll + # skip back to there once we're fully done. + self._back_state = editwindow.main_window_back_state + + def duplicate_game_pressed(self) -> bool: + """Duplicate the currently selected game in the playlist and insert it just after.""" + if not self._playlist or self._selected_index is None: + bs.screenmessage("No game selected to duplicate", ScreenmessageColors.ORANGE) + play_error_sound() + return False + + base_entry = self._playlist[self._selected_index] + new_entry = deepcopy(base_entry) + + ins_index = self._selected_index + 1 + self._playlist.insert(ins_index, new_entry) + self._selected_index = ins_index + self._edit_ui_selection = None + + try: + cls = babase.getclass(new_entry['type'], bs.GameActivity) + name = cls.getname() + except (ImportError, AttributeError): + name = new_entry['type'] + + bs.screenmessage(f"Game '{name}' duplicated", ScreenmessageColors.BS_GREEN) + return True + + def toggle_epic_mode(self) -> bool: + if not self._playlist or self._selected_index is None: + bs.screenmessage("No game selected to toggle epic mode", ScreenmessageColors.ORANGE) + play_error_sound() + return False + + entry = self._playlist[self._selected_index] + if (epic_setting := 'Epic Mode') in entry['settings'] and isinstance(entry['settings'][epic_setting], bool): + # pyright: ignore[reportGeneralTypeIssues] + self._playlist[self._selected_index]['settings'][epic_setting] = not entry['settings'][epic_setting] + else: + play_error_sound() + return False + return True + + def toggle_solo_mode(self) -> bool: + if not self._playlist or self._selected_index is None: + bs.screenmessage("No game selected to toggle solo mode", ScreenmessageColors.ORANGE) + play_error_sound() + return False + + entry = self._playlist[self._selected_index] + if (solo_setting := 'Solo Mode') in entry['settings'] and isinstance(entry['settings'][solo_setting], bool): + # pyright: ignore[reportGeneralTypeIssues] + self._playlist[self._selected_index]['settings'][solo_setting] = not entry['settings'][solo_setting] + else: + play_error_sound() + return False + return True + + def reslove_default_settings_defs(self, settings: OldTypedPlaylistDict) -> list[bs.Setting]: + default_settings = list[bs.Setting]() + if (epic_mode := 'Epic Mode') in settings: + default_settings.append( + bs.BoolSetting(epic_mode, default=settings[epic_mode]) + ) + if (solo_mode := 'Solo Mode') in settings: + default_settings.append( + bs.BoolSetting(solo_mode, default=settings[solo_mode]) + ) + if (time_limit := 'Time Limit') in settings: + default_settings.append( + bs.IntChoiceSetting( + time_limit, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=settings[time_limit], + ) + ) + if (respawn_times := 'Respawn Times') in settings: + default_settings.append( + bs.FloatChoiceSetting( + respawn_times, + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=settings[respawn_times], + ) + ) + return default_settings + + def edit_game_pressed(self, from_window: MainWindow) -> None: + if not self._playlist: + return + + playlist = self._playlist[self._selected_index] + try: + cls = babase.getclass( + playlist['type'], + subclassof=bs.GameActivity + ) + except AttributeError as e: + bs.screenmessage( + f'Can\'t edit game: {e}. Maybe try fix it?', ScreenmessageColors.YELLOW) + play_error_sound() + return + + except ModuleNotFoundError as e: + class FakeGameActivity(bs.GameActivity): + name = playlist['type'] + available_settings = self.reslove_default_settings_defs(playlist['settings']) + cls = FakeGameActivity + + self._show_edit_ui( + gametype=cls, + settings=playlist['settings'], # pyright: ignore[reportArgumentType] + from_window=from_window, + ) + + def _show_edit_ui( # pyright: ignore[reportIncompatibleMethodOverride] + self, + gametype: type[bs.GameActivity], + settings: TypedPlaylistDict | None, + from_window: bui.MainWindow, + ) -> None: + # pylint: disable=cyclic-import + if not from_window.main_window_has_control(): + return + + self._editing_game = settings is not None + self._editing_game_type = gametype + assert self._sessiontype is not None + + # Jump into an edit window. + editwindow = from_window.main_window_replace( + lambda: FluffyPlaylistEditGameWindow( + gametype, + self._sessiontype, + settings, + completion_call=self._edit_game_done, + ) + ) + assert editwindow is not None + + # Once we're there, store the back state. We'll use that to jump + # back out to our current location once the edit is done. + assert self._pre_game_edit_state is None + self._pre_game_edit_state = editwindow.main_window_back_state + + def _edit_game_done( # pyright: ignore[reportIncompatibleMethodOverride] + self, config: TypedPlaylistDict | None, from_window: bui.MainWindow + ) -> None: + """Called after finished editing/adding a game""" + # No-op if provided window isn't in charge. + if not from_window.main_window_has_control(): + return + + assert bui.app.classic is not None + if config is None: + play_powerdown_sound() + else: + # Make sure type is in there. + assert self._editing_game_type is not None + if self._editing_game: + playlist = self._playlist[self._selected_index] + config['type'] = playlist['type'] + else: + config['type'] = bui.get_type_name(self._editing_game_type) + + if self._editing_game: + self._playlist[self._selected_index] = deepcopy(config) + else: + # Add a new entry to the playlist. + insert_index = min( + len(self._playlist), self._selected_index + 1 + ) + + self._selected_index = insert_index + self._playlist.insert(insert_index, deepcopy(config)) + + if self._is_batch_add: + for _map in self._get_supported_maps_for_game(config, self._editing_game_type): + new_config = deepcopy(config) + new_config['map'] = _map + new_config['settings']['map'] = _map + self._playlist.insert(insert_index, new_config) + insert_index += 1 + + play_guncocking_sound() + + # If we're adding, jump to before the add started. + # Otherwise jump to before the edit started. + assert ( + self._pre_game_edit_state is not None + or self._pre_game_add_state is not None + ) + if self._pre_game_add_state is not None: + from_window.main_window_back_state = self._pre_game_add_state + elif self._pre_game_edit_state is not None: + from_window.main_window_back_state = self._pre_game_edit_state + + from_window.main_window_back() + self._pre_game_edit_state = None + self._pre_game_add_state = None + self._is_batch_add = False + + def _get_supported_maps_for_game(self, config: TypedPlaylistDict, gametype: type[bs.GameActivity]): + assert app.classic is not None + store = app.classic.store + + valid_maps = set(gametype.get_supported_maps(self._sessiontype)) + to_remove = set(store.get_unowned_maps()) + + if config_map := config.get('map') or (config.get('settings', {}) or {}).get('map'): + to_remove.add(config_map) + + valid_maps -= to_remove + + return sorted(valid_maps) + + def batch_add_game_pressed(self, from_window: MainWindow): + self._is_batch_add = True + self.add_game_pressed(from_window) + + +class FluffyPlaylistEditWindow(PlaylistEditWindow): + def __init__( + self, + editcontroller: FluffyPlaylistEditController, + transition: str | None = 'in_right', + origin_widget: Widget | None = None, + ): + self._list_widgets: list[Widget] = [] + self._editcontroller = editcontroller + editcontroller._is_batch_add = False # HACK: Couldn't think of any clean idea :p + + self._r = 'editGameListWindow' + prev_selection: str | None = self._editcontroller.get_edit_ui_selection() + + # Container Setup + uiscale = app.ui_v1.uiscale + is_small_ui = uiscale is UIScale.SMALL + # Keep a constant aspect ratio for all UIScale values. + # Let's use width:height (classic UI aspect) as a reference. + # We'll use a base width and scale it for each UI scale while maintaining the ratio. + base_width = 800 + aspect_ratio = 16 / 9 + self._width = base_width * 1.65 + + self._height = self._width / aspect_ratio + + # Adjust x_inset proportionally to width + self.x_inset = x_inset = int(self._width * (0.09 if is_small_ui else 0.02)) + self.yoffs = yoffs = -68 if is_small_ui else -15 + + # Round to integers for pixel positions + self._width = int(self._width) + self._height = int(self._height) + + MainWindow.__init__(self, # pyright: ignore[reportArgumentType] + root_widget=create_container( + size=(self._width, self._height), + scale=( + 1.20 if is_small_ui else + 0.80 if uiscale is UIScale.MEDIUM else + 0.60 + ), + toolbar_visibility=( + 'menu_minimal_no_back' + if is_small_ui + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, + ) + + # Widgets/children + # Buttons + self.b_color = b_color = color_theme.get_color('primary') + self.b_textcolor = b_textcolor = color_theme.get_color('secondary') + + cancel_button = buttonwidget( + parent=self._root_widget, + position=(35 + x_inset, self._height - 50 + yoffs), + scale=0.8, + size=(175, 60), + autoselect=True, + color=(b_color[0]*1.2, *b_color[1:]), + textcolor=b_textcolor, + label=bui.Lstr(resource='cancelText'), + text_scale=1.2, + ) + save_button = buttonwidget( + parent=self._root_widget, + position=( + self._width - ((225 if is_small_ui else 255) + x_inset), + self._height - 50 + yoffs + ), + scale=0.8, + size=(190, 60), + autoselect=True, + color=(b_color[0], b_color[1]*1.2, b_color[2]), + textcolor=b_textcolor, + left_widget=cancel_button, + label=bui.Lstr(resource='saveText'), + text_scale=1.2, + ) + + widget( + edit=save_button, + right_widget=bui.get_special_widget('squad_button'), + ) + + widget( + edit=cancel_button, + left_widget=cancel_button, + right_widget=save_button, + ) + + v_gap = 60 + h = 40 + x_inset + r_h = x_inset * (9.45 if is_small_ui else 45.25) + + left_button_v_size = 45 + right_button_v_size = 45 + + v = self._height - 172.0 + yoffs + v *= 1.1 + v -= 2.0 + v -= v_gap + buttonwidget( + parent=self._root_widget, + position=(r_h, v), + size=(110, right_button_v_size), + on_activate_call=self._toggle_epic_mode, + enable_sound=False, + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label="Toggle\nEpic Mode", + ) + v -= v_gap + + buttonwidget( + parent=self._root_widget, + position=(r_h, v), + size=(110, right_button_v_size), + on_activate_call=self._toggle_solo_mode, + enable_sound=False, + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label="Toggle\nSolo Mode", + ) + v -= v_gap + + v = self._height - 172.0 + yoffs + v *= 1.1 + v -= 2.0 + v -= v_gap + + add_game_button = buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, left_button_v_size), + on_activate_call=self._add, + on_select_call=bui.CallPartial(self._set_ui_selection, 'add_button'), + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label=bui.Lstr(resource=f'{self._r}.addGameText'), + ) + v -= v_gap + + batch_add_game_button = buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, left_button_v_size), + on_activate_call=self._batch_add, + on_select_call=bui.CallPartial(self._set_ui_selection, 'batch_add_button'), + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label=f"Batch {bui.Lstr(resource=f'{self._r}.addGameText').evaluate()}", + ) + v -= v_gap + + self._edit_button = edit_game_button = buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, left_button_v_size), + on_activate_call=self._edit, + on_select_call=bui.CallPartial(self._set_ui_selection, 'editButton'), + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label=bui.Lstr(resource=f'{self._r}.editGameText'), + ) + v -= v_gap + + remove_game_button = buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, left_button_v_size), + text_scale=0.8, + on_activate_call=self._remove, + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + label=bui.Lstr(resource=f'{self._r}.removeGameText'), + ) + v -= v_gap + + # Customs + duplicate_button = buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, left_button_v_size), + on_activate_call=self._duplicate_selected_game, + autoselect=True, + enable_sound=False, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label="Duplicate\nGame", + ) + v -= v_gap - 10 + + h += 9 + buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(42, 35), + on_activate_call=self._move_up, + label=bui.charstr(bui.SpecialChar.UP_ARROW), + button_type='square', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + repeat=True, + ) + h += 52 + buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(42, 35), + on_activate_call=self._move_down, + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + label=bui.charstr(bui.SpecialChar.DOWN_ARROW), + repeat=True, + ) + + # Scroller + v = self._height - 100 + yoffs + scroll_height = self._height - ( + 250 if is_small_ui else 155 + ) + self._scroll_width = self._width - (205 + (2.5 if is_small_ui else 5.5) * x_inset) + + scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(160 + x_inset, v - scroll_height), + highlight=False, + on_select_call=bui.CallStrict(self._set_ui_selection, 'gameList'), + size=(self._scroll_width, (scroll_height - 15)), + border_opacity=0.4, + ) + widget( + edit=scrollwidget, + left_widget=add_game_button, + right_widget=scrollwidget, + ) + self._columnwidget = columnwidget( + parent=scrollwidget, border=2, margin=0 + ) + + for button in [ + add_game_button, batch_add_game_button, edit_game_button, remove_game_button, duplicate_button + ]: + widget( + edit=button, left_widget=button, right_widget=scrollwidget + ) + + buttonwidget(edit=cancel_button, on_activate_call=self._cancel) + containerwidget( + edit=self._root_widget, + cancel_button=cancel_button, + selected_child=scrollwidget, + ) + + buttonwidget(edit=save_button, on_activate_call=self._save_press) + containerwidget(edit=self._root_widget, start_button=save_button) + + # Texts + textwidget( + parent=self._root_widget, + position=(-10, self._height - 50 + yoffs), + size=(self._width, 25), + text=bui.Lstr(resource=f'{self._r}.titleText'), + color=app.ui_v1.title_color, + scale=1.05, + h_align='center', + v_align='center', + maxwidth=270, + ) + + v = self._height - 115.0 + yoffs + + textwidget( + parent=self._root_widget, + text=bui.Lstr(resource=f'{self._r}.listNameText'), + position=(196 + x_inset, v + 31), + maxwidth=150, + color=(0.8, 0.8, 0.8, 0.5), + size=(0, 0), + scale=0.75, + h_align='right', + v_align='center', + ) + + self._text_field = text_field = textwidget( + parent=self._root_widget, + position=(210 + x_inset, v + 7), + size=(self._scroll_width - 53, 43), + text=self._editcontroller.getname(), + h_align='left', + v_align='center', + max_chars=40, + maxwidth=380, + autoselect=True, + color=(0.9, 0.9, 0.9, 1.0), + description=bui.Lstr(resource=f'{self._r}.listNameText'), + editable=True, + padding=4, + on_return_press_call=self._save_press_with_sound, + ) + widget(edit=self._columnwidget, up_widget=text_field) + widget(edit=cancel_button, down_widget=text_field) + widget(edit=add_game_button, up_widget=text_field) + widget(edit=batch_add_game_button, up_widget=add_game_button) + + if prev_selection == 'add_button': + containerwidget( + edit=self._root_widget, selected_child=add_game_button + ) + elif prev_selection == 'batch_add_button': + containerwidget( + edit=self._root_widget, selected_child=batch_add_game_button + ) + elif prev_selection == 'editButton': + containerwidget( + edit=self._root_widget, selected_child=edit_game_button + ) + elif prev_selection == 'gameList': + containerwidget( + edit=self._root_widget, selected_child=scrollwidget + ) + + self._refresh() + + def _get_invalid_game_name(self, pentry: TypedPlaylistDict): + name = pentry['type'] + # A few substitutions for 'Epic', 'Solo' etc. modes. + # FIXME: Should provide a way for game types to define filters of + # their own and should not rely on hard-coded settings names. + if (solo_mode := 'Solo Mode') in pentry['settings'] and pentry['settings'][solo_mode]: + name = babase.Lstr( + resource='soloNameFilterText', subs=[('${NAME}', name)] + ) + if (epic_mode := 'Epic Mode') in pentry['settings'] and pentry['settings'][epic_mode]: + name = babase.Lstr( + resource='epicNameFilterText', subs=[('${NAME}', name)] + ) + + # Resolve map name + if 'map' in pentry['settings']: + sval = babase.Lstr( + value='${NAME} @ ${MAP}', + subs=[ + ('${NAME}', name), + ('${MAP}', bs.get_map_display_string( + bs.get_filtered_map_name(pentry['settings']['map'])), + ), + ], + ) + elif 'map' in pentry: + sval = babase.Lstr( + value='${NAME} @ ${MAP}', + subs=[ + ('${NAME}', name), + ('${MAP}', bs.get_map_display_string( + bs.get_filtered_map_name(pentry['map'])), + ), + ], + ) + else: + print('invalid game config - expected map entry under settings') + sval = babase.Lstr(value='???') + + return sval + + def _duplicate_selected_game(self) -> None: + if self._editcontroller.duplicate_game_pressed(): + play_guncocking_sound() + self._refresh() + + def _move_down(self) -> None: + if len(self._editcontroller.get_playlist()) > 1: + super()._move_down() + + def _move_up(self) -> None: + if len(self._editcontroller.get_playlist()) > 1: + super()._move_up() + + def _toggle_epic_mode(self): + if self._editcontroller.toggle_epic_mode(): + play_guncocking_sound() + self._refresh() + + def _toggle_solo_mode(self): + if self._editcontroller.toggle_solo_mode(): + play_guncocking_sound() + self._refresh() + + def _refresh(self) -> None: + # Need to grab this here as rebuilding the list will + # change it otherwise. + old_selection_index = self._editcontroller.get_selected_index() + + while self._list_widgets: + self._list_widgets.pop().delete() + # pyright: ignore[reportAssignmentType] + for index, pentry in enumerate(self._editcontroller.get_playlist()): + pentry: TypedPlaylistDict + try: + cls = babase.getclass(pentry['type'], subclassof=bs.GameActivity) + # pyright: ignore[reportArgumentType] + desc = cls.get_settings_display_string(pentry) + color = (0.8, 0.8, 0.8, 1.0) + except Exception: + # logging.exception('Error in playlist refresh.') + # desc = "(invalid: '" + pentry['type'] + "')" + desc = self._get_invalid_game_name(pentry) + color = color_theme.get_color('unknown') + + txtw = textwidget( + parent=self._columnwidget, + size=(self._width - 80, 30), + on_select_call=bui.CallStrict(self._select, index), + always_highlight=True, + color=color, + padding=0, + maxwidth=self._scroll_width * 0.93, + text=desc, + on_activate_call=self._edit_button.activate, + v_align='center', + selectable=True, + ) + widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) + + # Wanna be able to jump up to the text field from the top one. + if index == 0: + widget(edit=txtw, up_widget=self._text_field) + self._list_widgets.append(txtw) + if old_selection_index == index: + columnwidget( + edit=self._columnwidget, + selected_child=txtw, + visible_child=txtw, + ) + + def _batch_add(self) -> None: + # Store list name then tell the session to perform an add. + self._editcontroller.setname( + cast(str, bui.textwidget(query=self._text_field)) + ) + self._editcontroller.batch_add_game_pressed(from_window=self) + + +class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): + + def __init__( + self, + gametype: type[bs.GameActivity], + sessiontype: type[bs.Session], + config: TypedPlaylistDict | None, + completion_call: Callable[[TypedPlaylistDict | None, bui.MainWindow], Any], + default_selection: str | None = None, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + edit_info: dict[str, Any] | None = None + ): + + assert app.classic is not None + store = app.classic.store + + self._scrollwidget: Widget | None = None + self._subcontainer: Widget | None = None + + self._gametype = gametype + self._sessiontype = sessiontype + + # If we're within an editing session we get passed edit_info + # (returning from map selection window, etc). + if edit_info is not None: + self._edit_info = edit_info + + # ..otherwise determine whether we're adding or editing a game based + # on whether an existing config was passed to us. + else: + if config is None: + self._edit_info = {'editType': 'add'} + else: + self._edit_info = {'editType': 'edit'} + + self._r = 'gameSettingsWindow' + + self._valid_maps = valid_maps = gametype.get_supported_maps(sessiontype) + if not valid_maps: + bui.screenmessage(bui.Lstr(resource='noValidMapsErrorText')) + raise RuntimeError('No valid maps found.') + self._valid_maps_owned = [m for m in self._valid_maps if m not in store.get_unowned_maps()] + + self._config = config + + self._settings_defs = gametype.get_available_settings(sessiontype) + self._completion_call = completion_call + + # If there's a valid map name in the existing config, use that. + self._map: str | None = None + # To start with, pick a random map out of the ones we own. + unowned_maps = store.get_unowned_maps() + try: + if ( + config is not None + and 'map' in config + ): + filtered_map_name = get_filtered_map_name( + config['map'] + ) + if filtered_map_name not in unowned_maps: + self._map = filtered_map_name + elif ( + config is not None + and (settings := config.get('settings')) + and (raw_map := settings.get('map')) + ): + filtered_map_name = get_filtered_map_name(raw_map) + if filtered_map_name not in unowned_maps: + self._map = filtered_map_name + # else: + # raise Exception() + except Exception: + logging.exception('Error getting map for editor.') + + if not self._map: + if valid_maps_owned := [m for m in valid_maps if m not in unowned_maps]: + self._map = choice(valid_maps_owned) + # Hmmm.. we own none of these maps.. just pick a random un-owned one + # I guess.. should this ever happen? + else: + self._map = choice(valid_maps) + + if config is not None: + if 'settings' in config: + self._settings = config['settings'] + else: + self._settings = config + else: + self._settings: OldTypedPlaylistDict = {} # pyright: ignore[reportAttributeAccessIssue] + self._settings['map'] = self._map + + self._default_settings = deepcopy(self._settings) + + try: + self.map_tex_name = get_map_class(self._map).get_preview_texture_name() + except babase.NotFoundError: + self.map_tex_name = 'null' + + if self.map_tex_name is None: + raise RuntimeError(f'No map preview tex found for {self._map}.') + self._choice_selections: dict[str, int] = {} + + # Container Setup + uiscale = app.ui_v1.uiscale + is_small_ui = uiscale is UIScale.SMALL + # Keep a constant aspect ratio for all UIScale values. + # Let's use width:height (classic UI aspect) as a reference. + # We'll use a base width and scale it for each UI scale while maintaining the ratio. + base_width = 900 + aspect_ratio = 5 / 4 + self._width = width = int(base_width * 1.65) + self._height = height = int(base_width / aspect_ratio) + + y_extra2 = 50 # For topper widget elements + + # Adjust x_inset proportionally to width + self.x_inset = int(width * (0.09 if is_small_ui else 0.0225)) + self.yoffs = yoffs = -68 if is_small_ui else -30 + + MainWindow.__init__(self, + root_widget=containerwidget( + size=(width, height), + color=color_theme.get_color('bg'), + scale=( + 1.10 if is_small_ui else + 0.80 if uiscale is UIScale.MEDIUM else + 0.70 + ), + toolbar_visibility=( + 'menu_minimal_no_back' + if uiscale is UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, + ) + + # Widgets/children + b_color = color_theme.get_color('primary') + b_textcolor = color_theme.get_color('secondary') + + is_add = self._edit_info['editType'] == 'add' + cancel_button = original_buttonwidget( + parent=self._root_widget, + position=(45 + self.x_inset, height - 82 + y_extra2 + yoffs), + size=(60, 48) if is_add else (180, 65), + label=( + bui.charstr(bui.SpecialChar.BACK) if is_add else + bui.Lstr(resource='cancelText') + ), + button_type='backSmall' if is_add else None, + autoselect=True, + scale=1.0 if is_add else 0.75, + text_scale=1.3, + color=(b_color[0]*1.2, *b_color[1:]), + textcolor=b_textcolor, + on_activate_call=bui.CallStrict(self._cancel), + ) + containerwidget(edit=self._root_widget, cancel_button=cancel_button) + + # Title + textwidget( + parent=self._root_widget, + position=((-20 if is_small_ui else -18), height - 70 + y_extra2 + yoffs), + size=(width, 25), + text=gametype.get_display_string(), + color=bui.app.ui_v1.title_color, + maxwidth=width*0.35, + scale=1.1, + h_align='center', + v_align='center', + ) + + self.add_button = add_button = original_buttonwidget( + parent=self._root_widget, + position=(width - ((255 if is_small_ui else 235) + self.x_inset), + height - 82 + y_extra2 + yoffs), + size=(200, 65), + scale=0.75, + text_scale=1.3, + color=(b_color[0], b_color[1]*1.2, b_color[2]), + textcolor=b_textcolor, + label=( + bui.Lstr(resource=f'{self._r}.addGameText') if is_add else + bui.Lstr(resource='applyText') + ), + ) + + base_h_pos = width - ((155 if is_small_ui else 135) + self.x_inset) + base_v_pos = height * 0.75 + right_buttons_gap = 75 + + reset_config_text = "Reset\nSettings" + original_buttonwidget( + parent=self._root_widget, + position=(base_h_pos, base_v_pos - right_buttons_gap), + size=(200, 100), + scale=0.75, + text_scale=1.3, + color=b_color, + textcolor=b_textcolor, + label=reset_config_text, + on_activate_call=create_confirm( + self._reset_settings, reset_config_text.replace('\n', ' ')), + icon=gettexture('replayIcon'), + iconscale=1.5 + ) + right_buttons_gap += right_buttons_gap + + restore_config_text = "Restore\nSettings" + original_buttonwidget( + parent=self._root_widget, + position=(base_h_pos, base_v_pos - right_buttons_gap), + size=(200, 100), + scale=0.75, + text_scale=1.3, + color=b_color, + textcolor=b_textcolor, + label=restore_config_text, + on_activate_call=( + create_confirm(self._restore_settings, restore_config_text.replace( + '\n', ' ')) if not is_add else play_error_sound + ), + icon=gettexture('leftButton'), + iconscale=1.5 + ) + + self._refresh_settings_items() + + original_buttonwidget( + edit=add_button, on_activate_call=bui.CallStrict(self._add) + ) + containerwidget( + edit=self._root_widget, + selected_child=add_button, + start_button=add_button, + ) + + if default_selection == 'map': + containerwidget( + edit=self._root_widget, selected_child=self._scrollwidget + ) + containerwidget( + edit=self._subcontainer, selected_child=self._map_buttonwidget + ) + + # Tools + def _reset_settings(self): + updated = False + if self._settings: + for setting in self._settings_defs: + if (value := setting.default) != (data := self._settings)[setting.name]: + data[setting.name] = value + updated = True + + if (def_map := self._default_settings['map']) != self._map: + self._map = def_map + try: + self.map_tex_name = get_map_class(def_map).get_preview_texture_name() + except babase.NotFoundError: + self.map_tex_name = 'null' + updated = True + + if updated: + self._refresh_settings_items() + play_shield_down_sound() + else: + play_error_sound() + + def _restore_settings(self): + updated = False + # Only update keys which are different, and track if anything actually changed + for key, value in self._default_settings.items(): + if key not in self._settings or self._settings[key] != value: + # print(f'key restored: {self._settings[key]} -> {key}') + self._settings[key] = value + updated = True + + # Check if map changed, update relevant attrs + restored_map = get_filtered_map_name(self._settings['map']) + if self._map != restored_map: + # print(f'map restored: {self._map} -> {restored_map}') + self._map = restored_map + try: + self.map_tex_name = get_map_class(self._map).get_preview_texture_name() + except babase.NotFoundError: + self.map_tex_name = 'null' + updated = True + + if updated: + self._refresh_settings_items() + self._update_map_widget() + play_ding_sound() + else: + play_error_sound() + + def _randomize_map(self): + valid_maps = self._valid_maps_owned + + # Hmmm.. we own none of these maps.. just pick a random un-owned one + # I guess.. should this ever happen? + cur_map = choice(valid_maps) + self._map = cur_map + self.map_tex_name = get_map_class(cur_map).get_preview_texture_name() + self._update_map_widget() + + play_deek_sound() + + def _update_map_widget(self): + assert self.map_tex_name and self._map + imagewidget( + edit=self._map_imagewidget, + texture=gettexture(self.map_tex_name) + ) + textwidget( + edit=self._map_textwidget, + text=get_map_display_string(self._map) + ) + + def _shift_selected_map(self, index: int): + """Shift selection in the valid maps list by target (wrap around)""" + valid_maps = self._valid_maps_owned + + cur_map = self._map + assert cur_map + try: + cur_map_index = valid_maps.index(cur_map) + except ValueError: + cur_map_index = 0 + new_index = (cur_map_index + index) % len(valid_maps) + + self._map = valid_maps[new_index] + self.map_tex_name = get_map_class(self._map).get_preview_texture_name() + + self._update_map_widget() + play_click_sound() + + def _refresh_settings_items(self): + uiscale = app.ui_v1.uiscale + is_small_ui = uiscale is UIScale.SMALL + + pbtn = get_special_widget('squad_button') + widget(edit=self.add_button, right_widget=pbtn, up_widget=pbtn) + + map_height = 100 + + scroll_width = self._width - (86 + (3 if is_small_ui else 5.5) * self.x_inset) + + spacing = 47 # Scroller playlist config items spacing + y_extra = 15 # For scroller widget + + # Calc our total height we'll need + scroll_height = map_height + 10 # map select and margin + scroll_height += spacing * len(self._settings_defs) + + if not self._scrollwidget: + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=( + 44 + self.x_inset, + (95 if uiscale is UIScale.SMALL else 55) + y_extra + self.yoffs, + ), + size=( + scroll_width, + self._height - (166 if uiscale is UIScale.SMALL else 116), + ), + highlight=False, + claims_left_right=True, + selection_loops_to_parent=True, + border_opacity=0.4, + ) + if self._subcontainer: + for child in self._subcontainer.get_children(): + child.delete() + self._subcontainer.delete() + + self._subcontainer = containerwidget( + parent=self._scrollwidget, + size=(scroll_width, scroll_height), + background=False, + claims_left_right=True, + selection_loops_to_parent=True, + ) + + v = scroll_height - 5 + h = -40 + + # Keep track of all the selectable widgets we make so we can wire + # them up conveniently. + widget_column: list[list[bui.Widget]] = [] + + b_color = color_theme.get_color('primary') + b_textcolor = color_theme.get_color('secondary') + + textwidget( + parent=self._subcontainer, + position=(h + 49, v - 63), + size=(100, 30), + maxwidth=110, + text=bui.Lstr(resource='mapText'), + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center', + ) + assert self.map_tex_name + map_tex = gettexture(self.map_tex_name) + + self._map_imagewidget = imagewidget( + parent=self._subcontainer, + size=(256 * 0.7, 125 * 0.7), + position=(h + scroll_width * 0.46, v - 90), + texture=map_tex, + mesh_opaque=bui.getmesh('level_select_button_opaque'), + mesh_transparent=bui.getmesh('level_select_button_transparent'), + mask_texture=gettexture('mapPreviewMask'), + ) + + self._map_buttonwidget = None + if len(self._valid_maps_owned) > 1: + original_buttonwidget( # map_prev + parent=self._subcontainer, + position=(h + scroll_width * 0.46 - 50 - 1, v - 63), + size=(35, 35), + label='<', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallPartial(self._shift_selected_map, -1), + enable_sound=False, + repeat=True, + ) + original_buttonwidget( # map_next + parent=self._subcontainer, + position=(h + scroll_width * (0.655 if is_small_ui else 0.6225) + 5, v - 63), + size=(35, 35), + label='>', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallPartial(self._shift_selected_map, 1), + enable_sound=False, + repeat=True, + ) + original_buttonwidget( + parent=self._subcontainer, + size=(140, 60), + position=(h + scroll_width * (0.775 if is_small_ui else 0.8), v - 72), + on_activate_call=bui.CallStrict(self._randomize_map), + enable_sound=False, + color=b_color, + textcolor=b_textcolor, + scale=0.7, + label="Randomize", + ) + # Map select button. + self._map_buttonwidget = original_buttonwidget( + parent=self._subcontainer, + size=(140, 60), + position=(h + scroll_width * 0.9, v - 72), + on_activate_call=bui.CallStrict(self._select_map), + scale=0.7, + color=b_color, + textcolor=b_textcolor, + label=bui.Lstr(resource='mapSelectText'), + ) + widget_column.append([self._map_buttonwidget]) + + assert self._map + self._map_textwidget = textwidget( + parent=self._subcontainer, + position=(h + scroll_width * 0.496, v - 114), + size=(100, 30), + flatness=1.0, + shadow=1.0, + scale=0.55, + maxwidth=256 * 0.7 * 0.8, + text=get_map_display_string(self._map), + h_align='center', + color=(0.6, 1.0, 0.6, 1.0), + v_align='center', + ) + v -= map_height + + config = self._settings + assert config + for setting in self._settings_defs: + value = setting.default + value_type = type(value) + + # Now, if there's an existing value for it in the config, + # override with that. + try: + if config is not None: + if ( + 'settings' in config + and (seeting_name := setting.name) in config['settings'] + ): + value = value_type(config['settings'][seeting_name]) + elif (seeting_name := setting.name) in config: + value = value_type(config[seeting_name]) + except Exception: + logging.exception('Error getting game setting.') + + # Shove the starting value in there to start. + self._settings[setting.name] = value + + name_translated = self._get_localized_setting_name(setting.name) + + mw1 = 280 + mw2 = 70 + + # Handle types with choices specially: + item_h_pos = h + scroll_width * 0.96 + if isinstance(setting, bs.ChoiceSetting): + invalid = False + for choice in setting.choices: + if len(choice) != 2: + raise ValueError( + "Expected 2-member tuples for 'choices'; got: " + + repr(choice) + ) + if not isinstance(choice[0], str): + raise TypeError( + 'First value for choice tuple must be a str; got: ' + + repr(choice) + ) + if not isinstance(choice[1], value_type): + invalid = True + # raise TypeError( + # 'Choice type does not match default value; choice:' + # + repr(choice) + # + '; setting:' + # + repr(setting) + # ) + if value_type not in (int, float): + raise TypeError( + 'Choice type setting must have int or float default; ' + 'got: ' + repr(setting) + ) + + # Start at the choice corresponding to the default if possible. + self._choice_selections[setting.name] = 0 + for index, choice in enumerate(setting.choices): + if choice[1] == value: + self._choice_selections[setting.name] = index + break + + v -= spacing + textwidget( + parent=self._subcontainer, + position=(h + 50, v), + size=(100, 30), + maxwidth=mw1, + text=name_translated, + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center', + ) + txt = textwidget( + parent=self._subcontainer, + position=(item_h_pos - 95, v), + size=(0, 28), + text=self._get_localized_setting_name( + setting.choices[self._choice_selections[setting.name]][ + 0 + ] + ), + editable=False, + color=(0.6, 1.0, 0.6, 1.0), + maxwidth=mw2, + h_align='right', + v_align='center', + padding=2, + ) + btn1 = original_buttonwidget( + parent=self._subcontainer, + position=(item_h_pos - 50 - 1, v), + size=(28, 28), + label='<', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallStrict( + self._choice_inc, setting.name, txt, setting, -1 + ) if not invalid else lambda: play_error_sound(), + repeat=True, + ) + btn2 = original_buttonwidget( + parent=self._subcontainer, + position=(item_h_pos + 5, v), + size=(28, 28), + label='>', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallStrict( + self._choice_inc, setting.name, txt, setting, 1 + ) if not invalid else lambda: play_error_sound(), + repeat=True, + ) + widget_column.append([btn1, btn2]) + + elif isinstance(setting, (bs.IntSetting, bs.FloatSetting)): + v -= spacing + min_value = setting.min_value + max_value = setting.max_value + increment = setting.increment + textwidget( + parent=self._subcontainer, + position=(h + 50, v), + size=(100, 30), + text=name_translated, + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center', + maxwidth=mw1, + ) + txt = textwidget( + parent=self._subcontainer, + position=(item_h_pos - 95, v), + size=(0, 28), + text=str(value), + editable=False, + color=(0.6, 1.0, 0.6, 1.0), + maxwidth=mw2, + h_align='right', + v_align='center', + padding=2, + ) + btn1 = original_buttonwidget( + parent=self._subcontainer, + position=(item_h_pos - 50 - 1, v), + size=(28, 28), + label='-', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallStrict( + self._inc, + txt, + min_value, + max_value, + -increment, + value_type, + setting.name, + ), + repeat=True, + ) + btn2 = original_buttonwidget( + parent=self._subcontainer, + position=(item_h_pos + 5, v), + size=(28, 28), + label='+', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallStrict( + self._inc, + txt, + min_value, + max_value, + increment, + value_type, + setting.name, + ), + repeat=True, + ) + widget_column.append([btn1, btn2]) + + elif value_type == bool: + v -= spacing + textwidget( + parent=self._subcontainer, + position=(h + 50, v), + size=(100, 30), + text=name_translated, + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center', + maxwidth=mw1, + ) + txt = textwidget( + parent=self._subcontainer, + position=(item_h_pos - 95, v), + size=(0, 28), + text=( + bui.Lstr(resource='onText') + if value + else bui.Lstr(resource='offText') + ), + editable=False, + color=(0.6, 1.0, 0.6, 1.0), + maxwidth=mw2, + h_align='right', + v_align='center', + padding=2, + ) + cbw = checkboxwidget( + parent=self._subcontainer, + text='', + position=(item_h_pos - 50 - 5, v - 2), + size=(200, 30), + autoselect=True, + color=b_color, + textcolor=b_textcolor, + value=value, + on_value_change_call=bui.CallPartial( + self._check_value_change, setting.name, txt + ), + ) + widget_column.append([cbw]) + + else: + raise TypeError(f'Invalid value type: {value_type}.') + + # Ok now wire up the column. + try: + prev_widgets: list[Widget] | None = None + for cwdg in widget_column: + if prev_widgets is not None: + # Wire our rightmost to their rightmost. + widget(edit=prev_widgets[-1], down_widget=cwdg[-1]) + widget(edit=cwdg[-1], up_widget=prev_widgets[-1]) + + # Wire our leftmost to their leftmost. + widget(edit=prev_widgets[0], down_widget=cwdg[0]) + widget(edit=cwdg[0], up_widget=prev_widgets[0]) + prev_widgets = cwdg + except Exception: + logging.exception( + 'Error wiring up game-settings-select widget column.' + ) + + +original_filter_playlist = filter_playlist + + +def new_filter_playlist( + playlist: TypedPlaylistDict, + sessiontype: type[bs.Session], + *, + add_resolved_type: bool = False, + remove_unowned: bool = True, + mark_unowned: bool = False, + name: str = '?', + print_exc: bool = True +) -> TypedPlaylistType: + """Return a filtered version of a playlist. + + Strips out or replaces invalid or unowned game types, makes sure all + settings are present, and adds in a 'resolved_type' which is the actual + type. + """ + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + from bascenev1 import get_filtered_map_name, GameActivity + + assert app.classic is not None + + goodlist = TypedPlaylistType() + available_maps: list[str] = list(app.classic.maps.keys()) + if (remove_unowned or mark_unowned) and app.classic is not None: + unowned_maps = app.classic.store.get_unowned_maps() + unowned_game_types = app.classic.store.get_unowned_game_types() + else: + unowned_maps = [] + unowned_game_types = set() + + for entry in deepcopy(playlist): # pyright: ignore[reportAssignmentType] + entry: TypedPlaylistDict + # 'map' used to be called 'level' here. + if 'level' in entry: + entry['map'] = entry['level'] + del entry['level'] + + # We now stuff map into settings instead of it being its own thing. + if 'map' in entry: + entry['settings']['map'] = entry['map'] + del entry['map'] + + # Update old map names to new ones. + entry['settings']['map'] = get_filtered_map_name( + entry['settings']['map'] + ) + if remove_unowned and entry['settings']['map'] in unowned_maps: + continue + + # Ok, for each game in our list, try to import the module and grab + # the actual game class. add successful ones to our initial list + # to present to the user. + if not isinstance(entry['type'], str): + raise TypeError('invalid entry format') + try: + # Do some type filters for backwards compat. + if entry['type'] in ( + 'Assault.AssaultGame', + 'Happy_Thoughts.HappyThoughtsGame', + 'bsAssault.AssaultGame', + 'bs_assault.AssaultGame', + 'bastd.game.assault.AssaultGame', + ): + entry['type'] = 'bascenev1lib.game.assault.AssaultGame' + if entry['type'] in ( + 'King_of_the_Hill.KingOfTheHillGame', + 'bsKingOfTheHill.KingOfTheHillGame', + 'bs_king_of_the_hill.KingOfTheHillGame', + 'bastd.game.kingofthehill.KingOfTheHillGame', + ): + entry['type'] = ( + 'bascenev1lib.game.kingofthehill.KingOfTheHillGame' + ) + if entry['type'] in ( + 'Capture_the_Flag.CTFGame', + 'bsCaptureTheFlag.CTFGame', + 'bs_capture_the_flag.CTFGame', + 'bastd.game.capturetheflag.CaptureTheFlagGame', + ): + entry['type'] = ( + 'bascenev1lib.game.capturetheflag.CaptureTheFlagGame' + ) + if entry['type'] in ( + 'Death_Match.DeathMatchGame', + 'bsDeathMatch.DeathMatchGame', + 'bs_death_match.DeathMatchGame', + 'bastd.game.deathmatch.DeathMatchGame', + ): + entry['type'] = 'bascenev1lib.game.deathmatch.DeathMatchGame' + if entry['type'] in ( + 'ChosenOne.ChosenOneGame', + 'bsChosenOne.ChosenOneGame', + 'bs_chosen_one.ChosenOneGame', + 'bastd.game.chosenone.ChosenOneGame', + ): + entry['type'] = 'bascenev1lib.game.chosenone.ChosenOneGame' + if entry['type'] in ( + 'Conquest.Conquest', + 'Conquest.ConquestGame', + 'bsConquest.ConquestGame', + 'bs_conquest.ConquestGame', + 'bastd.game.conquest.ConquestGame', + ): + entry['type'] = 'bascenev1lib.game.conquest.ConquestGame' + if entry['type'] in ( + 'Elimination.EliminationGame', + 'bsElimination.EliminationGame', + 'bs_elimination.EliminationGame', + 'bastd.game.elimination.EliminationGame', + ): + entry['type'] = 'bascenev1lib.game.elimination.EliminationGame' + if entry['type'] in ( + 'Football.FootballGame', + 'bsFootball.FootballTeamGame', + 'bs_football.FootballTeamGame', + 'bastd.game.football.FootballTeamGame', + ): + entry['type'] = 'bascenev1lib.game.football.FootballTeamGame' + if entry['type'] in ( + 'Hockey.HockeyGame', + 'bsHockey.HockeyGame', + 'bs_hockey.HockeyGame', + 'bastd.game.hockey.HockeyGame', + ): + entry['type'] = 'bascenev1lib.game.hockey.HockeyGame' + if entry['type'] in ( + 'Keep_Away.KeepAwayGame', + 'bsKeepAway.KeepAwayGame', + 'bs_keep_away.KeepAwayGame', + 'bastd.game.keepaway.KeepAwayGame', + ): + entry['type'] = 'bascenev1lib.game.keepaway.KeepAwayGame' + if entry['type'] in ( + 'Race.RaceGame', + 'bsRace.RaceGame', + 'bs_race.RaceGame', + 'bastd.game.race.RaceGame', + ): + entry['type'] = 'bascenev1lib.game.race.RaceGame' + if entry['type'] in ( + 'bsEasterEggHunt.EasterEggHuntGame', + 'bs_easter_egg_hunt.EasterEggHuntGame', + 'bastd.game.easteregghunt.EasterEggHuntGame', + ): + entry['type'] = ( + 'bascenev1lib.game.easteregghunt.EasterEggHuntGame' + ) + if entry['type'] in ( + 'bsMeteorShower.MeteorShowerGame', + 'bs_meteor_shower.MeteorShowerGame', + 'bastd.game.meteorshower.MeteorShowerGame', + ): + entry['type'] = ( + 'bascenev1lib.game.meteorshower.MeteorShowerGame' + ) + if entry['type'] in ( + 'bsTargetPractice.TargetPracticeGame', + 'bs_target_practice.TargetPracticeGame', + 'bastd.game.targetpractice.TargetPracticeGame', + ): + entry['type'] = ( + 'bascenev1lib.game.targetpractice.TargetPracticeGame' + ) + except Exception: + if print_exc: + logging.exception('Error in new_filter_playlist.') + + neededsettings = list[bs.Setting]() + gameclass = None + try: + gameclass = babase.getclass(entry['type'], GameActivity) + + if remove_unowned and gameclass in unowned_game_types: + continue + if add_resolved_type: + entry['resolved_type'] = gameclass + if mark_unowned and gameclass in unowned_game_types: + entry['is_unowned_game'] = True + neededsettings = gameclass.get_available_settings(sessiontype) + + except babase.MapNotFoundError: + if print_exc: + logging.warning( + 'Map \'%s\' not found while scanning playlist \'%s\'.', + entry['settings']['map'], + name, + ) + except ImportError as e: + if print_exc: + logging.warning( + 'Import failed while scanning playlist \'%s\': %s', name, e + ) + entry['is_game_invalid'] = True + # This exception usually happens when we could get the game 'module' + # but, we couldn't get game's GameActivity class name from `entry['type']` + except AttributeError as e: + logging.warning( + 'Get class failed while scanning playlist \'%s\': %s', name, e + ) + entry['is_game_invalid'] = True + + # We 'manually' add some of basic ba*.setting(s) to the filter + # if it exists in raw settings + if entry['settings']['map'] not in available_maps: + entry['is_map_invalid'] = True + + if mark_unowned and entry['settings']['map'] in unowned_maps: + entry['is_unowned_map'] = True + + # Make sure all settings the game defines are present. + for setting in neededsettings: + if setting.name not in entry['settings']: + entry['settings'][setting.name] = setting.default + + goodlist.append(entry) + + return goodlist + + +def apply_packages(): + # Playlist editor controller for PlaylistEditWindow + bauiv1lib.playlist.editcontroller.PlaylistEditController = FluffyPlaylistEditController + + bauiv1lib.playlist.editgame.PlaylistEditGameWindow = FluffyPlaylistEditGameWindow # Playlist Game Editer Window + bauiv1lib.playlist.edit.PlaylistEditWindow = FluffyPlaylistEditWindow # Playlist Editor Window + + bs.filter_playlist = new_filter_playlist + # pyright: ignore[reportAttributeAccessIssue] + bs._playlist.filter_playlist = new_filter_playlist + + +# ba_meta export babase.Plugin +class by_FluffyPal(Plugin): + def on_app_running(self) -> None: + apply_packages() diff --git a/plugins/utilities/glowing_profiles.py b/plugins/utilities/glowing_profiles.py new file mode 100644 index 0000000..799e7c8 --- /dev/null +++ b/plugins/utilities/glowing_profiles.py @@ -0,0 +1,222 @@ +# ba_meta require api 9 +from __future__ import annotations + +from typing import TYPE_CHECKING +import babase +import _babase +import bascenev1 as bs +from types import FunctionType +import enum + + +if TYPE_CHECKING: + from typing import Type, Callable, Any, Tuple + +plugman = dict( + plugin_name="glowing_profiles", + description="This plugin gives your profile glowlight, just like on some servers, but only offline", + external_url="https://m.youtube.com/watch?v=Jb_dKz99rhY", + authors=[ + {"name": "andrejkuroglo8", "email": "andrejkuroglo8@gmail.com", "discord": "andrewku"}, + ], + version="1.0.0", +) + + +def redefine_method(dst: Tuple[Any, str], src: Tuple[Any, str]) -> None: + if hasattr(getattr(*src), '__redefine_type') and getattr(*src).__redefine_type in ( + RedefineFlag.DECORATE_PRE, RedefineFlag.DECORATE_AFTER, RedefineFlag.DECORATE_ADVANCED): + new = getattr(*src) + old = getattr(*dst) + func: Callable + if getattr(*src).__redefine_type == RedefineFlag.DECORATE_AFTER: + def func(*args, **kwargs): + returned = old(*args, **kwargs) + return new(*args, **kwargs, returned=returned) + elif getattr(*src).__redefine_type == RedefineFlag.DECORATE_PRE: + def func(*args, **kwargs): + new(*args, **kwargs) + return old(*args, **kwargs) + else: + def func(*args, **kwargs): + return new(*args, **kwargs, old_function=old) + + setattr(*dst, func) + else: + setattr(*dst, getattr(*src)) + + # Fucking super()! + # dst.__code__ = CodeType( + # src.__code__.co_argcount, + # src.__code__.co_posonlyargcount, + # src.__code__.co_kwonlyargcount, + # src.__code__.co_nlocals, + # src.__code__.co_stacksize, + # src.__code__.co_flags, + # src.__code__.co_code, + # src.__code__.co_consts, + # src.__code__.co_names, + # src.__code__.co_varnames, + # src.__code__.co_filename, + # dst.__code__.co_name, + # src.__code__.co_firstlineno, + # src.__code__.co_lnotab, + # dst.__code__.co_freevars, + # dst.__code__.co_cellvars) + + +def redefine_class_methods(orig_cls: Type[object]) -> Callable[[Any], None]: + """Returns decorator that redefines all class methods + + Parameters: + orig_cls (Type[object]): class that will redefined""" + + def decorator(cls) -> None: + # for method in filter(lambda x: isinstance(getattr(cls, x), FunctionType), dir(cls)): + for method in cls._redefine_methods: + if hasattr(orig_cls, method): + redefine_method((orig_cls, method), (cls, method)) + else: + setattr(orig_cls, method, getattr(cls, method)) + # setattr(getattr(orig_cls, 'self'), '__class__', getattr(cls, method)) # Fucking super()!!!! + + return decorator + + +class RedefineFlag(enum.Enum): + REDEFINE = 0 + DECORATE_AFTER = 1 + DECORATE_PRE = 2 + DECORATE_ADVANCED = 3 + DECORATE = DECORATE_AFTER + + +def redefine_flag(*flags: RedefineFlag) -> Callable[[Callable], Callable]: + def decorator(func: Callable) -> Callable: + for flag in flags: + if flag in (RedefineFlag.DECORATE_AFTER, RedefineFlag.REDEFINE, RedefineFlag.DECORATE_PRE, + RedefineFlag.DECORATE_ADVANCED): + func.__redefine_type = flag + return func + + return decorator + + +def get_locale(*args): + return "Error" + + +@redefine_class_methods(bs.Chooser) +class Chooser: + _redefine_methods = ('_gcinit', '_get_glowing_colors', 'update_from_profile', + '_getname') + + def _gcinit(self): + if hasattr(self, '_gcinit_done'): + return + self.glow_dict = {} + self._markers = ('"', "'", '^', '%', ';', '`') + self._get_glowing_colors() + self._gcinit_done = True + + @redefine_flag(RedefineFlag.REDEFINE) + def _get_glowing_colors(self): + """Search glowing code among profiles.""" + try: + should_del = [] + for i in self._profilenames: + for m in self._markers: + if i.startswith(m + ','): + code = i.split(',') + self.glow_dict[code[0]] = ( + float(code[1]), + float(code[2]), + int(code[3]), + int(code[4])) + # should_del.append(i) + for i in should_del: + self._profilenames.remove(i) + except Exception as err: + print(err) + ba.screenmessage( + get_locale('init_glowing_code_error'), + color=(1, 0, 0), + clients=[self._player.get_input_device().client_id], + transient=True) + + @redefine_flag(RedefineFlag.DECORATE_ADVANCED) + def _getname(self, full=True, old_function=None): + name = old_function(self, full) + for m in self._markers: + name = name.replace(m, '') + return name + + @redefine_flag(RedefineFlag.DECORATE_ADVANCED) + def update_from_profile(self, old_function): + self._gcinit() + from bascenev1 import _profile + try: + self._profilename = self._profilenames[self._profileindex] + character = self._profiles[self._profilename]['character'] + + if self._profilename[0] in self.glow_dict: + if (character not in self._character_names + and character in _ba.app.spaz_appearances): + self._character_names.append(character) + self._character_index = self._character_names.index(character) + + player_glowing_dict = self.glow_dict[self._profilename[0]] + color_marker = player_glowing_dict[0] + color_marker = max(-999.0, min(color_marker, 50.0)) + + highlight_marker = float(player_glowing_dict[1]) + highlight_marker = max(-999.0, min(highlight_marker, 50.0)) + + stabilize_color = int(player_glowing_dict[2]) > 0 + stabilize_highlight = int(player_glowing_dict[3]) > 0 + self._color, self._highlight = \ + _profile.get_player_profile_colors( + self._profilename, + profiles=self._profiles) + + if stabilize_color: + m = max(self._color) + self._color = list(self._color) + for i in (0, 1, 2): + if self._color[i] == m: + self._color[i] = self._color[i] * color_marker + self._color = tuple(self._color) + else: + self._color = ( + self._color[0] * color_marker, + self._color[1] * color_marker, + self._color[2] * color_marker) + + if not stabilize_highlight: + self._highlight = ( + self._highlight[0] * highlight_marker, + self._highlight[1] * highlight_marker, + self._highlight[2] * highlight_marker) + else: + m = max(self._highlight) + self._highlight = list(self._highlight) + for i in (0, 1, 2): + if self._highlight[i] == m: + self._highlight[i] = \ + self._highlight[i] * highlight_marker + self._highlight = tuple(self._highlight) + else: + old_function(self) + except KeyError: + self.character_index = self._random_character_index + self._color = self._random_color + self._highlight = self._random_highlight + + self._update_icon() + self._update_text() + +# ba_meta export babase.Plugin + + +class Glowing(babase.Plugin): + pass diff --git a/plugins/utilities/powerup_manager.py b/plugins/utilities/powerup_manager.py index 2a81577..2dad733 100644 --- a/plugins/utilities/powerup_manager.py +++ b/plugins/utilities/powerup_manager.py @@ -1,2985 +1,2972 @@ -# ba_meta require api 9 -from __future__ import annotations - -import babase -import bauiv1 as bui -import bascenev1 as bs -import random -from bascenev1lib.actor import bomb -from bascenev1lib.actor import powerupbox as pupbox -from bascenev1lib.actor.spazbot import SpazBot -from bascenev1lib.actor.bomb import Bomb, Blast -from bauiv1lib.popup import PopupWindow, PopupMenuWindow, PopupMenu -from bascenev1lib.actor.spaz import ( - Spaz, - SpazFactory, - PickupMessage, - PunchHitMessage, - CurseExplodeMessage, - BombDiedMessage, -) -from bascenev1lib.mainmenu import MainMenuActivity, MainMenuSession -from bascenev1lib.gameutils import SharedObjects -from bascenev1lib.actor.powerupbox import PowerupBoxFactory -from bascenev1lib.actor.popuptext import PopupText -from bauiv1lib.confirm import ConfirmWindow -from bascenev1lib.actor.spaz import * - -from typing import TYPE_CHECKING - -plugman = dict( - plugin_name="powerup_manager", - description="This plugin add new modded powerups and features to manage them", - external_url="", - authors=[ - {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, - ], - version="1.0.0", -) - - -_sp_ = '\n' - -if TYPE_CHECKING: - pass - - -# === Mod updated by ATD and Less === - - -def getlanguage(text, subs: str = None, almacen: list = []): - if almacen == []: - almacen = list(range(1000)) - lang = bs.app.lang.language - translate = { - "Reset": {"Spanish": "Reiniciar", "English": "Reset", "Portuguese": "Reiniciar"}, - "Nothing": { - "Spanish": "Sin potenciadores", - "English": "No powerups", - "Portuguese": "Sem powerups", - }, - "Action 1": {"Spanish": "Potenciadores", "English": "Powerups", "Portuguese": "Powerups"}, - "Action 2": {"Spanish": "Configuración", "English": "Settings", "Portuguese": "Definições"}, - "Action 3": {"Spanish": "Extras", "English": "Extras", "Portuguese": "Extras"}, - "Action 4": {"Spanish": "Tienda", "English": "Store", "Portuguese": "Loja"}, - "Action 5": { - "Spanish": "Canjear código", - "English": "Enter Code", - "Portuguese": "Código promocional", - }, - "Custom": {"Spanish": "", "English": "Customize", "Portuguese": "Customizar"}, - "Impairment Bombs": { - "Spanish": "Bombas menoscabo", - "English": "Hyperactive bombs", - "Portuguese": "Bombas hiperativas", - }, - "Speed": {"Spanish": "Velocidad", "English": "Speed", "Portuguese": "Velocidade"}, - "Fire Bombs": { - "Spanish": "Bombas de fuego", - "English": "Fire Bombs", - "Portuguese": "Bombas de fogo", - }, - "Ice Man": { - "Spanish": "Hombre de hielo", - "English": "Ice man", - "Portuguese": "Homem de gelo", - }, - "Fly Bombs": { - "Spanish": "Bombas expansivas", - "English": "Expansive bombs", - "Portuguese": "Bombas expansivas", - }, - "Goodbye": {"Spanish": "¡Hasta luego!", "English": "Goodbye!", "Portuguese": "Adeus!"}, - "Healing Damage": { - "Spanish": "Auto-curación", - "English": "Healing Damage", - "Portuguese": "Auto-cura", - }, - "Tank Shield": { - "Spanish": "Súper blindaje", - "English": "Reinforced shield", - "Portuguese": "Escudo reforçado", - }, - "Tank Shield PTG": { - "Spanish": "Porcentaje de disminución", - "English": "Percentage decreased", - "Portuguese": "Percentual reduzido", - }, - "Healing Damage PTG": { - "Spanish": "Porcentaje de recuperación de salud", - "English": "Percentage of health recovered", - "Portuguese": "Porcentagem de recuperação de saúde", - }, - "SY: BALL": {"Spanish": "Esfera", "English": "Sphere", "Portuguese": "Esfera"}, - "SY: Impact": {"Spanish": "Especial", "English": "Special", "Portuguese": "Especial"}, - "SY: Egg": {"Spanish": "Huevito", "English": "Egg shape", "Portuguese": "Ovo"}, - "Powerup Scale": { - "Spanish": "Tamaño del potenciador", - "English": "Powerups size", - "Portuguese": "Tamanho de powerups", - }, - "Powerup With Shield": { - "Spanish": "Potenciadores con escudo", - "English": "Powerups with shield", - "Portuguese": "Powerups com escudo", - }, - "Powerup Time": { - "Spanish": "Mostrar Temporizador", - "English": "Show end time", - "Portuguese": "Mostrar cronômetro", - }, - "Powerup Style": { - "Spanish": "Forma de los potenciadores", - "English": "Shape of powerup", - "Portuguese": "Forma de powerup", - }, - "Powerup Name": { - "Spanish": "Mostrar nombre en los potenciadores", - "English": "Show name on powerups", - "Portuguese": "Mostrar nome em powerups", - }, - "Percentage": { - "Spanish": "Probabilidad", - "English": "Show percentage", - "Portuguese": "Mostrar porcentagem", - }, - "Only Items": { - "Spanish": "Sólo Accesorios", - "English": "Only utensils", - "Portuguese": "Apenas utensilios", - }, - "New": {"Spanish": "Nuevo", "English": "New", "Portuguese": "Novo"}, - "Only Bombs": { - "Spanish": "Sólo Bombas", - "English": "Only bombs", - "Portuguese": "Apenas bombas", - }, - "Coins 0": { - "Spanish": "Monedas Insuficientes", - "English": "Insufficient coins", - "Portuguese": "Moedas insuficientes", - }, - "Purchase": { - "Spanish": "Compra realizada correctamente", - "English": "Successful purchase", - "Portuguese": "Compra Bem Sucedida", - }, - "Double Product": { - "Spanish": "Ya has comprado este artículo", - "English": "You've already bought this", - "Portuguese": "Voce ja comprou isto", - }, - "Bought": {"Spanish": "Comprado", "English": "Bought", "Portuguese": "Comprou"}, - "Confirm Purchase": { - "Spanish": f'Tienes {subs} monedas. {_sp_} ¿Deseas comprar esto?', - "English": f'You have {subs} coins. {_sp_} Do you want to buy this?', - "Portuguese": f'Você tem {subs} moedas. {_sp_} Deseja comprar isto?', - }, - "FireBombs Store": { - "Spanish": 'Bombas de fuego', - "English": 'Fire bombs', - "Portuguese": 'Bombas de incêndio', - }, - "Timer Store": {"Spanish": 'Temporizador', "English": 'Timer', "Portuguese": 'Timer'}, - "Percentages Store": {"Spanish": 'Extras', "English": 'Extras', "Portuguese": 'Extras'}, - "Block Option Store": { - "Spanish": f"Uuups..{_sp_}Esta opción está bloqueada.{_sp_} Para acceder a ella puedes {_sp_} comprarla en la tienda.{_sp_} Gracias...", - "English": f"Oooops...{_sp_}This option is blocked. {_sp_} To access it you can buy {_sp_} it in the store.{_sp_} Thank you...", - "Portuguese": f"Ooops...{_sp_}Esta opção está bloqueada. {_sp_} Para acessá-lo, você pode {_sp_} comprá-lo na loja.{_sp_} Obrigado...", - }, - "True Code": { - "Spanish": "¡Código canjeado!", - "English": "Successful code!", - "Portuguese": "¡Código válido!", - }, - "False Code": { - "Spanish": "Código ya canjeado", - "English": "Expired code", - "Portuguese": "Código expirado", - }, - "Invalid Code": { - "Spanish": "Código inválido", - "English": "Invalid code", - "Portuguese": "Código inválido", - }, - "Reward Code": { - "Spanish": f"¡Felicitaciones! ¡Ganaste {subs} monedas!", - "English": f"Congratulations! You've {subs} coins", - "Portuguese": f"Parabéns! Você tem {subs} moedas", - }, - "Creator": { - "Spanish": "Mod edited by ATD", - "English": "Mod edited by ATD", - "Portuguese": "Mod edited by ATD", - }, - "Mod Info": { - "Spanish": f"Un mod genial que te permite gestionar {_sp_} los potenciadores a tu antojo. {_sp_} también incluye 8 potenciadores extra{_sp_} dejando 17 en total... ¡Guay!", - "English": f"A cool mod that allows you to manage {_sp_} powerups at your whims. {_sp_} also includes 8 extra powerups{_sp_} leaving 17 in total... Wow!", - "Portuguese": f"Um mod legal que permite que você gerencie os{_sp_} powerups de de acordo com seus caprichos. {_sp_} também inclui 8 powerups extras,{_sp_} deixando 17 no total... Uau!", - }, - "Coins Message": { - "Spanish": f"Recompensa: {subs} Monedas", - "English": f"Reward: {subs} Coins", - "Portuguese": f"Recompensa: {subs} Moedas", - }, - "Coins Limit Message": { - "Spanish": f"Ganaste {almacen[0]} Monedas.{_sp_} Pero has superado el límite de {almacen[1]}", - "English": f"You won {almacen[0]} Coins. {_sp_} But you have exceeded the limit of {almacen[1]}", - "Portuguese": f"Você ganhou {almacen[0]} Moedas. {_sp_} Mas você excedeu o limite de {almacen[1]}", - }, - } - languages = ['Spanish', 'Portuguese', 'English'] - if lang not in languages: - lang = 'English' - - if text not in translate: - return text - - return translate[text][lang] - - -def settings_distribution(): - return { - "Powers Gravity": False, - "Tank Shield PTG": 96, - "Healing Damage PTG": 72, - "Powerup Style": 'Auto', - "Powerup Scale": 1.0, - "Powerup Name": False, - "Powerup With Shield": False, - "Powerup Time": False, - } - - -apg = babase.app.config -if "PPM Settings" in apg: - old = apg['PPM Settings'] - for settings in settings_distribution(): - if settings not in old: - apg['PPM Settings'] = settings_distribution() -else: - apg['PPM Settings'] = settings_distribution() -apg.apply_and_commit() - -config = apg['PPM Settings'] - - -def default_powerups(): - return { - "Shield": 2, - "Punch": 3, - "Mine Bombs": 2, - "Impact Bombs": 3, - "Ice Bombs": 3, - "Triple": 3, - "Sticky Bombs": 3, - "Curse": 1, - "Health": 1, - "Speed": 2, - "Healing Damage": 1, - "Goodbye": 2, - "Ice Man": 1, - "Tank Shield": 1, - "Impairment Bombs": 2, - "Fire Bombs": 3, - "Fly Bombs": 3, - } - - -if "Powerups" in config: - p_old = config['Powerups'] - for powerups in default_powerups(): - if powerups not in p_old: - config['Powerups'] = default_powerups() -else: - config['Powerups'] = default_powerups() -apg.apply_and_commit() - -powerups = config['Powerups'] - -# === EXTRAS === - -GLOBAL = {"Tab": 'Action 1', "Cls Powerup": 0, "Coins Message": []} - - -# === STORE === -def promo_codes(): - return { - "G-Am54igO42Os": [True, 1100], - "P-tRo8nM8dZ": [True, 2800], - "Y-tU2B3S": [True, 500], - "B-0mB3RYT2z": [True, 910], - "B-Asd14mON9G0D": [True, 910], - "D-rAcK0cJ23": [True, 910], - "E-a27ZO6f3Y": [True, 600], - "E-Am54igO42Os": [True, 600], - "E-M4uN3K34XB": [True, 840], - "PM-731ClcAF": [True, 50000], - } - - -def store_items(): - return {"Buy Firebombs": True, "Buy Option": True, "Buy Percentage": True} - - -if apg.get('Bear Coin') is None: - apg['Bear Coin'] = 0 - apg.apply_and_commit() - -if apg.get('Bear Coin') is not None: - if apg['Bear Coin'] <= 0: - apg['Bear Coin'] = 0 - apg['Bear Coin'] = int(apg['Bear Coin']) - -if apg.get('Bear Store') is None: - apg['Bear Store'] = {} - -for i, j in store_items().items(): - store = apg['Bear Store'] - if i not in store: - if store.get(i) is None: - store[i] = j - apg.apply_and_commit() - -STORE = apg['Bear Store'] - -if STORE.get('Promo Code') is None: - STORE['Promo Code'] = promo_codes() - -for i, x in promo_codes().items(): - pmcode = STORE['Promo Code'] - if i not in pmcode: - if pmcode.get(i) is None: - pmcode[i] = x - -apg.apply_and_commit() - - -class BearStore: - def __init__(self, price: int = 1000, value: str = '', callback: call = None): - - self.price = price - self.value = value - self.store = STORE[value] - self.coins = apg['Bear Coin'] - self.callback = callback - - def buy(self): - if not self.store: - if self.coins >= (self.price): - - def confirm(): - STORE[self.value] = True - apg['Bear Coin'] -= int(self.price) - bs.broadcastmessage(getlanguage('Purchase'), (0, 1, 0)) - bui.getsound('cashRegister').play() - apg.apply_and_commit() - self.callback() - - ConfirmWindow( - getlanguage('Confirm Purchase', subs=self.coins), - width=400, - height=120, - action=confirm, - ok_text=babase.Lstr(resource='okText'), - ) - else: - bs.broadcastmessage(getlanguage('Coins 0'), (1, 0, 0)) - bui.getsound('error').play() - else: - bs.broadcastmessage(getlanguage('Double Product'), (1, 0, 0)) - bui.getsound('error').play() - - def __del__(self): - apg['Bear Coin'] = int(apg['Bear Coin']) - apg.apply_and_commit() - - -class PromoCode: - def __init__(self, code: str = ''): - self.code = code - self.codes_store = STORE['Promo Code'] - if self.code in self.codes_store: - self.code_type = STORE['Promo Code'][code] - self.promo_code_expire = self.code_type[0] - self.promo_code_amount = self.code_type[1] - - def __del__(self): - apg['Bear Coin'] = int(apg['Bear Coin']) - apg.apply_and_commit() - - def code_confirmation(self): - if self.code != "": - bs.broadcastmessage(babase.Lstr(resource='submittingPromoCodeText'), (0, 1, 0)) - try: - babase.pushcall(babase.CallPartial(self.validate_code), from_other_thread=True) - except: - pass - - def validate_code(self): - if self.code in self.codes_store: - if self.promo_code_expire: - with babase.ContextRef.empty(): - babase.pushcall( - babase.CallPartial(self.successful_code), from_other_thread=True - ) - bs.broadcastmessage(getlanguage('True Code'), (0, 1, 0)) - bui.getsound('cheer').play() - self.code_type[0] = False - else: - bs.broadcastmessage(getlanguage('False Code'), (1, 0, 0)) - bui.getsound('error').play() - else: - bs.broadcastmessage(getlanguage('Invalid Code'), (1, 0, 0)) - bui.getsound('error').play() - - def successful_code(self): - apg['Bear Coin'] += self.promo_code_amount - bs.broadcastmessage(getlanguage('Reward Code', subs=self.promo_code_amount), (0, 1, 0)) - bui.getsound('cashRegister2').play() - - -MainMenuActivity.super_transition_in = MainMenuActivity.on_transition_in - - -def new_on_transition_in(self): - self.super_transition_in() - limit = 8400 - bear_coin = apg['Bear Coin'] - coins_message = GLOBAL['Coins Message'] - try: - if not (STORE['Buy Firebombs'] and STORE['Buy Option'] and STORE['Buy Percentage']): - - if coins_message != []: - result = 0 - for i in coins_message: - result += i - - if not bear_coin >= (limit - 1): - bs.broadcastmessage(getlanguage('Coins Message', subs=result), (0, 1, 0)) - bui.getsound('cashRegister').play() - else: - bs.broadcastmessage( - getlanguage('Coins Limit Message', almacen=[result, limit]), (1, 0, 0) - ) - bui.getsound('error').play() - self.bear_coin_message = True - GLOBAL['Coins Message'] = [] - except: - pass - - -SpazBot.super_handlemessage = SpazBot.handlemessage - - -def bot_handlemessage(self, msg: Any): - self.super_handlemessage(msg) - if isinstance(msg, bs.DieMessage): - if not self.die: - self.die = True - self.limit = 8400 - self.free_coins = random.randint(1, 25) - self.bear_coins = apg['Bear Coin'] - - if not self.bear_coins >= (self.limit): - self.bear_coins += self.free_coins - GLOBAL['Coins Message'].append(self.free_coins) - - if self.bear_coins >= (self.limit): - self.bear_coins = self.limit - - apg['Bear Coin'] = int(self.bear_coins) - apg.apply_and_commit() - - else: - GLOBAL['Coins Message'].append(self.free_coins) - - -def cls_pow_color(): - return [ - (1, 0.1, 0.1), - (0.1, 0.5, 0.9), - (0.1, 0.9, 0.9), - (0.1, 0.9, 0.1), - (0.1, 1, 0.5), - (1, 1, 0.2), - (2, 0.5, 0.5), - (1, 0, 6), - ] - - -def random_color(): - a = random.random() * 3 - b = random.random() * 3 - c = random.random() * 3 - return (a, b, c) - - -def powerup_dist(): - return ( - ('triple_bombs', powerups['Triple']), - ('ice_bombs', powerups['Ice Bombs']), - ('punch', powerups['Punch']), - ('impact_bombs', powerups['Impact Bombs']), - ('land_mines', powerups['Mine Bombs']), - ('sticky_bombs', powerups['Sticky Bombs']), - ('shield', powerups['Shield']), - ('health', powerups['Health']), - ('curse', powerups['Curse']), - ('speed', powerups['Speed']), - ('health_damage', powerups['Healing Damage']), - ('goodbye', powerups['Goodbye']), - ('ice_man', powerups['Ice Man']), - ('tank_shield', powerups['Tank Shield']), - ('impairment_bombs', powerups['Impairment Bombs']), - ('fire_bombs', powerups['Fire Bombs']), - ('fly_bombs', powerups['Fly Bombs']), - ) - - -def percentage_tank_shield(): - percentage = config['Tank Shield PTG'] - percentage_text = ('0.') + str(percentage) - return float(percentage_text) - - -def percentage_health_damage(): - percentage = config['Healing Damage PTG'] - percentage_text = ('0.') + str(percentage) - return float(percentage_text) - - -# === Modify class === - - -class NewProfileBrowserWindow: - def __init__( - self, - transition: str = 'in_right', - in_main_menu: bool = True, - selected_profile: str = None, - origin_widget: bui.Widget = None, - ): - super().__init__(transition, in_main_menu, selected_profile, origin_widget) - - self.session = bs.get_foreground_host_session() - uiscale = bui.app.ui_v1.uiscale - width = 100 if uiscale is babase.UIScale.SMALL else -14 - size = 50 - position = (width * 1.65, 300) - - if isinstance(self.session, MainMenuSession): - self.button = bui.buttonwidget( - parent=self._root_widget, - autoselect=True, - position=position, - size=(size, size), - button_type='square', - label='', - on_activate_call=babase.CallPartial(self.powerupmanager_window), - ) - - size = size * 0.60 - self.image = bui.imagewidget( - parent=self._root_widget, - size=(size, size), - draw_controller=self.button, - position=(position[0] + 10.5, position[1] + 17), - texture=bui.gettexture('powerupSpeed'), - ) - - self.text = bui.textwidget( - parent=self._root_widget, - position=(position[0] + 25, position[1] + 10), - size=(0, 0), - scale=0.45, - color=(0.7, 0.9, 0.7, 1.0), - draw_controller=self.button, - maxwidth=60, - text=(f"Ultimate Powerup {_sp_}Manager"), - h_align='center', - v_align='center', - ) - - def powerupmanager_window(self): - bui.containerwidget(edit=self._root_widget, transition='out_left') - PowerupManagerWindow() - - -class NewPowerupBoxFactory(pupbox.PowerupBoxFactory): - def __init__(self) -> None: - super().__init__() - self.tex_speed = bs.gettexture('powerupSpeed') - self.tex_health_damage = bs.gettexture('heart') - self.tex_goodbye = bs.gettexture('achievementOnslaught') - self.tex_ice_man = bs.gettexture('ouyaUButton') - self.tex_tank_shield = bs.gettexture('achievementSuperPunch') - self.tex_impairment_bombs = bs.gettexture('levelIcon') - self.tex_fire_bombs = bs.gettexture('ouyaOButton') - self.tex_fly_bombs = bs.gettexture('star') - - self._powerupdist = [] - for powerup, freq in powerup_dist(): - for _i in range(int(freq)): - self._powerupdist.append(powerup) - - def get_random_powerup_type(self, forcetype=None, excludetypes=None): - - try: - self.mapa = bs.getactivity()._map.getname() - except: - self.mapa = None - - speed_banned_maps = ['Hockey Stadium', 'Lake Frigid', 'Happy Thoughts'] - - if self.mapa in speed_banned_maps: - powerup_disable = ['speed'] - else: - powerup_disable = [] - - if excludetypes is None: - excludetypes = [] - if forcetype: - ptype = forcetype - else: - if self._lastpoweruptype == 'curse': - ptype = 'health' - else: - while True: - ptype = self._powerupdist[random.randint(0, len(self._powerupdist) - 1)] - if ptype not in excludetypes and ptype not in powerup_disable: - break - self._lastpoweruptype = ptype - return ptype - - -def fire_effect(self): - if self.node.exists(): - bs.emitfx( - position=self.node.position, scale=3, count=50 * 2, spread=0.3, chunk_type='sweat' - ) - else: - self.fire_effect_time = None - - -# BOMBS -Bomb._pm_old_bomb = Bomb.__init__ - - -def _bomb_init( - self, - position: Sequence[float] = (0.0, 1.0, 0.0), - velocity: Sequence[float] = (0.0, 0.0, 0.0), - bomb_type: str = 'normal', - blast_radius: float = 2.0, - bomb_scale: float = 1.0, - source_player: bs.Player = None, - owner: bs.Node = None, -): - - self.bm_type = bomb_type - new_bomb_type = 'ice' if bomb_type in ['ice_bubble', 'impairment', 'fire', 'fly'] else bomb_type - - # Call original __init__ - self._pm_old_bomb( - position=position, - velocity=velocity, - bomb_type=new_bomb_type, - blast_radius=blast_radius, - bomb_scale=bomb_scale, - source_player=source_player, - owner=owner, - ) - - tex = self.node.color_texture - - if self.bm_type == 'ice_bubble': - self.bomb_type = self.bm_type - self.node.mesh = None - self.shield_ice = bs.newnode( - 'shield', owner=self.node, attrs={'color': (0.5, 1.0, 7.0), 'radius': 0.6} - ) - self.node.connectattr('position', self.shield_ice, 'position') - - elif self.bm_type == 'fire': - self.bomb_type = self.bm_type - self.node.mesh = None - self.shield_fire = bs.newnode( - 'shield', owner=self.node, attrs={'color': (6.5, 6.5, 2.0), 'radius': 0.6} - ) - self.node.connectattr('position', self.shield_fire, 'position') - self.fire_effect_time = bs.Timer(0.1, bs.WeakCallPartial(fire_effect, self), repeat=True) - elif self.bm_type == 'impairment': - self.bomb_type = self.bm_type - tex = bs.gettexture('eggTex3') - elif self.bm_type == 'fly': - self.bomb_type = self.bm_type - tex = bs.gettexture('eggTex1') - - if 'tex' in locals(): - self.node.color_texture = tex - self.hit_subtype = self.bomb_type - - if self.bomb_type == 'ice_bubble': - self.blast_radius *= 1.2 - elif self.bomb_type == 'fly': - self.blast_radius *= 2.2 - - -def bomb_handlemessage(self, msg: Any) -> Any: - assert not self.expired - - if isinstance(msg, bs.DieMessage): - if self.node: - self.node.delete() - - elif isinstance(msg, bomb.ExplodeHitMessage): - node = bs.getcollision().opposingnode - assert self.node - nodepos = self.node.position - mag = 2000.0 - if self.blast_type in ('ice', 'ice_bubble'): - mag *= 0.5 - elif self.blast_type == 'land_mine': - mag *= 2.5 - elif self.blast_type == 'tnt': - mag *= 2.0 - elif self.blast_type == 'fire': - mag *= 0.6 - elif self.blast_type == 'fly': - mag *= 5.5 - - node.handlemessage( - bs.HitMessage( - pos=nodepos, - velocity=(0, 0, 0), - magnitude=mag, - hit_type=self.hit_type, - hit_subtype=self.hit_subtype, - radius=self.radius, - source_player=babase.existing(self._source_player), - ) - ) - if self.blast_type in ('ice', 'ice_bubble'): - bomb.BombFactory.get().freeze_sound.play(10) - node.handlemessage(bs.FreezeMessage()) - - return None - - -def powerup_translated(self, type: str): - powerups_names = { - 'triple_bombs': babase.Lstr(resource='helpWindow.' + 'powerupBombNameText'), - 'ice_bombs': babase.Lstr(resource='helpWindow.' + 'powerupIceBombsNameText'), - 'punch': babase.Lstr(resource='helpWindow.' + 'powerupPunchNameText'), - 'impact_bombs': babase.Lstr(resource='helpWindow.' + 'powerupImpactBombsNameText'), - 'land_mines': babase.Lstr(resource='helpWindow.' + 'powerupLandMinesNameText'), - 'sticky_bombs': babase.Lstr(resource='helpWindow.' + 'powerupStickyBombsNameText'), - 'shield': babase.Lstr(resource='helpWindow.' + 'powerupShieldNameText'), - 'health': babase.Lstr(resource='helpWindow.' + 'powerupHealthNameText'), - 'curse': babase.Lstr(resource='helpWindow.' + 'powerupCurseNameText'), - 'speed': getlanguage('Speed'), - 'health_damage': getlanguage('Healing Damage'), - 'goodbye': getlanguage('Goodbye'), - 'ice_man': getlanguage('Ice Man'), - 'tank_shield': getlanguage('Tank Shield'), - 'impairment_bombs': getlanguage('Impairment Bombs'), - 'fire_bombs': getlanguage('Fire Bombs'), - 'fly_bombs': getlanguage('Fly Bombs'), - } - self.texts['Name'].text = powerups_names[type] - - -# POWERUP -pupbox.PowerupBox._old_pbx_ = pupbox.PowerupBox.__init__ - - -def _pbx_( - self, - position: Sequence[float] = (0.0, 1.0, 0.0), - poweruptype: str = 'triple_bombs', - expire: bool = True, -): - self.news: list = [] - for x, i in powerup_dist(): - self.news.append(x) - - self.box: list = [] - self.texts = {} - self.news = self.news[9:] - self.box.append(poweruptype) - self.npowerup = self.box[0] - factory = NewPowerupBoxFactory.get() - - if self.npowerup in self.news: - new_poweruptype = 'shield' - else: - new_poweruptype = poweruptype - self._old_pbx_(position, new_poweruptype, expire) - - type = new_poweruptype - tex = self.node.color_texture - mesh = self.node.mesh - - if self.npowerup == 'speed': - type = self.npowerup - tex = factory.tex_speed - elif self.npowerup == 'health_damage': - type = self.npowerup - tex = factory.tex_health_damage - elif self.npowerup == 'goodbye': - type = self.npowerup - tex = factory.tex_goodbye - elif self.npowerup == 'ice_man': - type = self.npowerup - tex = factory.tex_ice_man - elif self.npowerup == 'tank_shield': - type = self.npowerup - tex = factory.tex_tank_shield - elif self.npowerup == 'impairment_bombs': - type = self.npowerup - tex = factory.tex_impairment_bombs - elif self.npowerup == 'fire_bombs': - type = self.npowerup - tex = factory.tex_fire_bombs - elif self.npowerup == 'fly_bombs': - type = self.npowerup - tex = factory.tex_fly_bombs - - self.poweruptype = type - self.node.mesh = mesh - self.node.color_texture = tex - n_scale = config['Powerup Scale'] - style = config['Powerup Style'] - - curve = bs.animate(self.node, 'mesh_scale', {0: 0, 0.14: 1.6, 0.2: n_scale}) - bs.timer(0.2, curve.delete) - - def util_text( - type: str, - text: str, - scale: float = 1, - color: list = [1, 1, 1], - position: list = [0, 0.7, 0], - colors_name: bool = False, - ): - m = bs.newnode( - 'math', - owner=self.node, - attrs={'input1': (position[0], position[1], position[2]), 'operation': 'add'}, - ) - self.node.connectattr('position', m, 'input2') - self.texts[type] = bs.newnode( - 'text', - owner=self.node, - attrs={ - 'text': str(text), - 'in_world': True, - 'scale': 0.02, - 'shadow': 0.5, - 'flatness': 1.0, - 'color': (color[0], color[1], color[2]), - 'h_align': 'center', - }, - ) - m.connectattr('output', self.texts[type], 'position') - bs.animate(self.texts[type], 'scale', {0: 0.017, 0.4: 0.017, 0.5: 0.01 * scale}) - - if colors_name: - bs.animate_array( - self.texts[type], - 'color', - 3, - { - 0: (1, 0, 0), - 0.2: (1, 0.5, 0), - 0.4: (1, 1, 0), - 0.6: (0, 1, 0), - 0.8: (0, 1, 1), - 1.0: (1, 0, 1), - 1.2: (1, 0, 0), - }, - loop=True, - ) - - def update_time(time): - if hasattr(self, 'texts') and 'Time' in self.texts and self.texts['Time']: - self.texts['Time'].text = str(time) - - if config['Powerup Time']: - interval = int(pupbox.DEFAULT_POWERUP_INTERVAL) - time2 = interval - 1 - time = 1 - - util_text( - 'Time', time2, scale=1.5, color=(2, 2, 2), position=[0, 0.9, 0], colors_name=False - ) - - while interval + 3: - bs.timer(time - 1, babase.CallPartial(update_time, f'{time2}s')) - - if time2 == 0: - break - - time += 1 - time2 -= 1 - - if config['Powerup With Shield']: - scale = config['Powerup Scale'] - self.shield = bs.newnode( - 'shield', owner=self.node, attrs={'color': (1, 1, 0), 'radius': 1.3 * scale} - ) - self.node.connectattr('position', self.shield, 'position') - bs.animate_array( - self.shield, - 'color', - 3, - {0: (2, 0, 0), 0.5: (0, 2, 0), 1: (0, 1, 6), 1.5: (2, 0, 0)}, - loop=True, - ) - - if config['Powerup Name']: - util_text('Name', self.poweruptype, scale=1.2, position=[0, 0.4, 0], colors_name=True) - powerup_translated(self, self.poweruptype) - - if style == 'SY: BALL': - self.node.mesh = bs.getmesh('frostyPelvis') - elif style == 'SY: Impact': - self.node.mesh = bs.getmesh('impactBomb') - elif style == 'SY: Egg': - self.node.mesh = bs.getmesh('egg') - - -# SPAZ -def _speed_off_flash(self): - if self.node: - factory = NewPowerupBoxFactory.get() - self.node.billboard_texture = factory.tex_speed - self.node.billboard_opacity = 1.0 - self.node.billboard_cross_out = True - - -def _speed_wear_off(self): - if self.node: - self.node.hockey = False - self.node.billboard_opacity = 0.0 - bui.getsound('powerdown01').play() - - -def _ice_man_off_flash(self): - if self.node: - factory = NewPowerupBoxFactory.get() - self.node.billboard_texture = factory.tex_ice_man - self.node.billboard_opacity = 1.0 - self.node.billboard_cross_out = True - - -def _ice_man_wear_off(self): - if self.node: - f = self.color[0] - i = (0, 1, 4) - - bomb = self.bmb_color[0] - if bomb != 'ice_bubble': - self.bomb_type = bomb - else: - self.bomb_type = 'normal' - - self.freeze_punch = False - self.node.billboard_opacity = 0.0 - bs.animate_array(self.node, 'color', 3, {0: f, 0.3: i, 0.6: f}) - bui.getsound('powerdown01').play() - - -Spaz._pm2_spz_old = Spaz.__init__ - - -def _init_spaz_(self, *args, **kwargs): - self._pm2_spz_old(*args, **kwargs) - self.edg_eff = False - self.kill_eff = False - self.freeze_punch = False - self.die = False - self.color: list = [] - self.color.append(self.node.color) - - self.tankshield = {"Tank": False, "Reduction": False, "Shield": None} - - -Spaz._super_on_punch_press = Spaz.on_punch_press - - -def spaz_on_punch_press(self) -> None: - self._super_on_punch_press() - - if self.tankshield['Tank']: - try: - self.tankshield['Reduction'] = True - - shield = bs.newnode( - 'shield', owner=self.node, attrs={'color': (4, 1, 4), 'radius': 1.3} - ) - self.node.connectattr('position_center', shield, 'position') - - self.tankshield['Shield'] = shield - except: - pass - - -Spaz._super_on_punch_release = Spaz.on_punch_release - - -def spaz_on_punch_release(self) -> None: - self._super_on_punch_release() - try: - self.tankshield['Shield'].delete() - self.tankshield['Reduction'] = False - except: - pass - - -def new_get_bomb_type_tex(self) -> babase.Texture: - factory = NewPowerupBoxFactory.get() - if self.bomb_type == 'sticky': - return factory.tex_sticky_bombs - if self.bomb_type == 'ice': - return factory.tex_ice_bombs - if self.bomb_type == 'impact': - return factory.tex_impact_bombs - if self.bomb_type == 'impairment': - return factory.tex_impairment_bombs - if self.bomb_type == 'fire': - return factory.tex_fire_bombs - if self.bomb_type == 'fly': - return factory.tex_fly_bombs - return None - - -def new_handlemessage(self, msg: Any) -> Any: - assert not self.expired - - if isinstance(msg, bs.PickedUpMessage): - if self.node: - self.node.handlemessage('hurt_sound') - self.node.handlemessage('picked_up') - - self._num_times_hit += 1 - - elif isinstance(msg, bs.ShouldShatterMessage): - bs.timer(0.001, bs.WeakCallPartial(self.shatter)) - - elif isinstance(msg, bs.ImpactDamageMessage): - bs.timer(0.001, bs.WeakCallPartial(self._hit_self, msg.intensity)) - elif isinstance(msg, bs.PowerupMessage): - factory = NewPowerupBoxFactory.get() - if self._dead or not self.node: - return True - if self.pick_up_powerup_callback is not None: - self.pick_up_powerup_callback(self) - if msg.poweruptype == 'triple_bombs': - tex = PowerupBoxFactory.get().tex_bomb - self._flash_billboard(tex) - self.set_bomb_count(3) - if self.powerups_expire: - self.node.mini_billboard_1_texture = tex - t_ms = int(bs.time() * 1000) - assert isinstance(t_ms, int) - self.node.mini_billboard_1_start_time = t_ms - self.node.mini_billboard_1_end_time = t_ms + POWERUP_WEAR_OFF_TIME - self._multi_bomb_wear_off_timer = bs.Timer( - (POWERUP_WEAR_OFF_TIME - 2000), - babase.CallPartial(self._multi_bomb_wear_off_flash), - ) - self._multi_bomb_wear_off_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME, babase.CallPartial(self._multi_bomb_wear_off) - ) - elif msg.poweruptype == 'land_mines': - self.set_land_mine_count(min(self.land_mine_count + 3, 3)) - elif msg.poweruptype == 'impact_bombs': - self.bomb_type = 'impact' - tex = self._get_bomb_type_tex() - self._flash_billboard(tex) - if self.powerups_expire: - self.node.mini_billboard_2_texture = tex - t_ms = int(bs.time() * 1000) - assert isinstance(t_ms, int) - self.node.mini_billboard_2_start_time = t_ms - self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME - self._bomb_wear_off_flash_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME - 2000, babase.CallPartial(self._bomb_wear_off_flash) - ) - self._bomb_wear_off_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME, babase.CallPartial(self._bomb_wear_off) - ) - elif msg.poweruptype == 'sticky_bombs': - self.bomb_type = 'sticky' - tex = self._get_bomb_type_tex() - self._flash_billboard(tex) - if self.powerups_expire: - self.node.mini_billboard_2_texture = tex - t_ms = int(bs.time() * 1000) - assert isinstance(t_ms, int) - self.node.mini_billboard_2_start_time = t_ms - self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME - self._bomb_wear_off_flash_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME - 2000, babase.CallPartial(self._bomb_wear_off_flash) - ) - self._bomb_wear_off_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME, babase.CallPartial(self._bomb_wear_off) - ) - elif msg.poweruptype == 'punch': - self._has_boxing_gloves = True - tex = PowerupBoxFactory.get().tex_punch - self._flash_billboard(tex) - self.equip_boxing_gloves() - if self.powerups_expire: - self.node.boxing_gloves_flashing = False - self.node.mini_billboard_3_texture = tex - t_ms = int(bs.time() * 1000) - assert isinstance(t_ms, int) - self.node.mini_billboard_3_start_time = t_ms - self.node.mini_billboard_3_end_time = t_ms + POWERUP_WEAR_OFF_TIME - self._boxing_gloves_wear_off_flash_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME - 2000, bs.WeakCallPartial(self._gloves_wear_off_flash) - ) - self._boxing_gloves_wear_off_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME, - bs.WeakCallPartial(self._gloves_wear_off), - ) - elif msg.poweruptype == 'shield': - factory = SpazFactory.get() - self.equip_shields(decay=factory.shield_decay_rate > 0) - elif msg.poweruptype == 'curse': - self.curse() - elif msg.poweruptype == 'ice_bombs': - self.bomb_type = 'ice' - tex = self._get_bomb_type_tex() - self._flash_billboard(tex) - if self.powerups_expire: - self.node.mini_billboard_2_texture = tex - t_ms = bs.time() * 1000 - assert isinstance(t_ms, int) - self.node.mini_billboard_2_start_time = t_ms - self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME - self._bomb_wear_off_flash_timer = bs.Timer( - (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, - bs.WeakCallPartial(self._bomb_wear_off_flash), - ) - - self._bomb_wear_off_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) - ) - - elif msg.poweruptype == 'health': - if self.edg_eff: - f = self.color[0] - r = (2, 0, 0) - g = (0, 2, 0) - bs.animate_array(self.node, 'color', 3, {0: r, 0.6: g, 1.0: f}) - self.edg_eff = False - if self._cursed: - self._cursed = False - factory = SpazFactory.get() - for attr in ['materials', 'roller_materials']: - materials = getattr(self.node, attr) - if factory.curse_material in materials: - setattr( - self.node, - attr, - tuple(m for m in materials if m != factory.curse_material), - ) - self.node.curse_death_time = 0 - self.hitpoints = self.hitpoints_max - self._flash_billboard(PowerupBoxFactory.get().tex_health) - self.node.hurt = 0 - self._last_hit_time = None - self._num_times_hit = 0 - - elif msg.poweruptype == 'tank_shield': - self.tankshield['Tank'] = True - self.edg_eff = False - tex = factory.tex_tank_shield - self._flash_billboard(tex) - - elif msg.poweruptype == 'health_damage': - tex = factory.tex_health_damage - self.edg_eff = True - f = self.color[0] - i = (2, 0.5, 2) - bs.animate_array(self.node, 'color', 3, {0: i, 0.5: i, 0.6: f}) - self._flash_billboard(tex) - self.tankshield['Tank'] = False - self.freeze_punch = False - - elif msg.poweruptype == 'goodbye': - tex = factory.tex_goodbye - self._flash_billboard(tex) - self.kill_eff = True - - elif msg.poweruptype == 'fly_bombs': - self.bomb_type = 'fly' - tex = self._get_bomb_type_tex() - self._flash_billboard(tex) - if self.powerups_expire: - self.node.mini_billboard_2_texture = tex - t_ms = bs.time() * 1000 - assert isinstance(t_ms, int) - self.node.mini_billboard_2_start_time = t_ms - self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME - self._bomb_wear_off_flash_timer = bs.Timer( - (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, - bs.WeakCallStrict(self._bomb_wear_off_flash), - ) - - self._bomb_wear_off_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) - ) - - elif msg.poweruptype == 'fire_bombs': - self.bomb_type = 'fire' - tex = self._get_bomb_type_tex() - self._flash_billboard(tex) - if self.powerups_expire: - self.node.mini_billboard_2_texture = tex - t_ms = bs.time() * 1000 - assert isinstance(t_ms, int) - self.node.mini_billboard_2_start_time = t_ms - self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME - self._bomb_wear_off_flash_timer = bs.Timer( - (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, - bs.WeakCallStrict(self._bomb_wear_off_flash), - ) - - self._bomb_wear_off_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) - ) - - elif msg.poweruptype == 'impairment_bombs': - self.bomb_type = 'impairment' - tex = self._get_bomb_type_tex() - self._flash_billboard(tex) - if self.powerups_expire: - self.node.mini_billboard_2_texture = tex - t_ms = bs.time() * 1000 - assert isinstance(t_ms, int) - self.node.mini_billboard_2_start_time = t_ms - self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME - self._bomb_wear_off_flash_timer = bs.Timer( - (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, - bs.WeakCallStrict(self._bomb_wear_off_flash), - ) - - self._bomb_wear_off_timer = bs.Timer( - POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) - ) - - elif msg.poweruptype == 'ice_man': - tex = factory.tex_ice_man - self.bomb_type = 'ice_bubble' - self.freeze_punch = True - self.edg_eff = False - self.node.color = (0, 1, 4) - self._flash_billboard(tex) - - if self.powerups_expire: - ice_man_time = 17000 - self.node.mini_billboard_2_texture = tex - t_ms = bs.time() * 1000 - assert isinstance(t_ms, int) - self.node.mini_billboard_2_start_time = t_ms - self.node.mini_billboard_2_end_time = t_ms + ice_man_time - - self.ice_man_flash_timer = bs.Timer( - (ice_man_time - 2000) / 1000.0, babase.CallPartial(_ice_man_off_flash, self) - ) - - self.ice_man_timer = bs.Timer( - ice_man_time / 1000.0, babase.CallPartial(_ice_man_wear_off, self) - ) - - elif msg.poweruptype == 'speed': - self.node.hockey = True - tex = factory.tex_speed - self._flash_billboard(tex) - if self.powerups_expire: - speed_time = 15000 - self.node.mini_billboard_2_texture = tex - t_ms = bs.time() * 1000 - assert isinstance(t_ms, int) - self.node.mini_billboard_2_start_time = t_ms - self.node.mini_billboard_2_end_time = t_ms + speed_time - - self.speed_flash_timer = bs.Timer( - (speed_time - 2000) / 1000.0, babase.Call(_speed_off_flash, self) - ) - - self.speed_timer = bs.Timer( - speed_time / 1000.0, bs.WeakCallPartial(_speed_wear_off, self) - ) - - self.bmb_color: list = [] - self.bmb_color.append(self.bomb_type) - - self.node.handlemessage('flash') - if msg.sourcenode: - msg.sourcenode.handlemessage(bs.PowerupAcceptMessage()) - return True - - elif isinstance(msg, bs.FreezeMessage): - if not self.node: - return None - if self.node.invincible: - SpazFactory.get().block_sound.play(1.0, self.node.position) - return None - if self.shield: - return None - if not self.frozen: - self.frozen = True - self.node.frozen = True - bs.timer(5.0, bs.WeakCallPartial(self.handlemessage, bs.ThawMessage())) - if self.hitpoints <= 0: - self.shatter() - if self.freeze_punch: - self.handlemessage(bs.ThawMessage()) - - elif isinstance(msg, bs.ThawMessage): - if self.frozen and not self.shattered and self.node: - self.frozen = False - self.node.frozen = False - - elif isinstance(msg, bs.HitMessage): - if not self.node: - return None - if self.node.invincible: - SpazFactory.get().block_sound.play(1.0, self.node.position) - return True - - local_time = bs.time() * 1000 - assert isinstance(local_time, int) - if self._last_hit_time is None or local_time - self._last_hit_time > 1000: - self._num_times_hit += 1 - self._last_hit_time = local_time - - mag = msg.magnitude * self.impact_scale - velocity_mag = msg.velocity_magnitude * self.impact_scale - damage_scale = 0.22 - - def fire_effect(): - if not self.shield: - if self.node.exists(): - bs.emitfx( - position=self.node.position, - scale=3, - count=50 * 2, - spread=0.3, - chunk_type='sweat', - ) - self.node.handlemessage('celebrate', 560) - else: - self._fire_time = None - else: - self._fire_time = None - - def fire(time, damage): - if not self.shield and not self._dead: - self.hitpoints -= damage - bs.show_damage_count(f'-{damage}HP', self.node.position, msg.force_direction) - bui.getsound('fuse01').play() - - if duration != time: - self._fire_time = bs.Timer(0.1, babase.CallPartial(fire_effect), repeat=True) - else: - self._fire_time = None - - if self.hitpoints < 0: - self.node.handlemessage(bs.DieMessage()) - - if msg.hit_subtype == 'fly': - damage_scale = 0.0 - - if self.shield: - self.shield_hitpoints -= 300 - - if self.shield_hitpoints < 0: - self.shield.delete() - self.shield = None - SpazFactory.get().shield_down_sound.play(1.0, self.node.position) - elif msg.hit_subtype == 'fire': - index = 1 - duration = 5 - damage = 103 - if not self.shield: - for firex in range(duration): - bs.timer(index, bs.WeakCallPartial(fire, index, damage)) - self._fire_time = bs.Timer(0.1, babase.CallPartial(fire_effect), repeat=True) - index += 1 - else: - self.shield_hitpoints -= 80 - if self.shield_hitpoints < 1: - self.shield.delete() - self.shield = None - SpazFactory.get().shield_down_sound.play(1.0, self.node.position) - elif msg.hit_subtype == 'impairment': - damage_scale = 0 - - if self.shield: - self.shield.delete() - self.shield = None - SpazFactory.get().shield_down_sound.play(1.0, self.node.position) - else: - hitpoints = int(self.hitpoints * 0.80) - self.hitpoints -= int(hitpoints) - bs.show_damage_count( - (f'-{int(hitpoints / 10)}%'), self.node.position, msg.force_direction - ) - - if self.hitpoints < 0 or hitpoints < 95: - self.node.handlemessage(bs.DieMessage()) - - if self.shield: - if msg.flat_damage: - damage = msg.flat_damage * self.impact_scale - else: - assert msg.force_direction is not None - self.node.handlemessage( - 'impulse', - msg.pos[0], - msg.pos[1], - msg.pos[2], - msg.velocity[0], - msg.velocity[1], - msg.velocity[2], - mag, - velocity_mag, - msg.radius, - 1, - msg.force_direction[0], - msg.force_direction[1], - msg.force_direction[2], - ) - damage = damage_scale * self.node.damage - - assert self.shield_hitpoints is not None - self.shield_hitpoints -= int(damage) - self.shield.hurt = 1.0 - float(self.shield_hitpoints) / self.shield_hitpoints_max - - max_spillover = SpazFactory.get().max_shield_spillover_damage - if self.shield_hitpoints <= 0: - - self.shield.delete() - self.shield = None - SpazFactory.get().shield_down_sound.play(1.0, self.node.position) - - npos = self.node.position - bs.emitfx( - position=(npos[0], npos[1] + 0.9, npos[2]), - velocity=self.node.velocity, - count=random.randrange(20, 30), - scale=1.0, - spread=0.6, - chunk_type='spark', - ) - - else: - SpazFactory.get().shield_hit_sound.play(0.5, self.node.position) - - assert msg.force_direction is not None - bs.emitfx( - position=msg.pos, - velocity=( - msg.force_direction[0] * 1.0, - msg.force_direction[1] * 1.0, - msg.force_direction[2] * 1.0, - ), - count=min(30, 5 + int(damage * 0.005)), - scale=0.5, - spread=0.3, - chunk_type='spark', - ) - - if self.shield_hitpoints <= -max_spillover: - leftover_damage = -max_spillover - self.shield_hitpoints - shield_leftover_ratio = leftover_damage / damage - - mag *= shield_leftover_ratio - velocity_mag *= shield_leftover_ratio - else: - return True - else: - shield_leftover_ratio = 1.0 - - if msg.flat_damage: - damage = int(msg.flat_damage * self.impact_scale * shield_leftover_ratio) - else: - assert msg.force_direction is not None - self.node.handlemessage( - 'impulse', - msg.pos[0], - msg.pos[1], - msg.pos[2], - msg.velocity[0], - msg.velocity[1], - msg.velocity[2], - mag, - velocity_mag, - msg.radius, - 0, - msg.force_direction[0], - msg.force_direction[1], - msg.force_direction[2], - ) - - damage = int(damage_scale * self.node.damage) - - if self.tankshield['Reduction']: - porcentaje = percentage_tank_shield() - dism = int(damage * porcentaje) - damage = int(damage - dism) - - bs.show_damage_count('-' + str(int(damage / 10)) + '%', msg.pos, msg.force_direction) - - self.node.handlemessage('hurt_sound') - - if self.edg_eff: - porcentaje = percentage_health_damage() - dmg_dism = int(damage * porcentaje) - self.hitpoints += dmg_dism - - PopupText( - text=f'+{int(dmg_dism / 10)}%', - scale=1.5, - position=self.node.position, - color=(0, 1, 0), - ).autoretain() - bs.animate_array( - self.node, 'color', 3, {0: (0, 1, 0), 0.39: (0, 2, 0), 0.4: self.color[0]} - ) - bui.getsound('healthPowerup').play() - - if msg.hit_type == 'punch': - self.on_punched(damage) - - try: - if msg.get_source_player(bs.Player).actor.freeze_punch: - self.node.color = (0, 1, 4) - bui.getsound('freeze').play() - self.node.handlemessage(bs.FreezeMessage()) - except: - pass - - if damage > 350: - assert msg.force_direction is not None - bs.show_damage_count( - '-' + str(int(damage / 10)) + '%', msg.pos, msg.force_direction - ) - - if msg.hit_subtype == 'super_punch': - SpazFactory.get().punch_sound_stronger.play(1.0, self.node.position) - if damage > 500: - sounds = SpazFactory.get().punch_sound_strong - sound = sounds[random.randrange(len(sounds))] - else: - sound = SpazFactory.get().punch_sound - sound.play(1.0, self.node.position) - - assert msg.force_direction is not None - bs.emitfx( - position=msg.pos, - velocity=( - msg.force_direction[0] * 0.5, - msg.force_direction[1] * 0.5, - msg.force_direction[2] * 0.5, - ), - count=min(10, 1 + int(damage * 0.0025)), - scale=0.3, - spread=0.03, - ) - - bs.emitfx( - position=msg.pos, - chunk_type='sweat', - velocity=( - msg.force_direction[0] * 1.3, - msg.force_direction[1] * 1.3 + 5.0, - msg.force_direction[2] * 1.3, - ), - count=min(30, 1 + int(damage * 0.04)), - scale=0.9, - spread=0.28, - ) - - hurtiness = damage * 0.003 - punchpos = ( - msg.pos[0] + msg.force_direction[0] * 0.02, - msg.pos[1] + msg.force_direction[1] * 0.02, - msg.pos[2] + msg.force_direction[2] * 0.02, - ) - flash_color = (1.0, 0.8, 0.4) - light = bs.newnode( - 'light', - attrs={ - 'position': punchpos, - 'radius': 0.12 + hurtiness * 0.12, - 'intensity': 0.3 * (1.0 + 1.0 * hurtiness), - 'height_attenuated': False, - 'color': flash_color, - }, - ) - bs.timer(0.06, light.delete) - - flash = bs.newnode( - 'flash', - attrs={'position': punchpos, 'size': 0.17 + 0.17 * hurtiness, 'color': flash_color}, - ) - bs.timer(0.06, flash.delete) - - if msg.hit_type == 'impact': - assert msg.force_direction is not None - bs.emitfx( - position=msg.pos, - velocity=( - msg.force_direction[0] * 2.0, - msg.force_direction[1] * 2.0, - msg.force_direction[2] * 2.0, - ), - count=min(10, 1 + int(damage * 0.01)), - scale=0.4, - spread=0.1, - ) - if self.hitpoints > 0: - if msg.hit_type == 'impact' and damage > self.hitpoints: - newdamage = max(damage - 200, self.hitpoints - 10) - damage = newdamage - self.node.handlemessage('flash') - - if damage > 0.0 and self.node.hold_node: - self.node.hold_node = None - self.hitpoints -= damage - self.node.hurt = 1.0 - float(self.hitpoints) / self.hitpoints_max - - if self._cursed and damage > 0: - bs.timer( - 0.05, bs.WeakCallPartial(self.curse_explode, msg.get_source_player(bs.Player)) - ) - - if self.frozen and (damage > 200 or self.hitpoints <= 0): - self.shatter() - elif self.hitpoints <= 0: - self.node.handlemessage(bs.DieMessage(how=bs.DeathType.IMPACT)) - - if self.hitpoints <= 0: - damage_avg = self.node.damage_smoothed * damage_scale - if damage_avg > 1000: - self.shatter() - - elif isinstance(msg, BombDiedMessage): - self.bomb_count += 1 - - elif isinstance(msg, bs.DieMessage): - - def drop_bomb(): - for xbomb in range(3): - p = self.node.position - pos = (p[0] + xbomb, p[1] + 5, p[2] - xbomb) - ball = bomb.Bomb(position=pos, bomb_type='impact').autoretain() - ball.node.mesh_scale = 0.6 - ball.node.mesh = bs.getmesh('egg') - ball.node.gravity_scale = 2 - - if self.edg_eff: - self.edg_eff = False - - wasdead = self._dead - self._dead = True - self.hitpoints = 0 - if msg.immediate: - if self.node: - self.node.delete() - elif self.node: - self.node.hurt = 1.0 - if self.play_big_death_sound and not wasdead: - SpazFactory.get().single_player_death_sound.play() - self.node.dead = True - bs.timer(2.0, self.node.delete) - - t = 0 - if self.kill_eff: - for bombs in range(3): - bs.timer(t, babase.CallPartial(drop_bomb)) - t += 0.15 - self.kill_eff = False - - elif isinstance(msg, bs.OutOfBoundsMessage): - self.handlemessage(bs.DieMessage(how=bs.DeathType.FALL)) - - elif isinstance(msg, bs.StandMessage): - self._last_stand_pos = (msg.position[0], msg.position[1], msg.position[2]) - if self.node: - self.node.handlemessage( - 'stand', msg.position[0], msg.position[1], msg.position[2], msg.angle - ) - - elif isinstance(msg, CurseExplodeMessage): - self.curse_explode() - - elif isinstance(msg, PunchHitMessage): - if not self.node: - return None - node = bs.getcollision().opposingnode - - if node and (node not in self._punched_nodes): - - punch_momentum_angular = self.node.punch_momentum_angular * self._punch_power_scale - punch_power = self.node.punch_power * self._punch_power_scale - - if node.getnodetype() != 'spaz': - sounds = SpazFactory.get().impact_sounds_medium - sound = sounds[random.randrange(len(sounds))] - sound.play(1.0, self.node.position) - - ppos = self.node.punch_position - punchdir = self.node.punch_velocity - vel = self.node.punch_momentum_linear - - self._punched_nodes.add(node) - node.handlemessage( - bs.HitMessage( - pos=ppos, - velocity=vel, - magnitude=punch_power * punch_momentum_angular * 110.0, - velocity_magnitude=punch_power * 40, - radius=0, - srcnode=self.node, - source_player=self.source_player, - force_direction=punchdir, - hit_type='punch', - hit_subtype=('super_punch' if self._has_boxing_gloves else 'default'), - ) - ) - - mag = -400.0 - if self._hockey: - mag *= 0.5 - if len(self._punched_nodes) == 1: - self.node.handlemessage( - 'kick_back', - ppos[0], - ppos[1], - ppos[2], - punchdir[0], - punchdir[1], - punchdir[2], - mag, - ) - elif isinstance(msg, PickupMessage): - if not self.node: - return None - - try: - collision = bs.getcollision() - opposingnode = collision.opposingnode - opposingbody = collision.opposingbody - except bs.NotFoundError: - return True - - try: - if opposingnode.invincible: - return True - except Exception: - pass - - if ( - opposingnode.getnodetype() == 'spaz' - and not opposingnode.shattered - and opposingbody == 4 - ): - opposingbody = 1 - - held = self.node.hold_node - if held and held.getnodetype() == 'flag': - return True - - self.node.hold_body = opposingbody - self.node.hold_node = opposingnode - elif isinstance(msg, bs.CelebrateMessage): - if self.node: - self.node.handlemessage('celebrate', int(msg.duration * 1000)) - - return None - - -class PowerupManagerWindow(PopupWindow): - def __init__(self, transition='in_right'): - columns = 2 - self._width = width = 800 - self._height = height = 500 - self._sub_height = 200 - self._scroll_width = self._width * 0.90 - self._scroll_height = self._height - 180 - self._sub_width = self._scroll_width * 0.95 - self.tab_buttons: set = {} - self.list_cls_power: list = [] - self.default_powerups = default_powerups() - self.default_power_list = list(self.default_powerups) - self.coins = apg['Bear Coin'] - self.popup_cls_power = None - - if not STORE['Buy Firebombs']: - powerups['Fire Bombs'] = 0 - self.default_power_list.remove('Fire Bombs') - - self.charstr = [ - babase.charstr(babase.SpecialChar.LEFT_ARROW), - babase.charstr(babase.SpecialChar.RIGHT_ARROW), - babase.charstr(babase.SpecialChar.UP_ARROW), - babase.charstr(babase.SpecialChar.DOWN_ARROW), - ] - - self.tabdefs = { - "Action 1": ['powerupIceBombs', (1, 1, 1)], - "Action 2": ['settingsIcon', (0, 1, 0)], - "Action 3": ['inventoryIcon', (1, 1, 1)], - "Action 4": ['storeIcon', (1, 1, 1)], - "Action 5": ['advancedIcon', (1, 1, 1)], - "About": ['heart', (1.5, 0.3, 0.3)], - } - - if STORE['Buy Firebombs'] and STORE['Buy Option'] and STORE['Buy Percentage']: - self.tabdefs = { - "Action 1": ['powerupIceBombs', (1, 1, 1)], - "Action 2": ['settingsIcon', (0, 1, 0)], - "Action 3": ['inventoryIcon', (1, 1, 1)], - "About": ['heart', (1.5, 0.3, 0.3)], - } - - self.listdef = list(self.tabdefs) - - self.count = len(self.tabdefs) - - self._current_tab = GLOBAL['Tab'] - - app = bui.app.ui_v1 - uiscale = app.uiscale - - self._root_widget = bui.containerwidget( - size=(width + 90, height + 80), - transition=transition, - scale=1.5 if uiscale is babase.UIScale.SMALL else 1.0, - stack_offset=(0, -30) if uiscale is babase.UIScale.SMALL else (0, 0), - ) - - self._backButton = b = bui.buttonwidget( - parent=self._root_widget, - autoselect=True, - position=(60, self._height - 15), - size=(130, 60), - scale=0.8, - text_scale=1.2, - label=babase.Lstr(resource='backText'), - button_type='back', - on_activate_call=babase.CallPartial(self._back), - ) - bui.buttonwidget( - edit=self._backButton, - button_type='backSmall', - size=(60, 60), - label=babase.charstr(babase.SpecialChar.BACK), - ) - bui.containerwidget(edit=self._root_widget, cancel_button=b) - - self.titletext = bui.textwidget( - parent=self._root_widget, - position=(0, height - 15), - size=(width, 50), - h_align="center", - color=bui.app.ui_v1.title_color, - v_align="center", - maxwidth=width * 1.3, - ) - - index = 0 - for tab in range(self.count): - for tab2 in range(columns): - - tag = self.listdef[index] - - position = (620 + (tab2 * 120), self._height - 50 * 2.5 - (tab * 120)) - - if tag == 'About': - text = babase.Lstr(resource='gatherWindow.aboutText') - elif tab == 'Action 4': - text = babase.Lstr(resource='storeText') - else: - text = getlanguage(tag) - - self.tab_buttons[tag] = bui.buttonwidget( - parent=self._root_widget, - autoselect=True, - position=position, - size=(110, 110), - scale=1, - label='', - enable_sound=False, - button_type='square', - on_activate_call=babase.CallPartial(self._set_tab, tag, sound=True), - ) - - self.text = bui.textwidget( - parent=self._root_widget, - position=(position[0] + 55, position[1] + 30), - size=(0, 0), - scale=1, - color=bui.app.ui_v1.title_color, - draw_controller=self.tab_buttons[tag], - maxwidth=100, - text=text, - h_align='center', - v_align='center', - ) - - self.image = bui.imagewidget( - parent=self._root_widget, - size=(60, 60), - color=self.tabdefs[tag][1], - draw_controller=self.tab_buttons[tag], - position=(position[0] + 25, position[1] + 40), - texture=bui.gettexture(self.tabdefs[tag][0]), - ) - - index += 1 - - if self.count == index: - break - - if self.count == index: - break - - self._scrollwidget = None - self._tab_container = None - self._set_tab(self._current_tab) - - def __del__(self): - apg.apply_and_commit() - - def popup_menu_closing(self, window): - print("saliendo") - - def _set_tab(self, tab, sound: bool = False): - self.sound = sound - GLOBAL['Tab'] = tab - apg.apply_and_commit() - - if self._tab_container is not None and self._tab_container.exists(): - self._tab_container.delete() - - if self.sound: - bui.getsound('swish').play() - - if self._scrollwidget: - self._scrollwidget.delete() - - self._scrollwidget = bui.scrollwidget( - parent=self._root_widget, - position=(self._width * 0.08, 51 * 1.8), - size=(self._sub_width - 140, self._scroll_height + 60 * 1.2), - ) - - if tab == 'Action 4': - if self._scrollwidget: - self._scrollwidget.delete() - - self._scrollwidget = bui.scrollwidget( - parent=self._root_widget, - position=(self._width * 0.08, 51 * 1.8), - size=(self._sub_width - 140, self._scroll_height + 60 * 1.2), - capture_arrows=True, - center_small_content=False, - selection_loops_to_parent=True, - claims_left_right=True, - claims_up_down=False, - color=(0.3, 0.3, 0.4), - ) - bui.textwidget(edit=self.titletext, text=babase.Lstr(resource='storeText')) - - elif tab == 'About': - bui.textwidget(edit=self.titletext, text=babase.Lstr(resource='gatherWindow.aboutText')) - else: - bui.textwidget(edit=self.titletext, text=getlanguage(tab)) - - choices = ['Reset', 'Only Bombs', 'Only Items', 'New', 'Nothing'] - c_display = [] - - for display in choices: - choices_display = babase.Lstr(translate=("", getlanguage(display))) - c_display.append(choices_display) - - if tab == 'Action 1': - self.popup_cls_power = PopupMenu( - parent=self._root_widget, - position=(130, self._width * 0.61), - button_size=(150, 50), - scale=2.5, - choices=choices, - width=150, - choices_display=c_display, - current_choice=GLOBAL['Cls Powerup'], - on_value_change_call=self._set_concept, - ) - self.list_cls_power.append(self.popup_cls_power._button) - - self.button_cls_power = bui.buttonwidget( - parent=self._root_widget, - position=(500, self._width * 0.61), - size=(50, 50), - autoselect=True, - scale=1, - label=('%'), - text_scale=1, - button_type='square', - on_activate_call=self._percentage_window, - ) - self.list_cls_power.append(self.button_cls_power) - - rewindow = [self.popup_cls_power._button, self.button_cls_power] - - for ( - cls - ) in self.list_cls_power: # this is very important so that pupups don't accumulate - if cls not in rewindow: - cls.delete() - - elif tab == 'Action 4': - self.button_coin = bui.buttonwidget( - parent=self._root_widget, - icon=bui.gettexture('coin'), - position=(550, self._width * 0.614), - size=(160, 40), - textcolor=(0, 1, 0), - color=(0, 1, 6), - scale=1, - label=str(apg['Bear Coin']), - text_scale=1, - autoselect=True, - on_activate_call=None, - ) # self._percentage_window) - self.list_cls_power.append(self.button_coin) - - try: - rewindow.append(self.button_coin) - except: - rewindow = [self.button_coin] - for ( - cls - ) in self.list_cls_power: # this is very important so that pupups don't accumulate - if cls not in rewindow: - cls.delete() - - else: - try: - for cls in self.list_cls_power: - cls.delete() - except: - pass - - if tab == 'Action 1': - sub_height = len(self.default_power_list) * 90 - v = sub_height - 55 - width = 300 - posi = 0 - id_power = list(self.default_powerups) - new_powerups = id_power[9:] - self.listpower = {} - - self._tab_container = c = bui.containerwidget( - parent=self._scrollwidget, - size=(self._sub_width, sub_height), - background=False, - selection_loops_to_parent=True, - ) - - for power in self.default_power_list: - if power == id_power[0]: - text = 'helpWindow.powerupShieldNameText' - tex = bui.gettexture('powerupShield') - elif power == id_power[1]: - text = 'helpWindow.powerupPunchNameText' - tex = bui.gettexture('powerupPunch') - elif power == id_power[2]: - text = 'helpWindow.powerupLandMinesNameText' - tex = bui.gettexture('powerupLandMines') - elif power == id_power[3]: - text = 'helpWindow.powerupImpactBombsNameText' - tex = bui.gettexture('powerupImpactBombs') - elif power == id_power[4]: - text = 'helpWindow.powerupIceBombsNameText' - tex = bui.gettexture('powerupIceBombs') - elif power == id_power[5]: - text = 'helpWindow.powerupBombNameText' - tex = bui.gettexture('powerupBomb') - elif power == id_power[6]: - text = 'helpWindow.powerupStickyBombsNameText' - tex = bui.gettexture('powerupStickyBombs') - elif power == id_power[7]: - text = 'helpWindow.powerupCurseNameText' - tex = bui.gettexture('powerupCurse') - elif power == id_power[8]: - text = 'helpWindow.powerupHealthNameText' - tex = bui.gettexture('powerupHealth') - elif power == id_power[9]: - text = power - tex = bui.gettexture('powerupSpeed') - elif power == id_power[10]: - text = power - tex = bui.gettexture('heart') - elif power == id_power[11]: - text = "Goodbye!" - tex = bui.gettexture('achievementOnslaught') - elif power == id_power[12]: - text = power - tex = bui.gettexture('ouyaUButton') - elif power == id_power[13]: - text = power - tex = bui.gettexture('achievementSuperPunch') - elif power == id_power[14]: - text = power - tex = bui.gettexture('levelIcon') - elif power == id_power[15]: - text = power - tex = bui.gettexture('ouyaOButton') - elif power == id_power[16]: - text = power - tex = bui.gettexture('star') - - if power in new_powerups: - label = getlanguage(power) - else: - label = babase.Lstr(resource=text) - - apperance = powerups[power] - position = (90, v - posi) - - t = bui.textwidget( - parent=c, - position=(position[0] - 30, position[1] - 15), - size=(width, 50), - h_align="center", - color=(bui.app.ui_v1.title_color), - text=label, - v_align="center", - maxwidth=width * 1.3, - ) - - self.powprev = bui.imagewidget( - parent=c, - position=(position[0] - 70, position[1] - 10), - size=(50, 50), - texture=tex, - ) - - dipos = 0 - for direc in ['-', '+']: - bui.buttonwidget( - parent=c, - autoselect=True, - position=(position[0] + 270 + dipos, position[1] - 10), - size=(100, 100), - scale=0.4, - label=direc, - button_type='square', - text_scale=4, - on_activate_call=babase.CallPartial(self.apperance_powerups, power, direc), - ) - - dipos += 100 - - textwidget = bui.textwidget( - parent=c, - position=(position[0] + 190, position[1] - 15), - size=(width, 50), - h_align="center", - color=cls_pow_color()[apperance], - text=str(apperance), - v_align="center", - maxwidth=width * 1.3, - ) - self.listpower[power] = textwidget - - posi += 90 - - elif tab == 'Action 2': - sub_height = 370 if not STORE['Buy Option'] else 450 - v = sub_height - 55 - width = 300 - - self._tab_container = c = bui.containerwidget( - parent=self._scrollwidget, - size=(self._sub_width, sub_height), - background=False, - selection_loops_to_parent=True, - ) - - position = (40, v - 20) - - c_display = [] - choices = ['Auto', 'SY: BALL', 'SY: Impact', 'SY: Egg'] - for display in choices: - choices_display = babase.Lstr(translate=("", getlanguage(display))) - c_display.append(choices_display) - - popup = PopupMenu( - parent=c, - position=(position[0] + 300, position[1]), - button_size=(150, 50), - scale=2.5, - choices=choices, - width=150, - choices_display=c_display, - current_choice=config['Powerup Style'], - on_value_change_call=babase.CallPartial(self._all_popup, 'Powerup Style'), - ) - - text = getlanguage('Powerup Style') - wt = len(text) * 0.80 - t = bui.textwidget( - parent=c, - position=(position[0] - 60 + wt, position[1]), - size=(width, 50), - maxwidth=width * 0.9, - scale=1.1, - h_align="center", - color=bui.app.ui_v1.title_color, - text=getlanguage('Powerup Style'), - v_align="center", - ) - - dipos = 0 - for direc in ['-', '+']: - bui.buttonwidget( - parent=c, - autoselect=True, - position=(position[0] + 310 + dipos, position[1] - 100), - size=(100, 100), - repeat=True, - scale=0.4, - label=direc, - button_type='square', - text_scale=4, - on_activate_call=babase.CallPartial(self._powerups_scale, direc), - ) - dipos += 100 - - txt_scale = config['Powerup Scale'] - self.txt_scale = bui.textwidget( - parent=c, - position=(position[0] + 230, position[1] - 105), - size=(width, 50), - scale=1.1, - h_align="center", - color=(0, 1, 0), - text=str(txt_scale), - v_align="center", - maxwidth=width * 1.3, - ) - - text = getlanguage('Powerup Scale') - wt = len(text) * 0.80 - t = bui.textwidget( - parent=c, - position=(position[0] - 60 + wt, position[1] - 100), - size=(width, 50), - maxwidth=width * 0.9, - scale=1.1, - h_align="center", - color=bui.app.ui_v1.title_color, - text=text, - v_align="center", - ) - - position = (position[0] - 20, position[1] + 40) - - self.check = bui.checkboxwidget( - parent=c, - position=(position[0] + 30, position[1] - 230), - value=config['Powerup Name'], - on_value_change_call=babase.CallPartial(self._switches, 'Powerup Name'), - maxwidth=self._scroll_width * 0.9, - text=getlanguage('Powerup Name'), - autoselect=True, - ) - - self.check = bui.checkboxwidget( - parent=c, - position=(position[0] + 30, position[1] - 230 * 1.3), - value=config['Powerup With Shield'], - on_value_change_call=babase.CallPartial(self._switches, 'Powerup With Shield'), - maxwidth=self._scroll_width * 0.9, - text=getlanguage('Powerup With Shield'), - autoselect=True, - ) - - if STORE['Buy Option']: - self.check = bui.checkboxwidget( - parent=c, - position=(position[0] + 30, position[1] - 230 * 1.6), - value=config['Powerup Time'], - on_value_change_call=babase.CallPartial(self._switches, 'Powerup Time'), - maxwidth=self._scroll_width * 0.9, - text=getlanguage('Powerup Time'), - autoselect=True, - ) - - elif tab == 'Action 3': - sub_height = 300 - v = sub_height - 55 - width = 300 - - self._tab_container = c = bui.containerwidget( - parent=self._scrollwidget, - size=(self._sub_width, sub_height), - background=False, - selection_loops_to_parent=True, - ) - - v -= 20 - position = (110, v - 45 * 1.72) - - if not STORE['Buy Percentage']: - t = bui.textwidget( - parent=c, - position=(90, v - 100), - size=(30 + width, 50), - h_align="center", - text=getlanguage('Block Option Store'), - color=bui.app.ui_v1.title_color, - v_align="center", - maxwidth=width * 1.5, - scale=1.5, - ) - - i = bui.imagewidget( - parent=c, - position=(position[0] + 100, position[1] - 205), - size=(80, 80), - texture=bui.gettexture('lock'), - ) - else: - t = bui.textwidget( - parent=c, - position=(position[0] - 14, position[1] + 70), - size=(30 + width, 50), - h_align="center", - text=f"{getlanguage('Tank Shield PTG')} ({getlanguage('Tank Shield')})", - color=bui.app.ui_v1.title_color, - v_align="center", - maxwidth=width * 1.5, - scale=1.5, - ) - - b = bui.buttonwidget( - parent=c, - autoselect=True, - position=position, - size=(100, 100), - repeat=True, - scale=0.6, - label=self.charstr[3], - button_type='square', - text_scale=2, - on_activate_call=babase.CallPartial(self.tank_shield_percentage, 'Decrement'), - ) - - b = bui.buttonwidget( - parent=c, - autoselect=True, - repeat=True, - text_scale=2, - position=(position[0] * 3.2, position[1]), - size=(100, 100), - scale=0.6, - label=self.charstr[2], - button_type='square', - on_activate_call=babase.CallPartial(self.tank_shield_percentage, 'Increment'), - ) - - porcentaje = config['Tank Shield PTG'] - if porcentaje > 59: - color = (0, 1, 0) - elif porcentaje < 40: - color = (1, 1, 0) - else: - color = (0, 1, 0.8) - - self.tank_text = bui.textwidget( - parent=c, - position=(position[0] - 14, position[1] + 5), - size=(30 + width, 50), - h_align="center", - text=str(porcentaje) + '%', - color=color, - v_align="center", - maxwidth=width * 1.3, - scale=2, - ) - - # -----> - - position = (110, v - 160 * 1.6) - t = bui.textwidget( - parent=c, - position=(position[0] - 14, position[1] + 70), - size=(30 + width, 50), - h_align="center", - text=f"{getlanguage('Healing Damage PTG')}{_sp_}({getlanguage('Healing Damage')})", - color=bui.app.ui_v1.title_color, - v_align="center", - maxwidth=width * 1.3, - scale=1.4, - ) - - b = bui.buttonwidget( - parent=c, - autoselect=True, - position=position, - size=(100, 100), - repeat=True, - scale=0.6, - label=self.charstr[3], - button_type='square', - text_scale=2, - on_activate_call=babase.CallPartial(self.health_damage_percentage, 'Decrement'), - ) - - b = bui.buttonwidget( - parent=c, - autoselect=True, - repeat=True, - text_scale=2, - position=(position[0] * 3.2, position[1]), - size=(100, 100), - scale=0.6, - label=self.charstr[2], - button_type='square', - on_activate_call=babase.CallPartial(self.health_damage_percentage, 'Increment'), - ) - - porcentaje = config['Healing Damage PTG'] - if porcentaje > 59: - color = (0, 1, 0) - elif porcentaje < 40: - color = (1, 1, 0) - else: - color = (0, 1, 0.8) - - self.hlg_text = bui.textwidget( - parent=c, - position=(position[0] - 14, position[1] + 5), - size=(30 + width, 50), - h_align="center", - text=str(porcentaje) + '%', - color=color, - v_align="center", - maxwidth=width * 1.3, - scale=2, - ) - - elif tab == 'Percentage': - sub_height = len(self.default_power_list) * 90 - v = sub_height - 55 - width = 300 - posi = 0 - id_power = list(self.default_powerups) - new_powerups = id_power[9:] - self.listpower = {} - - self._tab_container = c = bui.containerwidget( - parent=self._scrollwidget, - size=(self._sub_width, sub_height), - background=False, - selection_loops_to_parent=True, - ) - - for power in self.default_power_list: - if power == id_power[0]: - text = 'helpWindow.powerupShieldNameText' - tex = bui.gettexture('powerupShield') - elif power == id_power[1]: - text = 'helpWindow.powerupPunchNameText' - tex = bui.gettexture('powerupPunch') - elif power == id_power[2]: - text = 'helpWindow.powerupLandMinesNameText' - tex = bui.gettexture('powerupLandMines') - elif power == id_power[3]: - text = 'helpWindow.powerupImpactBombsNameText' - tex = bui.gettexture('powerupImpactBombs') - elif power == id_power[4]: - text = 'helpWindow.powerupIceBombsNameText' - tex = bui.gettexture('powerupIceBombs') - elif power == id_power[5]: - text = 'helpWindow.powerupBombNameText' - tex = bui.gettexture('powerupBomb') - elif power == id_power[6]: - text = 'helpWindow.powerupStickyBombsNameText' - tex = bui.gettexture('powerupStickyBombs') - elif power == id_power[7]: - text = 'helpWindow.powerupCurseNameText' - tex = bui.gettexture('powerupCurse') - elif power == id_power[8]: - text = 'helpWindow.powerupHealthNameText' - tex = bui.gettexture('powerupHealth') - elif power == id_power[9]: - text = power - tex = bui.gettexture('powerupSpeed') - elif power == id_power[10]: - text = power - tex = bui.gettexture('heart') - elif power == id_power[11]: - text = "Goodbye!" - tex = bui.gettexture('achievementOnslaught') - elif power == id_power[12]: - text = power - tex = bui.gettexture('ouyaUButton') - elif power == id_power[13]: - text = power - tex = bui.gettexture('achievementSuperPunch') - elif power == id_power[14]: - text = power - tex = bui.gettexture('levelIcon') - elif power == id_power[15]: - text = power - tex = bui.gettexture('ouyaOButton') - elif power == id_power[16]: - text = power - tex = bui.gettexture('star') - - if power in new_powerups: - label = getlanguage(power) - else: - label = babase.Lstr(resource=text) - - apperance = powerups[power] - position = (90, v - posi) - - t = bui.textwidget( - parent=c, - position=(position[0] - 30, position[1] - 15), - size=(width, 50), - h_align="center", - color=(bui.app.ui_v1.title_color), - text=label, - v_align="center", - maxwidth=width * 1.3, - ) - - self.powprev = bui.imagewidget( - parent=c, - position=(position[0] - 70, position[1] - 10), - size=(50, 50), - texture=tex, - ) - - ptg = str(self.total_percentage(power)) - t = bui.textwidget( - parent=c, - position=(position[0] + 170, position[1] - 10), - size=(width, 50), - h_align="center", - color=(0, 1, 0), - text=(f'{ptg}%'), - v_align="center", - maxwidth=width * 1.3, - ) - - posi += 90 - - elif tab == 'Action 4': - sub_height = 370 - width = 300 - v = sub_height - 55 - u = width - 60 - - if not self._scrollwidget or not self._scrollwidget.exists(): - return - self._tab_container = c = bui.containerwidget( - parent=self._scrollwidget, - size=(width + 500, sub_height), - background=False, - selection_loops_to_parent=True, - ) - - position = (u + 150, v - 250) - n_pos = 0 - prices = [7560, 5150, 3360] - str_name = ["FireBombs Store", "Timer Store", "Percentages Store"] - images = ["ouyaOButton", "settingsIcon", "inventoryIcon"] - - index = 0 - for store in store_items(): - p = prices[index] - txt = str_name[index] - label = getlanguage(txt) - tx_pos = len(label) * 1.8 - lb_scale = len(label) * 0.20 - preview = images[index] - - if STORE[store]: - text = getlanguage('Bought') - icon = bui.gettexture('graphicsIcon') - color = (0.52, 0.48, 0.63) - txt_scale = 1.5 - else: - text = str(p) - icon = bui.gettexture('coin') - color = (0.5, 0.4, 0.93) - txt_scale = 2 - - b = bui.buttonwidget( - parent=c, - autoselect=True, - position=(position[0] + 210 - n_pos, position[1]), - size=(250, 80), - scale=0.7, - label=text, - text_scale=txt_scale, - icon=icon, - color=color, - iconscale=1.7, - on_activate_call=babase.CallPartial(self._buy_object, store, p), - ) - - s = 180 - b = bui.buttonwidget( - parent=c, - autoselect=True, - position=(position[0] + 210 - n_pos, position[1] + 55), - size=(s, s + 30), - scale=1, - label='', - color=color, - button_type='square', - on_activate_call=babase.CallPartial(self._buy_object, store, p), - ) - - s -= 80 - i = bui.imagewidget( - parent=c, - draw_controller=b, - position=(position[0] + 250 - n_pos, position[1] + 140), - size=(s, s), - texture=bui.gettexture(preview), - ) - - t = bui.textwidget( - parent=c, - position=(position[0] + 270 - n_pos, position[1] + 101), - h_align="center", - color=(bui.app.ui_v1.title_color), - text=label, - v_align="center", - maxwidth=130, - ) - - n_pos += 280 - index += 1 - - elif tab == 'Action 5': - sub_height = 370 - v = sub_height - 55 - width = 300 - - self._tab_container = c = bui.containerwidget( - parent=self._scrollwidget, - size=(self._sub_width, sub_height), - background=False, - selection_loops_to_parent=True, - ) - - position = (0, v - 30) - - t = bui.textwidget( - parent=c, - position=(position[0] + 80, position[1] - 30), - size=(width + 60, 50), - scale=1, - h_align="center", - color=(bui.app.ui_v1.title_color), - text=babase.Lstr(resource='settingsWindowAdvanced.enterPromoCodeText'), - v_align="center", - maxwidth=width * 1.3, - ) - - self.promocode_text = bui.textwidget( - parent=c, - position=(position[0] + 80, position[1] - 100), - size=(width + 60, 50), - scale=1, - editable=True, - h_align="center", - color=(bui.app.ui_v1.title_color), - text='', - v_align="center", - maxwidth=width * 1.3, - max_chars=30, - description=babase.Lstr(resource='settingsWindowAdvanced.enterPromoCodeText'), - ) - - self.promocode_button = bui.buttonwidget( - parent=c, - position=(position[0] + 160, position[1] - 170), - size=(200, 60), - scale=1.0, - label=babase.Lstr(resource='submitText'), - on_activate_call=self._promocode, - ) - - else: - sub_height = 0 - v = sub_height - 55 - width = 300 - - self._tab_container = c = bui.containerwidget( - parent=self._scrollwidget, - size=(self._sub_width, sub_height), - background=False, - selection_loops_to_parent=True, - ) - - t = bui.textwidget( - parent=c, - position=(110, v - 20), - size=(width, 50), - scale=1.4, - color=(0.2, 1.2, 0.2), - h_align="center", - v_align="center", - text=("Ultimate Powerup Manager v2.5"), - maxwidth=width * 30, - ) - - t = bui.textwidget( - parent=c, - position=(110, v - 90), - size=(width, 50), - scale=1, - color=(1.3, 0.5, 1.0), - h_align="center", - v_align="center", - text=getlanguage('Creator'), - maxwidth=width * 30, - ) - - t = bui.textwidget( - parent=c, - position=(110, v - 220), - size=(width, 50), - scale=1, - color=(1.0, 1.2, 0.3), - h_align="center", - v_align="center", - text=getlanguage('Mod Info'), - maxwidth=width * 30, - ) - - for select_tab, button_tab in self.tab_buttons.items(): - if select_tab == tab: - bui.buttonwidget(edit=button_tab, color=(0.5, 0.4, 1.5)) - else: - bui.buttonwidget(edit=button_tab, color=(0.52, 0.48, 0.63)) - - def _all_popup(self, tag: str, popup: str) -> None: - config[tag] = popup - apg.apply_and_commit() - - def _set_concept(self, concept: str) -> None: - GLOBAL['Cls Powerup'] = concept - - if concept == 'Reset': - for power, deflt in default_powerups().items(): - powerups[power] = deflt - elif concept == 'Nothing': - for power in default_powerups(): - powerups[power] = 0 - elif concept == 'Only Bombs': - for power, deflt in default_powerups().items(): - if 'Bombs' not in power: - powerups[power] = 0 - else: - powerups[power] = 3 - elif concept == 'Only Items': - for power, deflt in default_powerups().items(): - if 'Bombs' in power: - powerups[power] = 0 - else: - powerups[power] = deflt - elif concept == 'New': - default_power = default_powerups() - new_powerups = list(default_power)[9:] - for power, deflt in default_power.items(): - if power not in new_powerups: - powerups[power] = 0 - else: - powerups[power] = deflt - - if not STORE['Buy Firebombs']: - powerups['Fire Bombs'] = 0 - - self._set_tab('Action 1') - - def tank_shield_percentage(self, tag): - max = 96 - min = 40 - if tag == 'Increment': - config['Tank Shield PTG'] += 1 - if config['Tank Shield PTG'] > max: - config['Tank Shield PTG'] = min - elif tag == 'Decrement': - config['Tank Shield PTG'] -= 1 - if config['Tank Shield PTG'] < min: - config['Tank Shield PTG'] = max - - porcentaje = config['Tank Shield PTG'] - if porcentaje > 59: - color = (0, 1, 0) - elif porcentaje < 40: - color = (1, 1, 0) - else: - color = (0, 1, 0.8) - bui.textwidget(edit=self.tank_text, text=str(porcentaje) + '%', color=color) - - def health_damage_percentage(self, tag): - max = 80 - min = 35 - if tag == 'Increment': - config['Healing Damage PTG'] += 1 - if config['Healing Damage PTG'] > max: - config['Healing Damage PTG'] = min - elif tag == 'Decrement': - config['Healing Damage PTG'] -= 1 - if config['Healing Damage PTG'] < min: - config['Healing Damage PTG'] = max - - porcentaje = config['Healing Damage PTG'] - if porcentaje > 59: - color = (0, 1, 0) - elif porcentaje < 40: - color = (1, 1, 0) - else: - color = (0, 1, 0.8) - bui.textwidget(edit=self.hlg_text, text=str(porcentaje) + '%', color=color) - - def apperance_powerups(self, powerup: str, ID: str): - max = 7 - if ID == "-": - if powerups[powerup] == 0: - powerups[powerup] = max - else: - powerups[powerup] -= 1 - elif ID == "+": - if powerups[powerup] == max: - powerups[powerup] = 0 - else: - powerups[powerup] += 1 - enum = powerups[powerup] - bui.textwidget( - edit=self.listpower[powerup], text=str(powerups[powerup]), color=cls_pow_color()[enum] - ) - - def _powerups_scale(self, ID: str): - max = 1.5 - min = 0.5 - sc = 0.1 - if ID == "-": - if config['Powerup Scale'] < (min + 0.1): - config['Powerup Scale'] = max - else: - config['Powerup Scale'] -= sc - elif ID == "+": - if config['Powerup Scale'] > (max - 0.1): - config['Powerup Scale'] = min - else: - config['Powerup Scale'] += sc - config['Powerup Scale'] = round(config['Powerup Scale'], 1) - bui.textwidget(edit=self.txt_scale, text=str(config['Powerup Scale'])) - - def total_percentage(self, power): - total = 0 - pw = powerups[power] - for i, i2 in powerups.items(): - total += i2 - if total == 0: - return float(total) - else: - ptg = 100 * pw / total - result = round(ptg, 2) - return result - - def store_refresh(self, tag: str): - if tag == 'Buy Firebombs': - powerups['Fire Bombs'] = 3 - self.default_power_list.append('Fire Bombs') - self._set_tab('Action 4') - - def _buy_object(self, tag: str, price: int): - store = BearStore( - value=tag, price=price, callback=babase.CallPartial(self.store_refresh, tag) - ) - store.buy() - - def _promocode(self): - code = bui.textwidget(query=self.promocode_text) - promo = PromoCode(code=code) - promo.code_confirmation() - bui.textwidget(edit=self.promocode_text, text="") - - def _switches(self, tag, m): - config[tag] = False if m == 0 else True - apg.apply_and_commit() - - def _percentage_window(self): - self._set_tab('Percentage') - - def _back(self): - bui.containerwidget(edit=self._root_widget, transition='out_left') - babase.app.classic.profile_browser_window() - - -def add_plugin(): - try: - from baBearModz import BearPlugin - except Exception as e: - return bs.timer(2.5, lambda e=e: bs.broadcastmessage('Error plugin: ' + str(e), (1, 0, 0))) - BearPlugin( - icon='logo', - creator='UPDATE TO API 9 BY ATD(anas) and less', - button_color=(1, 1, 0), - plugin=UltimatePowerupManager, - window=PowerupManagerWindow, - ) - - -# ba_meta export babase.Plugin - - -class UltimatePowerupManager(babase.Plugin): - # browser.ProfileBrowserWindow = NewProfileBrowserWindow - pupbox.PowerupBoxFactory = NewPowerupBoxFactory - pupbox.PowerupBox.__init__ = _pbx_ - Bomb.__init__ = _bomb_init - SpazBot.handlemessage = bot_handlemessage - Blast.handlemessage = bomb_handlemessage - Spaz.handlemessage = new_handlemessage - Spaz.__init__ = _init_spaz_ - Spaz._get_bomb_type_tex = new_get_bomb_type_tex - Spaz.on_punch_press = spaz_on_punch_press - Spaz.on_punch_release = spaz_on_punch_release - MainMenuActivity.on_transition_in = new_on_transition_in - - def __init__(self) -> None: - - # add_plugin() - ... - - def has_settings_ui(self): - return True - - def show_settings_ui(self, origin_widget): - PowerupManagerWindow() +# ba_meta require api 9 +from __future__ import annotations + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor import bomb +from bascenev1lib.actor import powerupbox as pupbox +from bascenev1lib.actor.spazbot import SpazBot +from bascenev1lib.actor.bomb import Bomb, Blast +from bauiv1lib.popup import PopupWindow, PopupMenuWindow, PopupMenu +from bascenev1lib.actor.spaz import ( + Spaz, + SpazFactory, + PickupMessage, + PunchHitMessage, + CurseExplodeMessage, + BombDiedMessage, +) +from bascenev1lib.mainmenu import MainMenuActivity, MainMenuSession +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.actor.popuptext import PopupText +from bauiv1lib.confirm import ConfirmWindow +from bascenev1lib.actor.spaz import * + +from typing import TYPE_CHECKING + +plugman = dict( + plugin_name="powerup_manager", + description="This plugin add new modded powerups and features to manage them", + external_url="", + authors=[ + {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, + ], + version="1.0.1", +) + + +_sp_ = '\n' + +if TYPE_CHECKING: + pass + + +# === Mod updated by ATD and Less === + + +def getlanguage(text, subs: str = None, almacen: list = []): + if almacen == []: + almacen = list(range(1000)) + lang = bs.app.lang.language + translate = { + "Reset": {"Spanish": "Reiniciar", "English": "Reset", "Portuguese": "Reiniciar"}, + "Nothing": { + "Spanish": "Sin potenciadores", + "English": "No powerups", + "Portuguese": "Sem powerups", + }, + "Action 1": {"Spanish": "Potenciadores", "English": "Powerups", "Portuguese": "Powerups"}, + "Action 2": {"Spanish": "Configuración", "English": "Settings", "Portuguese": "Definições"}, + "Action 3": {"Spanish": "Extras", "English": "Extras", "Portuguese": "Extras"}, + "Action 4": {"Spanish": "Tienda", "English": "Store", "Portuguese": "Loja"}, + "Action 5": { + "Spanish": "Canjear código", + "English": "Enter Code", + "Portuguese": "Código promocional", + }, + "Custom": {"Spanish": "", "English": "Customize", "Portuguese": "Customizar"}, + "Impairment Bombs": { + "Spanish": "Bombas menoscabo", + "English": "Hyperactive bombs", + "Portuguese": "Bombas hiperativas", + }, + "Speed": {"Spanish": "Velocidad", "English": "Speed", "Portuguese": "Velocidade"}, + "Fire Bombs": { + "Spanish": "Bombas de fuego", + "English": "Fire Bombs", + "Portuguese": "Bombas de fogo", + }, + "Ice Man": { + "Spanish": "Hombre de hielo", + "English": "Ice man", + "Portuguese": "Homem de gelo", + }, + "Fly Bombs": { + "Spanish": "Bombas expansivas", + "English": "Expansive bombs", + "Portuguese": "Bombas expansivas", + }, + "Goodbye": {"Spanish": "¡Hasta luego!", "English": "Goodbye!", "Portuguese": "Adeus!"}, + "Healing Damage": { + "Spanish": "Auto-curación", + "English": "Healing Damage", + "Portuguese": "Auto-cura", + }, + "Tank Shield": { + "Spanish": "Súper blindaje", + "English": "Reinforced shield", + "Portuguese": "Escudo reforçado", + }, + "Tank Shield PTG": { + "Spanish": "Porcentaje de disminución", + "English": "Percentage decreased", + "Portuguese": "Percentual reduzido", + }, + "Healing Damage PTG": { + "Spanish": "Porcentaje de recuperación de salud", + "English": "Percentage of health recovered", + "Portuguese": "Porcentagem de recuperação de saúde", + }, + "SY: BALL": {"Spanish": "Esfera", "English": "Sphere", "Portuguese": "Esfera"}, + "SY: Impact": {"Spanish": "Especial", "English": "Special", "Portuguese": "Especial"}, + "SY: Egg": {"Spanish": "Huevito", "English": "Egg shape", "Portuguese": "Ovo"}, + "Powerup Scale": { + "Spanish": "Tamaño del potenciador", + "English": "Powerups size", + "Portuguese": "Tamanho de powerups", + }, + "Powerup With Shield": { + "Spanish": "Potenciadores con escudo", + "English": "Powerups with shield", + "Portuguese": "Powerups com escudo", + }, + "Powerup Time": { + "Spanish": "Mostrar Temporizador", + "English": "Show end time", + "Portuguese": "Mostrar cronômetro", + }, + "Powerup Style": { + "Spanish": "Forma de los potenciadores", + "English": "Shape of powerup", + "Portuguese": "Forma de powerup", + }, + "Powerup Name": { + "Spanish": "Mostrar nombre en los potenciadores", + "English": "Show name on powerups", + "Portuguese": "Mostrar nome em powerups", + }, + "Percentage": { + "Spanish": "Probabilidad", + "English": "Show percentage", + "Portuguese": "Mostrar porcentagem", + }, + "Only Items": { + "Spanish": "Sólo Accesorios", + "English": "Only utensils", + "Portuguese": "Apenas utensilios", + }, + "New": {"Spanish": "Nuevo", "English": "New", "Portuguese": "Novo"}, + "Only Bombs": { + "Spanish": "Sólo Bombas", + "English": "Only bombs", + "Portuguese": "Apenas bombas", + }, + "Coins 0": { + "Spanish": "Monedas Insuficientes", + "English": "Insufficient coins", + "Portuguese": "Moedas insuficientes", + }, + "Purchase": { + "Spanish": "Compra realizada correctamente", + "English": "Successful purchase", + "Portuguese": "Compra Bem Sucedida", + }, + "Double Product": { + "Spanish": "Ya has comprado este artículo", + "English": "You've already bought this", + "Portuguese": "Voce ja comprou isto", + }, + "Bought": {"Spanish": "Comprado", "English": "Bought", "Portuguese": "Comprou"}, + "Confirm Purchase": { + "Spanish": f'Tienes {subs} monedas. {_sp_} ¿Deseas comprar esto?', + "English": f'You have {subs} coins. {_sp_} Do you want to buy this?', + "Portuguese": f'Você tem {subs} moedas. {_sp_} Deseja comprar isto?', + }, + "FireBombs Store": { + "Spanish": 'Bombas de fuego', + "English": 'Fire bombs', + "Portuguese": 'Bombas de incêndio', + }, + "Timer Store": {"Spanish": 'Temporizador', "English": 'Timer', "Portuguese": 'Timer'}, + "Percentages Store": {"Spanish": 'Extras', "English": 'Extras', "Portuguese": 'Extras'}, + "Block Option Store": { + "Spanish": f"Uuups..{_sp_}Esta opción está bloqueada.{_sp_} Para acceder a ella puedes {_sp_} comprarla en la tienda.{_sp_} Gracias...", + "English": f"Oooops...{_sp_}This option is blocked. {_sp_} To access it you can buy {_sp_} it in the store.{_sp_} Thank you...", + "Portuguese": f"Ooops...{_sp_}Esta opção está bloqueada. {_sp_} Para acessá-lo, você pode {_sp_} comprá-lo na loja.{_sp_} Obrigado...", + }, + "True Code": { + "Spanish": "¡Código canjeado!", + "English": "Successful code!", + "Portuguese": "¡Código válido!", + }, + "False Code": { + "Spanish": "Código ya canjeado", + "English": "Expired code", + "Portuguese": "Código expirado", + }, + "Invalid Code": { + "Spanish": "Código inválido", + "English": "Invalid code", + "Portuguese": "Código inválido", + }, + "Reward Code": { + "Spanish": f"¡Felicitaciones! ¡Ganaste {subs} monedas!", + "English": f"Congratulations! You've {subs} coins", + "Portuguese": f"Parabéns! Você tem {subs} moedas", + }, + "Creator": { + "Spanish": "Mod edited by ATD", + "English": "Mod edited by ATD", + "Portuguese": "Mod edited by ATD", + }, + "Mod Info": { + "Spanish": f"Un mod genial que te permite gestionar {_sp_} los potenciadores a tu antojo. {_sp_} también incluye 8 potenciadores extra{_sp_} dejando 17 en total... ¡Guay!", + "English": f"A cool mod that allows you to manage {_sp_} powerups at your whims. {_sp_} also includes 8 extra powerups{_sp_} leaving 17 in total... Wow!", + "Portuguese": f"Um mod legal que permite que você gerencie os{_sp_} powerups de de acordo com seus caprichos. {_sp_} também inclui 8 powerups extras,{_sp_} deixando 17 no total... Uau!", + }, + "Coins Message": { + "Spanish": f"Recompensa: {subs} Monedas", + "English": f"Reward: {subs} Coins", + "Portuguese": f"Recompensa: {subs} Moedas", + }, + "Coins Limit Message": { + "Spanish": f"Ganaste {almacen[0]} Monedas.{_sp_} Pero has superado el límite de {almacen[1]}", + "English": f"You won {almacen[0]} Coins. {_sp_} But you have exceeded the limit of {almacen[1]}", + "Portuguese": f"Você ganhou {almacen[0]} Moedas. {_sp_} Mas você excedeu o limite de {almacen[1]}", + }, + } + languages = ['Spanish', 'Portuguese', 'English'] + if lang not in languages: + lang = 'English' + + if text not in translate: + return text + + return translate[text][lang] + + +def settings_distribution(): + return { + "Powers Gravity": False, + "Tank Shield PTG": 96, + "Healing Damage PTG": 72, + "Powerup Style": 'Auto', + "Powerup Scale": 1.0, + "Powerup Name": False, + "Powerup With Shield": False, + "Powerup Time": False, + } + + +apg = babase.app.config +if "PPM Settings" in apg: + old = apg['PPM Settings'] + for settings in settings_distribution(): + if settings not in old: + apg['PPM Settings'] = settings_distribution() +else: + apg['PPM Settings'] = settings_distribution() +apg.apply_and_commit() + +config = apg['PPM Settings'] + + +def default_powerups(): + return { + "Shield": 2, + "Punch": 3, + "Mine Bombs": 2, + "Impact Bombs": 3, + "Ice Bombs": 3, + "Triple": 3, + "Sticky Bombs": 3, + "Curse": 1, + "Health": 1, + "Speed": 2, + "Healing Damage": 1, + "Goodbye": 2, + "Ice Man": 1, + "Tank Shield": 1, + "Impairment Bombs": 2, + "Fire Bombs": 3, + "Fly Bombs": 3, + } + + +if "Powerups" in config: + p_old = config['Powerups'] + for powerups in default_powerups(): + if powerups not in p_old: + config['Powerups'] = default_powerups() +else: + config['Powerups'] = default_powerups() +apg.apply_and_commit() + +powerups = config['Powerups'] + +# === EXTRAS === + +GLOBAL = {"Tab": 'Action 1', "Cls Powerup": 0, "Coins Message": []} + + +# === STORE === +def promo_codes(): + return { + "G-Am54igO42Os": [True, 1100], + "P-tRo8nM8dZ": [True, 2800], + "Y-tU2B3S": [True, 500], + "B-0mB3RYT2z": [True, 910], + "B-Asd14mON9G0D": [True, 910], + "D-rAcK0cJ23": [True, 910], + "E-a27ZO6f3Y": [True, 600], + "E-Am54igO42Os": [True, 600], + "E-M4uN3K34XB": [True, 840], + "PM-731ClcAF": [True, 50000], + } + + +def store_items(): + return {"Buy Firebombs": True, "Buy Option": True, "Buy Percentage": True} + + +if apg.get('Bear Coin') is None: + apg['Bear Coin'] = 0 + apg.apply_and_commit() + +if apg.get('Bear Coin') is not None: + if apg['Bear Coin'] <= 0: + apg['Bear Coin'] = 0 + apg['Bear Coin'] = int(apg['Bear Coin']) + +if apg.get('Bear Store') is None: + apg['Bear Store'] = {} + +for i, j in store_items().items(): + store = apg['Bear Store'] + if i not in store: + if store.get(i) is None: + store[i] = j + apg.apply_and_commit() + +STORE = apg['Bear Store'] + +if STORE.get('Promo Code') is None: + STORE['Promo Code'] = promo_codes() + +for i, x in promo_codes().items(): + pmcode = STORE['Promo Code'] + if i not in pmcode: + if pmcode.get(i) is None: + pmcode[i] = x + +apg.apply_and_commit() + + +class BearStore: + def __init__(self, price: int = 1000, value: str = '', callback: call = None): + + self.price = price + self.value = value + self.store = STORE[value] + self.coins = apg['Bear Coin'] + self.callback = callback + + def buy(self): + if not self.store: + if self.coins >= (self.price): + + def confirm(): + STORE[self.value] = True + apg['Bear Coin'] -= int(self.price) + bs.broadcastmessage(getlanguage('Purchase'), (0, 1, 0)) + bui.getsound('cashRegister').play() + apg.apply_and_commit() + self.callback() + + ConfirmWindow( + getlanguage('Confirm Purchase', subs=self.coins), + width=400, + height=120, + action=confirm, + ok_text=babase.Lstr(resource='okText'), + ) + else: + bs.broadcastmessage(getlanguage('Coins 0'), (1, 0, 0)) + bui.getsound('error').play() + else: + bs.broadcastmessage(getlanguage('Double Product'), (1, 0, 0)) + bui.getsound('error').play() + + def __del__(self): + apg['Bear Coin'] = int(apg['Bear Coin']) + apg.apply_and_commit() + + +class PromoCode: + def __init__(self, code: str = ''): + self.code = code + self.codes_store = STORE['Promo Code'] + if self.code in self.codes_store: + self.code_type = STORE['Promo Code'][code] + self.promo_code_expire = self.code_type[0] + self.promo_code_amount = self.code_type[1] + + def __del__(self): + apg['Bear Coin'] = int(apg['Bear Coin']) + apg.apply_and_commit() + + def code_confirmation(self): + if self.code != "": + bs.broadcastmessage(babase.Lstr(resource='submittingPromoCodeText'), (0, 1, 0)) + try: + babase.pushcall(babase.CallPartial(self.validate_code), from_other_thread=True) + except: + pass + + def validate_code(self): + if self.code in self.codes_store: + if self.promo_code_expire: + with babase.ContextRef.empty(): + babase.pushcall( + babase.CallPartial(self.successful_code), from_other_thread=True + ) + bs.broadcastmessage(getlanguage('True Code'), (0, 1, 0)) + bui.getsound('cheer').play() + self.code_type[0] = False + else: + bs.broadcastmessage(getlanguage('False Code'), (1, 0, 0)) + bui.getsound('error').play() + else: + bs.broadcastmessage(getlanguage('Invalid Code'), (1, 0, 0)) + bui.getsound('error').play() + + def successful_code(self): + apg['Bear Coin'] += self.promo_code_amount + bs.broadcastmessage(getlanguage('Reward Code', subs=self.promo_code_amount), (0, 1, 0)) + bui.getsound('cashRegister2').play() + + +MainMenuActivity.super_transition_in = MainMenuActivity.on_transition_in + + +def new_on_transition_in(self): + self.super_transition_in() + limit = 8400 + bear_coin = apg['Bear Coin'] + coins_message = GLOBAL['Coins Message'] + try: + if not (STORE['Buy Firebombs'] and STORE['Buy Option'] and STORE['Buy Percentage']): + + if coins_message != []: + result = 0 + for i in coins_message: + result += i + + if not bear_coin >= (limit - 1): + bs.broadcastmessage(getlanguage('Coins Message', subs=result), (0, 1, 0)) + bui.getsound('cashRegister').play() + else: + bs.broadcastmessage( + getlanguage('Coins Limit Message', almacen=[result, limit]), (1, 0, 0) + ) + bui.getsound('error').play() + self.bear_coin_message = True + GLOBAL['Coins Message'] = [] + except: + pass + + +SpazBot.super_handlemessage = SpazBot.handlemessage + + +def bot_handlemessage(self, msg: Any): + self.super_handlemessage(msg) + if isinstance(msg, bs.DieMessage): + if not self.die: + self.die = True + self.limit = 8400 + self.free_coins = random.randint(1, 25) + self.bear_coins = apg['Bear Coin'] + + if not self.bear_coins >= (self.limit): + self.bear_coins += self.free_coins + GLOBAL['Coins Message'].append(self.free_coins) + + if self.bear_coins >= (self.limit): + self.bear_coins = self.limit + + apg['Bear Coin'] = int(self.bear_coins) + apg.apply_and_commit() + + else: + GLOBAL['Coins Message'].append(self.free_coins) + + +def cls_pow_color(): + return [ + (1, 0.1, 0.1), + (0.1, 0.5, 0.9), + (0.1, 0.9, 0.9), + (0.1, 0.9, 0.1), + (0.1, 1, 0.5), + (1, 1, 0.2), + (2, 0.5, 0.5), + (1, 0, 6), + ] + + +def random_color(): + a = random.random() * 3 + b = random.random() * 3 + c = random.random() * 3 + return (a, b, c) + + +def powerup_dist(): + return ( + ('triple_bombs', powerups['Triple']), + ('ice_bombs', powerups['Ice Bombs']), + ('punch', powerups['Punch']), + ('impact_bombs', powerups['Impact Bombs']), + ('land_mines', powerups['Mine Bombs']), + ('sticky_bombs', powerups['Sticky Bombs']), + ('shield', powerups['Shield']), + ('health', powerups['Health']), + ('curse', powerups['Curse']), + ('speed', powerups['Speed']), + ('health_damage', powerups['Healing Damage']), + ('goodbye', powerups['Goodbye']), + ('ice_man', powerups['Ice Man']), + ('tank_shield', powerups['Tank Shield']), + ('impairment_bombs', powerups['Impairment Bombs']), + ('fire_bombs', powerups['Fire Bombs']), + ('fly_bombs', powerups['Fly Bombs']), + ) + + +def percentage_tank_shield(): + percentage = config['Tank Shield PTG'] + percentage_text = ('0.') + str(percentage) + return float(percentage_text) + + +def percentage_health_damage(): + percentage = config['Healing Damage PTG'] + percentage_text = ('0.') + str(percentage) + return float(percentage_text) + + +# === Modify class === + + +class NewProfileBrowserWindow: + def __init__( + self, + transition: str = 'in_right', + in_main_menu: bool = True, + selected_profile: str = None, + origin_widget: bui.Widget = None, + ): + super().__init__(transition, in_main_menu, selected_profile, origin_widget) + + self.session = bs.get_foreground_host_session() + uiscale = bui.app.ui_v1.uiscale + width = 100 if uiscale is babase.UIScale.SMALL else -14 + size = 50 + position = (width * 1.65, 300) + + if isinstance(self.session, MainMenuSession): + self.button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=position, + size=(size, size), + button_type='square', + label='', + on_activate_call=babase.CallPartial(self.powerupmanager_window), + ) + + size = size * 0.60 + self.image = bui.imagewidget( + parent=self._root_widget, + size=(size, size), + draw_controller=self.button, + position=(position[0] + 10.5, position[1] + 17), + texture=bui.gettexture('powerupSpeed'), + ) + + self.text = bui.textwidget( + parent=self._root_widget, + position=(position[0] + 25, position[1] + 10), + size=(0, 0), + scale=0.45, + color=(0.5, 0.5, 0.5, 0.5), + draw_controller=self.button, + maxwidth=60, + text=(f"powerups manager 1.1.1"), + h_align='center', + v_align='center', + ) + + def powerupmanager_window(self): + bui.containerwidget(edit=self._root_widget, transition='out_left') + PowerupManagerWindow() + + +class NewPowerupBoxFactory(pupbox.PowerupBoxFactory): + def __init__(self) -> None: + super().__init__() + self.tex_speed = bs.gettexture('powerupSpeed') + self.tex_health_damage = bs.gettexture('heart') + self.tex_goodbye = bs.gettexture('achievementOnslaught') + self.tex_ice_man = bs.gettexture('ouyaUButton') + self.tex_tank_shield = bs.gettexture('achievementSuperPunch') + self.tex_impairment_bombs = bs.gettexture('levelIcon') + self.tex_fire_bombs = bs.gettexture('ouyaOButton') + self.tex_fly_bombs = bs.gettexture('star') + + self._powerupdist = [] + for powerup, freq in powerup_dist(): + for _i in range(int(freq)): + self._powerupdist.append(powerup) + + def get_random_powerup_type(self, forcetype=None, excludetypes=None): + + try: + self.mapa = bs.getactivity()._map.getname() + except: + self.mapa = None + + speed_banned_maps = ['Hockey Stadium', 'Lake Frigid', 'Happy Thoughts'] + + if self.mapa in speed_banned_maps: + powerup_disable = ['speed'] + else: + powerup_disable = [] + + if excludetypes is None: + excludetypes = [] + if forcetype: + ptype = forcetype + else: + if self._lastpoweruptype == 'curse': + ptype = 'health' + else: + while True: + ptype = self._powerupdist[random.randint(0, len(self._powerupdist) - 1)] + if ptype not in excludetypes and ptype not in powerup_disable: + break + self._lastpoweruptype = ptype + return ptype + + +def fire_effect(self): + if self.node.exists(): + bs.emitfx( + position=self.node.position, scale=3, count=50 * 2, spread=0.3, chunk_type='sweat' + ) + else: + self.fire_effect_time = None + + +# BOMBS +Bomb._pm_old_bomb = Bomb.__init__ + + +def _bomb_init( + self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + bomb_type: str = 'normal', + blast_radius: float = 2.0, + bomb_scale: float = 1.0, + source_player: bs.Player = None, + owner: bs.Node = None, +): + + self.bm_type = bomb_type + new_bomb_type = 'ice' if bomb_type in ['ice_bubble', 'impairment', 'fire', 'fly'] else bomb_type + + # Call original __init__ + self._pm_old_bomb( + position=position, + velocity=velocity, + bomb_type=new_bomb_type, + blast_radius=blast_radius, + bomb_scale=bomb_scale, + source_player=source_player, + owner=owner, + ) + + tex = self.node.color_texture + + if self.bm_type == 'ice_bubble': + self.bomb_type = self.bm_type + self.node.mesh = None + self.shield_ice = bs.newnode( + 'shield', owner=self.node, attrs={'color': (0.5, 1.0, 7.0), 'radius': 0.6} + ) + self.node.connectattr('position', self.shield_ice, 'position') + + elif self.bm_type == 'fire': + self.bomb_type = self.bm_type + self.node.mesh = None + self.shield_fire = bs.newnode( + 'shield', owner=self.node, attrs={'color': (6.5, 6.5, 2.0), 'radius': 0.6} + ) + self.node.connectattr('position', self.shield_fire, 'position') + self.fire_effect_time = bs.Timer(0.1, bs.CallPartial(fire_effect, self), repeat=True) + elif self.bm_type == 'impairment': + self.bomb_type = self.bm_type + tex = bs.gettexture('eggTex3') + elif self.bm_type == 'fly': + self.bomb_type = self.bm_type + tex = bs.gettexture('eggTex1') + + if 'tex' in locals(): + self.node.color_texture = tex + self.hit_subtype = self.bomb_type + + if self.bomb_type == 'ice_bubble': + self.blast_radius *= 1.2 + elif self.bomb_type == 'fly': + self.blast_radius *= 2.2 + + +def bomb_handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, bomb.ExplodeHitMessage): + node = bs.getcollision().opposingnode + assert self.node + nodepos = self.node.position + mag = 2000.0 + if self.blast_type in ('ice', 'ice_bubble'): + mag *= 0.5 + elif self.blast_type == 'land_mine': + mag *= 2.5 + elif self.blast_type == 'tnt': + mag *= 2.0 + elif self.blast_type == 'fire': + mag *= 0.6 + elif self.blast_type == 'fly': + mag *= 5.5 + + node.handlemessage( + bs.HitMessage( + pos=nodepos, + velocity=(0, 0, 0), + magnitude=mag, + hit_type=self.hit_type, + hit_subtype=self.hit_subtype, + radius=self.radius, + source_player=babase.existing(self._source_player), + ) + ) + if self.blast_type in ('ice', 'ice_bubble'): + bomb.BombFactory.get().freeze_sound.play(10) + node.handlemessage(bs.FreezeMessage()) + + return None + + +def powerup_translated(self, type: str): + powerups_names = { + 'triple_bombs': babase.Lstr(resource='helpWindow.' + 'powerupBombNameText'), + 'ice_bombs': babase.Lstr(resource='helpWindow.' + 'powerupIceBombsNameText'), + 'punch': babase.Lstr(resource='helpWindow.' + 'powerupPunchNameText'), + 'impact_bombs': babase.Lstr(resource='helpWindow.' + 'powerupImpactBombsNameText'), + 'land_mines': babase.Lstr(resource='helpWindow.' + 'powerupLandMinesNameText'), + 'sticky_bombs': babase.Lstr(resource='helpWindow.' + 'powerupStickyBombsNameText'), + 'shield': babase.Lstr(resource='helpWindow.' + 'powerupShieldNameText'), + 'health': babase.Lstr(resource='helpWindow.' + 'powerupHealthNameText'), + 'curse': babase.Lstr(resource='helpWindow.' + 'powerupCurseNameText'), + 'speed': getlanguage('Speed'), + 'health_damage': getlanguage('Healing Damage'), + 'goodbye': getlanguage('Goodbye'), + 'ice_man': getlanguage('Ice Man'), + 'tank_shield': getlanguage('Tank Shield'), + 'impairment_bombs': getlanguage('Impairment Bombs'), + 'fire_bombs': getlanguage('Fire Bombs'), + 'fly_bombs': getlanguage('Fly Bombs'), + } + self.texts['Name'].text = powerups_names[type] + + +# POWERUP +pupbox.PowerupBox._old_pbx_ = pupbox.PowerupBox.__init__ + + +def _pbx_( + self, + position: Sequence[float] = (0.0, 1.0, 0.0), + poweruptype: str = 'triple_bombs', + expire: bool = True, +): + self.news: list = [] + for x, i in powerup_dist(): + self.news.append(x) + + self.box: list = [] + self.texts = {} + self.news = self.news[9:] + self.box.append(poweruptype) + self.npowerup = self.box[0] + factory = NewPowerupBoxFactory.get() + + if self.npowerup in self.news: + new_poweruptype = 'shield' + else: + new_poweruptype = poweruptype + self._old_pbx_(position, new_poweruptype, expire) + + type = new_poweruptype + tex = self.node.color_texture + mesh = self.node.mesh + + if self.npowerup == 'speed': + type = self.npowerup + tex = factory.tex_speed + elif self.npowerup == 'health_damage': + type = self.npowerup + tex = factory.tex_health_damage + elif self.npowerup == 'goodbye': + type = self.npowerup + tex = factory.tex_goodbye + elif self.npowerup == 'ice_man': + type = self.npowerup + tex = factory.tex_ice_man + elif self.npowerup == 'tank_shield': + type = self.npowerup + tex = factory.tex_tank_shield + elif self.npowerup == 'impairment_bombs': + type = self.npowerup + tex = factory.tex_impairment_bombs + elif self.npowerup == 'fire_bombs': + type = self.npowerup + tex = factory.tex_fire_bombs + elif self.npowerup == 'fly_bombs': + type = self.npowerup + tex = factory.tex_fly_bombs + + self.poweruptype = type + self.node.mesh = mesh + self.node.color_texture = tex + n_scale = config['Powerup Scale'] + style = config['Powerup Style'] + + curve = bs.animate(self.node, 'mesh_scale', {0: 0, 0.14: 1.6, 0.2: n_scale}) + bs.timer(0.2, curve.delete) + + def util_text( + type: str, + text: str, + scale: float = 1, + color: list = [1, 1, 1], + position: list = [0, 0.7, 0], + colors_name: bool = False, + ): + m = bs.newnode( + 'math', + owner=self.node, + attrs={'input1': (position[0], position[1], position[2]), 'operation': 'add'}, + ) + self.node.connectattr('position', m, 'input2') + self.texts[type] = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': str(text), + 'in_world': True, + 'scale': 0.02, + 'shadow': 0.5, + 'flatness': 1.0, + 'color': (color[0], color[1], color[2]), + 'h_align': 'center', + }, + ) + m.connectattr('output', self.texts[type], 'position') + bs.animate(self.texts[type], 'scale', {0: 0.017, 0.4: 0.017, 0.5: 0.01 * scale}) + + if colors_name: + bs.animate_array( + self.texts[type], + 'color', + 3, + { + 0: (1, 0, 0), + 0.2: (1, 0.5, 0), + 0.4: (1, 1, 0), + 0.6: (0, 1, 0), + 0.8: (0, 1, 1), + 1.0: (1, 0, 1), + 1.2: (1, 0, 0), + }, + loop=True, + ) + + def update_time(time): + if hasattr(self, 'texts') and 'Time' in self.texts and self.texts['Time']: + self.texts['Time'].text = str(time) + + if config['Powerup Time']: + interval = int(pupbox.DEFAULT_POWERUP_INTERVAL) + time2 = interval - 1 + time = 1 + + util_text( + 'Time', time2, scale=1.5, color=(2, 2, 2), position=[0, 0.9, 0], colors_name=False + ) + + while interval + 3: + bs.timer(time - 1, babase.CallPartial(update_time, f'{time2}s')) + + if time2 == 0: + break + + time += 1 + time2 -= 1 + + if config['Powerup With Shield']: + scale = config['Powerup Scale'] + self.shield = bs.newnode( + 'shield', owner=self.node, attrs={'color': (1, 1, 0), 'radius': 1.3 * scale} + ) + self.node.connectattr('position', self.shield, 'position') + bs.animate_array( + self.shield, + 'color', + 3, + {0: (2, 0, 0), 0.5: (0, 2, 0), 1: (0, 1, 6), 1.5: (2, 0, 0)}, + loop=True, + ) + + if config['Powerup Name']: + util_text('Name', self.poweruptype, scale=1.2, position=[0, 0.4, 0], colors_name=True) + powerup_translated(self, self.poweruptype) + + if style == 'SY: BALL': + self.node.mesh = bs.getmesh('frostyPelvis') + elif style == 'SY: Impact': + self.node.mesh = bs.getmesh('impactBomb') + elif style == 'SY: Egg': + self.node.mesh = bs.getmesh('egg') + + +# SPAZ +def _speed_off_flash(self): + if self.node: + factory = NewPowerupBoxFactory.get() + self.node.billboard_texture = factory.tex_speed + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + + +def _speed_wear_off(self): + if self.node: + self.node.hockey = False + self.node.billboard_opacity = 0.0 + bui.getsound('powerdown01').play() + + +def _ice_man_off_flash(self): + if self.node: + factory = NewPowerupBoxFactory.get() + self.node.billboard_texture = factory.tex_ice_man + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + + +def _ice_man_wear_off(self): + if self.node: + f = self.color[0] + i = (0, 1, 4) + + bomb = self.bmb_color[0] + if bomb != 'ice_bubble': + self.bomb_type = bomb + else: + self.bomb_type = 'normal' + + self.freeze_punch = False + self.node.billboard_opacity = 0.0 + bs.animate_array(self.node, 'color', 3, {0: f, 0.3: i, 0.6: f}) + bui.getsound('powerdown01').play() + + +Spaz._pm2_spz_old = Spaz.__init__ + + +def _init_spaz_(self, *args, **kwargs): + self._pm2_spz_old(*args, **kwargs) + self.edg_eff = False + self.kill_eff = False + self.freeze_punch = False + self.die = False + self.color: list = [] + self.color.append(self.node.color) + + self.tankshield = {"Tank": False, "Reduction": False, "Shield": None} + + +Spaz._super_on_punch_press = Spaz.on_punch_press + + +def spaz_on_punch_press(self) -> None: + self._super_on_punch_press() + + if self.tankshield['Tank']: + try: + self.tankshield['Reduction'] = True + + shield = bs.newnode( + 'shield', owner=self.node, attrs={'color': (4, 1, 4), 'radius': 1.3} + ) + self.node.connectattr('position_center', shield, 'position') + + self.tankshield['Shield'] = shield + except: + pass + + +Spaz._super_on_punch_release = Spaz.on_punch_release + + +def spaz_on_punch_release(self) -> None: + self._super_on_punch_release() + try: + self.tankshield['Shield'].delete() + self.tankshield['Reduction'] = False + except: + pass + + +def new_get_bomb_type_tex(self) -> babase.Texture: + factory = NewPowerupBoxFactory.get() + if self.bomb_type == 'sticky': + return factory.tex_sticky_bombs + if self.bomb_type == 'ice': + return factory.tex_ice_bombs + if self.bomb_type == 'impact': + return factory.tex_impact_bombs + if self.bomb_type == 'impairment': + return factory.tex_impairment_bombs + if self.bomb_type == 'fire': + return factory.tex_fire_bombs + if self.bomb_type == 'fly': + return factory.tex_fly_bombs + return None + + +def new_handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, bs.PickedUpMessage): + if self.node: + self.node.handlemessage('hurt_sound') + self.node.handlemessage('picked_up') + + self._num_times_hit += 1 + + elif isinstance(msg, bs.ShouldShatterMessage): + bs.timer(0.001, bs.WeakCallPartial(self.shatter)) + + elif isinstance(msg, bs.ImpactDamageMessage): + bs.timer(0.001, bs.WeakCallPartial(self._hit_self, msg.intensity)) + elif isinstance(msg, bs.PowerupMessage): + factory = NewPowerupBoxFactory.get() + if self._dead or not self.node: + return True + if self.pick_up_powerup_callback is not None: + self.pick_up_powerup_callback(self) + if msg.poweruptype == 'triple_bombs': + tex = PowerupBoxFactory.get().tex_bomb + self._flash_billboard(tex) + self.set_bomb_count(3) + if self.powerups_expire: + self.node.mini_billboard_1_texture = tex + t_ms = int(bs.time() * 1000) + assert isinstance(t_ms, int) + self.node.mini_billboard_1_start_time = t_ms + self.node.mini_billboard_1_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._multi_bomb_wear_off_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000), + babase.CallPartial(self._multi_bomb_wear_off_flash), + ) + self._multi_bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME, babase.CallPartial(self._multi_bomb_wear_off) + ) + elif msg.poweruptype == 'land_mines': + self.set_land_mine_count(min(self.land_mine_count + 3, 3)) + elif msg.poweruptype == 'impact_bombs': + self.bomb_type = 'impact' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = int(bs.time() * 1000) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME - 2000, babase.CallPartial(self._bomb_wear_off_flash) + ) + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME, babase.CallPartial(self._bomb_wear_off) + ) + elif msg.poweruptype == 'sticky_bombs': + self.bomb_type = 'sticky' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = int(bs.time() * 1000) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME - 2000, babase.CallPartial(self._bomb_wear_off_flash) + ) + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME, babase.CallPartial(self._bomb_wear_off) + ) + elif msg.poweruptype == 'punch': + self._has_boxing_gloves = True + tex = PowerupBoxFactory.get().tex_punch + self._flash_billboard(tex) + self.equip_boxing_gloves() + if self.powerups_expire: + self.node.boxing_gloves_flashing = False + self.node.mini_billboard_3_texture = tex + t_ms = int(bs.time() * 1000) + assert isinstance(t_ms, int) + self.node.mini_billboard_3_start_time = t_ms + self.node.mini_billboard_3_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._boxing_gloves_wear_off_flash_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME - 2000, bs.WeakCallPartial(self._gloves_wear_off_flash) + ) + self._boxing_gloves_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME, + bs.WeakCallPartial(self._gloves_wear_off), + ) + elif msg.poweruptype == 'shield': + factory = SpazFactory.get() + self.equip_shields(decay=factory.shield_decay_rate > 0) + elif msg.poweruptype == 'curse': + self.curse() + elif msg.poweruptype == 'ice_bombs': + self.bomb_type = 'ice' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCallPartial(self._bomb_wear_off_flash), + ) + + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) + ) + + elif msg.poweruptype == 'health': + if self.edg_eff: + f = self.color[0] + r = (2, 0, 0) + g = (0, 2, 0) + bs.animate_array(self.node, 'color', 3, {0: r, 0.6: g, 1.0: f}) + self.edg_eff = False + if self._cursed: + self._cursed = False + factory = SpazFactory.get() + for attr in ['materials', 'roller_materials']: + materials = getattr(self.node, attr) + if factory.curse_material in materials: + setattr( + self.node, + attr, + tuple(m for m in materials if m != factory.curse_material), + ) + self.node.curse_death_time = 0 + self.hitpoints = self.hitpoints_max + self._flash_billboard(PowerupBoxFactory.get().tex_health) + self.node.hurt = 0 + self._last_hit_time = None + self._num_times_hit = 0 + + elif msg.poweruptype == 'tank_shield': + self.tankshield['Tank'] = True + self.edg_eff = False + tex = factory.tex_tank_shield + self._flash_billboard(tex) + + elif msg.poweruptype == 'health_damage': + tex = factory.tex_health_damage + self.edg_eff = True + f = self.color[0] + i = (2, 0.5, 2) + bs.animate_array(self.node, 'color', 3, {0: i, 0.5: i, 0.6: f}) + self._flash_billboard(tex) + self.tankshield['Tank'] = False + self.freeze_punch = False + + elif msg.poweruptype == 'goodbye': + tex = factory.tex_goodbye + self._flash_billboard(tex) + self.kill_eff = True + + elif msg.poweruptype == 'fly_bombs': + self.bomb_type = 'fly' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCallStrict(self._bomb_wear_off_flash), + ) + + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) + ) + + elif msg.poweruptype == 'fire_bombs': + self.bomb_type = 'fire' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCallStrict(self._bomb_wear_off_flash), + ) + + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) + ) + + elif msg.poweruptype == 'impairment_bombs': + self.bomb_type = 'impairment' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCallStrict(self._bomb_wear_off_flash), + ) + + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) + ) + + elif msg.poweruptype == 'ice_man': + tex = factory.tex_ice_man + self.bomb_type = 'ice_bubble' + self.freeze_punch = True + self.edg_eff = False + self.node.color = (0, 1, 4) + self._flash_billboard(tex) + + if self.powerups_expire: + ice_man_time = 17000 + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + ice_man_time + + self.ice_man_flash_timer = bs.Timer( + (ice_man_time - 2000) / 1000.0, babase.CallPartial(_ice_man_off_flash, self) + ) + + self.ice_man_timer = bs.Timer( + ice_man_time / 1000.0, babase.CallPartial(_ice_man_wear_off, self) + ) + + elif msg.poweruptype == 'speed': + self.node.hockey = True + tex = factory.tex_speed + self._flash_billboard(tex) + if self.powerups_expire: + speed_time = 15000 + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + speed_time + + self.speed_flash_timer = bs.Timer( + (speed_time - 2000) / 1000.0, babase.Call(_speed_off_flash, self) + ) + + self.speed_timer = bs.Timer( + speed_time / 1000.0, bs.WeakCallPartial(_speed_wear_off, self) + ) + + self.bmb_color: list = [] + self.bmb_color.append(self.bomb_type) + + self.node.handlemessage('flash') + if msg.sourcenode: + msg.sourcenode.handlemessage(bs.PowerupAcceptMessage()) + return True + + elif isinstance(msg, bs.FreezeMessage): + if not self.node: + return None + if self.node.invincible: + SpazFactory.get().block_sound.play(1.0, self.node.position) + return None + if self.shield: + return None + if not self.frozen: + self.frozen = True + self.node.frozen = True + bs.timer(5.0, bs.WeakCallPartial(self.handlemessage, bs.ThawMessage())) + if self.hitpoints <= 0: + self.shatter() + if self.freeze_punch: + self.handlemessage(bs.ThawMessage()) + + elif isinstance(msg, bs.ThawMessage): + if self.frozen and not self.shattered and self.node: + self.frozen = False + self.node.frozen = False + + elif isinstance(msg, bs.HitMessage): + if not self.node: + return None + if self.node.invincible: + SpazFactory.get().block_sound.play(1.0, self.node.position) + return True + + local_time = bs.time() * 1000 + assert isinstance(local_time, int) + if self._last_hit_time is None or local_time - self._last_hit_time > 1000: + self._num_times_hit += 1 + self._last_hit_time = local_time + + mag = msg.magnitude * self.impact_scale + velocity_mag = msg.velocity_magnitude * self.impact_scale + damage_scale = 0.22 + + def fire_effect(): + if not self.shield: + if self.node.exists(): + bs.emitfx( + position=self.node.position, + scale=3, + count=50 * 2, + spread=0.3, + chunk_type='sweat', + ) + self.node.handlemessage('celebrate', 560) + else: + self._fire_time = None + else: + self._fire_time = None + + def fire(time, damage): + if not self.shield and not self._dead: + self.hitpoints -= damage + bs.show_damage_count(f'-{damage}HP', self.node.position, msg.force_direction) + bui.getsound('fuse01').play() + + if duration != time: + self._fire_time = bs.Timer(0.1, babase.CallPartial(fire_effect), repeat=True) + else: + self._fire_time = None + + if self.hitpoints < 0: + self.node.handlemessage(bs.DieMessage()) + + if msg.hit_subtype == 'fly': + damage_scale = 0.0 + + if self.shield: + self.shield_hitpoints -= 300 + + if self.shield_hitpoints < 0: + self.shield.delete() + self.shield = None + SpazFactory.get().shield_down_sound.play(1.0, self.node.position) + elif msg.hit_subtype == 'fire': + index = 1 + duration = 5 + damage = 103 + if not self.shield: + for firex in range(duration): + bs.timer(index, bs.CallPartial(fire, index, damage)) + self._fire_time = bs.Timer(0.1, babase.CallPartial(fire_effect), repeat=True) + index += 1 + else: + self.shield_hitpoints -= 80 + if self.shield_hitpoints < 1: + self.shield.delete() + self.shield = None + SpazFactory.get().shield_down_sound.play(1.0, self.node.position) + elif msg.hit_subtype == 'impairment': + damage_scale = 0 + + if self.shield: + self.shield.delete() + self.shield = None + SpazFactory.get().shield_down_sound.play(1.0, self.node.position) + else: + hitpoints = int(self.hitpoints * 0.80) + self.hitpoints -= int(hitpoints) + bs.show_damage_count( + (f'-{int(hitpoints / 10)}%'), self.node.position, msg.force_direction + ) + + if self.hitpoints < 0 or hitpoints < 95: + self.node.handlemessage(bs.DieMessage()) + + if self.shield: + if msg.flat_damage: + damage = msg.flat_damage * self.impact_scale + else: + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', + msg.pos[0], + msg.pos[1], + msg.pos[2], + msg.velocity[0], + msg.velocity[1], + msg.velocity[2], + mag, + velocity_mag, + msg.radius, + 1, + msg.force_direction[0], + msg.force_direction[1], + msg.force_direction[2], + ) + damage = damage_scale * self.node.damage + + assert self.shield_hitpoints is not None + self.shield_hitpoints -= int(damage) + self.shield.hurt = 1.0 - float(self.shield_hitpoints) / self.shield_hitpoints_max + + max_spillover = SpazFactory.get().max_shield_spillover_damage + if self.shield_hitpoints <= 0: + + self.shield.delete() + self.shield = None + SpazFactory.get().shield_down_sound.play(1.0, self.node.position) + + npos = self.node.position + bs.emitfx( + position=(npos[0], npos[1] + 0.9, npos[2]), + velocity=self.node.velocity, + count=random.randrange(20, 30), + scale=1.0, + spread=0.6, + chunk_type='spark', + ) + + else: + SpazFactory.get().shield_hit_sound.play(0.5, self.node.position) + + assert msg.force_direction is not None + bs.emitfx( + position=msg.pos, + velocity=( + msg.force_direction[0] * 1.0, + msg.force_direction[1] * 1.0, + msg.force_direction[2] * 1.0, + ), + count=min(30, 5 + int(damage * 0.005)), + scale=0.5, + spread=0.3, + chunk_type='spark', + ) + + if self.shield_hitpoints <= -max_spillover: + leftover_damage = -max_spillover - self.shield_hitpoints + shield_leftover_ratio = leftover_damage / damage + + mag *= shield_leftover_ratio + velocity_mag *= shield_leftover_ratio + else: + return True + else: + shield_leftover_ratio = 1.0 + + if msg.flat_damage: + damage = int(msg.flat_damage * self.impact_scale * shield_leftover_ratio) + else: + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', + msg.pos[0], + msg.pos[1], + msg.pos[2], + msg.velocity[0], + msg.velocity[1], + msg.velocity[2], + mag, + velocity_mag, + msg.radius, + 0, + msg.force_direction[0], + msg.force_direction[1], + msg.force_direction[2], + ) + + damage = int(damage_scale * self.node.damage) + + if self.tankshield['Reduction']: + porcentaje = percentage_tank_shield() + dism = int(damage * porcentaje) + damage = int(damage - dism) + + bs.show_damage_count('-' + str(int(damage / 10)) + '%', msg.pos, msg.force_direction) + + self.node.handlemessage('hurt_sound') + + if self.edg_eff: + porcentaje = percentage_health_damage() + dmg_dism = int(damage * porcentaje) + self.hitpoints += dmg_dism + + PopupText( + text=f'+{int(dmg_dism / 10)}%', + scale=1.5, + position=self.node.position, + color=(0, 1, 0), + ).autoretain() + bs.animate_array( + self.node, 'color', 3, {0: (0, 1, 0), 0.39: (0, 2, 0), 0.4: self.color[0]} + ) + bui.getsound('healthPowerup').play() + + if msg.hit_type == 'punch': + self.on_punched(damage) + + try: + if msg.get_source_player(bs.Player).actor.freeze_punch: + self.node.color = (0, 1, 4) + bui.getsound('freeze').play() + self.node.handlemessage(bs.FreezeMessage()) + except: + pass + + if damage > 350: + assert msg.force_direction is not None + bs.show_damage_count( + '-' + str(int(damage / 10)) + '%', msg.pos, msg.force_direction + ) + + if msg.hit_subtype == 'super_punch': + SpazFactory.get().punch_sound_stronger.play(1.0, self.node.position) + if damage > 500: + sounds = SpazFactory.get().punch_sound_strong + sound = sounds[random.randrange(len(sounds))] + else: + sound = SpazFactory.get().punch_sound + sound.play(1.0, self.node.position) + + assert msg.force_direction is not None + bs.emitfx( + position=msg.pos, + velocity=( + msg.force_direction[0] * 0.5, + msg.force_direction[1] * 0.5, + msg.force_direction[2] * 0.5, + ), + count=min(10, 1 + int(damage * 0.0025)), + scale=0.3, + spread=0.03, + ) + + bs.emitfx( + position=msg.pos, + chunk_type='sweat', + velocity=( + msg.force_direction[0] * 1.3, + msg.force_direction[1] * 1.3 + 5.0, + msg.force_direction[2] * 1.3, + ), + count=min(30, 1 + int(damage * 0.04)), + scale=0.9, + spread=0.28, + ) + + hurtiness = damage * 0.003 + punchpos = ( + msg.pos[0] + msg.force_direction[0] * 0.02, + msg.pos[1] + msg.force_direction[1] * 0.02, + msg.pos[2] + msg.force_direction[2] * 0.02, + ) + flash_color = (1.0, 0.8, 0.4) + light = bs.newnode( + 'light', + attrs={ + 'position': punchpos, + 'radius': 0.12 + hurtiness * 0.12, + 'intensity': 0.3 * (1.0 + 1.0 * hurtiness), + 'height_attenuated': False, + 'color': flash_color, + }, + ) + bs.timer(0.06, light.delete) + + flash = bs.newnode( + 'flash', + attrs={'position': punchpos, 'size': 0.17 + 0.17 * hurtiness, 'color': flash_color}, + ) + bs.timer(0.06, flash.delete) + + if msg.hit_type == 'impact': + assert msg.force_direction is not None + bs.emitfx( + position=msg.pos, + velocity=( + msg.force_direction[0] * 2.0, + msg.force_direction[1] * 2.0, + msg.force_direction[2] * 2.0, + ), + count=min(10, 1 + int(damage * 0.01)), + scale=0.4, + spread=0.1, + ) + if self.hitpoints > 0: + if msg.hit_type == 'impact' and damage > self.hitpoints: + newdamage = max(damage - 200, self.hitpoints - 10) + damage = newdamage + self.node.handlemessage('flash') + + if damage > 0.0 and self.node.hold_node: + self.node.hold_node = None + self.hitpoints -= damage + self.node.hurt = 1.0 - float(self.hitpoints) / self.hitpoints_max + + if self._cursed and damage > 0: + bs.timer( + 0.05, bs.WeakCallPartial(self.curse_explode, msg.get_source_player(bs.Player)) + ) + + if self.frozen and (damage > 200 or self.hitpoints <= 0): + self.shatter() + elif self.hitpoints <= 0: + self.node.handlemessage(bs.DieMessage(how=bs.DeathType.IMPACT)) + + if self.hitpoints <= 0: + damage_avg = self.node.damage_smoothed * damage_scale + if damage_avg > 1000: + self.shatter() + + elif isinstance(msg, BombDiedMessage): + self.bomb_count += 1 + + elif isinstance(msg, bs.DieMessage): + + def drop_bomb(): + for xbomb in range(3): + p = self.node.position + pos = (p[0] + xbomb, p[1] + 5, p[2] - xbomb) + ball = bomb.Bomb(position=pos, bomb_type='impact').autoretain() + ball.node.mesh_scale = 0.6 + ball.node.mesh = bs.getmesh('egg') + ball.node.gravity_scale = 2 + + if self.edg_eff: + self.edg_eff = False + + wasdead = self._dead + self._dead = True + self.hitpoints = 0 + if msg.immediate: + if self.node: + self.node.delete() + elif self.node: + self.node.hurt = 1.0 + if self.play_big_death_sound and not wasdead: + SpazFactory.get().single_player_death_sound.play() + self.node.dead = True + bs.timer(2.0, self.node.delete) + + t = 0 + if self.kill_eff: + for bombs in range(3): + bs.timer(t, babase.CallPartial(drop_bomb)) + t += 0.15 + self.kill_eff = False + + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage(how=bs.DeathType.FALL)) + + elif isinstance(msg, bs.StandMessage): + self._last_stand_pos = (msg.position[0], msg.position[1], msg.position[2]) + if self.node: + self.node.handlemessage( + 'stand', msg.position[0], msg.position[1], msg.position[2], msg.angle + ) + + elif isinstance(msg, CurseExplodeMessage): + self.curse_explode() + + elif isinstance(msg, PunchHitMessage): + if not self.node: + return None + node = bs.getcollision().opposingnode + + if node and (node not in self._punched_nodes): + + punch_momentum_angular = self.node.punch_momentum_angular * self._punch_power_scale + punch_power = self.node.punch_power * self._punch_power_scale + + if node.getnodetype() != 'spaz': + sounds = SpazFactory.get().impact_sounds_medium + sound = sounds[random.randrange(len(sounds))] + sound.play(1.0, self.node.position) + + ppos = self.node.punch_position + punchdir = self.node.punch_velocity + vel = self.node.punch_momentum_linear + + self._punched_nodes.add(node) + node.handlemessage( + bs.HitMessage( + pos=ppos, + velocity=vel, + magnitude=punch_power * punch_momentum_angular * 110.0, + velocity_magnitude=punch_power * 40, + radius=0, + srcnode=self.node, + source_player=self.source_player, + force_direction=punchdir, + hit_type='punch', + hit_subtype=('super_punch' if self._has_boxing_gloves else 'default'), + ) + ) + + mag = -400.0 + if self._hockey: + mag *= 0.5 + if len(self._punched_nodes) == 1: + self.node.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + punchdir[0], + punchdir[1], + punchdir[2], + mag, + ) + elif isinstance(msg, PickupMessage): + if not self.node: + return None + + try: + collision = bs.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + except bs.NotFoundError: + return True + + try: + if opposingnode.invincible: + return True + except Exception: + pass + + if ( + opposingnode.getnodetype() == 'spaz' + and not opposingnode.shattered + and opposingbody == 4 + ): + opposingbody = 1 + + held = self.node.hold_node + if held and held.getnodetype() == 'flag': + return True + + self.node.hold_body = opposingbody + self.node.hold_node = opposingnode + elif isinstance(msg, bs.CelebrateMessage): + if self.node: + self.node.handlemessage('celebrate', int(msg.duration * 1000)) + + return None + + +class PowerupManagerWindow(PopupWindow): + def __init__(self, transition='in_right'): + columns = 2 + self._width = width = 800 + self._height = height = 500 + self._sub_height = 200 + self._scroll_width = self._width * 0.90 + self._scroll_height = self._height - 180 + self._sub_width = self._scroll_width * 0.95 + self.tab_buttons: set = {} + self.list_cls_power: list = [] + self.default_powerups = default_powerups() + self.default_power_list = list(self.default_powerups) + self.coins = apg['Bear Coin'] + self.popup_cls_power = None + + if not STORE['Buy Firebombs']: + powerups['Fire Bombs'] = 0 + self.default_power_list.remove('Fire Bombs') + + self.charstr = [ + babase.charstr(babase.SpecialChar.LEFT_ARROW), + babase.charstr(babase.SpecialChar.RIGHT_ARROW), + babase.charstr(babase.SpecialChar.UP_ARROW), + babase.charstr(babase.SpecialChar.DOWN_ARROW), + ] + + self.tabdefs = { + "Action 1": ['levelIcon', (1, 1, 1)], + "Action 2": ['settingsIcon', (1, 1, 1)], + "Action 3": ['inventoryIcon', (1, 1, 1)], + "Action 4": ['storeIcon', (1, 1, 1)], + "Action 5": ['advancedIcon', (1, 1, 1)], + "About": ['achievementEmpty', (1, 1, 1)], + } + + if STORE['Buy Firebombs'] and STORE['Buy Option'] and STORE['Buy Percentage']: + self.tabdefs = { + "Action 1": ['levelIcon', (1, 1, 1)], + "Action 2": ['settingsIcon', (1, 1, 1)], + "Action 3": ['inventoryIcon', (1, 1, 1)], + "About": ['achievementEmpty', (1, 1, 1)], + } + + self.listdef = list(self.tabdefs) + + self.count = len(self.tabdefs) + + self._current_tab = GLOBAL['Tab'] + + app = bui.app.ui_v1 + uiscale = app.uiscale + + self._root_widget = bui.containerwidget( + size=(width + 90, height + 80), + transition=transition, + scale=1.5 if uiscale is babase.UIScale.SMALL else 1.0, + stack_offset=(0, -30) if uiscale is babase.UIScale.SMALL else (0, 0), + ) + + self._backButton = b = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(60, self._height - 15), + size=(130, 60), + scale=0.8, + text_scale=1.2, + label=babase.Lstr(resource='backText'), + button_type='back', + on_activate_call=babase.CallPartial(self._back), + ) + bui.buttonwidget( + edit=self._backButton, + button_type='backSmall', + size=(60, 60), + label=babase.charstr(babase.SpecialChar.BACK), + ) + bui.containerwidget(edit=self._root_widget, cancel_button=b) + + self.titletext = bui.textwidget( + parent=self._root_widget, + position=(0, height - 15), + size=(width, 50), + h_align="center", + color=bui.app.ui_v1.title_color, + v_align="center", + maxwidth=width * 1.3, + ) + + index = 0 + for tab in range(self.count): + for tab2 in range(columns): + + tag = self.listdef[index] + + position = (620 + (tab2 * 120), self._height - 50 * 2.5 - (tab * 120)) + + if tag == 'About': + text = babase.Lstr(resource='gatherWindow.aboutText') + elif tab == 'Action 4': + text = babase.Lstr(resource='storeText') + else: + text = getlanguage(tag) + + self.tab_buttons[tag] = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=position, + size=(110, 110), + scale=1, + label='', + enable_sound=False, + button_type='square', + on_activate_call=babase.CallPartial(self._set_tab, tag, sound=True), + ) + + self.text = bui.textwidget( + parent=self._root_widget, + position=(position[0] + 55, position[1] + 30), + size=(0, 0), + scale=1, + color=bui.app.ui_v1.title_color, + draw_controller=self.tab_buttons[tag], + maxwidth=100, + text=text, + h_align='center', + v_align='center', + ) + + self.image = bui.imagewidget( + parent=self._root_widget, + size=(60, 60), + color=self.tabdefs[tag][1], + draw_controller=self.tab_buttons[tag], + position=(position[0] + 25, position[1] + 40), + texture=bui.gettexture(self.tabdefs[tag][0]), + ) + + index += 1 + + if self.count == index: + break + + if self.count == index: + break + + self._scrollwidget = None + self._tab_container = None + self._set_tab(self._current_tab) + + def __del__(self): + apg.apply_and_commit() + + def popup_menu_closing(self, window): + print("saliendo") + + def _set_tab(self, tab, sound: bool = False): + self.sound = sound + GLOBAL['Tab'] = tab + apg.apply_and_commit() + + if self._tab_container is not None and self._tab_container.exists(): + self._tab_container.delete() + + if self.sound: + bui.getsound('swish').play() + + if self._scrollwidget: + self._scrollwidget.delete() + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(self._width * 0.08, 51 * 1.8), + size=(self._sub_width - 140, self._scroll_height + 60 * 1.2), + ) + + if tab == 'Action 4': + if self._scrollwidget: + self._scrollwidget.delete() + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(self._width * 0.08, 51 * 1.8), + size=(self._sub_width - 140, self._scroll_height + 60 * 1.2), + capture_arrows=True, + center_small_content=False, + selection_loops_to_parent=True, + claims_left_right=True, + claims_up_down=False, + color=(0.3, 0.3, 0.4), + ) + bui.textwidget(edit=self.titletext, text=babase.Lstr(resource='storeText')) + + elif tab == 'About': + bui.textwidget(edit=self.titletext, text=babase.Lstr(resource='gatherWindow.aboutText')) + else: + bui.textwidget(edit=self.titletext, text=getlanguage(tab)) + + choices = ['Reset', 'Only Bombs', 'Only Items', 'New', 'Nothing'] + c_display = [] + + for display in choices: + choices_display = babase.Lstr(translate=("", getlanguage(display))) + c_display.append(choices_display) + + if tab == 'Action 1': + self.popup_cls_power = PopupMenu( + parent=self._root_widget, + position=(130, self._width * 0.61), + button_size=(150, 50), + scale=2.5, + choices=choices, + width=150, + choices_display=c_display, + current_choice=GLOBAL['Cls Powerup'], + on_value_change_call=self._set_concept, + ) + self.list_cls_power.append(self.popup_cls_power._button) + + self.button_cls_power = bui.buttonwidget( + parent=self._root_widget, + position=(500, self._width * 0.61), + size=(50, 50), + autoselect=True, + scale=1, + label=('%'), + text_scale=1, + button_type='square', + on_activate_call=self._percentage_window, + ) + self.list_cls_power.append(self.button_cls_power) + + rewindow = [self.popup_cls_power._button, self.button_cls_power] + + for ( + cls + ) in self.list_cls_power: # this is very important so that pupups don't accumulate + if cls not in rewindow: + cls.delete() + + elif tab == 'Action 4': + self.button_coin = bui.buttonwidget( + parent=self._root_widget, + icon=bui.gettexture('coin'), + position=(550, self._width * 0.614), + size=(160, 40), + textcolor=(0, 1, 0), + color=(0, 1, 6), + scale=1, + label=str(apg['Bear Coin']), + text_scale=1, + autoselect=True, + on_activate_call=None, + ) # self._percentage_window) + self.list_cls_power.append(self.button_coin) + + try: + rewindow.append(self.button_coin) + except: + rewindow = [self.button_coin] + for ( + cls + ) in self.list_cls_power: # this is very important so that pupups don't accumulate + if cls not in rewindow: + cls.delete() + + else: + try: + for cls in self.list_cls_power: + cls.delete() + except: + pass + + if tab == 'Action 1': + sub_height = len(self.default_power_list) * 90 + v = sub_height - 55 + width = 300 + posi = 0 + id_power = list(self.default_powerups) + new_powerups = id_power[9:] + self.listpower = {} + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + for power in self.default_power_list: + if power == id_power[0]: + text = 'helpWindow.powerupShieldNameText' + tex = bui.gettexture('powerupShield') + elif power == id_power[1]: + text = 'helpWindow.powerupPunchNameText' + tex = bui.gettexture('powerupPunch') + elif power == id_power[2]: + text = 'helpWindow.powerupLandMinesNameText' + tex = bui.gettexture('powerupLandMines') + elif power == id_power[3]: + text = 'helpWindow.powerupImpactBombsNameText' + tex = bui.gettexture('powerupImpactBombs') + elif power == id_power[4]: + text = 'helpWindow.powerupIceBombsNameText' + tex = bui.gettexture('powerupIceBombs') + elif power == id_power[5]: + text = 'helpWindow.powerupBombNameText' + tex = bui.gettexture('powerupBomb') + elif power == id_power[6]: + text = 'helpWindow.powerupStickyBombsNameText' + tex = bui.gettexture('powerupStickyBombs') + elif power == id_power[7]: + text = 'helpWindow.powerupCurseNameText' + tex = bui.gettexture('powerupCurse') + elif power == id_power[8]: + text = 'helpWindow.powerupHealthNameText' + tex = bui.gettexture('powerupHealth') + elif power == id_power[9]: + text = power + tex = bui.gettexture('powerupSpeed') + elif power == id_power[10]: + text = power + tex = bui.gettexture('heart') + elif power == id_power[11]: + text = "Goodbye!" + tex = bui.gettexture('achievementOnslaught') + elif power == id_power[12]: + text = power + tex = bui.gettexture('ouyaUButton') + elif power == id_power[13]: + text = power + tex = bui.gettexture('achievementSuperPunch') + elif power == id_power[14]: + text = power + tex = bui.gettexture('levelIcon') + elif power == id_power[15]: + text = power + tex = bui.gettexture('ouyaOButton') + elif power == id_power[16]: + text = power + tex = bui.gettexture('star') + + if power in new_powerups: + label = getlanguage(power) + else: + label = babase.Lstr(resource=text) + + apperance = powerups[power] + position = (90, v - posi) + + t = bui.textwidget( + parent=c, + position=(position[0] - 30, position[1] - 15), + size=(width, 50), + h_align="center", + color=(bui.app.ui_v1.title_color), + text=label, + v_align="center", + maxwidth=width * 1.3, + ) + + self.powprev = bui.imagewidget( + parent=c, + position=(position[0] - 70, position[1] - 10), + size=(50, 50), + texture=tex, + ) + + dipos = 0 + for direc in ['-', '+']: + bui.buttonwidget( + parent=c, + autoselect=True, + position=(position[0] + 270 + dipos, position[1] - 10), + size=(100, 100), + scale=0.4, + label=direc, + button_type='square', + text_scale=4, + on_activate_call=babase.CallPartial(self.apperance_powerups, power, direc), + ) + + dipos += 100 + + textwidget = bui.textwidget( + parent=c, + position=(position[0] + 190, position[1] - 15), + size=(width, 50), + h_align="center", + color=cls_pow_color()[apperance], + text=str(apperance), + v_align="center", + maxwidth=width * 1.3, + ) + self.listpower[power] = textwidget + + posi += 90 + + elif tab == 'Action 2': + sub_height = 370 if not STORE['Buy Option'] else 450 + v = sub_height - 55 + width = 300 + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + position = (40, v - 20) + + c_display = [] + choices = ['Auto', 'SY: BALL', 'SY: Impact', 'SY: Egg'] + for display in choices: + choices_display = babase.Lstr(translate=("", getlanguage(display))) + c_display.append(choices_display) + + popup = PopupMenu( + parent=c, + position=(position[0] + 300, position[1]), + button_size=(150, 50), + scale=2.5, + choices=choices, + width=150, + choices_display=c_display, + current_choice=config['Powerup Style'], + on_value_change_call=babase.CallPartial(self._all_popup, 'Powerup Style'), + ) + + text = getlanguage('Powerup Style') + wt = len(text) * 0.80 + t = bui.textwidget( + parent=c, + position=(position[0] - 60 + wt, position[1]), + size=(width, 50), + maxwidth=width * 0.9, + scale=1.1, + h_align="center", + color=bui.app.ui_v1.title_color, + text=getlanguage('Powerup Style'), + v_align="center", + ) + + dipos = 0 + for direc in ['-', '+']: + bui.buttonwidget( + parent=c, + autoselect=True, + position=(position[0] + 310 + dipos, position[1] - 100), + size=(100, 100), + repeat=True, + scale=0.4, + label=direc, + button_type='square', + text_scale=4, + on_activate_call=babase.CallPartial(self._powerups_scale, direc), + ) + dipos += 100 + + txt_scale = config['Powerup Scale'] + self.txt_scale = bui.textwidget( + parent=c, + position=(position[0] + 230, position[1] - 105), + size=(width, 50), + scale=1.1, + h_align="center", + color=(0, 1, 0), + text=str(txt_scale), + v_align="center", + maxwidth=width * 1.3, + ) + + text = getlanguage('Powerup Scale') + wt = len(text) * 0.80 + t = bui.textwidget( + parent=c, + position=(position[0] - 60 + wt, position[1] - 100), + size=(width, 50), + maxwidth=width * 0.9, + scale=1.1, + h_align="center", + color=bui.app.ui_v1.title_color, + text=text, + v_align="center", + ) + + position = (position[0] - 20, position[1] + 40) + + self.check = bui.checkboxwidget( + parent=c, + position=(position[0] + 30, position[1] - 230), + value=config['Powerup Name'], + on_value_change_call=babase.CallPartial(self._switches, 'Powerup Name'), + maxwidth=self._scroll_width * 0.9, + text=getlanguage('Powerup Name'), + autoselect=True, + ) + + self.check = bui.checkboxwidget( + parent=c, + position=(position[0] + 30, position[1] - 230 * 1.3), + value=config['Powerup With Shield'], + on_value_change_call=babase.CallPartial(self._switches, 'Powerup With Shield'), + maxwidth=self._scroll_width * 0.9, + text=getlanguage('Powerup With Shield'), + autoselect=True, + ) + + if STORE['Buy Option']: + self.check = bui.checkboxwidget( + parent=c, + position=(position[0] + 30, position[1] - 230 * 1.6), + value=config['Powerup Time'], + on_value_change_call=babase.CallPartial(self._switches, 'Powerup Time'), + maxwidth=self._scroll_width * 0.9, + text=getlanguage('Powerup Time'), + autoselect=True, + ) + + elif tab == 'Action 3': + sub_height = 300 + v = sub_height - 55 + width = 300 + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + v -= 20 + position = (110, v - 45 * 1.72) + + if not STORE['Buy Percentage']: + t = bui.textwidget( + parent=c, + position=(90, v - 100), + size=(30 + width, 50), + h_align="center", + text=getlanguage('Block Option Store'), + color=bui.app.ui_v1.title_color, + v_align="center", + maxwidth=width * 1.5, + scale=1.5, + ) + + i = bui.imagewidget( + parent=c, + position=(position[0] + 100, position[1] - 205), + size=(80, 80), + texture=bui.gettexture('lock'), + ) + else: + t = bui.textwidget( + parent=c, + position=(position[0] - 14, position[1] + 70), + size=(30 + width, 50), + h_align="center", + text=f"{getlanguage('Tank Shield PTG')} ({getlanguage('Tank Shield')})", + color=bui.app.ui_v1.title_color, + v_align="center", + maxwidth=width * 1.5, + scale=1.5, + ) + + b = bui.buttonwidget( + parent=c, + autoselect=True, + position=position, + size=(100, 100), + repeat=True, + scale=0.6, + label=self.charstr[3], + button_type='square', + text_scale=2, + on_activate_call=babase.CallPartial(self.tank_shield_percentage, 'Decrement'), + ) + + b = bui.buttonwidget( + parent=c, + autoselect=True, + repeat=True, + text_scale=2, + position=(position[0] * 3.2, position[1]), + size=(100, 100), + scale=0.6, + label=self.charstr[2], + button_type='square', + on_activate_call=babase.CallPartial(self.tank_shield_percentage, 'Increment'), + ) + + porcentaje = config['Tank Shield PTG'] + if porcentaje > 59: + color = (0, 1, 0) + elif porcentaje < 40: + color = (1, 1, 0) + else: + color = (0, 1, 0.8) + + self.tank_text = bui.textwidget( + parent=c, + position=(position[0] - 14, position[1] + 5), + size=(30 + width, 50), + h_align="center", + text=str(porcentaje) + '%', + color=color, + v_align="center", + maxwidth=width * 1.3, + scale=2, + ) + + # -----> + + position = (110, v - 160 * 1.6) + t = bui.textwidget( + parent=c, + position=(position[0] - 14, position[1] + 70), + size=(30 + width, 50), + h_align="center", + text=f"{getlanguage('Healing Damage PTG')}{_sp_}({getlanguage('Healing Damage')})", + color=bui.app.ui_v1.title_color, + v_align="center", + maxwidth=width * 1.3, + scale=1.4, + ) + + b = bui.buttonwidget( + parent=c, + autoselect=True, + position=position, + size=(100, 100), + repeat=True, + scale=0.6, + label=self.charstr[3], + button_type='square', + text_scale=2, + on_activate_call=babase.CallPartial(self.health_damage_percentage, 'Decrement'), + ) + + b = bui.buttonwidget( + parent=c, + autoselect=True, + repeat=True, + text_scale=2, + position=(position[0] * 3.2, position[1]), + size=(100, 100), + scale=0.6, + label=self.charstr[2], + button_type='square', + on_activate_call=babase.CallPartial(self.health_damage_percentage, 'Increment'), + ) + + porcentaje = config['Healing Damage PTG'] + if porcentaje > 59: + color = (0, 1, 0) + elif porcentaje < 40: + color = (1, 1, 0) + else: + color = (0, 1, 0.8) + + self.hlg_text = bui.textwidget( + parent=c, + position=(position[0] - 14, position[1] + 5), + size=(30 + width, 50), + h_align="center", + text=str(porcentaje) + '%', + color=color, + v_align="center", + maxwidth=width * 1.3, + scale=2, + ) + + elif tab == 'Percentage': + sub_height = len(self.default_power_list) * 90 + v = sub_height - 55 + width = 300 + posi = 0 + id_power = list(self.default_powerups) + new_powerups = id_power[9:] + self.listpower = {} + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + for power in self.default_power_list: + if power == id_power[0]: + text = 'helpWindow.powerupShieldNameText' + tex = bui.gettexture('powerupShield') + elif power == id_power[1]: + text = 'helpWindow.powerupPunchNameText' + tex = bui.gettexture('powerupPunch') + elif power == id_power[2]: + text = 'helpWindow.powerupLandMinesNameText' + tex = bui.gettexture('powerupLandMines') + elif power == id_power[3]: + text = 'helpWindow.powerupImpactBombsNameText' + tex = bui.gettexture('powerupImpactBombs') + elif power == id_power[4]: + text = 'helpWindow.powerupIceBombsNameText' + tex = bui.gettexture('powerupIceBombs') + elif power == id_power[5]: + text = 'helpWindow.powerupBombNameText' + tex = bui.gettexture('powerupBomb') + elif power == id_power[6]: + text = 'helpWindow.powerupStickyBombsNameText' + tex = bui.gettexture('powerupStickyBombs') + elif power == id_power[7]: + text = 'helpWindow.powerupCurseNameText' + tex = bui.gettexture('powerupCurse') + elif power == id_power[8]: + text = 'helpWindow.powerupHealthNameText' + tex = bui.gettexture('powerupHealth') + elif power == id_power[9]: + text = power + tex = bui.gettexture('powerupSpeed') + elif power == id_power[10]: + text = power + tex = bui.gettexture('heart') + elif power == id_power[11]: + text = "Goodbye!" + tex = bui.gettexture('achievementOnslaught') + elif power == id_power[12]: + text = power + tex = bui.gettexture('ouyaUButton') + elif power == id_power[13]: + text = power + tex = bui.gettexture('achievementSuperPunch') + elif power == id_power[14]: + text = power + tex = bui.gettexture('levelIcon') + elif power == id_power[15]: + text = power + tex = bui.gettexture('ouyaOButton') + elif power == id_power[16]: + text = power + tex = bui.gettexture('star') + + if power in new_powerups: + label = getlanguage(power) + else: + label = babase.Lstr(resource=text) + + apperance = powerups[power] + position = (90, v - posi) + + t = bui.textwidget( + parent=c, + position=(position[0] - 30, position[1] - 15), + size=(width, 50), + h_align="center", + color=(bui.app.ui_v1.title_color), + text=label, + v_align="center", + maxwidth=width * 1.3, + ) + + self.powprev = bui.imagewidget( + parent=c, + position=(position[0] - 70, position[1] - 10), + size=(50, 50), + texture=tex, + ) + + ptg = str(self.total_percentage(power)) + t = bui.textwidget( + parent=c, + position=(position[0] + 170, position[1] - 10), + size=(width, 50), + h_align="center", + color=(0, 1, 0), + text=(f'{ptg}%'), + v_align="center", + maxwidth=width * 1.3, + ) + + posi += 90 + + elif tab == 'Action 4': + sub_height = 370 + width = 300 + v = sub_height - 55 + u = width - 60 + + if not self._scrollwidget or not self._scrollwidget.exists(): + return + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(width + 500, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + position = (u + 150, v - 250) + n_pos = 0 + prices = [7560, 5150, 3360] + str_name = ["FireBombs Store", "Timer Store", "Percentages Store"] + images = ["ouyaOButton", "settingsIcon", "inventoryIcon"] + + index = 0 + for store in store_items(): + p = prices[index] + txt = str_name[index] + label = getlanguage(txt) + tx_pos = len(label) * 1.8 + lb_scale = len(label) * 0.20 + preview = images[index] + + if STORE[store]: + text = getlanguage('Bought') + icon = bui.gettexture('graphicsIcon') + color = (0.52, 0.48, 0.63) + txt_scale = 1.5 + else: + text = str(p) + icon = bui.gettexture('coin') + color = (0.5, 0.4, 0.93) + txt_scale = 2 + + b = bui.buttonwidget( + parent=c, + autoselect=True, + position=(position[0] + 210 - n_pos, position[1]), + size=(250, 80), + scale=0.7, + label=text, + text_scale=txt_scale, + icon=icon, + color=color, + iconscale=1.7, + on_activate_call=babase.CallPartial(self._buy_object, store, p), + ) + + s = 180 + b = bui.buttonwidget( + parent=c, + autoselect=True, + position=(position[0] + 210 - n_pos, position[1] + 55), + size=(s, s + 30), + scale=1, + label='', + color=color, + button_type='square', + on_activate_call=babase.CallPartial(self._buy_object, store, p), + ) + + s -= 80 + i = bui.imagewidget( + parent=c, + draw_controller=b, + position=(position[0] + 250 - n_pos, position[1] + 140), + size=(s, s), + texture=bui.gettexture(preview), + ) + + t = bui.textwidget( + parent=c, + position=(position[0] + 270 - n_pos, position[1] + 101), + h_align="center", + color=(bui.app.ui_v1.title_color), + text=label, + v_align="center", + maxwidth=130, + ) + + n_pos += 280 + index += 1 + + elif tab == 'Action 5': + sub_height = 370 + v = sub_height - 55 + width = 300 + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + position = (0, v - 30) + + t = bui.textwidget( + parent=c, + position=(position[0] + 80, position[1] - 30), + size=(width + 60, 50), + scale=1, + h_align="center", + color=(bui.app.ui_v1.title_color), + text=babase.Lstr(resource='settingsWindowAdvanced.enterPromoCodeText'), + v_align="center", + maxwidth=width * 1.3, + ) + + self.promocode_text = bui.textwidget( + parent=c, + position=(position[0] + 80, position[1] - 100), + size=(width + 60, 50), + scale=1, + editable=True, + h_align="center", + color=(bui.app.ui_v1.title_color), + text='', + v_align="center", + maxwidth=width * 1.3, + max_chars=30, + description=babase.Lstr(resource='settingsWindowAdvanced.enterPromoCodeText'), + ) + + self.promocode_button = bui.buttonwidget( + parent=c, + position=(position[0] + 160, position[1] - 170), + size=(200, 60), + scale=1.0, + label=babase.Lstr(resource='submitText'), + on_activate_call=self._promocode, + ) + + else: + sub_height = 0 + v = sub_height - 55 + width = 300 + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + t = bui.textwidget( + parent=c, + position=(110, v - 20), + size=(width, 50), + scale=1.4, + big=True, + color=(0.5, 0.5, 0.5), + h_align="center", + v_align="center", + text=("powerup manager 1.1.1"), + maxwidth=width * 30, + ) + + t = bui.textwidget( + parent=c, + position=(110, v - 90), + size=(width, 50), + scale=1, + color=(1.3, 0.5, 1.0), + h_align="center", + v_align="center", + text=getlanguage('Creator'), + maxwidth=width * 30, + ) + + t = bui.textwidget( + parent=c, + position=(110, v - 220), + size=(width, 50), + scale=1, + color=(1.0, 1.2, 0.3), + h_align="center", + v_align="center", + text=getlanguage('Mod Info'), + maxwidth=width * 30, + ) + + for select_tab, button_tab in self.tab_buttons.items(): + if select_tab == tab: + bui.buttonwidget(edit=button_tab, color=(0.5, 0.4, 1.5)) + else: + bui.buttonwidget(edit=button_tab, color=(0.52, 0.48, 0.63)) + + def _all_popup(self, tag: str, popup: str) -> None: + config[tag] = popup + apg.apply_and_commit() + + def _set_concept(self, concept: str) -> None: + GLOBAL['Cls Powerup'] = concept + + if concept == 'Reset': + for power, deflt in default_powerups().items(): + powerups[power] = deflt + elif concept == 'Nothing': + for power in default_powerups(): + powerups[power] = 0 + elif concept == 'Only Bombs': + for power, deflt in default_powerups().items(): + if 'Bombs' not in power: + powerups[power] = 0 + else: + powerups[power] = 3 + elif concept == 'Only Items': + for power, deflt in default_powerups().items(): + if 'Bombs' in power: + powerups[power] = 0 + else: + powerups[power] = deflt + elif concept == 'New': + default_power = default_powerups() + new_powerups = list(default_power)[9:] + for power, deflt in default_power.items(): + if power not in new_powerups: + powerups[power] = 0 + else: + powerups[power] = deflt + + if not STORE['Buy Firebombs']: + powerups['Fire Bombs'] = 0 + + self._set_tab('Action 1') + + def tank_shield_percentage(self, tag): + max = 96 + min = 40 + if tag == 'Increment': + config['Tank Shield PTG'] += 1 + if config['Tank Shield PTG'] > max: + config['Tank Shield PTG'] = min + elif tag == 'Decrement': + config['Tank Shield PTG'] -= 1 + if config['Tank Shield PTG'] < min: + config['Tank Shield PTG'] = max + + porcentaje = config['Tank Shield PTG'] + if porcentaje > 59: + color = (0, 1, 0) + elif porcentaje < 40: + color = (1, 1, 0) + else: + color = (0, 1, 0.8) + bui.textwidget(edit=self.tank_text, text=str(porcentaje) + '%', color=color) + + def health_damage_percentage(self, tag): + max = 80 + min = 35 + if tag == 'Increment': + config['Healing Damage PTG'] += 1 + if config['Healing Damage PTG'] > max: + config['Healing Damage PTG'] = min + elif tag == 'Decrement': + config['Healing Damage PTG'] -= 1 + if config['Healing Damage PTG'] < min: + config['Healing Damage PTG'] = max + + porcentaje = config['Healing Damage PTG'] + if porcentaje > 59: + color = (0, 1, 0) + elif porcentaje < 40: + color = (1, 1, 0) + else: + color = (0, 1, 0.8) + bui.textwidget(edit=self.hlg_text, text=str(porcentaje) + '%', color=color) + + def apperance_powerups(self, powerup: str, ID: str): + max = 7 + if ID == "-": + if powerups[powerup] == 0: + powerups[powerup] = max + else: + powerups[powerup] -= 1 + elif ID == "+": + if powerups[powerup] == max: + powerups[powerup] = 0 + else: + powerups[powerup] += 1 + enum = powerups[powerup] + bui.textwidget( + edit=self.listpower[powerup], text=str(powerups[powerup]), color=cls_pow_color()[enum] + ) + + def _powerups_scale(self, ID: str): + max = 1.5 + min = 0.5 + sc = 0.1 + if ID == "-": + if config['Powerup Scale'] < (min + 0.1): + config['Powerup Scale'] = max + else: + config['Powerup Scale'] -= sc + elif ID == "+": + if config['Powerup Scale'] > (max - 0.1): + config['Powerup Scale'] = min + else: + config['Powerup Scale'] += sc + config['Powerup Scale'] = round(config['Powerup Scale'], 1) + bui.textwidget(edit=self.txt_scale, text=str(config['Powerup Scale'])) + + def total_percentage(self, power): + total = 0 + pw = powerups[power] + for i, i2 in powerups.items(): + total += i2 + if total == 0: + return float(total) + else: + ptg = 100 * pw / total + result = round(ptg, 2) + return result + + def store_refresh(self, tag: str): + if tag == 'Buy Firebombs': + powerups['Fire Bombs'] = 3 + self.default_power_list.append('Fire Bombs') + self._set_tab('Action 4') + + def _buy_object(self, tag: str, price: int): + store = BearStore( + value=tag, price=price, callback=babase.CallPartial(self.store_refresh, tag) + ) + store.buy() + + def _promocode(self): + code = bui.textwidget(query=self.promocode_text) + promo = PromoCode(code=code) + promo.code_confirmation() + bui.textwidget(edit=self.promocode_text, text="") + + def _switches(self, tag, m): + config[tag] = False if m == 0 else True + apg.apply_and_commit() + + def _percentage_window(self): + self._set_tab('Percentage') + + def _back(self): + bui.containerwidget(edit=self._root_widget, transition='out_left') + babase.app.classic.profile_browser_window() + + +# ba_meta export babase.Plugin + + +class UltimatePowerupManager(babase.Plugin): + # ProfileBrowserWindow = NewProfileBrowserWindow + pupbox.PowerupBoxFactory = NewPowerupBoxFactory + pupbox.PowerupBox.__init__ = _pbx_ + Bomb.__init__ = _bomb_init + SpazBot.handlemessage = bot_handlemessage + Blast.handlemessage = bomb_handlemessage + Spaz.handlemessage = new_handlemessage + Spaz.__init__ = _init_spaz_ + Spaz._get_bomb_type_tex = new_get_bomb_type_tex + Spaz.on_punch_press = spaz_on_punch_press + Spaz.on_punch_release = spaz_on_punch_release + MainMenuActivity.on_transition_in = new_on_transition_in + + def __init__(self) -> None: + + # add_plugin() + ... + + def has_settings_ui(self): + return True + + def show_settings_ui(self, origin_widget): + PowerupManagerWindow() diff --git a/plugins/utilities/rank_system.py b/plugins/utilities/rank_system.py new file mode 100644 index 0000000..681bcb4 --- /dev/null +++ b/plugins/utilities/rank_system.py @@ -0,0 +1,142 @@ +# ba_meta require api 9 + +import os +import json +import babase +import bascenev1 as bs + +plugman = dict( + plugin_name="rank_system", + description="ranks system for servers or for local players", + external_url="", + authors=[ + {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, + ], + version="1.0.0", +) + +MODS_DIR = os.path.dirname(__file__) +STATS_DIR = os.path.join(MODS_DIR, "stats") +STATS_FILE = os.path.join(STATS_DIR, "ranks.json") + + +class RankSystem: + + def __init__(self): + self.data = {} + self.load() + + def load(self): + if not os.path.exists(STATS_DIR): + os.makedirs(STATS_DIR) + + if os.path.exists(STATS_FILE): + try: + with open(STATS_FILE, "r") as f: + self.data = json.load(f) + except: + self.data = {} + + def save(self): + with open(STATS_FILE, "w") as f: + json.dump(self.data, f, indent=4) + + def add_score(self, account_id, score=1): + if account_id not in self.data: + self.data[account_id] = { + "score": 0, + "rank": 0 + } + + self.data[account_id]["score"] += score + self.update_ranks() + self.save() + + def update_ranks(self): + sorted_players = sorted( + self.data.items(), + key=lambda x: x[1]["score"], + reverse=True + ) + + for i, (aid, p) in enumerate(sorted_players): + p["rank"] = i + 1 + + def get_rank(self, account_id): + return self.data.get(account_id, {}).get("rank") + + +rank_sys = RankSystem() + + +class RankTag: + def __init__(self, node, rank): + + m = bs.newnode( + "math", + owner=node, + attrs={ + "input1": (0, 1.2, 0), + "operation": "add" + } + ) + + node.connectattr("torso_position", m, "input2") + + if rank == 1: + text = "1" + color = (1, 1, 1) + elif rank == 2: + text = "2" + color = (1, 1, 1) + elif rank == 3: + text = "3" + color = (1, 1, 1) + else: + text = f"#{rank}" + color = (1, 1, 1) + + t = bs.newnode( + "text", + owner=node, + attrs={ + "text": text, + "in_world": True, + "color": color, + "scale": 0.01, + "h_align": "center" + } + ) + + m.connectattr("output", t, "position") + + +# 🔌 plugin +# ba_meta export babase.Plugin +class byATD(babase.Plugin): + + def on_app_running(self): + print("Rank System Loaded ") + + import bascenev1._gameactivity as ga + + old_spawn = ga.GameActivity.spawn_player_spaz + + def new_spawn(self, player, *args, **kwargs): + spaz = old_spawn(self, player, *args, **kwargs) + + try: + aid = player.sessionplayer.get_account_id() + + rank_sys.add_score(aid, 1) + + rank = rank_sys.get_rank(aid) + if rank: + RankTag(spaz.node, rank) + + except Exception as e: + print("Rank error:", e) + + return spaz + + ga.GameActivity.spawn_player_spaz = new_spawn diff --git a/plugins/utilities/sleep_on_afk.py b/plugins/utilities/sleep_on_afk.py new file mode 100644 index 0000000..3f3ad3b --- /dev/null +++ b/plugins/utilities/sleep_on_afk.py @@ -0,0 +1,129 @@ +# ba_meta require api 9 +from __future__ import annotations + +import babase +import bascenev1 as bs + +from bascenev1lib.actor.spaz import Spaz + +plugman = dict( + plugin_name="sleep_on_afk", + description="Staying idle for 40 seconds will make your character fall asleep, they need rest too..", + external_url="", + authors=[ + {"name": "DinoWattz", "email": "", "discord": ""} + ], + version="1.0.0", +) + +INGAME_TIME = 40 # (in seconds) + +# Spaz Changes + + +def _afk_sleep_knockout(self, value: float) -> None: + if not self.node: + return + if not self.is_alive() and hasattr(self.getplayer(self), 'sessionplayer'): + current_activity = self.getactivity() + player = self.getplayer(self).sessionplayer + # Pop timer data if the player is dead + if current_activity.customdata.get(str(player.id) + '_knockoutTimer') is not None: + current_activity.customdata.pop(str(player.id) + '_knockoutTimer') + return + + self.node.handlemessage('knockout', value) + + +Spaz._afk_sleep_knockout = _afk_sleep_knockout # type: ignore + +# Idle Checker + + +def idle_start(activity: bs.Activity): + activity.customdata['afk_timer'] = bs.Timer( + 0.5, bs.CallStrict(idle_check, activity), repeat=True) + + +def idle_check(current_activity: bs.Activity): + current_session = current_activity.session + wait_time = INGAME_TIME if not current_activity.slow_motion else round(INGAME_TIME / 3) + current = bs.time() * 1000 + if not current_session: + return + for player in current_session.sessionplayers: + if ( + not player.exists() + or not player.in_game + or not getattr(player, 'activityplayer', None) + or not getattr(player.activityplayer, 'actor', None) + or not getattr(player.activityplayer.actor, 'node', None) + or not player.activityplayer.is_alive() + or not player.activityplayer.actor.node + or not player.activityplayer.actor.node.exists() + and not getattr(player.activityplayer.actor.node, 'invincible', False) + ): + continue + + player_actor = player.activityplayer.actor + player_node = player_actor.node + player_data = player.activityplayer.customdata + player_turbo_times = player_actor._turbo_filter_times + + if player_node.move_up_down != 0.0 or player_node.move_left_right != 0.0: + player_data['last_input'] = current + elif player_turbo_times: + highest_turbo_time = player_turbo_times.get( + max(player_turbo_times, key=player_turbo_times.get)) + if highest_turbo_time > player_data.get('last_input', current): + player_data.update({'last_input': highest_turbo_time}) + player_data['last_input'] = player_data.get('last_input', current) + + last_input = max(0, player_data['last_input']) + afk_time = int((current - last_input) / 1000) + + # print(player_data) + # print(last_input) + # print(current) + # print(player.getname() + ": " + str(afk_time)) + # print(wait_time) + + if afk_time >= wait_time: + current_activity.customdata[str(player.id) + '_knockoutTimer'] = bs.Timer( + 0.1, bs.WeakCallStrict(player_actor._afk_sleep_knockout, 100.0), repeat=True) + + # Make the player's node not an area of interest if it was one + if not getattr(player_actor, '_previous_is_area_of_interest', False): + player_actor._previous_is_area_of_interest = player_node.is_area_of_interest + + if player_actor._previous_is_area_of_interest: + player_node.is_area_of_interest = False + + elif current_activity.customdata.get(str(player.id) + '_knockoutTimer') is not None: + current_activity.customdata.pop(str(player.id) + '_knockoutTimer') + + # Restore the player's node area of interest if necessary + if getattr(player_actor, '_previous_is_area_of_interest', False): + player_node.is_area_of_interest = True + + if hasattr(player_actor, "_previous_is_area_of_interest"): + delattr(player_actor, "_previous_is_area_of_interest") + + +# Setup new activity +org_on_begin = bs.Activity.on_begin + + +def patched_on_begin(self, *args, **kwargs): + idle_start(self) + + return org_on_begin(self, *args, **kwargs) + + +bs.Activity.on_begin = patched_on_begin + +# ba_meta export babase.Plugin + + +class Plugin(babase.Plugin): + pass