Compare commits
No commits in common. "1149111f2d652ba91001d8d6ee5526717facf1b4" and "0938a53c4a250bbbe9d9f9e0c4963405b8817da7" have entirely different histories.
1149111f2d
...
0938a53c4a
|
@ -1,645 +0,0 @@
|
||||||
"""
|
|
||||||
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!")
|
|
Loading…
Reference in New Issue