Compare commits
No commits in common. "main" and "master" have entirely different histories.
185
settings.py
185
settings.py
|
|
@ -1,185 +0,0 @@
|
||||||
# ==========================================
|
|
||||||
# WINDOW / DISPLAY SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
SCREEN_WIDTH = 1280
|
|
||||||
SCREEN_HEIGHT = 720
|
|
||||||
FPS = 60
|
|
||||||
VSYNC = True
|
|
||||||
FULLSCREEN = False
|
|
||||||
|
|
||||||
GAME_TITLE = "Terraria Clone"
|
|
||||||
|
|
||||||
BACKGROUND_COLOR = (135, 206, 235) # Sky blue
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# TILE SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
TILE_SIZE = 32
|
|
||||||
CHUNK_SIZE = 16 # 16x16 tiles per chunk
|
|
||||||
RENDER_DISTANCE = 3 # how many chunks visible around player
|
|
||||||
|
|
||||||
# Tile IDs
|
|
||||||
AIR = 0
|
|
||||||
DIRT = 1
|
|
||||||
GRASS = 2
|
|
||||||
STONE = 3
|
|
||||||
WOOD = 4
|
|
||||||
LEAVES = 5
|
|
||||||
IRON_ORE = 6
|
|
||||||
GOLD_ORE = 7
|
|
||||||
COPPER_ORE = 8
|
|
||||||
COAL_ORE = 9
|
|
||||||
|
|
||||||
# Items
|
|
||||||
ITEM_WOOD = 100
|
|
||||||
ITEM_STONE = 101
|
|
||||||
ITEM_IRON = 102
|
|
||||||
ITEM_GOLD = 103
|
|
||||||
ITEM_COPPER = 104
|
|
||||||
ITEM_COAL = 105
|
|
||||||
|
|
||||||
# Weapons
|
|
||||||
ITEM_IRON_SWORD = 200
|
|
||||||
ITEM_WOODEN_SWORD = 201
|
|
||||||
ITEM_GOLD_SWORD = 202
|
|
||||||
|
|
||||||
# Tile properties
|
|
||||||
TILE_PROPERTIES = {
|
|
||||||
AIR: {"solid": False, "color": (0, 0, 0), "drop": None},
|
|
||||||
DIRT: {"solid": True, "color": (139, 69, 19), "drop": ITEM_STONE},
|
|
||||||
GRASS: {"solid": True, "color": (34, 177, 76), "drop": ITEM_STONE},
|
|
||||||
STONE: {"solid": True, "color": (100, 100, 100), "drop": ITEM_STONE},
|
|
||||||
WOOD: {"solid": True, "color": (160, 82, 45), "drop": ITEM_WOOD},
|
|
||||||
LEAVES: {"solid": False, "color": (34, 139, 34), "drop": None},
|
|
||||||
IRON_ORE: {"solid": True, "color": (180, 180, 180), "drop": ITEM_IRON},
|
|
||||||
GOLD_ORE: {"solid": True, "color": (255, 215, 0), "drop": ITEM_GOLD},
|
|
||||||
COPPER_ORE: {"solid": True, "color": (210, 120, 60), "drop": ITEM_COPPER},
|
|
||||||
COAL_ORE: {"solid": True, "color": (40, 40, 40), "drop": ITEM_COAL},
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# WORLD GENERATION
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
WORLD_WIDTH = 200 # in tiles
|
|
||||||
WORLD_HEIGHT = 100 # in tiles
|
|
||||||
|
|
||||||
SEED = 42
|
|
||||||
|
|
||||||
SURFACE_LEVEL = 40
|
|
||||||
CAVE_THRESHOLD = 0.4
|
|
||||||
ORE_THRESHOLD = 0.75
|
|
||||||
|
|
||||||
NOISE_SCALE = 0.05
|
|
||||||
OCTAVES = 4
|
|
||||||
|
|
||||||
PERLIN_SCALE = 80.0
|
|
||||||
PERLIN_OCTAVES = 6
|
|
||||||
PERLIN_PERSISTENCE = 0.5
|
|
||||||
PERLIN_LACUNARITY = 2.0
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# PLAYER SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
PLAYER_WIDTH = 28
|
|
||||||
PLAYER_HEIGHT = 48
|
|
||||||
|
|
||||||
PLAYER_SPEED = 250
|
|
||||||
PLAYER_ACCELERATION = 2000
|
|
||||||
PLAYER_FRICTION = -0.15
|
|
||||||
|
|
||||||
GRAVITY = 2200
|
|
||||||
MAX_FALL_SPEED = 1000
|
|
||||||
|
|
||||||
JUMP_FORCE = -700
|
|
||||||
DOUBLE_JUMP = False
|
|
||||||
|
|
||||||
MAX_HEALTH = 100
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# CAMERA SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
CAMERA_SMOOTHING = 0.1
|
|
||||||
CAMERA_OFFSET_Y = -100
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# INVENTORY SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
INVENTORY_SIZE = 40
|
|
||||||
HOTBAR_SIZE = 10
|
|
||||||
STACK_LIMIT = 999
|
|
||||||
|
|
||||||
INVENTORY_SLOT_SIZE = 40
|
|
||||||
INVENTORY_PADDING = 4
|
|
||||||
|
|
||||||
HOTBAR_VISIBLE = True
|
|
||||||
HOTBAR_SLOT_COUNT = 8
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# COMBAT SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
WEAPON_STATS = {
|
|
||||||
ITEM_WOODEN_SWORD: {"name": "Wooden Sword", "damage": 8, "cooldown": 0.6, "knockback": 300, "color": (160, 82, 45)},
|
|
||||||
ITEM_IRON_SWORD: {"name": "Iron Sword", "damage": 15, "cooldown": 0.8, "knockback": 400, "color": (180, 180, 180)},
|
|
||||||
ITEM_GOLD_SWORD: {"name": "Gold Sword", "damage": 20, "cooldown": 1.0, "knockback": 500, "color": (255, 215, 0)},
|
|
||||||
}
|
|
||||||
|
|
||||||
ATTACK_RANGE = 60
|
|
||||||
ATTACK_COOLDOWN = 0.5
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# BLOCK BREAKING / PLACING
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
BREAK_RANGE = 5
|
|
||||||
BREAK_TIME = {
|
|
||||||
DIRT: 0.3, GRASS: 0.3, STONE: 0.8,
|
|
||||||
WOOD: 0.5, IRON_ORE: 1.2, GOLD_ORE: 1.5,
|
|
||||||
COPPER_ORE: 1.0, COAL_ORE: 0.9,
|
|
||||||
}
|
|
||||||
|
|
||||||
PLACE_RANGE = 5
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# ENEMY SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
MAX_ENEMIES = 10
|
|
||||||
ENEMY_SPAWN_RATE = 5 # seconds
|
|
||||||
ENEMY_SPEED = 100
|
|
||||||
ENEMY_HEALTH = 50
|
|
||||||
ENEMY_GRAVITY = 2200
|
|
||||||
ENEMY_KNOCKBACK_RESISTANCE = 0.8
|
|
||||||
|
|
||||||
# Added missing constants for combat
|
|
||||||
ENEMY_ATTACK_DAMAGE = 10
|
|
||||||
ENEMY_ATTACK_RANGE = 50
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# PHYSICS SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
TERMINAL_VELOCITY = 1200
|
|
||||||
COLLISION_STEPS = 4
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# LIGHTING
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
ENABLE_LIGHTING = False
|
|
||||||
LIGHT_RADIUS = 5
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# DEBUG SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
DEBUG_MODE = True
|
|
||||||
SHOW_FPS = True
|
|
||||||
SHOW_COLLIDERS = False
|
|
||||||
SHOW_CHUNK_BORDERS = False
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,61 +0,0 @@
|
||||||
# combat_system.py
|
|
||||||
import random
|
|
||||||
from settings import *
|
|
||||||
from items import get_item, ITEMS, WEAPON_STATS
|
|
||||||
|
|
||||||
class CombatSystem:
|
|
||||||
def __init__(self, weapon_manager):
|
|
||||||
self.weapon_manager = weapon_manager
|
|
||||||
|
|
||||||
def player_attack(self, player, enemies):
|
|
||||||
"""
|
|
||||||
Handles player attack on enemies using the currently equipped weapon.
|
|
||||||
"""
|
|
||||||
# Get weapon ID from weapon manager
|
|
||||||
weapon_id = self.weapon_manager.get_current_weapon()
|
|
||||||
if weapon_id is None:
|
|
||||||
print("DEBUG: No weapon equipped")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get weapon stats
|
|
||||||
damage = WEAPON_STATS.get(weapon_id, {}).get("damage", 1)
|
|
||||||
attack_range = WEAPON_STATS.get(weapon_id, {}).get("range", 50)
|
|
||||||
|
|
||||||
player_pos = player.pos
|
|
||||||
|
|
||||||
# Attack enemies within range
|
|
||||||
for enemy in enemies:
|
|
||||||
if not getattr(enemy, "alive", True):
|
|
||||||
continue # Skip dead enemies
|
|
||||||
|
|
||||||
enemy_pos = enemy.pos
|
|
||||||
distance = (enemy_pos - player_pos).length()
|
|
||||||
if distance <= attack_range:
|
|
||||||
enemy.health -= damage
|
|
||||||
print(f"DEBUG: {enemy.name if hasattr(enemy,'name') else 'Enemy'} took {damage} damage! (HP left: {enemy.health})")
|
|
||||||
if enemy.health <= 0:
|
|
||||||
enemy.alive = False
|
|
||||||
print(f"DEBUG: {enemy.name if hasattr(enemy,'name') else 'Enemy'} has been defeated!")
|
|
||||||
|
|
||||||
def enemy_attack(self, enemy, player):
|
|
||||||
"""
|
|
||||||
Handles enemy attack on player.
|
|
||||||
"""
|
|
||||||
if not getattr(enemy, "alive", True):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ensure enemy has attack properties
|
|
||||||
attack_range = getattr(enemy, "attack_range", 50)
|
|
||||||
attack_damage = getattr(enemy, "attack_damage", ENEMY_ATTACK_DAMAGE)
|
|
||||||
|
|
||||||
distance = (enemy.pos - player.pos).length()
|
|
||||||
if distance <= attack_range:
|
|
||||||
player.health -= attack_damage
|
|
||||||
print(f"DEBUG: Player took {attack_damage} damage! (HP left: {player.health})")
|
|
||||||
|
|
||||||
def update(self, dt):
|
|
||||||
"""
|
|
||||||
Updates combat-related state per frame.
|
|
||||||
Currently placeholder for cooldowns or status effects.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
126
enemy.py
126
enemy.py
|
|
@ -3,131 +3,15 @@ from settings import *
|
||||||
|
|
||||||
class Enemy:
|
class Enemy:
|
||||||
def __init__(self, x, y):
|
def __init__(self, x, y):
|
||||||
# Position & physics
|
|
||||||
self.pos = pygame.Vector2(x, y)
|
|
||||||
self.rect = pygame.Rect(x, y, 28, 48)
|
self.rect = pygame.Rect(x, y, 28, 48)
|
||||||
self.velocity = pygame.Vector2(0, 0)
|
self.velocity = pygame.Vector2(0, 0)
|
||||||
|
|
||||||
# Health
|
|
||||||
self.health = ENEMY_HEALTH
|
|
||||||
self.max_health = ENEMY_HEALTH
|
|
||||||
self.alive = True
|
|
||||||
|
|
||||||
# Combat
|
|
||||||
self.attack_rect = pygame.Rect(x, y, 60, 60)
|
|
||||||
self.attack_damage = ENEMY_ATTACK_DAMAGE
|
|
||||||
self.attack_range = ENEMY_ATTACK_RANGE
|
|
||||||
|
|
||||||
# Timers
|
|
||||||
self.knockback_timer = 0
|
|
||||||
self.invulnerable_timer = 0
|
|
||||||
|
|
||||||
def update(self, dt, world, player):
|
def update(self, dt, world, player):
|
||||||
if not self.alive:
|
# Basic AI: follow player
|
||||||
return
|
if player.rect.x < self.rect.x:
|
||||||
|
self.rect.x -= ENEMY_SPEED * dt
|
||||||
dt = min(dt, 0.05)
|
|
||||||
|
|
||||||
# Gravity
|
|
||||||
self.velocity.y += ENEMY_GRAVITY * dt
|
|
||||||
if self.velocity.y > MAX_FALL_SPEED:
|
|
||||||
self.velocity.y = MAX_FALL_SPEED
|
|
||||||
|
|
||||||
# Basic AI: move toward player
|
|
||||||
if self.knockback_timer <= 0:
|
|
||||||
if player.rect.x < self.rect.x:
|
|
||||||
self.velocity.x = -ENEMY_SPEED
|
|
||||||
else:
|
|
||||||
self.velocity.x = ENEMY_SPEED
|
|
||||||
else:
|
else:
|
||||||
self.knockback_timer -= dt
|
self.rect.x += ENEMY_SPEED * dt
|
||||||
|
|
||||||
# Update horizontal position
|
|
||||||
self.pos.x += self.velocity.x * dt
|
|
||||||
self.rect.x = round(self.pos.x)
|
|
||||||
self.handle_horizontal_collisions(world)
|
|
||||||
|
|
||||||
# Update vertical position
|
|
||||||
self.pos.y += self.velocity.y * dt
|
|
||||||
self.rect.y = round(self.pos.y)
|
|
||||||
self.handle_vertical_collisions(world)
|
|
||||||
|
|
||||||
# Update attack rectangle
|
|
||||||
self.attack_rect.center = self.rect.center
|
|
||||||
|
|
||||||
# Decay invulnerability
|
|
||||||
if self.invulnerable_timer > 0:
|
|
||||||
self.invulnerable_timer -= dt
|
|
||||||
|
|
||||||
# Check death
|
|
||||||
if self.health <= 0:
|
|
||||||
self.alive = False
|
|
||||||
|
|
||||||
def handle_horizontal_collisions(self, world):
|
|
||||||
for tile in world.get_nearby_tiles(self.rect):
|
|
||||||
if tile["solid"] and self.rect.colliderect(tile["rect"]):
|
|
||||||
if self.velocity.x > 0:
|
|
||||||
self.rect.right = tile["rect"].left
|
|
||||||
elif self.velocity.x < 0:
|
|
||||||
self.rect.left = tile["rect"].right
|
|
||||||
self.pos.x = self.rect.x
|
|
||||||
self.velocity.x = 0
|
|
||||||
|
|
||||||
def handle_vertical_collisions(self, world):
|
|
||||||
for tile in world.get_nearby_tiles(self.rect):
|
|
||||||
if tile["solid"] and self.rect.colliderect(tile["rect"]):
|
|
||||||
if self.velocity.y > 0:
|
|
||||||
self.rect.bottom = tile["rect"].top
|
|
||||||
elif self.velocity.y < 0:
|
|
||||||
self.rect.top = tile["rect"].bottom
|
|
||||||
self.pos.y = self.rect.y
|
|
||||||
self.velocity.y = 0
|
|
||||||
|
|
||||||
def take_damage(self, damage, direction):
|
|
||||||
if self.invulnerable_timer > 0 or not self.alive:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.health -= damage
|
|
||||||
self.invulnerable_timer = 0.3
|
|
||||||
self.knockback_timer = 0.15
|
|
||||||
|
|
||||||
# Knockback
|
|
||||||
knockback_force = 400 * ENEMY_KNOCKBACK_RESISTANCE
|
|
||||||
self.velocity.x = direction * knockback_force
|
|
||||||
self.velocity.y = -300
|
|
||||||
|
|
||||||
if self.health <= 0:
|
|
||||||
self.alive = False
|
|
||||||
|
|
||||||
def draw(self, screen, camera):
|
def draw(self, screen, camera):
|
||||||
if not self.alive:
|
pygame.draw.rect(screen, (0, 0, 255), camera.apply(self.rect))
|
||||||
return
|
|
||||||
|
|
||||||
draw_rect = camera.apply(self.rect)
|
|
||||||
|
|
||||||
# Flash when invulnerable
|
|
||||||
color = (100, 100, 255) if self.invulnerable_timer > 0 else (0, 0, 255)
|
|
||||||
pygame.draw.rect(screen, color, draw_rect)
|
|
||||||
|
|
||||||
if SHOW_COLLIDERS:
|
|
||||||
pygame.draw.rect(screen, (0, 255, 0), draw_rect, 2)
|
|
||||||
attack_draw_rect = camera.apply(self.attack_rect)
|
|
||||||
pygame.draw.rect(screen, (255, 0, 0), attack_draw_rect, 1)
|
|
||||||
|
|
||||||
# Health bar
|
|
||||||
health_bar_width = 28
|
|
||||||
health_bar_height = 4
|
|
||||||
health_percent = max(0, self.health / self.max_health)
|
|
||||||
health_bar_rect = pygame.Rect(
|
|
||||||
draw_rect.x,
|
|
||||||
draw_rect.y - 8,
|
|
||||||
health_bar_width * health_percent,
|
|
||||||
health_bar_height
|
|
||||||
)
|
|
||||||
pygame.draw.rect(screen, (0, 255, 0), health_bar_rect)
|
|
||||||
pygame.draw.rect(screen, (255, 0, 0), pygame.Rect(
|
|
||||||
draw_rect.x,
|
|
||||||
draw_rect.y - 8,
|
|
||||||
health_bar_width,
|
|
||||||
health_bar_height
|
|
||||||
), 1)
|
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,6 @@ class EnemyManager:
|
||||||
for enemy in self.enemies:
|
for enemy in self.enemies:
|
||||||
enemy.update(dt, self.world, self.player)
|
enemy.update(dt, self.world, self.player)
|
||||||
|
|
||||||
# Remove dead enemies
|
|
||||||
self.enemies = [enemy for enemy in self.enemies if enemy.health > 0]
|
|
||||||
|
|
||||||
def draw(self, screen, camera):
|
def draw(self, screen, camera):
|
||||||
for enemy in self.enemies:
|
for enemy in self.enemies:
|
||||||
enemy.draw(screen, camera)
|
enemy.draw(screen, camera)
|
||||||
|
|
|
||||||
400
game.py
400
game.py
|
|
@ -6,103 +6,38 @@ from player import Player
|
||||||
from camera import Camera
|
from camera import Camera
|
||||||
from inventory import Inventory
|
from inventory import Inventory
|
||||||
from enemy_manager import EnemyManager
|
from enemy_manager import EnemyManager
|
||||||
from menu_system import MainMenu, PauseMenu, NewGameDialog, LoadGameDialog, SettingsMenu, DeathScreen
|
|
||||||
from save_system import SaveSystem
|
|
||||||
from weapon_system import WeaponManager
|
|
||||||
from combat_system import CombatSystem
|
|
||||||
from hotbar import Hotbar
|
|
||||||
from items import get_item
|
|
||||||
|
|
||||||
|
|
||||||
class Game:
|
class Game:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pygame.init()
|
pygame.init() # <-- IMPORTANT
|
||||||
|
|
||||||
|
self.show_inventory = False
|
||||||
|
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
# Display Setup
|
# Display Setup
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
self.setup_display()
|
flags = pygame.SCALED
|
||||||
|
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), flags)
|
||||||
|
pygame.display.set_caption(GAME_TITLE)
|
||||||
|
|
||||||
self.clock = pygame.time.Clock()
|
self.clock = pygame.time.Clock()
|
||||||
self.running = True
|
self.running = True
|
||||||
self.paused = False
|
self.paused = False
|
||||||
|
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
# Game State Management
|
# Core Systems
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
self.game_state = "menu"
|
self.world = World()
|
||||||
self.show_inventory = False
|
self.player = Player(100, 100)
|
||||||
self.world_name = "Default"
|
self.camera = Camera(self.player)
|
||||||
|
self.inventory = Inventory()
|
||||||
# ----------------------------------
|
self.enemy_manager = EnemyManager(self.world, self.player)
|
||||||
# Menu Systems
|
|
||||||
# ----------------------------------
|
|
||||||
self.main_menu = MainMenu(self)
|
|
||||||
self.new_game_dialog = None
|
|
||||||
self.load_game_dialog = None
|
|
||||||
self.pause_menu = None
|
|
||||||
self.settings_menu = None
|
|
||||||
self.death_screen = None
|
|
||||||
self.save_system = SaveSystem()
|
|
||||||
|
|
||||||
# ----------------------------------
|
|
||||||
# Core Systems (initialized as None)
|
|
||||||
# ----------------------------------
|
|
||||||
self.world = None
|
|
||||||
self.player = None
|
|
||||||
self.camera = None
|
|
||||||
self.inventory = None
|
|
||||||
self.enemy_manager = None
|
|
||||||
self.weapon_manager = None
|
|
||||||
self.combat_system = None
|
|
||||||
self.hotbar = None
|
|
||||||
|
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
# Debug
|
# Debug
|
||||||
# ----------------------------------
|
|
||||||
self.font = pygame.font.SysFont("consolas", 18)
|
self.font = pygame.font.SysFont("consolas", 18)
|
||||||
|
|
||||||
def setup_display(self):
|
|
||||||
"""Setup display with current resolution and fullscreen setting"""
|
|
||||||
flags = pygame.SCALED
|
|
||||||
if FULLSCREEN:
|
|
||||||
flags |= pygame.FULLSCREEN
|
|
||||||
|
|
||||||
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), flags)
|
|
||||||
pygame.display.set_caption(GAME_TITLE)
|
|
||||||
|
|
||||||
def toggle_fullscreen(self):
|
|
||||||
"""Toggle fullscreen mode"""
|
|
||||||
global FULLSCREEN
|
|
||||||
FULLSCREEN = not FULLSCREEN
|
|
||||||
self.setup_display()
|
|
||||||
|
|
||||||
def init_game(self):
|
|
||||||
"""Initialize game systems (called when starting new game or loading)"""
|
|
||||||
self.world = World()
|
|
||||||
self.weapon_manager = WeaponManager()
|
|
||||||
self.player = Player(100, 100, self.weapon_manager)
|
|
||||||
self.camera = Camera(self.player)
|
|
||||||
self.inventory = Inventory()
|
|
||||||
self.hotbar = Hotbar(self.inventory, self.weapon_manager)
|
|
||||||
self.enemy_manager = EnemyManager(self.world, self.player)
|
|
||||||
self.combat_system = CombatSystem(self.weapon_manager)
|
|
||||||
self.show_inventory = False
|
|
||||||
|
|
||||||
# Add starting items and WEAPONS
|
|
||||||
self.inventory.add_item(ITEM_WOOD, 10)
|
|
||||||
self.inventory.add_item(ITEM_STONE, 5)
|
|
||||||
|
|
||||||
# Add all weapons to inventory
|
|
||||||
self.inventory.add_item(ITEM_WOODEN_SWORD, 1)
|
|
||||||
self.inventory.add_item(ITEM_IRON_SWORD, 1)
|
|
||||||
self.inventory.add_item(ITEM_GOLD_SWORD, 1)
|
|
||||||
|
|
||||||
# Add weapons to weapon manager
|
|
||||||
self.weapon_manager.add_weapon(ITEM_WOODEN_SWORD)
|
|
||||||
self.weapon_manager.add_weapon(ITEM_IRON_SWORD)
|
|
||||||
self.weapon_manager.add_weapon(ITEM_GOLD_SWORD)
|
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# MAIN LOOP
|
# MAIN LOOP
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
@ -110,185 +45,17 @@ class Game:
|
||||||
while self.running:
|
while self.running:
|
||||||
dt = self.clock.tick(FPS) / 1000
|
dt = self.clock.tick(FPS) / 1000
|
||||||
|
|
||||||
if self.game_state == "menu":
|
self.handle_events()
|
||||||
self.handle_menu_events()
|
|
||||||
self.update_menu()
|
|
||||||
self.draw_menu()
|
|
||||||
|
|
||||||
elif self.game_state == "settings":
|
if not self.paused:
|
||||||
self.handle_settings_events()
|
self.update(dt)
|
||||||
self.settings_menu.update(dt)
|
|
||||||
self.settings_menu.draw(self.screen)
|
|
||||||
|
|
||||||
elif self.game_state == "new_game_dialog":
|
self.draw()
|
||||||
self.handle_new_game_dialog_events()
|
|
||||||
self.new_game_dialog.update(dt)
|
|
||||||
self.new_game_dialog.draw(self.screen)
|
|
||||||
|
|
||||||
elif self.game_state == "load_game_dialog":
|
|
||||||
self.handle_load_game_dialog_events()
|
|
||||||
self.load_game_dialog.update()
|
|
||||||
self.load_game_dialog.draw(self.screen)
|
|
||||||
|
|
||||||
elif self.game_state == "playing":
|
|
||||||
self.handle_events()
|
|
||||||
if not self.paused:
|
|
||||||
self.update(dt)
|
|
||||||
self.draw()
|
|
||||||
|
|
||||||
elif self.game_state == "paused":
|
|
||||||
self.handle_pause_events()
|
|
||||||
self.pause_menu.update(dt)
|
|
||||||
self.draw_paused()
|
|
||||||
|
|
||||||
elif self.game_state == "dead":
|
|
||||||
self.handle_death_events()
|
|
||||||
self.death_screen.update(dt)
|
|
||||||
self.draw_dead()
|
|
||||||
|
|
||||||
pygame.quit()
|
pygame.quit()
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# MAIN MENU STATE
|
# EVENTS
|
||||||
# ==========================================================
|
|
||||||
def handle_menu_events(self):
|
|
||||||
action = self.main_menu.handle_events()
|
|
||||||
|
|
||||||
if action == "new_game":
|
|
||||||
self.new_game_dialog = NewGameDialog(self)
|
|
||||||
self.game_state = "new_game_dialog"
|
|
||||||
|
|
||||||
elif action == "load_game":
|
|
||||||
self.load_game_dialog = LoadGameDialog(self, self.save_system)
|
|
||||||
self.game_state = "load_game_dialog"
|
|
||||||
|
|
||||||
elif action == "settings":
|
|
||||||
self.settings_menu = SettingsMenu(self)
|
|
||||||
self.game_state = "settings"
|
|
||||||
|
|
||||||
elif action == "quit":
|
|
||||||
self.running = False
|
|
||||||
|
|
||||||
def update_menu(self):
|
|
||||||
self.main_menu.update()
|
|
||||||
|
|
||||||
def draw_menu(self):
|
|
||||||
self.main_menu.draw(self.screen)
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# SETTINGS STATE
|
|
||||||
# ==========================================================
|
|
||||||
def handle_settings_events(self):
|
|
||||||
action = self.settings_menu.handle_events()
|
|
||||||
|
|
||||||
if action == "back":
|
|
||||||
self.game_state = "menu"
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# NEW GAME DIALOG STATE
|
|
||||||
# ==========================================================
|
|
||||||
def handle_new_game_dialog_events(self):
|
|
||||||
action = self.new_game_dialog.handle_events()
|
|
||||||
|
|
||||||
if action and action.startswith("create:"):
|
|
||||||
world_name = action.replace("create:", "")
|
|
||||||
self.world_name = world_name
|
|
||||||
self.init_game()
|
|
||||||
self.game_state = "playing"
|
|
||||||
print(f"New game '{world_name}' started")
|
|
||||||
|
|
||||||
elif action == "cancel":
|
|
||||||
self.game_state = "menu"
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# LOAD GAME DIALOG STATE
|
|
||||||
# ==========================================================
|
|
||||||
def handle_load_game_dialog_events(self):
|
|
||||||
action = self.load_game_dialog.handle_events()
|
|
||||||
|
|
||||||
if action and action.startswith("load:"):
|
|
||||||
save_index = int(action.replace("load:", ""))
|
|
||||||
saves = self.save_system.get_save_files()
|
|
||||||
|
|
||||||
if 0 <= save_index < len(saves):
|
|
||||||
save_file = saves[save_index]["name"]
|
|
||||||
self.init_game()
|
|
||||||
|
|
||||||
if self.save_system.load_game(self, save_file):
|
|
||||||
self.game_state = "playing"
|
|
||||||
print(f"Game loaded: {save_file}")
|
|
||||||
else:
|
|
||||||
print("Failed to load game")
|
|
||||||
self.game_state = "load_game_dialog"
|
|
||||||
|
|
||||||
elif action and action.startswith("delete:"):
|
|
||||||
save_index = int(action.replace("delete:", ""))
|
|
||||||
saves = self.save_system.get_save_files()
|
|
||||||
|
|
||||||
if 0 <= save_index < len(saves):
|
|
||||||
save_file = saves[save_index]["name"]
|
|
||||||
self.save_system.delete_save(save_file)
|
|
||||||
self.load_game_dialog = LoadGameDialog(self, self.save_system)
|
|
||||||
|
|
||||||
elif action == "cancel":
|
|
||||||
self.game_state = "menu"
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# PAUSE STATE
|
|
||||||
# ==========================================================
|
|
||||||
def handle_pause_events(self):
|
|
||||||
action = self.pause_menu.handle_events()
|
|
||||||
|
|
||||||
if action == "resume":
|
|
||||||
self.paused = False
|
|
||||||
self.game_state = "playing"
|
|
||||||
|
|
||||||
elif action == "save":
|
|
||||||
self.save_system.save_game(self, self.world_name)
|
|
||||||
self.pause_menu.show_message("Game saved!")
|
|
||||||
|
|
||||||
elif action == "backup":
|
|
||||||
self.save_system.backup_game(self)
|
|
||||||
self.pause_menu.show_message("Backup created!")
|
|
||||||
|
|
||||||
elif action == "quit_to_menu":
|
|
||||||
self.paused = False
|
|
||||||
self.game_state = "menu"
|
|
||||||
|
|
||||||
def draw_paused(self):
|
|
||||||
self.screen.fill(BACKGROUND_COLOR)
|
|
||||||
self.world.draw(self.screen, self.camera)
|
|
||||||
self.enemy_manager.draw(self.screen, self.camera)
|
|
||||||
self.player.draw(self.screen, self.camera)
|
|
||||||
self.pause_menu.draw(self.screen)
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# DEATH STATE
|
|
||||||
# ==========================================================
|
|
||||||
def handle_death_events(self):
|
|
||||||
action = self.death_screen.handle_events()
|
|
||||||
|
|
||||||
if action == "respawn":
|
|
||||||
# Reset player health and position
|
|
||||||
self.player.health = self.player.max_health
|
|
||||||
self.player.pos = pygame.Vector2(100, 100)
|
|
||||||
self.player.rect.x = 100
|
|
||||||
self.player.rect.y = 100
|
|
||||||
self.player.velocity = pygame.Vector2(0, 0)
|
|
||||||
self.game_state = "playing"
|
|
||||||
|
|
||||||
elif action == "quit_to_menu":
|
|
||||||
self.game_state = "menu"
|
|
||||||
|
|
||||||
def draw_dead(self):
|
|
||||||
self.screen.fill(BACKGROUND_COLOR)
|
|
||||||
self.world.draw(self.screen, self.camera)
|
|
||||||
self.enemy_manager.draw(self.screen, self.camera)
|
|
||||||
self.player.draw(self.screen, self.camera)
|
|
||||||
self.death_screen.draw(self.screen)
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# GAMEPLAY STATE
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
def handle_events(self):
|
def handle_events(self):
|
||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
|
|
@ -298,85 +65,44 @@ class Game:
|
||||||
|
|
||||||
if event.type == pygame.KEYDOWN:
|
if event.type == pygame.KEYDOWN:
|
||||||
if event.key == pygame.K_ESCAPE:
|
if event.key == pygame.K_ESCAPE:
|
||||||
self.paused = True
|
self.paused = not self.paused
|
||||||
self.pause_menu = PauseMenu(self)
|
|
||||||
self.game_state = "paused"
|
|
||||||
|
|
||||||
# Toggle inventory
|
# Toggle inventory
|
||||||
if event.key == pygame.K_i:
|
if event.key == pygame.K_i:
|
||||||
self.show_inventory = not self.show_inventory
|
self.show_inventory = not self.show_inventory
|
||||||
|
|
||||||
# Hotbar input
|
# -------------------------------
|
||||||
if self.hotbar:
|
# Mouse input for breaking/placing
|
||||||
self.hotbar.handle_input(event)
|
# -------------------------------
|
||||||
|
|
||||||
# Mouse input for attacking and placing
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
if self.show_inventory:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if event.button == 1: # Left click
|
if event.button == 1: # Left click
|
||||||
# Get current held item
|
self.world.break_block(
|
||||||
items_list = list(self.inventory.slots.items())
|
pygame.mouse.get_pos(),
|
||||||
current_item = None
|
self.camera,
|
||||||
if self.hotbar.selected_slot < len(items_list):
|
self.inventory
|
||||||
current_item_id, _ = items_list[self.hotbar.selected_slot]
|
)
|
||||||
current_item = get_item(current_item_id)
|
|
||||||
|
|
||||||
# Check if holding a weapon
|
if event.button == 3: # Right click
|
||||||
is_holding_weapon = (current_item is not None and
|
self.world.place_block(
|
||||||
current_item.id in WEAPON_STATS)
|
pygame.mouse.get_pos(),
|
||||||
|
self.camera
|
||||||
if is_holding_weapon:
|
)
|
||||||
# Attack with sword
|
|
||||||
print(f"DEBUG: Attempting sword attack with {current_item.name}")
|
|
||||||
self.combat_system.player_attack(self.player, self.enemy_manager.enemies)
|
|
||||||
else:
|
|
||||||
# Mine block if not holding weapon
|
|
||||||
print("DEBUG: Mining block")
|
|
||||||
self.world.break_block(
|
|
||||||
pygame.mouse.get_pos(),
|
|
||||||
self.camera,
|
|
||||||
self.inventory,
|
|
||||||
self.player.rect
|
|
||||||
)
|
|
||||||
|
|
||||||
if event.button == 3: # Right click - place block
|
|
||||||
# Get current held item
|
|
||||||
items_list = list(self.inventory.slots.items())
|
|
||||||
if self.hotbar.selected_slot < len(items_list):
|
|
||||||
current_item_id, _ = items_list[self.hotbar.selected_slot]
|
|
||||||
current_item = get_item(current_item_id)
|
|
||||||
|
|
||||||
# Only place if it's placeable
|
|
||||||
if current_item and current_item.placeable:
|
|
||||||
print(f"DEBUG: Placing {current_item.name}")
|
|
||||||
self.world.place_block(
|
|
||||||
pygame.mouse.get_pos(),
|
|
||||||
self.camera,
|
|
||||||
self.player.rect,
|
|
||||||
current_item.place_tile
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# UPDATE
|
||||||
|
# ==========================================================
|
||||||
def update(self, dt):
|
def update(self, dt):
|
||||||
|
# If inventory is open, pause gameplay
|
||||||
if self.show_inventory:
|
if self.show_inventory:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.player.update(dt, self.world)
|
self.player.update(dt, self.world)
|
||||||
self.camera.update()
|
self.camera.update()
|
||||||
self.enemy_manager.update(dt)
|
self.enemy_manager.update(dt)
|
||||||
self.combat_system.update(dt)
|
|
||||||
|
|
||||||
# Check if player is dead
|
|
||||||
if self.player.health <= 0:
|
|
||||||
self.death_screen = DeathScreen(self)
|
|
||||||
self.game_state = "dead"
|
|
||||||
return
|
|
||||||
|
|
||||||
# Enemy AI - attack player
|
|
||||||
for enemy in self.enemy_manager.enemies:
|
|
||||||
self.combat_system.enemy_attack(enemy, self.player)
|
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# DRAW
|
||||||
|
# ==========================================================
|
||||||
def draw(self):
|
def draw(self):
|
||||||
self.screen.fill(BACKGROUND_COLOR)
|
self.screen.fill(BACKGROUND_COLOR)
|
||||||
|
|
||||||
|
|
@ -391,10 +117,6 @@ class Game:
|
||||||
if self.show_inventory:
|
if self.show_inventory:
|
||||||
self.inventory.draw(self.screen)
|
self.inventory.draw(self.screen)
|
||||||
|
|
||||||
# Draw hotbar
|
|
||||||
if self.hotbar and not self.show_inventory:
|
|
||||||
self.hotbar.draw(self.screen)
|
|
||||||
|
|
||||||
self.draw_ui()
|
self.draw_ui()
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
|
|
||||||
|
|
@ -415,52 +137,6 @@ class Game:
|
||||||
)
|
)
|
||||||
self.screen.blit(debug_text, (10, 30))
|
self.screen.blit(debug_text, (10, 30))
|
||||||
|
|
||||||
world_name_text = self.font.render(
|
|
||||||
f"World: {self.world_name}",
|
|
||||||
True,
|
|
||||||
(255, 255, 255)
|
|
||||||
)
|
|
||||||
self.screen.blit(world_name_text, (10, 50))
|
|
||||||
|
|
||||||
weapon = self.weapon_manager.get_current_weapon()
|
|
||||||
if weapon:
|
|
||||||
weapon_text = self.font.render(
|
|
||||||
f"Weapon: {weapon.name}",
|
|
||||||
True,
|
|
||||||
(255, 255, 255)
|
|
||||||
)
|
|
||||||
self.screen.blit(weapon_text, (10, 70))
|
|
||||||
|
|
||||||
# Health display
|
|
||||||
health_text = self.font.render(
|
|
||||||
f"Health: {int(self.player.health)}/{int(self.player.max_health)}",
|
|
||||||
True,
|
|
||||||
(255, 50, 50)
|
|
||||||
)
|
|
||||||
self.screen.blit(health_text, (10, 90))
|
|
||||||
|
|
||||||
# Enemy count
|
|
||||||
enemy_text = self.font.render(
|
|
||||||
f"Enemies: {len(self.enemy_manager.enemies)}",
|
|
||||||
True,
|
|
||||||
(255, 100, 100)
|
|
||||||
)
|
|
||||||
self.screen.blit(enemy_text, (10, 110))
|
|
||||||
|
|
||||||
# Draw currently holding item (top right)
|
|
||||||
items_list = list(self.inventory.slots.items())
|
|
||||||
if self.hotbar and items_list:
|
|
||||||
current_item_id, current_amount = items_list[self.hotbar.selected_slot] if self.hotbar.selected_slot < len(
|
|
||||||
items_list) else (None, 0)
|
|
||||||
if current_item_id:
|
|
||||||
item = get_item(current_item_id)
|
|
||||||
if item:
|
|
||||||
holding_text = self.font.render(f"Holding: {item.name}", True, (255, 255, 255))
|
|
||||||
self.screen.blit(holding_text, (SCREEN_WIDTH - 250, 10))
|
|
||||||
(255, 255, 255)
|
|
||||||
)
|
|
||||||
self.screen.blit(debug_text, (10, 30))
|
|
||||||
|
|
||||||
if self.paused:
|
if self.paused:
|
||||||
pause_text = self.font.render("PAUSED", True, (255, 0, 0))
|
pause_text = self.font.render("PAUSED", True, (255, 0, 0))
|
||||||
rect = pause_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2))
|
rect = pause_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2))
|
||||||
|
|
|
||||||
127
hotbar.py
127
hotbar.py
|
|
@ -1,127 +0,0 @@
|
||||||
import pygame
|
|
||||||
from settings import *
|
|
||||||
from items import get_item
|
|
||||||
|
|
||||||
|
|
||||||
class Hotbar:
|
|
||||||
"""Hotbar UI for quick access to items and weapons"""
|
|
||||||
|
|
||||||
def __init__(self, inventory, weapon_manager):
|
|
||||||
self.inventory = inventory
|
|
||||||
self.weapon_manager = weapon_manager
|
|
||||||
self.selected_slot = 0
|
|
||||||
self.visible_slots = HOTBAR_SLOT_COUNT
|
|
||||||
|
|
||||||
# UI positioning
|
|
||||||
self.slot_size = 50
|
|
||||||
self.slot_padding = 8
|
|
||||||
self.background_color = (40, 40, 40)
|
|
||||||
self.selected_color = (100, 150, 255)
|
|
||||||
self.border_color = (150, 150, 150)
|
|
||||||
|
|
||||||
# Center hotbar at bottom of screen
|
|
||||||
total_width = (self.slot_size * self.visible_slots) + (self.slot_padding * (self.visible_slots - 1)) + 20
|
|
||||||
self.x = (SCREEN_WIDTH - total_width) // 2
|
|
||||||
self.y = SCREEN_HEIGHT - 80
|
|
||||||
|
|
||||||
self.font = pygame.font.SysFont("consolas", 14)
|
|
||||||
|
|
||||||
def handle_input(self, event):
|
|
||||||
"""Handle hotbar input (number keys 1-8 and mouse clicks)"""
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
# Number keys 1-8
|
|
||||||
if pygame.K_1 <= event.key <= pygame.K_8:
|
|
||||||
slot_index = event.key - pygame.K_1
|
|
||||||
if slot_index < self.visible_slots:
|
|
||||||
self.selected_slot = slot_index
|
|
||||||
self.switch_weapon_at_slot()
|
|
||||||
return True
|
|
||||||
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
||||||
if event.button == 4: # Scroll up
|
|
||||||
self.selected_slot = (self.selected_slot - 1) % self.visible_slots
|
|
||||||
self.switch_weapon_at_slot()
|
|
||||||
return True
|
|
||||||
elif event.button == 5: # Scroll down
|
|
||||||
self.selected_slot = (self.selected_slot + 1) % self.visible_slots
|
|
||||||
self.switch_weapon_at_slot()
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Click on hotbar slots
|
|
||||||
if event.button == 1:
|
|
||||||
self.handle_click(event.pos)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def handle_click(self, mouse_pos):
|
|
||||||
"""Handle clicking on hotbar slots"""
|
|
||||||
for i in range(self.visible_slots):
|
|
||||||
slot_x = self.x + i * (self.slot_size + self.slot_padding)
|
|
||||||
slot_y = self.y
|
|
||||||
slot_rect = pygame.Rect(slot_x, slot_y, self.slot_size, self.slot_size)
|
|
||||||
|
|
||||||
if slot_rect.collidepoint(mouse_pos):
|
|
||||||
self.selected_slot = i
|
|
||||||
self.switch_weapon_at_slot()
|
|
||||||
break
|
|
||||||
|
|
||||||
def switch_weapon_at_slot(self):
|
|
||||||
"""Switch to weapon in selected slot"""
|
|
||||||
items_list = list(self.inventory.slots.items())
|
|
||||||
if self.selected_slot < len(items_list):
|
|
||||||
item_id, _ = items_list[self.selected_slot]
|
|
||||||
# Check if it's a weapon
|
|
||||||
if item_id in WEAPON_STATS:
|
|
||||||
self.weapon_manager.switch_weapon(item_id)
|
|
||||||
|
|
||||||
def get_selected_item(self):
|
|
||||||
"""Get the item in the selected hotbar slot"""
|
|
||||||
items_list = list(self.inventory.slots.items())
|
|
||||||
if self.selected_slot < len(items_list):
|
|
||||||
return items_list[self.selected_slot]
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def draw(self, screen):
|
|
||||||
"""Draw hotbar on screen"""
|
|
||||||
# Get inventory items
|
|
||||||
items_list = list(self.inventory.slots.items())
|
|
||||||
|
|
||||||
for i in range(self.visible_slots):
|
|
||||||
# Calculate slot position
|
|
||||||
slot_x = self.x + i * (self.slot_size + self.slot_padding)
|
|
||||||
slot_y = self.y
|
|
||||||
slot_rect = pygame.Rect(slot_x, slot_y, self.slot_size, self.slot_size)
|
|
||||||
|
|
||||||
# Draw background
|
|
||||||
if i == self.selected_slot:
|
|
||||||
pygame.draw.rect(screen, self.selected_color, slot_rect)
|
|
||||||
else:
|
|
||||||
pygame.draw.rect(screen, self.background_color, slot_rect)
|
|
||||||
|
|
||||||
# Draw border
|
|
||||||
pygame.draw.rect(screen, self.border_color, slot_rect, 2)
|
|
||||||
|
|
||||||
# Draw item if exists
|
|
||||||
if i < len(items_list):
|
|
||||||
item_id, amount = items_list[i]
|
|
||||||
item = get_item(item_id)
|
|
||||||
|
|
||||||
if item:
|
|
||||||
# Draw item icon (colored square)
|
|
||||||
icon_rect = pygame.Rect(
|
|
||||||
slot_x + 6,
|
|
||||||
slot_y + 6,
|
|
||||||
self.slot_size - 12,
|
|
||||||
self.slot_size - 12
|
|
||||||
)
|
|
||||||
pygame.draw.rect(screen, item.color, icon_rect)
|
|
||||||
|
|
||||||
# Draw amount text
|
|
||||||
if amount > 1:
|
|
||||||
amount_text = self.font.render(str(amount), True, (255, 255, 255))
|
|
||||||
screen.blit(amount_text, (slot_x + 4, slot_y + self.slot_size - 20))
|
|
||||||
|
|
||||||
# Draw slot number
|
|
||||||
num_text = self.font.render(str(i + 1), True, (200, 200, 200))
|
|
||||||
screen.blit(num_text, (slot_x + 4, slot_y + 4))
|
|
||||||
21
inventory.py
21
inventory.py
|
|
@ -1,6 +1,6 @@
|
||||||
import pygame
|
import pygame
|
||||||
from settings import *
|
from settings import *
|
||||||
from items import get_item
|
from items import ITEMS, get_item
|
||||||
|
|
||||||
|
|
||||||
class Inventory:
|
class Inventory:
|
||||||
|
|
@ -25,13 +25,6 @@ class Inventory:
|
||||||
else:
|
else:
|
||||||
self.slots[item_id] = amount
|
self.slots[item_id] = amount
|
||||||
|
|
||||||
def remove_item(self, item_id, amount=1):
|
|
||||||
"""Remove item from inventory"""
|
|
||||||
if item_id in self.slots:
|
|
||||||
self.slots[item_id] -= amount
|
|
||||||
if self.slots[item_id] <= 0:
|
|
||||||
del self.slots[item_id]
|
|
||||||
|
|
||||||
def draw(self, screen):
|
def draw(self, screen):
|
||||||
font = pygame.font.SysFont("consolas", 18)
|
font = pygame.font.SysFont("consolas", 18)
|
||||||
title_font = pygame.font.SysFont("consolas", 24)
|
title_font = pygame.font.SysFont("consolas", 24)
|
||||||
|
|
@ -53,13 +46,14 @@ class Inventory:
|
||||||
start_x = self.x + self.padding
|
start_x = self.x + self.padding
|
||||||
start_y = self.y + 70
|
start_y = self.y + 70
|
||||||
|
|
||||||
|
# Convert slots to list for display
|
||||||
items_list = list(self.slots.items())
|
items_list = list(self.slots.items())
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
mouse = pygame.mouse.get_pos()
|
mouse = pygame.mouse.get_pos()
|
||||||
|
|
||||||
for row in range(self.grid_rows):
|
for row in range(self.grid_rows):
|
||||||
for col in range(self.grid_cols):
|
for col in range(self.grid_cols):
|
||||||
|
|
||||||
cell_rect = pygame.Rect(
|
cell_rect = pygame.Rect(
|
||||||
start_x + col * (self.cell_size + self.cell_padding),
|
start_x + col * (self.cell_size + self.cell_padding),
|
||||||
start_y + row * (self.cell_size + self.cell_padding),
|
start_y + row * (self.cell_size + self.cell_padding),
|
||||||
|
|
@ -84,12 +78,11 @@ class Inventory:
|
||||||
)
|
)
|
||||||
pygame.draw.rect(screen, item.color, icon_rect)
|
pygame.draw.rect(screen, item.color, icon_rect)
|
||||||
|
|
||||||
# Draw amount if stack > 1
|
# Draw amount
|
||||||
if amount > 1:
|
amount_text = font.render(str(amount), True, (255, 255, 255))
|
||||||
amount_text = font.render(str(amount), True, (255, 255, 255))
|
screen.blit(amount_text, (cell_rect.x + 2, cell_rect.y + 2))
|
||||||
screen.blit(amount_text, (cell_rect.x + 2, cell_rect.y + 2))
|
|
||||||
|
|
||||||
# Tooltip when hovering
|
# Tooltip on hover
|
||||||
if cell_rect.collidepoint(mouse):
|
if cell_rect.collidepoint(mouse):
|
||||||
tooltip = font.render(item.name, True, (255, 255, 0))
|
tooltip = font.render(item.name, True, (255, 255, 0))
|
||||||
screen.blit(tooltip, (mouse[0] + 10, mouse[1] + 10))
|
screen.blit(tooltip, (mouse[0] + 10, mouse[1] + 10))
|
||||||
|
|
|
||||||
29
items.py
29
items.py
|
|
@ -1,5 +1,6 @@
|
||||||
from settings import *
|
from settings import *
|
||||||
|
|
||||||
|
|
||||||
class Item:
|
class Item:
|
||||||
def __init__(self, item_id, name, color, stack_limit=999, placeable=False, place_tile=None):
|
def __init__(self, item_id, name, color, stack_limit=999, placeable=False, place_tile=None):
|
||||||
self.id = item_id
|
self.id = item_id
|
||||||
|
|
@ -60,34 +61,8 @@ ITEMS = {
|
||||||
stack_limit=999,
|
stack_limit=999,
|
||||||
placeable=False
|
placeable=False
|
||||||
),
|
),
|
||||||
|
|
||||||
# Weapons
|
|
||||||
ITEM_WOODEN_SWORD: Item(
|
|
||||||
ITEM_WOODEN_SWORD,
|
|
||||||
"Wooden Sword",
|
|
||||||
(160, 82, 45),
|
|
||||||
stack_limit=1,
|
|
||||||
placeable=False
|
|
||||||
),
|
|
||||||
|
|
||||||
ITEM_IRON_SWORD: Item(
|
|
||||||
ITEM_IRON_SWORD,
|
|
||||||
"Iron Sword",
|
|
||||||
(180, 180, 180),
|
|
||||||
stack_limit=1,
|
|
||||||
placeable=False
|
|
||||||
),
|
|
||||||
|
|
||||||
ITEM_GOLD_SWORD: Item(
|
|
||||||
ITEM_GOLD_SWORD,
|
|
||||||
"Gold Sword",
|
|
||||||
(255, 215, 0),
|
|
||||||
stack_limit=1,
|
|
||||||
placeable=False
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_item(item_id):
|
def get_item(item_id):
|
||||||
"""Return the Item object for a given item_id, or None if it doesn't exist."""
|
return ITEMS.get(item_id, None)
|
||||||
return ITEMS.get(item_id)
|
|
||||||
|
|
|
||||||
675
menu_system.py
675
menu_system.py
|
|
@ -1,675 +0,0 @@
|
||||||
import pygame
|
|
||||||
from settings import *
|
|
||||||
|
|
||||||
|
|
||||||
class Button:
|
|
||||||
"""Reusable button class for UI"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height, text, color, text_color, hover_color):
|
|
||||||
self.rect = pygame.Rect(x, y, width, height)
|
|
||||||
self.text = text
|
|
||||||
self.color = color
|
|
||||||
self.hover_color = hover_color
|
|
||||||
self.text_color = text_color
|
|
||||||
self.is_hovered = False
|
|
||||||
|
|
||||||
def draw(self, screen, font):
|
|
||||||
current_color = self.hover_color if self.is_hovered else self.color
|
|
||||||
pygame.draw.rect(screen, current_color, self.rect)
|
|
||||||
pygame.draw.rect(screen, (255, 255, 255), self.rect, 2)
|
|
||||||
|
|
||||||
text_surface = font.render(self.text, True, self.text_color)
|
|
||||||
text_rect = text_surface.get_rect(center=self.rect.center)
|
|
||||||
screen.blit(text_surface, text_rect)
|
|
||||||
|
|
||||||
def check_hover(self, mouse_pos):
|
|
||||||
self.is_hovered = self.rect.collidepoint(mouse_pos)
|
|
||||||
|
|
||||||
def is_clicked(self, mouse_pos):
|
|
||||||
return self.rect.collidepoint(mouse_pos)
|
|
||||||
|
|
||||||
|
|
||||||
class InputBox:
|
|
||||||
"""Text input field for naming saves"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height, text=''):
|
|
||||||
self.rect = pygame.Rect(x, y, width, height)
|
|
||||||
self.text = text
|
|
||||||
self.active = False
|
|
||||||
self.cursor_visible = True
|
|
||||||
self.cursor_timer = 0
|
|
||||||
|
|
||||||
def handle_event(self, event):
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
||||||
if self.rect.collidepoint(event.pos):
|
|
||||||
self.active = True
|
|
||||||
else:
|
|
||||||
self.active = False
|
|
||||||
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if self.active:
|
|
||||||
if event.key == pygame.K_BACKSPACE:
|
|
||||||
self.text = self.text[:-1]
|
|
||||||
elif event.key == pygame.K_RETURN:
|
|
||||||
return "submit"
|
|
||||||
elif len(self.text) < 30:
|
|
||||||
if event.unicode.isprintable():
|
|
||||||
self.text += event.unicode
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update(self, dt):
|
|
||||||
self.cursor_timer += dt
|
|
||||||
if self.cursor_timer > 0.5:
|
|
||||||
self.cursor_visible = not self.cursor_visible
|
|
||||||
self.cursor_timer = 0
|
|
||||||
|
|
||||||
def draw(self, screen, font):
|
|
||||||
pygame.draw.rect(screen, (50, 50, 50), self.rect)
|
|
||||||
color = (100, 200, 100) if self.active else (100, 100, 100)
|
|
||||||
pygame.draw.rect(screen, color, self.rect, 2)
|
|
||||||
|
|
||||||
text_surface = font.render(self.text, True, (255, 255, 255))
|
|
||||||
screen.blit(text_surface, (self.rect.x + 10, self.rect.y + 8))
|
|
||||||
|
|
||||||
if self.active and self.cursor_visible:
|
|
||||||
cursor_x = self.rect.x + 10 + text_surface.get_width()
|
|
||||||
pygame.draw.line(screen, (255, 255, 255),
|
|
||||||
(cursor_x, self.rect.y + 5),
|
|
||||||
(cursor_x, self.rect.y + self.rect.height - 5))
|
|
||||||
|
|
||||||
|
|
||||||
class MainMenu:
|
|
||||||
"""Main menu screen"""
|
|
||||||
|
|
||||||
def __init__(self, game):
|
|
||||||
self.game = game
|
|
||||||
self.button_width = 300
|
|
||||||
self.button_height = 60
|
|
||||||
self.button_x = (SCREEN_WIDTH - self.button_width) // 2
|
|
||||||
|
|
||||||
self.new_game_btn = Button(
|
|
||||||
self.button_x,
|
|
||||||
SCREEN_HEIGHT // 2 - 100,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"New Game",
|
|
||||||
(50, 100, 150),
|
|
||||||
(255, 255, 255),
|
|
||||||
(100, 150, 200)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.load_game_btn = Button(
|
|
||||||
self.button_x,
|
|
||||||
SCREEN_HEIGHT // 2,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Load Game",
|
|
||||||
(50, 100, 150),
|
|
||||||
(255, 255, 255),
|
|
||||||
(100, 150, 200)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.settings_btn = Button(
|
|
||||||
self.button_x,
|
|
||||||
SCREEN_HEIGHT // 2 + 70,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Settings",
|
|
||||||
(100, 100, 100),
|
|
||||||
(255, 255, 255),
|
|
||||||
(150, 150, 150)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.quit_btn = Button(
|
|
||||||
self.button_x,
|
|
||||||
SCREEN_HEIGHT // 2 + 140,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Quit",
|
|
||||||
(150, 50, 50),
|
|
||||||
(255, 255, 255),
|
|
||||||
(200, 100, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.font_title = pygame.font.SysFont("consolas", 72, bold=True)
|
|
||||||
self.font_button = pygame.font.SysFont("consolas", 24)
|
|
||||||
|
|
||||||
def handle_events(self):
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.QUIT:
|
|
||||||
return "quit"
|
|
||||||
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
||||||
if event.button == 1:
|
|
||||||
if self.new_game_btn.is_clicked(event.pos):
|
|
||||||
return "new_game"
|
|
||||||
elif self.load_game_btn.is_clicked(event.pos):
|
|
||||||
return "load_game"
|
|
||||||
elif self.settings_btn.is_clicked(event.pos):
|
|
||||||
return "settings"
|
|
||||||
elif self.quit_btn.is_clicked(event.pos):
|
|
||||||
return "quit"
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
mouse_pos = pygame.mouse.get_pos()
|
|
||||||
self.new_game_btn.check_hover(mouse_pos)
|
|
||||||
self.load_game_btn.check_hover(mouse_pos)
|
|
||||||
self.settings_btn.check_hover(mouse_pos)
|
|
||||||
self.quit_btn.check_hover(mouse_pos)
|
|
||||||
|
|
||||||
def draw(self, screen):
|
|
||||||
screen.fill(BACKGROUND_COLOR)
|
|
||||||
|
|
||||||
title = self.font_title.render("DERRARIA", True, (255, 255, 255))
|
|
||||||
title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 100))
|
|
||||||
screen.blit(title, title_rect)
|
|
||||||
|
|
||||||
self.new_game_btn.draw(screen, self.font_button)
|
|
||||||
self.load_game_btn.draw(screen, self.font_button)
|
|
||||||
self.settings_btn.draw(screen, self.font_button)
|
|
||||||
self.quit_btn.draw(screen, self.font_button)
|
|
||||||
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsMenu:
|
|
||||||
"""Settings menu for fullscreen toggle"""
|
|
||||||
|
|
||||||
def __init__(self, game):
|
|
||||||
self.game = game
|
|
||||||
self.font_title = pygame.font.SysFont("consolas", 36, bold=True)
|
|
||||||
self.font_label = pygame.font.SysFont("consolas", 20)
|
|
||||||
self.font_button = pygame.font.SysFont("consolas", 18)
|
|
||||||
|
|
||||||
self.button_width = 200
|
|
||||||
self.button_height = 50
|
|
||||||
|
|
||||||
# Fullscreen toggle
|
|
||||||
self.fullscreen_btn = Button(
|
|
||||||
SCREEN_WIDTH // 2 - 100,
|
|
||||||
SCREEN_HEIGHT // 2 - 50,
|
|
||||||
200,
|
|
||||||
50,
|
|
||||||
f"Fullscreen: {'ON' if FULLSCREEN else 'OFF'}",
|
|
||||||
(100, 100, 100),
|
|
||||||
(255, 255, 255),
|
|
||||||
(150, 150, 150)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Back button
|
|
||||||
self.back_btn = Button(
|
|
||||||
SCREEN_WIDTH // 2 - 100,
|
|
||||||
SCREEN_HEIGHT // 2 + 50,
|
|
||||||
200,
|
|
||||||
50,
|
|
||||||
"Back",
|
|
||||||
(100, 100, 150),
|
|
||||||
(255, 255, 255),
|
|
||||||
(150, 150, 200)
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_events(self):
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.QUIT:
|
|
||||||
return "back"
|
|
||||||
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_ESCAPE:
|
|
||||||
return "back"
|
|
||||||
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
||||||
if event.button == 1:
|
|
||||||
# Check fullscreen button
|
|
||||||
if self.fullscreen_btn.is_clicked(event.pos):
|
|
||||||
self.game.toggle_fullscreen()
|
|
||||||
self.fullscreen_btn.text = f"Fullscreen: {'ON' if FULLSCREEN else 'OFF'}"
|
|
||||||
|
|
||||||
# Check back button
|
|
||||||
if self.back_btn.is_clicked(event.pos):
|
|
||||||
return "back"
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update(self, dt):
|
|
||||||
mouse_pos = pygame.mouse.get_pos()
|
|
||||||
self.back_btn.check_hover(mouse_pos)
|
|
||||||
self.fullscreen_btn.check_hover(mouse_pos)
|
|
||||||
|
|
||||||
def draw(self, screen):
|
|
||||||
screen.fill(BACKGROUND_COLOR)
|
|
||||||
|
|
||||||
title = self.font_title.render("Settings", True, (255, 255, 255))
|
|
||||||
title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 80))
|
|
||||||
screen.blit(title, title_rect)
|
|
||||||
|
|
||||||
# Draw fullscreen button
|
|
||||||
self.fullscreen_btn.draw(screen, self.font_button)
|
|
||||||
|
|
||||||
# Draw back button
|
|
||||||
self.back_btn.draw(screen, self.font_button)
|
|
||||||
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
|
|
||||||
class NewGameDialog:
|
|
||||||
"""Dialog for naming a new game"""
|
|
||||||
|
|
||||||
def __init__(self, game):
|
|
||||||
self.game = game
|
|
||||||
self.font_title = pygame.font.SysFont("consolas", 36, bold=True)
|
|
||||||
self.font_label = pygame.font.SysFont("consolas", 20)
|
|
||||||
|
|
||||||
self.name_input = InputBox(
|
|
||||||
SCREEN_WIDTH // 2 - 150,
|
|
||||||
SCREEN_HEIGHT // 2 - 20,
|
|
||||||
300,
|
|
||||||
50,
|
|
||||||
"My World"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.button_width = 120
|
|
||||||
self.button_height = 50
|
|
||||||
|
|
||||||
self.create_btn = Button(
|
|
||||||
SCREEN_WIDTH // 2 - 260,
|
|
||||||
SCREEN_HEIGHT // 2 + 80,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Create",
|
|
||||||
(50, 150, 50),
|
|
||||||
(255, 255, 255),
|
|
||||||
(100, 200, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cancel_btn = Button(
|
|
||||||
SCREEN_WIDTH // 2 + 140,
|
|
||||||
SCREEN_HEIGHT // 2 + 80,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Cancel",
|
|
||||||
(150, 50, 50),
|
|
||||||
(255, 255, 255),
|
|
||||||
(200, 100, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_events(self):
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.QUIT:
|
|
||||||
return "cancel"
|
|
||||||
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_ESCAPE:
|
|
||||||
return "cancel"
|
|
||||||
|
|
||||||
self.name_input.handle_event(event)
|
|
||||||
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
||||||
if event.button == 1:
|
|
||||||
if self.create_btn.is_clicked(event.pos):
|
|
||||||
if self.name_input.text.strip():
|
|
||||||
return f"create:{self.name_input.text}"
|
|
||||||
elif self.cancel_btn.is_clicked(event.pos):
|
|
||||||
return "cancel"
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update(self, dt):
|
|
||||||
self.name_input.update(dt)
|
|
||||||
mouse_pos = pygame.mouse.get_pos()
|
|
||||||
self.create_btn.check_hover(mouse_pos)
|
|
||||||
self.cancel_btn.check_hover(mouse_pos)
|
|
||||||
|
|
||||||
def draw(self, screen):
|
|
||||||
screen.fill(BACKGROUND_COLOR)
|
|
||||||
|
|
||||||
title = self.font_title.render("Create New World", True, (255, 255, 255))
|
|
||||||
title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 80))
|
|
||||||
screen.blit(title, title_rect)
|
|
||||||
|
|
||||||
label = self.font_label.render("World Name:", True, (255, 255, 255))
|
|
||||||
screen.blit(label, (SCREEN_WIDTH // 2 - 150, SCREEN_HEIGHT // 2 - 60))
|
|
||||||
|
|
||||||
self.name_input.draw(screen, self.font_label)
|
|
||||||
|
|
||||||
self.create_btn.draw(screen, self.font_label)
|
|
||||||
self.cancel_btn.draw(screen, self.font_label)
|
|
||||||
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
|
|
||||||
class LoadGameDialog:
|
|
||||||
"""Dialog for selecting a save to load"""
|
|
||||||
|
|
||||||
def __init__(self, game, save_system):
|
|
||||||
self.game = game
|
|
||||||
self.save_system = save_system
|
|
||||||
self.font_title = pygame.font.SysFont("consolas", 36, bold=True)
|
|
||||||
self.font_label = pygame.font.SysFont("consolas", 18)
|
|
||||||
self.font_small = pygame.font.SysFont("consolas", 14)
|
|
||||||
|
|
||||||
self.saves = self.save_system.get_save_files()
|
|
||||||
self.selected_index = 0
|
|
||||||
|
|
||||||
self.scroll_offset = 0
|
|
||||||
self.visible_saves = 5
|
|
||||||
|
|
||||||
self.button_width = 120
|
|
||||||
self.button_height = 50
|
|
||||||
|
|
||||||
self.load_btn = Button(
|
|
||||||
SCREEN_WIDTH // 2 - 260,
|
|
||||||
SCREEN_HEIGHT - 100,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Load",
|
|
||||||
(50, 150, 50),
|
|
||||||
(255, 255, 255),
|
|
||||||
(100, 200, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.delete_btn = Button(
|
|
||||||
SCREEN_WIDTH // 2 - 65,
|
|
||||||
SCREEN_HEIGHT - 100,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Delete",
|
|
||||||
(150, 50, 50),
|
|
||||||
(255, 255, 255),
|
|
||||||
(200, 100, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cancel_btn = Button(
|
|
||||||
SCREEN_WIDTH // 2 + 140,
|
|
||||||
SCREEN_HEIGHT - 100,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Cancel",
|
|
||||||
(100, 100, 150),
|
|
||||||
(255, 255, 255),
|
|
||||||
(150, 150, 200)
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_events(self):
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.QUIT:
|
|
||||||
return "cancel"
|
|
||||||
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_ESCAPE:
|
|
||||||
return "cancel"
|
|
||||||
elif event.key == pygame.K_UP:
|
|
||||||
self.selected_index = max(0, self.selected_index - 1)
|
|
||||||
elif event.key == pygame.K_DOWN:
|
|
||||||
self.selected_index = min(len(self.saves) - 1, self.selected_index + 1)
|
|
||||||
elif event.key == pygame.K_RETURN:
|
|
||||||
if self.saves:
|
|
||||||
return f"load:{self.selected_index}"
|
|
||||||
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
||||||
if event.button == 1:
|
|
||||||
if self.load_btn.is_clicked(event.pos):
|
|
||||||
if self.saves:
|
|
||||||
return f"load:{self.selected_index}"
|
|
||||||
elif self.delete_btn.is_clicked(event.pos):
|
|
||||||
if self.saves:
|
|
||||||
return f"delete:{self.selected_index}"
|
|
||||||
elif self.cancel_btn.is_clicked(event.pos):
|
|
||||||
return "cancel"
|
|
||||||
|
|
||||||
for i in range(self.visible_saves):
|
|
||||||
save_idx = self.scroll_offset + i
|
|
||||||
if save_idx < len(self.saves):
|
|
||||||
y = 150 + i * 80
|
|
||||||
if pygame.Rect(50, y, SCREEN_WIDTH - 100, 70).collidepoint(event.pos):
|
|
||||||
self.selected_index = save_idx
|
|
||||||
|
|
||||||
elif event.button == 4:
|
|
||||||
self.scroll_offset = max(0, self.scroll_offset - 1)
|
|
||||||
elif event.button == 5:
|
|
||||||
self.scroll_offset = min(max(0, len(self.saves) - self.visible_saves),
|
|
||||||
self.scroll_offset + 1)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
mouse_pos = pygame.mouse.get_pos()
|
|
||||||
self.load_btn.check_hover(mouse_pos)
|
|
||||||
self.delete_btn.check_hover(mouse_pos)
|
|
||||||
self.cancel_btn.check_hover(mouse_pos)
|
|
||||||
|
|
||||||
if self.selected_index < self.scroll_offset:
|
|
||||||
self.scroll_offset = self.selected_index
|
|
||||||
elif self.selected_index >= self.scroll_offset + self.visible_saves:
|
|
||||||
self.scroll_offset = self.selected_index - self.visible_saves + 1
|
|
||||||
|
|
||||||
def draw(self, screen):
|
|
||||||
screen.fill(BACKGROUND_COLOR)
|
|
||||||
|
|
||||||
title = self.font_title.render("Load Game", True, (255, 255, 255))
|
|
||||||
title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 50))
|
|
||||||
screen.blit(title, title_rect)
|
|
||||||
|
|
||||||
if not self.saves:
|
|
||||||
no_saves = self.font_label.render("No saves found!", True, (200, 100, 100))
|
|
||||||
screen.blit(no_saves, (SCREEN_WIDTH // 2 - no_saves.get_width() // 2, SCREEN_HEIGHT // 2))
|
|
||||||
else:
|
|
||||||
for i in range(self.visible_saves):
|
|
||||||
save_idx = self.scroll_offset + i
|
|
||||||
if save_idx < len(self.saves):
|
|
||||||
save = self.saves[save_idx]
|
|
||||||
y = 150 + i * 80
|
|
||||||
|
|
||||||
if save_idx == self.selected_index:
|
|
||||||
pygame.draw.rect(screen, (60, 100, 140), pygame.Rect(50, y, SCREEN_WIDTH - 100, 70))
|
|
||||||
else:
|
|
||||||
pygame.draw.rect(screen, (40, 40, 40), pygame.Rect(50, y, SCREEN_WIDTH - 100, 70))
|
|
||||||
|
|
||||||
pygame.draw.rect(screen, (150, 150, 150), pygame.Rect(50, y, SCREEN_WIDTH - 100, 70), 2)
|
|
||||||
|
|
||||||
filename = save["name"]
|
|
||||||
parts = filename.replace("save_", "").replace(".json", "").split("_")
|
|
||||||
world_name = parts[0] if parts else "Unknown"
|
|
||||||
|
|
||||||
name_text = self.font_label.render(f"World: {world_name}", True, (255, 255, 255))
|
|
||||||
screen.blit(name_text, (70, y + 10))
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
mod_time = datetime.fromtimestamp(save["modified"]).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
time_text = self.font_small.render(f"Saved: {mod_time}", True, (200, 200, 200))
|
|
||||||
screen.blit(time_text, (70, y + 40))
|
|
||||||
|
|
||||||
self.load_btn.draw(screen, self.font_label)
|
|
||||||
self.delete_btn.draw(screen, self.font_label)
|
|
||||||
self.cancel_btn.draw(screen, self.font_label)
|
|
||||||
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
|
|
||||||
class PauseMenu:
|
|
||||||
"""Pause menu overlay"""
|
|
||||||
|
|
||||||
def __init__(self, game):
|
|
||||||
self.game = game
|
|
||||||
self.button_width = 250
|
|
||||||
self.button_height = 50
|
|
||||||
self.button_x = (SCREEN_WIDTH - self.button_width) // 2
|
|
||||||
|
|
||||||
self.resume_btn = Button(
|
|
||||||
self.button_x,
|
|
||||||
SCREEN_HEIGHT // 2 - 100,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Resume",
|
|
||||||
(50, 150, 50),
|
|
||||||
(255, 255, 255),
|
|
||||||
(100, 200, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.save_btn = Button(
|
|
||||||
self.button_x,
|
|
||||||
SCREEN_HEIGHT // 2,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Save Game",
|
|
||||||
(50, 100, 150),
|
|
||||||
(255, 255, 255),
|
|
||||||
(100, 150, 200)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.backup_btn = Button(
|
|
||||||
self.button_x,
|
|
||||||
SCREEN_HEIGHT // 2 + 60,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Backup",
|
|
||||||
(150, 150, 50),
|
|
||||||
(255, 255, 255),
|
|
||||||
(200, 200, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.quit_btn = Button(
|
|
||||||
self.button_x,
|
|
||||||
SCREEN_HEIGHT // 2 + 120,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Quit to Menu",
|
|
||||||
(150, 50, 50),
|
|
||||||
(255, 255, 255),
|
|
||||||
(200, 100, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.font_title = pygame.font.SysFont("consolas", 48, bold=True)
|
|
||||||
self.font_button = pygame.font.SysFont("consolas", 20)
|
|
||||||
self.message = ""
|
|
||||||
self.message_timer = 0
|
|
||||||
|
|
||||||
def handle_events(self):
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.QUIT:
|
|
||||||
return "quit"
|
|
||||||
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_ESCAPE:
|
|
||||||
return "resume"
|
|
||||||
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
||||||
if event.button == 1:
|
|
||||||
if self.resume_btn.is_clicked(event.pos):
|
|
||||||
return "resume"
|
|
||||||
elif self.save_btn.is_clicked(event.pos):
|
|
||||||
self.show_message("Game saved!")
|
|
||||||
return "save"
|
|
||||||
elif self.backup_btn.is_clicked(event.pos):
|
|
||||||
self.show_message("Backup created!")
|
|
||||||
return "backup"
|
|
||||||
elif self.quit_btn.is_clicked(event.pos):
|
|
||||||
return "quit_to_menu"
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def show_message(self, text):
|
|
||||||
self.message = text
|
|
||||||
self.message_timer = 2.0
|
|
||||||
|
|
||||||
def update(self, dt):
|
|
||||||
mouse_pos = pygame.mouse.get_pos()
|
|
||||||
self.resume_btn.check_hover(mouse_pos)
|
|
||||||
self.save_btn.check_hover(mouse_pos)
|
|
||||||
self.backup_btn.check_hover(mouse_pos)
|
|
||||||
self.quit_btn.check_hover(mouse_pos)
|
|
||||||
|
|
||||||
if self.message_timer > 0:
|
|
||||||
self.message_timer -= dt
|
|
||||||
|
|
||||||
def draw(self, screen):
|
|
||||||
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
|
|
||||||
overlay.set_alpha(128)
|
|
||||||
overlay.fill((0, 0, 0))
|
|
||||||
screen.blit(overlay, (0, 0))
|
|
||||||
|
|
||||||
title = self.font_title.render("PAUSED", True, (255, 255, 255))
|
|
||||||
title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 80))
|
|
||||||
screen.blit(title, title_rect)
|
|
||||||
|
|
||||||
self.resume_btn.draw(screen, self.font_button)
|
|
||||||
self.save_btn.draw(screen, self.font_button)
|
|
||||||
self.backup_btn.draw(screen, self.font_button)
|
|
||||||
self.quit_btn.draw(screen, self.font_button)
|
|
||||||
|
|
||||||
if self.message_timer > 0:
|
|
||||||
message = self.font_button.render(self.message, True, (100, 255, 100))
|
|
||||||
message_rect = message.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 50))
|
|
||||||
screen.blit(message, message_rect)
|
|
||||||
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
|
|
||||||
class DeathScreen:
|
|
||||||
"""Death screen with respawn and quit options"""
|
|
||||||
|
|
||||||
def __init__(self, game):
|
|
||||||
self.game = game
|
|
||||||
self.button_width = 250
|
|
||||||
self.button_height = 60
|
|
||||||
self.button_x = (SCREEN_WIDTH - self.button_width) // 2
|
|
||||||
|
|
||||||
self.respawn_btn = Button(
|
|
||||||
self.button_x,
|
|
||||||
SCREEN_HEIGHT // 2 - 50,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Respawn",
|
|
||||||
(50, 150, 50),
|
|
||||||
(255, 255, 255),
|
|
||||||
(100, 200, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.quit_btn = Button(
|
|
||||||
self.button_x,
|
|
||||||
SCREEN_HEIGHT // 2 + 50,
|
|
||||||
self.button_width,
|
|
||||||
self.button_height,
|
|
||||||
"Quit to Menu",
|
|
||||||
(150, 50, 50),
|
|
||||||
(255, 255, 255),
|
|
||||||
(200, 100, 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.font_title = pygame.font.SysFont("consolas", 72, bold=True)
|
|
||||||
self.font_button = pygame.font.SysFont("consolas", 24)
|
|
||||||
|
|
||||||
def handle_events(self):
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.QUIT:
|
|
||||||
return "quit"
|
|
||||||
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
||||||
if event.button == 1:
|
|
||||||
if self.respawn_btn.is_clicked(event.pos):
|
|
||||||
return "respawn"
|
|
||||||
elif self.quit_btn.is_clicked(event.pos):
|
|
||||||
return "quit_to_menu"
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update(self, dt):
|
|
||||||
mouse_pos = pygame.mouse.get_pos()
|
|
||||||
self.respawn_btn.check_hover(mouse_pos)
|
|
||||||
self.quit_btn.check_hover(mouse_pos)
|
|
||||||
|
|
||||||
def draw(self, screen):
|
|
||||||
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
|
|
||||||
overlay.set_alpha(200)
|
|
||||||
overlay.fill((0, 0, 0))
|
|
||||||
screen.blit(overlay, (0, 0))
|
|
||||||
|
|
||||||
title = self.font_title.render("YOU DIED", True, (255, 0, 0))
|
|
||||||
title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 100))
|
|
||||||
screen.blit(title, title_rect)
|
|
||||||
|
|
||||||
self.respawn_btn.draw(screen, self.font_button)
|
|
||||||
self.quit_btn.draw(screen, self.font_button)
|
|
||||||
|
|
||||||
pygame.display.flip()
|
|
||||||
280
player.py
280
player.py
|
|
@ -3,269 +3,109 @@ from settings import *
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
class Player:
|
||||||
def __init__(self, x, y, weapon_manager=None):
|
def __init__(self, x, y):
|
||||||
# =========================
|
# Position & Size
|
||||||
# POSITION
|
|
||||||
# =========================
|
|
||||||
self.pos = pygame.Vector2(x, y)
|
|
||||||
self.rect = pygame.Rect(x, y, PLAYER_WIDTH, PLAYER_HEIGHT)
|
self.rect = pygame.Rect(x, y, PLAYER_WIDTH, PLAYER_HEIGHT)
|
||||||
|
|
||||||
# =========================
|
# Movement
|
||||||
# MOVEMENT
|
|
||||||
# =========================
|
|
||||||
self.velocity = pygame.Vector2(0, 0)
|
self.velocity = pygame.Vector2(0, 0)
|
||||||
|
self.acceleration = pygame.Vector2(0, 0)
|
||||||
|
|
||||||
|
# State
|
||||||
self.on_ground = False
|
self.on_ground = False
|
||||||
self.facing_right = True
|
self.facing_right = True
|
||||||
|
|
||||||
# Movement tuning
|
# Stats
|
||||||
self.move_accel = 2800
|
|
||||||
self.air_accel = 2200
|
|
||||||
self.max_run_speed = 320
|
|
||||||
self.ground_friction = -12
|
|
||||||
self.air_control = 0.65
|
|
||||||
|
|
||||||
self.gravity = 2200
|
|
||||||
self.max_fall_speed = 1000
|
|
||||||
self.fast_fall_speed = 1400
|
|
||||||
|
|
||||||
self.jump_force = -700
|
|
||||||
self.jump_cut_multiplier = 0.5
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# JUMP TECH
|
|
||||||
# =========================
|
|
||||||
self.coyote_time_max = 0.1
|
|
||||||
self.jump_buffer_max = 0.12
|
|
||||||
|
|
||||||
self.coyote_timer = 0
|
|
||||||
self.jump_buffer_timer = 0
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# COMBAT
|
|
||||||
# =========================
|
|
||||||
self.health = MAX_HEALTH
|
self.health = MAX_HEALTH
|
||||||
self.max_health = MAX_HEALTH
|
|
||||||
|
|
||||||
self.knockback_timer = 0
|
|
||||||
self.invulnerable_timer = 0
|
|
||||||
|
|
||||||
self.weapon_manager = weapon_manager
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# STATE
|
|
||||||
# =========================
|
|
||||||
self.state = "idle"
|
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# UPDATE
|
# UPDATE
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
def update(self, dt, world):
|
def update(self, dt, world):
|
||||||
dt = min(dt, 0.05)
|
self.handle_input()
|
||||||
|
self.apply_physics(dt)
|
||||||
self.handle_timers(dt)
|
self.handle_collisions(world)
|
||||||
self.handle_input(dt)
|
|
||||||
self.apply_gravity(dt)
|
|
||||||
|
|
||||||
# ---- Horizontal ----
|
|
||||||
self.pos.x += self.velocity.x * dt
|
|
||||||
self.rect.x = round(self.pos.x)
|
|
||||||
self.handle_horizontal_collisions(world)
|
|
||||||
|
|
||||||
# World border check
|
|
||||||
if self.rect.left < 0:
|
|
||||||
self.rect.left = 0
|
|
||||||
self.pos.x = 0
|
|
||||||
self.velocity.x = 0
|
|
||||||
if self.rect.right > world.width * TILE_SIZE:
|
|
||||||
self.rect.right = world.width * TILE_SIZE
|
|
||||||
self.pos.x = self.rect.x
|
|
||||||
self.velocity.x = 0
|
|
||||||
|
|
||||||
# ---- Vertical ----
|
|
||||||
self.pos.y += self.velocity.y * dt
|
|
||||||
self.rect.y = round(self.pos.y)
|
|
||||||
self.handle_vertical_collisions(world)
|
|
||||||
|
|
||||||
# World border - bottom
|
|
||||||
if self.rect.bottom > world.height * TILE_SIZE:
|
|
||||||
self.rect.bottom = world.height * TILE_SIZE
|
|
||||||
self.pos.y = self.rect.y
|
|
||||||
self.on_ground = True
|
|
||||||
self.velocity.y = 0
|
|
||||||
|
|
||||||
self.update_state()
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# TIMERS
|
|
||||||
# ==========================================================
|
|
||||||
def handle_timers(self, dt):
|
|
||||||
if self.coyote_timer > 0:
|
|
||||||
self.coyote_timer -= dt
|
|
||||||
|
|
||||||
if self.jump_buffer_timer > 0:
|
|
||||||
self.jump_buffer_timer -= dt
|
|
||||||
|
|
||||||
if self.knockback_timer > 0:
|
|
||||||
self.knockback_timer -= dt
|
|
||||||
|
|
||||||
if self.invulnerable_timer > 0:
|
|
||||||
self.invulnerable_timer -= dt
|
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# INPUT
|
# INPUT
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
def handle_input(self, dt):
|
def handle_input(self):
|
||||||
keys = pygame.key.get_pressed()
|
keys = pygame.key.get_pressed()
|
||||||
|
|
||||||
if self.knockback_timer > 0:
|
self.acceleration.x = 0
|
||||||
return
|
|
||||||
|
|
||||||
move = 0
|
# Horizontal movement
|
||||||
if keys[pygame.K_a]:
|
if keys[pygame.K_a]:
|
||||||
move -= 1
|
self.acceleration.x = -PLAYER_ACCELERATION
|
||||||
self.facing_right = False
|
self.facing_right = False
|
||||||
|
|
||||||
if keys[pygame.K_d]:
|
if keys[pygame.K_d]:
|
||||||
move += 1
|
self.acceleration.x = PLAYER_ACCELERATION
|
||||||
self.facing_right = True
|
self.facing_right = True
|
||||||
|
|
||||||
accel = self.move_accel if self.on_ground else self.air_accel * self.air_control
|
# Apply friction
|
||||||
|
self.acceleration.x += self.velocity.x * PLAYER_FRICTION
|
||||||
|
|
||||||
if move != 0:
|
# Jump
|
||||||
self.velocity.x += move * accel * dt
|
if keys[pygame.K_SPACE] and self.on_ground:
|
||||||
else:
|
self.velocity.y = JUMP_FORCE
|
||||||
if self.on_ground:
|
|
||||||
self.velocity.x += self.velocity.x * self.ground_friction * dt
|
|
||||||
|
|
||||||
# Clamp horizontal speed
|
|
||||||
self.velocity.x = max(-self.max_run_speed,
|
|
||||||
min(self.max_run_speed, self.velocity.x))
|
|
||||||
|
|
||||||
# Jump buffering
|
|
||||||
if keys[pygame.K_SPACE]:
|
|
||||||
self.jump_buffer_timer = self.jump_buffer_max
|
|
||||||
|
|
||||||
# Variable jump height (jump cut)
|
|
||||||
if not keys[pygame.K_SPACE] and self.velocity.y < 0:
|
|
||||||
self.velocity.y *= self.jump_cut_multiplier
|
|
||||||
|
|
||||||
# Fast fall
|
|
||||||
if keys[pygame.K_s] and not self.on_ground:
|
|
||||||
self.velocity.y = min(self.velocity.y + 3000 * dt,
|
|
||||||
self.fast_fall_speed)
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# GRAVITY
|
|
||||||
# ==========================================================
|
|
||||||
def apply_gravity(self, dt):
|
|
||||||
self.velocity.y += self.gravity * dt
|
|
||||||
|
|
||||||
if self.velocity.y > self.max_fall_speed:
|
|
||||||
self.velocity.y = self.max_fall_speed
|
|
||||||
|
|
||||||
if self.on_ground:
|
|
||||||
self.coyote_timer = self.coyote_time_max
|
|
||||||
|
|
||||||
# Perform jump if allowed
|
|
||||||
if self.jump_buffer_timer > 0 and self.coyote_timer > 0:
|
|
||||||
self.velocity.y = self.jump_force
|
|
||||||
self.jump_buffer_timer = 0
|
|
||||||
self.coyote_timer = 0
|
|
||||||
self.on_ground = False
|
self.on_ground = False
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# PHYSICS
|
||||||
|
# ==========================================================
|
||||||
|
def apply_physics(self, dt):
|
||||||
|
# Apply gravity
|
||||||
|
self.acceleration.y = GRAVITY
|
||||||
|
|
||||||
|
# Update velocity
|
||||||
|
self.velocity += self.acceleration * dt
|
||||||
|
|
||||||
|
# Clamp fall speed
|
||||||
|
if self.velocity.y > MAX_FALL_SPEED:
|
||||||
|
self.velocity.y = MAX_FALL_SPEED
|
||||||
|
|
||||||
|
# Move horizontally
|
||||||
|
self.rect.x += self.velocity.x * dt
|
||||||
|
|
||||||
|
# Move vertically
|
||||||
|
self.rect.y += self.velocity.y * dt
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# COLLISIONS
|
# COLLISIONS
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
def handle_horizontal_collisions(self, world):
|
def handle_collisions(self, world):
|
||||||
for tile in world.get_nearby_tiles(self.rect):
|
|
||||||
if tile["solid"] and self.rect.colliderect(tile["rect"]):
|
|
||||||
if self.velocity.x > 0:
|
|
||||||
self.rect.right = tile["rect"].left
|
|
||||||
elif self.velocity.x < 0:
|
|
||||||
self.rect.left = tile["rect"].right
|
|
||||||
|
|
||||||
self.pos.x = self.rect.x
|
|
||||||
self.velocity.x = 0
|
|
||||||
|
|
||||||
def handle_vertical_collisions(self, world):
|
|
||||||
self.on_ground = False
|
self.on_ground = False
|
||||||
|
|
||||||
for tile in world.get_nearby_tiles(self.rect):
|
# Get nearby tiles once
|
||||||
if tile["solid"] and self.rect.colliderect(tile["rect"]):
|
nearby_tiles = world.get_nearby_tiles(self.rect)
|
||||||
if self.velocity.y > 0:
|
|
||||||
|
# Horizontal collisions
|
||||||
|
for tile in nearby_tiles:
|
||||||
|
if tile["rect"].colliderect(self.rect) and tile["solid"]:
|
||||||
|
if self.velocity.x > 0: # Moving right
|
||||||
|
self.rect.right = tile["rect"].left
|
||||||
|
elif self.velocity.x < 0: # Moving left
|
||||||
|
self.rect.left = tile["rect"].right
|
||||||
|
self.velocity.x = 0
|
||||||
|
|
||||||
|
# Vertical collisions
|
||||||
|
for tile in nearby_tiles:
|
||||||
|
if tile["rect"].colliderect(self.rect) and tile["solid"]:
|
||||||
|
if self.velocity.y > 0: # Falling
|
||||||
self.rect.bottom = tile["rect"].top
|
self.rect.bottom = tile["rect"].top
|
||||||
self.on_ground = True
|
self.on_ground = True
|
||||||
elif self.velocity.y < 0:
|
elif self.velocity.y < 0: # Jumping up
|
||||||
self.rect.top = tile["rect"].bottom
|
self.rect.top = tile["rect"].bottom
|
||||||
|
|
||||||
self.pos.y = self.rect.y
|
|
||||||
self.velocity.y = 0
|
self.velocity.y = 0
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# STATE MACHINE
|
|
||||||
# ==========================================================
|
|
||||||
def update_state(self):
|
|
||||||
if not self.on_ground:
|
|
||||||
self.state = "jump" if self.velocity.y < 0 else "fall"
|
|
||||||
elif abs(self.velocity.x) > 10:
|
|
||||||
self.state = "run"
|
|
||||||
else:
|
|
||||||
self.state = "idle"
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# DAMAGE / KNOCKBACK
|
|
||||||
# ==========================================================
|
|
||||||
def take_damage(self, amount, direction):
|
|
||||||
if self.invulnerable_timer > 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.health -= amount
|
|
||||||
self.invulnerable_timer = 0.5
|
|
||||||
|
|
||||||
self.velocity.x = direction * 500
|
|
||||||
self.velocity.y = -400
|
|
||||||
self.knockback_timer = 0.2
|
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# DRAW
|
# DRAW
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
def draw(self, screen, camera):
|
def draw(self, screen, camera):
|
||||||
draw_rect = camera.apply(self.rect)
|
draw_rect = camera.apply(self.rect)
|
||||||
|
|
||||||
# Flash when invulnerable
|
pygame.draw.rect(screen, (255, 50, 50), draw_rect)
|
||||||
if self.invulnerable_timer > 0:
|
|
||||||
color = (255, 255, 255)
|
|
||||||
else:
|
|
||||||
color = (255, 50, 50)
|
|
||||||
|
|
||||||
pygame.draw.rect(screen, color, draw_rect)
|
|
||||||
|
|
||||||
if SHOW_COLLIDERS:
|
|
||||||
pygame.draw.rect(screen, (0, 255, 0), draw_rect, 2)
|
|
||||||
|
|
||||||
# Draw health bar
|
|
||||||
health_bar_width = 28
|
|
||||||
health_bar_height = 4
|
|
||||||
health_percent = self.health / self.max_health
|
|
||||||
|
|
||||||
health_bar_rect = pygame.Rect(
|
|
||||||
draw_rect.x,
|
|
||||||
draw_rect.y - 8,
|
|
||||||
health_bar_width * health_percent,
|
|
||||||
health_bar_height
|
|
||||||
)
|
|
||||||
pygame.draw.rect(screen, (0, 255, 0), health_bar_rect)
|
|
||||||
pygame.draw.rect(screen, (255, 0, 0), pygame.Rect(
|
|
||||||
draw_rect.x,
|
|
||||||
draw_rect.y - 8,
|
|
||||||
health_bar_width,
|
|
||||||
health_bar_height
|
|
||||||
), 1)
|
|
||||||
if self.invulnerable_timer > 0:
|
|
||||||
color = (255, 255, 255)
|
|
||||||
else:
|
|
||||||
color = (255, 50, 50)
|
|
||||||
|
|
||||||
pygame.draw.rect(screen, color, draw_rect)
|
|
||||||
|
|
||||||
if SHOW_COLLIDERS:
|
if SHOW_COLLIDERS:
|
||||||
pygame.draw.rect(screen, (0, 255, 0), draw_rect, 2)
|
pygame.draw.rect(screen, (0, 255, 0), draw_rect, 2)
|
||||||
176
save_system.py
176
save_system.py
|
|
@ -1,176 +0,0 @@
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
from settings import *
|
|
||||||
|
|
||||||
|
|
||||||
class SaveSystem:
|
|
||||||
"""Handle game saving and loading"""
|
|
||||||
|
|
||||||
SAVE_DIR = "saves"
|
|
||||||
BACKUP_DIR = "saves/backups"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# Create directories if they don't exist
|
|
||||||
os.makedirs(self.SAVE_DIR, exist_ok=True)
|
|
||||||
os.makedirs(self.BACKUP_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
def save_game(self, game, world_name="Default"):
|
|
||||||
"""Save game state to file with world name"""
|
|
||||||
try:
|
|
||||||
# Create filename with world name and timestamp
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
filename = f"save_{world_name}_{timestamp}.json"
|
|
||||||
|
|
||||||
save_data = {
|
|
||||||
"world_name": world_name,
|
|
||||||
"player": {
|
|
||||||
"x": game.player.pos.x,
|
|
||||||
"y": game.player.pos.y,
|
|
||||||
"health": game.player.health,
|
|
||||||
"velocity_x": game.player.velocity.x,
|
|
||||||
"velocity_y": game.player.velocity.y,
|
|
||||||
},
|
|
||||||
"world": {
|
|
||||||
"grid": game.world.grid,
|
|
||||||
"width": game.world.width,
|
|
||||||
"height": game.world.height,
|
|
||||||
},
|
|
||||||
"inventory": {
|
|
||||||
"slots": game.inventory.slots,
|
|
||||||
},
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
}
|
|
||||||
|
|
||||||
filepath = os.path.join(self.SAVE_DIR, filename)
|
|
||||||
with open(filepath, 'w') as f:
|
|
||||||
json.dump(save_data, f, indent=4)
|
|
||||||
|
|
||||||
print(f"Game '{world_name}' saved to {filepath}")
|
|
||||||
return True, filename
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error saving game: {e}")
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
def load_game(self, game, save_file):
|
|
||||||
"""Load game state from file"""
|
|
||||||
try:
|
|
||||||
filepath = os.path.join(self.SAVE_DIR, save_file)
|
|
||||||
|
|
||||||
if not os.path.exists(filepath):
|
|
||||||
print(f"Save file not found: {filepath}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
with open(filepath, 'r') as f:
|
|
||||||
save_data = json.load(f)
|
|
||||||
|
|
||||||
# Store world name in game for future saves
|
|
||||||
if "world_name" in save_data:
|
|
||||||
game.world_name = save_data["world_name"]
|
|
||||||
|
|
||||||
# Load player data
|
|
||||||
game.player.pos.x = save_data["player"]["x"]
|
|
||||||
game.player.pos.y = save_data["player"]["y"]
|
|
||||||
game.player.rect.x = int(game.player.pos.x)
|
|
||||||
game.player.rect.y = int(game.player.pos.y)
|
|
||||||
game.player.health = save_data["player"]["health"]
|
|
||||||
game.player.velocity.x = save_data["player"]["velocity_x"]
|
|
||||||
game.player.velocity.y = save_data["player"]["velocity_y"]
|
|
||||||
|
|
||||||
# Load world data
|
|
||||||
game.world.grid = save_data["world"]["grid"]
|
|
||||||
|
|
||||||
# Load inventory
|
|
||||||
game.inventory.slots = save_data["inventory"]["slots"]
|
|
||||||
|
|
||||||
print(f"Game '{game.world_name}' loaded from {filepath}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading game: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def backup_game(self, game):
|
|
||||||
"""Create a backup of current save"""
|
|
||||||
try:
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
backup_filename = f"backup_{game.world_name}_{timestamp}.json"
|
|
||||||
|
|
||||||
save_data = {
|
|
||||||
"world_name": game.world_name,
|
|
||||||
"player": {
|
|
||||||
"x": game.player.pos.x,
|
|
||||||
"y": game.player.pos.y,
|
|
||||||
"health": game.player.health,
|
|
||||||
"velocity_x": game.player.velocity.x,
|
|
||||||
"velocity_y": game.player.velocity.y,
|
|
||||||
},
|
|
||||||
"world": {
|
|
||||||
"grid": game.world.grid,
|
|
||||||
"width": game.world.width,
|
|
||||||
"height": game.world.height,
|
|
||||||
},
|
|
||||||
"inventory": {
|
|
||||||
"slots": game.inventory.slots,
|
|
||||||
},
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
}
|
|
||||||
|
|
||||||
filepath = os.path.join(self.BACKUP_DIR, backup_filename)
|
|
||||||
with open(filepath, 'w') as f:
|
|
||||||
json.dump(save_data, f, indent=4)
|
|
||||||
|
|
||||||
print(f"Backup created: {filepath}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error creating backup: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_save_files(self):
|
|
||||||
"""Get list of all save files"""
|
|
||||||
try:
|
|
||||||
files = []
|
|
||||||
for filename in os.listdir(self.SAVE_DIR):
|
|
||||||
if filename.endswith(".json") and filename.startswith("save_"):
|
|
||||||
filepath = os.path.join(self.SAVE_DIR, filename)
|
|
||||||
files.append({
|
|
||||||
"name": filename,
|
|
||||||
"path": filepath,
|
|
||||||
"modified": os.path.getmtime(filepath)
|
|
||||||
})
|
|
||||||
return sorted(files, key=lambda x: x["modified"], reverse=True)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error reading save files: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_backups(self):
|
|
||||||
"""Get list of all backup files"""
|
|
||||||
try:
|
|
||||||
files = []
|
|
||||||
for filename in os.listdir(self.BACKUP_DIR):
|
|
||||||
if filename.endswith(".json"):
|
|
||||||
filepath = os.path.join(self.BACKUP_DIR, filename)
|
|
||||||
files.append({
|
|
||||||
"name": filename,
|
|
||||||
"path": filepath,
|
|
||||||
"modified": os.path.getmtime(filepath)
|
|
||||||
})
|
|
||||||
return sorted(files, key=lambda x: x["modified"], reverse=True)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error reading backups: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def delete_save(self, save_file):
|
|
||||||
"""Delete a save file"""
|
|
||||||
try:
|
|
||||||
filepath = os.path.join(self.SAVE_DIR, save_file)
|
|
||||||
if os.path.exists(filepath):
|
|
||||||
os.remove(filepath)
|
|
||||||
print(f"Save deleted: {filepath}")
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error deleting save: {e}")
|
|
||||||
return False
|
|
||||||
65
settings.py
65
settings.py
|
|
@ -6,7 +6,6 @@ SCREEN_WIDTH = 1280
|
||||||
SCREEN_HEIGHT = 720
|
SCREEN_HEIGHT = 720
|
||||||
FPS = 60
|
FPS = 60
|
||||||
VSYNC = True
|
VSYNC = True
|
||||||
FULLSCREEN = False
|
|
||||||
|
|
||||||
GAME_TITLE = "Terraria Clone"
|
GAME_TITLE = "Terraria Clone"
|
||||||
|
|
||||||
|
|
@ -42,11 +41,6 @@ ITEM_GOLD = 103
|
||||||
ITEM_COPPER = 104
|
ITEM_COPPER = 104
|
||||||
ITEM_COAL = 105
|
ITEM_COAL = 105
|
||||||
|
|
||||||
# Weapons
|
|
||||||
ITEM_IRON_SWORD = 200
|
|
||||||
ITEM_WOODEN_SWORD = 201
|
|
||||||
ITEM_GOLD_SWORD = 202
|
|
||||||
|
|
||||||
# Tile properties
|
# Tile properties
|
||||||
TILE_PROPERTIES = {
|
TILE_PROPERTIES = {
|
||||||
AIR: {"solid": False, "color": (0, 0, 0), "drop": None},
|
AIR: {"solid": False, "color": (0, 0, 0), "drop": None},
|
||||||
|
|
@ -82,12 +76,6 @@ ORE_THRESHOLD = 0.75
|
||||||
NOISE_SCALE = 0.05
|
NOISE_SCALE = 0.05
|
||||||
OCTAVES = 4
|
OCTAVES = 4
|
||||||
|
|
||||||
# Perlin noise settings for smoother terrain
|
|
||||||
PERLIN_SCALE = 80.0
|
|
||||||
PERLIN_OCTAVES = 6
|
|
||||||
PERLIN_PERSISTENCE = 0.5
|
|
||||||
PERLIN_LACUNARITY = 2.0
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# PLAYER SETTINGS
|
# PLAYER SETTINGS
|
||||||
|
|
@ -100,10 +88,10 @@ PLAYER_SPEED = 250
|
||||||
PLAYER_ACCELERATION = 2000
|
PLAYER_ACCELERATION = 2000
|
||||||
PLAYER_FRICTION = -0.15
|
PLAYER_FRICTION = -0.15
|
||||||
|
|
||||||
GRAVITY = 2200
|
GRAVITY = 1500
|
||||||
MAX_FALL_SPEED = 1000
|
MAX_FALL_SPEED = 1000
|
||||||
|
|
||||||
JUMP_FORCE = -700
|
JUMP_FORCE = -500
|
||||||
DOUBLE_JUMP = False
|
DOUBLE_JUMP = False
|
||||||
|
|
||||||
MAX_HEALTH = 100
|
MAX_HEALTH = 100
|
||||||
|
|
@ -128,43 +116,6 @@ STACK_LIMIT = 999
|
||||||
INVENTORY_SLOT_SIZE = 40
|
INVENTORY_SLOT_SIZE = 40
|
||||||
INVENTORY_PADDING = 4
|
INVENTORY_PADDING = 4
|
||||||
|
|
||||||
# Hotbar settings
|
|
||||||
HOTBAR_VISIBLE = True
|
|
||||||
HOTBAR_SLOT_COUNT = 8
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# COMBAT SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
# Weapon stats
|
|
||||||
WEAPON_STATS = {
|
|
||||||
ITEM_WOODEN_SWORD: {
|
|
||||||
"name": "Wooden Sword",
|
|
||||||
"damage": 8,
|
|
||||||
"cooldown": 0.6,
|
|
||||||
"knockback": 300,
|
|
||||||
"color": (160, 82, 45),
|
|
||||||
},
|
|
||||||
ITEM_IRON_SWORD: {
|
|
||||||
"name": "Iron Sword",
|
|
||||||
"damage": 15,
|
|
||||||
"cooldown": 0.8,
|
|
||||||
"knockback": 400,
|
|
||||||
"color": (180, 180, 180),
|
|
||||||
},
|
|
||||||
ITEM_GOLD_SWORD: {
|
|
||||||
"name": "Gold Sword",
|
|
||||||
"damage": 20,
|
|
||||||
"cooldown": 1.0,
|
|
||||||
"knockback": 500,
|
|
||||||
"color": (255, 215, 0),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ATTACK_RANGE = 60 # pixels
|
|
||||||
ATTACK_COOLDOWN = 0.5 # seconds
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# BLOCK BREAKING / PLACING
|
# BLOCK BREAKING / PLACING
|
||||||
|
|
@ -194,8 +145,6 @@ ENEMY_SPAWN_RATE = 5 # seconds
|
||||||
ENEMY_SPEED = 100
|
ENEMY_SPEED = 100
|
||||||
ENEMY_DAMAGE = 10
|
ENEMY_DAMAGE = 10
|
||||||
ENEMY_HEALTH = 50
|
ENEMY_HEALTH = 50
|
||||||
ENEMY_GRAVITY = 2200 # Enemies affected by gravity
|
|
||||||
ENEMY_KNOCKBACK_RESISTANCE = 0.8 # How much knockback affects enemies
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
@ -214,16 +163,6 @@ ENABLE_LIGHTING = False
|
||||||
LIGHT_RADIUS = 5
|
LIGHT_RADIUS = 5
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# DEBUG SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
DEBUG_MODE = True
|
|
||||||
SHOW_FPS = True
|
|
||||||
SHOW_COLLIDERS = False
|
|
||||||
SHOW_CHUNK_BORDERS = False
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# DEBUG SETTINGS
|
# DEBUG SETTINGS
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
|
||||||
2
tiles.py
2
tiles.py
|
|
@ -36,5 +36,3 @@ TILE_PROPERTIES = {
|
||||||
|
|
||||||
def get_tile(tile_id):
|
def get_tile(tile_id):
|
||||||
return TILE_TYPES.get(tile_id, TILE_TYPES[AIR])
|
return TILE_TYPES.get(tile_id, TILE_TYPES[AIR])
|
||||||
def get_tile(tile_id):
|
|
||||||
return TILE_TYPES.get(tile_id, TILE_TYPES[AIR])
|
|
||||||
|
|
|
||||||
100
weapon_system.py
100
weapon_system.py
|
|
@ -1,100 +0,0 @@
|
||||||
import pygame
|
|
||||||
from settings import *
|
|
||||||
|
|
||||||
|
|
||||||
class Weapon:
|
|
||||||
"""Base weapon class"""
|
|
||||||
|
|
||||||
def __init__(self, item_id, name, damage, cooldown, knockback, color):
|
|
||||||
self.item_id = item_id
|
|
||||||
self.name = name
|
|
||||||
self.damage = damage
|
|
||||||
self.cooldown = cooldown
|
|
||||||
self.knockback = knockback
|
|
||||||
self.color = color
|
|
||||||
self.last_attack_time = 0
|
|
||||||
|
|
||||||
def can_attack(self, current_time):
|
|
||||||
"""Check if enough time has passed for another attack"""
|
|
||||||
return (current_time - self.last_attack_time) >= self.cooldown
|
|
||||||
|
|
||||||
def do_attack(self, current_time):
|
|
||||||
"""Perform attack and update cooldown"""
|
|
||||||
if self.can_attack(current_time):
|
|
||||||
self.last_attack_time = current_time
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_cooldown_percent(self, current_time):
|
|
||||||
"""Get cooldown progress (0.0 to 1.0)"""
|
|
||||||
time_since_attack = current_time - self.last_attack_time
|
|
||||||
if time_since_attack >= self.cooldown:
|
|
||||||
return 1.0
|
|
||||||
return time_since_attack / self.cooldown
|
|
||||||
|
|
||||||
|
|
||||||
class WeaponManager:
|
|
||||||
"""Manage player weapons and current selection"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.weapons = {}
|
|
||||||
self.current_weapon_id = None
|
|
||||||
self.current_time = 0
|
|
||||||
self.setup_default_weapons()
|
|
||||||
|
|
||||||
def setup_default_weapons(self):
|
|
||||||
"""Create default weapons"""
|
|
||||||
for weapon_id, stats in WEAPON_STATS.items():
|
|
||||||
weapon = Weapon(
|
|
||||||
weapon_id,
|
|
||||||
stats["name"],
|
|
||||||
stats["damage"],
|
|
||||||
stats["cooldown"],
|
|
||||||
stats["knockback"],
|
|
||||||
stats["color"]
|
|
||||||
)
|
|
||||||
self.weapons[weapon_id] = weapon
|
|
||||||
|
|
||||||
# Set first weapon as current
|
|
||||||
if self.weapons:
|
|
||||||
self.current_weapon_id = list(self.weapons.keys())[0]
|
|
||||||
|
|
||||||
def get_current_weapon(self):
|
|
||||||
"""Get the currently equipped weapon"""
|
|
||||||
if self.current_weapon_id in self.weapons:
|
|
||||||
return self.weapons[self.current_weapon_id]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def switch_weapon(self, weapon_id):
|
|
||||||
"""Switch to a different weapon"""
|
|
||||||
if weapon_id in self.weapons:
|
|
||||||
self.current_weapon_id = weapon_id
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def attack(self):
|
|
||||||
"""Attempt attack with current weapon"""
|
|
||||||
weapon = self.get_current_weapon()
|
|
||||||
if weapon and weapon.do_attack(self.current_time):
|
|
||||||
return weapon.damage, weapon.knockback
|
|
||||||
return 0, 0
|
|
||||||
|
|
||||||
def update(self, dt):
|
|
||||||
"""Update weapon manager"""
|
|
||||||
self.current_time += dt
|
|
||||||
|
|
||||||
def add_weapon(self, weapon_id):
|
|
||||||
"""Add weapon to collection (for inventory)"""
|
|
||||||
if weapon_id in WEAPON_STATS and weapon_id not in self.weapons:
|
|
||||||
stats = WEAPON_STATS[weapon_id]
|
|
||||||
weapon = Weapon(
|
|
||||||
weapon_id,
|
|
||||||
stats["name"],
|
|
||||||
stats["damage"],
|
|
||||||
stats["cooldown"],
|
|
||||||
stats["knockback"],
|
|
||||||
stats["color"]
|
|
||||||
)
|
|
||||||
self.weapons[weapon_id] = weapon
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
152
world.py
152
world.py
|
|
@ -1,63 +1,9 @@
|
||||||
import pygame
|
import pygame
|
||||||
import random
|
import random
|
||||||
import math
|
|
||||||
from settings import *
|
from settings import *
|
||||||
from tiles import get_tile
|
from tiles import get_tile
|
||||||
|
|
||||||
|
|
||||||
# Perlin noise implementation
|
|
||||||
def perlin_fade(t):
|
|
||||||
"""Fade function for Perlin noise"""
|
|
||||||
return t * t * t * (t * (t * 6 - 15) + 10)
|
|
||||||
|
|
||||||
|
|
||||||
def perlin_lerp(t, a, b):
|
|
||||||
"""Linear interpolation"""
|
|
||||||
return a + t * (b - a)
|
|
||||||
|
|
||||||
|
|
||||||
def perlin_noise_1d(x, seed=42):
|
|
||||||
"""Simple 1D Perlin-like noise"""
|
|
||||||
xi = int(x) & 0xFF
|
|
||||||
xf = x - int(x)
|
|
||||||
|
|
||||||
u = perlin_fade(xf)
|
|
||||||
|
|
||||||
# Generate consistent random values
|
|
||||||
random.seed(seed + xi)
|
|
||||||
a = random.random()
|
|
||||||
random.seed(seed + xi + 1)
|
|
||||||
b = random.random()
|
|
||||||
|
|
||||||
return perlin_lerp(u, a, b)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_noise_map(width, height, scale=50, octaves=4, seed=42):
|
|
||||||
"""Generate a noise map using Perlin-like noise"""
|
|
||||||
noise_map = [[0.0 for _ in range(width)] for _ in range(height)]
|
|
||||||
|
|
||||||
for y in range(height):
|
|
||||||
for x in range(width):
|
|
||||||
value = 0.0
|
|
||||||
amplitude = 1.0
|
|
||||||
frequency = 1.0
|
|
||||||
max_value = 0.0
|
|
||||||
|
|
||||||
for i in range(octaves):
|
|
||||||
sample_x = (x / scale) * frequency
|
|
||||||
noise = perlin_noise_1d(sample_x + (y * 73 * frequency), seed + i * 256)
|
|
||||||
|
|
||||||
value += noise * amplitude
|
|
||||||
max_value += amplitude
|
|
||||||
|
|
||||||
amplitude *= 0.5
|
|
||||||
frequency *= 2.0
|
|
||||||
|
|
||||||
noise_map[y][x] = value / max_value if max_value > 0 else 0.0
|
|
||||||
|
|
||||||
return noise_map
|
|
||||||
|
|
||||||
|
|
||||||
class World:
|
class World:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.width = WORLD_WIDTH
|
self.width = WORLD_WIDTH
|
||||||
|
|
@ -72,27 +18,31 @@ class World:
|
||||||
self.generate_world()
|
self.generate_world()
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# WORLD GENERATION - FLAT WITH TREES
|
# WORLD GENERATION
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
def generate_world(self):
|
def generate_world(self):
|
||||||
"""Generate flat world with trees"""
|
# First generate terrain
|
||||||
|
|
||||||
# Create flat surface at SURFACE_LEVEL
|
|
||||||
for x in range(self.width):
|
for x in range(self.width):
|
||||||
|
|
||||||
|
# Simple terrain height variation
|
||||||
|
surface_height = SURFACE_LEVEL + random.randint(-3, 3)
|
||||||
|
|
||||||
for y in range(self.height):
|
for y in range(self.height):
|
||||||
if y < SURFACE_LEVEL:
|
|
||||||
|
if y < surface_height:
|
||||||
self.grid[y][x] = AIR
|
self.grid[y][x] = AIR
|
||||||
elif y == SURFACE_LEVEL:
|
|
||||||
|
elif y == surface_height:
|
||||||
self.grid[y][x] = GRASS
|
self.grid[y][x] = GRASS
|
||||||
elif y < SURFACE_LEVEL + 5:
|
|
||||||
|
elif y < surface_height + 5:
|
||||||
self.grid[y][x] = DIRT
|
self.grid[y][x] = DIRT
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.grid[y][x] = STONE
|
self.grid[y][x] = STONE
|
||||||
|
|
||||||
# Add trees (more frequently)
|
# Then add trees + ores
|
||||||
self.generate_trees()
|
self.generate_trees()
|
||||||
|
|
||||||
# Add ores
|
|
||||||
self.generate_ores()
|
self.generate_ores()
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
@ -113,7 +63,7 @@ class World:
|
||||||
tile_id = self.grid[y][x]
|
tile_id = self.grid[y][x]
|
||||||
|
|
||||||
if tile_id != AIR:
|
if tile_id != AIR:
|
||||||
color = get_tile(tile_id).color
|
color = get_tile(tile_id).color # FIXED: no more KeyError
|
||||||
|
|
||||||
world_rect = pygame.Rect(
|
world_rect = pygame.Rect(
|
||||||
x * TILE_SIZE,
|
x * TILE_SIZE,
|
||||||
|
|
@ -165,22 +115,11 @@ class World:
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# BLOCK BREAKING
|
# BLOCK BREAKING
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
def break_block(self, mouse_pos, camera, inventory, player_rect):
|
def break_block(self, mouse_pos, camera, inventory):
|
||||||
"""Break block at mouse position if in range"""
|
|
||||||
world_x, world_y = camera.screen_to_world(mouse_pos)
|
world_x, world_y = camera.screen_to_world(mouse_pos)
|
||||||
tile_x = int(world_x // TILE_SIZE)
|
tile_x = int(world_x // TILE_SIZE)
|
||||||
tile_y = int(world_y // TILE_SIZE)
|
tile_y = int(world_y // TILE_SIZE)
|
||||||
|
|
||||||
# Check if in range
|
|
||||||
distance = math.sqrt(
|
|
||||||
(tile_x * TILE_SIZE - player_rect.centerx) ** 2 +
|
|
||||||
(tile_y * TILE_SIZE - player_rect.centery) ** 2
|
|
||||||
)
|
|
||||||
|
|
||||||
if distance > BREAK_RANGE * TILE_SIZE:
|
|
||||||
print(f"DEBUG: Block too far away! Distance: {distance}, Max: {BREAK_RANGE * TILE_SIZE}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.in_bounds(tile_x, tile_y):
|
if self.in_bounds(tile_x, tile_y):
|
||||||
tile_id = self.grid[tile_y][tile_x]
|
tile_id = self.grid[tile_y][tile_x]
|
||||||
tile = get_tile(tile_id)
|
tile = get_tile(tile_id)
|
||||||
|
|
@ -188,39 +127,20 @@ class World:
|
||||||
if tile_id != AIR:
|
if tile_id != AIR:
|
||||||
if tile.drop:
|
if tile.drop:
|
||||||
inventory.add_item(tile.drop, 1)
|
inventory.add_item(tile.drop, 1)
|
||||||
print(f"DEBUG: Broke {tile.name}, added {tile.drop} to inventory")
|
|
||||||
self.grid[tile_y][tile_x] = AIR
|
self.grid[tile_y][tile_x] = AIR
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# BLOCK PLACING
|
# BLOCK PLACING
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
def place_block(self, mouse_pos, camera, player_rect, block_type=DIRT):
|
def place_block(self, mouse_pos, camera, block_type=DIRT):
|
||||||
"""Place block at mouse position if in range"""
|
|
||||||
world_x, world_y = camera.screen_to_world(mouse_pos)
|
world_x, world_y = camera.screen_to_world(mouse_pos)
|
||||||
|
|
||||||
tile_x = int(world_x // TILE_SIZE)
|
tile_x = int(world_x // TILE_SIZE)
|
||||||
tile_y = int(world_y // TILE_SIZE)
|
tile_y = int(world_y // TILE_SIZE)
|
||||||
|
|
||||||
# Check if in range
|
|
||||||
distance = math.sqrt(
|
|
||||||
(tile_x * TILE_SIZE - player_rect.centerx) ** 2 +
|
|
||||||
(tile_y * TILE_SIZE - player_rect.centery) ** 2
|
|
||||||
)
|
|
||||||
|
|
||||||
if distance > PLACE_RANGE * TILE_SIZE:
|
|
||||||
print(f"DEBUG: Block placement too far away!")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.in_bounds(tile_x, tile_y):
|
if self.in_bounds(tile_x, tile_y):
|
||||||
if self.grid[tile_y][tile_x] == AIR:
|
if self.grid[tile_y][tile_x] == AIR:
|
||||||
self.grid[tile_y][tile_x] = block_type
|
self.grid[tile_y][tile_x] = block_type
|
||||||
print(f"DEBUG: Placed block at {tile_x}, {tile_y}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# UTIL
|
# UTIL
|
||||||
|
|
@ -229,7 +149,7 @@ class World:
|
||||||
return 0 <= x < self.width and 0 <= y < self.height
|
return 0 <= x < self.width and 0 <= y < self.height
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Get surface height for player spawn
|
# NEW: Get surface height for player spawn
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
def get_surface_height(self, x):
|
def get_surface_height(self, x):
|
||||||
for y in range(self.height):
|
for y in range(self.height):
|
||||||
|
|
@ -238,29 +158,29 @@ class World:
|
||||||
return self.height - 1
|
return self.height - 1
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# TREE GENERATION - MORE TREES
|
# TREE GENERATION
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
def generate_trees(self):
|
def generate_trees(self):
|
||||||
# Generate trees more frequently (every 4 tiles)
|
for x in range(0, self.width, 6):
|
||||||
for x in range(0, self.width, 4):
|
if random.random() < 0.25: # 25% chance
|
||||||
if random.random() < 0.6: # 60% chance for trees
|
# find surface
|
||||||
surface_y = SURFACE_LEVEL
|
for y in range(self.height):
|
||||||
if 0 <= x < self.width:
|
if self.grid[y][x] == GRASS:
|
||||||
self.spawn_tree(x, surface_y)
|
self.spawn_tree(x, y)
|
||||||
|
break
|
||||||
|
|
||||||
def spawn_tree(self, x, surface_y):
|
def spawn_tree(self, x, surface_y):
|
||||||
# trunk height
|
# trunk height
|
||||||
height = random.randint(5, 8)
|
height = random.randint(4, 7)
|
||||||
for i in range(height):
|
for i in range(height):
|
||||||
if surface_y - 1 - i >= 0:
|
self.grid[surface_y - 1 - i][x] = WOOD
|
||||||
self.grid[surface_y - 1 - i][x] = WOOD
|
|
||||||
|
|
||||||
# leaves - larger canopy
|
# leaves
|
||||||
leaf_start = surface_y - height - 1
|
leaf_start = surface_y - height - 1
|
||||||
for y in range(leaf_start, leaf_start - 5, -1):
|
for y in range(leaf_start, leaf_start - 4, -1):
|
||||||
for lx in range(x - 3, x + 4):
|
for lx in range(x - 2, x + 3):
|
||||||
if 0 <= lx < self.width and 0 <= y < self.height:
|
if 0 <= lx < self.width and 0 <= y < self.height:
|
||||||
if random.random() > 0.2: # 80% leaf density
|
if random.random() > 0.25:
|
||||||
self.grid[y][lx] = LEAVES
|
self.grid[y][lx] = LEAVES
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
@ -281,11 +201,3 @@ class World:
|
||||||
self.grid[y][x] = IRON_ORE
|
self.grid[y][x] = IRON_ORE
|
||||||
else:
|
else:
|
||||||
self.grid[y][x] = GOLD_ORE
|
self.grid[y][x] = GOLD_ORE
|
||||||
if r < 0.5:
|
|
||||||
self.grid[y][x] = COAL_ORE
|
|
||||||
elif r < 0.75:
|
|
||||||
self.grid[y][x] = COPPER_ORE
|
|
||||||
elif r < 0.9:
|
|
||||||
self.grid[y][x] = IRON_ORE
|
|
||||||
else:
|
|
||||||
self.grid[y][x] = GOLD_ORE
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue