diff --git a/dist/ba_root/mods/games/BetterDeathmatch.py b/dist/ba_root/mods/games/BetterDeathmatch.py new file mode 100644 index 0000000..e7af9f1 --- /dev/null +++ b/dist/ba_root/mods/games/BetterDeathmatch.py @@ -0,0 +1,268 @@ +#BetterDeathMatch +#Made by your friend: @[Just] Freak#4999 + +"""Defines a very-customisable DeathMatch mini-game""" + +# ba_meta require api 7 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class BetterDeathMatchGame(ba.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[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + + +## Add settings ## + ba.BoolSetting('Enable Gloves', False), + ba.BoolSetting('Enable Powerups', True), + ba.BoolSetting('Night Mode', False), + ba.BoolSetting('Icy Floor', False), + ba.BoolSetting('One Punch Kill', False), + ba.BoolSetting('Spawn with Shield', False), + ba.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, ba.FreeForAllSession): + settings.append( + ba.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = ba.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 = (ba.MusicType.EPIC if self._epic_mode else + ba.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 = ba.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: + ba.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, ba.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, ba.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: + ba.playsound(self._dingsound) + 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 + ba.playsound(self._dingsound) + + # 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): + ba.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + +## Run settings related: Spaz ## + def spawn_player(self, player: Player) -> ba.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 = ba.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/Bombers.py b/dist/ba_root/mods/games/Bombers.py index b09695d..bd322b7 100644 --- a/dist/ba_root/mods/games/Bombers.py +++ b/dist/ba_root/mods/games/Bombers.py @@ -1,4 +1,4 @@ -# ba_meta require api 6 +# ba_meta require api 7 #self._has_boxing_gloves = True from __future__ import annotations diff --git a/dist/ba_root/mods/games/BotShower.py b/dist/ba_root/mods/games/BotShower.py new file mode 100644 index 0000000..7e6ac73 --- /dev/null +++ b/dist/ba_root/mods/games/BotShower.py @@ -0,0 +1,191 @@ + +# ba_meta require api 7 + +from __future__ import annotations +from typing import TYPE_CHECKING + +import ba, random +from bastd.actor.onscreentimer import OnScreenTimer +from bastd.actor.spazbot import ( + SpazBot, SpazBotSet, + BomberBot, BrawlerBot, BouncyBot, + ChargerBot, StickyBot, TriggerBot, + ExplodeyBot) + +if TYPE_CHECKING: + from typing import Any, List, Type, Optional + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export game +class BotShowerGame(ba.TeamGameActivity[Player, Team]): + """A ba.MeteorShowerGame but replaced with bots.""" + + name = 'Bot Shower' + description = 'Survive from the bots.' + available_settings = [ + ba.BoolSetting('Spaz', default=True), + ba.BoolSetting('Zoe', default=True), + ba.BoolSetting('Kronk', default=True), + ba.BoolSetting('Snake Shadow', default=True), + ba.BoolSetting('Mel', default=True), + ba.BoolSetting('Jack Morgan', default=True), + ba.BoolSetting('Easter Bunny', default=True), + ba.BoolSetting('Epic Mode', default=False), + ] + + announce_player_deaths = True + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Football Stadium', 'Hockey Stadium'] + + def __init__(self, settings: dict) -> None: + super().__init__(settings) + self._epic_mode = settings['Epic Mode'] + self._last_player_death_time: Optional[float] = None + self._timer: Optional[OnScreenTimer] = None + self._bots: Optional[SpazBotSet] = None + self._bot_type: List[SpazBot] = [] + + if bool(settings['Spaz']) == True: + self._bot_type.append(BomberBot) + else: + if BomberBot in self._bot_type: + self._bot_type.remove(BomberBot) + if bool(settings['Zoe']) == True: + self._bot_type.append(TriggerBot) + else: + if TriggerBot in self._bot_type: + self._bot_type.remove(TriggerBot) + if bool(settings['Kronk']) == True: + self._bot_type.append(BrawlerBot) + else: + if BrawlerBot in self._bot_type: + self._bot_type.remove(BrawlerBot) + if bool(settings['Snake Shadow']) == True: + self._bot_type.append(ChargerBot) + else: + if ChargerBot in self._bot_type: + self._bot_type.remove(ChargerBot) + if bool(settings['Jack Morgan']) == True: + self._bot_type.append(ExplodeyBot) + else: + if ExplodeyBot in self._bot_type: + self._bot_type.remove(ExplodeyBot) + if bool(settings['Easter Bunny']) == True: + self._bot_type.append(BouncyBot) + else: + if BouncyBot in self._bot_type: + self._bot_type.remove(BouncyBot) + + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SURVIVAL) + + def on_begin(self) -> None: + super().on_begin() + self._bots = SpazBotSet() + self._timer = OnScreenTimer() + self._timer.start() + + if self._epic_mode: + ba.timer(1.0, self._start_spawning_bots) + else: + ba.timer(5.0, self._start_spawning_bots) + + ba.timer(5.0, self._check_end_game) + + def spawn_player(self, player: Player) -> None: + spaz = self.spawn_player_spaz(player) + spaz.connect_controls_to_player( + enable_punch=False, + enable_bomb=False, + enable_pickup=False) + return spaz + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(1, 1, 0), + ) + + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + self.spawn_player(player) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + curtime = ba.time() + msg.getplayer(Player).death_time = curtime + + ba.timer(1.0, self._check_end_game) + else: + super().handlemessage(msg) + + def _start_spawning_bots(self) -> None: + ba.timer(1.2, self._spawn_bot, repeat=True) + ba.timer(2.2, self._spawn_bot, repeat=True) + + def _spawn_bot(self) -> None: + assert self._bots is not None + self._bots.spawn_bot(random.choice(self._bot_type), pos=(random.uniform(-11, 11), (9.8 if self.map.getname() == 'Football Stadium' else 5.0), random.uniform(-5, 5))) + + 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 + + if living_team_count <= 1: + self.end_game() + + def end_game(self) -> None: + cur_time = ba.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + for team in self.teams: + for player in team.players: + survived = False + + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 + self.stats.player_scored(player, score, screenmessage=False) + + self._timer.stop(endtime=self._last_player_death_time) + + results = ba.GameResults() + + for team in self.teams: + + 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) + + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) \ No newline at end of file diff --git a/dist/ba_root/mods/games/Bounty.py b/dist/ba_root/mods/games/Bounty.py index a052ace..67ff4aa 100644 --- a/dist/ba_root/mods/games/Bounty.py +++ b/dist/ba_root/mods/games/Bounty.py @@ -2,7 +2,7 @@ # """DeathMatch game and support classes.""" -# ba_meta require api 6 +# ba_meta require api 7 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations diff --git a/dist/ba_root/mods/games/CanonFight.py b/dist/ba_root/mods/games/CanonFight.py new file mode 100644 index 0000000..4a362bf --- /dev/null +++ b/dist/ba_root/mods/games/CanonFight.py @@ -0,0 +1,199 @@ +# Released under the MIT License. See LICENSE for details. +# +"""DeathMatch game and support classes.""" + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Union, Sequence, Optional + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class CanonFightGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Canon Fight' + description = 'Kill a set number of enemies to win.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session]) -> list[ba.Setting]: + settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, ba.FreeForAllSession): + settings.append( + ba.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ["Step Right Up"] + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.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, ba.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: + ba.playsound(self._dingsound) + 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 + ba.playsound(self._dingsound) + + # 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): + ba.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 = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) \ No newline at end of file diff --git a/dist/ba_root/mods/games/Cursers.py b/dist/ba_root/mods/games/Cursers.py new file mode 100644 index 0000000..cf08e8e --- /dev/null +++ b/dist/ba_root/mods/games/Cursers.py @@ -0,0 +1,393 @@ + +# By itsre3 (: +# Just testing my abilities +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING +import ba +import _ba +import random +from bastd.actor.spazfactory import SpazFactory +from bastd.gameutils import SharedObjects +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Union, Sequence, Optional + + +class WwFactory(object): + def __init__(self): + self.last_shoot_time = 0 + shared = SharedObjects.get() + self.ball_material = ba.Material() + self.ball_material.add_actions(conditions = ((("we_are_younger_than", 5), "or", ("they_are_younger_than", 50)), "and", ("they_have_material", shared.object_material)), actions = ("modify_node_collision", 'collide', False)) + self.ball_material.add_actions( + conditions=('they_have_material', shared.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", shared.player_material), + actions = (("modify_part_collision", "physical", False), + ('message', 'our_node', 'at_connect', TouchedSpaz())), + ) + self.ball_material.add_actions( + conditions = (("they_dont_have_material", shared.player_material), 'and', + ("they_have_material", shared.object_material)), + actions = ('message', 'our_node', 'at_connect', TouchedObject()), + ) + self.ball_material.add_actions( + conditions = (("they_dont_have_material", shared.player_material), 'and', + ("they_have_material", shared.footing_material)), + actions = ('message', 'our_node', 'at_connect', TouchedFootingMaterial()), + ) + + def give_orb_ball(self, spaz: ba.Actor) -> None: + spaz.punch_callback = self.shoot_orb_ball + self.last_shoot_time = ba.time(timetype=ba.TimeType.BASE, timeformat=ba.TimeFormat.MILLISECONDS) + + def shoot_orb_ball(self, spaz: ba.Actor) -> None: + shoot_time = ba.time(timetype=ba.TimeType.BASE, timeformat=ba.TimeFormat.MILLISECONDS) + if shoot_time - self.last_shoot_time > 800: + + position1 = spaz.node.position_center + position2 = spaz.node.position_forward + ball_direction = (position1[0] - position2[0], 0.0, position1[2] - position2[2]) + magnitude = 10.0 / ba.Vec3(*ball_direction).length() + velocity = [v * magnitude for v in ball_direction] + OrbBall(position = spaz.node.position, velocity = (velocity[0]*2, velocity[1]*2, velocity[2]*2), owner = spaz.getplayer(playertype = ba.Player), source_player = spaz.getplayer(playertype = ba.Player)).autoretain() + +class TouchedSpaz(object): + pass + +class TouchedObject(object): + pass + +class TouchedFootingMaterial(object): + pass + +class OrbBall(ba.Actor): + def __init__(self, position = (0, 1, 0), velocity = (0, 0, 0), owner = None, source_player = None, sessiontype = type(ba.Session)) -> None: + ba.Actor.__init__(self) + factory = self.get_factory() + self.source_player = source_player + shared = SharedObjects.get() + self.node = ba.newnode("prop", + attrs = { + 'position': position, + 'velocity': velocity, + 'model': ba.getmodel("shield"), + 'body': 'sphere', + 'color_texture': ba.gettexture("powerupCurse"), + 'model_scale': 0.5, + 'is_area_of_interest': True, + 'body_scale': 1.3, + 'reflection': 'soft', + 'reflection_scale': [1.0], + 'materials': [shared.object_material, factory.ball_material] + }, + delegate = self) + self.owner = owner + self.source_player = source_player + self.light = ba.newnode("light", attrs = {"color": (0.8, 0.4, 0.2), "height_attenuated": False, "radius": 0.1}) + self.node.connectattr("position", self.light, "position") + ba.animate(self.light, "intensity", {0: 1.3, 250: 1.8, 500: 1.3}, loop = True, timetype = ba.TimeType.SIM, timeformat=ba.TimeFormat.MILLISECONDS) + self.orb_ball_life_timer = ba.Timer(1, ba.WeakCall(self.die)) + + def die(self) -> None: + self.light.delete() + self.node.handlemessage(ba.DieMessage()) + + @classmethod + def get_factory(cls): + activity = ba.getactivity() + if activity is None: + raise Exception("no current activity found!") + try: + return activity._orbBallFactory + except: + f = activity._orbBallFactory = WwFactory() + return f + + def handlemessage(self, msg): + super(self.__class__, self).handlemessage(msg) + if isinstance(msg, TouchedObject): + node = ba.getcollision().opposingnode + if node and node.exists(): + v = self.node.velocity + m = ba.Vec3(*v).length() * 40 + node.handlemessage( + ba.HitMessage(pos = self.node.position, + velocity = v, + magnitude = m, + velocity_magnitude = m, + radius = 0, + srcnode = self.node, + source_player = self.source_player, + force_direction = self.node.velocity) + ) + self.node.handlemessage(ba.DieMessage()) + + elif isinstance(msg, ba.DieMessage): + + if self.node.exists(): + velocity = self.node.velocity + explosion = ba.newnode("explosion", + attrs = {'position': self.node.position, + 'velocity': (velocity[0], max(-1.0, velocity[1]), velocity[2]), + 'radius': 2, + 'big': False + }) + ba.playsound(sound = ba.getsound(random.choice(['impactHard', 'impactHard2', 'impactHard3'])), position = self.node.position) + self.node.delete() + self.emit_timer = None + + elif isinstance(msg, ba.OutOfBoundsMessage): + self.node.handlemessage(ba.DieMessage()) + + elif isinstance(msg, ba.HitMessage): + 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] + ) + + elif isinstance(msg, TouchedSpaz): + node = ba.getcollision().opposingnode + if node and node.exists(): + node.handlemessage( + ba.PowerupMessage('curse')) + node.handlemessage('knockout', 250) + ba.playsound(sound = ba.getsound('impactHard2')) + + + self.node.handlemessage(ba.DieMessage()) + + elif isinstance(msg, TouchedFootingMaterial): + ba.playsound(sound = ba.getsound("blip"), position = self.node.position) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class CursersGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Curser\'s' + description = 'Teach enemies lesson\nPress punch to summon curse' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session]) -> list[ba.Setting]: + settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ba.BoolSetting('Credits', default=True) + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, ba.FreeForAllSession): + settings.append( + ba.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._cd_text: Optional[ba.Actor] = None + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + self._credits = bool(settings['Credits']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Curse ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'Curse ${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) + if self._credits: + self._cd_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': (0, 0), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.8, + 'v_attach': 'bottom', + 'color': (1, 1, 1), + 'text': 'By itsre3' + })) + + + # 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 spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player) + WwFactory().give_orb_ball(spaz) + spaz.connect_controls_to_player(enable_punch=True, + enable_bomb=False, + enable_pickup=True) +# This below overwrites default character. Vishuuuuuuuuuu lol + spaz.node.color_mask_texture = ba.gettexture('wizardColorMask') + spaz.node.color_texture = ba.gettexture('powerupCurse') + spaz.node.head_model = ba.getmodel('wizardHead') + spaz.node.hand_model = ba.getmodel('wizardHand') + spaz.node.torso_model = ba.getmodel('wizardTorso') + spaz.node.pelvis_model = ba.getmodel('wizardPelvis') + spaz.node.upper_arm_model = ba.getmodel('wizardUpperArm') + spaz.node.forearm_model = ba.getmodel('wizardForeArm') + spaz.node.upper_leg_model = ba.getmodel('wizardUpperLeg') + spaz.node.lower_leg_model = ba.getmodel('wizardLowerLeg') + spaz.node.toes_model = ba.getmodel('wizardToes') + wizard_sounds = [ba.getsound('wizard1'), ba.getsound('wizard2'), ba.getsound('wizard3'),ba.getsound('wizard4')] + spaz.node.jump_sounds = wizard_sounds + spaz.node.attack_sounds = wizard_sounds + spaz.node.impact_sounds = wizard_sounds + spaz.node.pickup_sounds = wizard_sounds + spaz.node.death_sounds = [ba.getsound('wizardDeath')] + spaz.node.fall_sounds = [ba.getsound('wizardFall')] + + return spaz + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.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, ba.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: + ba.playsound(self._dingsound) + 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 + ba.playsound(self._dingsound) + # 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): + ba.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 = ba.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/Explodo_Run.py b/dist/ba_root/mods/games/Explodo_Run.py index dfa822a..33eeaee 100644 --- a/dist/ba_root/mods/games/Explodo_Run.py +++ b/dist/ba_root/mods/games/Explodo_Run.py @@ -1,5 +1,5 @@ -# ba_meta require api 6 +# ba_meta require api 7 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations diff --git a/dist/ba_root/mods/games/FireBallFight.py b/dist/ba_root/mods/games/FireBallFight.py new file mode 100644 index 0000000..257301b --- /dev/null +++ b/dist/ba_root/mods/games/FireBallFight.py @@ -0,0 +1,218 @@ +# Released under the MIT License. See LICENSE for details. +"""FireBall Fight game and support classes.""" + +# FireBall Fight game +# This was Originally made by MattZ45986 +# I ported this to 1.6 and added some new things +# This is only version 1, there are more version to come yet. +# so till then, enjoy this and suggest what to add. +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING +import ba, random, base64 +from bastd.gameutils import SharedObjects +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Union, Sequence, Optional + +# encoding this code because it's messy, so it's better if you don't decode this. +exec(base64.b64decode("Y2xhc3MgRmlyZUJhbGxGYWN0b3J5KG9iamVjdCk6CiAgICBkZWYgX19pbml0X18oc2VsZik6CiAgICAgICAgc2VsZi5sYXN0X3Nob3RfdGltZSA9IDAKICAgICAgICBzaGFyZWQgPSBTaGFyZWRPYmplY3RzLmdldCgpCiAgICAgICAgc2VsZi5iYWxsX21hdGVyaWFsID0gYmEuTWF0ZXJpYWwoKQogICAgICAgIHNlbGYuYmFsbF9tYXRlcmlhbC5hZGRfYWN0aW9ucyhjb25kaXRpb25zID0gKCgoIndlX2FyZV95b3VuZ2VyX3RoYW4iLCA1KSwgIm9yIiwgKCJ0aGV5X2FyZV95b3VuZ2VyX3RoYW4iLCA1MCkpLCAiYW5kIiwgKCJ0aGV5X2hhdmVfbWF0ZXJpYWwiLCBzaGFyZWQub2JqZWN0X21hdGVyaWFsKSksIGFjdGlvbnMgPSAoIm1vZGlmeV9ub2RlX2NvbGxpc2lvbiIsICdjb2xsaWRlJywgRmFsc2UpKQogICAgICAgIHNlbGYuYmFsbF9tYXRlcmlhbC5hZGRfYWN0aW9ucygKICAgICAgICAgICAgY29uZGl0aW9ucz0oJ3RoZXlfaGF2ZV9tYXRlcmlhbCcsIHNoYXJlZC5waWNrdXBfbWF0ZXJpYWwpLAogICAgICAgICAgICBhY3Rpb25zPSgnbW9kaWZ5X3BhcnRfY29sbGlzaW9uJywgJ3VzZV9ub2RlX2NvbGxpZGUnLCBGYWxzZSksCiAgICAgICAgICAgICkKICAgICAgICBzZWxmLmJhbGxfbWF0ZXJpYWwuYWRkX2FjdGlvbnMoCiAgICAgICAgICAgIGFjdGlvbnMgPSAoIm1vZGlmeV9wYXJ0X2NvbGxpc2lvbiIsICJmcmljdGlvbiIsIDApCiAgICAgICAgICAgICkKICAgICAgICBzZWxmLmJhbGxfbWF0ZXJpYWwuYWRkX2FjdGlvbnMoCiAgICAgICAgICAgIGNvbmRpdGlvbnMgPSAoInRoZXlfaGF2ZV9tYXRlcmlhbCIsIHNoYXJlZC5wbGF5ZXJfbWF0ZXJpYWwpLAogICAgICAgICAgICBhY3Rpb25zID0gKCgibW9kaWZ5X3BhcnRfY29sbGlzaW9uIiwgInBoeXNpY2FsIiwgRmFsc2UpLAogICAgICAgICAgICAoJ21lc3NhZ2UnLCAnb3VyX25vZGUnLCAnYXRfY29ubmVjdCcsIFRvdWNoZWRTcGF6KCkpKSwKICAgICAgICAgICAgKQogICAgICAgIHNlbGYuYmFsbF9tYXRlcmlhbC5hZGRfYWN0aW9ucygKICAgICAgICAgICAgY29uZGl0aW9ucyA9ICgoInRoZXlfZG9udF9oYXZlX21hdGVyaWFsIiwgc2hhcmVkLnBsYXllcl9tYXRlcmlhbCksICdhbmQnLAogICAgICAgICAgICAoInRoZXlfaGF2ZV9tYXRlcmlhbCIsIHNoYXJlZC5vYmplY3RfbWF0ZXJpYWwpKSwKICAgICAgICAgICAgYWN0aW9ucyA9ICgnbWVzc2FnZScsICdvdXJfbm9kZScsICdhdF9jb25uZWN0JywgVG91Y2hlZE9iamVjdCgpKSwKICAgICAgICAgICAgKQogICAgICAgIHNlbGYuYmFsbF9tYXRlcmlhbC5hZGRfYWN0aW9ucygKICAgICAgICAgICAgY29uZGl0aW9ucyA9ICgoInRoZXlfZG9udF9oYXZlX21hdGVyaWFsIiwgc2hhcmVkLnBsYXllcl9tYXRlcmlhbCksICdhbmQnLAogICAgICAgICAgICAoInRoZXlfaGF2ZV9tYXRlcmlhbCIsIHNoYXJlZC5mb290aW5nX21hdGVyaWFsKSksCiAgICAgICAgICAgIGFjdGlvbnMgPSAoJ21lc3NhZ2UnLCAnb3VyX25vZGUnLCAnYXRfY29ubmVjdCcsIFRvdWNoZWRGb290aW5nTWF0ZXJpYWwoKSksCiAgICAgICAgICAgICkKICAgIAogICAgZGVmIGdyYW50X2ZpcmVfYmFsbChzZWxmLCBzcGF6OiBiYS5BY3RvcikgLT4gTm9uZToKICAgICAgICBzcGF6LnB1bmNoX2NhbGxiYWNrID0gc2VsZi5zaG90X2ZpcmVfYmFsbAogICAgICAgIHNlbGYubGFzdF9zaG90X3RpbWUgPSBiYS50aW1lKHRpbWV0eXBlPWJhLlRpbWVUeXBlLkJBU0UsIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAKICAgIGRlZiBzaG90X2ZpcmVfYmFsbChzZWxmLCBzcGF6OiBiYS5BY3RvcikgLT4gTm9uZToKICAgICAgICBzaG90X3RpbWUgPSBiYS50aW1lKHRpbWV0eXBlPWJhLlRpbWVUeXBlLkJBU0UsIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgaWYgc2hvdF90aW1lIC0gc2VsZi5sYXN0X3Nob3RfdGltZSA+IDkwMDoKICAgICAgICAgICAgcG9zaXRpb24xID0gc3Bhei5ub2RlLnBvc2l0aW9uX2NlbnRlcgogICAgICAgICAgICBwb3NpdGlvbjIgPSBzcGF6Lm5vZGUucG9zaXRpb25fZm9yd2FyZAogICAgICAgICAgICBiYWxsX2RpcmVjdGlvbiA9IChwb3NpdGlvbjFbMF0gLSBwb3NpdGlvbjJbMF0sIDAuMCwgcG9zaXRpb24xWzJdIC0gcG9zaXRpb24yWzJdKQogICAgICAgICAgICBtYWduaXR1ZGUgPSAxMC4wIC8gYmEuVmVjMygqYmFsbF9kaXJlY3Rpb24pLmxlbmd0aCgpCiAgICAgICAgICAgIHZlbG9jaXR5ID0gW3YgKiBtYWduaXR1ZGUgZm9yIHYgaW4gYmFsbF9kaXJlY3Rpb25dCiAgICAgICAgICAgIEZpcmVCYWxsKHBvc2l0aW9uID0gc3Bhei5ub2RlLnBvc2l0aW9uLCB2ZWxvY2l0eSA9ICh2ZWxvY2l0eVswXSoyLCB2ZWxvY2l0eVsxXSoyLCB2ZWxvY2l0eVsyXSoyKSwgb3duZXIgPSBzcGF6LmdldHBsYXllcihwbGF5ZXJ0eXBlID0gYmEuUGxheWVyKSwgc291cmNlX3BsYXllciA9IHNwYXouZ2V0cGxheWVyKHBsYXllcnR5cGUgPSBiYS5QbGF5ZXIpKS5hdXRvcmV0YWluKCkKCmNsYXNzIFRvdWNoZWRTcGF6KG9iamVjdCk6CiAgICBwYXNzCgpjbGFzcyBUb3VjaGVkT2JqZWN0KG9iamVjdCk6CiAgICBwYXNzCgpjbGFzcyBUb3VjaGVkRm9vdGluZ01hdGVyaWFsKG9iamVjdCk6CiAgICBwYXNzCgpjbGFzcyBGaXJlQmFsbChiYS5BY3Rvcik6CiAgICBkZWYgX19pbml0X18oc2VsZiwgcG9zaXRpb24gPSAoMCwgNSwgMCksIHZlbG9jaXR5ID0gKDAsIDUsIDApLCBvd25lciA9IE5vbmUsIHNvdXJjZV9wbGF5ZXIgPSBOb25lKSAtPiBOb25lOgogICAgICAgIGJhLkFjdG9yLl9faW5pdF9fKHNlbGYpCiAgICAgICAgZmFjdG9yeSA9IHNlbGYuZ2V0X2ZhY3RvcnkoKQogICAgICAgIHNoYXJlZCA9IFNoYXJlZE9iamVjdHMuZ2V0KCkKICAgICAgICBzZWxmLm5vZGUgPSBiYS5uZXdub2RlKCJwcm9wIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGF0dHJzID0gewogICAgICAgICAgICAgICAgICAgICAgICAgICAgJ3Bvc2l0aW9uJzogcG9zaXRpb24sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAndmVsb2NpdHknOiB2ZWxvY2l0eSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICdtb2RlbCc6IGJhLmdldG1vZGVsKCJpbXBhY3RCb21iIiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAnYm9keSc6ICdzcGhlcmUnLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgJ2NvbG9yX3RleHR1cmUnOiBiYS5nZXR0ZXh0dXJlKCJidW5ueUNvbG9yIiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAnbW9kZWxfc2NhbGUnOiAwLjIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAnaXNfYXJlYV9vZl9pbnRlcmVzdCc6IFRydWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAnYm9keV9zY2FsZSc6IDAuOCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICdtYXRlcmlhbHMnOiBbc2hhcmVkLm9iamVjdF9tYXRlcmlhbCwgZmFjdG9yeS5iYWxsX21hdGVyaWFsXQogICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRlbGVnYXRlID0gc2VsZikKICAgICAgICBzZWxmLnNvdXJjZV9wbGF5ZXIgPSBzb3VyY2VfcGxheWVyCiAgICAgICAgc2VsZi5vd25lciA9IG93bmVyCiAgICAgICAgc2VsZi5saWdodCA9IGJhLm5ld25vZGUoImxpZ2h0IiwgYXR0cnMgPSB7ImNvbG9yIjogKDEsIDAuNiwgMC40KSwgImhlaWdodF9hdHRlbnVhdGVkIjogRmFsc2UsICJyYWRpdXMiOiAwLjN9KQogICAgICAgIHNlbGYubm9kZS5jb25uZWN0YXR0cigicG9zaXRpb24iLCBzZWxmLmxpZ2h0LCAicG9zaXRpb24iKQogICAgICAgIGJhLmFuaW1hdGUoc2VsZi5saWdodCwgImludGVuc2l0eSIsIHswOiAxLjMsIDI1MDogMS44LCA1MDA6IDEuM30sIGxvb3AgPSBUcnVlLCB0aW1ldHlwZSA9IGJhLlRpbWVUeXBlLlNJTSwgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykKICAgICAgICBzZWxmLmZpcmVfYmFsbF9saWZlX3RpbWVyID0gYmEuVGltZXIoMSwgYmEuV2Vha0NhbGwoc2VsZi5kaWUpKQogICAgICAgIHNlbGYuZW1pdF90aW1lciA9IGJhLlRpbWVyKDAuMDE1LCBiYS5XZWFrQ2FsbChzZWxmLmVtaXQpLCByZXBlYXQgPSBUcnVlKQogICAgCiAgICBkZWYgZW1pdChzZWxmKSAtPiBOb25lOgogICAgICAgIGJhLmVtaXRmeChwb3NpdGlvbiA9IHNlbGYubm9kZS5wb3NpdGlvbiwgdmVsb2NpdHkgPSBzZWxmLm5vZGUudmVsb2NpdHksIHNjYWxlID0gNSwgc3ByZWFkID0gMC4xLCBjaHVua190eXBlID0gInN3ZWF0IikKICAgIAogICAgZGVmIGRpZShzZWxmKSAtPiBOb25lOgogICAgICAgIHNlbGYubGlnaHQuZGVsZXRlKCkKICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZShiYS5EaWVNZXNzYWdlKCkpCiAgICAKICAgIEBjbGFzc21ldGhvZAogICAgZGVmIGdldF9mYWN0b3J5KGNscyk6CiAgICAgICAgYWN0aXZpdHkgPSBiYS5nZXRhY3Rpdml0eSgpCiAgICAgICAgaWYgYWN0aXZpdHkgaXMgTm9uZTogcmFpc2UgRXhjZXB0aW9uKCJubyBjdXJyZW50IGFjdGl2aXR5IGZvdW5kISIpCiAgICAgICAgdHJ5OiByZXR1cm4gYWN0aXZpdHkuX2ZpcmVCYWxsRmFjdG9yeQogICAgICAgIGV4Y2VwdDoKICAgICAgICAgICAgZiA9IGFjdGl2aXR5Ll9maXJlQmFsbEZhY3RvcnkgPSBGaXJlQmFsbEZhY3RvcnkoKQogICAgICAgICAgICByZXR1cm4gZgoKICAgIGRlZiBoYW5kbGVtZXNzYWdlKHNlbGYsIG1zZyk6CiAgICAgICAgc3VwZXIoc2VsZi5fX2NsYXNzX18sIHNlbGYpLmhhbmRsZW1lc3NhZ2UobXNnKQogICAgICAgIGlmIGlzaW5zdGFuY2UobXNnLCBUb3VjaGVkT2JqZWN0KToKICAgICAgICAgICAgbm9kZSA9IGJhLmdldGNvbGxpc2lvbigpLm9wcG9zaW5nbm9kZQogICAgICAgICAgICBpZiBub2RlIGFuZCBub2RlLmV4aXN0cygpOgogICAgICAgICAgICAgICAgdiA9IHNlbGYubm9kZS52ZWxvY2l0eQogICAgICAgICAgICAgICAgbSA9IGJhLlZlYzMoKnYpLmxlbmd0aCgpICogNDAKICAgICAgICAgICAgICAgIG5vZGUuaGFuZGxlbWVzc2FnZSgKICAgICAgICAgICAgICAgICAgICBiYS5IaXRNZXNzYWdlKHBvcyA9IHNlbGYubm9kZS5wb3NpdGlvbiwKICAgICAgICAgICAgICAgICAgICB2ZWxvY2l0eSA9IHYsCiAgICAgICAgICAgICAgICAgICAgbWFnbml0dWRlID0gbSwKICAgICAgICAgICAgICAgICAgICB2ZWxvY2l0eV9tYWduaXR1ZGUgPSBtLAogICAgICAgICAgICAgICAgICAgIHJhZGl1cyA9IDAsCiAgICAgICAgICAgICAgICAgICAgc3Jjbm9kZSA9IHNlbGYubm9kZSwKICAgICAgICAgICAgICAgICAgICBzb3VyY2VfcGxheWVyID0gc2VsZi5zb3VyY2VfcGxheWVyLAogICAgICAgICAgICAgICAgICAgIGZvcmNlX2RpcmVjdGlvbiA9IHNlbGYubm9kZS52ZWxvY2l0eSkKICAgICAgICAgICAgICAgICAgICApCiAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKGJhLkRpZU1lc3NhZ2UoKSkKICAgICAgICAKICAgICAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBiYS5EaWVNZXNzYWdlKToKICAgICAgICAgICAgCiAgICAgICAgICAgIGlmIHNlbGYubm9kZS5leGlzdHMoKToKICAgICAgICAgICAgICAgIHZlbG9jaXR5ID0gc2VsZi5ub2RlLnZlbG9jaXR5CiAgICAgICAgICAgICAgICBleHBsb3Npb24gPSBiYS5uZXdub2RlKCJleHBsb3Npb24iLAogICAgICAgICAgICAgICAgYXR0cnMgPSB7J3Bvc2l0aW9uJzogc2VsZi5ub2RlLnBvc2l0aW9uLAogICAgICAgICAgICAgICAgJ3ZlbG9jaXR5JzogKHZlbG9jaXR5WzBdLCBtYXgoLTEuMCwgdmVsb2NpdHlbMV0pLCB2ZWxvY2l0eVsyXSksCiAgICAgICAgICAgICAgICAncmFkaXVzJzogMiwKICAgICAgICAgICAgICAgICdiaWcnOiBGYWxzZQogICAgICAgICAgICAgICAgfSkKICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChzb3VuZCA9IGJhLmdldHNvdW5kKHJhbmRvbS5jaG9pY2UoWydpbXBhY3RIYXJkJywgJ2ltcGFjdEhhcmQyJywgJ2ltcGFjdEhhcmQzJ10pKSwgcG9zaXRpb24gPSBzZWxmLm5vZGUucG9zaXRpb24pCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUuZGVsZXRlKCkKICAgICAgICAgICAgICAgIHNlbGYuZW1pdF90aW1lciA9IE5vbmUKICAgICAgICAKICAgICAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBiYS5PdXRPZkJvdW5kc01lc3NhZ2UpOgogICAgICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZShiYS5EaWVNZXNzYWdlKCkpCiAgICAgICAgCiAgICAgICAgZWxpZiBpc2luc3RhbmNlKG1zZywgYmEuSGl0TWVzc2FnZSk6CiAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKCJpbXB1bHNlIiwKICAgICAgICAgICAgbXNnLnBvc1swXSwgbXNnLnBvc1sxXSwgbXNnLnBvc1syXSwKICAgICAgICAgICAgbXNnLnZlbG9jaXR5WzBdLCBtc2cudmVsb2NpdHlbMV0sIG1zZy52ZWxvY2l0eVsyXSwKICAgICAgICAgICAgMS4wKm1zZy5tYWduaXR1ZGUsIDEuMCptc2cudmVsb2NpdHlfbWFnbml0dWRlLCBtc2cucmFkaXVzLCAwLAogICAgICAgICAgICBtc2cuZm9yY2VfZGlyZWN0aW9uWzBdLCBtc2cuZm9yY2VfZGlyZWN0aW9uWzFdLCBtc2cuZm9yY2VfZGlyZWN0aW9uWzJdCiAgICAgICAgICAgICkKCiAgICAgICAgZWxpZiBpc2luc3RhbmNlKG1zZywgVG91Y2hlZFNwYXopOgogICAgICAgICAgICBub2RlID0gYmEuZ2V0Y29sbGlzaW9uKCkub3Bwb3Npbmdub2RlCiAgICAgICAgICAgIGlmIG5vZGUgYW5kIG5vZGUuZXhpc3RzKCk6CiAgICAgICAgICAgICAgICBub2RlLmhhbmRsZW1lc3NhZ2UoCiAgICAgICAgICAgICAgICAgICAgYmEuSGl0TWVzc2FnZShwb3MgPSBzZWxmLm5vZGUucG9zaXRpb24sCiAgICAgICAgICAgICAgICAgICAgdmVsb2NpdHkgPSAoMTAsIDEwLCAxMCksCiAgICAgICAgICAgICAgICAgICAgbWFnbml0dWRlID0gNTAsCiAgICAgICAgICAgICAgICAgICAgdmVsb2NpdHlfbWFnbml0dWRlID0gNTAsCiAgICAgICAgICAgICAgICAgICAgcmFkaXVzID0gMCwKICAgICAgICAgICAgICAgICAgICBzcmNub2RlID0gc2VsZi5ub2RlLAogICAgICAgICAgICAgICAgICAgIHNvdXJjZV9wbGF5ZXIgPSBzZWxmLnNvdXJjZV9wbGF5ZXIsCiAgICAgICAgICAgICAgICAgICAgZm9yY2VfZGlyZWN0aW9uID0gc2VsZi5ub2RlLnZlbG9jaXR5KQogICAgICAgICAgICAgICAgICAgICkKICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoYmEuRGllTWVzc2FnZSgpKQogICAgICAgIAogICAgICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIFRvdWNoZWRGb290aW5nTWF0ZXJpYWwpOgogICAgICAgICAgICBiYS5wbGF5c291bmQoc291bmQgPSBiYS5nZXRzb3VuZCgiYmxpcCIpLCBwb3NpdGlvbiA9IHNlbGYubm9kZS5wb3NpdGlvbik=")) + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class FireBallGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'FireBall Fight' + description = 'Kill a set number of enemies with fire balls to win.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session]) -> list[ba.Setting]: + settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ba.BoolSetting("Equip Gloves", default = False), + ba.BoolSetting("NightMode", default = True) + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, ba.FreeForAllSession): + settings.append( + ba.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._night = bool(settings["NightMode"]) + self._gloves = bool(settings["Equip Gloves"]) + 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 = (ba.MusicType.EPIC if self._epic_mode else + ba.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() + if self._night: ba.getactivity().globalsnode.tint = (0.3, 0.3, 0.3) + 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 spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player) + FireBallFactory().grant_fire_ball(spaz) + spaz.connect_controls_to_player(enable_punch=True, + enable_bomb=False, + enable_pickup=True) + if self._gloves: spaz.equip_boxing_gloves() + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.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, ba.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: + ba.playsound(self._dingsound) + 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 + ba.playsound(self._dingsound) + + # 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): + ba.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 = ba.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/FlagDay.py b/dist/ba_root/mods/games/FlagDay.py index 8195966..48ed26c 100644 --- a/dist/ba_root/mods/games/FlagDay.py +++ b/dist/ba_root/mods/games/FlagDay.py @@ -1,614 +1,610 @@ - -#Ported by brostos to api 8 -# 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 json -import math -import random -from bascenev1lib.game.elimination import Icon -from bascenev1lib.actor.bomb import Bomb, Blast -from bascenev1lib.actor.playerspaz import PlayerSpaz -from bascenev1lib.actor.scoreboard import Scoreboard -from bascenev1lib.actor.powerupbox import PowerupBox -from bascenev1lib.actor.flag import Flag, FlagPickedUpMessage -from bascenev1lib.actor.spazbot import SpazBotSet, BrawlerBotLite, SpazBotDiedMessage - -if TYPE_CHECKING: - from typing import Any, Sequence - - -lang = bs.app.lang.language -if lang == 'Spanish': - name = 'Día de la Bandera' - description = ('Recoge las banderas para recibir un premio.\n' - 'Pero ten cuidado...') - slow_motion_deaths = 'Muertes en Cámara Lenta' - credits = 'Creado por MattZ45986 en Github | Actualizado por byANG3L' - you_were = 'Estas' - cursed_text = 'MALDITO' - run = 'CORRE' - climb_top = 'Escala a la cima' - bomb_rain = '¡LLUVIA DE BOMBAS!' - lame_guys = 'Chicos Ligeros' - jackpot = '¡PREMIO MAYOR!' - diedtxt = '¡' - diedtxt2 = ' ha sido eliminado!' -else: - name = 'Flag Day' - description = 'Pick up flags to receive a prize.\nBut beware...' - slow_motion_deaths = 'Slow Motion Deaths' - credits = 'Created by MattZ45986 on Github | Updated by byANG3L' - you_were = 'You were' - cursed_text = 'CURSED' - run = 'RUN' - climb_top = 'Climb to the top' - bomb_rain = 'BOMB RAIN!' - lame_guys = 'Lame Guys' - jackpot = '!JACKPOT!' - diedtxt = '' - diedtxt2 = ' died!' - - -class Icon(Icon): - - 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, - dead: bool = False, - ): - super().__init__(player,position,scale,show_lives,show_death, - name_scale,name_maxwidth,flatness,shadow) - if dead: - self._name_text.opacity = 0.2 - self.node.color = (0.7, 0.3, 0.3) - self.node.opacity = 0.2 - - -class FlagBearer(PlayerSpaz): - def handlemessage(self, msg: Any) -> Any: - super().handlemessage(msg) - if isinstance(msg, bs.PowerupMessage): - activity = self.activity - player = self.getplayer(Player) - if not player.is_alive(): - return - if activity.last_prize == 'curse': - player.team.score += 25 - activity._update_scoreboard() - elif activity.last_prize == 'land_mines': - player.team.score += 15 - activity._update_scoreboard() - self.connect_controls_to_player() - elif activity.last_prize == 'climb': - player.team.score += 50 - activity._update_scoreboard() - if msg.poweruptype == 'health': - activity.round_timer = None - bs.timer(0.2, activity.setup_next_round) - - -class Player(bs.Player['Team']): - """Our player type for this game.""" - - def __init__(self) -> None: - self.dead = False - self.icons: list[Icon] = [] - -class Team(bs.Team[Player]): - """Our team type for this game.""" - - def __init__(self) -> None: - self.score = 0 - - -# ba_meta export bascenev1.GameActivity -class FlagDayGame(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.BoolSetting(slow_motion_deaths, default=True), - 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.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) - self.credits() - self._scoreboard = Scoreboard() - self._dingsound = bui.getsound('dingSmall') - self._epic_mode = bool(settings['Epic Mode']) - self._slow_motion_deaths = bool(settings[slow_motion_deaths]) - self.current_player: Player | None = None - self.prize_recipient: Player | None = None - self.bomb_survivor: Player | None = None - self.bad_guy_cost: int = 0 - self.player_index: int = 0 - self.bombs: list = [] - self.queue_line: list = [] - self._bots: SpazBotSet | None = None - self.light: bs.Node | None = None - self.last_prize = 'none' - self._flag: Flag | None = None - self._flag2: Flag | None = None - self._flag3: Flag | None = None - self._flag4: Flag | None = None - self._flag5: Flag | None = None - self._flag6: Flag | None = None - self._flag7: Flag | None = None - self._flag8: Flag | None = None - self.set = False - self.round_timer: bs.Timer | None = None - self.give_points_timer: bs.Timer | None = None - - self._jackpot_sound = bui.getsound('achievement') - self._round_sound = bui.getsound('powerup01') - self._dingsound = bui.getsound('dingSmall') - - # 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 on_team_join(self, team: Team) -> None: - if self.has_begun(): - self._update_scoreboard() - - def on_player_leave(self, player: Player) -> None: - if player is self.current_player: - self.setup_next_round() - self._check_end_game() - super().on_player_leave(player) - self.queue_line.remove(player) - self._update_icons() - - def on_begin(self) -> None: - super().on_begin() - for player in self.players: - if player.actor: - player.actor.handlemessage(bs.DieMessage()) - player.actor.node.delete() - self.queue_line.append(player) - self.spawn_player_spaz( - self.queue_line[self.player_index % len(self.queue_line)], - (0.0, 3.0, -2.0)) - self.current_player = self.queue_line[0] - # Declare a set of bots (enemies) that we will use later - self._bots = SpazBotSet() - self.reset_flags() - self._update_icons() - self._update_scoreboard() - - def credits(self) -> None: - bs.newnode( - 'text', - attrs={ - 'v_attach': 'bottom', - 'h_align': 'center', - 'vr_depth': 0, - 'color': (0, 0.2, 0), - 'shadow': 1.0, - 'flatness': 1.0, - 'position': (0,0), - 'scale': 0.8, - 'text': credits - }) - - def _update_icons(self) -> None: - # pylint: disable=too-many-branches - for player in self.queue_line: - player.icons = [] - if player == self.current_player: - xval = 0 - x_offs = -78 - player.icons.append( - Icon(player, - position=(xval, 65), - scale=1.0, - name_maxwidth=130, - name_scale=0.8, - flatness=0.0, - shadow=0.5, - show_death=True, - show_lives=False)) - elif player.dead: - xval = 65 - x_offs = 78 - player.icons.append( - Icon(player, - position=(xval, 50), - scale=0.5, - name_maxwidth=75, - name_scale=1.0, - flatness=1.0, - shadow=1.0, - show_death=False, - show_lives=False, - dead=True)) - xval += x_offs * 0.56 - else: - xval = -65 - x_offs = 78 - player.icons.append( - Icon(player, - position=(xval, 50), - scale=0.5, - name_maxwidth=75, - name_scale=1.0, - flatness=1.0, - shadow=1.0, - show_death=False, - show_lives=False)) - xval -= x_offs * 0.56 - - def give_prize(self, prize: int) -> None: - if prize == 1: - # Curse him aka make him blow up in 5 seconds - # give them a nice message - bs.broadcastmessage(you_were, color=(0.1, 0.1, 0.1)) - bs.broadcastmessage(cursed_text, color=(1.0, 0.0, 0.0)) - self.make_health_box((0.0, 0.0, 0.0)) - self.last_prize = 'curse' - self.prize_recipient.actor.curse() - # bs.timer(5.5, self.setup_next_round) - if prize == 2: - self.setup_rof() - bs.broadcastmessage(run, color=(1.0, 0.2, 0.1)) - self.last_prize = 'ring_of_fire' - if prize == 3: - self.last_prize = 'climb' - self.light = bs.newnode( - 'locator', - attrs={ - 'shape': 'circle', - 'position': (0.0, 3.0, -9.0), - 'color': (1.0, 1.0, 1.0), - 'opacity': 1.0, - 'draw_beauty': True, - 'additive': True - }) - bs.broadcastmessage(climb_top, color=(0.5, 0.5, 0.5)) - bs.timer(3.0, babase.Call(self.make_health_box, (0.0, 6.0, -9.0))) - self.round_timer = bs.Timer(10.0, self.setup_next_round) - if prize == 4: - self.last_prize = 'land_mines' - self.make_health_box((6.0, 5.0, -2.0)) - self.make_land_mines() - self.prize_recipient.actor.connect_controls_to_player( - enable_bomb=False) - self.prize_recipient.actor.node.handlemessage( - bs.StandMessage(position=(-6.0, 3.0, -2.0))) - self.round_timer = bs.Timer(7.0, self.setup_next_round) - if prize == 5: - # Make it rain bombs - self.bomb_survivor = self.prize_recipient - bs.broadcastmessage(bomb_rain, color=(1.0, 0.5, 0.16)) - # Set positions for the bombs to drop - for bzz in range(-5,6): - for azz in range(-5,2): - # for each position make a bomb drop there - self.make_bomb(bzz, azz) - self.give_points_timer = bs.Timer(3.3, self.give_points) - self.last_prize = 'bombrain' - if prize == 6: - self.setup_br() - self.bomb_survivor = self.prize_recipient - self.give_points_timer = bs.Timer(7.0, self.give_points) - self.last_prize = 'bombroad' - if prize == 7: - # makes killing a bad guy worth ten points - self.bad_guy_cost = 2 - bs.broadcastmessage(lame_guys, color=(1.0, 0.5, 0.16)) - # makes a set of nine positions - for a in range(-1, 2): - for b in range(-3, 0): - # and spawns one in each position - self._bots.spawn_bot(BrawlerBotLite, pos=(a, 2.5, b)) - # and we give our player boxing gloves and a shield - self._player.equip_boxing_gloves() - self._player.equip_shields() - self.last_prize = 'lameguys' - if prize == 8: - self._jackpot_sound.play() - bs.broadcastmessage(jackpot, color=(1.0, 0.0, 0.0)) - bs.broadcastmessage(jackpot, color=(0.0, 1.0, 0.0)) - bs.broadcastmessage(jackpot, color=(0.0, 0.0, 1.0)) - team = self.prize_recipient.team - # GIVE THEM A WHOPPING 50 POINTS!!! - team.score += 50 - # and update the scores - self._update_scoreboard() - self.last_prize = 'jackpot' - bs.timer(2.0, self.setup_next_round) - - def setup_next_round(self) -> None: - if self._slow_motion_deaths: - bs.getactivity().globalsnode.slow_motion = False - if self.set: - return - if self.light: - self.light.delete() - for bomb in self.bombs: - bomb.handlemessage(bs.DieMessage()) - self.kill_flags() - self._bots.clear() - self.reset_flags() - self.current_player.actor.handlemessage( - bs.DieMessage(how='game')) - self.current_player.actor.node.delete() - c = 0 - self.player_index += 1 - self.player_index %= len(self.queue_line) - if len(self.queue_line) > 0: - while self.queue_line[self.player_index].dead: - if c > len(self.queue_line): - return - self.player_index += 1 - self.player_index %= len(self.queue_line) - c += 1 - self.spawn_player_spaz( - self.queue_line[self.player_index], (0.0, 3.0, -2.0)) - self.current_player = self.queue_line[self.player_index] - self.last_prize = 'none' - self._update_icons() - - def check_bots(self) -> None: - if not self._bots.have_living_bots(): - self.setup_next_round() - - def make_land_mines(self) -> None: - self.bombs = [] - for i in range(-11, 7): - self.bombs.append(Bomb( - position=(0.0, 6.0, i/2.0), - bomb_type='land_mine', - blast_radius=2.0)) - self.bombs[i+10].arm() - - def give_points(self) -> None: - if self.bomb_survivor is not None and self.bomb_survivor.is_alive(): - self.bomb_survivor.team.score += 20 - self._update_scoreboard() - self.round_timer = bs.Timer(1.0, self.setup_next_round) - - def make_health_box(self, position: Sequence[float]) -> None: - if position == (0.0, 3.0, 0.0): - position = (random.randint(-6, 6), 6, random.randint(-6, 4)) - elif position == (0,0,0): - position = random.choice( - ((-7, 6, -5), (7, 6, -5), (-7, 6, 1), (7, 6, 1))) - self.health_box = PowerupBox( - position=position, poweruptype='health').autoretain() - - # called in prize #5 - def make_bomb(self, xpos: float, zpos: float) -> None: - # makes a bomb at the given position then auto-retains it aka: - # makes sure it doesn't disappear because there is no reference to it - self.bombs.append(Bomb(position=(xpos, 12, zpos))) - - def setup_br(self) -> None: - self.make_bomb_row(6) - self.prize_recipient.actor.handlemessage( - bs.StandMessage(position=(6.0, 3.0, -2.0))) - - def make_bomb_row(self, num: int) -> None: - if not self.prize_recipient.is_alive(): - return - if num == 0: - self.round_timer = bs.Timer(1.0, self.setup_next_round) - return - for i in range(-11, 7): - self.bombs.append( - Bomb(position=(-3, 3, i/2.0), - velocity=(12, 0.0, 0.0), - bomb_type='normal', - blast_radius=1.2)) - bs.timer(1.0, babase.Call(self.make_bomb_row, num-1)) - - def setup_rof(self) -> None: - self.make_blast_ring(10) - self.prize_recipient.actor.handlemessage( - bs.StandMessage(position=(0.0, 3.0, -2.0))) - - def make_blast_ring(self, length: float) -> None: - if not self.prize_recipient.is_alive(): - return - if length == 0: - self.setup_next_round() - self.prize_recipient.team.score += 50 - self._update_scoreboard() - return - for angle in range(0, 360, 45): - angle += random.randint(0, 45) - angle %= 360 - x = length * math.cos(math.radians(angle)) - z = length * math.sin(math.radians(angle)) - blast = Blast(position=(x, 2.2, z-2), blast_radius=3.5) - bs.timer(0.75, babase.Call(self.make_blast_ring, length-1)) - - # a method to remake the flags - def reset_flags(self) -> None: - # remake the flags - self._flag = Flag( - position=(0.0, 3.0, 1.0), touchable=True, color=(0.0, 0.0, 1.0)) - self._flag2 = Flag( - position=(0.0, 3.0, -5.0), touchable=True, color=(1.0, 0.0, 0.0)) - self._flag3 = Flag( - position=(3.0, 3.0, -2.0), touchable=True, color=(0.0, 1.0, 0.0)) - self._flag4 = Flag( - position=(-3.0, 3.0, -2.0), touchable=True, color=(1.0, 1.0, 1.0)) - self._flag5 = Flag( - position=(1.8, 3.0, 0.2), touchable=True, color=(0.0, 1.0, 1.0)) - self._flag6 = Flag( - position=(-1.8, 3.0, 0.2), touchable=True, color=(1.0, 0.0, 1.0)) - self._flag7 = Flag( - position=(1.8, 3.0, -3.8), touchable=True, color=(1.0, 1.0, 0.0)) - self._flag8 = Flag( - position=(-1.8, 3.0, -3.8), touchable=True, color=(0.0, 0.0, 0.0)) - - # a method to kill the flags - def kill_flags(self) -> None: - # destroy all the flags by erasing all references to them, - # indicated by None similar to null - self._flag.node.delete() - self._flag2.node.delete() - self._flag3.node.delete() - self._flag4.node.delete() - self._flag5.node.delete() # 132, 210 ,12 - self._flag6.node.delete() - self._flag7.node.delete() - self._flag8.node.delete() - - def _check_end_game(self) -> None: - for player in self.queue_line: - if not player.dead: - return - self.end_game() - - def spawn_player_spaz( - self, - player: PlayerT, - 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 - - 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 = FlagBearer(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) - return spaz - - def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, bs.PlayerDiedMessage): - # give them a nice farewell - if bs.time() < 0.5: - return - if msg.how == 'game': - return - player = msg.getplayer(Player) - bs.broadcastmessage( - diedtxt + str(player.getname()) + diedtxt2, color=player.color) - player.dead = True - if player is self.current_player: - self.round_timer = None - self.give_points_timer = None - if not msg.how is bs.DeathType.FALL: - if self._slow_motion_deaths: - bs.getactivity().globalsnode.slow_motion = True - time = 0.5 - else: - time = 0.01 - # check to see if we can end the game - self._check_end_game() - bs.timer(time, self.setup_next_round) - elif isinstance(msg, FlagPickedUpMessage): - msg.flag.last_player_to_hold = msg.node.getdelegate( - FlagBearer, True - ).getplayer(Player, True) - self._player = msg.node.getdelegate( - FlagBearer, True - ) - self.prize_recipient = msg.node.getdelegate( - FlagBearer, True - ).getplayer(Player, True) - self.kill_flags() - self.give_prize(random.randint(1, 8)) - self._round_sound.play() - self.current_player = self.prize_recipient - elif isinstance(msg, SpazBotDiedMessage): - # find out which team the last person to hold a flag was on - team = self.prize_recipient.team - # give them their points - team.score += self.bad_guy_cost - self._dingsound.play(0.5) - # update the scores - for team in self.teams: - self._scoreboard.set_team_value(team, team.score) - bs.timer(0.3, self.check_bots) - return None - - def _update_scoreboard(self) -> None: - for player in self.queue_line: - if not player.dead: - if player.team.score > 0: - self._dingsound.play() - self._scoreboard.set_team_value(player.team, player.team.score) - - def end_game(self) -> None: - if self.set: - return - self.set = True - results = bs.GameResults() - for team in self.teams: - results.set_team_score(team, team.score) - self.end(results=results) +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +import json +import math +import random +from bastd.game.elimination import Icon +from bastd.actor.bomb import Bomb, Blast +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBox +from bastd.actor.flag import Flag, FlagPickedUpMessage +from bastd.actor.spazbot import SpazBotSet, BrawlerBotLite, SpazBotDiedMessage + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = ba.app.lang.language +if lang == 'Spanish': + name = 'Día de la Bandera' + description = ('Recoge las banderas para recibir un premio.\n' + 'Pero ten cuidado...') + slow_motion_deaths = 'Muertes en Cámara Lenta' + credits = 'Creado por MattZ45986 en Github | Actualizado por byANG3L' + you_were = 'Estas' + cursed_text = 'MALDITO' + run = 'CORRE' + climb_top = 'Escala a la cima' + bomb_rain = '¡LLUVIA DE BOMBAS!' + lame_guys = 'Chicos Ligeros' + jackpot = '¡PREMIO MAYOR!' + diedtxt = '¡' + diedtxt2 = ' ha sido eliminado!' +else: + name = 'Flag Day' + description = 'Pick up flags to receive a prize.\nBut beware...' + slow_motion_deaths = 'Slow Motion Deaths' + credits = 'Created by MattZ45986 on Github | Updated by byANG3L' + you_were = 'You were' + cursed_text = 'CURSED' + run = 'RUN' + climb_top = 'Climb to the top' + bomb_rain = 'BOMB RAIN!' + lame_guys = 'Lame Guys' + jackpot = '!JACKPOT!' + diedtxt = '' + diedtxt2 = ' died!' + + +class Icon(Icon): + + 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, + dead: bool = False, + ): + super().__init__(player,position,scale,show_lives,show_death, + name_scale,name_maxwidth,flatness,shadow) + if dead: + self._name_text.opacity = 0.2 + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + +class FlagBearer(PlayerSpaz): + def handlemessage(self, msg: Any) -> Any: + super().handlemessage(msg) + if isinstance(msg, ba.PowerupMessage): + activity = self.activity + player = self.getplayer(Player) + if not player.is_alive(): + return + if activity.last_prize == 'curse': + player.team.score += 25 + activity._update_scoreboard() + elif activity.last_prize == 'land_mines': + player.team.score += 15 + activity._update_scoreboard() + self.connect_controls_to_player() + elif activity.last_prize == 'climb': + player.team.score += 50 + activity._update_scoreboard() + if msg.poweruptype == 'health': + activity.round_timer = None + ba.timer(0.2, activity.setup_next_round) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.dead = False + self.icons: list[Icon] = [] + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class FlagDayGame(ba.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[ba.Session] + ) -> list[ba.Setting]: + settings = [ + ba.BoolSetting(slow_motion_deaths, default=True), + ba.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return ( + issubclass(sessiontype, ba.CoopSession) + or issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession) + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ['Courtyard'] + + def __init__(self, settings: dict): + super().__init__(settings) + self.credits() + self._scoreboard = Scoreboard() + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._slow_motion_deaths = bool(settings[slow_motion_deaths]) + self.current_player: Player | None = None + self.prize_recipient: Player | None = None + self.bomb_survivor: Player | None = None + self.bad_guy_cost: int = 0 + self.player_index: int = 0 + self.bombs: list = [] + self.queue_line: list = [] + self._bots: SpazBotSet | None = None + self.light: ba.Node | None = None + self.last_prize = 'none' + self._flag: Flag | None = None + self._flag2: Flag | None = None + self._flag3: Flag | None = None + self._flag4: Flag | None = None + self._flag5: Flag | None = None + self._flag6: Flag | None = None + self._flag7: Flag | None = None + self._flag8: Flag | None = None + self.set = False + self.round_timer: ba.Timer | None = None + self.give_points_timer: ba.Timer | None = None + + self._jackpot_sound = ba.getsound('achievement') + self._round_sound = ba.getsound('powerup01') + self._dingsound = ba.getsound('dingSmall') + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + ba.MusicType.EPIC if self._epic_mode else ba.MusicType.TO_THE_DEATH + ) + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + if player is self.current_player: + self.setup_next_round() + self._check_end_game() + super().on_player_leave(player) + self.queue_line.remove(player) + self._update_icons() + + def on_begin(self) -> None: + super().on_begin() + for player in self.players: + if player.actor: + player.actor.handlemessage(ba.DieMessage()) + player.actor.node.delete() + self.queue_line.append(player) + self.spawn_player_spaz( + self.queue_line[self.player_index % len(self.queue_line)], + (0.0, 3.0, -2.0)) + self.current_player = self.queue_line[0] + # Declare a set of bots (enemies) that we will use later + self._bots = SpazBotSet() + self.reset_flags() + self._update_icons() + self._update_scoreboard() + + def credits(self) -> None: + ba.newnode( + 'text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'vr_depth': 0, + 'color': (0, 0.2, 0), + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0,0), + 'scale': 0.8, + 'text': credits + }) + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + for player in self.queue_line: + player.icons = [] + if player == self.current_player: + xval = 0 + x_offs = -78 + player.icons.append( + Icon(player, + position=(xval, 65), + scale=1.0, + name_maxwidth=130, + name_scale=0.8, + flatness=0.0, + shadow=0.5, + show_death=True, + show_lives=False)) + elif player.dead: + xval = 65 + x_offs = 78 + player.icons.append( + Icon(player, + position=(xval, 50), + scale=0.5, + name_maxwidth=75, + name_scale=1.0, + flatness=1.0, + shadow=1.0, + show_death=False, + show_lives=False, + dead=True)) + xval += x_offs * 0.56 + else: + xval = -65 + x_offs = 78 + player.icons.append( + Icon(player, + position=(xval, 50), + scale=0.5, + name_maxwidth=75, + name_scale=1.0, + flatness=1.0, + shadow=1.0, + show_death=False, + show_lives=False)) + xval -= x_offs * 0.56 + + def give_prize(self, prize: int) -> None: + if prize == 1: + # Curse him aka make him blow up in 5 seconds + # give them a nice message + ba.screenmessage(you_were, color=(0.1, 0.1, 0.1)) + ba.screenmessage(cursed_text, color=(1.0, 0.0, 0.0)) + self.make_health_box((0.0, 0.0, 0.0)) + self.last_prize = 'curse' + self.prize_recipient.actor.curse() + # ba.timer(5.5, self.setup_next_round) + if prize == 2: + self.setup_rof() + ba.screenmessage(run, color=(1.0, 0.2, 0.1)) + self.last_prize = 'ring_of_fire' + if prize == 3: + self.last_prize = 'climb' + self.light = ba.newnode( + 'locator', + attrs={ + 'shape': 'circle', + 'position': (0.0, 3.0, -9.0), + 'color': (1.0, 1.0, 1.0), + 'opacity': 1.0, + 'draw_beauty': True, + 'additive': True + }) + ba.screenmessage(climb_top, color=(0.5, 0.5, 0.5)) + ba.timer(3.0, ba.Call(self.make_health_box, (0.0, 6.0, -9.0))) + self.round_timer = ba.Timer(10.0, self.setup_next_round) + if prize == 4: + self.last_prize = 'land_mines' + self.make_health_box((6.0, 5.0, -2.0)) + self.make_land_mines() + self.prize_recipient.actor.connect_controls_to_player( + enable_bomb=False) + self.prize_recipient.actor.node.handlemessage( + ba.StandMessage(position=(-6.0, 3.0, -2.0))) + self.round_timer = ba.Timer(7.0, self.setup_next_round) + if prize == 5: + # Make it rain bombs + self.bomb_survivor = self.prize_recipient + ba.screenmessage(bomb_rain, color=(1.0, 0.5, 0.16)) + # Set positions for the bombs to drop + for bzz in range(-5,6): + for azz in range(-5,2): + # for each position make a bomb drop there + self.make_bomb(bzz, azz) + self.give_points_timer = ba.Timer(3.3, self.give_points) + self.last_prize = 'bombrain' + if prize == 6: + self.setup_br() + self.bomb_survivor = self.prize_recipient + self.give_points_timer = ba.Timer(7.0, self.give_points) + self.last_prize = 'bombroad' + if prize == 7: + # makes killing a bad guy worth ten points + self.bad_guy_cost = 2 + ba.screenmessage(lame_guys, color=(1.0, 0.5, 0.16)) + # makes a set of nine positions + for a in range(-1, 2): + for b in range(-3, 0): + # and spawns one in each position + self._bots.spawn_bot(BrawlerBotLite, pos=(a, 2.5, b)) + # and we give our player boxing gloves and a shield + self._player.equip_boxing_gloves() + self._player.equip_shields() + self.last_prize = 'lameguys' + if prize == 8: + ba.playsound(self._jackpot_sound) + ba.screenmessage(jackpot, color=(1.0, 0.0, 0.0)) + ba.screenmessage(jackpot, color=(0.0, 1.0, 0.0)) + ba.screenmessage(jackpot, color=(0.0, 0.0, 1.0)) + team = self.prize_recipient.team + # GIVE THEM A WHOPPING 50 POINTS!!! + team.score += 50 + # and update the scores + self._update_scoreboard() + self.last_prize = 'jackpot' + ba.timer(2.0, self.setup_next_round) + + def setup_next_round(self) -> None: + if self._slow_motion_deaths: + ba.getactivity().globalsnode.slow_motion = False + if self.set: + return + if self.light: + self.light.delete() + for bomb in self.bombs: + bomb.handlemessage(ba.DieMessage()) + self.kill_flags() + self._bots.clear() + self.reset_flags() + self.current_player.actor.handlemessage( + ba.DieMessage(how='game')) + self.current_player.actor.node.delete() + c = 0 + self.player_index += 1 + self.player_index %= len(self.queue_line) + if len(self.queue_line) > 0: + while self.queue_line[self.player_index].dead: + if c > len(self.queue_line): + return + self.player_index += 1 + self.player_index %= len(self.queue_line) + c += 1 + self.spawn_player_spaz( + self.queue_line[self.player_index], (0.0, 3.0, -2.0)) + self.current_player = self.queue_line[self.player_index] + self.last_prize = 'none' + self._update_icons() + + def check_bots(self) -> None: + if not self._bots.have_living_bots(): + self.setup_next_round() + + def make_land_mines(self) -> None: + self.bombs = [] + for i in range(-11, 7): + self.bombs.append(Bomb( + position=(0.0, 6.0, i/2.0), + bomb_type='land_mine', + blast_radius=2.0)) + self.bombs[i+10].arm() + + def give_points(self) -> None: + if self.bomb_survivor is not None and self.bomb_survivor.is_alive(): + self.bomb_survivor.team.score += 20 + self._update_scoreboard() + self.round_timer = ba.Timer(1.0, self.setup_next_round) + + def make_health_box(self, position: Sequence[float]) -> None: + if position == (0.0, 3.0, 0.0): + position = (random.randint(-6, 6), 6, random.randint(-6, 4)) + elif position == (0,0,0): + position = random.choice( + ((-7, 6, -5), (7, 6, -5), (-7, 6, 1), (7, 6, 1))) + self.health_box = PowerupBox( + position=position, poweruptype='health').autoretain() + + # called in prize #5 + def make_bomb(self, xpos: float, zpos: float) -> None: + # makes a bomb at the given position then auto-retains it aka: + # makes sure it doesn't disappear because there is no reference to it + self.bombs.append(Bomb(position=(xpos, 12, zpos))) + + def setup_br(self) -> None: + self.make_bomb_row(6) + self.prize_recipient.actor.handlemessage( + ba.StandMessage(position=(6.0, 3.0, -2.0))) + + def make_bomb_row(self, num: int) -> None: + if not self.prize_recipient.is_alive(): + return + if num == 0: + self.round_timer = ba.Timer(1.0, self.setup_next_round) + return + for i in range(-11, 7): + self.bombs.append( + Bomb(position=(-3, 3, i/2.0), + velocity=(12, 0.0, 0.0), + bomb_type='normal', + blast_radius=1.2)) + ba.timer(1.0, ba.Call(self.make_bomb_row, num-1)) + + def setup_rof(self) -> None: + self.make_blast_ring(10) + self.prize_recipient.actor.handlemessage( + ba.StandMessage(position=(0.0, 3.0, -2.0))) + + def make_blast_ring(self, length: float) -> None: + if not self.prize_recipient.is_alive(): + return + if length == 0: + self.setup_next_round() + self.prize_recipient.team.score += 50 + self._update_scoreboard() + return + for angle in range(0, 360, 45): + angle += random.randint(0, 45) + angle %= 360 + x = length * math.cos(math.radians(angle)) + z = length * math.sin(math.radians(angle)) + blast = Blast(position=(x, 2.2, z-2), blast_radius=3.5) + ba.timer(0.75, ba.Call(self.make_blast_ring, length-1)) + + # a method to remake the flags + def reset_flags(self) -> None: + # remake the flags + self._flag = Flag( + position=(0.0, 3.0, 1.0), touchable=True, color=(0.0, 0.0, 1.0)) + self._flag2 = Flag( + position=(0.0, 3.0, -5.0), touchable=True, color=(1.0, 0.0, 0.0)) + self._flag3 = Flag( + position=(3.0, 3.0, -2.0), touchable=True, color=(0.0, 1.0, 0.0)) + self._flag4 = Flag( + position=(-3.0, 3.0, -2.0), touchable=True, color=(1.0, 1.0, 1.0)) + self._flag5 = Flag( + position=(1.8, 3.0, 0.2), touchable=True, color=(0.0, 1.0, 1.0)) + self._flag6 = Flag( + position=(-1.8, 3.0, 0.2), touchable=True, color=(1.0, 0.0, 1.0)) + self._flag7 = Flag( + position=(1.8, 3.0, -3.8), touchable=True, color=(1.0, 1.0, 0.0)) + self._flag8 = Flag( + position=(-1.8, 3.0, -3.8), touchable=True, color=(0.0, 0.0, 0.0)) + + # a method to kill the flags + def kill_flags(self) -> None: + # destroy all the flags by erasing all references to them, + # indicated by None similar to null + self._flag.node.delete() + self._flag2.node.delete() + self._flag3.node.delete() + self._flag4.node.delete() + self._flag5.node.delete() # 132, 210 ,12 + self._flag6.node.delete() + self._flag7.node.delete() + self._flag8.node.delete() + + def _check_end_game(self) -> None: + for player in self.queue_line: + if not player.dead: + return + self.end_game() + + def spawn_player_spaz( + self, + player: PlayerType, + position: Sequence[float] = (0, 0, 0), + angle: float | None = None, + ) -> PlayerSpaz: + from ba import _math + from ba._gameutils import animate + from ba._coopsession import CoopSession + + angle = None + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = ba.safecolor(color, target_intensity=0.75) + + spaz = FlagBearer(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( + ba.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + ba.playsound(self._spawn_sound, 1, position=spaz.node.position) + light = ba.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + ba.timer(0.5, light.delete) + return spaz + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # give them a nice farewell + if ba.time() < 0.5: + return + if msg.how == 'game': + return + player = msg.getplayer(Player) + ba.screenmessage( + diedtxt + str(player.getname()) + diedtxt2, color=player.color) + player.dead = True + if player is self.current_player: + self.round_timer = None + self.give_points_timer = None + if not msg.how is ba.DeathType.FALL: + if self._slow_motion_deaths: + ba.getactivity().globalsnode.slow_motion = True + time = 0.5 + else: + time = 0.01 + # check to see if we can end the game + self._check_end_game() + ba.timer(time, self.setup_next_round) + elif isinstance(msg, FlagPickedUpMessage): + msg.flag.last_player_to_hold = msg.node.getdelegate( + FlagBearer, True + ).getplayer(Player, True) + self._player = msg.node.getdelegate( + FlagBearer, True + ) + self.prize_recipient = msg.node.getdelegate( + FlagBearer, True + ).getplayer(Player, True) + self.kill_flags() + self.give_prize(random.randint(1, 8)) + ba.playsound(self._round_sound) + self.current_player = self.prize_recipient + elif isinstance(msg, SpazBotDiedMessage): + # find out which team the last person to hold a flag was on + team = self.prize_recipient.team + # give them their points + team.score += self.bad_guy_cost + ba.playsound(self._dingsound, 0.5) + # update the scores + for team in self.teams: + self._scoreboard.set_team_value(team, team.score) + ba.timer(0.3, self.check_bots) + return None + + def _update_scoreboard(self) -> None: + for player in self.queue_line: + if not player.dead: + if player.team.score > 0: + ba.playsound(self._dingsound) + self._scoreboard.set_team_value(player.team, player.team.score) + + def end_game(self) -> None: + if self.set: + return + self.set = True + results = ba.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/GetTheTarget.py b/dist/ba_root/mods/games/GetTheTarget.py index c5d61a4..689a9ea 100644 --- a/dist/ba_root/mods/games/GetTheTarget.py +++ b/dist/ba_root/mods/games/GetTheTarget.py @@ -1,5 +1,5 @@ # Made by Froshlee14 -# ba_meta require api 6 +# ba_meta require api 7 from __future__ import annotations @@ -221,7 +221,7 @@ class GetTheTargetGame(ba.TeamGameActivity[Player, Team]): 'maxwidth':100, 'h_align':'center', 'v_align':'center', 'shadow':1.0, 'flatness':1.0, 'color':(1,1,1), - 'scale':2,'position':(0,-100)})) + 'scale':1.2,'position':(0,-50)})) self._time_remaining -= 1 ba.playsound(self._tick_sound) diff --git a/dist/ba_root/mods/games/GravityFalls.py b/dist/ba_root/mods/games/GravityFalls.py index 8036e60..59d2563 100644 --- a/dist/ba_root/mods/games/GravityFalls.py +++ b/dist/ba_root/mods/games/GravityFalls.py @@ -7,7 +7,7 @@ from bastd.game.elimination import EliminationGame -# ba_meta require api 6 +# ba_meta require api 7 # ba_meta export game class GFGame(EliminationGame): name = 'Gravity Falls' diff --git a/dist/ba_root/mods/games/Heist.py b/dist/ba_root/mods/games/Heist.py index 7aa1ca3..580e846 100644 --- a/dist/ba_root/mods/games/Heist.py +++ b/dist/ba_root/mods/games/Heist.py @@ -1,4 +1,4 @@ -# ba_meta require api 6 +# ba_meta require api 7 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations diff --git a/dist/ba_root/mods/games/PowerUpShower.py b/dist/ba_root/mods/games/PowerUpShower.py new file mode 100644 index 0000000..6d92fd1 --- /dev/null +++ b/dist/ba_root/mods/games/PowerUpShower.py @@ -0,0 +1,388 @@ +# ba_meta require api 7 + + +#==============================================================================# +# +# version v1.0 (test, 2bugs) +# Create by Unknown_#7004 +# Github https://github.com/uwu-user +# +#==============================================================================# + + +from __future__ import annotations +from typing import TYPE_CHECKING, cast + +import ba +import _ba +import math +import random +from bastd.actor.bomb import Bomb +from bastd.actor.onscreentimer import OnScreenTimer +from bastd.actor.powerupbox import PowerupBox, PowerupBoxFactory + +if TYPE_CHECKING: + from typing import Any, Sequence, Callable, List, Dict, Tuple, Optional, Union + +#==============================================================================# + +_version = "1.0" +_by = "Unknown_#7004" + +#==============================================================================# + +# custom end time +custom_time = 10 # 10s + +# custom powerup drop time +custom_speed = 1.0 + +#==============================================================================# + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +#==============================================================================# + + +# ba_meta export game +class PowerUpShowerGame(ba.TeamGameActivity[Player, Team]): + """Minigame involving dodging falling bombs.""" + + name = 'PowerUp Shower' + description = 'Becareful with the curse powerups.' + scoreconfig = ba.ScoreConfig(label='Survived', + scoretype=ba.ScoreType.MILLISECONDS, + version='B') + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntChoiceSetting( + 'Time Limit', + choices=[ # memes + ('No timer', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('15 Minutes', 900), + ('30 Minutes', 1800), + ('60 Minutes', 3600), + ('2 Hours', 7200), + ('3 Hours', 10800), + ('4 Hours', 14400), + ('5 Hours', 18000), + ('6 Hours', 21600), + ('7 Hours', 25200), + ('8 Hours', 28800), + ('9 Hours', 32400), + ('10 Hours', 36000), + ('11 Hours', 39600), + ('12 Hours', 43200), + ('13 Hours', 46800), + ('14 Hours', 50400), + ('15 Hours', 54000), + ('16 Hours', 57600), + ('17 Hours', 61200), + ('18 Hours', 64800), + ('19 Hours', 68400), + ('20 Hours', 72000), + ('21 Hours', 75600), + ('22 Hours', 79200), + ('23 Hours', 82800), + ('1 days', 86400), + ('2 days', 172800), + ('3 days', 259200), + ('4 days', 345600), + ('5 days', 432000), + ('6 days', 518400), + ('7 days', 604800), + ('8 days', 691200), + ('9 days', 777600), + ('10 days', 864000), + ('11 days', 950400), + ('custom', custom_time) + ], + default=300, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('very fast', 0.001), + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0) + ], + default=1.0, + ), + ba.FloatChoiceSetting( + 'Respawn PowerUp Times', + choices=[ + ('Fast', 0.10), + ('Normal', 0.35), + ('Long', 0.7), + ('Longer', 1.0), + ('custom', custom_speed) + ], + default=0.3 + ), + ba.BoolSetting('Timer on Screen', default=False), + ba.BoolSetting('PowerUp Expire', default=False), + ba.BoolSetting('Epic Mode', default=False), + ba.BoolSetting('spaz » enable punch', default=False), + ba.BoolSetting('spaz » enable bomb', default=False), + ba.BoolSetting('spaz » enable pickup', default=False), + ba.BoolSetting('PowerUp » shield', default=False), + ba.BoolSetting('PowerUp » health', default=False), + ba.BoolSetting('PowerUp » curse', default=False), + ba.BoolSetting('PowerUp » Triple bombs', default=False), + ba.BoolSetting('PowerUp » ice bombs', default=False), + ba.BoolSetting('PowerUp » impact bombs', default=False), + ba.BoolSetting('PowerUp » land mines', default=False), + ba.BoolSetting('PowerUp » sticky bombs', default=False), + ba.BoolSetting('PowerUp » random', default=True) + ] + return settings + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession) + or issubclass(sessiontype, ba.CoopSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + + self._epic_mode = settings.get('Epic Mode', False) + self._punch_mode = settings.get('spaz » enable punch', False) + self._bomb_mode = settings.get('spaz » enable bomb', False) + self._pickup_mode = settings.get('spaz » enable pickup', False) + self._powerup_timer = settings.get('Respawn PowerUp Times', False) + self.expire_type = settings.get('PowerUp Expire', True) + self._time_limit = float(settings['Time Limit']) + self._timer_type = float(settings['Timer on Screen']) + self._powerup_shield = settings.get('PowerUp » shield', False) + self._powerup_health = settings.get('PowerUp » health', False) + self._powerup_curse = settings.get('PowerUp » curse', False) + self._powerup_triple_bombs = settings.get('PowerUp » Triple bombs', False) + self._powerup_ice_bombs = settings.get('PowerUp » ice bombs', False) + self._powerup_impact_bombs = settings.get('PowerUp » impact bombs', False) + self._powerup_land_mines = settings.get('PowerUp » land mines', False) + self._powerup_sticky_bombs = settings.get('PowerUp » sticky bombs', False) + self._powerup_random = settings.get('PowerUp » random', True) + + self._last_player_death_time: Optional[float] = None + self._meteor_time = 2.0 + self._timer: Optional[OnScreenTimer] = None + + # Some base class overrides: + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SURVIVAL) + if self._epic_mode: + self.slow_motion = True + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + + # Drop a wave every few seconds.. and every so often drop the time + # between waves ..lets have things increase faster if we have fewer + # players. + delay = 5.0 if len(self.players) > 2 else 2.5 + if self._epic_mode: + delay *= 0.25 + ba.timer(delay, self._decrement_meteor_time, repeat=True) + + # Kick off the first wave in a few seconds. + delay = 3.0 + if self._epic_mode: + delay *= 0.25 + ba.timer(delay, self._set_meteor_timer) + + if self._timer_type: + self._timer = OnScreenTimer() + self._timer.start() + else: + self._timer = None + + if not self._time_limit == 0: + ba.timer(self._time_limit, self._check_end_game) + else: pass + + def on_player_join(self, player: Player) -> None: + # Don't allow joining after we start + # (would enable leave/rejoin tomfoolery). + if self.has_begun(): + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + # For score purposes, mark them as having died right as the + # game started. + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + # Augment default behavior. + super().on_player_leave(player) + + # A departing player may trigger game-over. + self._check_end_game() + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=self._punch_mode, + enable_bomb=self._bomb_mode, + enable_pickup=self._pickup_mode) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + # Various high-level game events come through this method. + def handlemessage(self, msg: Any) -> Any: # Respawn + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, ba.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def _set_meteor_timer(self) -> None: + ba.timer(self._powerup_timer, self._drop_powerup_cluster) + + def _drop_powerup_cluster(self) -> None: + + # Random note: code like this is a handy way to plot out extents + # and debug things. + loc_test = False + if loc_test: + ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) + ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) + ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) + ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) + + # Drop several bombs in series. + delay = 0.0 + for _i in range(random.randrange(1, 3)): + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + pos = (-7.3 + 15.3 * random.random(), 11, -5.5 + 2.1 * random.random()*2) + dropdir = (-1.0 if pos[0] > 0 else 1.0) + vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0) + if self._powerup_shield or self._powerup_health or self._powerup_curse or self._powerup_triple_bombs or self._powerup_ice_bombs or self._powerup_impact_bombs or self._powerup_land_mines or self._powerup_sticky_bombs or self._powerup_random: + if not self._powerup_random: + if self._powerup_shield: ba.timer(delay /random.random(), ba.Call(self._drop_powerup, pos, vel, "shield")) + if self._powerup_health: ba.timer(delay / random.random(), ba.Call(self._drop_powerup, pos, vel, "health")) + if self._powerup_curse: ba.timer(delay / random.random(), ba.Call(self._drop_powerup, pos, vel, "curse")) + if self._powerup_triple_bombs: ba.timer(delay / random.random(), ba.Call(self._drop_powerup, pos, vel, "triple_bombs")) + if self._powerup_ice_bombs: ba.timer(delay / random.random(), ba.Call(self._drop_powerup, pos, vel, "ice_bombs")) + if self._powerup_impact_bombs: ba.timer(delay / random.random(), ba.Call(self._drop_powerup, pos, vel, "impact_bombs")) + if self._powerup_land_mines: ba.timer(delay / random.random(), ba.Call(self._drop_powerup, pos, vel, "land_mines")) + if self._powerup_sticky_bombs: ba.timer(delay / random.random(), ba.Call(self._drop_powerup, pos, vel, "sticky_bombs")) + else: + ba.timer(delay, ba.Call(self._drop_powerup, pos, vel, PowerupBoxFactory().get_random_powerup_type())) + else: pass + + delay += 0.5 + self._set_meteor_timer() + + def _drop_powerup(self, position: Sequence[float], velocity: Sequence[float], PowerupType) -> None: + PowerupBox(position=position,poweruptype=PowerupType,expire=self.expire_type).autoretain() + + def _decrement_meteor_time(self) -> None: + self._meteor_time = max(0.01, self._meteor_time * 0.9) + + def end_game(self) -> None: + cur_time = 0 + 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 = True + + # 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 += 69 # A bit extra for survivors. :p + self.stats.player_scored(player, score, screenmessage=False) + + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = ba.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + + # Set the team score to the max time survived by any player on + # that team. + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) diff --git a/dist/ba_root/mods/games/Runners.py b/dist/ba_root/mods/games/Runners.py new file mode 100644 index 0000000..1972c32 --- /dev/null +++ b/dist/ba_root/mods/games/Runners.py @@ -0,0 +1,339 @@ +# Made by: Froshlee14 +# Ported by: Freaku / @[Just] Freak#4999 + + + + + +from __future__ import annotations +from typing import TYPE_CHECKING +import ba, random +from bastd.actor import bomb, spazbot, powerupbox, bomb +from bastd.gameutils import SharedObjects +from bastd.actor.onscreentimer import OnScreenTimer +if TYPE_CHECKING: + from typing import Any, Sequence, Union, Optional, List, Dict, Type, Literal + + + +## MoreMinigames.py support ## +def ba_get_api_version(): + return 6 +def ba_get_levels(): + return [ba._level.Level('Runners',gametype=RunnersGame, settings={}, preview_texture_name = 'achievementGotTheMoves')] +## MoreMinigames.py support ## + + + +class SpookyBot(spazbot.BrawlerBot): + character = 'Bones' + color = (1,1,1) + +class Player(ba.Player['Team']): + """Our player type for this game.""" + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +# ba_meta require api 7 +# ba_meta export game +class RunnersGame(ba.TeamGameActivity[Player, Team]): + name = 'Runners' + description = 'Run for your Life!' + available_settings = [ba.BoolSetting('Epic Mode', default=False)] + scoreconfig = ba.ScoreConfig(label='Survived', + scoretype=ba.ScoreType.MILLISECONDS, + version='B') + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + # we're currently hard-coded for one map.. + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Dak'] + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession) + or issubclass(sessiontype, ba.CoopSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + self._epic_mode = settings.get('Epic Mode', False) + self._last_player_death_time: Optional[float] = None + self._timer: Optional[OnScreenTimer] = None + self._time2Object = 1 if self._epic_mode else 1.5 + + # Some base class overrides: + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SURVIVAL) + if self._epic_mode: + self.slow_motion = True + + + def on_begin(self) -> None: + super().on_begin() + + self._timer = OnScreenTimer() + self._timer.start() + + # Check for immediate end (if we've only got 1 player, etc). + ba.timer(5.0, self._check_end_game) + self._bots = spazbot.SpazBotSet() + for i in range(6+len(self.players)): + self.addBot() + + self.obstacleTime = 5 + ba.timer(3,self.start) + + def start(self): + self._obsTimer = ba.timer(self._time2Object,self.doObstacle,repeat=True) + self._move = ba.timer(0.8,ba.WeakCall(self.movePlayers),repeat=True) + self._fast = ba.timer(5,ba.WeakCall(self.faster),repeat=True) + + def faster(self): + if self.obstacleTime > 1: + self.obstacleTime -= 0.1 + + def addBot(self): + ba.timer(1, ba.Call(self._bots.spawn_bot, SpookyBot, pos=(random.randint(-4,4),5,-7.3), spawn_time=0)) + + def doObstacle(self): + for i in range(random.randrange(4,8)): + type = random.choice(['tnt','land_mine','land_mine','land_mine','powerup','land_mine','land_mine','land_mine','tnt','tnt','land_mine']) + pos = (random.randrange(-5,5),4.8 if type != 'land_mine' else 4.6,8) + if type == 'powerup': + b = powerupbox.PowerupBox(position=pos,poweruptype=random.choice(['health','shield'])).autoretain() + b.node.model_scale = 0.6 + else: + b = bomb.Bomb(position=pos,velocity=(0,0,0),bomb_type = type, bomb_scale = 0.6).autoretain() + if type == 'land_mine': b.arm() + pos = b.node.position + ba.animate_array(b.node,'position',3,{0:b.node.position,self.obstacleTime:(pos[0],pos[1],pos[2]-16)}) + + def movePlayers(self): + for p in self.players: + if p.is_alive(): + p.actor.node.move_up_down = -10 + + def on_player_join(self, player: Player) -> None: + # Don't allow joining after we start + # (would enable leave/rejoin tomfoolery). + if self.has_begun(): + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + # For score purposes, mark them as having died right as the + # game started. + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + # Augment default behavior. + super().on_player_leave(player) + + # A departing player may trigger game-over. + self._check_end_game() + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player, self.map.defs.points['spawn']) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + # Various high-level game events come through this method. + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = ba.time() + + # Record the player's moment of death. + # assert isinstance(msg.spaz.player + msg.getplayer(Player).death_time = curtime + + # In co-op mode, end the game the instant everyone dies + # (more accurate looking). + # In teams/ffa, allow a one-second fudge-factor so we can + # get more draws if players die basically at the same time. + if isinstance(self.session, ba.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + ba.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = curtime + else: + ba.timer(1.0, self._check_end_game) + elif isinstance(msg, spazbot.SpazBotDiedMessage): + self.addBot() + else: + # Default handler: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, ba.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def end_game(self) -> None: + cur_time = ba.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + # Mark death-time as now for any still-living players + # and award players points for how long they lasted. + # (these per-player scores are only meaningful in team-games) + for team in self.teams: + for player in team.players: + survived = False + + # Throw an extra fudge factor in so teams that + # didn't die come out ahead of teams that did. + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + # Award a per-player score depending on how many seconds + # they lasted (per-player scores only affect teams mode; + # everywhere else just looks at the per-team score). + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 # A bit extra for survivors. + self.stats.player_scored(player, score, screenmessage=False) + + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = ba.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + + # Set the team score to the max time survived by any player on + # that team. + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) + + + + + +class dakDefs(): + points = {} + boxes = {} + points['spawn'] = (0,5,0) + (0, 0, 0) + (10,0,0.5) + boxes['area_of_interest_bounds'] = (0,-1,-5) + (0, 0, 0) + (0, 0, 0) + boxes['map_bounds'] = (0,5,0) + (0, 0, 0) + (10,10,16) + +class dakMap(ba.Map): + defs = dakDefs() + name = 'Dak' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'achievementGotTheMoves' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'tex': ba.gettexture('white'), + 'bgmodel': ba.getmodel('thePadBG') + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self._playMaterial = ba.Material() + self._playMaterial.add_actions( + conditions=("they_have_material",shared.player_material), + actions=(("modify_part_collision","collide",True),("modify_part_collision","physical",True))) + self._bombMaterial = ba.Material() + self._bombMaterial.add_actions( + conditions=("they_have_material",bomb.BombFactory.get().bomb_material), + actions=(("modify_part_collision","collide",True),("modify_part_collision","physical",True))) + self.ground = ba.newnode('region', attrs={'position':(0,4,0),'scale':(10,1,20),'type': 'box', + 'materials':(self._playMaterial,self._bombMaterial,shared.footing_material)}) + self.playWall = ba.newnode('region', attrs={'position':(0,4,1.5),'scale':(10,10,0.5),'type': 'box', + 'materials':(self._playMaterial,shared.footing_material)}) + self.botWall = ba.newnode('region', attrs={'position':(0,4,-1),'scale':(10,10,0.25),'type': 'box', + 'materials':(self._playMaterial,shared.footing_material)}) + self.rightWall = ba.newnode('region', attrs={'position':(4,4,0),'scale':(0.5,10,10),'type': 'box', + 'materials':(self._playMaterial,shared.footing_material)}) + self.leftWall = ba.newnode('region', attrs={'position':(-4,4,0),'scale':(0.5,10,10),'type': 'box', + 'materials':(self._playMaterial,shared.footing_material)}) + self.bg = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['tex'] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.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 + + + + + + + +ba._map.register_map(dakMap) \ No newline at end of file diff --git a/dist/ba_root/mods/games/TnT_Error.py b/dist/ba_root/mods/games/TnT_Error.py index 3e9f827..57911f6 100644 --- a/dist/ba_root/mods/games/TnT_Error.py +++ b/dist/ba_root/mods/games/TnT_Error.py @@ -1,6 +1,6 @@ """Defines a Tnt-dodging mini-game.""" -# ba_meta require api 6 +# ba_meta require api 7 from __future__ import annotations diff --git a/dist/ba_root/mods/games/Tower_Rush.py b/dist/ba_root/mods/games/Tower_Rush.py index 961d50c..4b2cf37 100644 --- a/dist/ba_root/mods/games/Tower_Rush.py +++ b/dist/ba_root/mods/games/Tower_Rush.py @@ -2,7 +2,7 @@ # """Defines assault minigame.""" -# ba_meta require api 6 +# ba_meta require api 7 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations @@ -160,8 +160,8 @@ class BaseRaidGame(ba.TeamGameActivity[Player, Team]): 'color': (0.7,0.88,1.0,1.1), 'shadow': 1.0, 'flatness': 1.0, - 'position': (0, -120), - 'scale': 1.7, + 'position': (0, -70), + 'scale': 0.8, 'text': "..."}) self.setup_standard_time_limit(self._time_limit) ba.timer(4.5, ba.WeakCall(self.selection, first_run = True)) diff --git a/dist/ba_root/mods/games/baMeteorShowerMod.py b/dist/ba_root/mods/games/baMeteorShowerMod.py new file mode 100644 index 0000000..e612922 --- /dev/null +++ b/dist/ba_root/mods/games/baMeteorShowerMod.py @@ -0,0 +1,296 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines a bomb-dodging mini-game.""" + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba +from bastd.actor.bomb import Bomb +from bastd.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List, Dict, Type, Type + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export game +class MeteorShowerModGame(ba.TeamGameActivity[Player, Team]): + """Minigame involving dodging falling bombs.""" + + name = 'Meteor Shower+' + description = 'Dodge the falling bombs.' + available_settings = [ + ba.BoolSetting('Epic Mode', default=False), + ba.BoolSetting('Normal Bombs', default=True), + ba.BoolSetting('Frozen Bombs', default=False), + ba.BoolSetting('Impact Bombs', default=False), + ba.BoolSetting('Sticky Bombs', default=False), + ba.BoolSetting('TNTs', default=False), + ba.IntSetting('Bomb Drop Rate',min_value=0.05,default=2.0,increment=0.05), + ba.IntChoiceSetting('Bomb Velocity',choices=[ + ('Random', 1),('No Velocity', 2), + ], + default=1, + )] + scoreconfig = ba.ScoreConfig(label='Survived', + scoretype=ba.ScoreType.MILLISECONDS, + version='B') + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + # we're currently hard-coded for one map.. + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Rampage'] + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession) + or issubclass(sessiontype, ba.CoopSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + + self._epic_mode = settings.get('Epic Mode', False) + self._last_player_death_time: Optional[float] = None + self._meteor_time = 2.0 + self._bomb_entries = [] + if settings.get('Normal Bombs',True): + self._bomb_entries += ['normal'] + if settings.get('Frozen Bombs',True): + self._bomb_entries += ['ice'] + if settings.get('Impact Bombs',True): + self._bomb_entries += ['impact'] + if settings.get('Sticky Bombs',True): + self._bomb_entries += ['sticky'] + if settings.get('TNTs',True): + self._bomb_entries += ['tnt'] + if self._bomb_entries == []: self._bomb_entries = ['normal'] + self._bool = settings.get('Bomb Velocity') + self._bool_2 = settings.get('Bomb Drop Rate') + self._timer: Optional[OnScreenTimer] = None + + # Some base class overrides: + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SURVIVAL) + if self._epic_mode: + self.slow_motion = True + + def on_begin(self) -> None: + super().on_begin() + + # Drop a wave every few seconds.. and every so often drop the time + # between waves ..lets have things increase faster if we have fewer + # players. + delay = 5.0 if len(self.players) > 2 else 2.5 + if self._epic_mode: + delay *= 0.25 + ba.timer(delay, self._decrement_meteor_time, repeat=True) + + # Kick off the first wave in a few seconds. + delay = 3.0 + if self._epic_mode: + delay *= 0.25 + ba.timer(delay, self._set_meteor_timer) + + self._timer = OnScreenTimer() + self._timer.start() + + # Check for immediate end (if we've only got 1 player, etc). + #ba.timer(5.0, self._check_end_game) + + def on_player_join(self, player: Player) -> None: + # Don't allow joining after we start + # (would enable leave/rejoin tomfoolery). + if self.has_begun(): + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + # For score purposes, mark them as having died right as the + # game started. + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + # Augment default behavior. + super().on_player_leave(player) + + # A departing player may trigger game-over. + self._check_end_game() + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + # Various high-level game events come through this method. + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = ba.time() + + # Record the player's moment of death. + # assert isinstance(msg.spaz.player + msg.getplayer(Player).death_time = curtime + + # In co-op mode, end the game the instant everyone dies + # (more accurate looking). + # In teams/ffa, allow a one-second fudge-factor so we can + # get more draws if players die basically at the same time. + if isinstance(self.session, ba.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + ba.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = curtime + else: + ba.timer(1.0, self._check_end_game) + + else: + # Default handler: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, ba.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def _set_meteor_timer(self) -> None: + ba.timer(int((1 + self._bool_2 * random.random())) * self._meteor_time, + self._drop_bomb_cluster) + + def _drop_bomb_cluster(self) -> None: + + # Random note: code like this is a handy way to plot out extents + # and debug things. + loc_test = False + if loc_test: + ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) + ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) + ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) + ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) + + # Drop several bombs in series. + delay = 0.0 + for _i in range(random.randrange(1, 3)): + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + pos = (-7.3 + 15.3 * random.random(), 11, + -6.3 + random.uniform(0.1,4.2)) + dropdir = (-1.0 if pos[0] > 0 else 1.0) + if self._bool == 2: + vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0) + else: + vel = (0,0,0) + ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + def _drop_bomb(self, position: Sequence[float], + velocity: Sequence[float]) -> None: + Bomb(position=position, velocity=velocity, bomb_type=random.choice(self._bomb_entries)).autoretain() + + def _decrement_meteor_time(self) -> None: + self._meteor_time = max(0.01, self._meteor_time * 0.9) + + def end_game(self) -> None: + cur_time = ba.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + # Mark death-time as now for any still-living players + # and award players points for how long they lasted. + # (these per-player scores are only meaningful in team-games) + for team in self.teams: + for player in team.players: + survived = False + + # Throw an extra fudge factor in so teams that + # didn't die come out ahead of teams that did. + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + # Award a per-player score depending on how many seconds + # they lasted (per-player scores only affect teams mode; + # everywhere else just looks at the per-team score). + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 # A bit extra for survivors. + self.stats.player_scored(player, score, screenmessage=False) + + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = ba.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + + # Set the team score to the max time survived by any player on + # that team. + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) diff --git a/dist/ba_root/mods/games/safe_zone.py b/dist/ba_root/mods/games/safe_zone.py index 4c4fe28..ba5337e 100644 --- a/dist/ba_root/mods/games/safe_zone.py +++ b/dist/ba_root/mods/games/safe_zone.py @@ -1,716 +1,716 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Elimination mini-game.""" - -# Maded by Froshlee14 -# Update by SEBASTIAN2059 - -# ba_meta require api 6 -# (see https://ballistica.net/wiki/meta-tag-system) - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import ba, _ba -import random -from bastd.actor.spazfactory import SpazFactory -from bastd.actor.scoreboard import Scoreboard -from bastd.actor import spazbot as stdbot -from bastd.gameutils import SharedObjects as so - -if TYPE_CHECKING: - from typing import (Any, Tuple, Dict, Type, List, Sequence, Optional, - Union) - - -class Icon(ba.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 = ba.gettexture('characterIconMask') - - icon = player.get_icon() - self.node = ba.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 = ba.newnode( - 'text', - owner=self.node, - attrs={ - 'text': ba.Lstr(value=player.getname()), - 'color': ba.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 = ba.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: - ba.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: - ba.timer(0.6, self.update_for_lives) - - def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.DieMessage): - self.node.delete() - return None - return super().handlemessage(msg) - - -class Player(ba.Player['Team']): - """Our player type for this game.""" - - def __init__(self) -> None: - self.lives = 0 - self.icons: List[Icon] = [] - - -class Team(ba.Team[Player]): - """Our team type for this game.""" - - def __init__(self) -> None: - self.survival_seconds: Optional[int] = None - self.spawn_order: List[Player] = [] - -lang = ba.app.lang.language -if lang == 'Spanish': - description = 'Mantente en la zona segura.' - join_description = 'Corre hacia la zona segura.' - kill_timer = 'Kill timer: ' -else: - description = 'Stay in the safe zone.' - join_description = 'Run into the safe zone' - kill_timer = 'Kill timer: ' - -# ba_meta export game -class SafeZoneGame(ba.TeamGameActivity[Player, Team]): - """Game type where last player(s) left alive win.""" - - name = 'Safe Zone' - description = description - scoreconfig = ba.ScoreConfig(label='Survived', - scoretype=ba.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[ba.Session]) -> List[ba.Setting]: - settings = [ - ba.IntSetting( - 'Lives Per Player', - default=2, - min_value=1, - max_value=10, - increment=1, - ), - ba.IntChoiceSetting( - 'Time Limit', - choices=[ - ('None', 0), - ('1 Minute', 60), - ('2 Minutes', 120), - ('5 Minutes', 300), - ('10 Minutes', 600), - ('20 Minutes', 1200), - ], - default=0, - ), - ba.FloatChoiceSetting( - 'Respawn Times', - choices=[ - ('Short', 0.25), - ('Normal', 0.5), - ], - default=0.5, - ), - ba.BoolSetting('Epic Mode', default=False), - ] - if issubclass(sessiontype, ba.DualTeamSession): - settings.append(ba.BoolSetting('Solo Mode', default=False)) - settings.append( - ba.BoolSetting('Balance Total Lives', default=False)) - return settings - - @classmethod - def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: - return (issubclass(sessiontype, ba.DualTeamSession) - or issubclass(sessiontype, ba.FreeForAllSession)) - - @classmethod - def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: - return ['Football Stadium','Hockey Stadium'] - - def __init__(self, settings: dict): - super().__init__(settings) - self._scoreboard = Scoreboard() - self._start_time: Optional[float] = None - self._vs_text: Optional[ba.Actor] = None - self._round_end_timer: Optional[ba.Timer] = None - self._epic_mode = bool(settings['Epic Mode']) - self._lives_per_player = int(settings['Lives Per Player']) - self._time_limit = float(settings['Time Limit']) - self._balance_total_lives = bool( - settings.get('Balance Total Lives', False)) - self._solo_mode = bool(settings.get('Solo Mode', False)) - - # Base class overrides: - self.slow_motion = self._epic_mode - self.default_music = (ba.MusicType.EPIC - if self._epic_mode else ba.MusicType.SURVIVAL) - - self._tick_sound = ba.getsound('tick') - - def get_instance_description(self) -> Union[str, Sequence]: - return join_description - - def get_instance_description_short(self) -> Union[str, Sequence]: - return 'last team standing wins' if isinstance( - self.session, ba.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 - ba.screenmessage( - ba.Lstr(resource='playerDelayedJoinText', - subs=[('${PLAYER}', player.getname(full=True))]), - color=(0, 1, 0), - ) - return - - player.lives = self._lives_per_player - - if self._solo_mode: - player.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 = ba.time() - self.setup_standard_time_limit(self._time_limit) - #self.setup_standard_powerup_drops() - - ba.timer(5,self.spawn_zone) - self._bots = stdbot.SpazBotSet() - ba.timer(3,ba.Call(self.add_bot,'left')) - ba.timer(3,ba.Call(self.add_bot,'right')) - if len(self.initialplayerinfos) > 4: - ba.timer(5,ba.Call(self.add_bot,'right')) - ba.timer(5,ba.Call(self.add_bot,'left')) - - if self._solo_mode: - self._vs_text = ba.NodeActor( - ba.newnode('text', - attrs={ - 'position': (0, 105), - 'h_attach': 'center', - 'h_align': 'center', - 'maxwidth': 200, - 'shadow': 0.5, - 'vr_depth': 390, - 'scale': 0.6, - 'v_attach': 'bottom', - 'color': (0.8, 0.8, 0.3, 1.0), - 'text': ba.Lstr(resource='vsText') - })) - - # If balance-team-lives is on, add lives to the smaller team until - # total lives match. - if (isinstance(self.session, ba.DualTeamSession) - and self._balance_total_lives and self.teams[0].players - and self.teams[1].players): - if self._get_total_team_lives( - self.teams[0]) < self._get_total_team_lives(self.teams[1]): - lesser_team = self.teams[0] - greater_team = self.teams[1] - else: - lesser_team = self.teams[1] - greater_team = self.teams[0] - add_index = 0 - while (self._get_total_team_lives(lesser_team) < - self._get_total_team_lives(greater_team)): - lesser_team.players[add_index].lives += 1 - add_index = (add_index + 1) % len(lesser_team.players) - - self._update_icons() - - # We could check game-over conditions at explicit trigger points, - # but lets just do the simple thing and poll it. - ba.timer(1.0, self._update, repeat=True) - - def spawn_zone(self): - self.zone_pos = (random.randrange(-10,10),0.05,random.randrange(-5,5)) - self.zone = ba.newnode('locator',attrs={'shape':'circle','position':self.zone_pos,'color':(1, 1, 0),'opacity':0.8,'draw_beauty':True,'additive':False,'drawShadow':False}) - self.zone_limit = ba.newnode('locator',attrs={'shape':'circleOutline','position':self.zone_pos,'color':(1, 0.2, 0.2),'opacity':0.8,'draw_beauty':True,'additive':False,'drawShadow':False}) - ba.animate_array(self.zone, 'size', 1,{0:[0], 0.3:[self.get_players_count()*0.85], 0.35:[self.get_players_count()*0.8]}) - ba.animate_array(self.zone_limit, 'size', 1,{0:[0], 0.3:[self.get_players_count()*1.2], 0.35:[self.get_players_count()*0.95]}) - self.last_players_count = self.get_players_count() - ba.playsound(ba.getsound('laserReverse')) - self.start_timer() - self.move_zone() - - def delete_zone(self): - self.zone.delete() - self.zone = None - self.zone_limit.delete() - self.zone_limit = None - ba.playsound(ba.getsound('shieldDown')) - ba.timer(1,self.spawn_zone) - - def move_zone(self): - if self.zone_pos[0] > 0: x = random.randrange(0,10) - else: x = random.randrange(-10,0) - - if self.zone_pos[2] > 0: y = random.randrange(0,5) - else: y = random.randrange(-5,0) - - new_pos = (x,0.05,y) - ba.animate_array(self.zone, 'position', 3,{0:self.zone.position, 8:new_pos}) - ba.animate_array(self.zone_limit, 'position', 3,{0:self.zone_limit.position,8:new_pos}) - - def start_timer(self): - count = self.get_players_count() - self._time_remaining = 10 if count > 9 else count-1 if count > 6 else count if count > 2 else count*2 - self._timer_x = ba.Timer(1.0,ba.WeakCall(self.tick),repeat=True) - # gnode = ba.getactivity().globalsnode - # tint = gnode.tint - # ba.animate_array(gnode,'tint',3,{0:tint,self._time_remaining*1.5:(1.0,0.5,0.5),self._time_remaining*1.55:tint}) - - def stop_timer(self): - self._time = None - self._timer_x = None - - def tick(self): - self.check_players() - self._time = ba.NodeActor(ba.newnode('text', - attrs={'v_attach':'top','h_attach':'center', - 'text':kill_timer+str(self._time_remaining)+'s', - 'opacity':0.8,'maxwidth':100,'h_align':'center', - 'v_align':'center','shadow':1.0,'flatness':1.0, - 'color':(1,1,1),'scale':1.5,'position':(0,-50)} - ) - ) - self._time_remaining -= 1 - ba.playsound(self._tick_sound) - - def check_players(self): - if self._time_remaining <= 0: - self.stop_timer() - ba.animate_array(self.zone, 'size', 1,{0:[self.last_players_count*0.8], 1.4:[self.last_players_count*0.8],1.5:[0]}) - ba.animate_array(self.zone_limit, 'size', 1,{0:[self.last_players_count*0.95], 1.45:[self.last_players_count*0.95],1.5:[0]}) - ba.timer(1.5,self.delete_zone) - for player in self.players: - if not player.actor is None: - if player.actor.is_alive(): - p1 = player.actor.node.position - p2 = self.zone.position - diff = (ba.Vec3(p1[0]-p2[0],0.0,p1[2]-p2[2])) - dist = (diff.length()) - if dist > (self.get_players_count()*0.7): - player.actor.handlemessage(ba.DieMessage()) - - def get_players_count(self): - count = 0 - for player in self.players: - if not player.actor is None: - if player.actor.is_alive(): - count += 1 - return count - - 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, ba.FreeForAllSession): - count = len(self.teams) - x_offs = 85 - xval = x_offs * (count - 1) * -0.5 - for team in self.teams: - if len(team.players) == 1: - player = team.players[0] - for icon in player.icons: - icon.set_position_and_scale((xval, 30), 0.7) - icon.update_for_lives() - xval += x_offs - - # In teams mode we split up teams. - else: - if self._solo_mode: - # First off, clear out all icons. - for player in self.players: - player.icons = [] - - # Now for each team, cycle through our available players - # adding icons. - for team in self.teams: - if team.id == 0: - xval = -60 - x_offs = -78 - else: - xval = 60 - x_offs = 78 - is_first = True - test_lives = 1 - while True: - players_with_lives = [ - p for p in team.spawn_order - if p and p.lives >= test_lives - ] - if not players_with_lives: - break - for player in players_with_lives: - player.icons.append( - Icon(player, - position=(xval, (40 if is_first else 25)), - scale=1.0 if is_first else 0.5, - name_maxwidth=130 if is_first else 75, - name_scale=0.8 if is_first else 1.0, - flatness=0.0 if is_first else 1.0, - shadow=0.5 if is_first else 1.0, - show_death=is_first, - show_lives=False)) - xval += x_offs * (0.8 if is_first else 0.56) - is_first = False - test_lives += 1 - # Non-solo mode. - else: - for team in self.teams: - if team.id == 0: - xval = -50 - x_offs = -85 - else: - xval = 50 - x_offs = 85 - for player in team.players: - for icon in player.icons: - icon.set_position_and_scale((xval, 30), 0.7) - icon.update_for_lives() - xval += x_offs - - def _get_spawn_point(self, player: Player) -> Optional[ba.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 = ba.Vec3(living_player_pos) - points: List[Tuple[float, ba.Vec3]] = [] - for team in self.teams: - start_pos = ba.Vec3(self.map.get_start_position(team.id)) - points.append( - ((start_pos - player_pos).length(), start_pos)) - # Hmm.. we need to sorting vectors too? - points.sort(key=lambda x: x[0]) - return points[-1][1] - return None - - def spawn_player(self, player: Player) -> ba.Actor: - actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) - if not self._solo_mode: - ba.timer(0.3, ba.Call(self._print_lives, player)) - - # spaz but *without* the ability to attack or pick stuff up. - actor.connect_controls_to_player(enable_punch=False, - enable_bomb=False, - enable_pickup=False) - - # 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 bastd.actor import popuptext - - # We get called in a timer so it's possible our player has left/etc. - if not player or not player.is_alive() or not player.node: - return - - 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. - ba.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(ba.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, ba.PlayerDiedMessage): - - # Augment standard behavior. - super().handlemessage(msg) - player: Player = msg.getplayer(Player) - - player.lives -= 1 - if player.lives < 0: - ba.print_error( - "Got lives < 0 in Elim; this shouldn't happen. solo:" + - str(self._solo_mode)) - player.lives = 0 - - # If we have any icons, update their state. - for icon in player.icons: - icon.handle_player_died() - - # Play big death sound on our last death - # or for every one in solo mode. - if self._solo_mode or player.lives == 0: - ba.playsound(SpazFactory.get().single_player_death_sound) - - # If we hit zero lives, we're dead (and our team might be too). - if player.lives == 0: - # 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(ba.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) - elif isinstance(msg,stdbot.SpazBotDiedMessage): - self._on_spaz_bot_died(msg) - - def _on_spaz_bot_died(self,die_msg): - ba.timer(1,ba.Call(self.add_bot,die_msg.spazbot.node.position)) - - def _on_bot_spawn(self,spaz): - spaz.update_callback = self.move_bot - spaz_type = type(spaz) - spaz._charge_speed = self._get_bot_speed(spaz_type) - - def add_bot(self,pos=None): - if pos == 'left': position = (-11,0,random.randrange(-5,5)) - elif pos == 'right': position = (11,0,random.randrange(-5,5)) - else: position = pos - self._bots.spawn_bot(self.get_random_bot(),pos=position,spawn_time=1,on_spawn_call=ba.Call(self._on_bot_spawn)) - - def move_bot(self,bot): - p = bot.node.position - speed = -bot._charge_speed if(p[0]>=-11 and p[0]<0) else bot._charge_speed - - if (p[0]>=-11) and (p[0]<=11): - bot.node.move_left_right = speed - bot.node.move_up_down = 0.0 - bot.node.run = 0.0 - return True - return False - - def get_random_bot(self): - bots = [stdbot.BomberBotStatic, stdbot.TriggerBotStatic] - return (random.choice(bots)) - - def _get_bot_speed(self, bot_type): - if bot_type == stdbot.BomberBotStatic: - return 0.48 - elif bot_type == stdbot.TriggerBotStatic: - return 0.73 - else: - raise Exception('Invalid bot type to _getBotSpeed(): '+str(bot_type)) - - 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 = ba.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 = ba.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) +# Released under the MIT License. See LICENSE for details. +# +"""Elimination mini-game.""" + +# Maded by Froshlee14 +# Update by SEBASTIAN2059 + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba, _ba +import random +from bastd.actor.spazfactory import SpazFactory +from bastd.actor.scoreboard import Scoreboard +from bastd.actor import spazbot as stdbot +from bastd.gameutils import SharedObjects as so + +if TYPE_CHECKING: + from typing import (Any, Tuple, Dict, Type, List, Sequence, Optional, + Union) + + +class Icon(ba.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 = ba.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = ba.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 = ba.newnode( + 'text', + owner=self.node, + attrs={ + 'text': ba.Lstr(value=player.getname()), + 'color': ba.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 = ba.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: + ba.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: + ba.timer(0.6, self.update_for_lives) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.lives = 0 + self.icons: List[Icon] = [] + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.spawn_order: List[Player] = [] + +lang = ba.app.lang.language +if lang == 'Spanish': + description = 'Mantente en la zona segura.' + join_description = 'Corre hacia la zona segura.' + kill_timer = 'Kill timer: ' +else: + description = 'Stay in the safe zone.' + join_description = 'Run into the safe zone' + kill_timer = 'Kill timer: ' + +# ba_meta export game +class SafeZoneGame(ba.TeamGameActivity[Player, Team]): + """Game type where last player(s) left alive win.""" + + name = 'Safe Zone' + description = description + scoreconfig = ba.ScoreConfig(label='Survived', + scoretype=ba.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[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting( + 'Lives Per Player', + default=2, + min_value=1, + max_value=10, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Short', 0.25), + ('Normal', 0.5), + ], + default=0.5, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, ba.DualTeamSession): + settings.append(ba.BoolSetting('Solo Mode', default=False)) + settings.append( + ba.BoolSetting('Balance Total Lives', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Football Stadium','Hockey Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._start_time: Optional[float] = None + self._vs_text: Optional[ba.Actor] = None + self._round_end_timer: Optional[ba.Timer] = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = int(settings['Lives Per Player']) + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False)) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SURVIVAL) + + self._tick_sound = ba.getsound('tick') + + def get_instance_description(self) -> Union[str, Sequence]: + return join_description + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'last team standing wins' if isinstance( + self.session, ba.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 + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + + player.lives = self._lives_per_player + + if self._solo_mode: + player.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 = ba.time() + self.setup_standard_time_limit(self._time_limit) + #self.setup_standard_powerup_drops() + + ba.timer(5,self.spawn_zone) + self._bots = stdbot.SpazBotSet() + ba.timer(3,ba.Call(self.add_bot,'left')) + ba.timer(3,ba.Call(self.add_bot,'right')) + if len(self.initialplayerinfos) > 4: + ba.timer(5,ba.Call(self.add_bot,'right')) + ba.timer(5,ba.Call(self.add_bot,'left')) + + if self._solo_mode: + self._vs_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': ba.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, ba.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + ba.timer(1.0, self._update, repeat=True) + + def spawn_zone(self): + self.zone_pos = (random.randrange(-10,10),0.05,random.randrange(-5,5)) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':self.zone_pos,'color':(1, 1, 0),'opacity':0.8,'draw_beauty':True,'additive':False,'drawShadow':False}) + self.zone_limit = ba.newnode('locator',attrs={'shape':'circleOutline','position':self.zone_pos,'color':(1, 0.2, 0.2),'opacity':0.8,'draw_beauty':True,'additive':False,'drawShadow':False}) + ba.animate_array(self.zone, 'size', 1,{0:[0], 0.3:[self.get_players_count()*0.85], 0.35:[self.get_players_count()*0.8]}) + ba.animate_array(self.zone_limit, 'size', 1,{0:[0], 0.3:[self.get_players_count()*1.2], 0.35:[self.get_players_count()*0.95]}) + self.last_players_count = self.get_players_count() + ba.playsound(ba.getsound('laserReverse')) + self.start_timer() + self.move_zone() + + def delete_zone(self): + self.zone.delete() + self.zone = None + self.zone_limit.delete() + self.zone_limit = None + ba.playsound(ba.getsound('shieldDown')) + ba.timer(1,self.spawn_zone) + + def move_zone(self): + if self.zone_pos[0] > 0: x = random.randrange(0,10) + else: x = random.randrange(-10,0) + + if self.zone_pos[2] > 0: y = random.randrange(0,5) + else: y = random.randrange(-5,0) + + new_pos = (x,0.05,y) + ba.animate_array(self.zone, 'position', 3,{0:self.zone.position, 8:new_pos}) + ba.animate_array(self.zone_limit, 'position', 3,{0:self.zone_limit.position,8:new_pos}) + + def start_timer(self): + count = self.get_players_count() + self._time_remaining = 10 if count > 9 else count-1 if count > 6 else count if count > 2 else count*2 + self._timer_x = ba.Timer(1.0,ba.WeakCall(self.tick),repeat=True) + # gnode = ba.getactivity().globalsnode + # tint = gnode.tint + # ba.animate_array(gnode,'tint',3,{0:tint,self._time_remaining*1.5:(1.0,0.5,0.5),self._time_remaining*1.55:tint}) + + def stop_timer(self): + self._time = None + self._timer_x = None + + def tick(self): + self.check_players() + self._time = ba.NodeActor(ba.newnode('text', + attrs={'v_attach':'top','h_attach':'center', + 'text':kill_timer+str(self._time_remaining)+'s', + 'opacity':0.8,'maxwidth':100,'h_align':'center', + 'v_align':'center','shadow':1.0,'flatness':1.0, + 'color':(1,1,1),'scale':1.5,'position':(0,-50)} + ) + ) + self._time_remaining -= 1 + ba.playsound(self._tick_sound) + + def check_players(self): + if self._time_remaining <= 0: + self.stop_timer() + ba.animate_array(self.zone, 'size', 1,{0:[self.last_players_count*0.8], 1.4:[self.last_players_count*0.8],1.5:[0]}) + ba.animate_array(self.zone_limit, 'size', 1,{0:[self.last_players_count*0.95], 1.45:[self.last_players_count*0.95],1.5:[0]}) + ba.timer(1.5,self.delete_zone) + for player in self.players: + if not player.actor is None: + if player.actor.is_alive(): + p1 = player.actor.node.position + p2 = self.zone.position + diff = (ba.Vec3(p1[0]-p2[0],0.0,p1[2]-p2[2])) + dist = (diff.length()) + if dist > (self.get_players_count()*0.7): + player.actor.handlemessage(ba.DieMessage()) + + def get_players_count(self): + count = 0 + for player in self.players: + if not player.actor is None: + if player.actor.is_alive(): + count += 1 + return count + + 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, ba.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> Optional[ba.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 = ba.Vec3(living_player_pos) + points: List[Tuple[float, ba.Vec3]] = [] + for team in self.teams: + start_pos = ba.Vec3(self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos)) + # Hmm.. we need to sorting vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def spawn_player(self, player: Player) -> ba.Actor: + actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) + if not self._solo_mode: + ba.timer(0.3, ba.Call(self._print_lives, player)) + + # spaz but *without* the ability to attack or pick stuff up. + actor.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + + # 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 bastd.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + 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. + ba.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(ba.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, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + ba.print_error( + "Got lives < 0 in Elim; this shouldn't happen. solo:" + + str(self._solo_mode)) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + ba.playsound(SpazFactory.get().single_player_death_sound) + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # 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(ba.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) + elif isinstance(msg,stdbot.SpazBotDiedMessage): + self._on_spaz_bot_died(msg) + + def _on_spaz_bot_died(self,die_msg): + ba.timer(1,ba.Call(self.add_bot,die_msg.spazbot.node.position)) + + def _on_bot_spawn(self,spaz): + spaz.update_callback = self.move_bot + spaz_type = type(spaz) + spaz._charge_speed = self._get_bot_speed(spaz_type) + + def add_bot(self,pos=None): + if pos == 'left': position = (-11,0,random.randrange(-5,5)) + elif pos == 'right': position = (11,0,random.randrange(-5,5)) + else: position = pos + self._bots.spawn_bot(self.get_random_bot(),pos=position,spawn_time=1,on_spawn_call=ba.Call(self._on_bot_spawn)) + + def move_bot(self,bot): + p = bot.node.position + speed = -bot._charge_speed if(p[0]>=-11 and p[0]<0) else bot._charge_speed + + if (p[0]>=-11) and (p[0]<=11): + bot.node.move_left_right = speed + bot.node.move_up_down = 0.0 + bot.node.run = 0.0 + return True + return False + + def get_random_bot(self): + bots = [stdbot.BomberBotStatic, stdbot.TriggerBotStatic] + return (random.choice(bots)) + + def _get_bot_speed(self, bot_type): + if bot_type == stdbot.BomberBotStatic: + return 0.48 + elif bot_type == stdbot.TriggerBotStatic: + return 0.73 + else: + raise Exception('Invalid bot type to _getBotSpeed(): '+str(bot_type)) + + 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 = ba.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 = ba.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)