bastd.game.capturetheflag
Defines a capture-the-flag game.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Defines a capture-the-flag game.""" 4 5# ba_meta require api 7 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10from typing import TYPE_CHECKING 11 12import ba 13from bastd.actor.playerspaz import PlayerSpaz 14from bastd.actor.scoreboard import Scoreboard 15from bastd.actor.flag import ( 16 FlagFactory, 17 Flag, 18 FlagPickedUpMessage, 19 FlagDroppedMessage, 20 FlagDiedMessage, 21) 22 23if TYPE_CHECKING: 24 from typing import Any, Sequence 25 26 27class CTFFlag(Flag): 28 """Special flag type for CTF games.""" 29 30 activity: CaptureTheFlagGame 31 32 def __init__(self, team: Team): 33 assert team.flagmaterial is not None 34 super().__init__( 35 materials=[team.flagmaterial], 36 position=team.base_pos, 37 color=team.color, 38 ) 39 self._team = team 40 self.held_count = 0 41 self.counter = ba.newnode( 42 'text', 43 owner=self.node, 44 attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'}, 45 ) 46 self.reset_return_times() 47 self.last_player_to_hold: Player | None = None 48 self.time_out_respawn_time: int | None = None 49 self.touch_return_time: float | None = None 50 51 def reset_return_times(self) -> None: 52 """Clear flag related times in the activity.""" 53 self.time_out_respawn_time = int(self.activity.flag_idle_return_time) 54 self.touch_return_time = float(self.activity.flag_touch_return_time) 55 56 @property 57 def team(self) -> Team: 58 """The flag's team.""" 59 return self._team 60 61 62class Player(ba.Player['Team']): 63 """Our player type for this game.""" 64 65 def __init__(self) -> None: 66 self.touching_own_flag = 0 67 68 69class Team(ba.Team[Player]): 70 """Our team type for this game.""" 71 72 def __init__( 73 self, 74 base_pos: Sequence[float], 75 base_region_material: ba.Material, 76 base_region: ba.Node, 77 spaz_material_no_flag_physical: ba.Material, 78 spaz_material_no_flag_collide: ba.Material, 79 flagmaterial: ba.Material, 80 ): 81 self.base_pos = base_pos 82 self.base_region_material = base_region_material 83 self.base_region = base_region 84 self.spaz_material_no_flag_physical = spaz_material_no_flag_physical 85 self.spaz_material_no_flag_collide = spaz_material_no_flag_collide 86 self.flagmaterial = flagmaterial 87 self.score = 0 88 self.flag_return_touches = 0 89 self.home_flag_at_base = True 90 self.touch_return_timer: ba.Timer | None = None 91 self.enemy_flag_at_base = False 92 self.flag: CTFFlag | None = None 93 self.last_flag_leave_time: float | None = None 94 self.touch_return_timer_ticking: ba.NodeActor | None = None 95 96 97# ba_meta export game 98class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): 99 """Game of stealing other team's flag and returning it to your base.""" 100 101 name = 'Capture the Flag' 102 description = 'Return the enemy flag to score.' 103 available_settings = [ 104 ba.IntSetting('Score to Win', min_value=1, default=3), 105 ba.IntSetting( 106 'Flag Touch Return Time', 107 min_value=0, 108 default=0, 109 increment=1, 110 ), 111 ba.IntSetting( 112 'Flag Idle Return Time', 113 min_value=5, 114 default=30, 115 increment=5, 116 ), 117 ba.IntChoiceSetting( 118 'Time Limit', 119 choices=[ 120 ('None', 0), 121 ('1 Minute', 60), 122 ('2 Minutes', 120), 123 ('5 Minutes', 300), 124 ('10 Minutes', 600), 125 ('20 Minutes', 1200), 126 ], 127 default=0, 128 ), 129 ba.FloatChoiceSetting( 130 'Respawn Times', 131 choices=[ 132 ('Shorter', 0.25), 133 ('Short', 0.5), 134 ('Normal', 1.0), 135 ('Long', 2.0), 136 ('Longer', 4.0), 137 ], 138 default=1.0, 139 ), 140 ba.BoolSetting('Epic Mode', default=False), 141 ] 142 143 @classmethod 144 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 145 return issubclass(sessiontype, ba.DualTeamSession) 146 147 @classmethod 148 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 149 return ba.getmaps('team_flag') 150 151 def __init__(self, settings: dict): 152 super().__init__(settings) 153 self._scoreboard = Scoreboard() 154 self._alarmsound = ba.getsound('alarm') 155 self._ticking_sound = ba.getsound('ticking') 156 self._score_sound = ba.getsound('score') 157 self._swipsound = ba.getsound('swip') 158 self._last_score_time = 0 159 self._all_bases_material = ba.Material() 160 self._last_home_flag_notice_print_time = 0.0 161 self._score_to_win = int(settings['Score to Win']) 162 self._epic_mode = bool(settings['Epic Mode']) 163 self._time_limit = float(settings['Time Limit']) 164 165 self.flag_touch_return_time = float(settings['Flag Touch Return Time']) 166 self.flag_idle_return_time = float(settings['Flag Idle Return Time']) 167 168 # Base class overrides. 169 self.slow_motion = self._epic_mode 170 self.default_music = ( 171 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FLAG_CATCHER 172 ) 173 174 def get_instance_description(self) -> str | Sequence: 175 if self._score_to_win == 1: 176 return 'Steal the enemy flag.' 177 return 'Steal the enemy flag ${ARG1} times.', self._score_to_win 178 179 def get_instance_description_short(self) -> str | Sequence: 180 if self._score_to_win == 1: 181 return 'return 1 flag' 182 return 'return ${ARG1} flags', self._score_to_win 183 184 def create_team(self, sessionteam: ba.SessionTeam) -> Team: 185 186 # Create our team instance and its initial values. 187 188 base_pos = self.map.get_flag_position(sessionteam.id) 189 Flag.project_stand(base_pos) 190 191 ba.newnode( 192 'light', 193 attrs={ 194 'position': base_pos, 195 'intensity': 0.6, 196 'height_attenuated': False, 197 'volume_intensity_scale': 0.1, 198 'radius': 0.1, 199 'color': sessionteam.color, 200 }, 201 ) 202 203 base_region_mat = ba.Material() 204 pos = base_pos 205 base_region = ba.newnode( 206 'region', 207 attrs={ 208 'position': (pos[0], pos[1] + 0.75, pos[2]), 209 'scale': (0.5, 0.5, 0.5), 210 'type': 'sphere', 211 'materials': [base_region_mat, self._all_bases_material], 212 }, 213 ) 214 215 spaz_mat_no_flag_physical = ba.Material() 216 spaz_mat_no_flag_collide = ba.Material() 217 flagmat = ba.Material() 218 219 team = Team( 220 base_pos=base_pos, 221 base_region_material=base_region_mat, 222 base_region=base_region, 223 spaz_material_no_flag_physical=spaz_mat_no_flag_physical, 224 spaz_material_no_flag_collide=spaz_mat_no_flag_collide, 225 flagmaterial=flagmat, 226 ) 227 228 # Some parts of our spazzes don't collide physically with our 229 # flags but generate callbacks. 230 spaz_mat_no_flag_physical.add_actions( 231 conditions=('they_have_material', flagmat), 232 actions=( 233 ('modify_part_collision', 'physical', False), 234 ( 235 'call', 236 'at_connect', 237 lambda: self._handle_touching_own_flag(team, True), 238 ), 239 ( 240 'call', 241 'at_disconnect', 242 lambda: self._handle_touching_own_flag(team, False), 243 ), 244 ), 245 ) 246 247 # Other parts of our spazzes don't collide with our flags at all. 248 spaz_mat_no_flag_collide.add_actions( 249 conditions=('they_have_material', flagmat), 250 actions=('modify_part_collision', 'collide', False), 251 ) 252 253 # We wanna know when *any* flag enters/leaves our base. 254 base_region_mat.add_actions( 255 conditions=('they_have_material', FlagFactory.get().flagmaterial), 256 actions=( 257 ('modify_part_collision', 'collide', True), 258 ('modify_part_collision', 'physical', False), 259 ( 260 'call', 261 'at_connect', 262 lambda: self._handle_flag_entered_base(team), 263 ), 264 ( 265 'call', 266 'at_disconnect', 267 lambda: self._handle_flag_left_base(team), 268 ), 269 ), 270 ) 271 272 return team 273 274 def on_team_join(self, team: Team) -> None: 275 # Can't do this in create_team because the team's color/etc. have 276 # not been wired up yet at that point. 277 self._spawn_flag_for_team(team) 278 self._update_scoreboard() 279 280 def on_begin(self) -> None: 281 super().on_begin() 282 self.setup_standard_time_limit(self._time_limit) 283 self.setup_standard_powerup_drops() 284 ba.timer(1.0, call=self._tick, repeat=True) 285 286 def _spawn_flag_for_team(self, team: Team) -> None: 287 team.flag = CTFFlag(team) 288 team.flag_return_touches = 0 289 self._flash_base(team, length=1.0) 290 assert team.flag.node 291 ba.playsound(self._swipsound, position=team.flag.node.position) 292 293 def _handle_flag_entered_base(self, team: Team) -> None: 294 try: 295 flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True) 296 except ba.NotFoundError: 297 # Don't think this should logically ever happen. 298 print('Error getting CTFFlag in entering-base callback.') 299 return 300 301 if flag.team is team: 302 team.home_flag_at_base = True 303 304 # If the enemy flag is already here, score! 305 if team.enemy_flag_at_base: 306 # And show team name which scored (but actually we could 307 # show here player who returned enemy flag). 308 self.show_zoom_message( 309 ba.Lstr( 310 resource='nameScoresText', subs=[('${NAME}', team.name)] 311 ), 312 color=team.color, 313 ) 314 self._score(team) 315 else: 316 team.enemy_flag_at_base = True 317 if team.home_flag_at_base: 318 # Award points to whoever was carrying the enemy flag. 319 player = flag.last_player_to_hold 320 if player and player.team is team: 321 assert self.stats 322 self.stats.player_scored(player, 50, big_message=True) 323 324 # Update score and reset flags. 325 self._score(team) 326 327 # If the home-team flag isn't here, print a message to that effect. 328 else: 329 # Don't want slo-mo affecting this 330 curtime = ba.time(ba.TimeType.BASE) 331 if curtime - self._last_home_flag_notice_print_time > 5.0: 332 self._last_home_flag_notice_print_time = curtime 333 bpos = team.base_pos 334 tval = ba.Lstr(resource='ownFlagAtYourBaseWarning') 335 tnode = ba.newnode( 336 'text', 337 attrs={ 338 'text': tval, 339 'in_world': True, 340 'scale': 0.013, 341 'color': (1, 1, 0, 1), 342 'h_align': 'center', 343 'position': (bpos[0], bpos[1] + 3.2, bpos[2]), 344 }, 345 ) 346 ba.timer(5.1, tnode.delete) 347 ba.animate( 348 tnode, 'scale', {0.0: 0, 0.2: 0.013, 4.8: 0.013, 5.0: 0} 349 ) 350 351 def _tick(self) -> None: 352 # If either flag is away from base and not being held, tick down its 353 # respawn timer. 354 for team in self.teams: 355 flag = team.flag 356 assert flag is not None 357 358 if not team.home_flag_at_base and flag.held_count == 0: 359 time_out_counting_down = True 360 if flag.time_out_respawn_time is None: 361 flag.reset_return_times() 362 assert flag.time_out_respawn_time is not None 363 flag.time_out_respawn_time -= 1 364 if flag.time_out_respawn_time <= 0: 365 flag.handlemessage(ba.DieMessage()) 366 else: 367 time_out_counting_down = False 368 369 if flag.node and flag.counter: 370 pos = flag.node.position 371 flag.counter.position = (pos[0], pos[1] + 1.3, pos[2]) 372 373 # If there's no self-touches on this flag, set its text 374 # to show its auto-return counter. (if there's self-touches 375 # its showing that time). 376 if team.flag_return_touches == 0: 377 flag.counter.text = ( 378 str(flag.time_out_respawn_time) 379 if ( 380 time_out_counting_down 381 and flag.time_out_respawn_time is not None 382 and flag.time_out_respawn_time <= 10 383 ) 384 else '' 385 ) 386 flag.counter.color = (1, 1, 1, 0.5) 387 flag.counter.scale = 0.014 388 389 def _score(self, team: Team) -> None: 390 team.score += 1 391 ba.playsound(self._score_sound) 392 self._flash_base(team) 393 self._update_scoreboard() 394 395 # Have teammates celebrate. 396 for player in team.players: 397 if player.actor: 398 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 399 400 # Reset all flags/state. 401 for reset_team in self.teams: 402 if not reset_team.home_flag_at_base: 403 assert reset_team.flag is not None 404 reset_team.flag.handlemessage(ba.DieMessage()) 405 reset_team.enemy_flag_at_base = False 406 if team.score >= self._score_to_win: 407 self.end_game() 408 409 def end_game(self) -> None: 410 results = ba.GameResults() 411 for team in self.teams: 412 results.set_team_score(team, team.score) 413 self.end(results=results, announce_delay=0.8) 414 415 def _handle_flag_left_base(self, team: Team) -> None: 416 cur_time = ba.time() 417 try: 418 flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True) 419 except ba.NotFoundError: 420 # This can happen if the flag stops touching us due to being 421 # deleted; that's ok. 422 return 423 424 if flag.team is team: 425 426 # Check times here to prevent too much flashing. 427 if ( 428 team.last_flag_leave_time is None 429 or cur_time - team.last_flag_leave_time > 3.0 430 ): 431 ba.playsound(self._alarmsound, position=team.base_pos) 432 self._flash_base(team) 433 team.last_flag_leave_time = cur_time 434 team.home_flag_at_base = False 435 else: 436 team.enemy_flag_at_base = False 437 438 def _touch_return_update(self, team: Team) -> None: 439 # Count down only while its away from base and not being held. 440 assert team.flag is not None 441 if team.home_flag_at_base or team.flag.held_count > 0: 442 team.touch_return_timer_ticking = None 443 return # No need to return when its at home. 444 if team.touch_return_timer_ticking is None: 445 team.touch_return_timer_ticking = ba.NodeActor( 446 ba.newnode( 447 'sound', 448 attrs={ 449 'sound': self._ticking_sound, 450 'positional': False, 451 'loop': True, 452 }, 453 ) 454 ) 455 flag = team.flag 456 if flag.touch_return_time is not None: 457 flag.touch_return_time -= 0.1 458 if flag.counter: 459 flag.counter.text = f'{flag.touch_return_time:.1f}' 460 flag.counter.color = (1, 1, 0, 1) 461 flag.counter.scale = 0.02 462 463 if flag.touch_return_time <= 0.0: 464 self._award_players_touching_own_flag(team) 465 flag.handlemessage(ba.DieMessage()) 466 467 def _award_players_touching_own_flag(self, team: Team) -> None: 468 for player in team.players: 469 if player.touching_own_flag > 0: 470 return_score = 10 + 5 * int(self.flag_touch_return_time) 471 self.stats.player_scored( 472 player, return_score, screenmessage=False 473 ) 474 475 def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None: 476 """Called when a player touches or stops touching their own team flag. 477 478 We keep track of when each player is touching their own flag so we 479 can award points when returned. 480 """ 481 player: Player | None 482 try: 483 spaz = ba.getcollision().sourcenode.getdelegate(PlayerSpaz, True) 484 except ba.NotFoundError: 485 return 486 487 if not spaz.is_alive(): 488 return 489 490 player = spaz.getplayer(Player, True) 491 492 if player: 493 player.touching_own_flag += 1 if connecting else -1 494 495 # If return-time is zero, just kill it immediately.. otherwise keep 496 # track of touches and count down. 497 if float(self.flag_touch_return_time) <= 0.0: 498 assert team.flag is not None 499 if ( 500 connecting 501 and not team.home_flag_at_base 502 and team.flag.held_count == 0 503 ): 504 self._award_players_touching_own_flag(team) 505 ba.getcollision().opposingnode.handlemessage(ba.DieMessage()) 506 507 # Takes a non-zero amount of time to return. 508 else: 509 if connecting: 510 team.flag_return_touches += 1 511 if team.flag_return_touches == 1: 512 team.touch_return_timer = ba.Timer( 513 0.1, 514 call=ba.Call(self._touch_return_update, team), 515 repeat=True, 516 ) 517 team.touch_return_timer_ticking = None 518 else: 519 team.flag_return_touches -= 1 520 if team.flag_return_touches == 0: 521 team.touch_return_timer = None 522 team.touch_return_timer_ticking = None 523 if team.flag_return_touches < 0: 524 ba.print_error('CTF flag_return_touches < 0') 525 526 def _flash_base(self, team: Team, length: float = 2.0) -> None: 527 light = ba.newnode( 528 'light', 529 attrs={ 530 'position': team.base_pos, 531 'height_attenuated': False, 532 'radius': 0.3, 533 'color': team.color, 534 }, 535 ) 536 ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True) 537 ba.timer(length, light.delete) 538 539 def spawn_player_spaz( 540 self, 541 player: Player, 542 position: Sequence[float] | None = None, 543 angle: float | None = None, 544 ) -> PlayerSpaz: 545 """Intercept new spazzes and add our team material for them.""" 546 spaz = super().spawn_player_spaz(player, position, angle) 547 player = spaz.getplayer(Player, True) 548 team: Team = player.team 549 player.touching_own_flag = 0 550 no_physical_mats: list[ba.Material] = [ 551 team.spaz_material_no_flag_physical 552 ] 553 no_collide_mats: list[ba.Material] = [ 554 team.spaz_material_no_flag_collide 555 ] 556 557 # Our normal parts should still collide; just not physically 558 # (so we can calc restores). 559 assert spaz.node 560 spaz.node.materials = list(spaz.node.materials) + no_physical_mats 561 spaz.node.roller_materials = ( 562 list(spaz.node.roller_materials) + no_physical_mats 563 ) 564 565 # Pickups and punches shouldn't hit at all though. 566 spaz.node.punch_materials = ( 567 list(spaz.node.punch_materials) + no_collide_mats 568 ) 569 spaz.node.pickup_materials = ( 570 list(spaz.node.pickup_materials) + no_collide_mats 571 ) 572 spaz.node.extras_material = ( 573 list(spaz.node.extras_material) + no_collide_mats 574 ) 575 return spaz 576 577 def _update_scoreboard(self) -> None: 578 for team in self.teams: 579 self._scoreboard.set_team_value( 580 team, team.score, self._score_to_win 581 ) 582 583 def handlemessage(self, msg: Any) -> Any: 584 585 if isinstance(msg, ba.PlayerDiedMessage): 586 super().handlemessage(msg) # Augment standard behavior. 587 self.respawn_player(msg.getplayer(Player)) 588 589 elif isinstance(msg, FlagDiedMessage): 590 assert isinstance(msg.flag, CTFFlag) 591 ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team)) 592 593 elif isinstance(msg, FlagPickedUpMessage): 594 595 # Store the last player to hold the flag for scoring purposes. 596 assert isinstance(msg.flag, CTFFlag) 597 try: 598 msg.flag.last_player_to_hold = msg.node.getdelegate( 599 PlayerSpaz, True 600 ).getplayer(Player, True) 601 except ba.NotFoundError: 602 pass 603 604 msg.flag.held_count += 1 605 msg.flag.reset_return_times() 606 607 elif isinstance(msg, FlagDroppedMessage): 608 # Store the last player to hold the flag for scoring purposes. 609 assert isinstance(msg.flag, CTFFlag) 610 msg.flag.held_count -= 1 611 612 else: 613 super().handlemessage(msg)
28class CTFFlag(Flag): 29 """Special flag type for CTF games.""" 30 31 activity: CaptureTheFlagGame 32 33 def __init__(self, team: Team): 34 assert team.flagmaterial is not None 35 super().__init__( 36 materials=[team.flagmaterial], 37 position=team.base_pos, 38 color=team.color, 39 ) 40 self._team = team 41 self.held_count = 0 42 self.counter = ba.newnode( 43 'text', 44 owner=self.node, 45 attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'}, 46 ) 47 self.reset_return_times() 48 self.last_player_to_hold: Player | None = None 49 self.time_out_respawn_time: int | None = None 50 self.touch_return_time: float | None = None 51 52 def reset_return_times(self) -> None: 53 """Clear flag related times in the activity.""" 54 self.time_out_respawn_time = int(self.activity.flag_idle_return_time) 55 self.touch_return_time = float(self.activity.flag_touch_return_time) 56 57 @property 58 def team(self) -> Team: 59 """The flag's team.""" 60 return self._team
Special flag type for CTF games.
33 def __init__(self, team: Team): 34 assert team.flagmaterial is not None 35 super().__init__( 36 materials=[team.flagmaterial], 37 position=team.base_pos, 38 color=team.color, 39 ) 40 self._team = team 41 self.held_count = 0 42 self.counter = ba.newnode( 43 'text', 44 owner=self.node, 45 attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'}, 46 ) 47 self.reset_return_times() 48 self.last_player_to_hold: Player | None = None 49 self.time_out_respawn_time: int | None = None 50 self.touch_return_time: float | None = None
Instantiate a flag.
If 'touchable' is False, the flag will only touch terrain; useful for things like king-of-the-hill where players should not be moving the flag around.
'materials can be a list of extra ba.Material
s to apply to the flag.
If 'dropped_timeout' is provided (in seconds), the flag will die after remaining untouched for that long once it has been moved from its initial position.
The Activity this Actor was created in.
Raises a ba.ActivityNotFoundError if the Activity no longer exists.
52 def reset_return_times(self) -> None: 53 """Clear flag related times in the activity.""" 54 self.time_out_respawn_time = int(self.activity.flag_idle_return_time) 55 self.touch_return_time = float(self.activity.flag_touch_return_time)
Clear flag related times in the activity.
Inherited Members
- ba._actor.Actor
- autoretain
- on_expire
- expired
- exists
- is_alive
- getactivity
63class Player(ba.Player['Team']): 64 """Our player type for this game.""" 65 66 def __init__(self) -> None: 67 self.touching_own_flag = 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
70class Team(ba.Team[Player]): 71 """Our team type for this game.""" 72 73 def __init__( 74 self, 75 base_pos: Sequence[float], 76 base_region_material: ba.Material, 77 base_region: ba.Node, 78 spaz_material_no_flag_physical: ba.Material, 79 spaz_material_no_flag_collide: ba.Material, 80 flagmaterial: ba.Material, 81 ): 82 self.base_pos = base_pos 83 self.base_region_material = base_region_material 84 self.base_region = base_region 85 self.spaz_material_no_flag_physical = spaz_material_no_flag_physical 86 self.spaz_material_no_flag_collide = spaz_material_no_flag_collide 87 self.flagmaterial = flagmaterial 88 self.score = 0 89 self.flag_return_touches = 0 90 self.home_flag_at_base = True 91 self.touch_return_timer: ba.Timer | None = None 92 self.enemy_flag_at_base = False 93 self.flag: CTFFlag | None = None 94 self.last_flag_leave_time: float | None = None 95 self.touch_return_timer_ticking: ba.NodeActor | None = None
Our team type for this game.
73 def __init__( 74 self, 75 base_pos: Sequence[float], 76 base_region_material: ba.Material, 77 base_region: ba.Node, 78 spaz_material_no_flag_physical: ba.Material, 79 spaz_material_no_flag_collide: ba.Material, 80 flagmaterial: ba.Material, 81 ): 82 self.base_pos = base_pos 83 self.base_region_material = base_region_material 84 self.base_region = base_region 85 self.spaz_material_no_flag_physical = spaz_material_no_flag_physical 86 self.spaz_material_no_flag_collide = spaz_material_no_flag_collide 87 self.flagmaterial = flagmaterial 88 self.score = 0 89 self.flag_return_touches = 0 90 self.home_flag_at_base = True 91 self.touch_return_timer: ba.Timer | None = None 92 self.enemy_flag_at_base = False 93 self.flag: CTFFlag | None = None 94 self.last_flag_leave_time: float | None = None 95 self.touch_return_timer_ticking: ba.NodeActor | None = None
Inherited Members
- ba._team.Team
- manual_init
- customdata
- on_expire
- sessionteam
99class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): 100 """Game of stealing other team's flag and returning it to your base.""" 101 102 name = 'Capture the Flag' 103 description = 'Return the enemy flag to score.' 104 available_settings = [ 105 ba.IntSetting('Score to Win', min_value=1, default=3), 106 ba.IntSetting( 107 'Flag Touch Return Time', 108 min_value=0, 109 default=0, 110 increment=1, 111 ), 112 ba.IntSetting( 113 'Flag Idle Return Time', 114 min_value=5, 115 default=30, 116 increment=5, 117 ), 118 ba.IntChoiceSetting( 119 'Time Limit', 120 choices=[ 121 ('None', 0), 122 ('1 Minute', 60), 123 ('2 Minutes', 120), 124 ('5 Minutes', 300), 125 ('10 Minutes', 600), 126 ('20 Minutes', 1200), 127 ], 128 default=0, 129 ), 130 ba.FloatChoiceSetting( 131 'Respawn Times', 132 choices=[ 133 ('Shorter', 0.25), 134 ('Short', 0.5), 135 ('Normal', 1.0), 136 ('Long', 2.0), 137 ('Longer', 4.0), 138 ], 139 default=1.0, 140 ), 141 ba.BoolSetting('Epic Mode', default=False), 142 ] 143 144 @classmethod 145 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 146 return issubclass(sessiontype, ba.DualTeamSession) 147 148 @classmethod 149 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 150 return ba.getmaps('team_flag') 151 152 def __init__(self, settings: dict): 153 super().__init__(settings) 154 self._scoreboard = Scoreboard() 155 self._alarmsound = ba.getsound('alarm') 156 self._ticking_sound = ba.getsound('ticking') 157 self._score_sound = ba.getsound('score') 158 self._swipsound = ba.getsound('swip') 159 self._last_score_time = 0 160 self._all_bases_material = ba.Material() 161 self._last_home_flag_notice_print_time = 0.0 162 self._score_to_win = int(settings['Score to Win']) 163 self._epic_mode = bool(settings['Epic Mode']) 164 self._time_limit = float(settings['Time Limit']) 165 166 self.flag_touch_return_time = float(settings['Flag Touch Return Time']) 167 self.flag_idle_return_time = float(settings['Flag Idle Return Time']) 168 169 # Base class overrides. 170 self.slow_motion = self._epic_mode 171 self.default_music = ( 172 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FLAG_CATCHER 173 ) 174 175 def get_instance_description(self) -> str | Sequence: 176 if self._score_to_win == 1: 177 return 'Steal the enemy flag.' 178 return 'Steal the enemy flag ${ARG1} times.', self._score_to_win 179 180 def get_instance_description_short(self) -> str | Sequence: 181 if self._score_to_win == 1: 182 return 'return 1 flag' 183 return 'return ${ARG1} flags', self._score_to_win 184 185 def create_team(self, sessionteam: ba.SessionTeam) -> Team: 186 187 # Create our team instance and its initial values. 188 189 base_pos = self.map.get_flag_position(sessionteam.id) 190 Flag.project_stand(base_pos) 191 192 ba.newnode( 193 'light', 194 attrs={ 195 'position': base_pos, 196 'intensity': 0.6, 197 'height_attenuated': False, 198 'volume_intensity_scale': 0.1, 199 'radius': 0.1, 200 'color': sessionteam.color, 201 }, 202 ) 203 204 base_region_mat = ba.Material() 205 pos = base_pos 206 base_region = ba.newnode( 207 'region', 208 attrs={ 209 'position': (pos[0], pos[1] + 0.75, pos[2]), 210 'scale': (0.5, 0.5, 0.5), 211 'type': 'sphere', 212 'materials': [base_region_mat, self._all_bases_material], 213 }, 214 ) 215 216 spaz_mat_no_flag_physical = ba.Material() 217 spaz_mat_no_flag_collide = ba.Material() 218 flagmat = ba.Material() 219 220 team = Team( 221 base_pos=base_pos, 222 base_region_material=base_region_mat, 223 base_region=base_region, 224 spaz_material_no_flag_physical=spaz_mat_no_flag_physical, 225 spaz_material_no_flag_collide=spaz_mat_no_flag_collide, 226 flagmaterial=flagmat, 227 ) 228 229 # Some parts of our spazzes don't collide physically with our 230 # flags but generate callbacks. 231 spaz_mat_no_flag_physical.add_actions( 232 conditions=('they_have_material', flagmat), 233 actions=( 234 ('modify_part_collision', 'physical', False), 235 ( 236 'call', 237 'at_connect', 238 lambda: self._handle_touching_own_flag(team, True), 239 ), 240 ( 241 'call', 242 'at_disconnect', 243 lambda: self._handle_touching_own_flag(team, False), 244 ), 245 ), 246 ) 247 248 # Other parts of our spazzes don't collide with our flags at all. 249 spaz_mat_no_flag_collide.add_actions( 250 conditions=('they_have_material', flagmat), 251 actions=('modify_part_collision', 'collide', False), 252 ) 253 254 # We wanna know when *any* flag enters/leaves our base. 255 base_region_mat.add_actions( 256 conditions=('they_have_material', FlagFactory.get().flagmaterial), 257 actions=( 258 ('modify_part_collision', 'collide', True), 259 ('modify_part_collision', 'physical', False), 260 ( 261 'call', 262 'at_connect', 263 lambda: self._handle_flag_entered_base(team), 264 ), 265 ( 266 'call', 267 'at_disconnect', 268 lambda: self._handle_flag_left_base(team), 269 ), 270 ), 271 ) 272 273 return team 274 275 def on_team_join(self, team: Team) -> None: 276 # Can't do this in create_team because the team's color/etc. have 277 # not been wired up yet at that point. 278 self._spawn_flag_for_team(team) 279 self._update_scoreboard() 280 281 def on_begin(self) -> None: 282 super().on_begin() 283 self.setup_standard_time_limit(self._time_limit) 284 self.setup_standard_powerup_drops() 285 ba.timer(1.0, call=self._tick, repeat=True) 286 287 def _spawn_flag_for_team(self, team: Team) -> None: 288 team.flag = CTFFlag(team) 289 team.flag_return_touches = 0 290 self._flash_base(team, length=1.0) 291 assert team.flag.node 292 ba.playsound(self._swipsound, position=team.flag.node.position) 293 294 def _handle_flag_entered_base(self, team: Team) -> None: 295 try: 296 flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True) 297 except ba.NotFoundError: 298 # Don't think this should logically ever happen. 299 print('Error getting CTFFlag in entering-base callback.') 300 return 301 302 if flag.team is team: 303 team.home_flag_at_base = True 304 305 # If the enemy flag is already here, score! 306 if team.enemy_flag_at_base: 307 # And show team name which scored (but actually we could 308 # show here player who returned enemy flag). 309 self.show_zoom_message( 310 ba.Lstr( 311 resource='nameScoresText', subs=[('${NAME}', team.name)] 312 ), 313 color=team.color, 314 ) 315 self._score(team) 316 else: 317 team.enemy_flag_at_base = True 318 if team.home_flag_at_base: 319 # Award points to whoever was carrying the enemy flag. 320 player = flag.last_player_to_hold 321 if player and player.team is team: 322 assert self.stats 323 self.stats.player_scored(player, 50, big_message=True) 324 325 # Update score and reset flags. 326 self._score(team) 327 328 # If the home-team flag isn't here, print a message to that effect. 329 else: 330 # Don't want slo-mo affecting this 331 curtime = ba.time(ba.TimeType.BASE) 332 if curtime - self._last_home_flag_notice_print_time > 5.0: 333 self._last_home_flag_notice_print_time = curtime 334 bpos = team.base_pos 335 tval = ba.Lstr(resource='ownFlagAtYourBaseWarning') 336 tnode = ba.newnode( 337 'text', 338 attrs={ 339 'text': tval, 340 'in_world': True, 341 'scale': 0.013, 342 'color': (1, 1, 0, 1), 343 'h_align': 'center', 344 'position': (bpos[0], bpos[1] + 3.2, bpos[2]), 345 }, 346 ) 347 ba.timer(5.1, tnode.delete) 348 ba.animate( 349 tnode, 'scale', {0.0: 0, 0.2: 0.013, 4.8: 0.013, 5.0: 0} 350 ) 351 352 def _tick(self) -> None: 353 # If either flag is away from base and not being held, tick down its 354 # respawn timer. 355 for team in self.teams: 356 flag = team.flag 357 assert flag is not None 358 359 if not team.home_flag_at_base and flag.held_count == 0: 360 time_out_counting_down = True 361 if flag.time_out_respawn_time is None: 362 flag.reset_return_times() 363 assert flag.time_out_respawn_time is not None 364 flag.time_out_respawn_time -= 1 365 if flag.time_out_respawn_time <= 0: 366 flag.handlemessage(ba.DieMessage()) 367 else: 368 time_out_counting_down = False 369 370 if flag.node and flag.counter: 371 pos = flag.node.position 372 flag.counter.position = (pos[0], pos[1] + 1.3, pos[2]) 373 374 # If there's no self-touches on this flag, set its text 375 # to show its auto-return counter. (if there's self-touches 376 # its showing that time). 377 if team.flag_return_touches == 0: 378 flag.counter.text = ( 379 str(flag.time_out_respawn_time) 380 if ( 381 time_out_counting_down 382 and flag.time_out_respawn_time is not None 383 and flag.time_out_respawn_time <= 10 384 ) 385 else '' 386 ) 387 flag.counter.color = (1, 1, 1, 0.5) 388 flag.counter.scale = 0.014 389 390 def _score(self, team: Team) -> None: 391 team.score += 1 392 ba.playsound(self._score_sound) 393 self._flash_base(team) 394 self._update_scoreboard() 395 396 # Have teammates celebrate. 397 for player in team.players: 398 if player.actor: 399 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 400 401 # Reset all flags/state. 402 for reset_team in self.teams: 403 if not reset_team.home_flag_at_base: 404 assert reset_team.flag is not None 405 reset_team.flag.handlemessage(ba.DieMessage()) 406 reset_team.enemy_flag_at_base = False 407 if team.score >= self._score_to_win: 408 self.end_game() 409 410 def end_game(self) -> None: 411 results = ba.GameResults() 412 for team in self.teams: 413 results.set_team_score(team, team.score) 414 self.end(results=results, announce_delay=0.8) 415 416 def _handle_flag_left_base(self, team: Team) -> None: 417 cur_time = ba.time() 418 try: 419 flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True) 420 except ba.NotFoundError: 421 # This can happen if the flag stops touching us due to being 422 # deleted; that's ok. 423 return 424 425 if flag.team is team: 426 427 # Check times here to prevent too much flashing. 428 if ( 429 team.last_flag_leave_time is None 430 or cur_time - team.last_flag_leave_time > 3.0 431 ): 432 ba.playsound(self._alarmsound, position=team.base_pos) 433 self._flash_base(team) 434 team.last_flag_leave_time = cur_time 435 team.home_flag_at_base = False 436 else: 437 team.enemy_flag_at_base = False 438 439 def _touch_return_update(self, team: Team) -> None: 440 # Count down only while its away from base and not being held. 441 assert team.flag is not None 442 if team.home_flag_at_base or team.flag.held_count > 0: 443 team.touch_return_timer_ticking = None 444 return # No need to return when its at home. 445 if team.touch_return_timer_ticking is None: 446 team.touch_return_timer_ticking = ba.NodeActor( 447 ba.newnode( 448 'sound', 449 attrs={ 450 'sound': self._ticking_sound, 451 'positional': False, 452 'loop': True, 453 }, 454 ) 455 ) 456 flag = team.flag 457 if flag.touch_return_time is not None: 458 flag.touch_return_time -= 0.1 459 if flag.counter: 460 flag.counter.text = f'{flag.touch_return_time:.1f}' 461 flag.counter.color = (1, 1, 0, 1) 462 flag.counter.scale = 0.02 463 464 if flag.touch_return_time <= 0.0: 465 self._award_players_touching_own_flag(team) 466 flag.handlemessage(ba.DieMessage()) 467 468 def _award_players_touching_own_flag(self, team: Team) -> None: 469 for player in team.players: 470 if player.touching_own_flag > 0: 471 return_score = 10 + 5 * int(self.flag_touch_return_time) 472 self.stats.player_scored( 473 player, return_score, screenmessage=False 474 ) 475 476 def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None: 477 """Called when a player touches or stops touching their own team flag. 478 479 We keep track of when each player is touching their own flag so we 480 can award points when returned. 481 """ 482 player: Player | None 483 try: 484 spaz = ba.getcollision().sourcenode.getdelegate(PlayerSpaz, True) 485 except ba.NotFoundError: 486 return 487 488 if not spaz.is_alive(): 489 return 490 491 player = spaz.getplayer(Player, True) 492 493 if player: 494 player.touching_own_flag += 1 if connecting else -1 495 496 # If return-time is zero, just kill it immediately.. otherwise keep 497 # track of touches and count down. 498 if float(self.flag_touch_return_time) <= 0.0: 499 assert team.flag is not None 500 if ( 501 connecting 502 and not team.home_flag_at_base 503 and team.flag.held_count == 0 504 ): 505 self._award_players_touching_own_flag(team) 506 ba.getcollision().opposingnode.handlemessage(ba.DieMessage()) 507 508 # Takes a non-zero amount of time to return. 509 else: 510 if connecting: 511 team.flag_return_touches += 1 512 if team.flag_return_touches == 1: 513 team.touch_return_timer = ba.Timer( 514 0.1, 515 call=ba.Call(self._touch_return_update, team), 516 repeat=True, 517 ) 518 team.touch_return_timer_ticking = None 519 else: 520 team.flag_return_touches -= 1 521 if team.flag_return_touches == 0: 522 team.touch_return_timer = None 523 team.touch_return_timer_ticking = None 524 if team.flag_return_touches < 0: 525 ba.print_error('CTF flag_return_touches < 0') 526 527 def _flash_base(self, team: Team, length: float = 2.0) -> None: 528 light = ba.newnode( 529 'light', 530 attrs={ 531 'position': team.base_pos, 532 'height_attenuated': False, 533 'radius': 0.3, 534 'color': team.color, 535 }, 536 ) 537 ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True) 538 ba.timer(length, light.delete) 539 540 def spawn_player_spaz( 541 self, 542 player: Player, 543 position: Sequence[float] | None = None, 544 angle: float | None = None, 545 ) -> PlayerSpaz: 546 """Intercept new spazzes and add our team material for them.""" 547 spaz = super().spawn_player_spaz(player, position, angle) 548 player = spaz.getplayer(Player, True) 549 team: Team = player.team 550 player.touching_own_flag = 0 551 no_physical_mats: list[ba.Material] = [ 552 team.spaz_material_no_flag_physical 553 ] 554 no_collide_mats: list[ba.Material] = [ 555 team.spaz_material_no_flag_collide 556 ] 557 558 # Our normal parts should still collide; just not physically 559 # (so we can calc restores). 560 assert spaz.node 561 spaz.node.materials = list(spaz.node.materials) + no_physical_mats 562 spaz.node.roller_materials = ( 563 list(spaz.node.roller_materials) + no_physical_mats 564 ) 565 566 # Pickups and punches shouldn't hit at all though. 567 spaz.node.punch_materials = ( 568 list(spaz.node.punch_materials) + no_collide_mats 569 ) 570 spaz.node.pickup_materials = ( 571 list(spaz.node.pickup_materials) + no_collide_mats 572 ) 573 spaz.node.extras_material = ( 574 list(spaz.node.extras_material) + no_collide_mats 575 ) 576 return spaz 577 578 def _update_scoreboard(self) -> None: 579 for team in self.teams: 580 self._scoreboard.set_team_value( 581 team, team.score, self._score_to_win 582 ) 583 584 def handlemessage(self, msg: Any) -> Any: 585 586 if isinstance(msg, ba.PlayerDiedMessage): 587 super().handlemessage(msg) # Augment standard behavior. 588 self.respawn_player(msg.getplayer(Player)) 589 590 elif isinstance(msg, FlagDiedMessage): 591 assert isinstance(msg.flag, CTFFlag) 592 ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team)) 593 594 elif isinstance(msg, FlagPickedUpMessage): 595 596 # Store the last player to hold the flag for scoring purposes. 597 assert isinstance(msg.flag, CTFFlag) 598 try: 599 msg.flag.last_player_to_hold = msg.node.getdelegate( 600 PlayerSpaz, True 601 ).getplayer(Player, True) 602 except ba.NotFoundError: 603 pass 604 605 msg.flag.held_count += 1 606 msg.flag.reset_return_times() 607 608 elif isinstance(msg, FlagDroppedMessage): 609 # Store the last player to hold the flag for scoring purposes. 610 assert isinstance(msg.flag, CTFFlag) 611 msg.flag.held_count -= 1 612 613 else: 614 super().handlemessage(msg)
Game of stealing other team's flag and returning it to your base.
152 def __init__(self, settings: dict): 153 super().__init__(settings) 154 self._scoreboard = Scoreboard() 155 self._alarmsound = ba.getsound('alarm') 156 self._ticking_sound = ba.getsound('ticking') 157 self._score_sound = ba.getsound('score') 158 self._swipsound = ba.getsound('swip') 159 self._last_score_time = 0 160 self._all_bases_material = ba.Material() 161 self._last_home_flag_notice_print_time = 0.0 162 self._score_to_win = int(settings['Score to Win']) 163 self._epic_mode = bool(settings['Epic Mode']) 164 self._time_limit = float(settings['Time Limit']) 165 166 self.flag_touch_return_time = float(settings['Flag Touch Return Time']) 167 self.flag_idle_return_time = float(settings['Flag Idle Return Time']) 168 169 # Base class overrides. 170 self.slow_motion = self._epic_mode 171 self.default_music = ( 172 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FLAG_CATCHER 173 )
Instantiate the Activity.
144 @classmethod 145 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 146 return issubclass(sessiontype, ba.DualTeamSession)
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
148 @classmethod 149 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 150 return ba.getmaps('team_flag')
Called by the default ba.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given ba.Session type.
175 def get_instance_description(self) -> str | Sequence: 176 if self._score_to_win == 1: 177 return 'Steal the enemy flag.' 178 return 'Steal the enemy flag ${ARG1} times.', self._score_to_win
Return a description for this game instance, in English.
This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().
Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:
This will give us something like 'Score 3 goals.' in English
and can properly translate to 'Anota 3 goles.' in Spanish.
If we just returned the string 'Score 3 Goals' here, there would
have to be a translation entry for each specific number. ew.
return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]
This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.
180 def get_instance_description_short(self) -> str | Sequence: 181 if self._score_to_win == 1: 182 return 'return 1 flag' 183 return 'return ${ARG1} flags', self._score_to_win
Return a short description for this game instance in English.
This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.
Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:
This will give us something like 'score 3 goals' in English
and can properly translate to 'anota 3 goles' in Spanish.
If we just returned the string 'score 3 goals' here, there would
have to be a translation entry for each specific number. ew.
return ['score ${ARG1} goals', self.settings_raw['Score to Win']]
This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.
185 def create_team(self, sessionteam: ba.SessionTeam) -> Team: 186 187 # Create our team instance and its initial values. 188 189 base_pos = self.map.get_flag_position(sessionteam.id) 190 Flag.project_stand(base_pos) 191 192 ba.newnode( 193 'light', 194 attrs={ 195 'position': base_pos, 196 'intensity': 0.6, 197 'height_attenuated': False, 198 'volume_intensity_scale': 0.1, 199 'radius': 0.1, 200 'color': sessionteam.color, 201 }, 202 ) 203 204 base_region_mat = ba.Material() 205 pos = base_pos 206 base_region = ba.newnode( 207 'region', 208 attrs={ 209 'position': (pos[0], pos[1] + 0.75, pos[2]), 210 'scale': (0.5, 0.5, 0.5), 211 'type': 'sphere', 212 'materials': [base_region_mat, self._all_bases_material], 213 }, 214 ) 215 216 spaz_mat_no_flag_physical = ba.Material() 217 spaz_mat_no_flag_collide = ba.Material() 218 flagmat = ba.Material() 219 220 team = Team( 221 base_pos=base_pos, 222 base_region_material=base_region_mat, 223 base_region=base_region, 224 spaz_material_no_flag_physical=spaz_mat_no_flag_physical, 225 spaz_material_no_flag_collide=spaz_mat_no_flag_collide, 226 flagmaterial=flagmat, 227 ) 228 229 # Some parts of our spazzes don't collide physically with our 230 # flags but generate callbacks. 231 spaz_mat_no_flag_physical.add_actions( 232 conditions=('they_have_material', flagmat), 233 actions=( 234 ('modify_part_collision', 'physical', False), 235 ( 236 'call', 237 'at_connect', 238 lambda: self._handle_touching_own_flag(team, True), 239 ), 240 ( 241 'call', 242 'at_disconnect', 243 lambda: self._handle_touching_own_flag(team, False), 244 ), 245 ), 246 ) 247 248 # Other parts of our spazzes don't collide with our flags at all. 249 spaz_mat_no_flag_collide.add_actions( 250 conditions=('they_have_material', flagmat), 251 actions=('modify_part_collision', 'collide', False), 252 ) 253 254 # We wanna know when *any* flag enters/leaves our base. 255 base_region_mat.add_actions( 256 conditions=('they_have_material', FlagFactory.get().flagmaterial), 257 actions=( 258 ('modify_part_collision', 'collide', True), 259 ('modify_part_collision', 'physical', False), 260 ( 261 'call', 262 'at_connect', 263 lambda: self._handle_flag_entered_base(team), 264 ), 265 ( 266 'call', 267 'at_disconnect', 268 lambda: self._handle_flag_left_base(team), 269 ), 270 ), 271 ) 272 273 return team
Create the Team instance for this Activity.
Subclasses can override this if the activity's team class requires a custom constructor; otherwise it will be called with no args. Note that the team object should not be used at this point as it is not yet fully wired up; wait for on_team_join() for that.
275 def on_team_join(self, team: Team) -> None: 276 # Can't do this in create_team because the team's color/etc. have 277 # not been wired up yet at that point. 278 self._spawn_flag_for_team(team) 279 self._update_scoreboard()
Called when a new ba.Team joins the Activity.
(including the initial set of Teams)
281 def on_begin(self) -> None: 282 super().on_begin() 283 self.setup_standard_time_limit(self._time_limit) 284 self.setup_standard_powerup_drops() 285 ba.timer(1.0, call=self._tick, repeat=True)
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.
410 def end_game(self) -> None: 411 results = ba.GameResults() 412 for team in self.teams: 413 results.set_team_score(team, team.score) 414 self.end(results=results, announce_delay=0.8)
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.
540 def spawn_player_spaz( 541 self, 542 player: Player, 543 position: Sequence[float] | None = None, 544 angle: float | None = None, 545 ) -> PlayerSpaz: 546 """Intercept new spazzes and add our team material for them.""" 547 spaz = super().spawn_player_spaz(player, position, angle) 548 player = spaz.getplayer(Player, True) 549 team: Team = player.team 550 player.touching_own_flag = 0 551 no_physical_mats: list[ba.Material] = [ 552 team.spaz_material_no_flag_physical 553 ] 554 no_collide_mats: list[ba.Material] = [ 555 team.spaz_material_no_flag_collide 556 ] 557 558 # Our normal parts should still collide; just not physically 559 # (so we can calc restores). 560 assert spaz.node 561 spaz.node.materials = list(spaz.node.materials) + no_physical_mats 562 spaz.node.roller_materials = ( 563 list(spaz.node.roller_materials) + no_physical_mats 564 ) 565 566 # Pickups and punches shouldn't hit at all though. 567 spaz.node.punch_materials = ( 568 list(spaz.node.punch_materials) + no_collide_mats 569 ) 570 spaz.node.pickup_materials = ( 571 list(spaz.node.pickup_materials) + no_collide_mats 572 ) 573 spaz.node.extras_material = ( 574 list(spaz.node.extras_material) + no_collide_mats 575 ) 576 return spaz
Intercept new spazzes and add our team material for them.
584 def handlemessage(self, msg: Any) -> Any: 585 586 if isinstance(msg, ba.PlayerDiedMessage): 587 super().handlemessage(msg) # Augment standard behavior. 588 self.respawn_player(msg.getplayer(Player)) 589 590 elif isinstance(msg, FlagDiedMessage): 591 assert isinstance(msg.flag, CTFFlag) 592 ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team)) 593 594 elif isinstance(msg, FlagPickedUpMessage): 595 596 # Store the last player to hold the flag for scoring purposes. 597 assert isinstance(msg.flag, CTFFlag) 598 try: 599 msg.flag.last_player_to_hold = msg.node.getdelegate( 600 PlayerSpaz, True 601 ).getplayer(Player, True) 602 except ba.NotFoundError: 603 pass 604 605 msg.flag.held_count += 1 606 msg.flag.reset_return_times() 607 608 elif isinstance(msg, FlagDroppedMessage): 609 # Store the last player to hold the flag for scoring purposes. 610 assert isinstance(msg.flag, CTFFlag) 611 msg.flag.held_count -= 1 612 613 else: 614 super().handlemessage(msg)
General message handling; can be passed any message object.
Inherited Members
- ba._teamgame.TeamGameActivity
- on_transition_in
- end
- ba._gameactivity.GameActivity
- allow_pausing
- allow_kick_idle_players
- create_settings_ui
- getscoreconfig
- getname
- get_display_string
- get_team_display_string
- get_description
- get_description_display_string
- get_available_settings
- get_settings_display_string
- map
- get_instance_display_string
- get_instance_scoreboard_display_string
- on_continue
- is_waiting_for_continue
- continue_or_end_game
- on_player_join
- respawn_player
- spawn_player_if_exists
- spawn_player
- setup_standard_powerup_drops
- setup_standard_time_limit
- show_zoom_message
- ba._activity.Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- use_fixed_vr_overlay
- 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
- session
- on_player_leave
- on_team_leave
- on_transition_out
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- create_player
- ba._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps