feat: Add Part 5 tutorial - Entity Interactions
Implements comprehensive entity interaction system: - Entity class hierarchy inheriting from mcrfpy.Entity - Non-blocking movement animations with destination tracking - Bump interactions (combat when hitting enemies, pushing boulders) - Step-on interactions (buttons that open doors) - Basic enemy AI with line-of-sight pursuit - Concurrent animation system (enemies move while player moves) Also fixes C++ animation system to support Python subclasses: - Changed PyAnimation::start() to use PyObject_IsInstance instead of strcmp - Now properly supports inherited entity classes - Animation system works with any subclass of Frame, Caption, Sprite, Grid, or Entity This completes the core gameplay mechanics needed for roguelike development. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7aef412343
commit
994e8d186e
|
@ -0,0 +1,625 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 5: Entity Interactions
|
||||
|
||||
This tutorial builds on Part 4 by adding:
|
||||
- Entity class hierarchy (PlayerEntity, EnemyEntity, BoulderEntity, ButtonEntity)
|
||||
- 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
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
grid_width, grid_height = 40, 30
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
# Create the grid
|
||||
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)
|
||||
# ============================================================================
|
||||
|
||||
class Room:
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
def center(self):
|
||||
return ((self.x1 + self.x2) // 2, (self.y1 + self.y2) // 2)
|
||||
|
||||
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)
|
||||
|
||||
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 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)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
return rooms
|
||||
|
||||
# ============================================================================
|
||||
# Entity Management
|
||||
# ============================================================================
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
new_x, new_y = px, py
|
||||
|
||||
# Calculate movement direction
|
||||
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
|
||||
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 enemy.is_moving:
|
||||
continue # Skip if still animating
|
||||
|
||||
# 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
|
||||
# ============================================================================
|
||||
|
||||
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
|
||||
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
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="Part 5: Entity Interactions - WASD to move, bump enemies, push boulders!",
|
||||
)
|
||||
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")
|
|
@ -138,47 +138,67 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Check type by comparing type names
|
||||
const char* type_name = Py_TYPE(target_obj)->tp_name;
|
||||
// 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");
|
||||
|
||||
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
|
||||
bool handled = false;
|
||||
|
||||
// Use PyObject_IsInstance to support inheritance
|
||||
if (frame_type && PyObject_IsInstance(target_obj, frame_type)) {
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
|
||||
if (frame->data) {
|
||||
self->data->start(frame->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
|
||||
else if (caption_type && PyObject_IsInstance(target_obj, caption_type)) {
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
|
||||
if (caption->data) {
|
||||
self->data->start(caption->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
|
||||
else if (sprite_type && PyObject_IsInstance(target_obj, sprite_type)) {
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
|
||||
if (sprite->data) {
|
||||
self->data->start(sprite->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
|
||||
else if (grid_type && PyObject_IsInstance(target_obj, grid_type)) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
|
||||
if (grid->data) {
|
||||
self->data->start(grid->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
|
||||
else if (entity_type && PyObject_IsInstance(target_obj, entity_type)) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
else {
|
||||
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
|
||||
|
||||
// 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)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue