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

Our player type for this game.

class Team(bascenev1._team.Team[bascenev1lib.game.thelaststand.Player]):
54class Team(bs.Team[Player]):
55    """Our team type for this game."""

Our team type for this game.

class TheLastStandGame(bascenev1._coopgame.CoopGameActivity[bascenev1lib.game.thelaststand.Player, bascenev1lib.game.thelaststand.Team]):
 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()

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

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

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

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

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

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

def do_end(self, outcome: str) -> None:
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        )

End the game.

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

General message handling; can be passed any message object.

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

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.