draft tutorial revisions
This commit is contained in:
parent
838da4571d
commit
48359b5a48
70 changed files with 6216 additions and 28 deletions
74
docs/cookbook/grid/grid_cell_highlighting_animated.py
Normal file
74
docs/cookbook/grid/grid_cell_highlighting_animated.py
Normal 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()
|
||||
74
docs/cookbook/grid/grid_cell_highlighting_basic.py
Normal file
74
docs/cookbook/grid/grid_cell_highlighting_basic.py
Normal 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
|
||||
23
docs/cookbook/grid/grid_cell_highlighting_multi.py
Normal file
23
docs/cookbook/grid/grid_cell_highlighting_multi.py
Normal 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')
|
||||
31
docs/cookbook/grid/grid_dijkstra_basic.py
Normal file
31
docs/cookbook/grid/grid_dijkstra_basic.py
Normal 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
|
||||
44
docs/cookbook/grid/grid_dijkstra_multi.py
Normal file
44
docs/cookbook/grid/grid_dijkstra_multi.py
Normal 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
|
||||
125
docs/cookbook/grid/grid_dungeon_generator_basic.py
Normal file
125
docs/cookbook/grid/grid_dungeon_generator_basic.py
Normal 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
|
||||
148
docs/cookbook/grid/grid_dungeon_generator_complete.py
Normal file
148
docs/cookbook/grid/grid_dungeon_generator_complete.py
Normal 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
|
||||
20
docs/cookbook/grid/grid_fog_of_war.py
Normal file
20
docs/cookbook/grid/grid_fog_of_war.py
Normal 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)
|
||||
114
docs/cookbook/grid/grid_multi_layer_basic.py
Normal file
114
docs/cookbook/grid/grid_multi_layer_basic.py
Normal 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)
|
||||
38
docs/cookbook/grid/grid_multi_layer_complete.py
Normal file
38
docs/cookbook/grid/grid_multi_layer_complete.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue