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