# Released under the MIT License. See LICENSE for details. # """Defines the King of the Hill game.""" # ba_meta require api 7 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations import weakref from enum import Enum from typing import TYPE_CHECKING import ba from bastd.actor.flag import Flag from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.scoreboard import Scoreboard from bastd.gameutils import SharedObjects if TYPE_CHECKING: from typing import Any, Sequence class FlagState(Enum): """States our single flag can be in.""" NEW = 0 UNCONTESTED = 1 CONTESTED = 2 HELD = 3 class Player(ba.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: self.time_at_flag = 0 class Team(ba.Team[Player]): """Our team type for this game.""" def __init__(self, time_remaining: int) -> None: self.time_remaining = time_remaining # ba_meta export game class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): """Game where a team wins by holding a 'hill' for a set amount of time.""" name = 'King of the Hill' description = 'Secure the flag for a set length of time.' available_settings = [ ba.IntSetting( 'Hold Time', min_value=10, default=30, increment=10, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ba.BoolSetting('Epic Mode', default=True), ] scoreconfig = ba.ScoreConfig(label='Time Held') @classmethod def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: return issubclass(sessiontype, ba.MultiTeamSession) @classmethod def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: return ba.getmaps('king_of_the_hill') def __init__(self, settings: dict): super().__init__(settings) shared = SharedObjects.get() self._scoreboard = Scoreboard() self._swipsound = ba.getsound('swip') self._tick_sound = ba.getsound('tick') self._countdownsounds = { 10: ba.getsound('announceTen'), 9: ba.getsound('announceNine'), 8: ba.getsound('announceEight'), 7: ba.getsound('announceSeven'), 6: ba.getsound('announceSix'), 5: ba.getsound('announceFive'), 4: ba.getsound('announceFour'), 3: ba.getsound('announceThree'), 2: ba.getsound('announceTwo'), 1: ba.getsound('announceOne'), } self._flag_pos: Sequence[float] | None = None self._flag_state: FlagState | None = None self._flag: Flag | None = None self._flag_light: ba.Node | None = None self._scoring_team: weakref.ref[Team] | None = None self._hold_time = int(settings['Hold Time']) self._time_limit = float(settings['Time Limit']) self._epic_mode = bool(settings['Epic Mode']) self._flag_region_material = ba.Material() self._flag_region_material.add_actions( conditions=('they_have_material', shared.player_material), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ( 'call', 'at_connect', ba.Call(self._handle_player_flag_region_collide, True), ), ( 'call', 'at_disconnect', ba.Call(self._handle_player_flag_region_collide, False), ), ), ) # Base class overrides. self.slow_motion = self._epic_mode self.default_music = ( ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SCARY ) def get_instance_description(self) -> str | Sequence: return 'Secure the flag for ${ARG1} seconds.', self._hold_time def get_instance_description_short(self) -> str | Sequence: return 'secure the flag for ${ARG1} seconds', self._hold_time def create_team(self, sessionteam: ba.SessionTeam) -> Team: return Team(time_remaining=self._hold_time) def on_begin(self) -> None: super().on_begin() shared = SharedObjects.get() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._flag_pos = self.map.get_flag_position(None) ba.timer(1.0, self._tick, repeat=True) self._flag_state = FlagState.NEW Flag.project_stand(self._flag_pos) self._flag = Flag( position=self._flag_pos, touchable=False, color=(1, 1, 1) ) self._flag_light = ba.newnode( 'light', attrs={ 'position': self._flag_pos, 'intensity': 0.2, 'height_attenuated': False, 'radius': 0.4, 'color': (0.2, 0.2, 0.2), }, ) # Flag region. flagmats = [self._flag_region_material, shared.region_material] ba.newnode( 'region', attrs={ 'position': self._flag_pos, 'scale': (1.8, 1.8, 1.8), 'type': 'sphere', 'materials': flagmats, }, ) self._update_flag_state() def _tick(self) -> None: self._update_flag_state() # Give holding players points. for player in self.players: if player.time_at_flag > 0: self.stats.player_scored( player, 3, screenmessage=False, display=False ) if self._scoring_team is None: scoring_team = None else: scoring_team = self._scoring_team() if scoring_team: if scoring_team.time_remaining > 0: ba.playsound(self._tick_sound) scoring_team.time_remaining = max( 0, scoring_team.time_remaining - 1 ) self._update_scoreboard() if scoring_team.time_remaining > 0: assert self._flag is not None self._flag.set_score_text(str(scoring_team.time_remaining)) # Announce numbers we have sounds for. numsound = self._countdownsounds.get(scoring_team.time_remaining) if numsound is not None: ba.playsound(numsound) # winner if scoring_team.time_remaining <= 0: self.end_game() def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, self._hold_time - team.time_remaining) self.end(results=results, announce_delay=0) def _update_flag_state(self) -> None: holding_teams = set( player.team for player in self.players if player.time_at_flag ) prev_state = self._flag_state assert self._flag_light assert self._flag is not None assert self._flag.node if len(holding_teams) > 1: self._flag_state = FlagState.CONTESTED self._scoring_team = None self._flag_light.color = (0.6, 0.6, 0.1) self._flag.node.color = (1.0, 1.0, 0.4) elif len(holding_teams) == 1: holding_team = list(holding_teams)[0] self._flag_state = FlagState.HELD self._scoring_team = weakref.ref(holding_team) self._flag_light.color = ba.normalized_color(holding_team.color) self._flag.node.color = holding_team.color else: self._flag_state = FlagState.UNCONTESTED self._scoring_team = None self._flag_light.color = (0.2, 0.2, 0.2) self._flag.node.color = (1, 1, 1) if self._flag_state != prev_state: ba.playsound(self._swipsound) def _handle_player_flag_region_collide(self, colliding: bool) -> None: try: spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) except ba.NotFoundError: return if not spaz.is_alive(): return player = spaz.getplayer(Player, True) # Different parts of us can collide so a single value isn't enough # also don't count it if we're dead (flying heads shouldn't be able to # win the game :-) if colliding and player.is_alive(): player.time_at_flag += 1 else: player.time_at_flag = max(0, player.time_at_flag - 1) self._update_flag_state() def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value( team, team.time_remaining, self._hold_time, countdown=True ) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment default. # No longer can count as time_at_flag once dead. player = msg.getplayer(Player) player.time_at_flag = 0 self._update_flag_state() self.respawn_player(player)