bascenev1lib.game.easteregghunt

Provides an easter egg hunt game.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides an easter egg hunt game."""
  4
  5# ba_meta require api 8
  6# (see https://ballistica.net/wiki/meta-tag-system)
  7
  8from __future__ import annotations
  9
 10import random
 11from typing import TYPE_CHECKING
 12
 13from typing_extensions import override
 14import bascenev1 as bs
 15
 16from bascenev1lib.actor.bomb import Bomb
 17from bascenev1lib.actor.playerspaz import PlayerSpaz
 18from bascenev1lib.actor.spazbot import SpazBotSet, BouncyBot, SpazBotDiedMessage
 19from bascenev1lib.actor.onscreencountdown import OnScreenCountdown
 20from bascenev1lib.actor.scoreboard import Scoreboard
 21from bascenev1lib.actor.respawnicon import RespawnIcon
 22from bascenev1lib.gameutils import SharedObjects
 23
 24if TYPE_CHECKING:
 25    from typing import Any
 26
 27
 28class Player(bs.Player['Team']):
 29    """Our player type for this game."""
 30
 31    def __init__(self) -> None:
 32        self.respawn_timer: bs.Timer | None = None
 33        self.respawn_icon: RespawnIcon | None = None
 34
 35
 36class Team(bs.Team[Player]):
 37    """Our team type for this game."""
 38
 39    def __init__(self) -> None:
 40        self.score = 0
 41
 42
 43# ba_meta export bascenev1.GameActivity
 44class EasterEggHuntGame(bs.TeamGameActivity[Player, Team]):
 45    """A game where score is based on collecting eggs."""
 46
 47    name = 'Easter Egg Hunt'
 48    description = 'Gather eggs!'
 49    available_settings = [
 50        bs.BoolSetting('Pro Mode', default=False),
 51        bs.BoolSetting('Epic Mode', default=False),
 52    ]
 53    scoreconfig = bs.ScoreConfig(label='Score', scoretype=bs.ScoreType.POINTS)
 54
 55    # We're currently hard-coded for one map.
 56    @override
 57    @classmethod
 58    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
 59        return ['Tower D']
 60
 61    # We support teams, free-for-all, and co-op sessions.
 62    @override
 63    @classmethod
 64    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 65        return (
 66            issubclass(sessiontype, bs.CoopSession)
 67            or issubclass(sessiontype, bs.DualTeamSession)
 68            or issubclass(sessiontype, bs.FreeForAllSession)
 69        )
 70
 71    def __init__(self, settings: dict):
 72        super().__init__(settings)
 73        shared = SharedObjects.get()
 74        self._last_player_death_time = None
 75        self._scoreboard = Scoreboard()
 76        self.egg_mesh = bs.getmesh('egg')
 77        self.egg_tex_1 = bs.gettexture('eggTex1')
 78        self.egg_tex_2 = bs.gettexture('eggTex2')
 79        self.egg_tex_3 = bs.gettexture('eggTex3')
 80        self._collect_sound = bs.getsound('powerup01')
 81        self._pro_mode = settings.get('Pro Mode', False)
 82        self._epic_mode = settings.get('Epic Mode', False)
 83        self._max_eggs = 1.0
 84        self.egg_material = bs.Material()
 85        self.egg_material.add_actions(
 86            conditions=('they_have_material', shared.player_material),
 87            actions=(('call', 'at_connect', self._on_egg_player_collide),),
 88        )
 89        self._eggs: list[Egg] = []
 90        self._update_timer: bs.Timer | None = None
 91        self._countdown: OnScreenCountdown | None = None
 92        self._bots: SpazBotSet | None = None
 93
 94        # Base class overrides
 95        self.slow_motion = self._epic_mode
 96        self.default_music = (
 97            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH
 98        )
 99
100    @override
101    def on_team_join(self, team: Team) -> None:
102        if self.has_begun():
103            self._update_scoreboard()
104
105    # Called when our game actually starts.
106    @override
107    def on_begin(self) -> None:
108        from bascenev1lib.maps import TowerD
109
110        # There's a player-wall on the tower-d level to prevent
111        # players from getting up on the stairs.. we wanna kill that.
112        gamemap = self.map
113        assert isinstance(gamemap, TowerD)
114        gamemap.player_wall.delete()
115        super().on_begin()
116        self._update_scoreboard()
117        self._update_timer = bs.Timer(0.25, self._update, repeat=True)
118        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
119        bs.timer(4.0, self._countdown.start)
120        self._bots = SpazBotSet()
121
122        # Spawn evil bunny in co-op only.
123        if isinstance(self.session, bs.CoopSession) and self._pro_mode:
124            self._spawn_evil_bunny()
125
126    # Overriding the default character spawning.
127    @override
128    def spawn_player(self, player: Player) -> bs.Actor:
129        spaz = self.spawn_player_spaz(player)
130        spaz.connect_controls_to_player()
131        return spaz
132
133    def _spawn_evil_bunny(self) -> None:
134        assert self._bots is not None
135        self._bots.spawn_bot(BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0)
136
137    def _on_egg_player_collide(self) -> None:
138        if self.has_ended():
139            return
140        collision = bs.getcollision()
141
142        # Be defensive here; we could be hitting the corpse of a player
143        # who just left/etc.
144        try:
145            egg = collision.sourcenode.getdelegate(Egg, True)
146            player = collision.opposingnode.getdelegate(
147                PlayerSpaz, True
148            ).getplayer(Player, True)
149        except bs.NotFoundError:
150            return
151
152        player.team.score += 1
153
154        # Displays a +1 (and adds to individual player score in
155        # teams mode).
156        self.stats.player_scored(player, 1, screenmessage=False)
157        if self._max_eggs < 5:
158            self._max_eggs += 1.0
159        elif self._max_eggs < 10:
160            self._max_eggs += 0.5
161        elif self._max_eggs < 30:
162            self._max_eggs += 0.3
163        self._update_scoreboard()
164        self._collect_sound.play(0.5, position=egg.node.position)
165
166        # Create a flash.
167        light = bs.newnode(
168            'light',
169            attrs={
170                'position': egg.node.position,
171                'height_attenuated': False,
172                'radius': 0.1,
173                'color': (1, 1, 0),
174            },
175        )
176        bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False)
177        bs.timer(0.200, light.delete)
178        egg.handlemessage(bs.DieMessage())
179
180    def _update(self) -> None:
181        # Misc. periodic updating.
182        xpos = random.uniform(-7.1, 6.0)
183        ypos = random.uniform(3.5, 3.5)
184        zpos = random.uniform(-8.2, 3.7)
185
186        # Prune dead eggs from our list.
187        self._eggs = [e for e in self._eggs if e]
188
189        # Spawn more eggs if we've got space.
190        if len(self._eggs) < int(self._max_eggs):
191            # Occasionally spawn a land-mine in addition.
192            if self._pro_mode and random.random() < 0.25:
193                mine = Bomb(
194                    position=(xpos, ypos, zpos), bomb_type='land_mine'
195                ).autoretain()
196                mine.arm()
197            else:
198                self._eggs.append(Egg(position=(xpos, ypos, zpos)))
199
200    # Various high-level game events come through this method.
201    @override
202    def handlemessage(self, msg: Any) -> Any:
203        # Respawn dead players.
204        if isinstance(msg, bs.PlayerDiedMessage):
205            # Augment standard behavior.
206            super().handlemessage(msg)
207
208            # Respawn them shortly.
209            player = msg.getplayer(Player)
210            assert self.initialplayerinfos is not None
211            respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
212            player.respawn_timer = bs.Timer(
213                respawn_time, bs.Call(self.spawn_player_if_exists, player)
214            )
215            player.respawn_icon = RespawnIcon(player, respawn_time)
216
217        # Whenever our evil bunny dies, respawn him and spew some eggs.
218        elif isinstance(msg, SpazBotDiedMessage):
219            self._spawn_evil_bunny()
220            assert msg.spazbot.node
221            pos = msg.spazbot.node.position
222            for _i in range(6):
223                spread = 0.4
224                self._eggs.append(
225                    Egg(
226                        position=(
227                            pos[0] + random.uniform(-spread, spread),
228                            pos[1] + random.uniform(-spread, spread),
229                            pos[2] + random.uniform(-spread, spread),
230                        )
231                    )
232                )
233        else:
234            # Default handler.
235            return super().handlemessage(msg)
236        return None
237
238    def _update_scoreboard(self) -> None:
239        for team in self.teams:
240            self._scoreboard.set_team_value(team, team.score)
241
242    @override
243    def end_game(self) -> None:
244        results = bs.GameResults()
245        for team in self.teams:
246            results.set_team_score(team, team.score)
247        self.end(results)
248
249
250class Egg(bs.Actor):
251    """A lovely egg that can be picked up for points."""
252
253    def __init__(self, position: tuple[float, float, float] = (0.0, 1.0, 0.0)):
254        super().__init__()
255        activity = self.activity
256        assert isinstance(activity, EasterEggHuntGame)
257        shared = SharedObjects.get()
258
259        # Spawn just above the provided point.
260        self._spawn_pos = (position[0], position[1] + 1.0, position[2])
261        ctex = (activity.egg_tex_1, activity.egg_tex_2, activity.egg_tex_3)[
262            random.randrange(3)
263        ]
264        mats = [shared.object_material, activity.egg_material]
265        self.node = bs.newnode(
266            'prop',
267            delegate=self,
268            attrs={
269                'mesh': activity.egg_mesh,
270                'color_texture': ctex,
271                'body': 'capsule',
272                'reflection': 'soft',
273                'mesh_scale': 0.5,
274                'body_scale': 0.6,
275                'density': 4.0,
276                'reflection_scale': [0.15],
277                'shadow_size': 0.6,
278                'position': self._spawn_pos,
279                'materials': mats,
280            },
281        )
282
283    @override
284    def exists(self) -> bool:
285        return bool(self.node)
286
287    @override
288    def handlemessage(self, msg: Any) -> Any:
289        if isinstance(msg, bs.DieMessage):
290            if self.node:
291                self.node.delete()
292        elif isinstance(msg, bs.HitMessage):
293            if self.node:
294                assert msg.force_direction is not None
295                self.node.handlemessage(
296                    'impulse',
297                    msg.pos[0],
298                    msg.pos[1],
299                    msg.pos[2],
300                    msg.velocity[0],
301                    msg.velocity[1],
302                    msg.velocity[2],
303                    1.0 * msg.magnitude,
304                    1.0 * msg.velocity_magnitude,
305                    msg.radius,
306                    0,
307                    msg.force_direction[0],
308                    msg.force_direction[1],
309                    msg.force_direction[2],
310                )
311        else:
312            super().handlemessage(msg)
class Player(bascenev1._player.Player[ForwardRef('Team')]):
29class Player(bs.Player['Team']):
30    """Our player type for this game."""
31
32    def __init__(self) -> None:
33        self.respawn_timer: bs.Timer | None = None
34        self.respawn_icon: RespawnIcon | None = None

Our player type for this game.

respawn_timer: _bascenev1.Timer | None
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.easteregghunt.Player]):
37class Team(bs.Team[Player]):
38    """Our team type for this game."""
39
40    def __init__(self) -> None:
41        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 EasterEggHuntGame(bascenev1._teamgame.TeamGameActivity[bascenev1lib.game.easteregghunt.Player, bascenev1lib.game.easteregghunt.Team]):
 45class EasterEggHuntGame(bs.TeamGameActivity[Player, Team]):
 46    """A game where score is based on collecting eggs."""
 47
 48    name = 'Easter Egg Hunt'
 49    description = 'Gather eggs!'
 50    available_settings = [
 51        bs.BoolSetting('Pro Mode', default=False),
 52        bs.BoolSetting('Epic Mode', default=False),
 53    ]
 54    scoreconfig = bs.ScoreConfig(label='Score', scoretype=bs.ScoreType.POINTS)
 55
 56    # We're currently hard-coded for one map.
 57    @override
 58    @classmethod
 59    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
 60        return ['Tower D']
 61
 62    # We support teams, free-for-all, and co-op sessions.
 63    @override
 64    @classmethod
 65    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 66        return (
 67            issubclass(sessiontype, bs.CoopSession)
 68            or issubclass(sessiontype, bs.DualTeamSession)
 69            or issubclass(sessiontype, bs.FreeForAllSession)
 70        )
 71
 72    def __init__(self, settings: dict):
 73        super().__init__(settings)
 74        shared = SharedObjects.get()
 75        self._last_player_death_time = None
 76        self._scoreboard = Scoreboard()
 77        self.egg_mesh = bs.getmesh('egg')
 78        self.egg_tex_1 = bs.gettexture('eggTex1')
 79        self.egg_tex_2 = bs.gettexture('eggTex2')
 80        self.egg_tex_3 = bs.gettexture('eggTex3')
 81        self._collect_sound = bs.getsound('powerup01')
 82        self._pro_mode = settings.get('Pro Mode', False)
 83        self._epic_mode = settings.get('Epic Mode', False)
 84        self._max_eggs = 1.0
 85        self.egg_material = bs.Material()
 86        self.egg_material.add_actions(
 87            conditions=('they_have_material', shared.player_material),
 88            actions=(('call', 'at_connect', self._on_egg_player_collide),),
 89        )
 90        self._eggs: list[Egg] = []
 91        self._update_timer: bs.Timer | None = None
 92        self._countdown: OnScreenCountdown | None = None
 93        self._bots: SpazBotSet | None = None
 94
 95        # Base class overrides
 96        self.slow_motion = self._epic_mode
 97        self.default_music = (
 98            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH
 99        )
100
101    @override
102    def on_team_join(self, team: Team) -> None:
103        if self.has_begun():
104            self._update_scoreboard()
105
106    # Called when our game actually starts.
107    @override
108    def on_begin(self) -> None:
109        from bascenev1lib.maps import TowerD
110
111        # There's a player-wall on the tower-d level to prevent
112        # players from getting up on the stairs.. we wanna kill that.
113        gamemap = self.map
114        assert isinstance(gamemap, TowerD)
115        gamemap.player_wall.delete()
116        super().on_begin()
117        self._update_scoreboard()
118        self._update_timer = bs.Timer(0.25, self._update, repeat=True)
119        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
120        bs.timer(4.0, self._countdown.start)
121        self._bots = SpazBotSet()
122
123        # Spawn evil bunny in co-op only.
124        if isinstance(self.session, bs.CoopSession) and self._pro_mode:
125            self._spawn_evil_bunny()
126
127    # Overriding the default character spawning.
128    @override
129    def spawn_player(self, player: Player) -> bs.Actor:
130        spaz = self.spawn_player_spaz(player)
131        spaz.connect_controls_to_player()
132        return spaz
133
134    def _spawn_evil_bunny(self) -> None:
135        assert self._bots is not None
136        self._bots.spawn_bot(BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0)
137
138    def _on_egg_player_collide(self) -> None:
139        if self.has_ended():
140            return
141        collision = bs.getcollision()
142
143        # Be defensive here; we could be hitting the corpse of a player
144        # who just left/etc.
145        try:
146            egg = collision.sourcenode.getdelegate(Egg, True)
147            player = collision.opposingnode.getdelegate(
148                PlayerSpaz, True
149            ).getplayer(Player, True)
150        except bs.NotFoundError:
151            return
152
153        player.team.score += 1
154
155        # Displays a +1 (and adds to individual player score in
156        # teams mode).
157        self.stats.player_scored(player, 1, screenmessage=False)
158        if self._max_eggs < 5:
159            self._max_eggs += 1.0
160        elif self._max_eggs < 10:
161            self._max_eggs += 0.5
162        elif self._max_eggs < 30:
163            self._max_eggs += 0.3
164        self._update_scoreboard()
165        self._collect_sound.play(0.5, position=egg.node.position)
166
167        # Create a flash.
168        light = bs.newnode(
169            'light',
170            attrs={
171                'position': egg.node.position,
172                'height_attenuated': False,
173                'radius': 0.1,
174                'color': (1, 1, 0),
175            },
176        )
177        bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False)
178        bs.timer(0.200, light.delete)
179        egg.handlemessage(bs.DieMessage())
180
181    def _update(self) -> None:
182        # Misc. periodic updating.
183        xpos = random.uniform(-7.1, 6.0)
184        ypos = random.uniform(3.5, 3.5)
185        zpos = random.uniform(-8.2, 3.7)
186
187        # Prune dead eggs from our list.
188        self._eggs = [e for e in self._eggs if e]
189
190        # Spawn more eggs if we've got space.
191        if len(self._eggs) < int(self._max_eggs):
192            # Occasionally spawn a land-mine in addition.
193            if self._pro_mode and random.random() < 0.25:
194                mine = Bomb(
195                    position=(xpos, ypos, zpos), bomb_type='land_mine'
196                ).autoretain()
197                mine.arm()
198            else:
199                self._eggs.append(Egg(position=(xpos, ypos, zpos)))
200
201    # Various high-level game events come through this method.
202    @override
203    def handlemessage(self, msg: Any) -> Any:
204        # Respawn dead players.
205        if isinstance(msg, bs.PlayerDiedMessage):
206            # Augment standard behavior.
207            super().handlemessage(msg)
208
209            # Respawn them shortly.
210            player = msg.getplayer(Player)
211            assert self.initialplayerinfos is not None
212            respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
213            player.respawn_timer = bs.Timer(
214                respawn_time, bs.Call(self.spawn_player_if_exists, player)
215            )
216            player.respawn_icon = RespawnIcon(player, respawn_time)
217
218        # Whenever our evil bunny dies, respawn him and spew some eggs.
219        elif isinstance(msg, SpazBotDiedMessage):
220            self._spawn_evil_bunny()
221            assert msg.spazbot.node
222            pos = msg.spazbot.node.position
223            for _i in range(6):
224                spread = 0.4
225                self._eggs.append(
226                    Egg(
227                        position=(
228                            pos[0] + random.uniform(-spread, spread),
229                            pos[1] + random.uniform(-spread, spread),
230                            pos[2] + random.uniform(-spread, spread),
231                        )
232                    )
233                )
234        else:
235            # Default handler.
236            return super().handlemessage(msg)
237        return None
238
239    def _update_scoreboard(self) -> None:
240        for team in self.teams:
241            self._scoreboard.set_team_value(team, team.score)
242
243    @override
244    def end_game(self) -> None:
245        results = bs.GameResults()
246        for team in self.teams:
247            results.set_team_score(team, team.score)
248        self.end(results)

A game where score is based on collecting eggs.

EasterEggHuntGame(settings: dict)
72    def __init__(self, settings: dict):
73        super().__init__(settings)
74        shared = SharedObjects.get()
75        self._last_player_death_time = None
76        self._scoreboard = Scoreboard()
77        self.egg_mesh = bs.getmesh('egg')
78        self.egg_tex_1 = bs.gettexture('eggTex1')
79        self.egg_tex_2 = bs.gettexture('eggTex2')
80        self.egg_tex_3 = bs.gettexture('eggTex3')
81        self._collect_sound = bs.getsound('powerup01')
82        self._pro_mode = settings.get('Pro Mode', False)
83        self._epic_mode = settings.get('Epic Mode', False)
84        self._max_eggs = 1.0
85        self.egg_material = bs.Material()
86        self.egg_material.add_actions(
87            conditions=('they_have_material', shared.player_material),
88            actions=(('call', 'at_connect', self._on_egg_player_collide),),
89        )
90        self._eggs: list[Egg] = []
91        self._update_timer: bs.Timer | None = None
92        self._countdown: OnScreenCountdown | None = None
93        self._bots: SpazBotSet | None = None
94
95        # Base class overrides
96        self.slow_motion = self._epic_mode
97        self.default_music = (
98            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH
99        )

Instantiate the Activity.

name = 'Easter Egg Hunt'
description = 'Gather eggs!'
available_settings = [BoolSetting(name='Pro Mode', default=False), BoolSetting(name='Epic Mode', default=False)]
scoreconfig = ScoreConfig(label='Score', scoretype=<ScoreType.POINTS: 'p'>, lower_is_better=False, none_is_winner=False, version='')
@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1._session.Session]) -> list[str]:
57    @override
58    @classmethod
59    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
60        return ['Tower D']

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.

@override
@classmethod
def supports_session_type(cls, sessiontype: type[bascenev1._session.Session]) -> bool:
63    @override
64    @classmethod
65    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
66        return (
67            issubclass(sessiontype, bs.CoopSession)
68            or issubclass(sessiontype, bs.DualTeamSession)
69            or issubclass(sessiontype, bs.FreeForAllSession)
70        )

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

egg_mesh
egg_tex_1
egg_tex_2
egg_tex_3
egg_material
slow_motion = False

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

default_music = None
@override
def on_team_join(self, team: Team) -> None:
101    @override
102    def on_team_join(self, team: Team) -> None:
103        if self.has_begun():
104            self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def on_begin(self) -> None:
107    @override
108    def on_begin(self) -> None:
109        from bascenev1lib.maps import TowerD
110
111        # There's a player-wall on the tower-d level to prevent
112        # players from getting up on the stairs.. we wanna kill that.
113        gamemap = self.map
114        assert isinstance(gamemap, TowerD)
115        gamemap.player_wall.delete()
116        super().on_begin()
117        self._update_scoreboard()
118        self._update_timer = bs.Timer(0.25, self._update, repeat=True)
119        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
120        bs.timer(4.0, self._countdown.start)
121        self._bots = SpazBotSet()
122
123        # Spawn evil bunny in co-op only.
124        if isinstance(self.session, bs.CoopSession) and self._pro_mode:
125            self._spawn_evil_bunny()

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 spawn_player( self, player: Player) -> bascenev1._actor.Actor:
128    @override
129    def spawn_player(self, player: Player) -> bs.Actor:
130        spaz = self.spawn_player_spaz(player)
131        spaz.connect_controls_to_player()
132        return spaz

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

@override
def handlemessage(self, msg: Any) -> Any:
202    @override
203    def handlemessage(self, msg: Any) -> Any:
204        # Respawn dead players.
205        if isinstance(msg, bs.PlayerDiedMessage):
206            # Augment standard behavior.
207            super().handlemessage(msg)
208
209            # Respawn them shortly.
210            player = msg.getplayer(Player)
211            assert self.initialplayerinfos is not None
212            respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
213            player.respawn_timer = bs.Timer(
214                respawn_time, bs.Call(self.spawn_player_if_exists, player)
215            )
216            player.respawn_icon = RespawnIcon(player, respawn_time)
217
218        # Whenever our evil bunny dies, respawn him and spew some eggs.
219        elif isinstance(msg, SpazBotDiedMessage):
220            self._spawn_evil_bunny()
221            assert msg.spazbot.node
222            pos = msg.spazbot.node.position
223            for _i in range(6):
224                spread = 0.4
225                self._eggs.append(
226                    Egg(
227                        position=(
228                            pos[0] + random.uniform(-spread, spread),
229                            pos[1] + random.uniform(-spread, spread),
230                            pos[2] + random.uniform(-spread, spread),
231                        )
232                    )
233                )
234        else:
235            # Default handler.
236            return super().handlemessage(msg)
237        return None

General message handling; can be passed any message object.

@override
def end_game(self) -> None:
243    @override
244    def end_game(self) -> None:
245        results = bs.GameResults()
246        for team in self.teams:
247            results.set_team_score(team, team.score)
248        self.end(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.

Inherited Members
bascenev1._teamgame.TeamGameActivity
on_transition_in
spawn_player_spaz
end
bascenev1._gameactivity.GameActivity
tips
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
get_instance_description
get_instance_description_short
on_continue
is_waiting_for_continue
continue_or_end_game
on_player_join
respawn_player
spawn_player_if_exists
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
class Egg(bascenev1._actor.Actor):
251class Egg(bs.Actor):
252    """A lovely egg that can be picked up for points."""
253
254    def __init__(self, position: tuple[float, float, float] = (0.0, 1.0, 0.0)):
255        super().__init__()
256        activity = self.activity
257        assert isinstance(activity, EasterEggHuntGame)
258        shared = SharedObjects.get()
259
260        # Spawn just above the provided point.
261        self._spawn_pos = (position[0], position[1] + 1.0, position[2])
262        ctex = (activity.egg_tex_1, activity.egg_tex_2, activity.egg_tex_3)[
263            random.randrange(3)
264        ]
265        mats = [shared.object_material, activity.egg_material]
266        self.node = bs.newnode(
267            'prop',
268            delegate=self,
269            attrs={
270                'mesh': activity.egg_mesh,
271                'color_texture': ctex,
272                'body': 'capsule',
273                'reflection': 'soft',
274                'mesh_scale': 0.5,
275                'body_scale': 0.6,
276                'density': 4.0,
277                'reflection_scale': [0.15],
278                'shadow_size': 0.6,
279                'position': self._spawn_pos,
280                'materials': mats,
281            },
282        )
283
284    @override
285    def exists(self) -> bool:
286        return bool(self.node)
287
288    @override
289    def handlemessage(self, msg: Any) -> Any:
290        if isinstance(msg, bs.DieMessage):
291            if self.node:
292                self.node.delete()
293        elif isinstance(msg, bs.HitMessage):
294            if self.node:
295                assert msg.force_direction is not None
296                self.node.handlemessage(
297                    'impulse',
298                    msg.pos[0],
299                    msg.pos[1],
300                    msg.pos[2],
301                    msg.velocity[0],
302                    msg.velocity[1],
303                    msg.velocity[2],
304                    1.0 * msg.magnitude,
305                    1.0 * msg.velocity_magnitude,
306                    msg.radius,
307                    0,
308                    msg.force_direction[0],
309                    msg.force_direction[1],
310                    msg.force_direction[2],
311                )
312        else:
313            super().handlemessage(msg)

A lovely egg that can be picked up for points.

Egg(position: tuple[float, float, float] = (0.0, 1.0, 0.0))
254    def __init__(self, position: tuple[float, float, float] = (0.0, 1.0, 0.0)):
255        super().__init__()
256        activity = self.activity
257        assert isinstance(activity, EasterEggHuntGame)
258        shared = SharedObjects.get()
259
260        # Spawn just above the provided point.
261        self._spawn_pos = (position[0], position[1] + 1.0, position[2])
262        ctex = (activity.egg_tex_1, activity.egg_tex_2, activity.egg_tex_3)[
263            random.randrange(3)
264        ]
265        mats = [shared.object_material, activity.egg_material]
266        self.node = bs.newnode(
267            'prop',
268            delegate=self,
269            attrs={
270                'mesh': activity.egg_mesh,
271                'color_texture': ctex,
272                'body': 'capsule',
273                'reflection': 'soft',
274                'mesh_scale': 0.5,
275                'body_scale': 0.6,
276                'density': 4.0,
277                'reflection_scale': [0.15],
278                'shadow_size': 0.6,
279                'position': self._spawn_pos,
280                'materials': mats,
281            },
282        )

Instantiates an Actor in the current bascenev1.Activity.

node
@override
def exists(self) -> bool:
284    @override
285    def exists(self) -> bool:
286        return bool(self.node)

Returns whether the Actor is still present in a meaningful way.

Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see bascenev1.Actor.is_alive() for that).

If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with bascenev1.Actor.autoretain()

The default implementation of this method always return True.

Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.

@override
def handlemessage(self, msg: Any) -> Any:
288    @override
289    def handlemessage(self, msg: Any) -> Any:
290        if isinstance(msg, bs.DieMessage):
291            if self.node:
292                self.node.delete()
293        elif isinstance(msg, bs.HitMessage):
294            if self.node:
295                assert msg.force_direction is not None
296                self.node.handlemessage(
297                    'impulse',
298                    msg.pos[0],
299                    msg.pos[1],
300                    msg.pos[2],
301                    msg.velocity[0],
302                    msg.velocity[1],
303                    msg.velocity[2],
304                    1.0 * msg.magnitude,
305                    1.0 * msg.velocity_magnitude,
306                    msg.radius,
307                    0,
308                    msg.force_direction[0],
309                    msg.force_direction[1],
310                    msg.force_direction[2],
311                )
312        else:
313            super().handlemessage(msg)

General message handling; can be passed any message object.

Inherited Members
bascenev1._actor.Actor
autoretain
on_expire
expired
is_alive
activity
getactivity