Source code for bascenev1lib.game.football

# Released under the MIT License. See LICENSE for details.
#
"""Implements football games (both co-op and teams varieties)."""

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

from __future__ import annotations

import math
import random
import logging
from typing import TYPE_CHECKING, override

import bascenev1 as bs

from bascenev1lib.actor.bomb import TNTSpawner
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.respawnicon import RespawnIcon
from bascenev1lib.actor.powerupbox import PowerupBoxFactory, PowerupBox
from bascenev1lib.actor.flag import (
    FlagFactory,
    Flag,
    FlagPickedUpMessage,
    FlagDroppedMessage,
    FlagDiedMessage,
)
from bascenev1lib.actor.spazbot import (
    SpazBotDiedMessage,
    SpazBotPunchedMessage,
    SpazBotSet,
    BrawlerBotLite,
    BrawlerBot,
    BomberBotLite,
    BomberBot,
    TriggerBot,
    ChargerBot,
    TriggerBotPro,
    BrawlerBotPro,
    StickyBot,
    ExplodeyBot,
)

if TYPE_CHECKING:
    from typing import Any, Sequence

    from bascenev1lib.actor.spaz import Spaz
    from bascenev1lib.actor.spazbot import SpazBot


[docs] class FootballFlag(Flag): """Custom flag class for football games.""" def __init__(self, position: Sequence[float]): super().__init__( position=position, dropped_timeout=20, color=(1.0, 1.0, 0.3) ) assert self.node self.last_holding_player: bs.Player | None = None self.node.is_area_of_interest = True self.respawn_timer: bs.Timer | None = None self.scored = False self.held_count = 0 self.light = bs.newnode( 'light', owner=self.node, attrs={ 'intensity': 0.25, 'height_attenuated': False, 'radius': 0.2, 'color': (0.9, 0.7, 0.0), }, ) self.node.connectattr('position', self.light, 'position')
[docs] class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: self.respawn_timer: bs.Timer | None = None self.respawn_icon: RespawnIcon | None = None
[docs] class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: self.score = 0
# ba_meta export bascenev1.GameActivity
[docs] class FootballTeamGame(bs.TeamGameActivity[Player, Team]): """Football game for teams mode.""" name = 'Football' description = 'Get the flag to the enemy end zone.' available_settings = [ bs.IntSetting( 'Score to Win', min_value=7, default=21, increment=7, ), 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: # We only support two-team play. 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('football')
def __init__(self, settings: dict): super().__init__(settings) self._scoreboard: Scoreboard | None = Scoreboard() # Load some media we need. self._cheer_sound = bs.getsound('cheer') self._chant_sound = bs.getsound('crowdChant') self._score_sound = bs.getsound('score') self._swipsound = bs.getsound('swip') self._whistle_sound = bs.getsound('refWhistle') self._score_region_material = bs.Material() self._score_region_material.add_actions( conditions=('they_have_material', FlagFactory.get().flagmaterial), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_score), ), ) self._flag_spawn_pos: Sequence[float] | None = None self._score_regions: list[bs.NodeActor] = [] self._flag: FootballFlag | None = None self._flag_respawn_timer: bs.Timer | None = None self._flag_respawn_light: bs.NodeActor | None = None self._score_to_win = int(settings['Score to Win']) self._time_limit = float(settings['Time Limit']) self._epic_mode = bool(settings['Epic Mode']) self.slow_motion = self._epic_mode self.default_music = ( bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FOOTBALL )
[docs] @override def get_instance_description(self) -> str | Sequence: touchdowns = self._score_to_win / 7 # NOTE: if use just touchdowns = self._score_to_win // 7 # and we will need to score, for example, 27 points, # we will be required to score 3 (not 4) goals .. touchdowns = math.ceil(touchdowns) if touchdowns > 1: return 'Score ${ARG1} touchdowns.', touchdowns return 'Score a touchdown.'
[docs] @override def get_instance_description_short(self) -> str | Sequence: touchdowns = self._score_to_win / 7 touchdowns = math.ceil(touchdowns) if touchdowns > 1: return 'score ${ARG1} touchdowns', touchdowns return 'score a touchdown'
[docs] @override def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._flag_spawn_pos = self.map.get_flag_position(None) self._spawn_flag() defs = self.map.defs self._score_regions.append( bs.NodeActor( bs.newnode( 'region', attrs={ 'position': defs.boxes['goal1'][0:3], 'scale': defs.boxes['goal1'][6:9], 'type': 'box', 'materials': (self._score_region_material,), }, ) ) ) self._score_regions.append( bs.NodeActor( bs.newnode( 'region', attrs={ 'position': defs.boxes['goal2'][0:3], 'scale': defs.boxes['goal2'][6:9], 'type': 'box', 'materials': (self._score_region_material,), }, ) ) ) self._update_scoreboard() self._chant_sound.play()
[docs] @override def on_team_join(self, team: Team) -> None: self._update_scoreboard()
def _kill_flag(self) -> None: self._flag = None def _handle_score(self) -> None: """A point has been scored.""" # Our flag might stick around for a second or two # make sure it doesn't score again. assert self._flag is not None if self._flag.scored: return region = bs.getcollision().sourcenode i = None for i, score_region in enumerate(self._score_regions): if region == score_region.node: break for team in self.teams: if team.id == i: team.score += 7 # Tell all players to celebrate. for player in team.players: if player.actor: player.actor.handlemessage(bs.CelebrateMessage(2.0)) # If someone on this team was last to touch it, # give them points. assert self._flag is not None if ( self._flag.last_holding_player and team == self._flag.last_holding_player.team ): self.stats.player_scored( self._flag.last_holding_player, 50, big_message=True ) # End the game if we won. if team.score >= self._score_to_win: self.end_game() self._score_sound.play() self._cheer_sound.play() assert self._flag self._flag.scored = True # Kill the flag (it'll respawn shortly). bs.timer(1.0, self._kill_flag) light = bs.newnode( 'light', attrs={ 'position': bs.getcollision().position, 'height_attenuated': False, 'color': (1, 0, 0), }, ) bs.animate(light, 'intensity', {0.0: 0, 0.5: 1, 1.0: 0}, loop=True) bs.timer(1.0, light.delete) bs.cameraflash(duration=10.0) self._update_scoreboard()
[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, announce_delay=0.8)
def _update_scoreboard(self) -> None: assert self._scoreboard is not None for team in self.teams: self._scoreboard.set_team_value( team, team.score, self._score_to_win )
[docs] @override def handlemessage(self, msg: Any) -> Any: if isinstance(msg, FlagPickedUpMessage): assert isinstance(msg.flag, FootballFlag) try: msg.flag.last_holding_player = msg.node.getdelegate( PlayerSpaz, True ).getplayer(Player, True) except bs.NotFoundError: pass msg.flag.held_count += 1 elif isinstance(msg, FlagDroppedMessage): assert isinstance(msg.flag, FootballFlag) msg.flag.held_count -= 1 # Respawn dead players if they're still in the game. elif isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) self.respawn_player(msg.getplayer(Player)) # Respawn dead flags. elif isinstance(msg, FlagDiedMessage): if not self.has_ended(): self._flag_respawn_timer = bs.Timer(3.0, self._spawn_flag) self._flag_respawn_light = bs.NodeActor( bs.newnode( 'light', attrs={ 'position': self._flag_spawn_pos, 'height_attenuated': False, 'radius': 0.15, 'color': (1.0, 1.0, 0.3), }, ) ) assert self._flag_respawn_light.node bs.animate( self._flag_respawn_light.node, 'intensity', {0.0: 0, 0.25: 0.15, 0.5: 0}, loop=True, ) bs.timer(3.0, self._flag_respawn_light.node.delete) else: # Augment standard behavior. super().handlemessage(msg)
def _flash_flag_spawn(self) -> None: light = bs.newnode( 'light', attrs={ 'position': self._flag_spawn_pos, 'height_attenuated': False, 'color': (1, 1, 0), }, ) bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) bs.timer(1.0, light.delete) def _spawn_flag(self) -> None: self._swipsound.play() self._whistle_sound.play() self._flash_flag_spawn() assert self._flag_spawn_pos is not None self._flag = FootballFlag(position=self._flag_spawn_pos)
[docs] class FootballCoopGame(bs.CoopGameActivity[Player, Team]): """Co-op variant of football.""" name = 'Football' tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] scoreconfig = bs.ScoreConfig( scoretype=bs.ScoreType.MILLISECONDS, version='B' ) default_music = bs.MusicType.FOOTBALL # FIXME: Need to update co-op games to use getscoreconfig.
[docs] @override def get_score_type(self) -> str: return 'time'
[docs] @override def get_instance_description(self) -> str | Sequence: touchdowns = self._score_to_win / 7 touchdowns = math.ceil(touchdowns) if touchdowns > 1: return 'Score ${ARG1} touchdowns.', touchdowns return 'Score a touchdown.'
[docs] @override def get_instance_description_short(self) -> str | Sequence: touchdowns = self._score_to_win / 7 touchdowns = math.ceil(touchdowns) if touchdowns > 1: return 'score ${ARG1} touchdowns', touchdowns return 'score a touchdown'
def __init__(self, settings: dict): settings['map'] = 'Football Stadium' super().__init__(settings) self._preset = settings.get('preset', 'rookie') # Load some media we need. self._cheer_sound = bs.getsound('cheer') self._boo_sound = bs.getsound('boo') self._chant_sound = bs.getsound('crowdChant') self._score_sound = bs.getsound('score') self._swipsound = bs.getsound('swip') self._whistle_sound = bs.getsound('refWhistle') self._score_to_win = 21 self._score_region_material = bs.Material() self._score_region_material.add_actions( conditions=('they_have_material', FlagFactory.get().flagmaterial), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_score), ), ) self._powerup_center = (0, 2, 0) self._powerup_spread = (10, 5.5) self._player_has_dropped_bomb = False self._player_has_punched = False self._scoreboard: Scoreboard | None = None self._flag_spawn_pos: Sequence[float] | None = None self._score_regions: list[bs.NodeActor] = [] self._exclude_powerups: list[str] = [] self._have_tnt = False self._bot_types_initial: list[type[SpazBot]] | None = None self._bot_types_7: list[type[SpazBot]] | None = None self._bot_types_14: list[type[SpazBot]] | None = None self._bot_team: Team | None = None self._starttime_ms: int | None = None self._time_text: bs.NodeActor | None = None self._time_text_input: bs.NodeActor | None = None self._tntspawner: TNTSpawner | None = None self._bots = SpazBotSet() self._bot_spawn_timer: bs.Timer | None = None self._powerup_drop_timer: bs.Timer | None = None self._scoring_team: Team | None = None self._final_time_ms: int | None = None self._time_text_timer: bs.Timer | None = None self._flag_respawn_light: bs.Actor | None = None self._flag: FootballFlag | None = None
[docs] @override def on_transition_in(self) -> None: super().on_transition_in() self._scoreboard = Scoreboard() self._flag_spawn_pos = self.map.get_flag_position(None) self._spawn_flag() # Set up the two score regions. defs = self.map.defs self._score_regions.append( bs.NodeActor( bs.newnode( 'region', attrs={ 'position': defs.boxes['goal1'][0:3], 'scale': defs.boxes['goal1'][6:9], 'type': 'box', 'materials': [self._score_region_material], }, ) ) ) self._score_regions.append( bs.NodeActor( bs.newnode( 'region', attrs={ 'position': defs.boxes['goal2'][0:3], 'scale': defs.boxes['goal2'][6:9], 'type': 'box', 'materials': [self._score_region_material], }, ) ) ) self._chant_sound.play()
[docs] @override def on_begin(self) -> None: # FIXME: Split this up a bit. # pylint: disable=too-many-statements from bascenev1lib.actor import controlsguide super().on_begin() # Show controls help in demo or arcade mode. if bs.app.env.demo or bs.app.env.arcade: controlsguide.ControlsGuide( delay=3.0, lifespan=10.0, bright=True ).autoretain() assert self.initialplayerinfos is not None abot: type[SpazBot] bbot: type[SpazBot] cbot: type[SpazBot] if self._preset in ['rookie', 'rookie_easy']: self._exclude_powerups = ['curse'] self._have_tnt = False abot = ( BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot ) self._bot_types_initial = [abot] * len(self.initialplayerinfos) bbot = BomberBotLite if self._preset == 'rookie_easy' else BomberBot self._bot_types_7 = [bbot] * ( 1 if len(self.initialplayerinfos) < 3 else 2 ) cbot = BomberBot if self._preset == 'rookie_easy' else TriggerBot self._bot_types_14 = [cbot] * ( 1 if len(self.initialplayerinfos) < 3 else 2 ) elif self._preset == 'tournament': self._exclude_powerups = [] self._have_tnt = True self._bot_types_initial = [BrawlerBot] * ( 1 if len(self.initialplayerinfos) < 2 else 2 ) self._bot_types_7 = [TriggerBot] * ( 1 if len(self.initialplayerinfos) < 3 else 2 ) self._bot_types_14 = [ChargerBot] * ( 1 if len(self.initialplayerinfos) < 4 else 2 ) elif self._preset in ['pro', 'pro_easy', 'tournament_pro']: self._exclude_powerups = ['curse'] self._have_tnt = True self._bot_types_initial = [ChargerBot] * len( self.initialplayerinfos ) abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite typed_bot_list: list[type[SpazBot]] = [] self._bot_types_7 = ( typed_bot_list + [abot] + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2) ) bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot self._bot_types_14 = [bbot] * ( 1 if len(self.initialplayerinfos) < 3 else 2 ) elif self._preset in ['uber', 'uber_easy']: self._exclude_powerups = [] self._have_tnt = True abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot typed_bot_list_2: list[type[SpazBot]] = [] self._bot_types_initial = ( typed_bot_list_2 + [StickyBot] + [abot] * len(self.initialplayerinfos) ) self._bot_types_7 = [bbot] * ( 1 if len(self.initialplayerinfos) < 3 else 2 ) self._bot_types_14 = [ExplodeyBot] * ( 1 if len(self.initialplayerinfos) < 3 else 2 ) else: raise RuntimeError() self.setup_low_life_warning_sound() self._drop_powerups(standard_points=True) bs.timer(4.0, self._start_powerup_drops) # Make a bogus team for our bots. bad_team_name = self.get_team_display_string('Bad Guys') self._bot_team = Team() self._bot_team.manual_init( team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4) ) for team in [self.teams[0], self._bot_team]: team.score = 0 self.update_scores() # Time display. starttime_ms = int(bs.time() * 1000.0) assert isinstance(starttime_ms, int) self._starttime_ms = starttime_ms self._time_text = bs.NodeActor( bs.newnode( 'text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'color': (1, 1, 0.5, 1), 'flatness': 0.5, 'shadow': 0.5, 'position': (0, -50), 'scale': 1.3, 'text': '', }, ) ) self._time_text_input = bs.NodeActor( bs.newnode('timedisplay', attrs={'showsubseconds': True}) ) self.globalsnode.connectattr( 'time', self._time_text_input.node, 'time2' ) assert self._time_text_input.node assert self._time_text.node self._time_text_input.node.connectattr( 'output', self._time_text.node, 'text' ) # Our TNT spawner (if applicable). if self._have_tnt: self._tntspawner = TNTSpawner(position=(0, 1, -1)) self._bots = SpazBotSet() self._bot_spawn_timer = bs.Timer(1.0, self._update_bots, repeat=True) for bottype in self._bot_types_initial: self._spawn_bot(bottype)
def _on_bot_spawn(self, spaz: SpazBot) -> None: # We want to move to the left by default. spaz.target_point_default = bs.Vec3(0, 0, 0) def _spawn_bot( self, spaz_type: type[SpazBot], immediate: bool = False ) -> None: assert self._bot_team is not None pos = self.map.get_start_position(self._bot_team.id) self._bots.spawn_bot( spaz_type, pos=pos, spawn_time=0.001 if immediate else 3.0, on_spawn_call=self._on_bot_spawn, ) def _update_bots(self) -> None: bots = self._bots.get_living_bots() for bot in bots: bot.target_flag = None # If we've got a flag and no player are holding it, find the closest # bot to it, and make them the designated flag-bearer. assert self._flag is not None if self._flag.node: for player in self.players: if player.actor: assert isinstance(player.actor, PlayerSpaz) if ( player.actor.is_alive() and player.actor.node.hold_node == self._flag.node ): return flagpos = bs.Vec3(self._flag.node.position) closest_bot: SpazBot | None = None closest_dist = 0.0 # Always gets assigned first time through. for bot in bots: # If a bot is picked up, he should forget about the flag. if bot.held_count > 0: continue assert bot.node botpos = bs.Vec3(bot.node.position) botdist = (botpos - flagpos).length() if closest_bot is None or botdist < closest_dist: closest_bot = bot closest_dist = botdist if closest_bot is not None: closest_bot.target_flag = self._flag def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: if poweruptype is None: poweruptype = PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups ) PowerupBox( position=self.map.powerup_spawn_points[index], poweruptype=poweruptype, ).autoretain() def _start_powerup_drops(self) -> None: self._powerup_drop_timer = bs.Timer( 3.0, self._drop_powerups, repeat=True ) def _drop_powerups( self, standard_points: bool = False, poweruptype: str | None = None ) -> None: """Generic powerup drop.""" if standard_points: spawnpoints = self.map.powerup_spawn_points for i, _point in enumerate(spawnpoints): bs.timer( 1.0 + i * 0.5, bs.Call(self._drop_powerup, i, poweruptype) ) else: point = ( self._powerup_center[0] + random.uniform( -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0], ), self._powerup_center[1], self._powerup_center[2] + random.uniform( -self._powerup_spread[1], self._powerup_spread[1] ), ) # Drop one random one somewhere. PowerupBox( position=point, poweruptype=PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups ), ).autoretain() def _kill_flag(self) -> None: try: assert self._flag is not None self._flag.handlemessage(bs.DieMessage()) except Exception: logging.exception('Error in _kill_flag.') def _handle_score(self) -> None: """a point has been scored""" # FIXME tidy this up # pylint: disable=too-many-branches # Our flag might stick around for a second or two; # we don't want it to be able to score again. assert self._flag is not None if self._flag.scored: return # See which score region it was. region = bs.getcollision().sourcenode i = None for i, score_region in enumerate(self._score_regions): if region == score_region.node: break for team in [self.teams[0], self._bot_team]: assert team is not None if team.id == i: team.score += 7 # Tell all players (or bots) to celebrate. if i == 0: for player in team.players: if player.actor: player.actor.handlemessage(bs.CelebrateMessage(2.0)) else: self._bots.celebrate(2.0) # If the good guys scored, add more enemies. if i == 0: if self.teams[0].score == 7: assert self._bot_types_7 is not None for bottype in self._bot_types_7: self._spawn_bot(bottype) elif self.teams[0].score == 14: assert self._bot_types_14 is not None for bottype in self._bot_types_14: self._spawn_bot(bottype) self._score_sound.play() if i == 0: self._cheer_sound.play() else: self._boo_sound.play() # Kill the flag (it'll respawn shortly). self._flag.scored = True bs.timer(0.2, self._kill_flag) self.update_scores() light = bs.newnode( 'light', attrs={ 'position': bs.getcollision().position, 'height_attenuated': False, 'color': (1, 0, 0), }, ) bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) bs.timer(1.0, light.delete) if i == 0: bs.cameraflash(duration=10.0)
[docs] @override def end_game(self) -> None: bs.setmusic(None) self._bots.final_celebrate() bs.timer(0.001, bs.Call(self.do_end, 'defeat'))
[docs] def update_scores(self) -> None: """update scoreboard and check for winners""" # FIXME: tidy this up # pylint: disable=too-many-nested-blocks have_scoring_team = False win_score = self._score_to_win for team in [self.teams[0], self._bot_team]: assert team is not None assert self._scoreboard is not None self._scoreboard.set_team_value(team, team.score, win_score) if team.score >= win_score: if not have_scoring_team: self._scoring_team = team if team is self._bot_team: self.end_game() else: bs.setmusic(bs.MusicType.VICTORY) # Completion achievements. assert self._bot_team is not None if self._preset in ['rookie', 'rookie_easy']: self._award_achievement( 'Rookie Football Victory', sound=False ) if self._bot_team.score == 0: self._award_achievement( 'Rookie Football Shutout', sound=False ) elif self._preset in ['pro', 'pro_easy']: self._award_achievement( 'Pro Football Victory', sound=False ) if self._bot_team.score == 0: self._award_achievement( 'Pro Football Shutout', sound=False ) elif self._preset in ['uber', 'uber_easy']: self._award_achievement( 'Uber Football Victory', sound=False ) if self._bot_team.score == 0: self._award_achievement( 'Uber Football Shutout', sound=False ) if ( not self._player_has_dropped_bomb and not self._player_has_punched ): self._award_achievement( 'Got the Moves', sound=False ) self._bots.stop_moving() self.show_zoom_message( bs.Lstr(resource='victoryText'), scale=1.0, duration=4.0, ) self.celebrate(10.0) assert self._starttime_ms is not None self._final_time_ms = int( int(bs.time() * 1000.0) - self._starttime_ms ) self._time_text_timer = None assert ( self._time_text_input is not None and self._time_text_input.node ) self._time_text_input.node.timemax = self._final_time_ms # FIXME: Does this still need to be deferred? bs.pushcall(bs.Call(self.do_end, 'victory'))
[docs] def do_end(self, outcome: str) -> None: """End the game with the specified outcome.""" if outcome == 'defeat': self.fade_to_red() assert self._final_time_ms is not None scoreval = ( None if outcome == 'defeat' else int(self._final_time_ms // 10) ) self.end( delay=3.0, results={ 'outcome': outcome, 'score': scoreval, 'score_order': 'decreasing', 'playerinfos': self.initialplayerinfos, }, )
[docs] @override def handlemessage(self, msg: Any) -> Any: """handle high-level game messages""" if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) # Respawn them shortly. player = msg.getplayer(Player) assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 player.respawn_timer = bs.Timer( respawn_time, bs.Call(self.spawn_player_if_exists, player) ) player.respawn_icon = RespawnIcon(player, respawn_time) elif isinstance(msg, SpazBotDiedMessage): # Every time a bad guy dies, spawn a new one. bs.timer(3.0, bs.Call(self._spawn_bot, (type(msg.spazbot)))) elif isinstance(msg, SpazBotPunchedMessage): if self._preset in ['rookie', 'rookie_easy']: if msg.damage >= 500: self._award_achievement('Super Punch') elif self._preset in ['pro', 'pro_easy']: if msg.damage >= 1000: self._award_achievement('Super Mega Punch') # Respawn dead flags. elif isinstance(msg, FlagDiedMessage): assert isinstance(msg.flag, FootballFlag) msg.flag.respawn_timer = bs.Timer(3.0, self._spawn_flag) self._flag_respawn_light = bs.NodeActor( bs.newnode( 'light', attrs={ 'position': self._flag_spawn_pos, 'height_attenuated': False, 'radius': 0.15, 'color': (1.0, 1.0, 0.3), }, ) ) assert self._flag_respawn_light.node bs.animate( self._flag_respawn_light.node, 'intensity', {0: 0, 0.25: 0.15, 0.5: 0}, loop=True, ) bs.timer(3.0, self._flag_respawn_light.node.delete) else: return super().handlemessage(msg) return None
def _handle_player_dropped_bomb(self, player: Spaz, bomb: bs.Actor) -> None: del player, bomb # Unused. self._player_has_dropped_bomb = True def _handle_player_punched(self, player: Spaz) -> None: del player # Unused. self._player_has_punched = True
[docs] @override def spawn_player(self, player: Player) -> bs.Actor: spaz = self.spawn_player_spaz( player, position=self.map.get_start_position(player.team.id) ) if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']: spaz.impact_scale = 0.25 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) spaz.punch_callback = self._handle_player_punched return spaz
def _flash_flag_spawn(self) -> None: light = bs.newnode( 'light', attrs={ 'position': self._flag_spawn_pos, 'height_attenuated': False, 'color': (1, 1, 0), }, ) bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) bs.timer(1.0, light.delete) def _spawn_flag(self) -> None: self._swipsound.play() self._whistle_sound.play() self._flash_flag_spawn() assert self._flag_spawn_pos is not None self._flag = FootballFlag(position=self._flag_spawn_pos)