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('prop', 45 delegate=self, 46 attrs={ 47 'model': activity.puck_model, 48 'color_texture': activity.puck_tex, 49 'body': 'puck', 50 'reflection': 'soft', 51 'reflection_scale': [0.2], 52 'shadow_size': 1.0, 53 'is_area_of_interest': True, 54 'position': self._spawn_pos, 55 'materials': pmats 56 }) 57 ba.animate(self.node, 'model_scale', {0: 0, 0.2: 1.3, 0.26: 1}) 58 59 def handlemessage(self, msg: Any) -> Any: 60 if isinstance(msg, ba.DieMessage): 61 assert self.node 62 self.node.delete() 63 activity = self._activity() 64 if activity and not msg.immediate: 65 activity.handlemessage(PuckDiedMessage(self)) 66 67 # If we go out of bounds, move back to where we started. 68 elif isinstance(msg, ba.OutOfBoundsMessage): 69 assert self.node 70 self.node.position = self._spawn_pos 71 72 elif isinstance(msg, ba.HitMessage): 73 assert self.node 74 assert msg.force_direction is not None 75 self.node.handlemessage( 76 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], 77 msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, 78 1.0 * msg.velocity_magnitude, msg.radius, 0, 79 msg.force_direction[0], msg.force_direction[1], 80 msg.force_direction[2]) 81 82 # If this hit came from a player, log them as the last to touch us. 83 s_player = msg.get_source_player(Player) 84 if s_player is not None: 85 activity = self._activity() 86 if activity: 87 if s_player in activity.players: 88 self.last_players_to_touch[s_player.team.id] = s_player 89 else: 90 super().handlemessage(msg) 91 92 93class Player(ba.Player['Team']): 94 """Our player type for this game.""" 95 96 97class Team(ba.Team[Player]): 98 """Our team type for this game.""" 99 100 def __init__(self) -> None: 101 self.score = 0 102 103 104# ba_meta export game 105class HockeyGame(ba.TeamGameActivity[Player, Team]): 106 """Ice hockey game.""" 107 108 name = 'Hockey' 109 description = 'Score some goals.' 110 available_settings = [ 111 ba.IntSetting( 112 'Score to Win', 113 min_value=1, 114 default=1, 115 increment=1, 116 ), 117 ba.IntChoiceSetting( 118 'Time Limit', 119 choices=[ 120 ('None', 0), 121 ('1 Minute', 60), 122 ('2 Minutes', 120), 123 ('5 Minutes', 300), 124 ('10 Minutes', 600), 125 ('20 Minutes', 1200), 126 ], 127 default=0, 128 ), 129 ba.FloatChoiceSetting( 130 'Respawn Times', 131 choices=[ 132 ('Shorter', 0.25), 133 ('Short', 0.5), 134 ('Normal', 1.0), 135 ('Long', 2.0), 136 ('Longer', 4.0), 137 ], 138 default=1.0, 139 ), 140 ] 141 default_music = ba.MusicType.HOCKEY 142 143 @classmethod 144 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 145 return issubclass(sessiontype, ba.DualTeamSession) 146 147 @classmethod 148 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 149 return ba.getmaps('hockey') 150 151 def __init__(self, settings: dict): 152 super().__init__(settings) 153 shared = SharedObjects.get() 154 self._scoreboard = Scoreboard() 155 self._cheer_sound = ba.getsound('cheer') 156 self._chant_sound = ba.getsound('crowdChant') 157 self._foghorn_sound = ba.getsound('foghorn') 158 self._swipsound = ba.getsound('swip') 159 self._whistle_sound = ba.getsound('refWhistle') 160 self.puck_model = ba.getmodel('puck') 161 self.puck_tex = ba.gettexture('puckColor') 162 self._puck_sound = ba.getsound('metalHit') 163 self.puck_material = ba.Material() 164 self.puck_material.add_actions(actions=(('modify_part_collision', 165 'friction', 0.5))) 166 self.puck_material.add_actions(conditions=('they_have_material', 167 shared.pickup_material), 168 actions=('modify_part_collision', 169 'collide', False)) 170 self.puck_material.add_actions( 171 conditions=( 172 ('we_are_younger_than', 100), 173 'and', 174 ('they_have_material', shared.object_material), 175 ), 176 actions=('modify_node_collision', 'collide', False), 177 ) 178 self.puck_material.add_actions(conditions=('they_have_material', 179 shared.footing_material), 180 actions=('impact_sound', 181 self._puck_sound, 0.2, 5)) 182 183 # Keep track of which player last touched the puck 184 self.puck_material.add_actions( 185 conditions=('they_have_material', shared.player_material), 186 actions=(('call', 'at_connect', 187 self._handle_puck_player_collide), )) 188 189 # We want the puck to kill powerups; not get stopped by them 190 self.puck_material.add_actions( 191 conditions=('they_have_material', 192 PowerupBoxFactory.get().powerup_material), 193 actions=(('modify_part_collision', 'physical', False), 194 ('message', 'their_node', 'at_connect', ba.DieMessage()))) 195 self._score_region_material = ba.Material() 196 self._score_region_material.add_actions( 197 conditions=('they_have_material', self.puck_material), 198 actions=(('modify_part_collision', 'collide', 199 True), ('modify_part_collision', 'physical', False), 200 ('call', 'at_connect', self._handle_score))) 201 self._puck_spawn_pos: Sequence[float] | None = None 202 self._score_regions: list[ba.NodeActor] | None = None 203 self._puck: Puck | None = None 204 self._score_to_win = int(settings['Score to Win']) 205 self._time_limit = float(settings['Time Limit']) 206 207 def get_instance_description(self) -> str | Sequence: 208 if self._score_to_win == 1: 209 return 'Score a goal.' 210 return 'Score ${ARG1} goals.', self._score_to_win 211 212 def get_instance_description_short(self) -> str | Sequence: 213 if self._score_to_win == 1: 214 return 'score a goal' 215 return 'score ${ARG1} goals', self._score_to_win 216 217 def on_begin(self) -> None: 218 super().on_begin() 219 220 self.setup_standard_time_limit(self._time_limit) 221 self.setup_standard_powerup_drops() 222 self._puck_spawn_pos = self.map.get_flag_position(None) 223 self._spawn_puck() 224 225 # Set up the two score regions. 226 defs = self.map.defs 227 self._score_regions = [] 228 self._score_regions.append( 229 ba.NodeActor( 230 ba.newnode('region', 231 attrs={ 232 'position': defs.boxes['goal1'][0:3], 233 'scale': defs.boxes['goal1'][6:9], 234 'type': 'box', 235 'materials': [self._score_region_material] 236 }))) 237 self._score_regions.append( 238 ba.NodeActor( 239 ba.newnode('region', 240 attrs={ 241 'position': defs.boxes['goal2'][0:3], 242 'scale': defs.boxes['goal2'][6:9], 243 'type': 'box', 244 'materials': [self._score_region_material] 245 }))) 246 self._update_scoreboard() 247 ba.playsound(self._chant_sound) 248 249 def on_team_join(self, team: Team) -> None: 250 self._update_scoreboard() 251 252 def _handle_puck_player_collide(self) -> None: 253 collision = ba.getcollision() 254 try: 255 puck = collision.sourcenode.getdelegate(Puck, True) 256 player = collision.opposingnode.getdelegate(PlayerSpaz, 257 True).getplayer( 258 Player, True) 259 except ba.NotFoundError: 260 return 261 262 puck.last_players_to_touch[player.team.id] = player 263 264 def _kill_puck(self) -> None: 265 self._puck = None 266 267 def _handle_score(self) -> None: 268 """A point has been scored.""" 269 270 assert self._puck is not None 271 assert self._score_regions is not None 272 273 # Our puck might stick around for a second or two 274 # we don't want it to be able to score again. 275 if self._puck.scored: 276 return 277 278 region = ba.getcollision().sourcenode 279 index = 0 280 for index, score_region in enumerate(self._score_regions): 281 if region == score_region.node: 282 break 283 284 for team in self.teams: 285 if team.id == index: 286 scoring_team = team 287 team.score += 1 288 289 # Tell all players to celebrate. 290 for player in team.players: 291 if player.actor: 292 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 293 294 # If we've got the player from the scoring team that last 295 # touched us, give them points. 296 if (scoring_team.id in self._puck.last_players_to_touch 297 and self._puck.last_players_to_touch[scoring_team.id]): 298 self.stats.player_scored( 299 self._puck.last_players_to_touch[scoring_team.id], 300 100, 301 big_message=True) 302 303 # End game if we won. 304 if team.score >= self._score_to_win: 305 self.end_game() 306 307 ba.playsound(self._foghorn_sound) 308 ba.playsound(self._cheer_sound) 309 310 self._puck.scored = True 311 312 # Kill the puck (it'll respawn itself shortly). 313 ba.timer(1.0, self._kill_puck) 314 315 light = ba.newnode('light', 316 attrs={ 317 'position': ba.getcollision().position, 318 'height_attenuated': False, 319 'color': (1, 0, 0) 320 }) 321 ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) 322 ba.timer(1.0, light.delete) 323 324 ba.cameraflash(duration=10.0) 325 self._update_scoreboard() 326 327 def end_game(self) -> None: 328 results = ba.GameResults() 329 for team in self.teams: 330 results.set_team_score(team, team.score) 331 self.end(results=results) 332 333 def _update_scoreboard(self) -> None: 334 winscore = self._score_to_win 335 for team in self.teams: 336 self._scoreboard.set_team_value(team, team.score, winscore) 337 338 def handlemessage(self, msg: Any) -> Any: 339 340 # Respawn dead players if they're still in the game. 341 if isinstance(msg, ba.PlayerDiedMessage): 342 # Augment standard behavior... 343 super().handlemessage(msg) 344 self.respawn_player(msg.getplayer(Player)) 345 346 # Respawn dead pucks. 347 elif isinstance(msg, PuckDiedMessage): 348 if not self.has_ended(): 349 ba.timer(3.0, self._spawn_puck) 350 else: 351 super().handlemessage(msg) 352 353 def _flash_puck_spawn(self) -> None: 354 light = ba.newnode('light', 355 attrs={ 356 'position': self._puck_spawn_pos, 357 'height_attenuated': False, 358 'color': (1, 0, 0) 359 }) 360 ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) 361 ba.timer(1.0, light.delete) 362 363 def _spawn_puck(self) -> None: 364 ba.playsound(self._swipsound) 365 ba.playsound(self._whistle_sound) 366 self._flash_puck_spawn() 367 assert self._puck_spawn_pos is not None 368 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('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 ba.animate(self.node, 'model_scale', {0: 0, 0.2: 1.3, 0.26: 1}) 59 60 def handlemessage(self, msg: Any) -> Any: 61 if isinstance(msg, ba.DieMessage): 62 assert self.node 63 self.node.delete() 64 activity = self._activity() 65 if activity and not msg.immediate: 66 activity.handlemessage(PuckDiedMessage(self)) 67 68 # If we go out of bounds, move back to where we started. 69 elif isinstance(msg, ba.OutOfBoundsMessage): 70 assert self.node 71 self.node.position = self._spawn_pos 72 73 elif isinstance(msg, ba.HitMessage): 74 assert self.node 75 assert msg.force_direction is not None 76 self.node.handlemessage( 77 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], 78 msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, 79 1.0 * msg.velocity_magnitude, msg.radius, 0, 80 msg.force_direction[0], msg.force_direction[1], 81 msg.force_direction[2]) 82 83 # If this hit came from a player, log them as the last to touch us. 84 s_player = msg.get_source_player(Player) 85 if s_player is not None: 86 activity = self._activity() 87 if activity: 88 if s_player in activity.players: 89 self.last_players_to_touch[s_player.team.id] = s_player 90 else: 91 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('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 ba.animate(self.node, 'model_scale', {0: 0, 0.2: 1.3, 0.26: 1})
Instantiates an Actor in the current ba.Activity.
60 def handlemessage(self, msg: Any) -> Any: 61 if isinstance(msg, ba.DieMessage): 62 assert self.node 63 self.node.delete() 64 activity = self._activity() 65 if activity and not msg.immediate: 66 activity.handlemessage(PuckDiedMessage(self)) 67 68 # If we go out of bounds, move back to where we started. 69 elif isinstance(msg, ba.OutOfBoundsMessage): 70 assert self.node 71 self.node.position = self._spawn_pos 72 73 elif isinstance(msg, ba.HitMessage): 74 assert self.node 75 assert msg.force_direction is not None 76 self.node.handlemessage( 77 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], 78 msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, 79 1.0 * msg.velocity_magnitude, msg.radius, 0, 80 msg.force_direction[0], msg.force_direction[1], 81 msg.force_direction[2]) 82 83 # If this hit came from a player, log them as the last to touch us. 84 s_player = msg.get_source_player(Player) 85 if s_player is not None: 86 activity = self._activity() 87 if activity: 88 if s_player in activity.players: 89 self.last_players_to_touch[s_player.team.id] = s_player 90 else: 91 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
98class Team(ba.Team[Player]): 99 """Our team type for this game.""" 100 101 def __init__(self) -> None: 102 self.score = 0
Our team type for this game.
Inherited Members
106class HockeyGame(ba.TeamGameActivity[Player, Team]): 107 """Ice hockey game.""" 108 109 name = 'Hockey' 110 description = 'Score some goals.' 111 available_settings = [ 112 ba.IntSetting( 113 'Score to Win', 114 min_value=1, 115 default=1, 116 increment=1, 117 ), 118 ba.IntChoiceSetting( 119 'Time Limit', 120 choices=[ 121 ('None', 0), 122 ('1 Minute', 60), 123 ('2 Minutes', 120), 124 ('5 Minutes', 300), 125 ('10 Minutes', 600), 126 ('20 Minutes', 1200), 127 ], 128 default=0, 129 ), 130 ba.FloatChoiceSetting( 131 'Respawn Times', 132 choices=[ 133 ('Shorter', 0.25), 134 ('Short', 0.5), 135 ('Normal', 1.0), 136 ('Long', 2.0), 137 ('Longer', 4.0), 138 ], 139 default=1.0, 140 ), 141 ] 142 default_music = ba.MusicType.HOCKEY 143 144 @classmethod 145 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 146 return issubclass(sessiontype, ba.DualTeamSession) 147 148 @classmethod 149 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 150 return ba.getmaps('hockey') 151 152 def __init__(self, settings: dict): 153 super().__init__(settings) 154 shared = SharedObjects.get() 155 self._scoreboard = Scoreboard() 156 self._cheer_sound = ba.getsound('cheer') 157 self._chant_sound = ba.getsound('crowdChant') 158 self._foghorn_sound = ba.getsound('foghorn') 159 self._swipsound = ba.getsound('swip') 160 self._whistle_sound = ba.getsound('refWhistle') 161 self.puck_model = ba.getmodel('puck') 162 self.puck_tex = ba.gettexture('puckColor') 163 self._puck_sound = ba.getsound('metalHit') 164 self.puck_material = ba.Material() 165 self.puck_material.add_actions(actions=(('modify_part_collision', 166 'friction', 0.5))) 167 self.puck_material.add_actions(conditions=('they_have_material', 168 shared.pickup_material), 169 actions=('modify_part_collision', 170 'collide', False)) 171 self.puck_material.add_actions( 172 conditions=( 173 ('we_are_younger_than', 100), 174 'and', 175 ('they_have_material', shared.object_material), 176 ), 177 actions=('modify_node_collision', 'collide', False), 178 ) 179 self.puck_material.add_actions(conditions=('they_have_material', 180 shared.footing_material), 181 actions=('impact_sound', 182 self._puck_sound, 0.2, 5)) 183 184 # Keep track of which player last touched the puck 185 self.puck_material.add_actions( 186 conditions=('they_have_material', shared.player_material), 187 actions=(('call', 'at_connect', 188 self._handle_puck_player_collide), )) 189 190 # We want the puck to kill powerups; not get stopped by them 191 self.puck_material.add_actions( 192 conditions=('they_have_material', 193 PowerupBoxFactory.get().powerup_material), 194 actions=(('modify_part_collision', 'physical', False), 195 ('message', 'their_node', 'at_connect', ba.DieMessage()))) 196 self._score_region_material = ba.Material() 197 self._score_region_material.add_actions( 198 conditions=('they_have_material', self.puck_material), 199 actions=(('modify_part_collision', 'collide', 200 True), ('modify_part_collision', 'physical', False), 201 ('call', 'at_connect', self._handle_score))) 202 self._puck_spawn_pos: Sequence[float] | None = None 203 self._score_regions: list[ba.NodeActor] | None = None 204 self._puck: Puck | None = None 205 self._score_to_win = int(settings['Score to Win']) 206 self._time_limit = float(settings['Time Limit']) 207 208 def get_instance_description(self) -> str | Sequence: 209 if self._score_to_win == 1: 210 return 'Score a goal.' 211 return 'Score ${ARG1} goals.', self._score_to_win 212 213 def get_instance_description_short(self) -> str | Sequence: 214 if self._score_to_win == 1: 215 return 'score a goal' 216 return 'score ${ARG1} goals', self._score_to_win 217 218 def on_begin(self) -> None: 219 super().on_begin() 220 221 self.setup_standard_time_limit(self._time_limit) 222 self.setup_standard_powerup_drops() 223 self._puck_spawn_pos = self.map.get_flag_position(None) 224 self._spawn_puck() 225 226 # Set up the two score regions. 227 defs = self.map.defs 228 self._score_regions = [] 229 self._score_regions.append( 230 ba.NodeActor( 231 ba.newnode('region', 232 attrs={ 233 'position': defs.boxes['goal1'][0:3], 234 'scale': defs.boxes['goal1'][6:9], 235 'type': 'box', 236 'materials': [self._score_region_material] 237 }))) 238 self._score_regions.append( 239 ba.NodeActor( 240 ba.newnode('region', 241 attrs={ 242 'position': defs.boxes['goal2'][0:3], 243 'scale': defs.boxes['goal2'][6:9], 244 'type': 'box', 245 'materials': [self._score_region_material] 246 }))) 247 self._update_scoreboard() 248 ba.playsound(self._chant_sound) 249 250 def on_team_join(self, team: Team) -> None: 251 self._update_scoreboard() 252 253 def _handle_puck_player_collide(self) -> None: 254 collision = ba.getcollision() 255 try: 256 puck = collision.sourcenode.getdelegate(Puck, True) 257 player = collision.opposingnode.getdelegate(PlayerSpaz, 258 True).getplayer( 259 Player, True) 260 except ba.NotFoundError: 261 return 262 263 puck.last_players_to_touch[player.team.id] = player 264 265 def _kill_puck(self) -> None: 266 self._puck = None 267 268 def _handle_score(self) -> None: 269 """A point has been scored.""" 270 271 assert self._puck is not None 272 assert self._score_regions is not None 273 274 # Our puck might stick around for a second or two 275 # we don't want it to be able to score again. 276 if self._puck.scored: 277 return 278 279 region = ba.getcollision().sourcenode 280 index = 0 281 for index, score_region in enumerate(self._score_regions): 282 if region == score_region.node: 283 break 284 285 for team in self.teams: 286 if team.id == index: 287 scoring_team = team 288 team.score += 1 289 290 # Tell all players to celebrate. 291 for player in team.players: 292 if player.actor: 293 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 294 295 # If we've got the player from the scoring team that last 296 # touched us, give them points. 297 if (scoring_team.id in self._puck.last_players_to_touch 298 and self._puck.last_players_to_touch[scoring_team.id]): 299 self.stats.player_scored( 300 self._puck.last_players_to_touch[scoring_team.id], 301 100, 302 big_message=True) 303 304 # End game if we won. 305 if team.score >= self._score_to_win: 306 self.end_game() 307 308 ba.playsound(self._foghorn_sound) 309 ba.playsound(self._cheer_sound) 310 311 self._puck.scored = True 312 313 # Kill the puck (it'll respawn itself shortly). 314 ba.timer(1.0, self._kill_puck) 315 316 light = ba.newnode('light', 317 attrs={ 318 'position': ba.getcollision().position, 319 'height_attenuated': False, 320 'color': (1, 0, 0) 321 }) 322 ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) 323 ba.timer(1.0, light.delete) 324 325 ba.cameraflash(duration=10.0) 326 self._update_scoreboard() 327 328 def end_game(self) -> None: 329 results = ba.GameResults() 330 for team in self.teams: 331 results.set_team_score(team, team.score) 332 self.end(results=results) 333 334 def _update_scoreboard(self) -> None: 335 winscore = self._score_to_win 336 for team in self.teams: 337 self._scoreboard.set_team_value(team, team.score, winscore) 338 339 def handlemessage(self, msg: Any) -> Any: 340 341 # Respawn dead players if they're still in the game. 342 if isinstance(msg, ba.PlayerDiedMessage): 343 # Augment standard behavior... 344 super().handlemessage(msg) 345 self.respawn_player(msg.getplayer(Player)) 346 347 # Respawn dead pucks. 348 elif isinstance(msg, PuckDiedMessage): 349 if not self.has_ended(): 350 ba.timer(3.0, self._spawn_puck) 351 else: 352 super().handlemessage(msg) 353 354 def _flash_puck_spawn(self) -> None: 355 light = ba.newnode('light', 356 attrs={ 357 'position': self._puck_spawn_pos, 358 'height_attenuated': False, 359 'color': (1, 0, 0) 360 }) 361 ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) 362 ba.timer(1.0, light.delete) 363 364 def _spawn_puck(self) -> None: 365 ba.playsound(self._swipsound) 366 ba.playsound(self._whistle_sound) 367 self._flash_puck_spawn() 368 assert self._puck_spawn_pos is not None 369 self._puck = Puck(position=self._puck_spawn_pos)
Ice hockey game.
152 def __init__(self, settings: dict): 153 super().__init__(settings) 154 shared = SharedObjects.get() 155 self._scoreboard = Scoreboard() 156 self._cheer_sound = ba.getsound('cheer') 157 self._chant_sound = ba.getsound('crowdChant') 158 self._foghorn_sound = ba.getsound('foghorn') 159 self._swipsound = ba.getsound('swip') 160 self._whistle_sound = ba.getsound('refWhistle') 161 self.puck_model = ba.getmodel('puck') 162 self.puck_tex = ba.gettexture('puckColor') 163 self._puck_sound = ba.getsound('metalHit') 164 self.puck_material = ba.Material() 165 self.puck_material.add_actions(actions=(('modify_part_collision', 166 'friction', 0.5))) 167 self.puck_material.add_actions(conditions=('they_have_material', 168 shared.pickup_material), 169 actions=('modify_part_collision', 170 'collide', False)) 171 self.puck_material.add_actions( 172 conditions=( 173 ('we_are_younger_than', 100), 174 'and', 175 ('they_have_material', shared.object_material), 176 ), 177 actions=('modify_node_collision', 'collide', False), 178 ) 179 self.puck_material.add_actions(conditions=('they_have_material', 180 shared.footing_material), 181 actions=('impact_sound', 182 self._puck_sound, 0.2, 5)) 183 184 # Keep track of which player last touched the puck 185 self.puck_material.add_actions( 186 conditions=('they_have_material', shared.player_material), 187 actions=(('call', 'at_connect', 188 self._handle_puck_player_collide), )) 189 190 # We want the puck to kill powerups; not get stopped by them 191 self.puck_material.add_actions( 192 conditions=('they_have_material', 193 PowerupBoxFactory.get().powerup_material), 194 actions=(('modify_part_collision', 'physical', False), 195 ('message', 'their_node', 'at_connect', ba.DieMessage()))) 196 self._score_region_material = ba.Material() 197 self._score_region_material.add_actions( 198 conditions=('they_have_material', self.puck_material), 199 actions=(('modify_part_collision', 'collide', 200 True), ('modify_part_collision', 'physical', False), 201 ('call', 'at_connect', self._handle_score))) 202 self._puck_spawn_pos: Sequence[float] | None = None 203 self._score_regions: list[ba.NodeActor] | None = None 204 self._puck: Puck | None = None 205 self._score_to_win = int(settings['Score to Win']) 206 self._time_limit = float(settings['Time Limit'])
Instantiate the Activity.
144 @classmethod 145 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 146 return issubclass(sessiontype, ba.DualTeamSession)
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
148 @classmethod 149 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 150 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.
208 def get_instance_description(self) -> str | Sequence: 209 if self._score_to_win == 1: 210 return 'Score a goal.' 211 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.
213 def get_instance_description_short(self) -> str | Sequence: 214 if self._score_to_win == 1: 215 return 'score a goal' 216 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.
218 def on_begin(self) -> None: 219 super().on_begin() 220 221 self.setup_standard_time_limit(self._time_limit) 222 self.setup_standard_powerup_drops() 223 self._puck_spawn_pos = self.map.get_flag_position(None) 224 self._spawn_puck() 225 226 # Set up the two score regions. 227 defs = self.map.defs 228 self._score_regions = [] 229 self._score_regions.append( 230 ba.NodeActor( 231 ba.newnode('region', 232 attrs={ 233 'position': defs.boxes['goal1'][0:3], 234 'scale': defs.boxes['goal1'][6:9], 235 'type': 'box', 236 'materials': [self._score_region_material] 237 }))) 238 self._score_regions.append( 239 ba.NodeActor( 240 ba.newnode('region', 241 attrs={ 242 'position': defs.boxes['goal2'][0:3], 243 'scale': defs.boxes['goal2'][6:9], 244 'type': 'box', 245 'materials': [self._score_region_material] 246 }))) 247 self._update_scoreboard() 248 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)
328 def end_game(self) -> None: 329 results = ba.GameResults() 330 for team in self.teams: 331 results.set_team_score(team, team.score) 332 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.
339 def handlemessage(self, msg: Any) -> Any: 340 341 # Respawn dead players if they're still in the game. 342 if isinstance(msg, ba.PlayerDiedMessage): 343 # Augment standard behavior... 344 super().handlemessage(msg) 345 self.respawn_player(msg.getplayer(Player)) 346 347 # Respawn dead pucks. 348 elif isinstance(msg, PuckDiedMessage): 349 if not self.has_ended(): 350 ba.timer(3.0, self._spawn_puck) 351 else: 352 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
- tips
- scoreconfig
- allow_pausing
- allow_kick_idle_players
- show_kill_points
- 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
- slow_motion
- 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