Finished quest engine for evadventure
This commit is contained in:
parent
944420e92e
commit
dc431fd5a5
4 changed files with 465 additions and 67 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
150
evennia/contrib/tutorials/evadventure/tests/test_quests.py
Normal file
150
evennia/contrib/tutorials/evadventure/tests/test_quests.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue