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

Inform something that a puck has died.

PuckDiedMessage(puck: Puck)
27    def __init__(self, puck: Puck):
28        self.puck = puck
puck
class Puck(bascenev1._actor.Actor):
 31class Puck(bs.Actor):
 32    """A lovely giant hockey puck."""
 33
 34    def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)):
 35        super().__init__()
 36        shared = SharedObjects.get()
 37        activity = self.getactivity()
 38
 39        # Spawn just above the provided point.
 40        self._spawn_pos = (position[0], position[1] + 1.0, position[2])
 41        self.last_players_to_touch: dict[int, Player] = {}
 42        self.scored = False
 43        assert activity is not None
 44        assert isinstance(activity, HockeyGame)
 45        pmats = [shared.object_material, activity.puck_material]
 46        self.node = bs.newnode(
 47            'prop',
 48            delegate=self,
 49            attrs={
 50                'mesh': activity.puck_mesh,
 51                'color_texture': activity.puck_tex,
 52                'body': 'puck',
 53                'reflection': 'soft',
 54                'reflection_scale': [0.2],
 55                'shadow_size': 1.0,
 56                'is_area_of_interest': True,
 57                'position': self._spawn_pos,
 58                'materials': pmats,
 59            },
 60        )
 61        bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1})
 62
 63    @override
 64    def handlemessage(self, msg: Any) -> Any:
 65        if isinstance(msg, bs.DieMessage):
 66            if self.node:
 67                self.node.delete()
 68                activity = self._activity()
 69                if activity and not msg.immediate:
 70                    activity.handlemessage(PuckDiedMessage(self))
 71
 72        # If we go out of bounds, move back to where we started.
 73        elif isinstance(msg, bs.OutOfBoundsMessage):
 74            assert self.node
 75            self.node.position = self._spawn_pos
 76
 77        elif isinstance(msg, bs.HitMessage):
 78            assert self.node
 79            assert msg.force_direction is not None
 80            self.node.handlemessage(
 81                'impulse',
 82                msg.pos[0],
 83                msg.pos[1],
 84                msg.pos[2],
 85                msg.velocity[0],
 86                msg.velocity[1],
 87                msg.velocity[2],
 88                1.0 * msg.magnitude,
 89                1.0 * msg.velocity_magnitude,
 90                msg.radius,
 91                0,
 92                msg.force_direction[0],
 93                msg.force_direction[1],
 94                msg.force_direction[2],
 95            )
 96
 97            # If this hit came from a player, log them as the last to touch us.
 98            s_player = msg.get_source_player(Player)
 99            if s_player is not None:
100                activity = self._activity()
101                if activity:
102                    if s_player in activity.players:
103                        self.last_players_to_touch[s_player.team.id] = s_player
104        else:
105            super().handlemessage(msg)

A lovely giant hockey puck.

Puck(position: Sequence[float] = (0.0, 1.0, 0.0))
34    def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)):
35        super().__init__()
36        shared = SharedObjects.get()
37        activity = self.getactivity()
38
39        # Spawn just above the provided point.
40        self._spawn_pos = (position[0], position[1] + 1.0, position[2])
41        self.last_players_to_touch: dict[int, Player] = {}
42        self.scored = False
43        assert activity is not None
44        assert isinstance(activity, HockeyGame)
45        pmats = [shared.object_material, activity.puck_material]
46        self.node = bs.newnode(
47            'prop',
48            delegate=self,
49            attrs={
50                'mesh': activity.puck_mesh,
51                'color_texture': activity.puck_tex,
52                'body': 'puck',
53                'reflection': 'soft',
54                'reflection_scale': [0.2],
55                'shadow_size': 1.0,
56                'is_area_of_interest': True,
57                'position': self._spawn_pos,
58                'materials': pmats,
59            },
60        )
61        bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1})

Instantiates an Actor in the current bascenev1.Activity.

last_players_to_touch: dict[int, Player]
scored
node
@override
def handlemessage(self, msg: Any) -> Any:
 63    @override
 64    def handlemessage(self, msg: Any) -> Any:
 65        if isinstance(msg, bs.DieMessage):
 66            if self.node:
 67                self.node.delete()
 68                activity = self._activity()
 69                if activity and not msg.immediate:
 70                    activity.handlemessage(PuckDiedMessage(self))
 71
 72        # If we go out of bounds, move back to where we started.
 73        elif isinstance(msg, bs.OutOfBoundsMessage):
 74            assert self.node
 75            self.node.position = self._spawn_pos
 76
 77        elif isinstance(msg, bs.HitMessage):
 78            assert self.node
 79            assert msg.force_direction is not None
 80            self.node.handlemessage(
 81                'impulse',
 82                msg.pos[0],
 83                msg.pos[1],
 84                msg.pos[2],
 85                msg.velocity[0],
 86                msg.velocity[1],
 87                msg.velocity[2],
 88                1.0 * msg.magnitude,
 89                1.0 * msg.velocity_magnitude,
 90                msg.radius,
 91                0,
 92                msg.force_direction[0],
 93                msg.force_direction[1],
 94                msg.force_direction[2],
 95            )
 96
 97            # If this hit came from a player, log them as the last to touch us.
 98            s_player = msg.get_source_player(Player)
 99            if s_player is not None:
100                activity = self._activity()
101                if activity:
102                    if s_player in activity.players:
103                        self.last_players_to_touch[s_player.team.id] = s_player
104        else:
105            super().handlemessage(msg)

General message handling; can be passed any message object.

class Player(bascenev1._player.Player[ForwardRef('Team')]):
108class Player(bs.Player['Team']):
109    """Our player type for this game."""

Our player type for this game.

class Team(bascenev1._team.Team[bascenev1lib.game.hockey.Player]):
112class Team(bs.Team[Player]):
113    """Our team type for this game."""
114
115    def __init__(self) -> None:
116        self.score = 0

Our team type for this game.

score
class HockeyGame(bascenev1._teamgame.TeamGameActivity[bascenev1lib.game.hockey.Player, bascenev1lib.game.hockey.Team]):
120class HockeyGame(bs.TeamGameActivity[Player, Team]):
121    """Ice hockey game."""
122
123    name = 'Hockey'
124    description = 'Score some goals.'
125    available_settings = [
126        bs.IntSetting(
127            'Score to Win',
128            min_value=1,
129            default=1,
130            increment=1,
131        ),
132        bs.IntChoiceSetting(
133            'Time Limit',
134            choices=[
135                ('None', 0),
136                ('1 Minute', 60),
137                ('2 Minutes', 120),
138                ('5 Minutes', 300),
139                ('10 Minutes', 600),
140                ('20 Minutes', 1200),
141            ],
142            default=0,
143        ),
144        bs.FloatChoiceSetting(
145            'Respawn Times',
146            choices=[
147                ('Shorter', 0.25),
148                ('Short', 0.5),
149                ('Normal', 1.0),
150                ('Long', 2.0),
151                ('Longer', 4.0),
152            ],
153            default=1.0,
154        ),
155        bs.BoolSetting('Epic Mode', default=False),
156    ]
157
158    @override
159    @classmethod
160    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
161        return issubclass(sessiontype, bs.DualTeamSession)
162
163    @override
164    @classmethod
165    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
166        assert bs.app.classic is not None
167        return bs.app.classic.getmaps('hockey')
168
169    def __init__(self, settings: dict):
170        super().__init__(settings)
171        shared = SharedObjects.get()
172        self._scoreboard = Scoreboard()
173        self._cheer_sound = bs.getsound('cheer')
174        self._chant_sound = bs.getsound('crowdChant')
175        self._foghorn_sound = bs.getsound('foghorn')
176        self._swipsound = bs.getsound('swip')
177        self._whistle_sound = bs.getsound('refWhistle')
178        self.puck_mesh = bs.getmesh('puck')
179        self.puck_tex = bs.gettexture('puckColor')
180        self._puck_sound = bs.getsound('metalHit')
181        self.puck_material = bs.Material()
182        self.puck_material.add_actions(
183            actions=('modify_part_collision', 'friction', 0.5)
184        )
185        self.puck_material.add_actions(
186            conditions=('they_have_material', shared.pickup_material),
187            actions=('modify_part_collision', 'collide', False),
188        )
189        self.puck_material.add_actions(
190            conditions=(
191                ('we_are_younger_than', 100),
192                'and',
193                ('they_have_material', shared.object_material),
194            ),
195            actions=('modify_node_collision', 'collide', False),
196        )
197        self.puck_material.add_actions(
198            conditions=('they_have_material', shared.footing_material),
199            actions=('impact_sound', self._puck_sound, 0.2, 5),
200        )
201
202        # Keep track of which player last touched the puck
203        self.puck_material.add_actions(
204            conditions=('they_have_material', shared.player_material),
205            actions=(('call', 'at_connect', self._handle_puck_player_collide),),
206        )
207
208        # We want the puck to kill powerups; not get stopped by them
209        self.puck_material.add_actions(
210            conditions=(
211                'they_have_material',
212                PowerupBoxFactory.get().powerup_material,
213            ),
214            actions=(
215                ('modify_part_collision', 'physical', False),
216                ('message', 'their_node', 'at_connect', bs.DieMessage()),
217            ),
218        )
219        self._score_region_material = bs.Material()
220        self._score_region_material.add_actions(
221            conditions=('they_have_material', self.puck_material),
222            actions=(
223                ('modify_part_collision', 'collide', True),
224                ('modify_part_collision', 'physical', False),
225                ('call', 'at_connect', self._handle_score),
226            ),
227        )
228        self._puck_spawn_pos: Sequence[float] | None = None
229        self._score_regions: list[bs.NodeActor] | None = None
230        self._puck: Puck | None = None
231        self._score_to_win = int(settings['Score to Win'])
232        self._time_limit = float(settings['Time Limit'])
233        self._epic_mode = bool(settings['Epic Mode'])
234        self.slow_motion = self._epic_mode
235        self.default_music = (
236            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.HOCKEY
237        )
238
239    @override
240    def get_instance_description(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    @override
246    def get_instance_description_short(self) -> str | Sequence:
247        if self._score_to_win == 1:
248            return 'score a goal'
249        return 'score ${ARG1} goals', self._score_to_win
250
251    @override
252    def on_begin(self) -> None:
253        super().on_begin()
254
255        self.setup_standard_time_limit(self._time_limit)
256        self.setup_standard_powerup_drops()
257        self._puck_spawn_pos = self.map.get_flag_position(None)
258        self._spawn_puck()
259
260        # Set up the two score regions.
261        defs = self.map.defs
262        self._score_regions = []
263        self._score_regions.append(
264            bs.NodeActor(
265                bs.newnode(
266                    'region',
267                    attrs={
268                        'position': defs.boxes['goal1'][0:3],
269                        'scale': defs.boxes['goal1'][6:9],
270                        'type': 'box',
271                        'materials': [self._score_region_material],
272                    },
273                )
274            )
275        )
276        self._score_regions.append(
277            bs.NodeActor(
278                bs.newnode(
279                    'region',
280                    attrs={
281                        'position': defs.boxes['goal2'][0:3],
282                        'scale': defs.boxes['goal2'][6:9],
283                        'type': 'box',
284                        'materials': [self._score_region_material],
285                    },
286                )
287            )
288        )
289        self._update_scoreboard()
290        self._chant_sound.play()
291
292    @override
293    def on_team_join(self, team: Team) -> None:
294        self._update_scoreboard()
295
296    def _handle_puck_player_collide(self) -> None:
297        collision = bs.getcollision()
298        try:
299            puck = collision.sourcenode.getdelegate(Puck, True)
300            player = collision.opposingnode.getdelegate(
301                PlayerSpaz, True
302            ).getplayer(Player, True)
303        except bs.NotFoundError:
304            return
305
306        puck.last_players_to_touch[player.team.id] = player
307
308    def _kill_puck(self) -> None:
309        self._puck = None
310
311    def _handle_score(self) -> None:
312        """A point has been scored."""
313
314        assert self._puck is not None
315        assert self._score_regions is not None
316
317        # Our puck might stick around for a second or two
318        # we don't want it to be able to score again.
319        if self._puck.scored:
320            return
321
322        region = bs.getcollision().sourcenode
323        index = 0
324        for index, score_region in enumerate(self._score_regions):
325            if region == score_region.node:
326                break
327
328        for team in self.teams:
329            if team.id == index:
330                scoring_team = team
331                team.score += 1
332
333                # Tell all players to celebrate.
334                for player in team.players:
335                    if player.actor:
336                        player.actor.handlemessage(bs.CelebrateMessage(2.0))
337
338                # If we've got the player from the scoring team that last
339                # touched us, give them points.
340                if (
341                    scoring_team.id in self._puck.last_players_to_touch
342                    and self._puck.last_players_to_touch[scoring_team.id]
343                ):
344                    self.stats.player_scored(
345                        self._puck.last_players_to_touch[scoring_team.id],
346                        100,
347                        big_message=True,
348                    )
349
350                # End game if we won.
351                if team.score >= self._score_to_win:
352                    self.end_game()
353
354        self._foghorn_sound.play()
355        self._cheer_sound.play()
356
357        self._puck.scored = True
358
359        # Kill the puck (it'll respawn itself shortly).
360        bs.timer(1.0, self._kill_puck)
361
362        light = bs.newnode(
363            'light',
364            attrs={
365                'position': bs.getcollision().position,
366                'height_attenuated': False,
367                'color': (1, 0, 0),
368            },
369        )
370        bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True)
371        bs.timer(1.0, light.delete)
372
373        bs.cameraflash(duration=10.0)
374        self._update_scoreboard()
375
376    @override
377    def end_game(self) -> None:
378        results = bs.GameResults()
379        for team in self.teams:
380            results.set_team_score(team, team.score)
381        self.end(results=results)
382
383    def _update_scoreboard(self) -> None:
384        winscore = self._score_to_win
385        for team in self.teams:
386            self._scoreboard.set_team_value(team, team.score, winscore)
387
388    @override
389    def handlemessage(self, msg: Any) -> Any:
390        # Respawn dead players if they're still in the game.
391        if isinstance(msg, bs.PlayerDiedMessage):
392            # Augment standard behavior...
393            super().handlemessage(msg)
394            self.respawn_player(msg.getplayer(Player))
395
396        # Respawn dead pucks.
397        elif isinstance(msg, PuckDiedMessage):
398            if not self.has_ended():
399                bs.timer(3.0, self._spawn_puck)
400        else:
401            super().handlemessage(msg)
402
403    def _flash_puck_spawn(self) -> None:
404        light = bs.newnode(
405            'light',
406            attrs={
407                'position': self._puck_spawn_pos,
408                'height_attenuated': False,
409                'color': (1, 0, 0),
410            },
411        )
412        bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True)
413        bs.timer(1.0, light.delete)
414
415    def _spawn_puck(self) -> None:
416        self._swipsound.play()
417        self._whistle_sound.play()
418        self._flash_puck_spawn()
419        assert self._puck_spawn_pos is not None
420        self._puck = Puck(position=self._puck_spawn_pos)

Ice hockey game.

HockeyGame(settings: dict)
169    def __init__(self, settings: dict):
170        super().__init__(settings)
171        shared = SharedObjects.get()
172        self._scoreboard = Scoreboard()
173        self._cheer_sound = bs.getsound('cheer')
174        self._chant_sound = bs.getsound('crowdChant')
175        self._foghorn_sound = bs.getsound('foghorn')
176        self._swipsound = bs.getsound('swip')
177        self._whistle_sound = bs.getsound('refWhistle')
178        self.puck_mesh = bs.getmesh('puck')
179        self.puck_tex = bs.gettexture('puckColor')
180        self._puck_sound = bs.getsound('metalHit')
181        self.puck_material = bs.Material()
182        self.puck_material.add_actions(
183            actions=('modify_part_collision', 'friction', 0.5)
184        )
185        self.puck_material.add_actions(
186            conditions=('they_have_material', shared.pickup_material),
187            actions=('modify_part_collision', 'collide', False),
188        )
189        self.puck_material.add_actions(
190            conditions=(
191                ('we_are_younger_than', 100),
192                'and',
193                ('they_have_material', shared.object_material),
194            ),
195            actions=('modify_node_collision', 'collide', False),
196        )
197        self.puck_material.add_actions(
198            conditions=('they_have_material', shared.footing_material),
199            actions=('impact_sound', self._puck_sound, 0.2, 5),
200        )
201
202        # Keep track of which player last touched the puck
203        self.puck_material.add_actions(
204            conditions=('they_have_material', shared.player_material),
205            actions=(('call', 'at_connect', self._handle_puck_player_collide),),
206        )
207
208        # We want the puck to kill powerups; not get stopped by them
209        self.puck_material.add_actions(
210            conditions=(
211                'they_have_material',
212                PowerupBoxFactory.get().powerup_material,
213            ),
214            actions=(
215                ('modify_part_collision', 'physical', False),
216                ('message', 'their_node', 'at_connect', bs.DieMessage()),
217            ),
218        )
219        self._score_region_material = bs.Material()
220        self._score_region_material.add_actions(
221            conditions=('they_have_material', self.puck_material),
222            actions=(
223                ('modify_part_collision', 'collide', True),
224                ('modify_part_collision', 'physical', False),
225                ('call', 'at_connect', self._handle_score),
226            ),
227        )
228        self._puck_spawn_pos: Sequence[float] | None = None
229        self._score_regions: list[bs.NodeActor] | None = None
230        self._puck: Puck | None = None
231        self._score_to_win = int(settings['Score to Win'])
232        self._time_limit = float(settings['Time Limit'])
233        self._epic_mode = bool(settings['Epic Mode'])
234        self.slow_motion = self._epic_mode
235        self.default_music = (
236            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.HOCKEY
237        )

Instantiate the Activity.

name = 'Hockey'
description = 'Score some goals.'
available_settings = [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)]), BoolSetting(name='Epic Mode', default=False)]
@override
@classmethod
def supports_session_type(cls, sessiontype: type[bascenev1.Session]) -> bool:
158    @override
159    @classmethod
160    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
161        return issubclass(sessiontype, bs.DualTeamSession)

Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.

@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1.Session]) -> list[str]:
163    @override
164    @classmethod
165    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
166        assert bs.app.classic is not None
167        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.

puck_mesh
puck_tex
puck_material
slow_motion = False

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

default_music = None
@override
def get_instance_description(self) -> Union[str, Sequence]:
239    @override
240    def get_instance_description(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 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.

@override
def get_instance_description_short(self) -> Union[str, Sequence]:
245    @override
246    def get_instance_description_short(self) -> str | Sequence:
247        if self._score_to_win == 1:
248            return 'score a goal'
249        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.

@override
def on_begin(self) -> None:
251    @override
252    def on_begin(self) -> None:
253        super().on_begin()
254
255        self.setup_standard_time_limit(self._time_limit)
256        self.setup_standard_powerup_drops()
257        self._puck_spawn_pos = self.map.get_flag_position(None)
258        self._spawn_puck()
259
260        # Set up the two score regions.
261        defs = self.map.defs
262        self._score_regions = []
263        self._score_regions.append(
264            bs.NodeActor(
265                bs.newnode(
266                    'region',
267                    attrs={
268                        'position': defs.boxes['goal1'][0:3],
269                        'scale': defs.boxes['goal1'][6:9],
270                        'type': 'box',
271                        'materials': [self._score_region_material],
272                    },
273                )
274            )
275        )
276        self._score_regions.append(
277            bs.NodeActor(
278                bs.newnode(
279                    'region',
280                    attrs={
281                        'position': defs.boxes['goal2'][0:3],
282                        'scale': defs.boxes['goal2'][6:9],
283                        'type': 'box',
284                        'materials': [self._score_region_material],
285                    },
286                )
287            )
288        )
289        self._update_scoreboard()
290        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.

@override
def on_team_join(self, team: Team) -> None:
292    @override
293    def on_team_join(self, team: Team) -> None:
294        self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def end_game(self) -> None:
376    @override
377    def end_game(self) -> None:
378        results = bs.GameResults()
379        for team in self.teams:
380            results.set_team_score(team, team.score)
381        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.

@override
def handlemessage(self, msg: Any) -> Any:
388    @override
389    def handlemessage(self, msg: Any) -> Any:
390        # Respawn dead players if they're still in the game.
391        if isinstance(msg, bs.PlayerDiedMessage):
392            # Augment standard behavior...
393            super().handlemessage(msg)
394            self.respawn_player(msg.getplayer(Player))
395
396        # Respawn dead pucks.
397        elif isinstance(msg, PuckDiedMessage):
398            if not self.has_ended():
399                bs.timer(3.0, self._spawn_puck)
400        else:
401            super().handlemessage(msg)

General message handling; can be passed any message object.