diff --git a/roguelike_tutorial/part_4.py b/roguelike_tutorial/part_4.py index 1934317..423cca8 100644 --- a/roguelike_tutorial/part_4.py +++ b/roguelike_tutorial/part_4.py @@ -166,8 +166,8 @@ if len(rooms) > 1: enemy_x, enemy_y = rooms[1].center() enemy = mcrfpy.Entity( (enemy_x, enemy_y), - texture=texture, - sprite_index=117 # Enemy sprite + texture=hero_texture, + sprite_index=0 # Enemy sprite ) grid.entities.append(enemy) diff --git a/roguelike_tutorial/part_5.py b/roguelike_tutorial/part_5.py index 374061e..a8d544d 100644 --- a/roguelike_tutorial/part_5.py +++ b/roguelike_tutorial/part_5.py @@ -1,447 +1,289 @@ """ -McRogueFace Tutorial - Part 5: Entity Interactions +McRogueFace Tutorial - Part 5: Interacting with other entities This tutorial builds on Part 4 by adding: -- Entity class hierarchy (PlayerEntity, EnemyEntity, BoulderEntity, ButtonEntity) +- Subclassing mcrfpy.Entity - Non-blocking movement animations with destination tracking - Bump interactions (combat, pushing) -- Step-on interactions (buttons, doors) -- Concurrent enemy AI with smooth animations - -Key concepts: -- Entities inherit from mcrfpy.Entity for proper C++/Python integration -- Logic operates on destination positions during animations -- Player input is processed immediately, not blocked by animations """ import mcrfpy import random -# ============================================================================ -# Entity Classes - Inherit from mcrfpy.Entity -# ============================================================================ - -class GameEntity(mcrfpy.Entity): - """Base class for all game entities with interaction logic""" - def __init__(self, x, y, **kwargs): - # Extract grid before passing to parent - grid = kwargs.pop('grid', None) - super().__init__(x=x, y=y, **kwargs) - - # Current position is tracked by parent Entity.x/y - # Add destination tracking for animation system - self.dest_x = x - self.dest_y = y - self.is_moving = False - - # Game properties - self.blocks_movement = True - self.hp = 10 - self.max_hp = 10 - self.entity_type = "generic" - - # Add to grid if provided - if grid: - grid.entities.append(self) - - def start_move(self, new_x, new_y, duration=0.2, callback=None): - """Start animating movement to new position""" - self.dest_x = new_x - self.dest_y = new_y - self.is_moving = True - - # Create animations for smooth movement - if callback: - # Only x animation needs callback since they run in parallel - anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad", callback=callback) - else: - anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad") - anim_y = mcrfpy.Animation("y", float(new_y), duration, "easeInOutQuad") - - anim_x.start(self) - anim_y.start(self) - - 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): - """Called when another entity tries to move into our space""" - return False # Block movement by default - - def on_step(self, other): - """Called when another entity steps on us (non-blocking)""" - pass - - def take_damage(self, damage): - """Apply damage and handle death""" - self.hp -= damage - if self.hp <= 0: - self.hp = 0 - self.die() - - def die(self): - """Remove entity from grid""" - # The C++ die() method handles removal from grid - super().die() - -class PlayerEntity(GameEntity): - """The player character""" - def __init__(self, x, y, **kwargs): - kwargs['sprite_index'] = 64 # Hero sprite - super().__init__(x=x, y=y, **kwargs) - self.damage = 3 - self.entity_type = "player" - self.blocks_movement = True - - def on_bump(self, other): - """Player bumps into something""" - if other.entity_type == "enemy": - # Deal damage - other.take_damage(self.damage) - return False # Can't move into enemy space - elif other.entity_type == "boulder": - # Try to push - dx = self.dest_x - int(self.x) - dy = self.dest_y - int(self.y) - return other.try_push(dx, dy) - return False - -class EnemyEntity(GameEntity): - """Basic enemy with AI""" - def __init__(self, x, y, **kwargs): - kwargs['sprite_index'] = 65 # Enemy sprite - super().__init__(x=x, y=y, **kwargs) - self.damage = 1 - self.entity_type = "enemy" - self.ai_state = "wander" - self.hp = 5 - self.max_hp = 5 - - def on_bump(self, other): - """Enemy bumps into something""" - if other.entity_type == "player": - other.take_damage(self.damage) - return False - return False - - def can_see_player(self, player_pos, grid): - """Check if enemy can see the player position""" - # Simple check: within 6 tiles and has line of sight - mx, my = self.get_position() - px, py = player_pos - - dist = abs(px - mx) + abs(py - my) - if dist > 6: - return False - - # Use libtcod for line of sight - line = list(mcrfpy.libtcod.line(mx, my, px, py)) - if len(line) > 7: # Too far - return False - for x, y in line[1:-1]: # Skip start and end points - cell = grid.at(x, y) - if cell and not cell.transparent: - return False - return True - - def ai_turn(self, grid, player): - """Decide next move""" - px, py = player.get_position() - mx, my = self.get_position() - - # Simple AI: move toward player if visible - if self.can_see_player((px, py), grid): - # Calculate direction toward player - dx = 0 - dy = 0 - if px > mx: - dx = 1 - elif px < mx: - dx = -1 - if py > my: - dy = 1 - elif py < my: - dy = -1 - - # Prefer cardinal movement - if dx != 0 and dy != 0: - # Pick horizontal or vertical based on greater distance - if abs(px - mx) > abs(py - my): - dy = 0 - else: - dx = 0 - - return (mx + dx, my + dy) - else: - # Random movement - dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)]) - return (mx + dx, my + dy) - -class BoulderEntity(GameEntity): - """Pushable boulder""" - def __init__(self, x, y, **kwargs): - kwargs['sprite_index'] = 7 # Boulder sprite - super().__init__(x=x, y=y, **kwargs) - self.entity_type = "boulder" - self.pushable = True - - def try_push(self, dx, dy): - """Attempt to push boulder in direction""" - new_x = int(self.x) + dx - new_y = int(self.y) + dy - - # Check if destination is free - if can_move_to(new_x, new_y): - self.start_move(new_x, new_y) - return True - return False - -class ButtonEntity(GameEntity): - """Pressure plate that triggers when stepped on""" - def __init__(self, x, y, target=None, **kwargs): - kwargs['sprite_index'] = 8 # Button sprite - super().__init__(x=x, y=y, **kwargs) - self.blocks_movement = False # Can be walked over - self.entity_type = "button" - self.pressed = False - self.pressed_by = set() # Track who's pressing - self.target = target # Door or other triggerable - - def on_step(self, other): - """Activate when stepped on""" - if other not in self.pressed_by: - self.pressed_by.add(other) - if not self.pressed: - self.pressed = True - self.sprite_index = 9 # Pressed sprite - if self.target: - self.target.activate() - - def on_leave(self, other): - """Deactivate when entity leaves""" - if other in self.pressed_by: - self.pressed_by.remove(other) - if len(self.pressed_by) == 0 and self.pressed: - self.pressed = False - self.sprite_index = 8 # Unpressed sprite - if self.target: - self.target.deactivate() - -class DoorEntity(GameEntity): - """Door that can be opened by buttons""" - def __init__(self, x, y, **kwargs): - kwargs['sprite_index'] = 3 # Closed door sprite - super().__init__(x=x, y=y, **kwargs) - self.entity_type = "door" - self.is_open = False - - def activate(self): - """Open the door""" - self.is_open = True - self.blocks_movement = False - self.sprite_index = 11 # Open door sprite - - def deactivate(self): - """Close the door""" - self.is_open = False - self.blocks_movement = True - self.sprite_index = 3 # Closed door sprite - -# ============================================================================ -# Global Game State -# ============================================================================ - # Create and activate a new scene mcrfpy.createScene("tutorial") mcrfpy.setScene("tutorial") -# Load the texture +# 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 +# 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] -# Game state -player = None -enemies = [] -all_entities = [] -is_player_turn = True -move_duration = 0.2 - -# ============================================================================ -# Dungeon Generation (from Part 3) -# ============================================================================ - +# Room class for BSP class Room: - def __init__(self, x, y, width, height): + def __init__(self, x, y, w, h): self.x1 = x self.y1 = y - self.x2 = x + width - self.y2 = y + height - + self.x2 = x + w + self.y2 = y + h + self.w = w + self.h = h + def center(self): - return ((self.x1 + self.x2) // 2, (self.y1 + self.y2) // 2) - + 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) -def create_room(room): - """Carve out a room in the grid""" - for x in range(room.x1 + 1, room.x2): - for y in range(room.y1 + 1, room.y2): - cell = grid.at(x, y) - if cell: - cell.walkable = True - cell.transparent = True - cell.tilesprite = random.choice(FLOOR_TILES) +# 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 create_l_shaped_hallway(x1, y1, x2, y2): - """Create L-shaped hallway between two points""" - corner_x = x2 - corner_y = y1 - - if random.random() < 0.5: - corner_x = x1 - corner_y = y2 - - for x, y in mcrfpy.libtcod.line(x1, y1, corner_x, corner_y): - cell = grid.at(x, y) - if cell: - cell.walkable = True - cell.transparent = True - cell.tilesprite = random.choice(FLOOR_TILES) - - for x, y in mcrfpy.libtcod.line(corner_x, corner_y, x2, y2): - cell = grid.at(x, y) - if cell: - cell.walkable = True - cell.transparent = True - cell.tilesprite = random.choice(FLOOR_TILES) +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)) -def generate_dungeon(): - """Generate a simple dungeon with rooms and hallways""" - # Initialize all cells as walls - for x in range(grid_width): - for y in range(grid_height): - cell = grid.at(x, y) - if cell: - cell.walkable = False - cell.transparent = False - cell.tilesprite = random.choice(WALL_TILES) - + 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 = [] - num_rooms = 0 - for _ in range(30): - w = random.randint(4, 8) - h = random.randint(4, 8) - x = random.randint(0, grid_width - w - 1) - y = random.randint(0, grid_height - h - 1) + # 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) - # Check if room intersects with existing rooms - if any(new_room.intersects(other_room) for other_room in rooms): - continue + failed = False + for other_room in rooms: + if new_room.intersects(other_room): + failed = True + break + + if not failed: + carve_room(new_room) - create_room(new_room) - - if num_rooms > 0: - # Connect to previous room - new_x, new_y = new_room.center() - prev_x, prev_y = rooms[num_rooms - 1].center() - create_l_shaped_hallway(prev_x, prev_y, new_x, new_y) - - rooms.append(new_room) - num_rooms += 1 + 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 -# ============================================================================ -# Entity Management -# ============================================================================ +# Generate the dungeon +rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8) -def get_entities_at(x, y): - """Get all entities at a specific position (including moving ones)""" - entities = [] - for entity in all_entities: - ex, ey = entity.get_position() - if ex == x and ey == y: - entities.append(entity) - return entities +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) -def get_blocking_entity_at(x, y): - """Get the first blocking entity at position""" - for entity in get_entities_at(x, y): - if entity.blocks_movement: - return entity - return None +# 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 - - cell = grid.at(x, y) - if not cell or not cell.walkable: - return False - - # Check for blocking entities - if get_blocking_entity_at(x, y): - return False - - return True - -def can_entity_move_to(entity, x, y): - """Check if specific entity can move to position""" - if x < 0 or x >= grid_width or y < 0 or y >= grid_height: - return False - - cell = grid.at(x, y) - if not cell or not cell.walkable: - return False - - # Check for other blocking entities (not self) - blocker = get_blocking_entity_at(x, y) - if blocker and blocker != entity: - return False - - return True - -# ============================================================================ -# Turn Management -# ============================================================================ - -def process_player_move(key): - """Handle player input with immediate response""" - global is_player_turn - if not is_player_turn or player.is_moving: - return # Not player's turn or still animating - - px, py = player.get_position() + 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 - # Calculate movement direction if key == "W" or key == "Up": new_y -= 1 elif key == "S" or key == "Down": @@ -450,176 +292,72 @@ def process_player_move(key): new_x -= 1 elif key == "D" or key == "Right": new_x += 1 - else: - return # Not a movement key - - if new_x == px and new_y == py: - return # No movement - # Check what's at destination - cell = grid.at(new_x, new_y) - if not cell or not cell.walkable: - return # Can't move into walls - - blocking_entity = get_blocking_entity_at(new_x, new_y) - - if blocking_entity: - # Try bump interaction - if not player.on_bump(blocking_entity): - # Movement blocked, but turn still happens - is_player_turn = False - mcrfpy.setTimer("enemy_turn", process_enemy_turns, 50) - return - - # Movement is valid - start player animation - is_player_turn = False - player.start_move(new_x, new_y, duration=move_duration, callback=player_move_complete) - - # Update grid center to follow player - grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, move_duration, "linear") - grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, move_duration, "linear") - grid_anim_x.start(grid) - grid_anim_y.start(grid) - - # Start enemy turns after a short delay (so player sees their move start first) - mcrfpy.setTimer("enemy_turn", process_enemy_turns, 50) - -def process_enemy_turns(timer_name): - """Process all enemy AI decisions and start their animations""" - enemies_to_move = [] - - for enemy in enemies: - if enemy.hp <= 0: # Skip dead enemies - continue + 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 enemy.is_moving: - continue # Skip if still animating + 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) - # AI decides next move based on player's destination - target_x, target_y = enemy.ai_turn(grid, player) - - # Check if move is valid - cell = grid.at(target_x, target_y) - if not cell or not cell.walkable: - continue - - # Check what's at the destination - blocking_entity = get_blocking_entity_at(target_x, target_y) - - if blocking_entity and blocking_entity != enemy: - # Try bump interaction - enemy.on_bump(blocking_entity) - # Enemy doesn't move but still took its turn - else: - # Valid move - add to list - enemies_to_move.append((enemy, target_x, target_y)) - - # Start all enemy animations simultaneously - for enemy, tx, ty in enemies_to_move: - enemy.start_move(tx, ty, duration=move_duration) - -def player_move_complete(anim, entity): - """Called when player animation finishes""" - global is_player_turn - - player.is_moving = False - - # Check for step-on interactions at new position - for entity in get_entities_at(int(player.x), int(player.y)): - if entity != player and not entity.blocks_movement: - entity.on_step(player) - - # Update FOV from new position - update_fov() - - # Player's turn is ready again - is_player_turn = True - -def update_fov(): - """Update field of view from player position""" - px, py = int(player.x), int(player.y) - grid.compute_fov(px, py, radius=8) - player.update_visibility() - -# ============================================================================ -# Input Handling -# ============================================================================ + 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_player_move(key) - -# Register the key handler + process_move(key) + +# Register the keyboard handler mcrfpy.keypressScene(handle_keys) -# ============================================================================ -# Initialize Game -# ============================================================================ - -# Generate dungeon -rooms = generate_dungeon() - -# Place player in first room -if rooms: - start_x, start_y = rooms[0].center() - player = PlayerEntity(start_x, start_y, grid=grid) - all_entities.append(player) - - # Place enemies in other rooms - for i in range(1, min(6, len(rooms))): - room = rooms[i] - ex, ey = room.center() - enemy = EnemyEntity(ex, ey, grid=grid) - enemies.append(enemy) - all_entities.append(enemy) - - # Place some boulders - for i in range(3): - room = random.choice(rooms[1:]) - bx = random.randint(room.x1 + 1, room.x2 - 1) - by = random.randint(room.y1 + 1, room.y2 - 1) - if can_move_to(bx, by): - boulder = BoulderEntity(bx, by, grid=grid) - all_entities.append(boulder) - - # Place a button and door in one of the rooms - if len(rooms) > 2: - button_room = rooms[-2] - door_room = rooms[-1] - - # Place door at entrance to last room - dx, dy = door_room.center() - door = DoorEntity(dx, door_room.y1, grid=grid) - all_entities.append(door) - - # Place button in second to last room - bx, by = button_room.center() - button = ButtonEntity(bx, by, target=door, grid=grid) - all_entities.append(button) - - # Set grid perspective to player - grid.perspective = player - grid.center_x = (start_x + 0.5) * 16 - grid.center_y = (start_y + 0.5) * 16 - - # Initial FOV calculation - update_fov() - -# Add grid to scene -mcrfpy.sceneUI("tutorial").append(grid) - -# Show instructions +# Add UI elements title = mcrfpy.Caption((320, 10), - text="Part 5: Entity Interactions - WASD to move, bump enemies, push boulders!", + text="McRogueFace Tutorial - Part 5: Entity Collision", ) title.fill_color = mcrfpy.Color(255, 255, 255, 255) mcrfpy.sceneUI("tutorial").append(title) -print("Part 5: Entity Interactions - Tutorial loaded!") -print("- Bump into enemies to attack them") -print("- Push boulders by walking into them") -print("- Step on buttons to open doors") -print("- Enemies will pursue you when they can see you") \ No newline at end of file +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!")