From dcd1b0ca33d46639023221f4d7d52000b947dbdf Mon Sep 17 00:00:00 2001 From: John McCardle Date: Mon, 14 Jul 2025 01:33:40 -0400 Subject: [PATCH] Add roguelike tutorial implementation files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement Parts 0-2 of the classic roguelike tutorial adapted for McRogueFace: - Part 0: Basic grid setup and tile rendering - Part 1: Drawing '@' symbol and basic movement - Part 1b: Variant with sprite-based player - Part 2: Entity system and NPC implementation with three movement variants: - part_2.py: Standard implementation - part_2-naive.py: Naive movement approach - part_2-onemovequeued.py: Queued movement system Includes tutorial assets: - tutorial2.png: Tileset for dungeon tiles - tutorial_hero.png: Player sprite sheet These examples demonstrate McRogueFace's capabilities for traditional roguelike development. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- roguelike_tutorial/part_0.py | 80 +++++++ roguelike_tutorial/part_1.py | 116 ++++++++++ roguelike_tutorial/part_1b.py | 117 ++++++++++ roguelike_tutorial/part_2-naive.py | 149 +++++++++++++ roguelike_tutorial/part_2-onemovequeued.py | 241 +++++++++++++++++++++ roguelike_tutorial/part_2.py | 149 +++++++++++++ roguelike_tutorial/tutorial2.png | Bin 0 -> 5741 bytes roguelike_tutorial/tutorial_hero.png | Bin 0 -> 16742 bytes 8 files changed, 852 insertions(+) create mode 100644 roguelike_tutorial/part_0.py create mode 100644 roguelike_tutorial/part_1.py create mode 100644 roguelike_tutorial/part_1b.py create mode 100644 roguelike_tutorial/part_2-naive.py create mode 100644 roguelike_tutorial/part_2-onemovequeued.py create mode 100644 roguelike_tutorial/part_2.py create mode 100644 roguelike_tutorial/tutorial2.png create mode 100644 roguelike_tutorial/tutorial_hero.png diff --git a/roguelike_tutorial/part_0.py b/roguelike_tutorial/part_0.py new file mode 100644 index 0000000..eb9ed94 --- /dev/null +++ b/roguelike_tutorial/part_0.py @@ -0,0 +1,80 @@ +""" +McRogueFace Tutorial - Part 0: Introduction to Scene, Texture, and Grid + +This tutorial introduces the basic building blocks: +- Scene: A container for UI elements and game state +- Texture: Loading image assets for use in the game +- Grid: A tilemap component for rendering tile-based worlds +""" +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) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = zoom +grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 0", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((280, 750), + text="Scene + Texture + Grid = Tilemap!", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 0 loaded!") +print(f"Created a {grid.grid_size[0]}x{grid.grid_size[1]} grid") +print(f"Grid positioned at ({grid.x}, {grid.y})") diff --git a/roguelike_tutorial/part_1.py b/roguelike_tutorial/part_1.py new file mode 100644 index 0000000..4c19d6d --- /dev/null +++ b/roguelike_tutorial/part_1.py @@ -0,0 +1,116 @@ +""" +McRogueFace Tutorial - Part 1: Entities and Keyboard Input + +This tutorial builds on Part 0 by adding: +- Entity: A game object that can be placed in a grid +- Keyboard handling: Responding to key presses to move the entity +""" +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 (32x32 sprite sheet) +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = zoom +grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Create a player entity at position (4, 4) +player = mcrfpy.Entity( + (4, 4), # Entity positions are tile coordinates + texture=hero_texture, + sprite_index=0 # Use the first sprite in the texture +) + +# Add the player entity to the grid +grid.entities.append(player) + +# Define keyboard handler +def handle_keys(key, state): + """Handle keyboard input to move the player""" + if state == "start": # Only respond to key press, not release + # Get current player position in grid coordinates + px, py = player.x, player.y + + # Calculate new position based on key press + if key == "W" or key == "Up": + py -= 1 + elif key == "S" or key == "Down": + py += 1 + elif key == "A" or key == "Left": + px -= 1 + elif key == "D" or key == "Right": + px += 1 + + # Update player position (no collision checking yet) + player.x = px + player.y = py + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 1", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((200, 750), + text="Use WASD or Arrow Keys to move the hero!", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 1 loaded!") +print(f"Player entity created at grid position (4, 4)") +print("Use WASD or Arrow keys to move!") diff --git a/roguelike_tutorial/part_1b.py b/roguelike_tutorial/part_1b.py new file mode 100644 index 0000000..3894fc7 --- /dev/null +++ b/roguelike_tutorial/part_1b.py @@ -0,0 +1,117 @@ +""" +McRogueFace Tutorial - Part 1: Entities and Keyboard Input + +This tutorial builds on Part 0 by adding: +- Entity: A game object that can be placed in a grid +- Keyboard handling: Responding to key presses to move the entity +""" +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 (32x32 sprite sheet) +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big! + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Create a player entity at position (4, 4) +player = mcrfpy.Entity( + (4, 4), # Entity positions are tile coordinates + texture=hero_texture, + sprite_index=0 # Use the first sprite in the texture +) + +# Add the player entity to the grid +grid.entities.append(player) +grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates + +# Define keyboard handler +def handle_keys(key, state): + """Handle keyboard input to move the player""" + if state == "start": # Only respond to key press, not release + # Get current player position in grid coordinates + px, py = player.x, player.y + + # Calculate new position based on key press + if key == "W" or key == "Up": + py -= 1 + elif key == "S" or key == "Down": + py += 1 + elif key == "A" or key == "Left": + px -= 1 + elif key == "D" or key == "Right": + px += 1 + + # Update player position (no collision checking yet) + player.x = px + player.y = py + grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 1", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((200, 750), + text="Use WASD or Arrow Keys to move the hero!", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 1 loaded!") +print(f"Player entity created at grid position (4, 4)") +print("Use WASD or Arrow keys to move!") diff --git a/roguelike_tutorial/part_2-naive.py b/roguelike_tutorial/part_2-naive.py new file mode 100644 index 0000000..6959a4b --- /dev/null +++ b/roguelike_tutorial/part_2-naive.py @@ -0,0 +1,149 @@ +""" +McRogueFace Tutorial - Part 2: Animated Movement + +This tutorial builds on Part 1 by adding: +- Animation system for smooth movement +- Movement that takes 0.5 seconds per tile +- Input blocking during movement animation +""" +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 (32x32 sprite sheet) +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big! + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Create a player entity at position (4, 4) +player = mcrfpy.Entity( + (4, 4), # Entity positions are tile coordinates + texture=hero_texture, + sprite_index=0 # Use the first sprite in the texture +) + +# Add the player entity to the grid +grid.entities.append(player) +grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates + +# Movement state tracking +is_moving = False +move_animations = [] # Track active animations + +# Animation completion callback +def movement_complete(runtime): + """Called when movement animation completes""" + global is_moving + is_moving = False + # Ensure grid is centered on final position + grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 + +motion_speed = 0.30 # seconds per tile +# Define keyboard handler +def handle_keys(key, state): + """Handle keyboard input to move the player""" + global is_moving, move_animations + + if state == "start" and not is_moving: # Only respond to key press when not moving + # Get current player position in grid coordinates + px, py = player.x, player.y + new_x, new_y = px, py + + # Calculate new position based on key press + 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 position changed, start movement animation + if new_x != px or new_y != py: + is_moving = True + + # Create animations for player position + anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad") + anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad") + anim_x.start(player) + anim_y.start(player) + + # Animate grid center to follow player + center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear") + center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear") + center_x.start(grid) + center_y.start(grid) + + # Set a timer to mark movement as complete + mcrfpy.setTimer("move_complete", movement_complete, 500) + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 2", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((150, 750), + text="Smooth movement! Each step takes 0.5 seconds.", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 2 loaded!") +print(f"Player entity created at grid position (4, 4)") +print("Movement is now animated over 0.5 seconds per tile!") +print("Use WASD or Arrow keys to move!") diff --git a/roguelike_tutorial/part_2-onemovequeued.py b/roguelike_tutorial/part_2-onemovequeued.py new file mode 100644 index 0000000..126c433 --- /dev/null +++ b/roguelike_tutorial/part_2-onemovequeued.py @@ -0,0 +1,241 @@ +""" +McRogueFace Tutorial - Part 2: Enhanced with Single Move Queue + +This tutorial builds on Part 2 by adding: +- Single queued move system for responsive input +- Debug display showing position and queue status +- Smooth continuous movement when keys are held +- Animation callbacks to prevent race conditions +""" +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 (32x32 sprite sheet) +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big! + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Create a player entity at position (4, 4) +player = mcrfpy.Entity( + (4, 4), # Entity positions are tile coordinates + texture=hero_texture, + sprite_index=0 # Use the first sprite in the texture +) + +# Add the player entity to the grid +grid.entities.append(player) +grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates + +# Movement state tracking +is_moving = False +move_queue = [] # List to store queued moves (max 1 item) +#last_position = (4, 4) # Track last position +current_destination = None # Track where we're currently moving to +current_move = None # Track current move direction + +# Store animation references +player_anim_x = None +player_anim_y = None +grid_anim_x = None +grid_anim_y = None + +# Debug display caption +debug_caption = mcrfpy.Caption((10, 40), + text="Last: (4, 4) | Queue: 0 | Dest: None", +) +debug_caption.font_size = 16 +debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255) +mcrfpy.sceneUI("tutorial").append(debug_caption) + +# Additional debug caption for movement state +move_debug_caption = mcrfpy.Caption((10, 60), + text="Moving: False | Current: None | Queued: None", +) +move_debug_caption.font_size = 16 +move_debug_caption.fill_color = mcrfpy.Color(255, 200, 0, 255) +mcrfpy.sceneUI("tutorial").append(move_debug_caption) + +def key_to_direction(key): + """Convert key to direction string""" + if key == "W" or key == "Up": + return "Up" + elif key == "S" or key == "Down": + return "Down" + elif key == "A" or key == "Left": + return "Left" + elif key == "D" or key == "Right": + return "Right" + return None + +def update_debug_display(): + """Update the debug caption with current state""" + queue_count = len(move_queue) + dest_text = f"({current_destination[0]}, {current_destination[1]})" if current_destination else "None" + debug_caption.text = f"Last: ({player.x}, {player.y}) | Queue: {queue_count} | Dest: {dest_text}" + + # Update movement state debug + current_dir = key_to_direction(current_move) if current_move else "None" + queued_dir = key_to_direction(move_queue[0]) if move_queue else "None" + move_debug_caption.text = f"Moving: {is_moving} | Current: {current_dir} | Queued: {queued_dir}" + +# Animation completion callback +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 + print(f"In callback for animation: {anim=} {target=}") + # Clear movement state + is_moving = False + current_move = None + current_destination = None + # Clear animation references + player_anim_x = None + player_anim_y = None + + # Update last position to where we actually are now + #last_position = (int(player.x), int(player.y)) + + # Ensure grid is centered on final position + grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 + + # Check if there's a queued move + if move_queue: + # Pop the next move from the queue + next_move = move_queue.pop(0) + print(f"Processing queued move: {next_move}") + # Process it like a fresh input + process_move(next_move) + + update_debug_display() + +motion_speed = 0.30 # seconds per tile + +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 already moving, just update the queue + if is_moving: + print(f"process_move processing {key=} as a queued move (is_moving = True)") + # Clear queue and add new move (only keep 1 queued move) + move_queue.clear() + move_queue.append(key) + update_debug_display() + return + print(f"process_move processing {key=} as a new, immediate animation (is_moving = False)") + # Calculate new position from current position + px, py = int(player.x), int(player.y) + new_x, new_y = px, py + + # Calculate new position based on key press (only one tile movement) + 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 + + # Start the move if position changed + if new_x != px or new_y != py: + is_moving = True + current_move = key + current_destination = (new_x, new_y) + # only animate a single axis, same callback from either + 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) + + # Animate grid center to follow 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) + + update_debug_display() + +# Define keyboard handler +def handle_keys(key, state): + """Handle keyboard input to move the player""" + if state == "start": + # Only process movement keys + if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]: + print(f"handle_keys producing actual input: {key=}") + process_move(key) + + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 2 Enhanced", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((150, 750), + text="One-move queue system with animation callbacks!", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 2 Enhanced loaded!") +print(f"Player entity created at grid position (4, 4)") +print("Movement now uses animation callbacks to prevent race conditions!") +print("Use WASD or Arrow keys to move!") diff --git a/roguelike_tutorial/part_2.py b/roguelike_tutorial/part_2.py new file mode 100644 index 0000000..66a11b0 --- /dev/null +++ b/roguelike_tutorial/part_2.py @@ -0,0 +1,149 @@ +""" +McRogueFace Tutorial - Part 2: Animated Movement + +This tutorial builds on Part 1 by adding: +- Animation system for smooth movement +- Movement that takes 0.5 seconds per tile +- Input blocking during movement animation +""" +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 (32x32 sprite sheet) +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big! + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Create a player entity at position (4, 4) +player = mcrfpy.Entity( + (4, 4), # Entity positions are tile coordinates + texture=hero_texture, + sprite_index=0 # Use the first sprite in the texture +) + +# Add the player entity to the grid +grid.entities.append(player) +grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates + +# Movement state tracking +is_moving = False +move_animations = [] # Track active animations + +# Animation completion callback +def movement_complete(runtime): + """Called when movement animation completes""" + global is_moving + is_moving = False + # Ensure grid is centered on final position + grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 + +motion_speed = 0.30 # seconds per tile +# Define keyboard handler +def handle_keys(key, state): + """Handle keyboard input to move the player""" + global is_moving, move_animations + + if state == "start" and not is_moving: # Only respond to key press when not moving + # Get current player position in grid coordinates + px, py = player.x, player.y + new_x, new_y = px, py + + # Calculate new position based on key press + 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 position changed, start movement animation + if new_x != px or new_y != py: + is_moving = True + + # Create animations for player position + anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad") + anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad") + anim_x.start(player) + anim_y.start(player) + + # Animate grid center to follow player + center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear") + center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear") + center_x.start(grid) + center_y.start(grid) + + # Set a timer to mark movement as complete + mcrfpy.setTimer("move_complete", movement_complete, 500) + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 2", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((150, 750), + "Smooth movement! Each step takes 0.5 seconds.", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 2 loaded!") +print(f"Player entity created at grid position (4, 4)") +print("Movement is now animated over 0.5 seconds per tile!") +print("Use WASD or Arrow keys to move!") diff --git a/roguelike_tutorial/tutorial2.png b/roguelike_tutorial/tutorial2.png new file mode 100644 index 0000000000000000000000000000000000000000..e7854196517cae002aa2d3381728d0adbc125ff8 GIT binary patch literal 5741 zcmeHKc~BE+77qpuAeVxItUxBLin_TILLxyVK?xW@hzg@3oumU%a+ov_5Fx_i0HUa? zj-YZ4qIiq~>Np$<0t+&zc+3bg90MK$9)wFo5VjLMYqx64s@?xgCF$;W{N8)N?|m=% zQv7^YnHbp^ArJ@?jwjn6zPiC5g24>L1Q(vm-qyB-6)Vy=n_muU_31J&p0^(=&q{~LXnEJ&{8!(W zBrJ=XZ{r!6c4J@kfc|&+qyD~-(WwRAd1&gJ+l*M_aT}XMyZ8UjYF?LHch7P2%0I1I z*mL`U`_;)!(+v{;=xDg)T^0u{8!+nsM_S--Mezd#t*yJSvXW7HAc%RTSMS52P{JDA zLiRi)B!|`(K;%vJ*O?1+I&-2t5fk=^NL( z#C30!2X$rutKq08bl%U`#hJCvO{ zbijRE&RFd>*MF)jaZRV_w?>M(DL#lvvi!tPX!BKDotcOY*Ny4}qpH#KeR_keq?X zM@2>9qR2RzA{0-g)9H8u2~Q$nVGFD>S_%OwtW@csg_y!%gGzxyEQiE0DO!sO@MRGY z6N7>E==b?a2cUhj5a<)C=-Z<4Bah_NCTXO zAb_PfInl8cp)(%~h^Ta|le4oEML?uchyd^j6i2Fr0I2}fLc!oTF^uCZjiQ0p$}2UxgU%N+A5xth7)<7>Y&$KoZr7h$Zu>d@O}1 zqG0JlGJplh0+F*wKqEN=A{~@az*s3$NC0>_#S$PC#LK0jI)#>S#tJ_U6GOrgK1%$; z0Z0TpFfpse(g@W@1y3vi10X=lCXq%Zk?14>i9#Sa5uB%T*5R!K6-u}hwU|T#jy$E& zE(-%L1|}BJ_9+aYgN18huoNHw$rL=9ES!naLZG#tAJklUI|%^@U;_{cgAzzo27$&P z5_u#tgF<5vNLT`eLHNX8CKQXJ|C_aT^PpX(mfTaUgyTo+L{mE|0Ngh9H1!xR)@>y; zTDK_}fMBWwB@hV;b$P;AQz}6iAPoiK>G7^z-^<1Sp%q9X0m0b`q+t1U0o+o~d;rVm z!{vYi0g*s;qS1sz(&y+(nFxvk6ySf^@b3!7YiGv$ie2#k#)*s0;FBQ(`%TH<;RVk^{D)!qjx+7p`9Hqit;PQ_0u22% z$QSYZm9DRJeGvm+Wc;9PqU>Vyc=FUnGQ>&r0Hk5S?cKVpD1(?^t-d<^k*2wsvLkfa}6_LR3w8dPekQ z#$@B<$k@%zrIU3h89#0c#NbnHK$c}Gd6<}dO!_v-Zwb*Qgrqy`!8 z_C`6reOZ0KqPf;s9r^n-grnZIeV3nKB+~+_ADGQSxD_q_#vz+NqET7r=BNIgkqMZ` z=A|u&e-wkZvvk^r?9a)S3>?L#+n3j9pqaG=`iT1~lKH%!U(D*cVc`;|@h?Z2Cq|t% zN$d}H*-3@ac9e@10g_`|l~LaqxJ@sfg>Wm(xr}nKT3NI>m*=;p%{X=QG_T6^<6Dp^ zfvs+C4{VO{U}F9;6G;zw`P8DwTWx$j z_hH_o1#$Z60%d5!jhxe&d)cdr7bQ|z&mD7jj_rwrErBDo1%DqZq+J-9U9{$JOq<>p z3st9LI`mVj+8SQHvAOV)_FPYBm|rrhcOqY3c0R92@oY;{Rn=c;YtBDflhCju>()vQ zR9G?p^idMyv@0lshjulp3y~!+O(;}&bWs-@Cm~Az(EIJ{p;ojCRld(%CHcO(c5-e@T zV(8t-el)c{%_Q)L#s}fHW$Gmi$AIb+1J-cN;Z*%XmtC)u;rGhy_*ZK;_vKy)2=Dys zlfi~Nkrryjo9!l1d3tFD?Rq~huUko1m&IQ)w>^kIY#an2OV&IOiP$}Q{FUBe3ysmT z!FRlBh-?bOV-VXh4XNpi+ak{#6vTHzZS6%gy`lS6Z z2aj52{tag@|9)3^Z8^J9KmBIWvvwmS^sJ+|UDja>s7k@g$$3Gj;#ookLt`k9&-?*( z;hR;4uNS9qYAhd+S3F!@W9hY}G55|mj#wTr>C)G$Wx5IfR9du`4xiT_Z(bIz$es>20uSMp*3X+uPQm!vztKAj zj1M+vppuPuNUtCLeqv57^$z?w;6khT4WG^0zK-A14h+sW)!c=x0}4)_95^`X{g`-v>1?=_ z{G-!c(@h&LUX?Jy3re13bQE4ThAvwzDOo!iUMOAS{7~9;3l*0Z9B+zF+B2~HO4-$2 zVO-!M(kezhXWt(?vnE+v76og*H8ameZKSyMPJa^kwrnMh^^_KWM1Xx4 zdc4X;dRIjDS}-ep;wr6{dx*EY&Wdd}9FI6X*jng2geV);8!U-i(IfThhalAf& zJS#d<_Y`k|Ge2P8WHO|AU2c0b7;2#oC34j;C!+%+f55vYBNXFM5nM|)Ro zdW^cz-(r&82R8nsB;@Ij?;Sv19d(TiHY3>kmi;=$u8URfSbC$86NgxKqu9W;Z#cF? iyrX?!`*Hiu;&Bv>mn*qp^YotfM}_0z!~W4dB;j9PcJK@U literal 0 HcmV?d00001 diff --git a/roguelike_tutorial/tutorial_hero.png b/roguelike_tutorial/tutorial_hero.png new file mode 100644 index 0000000000000000000000000000000000000000..c202176caf74bd28325f0e4352e28e35db03d2ec GIT binary patch literal 16742 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