feat(engine): implement perspective FOV, pathfinding, and GUI text widgets
Major Engine Enhancements: - Complete FOV (Field of View) system with perspective rendering - UIGrid.perspective property for entity-based visibility - Three-layer overlay colors (unexplored, explored, visible) - Per-entity visibility state tracking - Perfect knowledge updates only for explored areas - Advanced Pathfinding Integration - A* pathfinding implementation in UIGrid - Entity.path_to() method for direct pathfinding - Dijkstra maps for multi-target pathfinding - Path caching for performance optimization - GUI Text Input Widgets - TextInputWidget class with cursor, selection, scrolling - Improved widget with proper text rendering and input handling - Example showcase of multiple text input fields - Foundation for in-game console and chat systems - Performance & Architecture Improvements - PyTexture copy operations optimized - GameEngine update cycle refined - UIEntity property handling enhanced - UITestScene modernized Test Suite: - Interactive visibility demos showing FOV in action - Pathfinding comparison (A* vs Dijkstra) - Debug utilities for visibility and empty path handling - Sizzle reel demo combining pathfinding and vision - Multiple text input test scenarios This commit brings McRogueFace closer to a complete roguelike engine with essential features like line-of-sight, intelligent pathfinding, and interactive text input capabilities. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
051a2ca951
commit
d13153ddb4
25 changed files with 3317 additions and 225 deletions
375
tests/path_vision_fixed.py
Normal file
375
tests/path_vision_fixed.py
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Path & Vision Sizzle Reel (Fixed)
|
||||
=================================
|
||||
|
||||
Fixed version with proper animation chaining to prevent glitches.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
class PathAnimator:
|
||||
"""Handles step-by-step animation with proper completion tracking"""
|
||||
|
||||
def __init__(self, entity, name="animator"):
|
||||
self.entity = entity
|
||||
self.name = name
|
||||
self.path = []
|
||||
self.current_index = 0
|
||||
self.step_duration = 0.4
|
||||
self.animating = False
|
||||
self.on_step = None
|
||||
self.on_complete = None
|
||||
|
||||
def set_path(self, path):
|
||||
"""Set the path to animate along"""
|
||||
self.path = path
|
||||
self.current_index = 0
|
||||
|
||||
def start(self):
|
||||
"""Start animating"""
|
||||
if not self.path:
|
||||
return
|
||||
|
||||
self.animating = True
|
||||
self.current_index = 0
|
||||
self._move_to_next()
|
||||
|
||||
def stop(self):
|
||||
"""Stop animating"""
|
||||
self.animating = False
|
||||
mcrfpy.delTimer(f"{self.name}_check")
|
||||
|
||||
def _move_to_next(self):
|
||||
"""Move to next position in path"""
|
||||
if not self.animating or self.current_index >= len(self.path):
|
||||
self.animating = False
|
||||
if self.on_complete:
|
||||
self.on_complete()
|
||||
return
|
||||
|
||||
# Get next position
|
||||
x, y = self.path[self.current_index]
|
||||
|
||||
# Create animations
|
||||
anim_x = mcrfpy.Animation("x", float(x), self.step_duration, "easeInOut")
|
||||
anim_y = mcrfpy.Animation("y", float(y), self.step_duration, "easeInOut")
|
||||
|
||||
anim_x.start(self.entity)
|
||||
anim_y.start(self.entity)
|
||||
|
||||
# Update visibility
|
||||
self.entity.update_visibility()
|
||||
|
||||
# Callback for each step
|
||||
if self.on_step:
|
||||
self.on_step(self.current_index, x, y)
|
||||
|
||||
# Schedule next move
|
||||
delay = int(self.step_duration * 1000) + 50 # Add small buffer
|
||||
mcrfpy.setTimer(f"{self.name}_next", self._handle_next, delay)
|
||||
|
||||
def _handle_next(self, dt):
|
||||
"""Timer callback to move to next position"""
|
||||
self.current_index += 1
|
||||
mcrfpy.delTimer(f"{self.name}_next")
|
||||
self._move_to_next()
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
player = None
|
||||
enemy = None
|
||||
player_animator = None
|
||||
enemy_animator = None
|
||||
demo_phase = 0
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo environment"""
|
||||
global grid, player, enemy
|
||||
|
||||
mcrfpy.createScene("fixed_demo")
|
||||
|
||||
# Create grid
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||
|
||||
# Simple dungeon layout
|
||||
map_layout = [
|
||||
"##############################",
|
||||
"#......#########.....#########",
|
||||
"#......#########.....#########",
|
||||
"#......#.........#...#########",
|
||||
"#......#.........#...#########",
|
||||
"####.###.........#.###########",
|
||||
"####.............#.###########",
|
||||
"####.............#.###########",
|
||||
"####.###.........#.###########",
|
||||
"#......#.........#...#########",
|
||||
"#......#.........#...#########",
|
||||
"#......#########.#...........#",
|
||||
"#......#########.#...........#",
|
||||
"#......#########.#...........#",
|
||||
"#......#########.#############",
|
||||
"####.###########.............#",
|
||||
"####.........................#",
|
||||
"####.###########.............#",
|
||||
"#......#########.............#",
|
||||
"##############################",
|
||||
]
|
||||
|
||||
# Build map
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
if char == '#':
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = mcrfpy.Color(40, 30, 30)
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = mcrfpy.Color(80, 80, 100)
|
||||
|
||||
# Create entities
|
||||
player = mcrfpy.Entity(3, 3, grid=grid)
|
||||
player.sprite_index = 64 # @
|
||||
|
||||
enemy = mcrfpy.Entity(26, 16, grid=grid)
|
||||
enemy.sprite_index = 69 # E
|
||||
|
||||
# Initial visibility
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
|
||||
# Set initial perspective
|
||||
grid.perspective = 0
|
||||
|
||||
def setup_ui():
|
||||
"""Create UI elements"""
|
||||
ui = mcrfpy.sceneUI("fixed_demo")
|
||||
ui.append(grid)
|
||||
|
||||
grid.position = (50, 80)
|
||||
grid.size = (700, 500)
|
||||
|
||||
title = mcrfpy.Caption("Path & Vision Demo (Fixed)", 300, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
global status_text, perspective_text
|
||||
status_text = mcrfpy.Caption("Initializing...", 50, 50)
|
||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status_text)
|
||||
|
||||
perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50)
|
||||
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
ui.append(perspective_text)
|
||||
|
||||
controls = mcrfpy.Caption("Space: Start/Pause | R: Restart | Q: Quit", 250, 600)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
def update_camera_smooth(target, duration=0.3):
|
||||
"""Smoothly move camera to entity"""
|
||||
center_x = target.x * 23 # Approximate pixel size
|
||||
center_y = target.y * 23
|
||||
|
||||
cam_anim = mcrfpy.Animation("center", (center_x, center_y), duration, "easeOut")
|
||||
cam_anim.start(grid)
|
||||
|
||||
def start_demo():
|
||||
"""Start the demo sequence"""
|
||||
global demo_phase, player_animator, enemy_animator
|
||||
|
||||
demo_phase = 1
|
||||
status_text.text = "Phase 1: Player movement with camera follow"
|
||||
|
||||
# Player path
|
||||
player_path = [
|
||||
(3, 3), (3, 6), (4, 6), (7, 6), (7, 8),
|
||||
(10, 8), (13, 8), (16, 8), (16, 10),
|
||||
(16, 13), (16, 16), (20, 16), (24, 16)
|
||||
]
|
||||
|
||||
# Setup player animator
|
||||
player_animator = PathAnimator(player, "player")
|
||||
player_animator.set_path(player_path)
|
||||
player_animator.step_duration = 0.5
|
||||
|
||||
def on_player_step(index, x, y):
|
||||
"""Called for each player step"""
|
||||
status_text.text = f"Player step {index+1}/{len(player_path)}"
|
||||
if grid.perspective == 0:
|
||||
update_camera_smooth(player, 0.4)
|
||||
|
||||
def on_player_complete():
|
||||
"""Called when player path is complete"""
|
||||
start_phase_2()
|
||||
|
||||
player_animator.on_step = on_player_step
|
||||
player_animator.on_complete = on_player_complete
|
||||
player_animator.start()
|
||||
|
||||
def start_phase_2():
|
||||
"""Start enemy movement phase"""
|
||||
global demo_phase
|
||||
|
||||
demo_phase = 2
|
||||
status_text.text = "Phase 2: Enemy movement (may enter player's view)"
|
||||
|
||||
# Enemy path
|
||||
enemy_path = [
|
||||
(26, 16), (22, 16), (18, 16), (16, 16),
|
||||
(16, 13), (16, 10), (16, 8), (13, 8),
|
||||
(10, 8), (7, 8), (7, 6), (4, 6)
|
||||
]
|
||||
|
||||
# Setup enemy animator
|
||||
enemy_animator.set_path(enemy_path)
|
||||
enemy_animator.step_duration = 0.4
|
||||
|
||||
def on_enemy_step(index, x, y):
|
||||
"""Check if enemy is visible to player"""
|
||||
if grid.perspective == 0:
|
||||
# Check if enemy is in player's view
|
||||
enemy_idx = int(y) * grid.grid_x + int(x)
|
||||
if enemy_idx < len(player.gridstate) and player.gridstate[enemy_idx].visible:
|
||||
status_text.text = "Enemy spotted in player's view!"
|
||||
|
||||
def on_enemy_complete():
|
||||
"""Start perspective transition"""
|
||||
start_phase_3()
|
||||
|
||||
enemy_animator.on_step = on_enemy_step
|
||||
enemy_animator.on_complete = on_enemy_complete
|
||||
enemy_animator.start()
|
||||
|
||||
def start_phase_3():
|
||||
"""Dramatic perspective shift"""
|
||||
global demo_phase
|
||||
|
||||
demo_phase = 3
|
||||
status_text.text = "Phase 3: Perspective shift..."
|
||||
|
||||
# Stop any ongoing animations
|
||||
player_animator.stop()
|
||||
enemy_animator.stop()
|
||||
|
||||
# Zoom out
|
||||
zoom_out = mcrfpy.Animation("zoom", 0.6, 2.0, "easeInExpo")
|
||||
zoom_out.start(grid)
|
||||
|
||||
# Schedule perspective switch
|
||||
mcrfpy.setTimer("switch_persp", switch_perspective, 2100)
|
||||
|
||||
def switch_perspective(dt):
|
||||
"""Switch to enemy perspective"""
|
||||
grid.perspective = 1
|
||||
perspective_text.text = "Perspective: Enemy"
|
||||
perspective_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
|
||||
# Update camera
|
||||
update_camera_smooth(enemy, 0.5)
|
||||
|
||||
# Zoom back in
|
||||
zoom_in = mcrfpy.Animation("zoom", 1.0, 2.0, "easeOutExpo")
|
||||
zoom_in.start(grid)
|
||||
|
||||
status_text.text = "Now following enemy perspective"
|
||||
|
||||
# Clean up timer
|
||||
mcrfpy.delTimer("switch_persp")
|
||||
|
||||
# Continue enemy movement after transition
|
||||
mcrfpy.setTimer("continue_enemy", continue_enemy_movement, 2500)
|
||||
|
||||
def continue_enemy_movement(dt):
|
||||
"""Continue enemy movement after perspective shift"""
|
||||
mcrfpy.delTimer("continue_enemy")
|
||||
|
||||
# Continue path
|
||||
enemy_path_2 = [
|
||||
(4, 6), (3, 6), (3, 3), (3, 2), (3, 1)
|
||||
]
|
||||
|
||||
enemy_animator.set_path(enemy_path_2)
|
||||
|
||||
def on_step(index, x, y):
|
||||
update_camera_smooth(enemy, 0.4)
|
||||
status_text.text = f"Following enemy: step {index+1}"
|
||||
|
||||
def on_complete():
|
||||
status_text.text = "Demo complete! Press R to restart"
|
||||
|
||||
enemy_animator.on_step = on_step
|
||||
enemy_animator.on_complete = on_complete
|
||||
enemy_animator.start()
|
||||
|
||||
# Control state
|
||||
running = False
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
global running
|
||||
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
key = key.lower()
|
||||
|
||||
if key == "q":
|
||||
sys.exit(0)
|
||||
elif key == "space":
|
||||
if not running:
|
||||
running = True
|
||||
start_demo()
|
||||
else:
|
||||
running = False
|
||||
player_animator.stop()
|
||||
enemy_animator.stop()
|
||||
status_text.text = "Paused"
|
||||
elif key == "r":
|
||||
# Reset everything
|
||||
player.x, player.y = 3, 3
|
||||
enemy.x, enemy.y = 26, 16
|
||||
grid.perspective = 0
|
||||
perspective_text.text = "Perspective: Player"
|
||||
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
grid.zoom = 1.0
|
||||
update_camera_smooth(player, 0.5)
|
||||
|
||||
if running:
|
||||
player_animator.stop()
|
||||
enemy_animator.stop()
|
||||
running = False
|
||||
|
||||
status_text.text = "Reset - Press SPACE to start"
|
||||
|
||||
# Initialize
|
||||
create_scene()
|
||||
setup_ui()
|
||||
|
||||
# Setup animators
|
||||
player_animator = PathAnimator(player, "player")
|
||||
enemy_animator = PathAnimator(enemy, "enemy")
|
||||
|
||||
# Set scene
|
||||
mcrfpy.setScene("fixed_demo")
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Initial camera
|
||||
grid.zoom = 1.0
|
||||
update_camera_smooth(player, 0.5)
|
||||
|
||||
print("Path & Vision Demo (Fixed)")
|
||||
print("==========================")
|
||||
print("This version properly chains animations to prevent glitches.")
|
||||
print()
|
||||
print("The demo will:")
|
||||
print("1. Move player with camera following")
|
||||
print("2. Move enemy (may enter player's view)")
|
||||
print("3. Dramatic perspective shift to enemy")
|
||||
print("4. Continue following enemy")
|
||||
print()
|
||||
print("Press SPACE to start, Q to quit")
|
||||
Loading…
Add table
Add a link
Reference in a new issue