bascenev1lib.game.hockey
Hockey game and support classes.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Hockey game and support classes.""" 4 5# ba_meta require api 9 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10from typing import TYPE_CHECKING, override 11 12import bascenev1 as bs 13 14from bascenev1lib.actor.playerspaz import PlayerSpaz 15from bascenev1lib.actor.scoreboard import Scoreboard 16from bascenev1lib.actor.powerupbox import PowerupBoxFactory 17from bascenev1lib.gameutils import SharedObjects 18 19if TYPE_CHECKING: 20 from typing import Any, Sequence 21 22 23class PuckDiedMessage: 24 """Inform something that a puck has died.""" 25 26 def __init__(self, puck: Puck): 27 self.puck = puck 28 29 30class Puck(bs.Actor): 31 """A lovely giant hockey puck.""" 32 33 def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): 34 super().__init__() 35 shared = SharedObjects.get() 36 activity = self.getactivity() 37 38 # Spawn just above the provided point. 39 self._spawn_pos = (position[0], position[1] + 1.0, position[2]) 40 self.last_players_to_touch: dict[int, Player] = {} 41 self.scored = False 42 assert activity is not None 43 assert isinstance(activity, HockeyGame) 44 pmats = [shared.object_material, activity.puck_material] 45 self.node = bs.newnode( 46 'prop', 47 delegate=self, 48 attrs={ 49 'mesh': activity.puck_mesh, 50 'color_texture': activity.puck_tex, 51 'body': 'puck', 52 'reflection': 'soft', 53 'reflection_scale': [0.2], 54 'shadow_size': 1.0, 55 'is_area_of_interest': True, 56 'position': self._spawn_pos, 57 'materials': pmats, 58 }, 59 ) 60 bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1}) 61 62 @override 63 def handlemessage(self, msg: Any) -> Any: 64 if isinstance(msg, bs.DieMessage): 65 if self.node: 66 self.node.delete() 67 activity = self._activity() 68 if activity and not msg.immediate: 69 activity.handlemessage(PuckDiedMessage(self)) 70 71 # If we go out of bounds, move back to where we started. 72 elif isinstance(msg, bs.OutOfBoundsMessage): 73 assert self.node 74 self.node.position = self._spawn_pos 75 76 elif isinstance(msg, bs.HitMessage): 77 assert self.node 78 assert msg.force_direction is not None 79 self.node.handlemessage( 80 'impulse', 81 msg.pos[0], 82 msg.pos[1], 83 msg.pos[2], 84 msg.velocity[0], 85 msg.velocity[1], 86 msg.velocity[2], 87 1.0 * msg.magnitude, 88 1.0 * msg.velocity_magnitude, 89 msg.radius, 90 0, 91 msg.force_direction[0], 92 msg.force_direction[1], 93 msg.force_direction[2], 94 ) 95 96 # If this hit came from a player, log them as the last to touch us. 97 s_player = msg.get_source_player(Player) 98 if s_player is not None: 99 activity = self._activity() 100 if activity: 101 if s_player in activity.players: 102 self.last_players_to_touch[s_player.team.id] = s_player 103 else: 104 super().handlemessage(msg) 105 106 107class Player(bs.Player['Team']): 108 """Our player type for this game.""" 109 110 111class Team(bs.Team[Player]): 112 """Our team type for this game.""" 113 114 def __init__(self) -> None: 115 self.score = 0 116 117 118# ba_meta export bascenev1.GameActivity 119class HockeyGame(bs.TeamGameActivity[Player, Team]): 120 """Ice hockey game.""" 121 122 name = 'Hockey' 123 description = 'Score some goals.' 124 available_settings = [ 125 bs.IntSetting( 126 'Score to Win', 127 min_value=1, 128 default=1, 129 increment=1, 130 ), 131 bs.IntChoiceSetting( 132 'Time Limit', 133 choices=[ 134 ('None', 0), 135 ('1 Minute', 60), 136 ('2 Minutes', 120), 137 ('5 Minutes', 300), 138 ('10 Minutes', 600), 139 ('20 Minutes', 1200), 140 ], 141 default=0, 142 ), 143 bs.FloatChoiceSetting( 144 'Respawn Times', 145 choices=[ 146 ('Shorter', 0.25), 147 ('Short', 0.5), 148 ('Normal', 1.0), 149 ('Long', 2.0), 150 ('Longer', 4.0), 151 ], 152 default=1.0, 153 ), 154 bs.BoolSetting('Epic Mode', default=False), 155 ] 156 157 @override 158 @classmethod 159 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 160 return issubclass(sessiontype, bs.DualTeamSession) 161 162 @override 163 @classmethod 164 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 165 assert bs.app.classic is not None 166 return bs.app.classic.getmaps('hockey') 167 168 def __init__(self, settings: dict): 169 super().__init__(settings) 170 shared = SharedObjects.get() 171 self._scoreboard = Scoreboard() 172 self._cheer_sound = bs.getsound('cheer') 173 self._chant_sound = bs.getsound('crowdChant') 174 self._foghorn_sound = bs.getsound('foghorn') 175 self._swipsound = bs.getsound('swip') 176 self._whistle_sound = bs.getsound('refWhistle') 177 self.puck_mesh = bs.getmesh('puck') 178 self.puck_tex = bs.gettexture('puckColor') 179 self._puck_sound = bs.getsound('metalHit') 180 self.puck_material = bs.Material() 181 self.puck_material.add_actions( 182 actions=('modify_part_collision', 'friction', 0.5) 183 ) 184 self.puck_material.add_actions( 185 conditions=('they_have_material', shared.pickup_material), 186 actions=('modify_part_collision', 'collide', False), 187 ) 188 self.puck_material.add_actions( 189 conditions=( 190 ('we_are_younger_than', 100), 191 'and', 192 ('they_have_material', shared.object_material), 193 ), 194 actions=('modify_node_collision', 'collide', False), 195 ) 196 self.puck_material.add_actions( 197 conditions=('they_have_material', shared.footing_material), 198 actions=('impact_sound', self._puck_sound, 0.2, 5), 199 ) 200 201 # Keep track of which player last touched the puck 202 self.puck_material.add_actions( 203 conditions=('they_have_material', shared.player_material), 204 actions=(('call', 'at_connect', self._handle_puck_player_collide),), 205 ) 206 207 # We want the puck to kill powerups; not get stopped by them 208 self.puck_material.add_actions( 209 conditions=( 210 'they_have_material', 211 PowerupBoxFactory.get().powerup_material, 212 ), 213 actions=( 214 ('modify_part_collision', 'physical', False), 215 ('message', 'their_node', 'at_connect', bs.DieMessage()), 216 ), 217 ) 218 self._score_region_material = bs.Material() 219 self._score_region_material.add_actions( 220 conditions=('they_have_material', self.puck_material), 221 actions=( 222 ('modify_part_collision', 'collide', True), 223 ('modify_part_collision', 'physical', False), 224 ('call', 'at_connect', self._handle_score), 225 ), 226 ) 227 self._puck_spawn_pos: Sequence[float] | None = None 228 self._score_regions: list[bs.NodeActor] | None = None 229 self._puck: Puck | None = None 230 self._score_to_win = int(settings['Score to Win']) 231 self._time_limit = float(settings['Time Limit']) 232 self._epic_mode = bool(settings['Epic Mode']) 233 self.slow_motion = self._epic_mode 234 self.default_music = ( 235 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.HOCKEY 236 ) 237 238 @override 239 def get_instance_description(self) -> str | Sequence: 240 if self._score_to_win == 1: 241 return 'Score a goal.' 242 return 'Score ${ARG1} goals.', self._score_to_win 243 244 @override 245 def get_instance_description_short(self) -> str | Sequence: 246 if self._score_to_win == 1: 247 return 'score a goal' 248 return 'score ${ARG1} goals', self._score_to_win 249 250 @override 251 def on_begin(self) -> None: 252 super().on_begin() 253 254 self.setup_standard_time_limit(self._time_limit) 255 self.setup_standard_powerup_drops() 256 self._puck_spawn_pos = self.map.get_flag_position(None) 257 self._spawn_puck() 258 259 # Set up the two score regions. 260 defs = self.map.defs 261 self._score_regions = [] 262 self._score_regions.append( 263 bs.NodeActor( 264 bs.newnode( 265 'region', 266 attrs={ 267 'position': defs.boxes['goal1'][0:3], 268 'scale': defs.boxes['goal1'][6:9], 269 'type': 'box', 270 'materials': [self._score_region_material], 271 }, 272 ) 273 ) 274 ) 275 self._score_regions.append( 276 bs.NodeActor( 277 bs.newnode( 278 'region', 279 attrs={ 280 'position': defs.boxes['goal2'][0:3], 281 'scale': defs.boxes['goal2'][6:9], 282 'type': 'box', 283 'materials': [self._score_region_material], 284 }, 285 ) 286 ) 287 ) 288 self._update_scoreboard() 289 self._chant_sound.play() 290 291 @override 292 def on_team_join(self, team: Team) -> None: 293 self._update_scoreboard() 294 295 def _handle_puck_player_collide(self) -> None: 296 collision = bs.getcollision() 297 try: 298 puck = collision.sourcenode.getdelegate(Puck, True) 299 player = collision.opposingnode.getdelegate( 300 PlayerSpaz, True 301 ).getplayer(Player, True) 302 except bs.NotFoundError: 303 return 304 305 puck.last_players_to_touch[player.team.id] = player 306 307 def _kill_puck(self) -> None: 308 self._puck = None 309 310 def _handle_score(self) -> None: 311 """A point has been scored.""" 312 313 assert self._puck is not None 314 assert self._score_regions is not None 315 316 # Our puck might stick around for a second or two 317 # we don't want it to be able to score again. 318 if self._puck.scored: 319 return 320 321 region = bs.getcollision().sourcenode 322 index = 0 323 for index, score_region in enumerate(self._score_regions): 324 if region == score_region.node: 325 break 326 327 for team in self.teams: 328 if team.id == index: 329 scoring_team = team 330 team.score += 1 331 332 # Tell all players to celebrate. 333 for player in team.players: 334 if player.actor: 335 player.actor.handlemessage(bs.CelebrateMessage(2.0)) 336 337 # If we've got the player from the scoring team that last 338 # touched us, give them points. 339 if ( 340 scoring_team.id in self._puck.last_players_to_touch 341 and self._puck.last_players_to_touch[scoring_team.id] 342 ): 343 self.stats.player_scored( 344 self._puck.last_players_to_touch[scoring_team.id], 345 100, 346 big_message=True, 347 ) 348 349 # End game if we won. 350 if team.score >= self._score_to_win: 351 self.end_game() 352 353 self._foghorn_sound.play() 354 self._cheer_sound.play() 355 356 self._puck.scored = True 357 358 # Kill the puck (it'll respawn itself shortly). 359 bs.timer(1.0, self._kill_puck) 360 361 light = bs.newnode( 362 'light', 363 attrs={ 364 'position': bs.getcollision().position, 365 'height_attenuated': False, 366 'color': (1, 0, 0), 367 }, 368 ) 369 bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) 370 bs.timer(1.0, light.delete) 371 372 bs.cameraflash(duration=10.0) 373 self._update_scoreboard() 374 375 @override 376 def end_game(self) -> None: 377 results = bs.GameResults() 378 for team in self.teams: 379 results.set_team_score(team, team.score) 380 self.end(results=results) 381 382 def _update_scoreboard(self) -> None: 383 winscore = self._score_to_win 384 for team in self.teams: 385 self._scoreboard.set_team_value(team, team.score, winscore) 386 387 @override 388 def handlemessage(self, msg: Any) -> Any: 389 # Respawn dead players if they're still in the game. 390 if isinstance(msg, bs.PlayerDiedMessage): 391 # Augment standard behavior... 392 super().handlemessage(msg) 393 self.respawn_player(msg.getplayer(Player)) 394 395 # Respawn dead pucks. 396 elif isinstance(msg, PuckDiedMessage): 397 if not self.has_ended(): 398 bs.timer(3.0, self._spawn_puck) 399 else: 400 super().handlemessage(msg) 401 402 def _flash_puck_spawn(self) -> None: 403 light = bs.newnode( 404 'light', 405 attrs={ 406 'position': self._puck_spawn_pos, 407 'height_attenuated': False, 408 'color': (1, 0, 0), 409 }, 410 ) 411 bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) 412 bs.timer(1.0, light.delete) 413 414 def _spawn_puck(self) -> None: 415 self._swipsound.play() 416 self._whistle_sound.play() 417 self._flash_puck_spawn() 418 assert self._puck_spawn_pos is not None 419 self._puck = Puck(position=self._puck_spawn_pos)
24class PuckDiedMessage: 25 """Inform something that a puck has died.""" 26 27 def __init__(self, puck: Puck): 28 self.puck = puck
Inform something that a puck has died.
31class Puck(bs.Actor): 32 """A lovely giant hockey puck.""" 33 34 def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): 35 super().__init__() 36 shared = SharedObjects.get() 37 activity = self.getactivity() 38 39 # Spawn just above the provided point. 40 self._spawn_pos = (position[0], position[1] + 1.0, position[2]) 41 self.last_players_to_touch: dict[int, Player] = {} 42 self.scored = False 43 assert activity is not None 44 assert isinstance(activity, HockeyGame) 45 pmats = [shared.object_material, activity.puck_material] 46 self.node = bs.newnode( 47 'prop', 48 delegate=self, 49 attrs={ 50 'mesh': activity.puck_mesh, 51 'color_texture': activity.puck_tex, 52 'body': 'puck', 53 'reflection': 'soft', 54 'reflection_scale': [0.2], 55 'shadow_size': 1.0, 56 'is_area_of_interest': True, 57 'position': self._spawn_pos, 58 'materials': pmats, 59 }, 60 ) 61 bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1}) 62 63 @override 64 def handlemessage(self, msg: Any) -> Any: 65 if isinstance(msg, bs.DieMessage): 66 if self.node: 67 self.node.delete() 68 activity = self._activity() 69 if activity and not msg.immediate: 70 activity.handlemessage(PuckDiedMessage(self)) 71 72 # If we go out of bounds, move back to where we started. 73 elif isinstance(msg, bs.OutOfBoundsMessage): 74 assert self.node 75 self.node.position = self._spawn_pos 76 77 elif isinstance(msg, bs.HitMessage): 78 assert self.node 79 assert msg.force_direction is not None 80 self.node.handlemessage( 81 'impulse', 82 msg.pos[0], 83 msg.pos[1], 84 msg.pos[2], 85 msg.velocity[0], 86 msg.velocity[1], 87 msg.velocity[2], 88 1.0 * msg.magnitude, 89 1.0 * msg.velocity_magnitude, 90 msg.radius, 91 0, 92 msg.force_direction[0], 93 msg.force_direction[1], 94 msg.force_direction[2], 95 ) 96 97 # If this hit came from a player, log them as the last to touch us. 98 s_player = msg.get_source_player(Player) 99 if s_player is not None: 100 activity = self._activity() 101 if activity: 102 if s_player in activity.players: 103 self.last_players_to_touch[s_player.team.id] = s_player 104 else: 105 super().handlemessage(msg)
A lovely giant hockey puck.
34 def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): 35 super().__init__() 36 shared = SharedObjects.get() 37 activity = self.getactivity() 38 39 # Spawn just above the provided point. 40 self._spawn_pos = (position[0], position[1] + 1.0, position[2]) 41 self.last_players_to_touch: dict[int, Player] = {} 42 self.scored = False 43 assert activity is not None 44 assert isinstance(activity, HockeyGame) 45 pmats = [shared.object_material, activity.puck_material] 46 self.node = bs.newnode( 47 'prop', 48 delegate=self, 49 attrs={ 50 'mesh': activity.puck_mesh, 51 'color_texture': activity.puck_tex, 52 'body': 'puck', 53 'reflection': 'soft', 54 'reflection_scale': [0.2], 55 'shadow_size': 1.0, 56 'is_area_of_interest': True, 57 'position': self._spawn_pos, 58 'materials': pmats, 59 }, 60 ) 61 bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1})
Instantiates an Actor in the current bascenev1.Activity.
63 @override 64 def handlemessage(self, msg: Any) -> Any: 65 if isinstance(msg, bs.DieMessage): 66 if self.node: 67 self.node.delete() 68 activity = self._activity() 69 if activity and not msg.immediate: 70 activity.handlemessage(PuckDiedMessage(self)) 71 72 # If we go out of bounds, move back to where we started. 73 elif isinstance(msg, bs.OutOfBoundsMessage): 74 assert self.node 75 self.node.position = self._spawn_pos 76 77 elif isinstance(msg, bs.HitMessage): 78 assert self.node 79 assert msg.force_direction is not None 80 self.node.handlemessage( 81 'impulse', 82 msg.pos[0], 83 msg.pos[1], 84 msg.pos[2], 85 msg.velocity[0], 86 msg.velocity[1], 87 msg.velocity[2], 88 1.0 * msg.magnitude, 89 1.0 * msg.velocity_magnitude, 90 msg.radius, 91 0, 92 msg.force_direction[0], 93 msg.force_direction[1], 94 msg.force_direction[2], 95 ) 96 97 # If this hit came from a player, log them as the last to touch us. 98 s_player = msg.get_source_player(Player) 99 if s_player is not None: 100 activity = self._activity() 101 if activity: 102 if s_player in activity.players: 103 self.last_players_to_touch[s_player.team.id] = s_player 104 else: 105 super().handlemessage(msg)
General message handling; can be passed any message object.
Our player type for this game.
112class Team(bs.Team[Player]): 113 """Our team type for this game.""" 114 115 def __init__(self) -> None: 116 self.score = 0
Our team type for this game.
120class HockeyGame(bs.TeamGameActivity[Player, Team]): 121 """Ice hockey game.""" 122 123 name = 'Hockey' 124 description = 'Score some goals.' 125 available_settings = [ 126 bs.IntSetting( 127 'Score to Win', 128 min_value=1, 129 default=1, 130 increment=1, 131 ), 132 bs.IntChoiceSetting( 133 'Time Limit', 134 choices=[ 135 ('None', 0), 136 ('1 Minute', 60), 137 ('2 Minutes', 120), 138 ('5 Minutes', 300), 139 ('10 Minutes', 600), 140 ('20 Minutes', 1200), 141 ], 142 default=0, 143 ), 144 bs.FloatChoiceSetting( 145 'Respawn Times', 146 choices=[ 147 ('Shorter', 0.25), 148 ('Short', 0.5), 149 ('Normal', 1.0), 150 ('Long', 2.0), 151 ('Longer', 4.0), 152 ], 153 default=1.0, 154 ), 155 bs.BoolSetting('Epic Mode', default=False), 156 ] 157 158 @override 159 @classmethod 160 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 161 return issubclass(sessiontype, bs.DualTeamSession) 162 163 @override 164 @classmethod 165 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 166 assert bs.app.classic is not None 167 return bs.app.classic.getmaps('hockey') 168 169 def __init__(self, settings: dict): 170 super().__init__(settings) 171 shared = SharedObjects.get() 172 self._scoreboard = Scoreboard() 173 self._cheer_sound = bs.getsound('cheer') 174 self._chant_sound = bs.getsound('crowdChant') 175 self._foghorn_sound = bs.getsound('foghorn') 176 self._swipsound = bs.getsound('swip') 177 self._whistle_sound = bs.getsound('refWhistle') 178 self.puck_mesh = bs.getmesh('puck') 179 self.puck_tex = bs.gettexture('puckColor') 180 self._puck_sound = bs.getsound('metalHit') 181 self.puck_material = bs.Material() 182 self.puck_material.add_actions( 183 actions=('modify_part_collision', 'friction', 0.5) 184 ) 185 self.puck_material.add_actions( 186 conditions=('they_have_material', shared.pickup_material), 187 actions=('modify_part_collision', 'collide', False), 188 ) 189 self.puck_material.add_actions( 190 conditions=( 191 ('we_are_younger_than', 100), 192 'and', 193 ('they_have_material', shared.object_material), 194 ), 195 actions=('modify_node_collision', 'collide', False), 196 ) 197 self.puck_material.add_actions( 198 conditions=('they_have_material', shared.footing_material), 199 actions=('impact_sound', self._puck_sound, 0.2, 5), 200 ) 201 202 # Keep track of which player last touched the puck 203 self.puck_material.add_actions( 204 conditions=('they_have_material', shared.player_material), 205 actions=(('call', 'at_connect', self._handle_puck_player_collide),), 206 ) 207 208 # We want the puck to kill powerups; not get stopped by them 209 self.puck_material.add_actions( 210 conditions=( 211 'they_have_material', 212 PowerupBoxFactory.get().powerup_material, 213 ), 214 actions=( 215 ('modify_part_collision', 'physical', False), 216 ('message', 'their_node', 'at_connect', bs.DieMessage()), 217 ), 218 ) 219 self._score_region_material = bs.Material() 220 self._score_region_material.add_actions( 221 conditions=('they_have_material', self.puck_material), 222 actions=( 223 ('modify_part_collision', 'collide', True), 224 ('modify_part_collision', 'physical', False), 225 ('call', 'at_connect', self._handle_score), 226 ), 227 ) 228 self._puck_spawn_pos: Sequence[float] | None = None 229 self._score_regions: list[bs.NodeActor] | None = None 230 self._puck: Puck | None = None 231 self._score_to_win = int(settings['Score to Win']) 232 self._time_limit = float(settings['Time Limit']) 233 self._epic_mode = bool(settings['Epic Mode']) 234 self.slow_motion = self._epic_mode 235 self.default_music = ( 236 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.HOCKEY 237 ) 238 239 @override 240 def get_instance_description(self) -> str | Sequence: 241 if self._score_to_win == 1: 242 return 'Score a goal.' 243 return 'Score ${ARG1} goals.', self._score_to_win 244 245 @override 246 def get_instance_description_short(self) -> str | Sequence: 247 if self._score_to_win == 1: 248 return 'score a goal' 249 return 'score ${ARG1} goals', self._score_to_win 250 251 @override 252 def on_begin(self) -> None: 253 super().on_begin() 254 255 self.setup_standard_time_limit(self._time_limit) 256 self.setup_standard_powerup_drops() 257 self._puck_spawn_pos = self.map.get_flag_position(None) 258 self._spawn_puck() 259 260 # Set up the two score regions. 261 defs = self.map.defs 262 self._score_regions = [] 263 self._score_regions.append( 264 bs.NodeActor( 265 bs.newnode( 266 'region', 267 attrs={ 268 'position': defs.boxes['goal1'][0:3], 269 'scale': defs.boxes['goal1'][6:9], 270 'type': 'box', 271 'materials': [self._score_region_material], 272 }, 273 ) 274 ) 275 ) 276 self._score_regions.append( 277 bs.NodeActor( 278 bs.newnode( 279 'region', 280 attrs={ 281 'position': defs.boxes['goal2'][0:3], 282 'scale': defs.boxes['goal2'][6:9], 283 'type': 'box', 284 'materials': [self._score_region_material], 285 }, 286 ) 287 ) 288 ) 289 self._update_scoreboard() 290 self._chant_sound.play() 291 292 @override 293 def on_team_join(self, team: Team) -> None: 294 self._update_scoreboard() 295 296 def _handle_puck_player_collide(self) -> None: 297 collision = bs.getcollision() 298 try: 299 puck = collision.sourcenode.getdelegate(Puck, True) 300 player = collision.opposingnode.getdelegate( 301 PlayerSpaz, True 302 ).getplayer(Player, True) 303 except bs.NotFoundError: 304 return 305 306 puck.last_players_to_touch[player.team.id] = player 307 308 def _kill_puck(self) -> None: 309 self._puck = None 310 311 def _handle_score(self) -> None: 312 """A point has been scored.""" 313 314 assert self._puck is not None 315 assert self._score_regions is not None 316 317 # Our puck might stick around for a second or two 318 # we don't want it to be able to score again. 319 if self._puck.scored: 320 return 321 322 region = bs.getcollision().sourcenode 323 index = 0 324 for index, score_region in enumerate(self._score_regions): 325 if region == score_region.node: 326 break 327 328 for team in self.teams: 329 if team.id == index: 330 scoring_team = team 331 team.score += 1 332 333 # Tell all players to celebrate. 334 for player in team.players: 335 if player.actor: 336 player.actor.handlemessage(bs.CelebrateMessage(2.0)) 337 338 # If we've got the player from the scoring team that last 339 # touched us, give them points. 340 if ( 341 scoring_team.id in self._puck.last_players_to_touch 342 and self._puck.last_players_to_touch[scoring_team.id] 343 ): 344 self.stats.player_scored( 345 self._puck.last_players_to_touch[scoring_team.id], 346 100, 347 big_message=True, 348 ) 349 350 # End game if we won. 351 if team.score >= self._score_to_win: 352 self.end_game() 353 354 self._foghorn_sound.play() 355 self._cheer_sound.play() 356 357 self._puck.scored = True 358 359 # Kill the puck (it'll respawn itself shortly). 360 bs.timer(1.0, self._kill_puck) 361 362 light = bs.newnode( 363 'light', 364 attrs={ 365 'position': bs.getcollision().position, 366 'height_attenuated': False, 367 'color': (1, 0, 0), 368 }, 369 ) 370 bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) 371 bs.timer(1.0, light.delete) 372 373 bs.cameraflash(duration=10.0) 374 self._update_scoreboard() 375 376 @override 377 def end_game(self) -> None: 378 results = bs.GameResults() 379 for team in self.teams: 380 results.set_team_score(team, team.score) 381 self.end(results=results) 382 383 def _update_scoreboard(self) -> None: 384 winscore = self._score_to_win 385 for team in self.teams: 386 self._scoreboard.set_team_value(team, team.score, winscore) 387 388 @override 389 def handlemessage(self, msg: Any) -> Any: 390 # Respawn dead players if they're still in the game. 391 if isinstance(msg, bs.PlayerDiedMessage): 392 # Augment standard behavior... 393 super().handlemessage(msg) 394 self.respawn_player(msg.getplayer(Player)) 395 396 # Respawn dead pucks. 397 elif isinstance(msg, PuckDiedMessage): 398 if not self.has_ended(): 399 bs.timer(3.0, self._spawn_puck) 400 else: 401 super().handlemessage(msg) 402 403 def _flash_puck_spawn(self) -> None: 404 light = bs.newnode( 405 'light', 406 attrs={ 407 'position': self._puck_spawn_pos, 408 'height_attenuated': False, 409 'color': (1, 0, 0), 410 }, 411 ) 412 bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) 413 bs.timer(1.0, light.delete) 414 415 def _spawn_puck(self) -> None: 416 self._swipsound.play() 417 self._whistle_sound.play() 418 self._flash_puck_spawn() 419 assert self._puck_spawn_pos is not None 420 self._puck = Puck(position=self._puck_spawn_pos)
Ice hockey game.
169 def __init__(self, settings: dict): 170 super().__init__(settings) 171 shared = SharedObjects.get() 172 self._scoreboard = Scoreboard() 173 self._cheer_sound = bs.getsound('cheer') 174 self._chant_sound = bs.getsound('crowdChant') 175 self._foghorn_sound = bs.getsound('foghorn') 176 self._swipsound = bs.getsound('swip') 177 self._whistle_sound = bs.getsound('refWhistle') 178 self.puck_mesh = bs.getmesh('puck') 179 self.puck_tex = bs.gettexture('puckColor') 180 self._puck_sound = bs.getsound('metalHit') 181 self.puck_material = bs.Material() 182 self.puck_material.add_actions( 183 actions=('modify_part_collision', 'friction', 0.5) 184 ) 185 self.puck_material.add_actions( 186 conditions=('they_have_material', shared.pickup_material), 187 actions=('modify_part_collision', 'collide', False), 188 ) 189 self.puck_material.add_actions( 190 conditions=( 191 ('we_are_younger_than', 100), 192 'and', 193 ('they_have_material', shared.object_material), 194 ), 195 actions=('modify_node_collision', 'collide', False), 196 ) 197 self.puck_material.add_actions( 198 conditions=('they_have_material', shared.footing_material), 199 actions=('impact_sound', self._puck_sound, 0.2, 5), 200 ) 201 202 # Keep track of which player last touched the puck 203 self.puck_material.add_actions( 204 conditions=('they_have_material', shared.player_material), 205 actions=(('call', 'at_connect', self._handle_puck_player_collide),), 206 ) 207 208 # We want the puck to kill powerups; not get stopped by them 209 self.puck_material.add_actions( 210 conditions=( 211 'they_have_material', 212 PowerupBoxFactory.get().powerup_material, 213 ), 214 actions=( 215 ('modify_part_collision', 'physical', False), 216 ('message', 'their_node', 'at_connect', bs.DieMessage()), 217 ), 218 ) 219 self._score_region_material = bs.Material() 220 self._score_region_material.add_actions( 221 conditions=('they_have_material', self.puck_material), 222 actions=( 223 ('modify_part_collision', 'collide', True), 224 ('modify_part_collision', 'physical', False), 225 ('call', 'at_connect', self._handle_score), 226 ), 227 ) 228 self._puck_spawn_pos: Sequence[float] | None = None 229 self._score_regions: list[bs.NodeActor] | None = None 230 self._puck: Puck | None = None 231 self._score_to_win = int(settings['Score to Win']) 232 self._time_limit = float(settings['Time Limit']) 233 self._epic_mode = bool(settings['Epic Mode']) 234 self.slow_motion = self._epic_mode 235 self.default_music = ( 236 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.HOCKEY 237 )
Instantiate the Activity.
158 @override 159 @classmethod 160 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 161 return issubclass(sessiontype, bs.DualTeamSession)
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
163 @override 164 @classmethod 165 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 166 assert bs.app.classic is not None 167 return bs.app.classic.getmaps('hockey')
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.
239 @override 240 def get_instance_description(self) -> str | Sequence: 241 if self._score_to_win == 1: 242 return 'Score a goal.' 243 return 'Score ${ARG1} goals.', self._score_to_win
Return a description for this game instance, in English.
This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().
Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:
This will give us something like 'Score 3 goals.' in English
and can properly translate to 'Anota 3 goles.' in Spanish.
If we just returned the string 'Score 3 Goals' here, there would
have to be a translation entry for each specific number. ew.
return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]
This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.
245 @override 246 def get_instance_description_short(self) -> str | Sequence: 247 if self._score_to_win == 1: 248 return 'score a goal' 249 return 'score ${ARG1} goals', self._score_to_win
Return a short description for this game instance in English.
This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.
Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:
This will give us something like 'score 3 goals' in English
and can properly translate to 'anota 3 goles' in Spanish.
If we just returned the string 'score 3 goals' here, there would
have to be a translation entry for each specific number. ew.
return ['score ${ARG1} goals', self.settings_raw['Score to Win']]
This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.
251 @override 252 def on_begin(self) -> None: 253 super().on_begin() 254 255 self.setup_standard_time_limit(self._time_limit) 256 self.setup_standard_powerup_drops() 257 self._puck_spawn_pos = self.map.get_flag_position(None) 258 self._spawn_puck() 259 260 # Set up the two score regions. 261 defs = self.map.defs 262 self._score_regions = [] 263 self._score_regions.append( 264 bs.NodeActor( 265 bs.newnode( 266 'region', 267 attrs={ 268 'position': defs.boxes['goal1'][0:3], 269 'scale': defs.boxes['goal1'][6:9], 270 'type': 'box', 271 'materials': [self._score_region_material], 272 }, 273 ) 274 ) 275 ) 276 self._score_regions.append( 277 bs.NodeActor( 278 bs.newnode( 279 'region', 280 attrs={ 281 'position': defs.boxes['goal2'][0:3], 282 'scale': defs.boxes['goal2'][6:9], 283 'type': 'box', 284 'materials': [self._score_region_material], 285 }, 286 ) 287 ) 288 ) 289 self._update_scoreboard() 290 self._chant_sound.play()
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.
Called when a new bascenev1.Team joins the Activity.
(including the initial set of Teams)
376 @override 377 def end_game(self) -> None: 378 results = bs.GameResults() 379 for team in self.teams: 380 results.set_team_score(team, team.score) 381 self.end(results=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.
388 @override 389 def handlemessage(self, msg: Any) -> Any: 390 # Respawn dead players if they're still in the game. 391 if isinstance(msg, bs.PlayerDiedMessage): 392 # Augment standard behavior... 393 super().handlemessage(msg) 394 self.respawn_player(msg.getplayer(Player)) 395 396 # Respawn dead pucks. 397 elif isinstance(msg, PuckDiedMessage): 398 if not self.has_ended(): 399 bs.timer(3.0, self._spawn_puck) 400 else: 401 super().handlemessage(msg)
General message handling; can be passed any message object.