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
import copy
import weakref
from typing import TYPE_CHECKING, override
import babase
if TYPE_CHECKING:
from typing import Any
import bascenev1
[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,
*,
displayname: str | None = None,
):
self._name = name
self._gametype = gametype
self._settings = settings
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_name(self) -> str:
"""The preview texture name for this level."""
return self._preview_texture_name
@property
def displayname(self) -> bascenev1.Lstr:
"""The localized name for this level."""
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 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 = 'High Scores' + self.get_score_version_string()
if high_scores_key not in config:
return {}
return copy.deepcopy(config[high_scores_key])
[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