Source code for bascenev1lib.game.assault

# Released under the MIT License. See LICENSE for details.
#
"""Defines assault minigame."""

# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)

from __future__ import annotations

import random
from typing import TYPE_CHECKING, override

import bascenev1 as bs

from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.flag import Flag
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.gameutils import SharedObjects

if TYPE_CHECKING:
    from typing import Any, Sequence


[docs] class Player(bs.Player['Team']): """Our player type for this game."""
[docs] class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self, base_pos: Sequence[float], flag: Flag) -> None: self.base_pos = base_pos self.flag = flag self.score = 0
# ba_meta export bascenev1.GameActivity
[docs] class AssaultGame(bs.TeamGameActivity[Player, Team]): """Game where you score by touching the other team's flag.""" name = 'Assault' description = 'Reach the enemy flag to score.' available_settings = [ bs.IntSetting( 'Score to Win', min_value=1, default=3, ), bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), bs.BoolSetting('Epic Mode', default=False), ]
[docs] @override @classmethod def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: return issubclass(sessiontype, bs.DualTeamSession)
[docs] @override @classmethod def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: assert bs.app.classic is not None return bs.app.classic.getmaps('team_flag')
def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._last_score_time = 0.0 self._score_sound = bs.getsound('score') self._base_region_materials: dict[int, bs.Material] = {} self._epic_mode = bool(settings['Epic Mode']) self._score_to_win = int(settings['Score to Win']) self._time_limit = float(settings['Time Limit']) # Base class overrides self.slow_motion = self._epic_mode self.default_music = ( bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH )
[docs] @override def get_instance_description(self) -> str | Sequence: if self._score_to_win == 1: return 'Touch the enemy flag.' return 'Touch the enemy flag ${ARG1} times.', self._score_to_win
[docs] @override def get_instance_description_short(self) -> str | Sequence: if self._score_to_win == 1: return 'touch 1 flag' return 'touch ${ARG1} flags', self._score_to_win
[docs] @override def create_team(self, sessionteam: bs.SessionTeam) -> Team: shared = SharedObjects.get() base_pos = self.map.get_flag_position(sessionteam.id) bs.newnode( 'light', attrs={ 'position': base_pos, 'intensity': 0.6, 'height_attenuated': False, 'volume_intensity_scale': 0.1, 'radius': 0.1, 'color': sessionteam.color, }, ) Flag.project_stand(base_pos) flag = Flag(touchable=False, position=base_pos, color=sessionteam.color) team = Team(base_pos=base_pos, flag=flag) mat = self._base_region_materials[sessionteam.id] = bs.Material() mat.add_actions( conditions=('they_have_material', shared.player_material), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ( 'call', 'at_connect', bs.Call(self._handle_base_collide, team), ), ), ) bs.newnode( 'region', owner=flag.node, attrs={ 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), 'scale': (0.5, 0.5, 0.5), 'type': 'sphere', 'materials': [self._base_region_materials[sessionteam.id]], }, ) return team
[docs] @override def on_team_join(self, team: Team) -> None: # Can't do this in create_team because the team's color/etc. have # not been wired up yet at that point. self._update_scoreboard()
[docs] @override def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops()
[docs] @override def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.PlayerDiedMessage): super().handlemessage(msg) # Augment standard. self.respawn_player(msg.getplayer(Player)) else: super().handlemessage(msg)
def _flash_base(self, team: Team, length: float = 2.0) -> None: light = bs.newnode( 'light', attrs={ 'position': team.base_pos, 'height_attenuated': False, 'radius': 0.3, 'color': team.color, }, ) bs.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) bs.timer(length, light.delete) def _handle_base_collide(self, team: Team) -> None: try: spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True) except bs.NotFoundError: return if not spaz.is_alive(): return try: player = spaz.getplayer(Player, True) except bs.NotFoundError: return # If its another team's player, they scored. player_team = player.team if player_team is not team: # Prevent multiple simultaneous scores. if bs.time() != self._last_score_time: self._last_score_time = bs.time() self.stats.player_scored(player, 50, big_message=True) self._score_sound.play() self._flash_base(team) # Move all players on the scoring team back to their start # and add flashes of light so its noticeable. for player in player_team.players: if player.is_alive(): pos = player.node.position light = bs.newnode( 'light', attrs={ 'position': pos, 'color': player_team.color, 'height_attenuated': False, 'radius': 0.4, }, ) bs.timer(0.5, light.delete) bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) new_pos = self.map.get_start_position(player_team.id) light = bs.newnode( 'light', attrs={ 'position': new_pos, 'color': player_team.color, 'radius': 0.4, 'height_attenuated': False, }, ) bs.timer(0.5, light.delete) bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) if player.actor: random_num = random.uniform(0, 360) # Slightly hacky workaround: normally, # teleporting back to base with a sticky # bomb stuck to you gives a crazy whiplash # rubber-band effect. Running the teleport # twice in a row seems to suppress that # though. Would be better to fix this at a # lower level, but this works for now. self._teleport(player, new_pos, random_num) bs.timer( 0.01, bs.Call( self._teleport, player, new_pos, random_num ), ) # Have teammates celebrate. for player in player_team.players: if player.actor: player.actor.handlemessage(bs.CelebrateMessage(2.0)) player_team.score += 1 self._update_scoreboard() if player_team.score >= self._score_to_win: self.end_game() def _teleport( self, client: Player, pos: Sequence[float], num: float ) -> None: if client.actor: client.actor.handlemessage(bs.StandMessage(pos, num))
[docs] @override def end_game(self) -> None: results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results)
def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value( team, team.score, self._score_to_win )