# Copyright (c) 2011-2019 Eric Froemling """Functionality related to individual levels in a campaign.""" from __future__ import annotations import copy import weakref from typing import TYPE_CHECKING import _ba if TYPE_CHECKING: from weakref import ReferenceType from typing import Type, Any, Dict, Optional import ba class Level: """An entry in a ba.Campaign consisting of a name, game type, and settings. category: Gameplay Classes """ def __init__(self, name: str, gametype: Type[ba.GameActivity], settings: Dict[str, Any], preview_texture_name: str, displayname: str = None): """Initializes a Level object with the provided values.""" self._name = name self._gametype = gametype self._settings = settings self._preview_texture_name = preview_texture_name self._displayname = displayname self._campaign: Optional[ReferenceType[ba.Campaign]] = None self._index: Optional[int] = None self._score_version_string: Optional[str] = None @property def name(self) -> str: """The unique name for this Level.""" return self._name def get_settings(self) -> Dict[str, Any]: """Returns the settings for this Level.""" settings = copy.deepcopy(self._settings) # So the game knows what the level is called. # Hmm; seems hacky; I think we should take this out. settings['name'] = self._name return settings @property def preview_texture_name(self) -> str: """The preview texture name for this Level.""" return self._preview_texture_name def get_preview_texture(self) -> ba.Texture: """Load/return the preview Texture for this Level.""" return _ba.gettexture(self._preview_texture_name) @property def displayname(self) -> ba.Lstr: """The localized name for this Level.""" from ba import _lang return _lang.Lstr( translate=('coopLevelNames', self._displayname if self._displayname is not None else self._name), subs=[('${GAME}', self._gametype.get_display_string(self._settings))]) @property def gametype(self) -> Type[ba.GameActivity]: """The type of game used for this Level.""" return self._gametype def get_campaign(self) -> Optional[ba.Campaign]: """Return the ba.Campaign this Level is associated with, or None.""" return None if self._campaign is None else self._campaign() @property def index(self) -> int: """The zero-based index of this Level in its ba.Campaign. Access results in a RuntimeError if the Level is not assigned to a Campaign. """ if self._index is None: raise RuntimeError("Level is not part of a Campaign") return self._index @property def complete(self) -> bool: """Whether this Level has been completed.""" config = self._get_config_dict() return config.get('Complete', False) def set_complete(self, val: bool) -> None: """Set whether or not this level is complete.""" old_val = self.complete assert isinstance(old_val, bool) and isinstance(val, bool) if val != old_val: config = self._get_config_dict() config['Complete'] = val def get_high_scores(self) -> Dict: """Return the current high scores for this Level.""" config = self._get_config_dict() high_scores_key = 'High Scores' + self.get_score_version_string() if high_scores_key not in config: return {} return copy.deepcopy(config[high_scores_key]) def set_high_scores(self, high_scores: Dict) -> None: """Set high scores for this level.""" config = self._get_config_dict() high_scores_key = 'High Scores' + self.get_score_version_string() config[high_scores_key] = high_scores def get_score_version_string(self) -> str: """Return the score version string for this Level. If a Level's gameplay changes significantly, its version string can be changed to separate its new high score lists/etc. from the old. """ if self._score_version_string is None: scorever = ( self._gametype.get_resolved_score_info()['score_version']) if scorever != '': scorever = ' ' + scorever self._score_version_string = scorever assert self._score_version_string is not None return self._score_version_string @property def rating(self) -> float: """The current rating for this Level.""" return self._get_config_dict().get('Rating', 0.0) def set_rating(self, rating: float) -> None: """Set a rating for this Level, replacing the old ONLY IF higher.""" old_rating = self.rating config = self._get_config_dict() config['Rating'] = max(old_rating, rating) def _get_config_dict(self) -> Dict[str, Any]: """Return/create the persistent state dict for this level. The referenced dict exists under the game's config dict and can be modified in place.""" campaign = self.get_campaign() if campaign is None: raise Exception("level is not in a campaign") campaign_config = campaign.get_config_dict() val: Dict[str, Any] = campaign_config.setdefault( self._name, { 'Rating': 0.0, 'Complete': False }) assert isinstance(val, dict) return val def set_campaign(self, campaign: ba.Campaign, index: int) -> None: """For use by ba.Campaign when adding levels to itself. (internal)""" self._campaign = weakref.ref(campaign) self._index = index