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