# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) """Ultimate Last Stand V2: Made by Cross Joy""" # Anyone who wanna help me in giving suggestion/ fix bugs/ by creating PR, # Can visit my github https://github.com/CrossJoy/Bombsquad-Modding # You can contact me through discord: # My Discord Id: Cross Joy#0721 # My BS Discord Server: https://discord.gg/JyBY6haARJ # ---------------------------------------------------------------------------- # V2 What's new? # - The "Player can't fight each other" system is removed, # players exploiting the features and, I know ideas how to fix it especially # the freeze handlemessage # - Added new bot: Ice Bot # - The bot spawn location will be more randomize rather than based on players # position, I don't wanna players stay at the corner of the map. # - Some codes clean up. # ---------------------------------------------------------------------------- # ba_meta require api 8 from __future__ import annotations import random from dataclasses import dataclass from typing import TYPE_CHECKING import babase import bauiv1 as bui import bascenev1 as bs from bascenev1lib.actor.playerspaz import PlayerSpaz from bascenev1lib.actor.bomb import TNTSpawner from bascenev1lib.actor.onscreentimer import OnScreenTimer from bascenev1lib.actor.scoreboard import Scoreboard from bascenev1lib.actor.spazfactory import SpazFactory from bascenev1lib.actor.spazbot import (SpazBot, SpazBotSet, BomberBot, BomberBotPro, BomberBotProShielded, BrawlerBot, BrawlerBotPro, BrawlerBotProShielded, TriggerBot, TriggerBotPro, TriggerBotProShielded, ChargerBot, StickyBot, ExplodeyBot) if TYPE_CHECKING: from typing import Any, Sequence from bascenev1lib.actor.spazbot import SpazBot class IceBot(SpazBot): """A slow moving bot with ice bombs. category: Bot Classes """ character = 'Pascal' punchiness = 0.9 throwiness = 1 charge_speed_min = 1 charge_speed_max = 1 throw_dist_min = 5.0 throw_dist_max = 20 run = True charge_dist_min = 10.0 charge_dist_max = 11.0 default_bomb_type = 'ice' default_bomb_count = 1 points_mult = 3 class Icon(bs.Actor): """Creates in in-game icon on screen.""" def __init__(self, player: Player, position: tuple[float, float], scale: float, show_lives: bool = True, show_death: bool = True, name_scale: float = 1.0, name_maxwidth: float = 115.0, flatness: float = 1.0, shadow: float = 1.0): super().__init__() self._player = player self._show_lives = show_lives self._show_death = show_death self._name_scale = name_scale self._outline_tex = bs.gettexture('characterIconMask') icon = player.get_icon() self.node = bs.newnode('image', delegate=self, attrs={ 'texture': icon['texture'], 'tint_texture': icon['tint_texture'], 'tint_color': icon['tint_color'], 'vr_depth': 400, 'tint2_color': icon['tint2_color'], 'mask_texture': self._outline_tex, 'opacity': 1.0, 'absolute_scale': True, 'attach': 'bottomCenter' }) self._name_text = bs.newnode( 'text', owner=self.node, attrs={ 'text': babase.Lstr(value=player.getname()), 'color': babase.safecolor(player.team.color), 'h_align': 'center', 'v_align': 'center', 'vr_depth': 410, 'maxwidth': name_maxwidth, 'shadow': shadow, 'flatness': flatness, 'h_attach': 'center', 'v_attach': 'bottom' }) if self._show_lives: self._lives_text = bs.newnode('text', owner=self.node, attrs={ 'text': 'x0', 'color': (1, 1, 0.5), 'h_align': 'left', 'vr_depth': 430, 'shadow': 1.0, 'flatness': 1.0, 'h_attach': 'center', 'v_attach': 'bottom' }) self.set_position_and_scale(position, scale) def set_position_and_scale(self, position: tuple[float, float], scale: float) -> None: """(Re)position the icon.""" assert self.node self.node.position = position self.node.scale = [70.0 * scale] self._name_text.position = (position[0], position[1] + scale * 52.0) self._name_text.scale = 1.0 * scale * self._name_scale if self._show_lives: self._lives_text.position = (position[0] + scale * 10.0, position[1] - scale * 43.0) self._lives_text.scale = 1.0 * scale def update_for_lives(self) -> None: """Update for the target player's current lives.""" if self._player: lives = self._player.lives else: lives = 0 if self._show_lives: if lives > 0: self._lives_text.text = 'x' + str(lives - 1) else: self._lives_text.text = '' if lives == 0: self._name_text.opacity = 0.2 assert self.node self.node.color = (0.7, 0.3, 0.3) self.node.opacity = 0.2 def handle_player_spawned(self) -> None: """Our player spawned; hooray!""" if not self.node: return self.node.opacity = 1.0 self.update_for_lives() def handle_player_died(self) -> None: """Well poo; our player died.""" if not self.node: return if self._show_death: bs.animate( self.node, 'opacity', { 0.00: 1.0, 0.05: 0.0, 0.10: 1.0, 0.15: 0.0, 0.20: 1.0, 0.25: 0.0, 0.30: 1.0, 0.35: 0.0, 0.40: 1.0, 0.45: 0.0, 0.50: 1.0, 0.55: 0.2 }) lives = self._player.lives if lives == 0: bs.timer(0.6, self.update_for_lives) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.DieMessage): self.node.delete() return None return super().handlemessage(msg) @dataclass class SpawnInfo: """Spawning info for a particular bot type.""" spawnrate: float increase: float dincrease: float class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: super().__init__() self.death_time: float | None = None self.lives = 0 self.icons: list[Icon] = [] class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: self.survival_seconds: int | None = None self.spawn_order: list[Player] = [] # ba_meta export bascenev1.GameActivity class UltimateLastStand(bs.TeamGameActivity[Player, Team]): """Minigame involving dodging falling bombs.""" name = 'Ultimate Last Stand' description = 'Only the strongest will stand at the end.' scoreconfig = bs.ScoreConfig(label='Survived', scoretype=bs.ScoreType.SECONDS, none_is_winner=True) # Print messages when players die (since its meaningful in this game). announce_player_deaths = True # Don't allow joining after we start # (would enable leave/rejoin tomfoolery). allow_mid_activity_joins = False @classmethod def get_available_settings( cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: settings = [ bs.IntSetting( 'Lives Per Player', default=1, min_value=1, max_value=10, increment=1, ), bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), bs.BoolSetting('Epic Mode', default=False), ] if issubclass(sessiontype, bs.DualTeamSession): settings.append( bs.BoolSetting('Balance Total Lives', default=False)) return settings # We're currently hard-coded for one map. @classmethod def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: return ['Rampage'] # We support teams, free-for-all, and co-op sessions. @classmethod def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: return (issubclass(sessiontype, bs.DualTeamSession) or issubclass(sessiontype, bs.FreeForAllSession)) def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._start_time: float | None = None self._vs_text: bs.Actor | None = None self._round_end_timer: bs.Timer | None = None self._lives_per_player = int(settings['Lives Per Player']) self._balance_total_lives = bool( settings.get('Balance Total Lives', False)) self._epic_mode = settings.get('Epic Mode', True) self._last_player_death_time: float | None = None self._timer: OnScreenTimer | None = None self._tntspawner: TNTSpawner | None = None self._new_wave_sound = bs.getsound('scoreHit01') self._bots = SpazBotSet() self._tntspawnpos = (0, 5.5, -6) self.spazList = [] # Base class overrides: self.slow_motion = self._epic_mode self.default_music = (bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL) self.node = bs.newnode('text', attrs={ 'v_attach': 'bottom', 'h_align': 'center', 'color': (0.83, 0.69, 0.21), 'flatness': 0.5, 'shadow': 0.5, 'position': (0, 75), 'scale': 0.7, 'text': 'By Cross Joy' }) # For each bot type: [spawnrate, increase, d_increase] self._bot_spawn_types = { BomberBot: SpawnInfo(1.00, 0.00, 0.000), BomberBotPro: SpawnInfo(0.00, 0.05, 0.001), BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002), BrawlerBot: SpawnInfo(1.00, 0.00, 0.000), BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001), BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), TriggerBot: SpawnInfo(0.30, 0.00, 0.000), TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001), TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), ChargerBot: SpawnInfo(0.30, 0.05, 0.000), StickyBot: SpawnInfo(0.10, 0.03, 0.001), IceBot: SpawnInfo(0.10, 0.03, 0.001), ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002) } # yapf: disable # Some base class overrides: self.default_music = (bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL) if self._epic_mode: self.slow_motion = True def get_instance_description(self) -> str | Sequence: return 'Only the strongest team will stand at the end.' if isinstance( self.session, bs.DualTeamSession) else 'Only the strongest will stand at the end.' def get_instance_description_short(self) -> str | Sequence: return 'Only the strongest team will stand at the end.' if isinstance( self.session, bs.DualTeamSession) else 'Only the strongest will stand at the end.' def on_transition_in(self) -> None: super().on_transition_in() bs.timer(1.3, self._new_wave_sound.play) def on_player_join(self, player: Player) -> None: player.lives = self._lives_per_player # Don't waste time doing this until begin. player.icons = [Icon(player, position=(0, 50), scale=0.8)] if player.lives > 0: self.spawn_player(player) if self.has_begun(): self._update_icons() def on_begin(self) -> None: super().on_begin() bs.animate_array(node=self.node, attr='color', size=3, keys={ 0.0: (0.5, 0.5, 0.5), 0.8: (0.83, 0.69, 0.21), 1.6: (0.5, 0.5, 0.5) }, loop=True) bs.timer(0.001, bs.WeakCall(self._start_bot_updates)) self._tntspawner = TNTSpawner(position=self._tntspawnpos, respawn_time=10.0) self._timer = OnScreenTimer() self._timer.start() self.setup_standard_powerup_drops() # Check for immediate end (if we've only got 1 player, etc). self._start_time = bs.time() # If balance-team-lives is on, add lives to the smaller team until # total lives match. if (isinstance(self.session, bs.DualTeamSession) and self._balance_total_lives and self.teams[0].players and self.teams[1].players): if self._get_total_team_lives( self.teams[0]) < self._get_total_team_lives(self.teams[1]): lesser_team = self.teams[0] greater_team = self.teams[1] else: lesser_team = self.teams[1] greater_team = self.teams[0] add_index = 0 while (self._get_total_team_lives(lesser_team) < self._get_total_team_lives(greater_team)): lesser_team.players[add_index].lives += 1 add_index = (add_index + 1) % len(lesser_team.players) bs.timer(1.0, self._update, repeat=True) self._update_icons() # We could check game-over conditions at explicit trigger points, # but lets just do the simple thing and poll it. def _update_icons(self) -> None: # pylint: disable=too-many-branches # In free-for-all mode, everyone is just lined up along the bottom. if isinstance(self.session, bs.FreeForAllSession): count = len(self.teams) x_offs = 85 xval = x_offs * (count - 1) * -0.5 for team in self.teams: if len(team.players) == 1: player = team.players[0] for icon in player.icons: icon.set_position_and_scale((xval, 30), 0.7) icon.update_for_lives() xval += x_offs # In teams mode we split up teams. else: for team in self.teams: if team.id == 0: xval = -50 x_offs = -85 else: xval = 50 x_offs = 85 for player in team.players: for icon in player.icons: icon.set_position_and_scale((xval, 30), 0.7) icon.update_for_lives() xval += x_offs def on_player_leave(self, player: Player) -> None: # Augment default behavior. super().on_player_leave(player) player.icons = [] # Update icons in a moment since our team will be gone from the # list then. bs.timer(0, self._update_icons) # If the player to leave was the last in spawn order and had # their final turn currently in-progress, mark the survival time # for their team. if self._get_total_team_lives(player.team) == 0: assert self._start_time is not None player.team.survival_seconds = int(bs.time() - self._start_time) # A departing player may trigger game-over. # overriding the default character spawning.. def spawn_player(self, player: Player) -> bs.Actor: actor = self.spawn_player_spaz(player) bs.timer(0.3, babase.Call(self._print_lives, player)) # If we have any icons, update their state. for icon in player.icons: icon.handle_player_spawned() return actor def _print_lives(self, player: Player) -> None: from bascenev1lib.actor import popuptext # We get called in a timer so it's possible our player has left/etc. if not player or not player.is_alive() or not player.node: return popuptext.PopupText('x' + str(player.lives - 1), color=(1, 1, 0, 1), offset=(0, -0.8, 0), random_offset=0.0, scale=1.8, position=player.node.position).autoretain() def _get_total_team_lives(self, team: Team) -> int: return sum(player.lives for player in team.players) def _start_bot_updates(self) -> None: self._bot_update_interval = 3.3 - 0.3 * (len(self.players)) self._update_bots() self._update_bots() if len(self.players) > 2: self._update_bots() if len(self.players) > 3: self._update_bots() self._bot_update_timer = bs.Timer(self._bot_update_interval, bs.WeakCall(self._update_bots)) def _update_bots(self) -> None: assert self._bot_update_interval is not None self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98) self._bot_update_timer = bs.Timer(self._bot_update_interval, bs.WeakCall(self._update_bots)) botspawnpts: list[Sequence[float]] = [[-5.0, 5.5, -4.14], [0.0, 5.5, -4.14], [5.0, 5.5, -4.14]] for player in self.players: try: if player.is_alive(): assert isinstance(player.actor, PlayerSpaz) assert player.actor.node except Exception: babase.print_exception('Error updating bots.') spawnpt = random.choice( [botspawnpts[0], botspawnpts[1], botspawnpts[2]]) spawnpt = (spawnpt[0] + 3.0 * (random.random() - 0.5), spawnpt[1], 2.0 * (random.random() - 0.5) + spawnpt[2]) # Normalize our bot type total and find a random number within that. total = 0.0 for spawninfo in self._bot_spawn_types.values(): total += spawninfo.spawnrate randval = random.random() * total # Now go back through and see where this value falls. total = 0 bottype: type[SpazBot] | None = None for spawntype, spawninfo in self._bot_spawn_types.items(): total += spawninfo.spawnrate if randval <= total: bottype = spawntype break spawn_time = 1.0 assert bottype is not None self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time) # After every spawn we adjust our ratios slightly to get more # difficult. for spawninfo in self._bot_spawn_types.values(): spawninfo.spawnrate += spawninfo.increase spawninfo.increase += spawninfo.dincrease # Various high-level game events come through this method. def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) curtime = bs.time() # Record the player's moment of death. # assert isinstance(msg.spaz.player msg.getplayer(Player).death_time = curtime player: Player = msg.getplayer(Player) player.lives -= 1 if player.lives < 0: player.lives = 0 # If we have any icons, update their state. for icon in player.icons: icon.handle_player_died() # Play big death sound on our last death # or for every one in solo mode. if player.lives == 0: SpazFactory.get().single_player_death_sound.play() # If we hit zero lives, we're dead (and our team might be too). if player.lives == 0: # If the whole team is now dead, mark their survival time. if self._get_total_team_lives(player.team) == 0: assert self._start_time is not None player.team.survival_seconds = int(bs.time() - self._start_time) else: # Otherwise, in regular mode, respawn. self.respawn_player(player) def _get_living_teams(self) -> list[Team]: return [ team for team in self.teams if len(team.players) > 0 and any(player.lives > 0 for player in team.players) ] def _update(self) -> None: # If we're down to 1 or fewer living teams, start a timer to end # the game (allows the dust to settle and draws to occur if deaths # are close enough). if len(self._get_living_teams()) < 2: self._round_end_timer = bs.Timer(0.5, self.end_game) def end_game(self) -> None: # Stop updating our time text, and set the final time to match # exactly when our last guy died. self._timer.stop(endtime=self._last_player_death_time) # Ok now calc game results: set a score for each team and then tell # the game to end. results = bs.GameResults() # Remember that 'free-for-all' mode is simply a special form # of 'teams' mode where each player gets their own team, so we can # just always deal in teams and have all cases covered. for team in self.teams: # Submit the score value in milliseconds. results.set_team_score(team, team.survival_seconds) self.end(results=results)