# Copyright (c) 2011-2019 Eric Froemling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # ----------------------------------------------------------------------------- """Functionality related to coop-mode sessions.""" from __future__ import annotations from typing import TYPE_CHECKING import _ba from ba._session import Session if TYPE_CHECKING: from typing import Any, List, Dict, Optional, Callable, Sequence import ba TEAM_COLORS = ((0.2, 0.4, 1.6), ) TEAM_NAMES = ("Good Guys", ) class CoopSession(Session): """A ba.Session which runs cooperative-mode games. Category: Gameplay Classes These generally consist of 1-4 players against the computer and include functionality such as high score lists. """ def __init__(self) -> None: """Instantiate a co-op mode session.""" # pylint: disable=cyclic-import from ba._campaign import get_campaign from bastd.activity.coopjoinscreen import CoopJoiningActivity _ba.increment_analytics_count('Co-op session start') app = _ba.app # If they passed in explicit min/max, honor that. # Otherwise defer to user overrides or defaults. if 'min_players' in app.coop_session_args: min_players = app.coop_session_args['min_players'] else: min_players = 1 if 'max_players' in app.coop_session_args: max_players = app.coop_session_args['max_players'] else: try: max_players = app.config['Coop Game Max Players'] except Exception: # Old pref value. try: max_players = app.config['Challenge Game Max Players'] except Exception: max_players = 4 print('FIXME: COOP SESSION WOULD CALC DEPS.') depsets: Sequence[ba.DependencySet] = [] super().__init__(depsets, team_names=TEAM_NAMES, team_colors=TEAM_COLORS, use_team_colors=False, min_players=min_players, max_players=max_players, allow_mid_activity_joins=False) # Tournament-ID if we correspond to a co-op tournament (otherwise None) self.tournament_id = (app.coop_session_args['tournament_id'] if 'tournament_id' in app.coop_session_args else None) # FIXME: Could be nice to pass this in as actual args. self.campaign_state = { 'campaign': (app.coop_session_args['campaign']), 'level': app.coop_session_args['level'] } self.campaign = get_campaign(self.campaign_state['campaign']) self._ran_tutorial_activity = False self._tutorial_activity: Optional[ba.Activity] = None self._custom_menu_ui: List[Dict[str, Any]] = [] # Start our joining screen. self.set_activity(_ba.new_activity(CoopJoiningActivity)) self._next_game_instance: Optional[ba.GameActivity] = None self._next_game_name: Optional[str] = None self._update_on_deck_game_instances() def get_current_game_instance(self) -> ba.GameActivity: """Get the game instance currently being played.""" return self._current_game_instance def _update_on_deck_game_instances(self) -> None: # pylint: disable=cyclic-import from ba._gameactivity import GameActivity # Instantiates levels we might be running soon # so they have time to load. # Build an instance for the current level. assert self.campaign is not None level = self.campaign.get_level(self.campaign_state['level']) gametype = level.gametype settings = level.get_settings() # Make sure all settings the game expects are present. neededsettings = gametype.get_settings(type(self)) for settingname, setting in neededsettings: if settingname not in settings: settings[settingname] = setting['default'] newactivity = _ba.new_activity(gametype, settings) assert isinstance(newactivity, GameActivity) self._current_game_instance: GameActivity = newactivity # Find the next level and build an instance for it too. levels = self.campaign.get_levels() level = self.campaign.get_level(self.campaign_state['level']) nextlevel: Optional[ba.Level] if level.index < len(levels) - 1: nextlevel = levels[level.index + 1] else: nextlevel = None if nextlevel: gametype = nextlevel.gametype settings = nextlevel.get_settings() # Make sure all settings the game expects are present. neededsettings = gametype.get_settings(type(self)) for settingname, setting in neededsettings: if settingname not in settings: settings[settingname] = setting['default'] # We wanna be in the activity's context while taking it down. newactivity = _ba.new_activity(gametype, settings) assert isinstance(newactivity, GameActivity) self._next_game_instance = newactivity self._next_game_name = nextlevel.name else: self._next_game_instance = None self._next_game_name = None # Special case: # If our current level is 'onslaught training', instantiate # our tutorial so its ready to go. (if we haven't run it yet). if (self.campaign_state['level'] == 'Onslaught Training' and self._tutorial_activity is None and not self._ran_tutorial_activity): from bastd.tutorial import TutorialActivity self._tutorial_activity = _ba.new_activity(TutorialActivity) def get_custom_menu_entries(self) -> List[Dict[str, Any]]: return self._custom_menu_ui def on_player_leave(self, player: ba.Player) -> None: from ba._general import WeakCall super().on_player_leave(player) # If all our players leave we wanna quit out of the session. _ba.timer(2.0, WeakCall(self._end_session_if_empty)) def _end_session_if_empty(self) -> None: activity = self.getactivity() if activity is None: return # Hmm what should we do in this case? # If there's still players in the current activity, we're good. if activity.players: return # If there's *no* players left in the current activity but there *is* # in the session, restart the activity to pull them into the game # (or quit if they're just in the lobby). if activity is not None and not activity.players and self.players: # Special exception for tourney games; don't auto-restart these. if self.tournament_id is not None: self.end() else: # Don't restart joining activities; this probably means there's # someone with a chooser up in that case. if not activity.is_joining_activity: self.restart() # Hmm; no players anywhere. lets just end the session. else: self.end() def _on_tournament_restart_menu_press(self, resume_callback: Callable[[], Any] ) -> None: # pylint: disable=cyclic-import from bastd.ui.tournamententry import TournamentEntryWindow from ba._gameactivity import GameActivity activity = self.getactivity() if activity is not None and not activity.is_expired(): assert self.tournament_id is not None assert isinstance(activity, GameActivity) TournamentEntryWindow(tournament_id=self.tournament_id, tournament_activity=activity, on_close_call=resume_callback) def restart(self) -> None: """Restart the current game activity.""" # Tell the current activity to end with a 'restart' outcome. # We use 'force' so that we apply even if end has already been called # (but is in its delay period). # Make an exception if there's no players left. Otherwise this # can override the default session end that occurs in that case. if not self.players: return # This method may get called from the UI context so make sure we # explicitly run in the activity's context. activity = self.getactivity() if activity is not None and not activity.is_expired(): activity.can_show_ad_on_death = True with _ba.Context(activity): activity.end(results={'outcome': 'restart'}, force=True) def on_activity_end(self, activity: ba.Activity, results: Any) -> None: """Method override for co-op sessions. Jumps between co-op games and score screens. """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=cyclic-import from ba._activitytypes import JoiningActivity, TransitionActivity from ba._lang import Lstr from ba._general import WeakCall from ba._coopgame import CoopGameActivity from ba._gameresults import TeamGameResults from bastd.tutorial import TutorialActivity from bastd.activity.coopscorescreen import CoopScoreScreen app = _ba.app # If we're running a TeamGameActivity we'll have a TeamGameResults # as results. Otherwise its an old CoopGameActivity so its giving # us a dict of random stuff. if isinstance(results, TeamGameResults): outcome = 'defeat' # This can't be 'beaten'. else: try: outcome = results['outcome'] except Exception: outcome = '' # If at any point we have no in-game players, quit out of the session # (this can happen if someone leaves in the tutorial for instance). active_players = [p for p in self.players if p.in_game] if not active_players: self.end() return # If we're in a between-round activity or a restart-activity, # hop into a round. if (isinstance( activity, (JoiningActivity, CoopScoreScreen, TransitionActivity))): if outcome == 'next_level': if self._next_game_instance is None: raise Exception() assert self._next_game_name is not None self.campaign_state['level'] = self._next_game_name next_game = self._next_game_instance else: next_game = self._current_game_instance # Special case: if we're coming from a joining-activity # and will be going into onslaught-training, show the # tutorial first. if (isinstance(activity, JoiningActivity) and self.campaign_state['level'] == 'Onslaught Training' and not app.kiosk_mode): if self._tutorial_activity is None: raise Exception("tutorial not preloaded properly") self.set_activity(self._tutorial_activity) self._tutorial_activity = None self._ran_tutorial_activity = True self._custom_menu_ui = [] # Normal case; launch the next round. else: # Reset stats for the new activity. self.stats.reset() for player in self.players: # Skip players that are still choosing a team. if player.in_game: self.stats.register_player(player) self.stats.set_activity(next_game) # Now flip the current activity. self.set_activity(next_game) if not app.kiosk_mode: if self.tournament_id is not None: self._custom_menu_ui = [{ 'label': Lstr(resource='restartText'), 'resume_on_call': False, 'call': WeakCall(self._on_tournament_restart_menu_press ) }] else: self._custom_menu_ui = [{ 'label': Lstr(resource='restartText'), 'call': WeakCall(self.restart) }] # If we were in a tutorial, just pop a transition to get to the # actual round. elif isinstance(activity, TutorialActivity): self.set_activity(_ba.new_activity(TransitionActivity)) else: # Generic team games. if isinstance(results, TeamGameResults): player_info = results.get_player_info() score = results.get_team_score(results.get_teams()[0]) fail_message = None score_order = ('decreasing' if results.get_lower_is_better() else 'increasing') if results.get_score_type() in ('seconds', 'milliseconds', 'time'): score_type = 'time' # Results contains milliseconds; ScoreScreen wants # hundredths; need to fix :-/ if score is not None: score //= 10 else: if results.get_score_type() != 'points': print(("Unknown score type: '" + results.get_score_type() + "'")) score_type = 'points' # Old coop-game-specific results; should migrate away from these. else: player_info = (results['player_info'] if 'player_info' in results else None) score = results['score'] if 'score' in results else None fail_message = (results['fail_message'] if 'fail_message' in results else None) score_order = (results['score_order'] if 'score_order' in results else 'increasing') activity_score_type = (activity.get_score_type() if isinstance( activity, CoopGameActivity) else None) assert activity_score_type is not None score_type = activity_score_type # Looks like we were in a round - check the outcome and # go from there. if outcome == 'restart': # This will pop up back in the same round. self.set_activity(_ba.new_activity(TransitionActivity)) else: self.set_activity( _ba.new_activity( CoopScoreScreen, { 'player_info': player_info, 'score': score, 'fail_message': fail_message, 'score_order': score_order, 'score_type': score_type, 'outcome': outcome, 'campaign': self.campaign, 'level': self.campaign_state['level'] })) # No matter what, get the next 2 levels ready to go. self._update_on_deck_game_instances()