bascenev1lib.game.hockey

Hockey game and support classes.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Hockey game and support classes."""
  4
  5# ba_meta require api 8
  6# (see https://ballistica.net/wiki/meta-tag-system)
  7
  8from __future__ import annotations
  9
 10from typing import TYPE_CHECKING
 11
 12from typing_extensions import override
 13import bascenev1 as bs
 14
 15from bascenev1lib.actor.playerspaz import PlayerSpaz
 16from bascenev1lib.actor.scoreboard import Scoreboard
 17from bascenev1lib.actor.powerupbox import PowerupBoxFactory
 18from bascenev1lib.gameutils import SharedObjects
 19
 20if TYPE_CHECKING:
 21    from typing import Any, Sequence
 22
 23
 24class PuckDiedMessage:
 25    """Inform something that a puck has died."""
 26
 27    def __init__(self, puck: Puck):
 28        self.puck = puck
 29
 30
 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)
106
107
108class Player(bs.Player['Team']):
109    """Our player type for this game."""
110
111
112class Team(bs.Team[Player]):
113    """Our team type for this game."""
114
115    def __init__(self) -> None:
116        self.score = 0
117
118
119# ba_meta export bascenev1.GameActivity
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)
class PuckDiedMessage:
25class PuckDiedMessage:
26    """Inform something that a puck has died."""
27
28    def __init__(self, puck: Puck):
29        self.puck = puck

Inform something that a puck has died.

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

A lovely giant hockey puck.

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

General message handling; can be passed any message object.

Inherited Members
bascenev1._actor.Actor
autoretain
on_expire
expired
exists
is_alive
activity
getactivity
class Player(bascenev1._player.Player[ForwardRef('Team')]):
109class Player(bs.Player['Team']):
110    """Our player type for this game."""

Our player type for this game.

Inherited Members
bascenev1._player.Player
character
actor
color
highlight
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(bascenev1._team.Team[bascenev1lib.game.hockey.Player]):
113class Team(bs.Team[Player]):
114    """Our team type for this game."""
115
116    def __init__(self) -> None:
117        self.score = 0

Our team type for this game.

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

Ice hockey game.

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

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.Session]) -> bool:
159    @override
160    @classmethod
161    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
162        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.Session]) -> list[str]:
164    @override
165    @classmethod
166    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
167        assert bs.app.classic is not None
168        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]:
240    @override
241    def get_instance_description(self) -> str | Sequence:
242        if self._score_to_win == 1:
243            return 'Score a goal.'
244        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]:
246    @override
247    def get_instance_description_short(self) -> str | Sequence:
248        if self._score_to_win == 1:
249            return 'score a goal'
250        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:
252    @override
253    def on_begin(self) -> None:
254        super().on_begin()
255
256        self.setup_standard_time_limit(self._time_limit)
257        self.setup_standard_powerup_drops()
258        self._puck_spawn_pos = self.map.get_flag_position(None)
259        self._spawn_puck()
260
261        # Set up the two score regions.
262        defs = self.map.defs
263        self._score_regions = []
264        self._score_regions.append(
265            bs.NodeActor(
266                bs.newnode(
267                    'region',
268                    attrs={
269                        'position': defs.boxes['goal1'][0:3],
270                        'scale': defs.boxes['goal1'][6:9],
271                        'type': 'box',
272                        'materials': [self._score_region_material],
273                    },
274                )
275            )
276        )
277        self._score_regions.append(
278            bs.NodeActor(
279                bs.newnode(
280                    'region',
281                    attrs={
282                        'position': defs.boxes['goal2'][0:3],
283                        'scale': defs.boxes['goal2'][6:9],
284                        'type': 'box',
285                        'materials': [self._score_region_material],
286                    },
287                )
288            )
289        )
290        self._update_scoreboard()
291        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:
293    @override
294    def on_team_join(self, team: Team) -> None:
295        self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def end_game(self) -> None:
377    @override
378    def end_game(self) -> None:
379        results = bs.GameResults()
380        for team in self.teams:
381            results.set_team_score(team, team.score)
382        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:
389    @override
390    def handlemessage(self, msg: Any) -> Any:
391        # Respawn dead players if they're still in the game.
392        if isinstance(msg, bs.PlayerDiedMessage):
393            # Augment standard behavior...
394            super().handlemessage(msg)
395            self.respawn_player(msg.getplayer(Player))
396
397        # Respawn dead pucks.
398        elif isinstance(msg, PuckDiedMessage):
399            if not self.has_ended():
400                bs.timer(3.0, self._spawn_puck)
401        else:
402            super().handlemessage(msg)

General message handling; can be passed any message object.

Inherited Members
bascenev1._teamgame.TeamGameActivity
on_transition_in
spawn_player_spaz
end
bascenev1._gameactivity.GameActivity
tips
scoreconfig
allow_pausing
allow_kick_idle_players
show_kill_points
create_settings_ui
getscoreconfig
getname
get_display_string
get_team_display_string
get_description
get_description_display_string
get_available_settings
get_settings_display_string
initialplayerinfos
map
get_instance_display_string
get_instance_scoreboard_display_string
on_continue
is_waiting_for_continue
continue_or_end_game
on_player_join
respawn_player
spawn_player_if_exists
spawn_player
setup_standard_powerup_drops
setup_standard_time_limit
show_zoom_message
bascenev1._activity.Activity
settings_raw
teams
players
announce_player_deaths
is_joining_activity
use_fixed_vr_overlay
inherits_slow_motion
inherits_music
inherits_vr_camera_offset
inherits_vr_overlay_center
inherits_tint
allow_mid_activity_joins
transition_time
can_show_ad_on_death
paused_text
preloads
lobby
context
globalsnode
stats
on_expire
customdata
expired
playertype
teamtype
retain_actor
add_actor_weak_ref
session
on_player_leave
on_team_leave
on_transition_out
has_transitioned_in
has_begun
has_ended
is_transitioning_out
transition_out
create_player
create_team
bascenev1._dependency.DependencyComponent
dep_is_present
get_dynamic_deps