draft tutorial revisions
This commit is contained in:
parent
838da4571d
commit
48359b5a48
70 changed files with 6216 additions and 28 deletions
313
docs/templates/complete/game.py
vendored
Normal file
313
docs/templates/complete/game.py
vendored
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
"""
|
||||
game.py - Main Entry Point for McRogueFace Complete Roguelike Template
|
||||
|
||||
This is the main game file that ties everything together:
|
||||
- Scene setup
|
||||
- Input handling
|
||||
- Game loop
|
||||
- Level transitions
|
||||
|
||||
To run: Copy this template to your McRogueFace scripts/ directory
|
||||
and rename to game.py (or import from game.py).
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from typing import List, Optional
|
||||
|
||||
# Import game modules
|
||||
from constants import (
|
||||
SCREEN_WIDTH, SCREEN_HEIGHT,
|
||||
GRID_X, GRID_Y, GRID_WIDTH, GRID_HEIGHT,
|
||||
DUNGEON_WIDTH, DUNGEON_HEIGHT,
|
||||
TEXTURE_PATH, FONT_PATH,
|
||||
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT,
|
||||
KEY_UP_LEFT, KEY_UP_RIGHT, KEY_DOWN_LEFT, KEY_DOWN_RIGHT,
|
||||
KEY_WAIT, KEY_DESCEND,
|
||||
MSG_WELCOME, MSG_DESCEND, MSG_BLOCKED, MSG_STAIRS, MSG_DEATH, MSG_NO_STAIRS,
|
||||
FOV_RADIUS, COLOR_FOG, COLOR_REMEMBERED, COLOR_VISIBLE
|
||||
)
|
||||
from dungeon import Dungeon, generate_dungeon
|
||||
from entities import Player, Enemy, create_player, create_enemy
|
||||
from turns import TurnManager, GameState
|
||||
from ui import GameUI, DeathScreen
|
||||
|
||||
|
||||
class Game:
|
||||
"""
|
||||
Main game class that manages the complete roguelike experience.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the game."""
|
||||
# Load resources
|
||||
self.texture = mcrfpy.Texture(TEXTURE_PATH, 16, 16)
|
||||
self.font = mcrfpy.Font(FONT_PATH)
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("game")
|
||||
self.ui_collection = mcrfpy.sceneUI("game")
|
||||
|
||||
# Create grid
|
||||
self.grid = mcrfpy.Grid(
|
||||
DUNGEON_WIDTH, DUNGEON_HEIGHT,
|
||||
self.texture,
|
||||
GRID_X, GRID_Y,
|
||||
GRID_WIDTH, GRID_HEIGHT
|
||||
)
|
||||
self.ui_collection.append(self.grid)
|
||||
|
||||
# Game state
|
||||
self.dungeon: Optional[Dungeon] = None
|
||||
self.player: Optional[Player] = None
|
||||
self.enemies: List[Enemy] = []
|
||||
self.turn_manager: Optional[TurnManager] = None
|
||||
self.current_level = 1
|
||||
|
||||
# UI
|
||||
self.game_ui = GameUI(self.font)
|
||||
self.game_ui.add_to_scene(self.ui_collection)
|
||||
|
||||
self.death_screen: Optional[DeathScreen] = None
|
||||
self.game_over = False
|
||||
|
||||
# Set up input handling
|
||||
mcrfpy.keypressScene(self.handle_keypress)
|
||||
|
||||
# Start the game
|
||||
self.new_game()
|
||||
|
||||
# Switch to game scene
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
def new_game(self) -> None:
|
||||
"""Start a new game from level 1."""
|
||||
self.current_level = 1
|
||||
self.game_over = False
|
||||
|
||||
# Clear any death screen
|
||||
if self.death_screen:
|
||||
self.death_screen.remove_from_scene(self.ui_collection)
|
||||
self.death_screen = None
|
||||
|
||||
# Generate first level
|
||||
self.generate_level()
|
||||
|
||||
# Welcome message
|
||||
self.game_ui.clear_messages()
|
||||
self.game_ui.add_message(MSG_WELCOME, (255, 255, 100, 255))
|
||||
|
||||
def generate_level(self) -> None:
|
||||
"""Generate a new dungeon level."""
|
||||
# Clear existing entities from grid
|
||||
while len(self.grid.entities) > 0:
|
||||
self.grid.entities.remove(0)
|
||||
|
||||
self.enemies.clear()
|
||||
|
||||
# Generate dungeon
|
||||
self.dungeon = generate_dungeon(self.current_level)
|
||||
self.dungeon.apply_to_grid(self.grid)
|
||||
|
||||
# Create player at start position
|
||||
start_x, start_y = self.dungeon.player_start
|
||||
self.player = create_player(start_x, start_y, self.texture, self.grid)
|
||||
self.player.dungeon_level = self.current_level
|
||||
|
||||
# Spawn enemies
|
||||
enemy_spawns = self.dungeon.get_enemy_spawns()
|
||||
for enemy_type, x, y in enemy_spawns:
|
||||
enemy = create_enemy(x, y, enemy_type, self.texture, self.grid)
|
||||
self.enemies.append(enemy)
|
||||
|
||||
# Set up turn manager
|
||||
self.turn_manager = TurnManager(self.player, self.enemies, self.dungeon)
|
||||
self.turn_manager.on_message = self.game_ui.add_message
|
||||
self.turn_manager.on_player_death = self.on_player_death
|
||||
|
||||
# Update FOV
|
||||
self.update_fov()
|
||||
|
||||
# Center camera on player
|
||||
self.center_camera()
|
||||
|
||||
# Update UI
|
||||
self.game_ui.update_level(self.current_level)
|
||||
self.update_ui()
|
||||
|
||||
def descend(self) -> None:
|
||||
"""Go down to the next dungeon level."""
|
||||
# Check if player is on stairs
|
||||
if self.player.pos != self.dungeon.stairs_pos:
|
||||
self.game_ui.add_message(MSG_NO_STAIRS, (150, 150, 150, 255))
|
||||
return
|
||||
|
||||
self.current_level += 1
|
||||
self.game_ui.add_message(MSG_DESCEND % self.current_level, (100, 100, 255, 255))
|
||||
|
||||
# Keep player stats
|
||||
old_hp = self.player.fighter.hp
|
||||
old_max_hp = self.player.fighter.max_hp
|
||||
old_attack = self.player.fighter.attack
|
||||
old_defense = self.player.fighter.defense
|
||||
old_xp = self.player.xp
|
||||
old_level = self.player.level
|
||||
|
||||
# Generate new level
|
||||
self.generate_level()
|
||||
|
||||
# Restore player stats
|
||||
self.player.fighter.hp = old_hp
|
||||
self.player.fighter.max_hp = old_max_hp
|
||||
self.player.fighter.attack = old_attack
|
||||
self.player.fighter.defense = old_defense
|
||||
self.player.xp = old_xp
|
||||
self.player.level = old_level
|
||||
|
||||
self.update_ui()
|
||||
|
||||
def update_fov(self) -> None:
|
||||
"""Update field of view and apply to grid tiles."""
|
||||
if not self.player or not self.dungeon:
|
||||
return
|
||||
|
||||
# Use entity's built-in FOV calculation
|
||||
self.player.entity.update_visibility()
|
||||
|
||||
# Apply visibility to tiles
|
||||
for x in range(self.dungeon.width):
|
||||
for y in range(self.dungeon.height):
|
||||
point = self.grid.at(x, y)
|
||||
tile = self.dungeon.get_tile(x, y)
|
||||
|
||||
if tile:
|
||||
state = self.player.entity.at(x, y)
|
||||
|
||||
if state.visible:
|
||||
# Currently visible
|
||||
tile.explored = True
|
||||
tile.visible = True
|
||||
point.color_overlay = mcrfpy.Color(*COLOR_VISIBLE)
|
||||
elif tile.explored:
|
||||
# Explored but not visible
|
||||
tile.visible = False
|
||||
point.color_overlay = mcrfpy.Color(*COLOR_REMEMBERED)
|
||||
else:
|
||||
# Never seen
|
||||
point.color_overlay = mcrfpy.Color(*COLOR_FOG)
|
||||
|
||||
def center_camera(self) -> None:
|
||||
"""Center the camera on the player."""
|
||||
if self.player:
|
||||
self.grid.center = (self.player.x, self.player.y)
|
||||
|
||||
def update_ui(self) -> None:
|
||||
"""Update all UI elements."""
|
||||
if self.player:
|
||||
self.game_ui.update_hp(
|
||||
self.player.fighter.hp,
|
||||
self.player.fighter.max_hp
|
||||
)
|
||||
|
||||
def on_player_death(self) -> None:
|
||||
"""Handle player death."""
|
||||
self.game_over = True
|
||||
self.game_ui.add_message(MSG_DEATH, (255, 0, 0, 255))
|
||||
|
||||
# Show death screen
|
||||
self.death_screen = DeathScreen(self.font)
|
||||
self.death_screen.add_to_scene(self.ui_collection)
|
||||
|
||||
def handle_keypress(self, key: str, state: str) -> None:
|
||||
"""
|
||||
Handle keyboard input.
|
||||
|
||||
Args:
|
||||
key: Key name
|
||||
state: "start" for key down, "end" for key up
|
||||
"""
|
||||
# Only handle key down events
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
# Handle restart when dead
|
||||
if self.game_over:
|
||||
if key == "R":
|
||||
self.new_game()
|
||||
return
|
||||
|
||||
# Handle movement
|
||||
dx, dy = 0, 0
|
||||
|
||||
if key in KEY_UP:
|
||||
dy = -1
|
||||
elif key in KEY_DOWN:
|
||||
dy = 1
|
||||
elif key in KEY_LEFT:
|
||||
dx = -1
|
||||
elif key in KEY_RIGHT:
|
||||
dx = 1
|
||||
elif key in KEY_UP_LEFT:
|
||||
dx, dy = -1, -1
|
||||
elif key in KEY_UP_RIGHT:
|
||||
dx, dy = 1, -1
|
||||
elif key in KEY_DOWN_LEFT:
|
||||
dx, dy = -1, 1
|
||||
elif key in KEY_DOWN_RIGHT:
|
||||
dx, dy = 1, 1
|
||||
elif key in KEY_WAIT:
|
||||
# Skip turn
|
||||
self.turn_manager.handle_wait()
|
||||
self.after_turn()
|
||||
return
|
||||
elif key in KEY_DESCEND:
|
||||
# Try to descend
|
||||
self.descend()
|
||||
return
|
||||
elif key == "Escape":
|
||||
# Quit game
|
||||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
# Process movement/attack
|
||||
if dx != 0 or dy != 0:
|
||||
if self.turn_manager.handle_player_action(dx, dy):
|
||||
self.after_turn()
|
||||
else:
|
||||
# Movement was blocked
|
||||
self.game_ui.add_message(MSG_BLOCKED, (150, 150, 150, 255))
|
||||
|
||||
def after_turn(self) -> None:
|
||||
"""Called after each player turn."""
|
||||
# Update FOV
|
||||
self.update_fov()
|
||||
|
||||
# Center camera
|
||||
self.center_camera()
|
||||
|
||||
# Update UI
|
||||
self.update_ui()
|
||||
|
||||
# Check if standing on stairs
|
||||
if self.player.pos == self.dungeon.stairs_pos:
|
||||
self.game_ui.add_message(MSG_STAIRS, (100, 255, 100, 255))
|
||||
|
||||
# Clean up dead enemies
|
||||
self.enemies = [e for e in self.enemies if e.is_alive]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ENTRY POINT
|
||||
# =============================================================================
|
||||
|
||||
# Global game instance
|
||||
game: Optional[Game] = None
|
||||
|
||||
|
||||
def start_game():
|
||||
"""Start the game."""
|
||||
global game
|
||||
game = Game()
|
||||
|
||||
|
||||
# Auto-start when this script is loaded
|
||||
start_game()
|
||||
Loading…
Add table
Add a link
Reference in a new issue