Finished quest engine for evadventure

This commit is contained in:
Griatch 2022-07-22 17:36:38 +02:00
parent 944420e92e
commit dc431fd5a5
4 changed files with 465 additions and 67 deletions

View file

@ -14,6 +14,10 @@ another quest.
"""
from copy import copy, deepcopy
from evennia.utils import dbserialize
class EvAdventureQuest:
"""
@ -22,59 +26,104 @@ class EvAdventureQuest:
Properties:
name (str): Main identifier for the quest.
category (str, optional): This + name must be globally unique.
steps (list): A list of strings, representing how many steps are
in the quest. The first step is always the beginning, when the quest is presented.
The last step is always the end of the quest. It is possible to abort the quest before
it ends - it then pauses after the last completed step.
Each step is represented by three methods on this object:
`check_<stepname>` and `complete_<stepname>`. `help_<stepname>` is used to get
a guide/reminder on what you are supposed to do.
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 '_quest_A_flag' 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.quester.db._quest_A_flag == True:
self.quester.msg("Completed the first step of the quest.")
self.current_step = "end"
self.progress()
def step_end(self, *args, **kwargs):
if len(self.quester.contents) > 4:
self.quester.msg("Quest complete!")
self.complete()
```
"""
# name + category must be globally unique. They are
# queried as name:category or just name, if category is empty.
key = "basequest"
desc = "This is the base quest. It will just step through its steps immediately."
desc = "This is the base quest class"
start_step = "start"
end_text = "This quest is completed!"
# help entries for quests
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, questhandler, start_step="start"):
def __init__(self, quester, start_step=None):
if " " in self.key:
raise TypeError("The Quest name must not have spaces in it.")
self.questhandler = questhandler
self.current_step = start_step
self.completed = False
self.quester = quester
self._current_step = start_step or self.start_step
self.is_completed = False
self.is_abandoned = False
def __serialize_dbobjs__(self):
self.quester = dbserialize.dbserialize(self.quester)
def __deserialize_dbobjs__(self):
if isinstance(self.quester, bytes):
self.quester = dbserialize.dbunserialize(self.quester)
@property
def quester(self):
return self.questhandler.obj
def questhandler(self):
return self.quester.quests
def end_quest(self):
@property
def current_step(self):
return self._current_step
@current_step.setter
def current_step(self, step_name):
self._current_step = step_name
self.questhandler.do_save = True
def abandon(self):
"""
Call when quest is abandoned.
"""
self.is_abandoned = True
self.cleanup()
def complete(self):
"""
Call this to end the quest.
"""
self.completed = True
self.is_completed = True
self.cleanup()
def progress(self, *args, **kwargs):
"""
This is called whenever the environment expects a quest may be complete.
This will determine which quest-step we are on, run check_<stepname>, and if it
succeeds, continue with complete_<stepname>.
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 check/complete methods.
*args, **kwargs: Will be passed into the step method.
"""
if getattr(self, f"check_{self.current_step}")(*args, **kwargs):
getattr(self, f"complete_{self.current_step}")(*args, **kwargs)
if not (self.is_completed or self.is_abandoned):
getattr(self, f"step_{self.current_step}")(*args, **kwargs)
def help(self):
"""
@ -85,6 +134,11 @@ class EvAdventureQuest:
str: The help text for the current step.
"""
if self.is_completed:
return self.completed_text
if self.is_abandoned:
return self.abandoned_text
help_resource = (
getattr(self, f"help_{self.current_step}", None)
or "You need to {self.current_step} ..."
@ -96,35 +150,22 @@ class EvAdventureQuest:
# normally it's just a string
return str(help_resource)
# step methods
# step methods and hooks
def check_start(self, *args, **kwargs):
def step_start(self, *args, **kwargs):
"""
Check if the starting conditions are met.
Returns:
bool: If this step is complete or not. If complete, the `complete_start`
method will fire.
Example step that completes immediately.
"""
return True
self.complete()
def complete_start(self, *args, **kwargs):
def cleanup(self):
"""
Completed start. This should change `.current_step` to the next step to complete
and call `self.progress()` just in case the next step is already completed too.
This is called both when completing the quest, or when it is abandoned prematurely.
Make sure to cleanup any quest-related data stored when following the quest.
"""
self.quester.msg("Completed the first step of the quest.")
self.current_step = "end"
self.progress()
def check_end(self, *args, **kwargs):
return True
def complete_end(self, *args, **kwargs):
self.quester.msg("Quest complete!")
self.end_quest()
pass
class EvAdventureQuestHandler:
@ -146,6 +187,7 @@ class EvAdventureQuestHandler:
def __init__(self, obj):
self.obj = obj
self.do_save = False
self._load()
def _load(self):
@ -161,6 +203,8 @@ class EvAdventureQuestHandler:
self.storage,
category=self.quest_storage_attribute_category,
)
self._load() # important
self.do_save = False
def has(self, quest_key):
"""
@ -190,52 +234,63 @@ class EvAdventureQuestHandler:
"""
return self.storage.get(quest_key)
def add(self, quest, autostart=True):
def add(self, quest):
"""
Add a new quest
Args:
quest (EvAdventureQuest): The quest to start.
autostart (bool, optional): If set, the quest will
start immediately.
quest (EvAdventureQuest): The quest class to start.
"""
self.storage[quest.key] = quest
self.storage[quest.key] = quest(self.obj)
self._save()
def remove(self, quest_key):
"""
Remove a quest.
Remove a quest. If not complete, it will be abandoned.
Args:
quest_key (str): The quest to remove.
"""
self.storage.pop(quest_key, None)
quest = self.storage.pop(quest_key, None)
if not quest.is_completed:
# make sure to cleanup
quest.abandon()
self._save()
def help(self, quest_key=None):
def get_help(self, quest_key=None):
"""
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.
Args:
quest_key (str, optional): The quest-key. If not given, get help for all
quests in handler.
Returns:
list: Help texts, one for each quest, or only one if `quest_key` is given.
"""
help_text = []
help_texts = []
if quest_key in self.storage:
quests = [self.storage[quest_key]]
else:
quests = self.storage.values()
for quest in quests:
help_text.append(f"|c{quest.key}|n\n {quest.desc}\n\n - {quest.help}")
return "---".join(help_text)
help_texts.append(f"|c{quest.key}|n\n {quest.desc}\n\n - {quest.help()}")
return help_texts
def progress(self, quest_key=None):
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.storage:
@ -244,4 +299,8 @@ class EvAdventureQuestHandler:
quests = self.storage.values()
for quest in quests:
quest.progress()
quest.progress(*args, **kwargs)
if self.do_save:
# do_save is set by the quest
self._save()

View file

@ -5,7 +5,6 @@ Test EvAdventure combat.
from unittest.mock import MagicMock, patch
from anything import Something
from evennia.utils import create
from evennia.utils.test_resources import BaseEvenniaTest
@ -13,13 +12,7 @@ from .. import combat_turnbased
from ..characters import EvAdventureCharacter
from ..enums import WieldLocation
from ..npcs import EvAdventureMob
from ..objects import (
EvAdventureConsumable,
EvAdventureRunestone,
EvAdventureWeapon,
WeaponEmptyHand,
)
from ..rooms import EvAdventureRoom
from ..objects import EvAdventureConsumable, EvAdventureRunestone, EvAdventureWeapon
from .mixins import EvAdventureMixin

View file

@ -0,0 +1,150 @@
"""
Testing Quest functionality.
"""
from unittest.mock import MagicMock
from evennia.utils.test_resources import BaseEvenniaTest
from .. import quests
from ..objects import EvAdventureObject
from .mixins import EvAdventureMixin
class _TestQuest(quests.EvAdventureQuest):
"""
Test quest.
"""
key = "testquest"
desc = "A test quest!"
start_step = "A"
end_text = "This task is completed."
help_A = "You need to do A first."
help_B = "Next, do B."
def step_A(self, *args, **kwargs):
"""
Quest-step A is completed when quester carries an item with tag "QuestA" and category
"quests".
"""
# note - this could be done with a direct db query instead to avoid a loop, for a
# unit test it's fine though
if any(obj for obj in self.quester.contents if obj.tags.has("QuestA", category="quests")):
self.quester.msg("Completed step A of quest!")
self.current_step = "B"
self.progress()
def step_B(self, *args, **kwargs):
"""
Quest-step B is completed when the progress-check is called with a special kwarg
"complete_quest_B"
"""
if kwargs.get("complete_quest_B", False):
self.quester.msg("Completed step B of quest!")
self.quester.db.test_quest_counter = 0
self.current_step = "C"
self.progress()
def help_C(self):
"""Testing the method-version of getting a help entry"""
return f"Only C left now, {self.quester.key}!"
def step_C(self, *args, **kwargs):
"""
Step C (final) step of quest completes when a counter on quester is big enough.
"""
if self.quester.db.test_quest_counter and self.quester.db.test_quest_counter > 5:
self.quester.msg("Quest complete! Get XP rewards!")
self.quester.db.xp += 10
self.complete()
def cleanup(self):
"""
Cleanup data related to quest.
"""
del self.quester.db.test_quest_counter
class EvAdventureQuestTest(EvAdventureMixin, BaseEvenniaTest):
"""
Test questing.
"""
def setUp(self):
super().setUp()
self.character.quests.add(_TestQuest)
self.character.msg = MagicMock()
def _get_quest(self):
return self.character.quests.get(_TestQuest.key)
def _fulfillA(self):
"""Fulfill quest step A"""
EvAdventureObject.create(
key="quest obj", location=self.character, tags=(("QuestA", "quests"),)
)
def _fulfillC(self):
"""Fullfill quest step C"""
self.character.db.test_quest_counter = 6
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."])
# help for finished quest
self._get_quest().is_completed = True
help_txt = self.character.quests.get_help()
self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - This quest is completed!"])
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)
# still on step A
self.assertEqual(self._get_quest().current_step, "A")
def test_progress(self):
"""
Fulfill the quest steps in sequess
"""
# A requires a certain object in inventory
self._fulfillA()
self.character.quests.progress()
self.assertEqual(self._get_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")
# should step (kwarg sent)
self.character.quests.progress(complete_quest_B=True)
self.assertEqual(self._get_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)