diff --git a/plugins/minigames.json b/plugins/minigames.json index fee8300..505ee9e 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -564,6 +564,44 @@ } } }, + "canon_fight": { + "description": "Blow up your enemy with powerfull cannon", + "external_url": "https://www.youtube.com/watch?v=7cv3ZSZeTns", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "505c948", + "released_on": "09-06-2024", + "md5sum": "44adbe3bd6ec4988d65ff4ee79b48d95" + } + } + }, + "laser_tracer": { + "description": "Dont touch dangerous laser light", + "external_url": "https://youtu.be/wTgwZKiykQw?si=Cr0ybDYAcKCUNFN4", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "505c948", + "released_on": "09-06-2024", + "md5sum": "08a6457ebd271a7f7860a506b731d272" + } + } + }, "arms_race": { "description": "Upgrade your weapons by eliminating enemies. Win by being first one to kill while cursed", "external_url": "", diff --git a/plugins/minigames/canon_fight.py b/plugins/minigames/canon_fight.py new file mode 100644 index 0000000..05da807 --- /dev/null +++ b/plugins/minigames/canon_fight.py @@ -0,0 +1,447 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# Created by Mr.Smoothy - +# https://discord.gg/ucyaesh +# https://bombsquad-community.web.app/home for more mods. +# +"""DeathMatch game and support classes.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +import random +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.bomb import BombFactory +from bascenev1lib.actor.bomb import Bomb + +from bascenev1lib.game.deathmatch import DeathMatchGame, Player, Team + +if TYPE_CHECKING: + from typing import Any, Union, Sequence, Optional + + +# ba_meta export bascenev1.GameActivity +class CanonFightGame(DeathMatchGame): + + """A game type based on acquiring kills.""" + + name = 'Canon Fight' + description = 'Kill a set number of enemies to win.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + 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.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ["Step Right Up"] + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + # 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.wtindex = 0 + self.wttimer = bs.timer(5, babase.Call(self.wt_), repeat=True) + self.wthighlights = ["Created by Mr.Smoothy", + "hey smoothy youtube", "smoothy#multiverse"] + + def wt_(self): + node = bs.newnode('text', + attrs={ + 'text': self.wthighlights[self.wtindex], + 'flatness': 1.0, + 'h_align': 'center', + 'v_attach': 'bottom', + 'scale': 0.7, + 'position': (0, 20), + 'color': (0.5, 0.5, 0.5) + }) + + self.delt = bs.timer(4, node.delete) + self.wtindex = int((self.wtindex+1) % len(self.wthighlights)) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + self.create_canon_A() + self.create_canon_B() + self.create_wall() + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + self.delete_text_nodes() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def delete_text_nodes(self): + self.canon.delete() + self.canon_.delete() + self.canon2.delete() + self.canon_2.delete() + self.curve.delete() + self.curve2.delete() + + def _handle_canon_load_A(self): + try: + bomb = bs.getcollision().opposingnode.getdelegate(Bomb, True) + # pos=bomb.position + owner = bomb.owner + type = bomb.bomb_type + source_player = bomb.get_source_player(bs.Player) + bs.getcollision().opposingnode.delete() + + # bomb.delete() + self.launch_bomb_byA(owner, type, source_player, 2) + except bs.NotFoundError: + # This can happen if the flag stops touching us due to being + # deleted; that's ok. + return + + def _handle_canon_load_B(self): + try: + bomb = bs.getcollision().opposingnode.getdelegate(Bomb, True) + # pos=bomb.position + owner = bomb.owner + type = bomb.bomb_type + source_player = bomb.get_source_player(bs.Player) + bs.getcollision().opposingnode.delete() + + # bomb.delete() + self.launch_bomb_byB(owner, type, source_player, 2) + except bs.NotFoundError: + # This can happen if the flag stops touching us due to being + # deleted; that's ok. + return + + def launch_bomb_byA(self, owner, type, source_player, count): + if count > 0: + y = random.randrange(2, 9, 2) + z = random.randrange(-4, 6) + self.fake_explosion( + (-5.708631629943848, 7.437141418457031, -4.525400638580322)) + + Bomb(position=(-6, 7.5, -4), bomb_type=type, owner=owner, + source_player=source_player, velocity=(19, y, z)).autoretain() + bs.timer(0.6, babase.Call(self.launch_bomb_byA, + owner, type, source_player, count-1)) + else: + return + + def launch_bomb_byB(self, owner, type, source_player, count): + if count > 0: + y = random.randrange(2, 9, 2) + z = random.randrange(-4, 6) + self.fake_explosion( + (5.708631629943848, 7.437141418457031, -4.525400638580322)) + + Bomb(position=(6, 7.5, -4), bomb_type=type, owner=owner, + source_player=source_player, velocity=(-19, y, z)).autoretain() + bs.timer(0.6, babase.Call(self.launch_bomb_byB, + owner, type, source_player, count-1)) + else: + return + + def fake_explosion(self, position: Sequence[float]): + explosion = bs.newnode('explosion', + attrs={'position': position, + 'radius': 1, 'big': False}) + bs.timer(0.4, explosion.delete) + sounds = ['explosion0'+str(n) for n in range(1, 6)] + sound = random.choice(sounds) + bs.getsound(sound).play() + + def create_canon_A(self): + shared = SharedObjects.get() + canon_load_mat = bs.Material() + factory = BombFactory.get() + + canon_load_mat.add_actions( + + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + + )) + canon_load_mat.add_actions( + conditions=('they_have_material', factory.bomb_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True), + ('call', 'at_connect', babase.Call(self._handle_canon_load_A)) + ), + ) + self.ud_1_r = bs.newnode('region', attrs={'position': (-8.908631629943848, 7.337141418457031, - + 4.525400638580322), 'scale': (2, 1, 1), 'type': 'box', 'materials': [canon_load_mat]}) + + self.node = bs.newnode('shield', + delegate=self, + attrs={ + 'position': (-8.308631629943848, 7.337141418457031, -4.525400638580322), + 'color': (0.3, 0.2, 2.8), + 'radius': 1.3 + }) + self.canon = bs.newnode('text', + attrs={ + 'text': '___________', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.3, 0.3, 0.8), + 'scale': 0.019, + 'h_align': 'left', + 'position': (-8.388631629943848, 7.837141418457031, -4.525400638580322) + }) + self.canon_ = bs.newnode('text', + attrs={ + 'text': '_________', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.3, 0.3, 0.8), + 'scale': 0.019, + 'h_align': 'left', + 'position': (-7.888631629943848, 7.237141418457031, -4.525400638580322) + }) + self.curve = bs.newnode('text', + attrs={ + 'text': '/\n', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.3, 0.3, 0.8), + 'scale': 0.019, + 'h_align': 'left', + 'position': (-8.788631629943848, 7.237141418457031, -4.525400638580322) + }) + + def create_canon_B(self): + shared = SharedObjects.get() + canon_load_mat = bs.Material() + factory = BombFactory.get() + + canon_load_mat.add_actions( + + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + + )) + canon_load_mat.add_actions( + conditions=('they_have_material', factory.bomb_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True), + ('call', 'at_connect', babase.Call(self._handle_canon_load_B)) + ), + ) + self.ud_1_r2 = bs.newnode('region', attrs={'position': ( + 8.908631629943848+0.81, 7.327141418457031, -4.525400638580322), 'scale': (2, 1, 1), 'type': 'box', 'materials': [canon_load_mat]}) + + self.node2 = bs.newnode('shield', + delegate=self, + attrs={ + 'position': (8.308631629943848+0.81, 7.327141418457031, -4.525400638580322), + 'color': (2.3, 0.2, 0.3), + 'radius': 1.3 + }) + self.canon2 = bs.newnode('text', + attrs={ + 'text': '___________', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.8, 0.3, 0.3), + 'scale': 0.019, + 'h_align': 'right', + 'position': (8.388631629943848+0.81, 7.837141418457031, -4.525400638580322) + }) + self.canon_2 = bs.newnode('text', + attrs={ + 'text': '_________', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.8, 0.3, 0.3), + 'scale': 0.019, + 'h_align': 'right', + 'position': (7.888631629943848+0.81, 7.237141418457031, -4.525400638580322) + }) + self.curve2 = bs.newnode('text', + attrs={ + 'text': '\\', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.8, 0.3, 0.3), + 'scale': 0.019, + 'h_align': 'right', + 'position': (8.788631629943848+0.81, 7.237141418457031, -4.525400638580322) + }) + + def create_wall(self): + shared = SharedObjects.get() + factory = BombFactory.get() + mat = bs.Material() + mat.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + )) + mat.add_actions( + conditions=( + ('they_have_material', factory.bomb_material)), + actions=( + ('modify_part_collision', 'collide', False) + )) + self.wall = bs.newnode('region', attrs={'position': ( + 0.61877517104148865, 4.312626838684082, -8.68477725982666), 'scale': (3, 7, 27), 'type': 'box', 'materials': [mat]}) diff --git a/plugins/minigames/laser_tracer.py b/plugins/minigames/laser_tracer.py new file mode 100644 index 0000000..e6e6067 --- /dev/null +++ b/plugins/minigames/laser_tracer.py @@ -0,0 +1,689 @@ + + +# Released under the MIT License. See LICENSE for details. +# https://youtu.be/wTgwZKiykQw?si=Cr0ybDYAcKCUNFN4 +# https://discord.gg/ucyaesh +# https://bombsquad-community.web.app/home +# by: Mr.Smoothy + +"""Elimination mini-game.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, Union +import random + + +class Icon(bs.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: tuple[float, float], + scale: float, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0): + super().__init__() + + self._player = player + self._show_lives = show_lives + self._show_death = show_death + self._name_scale = name_scale + self._outline_tex = bs.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = bs.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + self._name_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': flatness, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + if self._show_lives: + self._lives_text = bs.newnode('text', + owner=self.node, + attrs={ + 'text': 'x0', + 'color': (1, 1, 0.5), + 'h_align': 'left', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_position_and_scale(position, scale) + + def set_position_and_scale(self, position: tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + if self._show_lives: + self._lives_text.position = (position[0] + scale * 10.0, + position[1] - scale * 43.0) + self._lives_text.scale = 1.0 * scale + + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + if self._player: + lives = self._player.lives + else: + lives = 0 + if self._show_lives: + if lives > 0: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + def handle_player_spawned(self) -> None: + """Our player spawned; hooray!""" + if not self.node: + return + self.node.opacity = 1.0 + self.update_for_lives() + + def handle_player_died(self) -> None: + """Well poo; our player died.""" + if not self.node: + return + if self._show_death: + bs.animate( + self.node, 'opacity', { + 0.00: 1.0, + 0.05: 0.0, + 0.10: 1.0, + 0.15: 0.0, + 0.20: 1.0, + 0.25: 0.0, + 0.30: 1.0, + 0.35: 0.0, + 0.40: 1.0, + 0.45: 0.0, + 0.50: 1.0, + 0.55: 0.2 + }) + lives = self._player.lives + if lives == 0: + bs.timer(0.6, self.update_for_lives) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.lives = 0 + self.icons: list[Icon] = [] + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.spawn_order: list[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class LasorTracerGame(bs.TeamGameActivity[Player, Team]): + """Game type where last player(s) left alive win.""" + + name = 'Laser Tracer' + description = 'Last remaining alive wins.' + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + none_is_winner=True) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + 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.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Lives', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ["Courtyard"] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._start_time: Optional[float] = None + self._vs_text: Optional[bs.Actor] = None + self._round_end_timer: Optional[bs.Timer] = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = 1 + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False)) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + self.laser_material = bs.Material() + self.laser_material.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_part_collision', 'collide', True), + ('message', 'their_node', 'at_connect', bs.DieMessage())) + ) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Last team standing wins.' if isinstance( + self.session, bs.DualTeamSession) else 'Last one standing wins.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'last team standing wins' if isinstance( + self.session, bs.DualTeamSession) else 'last one standing wins' + + def on_player_join(self, player: Player) -> None: + player.lives = self._lives_per_player + + if self._solo_mode: + player.team.spawn_order.append(player) + self._update_solo_mode() + else: + # Create our icon and spawn. + # player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + # Don't waste time doing this until begin. + if self.has_begun(): + self._update_icons() + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + self.add_wall() + self.create_laser() + if self._solo_mode: + self._vs_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': babase.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + bs.timer(1.0, self._update, repeat=True) + + def _update_solo_mode(self) -> None: + # For both teams, find the first player on the spawn order list with + # lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + break + + def _update_icons(self) -> None: + return + # lets do nothing ;Eat 5 Star + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> Optional[babase.Vec3]: + del player # Unused. + + # In solo-mode, if there's an existing live player on the map, spawn at + # whichever spot is farthest from them (keeps the action spread out). + if self._solo_mode: + living_player = None + living_player_pos = None + for team in self.teams: + for tplayer in team.players: + if tplayer.is_alive(): + assert tplayer.node + ppos = tplayer.node.position + living_player = tplayer + living_player_pos = ppos + break + if living_player: + assert living_player_pos is not None + player_pos = babase.Vec3(living_player_pos) + points: list[tuple[float, babase.Vec3]] = [] + for team in self.teams: + start_pos = babase.Vec3( + self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos)) + # Hmm.. we need to sorting vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def spawn_player(self, player: Player) -> bs.Actor: + actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) + actor.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + return actor + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + popuptext.PopupText('x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=player.node.position).autoretain() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + player.icons = [] + + # Remove us from spawn-order. + if self._solo_mode: + if player in player.team.spawn_order: + player.team.spawn_order.remove(player) + + # Update icons in a moment since our team will be gone from the + # list then. + bs.timer(0, self._update_icons) + + # If the player to leave was the last in spawn order and had + # their final turn currently in-progress, mark the survival time + # for their team. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - self._start_time) + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + babase.print_error( + "Got lives < 0 in Elim; this shouldn't happen. solo:" + + str(self._solo_mode)) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - + self._start_time) + else: + # Otherwise, in regular mode, respawn. + if not self._solo_mode: + self.respawn_player(player) + + # In solo, put ourself at the back of the spawn order. + if self._solo_mode: + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) + + def _update(self) -> None: + if self._solo_mode: + # For both teams, find the first player on the spawn order + # list with lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + self._update_icons() + break + + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0.5, self.end_game) + + def _get_living_teams(self) -> list[Team]: + return [ + team for team in self.teams + if len(team.players) > 0 and any(player.lives > 0 + for player in team.players) + ] + + def end_game(self) -> None: + if self.has_ended(): + return + results = bs.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.survival_seconds) + self.end(results=results) + + def add_wall(self): + # FIXME: Chop this into vr and non-vr chunks. + shared = SharedObjects.get() + pwm = bs.Material() + cwwm = bs.Material() + pwm.add_actions( + actions=('modify_part_collision', 'friction', 0.0)) + # anything that needs to hit the wall should apply this. + + pwm.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=('modify_part_collision', 'collide', True)) + cmesh = bs.getcollisionmesh('courtyardPlayerWall') + self.player_wall = bs.newnode( + 'terrain', + attrs={ + 'collision_mesh': cmesh, + 'affect_bg_dynamics': False, + 'materials': [pwm] + }) + + def create_laser(self) -> None: + bs.timer(6, babase.Call(self.LRlaser, True)) + + bs.timer(7, babase.Call(self.UDlaser, True)) + bs.timer(30, babase.Call(self.create_laser)) + + def LRlaser(self, left): + ud_1_r = bs.newnode('region', attrs={'position': (-5, 2.6, 0), 'scale': ( + 0.1, 0.6, 15), 'type': 'box', 'materials': [self.laser_material]}) + shields = [] + x = -6 + for i in range(0, 30): + x = x+0.4 + node = bs.newnode('shield', owner=ud_1_r, attrs={ + 'color': (1, 0, 0), 'radius': 0.28}) + mnode = bs.newnode('math', + owner=ud_1_r, + attrs={ + 'input1': (0, 0.0, x), + 'operation': 'add' + }) + ud_1_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', node, 'position') + + _rcombine = bs.newnode('combine', + owner=ud_1_r, + attrs={ + 'input1': 2.6, + 'input2': -2, + 'size': 3 + }) + if left: + x1 = -10 + x2 = 10 + else: + x1 = 10 + x2 = -10 + bs.animate(_rcombine, 'input0', { + 0: x1, + 20: x2 + }) + _rcombine.connectattr('output', ud_1_r, 'position') + bs.timer(20, babase.Call(ud_1_r.delete)) + t = random.randrange(7, 13) + bs.timer(t, babase.Call(self.LRlaser, random.randrange(0, 2))) + + def UDlaser(self, up): + ud_2_r = bs.newnode('region', attrs={'position': (-3, 2.6, -6), 'scale': ( + 20, 0.6, 0.1), 'type': 'box', 'materials': [self.laser_material]}) + shields = [] + x = -6 + for i in range(0, 40): + x = x+0.4 + node = bs.newnode('shield', owner=ud_2_r, attrs={ + 'color': (1, 0, 0), 'radius': 0.28}) + mnode = bs.newnode('math', + owner=ud_2_r, + attrs={ + 'input1': (x, 0.0, 0), + 'operation': 'add' + }) + ud_2_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', node, 'position') + + _rcombine = bs.newnode('combine', + owner=ud_2_r, + attrs={ + 'input0': -2, + 'input1': 2.6, + 'size': 3 + }) + if up: + x1 = -9 + x2 = 6 + else: + x1 = 6 + x2 = -9 + bs.animate(_rcombine, 'input2', { + 0: x1, + 17: x2 + }) + _rcombine.connectattr('output', ud_2_r, 'position') + + bs.timer(17, babase.Call(ud_2_r.delete)) + t = random.randrange(6, 13) + bs.timer(t, babase.Call(self.UDlaser, random.randrange(0, 2))) diff --git a/plugins/utilities.json b/plugins/utilities.json index 115e3de..21ab43a 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -676,6 +676,12 @@ } ], "versions": { + "1.0.2": { + "api_version": 8, + "commit_sha": "505c948", + "released_on": "09-06-2024", + "md5sum": "ae32962255c357b29bd9c46c0551a19c" + }, "1.0.1": { "api_version": 8, "commit_sha": "b089293", diff --git a/plugins/utilities/file_share.py b/plugins/utilities/file_share.py index 55f1f5e..4b9a546 100644 --- a/plugins/utilities/file_share.py +++ b/plugins/utilities/file_share.py @@ -1,6 +1,6 @@ # ba_meta require api 8 ''' -File Share Mod for BombSquad 1.7.23 and above. +File Share Mod for BombSquad 1.7.30 and above. https://youtu.be/qtGsFU4cgic https://discord.gg/ucyaesh by : Mr.Smoothy @@ -19,7 +19,7 @@ import _baplus import _babase import babase from bauiv1lib.fileselector import FileSelectorWindow -from bauiv1lib.promocode import PromoCodeWindow +from bauiv1lib.sendinfo import SendInfoWindow from bauiv1lib.confirm import ConfirmWindow import bauiv1 as bui import os @@ -55,14 +55,16 @@ class UploadConfirmation(ConfirmWindow): origin_widget: bui.Widget | None = None, ): - super().__init__(text=text, action=action, origin_widget=origin_widget, ok_text=ok_text) + super().__init__(text=text, action=action, + origin_widget=origin_widget, ok_text=ok_text) self.status = status self.file_path = file_path def _ok(self) -> None: if self.status == "init": self._cancel() - UploadConfirmation("", "uploading", text="Uploading file wait !", ok_text="Wait") + UploadConfirmation( + "", "uploading", text="Uploading file wait !", ok_text="Wait") self._upload_file() elif self.status == "uploading": @@ -83,10 +85,10 @@ class UploadConfirmation(ConfirmWindow): ShowURLWindow(url) -class InputWindow(PromoCodeWindow): +class InputWindow(SendInfoWindow): def __init__( self, modal: bool = True, origin_widget: bui.Widget | None = None, path=None): - super().__init__(modal=modal, origin_widget=origin_widget) + super().__init__(modal=modal, legacy_code_mode=True, origin_widget=origin_widget) bui.textwidget(edit=self._text_field, max_chars=300) self._path = path self.message_widget = bui.textwidget( @@ -101,12 +103,15 @@ class InputWindow(PromoCodeWindow): def _do_enter(self): url = bui.textwidget(query=self._text_field) if self._path and self._path != "/bombsquad": - bui.textwidget(edit=self.message_widget, text="downloading.... wait...") + bui.textwidget(edit=self.message_widget, + text="downloading.... wait...") bui.screenmessage("Downloading started") - thread = Thread(target=handle_download, args=(url, self._path, self.on_download,)) + thread = Thread(target=handle_download, args=( + url, self._path, self.on_download,)) thread.start() else: - bui.textwidget(edit=self.message_widget, text="First select folder were to save file.") + bui.textwidget(edit=self.message_widget, + text="First select folder were to save file.") self.close() def on_download(self, output_path): @@ -306,7 +311,8 @@ def handle_upload(file, callback, root_widget): _babase.pushcall(Call(callback, json.loads(response.read().decode( 'utf-8'))["link"], root_widget), from_other_thread=True) else: - bui.screenmessage(f"Failed to Upload file. Status code: {response.getcode()}") + bui.screenmessage( + f"Failed to Upload file. Status code: {response.getcode()}") except urllib.error.URLError as e: bui.screenmessage(f"Error occurred: {e}") @@ -318,22 +324,26 @@ def handle_download(url, path, callback): if response.getcode() == 200: # Read the filename from the Content-Disposition header filename = None - content_disposition = response.headers.get('Content-Disposition', '') + content_disposition = response.headers.get( + 'Content-Disposition', '') match = re.search(r'filename\*?=(.+)', content_disposition) if match: - filename = urllib.parse.unquote(match.group(1), encoding='utf-8') + filename = urllib.parse.unquote( + match.group(1), encoding='utf-8') filename = filename.replace("UTF-8''", '') output_path = os.path.join(path, filename) with open(output_path, 'wb') as file: file.write(response.read()) - _babase.pushcall(Call(callback, output_path), from_other_thread=True) + _babase.pushcall(Call(callback, output_path), + from_other_thread=True) print(f"File downloaded and saved to: {output_path}") else: - print(f"Failed to download file. Status code: {response.getcode()}") + print( + f"Failed to download file. Status code: {response.getcode()}") except urllib.error.URLError as e: # bui.screenmessage(f'Error occured {e}') print(f"Error occurred: {e}")