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
272
tests/cookbook/lib/stat_bar.py
Normal file
272
tests/cookbook/lib/stat_bar.py
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
# McRogueFace Cookbook - Stat Bar Widget
|
||||
"""
|
||||
Horizontal bar showing current/max value with animation support.
|
||||
|
||||
Example:
|
||||
from lib.stat_bar import StatBar
|
||||
|
||||
hp_bar = StatBar(
|
||||
pos=(100, 50),
|
||||
size=(200, 20),
|
||||
current=75,
|
||||
maximum=100,
|
||||
fill_color=mcrfpy.Color(200, 50, 50), # Red for health
|
||||
label="HP"
|
||||
)
|
||||
scene.children.append(hp_bar.frame)
|
||||
|
||||
# Take damage
|
||||
hp_bar.set_value(50, animate=True)
|
||||
|
||||
# Flash on hit
|
||||
hp_bar.flash(mcrfpy.Color(255, 255, 255))
|
||||
"""
|
||||
import mcrfpy
|
||||
|
||||
|
||||
class StatBar:
|
||||
"""Horizontal bar showing current/max value.
|
||||
|
||||
Args:
|
||||
pos: (x, y) position tuple
|
||||
size: (width, height) tuple, default (200, 20)
|
||||
current: Current value (default: 100)
|
||||
maximum: Maximum value (default: 100)
|
||||
fill_color: Bar fill color (default: green)
|
||||
bg_color: Background color (default: dark gray)
|
||||
outline_color: Border color (default: white)
|
||||
outline: Border thickness (default: 1)
|
||||
label: Optional label prefix (e.g., "HP")
|
||||
show_text: Whether to show value text (default: True)
|
||||
text_color: Color of value text (default: white)
|
||||
font_size: Size of value text (default: 12)
|
||||
|
||||
Attributes:
|
||||
frame: The outer frame (add this to scene)
|
||||
bar: The inner fill bar
|
||||
current: Current value
|
||||
maximum: Maximum value
|
||||
"""
|
||||
|
||||
# Preset colors for common stat types
|
||||
HEALTH_COLOR = mcrfpy.Color(200, 50, 50) # Red
|
||||
MANA_COLOR = mcrfpy.Color(50, 100, 200) # Blue
|
||||
STAMINA_COLOR = mcrfpy.Color(50, 180, 80) # Green
|
||||
XP_COLOR = mcrfpy.Color(200, 180, 50) # Gold
|
||||
SHIELD_COLOR = mcrfpy.Color(100, 150, 200) # Light blue
|
||||
|
||||
DEFAULT_BG = mcrfpy.Color(30, 30, 35)
|
||||
DEFAULT_OUTLINE = mcrfpy.Color(150, 150, 160)
|
||||
DEFAULT_TEXT = mcrfpy.Color(255, 255, 255)
|
||||
|
||||
def __init__(self, pos, size=(200, 20), current=100, maximum=100,
|
||||
fill_color=None, bg_color=None, outline_color=None,
|
||||
outline=1, label=None, show_text=True,
|
||||
text_color=None, font_size=12):
|
||||
self.pos = pos
|
||||
self.size = size
|
||||
self._current = current
|
||||
self._maximum = maximum
|
||||
self.label = label
|
||||
self.show_text = show_text
|
||||
self.font_size = font_size
|
||||
|
||||
# Colors
|
||||
self.fill_color = fill_color or self.STAMINA_COLOR
|
||||
self.bg_color = bg_color or self.DEFAULT_BG
|
||||
self.text_color = text_color or self.DEFAULT_TEXT
|
||||
self.outline_color = outline_color or self.DEFAULT_OUTLINE
|
||||
|
||||
# Create outer frame (background)
|
||||
self.frame = mcrfpy.Frame(
|
||||
pos=pos,
|
||||
size=size,
|
||||
fill_color=self.bg_color,
|
||||
outline_color=self.outline_color,
|
||||
outline=outline
|
||||
)
|
||||
|
||||
# Create inner bar (the fill)
|
||||
bar_width = self._calculate_bar_width()
|
||||
self.bar = mcrfpy.Frame(
|
||||
pos=(0, 0),
|
||||
size=(bar_width, size[1]),
|
||||
fill_color=self.fill_color,
|
||||
outline=0
|
||||
)
|
||||
self.frame.children.append(self.bar)
|
||||
|
||||
# Create text label if needed
|
||||
if show_text:
|
||||
text = self._format_text()
|
||||
# Center text in the bar
|
||||
self.text = mcrfpy.Caption(
|
||||
text=text,
|
||||
pos=(size[0] / 2, (size[1] - font_size) / 2),
|
||||
fill_color=self.text_color,
|
||||
font_size=font_size
|
||||
)
|
||||
# Add outline for readability over bar
|
||||
self.text.outline = 1
|
||||
self.text.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
self.frame.children.append(self.text)
|
||||
else:
|
||||
self.text = None
|
||||
|
||||
def _calculate_bar_width(self):
|
||||
"""Calculate the fill bar width based on current/max ratio."""
|
||||
if self._maximum <= 0:
|
||||
return 0
|
||||
ratio = max(0, min(1, self._current / self._maximum))
|
||||
return ratio * self.size[0]
|
||||
|
||||
def _format_text(self):
|
||||
"""Format the display text."""
|
||||
if self.label:
|
||||
return f"{self.label}: {int(self._current)}/{int(self._maximum)}"
|
||||
return f"{int(self._current)}/{int(self._maximum)}"
|
||||
|
||||
def _update_display(self, animate=True):
|
||||
"""Update the bar width and text."""
|
||||
target_width = self._calculate_bar_width()
|
||||
|
||||
if animate:
|
||||
# Animate bar width change
|
||||
self.bar.animate("w", target_width, 0.3, mcrfpy.Easing.EASE_OUT)
|
||||
else:
|
||||
self.bar.w = target_width
|
||||
|
||||
# Update text
|
||||
if self.text:
|
||||
self.text.text = self._format_text()
|
||||
|
||||
@property
|
||||
def current(self):
|
||||
"""Current value."""
|
||||
return self._current
|
||||
|
||||
@current.setter
|
||||
def current(self, value):
|
||||
"""Set current value (no animation)."""
|
||||
self._current = max(0, min(value, self._maximum))
|
||||
self._update_display(animate=False)
|
||||
|
||||
@property
|
||||
def maximum(self):
|
||||
"""Maximum value."""
|
||||
return self._maximum
|
||||
|
||||
@maximum.setter
|
||||
def maximum(self, value):
|
||||
"""Set maximum value."""
|
||||
self._maximum = max(1, value)
|
||||
self._current = min(self._current, self._maximum)
|
||||
self._update_display(animate=False)
|
||||
|
||||
def set_value(self, current, maximum=None, animate=True):
|
||||
"""Set the bar value with optional animation.
|
||||
|
||||
Args:
|
||||
current: New current value
|
||||
maximum: New maximum value (optional)
|
||||
animate: Whether to animate the change
|
||||
"""
|
||||
if maximum is not None:
|
||||
self._maximum = max(1, maximum)
|
||||
self._current = max(0, min(current, self._maximum))
|
||||
self._update_display(animate=animate)
|
||||
|
||||
def flash(self, color=None, duration=0.2):
|
||||
"""Flash the bar a color (e.g., white on damage).
|
||||
|
||||
Args:
|
||||
color: Flash color (default: white)
|
||||
duration: Flash duration in seconds
|
||||
"""
|
||||
color = color or mcrfpy.Color(255, 255, 255)
|
||||
original_color = self.fill_color
|
||||
|
||||
# Flash to color
|
||||
self.bar.fill_color = color
|
||||
|
||||
# Create a timer to restore color
|
||||
# Using a closure to capture state
|
||||
bar_ref = self.bar
|
||||
restore_color = original_color
|
||||
|
||||
def restore(runtime):
|
||||
bar_ref.fill_color = restore_color
|
||||
|
||||
# Schedule restoration
|
||||
timer_name = f"flash_{id(self)}"
|
||||
mcrfpy.Timer(timer_name, restore, int(duration * 1000))
|
||||
|
||||
def set_colors(self, fill_color=None, bg_color=None, text_color=None):
|
||||
"""Update bar colors.
|
||||
|
||||
Args:
|
||||
fill_color: New fill color
|
||||
bg_color: New background color
|
||||
text_color: New text color
|
||||
"""
|
||||
if fill_color:
|
||||
self.fill_color = fill_color
|
||||
self.bar.fill_color = fill_color
|
||||
if bg_color:
|
||||
self.bg_color = bg_color
|
||||
self.frame.fill_color = bg_color
|
||||
if text_color and self.text:
|
||||
self.text_color = text_color
|
||||
self.text.fill_color = text_color
|
||||
|
||||
@property
|
||||
def ratio(self):
|
||||
"""Get current fill ratio (0.0 to 1.0)."""
|
||||
if self._maximum <= 0:
|
||||
return 0
|
||||
return self._current / self._maximum
|
||||
|
||||
def is_empty(self):
|
||||
"""Check if bar is empty (current <= 0)."""
|
||||
return self._current <= 0
|
||||
|
||||
def is_full(self):
|
||||
"""Check if bar is full (current >= maximum)."""
|
||||
return self._current >= self._maximum
|
||||
|
||||
|
||||
def create_stat_bar_group(stats, start_pos, spacing=30, size=(200, 20)):
|
||||
"""Create a vertical group of stat bars.
|
||||
|
||||
Args:
|
||||
stats: List of dicts with keys: name, current, max, color
|
||||
start_pos: (x, y) position of first bar
|
||||
spacing: Vertical pixels between bars
|
||||
size: (width, height) for all bars
|
||||
|
||||
Returns:
|
||||
Dict mapping stat names to StatBar objects
|
||||
|
||||
Example:
|
||||
bars = create_stat_bar_group([
|
||||
{"name": "HP", "current": 80, "max": 100, "color": StatBar.HEALTH_COLOR},
|
||||
{"name": "MP", "current": 50, "max": 80, "color": StatBar.MANA_COLOR},
|
||||
{"name": "XP", "current": 250, "max": 1000, "color": StatBar.XP_COLOR},
|
||||
], start_pos=(50, 50))
|
||||
"""
|
||||
bar_dict = {}
|
||||
x, y = start_pos
|
||||
|
||||
for stat in stats:
|
||||
bar = StatBar(
|
||||
pos=(x, y),
|
||||
size=size,
|
||||
current=stat.get("current", 100),
|
||||
maximum=stat.get("max", 100),
|
||||
fill_color=stat.get("color"),
|
||||
label=stat.get("name")
|
||||
)
|
||||
bar_dict[stat.get("name", f"stat_{len(bar_dict)}")] = bar
|
||||
y += size[1] + spacing
|
||||
|
||||
return bar_dict
|
||||
Loading…
Add table
Add a link
Reference in a new issue