#!/usr/bin/env python3 """Dialogue System - NPC dialogue with choices and mood Interactive controls: 1-4: Select dialogue choice Enter: Confirm selection Space: Advance dialogue (skip typewriter) R: Restart conversation ESC: Exit demo This demonstrates: - Portrait + text + choices pattern - State machine for NPC mood - Choice consequences - Dynamic UI updates """ import mcrfpy import sys import os # Add parent to path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from lib.text_box import DialogueBox from lib.choice_list import ChoiceList class NPC: """NPC with mood and dialogue state.""" MOODS = { "neutral": mcrfpy.Color(150, 150, 150), "happy": mcrfpy.Color(100, 200, 100), "angry": mcrfpy.Color(200, 80, 80), "sad": mcrfpy.Color(80, 80, 200), "suspicious": mcrfpy.Color(200, 180, 80), } def __init__(self, name, initial_mood="neutral"): self.name = name self.mood = initial_mood self.trust = 50 # 0-100 scale self.dialogue_state = "greeting" @property def mood_color(self): return self.MOODS.get(self.mood, self.MOODS["neutral"]) class DialogueSystem: """Complete dialogue system with NPC interaction.""" def __init__(self): self.scene = mcrfpy.Scene("dialogue_system") self.ui = self.scene.children # Create NPC self.npc = NPC("Elder Sage") # Dialogue tree self.dialogue_tree = self._create_dialogue_tree() self.setup() def _create_dialogue_tree(self): """Create the dialogue tree structure.""" return { "greeting": { "text": "Greetings, traveler. I am the Elder Sage of this village. What brings you to these remote lands?", "choices": [ ("I seek wisdom and knowledge.", "wise_response"), ("I'm looking for treasure!", "treasure_response"), ("None of your business.", "rude_response"), ("I'm lost. Can you help me?", "help_response"), ] }, "wise_response": { "text": "Ah, a seeker of truth! That is admirable. Knowledge is the greatest treasure one can possess. Tell me, what specific wisdom do you seek?", "mood": "happy", "trust_change": 10, "choices": [ ("I want to learn about the ancient prophecy.", "prophecy"), ("Teach me about magic.", "magic"), ("I wish to know the history of this land.", "history"), ] }, "treasure_response": { "text": "Treasure? Bah! Material wealth corrupts the soul. But... perhaps you could prove yourself worthy of learning where such things might be found.", "mood": "suspicious", "trust_change": -5, "choices": [ ("I apologize. I spoke hastily.", "apologize"), ("I don't need your approval!", "defiant"), ("What must I do to prove myself?", "prove_worthy"), ] }, "rude_response": { "text": "How dare you speak to me with such disrespect! Leave my presence at once!", "mood": "angry", "trust_change": -30, "choices": [ ("I'm sorry, I didn't mean that.", "apologize_angry"), ("Make me!", "defiant"), ("*Leave quietly*", "leave"), ] }, "help_response": { "text": "Lost? Poor soul. These mountains can be treacherous. I will help you find your way, but first, tell me where you wish to go.", "mood": "neutral", "trust_change": 5, "choices": [ ("To the nearest town.", "directions"), ("Anywhere but here.", "sad_path"), ("Actually, maybe I'll stay a while.", "stay"), ] }, "prophecy": { "text": "The ancient prophecy speaks of a chosen one who will restore balance when darkness falls. Many believe that time is now approaching...", "mood": "neutral", "choices": [ ("Am I the chosen one?", "chosen"), ("How can I help prevent this darkness?", "help_prevent"), ("That sounds like nonsense.", "skeptic"), ] }, "magic": { "text": "Magic is not learned from books alone. It flows from within, from understanding the natural world. Your journey has only begun.", "mood": "happy", "choices": [ ("Will you teach me?", "teach"), ("I understand. Thank you.", "thanks"), ] }, "history": { "text": "This land was once a great kingdom, until the Shadow Wars tore it asunder. Now only ruins remain of its former glory.", "mood": "sad", "choices": [ ("What caused the Shadow Wars?", "shadow_wars"), ("Can the kingdom be restored?", "restore"), ("Thank you for sharing.", "thanks"), ] }, "apologize": { "text": "Hmm... perhaps I misjudged you. True wisdom includes recognizing one's mistakes. Let us start again.", "mood": "neutral", "trust_change": 5, "choices": [ ("Thank you for understanding.", "wise_response"), ] }, "apologize_angry": { "text": "*sighs* Very well. I accept your apology. But mind your tongue in the future.", "mood": "neutral", "trust_change": -10, "choices": [ ("I will. Now, I seek wisdom.", "wise_response"), ("Thank you, Elder.", "thanks"), ] }, "defiant": { "text": "GUARDS! Remove this insolent fool from my sight!", "mood": "angry", "trust_change": -50, "choices": [ ("*Run away!*", "escape"), ("*Fight the guards*", "fight"), ] }, "prove_worthy": { "text": "There is a sacred trial in the mountains. Complete it, and I may reconsider my opinion of you.", "mood": "neutral", "trust_change": 5, "choices": [ ("I accept the challenge!", "accept_trial"), ("What kind of trial?", "trial_info"), ("Perhaps another time.", "decline"), ] }, "thanks": { "text": "You are welcome, traveler. May your journey be blessed with wisdom and good fortune. Return if you need guidance.", "mood": "happy", "choices": [ ("Farewell, Elder.", "end_good"), ("I have more questions.", "greeting"), ] }, "end_good": { "text": "Farewell. May the ancient spirits watch over you.", "mood": "happy", "choices": [ ("*Restart conversation*", "greeting"), ] }, "leave": { "text": "Perhaps it is for the best. Safe travels... if you can find your way.", "mood": "neutral", "choices": [ ("*Restart conversation*", "greeting"), ] }, "escape": { "text": "*You flee into the wilderness, the guards' shouts fading behind you...*", "mood": "neutral", "choices": [ ("*Restart conversation*", "greeting"), ] }, "fight": { "text": "*The guards overwhelm you. You wake up in a cell...*", "mood": "angry", "choices": [ ("*Restart conversation*", "greeting"), ] }, # Default responses for missing states "chosen": {"text": "That remains to be seen. Only time will tell.", "mood": "neutral", "choices": [("I understand.", "thanks")]}, "help_prevent": {"text": "Prepare yourself. Train hard. The darkness comes for us all.", "mood": "neutral", "choices": [("I will.", "thanks")]}, "skeptic": {"text": "Believe what you will. But when darkness comes, remember my words.", "mood": "sad", "choices": [("Perhaps you're right.", "apologize")]}, "teach": {"text": "In time, perhaps. First, prove your dedication.", "mood": "neutral", "choices": [("How?", "prove_worthy")]}, "shadow_wars": {"text": "Ancient evils that should have remained buried. Let us speak no more of it.", "mood": "sad", "choices": [("I understand.", "thanks")]}, "restore": {"text": "Perhaps... if the chosen one rises. Perhaps.", "mood": "neutral", "choices": [("I hope so.", "thanks")]}, "directions": {"text": "Head east through the mountain pass. The town of Millbrook lies two days' journey.", "mood": "neutral", "choices": [("Thank you!", "thanks")]}, "sad_path": {"text": "I sense great pain in you. Perhaps you should stay and heal.", "mood": "sad", "choices": [("You're right.", "stay")]}, "stay": {"text": "You are welcome here. Rest, and we shall talk more.", "mood": "happy", "choices": [("Thank you, Elder.", "thanks")]}, "accept_trial": {"text": "Brave soul! Seek the Cave of Trials to the north. Return victorious!", "mood": "happy", "choices": [("I will return!", "thanks")]}, "trial_info": {"text": "It tests courage, wisdom, and heart. Few have succeeded.", "mood": "neutral", "choices": [("I'll try anyway!", "accept_trial"), ("Maybe not...", "decline")]}, "decline": {"text": "Perhaps another time then. The offer stands.", "mood": "neutral", "choices": [("Thank you.", "thanks")]}, } def setup(self): """Build the dialogue UI.""" # Background bg = mcrfpy.Frame( pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(25, 25, 30) ) self.ui.append(bg) # Scene background (simple village scene) scene_bg = mcrfpy.Frame( pos=(0, 0), size=(1024, 400), fill_color=mcrfpy.Color(40, 60, 40) ) self.ui.append(scene_bg) # Ground ground = mcrfpy.Frame( pos=(0, 350), size=(1024, 50), fill_color=mcrfpy.Color(60, 45, 30) ) self.ui.append(ground) # Simple building shapes for i in range(3): building = mcrfpy.Frame( pos=(100 + i * 300, 200 - i * 20), size=(200, 150 + i * 20), fill_color=mcrfpy.Color(70 + i * 10, 60 + i * 5, 50), outline_color=mcrfpy.Color(40, 35, 30), outline=2 ) self.ui.append(building) # Portrait frame self.portrait_frame = mcrfpy.Frame( pos=(50, 420), size=(150, 180), fill_color=mcrfpy.Color(50, 50, 60), outline_color=mcrfpy.Color(100, 100, 120), outline=3 ) self.ui.append(self.portrait_frame) # NPC "face" (simple representation) face_bg = mcrfpy.Frame( pos=(15, 15), size=(120, 120), fill_color=mcrfpy.Color(200, 180, 160), outline=0 ) self.portrait_frame.children.append(face_bg) # Mood indicator (eyes/expression) self.mood_indicator = mcrfpy.Frame( pos=(35, 50), size=(80, 30), fill_color=self.npc.mood_color, outline=0 ) self.portrait_frame.children.append(self.mood_indicator) # Name label name_label = mcrfpy.Caption( text=self.npc.name, pos=(75, 150), font_size=14, fill_color=mcrfpy.Color(255, 255, 255) ) self.portrait_frame.children.append(name_label) # Dialogue box self.dialogue_box = DialogueBox( pos=(220, 420), size=(550, 180), speaker=self.npc.name, text="", chars_per_second=40 ) self.ui.append(self.dialogue_box.frame) # Choice list self.choice_list = ChoiceList( pos=(220, 610), size=(550, 120), choices=["Loading..."], on_select=self.on_choice, item_height=28 ) self.ui.append(self.choice_list.frame) # Trust meter trust_label = mcrfpy.Caption( text="Trust:", pos=(820, 430), font_size=14, fill_color=mcrfpy.Color(150, 150, 150) ) self.ui.append(trust_label) self.trust_bar_bg = mcrfpy.Frame( pos=(820, 450), size=(150, 20), fill_color=mcrfpy.Color(40, 40, 50), outline_color=mcrfpy.Color(80, 80, 100), outline=1 ) self.ui.append(self.trust_bar_bg) self.trust_bar = mcrfpy.Frame( pos=(0, 0), size=(75, 20), # 50% initial fill_color=mcrfpy.Color(100, 150, 100), outline=0 ) self.trust_bar_bg.children.append(self.trust_bar) self.trust_value = mcrfpy.Caption( text="50", pos=(895, 473), font_size=12, fill_color=mcrfpy.Color(200, 200, 200) ) self.ui.append(self.trust_value) # Mood display mood_label = mcrfpy.Caption( text="Mood:", pos=(820, 500), font_size=14, fill_color=mcrfpy.Color(150, 150, 150) ) self.ui.append(mood_label) self.mood_display = mcrfpy.Caption( text="Neutral", pos=(870, 500), font_size=14, fill_color=self.npc.mood_color ) self.ui.append(self.mood_display) # Instructions instr = mcrfpy.Caption( text="1-4: Select choice | Enter: Confirm | Space: Skip | R: Restart | ESC: Exit", pos=(50, 740), font_size=14, fill_color=mcrfpy.Color(100, 100, 100) ) self.ui.append(instr) # Start dialogue self._load_dialogue_state("greeting") def _load_dialogue_state(self, state_name): """Load a dialogue state.""" self.npc.dialogue_state = state_name if state_name not in self.dialogue_tree: state_name = "greeting" self.npc.dialogue_state = state_name state = self.dialogue_tree[state_name] # Apply mood change if "mood" in state: self.npc.mood = state["mood"] self._update_mood_display() # Apply trust change if "trust_change" in state: self.npc.trust = max(0, min(100, self.npc.trust + state["trust_change"])) self._update_trust_display() # Update dialogue self.dialogue_box.set_dialogue(self.npc.name, state["text"], animate=True) # Update choices choices = [choice[0] for choice in state.get("choices", [])] self._choice_targets = [choice[1] for choice in state.get("choices", [])] if choices: self.choice_list.choices = choices else: self.choice_list.choices = ["*Continue*"] self._choice_targets = ["greeting"] def _update_mood_display(self): """Update the mood indicators.""" self.mood_indicator.fill_color = self.npc.mood_color self.mood_display.text = self.npc.mood.capitalize() self.mood_display.fill_color = self.npc.mood_color def _update_trust_display(self): """Update the trust bar.""" bar_width = int((self.npc.trust / 100) * 150) self.trust_bar.w = bar_width self.trust_value.text = str(self.npc.trust) # Color based on trust level if self.npc.trust >= 70: self.trust_bar.fill_color = mcrfpy.Color(100, 200, 100) elif self.npc.trust >= 30: self.trust_bar.fill_color = mcrfpy.Color(200, 200, 100) else: self.trust_bar.fill_color = mcrfpy.Color(200, 100, 100) def on_choice(self, index, value): """Handle choice selection.""" if index < len(self._choice_targets): next_state = self._choice_targets[index] self._load_dialogue_state(next_state) def on_key(self, key, state): """Handle keyboard input.""" if state != "start": return if key == "Escape": sys.exit(0) elif key in ("Num1", "Num2", "Num3", "Num4"): idx = int(key[-1]) - 1 if idx < len(self.choice_list.choices): self.choice_list.set_selected(idx) self.choice_list.confirm() elif key == "Up": self.choice_list.navigate(-1) elif key == "Down": self.choice_list.navigate(1) elif key == "Enter": self.choice_list.confirm() elif key == "Space": self.dialogue_box.skip_animation() elif key == "R": # Restart self.npc.mood = "neutral" self.npc.trust = 50 self._update_mood_display() self._update_trust_display() self._load_dialogue_state("greeting") def activate(self): """Activate the dialogue scene.""" self.scene.on_key = self.on_key mcrfpy.current_scene = self.scene def main(): """Run the dialogue system demo.""" dialogue = DialogueSystem() dialogue.activate() # Headless mode: capture screenshot and exit try: if mcrfpy.headless_mode(): from mcrfpy import automation mcrfpy.Timer("screenshot", lambda rt: ( automation.screenshot("screenshots/apps/dialogue_system.png"), sys.exit(0) ), 200) except AttributeError: pass if __name__ == "__main__": main()