bascenev1lib.game.thelaststand

Defines the last stand minigame.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Defines the last stand minigame."""
  4
  5from __future__ import annotations
  6
  7import random
  8import logging
  9from dataclasses import dataclass
 10from typing import TYPE_CHECKING
 11
 12from typing_extensions import override
 13import bascenev1 as bs
 14
 15from bascenev1lib.actor.playerspaz import PlayerSpaz
 16from bascenev1lib.actor.bomb import TNTSpawner
 17from bascenev1lib.actor.scoreboard import Scoreboard
 18from bascenev1lib.actor.powerupbox import PowerupBoxFactory, PowerupBox
 19from bascenev1lib.actor.spazbot import (
 20    SpazBotSet,
 21    SpazBotDiedMessage,
 22    BomberBot,
 23    BomberBotPro,
 24    BomberBotProShielded,
 25    BrawlerBot,
 26    BrawlerBotPro,
 27    BrawlerBotProShielded,
 28    TriggerBot,
 29    TriggerBotPro,
 30    TriggerBotProShielded,
 31    ChargerBot,
 32    StickyBot,
 33    ExplodeyBot,
 34)
 35
 36if TYPE_CHECKING:
 37    from typing import Any, Sequence
 38    from bascenev1lib.actor.spazbot import SpazBot
 39
 40
 41@dataclass
 42class SpawnInfo:
 43    """Spawning info for a particular bot type."""
 44
 45    spawnrate: float
 46    increase: float
 47    dincrease: float
 48
 49
 50class Player(bs.Player['Team']):
 51    """Our player type for this game."""
 52
 53
 54class Team(bs.Team[Player]):
 55    """Our team type for this game."""
 56
 57
 58class TheLastStandGame(bs.CoopGameActivity[Player, Team]):
 59    """Slow motion how-long-can-you-last game."""
 60
 61    name = 'The Last Stand'
 62    description = 'Final glorious epic slow motion battle to the death.'
 63    tips = [
 64        'This level never ends, but a high score here\n'
 65        'will earn you eternal respect throughout the world.'
 66    ]
 67
 68    # Show messages when players die since it matters here.
 69    announce_player_deaths = True
 70
 71    # And of course the most important part.
 72    slow_motion = True
 73
 74    default_music = bs.MusicType.EPIC
 75
 76    def __init__(self, settings: dict):
 77        settings['map'] = 'Rampage'
 78        super().__init__(settings)
 79        self._new_wave_sound = bs.getsound('scoreHit01')
 80        self._winsound = bs.getsound('score')
 81        self._cashregistersound = bs.getsound('cashRegister')
 82        self._spawn_center = (0, 5.5, -4.14)
 83        self._tntspawnpos = (0, 5.5, -6)
 84        self._powerup_center = (0, 7, -4.14)
 85        self._powerup_spread = (7, 2)
 86        self._preset = str(settings.get('preset', 'default'))
 87        self._excludepowerups: list[str] = []
 88        self._scoreboard: Scoreboard | None = None
 89        self._score = 0
 90        self._bots = SpazBotSet()
 91        self._dingsound = bs.getsound('dingSmall')
 92        self._dingsoundhigh = bs.getsound('dingSmallHigh')
 93        self._tntspawner: TNTSpawner | None = None
 94        self._bot_update_interval: float | None = None
 95        self._bot_update_timer: bs.Timer | None = None
 96        self._powerup_drop_timer = None
 97
 98        # For each bot type: [spawnrate, increase, d_increase]
 99        self._bot_spawn_types = {
100            BomberBot: SpawnInfo(1.00, 0.00, 0.000),
101            BomberBotPro: SpawnInfo(0.00, 0.05, 0.001),
102            BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
103            BrawlerBot: SpawnInfo(1.00, 0.00, 0.000),
104            BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001),
105            BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
106            TriggerBot: SpawnInfo(0.30, 0.00, 0.000),
107            TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001),
108            TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
109            ChargerBot: SpawnInfo(0.30, 0.05, 0.000),
110            StickyBot: SpawnInfo(0.10, 0.03, 0.001),
111            ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002),
112        }
113
114    @override
115    def on_transition_in(self) -> None:
116        super().on_transition_in()
117        bs.timer(1.3, self._new_wave_sound.play)
118        self._scoreboard = Scoreboard(
119            label=bs.Lstr(resource='scoreText'), score_split=0.5
120        )
121
122    @override
123    def on_begin(self) -> None:
124        super().on_begin()
125
126        # Spit out a few powerups and start dropping more shortly.
127        self._drop_powerups(standard_points=True)
128        bs.timer(2.0, bs.WeakCall(self._start_powerup_drops))
129        bs.timer(0.001, bs.WeakCall(self._start_bot_updates))
130        self.setup_low_life_warning_sound()
131        self._update_scores()
132        self._tntspawner = TNTSpawner(
133            position=self._tntspawnpos, respawn_time=10.0
134        )
135
136    @override
137    def spawn_player(self, player: Player) -> bs.Actor:
138        pos = (
139            self._spawn_center[0] + random.uniform(-1.5, 1.5),
140            self._spawn_center[1],
141            self._spawn_center[2] + random.uniform(-1.5, 1.5),
142        )
143        return self.spawn_player_spaz(player, position=pos)
144
145    def _start_bot_updates(self) -> None:
146        self._bot_update_interval = 3.3 - 0.3 * (len(self.players))
147        self._update_bots()
148        self._update_bots()
149        if len(self.players) > 2:
150            self._update_bots()
151        if len(self.players) > 3:
152            self._update_bots()
153        self._bot_update_timer = bs.Timer(
154            self._bot_update_interval, bs.WeakCall(self._update_bots)
155        )
156
157    def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None:
158        if poweruptype is None:
159            poweruptype = PowerupBoxFactory.get().get_random_powerup_type(
160                excludetypes=self._excludepowerups
161            )
162        PowerupBox(
163            position=self.map.powerup_spawn_points[index],
164            poweruptype=poweruptype,
165        ).autoretain()
166
167    def _start_powerup_drops(self) -> None:
168        self._powerup_drop_timer = bs.Timer(
169            3.0, bs.WeakCall(self._drop_powerups), repeat=True
170        )
171
172    def _drop_powerups(
173        self, standard_points: bool = False, force_first: str | None = None
174    ) -> None:
175        """Generic powerup drop."""
176        from bascenev1lib.actor import powerupbox
177
178        if standard_points:
179            pts = self.map.powerup_spawn_points
180            for i in range(len(pts)):
181                bs.timer(
182                    1.0 + i * 0.5,
183                    bs.WeakCall(
184                        self._drop_powerup, i, force_first if i == 0 else None
185                    ),
186                )
187        else:
188            drop_pt = (
189                self._powerup_center[0]
190                + random.uniform(
191                    -1.0 * self._powerup_spread[0],
192                    1.0 * self._powerup_spread[0],
193                ),
194                self._powerup_center[1],
195                self._powerup_center[2]
196                + random.uniform(
197                    -self._powerup_spread[1], self._powerup_spread[1]
198                ),
199            )
200
201            # Drop one random one somewhere.
202            powerupbox.PowerupBox(
203                position=drop_pt,
204                poweruptype=PowerupBoxFactory.get().get_random_powerup_type(
205                    excludetypes=self._excludepowerups
206                ),
207            ).autoretain()
208
209    def do_end(self, outcome: str) -> None:
210        """End the game."""
211        if outcome == 'defeat':
212            self.fade_to_red()
213        self.end(
214            delay=2.0,
215            results={
216                'outcome': outcome,
217                'score': self._score,
218                'playerinfos': self.initialplayerinfos,
219            },
220        )
221
222    def _update_bots(self) -> None:
223        assert self._bot_update_interval is not None
224        self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98)
225        self._bot_update_timer = bs.Timer(
226            self._bot_update_interval, bs.WeakCall(self._update_bots)
227        )
228        botspawnpts: list[Sequence[float]] = [
229            [-5.0, 5.5, -4.14],
230            [0.0, 5.5, -4.14],
231            [5.0, 5.5, -4.14],
232        ]
233        dists = [0.0, 0.0, 0.0]
234        playerpts: list[Sequence[float]] = []
235        for player in self.players:
236            try:
237                if player.is_alive():
238                    assert isinstance(player.actor, PlayerSpaz)
239                    assert player.actor.node
240                    playerpts.append(player.actor.node.position)
241            except Exception:
242                logging.exception('Error updating bots.')
243        for i in range(3):
244            for playerpt in playerpts:
245                dists[i] += abs(playerpt[0] - botspawnpts[i][0])
246            dists[i] += random.random() * 5.0  # Minor random variation.
247        if dists[0] > dists[1] and dists[0] > dists[2]:
248            spawnpt = botspawnpts[0]
249        elif dists[1] > dists[2]:
250            spawnpt = botspawnpts[1]
251        else:
252            spawnpt = botspawnpts[2]
253
254        spawnpt = (
255            spawnpt[0] + 3.0 * (random.random() - 0.5),
256            spawnpt[1],
257            2.0 * (random.random() - 0.5) + spawnpt[2],
258        )
259
260        # Normalize our bot type total and find a random number within that.
261        total = 0.0
262        for spawninfo in self._bot_spawn_types.values():
263            total += spawninfo.spawnrate
264        randval = random.random() * total
265
266        # Now go back through and see where this value falls.
267        total = 0
268        bottype: type[SpazBot] | None = None
269        for spawntype, spawninfo in self._bot_spawn_types.items():
270            total += spawninfo.spawnrate
271            if randval <= total:
272                bottype = spawntype
273                break
274        spawn_time = 1.0
275        assert bottype is not None
276        self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time)
277
278        # After every spawn we adjust our ratios slightly to get more
279        # difficult.
280        for spawninfo in self._bot_spawn_types.values():
281            spawninfo.spawnrate += spawninfo.increase
282            spawninfo.increase += spawninfo.dincrease
283
284    def _update_scores(self) -> None:
285        score = self._score
286
287        # Achievements apply to the default preset only.
288        if self._preset == 'default':
289            if score >= 250:
290                self._award_achievement('Last Stand Master')
291            if score >= 500:
292                self._award_achievement('Last Stand Wizard')
293            if score >= 1000:
294                self._award_achievement('Last Stand God')
295        assert self._scoreboard is not None
296        self._scoreboard.set_team_value(self.teams[0], score, max_score=None)
297
298    @override
299    def handlemessage(self, msg: Any) -> Any:
300        if isinstance(msg, bs.PlayerDiedMessage):
301            player = msg.getplayer(Player)
302            self.stats.player_was_killed(player)
303            bs.timer(0.1, self._checkroundover)
304
305        elif isinstance(msg, bs.PlayerScoredMessage):
306            self._score += msg.score
307            self._update_scores()
308
309        elif isinstance(msg, SpazBotDiedMessage):
310            pts, importance = msg.spazbot.get_death_points(msg.how)
311            target: Sequence[float] | None
312            if msg.killerplayer:
313                assert msg.spazbot.node
314                target = msg.spazbot.node.position
315                self.stats.player_scored(
316                    msg.killerplayer,
317                    pts,
318                    target=target,
319                    kill=True,
320                    screenmessage=False,
321                    importance=importance,
322                )
323                diesound = (
324                    self._dingsound if importance == 1 else self._dingsoundhigh
325                )
326                diesound.play(volume=0.6)
327
328            # Normally we pull scores from the score-set, but if there's no
329            # player lets be explicit.
330            else:
331                self._score += pts
332            self._update_scores()
333        else:
334            super().handlemessage(msg)
335
336    @override
337    def end_game(self) -> None:
338        # Tell our bots to celebrate just to rub it in.
339        self._bots.final_celebrate()
340        bs.setmusic(None)
341        bs.pushcall(bs.WeakCall(self.do_end, 'defeat'))
342
343    def _checkroundover(self) -> None:
344        """End the round if conditions are met."""
345        if not any(player.is_alive() for player in self.teams[0].players):
346            self.end_game()
@dataclass
class SpawnInfo:
42@dataclass
43class SpawnInfo:
44    """Spawning info for a particular bot type."""
45
46    spawnrate: float
47    increase: float
48    dincrease: float

Spawning info for a particular bot type.

SpawnInfo(spawnrate: float, increase: float, dincrease: float)
spawnrate: float
increase: float
dincrease: float
class Player(bascenev1._player.Player[ForwardRef('Team')]):
51class Player(bs.Player['Team']):
52    """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.thelaststand.Player]):
55class Team(bs.Team[Player]):
56    """Our team type for this game."""

Our team type for this game.

Inherited Members
bascenev1._team.Team
players
id
name
color
manual_init
customdata
on_expire
sessionteam
class TheLastStandGame(bascenev1._coopgame.CoopGameActivity[bascenev1lib.game.thelaststand.Player, bascenev1lib.game.thelaststand.Team]):
 59class TheLastStandGame(bs.CoopGameActivity[Player, Team]):
 60    """Slow motion how-long-can-you-last game."""
 61
 62    name = 'The Last Stand'
 63    description = 'Final glorious epic slow motion battle to the death.'
 64    tips = [
 65        'This level never ends, but a high score here\n'
 66        'will earn you eternal respect throughout the world.'
 67    ]
 68
 69    # Show messages when players die since it matters here.
 70    announce_player_deaths = True
 71
 72    # And of course the most important part.
 73    slow_motion = True
 74
 75    default_music = bs.MusicType.EPIC
 76
 77    def __init__(self, settings: dict):
 78        settings['map'] = 'Rampage'
 79        super().__init__(settings)
 80        self._new_wave_sound = bs.getsound('scoreHit01')
 81        self._winsound = bs.getsound('score')
 82        self._cashregistersound = bs.getsound('cashRegister')
 83        self._spawn_center = (0, 5.5, -4.14)
 84        self._tntspawnpos = (0, 5.5, -6)
 85        self._powerup_center = (0, 7, -4.14)
 86        self._powerup_spread = (7, 2)
 87        self._preset = str(settings.get('preset', 'default'))
 88        self._excludepowerups: list[str] = []
 89        self._scoreboard: Scoreboard | None = None
 90        self._score = 0
 91        self._bots = SpazBotSet()
 92        self._dingsound = bs.getsound('dingSmall')
 93        self._dingsoundhigh = bs.getsound('dingSmallHigh')
 94        self._tntspawner: TNTSpawner | None = None
 95        self._bot_update_interval: float | None = None
 96        self._bot_update_timer: bs.Timer | None = None
 97        self._powerup_drop_timer = None
 98
 99        # For each bot type: [spawnrate, increase, d_increase]
100        self._bot_spawn_types = {
101            BomberBot: SpawnInfo(1.00, 0.00, 0.000),
102            BomberBotPro: SpawnInfo(0.00, 0.05, 0.001),
103            BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
104            BrawlerBot: SpawnInfo(1.00, 0.00, 0.000),
105            BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001),
106            BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
107            TriggerBot: SpawnInfo(0.30, 0.00, 0.000),
108            TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001),
109            TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
110            ChargerBot: SpawnInfo(0.30, 0.05, 0.000),
111            StickyBot: SpawnInfo(0.10, 0.03, 0.001),
112            ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002),
113        }
114
115    @override
116    def on_transition_in(self) -> None:
117        super().on_transition_in()
118        bs.timer(1.3, self._new_wave_sound.play)
119        self._scoreboard = Scoreboard(
120            label=bs.Lstr(resource='scoreText'), score_split=0.5
121        )
122
123    @override
124    def on_begin(self) -> None:
125        super().on_begin()
126
127        # Spit out a few powerups and start dropping more shortly.
128        self._drop_powerups(standard_points=True)
129        bs.timer(2.0, bs.WeakCall(self._start_powerup_drops))
130        bs.timer(0.001, bs.WeakCall(self._start_bot_updates))
131        self.setup_low_life_warning_sound()
132        self._update_scores()
133        self._tntspawner = TNTSpawner(
134            position=self._tntspawnpos, respawn_time=10.0
135        )
136
137    @override
138    def spawn_player(self, player: Player) -> bs.Actor:
139        pos = (
140            self._spawn_center[0] + random.uniform(-1.5, 1.5),
141            self._spawn_center[1],
142            self._spawn_center[2] + random.uniform(-1.5, 1.5),
143        )
144        return self.spawn_player_spaz(player, position=pos)
145
146    def _start_bot_updates(self) -> None:
147        self._bot_update_interval = 3.3 - 0.3 * (len(self.players))
148        self._update_bots()
149        self._update_bots()
150        if len(self.players) > 2:
151            self._update_bots()
152        if len(self.players) > 3:
153            self._update_bots()
154        self._bot_update_timer = bs.Timer(
155            self._bot_update_interval, bs.WeakCall(self._update_bots)
156        )
157
158    def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None:
159        if poweruptype is None:
160            poweruptype = PowerupBoxFactory.get().get_random_powerup_type(
161                excludetypes=self._excludepowerups
162            )
163        PowerupBox(
164            position=self.map.powerup_spawn_points[index],
165            poweruptype=poweruptype,
166        ).autoretain()
167
168    def _start_powerup_drops(self) -> None:
169        self._powerup_drop_timer = bs.Timer(
170            3.0, bs.WeakCall(self._drop_powerups), repeat=True
171        )
172
173    def _drop_powerups(
174        self, standard_points: bool = False, force_first: str | None = None
175    ) -> None:
176        """Generic powerup drop."""
177        from bascenev1lib.actor import powerupbox
178
179        if standard_points:
180            pts = self.map.powerup_spawn_points
181            for i in range(len(pts)):
182                bs.timer(
183                    1.0 + i * 0.5,
184                    bs.WeakCall(
185                        self._drop_powerup, i, force_first if i == 0 else None
186                    ),
187                )
188        else:
189            drop_pt = (
190                self._powerup_center[0]
191                + random.uniform(
192                    -1.0 * self._powerup_spread[0],
193                    1.0 * self._powerup_spread[0],
194                ),
195                self._powerup_center[1],
196                self._powerup_center[2]
197                + random.uniform(
198                    -self._powerup_spread[1], self._powerup_spread[1]
199                ),
200            )
201
202            # Drop one random one somewhere.
203            powerupbox.PowerupBox(
204                position=drop_pt,
205                poweruptype=PowerupBoxFactory.get().get_random_powerup_type(
206                    excludetypes=self._excludepowerups
207                ),
208            ).autoretain()
209
210    def do_end(self, outcome: str) -> None:
211        """End the game."""
212        if outcome == 'defeat':
213            self.fade_to_red()
214        self.end(
215            delay=2.0,
216            results={
217                'outcome': outcome,
218                'score': self._score,
219                'playerinfos': self.initialplayerinfos,
220            },
221        )
222
223    def _update_bots(self) -> None:
224        assert self._bot_update_interval is not None
225        self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98)
226        self._bot_update_timer = bs.Timer(
227            self._bot_update_interval, bs.WeakCall(self._update_bots)
228        )
229        botspawnpts: list[Sequence[float]] = [
230            [-5.0, 5.5, -4.14],
231            [0.0, 5.5, -4.14],
232            [5.0, 5.5, -4.14],
233        ]
234        dists = [0.0, 0.0, 0.0]
235        playerpts: list[Sequence[float]] = []
236        for player in self.players:
237            try:
238                if player.is_alive():
239                    assert isinstance(player.actor, PlayerSpaz)
240                    assert player.actor.node
241                    playerpts.append(player.actor.node.position)
242            except Exception:
243                logging.exception('Error updating bots.')
244        for i in range(3):
245            for playerpt in playerpts:
246                dists[i] += abs(playerpt[0] - botspawnpts[i][0])
247            dists[i] += random.random() * 5.0  # Minor random variation.
248        if dists[0] > dists[1] and dists[0] > dists[2]:
249            spawnpt = botspawnpts[0]
250        elif dists[1] > dists[2]:
251            spawnpt = botspawnpts[1]
252        else:
253            spawnpt = botspawnpts[2]
254
255        spawnpt = (
256            spawnpt[0] + 3.0 * (random.random() - 0.5),
257            spawnpt[1],
258            2.0 * (random.random() - 0.5) + spawnpt[2],
259        )
260
261        # Normalize our bot type total and find a random number within that.
262        total = 0.0
263        for spawninfo in self._bot_spawn_types.values():
264            total += spawninfo.spawnrate
265        randval = random.random() * total
266
267        # Now go back through and see where this value falls.
268        total = 0
269        bottype: type[SpazBot] | None = None
270        for spawntype, spawninfo in self._bot_spawn_types.items():
271            total += spawninfo.spawnrate
272            if randval <= total:
273                bottype = spawntype
274                break
275        spawn_time = 1.0
276        assert bottype is not None
277        self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time)
278
279        # After every spawn we adjust our ratios slightly to get more
280        # difficult.
281        for spawninfo in self._bot_spawn_types.values():
282            spawninfo.spawnrate += spawninfo.increase
283            spawninfo.increase += spawninfo.dincrease
284
285    def _update_scores(self) -> None:
286        score = self._score
287
288        # Achievements apply to the default preset only.
289        if self._preset == 'default':
290            if score >= 250:
291                self._award_achievement('Last Stand Master')
292            if score >= 500:
293                self._award_achievement('Last Stand Wizard')
294            if score >= 1000:
295                self._award_achievement('Last Stand God')
296        assert self._scoreboard is not None
297        self._scoreboard.set_team_value(self.teams[0], score, max_score=None)
298
299    @override
300    def handlemessage(self, msg: Any) -> Any:
301        if isinstance(msg, bs.PlayerDiedMessage):
302            player = msg.getplayer(Player)
303            self.stats.player_was_killed(player)
304            bs.timer(0.1, self._checkroundover)
305
306        elif isinstance(msg, bs.PlayerScoredMessage):
307            self._score += msg.score
308            self._update_scores()
309
310        elif isinstance(msg, SpazBotDiedMessage):
311            pts, importance = msg.spazbot.get_death_points(msg.how)
312            target: Sequence[float] | None
313            if msg.killerplayer:
314                assert msg.spazbot.node
315                target = msg.spazbot.node.position
316                self.stats.player_scored(
317                    msg.killerplayer,
318                    pts,
319                    target=target,
320                    kill=True,
321                    screenmessage=False,
322                    importance=importance,
323                )
324                diesound = (
325                    self._dingsound if importance == 1 else self._dingsoundhigh
326                )
327                diesound.play(volume=0.6)
328
329            # Normally we pull scores from the score-set, but if there's no
330            # player lets be explicit.
331            else:
332                self._score += pts
333            self._update_scores()
334        else:
335            super().handlemessage(msg)
336
337    @override
338    def end_game(self) -> None:
339        # Tell our bots to celebrate just to rub it in.
340        self._bots.final_celebrate()
341        bs.setmusic(None)
342        bs.pushcall(bs.WeakCall(self.do_end, 'defeat'))
343
344    def _checkroundover(self) -> None:
345        """End the round if conditions are met."""
346        if not any(player.is_alive() for player in self.teams[0].players):
347            self.end_game()

Slow motion how-long-can-you-last game.

TheLastStandGame(settings: dict)
 77    def __init__(self, settings: dict):
 78        settings['map'] = 'Rampage'
 79        super().__init__(settings)
 80        self._new_wave_sound = bs.getsound('scoreHit01')
 81        self._winsound = bs.getsound('score')
 82        self._cashregistersound = bs.getsound('cashRegister')
 83        self._spawn_center = (0, 5.5, -4.14)
 84        self._tntspawnpos = (0, 5.5, -6)
 85        self._powerup_center = (0, 7, -4.14)
 86        self._powerup_spread = (7, 2)
 87        self._preset = str(settings.get('preset', 'default'))
 88        self._excludepowerups: list[str] = []
 89        self._scoreboard: Scoreboard | None = None
 90        self._score = 0
 91        self._bots = SpazBotSet()
 92        self._dingsound = bs.getsound('dingSmall')
 93        self._dingsoundhigh = bs.getsound('dingSmallHigh')
 94        self._tntspawner: TNTSpawner | None = None
 95        self._bot_update_interval: float | None = None
 96        self._bot_update_timer: bs.Timer | None = None
 97        self._powerup_drop_timer = None
 98
 99        # For each bot type: [spawnrate, increase, d_increase]
100        self._bot_spawn_types = {
101            BomberBot: SpawnInfo(1.00, 0.00, 0.000),
102            BomberBotPro: SpawnInfo(0.00, 0.05, 0.001),
103            BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
104            BrawlerBot: SpawnInfo(1.00, 0.00, 0.000),
105            BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001),
106            BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
107            TriggerBot: SpawnInfo(0.30, 0.00, 0.000),
108            TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001),
109            TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
110            ChargerBot: SpawnInfo(0.30, 0.05, 0.000),
111            StickyBot: SpawnInfo(0.10, 0.03, 0.001),
112            ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002),
113        }

Instantiate the Activity.

name = 'The Last Stand'
description = 'Final glorious epic slow motion battle to the death.'
tips = ['This level never ends, but a high score here\nwill earn you eternal respect throughout the world.']
announce_player_deaths = True

Whether to print every time a player dies. This can be pertinent in games such as Death-Match but can be annoying in games where it doesn't matter.

slow_motion = True

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

default_music = <MusicType.EPIC: 'Epic'>
@override
def on_transition_in(self) -> None:
115    @override
116    def on_transition_in(self) -> None:
117        super().on_transition_in()
118        bs.timer(1.3, self._new_wave_sound.play)
119        self._scoreboard = Scoreboard(
120            label=bs.Lstr(resource='scoreText'), score_split=0.5
121        )

Called when the Activity is first becoming visible.

Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.

@override
def on_begin(self) -> None:
123    @override
124    def on_begin(self) -> None:
125        super().on_begin()
126
127        # Spit out a few powerups and start dropping more shortly.
128        self._drop_powerups(standard_points=True)
129        bs.timer(2.0, bs.WeakCall(self._start_powerup_drops))
130        bs.timer(0.001, bs.WeakCall(self._start_bot_updates))
131        self.setup_low_life_warning_sound()
132        self._update_scores()
133        self._tntspawner = TNTSpawner(
134            position=self._tntspawnpos, respawn_time=10.0
135        )

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:
137    @override
138    def spawn_player(self, player: Player) -> bs.Actor:
139        pos = (
140            self._spawn_center[0] + random.uniform(-1.5, 1.5),
141            self._spawn_center[1],
142            self._spawn_center[2] + random.uniform(-1.5, 1.5),
143        )
144        return self.spawn_player_spaz(player, position=pos)

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

def do_end(self, outcome: str) -> None:
210    def do_end(self, outcome: str) -> None:
211        """End the game."""
212        if outcome == 'defeat':
213            self.fade_to_red()
214        self.end(
215            delay=2.0,
216            results={
217                'outcome': outcome,
218                'score': self._score,
219                'playerinfos': self.initialplayerinfos,
220            },
221        )

End the game.

@override
def handlemessage(self, msg: Any) -> Any:
299    @override
300    def handlemessage(self, msg: Any) -> Any:
301        if isinstance(msg, bs.PlayerDiedMessage):
302            player = msg.getplayer(Player)
303            self.stats.player_was_killed(player)
304            bs.timer(0.1, self._checkroundover)
305
306        elif isinstance(msg, bs.PlayerScoredMessage):
307            self._score += msg.score
308            self._update_scores()
309
310        elif isinstance(msg, SpazBotDiedMessage):
311            pts, importance = msg.spazbot.get_death_points(msg.how)
312            target: Sequence[float] | None
313            if msg.killerplayer:
314                assert msg.spazbot.node
315                target = msg.spazbot.node.position
316                self.stats.player_scored(
317                    msg.killerplayer,
318                    pts,
319                    target=target,
320                    kill=True,
321                    screenmessage=False,
322                    importance=importance,
323                )
324                diesound = (
325                    self._dingsound if importance == 1 else self._dingsoundhigh
326                )
327                diesound.play(volume=0.6)
328
329            # Normally we pull scores from the score-set, but if there's no
330            # player lets be explicit.
331            else:
332                self._score += pts
333            self._update_scores()
334        else:
335            super().handlemessage(msg)

General message handling; can be passed any message object.

@override
def end_game(self) -> None:
337    @override
338    def end_game(self) -> None:
339        # Tell our bots to celebrate just to rub it in.
340        self._bots.final_celebrate()
341        bs.setmusic(None)
342        bs.pushcall(bs.WeakCall(self.do_end, 'defeat'))

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._coopgame.CoopGameActivity
session
supports_session_type
get_score_type
celebrate
spawn_player_spaz
fade_to_red
setup_low_life_warning_sound
bascenev1._gameactivity.GameActivity
available_settings
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_supported_maps
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
end
respawn_player
spawn_player_if_exists
setup_standard_powerup_drops
setup_standard_time_limit
show_zoom_message
bascenev1._activity.Activity
settings_raw
teams
players
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
on_player_leave
on_team_join
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