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", [])