Compare commits
No commits in common. "0938a53c4a250bbbe9d9f9e0c4963405b8817da7" and "bd6407db29aee1b7d6993b9353653668855d9458" have entirely different histories.
0938a53c4a
...
bd6407db29
|
@ -1,313 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 3: Procedural Dungeon Generation
|
|
||||||
|
|
||||||
This tutorial builds on Part 2 by adding:
|
|
||||||
- Binary Space Partition (BSP) dungeon generation
|
|
||||||
- Rooms connected by hallways using libtcod.line()
|
|
||||||
- Walkable/non-walkable terrain
|
|
||||||
- Player spawning in a valid location
|
|
||||||
- Wall tiles that block movement
|
|
||||||
|
|
||||||
Key code references:
|
|
||||||
- src/scripts/cos_level.py (lines 7-15, 184-217, 218-224) - BSP algorithm
|
|
||||||
- mcrfpy.libtcod.line() for smooth hallway generation
|
|
||||||
"""
|
|
||||||
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 # Larger grid for dungeon
|
|
||||||
|
|
||||||
# Calculate the size in pixels to fit the entire grid on-screen
|
|
||||||
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):
|
|
||||||
"""Return the center coordinates of the room"""
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return (center_x, center_y)
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
"""Return True if this room overlaps with another"""
|
|
||||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
|
||||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
|
||||||
|
|
||||||
# Dungeon generation functions
|
|
||||||
def carve_room(room):
|
|
||||||
"""Carve out a room in the grid - referenced from cos_level.py lines 117-120"""
|
|
||||||
# Using individual updates for now (batch updates would be more efficient)
|
|
||||||
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):
|
|
||||||
"""Carve a hallway between two points using libtcod.line()
|
|
||||||
Referenced from cos_level.py lines 184-217, improved with libtcod.line()
|
|
||||||
"""
|
|
||||||
# Get all points along the line
|
|
||||||
|
|
||||||
# Simple solution: works if your characters have diagonal movement
|
|
||||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
|
||||||
|
|
||||||
# We don't, so we're going to carve a path with an elbow in it
|
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
# Carve out each point
|
|
||||||
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):
|
|
||||||
"""Generate a dungeon using simplified BSP approach
|
|
||||||
Referenced from cos_level.py lines 218-224
|
|
||||||
"""
|
|
||||||
rooms = []
|
|
||||||
|
|
||||||
# First, fill everything 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):
|
|
||||||
# Random room size
|
|
||||||
w = random.randint(room_min_size, room_max_size)
|
|
||||||
h = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
# Random position (with margin from edges)
|
|
||||||
x = random.randint(1, grid_width - w - 1)
|
|
||||||
y = random.randint(1, grid_height - h - 1)
|
|
||||||
|
|
||||||
new_room = Room(x, y, w, h)
|
|
||||||
|
|
||||||
# Check if it overlaps with existing rooms
|
|
||||||
failed = False
|
|
||||||
for other_room in rooms:
|
|
||||||
if new_room.intersects(other_room):
|
|
||||||
failed = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not failed:
|
|
||||||
# Carve out the room
|
|
||||||
carve_room(new_room)
|
|
||||||
|
|
||||||
# If not the first room, connect to previous room
|
|
||||||
if rooms:
|
|
||||||
# Get centers
|
|
||||||
prev_x, prev_y = rooms[-1].center()
|
|
||||||
new_x, new_y = new_room.center()
|
|
||||||
|
|
||||||
# Carve hallway using libtcod.line()
|
|
||||||
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:
|
|
||||||
# Fallback spawn position
|
|
||||||
spawn_x, spawn_y = 4, 4
|
|
||||||
|
|
||||||
# Create a player entity at the spawn position
|
|
||||||
player = mcrfpy.Entity(
|
|
||||||
(spawn_x, spawn_y),
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
|
|
||||||
# Movement state tracking (from Part 2)
|
|
||||||
is_moving = False
|
|
||||||
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 is_moving, move_queue, current_destination, current_move
|
|
||||||
global player_anim_x, player_anim_y
|
|
||||||
|
|
||||||
is_moving = False
|
|
||||||
current_move = None
|
|
||||||
current_destination = None
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
|
|
||||||
if move_queue:
|
|
||||||
next_move = move_queue.pop(0)
|
|
||||||
process_move(next_move)
|
|
||||||
|
|
||||||
motion_speed = 0.20 # Slightly faster for dungeon exploration
|
|
||||||
|
|
||||||
def can_move_to(x, y):
|
|
||||||
"""Check if a position is valid for movement"""
|
|
||||||
# Boundary check
|
|
||||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Walkability check
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point and point.walkable:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def process_move(key):
|
|
||||||
"""Process a move based on the key"""
|
|
||||||
global is_moving, current_move, current_destination, move_queue
|
|
||||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
|
||||||
|
|
||||||
if 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
|
|
||||||
|
|
||||||
# Check if we can move to the new position
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
if can_move_to(new_x, new_y):
|
|
||||||
is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
|
|
||||||
if new_x != px:
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
elif new_y != py:
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_y.start(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:
|
|
||||||
# Play a "bump" sound or visual feedback here
|
|
||||||
print(f"Can't move to ({new_x}, {new_y}) - blocked!")
|
|
||||||
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input to move the player"""
|
|
||||||
if state == "start":
|
|
||||||
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 3: Dungeon Generation",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption((150, 750),
|
|
||||||
text=f"Procedural dungeon with {len(rooms)} rooms connected by hallways!",
|
|
||||||
)
|
|
||||||
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} | Player spawned at ({spawn_x}, {spawn_y})",
|
|
||||||
)
|
|
||||||
debug_caption.font_size = 16
|
|
||||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
|
||||||
|
|
||||||
print("Tutorial Part 3 loaded!")
|
|
||||||
print(f"Generated dungeon with {len(rooms)} rooms")
|
|
||||||
print(f"Player spawned at ({spawn_x}, {spawn_y})")
|
|
||||||
print("Walls now block movement!")
|
|
||||||
print("Use WASD or Arrow keys to explore the dungeon!")
|
|
|
@ -1,366 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 4: Field of View
|
|
||||||
|
|
||||||
This tutorial builds on Part 3 by adding:
|
|
||||||
- Field of view calculation using grid.compute_fov()
|
|
||||||
- Entity perspective rendering with grid.perspective
|
|
||||||
- Three visibility states: unexplored (black), explored (dark), visible (lit)
|
|
||||||
- Memory of previously seen areas
|
|
||||||
- Enemy entity to demonstrate perspective switching
|
|
||||||
|
|
||||||
Key code references:
|
|
||||||
- tests/unit/test_tcod_fov_entities.py (lines 89-118) - FOV with multiple entities
|
|
||||||
- ROADMAP.md (lines 216-229) - FOV system implementation details
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
# Create a player entity
|
|
||||||
player = mcrfpy.Entity(
|
|
||||||
(spawn_x, spawn_y),
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
|
|
||||||
# Create an enemy entity in another room (to demonstrate perspective switching)
|
|
||||||
enemy = None
|
|
||||||
if len(rooms) > 1:
|
|
||||||
enemy_x, enemy_y = rooms[1].center()
|
|
||||||
enemy = mcrfpy.Entity(
|
|
||||||
(enemy_x, enemy_y),
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Enemy sprite
|
|
||||||
)
|
|
||||||
grid.entities.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
|
|
||||||
Referenced from test_tcod_fov_entities.py lines 89-118
|
|
||||||
"""
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
|
|
||||||
player.update_visibility()
|
|
||||||
elif enemy and grid.perspective == enemy:
|
|
||||||
grid.compute_fov(int(enemy.x), int(enemy.y), radius=6, algorithm=0)
|
|
||||||
enemy.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
|
|
||||||
elif enemy and grid.perspective == enemy:
|
|
||||||
grid.center = (enemy.x + 0.5) * 16, (enemy.y + 0.5) * 16
|
|
||||||
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
# Movement state tracking (from Part 3)
|
|
||||||
is_moving = False
|
|
||||||
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 is_moving, move_queue, current_destination, current_move
|
|
||||||
global player_anim_x, player_anim_y
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
if move_queue:
|
|
||||||
next_move = move_queue.pop(0)
|
|
||||||
process_move(next_move)
|
|
||||||
|
|
||||||
motion_speed = 0.20
|
|
||||||
|
|
||||||
def can_move_to(x, y):
|
|
||||||
"""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 point and point.walkable:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def process_move(key):
|
|
||||||
"""Process a move based on the key"""
|
|
||||||
global is_moving, current_move, current_destination, move_queue
|
|
||||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
|
||||||
|
|
||||||
# Only allow player movement when in player perspective
|
|
||||||
if grid.perspective != player:
|
|
||||||
return
|
|
||||||
|
|
||||||
if 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:
|
|
||||||
if can_move_to(new_x, new_y):
|
|
||||||
is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
|
|
||||||
if new_x != px:
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
elif new_y != py:
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_y.start(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)
|
|
||||||
|
|
||||||
# Perspective switching
|
|
||||||
elif key == "Tab":
|
|
||||||
# Switch perspective between player and enemy
|
|
||||||
if enemy:
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.perspective = enemy
|
|
||||||
print("Switched to enemy perspective")
|
|
||||||
else:
|
|
||||||
grid.perspective = player
|
|
||||||
print("Switched to player perspective")
|
|
||||||
|
|
||||||
# Update FOV and camera for new perspective
|
|
||||||
update_fov()
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add UI elements
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 4: Field of View",
|
|
||||||
)
|
|
||||||
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. Press Tab to switch perspective!",
|
|
||||||
)
|
|
||||||
instructions.font_size = 18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
# FOV info
|
|
||||||
fov_caption = mcrfpy.Caption((150, 745),
|
|
||||||
text="FOV: Player (radius 8) | Enemy visible in other room",
|
|
||||||
)
|
|
||||||
fov_caption.font_size = 16
|
|
||||||
fov_caption.fill_color = mcrfpy.Color(100, 200, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(fov_caption)
|
|
||||||
|
|
||||||
# Debug info
|
|
||||||
debug_caption = mcrfpy.Caption((10, 40),
|
|
||||||
text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: Player",
|
|
||||||
)
|
|
||||||
debug_caption.font_size = 16
|
|
||||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
|
||||||
|
|
||||||
# Update function for perspective display
|
|
||||||
def update_perspective_display():
|
|
||||||
current_perspective = "Player" if grid.perspective == player else "Enemy"
|
|
||||||
debug_caption.text = f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: {current_perspective}"
|
|
||||||
|
|
||||||
if grid.perspective == player:
|
|
||||||
fov_caption.text = "FOV: Player (radius 8) | Tab to switch perspective"
|
|
||||||
else:
|
|
||||||
fov_caption.text = "FOV: Enemy (radius 6) | Tab to switch perspective"
|
|
||||||
|
|
||||||
# Timer to update display
|
|
||||||
def update_display(runtime):
|
|
||||||
update_perspective_display()
|
|
||||||
|
|
||||||
mcrfpy.setTimer("display_update", update_display, 100)
|
|
||||||
|
|
||||||
print("Tutorial Part 4 loaded!")
|
|
||||||
print("Field of View system active!")
|
|
||||||
print("- Unexplored areas are black")
|
|
||||||
print("- Previously seen areas are dark")
|
|
||||||
print("- Currently visible areas are lit")
|
|
||||||
print("Press Tab to switch between player and enemy perspective!")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
|
@ -1,363 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 5: Interacting with other entities
|
|
||||||
|
|
||||||
This tutorial builds on Part 4 by adding:
|
|
||||||
- Subclassing mcrfpy.Entity
|
|
||||||
- Non-blocking movement animations with destination tracking
|
|
||||||
- Bump interactions (combat, pushing)
|
|
||||||
"""
|
|
||||||
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 BumpableEntity(GameEntity):
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
super().__init__(x, y, **kwargs)
|
|
||||||
|
|
||||||
def on_bump(self, other):
|
|
||||||
print(f"Watch it, {other}! You bumped into {self}!")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Create a player entity
|
|
||||||
player = GameEntity(
|
|
||||||
spawn_x, spawn_y,
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
for r in rooms:
|
|
||||||
enemy_x, enemy_y = r.center()
|
|
||||||
enemy = BumpableEntity(
|
|
||||||
enemy_x, enemy_y,
|
|
||||||
grid=grid,
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Enemy sprite
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
Referenced from test_tcod_fov_entities.py lines 89-118
|
|
||||||
"""
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
|
|
||||||
player.update_visibility()
|
|
||||||
elif enemy and grid.perspective == enemy:
|
|
||||||
grid.compute_fov(int(enemy.x), int(enemy.y), radius=6, algorithm=0)
|
|
||||||
enemy.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
|
|
||||||
elif enemy and grid.perspective == enemy:
|
|
||||||
grid.center = (enemy.x + 0.5) * 16, (enemy.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
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
if move_queue:
|
|
||||||
next_move = move_queue.pop(0)
|
|
||||||
process_move(next_move)
|
|
||||||
|
|
||||||
motion_speed = 0.20
|
|
||||||
|
|
||||||
def can_move_to(x, y):
|
|
||||||
"""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 point and point.walkable:
|
|
||||||
for e in grid.entities:
|
|
||||||
if not e.walkable and (x, y) == e.get_position(): # blocking the way
|
|
||||||
e.on_bump(player)
|
|
||||||
return False
|
|
||||||
return True # all checks passed, no collision
|
|
||||||
return False
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
if can_move_to(new_x, new_y):
|
|
||||||
player.is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
|
|
||||||
if new_x != px:
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
elif new_y != py:
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_y.start(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 5: Entity Collision",
|
|
||||||
)
|
|
||||||
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. Try to bump into the other entity!",
|
|
||||||
)
|
|
||||||
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)} | Perspective: Player",
|
|
||||||
)
|
|
||||||
debug_caption.font_size = 16
|
|
||||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
|
||||||
|
|
||||||
# Update function for perspective display
|
|
||||||
def update_perspective_display():
|
|
||||||
current_perspective = "Player" if grid.perspective == player else "Enemy"
|
|
||||||
debug_caption.text = f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: {current_perspective}"
|
|
||||||
|
|
||||||
# Timer to update display
|
|
||||||
def update_display(runtime):
|
|
||||||
update_perspective_display()
|
|
||||||
|
|
||||||
mcrfpy.setTimer("display_update", update_display, 100)
|
|
||||||
|
|
||||||
print("Tutorial Part 4 loaded!")
|
|
||||||
print("Field of View system active!")
|
|
||||||
print("- Unexplored areas are black")
|
|
||||||
print("- Previously seen areas are dark")
|
|
||||||
print("- Currently visible areas are lit")
|
|
||||||
print("Press Tab to switch between player and enemy perspective!")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
|
@ -138,67 +138,47 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get type objects from the module to ensure they're initialized
|
// Check type by comparing type names
|
||||||
PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
|
const char* type_name = Py_TYPE(target_obj)->tp_name;
|
||||||
PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
|
|
||||||
PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
|
|
||||||
PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
|
|
||||||
PyObject* entity_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
||||||
|
|
||||||
bool handled = false;
|
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
|
||||||
|
|
||||||
// Use PyObject_IsInstance to support inheritance
|
|
||||||
if (frame_type && PyObject_IsInstance(target_obj, frame_type)) {
|
|
||||||
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
|
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
|
||||||
if (frame->data) {
|
if (frame->data) {
|
||||||
self->data->start(frame->data);
|
self->data->start(frame->data);
|
||||||
AnimationManager::getInstance().addAnimation(self->data);
|
AnimationManager::getInstance().addAnimation(self->data);
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (caption_type && PyObject_IsInstance(target_obj, caption_type)) {
|
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
|
||||||
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
|
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
|
||||||
if (caption->data) {
|
if (caption->data) {
|
||||||
self->data->start(caption->data);
|
self->data->start(caption->data);
|
||||||
AnimationManager::getInstance().addAnimation(self->data);
|
AnimationManager::getInstance().addAnimation(self->data);
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (sprite_type && PyObject_IsInstance(target_obj, sprite_type)) {
|
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
|
||||||
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
|
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
|
||||||
if (sprite->data) {
|
if (sprite->data) {
|
||||||
self->data->start(sprite->data);
|
self->data->start(sprite->data);
|
||||||
AnimationManager::getInstance().addAnimation(self->data);
|
AnimationManager::getInstance().addAnimation(self->data);
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (grid_type && PyObject_IsInstance(target_obj, grid_type)) {
|
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
|
||||||
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
|
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
|
||||||
if (grid->data) {
|
if (grid->data) {
|
||||||
self->data->start(grid->data);
|
self->data->start(grid->data);
|
||||||
AnimationManager::getInstance().addAnimation(self->data);
|
AnimationManager::getInstance().addAnimation(self->data);
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (entity_type && PyObject_IsInstance(target_obj, entity_type)) {
|
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
|
||||||
// Special handling for Entity since it doesn't inherit from UIDrawable
|
// Special handling for Entity since it doesn't inherit from UIDrawable
|
||||||
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
|
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
|
||||||
if (entity->data) {
|
if (entity->data) {
|
||||||
self->data->startEntity(entity->data);
|
self->data->startEntity(entity->data);
|
||||||
AnimationManager::getInstance().addAnimation(self->data);
|
AnimationManager::getInstance().addAnimation(self->data);
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
// Clean up references
|
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
|
||||||
Py_XDECREF(frame_type);
|
|
||||||
Py_XDECREF(caption_type);
|
|
||||||
Py_XDECREF(sprite_type);
|
|
||||||
Py_XDECREF(grid_type);
|
|
||||||
Py_XDECREF(entity_type);
|
|
||||||
|
|
||||||
if (!handled) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity (or a subclass of these)");
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
257
src/UIGrid.cpp
257
src/UIGrid.cpp
|
@ -2,14 +2,13 @@
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "UIEntity.h"
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
|
|
||||||
UIGrid::UIGrid()
|
UIGrid::UIGrid()
|
||||||
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
|
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
|
||||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||||
perspective_enabled(false) // Default to omniscient view
|
perspective(-1) // Default to omniscient view
|
||||||
{
|
{
|
||||||
// Initialize entities list
|
// Initialize entities list
|
||||||
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
||||||
|
@ -37,7 +36,7 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
||||||
zoom(1.0f),
|
zoom(1.0f),
|
||||||
ptex(_ptex), points(gx * gy),
|
ptex(_ptex), points(gx * gy),
|
||||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||||
perspective_enabled(false) // Default to omniscient view
|
perspective(-1) // Default to omniscient view
|
||||||
{
|
{
|
||||||
// Use texture dimensions if available, otherwise use defaults
|
// Use texture dimensions if available, otherwise use defaults
|
||||||
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
|
@ -190,78 +189,54 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
|
|
||||||
|
|
||||||
// top layer - opacity for discovered / visible status based on perspective
|
// top layer - opacity for discovered / visible status based on perspective
|
||||||
// Only render visibility overlay if perspective is enabled
|
// Only render visibility overlay if perspective is set (not omniscient)
|
||||||
if (perspective_enabled) {
|
if (perspective >= 0 && perspective < static_cast<int>(entities->size())) {
|
||||||
auto entity = perspective_entity.lock();
|
// Get the entity whose perspective we're using
|
||||||
|
auto it = entities->begin();
|
||||||
|
std::advance(it, perspective);
|
||||||
|
auto& entity = *it;
|
||||||
|
|
||||||
// Create rectangle for overlays
|
// Create rectangle for overlays
|
||||||
sf::RectangleShape overlay;
|
sf::RectangleShape overlay;
|
||||||
overlay.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
overlay.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
||||||
|
|
||||||
if (entity) {
|
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
||||||
// Valid entity - use its gridstate for visibility
|
x < x_limit;
|
||||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
x+=1)
|
||||||
x < x_limit;
|
{
|
||||||
x+=1)
|
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
||||||
|
y < y_limit;
|
||||||
|
y+=1)
|
||||||
{
|
{
|
||||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
// Skip out-of-bounds cells
|
||||||
y < y_limit;
|
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
||||||
y+=1)
|
|
||||||
{
|
auto pixel_pos = sf::Vector2f(
|
||||||
// Skip out-of-bounds cells
|
(x*cell_width - left_spritepixels) * zoom,
|
||||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
(y*cell_height - top_spritepixels) * zoom );
|
||||||
|
|
||||||
auto pixel_pos = sf::Vector2f(
|
|
||||||
(x*cell_width - left_spritepixels) * zoom,
|
|
||||||
(y*cell_height - top_spritepixels) * zoom );
|
|
||||||
|
|
||||||
// Get visibility state from entity's perspective
|
// Get visibility state from entity's perspective
|
||||||
int idx = y * grid_x + x;
|
int idx = y * grid_x + x;
|
||||||
if (idx >= 0 && idx < static_cast<int>(entity->gridstate.size())) {
|
if (idx >= 0 && idx < static_cast<int>(entity->gridstate.size())) {
|
||||||
const auto& state = entity->gridstate[idx];
|
const auto& state = entity->gridstate[idx];
|
||||||
|
|
||||||
overlay.setPosition(pixel_pos);
|
|
||||||
|
|
||||||
// Three overlay colors as specified:
|
|
||||||
if (!state.discovered) {
|
|
||||||
// Never seen - black
|
|
||||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
|
||||||
renderTexture.draw(overlay);
|
|
||||||
} else if (!state.visible) {
|
|
||||||
// Discovered but not currently visible - dark gray
|
|
||||||
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
|
||||||
renderTexture.draw(overlay);
|
|
||||||
}
|
|
||||||
// If visible and discovered, no overlay (fully visible)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Invalid/destroyed entity with perspective_enabled = true
|
|
||||||
// Show all cells as undiscovered (black)
|
|
||||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
|
||||||
x < x_limit;
|
|
||||||
x+=1)
|
|
||||||
{
|
|
||||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
|
||||||
y < y_limit;
|
|
||||||
y+=1)
|
|
||||||
{
|
|
||||||
// Skip out-of-bounds cells
|
|
||||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
|
||||||
|
|
||||||
auto pixel_pos = sf::Vector2f(
|
|
||||||
(x*cell_width - left_spritepixels) * zoom,
|
|
||||||
(y*cell_height - top_spritepixels) * zoom );
|
|
||||||
|
|
||||||
overlay.setPosition(pixel_pos);
|
overlay.setPosition(pixel_pos);
|
||||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
|
||||||
renderTexture.draw(overlay);
|
// Three overlay colors as specified:
|
||||||
|
if (!state.discovered) {
|
||||||
|
// Never seen - black
|
||||||
|
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||||
|
renderTexture.draw(overlay);
|
||||||
|
} else if (!state.visible) {
|
||||||
|
// Discovered but not currently visible - dark gray
|
||||||
|
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
||||||
|
renderTexture.draw(overlay);
|
||||||
|
}
|
||||||
|
// If visible and discovered, no overlay (fully visible)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// else: omniscient view (no overlays)
|
|
||||||
|
|
||||||
// grid lines for testing & validation
|
// grid lines for testing & validation
|
||||||
/*
|
/*
|
||||||
|
@ -341,7 +316,6 @@ void UIGrid::computeFOV(int x, int y, int radius, bool light_walls, TCOD_fov_alg
|
||||||
{
|
{
|
||||||
if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return;
|
if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return;
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(fov_mutex);
|
|
||||||
tcod_map->computeFov(x, y, radius, light_walls, algo);
|
tcod_map->computeFov(x, y, radius, light_walls, algo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,7 +323,6 @@ bool UIGrid::isInFOV(int x, int y) const
|
||||||
{
|
{
|
||||||
if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return false;
|
if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return false;
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(fov_mutex);
|
|
||||||
return tcod_map->isInFov(x, y);
|
return tcod_map->isInFov(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -554,7 +527,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
PyObject* click_handler = nullptr;
|
PyObject* click_handler = nullptr;
|
||||||
float center_x = 0.0f, center_y = 0.0f;
|
float center_x = 0.0f, center_y = 0.0f;
|
||||||
float zoom = 1.0f;
|
float zoom = 1.0f;
|
||||||
// perspective is now handled via properties, not init args
|
int perspective = -1; // perspective is a difficult __init__ arg; needs an entity in collection to work
|
||||||
int visible = 1;
|
int visible = 1;
|
||||||
float opacity = 1.0f;
|
float opacity = 1.0f;
|
||||||
int z_index = 0;
|
int z_index = 0;
|
||||||
|
@ -566,15 +539,15 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
static const char* kwlist[] = {
|
static const char* kwlist[] = {
|
||||||
"pos", "size", "grid_size", "texture", // Positional args (as per spec)
|
"pos", "size", "grid_size", "texture", // Positional args (as per spec)
|
||||||
// Keyword-only args
|
// Keyword-only args
|
||||||
"fill_color", "click", "center_x", "center_y", "zoom",
|
"fill_color", "click", "center_x", "center_y", "zoom", "perspective",
|
||||||
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y",
|
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y",
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse arguments with | for optional positional args
|
// Parse arguments with | for optional positional args
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffii", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffiifizffffii", const_cast<char**>(kwlist),
|
||||||
&pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional
|
&pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional
|
||||||
&fill_color, &click_handler, ¢er_x, ¢er_y, &zoom,
|
&fill_color, &click_handler, ¢er_x, ¢er_y, &zoom, &perspective,
|
||||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) {
|
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@ -680,8 +653,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
self->data->center_x = center_x;
|
self->data->center_x = center_x;
|
||||||
self->data->center_y = center_y;
|
self->data->center_y = center_y;
|
||||||
self->data->zoom = zoom;
|
self->data->zoom = zoom;
|
||||||
// perspective is now handled by perspective_entity and perspective_enabled
|
self->data->perspective = perspective;
|
||||||
// self->data->perspective = perspective;
|
|
||||||
self->data->visible = visible;
|
self->data->visible = visible;
|
||||||
self->data->opacity = opacity;
|
self->data->opacity = opacity;
|
||||||
self->data->z_index = z_index;
|
self->data->z_index = z_index;
|
||||||
|
@ -969,77 +941,33 @@ int UIGrid::set_fill_color(PyUIGridObject* self, PyObject* value, void* closure)
|
||||||
|
|
||||||
PyObject* UIGrid::get_perspective(PyUIGridObject* self, void* closure)
|
PyObject* UIGrid::get_perspective(PyUIGridObject* self, void* closure)
|
||||||
{
|
{
|
||||||
auto locked = self->data->perspective_entity.lock();
|
return PyLong_FromLong(self->data->perspective);
|
||||||
if (locked) {
|
|
||||||
// Check cache first to preserve derived class
|
|
||||||
if (locked->serial_number != 0) {
|
|
||||||
PyObject* cached = PythonObjectCache::getInstance().lookup(locked->serial_number);
|
|
||||||
if (cached) {
|
|
||||||
return cached; // Already INCREF'd by lookup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy: If the entity has a stored Python object reference
|
|
||||||
if (locked->self != nullptr) {
|
|
||||||
Py_INCREF(locked->self);
|
|
||||||
return locked->self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, create a new base Entity object
|
|
||||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
||||||
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
|
||||||
if (o) {
|
|
||||||
o->data = locked;
|
|
||||||
o->weakreflist = NULL;
|
|
||||||
Py_DECREF(type);
|
|
||||||
return (PyObject*)o;
|
|
||||||
}
|
|
||||||
Py_XDECREF(type);
|
|
||||||
}
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure)
|
int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure)
|
||||||
{
|
{
|
||||||
if (value == Py_None) {
|
long perspective = PyLong_AsLong(value);
|
||||||
// Clear perspective but keep perspective_enabled unchanged
|
if (PyErr_Occurred()) {
|
||||||
self->data->perspective_entity.reset();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract UIEntity from PyObject
|
|
||||||
// Get the Entity type from the module
|
|
||||||
auto entity_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
||||||
if (!entity_type) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "Could not get Entity type from mcrfpy module");
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!PyObject_IsInstance(value, entity_type)) {
|
// Validate perspective (-1 for omniscient, or valid entity index)
|
||||||
Py_DECREF(entity_type);
|
if (perspective < -1) {
|
||||||
PyErr_SetString(PyExc_TypeError, "perspective must be a UIEntity or None");
|
PyErr_SetString(PyExc_ValueError, "perspective must be -1 (omniscient) or a valid entity index");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
Py_DECREF(entity_type);
|
|
||||||
|
|
||||||
PyUIEntityObject* entity_obj = (PyUIEntityObject*)value;
|
// Check if entity index is valid (if not omniscient)
|
||||||
self->data->perspective_entity = entity_obj->data;
|
if (perspective >= 0 && self->data->entities) {
|
||||||
self->data->perspective_enabled = true; // Enable perspective when entity assigned
|
int entity_count = self->data->entities->size();
|
||||||
return 0;
|
if (perspective >= entity_count) {
|
||||||
}
|
PyErr_Format(PyExc_IndexError, "perspective index %ld out of range (grid has %d entities)",
|
||||||
|
perspective, entity_count);
|
||||||
PyObject* UIGrid::get_perspective_enabled(PyUIGridObject* self, void* closure)
|
return -1;
|
||||||
{
|
}
|
||||||
return PyBool_FromLong(self->data->perspective_enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
int UIGrid::set_perspective_enabled(PyUIGridObject* self, PyObject* value, void* closure)
|
|
||||||
{
|
|
||||||
int enabled = PyObject_IsTrue(value);
|
|
||||||
if (enabled == -1) {
|
|
||||||
return -1; // Error occurred
|
|
||||||
}
|
}
|
||||||
self->data->perspective_enabled = enabled;
|
|
||||||
|
self->data->perspective = perspective;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1056,43 +984,8 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject*
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute FOV
|
|
||||||
self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
|
self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
|
||||||
|
Py_RETURN_NONE;
|
||||||
// Build list of visible cells as tuples (x, y, visible, discovered)
|
|
||||||
PyObject* result_list = PyList_New(0);
|
|
||||||
if (!result_list) return NULL;
|
|
||||||
|
|
||||||
// Iterate through grid and collect visible cells
|
|
||||||
for (int gy = 0; gy < self->data->grid_y; gy++) {
|
|
||||||
for (int gx = 0; gx < self->data->grid_x; gx++) {
|
|
||||||
if (self->data->isInFOV(gx, gy)) {
|
|
||||||
// Create tuple (x, y, visible, discovered)
|
|
||||||
PyObject* cell_tuple = PyTuple_New(4);
|
|
||||||
if (!cell_tuple) {
|
|
||||||
Py_DECREF(result_list);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyTuple_SET_ITEM(cell_tuple, 0, PyLong_FromLong(gx));
|
|
||||||
PyTuple_SET_ITEM(cell_tuple, 1, PyLong_FromLong(gy));
|
|
||||||
PyTuple_SET_ITEM(cell_tuple, 2, Py_True); // visible
|
|
||||||
PyTuple_SET_ITEM(cell_tuple, 3, Py_True); // discovered
|
|
||||||
Py_INCREF(Py_True); // Need to increment ref count for True
|
|
||||||
Py_INCREF(Py_True);
|
|
||||||
|
|
||||||
// Append to list
|
|
||||||
if (PyList_Append(result_list, cell_tuple) < 0) {
|
|
||||||
Py_DECREF(cell_tuple);
|
|
||||||
Py_DECREF(result_list);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
Py_DECREF(cell_tuple); // List now owns the reference
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result_list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args)
|
PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args)
|
||||||
|
@ -1210,20 +1103,16 @@ PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, Py
|
||||||
PyMethodDef UIGrid::methods[] = {
|
PyMethodDef UIGrid::methods[] = {
|
||||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
||||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]\n\n"
|
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
|
||||||
"Compute field of view from a position and return visible cells.\n\n"
|
"Compute field of view from a position.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" x: X coordinate of the viewer\n"
|
" x: X coordinate of the viewer\n"
|
||||||
" y: Y coordinate of the viewer\n"
|
" y: Y coordinate of the viewer\n"
|
||||||
" radius: Maximum view distance (0 = unlimited)\n"
|
" radius: Maximum view distance (0 = unlimited)\n"
|
||||||
" light_walls: Whether walls are lit when visible\n"
|
" light_walls: Whether walls are lit when visible\n"
|
||||||
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
||||||
"Returns:\n"
|
"Updates the internal FOV state. Use is_in_fov() to check visibility after calling this.\n"
|
||||||
" List of tuples (x, y, visible, discovered) for all visible cells:\n"
|
"When perspective is set, this also updates visibility overlays automatically."},
|
||||||
" - x, y: Grid coordinates\n"
|
|
||||||
" - visible: True (all returned cells are visible)\n"
|
|
||||||
" - discovered: True (FOV implies discovery)\n\n"
|
|
||||||
"Also updates the internal FOV state for use with is_in_fov()."},
|
|
||||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
||||||
"is_in_fov(x: int, y: int) -> bool\n\n"
|
"is_in_fov(x: int, y: int) -> bool\n\n"
|
||||||
"Check if a cell is in the field of view.\n\n"
|
"Check if a cell is in the field of view.\n\n"
|
||||||
|
@ -1296,20 +1185,16 @@ PyMethodDef UIGrid_all_methods[] = {
|
||||||
UIDRAWABLE_METHODS,
|
UIDRAWABLE_METHODS,
|
||||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
||||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]\n\n"
|
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
|
||||||
"Compute field of view from a position and return visible cells.\n\n"
|
"Compute field of view from a position.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" x: X coordinate of the viewer\n"
|
" x: X coordinate of the viewer\n"
|
||||||
" y: Y coordinate of the viewer\n"
|
" y: Y coordinate of the viewer\n"
|
||||||
" radius: Maximum view distance (0 = unlimited)\n"
|
" radius: Maximum view distance (0 = unlimited)\n"
|
||||||
" light_walls: Whether walls are lit when visible\n"
|
" light_walls: Whether walls are lit when visible\n"
|
||||||
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
||||||
"Returns:\n"
|
"Updates the internal FOV state. Use is_in_fov() to check visibility after calling this.\n"
|
||||||
" List of tuples (x, y, visible, discovered) for all visible cells:\n"
|
"When perspective is set, this also updates visibility overlays automatically."},
|
||||||
" - x, y: Grid coordinates\n"
|
|
||||||
" - visible: True (all returned cells are visible)\n"
|
|
||||||
" - discovered: True (FOV implies discovery)\n\n"
|
|
||||||
"Also updates the internal FOV state for use with is_in_fov()."},
|
|
||||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
||||||
"is_in_fov(x: int, y: int) -> bool\n\n"
|
"is_in_fov(x: int, y: int) -> bool\n\n"
|
||||||
"Check if a cell is in the field of view.\n\n"
|
"Check if a cell is in the field of view.\n\n"
|
||||||
|
@ -1400,11 +1285,9 @@ PyGetSetDef UIGrid::getsetters[] = {
|
||||||
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
|
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
|
||||||
{"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL},
|
{"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL},
|
||||||
{"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective,
|
{"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective,
|
||||||
"Entity whose perspective to use for FOV rendering (None for omniscient view). "
|
"Entity perspective index for FOV rendering (-1 for omniscient view, 0+ for entity index). "
|
||||||
"Setting an entity automatically enables perspective mode.", NULL},
|
"When set to an entity index, only cells visible to that entity are rendered normally; "
|
||||||
{"perspective_enabled", (getter)UIGrid::get_perspective_enabled, (setter)UIGrid::set_perspective_enabled,
|
"explored but not visible cells are darkened, and unexplored cells are black.", NULL},
|
||||||
"Whether to use perspective-based FOV rendering. When True with no valid entity, "
|
|
||||||
"all cells appear undiscovered.", NULL},
|
|
||||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
|
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
|
||||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
|
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
#include "Resources.h"
|
#include "Resources.h"
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <libtcod.h>
|
#include <libtcod.h>
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
#include "PyCallable.h"
|
#include "PyCallable.h"
|
||||||
#include "PyTexture.h"
|
#include "PyTexture.h"
|
||||||
|
@ -30,7 +29,6 @@ private:
|
||||||
TCODMap* tcod_map; // TCOD map for FOV and pathfinding
|
TCODMap* tcod_map; // TCOD map for FOV and pathfinding
|
||||||
TCODDijkstra* tcod_dijkstra; // Dijkstra pathfinding
|
TCODDijkstra* tcod_dijkstra; // Dijkstra pathfinding
|
||||||
TCODPath* tcod_path; // A* pathfinding
|
TCODPath* tcod_path; // A* pathfinding
|
||||||
mutable std::mutex fov_mutex; // Mutex for thread-safe FOV operations
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UIGrid();
|
UIGrid();
|
||||||
|
@ -79,9 +77,8 @@ public:
|
||||||
// Background rendering
|
// Background rendering
|
||||||
sf::Color fill_color;
|
sf::Color fill_color;
|
||||||
|
|
||||||
// Perspective system - entity whose view to render
|
// Perspective system - which entity's view to render (-1 = omniscient/default)
|
||||||
std::weak_ptr<UIEntity> perspective_entity; // Weak reference to perspective entity
|
int perspective;
|
||||||
bool perspective_enabled; // Whether to use perspective rendering
|
|
||||||
|
|
||||||
// Property system for animations
|
// Property system for animations
|
||||||
bool setProperty(const std::string& name, float value) override;
|
bool setProperty(const std::string& name, float value) override;
|
||||||
|
@ -106,8 +103,6 @@ public:
|
||||||
static int set_fill_color(PyUIGridObject* self, PyObject* value, void* closure);
|
static int set_fill_color(PyUIGridObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_perspective(PyUIGridObject* self, void* closure);
|
static PyObject* get_perspective(PyUIGridObject* self, void* closure);
|
||||||
static int set_perspective(PyUIGridObject* self, PyObject* value, void* closure);
|
static int set_perspective(PyUIGridObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_perspective_enabled(PyUIGridObject* self, void* closure);
|
|
||||||
static int set_perspective_enabled(PyUIGridObject* self, PyObject* value, void* closure);
|
|
||||||
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args);
|
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args);
|
||||||
|
|
Loading…
Reference in New Issue