Implemented EvAdventure quest code+tutorial lesson
This commit is contained in:
parent
5fcccca7d1
commit
f1f1f6219a
10 changed files with 600 additions and 227 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue