Source code for bascenev1lib.game.conquest

# Released under the MIT License. See LICENSE for details.
#
"""Provides the Conquest game."""

# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)

from __future__ import annotations

import random
from typing import TYPE_CHECKING, override

import bascenev1 as bs

from bascenev1lib.actor.flag import Flag
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.gameutils import SharedObjects
from bascenev1lib.actor.respawnicon import RespawnIcon

if TYPE_CHECKING:
    from typing import Any, Sequence


[docs] class ConquestFlag(Flag): """A custom flag for use with Conquest games.""" def __init__(self, *args: Any, **keywds: Any): super().__init__(*args, **keywds) self._team: Team | None = None self.light: bs.Node | None = None @property def team(self) -> Team | None: """The team that owns this flag.""" return self._team @team.setter def team(self, team: Team) -> None: """Set the team that owns this flag.""" self._team = team
[docs] class Player(bs.Player['Team']): """Our player type for this game.""" # FIXME: We shouldn't be using customdata here # (but need to update respawn funcs accordingly first). @property def respawn_timer(self) -> bs.Timer | None: """Type safe access to standard respawn timer.""" val = self.customdata.get('respawn_timer', None) assert isinstance(val, (bs.Timer, type(None))) return val @respawn_timer.setter def respawn_timer(self, value: bs.Timer | None) -> None: self.customdata['respawn_timer'] = value @property def respawn_icon(self) -> RespawnIcon | None: """Type safe access to standard respawn icon.""" val = self.customdata.get('respawn_icon', None) assert isinstance(val, (RespawnIcon, type(None))) return val @respawn_icon.setter def respawn_icon(self, value: RespawnIcon | None) -> None: self.customdata['respawn_icon'] = value
[docs] class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: self.flags_held = 0
# ba_meta export bascenev1.GameActivity
[docs] class ConquestGame(bs.TeamGameActivity[Player, Team]): """A game where teams try to claim all flags on the map.""" name = 'Conquest' description = 'Secure all flags on the map to win.' available_settings = [ bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), bs.BoolSetting('Epic Mode', default=False), ]
[docs] @override @classmethod def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: return issubclass(sessiontype, bs.DualTeamSession)
[docs] @override @classmethod def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: assert bs.app.classic is not None return bs.app.classic.getmaps('conquest')
def __init__(self, settings: dict): super().__init__(settings) shared = SharedObjects.get() self._scoreboard = Scoreboard() self._score_sound = bs.getsound('score') self._swipsound = bs.getsound('swip') self._extraflagmat = bs.Material() self._flags: list[ConquestFlag] = [] self._epic_mode = bool(settings['Epic Mode']) self._time_limit = float(settings['Time Limit']) # Base class overrides. self.slow_motion = self._epic_mode self.default_music = ( bs.MusicType.EPIC if self._epic_mode else bs.MusicType.GRAND_ROMP ) # We want flags to tell us they've been hit but not react physically. self._extraflagmat.add_actions( conditions=('they_have_material', shared.player_material), actions=( ('modify_part_collision', 'collide', True), ('call', 'at_connect', self._handle_flag_player_collide), ), )
[docs] @override def get_instance_description(self) -> str | Sequence: return 'Secure all ${ARG1} flags.', len(self.map.flag_points)
[docs] @override def get_instance_description_short(self) -> str | Sequence: return 'secure all ${ARG1} flags', len(self.map.flag_points)
[docs] @override def on_team_join(self, team: Team) -> None: if self.has_begun(): self._update_scores()
[docs] @override def on_player_join(self, player: Player) -> None: player.respawn_timer = None # Only spawn if this player's team has a flag currently. if player.team.flags_held > 0: self.spawn_player(player)
[docs] @override def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() # Set up flags with marker lights. for i, flag_point in enumerate(self.map.flag_points): point = flag_point flag = ConquestFlag( position=point, touchable=False, materials=[self._extraflagmat] ) self._flags.append(flag) Flag.project_stand(point) flag.light = bs.newnode( 'light', owner=flag.node, attrs={ 'position': point, 'intensity': 0.25, 'height_attenuated': False, 'radius': 0.3, 'color': (1, 1, 1), }, ) # Give teams a flag to start with. for i, team in enumerate(self.teams): self._flags[i].team = team light = self._flags[i].light assert light node = self._flags[i].node assert node light.color = team.color node.color = team.color self._update_scores() # Initial joiners didn't spawn due to no flags being owned yet; # spawn them now. for player in self.players: self.spawn_player(player)
def _update_scores(self) -> None: for team in self.teams: team.flags_held = 0 for flag in self._flags: if flag.team is not None: flag.team.flags_held += 1 for team in self.teams: # If a team finds themselves with no flags, cancel all # outstanding spawn-timers. if team.flags_held == 0: for player in team.players: player.respawn_timer = None player.respawn_icon = None if team.flags_held == len(self._flags): self.end_game() self._scoreboard.set_team_value( team, team.flags_held, len(self._flags) )
[docs] @override def end_game(self) -> None: results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.flags_held) self.end(results=results)
def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None: assert flag.node assert flag.light light = bs.newnode( 'light', attrs={ 'position': flag.node.position, 'height_attenuated': False, 'color': flag.light.color, }, ) bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True) bs.timer(length, light.delete) def _handle_flag_player_collide(self) -> None: collision = bs.getcollision() try: flag = collision.sourcenode.getdelegate(ConquestFlag, True) player = collision.opposingnode.getdelegate( PlayerSpaz, True ).getplayer(Player, True) except bs.NotFoundError: return assert flag.light if flag.team is not player.team: flag.team = player.team flag.light.color = player.team.color flag.node.color = player.team.color self.stats.player_scored(player, 10, screenmessage=False) self._swipsound.play() self._flash_flag(flag) self._update_scores() # Respawn any players on this team that were in limbo due to the # lack of a flag for their team. for otherplayer in self.players: if ( otherplayer.team is flag.team and otherplayer.actor is not None and not otherplayer.is_alive() and otherplayer.respawn_timer is None ): self.spawn_player(otherplayer)
[docs] @override def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) # Respawn only if this team has a flag. player = msg.getplayer(Player) if player.team.flags_held > 0: self.respawn_player(player) else: player.respawn_timer = None else: super().handlemessage(msg)
[docs] @override def spawn_player(self, player: Player) -> bs.Actor: # We spawn players at different places based on what flags are held. return self.spawn_player_spaz( player, self._get_player_spawn_position(player) )
def _get_player_spawn_position(self, player: Player) -> Sequence[float]: # Iterate until we find a spawn owned by this team. spawn_count = len(self.map.spawn_by_flag_points) # Get all spawns owned by this team. spawns = [ i for i in range(spawn_count) if self._flags[i].team is player.team ] closest_spawn = 0 closest_distance = 9999.0 # Now find the spawn that's closest to a spawn not owned by us; # we'll use that one. for spawn in spawns: spt = self.map.spawn_by_flag_points[spawn] our_pt = bs.Vec3(spt[0], spt[1], spt[2]) for otherspawn in [ i for i in range(spawn_count) if self._flags[i].team is not player.team ]: spt = self.map.spawn_by_flag_points[otherspawn] their_pt = bs.Vec3(spt[0], spt[1], spt[2]) dist = (their_pt - our_pt).length() if dist < closest_distance: closest_distance = dist closest_spawn = spawn pos = self.map.spawn_by_flag_points[closest_spawn] x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3]) z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5]) pos = ( pos[0] + random.uniform(*x_range), pos[1], pos[2] + random.uniform(*z_range), ) return pos