402 lines
11 KiB
Python
402 lines
11 KiB
Python
"""
|
|
A simple quest system for EvAdventure.
|
|
|
|
A quest is represented by a quest-handler sitting as
|
|
`.quests` on a Character. Individual Quests are child classes of `EvAdventureQuest` with
|
|
methods for each step of the quest. The quest handler can add, remove, and track the progress
|
|
by calling the `progress` method on the quest. Persistent changes are stored on the quester
|
|
using the `add_data` and `get_data` methods with an Attribute as storage backend.
|
|
|
|
A quest ending can mean a reward or the start of
|
|
another quest.
|
|
|
|
"""
|
|
|
|
from evennia import Command
|
|
|
|
|
|
class EvAdventureQuest:
|
|
"""
|
|
This represents a single questing unit of quest.
|
|
|
|
Properties:
|
|
name (str): Main identifier for the quest.
|
|
category (str, optional): This + name must be globally unique.
|
|
it ends - it then pauses after the last completed step.
|
|
|
|
Each step of the quest is represented by a `.step_<stepname>` method. This should check
|
|
the status of the quest-step and update the `.current_step` or call `.complete()`. There
|
|
are also `.help_<stepname>` which is either a class-level help string or a method
|
|
returning a help text. All properties should be stored on the quester.
|
|
|
|
Example:
|
|
```py
|
|
class MyQuest(EvAdventureQuest):
|
|
'''A quest with two steps that ar'''
|
|
|
|
start_step = "A"
|
|
|
|
help_A = "You need a 'A_flag' attribute on yourself to finish this step!"
|
|
help_B = "Finally, you need more than 4 items in your inventory!"
|
|
|
|
def step_A(self, *args, **kwargs):
|
|
if self.get_data("A_flag") == True:
|
|
self.quester.msg("Completed the first step of the quest.")
|
|
self.current_step = "end"
|
|
self.progress()
|
|
|
|
def step_B(self, *args, **kwargs):
|
|
|
|
|
|
def step_end(self, *args, **kwargs):
|
|
if len(self.quester.contents) > 4:
|
|
self.quester.msg("Quest complete!")
|
|
self.complete()
|
|
```
|
|
"""
|
|
|
|
key = "base quest"
|
|
desc = "This is the base quest class"
|
|
start_step = "start"
|
|
|
|
|
|
# help entries for quests (could also be methods)
|
|
help_start = "You need to start first"
|
|
help_end = "You need to end the quest"
|
|
|
|
def __init__(self, quester):
|
|
self.quester = quester
|
|
self.data = self.questhandler.load_quest_data(self.key)
|
|
self._current_step = self.get_data("current_step")
|
|
|
|
if not self.current_step:
|
|
self.current_step = self.start_step
|
|
|
|
def add_data(self, key, value):
|
|
"""
|
|
Add data to the quest. This saves it permanently.
|
|
|
|
Args:
|
|
key (str): The key to store the data under.
|
|
value (any): The data to store.
|
|
|
|
"""
|
|
self.data[key] = value
|
|
self.questhandler.save_quest_data(self.key)
|
|
|
|
def get_data(self, key, default=None):
|
|
"""
|
|
Get data from the quest.
|
|
|
|
Args:
|
|
key (str): The key to get data for.
|
|
default (any, optional): The default value to return if key is not found.
|
|
|
|
Returns:
|
|
any: The data stored under the key.
|
|
|
|
"""
|
|
return self.data.get(key, default)
|
|
|
|
def remove_data(self, key):
|
|
"""
|
|
Remove data from the quest permanently.
|
|
|
|
Args:
|
|
key (str): The key to remove.
|
|
|
|
"""
|
|
self.data.pop(key, None)
|
|
self.questhandler.save_quest_data(self.key)
|
|
|
|
@property
|
|
def questhandler(self):
|
|
return self.quester.quests
|
|
|
|
@property
|
|
def current_step(self):
|
|
return self._current_step
|
|
|
|
@current_step.setter
|
|
def current_step(self, step_name):
|
|
self._current_step = step_name
|
|
self.add_data("current_step", step_name)
|
|
|
|
@property
|
|
def status(self):
|
|
return self.get_data("status", "started")
|
|
|
|
@status.setter
|
|
def status(self, value):
|
|
self.add_data("status", value)
|
|
|
|
@property
|
|
def is_completed(self):
|
|
return self.status == "completed"
|
|
|
|
@property
|
|
def is_abandoned(self):
|
|
return self.status == "abandoned"
|
|
|
|
@property
|
|
def is_failed(self):
|
|
return self.status == "failed"
|
|
|
|
def complete(self):
|
|
"""
|
|
Complete the quest.
|
|
|
|
"""
|
|
self.status = "completed"
|
|
|
|
def abandon(self):
|
|
"""
|
|
Abandon the quest.
|
|
|
|
"""
|
|
self.status = "abandoned"
|
|
|
|
def fail(self):
|
|
"""
|
|
Fail the quest.
|
|
|
|
"""
|
|
self.status = "failed"
|
|
|
|
def progress(self, *args, **kwargs):
|
|
"""
|
|
This is called whenever the environment expects a quest may need stepping. This will
|
|
determine which quest-step we are on and run `step_<stepname>`, which in turn will figure
|
|
out if the step is complete or not.
|
|
|
|
Args:
|
|
*args, **kwargs: Will be passed into the step method.
|
|
|
|
Notes:
|
|
`self.quester` is available as the character following the quest.
|
|
|
|
"""
|
|
getattr(self, f"step_{self.current_step}")(*args, **kwargs)
|
|
|
|
def help(self, *args, **kwargs):
|
|
"""
|
|
This is used to get help (or a reminder) of what needs to be done to complete the current
|
|
quest-step. It will look for a `help_<stepname>` method or string attribute on the quest.
|
|
|
|
Args:
|
|
*args, **kwargs: Will be passed into any help_* method.
|
|
|
|
Returns:
|
|
str: The help text for the current step.
|
|
|
|
"""
|
|
if self.status in ("abandoned", "completed", "failed"):
|
|
help_resource = getattr(self, f"help_{self.status}",
|
|
f"You have {self.status} this quest.")
|
|
else:
|
|
help_resource = getattr(self, f"help_{self.current_step}", "No help available.")
|
|
|
|
if callable(help_resource):
|
|
# the help_* methods can be used to dynamically generate help
|
|
return help_resource(*args, **kwargs)
|
|
else:
|
|
# normally it's just a string
|
|
return str(help_resource)
|
|
|
|
|
|
# step methods and hooks
|
|
|
|
def step_start(self, *args, **kwargs):
|
|
"""
|
|
Example step that completes immediately.
|
|
|
|
"""
|
|
self.complete()
|
|
|
|
def cleanup(self):
|
|
"""
|
|
This is called both when completing the quest, or when it is abandoned prematurely.
|
|
|
|
This is for cleaning up any extra state that were set during the quest (stuff in self.data
|
|
is automatically cleaned up)
|
|
"""
|
|
pass
|
|
|
|
|
|
class EvAdventureQuestHandler:
|
|
"""
|
|
This sits on the Character, as `.quests`.
|
|
|
|
It's initiated using a lazy property on the Character:
|
|
|
|
```
|
|
@lazy_property
|
|
def quests(self):
|
|
return EvAdventureQuestHandler(self)
|
|
```
|
|
|
|
"""
|
|
|
|
quest_storage_attribute_key = "_quests"
|
|
quest_storage_attribute_category = "evadventure"
|
|
|
|
quest_data_attribute_template = "_quest_data_{quest_key}"
|
|
quest_data_attribute_category = "evadventure"
|
|
|
|
def __init__(self, obj):
|
|
self.obj = obj
|
|
self.quests = {}
|
|
self.quest_classes = {}
|
|
self._load()
|
|
|
|
def _load(self):
|
|
self.quest_classes = self.obj.attributes.get(
|
|
self.quest_storage_attribute_key,
|
|
category=self.quest_storage_attribute_category,
|
|
default={},
|
|
)
|
|
# instantiate all quests
|
|
for quest_key, quest_class in self.quest_classes.items():
|
|
self.quests[quest_key] = quest_class(self.obj)
|
|
|
|
def _save(self):
|
|
self.obj.attributes.add(
|
|
self.quest_storage_attribute_key,
|
|
self.quest_classes,
|
|
category=self.quest_storage_attribute_category,
|
|
)
|
|
|
|
def has(self, quest_key):
|
|
"""
|
|
Check if a given quest is registered with the Character.
|
|
|
|
Args:
|
|
quest_key (str): The name of the quest to check for.
|
|
quest_category (str, optional): Quest category, if any.
|
|
|
|
Returns:
|
|
bool: If the character is following this quest or not.
|
|
|
|
"""
|
|
return bool(self.quests.get(quest_key))
|
|
|
|
def get(self, quest_key):
|
|
"""
|
|
Get the quest stored on character, if any.
|
|
|
|
Args:
|
|
quest_key (str): The name of the quest to check for.
|
|
|
|
Returns:
|
|
EvAdventureQuest or None: The quest stored, or None if
|
|
Character is not on this quest.
|
|
|
|
"""
|
|
return self.quests.get(quest_key)
|
|
|
|
def all(self):
|
|
"""
|
|
Get all quests stored on character.
|
|
|
|
Returns:
|
|
list: All quests stored on character.
|
|
|
|
"""
|
|
return list(self.quests.values())
|
|
|
|
def add(self, quest_class):
|
|
"""
|
|
Add a new quest
|
|
|
|
Args:
|
|
quest_class (EvAdventureQuest): The quest class to start.
|
|
|
|
"""
|
|
self.quest_classes[quest_class.key] = quest_class
|
|
self.quests[quest_class.key] = quest_class(self.obj)
|
|
self._save()
|
|
|
|
def remove(self, quest_key):
|
|
"""
|
|
Remove a quest. If not complete, it will be abandoned.
|
|
|
|
Args:
|
|
quest_key (str): The quest to remove.
|
|
|
|
"""
|
|
quest = self.quests.pop(quest_key, None)
|
|
if not quest.is_completed:
|
|
# make sure to cleanup
|
|
quest.abandon()
|
|
self.quest_classes.pop(quest_key, None)
|
|
self.quests.pop(quest_key, None)
|
|
self._save()
|
|
|
|
def save_quest_data(self, quest_key):
|
|
"""
|
|
Save data for a quest. We store this on the quester as well as updating the quest itself.
|
|
|
|
Args:
|
|
quest_key (str): The quest to save data for. The data is assumed to be stored on the
|
|
quest as `.data` (a dict).
|
|
|
|
"""
|
|
quest = self.get(quest_key)
|
|
if quest:
|
|
self.obj.attributes.add(
|
|
self.quest_data_attribute_template.format(quest_key=quest_key),
|
|
quest.data,
|
|
category=self.quest_data_attribute_category,
|
|
)
|
|
|
|
def load_quest_data(self, quest_key):
|
|
"""
|
|
Load data for a quest.
|
|
|
|
Args:
|
|
quest_key (str): The quest to load data for.
|
|
|
|
Returns:
|
|
dict: The data stored for the quest.
|
|
|
|
"""
|
|
return self.obj.attributes.get(
|
|
self.quest_data_attribute_template.format(quest_key=quest_key),
|
|
category=self.quest_data_attribute_category,
|
|
default={},
|
|
)
|
|
|
|
|
|
class CmdQuests(Command):
|
|
"""
|
|
List all quests and their statuses as well as get info about the status of
|
|
a specific quest.
|
|
|
|
Usage:
|
|
quests
|
|
quest <questname>
|
|
|
|
"""
|
|
key = "quests"
|
|
aliases = ["quest"]
|
|
|
|
def parse(self):
|
|
self.quest_name = self.args.strip()
|
|
|
|
def func(self):
|
|
if self.quest_name:
|
|
quest = self.caller.quests.get(self.quest_name)
|
|
if not quest:
|
|
self.msg(f"Quest {self.quest_name} not found.")
|
|
return
|
|
self.msg(f"Quest {quest.key}: {quest.status}\n{quest.help()}")
|
|
return
|
|
|
|
quests = self.caller.quests.all()
|
|
if not quests:
|
|
self.msg("No quests.")
|
|
return
|
|
|
|
for quest in quests:
|
|
self.msg(f"Quest {quest.key}: {quest.status}")
|
|
|