diff --git a/roguelike_tutorial/part_3.py b/roguelike_tutorial/part_3.py deleted file mode 100644 index cb48b8b..0000000 --- a/roguelike_tutorial/part_3.py +++ /dev/null @@ -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!") diff --git a/roguelike_tutorial/part_4.py b/roguelike_tutorial/part_4.py deleted file mode 100644 index 423cca8..0000000 --- a/roguelike_tutorial/part_4.py +++ /dev/null @@ -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!") diff --git a/roguelike_tutorial/part_5.py b/roguelike_tutorial/part_5.py deleted file mode 100644 index a8d544d..0000000 --- a/roguelike_tutorial/part_5.py +++ /dev/null @@ -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!") diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp index c81a2ea..d45c6eb 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -138,67 +138,47 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) { return NULL; } - // Get type objects from the module to ensure they're initialized - PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"); - 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"); + // Check type by comparing type names + const char* type_name = Py_TYPE(target_obj)->tp_name; - bool handled = false; - - // Use PyObject_IsInstance to support inheritance - if (frame_type && PyObject_IsInstance(target_obj, frame_type)) { + if (strcmp(type_name, "mcrfpy.Frame") == 0) { PyUIFrameObject* frame = (PyUIFrameObject*)target_obj; if (frame->data) { self->data->start(frame->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; if (caption->data) { self->data->start(caption->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; if (sprite->data) { self->data->start(sprite->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; if (grid->data) { self->data->start(grid->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 PyUIEntityObject* entity = (PyUIEntityObject*)target_obj; if (entity->data) { self->data->startEntity(entity->data); AnimationManager::getInstance().addAnimation(self->data); - handled = true; } } - - // Clean up references - 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)"); + else { + PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity"); return NULL; } diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 62e8d22..a282b6d 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -2,14 +2,13 @@ #include "GameEngine.h" #include "McRFPy_API.h" #include "PythonObjectCache.h" -#include "UIEntity.h" #include // UIDrawable methods now in UIBase.h UIGrid::UIGrid() : 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), - perspective_enabled(false) // Default to omniscient view + perspective(-1) // Default to omniscient view { // Initialize entities list entities = std::make_shared>>(); @@ -37,7 +36,7 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr _ptex, sf::Vector2f _x zoom(1.0f), ptex(_ptex), points(gx * gy), 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 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 - // Only render visibility overlay if perspective is enabled - if (perspective_enabled) { - auto entity = perspective_entity.lock(); + // Only render visibility overlay if perspective is set (not omniscient) + if (perspective >= 0 && perspective < static_cast(entities->size())) { + // Get the entity whose perspective we're using + auto it = entities->begin(); + std::advance(it, perspective); + auto& entity = *it; // Create rectangle for overlays sf::RectangleShape overlay; overlay.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom)); - if (entity) { - // Valid entity - use its gridstate for visibility - for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); - x < x_limit; - x+=1) + 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) { - 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 ); + // 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 ); - // Get visibility state from entity's perspective - int idx = y * grid_x + x; - if (idx >= 0 && idx < static_cast(entity->gridstate.size())) { - 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 ); + // Get visibility state from entity's perspective + int idx = y * grid_x + x; + if (idx >= 0 && idx < static_cast(entity->gridstate.size())) { + const auto& state = entity->gridstate[idx]; 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 /* @@ -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; - std::lock_guard lock(fov_mutex); 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; - std::lock_guard lock(fov_mutex); return tcod_map->isInFov(x, y); } @@ -554,7 +527,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { PyObject* click_handler = nullptr; float center_x = 0.0f, center_y = 0.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; float opacity = 1.0f; int z_index = 0; @@ -566,15 +539,15 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { static const char* kwlist[] = { "pos", "size", "grid_size", "texture", // Positional args (as per spec) // 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", nullptr }; // Parse arguments with | for optional positional args - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffii", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffiifizffffii", const_cast(kwlist), &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)) { return -1; } @@ -680,8 +653,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { self->data->center_x = center_x; self->data->center_y = center_y; 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->opacity = opacity; 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) { - auto locked = self->data->perspective_entity.lock(); - 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; + return PyLong_FromLong(self->data->perspective); } int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure) { - if (value == Py_None) { - // Clear perspective but keep perspective_enabled unchanged - 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"); + long perspective = PyLong_AsLong(value); + if (PyErr_Occurred()) { return -1; } - if (!PyObject_IsInstance(value, entity_type)) { - Py_DECREF(entity_type); - PyErr_SetString(PyExc_TypeError, "perspective must be a UIEntity or None"); + // Validate perspective (-1 for omniscient, or valid entity index) + if (perspective < -1) { + PyErr_SetString(PyExc_ValueError, "perspective must be -1 (omniscient) or a valid entity index"); return -1; } - Py_DECREF(entity_type); - PyUIEntityObject* entity_obj = (PyUIEntityObject*)value; - self->data->perspective_entity = entity_obj->data; - self->data->perspective_enabled = true; // Enable perspective when entity assigned - return 0; -} - -PyObject* UIGrid::get_perspective_enabled(PyUIGridObject* self, void* closure) -{ - 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 + // Check if entity index is valid (if not omniscient) + if (perspective >= 0 && self->data->entities) { + int entity_count = self->data->entities->size(); + if (perspective >= entity_count) { + PyErr_Format(PyExc_IndexError, "perspective index %ld out of range (grid has %d entities)", + perspective, entity_count); + return -1; + } } - self->data->perspective_enabled = enabled; + + self->data->perspective = perspective; return 0; } @@ -1056,43 +984,8 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* return NULL; } - // Compute FOV self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm); - - // 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; + Py_RETURN_NONE; } 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[] = { {"at", (PyCFunction)UIGrid::py_at, 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 field of view from a position and return visible cells.\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.\n\n" "Args:\n" " x: X coordinate of the viewer\n" " y: Y coordinate of the viewer\n" " radius: Maximum view distance (0 = unlimited)\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" - "Returns:\n" - " List of tuples (x, y, visible, discovered) for all visible cells:\n" - " - 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()."}, + "Updates the internal FOV state. Use is_in_fov() to check visibility after calling this.\n" + "When perspective is set, this also updates visibility overlays automatically."}, {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS, "is_in_fov(x: int, y: int) -> bool\n\n" "Check if a cell is in the field of view.\n\n" @@ -1296,20 +1185,16 @@ PyMethodDef UIGrid_all_methods[] = { UIDRAWABLE_METHODS, {"at", (PyCFunction)UIGrid::py_at, 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 field of view from a position and return visible cells.\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.\n\n" "Args:\n" " x: X coordinate of the viewer\n" " y: Y coordinate of the viewer\n" " radius: Maximum view distance (0 = unlimited)\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" - "Returns:\n" - " List of tuples (x, y, visible, discovered) for all visible cells:\n" - " - 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()."}, + "Updates the internal FOV state. Use is_in_fov() to check visibility after calling this.\n" + "When perspective is set, this also updates visibility overlays automatically."}, {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS, "is_in_fov(x: int, y: int) -> bool\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 {"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, - "Entity whose perspective to use for FOV rendering (None for omniscient view). " - "Setting an entity automatically enables perspective mode.", NULL}, - {"perspective_enabled", (getter)UIGrid::get_perspective_enabled, (setter)UIGrid::set_perspective_enabled, - "Whether to use perspective-based FOV rendering. When True with no valid entity, " - "all cells appear undiscovered.", NULL}, + "Entity perspective index for FOV rendering (-1 for omniscient view, 0+ for entity index). " + "When set to an entity index, only cells visible to that entity are rendered normally; " + "explored but not visible cells are darkened, and unexplored cells are black.", NULL}, {"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}, UIDRAWABLE_GETSETTERS, diff --git a/src/UIGrid.h b/src/UIGrid.h index e8f9311..af1c078 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -6,7 +6,6 @@ #include "Resources.h" #include #include -#include #include "PyCallable.h" #include "PyTexture.h" @@ -30,7 +29,6 @@ private: TCODMap* tcod_map; // TCOD map for FOV and pathfinding TCODDijkstra* tcod_dijkstra; // Dijkstra pathfinding TCODPath* tcod_path; // A* pathfinding - mutable std::mutex fov_mutex; // Mutex for thread-safe FOV operations public: UIGrid(); @@ -79,9 +77,8 @@ public: // Background rendering sf::Color fill_color; - // Perspective system - entity whose view to render - std::weak_ptr perspective_entity; // Weak reference to perspective entity - bool perspective_enabled; // Whether to use perspective rendering + // Perspective system - which entity's view to render (-1 = omniscient/default) + int perspective; // Property system for animations 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 PyObject* get_perspective(PyUIGridObject* self, 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_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args);