scripts/quests.pyΒΆ
Part of Dungeon Explorer.
1"""Quest system: tracking, progression, and rewards."""
2
3from pathlib import Path
4
5
6class Quest:
7 """A single quest instance with progress tracking."""
8
9 def __init__(
10 self,
11 quest_id: str,
12 name: str,
13 quest_type: str,
14 target: str,
15 count: int,
16 rewards: dict,
17 min_floor: int = 1,
18 prerequisite_quest: str = "",
19 ):
20 self.quest_id = quest_id
21 self.name = name
22 self.quest_type = quest_type # "kill", "floor", "boss", "collect"
23 self.target = target
24 self.count = count
25 self.progress = 0
26 self.rewards = rewards
27 self.completed = False
28 self.min_floor = min_floor
29 self.prerequisite_quest = prerequisite_quest
30
31 @property
32 def is_complete(self) -> bool:
33 return self.progress >= self.count
34
35 def to_dict(self) -> dict:
36 return {
37 "quest_id": self.quest_id,
38 "name": self.name,
39 "quest_type": self.quest_type,
40 "target": self.target,
41 "count": self.count,
42 "progress": self.progress,
43 "rewards": self.rewards,
44 "completed": self.completed,
45 "min_floor": self.min_floor,
46 "prerequisite_quest": self.prerequisite_quest,
47 }
48
49 @classmethod
50 def from_dict(cls, d: dict) -> Quest:
51 q = cls(
52 d["quest_id"],
53 d["name"],
54 d["quest_type"],
55 d["target"],
56 d["count"],
57 d.get("rewards", {}),
58 d.get("min_floor", 1),
59 d.get("prerequisite_quest", ""),
60 )
61 q.progress = d.get("progress", 0)
62 q.completed = d.get("completed", False)
63 return q
64
65
66class QuestManager:
67 """Manages active, completed, and available quests."""
68
69 MAX_ACTIVE = 3
70
71 def __init__(self, quest_defs: list[dict] | None = None):
72 self._defs: list[dict] = quest_defs or []
73 self.active: list[Quest] = []
74 self.completed: list[str] = [] # Quest IDs
75
76 def load_from_toml(self, path: Path):
77 try:
78 import tomllib
79 except ImportError:
80 import tomli as tomllib
81 with open(path, "rb") as f:
82 data = tomllib.load(f)
83 self._defs = data.get("quests", [])
84
85 def available_quests(self, current_floor: int = 1) -> list[dict]:
86 active_ids = {q.quest_id for q in self.active}
87 result = []
88 for d in self._defs:
89 if d["id"] in active_ids or d["id"] in self.completed:
90 continue
91 if d.get("min_floor", 1) > current_floor:
92 continue
93 # Chain quest prerequisite check
94 prereq = d.get("prerequisite_quest", "")
95 if prereq and prereq not in self.completed:
96 continue
97 result.append(d)
98 return result
99
100 def accept_quest(self, quest_id: str) -> bool:
101 if len(self.active) >= self.MAX_ACTIVE:
102 return False
103 qdef = next((d for d in self._defs if d["id"] == quest_id), None)
104 if not qdef or quest_id in self.completed:
105 return False
106 if any(q.quest_id == quest_id for q in self.active):
107 return False
108 # Check prerequisite
109 prereq = qdef.get("prerequisite_quest", "")
110 if prereq and prereq not in self.completed:
111 return False
112 quest = Quest(
113 quest_id=qdef["id"],
114 name=qdef["name"],
115 quest_type=qdef["type"],
116 target=qdef.get("target", ""),
117 count=qdef["count"],
118 rewards=qdef.get("rewards", {}),
119 min_floor=qdef.get("min_floor", 1),
120 prerequisite_quest=prereq,
121 )
122 self.active.append(quest)
123 return True
124
125 def on_enemy_killed(self, archetype: str):
126 for quest in self.active:
127 if quest.completed:
128 continue
129 if quest.quest_type == "kill":
130 if quest.target == "" or quest.target == archetype:
131 quest.progress = min(quest.progress + 1, quest.count)
132
133 def on_floor_reached(self, floor: int):
134 for quest in self.active:
135 if quest.completed:
136 continue
137 if quest.quest_type == "floor" and floor >= quest.count:
138 quest.progress = quest.count
139
140 def on_item_collected(self, rarity_name: str):
141 """Track item collection for collect quests.
142
143 Args:
144 rarity_name: Lowercase rarity name (e.g. "uncommon", "rare", "epic", "legendary").
145 """
146 for quest in self.active:
147 if quest.completed:
148 continue
149 if quest.quest_type == "collect":
150 if quest.target == "" or quest.target == rarity_name.lower():
151 quest.progress = min(quest.progress + 1, quest.count)
152
153 def claim_rewards(self, quest_id: str) -> dict | None:
154 for i, quest in enumerate(self.active):
155 if quest.quest_id == quest_id and quest.is_complete:
156 quest.completed = True
157 self.completed.append(quest_id)
158 self.active.pop(i)
159 return quest.rewards
160 return None
161
162 def to_dict(self) -> dict:
163 return {
164 "active": [q.to_dict() for q in self.active],
165 "completed": list(self.completed),
166 }
167
168 def from_dict(self, d: dict):
169 self.active = [Quest.from_dict(qd) for qd in d.get("active", [])]
170 self.completed = d.get("completed", [])