Draft tutorials
This commit is contained in:
parent
9be3161a24
commit
64ffe1f699
|
@ -9,6 +9,7 @@ obj
|
||||||
build
|
build
|
||||||
lib
|
lib
|
||||||
obj
|
obj
|
||||||
|
__pycache__
|
||||||
|
|
||||||
.cache/
|
.cache/
|
||||||
7DRL2025 Release/
|
7DRL2025 Release/
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Create a new scene called "hello"
|
||||||
|
mcrfpy.createScene("hello")
|
||||||
|
|
||||||
|
# Switch to our new scene
|
||||||
|
mcrfpy.setScene("hello")
|
||||||
|
|
||||||
|
# Get the UI container for our scene
|
||||||
|
ui = mcrfpy.sceneUI("hello")
|
||||||
|
|
||||||
|
# Create a text caption
|
||||||
|
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
|
||||||
|
caption.font_size = 32
|
||||||
|
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
|
||||||
|
|
||||||
|
# Add the caption to our scene
|
||||||
|
ui.append(caption)
|
||||||
|
|
||||||
|
# Create a smaller instruction caption
|
||||||
|
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
|
||||||
|
instruction.font_size = 16
|
||||||
|
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
|
||||||
|
ui.append(instruction)
|
||||||
|
|
||||||
|
# Set up a simple key handler
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state == "start" and key == "Escape":
|
||||||
|
mcrfpy.setScene(None) # This exits the game
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
print("Hello Roguelike is running!")
|
|
@ -0,0 +1,55 @@
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Create our test scene
|
||||||
|
mcrfpy.createScene("test")
|
||||||
|
mcrfpy.setScene("test")
|
||||||
|
ui = mcrfpy.sceneUI("test")
|
||||||
|
|
||||||
|
# Create a background frame
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
|
||||||
|
ui.append(background)
|
||||||
|
|
||||||
|
# Title text
|
||||||
|
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
|
||||||
|
title.font_size = 36
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
|
||||||
|
ui.append(title)
|
||||||
|
|
||||||
|
# Status text that will update
|
||||||
|
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
|
||||||
|
status_text.font_size = 20
|
||||||
|
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
ui.append(status_text)
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
instructions = [
|
||||||
|
"Arrow Keys: Test movement input",
|
||||||
|
"Space: Test action input",
|
||||||
|
"Mouse Click: Test mouse input",
|
||||||
|
"ESC: Exit"
|
||||||
|
]
|
||||||
|
|
||||||
|
y_offset = 400
|
||||||
|
for instruction in instructions:
|
||||||
|
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
|
||||||
|
inst_caption.font_size = 16
|
||||||
|
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
|
ui.append(inst_caption)
|
||||||
|
y_offset += 30
|
||||||
|
|
||||||
|
# Input handler
|
||||||
|
def handle_input(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
if key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
else:
|
||||||
|
status_text.text = f"You pressed: {key}"
|
||||||
|
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
|
||||||
|
|
||||||
|
# Set up input handling
|
||||||
|
mcrfpy.keypressScene(handle_input)
|
||||||
|
|
||||||
|
print("Setup test is running! Try pressing different keys.")
|
|
@ -0,0 +1,162 @@
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Window configuration
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 1"
|
||||||
|
|
||||||
|
# Get the UI container for our scene
|
||||||
|
ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
# Create a dark background
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
ui.append(background)
|
||||||
|
|
||||||
|
# Load the ASCII tileset
|
||||||
|
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the game grid
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 30
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
|
||||||
|
grid.position = (100, 100)
|
||||||
|
grid.size = (800, 480)
|
||||||
|
ui.append(grid)
|
||||||
|
|
||||||
|
def create_room():
|
||||||
|
"""Create a room with walls around the edges"""
|
||||||
|
# Fill everything with floor tiles first
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
cell.sprite_index = 46 # '.' character
|
||||||
|
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
||||||
|
|
||||||
|
# Create walls around the edges
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
# Top wall
|
||||||
|
cell = grid.at(x, 0)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
|
||||||
|
|
||||||
|
# Bottom wall
|
||||||
|
cell = grid.at(x, GRID_HEIGHT - 1)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100)
|
||||||
|
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
# Left wall
|
||||||
|
cell = grid.at(0, y)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100)
|
||||||
|
|
||||||
|
# Right wall
|
||||||
|
cell = grid.at(GRID_WIDTH - 1, y)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100)
|
||||||
|
|
||||||
|
# Create the room
|
||||||
|
create_room()
|
||||||
|
|
||||||
|
# Create the player entity
|
||||||
|
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
|
||||||
|
player.sprite_index = 64 # '@' character
|
||||||
|
player.color = mcrfpy.Color(255, 255, 255) # White
|
||||||
|
|
||||||
|
def move_player(dx, dy):
|
||||||
|
"""Move the player if the destination is walkable"""
|
||||||
|
# Calculate new position
|
||||||
|
new_x = player.x + dx
|
||||||
|
new_y = player.y + dy
|
||||||
|
|
||||||
|
# Check bounds
|
||||||
|
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the destination is walkable
|
||||||
|
destination = grid.at(new_x, new_y)
|
||||||
|
if destination.walkable:
|
||||||
|
# Move the player
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
|
||||||
|
def handle_input(key, state):
|
||||||
|
"""Handle keyboard input for player movement"""
|
||||||
|
# Only process key presses, not releases
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Movement deltas
|
||||||
|
dx, dy = 0, 0
|
||||||
|
|
||||||
|
# Arrow keys
|
||||||
|
if key == "Up":
|
||||||
|
dy = -1
|
||||||
|
elif key == "Down":
|
||||||
|
dy = 1
|
||||||
|
elif key == "Left":
|
||||||
|
dx = -1
|
||||||
|
elif key == "Right":
|
||||||
|
dx = 1
|
||||||
|
|
||||||
|
# Numpad movement (for true roguelike feel!)
|
||||||
|
elif key == "Num7": # Northwest
|
||||||
|
dx, dy = -1, -1
|
||||||
|
elif key == "Num8": # North
|
||||||
|
dy = -1
|
||||||
|
elif key == "Num9": # Northeast
|
||||||
|
dx, dy = 1, -1
|
||||||
|
elif key == "Num4": # West
|
||||||
|
dx = -1
|
||||||
|
elif key == "Num6": # East
|
||||||
|
dx = 1
|
||||||
|
elif key == "Num1": # Southwest
|
||||||
|
dx, dy = -1, 1
|
||||||
|
elif key == "Num2": # South
|
||||||
|
dy = 1
|
||||||
|
elif key == "Num3": # Southeast
|
||||||
|
dx, dy = 1, 1
|
||||||
|
|
||||||
|
# Escape to quit
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If there's movement, try to move the player
|
||||||
|
if dx != 0 or dy != 0:
|
||||||
|
move_player(dx, dy)
|
||||||
|
|
||||||
|
# Register the input handler
|
||||||
|
mcrfpy.keypressScene(handle_input)
|
||||||
|
|
||||||
|
# Add UI elements
|
||||||
|
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
ui.append(instructions)
|
||||||
|
|
||||||
|
status = mcrfpy.Caption("@ You", 100, 600)
|
||||||
|
status.font_size = 18
|
||||||
|
status.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
ui.append(status)
|
||||||
|
|
||||||
|
print("Part 1: The @ symbol moves!")
|
|
@ -0,0 +1,217 @@
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects (player, monsters, items)"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount if possible"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_x = self.x + dx
|
||||||
|
new_y = self.y + dy
|
||||||
|
|
||||||
|
self.x = new_x
|
||||||
|
self.y = new_y
|
||||||
|
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = new_x
|
||||||
|
self._entity.y = new_y
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
self.fill_with_walls()
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, color=(100, 100, 100))
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*color)
|
||||||
|
|
||||||
|
def create_room(self, x1, y1, x2, y2):
|
||||||
|
"""Carve out a room in the map"""
|
||||||
|
x1, x2 = min(x1, x2), max(x1, x2)
|
||||||
|
y1, y2 = min(y1, y2), max(y1, y2)
|
||||||
|
|
||||||
|
for y in range(y1, y2 + 1):
|
||||||
|
for x in range(x1, x2 + 1):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def create_tunnel_h(self, x1, x2, y):
|
||||||
|
"""Create a horizontal tunnel"""
|
||||||
|
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def create_tunnel_v(self, y1, y2, x):
|
||||||
|
"""Create a vertical tunnel"""
|
||||||
|
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
def get_blocking_entity_at(self, x, y):
|
||||||
|
"""Return any blocking entity at the given position"""
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine that manages game state"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 2"
|
||||||
|
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
self.setup_game()
|
||||||
|
self.setup_input()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
self.game_map = GameMap(50, 30)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
self.game_map.create_room(10, 10, 20, 20)
|
||||||
|
self.game_map.create_room(30, 15, 40, 25)
|
||||||
|
self.game_map.create_room(15, 22, 25, 28)
|
||||||
|
|
||||||
|
self.game_map.create_tunnel_h(20, 30, 15)
|
||||||
|
self.game_map.create_tunnel_v(20, 22, 20)
|
||||||
|
|
||||||
|
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
|
||||||
|
self.game_map.add_entity(npc)
|
||||||
|
self.entities.append(npc)
|
||||||
|
|
||||||
|
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
|
||||||
|
self.game_map.add_entity(potion)
|
||||||
|
self.entities.append(potion)
|
||||||
|
|
||||||
|
def handle_movement(self, dx, dy):
|
||||||
|
"""Handle player movement"""
|
||||||
|
new_x = self.player.x + dx
|
||||||
|
new_y = self.player.y + dy
|
||||||
|
|
||||||
|
if not self.game_map.is_blocked(new_x, new_y):
|
||||||
|
self.player.move(dx, dy)
|
||||||
|
else:
|
||||||
|
target = self.game_map.get_blocking_entity_at(new_x, new_y)
|
||||||
|
if target:
|
||||||
|
print(f"You bump into the {target.name}!")
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1), "Down": (0, 1),
|
||||||
|
"Left": (-1, 0), "Right": (1, 0),
|
||||||
|
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0), "Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
self.handle_movement(dx, dy)
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
|
||||||
|
# Create and run the game
|
||||||
|
engine = Engine()
|
||||||
|
print("Part 2: Entities and Maps!")
|
|
@ -0,0 +1,312 @@
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects"""
|
||||||
|
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
self.x += dx
|
||||||
|
self.y += dy
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = self.x
|
||||||
|
self._entity.y = self.y
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, width, height):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self):
|
||||||
|
"""Return the inner area of the room"""
|
||||||
|
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
def tunnel_between(start, end):
|
||||||
|
"""Return an L-shaped tunnel between two points"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
corner_x = x2
|
||||||
|
corner_y = y1
|
||||||
|
else:
|
||||||
|
corner_x = x1
|
||||||
|
corner_y = y2
|
||||||
|
|
||||||
|
# Generate the coordinates
|
||||||
|
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||||
|
yield x, y1
|
||||||
|
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||||
|
yield corner_x, y
|
||||||
|
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||||
|
yield x, corner_y
|
||||||
|
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||||
|
yield x2, y
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
self.rooms = []
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, color=(100, 100, 100))
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*color)
|
||||||
|
|
||||||
|
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
||||||
|
"""Generate a new dungeon map"""
|
||||||
|
self.fill_with_walls()
|
||||||
|
|
||||||
|
for r in range(max_rooms):
|
||||||
|
room_width = random.randint(room_min_size, room_max_size)
|
||||||
|
room_height = random.randint(room_min_size, room_max_size)
|
||||||
|
|
||||||
|
x = random.randint(0, self.width - room_width - 1)
|
||||||
|
y = random.randint(0, self.height - room_height - 1)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.carve_room(new_room)
|
||||||
|
|
||||||
|
if len(self.rooms) == 0:
|
||||||
|
player.x, player.y = new_room.center
|
||||||
|
if player._entity:
|
||||||
|
player._entity.x, player._entity.y = new_room.center
|
||||||
|
else:
|
||||||
|
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||||
|
|
||||||
|
self.rooms.append(new_room)
|
||||||
|
|
||||||
|
def carve_room(self, room):
|
||||||
|
"""Carve out a room"""
|
||||||
|
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||||
|
|
||||||
|
for y in range(inner_y1, inner_y2):
|
||||||
|
for x in range(inner_x1, inner_x2):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def carve_tunnel(self, start, end):
|
||||||
|
"""Carve a tunnel between two points"""
|
||||||
|
for x, y in tunnel_between(start, end):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(30, 30, 40))
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 3"
|
||||||
|
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
self.setup_game()
|
||||||
|
self.setup_input()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
self.game_map = GameMap(80, 45)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
# Create player (before dungeon generation)
|
||||||
|
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||||
|
|
||||||
|
# Generate the dungeon
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=30,
|
||||||
|
room_min_size=6,
|
||||||
|
room_max_size=10,
|
||||||
|
player=self.player
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add player to map
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Add some monsters in random rooms
|
||||||
|
for i in range(5):
|
||||||
|
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
|
||||||
|
room = self.game_map.rooms[i + 1]
|
||||||
|
x, y = room.center
|
||||||
|
|
||||||
|
# Create an orc
|
||||||
|
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||||
|
self.game_map.add_entity(orc)
|
||||||
|
self.entities.append(orc)
|
||||||
|
|
||||||
|
def handle_movement(self, dx, dy):
|
||||||
|
"""Handle player movement"""
|
||||||
|
new_x = self.player.x + dx
|
||||||
|
new_y = self.player.y + dy
|
||||||
|
|
||||||
|
if not self.game_map.is_blocked(new_x, new_y):
|
||||||
|
self.player.move(dx, dy)
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1), "Down": (0, 1),
|
||||||
|
"Left": (-1, 0), "Right": (1, 0),
|
||||||
|
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0), "Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
self.handle_movement(dx, dy)
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
elif key == "Space":
|
||||||
|
# Regenerate the dungeon
|
||||||
|
self.regenerate_dungeon()
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def regenerate_dungeon(self):
|
||||||
|
"""Generate a new dungeon"""
|
||||||
|
# Clear existing entities
|
||||||
|
self.game_map.entities.clear()
|
||||||
|
self.game_map.rooms.clear()
|
||||||
|
self.entities.clear()
|
||||||
|
|
||||||
|
# Clear the entity list in the grid
|
||||||
|
if self.game_map.grid:
|
||||||
|
self.game_map.grid.entities.clear()
|
||||||
|
|
||||||
|
# Regenerate
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=30,
|
||||||
|
room_min_size=6,
|
||||||
|
room_max_size=10,
|
||||||
|
player=self.player
|
||||||
|
)
|
||||||
|
|
||||||
|
# Re-add player
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Add new monsters
|
||||||
|
for i in range(5):
|
||||||
|
if i < len(self.game_map.rooms) - 1:
|
||||||
|
room = self.game_map.rooms[i + 1]
|
||||||
|
x, y = room.center
|
||||||
|
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||||
|
self.game_map.add_entity(orc)
|
||||||
|
self.entities.append(orc)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
|
||||||
|
# Create and run the game
|
||||||
|
engine = Engine()
|
||||||
|
print("Part 3: Procedural Dungeon Generation!")
|
||||||
|
print("Press SPACE to generate a new dungeon")
|
|
@ -0,0 +1,334 @@
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Color configurations for visibility
|
||||||
|
COLORS_VISIBLE = {
|
||||||
|
'wall': (100, 100, 100),
|
||||||
|
'floor': (50, 50, 50),
|
||||||
|
'tunnel': (30, 30, 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects"""
|
||||||
|
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
self.x += dx
|
||||||
|
self.y += dy
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = self.x
|
||||||
|
self._entity.y = self.y
|
||||||
|
# Update FOV when player moves
|
||||||
|
if self.name == "Player":
|
||||||
|
self.update_fov()
|
||||||
|
|
||||||
|
def update_fov(self):
|
||||||
|
"""Update field of view from this entity's position"""
|
||||||
|
if self._entity and self.grid:
|
||||||
|
self._entity.update_fov(radius=8)
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, width, height):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self):
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self):
|
||||||
|
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||||
|
|
||||||
|
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 tunnel_between(start, end):
|
||||||
|
"""Return an L-shaped tunnel between two points"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
corner_x = x2
|
||||||
|
corner_y = y1
|
||||||
|
else:
|
||||||
|
corner_x = x1
|
||||||
|
corner_y = y2
|
||||||
|
|
||||||
|
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||||
|
yield x, y1
|
||||||
|
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||||
|
yield corner_x, y
|
||||||
|
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||||
|
yield x, corner_y
|
||||||
|
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||||
|
yield x2, y
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
self.rooms = []
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
|
||||||
|
# Enable perspective rendering (0 = first entity = player)
|
||||||
|
self.grid.perspective = 0
|
||||||
|
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, tile_type='wall')
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||||
|
|
||||||
|
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
||||||
|
"""Generate a new dungeon map"""
|
||||||
|
self.fill_with_walls()
|
||||||
|
|
||||||
|
for r in range(max_rooms):
|
||||||
|
room_width = random.randint(room_min_size, room_max_size)
|
||||||
|
room_height = random.randint(room_min_size, room_max_size)
|
||||||
|
|
||||||
|
x = random.randint(0, self.width - room_width - 1)
|
||||||
|
y = random.randint(0, self.height - room_height - 1)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.carve_room(new_room)
|
||||||
|
|
||||||
|
if len(self.rooms) == 0:
|
||||||
|
player.x, player.y = new_room.center
|
||||||
|
if player._entity:
|
||||||
|
player._entity.x, player._entity.y = new_room.center
|
||||||
|
else:
|
||||||
|
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||||
|
|
||||||
|
self.rooms.append(new_room)
|
||||||
|
|
||||||
|
def carve_room(self, room):
|
||||||
|
"""Carve out a room"""
|
||||||
|
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||||
|
|
||||||
|
for y in range(inner_y1, inner_y2):
|
||||||
|
for x in range(inner_x1, inner_x2):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='floor')
|
||||||
|
|
||||||
|
def carve_tunnel(self, start, end):
|
||||||
|
"""Carve a tunnel between two points"""
|
||||||
|
for x, y in tunnel_between(start, end):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='tunnel')
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
self.fov_radius = 8
|
||||||
|
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 4"
|
||||||
|
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
self.setup_game()
|
||||||
|
self.setup_input()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
self.game_map = GameMap(80, 45)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
# Create player
|
||||||
|
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||||
|
|
||||||
|
# Generate the dungeon
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=30,
|
||||||
|
room_min_size=6,
|
||||||
|
room_max_size=10,
|
||||||
|
player=self.player
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add player to map
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Add monsters in random rooms
|
||||||
|
for i in range(10):
|
||||||
|
if i < len(self.game_map.rooms) - 1:
|
||||||
|
room = self.game_map.rooms[i + 1]
|
||||||
|
x, y = room.center
|
||||||
|
|
||||||
|
# Randomly offset from center
|
||||||
|
x += random.randint(-2, 2)
|
||||||
|
y += random.randint(-2, 2)
|
||||||
|
|
||||||
|
# Make sure position is walkable
|
||||||
|
if self.game_map.grid.at(x, y).walkable:
|
||||||
|
if i % 2 == 0:
|
||||||
|
# Create an orc
|
||||||
|
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||||
|
self.game_map.add_entity(orc)
|
||||||
|
self.entities.append(orc)
|
||||||
|
else:
|
||||||
|
# Create a troll
|
||||||
|
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
||||||
|
self.game_map.add_entity(troll)
|
||||||
|
self.entities.append(troll)
|
||||||
|
|
||||||
|
# Initial FOV calculation
|
||||||
|
self.player.update_fov()
|
||||||
|
|
||||||
|
def handle_movement(self, dx, dy):
|
||||||
|
"""Handle player movement"""
|
||||||
|
new_x = self.player.x + dx
|
||||||
|
new_y = self.player.y + dy
|
||||||
|
|
||||||
|
if not self.game_map.is_blocked(new_x, new_y):
|
||||||
|
self.player.move(dx, dy)
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1), "Down": (0, 1),
|
||||||
|
"Left": (-1, 0), "Right": (1, 0),
|
||||||
|
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0), "Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
self.handle_movement(dx, dy)
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
elif key == "v":
|
||||||
|
# Toggle FOV on/off
|
||||||
|
if self.game_map.grid.perspective == 0:
|
||||||
|
self.game_map.grid.perspective = -1 # Omniscient
|
||||||
|
print("FOV disabled - omniscient view")
|
||||||
|
else:
|
||||||
|
self.game_map.grid.perspective = 0 # Player perspective
|
||||||
|
print("FOV enabled - player perspective")
|
||||||
|
elif key == "Plus" or key == "Equals":
|
||||||
|
# Increase FOV radius
|
||||||
|
self.fov_radius = min(self.fov_radius + 1, 20)
|
||||||
|
self.player._entity.update_fov(radius=self.fov_radius)
|
||||||
|
print(f"FOV radius: {self.fov_radius}")
|
||||||
|
elif key == "Minus":
|
||||||
|
# Decrease FOV radius
|
||||||
|
self.fov_radius = max(self.fov_radius - 1, 3)
|
||||||
|
self.player._entity.update_fov(radius=self.fov_radius)
|
||||||
|
print(f"FOV radius: {self.fov_radius}")
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
title = mcrfpy.Caption("Field of View", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
|
||||||
|
# FOV indicator
|
||||||
|
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
|
||||||
|
self.fov_text.font_size = 14
|
||||||
|
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
|
||||||
|
self.ui.append(self.fov_text)
|
||||||
|
|
||||||
|
# Create and run the game
|
||||||
|
engine = Engine()
|
||||||
|
print("Part 4: Field of View!")
|
||||||
|
print("Press V to toggle FOV on/off")
|
||||||
|
print("Press +/- to adjust FOV radius")
|
|
@ -0,0 +1,388 @@
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Color configurations
|
||||||
|
COLORS_VISIBLE = {
|
||||||
|
'wall': (100, 100, 100),
|
||||||
|
'floor': (50, 50, 50),
|
||||||
|
'tunnel': (30, 30, 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
class Action:
|
||||||
|
"""Base class for all actions"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MovementAction(Action):
|
||||||
|
"""Action for moving an entity"""
|
||||||
|
def __init__(self, dx, dy):
|
||||||
|
self.dx = dx
|
||||||
|
self.dy = dy
|
||||||
|
|
||||||
|
class WaitAction(Action):
|
||||||
|
"""Action for waiting/skipping turn"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects"""
|
||||||
|
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
self.x += dx
|
||||||
|
self.y += dy
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = self.x
|
||||||
|
self._entity.y = self.y
|
||||||
|
# Update FOV when player moves
|
||||||
|
if self.name == "Player":
|
||||||
|
self.update_fov()
|
||||||
|
|
||||||
|
def update_fov(self):
|
||||||
|
"""Update field of view from this entity's position"""
|
||||||
|
if self._entity and self.grid:
|
||||||
|
self._entity.update_fov(radius=8)
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, width, height):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self):
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self):
|
||||||
|
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||||
|
|
||||||
|
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 tunnel_between(start, end):
|
||||||
|
"""Return an L-shaped tunnel between two points"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
corner_x = x2
|
||||||
|
corner_y = y1
|
||||||
|
else:
|
||||||
|
corner_x = x1
|
||||||
|
corner_y = y2
|
||||||
|
|
||||||
|
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||||
|
yield x, y1
|
||||||
|
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||||
|
yield corner_x, y
|
||||||
|
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||||
|
yield x, corner_y
|
||||||
|
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||||
|
yield x2, y
|
||||||
|
|
||||||
|
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
||||||
|
"""Spawn between 0 and max_enemies in a room"""
|
||||||
|
number_of_enemies = random.randint(0, max_enemies)
|
||||||
|
|
||||||
|
enemies_spawned = []
|
||||||
|
|
||||||
|
for i in range(number_of_enemies):
|
||||||
|
# Try to find a valid position
|
||||||
|
attempts = 10
|
||||||
|
while attempts > 0:
|
||||||
|
# Random position within room bounds
|
||||||
|
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||||
|
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||||
|
|
||||||
|
# Check if position is valid
|
||||||
|
if not game_map.is_blocked(x, y):
|
||||||
|
# 80% chance for orc, 20% for troll
|
||||||
|
if random.random() < 0.8:
|
||||||
|
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||||
|
else:
|
||||||
|
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
||||||
|
|
||||||
|
game_map.add_entity(enemy)
|
||||||
|
enemies_spawned.append(enemy)
|
||||||
|
break
|
||||||
|
|
||||||
|
attempts -= 1
|
||||||
|
|
||||||
|
return enemies_spawned
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
self.rooms = []
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
|
||||||
|
# Enable perspective rendering
|
||||||
|
self.grid.perspective = 0
|
||||||
|
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, tile_type='wall')
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||||
|
|
||||||
|
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
||||||
|
"""Generate a new dungeon map"""
|
||||||
|
self.fill_with_walls()
|
||||||
|
|
||||||
|
for r in range(max_rooms):
|
||||||
|
room_width = random.randint(room_min_size, room_max_size)
|
||||||
|
room_height = random.randint(room_min_size, room_max_size)
|
||||||
|
|
||||||
|
x = random.randint(0, self.width - room_width - 1)
|
||||||
|
y = random.randint(0, self.height - room_height - 1)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.carve_room(new_room)
|
||||||
|
|
||||||
|
if len(self.rooms) == 0:
|
||||||
|
# First room - place player
|
||||||
|
player.x, player.y = new_room.center
|
||||||
|
if player._entity:
|
||||||
|
player._entity.x, player._entity.y = new_room.center
|
||||||
|
else:
|
||||||
|
# All other rooms - add tunnel and enemies
|
||||||
|
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||||
|
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
||||||
|
|
||||||
|
self.rooms.append(new_room)
|
||||||
|
|
||||||
|
def carve_room(self, room):
|
||||||
|
"""Carve out a room"""
|
||||||
|
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||||
|
|
||||||
|
for y in range(inner_y1, inner_y2):
|
||||||
|
for x in range(inner_x1, inner_x2):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='floor')
|
||||||
|
|
||||||
|
def carve_tunnel(self, start, end):
|
||||||
|
"""Carve a tunnel between two points"""
|
||||||
|
for x, y in tunnel_between(start, end):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='tunnel')
|
||||||
|
|
||||||
|
def get_blocking_entity_at(self, x, y):
|
||||||
|
"""Return any blocking entity at the given position"""
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.get_blocking_entity_at(x, y):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 5"
|
||||||
|
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
self.setup_game()
|
||||||
|
self.setup_input()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
self.game_map = GameMap(80, 45)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
# Create player
|
||||||
|
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||||
|
|
||||||
|
# Generate the dungeon
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=30,
|
||||||
|
room_min_size=6,
|
||||||
|
room_max_size=10,
|
||||||
|
player=self.player,
|
||||||
|
max_enemies_per_room=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add player to map
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Store reference to all entities
|
||||||
|
self.entities = [e for e in self.game_map.entities if e != self.player]
|
||||||
|
|
||||||
|
# Initial FOV calculation
|
||||||
|
self.player.update_fov()
|
||||||
|
|
||||||
|
def handle_player_turn(self, action):
|
||||||
|
"""Process the player's action"""
|
||||||
|
if isinstance(action, MovementAction):
|
||||||
|
dest_x = self.player.x + action.dx
|
||||||
|
dest_y = self.player.y + action.dy
|
||||||
|
|
||||||
|
# Check what's at the destination
|
||||||
|
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||||
|
|
||||||
|
if target:
|
||||||
|
# We bumped into something!
|
||||||
|
print(f"You kick the {target.name} in the shins, much to its annoyance!")
|
||||||
|
self.status_text.text = f"You kick the {target.name}!"
|
||||||
|
elif not self.game_map.is_blocked(dest_x, dest_y):
|
||||||
|
# Move the player
|
||||||
|
self.player.move(action.dx, action.dy)
|
||||||
|
self.status_text.text = ""
|
||||||
|
else:
|
||||||
|
# Bumped into a wall
|
||||||
|
self.status_text.text = "Blocked!"
|
||||||
|
|
||||||
|
elif isinstance(action, WaitAction):
|
||||||
|
self.status_text.text = "You wait..."
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
action = None
|
||||||
|
|
||||||
|
# Movement keys
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1), "Down": (0, 1),
|
||||||
|
"Left": (-1, 0), "Right": (1, 0),
|
||||||
|
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
if dx == 0 and dy == 0:
|
||||||
|
action = WaitAction()
|
||||||
|
else:
|
||||||
|
action = MovementAction(dx, dy)
|
||||||
|
elif key == "Period":
|
||||||
|
action = WaitAction()
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process the action
|
||||||
|
if action:
|
||||||
|
self.handle_player_turn(action)
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
title = mcrfpy.Caption("Placing Enemies", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
|
||||||
|
# Status text
|
||||||
|
self.status_text = mcrfpy.Caption("", 512, 600)
|
||||||
|
self.status_text.font_size = 18
|
||||||
|
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
|
||||||
|
self.ui.append(self.status_text)
|
||||||
|
|
||||||
|
# Entity count
|
||||||
|
entity_count = len(self.entities)
|
||||||
|
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
|
||||||
|
count_text.font_size = 14
|
||||||
|
count_text.fill_color = mcrfpy.Color(150, 150, 255)
|
||||||
|
self.ui.append(count_text)
|
||||||
|
|
||||||
|
# Create and run the game
|
||||||
|
engine = Engine()
|
||||||
|
print("Part 5: Placing Enemies!")
|
||||||
|
print("Try bumping into enemies - combat coming in Part 6!")
|
|
@ -0,0 +1,568 @@
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Color configurations
|
||||||
|
COLORS_VISIBLE = {
|
||||||
|
'wall': (100, 100, 100),
|
||||||
|
'floor': (50, 50, 50),
|
||||||
|
'tunnel': (30, 30, 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Message colors
|
||||||
|
COLOR_PLAYER_ATK = (230, 230, 230)
|
||||||
|
COLOR_ENEMY_ATK = (255, 200, 200)
|
||||||
|
COLOR_PLAYER_DIE = (255, 100, 100)
|
||||||
|
COLOR_ENEMY_DIE = (255, 165, 0)
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
class Action:
|
||||||
|
"""Base class for all actions"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MovementAction(Action):
|
||||||
|
"""Action for moving an entity"""
|
||||||
|
def __init__(self, dx, dy):
|
||||||
|
self.dx = dx
|
||||||
|
self.dy = dy
|
||||||
|
|
||||||
|
class MeleeAction(Action):
|
||||||
|
"""Action for melee attacks"""
|
||||||
|
def __init__(self, attacker, target):
|
||||||
|
self.attacker = attacker
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
def perform(self):
|
||||||
|
"""Execute the attack"""
|
||||||
|
if not self.target.is_alive:
|
||||||
|
return None
|
||||||
|
|
||||||
|
damage = self.attacker.power - self.target.defense
|
||||||
|
|
||||||
|
if damage > 0:
|
||||||
|
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
|
||||||
|
self.target.take_damage(damage)
|
||||||
|
|
||||||
|
# Choose color based on attacker
|
||||||
|
if self.attacker.name == "Player":
|
||||||
|
color = COLOR_PLAYER_ATK
|
||||||
|
else:
|
||||||
|
color = COLOR_ENEMY_ATK
|
||||||
|
|
||||||
|
return attack_desc, color
|
||||||
|
else:
|
||||||
|
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
|
||||||
|
return attack_desc, (150, 150, 150)
|
||||||
|
|
||||||
|
class WaitAction(Action):
|
||||||
|
"""Action for waiting/skipping turn"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects"""
|
||||||
|
def __init__(self, x, y, sprite_index, color, name,
|
||||||
|
blocks=False, hp=0, defense=0, power=0):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
# Combat stats
|
||||||
|
self.max_hp = hp
|
||||||
|
self.hp = hp
|
||||||
|
self.defense = defense
|
||||||
|
self.power = power
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_alive(self):
|
||||||
|
"""Returns True if this entity can act"""
|
||||||
|
return self.hp > 0
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
self.x += dx
|
||||||
|
self.y += dy
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = self.x
|
||||||
|
self._entity.y = self.y
|
||||||
|
# Update FOV when player moves
|
||||||
|
if self.name == "Player":
|
||||||
|
self.update_fov()
|
||||||
|
|
||||||
|
def update_fov(self):
|
||||||
|
"""Update field of view from this entity's position"""
|
||||||
|
if self._entity and self.grid:
|
||||||
|
self._entity.update_fov(radius=8)
|
||||||
|
|
||||||
|
def take_damage(self, amount):
|
||||||
|
"""Apply damage to this entity"""
|
||||||
|
self.hp -= amount
|
||||||
|
|
||||||
|
# Check for death
|
||||||
|
if self.hp <= 0:
|
||||||
|
self.die()
|
||||||
|
|
||||||
|
def die(self):
|
||||||
|
"""Handle entity death"""
|
||||||
|
if self.name == "Player":
|
||||||
|
# Player death
|
||||||
|
self.sprite_index = 64 # Stay as @
|
||||||
|
self.color = (127, 0, 0) # Dark red
|
||||||
|
if self._entity:
|
||||||
|
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||||
|
else:
|
||||||
|
# Enemy death
|
||||||
|
self.sprite_index = 37 # % character for corpse
|
||||||
|
self.color = (127, 0, 0) # Dark red
|
||||||
|
self.blocks = False # Corpses don't block
|
||||||
|
self.name = f"remains of {self.name}"
|
||||||
|
|
||||||
|
if self._entity:
|
||||||
|
self._entity.sprite_index = 37
|
||||||
|
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||||
|
|
||||||
|
# Entity factories
|
||||||
|
def create_player(x, y):
|
||||||
|
"""Create the player entity"""
|
||||||
|
return GameObject(
|
||||||
|
x=x, y=y,
|
||||||
|
sprite_index=64, # @
|
||||||
|
color=(255, 255, 255),
|
||||||
|
name="Player",
|
||||||
|
blocks=True,
|
||||||
|
hp=30,
|
||||||
|
defense=2,
|
||||||
|
power=5
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_orc(x, y):
|
||||||
|
"""Create an orc enemy"""
|
||||||
|
return GameObject(
|
||||||
|
x=x, y=y,
|
||||||
|
sprite_index=111, # o
|
||||||
|
color=(63, 127, 63),
|
||||||
|
name="Orc",
|
||||||
|
blocks=True,
|
||||||
|
hp=10,
|
||||||
|
defense=0,
|
||||||
|
power=3
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_troll(x, y):
|
||||||
|
"""Create a troll enemy"""
|
||||||
|
return GameObject(
|
||||||
|
x=x, y=y,
|
||||||
|
sprite_index=84, # T
|
||||||
|
color=(0, 127, 0),
|
||||||
|
name="Troll",
|
||||||
|
blocks=True,
|
||||||
|
hp=16,
|
||||||
|
defense=1,
|
||||||
|
power=4
|
||||||
|
)
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, width, height):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self):
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self):
|
||||||
|
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||||
|
|
||||||
|
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 tunnel_between(start, end):
|
||||||
|
"""Return an L-shaped tunnel between two points"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
corner_x = x2
|
||||||
|
corner_y = y1
|
||||||
|
else:
|
||||||
|
corner_x = x1
|
||||||
|
corner_y = y2
|
||||||
|
|
||||||
|
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||||
|
yield x, y1
|
||||||
|
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||||
|
yield corner_x, y
|
||||||
|
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||||
|
yield x, corner_y
|
||||||
|
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||||
|
yield x2, y
|
||||||
|
|
||||||
|
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
||||||
|
"""Spawn between 0 and max_enemies in a room"""
|
||||||
|
number_of_enemies = random.randint(0, max_enemies)
|
||||||
|
|
||||||
|
enemies_spawned = []
|
||||||
|
|
||||||
|
for i in range(number_of_enemies):
|
||||||
|
attempts = 10
|
||||||
|
while attempts > 0:
|
||||||
|
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||||
|
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||||
|
|
||||||
|
if not game_map.is_blocked(x, y):
|
||||||
|
# 80% chance for orc, 20% for troll
|
||||||
|
if random.random() < 0.8:
|
||||||
|
enemy = create_orc(x, y)
|
||||||
|
else:
|
||||||
|
enemy = create_troll(x, y)
|
||||||
|
|
||||||
|
game_map.add_entity(enemy)
|
||||||
|
enemies_spawned.append(enemy)
|
||||||
|
break
|
||||||
|
|
||||||
|
attempts -= 1
|
||||||
|
|
||||||
|
return enemies_spawned
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
self.rooms = []
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
|
||||||
|
# Enable perspective rendering
|
||||||
|
self.grid.perspective = 0
|
||||||
|
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, tile_type='wall')
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||||
|
|
||||||
|
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
||||||
|
"""Generate a new dungeon map"""
|
||||||
|
self.fill_with_walls()
|
||||||
|
|
||||||
|
for r in range(max_rooms):
|
||||||
|
room_width = random.randint(room_min_size, room_max_size)
|
||||||
|
room_height = random.randint(room_min_size, room_max_size)
|
||||||
|
|
||||||
|
x = random.randint(0, self.width - room_width - 1)
|
||||||
|
y = random.randint(0, self.height - room_height - 1)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.carve_room(new_room)
|
||||||
|
|
||||||
|
if len(self.rooms) == 0:
|
||||||
|
# First room - place player
|
||||||
|
player.x, player.y = new_room.center
|
||||||
|
if player._entity:
|
||||||
|
player._entity.x, player._entity.y = new_room.center
|
||||||
|
else:
|
||||||
|
# All other rooms - add tunnel and enemies
|
||||||
|
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||||
|
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
||||||
|
|
||||||
|
self.rooms.append(new_room)
|
||||||
|
|
||||||
|
def carve_room(self, room):
|
||||||
|
"""Carve out a room"""
|
||||||
|
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||||
|
|
||||||
|
for y in range(inner_y1, inner_y2):
|
||||||
|
for x in range(inner_x1, inner_x2):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='floor')
|
||||||
|
|
||||||
|
def carve_tunnel(self, start, end):
|
||||||
|
"""Carve a tunnel between two points"""
|
||||||
|
for x, y in tunnel_between(start, end):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='tunnel')
|
||||||
|
|
||||||
|
def get_blocking_entity_at(self, x, y):
|
||||||
|
"""Return any blocking entity at the given position"""
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.get_blocking_entity_at(x, y):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
self.messages = [] # Simple message log
|
||||||
|
self.max_messages = 5
|
||||||
|
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 6"
|
||||||
|
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
self.setup_game()
|
||||||
|
self.setup_input()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def add_message(self, text, color=(255, 255, 255)):
|
||||||
|
"""Add a message to the log"""
|
||||||
|
self.messages.append((text, color))
|
||||||
|
if len(self.messages) > self.max_messages:
|
||||||
|
self.messages.pop(0)
|
||||||
|
self.update_message_display()
|
||||||
|
|
||||||
|
def update_message_display(self):
|
||||||
|
"""Update the message display"""
|
||||||
|
# Clear old messages
|
||||||
|
for caption in self.message_captions:
|
||||||
|
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
|
||||||
|
caption.text = ""
|
||||||
|
|
||||||
|
# Display current messages
|
||||||
|
for i, (text, color) in enumerate(self.messages):
|
||||||
|
if i < len(self.message_captions):
|
||||||
|
self.message_captions[i].text = text
|
||||||
|
self.message_captions[i].fill_color = mcrfpy.Color(*color)
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
self.game_map = GameMap(80, 45)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
# Create player
|
||||||
|
self.player = create_player(0, 0)
|
||||||
|
|
||||||
|
# Generate the dungeon
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=30,
|
||||||
|
room_min_size=6,
|
||||||
|
room_max_size=10,
|
||||||
|
player=self.player,
|
||||||
|
max_enemies_per_room=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add player to map
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Store reference to all entities
|
||||||
|
self.entities = [e for e in self.game_map.entities if e != self.player]
|
||||||
|
|
||||||
|
# Initial FOV calculation
|
||||||
|
self.player.update_fov()
|
||||||
|
|
||||||
|
# Welcome message
|
||||||
|
self.add_message("Welcome to the dungeon!", (100, 100, 255))
|
||||||
|
|
||||||
|
def handle_player_turn(self, action):
|
||||||
|
"""Process the player's action"""
|
||||||
|
if not self.player.is_alive:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(action, MovementAction):
|
||||||
|
dest_x = self.player.x + action.dx
|
||||||
|
dest_y = self.player.y + action.dy
|
||||||
|
|
||||||
|
# Check what's at the destination
|
||||||
|
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||||
|
|
||||||
|
if target:
|
||||||
|
# Attack!
|
||||||
|
attack = MeleeAction(self.player, target)
|
||||||
|
result = attack.perform()
|
||||||
|
if result:
|
||||||
|
text, color = result
|
||||||
|
self.add_message(text, color)
|
||||||
|
|
||||||
|
# Check if target died
|
||||||
|
if not target.is_alive:
|
||||||
|
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
|
||||||
|
self.add_message(death_msg, COLOR_ENEMY_DIE)
|
||||||
|
|
||||||
|
elif not self.game_map.is_blocked(dest_x, dest_y):
|
||||||
|
# Move the player
|
||||||
|
self.player.move(action.dx, action.dy)
|
||||||
|
|
||||||
|
elif isinstance(action, WaitAction):
|
||||||
|
pass # Do nothing
|
||||||
|
|
||||||
|
# Enemy turns
|
||||||
|
self.handle_enemy_turns()
|
||||||
|
|
||||||
|
def handle_enemy_turns(self):
|
||||||
|
"""Let all enemies take their turn"""
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.is_alive:
|
||||||
|
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
|
||||||
|
dx = entity.x - self.player.x
|
||||||
|
dy = entity.y - self.player.y
|
||||||
|
distance = abs(dx) + abs(dy)
|
||||||
|
|
||||||
|
if distance == 1: # Adjacent to player
|
||||||
|
attack = MeleeAction(entity, self.player)
|
||||||
|
result = attack.perform()
|
||||||
|
if result:
|
||||||
|
text, color = result
|
||||||
|
self.add_message(text, color)
|
||||||
|
|
||||||
|
# Check if player died
|
||||||
|
if not self.player.is_alive:
|
||||||
|
self.add_message("You have died!", COLOR_PLAYER_DIE)
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
action = None
|
||||||
|
|
||||||
|
# Movement keys
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1), "Down": (0, 1),
|
||||||
|
"Left": (-1, 0), "Right": (1, 0),
|
||||||
|
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
if dx == 0 and dy == 0:
|
||||||
|
action = WaitAction()
|
||||||
|
else:
|
||||||
|
action = MovementAction(dx, dy)
|
||||||
|
elif key == "Period":
|
||||||
|
action = WaitAction()
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process the action
|
||||||
|
if action:
|
||||||
|
self.handle_player_turn(action)
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
title = mcrfpy.Caption("Combat System", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
|
||||||
|
# Player stats
|
||||||
|
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
|
||||||
|
self.hp_text.font_size = 18
|
||||||
|
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
self.ui.append(self.hp_text)
|
||||||
|
|
||||||
|
# Message log
|
||||||
|
self.message_captions = []
|
||||||
|
for i in range(self.max_messages):
|
||||||
|
caption = mcrfpy.Caption("", 50, 620 + i * 20)
|
||||||
|
caption.font_size = 14
|
||||||
|
caption.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(caption)
|
||||||
|
self.message_captions.append(caption)
|
||||||
|
|
||||||
|
# Timer to update HP display
|
||||||
|
def update_stats(dt):
|
||||||
|
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
|
||||||
|
if self.player.hp <= 0:
|
||||||
|
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
|
||||||
|
elif self.player.hp < self.player.max_hp // 3:
|
||||||
|
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
else:
|
||||||
|
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
|
||||||
|
|
||||||
|
mcrfpy.setTimer("update_stats", update_stats, 100)
|
||||||
|
|
||||||
|
# Create and run the game
|
||||||
|
engine = Engine()
|
||||||
|
print("Part 6: Combat System!")
|
||||||
|
print("Attack enemies to defeat them, but watch your HP!")
|
Loading…
Reference in New Issue