# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations from typing import TYPE_CHECKING import babase import bauiv1 as bui import bascenev1 as bs import random from bascenev1lib.actor.onscreentimer import OnScreenTimer from bascenev1lib.actor.spaz import BombDiedMessage from bascenev1lib.actor.playerspaz import PlayerSpaz from bascenev1lib.actor import bomb as stdbomb if TYPE_CHECKING: from typing import Any, Sequence lang = bs.app.lang.language if lang == 'Spanish': name = 'Bomba en mi Cabeza' description = ('Siempre tendrás una bomba en la cabeza.\n' '¡Sobrevive tanto como puedas!') description_ingame = '¡Sobrevive tanto como puedas!' # description_short = 'Elimina {} Jugadores para ganar' maxbomblimit = 'Límite Máximo de Bombas' mbltwo = 'Dos' mblthree = 'Tres' mblfour = 'Cuatro' else: name = 'Bomb on my Head' description = ('You\'ll always have a bomb on your head.\n' 'Survive as long as you can!') description_ingame = 'Survive as long as you can!' # description_short = 'Kill {} Players to win' maxbomblimit = 'Max Bomb Limit' mbltwo = 'Two' mblthree = 'Three' mblfour = 'Four' class NewPlayerSpaz(PlayerSpaz): def handlemessage(self, msg: Any) -> Any: if isinstance(msg, BombDiedMessage): self.bomb_count += 1 self.check_avalible_bombs() else: return super().handlemessage(msg) def check_avalible_bombs(self) -> None: if not self.node: return if self.bomb_count <= 0: return if not self.node.hold_node: self.on_bomb_press() self.on_bomb_release() def start_bomb_checking(self) -> None: self.check_avalible_bombs() self._bomb_check_timer = bs.timer( 0.5, bs.WeakCall(self.check_avalible_bombs), repeat=True) def drop_bomb(self) -> stdbomb.Bomb | None: lifespan = 3.0 if self.bomb_count <= 0 or self.frozen: return None assert self.node pos = self.node.position_forward vel = self.node.velocity bomb_type = 'normal' bomb = stdbomb.Bomb( position=(pos[0], pos[1] - 0.0, pos[2]), velocity=(vel[0], vel[1], vel[2]), bomb_type=bomb_type, blast_radius=self.blast_radius, source_player=self.source_player, owner=self.node, ).autoretain() bs.animate(bomb.node, 'mesh_scale', { 0.0: 0.0, lifespan*0.1: 1.5, lifespan*0.5: 1.0 }) self.bomb_count -= 1 bomb.node.add_death_action( bs.WeakCall(self.handlemessage, BombDiedMessage()) ) self._pick_up(bomb.node) for clb in self._dropped_bomb_callbacks: clb(self, bomb) return bomb class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: super().__init__() self.death_time: float | None = None class Team(bs.Team[Player]): """Our team type for this game.""" # ba_meta export bascenev1.GameActivity class BombOnMyHeadGame(bs.TeamGameActivity[Player, Team]): name = name description = description scoreconfig = bs.ScoreConfig( label='Survived', scoretype=bs.ScoreType.MILLISECONDS, version='B' ) # Show messages when players die since it's meaningful here. announce_player_deaths = True allow_mid_activity_joins = False @classmethod def get_available_settings( cls, sessiontype: type[bs.Session] ) -> list[babase.Setting]: settings = [ bs.IntChoiceSetting( maxbomblimit, choices=[ ('Normal', 1), (mbltwo, 2), (mblthree, 3), (mblfour, 4), ], default=1, ), bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), bs.BoolSetting('Epic Mode', default=False), ] return settings @classmethod def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: return issubclass(sessiontype, bs.DualTeamSession) or issubclass( sessiontype, bs.FreeForAllSession ) @classmethod def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: return bs.app.classic.getmaps('melee') def __init__(self, settings: dict): super().__init__(settings) self._max_bomb_limit = int(settings[maxbomblimit]) self._epic_mode = bool(settings['Epic Mode']) self._time_limit = float(settings['Time Limit']) self._last_player_death_time: float | None = None self._timer: OnScreenTimer | None = None # Some base class overrides: self.default_music = ( bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL ) if self._epic_mode: self.slow_motion = True def get_instance_description(self) -> str | Sequence: return description_ingame def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self._timer = OnScreenTimer() self._timer.start() def spawn_player(self, player: Player) -> bs.Actor: from babase import _math from bascenev1._gameutils import animate from bascenev1._coopsession import CoopSession if isinstance(self.session, bs.DualTeamSession): position = self.map.get_start_position(player.team.id) else: # otherwise do free-for-all spawn locations position = self.map.get_ffa_start_position(self.players) angle = None name = player.getname() color = player.color highlight = player.highlight light_color = _math.normalized_color(color) display_color = babase.safecolor(color, target_intensity=0.75) spaz = NewPlayerSpaz(color=color, highlight=highlight, character=player.character, player=player) player.actor = spaz assert spaz.node spaz.node.name = name spaz.node.name_color = display_color spaz.connect_controls_to_player() # Move to the stand position and add a flash of light. spaz.handlemessage( bs.StandMessage( position, angle if angle is not None else random.uniform(0, 360))) self._spawn_sound.play(1, position=spaz.node.position) light = bs.newnode('light', attrs={'color': light_color}) spaz.node.connectattr('position', light, 'position') animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) bs.timer(0.5, light.delete) bs.timer(1.0, bs.WeakCall(spaz.start_bomb_checking)) spaz.set_bomb_count(self._max_bomb_limit) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) curtime = bs.time() # Record the player's moment of death. # assert isinstance(msg.spaz.player msg.getplayer(Player).death_time = curtime # In co-op mode, end the game the instant everyone dies # (more accurate looking). # In teams/ffa, allow a one-second fudge-factor so we can # get more draws if players die basically at the same time. if isinstance(self.session, bs.CoopSession): # Teams will still show up if we check now.. check in # the next cycle. babase.pushcall(self._check_end_game) # Also record this for a final setting of the clock. self._last_player_death_time = curtime else: bs.timer(1.0, self._check_end_game) else: # Default handler: return super().handlemessage(msg) return None def _check_end_game(self) -> None: living_team_count = 0 for team in self.teams: for player in team.players: if player.is_alive(): living_team_count += 1 break # In co-op, we go till everyone is dead.. otherwise we go # until one team remains. if isinstance(self.session, bs.CoopSession): if living_team_count <= 0: self.end_game() else: if living_team_count <= 1: self.end_game() def end_game(self) -> None: cur_time = bs.time() assert self._timer is not None start_time = self._timer.getstarttime() # Mark death-time as now for any still-living players # and award players points for how long they lasted. # (these per-player scores are only meaningful in team-games) for team in self.teams: for player in team.players: survived = False # Throw an extra fudge factor in so teams that # didn't die come out ahead of teams that did. if player.death_time is None: survived = True player.death_time = cur_time + 1 # Award a per-player score depending on how many seconds # they lasted (per-player scores only affect teams mode; # everywhere else just looks at the per-team score). score = int(player.death_time - self._timer.getstarttime()) if survived: score += 50 # A bit extra for survivors. self.stats.player_scored(player, score, screenmessage=False) # Stop updating our time text, and set the final time to match # exactly when our last guy died. self._timer.stop(endtime=self._last_player_death_time) # Ok now calc game results: set a score for each team and then tell # the game to end. results = bs.GameResults() # Remember that 'free-for-all' mode is simply a special form # of 'teams' mode where each player gets their own team, so we can # just always deal in teams and have all cases covered. for team in self.teams: # Set the team score to the max time survived by any player on # that team. longest_life = 0.0 for player in team.players: assert player.death_time is not None longest_life = max(longest_life, player.death_time - start_time) # Submit the score value in milliseconds. results.set_team_score(team, int(1000.0 * longest_life)) self.end(results=results)