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