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)
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('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.

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('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.

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

Our player type for this game.

Player()
class Team(ba._team.Team[bastd.game.hockey.Player]):
 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.

Team()
101    def __init__(self) -> None:
102        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]):
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.

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

name: str | None = 'Hockey'
description: str | None = 'Score some goals.'
available_settings: list[ba._settings.Setting] | None = [IntSetting(name='Score to Win', default=1, min_value=1, max_value=9999, increment=1), IntChoiceSetting(name='Time Limit', default=0, choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)]), FloatChoiceSetting(name='Respawn Times', default=1.0, choices=[('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)])]
default_music: ba._music.MusicType | None = <MusicType.HOCKEY: 'Hockey'>
@classmethod
def supports_session_type(cls, sessiontype: type[ba._session.Session]) -> bool:
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.

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

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

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

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

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

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

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

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