Cookbook structure: - lib/: Reusable component library (Button, StatBar, AnimationChain, etc.) - primitives/: Demo apps for individual components - features/: Demo apps for complex features (animation chaining, shaders) - apps/: Complete mini-applications (calculator, dialogue system) - automation/: Screenshot capture utilities API signature updates applied: - on_enter/on_exit/on_move callbacks now only receive (pos) per #230 - on_cell_enter/on_cell_exit callbacks only receive (cell_pos) per #230 - Animation chain library uses Timer-based sequencing (unaffected by #229) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2daebc84b5
commit
55f6ea9502
41 changed files with 8493 additions and 0 deletions
15
tests/cookbook/apps/__init__.py
Normal file
15
tests/cookbook/apps/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# McRogueFace Cookbook - Mini-Application Demos
|
||||
"""
|
||||
Complete mini-applications demonstrating real-world patterns.
|
||||
|
||||
Applications:
|
||||
calculator.py - Boss-key calculator with scene switching
|
||||
dialogue_system.py - NPC dialogue with choices and mood
|
||||
day_night_shadows.py - HeightMap shadow projection demo
|
||||
remote_control.py - FOV + entity perspective switching
|
||||
health_bar_hover.py - Hover-based health display
|
||||
toast_demo.py - Notification system demo
|
||||
banter_chat.py - Timed chat messages
|
||||
loot_detector.py - Directional arrow compass
|
||||
grid_puzzle.py - Interactive Sokoban-style puzzle
|
||||
"""
|
||||
320
tests/cookbook/apps/calculator.py
Normal file
320
tests/cookbook/apps/calculator.py
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Calculator App - Boss-key calculator with scene switching
|
||||
|
||||
Interactive controls:
|
||||
0-9: Number input
|
||||
+, -, *, /: Operations
|
||||
Enter/=: Calculate result
|
||||
C: Clear
|
||||
ESC: Toggle between game and calculator scenes
|
||||
Backspace: Delete last digit
|
||||
|
||||
This demonstrates:
|
||||
- Scene switching (boss key pattern)
|
||||
- Button grid layout
|
||||
- State management
|
||||
- Click handlers
|
||||
"""
|
||||
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.button import Button
|
||||
|
||||
|
||||
class Calculator:
|
||||
"""A simple calculator with GUI."""
|
||||
|
||||
def __init__(self):
|
||||
self.scene = mcrfpy.Scene("calculator")
|
||||
self.ui = self.scene.children
|
||||
self.expression = ""
|
||||
self.result = ""
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
"""Build the calculator UI."""
|
||||
# Background
|
||||
bg = mcrfpy.Frame(
|
||||
pos=(0, 0),
|
||||
size=(1024, 768),
|
||||
fill_color=mcrfpy.Color(30, 30, 35)
|
||||
)
|
||||
self.ui.append(bg)
|
||||
|
||||
# Calculator frame
|
||||
calc_frame = mcrfpy.Frame(
|
||||
pos=(312, 100),
|
||||
size=(400, 550),
|
||||
fill_color=mcrfpy.Color(40, 40, 50),
|
||||
outline_color=mcrfpy.Color(80, 80, 100),
|
||||
outline=2
|
||||
)
|
||||
self.ui.append(calc_frame)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(
|
||||
text="Calculator",
|
||||
pos=(512, 130),
|
||||
font_size=24,
|
||||
fill_color=mcrfpy.Color(200, 200, 200)
|
||||
)
|
||||
self.ui.append(title)
|
||||
|
||||
# Display frame
|
||||
display_bg = mcrfpy.Frame(
|
||||
pos=(332, 170),
|
||||
size=(360, 80),
|
||||
fill_color=mcrfpy.Color(20, 25, 30),
|
||||
outline_color=mcrfpy.Color(60, 60, 80),
|
||||
outline=1
|
||||
)
|
||||
self.ui.append(display_bg)
|
||||
|
||||
# Expression display
|
||||
self.expr_display = mcrfpy.Caption(
|
||||
text="",
|
||||
pos=(682, 180), # Right-aligned
|
||||
font_size=18,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(self.expr_display)
|
||||
|
||||
# Result display
|
||||
self.result_display = mcrfpy.Caption(
|
||||
text="0",
|
||||
pos=(682, 210), # Right-aligned
|
||||
font_size=32,
|
||||
fill_color=mcrfpy.Color(255, 255, 255)
|
||||
)
|
||||
self.ui.append(self.result_display)
|
||||
|
||||
# Button layout
|
||||
button_layout = [
|
||||
["C", "(", ")", "/"],
|
||||
["7", "8", "9", "*"],
|
||||
["4", "5", "6", "-"],
|
||||
["1", "2", "3", "+"],
|
||||
["0", ".", "DEL", "="],
|
||||
]
|
||||
|
||||
start_x = 342
|
||||
start_y = 270
|
||||
btn_width = 80
|
||||
btn_height = 50
|
||||
spacing = 8
|
||||
|
||||
for row_idx, row in enumerate(button_layout):
|
||||
for col_idx, label in enumerate(row):
|
||||
x = start_x + col_idx * (btn_width + spacing)
|
||||
y = start_y + row_idx * (btn_height + spacing)
|
||||
|
||||
# Different colors for different button types
|
||||
if label in "0123456789.":
|
||||
fill = mcrfpy.Color(60, 60, 70)
|
||||
hover = mcrfpy.Color(80, 80, 95)
|
||||
elif label == "=":
|
||||
fill = mcrfpy.Color(80, 120, 80)
|
||||
hover = mcrfpy.Color(100, 150, 100)
|
||||
elif label == "C":
|
||||
fill = mcrfpy.Color(120, 60, 60)
|
||||
hover = mcrfpy.Color(150, 80, 80)
|
||||
else:
|
||||
fill = mcrfpy.Color(70, 70, 90)
|
||||
hover = mcrfpy.Color(90, 90, 115)
|
||||
|
||||
btn = Button(
|
||||
label,
|
||||
pos=(x, y),
|
||||
size=(btn_width, btn_height),
|
||||
fill_color=fill,
|
||||
hover_color=hover,
|
||||
callback=lambda l=label: self.on_button(l),
|
||||
font_size=20
|
||||
)
|
||||
self.ui.append(btn.frame)
|
||||
|
||||
# Instructions
|
||||
instr = mcrfpy.Caption(
|
||||
text="Press ESC to switch to game | Click buttons or use keyboard",
|
||||
pos=(512, 580),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(100, 100, 100)
|
||||
)
|
||||
self.ui.append(instr)
|
||||
|
||||
def on_button(self, label):
|
||||
"""Handle button press."""
|
||||
if label == "C":
|
||||
self.expression = ""
|
||||
self.result = "0"
|
||||
elif label == "DEL":
|
||||
self.expression = self.expression[:-1]
|
||||
elif label == "=":
|
||||
self.calculate()
|
||||
else:
|
||||
self.expression += label
|
||||
|
||||
self._update_display()
|
||||
|
||||
def calculate(self):
|
||||
"""Evaluate the expression."""
|
||||
if not self.expression:
|
||||
return
|
||||
|
||||
try:
|
||||
# Safe evaluation (only math operations)
|
||||
allowed = set("0123456789+-*/.()")
|
||||
if all(c in allowed for c in self.expression):
|
||||
self.result = str(eval(self.expression))
|
||||
if self.result.endswith('.0'):
|
||||
self.result = self.result[:-2]
|
||||
else:
|
||||
self.result = "Error"
|
||||
except Exception:
|
||||
self.result = "Error"
|
||||
|
||||
def _update_display(self):
|
||||
"""Update the display captions."""
|
||||
self.expr_display.text = self.expression or ""
|
||||
self.result_display.text = self.result or "0"
|
||||
|
||||
def on_key(self, key, state):
|
||||
"""Handle keyboard input."""
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
# Switch to game scene
|
||||
mcrfpy.current_scene = game_scene
|
||||
return
|
||||
|
||||
# Map keys to buttons
|
||||
key_map = {
|
||||
"Num0": "0", "Num1": "1", "Num2": "2", "Num3": "3",
|
||||
"Num4": "4", "Num5": "5", "Num6": "6", "Num7": "7",
|
||||
"Num8": "8", "Num9": "9",
|
||||
"Period": ".", "Add": "+", "Subtract": "-",
|
||||
"Multiply": "*", "Divide": "/",
|
||||
"Enter": "=", "Return": "=",
|
||||
"C": "C", "Backspace": "DEL",
|
||||
"LParen": "(", "RParen": ")",
|
||||
}
|
||||
|
||||
if key in key_map:
|
||||
self.on_button(key_map[key])
|
||||
|
||||
def activate(self):
|
||||
"""Activate the calculator scene."""
|
||||
self.scene.on_key = self.on_key
|
||||
mcrfpy.current_scene = self.scene
|
||||
|
||||
|
||||
class GamePlaceholder:
|
||||
"""Placeholder game scene to switch to/from."""
|
||||
|
||||
def __init__(self):
|
||||
self.scene = mcrfpy.Scene("game_placeholder")
|
||||
self.ui = self.scene.children
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
"""Build the game placeholder UI."""
|
||||
# Background
|
||||
bg = mcrfpy.Frame(
|
||||
pos=(0, 0),
|
||||
size=(1024, 768),
|
||||
fill_color=mcrfpy.Color(20, 40, 20)
|
||||
)
|
||||
self.ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(
|
||||
text="GAME SCENE",
|
||||
pos=(512, 350),
|
||||
font_size=48,
|
||||
fill_color=mcrfpy.Color(100, 200, 100)
|
||||
)
|
||||
title.outline = 3
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(title)
|
||||
|
||||
# Subtitle
|
||||
subtitle = mcrfpy.Caption(
|
||||
text="Press ESC for Calculator (Boss Key!)",
|
||||
pos=(512, 420),
|
||||
font_size=20,
|
||||
fill_color=mcrfpy.Color(150, 200, 150)
|
||||
)
|
||||
self.ui.append(subtitle)
|
||||
|
||||
# Fake game elements
|
||||
for i in range(5):
|
||||
fake_element = mcrfpy.Frame(
|
||||
pos=(100 + i * 180, 550),
|
||||
size=(150, 100),
|
||||
fill_color=mcrfpy.Color(40 + i * 10, 60 + i * 5, 40),
|
||||
outline_color=mcrfpy.Color(80, 120, 80),
|
||||
outline=1
|
||||
)
|
||||
self.ui.append(fake_element)
|
||||
|
||||
label = mcrfpy.Caption(
|
||||
text=f"Game Element {i+1}",
|
||||
pos=(175 + i * 180, 590),
|
||||
font_size=12,
|
||||
fill_color=mcrfpy.Color(200, 200, 200)
|
||||
)
|
||||
self.ui.append(label)
|
||||
|
||||
def on_key(self, key, state):
|
||||
"""Handle keyboard input."""
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
# Switch to calculator
|
||||
calculator.activate()
|
||||
|
||||
def activate(self):
|
||||
"""Activate the game scene."""
|
||||
self.scene.on_key = self.on_key
|
||||
mcrfpy.current_scene = self.scene
|
||||
|
||||
|
||||
# Global instances
|
||||
calculator = None
|
||||
game_scene = None
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the calculator app."""
|
||||
global calculator, game_scene
|
||||
|
||||
calculator = Calculator()
|
||||
game_scene = GamePlaceholder()
|
||||
|
||||
# Start with the game scene
|
||||
game_scene.activate()
|
||||
|
||||
# Headless mode: capture screenshot and exit
|
||||
try:
|
||||
if mcrfpy.headless_mode():
|
||||
from mcrfpy import automation
|
||||
# Show calculator for screenshot
|
||||
calculator.activate()
|
||||
calculator.expression = "7+8"
|
||||
calculator._update_display()
|
||||
|
||||
mcrfpy.Timer("screenshot", lambda rt: (
|
||||
automation.screenshot("screenshots/apps/calculator.png"),
|
||||
sys.exit(0)
|
||||
), 100)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
497
tests/cookbook/apps/dialogue_system.py
Normal file
497
tests/cookbook/apps/dialogue_system.py
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue