applying changes from feedback

This commit is contained in:
Cal 2024-03-29 16:46:45 -06:00
parent be9ad0278a
commit 30151b7e1f
3 changed files with 191 additions and 112 deletions

View file

@ -38,7 +38,7 @@ To add achievement tracking, put `track_achievements` in your relevant hooks.
Example:
def at_use(self, user, **kwargs):
# track this use for any achievements about using an object named our name
# track this use for any achievements that are categorized as "use" and are tracking something that matches our key
finished_achievements = track_achievements(user, category="use", tracking=self.key)
Despite the example, it's likely to be more useful to reference a tag than the object's key.
@ -53,30 +53,61 @@ from evennia.commands.default.muxcommand import MuxCommand
# this is either a string of the attribute name, or a tuple of strings of the attribute name and category
_ACHIEVEMENT_ATTR = make_iter(getattr(settings, "ACHIEVEMENT_CONTRIB_ATTRIBUTE", "achievements"))
_ATTR_KEY = _ACHIEVEMENT_ATTR[0]
_ATTR_CAT = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None
_ACHIEVEMENT_INFO = None
# load the achievements data
_ACHIEVEMENT_DATA = {}
if modules := getattr(settings, "ACHIEVEMENT_CONTRIB_MODULES", None):
for module_path in make_iter(modules):
module_achieves = {
val.key("key", key).lower(): val
for key, val in all_from_module(module_path).items()
if isinstance(val, dict) and not key.startswith("_")
}
if any(key in _ACHIEVEMENT_DATA for key in module_achieves.keys()):
logger.log_warn(
"There are conflicting achievement keys! Only the last achievement registered to the key will be recognized."
)
_ACHIEVEMENT_DATA |= module_achieves
else:
logger.log_warn("No achievement modules have been added to settings.")
def _load_achievements():
def _read_player_data(achiever):
"""
Loads the achievement data from settings, if it hasn't already been loaded.
helper function to get a player's achievement data from the database.
Args:
achiever (Object or Account): The achieving entity
Returns:
achievements (dict) - the loaded achievement info
dict: The deserialized achievement data.
"""
global _ACHIEVEMENT_INFO
if _ACHIEVEMENT_INFO is None:
_ACHIEVEMENT_INFO = {}
if modules := getattr(settings, "ACHIEVEMENT_CONTRIB_MODULES", None):
for module_path in make_iter(modules):
_ACHIEVEMENT_INFO |= {
key: val
for key, val in all_from_module(module_path).items()
if isinstance(val, dict) and not key.startswith('_')
}
else:
logger.log_warn("No achievement modules have been added to settings.")
return _ACHIEVEMENT_INFO
if data := achiever.attributes.get(_ATTR_KEY, default={}, category=_ATTR_CAT):
# detach the data from the db
data.deserialize()
# return the data
return data
def _write_player_data(achiever, data):
"""
helper function to write a player's achievement data to the database.
Args:
achiever (Object or Account): The achieving entity
data (dict): The full achievement data for this entity.
Returns:
None
Notes:
This function will overwrite any existing achievement data for the entity.
"""
if not isinstance(data, dict):
raise ValueError("Achievement data must be a dict.")
achiever.attributes.add(_ATTR_KEY, data, category=_ATTR_CAT)
def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs):
@ -91,26 +122,25 @@ def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs
tracking (str or None): The specific item being tracked in the achievement.
Returns:
completed (tuple): The keys of any achievements that were completed by this update.
tuple: The keys of any achievements that were completed by this update.
"""
if not (all_achievements := _load_achievements()):
if not _ACHIEVEMENT_DATA:
# there are no achievements available, there's nothing to do
return tuple()
# split out the achievement attribute info
attr_key = _ACHIEVEMENT_ATTR[0]
attr_cat = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None
# get the achiever's progress data, and detach from the db so we only read/write once
if progress_data := achiever.attributes.get(attr_key, default={}, category=attr_cat):
progress_data = progress_data.deserialize()
# get the achiever's progress data
progress_data = _read_player_data(achiever)
# filter all of the achievements down to the relevant ones
relevant_achievements = (
(key, val)
for key, val in all_achievements.items()
if (not category or category in make_iter(val.get("category",[]))) # filter by category
and (not tracking or not val.get("tracking") or tracking in make_iter(val.get("tracking",[]))) # filter by tracked item
for key, val in _ACHIEVEMENT_DATA.items()
if (not category or category in make_iter(val.get("category", []))) # filter by category
and (
not tracking
or not val.get("tracking")
or tracking in make_iter(val.get("tracking", []))
) # filter by tracked item
and not progress_data.get(key, {}).get("completed") # filter by completion status
and all(
progress_data.get(prereq, {}).get("completed")
@ -121,7 +151,7 @@ def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs
completed = []
# loop through all the relevant achievements and update the progress data
for achieve_key, achieve_data in relevant_achievements:
if target_count := achieve_data.get("count"):
if target_count := achieve_data.get("count", 1):
# check if we need to track things individually or not
separate_totals = achieve_data.get("tracking_type", "sum") == "separate"
if achieve_key not in progress_data:
@ -156,7 +186,7 @@ def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs
progress_data[key]["completed"] = True
# write the updated progress back to the achievement attribute
achiever.attributes.add(attr_key, progress_data, category=attr_cat)
_write_player_data(achiever, progress_data)
# return all the achievements we just completed
return tuple(completed)
@ -172,10 +202,10 @@ def get_achievement(key):
Returns:
dict or None: The achievement data, or None if it doesn't exist
"""
if not (all_achievements := _load_achievements()):
if not _ACHIEVEMENT_DATA:
# there are no achievements available, there's nothing to do
return None
if data := all_achievements.get(key):
if data := _ACHIEVEMENT_DATA.get(key.lower()):
return dict(data)
return None
@ -183,12 +213,15 @@ def get_achievement(key):
def all_achievements():
"""
Returns a dict of all achievements in the game.
Returns:
dict
"""
# we do this to prevent accidental in-memory modification of reference data
return dict((key, dict(val)) for key, val in _load_achievements().items())
# we do this to mitigate accidental in-memory modification of reference data
return dict((key, dict(val)) for key, val in _ACHIEVEMENT_DATA.items())
def get_progress(achiever, key):
def get_achievement_progress(achiever, key):
"""
Retrieve the progress data on a particular achievement for a particular achiever.
@ -197,14 +230,11 @@ def get_progress(achiever, key):
key (str): The achievement key
Returns:
data (dict): The progress data
dict: The progress data
"""
# split out the achievement attribute info
attr_key = _ACHIEVEMENT_ATTR[0]
attr_cat = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None
if progress_data := achiever.attributes.get(attr_key, default={}, category=attr_cat):
# detach the data from the db to avoid data corruption and return the data
return progress_data.deserialize().get(key, {})
if progress_data := _read_player_data(achiever):
# get the specific key's data
return progress_data.get(key, {})
else:
# just return an empty dict
return {}
@ -212,21 +242,26 @@ def get_progress(achiever, key):
def search_achievement(search_term):
"""
Search for an achievement by name.
Search for an achievement containing the search term. If no matches are found in the achievement names, it searches
in the achievement descriptions.
Args:
search_term (str): The string to search for.
Returns:
results (dict): A dict of key:data pairs of matching achievements.
dict: A dict of key:data pairs of matching achievements.
"""
if not (all_achievements := _load_achievements()):
if not _ACHIEVEMENT_DATA:
# there are no achievements available, there's nothing to do
return {}
keys, names = zip(*((key, val["name"]) for key, val in all_achievements.items()))
keys, names, descs = zip(
*((key, val["name"], val["desc"]) for key, val in _ACHIEVEMENT_DATA.items())
)
indices = string_partial_matching(names, search_term)
if not indices:
indices = string_partial_matching(descs, search_term)
return dict((keys[i], dict(all_achievements[keys[i]])) for i in indices)
return dict((keys[i], dict(_ACHIEVEMENT_DATA[keys[i]])) for i in indices)
class CmdAchieve(MuxCommand):
@ -256,9 +291,16 @@ class CmdAchieve(MuxCommand):
aliases = (
"achievement",
"achieve",
"achieves",
)
switch_options = ("progress", "completed", "done", "all")
template = """\
|w{name}|n
{desc}
{status}
""".rstrip()
def format_achievement(self, achievement_data):
"""
Formats the raw achievement data for display.
@ -270,11 +312,6 @@ class CmdAchieve(MuxCommand):
str: The display string to be sent to the caller.
"""
template = """\
|w{name}|n
{desc}
{status}
""".rstrip()
if achievement_data.get("completed"):
# it's done!
@ -293,7 +330,7 @@ class CmdAchieve(MuxCommand):
pct = (achievement_data["progress"] * 100) // count
status = f"{pct}% complete"
return template.format(
return self.template.format(
name=achievement_data.get("name", ""),
desc=achievement_data.get("desc", ""),
status=status,
@ -311,19 +348,10 @@ class CmdAchieve(MuxCommand):
self.msg("There are no achievements in this game.")
return
# split out the achievement attribute info
attr_key = _ACHIEVEMENT_ATTR[0]
attr_cat = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None
# get the achiever's progress data, and detach from the db so we only read once
if progress_data := self.caller.attributes.get(attr_key, default={}, category=attr_cat):
progress_data = progress_data.deserialize()
# if the caller is not an account, we get their account progress too
# get the achiever's progress data
progress_data = _read_player_data(self.caller)
if self.caller != self.account:
if account_progress := self.account.attributes.get(
attr_key, default={}, category=attr_cat
):
progress_data |= account_progress.deserialize()
progress_data |= _read_player_data(self.account)
# go through switch options
# we only show achievements that are in progress