bascenev1lib.game.ninjafight
Provides Ninja Fight mini-game.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides Ninja Fight mini-game.""" 4 5# ba_meta require api 9 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10import random 11from typing import TYPE_CHECKING, override 12 13import bascenev1 as bs 14 15from bascenev1lib.actor.spazbot import ( 16 SpazBotSet, 17 ChargerBot, 18 SpazBotDiedMessage, 19) 20from bascenev1lib.actor.onscreentimer import OnScreenTimer 21 22if TYPE_CHECKING: 23 from typing import Any 24 25 26class Player(bs.Player['Team']): 27 """Our player type for this game.""" 28 29 30class Team(bs.Team[Player]): 31 """Our team type for this game.""" 32 33 34# ba_meta export bascenev1.GameActivity 35class NinjaFightGame(bs.TeamGameActivity[Player, Team]): 36 """ 37 A co-op game where you try to defeat a group 38 of Ninjas as fast as possible 39 """ 40 41 name = 'Ninja Fight' 42 description = 'How fast can you defeat the ninjas?' 43 scoreconfig = bs.ScoreConfig( 44 label='Time', scoretype=bs.ScoreType.MILLISECONDS, lower_is_better=True 45 ) 46 default_music = bs.MusicType.TO_THE_DEATH 47 48 @override 49 @classmethod 50 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 51 # For now we're hard-coding spawn positions and whatnot 52 # so we need to be sure to specify that we only support 53 # a specific map. 54 return ['Courtyard'] 55 56 @override 57 @classmethod 58 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 59 # We currently support Co-Op only. 60 return issubclass(sessiontype, bs.CoopSession) 61 62 # In the constructor we should load any media we need/etc. 63 # ...but not actually create anything yet. 64 def __init__(self, settings: dict): 65 super().__init__(settings) 66 self._winsound = bs.getsound('score') 67 self._won = False 68 self._timer: OnScreenTimer | None = None 69 self._bots = SpazBotSet() 70 self._preset = str(settings['preset']) 71 72 # Called when our game actually begins. 73 @override 74 def on_begin(self) -> None: 75 super().on_begin() 76 is_pro = self._preset == 'pro' 77 78 # In pro mode there's no powerups. 79 if not is_pro: 80 self.setup_standard_powerup_drops() 81 82 # Make our on-screen timer and start it roughly when our bots appear. 83 self._timer = OnScreenTimer() 84 bs.timer(4.0, self._timer.start) 85 86 # Spawn some baddies. 87 bs.timer( 88 1.0, 89 lambda: self._bots.spawn_bot( 90 ChargerBot, pos=(3, 3, -2), spawn_time=3.0 91 ), 92 ) 93 bs.timer( 94 2.0, 95 lambda: self._bots.spawn_bot( 96 ChargerBot, pos=(-3, 3, -2), spawn_time=3.0 97 ), 98 ) 99 bs.timer( 100 3.0, 101 lambda: self._bots.spawn_bot( 102 ChargerBot, pos=(5, 3, -2), spawn_time=3.0 103 ), 104 ) 105 bs.timer( 106 4.0, 107 lambda: self._bots.spawn_bot( 108 ChargerBot, pos=(-5, 3, -2), spawn_time=3.0 109 ), 110 ) 111 112 # Add some extras for multiplayer or pro mode. 113 assert self.initialplayerinfos is not None 114 if len(self.initialplayerinfos) > 2 or is_pro: 115 bs.timer( 116 5.0, 117 lambda: self._bots.spawn_bot( 118 ChargerBot, pos=(0, 3, -5), spawn_time=3.0 119 ), 120 ) 121 if len(self.initialplayerinfos) > 3 or is_pro: 122 bs.timer( 123 6.0, 124 lambda: self._bots.spawn_bot( 125 ChargerBot, pos=(0, 3, 1), spawn_time=3.0 126 ), 127 ) 128 129 # Called for each spawning player. 130 @override 131 def spawn_player(self, player: Player) -> bs.Actor: 132 # Let's spawn close to the center. 133 spawn_center = (0, 3, -2) 134 pos = ( 135 spawn_center[0] + random.uniform(-1.5, 1.5), 136 spawn_center[1], 137 spawn_center[2] + random.uniform(-1.5, 1.5), 138 ) 139 return self.spawn_player_spaz(player, position=pos) 140 141 def _check_if_won(self) -> None: 142 # Simply end the game if there's no living bots. 143 # FIXME: Should also make sure all bots have been spawned; 144 # if spawning is spread out enough that we're able to kill 145 # all living bots before the next spawns, it would incorrectly 146 # count as a win. 147 if not self._bots.have_living_bots(): 148 self._won = True 149 self.end_game() 150 151 # Called for miscellaneous messages. 152 @override 153 def handlemessage(self, msg: Any) -> Any: 154 # A player has died. 155 if isinstance(msg, bs.PlayerDiedMessage): 156 super().handlemessage(msg) # Augment standard behavior. 157 self.respawn_player(msg.getplayer(Player)) 158 159 # A spaz-bot has died. 160 elif isinstance(msg, SpazBotDiedMessage): 161 # Unfortunately the bot-set will always tell us there are living 162 # bots if we ask here (the currently-dying bot isn't officially 163 # marked dead yet) ..so lets push a call into the event loop to 164 # check once this guy has finished dying. 165 bs.pushcall(self._check_if_won) 166 167 # Let the base class handle anything we don't. 168 else: 169 return super().handlemessage(msg) 170 return None 171 172 # When this is called, we should fill out results and end the game 173 # *regardless* of whether is has been won. (this may be called due 174 # to a tournament ending or other external reason). 175 @override 176 def end_game(self) -> None: 177 # Stop our on-screen timer so players can see what they got. 178 assert self._timer is not None 179 self._timer.stop() 180 181 results = bs.GameResults() 182 183 # If we won, set our score to the elapsed time in milliseconds. 184 # (there should just be 1 team here since this is co-op). 185 # ..if we didn't win, leave scores as default (None) which means 186 # we lost. 187 if self._won: 188 elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0) 189 bs.cameraflash() 190 self._winsound.play() 191 for team in self.teams: 192 for player in team.players: 193 if player.actor: 194 player.actor.handlemessage(bs.CelebrateMessage()) 195 results.set_team_score(team, elapsed_time_ms) 196 197 # Ends the activity. 198 self.end(results)
Our player type for this game.
Our team type for this game.
36class NinjaFightGame(bs.TeamGameActivity[Player, Team]): 37 """ 38 A co-op game where you try to defeat a group 39 of Ninjas as fast as possible 40 """ 41 42 name = 'Ninja Fight' 43 description = 'How fast can you defeat the ninjas?' 44 scoreconfig = bs.ScoreConfig( 45 label='Time', scoretype=bs.ScoreType.MILLISECONDS, lower_is_better=True 46 ) 47 default_music = bs.MusicType.TO_THE_DEATH 48 49 @override 50 @classmethod 51 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 52 # For now we're hard-coding spawn positions and whatnot 53 # so we need to be sure to specify that we only support 54 # a specific map. 55 return ['Courtyard'] 56 57 @override 58 @classmethod 59 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 60 # We currently support Co-Op only. 61 return issubclass(sessiontype, bs.CoopSession) 62 63 # In the constructor we should load any media we need/etc. 64 # ...but not actually create anything yet. 65 def __init__(self, settings: dict): 66 super().__init__(settings) 67 self._winsound = bs.getsound('score') 68 self._won = False 69 self._timer: OnScreenTimer | None = None 70 self._bots = SpazBotSet() 71 self._preset = str(settings['preset']) 72 73 # Called when our game actually begins. 74 @override 75 def on_begin(self) -> None: 76 super().on_begin() 77 is_pro = self._preset == 'pro' 78 79 # In pro mode there's no powerups. 80 if not is_pro: 81 self.setup_standard_powerup_drops() 82 83 # Make our on-screen timer and start it roughly when our bots appear. 84 self._timer = OnScreenTimer() 85 bs.timer(4.0, self._timer.start) 86 87 # Spawn some baddies. 88 bs.timer( 89 1.0, 90 lambda: self._bots.spawn_bot( 91 ChargerBot, pos=(3, 3, -2), spawn_time=3.0 92 ), 93 ) 94 bs.timer( 95 2.0, 96 lambda: self._bots.spawn_bot( 97 ChargerBot, pos=(-3, 3, -2), spawn_time=3.0 98 ), 99 ) 100 bs.timer( 101 3.0, 102 lambda: self._bots.spawn_bot( 103 ChargerBot, pos=(5, 3, -2), spawn_time=3.0 104 ), 105 ) 106 bs.timer( 107 4.0, 108 lambda: self._bots.spawn_bot( 109 ChargerBot, pos=(-5, 3, -2), spawn_time=3.0 110 ), 111 ) 112 113 # Add some extras for multiplayer or pro mode. 114 assert self.initialplayerinfos is not None 115 if len(self.initialplayerinfos) > 2 or is_pro: 116 bs.timer( 117 5.0, 118 lambda: self._bots.spawn_bot( 119 ChargerBot, pos=(0, 3, -5), spawn_time=3.0 120 ), 121 ) 122 if len(self.initialplayerinfos) > 3 or is_pro: 123 bs.timer( 124 6.0, 125 lambda: self._bots.spawn_bot( 126 ChargerBot, pos=(0, 3, 1), spawn_time=3.0 127 ), 128 ) 129 130 # Called for each spawning player. 131 @override 132 def spawn_player(self, player: Player) -> bs.Actor: 133 # Let's spawn close to the center. 134 spawn_center = (0, 3, -2) 135 pos = ( 136 spawn_center[0] + random.uniform(-1.5, 1.5), 137 spawn_center[1], 138 spawn_center[2] + random.uniform(-1.5, 1.5), 139 ) 140 return self.spawn_player_spaz(player, position=pos) 141 142 def _check_if_won(self) -> None: 143 # Simply end the game if there's no living bots. 144 # FIXME: Should also make sure all bots have been spawned; 145 # if spawning is spread out enough that we're able to kill 146 # all living bots before the next spawns, it would incorrectly 147 # count as a win. 148 if not self._bots.have_living_bots(): 149 self._won = True 150 self.end_game() 151 152 # Called for miscellaneous messages. 153 @override 154 def handlemessage(self, msg: Any) -> Any: 155 # A player has died. 156 if isinstance(msg, bs.PlayerDiedMessage): 157 super().handlemessage(msg) # Augment standard behavior. 158 self.respawn_player(msg.getplayer(Player)) 159 160 # A spaz-bot has died. 161 elif isinstance(msg, SpazBotDiedMessage): 162 # Unfortunately the bot-set will always tell us there are living 163 # bots if we ask here (the currently-dying bot isn't officially 164 # marked dead yet) ..so lets push a call into the event loop to 165 # check once this guy has finished dying. 166 bs.pushcall(self._check_if_won) 167 168 # Let the base class handle anything we don't. 169 else: 170 return super().handlemessage(msg) 171 return None 172 173 # When this is called, we should fill out results and end the game 174 # *regardless* of whether is has been won. (this may be called due 175 # to a tournament ending or other external reason). 176 @override 177 def end_game(self) -> None: 178 # Stop our on-screen timer so players can see what they got. 179 assert self._timer is not None 180 self._timer.stop() 181 182 results = bs.GameResults() 183 184 # If we won, set our score to the elapsed time in milliseconds. 185 # (there should just be 1 team here since this is co-op). 186 # ..if we didn't win, leave scores as default (None) which means 187 # we lost. 188 if self._won: 189 elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0) 190 bs.cameraflash() 191 self._winsound.play() 192 for team in self.teams: 193 for player in team.players: 194 if player.actor: 195 player.actor.handlemessage(bs.CelebrateMessage()) 196 results.set_team_score(team, elapsed_time_ms) 197 198 # Ends the activity. 199 self.end(results)
A co-op game where you try to defeat a group of Ninjas as fast as possible
65 def __init__(self, settings: dict): 66 super().__init__(settings) 67 self._winsound = bs.getsound('score') 68 self._won = False 69 self._timer: OnScreenTimer | None = None 70 self._bots = SpazBotSet() 71 self._preset = str(settings['preset'])
Instantiate the Activity.
49 @override 50 @classmethod 51 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 52 # For now we're hard-coding spawn positions and whatnot 53 # so we need to be sure to specify that we only support 54 # a specific map. 55 return ['Courtyard']
Called by the default bascenev1.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given bascenev1.Session type.
57 @override 58 @classmethod 59 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 60 # We currently support Co-Op only. 61 return issubclass(sessiontype, bs.CoopSession)
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
74 @override 75 def on_begin(self) -> None: 76 super().on_begin() 77 is_pro = self._preset == 'pro' 78 79 # In pro mode there's no powerups. 80 if not is_pro: 81 self.setup_standard_powerup_drops() 82 83 # Make our on-screen timer and start it roughly when our bots appear. 84 self._timer = OnScreenTimer() 85 bs.timer(4.0, self._timer.start) 86 87 # Spawn some baddies. 88 bs.timer( 89 1.0, 90 lambda: self._bots.spawn_bot( 91 ChargerBot, pos=(3, 3, -2), spawn_time=3.0 92 ), 93 ) 94 bs.timer( 95 2.0, 96 lambda: self._bots.spawn_bot( 97 ChargerBot, pos=(-3, 3, -2), spawn_time=3.0 98 ), 99 ) 100 bs.timer( 101 3.0, 102 lambda: self._bots.spawn_bot( 103 ChargerBot, pos=(5, 3, -2), spawn_time=3.0 104 ), 105 ) 106 bs.timer( 107 4.0, 108 lambda: self._bots.spawn_bot( 109 ChargerBot, pos=(-5, 3, -2), spawn_time=3.0 110 ), 111 ) 112 113 # Add some extras for multiplayer or pro mode. 114 assert self.initialplayerinfos is not None 115 if len(self.initialplayerinfos) > 2 or is_pro: 116 bs.timer( 117 5.0, 118 lambda: self._bots.spawn_bot( 119 ChargerBot, pos=(0, 3, -5), spawn_time=3.0 120 ), 121 ) 122 if len(self.initialplayerinfos) > 3 or is_pro: 123 bs.timer( 124 6.0, 125 lambda: self._bots.spawn_bot( 126 ChargerBot, pos=(0, 3, 1), spawn_time=3.0 127 ), 128 )
Called once the previous Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
131 @override 132 def spawn_player(self, player: Player) -> bs.Actor: 133 # Let's spawn close to the center. 134 spawn_center = (0, 3, -2) 135 pos = ( 136 spawn_center[0] + random.uniform(-1.5, 1.5), 137 spawn_center[1], 138 spawn_center[2] + random.uniform(-1.5, 1.5), 139 ) 140 return self.spawn_player_spaz(player, position=pos)
Spawn something for the provided bascenev1.Player.
The default implementation simply calls spawn_player_spaz().
153 @override 154 def handlemessage(self, msg: Any) -> Any: 155 # A player has died. 156 if isinstance(msg, bs.PlayerDiedMessage): 157 super().handlemessage(msg) # Augment standard behavior. 158 self.respawn_player(msg.getplayer(Player)) 159 160 # A spaz-bot has died. 161 elif isinstance(msg, SpazBotDiedMessage): 162 # Unfortunately the bot-set will always tell us there are living 163 # bots if we ask here (the currently-dying bot isn't officially 164 # marked dead yet) ..so lets push a call into the event loop to 165 # check once this guy has finished dying. 166 bs.pushcall(self._check_if_won) 167 168 # Let the base class handle anything we don't. 169 else: 170 return super().handlemessage(msg) 171 return None
General message handling; can be passed any message object.
176 @override 177 def end_game(self) -> None: 178 # Stop our on-screen timer so players can see what they got. 179 assert self._timer is not None 180 self._timer.stop() 181 182 results = bs.GameResults() 183 184 # If we won, set our score to the elapsed time in milliseconds. 185 # (there should just be 1 team here since this is co-op). 186 # ..if we didn't win, leave scores as default (None) which means 187 # we lost. 188 if self._won: 189 elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0) 190 bs.cameraflash() 191 self._winsound.play() 192 for team in self.teams: 193 for player in team.players: 194 if player.actor: 195 player.actor.handlemessage(bs.CelebrateMessage()) 196 results.set_team_score(team, elapsed_time_ms) 197 198 # Ends the activity. 199 self.end(results)
Tell the game to wrap up and call bascenev1.Activity.end().
This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.