Continue with quest beginner tutorial

This commit is contained in:
Griatch 2024-03-30 09:47:07 +01:00
parent a1023ebc7e
commit c05714d0d4
4 changed files with 165 additions and 38 deletions

View file

@ -1,5 +1,9 @@
# Game Quests
```{warning}
This part of the Beginner tutorial is still being developed.
```
A _quest_ is a common feature of games. From classic fetch-quests like retrieving 10 flowers to complex quest chains involving drama and intrigue, quests need to be properly tracked in our game.
A quest follows a specific development:
1. The quest is _started_. This normally involves the player accepting the quest, from a quest-giver, job board or other source. But the quest could also be thrust on the player ("save the family from the burning house before it collapses!")
2. A quest may consist of one or more 'steps'. Each step has its own set of finish conditions.
3. At suitable times the quest is _checked_. This could happen on a timer or when trying to 'hand in' the quest. When checking, the current 'step' is checked against its finish conditions. If ok, that step is closed and the next step is checked until it either hits a step that is not yet complete, or there are no more steps, in which case the entire quest is complete.

View file

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

View file

@ -14,9 +14,7 @@ another quest.
"""
from copy import copy, deepcopy
from evennia.utils import dbserialize
from .enums import QuestStatus
class EvAdventureQuest:
@ -40,11 +38,11 @@ class EvAdventureQuest:
start_step = "A"
help_A = "You need a '_quest_A_flag' on yourself to finish this step!"
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.quester.db._quest_A_flag == True:
if self.get_data("A_flag") == True:
self.quester.msg("Completed the first step of the quest.")
self.current_step = "end"
self.progress()
@ -67,21 +65,16 @@ class EvAdventureQuest:
help_start = "You need to start first"
help_end = "You need to end the quest"
def __init__(self, quester, start_step=None):
def __init__(self, quester, data=None):
if " " in self.key:
raise TypeError("The Quest name must not have spaces in it.")
self.quester = quester
self._current_step = start_step or self.start_step
self.is_completed = False
self.is_abandoned = False
self.data = data or dict()
self._current_step = self.get_data("current_step")
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)
if not self.current_step:
self.current_step = self.start_step
@property
def questhandler(self):
@ -94,14 +87,73 @@ class EvAdventureQuest:
@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.
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, 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)
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 abandon(self):
"""
Call when quest is abandoned.
"""
self.is_abandoned = True
self.add_data("status", QuestStatus.ABANDONED)
self.questhandler.clean_quest_data(self.key)
self.cleanup()
def complete(self):
@ -109,7 +161,8 @@ class EvAdventureQuest:
Call this to end the quest.
"""
self.is_completed = True
self.add_data("status", QuestStatus.COMPLETED)
self.questhandler.clean_quest_data(self.key)
self.cleanup()
def progress(self, *args, **kwargs):
@ -122,8 +175,7 @@ class EvAdventureQuest:
*args, **kwargs: Will be passed into the step method.
"""
if not (self.is_completed or self.is_abandoned):
getattr(self, f"step_{self.current_step}")(*args, **kwargs)
return getattr(self, f"step_{self.current_step}")(*args, **kwargs)
def help(self):
"""
@ -162,8 +214,9 @@ class EvAdventureQuest:
def cleanup(self):
"""
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.
This is for cleaning up any extra state that were set during the quest (stuff in self.data
is automatically cleaned up)
"""
pass
@ -185,27 +238,84 @@ class EvAdventureQuestHandler:
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.do_save = False
self.quests = {}
self.quest_classes = {}
self._load()
def _load(self):
self.storage = self.obj.attributes.get(
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, self.load_quest_data(quest_key))
def _save(self):
self.obj.attributes.add(
self.quest_storage_attribute_key,
self.storage,
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):
"""
Check if a given quest is registered with the Character.
@ -218,7 +328,7 @@ class EvAdventureQuestHandler:
bool: If the character is following this quest or not.
"""
return bool(self.storage.get(quest_key))
return bool(self.quests.get(quest_key))
def get(self, quest_key):
"""
@ -232,17 +342,17 @@ class EvAdventureQuestHandler:
Character is not on this quest.
"""
return self.storage.get(quest_key)
return self.quests.get(quest_key)
def add(self, quest):
def add(self, quest_class):
"""
Add a new quest
Args:
quest (EvAdventureQuest): The quest class to start.
quest_class (EvAdventureQuest): The quest class to start.
"""
self.storage[quest.key] = quest(self.obj)
self.quest_classes[quest_class.key] = quest_class
self._save()
def remove(self, quest_key):
@ -253,10 +363,11 @@ class EvAdventureQuestHandler:
quest_key (str): The quest to remove.
"""
quest = self.storage.pop(quest_key, None)
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._save()
def get_help(self, quest_key=None):
@ -274,10 +385,10 @@ class EvAdventureQuestHandler:
"""
help_texts = []
if quest_key in self.storage:
quests = [self.storage[quest_key]]
if quest_key in self.quests:
quests = [self.quests[quest_key]]
else:
quests = self.storage.values()
quests = self.quests.values()
for quest in quests:
help_texts.append(f"|c{quest.key}|n\n {quest.desc}\n\n - {quest.help()}")
@ -293,10 +404,10 @@ class EvAdventureQuestHandler:
*args, **kwargs: Will be passed into each quest's `progress` call.
"""
if quest_key in self.storage:
quests = [self.storage[quest_key]]
if quest_key in self.quests:
quests = [self.quests[quest_key]]
else:
quests = self.storage.values()
quests = self.quests.values()
for quest in quests:
quest.progress(*args, **kwargs)

View file

@ -108,7 +108,7 @@ class EvAdventureQuestTest(EvAdventureMixin, BaseEvenniaTest):
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
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!"])