scripts/tutorial.pyΒΆ

Part of Dungeon Explorer.

 1"""Tutorial system: contextual tooltips for first-time players."""
 2
 3
 4class TutorialManager:
 5    """Tracks which tutorial tips have been shown and provides contextual tooltips.
 6
 7    Integrates with the game by checking conditions each frame and returning
 8    the appropriate tooltip text. The game's HUD draws the tooltip.
 9    """
10
11    TIPS = {
12        "move": ("Use WASD or Arrow Keys to move", "move"),
13        "attack": ("Press SPACE to attack enemies", "first_enemy"),
14        "dodge": ("Press SHIFT to dodge roll through attacks", "first_damage"),
15        "interact": ("Press E to interact with stairs and NPCs", "first_stairs"),
16        "inventory": ("Press I to open your inventory", "first_item"),
17        "level_up": ("Press L to allocate stat points", "first_level_up"),
18        "skill_tree": ("Press K to view the skill tree", "first_skill_point"),
19        "quest": ("Press J to view your quest log", "first_quest"),
20        "shop": ("Press E near the dungeon entrance to begin your descent", "first_town"),
21        "hotbar": ("Press 1-4 to use abilities from the hotbar", "first_ability"),
22    }
23
24    def __init__(self):
25        self.seen: set[str] = set()
26        self._active_tip: str | None = None
27        self._tip_timer: float = 0.0
28        self._tip_duration: float = 5.0
29
30    @property
31    def tutorial_seen(self) -> bool:
32        """True if all basic tips have been shown."""
33        return len(self.seen) >= 5  # After 5 tips, consider tutorial "done"
34
35    def mark_seen(self, tip_key: str):
36        """Mark a tutorial tip as seen."""
37        self.seen.add(tip_key)
38
39    def trigger(self, condition: str) -> str | None:
40        """Check if a condition triggers a tutorial tip. Returns tip text or None."""
41        for key, (text, cond) in self.TIPS.items():
42            if cond == condition and key not in self.seen:
43                self.seen.add(key)
44                self._active_tip = text
45                self._tip_timer = self._tip_duration
46                return text
47        return None
48
49    def update(self, dt: float):
50        """Update tip display timer."""
51        if self._tip_timer > 0:
52            self._tip_timer -= dt
53            if self._tip_timer <= 0:
54                self._active_tip = None
55
56    @property
57    def current_tip(self) -> str | None:
58        """Currently displayed tip text, or None."""
59        return self._active_tip if self._tip_timer > 0 else None
60
61    @property
62    def tip_alpha(self) -> float:
63        """Alpha for fading the tip in/out."""
64        if self._tip_timer <= 0:
65            return 0.0
66        if self._tip_timer < 1.0:
67            return self._tip_timer  # Fade out
68        if self._tip_timer > self._tip_duration - 0.5:
69            return (self._tip_duration - self._tip_timer) * 2.0  # Fade in
70        return 1.0
71
72    def to_dict(self) -> dict:
73        """Serialise for save."""
74        return {"seen": list(self.seen)}
75
76    def from_dict(self, data: dict):
77        """Restore from save."""
78        self.seen = set(data.get("seen", []))
79
80    def draw_tip(self, renderer, sw: float, sh: float):
81        """Draw the current tooltip if active."""
82        tip = self.current_tip
83        if tip is None:
84            return
85        alpha = self.tip_alpha
86        # Draw centred near the bottom
87        box_w = min(len(tip) * 9 + 30, 500)
88        box_x = (sw - box_w) / 2
89        box_y = sh - 100
90        renderer.draw_rect((box_x, box_y), (box_w, 32), colour=(0.1, 0.1, 0.15, 0.85 * alpha), filled=True)
91        renderer.draw_rect((box_x, box_y), (box_w, 2), colour=(0.4, 0.6, 0.9, 0.6 * alpha), filled=True)
92        renderer.draw_text(tip, (box_x + 12, box_y + 8), scale=1.0, colour=(0.9, 0.9, 1.0, alpha))