bascenev1lib.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 8 9# (see https://ballistica.net/wiki/meta-tag-system) 10 11from __future__ import annotations 12 13import random 14import logging 15from enum import Enum 16from dataclasses import dataclass 17from typing import TYPE_CHECKING 18 19from bascenev1lib.actor.popuptext import PopupText 20from bascenev1lib.actor.bomb import TNTSpawner 21from bascenev1lib.actor.scoreboard import Scoreboard 22from bascenev1lib.actor.respawnicon import RespawnIcon 23from bascenev1lib.actor.powerupbox import PowerupBox, PowerupBoxFactory 24from bascenev1lib.gameutils import SharedObjects 25from bascenev1lib.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) 43import bascenev1 as bs 44 45if TYPE_CHECKING: 46 from typing import Any, Sequence 47 48 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' 60 61 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' 68 69 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 78 79 80@dataclass 81class Spacing: 82 """Defines spacing between spawns.""" 83 84 duration: float 85 86 87@dataclass 88class Wave: 89 """Defines a wave of enemies.""" 90 91 entries: list[Spawn | Spacing | None] 92 93 94class Player(bs.Player['Team']): 95 """Our player type for this game.""" 96 97 def __init__(self) -> None: 98 self.respawn_timer: bs.Timer | None = None 99 self.respawn_icon: RespawnIcon | None = None 100 101 102class Team(bs.Team[Player]): 103 """Our team type for this game.""" 104 105 106class RunaroundGame(bs.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 = bs.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 = bs.getsound('playerDeath') 142 self._new_wave_sound = bs.getsound('scoreHit01') 143 self._winsound = bs.getsound('score') 144 self._cashregistersound = bs.getsound('cashRegister') 145 self._bad_guy_score_sound = bs.getsound('shieldDown') 146 self._heart_tex = bs.gettexture('heart') 147 self._heart_mesh_opaque = bs.getmesh('heartOpaque') 148 self._heart_mesh_transparent = bs.getmesh('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 = bs.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 = bs.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: bs.Actor | None = None 178 self._dingsound = bs.getsound('dingSmall') 179 self._dingsoundhigh = bs.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: bs.NodeActor | None = None 186 self._start_lives = 10 187 self._lives = self._start_lives 188 self._lives_text: bs.NodeActor | None = None 189 self._flawless = True 190 self._time_bonus_timer: bs.Timer | None = None 191 self._time_bonus_text: bs.NodeActor | None = None 192 self._time_bonus_mult: float | None = None 193 self._wave_text: bs.NodeActor | None = None 194 self._flawless_bonus: int | None = None 195 self._wave_update_timer: bs.Timer | None = None 196 197 def on_transition_in(self) -> None: 198 super().on_transition_in() 199 self._scoreboard = Scoreboard( 200 label=bs.Lstr(resource='scoreText'), score_split=0.5 201 ) 202 self._score_region = bs.NodeActor( 203 bs.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 bs.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 assert bs.app.classic is not None 456 uiscale = bs.app.ui_v1.uiscale 457 l_offs = ( 458 -80 459 if uiscale is bs.UIScale.SMALL 460 else -40 461 if uiscale is bs.UIScale.MEDIUM 462 else 0 463 ) 464 465 self._lives_bg = bs.NodeActor( 466 bs.newnode( 467 'image', 468 attrs={ 469 'texture': self._heart_tex, 470 'mesh_opaque': self._heart_mesh_opaque, 471 'mesh_transparent': self._heart_mesh_transparent, 472 'attach': 'topRight', 473 'scale': (90, 90), 474 'position': (-110 + l_offs, -50), 475 'color': (1, 0.2, 0.2), 476 }, 477 ) 478 ) 479 # FIXME; should not set things based on vr mode. 480 # (won't look right to non-vr connected clients, etc) 481 vrmode = bs.app.env.vr 482 self._lives_text = bs.NodeActor( 483 bs.newnode( 484 'text', 485 attrs={ 486 'v_attach': 'top', 487 'h_attach': 'right', 488 'h_align': 'center', 489 'color': (1, 1, 1, 1) if vrmode else (0.8, 0.8, 0.8, 1.0), 490 'flatness': 1.0 if vrmode else 0.5, 491 'shadow': 1.0 if vrmode else 0.5, 492 'vr_depth': 10, 493 'position': (-113 + l_offs, -69), 494 'scale': 1.3, 495 'text': str(self._lives), 496 }, 497 ) 498 ) 499 500 bs.timer(2.0, self._start_updating_waves) 501 502 def _handle_reached_end(self) -> None: 503 spaz = bs.getcollision().opposingnode.getdelegate(SpazBot, True) 504 if not spaz.is_alive(): 505 return # Ignore bodies flying in. 506 507 self._flawless = False 508 pos = spaz.node.position 509 self._bad_guy_score_sound.play(position=pos) 510 light = bs.newnode( 511 'light', attrs={'position': pos, 'radius': 0.5, 'color': (1, 0, 0)} 512 ) 513 bs.animate(light, 'intensity', {0.0: 0, 0.1: 1, 0.5: 0}, loop=False) 514 bs.timer(1.0, light.delete) 515 spaz.handlemessage( 516 bs.DieMessage(immediate=True, how=bs.DeathType.REACHED_GOAL) 517 ) 518 519 if self._lives > 0: 520 self._lives -= 1 521 if self._lives == 0: 522 self._bots.stop_moving() 523 self.continue_or_end_game() 524 assert self._lives_text is not None 525 assert self._lives_text.node 526 self._lives_text.node.text = str(self._lives) 527 delay = 0.0 528 529 def _safesetattr(node: bs.Node, attr: str, value: Any) -> None: 530 if node: 531 setattr(node, attr, value) 532 533 for _i in range(4): 534 bs.timer( 535 delay, 536 bs.Call( 537 _safesetattr, 538 self._lives_text.node, 539 'color', 540 (1, 0, 0, 1.0), 541 ), 542 ) 543 assert self._lives_bg is not None 544 assert self._lives_bg.node 545 bs.timer( 546 delay, 547 bs.Call(_safesetattr, self._lives_bg.node, 'opacity', 0.5), 548 ) 549 delay += 0.125 550 bs.timer( 551 delay, 552 bs.Call( 553 _safesetattr, 554 self._lives_text.node, 555 'color', 556 (1.0, 1.0, 0.0, 1.0), 557 ), 558 ) 559 bs.timer( 560 delay, 561 bs.Call(_safesetattr, self._lives_bg.node, 'opacity', 1.0), 562 ) 563 delay += 0.125 564 bs.timer( 565 delay, 566 bs.Call( 567 _safesetattr, 568 self._lives_text.node, 569 'color', 570 (0.8, 0.8, 0.8, 1.0), 571 ), 572 ) 573 574 def on_continue(self) -> None: 575 self._lives = 3 576 assert self._lives_text is not None 577 assert self._lives_text.node 578 self._lives_text.node.text = str(self._lives) 579 self._bots.start_moving() 580 581 def spawn_player(self, player: Player) -> bs.Actor: 582 pos = ( 583 self._spawn_center[0] + random.uniform(-1.5, 1.5), 584 self._spawn_center[1], 585 self._spawn_center[2] + random.uniform(-1.5, 1.5), 586 ) 587 spaz = self.spawn_player_spaz(player, position=pos) 588 if self._preset in {Preset.PRO_EASY, Preset.UBER_EASY}: 589 spaz.impact_scale = 0.25 590 591 # Add the material that causes us to hit the player-wall. 592 spaz.pick_up_powerup_callback = self._on_player_picked_up_powerup 593 return spaz 594 595 def _on_player_picked_up_powerup(self, player: bs.Actor) -> None: 596 del player # Unused. 597 self._player_has_picked_up_powerup = True 598 599 def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: 600 if poweruptype is None: 601 poweruptype = PowerupBoxFactory.get().get_random_powerup_type( 602 excludetypes=self._exclude_powerups 603 ) 604 PowerupBox( 605 position=self.map.powerup_spawn_points[index], 606 poweruptype=poweruptype, 607 ).autoretain() 608 609 def _start_powerup_drops(self) -> None: 610 bs.timer(3.0, self._drop_powerups, repeat=True) 611 612 def _drop_powerups( 613 self, standard_points: bool = False, force_first: str | None = None 614 ) -> None: 615 """Generic powerup drop.""" 616 617 # If its been a minute since our last wave finished emerging, stop 618 # giving out land-mine powerups. (prevents players from waiting 619 # around for them on purpose and filling the map up) 620 if bs.time() - self._last_wave_end_time > 60.0: 621 extra_excludes = ['land_mines'] 622 else: 623 extra_excludes = [] 624 625 if standard_points: 626 points = self.map.powerup_spawn_points 627 for i in range(len(points)): 628 bs.timer( 629 1.0 + i * 0.5, 630 bs.Call( 631 self._drop_powerup, i, force_first if i == 0 else None 632 ), 633 ) 634 else: 635 pos = ( 636 self._powerup_center[0] 637 + random.uniform( 638 -1.0 * self._powerup_spread[0], 639 1.0 * self._powerup_spread[0], 640 ), 641 self._powerup_center[1], 642 self._powerup_center[2] 643 + random.uniform( 644 -self._powerup_spread[1], self._powerup_spread[1] 645 ), 646 ) 647 648 # drop one random one somewhere.. 649 assert self._exclude_powerups is not None 650 PowerupBox( 651 position=pos, 652 poweruptype=PowerupBoxFactory.get().get_random_powerup_type( 653 excludetypes=self._exclude_powerups + extra_excludes 654 ), 655 ).autoretain() 656 657 def end_game(self) -> None: 658 bs.pushcall(bs.Call(self.do_end, 'defeat')) 659 bs.setmusic(None) 660 self._player_death_sound.play() 661 662 def do_end(self, outcome: str) -> None: 663 """End the game now with the provided outcome.""" 664 665 if outcome == 'defeat': 666 delay = 2.0 667 self.fade_to_red() 668 else: 669 delay = 0 670 671 score: int | None 672 if self._wavenum >= 2: 673 score = self._score 674 fail_message = None 675 else: 676 score = None 677 fail_message = bs.Lstr(resource='reachWave2Text') 678 679 self.end( 680 delay=delay, 681 results={ 682 'outcome': outcome, 683 'score': score, 684 'fail_message': fail_message, 685 'playerinfos': self.initialplayerinfos, 686 }, 687 ) 688 689 def _update_waves(self) -> None: 690 # pylint: disable=too-many-branches 691 692 # If we have no living bots, go to the next wave. 693 if ( 694 self._can_end_wave 695 and not self._bots.have_living_bots() 696 and not self._game_over 697 and self._lives > 0 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 bs.timer(0, self._cashregistersound.play) 713 bs.timer( 714 base_delay, 715 bs.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 bs.timer(base_delay, self._award_flawless_bonus) 722 base_delay += 1.0 723 724 self._flawless = True # reset 725 726 if won: 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 bs.Lstr(resource='victoryText'), scale=1.0, duration=4.0 750 ) 751 752 self.celebrate(10.0) 753 bs.timer(base_delay, self._award_lives_bonus) 754 base_delay += 1.0 755 bs.timer(base_delay, self._award_completion_bonus) 756 base_delay += 0.85 757 self._winsound.play() 758 bs.cameraflash() 759 bs.setmusic(bs.MusicType.VICTORY) 760 self._game_over = True 761 bs.timer(base_delay, bs.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 bs.timer(base_delay, self._start_next_wave) 771 772 def _award_completion_bonus(self) -> None: 773 bonus = 200 774 self._cashregistersound.play() 775 PopupText( 776 bs.Lstr( 777 value='+${A} ${B}', 778 subs=[ 779 ('${A}', str(bonus)), 780 ('${B}', bs.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 self._cashregistersound.play() 793 PopupText( 794 bs.Lstr( 795 value='+${A} ${B}', 796 subs=[ 797 ('${A}', str(bonus)), 798 ('${B}', bs.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 self._cashregistersound.play() 810 PopupText( 811 bs.Lstr( 812 value='+${A} ${B}', 813 subs=[ 814 ('${A}', str(bonus)), 815 ('${B}', bs.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 self._cashregistersound.play() 828 PopupText( 829 bs.Lstr( 830 value='+${A} ${B}', 831 subs=[ 832 ('${A}', str(self._flawless_bonus)), 833 ('${B}', bs.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 = bs.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 bs.Lstr( 857 value='${A} ${B}', 858 subs=[ 859 ('${A}', bs.Lstr(resource='waveText')), 860 ('${B}', str(self._wavenum)), 861 ], 862 ), 863 scale=1.0, 864 duration=1.0, 865 trail=True, 866 ) 867 bs.timer(0.4, self._new_wave_sound.play) 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 = bs.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 bs.timer(t_sec, tcall) 1071 t_sec += delay * 0.5 1072 1073 # We can end the wave after all the spawning happens. 1074 bs.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 = bs.Lstr( 1088 value='${A}: ${B}', 1089 subs=[ 1090 ('${A}', bs.Lstr(resource='timeBonusText')), 1091 ('${B}', str(int(self._time_bonus * self._time_bonus_mult))), 1092 ], 1093 ) 1094 self._time_bonus_text = bs.NodeActor( 1095 bs.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 bs.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 = bs.Time(bs.time() + t_sec) 1118 totalwaves = str(len(self._waves)) if self._waves is not None else '??' 1119 txtval = bs.Lstr( 1120 value='${A} ${B}', 1121 subs=[ 1122 ('${A}', bs.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 = bs.NodeActor( 1136 bs.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 # Add our custom update callback and set some info for this bot. 1155 spaz_type = type(spaz) 1156 assert spaz is not None 1157 spaz.update_callback = self._update_bot 1158 1159 # Tack some custom attrs onto the spaz. 1160 setattr(spaz, 'r_walk_row', path) 1161 setattr(spaz, 'r_walk_speed', self._get_bot_speed(spaz_type)) 1162 1163 def add_bot_at_point( 1164 self, 1165 point: Point, 1166 spaztype: type[SpazBot], 1167 path: int, 1168 spawn_time: float = 0.1, 1169 ) -> None: 1170 """Add the given type bot with the given delay (in seconds).""" 1171 1172 # Don't add if the game has ended. 1173 if self._game_over: 1174 return 1175 pos = self.map.defs.points[point.value][:3] 1176 self._bots.spawn_bot( 1177 spaztype, 1178 pos=pos, 1179 spawn_time=spawn_time, 1180 on_spawn_call=bs.Call(self._on_bot_spawn, path), 1181 ) 1182 1183 def _update_time_bonus(self) -> None: 1184 self._time_bonus = int(self._time_bonus * 0.91) 1185 if self._time_bonus > 0 and self._time_bonus_text is not None: 1186 assert self._time_bonus_text.node 1187 assert self._time_bonus_mult 1188 self._time_bonus_text.node.text = bs.Lstr( 1189 value='${A}: ${B}', 1190 subs=[ 1191 ('${A}', bs.Lstr(resource='timeBonusText')), 1192 ( 1193 '${B}', 1194 str(int(self._time_bonus * self._time_bonus_mult)), 1195 ), 1196 ], 1197 ) 1198 else: 1199 self._time_bonus_text = None 1200 1201 def _start_updating_waves(self) -> None: 1202 self._wave_update_timer = bs.Timer(2.0, self._update_waves, repeat=True) 1203 1204 def _update_scores(self) -> None: 1205 score = self._score 1206 if self._preset is Preset.ENDLESS: 1207 if score >= 500: 1208 self._award_achievement('Runaround Master') 1209 if score >= 1000: 1210 self._award_achievement('Runaround Wizard') 1211 if score >= 2000: 1212 self._award_achievement('Runaround God') 1213 1214 assert self._scoreboard is not None 1215 self._scoreboard.set_team_value(self.teams[0], score, max_score=None) 1216 1217 def _update_bot(self, bot: SpazBot) -> bool: 1218 # Yup; that's a lot of return statements right there. 1219 # pylint: disable=too-many-return-statements 1220 1221 if not bool(bot): 1222 return True 1223 1224 assert bot.node 1225 1226 # FIXME: Do this in a type safe way. 1227 r_walk_speed: float = getattr(bot, 'r_walk_speed') 1228 r_walk_row: int = getattr(bot, 'r_walk_row') 1229 1230 speed = r_walk_speed 1231 pos = bot.node.position 1232 boxes = self.map.defs.boxes 1233 1234 # Bots in row 1 attempt the high road.. 1235 if r_walk_row == 1: 1236 if bs.is_point_in_box(pos, boxes['b4']): 1237 bot.node.move_up_down = speed 1238 bot.node.move_left_right = 0 1239 bot.node.run = 0.0 1240 return True 1241 1242 # Row 1 and 2 bots attempt the middle road.. 1243 if r_walk_row in [1, 2]: 1244 if bs.is_point_in_box(pos, boxes['b1']): 1245 bot.node.move_up_down = speed 1246 bot.node.move_left_right = 0 1247 bot.node.run = 0.0 1248 return True 1249 1250 # All bots settle for the third row. 1251 if bs.is_point_in_box(pos, boxes['b7']): 1252 bot.node.move_up_down = speed 1253 bot.node.move_left_right = 0 1254 bot.node.run = 0.0 1255 return True 1256 if bs.is_point_in_box(pos, boxes['b2']): 1257 bot.node.move_up_down = -speed 1258 bot.node.move_left_right = 0 1259 bot.node.run = 0.0 1260 return True 1261 if bs.is_point_in_box(pos, boxes['b3']): 1262 bot.node.move_up_down = -speed 1263 bot.node.move_left_right = 0 1264 bot.node.run = 0.0 1265 return True 1266 if bs.is_point_in_box(pos, boxes['b5']): 1267 bot.node.move_up_down = -speed 1268 bot.node.move_left_right = 0 1269 bot.node.run = 0.0 1270 return True 1271 if bs.is_point_in_box(pos, boxes['b6']): 1272 bot.node.move_up_down = speed 1273 bot.node.move_left_right = 0 1274 bot.node.run = 0.0 1275 return True 1276 if ( 1277 bs.is_point_in_box(pos, boxes['b8']) 1278 and not bs.is_point_in_box(pos, boxes['b9']) 1279 ) or pos == (0.0, 0.0, 0.0): 1280 # Default to walking right if we're still in the walking area. 1281 bot.node.move_left_right = speed 1282 bot.node.move_up_down = 0 1283 bot.node.run = 0.0 1284 return True 1285 1286 # Revert to normal bot behavior otherwise.. 1287 return False 1288 1289 def handlemessage(self, msg: Any) -> Any: 1290 if isinstance(msg, bs.PlayerScoredMessage): 1291 self._score += msg.score 1292 self._update_scores() 1293 1294 elif isinstance(msg, bs.PlayerDiedMessage): 1295 # Augment standard behavior. 1296 super().handlemessage(msg) 1297 1298 self._a_player_has_been_killed = True 1299 1300 # Respawn them shortly. 1301 player = msg.getplayer(Player) 1302 assert self.initialplayerinfos is not None 1303 respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 1304 player.respawn_timer = bs.Timer( 1305 respawn_time, bs.Call(self.spawn_player_if_exists, player) 1306 ) 1307 player.respawn_icon = RespawnIcon(player, respawn_time) 1308 1309 elif isinstance(msg, SpazBotDiedMessage): 1310 if msg.how is bs.DeathType.REACHED_GOAL: 1311 return None 1312 pts, importance = msg.spazbot.get_death_points(msg.how) 1313 if msg.killerplayer is not None: 1314 target: Sequence[float] | None 1315 try: 1316 assert msg.spazbot is not None 1317 assert msg.spazbot.node 1318 target = msg.spazbot.node.position 1319 except Exception: 1320 logging.exception('Error getting SpazBotDied target.') 1321 target = None 1322 try: 1323 if msg.killerplayer: 1324 self.stats.player_scored( 1325 msg.killerplayer, 1326 pts, 1327 target=target, 1328 kill=True, 1329 screenmessage=False, 1330 importance=importance, 1331 ) 1332 dingsound = ( 1333 self._dingsound 1334 if importance == 1 1335 else self._dingsoundhigh 1336 ) 1337 dingsound.play(volume=0.6) 1338 except Exception: 1339 logging.exception('Error on SpazBotDiedMessage.') 1340 1341 # Normally we pull scores from the score-set, but if there's no 1342 # player lets be explicit. 1343 else: 1344 self._score += pts 1345 self._update_scores() 1346 1347 else: 1348 return super().handlemessage(msg) 1349 return None 1350 1351 def _get_bot_speed(self, bot_type: type[SpazBot]) -> float: 1352 speed = self._bot_speed_map.get(bot_type) 1353 if speed is None: 1354 raise TypeError( 1355 'Invalid bot type to _get_bot_speed(): ' + str(bot_type) 1356 ) 1357 return speed 1358 1359 def _set_can_end_wave(self) -> None: 1360 self._can_end_wave = True
50class Preset(Enum): 51 """Play presets.""" 52 53 ENDLESS = 'endless' 54 ENDLESS_TOURNAMENT = 'endless_tournament' 55 PRO = 'pro' 56 PRO_EASY = 'pro_easy' 57 UBER = 'uber' 58 UBER_EASY = 'uber_easy' 59 TOURNAMENT = 'tournament' 60 TOURNAMENT_UBER = 'tournament_uber'
Play presets.
Inherited Members
- enum.Enum
- name
- value
63class Point(Enum): 64 """Where we can spawn stuff and the corresponding map attr name.""" 65 66 BOTTOM_LEFT = 'bot_spawn_bottom_left' 67 BOTTOM_RIGHT = 'bot_spawn_bottom_right' 68 START = 'bot_spawn_start'
Where we can spawn stuff and the corresponding map attr name.
Inherited Members
- enum.Enum
- name
- value
71@dataclass 72class Spawn: 73 """Defines a bot spawn event.""" 74 75 # noinspection PyUnresolvedReferences 76 type: type[SpazBot] 77 path: int = 0 78 point: Point | None = None
Defines a bot spawn event.
Defines spacing between spawns.
88@dataclass 89class Wave: 90 """Defines a wave of enemies.""" 91 92 entries: list[Spawn | Spacing | None]
Defines a wave of enemies.
95class Player(bs.Player['Team']): 96 """Our player type for this game.""" 97 98 def __init__(self) -> None: 99 self.respawn_timer: bs.Timer | None = None 100 self.respawn_icon: RespawnIcon | None = None
Our player type for this game.
Inherited Members
- bascenev1._player.Player
- character
- actor
- color
- highlight
- on_expire
- team
- customdata
- sessionplayer
- node
- position
- exists
- getname
- is_alive
- get_icon
- assigninput
- resetinput
Our team type for this game.
Inherited Members
- bascenev1._team.Team
- players
- id
- name
- color
- manual_init
- customdata
- on_expire
- sessionteam
107class RunaroundGame(bs.CoopGameActivity[Player, Team]): 108 """Game involving trying to bomb bots as they walk through the map.""" 109 110 name = 'Runaround' 111 description = 'Prevent enemies from reaching the exit.' 112 tips = [ 113 'Jump just as you\'re throwing to get bombs up to the highest levels.', 114 'No, you can\'t get up on the ledge. You have to throw bombs.', 115 'Whip back and forth to get more distance on your throws..', 116 ] 117 default_music = bs.MusicType.MARCHING 118 119 # How fast our various bot types walk. 120 _bot_speed_map: dict[type[SpazBot], float] = { 121 BomberBot: 0.48, 122 BomberBotPro: 0.48, 123 BomberBotProShielded: 0.48, 124 BrawlerBot: 0.57, 125 BrawlerBotPro: 0.57, 126 BrawlerBotProShielded: 0.57, 127 TriggerBot: 0.73, 128 TriggerBotPro: 0.78, 129 TriggerBotProShielded: 0.78, 130 ChargerBot: 1.0, 131 ChargerBotProShielded: 1.0, 132 ExplodeyBot: 1.0, 133 StickyBot: 0.5, 134 } 135 136 def __init__(self, settings: dict): 137 settings['map'] = 'Tower D' 138 super().__init__(settings) 139 shared = SharedObjects.get() 140 self._preset = Preset(settings.get('preset', 'pro')) 141 142 self._player_death_sound = bs.getsound('playerDeath') 143 self._new_wave_sound = bs.getsound('scoreHit01') 144 self._winsound = bs.getsound('score') 145 self._cashregistersound = bs.getsound('cashRegister') 146 self._bad_guy_score_sound = bs.getsound('shieldDown') 147 self._heart_tex = bs.gettexture('heart') 148 self._heart_mesh_opaque = bs.getmesh('heartOpaque') 149 self._heart_mesh_transparent = bs.getmesh('heartTransparent') 150 151 self._a_player_has_been_killed = False 152 self._spawn_center = self._map_type.defs.points['spawn1'][0:3] 153 self._tntspawnpos = self._map_type.defs.points['tnt_loc'][0:3] 154 self._powerup_center = self._map_type.defs.boxes['powerup_region'][0:3] 155 self._powerup_spread = ( 156 self._map_type.defs.boxes['powerup_region'][6] * 0.5, 157 self._map_type.defs.boxes['powerup_region'][8] * 0.5, 158 ) 159 160 self._score_region_material = bs.Material() 161 self._score_region_material.add_actions( 162 conditions=('they_have_material', shared.player_material), 163 actions=( 164 ('modify_part_collision', 'collide', True), 165 ('modify_part_collision', 'physical', False), 166 ('call', 'at_connect', self._handle_reached_end), 167 ), 168 ) 169 170 self._last_wave_end_time = bs.time() 171 self._player_has_picked_up_powerup = False 172 self._scoreboard: Scoreboard | None = None 173 self._game_over = False 174 self._wavenum = 0 175 self._can_end_wave = True 176 self._score = 0 177 self._time_bonus = 0 178 self._score_region: bs.Actor | None = None 179 self._dingsound = bs.getsound('dingSmall') 180 self._dingsoundhigh = bs.getsound('dingSmallHigh') 181 self._exclude_powerups: list[str] | None = None 182 self._have_tnt: bool | None = None 183 self._waves: list[Wave] | None = None 184 self._bots = SpazBotSet() 185 self._tntspawner: TNTSpawner | None = None 186 self._lives_bg: bs.NodeActor | None = None 187 self._start_lives = 10 188 self._lives = self._start_lives 189 self._lives_text: bs.NodeActor | None = None 190 self._flawless = True 191 self._time_bonus_timer: bs.Timer | None = None 192 self._time_bonus_text: bs.NodeActor | None = None 193 self._time_bonus_mult: float | None = None 194 self._wave_text: bs.NodeActor | None = None 195 self._flawless_bonus: int | None = None 196 self._wave_update_timer: bs.Timer | None = None 197 198 def on_transition_in(self) -> None: 199 super().on_transition_in() 200 self._scoreboard = Scoreboard( 201 label=bs.Lstr(resource='scoreText'), score_split=0.5 202 ) 203 self._score_region = bs.NodeActor( 204 bs.newnode( 205 'region', 206 attrs={ 207 'position': self.map.defs.boxes['score_region'][0:3], 208 'scale': self.map.defs.boxes['score_region'][6:9], 209 'type': 'box', 210 'materials': [self._score_region_material], 211 }, 212 ) 213 ) 214 215 def on_begin(self) -> None: 216 super().on_begin() 217 player_count = len(self.players) 218 hard = self._preset not in {Preset.PRO_EASY, Preset.UBER_EASY} 219 220 if self._preset in {Preset.PRO, Preset.PRO_EASY, Preset.TOURNAMENT}: 221 self._exclude_powerups = ['curse'] 222 self._have_tnt = True 223 self._waves = [ 224 Wave( 225 entries=[ 226 Spawn(BomberBot, path=3 if hard else 2), 227 Spawn(BomberBot, path=2), 228 Spawn(BomberBot, path=2) if hard else None, 229 Spawn(BomberBot, path=2) if player_count > 1 else None, 230 Spawn(BomberBot, path=1) if hard else None, 231 Spawn(BomberBot, path=1) if player_count > 2 else None, 232 Spawn(BomberBot, path=1) if player_count > 3 else None, 233 ] 234 ), 235 Wave( 236 entries=[ 237 Spawn(BomberBot, path=1) if hard else None, 238 Spawn(BomberBot, path=2) if hard else None, 239 Spawn(BomberBot, path=2), 240 Spawn(BomberBot, path=2), 241 Spawn(BomberBot, path=2) if player_count > 3 else None, 242 Spawn(BrawlerBot, path=3), 243 Spawn(BrawlerBot, path=3), 244 Spawn(BrawlerBot, path=3) if hard else None, 245 Spawn(BrawlerBot, path=3) if player_count > 1 else None, 246 Spawn(BrawlerBot, path=3) if player_count > 2 else None, 247 ] 248 ), 249 Wave( 250 entries=[ 251 Spawn(ChargerBot, path=2) if hard else None, 252 Spawn(ChargerBot, path=2) if player_count > 2 else None, 253 Spawn(TriggerBot, path=2), 254 Spawn(TriggerBot, path=2) if player_count > 1 else None, 255 Spacing(duration=3.0), 256 Spawn(BomberBot, path=2) if hard else None, 257 Spawn(BomberBot, path=2) if hard else None, 258 Spawn(BomberBot, path=2), 259 Spawn(BomberBot, path=3) if hard else None, 260 Spawn(BomberBot, path=3), 261 Spawn(BomberBot, path=3), 262 Spawn(BomberBot, path=3) if player_count > 3 else None, 263 ] 264 ), 265 Wave( 266 entries=[ 267 Spawn(TriggerBot, path=1) if hard else None, 268 Spacing(duration=1.0) if hard else None, 269 Spawn(TriggerBot, path=2), 270 Spacing(duration=1.0), 271 Spawn(TriggerBot, path=3), 272 Spacing(duration=1.0), 273 Spawn(TriggerBot, path=1) if hard else None, 274 Spacing(duration=1.0) if hard else None, 275 Spawn(TriggerBot, path=2), 276 Spacing(duration=1.0), 277 Spawn(TriggerBot, path=3), 278 Spacing(duration=1.0), 279 Spawn(TriggerBot, path=1) 280 if (player_count > 1 and hard) 281 else None, 282 Spacing(duration=1.0), 283 Spawn(TriggerBot, path=2) if player_count > 2 else None, 284 Spacing(duration=1.0), 285 Spawn(TriggerBot, path=3) if player_count > 3 else None, 286 Spacing(duration=1.0), 287 ] 288 ), 289 Wave( 290 entries=[ 291 Spawn( 292 ChargerBotProShielded if hard else ChargerBot, 293 path=1, 294 ), 295 Spawn(BrawlerBot, path=2) if hard else None, 296 Spawn(BrawlerBot, path=2), 297 Spawn(BrawlerBot, path=2), 298 Spawn(BrawlerBot, path=3) if hard else None, 299 Spawn(BrawlerBot, path=3), 300 Spawn(BrawlerBot, path=3), 301 Spawn(BrawlerBot, path=3) if player_count > 1 else None, 302 Spawn(BrawlerBot, path=3) if player_count > 2 else None, 303 Spawn(BrawlerBot, path=3) if player_count > 3 else None, 304 ] 305 ), 306 Wave( 307 entries=[ 308 Spawn(BomberBotProShielded, path=3), 309 Spacing(duration=1.5), 310 Spawn(BomberBotProShielded, path=2), 311 Spacing(duration=1.5), 312 Spawn(BomberBotProShielded, path=1) if hard else None, 313 Spacing(duration=1.0) if hard else None, 314 Spawn(BomberBotProShielded, path=3), 315 Spacing(duration=1.5), 316 Spawn(BomberBotProShielded, path=2), 317 Spacing(duration=1.5), 318 Spawn(BomberBotProShielded, path=1) if hard else None, 319 Spacing(duration=1.5) if hard else None, 320 Spawn(BomberBotProShielded, path=3) 321 if player_count > 1 322 else None, 323 Spacing(duration=1.5), 324 Spawn(BomberBotProShielded, path=2) 325 if player_count > 2 326 else None, 327 Spacing(duration=1.5), 328 Spawn(BomberBotProShielded, path=1) 329 if player_count > 3 330 else None, 331 ] 332 ), 333 ] 334 elif self._preset in { 335 Preset.UBER_EASY, 336 Preset.UBER, 337 Preset.TOURNAMENT_UBER, 338 }: 339 self._exclude_powerups = [] 340 self._have_tnt = True 341 self._waves = [ 342 Wave( 343 entries=[ 344 Spawn(TriggerBot, path=1) if hard else None, 345 Spawn(TriggerBot, path=2), 346 Spawn(TriggerBot, path=2), 347 Spawn(TriggerBot, path=3), 348 Spawn( 349 BrawlerBotPro if hard else BrawlerBot, 350 point=Point.BOTTOM_LEFT, 351 ), 352 Spawn(BrawlerBotPro, point=Point.BOTTOM_RIGHT) 353 if player_count > 2 354 else None, 355 ] 356 ), 357 Wave( 358 entries=[ 359 Spawn(ChargerBot, path=2), 360 Spawn(ChargerBot, path=3), 361 Spawn(ChargerBot, path=1) if hard else None, 362 Spawn(ChargerBot, path=2), 363 Spawn(ChargerBot, path=3), 364 Spawn(ChargerBot, path=1) if player_count > 2 else None, 365 ] 366 ), 367 Wave( 368 entries=[ 369 Spawn(BomberBotProShielded, path=1) if hard else None, 370 Spawn(BomberBotProShielded, path=2), 371 Spawn(BomberBotProShielded, path=2), 372 Spawn(BomberBotProShielded, path=3), 373 Spawn(BomberBotProShielded, path=3), 374 Spawn(ChargerBot, point=Point.BOTTOM_RIGHT), 375 Spawn(ChargerBot, point=Point.BOTTOM_LEFT) 376 if player_count > 2 377 else None, 378 ] 379 ), 380 Wave( 381 entries=[ 382 Spawn(TriggerBotPro, path=1) if hard else None, 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 Spawn(TriggerBotPro, path=1 if hard else 2) 389 if player_count > 1 390 else None, 391 Spawn(TriggerBotPro, path=1 if hard else 2) 392 if player_count > 3 393 else None, 394 ] 395 ), 396 Wave( 397 entries=[ 398 Spawn( 399 TriggerBotProShielded if hard else TriggerBotPro, 400 point=Point.BOTTOM_LEFT, 401 ), 402 Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT) 403 if hard 404 else None, 405 Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT) 406 if player_count > 2 407 else None, 408 Spawn(BomberBot, path=3), 409 Spawn(BomberBot, path=3), 410 Spacing(duration=5.0), 411 Spawn(BrawlerBot, path=2), 412 Spawn(BrawlerBot, path=2), 413 Spacing(duration=5.0), 414 Spawn(TriggerBot, path=1) if hard else None, 415 Spawn(TriggerBot, path=1) if hard else None, 416 ] 417 ), 418 Wave( 419 entries=[ 420 Spawn(BomberBotProShielded, path=2), 421 Spawn(BomberBotProShielded, path=2) if hard else None, 422 Spawn(StickyBot, point=Point.BOTTOM_RIGHT), 423 Spawn(BomberBotProShielded, path=2), 424 Spawn(BomberBotProShielded, path=2), 425 Spawn(StickyBot, point=Point.BOTTOM_RIGHT) 426 if player_count > 2 427 else None, 428 Spawn(BomberBotProShielded, path=2), 429 Spawn(ExplodeyBot, point=Point.BOTTOM_LEFT), 430 Spawn(BomberBotProShielded, path=2), 431 Spawn(BomberBotProShielded, path=2) 432 if player_count > 1 433 else None, 434 Spacing(duration=5.0), 435 Spawn(StickyBot, point=Point.BOTTOM_LEFT), 436 Spacing(duration=2.0), 437 Spawn(ExplodeyBot, point=Point.BOTTOM_RIGHT), 438 ] 439 ), 440 ] 441 elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: 442 self._exclude_powerups = [] 443 self._have_tnt = True 444 445 # Spit out a few powerups and start dropping more shortly. 446 self._drop_powerups(standard_points=True) 447 bs.timer(4.0, self._start_powerup_drops) 448 self.setup_low_life_warning_sound() 449 self._update_scores() 450 451 # Our TNT spawner (if applicable). 452 if self._have_tnt: 453 self._tntspawner = TNTSpawner(position=self._tntspawnpos) 454 455 # Make sure to stay out of the way of menu/party buttons in the corner. 456 assert bs.app.classic is not None 457 uiscale = bs.app.ui_v1.uiscale 458 l_offs = ( 459 -80 460 if uiscale is bs.UIScale.SMALL 461 else -40 462 if uiscale is bs.UIScale.MEDIUM 463 else 0 464 ) 465 466 self._lives_bg = bs.NodeActor( 467 bs.newnode( 468 'image', 469 attrs={ 470 'texture': self._heart_tex, 471 'mesh_opaque': self._heart_mesh_opaque, 472 'mesh_transparent': self._heart_mesh_transparent, 473 'attach': 'topRight', 474 'scale': (90, 90), 475 'position': (-110 + l_offs, -50), 476 'color': (1, 0.2, 0.2), 477 }, 478 ) 479 ) 480 # FIXME; should not set things based on vr mode. 481 # (won't look right to non-vr connected clients, etc) 482 vrmode = bs.app.env.vr 483 self._lives_text = bs.NodeActor( 484 bs.newnode( 485 'text', 486 attrs={ 487 'v_attach': 'top', 488 'h_attach': 'right', 489 'h_align': 'center', 490 'color': (1, 1, 1, 1) if vrmode else (0.8, 0.8, 0.8, 1.0), 491 'flatness': 1.0 if vrmode else 0.5, 492 'shadow': 1.0 if vrmode else 0.5, 493 'vr_depth': 10, 494 'position': (-113 + l_offs, -69), 495 'scale': 1.3, 496 'text': str(self._lives), 497 }, 498 ) 499 ) 500 501 bs.timer(2.0, self._start_updating_waves) 502 503 def _handle_reached_end(self) -> None: 504 spaz = bs.getcollision().opposingnode.getdelegate(SpazBot, True) 505 if not spaz.is_alive(): 506 return # Ignore bodies flying in. 507 508 self._flawless = False 509 pos = spaz.node.position 510 self._bad_guy_score_sound.play(position=pos) 511 light = bs.newnode( 512 'light', attrs={'position': pos, 'radius': 0.5, 'color': (1, 0, 0)} 513 ) 514 bs.animate(light, 'intensity', {0.0: 0, 0.1: 1, 0.5: 0}, loop=False) 515 bs.timer(1.0, light.delete) 516 spaz.handlemessage( 517 bs.DieMessage(immediate=True, how=bs.DeathType.REACHED_GOAL) 518 ) 519 520 if self._lives > 0: 521 self._lives -= 1 522 if self._lives == 0: 523 self._bots.stop_moving() 524 self.continue_or_end_game() 525 assert self._lives_text is not None 526 assert self._lives_text.node 527 self._lives_text.node.text = str(self._lives) 528 delay = 0.0 529 530 def _safesetattr(node: bs.Node, attr: str, value: Any) -> None: 531 if node: 532 setattr(node, attr, value) 533 534 for _i in range(4): 535 bs.timer( 536 delay, 537 bs.Call( 538 _safesetattr, 539 self._lives_text.node, 540 'color', 541 (1, 0, 0, 1.0), 542 ), 543 ) 544 assert self._lives_bg is not None 545 assert self._lives_bg.node 546 bs.timer( 547 delay, 548 bs.Call(_safesetattr, self._lives_bg.node, 'opacity', 0.5), 549 ) 550 delay += 0.125 551 bs.timer( 552 delay, 553 bs.Call( 554 _safesetattr, 555 self._lives_text.node, 556 'color', 557 (1.0, 1.0, 0.0, 1.0), 558 ), 559 ) 560 bs.timer( 561 delay, 562 bs.Call(_safesetattr, self._lives_bg.node, 'opacity', 1.0), 563 ) 564 delay += 0.125 565 bs.timer( 566 delay, 567 bs.Call( 568 _safesetattr, 569 self._lives_text.node, 570 'color', 571 (0.8, 0.8, 0.8, 1.0), 572 ), 573 ) 574 575 def on_continue(self) -> None: 576 self._lives = 3 577 assert self._lives_text is not None 578 assert self._lives_text.node 579 self._lives_text.node.text = str(self._lives) 580 self._bots.start_moving() 581 582 def spawn_player(self, player: Player) -> bs.Actor: 583 pos = ( 584 self._spawn_center[0] + random.uniform(-1.5, 1.5), 585 self._spawn_center[1], 586 self._spawn_center[2] + random.uniform(-1.5, 1.5), 587 ) 588 spaz = self.spawn_player_spaz(player, position=pos) 589 if self._preset in {Preset.PRO_EASY, Preset.UBER_EASY}: 590 spaz.impact_scale = 0.25 591 592 # Add the material that causes us to hit the player-wall. 593 spaz.pick_up_powerup_callback = self._on_player_picked_up_powerup 594 return spaz 595 596 def _on_player_picked_up_powerup(self, player: bs.Actor) -> None: 597 del player # Unused. 598 self._player_has_picked_up_powerup = True 599 600 def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: 601 if poweruptype is None: 602 poweruptype = PowerupBoxFactory.get().get_random_powerup_type( 603 excludetypes=self._exclude_powerups 604 ) 605 PowerupBox( 606 position=self.map.powerup_spawn_points[index], 607 poweruptype=poweruptype, 608 ).autoretain() 609 610 def _start_powerup_drops(self) -> None: 611 bs.timer(3.0, self._drop_powerups, repeat=True) 612 613 def _drop_powerups( 614 self, standard_points: bool = False, force_first: str | None = None 615 ) -> None: 616 """Generic powerup drop.""" 617 618 # If its been a minute since our last wave finished emerging, stop 619 # giving out land-mine powerups. (prevents players from waiting 620 # around for them on purpose and filling the map up) 621 if bs.time() - self._last_wave_end_time > 60.0: 622 extra_excludes = ['land_mines'] 623 else: 624 extra_excludes = [] 625 626 if standard_points: 627 points = self.map.powerup_spawn_points 628 for i in range(len(points)): 629 bs.timer( 630 1.0 + i * 0.5, 631 bs.Call( 632 self._drop_powerup, i, force_first if i == 0 else None 633 ), 634 ) 635 else: 636 pos = ( 637 self._powerup_center[0] 638 + random.uniform( 639 -1.0 * self._powerup_spread[0], 640 1.0 * self._powerup_spread[0], 641 ), 642 self._powerup_center[1], 643 self._powerup_center[2] 644 + random.uniform( 645 -self._powerup_spread[1], self._powerup_spread[1] 646 ), 647 ) 648 649 # drop one random one somewhere.. 650 assert self._exclude_powerups is not None 651 PowerupBox( 652 position=pos, 653 poweruptype=PowerupBoxFactory.get().get_random_powerup_type( 654 excludetypes=self._exclude_powerups + extra_excludes 655 ), 656 ).autoretain() 657 658 def end_game(self) -> None: 659 bs.pushcall(bs.Call(self.do_end, 'defeat')) 660 bs.setmusic(None) 661 self._player_death_sound.play() 662 663 def do_end(self, outcome: str) -> None: 664 """End the game now with the provided outcome.""" 665 666 if outcome == 'defeat': 667 delay = 2.0 668 self.fade_to_red() 669 else: 670 delay = 0 671 672 score: int | None 673 if self._wavenum >= 2: 674 score = self._score 675 fail_message = None 676 else: 677 score = None 678 fail_message = bs.Lstr(resource='reachWave2Text') 679 680 self.end( 681 delay=delay, 682 results={ 683 'outcome': outcome, 684 'score': score, 685 'fail_message': fail_message, 686 'playerinfos': self.initialplayerinfos, 687 }, 688 ) 689 690 def _update_waves(self) -> None: 691 # pylint: disable=too-many-branches 692 693 # If we have no living bots, go to the next wave. 694 if ( 695 self._can_end_wave 696 and not self._bots.have_living_bots() 697 and not self._game_over 698 and self._lives > 0 699 ): 700 self._can_end_wave = False 701 self._time_bonus_timer = None 702 self._time_bonus_text = None 703 704 if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: 705 won = False 706 else: 707 assert self._waves is not None 708 won = self._wavenum == len(self._waves) 709 710 # Reward time bonus. 711 base_delay = 4.0 if won else 0 712 if self._time_bonus > 0: 713 bs.timer(0, self._cashregistersound.play) 714 bs.timer( 715 base_delay, 716 bs.Call(self._award_time_bonus, self._time_bonus), 717 ) 718 base_delay += 1.0 719 720 # Reward flawless bonus. 721 if self._wavenum > 0 and self._flawless: 722 bs.timer(base_delay, self._award_flawless_bonus) 723 base_delay += 1.0 724 725 self._flawless = True # reset 726 727 if won: 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 bs.Lstr(resource='victoryText'), scale=1.0, duration=4.0 751 ) 752 753 self.celebrate(10.0) 754 bs.timer(base_delay, self._award_lives_bonus) 755 base_delay += 1.0 756 bs.timer(base_delay, self._award_completion_bonus) 757 base_delay += 0.85 758 self._winsound.play() 759 bs.cameraflash() 760 bs.setmusic(bs.MusicType.VICTORY) 761 self._game_over = True 762 bs.timer(base_delay, bs.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 bs.timer(base_delay, self._start_next_wave) 772 773 def _award_completion_bonus(self) -> None: 774 bonus = 200 775 self._cashregistersound.play() 776 PopupText( 777 bs.Lstr( 778 value='+${A} ${B}', 779 subs=[ 780 ('${A}', str(bonus)), 781 ('${B}', bs.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 self._cashregistersound.play() 794 PopupText( 795 bs.Lstr( 796 value='+${A} ${B}', 797 subs=[ 798 ('${A}', str(bonus)), 799 ('${B}', bs.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 self._cashregistersound.play() 811 PopupText( 812 bs.Lstr( 813 value='+${A} ${B}', 814 subs=[ 815 ('${A}', str(bonus)), 816 ('${B}', bs.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 self._cashregistersound.play() 829 PopupText( 830 bs.Lstr( 831 value='+${A} ${B}', 832 subs=[ 833 ('${A}', str(self._flawless_bonus)), 834 ('${B}', bs.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 = bs.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 bs.Lstr( 858 value='${A} ${B}', 859 subs=[ 860 ('${A}', bs.Lstr(resource='waveText')), 861 ('${B}', str(self._wavenum)), 862 ], 863 ), 864 scale=1.0, 865 duration=1.0, 866 trail=True, 867 ) 868 bs.timer(0.4, self._new_wave_sound.play) 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 = bs.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 bs.timer(t_sec, tcall) 1072 t_sec += delay * 0.5 1073 1074 # We can end the wave after all the spawning happens. 1075 bs.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 = bs.Lstr( 1089 value='${A}: ${B}', 1090 subs=[ 1091 ('${A}', bs.Lstr(resource='timeBonusText')), 1092 ('${B}', str(int(self._time_bonus * self._time_bonus_mult))), 1093 ], 1094 ) 1095 self._time_bonus_text = bs.NodeActor( 1096 bs.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 bs.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 = bs.Time(bs.time() + t_sec) 1119 totalwaves = str(len(self._waves)) if self._waves is not None else '??' 1120 txtval = bs.Lstr( 1121 value='${A} ${B}', 1122 subs=[ 1123 ('${A}', bs.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 = bs.NodeActor( 1137 bs.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 # 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=bs.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 = bs.Lstr( 1190 value='${A}: ${B}', 1191 subs=[ 1192 ('${A}', bs.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 = bs.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 bs.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 bs.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 bs.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 bs.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 bs.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 bs.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 bs.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 bs.is_point_in_box(pos, boxes['b8']) 1279 and not bs.is_point_in_box(pos, boxes['b9']) 1280 ) or pos == (0.0, 0.0, 0.0): 1281 # Default to walking right if we're still in the walking area. 1282 bot.node.move_left_right = speed 1283 bot.node.move_up_down = 0 1284 bot.node.run = 0.0 1285 return True 1286 1287 # Revert to normal bot behavior otherwise.. 1288 return False 1289 1290 def handlemessage(self, msg: Any) -> Any: 1291 if isinstance(msg, bs.PlayerScoredMessage): 1292 self._score += msg.score 1293 self._update_scores() 1294 1295 elif isinstance(msg, bs.PlayerDiedMessage): 1296 # Augment standard behavior. 1297 super().handlemessage(msg) 1298 1299 self._a_player_has_been_killed = True 1300 1301 # Respawn them shortly. 1302 player = msg.getplayer(Player) 1303 assert self.initialplayerinfos is not None 1304 respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 1305 player.respawn_timer = bs.Timer( 1306 respawn_time, bs.Call(self.spawn_player_if_exists, player) 1307 ) 1308 player.respawn_icon = RespawnIcon(player, respawn_time) 1309 1310 elif isinstance(msg, SpazBotDiedMessage): 1311 if msg.how is bs.DeathType.REACHED_GOAL: 1312 return None 1313 pts, importance = msg.spazbot.get_death_points(msg.how) 1314 if msg.killerplayer is not None: 1315 target: Sequence[float] | None 1316 try: 1317 assert msg.spazbot is not None 1318 assert msg.spazbot.node 1319 target = msg.spazbot.node.position 1320 except Exception: 1321 logging.exception('Error getting SpazBotDied target.') 1322 target = None 1323 try: 1324 if msg.killerplayer: 1325 self.stats.player_scored( 1326 msg.killerplayer, 1327 pts, 1328 target=target, 1329 kill=True, 1330 screenmessage=False, 1331 importance=importance, 1332 ) 1333 dingsound = ( 1334 self._dingsound 1335 if importance == 1 1336 else self._dingsoundhigh 1337 ) 1338 dingsound.play(volume=0.6) 1339 except Exception: 1340 logging.exception('Error on SpazBotDiedMessage.') 1341 1342 # Normally we pull scores from the score-set, but if there's no 1343 # player lets be explicit. 1344 else: 1345 self._score += pts 1346 self._update_scores() 1347 1348 else: 1349 return super().handlemessage(msg) 1350 return None 1351 1352 def _get_bot_speed(self, bot_type: type[SpazBot]) -> float: 1353 speed = self._bot_speed_map.get(bot_type) 1354 if speed is None: 1355 raise TypeError( 1356 'Invalid bot type to _get_bot_speed(): ' + str(bot_type) 1357 ) 1358 return speed 1359 1360 def _set_can_end_wave(self) -> None: 1361 self._can_end_wave = True
Game involving trying to bomb bots as they walk through the map.
136 def __init__(self, settings: dict): 137 settings['map'] = 'Tower D' 138 super().__init__(settings) 139 shared = SharedObjects.get() 140 self._preset = Preset(settings.get('preset', 'pro')) 141 142 self._player_death_sound = bs.getsound('playerDeath') 143 self._new_wave_sound = bs.getsound('scoreHit01') 144 self._winsound = bs.getsound('score') 145 self._cashregistersound = bs.getsound('cashRegister') 146 self._bad_guy_score_sound = bs.getsound('shieldDown') 147 self._heart_tex = bs.gettexture('heart') 148 self._heart_mesh_opaque = bs.getmesh('heartOpaque') 149 self._heart_mesh_transparent = bs.getmesh('heartTransparent') 150 151 self._a_player_has_been_killed = False 152 self._spawn_center = self._map_type.defs.points['spawn1'][0:3] 153 self._tntspawnpos = self._map_type.defs.points['tnt_loc'][0:3] 154 self._powerup_center = self._map_type.defs.boxes['powerup_region'][0:3] 155 self._powerup_spread = ( 156 self._map_type.defs.boxes['powerup_region'][6] * 0.5, 157 self._map_type.defs.boxes['powerup_region'][8] * 0.5, 158 ) 159 160 self._score_region_material = bs.Material() 161 self._score_region_material.add_actions( 162 conditions=('they_have_material', shared.player_material), 163 actions=( 164 ('modify_part_collision', 'collide', True), 165 ('modify_part_collision', 'physical', False), 166 ('call', 'at_connect', self._handle_reached_end), 167 ), 168 ) 169 170 self._last_wave_end_time = bs.time() 171 self._player_has_picked_up_powerup = False 172 self._scoreboard: Scoreboard | None = None 173 self._game_over = False 174 self._wavenum = 0 175 self._can_end_wave = True 176 self._score = 0 177 self._time_bonus = 0 178 self._score_region: bs.Actor | None = None 179 self._dingsound = bs.getsound('dingSmall') 180 self._dingsoundhigh = bs.getsound('dingSmallHigh') 181 self._exclude_powerups: list[str] | None = None 182 self._have_tnt: bool | None = None 183 self._waves: list[Wave] | None = None 184 self._bots = SpazBotSet() 185 self._tntspawner: TNTSpawner | None = None 186 self._lives_bg: bs.NodeActor | None = None 187 self._start_lives = 10 188 self._lives = self._start_lives 189 self._lives_text: bs.NodeActor | None = None 190 self._flawless = True 191 self._time_bonus_timer: bs.Timer | None = None 192 self._time_bonus_text: bs.NodeActor | None = None 193 self._time_bonus_mult: float | None = None 194 self._wave_text: bs.NodeActor | None = None 195 self._flawless_bonus: int | None = None 196 self._wave_update_timer: bs.Timer | None = None
Instantiate the Activity.
198 def on_transition_in(self) -> None: 199 super().on_transition_in() 200 self._scoreboard = Scoreboard( 201 label=bs.Lstr(resource='scoreText'), score_split=0.5 202 ) 203 self._score_region = bs.NodeActor( 204 bs.newnode( 205 'region', 206 attrs={ 207 'position': self.map.defs.boxes['score_region'][0:3], 208 'scale': self.map.defs.boxes['score_region'][6:9], 209 'type': 'box', 210 'materials': [self._score_region_material], 211 }, 212 ) 213 )
Called when the Activity is first becoming visible.
Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.
215 def on_begin(self) -> None: 216 super().on_begin() 217 player_count = len(self.players) 218 hard = self._preset not in {Preset.PRO_EASY, Preset.UBER_EASY} 219 220 if self._preset in {Preset.PRO, Preset.PRO_EASY, Preset.TOURNAMENT}: 221 self._exclude_powerups = ['curse'] 222 self._have_tnt = True 223 self._waves = [ 224 Wave( 225 entries=[ 226 Spawn(BomberBot, path=3 if hard else 2), 227 Spawn(BomberBot, path=2), 228 Spawn(BomberBot, path=2) if hard else None, 229 Spawn(BomberBot, path=2) if player_count > 1 else None, 230 Spawn(BomberBot, path=1) if hard else None, 231 Spawn(BomberBot, path=1) if player_count > 2 else None, 232 Spawn(BomberBot, path=1) if player_count > 3 else None, 233 ] 234 ), 235 Wave( 236 entries=[ 237 Spawn(BomberBot, path=1) if hard else None, 238 Spawn(BomberBot, path=2) if hard else None, 239 Spawn(BomberBot, path=2), 240 Spawn(BomberBot, path=2), 241 Spawn(BomberBot, path=2) if player_count > 3 else None, 242 Spawn(BrawlerBot, path=3), 243 Spawn(BrawlerBot, path=3), 244 Spawn(BrawlerBot, path=3) if hard else None, 245 Spawn(BrawlerBot, path=3) if player_count > 1 else None, 246 Spawn(BrawlerBot, path=3) if player_count > 2 else None, 247 ] 248 ), 249 Wave( 250 entries=[ 251 Spawn(ChargerBot, path=2) if hard else None, 252 Spawn(ChargerBot, path=2) if player_count > 2 else None, 253 Spawn(TriggerBot, path=2), 254 Spawn(TriggerBot, path=2) if player_count > 1 else None, 255 Spacing(duration=3.0), 256 Spawn(BomberBot, path=2) if hard else None, 257 Spawn(BomberBot, path=2) if hard else None, 258 Spawn(BomberBot, path=2), 259 Spawn(BomberBot, path=3) if hard else None, 260 Spawn(BomberBot, path=3), 261 Spawn(BomberBot, path=3), 262 Spawn(BomberBot, path=3) if player_count > 3 else None, 263 ] 264 ), 265 Wave( 266 entries=[ 267 Spawn(TriggerBot, path=1) if hard else None, 268 Spacing(duration=1.0) if hard else None, 269 Spawn(TriggerBot, path=2), 270 Spacing(duration=1.0), 271 Spawn(TriggerBot, path=3), 272 Spacing(duration=1.0), 273 Spawn(TriggerBot, path=1) if hard else None, 274 Spacing(duration=1.0) if hard else None, 275 Spawn(TriggerBot, path=2), 276 Spacing(duration=1.0), 277 Spawn(TriggerBot, path=3), 278 Spacing(duration=1.0), 279 Spawn(TriggerBot, path=1) 280 if (player_count > 1 and hard) 281 else None, 282 Spacing(duration=1.0), 283 Spawn(TriggerBot, path=2) if player_count > 2 else None, 284 Spacing(duration=1.0), 285 Spawn(TriggerBot, path=3) if player_count > 3 else None, 286 Spacing(duration=1.0), 287 ] 288 ), 289 Wave( 290 entries=[ 291 Spawn( 292 ChargerBotProShielded if hard else ChargerBot, 293 path=1, 294 ), 295 Spawn(BrawlerBot, path=2) if hard else None, 296 Spawn(BrawlerBot, path=2), 297 Spawn(BrawlerBot, path=2), 298 Spawn(BrawlerBot, path=3) if hard else None, 299 Spawn(BrawlerBot, path=3), 300 Spawn(BrawlerBot, path=3), 301 Spawn(BrawlerBot, path=3) if player_count > 1 else None, 302 Spawn(BrawlerBot, path=3) if player_count > 2 else None, 303 Spawn(BrawlerBot, path=3) if player_count > 3 else None, 304 ] 305 ), 306 Wave( 307 entries=[ 308 Spawn(BomberBotProShielded, path=3), 309 Spacing(duration=1.5), 310 Spawn(BomberBotProShielded, path=2), 311 Spacing(duration=1.5), 312 Spawn(BomberBotProShielded, path=1) if hard else None, 313 Spacing(duration=1.0) if hard else None, 314 Spawn(BomberBotProShielded, path=3), 315 Spacing(duration=1.5), 316 Spawn(BomberBotProShielded, path=2), 317 Spacing(duration=1.5), 318 Spawn(BomberBotProShielded, path=1) if hard else None, 319 Spacing(duration=1.5) if hard else None, 320 Spawn(BomberBotProShielded, path=3) 321 if player_count > 1 322 else None, 323 Spacing(duration=1.5), 324 Spawn(BomberBotProShielded, path=2) 325 if player_count > 2 326 else None, 327 Spacing(duration=1.5), 328 Spawn(BomberBotProShielded, path=1) 329 if player_count > 3 330 else None, 331 ] 332 ), 333 ] 334 elif self._preset in { 335 Preset.UBER_EASY, 336 Preset.UBER, 337 Preset.TOURNAMENT_UBER, 338 }: 339 self._exclude_powerups = [] 340 self._have_tnt = True 341 self._waves = [ 342 Wave( 343 entries=[ 344 Spawn(TriggerBot, path=1) if hard else None, 345 Spawn(TriggerBot, path=2), 346 Spawn(TriggerBot, path=2), 347 Spawn(TriggerBot, path=3), 348 Spawn( 349 BrawlerBotPro if hard else BrawlerBot, 350 point=Point.BOTTOM_LEFT, 351 ), 352 Spawn(BrawlerBotPro, point=Point.BOTTOM_RIGHT) 353 if player_count > 2 354 else None, 355 ] 356 ), 357 Wave( 358 entries=[ 359 Spawn(ChargerBot, path=2), 360 Spawn(ChargerBot, path=3), 361 Spawn(ChargerBot, path=1) if hard else None, 362 Spawn(ChargerBot, path=2), 363 Spawn(ChargerBot, path=3), 364 Spawn(ChargerBot, path=1) if player_count > 2 else None, 365 ] 366 ), 367 Wave( 368 entries=[ 369 Spawn(BomberBotProShielded, path=1) if hard else None, 370 Spawn(BomberBotProShielded, path=2), 371 Spawn(BomberBotProShielded, path=2), 372 Spawn(BomberBotProShielded, path=3), 373 Spawn(BomberBotProShielded, path=3), 374 Spawn(ChargerBot, point=Point.BOTTOM_RIGHT), 375 Spawn(ChargerBot, point=Point.BOTTOM_LEFT) 376 if player_count > 2 377 else None, 378 ] 379 ), 380 Wave( 381 entries=[ 382 Spawn(TriggerBotPro, path=1) if hard else None, 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 Spawn(TriggerBotPro, path=1 if hard else 2) 389 if player_count > 1 390 else None, 391 Spawn(TriggerBotPro, path=1 if hard else 2) 392 if player_count > 3 393 else None, 394 ] 395 ), 396 Wave( 397 entries=[ 398 Spawn( 399 TriggerBotProShielded if hard else TriggerBotPro, 400 point=Point.BOTTOM_LEFT, 401 ), 402 Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT) 403 if hard 404 else None, 405 Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT) 406 if player_count > 2 407 else None, 408 Spawn(BomberBot, path=3), 409 Spawn(BomberBot, path=3), 410 Spacing(duration=5.0), 411 Spawn(BrawlerBot, path=2), 412 Spawn(BrawlerBot, path=2), 413 Spacing(duration=5.0), 414 Spawn(TriggerBot, path=1) if hard else None, 415 Spawn(TriggerBot, path=1) if hard else None, 416 ] 417 ), 418 Wave( 419 entries=[ 420 Spawn(BomberBotProShielded, path=2), 421 Spawn(BomberBotProShielded, path=2) if hard else None, 422 Spawn(StickyBot, point=Point.BOTTOM_RIGHT), 423 Spawn(BomberBotProShielded, path=2), 424 Spawn(BomberBotProShielded, path=2), 425 Spawn(StickyBot, point=Point.BOTTOM_RIGHT) 426 if player_count > 2 427 else None, 428 Spawn(BomberBotProShielded, path=2), 429 Spawn(ExplodeyBot, point=Point.BOTTOM_LEFT), 430 Spawn(BomberBotProShielded, path=2), 431 Spawn(BomberBotProShielded, path=2) 432 if player_count > 1 433 else None, 434 Spacing(duration=5.0), 435 Spawn(StickyBot, point=Point.BOTTOM_LEFT), 436 Spacing(duration=2.0), 437 Spawn(ExplodeyBot, point=Point.BOTTOM_RIGHT), 438 ] 439 ), 440 ] 441 elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: 442 self._exclude_powerups = [] 443 self._have_tnt = True 444 445 # Spit out a few powerups and start dropping more shortly. 446 self._drop_powerups(standard_points=True) 447 bs.timer(4.0, self._start_powerup_drops) 448 self.setup_low_life_warning_sound() 449 self._update_scores() 450 451 # Our TNT spawner (if applicable). 452 if self._have_tnt: 453 self._tntspawner = TNTSpawner(position=self._tntspawnpos) 454 455 # Make sure to stay out of the way of menu/party buttons in the corner. 456 assert bs.app.classic is not None 457 uiscale = bs.app.ui_v1.uiscale 458 l_offs = ( 459 -80 460 if uiscale is bs.UIScale.SMALL 461 else -40 462 if uiscale is bs.UIScale.MEDIUM 463 else 0 464 ) 465 466 self._lives_bg = bs.NodeActor( 467 bs.newnode( 468 'image', 469 attrs={ 470 'texture': self._heart_tex, 471 'mesh_opaque': self._heart_mesh_opaque, 472 'mesh_transparent': self._heart_mesh_transparent, 473 'attach': 'topRight', 474 'scale': (90, 90), 475 'position': (-110 + l_offs, -50), 476 'color': (1, 0.2, 0.2), 477 }, 478 ) 479 ) 480 # FIXME; should not set things based on vr mode. 481 # (won't look right to non-vr connected clients, etc) 482 vrmode = bs.app.env.vr 483 self._lives_text = bs.NodeActor( 484 bs.newnode( 485 'text', 486 attrs={ 487 'v_attach': 'top', 488 'h_attach': 'right', 489 'h_align': 'center', 490 'color': (1, 1, 1, 1) if vrmode else (0.8, 0.8, 0.8, 1.0), 491 'flatness': 1.0 if vrmode else 0.5, 492 'shadow': 1.0 if vrmode else 0.5, 493 'vr_depth': 10, 494 'position': (-113 + l_offs, -69), 495 'scale': 1.3, 496 'text': str(self._lives), 497 }, 498 ) 499 ) 500 501 bs.timer(2.0, self._start_updating_waves)
Called once the previous Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
575 def on_continue(self) -> None: 576 self._lives = 3 577 assert self._lives_text is not None 578 assert self._lives_text.node 579 self._lives_text.node.text = str(self._lives) 580 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.
582 def spawn_player(self, player: Player) -> bs.Actor: 583 pos = ( 584 self._spawn_center[0] + random.uniform(-1.5, 1.5), 585 self._spawn_center[1], 586 self._spawn_center[2] + random.uniform(-1.5, 1.5), 587 ) 588 spaz = self.spawn_player_spaz(player, position=pos) 589 if self._preset in {Preset.PRO_EASY, Preset.UBER_EASY}: 590 spaz.impact_scale = 0.25 591 592 # Add the material that causes us to hit the player-wall. 593 spaz.pick_up_powerup_callback = self._on_player_picked_up_powerup 594 return spaz
Spawn something for the provided bascenev1.Player.
The default implementation simply calls spawn_player_spaz().
658 def end_game(self) -> None: 659 bs.pushcall(bs.Call(self.do_end, 'defeat')) 660 bs.setmusic(None) 661 self._player_death_sound.play()
Tell the game to wrap up and call bascenev1.Activity.end().
This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.
663 def do_end(self, outcome: str) -> None: 664 """End the game now with the provided outcome.""" 665 666 if outcome == 'defeat': 667 delay = 2.0 668 self.fade_to_red() 669 else: 670 delay = 0 671 672 score: int | None 673 if self._wavenum >= 2: 674 score = self._score 675 fail_message = None 676 else: 677 score = None 678 fail_message = bs.Lstr(resource='reachWave2Text') 679 680 self.end( 681 delay=delay, 682 results={ 683 'outcome': outcome, 684 'score': score, 685 'fail_message': fail_message, 686 'playerinfos': self.initialplayerinfos, 687 }, 688 )
End the game now with the provided outcome.
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=bs.Call(self._on_bot_spawn, path), 1182 )
Add the given type bot with the given delay (in seconds).
1290 def handlemessage(self, msg: Any) -> Any: 1291 if isinstance(msg, bs.PlayerScoredMessage): 1292 self._score += msg.score 1293 self._update_scores() 1294 1295 elif isinstance(msg, bs.PlayerDiedMessage): 1296 # Augment standard behavior. 1297 super().handlemessage(msg) 1298 1299 self._a_player_has_been_killed = True 1300 1301 # Respawn them shortly. 1302 player = msg.getplayer(Player) 1303 assert self.initialplayerinfos is not None 1304 respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 1305 player.respawn_timer = bs.Timer( 1306 respawn_time, bs.Call(self.spawn_player_if_exists, player) 1307 ) 1308 player.respawn_icon = RespawnIcon(player, respawn_time) 1309 1310 elif isinstance(msg, SpazBotDiedMessage): 1311 if msg.how is bs.DeathType.REACHED_GOAL: 1312 return None 1313 pts, importance = msg.spazbot.get_death_points(msg.how) 1314 if msg.killerplayer is not None: 1315 target: Sequence[float] | None 1316 try: 1317 assert msg.spazbot is not None 1318 assert msg.spazbot.node 1319 target = msg.spazbot.node.position 1320 except Exception: 1321 logging.exception('Error getting SpazBotDied target.') 1322 target = None 1323 try: 1324 if msg.killerplayer: 1325 self.stats.player_scored( 1326 msg.killerplayer, 1327 pts, 1328 target=target, 1329 kill=True, 1330 screenmessage=False, 1331 importance=importance, 1332 ) 1333 dingsound = ( 1334 self._dingsound 1335 if importance == 1 1336 else self._dingsoundhigh 1337 ) 1338 dingsound.play(volume=0.6) 1339 except Exception: 1340 logging.exception('Error on SpazBotDiedMessage.') 1341 1342 # Normally we pull scores from the score-set, but if there's no 1343 # player lets be explicit. 1344 else: 1345 self._score += pts 1346 self._update_scores() 1347 1348 else: 1349 return super().handlemessage(msg) 1350 return None
General message handling; can be passed any message object.
Inherited Members
- bascenev1._coopgame.CoopGameActivity
- session
- supports_session_type
- get_score_type
- celebrate
- spawn_player_spaz
- fade_to_red
- setup_low_life_warning_sound
- bascenev1._gameactivity.GameActivity
- available_settings
- scoreconfig
- allow_pausing
- allow_kick_idle_players
- show_kill_points
- create_settings_ui
- getscoreconfig
- getname
- get_display_string
- get_team_display_string
- get_description
- get_description_display_string
- get_available_settings
- get_supported_maps
- get_settings_display_string
- initialplayerinfos
- 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
- bascenev1._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
- paused_text
- preloads
- lobby
- context
- 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
- bascenev1._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps