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 8 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10from typing import TYPE_CHECKING 11 12from bascenev1lib.actor.playerspaz import PlayerSpaz 13from bascenev1lib.actor.scoreboard import Scoreboard 14from bascenev1lib.actor.powerupbox import PowerupBoxFactory 15from bascenev1lib.gameutils import SharedObjects 16import bascenev1 as bs 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(bs.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 = bs.newnode( 45 'prop', 46 delegate=self, 47 attrs={ 48 'mesh': activity.puck_mesh, 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 bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1}) 60 61 def handlemessage(self, msg: Any) -> Any: 62 if isinstance(msg, bs.DieMessage): 63 if 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, bs.OutOfBoundsMessage): 71 assert self.node 72 self.node.position = self._spawn_pos 73 74 elif isinstance(msg, bs.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(bs.Player['Team']): 106 """Our player type for this game.""" 107 108 109class Team(bs.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 bascenev1.GameActivity 117class HockeyGame(bs.TeamGameActivity[Player, Team]): 118 """Ice hockey game.""" 119 120 name = 'Hockey' 121 description = 'Score some goals.' 122 available_settings = [ 123 bs.IntSetting( 124 'Score to Win', 125 min_value=1, 126 default=1, 127 increment=1, 128 ), 129 bs.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 bs.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 bs.BoolSetting('Epic Mode', default=False), 153 ] 154 155 @classmethod 156 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 157 return issubclass(sessiontype, bs.DualTeamSession) 158 159 @classmethod 160 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 161 assert bs.app.classic is not None 162 return bs.app.classic.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 = bs.getsound('cheer') 169 self._chant_sound = bs.getsound('crowdChant') 170 self._foghorn_sound = bs.getsound('foghorn') 171 self._swipsound = bs.getsound('swip') 172 self._whistle_sound = bs.getsound('refWhistle') 173 self.puck_mesh = bs.getmesh('puck') 174 self.puck_tex = bs.gettexture('puckColor') 175 self._puck_sound = bs.getsound('metalHit') 176 self.puck_material = bs.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', bs.DieMessage()), 212 ), 213 ) 214 self._score_region_material = bs.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[bs.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 bs.MusicType.EPIC if self._epic_mode else bs.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 bs.NodeActor( 257 bs.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 bs.NodeActor( 270 bs.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 self._chant_sound.play() 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 = bs.getcollision() 289 try: 290 puck = collision.sourcenode.getdelegate(Puck, True) 291 player = collision.opposingnode.getdelegate( 292 PlayerSpaz, True 293 ).getplayer(Player, True) 294 except bs.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 = bs.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(bs.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 self._foghorn_sound.play() 346 self._cheer_sound.play() 347 348 self._puck.scored = True 349 350 # Kill the puck (it'll respawn itself shortly). 351 bs.timer(1.0, self._kill_puck) 352 353 light = bs.newnode( 354 'light', 355 attrs={ 356 'position': bs.getcollision().position, 357 'height_attenuated': False, 358 'color': (1, 0, 0), 359 }, 360 ) 361 bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) 362 bs.timer(1.0, light.delete) 363 364 bs.cameraflash(duration=10.0) 365 self._update_scoreboard() 366 367 def end_game(self) -> None: 368 results = bs.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 # Respawn dead players if they're still in the game. 380 if isinstance(msg, bs.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 bs.timer(3.0, self._spawn_puck) 389 else: 390 super().handlemessage(msg) 391 392 def _flash_puck_spawn(self) -> None: 393 light = bs.newnode( 394 'light', 395 attrs={ 396 'position': self._puck_spawn_pos, 397 'height_attenuated': False, 398 'color': (1, 0, 0), 399 }, 400 ) 401 bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) 402 bs.timer(1.0, light.delete) 403 404 def _spawn_puck(self) -> None: 405 self._swipsound.play() 406 self._whistle_sound.play() 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(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 def handlemessage(self, msg: Any) -> Any: 63 if isinstance(msg, bs.DieMessage): 64 if 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, bs.OutOfBoundsMessage): 72 assert self.node 73 self.node.position = self._spawn_pos 74 75 elif isinstance(msg, bs.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 = 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})
Instantiates an Actor in the current bascenev1.Activity.
62 def handlemessage(self, msg: Any) -> Any: 63 if isinstance(msg, bs.DieMessage): 64 if 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, bs.OutOfBoundsMessage): 72 assert self.node 73 self.node.position = self._spawn_pos 74 75 elif isinstance(msg, bs.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
- bascenev1._actor.Actor
- autoretain
- on_expire
- expired
- exists
- is_alive
- activity
- getactivity
Our player type for this game.
Inherited Members
- bascenev1._player.Player
- character
- actor
- color
- highlight
- on_expire
- team
- customdata
- sessionplayer
- node
- position
- exists
- getname
- is_alive
- get_icon
- assigninput
- resetinput
110class Team(bs.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
- bascenev1._team.Team
- players
- id
- name
- color
- manual_init
- customdata
- on_expire
- sessionteam
118class HockeyGame(bs.TeamGameActivity[Player, Team]): 119 """Ice hockey game.""" 120 121 name = 'Hockey' 122 description = 'Score some goals.' 123 available_settings = [ 124 bs.IntSetting( 125 'Score to Win', 126 min_value=1, 127 default=1, 128 increment=1, 129 ), 130 bs.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 bs.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 bs.BoolSetting('Epic Mode', default=False), 154 ] 155 156 @classmethod 157 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 158 return issubclass(sessiontype, bs.DualTeamSession) 159 160 @classmethod 161 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 162 assert bs.app.classic is not None 163 return bs.app.classic.getmaps('hockey') 164 165 def __init__(self, settings: dict): 166 super().__init__(settings) 167 shared = SharedObjects.get() 168 self._scoreboard = Scoreboard() 169 self._cheer_sound = bs.getsound('cheer') 170 self._chant_sound = bs.getsound('crowdChant') 171 self._foghorn_sound = bs.getsound('foghorn') 172 self._swipsound = bs.getsound('swip') 173 self._whistle_sound = bs.getsound('refWhistle') 174 self.puck_mesh = bs.getmesh('puck') 175 self.puck_tex = bs.gettexture('puckColor') 176 self._puck_sound = bs.getsound('metalHit') 177 self.puck_material = bs.Material() 178 self.puck_material.add_actions( 179 actions=('modify_part_collision', 'friction', 0.5) 180 ) 181 self.puck_material.add_actions( 182 conditions=('they_have_material', shared.pickup_material), 183 actions=('modify_part_collision', 'collide', False), 184 ) 185 self.puck_material.add_actions( 186 conditions=( 187 ('we_are_younger_than', 100), 188 'and', 189 ('they_have_material', shared.object_material), 190 ), 191 actions=('modify_node_collision', 'collide', False), 192 ) 193 self.puck_material.add_actions( 194 conditions=('they_have_material', shared.footing_material), 195 actions=('impact_sound', self._puck_sound, 0.2, 5), 196 ) 197 198 # Keep track of which player last touched the puck 199 self.puck_material.add_actions( 200 conditions=('they_have_material', shared.player_material), 201 actions=(('call', 'at_connect', self._handle_puck_player_collide),), 202 ) 203 204 # We want the puck to kill powerups; not get stopped by them 205 self.puck_material.add_actions( 206 conditions=( 207 'they_have_material', 208 PowerupBoxFactory.get().powerup_material, 209 ), 210 actions=( 211 ('modify_part_collision', 'physical', False), 212 ('message', 'their_node', 'at_connect', bs.DieMessage()), 213 ), 214 ) 215 self._score_region_material = bs.Material() 216 self._score_region_material.add_actions( 217 conditions=('they_have_material', self.puck_material), 218 actions=( 219 ('modify_part_collision', 'collide', True), 220 ('modify_part_collision', 'physical', False), 221 ('call', 'at_connect', self._handle_score), 222 ), 223 ) 224 self._puck_spawn_pos: Sequence[float] | None = None 225 self._score_regions: list[bs.NodeActor] | None = None 226 self._puck: Puck | None = None 227 self._score_to_win = int(settings['Score to Win']) 228 self._time_limit = float(settings['Time Limit']) 229 self._epic_mode = bool(settings['Epic Mode']) 230 self.slow_motion = self._epic_mode 231 self.default_music = ( 232 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.HOCKEY 233 ) 234 235 def get_instance_description(self) -> str | Sequence: 236 if self._score_to_win == 1: 237 return 'Score a goal.' 238 return 'Score ${ARG1} goals.', self._score_to_win 239 240 def get_instance_description_short(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 def on_begin(self) -> None: 246 super().on_begin() 247 248 self.setup_standard_time_limit(self._time_limit) 249 self.setup_standard_powerup_drops() 250 self._puck_spawn_pos = self.map.get_flag_position(None) 251 self._spawn_puck() 252 253 # Set up the two score regions. 254 defs = self.map.defs 255 self._score_regions = [] 256 self._score_regions.append( 257 bs.NodeActor( 258 bs.newnode( 259 'region', 260 attrs={ 261 'position': defs.boxes['goal1'][0:3], 262 'scale': defs.boxes['goal1'][6:9], 263 'type': 'box', 264 'materials': [self._score_region_material], 265 }, 266 ) 267 ) 268 ) 269 self._score_regions.append( 270 bs.NodeActor( 271 bs.newnode( 272 'region', 273 attrs={ 274 'position': defs.boxes['goal2'][0:3], 275 'scale': defs.boxes['goal2'][6:9], 276 'type': 'box', 277 'materials': [self._score_region_material], 278 }, 279 ) 280 ) 281 ) 282 self._update_scoreboard() 283 self._chant_sound.play() 284 285 def on_team_join(self, team: Team) -> None: 286 self._update_scoreboard() 287 288 def _handle_puck_player_collide(self) -> None: 289 collision = bs.getcollision() 290 try: 291 puck = collision.sourcenode.getdelegate(Puck, True) 292 player = collision.opposingnode.getdelegate( 293 PlayerSpaz, True 294 ).getplayer(Player, True) 295 except bs.NotFoundError: 296 return 297 298 puck.last_players_to_touch[player.team.id] = player 299 300 def _kill_puck(self) -> None: 301 self._puck = None 302 303 def _handle_score(self) -> None: 304 """A point has been scored.""" 305 306 assert self._puck is not None 307 assert self._score_regions is not None 308 309 # Our puck might stick around for a second or two 310 # we don't want it to be able to score again. 311 if self._puck.scored: 312 return 313 314 region = bs.getcollision().sourcenode 315 index = 0 316 for index, score_region in enumerate(self._score_regions): 317 if region == score_region.node: 318 break 319 320 for team in self.teams: 321 if team.id == index: 322 scoring_team = team 323 team.score += 1 324 325 # Tell all players to celebrate. 326 for player in team.players: 327 if player.actor: 328 player.actor.handlemessage(bs.CelebrateMessage(2.0)) 329 330 # If we've got the player from the scoring team that last 331 # touched us, give them points. 332 if ( 333 scoring_team.id in self._puck.last_players_to_touch 334 and self._puck.last_players_to_touch[scoring_team.id] 335 ): 336 self.stats.player_scored( 337 self._puck.last_players_to_touch[scoring_team.id], 338 100, 339 big_message=True, 340 ) 341 342 # End game if we won. 343 if team.score >= self._score_to_win: 344 self.end_game() 345 346 self._foghorn_sound.play() 347 self._cheer_sound.play() 348 349 self._puck.scored = True 350 351 # Kill the puck (it'll respawn itself shortly). 352 bs.timer(1.0, self._kill_puck) 353 354 light = bs.newnode( 355 'light', 356 attrs={ 357 'position': bs.getcollision().position, 358 'height_attenuated': False, 359 'color': (1, 0, 0), 360 }, 361 ) 362 bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) 363 bs.timer(1.0, light.delete) 364 365 bs.cameraflash(duration=10.0) 366 self._update_scoreboard() 367 368 def end_game(self) -> None: 369 results = bs.GameResults() 370 for team in self.teams: 371 results.set_team_score(team, team.score) 372 self.end(results=results) 373 374 def _update_scoreboard(self) -> None: 375 winscore = self._score_to_win 376 for team in self.teams: 377 self._scoreboard.set_team_value(team, team.score, winscore) 378 379 def handlemessage(self, msg: Any) -> Any: 380 # Respawn dead players if they're still in the game. 381 if isinstance(msg, bs.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 bs.timer(3.0, self._spawn_puck) 390 else: 391 super().handlemessage(msg) 392 393 def _flash_puck_spawn(self) -> None: 394 light = bs.newnode( 395 'light', 396 attrs={ 397 'position': self._puck_spawn_pos, 398 'height_attenuated': False, 399 'color': (1, 0, 0), 400 }, 401 ) 402 bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) 403 bs.timer(1.0, light.delete) 404 405 def _spawn_puck(self) -> None: 406 self._swipsound.play() 407 self._whistle_sound.play() 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.
165 def __init__(self, settings: dict): 166 super().__init__(settings) 167 shared = SharedObjects.get() 168 self._scoreboard = Scoreboard() 169 self._cheer_sound = bs.getsound('cheer') 170 self._chant_sound = bs.getsound('crowdChant') 171 self._foghorn_sound = bs.getsound('foghorn') 172 self._swipsound = bs.getsound('swip') 173 self._whistle_sound = bs.getsound('refWhistle') 174 self.puck_mesh = bs.getmesh('puck') 175 self.puck_tex = bs.gettexture('puckColor') 176 self._puck_sound = bs.getsound('metalHit') 177 self.puck_material = bs.Material() 178 self.puck_material.add_actions( 179 actions=('modify_part_collision', 'friction', 0.5) 180 ) 181 self.puck_material.add_actions( 182 conditions=('they_have_material', shared.pickup_material), 183 actions=('modify_part_collision', 'collide', False), 184 ) 185 self.puck_material.add_actions( 186 conditions=( 187 ('we_are_younger_than', 100), 188 'and', 189 ('they_have_material', shared.object_material), 190 ), 191 actions=('modify_node_collision', 'collide', False), 192 ) 193 self.puck_material.add_actions( 194 conditions=('they_have_material', shared.footing_material), 195 actions=('impact_sound', self._puck_sound, 0.2, 5), 196 ) 197 198 # Keep track of which player last touched the puck 199 self.puck_material.add_actions( 200 conditions=('they_have_material', shared.player_material), 201 actions=(('call', 'at_connect', self._handle_puck_player_collide),), 202 ) 203 204 # We want the puck to kill powerups; not get stopped by them 205 self.puck_material.add_actions( 206 conditions=( 207 'they_have_material', 208 PowerupBoxFactory.get().powerup_material, 209 ), 210 actions=( 211 ('modify_part_collision', 'physical', False), 212 ('message', 'their_node', 'at_connect', bs.DieMessage()), 213 ), 214 ) 215 self._score_region_material = bs.Material() 216 self._score_region_material.add_actions( 217 conditions=('they_have_material', self.puck_material), 218 actions=( 219 ('modify_part_collision', 'collide', True), 220 ('modify_part_collision', 'physical', False), 221 ('call', 'at_connect', self._handle_score), 222 ), 223 ) 224 self._puck_spawn_pos: Sequence[float] | None = None 225 self._score_regions: list[bs.NodeActor] | None = None 226 self._puck: Puck | None = None 227 self._score_to_win = int(settings['Score to Win']) 228 self._time_limit = float(settings['Time Limit']) 229 self._epic_mode = bool(settings['Epic Mode']) 230 self.slow_motion = self._epic_mode 231 self.default_music = ( 232 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.HOCKEY 233 )
Instantiate the Activity.
156 @classmethod 157 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 158 return issubclass(sessiontype, bs.DualTeamSession)
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
160 @classmethod 161 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 162 assert bs.app.classic is not None 163 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.
235 def get_instance_description(self) -> str | Sequence: 236 if self._score_to_win == 1: 237 return 'Score a goal.' 238 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.
240 def get_instance_description_short(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 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.
245 def on_begin(self) -> None: 246 super().on_begin() 247 248 self.setup_standard_time_limit(self._time_limit) 249 self.setup_standard_powerup_drops() 250 self._puck_spawn_pos = self.map.get_flag_position(None) 251 self._spawn_puck() 252 253 # Set up the two score regions. 254 defs = self.map.defs 255 self._score_regions = [] 256 self._score_regions.append( 257 bs.NodeActor( 258 bs.newnode( 259 'region', 260 attrs={ 261 'position': defs.boxes['goal1'][0:3], 262 'scale': defs.boxes['goal1'][6:9], 263 'type': 'box', 264 'materials': [self._score_region_material], 265 }, 266 ) 267 ) 268 ) 269 self._score_regions.append( 270 bs.NodeActor( 271 bs.newnode( 272 'region', 273 attrs={ 274 'position': defs.boxes['goal2'][0:3], 275 'scale': defs.boxes['goal2'][6:9], 276 'type': 'box', 277 'materials': [self._score_region_material], 278 }, 279 ) 280 ) 281 ) 282 self._update_scoreboard() 283 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)
368 def end_game(self) -> None: 369 results = bs.GameResults() 370 for team in self.teams: 371 results.set_team_score(team, team.score) 372 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.
379 def handlemessage(self, msg: Any) -> Any: 380 # Respawn dead players if they're still in the game. 381 if isinstance(msg, bs.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 bs.timer(3.0, self._spawn_puck) 390 else: 391 super().handlemessage(msg)
General message handling; can be passed any message object.
Inherited Members
- bascenev1._teamgame.TeamGameActivity
- on_transition_in
- spawn_player_spaz
- end
- bascenev1._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
- initialplayerinfos
- 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
- bascenev1._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
- paused_text
- preloads
- lobby
- context
- 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
- bascenev1._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps