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

Spawning info for a particular bot type.

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

Our player type for this game.

Player()
Inherited Members
ba._player.Player
actor
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(ba._team.Team[bastd.game.thelaststand.Player]):
52class Team(ba.Team[Player]):
53    """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]):
 56class TheLastStandGame(ba.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 = ba.MusicType.EPIC
 73
 74    def __init__(self, settings: dict):
 75        settings['map'] = 'Rampage'
 76        super().__init__(settings)
 77        self._new_wave_sound = ba.getsound('scoreHit01')
 78        self._winsound = ba.getsound('score')
 79        self._cashregistersound = ba.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 = ba.getsound('dingSmall')
 90        self._dingsoundhigh = ba.getsound('dingSmallHigh')
 91        self._tntspawner: TNTSpawner | None = None
 92        self._bot_update_interval: float | None = None
 93        self._bot_update_timer: ba.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        }  # yapf: disable
111
112    def on_transition_in(self) -> None:
113        super().on_transition_in()
114        ba.timer(1.3, ba.Call(ba.playsound, self._new_wave_sound))
115        self._scoreboard = Scoreboard(
116            label=ba.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        ba.timer(2.0, ba.WeakCall(self._start_powerup_drops))
125        ba.timer(0.001, ba.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) -> ba.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 = ba.Timer(
149            self._bot_update_interval, ba.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 = ba.Timer(
164            3.0, ba.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 bastd.actor import powerupbox
172
173        if standard_points:
174            pts = self.map.powerup_spawn_points
175            for i in range(len(pts)):
176                ba.timer(
177                    1.0 + i * 0.5,
178                    ba.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 = ba.Timer(
221            self._bot_update_interval, ba.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                ba.print_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, ba.PlayerDiedMessage):
295            player = msg.getplayer(Player)
296            self.stats.player_was_killed(player)
297            ba.timer(0.1, self._checkroundover)
298
299        elif isinstance(msg, ba.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                ba.playsound(
318                    self._dingsound if importance == 1 else self._dingsoundhigh,
319                    volume=0.6,
320                )
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        ba.setmusic(None)
334        ba.pushcall(ba.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()

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

TheLastStandGame(settings: dict)
 74    def __init__(self, settings: dict):
 75        settings['map'] = 'Rampage'
 76        super().__init__(settings)
 77        self._new_wave_sound = ba.getsound('scoreHit01')
 78        self._winsound = ba.getsound('score')
 79        self._cashregistersound = ba.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 = ba.getsound('dingSmall')
 90        self._dingsoundhigh = ba.getsound('dingSmallHigh')
 91        self._tntspawner: TNTSpawner | None = None
 92        self._bot_update_interval: float | None = None
 93        self._bot_update_timer: ba.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        }  # yapf: disable

Instantiate the Activity.

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

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:
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        ba.timer(2.0, ba.WeakCall(self._start_powerup_drops))
125        ba.timer(0.001, ba.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        )

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:
132    def spawn_player(self, player: Player) -> ba.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)

Spawn something for the provided ba.Player.

The default implementation simply calls spawn_player_spaz().

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

End the game.

def handlemessage(self, msg: Any) -> Any:
293    def handlemessage(self, msg: Any) -> Any:
294        if isinstance(msg, ba.PlayerDiedMessage):
295            player = msg.getplayer(Player)
296            self.stats.player_was_killed(player)
297            ba.timer(0.1, self._checkroundover)
298
299        elif isinstance(msg, ba.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                ba.playsound(
318                    self._dingsound if importance == 1 else self._dingsoundhigh,
319                    volume=0.6,
320                )
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)

General message handling; can be passed any message object.

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