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)
class PuckDiedMessage:
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.

PuckDiedMessage(puck: bastd.game.hockey.Puck)
26    def __init__(self, puck: Puck):
27        self.puck = puck
class Puck(ba._actor.Actor):
 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.

Puck(position: Sequence[float] = (0.0, 1.0, 0.0))
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.

def handlemessage(self, msg: Any) -> Any:
 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
class Player(ba._player.Player[ForwardRef('Team')]):
106class Player(ba.Player['Team']):
107    """Our player type for this game."""

Our player type for this game.

Player()
Inherited Members
ba._player.Player
actor
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(ba._team.Team[bastd.game.hockey.Player]):
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.

Team()
113    def __init__(self) -> None:
114        self.score = 0
Inherited Members
ba._team.Team
manual_init
customdata
on_expire
sessionteam
class HockeyGame(ba._teamgame.TeamGameActivity[bastd.game.hockey.Player, bastd.game.hockey.Team]):
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.

HockeyGame(settings: dict)
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.

@classmethod
def supports_session_type(cls, sessiontype: type[ba._session.Session]) -> bool:
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.

@classmethod
def get_supported_maps(cls, sessiontype: type[ba._session.Session]) -> list[str]:
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.

slow_motion = False

If True, runs in slow motion and turns down sound pitch.

def get_instance_description(self) -> Union[str, Sequence]:
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.

def get_instance_description_short(self) -> Union[str, Sequence]:
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.

def on_begin(self) -> None:
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.

def on_team_join(self, team: bastd.game.hockey.Team) -> None:
284    def on_team_join(self, team: Team) -> None:
285        self._update_scoreboard()

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

def end_game(self) -> None:
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.

def handlemessage(self, msg: Any) -> Any:
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