bastd.game.onslaught
Provides Onslaught Co-op game.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides Onslaught Co-op game.""" 4 5# Yes this is a long one.. 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 math 14import random 15from enum import Enum, unique 16from dataclasses import dataclass 17from typing import TYPE_CHECKING 18 19import ba 20from bastd.actor.popuptext import PopupText 21from bastd.actor.bomb import TNTSpawner 22from bastd.actor.playerspaz import PlayerSpazHurtMessage 23from bastd.actor.scoreboard import Scoreboard 24from bastd.actor.controlsguide import ControlsGuide 25from bastd.actor.powerupbox import PowerupBox, PowerupBoxFactory 26from bastd.actor.spazbot import ( 27 SpazBotDiedMessage, SpazBotSet, ChargerBot, StickyBot, BomberBot, 28 BomberBotLite, BrawlerBot, BrawlerBotLite, TriggerBot, BomberBotStaticLite, 29 TriggerBotStatic, BomberBotProStatic, TriggerBotPro, ExplodeyBot, 30 BrawlerBotProShielded, ChargerBotProShielded, BomberBotPro, 31 TriggerBotProShielded, BrawlerBotPro, BomberBotProShielded) 32 33if TYPE_CHECKING: 34 from typing import Any, Sequence 35 from bastd.actor.spazbot import SpazBot 36 37 38@dataclass 39class Wave: 40 """A wave of enemies.""" 41 entries: list[Spawn | Spacing | Delay | None] 42 base_angle: float = 0.0 43 44 45@dataclass 46class Spawn: 47 """A bot spawn event in a wave.""" 48 bottype: type[SpazBot] | str 49 point: Point | None = None 50 spacing: float = 5.0 51 52 53@dataclass 54class Spacing: 55 """Empty space in a wave.""" 56 spacing: float = 5.0 57 58 59@dataclass 60class Delay: 61 """A delay between events in a wave.""" 62 duration: float 63 64 65class Preset(Enum): 66 """Game presets we support.""" 67 TRAINING = 'training' 68 TRAINING_EASY = 'training_easy' 69 ROOKIE = 'rookie' 70 ROOKIE_EASY = 'rookie_easy' 71 PRO = 'pro' 72 PRO_EASY = 'pro_easy' 73 UBER = 'uber' 74 UBER_EASY = 'uber_easy' 75 ENDLESS = 'endless' 76 ENDLESS_TOURNAMENT = 'endless_tournament' 77 78 79@unique 80class Point(Enum): 81 """Points on the map we can spawn at.""" 82 LEFT_UPPER_MORE = 'bot_spawn_left_upper_more' 83 LEFT_UPPER = 'bot_spawn_left_upper' 84 TURRET_TOP_RIGHT = 'bot_spawn_turret_top_right' 85 RIGHT_UPPER = 'bot_spawn_right_upper' 86 TURRET_TOP_MIDDLE_LEFT = 'bot_spawn_turret_top_middle_left' 87 TURRET_TOP_MIDDLE_RIGHT = 'bot_spawn_turret_top_middle_right' 88 TURRET_TOP_LEFT = 'bot_spawn_turret_top_left' 89 TOP_RIGHT = 'bot_spawn_top_right' 90 TOP_LEFT = 'bot_spawn_top_left' 91 TOP = 'bot_spawn_top' 92 BOTTOM = 'bot_spawn_bottom' 93 LEFT = 'bot_spawn_left' 94 RIGHT = 'bot_spawn_right' 95 RIGHT_UPPER_MORE = 'bot_spawn_right_upper_more' 96 RIGHT_LOWER = 'bot_spawn_right_lower' 97 RIGHT_LOWER_MORE = 'bot_spawn_right_lower_more' 98 BOTTOM_RIGHT = 'bot_spawn_bottom_right' 99 BOTTOM_LEFT = 'bot_spawn_bottom_left' 100 TURRET_BOTTOM_RIGHT = 'bot_spawn_turret_bottom_right' 101 TURRET_BOTTOM_LEFT = 'bot_spawn_turret_bottom_left' 102 LEFT_LOWER = 'bot_spawn_left_lower' 103 LEFT_LOWER_MORE = 'bot_spawn_left_lower_more' 104 TURRET_TOP_MIDDLE = 'bot_spawn_turret_top_middle' 105 BOTTOM_HALF_RIGHT = 'bot_spawn_bottom_half_right' 106 BOTTOM_HALF_LEFT = 'bot_spawn_bottom_half_left' 107 TOP_HALF_RIGHT = 'bot_spawn_top_half_right' 108 TOP_HALF_LEFT = 'bot_spawn_top_half_left' 109 110 111class Player(ba.Player['Team']): 112 """Our player type for this game.""" 113 114 def __init__(self) -> None: 115 self.has_been_hurt = False 116 self.respawn_wave = 0 117 118 119class Team(ba.Team[Player]): 120 """Our team type for this game.""" 121 122 123class OnslaughtGame(ba.CoopGameActivity[Player, Team]): 124 """Co-op game where players try to survive attacking waves of enemies.""" 125 126 name = 'Onslaught' 127 description = 'Defeat all enemies.' 128 129 tips: list[str | ba.GameTip] = [ 130 'Hold any button to run.' 131 ' (Trigger buttons work well if you have them)', 132 'Try tricking enemies into killing eachother or running off cliffs.', 133 'Try \'Cooking off\' bombs for a second or two before throwing them.', 134 'It\'s easier to win with a friend or two helping.', 135 'If you stay in one place, you\'re toast. Run and dodge to survive..', 136 'Practice using your momentum to throw bombs more accurately.', 137 'Your punches do much more damage if you are running or spinning.' 138 ] 139 140 # Show messages when players die since it matters here. 141 announce_player_deaths = True 142 143 def __init__(self, settings: dict): 144 145 self._preset = Preset(settings.get('preset', 'training')) 146 if self._preset in { 147 Preset.TRAINING, Preset.TRAINING_EASY, Preset.PRO, 148 Preset.PRO_EASY, Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT 149 }: 150 settings['map'] = 'Doom Shroom' 151 else: 152 settings['map'] = 'Courtyard' 153 154 super().__init__(settings) 155 156 self._new_wave_sound = ba.getsound('scoreHit01') 157 self._winsound = ba.getsound('score') 158 self._cashregistersound = ba.getsound('cashRegister') 159 self._a_player_has_been_hurt = False 160 self._player_has_dropped_bomb = False 161 162 # FIXME: should use standard map defs. 163 if settings['map'] == 'Doom Shroom': 164 self._spawn_center = (0, 3, -5) 165 self._tntspawnpos = (0.0, 3.0, -5.0) 166 self._powerup_center = (0, 5, -3.6) 167 self._powerup_spread = (6.0, 4.0) 168 elif settings['map'] == 'Courtyard': 169 self._spawn_center = (0, 3, -2) 170 self._tntspawnpos = (0.0, 3.0, 2.1) 171 self._powerup_center = (0, 5, -1.6) 172 self._powerup_spread = (4.6, 2.7) 173 else: 174 raise Exception('Unsupported map: ' + str(settings['map'])) 175 self._scoreboard: Scoreboard | None = None 176 self._game_over = False 177 self._wavenum = 0 178 self._can_end_wave = True 179 self._score = 0 180 self._time_bonus = 0 181 self._spawn_info_text: ba.NodeActor | None = None 182 self._dingsound = ba.getsound('dingSmall') 183 self._dingsoundhigh = ba.getsound('dingSmallHigh') 184 self._have_tnt = False 185 self._excluded_powerups: list[str] | None = None 186 self._waves: list[Wave] = [] 187 self._tntspawner: TNTSpawner | None = None 188 self._bots: SpazBotSet | None = None 189 self._powerup_drop_timer: ba.Timer | None = None 190 self._time_bonus_timer: ba.Timer | None = None 191 self._time_bonus_text: ba.NodeActor | None = None 192 self._flawless_bonus: int | None = None 193 self._wave_text: ba.NodeActor | None = None 194 self._wave_update_timer: ba.Timer | None = None 195 self._throw_off_kills = 0 196 self._land_mine_kills = 0 197 self._tnt_kills = 0 198 199 def on_transition_in(self) -> None: 200 super().on_transition_in() 201 customdata = ba.getsession().customdata 202 203 # Show special landmine tip on rookie preset. 204 if self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: 205 # Show once per session only (then we revert to regular tips). 206 if not customdata.get('_showed_onslaught_landmine_tip', False): 207 customdata['_showed_onslaught_landmine_tip'] = True 208 self.tips = [ 209 ba.GameTip( 210 'Land-mines are a good way to stop speedy enemies.', 211 icon=ba.gettexture('powerupLandMines'), 212 sound=ba.getsound('ding')) 213 ] 214 215 # Show special tnt tip on pro preset. 216 if self._preset in {Preset.PRO, Preset.PRO_EASY}: 217 # Show once per session only (then we revert to regular tips). 218 if not customdata.get('_showed_onslaught_tnt_tip', False): 219 customdata['_showed_onslaught_tnt_tip'] = True 220 self.tips = [ 221 ba.GameTip( 222 'Take out a group of enemies by\n' 223 'setting off a bomb near a TNT box.', 224 icon=ba.gettexture('tnt'), 225 sound=ba.getsound('ding')) 226 ] 227 228 # Show special curse tip on uber preset. 229 if self._preset in {Preset.UBER, Preset.UBER_EASY}: 230 # Show once per session only (then we revert to regular tips). 231 if not customdata.get('_showed_onslaught_curse_tip', False): 232 customdata['_showed_onslaught_curse_tip'] = True 233 self.tips = [ 234 ba.GameTip( 235 'Curse boxes turn you into a ticking time bomb.\n' 236 'The only cure is to quickly grab a health-pack.', 237 icon=ba.gettexture('powerupCurse'), 238 sound=ba.getsound('ding')) 239 ] 240 241 self._spawn_info_text = ba.NodeActor( 242 ba.newnode('text', 243 attrs={ 244 'position': (15, -130), 245 'h_attach': 'left', 246 'v_attach': 'top', 247 'scale': 0.55, 248 'color': (0.3, 0.8, 0.3, 1.0), 249 'text': '' 250 })) 251 ba.setmusic(ba.MusicType.ONSLAUGHT) 252 253 self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), 254 score_split=0.5) 255 256 def on_begin(self) -> None: 257 super().on_begin() 258 player_count = len(self.players) 259 hard = self._preset not in { 260 Preset.TRAINING_EASY, Preset.ROOKIE_EASY, Preset.PRO_EASY, 261 Preset.UBER_EASY 262 } 263 if self._preset in {Preset.TRAINING, Preset.TRAINING_EASY}: 264 ControlsGuide(delay=3.0, lifespan=10.0, bright=True).autoretain() 265 266 self._have_tnt = False 267 self._excluded_powerups = ['curse', 'land_mines'] 268 self._waves = [ 269 Wave(base_angle=195, 270 entries=[ 271 Spawn(BomberBotLite, spacing=5), 272 ] * player_count), 273 Wave(base_angle=130, 274 entries=[ 275 Spawn(BrawlerBotLite, spacing=5), 276 ] * player_count), 277 Wave(base_angle=195, 278 entries=[Spawn(BomberBotLite, spacing=10)] * 279 (player_count + 1)), 280 Wave(base_angle=130, 281 entries=[ 282 Spawn(BrawlerBotLite, spacing=10), 283 ] * (player_count + 1)), 284 Wave(base_angle=130, 285 entries=[ 286 Spawn(BrawlerBotLite, spacing=5) 287 if player_count > 1 else None, 288 Spawn(BrawlerBotLite, spacing=5), 289 Spacing(30), 290 Spawn(BomberBotLite, spacing=5) 291 if player_count > 3 else None, 292 Spawn(BomberBotLite, spacing=5), 293 Spacing(30), 294 Spawn(BrawlerBotLite, spacing=5), 295 Spawn(BrawlerBotLite, spacing=5) 296 if player_count > 2 else None, 297 ]), 298 Wave(base_angle=195, 299 entries=[ 300 Spawn(TriggerBot, spacing=90), 301 Spawn(TriggerBot, spacing=90) 302 if player_count > 1 else None, 303 ]), 304 ] 305 306 elif self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: 307 self._have_tnt = False 308 self._excluded_powerups = ['curse'] 309 self._waves = [ 310 Wave(entries=[ 311 Spawn(ChargerBot, Point.LEFT_UPPER_MORE 312 ) if player_count > 2 else None, 313 Spawn(ChargerBot, Point.LEFT_UPPER), 314 ]), 315 Wave(entries=[ 316 Spawn(BomberBotStaticLite, Point.TURRET_TOP_RIGHT), 317 Spawn(BrawlerBotLite, Point.RIGHT_UPPER), 318 Spawn(BrawlerBotLite, Point.RIGHT_LOWER 319 ) if player_count > 1 else None, 320 Spawn(BomberBotStaticLite, Point.TURRET_BOTTOM_RIGHT 321 ) if player_count > 2 else None, 322 ]), 323 Wave(entries=[ 324 Spawn(BomberBotStaticLite, Point.TURRET_BOTTOM_LEFT), 325 Spawn(TriggerBot, Point.LEFT), 326 Spawn(TriggerBot, Point.LEFT_LOWER 327 ) if player_count > 1 else None, 328 Spawn(TriggerBot, Point.LEFT_UPPER 329 ) if player_count > 2 else None, 330 ]), 331 Wave(entries=[ 332 Spawn(BrawlerBotLite, Point.TOP_RIGHT), 333 Spawn(BrawlerBot, Point.TOP_HALF_RIGHT 334 ) if player_count > 1 else None, 335 Spawn(BrawlerBotLite, Point.TOP_LEFT), 336 Spawn(BrawlerBotLite, Point.TOP_HALF_LEFT 337 ) if player_count > 2 else None, 338 Spawn(BrawlerBot, Point.TOP), 339 Spawn(BomberBotStaticLite, Point.TURRET_TOP_MIDDLE), 340 ]), 341 Wave(entries=[ 342 Spawn(TriggerBotStatic, Point.TURRET_BOTTOM_LEFT), 343 Spawn(TriggerBotStatic, Point.TURRET_BOTTOM_RIGHT), 344 Spawn(TriggerBot, Point.BOTTOM), 345 Spawn(TriggerBot, Point.BOTTOM_HALF_RIGHT 346 ) if player_count > 1 else None, 347 Spawn(TriggerBot, Point.BOTTOM_HALF_LEFT 348 ) if player_count > 2 else None, 349 ]), 350 Wave(entries=[ 351 Spawn(BomberBotStaticLite, Point.TURRET_TOP_LEFT), 352 Spawn(BomberBotStaticLite, Point.TURRET_TOP_RIGHT), 353 Spawn(ChargerBot, Point.BOTTOM), 354 Spawn(ChargerBot, Point.BOTTOM_HALF_LEFT 355 ) if player_count > 1 else None, 356 Spawn(ChargerBot, Point.BOTTOM_HALF_RIGHT 357 ) if player_count > 2 else None, 358 ]), 359 ] 360 361 elif self._preset in {Preset.PRO, Preset.PRO_EASY}: 362 self._excluded_powerups = ['curse'] 363 self._have_tnt = True 364 self._waves = [ 365 Wave(base_angle=-50, 366 entries=[ 367 Spawn(BrawlerBot, spacing=12) 368 if player_count > 3 else None, 369 Spawn(BrawlerBot, spacing=12), 370 Spawn(BomberBot, spacing=6), 371 Spawn(BomberBot, spacing=6) 372 if self._preset is Preset.PRO else None, 373 Spawn(BomberBot, spacing=6) 374 if player_count > 1 else None, 375 Spawn(BrawlerBot, spacing=12), 376 Spawn(BrawlerBot, spacing=12) 377 if player_count > 2 else None, 378 ]), 379 Wave(base_angle=180, 380 entries=[ 381 Spawn(BrawlerBot, spacing=6) 382 if player_count > 3 else None, 383 Spawn(BrawlerBot, spacing=6) 384 if self._preset is Preset.PRO else None, 385 Spawn(BrawlerBot, spacing=6), 386 Spawn(ChargerBot, spacing=45), 387 Spawn(ChargerBot, spacing=45) 388 if player_count > 1 else None, 389 Spawn(BrawlerBot, spacing=6), 390 Spawn(BrawlerBot, spacing=6) 391 if self._preset is Preset.PRO else None, 392 Spawn(BrawlerBot, spacing=6) 393 if player_count > 2 else None, 394 ]), 395 Wave(base_angle=0, 396 entries=[ 397 Spawn(ChargerBot, spacing=30), 398 Spawn(TriggerBot, spacing=30), 399 Spawn(TriggerBot, spacing=30), 400 Spawn(TriggerBot, spacing=30) 401 if self._preset is Preset.PRO else None, 402 Spawn(TriggerBot, spacing=30) 403 if player_count > 1 else None, 404 Spawn(TriggerBot, spacing=30) 405 if player_count > 3 else None, 406 Spawn(ChargerBot, spacing=30), 407 ]), 408 Wave(base_angle=90, 409 entries=[ 410 Spawn(StickyBot, spacing=50), 411 Spawn(StickyBot, spacing=50) 412 if self._preset is Preset.PRO else None, 413 Spawn(StickyBot, spacing=50), 414 Spawn(StickyBot, spacing=50) 415 if player_count > 1 else None, 416 Spawn(StickyBot, spacing=50) 417 if player_count > 3 else None, 418 ]), 419 Wave(base_angle=0, 420 entries=[ 421 Spawn(TriggerBot, spacing=72), 422 Spawn(TriggerBot, spacing=72), 423 Spawn(TriggerBot, spacing=72) 424 if self._preset is Preset.PRO else None, 425 Spawn(TriggerBot, spacing=72), 426 Spawn(TriggerBot, spacing=72), 427 Spawn(TriggerBot, spacing=36) 428 if player_count > 2 else None, 429 ]), 430 Wave(base_angle=30, 431 entries=[ 432 Spawn(ChargerBotProShielded, spacing=50), 433 Spawn(ChargerBotProShielded, spacing=50), 434 Spawn(ChargerBotProShielded, spacing=50) 435 if self._preset is Preset.PRO else None, 436 Spawn(ChargerBotProShielded, spacing=50) 437 if player_count > 1 else None, 438 Spawn(ChargerBotProShielded, spacing=50) 439 if player_count > 2 else None, 440 ]) 441 ] 442 443 elif self._preset in {Preset.UBER, Preset.UBER_EASY}: 444 445 # Show controls help in demo/arcade modes. 446 if ba.app.demo_mode or ba.app.arcade_mode: 447 ControlsGuide(delay=3.0, lifespan=10.0, 448 bright=True).autoretain() 449 450 self._have_tnt = True 451 self._excluded_powerups = [] 452 self._waves = [ 453 Wave(entries=[ 454 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_LEFT 455 ) if hard else None, 456 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_RIGHT), 457 Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT 458 ) if player_count > 2 else None, 459 Spawn(ExplodeyBot, Point.TOP_RIGHT), 460 Delay(4.0), 461 Spawn(ExplodeyBot, Point.TOP_LEFT), 462 ]), 463 Wave(entries=[ 464 Spawn(ChargerBot, Point.LEFT), 465 Spawn(ChargerBot, Point.RIGHT), 466 Spawn(ChargerBot, Point.RIGHT_UPPER_MORE 467 ) if player_count > 2 else None, 468 Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT), 469 Spawn(BomberBotProStatic, Point.TURRET_TOP_RIGHT), 470 ]), 471 Wave(entries=[ 472 Spawn(TriggerBotPro, Point.TOP_RIGHT), 473 Spawn(TriggerBotPro, Point.RIGHT_UPPER_MORE 474 ) if player_count > 1 else None, 475 Spawn(TriggerBotPro, Point.RIGHT_UPPER), 476 Spawn(TriggerBotPro, Point.RIGHT_LOWER) if hard else None, 477 Spawn(TriggerBotPro, Point.RIGHT_LOWER_MORE 478 ) if player_count > 2 else None, 479 Spawn(TriggerBotPro, Point.BOTTOM_RIGHT), 480 ]), 481 Wave(entries=[ 482 Spawn(ChargerBotProShielded, Point.BOTTOM_RIGHT), 483 Spawn(ChargerBotProShielded, Point.BOTTOM 484 ) if player_count > 2 else None, 485 Spawn(ChargerBotProShielded, Point.BOTTOM_LEFT), 486 Spawn(ChargerBotProShielded, Point.TOP) if hard else None, 487 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE), 488 ]), 489 Wave(entries=[ 490 Spawn(ExplodeyBot, Point.LEFT_UPPER), 491 Delay(1.0), 492 Spawn(BrawlerBotProShielded, Point.LEFT_LOWER), 493 Spawn(BrawlerBotProShielded, Point.LEFT_LOWER_MORE), 494 Delay(4.0), 495 Spawn(ExplodeyBot, Point.RIGHT_UPPER), 496 Delay(1.0), 497 Spawn(BrawlerBotProShielded, Point.RIGHT_LOWER), 498 Spawn(BrawlerBotProShielded, Point.RIGHT_UPPER_MORE), 499 Delay(4.0), 500 Spawn(ExplodeyBot, Point.LEFT), 501 Delay(5.0), 502 Spawn(ExplodeyBot, Point.RIGHT), 503 ]), 504 Wave(entries=[ 505 Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT), 506 Spawn(BomberBotProStatic, Point.TURRET_TOP_RIGHT), 507 Spawn(BomberBotProStatic, Point.TURRET_BOTTOM_LEFT), 508 Spawn(BomberBotProStatic, Point.TURRET_BOTTOM_RIGHT), 509 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_LEFT 510 ) if hard else None, 511 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_RIGHT 512 ) if hard else None, 513 ]) 514 ] 515 516 # We generate these on the fly in endless. 517 elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: 518 self._have_tnt = True 519 self._excluded_powerups = [] 520 self._waves = [] 521 522 else: 523 raise RuntimeError(f'Invalid preset: {self._preset}') 524 525 # FIXME: Should migrate to use setup_standard_powerup_drops(). 526 527 # Spit out a few powerups and start dropping more shortly. 528 self._drop_powerups(standard_points=True, 529 poweruptype='curse' if self._preset 530 in [Preset.UBER, Preset.UBER_EASY] else 531 ('land_mines' if self._preset 532 in [Preset.ROOKIE, Preset.ROOKIE_EASY] else None)) 533 ba.timer(4.0, self._start_powerup_drops) 534 535 # Our TNT spawner (if applicable). 536 if self._have_tnt: 537 self._tntspawner = TNTSpawner(position=self._tntspawnpos) 538 539 self.setup_low_life_warning_sound() 540 self._update_scores() 541 self._bots = SpazBotSet() 542 ba.timer(4.0, self._start_updating_waves) 543 544 def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None: 545 self._show_standard_scores_to_beat_ui(scores) 546 547 def _get_dist_grp_totals(self, grps: list[Any]) -> tuple[int, int]: 548 totalpts = 0 549 totaldudes = 0 550 for grp in grps: 551 for grpentry in grp: 552 dudes = grpentry[1] 553 totalpts += grpentry[0] * dudes 554 totaldudes += dudes 555 return totalpts, totaldudes 556 557 def _get_distribution(self, target_points: int, min_dudes: int, 558 max_dudes: int, group_count: int, 559 max_level: int) -> list[list[tuple[int, int]]]: 560 """Calculate a distribution of bad guys given some params.""" 561 max_iterations = 10 + max_dudes * 2 562 563 groups: list[list[tuple[int, int]]] = [] 564 for _g in range(group_count): 565 groups.append([]) 566 types = [1] 567 if max_level > 1: 568 types.append(2) 569 if max_level > 2: 570 types.append(3) 571 if max_level > 3: 572 types.append(4) 573 for iteration in range(max_iterations): 574 diff = self._add_dist_entry_if_possible(groups, max_dudes, 575 target_points, types) 576 577 total_points, total_dudes = self._get_dist_grp_totals(groups) 578 full = (total_points >= target_points) 579 580 if full: 581 # Every so often, delete a random entry just to 582 # shake up our distribution. 583 if random.random() < 0.2 and iteration != max_iterations - 1: 584 self._delete_random_dist_entry(groups) 585 586 # If we don't have enough dudes, kill the group with 587 # the biggest point value. 588 elif (total_dudes < min_dudes 589 and iteration != max_iterations - 1): 590 self._delete_biggest_dist_entry(groups) 591 592 # If we've got too many dudes, kill the group with the 593 # smallest point value. 594 elif (total_dudes > max_dudes 595 and iteration != max_iterations - 1): 596 self._delete_smallest_dist_entry(groups) 597 598 # Close enough.. we're done. 599 else: 600 if diff == 0: 601 break 602 603 return groups 604 605 def _add_dist_entry_if_possible(self, groups: list[list[tuple[int, int]]], 606 max_dudes: int, target_points: int, 607 types: list[int]) -> int: 608 # See how much we're off our target by. 609 total_points, total_dudes = self._get_dist_grp_totals(groups) 610 diff = target_points - total_points 611 dudes_diff = max_dudes - total_dudes 612 613 # Add an entry if one will fit. 614 value = types[random.randrange(len(types))] 615 group = groups[random.randrange(len(groups))] 616 if not group: 617 max_count = random.randint(1, 6) 618 else: 619 max_count = 2 * random.randint(1, 3) 620 max_count = min(max_count, dudes_diff) 621 count = min(max_count, diff // value) 622 if count > 0: 623 group.append((value, count)) 624 total_points += value * count 625 total_dudes += count 626 diff = target_points - total_points 627 return diff 628 629 def _delete_smallest_dist_entry( 630 self, groups: list[list[tuple[int, int]]]) -> None: 631 smallest_value = 9999 632 smallest_entry = None 633 smallest_entry_group = None 634 for group in groups: 635 for entry in group: 636 if entry[0] < smallest_value or smallest_entry is None: 637 smallest_value = entry[0] 638 smallest_entry = entry 639 smallest_entry_group = group 640 assert smallest_entry is not None 641 assert smallest_entry_group is not None 642 smallest_entry_group.remove(smallest_entry) 643 644 def _delete_biggest_dist_entry( 645 self, groups: list[list[tuple[int, int]]]) -> None: 646 biggest_value = 9999 647 biggest_entry = None 648 biggest_entry_group = None 649 for group in groups: 650 for entry in group: 651 if entry[0] > biggest_value or biggest_entry is None: 652 biggest_value = entry[0] 653 biggest_entry = entry 654 biggest_entry_group = group 655 if biggest_entry is not None: 656 assert biggest_entry_group is not None 657 biggest_entry_group.remove(biggest_entry) 658 659 def _delete_random_dist_entry(self, 660 groups: list[list[tuple[int, int]]]) -> None: 661 entry_count = 0 662 for group in groups: 663 for _ in group: 664 entry_count += 1 665 if entry_count > 1: 666 del_entry = random.randrange(entry_count) 667 entry_count = 0 668 for group in groups: 669 for entry in group: 670 if entry_count == del_entry: 671 group.remove(entry) 672 break 673 entry_count += 1 674 675 def spawn_player(self, player: Player) -> ba.Actor: 676 677 # We keep track of who got hurt each wave for score purposes. 678 player.has_been_hurt = False 679 pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5), 680 self._spawn_center[1], 681 self._spawn_center[2] + random.uniform(-1.5, 1.5)) 682 spaz = self.spawn_player_spaz(player, position=pos) 683 if self._preset in { 684 Preset.TRAINING_EASY, Preset.ROOKIE_EASY, Preset.PRO_EASY, 685 Preset.UBER_EASY 686 }: 687 spaz.impact_scale = 0.25 688 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) 689 return spaz 690 691 def _handle_player_dropped_bomb(self, player: ba.Actor, 692 bomb: ba.Actor) -> None: 693 del player, bomb # Unused. 694 self._player_has_dropped_bomb = True 695 696 def _drop_powerup(self, 697 index: int, 698 poweruptype: str | None = None) -> None: 699 poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( 700 forcetype=poweruptype, excludetypes=self._excluded_powerups)) 701 PowerupBox(position=self.map.powerup_spawn_points[index], 702 poweruptype=poweruptype).autoretain() 703 704 def _start_powerup_drops(self) -> None: 705 self._powerup_drop_timer = ba.Timer(3.0, 706 ba.WeakCall(self._drop_powerups), 707 repeat=True) 708 709 def _drop_powerups(self, 710 standard_points: bool = False, 711 poweruptype: str | None = None) -> None: 712 """Generic powerup drop.""" 713 if standard_points: 714 points = self.map.powerup_spawn_points 715 for i in range(len(points)): 716 ba.timer( 717 1.0 + i * 0.5, 718 ba.WeakCall(self._drop_powerup, i, 719 poweruptype if i == 0 else None)) 720 else: 721 point = (self._powerup_center[0] + random.uniform( 722 -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), 723 self._powerup_center[1], 724 self._powerup_center[2] + random.uniform( 725 -self._powerup_spread[1], self._powerup_spread[1])) 726 727 # Drop one random one somewhere. 728 PowerupBox( 729 position=point, 730 poweruptype=PowerupBoxFactory.get().get_random_powerup_type( 731 excludetypes=self._excluded_powerups)).autoretain() 732 733 def do_end(self, outcome: str, delay: float = 0.0) -> None: 734 """End the game with the specified outcome.""" 735 if outcome == 'defeat': 736 self.fade_to_red() 737 score: int | None 738 if self._wavenum >= 2: 739 score = self._score 740 fail_message = None 741 else: 742 score = None 743 fail_message = ba.Lstr(resource='reachWave2Text') 744 self.end( 745 { 746 'outcome': outcome, 747 'score': score, 748 'fail_message': fail_message, 749 'playerinfos': self.initialplayerinfos 750 }, 751 delay=delay) 752 753 def _award_completion_achievements(self) -> None: 754 if self._preset in {Preset.TRAINING, Preset.TRAINING_EASY}: 755 self._award_achievement('Onslaught Training Victory', sound=False) 756 if not self._player_has_dropped_bomb: 757 self._award_achievement('Boxer', sound=False) 758 elif self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: 759 self._award_achievement('Rookie Onslaught Victory', sound=False) 760 if not self._a_player_has_been_hurt: 761 self._award_achievement('Flawless Victory', sound=False) 762 elif self._preset in {Preset.PRO, Preset.PRO_EASY}: 763 self._award_achievement('Pro Onslaught Victory', sound=False) 764 if not self._player_has_dropped_bomb: 765 self._award_achievement('Pro Boxer', sound=False) 766 elif self._preset in {Preset.UBER, Preset.UBER_EASY}: 767 self._award_achievement('Uber Onslaught Victory', sound=False) 768 769 def _update_waves(self) -> None: 770 771 # If we have no living bots, go to the next wave. 772 assert self._bots is not None 773 if (self._can_end_wave and not self._bots.have_living_bots() 774 and not self._game_over): 775 self._can_end_wave = False 776 self._time_bonus_timer = None 777 self._time_bonus_text = None 778 if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: 779 won = False 780 else: 781 won = (self._wavenum == len(self._waves)) 782 783 base_delay = 4.0 if won else 0.0 784 785 # Reward time bonus. 786 if self._time_bonus > 0: 787 ba.timer(0, lambda: ba.playsound(self._cashregistersound)) 788 ba.timer(base_delay, 789 ba.WeakCall(self._award_time_bonus, self._time_bonus)) 790 base_delay += 1.0 791 792 # Reward flawless bonus. 793 if self._wavenum > 0: 794 have_flawless = False 795 for player in self.players: 796 if player.is_alive() and not player.has_been_hurt: 797 have_flawless = True 798 ba.timer( 799 base_delay, 800 ba.WeakCall(self._award_flawless_bonus, player)) 801 player.has_been_hurt = False # reset 802 if have_flawless: 803 base_delay += 1.0 804 805 if won: 806 self.show_zoom_message(ba.Lstr(resource='victoryText'), 807 scale=1.0, 808 duration=4.0) 809 self.celebrate(20.0) 810 self._award_completion_achievements() 811 ba.timer(base_delay, ba.WeakCall(self._award_completion_bonus)) 812 base_delay += 0.85 813 ba.playsound(self._winsound) 814 ba.cameraflash() 815 ba.setmusic(ba.MusicType.VICTORY) 816 self._game_over = True 817 818 # Can't just pass delay to do_end because our extra bonuses 819 # haven't been added yet (once we call do_end the score 820 # gets locked in). 821 ba.timer(base_delay, ba.WeakCall(self.do_end, 'victory')) 822 return 823 824 self._wavenum += 1 825 826 # Short celebration after waves. 827 if self._wavenum > 1: 828 self.celebrate(0.5) 829 ba.timer(base_delay, ba.WeakCall(self._start_next_wave)) 830 831 def _award_completion_bonus(self) -> None: 832 ba.playsound(self._cashregistersound) 833 for player in self.players: 834 try: 835 if player.is_alive(): 836 assert self.initialplayerinfos is not None 837 self.stats.player_scored( 838 player, 839 int(100 / len(self.initialplayerinfos)), 840 scale=1.4, 841 color=(0.6, 0.6, 1.0, 1.0), 842 title=ba.Lstr(resource='completionBonusText'), 843 screenmessage=False) 844 except Exception: 845 ba.print_exception() 846 847 def _award_time_bonus(self, bonus: int) -> None: 848 ba.playsound(self._cashregistersound) 849 PopupText(ba.Lstr(value='+${A} ${B}', 850 subs=[('${A}', str(bonus)), 851 ('${B}', ba.Lstr(resource='timeBonusText'))]), 852 color=(1, 1, 0.5, 1), 853 scale=1.0, 854 position=(0, 3, -1)).autoretain() 855 self._score += self._time_bonus 856 self._update_scores() 857 858 def _award_flawless_bonus(self, player: Player) -> None: 859 ba.playsound(self._cashregistersound) 860 try: 861 if player.is_alive(): 862 assert self._flawless_bonus is not None 863 self.stats.player_scored( 864 player, 865 self._flawless_bonus, 866 scale=1.2, 867 color=(0.6, 1.0, 0.6, 1.0), 868 title=ba.Lstr(resource='flawlessWaveText'), 869 screenmessage=False) 870 except Exception: 871 ba.print_exception() 872 873 def _start_time_bonus_timer(self) -> None: 874 self._time_bonus_timer = ba.Timer(1.0, 875 ba.WeakCall(self._update_time_bonus), 876 repeat=True) 877 878 def _update_player_spawn_info(self) -> None: 879 880 # If we have no living players lets just blank this. 881 assert self._spawn_info_text is not None 882 assert self._spawn_info_text.node 883 if not any(player.is_alive() for player in self.teams[0].players): 884 self._spawn_info_text.node.text = '' 885 else: 886 text: str | ba.Lstr = '' 887 for player in self.players: 888 if (not player.is_alive() 889 and (self._preset 890 in [Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT] or 891 (player.respawn_wave <= len(self._waves)))): 892 rtxt = ba.Lstr(resource='onslaughtRespawnText', 893 subs=[('${PLAYER}', player.getname()), 894 ('${WAVE}', str(player.respawn_wave)) 895 ]) 896 text = ba.Lstr(value='${A}${B}\n', 897 subs=[ 898 ('${A}', text), 899 ('${B}', rtxt), 900 ]) 901 self._spawn_info_text.node.text = text 902 903 def _respawn_players_for_wave(self) -> None: 904 # Respawn applicable players. 905 if self._wavenum > 1 and not self.is_waiting_for_continue(): 906 for player in self.players: 907 if (not player.is_alive() 908 and player.respawn_wave == self._wavenum): 909 self.spawn_player(player) 910 self._update_player_spawn_info() 911 912 def _setup_wave_spawns(self, wave: Wave) -> None: 913 tval = 0.0 914 dtime = 0.2 915 if self._wavenum == 1: 916 spawn_time = 3.973 917 tval += 0.5 918 else: 919 spawn_time = 2.648 920 921 bot_angle = wave.base_angle 922 self._time_bonus = 0 923 self._flawless_bonus = 0 924 for info in wave.entries: 925 if info is None: 926 continue 927 if isinstance(info, Delay): 928 spawn_time += info.duration 929 continue 930 if isinstance(info, Spacing): 931 bot_angle += info.spacing 932 continue 933 bot_type_2 = info.bottype 934 if bot_type_2 is not None: 935 assert not isinstance(bot_type_2, str) 936 self._time_bonus += bot_type_2.points_mult * 20 937 self._flawless_bonus += bot_type_2.points_mult * 5 938 939 # If its got a position, use that. 940 point = info.point 941 if point is not None: 942 assert bot_type_2 is not None 943 spcall = ba.WeakCall(self.add_bot_at_point, point, bot_type_2, 944 spawn_time) 945 ba.timer(tval, spcall) 946 tval += dtime 947 else: 948 spacing = info.spacing 949 bot_angle += spacing * 0.5 950 if bot_type_2 is not None: 951 tcall = ba.WeakCall(self.add_bot_at_angle, bot_angle, 952 bot_type_2, spawn_time) 953 ba.timer(tval, tcall) 954 tval += dtime 955 bot_angle += spacing * 0.5 956 957 # We can end the wave after all the spawning happens. 958 ba.timer(tval + spawn_time - dtime + 0.01, 959 ba.WeakCall(self._set_can_end_wave)) 960 961 def _start_next_wave(self) -> None: 962 963 # This can happen if we beat a wave as we die. 964 # We don't wanna respawn players and whatnot if this happens. 965 if self._game_over: 966 return 967 968 self._respawn_players_for_wave() 969 if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: 970 wave = self._generate_random_wave() 971 else: 972 wave = self._waves[self._wavenum - 1] 973 self._setup_wave_spawns(wave) 974 self._update_wave_ui_and_bonuses() 975 ba.timer(0.4, ba.Call(ba.playsound, self._new_wave_sound)) 976 977 def _update_wave_ui_and_bonuses(self) -> None: 978 979 self.show_zoom_message(ba.Lstr(value='${A} ${B}', 980 subs=[('${A}', 981 ba.Lstr(resource='waveText')), 982 ('${B}', str(self._wavenum))]), 983 scale=1.0, 984 duration=1.0, 985 trail=True) 986 987 # Reset our time bonus. 988 tbtcolor = (1, 1, 0, 1) 989 tbttxt = ba.Lstr(value='${A}: ${B}', 990 subs=[ 991 ('${A}', ba.Lstr(resource='timeBonusText')), 992 ('${B}', str(self._time_bonus)), 993 ]) 994 self._time_bonus_text = ba.NodeActor( 995 ba.newnode('text', 996 attrs={ 997 'v_attach': 'top', 998 'h_attach': 'center', 999 'h_align': 'center', 1000 'vr_depth': -30, 1001 'color': tbtcolor, 1002 'shadow': 1.0, 1003 'flatness': 1.0, 1004 'position': (0, -60), 1005 'scale': 0.8, 1006 'text': tbttxt 1007 })) 1008 1009 ba.timer(5.0, ba.WeakCall(self._start_time_bonus_timer)) 1010 wtcolor = (1, 1, 1, 1) 1011 wttxt = ba.Lstr( 1012 value='${A} ${B}', 1013 subs=[('${A}', ba.Lstr(resource='waveText')), 1014 ('${B}', str(self._wavenum) + 1015 ('' if self._preset 1016 in [Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT] else 1017 ('/' + str(len(self._waves)))))]) 1018 self._wave_text = ba.NodeActor( 1019 ba.newnode('text', 1020 attrs={ 1021 'v_attach': 'top', 1022 'h_attach': 'center', 1023 'h_align': 'center', 1024 'vr_depth': -10, 1025 'color': wtcolor, 1026 'shadow': 1.0, 1027 'flatness': 1.0, 1028 'position': (0, -40), 1029 'scale': 1.3, 1030 'text': wttxt 1031 })) 1032 1033 def _bot_levels_for_wave(self) -> list[list[type[SpazBot]]]: 1034 level = self._wavenum 1035 bot_types = [ 1036 BomberBot, BrawlerBot, TriggerBot, ChargerBot, BomberBotPro, 1037 BrawlerBotPro, TriggerBotPro, BomberBotProShielded, ExplodeyBot, 1038 ChargerBotProShielded, StickyBot, BrawlerBotProShielded, 1039 TriggerBotProShielded 1040 ] 1041 if level > 5: 1042 bot_types += [ 1043 ExplodeyBot, 1044 TriggerBotProShielded, 1045 BrawlerBotProShielded, 1046 ChargerBotProShielded, 1047 ] 1048 if level > 7: 1049 bot_types += [ 1050 ExplodeyBot, 1051 TriggerBotProShielded, 1052 BrawlerBotProShielded, 1053 ChargerBotProShielded, 1054 ] 1055 if level > 10: 1056 bot_types += [ 1057 TriggerBotProShielded, TriggerBotProShielded, 1058 TriggerBotProShielded, TriggerBotProShielded 1059 ] 1060 if level > 13: 1061 bot_types += [ 1062 TriggerBotProShielded, TriggerBotProShielded, 1063 TriggerBotProShielded, TriggerBotProShielded 1064 ] 1065 bot_levels = [[b for b in bot_types if b.points_mult == 1], 1066 [b for b in bot_types if b.points_mult == 2], 1067 [b for b in bot_types if b.points_mult == 3], 1068 [b for b in bot_types if b.points_mult == 4]] 1069 1070 # Make sure all lists have something in them 1071 if not all(bot_levels): 1072 raise RuntimeError('Got empty bot level') 1073 return bot_levels 1074 1075 def _add_entries_for_distribution_group( 1076 self, group: list[tuple[int, int]], 1077 bot_levels: list[list[type[SpazBot]]], 1078 all_entries: list[Spawn | Spacing | Delay | None]) -> None: 1079 entries: list[Spawn | Spacing | Delay | None] = [] 1080 for entry in group: 1081 bot_level = bot_levels[entry[0] - 1] 1082 bot_type = bot_level[random.randrange(len(bot_level))] 1083 rval = random.random() 1084 if rval < 0.5: 1085 spacing = 10.0 1086 elif rval < 0.9: 1087 spacing = 20.0 1088 else: 1089 spacing = 40.0 1090 split = random.random() > 0.3 1091 for i in range(entry[1]): 1092 if split and i % 2 == 0: 1093 entries.insert(0, Spawn(bot_type, spacing=spacing)) 1094 else: 1095 entries.append(Spawn(bot_type, spacing=spacing)) 1096 if entries: 1097 all_entries += entries 1098 all_entries.append( 1099 Spacing(40.0 if random.random() < 0.5 else 80.0)) 1100 1101 def _generate_random_wave(self) -> Wave: 1102 level = self._wavenum 1103 bot_levels = self._bot_levels_for_wave() 1104 1105 target_points = level * 3 - 2 1106 min_dudes = min(1 + level // 3, 10) 1107 max_dudes = min(10, level + 1) 1108 max_level = 4 if level > 6 else (3 if level > 3 else 1109 (2 if level > 2 else 1)) 1110 group_count = 3 1111 distribution = self._get_distribution(target_points, min_dudes, 1112 max_dudes, group_count, 1113 max_level) 1114 all_entries: list[Spawn | Spacing | Delay | None] = [] 1115 for group in distribution: 1116 self._add_entries_for_distribution_group(group, bot_levels, 1117 all_entries) 1118 angle_rand = random.random() 1119 if angle_rand > 0.75: 1120 base_angle = 130.0 1121 elif angle_rand > 0.5: 1122 base_angle = 210.0 1123 elif angle_rand > 0.25: 1124 base_angle = 20.0 1125 else: 1126 base_angle = -30.0 1127 base_angle += (0.5 - random.random()) * 20.0 1128 wave = Wave(base_angle=base_angle, entries=all_entries) 1129 return wave 1130 1131 def add_bot_at_point(self, 1132 point: Point, 1133 spaz_type: type[SpazBot], 1134 spawn_time: float = 1.0) -> None: 1135 """Add a new bot at a specified named point.""" 1136 if self._game_over: 1137 return 1138 assert isinstance(point.value, str) 1139 pointpos = self.map.defs.points[point.value] 1140 assert self._bots is not None 1141 self._bots.spawn_bot(spaz_type, pos=pointpos, spawn_time=spawn_time) 1142 1143 def add_bot_at_angle(self, 1144 angle: float, 1145 spaz_type: type[SpazBot], 1146 spawn_time: float = 1.0) -> None: 1147 """Add a new bot at a specified angle (for circular maps).""" 1148 if self._game_over: 1149 return 1150 angle_radians = angle / 57.2957795 1151 xval = math.sin(angle_radians) * 1.06 1152 zval = math.cos(angle_radians) * 1.06 1153 point = (xval / 0.125, 2.3, (zval / 0.2) - 3.7) 1154 assert self._bots is not None 1155 self._bots.spawn_bot(spaz_type, pos=point, spawn_time=spawn_time) 1156 1157 def _update_time_bonus(self) -> None: 1158 self._time_bonus = int(self._time_bonus * 0.93) 1159 if self._time_bonus > 0 and self._time_bonus_text is not None: 1160 assert self._time_bonus_text.node 1161 self._time_bonus_text.node.text = ba.Lstr( 1162 value='${A}: ${B}', 1163 subs=[('${A}', ba.Lstr(resource='timeBonusText')), 1164 ('${B}', str(self._time_bonus))]) 1165 else: 1166 self._time_bonus_text = None 1167 1168 def _start_updating_waves(self) -> None: 1169 self._wave_update_timer = ba.Timer(2.0, 1170 ba.WeakCall(self._update_waves), 1171 repeat=True) 1172 1173 def _update_scores(self) -> None: 1174 score = self._score 1175 if self._preset is Preset.ENDLESS: 1176 if score >= 500: 1177 self._award_achievement('Onslaught Master') 1178 if score >= 1000: 1179 self._award_achievement('Onslaught Wizard') 1180 if score >= 5000: 1181 self._award_achievement('Onslaught God') 1182 assert self._scoreboard is not None 1183 self._scoreboard.set_team_value(self.teams[0], score, max_score=None) 1184 1185 def handlemessage(self, msg: Any) -> Any: 1186 1187 if isinstance(msg, PlayerSpazHurtMessage): 1188 msg.spaz.getplayer(Player, True).has_been_hurt = True 1189 self._a_player_has_been_hurt = True 1190 1191 elif isinstance(msg, ba.PlayerScoredMessage): 1192 self._score += msg.score 1193 self._update_scores() 1194 1195 elif isinstance(msg, ba.PlayerDiedMessage): 1196 super().handlemessage(msg) # Augment standard behavior. 1197 player = msg.getplayer(Player) 1198 self._a_player_has_been_hurt = True 1199 1200 # Make note with the player when they can respawn: 1201 if self._wavenum < 10: 1202 player.respawn_wave = max(2, self._wavenum + 1) 1203 elif self._wavenum < 15: 1204 player.respawn_wave = max(2, self._wavenum + 2) 1205 else: 1206 player.respawn_wave = max(2, self._wavenum + 3) 1207 ba.timer(0.1, self._update_player_spawn_info) 1208 ba.timer(0.1, self._checkroundover) 1209 1210 elif isinstance(msg, SpazBotDiedMessage): 1211 pts, importance = msg.spazbot.get_death_points(msg.how) 1212 if msg.killerplayer is not None: 1213 self._handle_kill_achievements(msg) 1214 target: Sequence[float] | None 1215 if msg.spazbot.node: 1216 target = msg.spazbot.node.position 1217 else: 1218 target = None 1219 1220 killerplayer = msg.killerplayer 1221 self.stats.player_scored(killerplayer, 1222 pts, 1223 target=target, 1224 kill=True, 1225 screenmessage=False, 1226 importance=importance) 1227 ba.playsound(self._dingsound 1228 if importance == 1 else self._dingsoundhigh, 1229 volume=0.6) 1230 1231 # Normally we pull scores from the score-set, but if there's 1232 # no player lets be explicit. 1233 else: 1234 self._score += pts 1235 self._update_scores() 1236 else: 1237 super().handlemessage(msg) 1238 1239 def _handle_kill_achievements(self, msg: SpazBotDiedMessage) -> None: 1240 if self._preset in {Preset.TRAINING, Preset.TRAINING_EASY}: 1241 self._handle_training_kill_achievements(msg) 1242 elif self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: 1243 self._handle_rookie_kill_achievements(msg) 1244 elif self._preset in {Preset.PRO, Preset.PRO_EASY}: 1245 self._handle_pro_kill_achievements(msg) 1246 elif self._preset in {Preset.UBER, Preset.UBER_EASY}: 1247 self._handle_uber_kill_achievements(msg) 1248 1249 def _handle_uber_kill_achievements(self, msg: SpazBotDiedMessage) -> None: 1250 1251 # Uber mine achievement: 1252 if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): 1253 self._land_mine_kills += 1 1254 if self._land_mine_kills >= 6: 1255 self._award_achievement('Gold Miner') 1256 1257 # Uber tnt achievement: 1258 if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): 1259 self._tnt_kills += 1 1260 if self._tnt_kills >= 6: 1261 ba.timer(0.5, ba.WeakCall(self._award_achievement, 1262 'TNT Terror')) 1263 1264 def _handle_pro_kill_achievements(self, msg: SpazBotDiedMessage) -> None: 1265 1266 # TNT achievement: 1267 if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): 1268 self._tnt_kills += 1 1269 if self._tnt_kills >= 3: 1270 ba.timer( 1271 0.5, 1272 ba.WeakCall(self._award_achievement, 1273 'Boom Goes the Dynamite')) 1274 1275 def _handle_rookie_kill_achievements(self, 1276 msg: SpazBotDiedMessage) -> None: 1277 # Land-mine achievement: 1278 if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): 1279 self._land_mine_kills += 1 1280 if self._land_mine_kills >= 3: 1281 self._award_achievement('Mine Games') 1282 1283 def _handle_training_kill_achievements(self, 1284 msg: SpazBotDiedMessage) -> None: 1285 # Toss-off-map achievement: 1286 if msg.spazbot.last_attacked_type == ('picked_up', 'default'): 1287 self._throw_off_kills += 1 1288 if self._throw_off_kills >= 3: 1289 self._award_achievement('Off You Go Then') 1290 1291 def _set_can_end_wave(self) -> None: 1292 self._can_end_wave = True 1293 1294 def end_game(self) -> None: 1295 # Tell our bots to celebrate just to rub it in. 1296 assert self._bots is not None 1297 self._bots.final_celebrate() 1298 self._game_over = True 1299 self.do_end('defeat', delay=2.0) 1300 ba.setmusic(None) 1301 1302 def on_continue(self) -> None: 1303 for player in self.players: 1304 if not player.is_alive(): 1305 self.spawn_player(player) 1306 1307 def _checkroundover(self) -> None: 1308 """Potentially end the round based on the state of the game.""" 1309 if self.has_ended(): 1310 return 1311 if not any(player.is_alive() for player in self.teams[0].players): 1312 # Allow continuing after wave 1. 1313 if self._wavenum > 1: 1314 self.continue_or_end_game() 1315 else: 1316 self.end_game()
39@dataclass 40class Wave: 41 """A wave of enemies.""" 42 entries: list[Spawn | Spacing | Delay | None] 43 base_angle: float = 0.0
A wave of enemies.
46@dataclass 47class Spawn: 48 """A bot spawn event in a wave.""" 49 bottype: type[SpazBot] | str 50 point: Point | None = None 51 spacing: float = 5.0
A bot spawn event in a wave.
Empty space in a wave.
A delay between events in a wave.
66class Preset(Enum): 67 """Game presets we support.""" 68 TRAINING = 'training' 69 TRAINING_EASY = 'training_easy' 70 ROOKIE = 'rookie' 71 ROOKIE_EASY = 'rookie_easy' 72 PRO = 'pro' 73 PRO_EASY = 'pro_easy' 74 UBER = 'uber' 75 UBER_EASY = 'uber_easy' 76 ENDLESS = 'endless' 77 ENDLESS_TOURNAMENT = 'endless_tournament'
Game presets we support.
Inherited Members
- enum.Enum
- name
- value
80@unique 81class Point(Enum): 82 """Points on the map we can spawn at.""" 83 LEFT_UPPER_MORE = 'bot_spawn_left_upper_more' 84 LEFT_UPPER = 'bot_spawn_left_upper' 85 TURRET_TOP_RIGHT = 'bot_spawn_turret_top_right' 86 RIGHT_UPPER = 'bot_spawn_right_upper' 87 TURRET_TOP_MIDDLE_LEFT = 'bot_spawn_turret_top_middle_left' 88 TURRET_TOP_MIDDLE_RIGHT = 'bot_spawn_turret_top_middle_right' 89 TURRET_TOP_LEFT = 'bot_spawn_turret_top_left' 90 TOP_RIGHT = 'bot_spawn_top_right' 91 TOP_LEFT = 'bot_spawn_top_left' 92 TOP = 'bot_spawn_top' 93 BOTTOM = 'bot_spawn_bottom' 94 LEFT = 'bot_spawn_left' 95 RIGHT = 'bot_spawn_right' 96 RIGHT_UPPER_MORE = 'bot_spawn_right_upper_more' 97 RIGHT_LOWER = 'bot_spawn_right_lower' 98 RIGHT_LOWER_MORE = 'bot_spawn_right_lower_more' 99 BOTTOM_RIGHT = 'bot_spawn_bottom_right' 100 BOTTOM_LEFT = 'bot_spawn_bottom_left' 101 TURRET_BOTTOM_RIGHT = 'bot_spawn_turret_bottom_right' 102 TURRET_BOTTOM_LEFT = 'bot_spawn_turret_bottom_left' 103 LEFT_LOWER = 'bot_spawn_left_lower' 104 LEFT_LOWER_MORE = 'bot_spawn_left_lower_more' 105 TURRET_TOP_MIDDLE = 'bot_spawn_turret_top_middle' 106 BOTTOM_HALF_RIGHT = 'bot_spawn_bottom_half_right' 107 BOTTOM_HALF_LEFT = 'bot_spawn_bottom_half_left' 108 TOP_HALF_RIGHT = 'bot_spawn_top_half_right' 109 TOP_HALF_LEFT = 'bot_spawn_top_half_left'
Points on the map we can spawn at.
Inherited Members
- enum.Enum
- name
- value
112class Player(ba.Player['Team']): 113 """Our player type for this game.""" 114 115 def __init__(self) -> None: 116 self.has_been_hurt = False 117 self.respawn_wave = 0
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
124class OnslaughtGame(ba.CoopGameActivity[Player, Team]): 125 """Co-op game where players try to survive attacking waves of enemies.""" 126 127 name = 'Onslaught' 128 description = 'Defeat all enemies.' 129 130 tips: list[str | ba.GameTip] = [ 131 'Hold any button to run.' 132 ' (Trigger buttons work well if you have them)', 133 'Try tricking enemies into killing eachother or running off cliffs.', 134 'Try \'Cooking off\' bombs for a second or two before throwing them.', 135 'It\'s easier to win with a friend or two helping.', 136 'If you stay in one place, you\'re toast. Run and dodge to survive..', 137 'Practice using your momentum to throw bombs more accurately.', 138 'Your punches do much more damage if you are running or spinning.' 139 ] 140 141 # Show messages when players die since it matters here. 142 announce_player_deaths = True 143 144 def __init__(self, settings: dict): 145 146 self._preset = Preset(settings.get('preset', 'training')) 147 if self._preset in { 148 Preset.TRAINING, Preset.TRAINING_EASY, Preset.PRO, 149 Preset.PRO_EASY, Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT 150 }: 151 settings['map'] = 'Doom Shroom' 152 else: 153 settings['map'] = 'Courtyard' 154 155 super().__init__(settings) 156 157 self._new_wave_sound = ba.getsound('scoreHit01') 158 self._winsound = ba.getsound('score') 159 self._cashregistersound = ba.getsound('cashRegister') 160 self._a_player_has_been_hurt = False 161 self._player_has_dropped_bomb = False 162 163 # FIXME: should use standard map defs. 164 if settings['map'] == 'Doom Shroom': 165 self._spawn_center = (0, 3, -5) 166 self._tntspawnpos = (0.0, 3.0, -5.0) 167 self._powerup_center = (0, 5, -3.6) 168 self._powerup_spread = (6.0, 4.0) 169 elif settings['map'] == 'Courtyard': 170 self._spawn_center = (0, 3, -2) 171 self._tntspawnpos = (0.0, 3.0, 2.1) 172 self._powerup_center = (0, 5, -1.6) 173 self._powerup_spread = (4.6, 2.7) 174 else: 175 raise Exception('Unsupported map: ' + str(settings['map'])) 176 self._scoreboard: Scoreboard | None = None 177 self._game_over = False 178 self._wavenum = 0 179 self._can_end_wave = True 180 self._score = 0 181 self._time_bonus = 0 182 self._spawn_info_text: ba.NodeActor | None = None 183 self._dingsound = ba.getsound('dingSmall') 184 self._dingsoundhigh = ba.getsound('dingSmallHigh') 185 self._have_tnt = False 186 self._excluded_powerups: list[str] | None = None 187 self._waves: list[Wave] = [] 188 self._tntspawner: TNTSpawner | None = None 189 self._bots: SpazBotSet | None = None 190 self._powerup_drop_timer: ba.Timer | None = None 191 self._time_bonus_timer: ba.Timer | None = None 192 self._time_bonus_text: ba.NodeActor | None = None 193 self._flawless_bonus: int | None = None 194 self._wave_text: ba.NodeActor | None = None 195 self._wave_update_timer: ba.Timer | None = None 196 self._throw_off_kills = 0 197 self._land_mine_kills = 0 198 self._tnt_kills = 0 199 200 def on_transition_in(self) -> None: 201 super().on_transition_in() 202 customdata = ba.getsession().customdata 203 204 # Show special landmine tip on rookie preset. 205 if self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: 206 # Show once per session only (then we revert to regular tips). 207 if not customdata.get('_showed_onslaught_landmine_tip', False): 208 customdata['_showed_onslaught_landmine_tip'] = True 209 self.tips = [ 210 ba.GameTip( 211 'Land-mines are a good way to stop speedy enemies.', 212 icon=ba.gettexture('powerupLandMines'), 213 sound=ba.getsound('ding')) 214 ] 215 216 # Show special tnt tip on pro preset. 217 if self._preset in {Preset.PRO, Preset.PRO_EASY}: 218 # Show once per session only (then we revert to regular tips). 219 if not customdata.get('_showed_onslaught_tnt_tip', False): 220 customdata['_showed_onslaught_tnt_tip'] = True 221 self.tips = [ 222 ba.GameTip( 223 'Take out a group of enemies by\n' 224 'setting off a bomb near a TNT box.', 225 icon=ba.gettexture('tnt'), 226 sound=ba.getsound('ding')) 227 ] 228 229 # Show special curse tip on uber preset. 230 if self._preset in {Preset.UBER, Preset.UBER_EASY}: 231 # Show once per session only (then we revert to regular tips). 232 if not customdata.get('_showed_onslaught_curse_tip', False): 233 customdata['_showed_onslaught_curse_tip'] = True 234 self.tips = [ 235 ba.GameTip( 236 'Curse boxes turn you into a ticking time bomb.\n' 237 'The only cure is to quickly grab a health-pack.', 238 icon=ba.gettexture('powerupCurse'), 239 sound=ba.getsound('ding')) 240 ] 241 242 self._spawn_info_text = ba.NodeActor( 243 ba.newnode('text', 244 attrs={ 245 'position': (15, -130), 246 'h_attach': 'left', 247 'v_attach': 'top', 248 'scale': 0.55, 249 'color': (0.3, 0.8, 0.3, 1.0), 250 'text': '' 251 })) 252 ba.setmusic(ba.MusicType.ONSLAUGHT) 253 254 self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), 255 score_split=0.5) 256 257 def on_begin(self) -> None: 258 super().on_begin() 259 player_count = len(self.players) 260 hard = self._preset not in { 261 Preset.TRAINING_EASY, Preset.ROOKIE_EASY, Preset.PRO_EASY, 262 Preset.UBER_EASY 263 } 264 if self._preset in {Preset.TRAINING, Preset.TRAINING_EASY}: 265 ControlsGuide(delay=3.0, lifespan=10.0, bright=True).autoretain() 266 267 self._have_tnt = False 268 self._excluded_powerups = ['curse', 'land_mines'] 269 self._waves = [ 270 Wave(base_angle=195, 271 entries=[ 272 Spawn(BomberBotLite, spacing=5), 273 ] * player_count), 274 Wave(base_angle=130, 275 entries=[ 276 Spawn(BrawlerBotLite, spacing=5), 277 ] * player_count), 278 Wave(base_angle=195, 279 entries=[Spawn(BomberBotLite, spacing=10)] * 280 (player_count + 1)), 281 Wave(base_angle=130, 282 entries=[ 283 Spawn(BrawlerBotLite, spacing=10), 284 ] * (player_count + 1)), 285 Wave(base_angle=130, 286 entries=[ 287 Spawn(BrawlerBotLite, spacing=5) 288 if player_count > 1 else None, 289 Spawn(BrawlerBotLite, spacing=5), 290 Spacing(30), 291 Spawn(BomberBotLite, spacing=5) 292 if player_count > 3 else None, 293 Spawn(BomberBotLite, spacing=5), 294 Spacing(30), 295 Spawn(BrawlerBotLite, spacing=5), 296 Spawn(BrawlerBotLite, spacing=5) 297 if player_count > 2 else None, 298 ]), 299 Wave(base_angle=195, 300 entries=[ 301 Spawn(TriggerBot, spacing=90), 302 Spawn(TriggerBot, spacing=90) 303 if player_count > 1 else None, 304 ]), 305 ] 306 307 elif self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: 308 self._have_tnt = False 309 self._excluded_powerups = ['curse'] 310 self._waves = [ 311 Wave(entries=[ 312 Spawn(ChargerBot, Point.LEFT_UPPER_MORE 313 ) if player_count > 2 else None, 314 Spawn(ChargerBot, Point.LEFT_UPPER), 315 ]), 316 Wave(entries=[ 317 Spawn(BomberBotStaticLite, Point.TURRET_TOP_RIGHT), 318 Spawn(BrawlerBotLite, Point.RIGHT_UPPER), 319 Spawn(BrawlerBotLite, Point.RIGHT_LOWER 320 ) if player_count > 1 else None, 321 Spawn(BomberBotStaticLite, Point.TURRET_BOTTOM_RIGHT 322 ) if player_count > 2 else None, 323 ]), 324 Wave(entries=[ 325 Spawn(BomberBotStaticLite, Point.TURRET_BOTTOM_LEFT), 326 Spawn(TriggerBot, Point.LEFT), 327 Spawn(TriggerBot, Point.LEFT_LOWER 328 ) if player_count > 1 else None, 329 Spawn(TriggerBot, Point.LEFT_UPPER 330 ) if player_count > 2 else None, 331 ]), 332 Wave(entries=[ 333 Spawn(BrawlerBotLite, Point.TOP_RIGHT), 334 Spawn(BrawlerBot, Point.TOP_HALF_RIGHT 335 ) if player_count > 1 else None, 336 Spawn(BrawlerBotLite, Point.TOP_LEFT), 337 Spawn(BrawlerBotLite, Point.TOP_HALF_LEFT 338 ) if player_count > 2 else None, 339 Spawn(BrawlerBot, Point.TOP), 340 Spawn(BomberBotStaticLite, Point.TURRET_TOP_MIDDLE), 341 ]), 342 Wave(entries=[ 343 Spawn(TriggerBotStatic, Point.TURRET_BOTTOM_LEFT), 344 Spawn(TriggerBotStatic, Point.TURRET_BOTTOM_RIGHT), 345 Spawn(TriggerBot, Point.BOTTOM), 346 Spawn(TriggerBot, Point.BOTTOM_HALF_RIGHT 347 ) if player_count > 1 else None, 348 Spawn(TriggerBot, Point.BOTTOM_HALF_LEFT 349 ) if player_count > 2 else None, 350 ]), 351 Wave(entries=[ 352 Spawn(BomberBotStaticLite, Point.TURRET_TOP_LEFT), 353 Spawn(BomberBotStaticLite, Point.TURRET_TOP_RIGHT), 354 Spawn(ChargerBot, Point.BOTTOM), 355 Spawn(ChargerBot, Point.BOTTOM_HALF_LEFT 356 ) if player_count > 1 else None, 357 Spawn(ChargerBot, Point.BOTTOM_HALF_RIGHT 358 ) if player_count > 2 else None, 359 ]), 360 ] 361 362 elif self._preset in {Preset.PRO, Preset.PRO_EASY}: 363 self._excluded_powerups = ['curse'] 364 self._have_tnt = True 365 self._waves = [ 366 Wave(base_angle=-50, 367 entries=[ 368 Spawn(BrawlerBot, spacing=12) 369 if player_count > 3 else None, 370 Spawn(BrawlerBot, spacing=12), 371 Spawn(BomberBot, spacing=6), 372 Spawn(BomberBot, spacing=6) 373 if self._preset is Preset.PRO else None, 374 Spawn(BomberBot, spacing=6) 375 if player_count > 1 else None, 376 Spawn(BrawlerBot, spacing=12), 377 Spawn(BrawlerBot, spacing=12) 378 if player_count > 2 else None, 379 ]), 380 Wave(base_angle=180, 381 entries=[ 382 Spawn(BrawlerBot, spacing=6) 383 if player_count > 3 else None, 384 Spawn(BrawlerBot, spacing=6) 385 if self._preset is Preset.PRO else None, 386 Spawn(BrawlerBot, spacing=6), 387 Spawn(ChargerBot, spacing=45), 388 Spawn(ChargerBot, spacing=45) 389 if player_count > 1 else None, 390 Spawn(BrawlerBot, spacing=6), 391 Spawn(BrawlerBot, spacing=6) 392 if self._preset is Preset.PRO else None, 393 Spawn(BrawlerBot, spacing=6) 394 if player_count > 2 else None, 395 ]), 396 Wave(base_angle=0, 397 entries=[ 398 Spawn(ChargerBot, spacing=30), 399 Spawn(TriggerBot, spacing=30), 400 Spawn(TriggerBot, spacing=30), 401 Spawn(TriggerBot, spacing=30) 402 if self._preset is Preset.PRO else None, 403 Spawn(TriggerBot, spacing=30) 404 if player_count > 1 else None, 405 Spawn(TriggerBot, spacing=30) 406 if player_count > 3 else None, 407 Spawn(ChargerBot, spacing=30), 408 ]), 409 Wave(base_angle=90, 410 entries=[ 411 Spawn(StickyBot, spacing=50), 412 Spawn(StickyBot, spacing=50) 413 if self._preset is Preset.PRO else None, 414 Spawn(StickyBot, spacing=50), 415 Spawn(StickyBot, spacing=50) 416 if player_count > 1 else None, 417 Spawn(StickyBot, spacing=50) 418 if player_count > 3 else None, 419 ]), 420 Wave(base_angle=0, 421 entries=[ 422 Spawn(TriggerBot, spacing=72), 423 Spawn(TriggerBot, spacing=72), 424 Spawn(TriggerBot, spacing=72) 425 if self._preset is Preset.PRO else None, 426 Spawn(TriggerBot, spacing=72), 427 Spawn(TriggerBot, spacing=72), 428 Spawn(TriggerBot, spacing=36) 429 if player_count > 2 else None, 430 ]), 431 Wave(base_angle=30, 432 entries=[ 433 Spawn(ChargerBotProShielded, spacing=50), 434 Spawn(ChargerBotProShielded, spacing=50), 435 Spawn(ChargerBotProShielded, spacing=50) 436 if self._preset is Preset.PRO else None, 437 Spawn(ChargerBotProShielded, spacing=50) 438 if player_count > 1 else None, 439 Spawn(ChargerBotProShielded, spacing=50) 440 if player_count > 2 else None, 441 ]) 442 ] 443 444 elif self._preset in {Preset.UBER, Preset.UBER_EASY}: 445 446 # Show controls help in demo/arcade modes. 447 if ba.app.demo_mode or ba.app.arcade_mode: 448 ControlsGuide(delay=3.0, lifespan=10.0, 449 bright=True).autoretain() 450 451 self._have_tnt = True 452 self._excluded_powerups = [] 453 self._waves = [ 454 Wave(entries=[ 455 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_LEFT 456 ) if hard else None, 457 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_RIGHT), 458 Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT 459 ) if player_count > 2 else None, 460 Spawn(ExplodeyBot, Point.TOP_RIGHT), 461 Delay(4.0), 462 Spawn(ExplodeyBot, Point.TOP_LEFT), 463 ]), 464 Wave(entries=[ 465 Spawn(ChargerBot, Point.LEFT), 466 Spawn(ChargerBot, Point.RIGHT), 467 Spawn(ChargerBot, Point.RIGHT_UPPER_MORE 468 ) if player_count > 2 else None, 469 Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT), 470 Spawn(BomberBotProStatic, Point.TURRET_TOP_RIGHT), 471 ]), 472 Wave(entries=[ 473 Spawn(TriggerBotPro, Point.TOP_RIGHT), 474 Spawn(TriggerBotPro, Point.RIGHT_UPPER_MORE 475 ) if player_count > 1 else None, 476 Spawn(TriggerBotPro, Point.RIGHT_UPPER), 477 Spawn(TriggerBotPro, Point.RIGHT_LOWER) if hard else None, 478 Spawn(TriggerBotPro, Point.RIGHT_LOWER_MORE 479 ) if player_count > 2 else None, 480 Spawn(TriggerBotPro, Point.BOTTOM_RIGHT), 481 ]), 482 Wave(entries=[ 483 Spawn(ChargerBotProShielded, Point.BOTTOM_RIGHT), 484 Spawn(ChargerBotProShielded, Point.BOTTOM 485 ) if player_count > 2 else None, 486 Spawn(ChargerBotProShielded, Point.BOTTOM_LEFT), 487 Spawn(ChargerBotProShielded, Point.TOP) if hard else None, 488 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE), 489 ]), 490 Wave(entries=[ 491 Spawn(ExplodeyBot, Point.LEFT_UPPER), 492 Delay(1.0), 493 Spawn(BrawlerBotProShielded, Point.LEFT_LOWER), 494 Spawn(BrawlerBotProShielded, Point.LEFT_LOWER_MORE), 495 Delay(4.0), 496 Spawn(ExplodeyBot, Point.RIGHT_UPPER), 497 Delay(1.0), 498 Spawn(BrawlerBotProShielded, Point.RIGHT_LOWER), 499 Spawn(BrawlerBotProShielded, Point.RIGHT_UPPER_MORE), 500 Delay(4.0), 501 Spawn(ExplodeyBot, Point.LEFT), 502 Delay(5.0), 503 Spawn(ExplodeyBot, Point.RIGHT), 504 ]), 505 Wave(entries=[ 506 Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT), 507 Spawn(BomberBotProStatic, Point.TURRET_TOP_RIGHT), 508 Spawn(BomberBotProStatic, Point.TURRET_BOTTOM_LEFT), 509 Spawn(BomberBotProStatic, Point.TURRET_BOTTOM_RIGHT), 510 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_LEFT 511 ) if hard else None, 512 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_RIGHT 513 ) if hard else None, 514 ]) 515 ] 516 517 # We generate these on the fly in endless. 518 elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: 519 self._have_tnt = True 520 self._excluded_powerups = [] 521 self._waves = [] 522 523 else: 524 raise RuntimeError(f'Invalid preset: {self._preset}') 525 526 # FIXME: Should migrate to use setup_standard_powerup_drops(). 527 528 # Spit out a few powerups and start dropping more shortly. 529 self._drop_powerups(standard_points=True, 530 poweruptype='curse' if self._preset 531 in [Preset.UBER, Preset.UBER_EASY] else 532 ('land_mines' if self._preset 533 in [Preset.ROOKIE, Preset.ROOKIE_EASY] else None)) 534 ba.timer(4.0, self._start_powerup_drops) 535 536 # Our TNT spawner (if applicable). 537 if self._have_tnt: 538 self._tntspawner = TNTSpawner(position=self._tntspawnpos) 539 540 self.setup_low_life_warning_sound() 541 self._update_scores() 542 self._bots = SpazBotSet() 543 ba.timer(4.0, self._start_updating_waves) 544 545 def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None: 546 self._show_standard_scores_to_beat_ui(scores) 547 548 def _get_dist_grp_totals(self, grps: list[Any]) -> tuple[int, int]: 549 totalpts = 0 550 totaldudes = 0 551 for grp in grps: 552 for grpentry in grp: 553 dudes = grpentry[1] 554 totalpts += grpentry[0] * dudes 555 totaldudes += dudes 556 return totalpts, totaldudes 557 558 def _get_distribution(self, target_points: int, min_dudes: int, 559 max_dudes: int, group_count: int, 560 max_level: int) -> list[list[tuple[int, int]]]: 561 """Calculate a distribution of bad guys given some params.""" 562 max_iterations = 10 + max_dudes * 2 563 564 groups: list[list[tuple[int, int]]] = [] 565 for _g in range(group_count): 566 groups.append([]) 567 types = [1] 568 if max_level > 1: 569 types.append(2) 570 if max_level > 2: 571 types.append(3) 572 if max_level > 3: 573 types.append(4) 574 for iteration in range(max_iterations): 575 diff = self._add_dist_entry_if_possible(groups, max_dudes, 576 target_points, types) 577 578 total_points, total_dudes = self._get_dist_grp_totals(groups) 579 full = (total_points >= target_points) 580 581 if full: 582 # Every so often, delete a random entry just to 583 # shake up our distribution. 584 if random.random() < 0.2 and iteration != max_iterations - 1: 585 self._delete_random_dist_entry(groups) 586 587 # If we don't have enough dudes, kill the group with 588 # the biggest point value. 589 elif (total_dudes < min_dudes 590 and iteration != max_iterations - 1): 591 self._delete_biggest_dist_entry(groups) 592 593 # If we've got too many dudes, kill the group with the 594 # smallest point value. 595 elif (total_dudes > max_dudes 596 and iteration != max_iterations - 1): 597 self._delete_smallest_dist_entry(groups) 598 599 # Close enough.. we're done. 600 else: 601 if diff == 0: 602 break 603 604 return groups 605 606 def _add_dist_entry_if_possible(self, groups: list[list[tuple[int, int]]], 607 max_dudes: int, target_points: int, 608 types: list[int]) -> int: 609 # See how much we're off our target by. 610 total_points, total_dudes = self._get_dist_grp_totals(groups) 611 diff = target_points - total_points 612 dudes_diff = max_dudes - total_dudes 613 614 # Add an entry if one will fit. 615 value = types[random.randrange(len(types))] 616 group = groups[random.randrange(len(groups))] 617 if not group: 618 max_count = random.randint(1, 6) 619 else: 620 max_count = 2 * random.randint(1, 3) 621 max_count = min(max_count, dudes_diff) 622 count = min(max_count, diff // value) 623 if count > 0: 624 group.append((value, count)) 625 total_points += value * count 626 total_dudes += count 627 diff = target_points - total_points 628 return diff 629 630 def _delete_smallest_dist_entry( 631 self, groups: list[list[tuple[int, int]]]) -> None: 632 smallest_value = 9999 633 smallest_entry = None 634 smallest_entry_group = None 635 for group in groups: 636 for entry in group: 637 if entry[0] < smallest_value or smallest_entry is None: 638 smallest_value = entry[0] 639 smallest_entry = entry 640 smallest_entry_group = group 641 assert smallest_entry is not None 642 assert smallest_entry_group is not None 643 smallest_entry_group.remove(smallest_entry) 644 645 def _delete_biggest_dist_entry( 646 self, groups: list[list[tuple[int, int]]]) -> None: 647 biggest_value = 9999 648 biggest_entry = None 649 biggest_entry_group = None 650 for group in groups: 651 for entry in group: 652 if entry[0] > biggest_value or biggest_entry is None: 653 biggest_value = entry[0] 654 biggest_entry = entry 655 biggest_entry_group = group 656 if biggest_entry is not None: 657 assert biggest_entry_group is not None 658 biggest_entry_group.remove(biggest_entry) 659 660 def _delete_random_dist_entry(self, 661 groups: list[list[tuple[int, int]]]) -> None: 662 entry_count = 0 663 for group in groups: 664 for _ in group: 665 entry_count += 1 666 if entry_count > 1: 667 del_entry = random.randrange(entry_count) 668 entry_count = 0 669 for group in groups: 670 for entry in group: 671 if entry_count == del_entry: 672 group.remove(entry) 673 break 674 entry_count += 1 675 676 def spawn_player(self, player: Player) -> ba.Actor: 677 678 # We keep track of who got hurt each wave for score purposes. 679 player.has_been_hurt = False 680 pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5), 681 self._spawn_center[1], 682 self._spawn_center[2] + random.uniform(-1.5, 1.5)) 683 spaz = self.spawn_player_spaz(player, position=pos) 684 if self._preset in { 685 Preset.TRAINING_EASY, Preset.ROOKIE_EASY, Preset.PRO_EASY, 686 Preset.UBER_EASY 687 }: 688 spaz.impact_scale = 0.25 689 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) 690 return spaz 691 692 def _handle_player_dropped_bomb(self, player: ba.Actor, 693 bomb: ba.Actor) -> None: 694 del player, bomb # Unused. 695 self._player_has_dropped_bomb = True 696 697 def _drop_powerup(self, 698 index: int, 699 poweruptype: str | None = None) -> None: 700 poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( 701 forcetype=poweruptype, excludetypes=self._excluded_powerups)) 702 PowerupBox(position=self.map.powerup_spawn_points[index], 703 poweruptype=poweruptype).autoretain() 704 705 def _start_powerup_drops(self) -> None: 706 self._powerup_drop_timer = ba.Timer(3.0, 707 ba.WeakCall(self._drop_powerups), 708 repeat=True) 709 710 def _drop_powerups(self, 711 standard_points: bool = False, 712 poweruptype: str | None = None) -> None: 713 """Generic powerup drop.""" 714 if standard_points: 715 points = self.map.powerup_spawn_points 716 for i in range(len(points)): 717 ba.timer( 718 1.0 + i * 0.5, 719 ba.WeakCall(self._drop_powerup, i, 720 poweruptype if i == 0 else None)) 721 else: 722 point = (self._powerup_center[0] + random.uniform( 723 -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), 724 self._powerup_center[1], 725 self._powerup_center[2] + random.uniform( 726 -self._powerup_spread[1], self._powerup_spread[1])) 727 728 # Drop one random one somewhere. 729 PowerupBox( 730 position=point, 731 poweruptype=PowerupBoxFactory.get().get_random_powerup_type( 732 excludetypes=self._excluded_powerups)).autoretain() 733 734 def do_end(self, outcome: str, delay: float = 0.0) -> None: 735 """End the game with the specified outcome.""" 736 if outcome == 'defeat': 737 self.fade_to_red() 738 score: int | None 739 if self._wavenum >= 2: 740 score = self._score 741 fail_message = None 742 else: 743 score = None 744 fail_message = ba.Lstr(resource='reachWave2Text') 745 self.end( 746 { 747 'outcome': outcome, 748 'score': score, 749 'fail_message': fail_message, 750 'playerinfos': self.initialplayerinfos 751 }, 752 delay=delay) 753 754 def _award_completion_achievements(self) -> None: 755 if self._preset in {Preset.TRAINING, Preset.TRAINING_EASY}: 756 self._award_achievement('Onslaught Training Victory', sound=False) 757 if not self._player_has_dropped_bomb: 758 self._award_achievement('Boxer', sound=False) 759 elif self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: 760 self._award_achievement('Rookie Onslaught Victory', sound=False) 761 if not self._a_player_has_been_hurt: 762 self._award_achievement('Flawless Victory', sound=False) 763 elif self._preset in {Preset.PRO, Preset.PRO_EASY}: 764 self._award_achievement('Pro Onslaught Victory', sound=False) 765 if not self._player_has_dropped_bomb: 766 self._award_achievement('Pro Boxer', sound=False) 767 elif self._preset in {Preset.UBER, Preset.UBER_EASY}: 768 self._award_achievement('Uber Onslaught Victory', sound=False) 769 770 def _update_waves(self) -> None: 771 772 # If we have no living bots, go to the next wave. 773 assert self._bots is not None 774 if (self._can_end_wave and not self._bots.have_living_bots() 775 and not self._game_over): 776 self._can_end_wave = False 777 self._time_bonus_timer = None 778 self._time_bonus_text = None 779 if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: 780 won = False 781 else: 782 won = (self._wavenum == len(self._waves)) 783 784 base_delay = 4.0 if won else 0.0 785 786 # Reward time bonus. 787 if self._time_bonus > 0: 788 ba.timer(0, lambda: ba.playsound(self._cashregistersound)) 789 ba.timer(base_delay, 790 ba.WeakCall(self._award_time_bonus, self._time_bonus)) 791 base_delay += 1.0 792 793 # Reward flawless bonus. 794 if self._wavenum > 0: 795 have_flawless = False 796 for player in self.players: 797 if player.is_alive() and not player.has_been_hurt: 798 have_flawless = True 799 ba.timer( 800 base_delay, 801 ba.WeakCall(self._award_flawless_bonus, player)) 802 player.has_been_hurt = False # reset 803 if have_flawless: 804 base_delay += 1.0 805 806 if won: 807 self.show_zoom_message(ba.Lstr(resource='victoryText'), 808 scale=1.0, 809 duration=4.0) 810 self.celebrate(20.0) 811 self._award_completion_achievements() 812 ba.timer(base_delay, ba.WeakCall(self._award_completion_bonus)) 813 base_delay += 0.85 814 ba.playsound(self._winsound) 815 ba.cameraflash() 816 ba.setmusic(ba.MusicType.VICTORY) 817 self._game_over = True 818 819 # Can't just pass delay to do_end because our extra bonuses 820 # haven't been added yet (once we call do_end the score 821 # gets locked in). 822 ba.timer(base_delay, ba.WeakCall(self.do_end, 'victory')) 823 return 824 825 self._wavenum += 1 826 827 # Short celebration after waves. 828 if self._wavenum > 1: 829 self.celebrate(0.5) 830 ba.timer(base_delay, ba.WeakCall(self._start_next_wave)) 831 832 def _award_completion_bonus(self) -> None: 833 ba.playsound(self._cashregistersound) 834 for player in self.players: 835 try: 836 if player.is_alive(): 837 assert self.initialplayerinfos is not None 838 self.stats.player_scored( 839 player, 840 int(100 / len(self.initialplayerinfos)), 841 scale=1.4, 842 color=(0.6, 0.6, 1.0, 1.0), 843 title=ba.Lstr(resource='completionBonusText'), 844 screenmessage=False) 845 except Exception: 846 ba.print_exception() 847 848 def _award_time_bonus(self, bonus: int) -> None: 849 ba.playsound(self._cashregistersound) 850 PopupText(ba.Lstr(value='+${A} ${B}', 851 subs=[('${A}', str(bonus)), 852 ('${B}', ba.Lstr(resource='timeBonusText'))]), 853 color=(1, 1, 0.5, 1), 854 scale=1.0, 855 position=(0, 3, -1)).autoretain() 856 self._score += self._time_bonus 857 self._update_scores() 858 859 def _award_flawless_bonus(self, player: Player) -> None: 860 ba.playsound(self._cashregistersound) 861 try: 862 if player.is_alive(): 863 assert self._flawless_bonus is not None 864 self.stats.player_scored( 865 player, 866 self._flawless_bonus, 867 scale=1.2, 868 color=(0.6, 1.0, 0.6, 1.0), 869 title=ba.Lstr(resource='flawlessWaveText'), 870 screenmessage=False) 871 except Exception: 872 ba.print_exception() 873 874 def _start_time_bonus_timer(self) -> None: 875 self._time_bonus_timer = ba.Timer(1.0, 876 ba.WeakCall(self._update_time_bonus), 877 repeat=True) 878 879 def _update_player_spawn_info(self) -> None: 880 881 # If we have no living players lets just blank this. 882 assert self._spawn_info_text is not None 883 assert self._spawn_info_text.node 884 if not any(player.is_alive() for player in self.teams[0].players): 885 self._spawn_info_text.node.text = '' 886 else: 887 text: str | ba.Lstr = '' 888 for player in self.players: 889 if (not player.is_alive() 890 and (self._preset 891 in [Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT] or 892 (player.respawn_wave <= len(self._waves)))): 893 rtxt = ba.Lstr(resource='onslaughtRespawnText', 894 subs=[('${PLAYER}', player.getname()), 895 ('${WAVE}', str(player.respawn_wave)) 896 ]) 897 text = ba.Lstr(value='${A}${B}\n', 898 subs=[ 899 ('${A}', text), 900 ('${B}', rtxt), 901 ]) 902 self._spawn_info_text.node.text = text 903 904 def _respawn_players_for_wave(self) -> None: 905 # Respawn applicable players. 906 if self._wavenum > 1 and not self.is_waiting_for_continue(): 907 for player in self.players: 908 if (not player.is_alive() 909 and player.respawn_wave == self._wavenum): 910 self.spawn_player(player) 911 self._update_player_spawn_info() 912 913 def _setup_wave_spawns(self, wave: Wave) -> None: 914 tval = 0.0 915 dtime = 0.2 916 if self._wavenum == 1: 917 spawn_time = 3.973 918 tval += 0.5 919 else: 920 spawn_time = 2.648 921 922 bot_angle = wave.base_angle 923 self._time_bonus = 0 924 self._flawless_bonus = 0 925 for info in wave.entries: 926 if info is None: 927 continue 928 if isinstance(info, Delay): 929 spawn_time += info.duration 930 continue 931 if isinstance(info, Spacing): 932 bot_angle += info.spacing 933 continue 934 bot_type_2 = info.bottype 935 if bot_type_2 is not None: 936 assert not isinstance(bot_type_2, str) 937 self._time_bonus += bot_type_2.points_mult * 20 938 self._flawless_bonus += bot_type_2.points_mult * 5 939 940 # If its got a position, use that. 941 point = info.point 942 if point is not None: 943 assert bot_type_2 is not None 944 spcall = ba.WeakCall(self.add_bot_at_point, point, bot_type_2, 945 spawn_time) 946 ba.timer(tval, spcall) 947 tval += dtime 948 else: 949 spacing = info.spacing 950 bot_angle += spacing * 0.5 951 if bot_type_2 is not None: 952 tcall = ba.WeakCall(self.add_bot_at_angle, bot_angle, 953 bot_type_2, spawn_time) 954 ba.timer(tval, tcall) 955 tval += dtime 956 bot_angle += spacing * 0.5 957 958 # We can end the wave after all the spawning happens. 959 ba.timer(tval + spawn_time - dtime + 0.01, 960 ba.WeakCall(self._set_can_end_wave)) 961 962 def _start_next_wave(self) -> None: 963 964 # This can happen if we beat a wave as we die. 965 # We don't wanna respawn players and whatnot if this happens. 966 if self._game_over: 967 return 968 969 self._respawn_players_for_wave() 970 if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: 971 wave = self._generate_random_wave() 972 else: 973 wave = self._waves[self._wavenum - 1] 974 self._setup_wave_spawns(wave) 975 self._update_wave_ui_and_bonuses() 976 ba.timer(0.4, ba.Call(ba.playsound, self._new_wave_sound)) 977 978 def _update_wave_ui_and_bonuses(self) -> None: 979 980 self.show_zoom_message(ba.Lstr(value='${A} ${B}', 981 subs=[('${A}', 982 ba.Lstr(resource='waveText')), 983 ('${B}', str(self._wavenum))]), 984 scale=1.0, 985 duration=1.0, 986 trail=True) 987 988 # Reset our time bonus. 989 tbtcolor = (1, 1, 0, 1) 990 tbttxt = ba.Lstr(value='${A}: ${B}', 991 subs=[ 992 ('${A}', ba.Lstr(resource='timeBonusText')), 993 ('${B}', str(self._time_bonus)), 994 ]) 995 self._time_bonus_text = ba.NodeActor( 996 ba.newnode('text', 997 attrs={ 998 'v_attach': 'top', 999 'h_attach': 'center', 1000 'h_align': 'center', 1001 'vr_depth': -30, 1002 'color': tbtcolor, 1003 'shadow': 1.0, 1004 'flatness': 1.0, 1005 'position': (0, -60), 1006 'scale': 0.8, 1007 'text': tbttxt 1008 })) 1009 1010 ba.timer(5.0, ba.WeakCall(self._start_time_bonus_timer)) 1011 wtcolor = (1, 1, 1, 1) 1012 wttxt = ba.Lstr( 1013 value='${A} ${B}', 1014 subs=[('${A}', ba.Lstr(resource='waveText')), 1015 ('${B}', str(self._wavenum) + 1016 ('' if self._preset 1017 in [Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT] else 1018 ('/' + str(len(self._waves)))))]) 1019 self._wave_text = ba.NodeActor( 1020 ba.newnode('text', 1021 attrs={ 1022 'v_attach': 'top', 1023 'h_attach': 'center', 1024 'h_align': 'center', 1025 'vr_depth': -10, 1026 'color': wtcolor, 1027 'shadow': 1.0, 1028 'flatness': 1.0, 1029 'position': (0, -40), 1030 'scale': 1.3, 1031 'text': wttxt 1032 })) 1033 1034 def _bot_levels_for_wave(self) -> list[list[type[SpazBot]]]: 1035 level = self._wavenum 1036 bot_types = [ 1037 BomberBot, BrawlerBot, TriggerBot, ChargerBot, BomberBotPro, 1038 BrawlerBotPro, TriggerBotPro, BomberBotProShielded, ExplodeyBot, 1039 ChargerBotProShielded, StickyBot, BrawlerBotProShielded, 1040 TriggerBotProShielded 1041 ] 1042 if level > 5: 1043 bot_types += [ 1044 ExplodeyBot, 1045 TriggerBotProShielded, 1046 BrawlerBotProShielded, 1047 ChargerBotProShielded, 1048 ] 1049 if level > 7: 1050 bot_types += [ 1051 ExplodeyBot, 1052 TriggerBotProShielded, 1053 BrawlerBotProShielded, 1054 ChargerBotProShielded, 1055 ] 1056 if level > 10: 1057 bot_types += [ 1058 TriggerBotProShielded, TriggerBotProShielded, 1059 TriggerBotProShielded, TriggerBotProShielded 1060 ] 1061 if level > 13: 1062 bot_types += [ 1063 TriggerBotProShielded, TriggerBotProShielded, 1064 TriggerBotProShielded, TriggerBotProShielded 1065 ] 1066 bot_levels = [[b for b in bot_types if b.points_mult == 1], 1067 [b for b in bot_types if b.points_mult == 2], 1068 [b for b in bot_types if b.points_mult == 3], 1069 [b for b in bot_types if b.points_mult == 4]] 1070 1071 # Make sure all lists have something in them 1072 if not all(bot_levels): 1073 raise RuntimeError('Got empty bot level') 1074 return bot_levels 1075 1076 def _add_entries_for_distribution_group( 1077 self, group: list[tuple[int, int]], 1078 bot_levels: list[list[type[SpazBot]]], 1079 all_entries: list[Spawn | Spacing | Delay | None]) -> None: 1080 entries: list[Spawn | Spacing | Delay | None] = [] 1081 for entry in group: 1082 bot_level = bot_levels[entry[0] - 1] 1083 bot_type = bot_level[random.randrange(len(bot_level))] 1084 rval = random.random() 1085 if rval < 0.5: 1086 spacing = 10.0 1087 elif rval < 0.9: 1088 spacing = 20.0 1089 else: 1090 spacing = 40.0 1091 split = random.random() > 0.3 1092 for i in range(entry[1]): 1093 if split and i % 2 == 0: 1094 entries.insert(0, Spawn(bot_type, spacing=spacing)) 1095 else: 1096 entries.append(Spawn(bot_type, spacing=spacing)) 1097 if entries: 1098 all_entries += entries 1099 all_entries.append( 1100 Spacing(40.0 if random.random() < 0.5 else 80.0)) 1101 1102 def _generate_random_wave(self) -> Wave: 1103 level = self._wavenum 1104 bot_levels = self._bot_levels_for_wave() 1105 1106 target_points = level * 3 - 2 1107 min_dudes = min(1 + level // 3, 10) 1108 max_dudes = min(10, level + 1) 1109 max_level = 4 if level > 6 else (3 if level > 3 else 1110 (2 if level > 2 else 1)) 1111 group_count = 3 1112 distribution = self._get_distribution(target_points, min_dudes, 1113 max_dudes, group_count, 1114 max_level) 1115 all_entries: list[Spawn | Spacing | Delay | None] = [] 1116 for group in distribution: 1117 self._add_entries_for_distribution_group(group, bot_levels, 1118 all_entries) 1119 angle_rand = random.random() 1120 if angle_rand > 0.75: 1121 base_angle = 130.0 1122 elif angle_rand > 0.5: 1123 base_angle = 210.0 1124 elif angle_rand > 0.25: 1125 base_angle = 20.0 1126 else: 1127 base_angle = -30.0 1128 base_angle += (0.5 - random.random()) * 20.0 1129 wave = Wave(base_angle=base_angle, entries=all_entries) 1130 return wave 1131 1132 def add_bot_at_point(self, 1133 point: Point, 1134 spaz_type: type[SpazBot], 1135 spawn_time: float = 1.0) -> None: 1136 """Add a new bot at a specified named point.""" 1137 if self._game_over: 1138 return 1139 assert isinstance(point.value, str) 1140 pointpos = self.map.defs.points[point.value] 1141 assert self._bots is not None 1142 self._bots.spawn_bot(spaz_type, pos=pointpos, spawn_time=spawn_time) 1143 1144 def add_bot_at_angle(self, 1145 angle: float, 1146 spaz_type: type[SpazBot], 1147 spawn_time: float = 1.0) -> None: 1148 """Add a new bot at a specified angle (for circular maps).""" 1149 if self._game_over: 1150 return 1151 angle_radians = angle / 57.2957795 1152 xval = math.sin(angle_radians) * 1.06 1153 zval = math.cos(angle_radians) * 1.06 1154 point = (xval / 0.125, 2.3, (zval / 0.2) - 3.7) 1155 assert self._bots is not None 1156 self._bots.spawn_bot(spaz_type, pos=point, spawn_time=spawn_time) 1157 1158 def _update_time_bonus(self) -> None: 1159 self._time_bonus = int(self._time_bonus * 0.93) 1160 if self._time_bonus > 0 and self._time_bonus_text is not None: 1161 assert self._time_bonus_text.node 1162 self._time_bonus_text.node.text = ba.Lstr( 1163 value='${A}: ${B}', 1164 subs=[('${A}', ba.Lstr(resource='timeBonusText')), 1165 ('${B}', str(self._time_bonus))]) 1166 else: 1167 self._time_bonus_text = None 1168 1169 def _start_updating_waves(self) -> None: 1170 self._wave_update_timer = ba.Timer(2.0, 1171 ba.WeakCall(self._update_waves), 1172 repeat=True) 1173 1174 def _update_scores(self) -> None: 1175 score = self._score 1176 if self._preset is Preset.ENDLESS: 1177 if score >= 500: 1178 self._award_achievement('Onslaught Master') 1179 if score >= 1000: 1180 self._award_achievement('Onslaught Wizard') 1181 if score >= 5000: 1182 self._award_achievement('Onslaught God') 1183 assert self._scoreboard is not None 1184 self._scoreboard.set_team_value(self.teams[0], score, max_score=None) 1185 1186 def handlemessage(self, msg: Any) -> Any: 1187 1188 if isinstance(msg, PlayerSpazHurtMessage): 1189 msg.spaz.getplayer(Player, True).has_been_hurt = True 1190 self._a_player_has_been_hurt = True 1191 1192 elif isinstance(msg, ba.PlayerScoredMessage): 1193 self._score += msg.score 1194 self._update_scores() 1195 1196 elif isinstance(msg, ba.PlayerDiedMessage): 1197 super().handlemessage(msg) # Augment standard behavior. 1198 player = msg.getplayer(Player) 1199 self._a_player_has_been_hurt = True 1200 1201 # Make note with the player when they can respawn: 1202 if self._wavenum < 10: 1203 player.respawn_wave = max(2, self._wavenum + 1) 1204 elif self._wavenum < 15: 1205 player.respawn_wave = max(2, self._wavenum + 2) 1206 else: 1207 player.respawn_wave = max(2, self._wavenum + 3) 1208 ba.timer(0.1, self._update_player_spawn_info) 1209 ba.timer(0.1, self._checkroundover) 1210 1211 elif isinstance(msg, SpazBotDiedMessage): 1212 pts, importance = msg.spazbot.get_death_points(msg.how) 1213 if msg.killerplayer is not None: 1214 self._handle_kill_achievements(msg) 1215 target: Sequence[float] | None 1216 if msg.spazbot.node: 1217 target = msg.spazbot.node.position 1218 else: 1219 target = None 1220 1221 killerplayer = msg.killerplayer 1222 self.stats.player_scored(killerplayer, 1223 pts, 1224 target=target, 1225 kill=True, 1226 screenmessage=False, 1227 importance=importance) 1228 ba.playsound(self._dingsound 1229 if importance == 1 else self._dingsoundhigh, 1230 volume=0.6) 1231 1232 # Normally we pull scores from the score-set, but if there's 1233 # no player lets be explicit. 1234 else: 1235 self._score += pts 1236 self._update_scores() 1237 else: 1238 super().handlemessage(msg) 1239 1240 def _handle_kill_achievements(self, msg: SpazBotDiedMessage) -> None: 1241 if self._preset in {Preset.TRAINING, Preset.TRAINING_EASY}: 1242 self._handle_training_kill_achievements(msg) 1243 elif self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: 1244 self._handle_rookie_kill_achievements(msg) 1245 elif self._preset in {Preset.PRO, Preset.PRO_EASY}: 1246 self._handle_pro_kill_achievements(msg) 1247 elif self._preset in {Preset.UBER, Preset.UBER_EASY}: 1248 self._handle_uber_kill_achievements(msg) 1249 1250 def _handle_uber_kill_achievements(self, msg: SpazBotDiedMessage) -> None: 1251 1252 # Uber mine achievement: 1253 if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): 1254 self._land_mine_kills += 1 1255 if self._land_mine_kills >= 6: 1256 self._award_achievement('Gold Miner') 1257 1258 # Uber tnt achievement: 1259 if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): 1260 self._tnt_kills += 1 1261 if self._tnt_kills >= 6: 1262 ba.timer(0.5, ba.WeakCall(self._award_achievement, 1263 'TNT Terror')) 1264 1265 def _handle_pro_kill_achievements(self, msg: SpazBotDiedMessage) -> None: 1266 1267 # TNT achievement: 1268 if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): 1269 self._tnt_kills += 1 1270 if self._tnt_kills >= 3: 1271 ba.timer( 1272 0.5, 1273 ba.WeakCall(self._award_achievement, 1274 'Boom Goes the Dynamite')) 1275 1276 def _handle_rookie_kill_achievements(self, 1277 msg: SpazBotDiedMessage) -> None: 1278 # Land-mine achievement: 1279 if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): 1280 self._land_mine_kills += 1 1281 if self._land_mine_kills >= 3: 1282 self._award_achievement('Mine Games') 1283 1284 def _handle_training_kill_achievements(self, 1285 msg: SpazBotDiedMessage) -> None: 1286 # Toss-off-map achievement: 1287 if msg.spazbot.last_attacked_type == ('picked_up', 'default'): 1288 self._throw_off_kills += 1 1289 if self._throw_off_kills >= 3: 1290 self._award_achievement('Off You Go Then') 1291 1292 def _set_can_end_wave(self) -> None: 1293 self._can_end_wave = True 1294 1295 def end_game(self) -> None: 1296 # Tell our bots to celebrate just to rub it in. 1297 assert self._bots is not None 1298 self._bots.final_celebrate() 1299 self._game_over = True 1300 self.do_end('defeat', delay=2.0) 1301 ba.setmusic(None) 1302 1303 def on_continue(self) -> None: 1304 for player in self.players: 1305 if not player.is_alive(): 1306 self.spawn_player(player) 1307 1308 def _checkroundover(self) -> None: 1309 """Potentially end the round based on the state of the game.""" 1310 if self.has_ended(): 1311 return 1312 if not any(player.is_alive() for player in self.teams[0].players): 1313 # Allow continuing after wave 1. 1314 if self._wavenum > 1: 1315 self.continue_or_end_game() 1316 else: 1317 self.end_game()
Co-op game where players try to survive attacking waves of enemies.
144 def __init__(self, settings: dict): 145 146 self._preset = Preset(settings.get('preset', 'training')) 147 if self._preset in { 148 Preset.TRAINING, Preset.TRAINING_EASY, Preset.PRO, 149 Preset.PRO_EASY, Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT 150 }: 151 settings['map'] = 'Doom Shroom' 152 else: 153 settings['map'] = 'Courtyard' 154 155 super().__init__(settings) 156 157 self._new_wave_sound = ba.getsound('scoreHit01') 158 self._winsound = ba.getsound('score') 159 self._cashregistersound = ba.getsound('cashRegister') 160 self._a_player_has_been_hurt = False 161 self._player_has_dropped_bomb = False 162 163 # FIXME: should use standard map defs. 164 if settings['map'] == 'Doom Shroom': 165 self._spawn_center = (0, 3, -5) 166 self._tntspawnpos = (0.0, 3.0, -5.0) 167 self._powerup_center = (0, 5, -3.6) 168 self._powerup_spread = (6.0, 4.0) 169 elif settings['map'] == 'Courtyard': 170 self._spawn_center = (0, 3, -2) 171 self._tntspawnpos = (0.0, 3.0, 2.1) 172 self._powerup_center = (0, 5, -1.6) 173 self._powerup_spread = (4.6, 2.7) 174 else: 175 raise Exception('Unsupported map: ' + str(settings['map'])) 176 self._scoreboard: Scoreboard | None = None 177 self._game_over = False 178 self._wavenum = 0 179 self._can_end_wave = True 180 self._score = 0 181 self._time_bonus = 0 182 self._spawn_info_text: ba.NodeActor | None = None 183 self._dingsound = ba.getsound('dingSmall') 184 self._dingsoundhigh = ba.getsound('dingSmallHigh') 185 self._have_tnt = False 186 self._excluded_powerups: list[str] | None = None 187 self._waves: list[Wave] = [] 188 self._tntspawner: TNTSpawner | None = None 189 self._bots: SpazBotSet | None = None 190 self._powerup_drop_timer: ba.Timer | None = None 191 self._time_bonus_timer: ba.Timer | None = None 192 self._time_bonus_text: ba.NodeActor | None = None 193 self._flawless_bonus: int | None = None 194 self._wave_text: ba.NodeActor | None = None 195 self._wave_update_timer: ba.Timer | None = None 196 self._throw_off_kills = 0 197 self._land_mine_kills = 0 198 self._tnt_kills = 0
Instantiate the Activity.
Whether to print every time a player dies. This can be pertinent in games such as Death-Match but can be annoying in games where it doesn't matter.
200 def on_transition_in(self) -> None: 201 super().on_transition_in() 202 customdata = ba.getsession().customdata 203 204 # Show special landmine tip on rookie preset. 205 if self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: 206 # Show once per session only (then we revert to regular tips). 207 if not customdata.get('_showed_onslaught_landmine_tip', False): 208 customdata['_showed_onslaught_landmine_tip'] = True 209 self.tips = [ 210 ba.GameTip( 211 'Land-mines are a good way to stop speedy enemies.', 212 icon=ba.gettexture('powerupLandMines'), 213 sound=ba.getsound('ding')) 214 ] 215 216 # Show special tnt tip on pro preset. 217 if self._preset in {Preset.PRO, Preset.PRO_EASY}: 218 # Show once per session only (then we revert to regular tips). 219 if not customdata.get('_showed_onslaught_tnt_tip', False): 220 customdata['_showed_onslaught_tnt_tip'] = True 221 self.tips = [ 222 ba.GameTip( 223 'Take out a group of enemies by\n' 224 'setting off a bomb near a TNT box.', 225 icon=ba.gettexture('tnt'), 226 sound=ba.getsound('ding')) 227 ] 228 229 # Show special curse tip on uber preset. 230 if self._preset in {Preset.UBER, Preset.UBER_EASY}: 231 # Show once per session only (then we revert to regular tips). 232 if not customdata.get('_showed_onslaught_curse_tip', False): 233 customdata['_showed_onslaught_curse_tip'] = True 234 self.tips = [ 235 ba.GameTip( 236 'Curse boxes turn you into a ticking time bomb.\n' 237 'The only cure is to quickly grab a health-pack.', 238 icon=ba.gettexture('powerupCurse'), 239 sound=ba.getsound('ding')) 240 ] 241 242 self._spawn_info_text = ba.NodeActor( 243 ba.newnode('text', 244 attrs={ 245 'position': (15, -130), 246 'h_attach': 'left', 247 'v_attach': 'top', 248 'scale': 0.55, 249 'color': (0.3, 0.8, 0.3, 1.0), 250 'text': '' 251 })) 252 ba.setmusic(ba.MusicType.ONSLAUGHT) 253 254 self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), 255 score_split=0.5)
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.
257 def on_begin(self) -> None: 258 super().on_begin() 259 player_count = len(self.players) 260 hard = self._preset not in { 261 Preset.TRAINING_EASY, Preset.ROOKIE_EASY, Preset.PRO_EASY, 262 Preset.UBER_EASY 263 } 264 if self._preset in {Preset.TRAINING, Preset.TRAINING_EASY}: 265 ControlsGuide(delay=3.0, lifespan=10.0, bright=True).autoretain() 266 267 self._have_tnt = False 268 self._excluded_powerups = ['curse', 'land_mines'] 269 self._waves = [ 270 Wave(base_angle=195, 271 entries=[ 272 Spawn(BomberBotLite, spacing=5), 273 ] * player_count), 274 Wave(base_angle=130, 275 entries=[ 276 Spawn(BrawlerBotLite, spacing=5), 277 ] * player_count), 278 Wave(base_angle=195, 279 entries=[Spawn(BomberBotLite, spacing=10)] * 280 (player_count + 1)), 281 Wave(base_angle=130, 282 entries=[ 283 Spawn(BrawlerBotLite, spacing=10), 284 ] * (player_count + 1)), 285 Wave(base_angle=130, 286 entries=[ 287 Spawn(BrawlerBotLite, spacing=5) 288 if player_count > 1 else None, 289 Spawn(BrawlerBotLite, spacing=5), 290 Spacing(30), 291 Spawn(BomberBotLite, spacing=5) 292 if player_count > 3 else None, 293 Spawn(BomberBotLite, spacing=5), 294 Spacing(30), 295 Spawn(BrawlerBotLite, spacing=5), 296 Spawn(BrawlerBotLite, spacing=5) 297 if player_count > 2 else None, 298 ]), 299 Wave(base_angle=195, 300 entries=[ 301 Spawn(TriggerBot, spacing=90), 302 Spawn(TriggerBot, spacing=90) 303 if player_count > 1 else None, 304 ]), 305 ] 306 307 elif self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: 308 self._have_tnt = False 309 self._excluded_powerups = ['curse'] 310 self._waves = [ 311 Wave(entries=[ 312 Spawn(ChargerBot, Point.LEFT_UPPER_MORE 313 ) if player_count > 2 else None, 314 Spawn(ChargerBot, Point.LEFT_UPPER), 315 ]), 316 Wave(entries=[ 317 Spawn(BomberBotStaticLite, Point.TURRET_TOP_RIGHT), 318 Spawn(BrawlerBotLite, Point.RIGHT_UPPER), 319 Spawn(BrawlerBotLite, Point.RIGHT_LOWER 320 ) if player_count > 1 else None, 321 Spawn(BomberBotStaticLite, Point.TURRET_BOTTOM_RIGHT 322 ) if player_count > 2 else None, 323 ]), 324 Wave(entries=[ 325 Spawn(BomberBotStaticLite, Point.TURRET_BOTTOM_LEFT), 326 Spawn(TriggerBot, Point.LEFT), 327 Spawn(TriggerBot, Point.LEFT_LOWER 328 ) if player_count > 1 else None, 329 Spawn(TriggerBot, Point.LEFT_UPPER 330 ) if player_count > 2 else None, 331 ]), 332 Wave(entries=[ 333 Spawn(BrawlerBotLite, Point.TOP_RIGHT), 334 Spawn(BrawlerBot, Point.TOP_HALF_RIGHT 335 ) if player_count > 1 else None, 336 Spawn(BrawlerBotLite, Point.TOP_LEFT), 337 Spawn(BrawlerBotLite, Point.TOP_HALF_LEFT 338 ) if player_count > 2 else None, 339 Spawn(BrawlerBot, Point.TOP), 340 Spawn(BomberBotStaticLite, Point.TURRET_TOP_MIDDLE), 341 ]), 342 Wave(entries=[ 343 Spawn(TriggerBotStatic, Point.TURRET_BOTTOM_LEFT), 344 Spawn(TriggerBotStatic, Point.TURRET_BOTTOM_RIGHT), 345 Spawn(TriggerBot, Point.BOTTOM), 346 Spawn(TriggerBot, Point.BOTTOM_HALF_RIGHT 347 ) if player_count > 1 else None, 348 Spawn(TriggerBot, Point.BOTTOM_HALF_LEFT 349 ) if player_count > 2 else None, 350 ]), 351 Wave(entries=[ 352 Spawn(BomberBotStaticLite, Point.TURRET_TOP_LEFT), 353 Spawn(BomberBotStaticLite, Point.TURRET_TOP_RIGHT), 354 Spawn(ChargerBot, Point.BOTTOM), 355 Spawn(ChargerBot, Point.BOTTOM_HALF_LEFT 356 ) if player_count > 1 else None, 357 Spawn(ChargerBot, Point.BOTTOM_HALF_RIGHT 358 ) if player_count > 2 else None, 359 ]), 360 ] 361 362 elif self._preset in {Preset.PRO, Preset.PRO_EASY}: 363 self._excluded_powerups = ['curse'] 364 self._have_tnt = True 365 self._waves = [ 366 Wave(base_angle=-50, 367 entries=[ 368 Spawn(BrawlerBot, spacing=12) 369 if player_count > 3 else None, 370 Spawn(BrawlerBot, spacing=12), 371 Spawn(BomberBot, spacing=6), 372 Spawn(BomberBot, spacing=6) 373 if self._preset is Preset.PRO else None, 374 Spawn(BomberBot, spacing=6) 375 if player_count > 1 else None, 376 Spawn(BrawlerBot, spacing=12), 377 Spawn(BrawlerBot, spacing=12) 378 if player_count > 2 else None, 379 ]), 380 Wave(base_angle=180, 381 entries=[ 382 Spawn(BrawlerBot, spacing=6) 383 if player_count > 3 else None, 384 Spawn(BrawlerBot, spacing=6) 385 if self._preset is Preset.PRO else None, 386 Spawn(BrawlerBot, spacing=6), 387 Spawn(ChargerBot, spacing=45), 388 Spawn(ChargerBot, spacing=45) 389 if player_count > 1 else None, 390 Spawn(BrawlerBot, spacing=6), 391 Spawn(BrawlerBot, spacing=6) 392 if self._preset is Preset.PRO else None, 393 Spawn(BrawlerBot, spacing=6) 394 if player_count > 2 else None, 395 ]), 396 Wave(base_angle=0, 397 entries=[ 398 Spawn(ChargerBot, spacing=30), 399 Spawn(TriggerBot, spacing=30), 400 Spawn(TriggerBot, spacing=30), 401 Spawn(TriggerBot, spacing=30) 402 if self._preset is Preset.PRO else None, 403 Spawn(TriggerBot, spacing=30) 404 if player_count > 1 else None, 405 Spawn(TriggerBot, spacing=30) 406 if player_count > 3 else None, 407 Spawn(ChargerBot, spacing=30), 408 ]), 409 Wave(base_angle=90, 410 entries=[ 411 Spawn(StickyBot, spacing=50), 412 Spawn(StickyBot, spacing=50) 413 if self._preset is Preset.PRO else None, 414 Spawn(StickyBot, spacing=50), 415 Spawn(StickyBot, spacing=50) 416 if player_count > 1 else None, 417 Spawn(StickyBot, spacing=50) 418 if player_count > 3 else None, 419 ]), 420 Wave(base_angle=0, 421 entries=[ 422 Spawn(TriggerBot, spacing=72), 423 Spawn(TriggerBot, spacing=72), 424 Spawn(TriggerBot, spacing=72) 425 if self._preset is Preset.PRO else None, 426 Spawn(TriggerBot, spacing=72), 427 Spawn(TriggerBot, spacing=72), 428 Spawn(TriggerBot, spacing=36) 429 if player_count > 2 else None, 430 ]), 431 Wave(base_angle=30, 432 entries=[ 433 Spawn(ChargerBotProShielded, spacing=50), 434 Spawn(ChargerBotProShielded, spacing=50), 435 Spawn(ChargerBotProShielded, spacing=50) 436 if self._preset is Preset.PRO else None, 437 Spawn(ChargerBotProShielded, spacing=50) 438 if player_count > 1 else None, 439 Spawn(ChargerBotProShielded, spacing=50) 440 if player_count > 2 else None, 441 ]) 442 ] 443 444 elif self._preset in {Preset.UBER, Preset.UBER_EASY}: 445 446 # Show controls help in demo/arcade modes. 447 if ba.app.demo_mode or ba.app.arcade_mode: 448 ControlsGuide(delay=3.0, lifespan=10.0, 449 bright=True).autoretain() 450 451 self._have_tnt = True 452 self._excluded_powerups = [] 453 self._waves = [ 454 Wave(entries=[ 455 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_LEFT 456 ) if hard else None, 457 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_RIGHT), 458 Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT 459 ) if player_count > 2 else None, 460 Spawn(ExplodeyBot, Point.TOP_RIGHT), 461 Delay(4.0), 462 Spawn(ExplodeyBot, Point.TOP_LEFT), 463 ]), 464 Wave(entries=[ 465 Spawn(ChargerBot, Point.LEFT), 466 Spawn(ChargerBot, Point.RIGHT), 467 Spawn(ChargerBot, Point.RIGHT_UPPER_MORE 468 ) if player_count > 2 else None, 469 Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT), 470 Spawn(BomberBotProStatic, Point.TURRET_TOP_RIGHT), 471 ]), 472 Wave(entries=[ 473 Spawn(TriggerBotPro, Point.TOP_RIGHT), 474 Spawn(TriggerBotPro, Point.RIGHT_UPPER_MORE 475 ) if player_count > 1 else None, 476 Spawn(TriggerBotPro, Point.RIGHT_UPPER), 477 Spawn(TriggerBotPro, Point.RIGHT_LOWER) if hard else None, 478 Spawn(TriggerBotPro, Point.RIGHT_LOWER_MORE 479 ) if player_count > 2 else None, 480 Spawn(TriggerBotPro, Point.BOTTOM_RIGHT), 481 ]), 482 Wave(entries=[ 483 Spawn(ChargerBotProShielded, Point.BOTTOM_RIGHT), 484 Spawn(ChargerBotProShielded, Point.BOTTOM 485 ) if player_count > 2 else None, 486 Spawn(ChargerBotProShielded, Point.BOTTOM_LEFT), 487 Spawn(ChargerBotProShielded, Point.TOP) if hard else None, 488 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE), 489 ]), 490 Wave(entries=[ 491 Spawn(ExplodeyBot, Point.LEFT_UPPER), 492 Delay(1.0), 493 Spawn(BrawlerBotProShielded, Point.LEFT_LOWER), 494 Spawn(BrawlerBotProShielded, Point.LEFT_LOWER_MORE), 495 Delay(4.0), 496 Spawn(ExplodeyBot, Point.RIGHT_UPPER), 497 Delay(1.0), 498 Spawn(BrawlerBotProShielded, Point.RIGHT_LOWER), 499 Spawn(BrawlerBotProShielded, Point.RIGHT_UPPER_MORE), 500 Delay(4.0), 501 Spawn(ExplodeyBot, Point.LEFT), 502 Delay(5.0), 503 Spawn(ExplodeyBot, Point.RIGHT), 504 ]), 505 Wave(entries=[ 506 Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT), 507 Spawn(BomberBotProStatic, Point.TURRET_TOP_RIGHT), 508 Spawn(BomberBotProStatic, Point.TURRET_BOTTOM_LEFT), 509 Spawn(BomberBotProStatic, Point.TURRET_BOTTOM_RIGHT), 510 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_LEFT 511 ) if hard else None, 512 Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_RIGHT 513 ) if hard else None, 514 ]) 515 ] 516 517 # We generate these on the fly in endless. 518 elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: 519 self._have_tnt = True 520 self._excluded_powerups = [] 521 self._waves = [] 522 523 else: 524 raise RuntimeError(f'Invalid preset: {self._preset}') 525 526 # FIXME: Should migrate to use setup_standard_powerup_drops(). 527 528 # Spit out a few powerups and start dropping more shortly. 529 self._drop_powerups(standard_points=True, 530 poweruptype='curse' if self._preset 531 in [Preset.UBER, Preset.UBER_EASY] else 532 ('land_mines' if self._preset 533 in [Preset.ROOKIE, Preset.ROOKIE_EASY] else None)) 534 ba.timer(4.0, self._start_powerup_drops) 535 536 # Our TNT spawner (if applicable). 537 if self._have_tnt: 538 self._tntspawner = TNTSpawner(position=self._tntspawnpos) 539 540 self.setup_low_life_warning_sound() 541 self._update_scores() 542 self._bots = SpazBotSet() 543 ba.timer(4.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.
676 def spawn_player(self, player: Player) -> ba.Actor: 677 678 # We keep track of who got hurt each wave for score purposes. 679 player.has_been_hurt = False 680 pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5), 681 self._spawn_center[1], 682 self._spawn_center[2] + random.uniform(-1.5, 1.5)) 683 spaz = self.spawn_player_spaz(player, position=pos) 684 if self._preset in { 685 Preset.TRAINING_EASY, Preset.ROOKIE_EASY, Preset.PRO_EASY, 686 Preset.UBER_EASY 687 }: 688 spaz.impact_scale = 0.25 689 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) 690 return spaz
Spawn something for the provided ba.Player.
The default implementation simply calls spawn_player_spaz().
734 def do_end(self, outcome: str, delay: float = 0.0) -> None: 735 """End the game with the specified outcome.""" 736 if outcome == 'defeat': 737 self.fade_to_red() 738 score: int | None 739 if self._wavenum >= 2: 740 score = self._score 741 fail_message = None 742 else: 743 score = None 744 fail_message = ba.Lstr(resource='reachWave2Text') 745 self.end( 746 { 747 'outcome': outcome, 748 'score': score, 749 'fail_message': fail_message, 750 'playerinfos': self.initialplayerinfos 751 }, 752 delay=delay)
End the game with the specified outcome.
1132 def add_bot_at_point(self, 1133 point: Point, 1134 spaz_type: type[SpazBot], 1135 spawn_time: float = 1.0) -> None: 1136 """Add a new bot at a specified named point.""" 1137 if self._game_over: 1138 return 1139 assert isinstance(point.value, str) 1140 pointpos = self.map.defs.points[point.value] 1141 assert self._bots is not None 1142 self._bots.spawn_bot(spaz_type, pos=pointpos, spawn_time=spawn_time)
Add a new bot at a specified named point.
1144 def add_bot_at_angle(self, 1145 angle: float, 1146 spaz_type: type[SpazBot], 1147 spawn_time: float = 1.0) -> None: 1148 """Add a new bot at a specified angle (for circular maps).""" 1149 if self._game_over: 1150 return 1151 angle_radians = angle / 57.2957795 1152 xval = math.sin(angle_radians) * 1.06 1153 zval = math.cos(angle_radians) * 1.06 1154 point = (xval / 0.125, 2.3, (zval / 0.2) - 3.7) 1155 assert self._bots is not None 1156 self._bots.spawn_bot(spaz_type, pos=point, spawn_time=spawn_time)
Add a new bot at a specified angle (for circular maps).
1186 def handlemessage(self, msg: Any) -> Any: 1187 1188 if isinstance(msg, PlayerSpazHurtMessage): 1189 msg.spaz.getplayer(Player, True).has_been_hurt = True 1190 self._a_player_has_been_hurt = True 1191 1192 elif isinstance(msg, ba.PlayerScoredMessage): 1193 self._score += msg.score 1194 self._update_scores() 1195 1196 elif isinstance(msg, ba.PlayerDiedMessage): 1197 super().handlemessage(msg) # Augment standard behavior. 1198 player = msg.getplayer(Player) 1199 self._a_player_has_been_hurt = True 1200 1201 # Make note with the player when they can respawn: 1202 if self._wavenum < 10: 1203 player.respawn_wave = max(2, self._wavenum + 1) 1204 elif self._wavenum < 15: 1205 player.respawn_wave = max(2, self._wavenum + 2) 1206 else: 1207 player.respawn_wave = max(2, self._wavenum + 3) 1208 ba.timer(0.1, self._update_player_spawn_info) 1209 ba.timer(0.1, self._checkroundover) 1210 1211 elif isinstance(msg, SpazBotDiedMessage): 1212 pts, importance = msg.spazbot.get_death_points(msg.how) 1213 if msg.killerplayer is not None: 1214 self._handle_kill_achievements(msg) 1215 target: Sequence[float] | None 1216 if msg.spazbot.node: 1217 target = msg.spazbot.node.position 1218 else: 1219 target = None 1220 1221 killerplayer = msg.killerplayer 1222 self.stats.player_scored(killerplayer, 1223 pts, 1224 target=target, 1225 kill=True, 1226 screenmessage=False, 1227 importance=importance) 1228 ba.playsound(self._dingsound 1229 if importance == 1 else self._dingsoundhigh, 1230 volume=0.6) 1231 1232 # Normally we pull scores from the score-set, but if there's 1233 # no player lets be explicit. 1234 else: 1235 self._score += pts 1236 self._update_scores() 1237 else: 1238 super().handlemessage(msg)
General message handling; can be passed any message object.
1295 def end_game(self) -> None: 1296 # Tell our bots to celebrate just to rub it in. 1297 assert self._bots is not None 1298 self._bots.final_celebrate() 1299 self._game_over = True 1300 self.do_end('defeat', delay=2.0) 1301 ba.setmusic(None)
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.
1303 def on_continue(self) -> None: 1304 for player in self.players: 1305 if not player.is_alive(): 1306 self.spawn_player(player)
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.
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
- default_music
- 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
- 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