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

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

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

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'>
def on_transition_in(self) -> None:
113    def on_transition_in(self) -> None:
114        super().on_transition_in()
115        bs.timer(1.3, self._new_wave_sound.play)
116        self._scoreboard = Scoreboard(
117            label=bs.Lstr(resource='scoreText'), score_split=0.5
118        )

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.

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

Called once the previous Activity has finished transitioning out.

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

def spawn_player( self, player: Player) -> bascenev1._actor.Actor:
133    def spawn_player(self, player: Player) -> bs.Actor:
134        pos = (
135            self._spawn_center[0] + random.uniform(-1.5, 1.5),
136            self._spawn_center[1],
137            self._spawn_center[2] + random.uniform(-1.5, 1.5),
138        )
139        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:
205    def do_end(self, outcome: str) -> None:
206        """End the game."""
207        if outcome == 'defeat':
208            self.fade_to_red()
209        self.end(
210            delay=2.0,
211            results={
212                'outcome': outcome,
213                'score': self._score,
214                'playerinfos': self.initialplayerinfos,
215            },
216        )

End the game.

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

General message handling; can be passed any message object.

def end_game(self) -> None:
331    def end_game(self) -> None:
332        # Tell our bots to celebrate just to rub it in.
333        self._bots.final_celebrate()
334        bs.setmusic(None)
335        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