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))