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>
320 lines
9 KiB
Python
320 lines
9 KiB
Python
#!/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()
|