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