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

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]):
36class Team(bs.Team[Player]):
37    """Our team type for this game."""
38
39    def __init__(self) -> None:
40        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]):
 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)

A game where score is based on collecting eggs.

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

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]) -> list[str]:
56    @override
57    @classmethod
58    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
59        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]) -> bool:
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        )

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:
100    @override
101    def on_team_join(self, team: Team) -> None:
102        if self.has_begun():
103            self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

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

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:
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

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

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

General message handling; can be passed any message object.

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

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):
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)

A lovely egg that can be picked up for points.

Egg(position: tuple[float, float, float] = (0.0, 1.0, 0.0))
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        )

Instantiates an Actor in the current bascenev1.Activity.

node
@override
def exists(self) -> bool:
283    @override
284    def exists(self) -> bool:
285        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:
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)

General message handling; can be passed any message object.

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