Compare commits

..

No commits in common. "main" and "master" have entirely different histories.
main ... master

25 changed files with 147 additions and 2257 deletions

View File

@ -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.

View File

@ -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
View File

@ -3,131 +3,15 @@ from settings import *
class Enemy:
def __init__(self, x, y):
# Position & physics
self.pos = pygame.Vector2(x, y)
self.rect = pygame.Rect(x, y, 28, 48)
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):
if not self.alive:
return
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
# Basic AI: follow player
if player.rect.x < self.rect.x:
self.rect.x -= ENEMY_SPEED * dt
else:
self.knockback_timer -= 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
self.rect.x += ENEMY_SPEED * dt
def draw(self, screen, camera):
if not self.alive:
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)
pygame.draw.rect(screen, (0, 0, 255), camera.apply(self.rect))

View File

@ -23,9 +23,6 @@ class EnemyManager:
for enemy in self.enemies:
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):
for enemy in self.enemies:
enemy.draw(screen, camera)

400
game.py
View File

@ -6,103 +6,38 @@ from player import Player
from camera import Camera
from inventory import Inventory
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:
def __init__(self):
pygame.init()
pygame.init() # <-- IMPORTANT
self.show_inventory = False
# ----------------------------------
# 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.running = True
self.paused = False
# ----------------------------------
# Game State Management
# Core Systems
# ----------------------------------
self.game_state = "menu"
self.show_inventory = False
self.world_name = "Default"
# ----------------------------------
# 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
self.world = World()
self.player = Player(100, 100)
self.camera = Camera(self.player)
self.inventory = Inventory()
self.enemy_manager = EnemyManager(self.world, self.player)
# ----------------------------------
# Debug
# ----------------------------------
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
# ==========================================================
@ -110,185 +45,17 @@ class Game:
while self.running:
dt = self.clock.tick(FPS) / 1000
if self.game_state == "menu":
self.handle_menu_events()
self.update_menu()
self.draw_menu()
self.handle_events()
elif self.game_state == "settings":
self.handle_settings_events()
self.settings_menu.update(dt)
self.settings_menu.draw(self.screen)
if not self.paused:
self.update(dt)
elif self.game_state == "new_game_dialog":
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()
self.draw()
pygame.quit()
# ==========================================================
# MAIN MENU STATE
# ==========================================================
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
# EVENTS
# ==========================================================
def handle_events(self):
for event in pygame.event.get():
@ -298,85 +65,44 @@ class Game:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.paused = True
self.pause_menu = PauseMenu(self)
self.game_state = "paused"
self.paused = not self.paused
# Toggle inventory
if event.key == pygame.K_i:
self.show_inventory = not self.show_inventory
# Hotbar input
if self.hotbar:
self.hotbar.handle_input(event)
# Mouse input for attacking and placing
# -------------------------------
# Mouse input for breaking/placing
# -------------------------------
if event.type == pygame.MOUSEBUTTONDOWN:
if self.show_inventory:
continue
if event.button == 1: # Left click
# Get current held item
items_list = list(self.inventory.slots.items())
current_item = None
if self.hotbar.selected_slot < len(items_list):
current_item_id, _ = items_list[self.hotbar.selected_slot]
current_item = get_item(current_item_id)
self.world.break_block(
pygame.mouse.get_pos(),
self.camera,
self.inventory
)
# Check if holding a weapon
is_holding_weapon = (current_item is not None and
current_item.id in WEAPON_STATS)
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
)
if event.button == 3: # Right click
self.world.place_block(
pygame.mouse.get_pos(),
self.camera
)
# ==========================================================
# UPDATE
# ==========================================================
def update(self, dt):
# If inventory is open, pause gameplay
if self.show_inventory:
return
self.player.update(dt, self.world)
self.camera.update()
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):
self.screen.fill(BACKGROUND_COLOR)
@ -391,10 +117,6 @@ class Game:
if self.show_inventory:
self.inventory.draw(self.screen)
# Draw hotbar
if self.hotbar and not self.show_inventory:
self.hotbar.draw(self.screen)
self.draw_ui()
pygame.display.flip()
@ -415,52 +137,6 @@ class Game:
)
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:
pause_text = self.font.render("PAUSED", True, (255, 0, 0))
rect = pause_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2))

127
hotbar.py
View File

@ -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))

View File

@ -1,6 +1,6 @@
import pygame
from settings import *
from items import get_item
from items import ITEMS, get_item
class Inventory:
@ -25,13 +25,6 @@ class Inventory:
else:
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):
font = pygame.font.SysFont("consolas", 18)
title_font = pygame.font.SysFont("consolas", 24)
@ -53,13 +46,14 @@ class Inventory:
start_x = self.x + self.padding
start_y = self.y + 70
# Convert slots to list for display
items_list = list(self.slots.items())
index = 0
mouse = pygame.mouse.get_pos()
for row in range(self.grid_rows):
for col in range(self.grid_cols):
cell_rect = pygame.Rect(
start_x + col * (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)
# Draw amount if stack > 1
if amount > 1:
amount_text = font.render(str(amount), True, (255, 255, 255))
screen.blit(amount_text, (cell_rect.x + 2, cell_rect.y + 2))
# Draw amount
amount_text = font.render(str(amount), True, (255, 255, 255))
screen.blit(amount_text, (cell_rect.x + 2, cell_rect.y + 2))
# Tooltip when hovering
# Tooltip on hover
if cell_rect.collidepoint(mouse):
tooltip = font.render(item.name, True, (255, 255, 0))
screen.blit(tooltip, (mouse[0] + 10, mouse[1] + 10))

View File

@ -1,5 +1,6 @@
from settings import *
class Item:
def __init__(self, item_id, name, color, stack_limit=999, placeable=False, place_tile=None):
self.id = item_id
@ -60,34 +61,8 @@ ITEMS = {
stack_limit=999,
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):
"""Return the Item object for a given item_id, or None if it doesn't exist."""
return ITEMS.get(item_id)
return ITEMS.get(item_id, None)

View File

@ -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
View File

@ -3,269 +3,109 @@ from settings import *
class Player:
def __init__(self, x, y, weapon_manager=None):
# =========================
# POSITION
# =========================
self.pos = pygame.Vector2(x, y)
def __init__(self, x, y):
# Position & Size
self.rect = pygame.Rect(x, y, PLAYER_WIDTH, PLAYER_HEIGHT)
# =========================
# MOVEMENT
# =========================
# Movement
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)
# State
self.on_ground = False
self.facing_right = True
# Movement tuning
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
# =========================
# Stats
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
# ==========================================================
def update(self, dt, world):
dt = min(dt, 0.05)
self.handle_timers(dt)
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
self.handle_input()
self.apply_physics(dt)
self.handle_collisions(world)
# ==========================================================
# INPUT
# ==========================================================
def handle_input(self, dt):
def handle_input(self):
keys = pygame.key.get_pressed()
if self.knockback_timer > 0:
return
self.acceleration.x = 0
move = 0
# Horizontal movement
if keys[pygame.K_a]:
move -= 1
self.acceleration.x = -PLAYER_ACCELERATION
self.facing_right = False
if keys[pygame.K_d]:
move += 1
self.acceleration.x = PLAYER_ACCELERATION
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:
self.velocity.x += move * accel * dt
else:
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
# Jump
if keys[pygame.K_SPACE] and self.on_ground:
self.velocity.y = JUMP_FORCE
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
# ==========================================================
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):
def handle_collisions(self, world):
self.on_ground = False
for tile in world.get_nearby_tiles(self.rect):
if tile["solid"] and self.rect.colliderect(tile["rect"]):
if self.velocity.y > 0:
# Get nearby tiles once
nearby_tiles = world.get_nearby_tiles(self.rect)
# 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.on_ground = True
elif self.velocity.y < 0:
elif self.velocity.y < 0: # Jumping up
self.rect.top = tile["rect"].bottom
self.pos.y = self.rect.y
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
# ==========================================================
def draw(self, screen, camera):
draw_rect = camera.apply(self.rect)
# Flash when invulnerable
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)
pygame.draw.rect(screen, (255, 50, 50), draw_rect)
if SHOW_COLLIDERS:
pygame.draw.rect(screen, (0, 255, 0), draw_rect, 2)

View File

@ -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

View File

@ -6,7 +6,6 @@ SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
FPS = 60
VSYNC = True
FULLSCREEN = False
GAME_TITLE = "Terraria Clone"
@ -42,11 +41,6 @@ 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},
@ -82,12 +76,6 @@ ORE_THRESHOLD = 0.75
NOISE_SCALE = 0.05
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
@ -100,10 +88,10 @@ PLAYER_SPEED = 250
PLAYER_ACCELERATION = 2000
PLAYER_FRICTION = -0.15
GRAVITY = 2200
GRAVITY = 1500
MAX_FALL_SPEED = 1000
JUMP_FORCE = -700
JUMP_FORCE = -500
DOUBLE_JUMP = False
MAX_HEALTH = 100
@ -128,43 +116,6 @@ STACK_LIMIT = 999
INVENTORY_SLOT_SIZE = 40
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
@ -194,8 +145,6 @@ ENEMY_SPAWN_RATE = 5 # seconds
ENEMY_SPEED = 100
ENEMY_DAMAGE = 10
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
# ==========================================
# DEBUG SETTINGS
# ==========================================
DEBUG_MODE = True
SHOW_FPS = True
SHOW_COLLIDERS = False
SHOW_CHUNK_BORDERS = False
# ==========================================
# DEBUG SETTINGS
# ==========================================

View File

@ -36,5 +36,3 @@ TILE_PROPERTIES = {
def get_tile(tile_id):
return TILE_TYPES.get(tile_id, TILE_TYPES[AIR])
def get_tile(tile_id):
return TILE_TYPES.get(tile_id, TILE_TYPES[AIR])

View File

@ -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
View File

@ -1,63 +1,9 @@
import pygame
import random
import math
from settings import *
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:
def __init__(self):
self.width = WORLD_WIDTH
@ -72,27 +18,31 @@ class World:
self.generate_world()
# ==========================================================
# WORLD GENERATION - FLAT WITH TREES
# WORLD GENERATION
# ==========================================================
def generate_world(self):
"""Generate flat world with trees"""
# Create flat surface at SURFACE_LEVEL
# First generate terrain
for x in range(self.width):
# Simple terrain height variation
surface_height = SURFACE_LEVEL + random.randint(-3, 3)
for y in range(self.height):
if y < SURFACE_LEVEL:
if y < surface_height:
self.grid[y][x] = AIR
elif y == SURFACE_LEVEL:
elif y == surface_height:
self.grid[y][x] = GRASS
elif y < SURFACE_LEVEL + 5:
elif y < surface_height + 5:
self.grid[y][x] = DIRT
else:
self.grid[y][x] = STONE
# Add trees (more frequently)
# Then add trees + ores
self.generate_trees()
# Add ores
self.generate_ores()
# ==========================================================
@ -113,7 +63,7 @@ class World:
tile_id = self.grid[y][x]
if tile_id != AIR:
color = get_tile(tile_id).color
color = get_tile(tile_id).color # FIXED: no more KeyError
world_rect = pygame.Rect(
x * TILE_SIZE,
@ -165,22 +115,11 @@ class World:
# ==========================================================
# BLOCK BREAKING
# ==========================================================
def break_block(self, mouse_pos, camera, inventory, player_rect):
"""Break block at mouse position if in range"""
def break_block(self, mouse_pos, camera, inventory):
world_x, world_y = camera.screen_to_world(mouse_pos)
tile_x = int(world_x // 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):
tile_id = self.grid[tile_y][tile_x]
tile = get_tile(tile_id)
@ -188,39 +127,20 @@ class World:
if tile_id != AIR:
if tile.drop:
inventory.add_item(tile.drop, 1)
print(f"DEBUG: Broke {tile.name}, added {tile.drop} to inventory")
self.grid[tile_y][tile_x] = AIR
return True
return False
# ==========================================================
# BLOCK PLACING
# ==========================================================
def place_block(self, mouse_pos, camera, player_rect, block_type=DIRT):
"""Place block at mouse position if in range"""
def place_block(self, mouse_pos, camera, block_type=DIRT):
world_x, world_y = camera.screen_to_world(mouse_pos)
tile_x = int(world_x // 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.grid[tile_y][tile_x] == AIR:
self.grid[tile_y][tile_x] = block_type
print(f"DEBUG: Placed block at {tile_x}, {tile_y}")
return True
return False
# ==========================================================
# UTIL
@ -229,7 +149,7 @@ class World:
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):
for y in range(self.height):
@ -238,29 +158,29 @@ class World:
return self.height - 1
# ==========================================================
# TREE GENERATION - MORE TREES
# TREE GENERATION
# ==========================================================
def generate_trees(self):
# Generate trees more frequently (every 4 tiles)
for x in range(0, self.width, 4):
if random.random() < 0.6: # 60% chance for trees
surface_y = SURFACE_LEVEL
if 0 <= x < self.width:
self.spawn_tree(x, surface_y)
for x in range(0, self.width, 6):
if random.random() < 0.25: # 25% chance
# find surface
for y in range(self.height):
if self.grid[y][x] == GRASS:
self.spawn_tree(x, y)
break
def spawn_tree(self, x, surface_y):
# trunk height
height = random.randint(5, 8)
height = random.randint(4, 7)
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
for y in range(leaf_start, leaf_start - 5, -1):
for lx in range(x - 3, x + 4):
for y in range(leaf_start, leaf_start - 4, -1):
for lx in range(x - 2, x + 3):
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
# ==========================================================
@ -281,11 +201,3 @@ class World:
self.grid[y][x] = IRON_ORE
else:
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