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