bascenev1lib.game.hockey

Hockey game and support classes.

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

A lovely giant hockey puck.

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 = bs.newnode(
46            'prop',
47            delegate=self,
48            attrs={
49                'mesh': activity.puck_mesh,
50                'color_texture': activity.puck_tex,
51                'body': 'puck',
52                'reflection': 'soft',
53                'reflection_scale': [0.2],
54                'shadow_size': 1.0,
55                'is_area_of_interest': True,
56                'position': self._spawn_pos,
57                'materials': pmats,
58            },
59        )
60        bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1})

Instantiates an Actor in the current bascenev1.Activity.

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

General message handling; can be passed any message object.

Inherited Members
bascenev1._actor.Actor
autoretain
on_expire
expired
exists
is_alive
activity
getactivity
class Player(bascenev1._player.Player[ForwardRef('Team')]):
106class Player(bs.Player['Team']):
107    """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]):
110class Team(bs.Team[Player]):
111    """Our team type for this game."""
112
113    def __init__(self) -> None:
114        self.score = 0

Our team type for this game.

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

Ice hockey game.

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

Instantiate the Activity.

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)]
@classmethod
def supports_session_type(cls, sessiontype: type[bascenev1._session.Session]) -> bool:
156    @classmethod
157    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
158        return issubclass(sessiontype, bs.DualTeamSession)

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

@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1._session.Session]) -> list[str]:
160    @classmethod
161    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
162        assert bs.app.classic is not None
163        return bs.app.classic.getmaps('hockey')

Called by the default bascenev1.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given bascenev1.Session type.

puck_mesh
puck_tex
puck_material
slow_motion = False

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

default_music = None
def get_instance_description(self) -> Union[str, Sequence]:
235    def get_instance_description(self) -> str | Sequence:
236        if self._score_to_win == 1:
237            return 'Score a goal.'
238        return 'Score ${ARG1} goals.', self._score_to_win

Return a description for this game instance, in English.

This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'Score 3 goals.' in English

and can properly translate to 'Anota 3 goles.' in Spanish.

If we just returned the string 'Score 3 Goals' here, there would

have to be a translation entry for each specific number. ew.

return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

def get_instance_description_short(self) -> Union[str, Sequence]:
240    def get_instance_description_short(self) -> str | Sequence:
241        if self._score_to_win == 1:
242            return 'score a goal'
243        return 'score ${ARG1} goals', self._score_to_win

Return a short description for this game instance in English.

This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'score 3 goals' in English

and can properly translate to 'anota 3 goles' in Spanish.

If we just returned the string 'score 3 goals' here, there would

have to be a translation entry for each specific number. ew.

return ['score ${ARG1} goals', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

def on_begin(self) -> None:
245    def on_begin(self) -> None:
246        super().on_begin()
247
248        self.setup_standard_time_limit(self._time_limit)
249        self.setup_standard_powerup_drops()
250        self._puck_spawn_pos = self.map.get_flag_position(None)
251        self._spawn_puck()
252
253        # Set up the two score regions.
254        defs = self.map.defs
255        self._score_regions = []
256        self._score_regions.append(
257            bs.NodeActor(
258                bs.newnode(
259                    'region',
260                    attrs={
261                        'position': defs.boxes['goal1'][0:3],
262                        'scale': defs.boxes['goal1'][6:9],
263                        'type': 'box',
264                        'materials': [self._score_region_material],
265                    },
266                )
267            )
268        )
269        self._score_regions.append(
270            bs.NodeActor(
271                bs.newnode(
272                    'region',
273                    attrs={
274                        'position': defs.boxes['goal2'][0:3],
275                        'scale': defs.boxes['goal2'][6:9],
276                        'type': 'box',
277                        'materials': [self._score_region_material],
278                    },
279                )
280            )
281        )
282        self._update_scoreboard()
283        self._chant_sound.play()

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

def on_team_join(self, team: Team) -> None:
285    def on_team_join(self, team: Team) -> None:
286        self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

def end_game(self) -> None:
368    def end_game(self) -> None:
369        results = bs.GameResults()
370        for team in self.teams:
371            results.set_team_score(team, team.score)
372        self.end(results=results)

Tell the game to wrap up and call bascenev1.Activity.end().

This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.

def handlemessage(self, msg: Any) -> Any:
379    def handlemessage(self, msg: Any) -> Any:
380        # Respawn dead players if they're still in the game.
381        if isinstance(msg, bs.PlayerDiedMessage):
382            # Augment standard behavior...
383            super().handlemessage(msg)
384            self.respawn_player(msg.getplayer(Player))
385
386        # Respawn dead pucks.
387        elif isinstance(msg, PuckDiedMessage):
388            if not self.has_ended():
389                bs.timer(3.0, self._spawn_puck)
390        else:
391            super().handlemessage(msg)

General message handling; can be passed any message object.

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