draft tutorial revisions

This commit is contained in:
John McCardle 2026-01-03 11:01:10 -05:00
commit 48359b5a48
70 changed files with 6216 additions and 28 deletions

View file

@ -0,0 +1,13 @@
"""McRogueFace - Animated Movement (basic)
Documentation: https://mcrogueface.github.io/cookbook/combat_animated_movement
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_animated_movement_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
if new_x != current_x:
anim = mcrfpy.Animation("x", float(new_x), duration, "easeInOut", callback=done)
else:
anim = mcrfpy.Animation("y", float(new_y), duration, "easeInOut", callback=done)

View file

@ -0,0 +1,12 @@
"""McRogueFace - Animated Movement (basic_2)
Documentation: https://mcrogueface.github.io/cookbook/combat_animated_movement
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_animated_movement_basic_2.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
current_anim = mcrfpy.Animation("x", 100.0, 0.5, "linear")
current_anim.start(entity)
# Later: current_anim = None # Let it complete or create new one

View file

@ -0,0 +1,45 @@
"""McRogueFace - Basic Enemy AI (basic)
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import random
def wander(enemy, grid):
"""Move randomly to an adjacent walkable tile."""
ex, ey = int(enemy.x), int(enemy.y)
# Get valid adjacent tiles
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
random.shuffle(directions)
for dx, dy in directions:
new_x, new_y = ex + dx, ey + dy
if is_walkable(grid, new_x, new_y) and not is_occupied(new_x, new_y):
enemy.x = new_x
enemy.y = new_y
return
# No valid moves - stay in place
def is_walkable(grid, x, y):
"""Check if a tile can be walked on."""
grid_w, grid_h = grid.grid_size
if x < 0 or x >= grid_w or y < 0 or y >= grid_h:
return False
return grid.at(x, y).walkable
def is_occupied(x, y, entities=None):
"""Check if a tile is occupied by another entity."""
if entities is None:
return False
for entity in entities:
if int(entity.x) == x and int(entity.y) == y:
return True
return False

View file

@ -0,0 +1,11 @@
"""McRogueFace - Basic Enemy AI (multi)
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
# Filter to cardinal directions only
path = [p for p in path if abs(p[0] - ex) + abs(p[1] - ey) == 1]

View file

@ -0,0 +1,14 @@
"""McRogueFace - Basic Enemy AI (multi_2)
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_multi_2.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def alert_nearby(x, y, radius, enemies):
for enemy in enemies:
dist = abs(enemy.entity.x - x) + abs(enemy.entity.y - y)
if dist <= radius and hasattr(enemy.ai, 'alert'):
enemy.ai.alert = True

View file

@ -0,0 +1,82 @@
"""McRogueFace - Melee Combat System (basic)
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class CombatLog:
"""Scrolling combat message log."""
def __init__(self, x, y, width, height, max_messages=10):
self.x = x
self.y = y
self.width = width
self.height = height
self.max_messages = max_messages
self.messages = []
self.captions = []
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
# Background
self.frame = mcrfpy.Frame(x, y, width, height)
self.frame.fill_color = mcrfpy.Color(0, 0, 0, 180)
ui.append(self.frame)
def add_message(self, text, color=None):
"""Add a message to the log."""
if color is None:
color = mcrfpy.Color(200, 200, 200)
self.messages.append((text, color))
# Keep only recent messages
if len(self.messages) > self.max_messages:
self.messages.pop(0)
self._refresh_display()
def _refresh_display(self):
"""Redraw all messages."""
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
# Remove old captions
for caption in self.captions:
try:
ui.remove(caption)
except:
pass
self.captions.clear()
# Create new captions
line_height = 18
for i, (text, color) in enumerate(self.messages):
caption = mcrfpy.Caption(text, self.x + 5, self.y + 5 + i * line_height)
caption.fill_color = color
ui.append(caption)
self.captions.append(caption)
def log_attack(self, attacker_name, defender_name, damage, killed=False, critical=False):
"""Log an attack event."""
if critical:
text = f"{attacker_name} CRITS {defender_name} for {damage}!"
color = mcrfpy.Color(255, 255, 0)
else:
text = f"{attacker_name} hits {defender_name} for {damage}."
color = mcrfpy.Color(200, 200, 200)
self.add_message(text, color)
if killed:
self.add_message(f"{defender_name} is defeated!", mcrfpy.Color(255, 100, 100))
# Global combat log
combat_log = None
def init_combat_log():
global combat_log
combat_log = CombatLog(10, 500, 400, 200)

View file

@ -0,0 +1,15 @@
"""McRogueFace - Melee Combat System (complete)
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_complete.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def die_with_animation(entity):
# Play death animation
anim = mcrfpy.Animation("opacity", 0.0, 0.5, "linear")
anim.start(entity)
# Remove after animation
mcrfpy.setTimer("remove", lambda dt: remove_entity(entity), 500)

View file

@ -0,0 +1,14 @@
"""McRogueFace - Melee Combat System (complete_2)
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_complete_2.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
@dataclass
class AdvancedFighter(Fighter):
fire_resist: float = 0.0
ice_resist: float = 0.0
physical_resist: float = 0.0

View file

@ -0,0 +1,56 @@
"""McRogueFace - Status Effects (basic)
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class StackableEffect(StatusEffect):
"""Effect that stacks intensity."""
def __init__(self, name, duration, intensity=1, max_stacks=5, **kwargs):
super().__init__(name, duration, **kwargs)
self.intensity = intensity
self.max_stacks = max_stacks
self.stacks = 1
def add_stack(self):
"""Add another stack."""
if self.stacks < self.max_stacks:
self.stacks += 1
return True
return False
class StackingEffectManager(EffectManager):
"""Effect manager with stacking support."""
def add_effect(self, effect):
if isinstance(effect, StackableEffect):
# Check for existing stacks
for existing in self.effects:
if existing.name == effect.name:
if existing.add_stack():
# Refresh duration
existing.duration = max(existing.duration, effect.duration)
return
else:
return # Max stacks
# Default behavior
super().add_effect(effect)
# Stacking poison example
def create_stacking_poison(base_damage=1, duration=5):
def on_tick(target):
# Find the poison effect to get stack count
effect = target.effects.get_effect("poison")
if effect:
damage = base_damage * effect.stacks
target.hp -= damage
print(f"{target.name} takes {damage} poison damage! ({effect.stacks} stacks)")
return StackableEffect("poison", duration, on_tick=on_tick, max_stacks=5)

View file

@ -0,0 +1,16 @@
"""McRogueFace - Status Effects (basic_2)
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic_2.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def apply_effect(self, effect):
if effect.name in self.immunities:
print(f"{self.name} is immune to {effect.name}!")
return
if effect.name in self.resistances:
effect.duration //= 2 # Half duration
self.effects.add_effect(effect)

View file

@ -0,0 +1,12 @@
"""McRogueFace - Status Effects (basic_3)
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic_3.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def serialize_effects(effect_manager):
return [{"name": e.name, "duration": e.duration}
for e in effect_manager.effects]

View file

@ -0,0 +1,45 @@
"""McRogueFace - Turn-Based Game Loop (combat_turn_system)
Documentation: https://mcrogueface.github.io/cookbook/combat_turn_system
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_turn_system.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def create_turn_order_ui(turn_manager, x=800, y=50):
"""Create a visual turn order display."""
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
# Background frame
frame = mcrfpy.Frame(x, y, 200, 300)
frame.fill_color = mcrfpy.Color(30, 30, 30, 200)
frame.outline = 2
frame.outline_color = mcrfpy.Color(100, 100, 100)
ui.append(frame)
# Title
title = mcrfpy.Caption("Turn Order", x + 10, y + 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
return frame
def update_turn_order_display(frame, turn_manager, x=800, y=50):
"""Update the turn order display."""
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
# Clear old entries (keep frame and title)
# In practice, store references to caption objects and update them
for i, actor_data in enumerate(turn_manager.actors):
actor = actor_data["actor"]
is_current = (i == turn_manager.current)
# Actor name/type
name = getattr(actor, 'name', f"Actor {i}")
color = mcrfpy.Color(255, 255, 0) if is_current else mcrfpy.Color(200, 200, 200)
caption = mcrfpy.Caption(name, x + 10, y + 40 + i * 25)
caption.fill_color = color
ui.append(caption)

View file

@ -0,0 +1,118 @@
"""McRogueFace - Color Pulse Effect (basic)
Documentation: https://mcrogueface.github.io/cookbook/effects_color_pulse
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_color_pulse_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class PulsingCell:
"""A cell that continuously pulses until stopped."""
def __init__(self, grid, x, y, color, period=1.0, max_alpha=180):
"""
Args:
grid: Grid with color layer
x, y: Cell position
color: RGB tuple
period: Time for one complete pulse cycle
max_alpha: Maximum alpha value (0-255)
"""
self.grid = grid
self.x = x
self.y = y
self.color = color
self.period = period
self.max_alpha = max_alpha
self.is_pulsing = False
self.pulse_id = 0
self.cell = None
self._setup_layer()
def _setup_layer(self):
"""Ensure color layer exists and get cell reference."""
color_layer = None
for layer in self.grid.layers:
if isinstance(layer, mcrfpy.ColorLayer):
color_layer = layer
break
if not color_layer:
self.grid.add_layer("color")
color_layer = self.grid.layers[-1]
self.cell = color_layer.at(self.x, self.y)
if self.cell:
self.cell.color = mcrfpy.Color(self.color[0], self.color[1],
self.color[2], 0)
def start(self):
"""Start continuous pulsing."""
if self.is_pulsing or not self.cell:
return
self.is_pulsing = True
self.pulse_id += 1
self._pulse_up()
def _pulse_up(self):
"""Animate alpha increasing."""
if not self.is_pulsing:
return
current_id = self.pulse_id
half_period = self.period / 2
anim = mcrfpy.Animation("a", float(self.max_alpha), half_period, "easeInOut")
anim.start(self.cell.color)
def next_phase(timer_name):
if self.is_pulsing and self.pulse_id == current_id:
self._pulse_down()
mcrfpy.Timer(f"pulse_up_{id(self)}_{current_id}",
next_phase, int(half_period * 1000), once=True)
def _pulse_down(self):
"""Animate alpha decreasing."""
if not self.is_pulsing:
return
current_id = self.pulse_id
half_period = self.period / 2
anim = mcrfpy.Animation("a", 0.0, half_period, "easeInOut")
anim.start(self.cell.color)
def next_phase(timer_name):
if self.is_pulsing and self.pulse_id == current_id:
self._pulse_up()
mcrfpy.Timer(f"pulse_down_{id(self)}_{current_id}",
next_phase, int(half_period * 1000), once=True)
def stop(self):
"""Stop pulsing and fade out."""
self.is_pulsing = False
if self.cell:
anim = mcrfpy.Animation("a", 0.0, 0.2, "easeOut")
anim.start(self.cell.color)
def set_color(self, color):
"""Change pulse color."""
self.color = color
if self.cell:
current_alpha = self.cell.color.a
self.cell.color = mcrfpy.Color(color[0], color[1], color[2], current_alpha)
# Usage
objective_pulse = PulsingCell(grid, 10, 10, (0, 255, 100), period=1.5)
objective_pulse.start()
# Later, when objective is reached:
objective_pulse.stop()

View file

@ -0,0 +1,61 @@
"""McRogueFace - Color Pulse Effect (multi)
Documentation: https://mcrogueface.github.io/cookbook/effects_color_pulse
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_color_pulse_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
def ripple_effect(grid, center_x, center_y, color, max_radius=5, duration=1.0):
"""
Create an expanding ripple effect.
Args:
grid: Grid with color layer
center_x, center_y: Ripple origin
color: RGB tuple
max_radius: Maximum ripple size
duration: Total animation time
"""
# Get color layer
color_layer = None
for layer in grid.layers:
if isinstance(layer, mcrfpy.ColorLayer):
color_layer = layer
break
if not color_layer:
grid.add_layer("color")
color_layer = grid.layers[-1]
step_duration = duration / max_radius
for radius in range(max_radius + 1):
# Get cells at this radius (ring, not filled)
ring_cells = []
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
dist_sq = dx * dx + dy * dy
# Include cells approximately on the ring edge
if radius * radius - radius <= dist_sq <= radius * radius + radius:
cell = color_layer.at(center_x + dx, center_y + dy)
if cell:
ring_cells.append(cell)
# Schedule this ring to animate
def animate_ring(timer_name, cells=ring_cells, c=color):
for cell in cells:
cell.color = mcrfpy.Color(c[0], c[1], c[2], 200)
# Fade out
anim = mcrfpy.Animation("a", 0.0, step_duration * 2, "easeOut")
anim.start(cell.color)
delay = int(radius * step_duration * 1000)
mcrfpy.Timer(f"ripple_{radius}", animate_ring, delay, once=True)
# Usage
ripple_effect(grid, 10, 10, (100, 200, 255), max_radius=6, duration=0.8)

View file

@ -0,0 +1,41 @@
"""McRogueFace - Damage Flash Effect (basic)
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Add a color layer to your grid (do this once during setup)
grid.add_layer("color")
color_layer = grid.layers[-1] # Get the color layer
def flash_cell(grid, x, y, color, duration=0.3):
"""Flash a grid cell with a color overlay."""
# Get the color layer (assumes it's the last layer added)
color_layer = None
for layer in grid.layers:
if isinstance(layer, mcrfpy.ColorLayer):
color_layer = layer
break
if not color_layer:
return
# Set cell to flash color
cell = color_layer.at(x, y)
cell.color = mcrfpy.Color(color[0], color[1], color[2], 200)
# Animate alpha back to 0
anim = mcrfpy.Animation("a", 0.0, duration, "easeOut")
anim.start(cell.color)
def damage_at_position(grid, x, y, duration=0.3):
"""Flash red at a grid position when damage occurs."""
flash_cell(grid, x, y, (255, 0, 0), duration)
# Usage when entity takes damage
damage_at_position(grid, int(enemy.x), int(enemy.y))

View file

@ -0,0 +1,85 @@
"""McRogueFace - Damage Flash Effect (complete)
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_complete.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class DamageEffects:
"""Manages visual damage feedback effects."""
# Color presets
DAMAGE_RED = (255, 50, 50)
HEAL_GREEN = (50, 255, 50)
POISON_PURPLE = (150, 50, 200)
FIRE_ORANGE = (255, 150, 50)
ICE_BLUE = (100, 200, 255)
def __init__(self, grid):
self.grid = grid
self.color_layer = None
self._setup_color_layer()
def _setup_color_layer(self):
"""Ensure grid has a color layer for effects."""
self.grid.add_layer("color")
self.color_layer = self.grid.layers[-1]
def flash_entity(self, entity, color, duration=0.3):
"""Flash an entity with a color tint."""
# Flash at entity's grid position
x, y = int(entity.x), int(entity.y)
self.flash_cell(x, y, color, duration)
def flash_cell(self, x, y, color, duration=0.3):
"""Flash a specific grid cell."""
if not self.color_layer:
return
cell = self.color_layer.at(x, y)
if cell:
cell.color = mcrfpy.Color(color[0], color[1], color[2], 180)
# Fade out
anim = mcrfpy.Animation("a", 0.0, duration, "easeOut")
anim.start(cell.color)
def damage(self, entity, amount, duration=0.3):
"""Standard damage flash."""
self.flash_entity(entity, self.DAMAGE_RED, duration)
def heal(self, entity, amount, duration=0.4):
"""Healing effect - green flash."""
self.flash_entity(entity, self.HEAL_GREEN, duration)
def poison(self, entity, duration=0.5):
"""Poison damage - purple flash."""
self.flash_entity(entity, self.POISON_PURPLE, duration)
def fire(self, entity, duration=0.3):
"""Fire damage - orange flash."""
self.flash_entity(entity, self.FIRE_ORANGE, duration)
def ice(self, entity, duration=0.4):
"""Ice damage - blue flash."""
self.flash_entity(entity, self.ICE_BLUE, duration)
def area_damage(self, center_x, center_y, radius, color, duration=0.4):
"""Flash all cells in a radius."""
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
if dx * dx + dy * dy <= radius * radius:
self.flash_cell(center_x + dx, center_y + dy, color, duration)
# Setup
effects = DamageEffects(grid)
# Usage examples
effects.damage(player, 10) # Red flash
effects.heal(player, 5) # Green flash
effects.poison(enemy) # Purple flash
effects.area_damage(5, 5, 3, effects.FIRE_ORANGE) # Area effect

View file

@ -0,0 +1,25 @@
"""McRogueFace - Damage Flash Effect (multi)
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
def multi_flash(grid, x, y, color, flashes=3, flash_duration=0.1):
"""Flash a cell multiple times for emphasis."""
delay = 0
for i in range(flashes):
# Schedule each flash with increasing delay
def do_flash(timer_name, fx=x, fy=y, fc=color, fd=flash_duration):
flash_cell(grid, fx, fy, fc, fd)
mcrfpy.Timer(f"flash_{x}_{y}_{i}", do_flash, int(delay * 1000), once=True)
delay += flash_duration * 1.5 # Gap between flashes
# Usage for critical hit
multi_flash(grid, int(enemy.x), int(enemy.y), (255, 255, 0), flashes=3)

View file

@ -0,0 +1,42 @@
"""McRogueFace - Floating Damage Numbers (effects_floating_text)
Documentation: https://mcrogueface.github.io/cookbook/effects_floating_text
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_floating_text.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class StackedFloatingText:
"""Prevents overlapping text by stacking vertically."""
def __init__(self, scene_name, grid=None):
self.manager = FloatingTextManager(scene_name, grid)
self.position_stack = {} # Track recent spawns per position
def spawn_stacked(self, x, y, text, color, **kwargs):
"""Spawn with automatic vertical stacking."""
key = (int(x), int(y))
# Calculate offset based on recent spawns at this position
offset = self.position_stack.get(key, 0)
actual_y = y - (offset * 20) # 20 pixels between stacked texts
self.manager.spawn(x, actual_y, text, color, **kwargs)
# Increment stack counter
self.position_stack[key] = offset + 1
# Reset stack after delay
def reset_stack(timer_name, k=key):
if k in self.position_stack:
self.position_stack[k] = max(0, self.position_stack[k] - 1)
mcrfpy.Timer(f"stack_reset_{x}_{y}_{offset}", reset_stack, 300, once=True)
# Usage
stacked = StackedFloatingText("game", grid)
# Rapid hits will stack vertically instead of overlapping
stacked.spawn_stacked(5, 5, "-10", (255, 0, 0), is_grid_pos=True)
stacked.spawn_stacked(5, 5, "-8", (255, 0, 0), is_grid_pos=True)
stacked.spawn_stacked(5, 5, "-12", (255, 0, 0), is_grid_pos=True)

View file

@ -0,0 +1,65 @@
"""McRogueFace - Path Animation (Multi-Step Movement) (effects_path_animation)
Documentation: https://mcrogueface.github.io/cookbook/effects_path_animation
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_path_animation.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class CameraFollowingPath:
"""Path animator that also moves the camera."""
def __init__(self, entity, grid, path, step_duration=0.2):
self.entity = entity
self.grid = grid
self.path = path
self.step_duration = step_duration
self.index = 0
self.on_complete = None
def start(self):
self.index = 0
self._next()
def _next(self):
if self.index >= len(self.path):
if self.on_complete:
self.on_complete(self)
return
x, y = self.path[self.index]
def done(anim, target):
self.index += 1
self._next()
# Animate entity
if self.entity.x != x:
anim = mcrfpy.Animation("x", float(x), self.step_duration,
"easeInOut", callback=done)
anim.start(self.entity)
elif self.entity.y != y:
anim = mcrfpy.Animation("y", float(y), self.step_duration,
"easeInOut", callback=done)
anim.start(self.entity)
else:
done(None, None)
return
# Animate camera to follow
cam_x = mcrfpy.Animation("center_x", (x + 0.5) * 16,
self.step_duration, "easeInOut")
cam_y = mcrfpy.Animation("center_y", (y + 0.5) * 16,
self.step_duration, "easeInOut")
cam_x.start(self.grid)
cam_y.start(self.grid)
# Usage
path = [(5, 5), (5, 10), (10, 10)]
mover = CameraFollowingPath(player, grid, path)
mover.on_complete = lambda m: print("Journey complete!")
mover.start()

View file

@ -0,0 +1,166 @@
"""McRogueFace - Scene Transition Effects (effects_scene_transitions)
Documentation: https://mcrogueface.github.io/cookbook/effects_scene_transitions
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_scene_transitions.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class TransitionManager:
"""Manages scene transitions with multiple effect types."""
def __init__(self, screen_width=1024, screen_height=768):
self.width = screen_width
self.height = screen_height
self.is_transitioning = False
def go_to(self, scene_name, effect="fade", duration=0.5, **kwargs):
"""
Transition to a scene with the specified effect.
Args:
scene_name: Target scene
effect: "fade", "flash", "wipe", "instant"
duration: Transition duration
**kwargs: Effect-specific options (color, direction)
"""
if self.is_transitioning:
return
self.is_transitioning = True
if effect == "instant":
mcrfpy.setScene(scene_name)
self.is_transitioning = False
elif effect == "fade":
color = kwargs.get("color", (0, 0, 0))
self._fade(scene_name, duration, color)
elif effect == "flash":
color = kwargs.get("color", (255, 255, 255))
self._flash(scene_name, duration, color)
elif effect == "wipe":
direction = kwargs.get("direction", "right")
color = kwargs.get("color", (0, 0, 0))
self._wipe(scene_name, duration, direction, color)
def _fade(self, scene, duration, color):
half = duration / 2
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
overlay = mcrfpy.Frame(0, 0, self.width, self.height)
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0)
overlay.z_index = 9999
ui.append(overlay)
anim = mcrfpy.Animation("opacity", 1.0, half, "easeIn")
anim.start(overlay)
def phase2(timer_name):
mcrfpy.setScene(scene)
new_ui = mcrfpy.sceneUI(scene)
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
new_overlay.z_index = 9999
new_ui.append(new_overlay)
anim2 = mcrfpy.Animation("opacity", 0.0, half, "easeOut")
anim2.start(new_overlay)
def cleanup(timer_name):
for i, elem in enumerate(new_ui):
if elem is new_overlay:
new_ui.remove(i)
break
self.is_transitioning = False
mcrfpy.Timer("fade_done", cleanup, int(half * 1000) + 50, once=True)
mcrfpy.Timer("fade_switch", phase2, int(half * 1000), once=True)
def _flash(self, scene, duration, color):
quarter = duration / 4
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
overlay = mcrfpy.Frame(0, 0, self.width, self.height)
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0)
overlay.z_index = 9999
ui.append(overlay)
anim = mcrfpy.Animation("opacity", 1.0, quarter, "easeOut")
anim.start(overlay)
def phase2(timer_name):
mcrfpy.setScene(scene)
new_ui = mcrfpy.sceneUI(scene)
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
new_overlay.z_index = 9999
new_ui.append(new_overlay)
anim2 = mcrfpy.Animation("opacity", 0.0, duration / 2, "easeIn")
anim2.start(new_overlay)
def cleanup(timer_name):
for i, elem in enumerate(new_ui):
if elem is new_overlay:
new_ui.remove(i)
break
self.is_transitioning = False
mcrfpy.Timer("flash_done", cleanup, int(duration * 500) + 50, once=True)
mcrfpy.Timer("flash_switch", phase2, int(quarter * 2000), once=True)
def _wipe(self, scene, duration, direction, color):
# Simplified wipe - right direction only for brevity
half = duration / 2
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
overlay = mcrfpy.Frame(0, 0, 0, self.height)
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
overlay.z_index = 9999
ui.append(overlay)
anim = mcrfpy.Animation("w", float(self.width), half, "easeInOut")
anim.start(overlay)
def phase2(timer_name):
mcrfpy.setScene(scene)
new_ui = mcrfpy.sceneUI(scene)
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
new_overlay.z_index = 9999
new_ui.append(new_overlay)
anim2 = mcrfpy.Animation("x", float(self.width), half, "easeInOut")
anim2.start(new_overlay)
def cleanup(timer_name):
for i, elem in enumerate(new_ui):
if elem is new_overlay:
new_ui.remove(i)
break
self.is_transitioning = False
mcrfpy.Timer("wipe_done", cleanup, int(half * 1000) + 50, once=True)
mcrfpy.Timer("wipe_switch", phase2, int(half * 1000), once=True)
# Usage
transitions = TransitionManager()
# Various transition styles
transitions.go_to("game", effect="fade", duration=0.5)
transitions.go_to("menu", effect="flash", color=(255, 255, 255), duration=0.4)
transitions.go_to("next_level", effect="wipe", direction="right", duration=0.6)
transitions.go_to("options", effect="instant")

View file

@ -0,0 +1,38 @@
"""McRogueFace - Screen Shake Effect (basic)
Documentation: https://mcrogueface.github.io/cookbook/effects_screen_shake
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_screen_shake_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
def screen_shake(frame, intensity=5, duration=0.2):
"""
Shake a frame/container by animating its position.
Args:
frame: The UI Frame to shake (often a container for all game elements)
intensity: Maximum pixel offset
duration: Total shake duration in seconds
"""
original_x = frame.x
original_y = frame.y
# Quick shake to offset position
shake_x = mcrfpy.Animation("x", float(original_x + intensity), duration / 4, "easeOut")
shake_x.start(frame)
# Schedule return to center
def return_to_center(timer_name):
anim = mcrfpy.Animation("x", float(original_x), duration / 2, "easeInOut")
anim.start(frame)
mcrfpy.Timer("shake_return", return_to_center, int(duration * 250), once=True)
# Usage - wrap your game content in a Frame
game_container = mcrfpy.Frame(0, 0, 1024, 768)
# ... add game elements to game_container.children ...
screen_shake(game_container, intensity=8, duration=0.3)

View file

@ -0,0 +1,58 @@
"""McRogueFace - Screen Shake Effect (multi)
Documentation: https://mcrogueface.github.io/cookbook/effects_screen_shake
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_screen_shake_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
import math
def directional_shake(shaker, direction_x, direction_y, intensity=10, duration=0.2):
"""
Shake in a specific direction (e.g., direction of impact).
Args:
shaker: ScreenShakeManager instance
direction_x, direction_y: Direction vector (will be normalized)
intensity: Shake strength
duration: Shake duration
"""
# Normalize direction
length = math.sqrt(direction_x * direction_x + direction_y * direction_y)
if length == 0:
return
dir_x = direction_x / length
dir_y = direction_y / length
# Shake in the direction, then opposite, then back
shaker._animate_position(
shaker.original_x + dir_x * intensity,
shaker.original_y + dir_y * intensity,
duration / 3
)
def reverse(timer_name):
shaker._animate_position(
shaker.original_x - dir_x * intensity * 0.5,
shaker.original_y - dir_y * intensity * 0.5,
duration / 3
)
def reset(timer_name):
shaker._animate_position(
shaker.original_x,
shaker.original_y,
duration / 3
)
shaker.is_shaking = False
mcrfpy.Timer("dir_shake_rev", reverse, int(duration * 333), once=True)
mcrfpy.Timer("dir_shake_reset", reset, int(duration * 666), once=True)
# Usage: shake away from impact direction
hit_from_x, hit_from_y = -1, 0 # Hit from the left
directional_shake(shaker, hit_from_x, hit_from_y, intensity=12)

View file

@ -0,0 +1,74 @@
"""McRogueFace - Cell Highlighting (Targeting) (animated)
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_animated.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class TargetingSystem:
"""Handle ability targeting with visual feedback."""
def __init__(self, grid, player):
self.grid = grid
self.player = player
self.highlights = HighlightManager(grid)
self.current_ability = None
self.valid_targets = set()
def start_targeting(self, ability):
"""Begin targeting for an ability."""
self.current_ability = ability
px, py = self.player.pos
# Get valid targets based on ability
if ability.target_type == 'self':
self.valid_targets = {(px, py)}
elif ability.target_type == 'adjacent':
self.valid_targets = get_adjacent(px, py)
elif ability.target_type == 'ranged':
self.valid_targets = get_radius_range(px, py, ability.range)
elif ability.target_type == 'line':
self.valid_targets = get_line_range(px, py, ability.range)
# Filter to visible tiles only
self.valid_targets = {
(x, y) for x, y in self.valid_targets
if grid.is_in_fov(x, y)
}
# Show valid targets
self.highlights.add('attack', self.valid_targets)
def update_hover(self, x, y):
"""Update when cursor moves."""
if not self.current_ability:
return
# Clear previous AoE preview
self.highlights.remove('danger')
if (x, y) in self.valid_targets:
# Valid target - highlight it
self.highlights.add('select', [(x, y)])
# Show AoE if applicable
if self.current_ability.aoe_radius > 0:
aoe = get_radius_range(x, y, self.current_ability.aoe_radius, True)
self.highlights.add('danger', aoe)
else:
self.highlights.remove('select')
def confirm_target(self, x, y):
"""Confirm target selection."""
if (x, y) in self.valid_targets:
self.cancel_targeting()
return (x, y)
return None
def cancel_targeting(self):
"""Cancel targeting mode."""
self.current_ability = None
self.valid_targets = set()
self.highlights.clear()

View file

@ -0,0 +1,74 @@
"""McRogueFace - Cell Highlighting (Targeting) (basic)
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def get_line_range(start_x, start_y, max_range):
"""Get cells in cardinal directions (ranged attack)."""
cells = set()
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
for dist in range(1, max_range + 1):
x = start_x + dx * dist
y = start_y + dy * dist
# Stop if wall blocks line of sight
if not grid.at(x, y).transparent:
break
cells.add((x, y))
return cells
def get_radius_range(center_x, center_y, radius, include_center=False):
"""Get cells within a radius (spell area)."""
cells = set()
for x in range(center_x - radius, center_x + radius + 1):
for y in range(center_y - radius, center_y + radius + 1):
# Euclidean distance
dist = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5
if dist <= radius:
if include_center or (x, y) != (center_x, center_y):
cells.add((x, y))
return cells
def get_cone_range(origin_x, origin_y, direction, length, spread):
"""Get cells in a cone (breath attack)."""
import math
cells = set()
# Direction angles (in radians)
angles = {
'n': -math.pi / 2,
's': math.pi / 2,
'e': 0,
'w': math.pi,
'ne': -math.pi / 4,
'nw': -3 * math.pi / 4,
'se': math.pi / 4,
'sw': 3 * math.pi / 4
}
base_angle = angles.get(direction, 0)
half_spread = math.radians(spread / 2)
for x in range(origin_x - length, origin_x + length + 1):
for y in range(origin_y - length, origin_y + length + 1):
dx = x - origin_x
dy = y - origin_y
dist = (dx * dx + dy * dy) ** 0.5
if dist > 0 and dist <= length:
angle = math.atan2(dy, dx)
angle_diff = abs((angle - base_angle + math.pi) % (2 * math.pi) - math.pi)
if angle_diff <= half_spread:
cells.add((x, y))
return cells

View file

@ -0,0 +1,23 @@
"""McRogueFace - Cell Highlighting (Targeting) (multi)
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def show_path_preview(start, end):
"""Highlight the path between two points."""
path = find_path(start, end) # Your pathfinding function
if path:
highlights.add('path', path)
# Highlight destination specially
highlights.add('select', [end])
def hide_path_preview():
"""Clear path display."""
highlights.remove('path')
highlights.remove('select')

View file

@ -0,0 +1,31 @@
"""McRogueFace - Dijkstra Distance Maps (basic)
Documentation: https://mcrogueface.github.io/cookbook/grid_dijkstra
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dijkstra_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def ai_flee(entity, threat_x, threat_y):
"""Move entity away from threat using Dijkstra map."""
grid.compute_dijkstra(threat_x, threat_y)
ex, ey = entity.pos
current_dist = grid.get_dijkstra_distance(ex, ey)
# Find neighbor with highest distance
best_move = None
best_dist = current_dist
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx, ny = ex + dx, ey + dy
if grid.at(nx, ny).walkable:
dist = grid.get_dijkstra_distance(nx, ny)
if dist > best_dist:
best_dist = dist
best_move = (nx, ny)
if best_move:
entity.pos = best_move

View file

@ -0,0 +1,44 @@
"""McRogueFace - Dijkstra Distance Maps (multi)
Documentation: https://mcrogueface.github.io/cookbook/grid_dijkstra
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dijkstra_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
# Cache Dijkstra maps when possible
class CachedDijkstra:
"""Cache Dijkstra computations."""
def __init__(self, grid):
self.grid = grid
self.cache = {}
self.cache_valid = False
def invalidate(self):
"""Call when map changes."""
self.cache = {}
self.cache_valid = False
def get_distance(self, from_x, from_y, to_x, to_y):
"""Get cached distance or compute."""
key = (to_x, to_y) # Cache by destination
if key not in self.cache:
self.grid.compute_dijkstra(to_x, to_y)
# Store all distances from this computation
self.cache[key] = self._snapshot_distances()
return self.cache[key].get((from_x, from_y), float('inf'))
def _snapshot_distances(self):
"""Capture current distance values."""
grid_w, grid_h = self.grid.grid_size
distances = {}
for x in range(grid_w):
for y in range(grid_h):
dist = self.grid.get_dijkstra_distance(x, y)
if dist != float('inf'):
distances[(x, y)] = dist
return distances

View file

@ -0,0 +1,125 @@
"""McRogueFace - Room and Corridor Generator (basic)
Documentation: https://mcrogueface.github.io/cookbook/grid_dungeon_generator
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dungeon_generator_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class BSPNode:
"""Node in a BSP tree for dungeon generation."""
MIN_SIZE = 6
def __init__(self, x, y, w, h):
self.x = x
self.y = y
self.w = w
self.h = h
self.left = None
self.right = None
self.room = None
def split(self):
"""Recursively split this node."""
if self.left or self.right:
return False
# Choose split direction
if self.w > self.h and self.w / self.h >= 1.25:
horizontal = False
elif self.h > self.w and self.h / self.w >= 1.25:
horizontal = True
else:
horizontal = random.random() < 0.5
max_size = (self.h if horizontal else self.w) - self.MIN_SIZE
if max_size <= self.MIN_SIZE:
return False
split = random.randint(self.MIN_SIZE, max_size)
if horizontal:
self.left = BSPNode(self.x, self.y, self.w, split)
self.right = BSPNode(self.x, self.y + split, self.w, self.h - split)
else:
self.left = BSPNode(self.x, self.y, split, self.h)
self.right = BSPNode(self.x + split, self.y, self.w - split, self.h)
return True
def create_rooms(self, grid):
"""Create rooms in leaf nodes and connect siblings."""
if self.left or self.right:
if self.left:
self.left.create_rooms(grid)
if self.right:
self.right.create_rooms(grid)
# Connect children
if self.left and self.right:
left_room = self.left.get_room()
right_room = self.right.get_room()
if left_room and right_room:
connect_points(grid, left_room.center, right_room.center)
else:
# Leaf node - create room
w = random.randint(3, self.w - 2)
h = random.randint(3, self.h - 2)
x = self.x + random.randint(1, self.w - w - 1)
y = self.y + random.randint(1, self.h - h - 1)
self.room = Room(x, y, w, h)
carve_room(grid, self.room)
def get_room(self):
"""Get a room from this node or its children."""
if self.room:
return self.room
left_room = self.left.get_room() if self.left else None
right_room = self.right.get_room() if self.right else None
if left_room and right_room:
return random.choice([left_room, right_room])
return left_room or right_room
def generate_bsp_dungeon(grid, iterations=4):
"""Generate a BSP-based dungeon."""
grid_w, grid_h = grid.grid_size
# Fill with walls
for x in range(grid_w):
for y in range(grid_h):
point = grid.at(x, y)
point.tilesprite = TILE_WALL
point.walkable = False
point.transparent = False
# Build BSP tree
root = BSPNode(0, 0, grid_w, grid_h)
nodes = [root]
for _ in range(iterations):
new_nodes = []
for node in nodes:
if node.split():
new_nodes.extend([node.left, node.right])
nodes = new_nodes or nodes
# Create rooms and corridors
root.create_rooms(grid)
# Collect all rooms
rooms = []
def collect_rooms(node):
if node.room:
rooms.append(node.room)
if node.left:
collect_rooms(node.left)
if node.right:
collect_rooms(node.right)
collect_rooms(root)
return rooms

View file

@ -0,0 +1,148 @@
"""McRogueFace - Room and Corridor Generator (complete)
Documentation: https://mcrogueface.github.io/cookbook/grid_dungeon_generator
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dungeon_generator_complete.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
import random
# Tile indices (adjust for your tileset)
TILE_FLOOR = 0
TILE_WALL = 1
TILE_DOOR = 2
TILE_STAIRS_DOWN = 3
TILE_STAIRS_UP = 4
class DungeonGenerator:
"""Procedural dungeon generator with rooms and corridors."""
def __init__(self, grid, seed=None):
self.grid = grid
self.grid_w, self.grid_h = grid.grid_size
self.rooms = []
if seed is not None:
random.seed(seed)
def generate(self, room_count=8, min_room=4, max_room=10):
"""Generate a complete dungeon level."""
self.rooms = []
# Fill with walls
self._fill_walls()
# Place rooms
attempts = 0
max_attempts = room_count * 10
while len(self.rooms) < room_count and attempts < max_attempts:
attempts += 1
# Random room size
w = random.randint(min_room, max_room)
h = random.randint(min_room, max_room)
# Random position (leaving border)
x = random.randint(1, self.grid_w - w - 2)
y = random.randint(1, self.grid_h - h - 2)
room = Room(x, y, w, h)
# Check overlap
if not any(room.intersects(r) for r in self.rooms):
self._carve_room(room)
# Connect to previous room
if self.rooms:
self._dig_corridor(self.rooms[-1].center, room.center)
self.rooms.append(room)
# Place stairs
if len(self.rooms) >= 2:
self._place_stairs()
return self.rooms
def _fill_walls(self):
"""Fill the entire grid with wall tiles."""
for x in range(self.grid_w):
for y in range(self.grid_h):
point = self.grid.at(x, y)
point.tilesprite = TILE_WALL
point.walkable = False
point.transparent = False
def _carve_room(self, room):
"""Carve out a room, making it walkable."""
for x in range(room.x, room.x + room.width):
for y in range(room.y, room.y + room.height):
self._set_floor(x, y)
def _set_floor(self, x, y):
"""Set a single tile as floor."""
if 0 <= x < self.grid_w and 0 <= y < self.grid_h:
point = self.grid.at(x, y)
point.tilesprite = TILE_FLOOR
point.walkable = True
point.transparent = True
def _dig_corridor(self, start, end):
"""Dig an L-shaped corridor between two points."""
x1, y1 = start
x2, y2 = end
# Randomly choose horizontal-first or vertical-first
if random.random() < 0.5:
# Horizontal then vertical
self._dig_horizontal(x1, x2, y1)
self._dig_vertical(y1, y2, x2)
else:
# Vertical then horizontal
self._dig_vertical(y1, y2, x1)
self._dig_horizontal(x1, x2, y2)
def _dig_horizontal(self, x1, x2, y):
"""Dig a horizontal tunnel."""
for x in range(min(x1, x2), max(x1, x2) + 1):
self._set_floor(x, y)
def _dig_vertical(self, y1, y2, x):
"""Dig a vertical tunnel."""
for y in range(min(y1, y2), max(y1, y2) + 1):
self._set_floor(x, y)
def _place_stairs(self):
"""Place stairs in first and last rooms."""
# Stairs up in first room
start_room = self.rooms[0]
sx, sy = start_room.center
point = self.grid.at(sx, sy)
point.tilesprite = TILE_STAIRS_UP
# Stairs down in last room
end_room = self.rooms[-1]
ex, ey = end_room.center
point = self.grid.at(ex, ey)
point.tilesprite = TILE_STAIRS_DOWN
return (sx, sy), (ex, ey)
def get_spawn_point(self):
"""Get a good spawn point for the player."""
if self.rooms:
return self.rooms[0].center
return (self.grid_w // 2, self.grid_h // 2)
def get_random_floor(self):
"""Get a random walkable floor tile."""
floors = []
for x in range(self.grid_w):
for y in range(self.grid_h):
if self.grid.at(x, y).walkable:
floors.append((x, y))
return random.choice(floors) if floors else None

View file

@ -0,0 +1,20 @@
"""McRogueFace - Basic Fog of War (grid_fog_of_war)
Documentation: https://mcrogueface.github.io/cookbook/grid_fog_of_war
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_fog_of_war.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
# Shadowcasting (default) - fast and produces nice results
grid.compute_fov(x, y, 10, mcrfpy.FOV.SHADOW)
# Recursive shadowcasting - slightly different corner behavior
grid.compute_fov(x, y, 10, mcrfpy.FOV.RECURSIVE_SHADOW)
# Diamond - simple but produces diamond-shaped FOV
grid.compute_fov(x, y, 10, mcrfpy.FOV.DIAMOND)
# Permissive - sees more tiles, good for tactical games
grid.compute_fov(x, y, 10, mcrfpy.FOV.PERMISSIVE)

View file

@ -0,0 +1,114 @@
"""McRogueFace - Multi-Layer Tiles (basic)
Documentation: https://mcrogueface.github.io/cookbook/grid_multi_layer
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_multi_layer_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class EffectLayer:
"""Manage visual effects with color overlays."""
def __init__(self, grid, z_index=2):
self.grid = grid
self.layer = grid.add_layer("color", z_index=z_index)
self.effects = {} # (x, y) -> effect_data
def add_effect(self, x, y, effect_type, duration=None, **kwargs):
"""Add a visual effect."""
self.effects[(x, y)] = {
'type': effect_type,
'duration': duration,
'time': 0,
**kwargs
}
def remove_effect(self, x, y):
"""Remove an effect."""
if (x, y) in self.effects:
del self.effects[(x, y)]
self.layer.set(x, y, mcrfpy.Color(0, 0, 0, 0))
def update(self, dt):
"""Update all effects."""
import math
to_remove = []
for (x, y), effect in self.effects.items():
effect['time'] += dt
# Check expiration
if effect['duration'] and effect['time'] >= effect['duration']:
to_remove.append((x, y))
continue
# Calculate color based on effect type
color = self._calculate_color(effect)
self.layer.set(x, y, color)
for pos in to_remove:
self.remove_effect(*pos)
def _calculate_color(self, effect):
"""Get color for an effect at current time."""
import math
t = effect['time']
effect_type = effect['type']
if effect_type == 'fire':
# Flickering orange/red
flicker = 0.7 + 0.3 * math.sin(t * 10)
return mcrfpy.Color(
255,
int(100 + 50 * math.sin(t * 8)),
0,
int(180 * flicker)
)
elif effect_type == 'poison':
# Pulsing green
pulse = 0.5 + 0.5 * math.sin(t * 3)
return mcrfpy.Color(0, 200, 0, int(100 * pulse))
elif effect_type == 'ice':
# Static blue with shimmer
shimmer = 0.8 + 0.2 * math.sin(t * 5)
return mcrfpy.Color(100, 150, 255, int(120 * shimmer))
elif effect_type == 'blood':
# Fading red
duration = effect.get('duration', 5)
fade = 1 - (t / duration) if duration else 1
return mcrfpy.Color(150, 0, 0, int(150 * fade))
elif effect_type == 'highlight':
# Pulsing highlight
pulse = 0.5 + 0.5 * math.sin(t * 4)
base = effect.get('color', mcrfpy.Color(255, 255, 0, 100))
return mcrfpy.Color(base.r, base.g, base.b, int(base.a * pulse))
return mcrfpy.Color(128, 128, 128, 50)
# Usage
effects = EffectLayer(grid)
# Add fire effect (permanent)
effects.add_effect(5, 5, 'fire')
# Add blood stain (fades over 10 seconds)
effects.add_effect(10, 10, 'blood', duration=10)
# Add poison cloud
for x in range(8, 12):
for y in range(8, 12):
effects.add_effect(x, y, 'poison', duration=5)
# Update in game loop
def game_update(runtime):
effects.update(0.016) # 60 FPS
mcrfpy.setTimer("effects", game_update, 16)

View file

@ -0,0 +1,38 @@
"""McRogueFace - Multi-Layer Tiles (complete)
Documentation: https://mcrogueface.github.io/cookbook/grid_multi_layer
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_multi_layer_complete.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class OptimizedLayers:
"""Performance-optimized layer management."""
def __init__(self, grid):
self.grid = grid
self.dirty_effects = set() # Only update changed cells
self.batch_updates = []
def mark_dirty(self, x, y):
"""Mark a cell as needing update."""
self.dirty_effects.add((x, y))
def batch_set(self, layer, cells_and_values):
"""Queue batch updates."""
self.batch_updates.append((layer, cells_and_values))
def flush(self):
"""Apply all queued updates."""
for layer, updates in self.batch_updates:
for x, y, value in updates:
layer.set(x, y, value)
self.batch_updates = []
def update_dirty_only(self, effect_layer, effect_calculator):
"""Only update cells marked dirty."""
for x, y in self.dirty_effects:
color = effect_calculator(x, y)
effect_layer.set(x, y, color)
self.dirty_effects.clear()

View file

@ -0,0 +1,120 @@
"""McRogueFace - Health Bar Widget (animated)
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_animated.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class AnimatedHealthBar:
"""Health bar with smooth fill animation."""
def __init__(self, x, y, w, h, current, maximum):
self.x = x
self.y = y
self.w = w
self.h = h
self.current = current
self.display_current = current # What's visually shown
self.maximum = maximum
self.timer_name = f"hp_anim_{id(self)}"
# Background
self.background = mcrfpy.Frame(x, y, w, h)
self.background.fill_color = mcrfpy.Color(40, 40, 40)
self.background.outline = 2
self.background.outline_color = mcrfpy.Color(60, 60, 60)
# Damage preview (shows recent damage in different color)
self.damage_fill = mcrfpy.Frame(x + 2, y + 2, w - 4, h - 4)
self.damage_fill.fill_color = mcrfpy.Color(180, 50, 50)
self.damage_fill.outline = 0
# Main fill
self.fill = mcrfpy.Frame(x + 2, y + 2, w - 4, h - 4)
self.fill.fill_color = mcrfpy.Color(50, 200, 50)
self.fill.outline = 0
self._update_display()
def _update_display(self):
"""Update the visual fill based on display_current."""
ratio = max(0, min(1, self.display_current / self.maximum))
self.fill.w = (self.w - 4) * ratio
# Color based on ratio
if ratio > 0.6:
self.fill.fill_color = mcrfpy.Color(50, 200, 50)
elif ratio > 0.3:
self.fill.fill_color = mcrfpy.Color(230, 180, 30)
else:
self.fill.fill_color = mcrfpy.Color(200, 50, 50)
def set_health(self, new_current, animate=True):
"""
Set health with optional animation.
Args:
new_current: New health value
animate: Whether to animate the transition
"""
old_current = self.current
self.current = max(0, min(self.maximum, new_current))
if not animate:
self.display_current = self.current
self._update_display()
return
# Show damage preview immediately
if self.current < old_current:
damage_ratio = self.current / self.maximum
self.damage_fill.w = (self.w - 4) * (old_current / self.maximum)
# Animate the fill
self._start_animation()
def _start_animation(self):
"""Start animating toward target health."""
mcrfpy.delTimer(self.timer_name)
def animate_step(dt):
# Lerp toward target
diff = self.current - self.display_current
if abs(diff) < 0.5:
self.display_current = self.current
mcrfpy.delTimer(self.timer_name)
# Also update damage preview
self.damage_fill.w = self.fill.w
else:
# Move 10% of the way each frame
self.display_current += diff * 0.1
self._update_display()
mcrfpy.setTimer(self.timer_name, animate_step, 16)
def damage(self, amount):
"""Apply damage with animation."""
self.set_health(self.current - amount, animate=True)
def heal(self, amount):
"""Apply healing with animation."""
self.set_health(self.current + amount, animate=True)
def add_to_scene(self, ui):
"""Add all frames to scene."""
ui.append(self.background)
ui.append(self.damage_fill)
ui.append(self.fill)
# Usage
hp_bar = AnimatedHealthBar(50, 50, 300, 30, current=100, maximum=100)
hp_bar.add_to_scene(ui)
# Damage will animate smoothly
hp_bar.damage(40)

View file

@ -0,0 +1,43 @@
"""McRogueFace - Health Bar Widget (basic)
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
mcrfpy.createScene("game")
mcrfpy.setScene("game")
ui = mcrfpy.sceneUI("game")
# Player health bar at top
player_hp = EnhancedHealthBar(10, 10, 300, 30, 100, 100)
player_hp.add_to_scene(ui)
# Enemy health bar
enemy_hp = EnhancedHealthBar(400, 10, 200, 20, 50, 50)
enemy_hp.add_to_scene(ui)
# Simulate combat
def combat_tick(dt):
import random
if random.random() < 0.3:
player_hp.damage(random.randint(5, 15))
if random.random() < 0.4:
enemy_hp.damage(random.randint(3, 8))
mcrfpy.setTimer("combat", combat_tick, 1000)
# Keyboard controls for testing
def on_key(key, state):
if state != "start":
return
if key == "H":
player_hp.heal(20)
elif key == "D":
player_hp.damage(10)
mcrfpy.keypressScene(on_key)

View file

@ -0,0 +1,123 @@
"""McRogueFace - Health Bar Widget (enhanced)
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_enhanced.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class EnhancedHealthBar:
"""Health bar with text display, color transitions, and animations."""
def __init__(self, x, y, w, h, current, maximum, show_text=True):
self.x = x
self.y = y
self.w = w
self.h = h
self.current = current
self.maximum = maximum
self.show_text = show_text
# Color thresholds (ratio -> color)
self.colors = {
0.6: mcrfpy.Color(50, 205, 50), # Green when > 60%
0.3: mcrfpy.Color(255, 165, 0), # Orange when > 30%
0.0: mcrfpy.Color(220, 20, 20), # Red when <= 30%
}
# Background frame with dark fill
self.background = mcrfpy.Frame(x, y, w, h)
self.background.fill_color = mcrfpy.Color(30, 30, 30)
self.background.outline = 2
self.background.outline_color = mcrfpy.Color(100, 100, 100)
# Fill frame (nested inside background conceptually)
padding = 2
self.fill = mcrfpy.Frame(
x + padding,
y + padding,
w - padding * 2,
h - padding * 2
)
self.fill.outline = 0
# Text label
self.label = None
if show_text:
self.label = mcrfpy.Caption(
"",
mcrfpy.default_font,
x + w / 2 - 20,
y + h / 2 - 8
)
self.label.fill_color = mcrfpy.Color(255, 255, 255)
self.label.outline = 1
self.label.outline_color = mcrfpy.Color(0, 0, 0)
self._update()
def _get_color_for_ratio(self, ratio):
"""Get the appropriate color based on health ratio."""
for threshold, color in sorted(self.colors.items(), reverse=True):
if ratio > threshold:
return color
# Return the lowest threshold color if ratio is 0 or below
return self.colors[0.0]
def _update(self):
"""Update fill width, color, and text."""
ratio = max(0, min(1, self.current / self.maximum))
# Update fill width (accounting for padding)
padding = 2
self.fill.w = (self.w - padding * 2) * ratio
# Update color based on ratio
self.fill.fill_color = self._get_color_for_ratio(ratio)
# Update text
if self.label:
self.label.text = f"{int(self.current)}/{int(self.maximum)}"
# Center the text
text_width = len(self.label.text) * 8 # Approximate
self.label.x = self.x + (self.w - text_width) / 2
def set_health(self, current, maximum=None):
"""Update health values."""
self.current = max(0, current)
if maximum is not None:
self.maximum = maximum
self._update()
def damage(self, amount):
"""Apply damage (convenience method)."""
self.set_health(self.current - amount)
def heal(self, amount):
"""Apply healing (convenience method)."""
self.set_health(min(self.maximum, self.current + amount))
def add_to_scene(self, ui):
"""Add all components to scene UI."""
ui.append(self.background)
ui.append(self.fill)
if self.label:
ui.append(self.label)
# Usage
mcrfpy.createScene("demo")
mcrfpy.setScene("demo")
ui = mcrfpy.sceneUI("demo")
# Create enhanced health bar
hp = EnhancedHealthBar(50, 50, 250, 25, current=100, maximum=100)
hp.add_to_scene(ui)
# Simulate damage
hp.damage(30) # Now 70/100, shows green
hp.damage(25) # Now 45/100, shows orange
hp.damage(20) # Now 25/100, shows red

View file

@ -0,0 +1,108 @@
"""McRogueFace - Health Bar Widget (multi)
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class ResourceBar:
"""Generic resource bar that can represent any stat."""
def __init__(self, x, y, w, h, current, maximum,
fill_color, bg_color=None, label=""):
self.x = x
self.y = y
self.w = w
self.h = h
self.current = current
self.maximum = maximum
self.label_text = label
if bg_color is None:
bg_color = mcrfpy.Color(30, 30, 30)
# Background
self.background = mcrfpy.Frame(x, y, w, h)
self.background.fill_color = bg_color
self.background.outline = 1
self.background.outline_color = mcrfpy.Color(60, 60, 60)
# Fill
self.fill = mcrfpy.Frame(x + 1, y + 1, w - 2, h - 2)
self.fill.fill_color = fill_color
self.fill.outline = 0
# Label (left side)
self.label = mcrfpy.Caption(label, mcrfpy.default_font, x - 30, y + 2)
self.label.fill_color = mcrfpy.Color(200, 200, 200)
self._update()
def _update(self):
ratio = max(0, min(1, self.current / self.maximum))
self.fill.w = (self.w - 2) * ratio
def set_value(self, current, maximum=None):
self.current = max(0, current)
if maximum:
self.maximum = maximum
self._update()
def add_to_scene(self, ui):
if self.label_text:
ui.append(self.label)
ui.append(self.background)
ui.append(self.fill)
class PlayerStats:
"""Collection of resource bars for a player."""
def __init__(self, x, y):
bar_width = 200
bar_height = 18
spacing = 25
self.hp = ResourceBar(
x, y, bar_width, bar_height,
current=100, maximum=100,
fill_color=mcrfpy.Color(220, 50, 50),
label="HP"
)
self.mp = ResourceBar(
x, y + spacing, bar_width, bar_height,
current=50, maximum=50,
fill_color=mcrfpy.Color(50, 100, 220),
label="MP"
)
self.stamina = ResourceBar(
x, y + spacing * 2, bar_width, bar_height,
current=80, maximum=80,
fill_color=mcrfpy.Color(50, 180, 50),
label="SP"
)
def add_to_scene(self, ui):
self.hp.add_to_scene(ui)
self.mp.add_to_scene(ui)
self.stamina.add_to_scene(ui)
# Usage
mcrfpy.createScene("stats_demo")
mcrfpy.setScene("stats_demo")
ui = mcrfpy.sceneUI("stats_demo")
stats = PlayerStats(80, 20)
stats.add_to_scene(ui)
# Update individual stats
stats.hp.set_value(75)
stats.mp.set_value(30)
stats.stamina.set_value(60)

View file

@ -0,0 +1,53 @@
"""McRogueFace - Selection Menu Widget (basic)
Documentation: https://mcrogueface.github.io/cookbook/ui_menu
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_menu_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Setup
mcrfpy.createScene("main_menu")
mcrfpy.setScene("main_menu")
ui = mcrfpy.sceneUI("main_menu")
# Background
bg = mcrfpy.Frame(0, 0, 1024, 768)
bg.fill_color = mcrfpy.Color(20, 20, 35)
ui.append(bg)
# Title
title = mcrfpy.Caption("DUNGEON QUEST", mcrfpy.default_font, 350, 100)
title.fill_color = mcrfpy.Color(255, 200, 50)
ui.append(title)
# Menu
def start_game():
print("Starting game...")
def show_options():
print("Options...")
menu = Menu(
362, 250,
["New Game", "Continue", "Options", "Quit"],
lambda i, opt: {
0: start_game,
1: lambda: print("Continue..."),
2: show_options,
3: mcrfpy.exit
}.get(i, lambda: None)(),
title="Main Menu"
)
menu.add_to_scene(ui)
# Input
def on_key(key, state):
if state != "start":
return
menu.handle_key(key)
mcrfpy.keypressScene(on_key)

View file

@ -0,0 +1,159 @@
"""McRogueFace - Selection Menu Widget (enhanced)
Documentation: https://mcrogueface.github.io/cookbook/ui_menu
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_menu_enhanced.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class MenuBar:
"""Horizontal menu bar with dropdown submenus."""
def __init__(self, y=0, items=None):
"""
Create a menu bar.
Args:
y: Y position (usually 0 for top)
items: List of dicts with 'label' and 'options' keys
"""
self.y = y
self.items = items or []
self.selected_item = 0
self.dropdown_open = False
self.dropdown_selected = 0
self.item_width = 100
self.height = 30
# Main bar frame
self.bar = mcrfpy.Frame(0, y, 1024, self.height)
self.bar.fill_color = mcrfpy.Color(50, 50, 70)
self.bar.outline = 0
# Item captions
self.item_captions = []
for i, item in enumerate(items):
cap = mcrfpy.Caption(
item['label'],
mcrfpy.default_font,
10 + i * self.item_width,
y + 7
)
cap.fill_color = mcrfpy.Color(200, 200, 200)
self.item_captions.append(cap)
# Dropdown panel (hidden initially)
self.dropdown = None
self.dropdown_captions = []
def _update_highlight(self):
"""Update visual selection on bar."""
for i, cap in enumerate(self.item_captions):
if i == self.selected_item and self.dropdown_open:
cap.fill_color = mcrfpy.Color(255, 255, 100)
else:
cap.fill_color = mcrfpy.Color(200, 200, 200)
def _show_dropdown(self, ui):
"""Show dropdown for selected item."""
# Remove existing dropdown
self._hide_dropdown(ui)
item = self.items[self.selected_item]
options = item.get('options', [])
if not options:
return
x = 5 + self.selected_item * self.item_width
y = self.y + self.height
width = 150
height = len(options) * 25 + 10
self.dropdown = mcrfpy.Frame(x, y, width, height)
self.dropdown.fill_color = mcrfpy.Color(40, 40, 60, 250)
self.dropdown.outline = 1
self.dropdown.outline_color = mcrfpy.Color(80, 80, 100)
ui.append(self.dropdown)
self.dropdown_captions = []
for i, opt in enumerate(options):
cap = mcrfpy.Caption(
opt['label'],
mcrfpy.default_font,
x + 10,
y + 5 + i * 25
)
cap.fill_color = mcrfpy.Color(200, 200, 200)
self.dropdown_captions.append(cap)
ui.append(cap)
self.dropdown_selected = 0
self._update_dropdown_highlight()
def _hide_dropdown(self, ui):
"""Hide dropdown menu."""
if self.dropdown:
try:
ui.remove(self.dropdown)
except:
pass
self.dropdown = None
for cap in self.dropdown_captions:
try:
ui.remove(cap)
except:
pass
self.dropdown_captions = []
def _update_dropdown_highlight(self):
"""Update dropdown selection highlight."""
for i, cap in enumerate(self.dropdown_captions):
if i == self.dropdown_selected:
cap.fill_color = mcrfpy.Color(255, 255, 100)
else:
cap.fill_color = mcrfpy.Color(200, 200, 200)
def add_to_scene(self, ui):
ui.append(self.bar)
for cap in self.item_captions:
ui.append(cap)
def handle_key(self, key, ui):
"""Handle keyboard navigation."""
if not self.dropdown_open:
if key == "Left":
self.selected_item = (self.selected_item - 1) % len(self.items)
self._update_highlight()
elif key == "Right":
self.selected_item = (self.selected_item + 1) % len(self.items)
self._update_highlight()
elif key == "Return" or key == "Down":
self.dropdown_open = True
self._show_dropdown(ui)
self._update_highlight()
else:
if key == "Up":
options = self.items[self.selected_item].get('options', [])
self.dropdown_selected = (self.dropdown_selected - 1) % len(options)
self._update_dropdown_highlight()
elif key == "Down":
options = self.items[self.selected_item].get('options', [])
self.dropdown_selected = (self.dropdown_selected + 1) % len(options)
self._update_dropdown_highlight()
elif key == "Return":
opt = self.items[self.selected_item]['options'][self.dropdown_selected]
if opt.get('action'):
opt['action']()
self.dropdown_open = False
self._hide_dropdown(ui)
self._update_highlight()
elif key == "Escape":
self.dropdown_open = False
self._hide_dropdown(ui)
self._update_highlight()

View file

@ -0,0 +1,54 @@
"""McRogueFace - Message Log Widget (basic)
Documentation: https://mcrogueface.github.io/cookbook/ui_message_log
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_message_log_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Initialize
mcrfpy.createScene("game")
mcrfpy.setScene("game")
ui = mcrfpy.sceneUI("game")
# Create log at bottom of screen
log = EnhancedMessageLog(10, 500, 700, 250, line_height=20)
ui.append(log.frame)
# Simulate game events
def simulate_combat(dt):
import random
events = [
("You swing your sword!", "combat"),
("The orc dodges!", "combat"),
("Critical hit!", "combat"),
("You found a potion!", "loot"),
]
event = random.choice(events)
log.add(event[0], event[1])
# Add messages every 2 seconds for demo
mcrfpy.setTimer("combat_sim", simulate_combat, 2000)
# Keyboard controls
def on_key(key, state):
if state != "start":
return
if key == "PageUp":
log.scroll_up(3)
elif key == "PageDown":
log.scroll_down(3)
elif key == "C":
log.set_filter('combat')
elif key == "L":
log.set_filter('loot')
elif key == "A":
log.set_filter(None) # All
mcrfpy.keypressScene(on_key)
log.system("Press PageUp/PageDown to scroll")
log.system("Press C for combat, L for loot, A for all")

View file

@ -0,0 +1,27 @@
"""McRogueFace - Message Log Widget (enhanced)
Documentation: https://mcrogueface.github.io/cookbook/ui_message_log
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_message_log_enhanced.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def handle_keys(key, state):
if state != "start":
return
if key == "PageUp":
log.scroll_up(5)
elif key == "PageDown":
log.scroll_down(5)
mcrfpy.keypressScene(handle_keys)
# Or with mouse scroll on the frame
def on_log_scroll(x, y, button, action):
# Note: You may need to implement scroll detection
# based on your input system
pass
log.frame.click = on_log_scroll

View file

@ -0,0 +1,69 @@
"""McRogueFace - Modal Dialog Widget (basic)
Documentation: https://mcrogueface.github.io/cookbook/ui_modal_dialog
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_modal_dialog_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Scene setup
mcrfpy.createScene("game")
mcrfpy.setScene("game")
ui = mcrfpy.sceneUI("game")
# Game background
bg = mcrfpy.Frame(0, 0, 1024, 768)
bg.fill_color = mcrfpy.Color(25, 35, 45)
ui.append(bg)
title = mcrfpy.Caption("My Game", mcrfpy.default_font, 450, 50)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Quit button
quit_btn = mcrfpy.Frame(430, 400, 160, 50)
quit_btn.fill_color = mcrfpy.Color(150, 50, 50)
quit_btn.outline = 2
quit_btn.outline_color = mcrfpy.Color(200, 100, 100)
ui.append(quit_btn)
quit_label = mcrfpy.Caption("Quit Game", mcrfpy.default_font, 460, 415)
quit_label.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(quit_label)
# Confirmation dialog
confirm_dialog = None
def show_quit_confirm():
global confirm_dialog
def on_response(index, label):
if label == "Yes":
mcrfpy.exit()
confirm_dialog = EnhancedDialog(
"Quit Game?",
"Are you sure you want to quit?\nUnsaved progress will be lost.",
["Yes", "No"],
DialogStyle.WARNING,
on_response
)
confirm_dialog.add_to_scene(ui)
confirm_dialog.show()
quit_btn.click = lambda x, y, b, a: show_quit_confirm() if a == "start" else None
def on_key(key, state):
if state != "start":
return
if confirm_dialog and confirm_dialog.handle_key(key):
return
if key == "Escape":
show_quit_confirm()
mcrfpy.keypressScene(on_key)

View file

@ -0,0 +1,78 @@
"""McRogueFace - Modal Dialog Widget (enhanced)
Documentation: https://mcrogueface.github.io/cookbook/ui_modal_dialog
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_modal_dialog_enhanced.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class DialogManager:
"""Manages a queue of dialogs."""
def __init__(self, ui):
self.ui = ui
self.queue = []
self.current = None
def show(self, title, message, buttons=None, style=None, callback=None):
"""
Queue a dialog to show.
If no dialog is active, shows immediately.
Otherwise, queues for later.
"""
dialog_data = {
'title': title,
'message': message,
'buttons': buttons or ["OK"],
'style': style or DialogStyle.INFO,
'callback': callback
}
if self.current is None:
self._show_dialog(dialog_data)
else:
self.queue.append(dialog_data)
def _show_dialog(self, data):
"""Actually display a dialog."""
def on_close(index, label):
if data['callback']:
data['callback'](index, label)
self._on_dialog_closed()
self.current = EnhancedDialog(
data['title'],
data['message'],
data['buttons'],
data['style'],
on_close
)
self.current.add_to_scene(self.ui)
self.current.show()
def _on_dialog_closed(self):
"""Handle dialog close, show next if queued."""
self.current = None
if self.queue:
next_dialog = self.queue.pop(0)
self._show_dialog(next_dialog)
def handle_key(self, key):
"""Forward key events to current dialog."""
if self.current:
return self.current.handle_key(key)
return False
# Usage
manager = DialogManager(ui)
# Queue multiple dialogs
manager.show("First", "This is the first message")
manager.show("Second", "This appears after closing the first")
manager.show("Third", "And this is last", ["Done"])

View file

@ -0,0 +1,65 @@
"""McRogueFace - Tooltip on Hover (basic)
Documentation: https://mcrogueface.github.io/cookbook/ui_tooltip
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_tooltip_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
mcrfpy.createScene("game")
mcrfpy.setScene("game")
ui = mcrfpy.sceneUI("game")
# Background
bg = mcrfpy.Frame(0, 0, 1024, 768)
bg.fill_color = mcrfpy.Color(25, 25, 35)
ui.append(bg)
# Create inventory slots with tooltips
class InventorySlot:
def __init__(self, x, y, item_name, item_desc, tooltip_mgr):
self.frame = mcrfpy.Frame(x, y, 50, 50)
self.frame.fill_color = mcrfpy.Color(50, 50, 60)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(80, 80, 90)
self.label = mcrfpy.Caption(item_name[:3], mcrfpy.default_font, x + 10, y + 15)
self.label.fill_color = mcrfpy.Color(200, 200, 200)
tooltip_mgr.register(self.frame, item_desc, title=item_name)
def add_to_scene(self, ui):
ui.append(self.frame)
ui.append(self.label)
# Setup tooltip manager
tips = TooltipManager()
tips.hover_delay = 300
# Create inventory
items = [
("Health Potion", "Restores 50 HP\nConsumable"),
("Mana Crystal", "Restores 30 MP\nConsumable"),
("Iron Key", "Opens iron doors\nQuest Item"),
("Gold Ring", "Worth 100 gold\nSell to merchant"),
]
slots = []
for i, (name, desc) in enumerate(items):
slot = InventorySlot(100 + i * 60, 100, name, desc, tips)
slot.add_to_scene(ui)
slots.append(slot)
# Add tooltip last
tips.add_to_scene(ui)
# Update loop
def update(dt):
from mcrfpy import automation
x, y = automation.position()
tips.update(x, y)
mcrfpy.setTimer("update", update, 50)

View file

@ -0,0 +1,80 @@
"""McRogueFace - Tooltip on Hover (multi)
Documentation: https://mcrogueface.github.io/cookbook/ui_tooltip
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_tooltip_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
def create_info_icon(x, y, tooltip_text, ui):
"""
Create an info icon that shows tooltip on hover.
Args:
x, y: Position of the icon
tooltip_text: Text to show
ui: Scene UI to add elements to
"""
# Info icon (small circle with "i")
icon = mcrfpy.Frame(x, y, 20, 20)
icon.fill_color = mcrfpy.Color(70, 130, 180)
icon.outline = 1
icon.outline_color = mcrfpy.Color(100, 160, 210)
icon_label = mcrfpy.Caption("i", mcrfpy.default_font, x + 6, y + 2)
icon_label.fill_color = mcrfpy.Color(255, 255, 255)
# Tooltip (positioned to the right of icon)
tip_frame = mcrfpy.Frame(x + 25, y - 5, 180, 50)
tip_frame.fill_color = mcrfpy.Color(40, 40, 55, 240)
tip_frame.outline = 1
tip_frame.outline_color = mcrfpy.Color(80, 80, 100)
tip_frame.visible = False
tip_text = mcrfpy.Caption(tooltip_text, mcrfpy.default_font, x + 33, y + 3)
tip_text.fill_color = mcrfpy.Color(220, 220, 220)
tip_text.visible = False
# Hover behavior
def on_icon_hover(mx, my, button, action):
tip_frame.visible = True
tip_text.visible = True
icon.click = on_icon_hover
# Track when to hide
def check_hover(dt):
from mcrfpy import automation
mx, my = automation.position()
if not (icon.x <= mx <= icon.x + icon.w and
icon.y <= my <= icon.y + icon.h):
if tip_frame.visible:
tip_frame.visible = False
tip_text.visible = False
timer_name = f"info_hover_{id(icon)}"
mcrfpy.setTimer(timer_name, check_hover, 100)
# Add to scene
ui.append(icon)
ui.append(icon_label)
ui.append(tip_frame)
ui.append(tip_text)
return icon
# Usage
mcrfpy.createScene("info_demo")
mcrfpy.setScene("info_demo")
ui = mcrfpy.sceneUI("info_demo")
# Setting with info icon
setting_label = mcrfpy.Caption("Difficulty:", mcrfpy.default_font, 100, 100)
setting_label.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(setting_label)
create_info_icon(200, 98, "Affects enemy\nHP and damage", ui)