Source code for bascenev1._level

# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to individual levels in a campaign."""

from __future__ import annotations  # Docs-generation hack.

import copy
import weakref
from typing import TYPE_CHECKING, override

import babase

if TYPE_CHECKING:
    from typing import Any

    import bauiv1

    import bascenev1


def _get_level_display_name(key: str, game: babase.LangStr) -> babase.LangStr:
    """Return a displayable name for a campaign level.

    ``key`` is the level's displayname, or its name when it declares
    none. A bare ``${GAME}`` is simply the game's own name; the
    difficulty-prefixed forms are parameterized entries. Anything else
    -- a mod's campaign level -- shows its own text, with any
    ``${GAME}`` token substituted in flat.
    """
    # Safe up-call: bascenev1 is fully imported by the time this runs;
    # the cycle pylint sees is structural only.
    # pylint: disable-next=cyclic-import
    from bascenev1 import classicassets

    if key == '${GAME}':
        return game

    s = classicassets.strings.coop_levels
    if key == 'Pro ${GAME}':
        return s.pro_variant(game=game)
    if key == 'Uber ${GAME}':
        return s.uber_variant(game=game)

    entry = {
        'Infinite Onslaught': s.infinite_onslaught,
        'Infinite Runaround': s.infinite_runaround,
        'Onslaught Training': s.onslaught_training,
        'Pro Football': s.pro_football,
        'Pro Onslaught': s.pro_onslaught,
        'Pro Runaround': s.pro_runaround,
        'Rookie Football': s.rookie_football,
        'Rookie Onslaught': s.rookie_onslaught,
        'The Last Stand': s.the_last_stand,
        'Uber Football': s.uber_football,
        'Uber Onslaught': s.uber_onslaught,
        'Uber Runaround': s.uber_runaround,
    }.get(key)
    if entry is not None:
        return entry

    # A mod's level; show its own text.
    if '${GAME}' in key:
        key = key.replace('${GAME}', game.evaluate())
    return babase.LangStr.from_text(key)


[docs] class Level: """An entry in a :class:`~bascenev1.Campaign`.""" def __init__( self, name: str, gametype: type[bascenev1.GameActivity], settings: dict, preview_texture_name: str | None = None, *, displayname: str | None = None, preview_texture: bauiv1.TextureVerifiedSpec | None = None, ): if preview_texture is None and preview_texture_name is None: raise TypeError( 'A preview_texture is required (the deprecated' ' preview_texture_name is also still accepted).' ) self._name = name self._gametype = gametype self._settings = settings self._preview_texture_ref = preview_texture self._preview_texture: bauiv1.Texture | None = None self._preview_texture_name = preview_texture_name self._displayname = displayname self._campaign: weakref.ref[bascenev1.Campaign] | None = None self._index: int | None = None self._score_version_string: str | None = None @override def __repr__(self) -> str: cls = type(self) return f"<{cls.__module__}.{cls.__name__} '{self._name}'>" @property def name(self) -> str: """The unique name for this level.""" return self._name
[docs] 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(self) -> bauiv1.Texture: """The preview texture for this level. Level previews are drawn by ui code, so this is a loaded :class:`~bauiv1.Texture`. Levels are built during app-loading, which is *before* construct-mode has resolved asset-packages, so the constructor takes the wrapper reference (``someassets.textures.my_level_preview``, no ``.get()``) and the texture is loaded here on first access -- by which time the ui that wants to draw it exists and the package is registered. """ tex = self._preview_texture if tex is None: if self._preview_texture_ref is not None: tex = self._preview_texture_ref.get() else: # Constructed the deprecated way; look the name up # instead. Goes away when api 9 support ends. assert self._preview_texture_name is not None # Deferred: the ui feature-set is not a dependency of # ours, and only this legacy fallback needs it. import bauiv1 tex = bauiv1.gettexture(self._preview_texture_name) self._preview_texture = tex return tex @property def preview_texture_name(self) -> str | None: """The preview texture name for this level. .. deprecated:: 1.8.0 Use :attr:`preview_texture`, and pass ``preview_texture`` to the constructor rather than ``preview_texture_name``. This returns ``None`` for a level constructed the new way, so built-in levels now report ``None`` here. Removed when api 9 support ends. """ return self._preview_texture_name @property def displayname(self) -> bascenev1.Lstr: """The localized name for this level. .. deprecated:: 1.8.0 Use :attr:`displayname_langstr`. This property's type changes to :class:`~babase.LangStr` when api 9 support ends. """ return babase.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 displayname_langstr(self) -> babase.LangStr: """The localized name for this level. This is the :class:`~babase.LangStr` flavor of :attr:`displayname`. It exists only for the transition; once api 9 support ends, :attr:`displayname` returns this and this property goes away with the removal of api 10. """ return _get_level_display_name( ( self._displayname if self._displayname is not None else self._name ), self._gametype.get_display_string(self._settings, langstr=True), ) @property def gametype(self) -> type[bascenev1.GameActivity]: """The type of game used for this level.""" return self._gametype @property def campaign(self) -> bascenev1.Campaign | None: """The 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 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() val = config.get('Complete', False) assert isinstance(val, bool) return val
[docs] def set_complete(self, val: bool) -> None: """Set whether or not this level is complete.""" old_val = self.complete assert isinstance(old_val, bool) assert isinstance(val, bool) if val != old_val: config = self._get_config_dict() config['Complete'] = val
[docs] def get_high_scores(self) -> dict: """Return the current high scores for this level.""" config = self._get_config_dict() high_scores_key = f'High Scores{self.get_score_version_string()}' val = config.get(high_scores_key) if isinstance(val, dict): return copy.deepcopy(val) return {}
[docs] 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
[docs] 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.getscoreconfig().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.""" val = self._get_config_dict().get('Rating', 0.0) assert isinstance(val, float) return val
[docs] 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.campaign if campaign is None: raise RuntimeError('Level is not in a campaign.') configdict = campaign.configdict val: dict[str, Any] = configdict.setdefault( self._name, {'Rating': 0.0, 'Complete': False} ) assert isinstance(val, dict) return val def set_campaign(self, campaign: bascenev1.Campaign, index: int) -> None: """Internal: Used by campaign when adding levels to itself. :meta private: """ self._campaign = weakref.ref(campaign) self._index = index
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum