From b22ea228652341501077df34bfee5891ec6ec2ab Mon Sep 17 00:00:00 2001 From: Vishal Date: Thu, 21 Nov 2024 22:01:56 +0530 Subject: [PATCH] Adding more api 8 games --- dist/ba_root/mods/games/avalanche.py | 143 ++ dist/ba_root/mods/games/basket_bomb.py | 774 +++++++++++ dist/ba_root/mods/games/better_deathmatch.py | 269 ++++ dist/ba_root/mods/games/better_elimination.py | 659 +++++++++ dist/ba_root/mods/games/bomb_on_my_head.py | 339 +++++ .../ba_root/mods/games/down_into_the_abyss.py | 790 +++++++++++ dist/ba_root/mods/games/drone_war.py | 445 ++++++ dist/ba_root/mods/games/egg_game.py | 493 +++++++ dist/ba_root/mods/games/fat_pigs.py | 339 +++++ dist/ba_root/mods/games/gravity_falls.py | 35 + dist/ba_root/mods/games/hot_potato.py | 1061 ++++++++++++++ dist/ba_root/mods/games/hyper_race.py | 1238 +++++++++++++++++ dist/ba_root/mods/games/infection.py | 526 +++++++ dist/ba_root/mods/games/invisible_one.py | 333 +++++ dist/ba_root/mods/games/laser_tracer.py | 689 +++++++++ dist/ba_root/mods/games/last_punch_stand.py | 691 ++++----- .../mods/games/meteor_shower_deluxe.py | 67 + dist/ba_root/mods/games/shimla.py | 411 ++++++ dist/ba_root/mods/games/snow_ball_fight.py | 643 +++++++++ dist/ba_root/mods/games/squid_race.py | 953 +++++++++++++ dist/ba_root/mods/games/the_spaz_game.py | 127 ++ .../ba_root/mods/games/ultimate_last_stand.py | 623 +++++++++ dist/ba_root/mods/games/zombie_horde.py | 885 ++++++++++++ 23 files changed, 12117 insertions(+), 416 deletions(-) create mode 100644 dist/ba_root/mods/games/avalanche.py create mode 100644 dist/ba_root/mods/games/basket_bomb.py create mode 100644 dist/ba_root/mods/games/better_deathmatch.py create mode 100644 dist/ba_root/mods/games/better_elimination.py create mode 100644 dist/ba_root/mods/games/bomb_on_my_head.py create mode 100644 dist/ba_root/mods/games/down_into_the_abyss.py create mode 100644 dist/ba_root/mods/games/drone_war.py create mode 100644 dist/ba_root/mods/games/egg_game.py create mode 100644 dist/ba_root/mods/games/fat_pigs.py create mode 100644 dist/ba_root/mods/games/gravity_falls.py create mode 100644 dist/ba_root/mods/games/hot_potato.py create mode 100644 dist/ba_root/mods/games/hyper_race.py create mode 100644 dist/ba_root/mods/games/infection.py create mode 100644 dist/ba_root/mods/games/invisible_one.py create mode 100644 dist/ba_root/mods/games/laser_tracer.py create mode 100644 dist/ba_root/mods/games/meteor_shower_deluxe.py create mode 100644 dist/ba_root/mods/games/shimla.py create mode 100644 dist/ba_root/mods/games/snow_ball_fight.py create mode 100644 dist/ba_root/mods/games/squid_race.py create mode 100644 dist/ba_root/mods/games/the_spaz_game.py create mode 100644 dist/ba_root/mods/games/ultimate_last_stand.py create mode 100644 dist/ba_root/mods/games/zombie_horde.py diff --git a/dist/ba_root/mods/games/avalanche.py b/dist/ba_root/mods/games/avalanche.py new file mode 100644 index 0000000..43eed2f --- /dev/null +++ b/dist/ba_root/mods/games/avalanche.py @@ -0,0 +1,143 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +"""Avalancha mini-game.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations +import random +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.game.meteorshower import * +from bascenev1lib.actor.spazbot import * +from bascenev1lib.actor.spaz import PunchHitMessage +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List, Dict, Type, Type + +## MoreMinigames.py support ## +randomPic = ["lakeFrigidPreview", "hockeyStadiumPreview"] + + +def ba_get_api_version(): + return 8 + + +def ba_get_levels(): + return [ + bs._level.Level( + "Icy Emits", + gametype=IcyEmitsGame, + settings={}, + preview_texture_name=random.choice(randomPic), + ) + ] + + +## MoreMinigames.py support ## + + +class PascalBot(BrawlerBot): + color = (0, 0, 3) + highlight = (0.2, 0.2, 1) + character = "Pascal" + bouncy = True + punchiness = 0.7 + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, bs.FreezeMessage): + return + else: + super().handlemessage(msg) + + +# ba_meta export bascenev1.GameActivity +class AvalanchaGame(MeteorShowerGame): + """Minigame involving dodging falling bombs.""" + + name = "Avalanche" + description = "Dodge the ice-bombs." + available_settings = [ + bs.BoolSetting("Epic Mode", default=False), + bs.IntSetting("Difficulty", default=1, min_value=1, max_value=3, increment=1), + ] + scoreconfig = bs.ScoreConfig( + label="Survived", scoretype=bs.ScoreType.MILLISECONDS, version="B" + ) + + announce_player_deaths = True + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ["Tip Top"] + + def __init__(self, settings: dict): + super().__init__(settings) + + self._epic_mode = settings.get("Epic Mode", False) + self._last_player_death_time: Optional[float] = None + self._meteor_time = 2.0 + if settings["Difficulty"] == 1: + self._min_delay = 0.4 + elif settings["Difficulty"] == 2: + self._min_delay = 0.3 + else: + self._min_delay = 0.1 + + self._timer: Optional[OnScreenTimer] = None + self._bots = SpazBotSet() + + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + ) + if self._epic_mode: + self.slow_motion = True + + def on_transition_in(self) -> None: + super().on_transition_in() + gnode = bs.getactivity().globalsnode + gnode.tint = (0.5, 0.5, 1) + + act = bs.getactivity().map + shared = SharedObjects.get() + mat = bs.Material() + mat.add_actions(actions=("modify_part_collision", "friction", 0.18)) + + act.node.color = act.bottom.color = (1, 1, 1.2) + act.node.reflection = act.bottom.reflection = "soft" + act.node.materials = [shared.footing_material, mat] + + def _set_meteor_timer(self) -> None: + bs.timer( + (1.0 + 0.2 * random.random()) * self._meteor_time, self._drop_bomb_cluster + ) + + def _drop_bomb_cluster(self) -> None: + defs = self.map.defs + delay = 0.0 + for _i in range(random.randrange(1, 3)): + pos = defs.points["flag_default"] + pos = (pos[0], pos[1] + 0.4, pos[2]) + dropdir = -1.0 if pos[0] > 0 else 1.0 + vel = (random.randrange(-4, 4), 7.0, random.randrange(0, 4)) + bs.timer(delay, babase.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + def _drop_bomb(self, position: Sequence[float], velocity: Sequence[float]) -> None: + Bomb(position=position, velocity=velocity, bomb_type="ice").autoretain() + + def _decrement_meteor_time(self) -> None: + if self._meteor_time < self._min_delay: + return + self._meteor_time = max(0.01, self._meteor_time * 0.9) + if random.choice([0, 0, 1]) == 1: + pos = self.map.defs.points["flag_default"] + self._bots.spawn_bot(PascalBot, pos=pos, spawn_time=2) diff --git a/dist/ba_root/mods/games/basket_bomb.py b/dist/ba_root/mods/games/basket_bomb.py new file mode 100644 index 0000000..8c33edb --- /dev/null +++ b/dist/ba_root/mods/games/basket_bomb.py @@ -0,0 +1,774 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor import playerspaz as ps +from bascenev1lib import maps + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + +bsuSpaz = None + + +def getlanguage(text, sub: str = ''): + lang = bs.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(bs.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 = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': activity.ball_mesh, + '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 + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: scale*1.3, 0.26: scale}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(BallDiedMessage(self)) + + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + self.node.velocity = (0.0, 0.0, 0.0) + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +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 bascenev1.GameActivity + + +class BasketGame(bs.TeamGameActivity[Player, Team]): + + name = getlanguage('Name') + description = getlanguage('Info') + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting(getlanguage('S: Powerups'), default=True), + bs.BoolSetting(getlanguage('S: Velocity'), default=False), + bs.BoolSetting('Epic Mode', default=False), + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['BasketBall Stadium', 'BasketBall Stadium V2'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = bs.getsound('cheer') + self._chant_sound = bs.getsound('crowdChant') + self._foghorn_sound = bs.getsound('foghorn') + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self.ball_mesh = bs.getmesh('shield') + self.ball_tex = bs.gettexture('fontExtras3') + self._ball_sound = bs.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 = bs.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 = bs.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[bs.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( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': defs.boxes['goal1'][0:3], + 'scale': defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials': [] + }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': defs.boxes['goal2'][0:3], + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': [] + }))) + self._update_scoreboard() + self._chant_sound.play() + + 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 = bs.getcollision() + try: + ball = collision.sourcenode.getdelegate(Ball, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.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 = bs.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(bs.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() + + # self._foghorn_sound.play() + self._cheer_sound.play() + + self._ball.scored = True + + # Kill the ball (it'll respawn itself shortly). + bs.timer(1.0, self._kill_ball) + + light = bs.newnode('light', + attrs={ + 'position': bs.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + bs.timer(1.0, light.delete) + + bs.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.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) -> bs.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, bs.PlayerDiedMessage): + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + elif isinstance(msg, BallDiedMessage): + if not self.has_ended(): + bs.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 = bs.newnode('light', + attrs={ + 'position': self._ball_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _spawn_ball(self) -> None: + self._swipsound.play() + self._whistle_sound.play() + self._flash_ball_spawn() + assert self._ball_spawn_pos is not None + self._ball = Ball(position=self._ball_spawn_pos) + + +class Aro(bs.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 = bs.Material() + self.no_collision.add_actions( + actions=(('modify_part_collision', 'collide', False))) + + self.collision = bs.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + # Score + self._score_region_material = bs.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] + + mesh = None + tex = bs.gettexture('null') + + pmats = [self.no_collision] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': mesh, + '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 + bs.animate(self.node, 'mesh_scale', {0: 0}) + + pos = (position[0], position[1]+0.6, position[2]) + self.regions: List[bs.Node] = [ + bs.newnode('region', + attrs={'position': position, + 'scale': (0.6, 0.05, 0.6), + 'type': 'box', + 'materials': self._materials_region0}), + + bs.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(bs.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, bs.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) + + +class Cuadro(bs.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 = bs.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + pos = (position[0], position[1]+0.9, position[2]+1.5) + self.region: bs.Node = bs.newnode('region', + attrs={'position': pos, + 'scale': (0.5, 2.7, 2.5), + 'type': 'box', + 'materials': [self.collision, + shared.footing_material]}) + + # self.shield = bs.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( + bs.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, bs.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) + + +class Palos(bs.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 = bs.Material() + self.no_collision.add_actions( + actions=(('modify_part_collision', 'collide', False))) + + # + self.collision = bs.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]) + + mesh = bs.getmesh('flagPole') + tex = bs.gettexture('flagPoleColor') + + pmats = [self.no_collision] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': mesh, + '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 + bs.animate(self.node, 'mesh_scale', {0: scale}) + + self.loc = bs.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 = bs.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, bs.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 = bs.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.collision_mesh = bs.getcollisionmesh('footballStadiumCollide') + self.node.mesh = None + self.stands.mesh = None + self.floor.reflection = 'soft' + self.floor.reflection_scale = [1.6] + self.floor.color = (1.1, 0.05, 0.8) + + self.background = bs.newnode('terrain', + attrs={'mesh': bs.getmesh('thePadBG'), + 'lighting': False, + 'background': True, + 'color': (1.0, 0.2, 1.0), + 'color_texture': bs.gettexture('menuBG')}) + + gnode = bs.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 = bs.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + self.regions: List[bs.Node] = [ + bs.newnode('region', + attrs={'position': (12.676897048950195, 0.2997918128967285, 5.583303928375244), + 'scale': (1.01, 12, 28), + 'type': 'box', + 'materials': [self.collision]}), + + bs.newnode('region', + attrs={'position': (11.871315956115723, 0.29975247383117676, 5.711406707763672), + 'scale': (50, 12, 0.9), + 'type': 'box', + 'materials': [self.collision]}), + + bs.newnode('region', + attrs={'position': (-12.776557922363281, 0.30036890506744385, 4.96237850189209), + 'scale': (1.01, 12, 28), + 'type': 'box', + 'materials': [self.collision]}), + ] + + +bs._map.register_map(BasketMap) +bs._map.register_map(BasketMapV2) diff --git a/dist/ba_root/mods/games/better_deathmatch.py b/dist/ba_root/mods/games/better_deathmatch.py new file mode 100644 index 0000000..6882c8a --- /dev/null +++ b/dist/ba_root/mods/games/better_deathmatch.py @@ -0,0 +1,269 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# BetterDeathMatch +# Made by your friend: @[Just] Freak#4999 + +"""Defines a very-customisable DeathMatch mini-game""" + +# ba_meta require api 8 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class BetterDeathMatchGame(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Btrr Death Match' + description = 'Kill a set number of enemies to win.\nbyFREAK' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + + + ## Add settings ## + bs.BoolSetting('Enable Gloves', False), + bs.BoolSetting('Enable Powerups', True), + bs.BoolSetting('Night Mode', False), + bs.BoolSetting('Icy Floor', False), + bs.BoolSetting('One Punch Kill', False), + bs.BoolSetting('Spawn with Shield', False), + bs.BoolSetting('Punching Only', False), + ## Add settings ## + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = bui.getsound('dingSmall') + + +## Take applied settings ## + self._boxing_gloves = bool(settings['Enable Gloves']) + self._enable_powerups = bool(settings['Enable Powerups']) + self._night_mode = bool(settings['Night Mode']) + self._icy_floor = bool(settings['Icy Floor']) + self._one_punch_kill = bool(settings['One Punch Kill']) + self._shield_ = bool(settings['Spawn with Shield']) + self._only_punch = bool(settings['Punching Only']) +## Take applied settings ## + + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Crush ${ARG1} of your enemies. byFREAK', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies. byFREAK', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + +## Run settings related: IcyFloor ## + + + def on_transition_in(self) -> None: + super().on_transition_in() + activity = bs.getactivity() + if self._icy_floor: + activity.map.is_hockey = True + else: + return +## Run settings related: IcyFloor ## + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + + +## Run settings related: NightMode,Powerups ## + if self._night_mode: + bs.getactivity().globalsnode.tint = (0.5, 0.7, 1) + else: + pass +# -# Tried return here, pfft. Took me 30mins to figure out why pwps spawning only on NightMode +# -# Now its fixed :) + if self._enable_powerups: + self.setup_standard_powerup_drops() + else: + pass +## Run settings related: NightMode,Powerups ## + + # 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 handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + +## Run settings related: Spaz ## + + + def spawn_player(self, player: Player) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + if self._boxing_gloves: + spaz.equip_boxing_gloves() + if self._one_punch_kill: + spaz._punch_power_scale = 15 + if self._shield_: + spaz.equip_shields() + if self._only_punch: + spaz.connect_controls_to_player(enable_bomb=False, enable_pickup=False) + + return spaz +## Run settings related: Spaz ## + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/dist/ba_root/mods/games/better_elimination.py b/dist/ba_root/mods/games/better_elimination.py new file mode 100644 index 0000000..8edd4c1 --- /dev/null +++ b/dist/ba_root/mods/games/better_elimination.py @@ -0,0 +1,659 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# BetterElimination +# Made by your friend: @[Just] Freak#4999 + +# Huge Thx to Nippy for "Live Team Balance" + + +"""Defines a very-customisable Elimination mini-game""" + +# ba_meta require api 8 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import (Any, Tuple, Dict, Type, List, Sequence, Optional, + Union) + + +class Icon(bs.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: Tuple[float, float], + scale: float, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0): + super().__init__() + + self._player = player + self._show_lives = show_lives + self._show_death = show_death + self._name_scale = name_scale + self._outline_tex = bs.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = bs.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + self._name_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': flatness, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + if self._show_lives: + self._lives_text = bs.newnode('text', + owner=self.node, + attrs={ + 'text': 'x0', + 'color': (1, 1, 0.5), + 'h_align': 'left', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_position_and_scale(position, scale) + + def set_position_and_scale(self, position: Tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + if self._show_lives: + self._lives_text.position = (position[0] + scale * 10.0, + position[1] - scale * 43.0) + self._lives_text.scale = 1.0 * scale + + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + if self._player: + lives = self._player.lives + else: + lives = 0 + if self._show_lives: + if lives > 0: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + def handle_player_spawned(self) -> None: + """Our player spawned; hooray!""" + if not self.node: + return + self.node.opacity = 1.0 + self.update_for_lives() + + def handle_player_died(self) -> None: + """Well poo; our player died.""" + if not self.node: + return + if self._show_death: + bs.animate( + self.node, 'opacity', { + 0.00: 1.0, + 0.05: 0.0, + 0.10: 1.0, + 0.15: 0.0, + 0.20: 1.0, + 0.25: 0.0, + 0.30: 1.0, + 0.35: 0.0, + 0.40: 1.0, + 0.45: 0.0, + 0.50: 1.0, + 0.55: 0.2 + }) + lives = self._player.lives + if lives == 0: + bs.timer(0.6, self.update_for_lives) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.lives = 0 + self.icons: List[Icon] = [] + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.spawn_order: List[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class BetterEliminationGame(bs.TeamGameActivity[Player, Team]): + """Game type where last player(s) left alive win.""" + + name = 'Bttr Elimination' + description = 'Last remaining alive wins.\nbyFREAK' + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + none_is_winner=True) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + 'Life\'s Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + + + ## Add settings ## + bs.BoolSetting('Live Team Balance (by Nippy#2677)', True), + bs.BoolSetting('Enable Gloves', False), + bs.BoolSetting('Enable Powerups', True), + bs.BoolSetting('Night Mode', False), + bs.BoolSetting('Icy Floor', False), + bs.BoolSetting('One Punch Kill', False), + bs.BoolSetting('Spawn with Shield', False), + bs.BoolSetting('Punching Only', False), + ## Add settings ## + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Life\'s (on spawn only)', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._start_time: Optional[float] = None + self._vs_text: Optional[bs.Actor] = None + self._round_end_timer: Optional[bs.Timer] = None + +## Take applied settings ## + self._live_team_balance = bool(settings['Live Team Balance (by Nippy#2677)']) + self._boxing_gloves = bool(settings['Enable Gloves']) + self._enable_powerups = bool(settings['Enable Powerups']) + self._night_mode = bool(settings['Night Mode']) + self._icy_floor = bool(settings['Icy Floor']) + self._one_punch_kill = bool(settings['One Punch Kill']) + self._shield_ = bool(settings['Spawn with Shield']) + self._only_punch = bool(settings['Punching Only']) +## Take applied settings ## + + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = int(settings['Life\'s Per Player']) + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Life\'s (on spawn only)', False)) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Last team standing wins. byFREAK' if isinstance( + self.session, bs.DualTeamSession) else 'Last one standing wins.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'last team standing wins. byFREAK' if isinstance( + self.session, bs.DualTeamSession) else 'last one standing wins' + + def on_player_join(self, player: Player) -> None: + + # No longer allowing mid-game joiners here; too easy to exploit. + if self.has_begun(): + + # Make sure their team has survival seconds set if they're all dead + # (otherwise blocked new ffa players are considered 'still alive' + # in score tallying). + if (self._get_total_team_lives(player.team) == 0 + and player.team.survival_seconds is None): + player.team.survival_seconds = 0 + bui.screenmessage( + babase.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.team.spawn_order.append(player) + self._update_solo_mode() + else: + # Create our icon and spawn. + player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + # Don't waste time doing this until begin. + if self.has_begun(): + self._update_icons() + + +## Run settings related: IcyFloor ## + + + def on_transition_in(self) -> None: + super().on_transition_in() + activity = bs.getactivity() + if self._icy_floor: + activity.map.is_hockey = True + else: + return +## Run settings related: IcyFloor ## + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.setup_standard_time_limit(self._time_limit) + + +## Run settings related: NightMode,Powerups ## + if self._night_mode: + bs.getactivity().globalsnode.tint = (0.5, 0.7, 1) + else: + pass +# -# Tried return here, pfft. Took me 30mins to figure out why pwps spawning only on NightMode +# -# Now its fixed :) + if self._enable_powerups: + self.setup_standard_powerup_drops() + else: + pass +## Run settings related: NightMode,Powerups ## + + if self._solo_mode: + self._vs_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': babase.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + bs.timer(1.0, self._update, repeat=True) + + def _update_solo_mode(self) -> None: + # For both teams, find the first player on the spawn order list with + # lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + break + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> Optional[babase.Vec3]: + del player # Unused. + + # In solo-mode, if there's an existing live player on the map, spawn at + # whichever spot is farthest from them (keeps the action spread out). + if self._solo_mode: + living_player = None + living_player_pos = None + for team in self.teams: + for tplayer in team.players: + if tplayer.is_alive(): + assert tplayer.node + ppos = tplayer.node.position + living_player = tplayer + living_player_pos = ppos + break + if living_player: + assert living_player_pos is not None + player_pos = babase.Vec3(living_player_pos) + points: List[Tuple[float, babase.Vec3]] = [] + for team in self.teams: + start_pos = babase.Vec3(self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos)) + # Hmm.. we need to sorting vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def spawn_player(self, player: Player) -> bs.Actor: + actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + +## Run settings related: Spaz ## + if self._boxing_gloves: + actor.equip_boxing_gloves() + if self._one_punch_kill: + actor._punch_power_scale = 15 + if self._shield_: + actor.equip_shields() + if self._only_punch: + actor.connect_controls_to_player(enable_bomb=False, enable_pickup=False) + + return actor +## Run settings related: Spaz ## + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + popuptext.PopupText('x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=player.node.position).autoretain() + + def on_player_leave(self, player: Player) -> None: + # Nippy#2677 + team_count = 1 # Just initiating + if player.lives > 0 and self._live_team_balance: + team_mem = [] + for teamer in player.team.players: + if player != teamer: + team_mem.append(teamer) # Got Dead players Team + live = player.lives + team_count = len(team_mem) + # Extending Player List for Sorted Players + for i in range(int((live if live % 2 == 0 else live+1)/2)): + team_mem.extend(team_mem) + if team_count > 0: + for i in range(live): + team_mem[i].lives += 1 + + if team_count <= 0: # Draw if Player Leaves + self.end_game() + # Nippy#2677 + super().on_player_leave(player) + player.icons = [] + + # Remove us from spawn-order. + if self._solo_mode: + if player in player.team.spawn_order: + player.team.spawn_order.remove(player) + + # Update icons in a moment since our team will be gone from the + # list then. + bs.timer(0, self._update_icons) + + # If the player to leave was the last in spawn order and had + # their final turn currently in-progress, mark the survival time + # for their team. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - self._start_time) + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + babase.print_error( + "Got lives < 0 in Elim; this shouldn't happen. solo:" + + str(self._solo_mode)) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - + self._start_time) + else: + # Otherwise, in regular mode, respawn. + if not self._solo_mode: + self.respawn_player(player) + + # In solo, put ourself at the back of the spawn order. + if self._solo_mode: + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) + + def _update(self) -> None: + if self._solo_mode: + # For both teams, find the first player on the spawn order + # list with lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + self._update_icons() + break + + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0.5, self.end_game) + + def _get_living_teams(self) -> List[Team]: + return [ + team for team in self.teams + if len(team.players) > 0 and any(player.lives > 0 + for player in team.players) + ] + + def end_game(self) -> None: + if self.has_ended(): + return + results = bs.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.survival_seconds) + self.end(results=results) diff --git a/dist/ba_root/mods/games/bomb_on_my_head.py b/dist/ba_root/mods/games/bomb_on_my_head.py new file mode 100644 index 0000000..91a6a62 --- /dev/null +++ b/dist/ba_root/mods/games/bomb_on_my_head.py @@ -0,0 +1,339 @@ +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.actor.spaz import BombDiedMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor import bomb as stdbomb + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = bs.app.lang.language + +if lang == 'Spanish': + name = 'Bomba en mi Cabeza' + description = ('Siempre tendrás una bomba en la cabeza.\n' + '¡Sobrevive tanto como puedas!') + description_ingame = '¡Sobrevive tanto como puedas!' + # description_short = 'Elimina {} Jugadores para ganar' + maxbomblimit = 'Límite Máximo de Bombas' + mbltwo = 'Dos' + mblthree = 'Tres' + mblfour = 'Cuatro' +else: + name = 'Bomb on my Head' + description = ('You\'ll always have a bomb on your head.\n' + 'Survive as long as you can!') + description_ingame = 'Survive as long as you can!' + # description_short = 'Kill {} Players to win' + maxbomblimit = 'Max Bomb Limit' + mbltwo = 'Two' + mblthree = 'Three' + mblfour = 'Four' + + +class NewPlayerSpaz(PlayerSpaz): + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, BombDiedMessage): + self.bomb_count += 1 + self.check_avalible_bombs() + else: + return super().handlemessage(msg) + + def check_avalible_bombs(self) -> None: + if not self.node: + return + if self.bomb_count <= 0: + return + if not self.node.hold_node: + self.on_bomb_press() + self.on_bomb_release() + + def start_bomb_checking(self) -> None: + self.check_avalible_bombs() + self._bomb_check_timer = bs.timer( + 0.5, + bs.WeakCall(self.check_avalible_bombs), + repeat=True) + + def drop_bomb(self) -> stdbomb.Bomb | None: + lifespan = 3.0 + + if self.bomb_count <= 0 or self.frozen: + return None + assert self.node + pos = self.node.position_forward + vel = self.node.velocity + + bomb_type = 'normal' + + bomb = stdbomb.Bomb( + position=(pos[0], pos[1] - 0.0, pos[2]), + velocity=(vel[0], vel[1], vel[2]), + bomb_type=bomb_type, + blast_radius=self.blast_radius, + source_player=self.source_player, + owner=self.node, + ).autoretain() + + bs.animate(bomb.node, 'mesh_scale', { + 0.0: 0.0, + lifespan*0.1: 1.5, + lifespan*0.5: 1.0 + }) + + self.bomb_count -= 1 + bomb.node.add_death_action( + bs.WeakCall(self.handlemessage, BombDiedMessage()) + ) + self._pick_up(bomb.node) + + for clb in self._dropped_bomb_callbacks: + clb(self, bomb) + + return bomb + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: float | None = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class BombOnMyHeadGame(bs.TeamGameActivity[Player, Team]): + + name = name + description = description + scoreconfig = bs.ScoreConfig( + label='Survived', scoretype=bs.ScoreType.MILLISECONDS, version='B' + ) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[babase.Setting]: + settings = [ + bs.IntChoiceSetting( + maxbomblimit, + choices=[ + ('Normal', 1), + (mbltwo, 2), + (mblthree, 3), + (mblfour, 4), + ], + default=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._max_bomb_limit = int(settings[maxbomblimit]) + self._epic_mode = bool(settings['Epic Mode']) + self._time_limit = float(settings['Time Limit']) + self._last_player_death_time: float | None = None + self._timer: OnScreenTimer | None = None + + # Some base class overrides: + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + ) + if self._epic_mode: + self.slow_motion = True + + def get_instance_description(self) -> str | Sequence: + return description_ingame + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self._timer = OnScreenTimer() + self._timer.start() + + def spawn_player(self, player: Player) -> bs.Actor: + from babase import _math + from bascenev1._gameutils import animate + from bascenev1._coopsession import CoopSession + + if isinstance(self.session, bs.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 = babase.safecolor(color, target_intensity=0.75) + + spaz = NewPlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + bs.timer(1.0, bs.WeakCall(spaz.start_bomb_checking)) + spaz.set_bomb_count(self._max_bomb_limit) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = bs.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, bs.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + babase.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = curtime + else: + bs.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, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def end_game(self) -> None: + cur_time = bs.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 = bs.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) diff --git a/dist/ba_root/mods/games/down_into_the_abyss.py b/dist/ba_root/mods/games/down_into_the_abyss.py new file mode 100644 index 0000000..ab26ef5 --- /dev/null +++ b/dist/ba_root/mods/games/down_into_the_abyss.py @@ -0,0 +1,790 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import random +from bascenev1._map import register_map +from bascenev1lib.actor.spaz import PickupMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.spazbot import SpazBotSet, ChargerBotPro, TriggerBotPro +from bascenev1lib.actor.bomb import Blast +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = bs.app.lang.language + +if lang == 'Spanish': + name = 'Abajo en el Abismo' + description = 'Sobrevive tanto como puedas' + help = 'El mapa es 3D, ¡ten cuidado!' + author = 'Autor: Deva' + github = 'GitHub: spdv123' + blog = 'Blog: superdeva.info' + peaceTime = 'Tiempo de Paz' + npcDensity = 'Densidad de Enemigos' + hint_use_punch = '¡Ahora puedes golpear a los enemigos!' +elif lang == 'Chinese': + name = '无尽深渊' + description = '在无穷尽的坠落中存活更长时间' + help = '' + author = '作者: Deva' + github = 'GitHub: spdv123' + blog = '博客: superdeva.info' + peaceTime = '和平时间' + npcDensity = 'NPC密度' + hint_use_punch = u'现在可以使用拳头痛扁你的敌人了' +else: + name = 'Down Into The Abyss' + description = 'Survive as long as you can' + help = 'The map is 3D, be careful!' + author = 'Author: Deva' + github = 'GitHub: spdv123' + blog = 'Blog: superdeva.info' + peaceTime = 'Peace Time' + npcDensity = 'NPC Density' + hint_use_punch = 'You can punch your enemies now!' + + +class AbyssMap(bs.Map): + from bascenev1lib.mapdata import happy_thoughts as defs + # Add the y-dimension space for players + defs.boxes['map_bounds'] = (-0.8748348681, 9.212941713, -9.729538885) \ + + (0.0, 0.0, 0.0) \ + + (36.09666006, 26.19950145, 20.89541168) + name = 'Abyss Unhappy' + + @classmethod + def get_play_types(cls) -> list[str]: + """Return valid play types for this map.""" + return ['abyss'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'alwaysLandPreview' + + @classmethod + def on_preload(cls) -> Any: + data: dict[str, Any] = { + 'mesh': bs.getmesh('alwaysLandLevel'), + 'bottom_mesh': bs.getmesh('alwaysLandLevelBottom'), + 'bgmesh': bs.getmesh('alwaysLandBG'), + 'collision_mesh': bs.getcollisionmesh('alwaysLandLevelCollide'), + 'tex': bs.gettexture('alwaysLandLevelColor'), + 'bgtex': bs.gettexture('alwaysLandBGColor'), + 'vr_fill_mound_mesh': bs.getmesh('alwaysLandVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound') + } + return data + + @classmethod + def get_music_type(cls) -> bs.MusicType: + return bs.MusicType.FLYING + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -3.7, 2.5)) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['vr_fill_mound_mesh'], + 'lighting': False, + 'vr_only': True, + 'color': (0.2, 0.25, 0.2), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + gnode = bs.getactivity().globalsnode + gnode.happy_thoughts_mode = True + gnode.shadow_offset = (0.0, 8.0, 5.0) + gnode.tint = (1.3, 1.23, 1.0) + gnode.ambient_color = (1.3, 1.23, 1.0) + gnode.vignette_outer = (0.64, 0.59, 0.69) + gnode.vignette_inner = (0.95, 0.95, 0.93) + gnode.vr_near_clip = 1.0 + self.is_flying = True + + +register_map(AbyssMap) + + +class SpazTouchFoothold: + pass + + +class BombToDieMessage: + pass + + +class Foothold(bs.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + power: str = 'random', + size: float = 6.0, + breakable: bool = True, + moving: bool = False): + super().__init__() + shared = SharedObjects.get() + powerup = PowerupBoxFactory.get() + + fmesh = bs.getmesh('landMine') + fmeshs = bs.getmesh('powerupSimple') + self.died = False + self.breakable = breakable + self.moving = moving # move right and left + self.lrSig = 1 # left or right signal + self.lrSpeedPlus = random.uniform(1 / 2.0, 1 / 0.7) + self._npcBots = SpazBotSet() + + self.foothold_material = bs.Material() + self.impact_sound = bui.getsound('impactMedium') + + self.foothold_material.add_actions( + conditions=(('they_dont_have_material', shared.player_material), + 'and', + ('they_have_material', shared.object_material), + 'or', + ('they_have_material', shared.footing_material)), + actions=(('modify_node_collision', 'collide', True), + )) + + self.foothold_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'physical', True), + ('modify_part_collision', 'stiffness', 0.05), + ('message', 'our_node', 'at_connect', SpazTouchFoothold()), + )) + + self.foothold_material.add_actions( + conditions=('they_have_material', self.foothold_material), + actions=('modify_node_collision', 'collide', False), + ) + + tex = { + 'punch': powerup.tex_punch, + 'sticky_bombs': powerup.tex_sticky_bombs, + 'ice_bombs': powerup.tex_ice_bombs, + 'impact_bombs': powerup.tex_impact_bombs, + 'health': powerup.tex_health, + 'curse': powerup.tex_curse, + 'shield': powerup.tex_shield, + 'land_mines': powerup.tex_land_mines, + 'tnt': bs.gettexture('tnt'), + }.get(power, bs.gettexture('tnt')) + + powerupdist = { + powerup.tex_bomb: 3, + powerup.tex_ice_bombs: 2, + powerup.tex_punch: 3, + powerup.tex_impact_bombs: 3, + powerup.tex_land_mines: 3, + powerup.tex_sticky_bombs: 4, + powerup.tex_shield: 4, + powerup.tex_health: 3, + powerup.tex_curse: 1, + bs.gettexture('tnt'): 2 + } + + self.randtex = [] + + for keyTex in powerupdist: + for i in range(powerupdist[keyTex]): + self.randtex.append(keyTex) + + if power == 'random': + random.seed() + tex = random.choice(self.randtex) + + self.tex = tex + self.powerup_type = { + powerup.tex_punch: 'punch', + powerup.tex_bomb: 'triple_bombs', + powerup.tex_ice_bombs: 'ice_bombs', + powerup.tex_impact_bombs: 'impact_bombs', + powerup.tex_land_mines: 'land_mines', + powerup.tex_sticky_bombs: 'sticky_bombs', + powerup.tex_shield: 'shield', + powerup.tex_health: 'health', + powerup.tex_curse: 'curse', + bs.gettexture('tnt'): 'tnt' + }.get(self.tex, '') + + self._spawn_pos = (position[0], position[1], position[2]) + + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'body': 'landMine', + 'position': self._spawn_pos, + 'mesh': fmesh, + 'light_mesh': fmeshs, + 'shadow_size': 0.5, + 'velocity': (0, 0, 0), + 'density': 90000000000, + 'sticky': False, + 'body_scale': size, + 'mesh_scale': size, + 'color_texture': tex, + 'reflection': 'powerup', + 'is_area_of_interest': True, + 'gravity_scale': 0.0, + 'reflection_scale': [0], + 'materials': [self.foothold_material, + shared.object_material, + shared.footing_material] + }) + self.touchedSpazs = set() + self.keep_vel() + + def keep_vel(self) -> None: + if self.node and not self.died: + speed = bs.getactivity().cur_speed + if self.moving: + if abs(self.node.position[0]) > 10: + self.lrSig *= -1 + self.node.velocity = ( + self.lrSig * speed * self.lrSpeedPlus, speed, 0) + bs.timer(0.1, bs.WeakCall(self.keep_vel)) + else: + self.node.velocity = (0, speed, 0) + # self.node.extraacceleration = (0, self.speed, 0) + bs.timer(0.1, bs.WeakCall(self.keep_vel)) + + def tnt_explode(self) -> None: + pos = self.node.position + Blast(position=pos, + blast_radius=6.0, + blast_type='tnt', + source_player=None).autoretain() + + def spawn_npc(self) -> None: + if not self.breakable: + return + if self._npcBots.have_living_bots(): + return + if random.randint(0, 3) >= bs.getactivity().npc_density: + return + pos = self.node.position + pos = (pos[0], pos[1] + 1, pos[2]) + self._npcBots.spawn_bot( + bot_type=random.choice([ChargerBotPro, TriggerBotPro]), + pos=pos, + spawn_time=10) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + self.died = True + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + elif isinstance(msg, BombToDieMessage): + if self.powerup_type == 'tnt': + self.tnt_explode() + self.handlemessage(bs.DieMessage()) + elif isinstance(msg, bs.HitMessage): + ispunched = (msg.srcnode and msg.srcnode.getnodetype() == 'spaz') + if not ispunched: + if self.breakable: + self.handlemessage(BombToDieMessage()) + elif isinstance(msg, SpazTouchFoothold): + node = bs.getcollision().opposingnode + if node is not None and node: + try: + spaz = node.getdelegate(object) + if not isinstance(spaz, AbyssPlayerSpaz): + return + if spaz in self.touchedSpazs: + return + self.touchedSpazs.add(spaz) + self.spawn_npc() + spaz.fix_2D_position() + if self.powerup_type not in ['', 'tnt']: + node.handlemessage( + bs.PowerupMessage(self.powerup_type)) + except Exception as e: + print(e) + pass + + +class AbyssPlayerSpaz(PlayerSpaz): + + def __init__(self, + player: bs.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().__init__(player=player, + color=color, + highlight=highlight, + character=character, + powerups_expire=powerups_expire) + self.node.fly = False + self.node.hockey = True + self.hitpoints_max = self.hitpoints = 1500 # more HP to handle drop + bs.timer(bs.getactivity().peace_time, + bs.WeakCall(self.safe_connect_controls_to_player)) + + def safe_connect_controls_to_player(self) -> None: + try: + self.connect_controls_to_player() + except: + pass + + def on_move_up_down(self, value: float) -> None: + """ + Called to set the up/down joystick amount on this spaz; + used for player or AI connections. + value will be between -32768 to 32767 + WARNING: deprecated; use on_move instead. + """ + if not self.node: + return + if self.node.run > 0.1: + self.node.move_up_down = value + else: + self.node.move_up_down = value / 3. + + def on_move_left_right(self, value: float) -> None: + """ + Called to set the left/right joystick amount on this spaz; + used for player or AI connections. + value will be between -32768 to 32767 + WARNING: deprecated; use on_move instead. + """ + if not self.node: + return + if self.node.run > 0.1: + self.node.move_left_right = value + else: + self.node.move_left_right = value / 1.5 + + def fix_2D_position(self) -> None: + self.node.fly = True + bs.timer(0.02, bs.WeakCall(self.disable_fly)) + + def disable_fly(self) -> None: + if self.node: + self.node.fly = False + + def curse(self) -> None: + """ + Give this poor spaz a curse; + he will explode in 5 seconds. + """ + if not self._cursed: + factory = SpazFactory.get() + self._cursed = True + + # Add the curse material. + for attr in ['materials', 'roller_materials']: + materials = getattr(self.node, attr) + if factory.curse_material not in materials: + setattr(self.node, attr, + materials + (factory.curse_material, )) + + # None specifies no time limit + assert self.node + if self.curse_time == -1: + self.node.curse_death_time = -1 + else: + # Note: curse-death-time takes milliseconds. + tval = bs.time() + assert isinstance(tval, (float, int)) + self.node.curse_death_time = bs.time() + 15 + bs.timer(15, bs.WeakCall(self.curse_explode)) + + def handlemessage(self, msg: Any) -> Any: + dontUp = False + + if isinstance(msg, PickupMessage): + dontUp = True + collision = bs.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + + if opposingnode is None or not opposingnode: + return True + opposingdelegate = opposingnode.getdelegate(object) + # Don't pick up the foothold + if isinstance(opposingdelegate, Foothold): + return True + + # dont allow picking up of invincible dudes + try: + if opposingnode.invincible: + return True + except Exception: + pass + + # if we're grabbing the pelvis of a non-shattered spaz, + # we wanna grab the torso instead + if (opposingnode.getnodetype() == 'spaz' + and not opposingnode.shattered and opposingbody == 4): + opposingbody = 1 + + # Special case - if we're holding a flag, don't replace it + # (hmm - should make this customizable or more low level). + held = self.node.hold_node + if held and held.getnodetype() == 'flag': + return True + + # Note: hold_body needs to be set before hold_node. + self.node.hold_body = opposingbody + self.node.hold_node = opposingnode + + if not dontUp: + PlayerSpaz.handlemessage(self, msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: float | None = None + self.notIn: bool = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class AbyssGame(bs.TeamGameActivity[Player, Team]): + + name = name + description = description + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.MILLISECONDS, + version='B') + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + # We're currently hard-coded for one map. + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Abyss Unhappy'] + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.FloatChoiceSetting( + peaceTime, + choices=[ + ('None', 0.0), + ('Shorter', 2.5), + ('Short', 5.0), + ('Normal', 10.0), + ('Long', 15.0), + ('Longer', 20.0), + ], + default=10.0, + ), + bs.FloatChoiceSetting( + npcDensity, + choices=[ + ('0%', 0), + ('25%', 1), + ('50%', 2), + ('75%', 3), + ('100%', 4), + ], + default=2, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession) + or issubclass(sessiontype, bs.CoopSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + self._epic_mode = settings.get('Epic Mode', False) + self._last_player_death_time: float | None = None + self._timer: OnScreenTimer | None = None + self.fix_y = -5.614479365 + self.start_z = 0 + self.init_position = (0, self.start_z, self.fix_y) + self.team_init_positions = [(-5, self.start_z, self.fix_y), + (5, self.start_z, self.fix_y)] + self.cur_speed = 2.5 + # TODO: The variable below should be set in settings + self.peace_time = float(settings[peaceTime]) + self.npc_density = float(settings[npcDensity]) + + # Some base class overrides: + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + if self._epic_mode: + self.slow_motion = True + + self._game_credit = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'vr_depth': 0, + 'color': (0.0, 0.7, 1.0), + 'shadow': 1.0 if True else 0.5, + 'flatness': 1.0 if True else 0.5, + 'position': (0, 0), + 'scale': 0.8, + 'text': ' | '.join([author, github, blog]) + })) + + def get_instance_description(self) -> str | Sequence: + return description + + def get_instance_description_short(self) -> str | Sequence: + return self.get_instance_description() + '\n' + help + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + player.notIn = True + bs.broadcastmessage(babase.Lstr( + resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0)) + self.spawn_player(player) + + def on_begin(self) -> None: + super().on_begin() + self._timer = OnScreenTimer() + self._timer.start() + + self.level_cnt = 1 + + if self.teams_or_ffa() == 'teams': + ip0 = self.team_init_positions[0] + ip1 = self.team_init_positions[1] + Foothold( + (ip0[0], ip0[1] - 2, ip0[2]), + power='shield', breakable=False).autoretain() + Foothold( + (ip1[0], ip1[1] - 2, ip1[2]), + power='shield', breakable=False).autoretain() + else: + ip = self.init_position + Foothold( + (ip[0], ip[1] - 2, ip[2]), + power='shield', breakable=False).autoretain() + + bs.timer(int(5.0 / self.cur_speed), + bs.WeakCall(self.add_foothold), repeat=True) + + # Repeat check game end + bs.timer(1.0, self._check_end_game, repeat=True) + bs.timer(self.peace_time + 0.1, + bs.WeakCall(self.tip_hint, hint_use_punch)) + bs.timer(6.0, bs.WeakCall(self.faster_speed), repeat=True) + + def tip_hint(self, text: str) -> None: + bs.broadcastmessage(text, color=(0.2, 0.2, 1)) + + def faster_speed(self) -> None: + self.cur_speed *= 1.15 + + def add_foothold(self) -> None: + ip = self.init_position + ip_1 = (ip[0] - 7, ip[1], ip[2]) + ip_2 = (ip[0] + 7, ip[1], ip[2]) + ru = random.uniform + self.level_cnt += 1 + if self.level_cnt % 3: + Foothold(( + ip_1[0] + ru(-5, 5), + ip[1] - 2, + ip[2] + ru(-0.0, 0.0))).autoretain() + Foothold(( + ip_2[0] + ru(-5, 5), + ip[1] - 2, + ip[2] + ru(-0.0, 0.0))).autoretain() + else: + Foothold(( + ip[0] + ru(-8, 8), + ip[1] - 2, + ip[2]), moving=True).autoretain() + + def teams_or_ffa(self) -> None: + if isinstance(self.session, bs.DualTeamSession): + return 'teams' + return 'ffa' + + def spawn_player_spaz(self, + player: Player, + position: Sequence[float] = (0, 0, 0), + angle: float | None = None) -> PlayerSpaz: + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from babase import _math + from bascenev1._gameutils import animate + + position = self.init_position + if self.teams_or_ffa() == 'teams': + position = self.team_init_positions[player.team.id % 2] + angle = None + + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = _babase.safecolor(color, target_intensity=0.75) + spaz = AbyssPlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=True, + enable_pickup=False) + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + return spaz + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = bs.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, bs.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + babase.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = curtime + else: + bs.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, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 0: + self.end_game() + + def end_game(self) -> None: + cur_time = bs.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 + if player.notIn: + player.death_time = 0 + + # 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 = bs.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) diff --git a/dist/ba_root/mods/games/drone_war.py b/dist/ba_root/mods/games/drone_war.py new file mode 100644 index 0000000..6dcd5c3 --- /dev/null +++ b/dist/ba_root/mods/games/drone_war.py @@ -0,0 +1,445 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# +""" +DroneWar - Attack enemies with drone, Fly with drone and fire rocket launcher. +Author: Mr.Smoothy +Discord: https://discord.gg/ucyaesh +Youtube: https://www.youtube.com/c/HeySmoothy +Website: https://bombsquad-community.web.app +Github: https://github.com/bombsquad-community +""" +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from babase._mgen.enums import InputType +from bascenev1lib.actor.bomb import Blast + +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.playerspaz import PlayerSpaz, PlayerT +from bascenev1lib.game.deathmatch import DeathMatchGame, Player +from bascenev1lib.actor import spaz +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + +STORAGE_ATTR_NAME = f'_shared_{__name__}_factory' + +# SMoothy's Drone (fixed version of floater) with rocket launcher +# use drone as long as you want , unlike floater which dies after being idle. + + +class Drone(bs.Actor): + def __init__(self, spaz): + super().__init__() + shared = SharedObjects.get() + self._drone_material = bs.Material() + self.loop_ascend = None + self.loop_descend = None + self.loop_lr = None + self.loop_ud = None + self.rocket_launcher = None + self.x_direction = 0 + self.z_direction = 0 + self.spaz = spaz + self._drone_material.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_node_collision', 'collide', True), + ('modify_part_collision', 'physical', True))) + self._drone_material.add_actions( + conditions=(('they_have_material', + shared.object_material), 'or', + ('they_have_material', + shared.footing_material), 'or', + ('they_have_material', + self._drone_material)), + actions=('modify_part_collision', 'physical', False)) + self.node = bs.newnode( + 'prop', + delegate=self, + owner=None, + attrs={ + 'position': spaz.node.position, + 'mesh': bs.getmesh('landMine'), + 'light_mesh': bs.getmesh('landMine'), + 'body': 'landMine', + 'body_scale': 1, + 'mesh_scale': 1, + 'shadow_size': 0.25, + 'density': 999999, + 'gravity_scale': 0.0, + 'color_texture': bs.gettexture('achievementCrossHair'), + 'reflection': 'soft', + 'reflection_scale': [0.25], + 'materials': [shared.footing_material, self._drone_material] + }) + self.grab_node = bs.newnode( + 'prop', + owner=self.node, + attrs={ + 'position': (0, 0, 0), + 'body': 'sphere', + 'mesh': None, + 'color_texture': None, + 'body_scale': 0.2, + 'reflection': 'powerup', + 'density': 999999, + 'reflection_scale': [1.0], + 'mesh_scale': 0.2, + 'gravity_scale': 0, + 'shadow_size': 0.1, + 'is_area_of_interest': True, + 'materials': [shared.object_material, self._drone_material] + }) + self.node.connectattr('position', self.grab_node, 'position') + self._rcombine = bs.newnode('combine', + owner=self.node, + attrs={ + 'input0': self.spaz.node.position[0], + 'input1': self.spaz.node.position[1]+3, + 'input2': self.spaz.node.position[2], + 'size': 3 + }) + + self._rcombine.connectattr('output', self.node, 'position') + + def set_rocket_launcher(self, launcher: RocketLauncher): + self.rocket_launcher = launcher + + def fire(self): + if hasattr(self.grab_node, "position"): + self.rocket_launcher.shot(self.spaz, self.x_direction, self.z_direction, ( + self.grab_node.position[0], self.grab_node.position[1] - 1, self.grab_node.position[2])) + + def ascend(self): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input1', { + 0: self.node.position[1], + 1: self.node.position[1] + 2 + }) + loop() + self.loop_ascend = bs.Timer(1, loop, repeat=True) + + def pause_movement(self): + self.loop_ascend = None + + def decend(self): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input1', { + 0: self.node.position[1], + 1: self.node.position[1] - 2 + }) + loop() + self.loop_ascend = bs.Timer(1, loop, repeat=True) + + def pause_lr(self): + self.loop_lr = None + + def pause_ud(self): + self.loop_ud = None + + def left_(self, value=-1): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input0', { + 0: self.node.position[0], + 1: self.node.position[0] + 2 * value + }) + if value == 0.0: + self.loop_lr = None + else: + self.x_direction = value + self.z_direction = 0 + loop() + self.loop_lr = bs.Timer(1, loop, repeat=True) + + def right_(self, value=1): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input0', { + 0: self.node.position[0], + 1: self.node.position[0] + 2 * value + }) + if value == 0.0: + self.loop_lr = None + else: + self.x_direction = value + self.z_direction = 0 + loop() + self.loop_lr = bs.Timer(1, loop, repeat=True) + + def up_(self, value=1): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input2', { + 0: self.node.position[2], + 1: self.node.position[2] - 2 * value + }) + if value == 0.0: + self.loop_ud = None + else: + self.x_direction = 0 + self.z_direction = - value + loop() + self.loop_ud = bs.Timer(1, loop, repeat=True) + + def down_(self, value=-1): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input2', { + 0: self.node.position[2], + 1: self.node.position[2] - 2 * value + }) + if value == 0.0: + self.loop_ud = None + else: + self.x_direction = 0 + self.z_direction = - value + loop() + self.loop_ud = bs.Timer(1, loop, repeat=True) + + def handlemessage(self, msg): + if isinstance(msg, bs.DieMessage): + self.node.delete() + self.grab_node.delete() + self.loop_ascend = None + self.loop_ud = None + self.loop_lr = None + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + else: + super().handlemessage(msg) + +# =============================================Copied from Quake Game - Dliwk ===================================================================== + + +class RocketFactory: + """Quake Rocket factory""" + + def __init__(self) -> None: + self.ball_material = bs.Material() + + self.ball_material.add_actions( + conditions=((('we_are_younger_than', 5), 'or', + ('they_are_younger_than', 5)), 'and', + ('they_have_material', + SharedObjects.get().object_material)), + actions=('modify_node_collision', 'collide', False)) + + self.ball_material.add_actions( + conditions=('they_have_material', + SharedObjects.get().pickup_material), + actions=('modify_part_collision', 'use_node_collide', False)) + + self.ball_material.add_actions(actions=('modify_part_collision', + 'friction', 0)) + + self.ball_material.add_actions( + conditions=(('they_have_material', + SharedObjects.get().footing_material), 'or', + ('they_have_material', + SharedObjects.get().object_material)), + actions=('message', 'our_node', 'at_connect', ImpactMessage())) + + @classmethod + def get(cls): + """Get factory if exists else create new""" + activity = bs.getactivity() + if hasattr(activity, STORAGE_ATTR_NAME): + return getattr(activity, STORAGE_ATTR_NAME) + factory = cls() + setattr(activity, STORAGE_ATTR_NAME, factory) + return factory + + +class RocketLauncher: + """Very dangerous weapon""" + + def __init__(self): + self.last_shot = bs.time() + + def give(self, spaz: spaz.Spaz) -> None: + """Give spaz a rocket launcher""" + spaz.punch_callback = self.shot + self.last_shot = bs.time() + + # FIXME + # noinspection PyUnresolvedReferences + def shot(self, spaz, x, z, position) -> None: + """Release a rocket""" + time = bs.time() + if time - self.last_shot > 0.6: + self.last_shot = time + + direction = [x, 0, z] + direction[1] = 0.0 + + mag = 10.0 / \ + 1 if babase.Vec3(*direction).length() == 0 else babase.Vec3(*direction).length() + vel = [v * mag for v in direction] + Rocket(position=position, + velocity=vel, + owner=spaz.getplayer(bs.Player), + source_player=spaz.getplayer(bs.Player), + color=spaz.node.color).autoretain() + + +class ImpactMessage: + """Rocket touched something""" + + +class Rocket(bs.Actor): + """Epic rocket from rocket launcher""" + + def __init__(self, + position=(0, 5, 0), + velocity=(1, 0, 0), + source_player=None, + owner=None, + color=(1.0, 0.2, 0.2)) -> None: + super().__init__() + self.source_player = source_player + self.owner = owner + self._color = color + factory = RocketFactory.get() + + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'mesh': bs.getmesh('impactBomb'), + 'body': 'sphere', + 'color_texture': bs.gettexture( + 'bunnyColor'), + 'mesh_scale': 0.2, + 'is_area_of_interest': True, + 'body_scale': 0.8, + 'materials': [ + SharedObjects.get().object_material, + factory.ball_material] + }) # yapf: disable + self.node.extra_acceleration = (self.node.velocity[0] * 200, 0, + self.node.velocity[2] * 200) + + self._life_timer = bs.Timer( + 5, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + self._emit_timer = bs.Timer(0.001, bs.WeakCall(self.emit), repeat=True) + self.base_pos_y = self.node.position[1] + + bs.camerashake(5.0) + + def emit(self) -> None: + """Emit a trace after rocket""" + bs.emitfx(position=self.node.position, + scale=0.4, + spread=0.01, + chunk_type='spark') + if not self.node: + return + self.node.position = (self.node.position[0], self.base_pos_y, + self.node.position[2]) # ignore y + bs.newnode('explosion', + owner=self.node, + attrs={ + 'position': self.node.position, + 'radius': 0.2, + 'color': self._color + }) + + def handlemessage(self, msg: Any) -> Any: + """Message handling for rocket""" + super().handlemessage(msg) + if isinstance(msg, ImpactMessage): + self.node.handlemessage(bs.DieMessage()) + + elif isinstance(msg, bs.DieMessage): + if self.node: + Blast(position=self.node.position, + blast_radius=2, + source_player=self.source_player) + + self.node.delete() + self._emit_timer = None + + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + + +# ba_meta export bascenev1.GameActivity +class ChooseQueen(DeathMatchGame): + name = 'Drone War' + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + def spawn_player_spaz( + self, + player: PlayerT, + position: Sequence[float] | None = None, + angle: float | None = None, + ) -> PlayerSpaz: + spaz = super().spawn_player_spaz(player, position, angle) + self.spawn_drone(spaz) + return spaz + + def on_begin(self): + super().on_begin() + shared = SharedObjects.get() + self.ground_material = bs.Material() + self.ground_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('call', 'at_connect', babase.Call(self._handle_player_collide)), + ), + ) + pos = (0, 0.1, -5) + self.main_region = bs.newnode('region', attrs={'position': pos, 'scale': ( + 30, 0.001, 23), 'type': 'box', 'materials': [shared.footing_material, self.ground_material]}) + + def _handle_player_collide(self): + try: + player = bs.getcollision().opposingnode.getdelegate( + PlayerSpaz, True) + except bs.NotFoundError: + return + if player.is_alive(): + player.shatter(True) + + def spawn_drone(self, spaz): + with bs.get_foreground_host_activity().context: + + drone = Drone(spaz) + r_launcher = RocketLauncher() + drone.set_rocket_launcher(r_launcher) + player = spaz.getplayer(Player, True) + spaz.node.hold_node = drone.grab_node + player.actor.disconnect_controls_from_player() + player.resetinput() + player.assigninput(InputType.PICK_UP_PRESS, drone.ascend) + player.assigninput(InputType.PICK_UP_RELEASE, drone.pause_movement) + player.assigninput(InputType.JUMP_PRESS, drone.decend) + player.assigninput(InputType.PUNCH_PRESS, drone.fire) + player.assigninput(InputType.LEFT_PRESS, drone.left_) + player.assigninput(InputType.RIGHT_PRESS, drone.right_) + player.assigninput(InputType.LEFT_RELEASE, drone.pause_lr) + player.assigninput(InputType.RIGHT_RELEASE, drone.pause_lr) + player.assigninput(InputType.UP_PRESS, drone.up_) + player.assigninput(InputType.DOWN_PRESS, drone.down_) + player.assigninput(InputType.UP_RELEASE, drone.pause_ud) + player.assigninput(InputType.DOWN_RELEASE, drone.pause_ud) diff --git a/dist/ba_root/mods/games/egg_game.py b/dist/ba_root/mods/games/egg_game.py new file mode 100644 index 0000000..09ef244 --- /dev/null +++ b/dist/ba_root/mods/games/egg_game.py @@ -0,0 +1,493 @@ +# Ported by brostos to api 8 +# Tool used to make porting easier.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. + +"""Egg game and support classes.""" +# The Egg Game - throw egg as far as you can +# created in BCS (Bombsquad Consultancy Service) - opensource bombsquad mods for all +# discord.gg/ucyaesh join now and give your contribution +# The Egg game by mr.smoothy +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.flag import Flag +import math +import random +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +class Puck(bs.Actor): + """A lovely giant hockey puck.""" + + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch = None + self.scored = False + self.egg_mesh = bs.getmesh('egg') + self.egg_tex_1 = bs.gettexture('eggTex1') + self.egg_tex_2 = bs.gettexture('eggTex2') + self.egg_tex_3 = bs.gettexture('eggTex3') + self.eggtx = [self.egg_tex_1, self.egg_tex_2, self.egg_tex_3] + regg = random.randrange(0, 3) + assert activity is not None + assert isinstance(activity, EggGame) + pmats = [shared.object_material, activity.puck_material] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': self.egg_mesh, + 'color_texture': self.eggtx[regg], + 'body': 'capsule', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.5, + 'body_scale': 0.7, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 0.7, 0.26: 0.6}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch = s_player + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class EggGame(bs.TeamGameActivity[Player, Team]): + """Egg game.""" + + name = 'Epic Egg Game' + description = 'Score some goals.' + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('40 Seconds', 40), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.1), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('football') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self.slow_motion = True + self._scoreboard = Scoreboard() + self._cheer_sound = bui.getsound('cheer') + self._chant_sound = bui.getsound('crowdChant') + self._foghorn_sound = bui.getsound('foghorn') + self._swipsound = bui.getsound('swip') + self._whistle_sound = bui.getsound('refWhistle') + self.puck_mesh = bs.getmesh('bomb') + self.puck_tex = bs.gettexture('landMine') + self.puck_scored_tex = bs.gettexture('landMineLit') + self._puck_sound = bui.getsound('metalHit') + self.puck_material = bs.Material() + self._fake_wall_material = bs.Material() + self.HIGHEST = 0 + self._fake_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + # self.puck_material.add_actions(conditions=('they_have_material', + # shared.footing_material), + # actions=('impact_sound', + # self._puck_sound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', bs.DieMessage()))) + # self.puck_material.add_actions( + # conditions=('they_have_material',shared.footing_material) + # actions=(('modify_part_collision', 'collide', + # True), ('modify_part_collision', 'physical', True), + # ('call', 'at_connect', self._handle_egg_collision)) + # ) + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self.main_ground_material = bs.Material() + + self.main_ground_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_egg_collision))) + + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[bs.NodeActor]] = None + self._puck: Optional[Puck] = None + self._pucks = [] + 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 "Throw Egg as far u can" + + def get_instance_description_short(self) -> Union[str, Sequence]: + return "Throw Egg as far u can" + + def on_begin(self) -> None: + super().on_begin() + if self._time_limit == 0.0: + self._time_limit = 60 + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + self._puck_spawn_pos = self.map.get_flag_position(None) + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + pos = (11.88630542755127, 0.3009839951992035, 1.33331298828125) + # mat=bs.Material() + # mat.add_actions( + + # actions=( ('modify_part_collision','physical',True), + # ('modify_part_collision','collide',True)) + # ) + # self._score_regions.append( + # bs.NodeActor( + # bs.newnode('region', + # attrs={ + # 'position': pos, + # 'scale': (2,3,5), + # 'type': 'box', + # 'materials': [self._score_region_material] + # }))) + # pos=(-11.88630542755127, 0.3009839951992035, 1.33331298828125) + # self._score_regions.append( + # bs.NodeActor( + # bs.newnode('region', + # attrs={ + # 'position': pos, + # 'scale': (2,3,5), + # 'type': 'box', + # 'materials': [self._score_region_material] + # }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (-9.21, defs.boxes['goal2'][0:3][1], defs.boxes['goal2'][0:3][2]), + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': (self._fake_wall_material, ) + }))) + pos = (0, 0.1, -5) + self.main_ground = bs.newnode('region', attrs={'position': pos, 'scale': ( + 25, 0.001, 22), 'type': 'box', 'materials': [self.main_ground_material]}) + self._update_scoreboard() + self._chant_sound.play() + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = bs.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + puck.last_players_to_touch = player + + def _kill_puck(self) -> None: + self._puck = None + + def _handle_egg_collision(self) -> None: + + no = bs.getcollision().opposingnode + pos = no.position + egg = no.getdelegate(Puck) + source_player = egg.last_players_to_touch + if source_player == None or pos[0] < -8 or not source_player.node.exists(): + return + + try: + col = source_player.team.color + self.flagg = Flag(pos, touchable=False, color=col).autoretain() + self.flagg.is_area_of_interest = True + player_pos = source_player.node.position + + distance = math.sqrt(pow(player_pos[0]-pos[0], 2) + pow(player_pos[2]-pos[2], 2)) + + dis_mark = bs.newnode('text', + + attrs={ + 'text': str(round(distance, 2))+"m", + 'in_world': True, + 'scale': 0.02, + 'h_align': 'center', + 'position': (pos[0], 1.6, pos[2]), + 'color': col + }) + bs.animate(dis_mark, 'scale', { + 0.0: 0, 0.5: 0.01 + }) + if distance > self.HIGHEST: + self.HIGHEST = distance + self.stats.player_scored( + source_player, + 10, + big_message=False) + + no.delete() + bs.timer(2, self._spawn_puck) + source_player.team.score = int(distance) + + except (): + pass + + def spawn_player(self, player: Player) -> bs.Actor: + + zoo = random.randrange(-4, 5) + pos = (-11.204887390136719, 0.2998693287372589, zoo) + spaz = self.spawn_player_spaz( + player, position=pos, angle=90) + assert spaz.node + + # Prevent controlling of characters before the start of the race. + + return spaz + + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = bs.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 20, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + self._foghorn_sound.play() + self._cheer_sound.play() + + # self._puck.scored = True + + # Change puck texture to something cool + # self._puck.node.color_texture = self.puck_scored_tex + # Kill the puck (it'll respawn itself shortly). + bs.timer(1.0, self._kill_puck) + + # light = bs.newnode('light', + # attrs={ + # 'position': bs.getcollision().position, + # 'height_attenuated': False, + # 'color': (1, 0, 0) + # }) + # bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + # bs.timer(1.0, light.delete) + + bs.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + # for team in self.teams: + # self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + bs.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + # light = bs.newnode('light', + # attrs={ + # 'position': self._puck_spawn_pos, + # 'height_attenuated': False, + # 'color': (1, 0, 0) + # }) + # bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + # bs.timer(1.0, light.delete) + pass + + def _spawn_puck(self) -> None: + # self._swipsound.play() + # self._whistle_sound.play() + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + zoo = random.randrange(-5, 6) + pos = (-11.204887390136719, 0.2998693287372589, zoo) + self._pucks.append(Puck(position=pos)) diff --git a/dist/ba_root/mods/games/fat_pigs.py b/dist/ba_root/mods/games/fat_pigs.py new file mode 100644 index 0000000..78c56c3 --- /dev/null +++ b/dist/ba_root/mods/games/fat_pigs.py @@ -0,0 +1,339 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 + +# - - - - - - - - - - - - - - - - - - - - - +# - Fat-Pigs! by Zacker Tz || Zacker#5505 - +# - Version 0.01 :v - +# - - - - - - - - - - - - - - - - - - - - - + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import random +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Union, Sequence, Optional + +# - - - - - - - Mini - Settings - - - - - - - - - - - - - - - - # + +zkBombs_limit = 3 # Number of bombs you can use | Default = 3 +zkPunch = False # Enable/Disable punchs | Default = False +zkPickup = False # Enable/Disable pickup | Default = False + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + +# ba_meta export bascenev1.GameActivity + + +class FatPigs(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Fat-Pigs!' + description = 'Survive...' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=0.25, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Courtyard', 'Rampage', 'Monkey Face', 'Lake Frigid', 'Step Right Up'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._meteor_time = 2.0 + self._score_to_win: Optional[int] = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + # self._text_credit = bool(settings['Credits']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + # Ambiente + gnode = bs.getactivity().globalsnode + gnode.tint = (0.8, 1.2, 0.8) + gnode.ambient_color = (0.7, 1.0, 0.6) + gnode.vignette_outer = (0.4, 0.6, 0.4) # C + # gnode.vignette_inner = (0.9, 0.9, 0.9) + + # 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() + + delay = 5.0 if len(self.players) > 2 else 2.5 + if self._epic_mode: + delay *= 0.25 + bs.timer(delay, self._decrement_meteor_time, repeat=False) + + # Kick off the first wave in a few seconds. + delay = 3.0 + if self._epic_mode: + delay *= 0.25 + bs.timer(delay, self._set_meteor_timer) + + # self._timer = OnScreenTimer() + # self._timer.start() + + # Check for immediate end (if we've only got 1 player, etc). + bs.timer(5.0, self._check_end_game) + + t = bs.newnode('text', + attrs={'text': "Minigame by Zacker Tz", + 'scale': 0.7, + 'position': (0.001, 625), + 'shadow': 0.5, + 'opacity': 0.7, + 'flatness': 1.2, + 'color': (0.6, 1, 0.6), + 'h_align': 'center', + 'v_attach': 'bottom'}) + + def spawn_player(self, player: Player) -> bs.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=zkPunch, + enable_bomb=True, + enable_pickup=zkPickup) + + spaz.bomb_count = zkBombs_limit + spaz._max_bomb_count = zkBombs_limit + spaz.bomb_type_default = 'sticky' + spaz.bomb_type = 'sticky' + + # cerdo gordo + spaz.node.color_mask_texture = bs.gettexture('melColorMask') + spaz.node.color_texture = bs.gettexture('melColor') + spaz.node.head_mesh = bs.getmesh('melHead') + spaz.node.hand_mesh = bs.getmesh('melHand') + spaz.node.torso_mesh = bs.getmesh('melTorso') + spaz.node.pelvis_mesh = bs.getmesh('kronkPelvis') + spaz.node.upper_arm_mesh = bs.getmesh('melUpperArm') + spaz.node.forearm_mesh = bs.getmesh('melForeArm') + spaz.node.upper_leg_mesh = bs.getmesh('melUpperLeg') + spaz.node.lower_leg_mesh = bs.getmesh('melLowerLeg') + spaz.node.toes_mesh = bs.getmesh('melToes') + spaz.node.style = 'mel' + # Sounds cerdo gordo + mel_sounds = [bs.getsound('mel01'), bs.getsound('mel02'), bs.getsound('mel03'), bs.getsound('mel04'), bs.getsound('mel05'), + bs.getsound('mel06'), bs.getsound('mel07'), bs.getsound('mel08'), bs.getsound('mel09'), bs.getsound('mel10')] + spaz.node.jump_sounds = mel_sounds + spaz.node.attack_sounds = mel_sounds + spaz.node.impact_sounds = mel_sounds + spaz.node.pickup_sounds = mel_sounds + spaz.node.death_sounds = [bs.getsound('melDeath01')] + spaz.node.fall_sounds = [bs.getsound('melFall01')] + + def _set_meteor_timer(self) -> None: + bs.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: + bs.newnode('locator', attrs={'position': (8, 6, -5.5)}) + bs.newnode('locator', attrs={'position': (8, 6, -2.3)}) + bs.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) + bs.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.5 + 2.1 * random.random()) + dropdir = (-1.0 if pos[0] > 0 else 1.0) + vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0) + bs.timer(delay, babase.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + def _drop_bomb(self, position: Sequence[float], + velocity: Sequence[float]) -> None: + Bomb(position=position, velocity=velocity, bomb_type='sticky').autoretain() + + def _decrement_meteor_time(self) -> None: + self._meteor_time = max(0.01, self._meteor_time * 0.9) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _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, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/dist/ba_root/mods/games/gravity_falls.py b/dist/ba_root/mods/games/gravity_falls.py new file mode 100644 index 0000000..e4c7e44 --- /dev/null +++ b/dist/ba_root/mods/games/gravity_falls.py @@ -0,0 +1,35 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# Made by MattZ45986 on GitHub +# Ported by: Freaku / @[Just] Freak#4999 + + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.game.elimination import EliminationGame + + +# ba_meta require api 8 +# ba_meta export bascenev1.GameActivity +class GFGame(EliminationGame): + name = 'Gravity Falls' + + def spawn_player(self, player): + actor = self.spawn_player_spaz(player, (0, 5, 0)) + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + bs.timer(1, babase.Call(self.raise_player, player)) + return actor + + def raise_player(self, player): + if player.is_alive(): + try: + player.actor.node.handlemessage( + "impulse", player.actor.node.position[0], player.actor.node.position[1]+.5, player.actor.node.position[2], 0, 5, 0, 3, 10, 0, 0, 0, 5, 0) + except: + pass + bs.timer(0.05, babase.Call(self.raise_player, player)) diff --git a/dist/ba_root/mods/games/hot_potato.py b/dist/ba_root/mods/games/hot_potato.py new file mode 100644 index 0000000..658f440 --- /dev/null +++ b/dist/ba_root/mods/games/hot_potato.py @@ -0,0 +1,1061 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +""" + + Hot Potato by TheMikirog#1984 + + A random player(s) gets Marked. + They will die if they don't pass the mark to other players. + After they die, another random player gets Marked. + Last player standing wins! + + Heavily commented for easy modding learning! + + No Rights Reserved + +""" + +# ba_meta require api 8 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +# Define only what we need and nothing more +from baenv import TARGET_BALLISTICA_BUILD as build_number +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spaz import SpazFactory +from bascenev1lib.actor.spaz import PickupMessage +from bascenev1lib.actor.spaz import BombDiedMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.bomb import Blast +from enum import Enum +import random + +if TYPE_CHECKING: + pass + +# Let's define stun times for falling. +# First element is stun for the first fall, second element is stun for the second fall and so on. +# If we fall more than the amount of elements on this list, we'll use the last entry. +FALL_PENALTIES = [1.5, + 2.5, + 3.5, + 5.0, + 6.0, + 7.0, + 8.0, + 9.0, + 10.0] + +RED_COLOR = (1.0, 0.2, 0.2) +YELLOW_COLOR = (1.0, 1.0, 0.2) + + +# The player in Hot Potato can be in one of these states: +class PlayerState(Enum): + # REGULAR - the state all players start in. + REGULAR = 0 + # MARKED - when a player is marked, they'll be eliminated when the timer hits zero. + # Marked players pass the mark to REGULAR or STUNNED players by harming or grabbing other players. + # MARKED players respawn instantly if they somehow get knocked off the map. + MARKED = 1 + # ELIMINATED - a player is eliminated if the timer runs out during the MARKED state or they leave the game. + # These players can't win and won't respawn. + ELIMINATED = 2 + # STUNNED - if a REGULAR player falls out of the map, they'll receive the STUNNED state. + # STUNNED players are incapable of all movement and actions. + # STUNNED players can still get MARKED, but can't be punched, grabbed or knocked around by REGULAR players. + # STUNNED players will go back to the REGULAR state after several seconds. + # The time it takes to go back to the REGULAR state gets more severe the more times the player dies by falling off the map. + STUNNED = 3 + +# To make the game easier to parse, I added Elimination style icons to the bottom of the screen. +# Here's the behavior of each icon. + + +class Icon(bs.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: tuple[float, float], + scale: float, + name_scale: float = 1.0, + name_maxwidth: float = 100.0, + shadow: float = 1.0): + super().__init__() + + # Define the player this icon belongs to + self._player = player + self._name_scale = name_scale + + self._outline_tex = bs.gettexture('characterIconMask') + + # Character portrait + icon = player.get_icon() + self.node = bs.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + # Player name + self._name_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + # Status text (such as Marked!, Stunned! and You're Out!) + self._marked_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': '', + 'color': (1, 0.1, 0.0), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + # Status icon overlaying the character portrait + self._marked_icon = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.charstr(babase.SpecialChar.HAL), + 'color': (1, 1, 1), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 430, + 'shadow': 0.0, + 'opacity': 0.0, + 'flatness': 1.0, + 'scale': 2.1, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_marked_icon(player.state) + self.set_position_and_scale(position, scale) + + # Change our icon's appearance depending on the player state. + def set_marked_icon(self, type: PlayerState) -> None: + pos = self.node.position + # Regular players get no icons or status text + if type is PlayerState.REGULAR: + self._marked_icon.text = '' + self._marked_text.text = '' + self._marked_icon.opacity = 0.0 + self._name_text.flatness = 1.0 + assert self.node + self.node.color = (1.0, 1.0, 1.0) + # Marked players get ALL of the attention - red portrait, red text and icon overlaying the portrait + elif type is PlayerState.MARKED: + self._marked_icon.text = babase.charstr(babase.SpecialChar.HAL) + self._marked_icon.position = (pos[0] - 1, pos[1] - 13) + self._marked_icon.opacity = 1.0 + self._marked_text.text = 'Marked!' + self._marked_text.color = (1.0, 0.0, 0.0) + self._name_text.flatness = 0.0 + assert self.node + self.node.color = (1.0, 0.2, 0.2) + # Stunned players are just as important - yellow portrait, yellow text and moon icon. + elif type is PlayerState.STUNNED: + self._marked_icon.text = babase.charstr(babase.SpecialChar.MOON) + self._marked_icon.position = (pos[0] - 2, pos[1] - 12) + self._marked_icon.opacity = 1.0 + self._marked_text.text = 'Stunned!' + self._marked_text.color = (1.0, 1.0, 0.0) + assert self.node + self.node.color = (0.75, 0.75, 0.0) + # Eliminated players get special treatment. + # We make the portrait semi-transparent, while adding some visual flair with an fading skull icon and text. + elif type is PlayerState.ELIMINATED: + self._marked_icon.text = babase.charstr(babase.SpecialChar.SKULL) + self._marked_icon.position = (pos[0] - 2, pos[1] - 12) + self._marked_text.text = 'You\'re Out!' + self._marked_text.color = (0.5, 0.5, 0.5) + + # Animate text and icon + animation_end_time = 1.5 if bool(self.activity.settings['Epic Mode']) else 3.0 + bs.animate(self._marked_icon, 'opacity', { + 0: 1.0, + animation_end_time: 0.0}) + bs.animate(self._marked_text, 'opacity', { + 0: 1.0, + animation_end_time: 0.0}) + + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + else: + # If we beef something up, let the game know we made a mess in the code by providing a non-existant state. + raise Exception("invalid PlayerState type") + + # Set where our icon is positioned on the screen and how big it is. + def set_position_and_scale(self, position: tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + self._marked_text.position = (position[0], position[1] - scale * 52.0) + self._marked_text.scale = 0.8 * scale + +# This gamemode heavily relies on edited player behavior. +# We need that amount of control, so we're gonna create our own class and use the original PlayerSpaz as our blueprint. + + +class PotatoPlayerSpaz(PlayerSpaz): + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) # unchanged Spaz __init__ code goes here + self.dropped_bombs = [] # we use this to track bombs thrown by the player + + # Define a marked light + self.marked_light = bs.newnode('light', + owner=self.node, + attrs={'position': self.node.position, + 'radius': 0.15, + 'intensity': 0.0, + 'height_attenuated': False, + 'color': (1.0, 0.0, 0.0)}) + + # Pulsing red light when the player is Marked + bs.animate(self.marked_light, 'radius', { + 0: 0.1, + 0.3: 0.15, + 0.6: 0.1}, + loop=True) + self.node.connectattr('position_center', self.marked_light, 'position') + + # Marked timer. It should be above our head, so we attach the text to the offset that's attached to the player. + self.marked_timer_offset = bs.newnode('math', owner=self.node, attrs={ + 'input1': (0, 1.2, 0), + 'operation': 'add'}) + self.node.connectattr('torso_position', self.marked_timer_offset, 'input2') + + self.marked_timer_text = bs.newnode('text', owner=self.node, attrs={ + 'text': '', + 'in_world': True, + 'shadow': 0.4, + 'color': (RED_COLOR[0], RED_COLOR[1], RED_COLOR[2], 0.0), + 'flatness': 0, + 'scale': 0.02, + 'h_align': 'center'}) + self.marked_timer_offset.connectattr('output', self.marked_timer_text, 'position') + + # Modified behavior when dropping bombs + def drop_bomb(self) -> stdbomb.Bomb | None: + # The original function returns the Bomb the player created. + # This is super helpful for us, since all we need is to mark the bombs red + # if they belong to the Marked player and nothing else. + bomb = super().drop_bomb() + # Let's make sure the player actually created a new bomb + if bomb: + # Add our bomb to the list of our tracked bombs + self.dropped_bombs.append(bomb) + # Bring a light + bomb.bomb_marked_light = bs.newnode('light', + owner=bomb.node, + attrs={'position': bomb.node.position, + 'radius': 0.04, + 'intensity': 0.0, + 'height_attenuated': False, + 'color': (1.0, 0.0, 0.0)}) + # Attach the light to the bomb + bomb.node.connectattr('position', bomb.bomb_marked_light, 'position') + # Let's adjust all lights for all bombs that we own. + self.set_bombs_marked() + # When the bomb physics node dies, call a function. + bomb.node.add_death_action( + bs.WeakCall(self.bomb_died, bomb)) + + # Here's the function that gets called when one of the player's bombs dies. + # We reference the player's dropped_bombs list and remove the bomb that died. + + def bomb_died(self, bomb): + self.dropped_bombs.remove(bomb) + + # Go through all the bombs this player has in the world. + # Paint them red if the owner is marked, turn off the light otherwise. + # We need this light to inform the player about bombs YOU DON'T want to get hit by. + def set_bombs_marked(self): + for bomb in self.dropped_bombs: + bomb.bomb_marked_light.intensity = 20.0 if self._player.state == PlayerState.MARKED else 0.0 + + # Since our gamemode relies heavily on players passing the mark to other players + # we need to have access to this message. This gets called when the player takes damage for any reason. + def handlemessage(self, msg): + if isinstance(msg, bs.HitMessage): + # This is basically the same HitMessage code as in the original Spaz. + # The only difference is that there is no health bar and you can't die with punches or bombs. + # Also some useless or redundant code was removed. + # I'm still gonna comment all of it since we're here. + if not self.node: + return None + + # If the attacker is marked, pass that mark to us. + self.activity.pass_mark(msg._source_player, self._player) + + # When stun timer runs out, we explode. Let's make sure our own explosion does throw us around. + if msg.hit_type == 'stun_blast' and msg._source_player == self.source_player: + return True + # If the attacker is healthy and we're stunned, do a flash and play a sound, then ignore the rest of the code. + if self.source_player.state == PlayerState.STUNNED and msg._source_player != PlayerState.MARKED: + self.node.handlemessage('flash') + SpazFactory.get().block_sound.play(1.0, position=self.node.position) + return True + + # Here's all the damage and force calculations unchanged from the source. + mag = msg.magnitude * self.impact_scale + velocity_mag = msg.velocity_magnitude * self.impact_scale + damage_scale = 0.22 + + # We use them to apply a physical force to the player. + # Normally this is also used for damage, but we we're not gonna do it. + # We're still gonna calculate it, because it's still responsible for knockback. + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], mag, + velocity_mag, msg.radius, 0, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + damage = int(damage_scale * self.node.damage) + self.node.handlemessage('hurt_sound') # That's how we play spaz node's hurt sound + + # Play punch impact sounds based on damage if it was a punch. + # We don't show damage percentages, because it's irrelevant. + if msg.hit_type == 'punch': + self.on_punched(damage) + + if damage >= 500: + sounds = SpazFactory.get().punch_sound_strong + sound = sounds[random.randrange(len(sounds))] + elif damage >= 100: + sound = SpazFactory.get().punch_sound + else: + sound = SpazFactory.get().punch_sound_weak + sound.play(1.0, position=self.node.position) + + # Throw up some chunks. + assert msg.force_direction is not None + bs.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 0.5, + msg.force_direction[1] * 0.5, + msg.force_direction[2] * 0.5), + count=min(10, 1 + int(damage * 0.0025)), + scale=0.3, + spread=0.03) + + bs.emitfx(position=msg.pos, + chunk_type='sweat', + velocity=(msg.force_direction[0] * 1.3, + msg.force_direction[1] * 1.3 + 5.0, + msg.force_direction[2] * 1.3), + count=min(30, 1 + int(damage * 0.04)), + scale=0.9, + spread=0.28) + + # Momentary flash. This spawns around where the Spaz's punch would be (we're kind of guessing here). + hurtiness = damage * 0.003 + punchpos = (msg.pos[0] + msg.force_direction[0] * 0.02, + msg.pos[1] + msg.force_direction[1] * 0.02, + msg.pos[2] + msg.force_direction[2] * 0.02) + flash_color = (1.0, 0.8, 0.4) + light = bs.newnode( + 'light', + attrs={ + 'position': punchpos, + 'radius': 0.12 + hurtiness * 0.12, + 'intensity': 0.3 * (1.0 + 1.0 * hurtiness), + 'height_attenuated': False, + 'color': flash_color + }) + bs.timer(0.06, light.delete) + + flash = bs.newnode('flash', + attrs={ + 'position': punchpos, + 'size': 0.17 + 0.17 * hurtiness, + 'color': flash_color + }) + bs.timer(0.06, flash.delete) + + # Physics collision particles. + if msg.hit_type == 'impact': + assert msg.force_direction is not None + bs.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 2.0, + msg.force_direction[1] * 2.0, + msg.force_direction[2] * 2.0), + count=min(10, 1 + int(damage * 0.01)), + scale=0.4, + spread=0.1) + + # Briefly flash when hit. + # We shouldn't do this if we're dead. + if self.hitpoints > 0: + + self.node.handlemessage('flash') + + # If we're holding something, drop it. + if damage > 0.0 and self.node.hold_node: + self.node.hold_node = None + # If we get grabbed, this function is called. + # We want to pass along the mark with grabs too. + elif isinstance(msg, PickupMessage): + # Make sure our body exists. + if not self.node: + return None + + # Let's get all collision data if we can. Otherwise cancel. + try: + collision = bs.getcollision() + opposingnode = collision.opposingnode + except bs.NotFoundError: + return True + + # Our grabber needs to be a Spaz + if opposingnode.getnodetype() == 'spaz': + # Disallow grabbing if a healthy player tries to grab us and we're stunned. + # If they're marked, continue with our scheduled program. + # It's the same sound and flashing behavior as hitting a stunned player as a healthy player. + if (opposingnode.source_player.state == PlayerState.STUNNED and self.source_player.state != PlayerState.MARKED): + opposingnode.handlemessage('flash') + SpazFactory.get().block_sound.play(1.0, position=opposingnode.position) + return True + # If they're marked and we're healthy or stunned, pass that mark along to us. + elif opposingnode.source_player.state in [PlayerState.REGULAR, PlayerState.STUNNED] and self.source_player.state == PlayerState.MARKED: + self.activity.pass_mark(self.source_player, opposingnode.source_player) + + # Our work is done. Continue with the rest of the grabbing behavior as usual. + super().handlemessage(msg) + # Dying is important in this gamemode and as such we need to address this behavior. + elif isinstance(msg, bs.DieMessage): + + # If a player left the game, inform our gamemode logic. + if msg.how == bs.DeathType.LEFT_GAME: + self.activity.player_left(self.source_player) + + # If a MARKED or STUNNED player dies, hide the text from the previous spaz. + if self.source_player.state in [PlayerState.MARKED, PlayerState.STUNNED]: + self.marked_timer_text.color = (self.marked_timer_text.color[0], + self.marked_timer_text.color[1], + self.marked_timer_text.color[2], + 0.0) + bs.animate(self.marked_light, 'intensity', { + 0: self.marked_light.intensity, + 0.5: 0.0}) + + # Continue with the rest of the behavior. + super().handlemessage(msg) + # If a message is something we haven't modified yet, let's pass it along to the original. + else: + super().handlemessage(msg) + +# A concept of a player is very useful to reference if we don't have a player character present (maybe they died). + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + # Most of these are self explanatory. + self.icon: Icon = None + self.fall_times: int = 0 + self.state: PlayerState = PlayerState.REGULAR + self.stunned_time_remaining = None + # These are references to timers responsible for handling stunned behavior. + self.stunned_timer = None + self.stunned_update_timer = None + + # If we're stunned, a timer calls this every 0.1 seconds. + def stunned_timer_tick(self) -> None: + # Decrease our time remaining then change the text displayed above the Spaz's head + self.stunned_time_remaining -= 0.1 + self.stunned_time_remaining = max(0.0, self.stunned_time_remaining) + self.actor.marked_timer_text.text = str(round(self.stunned_time_remaining, 2)) + + # When stun time is up, call this function. + def stun_remove(self) -> None: + # Let's proceed only if we're stunned + if self.state != PlayerState.STUNNED: + return + # Do an explosion where we're standing. Normally it would throw us around, but we dealt + # with this issue in PlayerSpaz's edited HitMessage in line 312. + Blast(position=self.actor.node.position, + velocity=self.actor.node.velocity, + blast_radius=2.5, + # This hit type allows us to ignore our own stun blast explosions. + hit_type='stun_blast', + source_player=self).autoretain() + # Let's switch our state back to healthy. + self.set_state(PlayerState.REGULAR) + + # States are a key part of this gamemode and a lot of logic has to be done to acknowledge these state changes. + def set_state(self, state: PlayerState) -> None: + # Let's remember our old state before we change it. + old_state = self.state + + # If we just became stunned, do all of this: + if old_state != PlayerState.STUNNED and state == PlayerState.STUNNED: + self.actor.disconnect_controls_from_player() # Disallow all movement and actions + # Let's set our stun time based on the amount of times we fell out of the map. + if self.fall_times < len(FALL_PENALTIES): + stun_time = FALL_PENALTIES[self.fall_times] + else: + stun_time = FALL_PENALTIES[len(FALL_PENALTIES) - 1] + + self.stunned_time_remaining = stun_time # Set our stun time remaining + # Remove our stun once the time is up + self.stunned_timer = bs.Timer(stun_time + 0.1, babase.Call(self.stun_remove)) + self.stunned_update_timer = bs.Timer(0.1, babase.Call( + self.stunned_timer_tick), repeat=True) # Call a function every 0.1 seconds + self.fall_times += 1 # Increase the amount of times we fell by one + # Change the text above the Spaz's head to total stun time + self.actor.marked_timer_text.text = str(stun_time) + + # If we were stunned, but now we're not, let's reconnect our controls. + # CODING CHALLENGE: to punch or bomb immediately after the stun ends, you need to + # time the button press frame-perfectly in order for it to work. + # What if we could press the button shortly before stun ends to do the action as soon as possible? + # If you're feeling up to the challenge, feel free to implement that! + if old_state == PlayerState.STUNNED and state != PlayerState.STUNNED: + self.actor.connect_controls_to_player() + + # When setting a state that is not STUNNED, clear all timers. + if state != PlayerState.STUNNED: + self.stunned_timer = None + self.stunned_update_timer = None + + # Here's all the light and text colors that we set depending on the state. + if state == PlayerState.MARKED: + self.actor.marked_light.intensity = 1.5 + self.actor.marked_light.color = (1.0, 0.0, 0.0) + self.actor.marked_timer_text.color = (RED_COLOR[0], + RED_COLOR[1], + RED_COLOR[2], + 1.0) + elif state == PlayerState.STUNNED: + self.actor.marked_light.intensity = 0.5 + self.actor.marked_light.color = (1.0, 1.0, 0.0) + self.actor.marked_timer_text.color = (YELLOW_COLOR[0], + YELLOW_COLOR[1], + YELLOW_COLOR[2], + 1.0) + else: + self.actor.marked_light.intensity = 0.0 + self.actor.marked_timer_text.text = '' + + self.state = state + self.actor.set_bombs_marked() # Light our bombs red if we're Marked, removes the light otherwise + self.icon.set_marked_icon(state) # Update our icon + + +# ba_meta export bascenev1.GameActivity +class HotPotato(bs.TeamGameActivity[Player, bs.Team]): + + # Let's define the basics like the name of the game, description and some tips that should appear at the start of a match. + name = 'Hot Potato' + description = ('A random player gets marked.\n' + 'Pass the mark to other players.\n' + 'Marked player gets eliminated when time runs out.\n' + 'Last one standing wins!') + tips = [ + 'You can pass the mark not only with punches and grabs, but bombs as well.', + 'If you\'re not marked, DON\'T fall off the map!\nEach fall will be punished with immobility.', + 'Falling can be a good escape strategy, but don\'t over rely on it.\nYou\'ll be defenseless if you respawn!', + 'Stunned players are immune to healthy players, but not to Marked players!', + 'Each fall when not Marked increases your time spent stunned.', + 'Try throwing healthy players off the map to make their timers\nlonger the next time they get stunned.', + 'Marked players don\'t get stunned when falling off the map.', + 'For total disrespect, try throwing the Marked player off the map\nwithout getting marked yourself!', + 'Feeling evil? Throw healthy players towards the Marked player!', + 'Red bombs belong to the Marked player!\nWatch out for those!', + 'Stunned players explode when their stun timer runs out.\nIf that time is close to zero, keep your distance!' + ] + + # We're gonna distribute end of match session scores based on who dies first and who survives. + # First place gets most points, then second, then third. + scoreconfig = bs.ScoreConfig(label='Place', + scoretype=bs.ScoreType.POINTS, + lower_is_better=True) + + # These variables are self explanatory too. + show_kill_points = False + allow_mid_activity_joins = False + + # Let's define some settings the user can mess around with to fit their needs. + available_settings = [ + bs.IntSetting('Elimination Timer', + min_value=5, + default=15, + increment=1, + ), + bs.BoolSetting('Marked Players use Impact Bombs', default=False), + bs.BoolSetting('Epic Mode', default=False), + ] + + # Hot Potato is strictly a Free-For-All gamemode, so only picking the gamemode in FFA playlists. + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.FreeForAllSession) + + # Most maps should work in Hot Potato. Generally maps marked as 'melee' are the most versatile map types of them all. + # As the name implies, fisticuffs are common forms of engagement. + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('melee') + + # Here we define everything the gamemode needs, like sounds and settings. + def __init__(self, settings: dict): + super().__init__(settings) + self.settings = settings + + # Let's define all of the sounds we need. + self._tick_sound = bs.getsound('tick') + self._player_eliminated_sound = bs.getsound('playerDeath') + # These next sounds are arrays instead of single sounds. + # We'll use that fact later. + self._danger_tick_sounds = [bs.getsound('orchestraHit'), + bs.getsound('orchestraHit2'), + bs.getsound('orchestraHit3')] + self._marked_sounds = [bs.getsound('powerdown01'), + bs.getsound('activateBeep'), + bs.getsound('hiss')] + + # Normally play KOTH music, but switch to Epic music if we're in slow motion. + self._epic_mode = bool(settings['Epic Mode']) + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.SCARY) + + # This description appears below the title card after it comes crashing when the game begins. + def get_instance_description(self) -> str | Sequence: + return 'Pass the mark to someone else before you explode!' + + # This is the tiny text that is displayed in the corner during the game as a quick reminder of the objective. + def get_instance_description_short(self) -> str | Sequence: + return 'pass the mark' + + # Set up our player every time they join. + # Because you can't join mid-match, this will always be called at the beginning of the game. + def on_player_join(self, player: Player) -> None: + + player.state = PlayerState.REGULAR + player.fall_times = 0 + + # Create our icon and spawn. + if not self.has_begun(): + player.icon = Icon(player, position=(0, 50), scale=0.8) + self.spawn_player(player) + + # Returns every single marked player. + # This piece of info is used excensively in this gamemode, so it's advantageous to have a function to cut on + # work and make the gamemode easier to maintain + def get_marked_players(self) -> Sequence[bs.Player]: + marked_players = [] + for p in self.players: + if p.state == PlayerState.MARKED: + marked_players.append(p) + return marked_players + + # Marks a player. This sets their state, spawns some particles and sets the timer text above their heads. + def mark(self, target: Player) -> None: + target.set_state(PlayerState.MARKED) + + bs.emitfx(position=target.actor.node.position, + velocity=target.actor.node.velocity, + chunk_type='spark', + count=int(20.0+random.random()*20), + scale=1.0, + spread=1.0) + if bool(self.settings['Marked Players use Impact Bombs']): + target.actor.bomb_type = 'impact' + target.actor.marked_timer_text.text = str(self.elimination_timer_display) + + # Removes the mark from the player. This restores the player to its initial state. + def remove_mark(self, target: Player) -> None: + if target.state != PlayerState.MARKED: + return + + target.actor.bomb_type = 'normal' + + target.set_state(PlayerState.REGULAR) + target.actor.marked_timer_text.text = '' + + # Pass the mark from one player to another. + # This is more desirable than calling mark and remove_mark functions constantly and gives us + # more control over the mark spreading mechanic. + def pass_mark(self, marked_player: Player, hit_player: Player) -> None: + # Make sure both players meet the requirements + if not marked_player or not hit_player: + return + if marked_player.state == PlayerState.MARKED and hit_player.state != PlayerState.MARKED: + self.mark(hit_player) + self.remove_mark(marked_player) + + # This function is called every second a marked player exists. + def _eliminate_tick(self) -> None: + marked_players = self.get_marked_players() + marked_player_amount = len(marked_players) + + # If there is no marked players, raise an exception. + # This is used for debugging purposes, which lets us know we messed up somewhere else in the code. + if len(self.get_marked_players()) == 0: + raise Exception("no marked players!") + + self.elimination_timer_display -= 1 # Decrease our timer by one second. + if self.elimination_timer_display > 1: + self.elimination_timer_display -= 1 # Decrease our timer by one second. + sound_volume = 1.0 / marked_player_amount + + for target in marked_players: + self._tick_sound.play(sound_volume, target.actor.node.position) + target.actor.marked_timer_text.text = str(self.elimination_timer_display) + + # When counting down 3, 2, 1 play some dramatic sounds + if self.elimination_timer_display <= 3: + # We store our dramatic sounds in an array, so we target a specific element on the array + # depending on time remaining. Arrays start at index 0, so we need to decrease + # our variable by 1 to get the element index. + self._danger_tick_sounds[self.elimination_timer_display - 1].play(1.5) + else: + # Elimination timer is up! Let's eliminate all marked players. + self.elimination_timer_display -= 1 # Decrease our timer by one second. + self._eliminate_marked_players() + + # This function explodes all marked players + def _eliminate_marked_players(self) -> None: + self.marked_tick_timer = None + for target in self.get_marked_players(): + target.set_state(PlayerState.ELIMINATED) + target.actor.marked_timer_text.text = '' + + Blast(position=target.actor.node.position, + velocity=target.actor.node.velocity, + blast_radius=3.0, + source_player=target).autoretain() + bs.emitfx(position=target.actor.node.position, + velocity=target.actor.node.velocity, + count=int(16.0+random.random()*60), + scale=1.5, + spread=2, + chunk_type='spark') + target.actor.handlemessage(bs.DieMessage(how='marked_elimination')) + target.actor.shatter(extreme=True) + + self.match_placement.append(target.team) + + self._player_eliminated_sound.play(1.0) + + # Let the gamemode know a Marked + self.marked_players_died() + + # This function should be called when a Marked player dies, like when timer runs out or they leave the game. + def marked_players_died(self) -> bool: + alive_players = self.get_alive_players() + # Is there only one player remaining? Or none at all? Let's end the gamemode + if len(alive_players) < 2: + if len(alive_players) == 1: + # Let's add our lone survivor to the match placement list. + self.match_placement.append(alive_players[0].team) + # Wait a while to let this sink in before we announce our victor. + self._end_game_timer = bs.Timer(1.25, babase.Call(self.end_game)) + else: + # There's still players remaining, so let's wait a while before marking a new player. + self.new_mark_timer = bs.Timer( + 2.0 if self.slow_motion else 4.0, babase.Call(self.new_mark)) + + # Another extensively used function that returns all alive players. + def get_alive_players(self) -> Sequence[bs.Player]: + alive_players = [] + for player in self.players: + if player.state == PlayerState.ELIMINATED: + continue # Ignore players who have been eliminated + if player.is_alive(): + alive_players.append(player) + return alive_players + + # This function is called every time we want to start a new "round" by marking a random player. + def new_mark(self) -> None: + + # Don't mark a new player if we've already announced a victor. + if self.has_ended(): + return + + possible_targets = self.get_alive_players() + all_victims = [] + # Let's mark TWO players at once if there's 6 or more players. Helps with the pacing. + multi_choice = len(possible_targets) > 5 + + if multi_choice: + # Pick our first victim at random. + first_victim = random.choice(possible_targets) + all_victims.append(first_victim) + possible_targets.remove(first_victim) + # Let's pick our second victim, but this time excluding the player we picked earlier. + all_victims.append(random.choice(possible_targets)) + else: + # Pick one victim at random. + all_victims = [random.choice(possible_targets)] + + # Set time until marked players explode + self.elimination_timer_display = self.settings['Elimination Timer'] + # Set a timer that calls _eliminate_tick every second + self.marked_tick_timer = bs.Timer(1.0, babase.Call(self._eliminate_tick), repeat=True) + # Mark all chosen victims and play a sound + for new_victim in all_victims: + # _marked_sounds is an array. + # To make a nice marked sound effect, I play multiple sounds at once + # All of them are contained in the array. + for sound in self._marked_sounds: + bs.Sound.play(sound, 1.0, new_victim.actor.node.position) + self.mark(new_victim) + + # This function is called when the gamemode first loads. + def on_begin(self) -> None: + super().on_begin() # Do standard gamemode on_begin behavior + + self.elimination_timer_display = 0 + self.match_placement = [] + + # End the game if there's only one player + if len(self.players) < 2: + self.match_placement.append(self.players[0].team) + self._round_end_timer = bs.Timer(0.5, self.end_game) + else: + # Pick random player(s) to get marked + self.new_mark_timer = bs.Timer( + 2.0 if self.slow_motion else 5.2, babase.Call(self.new_mark)) + + self._update_icons() # Create player state icons + + # This function creates and positions player state icons + def _update_icons(self): + count = len(self.teams) + x_offs = 100 + xval = x_offs * (count - 1) * -0.5 + # FUN FACT: In FFA games, every player belongs to a one-player team. + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + player.icon.set_position_and_scale((xval, 50), 0.8) + xval += x_offs + + # Hot Potato can be a bit much, so I opted to show gameplay tips at the start of the match. + # However because I put player state icons, the tips overlay the icons. + # I'm gonna modify this function to move the tip text above the icons. + def _show_tip(self) -> None: + + from bascenev1._gameutils import animate, GameTip + from babase._mgen.enums import SpecialChar + from babase._language import Lstr + + # If there's any tips left on the list, display one. + if self.tips: + tip = self.tips.pop(random.randrange(len(self.tips))) + tip_title = Lstr(value='${A}:', + subs=[('${A}', Lstr(resource='tipText'))]) + icon: babase.Texture | None = None + sound: babase.Sound | None = None + if isinstance(tip, GameTip): + icon = tip.icon + sound = tip.sound + tip = tip.text + assert isinstance(tip, str) + + # Do a few replacements. + tip_lstr = Lstr(translate=('tips', tip), + subs=[('${PICKUP}', + babase.charstr(SpecialChar.TOP_BUTTON))]) + base_position = (75, 50) + tip_scale = 0.8 + tip_title_scale = 1.2 + vrmode = babase.app.vr_mode if build_number < 21282 else babase.app.env.vr + + t_offs = -350.0 + height_offs = 100.0 + tnode = bs.newnode('text', + attrs={ + 'text': tip_lstr, + 'scale': tip_scale, + 'maxwidth': 900, + 'position': (base_position[0] + t_offs, + base_position[1] + height_offs), + 'h_align': 'left', + 'vr_depth': 300, + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': 1.0 if vrmode else 0.5, + 'v_align': 'center', + 'v_attach': 'bottom' + }) + t2pos = (base_position[0] + t_offs - (20 if icon is None else 82), + base_position[1] + 2 + height_offs) + t2node = bs.newnode('text', + owner=tnode, + attrs={ + 'text': tip_title, + 'scale': tip_title_scale, + 'position': t2pos, + 'h_align': 'right', + 'vr_depth': 300, + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': 1.0 if vrmode else 0.5, + 'maxwidth': 140, + 'v_align': 'center', + 'v_attach': 'bottom' + }) + if icon is not None: + ipos = (base_position[0] + t_offs - 40, base_position[1] + 1 + height_offs) + img = bs.newnode('image', + attrs={ + 'texture': icon, + 'position': ipos, + 'scale': (50, 50), + 'opacity': 1.0, + 'vr_depth': 315, + 'color': (1, 1, 1), + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) + bs.timer(5.0, img.delete) + if sound is not None: + sound.play() + + combine = bs.newnode('combine', + owner=tnode, + attrs={ + 'input0': 1.0, + 'input1': 0.8, + 'input2': 1.0, + 'size': 4 + }) + combine.connectattr('output', tnode, 'color') + combine.connectattr('output', t2node, 'color') + animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) + bs.timer(5.0, tnode.delete) + + # This function is called when a player leaves the game. + # This is only called when the player already joined with a character. + def player_left(self, player: Player) -> None: + # If the leaving player is marked, remove the mark + if player.state == PlayerState.MARKED: + self.remove_mark(player) + + # If the leaving player is stunned, remove all stun timers + elif player.state == PlayerState.STUNNED: + player.stunned_timer = None + player.stunned_update_timer = None + + if len(self.get_marked_players()) == len(self.get_alive_players()): + for i in self.get_marked_players(): + self.remove_mark(i) + + if len(self.get_marked_players()) == 0: + self.marked_tick_timer = None + self.marked_players_died() + + player.set_state(PlayerState.ELIMINATED) + + # This function is called every time a player spawns + def spawn_player(self, player: Player) -> bs.Actor: + position = self.map.get_ffa_start_position(self.players) + position = (position[0], + position[1] - 0.3, # Move the spawn a bit lower + position[2]) + + name = player.getname() + + light_color = babase.normalized_color(player.color) + display_color = babase.safecolor(player.color, target_intensity=0.75) + + # Here we actually crate the player character + spaz = PotatoPlayerSpaz(color=player.color, + highlight=player.highlight, + character=player.character, + player=player) + spaz.node.invincible = False # Immediately turn off invincibility + player.actor = spaz # Assign player character to the owner + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light + spaz.handlemessage(bs.StandMessage(position, random.uniform(0, 360))) + t = bs.time() + self._spawn_sound.play(1.0, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + bs.animate(light, 'intensity', {0: 0, + 0.25: 1, + 0.5: 0}) + bs.timer(0.5, light.delete) + + # Game reacts to various events + def handlemessage(self, msg: Any) -> Any: + # This is called if the player dies. + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) + player = msg.getplayer(Player) + + # If a player gets eliminated, don't respawn + if msg.how == 'marked_elimination': + return + + self.spawn_player(player) # Spawn a new player character + + # If a REGULAR player dies, they respawn STUNNED. + # If a STUNNED player dies, reapply all visual effects. + if player.state in [PlayerState.REGULAR, PlayerState.STUNNED]: + player.set_state(PlayerState.STUNNED) + + # If a MARKED player falls off the map, apply the MARKED effects on the new spaz that respawns. + if player.state == PlayerState.MARKED: + self.mark(player) + + # This is called when we want to end the game and announce a victor + def end_game(self) -> None: + # Proceed only if the game hasn't ended yet. + if self.has_ended(): + return + results = bs.GameResults() + # By this point our match placement list should be filled with all players. + # Players that died/left earliest should be the first entries. + # We're gonna use array indexes to decide match placements. + # Because of that, we're gonna flip the order of our array, so the last entries are first. + self.match_placement.reverse() + for team in self.teams: + # Use each player's index in the array for our scoring + # 0 is the first index, so we add 1 to the score. + results.set_team_score(team, self.match_placement.index(team) + 1) + self.end(results=results) # Standard game ending behavior diff --git a/dist/ba_root/mods/games/hyper_race.py b/dist/ba_root/mods/games/hyper_race.py new file mode 100644 index 0000000..c5f5aaa --- /dev/null +++ b/dist/ba_root/mods/games/hyper_race.py @@ -0,0 +1,1238 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1 import _map +from bascenev1lib.actor.bomb import Bomb, Blast, BombFactory +from bascenev1lib.actor.powerupbox import PowerupBox +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict, + Union) + from bascenev1lib.actor.onscreentimer import OnScreenTimer + + +class ThePadDefs: + points = {} + boxes = {} + points['race_mine1'] = (0, 5, 12) + points['race_point1'] = (0.2, 5, 2.86308) + (0.507, 4.673, 1.1) + points['race_point2'] = (6.9301, 5.04988, 2.82066) + (0.911, 4.577, 1.073) + points['race_point3'] = (6.98857, 4.5011, -8.88703) + (1.083, 4.673, 1.076) + points['race_point4'] = (-6.4441, 4.5011, -8.88703) + (1.083, 4.673, 1.076) + points['race_point5'] = (-6.31128, 4.5011, 2.82669) + (0.894, 4.673, 0.941) + boxes['area_of_interest_bounds'] = ( + 0.3544110667, 4.493562578, -2.518391331) + ( + 0.0, 0.0, 0.0) + (16.64754831, 8.06138989, 18.5029888) + points['ffa_spawn1'] = (-0, 5, 2.5) + points['flag1'] = (-7.026110145, 4.308759233, -6.302807727) + points['flag2'] = (7.632557137, 4.366002373, -6.287969342) + points['flagDefault'] = (0.4611826686, 4.382076338, 3.680881802) + boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + ( + 0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344) + points['powerup_spawn1'] = (-4.166594349, 5.281834349, -6.427493781) + points['powerup_spawn2'] = (4.426873526, 5.342460464, -6.329745237) + points['powerup_spawn3'] = (-4.201686731, 5.123385835, 0.4400721376) + points['powerup_spawn4'] = (4.758924722, 5.123385835, 0.3494054559) + points['shadow_lower_bottom'] = (-0.2912522507, 2.020798381, 5.341226521) + points['shadow_lower_top'] = (-0.2912522507, 3.206066063, 5.341226521) + points['shadow_upper_bottom'] = (-0.2912522507, 6.062361813, 5.341226521) + points['shadow_upper_top'] = (-0.2912522507, 9.827201965, 5.341226521) + points['spawn1'] = (-0, 5, 2.5) + points['tnt1'] = (0.4599593402, 4.044276501, -6.573537395) + + +class ThePadMapb(bs.Map): + defs = ThePadDefs() + name = 'Racing' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['hyper'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'thePadPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'mesh': bs.getmesh('thePadLevel'), + 'bottom_mesh': bs.getmesh('thePadLevelBottom'), + 'collision_mesh': bs.getcollisionmesh('thePadLevelCollide'), + 'tex': bs.gettexture('thePadLevelColor'), + 'bgtex': bs.gettexture('black'), + 'bgmesh': bs.getmesh('thePadBG'), + 'railing_collision_mesh': bs.getcollisionmesh('thePadLevelBumper'), + 'vr_fill_mound_mesh': bs.getmesh('thePadVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound') + } + # fixme should chop this into vr/non-vr sections for efficiency + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['bottom_mesh'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + self.railing = bs.newnode( + 'terrain', + attrs={ + 'collision_mesh': self.preloaddata['railing_collision_mesh'], + 'materials': [shared.railing_material], + 'bumper': True + }) + bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['vr_fill_mound_mesh'], + 'lighting': False, + 'vr_only': True, + 'color': (0.56, 0.55, 0.47), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + gnode = bs.getactivity().globalsnode + gnode.tint = (1.1, 1.1, 1.0) + gnode.ambient_color = (1.1, 1.1, 1.0) + gnode.vignette_outer = (0.7, 0.65, 0.75) + gnode.vignette_inner = (0.95, 0.95, 0.93) + + +# ba_meta export plugin +class NewMap(babase.Plugin): + """My first ballistica plugin!""" + + def on_app_running(self) -> None: + _map.register_map(ThePadMapb) + + +class NewBlast(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: bs.Player = None, + hit_type: str = 'explosion', + hit_subtype: str = 'normal'): + bs.Actor.__init__(self) + + shared = SharedObjects.get() + factory = BombFactory.get() + + self.blast_type = blast_type + self._source_player = source_player + self.hit_type = hit_type + self.hit_subtype = hit_subtype + self.radius = blast_radius + + # Set our position a bit lower so we throw more things upward. + rmats = (factory.blast_material, shared.attack_material) + self.node = bs.newnode( + 'region', + delegate=self, + attrs={ + 'position': (position[0], position[1] - 0.1, position[2]), + 'scale': (self.radius, self.radius, self.radius), + 'type': 'sphere', + 'materials': rmats + }, + ) + + bs.timer(0.05, self.node.delete) + + # Throw in an explosion and flash. + evel = (velocity[0], max(-1.0, velocity[1]), velocity[2]) + explosion = bs.newnode('explosion', + attrs={ + 'position': position, + 'velocity': evel, + 'radius': self.radius, + 'big': (self.blast_type == 'tnt') + }) + if self.blast_type == 'ice': + explosion.color = (0, 0.05, 0.4) + + bs.timer(1.0, explosion.delete) + + if self.blast_type != 'ice': + bs.emitfx(position=position, + velocity=velocity, + count=int(1.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='thin_smoke') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='ice' if self.blast_type == 'ice' else 'smoke') + bs.emitfx(position=position, + emit_type='distortion', + spread=1.0 if self.blast_type == 'tnt' else 2.0) + + # And emit some shrapnel. + if self.blast_type == 'ice': + + def emit() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=30, + spread=2.0, + scale=0.4, + chunk_type='ice', + emit_type='stickers') + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + elif self.blast_type == 'sticky': + + def emit() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + spread=0.7, + chunk_type='slime') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.5, + spread=0.7, + chunk_type='slime') + bs.emitfx(position=position, + velocity=velocity, + count=15, + scale=0.6, + chunk_type='slime', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=20, + scale=0.7, + chunk_type='spark', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=int(6.0 + random.random() * 12), + scale=0.8, + spread=1.5, + chunk_type='spark') + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + elif self.blast_type == 'impact': + + def emit() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.8, + chunk_type='metal') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.4, + chunk_type='metal') + bs.emitfx(position=position, + velocity=velocity, + count=20, + scale=0.7, + chunk_type='spark', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=int(8.0 + random.random() * 15), + scale=0.8, + spread=1.5, + chunk_type='spark') + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + else: # Regular or land mine bomb shrapnel. + + def emit() -> None: + if self.blast_type != 'tnt': + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + chunk_type='rock') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.5, + chunk_type='rock') + bs.emitfx(position=position, + velocity=velocity, + count=30, + scale=1.0 if self.blast_type == 'tnt' else 0.7, + chunk_type='spark', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=int(18.0 + random.random() * 20), + scale=1.0 if self.blast_type == 'tnt' else 0.8, + spread=1.5, + chunk_type='spark') + + # TNT throws splintery chunks. + if self.blast_type == 'tnt': + + def emit_splinters() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(20.0 + random.random() * 25), + scale=0.8, + spread=1.0, + chunk_type='splinter') + + bs.timer(0.01, emit_splinters) + + # Every now and then do a sparky one. + if self.blast_type == 'tnt' or random.random() < 0.1: + + def emit_extra_sparks() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(10.0 + random.random() * 20), + scale=0.8, + spread=1.5, + chunk_type='spark') + + bs.timer(0.02, emit_extra_sparks) + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + lcolor = ((0.6, 0.6, 1.0) if self.blast_type == 'ice' else + (1, 0.3, 0.1)) + light = bs.newnode('light', + attrs={ + 'position': position, + 'volume_intensity_scale': 10.0, + 'color': lcolor + }) + + scl = random.uniform(0.6, 0.9) + scorch_radius = light_radius = self.radius + if self.blast_type == 'tnt': + light_radius *= 1.4 + scorch_radius *= 1.15 + scl *= 3.0 + + iscale = 1.6 + bs.animate( + light, 'intensity', { + 0: 2.0 * iscale, + scl * 0.02: 0.1 * iscale, + scl * 0.025: 0.2 * iscale, + scl * 0.05: 17.0 * iscale, + scl * 0.06: 5.0 * iscale, + scl * 0.08: 4.0 * iscale, + scl * 0.2: 0.6 * iscale, + scl * 2.0: 0.00 * iscale, + scl * 3.0: 0.0 + }) + bs.animate( + light, 'radius', { + 0: light_radius * 0.2, + scl * 0.05: light_radius * 0.55, + scl * 0.1: light_radius * 0.3, + scl * 0.3: light_radius * 0.15, + scl * 1.0: light_radius * 0.05 + }) + bs.timer(scl * 3.0, light.delete) + + # Make a scorch that fades over time. + scorch = bs.newnode('scorch', + attrs={ + 'position': position, + 'size': scorch_radius * 0.5, + 'big': (self.blast_type == 'tnt') + }) + if self.blast_type == 'ice': + scorch.color = (1, 1, 1.5) + + bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) + bs.timer(13.0, scorch.delete) + + if self.blast_type == 'ice': + factory.hiss_sound.play(position=light.position) + + lpos = light.position + factory.random_explode_sound().play(position=lpos) + factory.debris_fall_sound.play(position=lpos) + + bs.camerashake(0.0) + + # TNT is more epic. + if self.blast_type == 'tnt': + factory.random_explode_sound().play(position=lpos) + + def _extra_boom() -> None: + factory.random_explode_sound().play(position=lpos) + + bs.timer(0.25, _extra_boom) + + def _extra_debris_sound() -> None: + factory.debris_fall_sound.play(position=lpos) + factory.wood_debris_fall_sound.play(position=lpos) + + bs.timer(0.4, _extra_debris_sound) + + +class NewBomb(Bomb): + + def explode(self) -> None: + """Blows up the bomb if it has not yet done so.""" + if self._exploded: + return + self._exploded = True + if self.node: + blast = NewBlast(position=self.node.position, + velocity=self.node.velocity, + blast_radius=self.blast_radius, + blast_type=self.bomb_type, + source_player=babase.existing(self._source_player), + hit_type=self.hit_type, + hit_subtype=self.hit_subtype).autoretain() + for callback in self._explode_callbacks: + callback(self, blast) + + # We blew up so we need to go away. + # NOTE TO SELF: do we actually need this delay? + bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + +class TNT(bs.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + tnt_scale: float = 1.0, + teleport: bool = True): + super().__init__() + self.position = position + self.teleport = teleport + + self._no_collide_material = bs.Material() + self._no_collide_material.add_actions( + actions=('modify_part_collision', 'collide', False), + ) + self._collide_material = bs.Material() + self._collide_material.add_actions( + actions=('modify_part_collision', 'collide', True), + ) + + if teleport: + collide = self._collide_material + else: + collide = self._no_collide_material + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'mesh': bs.getmesh('tnt'), + 'color_texture': bs.gettexture('tnt'), + 'body': 'crate', + 'mesh_scale': tnt_scale, + 'body_scale': tnt_scale, + 'density': 2.0, + 'gravity_scale': 2.0, + 'materials': [collide] + } + ) + if not teleport: + bs.timer(0.1, self._collide) + + def _collide(self) -> None: + self.node.materials += (self._collide_material,) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.OutOfBoundsMessage): + if self.teleport: + self.node.position = self.position + self.node.velocity = (0, 0, 0) + else: + self.node.delete() + else: + super().handlemessage(msg) + + +class RaceRegion(bs.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 = bs.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] + }) + + +# MINIGAME +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.distance_txt: Optional[bs.Node] = None + self.last_region = 0 + self.lap = 0 + self.distance = 0.0 + self.finished = False + self.rank: Optional[int] = None + + +class Team(bs.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 bascenev1.GameActivity +class RaceGame(bs.TeamGameActivity[Player, Team]): + """Game of racing around a track.""" + + name = 'Hyper Race' + description = 'Creado Por Cebolla!!' + scoreconfig = bs.ScoreConfig(label='Time', + lower_is_better=True, + scoretype=bs.ScoreType.MILLISECONDS) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting('Laps', min_value=1, default=3, increment=1), + bs.IntChoiceSetting( + 'Time Limit', + default=0, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + ), + bs.BoolSetting('Epic Mode', default=False), + ] + + # We have some specific settings in teams mode. + if issubclass(sessiontype, bs.DualTeamSession): + settings.append( + bs.BoolSetting('Entire Team Must Finish', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('hyper') + + def __init__(self, settings: dict): + self._race_started = False + super().__init__(settings) + self.factory = factory = BombFactory.get() + self.shared = shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') + self._last_team_time: Optional[float] = None + self._front_race_region: Optional[int] = None + self._nub_tex = bs.gettexture('nub') + self._beep_1_sound = bs.getsound('raceBeep1') + self._beep_2_sound = bs.getsound('raceBeep2') + self.race_region_material: Optional[bs.Material] = None + self._regions: List[RaceRegion] = [] + self._team_finish_pts: Optional[int] = None + self._time_text: Optional[bs.Actor] = None + self._timer: Optional[OnScreenTimer] = None + self._scoreboard_timer: Optional[bs.Timer] = None + self._player_order_update_timer: Optional[bs.Timer] = None + self._start_lights: Optional[List[bs.Node]] = 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._epic_mode = bool(settings['Epic Mode']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC_RACE + if self._epic_mode else bs.MusicType.RACE) + + self._safe_region_material = bs.Material() + self._safe_region_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True)) + ) + + def get_instance_description(self) -> Union[str, Sequence]: + if (isinstance(self.session, bs.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 = bs.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))) + + bs.newnode( + 'region', + attrs={ + 'position': (0.3, 4.044276501, -2.9), + 'scale': (11.7, 15, 9.5), + 'type': 'box', + 'materials': [self._safe_region_material] + } + ) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = bs.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + bs.timer(0.5, light.delete) + bs.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 = bs.getcollision() + try: + region = collision.sourcenode.getdelegate(RaceRegion, True) + spaz = collision.opposingnode.getdelegate(PlayerSpaz, True) + except bs.NotFoundError: + return + + if not spaz.is_alive(): + return + + try: + player = spaz.getplayer(Player, True) + except bs.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(bs.DieMessage()) + bs.broadcastmessage(babase.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, bs.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, bs.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( + bs.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: + self._score_sound.play() + player.team.finished = True + assert self._timer is not None + elapsed = bs.time() - self._timer.getstarttime() + self._last_team_time = player.team.time = elapsed + self._check_end_game() + + # Team has yet to finish. + else: + self._swipsound.play() + + # They've just finished a lap but not the race. + else: + self._swipsound.play() + self._flash_player(player, 0.3) + + # Print their lap number over their head. + try: + assert isinstance(player.actor, PlayerSpaz) + mathnode = bs.newnode('math', + owner=player.actor.node, + attrs={ + 'input1': (0, 1.9, 0), + 'operation': 'add' + }) + player.actor.node.connectattr( + 'torso_position', mathnode, 'input2') + tstr = babase.Lstr(resource='lapNumberText', + subs=[('${CURRENT}', + str(player.lap + 1)), + ('${TOTAL}', str(self._laps)) + ]) + txtnode = bs.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') + bs.animate(txtnode, 'scale', { + 0.0: 0, + 0.2: 0.019, + 2.0: 0.019, + 2.2: 0 + }) + bs.timer(2.3, mathnode.delete) + except Exception: + babase.print_exception('Error printing lap.') + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + 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, bs.DualTeamSession) + and self._entire_team_must_finish): + bs.broadcastmessage(babase.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 + bs.getsound('boo').play() + for otherplayer in player.team.players: + otherplayer.lap = 0 + otherplayer.finished = True + try: + if otherplayer.actor is not None: + otherplayer.actor.handlemessage(bs.DieMessage()) + except Exception: + babase.print_exception('Error sending DieMessage.') + + # Defer so team/player lists will be updated. + babase.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, bs.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 bascenev1lib.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 = bs.NodeActor( + bs.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() + + self._scoreboard_timer = bs.Timer(0.25, + self._update_scoreboard, + repeat=True) + self._player_order_update_timer = bs.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 + + bs.timer(lstart, self._do_light_1) + bs.timer(lstart + inc, self._do_light_2) + bs.timer(lstart + 2 * inc, self._do_light_3) + bs.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = bs.newnode('image', + attrs={ + 'texture': bs.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + bs.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 + }) + bs.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._obstacles() + + pts = self.map.get_def_points('race_point') + for rpt in pts: + bs.newnode( + 'locator', + attrs={ + 'shape': 'circle', + 'position': (rpt[0], 4.382076338, rpt[2]), + 'size': (rpt[3] * 2.0, 0, rpt[5] * 2.0), + 'color': (0, 1, 0), + 'opacity': 1.0, + 'draw_beauty': False, + 'additive': True + } + ) + + def _obstacles(self) -> None: + 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._tnt((1.5, 5, 2.3), (0, 0, 0), 1.0) + self._tnt((1.5, 5, 3.3), (0, 0, 0), 1.0) + + self._tnt((3.5, 5, 2.3), (0, 0, 0), 1.0) + self._tnt((3.5, 5, 3.3), (0, 0, 0), 1.0) + + self._tnt((5.5, 5, 2.3), (0, 0, 0), 1.0) + self._tnt((5.5, 5, 3.3), (0, 0, 0), 1.0) + + self._tnt((-6, 5, -7), (0, 0, 0), 1.3) + self._tnt((-7, 5, -5), (0, 0, 0), 1.3) + self._tnt((-6, 5, -3), (0, 0, 0), 1.3) + self._tnt((-7, 5, -1), (0, 0, 0), 1.3) + self._tnt((-6, 5, 1), (0, 0, 0), 1.3) + + bs.timer(0.1, bs.WeakCall(self._tnt, (-3.2, 5, 1), + (0, 0, 0), 1.0, (0, 20, 60)), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (-5, 5, 0), (0, 0, 0), 1.0, 1.0, (0, 20, 3)), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (-1.5, 5, 0), (0, 0, 0), 1.0, 1.0, (0, 20, 3)), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-1, 5, -8), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-1, 5, -9), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-1, 5, -10), (0, 10, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-4.6, 5, -8), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-4.6, 5, -9), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-4.6, 5, -10), (0, 10, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall( + self._powerup, (2, 5, -5), 'curse', (0, 20, -3)), repeat=True) + bs.timer(1.6, bs.WeakCall( + self._powerup, (4, 5, -5), 'curse', (0, 20, -3)), repeat=True) + + def _tnt(self, + position: float, + velocity: float, + tnt_scale: float, + extra_acceleration: float = None) -> None: + if extra_acceleration: + TNT(position, velocity, tnt_scale, False).autoretain( + ).node.extra_acceleration = extra_acceleration + else: + TNT(position, velocity, tnt_scale).autoretain() + + def _bomb(self, + type: str, + position: float, + velocity: float, + mesh_scale: float, + body_scale: float, + extra_acceleration: float = None) -> None: + if extra_acceleration: + NewBomb(position=position, + velocity=velocity, + bomb_type=type).autoretain( + ).node.extra_acceleration = extra_acceleration + else: + NewBomb(position=position, + velocity=velocity, + bomb_type=type).autoretain() + + def _powerup(self, + position: float, + poweruptype: str, + extra_acceleration: float = None) -> None: + if extra_acceleration: + PowerupBox(position=position, + poweruptype=poweruptype).autoretain( + ).node.extra_acceleration = extra_acceleration + else: + PowerupBox(position=position, poweruptype=poweruptype).autoretain() + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + self._beep_1_sound.play() + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + self._beep_2_sound.play() + 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: + babase.print_exception('Error in race player connects.') + assert self._timer is not None + self._timer.start() + + self._race_started = True + + def _update_player_order(self) -> None: + + # Calc all player distances. + for player in self.players: + pos: Optional[babase.Vec3] + try: + pos = player.position + except bs.NotFoundError: + pos = None + if pos is not None: + r_index = player.last_region + rg1 = self._regions[r_index] + r1pt = babase.Vec3(rg1.pos[:3]) + rg2 = self._regions[0] if r_index == len( + self._regions) - 1 else self._regions[r_index + 1] + r2pt = babase.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_player(self, player: Player) -> bs.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 = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + + distance_txt = bs.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, bs.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, bs.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 = bs.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, + bs.DualTeamSession)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment default behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + if not player.finished: + self.respawn_player(player, respawn_time=1) + else: + super().handlemessage(msg) diff --git a/dist/ba_root/mods/games/infection.py b/dist/ba_root/mods/games/infection.py new file mode 100644 index 0000000..b46b1e2 --- /dev/null +++ b/dist/ba_root/mods/games/infection.py @@ -0,0 +1,526 @@ +"""New Duel / Created by: byANG3L""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import random +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + pass + +lang = bs.app.lang.language +if lang == 'Spanish': + name = 'Infección' + description = '¡Se está extendiendo!' + instance_description = '¡Evite la propagación!' + mines = 'Minas' + enable_bombs = 'Habilitar Bombas' + extra_mines = 'Seg/Mina Extra' + max_infected_size = 'Tamaño Máx. de Infección' + max_size_increases = 'Incrementar Tamaño Cada' + infection_spread_rate = 'Velocidad de Infección' + faster = 'Muy Rápido' + fast = 'Rápido' + normal = 'Normal' + slow = 'Lento' + slowest = 'Muy Lento' + insane = 'Insano' +else: + name = 'Infection' + description = "It's spreading!" + instance_description = 'Avoid the spread!' + mines = 'Mines' + enable_bombs = 'Enable Bombs' + extra_mines = 'Sec/Extra Mine' + max_infected_size = 'Max Infected Size' + max_size_increases = 'Max Size Increases Every' + infection_spread_rate = 'Infection Spread Rate' + faster = 'Faster' + fast = 'Fast' + normal = 'Normal' + slow = 'Slow' + slowest = 'Slowest' + insane = 'Insane' + + +def ba_get_api_version(): + return 8 + + +def ba_get_levels(): + return [bs._level.Level( + name, + gametype=Infection, + settings={}, + preview_texture_name='footballStadiumPreview')] + + +class myMine(Bomb): + # reason for the mine class is so we can add the death zone + def __init__(self, + pos: Sequence[float] = (0.0, 1.0, 0.0)): + Bomb.__init__(self, position=pos, bomb_type='land_mine') + showInSpace = False + self.died = False + self.rad = 0.3 + self.zone = bs.newnode( + 'locator', + attrs={ + 'shape': 'circle', + 'position': self.node.position, + 'color': (1, 0, 0), + 'opacity': 0.5, + 'draw_beauty': showInSpace, + 'additive': True}) + bs.animate_array( + self.zone, + 'size', + 1, + {0: [0.0], 0.05: [2*self.rad]}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if not self.died: + self.getactivity().mine_count -= 1 + self.died = True + bs.animate_array( + self.zone, + 'size', + 1, + {0: [2*self.rad], 0.05: [0]}) + self.zone = None + super().handlemessage(msg) + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.death_time: Optional[float] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class Infection(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = name + description = description + + # Print messages when players die since it matters here. + announce_player_deaths = True + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + mines, + min_value=5, + default=10, + increment=5, + ), + bs.BoolSetting(enable_bombs, default=True), + bs.IntSetting( + extra_mines, + min_value=1, + default=10, + increment=1, + ), + bs.IntSetting( + max_infected_size, + min_value=4, + default=6, + increment=1, + ), + bs.IntChoiceSetting( + max_size_increases, + choices=[ + ('10s', 10), + ('20s', 20), + ('30s', 30), + ('1 Minute', 60), + ], + default=20, + ), + bs.IntChoiceSetting( + infection_spread_rate, + choices=[ + (slowest, 0.01), + (slow, 0.02), + (normal, 0.03), + (fast, 0.04), + (faster, 0.05), + (insane, 0.08), + ], + default=0.03, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.CoopSession) + or issubclass(sessiontype, bs.MultiTeamSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Doom Shroom', 'Rampage', 'Hockey Stadium', + 'Crag Castle', 'Big G', 'Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + self.mines: List = [] + self._update_rate = 0.1 + self._last_player_death_time = None + self._start_time: Optional[float] = None + self._timer: Optional[OnScreenTimer] = None + self._epic_mode = bool(settings['Epic Mode']) + self._max_mines = int(settings[mines]) + self._extra_mines = int(settings[extra_mines]) + self._enable_bombs = bool(settings[enable_bombs]) + self._max_size = int(settings[max_infected_size]) + self._max_size_increases = float(settings[max_size_increases]) + self._growth_rate = float(settings[infection_spread_rate]) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.SURVIVAL) + + def get_instance_description(self) -> Union[str, Sequence]: + return instance_description + + def get_instance_description_short(self) -> Union[str, Sequence]: + return instance_description + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.mine_count = 0 + bs.timer(self._update_rate, + bs.WeakCall(self._mine_update), + repeat=True) + bs.timer(self._max_size_increases*1.0, + bs.WeakCall(self._max_size_update), + repeat=True) + bs.timer(self._extra_mines*1.0, + bs.WeakCall(self._max_mine_update), + repeat=True) + self._timer = OnScreenTimer() + self._timer.start() + + # Check for immediate end (if we've only got 1 player, etc). + bs.timer(5.0, self._check_end_game) + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + assert self._timer is not None + player.survival_seconds = self._timer.getstarttime() + bs.broadcastmessage( + babase.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) + self._check_end_game() + + def _max_mine_update(self) -> None: + self._max_mines += 1 + + def _max_size_update(self) -> None: + self._max_size += 1 + + def _mine_update(self) -> None: + # print self.mineCount + # purge dead mines, update their animantion, check if players died + for m in self.mines: + if not m.node: + self.mines.remove(m) + else: + # First, check if any player is within the current death zone + for player in self.players: + if not player.actor is None: + if player.actor.is_alive(): + p1 = player.actor.node.position + p2 = m.node.position + diff = (babase.Vec3(p1[0]-p2[0], + 0.0, + p1[2]-p2[2])) + dist = (diff.length()) + if dist < m.rad: + player.actor.handlemessage(bs.DieMessage()) + # Now tell the circle to grow to the new size + if m.rad < self._max_size: + bs.animate_array( + m.zone, 'size', 1, + {0: [m.rad*2], + self._update_rate: [(m.rad+self._growth_rate)*2]}) + # Tell the circle to be the new size. + # This will be the new check radius next time. + m.rad += self._growth_rate + # make a new mine if needed. + if self.mine_count < self._max_mines: + pos = self.getRandomPowerupPoint() + self.mine_count += 1 + self._flash_mine(pos) + bs.timer(0.95, babase.Call(self._make_mine, pos)) + + def _make_mine(self, posn: Sequence[float]) -> None: + m = myMine(pos=posn) + m.arm() + self.mines.append(m) + + def _flash_mine(self, pos: Sequence[float]) -> None: + light = bs.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 0.2, 0.2), + 'radius': 0.1, + 'height_attenuated': False + }) + bs.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) + bs.timer(1.0, light.delete) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, int(team.survival_seconds)) + self.end(results=results, announce_delay=0.8) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = bs.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + bs.timer(0.5, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + death_time = bs.time() + msg.getplayer(Player).death_time = death_time + + if isinstance(self.session, bs.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + babase.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = death_time + else: + bs.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, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def spawn_player(self, player: PlayerType) -> bs.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=self._enable_bombs, + enable_pickup=False) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + def spawn_player_spaz(self, + player: PlayerType, + position: Sequence[float] = (0, 0, 0), + angle: float = None) -> PlayerSpaz: + """Create and wire up a bs.PlayerSpaz for the provided bs.Player.""" + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + position = self.map.get_ffa_start_position(self.players) + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = babase._math.normalized_color(color) + display_color = _babase.safecolor(color, target_intensity=0.75) + spaz = PlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + # If this is co-op and we're on Courtyard or Runaround, add the + # material that allows us to collide with the player-walls. + # FIXME: Need to generalize this. + if isinstance(self.session, bs.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() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + return spaz + + def getRandomPowerupPoint(self) -> None: + # So far, randomized points only figured out for mostly rectangular maps. + # Boxes will still fall through holes, but shouldn't be terrible problem (hopefully) + # If you add stuff here, need to add to "supported maps" above. + # ['Doom Shroom', 'Rampage', 'Hockey Stadium', 'Courtyard', 'Crag Castle', 'Big G', 'Football Stadium'] + myMap = self.map.getname() + # print(myMap) + 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, 2.5, -3.5+5.0*y)) + elif myMap == 'Rampage': + x = random.uniform(-6.0, 7.0) + y = random.uniform(-6.0, -2.5) + return ((x, 5.2, y)) + elif myMap == 'Hockey Stadium': + x = random.uniform(-11.5, 11.5) + y = random.uniform(-4.5, 4.5) + return ((x, 0.2, y)) + elif myMap == 'Courtyard': + x = random.uniform(-4.3, 4.3) + y = random.uniform(-4.4, 0.3) + return ((x, 3.0, y)) + elif myMap == 'Crag Castle': + x = random.uniform(-6.7, 8.0) + y = random.uniform(-6.0, 0.0) + return ((x, 10.0, y)) + elif myMap == 'Big G': + x = random.uniform(-8.7, 8.0) + y = random.uniform(-7.5, 6.5) + return ((x, 3.5, y)) + elif myMap == 'Football Stadium': + x = random.uniform(-12.5, 12.5) + y = random.uniform(-5.0, 5.5) + return ((x, 0.32, y)) + else: + x = random.uniform(-5.0, 5.0) + y = random.uniform(-6.0, 0.0) + return ((x, 8.0, y)) + + def end_game(self) -> None: + cur_time = bs.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 = bs.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) diff --git a/dist/ba_root/mods/games/invisible_one.py b/dist/ba_root/mods/games/invisible_one.py new file mode 100644 index 0000000..ad5676c --- /dev/null +++ b/dist/ba_root/mods/games/invisible_one.py @@ -0,0 +1,333 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# +# By itsre3 +# =>3<= +# Don't mind my spelling. i realized that they were not correct after making last change and saving +# Besides that, enjoy.......!! +"""Provides the chosen-one mini-game.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.flag import Flag +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Optional, Sequence, Union + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.chosen_light: Optional[bs.NodeActor] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self, time_remaining: int) -> None: + self.time_remaining = time_remaining + + +# ba_meta export bascenev1.GameActivity +class InvicibleOneGame(bs.TeamGameActivity[Player, Team]): + """ + Game involving trying to remain the one 'invisible one' + for a set length of time while everyone else tries to + kill you and become the invisible one themselves. + """ + + name = 'Invisible One' + description = ('Be the invisible one for a length of time to win.\n' + 'Kill the invisible one to become it.') + available_settings = [ + bs.IntSetting( + 'Invicible One Time', + min_value=10, + default=30, + increment=10, + ), + bs.BoolSetting('Invicible one is lazy', default=True), + bs.BoolSetting('Night mode', default=False), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + scoreconfig = bs.ScoreConfig(label='Time Held') + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('keep_away') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._invicible_one_player: Optional[Player] = None + self._swipsound = bs.getsound('swip') + self._countdownsounds: Dict[int, babase.Sound] = { + 10: bs.getsound('announceTen'), + 9: bs.getsound('announceNine'), + 8: bs.getsound('announceEight'), + 7: bs.getsound('announceSeven'), + 6: bs.getsound('announceSix'), + 5: bs.getsound('announceFive'), + 4: bs.getsound('announceFour'), + 3: bs.getsound('announceThree'), + 2: bs.getsound('announceTwo'), + 1: bs.getsound('announceOne') + } + self._flag_spawn_pos: Optional[Sequence[float]] = None + self._reset_region_material: Optional[bs.Material] = None + self._flag: Optional[Flag] = None + self._reset_region: Optional[bs.Node] = None + self._epic_mode = bool(settings['Epic Mode']) + self._invicible_one_time = int(settings['Invicible One Time']) + self._time_limit = float(settings['Time Limit']) + self._invicible_one_is_lazy = bool(settings['Invicible one is lazy']) + self._night_mode = bool(settings['Night mode']) + + # Base class overrides + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.CHOSEN_ONE) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Show your invisibility powers.' + + def create_team(self, sessionteam: bs.SessionTeam) -> Team: + return Team(time_remaining=self._invicible_one_time) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + if self._get_invicible_one_player() is player: + self._set_invicible_one_player(None) + + def on_begin(self) -> None: + super().on_begin() + shared = SharedObjects.get() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._flag_spawn_pos = self.map.get_flag_position(None) + Flag.project_stand(self._flag_spawn_pos) + self._set_invicible_one_player(None) + if self._night_mode: + gnode = bs.getactivity().globalsnode + gnode.tint = (0.4, 0.4, 0.4) + + pos = self._flag_spawn_pos + bs.timer(1.0, call=self._tick, repeat=True) + + mat = self._reset_region_material = bs.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', + bs.WeakCall(self._handle_reset_collide)), + ), + ) + + self._reset_region = bs.newnode('region', + attrs={ + 'position': (pos[0], pos[1] + 0.75, + pos[2]), + 'scale': (0.5, 0.5, 0.5), + 'type': 'sphere', + 'materials': [mat] + }) + + def _get_invicible_one_player(self) -> Optional[Player]: + # Should never return invalid references; return None in that case. + if self._invicible_one_player: + return self._invicible_one_player + return None + + def _handle_reset_collide(self) -> None: + # If we have a chosen one, ignore these. + if self._get_invicible_one_player() is not None: + return + + # Attempt to get a Player controlling a Spaz that we hit. + try: + player = bs.getcollision().opposingnode.getdelegate( + PlayerSpaz, True).getplayer(Player, True) + except bs.NotFoundError: + return + + if player.is_alive(): + self._set_invicible_one_player(player) + + def _flash_flag_spawn(self) -> None: + light = bs.newnode('light', + attrs={ + 'position': self._flag_spawn_pos, + 'color': (1, 1, 1), + 'radius': 0.3, + 'height_attenuated': False + }) + bs.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _tick(self) -> None: + + # Give the chosen one points. + player = self._get_invicible_one_player() + if player is not None: + + # This shouldn't happen, but just in case. + if not player.is_alive(): + babase.print_error('got dead player as chosen one in _tick') + self._set_invicible_one_player(None) + else: + scoring_team = player.team + assert self.stats + self.stats.player_scored(player, + 3, + screenmessage=False, + display=False) + + scoring_team.time_remaining = max( + 0, scoring_team.time_remaining - 1) + + self._update_scoreboard() + + # announce numbers we have sounds for + if scoring_team.time_remaining in self._countdownsounds: + self._countdownsounds[scoring_team.time_remaining].play() + # Winner! + if scoring_team.time_remaining <= 0: + self.end_game() + + else: + # (player is None) + # This shouldn't happen, but just in case. + # (Chosen-one player ceasing to exist should + # trigger on_player_leave which resets chosen-one) + if self._invicible_one_player is not None: + babase.print_error('got nonexistent player as chosen one in _tick') + self._set_invicible_one_player(None) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, + self._invicible_one_time - team.time_remaining) + self.end(results=results, announce_delay=0) + + def _set_invicible_one_player(self, player: Optional[Player]) -> None: + existing = self._get_invicible_one_player() + if existing: + existing.chosen_light = None + self._swipsound.play() + if not player: + assert self._flag_spawn_pos is not None + self._flag = Flag(color=(1, 0.9, 0.2), + position=self._flag_spawn_pos, + touchable=False) + self._invicible_one_player = None + + # Create a light to highlight the flag; + # this will go away when the flag dies. + bs.newnode('light', + owner=self._flag.node, + attrs={ + 'position': self._flag_spawn_pos, + 'intensity': 0.6, + 'height_attenuated': False, + 'volume_intensity_scale': 0.1, + 'radius': 0.1, + 'color': (1.2, 1.2, 0.4) + }) + + # Also an extra momentary flash. + self._flash_flag_spawn() + else: + if player.actor: + self._flag = None + self._invicible_one_player = player + + if self._invicible_one_is_lazy: + player.actor.connect_controls_to_player( + enable_punch=False, enable_pickup=False, enable_bomb=False) + if player.actor.node.torso_mesh != None: + player.actor.node.color_mask_texture = None + player.actor.node.color_texture = None + player.actor.node.head_mesh = None + player.actor.node.torso_mesh = None + player.actor.node.upper_arm_mesh = None + player.actor.node.forearm_mesh = None + player.actor.node.pelvis_mesh = None + player.actor.node.toes_mesh = None + player.actor.node.upper_leg_mesh = None + player.actor.node.lower_leg_mesh = None + player.actor.node.hand_mesh = None + player.actor.node.style = 'cyborg' + invi_sound = [] + player.actor.node.jump_sounds = invi_sound + player.actor.attack_sounds = invi_sound + player.actor.impact_sounds = invi_sound + player.actor.pickup_sounds = invi_sound + player.actor.death_sounds = invi_sound + player.actor.fall_sounds = invi_sound + + player.actor.node.name = '' + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + if player is self._get_invicible_one_player(): + killerplayer = msg.getkillerplayer(Player) + self._set_invicible_one_player(None if ( + killerplayer is None or killerplayer is player + or not killerplayer.is_alive()) else killerplayer) + self.respawn_player(player) + else: + super().handlemessage(msg) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, + team.time_remaining, + self._invicible_one_time, + countdown=True) diff --git a/dist/ba_root/mods/games/laser_tracer.py b/dist/ba_root/mods/games/laser_tracer.py new file mode 100644 index 0000000..e6e6067 --- /dev/null +++ b/dist/ba_root/mods/games/laser_tracer.py @@ -0,0 +1,689 @@ + + +# Released under the MIT License. See LICENSE for details. +# https://youtu.be/wTgwZKiykQw?si=Cr0ybDYAcKCUNFN4 +# https://discord.gg/ucyaesh +# https://bombsquad-community.web.app/home +# by: Mr.Smoothy + +"""Elimination mini-game.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, Union +import random + + +class Icon(bs.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: tuple[float, float], + scale: float, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0): + super().__init__() + + self._player = player + self._show_lives = show_lives + self._show_death = show_death + self._name_scale = name_scale + self._outline_tex = bs.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = bs.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + self._name_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': flatness, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + if self._show_lives: + self._lives_text = bs.newnode('text', + owner=self.node, + attrs={ + 'text': 'x0', + 'color': (1, 1, 0.5), + 'h_align': 'left', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_position_and_scale(position, scale) + + def set_position_and_scale(self, position: tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + if self._show_lives: + self._lives_text.position = (position[0] + scale * 10.0, + position[1] - scale * 43.0) + self._lives_text.scale = 1.0 * scale + + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + if self._player: + lives = self._player.lives + else: + lives = 0 + if self._show_lives: + if lives > 0: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + def handle_player_spawned(self) -> None: + """Our player spawned; hooray!""" + if not self.node: + return + self.node.opacity = 1.0 + self.update_for_lives() + + def handle_player_died(self) -> None: + """Well poo; our player died.""" + if not self.node: + return + if self._show_death: + bs.animate( + self.node, 'opacity', { + 0.00: 1.0, + 0.05: 0.0, + 0.10: 1.0, + 0.15: 0.0, + 0.20: 1.0, + 0.25: 0.0, + 0.30: 1.0, + 0.35: 0.0, + 0.40: 1.0, + 0.45: 0.0, + 0.50: 1.0, + 0.55: 0.2 + }) + lives = self._player.lives + if lives == 0: + bs.timer(0.6, self.update_for_lives) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.lives = 0 + self.icons: list[Icon] = [] + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.spawn_order: list[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class LasorTracerGame(bs.TeamGameActivity[Player, Team]): + """Game type where last player(s) left alive win.""" + + name = 'Laser Tracer' + description = 'Last remaining alive wins.' + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + none_is_winner=True) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Lives', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ["Courtyard"] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._start_time: Optional[float] = None + self._vs_text: Optional[bs.Actor] = None + self._round_end_timer: Optional[bs.Timer] = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = 1 + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False)) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + self.laser_material = bs.Material() + self.laser_material.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_part_collision', 'collide', True), + ('message', 'their_node', 'at_connect', bs.DieMessage())) + ) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Last team standing wins.' if isinstance( + self.session, bs.DualTeamSession) else 'Last one standing wins.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'last team standing wins' if isinstance( + self.session, bs.DualTeamSession) else 'last one standing wins' + + def on_player_join(self, player: Player) -> None: + player.lives = self._lives_per_player + + if self._solo_mode: + player.team.spawn_order.append(player) + self._update_solo_mode() + else: + # Create our icon and spawn. + # player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + # Don't waste time doing this until begin. + if self.has_begun(): + self._update_icons() + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + self.add_wall() + self.create_laser() + if self._solo_mode: + self._vs_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': babase.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + bs.timer(1.0, self._update, repeat=True) + + def _update_solo_mode(self) -> None: + # For both teams, find the first player on the spawn order list with + # lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + break + + def _update_icons(self) -> None: + return + # lets do nothing ;Eat 5 Star + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> Optional[babase.Vec3]: + del player # Unused. + + # In solo-mode, if there's an existing live player on the map, spawn at + # whichever spot is farthest from them (keeps the action spread out). + if self._solo_mode: + living_player = None + living_player_pos = None + for team in self.teams: + for tplayer in team.players: + if tplayer.is_alive(): + assert tplayer.node + ppos = tplayer.node.position + living_player = tplayer + living_player_pos = ppos + break + if living_player: + assert living_player_pos is not None + player_pos = babase.Vec3(living_player_pos) + points: list[tuple[float, babase.Vec3]] = [] + for team in self.teams: + start_pos = babase.Vec3( + self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos)) + # Hmm.. we need to sorting vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def spawn_player(self, player: Player) -> bs.Actor: + actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) + actor.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + return actor + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + popuptext.PopupText('x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=player.node.position).autoretain() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + player.icons = [] + + # Remove us from spawn-order. + if self._solo_mode: + if player in player.team.spawn_order: + player.team.spawn_order.remove(player) + + # Update icons in a moment since our team will be gone from the + # list then. + bs.timer(0, self._update_icons) + + # If the player to leave was the last in spawn order and had + # their final turn currently in-progress, mark the survival time + # for their team. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - self._start_time) + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + babase.print_error( + "Got lives < 0 in Elim; this shouldn't happen. solo:" + + str(self._solo_mode)) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - + self._start_time) + else: + # Otherwise, in regular mode, respawn. + if not self._solo_mode: + self.respawn_player(player) + + # In solo, put ourself at the back of the spawn order. + if self._solo_mode: + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) + + def _update(self) -> None: + if self._solo_mode: + # For both teams, find the first player on the spawn order + # list with lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + self._update_icons() + break + + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0.5, self.end_game) + + def _get_living_teams(self) -> list[Team]: + return [ + team for team in self.teams + if len(team.players) > 0 and any(player.lives > 0 + for player in team.players) + ] + + def end_game(self) -> None: + if self.has_ended(): + return + results = bs.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.survival_seconds) + self.end(results=results) + + def add_wall(self): + # FIXME: Chop this into vr and non-vr chunks. + shared = SharedObjects.get() + pwm = bs.Material() + cwwm = bs.Material() + pwm.add_actions( + actions=('modify_part_collision', 'friction', 0.0)) + # anything that needs to hit the wall should apply this. + + pwm.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=('modify_part_collision', 'collide', True)) + cmesh = bs.getcollisionmesh('courtyardPlayerWall') + self.player_wall = bs.newnode( + 'terrain', + attrs={ + 'collision_mesh': cmesh, + 'affect_bg_dynamics': False, + 'materials': [pwm] + }) + + def create_laser(self) -> None: + bs.timer(6, babase.Call(self.LRlaser, True)) + + bs.timer(7, babase.Call(self.UDlaser, True)) + bs.timer(30, babase.Call(self.create_laser)) + + def LRlaser(self, left): + ud_1_r = bs.newnode('region', attrs={'position': (-5, 2.6, 0), 'scale': ( + 0.1, 0.6, 15), 'type': 'box', 'materials': [self.laser_material]}) + shields = [] + x = -6 + for i in range(0, 30): + x = x+0.4 + node = bs.newnode('shield', owner=ud_1_r, attrs={ + 'color': (1, 0, 0), 'radius': 0.28}) + mnode = bs.newnode('math', + owner=ud_1_r, + attrs={ + 'input1': (0, 0.0, x), + 'operation': 'add' + }) + ud_1_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', node, 'position') + + _rcombine = bs.newnode('combine', + owner=ud_1_r, + attrs={ + 'input1': 2.6, + 'input2': -2, + 'size': 3 + }) + if left: + x1 = -10 + x2 = 10 + else: + x1 = 10 + x2 = -10 + bs.animate(_rcombine, 'input0', { + 0: x1, + 20: x2 + }) + _rcombine.connectattr('output', ud_1_r, 'position') + bs.timer(20, babase.Call(ud_1_r.delete)) + t = random.randrange(7, 13) + bs.timer(t, babase.Call(self.LRlaser, random.randrange(0, 2))) + + def UDlaser(self, up): + ud_2_r = bs.newnode('region', attrs={'position': (-3, 2.6, -6), 'scale': ( + 20, 0.6, 0.1), 'type': 'box', 'materials': [self.laser_material]}) + shields = [] + x = -6 + for i in range(0, 40): + x = x+0.4 + node = bs.newnode('shield', owner=ud_2_r, attrs={ + 'color': (1, 0, 0), 'radius': 0.28}) + mnode = bs.newnode('math', + owner=ud_2_r, + attrs={ + 'input1': (x, 0.0, 0), + 'operation': 'add' + }) + ud_2_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', node, 'position') + + _rcombine = bs.newnode('combine', + owner=ud_2_r, + attrs={ + 'input0': -2, + 'input1': 2.6, + 'size': 3 + }) + if up: + x1 = -9 + x2 = 6 + else: + x1 = 6 + x2 = -9 + bs.animate(_rcombine, 'input2', { + 0: x1, + 17: x2 + }) + _rcombine.connectattr('output', ud_2_r, 'position') + + bs.timer(17, babase.Call(ud_2_r.delete)) + t = random.randrange(6, 13) + bs.timer(t, babase.Call(self.UDlaser, random.randrange(0, 2))) diff --git a/dist/ba_root/mods/games/last_punch_stand.py b/dist/ba_root/mods/games/last_punch_stand.py index bfd81e5..75d3a40 100644 --- a/dist/ba_root/mods/games/last_punch_stand.py +++ b/dist/ba_root/mods/games/last_punch_stand.py @@ -1,416 +1,275 @@ -# ba_meta require api 8 - -from typing import Sequence -import random -import bascenev1, babase, baclassic, baplus, bauiv1 -from bascenev1lib.actor.spaz import Spaz -from bascenev1lib.actor.scoreboard import Scoreboard -from bascenev1lib.gameutils import SharedObjects -from bascenev1lib.actor.powerupbox import PowerupBoxFactory - -class Player(bascenev1.Player['Team']): - """Our player type for this game.""" - -class Team(bascenev1.Team[Player]): - """Our team type for this game.""" - - def __init__(self) -> None: - super().__init__() - self.score = 1 - -class ChoosingThingHitMessage: - def __init__(self, hitter:Player) -> None: - self.hitter = hitter - -class ChoosingThingDieMessage: - def __init__(self, how:bascenev1.DeathType) -> None: - self.how = how - -class ChoosingThing(): - def __init__(self, pos, color) -> None: - pass - - def recolor(self, color:list[int | float], highlight:list[int, float] = (1,1,1)): - raise NotImplementedError() - - def is_dead(self): - raise NotImplementedError() - - def _is_dead(self): - return self.is_dead() - - def create_locator(self, node:bascenev1.Node, pos, color): - loc = bascenev1.newnode( - 'locator', - attrs={ - 'shape': 'circleOutline', - 'position': pos, - 'color': color, - 'opacity': 1, - 'draw_beauty': False, - 'additive': True, - }, - ) - node.connectattr("position", loc, "position") - bascenev1.animate_array(loc, "size", 1, keys={0:[0.5,], 1:[2,], 1.5:[0.5]}, loop=True) - - return loc - - dead = property(_is_dead) - -class ChoosingSpaz(Spaz, ChoosingThing): - def __init__( - self, - pos:Sequence[float], - color: Sequence[float] = (1.0, 1.0, 1.0), - highlight: Sequence[float] = (0.5, 0.5, 0.5), - ): - super().__init__(color, highlight, "Spaz", None, True, True, False, False) - self.stand(pos) - self.loc = self.create_locator(self.node, pos, color) - - def handlemessage(self, msg): - if isinstance(msg, bascenev1.FreezeMessage): - return - - if isinstance(msg, bascenev1.PowerupMessage): - if not(msg.poweruptype == "health"): - return - - super().handlemessage(msg) - - if isinstance(msg, bascenev1.HitMessage): - self.handlemessage(bascenev1.PowerupMessage("health")) - - player = msg.get_source_player(Player) - if self.is_alive(): - self.activity.handlemessage(ChoosingThingHitMessage(player)) - - elif isinstance(msg, bascenev1.DieMessage): - self._dead = True - self.activity.handlemessage(ChoosingThingDieMessage(msg.how)) - - self.loc.delete() - - def stand(self, pos = (0,0,0), angle = 0): - self.handlemessage(bascenev1.StandMessage(pos,angle)) - - def recolor(self, color, highlight = (1,1,1)): - self.node.color = color - self.node.highlight = highlight - self.loc.color = color - - def is_dead(self): - return self._dead - -class ChoosingBall(bascenev1.Actor, ChoosingThing): - def __init__(self, pos, color = (0.5,0.5,0.5)) -> None: - super().__init__() - shared = SharedObjects.get() - - pos = (pos[0], pos[1] + 2, pos[2]) - - # We want the puck to kill powerups; not get stopped by them - self.puck_material = bascenev1.Material() - self.puck_material.add_actions( - conditions=('they_have_material', - PowerupBoxFactory.get().powerup_material), - actions=(('modify_part_collision', 'physical', False), - ('message', 'their_node', 'at_connect', bascenev1.DieMessage()))) - - #fontSmall0 jumpsuitColor menuBG operaSingerColor rgbStripes zoeColor - self.node = bascenev1.newnode('prop', - delegate=self, - attrs={ - 'mesh': bascenev1.getmesh('frostyPelvis'), - 'color_texture': - bascenev1.gettexture('gameCenterIcon'), - 'body': 'sphere', - 'reflection': 'soft', - 'reflection_scale': [0.2], - 'shadow_size': 0.5, - 'is_area_of_interest': True, - 'position': pos, - "materials": [shared.object_material, self.puck_material] - }) - #seince this ball allways jumps a specefic direction when it spawned, we just jump it randomly - self.node.handlemessage( - 'impulse', - random.uniform(-10, 10), - random.uniform(-10, 10), - random.uniform(-10, 10), - random.uniform(-10, 10), - random.uniform(-10, 10), - random.uniform(-10, 10), - random.uniform(-10, 10), - random.uniform(-10, 10), - 0, - random.uniform(-10, 10), - random.uniform(-10, 10), - random.uniform(-10, 10), - random.uniform(-10, 10), - ) - - self.loc = self.create_locator(self.node, pos, color) - - self._died = False - - def handlemessage(self, msg): - if isinstance(msg, bascenev1.HitMessage): - player = msg.get_source_player(Player) - self.activity.handlemessage(ChoosingThingHitMessage(player)) - - mag = msg.magnitude - velocity_mag = msg.velocity_magnitude - - self.node.handlemessage( - 'impulse', - msg.pos[0], - msg.pos[1], - msg.pos[2], - msg.velocity[0], - msg.velocity[1], - msg.velocity[2], - mag, - velocity_mag, - msg.radius, - 1, - msg.force_direction[0], - msg.force_direction[1], - msg.force_direction[2], - ) - - elif isinstance(msg, bascenev1.DieMessage): - if self.node.exists(): - self.node.delete() - self.loc.delete() - - self._died = True - self.activity.handlemessage(ChoosingThingDieMessage(msg.how)) - - return super().handlemessage(msg) - - def exists(self) -> bool: - return not self.dead - - def is_alive(self) -> bool: - return not self.dead - - def recolor(self, color: list[int | float], highlight: list[int] = (1, 1, 1)): - self.loc.color = color - - def is_dead(self): - return self._died - -class ChooseBilbord(bascenev1.Actor): - def __init__(self, player:Player, delay = 0.1) -> None: - super().__init__() - - icon = player.get_icon() - self.scale = 100 - - self.node = bascenev1.newnode( - 'image', - delegate=self, - attrs={ - "position":(60,-125), - 'texture': icon['texture'], - 'tint_texture': icon['tint_texture'], - 'tint_color': icon['tint_color'], - 'tint2_color': icon['tint2_color'], - 'opacity': 1.0, - 'absolute_scale': True, - 'attach': "topLeft" - }, - ) - - self.name_node = bascenev1.newnode( - 'text', - owner=self.node, - attrs={ - 'position': (60,-185), - 'text': bascenev1.Lstr(value=player.getname()), - 'color': bascenev1.safecolor(player.team.color), - 'h_align': 'center', - 'v_align': 'center', - 'vr_depth': 410, - 'flatness': 1.0, - 'h_attach': 'left', - 'v_attach': 'top', - 'maxwidth':self.scale - }, - ) - - bascenev1.animate_array(self.node, "scale", keys={0 + delay:[0,0], 0.05 + delay:[self.scale, self.scale]}, size=1) - bascenev1.animate(self.name_node, "scale", {0 + delay:0, 0.07 + delay:1}) - - def handlemessage(self, msg): - super().handlemessage(msg) - if isinstance(msg, bascenev1.DieMessage): - bascenev1.animate_array(self.node, "scale", keys={0:self.node.scale, 0.05:[0,0]}, size=1) - bascenev1.animate(self.name_node, "scale", {0:self.name_node.scale, 0.07:0}) - - def __delete(): - self.node.delete() - self.name_node.delete() - - bascenev1.timer(0.2, __delete) - -# ba_meta export bascenev1.GameActivity -class LastPunchStand(bascenev1.TeamGameActivity[Player, Team]): - name = "Last Punch Stand" - description = "Last one punchs the choosing thing wins" - tips = [ - 'keep punching the choosing thing to be last punched player at times up!', - 'you can not frezz the choosing spaz', - "evry time you punch the choosing thing, you will get one point", - ] - - default_music = bascenev1.MusicType.TO_THE_DEATH - - def get_instance_display_string(self) -> bascenev1.Lstr: - name = self.name - if self.settings_raw["Ball Mode"]: - name += " Ball Mode" - - return name - - def get_instance_description_short(self) -> str | Sequence: - if self.settings_raw["Ball Mode"]: - return "Punch the Ball" - else: - return "Punch the Spaz" - - available_settings = [ - bascenev1.BoolSetting("Ball Mode", False), - bascenev1.FloatSetting("min time limit (in seconds)", 50.0, min_value=30.0), - bascenev1.FloatSetting("max time limit (in seconds)", 160.0, 60), - ] - - def __init__(self, settings: dict): - super().__init__(settings) - self._min_timelimit = settings["min time limit (in seconds)"] - self._max_timelimit = settings["max time limit (in seconds)"] - self.ball_mod:bool = settings["Ball Mode"] - if (self._min_timelimit > self._max_timelimit): - self._max_timelimit = self._min_timelimit - - self._choosing_thing_defcolor = (0.5,0.5,0.5) - self.choosing_thing:ChoosingThing = None - self.choosed_player = None - self.times_uped = False - self.scoreboard = Scoreboard() - - @classmethod - def get_supported_maps(cls, sessiontype: type[bascenev1.Session]) -> list[str]: - assert bascenev1.app.classic is not None - return bascenev1.app.classic.getmaps('team_flag') - - def times_up(self): - self.times_uped = True - self.end_game() - - for player in self.players: - try: - if self.choosed_player and player and (player.team.id != self.choosed_player.team.id): - player.actor._cursed = True - player.actor.curse_explode() - except AttributeError: - pass - - def __get_thing_spawn_point(self): - if len(self.map.flag_points_default) > 0: - return self.map.get_flag_position(None) - elif len(self.map.tnt_points) > 0: - return self.map.tnt_points[random.randint(0, len(self.map.tnt_points)-1)] - else: - return (0, 6, 0) - - def spaw_bot(self): - "spawns a choosing bot" - if self.ball_mod: - self.choosing_thing = ChoosingBall(self.__get_thing_spawn_point()) - else: - self.choosing_thing = ChoosingSpaz(self.__get_thing_spawn_point()) - self.choose_bilbord = None - - def on_begin(self) -> None: - super().on_begin() - time_limit = random.randint(self._min_timelimit, self._max_timelimit) - self.spaw_bot() - bascenev1.timer(time_limit, self.times_up) - - self.setup_standard_powerup_drops(False) - - def end_game(self) -> None: - results = bascenev1.GameResults() - total = 0 - - for team in self.teams: - total = team.score - - for team in self.teams: - if self.choosed_player and (team.id == self.choosed_player.team.id): team.score += total - results.set_team_score(team, team.score) - - self.end(results=results) - - def change_choosed_player(self, hitter:Player): - if hitter == self.choosed_player: - return - - if hitter: - self.choosing_thing.recolor(hitter.color, hitter.highlight) - self.choosed_player = hitter - hitter.team.score += 1 - self.choose_bilbord = ChooseBilbord(hitter) - self.hide_score_board() - else: - self.choosing_thing.recolor(self._choosing_thing_defcolor) - self.choosed_player = None - self.choose_bilbord = None - self.show_score_board() - - def show_score_board(self): - self.scoreboard = Scoreboard() - for team in self.teams: - self.scoreboard.set_team_value(team, team.score) - - def hide_score_board(self): - self.scoreboard = None - - def _watch_dog_(self): - "checks if choosing spaz exists" - #choosing spaz wont respawn if death type if generic - #this becuse we dont want to keep respawn him when he dies because of losing referce - #but sometimes "choosing spaz" dies naturaly and his death type is generic! so it wont respawn back again - #thats why we have this function; to check if spaz exits in the case that he didnt respawned - - if self.choosing_thing: - if self.choosing_thing.dead: - self.spaw_bot() - else: - self.spaw_bot() - - def handlemessage(self, msg): - super().handlemessage(msg) - - if isinstance(msg, ChoosingThingHitMessage): - hitter = msg.hitter - if hitter: - self.change_choosed_player(hitter) - - elif isinstance(msg, ChoosingThingDieMessage): - if msg.how.value != bascenev1.DeathType.GENERIC.value: - self.spaw_bot() - self.change_choosed_player(None) - - elif isinstance(msg, bascenev1.PlayerDiedMessage): - player = msg.getplayer(Player) - if not (self.has_ended() or self.times_uped): - self.respawn_player(player, 0) - - if self.choosed_player and (player.getname(True) == self.choosed_player.getname(True)): - self.change_choosed_player(None) - - self._watch_dog_() \ No newline at end of file +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +from typing import Sequence +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import random +from bascenev1lib.actor.spaz import Spaz +from bascenev1lib.actor.scoreboard import Scoreboard + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.score = 1 + + +class ChooseingSpazHitMessage: + def __init__(self, hitter: Player) -> None: + self.hitter = hitter + + +class ChooseingSpazDieMessage: + def __init__(self, killer: Player) -> None: + self.killer = killer + + +class ChooseingSpaz(Spaz): + def __init__( + self, + pos: Sequence[float], + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + ): + super().__init__(color, highlight, "Spaz", None, True, True, False, False) + self.last_player_attacked_by = None + self.stand(pos) + self.loc = bs.newnode( + 'locator', + attrs={ + 'shape': 'circleOutline', + 'position': pos, + 'color': color, + 'opacity': 1, + 'draw_beauty': False, + 'additive': True, + }, + ) + self.node.connectattr("position", self.loc, "position") + bs.animate_array(self.loc, "size", 1, keys={0: [0.5,], 1: [2,], 1.5: [0.5]}, loop=True) + + def handlemessage(self, msg): + if isinstance(msg, bs.FreezeMessage): + return + + if isinstance(msg, bs.PowerupMessage): + if not (msg.poweruptype == "health"): + return + + super().handlemessage(msg) + + if isinstance(msg, bs.HitMessage): + self.handlemessage(bs.PowerupMessage("health")) + + player = msg.get_source_player(Player) + if self.is_alive(): + self.activity.handlemessage(ChooseingSpazHitMessage(player)) + self.last_player_attacked_by = player + + elif isinstance(msg, bs.DieMessage): + player = self.last_player_attacked_by + + if msg.how.value != bs.DeathType.GENERIC.value: + self._dead = True + self.activity.handlemessage(ChooseingSpazDieMessage(player)) + + self.loc.delete() + + def stand(self, pos=(0, 0, 0), angle=0): + self.handlemessage(bs.StandMessage(pos, angle)) + + def recolor(self, color, highlight=(1, 1, 1)): + self.node.color = color + self.node.highlight = highlight + self.loc.color = color + + +class ChooseBilbord(bs.Actor): + def __init__(self, player: Player, delay=0.1) -> None: + super().__init__() + + icon = player.get_icon() + self.scale = 100 + + self.node = bs.newnode( + 'image', + delegate=self, + attrs={ + "position": (60, -125), + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'tint2_color': icon['tint2_color'], + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': "topLeft" + }, + ) + + self.name_node = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'position': (60, -185), + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'flatness': 1.0, + 'h_attach': 'left', + 'v_attach': 'top', + 'maxwidth': self.scale + }, + ) + + bs.animate_array(self.node, "scale", keys={ + 0 + delay: [0, 0], 0.05 + delay: [self.scale, self.scale]}, size=1) + bs.animate(self.name_node, "scale", {0 + delay: 0, 0.07 + delay: 1}) + + def handlemessage(self, msg): + super().handlemessage(msg) + if isinstance(msg, bs.DieMessage): + bs.animate_array(self.node, "scale", keys={0: self.node.scale, 0.05: [0, 0]}, size=1) + bs.animate(self.name_node, "scale", {0: self.name_node.scale, 0.07: 0}) + + def __delete(): + self.node.delete() + self.name_node.delete() + + bs.timer(0.2, __delete) + +# ba_meta export bascenev1.GameActivity + + +class LastPunchStand(bs.TeamGameActivity[Player, Team]): + name = "Last Punch Stand" + description = "Last one punchs the choosing spaz wins" + tips = [ + 'keep punching the choosing spaz to be last punched player at times up!', + 'you can not frezz the choosing spaz', + "evry time you punch the choosing spaz, you will get one point", + ] + + default_music = bs.MusicType.TO_THE_DEATH + + available_settings = [ + bs.FloatSetting("min time limit (in seconds)", 50.0, min_value=30.0), + bs.FloatSetting("max time limit (in seconds)", 160.0, 60), + + ] + + def __init__(self, settings: dict): + super().__init__(settings) + self._min_timelimit = settings["min time limit (in seconds)"] + self._max_timelimit = settings["max time limit (in seconds)"] + if (self._min_timelimit > self._max_timelimit): + self._max_timelimit = self._min_timelimit + + self._choosing_spaz_defcolor = (0.5, 0.5, 0.5) + self.choosing_spaz = None + self.choosed_player = None + self.times_uped = False + self.scoreboard = Scoreboard() + + def times_up(self): + self.times_uped = True + + for player in self.players: + if self.choosed_player and (player.team.id != self.choosed_player.team.id): + player.actor._cursed = True + player.actor.curse_explode() + + self.end_game() + + def __get_spaz_bot_spawn_point(self): + if len(self.map.tnt_points) > 0: + return self.map.tnt_points[random.randint(0, len(self.map.tnt_points)-1)] + else: + return (0, 6, 0) + + def spaw_bot(self): + "spawns a choosing bot" + + self.choosing_spaz = ChooseingSpaz(self.__get_spaz_bot_spawn_point()) + self.choose_bilbord = None + + def on_begin(self) -> None: + super().on_begin() + time_limit = random.randint(self._min_timelimit, self._max_timelimit) + self.spaw_bot() + bs.timer(time_limit, self.times_up) + + self.setup_standard_powerup_drops(False) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + if self.choosed_player and (team.id == self.choosed_player.team.id): + team.score += 100 + results.set_team_score(team, team.score) + self.end(results=results) + + def change_choosed_player(self, hitter: Player): + if hitter: + self.choosing_spaz.recolor(hitter.color, hitter.highlight) + self.choosed_player = hitter + hitter.team.score += 1 + self.choose_bilbord = ChooseBilbord(hitter) + self.hide_score_board() + else: + self.choosing_spaz.recolor(self._choosing_spaz_defcolor) + self.choosed_player = None + self.choose_bilbord = None + self.show_score_board() + + def show_score_board(self): + self.scoreboard = Scoreboard() + for team in self.teams: + self.scoreboard.set_team_value(team, team.score) + + def hide_score_board(self): + self.scoreboard = None + + def _watch_dog_(self): + "checks if choosing spaz exists" + # choosing spaz wont respawn if death type if generic + # this becuse we dont want to keep respawn him when he dies because of losing referce + # but sometimes "choosing spaz" dies naturaly and his death type is generic! so it wont respawn back again + # thats why we have this function; to check if spaz exits in the case that he didnt respawned + + if self.choosing_spaz: + if self.choosing_spaz._dead: + self.spaw_bot() + else: + self.spaw_bot() + + def handlemessage(self, msg): + super().handlemessage(msg) + + if isinstance(msg, ChooseingSpazHitMessage): + hitter = msg.hitter + if self.choosing_spaz.node and hitter: + self.change_choosed_player(hitter) + + elif isinstance(msg, ChooseingSpazDieMessage): + self.spaw_bot() + self.change_choosed_player(None) + + elif isinstance(msg, bs.PlayerDiedMessage): + player = msg.getplayer(Player) + if not (self.has_ended() or self.times_uped): + self.respawn_player(player, 0) + + if self.choosed_player and (player.getname(True) == self.choosed_player.getname(True)): + self.change_choosed_player(None) + + self._watch_dog_() diff --git a/dist/ba_root/mods/games/meteor_shower_deluxe.py b/dist/ba_root/mods/games/meteor_shower_deluxe.py new file mode 100644 index 0000000..296c8dc --- /dev/null +++ b/dist/ba_root/mods/games/meteor_shower_deluxe.py @@ -0,0 +1,67 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +""" +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. <[1](https://fsf.org/)> + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This license is designed to ensure cooperation with the community in the case of network server software. It is a free, copyleft license for software and other kinds of works. The license guarantees your freedom to share and change all versions of a program, to make sure it remains free software for all its users. + +The license identifier refers to the choice to use code under AGPL-3.0-or-later (i.e., AGPL-3.0 or some later version), as distinguished from use of code under AGPL-3.0-only. The license notice states which of these applies the code in the file. + + +""" +import random +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.game.meteorshower import MeteorShowerGame +from bascenev1lib.actor.bomb import Bomb + + +class NewMeteorShowerGame(MeteorShowerGame): + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps("melee") + + def _drop_bomb_cluster(self) -> None: + # Drop several bombs in series. + delay = 0.0 + bounds = list(self._map.get_def_bound_box("map_bounds")) + for _i in range(random.randrange(1, 3)): + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + pos = ( + random.uniform(bounds[0], bounds[3]), + bounds[4], + random.uniform(bounds[2], bounds[5]), + ) + dropdirx = -1 if pos[0] > 0 else 1 + dropdirz = -1 if pos[2] > 0 else 1 + forcex = ( + bounds[0] - bounds[3] + if bounds[0] - bounds[3] > 0 + else -(bounds[0] - bounds[3]) + ) + forcez = ( + bounds[2] - bounds[5] + if bounds[2] - bounds[5] > 0 + else -(bounds[2] - bounds[5]) + ) + vel = ( + (-5 + random.random() * forcex) * dropdirx, + random.uniform(-3.066, -4.12), + (-5 + random.random() * forcez) * dropdirz, + ) + bs.timer(delay, babase.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + +# ba_meta export plugin +class byEra0S(babase.Plugin): + MeteorShowerGame.get_supported_maps = NewMeteorShowerGame.get_supported_maps + MeteorShowerGame._drop_bomb_cluster = NewMeteorShowerGame._drop_bomb_cluster diff --git a/dist/ba_root/mods/games/shimla.py b/dist/ba_root/mods/games/shimla.py new file mode 100644 index 0000000..2316194 --- /dev/null +++ b/dist/ba_root/mods/games/shimla.py @@ -0,0 +1,411 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# +"""DeathMatch game and support classes.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.game.deathmatch import DeathMatchGame, Player +from bascenev1lib.gameutils import SharedObjects +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + +# ba_meta export bascenev1.GameActivity + + +class ShimlaGame(DeathMatchGame): + name = 'Shimla' + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Creative Thoughts'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self.lifts = {} + self._real_wall_material = bs.Material() + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + + self._real_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._lift_material = bs.Material() + self._lift_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._lift_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', self._handle_lift),), + ) + self._lift_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_disconnect', self._handle_lift_disconnect),), + ) + + def on_begin(self): + bs.getactivity().globalsnode.happy_thoughts_mode = False + super().on_begin() + + self.make_map() + bs.timer(2, self.disable_fly) + + def disable_fly(self): + activity = bs.get_foreground_host_activity() + + for players in activity.players: + players.actor.node.fly = False + + def spawn_player_spaz( + self, + player: Player, + position: Sequence[float] | None = None, + angle: float | None = None, + ) -> PlayerSpaz: + """Intercept new spazzes and add our team material for them.""" + spaz = super().spawn_player_spaz(player, position, angle) + + spaz.connect_controls_to_player(enable_punch=True, + enable_bomb=True, + enable_pickup=True, + enable_fly=False, + enable_jump=True) + spaz.fly = False + return spaz + + def make_map(self): + shared = SharedObjects.get() + bs.get_foreground_host_activity()._map.leftwall.materials = [ + shared.footing_material, self._real_wall_material] + + bs.get_foreground_host_activity()._map.rightwall.materials = [ + shared.footing_material, self._real_wall_material] + + bs.get_foreground_host_activity()._map.topwall.materials = [ + shared.footing_material, self._real_wall_material] + + self.floorwall1 = bs.newnode('region', attrs={'position': (-10, 5, -5.52), 'scale': + (15, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.floorwall2 = bs.newnode('region', attrs={'position': (10, 5, -5.52), 'scale': ( + 15, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + + self.wall1 = bs.newnode('region', attrs={'position': (0, 11, -6.90), 'scale': ( + 35.4, 20, 1), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.wall2 = bs.newnode('region', attrs={'position': (0, 11, -4.14), 'scale': ( + 35.4, 20, 1), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + + bs.newnode('locator', attrs={'shape': 'box', 'position': (-10, 5, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (15, 0.2, 2)}) + + bs.newnode('locator', attrs={'shape': 'box', 'position': (10, 5, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (15, 0.2, 2)}) + self.create_lift(-16.65, 8) + + self.create_lift(16.65, 8) + + self.create_static_step(0, 18.29) + self.create_static_step(0, 7) + + self.create_static_step(13, 17) + self.create_static_step(-13, 17) + self.create_slope(8, 15, True) + self.create_slope(-8, 15, False) + self.create_static_step(5, 15) + self.create_static_step(-5, 15) + + self.create_static_step(13, 12) + self.create_static_step(-13, 12) + self.create_slope(8, 10, True) + self.create_slope(-8, 10, False) + self.create_static_step(5, 10) + self.create_static_step(-5, 10) + + def create_static_step(self, x, y): + + shared = SharedObjects.get() + + bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': (5.5, 0.1, 6), + 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (x, y, -5.52), 'color': ( + 1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (5.5, 0.1, 2)}) + + def create_lift(self, x, y): + shared = SharedObjects.get() + color = (0.7, 0.6, 0.5) + + floor = bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': ( + 1.8, 0.1, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material, self._lift_material]}) + + cleaner = bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': ( + 2, 0.3, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + + lift = bs.newnode('locator', attrs={'shape': 'box', 'position': ( + x, y, -5.52), 'color': color, 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (1.8, 3.7, 2)}) + + _tcombine = bs.newnode('combine', + owner=floor, + attrs={ + 'input0': x, + 'input2': -5.5, + 'size': 3 + }) + mnode = bs.newnode('math', + owner=lift, + attrs={ + 'input1': (0, 2, 0), + 'operation': 'add' + }) + _tcombine.connectattr('output', mnode, 'input2') + + _cleaner_combine = bs.newnode('combine', + owner=cleaner, + attrs={ + 'input1': 5.6, + 'input2': -5.5, + 'size': 3 + }) + _cleaner_combine.connectattr('output', cleaner, 'position') + bs.animate(_tcombine, 'input1', { + 0: 5.1, + }) + bs.animate(_cleaner_combine, 'input0', { + 0: -19 if x < 0 else 19, + }) + + _tcombine.connectattr('output', floor, 'position') + mnode.connectattr('output', lift, 'position') + self.lifts[floor] = {"state": "origin", "lift": _tcombine, + "cleaner": _cleaner_combine, 'leftLift': x < 0} + + def _handle_lift(self): + region = bs.getcollision().sourcenode + lift = self.lifts[region] + + def clean(lift): + bs.animate(lift["cleaner"], 'input0', { + 0: -19 if lift["leftLift"] else 19, + 2: -16 if lift["leftLift"] else 16, + 4.3: -19 if lift["leftLift"] else 19 + }) + if lift["state"] == "origin": + lift["state"] = "transition" + bs.animate(lift["lift"], 'input1', { + 0: 5.1, + 1.3: 5.1, + 6: 5+12, + 9: 5+12, + 15: 5.1 + }) + bs.timer(16, babase.Call(lambda lift: lift.update({'state': 'end'}), lift)) + bs.timer(12, babase.Call(clean, lift)) + + def _handle_lift_disconnect(self): + region = bs.getcollision().sourcenode + lift = self.lifts[region] + if lift["state"] == 'end': + lift["state"] = "origin" + + def create_slope(self, x, y, backslash): + shared = SharedObjects.get() + + for i in range(0, 21): + bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': (0.2, 0.1, 6), + 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (x, y, -5.52), 'color': ( + 1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.2, 0.1, 2)}) + if backslash: + x = x+0.1 + y = y+0.1 + else: + x = x-0.1 + y = y+0.1 + + +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (-1.045859963, 12.67722855, + -5.401537075) + (0.0, 0.0, 0.0) + ( + 42.46156851, 20.94044653, 0.6931564611) + points['ffa_spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) + points['ffa_spawn2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) + points['ffa_spawn3'] = (9.55724115, 11.30789446, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn4'] = (-11.55747023, 10.99170684, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn5'] = (-1.878892369, 9.46490571, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn6'] = (-0.4912812943, 5.077006397, -5.521672101) + ( + 1.878332089, 1.453808816, 0.007578097856) + points['flag1'] = (-11.75152479, 8.057427485, -5.52) + points['flag2'] = (9.840909039, 8.188634282, -5.52) + points['flag3'] = (-0.2195258696, 5.010273907, -5.52) + points['flag4'] = (-0.04605809154, 12.73369108, -5.52) + points['flag_default'] = (-0.04201942896, 12.72374492, -5.52) + boxes['map_bounds'] = (-0.8748348681, 9.212941713, -5.729538885) + ( + 0.0, 0.0, 0.0) + (42.09666006, 26.19950145, 7.89541168) + points['powerup_spawn1'] = (1.160232442, 6.745963662, -5.469115985) + points['powerup_spawn2'] = (-1.899700206, 10.56447241, -5.505721177) + points['powerup_spawn3'] = (10.56098871, 12.25165669, -5.576232453) + points['powerup_spawn4'] = (-12.33530337, 12.25165669, -5.576232453) + points['spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) + points['spawn2'] = (7.484707127, 8.172681752, + -5.614479365) + (1.553861796, 1.453808816, 0.04419853907) + points['spawn_by_flag1'] = (-9.295167711, 8.010664315, -5.44451005) + ( + 1.555840357, 1.453808816, 0.1165648888) + points['spawn_by_flag2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) + points['spawn_by_flag3'] = (-1.45994593, 5.038762459, -5.535288724) + ( + 0.9516389866, 0.6666414677, 0.08607244075) + points['spawn_by_flag4'] = (0.4932087091, 12.74493212, -5.598987003) + ( + 0.5245740665, 0.5245740665, 0.01941146064) + + +class CreativeThoughts(bs.Map): + """Freaking map by smoothy.""" + + defs = mapdefs + + name = 'Creative Thoughts' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [ + 'melee', 'keep_away', 'team_flag' + ] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'alwaysLandPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'mesh': bs.getmesh('alwaysLandLevel'), + 'bottom_mesh': bs.getmesh('alwaysLandLevelBottom'), + 'bgmesh': bs.getmesh('alwaysLandBG'), + 'collision_mesh': bs.getcollisionmesh('alwaysLandLevelCollide'), + 'tex': bs.gettexture('alwaysLandLevelColor'), + 'bgtex': bs.gettexture('alwaysLandBGColor'), + 'vr_fill_mound_mesh': bs.getmesh('alwaysLandVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound') + } + return data + + @classmethod + def get_music_type(cls) -> bs.MusicType: + return bs.MusicType.FLYING + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -3.7, 2.5)) + shared = SharedObjects.get() + self._fake_wall_material = bs.Material() + self._real_wall_material = bs.Material() + self._fake_wall_material.add_actions( + conditions=(('they_are_younger_than', 9000), 'and', + ('they_have_material', shared.player_material)), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._real_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': bs.gettexture("rampageBGColor") + }) + + self.leftwall = bs.newnode('region', attrs={'position': (-17.75152479, 13, -5.52), 'scale': ( + 0.1, 15.5, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.rightwall = bs.newnode('region', attrs={'position': (17.75, 13, -5.52), 'scale': ( + 0.1, 15.5, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.topwall = bs.newnode('region', attrs={'position': (0, 21.0, -5.52), 'scale': ( + 35.4, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (-17.75152479, 13, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.1, 15.5, 2)}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (17.75, 13, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.1, 15.5, 2)}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (0, 21.0, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (35.4, 0.2, 2)}) + + gnode = bs.getactivity().globalsnode + gnode.happy_thoughts_mode = True + gnode.shadow_offset = (0.0, 8.0, 5.0) + gnode.tint = (1.3, 1.23, 1.0) + gnode.ambient_color = (1.3, 1.23, 1.0) + gnode.vignette_outer = (0.64, 0.59, 0.69) + gnode.vignette_inner = (0.95, 0.95, 0.93) + gnode.vr_near_clip = 1.0 + self.is_flying = True + + # throw out some tips on flying + txt = bs.newnode('text', + attrs={ + 'text': babase.Lstr(resource='pressJumpToFlyText'), + 'scale': 1.2, + 'maxwidth': 800, + 'position': (0, 200), + 'shadow': 0.5, + 'flatness': 0.5, + 'h_align': 'center', + 'v_attach': 'bottom' + }) + cmb = bs.newnode('combine', + owner=txt, + attrs={ + 'size': 4, + 'input0': 0.3, + 'input1': 0.9, + 'input2': 0.0 + }) + bs.animate(cmb, 'input3', {3.0: 0, 4.0: 1, 9.0: 1, 10.0: 0}) + cmb.connectattr('output', txt, 'color') + bs.timer(10.0, txt.delete) + + +try: + bs._map.register_map(CreativeThoughts) +except: + pass diff --git a/dist/ba_root/mods/games/snow_ball_fight.py b/dist/ba_root/mods/games/snow_ball_fight.py new file mode 100644 index 0000000..0bffe2d --- /dev/null +++ b/dist/ba_root/mods/games/snow_ball_fight.py @@ -0,0 +1,643 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor.bomb import Blast +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.spaz import PunchHitMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.spazfactory import SpazFactory + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = bs.app.lang.language + +if lang == 'Spanish': + name = 'Guerra de Nieve' + snowball_rate = 'Intervalo de Ataque' + snowball_slowest = 'Más Lento' + snowball_slow = 'Lento' + snowball_fast = 'Rápido' + snowball_lagcity = 'Más Rápido' + snowball_scale = 'Tamaño de Bola de Nieve' + snowball_smallest = 'Más Pequeño' + snowball_small = 'Pequeño' + snowball_big = 'Grande' + snowball_biggest = 'Más Grande' + snowball_insane = 'Insano' + snowball_melt = 'Derretir Bola de Nieve' + snowball_bust = 'Rebotar Bola de Nieve' + snowball_explode = 'Explotar al Impactar' + snowball_snow = 'Modo Nieve' +else: + name = 'Snowball Fight' + snowball_rate = 'Snowball Rate' + snowball_slowest = 'Slowest' + snowball_slow = 'Slow' + snowball_fast = 'Fast' + snowball_lagcity = 'Lag City' + snowball_scale = 'Snowball Scale' + snowball_smallest = 'Smallest' + snowball_small = 'Small' + snowball_big = 'Big' + snowball_biggest = 'Biggest' + snowball_insane = 'Insane' + snowball_melt = 'Snowballs Melt' + snowball_bust = 'Snowballs Bust' + snowball_explode = 'Snowballs Explode' + snowball_snow = 'Snow Mode' + + +class Snowball(bs.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + blast_radius: float = 0.7, + bomb_scale: float = 0.8, + source_player: bs.Player | None = None, + owner: bs.Node | None = None, + melt: bool = True, + bounce: bool = True, + explode: bool = False): + super().__init__() + shared = SharedObjects.get() + self._exploded = False + self.scale = bomb_scale + self.blast_radius = blast_radius + self._source_player = source_player + self.owner = owner + self._hit_nodes = set() + self.snowball_melt = melt + self.snowball_bounce = bounce + self.snowball_explode = explode + self.radius = bomb_scale * 0.1 + if bomb_scale <= 1.0: + shadow_size = 0.6 + elif bomb_scale <= 2.0: + shadow_size = 0.4 + elif bomb_scale <= 3.0: + shadow_size = 0.2 + else: + shadow_size = 0.1 + + self.snowball_material = bs.Material() + self.snowball_material.add_actions( + conditions=( + ( + ('we_are_younger_than', 5), + 'or', + ('they_are_younger_than', 100), + ), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + + self.snowball_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=('modify_part_collision', 'use_node_collide', False), + ) + + self.snowball_material.add_actions(actions=('modify_part_collision', + 'friction', 0.3)) + + self.snowball_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'physical', False), + ('call', 'at_connect', self.hit))) + + self.snowball_material.add_actions( + conditions=(('they_dont_have_material', shared.player_material), + 'and', + ('they_have_material', shared.object_material), + 'or', + ('they_have_material', shared.footing_material)), + actions=('call', 'at_connect', self.bounce)) + + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'body': 'sphere', + 'body_scale': self.scale, + 'mesh': bs.getmesh('frostyPelvis'), + 'shadow_size': shadow_size, + 'color_texture': bs.gettexture('bunnyColor'), + 'reflection': 'soft', + 'reflection_scale': [0.15], + 'density': 1.0, + 'materials': [self.snowball_material] + }) + self.light = bs.newnode( + 'light', + owner=self.node, + attrs={ + 'color': (0.6, 0.6, 1.0), + 'intensity': 0.8, + 'radius': self.radius + }) + self.node.connectattr('position', self.light, 'position') + bs.animate(self.node, 'mesh_scale', { + 0: 0, + 0.2: 1.3 * self.scale, + 0.26: self.scale + }) + bs.animate(self.light, 'radius', { + 0: 0, + 0.2: 1.3 * self.radius, + 0.26: self.radius + }) + if self.snowball_melt: + bs.timer(1.5, bs.WeakCall(self._disappear)) + + def hit(self) -> None: + if not self.node: + return + if self._exploded: + return + if self.snowball_explode: + self._exploded = True + self.do_explode() + bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage())) + else: + self.do_hit() + + def do_hit(self) -> None: + v = self.node.velocity + if babase.Vec3(*v).length() > 5.0: + node = bs.getcollision().opposingnode + if node is not None and node and not ( + node in self._hit_nodes): + t = self.node.position + hitdir = self.node.velocity + self._hit_nodes.add(node) + node.handlemessage( + bs.HitMessage( + pos=t, + velocity=v, + magnitude=babase.Vec3(*v).length()*0.5, + velocity_magnitude=babase.Vec3(*v).length()*0.5, + radius=0, + srcnode=self.node, + source_player=self._source_player, + force_direction=hitdir, + hit_type='snoBall', + hit_subtype='default')) + + if not self.snowball_bounce: + bs.timer(0.05, bs.WeakCall(self.do_bounce)) + + def do_explode(self) -> None: + Blast(position=self.node.position, + velocity=self.node.velocity, + blast_radius=self.blast_radius, + source_player=babase.existing(self._source_player), + blast_type='impact', + hit_subtype='explode').autoretain() + + def bounce(self) -> None: + if not self.node: + return + if self._exploded: + return + if not self.snowball_bounce: + vel = self.node.velocity + bs.timer(0.01, bs.WeakCall(self.calc_bounce, vel)) + else: + return + + def calc_bounce(self, vel) -> None: + if not self.node: + return + ospd = babase.Vec3(*vel).length() + dot = sum(x*y for x, y in zip(vel, self.node.velocity)) + if ospd*ospd - dot > 50.0: + bs.timer(0.05, bs.WeakCall(self.do_bounce)) + + def do_bounce(self) -> None: + if not self.node: + return + if not self._exploded: + self.do_effect() + + def do_effect(self) -> None: + self._exploded = True + bs.emitfx(position=self.node.position, + velocity=[v*0.1 for v in self.node.velocity], + count=10, + spread=0.1, + scale=0.4, + chunk_type='ice') + sound = bs.getsound('impactMedium') + sound.play(1.0, position=self.node.position) + scl = self.node.mesh_scale + bs.animate(self.node, 'mesh_scale', { + 0.0: scl*1.0, + 0.02: scl*0.5, + 0.05: 0.0 + }) + lr = self.light.radius + bs.animate(self.light, 'radius', { + 0.0: lr*1.0, + 0.02: lr*0.5, + 0.05: 0.0 + }) + bs.timer(0.08, + bs.WeakCall(self.handlemessage, bs.DieMessage())) + + def _disappear(self) -> None: + self._exploded = True + if self.node: + scl = self.node.mesh_scale + bs.animate(self.node, 'mesh_scale', { + 0.0: scl*1.0, + 0.3: scl*0.5, + 0.5: 0.0 + }) + lr = self.light.radius + bs.animate(self.light, 'radius', { + 0.0: lr*1.0, + 0.3: lr*0.5, + 0.5: 0.0 + }) + bs.timer(0.55, + bs.WeakCall(self.handlemessage, bs.DieMessage())) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + else: + super().handlemessage(msg) + + +class NewPlayerSpaz(PlayerSpaz): + + def __init__(self, *args: Any, **kwds: Any): + super().__init__(*args, **kwds) + self.snowball_scale = 1.0 + self.snowball_melt = True + self.snowball_bounce = True + self.snowball_explode = False + + def on_punch_press(self) -> None: + if not self.node or self.frozen or self.node.knockout > 0.0: + return + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + if t_ms - self.last_punch_time_ms >= self._punch_cooldown: + if self.punch_callback is not None: + self.punch_callback(self) + + # snowball + pos = self.node.position + p1 = self.node.position_center + p2 = self.node.position_forward + direction = [p1[0]-p2[0], p2[1]-p1[1], p1[2]-p2[2]] + direction[1] = 0.03 + mag = 20.0/babase.Vec3(*direction).length() + vel = [v * mag for v in direction] + Snowball(position=(pos[0], pos[1] + 0.1, pos[2]), + velocity=vel, + blast_radius=self.blast_radius, + bomb_scale=self.snowball_scale, + source_player=self.source_player, + owner=self.node, + melt=self.snowball_melt, + bounce=self.snowball_bounce, + explode=self.snowball_explode).autoretain() + + self._punched_nodes = set() # Reset this. + self.last_punch_time_ms = t_ms + self.node.punch_pressed = True + if not self.node.hold_node: + bs.timer( + 0.1, + bs.WeakCall(self._safe_play_sound, + SpazFactory.get().swish_sound, 0.8)) + self._turbo_filter_add_press('punch') + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, PunchHitMessage): + pass + else: + return super().handlemessage(msg) + return None + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class SnowballFightGame(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = name + description = 'Kill a set number of enemies to win.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.IntChoiceSetting( + snowball_rate, + choices=[ + (snowball_slowest, 500), + (snowball_slow, 400), + ('Normal', 300), + (snowball_fast, 200), + (snowball_lagcity, 100), + ], + default=300, + ), + bs.FloatChoiceSetting( + snowball_scale, + choices=[ + (snowball_smallest, 0.4), + (snowball_small, 0.6), + ('Normal', 0.8), + (snowball_big, 1.4), + (snowball_biggest, 3.0), + (snowball_insane, 6.0), + ], + default=0.8, + ), + bs.BoolSetting(snowball_melt, default=True), + bs.BoolSetting(snowball_bust, default=True), + bs.BoolSetting(snowball_explode, default=False), + bs.BoolSetting(snowball_snow, default=True), + bs.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: int | None = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + self._snowball_rate = int(settings[snowball_rate]) + self._snowball_scale = float(settings[snowball_scale]) + self._snowball_melt = bool(settings[snowball_melt]) + self._snowball_bounce = bool(settings[snowball_bust]) + self._snowball_explode = bool(settings[snowball_explode]) + self._snow_mode = bool(settings[snowball_snow]) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> str | Sequence: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> str | Sequence: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_transition_in(self) -> None: + super().on_transition_in() + if self._snow_mode: + gnode = bs.getactivity().globalsnode + gnode.tint = (0.8, 0.8, 1.3) + bs.timer(0.02, self.emit_snowball, repeat=True) + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + def emit_snowball(self) -> None: + pos = (-10 + (random.random() * 30), 15, + -10 + (random.random() * 30)) + vel = ((-5.0 + random.random() * 30.0) * (-1.0 if pos[0] > 0 else 1.0), + -50.0, (-5.0 + random.random() * 30.0) * ( + -1.0 if pos[0] > 0 else 1.0)) + bs.emitfx(position=pos, + velocity=vel, + count=10, + scale=1.0 + random.random(), + spread=0.0, + chunk_type='spark') + + def spawn_player_spaz(self, + player: Player, + position: Sequence[float] = (0, 0, 0), + angle: float | None = None) -> PlayerSpaz: + from babase import _math + from bascenev1._gameutils import animate + from bascenev1._coopsession import CoopSession + + if isinstance(self.session, bs.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) + + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = babase.safecolor(color, target_intensity=0.75) + + spaz = NewPlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + # If this is co-op and we're on Courtyard or Runaround, add the + # material that allows us to collide with the player-walls. + # FIXME: Need to generalize this. + 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_pickup=False, enable_bomb=False) + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + # custom + spaz._punch_cooldown = self._snowball_rate + spaz.snowball_scale = self._snowball_scale + spaz.snowball_melt = self._snowball_melt + spaz.snowball_bounce = self._snowball_bounce + spaz.snowball_explode = self._snowball_explode + + return spaz + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/dist/ba_root/mods/games/squid_race.py b/dist/ba_root/mods/games/squid_race.py new file mode 100644 index 0000000..d665e5b --- /dev/null +++ b/dist/ba_root/mods/games/squid_race.py @@ -0,0 +1,953 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# +"""Defines Race mini-game.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.bomb import Bomb, Blast, ExplodeHitMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict, + Union) + from bascenev1lib.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(bs.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 = bs.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(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.distance_txt: Optional[bs.Node] = None + self.last_region = 0 + self.lap = 0 + self.distance = 0.0 + self.finished = False + self.rank: Optional[int] = None + + +class Team(bs.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 bascenev1.GameActivity +class SquidRaceGame(bs.TeamGameActivity[Player, Team]): + """Game of racing around a track.""" + + name = 'Squid Race' + description = 'Run real fast!' + scoreconfig = bs.ScoreConfig(label='Time', + lower_is_better=True, + scoretype=bs.ScoreType.MILLISECONDS) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting('Laps', min_value=1, default=3, increment=1), + bs.IntChoiceSetting( + 'Time Limit', + default=0, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + ), + bs.IntChoiceSetting( + 'Mine Spawning', + default=4000, + choices=[ + ('No Mines', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ], + ), + bs.IntChoiceSetting( + 'Bomb Spawning', + choices=[ + ('None', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ('1 Second', 1000), + ], + default=2000, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + + # We have some specific settings in teams mode. + if issubclass(sessiontype, bs.DualTeamSession): + settings.append( + bs.BoolSetting('Entire Team Must Finish', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('race') + + def __init__(self, settings: dict): + self._race_started = False + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') + self._last_team_time: Optional[float] = None + self._front_race_region: Optional[int] = None + self._nub_tex = bs.gettexture('nub') + self._beep_1_sound = bs.getsound('raceBeep1') + self._beep_2_sound = bs.getsound('raceBeep2') + self.race_region_material: Optional[bs.Material] = None + self._regions: List[RaceRegion] = [] + self._team_finish_pts: Optional[int] = None + self._time_text: Optional[bs.Actor] = None + self._timer: Optional[OnScreenTimer] = None + self._race_mines: Optional[List[RaceMine]] = None + self._race_mine_timer: Optional[bs.Timer] = None + self._scoreboard_timer: Optional[bs.Timer] = None + self._player_order_update_timer: Optional[bs.Timer] = None + self._start_lights: Optional[List[bs.Node]] = None + self._squid_lights: Optional[List[bs.Node]] = None + self._countdown_timer: int = 0 + self._sq_mode: str = 'Easy' + self._tick_timer: Optional[bs.Timer] = None + self._bomb_spawn_timer: Optional[bs.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: bs.getsound('announceTen'), + 9: bs.getsound('announceNine'), + 8: bs.getsound('announceEight'), + 7: bs.getsound('announceSeven'), + 6: bs.getsound('announceSix'), + 5: bs.getsound('announceFive'), + 4: bs.getsound('announceFour'), + 3: bs.getsound('announceThree'), + 2: bs.getsound('announceTwo'), + 1: bs.getsound('announceOne') + } + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC_RACE + if self._epic_mode else bs.MusicType.RACE) + + def get_instance_description(self) -> Union[str, Sequence]: + if (isinstance(self.session, bs.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 = bs.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 = bs.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + bs.timer(0.5, light.delete) + bs.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 = bs.getcollision() + try: + region = collision.sourcenode.getdelegate(RaceRegion, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.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(bs.DieMessage()) + bs.broadcastmessage(babase.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, bs.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, bs.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( + bs.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: + self._score_sound.play() + player.team.finished = True + assert self._timer is not None + elapsed = bs.time() - self._timer.getstarttime() + self._last_team_time = player.team.time = elapsed + + # Team has yet to finish. + else: + self._swipsound.play() + + # They've just finished a lap but not the race. + else: + self._swipsound.play() + self._flash_player(player, 0.3) + + # Print their lap number over their head. + try: + assert isinstance(player.actor, PlayerSpaz) + mathnode = bs.newnode('math', + owner=player.actor.node, + attrs={ + 'input1': (0, 1.9, 0), + 'operation': 'add' + }) + player.actor.node.connectattr( + 'torso_position', mathnode, 'input2') + tstr = babase.Lstr(resource='lapNumberText', + subs=[('${CURRENT}', + str(player.lap + 1)), + ('${TOTAL}', str(self._laps)) + ]) + txtnode = bs.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') + bs.animate(txtnode, 'scale', { + 0.0: 0, + 0.2: 0.019, + 2.0: 0.019, + 2.2: 0 + }) + bs.timer(2.3, mathnode.delete) + except Exception: + babase.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(): + bs.broadcastmessage( + babase.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, bs.DualTeamSession) + and self._entire_team_must_finish): + bs.broadcastmessage(babase.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 + bs.getsound('boo').play() + for otherplayer in player.team.players: + otherplayer.lap = 0 + otherplayer.finished = True + try: + if otherplayer.actor is not None: + otherplayer.actor.handlemessage(bs.DieMessage()) + except Exception: + babase.print_exception('Error sending DieMessage.') + + # Defer so team/player lists will be updated. + babase.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, bs.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 bascenev1lib.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 = bs.NodeActor( + bs.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 = bs.Timer(0.001 * self._mine_spawning, + self._update_race_mine, + repeat=True) + + self._scoreboard_timer = bs.Timer(0.25, + self._update_scoreboard, + repeat=True) + self._player_order_update_timer = bs.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 + + bs.timer(lstart, self._do_light_1) + bs.timer(lstart + inc, self._do_light_2) + bs.timer(lstart + 2 * inc, self._do_light_3) + bs.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = bs.newnode('image', + attrs={ + 'texture': bs.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + bs.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 + }) + bs.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 = bs.newnode('image', + attrs={ + 'texture': bs.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-33 + i * 65, 220), + 'scale': (60, 60), + 'attach': 'center' + }) + bs.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) + + bs.timer(1.0, self._check_squid_end, repeat=True) + self._squidgame_countdown() + + def _squidgame_countdown(self) -> None: + self._countdown_timer = 80 * self._laps # 80 + bs.newnode( + 'image', + attrs={ + 'opacity': 0.7, + 'color': (0.2, 0.2, 0.2), + 'attach': 'topCenter', + 'position': (-220, -40), + 'scale': (135, 45), + 'texture': bs.gettexture('bar')}) + bs.newnode( + 'image', + attrs={ + 'opacity': 1.0, + 'color': (1.0, 0.0, 0.0), + 'attach': 'topCenter', + 'position': (-220, -38), + 'scale': (155, 65), + 'texture': bs.gettexture('uiAtlas'), + 'mesh_transparent': bs.getmesh('meterTransparent')}) + self._sgcountdown_text = bs.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' + bs.getsound('alarm').play() + 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: + self._countdownsounds[self._countdown_timer].play() + 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(bs.DieMessage()) + NewBlast( + position=player.actor.node.position, + velocity=player.actor.node.velocity, + blast_radius=3.0, + blast_type='normal').autoretain() + player.actor.handlemessage( + bs.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: + bs.getsound('tick').play() + self._tick_timer = bs.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: + # bs.getsound('lrlg_06s').play() + # elif random_number == 5.5: + # bs.getsound('lrlg_055s').play() + # elif random_number == 5: + # bs.getsound('lrlg_05s').play() + # elif random_number == 4.5: + # bs.getsound('lrlg_045s').play() + # elif random_number == 4: + # bs.getsound('lrlg_04s').play() + # elif random_number == 3.5: + # bs.getsound('lrlg_035s').play() + # elif random_number == 3: + # bs.getsound('lrlg_03s').play() + 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 + bs.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) + bs.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 + bs.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(bs.DieMessage()) + NewBlast( + position=player.actor.node.position, + velocity=player.actor.node.velocity, + blast_radius=3.0, + blast_type='normal').autoretain() + player.actor.handlemessage( + bs.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) + self._beep_1_sound.play() + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + self._beep_1_sound.play() + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + self._beep_2_sound.play() + 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: + babase.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 = bs.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() + bs.timer(0.2, self._start_delete, repeat=True) + bs.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[babase.Vec3] + try: + pos = player.position + except bs.NotFoundError: + pos = None + if pos is not None: + r_index = player.last_region + rg1 = self._regions[r_index] + r1pt = babase.Vec3(rg1.pos[:3]) + rg2 = self._regions[0] if r_index == len( + self._regions) - 1 else self._regions[r_index + 1] + r2pt = babase.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)) + bs.timer(random.uniform(0.0, 2.0), + bs.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 = bs.newnode('light', + attrs={ + 'position': rmine.point[:3], + 'color': (1, 0.2, 0.2), + 'radius': 0.1, + 'height_attenuated': False + }) + bs.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) + bs.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) + bs.timer(0.95, babase.Call(self._make_mine, m_index)) + + def spawn_player(self, player: Player) -> bs.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 = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + + distance_txt = bs.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, bs.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, bs.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 = bs.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, + bs.DualTeamSession)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment default behavior. + super().handlemessage(msg) + else: + super().handlemessage(msg) diff --git a/dist/ba_root/mods/games/the_spaz_game.py b/dist/ba_root/mods/games/the_spaz_game.py new file mode 100644 index 0000000..74857c1 --- /dev/null +++ b/dist/ba_root/mods/games/the_spaz_game.py @@ -0,0 +1,127 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) + +# ba_meta require api 8 +""" +TheSpazGame - Mini game where all characters looks identical , identify enemies and kill them. +Author: Mr.Smoothy +Discord: https://discord.gg/ucyaesh +Youtube: https://www.youtube.com/c/HeySmoothy +Website: https://bombsquad-community.web.app +Github: https://github.com/bombsquad-community +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.game.elimination import EliminationGame, Player +from bascenev1lib.actor.spazfactory import SpazFactory +import random + +if TYPE_CHECKING: + from typing import Any, Sequence + + +CHARACTER = 'Spaz' + +# ba_meta export bascenev1.GameActivity + + +class TheSpazGame(EliminationGame): + name = 'TheSpazGame' + description = 'Enemy Spaz AmongUs. Kill them all' + scoreconfig = bs.ScoreConfig( + label='Survived', scoretype=bs.ScoreType.SECONDS, none_is_winner=True + ) + + announce_player_deaths = False + + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.15) + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Lives', default=False) + ) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('melee') + + def get_instance_description(self) -> str | Sequence: + return ( + 'Enemy Spaz AmongUs. Kill them all' + ) + + def get_instance_description_short(self) -> str | Sequence: + return ( + 'Enemy Spaz AmongUs. Kill them all' + ) + + def __init__(self, settings: dict): + super().__init__(settings) + self._solo_mode = False + + def spawn_player(self, player: Player) -> bs.Actor: + p = [-6, -4.3, -2.6, -0.9, 0.8, 2.5, 4.2, 5.9] + q = [-4, -2.3, -0.6, 1.1, 2.8, 4.5] + + x = random.randrange(0, len(p)) + y = random.randrange(0, len(q)) + spaz = self.spawn_player_spaz(player, position=(p[x], 1.8, q[y])) + spaz.node.color = (1, 1, 1) + spaz.node.highlight = (1, 0.4, 1) + self.update_appearance(spaz, character=CHARACTER) + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + def update_appearance(self, spaz, character): + factory = SpazFactory.get() + media = factory.get_media(character) + for field, value in media.items(): + setattr(spaz.node, field, value) + spaz.node.style = factory.get_style(character) + spaz.node.name = '' diff --git a/dist/ba_root/mods/games/ultimate_last_stand.py b/dist/ba_root/mods/games/ultimate_last_stand.py new file mode 100644 index 0000000..67f36cf --- /dev/null +++ b/dist/ba_root/mods/games/ultimate_last_stand.py @@ -0,0 +1,623 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +"""Ultimate Last Stand V2: +Made by Cross Joy""" + +# Anyone who wanna help me in giving suggestion/ fix bugs/ by creating PR, +# Can visit my github https://github.com/CrossJoy/Bombsquad-Modding + +# You can contact me through discord: +# My Discord Id: Cross Joy#0721 +# My BS Discord Server: https://discord.gg/JyBY6haARJ + + +# ---------------------------------------------------------------------------- +# V2 What's new? + +# - The "Player can't fight each other" system is removed, +# players exploiting the features and, I know ideas how to fix it especially +# the freeze handlemessage + +# - Added new bot: Ice Bot + +# - The bot spawn location will be more randomize rather than based on players +# position, I don't wanna players stay at the corner of the map. + +# - Some codes clean up. + +# ---------------------------------------------------------------------------- + +# ba_meta require api 8 + +from __future__ import annotations + +import random +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.bomb import TNTSpawner +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.spazbot import (SpazBot, SpazBotSet, BomberBot, + BomberBotPro, BomberBotProShielded, + BrawlerBot, BrawlerBotPro, + BrawlerBotProShielded, TriggerBot, + TriggerBotPro, TriggerBotProShielded, + ChargerBot, StickyBot, ExplodeyBot) + +if TYPE_CHECKING: + from typing import Any, Sequence + from bascenev1lib.actor.spazbot import SpazBot + + +class IceBot(SpazBot): + """A slow moving bot with ice bombs. + + category: Bot Classes + """ + character = 'Pascal' + punchiness = 0.9 + throwiness = 1 + charge_speed_min = 1 + charge_speed_max = 1 + throw_dist_min = 5.0 + throw_dist_max = 20 + run = True + charge_dist_min = 10.0 + charge_dist_max = 11.0 + default_bomb_type = 'ice' + default_bomb_count = 1 + points_mult = 3 + + +class Icon(bs.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: tuple[float, float], + scale: float, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0): + super().__init__() + + self._player = player + self._show_lives = show_lives + self._show_death = show_death + self._name_scale = name_scale + self._outline_tex = bs.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = bs.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + self._name_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': flatness, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + if self._show_lives: + self._lives_text = bs.newnode('text', + owner=self.node, + attrs={ + 'text': 'x0', + 'color': (1, 1, 0.5), + 'h_align': 'left', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_position_and_scale(position, scale) + + def set_position_and_scale(self, position: tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + if self._show_lives: + self._lives_text.position = (position[0] + scale * 10.0, + position[1] - scale * 43.0) + self._lives_text.scale = 1.0 * scale + + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + if self._player: + lives = self._player.lives + else: + lives = 0 + if self._show_lives: + if lives > 0: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + def handle_player_spawned(self) -> None: + """Our player spawned; hooray!""" + if not self.node: + return + self.node.opacity = 1.0 + self.update_for_lives() + + def handle_player_died(self) -> None: + """Well poo; our player died.""" + if not self.node: + return + if self._show_death: + bs.animate( + self.node, 'opacity', { + 0.00: 1.0, + 0.05: 0.0, + 0.10: 1.0, + 0.15: 0.0, + 0.20: 1.0, + 0.25: 0.0, + 0.30: 1.0, + 0.35: 0.0, + 0.40: 1.0, + 0.45: 0.0, + 0.50: 1.0, + 0.55: 0.2 + }) + lives = self._player.lives + if lives == 0: + bs.timer(0.6, self.update_for_lives) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +@dataclass +class SpawnInfo: + """Spawning info for a particular bot type.""" + spawnrate: float + increase: float + dincrease: float + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: float | None = None + self.lives = 0 + self.icons: list[Icon] = [] + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: int | None = None + self.spawn_order: list[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class UltimateLastStand(bs.TeamGameActivity[Player, Team]): + """Minigame involving dodging falling bombs.""" + + name = 'Ultimate Last Stand' + description = 'Only the strongest will stand at the end.' + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + none_is_winner=True) + + # 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[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append( + bs.BoolSetting('Balance Total Lives', default=False)) + return settings + + # We're currently hard-coded for one map. + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Rampage'] + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + + self._scoreboard = Scoreboard() + self._start_time: float | None = None + self._vs_text: bs.Actor | None = None + self._round_end_timer: bs.Timer | None = None + self._lives_per_player = int(settings['Lives Per Player']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False)) + self._epic_mode = settings.get('Epic Mode', True) + self._last_player_death_time: float | None = None + self._timer: OnScreenTimer | None = None + self._tntspawner: TNTSpawner | None = None + self._new_wave_sound = bs.getsound('scoreHit01') + self._bots = SpazBotSet() + self._tntspawnpos = (0, 5.5, -6) + self.spazList = [] + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + + self.node = bs.newnode('text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'color': (0.83, 0.69, 0.21), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, 75), + 'scale': 0.7, + 'text': 'By Cross Joy' + }) + + # For each bot type: [spawnrate, increase, d_increase] + self._bot_spawn_types = { + BomberBot: SpawnInfo(1.00, 0.00, 0.000), + BomberBotPro: SpawnInfo(0.00, 0.05, 0.001), + BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002), + BrawlerBot: SpawnInfo(1.00, 0.00, 0.000), + BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001), + BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), + TriggerBot: SpawnInfo(0.30, 0.00, 0.000), + TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001), + TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), + ChargerBot: SpawnInfo(0.30, 0.05, 0.000), + StickyBot: SpawnInfo(0.10, 0.03, 0.001), + IceBot: SpawnInfo(0.10, 0.03, 0.001), + ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002) + } # yapf: disable + + # Some base class overrides: + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + if self._epic_mode: + self.slow_motion = True + + def get_instance_description(self) -> str | Sequence: + return 'Only the strongest team will stand at the end.' if isinstance( + self.session, + bs.DualTeamSession) else 'Only the strongest will stand at the end.' + + def get_instance_description_short(self) -> str | Sequence: + return 'Only the strongest team will stand at the end.' if isinstance( + self.session, + bs.DualTeamSession) else 'Only the strongest will stand at the end.' + + def on_transition_in(self) -> None: + super().on_transition_in() + bs.timer(1.3, self._new_wave_sound.play) + + def on_player_join(self, player: Player) -> None: + player.lives = self._lives_per_player + + # Don't waste time doing this until begin. + 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 on_begin(self) -> None: + super().on_begin() + bs.animate_array(node=self.node, attr='color', size=3, keys={ + 0.0: (0.5, 0.5, 0.5), + 0.8: (0.83, 0.69, 0.21), + 1.6: (0.5, 0.5, 0.5) + }, loop=True) + + bs.timer(0.001, bs.WeakCall(self._start_bot_updates)) + self._tntspawner = TNTSpawner(position=self._tntspawnpos, + respawn_time=10.0) + + self._timer = OnScreenTimer() + self._timer.start() + self.setup_standard_powerup_drops() + + # Check for immediate end (if we've only got 1 player, etc). + self._start_time = bs.time() + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + bs.timer(1.0, self._update, repeat=True) + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + + 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, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + 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 on_player_leave(self, player: Player) -> None: + # Augment default behavior. + super().on_player_leave(player) + player.icons = [] + + # Update icons in a moment since our team will be gone from the + # list then. + bs.timer(0, self._update_icons) + + # If the player to leave was the last in spawn order and had + # their final turn currently in-progress, mark the survival time + # for their team. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - self._start_time) + + # A departing player may trigger game-over. + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> bs.Actor: + actor = self.spawn_player_spaz(player) + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + return actor + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + popuptext.PopupText('x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=player.node.position).autoretain() + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def _start_bot_updates(self) -> None: + self._bot_update_interval = 3.3 - 0.3 * (len(self.players)) + self._update_bots() + self._update_bots() + if len(self.players) > 2: + self._update_bots() + if len(self.players) > 3: + self._update_bots() + self._bot_update_timer = bs.Timer(self._bot_update_interval, + bs.WeakCall(self._update_bots)) + + def _update_bots(self) -> None: + assert self._bot_update_interval is not None + self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98) + self._bot_update_timer = bs.Timer(self._bot_update_interval, + bs.WeakCall(self._update_bots)) + botspawnpts: list[Sequence[float]] = [[-5.0, 5.5, -4.14], + [0.0, 5.5, -4.14], + [5.0, 5.5, -4.14]] + for player in self.players: + try: + if player.is_alive(): + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + except Exception: + babase.print_exception('Error updating bots.') + + spawnpt = random.choice( + [botspawnpts[0], botspawnpts[1], botspawnpts[2]]) + + spawnpt = (spawnpt[0] + 3.0 * (random.random() - 0.5), spawnpt[1], + 2.0 * (random.random() - 0.5) + spawnpt[2]) + + # Normalize our bot type total and find a random number within that. + total = 0.0 + for spawninfo in self._bot_spawn_types.values(): + total += spawninfo.spawnrate + randval = random.random() * total + + # Now go back through and see where this value falls. + total = 0 + bottype: type[SpazBot] | None = None + for spawntype, spawninfo in self._bot_spawn_types.items(): + total += spawninfo.spawnrate + if randval <= total: + bottype = spawntype + break + spawn_time = 1.0 + assert bottype is not None + self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time) + + # After every spawn we adjust our ratios slightly to get more + # difficult. + for spawninfo in self._bot_spawn_types.values(): + spawninfo.spawnrate += spawninfo.increase + spawninfo.increase += spawninfo.dincrease + + # Various high-level game events come through this method. + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = bs.time() + + # Record the player's moment of death. + # assert isinstance(msg.spaz.player + msg.getplayer(Player).death_time = curtime + + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + 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 player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - + self._start_time) + else: + # Otherwise, in regular mode, respawn. + self.respawn_player(player) + + 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 _update(self) -> None: + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0.5, self.end_game) + + def end_game(self) -> None: + # 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 = bs.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: + # Submit the score value in milliseconds. + results.set_team_score(team, team.survival_seconds) + + self.end(results=results) diff --git a/dist/ba_root/mods/games/zombie_horde.py b/dist/ba_root/mods/games/zombie_horde.py new file mode 100644 index 0000000..bd8bf2b --- /dev/null +++ b/dist/ba_root/mods/games/zombie_horde.py @@ -0,0 +1,885 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import copy +import random +from babase import _math +from bascenev1._coopsession import CoopSession +from bascenev1._messages import PlayerDiedMessage, StandMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.game.elimination import Icon, Player +from bascenev1lib.actor.spaz import PickupMessage +from bascenev1lib.actor.spazbot import SpazBotSet, BrawlerBot, SpazBotDiedMessage +from bascenev1lib.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, bs.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, bs.FreezeMessage): + pass + + elif isinstance(m, PickupMessage): + if not self.node: + return None + + try: + collision = bs.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + except bs.NotFoundError: + return True + + try: + if opposingnode.invincible: + return True + except Exception: + pass + + 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, bs.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 = bs.Timer(0.05, + bs.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 = [] + babase.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 bs.getactivity().players: + assert isinstance(player, bs.Player) + try: + if player.is_alive(): + assert isinstance(player.actor, Spaz) + assert player.actor.node + if player.lives > 0: + player_pts.append( + (babase.Vec3(player.actor.node.position), + babase.Vec3(player.actor.node.velocity))) + except Exception: + babase.print_exception('Error on bot-set _update.') + + for bot in bot_list: + bot.set_player_points(player_pts) + bot.update_ai() + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + self.spawn_order: list[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class ZombieHorde(bs.TeamGameActivity[Player, Team]): + + name = 'Zombie Horde' + description = 'Kill walkers for points!' + scoreconfig = bs.ScoreConfig(label='Score', + scoretype=bs.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[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.IntSetting( + 'Max Zombies', + default=10, + min_value=5, + max_value=50, + increment=5, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Lives', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._start_time: float | None = None + self._vs_text: bs.Actor | None = None + self._round_end_timer: bs.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 = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + + self.spazList = [] + self.zombieQ = 0 + + activity = bs.getactivity() + my_factory = SpazFactory.get() + + appears = ['Kronk', 'Zoe', 'Pixel', 'Agent Johnson', + 'Bones', 'Frosty', 'Kronk2'] + myAppear = copy.copy(babase.app.classic.spaz_appearances['Kronk']) + myAppear.name = 'Kronk2' + babase.app.classic.spaz_appearances['Kronk2'] = myAppear + for appear in appears: + my_factory.get_media(appear) + med = my_factory.spaz_media + med['Kronk2']['head_mesh'] = med['Zoe']['head_mesh'] + med['Kronk2']['color_texture'] = med['Agent Johnson']['color_texture'] + med['Kronk2']['color_mask_texture'] = med['Pixel']['color_mask_texture'] + med['Kronk2']['torso_mesh'] = med['Bones']['torso_mesh'] + med['Kronk2']['pelvis_mesh'] = med['Pixel']['pelvis_mesh'] + med['Kronk2']['upper_arm_mesh'] = med['Frosty']['upper_arm_mesh'] + med['Kronk2']['forearm_mesh'] = med['Frosty']['forearm_mesh'] + med['Kronk2']['hand_mesh'] = med['Bones']['hand_mesh'] + med['Kronk2']['upper_leg_mesh'] = med['Bones']['upper_leg_mesh'] + med['Kronk2']['lower_leg_mesh'] = med['Pixel']['lower_leg_mesh'] + med['Kronk2']['toes_mesh'] = med['Bones']['toes_mesh'] + + def get_instance_description(self) -> str | Sequence: + return ('Kill walkers for points! ', + 'Dead player walker: 2 points!') if isinstance( + self.session, bs.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, bs.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 = [] + bs.broadcastmessage( + babase.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 = bs.time() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self.zombieQ = 1 + if self._solo_mode: + self._vs_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': babase.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._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. + bs.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, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> babase.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 = babase.Vec3(living_player_pos) + points: list[tuple[float, babase.Vec3]] = [] + for team in self.teams: + start_pos = babase.Vec3(self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos)) + # Hmm.. we need to sorting vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def spawn_player(self, player: Player) -> bs.Actor: + position = self.map.get_ffa_start_position(self.players) + angle = 20 + name = player.getname() + + light_color = _math.normalized_color(player.color) + display_color = _babase.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))) + bs.Sound.play(self._spawn_sound, 1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + if not self._solo_mode: + bs.timer(0.3, babase.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 bascenev1lib.actor.respawnicon import RespawnIcon + player.customdata['respawn_timer'] = bs.Timer( + respawn_time, bs.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: PlayerT) -> None: + """ + A utility method which calls self.spawn_player() *only* if the + bs.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: PlayerT) -> bs.Actor: + position = self.map.get_ffa_start_position(self.players) + angle = 20 + name = player.getname() + + light_color = _math.normalized_color(player.color) + display_color = _babase.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))) + bs.Sound.play(self._spawn_sound, 1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + for icon in player.icons: + icon.handle_player_spawned() + return spaz + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + 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. + bs.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, bs.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: + babase.print_error( + "Got lives < 0 in Elim; this shouldn't happen. solo:" + + str(self._solo_mode)) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + 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 = bs.Timer(0.5, self.end_game) + else: + self._round_end_timer = bs.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] + bs.timer(0.1, babase.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 = bs.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 bascenev1.GameActivity +class ZombieHordeCoop(ZombieHorde): + + name = 'Zombie Horde' + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Football Stadium'] + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.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 = bs.Timer(0.5, self.end_game) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior. + bs.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(babase.Plugin): + def on_app_running(self) -> None: + babase.app.classic.add_coop_practice_level( + bs._level.Level( + 'Zombie Horde', + gametype=ZombieHordeCoop, + settings={}, + preview_texture_name='footballStadiumPreview', + ) + )