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 # Game Quests
```{warning} 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.
This part of the Beginner tutorial is still being developed.
``` 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" MAGIC = "magic"
QUEST = "quest" QUEST = "quest"
TREASURE = "treasure" 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 .enums import QuestStatus
from evennia.utils import dbserialize
class EvAdventureQuest: class EvAdventureQuest:
@ -40,11 +38,11 @@ class EvAdventureQuest:
start_step = "A" 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!" help_B = "Finally, you need more than 4 items in your inventory!"
def step_A(self, *args, **kwargs): 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.quester.msg("Completed the first step of the quest.")
self.current_step = "end" self.current_step = "end"
self.progress() self.progress()
@ -67,21 +65,16 @@ class EvAdventureQuest:
help_start = "You need to start first" help_start = "You need to start first"
help_end = "You need to end the quest" 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: if " " in self.key:
raise TypeError("The Quest name must not have spaces in it.") raise TypeError("The Quest name must not have spaces in it.")
self.quester = quester self.quester = quester
self._current_step = start_step or self.start_step self.data = data or dict()
self.is_completed = False self._current_step = self.get_data("current_step")
self.is_abandoned = False
def __serialize_dbobjs__(self): if not self.current_step:
self.quester = dbserialize.dbserialize(self.quester) self.current_step = self.start_step
def __deserialize_dbobjs__(self):
if isinstance(self.quester, bytes):
self.quester = dbserialize.dbunserialize(self.quester)
@property @property
def questhandler(self): def questhandler(self):
@ -94,14 +87,73 @@ class EvAdventureQuest:
@current_step.setter @current_step.setter
def current_step(self, step_name): def current_step(self, step_name):
self._current_step = step_name self._current_step = step_name
self.add_data("current_step", step_name)
self.questhandler.do_save = True 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): def abandon(self):
""" """
Call when quest is abandoned. Call when quest is abandoned.
""" """
self.is_abandoned = True self.add_data("status", QuestStatus.ABANDONED)
self.questhandler.clean_quest_data(self.key)
self.cleanup() self.cleanup()
def complete(self): def complete(self):
@ -109,7 +161,8 @@ class EvAdventureQuest:
Call this to end the quest. 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() self.cleanup()
def progress(self, *args, **kwargs): def progress(self, *args, **kwargs):
@ -122,8 +175,7 @@ class EvAdventureQuest:
*args, **kwargs: Will be passed into the step method. *args, **kwargs: Will be passed into the step method.
""" """
if not (self.is_completed or self.is_abandoned): return getattr(self, f"step_{self.current_step}")(*args, **kwargs)
getattr(self, f"step_{self.current_step}")(*args, **kwargs)
def help(self): def help(self):
""" """
@ -162,8 +214,9 @@ class EvAdventureQuest:
def cleanup(self): def cleanup(self):
""" """
This is called both when completing the quest, or when it is abandoned prematurely. 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 pass
@ -185,27 +238,84 @@ class EvAdventureQuestHandler:
quest_storage_attribute_key = "_quests" quest_storage_attribute_key = "_quests"
quest_storage_attribute_category = "evadventure" quest_storage_attribute_category = "evadventure"
quest_data_attribute_template = "_quest_data_{quest_key}"
quest_data_attribute_category = "evadventure"
def __init__(self, obj): def __init__(self, obj):
self.obj = obj self.obj = obj
self.do_save = False self.do_save = False
self.quests = {}
self.quest_classes = {}
self._load() self._load()
def _load(self): def _load(self):
self.storage = self.obj.attributes.get( self.quest_classes = self.obj.attributes.get(
self.quest_storage_attribute_key, self.quest_storage_attribute_key,
category=self.quest_storage_attribute_category, category=self.quest_storage_attribute_category,
default={}, 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): def _save(self):
self.obj.attributes.add( self.obj.attributes.add(
self.quest_storage_attribute_key, self.quest_storage_attribute_key,
self.storage, self.quest_classes,
category=self.quest_storage_attribute_category, category=self.quest_storage_attribute_category,
) )
self._load() # important self._load() # important
self.do_save = False 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): def has(self, quest_key):
""" """
Check if a given quest is registered with the Character. 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. 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): def get(self, quest_key):
""" """
@ -232,17 +342,17 @@ class EvAdventureQuestHandler:
Character is not on this quest. 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 Add a new quest
Args: 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() self._save()
def remove(self, quest_key): def remove(self, quest_key):
@ -253,10 +363,11 @@ class EvAdventureQuestHandler:
quest_key (str): The quest to remove. 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: if not quest.is_completed:
# make sure to cleanup # make sure to cleanup
quest.abandon() quest.abandon()
self.quest_classes.pop(quest_key, None)
self._save() self._save()
def get_help(self, quest_key=None): def get_help(self, quest_key=None):
@ -274,10 +385,10 @@ class EvAdventureQuestHandler:
""" """
help_texts = [] help_texts = []
if quest_key in self.storage: if quest_key in self.quests:
quests = [self.storage[quest_key]] quests = [self.quests[quest_key]]
else: else:
quests = self.storage.values() quests = self.quests.values()
for quest in quests: for quest in quests:
help_texts.append(f"|c{quest.key}|n\n {quest.desc}\n\n - {quest.help()}") 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. *args, **kwargs: Will be passed into each quest's `progress` call.
""" """
if quest_key in self.storage: if quest_key in self.quests:
quests = [self.storage[quest_key]] quests = [self.quests[quest_key]]
else: else:
quests = self.storage.values() quests = self.quests.values()
for quest in quests: for quest in quests:
quest.progress(*args, **kwargs) 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."]) self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - You need to do A first."])
# help for finished quest # help for finished quest
self._get_quest().is_completed = True self._get_quest().complete()
help_txt = self.character.quests.get_help() help_txt = self.character.quests.get_help()
self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - This quest is completed!"]) self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - This quest is completed!"])