diff --git a/plugins/minigames.json b/plugins/minigames.json index 989ca69..210e4fe 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -3,6 +3,139 @@ "description": "Minigames", "plugins_base_url": "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/minigames", "plugins": { + "soccer": { + "description": "Shoot the ball in left or right edge of the map to score", + "external_url": "", + "authors": [ + { + "name": "Stary_Agent", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "e59073b", + "released_on": "02-11-2022", + "md5sum": "ceb7ac417c85396722fa9f7fee3cfc01" + } + } + }, + "simon_says": { + "description": "You better do what Simon says", + "external_url": "", + "authors": [ + { + "name": "Unknown", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "def39482b339095a7affb574af829ea0" + } + } + }, + "squid_race": { + "description": "Race inspired by the squid games", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "! JoseANG3L#0268" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "7ec43481e6b35b4da4f6425977ee0004" + } + } + }, + "zombie_horde": { + "description": "Fight or defend against horde of zombies", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "! JoseANG3L#0268" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "671e7b98a9d3eb0aeb7e47448db25af9" + } + } + }, + "meteor_shower": { + "description": "MeteorShower with many different bombs", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "! JoseANG3L#0268" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "1ce8c1a3f3ec240af39b7a73375e7891" + } + } + }, + "boxing": { + "description": "Simple boxing minigame", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "! JoseANG3L#0268" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "c1b615df09886ba05622c4e8b4f6be40" + } + } + }, + "basket_bomb": { + "description": "Score all the baskets and be the MVP.", + "external_url": "", + "authors": [ + { + "name": "Unknown", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "7ec87edbdd5183ec4b2daffad0019820" + } + } + }, "ultimate_last_stand": { "description": "Bring Last Stand minigame into team fight and FFA.", "external_url": "", @@ -97,25 +230,6 @@ "md5sum": "c84b7f415de5d3e9189ee73fc0e3ce93" } } - }, - "soccer": { - "description": "Shoot the ball in left or right edge of the map to score", - "external_url": "", - "authors": [ - { - "name": "Stary_Agent", - "email": "", - "discord": "" - } - ], - "versions": { - "1.0.0": { - "api_version": 7, - "commit_sha": "e59073b", - "released_on": "02-11-2022", - "md5sum": "ceb7ac417c85396722fa9f7fee3cfc01" - } - } } } } \ No newline at end of file diff --git a/plugins/minigames/basket_bomb.py b/plugins/minigames/basket_bomb.py new file mode 100644 index 0000000..78c35b8 --- /dev/null +++ b/plugins/minigames/basket_bomb.py @@ -0,0 +1,771 @@ +# Released under the MIT License. See LICENSE for details. +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.gameutils import SharedObjects +from bastd.actor import playerspaz as ps +from bastd import maps + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + +bsuSpaz = None + + +def getlanguage(text, sub: str = ''): + lang = _ba.app.lang.language + translate = { + "Name": + {"Spanish": "Baloncesto", + "English": "Basketbomb", + "Portuguese": "Basketbomb"}, + "Info": + {"Spanish": "Anota todas las canastas y sé el MVP.", + "English": "Score all the baskets and be the MVP.", + "Portuguese": "Marque cada cesta e seja o MVP."}, + "Info-Short": + {"Spanish": f"Anota {sub} canasta(s) para ganar", + "English": f"Score {sub} baskets to win", + "Portuguese": f"Cestas de {sub} pontos para ganhar"}, + "S: Powerups": + {"Spanish": "Aparecer Potenciadores", + "English": "Powerups Spawn", + "Portuguese": "Habilitar Potenciadores"}, + "S: Velocity": + {"Spanish": "Activar velocidad", + "English": "Enable speed", + "Portuguese": "Ativar velocidade"}, + } + + languages = ['Spanish', 'Portuguese', 'English'] + if lang not in languages: + lang = 'English' + + if text not in translate: + return text + return translate[text][lang] + + +class BallDiedMessage: + def __init__(self, ball: Ball): + self.ball = ball + + +class Ball(ba.Actor): + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + velocty = (0.0, 8.0, 0.0) + _scale = 1.2 + + self._spawn_pos = (position[0], position[1] + 0.5, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + + assert activity is not None + assert isinstance(activity, BasketGame) + + pmats = [shared.object_material, activity.ball_material] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': activity.ball_model, + 'color_texture': activity.ball_tex, + 'body': 'sphere', + 'reflection': 'soft', + 'body_scale': 1.0 * _scale, + 'reflection_scale': [1.3], + 'shadow_size': 1.0, + 'gravity_scale': 0.92, + 'density': max(0.4 * _scale, 0.3), + 'position': self._spawn_pos, + 'velocity': velocty, + 'materials': pmats}) + self.scale = scale = 0.25 * _scale + ba.animate(self.node, 'model_scale', {0: 0, 0.2: scale*1.3, 0.26: scale}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(BallDiedMessage(self)) + + elif isinstance(msg, ba.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + self.node.velocity = (0.0, 0.0, 0.0) + + elif isinstance(msg, ba.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]) + + 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(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +class Points: + postes = dict() + # 10.736066818237305, 0.3002409040927887, 0.5281256437301636 + postes['pal_0'] = (10.64702320098877, 0.0000000000000000, 0.0000000000000000) + postes['pal_1'] = (-10.64702320098877, 0.0000000000000000, 0.0000000000000000) + +# ba_meta export game + + +class BasketGame(ba.TeamGameActivity[Player, Team]): + + name = getlanguage('Name') + description = getlanguage('Info') + available_settings = [ + ba.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting(getlanguage('S: Powerups'), default=True), + ba.BoolSetting(getlanguage('S: Velocity'), default=False), + ba.BoolSetting('Epic Mode', default=False), + ] + default_music = ba.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['BasketBall Stadium', 'BasketBall Stadium V2'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = ba.getsound('cheer') + self._chant_sound = ba.getsound('crowdChant') + self._foghorn_sound = ba.getsound('foghorn') + self._swipsound = ba.getsound('swip') + self._whistle_sound = ba.getsound('refWhistle') + self.ball_model = ba.getmodel('shield') + self.ball_tex = ba.gettexture('fontExtras3') + self._ball_sound = ba.getsound('bunnyJump') + self._powerups = bool(settings[getlanguage('S: Powerups')]) + self._speed = bool(settings[getlanguage('S: Velocity')]) + self._epic_mode = bool(settings['Epic Mode']) + self.slow_motion = self._epic_mode + + self.ball_material = ba.Material() + self.ball_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.ball_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.ball_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.ball_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._ball_sound, 0.2, 5)) + + # Keep track of which player last touched the ball + self.ball_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_ball_player_collide), )) + + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.ball_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self._ball_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[ba.NodeActor]] = None + self._ball: Optional[Ball] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + return getlanguage('Info-Short', sub=self._score_to_win) + + def get_instance_description_short(self) -> Union[str, Sequence]: + return getlanguage('Info-Short', sub=self._score_to_win) + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + + if self._powerups: + self.setup_standard_powerup_drops() + + self._ball_spawn_pos = self.map.get_flag_position(None) + self._spawn_ball() + + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': defs.boxes['goal1'][0:3], + 'scale': defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials': [] + }))) + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': defs.boxes['goal2'][0:3], + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': [] + }))) + self._update_scoreboard() + ba.playsound(self._chant_sound) + + for id, team in enumerate(self.teams): + self.postes(id) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_ball_player_collide(self) -> None: + collision = ba.getcollision() + try: + ball = collision.sourcenode.getdelegate(Ball, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + ball.last_players_to_touch[player.team.id] = player + + def _kill_ball(self) -> None: + self._ball = None + + def _handle_score(self, team_index: int = None) -> None: + assert self._ball is not None + assert self._score_regions is not None + + if self._ball.scored: + return + + region = ba.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + if team_index is not None: + index = team_index + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + + if (scoring_team.id in self._ball.last_players_to_touch + and self._ball.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._ball.last_players_to_touch[scoring_team.id], + 100, big_message=True) + + if team.score >= self._score_to_win: + self.end_game() + + # ba.playsound(self._foghorn_sound) + ba.playsound(self._cheer_sound) + + self._ball.scored = True + + # Kill the ball (it'll respawn itself shortly). + ba.timer(1.0, self._kill_ball) + + light = ba.newnode('light', + attrs={ + 'position': ba.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + ba.timer(1.0, light.delete) + + ba.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for id, team in enumerate(self.teams): + self._scoreboard.set_team_value(team, team.score, winscore) + # self.postes(id) + + def spawn_player(self, player: Player) -> ba.Actor: + if bsuSpaz is None: + spaz = self.spawn_player_spaz(player) + else: + ps.PlayerSpaz = bsuSpaz.BskSpaz + spaz = self.spawn_player_spaz(player) + ps.PlayerSpaz = bsuSpaz.OldPlayerSpaz + + if self._speed: + spaz.node.hockey = True + return spaz + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + elif isinstance(msg, BallDiedMessage): + if not self.has_ended(): + ba.timer(3.0, self._spawn_ball) + else: + super().handlemessage(msg) + + def postes(self, team_id: int): + if not hasattr(self._map, 'poste_'+str(team_id)): + setattr(self._map, 'poste_'+str(team_id), + Palos(team=team_id, + position=Points.postes['pal_' + + str(team_id)]).autoretain()) + + def _flash_ball_spawn(self) -> None: + light = ba.newnode('light', + attrs={ + 'position': self._ball_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _spawn_ball(self) -> None: + ba.playsound(self._swipsound) + ba.playsound(self._whistle_sound) + self._flash_ball_spawn() + assert self._ball_spawn_pos is not None + self._ball = Ball(position=self._ball_spawn_pos) + + +class Aro(ba.Actor): + def __init__(self, team: int = 0, + position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + act = self.getactivity() + shared = SharedObjects.get() + setattr(self, 'team', team) + setattr(self, 'locs', []) + + # Material Para; Traspasar Objetos + self.no_collision = ba.Material() + self.no_collision.add_actions( + actions=(('modify_part_collision', 'collide', False))) + + self.collision = ba.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + # Score + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', act.ball_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._annotation))) + + self._spawn_pos = (position[0], position[1], position[2]) + self._materials_region0 = [self.collision, + shared.footing_material] + + model = None + tex = ba.gettexture('null') + + pmats = [self.no_collision] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': model, + 'color_texture': tex, + 'body': 'box', + 'reflection': 'soft', + 'reflection_scale': [1.5], + 'shadow_size': 0.1, + 'position': self._spawn_pos, + 'materials': pmats}) + + self.scale = scale = 1.4 + ba.animate(self.node, 'model_scale', {0: 0}) + + pos = (position[0], position[1]+0.6, position[2]) + self.regions: List[ba.Node] = [ + ba.newnode('region', + attrs={'position': position, + 'scale': (0.6, 0.05, 0.6), + 'type': 'box', + 'materials': self._materials_region0}), + + ba.newnode('region', + attrs={'position': pos, + 'scale': (0.5, 0.3, 0.9), + 'type': 'box', + 'materials': [self._score_region_material]}) + ] + self.regions[0].connectattr('position', self.node, 'position') + #self.regions[0].connectattr('position', self.regions[1], 'position') + + locs_count = 9 + pos = list(position) + + try: + id = 0 if team == 1 else 1 + color = act.teams[id].color + except: + color = (1, 1, 1) + + while locs_count > 1: + scale = (1.5 * 0.1 * locs_count) + 0.8 + + self.locs.append(ba.newnode('locator', + owner=self.node, + attrs={'shape': 'circleOutline', + 'position': pos, + 'color': color, + 'opacity': 1.0, + 'size': [scale], + 'draw_beauty': True, + 'additive': False})) + + pos[1] -= 0.1 + locs_count -= 1 + + def _annotation(self): + assert len(self.regions) >= 2 + ball = self.getactivity()._ball + + if ball: + p = self.regions[0].position + ball.node.position = p + ball.node.velocity = (0.0, 0.0, 0.0) + + act = self.getactivity() + act._handle_score(self.team) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) + + +class Cuadro(ba.Actor): + def __init__(self, team: int = 0, + position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + act = self.getactivity() + shared = SharedObjects.get() + setattr(self, 'locs', []) + + self.collision = ba.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + pos = (position[0], position[1]+0.9, position[2]+1.5) + self.region: ba.Node = ba.newnode('region', + attrs={'position': pos, + 'scale': (0.5, 2.7, 2.5), + 'type': 'box', + 'materials': [self.collision, + shared.footing_material]}) + + #self.shield = ba.newnode('shield', attrs={'radius': 1.0, 'color': (0,10,0)}) + #self.region.connectattr('position', self.shield, 'position') + + position = (position[0], position[1], position[2]+0.09) + pos = list(position) + oldpos = list(position) + old_count = 14 + + count = old_count + count_y = 9 + + try: + id = 0 if team == 1 else 1 + color = act.teams[id].color + except: + color = (1, 1, 1) + + while (count_y != 1): + + while (count != 1): + pos[2] += 0.19 + + self.locs.append( + ba.newnode('locator', + owner=self.region, + attrs={'shape': 'circle', + 'position': pos, + 'size': [0.5], + 'color': color, + 'opacity': 1.0, + 'draw_beauty': True, + 'additive': False})) + count -= 1 + + count = old_count + pos[1] += 0.2 + pos[2] = oldpos[2] + count_y -= 1 + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) + + +class Palos(ba.Actor): + def __init__(self, team: int = 0, + position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + self._pos = position + self.aro = None + self.cua = None + + # Material Para; Traspasar Objetos + self.no_collision = ba.Material() + self.no_collision.add_actions( + actions=(('modify_part_collision', 'collide', False))) + + # + self.collision = ba.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[2]+2.5, position[2]) + + model = ba.getmodel('flagPole') + tex = ba.gettexture('flagPoleColor') + + pmats = [self.no_collision] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': model, + 'color_texture': tex, + 'body': 'puck', + 'reflection': 'soft', + 'reflection_scale': [2.6], + 'shadow_size': 0, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + self.scale = scale = 4.0 + ba.animate(self.node, 'model_scale', {0: scale}) + + self.loc = ba.newnode('locator', + owner=self.node, + attrs={'shape': 'circle', + 'position': position, + 'color': (1, 1, 0), + 'opacity': 1.0, + 'draw_beauty': False, + 'additive': True}) + + self._y = _y = 0.30 + _x = -0.25 if team == 1 else 0.25 + _pos = (position[0]+_x, position[1]-1.5 + _y, position[2]) + self.region = ba.newnode('region', + attrs={ + 'position': _pos, + 'scale': (0.4, 8, 0.4), + 'type': 'box', + 'materials': [self.collision]}) + self.region.connectattr('position', self.node, 'position') + + _y = self._y + position = self._pos + if team == 0: + pos = (position[0]-0.8, position[1] + 2.0 + _y, position[2]) + else: + pos = (position[0]+0.8, position[1] + 2.0 + _y, position[2]) + + if self.aro is None: + self.aro = Aro(team, pos).autoretain() + + if self.cua is None: + pos = (position[0], position[1] + 1.8 + _y, position[2]-1.4) + self.cua = Cuadro(team, pos).autoretain() + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) + + +class BasketMap(maps.FootballStadium): + name = 'BasketBall Stadium' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [] + + def __init__(self) -> None: + super().__init__() + + gnode = ba.getactivity().globalsnode + gnode.tint = [(0.806, 0.8, 1.0476), (1.3, 1.2, 1.0)][0] + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + + +class BasketMapV2(maps.HockeyStadium): + name = 'BasketBall Stadium V2' + + def __init__(self) -> None: + super().__init__() + + shared = SharedObjects.get() + self.node.materials = [shared.footing_material] + self.node.collide_model = ba.getcollidemodel('footballStadiumCollide') + self.node.model = None + self.stands.model = None + self.floor.reflection = 'soft' + self.floor.reflection_scale = [1.6] + self.floor.color = (1.1, 0.05, 0.8) + + self.background = ba.newnode('terrain', + attrs={'model': ba.getmodel('thePadBG'), + 'lighting': False, + 'background': True, + 'color': (1.0, 0.2, 1.0), + 'color_texture': ba.gettexture('menuBG')}) + + gnode = ba.getactivity().globalsnode + gnode.floor_reflection = True + gnode.debris_friction = 0.3 + gnode.debris_kill_height = -0.3 + gnode.tint = [(1.2, 1.3, 1.33), (0.7, 0.9, 1.0)][1] + gnode.ambient_color = (1.15, 1.25, 1.6) + gnode.vignette_outer = (0.66, 0.67, 0.73) + gnode.vignette_inner = (0.93, 0.93, 0.95) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + self.is_hockey = False + + ################## + self.collision = ba.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + self.regions: List[ba.Node] = [ + ba.newnode('region', + attrs={'position': (12.676897048950195, 0.2997918128967285, 5.583303928375244), + 'scale': (1.01, 12, 28), + 'type': 'box', + 'materials': [self.collision]}), + + ba.newnode('region', + attrs={'position': (11.871315956115723, 0.29975247383117676, 5.711406707763672), + 'scale': (50, 12, 0.9), + 'type': 'box', + 'materials': [self.collision]}), + + ba.newnode('region', + attrs={'position': (-12.776557922363281, 0.30036890506744385, 4.96237850189209), + 'scale': (1.01, 12, 28), + 'type': 'box', + 'materials': [self.collision]}), + ] + + +ba._map.register_map(BasketMap) +ba._map.register_map(BasketMapV2) diff --git a/plugins/minigames/boxing.py b/plugins/minigames/boxing.py new file mode 100644 index 0000000..35a7c75 --- /dev/null +++ b/plugins/minigames/boxing.py @@ -0,0 +1,240 @@ +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.game.deathmatch import DeathMatchGame + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = ba.app.lang.language + +if lang == 'Spanish': + name = 'Super Boxeo' + description = ('¡Sin bombas!\n' + '¡Noquea a los enemigos con tus propias manos!\n') + super_jump_text = 'Super Salto' + enable_powerups = 'Habilitar Potenciadores' +else: + name = 'Super Boxing' + description = ('No bombs!\n' + 'Knock out your enemies using your bare hands!\n') + super_jump_text = 'Super Jump' + enable_powerups = 'Enable Powerups' + + +class NewPlayerSpaz(PlayerSpaz): + + def __init__(self, + player: ba.Player, + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + character: str = 'Spaz', + powerups_expire: bool = True, + super_jump: bool = False): + super().__init__(player=player, + color=color, + highlight=highlight, + character=character, + powerups_expire=powerups_expire) + from bastd.gameutils import SharedObjects + shared = SharedObjects.get() + self._super_jump = super_jump + self.jump_mode = False + self.super_jump_material = ba.Material() + self.super_jump_material.add_actions( + conditions=('they_have_material', shared.footing_material), + actions=( + ('call', 'at_connect', ba.Call(self.jump_state, True)), + ('call', 'at_disconnect', ba.Call(self.jump_state, False)) + ), + ) + self.node.roller_materials += (self.super_jump_material, ) + + def jump_state(self, mode: bool) -> None: + self.jump_mode = mode + + def on_jump_press(self) -> None: + """ + Called to 'press jump' on this spaz; + used by player or AI connections. + """ + if not self.node: + return + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + if t_ms - self.last_jump_time_ms >= self._jump_cooldown: + self.node.jump_pressed = True + self.last_jump_time_ms = t_ms + if self._player.is_alive() and self.jump_mode and ( + self._super_jump): + def do_jump(): + self.node.handlemessage( + 'impulse', + self.node.position[0], + self.node.position[1], + self.node.position[2], + 0, 0, 0, 150, 150, 0, 0, 0, 1, 0 + ) + ba.timer(0.0, do_jump) + ba.timer(0.1, do_jump) + ba.timer(0.2, do_jump) + self._turbo_filter_add_press('jump') + + +# ba_meta export game +class BoxingGame(DeathMatchGame): + + name = name + description = description + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session] + ) -> list[ba.Setting]: + settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting(super_jump_text, default=False), + ba.BoolSetting(enable_powerups, default=False), + ba.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, ba.FreeForAllSession): + settings.append( + ba.BoolSetting('Allow Negative Scores', default=False) + ) + + return settings + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: int | None = None + self._dingsound = ba.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) + ) + self._super_jump = bool(settings[super_jump_text]) + self._enable_powerups = bool(settings[enable_powerups]) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + ba.MusicType.EPIC if self._epic_mode else ba.MusicType.TO_THE_DEATH + ) + + def on_begin(self) -> None: + ba.TeamGameActivity.on_begin(self) + self.setup_standard_time_limit(self._time_limit) + if self._enable_powerups: + 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() + + def _standard_drop_powerup(self, index: int, expire: bool = True) -> None: + # pylint: disable=cyclic-import + from bastd.actor.powerupbox import PowerupBox, PowerupBoxFactory + + PowerupBox( + position=self.map.powerup_spawn_points[index], + poweruptype=PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=['triple_bombs', 'ice_bombs', 'impact_bombs', + 'land_mines', 'sticky_bombs', 'punch'] + ), + expire=expire, + ).autoretain() + + def spawn_player(self, player: Player) -> ba.Actor: + import random + from ba import _math + from ba._gameutils import animate + from ba._coopsession import CoopSession + + if isinstance(self.session, ba.DualTeamSession): + position = self.map.get_start_position(player.team.id) + else: + # otherwise do free-for-all spawn locations + position = self.map.get_ffa_start_position(self.players) + angle = None + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = ba.safecolor(color, target_intensity=0.75) + + spaz = NewPlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player, + super_jump=self._super_jump) + + player.actor = spaz + assert spaz.node + + spaz.node.name = name + spaz.node.name_color = display_color + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + ba.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + ba.playsound(self._spawn_sound, 1, position=spaz.node.position) + light = ba.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + ba.timer(0.5, light.delete) + + # custom + spaz.connect_controls_to_player(enable_bomb=False) + spaz.equip_boxing_gloves() + + return spaz diff --git a/plugins/minigames/meteor_shower.py b/plugins/minigames/meteor_shower.py new file mode 100644 index 0000000..0f87f39 --- /dev/null +++ b/plugins/minigames/meteor_shower.py @@ -0,0 +1,406 @@ +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba +from bastd.actor.bomb import Bomb +from bastd.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = ba.app.lang.language + +if lang == 'Spanish': + name = 'Lluvia de Meteoritos v2' + bomb_type = 'Tipo de Bomba' + ice = 'hielo' + sticky = 'pegajosa' + impact = 'insta-bomba' + land_mine = 'mina terrestre' + random_bomb = 'aleatoria' + normal_rain = 'Lluvia Normal' + frozen_rain = 'Lluvia Congelada' + sticky_rain = 'Lluvia Pegajosa' + impact_rain = 'Lluvia de Impacto' + mine_rain = 'Lluvia de Minas' + tnt_rain = 'Lluvia de TNT' + random_rain = 'Lluvia Aleatoria' +else: + name = 'Meteor Shower v2' + bomb_type = 'Bomb Type' + ice = 'ice' + sticky = 'sticky' + impact = 'impact' + land_mine = 'land mine' + random_bomb = 'random' + normal_rain = 'Normal Rain' + frozen_rain = 'Frozen Rain' + sticky_rain = 'Sticky Rain' + impact_rain = 'Impact Rain' + mine_rain = 'Mine Rain' + tnt_rain = 'TNT Rain' + random_rain = 'Random Rain' + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: float | None = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export game +class MeteorShowerv2Game(ba.TeamGameActivity[Player, Team]): + """Minigame involving dodging falling bombs.""" + + name = name + description = 'Dodge the falling bombs.' + scoreconfig = ba.ScoreConfig( + label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B' + ) + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + # Don't allow joining after we start + # (would enable leave/rejoin tomfoolery). + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session] + ) -> list[ba.Setting]: + settings = [ + ba.IntChoiceSetting( + bomb_type, + choices=[ + ('normal', 0), + (ice, 1), + (sticky, 2), + (impact, 3), + (land_mine, 4), + ('tnt', 5), + (random_bomb, 6) + ], + default=0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + return settings + + # We're currently hard-coded for one map. + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ['Rampage'] + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return ( + issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession) + or issubclass(sessiontype, ba.CoopSession) + ) + + def __init__(self, settings: dict): + super().__init__(settings) + btype = int(settings[bomb_type]) + if btype == 0: + newbtype = 'normal' + elif btype == 1: + newbtype = 'ice' + elif btype == 2: + newbtype = 'sticky' + elif btype == 3: + newbtype = 'impact' + elif btype == 4: + newbtype = 'land_mine' + elif btype == 5: + newbtype = 'tnt' + else: + newbtype = 'random' + self._bomb_type = newbtype + self._epic_mode = settings.get('Epic Mode', False) + self._last_player_death_time: float | None = None + self._meteor_time = 2.0 + self._timer: OnScreenTimer | None = None + + # Some base class overrides: + self.default_music = ( + ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL + ) + if self._epic_mode: + self.slow_motion = True + + def on_begin(self) -> None: + super().on_begin() + + # Drop a wave every few seconds.. and every so often drop the time + # between waves ..lets have things increase faster if we have fewer + # players. + delay = 5.0 if len(self.players) > 2 else 2.5 + if self._epic_mode: + delay *= 0.25 + ba.timer(delay, self._decrement_meteor_time, repeat=True) + + # Kick off the first wave in a few seconds. + delay = 3.0 + if self._epic_mode: + delay *= 0.25 + ba.timer(delay, self._set_meteor_timer) + + self._timer = OnScreenTimer() + self._timer.start() + + # Check for immediate end (if we've only got 1 player, etc). + ba.timer(5.0, self._check_end_game) + + def on_player_leave(self, player: Player) -> None: + # Augment default behavior. + super().on_player_leave(player) + + # A departing player may trigger game-over. + self._check_end_game() + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player( + enable_punch=False, enable_bomb=False, enable_pickup=False + ) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + # Various high-level game events come through this method. + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = ba.time() + + # Record the player's moment of death. + # assert isinstance(msg.spaz.player + msg.getplayer(Player).death_time = curtime + + # In co-op mode, end the game the instant everyone dies + # (more accurate looking). + # In teams/ffa, allow a one-second fudge-factor so we can + # get more draws if players die basically at the same time. + if isinstance(self.session, ba.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + ba.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = curtime + else: + ba.timer(1.0, self._check_end_game) + + else: + # Default handler: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, ba.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def _set_meteor_timer(self) -> None: + ba.timer( + (1.0 + 0.2 * random.random()) * self._meteor_time, + self._drop_bomb_cluster, + ) + + def _drop_bomb_cluster(self) -> None: + + # Random note: code like this is a handy way to plot out extents + # and debug things. + loc_test = False + if loc_test: + ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) + ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) + ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) + ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) + + # Drop several bombs in series. + delay = 0.0 + for _i in range(random.randrange(1, 3)): + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + pos = ( + -7.3 + 15.3 * random.random(), + 11, + -5.57 + 2.1 * random.random(), + ) + dropdir = -1.0 if pos[0] > 0 else 1.0 + vel = ( + (-5.0 + random.random() * 30.0) * dropdir, + random.uniform(-3.066, -4.12), + 0, + ) + ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + def _drop_bomb( + self, position: Sequence[float], velocity: Sequence[float] + ) -> None: + if self._bomb_type == 'tnt': + bomb_type = random.choice(['tnt', 'tnt', 'tnt', 'tnt', 'impact']) + elif self._bomb_type == 'land_mine': + bomb_type = random.choice([ + 'land_mine', 'land_mine', 'land_mine', 'land_mine', 'impact']) + elif self._bomb_type == 'random': + bomb_type = random.choice([ + 'normal', 'ice', 'sticky', 'impact', 'land_mine', 'tnt']) + else: + bomb_type = self._bomb_type + Bomb(position=position, + velocity=velocity, + bomb_type=bomb_type).autoretain() + + def _decrement_meteor_time(self) -> None: + self._meteor_time = max(0.01, self._meteor_time * 0.9) + + def end_game(self) -> None: + cur_time = ba.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + # Mark death-time as now for any still-living players + # and award players points for how long they lasted. + # (these per-player scores are only meaningful in team-games) + for team in self.teams: + for player in team.players: + survived = False + + # Throw an extra fudge factor in so teams that + # didn't die come out ahead of teams that did. + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + # Award a per-player score depending on how many seconds + # they lasted (per-player scores only affect teams mode; + # everywhere else just looks at the per-team score). + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 # A bit extra for survivors. + self.stats.player_scored(player, score, screenmessage=False) + + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = ba.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + + # Set the team score to the max time survived by any player on + # that team. + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) + + +# ba_meta export plugin +class MeteorShowerv2Coop(ba.Plugin): + def on_app_running(self) -> None: + ba.app.add_coop_practice_level( + ba.Level( + normal_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 0}, + preview_texture_name='rampagePreview', + ) + ) + ba.app.add_coop_practice_level( + ba.Level( + frozen_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 1}, + preview_texture_name='rampagePreview', + ) + ) + ba.app.add_coop_practice_level( + ba.Level( + sticky_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 2}, + preview_texture_name='rampagePreview', + ) + ) + ba.app.add_coop_practice_level( + ba.Level( + impact_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 3}, + preview_texture_name='rampagePreview', + ) + ) + ba.app.add_coop_practice_level( + ba.Level( + mine_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 4}, + preview_texture_name='rampagePreview', + ) + ) + ba.app.add_coop_practice_level( + ba.Level( + tnt_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 5}, + preview_texture_name='rampagePreview', + ) + ) + ba.app.add_coop_practice_level( + ba.Level( + random_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 6}, + preview_texture_name='rampagePreview', + ) + ) diff --git a/plugins/minigames/simon_says.py b/plugins/minigames/simon_says.py new file mode 100644 index 0000000..ad3ac27 --- /dev/null +++ b/plugins/minigames/simon_says.py @@ -0,0 +1,370 @@ +# SimonSays +# you had really better do what Simon says... +# ba_meta require api 7 +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Union, Sequence + +from ba import _gameutils +import ba +import random + + +class CustomText(ba.Actor): + """Text that pops up above a position to denote something special. + + category: Gameplay Classes + """ + + def __init__(self, + text: Union[str, ba.Lstr], + position: Sequence[float] = (0.0, 0.0, 0.0), + color: Sequence[float] = (1.0, 1.0, 1.0, 1.0), + random_offset: float = 0.5, + duration: float = 1.5, + offset: Sequence[float] = (0.0, 0.0, 0.0), + scale: float = 1.0): + super().__init__() + if len(color) == 3: + color = (color[0], color[1], color[2], 1.0) + pos = (position[0] + offset[0] + random_offset * + (0.5 - random.random()), position[1] + offset[0] + + random_offset * (0.5 - random.random()), position[2] + + offset[0] + random_offset * (0.5 - random.random())) + self.node = ba.newnode('text', + attrs={ + 'text': text, + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_align': 'center'}, delegate=self) + lifespan = duration + ba.animate( + self.node, 'scale', { + 0: 0.0, + lifespan * 0.11: 0.020 * 0.7 * scale, + lifespan * 0.16: 0.013 * 0.7 * scale, + lifespan * 0.25: 0.014 * 0.7 * scale + }) + self._tcombine = ba.newnode('combine', + owner=self.node, + attrs={ + 'input0': pos[0], + 'input2': pos[2], + 'size': 3 + }) + ba.animate(self._tcombine, 'input1', { + 0: pos[1] + 1.5, + lifespan: pos[1] + 2.0 + }) + self._tcombine.connectattr('output', self.node, 'position') + # fade our opacity in/out + self._combine = ba.newnode('combine', + owner=self.node, + attrs={ + 'input0': color[0], + 'input1': color[1], + 'input2': color[2], + 'size': 4 + }) + for i in range(4): + ba.animate( + self._combine, 'input' + str(i), { + 0.13 * lifespan: color[i], + 0.18 * lifespan: 4.0 * color[i], + 0.22 * lifespan: color[i]}) + ba.animate(self._combine, 'input3', { + 0: 0, + 0.1 * lifespan: color[3], + 0.7 * lifespan: color[3], + lifespan: 0}) + self._combine.connectattr('output', self.node, 'color') + self._die_timer = ba.Timer( + lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())) + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + else: + super().handlemessage(msg) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + +# ba_meta export game + + +class SimonSays(ba.TeamGameActivity[Player, Team]): + name = "Simon Says" + description = "You have to better do what Simon says!" + + @classmethod + def get_available_settings(cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.BoolSetting("Epic Mode", default=False), + ba.BoolSetting("Enable Jumping", default=False), + ba.BoolSetting("Enable Punching", default=False), + ba.BoolSetting("Enable Picking Up", default=False), + ba.IntChoiceSetting("Timer Speed", + choices=[("Snaily", 1200), + ("Slow", 900), + ("Normal", 655), + ("Fast", 544), + ("Turbo", 460)], default=655), + + ba.FloatChoiceSetting("Text Duration", + choices=[("Slow", 2.5), + ("Normal", 1.5), + ("Mediocre", 1.0), + ("Quick", 0.75)], default=1.5)] + return settings + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ["Courtyard"] + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.FreeForAllSession) + + def __init__(self, settings: dict): + super().__init__(settings) + self.settings = settings + self._is_slow_motion = bool(settings['Epic Mode']) + self.speed = float(settings['Timer Speed']) + self.lifespan = float(settings['Text Duration']) + self.round_num = 0 + self.string = "" + self.now = 0 + self.simon = False + self.ended = False + self.counter_loop = None + self.time = 5000 + self._r1 = 2 + self.ct_text = ba.newnode('text', attrs={ + 'in_world': True, + 'text': '......', + 'shadow': 1.0, + 'color': (1.0, 1.0, 1.0), + 'flatness': 0.5, + 'position': (-5.627144702, 3.3275475, -9.572879116), + 'scale': 0.05}) + self.n1 = ba.newnode('locator', attrs={'shape': 'circle', 'position': (-4, 0, -6), + 'color': (1, 0, 0), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n2 = ba.newnode('locator', attrs={'shape': 'circle', 'position': (0, 0, -6), + 'color': (0, 1, 0), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n3 = ba.newnode('locator', attrs={'shape': 'circle', 'position': (4, 0, -6), + 'color': (0, 0, 1), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n4 = ba.newnode('locator', attrs={'shape': 'circle', 'position': (-4, 0, -2), + 'color': (1, 1, 0), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n5 = ba.newnode('locator', attrs={'shape': 'circle', 'position': (0, 0, -2), + 'color': (0, 1, 1), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n6 = ba.newnode('locator', attrs={'shape': 'circle', 'position': (4, 0, -2), + 'color': (1, 0, 1), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n7 = ba.newnode('locator', attrs={'shape': 'circle', 'position': (-4, 0, 2), + 'color': (.5, .5, .5), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n8 = ba.newnode('locator', attrs={'shape': 'circle', 'position': (0, 0, 2), + 'color': (.5, .325, 0), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n9 = ba.newnode('locator', attrs={'shape': 'circle', 'position': (4, 0, 2), + 'color': (1, 1, 1), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.options = ["red", "green", "blue", "yellow", "teal", "purple", "gray", "orange", + "white", "top", "bottom", "middle row", "left", "right", "center column", "outside"] + self.default_music = ba.MusicType.FLAG_CATCHER + + def get_instance_description(self) -> str: + return 'Follow the commands... but only when \"Simon says!"' + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0),) + return + else: + self.spawn_player(player) + + def on_begin(self) -> None: + super().on_begin() + s = self.settings + _gameutils.animate_array(self.n1, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n2, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n3, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n4, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n5, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n6, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n7, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n8, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n9, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + for team in self.teams: + team.score = 0 + for player in self.players: + player.score = 0 + # check for immediate end if theres only 1 player + if len(self.players) == 1: + ba.timer(4000, lambda: self.check_end(), timeformat=ba.TimeFormat.MILLISECONDS) + else: + ba.timer(6000, self.call_round, timeformat=ba.TimeFormat.MILLISECONDS) + + def spawn_player(self, player: PlayerType) -> ba.Actor: + assert player + spaz = self.spawn_player_spaz(player, position=( + 0 + random.uniform(-3.6, 3.6), 2.9, -2 + random.uniform(-3.6, 3.6))) + assert spaz.node + spaz.connect_controls_to_player( + enable_bomb=False, + enable_run=True, + enable_punch=self.settings["Enable Punching"], + enable_pickup=self.settings["Enable Picking Up"], + enable_jump=self.settings["Enable Jumping"]) + + def call_round(self) -> None: + if self.ended: + return + self.round_num += 1 + self.num = random.randint(0, 15) + self.numa = self.num + self.simon = random.choice([True, False]) + false_prefix = random.choices(['Simon say r', 'Simon said r', 'Simon r', + 'Simons says r', 'Simons r', 'R'], weights=[35, 45, 45, 39, 49, 100])[0] + if self.numa < 9: + if not self.simon: + line = false_prefix + "un to the " + self.options[self.numa] + " circle!" + else: + line = "Run to the " + self.options[self.numa] + " circle!" + + elif self.numa < 15: + if not self.simon: + line = false_prefix + "un to the " + self.options[self.numa] + "!" + else: + line = "Run to the " + self.options[self.numa] + "!" + + else: + if not self.simon: + line = false_prefix + "un outside of the circles!" + else: + line = "Run outside of the circles!" + + if self.simon: + line = "Simon says " + line[0].lower() + line[1:] + self.text = CustomText(line, + position=(0, 5, -4), + color=(0.68, 0.95, 1.12), + random_offset=0.5, + offset=(0, 0, 0), + duration=self.lifespan, + scale=2.0).autoretain() + self.now = 6 + + def dummy_check(): + self.string = "...." + self.check_round() + + def set_counter(): + self.now = self.now - 1 + if self.now == 0: + self.string = "0" + self.ct_text.text = self.string + self.counter_loop = None + ba.timer(1, dummy_check, timeformat=ba.TimeFormat.MILLISECONDS) + else: + self.ct_text.text = str(self.now) + ba.playsound(ba.getsound('tick')) + self.counter_loop = ba.Timer(self.speed, set_counter, + timeformat=ba.TimeFormat.MILLISECONDS, repeat=True) + + def check_round(self) -> None: + if self.ended: + return + for player in self.players: + if player.is_alive(): + safe = True if self.options[self.numa] in self.in_circle( + player.actor.node.position_center) else False + if ((self.simon and safe == False) or ((not self.simon) and safe == True)): + player.team.score = self.round_num + player.actor.handlemessage(ba.DieMessage()) + ba.timer(1633, self.call_round, timeformat=ba.TimeFormat.MILLISECONDS) + + def in_circle(self, pos) -> None: + circles = [] + x = pos[0] + z = pos[2] + if (x + 4) ** 2 + (z + 6) ** 2 < 4: + circles.append("red") + elif (x) ** 2 + (z + 6) ** 2 < 4: + circles.append("green") + elif (x - 4) ** 2 + (z + 6) ** 2 < 4: + circles.append("blue") + elif (x + 4) ** 2 + (z + 2) ** 2 < 4: + circles.append("yellow") + elif (x) ** 2 + (z + 2) ** 2 < 4: + circles.append("teal") + elif (x - 4) ** 2 + (z + 2) ** 2 < 4: + circles.append("purple") + elif (x + 4) ** 2 + (z - 2) ** 2 < 4: + circles.append("gray") + elif (x) ** 2 + (z - 2) ** 2 < 4: + circles.append("orange") + elif (x - 4) ** 2 + (z - 2) ** 2 < 4: + circles.append("white") + else: + circles.append("outside") + if x < -2: + circles.append("left") + if x > 2: + circles.append("right") + if x > -2 and x < 2: + circles.append("center column") + if z > 0: + circles.append("bottom") + if z < -4: + circles.append("top") + if z < 0 and z > -4: + circles.append("middle row") + return circles + + def handlemessage(self, msg) -> None: + if isinstance(msg, ba.PlayerDiedMessage): + self.check_end() + else: + super().handlemessage(msg) + + def end_game(self) -> None: + self.ended = True + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def check_end(self) -> None: + i = 0 + for player in self.players: + if player.is_alive(): + i += 1 + if i <= 2: + ba.timer(0.6, lambda: self.end_game()) diff --git a/plugins/minigames/squid_race.py b/plugins/minigames/squid_race.py new file mode 100644 index 0000000..d8cd54b --- /dev/null +++ b/plugins/minigames/squid_race.py @@ -0,0 +1,950 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Race mini-game.""" + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import ba +from bastd.actor.bomb import Bomb, Blast, ExplodeHitMessage +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict, + Union) + from bastd.actor.onscreentimer import OnScreenTimer + + +class NewBlast(Blast): + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ExplodeHitMessage): + pass + else: + return super().handlemessage(msg) + + +@dataclass +class RaceMine: + """Holds info about a mine on the track.""" + point: Sequence[float] + mine: Optional[Bomb] + + +class RaceRegion(ba.Actor): + """Region used to track progress during a race.""" + + def __init__(self, pt: Sequence[float], index: int): + super().__init__() + activity = self.activity + assert isinstance(activity, RaceGame) + self.pos = pt + self.index = index + self.node = ba.newnode( + 'region', + delegate=self, + attrs={ + 'position': pt[:3], + 'scale': (pt[3] * 2.0, pt[4] * 2.0, pt[5] * 2.0), + 'type': 'box', + 'materials': [activity.race_region_material] + }) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.distance_txt: Optional[ba.Node] = None + self.last_region = 0 + self.lap = 0 + self.distance = 0.0 + self.finished = False + self.rank: Optional[int] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.time: Optional[float] = None + self.lap = 0 + self.finished = False + + +# ba_meta export game +class SquidRaceGame(ba.TeamGameActivity[Player, Team]): + """Game of racing around a track.""" + + name = 'Squid Race' + description = 'Run real fast!' + scoreconfig = ba.ScoreConfig(label='Time', + lower_is_better=True, + scoretype=ba.ScoreType.MILLISECONDS) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting('Laps', min_value=1, default=3, increment=1), + ba.IntChoiceSetting( + 'Time Limit', + default=0, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + ), + ba.IntChoiceSetting( + 'Mine Spawning', + default=4000, + choices=[ + ('No Mines', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ], + ), + ba.IntChoiceSetting( + 'Bomb Spawning', + choices=[ + ('None', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ('1 Second', 1000), + ], + default=2000, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + # We have some specific settings in teams mode. + if issubclass(sessiontype, ba.DualTeamSession): + settings.append( + ba.BoolSetting('Entire Team Must Finish', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('race') + + def __init__(self, settings: dict): + self._race_started = False + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_sound = ba.getsound('score') + self._swipsound = ba.getsound('swip') + self._last_team_time: Optional[float] = None + self._front_race_region: Optional[int] = None + self._nub_tex = ba.gettexture('nub') + self._beep_1_sound = ba.getsound('raceBeep1') + self._beep_2_sound = ba.getsound('raceBeep2') + self.race_region_material: Optional[ba.Material] = None + self._regions: List[RaceRegion] = [] + self._team_finish_pts: Optional[int] = None + self._time_text: Optional[ba.Actor] = None + self._timer: Optional[OnScreenTimer] = None + self._race_mines: Optional[List[RaceMine]] = None + self._race_mine_timer: Optional[ba.Timer] = None + self._scoreboard_timer: Optional[ba.Timer] = None + self._player_order_update_timer: Optional[ba.Timer] = None + self._start_lights: Optional[List[ba.Node]] = None + self._squid_lights: Optional[List[ba.Node]] = None + self._countdown_timer: int = 0 + self._sq_mode: str = 'Easy' + self._tick_timer: Optional[ba.Timer] = None + self._bomb_spawn_timer: Optional[ba.Timer] = None + self._laps = int(settings['Laps']) + self._entire_team_must_finish = bool( + settings.get('Entire Team Must Finish', False)) + self._time_limit = float(settings['Time Limit']) + self._mine_spawning = int(settings['Mine Spawning']) + self._bomb_spawning = int(settings['Bomb Spawning']) + self._epic_mode = bool(settings['Epic Mode']) + + self._countdownsounds = { + 10: ba.getsound('announceTen'), + 9: ba.getsound('announceNine'), + 8: ba.getsound('announceEight'), + 7: ba.getsound('announceSeven'), + 6: ba.getsound('announceSix'), + 5: ba.getsound('announceFive'), + 4: ba.getsound('announceFour'), + 3: ba.getsound('announceThree'), + 2: ba.getsound('announceTwo'), + 1: ba.getsound('announceOne') + } + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC_RACE + if self._epic_mode else ba.MusicType.RACE) + + def get_instance_description(self) -> Union[str, Sequence]: + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + t_str = ' Your entire team has to finish.' + else: + t_str = '' + + if self._laps > 1: + return 'Run ${ARG1} laps.' + t_str, self._laps + return 'Run 1 lap.' + t_str + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._laps > 1: + return 'run ${ARG1} laps', self._laps + return 'run 1 lap' + + def on_transition_in(self) -> None: + super().on_transition_in() + shared = SharedObjects.get() + pts = self.map.get_def_points('race_point') + mat = self.race_region_material = ba.Material() + mat.add_actions(conditions=('they_have_material', + shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + self._handle_race_point_collide), + )) + for rpt in pts: + self._regions.append(RaceRegion(rpt, len(self._regions))) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = ba.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + ba.timer(0.5, light.delete) + ba.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) + + def _handle_race_point_collide(self) -> None: + # FIXME: Tidy this up. + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + collision = ba.getcollision() + try: + region = collision.sourcenode.getdelegate(RaceRegion, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + last_region = player.last_region + this_region = region.index + + if last_region != this_region: + + # If a player tries to skip regions, smite them. + # Allow a one region leeway though (its plausible players can get + # blown over a region, etc). + if this_region > last_region + 2: + if player.is_alive(): + assert player.actor + player.actor.handlemessage(ba.DieMessage()) + ba.screenmessage(ba.Lstr( + translate=('statements', 'Killing ${NAME} for' + ' skipping part of the track!'), + subs=[('${NAME}', player.getname(full=True))]), + color=(1, 0, 0)) + else: + # If this player is in first, note that this is the + # front-most race-point. + if player.rank == 0: + self._front_race_region = this_region + + player.last_region = this_region + if last_region >= len(self._regions) - 2 and this_region == 0: + team = player.team + player.lap = min(self._laps, player.lap + 1) + + # In teams mode with all-must-finish on, the team lap + # value is the min of all team players. + # Otherwise its the max. + if isinstance(self.session, ba.DualTeamSession + ) and self._entire_team_must_finish: + team.lap = min([p.lap for p in team.players]) + else: + team.lap = max([p.lap for p in team.players]) + + # A player is finishing. + if player.lap == self._laps: + + # In teams mode, hand out points based on the order + # players come in. + if isinstance(self.session, ba.DualTeamSession): + assert self._team_finish_pts is not None + if self._team_finish_pts > 0: + self.stats.player_scored(player, + self._team_finish_pts, + screenmessage=False) + self._team_finish_pts -= 25 + + # Flash where the player is. + self._flash_player(player, 1.0) + player.finished = True + assert player.actor + player.actor.handlemessage( + ba.DieMessage(immediate=True)) + + # Makes sure noone behind them passes them in rank + # while finishing. + player.distance = 9999.0 + + # If the whole team has finished the race. + if team.lap == self._laps: + ba.playsound(self._score_sound) + player.team.finished = True + assert self._timer is not None + elapsed = ba.time() - self._timer.getstarttime() + self._last_team_time = player.team.time = elapsed + + # Team has yet to finish. + else: + ba.playsound(self._swipsound) + + # They've just finished a lap but not the race. + else: + ba.playsound(self._swipsound) + self._flash_player(player, 0.3) + + # Print their lap number over their head. + try: + assert isinstance(player.actor, PlayerSpaz) + mathnode = ba.newnode('math', + owner=player.actor.node, + attrs={ + 'input1': (0, 1.9, 0), + 'operation': 'add' + }) + player.actor.node.connectattr( + 'torso_position', mathnode, 'input2') + tstr = ba.Lstr(resource='lapNumberText', + subs=[('${CURRENT}', + str(player.lap + 1)), + ('${TOTAL}', str(self._laps)) + ]) + txtnode = ba.newnode('text', + owner=mathnode, + attrs={ + 'text': tstr, + 'in_world': True, + 'color': (1, 1, 0, 1), + 'scale': 0.015, + 'h_align': 'center' + }) + mathnode.connectattr('output', txtnode, 'position') + ba.animate(txtnode, 'scale', { + 0.0: 0, + 0.2: 0.019, + 2.0: 0.019, + 2.2: 0 + }) + ba.timer(2.3, mathnode.delete) + except Exception: + ba.print_exception('Error printing lap.') + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_join(self, player: Player) -> None: + # Don't allow joining after we start + # (would enable leave/rejoin tomfoolery). + if self.has_begun(): + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + + # A player leaving disqualifies the team if 'Entire Team Must Finish' + # is on (otherwise in teams mode everyone could just leave except the + # leading player to win). + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + ba.screenmessage(ba.Lstr( + translate=('statements', + '${TEAM} is disqualified because ${PLAYER} left'), + subs=[('${TEAM}', player.team.name), + ('${PLAYER}', player.getname(full=True))]), + color=(1, 1, 0)) + player.team.finished = True + player.team.time = None + player.team.lap = 0 + ba.playsound(ba.getsound('boo')) + for otherplayer in player.team.players: + otherplayer.lap = 0 + otherplayer.finished = True + try: + if otherplayer.actor is not None: + otherplayer.actor.handlemessage(ba.DieMessage()) + except Exception: + ba.print_exception('Error sending DieMessage.') + + # Defer so team/player lists will be updated. + ba.pushcall(self._check_end_game) + + def _update_scoreboard(self) -> None: + for team in self.teams: + distances = [player.distance for player in team.players] + if not distances: + teams_dist = 0.0 + else: + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + teams_dist = min(distances) + else: + teams_dist = max(distances) + self._scoreboard.set_team_value( + team, + teams_dist, + self._laps, + flash=(teams_dist >= float(self._laps)), + show_value=False) + + def on_begin(self) -> None: + from bastd.actor.onscreentimer import OnScreenTimer + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._team_finish_pts = 100 + + # Throw a timer up on-screen. + self._time_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, -50), + 'scale': 1.4, + 'text': '' + })) + self._timer = OnScreenTimer() + + if self._mine_spawning != 0: + self._race_mines = [ + RaceMine(point=p, mine=None) + for p in self.map.get_def_points('race_mine') + ] + if self._race_mines: + self._race_mine_timer = ba.Timer(0.001 * self._mine_spawning, + self._update_race_mine, + repeat=True) + + self._scoreboard_timer = ba.Timer(0.25, + self._update_scoreboard, + repeat=True) + self._player_order_update_timer = ba.Timer(0.25, + self._update_player_order, + repeat=True) + + if self.slow_motion: + t_scale = 0.4 + light_y = 50 + else: + t_scale = 1.0 + light_y = 150 + lstart = 7.1 * t_scale + inc = 1.25 * t_scale + + ba.timer(lstart, self._do_light_1) + ba.timer(lstart + inc, self._do_light_2) + ba.timer(lstart + 2 * inc, self._do_light_3) + ba.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = ba.newnode('image', + attrs={ + 'texture': ba.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + ba.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0, + 12.0 * t_scale: 1.0, + 12.5 * t_scale: 0.0 + }) + ba.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._start_lights[0].color = (0.2, 0, 0) + self._start_lights[1].color = (0.2, 0, 0) + self._start_lights[2].color = (0.2, 0.05, 0) + self._start_lights[3].color = (0.0, 0.3, 0) + + self._squid_lights = [] + for i in range(2): + lnub = ba.newnode('image', + attrs={ + 'texture': ba.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-33 + i * 65, 220), + 'scale': (60, 60), + 'attach': 'center' + }) + ba.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0}) + self._squid_lights.append(lnub) + self._squid_lights[0].color = (0.2, 0, 0) + self._squid_lights[1].color = (0.0, 0.3, 0) + + ba.timer(1.0, self._check_squid_end, repeat=True) + self._squidgame_countdown() + + def _squidgame_countdown(self) -> None: + self._countdown_timer = 80 * self._laps # 80 + ba.newnode( + 'image', + attrs={ + 'opacity': 0.7, + 'color': (0.2, 0.2, 0.2), + 'attach': 'topCenter', + 'position': (-220, -40), + 'scale': (135, 45), + 'texture': ba.gettexture('bar')}) + ba.newnode( + 'image', + attrs={ + 'opacity': 1.0, + 'color': (1.0, 0.0, 0.0), + 'attach': 'topCenter', + 'position': (-220, -38), + 'scale': (155, 65), + 'texture': ba.gettexture('uiAtlas'), + 'model_transparent': ba.getmodel('meterTransparent')}) + self._sgcountdown_text = ba.newnode( + 'text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'position': (-220, -57), + 'shadow': 0.5, + 'flatness': 0.5, + 'scale': 1.1, + 'text': str(self._countdown_timer)+"s"}) + + def _update_sgcountdown(self) -> None: + self._countdown_timer -= 1 + self._countdown_timer + if self._countdown_timer <= 0: + self._countdown_timer = 0 + self._squid_game_all_die() + if self._countdown_timer == 20: + self._sq_mode = 'Hard' + ba.playsound(ba.getsound('alarm')) + if self._countdown_timer == 40: + self._sq_mode = 'Normal' + if self._countdown_timer <= 20: + self._sgcountdown_text.color = (1.2, 0.0, 0.0) + self._sgcountdown_text.scale = 1.2 + if self._countdown_timer in self._countdownsounds: + ba.playsound(self._countdownsounds[self._countdown_timer]) + else: + self._sgcountdown_text.color = (1.0, 1.0, 1.0) + self._sgcountdown_text.text = str(self._countdown_timer)+"s" + + def _squid_game_all_die(self) -> None: + for player in self.players: + if player.is_alive(): + player.actor._cursed = True + player.actor.handlemessage(ba.DieMessage()) + NewBlast( + position=player.actor.node.position, + velocity=player.actor.node.velocity, + blast_radius=3.0, + blast_type='normal').autoretain() + player.actor.handlemessage( + ba.HitMessage( + pos=player.actor.node.position, + velocity=player.actor.node.velocity, + magnitude=2000, + hit_type='explosion', + hit_subtype='normal', + radius=2.0, + source_player=None)) + player.actor._cursed = False + + def _do_ticks(self) -> None: + def do_ticks(): + if self._ticks: + ba.playsound(ba.getsound('tick')) + self._tick_timer = ba.timer(1.0, do_ticks, repeat=True) + + def _start_squid_game(self) -> None: + easy = [4.5, 5, 5.5, 6] + normal = [4, 4.5, 5] + hard = [3, 3.5, 4] + random_number = random.choice( + hard if self._sq_mode == 'Hard' else + normal if self._sq_mode == 'Normal' else easy) + # if random_number == 6: + # ba.playsound(ba.getsound('lrlg_06s')) + # elif random_number == 5.5: + # ba.playsound(ba.getsound('lrlg_055s')) + # elif random_number == 5: + # ba.playsound(ba.getsound('lrlg_05s')) + # elif random_number == 4.5: + # ba.playsound(ba.getsound('lrlg_045s')) + # elif random_number == 4: + # ba.playsound(ba.getsound('lrlg_04s')) + # elif random_number == 3.5: + # ba.playsound(ba.getsound('lrlg_035s')) + # elif random_number == 3: + # ba.playsound(ba.getsound('lrlg_03s')) + self._squid_lights[0].color = (0.2, 0, 0) + self._squid_lights[1].color = (0.0, 1.0, 0) + self._do_delete = False + self._ticks = True + ba.timer(random_number, self._stop_squid_game) + + def _stop_squid_game(self) -> None: + self._ticks = False + self._squid_lights[0].color = (1.0, 0, 0) + self._squid_lights[1].color = (0.0, 0.3, 0) + ba.timer(0.2, self._check_delete) + + def _check_delete(self) -> None: + for player in self.players: + if player.is_alive(): + player.customdata['position'] = None + player.customdata['position'] = player.actor.node.position + self._do_delete = True + ba.timer(3.0 if self._sq_mode == 'Hard' else 4.0, + self._start_squid_game) + + def _start_delete(self) -> None: + for player in self.players: + if player.is_alive() and self._do_delete: + + posx = float("%.1f" % player.customdata['position'][0]) + posz = float("%.1f" % player.customdata['position'][1]) + posy = float("%.1f" % player.customdata['position'][2]) + + posx_list = [ + round(posx, 1), round(posx+0.1, 1), round(posx+0.2, 1), + round(posx-0.1, 1), round(posx-0.2, 1)] + current_posx = float("%.1f" % player.actor.node.position[0]) + + posz_list = [ + round(posz, 1), round(posz+0.1, 1), round(posz+0.2, 1), + round(posz-0.1, 1), round(posz-0.2, 1)] + current_posz = float("%.1f" % player.actor.node.position[1]) + + posy_list = [ + round(posy, 1), round(posy+0.1, 1), round(posy+0.2, 1), + round(posy-0.1, 1), round(posy-0.2, 1)] + current_posy = float("%.1f" % player.actor.node.position[2]) + + if not (current_posx in posx_list) or not ( + current_posz in posz_list) or not ( + current_posy in posy_list): + player.actor._cursed = True + player.actor.handlemessage(ba.DieMessage()) + NewBlast( + position=player.actor.node.position, + velocity=player.actor.node.velocity, + blast_radius=3.0, + blast_type='normal').autoretain() + player.actor.handlemessage( + ba.HitMessage( + pos=player.actor.node.position, + velocity=player.actor.node.velocity, + magnitude=2000, + hit_type='explosion', + hit_subtype='normal', + radius=2.0, + source_player=None)) + player.actor._cursed = False + + def _check_squid_end(self) -> None: + squid_player_alive = 0 + for player in self.players: + if player.is_alive(): + squid_player_alive += 1 + break + if squid_player_alive < 1: + self.end_game() + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + ba.playsound(self._beep_1_sound) + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + ba.playsound(self._beep_1_sound) + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + ba.playsound(self._beep_1_sound) + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + ba.playsound(self._beep_2_sound) + for player in self.players: + if player.actor is not None: + try: + assert isinstance(player.actor, PlayerSpaz) + player.actor.connect_controls_to_player() + except Exception: + ba.print_exception('Error in race player connects.') + assert self._timer is not None + self._timer.start() + + if self._bomb_spawning != 0: + self._bomb_spawn_timer = ba.Timer(0.001 * self._bomb_spawning, + self._spawn_bomb, + repeat=True) + + self._race_started = True + self._squid_lights[1].color = (0.0, 1.0, 0) + self._start_squid_game() + self._do_ticks() + ba.timer(0.2, self._start_delete, repeat=True) + ba.timer(1.0, self._update_sgcountdown, repeat=True) + + def _update_player_order(self) -> None: + + # Calc all player distances. + for player in self.players: + pos: Optional[ba.Vec3] + try: + pos = player.position + except ba.NotFoundError: + pos = None + if pos is not None: + r_index = player.last_region + rg1 = self._regions[r_index] + r1pt = ba.Vec3(rg1.pos[:3]) + rg2 = self._regions[0] if r_index == len( + self._regions) - 1 else self._regions[r_index + 1] + r2pt = ba.Vec3(rg2.pos[:3]) + r2dist = (pos - r2pt).length() + amt = 1.0 - (r2dist / (r2pt - r1pt).length()) + amt = player.lap + (r_index + amt) * (1.0 / len(self._regions)) + player.distance = amt + + # Sort players by distance and update their ranks. + p_list = [(player.distance, player) for player in self.players] + + p_list.sort(reverse=True, key=lambda x: x[0]) + for i, plr in enumerate(p_list): + plr[1].rank = i + if plr[1].actor: + node = plr[1].distance_txt + if node: + node.text = str(i + 1) if plr[1].is_alive() else '' + + def _spawn_bomb(self) -> None: + if self._front_race_region is None: + return + region = (self._front_race_region + 3) % len(self._regions) + pos = self._regions[region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1] + 1.0, + pos[2] + random.uniform(*z_range)) + ba.timer(random.uniform(0.0, 2.0), + ba.WeakCall(self._spawn_bomb_at_pos, pos)) + + def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None: + if self.has_ended(): + return + Bomb(position=pos, bomb_type='normal').autoretain() + + def _make_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + rmine.mine = Bomb(position=rmine.point[:3], bomb_type='land_mine') + rmine.mine.arm() + + def _flash_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + light = ba.newnode('light', + attrs={ + 'position': rmine.point[:3], + 'color': (1, 0.2, 0.2), + 'radius': 0.1, + 'height_attenuated': False + }) + ba.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _update_race_mine(self) -> None: + assert self._race_mines is not None + m_index = -1 + rmine = None + for _i in range(3): + m_index = random.randrange(len(self._race_mines)) + rmine = self._race_mines[m_index] + if not rmine.mine: + break + assert rmine is not None + if not rmine.mine: + self._flash_mine(m_index) + ba.timer(0.95, ba.Call(self._make_mine, m_index)) + + def spawn_player(self, player: Player) -> ba.Actor: + if player.team.finished: + # FIXME: This is not type-safe! + # This call is expected to always return an Actor! + # Perhaps we need something like can_spawn_player()... + # noinspection PyTypeChecker + return None # type: ignore + pos = self._regions[player.last_region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1], + pos[2] + random.uniform(*z_range)) + spaz = self.spawn_player_spaz( + player, position=pos, angle=90 if not self._race_started else None) + assert spaz.node + + # Prevent controlling of characters before the start of the race. + if not self._race_started: + spaz.disconnect_controls_from_player() + + mathnode = ba.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + + distance_txt = ba.newnode('text', + owner=spaz.node, + attrs={ + 'text': '', + 'in_world': True, + 'color': (1, 1, 0.4), + 'scale': 0.02, + 'h_align': 'center' + }) + player.distance_txt = distance_txt + mathnode.connectattr('output', distance_txt, 'position') + return spaz + + def _check_end_game(self) -> None: + + # If there's no teams left racing, finish. + teams_still_in = len([t for t in self.teams if not t.finished]) + if teams_still_in == 0: + self.end_game() + return + + # Count the number of teams that have completed the race. + teams_completed = len( + [t for t in self.teams if t.finished and t.time is not None]) + + if teams_completed > 0: + session = self.session + + # In teams mode its over as soon as any team finishes the race + + # FIXME: The get_ffa_point_awards code looks dangerous. + if isinstance(session, ba.DualTeamSession): + self.end_game() + else: + # In ffa we keep the race going while there's still any points + # to be handed out. Find out how many points we have to award + # and how many teams have finished, and once that matches + # we're done. + assert isinstance(session, ba.FreeForAllSession) + points_to_award = len(session.get_ffa_point_awards()) + if teams_completed >= points_to_award - teams_completed: + self.end_game() + return + + def end_game(self) -> None: + + # Stop updating our time text, and set it to show the exact last + # finish time if we have one. (so users don't get upset if their + # final time differs from what they see onscreen by a tiny amount) + assert self._timer is not None + if self._timer.has_started(): + self._timer.stop( + endtime=None if self._last_team_time is None else ( + self._timer.getstarttime() + self._last_team_time)) + + results = ba.GameResults() + + for team in self.teams: + if team.time is not None: + # We store time in seconds, but pass a score in milliseconds. + results.set_team_score(team, int(team.time * 1000.0)) + else: + results.set_team_score(team, None) + + # We don't announce a winner in ffa mode since its probably been a + # while since the first place guy crossed the finish line so it seems + # odd to be announcing that now. + self.end(results=results, + announce_winning_team=isinstance(self.session, + ba.DualTeamSession)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # Augment default behavior. + super().handlemessage(msg) + else: + super().handlemessage(msg) diff --git a/plugins/minigames/zombie_horde.py b/plugins/minigames/zombie_horde.py new file mode 100644 index 0000000..afb48f3 --- /dev/null +++ b/plugins/minigames/zombie_horde.py @@ -0,0 +1,882 @@ +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +import copy +import random +from ba import _math +from ba._coopsession import CoopSession +from ba._messages import PlayerDiedMessage, StandMessage +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.game.elimination import Icon, Player +from bastd.actor.spaz import PickupMessage +from bastd.actor.spazbot import SpazBotSet, BrawlerBot, SpazBotDiedMessage +from bastd.actor.spazfactory import SpazFactory + + +if TYPE_CHECKING: + from typing import Any, Sequence + + +class PlayerSpaz_Zom(PlayerSpaz): + def handlemessage(self, m: Any) -> Any: + if isinstance(m, ba.HitMessage): + if not self.node: + return + if not m._source_player is None: + try: + playa = m._source_player.getname(True, False) + if not playa is None: + if m._source_player.lives < 1: + super().handlemessage(m) + except: + super().handlemessage(m) + else: + super().handlemessage(m) + + elif isinstance(m, ba.FreezeMessage): + pass + + elif isinstance(m, PickupMessage): + if not self.node: + return None + + try: + collision = ba.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + except ba.NotFoundError: + return True + + try: + if opposingnode.invincible: + return True + except Exception: + pass + + try: + playa = opposingnode._source_player.getname(True, False) + if not playa is None: + if opposingnode._source_player.lives > 0: + 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 + else: + return super().handlemessage(m) + return None + + +class PlayerZombie(PlayerSpaz): + def handlemessage(self, m: Any) -> Any: + if isinstance(m, ba.HitMessage): + if not self.node: + return None + if not m._source_player is None: + try: + playa = m._source_player.getname(True, False) + if playa is None: + pass + else: + super().handlemessage(m) + except: + super().handlemessage(m) + else: + super().handlemessage(m) + else: + super().handlemessage(m) + + +class zBotSet(SpazBotSet): + def start_moving(self) -> None: + """Start processing bot AI updates so they start doing their thing.""" + self._bot_update_timer = ba.Timer(0.05, + ba.WeakCall(self.zUpdate), + repeat=True) + + def zUpdate(self) -> None: + + try: + bot_list = self._bot_lists[self._bot_update_list] = ([ + b for b in self._bot_lists[self._bot_update_list] if b + ]) + except Exception: + bot_list = [] + ba.print_exception('Error updating bot list: ' + + str(self._bot_lists[self._bot_update_list])) + self._bot_update_list = (self._bot_update_list + + 1) % self._bot_list_count + + player_pts = [] + for player in ba.getactivity().players: + assert isinstance(player, ba.Player) + try: + if player.is_alive(): + assert isinstance(player.actor, Spaz) + assert player.actor.node + if player.lives > 0: + player_pts.append( + (ba.Vec3(player.actor.node.position), + ba.Vec3(player.actor.node.velocity))) + except Exception: + ba.print_exception('Error on bot-set _update.') + + for bot in bot_list: + bot.set_player_points(player_pts) + bot.update_ai() + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + self.spawn_order: list[Player] = [] + + +# ba_meta export game +class ZombieHorde(ba.TeamGameActivity[Player, Team]): + + name = 'Zombie Horde' + description = 'Kill walkers for points!' + scoreconfig = ba.ScoreConfig(label='Score', + scoretype=ba.ScoreType.POINTS, + none_is_winner=False, + lower_is_better=False) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session]) -> list[ba.Setting]: + settings = [ + ba.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + ba.IntSetting( + 'Max Zombies', + default=10, + min_value=5, + max_value=50, + increment=5, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, ba.DualTeamSession): + settings.append(ba.BoolSetting('Solo Mode', default=False)) + settings.append( + ba.BoolSetting('Balance Total Lives', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._start_time: float | None = None + self._vs_text: ba.Actor | None = None + self._round_end_timer: ba.Timer | None = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = int(settings['Lives Per Player']) + self._max_zombies = int(settings['Max Zombies']) + 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 = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SURVIVAL) + + self.spazList = [] + self.zombieQ = 0 + + activity = ba.getactivity() + my_factory = SpazFactory.get() + + appears = ['Kronk', 'Zoe', 'Pixel', 'Agent Johnson', + 'Bones', 'Frosty', 'Kronk2'] + myAppear = copy.copy(ba.app.spaz_appearances['Kronk']) + myAppear.name = 'Kronk2' + ba.app.spaz_appearances['Kronk2'] = myAppear + for appear in appears: + my_factory.get_media(appear) + med = my_factory.spaz_media + med['Kronk2']['head_model'] = med['Zoe']['head_model'] + med['Kronk2']['color_texture'] = med['Agent Johnson']['color_texture'] + med['Kronk2']['color_mask_texture'] = med['Pixel']['color_mask_texture'] + med['Kronk2']['torso_model'] = med['Bones']['torso_model'] + med['Kronk2']['pelvis_model'] = med['Pixel']['pelvis_model'] + med['Kronk2']['upper_arm_model'] = med['Frosty']['upper_arm_model'] + med['Kronk2']['forearm_model'] = med['Frosty']['forearm_model'] + med['Kronk2']['hand_model'] = med['Bones']['hand_model'] + med['Kronk2']['upper_leg_model'] = med['Bones']['upper_leg_model'] + med['Kronk2']['lower_leg_model'] = med['Pixel']['lower_leg_model'] + med['Kronk2']['toes_model'] = med['Bones']['toes_model'] + + def get_instance_description(self) -> str | Sequence: + return ('Kill walkers for points! ', + 'Dead player walker: 2 points!') if isinstance( + self.session, ba.DualTeamSession) else ( + 'Kill walkers for points! Dead player walker: 2 points!') + + def get_instance_description_short(self) -> str | Sequence: + return ('Kill walkers for points! ', + 'Dead player walker: 2 points!') if isinstance( + self.session, ba.DualTeamSession) else ( + 'Kill walkers for points! Dead player walker: 2 points!') + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + player.lives = 0 + player.icons = [] + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + + player.lives = self._lives_per_player + + if self._solo_mode: + player.icons = [] + player.team.spawn_order.append(player) + self._update_solo_mode() + else: + player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + if self.has_begun(): + self._update_icons() + + def _update_solo_mode(self) -> None: + for team in self.teams: + 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 on_begin(self) -> None: + super().on_begin() + self._start_time = ba.time() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self.zombieQ = 1 + if self._solo_mode: + self._vs_text = ba.NodeActor( + ba.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': ba.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, ba.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._bots = zBotSet() + + # Set colors and character for ToughGuyBot to be zombie + setattr(BrawlerBot, 'color', (0.4, 0.1, 0.05)) + setattr(BrawlerBot, 'highlight', (0.2, 0.4, 0.3)) + setattr(BrawlerBot, 'character', 'Kronk2') + # start some timers to spawn bots + thePt = self.map.get_ffa_start_position(self.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + ba.timer(1.0, self._update, repeat=True) + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, ba.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) -> ba.Vec3 | None: + 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 = ba.Vec3(living_player_pos) + points: list[tuple[float, ba.Vec3]] = [] + for team in self.teams: + start_pos = ba.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) -> ba.Actor: + position = self.map.get_ffa_start_position(self.players) + angle = 20 + name = player.getname() + + light_color = _math.normalized_color(player.color) + display_color = _ba.safecolor(player.color, target_intensity=0.75) + spaz = PlayerSpaz_Zom(color=player.color, + highlight=player.highlight, + character=player.character, + player=player) + player.actor = spaz + assert spaz.node + self.spazList.append(spaz) + + if isinstance(self.session, CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + factory = SpazFactory() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + _ba.playsound(self._spawn_sound, 1, position=spaz.node.position) + light = _ba.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + _ba.timer(0.5, light.delete) + + if not self._solo_mode: + ba.timer(0.3, ba.Call(self._print_lives, player)) + + for icon in player.icons: + icon.handle_player_spawned() + return spaz + + def respawn_player_zombie(self, + player: Player, + respawn_time: float | None = None) -> None: + # pylint: disable=cyclic-import + + assert player + if respawn_time is None: + teamsize = len(player.team.players) + if teamsize == 1: + respawn_time = 3.0 + elif teamsize == 2: + respawn_time = 5.0 + elif teamsize == 3: + respawn_time = 6.0 + else: + respawn_time = 7.0 + + # If this standard setting is present, factor it in. + if 'Respawn Times' in self.settings_raw: + respawn_time *= self.settings_raw['Respawn Times'] + + # We want whole seconds. + assert respawn_time is not None + respawn_time = round(max(1.0, respawn_time), 0) + + if player.actor and not self.has_ended(): + from bastd.actor.respawnicon import RespawnIcon + player.customdata['respawn_timer'] = _ba.Timer( + respawn_time, ba.WeakCall( + self.spawn_player_if_exists_as_zombie, player)) + player.customdata['respawn_icon'] = RespawnIcon( + player, respawn_time) + + def spawn_player_if_exists_as_zombie(self, player: PlayerType) -> None: + """ + A utility method which calls self.spawn_player() *only* if the + ba.Player provided still exists; handy for use in timers and whatnot. + + There is no need to override this; just override spawn_player(). + """ + if player: + self.spawn_player_zombie(player) + + def spawn_player_zombie(self, player: PlayerType) -> ba.Actor: + position = self.map.get_ffa_start_position(self.players) + angle = 20 + name = player.getname() + + light_color = _math.normalized_color(player.color) + display_color = _ba.safecolor(player.color, target_intensity=0.75) + spaz = PlayerSpaz_Zom(color=player.color, + highlight=player.highlight, + character='Kronk2', + player=player) + player.actor = spaz + assert spaz.node + self.spazList.append(spaz) + + if isinstance(self.session, CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player(enable_punch=True, + enable_bomb=False, + enable_pickup=False) + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + _ba.playsound(self._spawn_sound, 1, position=spaz.node.position) + light = _ba.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + _ba.timer(0.5, light.delete) + + if not self._solo_mode: + ba.timer(0.3, ba.Call(self._print_lives, player)) + + for icon in player.icons: + icon.handle_player_spawned() + return spaz + + def _print_lives(self, player: Player) -> None: + from bastd.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 + + try: + pos = player.actor.node.position + except Exception as e: + print('EXC getting player pos in bsElim', e) + return + if player.lives > 0: + 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=pos).autoretain() + else: + popuptext.PopupText('Dead!', + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=pos).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. + ba.timer(0, self._update_icons) + + 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, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + if player.lives > 0: + player.lives -= 1 + else: + if msg._killerplayer: + if msg._killerplayer.lives > 0: + msg._killerplayer.team.score += 2 + self._update_scoreboard() + + if msg._player in self.spazList: + self.spazList.remove(msg._player) + if player.lives < 0: + ba.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: + ba.playsound(SpazFactory.get().single_player_death_sound) + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + self.respawn_player_zombie(player) + 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) + + elif isinstance(msg, SpazBotDiedMessage): + self._onSpazBotDied(msg) + # bs.PopupText("died",position=self._position,color=popupColor,scale=popupScale).autoRetain() + super().handlemessage(msg) + else: + super().handlemessage(msg) + + def _update(self) -> None: + if self.zombieQ > 0: + self.zombieQ -= 1 + self.spawn_zombie() + 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). + teamsRemain = self._get_living_teams() + if len(teamsRemain) < 2: + if len(teamsRemain) == 1: + theScores = [] + for team in self.teams: + theScores.append(team.score) + if teamsRemain[0].score < max(theScores): + pass + elif teamsRemain[0].score == max( + theScores) and theScores.count(max(theScores)) > 1: + pass + else: + self._round_end_timer = ba.Timer(0.5, self.end_game) + else: + self._round_end_timer = ba.Timer(0.5, self.end_game) + + def spawn_zombie(self) -> None: + # We need a Z height... + thePt = list(self.get_random_point_in_play()) + thePt2 = self.map.get_ffa_start_position(self.players) + thePt[1] = thePt2[1] + ba.timer(0.1, ba.Call( + self._bots.spawn_bot, BrawlerBot, pos=thePt, spawn_time=1.0)) + + def _onSpazBotDied(self, DeathMsg) -> None: + # Just in case we are over max... + if len(self._bots.get_living_bots()) < self._max_zombies: + self.zombieQ += 1 + + if DeathMsg.killerplayer is None: + pass + else: + player = DeathMsg.killerplayer + if not player: + return + if player.lives < 1: + return + player.team.score += 1 + self.zombieQ += 1 + self._update_scoreboard() + + def get_random_point_in_play(self) -> None: + myMap = self.map.getname() + if myMap == 'Doom Shroom': + while True: + x = random.uniform(-1.0, 1.0) + y = random.uniform(-1.0, 1.0) + if x*x+y*y < 1.0: + break + return ((8.0*x, 8.0, -3.5+5.0*y)) + elif myMap == 'Rampage': + x = random.uniform(-6.0, 7.0) + y = random.uniform(-6.0, -2.5) + return ((x, 8.0, y)) + elif myMap == 'Hockey Stadium': + x = random.uniform(-11.5, 11.5) + y = random.uniform(-4.5, 4.5) + return ((x, 5.0, y)) + elif myMap == 'Courtyard': + x = random.uniform(-4.3, 4.3) + y = random.uniform(-4.4, 0.3) + return ((x, 8.0, y)) + elif myMap == 'Crag Castle': + x = random.uniform(-6.7, 8.0) + y = random.uniform(-6.0, 0.0) + return ((x, 12.0, y)) + elif myMap == 'Big G': + x = random.uniform(-8.7, 8.0) + y = random.uniform(-7.5, 6.5) + return ((x, 8.0, y)) + elif myMap == 'Football Stadium': + x = random.uniform(-12.5, 12.5) + y = random.uniform(-5.0, 5.5) + return ((x, 8.0, y)) + else: + x = random.uniform(-5.0, 5.0) + y = random.uniform(-6.0, 0.0) + return ((x, 8.0, y)) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score) + + 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 + setattr(BrawlerBot, 'color', (0.6, 0.6, 0.6)) + setattr(BrawlerBot, 'highlight', (0.6, 0.6, 0.6)) + setattr(BrawlerBot, 'character', 'Kronk') + results = ba.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + +# ba_meta export game +class ZombieHordeCoop(ZombieHorde): + + name = 'Zombie Horde' + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ['Football Stadium'] + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.CoopSession)) + + def _update(self) -> None: + if self.zombieQ > 0: + self.zombieQ -= 1 + self.spawn_zombie() + 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 not any(player.is_alive() for player in self.teams[0].players): + self._round_end_timer = ba.Timer(0.5, self.end_game) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior. + ba.TeamGameActivity.handlemessage(self, msg) + player: Player = msg.getplayer(Player) + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + else: + super().handlemessage(msg) + + +# ba_meta export plugin +class ZombieHordeLevel(ba.Plugin): + def on_app_running(self) -> None: + ba.app.add_coop_practice_level( + ba.Level( + 'Zombie Horde', + gametype=ZombieHordeCoop, + settings={}, + preview_texture_name='footballStadiumPreview', + ) + ) diff --git a/plugins/utilities.json b/plugins/utilities.json index 63413f6..4f69da8 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -3,6 +3,125 @@ "description": "Utilities", "plugins_base_url": "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/utilities", "plugins": { + "share_replay": { + "description": "Export replays to mods folder and share them with friends or have a backup", + "external_url": "", + "authors": [ + { + "name": "LoupGarou", + "email": "LoupGarou5418@outlook.com", + "discord": "ʟօʊքɢǟʀօʊ#3063" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "d475487c221d78c7f1278410d5ce229e" + } + } + }, + "custom_death": { + "description": "Characters turn to Bones after death", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "! JoseANG3L#0268" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "7b1941512532321eec18140087569215" + } + } + }, + "max_players": { + "description": "Increase the max player limit of 8 players", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "! JoseANG3L#0268" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "f5e129f009e9732b3179c7edaf75478e" + } + } + }, + "quick_custom_game": { + "description": "Quckly create a custom game with any gamemode", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "! JoseANG3L#0268" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "b691e35afb17cd3b5f6fe21c0194567d" + } + } + }, + "random_colors": { + "description": "Creates patches of random color after bomb explosions", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "! JoseANG3L#0268" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "0c351a0aebed77c3771053cde0c18d7b" + } + } + }, + "chat_cmd": { + "description": "chatcmd for a beutiful game - BombSquad OwO,type /help in chat for more info", + "external_url": "", + "authors": [ + { + "name": "IM_NOT_PRANAV#7874", + "email": "", + "discord": "IM_NOT_PRANAV#7874" + }, + { + "name": "FireFighter1037 ", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "c84d9a1c9e63e77949134020e1f11834" + } + } + }, "mood_light": { "description": "Dynamic lighting in co-op games (adjustable using \"ml\" chat command)", "external_url": "", @@ -14,6 +133,12 @@ } ], "versions": { + "1.2.1": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "ee8eede4a6c47535ab831abd0111aab0" + }, "1.2.0": { "api_version": 7, "commit_sha": "d838115", diff --git a/plugins/utilities/chat_cmd.py b/plugins/utilities/chat_cmd.py new file mode 100644 index 0000000..5bb3da4 --- /dev/null +++ b/plugins/utilities/chat_cmd.py @@ -0,0 +1,543 @@ +"""python 3.9 | chatcmd for a beutiful game - BombSquad OwO""" +# modded by IM_NOT_PRANAV#7874 + +# biggggggg thankssssssssssssss to FireFighter1037 for helping everything + +# -*- coding: utf-8 -*- +# ba_meta require api 7 +from _ba import env, get_foreground_host_activity, get_foreground_host_session, get_game_roster, get_chat_messages, set_party_icon_always_visible, chatmessage as cmsg, screenmessage as smsg +import ba + +# our prefix that what we starts cmds with +px = '/' + +# main class + + +class _cmds: + + def _process_cmd(): + set_party_icon_always_visible(True) + messages = get_chat_messages() + if len(messages) > 1: + lastmsg = messages[len(messages)-1] + + m = lastmsg.split(' ')[1] + if m.startswith(px): + return _cmds._handle() + else: + pass + + def _handle(): + messages = get_chat_messages() + if len(messages) > 1: + lastmsg = messages[len(messages)-1] + + m = lastmsg.split(' ')[1] # cmd + n = lastmsg.split(' ')[2:] # aguments + + roster = get_game_roster() + session = get_foreground_host_session() + session_players = session.sessionplayers + + activity = get_foreground_host_activity() + activity_players = activity.players + + if m == px: + cmsg(px+'help for help') + + elif m == px+'help': + if n == []: + cmsg('===========================================') + cmsg(f' {px}help 1 - for page 1 | simple commands') + cmsg(f' {px}help 2 - for page 2 | all or number of list cmds') + cmsg(f' {px}help 3 - for page 3 | Other useful cmds') + cmsg('===========================================') + elif n[0] == '1': + cmsg('============================') + cmsg(f' {px}help 1 page 1 |') + cmsg(f' {px}help 1 page 2 |') + cmsg('============================') + if n[1] in ['page', 'Page', ]: + if n[2] == '1': + cmsg('============== Help 1, Page 1 ==============') + cmsg(f' your command prefix is or all commands starts with - {px}') + cmsg(f' {px}list or {px}l -- to see ids of players and execute commands') + cmsg(f' {px}uniqeid or {px}id -- to see accountid/uniqeid of player') + cmsg(f' {px}quit or {px}restart -- to restart the game') + cmsg(f' {px}mute/unmute -- to mute chat for everyone in your game') + elif n[2] == '2': + cmsg('============== Help 1, Page 2 ==============') + cmsg(f' {px}pause -- to pause everyone in your game') + cmsg(f' {px}nv or {px}night -- to make night in your game') + cmsg(f' {px}dv or {px}day -- to make night in your game') + cmsg(f' {px}camera_mode -- to rotate camera ,repat to off') + cmsg('===========================================') + elif n[0] == '2': + cmsg('============================') + cmsg(f' {px}help 2 page 1 |') + cmsg(f' {px}help 2 page 2 |') + cmsg(f' {px}help 2 page 3 |') + cmsg('============================') + if n[1] in ['page', 'Page']: + if n[2] == '1': + cmsg('============== Help 2 Page 1 ==============') + cmsg(f' {px}kill all or {px}kill number of list | kills the player') + cmsg(f' {px}heal all or {px}heal number of list | heals the players') + cmsg(f' {px}freeze all or {px}freeze number of list | freeze the player') + cmsg( + f' {px}unfreeze/thaw all or {px}unfreeze/thaw number of list | unfreeze the player') + cmsg(f' {px}gloves all or {px}gloves number of list | give gloves to player') + cmsg('============================') + elif n[2] == '2': + cmsg('============== Help 2 Page 2 ==============') + cmsg(f' {px}shield all or {px}shield number of list | give shield the player') + cmsg( + f' {px}fall all or {px}fall number of list | teleport in down and fall up the player') + cmsg(f' {px}curse all or {px}curse number of list | curse the player') + cmsg( + f' {px}creepy all or {px}creepy number of list | make creepy actor of player') + cmsg(f' {px}inv all or {px}inv number of list | makes invisible player') + cmsg( + f' {px}celebrate all or {px}celebrate number of list | celebrate action to the player') + cmsg('============================') + elif n[2] == '3': + cmsg('============== Help 2 Page 3 ==============') + cmsg(f' {px}gm all or {px}gm number of list | give bs gods like powers to player') + cmsg( + f' {px}sp all or {px}sp number of list | give superrrrrrr damages when punch to player') + cmsg(f' {px}sleep all or {px}sleep number of list | sleep up the player') + cmsg(f' {px}fly all or {px}fly number of list | fly up the player ') + cmsg(f' {px}hug number of list | hugup the player') + cmsg('============================') + + elif n[0] == '3': + cmsg('============================') + cmsg(f" {px}d_bomb bombType | changes default bomb | do {px}d_bomb help for bomb names ") + cmsg(f' {px}dbc (number of bombs) | changes default count of player') + cmsg('============================') + + elif m in [px+'list', px+'l', px+'clientids', px+'ids', px+'playerids']: + cmsg('======= Indexs ======') + for i in session_players: + cmsg(i.getname()+' --> '+str(session_players.index(i))+'\n') + if not roster == []: + for i in roster: + cmsg(f'======For {px}kick only======') + cmsg(str(i['players'][0]['nam_full'])+' - '+str(i['client_id'])) + + elif m in [px+'uniqeid', px+'id', px+'pb-id', px+'pb', px+'accountid']: + if n == []: + cmsg(f'use : {px}uniqeid number of list') + else: + try: + id = session_players[int(n[0])] + cmsg(id.getname()+"'s accountid is "+id.get_v1_account_id()) + except: + cmsg('could not found player') + + elif m in [px+'quit', px+'restart']: + ba.quit() + + elif m in [px+'mute', px+'mutechat']: + cfg = ba.app.config + cfg['Chat Muted'] = True + cfg.apply_and_commit() + cmsg('muted') + smsg(f'chat muted use {px}unmute and click on send to unmute') + + elif m in [px+'unmute', px+'unmutechat']: + cfg = ba.app.config + cfg['Chat Muted'] = False + cfg.apply_and_commit() + cmsg('un_muted') + smsg('chat un_muted') + + elif m in [px+'end', px+'next']: + if n == []: + try: + activity.end_game() + cmsg('Game ended Hope you scored great') + except: + cmsg('Game already ended') + + elif m in [px+'dv', px+'day']: + if activity.globalsnode.tint == (1.0, 1.0, 1.0): + cmsg(f'alwardy {px}dv is on ,do {px}nv for night') + else: + activity.globalsnode.tint = (1.0, 1.0, 1.0) + cmsg('day mode on!') + + elif m in [px+'nv', px+'night']: + if activity.globalsnode.tint == (0.5, 0.7, 1.0): + cmsg(f'alwardy {px}nv is on ,do {px}dv for day') + else: + activity.globalsnode.tint = (0.5, 0.7, 1.0) + cmsg('night mode on!') + + elif m in [px+'sm', px+'slow', px+'slowmo']: + if n == []: + if not activity.globalsnode.slow_motion: + activity.globalsnode.slow_motion = True + cmsg('Game in Epic Mode Now') + else: + activity.globalsnode.slow_motion = False + cmsg('Game in normal mode now ') + + elif m in [px+'pause', px+'pausegame']: + if n == []: + if not activity.globalsnode.paused: + activity.globalsnode.paused = True + cmsg('Game Paused') + else: + activity.globalsnode.paused = False + cmsg('Game un paused') + + elif m in [px+'cameraMode', px+'camera_mode', px+'rotate_camera']: + if n == []: + if not activity.globalsnode.camera_mode == 'rotate': + activity.globalsnode.camera_mode = 'rotate' + cmsg('camera mode is rotate now') + else: + activity.globalsnode.camera_mode = 'follow' + cmsg('camera mode is normal now') + + elif m in [px+'remove', px+'rm']: + if n == []: + cmsg(f'{px}remove all or {px}remove number in list') + elif n[0] == 'all': + for i in session_players: + i.remove_from_game() + cmsg('Removed All') + else: + try: + r = session_players[int(n[0])] + r.remove_from_game() + cmsg('Removed') # cant use getname() activity alwrady finish + except: + cmsg('could not found player') + elif m in [px+'inv', px+'invisible']: + if n == []: + cmsg(f'help : {px}inv all or {px}inv number of list') + elif n[0] == 'all': + for i in activity_players: + body = i.actor.node + if not body.torso_model == None: + body.head_model = None + body.torso_model = None + body.upper_arm_model = None + body.forearm_model = None + body.pelvis_model = None + body.hand_model = None + body.toes_model = None + body.upper_leg_model = None + body.lower_leg_model = None + body.style = 'cyborg' + cmsg('All invisible now Dont get cought') + else: + cmsg('alwardy invisible') + else: + body = activity_players[int(n[0])].actor.node + is_name = session_players[int(n[0])].getname() + if not body.torso_model == None: + body.head_model = None + body.torso_model = None + body.upper_arm_model = None + body.forearm_model = None + body.pelvis_model = None + body.hand_model = None + body.toes_model = None + body.upper_leg_model = None + body.lower_leg_model = None + body.style = 'cyborg' + cmsg(is_name+' using invisiblelity ') + else: + cmsg('alwardy invisible') + + elif m in [px+'hl', px+'headless']: + if n == []: + cmsg(f'help : {px}spaz all or {px}spaz number of list') + elif n[0] == 'all': + for i in activity_players: + body = i.actor.node + if not body.head_model == None: + body.head_model = None + body.style = 'cyborg' + cmsg('headless ? xD') + else: + cmsg('alwardy headless are you really headless?') + else: + body = activity_players[int(n[0])].actor.node + is_name = session_players[int(n[0])].getname() + if not body.head_model == None: + body.head_model = None + body.style = 'cyborg' + cmsg(is_name+'is headless now xD') + else: + cmsg('alwardy headless are you really headless?') + + elif m in [px+'creepy', px+'creep']: + if n == []: + cmsg(f'use: {px}creepy all or {px}creepy number of list') + elif n[0] == 'all': + for i in activity_players: + body = i.actor.node + body.head_model = None + body.handlemessage(ba.PowerupMessage(poweruptype='punch')) + body.handlemessage(ba.PowerupMessage(poweruptype='shield')) + cmsg('dont creep out childs all will be scared') + else: + try: + body = activity_players[int(n[0])].actor.node + body.head_model = None + body.handlemessage(ba.PowerupMessage(poweruptype='punch')) + body.handlemessage(ba.PowerupMessage(poweruptype='shield')) + cmsg('dont creep out childs all will be scared') + except: + cmsg('could not found player to make') + + elif m in [px+'kill', px+'die']: + if n == []: + cmsg(f'Use : {px}kill all or {px}kill number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(ba.DieMessage()) + cmsg('Killed all') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage(ba.DieMessage()) + cmsg('Killed '+is_name) + + elif m in [px+'heal', px+'heath']: + if n == []: + cmsg(f'Use: {px}heal all or {px}heal number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(ba.PowerupMessage(poweruptype='health')) + cmsg('Heald all') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage( + ba.PowerupMessage(poweruptype='health')) + cmsg('Heald '+is_name) + + elif m in [px+'curse', px+'cur']: + if n == []: + cmsg(f'Use: {px}curse all or {px}curse number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(ba.PowerupMessage(poweruptype='curse')) + cmsg('Cursed all') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage( + ba.PowerupMessage(poweruptype='curse')) + cmsg('Cursed '+is_name) + + elif m in [px+'sleep']: + if n == []: + cmsg(f'Use: {px}sleep all or {px}sleep number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage('knockout', 8000) + cmsg('Sleep all its Night :)') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage('knockout', 8000) + cmsg(is_name+' sleeped now') + + elif m in [px+'sp', px+'superpunch']: + if n == []: + cmsg(f'Use : {px}sp/superpunch all or {px}sp/superpunch number of list') + elif n[0] == 'all': + for i in activity_players: + if not i.actor._punch_power_scale == 15: + i.actor._punch_power_scale = 15 + i.actor._punch_cooldown = 0 + cmsg('Everyone enjoy your Super punches') + else: + i.actor._punch_power_scale = 1.2 + i.actor._punch_cooldown = 400 + cmsg('Super punches off now') + else: + try: + if not activity_players[int(n[0])].actor._punch_power_scale == 15: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor._punch_power_scale = 15 + activity_players[int(n[0])].actor._punch_cooldown = 0 + cmsg(is_name+' using super punches get away from him') + else: + activity_players[int(n[0])].actor._punch_power_scale = 1.2 + activity_players[int(n[0])].actor._punch_cooldown = 400 + cmsg(':( ') + except: + pass + + elif m in [px+'gloves', px+'punch']: + if n == []: + cmsg(f'Use: {px}gloves all or {px}gloves number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(ba.PowerupMessage(poweruptype='punch')) + cmsg('Free Gloves enjoy all') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage( + ba.PowerupMessage(poweruptype='punch')) + cmsg(is_name+' using gloves') + + elif m in [px+'shield', px+'protect']: + if n == []: + cmsg(f'Use: {px}shield all or {px}shield number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(ba.PowerupMessage(poweruptype='shield')) + cmsg('Everyone enjoy free shield :)') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage( + ba.PowerupMessage(poweruptype='shield')) + cmsg(is_name+' using shield') + + elif m in [px+'freeze', px+'ice']: + if n == []: + cmsg(f'Use: {px}freeze all or {px}freeze number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(ba.FreezeMessage()) + cmsg('Freezed all') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage(ba.FreezeMessage()) + cmsg('Un freezed '+is_name) + + elif m in [px+'unfreeze', px+'thaw']: + if n == []: + cmsg(f'Use: {px}unfreeze/thaw all or {px}unfreeze/thaw number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(ba.ThawMessage()) + cmsg('Un freezed all ') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage(ba.ThawMessage()) + cmsg('Un freezed '+is_name) + + elif m in [px+'fall']: + if n == []: + cmsg(f'Use: {px}fall all or {px}fall number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(ba.StandMessage()) + cmsg('Felt everyone') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage(ba.StandMessage()) + cmsg(is_name+' got felt') + + elif m in [px+'celebrate', px+'celeb']: + if n == []: + cmsg(f'Use: {px}celebrate all or {px}celebrate number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(ba.CelebrateMessage()) + cmsg('Celebrate all :)') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage(ba.CelebrateMessage()) + cmsg(is_name+' is celebrating bt why?') + + elif m in [px+'fly']: + if n == []: + cmsg(f'Use: {px}fly all or {px}fly number of list') + elif n[0] == 'all': + for i in activity_players: + if not i.actor.node.fly == True: + i.actor.node.fly = True + cmsg('fly all dont go out ok') + else: + i.actor.node.fly = False + cmsg('flying mode off') + else: + try: + is_name = session_players[int(n[0])].getname() + if not activity_players[int(n[0])].actor.node.fly == True: + activity_players[int(n[0])].actor.node.fly = True + cmsg(is_name+' is flying') + else: + activity_players[int(n[0])].actor.node.fly = False + cmsg('fly off :(') + except: + cmsg('player not found') + pass + + elif m in [px+'gm', px+'godmode']: + if n == []: + cmsg(f'Use: {px}gm all or {px}gm number of list') + elif n[0] == 'all': + for i in activity_players: + if not i.actor.node.hockey == True: + i.actor.node.hockey = True + i.actor.node.invincible = True + i.actor._punch_power_scale = 7 + cmsg('Gmed all ') + else: + i.actor.node.hockey = False + i.actor.node.invincible = False + i.actor._punch_power_scale = 1.2 + cmsg('Un gmed all') + else: + try: + is_name = session_players[int(n[0])].getname() + if not activity_players[int(n[0])].actor.node.hockey == True: + activity_players[int(n[0])].actor.node.hockey = True + activity_players[int(n[0])].actor.node.invincible = True + activity_players[int(n[0])].actor._punch_power_scale = 7 + cmsg('Gmed '+is_name) + else: + activity_players[int(n[0])].actor.node.hockey = False + activity_players[int(n[0])].actor.node.invincible = False + activity_players[int(n[0])].actor._punch_power_scale = 1.2 + cmsg('un gmed '+is_name) + except: + cmsg('could not found player') + elif m in [px+'d_bomb', px+'default_bomb']: + if n == []: + cmsg( + f'Use: {px}d_bomb/default_bomb all or {px}d_bomb number of list ,type {px}d_bomb help for help') + elif n[0] == 'help': + cmsg("bombtypes - ['ice', 'impact', 'land_mine', 'normal', 'sticky','tnt']") + elif n[0] in ['ice', 'impact', 'land_mine', 'normal', 'sticky', 'tnt']: + for i in activity_players: + i.actor.bomb_type = n[0] + cmsg('default bomb type - '+str(n[0])+' now') + else: + cmsg('unkwon bombtype , type {px}d_bomb help for help') + + elif m in [px+'d_bomb_count', px+'default_bomb_count', px+'dbc']: + if n == []: + cmsg( + f'Use: {px}d_bomb_count/default_bomb/dbc all or {px}d_bomb_count/default_bomb_count/dbc number of list') + else: + try: + for i in activity_players: + i.actor.set_bomb_count(int(n[0])) + cmsg('default bomb count is '+(str(n[0]))+' now') + except: + cmsg('Must use number to use') + elif m in [px+'credits']: + if n == []: + cmsg(u'\U0001F95A created by Nazz \U0001F95A') + cmsg(u'\U0001F95A Nazz are past/present/future \U0001F95A') + cmsg(u'\U0001F95A everything is Nazz \U0001F95A') + + +# ba.timer(0.05, _update, repeat=True) +def same(): + ba.timer(0.5, _cmds._process_cmd, True) + +# ba_meta export plugin + + +class _enableee(ba.Plugin): + same() diff --git a/plugins/utilities/custom_death.py b/plugins/utilities/custom_death.py new file mode 100644 index 0000000..b3730e1 --- /dev/null +++ b/plugins/utilities/custom_death.py @@ -0,0 +1,40 @@ +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.spaz import Spaz + +if TYPE_CHECKING: + from typing import Any + + +Spaz.oldhandlemessage = Spaz.handlemessage + + +def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.color_texture = ba.gettexture('bonesColor') + self.node.color_mask_texture = ba.gettexture('bonesColorMask') + self.node.head_model = ba.getmodel('bonesHead') + self.node.torso_model = ba.getmodel('bonesTorso') + self.node.pelvis_model = ba.getmodel('bonesPelvis') + self.node.upper_arm_model = ba.getmodel('bonesUpperArm') + self.node.forearm_model = ba.getmodel('bonesForeArm') + self.node.hand_model = ba.getmodel('bonesHand') + self.node.upper_leg_model = ba.getmodel('bonesUpperLeg') + self.node.lower_leg_model = ba.getmodel('bonesLowerLeg') + self.node.toes_model = ba.getmodel('bonesToes') + self.node.style = 'bones' + self.oldhandlemessage(msg) + else: + return self.oldhandlemessage(msg) + + +# ba_meta export plugin +class CustomDeath(ba.Plugin): + Spaz.handlemessage = handlemessage diff --git a/plugins/utilities/max_players.py b/plugins/utilities/max_players.py new file mode 100644 index 0000000..6d6257a --- /dev/null +++ b/plugins/utilities/max_players.py @@ -0,0 +1,368 @@ +"""===========MAX_PLAYERS===========""" + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations +from typing import TYPE_CHECKING + +import ba +import _ba +from ba._session import Session +from ba._coopsession import CoopSession, TEAM_COLORS, TEAM_NAMES +from ba._multiteamsession import MultiTeamSession +from bastd.ui.gather import GatherWindow +from bastd.ui.popup import PopupWindow + +if TYPE_CHECKING: + from typing import List, Any, Optional, Sequence + + +cfg = ba.app.config +cmp = {'coop_max_players': 4, + 'teams_max_players': 8, + 'ffa_max_players': 8} + +lang = ba.app.lang.language +if lang == 'Spanish': + title_text = 'Máximo de Jugadores' + title_short_text = 'Jugadores' + coop_text = 'Cooperativo' + teams_text = 'Equipos' + ffa_text = 'Todos Contra Todos' +else: + title_text = 'Max Players' + title_short_text = 'Players' + coop_text = 'Co-op' + teams_text = 'Teams' + ffa_text = 'FFA' + + +class ConfigNumberEdit: + + def __init__(self, + parent: ba.Widget, + position: Tuple[float, float], + value: int, + config: str, + text: str): + self._increment = 1 + self._minval = 1 + self._maxval = 100 + self._value = value + self._config = config + + textscale = 1.0 + self.nametext = ba.textwidget( + parent=parent, + position=(position[0], position[1]), + size=(100, 30), + text=text, + maxwidth=150, + color=(0.8, 0.8, 0.8, 1.0), + h_align='left', + v_align='center', + scale=textscale) + self.valuetext = ba.textwidget( + parent=parent, + position=(position[0]+150, position[1]), + size=(60, 28), + editable=False, + color=(0.3, 1.0, 0.3, 1.0), + h_align='right', + v_align='center', + text=str(value), + padding=2) + self.minusbutton = ba.buttonwidget( + parent=parent, + position=(position[0]+240, position[1]), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=ba.Call(self._down), + repeat=True) + self.plusbutton = ba.buttonwidget( + parent=parent, + position=(position[0]+290, position[1]), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=ba.Call(self._up), + repeat=True) + + def _up(self) -> None: + self._value = min(self._maxval, self._value + self._increment) + self._update_display() + + def _down(self) -> None: + self._value = max(self._minval, self._value - self._increment) + self._update_display() + + def _update_display(self) -> None: + ba.textwidget(edit=self.valuetext, text=str(self._value)) + cfg['Config Max Players'][self._config] = self._value + cfg.apply_and_commit() + + +class SettingsMaxPlayers(PopupWindow): + + def __init__(self): + # pylint: disable=too-many-locals + uiscale = ba.app.ui.uiscale + self._transitioning_out = False + self._width = 400 + self._height = 220 + bg_color = (0.5, 0.4, 0.6) + + # creates our _root_widget + PopupWindow.__init__(self, + position=(0.0, 0.0), + size=(self._width, self._height), + scale=1.2, + bg_color=bg_color) + + self._cancel_button = ba.buttonwidget( + parent=self.root_widget, + position=(25, self._height - 40), + size=(50, 50), + scale=0.58, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.containerwidget(edit=self.root_widget, + cancel_button=self._cancel_button) + + ba.textwidget( + parent=self.root_widget, + position=(self._width * 0.5, self._height - 30), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.8, + text=title_text, + maxwidth=200, + color=ba.app.ui.title_color) + + posx = 33 + posy = self._height + + # co-op + ConfigNumberEdit(parent=self.root_widget, + position=(posx, posy*0.6), + value=cfg['Config Max Players']['coop_max_players'], + config='coop_max_players', + text=coop_text) + + # teams + ConfigNumberEdit(parent=self.root_widget, + position=(posx, posy*0.38), + value=cfg['Config Max Players']['teams_max_players'], + config='teams_max_players', + text=teams_text) + + # ffa + ConfigNumberEdit(parent=self.root_widget, + position=(posx, posy*0.16), + value=cfg['Config Max Players']['ffa_max_players'], + config='ffa_max_players', + text=ffa_text) + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() + + +def __init__(self) -> None: + """Instantiate a co-op mode session.""" + # pylint: disable=cyclic-import + from ba._campaign import getcampaign + from bastd.activity.coopjoin import CoopJoinActivity + + _ba.increment_analytics_count('Co-op session start') + app = _ba.app + + # If they passed in explicit min/max, honor that. + # Otherwise defer to user overrides or defaults. + if 'min_players' in app.coop_session_args: + min_players = app.coop_session_args['min_players'] + else: + min_players = 1 + if 'max_players' in app.coop_session_args: + max_players = app.coop_session_args['max_players'] + else: + max_players = app.config.get( + 'Coop Game Max Players', + cfg['Config Max Players']['coop_max_players']) + + # print('FIXME: COOP SESSION WOULD CALC DEPS.') + depsets: Sequence[ba.DependencySet] = [] + + Session.__init__(self, + depsets, + team_names=TEAM_NAMES, + team_colors=TEAM_COLORS, + min_players=min_players, + max_players=max_players) + + # Tournament-ID if we correspond to a co-op tournament (otherwise None) + self.tournament_id: Optional[str] = ( + app.coop_session_args.get('tournament_id')) + + self.campaign = getcampaign(app.coop_session_args['campaign']) + self.campaign_level_name: str = app.coop_session_args['level'] + + self._ran_tutorial_activity = False + self._tutorial_activity: Optional[ba.Activity] = None + self._custom_menu_ui: List[Dict[str, Any]] = [] + + # Start our joining screen. + self.setactivity(_ba.newactivity(CoopJoinActivity)) + + self._next_game_instance: Optional[ba.GameActivity] = None + self._next_game_level_name: Optional[str] = None + self._update_on_deck_game_instances() + + +def get_max_players(self) -> int: + """Return max number of ba.Players allowed to join the game at once.""" + if self.use_teams: + return _ba.app.config.get( + 'Team Game Max Players', + cfg['Config Max Players']['teams_max_players']) + return _ba.app.config.get( + 'Free-for-All Max Players', + cfg['Config Max Players']['ffa_max_players']) + + +GatherWindow.__old_init__ = GatherWindow.__init__ + + +def __gather_init__(self, + transition: Optional[str] = 'in_right', + origin_widget: ba.Widget = None): + self.__old_init__(transition, origin_widget) + + def _do_max_players(): + SettingsMaxPlayers() + self._max_players_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width*0.72, self._height*0.91), + size=(220, 60), + scale=1.0, + color=(0.6, 0.0, 0.9), + icon=ba.gettexture('usersButton'), + iconscale=1.5, + autoselect=True, + label=title_short_text, + button_type='regular', + on_activate_call=_do_max_players) + + +def _save_state(self) -> None: + try: + for tab in self._tabs.values(): + tab.save_state() + + sel = self._root_widget.get_selected_child() + selected_tab_ids = [ + tab_id for tab_id, tab in self._tab_row.tabs.items() + if sel == tab.button + ] + if sel == self._back_button: + sel_name = 'Back' + elif sel == self._max_players_button: + sel_name = 'Max Players' + elif selected_tab_ids: + assert len(selected_tab_ids) == 1 + sel_name = f'Tab:{selected_tab_ids[0].value}' + elif sel == self._tab_container: + sel_name = 'TabContainer' + else: + raise ValueError(f'unrecognized selection: \'{sel}\'') + ba.app.ui.window_states[type(self)] = { + 'sel_name': sel_name, + } + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + +def _restore_state(self) -> None: + from efro.util import enum_by_value + try: + for tab in self._tabs.values(): + tab.restore_state() + + sel: Optional[ba.Widget] + winstate = ba.app.ui.window_states.get(type(self), {}) + sel_name = winstate.get('sel_name', None) + assert isinstance(sel_name, (str, type(None))) + current_tab = self.TabID.ABOUT + gather_tab_val = ba.app.config.get('Gather Tab') + try: + stored_tab = enum_by_value(self.TabID, gather_tab_val) + if stored_tab in self._tab_row.tabs: + current_tab = stored_tab + except ValueError: + pass + self._set_tab(current_tab) + if sel_name == 'Back': + sel = self._back_button + elif sel_name == 'Max Players': + sel = self._back_button + elif sel_name == 'TabContainer': + sel = self._tab_container + elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): + try: + sel_tab_id = enum_by_value(self.TabID, + sel_name.split(':')[-1]) + except ValueError: + sel_tab_id = self.TabID.ABOUT + sel = self._tab_row.tabs[sel_tab_id].button + else: + sel = self._tab_row.tabs[current_tab].button + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception('Error restoring gather-win state.') + +# ba_meta export plugin + + +class MaxPlayersPlugin(ba.Plugin): + + def has_settings_ui(self) -> bool: + return True + + def show_settings_ui(self, source_widget: ba.Widget | None) -> None: + SettingsMaxPlayers() + + if 'Config Max Players' in ba.app.config: + old_config = ba.app.config['Config Max Players'] + for setting in cmp: + if setting not in old_config: + ba.app.config['Config Max Players'].update({setting: cmp[setting]}) + remove_list = [] + for setting in old_config: + if setting not in cmp: + remove_list.append(setting) + for element in remove_list: + ba.app.config['Config Max Players'].pop(element) + else: + ba.app.config['Config Max Players'] = cmp + ba.app.config.apply_and_commit() + + CoopSession.__init__ = __init__ + MultiTeamSession.get_max_players = get_max_players + GatherWindow.__init__ = __gather_init__ + GatherWindow._save_state = _save_state + GatherWindow._restore_state = _restore_state diff --git a/plugins/utilities/mood_light.py b/plugins/utilities/mood_light.py index 0a0b827..225df54 100644 --- a/plugins/utilities/mood_light.py +++ b/plugins/utilities/mood_light.py @@ -4,26 +4,31 @@ from typing import TYPE_CHECKING, cast if TYPE_CHECKING: from typing import Any, Sequence, Callable, List, Dict, Tuple, Optional, Union +import random import ba import _ba -import random from ba._map import Map from bastd import mainmenu from bastd.ui.party import PartyWindow from bastd.gameutils import SharedObjects -from time import sleep """mood light plugin by ʟօʊքɢǟʀօʊ type ml in chat or use plugin manager to open settings""" def Print(*args): - out = " ".join(args) + out = "" + for arg in args: + a = str(arg) + out += a ba.screenmessage(out) def cprint(*args): - out = "\n".join(args) + out = "" + for arg in args: + a = str(arg) + out += a _ba.chatmessage(out) @@ -238,7 +243,9 @@ class SettingWindow(ba.Window): loop = True label = "DISABLE" color = (1, 0, 0) - Print("Restart level to enable") + in_game = not isinstance(_ba.get_foreground_host_session(), mainmenu.MainMenuSession) + if in_game: + Print("Restart level to apply") ba.app.config["moodlightEnabled"] = loop ba.app.config.commit() ba.buttonwidget(edit=self.enable_button, label=label, color=color) @@ -257,11 +264,12 @@ def new_chat_message(msg: Union[str, ba.Lstr], clients: Sequence[int] = None, se old_fcm(msg, clients, sender_override) if msg == 'ml': try: + global Ldefault, Udefault Ldefault, Udefault = ba.app.config.get("moodlightingSettings") SettingWindow() cprint("Mood light settings opened") except Exception as err: - Print(err) + Print(err, "-from new_chat_message") old_fcm = _ba.chatmessage @@ -279,7 +287,13 @@ class moodlight(ba.Plugin): def on_app_running(self): _ba.show_progress_bar() - def on_plugin_manager_prompt(self): # called by plugin manager + def on_plugin_manager_prompt(self): + SettingWindow() + + def has_settings_ui(self): + return True + + def show_settings_ui(self, button): SettingWindow() def _new_init(self, vr_overlay_offset: Optional[Sequence[float]] = None) -> None: diff --git a/plugins/utilities/quick_custom_game.py b/plugins/utilities/quick_custom_game.py new file mode 100644 index 0000000..259dff2 --- /dev/null +++ b/plugins/utilities/quick_custom_game.py @@ -0,0 +1,381 @@ +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +from bastd.ui.play import PlayWindow +from bastd.ui.playlist.addgame import PlaylistAddGameWindow +from ba._freeforallsession import FreeForAllSession +from bastd.activity.multiteamjoin import MultiTeamJoinActivity + +if TYPE_CHECKING: + pass + + +lang = ba.app.lang.language + +if lang == 'Spanish': + custom_txt = 'personalizar...' +else: + custom_txt = 'custom...' + + +if 'quick_game_button' in ba.app.config: + config = ba.app.config['quick_game_button'] +else: + config = {'selected': None, 'config': None} + ba.app.config['quick_game_button'] = config + ba.app.config.commit() + + +def start_game(session: ba.Session, fadeout: bool = True): + def callback(): + if fadeout: + _ba.unlock_all_input() + try: + _ba.new_host_session(session) + except Exception: + from bastd import mainmenu + ba.print_exception('exception running session', session) + + # Drop back into a main menu session. + _ba.new_host_session(mainmenu.MainMenuSession) + + if fadeout: + _ba.fade_screen(False, time=0.25, endcall=callback) + _ba.lock_all_input() + else: + callback() + + +class SimplePlaylist: + + def __init__(self, + settings: dict, + gametype: type[ba.GameActivity]): + self.settings = settings + self.gametype = gametype + + def pull_next(self) -> None: + if 'map' not in self.settings['settings']: + settings = dict( + map=self.settings['map'], **self.settings['settings']) + else: + settings = self.settings['settings'] + return dict(resolved_type=self.gametype, settings=settings) + + +class CustomSession(FreeForAllSession): + + def __init__(self, *args, **kwargs): + # pylint: disable=cyclic-import + self.use_teams = False + self._tutorial_activity_instance = None + ba.Session.__init__(self, depsets=[], + team_names=None, + team_colors=None, + min_players=1, + max_players=self.get_max_players()) + + self._series_length = 1 + self._ffa_series_length = 1 + + # Which game activity we're on. + self._game_number = 0 + self._playlist = SimplePlaylist(self._config, self._gametype) + config['selected'] = self._gametype.__name__ + config['config'] = self._config + ba.app.config.commit() + + # Get a game on deck ready to go. + self._current_game_spec: Optional[Dict[str, Any]] = None + self._next_game_spec: Dict[str, Any] = self._playlist.pull_next() + self._next_game: Type[ba.GameActivity] = ( + self._next_game_spec['resolved_type']) + + # Go ahead and instantiate the next game we'll + # use so it has lots of time to load. + self._instantiate_next_game() + + # Start in our custom join screen. + self.setactivity(_ba.newactivity(MultiTeamJoinActivity)) + + +class SelectGameWindow(PlaylistAddGameWindow): + + def __init__(self, transition: str = 'in_right'): + class EditController: + _sessiontype = ba.FreeForAllSession + + def get_session_type(self) -> Type[ba.Session]: + return self._sessiontype + + self._editcontroller = EditController() + self._r = 'addGameWindow' + uiscale = ba.app.ui.uiscale + self._width = 750 if uiscale is ba.UIScale.SMALL else 650 + x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + self._height = (346 if uiscale is ba.UIScale.SMALL else + 380 if uiscale is ba.UIScale.MEDIUM else 440) + top_extra = 30 if uiscale is ba.UIScale.SMALL else 20 + self._scroll_width = 210 + + self._root_widget = ba.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + scale=(2.17 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, 1) if uiscale is ba.UIScale.SMALL else (0, 0)) + + self._back_button = ba.buttonwidget(parent=self._root_widget, + position=(58 + x_inset, + self._height - 53), + size=(165, 70), + scale=0.75, + text_scale=1.2, + label=ba.Lstr(resource='backText'), + autoselect=True, + button_type='back', + on_activate_call=self._back) + self._select_button = select_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width - (172 + x_inset), self._height - 50), + autoselect=True, + size=(160, 60), + scale=0.75, + text_scale=1.2, + label=ba.Lstr(resource='selectText'), + on_activate_call=self._add) + + if ba.app.ui.use_toolbars: + ba.widget(edit=select_button, + right_widget=_ba.get_special_widget('party_button')) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 28), + size=(0, 0), + scale=1.0, + text=ba.Lstr(resource=self._r + '.titleText'), + h_align='center', + color=ba.app.ui.title_color, + maxwidth=250, + v_align='center') + v = self._height - 64 + + self._selected_title_text = ba.textwidget( + parent=self._root_widget, + position=(x_inset + self._scroll_width + 50 + 30, v - 15), + size=(0, 0), + scale=1.0, + color=(0.7, 1.0, 0.7, 1.0), + maxwidth=self._width - self._scroll_width - 150 - x_inset * 2, + h_align='left', + v_align='center') + v -= 30 + + self._selected_description_text = ba.textwidget( + parent=self._root_widget, + position=(x_inset + self._scroll_width + 50 + 30, v), + size=(0, 0), + scale=0.7, + color=(0.5, 0.8, 0.5, 1.0), + maxwidth=self._width - self._scroll_width - 150 - x_inset * 2, + h_align='left') + + scroll_height = self._height - 100 + + v = self._height - 60 + + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + position=(x_inset + 61, + v - scroll_height), + size=(self._scroll_width, + scroll_height), + highlight=False) + ba.widget(edit=self._scrollwidget, + up_widget=self._back_button, + left_widget=self._back_button, + right_widget=select_button) + self._column: Optional[ba.Widget] = None + + v -= 35 + ba.containerwidget(edit=self._root_widget, + cancel_button=self._back_button, + start_button=select_button) + self._selected_game_type: Optional[Type[ba.GameActivity]] = None + + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + + self._game_types: list[type[ba.GameActivity]] = [] + + # Get actual games loading in the bg. + ba.app.meta.load_exported_classes(ba.GameActivity, + self._on_game_types_loaded, + completion_cb_in_bg_thread=True) + + # Refresh with our initial empty list. We'll refresh again once + # game loading is complete. + self._refresh() + + if config['selected']: + for gt in self._game_types: + if gt.__name__ == config['selected']: + self._refresh(selected=gt) + self._set_selected_game_type(gt) + + def _refresh(self, + select_get_more_games_button: bool = False, + selected: bool = None) -> None: + # from ba.internal import get_game_types + + if self._column is not None: + self._column.delete() + + self._column = ba.columnwidget(parent=self._scrollwidget, + border=2, + margin=0) + + for i, gametype in enumerate(self._game_types): + + def _doit() -> None: + if self._select_button: + ba.timer(0.1, + self._select_button.activate, + timetype=ba.TimeType.REAL) + + txt = ba.textwidget(parent=self._column, + position=(0, 0), + size=(self._width - 88, 24), + text=gametype.get_display_string(), + h_align='left', + v_align='center', + color=(0.8, 0.8, 0.8, 1.0), + maxwidth=self._scroll_width * 0.8, + on_select_call=ba.Call( + self._set_selected_game_type, gametype), + always_highlight=True, + selectable=True, + on_activate_call=_doit) + if i == 0: + ba.widget(edit=txt, up_widget=self._back_button) + + self._get_more_games_button = ba.buttonwidget( + parent=self._column, + autoselect=True, + label=ba.Lstr(resource=self._r + '.getMoreGamesText'), + color=(0.54, 0.52, 0.67), + textcolor=(0.7, 0.65, 0.7), + on_activate_call=self._on_get_more_games_press, + size=(178, 50)) + if select_get_more_games_button: + ba.containerwidget(edit=self._column, + selected_child=self._get_more_games_button, + visible_child=self._get_more_games_button) + + def _add(self) -> None: + _ba.lock_all_input() # Make sure no more commands happen. + ba.timer(0.1, _ba.unlock_all_input, timetype=ba.TimeType.REAL) + gameconfig = {} + if config['selected'] == self._selected_game_type.__name__: + if config['config']: + gameconfig = config['config'] + if 'map' in gameconfig: + gameconfig['settings']['map'] = gameconfig.pop('map') + self._selected_game_type.create_settings_ui( + self._editcontroller.get_session_type(), + gameconfig, + self._edit_game_done) + + def _edit_game_done(self, config: Optional[Dict[str, Any]]) -> None: + if config: + CustomSession._config = config + CustomSession._gametype = self._selected_game_type + start_game(CustomSession) + else: + ba.app.ui.clear_main_menu_window(transition='out_right') + ba.app.ui.set_main_menu_window( + SelectGameWindow(transition='in_left').get_root_widget()) + + def _back(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + PlayWindow(transition='in_left').get_root_widget()) + + +PlayWindow._old_init = PlayWindow.__init__ + + +def __init__(self, *args, **kwargs): + self._old_init() + + width = 800 + height = 550 + + def do_quick_game() -> None: + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + SelectGameWindow().get_root_widget()) + + self._quick_game_button = ba.buttonwidget( + parent=self._root_widget, + position=(width - 55 - 120, height - 132), + autoselect=True, + size=(120, 60), + scale=1.1, + text_scale=1.2, + label=custom_txt, + on_activate_call=do_quick_game, + color=(0.54, 0.52, 0.67), + textcolor=(0.7, 0.65, 0.7)) + + self._restore_state() + + +def states(self) -> None: + return { + 'Team Games': self._teams_button, + 'Co-op Games': self._coop_button, + 'Free-for-All Games': self._free_for_all_button, + 'Back': self._back_button, + 'Quick Game': self._quick_game_button + } + + +def _save_state(self) -> None: + swapped = {v: k for k, v in states(self).items()} + if self._root_widget.get_selected_child() in swapped: + ba.app.ui.window_states[ + self.__class__.__name__] = swapped[ + self._root_widget.get_selected_child()] + else: + ba.print_exception(f'Error saving state for {self}.') + + +def _restore_state(self) -> None: + if not hasattr(self, '_quick_game_button'): + return # ensure that our monkey patched init ran + if self.__class__.__name__ not in ba.app.ui.window_states: + ba.containerwidget(edit=self._root_widget, + selected_child=self._coop_button) + return + sel = states(self).get( + ba.app.ui.window_states[self.__class__.__name__], None) + if sel: + ba.containerwidget(edit=self._root_widget, selected_child=sel) + else: + ba.containerwidget(edit=self._root_widget, + selected_child=self._coop_button) + ba.print_exception(f'Error restoring state for {self}.') + + +# ba_meta export plugin +class QuickGamePlugin(ba.Plugin): + PlayWindow.__init__ = __init__ + PlayWindow._save_state = _save_state + PlayWindow._restore_state = _restore_state diff --git a/plugins/utilities/random_colors.py b/plugins/utilities/random_colors.py new file mode 100644 index 0000000..3ea0ba6 --- /dev/null +++ b/plugins/utilities/random_colors.py @@ -0,0 +1,48 @@ +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import random +from bastd.actor import bomb + +if TYPE_CHECKING: + from typing import Sequence + + +class NewBlast(bomb.Blast): + def __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: ba.Player | None = None, + hit_type: str = 'explosion', + hit_subtype: str = 'normal', + ): + super().__init__(position, velocity, blast_radius, blast_type, + source_player, hit_type, hit_subtype) + scorch_radius = light_radius = self.radius + if self.blast_type == 'tnt': + scorch_radius *= 1.15 + scorch = ba.newnode( + 'scorch', + attrs={ + 'position': position, + 'size': scorch_radius * 0.5, + 'big': (self.blast_type == 'tnt'), + }, + ) + random_color = (random.random(), random.random(), random.random()) + scorch.color = ba.safecolor(random_color) + ba.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) + ba.timer(13.0, scorch.delete) + + +# ba_meta export plugin +class RandomColorsPlugin(ba.Plugin): + bomb.Blast = NewBlast diff --git a/plugins/utilities/share_replay.py b/plugins/utilities/share_replay.py new file mode 100644 index 0000000..705b587 --- /dev/null +++ b/plugins/utilities/share_replay.py @@ -0,0 +1,192 @@ +# ba_meta require api 7 +from __future__ import annotations +from typing import TYPE_CHECKING, cast +if TYPE_CHECKING: + from typing import Any, Sequence, Callable, List, Dict, Tuple, Optional, Union + +from os import listdir, mkdir, path, sep +from shutil import copy, copytree + +import ba +import _ba +from bastd.ui.watch import WatchWindow as ww +from bastd.ui.popup import PopupWindow + +# mod by ʟօʊքɢǟʀօʊ +# export replays to mods folder and share with your friends or have a backup + + +def Print(*args, color=None, top=None): + out = "" + for arg in args: + a = str(arg) + out += a + ba.screenmessage(out, color=color, top=top) + + +def cprint(*args): + out = "" + for arg in args: + a = str(arg) + out += a + _ba.chatmessage(out) + + +internal_dir = path.join("ba_data", "..", "..", "..", "files", "bombsquad_config", "replays" + sep) +external_dir = path.join(_ba.env()["python_directory_user"], "replays"+sep) + +# colors +pink = (1, 0.2, 0.8) +green = (0.4, 1, 0.4) +red = (1, 0, 0) + +if not path.exists(external_dir): + mkdir(external_dir) + Print("You are ready to share replays", color=pink) + + +class Help(PopupWindow): + def __init__(self): + uiscale = ba.app.ui.uiscale + self.width = 800 + self.height = 300 + + PopupWindow.__init__(self, + position=(0.0, 0.0), + size=(self.width, self.height), + scale=1.2,) + + ba.containerwidget(edit=self.root_widget, on_outside_click_call=self.close) + ba.textwidget(parent=self.root_widget, position=(0, self.height * 0.6), + text=f"•Replays are exported to\n {external_dir}\n•Importing replay and other features comming in v1.2") + + def close(self): + ba.playsound(ba.getsound('swish')) + ba.containerwidget(edit=self.root_widget, transition="out_right",) + + +class SettingWindow(): + def __init__(self): + self.draw_ui() + self.selected_widget = None + self.selected_name = None + + def on_select_text(self, widget, name): + if self.selected_widget is not None: + ba.textwidget(edit=self.selected_widget, color=(1, 1, 1)) + ba.textwidget(edit=widget, color=(1, 1, 0)) + self.selected_name = name + self.selected_widget = widget + + def draw_ui(self): + self.uiscale = ba.app.ui.uiscale + self.root = ba.Window(ba.containerwidget( + size=(900, 670), on_outside_click_call=self.close, transition="in_right")).get_root_widget() + + ba.textwidget( + parent=self.root, + size=(200, 100), + position=(150, 550), + scale=2, + selectable=False, + h_align="center", + v_align="center", + text="ShareReplay", + color=green) + + ba.buttonwidget( + parent=self.root, + position=(400, 580), + size=(35, 35), + texture=ba.gettexture("achievementEmpty"), + label="", + on_activate_call=Help) + + ba.buttonwidget( + parent=self.root, + position=(770, 460), + size=(90, 70), + scale=1.5, + label="EXPORT", + on_activate_call=self.export) + + self.close_button = ba.buttonwidget( + parent=self.root, + position=(820, 590), + size=(35, 35), + texture=ba.gettexture("crossOut"), + label="", + scale=2, + color=(1, 0.2, 0.2), + extra_touch_border_scale=3, + on_activate_call=self.close) + + scroll = ba.scrollwidget( + parent=self.root, + size=(500, 400), + position=(200, 150)) + self.scroll = ba.columnwidget(parent=scroll, size=( + 500, 900), selection_loops_to_parent=True, single_depth=True) + + height = 900 + for i in listdir(internal_dir): + height -= 40 + a = i + i = ba.textwidget( + parent=self.scroll, + size=(500, 50), + text=i.split(".")[0], + position=(10, height), + selectable=True, + max_chars=40, + click_activate=True,) + + ba.textwidget(edit=i, on_activate_call=ba.Call(self.on_select_text, i, a)) + + def export(self): + if self.selected_name is None: + Print("Select a replay", color=red) + return + copy(internal_dir+"/"+self.selected_name, external_dir+"/"+self.selected_name) + # image={"texture":ba.gettexture("bombColor"),"tint_texture":None,"tint_color":None,"tint2_color":None}) + Print(self.selected_name[0:-4]+" exported", top=True, color=pink) + + def close(self): + ba.playsound(ba.getsound('swish')) + ba.containerwidget(edit=self.root, transition="out_right",) + + +# ++++++++++++++++for keyboard navigation++++++++++++++++ + + #ba.widget(edit=self.enable_button, up_widget=decrease_button, down_widget=self.lower_text,left_widget=save_button, right_widget=save_button) + +# -------------------------------------------------------------------------------------------------- + +ww.__old_init__ = ww.__init__ + + +def new_init(self, transition="in_right", origin_widget=None): + self.__old_init__(transition, origin_widget) + self._share_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width*0.70, self._height*0.80), + size=(220, 60), + scale=1.0, + color=green, + icon=ba.gettexture('usersButton'), + iconscale=1.5, + label="SHARE REPLAY", + on_activate_call=SettingWindow) + + +# ba_meta export plugin + +class Loup(ba.Plugin): + def on_app_running(self): + ww.__init__ = new_init + + def has_settings_ui(self): + return True + + def show_settings_ui(self, button): + SettingWindow()