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,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)