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

Spawning info for a particular bot type.

SpawnInfo(spawnrate: float, increase: float, dincrease: float)
class Player(ba._player.Player[ForwardRef('Team')]):
37class Player(ba.Player['Team']):
38    """Our player type for this game."""

Our player type for this game.

Player()
class Team(ba._team.Team[bastd.game.thelaststand.Player]):
41class Team(ba.Team[Player]):
42    """Our team type for this game."""

Our team type for this game.

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

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

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

Instantiate the Activity.

name: str | None = 'The Last Stand'
description: str | None = 'Final glorious epic slow motion battle to the death.'
tips: list[str | ba._gameutils.GameTip] = ['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: ba._music.MusicType | None = <MusicType.EPIC: 'Epic'>
def on_transition_in(self) -> None:
101    def on_transition_in(self) -> None:
102        super().on_transition_in()
103        ba.timer(1.3, ba.Call(ba.playsound, self._new_wave_sound))
104        self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'),
105                                      score_split=0.5)

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 ba.Activity.on_begin() is called.

def on_begin(self) -> None:
107    def on_begin(self) -> None:
108        super().on_begin()
109
110        # Spit out a few powerups and start dropping more shortly.
111        self._drop_powerups(standard_points=True)
112        ba.timer(2.0, ba.WeakCall(self._start_powerup_drops))
113        ba.timer(0.001, ba.WeakCall(self._start_bot_updates))
114        self.setup_low_life_warning_sound()
115        self._update_scores()
116        self._tntspawner = TNTSpawner(position=self._tntspawnpos,
117                                      respawn_time=10.0)

Called once the previous ba.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: bastd.game.thelaststand.Player) -> ba._actor.Actor:
119    def spawn_player(self, player: Player) -> ba.Actor:
120        pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5),
121               self._spawn_center[1],
122               self._spawn_center[2] + random.uniform(-1.5, 1.5))
123        return self.spawn_player_spaz(player, position=pos)

Spawn something for the provided ba.Player.

The default implementation simply calls spawn_player_spaz().

def do_end(self, outcome: str) -> None:
175    def do_end(self, outcome: str) -> None:
176        """End the game."""
177        if outcome == 'defeat':
178            self.fade_to_red()
179        self.end(delay=2.0,
180                 results={
181                     'outcome': outcome,
182                     'score': self._score,
183                     'playerinfos': self.initialplayerinfos
184                 })

End the game.

def handlemessage(self, msg: Any) -> Any:
256    def handlemessage(self, msg: Any) -> Any:
257        if isinstance(msg, ba.PlayerDiedMessage):
258            player = msg.getplayer(Player)
259            self.stats.player_was_killed(player)
260            ba.timer(0.1, self._checkroundover)
261
262        elif isinstance(msg, ba.PlayerScoredMessage):
263            self._score += msg.score
264            self._update_scores()
265
266        elif isinstance(msg, SpazBotDiedMessage):
267            pts, importance = msg.spazbot.get_death_points(msg.how)
268            target: Sequence[float] | None
269            if msg.killerplayer:
270                assert msg.spazbot.node
271                target = msg.spazbot.node.position
272                self.stats.player_scored(msg.killerplayer,
273                                         pts,
274                                         target=target,
275                                         kill=True,
276                                         screenmessage=False,
277                                         importance=importance)
278                ba.playsound(self._dingsound
279                             if importance == 1 else self._dingsoundhigh,
280                             volume=0.6)
281
282            # Normally we pull scores from the score-set, but if there's no
283            # player lets be explicit.
284            else:
285                self._score += pts
286            self._update_scores()
287        else:
288            super().handlemessage(msg)

General message handling; can be passed any message object.

def end_game(self) -> None:
293    def end_game(self) -> None:
294        # Tell our bots to celebrate just to rub it in.
295        self._bots.final_celebrate()
296        ba.setmusic(None)
297        ba.pushcall(ba.WeakCall(self.do_end, 'defeat'))

Tell the game to wrap up and call ba.Activity.end() immediately.

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 (ba.GameActivity.setup_standard_time_limit()) will work with the game.

Inherited Members
ba._coopgame.CoopGameActivity
session
supports_session_type
get_score_type
celebrate
spawn_player_spaz
fade_to_red
setup_low_life_warning_sound
ba._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
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
ba._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
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
ba._dependency.DependencyComponent
dep_is_present
get_dynamic_deps