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