Compare commits

..

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

25 changed files with 2255 additions and 145 deletions

185
settings.py 100644
View File

@ -0,0 +1,185 @@
# ==========================================
# 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.

61
combat_system.py 100644
View File

@ -0,0 +1,61 @@
# 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,15 +3,131 @@ 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):
# Basic AI: follow player if not self.alive:
if player.rect.x < self.rect.x: return
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.rect.x += ENEMY_SPEED * dt 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
def draw(self, screen, camera): def draw(self, screen, camera):
pygame.draw.rect(screen, (0, 0, 255), camera.apply(self.rect)) 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)

View File

@ -23,6 +23,9 @@ 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
View File

@ -6,38 +6,103 @@ 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() # <-- IMPORTANT pygame.init()
self.show_inventory = False
# ---------------------------------- # ----------------------------------
# Display Setup # Display Setup
# ---------------------------------- # ----------------------------------
flags = pygame.SCALED self.setup_display()
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
# ---------------------------------- # ----------------------------------
# Core Systems # Game State Management
# ---------------------------------- # ----------------------------------
self.world = World() self.game_state = "menu"
self.player = Player(100, 100) self.show_inventory = False
self.camera = Camera(self.player) self.world_name = "Default"
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
# ========================================================== # ==========================================================
@ -45,17 +110,185 @@ class Game:
while self.running: while self.running:
dt = self.clock.tick(FPS) / 1000 dt = self.clock.tick(FPS) / 1000
self.handle_events() if self.game_state == "menu":
self.handle_menu_events()
self.update_menu()
self.draw_menu()
if not self.paused: elif self.game_state == "settings":
self.update(dt) self.handle_settings_events()
self.settings_menu.update(dt)
self.settings_menu.draw(self.screen)
self.draw() 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()
pygame.quit() pygame.quit()
# ========================================================== # ==========================================================
# EVENTS # 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
# ========================================================== # ==========================================================
def handle_events(self): def handle_events(self):
for event in pygame.event.get(): for event in pygame.event.get():
@ -65,44 +298,85 @@ 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 = not self.paused self.paused = True
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
# Mouse input for breaking/placing if self.hotbar:
# ------------------------------- 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
self.world.break_block( # Get current held item
pygame.mouse.get_pos(), items_list = list(self.inventory.slots.items())
self.camera, current_item = None
self.inventory if self.hotbar.selected_slot < len(items_list):
) current_item_id, _ = items_list[self.hotbar.selected_slot]
current_item = get_item(current_item_id)
if event.button == 3: # Right click # Check if holding a weapon
self.world.place_block( is_holding_weapon = (current_item is not None and
pygame.mouse.get_pos(), current_item.id in WEAPON_STATS)
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)
@ -117,6 +391,10 @@ 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()
@ -137,6 +415,52 @@ 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 100644
View File

@ -0,0 +1,127 @@
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 import pygame
from settings import * from settings import *
from items import ITEMS, get_item from items import get_item
class Inventory: class Inventory:
@ -25,6 +25,13 @@ 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)
@ -46,14 +53,13 @@ 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),
@ -78,13 +84,14 @@ class Inventory:
) )
pygame.draw.rect(screen, item.color, icon_rect) pygame.draw.rect(screen, item.color, icon_rect)
# Draw amount # Draw amount if stack > 1
amount_text = font.render(str(amount), True, (255, 255, 255)) if amount > 1:
screen.blit(amount_text, (cell_rect.x + 2, cell_rect.y + 2)) amount_text = font.render(str(amount), True, (255, 255, 255))
screen.blit(amount_text, (cell_rect.x + 2, cell_rect.y + 2))
# Tooltip on hover # Tooltip when hovering
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))
index += 1 index += 1

View File

@ -1,6 +1,5 @@
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
@ -61,8 +60,34 @@ 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 ITEMS.get(item_id, None) """Return the Item object for a given item_id, or None if it doesn't exist."""
return ITEMS.get(item_id)

675
menu_system.py 100644
View File

@ -0,0 +1,675 @@
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()

276
player.py
View File

@ -3,109 +3,269 @@ from settings import *
class Player: class Player:
def __init__(self, x, y): def __init__(self, x, y, weapon_manager=None):
# 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
# Stats # 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
# =========================
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):
self.handle_input() dt = min(dt, 0.05)
self.apply_physics(dt)
self.handle_collisions(world) 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
# ========================================================== # ==========================================================
# INPUT # INPUT
# ========================================================== # ==========================================================
def handle_input(self): def handle_input(self, dt):
keys = pygame.key.get_pressed() keys = pygame.key.get_pressed()
self.acceleration.x = 0 if self.knockback_timer > 0:
return
# Horizontal movement move = 0
if keys[pygame.K_a]: if keys[pygame.K_a]:
self.acceleration.x = -PLAYER_ACCELERATION move -= 1
self.facing_right = False self.facing_right = False
if keys[pygame.K_d]: if keys[pygame.K_d]:
self.acceleration.x = PLAYER_ACCELERATION move += 1
self.facing_right = True self.facing_right = True
# Apply friction accel = self.move_accel if self.on_ground else self.air_accel * self.air_control
self.acceleration.x += self.velocity.x * PLAYER_FRICTION
# Jump if move != 0:
if keys[pygame.K_SPACE] and self.on_ground: self.velocity.x += move * accel * dt
self.velocity.y = JUMP_FORCE 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
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_collisions(self, world): def handle_horizontal_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"]):
# Get nearby tiles once if self.velocity.x > 0:
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 self.rect.right = tile["rect"].left
elif self.velocity.x < 0: # Moving left elif self.velocity.x < 0:
self.rect.left = tile["rect"].right self.rect.left = tile["rect"].right
self.pos.x = self.rect.x
self.velocity.x = 0 self.velocity.x = 0
# Vertical collisions def handle_vertical_collisions(self, world):
for tile in nearby_tiles: self.on_ground = False
if tile["rect"].colliderect(self.rect) and tile["solid"]:
if self.velocity.y > 0: # Falling 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 self.rect.bottom = tile["rect"].top
self.on_ground = True self.on_ground = True
elif self.velocity.y < 0: # Jumping up elif self.velocity.y < 0:
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)
pygame.draw.rect(screen, (255, 50, 50), draw_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: if SHOW_COLLIDERS:
pygame.draw.rect(screen, (0, 255, 0), draw_rect, 2) 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:
pygame.draw.rect(screen, (0, 255, 0), draw_rect, 2)

176
save_system.py 100644
View File

@ -0,0 +1,176 @@
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,6 +6,7 @@ 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"
@ -41,6 +42,11 @@ 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},
@ -76,6 +82,12 @@ 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
@ -88,10 +100,10 @@ PLAYER_SPEED = 250
PLAYER_ACCELERATION = 2000 PLAYER_ACCELERATION = 2000
PLAYER_FRICTION = -0.15 PLAYER_FRICTION = -0.15
GRAVITY = 1500 GRAVITY = 2200
MAX_FALL_SPEED = 1000 MAX_FALL_SPEED = 1000
JUMP_FORCE = -500 JUMP_FORCE = -700
DOUBLE_JUMP = False DOUBLE_JUMP = False
MAX_HEALTH = 100 MAX_HEALTH = 100
@ -116,6 +128,43 @@ 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
@ -145,6 +194,8 @@ 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
# ========================================== # ==========================================
@ -163,6 +214,16 @@ 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
# ========================================== # ==========================================

View File

@ -36,3 +36,5 @@ 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 100644
View File

@ -0,0 +1,100 @@
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,9 +1,63 @@
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
@ -18,31 +72,27 @@ class World:
self.generate_world() self.generate_world()
# ========================================================== # ==========================================================
# WORLD GENERATION # WORLD GENERATION - FLAT WITH TREES
# ========================================================== # ==========================================================
def generate_world(self): def generate_world(self):
# First generate terrain """Generate flat world with trees"""
# 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
# Then add trees + ores # Add trees (more frequently)
self.generate_trees() self.generate_trees()
# Add ores
self.generate_ores() self.generate_ores()
# ========================================================== # ==========================================================
@ -63,7 +113,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 # FIXED: no more KeyError color = get_tile(tile_id).color
world_rect = pygame.Rect( world_rect = pygame.Rect(
x * TILE_SIZE, x * TILE_SIZE,
@ -115,11 +165,22 @@ class World:
# ========================================================== # ==========================================================
# BLOCK BREAKING # BLOCK BREAKING
# ========================================================== # ==========================================================
def break_block(self, mouse_pos, camera, inventory): def break_block(self, mouse_pos, camera, inventory, player_rect):
"""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)
@ -127,20 +188,39 @@ 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, block_type=DIRT): def place_block(self, mouse_pos, camera, player_rect, 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
@ -149,7 +229,7 @@ class World:
return 0 <= x < self.width and 0 <= y < self.height return 0 <= x < self.width and 0 <= y < self.height
# ========================================================== # ==========================================================
# NEW: Get surface height for player spawn # 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):
@ -158,29 +238,29 @@ class World:
return self.height - 1 return self.height - 1
# ========================================================== # ==========================================================
# TREE GENERATION # TREE GENERATION - MORE TREES
# ========================================================== # ==========================================================
def generate_trees(self): def generate_trees(self):
for x in range(0, self.width, 6): # Generate trees more frequently (every 4 tiles)
if random.random() < 0.25: # 25% chance for x in range(0, self.width, 4):
# find surface if random.random() < 0.6: # 60% chance for trees
for y in range(self.height): surface_y = SURFACE_LEVEL
if self.grid[y][x] == GRASS: if 0 <= x < self.width:
self.spawn_tree(x, y) self.spawn_tree(x, surface_y)
break
def spawn_tree(self, x, surface_y): def spawn_tree(self, x, surface_y):
# trunk height # trunk height
height = random.randint(4, 7) height = random.randint(5, 8)
for i in range(height): for i in range(height):
self.grid[surface_y - 1 - i][x] = WOOD if surface_y - 1 - i >= 0:
self.grid[surface_y - 1 - i][x] = WOOD
# leaves # leaves - larger canopy
leaf_start = surface_y - height - 1 leaf_start = surface_y - height - 1
for y in range(leaf_start, leaf_start - 4, -1): for y in range(leaf_start, leaf_start - 5, -1):
for lx in range(x - 2, x + 3): for lx in range(x - 3, x + 4):
if 0 <= lx < self.width and 0 <= y < self.height: if 0 <= lx < self.width and 0 <= y < self.height:
if random.random() > 0.25: if random.random() > 0.2: # 80% leaf density
self.grid[y][lx] = LEAVES self.grid[y][lx] = LEAVES
# ========================================================== # ==========================================================
@ -201,3 +281,11 @@ 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