646 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			646 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Python
		
	
	
	
"""
 | 
						|
McRogueFace Tutorial - Part 6: Turn-based enemy movement
 | 
						|
 | 
						|
This tutorial builds on Part 5 by adding:
 | 
						|
- Turn cycles where enemies move after the player
 | 
						|
- Enemy AI that pursues or wanders
 | 
						|
- Shared collision detection for all entities
 | 
						|
"""
 | 
						|
import mcrfpy
 | 
						|
import random
 | 
						|
 | 
						|
# Create and activate a new scene
 | 
						|
mcrfpy.createScene("tutorial")
 | 
						|
mcrfpy.setScene("tutorial")
 | 
						|
 | 
						|
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
 | 
						|
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
 | 
						|
 | 
						|
# Load the hero sprite texture
 | 
						|
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
 | 
						|
 | 
						|
# Create a grid of tiles
 | 
						|
grid_width, grid_height = 40, 30
 | 
						|
 | 
						|
# Calculate the size in pixels
 | 
						|
zoom = 2.0
 | 
						|
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
 | 
						|
 | 
						|
# Calculate the position to center the grid on the screen
 | 
						|
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
 | 
						|
 | 
						|
# Create the grid with a TCODMap for pathfinding/FOV
 | 
						|
grid = mcrfpy.Grid(
 | 
						|
    pos=grid_position,
 | 
						|
    grid_size=(grid_width, grid_height),
 | 
						|
    texture=texture,
 | 
						|
    size=grid_size,
 | 
						|
)
 | 
						|
 | 
						|
grid.zoom = zoom
 | 
						|
 | 
						|
# Define tile types
 | 
						|
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
 | 
						|
WALL_TILES = [3, 7, 11]
 | 
						|
 | 
						|
# Room class for BSP
 | 
						|
class Room:
 | 
						|
    def __init__(self, x, y, w, h):
 | 
						|
        self.x1 = x
 | 
						|
        self.y1 = y
 | 
						|
        self.x2 = x + w
 | 
						|
        self.y2 = y + h
 | 
						|
        self.w = w
 | 
						|
        self.h = h
 | 
						|
    
 | 
						|
    def center(self):
 | 
						|
        center_x = (self.x1 + self.x2) // 2
 | 
						|
        center_y = (self.y1 + self.y2) // 2
 | 
						|
        return (center_x, center_y)
 | 
						|
    
 | 
						|
    def intersects(self, other):
 | 
						|
        return (self.x1 <= other.x2 and self.x2 >= other.x1 and
 | 
						|
                self.y1 <= other.y2 and self.y2 >= other.y1)
 | 
						|
 | 
						|
# Dungeon generation functions (from Part 3)
 | 
						|
def carve_room(room):
 | 
						|
    for x in range(room.x1, room.x2):
 | 
						|
        for y in range(room.y1, room.y2):
 | 
						|
            if 0 <= x < grid_width and 0 <= y < grid_height:
 | 
						|
                point = grid.at(x, y)
 | 
						|
                if point:
 | 
						|
                    point.tilesprite = random.choice(FLOOR_TILES)
 | 
						|
                    point.walkable = True
 | 
						|
                    point.transparent = True
 | 
						|
 | 
						|
def carve_hallway(x1, y1, x2, y2):
 | 
						|
    #points = mcrfpy.libtcod.line(x1, y1, x2, y2)
 | 
						|
    points = []
 | 
						|
    if random.choice([True, False]):
 | 
						|
        # x1,y1 -> x2,y1 -> x2,y2
 | 
						|
        points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
 | 
						|
        points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
 | 
						|
    else:
 | 
						|
        # x1,y1 -> x1,y2 -> x2,y2
 | 
						|
        points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
 | 
						|
        points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
 | 
						|
 | 
						|
    for x, y in points:
 | 
						|
        if 0 <= x < grid_width and 0 <= y < grid_height:
 | 
						|
            point = grid.at(x, y)
 | 
						|
            if point:
 | 
						|
                point.tilesprite = random.choice(FLOOR_TILES)
 | 
						|
                point.walkable = True
 | 
						|
                point.transparent = True
 | 
						|
 | 
						|
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
 | 
						|
    rooms = []
 | 
						|
    
 | 
						|
    # Fill with walls
 | 
						|
    for y in range(grid_height):
 | 
						|
        for x in range(grid_width):
 | 
						|
            point = grid.at(x, y)
 | 
						|
            if point:
 | 
						|
                point.tilesprite = random.choice(WALL_TILES)
 | 
						|
                point.walkable = False
 | 
						|
                point.transparent = False
 | 
						|
    
 | 
						|
    # Generate rooms
 | 
						|
    for _ in range(max_rooms):
 | 
						|
        w = random.randint(room_min_size, room_max_size)
 | 
						|
        h = random.randint(room_min_size, room_max_size)
 | 
						|
        x = random.randint(1, grid_width - w - 1)
 | 
						|
        y = random.randint(1, grid_height - h - 1)
 | 
						|
        
 | 
						|
        new_room = Room(x, y, w, h)
 | 
						|
        
 | 
						|
        failed = False
 | 
						|
        for other_room in rooms:
 | 
						|
            if new_room.intersects(other_room):
 | 
						|
                failed = True
 | 
						|
                break
 | 
						|
        
 | 
						|
        if not failed:
 | 
						|
            carve_room(new_room)
 | 
						|
            
 | 
						|
            if rooms:
 | 
						|
                prev_x, prev_y = rooms[-1].center()
 | 
						|
                new_x, new_y = new_room.center()
 | 
						|
                carve_hallway(prev_x, prev_y, new_x, new_y)
 | 
						|
            
 | 
						|
            rooms.append(new_room)
 | 
						|
    
 | 
						|
    return rooms
 | 
						|
 | 
						|
# Generate the dungeon
 | 
						|
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
 | 
						|
 | 
						|
# Add the grid to the scene
 | 
						|
mcrfpy.sceneUI("tutorial").append(grid)
 | 
						|
 | 
						|
# Spawn player in the first room
 | 
						|
if rooms:
 | 
						|
    spawn_x, spawn_y = rooms[0].center()
 | 
						|
else:
 | 
						|
    spawn_x, spawn_y = 4, 4
 | 
						|
 | 
						|
class GameEntity(mcrfpy.Entity):
 | 
						|
    """An entity whose default behavior is to prevent others from moving into its tile."""
 | 
						|
 | 
						|
    def __init__(self, x, y, walkable=False, **kwargs):
 | 
						|
        super().__init__(x=x, y=y, **kwargs)
 | 
						|
        self.walkable = walkable
 | 
						|
        self.dest_x = x
 | 
						|
        self.dest_y = y
 | 
						|
        self.is_moving = False
 | 
						|
 | 
						|
    def get_position(self):
 | 
						|
        """Get logical position (destination if moving, otherwise current)"""
 | 
						|
        if self.is_moving:
 | 
						|
            return (self.dest_x, self.dest_y)
 | 
						|
        return (int(self.x), int(self.y))
 | 
						|
 | 
						|
    def on_bump(self, other):
 | 
						|
        return self.walkable # allow other's motion to proceed if entity is walkable
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        return f"<{self.__class__.__name__} x={self.x}, y={self.y}, sprite_index={self.sprite_index}>"
 | 
						|
 | 
						|
class CombatEntity(GameEntity):
 | 
						|
    def __init__(self, x, y, hp=10, damage=(1,3), **kwargs):
 | 
						|
        super().__init__(x=x, y=y, **kwargs)
 | 
						|
        self.hp = hp
 | 
						|
        self.damage = damage
 | 
						|
 | 
						|
    def is_dead(self):
 | 
						|
        return self.hp <= 0
 | 
						|
    
 | 
						|
    def start_move(self, new_x, new_y, duration=0.2, callback=None):
 | 
						|
        """Start animating movement to new position"""
 | 
						|
        self.dest_x = new_x
 | 
						|
        self.dest_y = new_y
 | 
						|
        self.is_moving = True
 | 
						|
        
 | 
						|
        # Define completion callback that resets is_moving
 | 
						|
        def movement_done(anim, entity):
 | 
						|
            self.is_moving = False
 | 
						|
            if callback:
 | 
						|
                callback(anim, entity)
 | 
						|
        
 | 
						|
        # Create animations for smooth movement
 | 
						|
        anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad", callback=movement_done)
 | 
						|
        anim_y = mcrfpy.Animation("y", float(new_y), duration, "easeInOutQuad")
 | 
						|
        
 | 
						|
        anim_x.start(self)
 | 
						|
        anim_y.start(self)
 | 
						|
    
 | 
						|
    def can_see(self, target_x, target_y):
 | 
						|
        """Check if this entity can see the target position"""
 | 
						|
        mx, my = self.get_position()
 | 
						|
        
 | 
						|
        # Simple distance check first
 | 
						|
        dist = abs(target_x - mx) + abs(target_y - my)
 | 
						|
        if dist > 6:
 | 
						|
            return False
 | 
						|
        
 | 
						|
        # Line of sight check
 | 
						|
        line = list(mcrfpy.libtcod.line(mx, my, target_x, target_y))
 | 
						|
        for x, y in line[1:-1]:  # Skip start and end
 | 
						|
            cell = grid.at(x, y)
 | 
						|
            if cell and not cell.transparent:
 | 
						|
                return False
 | 
						|
        return True
 | 
						|
    
 | 
						|
    def ai_turn(self, player_pos):
 | 
						|
        """Decide next move"""
 | 
						|
        mx, my = self.get_position()
 | 
						|
        px, py = player_pos
 | 
						|
        
 | 
						|
        # Simple AI: move toward player if visible
 | 
						|
        if self.can_see(px, py):
 | 
						|
            # Calculate direction toward player
 | 
						|
            dx = 0
 | 
						|
            dy = 0
 | 
						|
            if px > mx:
 | 
						|
                dx = 1
 | 
						|
            elif px < mx:
 | 
						|
                dx = -1
 | 
						|
            if py > my:
 | 
						|
                dy = 1
 | 
						|
            elif py < my:
 | 
						|
                dy = -1
 | 
						|
                
 | 
						|
            # Prefer cardinal movement
 | 
						|
            if dx != 0 and dy != 0:
 | 
						|
                # Pick horizontal or vertical based on greater distance
 | 
						|
                if abs(px - mx) > abs(py - my):
 | 
						|
                    dy = 0
 | 
						|
                else:
 | 
						|
                    dx = 0
 | 
						|
                    
 | 
						|
            return (mx + dx, my + dy)
 | 
						|
        else:
 | 
						|
            # Random wander
 | 
						|
            dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)])
 | 
						|
            return (mx + dx, my + dy)
 | 
						|
    
 | 
						|
    def ai_turn_dijkstra(self):
 | 
						|
        """Decide next move using precomputed Dijkstra map"""
 | 
						|
        mx, my = self.get_position()
 | 
						|
        
 | 
						|
        # Get current distance to player
 | 
						|
        current_dist = grid.get_dijkstra_distance(mx, my)
 | 
						|
        if current_dist is None or current_dist > 20:
 | 
						|
            # Too far or unreachable - random wander
 | 
						|
            dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)])
 | 
						|
            return (mx + dx, my + dy)
 | 
						|
        
 | 
						|
        # Check all adjacent cells for best move
 | 
						|
        best_moves = []
 | 
						|
        for dx, dy in [(0,1), (0,-1), (1,0), (-1,0)]:
 | 
						|
            nx, ny = mx + dx, my + dy
 | 
						|
            
 | 
						|
            # Skip if out of bounds
 | 
						|
            if nx < 0 or nx >= grid_width or ny < 0 or ny >= grid_height:
 | 
						|
                continue
 | 
						|
                
 | 
						|
            # Skip if not walkable
 | 
						|
            cell = grid.at(nx, ny)
 | 
						|
            if not cell or not cell.walkable:
 | 
						|
                continue
 | 
						|
            
 | 
						|
            # Get distance from this cell
 | 
						|
            dist = grid.get_dijkstra_distance(nx, ny)
 | 
						|
            if dist is not None:
 | 
						|
                best_moves.append((dist, nx, ny))
 | 
						|
        
 | 
						|
        if best_moves:
 | 
						|
            # Sort by distance
 | 
						|
            best_moves.sort()
 | 
						|
            
 | 
						|
            # If multiple moves have the same best distance, pick randomly
 | 
						|
            best_dist = best_moves[0][0]
 | 
						|
            equal_moves = [(nx, ny) for dist, nx, ny in best_moves if dist == best_dist]
 | 
						|
            
 | 
						|
            if len(equal_moves) > 1:
 | 
						|
                # Random choice among equally good moves
 | 
						|
                nx, ny = random.choice(equal_moves)
 | 
						|
            else:
 | 
						|
                _, nx, ny = best_moves[0]
 | 
						|
                
 | 
						|
            return (nx, ny)
 | 
						|
        else:
 | 
						|
            # No valid moves
 | 
						|
            return (mx, my)
 | 
						|
 | 
						|
# Create a player entity
 | 
						|
player = CombatEntity(
 | 
						|
    spawn_x, spawn_y,
 | 
						|
    texture=hero_texture,
 | 
						|
    sprite_index=0
 | 
						|
)
 | 
						|
 | 
						|
# Add the player entity to the grid
 | 
						|
grid.entities.append(player)
 | 
						|
 | 
						|
# Track all enemies
 | 
						|
enemies = []
 | 
						|
 | 
						|
# Spawn enemies in other rooms
 | 
						|
for i, room in enumerate(rooms[1:], 1):  # Skip first room (player spawn)
 | 
						|
    if i <= 3:  # Limit to 3 enemies for now
 | 
						|
        enemy_x, enemy_y = room.center()
 | 
						|
        enemy = CombatEntity(
 | 
						|
            enemy_x, enemy_y,
 | 
						|
            texture=hero_texture,
 | 
						|
            sprite_index=0  # Enemy sprite (borrow player's)
 | 
						|
        )
 | 
						|
        grid.entities.append(enemy)
 | 
						|
        enemies.append(enemy)
 | 
						|
 | 
						|
# Set the grid perspective to the player by default
 | 
						|
# Note: The new perspective system uses entity references directly
 | 
						|
grid.perspective = player
 | 
						|
 | 
						|
# Initial FOV computation
 | 
						|
def update_fov():
 | 
						|
    """Update field of view from current perspective"""
 | 
						|
    if grid.perspective == player:
 | 
						|
        grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
 | 
						|
        player.update_visibility()
 | 
						|
 | 
						|
# Perform initial FOV calculation
 | 
						|
update_fov()
 | 
						|
 | 
						|
# Center grid on current perspective
 | 
						|
def center_on_perspective():
 | 
						|
    if grid.perspective == player:
 | 
						|
        grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
 | 
						|
 | 
						|
center_on_perspective()
 | 
						|
 | 
						|
# Movement state tracking (from Part 3)
 | 
						|
#is_moving = False # make it an entity property
 | 
						|
move_queue = []
 | 
						|
current_destination = None
 | 
						|
current_move = None
 | 
						|
 | 
						|
# Store animation references
 | 
						|
player_anim_x = None
 | 
						|
player_anim_y = None
 | 
						|
grid_anim_x = None
 | 
						|
grid_anim_y = None
 | 
						|
 | 
						|
def movement_complete(anim, target):
 | 
						|
    """Called when movement animation completes"""
 | 
						|
    global move_queue, current_destination, current_move
 | 
						|
    global player_anim_x, player_anim_y, is_player_turn
 | 
						|
    
 | 
						|
    player.is_moving = False
 | 
						|
    current_move = None
 | 
						|
    current_destination = None
 | 
						|
    player_anim_x = None
 | 
						|
    player_anim_y = None
 | 
						|
    
 | 
						|
    # Update FOV after movement
 | 
						|
    update_fov()
 | 
						|
    center_on_perspective()
 | 
						|
    
 | 
						|
    # Player turn complete, start enemy turns and queued player move simultaneously
 | 
						|
    is_player_turn = False
 | 
						|
    process_enemy_turns_and_player_queue()
 | 
						|
 | 
						|
motion_speed = 0.20
 | 
						|
is_player_turn = True  # Track whose turn it is
 | 
						|
 | 
						|
def get_blocking_entity_at(x, y):
 | 
						|
    """Get blocking entity at position"""
 | 
						|
    for e in grid.entities:
 | 
						|
        if not e.walkable and (x, y) == e.get_position():
 | 
						|
            return e
 | 
						|
    return None
 | 
						|
 | 
						|
def can_move_to(x, y, mover=None):
 | 
						|
    """Check if a position is valid for movement"""
 | 
						|
    if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
 | 
						|
        return False
 | 
						|
    
 | 
						|
    point = grid.at(x, y)
 | 
						|
    if not point or not point.walkable:
 | 
						|
        return False
 | 
						|
        
 | 
						|
    # Check for blocking entities
 | 
						|
    blocker = get_blocking_entity_at(x, y)
 | 
						|
    if blocker and blocker != mover:
 | 
						|
        return False
 | 
						|
        
 | 
						|
    return True
 | 
						|
 | 
						|
def process_enemy_turns_and_player_queue():
 | 
						|
    """Process all enemy AI decisions and player's queued move simultaneously"""
 | 
						|
    global is_player_turn, move_queue
 | 
						|
    
 | 
						|
    # Compute Dijkstra map once for all enemies (if using Dijkstra)
 | 
						|
    if USE_DIJKSTRA:
 | 
						|
        px, py = player.get_position()
 | 
						|
        grid.compute_dijkstra(px, py, diagonal_cost=1.41)
 | 
						|
    
 | 
						|
    enemies_to_move = []
 | 
						|
    claimed_positions = set()  # Track where enemies plan to move
 | 
						|
    
 | 
						|
    # Collect all enemy moves
 | 
						|
    for i, enemy in enumerate(enemies):
 | 
						|
        if enemy.is_dead():
 | 
						|
            continue
 | 
						|
            
 | 
						|
        # AI decides next move
 | 
						|
        if USE_DIJKSTRA:
 | 
						|
            target_x, target_y = enemy.ai_turn_dijkstra()
 | 
						|
        else:
 | 
						|
            target_x, target_y = enemy.ai_turn(player.get_position())
 | 
						|
        
 | 
						|
        # Check if move is valid and not claimed by another enemy
 | 
						|
        if can_move_to(target_x, target_y, enemy) and (target_x, target_y) not in claimed_positions:
 | 
						|
            enemies_to_move.append((enemy, target_x, target_y))
 | 
						|
            claimed_positions.add((target_x, target_y))
 | 
						|
    
 | 
						|
    # Start all enemy animations simultaneously
 | 
						|
    any_enemy_moved = False
 | 
						|
    if enemies_to_move:
 | 
						|
        for enemy, tx, ty in enemies_to_move:
 | 
						|
            enemy.start_move(tx, ty, duration=motion_speed)
 | 
						|
            any_enemy_moved = True
 | 
						|
    
 | 
						|
    # Process player's queued move at the same time
 | 
						|
    if move_queue:
 | 
						|
        next_move = move_queue.pop(0)
 | 
						|
        process_player_queued_move(next_move)
 | 
						|
    else:
 | 
						|
        # No queued move, set up callback to return control when animations finish
 | 
						|
        if any_enemy_moved:
 | 
						|
            # Wait for animations to complete
 | 
						|
            mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50)
 | 
						|
        else:
 | 
						|
            # No animations, return control immediately
 | 
						|
            is_player_turn = True
 | 
						|
 | 
						|
def process_player_queued_move(key):
 | 
						|
    """Process player's queued move during enemy turn"""
 | 
						|
    global current_move, current_destination
 | 
						|
    global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
 | 
						|
    
 | 
						|
    px, py = int(player.x), int(player.y)
 | 
						|
    new_x, new_y = px, py
 | 
						|
    
 | 
						|
    if key == "W" or key == "Up":
 | 
						|
        new_y -= 1
 | 
						|
    elif key == "S" or key == "Down":
 | 
						|
        new_y += 1
 | 
						|
    elif key == "A" or key == "Left":
 | 
						|
        new_x -= 1
 | 
						|
    elif key == "D" or key == "Right":
 | 
						|
        new_x += 1
 | 
						|
    
 | 
						|
    if new_x != px or new_y != py:
 | 
						|
        # Check destination at animation end time (considering enemy destinations)
 | 
						|
        future_blocker = get_future_blocking_entity_at(new_x, new_y)
 | 
						|
        
 | 
						|
        if future_blocker:
 | 
						|
            # Will bump at destination
 | 
						|
            # Schedule bump for when animations complete
 | 
						|
            mcrfpy.setTimer("delayed_bump", lambda t: handle_delayed_bump(future_blocker), int(motion_speed * 1000))
 | 
						|
        elif can_move_to(new_x, new_y, player):
 | 
						|
            # Valid move, start animation
 | 
						|
            player.is_moving = True
 | 
						|
            current_move = key
 | 
						|
            current_destination = (new_x, new_y)
 | 
						|
            player.dest_x = new_x
 | 
						|
            player.dest_y = new_y
 | 
						|
            
 | 
						|
            # Player animation with callback
 | 
						|
            player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=player_queued_move_complete)
 | 
						|
            player_anim_x.start(player)
 | 
						|
            player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
 | 
						|
            player_anim_y.start(player)
 | 
						|
            
 | 
						|
            # Move camera with player
 | 
						|
            grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
 | 
						|
            grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
 | 
						|
            grid_anim_x.start(grid)
 | 
						|
            grid_anim_y.start(grid)
 | 
						|
        else:
 | 
						|
            # Blocked by wall, wait for turn to complete
 | 
						|
            mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50)
 | 
						|
 | 
						|
def get_future_blocking_entity_at(x, y):
 | 
						|
    """Get entity that will be blocking at position after current animations"""
 | 
						|
    for e in grid.entities:
 | 
						|
        if not e.walkable and (x, y) == (e.dest_x, e.dest_y):
 | 
						|
            return e
 | 
						|
    return None
 | 
						|
 | 
						|
def handle_delayed_bump(entity):
 | 
						|
    """Handle bump after animations complete"""
 | 
						|
    global is_player_turn
 | 
						|
    entity.on_bump(player)
 | 
						|
    is_player_turn = True
 | 
						|
 | 
						|
def player_queued_move_complete(anim, target):
 | 
						|
    """Called when player's queued movement completes"""
 | 
						|
    global is_player_turn
 | 
						|
    player.is_moving = False
 | 
						|
    update_fov()
 | 
						|
    center_on_perspective()
 | 
						|
    is_player_turn = True
 | 
						|
 | 
						|
def check_turn_complete(timer_name):
 | 
						|
    """Check if all animations are complete"""
 | 
						|
    global is_player_turn
 | 
						|
    
 | 
						|
    # Check if any entity is still moving
 | 
						|
    if player.is_moving:
 | 
						|
        mcrfpy.setTimer("turn_complete", check_turn_complete, 50)
 | 
						|
        return
 | 
						|
        
 | 
						|
    for enemy in enemies:
 | 
						|
        if enemy.is_moving:
 | 
						|
            mcrfpy.setTimer("turn_complete", check_turn_complete, 50)
 | 
						|
            return
 | 
						|
    
 | 
						|
    # All done
 | 
						|
    is_player_turn = True
 | 
						|
 | 
						|
def process_move(key):
 | 
						|
    """Process a move based on the key"""
 | 
						|
    global current_move, current_destination, move_queue
 | 
						|
    global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y, is_player_turn
 | 
						|
    
 | 
						|
    # Only allow player movement on player's turn
 | 
						|
    if not is_player_turn:
 | 
						|
        return
 | 
						|
    
 | 
						|
    # Only allow player movement when in player perspective
 | 
						|
    if grid.perspective != player:
 | 
						|
        return
 | 
						|
    
 | 
						|
    if player.is_moving:
 | 
						|
        move_queue.clear()
 | 
						|
        move_queue.append(key)
 | 
						|
        return
 | 
						|
    
 | 
						|
    px, py = int(player.x), int(player.y)
 | 
						|
    new_x, new_y = px, py
 | 
						|
    
 | 
						|
    if key == "W" or key == "Up":
 | 
						|
        new_y -= 1
 | 
						|
    elif key == "S" or key == "Down":
 | 
						|
        new_y += 1
 | 
						|
    elif key == "A" or key == "Left":
 | 
						|
        new_x -= 1
 | 
						|
    elif key == "D" or key == "Right":
 | 
						|
        new_x += 1
 | 
						|
    
 | 
						|
    if new_x != px or new_y != py:
 | 
						|
        # Check what's at destination
 | 
						|
        blocker = get_blocking_entity_at(new_x, new_y)
 | 
						|
        
 | 
						|
        if blocker:
 | 
						|
            # Bump interaction (combat will go here later)
 | 
						|
            blocker.on_bump(player)
 | 
						|
            # Still counts as a turn
 | 
						|
            is_player_turn = False
 | 
						|
            process_enemy_turns_and_player_queue()
 | 
						|
        elif can_move_to(new_x, new_y, player):
 | 
						|
            player.is_moving = True
 | 
						|
            current_move = key
 | 
						|
            current_destination = (new_x, new_y)
 | 
						|
            player.dest_x = new_x
 | 
						|
            player.dest_y = new_y
 | 
						|
            
 | 
						|
            # Start player move animation
 | 
						|
            player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
 | 
						|
            player_anim_x.start(player)
 | 
						|
            player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
 | 
						|
            player_anim_y.start(player)
 | 
						|
            
 | 
						|
            # Move camera with player
 | 
						|
            grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
 | 
						|
            grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
 | 
						|
            grid_anim_x.start(grid)
 | 
						|
            grid_anim_y.start(grid)
 | 
						|
 | 
						|
def handle_keys(key, state):
 | 
						|
    """Handle keyboard input"""
 | 
						|
    if state == "start":
 | 
						|
        # Movement keys
 | 
						|
        if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
 | 
						|
            process_move(key)
 | 
						|
        
 | 
						|
# Register the keyboard handler
 | 
						|
mcrfpy.keypressScene(handle_keys)
 | 
						|
 | 
						|
# Add UI elements
 | 
						|
title = mcrfpy.Caption((320, 10),
 | 
						|
    text="McRogueFace Tutorial - Part 6: Turn-based Movement",
 | 
						|
)
 | 
						|
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
 | 
						|
mcrfpy.sceneUI("tutorial").append(title)
 | 
						|
 | 
						|
instructions = mcrfpy.Caption((150, 720),
 | 
						|
    text="Use WASD/Arrows to move. Enemies move after you!",
 | 
						|
)
 | 
						|
instructions.font_size = 18
 | 
						|
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
 | 
						|
mcrfpy.sceneUI("tutorial").append(instructions)
 | 
						|
 | 
						|
# Debug info
 | 
						|
debug_caption = mcrfpy.Caption((10, 40),
 | 
						|
    text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Enemies: {len(enemies)}",
 | 
						|
)
 | 
						|
debug_caption.font_size = 16
 | 
						|
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
 | 
						|
mcrfpy.sceneUI("tutorial").append(debug_caption)
 | 
						|
 | 
						|
# Update function for turn display
 | 
						|
def update_turn_display():
 | 
						|
    turn_text = "Player" if is_player_turn else "Enemy"
 | 
						|
    alive_enemies = sum(1 for e in enemies if not e.is_dead())
 | 
						|
    debug_caption.text = f"Grid: {grid_width}x{grid_height} | Turn: {turn_text} | Enemies: {alive_enemies}/{len(enemies)}"
 | 
						|
 | 
						|
# Configuration toggle
 | 
						|
USE_DIJKSTRA = True  # Set to False to use old line-of-sight AI
 | 
						|
 | 
						|
# Timer to update display
 | 
						|
def update_display(runtime):
 | 
						|
    update_turn_display()
 | 
						|
 | 
						|
mcrfpy.setTimer("display_update", update_display, 100)
 | 
						|
 | 
						|
print("Tutorial Part 6 loaded!")
 | 
						|
print("Turn-based movement system active!")
 | 
						|
print(f"Using {'Dijkstra' if USE_DIJKSTRA else 'Line-of-sight'} AI pathfinding")
 | 
						|
print("- Enemies move after the player")
 | 
						|
print("- Enemies pursue when they can see you" if not USE_DIJKSTRA else "- Enemies use optimal pathfinding")
 | 
						|
print("- Enemies wander when they can't" if not USE_DIJKSTRA else "- All enemies share one pathfinding map")
 | 
						|
print("Use WASD or Arrow keys to move!")
 |