bastd.game.runaround

Defines the runaround co-op game.

   1# Released under the MIT License. See LICENSE for details.
   2#
   3"""Defines the runaround co-op game."""
   4
   5# We wear the cone of shame.
   6# pylint: disable=too-many-lines
   7
   8# ba_meta require api 7
   9# (see https://ballistica.net/wiki/meta-tag-system)
  10
  11from __future__ import annotations
  12
  13import random
  14from dataclasses import dataclass
  15from enum import Enum
  16from typing import TYPE_CHECKING
  17
  18import ba
  19from bastd.actor.popuptext import PopupText
  20from bastd.actor.bomb import TNTSpawner
  21from bastd.actor.scoreboard import Scoreboard
  22from bastd.actor.respawnicon import RespawnIcon
  23from bastd.actor.powerupbox import PowerupBox, PowerupBoxFactory
  24from bastd.gameutils import SharedObjects
  25from bastd.actor.spazbot import (
  26    SpazBotSet, SpazBot, SpazBotDiedMessage, BomberBot, BrawlerBot, TriggerBot,
  27    TriggerBotPro, BomberBotProShielded, TriggerBotProShielded, ChargerBot,
  28    ChargerBotProShielded, StickyBot, ExplodeyBot, BrawlerBotProShielded,
  29    BomberBotPro, BrawlerBotPro)
  30
  31if TYPE_CHECKING:
  32    from typing import Any, Sequence
  33
  34
  35class Preset(Enum):
  36    """Play presets."""
  37    ENDLESS = 'endless'
  38    ENDLESS_TOURNAMENT = 'endless_tournament'
  39    PRO = 'pro'
  40    PRO_EASY = 'pro_easy'
  41    UBER = 'uber'
  42    UBER_EASY = 'uber_easy'
  43    TOURNAMENT = 'tournament'
  44    TOURNAMENT_UBER = 'tournament_uber'
  45
  46
  47class Point(Enum):
  48    """Where we can spawn stuff and the corresponding map attr name."""
  49    BOTTOM_LEFT = 'bot_spawn_bottom_left'
  50    BOTTOM_RIGHT = 'bot_spawn_bottom_right'
  51    START = 'bot_spawn_start'
  52
  53
  54@dataclass
  55class Spawn:
  56    """Defines a bot spawn event."""
  57    # noinspection PyUnresolvedReferences
  58    type: type[SpazBot]
  59    path: int = 0
  60    point: Point | None = None
  61
  62
  63@dataclass
  64class Spacing:
  65    """Defines spacing between spawns."""
  66    duration: float
  67
  68
  69@dataclass
  70class Wave:
  71    """Defines a wave of enemies."""
  72    entries: list[Spawn | Spacing | None]
  73
  74
  75class Player(ba.Player['Team']):
  76    """Our player type for this game."""
  77
  78    def __init__(self) -> None:
  79        self.respawn_timer: ba.Timer | None = None
  80        self.respawn_icon: RespawnIcon | None = None
  81
  82
  83class Team(ba.Team[Player]):
  84    """Our team type for this game."""
  85
  86
  87class RunaroundGame(ba.CoopGameActivity[Player, Team]):
  88    """Game involving trying to bomb bots as they walk through the map."""
  89
  90    name = 'Runaround'
  91    description = 'Prevent enemies from reaching the exit.'
  92    tips = [
  93        'Jump just as you\'re throwing to get bombs up to the highest levels.',
  94        'No, you can\'t get up on the ledge. You have to throw bombs.',
  95        'Whip back and forth to get more distance on your throws..'
  96    ]
  97    default_music = ba.MusicType.MARCHING
  98
  99    # How fast our various bot types walk.
 100    _bot_speed_map: dict[type[SpazBot], float] = {
 101        BomberBot: 0.48,
 102        BomberBotPro: 0.48,
 103        BomberBotProShielded: 0.48,
 104        BrawlerBot: 0.57,
 105        BrawlerBotPro: 0.57,
 106        BrawlerBotProShielded: 0.57,
 107        TriggerBot: 0.73,
 108        TriggerBotPro: 0.78,
 109        TriggerBotProShielded: 0.78,
 110        ChargerBot: 1.0,
 111        ChargerBotProShielded: 1.0,
 112        ExplodeyBot: 1.0,
 113        StickyBot: 0.5
 114    }
 115
 116    def __init__(self, settings: dict):
 117        settings['map'] = 'Tower D'
 118        super().__init__(settings)
 119        shared = SharedObjects.get()
 120        self._preset = Preset(settings.get('preset', 'pro'))
 121
 122        self._player_death_sound = ba.getsound('playerDeath')
 123        self._new_wave_sound = ba.getsound('scoreHit01')
 124        self._winsound = ba.getsound('score')
 125        self._cashregistersound = ba.getsound('cashRegister')
 126        self._bad_guy_score_sound = ba.getsound('shieldDown')
 127        self._heart_tex = ba.gettexture('heart')
 128        self._heart_model_opaque = ba.getmodel('heartOpaque')
 129        self._heart_model_transparent = ba.getmodel('heartTransparent')
 130
 131        self._a_player_has_been_killed = False
 132        self._spawn_center = self._map_type.defs.points['spawn1'][0:3]
 133        self._tntspawnpos = self._map_type.defs.points['tnt_loc'][0:3]
 134        self._powerup_center = self._map_type.defs.boxes['powerup_region'][0:3]
 135        self._powerup_spread = (
 136            self._map_type.defs.boxes['powerup_region'][6] * 0.5,
 137            self._map_type.defs.boxes['powerup_region'][8] * 0.5)
 138
 139        self._score_region_material = ba.Material()
 140        self._score_region_material.add_actions(
 141            conditions=('they_have_material', shared.player_material),
 142            actions=(
 143                ('modify_part_collision', 'collide', True),
 144                ('modify_part_collision', 'physical', False),
 145                ('call', 'at_connect', self._handle_reached_end),
 146            ))
 147
 148        self._last_wave_end_time = ba.time()
 149        self._player_has_picked_up_powerup = False
 150        self._scoreboard: Scoreboard | None = None
 151        self._game_over = False
 152        self._wavenum = 0
 153        self._can_end_wave = True
 154        self._score = 0
 155        self._time_bonus = 0
 156        self._score_region: ba.Actor | None = None
 157        self._dingsound = ba.getsound('dingSmall')
 158        self._dingsoundhigh = ba.getsound('dingSmallHigh')
 159        self._exclude_powerups: list[str] | None = None
 160        self._have_tnt: bool | None = None
 161        self._waves: list[Wave] | None = None
 162        self._bots = SpazBotSet()
 163        self._tntspawner: TNTSpawner | None = None
 164        self._lives_bg: ba.NodeActor | None = None
 165        self._start_lives = 10
 166        self._lives = self._start_lives
 167        self._lives_text: ba.NodeActor | None = None
 168        self._flawless = True
 169        self._time_bonus_timer: ba.Timer | None = None
 170        self._time_bonus_text: ba.NodeActor | None = None
 171        self._time_bonus_mult: float | None = None
 172        self._wave_text: ba.NodeActor | None = None
 173        self._flawless_bonus: int | None = None
 174        self._wave_update_timer: ba.Timer | None = None
 175
 176    def on_transition_in(self) -> None:
 177        super().on_transition_in()
 178        self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'),
 179                                      score_split=0.5)
 180        self._score_region = ba.NodeActor(
 181            ba.newnode(
 182                'region',
 183                attrs={
 184                    'position': self.map.defs.boxes['score_region'][0:3],
 185                    'scale': self.map.defs.boxes['score_region'][6:9],
 186                    'type': 'box',
 187                    'materials': [self._score_region_material]
 188                }))
 189
 190    def on_begin(self) -> None:
 191        super().on_begin()
 192        player_count = len(self.players)
 193        hard = self._preset not in {Preset.PRO_EASY, Preset.UBER_EASY}
 194
 195        if self._preset in {Preset.PRO, Preset.PRO_EASY, Preset.TOURNAMENT}:
 196            self._exclude_powerups = ['curse']
 197            self._have_tnt = True
 198            self._waves = [
 199                Wave(entries=[
 200                    Spawn(BomberBot, path=3 if hard else 2),
 201                    Spawn(BomberBot, path=2),
 202                    Spawn(BomberBot, path=2) if hard else None,
 203                    Spawn(BomberBot, path=2) if player_count > 1 else None,
 204                    Spawn(BomberBot, path=1) if hard else None,
 205                    Spawn(BomberBot, path=1) if player_count > 2 else None,
 206                    Spawn(BomberBot, path=1) if player_count > 3 else None,
 207                ]),
 208                Wave(entries=[
 209                    Spawn(BomberBot, path=1) if hard else None,
 210                    Spawn(BomberBot, path=2) if hard else None,
 211                    Spawn(BomberBot, path=2),
 212                    Spawn(BomberBot, path=2),
 213                    Spawn(BomberBot, path=2) if player_count > 3 else None,
 214                    Spawn(BrawlerBot, path=3),
 215                    Spawn(BrawlerBot, path=3),
 216                    Spawn(BrawlerBot, path=3) if hard else None,
 217                    Spawn(BrawlerBot, path=3) if player_count > 1 else None,
 218                    Spawn(BrawlerBot, path=3) if player_count > 2 else None,
 219                ]),
 220                Wave(entries=[
 221                    Spawn(ChargerBot, path=2) if hard else None,
 222                    Spawn(ChargerBot, path=2) if player_count > 2 else None,
 223                    Spawn(TriggerBot, path=2),
 224                    Spawn(TriggerBot, path=2) if player_count > 1 else None,
 225                    Spacing(duration=3.0),
 226                    Spawn(BomberBot, path=2) if hard else None,
 227                    Spawn(BomberBot, path=2) if hard else None,
 228                    Spawn(BomberBot, path=2),
 229                    Spawn(BomberBot, path=3) if hard else None,
 230                    Spawn(BomberBot, path=3),
 231                    Spawn(BomberBot, path=3),
 232                    Spawn(BomberBot, path=3) if player_count > 3 else None,
 233                ]),
 234                Wave(entries=[
 235                    Spawn(TriggerBot, path=1) if hard else None,
 236                    Spacing(duration=1.0) if hard else None,
 237                    Spawn(TriggerBot, path=2),
 238                    Spacing(duration=1.0),
 239                    Spawn(TriggerBot, path=3),
 240                    Spacing(duration=1.0),
 241                    Spawn(TriggerBot, path=1) if hard else None,
 242                    Spacing(duration=1.0) if hard else None,
 243                    Spawn(TriggerBot, path=2),
 244                    Spacing(duration=1.0),
 245                    Spawn(TriggerBot, path=3),
 246                    Spacing(duration=1.0),
 247                    Spawn(TriggerBot, path=1) if (
 248                        player_count > 1 and hard) else None,
 249                    Spacing(duration=1.0),
 250                    Spawn(TriggerBot, path=2) if player_count > 2 else None,
 251                    Spacing(duration=1.0),
 252                    Spawn(TriggerBot, path=3) if player_count > 3 else None,
 253                    Spacing(duration=1.0),
 254                ]),
 255                Wave(entries=[
 256                    Spawn(ChargerBotProShielded if hard else ChargerBot,
 257                          path=1),
 258                    Spawn(BrawlerBot, path=2) if hard else None,
 259                    Spawn(BrawlerBot, path=2),
 260                    Spawn(BrawlerBot, path=2),
 261                    Spawn(BrawlerBot, path=3) if hard else None,
 262                    Spawn(BrawlerBot, path=3),
 263                    Spawn(BrawlerBot, path=3),
 264                    Spawn(BrawlerBot, path=3) if player_count > 1 else None,
 265                    Spawn(BrawlerBot, path=3) if player_count > 2 else None,
 266                    Spawn(BrawlerBot, path=3) if player_count > 3 else None,
 267                ]),
 268                Wave(entries=[
 269                    Spawn(BomberBotProShielded, path=3),
 270                    Spacing(duration=1.5),
 271                    Spawn(BomberBotProShielded, path=2),
 272                    Spacing(duration=1.5),
 273                    Spawn(BomberBotProShielded, path=1) if hard else None,
 274                    Spacing(duration=1.0) if hard else None,
 275                    Spawn(BomberBotProShielded, path=3),
 276                    Spacing(duration=1.5),
 277                    Spawn(BomberBotProShielded, path=2),
 278                    Spacing(duration=1.5),
 279                    Spawn(BomberBotProShielded, path=1) if hard else None,
 280                    Spacing(duration=1.5) if hard else None,
 281                    Spawn(BomberBotProShielded, path=3
 282                          ) if player_count > 1 else None,
 283                    Spacing(duration=1.5),
 284                    Spawn(BomberBotProShielded, path=2
 285                          ) if player_count > 2 else None,
 286                    Spacing(duration=1.5),
 287                    Spawn(BomberBotProShielded, path=1
 288                          ) if player_count > 3 else None,
 289                ]),
 290            ]
 291        elif self._preset in {
 292                Preset.UBER_EASY, Preset.UBER, Preset.TOURNAMENT_UBER
 293        }:
 294            self._exclude_powerups = []
 295            self._have_tnt = True
 296            self._waves = [
 297                Wave(entries=[
 298                    Spawn(TriggerBot, path=1) if hard else None,
 299                    Spawn(TriggerBot, path=2),
 300                    Spawn(TriggerBot, path=2),
 301                    Spawn(TriggerBot, path=3),
 302                    Spawn(BrawlerBotPro if hard else BrawlerBot,
 303                          point=Point.BOTTOM_LEFT),
 304                    Spawn(BrawlerBotPro, point=Point.BOTTOM_RIGHT
 305                          ) if player_count > 2 else None,
 306                ]),
 307                Wave(entries=[
 308                    Spawn(ChargerBot, path=2),
 309                    Spawn(ChargerBot, path=3),
 310                    Spawn(ChargerBot, path=1) if hard else None,
 311                    Spawn(ChargerBot, path=2),
 312                    Spawn(ChargerBot, path=3),
 313                    Spawn(ChargerBot, path=1) if player_count > 2 else None,
 314                ]),
 315                Wave(entries=[
 316                    Spawn(BomberBotProShielded, path=1) if hard else None,
 317                    Spawn(BomberBotProShielded, path=2),
 318                    Spawn(BomberBotProShielded, path=2),
 319                    Spawn(BomberBotProShielded, path=3),
 320                    Spawn(BomberBotProShielded, path=3),
 321                    Spawn(ChargerBot, point=Point.BOTTOM_RIGHT),
 322                    Spawn(ChargerBot, point=Point.BOTTOM_LEFT
 323                          ) if player_count > 2 else None,
 324                ]),
 325                Wave(entries=[
 326                    Spawn(TriggerBotPro, path=1) if hard else None,
 327                    Spawn(TriggerBotPro, path=1 if hard else 2),
 328                    Spawn(TriggerBotPro, path=1 if hard else 2),
 329                    Spawn(TriggerBotPro, path=1 if hard else 2),
 330                    Spawn(TriggerBotPro, path=1 if hard else 2),
 331                    Spawn(TriggerBotPro, path=1 if hard else 2),
 332                    Spawn(TriggerBotPro, path=1 if hard else 2
 333                          ) if player_count > 1 else None,
 334                    Spawn(TriggerBotPro, path=1 if hard else 2
 335                          ) if player_count > 3 else None,
 336                ]),
 337                Wave(entries=[
 338                    Spawn(TriggerBotProShielded if hard else TriggerBotPro,
 339                          point=Point.BOTTOM_LEFT),
 340                    Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT
 341                          ) if hard else None,
 342                    Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT
 343                          ) if player_count > 2 else None,
 344                    Spawn(BomberBot, path=3),
 345                    Spawn(BomberBot, path=3),
 346                    Spacing(duration=5.0),
 347                    Spawn(BrawlerBot, path=2),
 348                    Spawn(BrawlerBot, path=2),
 349                    Spacing(duration=5.0),
 350                    Spawn(TriggerBot, path=1) if hard else None,
 351                    Spawn(TriggerBot, path=1) if hard else None,
 352                ]),
 353                Wave(entries=[
 354                    Spawn(BomberBotProShielded, path=2),
 355                    Spawn(BomberBotProShielded, path=2) if hard else None,
 356                    Spawn(StickyBot, point=Point.BOTTOM_RIGHT),
 357                    Spawn(BomberBotProShielded, path=2),
 358                    Spawn(BomberBotProShielded, path=2),
 359                    Spawn(StickyBot, point=Point.BOTTOM_RIGHT
 360                          ) if player_count > 2 else None,
 361                    Spawn(BomberBotProShielded, path=2),
 362                    Spawn(ExplodeyBot, point=Point.BOTTOM_LEFT),
 363                    Spawn(BomberBotProShielded, path=2),
 364                    Spawn(BomberBotProShielded, path=2
 365                          ) if player_count > 1 else None,
 366                    Spacing(duration=5.0),
 367                    Spawn(StickyBot, point=Point.BOTTOM_LEFT),
 368                    Spacing(duration=2.0),
 369                    Spawn(ExplodeyBot, point=Point.BOTTOM_RIGHT),
 370                ]),
 371            ]
 372        elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}:
 373            self._exclude_powerups = []
 374            self._have_tnt = True
 375
 376        # Spit out a few powerups and start dropping more shortly.
 377        self._drop_powerups(standard_points=True)
 378        ba.timer(4.0, self._start_powerup_drops)
 379        self.setup_low_life_warning_sound()
 380        self._update_scores()
 381
 382        # Our TNT spawner (if applicable).
 383        if self._have_tnt:
 384            self._tntspawner = TNTSpawner(position=self._tntspawnpos)
 385
 386        # Make sure to stay out of the way of menu/party buttons in the corner.
 387        uiscale = ba.app.ui.uiscale
 388        l_offs = (-80 if uiscale is ba.UIScale.SMALL else
 389                  -40 if uiscale is ba.UIScale.MEDIUM else 0)
 390
 391        self._lives_bg = ba.NodeActor(
 392            ba.newnode('image',
 393                       attrs={
 394                           'texture': self._heart_tex,
 395                           'model_opaque': self._heart_model_opaque,
 396                           'model_transparent': self._heart_model_transparent,
 397                           'attach': 'topRight',
 398                           'scale': (90, 90),
 399                           'position': (-110 + l_offs, -50),
 400                           'color': (1, 0.2, 0.2)
 401                       }))
 402        # FIXME; should not set things based on vr mode.
 403        #  (won't look right to non-vr connected clients, etc)
 404        vrmode = ba.app.vr_mode
 405        self._lives_text = ba.NodeActor(
 406            ba.newnode(
 407                'text',
 408                attrs={
 409                    'v_attach': 'top',
 410                    'h_attach': 'right',
 411                    'h_align': 'center',
 412                    'color': (1, 1, 1, 1) if vrmode else (0.8, 0.8, 0.8, 1.0),
 413                    'flatness': 1.0 if vrmode else 0.5,
 414                    'shadow': 1.0 if vrmode else 0.5,
 415                    'vr_depth': 10,
 416                    'position': (-113 + l_offs, -69),
 417                    'scale': 1.3,
 418                    'text': str(self._lives)
 419                }))
 420
 421        ba.timer(2.0, self._start_updating_waves)
 422
 423    def _handle_reached_end(self) -> None:
 424        spaz = ba.getcollision().opposingnode.getdelegate(SpazBot, True)
 425        if not spaz.is_alive():
 426            return  # Ignore bodies flying in.
 427
 428        self._flawless = False
 429        pos = spaz.node.position
 430        ba.playsound(self._bad_guy_score_sound, position=pos)
 431        light = ba.newnode('light',
 432                           attrs={
 433                               'position': pos,
 434                               'radius': 0.5,
 435                               'color': (1, 0, 0)
 436                           })
 437        ba.animate(light, 'intensity', {0.0: 0, 0.1: 1, 0.5: 0}, loop=False)
 438        ba.timer(1.0, light.delete)
 439        spaz.handlemessage(
 440            ba.DieMessage(immediate=True, how=ba.DeathType.REACHED_GOAL))
 441
 442        if self._lives > 0:
 443            self._lives -= 1
 444            if self._lives == 0:
 445                self._bots.stop_moving()
 446                self.continue_or_end_game()
 447            assert self._lives_text is not None
 448            assert self._lives_text.node
 449            self._lives_text.node.text = str(self._lives)
 450            delay = 0.0
 451
 452            def _safesetattr(node: ba.Node, attr: str, value: Any) -> None:
 453                if node:
 454                    setattr(node, attr, value)
 455
 456            for _i in range(4):
 457                ba.timer(
 458                    delay,
 459                    ba.Call(_safesetattr, self._lives_text.node, 'color',
 460                            (1, 0, 0, 1.0)))
 461                assert self._lives_bg is not None
 462                assert self._lives_bg.node
 463                ba.timer(
 464                    delay,
 465                    ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 0.5))
 466                delay += 0.125
 467                ba.timer(
 468                    delay,
 469                    ba.Call(_safesetattr, self._lives_text.node, 'color',
 470                            (1.0, 1.0, 0.0, 1.0)))
 471                ba.timer(
 472                    delay,
 473                    ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 1.0))
 474                delay += 0.125
 475            ba.timer(
 476                delay,
 477                ba.Call(_safesetattr, self._lives_text.node, 'color',
 478                        (0.8, 0.8, 0.8, 1.0)))
 479
 480    def on_continue(self) -> None:
 481        self._lives = 3
 482        assert self._lives_text is not None
 483        assert self._lives_text.node
 484        self._lives_text.node.text = str(self._lives)
 485        self._bots.start_moving()
 486
 487    def spawn_player(self, player: Player) -> ba.Actor:
 488        pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5),
 489               self._spawn_center[1],
 490               self._spawn_center[2] + random.uniform(-1.5, 1.5))
 491        spaz = self.spawn_player_spaz(player, position=pos)
 492        if self._preset in {Preset.PRO_EASY, Preset.UBER_EASY}:
 493            spaz.impact_scale = 0.25
 494
 495        # Add the material that causes us to hit the player-wall.
 496        spaz.pick_up_powerup_callback = self._on_player_picked_up_powerup
 497        return spaz
 498
 499    def _on_player_picked_up_powerup(self, player: ba.Actor) -> None:
 500        del player  # Unused.
 501        self._player_has_picked_up_powerup = True
 502
 503    def _drop_powerup(self,
 504                      index: int,
 505                      poweruptype: str | None = None) -> None:
 506        if poweruptype is None:
 507            poweruptype = (PowerupBoxFactory.get().get_random_powerup_type(
 508                excludetypes=self._exclude_powerups))
 509        PowerupBox(position=self.map.powerup_spawn_points[index],
 510                   poweruptype=poweruptype).autoretain()
 511
 512    def _start_powerup_drops(self) -> None:
 513        ba.timer(3.0, self._drop_powerups, repeat=True)
 514
 515    def _drop_powerups(self,
 516                       standard_points: bool = False,
 517                       force_first: str | None = None) -> None:
 518        """Generic powerup drop."""
 519
 520        # If its been a minute since our last wave finished emerging, stop
 521        # giving out land-mine powerups. (prevents players from waiting
 522        # around for them on purpose and filling the map up)
 523        if ba.time() - self._last_wave_end_time > 60.0:
 524            extra_excludes = ['land_mines']
 525        else:
 526            extra_excludes = []
 527
 528        if standard_points:
 529            points = self.map.powerup_spawn_points
 530            for i in range(len(points)):
 531                ba.timer(
 532                    1.0 + i * 0.5,
 533                    ba.Call(self._drop_powerup, i,
 534                            force_first if i == 0 else None))
 535        else:
 536            pos = (self._powerup_center[0] + random.uniform(
 537                -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]),
 538                   self._powerup_center[1],
 539                   self._powerup_center[2] + random.uniform(
 540                       -self._powerup_spread[1], self._powerup_spread[1]))
 541
 542            # drop one random one somewhere..
 543            assert self._exclude_powerups is not None
 544            PowerupBox(
 545                position=pos,
 546                poweruptype=PowerupBoxFactory.get().get_random_powerup_type(
 547                    excludetypes=self._exclude_powerups +
 548                    extra_excludes)).autoretain()
 549
 550    def end_game(self) -> None:
 551        ba.pushcall(ba.Call(self.do_end, 'defeat'))
 552        ba.setmusic(None)
 553        ba.playsound(self._player_death_sound)
 554
 555    def do_end(self, outcome: str) -> None:
 556        """End the game now with the provided outcome."""
 557
 558        if outcome == 'defeat':
 559            delay = 2.0
 560            self.fade_to_red()
 561        else:
 562            delay = 0
 563
 564        score: int | None
 565        if self._wavenum >= 2:
 566            score = self._score
 567            fail_message = None
 568        else:
 569            score = None
 570            fail_message = ba.Lstr(resource='reachWave2Text')
 571
 572        self.end(delay=delay,
 573                 results={
 574                     'outcome': outcome,
 575                     'score': score,
 576                     'fail_message': fail_message,
 577                     'playerinfos': self.initialplayerinfos
 578                 })
 579
 580    def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
 581        self._show_standard_scores_to_beat_ui(scores)
 582
 583    def _update_waves(self) -> None:
 584        # pylint: disable=too-many-branches
 585
 586        # If we have no living bots, go to the next wave.
 587        if (self._can_end_wave and not self._bots.have_living_bots()
 588                and not self._game_over and self._lives > 0):
 589
 590            self._can_end_wave = False
 591            self._time_bonus_timer = None
 592            self._time_bonus_text = None
 593
 594            if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}:
 595                won = False
 596            else:
 597                assert self._waves is not None
 598                won = (self._wavenum == len(self._waves))
 599
 600            # Reward time bonus.
 601            base_delay = 4.0 if won else 0
 602            if self._time_bonus > 0:
 603                ba.timer(0, ba.Call(ba.playsound, self._cashregistersound))
 604                ba.timer(base_delay,
 605                         ba.Call(self._award_time_bonus, self._time_bonus))
 606                base_delay += 1.0
 607
 608            # Reward flawless bonus.
 609            if self._wavenum > 0 and self._flawless:
 610                ba.timer(base_delay, self._award_flawless_bonus)
 611                base_delay += 1.0
 612
 613            self._flawless = True  # reset
 614
 615            if won:
 616
 617                # Completion achievements:
 618                if self._preset in {Preset.PRO, Preset.PRO_EASY}:
 619                    self._award_achievement('Pro Runaround Victory',
 620                                            sound=False)
 621                    if self._lives == self._start_lives:
 622                        self._award_achievement('The Wall', sound=False)
 623                    if not self._player_has_picked_up_powerup:
 624                        self._award_achievement('Precision Bombing',
 625                                                sound=False)
 626                elif self._preset in {Preset.UBER, Preset.UBER_EASY}:
 627                    self._award_achievement('Uber Runaround Victory',
 628                                            sound=False)
 629                    if self._lives == self._start_lives:
 630                        self._award_achievement('The Great Wall', sound=False)
 631                    if not self._a_player_has_been_killed:
 632                        self._award_achievement('Stayin\' Alive', sound=False)
 633
 634                # Give remaining players some points and have them celebrate.
 635                self.show_zoom_message(ba.Lstr(resource='victoryText'),
 636                                       scale=1.0,
 637                                       duration=4.0)
 638
 639                self.celebrate(10.0)
 640                ba.timer(base_delay, self._award_lives_bonus)
 641                base_delay += 1.0
 642                ba.timer(base_delay, self._award_completion_bonus)
 643                base_delay += 0.85
 644                ba.playsound(self._winsound)
 645                ba.cameraflash()
 646                ba.setmusic(ba.MusicType.VICTORY)
 647                self._game_over = True
 648                ba.timer(base_delay, ba.Call(self.do_end, 'victory'))
 649                return
 650
 651            self._wavenum += 1
 652
 653            # Short celebration after waves.
 654            if self._wavenum > 1:
 655                self.celebrate(0.5)
 656
 657            ba.timer(base_delay, self._start_next_wave)
 658
 659    def _award_completion_bonus(self) -> None:
 660        bonus = 200
 661        ba.playsound(self._cashregistersound)
 662        PopupText(ba.Lstr(value='+${A} ${B}',
 663                          subs=[('${A}', str(bonus)),
 664                                ('${B}',
 665                                 ba.Lstr(resource='completionBonusText'))]),
 666                  color=(0.7, 0.7, 1.0, 1),
 667                  scale=1.6,
 668                  position=(0, 1.5, -1)).autoretain()
 669        self._score += bonus
 670        self._update_scores()
 671
 672    def _award_lives_bonus(self) -> None:
 673        bonus = self._lives * 30
 674        ba.playsound(self._cashregistersound)
 675        PopupText(ba.Lstr(value='+${A} ${B}',
 676                          subs=[('${A}', str(bonus)),
 677                                ('${B}', ba.Lstr(resource='livesBonusText'))]),
 678                  color=(0.7, 1.0, 0.3, 1),
 679                  scale=1.3,
 680                  position=(0, 1, -1)).autoretain()
 681        self._score += bonus
 682        self._update_scores()
 683
 684    def _award_time_bonus(self, bonus: int) -> None:
 685        ba.playsound(self._cashregistersound)
 686        PopupText(ba.Lstr(value='+${A} ${B}',
 687                          subs=[('${A}', str(bonus)),
 688                                ('${B}', ba.Lstr(resource='timeBonusText'))]),
 689                  color=(1, 1, 0.5, 1),
 690                  scale=1.0,
 691                  position=(0, 3, -1)).autoretain()
 692
 693        self._score += self._time_bonus
 694        self._update_scores()
 695
 696    def _award_flawless_bonus(self) -> None:
 697        ba.playsound(self._cashregistersound)
 698        PopupText(ba.Lstr(value='+${A} ${B}',
 699                          subs=[('${A}', str(self._flawless_bonus)),
 700                                ('${B}', ba.Lstr(resource='perfectWaveText'))
 701                                ]),
 702                  color=(1, 1, 0.2, 1),
 703                  scale=1.2,
 704                  position=(0, 2, -1)).autoretain()
 705
 706        assert self._flawless_bonus is not None
 707        self._score += self._flawless_bonus
 708        self._update_scores()
 709
 710    def _start_time_bonus_timer(self) -> None:
 711        self._time_bonus_timer = ba.Timer(1.0,
 712                                          self._update_time_bonus,
 713                                          repeat=True)
 714
 715    def _start_next_wave(self) -> None:
 716        # FIXME: Need to split this up.
 717        # pylint: disable=too-many-locals
 718        # pylint: disable=too-many-branches
 719        # pylint: disable=too-many-statements
 720        self.show_zoom_message(ba.Lstr(value='${A} ${B}',
 721                                       subs=[('${A}',
 722                                              ba.Lstr(resource='waveText')),
 723                                             ('${B}', str(self._wavenum))]),
 724                               scale=1.0,
 725                               duration=1.0,
 726                               trail=True)
 727        ba.timer(0.4, ba.Call(ba.playsound, self._new_wave_sound))
 728        t_sec = 0.0
 729        base_delay = 0.5
 730        delay = 0.0
 731        bot_types: list[Spawn | Spacing | None] = []
 732
 733        if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}:
 734            level = self._wavenum
 735            target_points = (level + 1) * 8.0
 736            group_count = random.randint(1, 3)
 737            entries: list[Spawn | Spacing | None] = []
 738            spaz_types: list[tuple[type[SpazBot], float]] = []
 739            if level < 6:
 740                spaz_types += [(BomberBot, 5.0)]
 741            if level < 10:
 742                spaz_types += [(BrawlerBot, 5.0)]
 743            if level < 15:
 744                spaz_types += [(TriggerBot, 6.0)]
 745            if level > 5:
 746                spaz_types += [(TriggerBotPro, 7.5)] * (1 + (level - 5) // 7)
 747            if level > 2:
 748                spaz_types += [(BomberBotProShielded, 8.0)
 749                               ] * (1 + (level - 2) // 6)
 750            if level > 6:
 751                spaz_types += [(TriggerBotProShielded, 12.0)
 752                               ] * (1 + (level - 6) // 5)
 753            if level > 1:
 754                spaz_types += ([(ChargerBot, 10.0)] * (1 + (level - 1) // 4))
 755            if level > 7:
 756                spaz_types += [(ChargerBotProShielded, 15.0)
 757                               ] * (1 + (level - 7) // 3)
 758
 759            # Bot type, their effect on target points.
 760            defender_types: list[tuple[type[SpazBot], float]] = [
 761                (BomberBot, 0.9),
 762                (BrawlerBot, 0.9),
 763                (TriggerBot, 0.85),
 764            ]
 765            if level > 2:
 766                defender_types += [(ChargerBot, 0.75)]
 767            if level > 4:
 768                defender_types += ([(StickyBot, 0.7)] * (1 + (level - 5) // 6))
 769            if level > 6:
 770                defender_types += ([(ExplodeyBot, 0.7)] * (1 +
 771                                                           (level - 5) // 5))
 772            if level > 8:
 773                defender_types += ([(BrawlerBotProShielded, 0.65)] *
 774                                   (1 + (level - 5) // 4))
 775            if level > 10:
 776                defender_types += ([(TriggerBotProShielded, 0.6)] *
 777                                   (1 + (level - 6) // 3))
 778
 779            for group in range(group_count):
 780                this_target_point_s = target_points / group_count
 781
 782                # Adding spacing makes things slightly harder.
 783                rval = random.random()
 784                if rval < 0.07:
 785                    spacing = 1.5
 786                    this_target_point_s *= 0.85
 787                elif rval < 0.15:
 788                    spacing = 1.0
 789                    this_target_point_s *= 0.9
 790                else:
 791                    spacing = 0.0
 792
 793                path = random.randint(1, 3)
 794
 795                # Don't allow hard paths on early levels.
 796                if level < 3:
 797                    if path == 1:
 798                        path = 3
 799
 800                # Easy path.
 801                if path == 3:
 802                    pass
 803
 804                # Harder path.
 805                elif path == 2:
 806                    this_target_point_s *= 0.8
 807
 808                # Even harder path.
 809                elif path == 1:
 810                    this_target_point_s *= 0.7
 811
 812                # Looping forward.
 813                elif path == 4:
 814                    this_target_point_s *= 0.7
 815
 816                # Looping backward.
 817                elif path == 5:
 818                    this_target_point_s *= 0.7
 819
 820                # Random.
 821                elif path == 6:
 822                    this_target_point_s *= 0.7
 823
 824                def _add_defender(defender_type: tuple[type[SpazBot], float],
 825                                  pnt: Point) -> tuple[float, Spawn]:
 826                    # This is ok because we call it immediately.
 827                    # pylint: disable=cell-var-from-loop
 828                    return this_target_point_s * defender_type[1], Spawn(
 829                        defender_type[0], point=pnt)
 830
 831                # Add defenders.
 832                defender_type1 = defender_types[random.randrange(
 833                    len(defender_types))]
 834                defender_type2 = defender_types[random.randrange(
 835                    len(defender_types))]
 836                defender1 = defender2 = None
 837                if ((group == 0) or (group == 1 and level > 3)
 838                        or (group == 2 and level > 5)):
 839                    if random.random() < min(0.75, (level - 1) * 0.11):
 840                        this_target_point_s, defender1 = _add_defender(
 841                            defender_type1, Point.BOTTOM_LEFT)
 842                    if random.random() < min(0.75, (level - 1) * 0.04):
 843                        this_target_point_s, defender2 = _add_defender(
 844                            defender_type2, Point.BOTTOM_RIGHT)
 845
 846                spaz_type = spaz_types[random.randrange(len(spaz_types))]
 847                member_count = max(
 848                    1, int(round(this_target_point_s / spaz_type[1])))
 849                for i, _member in enumerate(range(member_count)):
 850                    if path == 4:
 851                        this_path = i % 3  # Looping forward.
 852                    elif path == 5:
 853                        this_path = 3 - (i % 3)  # Looping backward.
 854                    elif path == 6:
 855                        this_path = random.randint(1, 3)  # Random.
 856                    else:
 857                        this_path = path
 858                    entries.append(Spawn(spaz_type[0], path=this_path))
 859                    if spacing != 0.0:
 860                        entries.append(Spacing(duration=spacing))
 861
 862                if defender1 is not None:
 863                    entries.append(defender1)
 864                if defender2 is not None:
 865                    entries.append(defender2)
 866
 867                # Some spacing between groups.
 868                rval = random.random()
 869                if rval < 0.1:
 870                    spacing = 5.0
 871                elif rval < 0.5:
 872                    spacing = 1.0
 873                else:
 874                    spacing = 1.0
 875                entries.append(Spacing(duration=spacing))
 876
 877            wave = Wave(entries=entries)
 878
 879        else:
 880            assert self._waves is not None
 881            wave = self._waves[self._wavenum - 1]
 882
 883        bot_types += wave.entries
 884        self._time_bonus_mult = 1.0
 885        this_flawless_bonus = 0
 886        non_runner_spawn_time = 1.0
 887
 888        for info in bot_types:
 889            if info is None:
 890                continue
 891            if isinstance(info, Spacing):
 892                t_sec += info.duration
 893                continue
 894            bot_type = info.type
 895            path = info.path
 896            self._time_bonus_mult += bot_type.points_mult * 0.02
 897            this_flawless_bonus += bot_type.points_mult * 5
 898
 899            # If its got a position, use that.
 900            if info.point is not None:
 901                point = info.point
 902            else:
 903                point = Point.START
 904
 905            # Space our our slower bots.
 906            delay = base_delay
 907            delay /= self._get_bot_speed(bot_type)
 908            t_sec += delay * 0.5
 909            tcall = ba.Call(
 910                self.add_bot_at_point, point, bot_type, path,
 911                0.1 if point is Point.START else non_runner_spawn_time)
 912            ba.timer(t_sec, tcall)
 913            t_sec += delay * 0.5
 914
 915        # We can end the wave after all the spawning happens.
 916        ba.timer(t_sec - delay * 0.5 + non_runner_spawn_time + 0.01,
 917                 self._set_can_end_wave)
 918
 919        # Reset our time bonus.
 920        # In this game we use a constant time bonus so it erodes away in
 921        # roughly the same time (since the time limit a wave can take is
 922        # relatively constant) ..we then post-multiply a modifier to adjust
 923        # points.
 924        self._time_bonus = 150
 925        self._flawless_bonus = this_flawless_bonus
 926        assert self._time_bonus_mult is not None
 927        txtval = ba.Lstr(
 928            value='${A}: ${B}',
 929            subs=[('${A}', ba.Lstr(resource='timeBonusText')),
 930                  ('${B}', str(int(self._time_bonus * self._time_bonus_mult)))
 931                  ])
 932        self._time_bonus_text = ba.NodeActor(
 933            ba.newnode('text',
 934                       attrs={
 935                           'v_attach': 'top',
 936                           'h_attach': 'center',
 937                           'h_align': 'center',
 938                           'color': (1, 1, 0.0, 1),
 939                           'shadow': 1.0,
 940                           'vr_depth': -30,
 941                           'flatness': 1.0,
 942                           'position': (0, -60),
 943                           'scale': 0.8,
 944                           'text': txtval
 945                       }))
 946
 947        ba.timer(t_sec, self._start_time_bonus_timer)
 948
 949        # Keep track of when this wave finishes emerging. We wanna stop
 950        # dropping land-mines powerups at some point (otherwise a crafty
 951        # player could fill the whole map with them)
 952        self._last_wave_end_time = ba.time() + t_sec
 953        totalwaves = str(len(self._waves)) if self._waves is not None else '??'
 954        txtval = ba.Lstr(value='${A} ${B}',
 955                         subs=[('${A}', ba.Lstr(resource='waveText')),
 956                               ('${B}',
 957                                str(self._wavenum) + ('' if self._preset in {
 958                                    Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT
 959                                } else f'/{totalwaves}'))])
 960        self._wave_text = ba.NodeActor(
 961            ba.newnode('text',
 962                       attrs={
 963                           'v_attach': 'top',
 964                           'h_attach': 'center',
 965                           'h_align': 'center',
 966                           'vr_depth': -10,
 967                           'color': (1, 1, 1, 1),
 968                           'shadow': 1.0,
 969                           'flatness': 1.0,
 970                           'position': (0, -40),
 971                           'scale': 1.3,
 972                           'text': txtval
 973                       }))
 974
 975    def _on_bot_spawn(self, path: int, spaz: SpazBot) -> None:
 976
 977        # Add our custom update callback and set some info for this bot.
 978        spaz_type = type(spaz)
 979        assert spaz is not None
 980        spaz.update_callback = self._update_bot
 981
 982        # Tack some custom attrs onto the spaz.
 983        setattr(spaz, 'r_walk_row', path)
 984        setattr(spaz, 'r_walk_speed', self._get_bot_speed(spaz_type))
 985
 986    def add_bot_at_point(self,
 987                         point: Point,
 988                         spaztype: type[SpazBot],
 989                         path: int,
 990                         spawn_time: float = 0.1) -> None:
 991        """Add the given type bot with the given delay (in seconds)."""
 992
 993        # Don't add if the game has ended.
 994        if self._game_over:
 995            return
 996        pos = self.map.defs.points[point.value][:3]
 997        self._bots.spawn_bot(spaztype,
 998                             pos=pos,
 999                             spawn_time=spawn_time,
1000                             on_spawn_call=ba.Call(self._on_bot_spawn, path))
1001
1002    def _update_time_bonus(self) -> None:
1003        self._time_bonus = int(self._time_bonus * 0.91)
1004        if self._time_bonus > 0 and self._time_bonus_text is not None:
1005            assert self._time_bonus_text.node
1006            assert self._time_bonus_mult
1007            self._time_bonus_text.node.text = ba.Lstr(
1008                value='${A}: ${B}',
1009                subs=[('${A}', ba.Lstr(resource='timeBonusText')),
1010                      ('${B}',
1011                       str(int(self._time_bonus * self._time_bonus_mult)))])
1012        else:
1013            self._time_bonus_text = None
1014
1015    def _start_updating_waves(self) -> None:
1016        self._wave_update_timer = ba.Timer(2.0,
1017                                           self._update_waves,
1018                                           repeat=True)
1019
1020    def _update_scores(self) -> None:
1021        score = self._score
1022        if self._preset is Preset.ENDLESS:
1023            if score >= 500:
1024                self._award_achievement('Runaround Master')
1025            if score >= 1000:
1026                self._award_achievement('Runaround Wizard')
1027            if score >= 2000:
1028                self._award_achievement('Runaround God')
1029
1030        assert self._scoreboard is not None
1031        self._scoreboard.set_team_value(self.teams[0], score, max_score=None)
1032
1033    def _update_bot(self, bot: SpazBot) -> bool:
1034        # Yup; that's a lot of return statements right there.
1035        # pylint: disable=too-many-return-statements
1036
1037        if not bool(bot):
1038            return True
1039
1040        assert bot.node
1041
1042        # FIXME: Do this in a type safe way.
1043        r_walk_speed: float = getattr(bot, 'r_walk_speed')
1044        r_walk_row: int = getattr(bot, 'r_walk_row')
1045
1046        speed = r_walk_speed
1047        pos = bot.node.position
1048        boxes = self.map.defs.boxes
1049
1050        # Bots in row 1 attempt the high road..
1051        if r_walk_row == 1:
1052            if ba.is_point_in_box(pos, boxes['b4']):
1053                bot.node.move_up_down = speed
1054                bot.node.move_left_right = 0
1055                bot.node.run = 0.0
1056                return True
1057
1058        # Row 1 and 2 bots attempt the middle road..
1059        if r_walk_row in [1, 2]:
1060            if ba.is_point_in_box(pos, boxes['b1']):
1061                bot.node.move_up_down = speed
1062                bot.node.move_left_right = 0
1063                bot.node.run = 0.0
1064                return True
1065
1066        # All bots settle for the third row.
1067        if ba.is_point_in_box(pos, boxes['b7']):
1068            bot.node.move_up_down = speed
1069            bot.node.move_left_right = 0
1070            bot.node.run = 0.0
1071            return True
1072        if ba.is_point_in_box(pos, boxes['b2']):
1073            bot.node.move_up_down = -speed
1074            bot.node.move_left_right = 0
1075            bot.node.run = 0.0
1076            return True
1077        if ba.is_point_in_box(pos, boxes['b3']):
1078            bot.node.move_up_down = -speed
1079            bot.node.move_left_right = 0
1080            bot.node.run = 0.0
1081            return True
1082        if ba.is_point_in_box(pos, boxes['b5']):
1083            bot.node.move_up_down = -speed
1084            bot.node.move_left_right = 0
1085            bot.node.run = 0.0
1086            return True
1087        if ba.is_point_in_box(pos, boxes['b6']):
1088            bot.node.move_up_down = speed
1089            bot.node.move_left_right = 0
1090            bot.node.run = 0.0
1091            return True
1092        if ((ba.is_point_in_box(pos, boxes['b8'])
1093             and not ba.is_point_in_box(pos, boxes['b9']))
1094                or pos == (0.0, 0.0, 0.0)):
1095
1096            # Default to walking right if we're still in the walking area.
1097            bot.node.move_left_right = speed
1098            bot.node.move_up_down = 0
1099            bot.node.run = 0.0
1100            return True
1101
1102        # Revert to normal bot behavior otherwise..
1103        return False
1104
1105    def handlemessage(self, msg: Any) -> Any:
1106        if isinstance(msg, ba.PlayerScoredMessage):
1107            self._score += msg.score
1108            self._update_scores()
1109
1110        elif isinstance(msg, ba.PlayerDiedMessage):
1111            # Augment standard behavior.
1112            super().handlemessage(msg)
1113
1114            self._a_player_has_been_killed = True
1115
1116            # Respawn them shortly.
1117            player = msg.getplayer(Player)
1118            assert self.initialplayerinfos is not None
1119            respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
1120            player.respawn_timer = ba.Timer(
1121                respawn_time, ba.Call(self.spawn_player_if_exists, player))
1122            player.respawn_icon = RespawnIcon(player, respawn_time)
1123
1124        elif isinstance(msg, SpazBotDiedMessage):
1125            if msg.how is ba.DeathType.REACHED_GOAL:
1126                return None
1127            pts, importance = msg.spazbot.get_death_points(msg.how)
1128            if msg.killerplayer is not None:
1129                target: Sequence[float] | None
1130                try:
1131                    assert msg.spazbot is not None
1132                    assert msg.spazbot.node
1133                    target = msg.spazbot.node.position
1134                except Exception:
1135                    ba.print_exception()
1136                    target = None
1137                try:
1138                    if msg.killerplayer:
1139                        self.stats.player_scored(msg.killerplayer,
1140                                                 pts,
1141                                                 target=target,
1142                                                 kill=True,
1143                                                 screenmessage=False,
1144                                                 importance=importance)
1145                        ba.playsound(self._dingsound if importance == 1 else
1146                                     self._dingsoundhigh,
1147                                     volume=0.6)
1148                except Exception:
1149                    ba.print_exception('Error on SpazBotDiedMessage.')
1150
1151            # Normally we pull scores from the score-set, but if there's no
1152            # player lets be explicit.
1153            else:
1154                self._score += pts
1155            self._update_scores()
1156
1157        else:
1158            return super().handlemessage(msg)
1159        return None
1160
1161    def _get_bot_speed(self, bot_type: type[SpazBot]) -> float:
1162        speed = self._bot_speed_map.get(bot_type)
1163        if speed is None:
1164            raise TypeError('Invalid bot type to _get_bot_speed(): ' +
1165                            str(bot_type))
1166        return speed
1167
1168    def _set_can_end_wave(self) -> None:
1169        self._can_end_wave = True
class Preset(enum.Enum):
36class Preset(Enum):
37    """Play presets."""
38    ENDLESS = 'endless'
39    ENDLESS_TOURNAMENT = 'endless_tournament'
40    PRO = 'pro'
41    PRO_EASY = 'pro_easy'
42    UBER = 'uber'
43    UBER_EASY = 'uber_easy'
44    TOURNAMENT = 'tournament'
45    TOURNAMENT_UBER = 'tournament_uber'

Play presets.

ENDLESS = <Preset.ENDLESS: 'endless'>
ENDLESS_TOURNAMENT = <Preset.ENDLESS_TOURNAMENT: 'endless_tournament'>
PRO = <Preset.PRO: 'pro'>
PRO_EASY = <Preset.PRO_EASY: 'pro_easy'>
UBER = <Preset.UBER: 'uber'>
UBER_EASY = <Preset.UBER_EASY: 'uber_easy'>
TOURNAMENT = <Preset.TOURNAMENT: 'tournament'>
TOURNAMENT_UBER = <Preset.TOURNAMENT_UBER: 'tournament_uber'>
Inherited Members
enum.Enum
name
value
class Point(enum.Enum):
48class Point(Enum):
49    """Where we can spawn stuff and the corresponding map attr name."""
50    BOTTOM_LEFT = 'bot_spawn_bottom_left'
51    BOTTOM_RIGHT = 'bot_spawn_bottom_right'
52    START = 'bot_spawn_start'

Where we can spawn stuff and the corresponding map attr name.

BOTTOM_LEFT = <Point.BOTTOM_LEFT: 'bot_spawn_bottom_left'>
BOTTOM_RIGHT = <Point.BOTTOM_RIGHT: 'bot_spawn_bottom_right'>
START = <Point.START: 'bot_spawn_start'>
Inherited Members
enum.Enum
name
value
@dataclass
class Spawn:
55@dataclass
56class Spawn:
57    """Defines a bot spawn event."""
58    # noinspection PyUnresolvedReferences
59    type: type[SpazBot]
60    path: int = 0
61    point: Point | None = None

Defines a bot spawn event.

Spawn( type: type[bastd.actor.spazbot.SpazBot], path: int = 0, point: bastd.game.runaround.Point | None = None)
path: int = 0
point: bastd.game.runaround.Point | None = None
@dataclass
class Spacing:
64@dataclass
65class Spacing:
66    """Defines spacing between spawns."""
67    duration: float

Defines spacing between spawns.

Spacing(duration: float)
@dataclass
class Wave:
70@dataclass
71class Wave:
72    """Defines a wave of enemies."""
73    entries: list[Spawn | Spacing | None]

Defines a wave of enemies.

class Player(ba._player.Player[ForwardRef('Team')]):
76class Player(ba.Player['Team']):
77    """Our player type for this game."""
78
79    def __init__(self) -> None:
80        self.respawn_timer: ba.Timer | None = None
81        self.respawn_icon: RespawnIcon | None = None

Our player type for this game.

Player()
79    def __init__(self) -> None:
80        self.respawn_timer: ba.Timer | None = None
81        self.respawn_icon: RespawnIcon | None = None
class Team(ba._team.Team[bastd.game.runaround.Player]):
84class Team(ba.Team[Player]):
85    """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 RunaroundGame(ba._coopgame.CoopGameActivity[bastd.game.runaround.Player, bastd.game.runaround.Team]):
  88class RunaroundGame(ba.CoopGameActivity[Player, Team]):
  89    """Game involving trying to bomb bots as they walk through the map."""
  90
  91    name = 'Runaround'
  92    description = 'Prevent enemies from reaching the exit.'
  93    tips = [
  94        'Jump just as you\'re throwing to get bombs up to the highest levels.',
  95        'No, you can\'t get up on the ledge. You have to throw bombs.',
  96        'Whip back and forth to get more distance on your throws..'
  97    ]
  98    default_music = ba.MusicType.MARCHING
  99
 100    # How fast our various bot types walk.
 101    _bot_speed_map: dict[type[SpazBot], float] = {
 102        BomberBot: 0.48,
 103        BomberBotPro: 0.48,
 104        BomberBotProShielded: 0.48,
 105        BrawlerBot: 0.57,
 106        BrawlerBotPro: 0.57,
 107        BrawlerBotProShielded: 0.57,
 108        TriggerBot: 0.73,
 109        TriggerBotPro: 0.78,
 110        TriggerBotProShielded: 0.78,
 111        ChargerBot: 1.0,
 112        ChargerBotProShielded: 1.0,
 113        ExplodeyBot: 1.0,
 114        StickyBot: 0.5
 115    }
 116
 117    def __init__(self, settings: dict):
 118        settings['map'] = 'Tower D'
 119        super().__init__(settings)
 120        shared = SharedObjects.get()
 121        self._preset = Preset(settings.get('preset', 'pro'))
 122
 123        self._player_death_sound = ba.getsound('playerDeath')
 124        self._new_wave_sound = ba.getsound('scoreHit01')
 125        self._winsound = ba.getsound('score')
 126        self._cashregistersound = ba.getsound('cashRegister')
 127        self._bad_guy_score_sound = ba.getsound('shieldDown')
 128        self._heart_tex = ba.gettexture('heart')
 129        self._heart_model_opaque = ba.getmodel('heartOpaque')
 130        self._heart_model_transparent = ba.getmodel('heartTransparent')
 131
 132        self._a_player_has_been_killed = False
 133        self._spawn_center = self._map_type.defs.points['spawn1'][0:3]
 134        self._tntspawnpos = self._map_type.defs.points['tnt_loc'][0:3]
 135        self._powerup_center = self._map_type.defs.boxes['powerup_region'][0:3]
 136        self._powerup_spread = (
 137            self._map_type.defs.boxes['powerup_region'][6] * 0.5,
 138            self._map_type.defs.boxes['powerup_region'][8] * 0.5)
 139
 140        self._score_region_material = ba.Material()
 141        self._score_region_material.add_actions(
 142            conditions=('they_have_material', shared.player_material),
 143            actions=(
 144                ('modify_part_collision', 'collide', True),
 145                ('modify_part_collision', 'physical', False),
 146                ('call', 'at_connect', self._handle_reached_end),
 147            ))
 148
 149        self._last_wave_end_time = ba.time()
 150        self._player_has_picked_up_powerup = False
 151        self._scoreboard: Scoreboard | None = None
 152        self._game_over = False
 153        self._wavenum = 0
 154        self._can_end_wave = True
 155        self._score = 0
 156        self._time_bonus = 0
 157        self._score_region: ba.Actor | None = None
 158        self._dingsound = ba.getsound('dingSmall')
 159        self._dingsoundhigh = ba.getsound('dingSmallHigh')
 160        self._exclude_powerups: list[str] | None = None
 161        self._have_tnt: bool | None = None
 162        self._waves: list[Wave] | None = None
 163        self._bots = SpazBotSet()
 164        self._tntspawner: TNTSpawner | None = None
 165        self._lives_bg: ba.NodeActor | None = None
 166        self._start_lives = 10
 167        self._lives = self._start_lives
 168        self._lives_text: ba.NodeActor | None = None
 169        self._flawless = True
 170        self._time_bonus_timer: ba.Timer | None = None
 171        self._time_bonus_text: ba.NodeActor | None = None
 172        self._time_bonus_mult: float | None = None
 173        self._wave_text: ba.NodeActor | None = None
 174        self._flawless_bonus: int | None = None
 175        self._wave_update_timer: ba.Timer | None = None
 176
 177    def on_transition_in(self) -> None:
 178        super().on_transition_in()
 179        self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'),
 180                                      score_split=0.5)
 181        self._score_region = ba.NodeActor(
 182            ba.newnode(
 183                'region',
 184                attrs={
 185                    'position': self.map.defs.boxes['score_region'][0:3],
 186                    'scale': self.map.defs.boxes['score_region'][6:9],
 187                    'type': 'box',
 188                    'materials': [self._score_region_material]
 189                }))
 190
 191    def on_begin(self) -> None:
 192        super().on_begin()
 193        player_count = len(self.players)
 194        hard = self._preset not in {Preset.PRO_EASY, Preset.UBER_EASY}
 195
 196        if self._preset in {Preset.PRO, Preset.PRO_EASY, Preset.TOURNAMENT}:
 197            self._exclude_powerups = ['curse']
 198            self._have_tnt = True
 199            self._waves = [
 200                Wave(entries=[
 201                    Spawn(BomberBot, path=3 if hard else 2),
 202                    Spawn(BomberBot, path=2),
 203                    Spawn(BomberBot, path=2) if hard else None,
 204                    Spawn(BomberBot, path=2) if player_count > 1 else None,
 205                    Spawn(BomberBot, path=1) if hard else None,
 206                    Spawn(BomberBot, path=1) if player_count > 2 else None,
 207                    Spawn(BomberBot, path=1) if player_count > 3 else None,
 208                ]),
 209                Wave(entries=[
 210                    Spawn(BomberBot, path=1) if hard else None,
 211                    Spawn(BomberBot, path=2) if hard else None,
 212                    Spawn(BomberBot, path=2),
 213                    Spawn(BomberBot, path=2),
 214                    Spawn(BomberBot, path=2) if player_count > 3 else None,
 215                    Spawn(BrawlerBot, path=3),
 216                    Spawn(BrawlerBot, path=3),
 217                    Spawn(BrawlerBot, path=3) if hard else None,
 218                    Spawn(BrawlerBot, path=3) if player_count > 1 else None,
 219                    Spawn(BrawlerBot, path=3) if player_count > 2 else None,
 220                ]),
 221                Wave(entries=[
 222                    Spawn(ChargerBot, path=2) if hard else None,
 223                    Spawn(ChargerBot, path=2) if player_count > 2 else None,
 224                    Spawn(TriggerBot, path=2),
 225                    Spawn(TriggerBot, path=2) if player_count > 1 else None,
 226                    Spacing(duration=3.0),
 227                    Spawn(BomberBot, path=2) if hard else None,
 228                    Spawn(BomberBot, path=2) if hard else None,
 229                    Spawn(BomberBot, path=2),
 230                    Spawn(BomberBot, path=3) if hard else None,
 231                    Spawn(BomberBot, path=3),
 232                    Spawn(BomberBot, path=3),
 233                    Spawn(BomberBot, path=3) if player_count > 3 else None,
 234                ]),
 235                Wave(entries=[
 236                    Spawn(TriggerBot, path=1) if hard else None,
 237                    Spacing(duration=1.0) if hard else None,
 238                    Spawn(TriggerBot, path=2),
 239                    Spacing(duration=1.0),
 240                    Spawn(TriggerBot, path=3),
 241                    Spacing(duration=1.0),
 242                    Spawn(TriggerBot, path=1) if hard else None,
 243                    Spacing(duration=1.0) if hard else None,
 244                    Spawn(TriggerBot, path=2),
 245                    Spacing(duration=1.0),
 246                    Spawn(TriggerBot, path=3),
 247                    Spacing(duration=1.0),
 248                    Spawn(TriggerBot, path=1) if (
 249                        player_count > 1 and hard) else None,
 250                    Spacing(duration=1.0),
 251                    Spawn(TriggerBot, path=2) if player_count > 2 else None,
 252                    Spacing(duration=1.0),
 253                    Spawn(TriggerBot, path=3) if player_count > 3 else None,
 254                    Spacing(duration=1.0),
 255                ]),
 256                Wave(entries=[
 257                    Spawn(ChargerBotProShielded if hard else ChargerBot,
 258                          path=1),
 259                    Spawn(BrawlerBot, path=2) if hard else None,
 260                    Spawn(BrawlerBot, path=2),
 261                    Spawn(BrawlerBot, path=2),
 262                    Spawn(BrawlerBot, path=3) if hard else None,
 263                    Spawn(BrawlerBot, path=3),
 264                    Spawn(BrawlerBot, path=3),
 265                    Spawn(BrawlerBot, path=3) if player_count > 1 else None,
 266                    Spawn(BrawlerBot, path=3) if player_count > 2 else None,
 267                    Spawn(BrawlerBot, path=3) if player_count > 3 else None,
 268                ]),
 269                Wave(entries=[
 270                    Spawn(BomberBotProShielded, path=3),
 271                    Spacing(duration=1.5),
 272                    Spawn(BomberBotProShielded, path=2),
 273                    Spacing(duration=1.5),
 274                    Spawn(BomberBotProShielded, path=1) if hard else None,
 275                    Spacing(duration=1.0) if hard else None,
 276                    Spawn(BomberBotProShielded, path=3),
 277                    Spacing(duration=1.5),
 278                    Spawn(BomberBotProShielded, path=2),
 279                    Spacing(duration=1.5),
 280                    Spawn(BomberBotProShielded, path=1) if hard else None,
 281                    Spacing(duration=1.5) if hard else None,
 282                    Spawn(BomberBotProShielded, path=3
 283                          ) if player_count > 1 else None,
 284                    Spacing(duration=1.5),
 285                    Spawn(BomberBotProShielded, path=2
 286                          ) if player_count > 2 else None,
 287                    Spacing(duration=1.5),
 288                    Spawn(BomberBotProShielded, path=1
 289                          ) if player_count > 3 else None,
 290                ]),
 291            ]
 292        elif self._preset in {
 293                Preset.UBER_EASY, Preset.UBER, Preset.TOURNAMENT_UBER
 294        }:
 295            self._exclude_powerups = []
 296            self._have_tnt = True
 297            self._waves = [
 298                Wave(entries=[
 299                    Spawn(TriggerBot, path=1) if hard else None,
 300                    Spawn(TriggerBot, path=2),
 301                    Spawn(TriggerBot, path=2),
 302                    Spawn(TriggerBot, path=3),
 303                    Spawn(BrawlerBotPro if hard else BrawlerBot,
 304                          point=Point.BOTTOM_LEFT),
 305                    Spawn(BrawlerBotPro, point=Point.BOTTOM_RIGHT
 306                          ) if player_count > 2 else None,
 307                ]),
 308                Wave(entries=[
 309                    Spawn(ChargerBot, path=2),
 310                    Spawn(ChargerBot, path=3),
 311                    Spawn(ChargerBot, path=1) if hard else None,
 312                    Spawn(ChargerBot, path=2),
 313                    Spawn(ChargerBot, path=3),
 314                    Spawn(ChargerBot, path=1) if player_count > 2 else None,
 315                ]),
 316                Wave(entries=[
 317                    Spawn(BomberBotProShielded, path=1) if hard else None,
 318                    Spawn(BomberBotProShielded, path=2),
 319                    Spawn(BomberBotProShielded, path=2),
 320                    Spawn(BomberBotProShielded, path=3),
 321                    Spawn(BomberBotProShielded, path=3),
 322                    Spawn(ChargerBot, point=Point.BOTTOM_RIGHT),
 323                    Spawn(ChargerBot, point=Point.BOTTOM_LEFT
 324                          ) if player_count > 2 else None,
 325                ]),
 326                Wave(entries=[
 327                    Spawn(TriggerBotPro, path=1) if hard else None,
 328                    Spawn(TriggerBotPro, path=1 if hard else 2),
 329                    Spawn(TriggerBotPro, path=1 if hard else 2),
 330                    Spawn(TriggerBotPro, path=1 if hard else 2),
 331                    Spawn(TriggerBotPro, path=1 if hard else 2),
 332                    Spawn(TriggerBotPro, path=1 if hard else 2),
 333                    Spawn(TriggerBotPro, path=1 if hard else 2
 334                          ) if player_count > 1 else None,
 335                    Spawn(TriggerBotPro, path=1 if hard else 2
 336                          ) if player_count > 3 else None,
 337                ]),
 338                Wave(entries=[
 339                    Spawn(TriggerBotProShielded if hard else TriggerBotPro,
 340                          point=Point.BOTTOM_LEFT),
 341                    Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT
 342                          ) if hard else None,
 343                    Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT
 344                          ) if player_count > 2 else None,
 345                    Spawn(BomberBot, path=3),
 346                    Spawn(BomberBot, path=3),
 347                    Spacing(duration=5.0),
 348                    Spawn(BrawlerBot, path=2),
 349                    Spawn(BrawlerBot, path=2),
 350                    Spacing(duration=5.0),
 351                    Spawn(TriggerBot, path=1) if hard else None,
 352                    Spawn(TriggerBot, path=1) if hard else None,
 353                ]),
 354                Wave(entries=[
 355                    Spawn(BomberBotProShielded, path=2),
 356                    Spawn(BomberBotProShielded, path=2) if hard else None,
 357                    Spawn(StickyBot, point=Point.BOTTOM_RIGHT),
 358                    Spawn(BomberBotProShielded, path=2),
 359                    Spawn(BomberBotProShielded, path=2),
 360                    Spawn(StickyBot, point=Point.BOTTOM_RIGHT
 361                          ) if player_count > 2 else None,
 362                    Spawn(BomberBotProShielded, path=2),
 363                    Spawn(ExplodeyBot, point=Point.BOTTOM_LEFT),
 364                    Spawn(BomberBotProShielded, path=2),
 365                    Spawn(BomberBotProShielded, path=2
 366                          ) if player_count > 1 else None,
 367                    Spacing(duration=5.0),
 368                    Spawn(StickyBot, point=Point.BOTTOM_LEFT),
 369                    Spacing(duration=2.0),
 370                    Spawn(ExplodeyBot, point=Point.BOTTOM_RIGHT),
 371                ]),
 372            ]
 373        elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}:
 374            self._exclude_powerups = []
 375            self._have_tnt = True
 376
 377        # Spit out a few powerups and start dropping more shortly.
 378        self._drop_powerups(standard_points=True)
 379        ba.timer(4.0, self._start_powerup_drops)
 380        self.setup_low_life_warning_sound()
 381        self._update_scores()
 382
 383        # Our TNT spawner (if applicable).
 384        if self._have_tnt:
 385            self._tntspawner = TNTSpawner(position=self._tntspawnpos)
 386
 387        # Make sure to stay out of the way of menu/party buttons in the corner.
 388        uiscale = ba.app.ui.uiscale
 389        l_offs = (-80 if uiscale is ba.UIScale.SMALL else
 390                  -40 if uiscale is ba.UIScale.MEDIUM else 0)
 391
 392        self._lives_bg = ba.NodeActor(
 393            ba.newnode('image',
 394                       attrs={
 395                           'texture': self._heart_tex,
 396                           'model_opaque': self._heart_model_opaque,
 397                           'model_transparent': self._heart_model_transparent,
 398                           'attach': 'topRight',
 399                           'scale': (90, 90),
 400                           'position': (-110 + l_offs, -50),
 401                           'color': (1, 0.2, 0.2)
 402                       }))
 403        # FIXME; should not set things based on vr mode.
 404        #  (won't look right to non-vr connected clients, etc)
 405        vrmode = ba.app.vr_mode
 406        self._lives_text = ba.NodeActor(
 407            ba.newnode(
 408                'text',
 409                attrs={
 410                    'v_attach': 'top',
 411                    'h_attach': 'right',
 412                    'h_align': 'center',
 413                    'color': (1, 1, 1, 1) if vrmode else (0.8, 0.8, 0.8, 1.0),
 414                    'flatness': 1.0 if vrmode else 0.5,
 415                    'shadow': 1.0 if vrmode else 0.5,
 416                    'vr_depth': 10,
 417                    'position': (-113 + l_offs, -69),
 418                    'scale': 1.3,
 419                    'text': str(self._lives)
 420                }))
 421
 422        ba.timer(2.0, self._start_updating_waves)
 423
 424    def _handle_reached_end(self) -> None:
 425        spaz = ba.getcollision().opposingnode.getdelegate(SpazBot, True)
 426        if not spaz.is_alive():
 427            return  # Ignore bodies flying in.
 428
 429        self._flawless = False
 430        pos = spaz.node.position
 431        ba.playsound(self._bad_guy_score_sound, position=pos)
 432        light = ba.newnode('light',
 433                           attrs={
 434                               'position': pos,
 435                               'radius': 0.5,
 436                               'color': (1, 0, 0)
 437                           })
 438        ba.animate(light, 'intensity', {0.0: 0, 0.1: 1, 0.5: 0}, loop=False)
 439        ba.timer(1.0, light.delete)
 440        spaz.handlemessage(
 441            ba.DieMessage(immediate=True, how=ba.DeathType.REACHED_GOAL))
 442
 443        if self._lives > 0:
 444            self._lives -= 1
 445            if self._lives == 0:
 446                self._bots.stop_moving()
 447                self.continue_or_end_game()
 448            assert self._lives_text is not None
 449            assert self._lives_text.node
 450            self._lives_text.node.text = str(self._lives)
 451            delay = 0.0
 452
 453            def _safesetattr(node: ba.Node, attr: str, value: Any) -> None:
 454                if node:
 455                    setattr(node, attr, value)
 456
 457            for _i in range(4):
 458                ba.timer(
 459                    delay,
 460                    ba.Call(_safesetattr, self._lives_text.node, 'color',
 461                            (1, 0, 0, 1.0)))
 462                assert self._lives_bg is not None
 463                assert self._lives_bg.node
 464                ba.timer(
 465                    delay,
 466                    ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 0.5))
 467                delay += 0.125
 468                ba.timer(
 469                    delay,
 470                    ba.Call(_safesetattr, self._lives_text.node, 'color',
 471                            (1.0, 1.0, 0.0, 1.0)))
 472                ba.timer(
 473                    delay,
 474                    ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 1.0))
 475                delay += 0.125
 476            ba.timer(
 477                delay,
 478                ba.Call(_safesetattr, self._lives_text.node, 'color',
 479                        (0.8, 0.8, 0.8, 1.0)))
 480
 481    def on_continue(self) -> None:
 482        self._lives = 3
 483        assert self._lives_text is not None
 484        assert self._lives_text.node
 485        self._lives_text.node.text = str(self._lives)
 486        self._bots.start_moving()
 487
 488    def spawn_player(self, player: Player) -> ba.Actor:
 489        pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5),
 490               self._spawn_center[1],
 491               self._spawn_center[2] + random.uniform(-1.5, 1.5))
 492        spaz = self.spawn_player_spaz(player, position=pos)
 493        if self._preset in {Preset.PRO_EASY, Preset.UBER_EASY}:
 494            spaz.impact_scale = 0.25
 495
 496        # Add the material that causes us to hit the player-wall.
 497        spaz.pick_up_powerup_callback = self._on_player_picked_up_powerup
 498        return spaz
 499
 500    def _on_player_picked_up_powerup(self, player: ba.Actor) -> None:
 501        del player  # Unused.
 502        self._player_has_picked_up_powerup = True
 503
 504    def _drop_powerup(self,
 505                      index: int,
 506                      poweruptype: str | None = None) -> None:
 507        if poweruptype is None:
 508            poweruptype = (PowerupBoxFactory.get().get_random_powerup_type(
 509                excludetypes=self._exclude_powerups))
 510        PowerupBox(position=self.map.powerup_spawn_points[index],
 511                   poweruptype=poweruptype).autoretain()
 512
 513    def _start_powerup_drops(self) -> None:
 514        ba.timer(3.0, self._drop_powerups, repeat=True)
 515
 516    def _drop_powerups(self,
 517                       standard_points: bool = False,
 518                       force_first: str | None = None) -> None:
 519        """Generic powerup drop."""
 520
 521        # If its been a minute since our last wave finished emerging, stop
 522        # giving out land-mine powerups. (prevents players from waiting
 523        # around for them on purpose and filling the map up)
 524        if ba.time() - self._last_wave_end_time > 60.0:
 525            extra_excludes = ['land_mines']
 526        else:
 527            extra_excludes = []
 528
 529        if standard_points:
 530            points = self.map.powerup_spawn_points
 531            for i in range(len(points)):
 532                ba.timer(
 533                    1.0 + i * 0.5,
 534                    ba.Call(self._drop_powerup, i,
 535                            force_first if i == 0 else None))
 536        else:
 537            pos = (self._powerup_center[0] + random.uniform(
 538                -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]),
 539                   self._powerup_center[1],
 540                   self._powerup_center[2] + random.uniform(
 541                       -self._powerup_spread[1], self._powerup_spread[1]))
 542
 543            # drop one random one somewhere..
 544            assert self._exclude_powerups is not None
 545            PowerupBox(
 546                position=pos,
 547                poweruptype=PowerupBoxFactory.get().get_random_powerup_type(
 548                    excludetypes=self._exclude_powerups +
 549                    extra_excludes)).autoretain()
 550
 551    def end_game(self) -> None:
 552        ba.pushcall(ba.Call(self.do_end, 'defeat'))
 553        ba.setmusic(None)
 554        ba.playsound(self._player_death_sound)
 555
 556    def do_end(self, outcome: str) -> None:
 557        """End the game now with the provided outcome."""
 558
 559        if outcome == 'defeat':
 560            delay = 2.0
 561            self.fade_to_red()
 562        else:
 563            delay = 0
 564
 565        score: int | None
 566        if self._wavenum >= 2:
 567            score = self._score
 568            fail_message = None
 569        else:
 570            score = None
 571            fail_message = ba.Lstr(resource='reachWave2Text')
 572
 573        self.end(delay=delay,
 574                 results={
 575                     'outcome': outcome,
 576                     'score': score,
 577                     'fail_message': fail_message,
 578                     'playerinfos': self.initialplayerinfos
 579                 })
 580
 581    def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
 582        self._show_standard_scores_to_beat_ui(scores)
 583
 584    def _update_waves(self) -> None:
 585        # pylint: disable=too-many-branches
 586
 587        # If we have no living bots, go to the next wave.
 588        if (self._can_end_wave and not self._bots.have_living_bots()
 589                and not self._game_over and self._lives > 0):
 590
 591            self._can_end_wave = False
 592            self._time_bonus_timer = None
 593            self._time_bonus_text = None
 594
 595            if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}:
 596                won = False
 597            else:
 598                assert self._waves is not None
 599                won = (self._wavenum == len(self._waves))
 600
 601            # Reward time bonus.
 602            base_delay = 4.0 if won else 0
 603            if self._time_bonus > 0:
 604                ba.timer(0, ba.Call(ba.playsound, self._cashregistersound))
 605                ba.timer(base_delay,
 606                         ba.Call(self._award_time_bonus, self._time_bonus))
 607                base_delay += 1.0
 608
 609            # Reward flawless bonus.
 610            if self._wavenum > 0 and self._flawless:
 611                ba.timer(base_delay, self._award_flawless_bonus)
 612                base_delay += 1.0
 613
 614            self._flawless = True  # reset
 615
 616            if won:
 617
 618                # Completion achievements:
 619                if self._preset in {Preset.PRO, Preset.PRO_EASY}:
 620                    self._award_achievement('Pro Runaround Victory',
 621                                            sound=False)
 622                    if self._lives == self._start_lives:
 623                        self._award_achievement('The Wall', sound=False)
 624                    if not self._player_has_picked_up_powerup:
 625                        self._award_achievement('Precision Bombing',
 626                                                sound=False)
 627                elif self._preset in {Preset.UBER, Preset.UBER_EASY}:
 628                    self._award_achievement('Uber Runaround Victory',
 629                                            sound=False)
 630                    if self._lives == self._start_lives:
 631                        self._award_achievement('The Great Wall', sound=False)
 632                    if not self._a_player_has_been_killed:
 633                        self._award_achievement('Stayin\' Alive', sound=False)
 634
 635                # Give remaining players some points and have them celebrate.
 636                self.show_zoom_message(ba.Lstr(resource='victoryText'),
 637                                       scale=1.0,
 638                                       duration=4.0)
 639
 640                self.celebrate(10.0)
 641                ba.timer(base_delay, self._award_lives_bonus)
 642                base_delay += 1.0
 643                ba.timer(base_delay, self._award_completion_bonus)
 644                base_delay += 0.85
 645                ba.playsound(self._winsound)
 646                ba.cameraflash()
 647                ba.setmusic(ba.MusicType.VICTORY)
 648                self._game_over = True
 649                ba.timer(base_delay, ba.Call(self.do_end, 'victory'))
 650                return
 651
 652            self._wavenum += 1
 653
 654            # Short celebration after waves.
 655            if self._wavenum > 1:
 656                self.celebrate(0.5)
 657
 658            ba.timer(base_delay, self._start_next_wave)
 659
 660    def _award_completion_bonus(self) -> None:
 661        bonus = 200
 662        ba.playsound(self._cashregistersound)
 663        PopupText(ba.Lstr(value='+${A} ${B}',
 664                          subs=[('${A}', str(bonus)),
 665                                ('${B}',
 666                                 ba.Lstr(resource='completionBonusText'))]),
 667                  color=(0.7, 0.7, 1.0, 1),
 668                  scale=1.6,
 669                  position=(0, 1.5, -1)).autoretain()
 670        self._score += bonus
 671        self._update_scores()
 672
 673    def _award_lives_bonus(self) -> None:
 674        bonus = self._lives * 30
 675        ba.playsound(self._cashregistersound)
 676        PopupText(ba.Lstr(value='+${A} ${B}',
 677                          subs=[('${A}', str(bonus)),
 678                                ('${B}', ba.Lstr(resource='livesBonusText'))]),
 679                  color=(0.7, 1.0, 0.3, 1),
 680                  scale=1.3,
 681                  position=(0, 1, -1)).autoretain()
 682        self._score += bonus
 683        self._update_scores()
 684
 685    def _award_time_bonus(self, bonus: int) -> None:
 686        ba.playsound(self._cashregistersound)
 687        PopupText(ba.Lstr(value='+${A} ${B}',
 688                          subs=[('${A}', str(bonus)),
 689                                ('${B}', ba.Lstr(resource='timeBonusText'))]),
 690                  color=(1, 1, 0.5, 1),
 691                  scale=1.0,
 692                  position=(0, 3, -1)).autoretain()
 693
 694        self._score += self._time_bonus
 695        self._update_scores()
 696
 697    def _award_flawless_bonus(self) -> None:
 698        ba.playsound(self._cashregistersound)
 699        PopupText(ba.Lstr(value='+${A} ${B}',
 700                          subs=[('${A}', str(self._flawless_bonus)),
 701                                ('${B}', ba.Lstr(resource='perfectWaveText'))
 702                                ]),
 703                  color=(1, 1, 0.2, 1),
 704                  scale=1.2,
 705                  position=(0, 2, -1)).autoretain()
 706
 707        assert self._flawless_bonus is not None
 708        self._score += self._flawless_bonus
 709        self._update_scores()
 710
 711    def _start_time_bonus_timer(self) -> None:
 712        self._time_bonus_timer = ba.Timer(1.0,
 713                                          self._update_time_bonus,
 714                                          repeat=True)
 715
 716    def _start_next_wave(self) -> None:
 717        # FIXME: Need to split this up.
 718        # pylint: disable=too-many-locals
 719        # pylint: disable=too-many-branches
 720        # pylint: disable=too-many-statements
 721        self.show_zoom_message(ba.Lstr(value='${A} ${B}',
 722                                       subs=[('${A}',
 723                                              ba.Lstr(resource='waveText')),
 724                                             ('${B}', str(self._wavenum))]),
 725                               scale=1.0,
 726                               duration=1.0,
 727                               trail=True)
 728        ba.timer(0.4, ba.Call(ba.playsound, self._new_wave_sound))
 729        t_sec = 0.0
 730        base_delay = 0.5
 731        delay = 0.0
 732        bot_types: list[Spawn | Spacing | None] = []
 733
 734        if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}:
 735            level = self._wavenum
 736            target_points = (level + 1) * 8.0
 737            group_count = random.randint(1, 3)
 738            entries: list[Spawn | Spacing | None] = []
 739            spaz_types: list[tuple[type[SpazBot], float]] = []
 740            if level < 6:
 741                spaz_types += [(BomberBot, 5.0)]
 742            if level < 10:
 743                spaz_types += [(BrawlerBot, 5.0)]
 744            if level < 15:
 745                spaz_types += [(TriggerBot, 6.0)]
 746            if level > 5:
 747                spaz_types += [(TriggerBotPro, 7.5)] * (1 + (level - 5) // 7)
 748            if level > 2:
 749                spaz_types += [(BomberBotProShielded, 8.0)
 750                               ] * (1 + (level - 2) // 6)
 751            if level > 6:
 752                spaz_types += [(TriggerBotProShielded, 12.0)
 753                               ] * (1 + (level - 6) // 5)
 754            if level > 1:
 755                spaz_types += ([(ChargerBot, 10.0)] * (1 + (level - 1) // 4))
 756            if level > 7:
 757                spaz_types += [(ChargerBotProShielded, 15.0)
 758                               ] * (1 + (level - 7) // 3)
 759
 760            # Bot type, their effect on target points.
 761            defender_types: list[tuple[type[SpazBot], float]] = [
 762                (BomberBot, 0.9),
 763                (BrawlerBot, 0.9),
 764                (TriggerBot, 0.85),
 765            ]
 766            if level > 2:
 767                defender_types += [(ChargerBot, 0.75)]
 768            if level > 4:
 769                defender_types += ([(StickyBot, 0.7)] * (1 + (level - 5) // 6))
 770            if level > 6:
 771                defender_types += ([(ExplodeyBot, 0.7)] * (1 +
 772                                                           (level - 5) // 5))
 773            if level > 8:
 774                defender_types += ([(BrawlerBotProShielded, 0.65)] *
 775                                   (1 + (level - 5) // 4))
 776            if level > 10:
 777                defender_types += ([(TriggerBotProShielded, 0.6)] *
 778                                   (1 + (level - 6) // 3))
 779
 780            for group in range(group_count):
 781                this_target_point_s = target_points / group_count
 782
 783                # Adding spacing makes things slightly harder.
 784                rval = random.random()
 785                if rval < 0.07:
 786                    spacing = 1.5
 787                    this_target_point_s *= 0.85
 788                elif rval < 0.15:
 789                    spacing = 1.0
 790                    this_target_point_s *= 0.9
 791                else:
 792                    spacing = 0.0
 793
 794                path = random.randint(1, 3)
 795
 796                # Don't allow hard paths on early levels.
 797                if level < 3:
 798                    if path == 1:
 799                        path = 3
 800
 801                # Easy path.
 802                if path == 3:
 803                    pass
 804
 805                # Harder path.
 806                elif path == 2:
 807                    this_target_point_s *= 0.8
 808
 809                # Even harder path.
 810                elif path == 1:
 811                    this_target_point_s *= 0.7
 812
 813                # Looping forward.
 814                elif path == 4:
 815                    this_target_point_s *= 0.7
 816
 817                # Looping backward.
 818                elif path == 5:
 819                    this_target_point_s *= 0.7
 820
 821                # Random.
 822                elif path == 6:
 823                    this_target_point_s *= 0.7
 824
 825                def _add_defender(defender_type: tuple[type[SpazBot], float],
 826                                  pnt: Point) -> tuple[float, Spawn]:
 827                    # This is ok because we call it immediately.
 828                    # pylint: disable=cell-var-from-loop
 829                    return this_target_point_s * defender_type[1], Spawn(
 830                        defender_type[0], point=pnt)
 831
 832                # Add defenders.
 833                defender_type1 = defender_types[random.randrange(
 834                    len(defender_types))]
 835                defender_type2 = defender_types[random.randrange(
 836                    len(defender_types))]
 837                defender1 = defender2 = None
 838                if ((group == 0) or (group == 1 and level > 3)
 839                        or (group == 2 and level > 5)):
 840                    if random.random() < min(0.75, (level - 1) * 0.11):
 841                        this_target_point_s, defender1 = _add_defender(
 842                            defender_type1, Point.BOTTOM_LEFT)
 843                    if random.random() < min(0.75, (level - 1) * 0.04):
 844                        this_target_point_s, defender2 = _add_defender(
 845                            defender_type2, Point.BOTTOM_RIGHT)
 846
 847                spaz_type = spaz_types[random.randrange(len(spaz_types))]
 848                member_count = max(
 849                    1, int(round(this_target_point_s / spaz_type[1])))
 850                for i, _member in enumerate(range(member_count)):
 851                    if path == 4:
 852                        this_path = i % 3  # Looping forward.
 853                    elif path == 5:
 854                        this_path = 3 - (i % 3)  # Looping backward.
 855                    elif path == 6:
 856                        this_path = random.randint(1, 3)  # Random.
 857                    else:
 858                        this_path = path
 859                    entries.append(Spawn(spaz_type[0], path=this_path))
 860                    if spacing != 0.0:
 861                        entries.append(Spacing(duration=spacing))
 862
 863                if defender1 is not None:
 864                    entries.append(defender1)
 865                if defender2 is not None:
 866                    entries.append(defender2)
 867
 868                # Some spacing between groups.
 869                rval = random.random()
 870                if rval < 0.1:
 871                    spacing = 5.0
 872                elif rval < 0.5:
 873                    spacing = 1.0
 874                else:
 875                    spacing = 1.0
 876                entries.append(Spacing(duration=spacing))
 877
 878            wave = Wave(entries=entries)
 879
 880        else:
 881            assert self._waves is not None
 882            wave = self._waves[self._wavenum - 1]
 883
 884        bot_types += wave.entries
 885        self._time_bonus_mult = 1.0
 886        this_flawless_bonus = 0
 887        non_runner_spawn_time = 1.0
 888
 889        for info in bot_types:
 890            if info is None:
 891                continue
 892            if isinstance(info, Spacing):
 893                t_sec += info.duration
 894                continue
 895            bot_type = info.type
 896            path = info.path
 897            self._time_bonus_mult += bot_type.points_mult * 0.02
 898            this_flawless_bonus += bot_type.points_mult * 5
 899
 900            # If its got a position, use that.
 901            if info.point is not None:
 902                point = info.point
 903            else:
 904                point = Point.START
 905
 906            # Space our our slower bots.
 907            delay = base_delay
 908            delay /= self._get_bot_speed(bot_type)
 909            t_sec += delay * 0.5
 910            tcall = ba.Call(
 911                self.add_bot_at_point, point, bot_type, path,
 912                0.1 if point is Point.START else non_runner_spawn_time)
 913            ba.timer(t_sec, tcall)
 914            t_sec += delay * 0.5
 915
 916        # We can end the wave after all the spawning happens.
 917        ba.timer(t_sec - delay * 0.5 + non_runner_spawn_time + 0.01,
 918                 self._set_can_end_wave)
 919
 920        # Reset our time bonus.
 921        # In this game we use a constant time bonus so it erodes away in
 922        # roughly the same time (since the time limit a wave can take is
 923        # relatively constant) ..we then post-multiply a modifier to adjust
 924        # points.
 925        self._time_bonus = 150
 926        self._flawless_bonus = this_flawless_bonus
 927        assert self._time_bonus_mult is not None
 928        txtval = ba.Lstr(
 929            value='${A}: ${B}',
 930            subs=[('${A}', ba.Lstr(resource='timeBonusText')),
 931                  ('${B}', str(int(self._time_bonus * self._time_bonus_mult)))
 932                  ])
 933        self._time_bonus_text = ba.NodeActor(
 934            ba.newnode('text',
 935                       attrs={
 936                           'v_attach': 'top',
 937                           'h_attach': 'center',
 938                           'h_align': 'center',
 939                           'color': (1, 1, 0.0, 1),
 940                           'shadow': 1.0,
 941                           'vr_depth': -30,
 942                           'flatness': 1.0,
 943                           'position': (0, -60),
 944                           'scale': 0.8,
 945                           'text': txtval
 946                       }))
 947
 948        ba.timer(t_sec, self._start_time_bonus_timer)
 949
 950        # Keep track of when this wave finishes emerging. We wanna stop
 951        # dropping land-mines powerups at some point (otherwise a crafty
 952        # player could fill the whole map with them)
 953        self._last_wave_end_time = ba.time() + t_sec
 954        totalwaves = str(len(self._waves)) if self._waves is not None else '??'
 955        txtval = ba.Lstr(value='${A} ${B}',
 956                         subs=[('${A}', ba.Lstr(resource='waveText')),
 957                               ('${B}',
 958                                str(self._wavenum) + ('' if self._preset in {
 959                                    Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT
 960                                } else f'/{totalwaves}'))])
 961        self._wave_text = ba.NodeActor(
 962            ba.newnode('text',
 963                       attrs={
 964                           'v_attach': 'top',
 965                           'h_attach': 'center',
 966                           'h_align': 'center',
 967                           'vr_depth': -10,
 968                           'color': (1, 1, 1, 1),
 969                           'shadow': 1.0,
 970                           'flatness': 1.0,
 971                           'position': (0, -40),
 972                           'scale': 1.3,
 973                           'text': txtval
 974                       }))
 975
 976    def _on_bot_spawn(self, path: int, spaz: SpazBot) -> None:
 977
 978        # Add our custom update callback and set some info for this bot.
 979        spaz_type = type(spaz)
 980        assert spaz is not None
 981        spaz.update_callback = self._update_bot
 982
 983        # Tack some custom attrs onto the spaz.
 984        setattr(spaz, 'r_walk_row', path)
 985        setattr(spaz, 'r_walk_speed', self._get_bot_speed(spaz_type))
 986
 987    def add_bot_at_point(self,
 988                         point: Point,
 989                         spaztype: type[SpazBot],
 990                         path: int,
 991                         spawn_time: float = 0.1) -> None:
 992        """Add the given type bot with the given delay (in seconds)."""
 993
 994        # Don't add if the game has ended.
 995        if self._game_over:
 996            return
 997        pos = self.map.defs.points[point.value][:3]
 998        self._bots.spawn_bot(spaztype,
 999                             pos=pos,
1000                             spawn_time=spawn_time,
1001                             on_spawn_call=ba.Call(self._on_bot_spawn, path))
1002
1003    def _update_time_bonus(self) -> None:
1004        self._time_bonus = int(self._time_bonus * 0.91)
1005        if self._time_bonus > 0 and self._time_bonus_text is not None:
1006            assert self._time_bonus_text.node
1007            assert self._time_bonus_mult
1008            self._time_bonus_text.node.text = ba.Lstr(
1009                value='${A}: ${B}',
1010                subs=[('${A}', ba.Lstr(resource='timeBonusText')),
1011                      ('${B}',
1012                       str(int(self._time_bonus * self._time_bonus_mult)))])
1013        else:
1014            self._time_bonus_text = None
1015
1016    def _start_updating_waves(self) -> None:
1017        self._wave_update_timer = ba.Timer(2.0,
1018                                           self._update_waves,
1019                                           repeat=True)
1020
1021    def _update_scores(self) -> None:
1022        score = self._score
1023        if self._preset is Preset.ENDLESS:
1024            if score >= 500:
1025                self._award_achievement('Runaround Master')
1026            if score >= 1000:
1027                self._award_achievement('Runaround Wizard')
1028            if score >= 2000:
1029                self._award_achievement('Runaround God')
1030
1031        assert self._scoreboard is not None
1032        self._scoreboard.set_team_value(self.teams[0], score, max_score=None)
1033
1034    def _update_bot(self, bot: SpazBot) -> bool:
1035        # Yup; that's a lot of return statements right there.
1036        # pylint: disable=too-many-return-statements
1037
1038        if not bool(bot):
1039            return True
1040
1041        assert bot.node
1042
1043        # FIXME: Do this in a type safe way.
1044        r_walk_speed: float = getattr(bot, 'r_walk_speed')
1045        r_walk_row: int = getattr(bot, 'r_walk_row')
1046
1047        speed = r_walk_speed
1048        pos = bot.node.position
1049        boxes = self.map.defs.boxes
1050
1051        # Bots in row 1 attempt the high road..
1052        if r_walk_row == 1:
1053            if ba.is_point_in_box(pos, boxes['b4']):
1054                bot.node.move_up_down = speed
1055                bot.node.move_left_right = 0
1056                bot.node.run = 0.0
1057                return True
1058
1059        # Row 1 and 2 bots attempt the middle road..
1060        if r_walk_row in [1, 2]:
1061            if ba.is_point_in_box(pos, boxes['b1']):
1062                bot.node.move_up_down = speed
1063                bot.node.move_left_right = 0
1064                bot.node.run = 0.0
1065                return True
1066
1067        # All bots settle for the third row.
1068        if ba.is_point_in_box(pos, boxes['b7']):
1069            bot.node.move_up_down = speed
1070            bot.node.move_left_right = 0
1071            bot.node.run = 0.0
1072            return True
1073        if ba.is_point_in_box(pos, boxes['b2']):
1074            bot.node.move_up_down = -speed
1075            bot.node.move_left_right = 0
1076            bot.node.run = 0.0
1077            return True
1078        if ba.is_point_in_box(pos, boxes['b3']):
1079            bot.node.move_up_down = -speed
1080            bot.node.move_left_right = 0
1081            bot.node.run = 0.0
1082            return True
1083        if ba.is_point_in_box(pos, boxes['b5']):
1084            bot.node.move_up_down = -speed
1085            bot.node.move_left_right = 0
1086            bot.node.run = 0.0
1087            return True
1088        if ba.is_point_in_box(pos, boxes['b6']):
1089            bot.node.move_up_down = speed
1090            bot.node.move_left_right = 0
1091            bot.node.run = 0.0
1092            return True
1093        if ((ba.is_point_in_box(pos, boxes['b8'])
1094             and not ba.is_point_in_box(pos, boxes['b9']))
1095                or pos == (0.0, 0.0, 0.0)):
1096
1097            # Default to walking right if we're still in the walking area.
1098            bot.node.move_left_right = speed
1099            bot.node.move_up_down = 0
1100            bot.node.run = 0.0
1101            return True
1102
1103        # Revert to normal bot behavior otherwise..
1104        return False
1105
1106    def handlemessage(self, msg: Any) -> Any:
1107        if isinstance(msg, ba.PlayerScoredMessage):
1108            self._score += msg.score
1109            self._update_scores()
1110
1111        elif isinstance(msg, ba.PlayerDiedMessage):
1112            # Augment standard behavior.
1113            super().handlemessage(msg)
1114
1115            self._a_player_has_been_killed = True
1116
1117            # Respawn them shortly.
1118            player = msg.getplayer(Player)
1119            assert self.initialplayerinfos is not None
1120            respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
1121            player.respawn_timer = ba.Timer(
1122                respawn_time, ba.Call(self.spawn_player_if_exists, player))
1123            player.respawn_icon = RespawnIcon(player, respawn_time)
1124
1125        elif isinstance(msg, SpazBotDiedMessage):
1126            if msg.how is ba.DeathType.REACHED_GOAL:
1127                return None
1128            pts, importance = msg.spazbot.get_death_points(msg.how)
1129            if msg.killerplayer is not None:
1130                target: Sequence[float] | None
1131                try:
1132                    assert msg.spazbot is not None
1133                    assert msg.spazbot.node
1134                    target = msg.spazbot.node.position
1135                except Exception:
1136                    ba.print_exception()
1137                    target = None
1138                try:
1139                    if msg.killerplayer:
1140                        self.stats.player_scored(msg.killerplayer,
1141                                                 pts,
1142                                                 target=target,
1143                                                 kill=True,
1144                                                 screenmessage=False,
1145                                                 importance=importance)
1146                        ba.playsound(self._dingsound if importance == 1 else
1147                                     self._dingsoundhigh,
1148                                     volume=0.6)
1149                except Exception:
1150                    ba.print_exception('Error on SpazBotDiedMessage.')
1151
1152            # Normally we pull scores from the score-set, but if there's no
1153            # player lets be explicit.
1154            else:
1155                self._score += pts
1156            self._update_scores()
1157
1158        else:
1159            return super().handlemessage(msg)
1160        return None
1161
1162    def _get_bot_speed(self, bot_type: type[SpazBot]) -> float:
1163        speed = self._bot_speed_map.get(bot_type)
1164        if speed is None:
1165            raise TypeError('Invalid bot type to _get_bot_speed(): ' +
1166                            str(bot_type))
1167        return speed
1168
1169    def _set_can_end_wave(self) -> None:
1170        self._can_end_wave = True

Game involving trying to bomb bots as they walk through the map.

RunaroundGame(settings: dict)
117    def __init__(self, settings: dict):
118        settings['map'] = 'Tower D'
119        super().__init__(settings)
120        shared = SharedObjects.get()
121        self._preset = Preset(settings.get('preset', 'pro'))
122
123        self._player_death_sound = ba.getsound('playerDeath')
124        self._new_wave_sound = ba.getsound('scoreHit01')
125        self._winsound = ba.getsound('score')
126        self._cashregistersound = ba.getsound('cashRegister')
127        self._bad_guy_score_sound = ba.getsound('shieldDown')
128        self._heart_tex = ba.gettexture('heart')
129        self._heart_model_opaque = ba.getmodel('heartOpaque')
130        self._heart_model_transparent = ba.getmodel('heartTransparent')
131
132        self._a_player_has_been_killed = False
133        self._spawn_center = self._map_type.defs.points['spawn1'][0:3]
134        self._tntspawnpos = self._map_type.defs.points['tnt_loc'][0:3]
135        self._powerup_center = self._map_type.defs.boxes['powerup_region'][0:3]
136        self._powerup_spread = (
137            self._map_type.defs.boxes['powerup_region'][6] * 0.5,
138            self._map_type.defs.boxes['powerup_region'][8] * 0.5)
139
140        self._score_region_material = ba.Material()
141        self._score_region_material.add_actions(
142            conditions=('they_have_material', shared.player_material),
143            actions=(
144                ('modify_part_collision', 'collide', True),
145                ('modify_part_collision', 'physical', False),
146                ('call', 'at_connect', self._handle_reached_end),
147            ))
148
149        self._last_wave_end_time = ba.time()
150        self._player_has_picked_up_powerup = False
151        self._scoreboard: Scoreboard | None = None
152        self._game_over = False
153        self._wavenum = 0
154        self._can_end_wave = True
155        self._score = 0
156        self._time_bonus = 0
157        self._score_region: ba.Actor | None = None
158        self._dingsound = ba.getsound('dingSmall')
159        self._dingsoundhigh = ba.getsound('dingSmallHigh')
160        self._exclude_powerups: list[str] | None = None
161        self._have_tnt: bool | None = None
162        self._waves: list[Wave] | None = None
163        self._bots = SpazBotSet()
164        self._tntspawner: TNTSpawner | None = None
165        self._lives_bg: ba.NodeActor | None = None
166        self._start_lives = 10
167        self._lives = self._start_lives
168        self._lives_text: ba.NodeActor | None = None
169        self._flawless = True
170        self._time_bonus_timer: ba.Timer | None = None
171        self._time_bonus_text: ba.NodeActor | None = None
172        self._time_bonus_mult: float | None = None
173        self._wave_text: ba.NodeActor | None = None
174        self._flawless_bonus: int | None = None
175        self._wave_update_timer: ba.Timer | None = None

Instantiate the Activity.

name: str | None = 'Runaround'
description: str | None = 'Prevent enemies from reaching the exit.'
tips: list[str | ba._gameutils.GameTip] = ["Jump just as you're throwing to get bombs up to the highest levels.", "No, you can't get up on the ledge. You have to throw bombs.", 'Whip back and forth to get more distance on your throws..']
default_music: ba._music.MusicType | None = <MusicType.MARCHING: 'Marching'>
def on_transition_in(self) -> None:
177    def on_transition_in(self) -> None:
178        super().on_transition_in()
179        self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'),
180                                      score_split=0.5)
181        self._score_region = ba.NodeActor(
182            ba.newnode(
183                'region',
184                attrs={
185                    'position': self.map.defs.boxes['score_region'][0:3],
186                    'scale': self.map.defs.boxes['score_region'][6:9],
187                    'type': 'box',
188                    'materials': [self._score_region_material]
189                }))

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:
191    def on_begin(self) -> None:
192        super().on_begin()
193        player_count = len(self.players)
194        hard = self._preset not in {Preset.PRO_EASY, Preset.UBER_EASY}
195
196        if self._preset in {Preset.PRO, Preset.PRO_EASY, Preset.TOURNAMENT}:
197            self._exclude_powerups = ['curse']
198            self._have_tnt = True
199            self._waves = [
200                Wave(entries=[
201                    Spawn(BomberBot, path=3 if hard else 2),
202                    Spawn(BomberBot, path=2),
203                    Spawn(BomberBot, path=2) if hard else None,
204                    Spawn(BomberBot, path=2) if player_count > 1 else None,
205                    Spawn(BomberBot, path=1) if hard else None,
206                    Spawn(BomberBot, path=1) if player_count > 2 else None,
207                    Spawn(BomberBot, path=1) if player_count > 3 else None,
208                ]),
209                Wave(entries=[
210                    Spawn(BomberBot, path=1) if hard else None,
211                    Spawn(BomberBot, path=2) if hard else None,
212                    Spawn(BomberBot, path=2),
213                    Spawn(BomberBot, path=2),
214                    Spawn(BomberBot, path=2) if player_count > 3 else None,
215                    Spawn(BrawlerBot, path=3),
216                    Spawn(BrawlerBot, path=3),
217                    Spawn(BrawlerBot, path=3) if hard else None,
218                    Spawn(BrawlerBot, path=3) if player_count > 1 else None,
219                    Spawn(BrawlerBot, path=3) if player_count > 2 else None,
220                ]),
221                Wave(entries=[
222                    Spawn(ChargerBot, path=2) if hard else None,
223                    Spawn(ChargerBot, path=2) if player_count > 2 else None,
224                    Spawn(TriggerBot, path=2),
225                    Spawn(TriggerBot, path=2) if player_count > 1 else None,
226                    Spacing(duration=3.0),
227                    Spawn(BomberBot, path=2) if hard else None,
228                    Spawn(BomberBot, path=2) if hard else None,
229                    Spawn(BomberBot, path=2),
230                    Spawn(BomberBot, path=3) if hard else None,
231                    Spawn(BomberBot, path=3),
232                    Spawn(BomberBot, path=3),
233                    Spawn(BomberBot, path=3) if player_count > 3 else None,
234                ]),
235                Wave(entries=[
236                    Spawn(TriggerBot, path=1) if hard else None,
237                    Spacing(duration=1.0) if hard else None,
238                    Spawn(TriggerBot, path=2),
239                    Spacing(duration=1.0),
240                    Spawn(TriggerBot, path=3),
241                    Spacing(duration=1.0),
242                    Spawn(TriggerBot, path=1) if hard else None,
243                    Spacing(duration=1.0) if hard else None,
244                    Spawn(TriggerBot, path=2),
245                    Spacing(duration=1.0),
246                    Spawn(TriggerBot, path=3),
247                    Spacing(duration=1.0),
248                    Spawn(TriggerBot, path=1) if (
249                        player_count > 1 and hard) else None,
250                    Spacing(duration=1.0),
251                    Spawn(TriggerBot, path=2) if player_count > 2 else None,
252                    Spacing(duration=1.0),
253                    Spawn(TriggerBot, path=3) if player_count > 3 else None,
254                    Spacing(duration=1.0),
255                ]),
256                Wave(entries=[
257                    Spawn(ChargerBotProShielded if hard else ChargerBot,
258                          path=1),
259                    Spawn(BrawlerBot, path=2) if hard else None,
260                    Spawn(BrawlerBot, path=2),
261                    Spawn(BrawlerBot, path=2),
262                    Spawn(BrawlerBot, path=3) if hard else None,
263                    Spawn(BrawlerBot, path=3),
264                    Spawn(BrawlerBot, path=3),
265                    Spawn(BrawlerBot, path=3) if player_count > 1 else None,
266                    Spawn(BrawlerBot, path=3) if player_count > 2 else None,
267                    Spawn(BrawlerBot, path=3) if player_count > 3 else None,
268                ]),
269                Wave(entries=[
270                    Spawn(BomberBotProShielded, path=3),
271                    Spacing(duration=1.5),
272                    Spawn(BomberBotProShielded, path=2),
273                    Spacing(duration=1.5),
274                    Spawn(BomberBotProShielded, path=1) if hard else None,
275                    Spacing(duration=1.0) if hard else None,
276                    Spawn(BomberBotProShielded, path=3),
277                    Spacing(duration=1.5),
278                    Spawn(BomberBotProShielded, path=2),
279                    Spacing(duration=1.5),
280                    Spawn(BomberBotProShielded, path=1) if hard else None,
281                    Spacing(duration=1.5) if hard else None,
282                    Spawn(BomberBotProShielded, path=3
283                          ) if player_count > 1 else None,
284                    Spacing(duration=1.5),
285                    Spawn(BomberBotProShielded, path=2
286                          ) if player_count > 2 else None,
287                    Spacing(duration=1.5),
288                    Spawn(BomberBotProShielded, path=1
289                          ) if player_count > 3 else None,
290                ]),
291            ]
292        elif self._preset in {
293                Preset.UBER_EASY, Preset.UBER, Preset.TOURNAMENT_UBER
294        }:
295            self._exclude_powerups = []
296            self._have_tnt = True
297            self._waves = [
298                Wave(entries=[
299                    Spawn(TriggerBot, path=1) if hard else None,
300                    Spawn(TriggerBot, path=2),
301                    Spawn(TriggerBot, path=2),
302                    Spawn(TriggerBot, path=3),
303                    Spawn(BrawlerBotPro if hard else BrawlerBot,
304                          point=Point.BOTTOM_LEFT),
305                    Spawn(BrawlerBotPro, point=Point.BOTTOM_RIGHT
306                          ) if player_count > 2 else None,
307                ]),
308                Wave(entries=[
309                    Spawn(ChargerBot, path=2),
310                    Spawn(ChargerBot, path=3),
311                    Spawn(ChargerBot, path=1) if hard else None,
312                    Spawn(ChargerBot, path=2),
313                    Spawn(ChargerBot, path=3),
314                    Spawn(ChargerBot, path=1) if player_count > 2 else None,
315                ]),
316                Wave(entries=[
317                    Spawn(BomberBotProShielded, path=1) if hard else None,
318                    Spawn(BomberBotProShielded, path=2),
319                    Spawn(BomberBotProShielded, path=2),
320                    Spawn(BomberBotProShielded, path=3),
321                    Spawn(BomberBotProShielded, path=3),
322                    Spawn(ChargerBot, point=Point.BOTTOM_RIGHT),
323                    Spawn(ChargerBot, point=Point.BOTTOM_LEFT
324                          ) if player_count > 2 else None,
325                ]),
326                Wave(entries=[
327                    Spawn(TriggerBotPro, path=1) if hard else None,
328                    Spawn(TriggerBotPro, path=1 if hard else 2),
329                    Spawn(TriggerBotPro, path=1 if hard else 2),
330                    Spawn(TriggerBotPro, path=1 if hard else 2),
331                    Spawn(TriggerBotPro, path=1 if hard else 2),
332                    Spawn(TriggerBotPro, path=1 if hard else 2),
333                    Spawn(TriggerBotPro, path=1 if hard else 2
334                          ) if player_count > 1 else None,
335                    Spawn(TriggerBotPro, path=1 if hard else 2
336                          ) if player_count > 3 else None,
337                ]),
338                Wave(entries=[
339                    Spawn(TriggerBotProShielded if hard else TriggerBotPro,
340                          point=Point.BOTTOM_LEFT),
341                    Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT
342                          ) if hard else None,
343                    Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT
344                          ) if player_count > 2 else None,
345                    Spawn(BomberBot, path=3),
346                    Spawn(BomberBot, path=3),
347                    Spacing(duration=5.0),
348                    Spawn(BrawlerBot, path=2),
349                    Spawn(BrawlerBot, path=2),
350                    Spacing(duration=5.0),
351                    Spawn(TriggerBot, path=1) if hard else None,
352                    Spawn(TriggerBot, path=1) if hard else None,
353                ]),
354                Wave(entries=[
355                    Spawn(BomberBotProShielded, path=2),
356                    Spawn(BomberBotProShielded, path=2) if hard else None,
357                    Spawn(StickyBot, point=Point.BOTTOM_RIGHT),
358                    Spawn(BomberBotProShielded, path=2),
359                    Spawn(BomberBotProShielded, path=2),
360                    Spawn(StickyBot, point=Point.BOTTOM_RIGHT
361                          ) if player_count > 2 else None,
362                    Spawn(BomberBotProShielded, path=2),
363                    Spawn(ExplodeyBot, point=Point.BOTTOM_LEFT),
364                    Spawn(BomberBotProShielded, path=2),
365                    Spawn(BomberBotProShielded, path=2
366                          ) if player_count > 1 else None,
367                    Spacing(duration=5.0),
368                    Spawn(StickyBot, point=Point.BOTTOM_LEFT),
369                    Spacing(duration=2.0),
370                    Spawn(ExplodeyBot, point=Point.BOTTOM_RIGHT),
371                ]),
372            ]
373        elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}:
374            self._exclude_powerups = []
375            self._have_tnt = True
376
377        # Spit out a few powerups and start dropping more shortly.
378        self._drop_powerups(standard_points=True)
379        ba.timer(4.0, self._start_powerup_drops)
380        self.setup_low_life_warning_sound()
381        self._update_scores()
382
383        # Our TNT spawner (if applicable).
384        if self._have_tnt:
385            self._tntspawner = TNTSpawner(position=self._tntspawnpos)
386
387        # Make sure to stay out of the way of menu/party buttons in the corner.
388        uiscale = ba.app.ui.uiscale
389        l_offs = (-80 if uiscale is ba.UIScale.SMALL else
390                  -40 if uiscale is ba.UIScale.MEDIUM else 0)
391
392        self._lives_bg = ba.NodeActor(
393            ba.newnode('image',
394                       attrs={
395                           'texture': self._heart_tex,
396                           'model_opaque': self._heart_model_opaque,
397                           'model_transparent': self._heart_model_transparent,
398                           'attach': 'topRight',
399                           'scale': (90, 90),
400                           'position': (-110 + l_offs, -50),
401                           'color': (1, 0.2, 0.2)
402                       }))
403        # FIXME; should not set things based on vr mode.
404        #  (won't look right to non-vr connected clients, etc)
405        vrmode = ba.app.vr_mode
406        self._lives_text = ba.NodeActor(
407            ba.newnode(
408                'text',
409                attrs={
410                    'v_attach': 'top',
411                    'h_attach': 'right',
412                    'h_align': 'center',
413                    'color': (1, 1, 1, 1) if vrmode else (0.8, 0.8, 0.8, 1.0),
414                    'flatness': 1.0 if vrmode else 0.5,
415                    'shadow': 1.0 if vrmode else 0.5,
416                    'vr_depth': 10,
417                    'position': (-113 + l_offs, -69),
418                    'scale': 1.3,
419                    'text': str(self._lives)
420                }))
421
422        ba.timer(2.0, self._start_updating_waves)

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 on_continue(self) -> None:
481    def on_continue(self) -> None:
482        self._lives = 3
483        assert self._lives_text is not None
484        assert self._lives_text.node
485        self._lives_text.node.text = str(self._lives)
486        self._bots.start_moving()

This is called if a game supports and offers a continue and the player accepts. In this case the player should be given an extra life or whatever is relevant to keep the game going.

def spawn_player(self, player: bastd.game.runaround.Player) -> ba._actor.Actor:
488    def spawn_player(self, player: Player) -> ba.Actor:
489        pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5),
490               self._spawn_center[1],
491               self._spawn_center[2] + random.uniform(-1.5, 1.5))
492        spaz = self.spawn_player_spaz(player, position=pos)
493        if self._preset in {Preset.PRO_EASY, Preset.UBER_EASY}:
494            spaz.impact_scale = 0.25
495
496        # Add the material that causes us to hit the player-wall.
497        spaz.pick_up_powerup_callback = self._on_player_picked_up_powerup
498        return spaz

Spawn something for the provided ba.Player.

The default implementation simply calls spawn_player_spaz().

def end_game(self) -> None:
551    def end_game(self) -> None:
552        ba.pushcall(ba.Call(self.do_end, 'defeat'))
553        ba.setmusic(None)
554        ba.playsound(self._player_death_sound)

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.

def do_end(self, outcome: str) -> None:
556    def do_end(self, outcome: str) -> None:
557        """End the game now with the provided outcome."""
558
559        if outcome == 'defeat':
560            delay = 2.0
561            self.fade_to_red()
562        else:
563            delay = 0
564
565        score: int | None
566        if self._wavenum >= 2:
567            score = self._score
568            fail_message = None
569        else:
570            score = None
571            fail_message = ba.Lstr(resource='reachWave2Text')
572
573        self.end(delay=delay,
574                 results={
575                     'outcome': outcome,
576                     'score': score,
577                     'fail_message': fail_message,
578                     'playerinfos': self.initialplayerinfos
579                 })

End the game now with the provided outcome.

def add_bot_at_point( self, point: bastd.game.runaround.Point, spaztype: type[bastd.actor.spazbot.SpazBot], path: int, spawn_time: float = 0.1) -> None:
 987    def add_bot_at_point(self,
 988                         point: Point,
 989                         spaztype: type[SpazBot],
 990                         path: int,
 991                         spawn_time: float = 0.1) -> None:
 992        """Add the given type bot with the given delay (in seconds)."""
 993
 994        # Don't add if the game has ended.
 995        if self._game_over:
 996            return
 997        pos = self.map.defs.points[point.value][:3]
 998        self._bots.spawn_bot(spaztype,
 999                             pos=pos,
1000                             spawn_time=spawn_time,
1001                             on_spawn_call=ba.Call(self._on_bot_spawn, path))

Add the given type bot with the given delay (in seconds).

def handlemessage(self, msg: Any) -> Any:
1106    def handlemessage(self, msg: Any) -> Any:
1107        if isinstance(msg, ba.PlayerScoredMessage):
1108            self._score += msg.score
1109            self._update_scores()
1110
1111        elif isinstance(msg, ba.PlayerDiedMessage):
1112            # Augment standard behavior.
1113            super().handlemessage(msg)
1114
1115            self._a_player_has_been_killed = True
1116
1117            # Respawn them shortly.
1118            player = msg.getplayer(Player)
1119            assert self.initialplayerinfos is not None
1120            respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
1121            player.respawn_timer = ba.Timer(
1122                respawn_time, ba.Call(self.spawn_player_if_exists, player))
1123            player.respawn_icon = RespawnIcon(player, respawn_time)
1124
1125        elif isinstance(msg, SpazBotDiedMessage):
1126            if msg.how is ba.DeathType.REACHED_GOAL:
1127                return None
1128            pts, importance = msg.spazbot.get_death_points(msg.how)
1129            if msg.killerplayer is not None:
1130                target: Sequence[float] | None
1131                try:
1132                    assert msg.spazbot is not None
1133                    assert msg.spazbot.node
1134                    target = msg.spazbot.node.position
1135                except Exception:
1136                    ba.print_exception()
1137                    target = None
1138                try:
1139                    if msg.killerplayer:
1140                        self.stats.player_scored(msg.killerplayer,
1141                                                 pts,
1142                                                 target=target,
1143                                                 kill=True,
1144                                                 screenmessage=False,
1145                                                 importance=importance)
1146                        ba.playsound(self._dingsound if importance == 1 else
1147                                     self._dingsoundhigh,
1148                                     volume=0.6)
1149                except Exception:
1150                    ba.print_exception('Error on SpazBotDiedMessage.')
1151
1152            # Normally we pull scores from the score-set, but if there's no
1153            # player lets be explicit.
1154            else:
1155                self._score += pts
1156            self._update_scores()
1157
1158        else:
1159            return super().handlemessage(msg)
1160        return None

General message handling; can be passed any message object.

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
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
announce_player_deaths
is_joining_activity
use_fixed_vr_overlay
slow_motion
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