Implemented EvAdventure quest code+tutorial lesson

This commit is contained in:
Griatch 2024-03-30 23:42:16 +01:00
parent 5fcccca7d1
commit f1f1f6219a
10 changed files with 600 additions and 227 deletions

View file

@ -47,17 +47,8 @@ from collections import deque
from django.conf import settings
from django.db.models import Q
from evennia import (
CmdSet,
DefaultRoom,
EvEditor,
FuncParser,
InterruptCommand,
default_cmds,
gametime,
utils,
)
from evennia import (CmdSet, DefaultRoom, EvEditor, FuncParser,
InterruptCommand, default_cmds, gametime, utils)
from evennia.typeclasses.attributes import AttributeProperty
from evennia.utils.utils import list_to_string, repeat

View file

@ -84,15 +84,3 @@ class ObjType(Enum):
MAGIC = "magic"
QUEST = "quest"
TREASURE = "treasure"
class QuestStatus(Enum):
"""
Quest status
"""
STARTED = "started"
COMPLETED = "completed"
ABANDONED = "abandoned"
FAILED = "failed"

View file

@ -2,19 +2,17 @@
A simple quest system for EvAdventure.
A quest is represented by a quest-handler sitting as
.quest on a Character. Individual Quests are objects
that track the state and can have multiple steps, each
of which are checked off during the quest's progress.
The player can use the quest handler to track the
progress of their quests.
`.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 .enums import QuestStatus
from evennia import Command
class EvAdventureQuest:
@ -47,6 +45,9 @@ class EvAdventureQuest:
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!")
@ -54,62 +55,23 @@ class EvAdventureQuest:
```
"""
key = "basequest"
key = "base quest"
desc = "This is the base quest class"
start_step = "start"
completed_text = "This quest is completed!"
abandoned_text = "This quest is abandoned."
# 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, data=None):
if " " in self.key:
raise TypeError("The Quest name must not have spaces in it.")
def __init__(self, quester):
self.quester = quester
self.data = data or dict()
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
@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)
self.questhandler.do_save = True
@property
def status(self):
return self.get_data("status", QuestStatus.STARTED)
@status.setter
def status(self, value):
self.add_data("status", value)
@property
def is_completed(self):
return self.status == QuestStatus.COMPLETED
@property
def is_abandoned(self):
return self.status == QuestStatus.ABANDONED
@property
def is_failed(self):
return self.status == QuestStatus.FAILED
def add_data(self, key, value):
"""
Add data to the quest. This saves it permanently.
@ -120,18 +82,7 @@ class EvAdventureQuest:
"""
self.data[key] = value
self.questhandler.save_quest_data(self.key, self.data)
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, self.data)
self.questhandler.save_quest_data(self.key)
def get_data(self, key, default=None):
"""
@ -147,23 +98,70 @@ class EvAdventureQuest:
"""
return self.data.get(key, default)
def abandon(self):
def remove_data(self, key):
"""
Call when quest is abandoned.
Remove data from the quest permanently.
Args:
key (str): The key to remove.
"""
self.add_data("status", QuestStatus.ABANDONED)
self.questhandler.clean_quest_data(self.key)
self.cleanup()
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):
"""
Call this to end the quest.
Complete the quest.
"""
self.add_data("status", QuestStatus.COMPLETED)
self.questhandler.clean_quest_data(self.key)
self.cleanup()
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):
"""
@ -174,34 +172,38 @@ class EvAdventureQuest:
Args:
*args, **kwargs: Will be passed into the step method.
"""
return getattr(self, f"step_{self.current_step}")(*args, **kwargs)
Notes:
`self.quester` is available as the character following the quest.
def help(self):
"""
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.
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.is_completed:
return self.completed_text
if self.is_abandoned:
return self.abandoned_text
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.")
help_resource = (
getattr(self, f"help_{self.current_step}", None)
or "You need to {self.current_step} ..."
)
if callable(help_resource):
# the help_<current_step> can be a method to call
return 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):
@ -243,7 +245,6 @@ class EvAdventureQuestHandler:
def __init__(self, obj):
self.obj = obj
self.do_save = False
self.quests = {}
self.quest_classes = {}
self._load()
@ -256,7 +257,7 @@ class EvAdventureQuestHandler:
)
# instantiate all quests
for quest_key, quest_class in self.quest_classes.items():
self.quests[quest_key] = quest_class(self.obj, self.load_quest_data(quest_key))
self.quests[quest_key] = quest_class(self.obj)
def _save(self):
self.obj.attributes.add(
@ -264,57 +265,6 @@ class EvAdventureQuestHandler:
self.quest_classes,
category=self.quest_storage_attribute_category,
)
self._load() # important
self.do_save = False
def save_quest_data(self, quest_key, data):
"""
Save data for a quest. We store this on the quester as well as updating the quest itself.
Args:
data (dict): The data to store. This is commonly flags or other data needed to track the
quest.
"""
quest = self.get(quest_key)
if quest:
quest.data = data
self.obj.attributes.add(
self.quest_data_attribute_template.format(quest_key=quest_key),
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={},
)
def clean_quest_data(self, quest_key):
"""
Remove data for a quest.
Args:
quest_key (str): The quest to remove data for.
"""
self.obj.attributes.remove(
self.quest_data_attribute_template.format(quest_key=quest_key),
category=self.quest_data_attribute_category,
)
def has(self, quest_key):
"""
@ -344,6 +294,16 @@ class EvAdventureQuestHandler:
"""
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
@ -353,6 +313,7 @@ class EvAdventureQuestHandler:
"""
self.quest_classes[quest_class.key] = quest_class
self.quests[quest_class.key] = quest_class(self.obj)
self._save()
def remove(self, quest_key):
@ -368,50 +329,74 @@ class EvAdventureQuestHandler:
# make sure to cleanup
quest.abandon()
self.quest_classes.pop(quest_key, None)
self.quests.pop(quest_key, None)
self._save()
def get_help(self, quest_key=None):
def save_quest_data(self, quest_key):
"""
Get help text for a quest or for all quests. The help text is
a combination of the description of the quest and the help-text
of the current step.
Save data for a quest. We store this on the quester as well as updating the quest itself.
Args:
quest_key (str, optional): The quest-key. If not given, get help for all
quests in handler.
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:
list: Help texts, one for each quest, or only one if `quest_key` is given.
dict: The data stored for the quest.
"""
help_texts = []
if quest_key in self.quests:
quests = [self.quests[quest_key]]
else:
quests = self.quests.values()
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:
help_texts.append(f"|c{quest.key}|n\n {quest.desc}\n\n - {quest.help()}")
return help_texts
self.msg(f"Quest {quest.key}: {quest.status}")
def progress(self, quest_key=None, *args, **kwargs):
"""
Check progress of a given quest or all quests.
Args:
quest_key (str, optional): If given, check the progress of this quest (if we have it),
otherwise check progress on all quests.
*args, **kwargs: Will be passed into each quest's `progress` call.
"""
if quest_key in self.quests:
quests = [self.quests[quest_key]]
else:
quests = self.quests.values()
for quest in quests:
quest.progress(*args, **kwargs)
if self.do_save:
# do_save is set by the quest
self._save()

View file

@ -99,52 +99,50 @@ class EvAdventureQuestTest(EvAdventureMixin, BaseEvenniaTest):
def test_help(self):
"""Get help"""
# get help for all quests
help_txt = self.character.quests.get_help()
self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - You need to do A first."])
# get help for one specific quest
help_txt = self.character.quests.get_help(_TestQuest.key)
self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - You need to do A first."])
quest = self._get_quest()
# get help for a specific quest
help_txt = quest.help()
self.assertEqual(help_txt, "You need to do A first.")
# help for finished quest
self._get_quest().complete()
help_txt = self.character.quests.get_help()
self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - This quest is completed!"])
quest.complete()
help_txt = quest.help()
self.assertEqual(help_txt, "You have completed this quest.")
def test_progress__fail(self):
"""
Check progress without having any.
"""
# progress all quests
self.character.quests.progress()
# progress one quest
self.character.quests.progress(_TestQuest.key)
quest = self._get_quest()
# progress quest
quest.progress()
# still on step A
self.assertEqual(self._get_quest().current_step, "A")
self.assertEqual(quest.current_step, "A")
def test_progress(self):
"""
Fulfill the quest steps in sequess
Fulfill the quest steps in sequence.
"""
quest = self._get_quest()
# A requires a certain object in inventory
self._fulfillA()
self.character.quests.progress()
self.assertEqual(self._get_quest().current_step, "B")
quest.progress()
self.assertEqual(quest.current_step, "B")
# B requires progress be called with specific kwarg
# should not step (no kwarg)
self.character.quests.progress()
self.assertEqual(self._get_quest().current_step, "B")
quest.progress()
self.assertEqual(quest.current_step, "B")
# should step (kwarg sent)
self.character.quests.progress(complete_quest_B=True)
self.assertEqual(self._get_quest().current_step, "C")
quest.progress(complete_quest_B=True)
self.assertEqual(quest.current_step, "C")
# C requires a counter Attribute on char be high enough
self._fulfillC()
self.character.quests.progress()
self.assertEqual(self._get_quest().current_step, "C") # still on last step
self.assertEqual(self._get_quest().is_completed, True)
quest.progress()
self.assertEqual(quest.current_step, "C") # still on last step
self.assertEqual(quest.is_completed, True)