'local'
commit
cddd6289e6
|
|
@ -0,0 +1,5 @@
|
||||||
|
Copyright (C) 2026 by fkalnins fkalnins.e@rkg.lv
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
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.
|
|
@ -0,0 +1,21 @@
|
||||||
|
import pygame
|
||||||
|
from settings import *
|
||||||
|
|
||||||
|
class Camera:
|
||||||
|
def __init__(self, target):
|
||||||
|
self.target = target
|
||||||
|
self.offset = pygame.Vector2(0, 0)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
# Smooth camera movement
|
||||||
|
target_x = self.target.rect.centerx - SCREEN_WIDTH // 2
|
||||||
|
target_y = self.target.rect.centery - SCREEN_HEIGHT // 2
|
||||||
|
|
||||||
|
self.offset.x += (target_x - self.offset.x) * CAMERA_SMOOTHING
|
||||||
|
self.offset.y += (target_y - self.offset.y) * CAMERA_SMOOTHING
|
||||||
|
|
||||||
|
def apply(self, rect):
|
||||||
|
return rect.move(-self.offset.x, -self.offset.y)
|
||||||
|
|
||||||
|
def screen_to_world(self, pos):
|
||||||
|
return (pos[0] + self.offset.x, pos[1] + self.offset.y)
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import pygame
|
||||||
|
from settings import *
|
||||||
|
|
||||||
|
class Enemy:
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self.rect = pygame.Rect(x, y, 28, 48)
|
||||||
|
self.velocity = pygame.Vector2(0, 0)
|
||||||
|
|
||||||
|
def update(self, dt, world, player):
|
||||||
|
# Basic AI: follow player
|
||||||
|
if player.rect.x < self.rect.x:
|
||||||
|
self.rect.x -= ENEMY_SPEED * dt
|
||||||
|
else:
|
||||||
|
self.rect.x += ENEMY_SPEED * dt
|
||||||
|
|
||||||
|
def draw(self, screen, camera):
|
||||||
|
pygame.draw.rect(screen, (0, 0, 255), camera.apply(self.rect))
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import pygame
|
||||||
|
import random
|
||||||
|
from settings import *
|
||||||
|
from enemy import Enemy
|
||||||
|
|
||||||
|
|
||||||
|
class EnemyManager:
|
||||||
|
def __init__(self, world, player):
|
||||||
|
self.world = world
|
||||||
|
self.player = player
|
||||||
|
self.enemies = []
|
||||||
|
|
||||||
|
# spawn a few enemies to start
|
||||||
|
for _ in range(5):
|
||||||
|
self.spawn_enemy()
|
||||||
|
|
||||||
|
def spawn_enemy(self):
|
||||||
|
x = random.randint(0, self.world.width * TILE_SIZE)
|
||||||
|
y = (SURFACE_LEVEL - 5) * TILE_SIZE
|
||||||
|
self.enemies.append(Enemy(x, y))
|
||||||
|
|
||||||
|
def update(self, dt):
|
||||||
|
for enemy in self.enemies:
|
||||||
|
enemy.update(dt, self.world, self.player)
|
||||||
|
|
||||||
|
def draw(self, screen, camera):
|
||||||
|
for enemy in self.enemies:
|
||||||
|
enemy.draw(screen, camera)
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
import pygame
|
||||||
|
from settings import *
|
||||||
|
|
||||||
|
from world import World
|
||||||
|
from player import Player
|
||||||
|
from camera import Camera
|
||||||
|
from inventory import Inventory
|
||||||
|
from enemy_manager import EnemyManager
|
||||||
|
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
def __init__(self):
|
||||||
|
pygame.init() # <-- IMPORTANT
|
||||||
|
|
||||||
|
self.show_inventory = False
|
||||||
|
|
||||||
|
# ----------------------------------
|
||||||
|
# Display Setup
|
||||||
|
# ----------------------------------
|
||||||
|
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
|
||||||
|
|
||||||
|
# ----------------------------------
|
||||||
|
# Core Systems
|
||||||
|
# ----------------------------------
|
||||||
|
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)
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# MAIN LOOP
|
||||||
|
# ==========================================================
|
||||||
|
def run(self):
|
||||||
|
while self.running:
|
||||||
|
dt = self.clock.tick(FPS) / 1000
|
||||||
|
|
||||||
|
self.handle_events()
|
||||||
|
|
||||||
|
if not self.paused:
|
||||||
|
self.update(dt)
|
||||||
|
|
||||||
|
self.draw()
|
||||||
|
|
||||||
|
pygame.quit()
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# EVENTS
|
||||||
|
# ==========================================================
|
||||||
|
def handle_events(self):
|
||||||
|
for event in pygame.event.get():
|
||||||
|
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_ESCAPE:
|
||||||
|
self.paused = not self.paused
|
||||||
|
|
||||||
|
# Toggle inventory
|
||||||
|
if event.key == pygame.K_i:
|
||||||
|
self.show_inventory = not self.show_inventory
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Mouse input for breaking/placing
|
||||||
|
# -------------------------------
|
||||||
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
|
if event.button == 1: # Left click
|
||||||
|
self.world.break_block(
|
||||||
|
pygame.mouse.get_pos(),
|
||||||
|
self.camera,
|
||||||
|
self.inventory
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# DRAW
|
||||||
|
# ==========================================================
|
||||||
|
def draw(self):
|
||||||
|
self.screen.fill(BACKGROUND_COLOR)
|
||||||
|
|
||||||
|
# Draw world relative to camera
|
||||||
|
self.world.draw(self.screen, self.camera)
|
||||||
|
|
||||||
|
# Draw entities
|
||||||
|
self.enemy_manager.draw(self.screen, self.camera)
|
||||||
|
self.player.draw(self.screen, self.camera)
|
||||||
|
|
||||||
|
# UI
|
||||||
|
if self.show_inventory:
|
||||||
|
self.inventory.draw(self.screen)
|
||||||
|
|
||||||
|
self.draw_ui()
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# UI / DEBUG
|
||||||
|
# ==========================================================
|
||||||
|
def draw_ui(self):
|
||||||
|
if SHOW_FPS:
|
||||||
|
fps = self.clock.get_fps()
|
||||||
|
fps_text = self.font.render(f"FPS: {int(fps)}", True, (255, 255, 255))
|
||||||
|
self.screen.blit(fps_text, (10, 10))
|
||||||
|
|
||||||
|
if DEBUG_MODE:
|
||||||
|
debug_text = self.font.render(
|
||||||
|
f"Player: ({int(self.player.rect.x)}, {int(self.player.rect.y)})",
|
||||||
|
True,
|
||||||
|
(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))
|
||||||
|
self.screen.blit(pause_text, rect)
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import pygame
|
||||||
|
from settings import *
|
||||||
|
from items import ITEMS, get_item
|
||||||
|
|
||||||
|
|
||||||
|
class Inventory:
|
||||||
|
def __init__(self):
|
||||||
|
self.slots = {}
|
||||||
|
|
||||||
|
# UI settings
|
||||||
|
self.width = 420
|
||||||
|
self.height = 300
|
||||||
|
self.x = (SCREEN_WIDTH - self.width) // 2
|
||||||
|
self.y = (SCREEN_HEIGHT - self.height) // 2
|
||||||
|
|
||||||
|
self.grid_cols = 8
|
||||||
|
self.grid_rows = 4
|
||||||
|
self.cell_size = 40
|
||||||
|
self.padding = 10
|
||||||
|
self.cell_padding = 4
|
||||||
|
|
||||||
|
def add_item(self, item_id, amount=1):
|
||||||
|
if item_id in self.slots:
|
||||||
|
self.slots[item_id] += amount
|
||||||
|
else:
|
||||||
|
self.slots[item_id] = amount
|
||||||
|
|
||||||
|
def draw(self, screen):
|
||||||
|
font = pygame.font.SysFont("consolas", 18)
|
||||||
|
title_font = pygame.font.SysFont("consolas", 24)
|
||||||
|
|
||||||
|
# Panel background
|
||||||
|
panel_rect = pygame.Rect(self.x, self.y, self.width, self.height)
|
||||||
|
pygame.draw.rect(screen, (30, 30, 30), panel_rect)
|
||||||
|
pygame.draw.rect(screen, (255, 255, 255), panel_rect, 2)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = title_font.render("Inventory", True, (255, 255, 255))
|
||||||
|
screen.blit(title, (self.x + 15, self.y + 10))
|
||||||
|
|
||||||
|
# Close hint
|
||||||
|
hint = font.render("Press I to close", True, (200, 200, 200))
|
||||||
|
screen.blit(hint, (self.x + 15, self.y + 40))
|
||||||
|
|
||||||
|
# Draw grid
|
||||||
|
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),
|
||||||
|
self.cell_size,
|
||||||
|
self.cell_size
|
||||||
|
)
|
||||||
|
|
||||||
|
pygame.draw.rect(screen, (60, 60, 60), cell_rect)
|
||||||
|
pygame.draw.rect(screen, (120, 120, 120), cell_rect, 2)
|
||||||
|
|
||||||
|
if index < len(items_list):
|
||||||
|
item_id, amount = items_list[index]
|
||||||
|
item = get_item(item_id)
|
||||||
|
|
||||||
|
if item:
|
||||||
|
# Draw item icon
|
||||||
|
icon_rect = pygame.Rect(
|
||||||
|
cell_rect.x + 6,
|
||||||
|
cell_rect.y + 6,
|
||||||
|
28,
|
||||||
|
28
|
||||||
|
)
|
||||||
|
pygame.draw.rect(screen, item.color, icon_rect)
|
||||||
|
|
||||||
|
# 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 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))
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
from settings import *
|
||||||
|
|
||||||
|
|
||||||
|
class Item:
|
||||||
|
def __init__(self, item_id, name, color, stack_limit=999, placeable=False, place_tile=None):
|
||||||
|
self.id = item_id
|
||||||
|
self.name = name
|
||||||
|
self.color = color
|
||||||
|
self.stack_limit = stack_limit
|
||||||
|
self.placeable = placeable
|
||||||
|
self.place_tile = place_tile
|
||||||
|
|
||||||
|
|
||||||
|
ITEMS = {
|
||||||
|
ITEM_WOOD: Item(
|
||||||
|
ITEM_WOOD,
|
||||||
|
"Wood",
|
||||||
|
(160, 82, 45),
|
||||||
|
stack_limit=999,
|
||||||
|
placeable=True,
|
||||||
|
place_tile=WOOD
|
||||||
|
),
|
||||||
|
|
||||||
|
ITEM_STONE: Item(
|
||||||
|
ITEM_STONE,
|
||||||
|
"Stone",
|
||||||
|
(100, 100, 100),
|
||||||
|
stack_limit=999,
|
||||||
|
placeable=True,
|
||||||
|
place_tile=STONE
|
||||||
|
),
|
||||||
|
|
||||||
|
ITEM_COAL: Item(
|
||||||
|
ITEM_COAL,
|
||||||
|
"Coal",
|
||||||
|
(40, 40, 40),
|
||||||
|
stack_limit=999,
|
||||||
|
placeable=False
|
||||||
|
),
|
||||||
|
|
||||||
|
ITEM_COPPER: Item(
|
||||||
|
ITEM_COPPER,
|
||||||
|
"Copper Ore",
|
||||||
|
(210, 120, 60),
|
||||||
|
stack_limit=999,
|
||||||
|
placeable=False
|
||||||
|
),
|
||||||
|
|
||||||
|
ITEM_IRON: Item(
|
||||||
|
ITEM_IRON,
|
||||||
|
"Iron Ore",
|
||||||
|
(180, 180, 180),
|
||||||
|
stack_limit=999,
|
||||||
|
placeable=False
|
||||||
|
),
|
||||||
|
|
||||||
|
ITEM_GOLD: Item(
|
||||||
|
ITEM_GOLD,
|
||||||
|
"Gold Ore",
|
||||||
|
(255, 215, 0),
|
||||||
|
stack_limit=999,
|
||||||
|
placeable=False
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_item(item_id):
|
||||||
|
return ITEMS.get(item_id, None)
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import traceback
|
||||||
|
from game import Game
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
print("Starting game...")
|
||||||
|
game = Game()
|
||||||
|
print("Game initialized.")
|
||||||
|
game.run()
|
||||||
|
except Exception:
|
||||||
|
print("An error occurred:")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import pygame
|
||||||
|
from settings import *
|
||||||
|
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
def __init__(self, x, y):
|
||||||
|
# Position & Size
|
||||||
|
self.rect = pygame.Rect(x, y, PLAYER_WIDTH, PLAYER_HEIGHT)
|
||||||
|
|
||||||
|
# Movement
|
||||||
|
self.velocity = pygame.Vector2(0, 0)
|
||||||
|
self.acceleration = pygame.Vector2(0, 0)
|
||||||
|
|
||||||
|
# State
|
||||||
|
self.on_ground = False
|
||||||
|
self.facing_right = True
|
||||||
|
|
||||||
|
# Stats
|
||||||
|
self.health = MAX_HEALTH
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# UPDATE
|
||||||
|
# ==========================================================
|
||||||
|
def update(self, dt, world):
|
||||||
|
self.handle_input()
|
||||||
|
self.apply_physics(dt)
|
||||||
|
self.handle_collisions(world)
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# INPUT
|
||||||
|
# ==========================================================
|
||||||
|
def handle_input(self):
|
||||||
|
keys = pygame.key.get_pressed()
|
||||||
|
|
||||||
|
self.acceleration.x = 0
|
||||||
|
|
||||||
|
# Horizontal movement
|
||||||
|
if keys[pygame.K_a]:
|
||||||
|
self.acceleration.x = -PLAYER_ACCELERATION
|
||||||
|
self.facing_right = False
|
||||||
|
|
||||||
|
if keys[pygame.K_d]:
|
||||||
|
self.acceleration.x = PLAYER_ACCELERATION
|
||||||
|
self.facing_right = True
|
||||||
|
|
||||||
|
# Apply friction
|
||||||
|
self.acceleration.x += self.velocity.x * PLAYER_FRICTION
|
||||||
|
|
||||||
|
# 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_collisions(self, world):
|
||||||
|
self.on_ground = False
|
||||||
|
|
||||||
|
# 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: # Jumping up
|
||||||
|
self.rect.top = tile["rect"].bottom
|
||||||
|
self.velocity.y = 0
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# DRAW
|
||||||
|
# ==========================================================
|
||||||
|
def draw(self, screen, camera):
|
||||||
|
draw_rect = camera.apply(self.rect)
|
||||||
|
|
||||||
|
pygame.draw.rect(screen, (255, 50, 50), draw_rect)
|
||||||
|
|
||||||
|
if SHOW_COLLIDERS:
|
||||||
|
pygame.draw.rect(screen, (0, 255, 0), draw_rect, 2)
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
# ==========================================
|
||||||
|
# WINDOW / DISPLAY SETTINGS
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
SCREEN_WIDTH = 1280
|
||||||
|
SCREEN_HEIGHT = 720
|
||||||
|
FPS = 60
|
||||||
|
VSYNC = True
|
||||||
|
|
||||||
|
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
|
||||||
|
# Tiles
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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},
|
||||||
|
|
||||||
|
# ORES
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# PLAYER SETTINGS
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
PLAYER_WIDTH = 28
|
||||||
|
PLAYER_HEIGHT = 48
|
||||||
|
|
||||||
|
PLAYER_SPEED = 250
|
||||||
|
PLAYER_ACCELERATION = 2000
|
||||||
|
PLAYER_FRICTION = -0.15
|
||||||
|
|
||||||
|
GRAVITY = 1500
|
||||||
|
MAX_FALL_SPEED = 1000
|
||||||
|
|
||||||
|
JUMP_FORCE = -500
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# BLOCK BREAKING / PLACING
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
BREAK_RANGE = 5 # tiles
|
||||||
|
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_DAMAGE = 10
|
||||||
|
ENEMY_HEALTH = 50
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# PHYSICS SETTINGS
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
TERMINAL_VELOCITY = 1200
|
||||||
|
COLLISION_STEPS = 4
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# LIGHTING (for future use)
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
ENABLE_LIGHTING = False
|
||||||
|
LIGHT_RADIUS = 5
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# DEBUG SETTINGS
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
DEBUG_MODE = True
|
||||||
|
SHOW_FPS = True
|
||||||
|
SHOW_COLLIDERS = False
|
||||||
|
SHOW_CHUNK_BORDERS = False
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
from settings import *
|
||||||
|
|
||||||
|
class Tile:
|
||||||
|
def __init__(self, tile_id, name, collidable, color, hardness=1.0, drop=None):
|
||||||
|
self.id = tile_id
|
||||||
|
self.name = name
|
||||||
|
self.collidable = collidable
|
||||||
|
self.color = color
|
||||||
|
self.hardness = hardness
|
||||||
|
self.drop = drop
|
||||||
|
|
||||||
|
TILE_TYPES = {
|
||||||
|
AIR: Tile(AIR, "Air", False, (0,0,0), hardness=0, drop=None),
|
||||||
|
DIRT: Tile(DIRT, "Dirt", True, (139,69,19), hardness=0.4, drop=ITEM_STONE),
|
||||||
|
GRASS: Tile(GRASS, "Grass", True, (34,177,76), hardness=0.4, drop=ITEM_STONE),
|
||||||
|
STONE: Tile(STONE, "Stone", True, (100,100,100), hardness=0.8, drop=ITEM_STONE),
|
||||||
|
WOOD: Tile(WOOD, "Wood", True, (160,82,45), hardness=0.5, drop=ITEM_WOOD),
|
||||||
|
LEAVES: Tile(LEAVES, "Leaves", False, (34,139,34), hardness=0.2, drop=None),
|
||||||
|
IRON_ORE: Tile(IRON_ORE, "Iron Ore", True, (180,180,180), hardness=1.2, drop=ITEM_IRON),
|
||||||
|
GOLD_ORE: Tile(GOLD_ORE, "Gold Ore", True, (255,215,0), hardness=1.5, drop=ITEM_GOLD),
|
||||||
|
COPPER_ORE: Tile(COPPER_ORE, "Copper Ore", True, (210,120,60), hardness=1.0, drop=ITEM_COPPER),
|
||||||
|
COAL_ORE: Tile(COAL_ORE, "Coal Ore", True, (40,40,40), hardness=0.9, drop=ITEM_COAL),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Compatibility for world.py
|
||||||
|
# ----------------------------
|
||||||
|
TILE_PROPERTIES = {
|
||||||
|
tile_id: {
|
||||||
|
"solid": tile.collidable,
|
||||||
|
"color": tile.color,
|
||||||
|
"drop": tile.drop
|
||||||
|
}
|
||||||
|
for tile_id, tile in TILE_TYPES.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_tile(tile_id):
|
||||||
|
return TILE_TYPES.get(tile_id, TILE_TYPES[AIR])
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
import pygame
|
||||||
|
import random
|
||||||
|
from settings import *
|
||||||
|
from tiles import get_tile
|
||||||
|
|
||||||
|
|
||||||
|
class World:
|
||||||
|
def __init__(self):
|
||||||
|
self.width = WORLD_WIDTH
|
||||||
|
self.height = WORLD_HEIGHT
|
||||||
|
|
||||||
|
# 2D grid: world[y][x]
|
||||||
|
self.grid = [
|
||||||
|
[AIR for _ in range(self.width)]
|
||||||
|
for _ in range(self.height)
|
||||||
|
]
|
||||||
|
|
||||||
|
self.generate_world()
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# WORLD GENERATION
|
||||||
|
# ==========================================================
|
||||||
|
def generate_world(self):
|
||||||
|
# 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_height:
|
||||||
|
self.grid[y][x] = AIR
|
||||||
|
|
||||||
|
elif y == surface_height:
|
||||||
|
self.grid[y][x] = GRASS
|
||||||
|
|
||||||
|
elif y < surface_height + 5:
|
||||||
|
self.grid[y][x] = DIRT
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.grid[y][x] = STONE
|
||||||
|
|
||||||
|
# Then add trees + ores
|
||||||
|
self.generate_trees()
|
||||||
|
self.generate_ores()
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# DRAW
|
||||||
|
# ==========================================================
|
||||||
|
def draw(self, screen, camera):
|
||||||
|
|
||||||
|
# Determine visible tile range
|
||||||
|
start_x = max(0, camera.offset.x // TILE_SIZE)
|
||||||
|
end_x = min(self.width, (camera.offset.x + SCREEN_WIDTH) // TILE_SIZE + 2)
|
||||||
|
|
||||||
|
start_y = max(0, camera.offset.y // TILE_SIZE)
|
||||||
|
end_y = min(self.height, (camera.offset.y + SCREEN_HEIGHT) // TILE_SIZE + 2)
|
||||||
|
|
||||||
|
for y in range(int(start_y), int(end_y)):
|
||||||
|
for x in range(int(start_x), int(end_x)):
|
||||||
|
|
||||||
|
tile_id = self.grid[y][x]
|
||||||
|
|
||||||
|
if tile_id != AIR:
|
||||||
|
color = get_tile(tile_id).color # FIXED: no more KeyError
|
||||||
|
|
||||||
|
world_rect = pygame.Rect(
|
||||||
|
x * TILE_SIZE,
|
||||||
|
y * TILE_SIZE,
|
||||||
|
TILE_SIZE,
|
||||||
|
TILE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
screen_rect = camera.apply(world_rect)
|
||||||
|
|
||||||
|
pygame.draw.rect(screen, color, screen_rect)
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# COLLISION SUPPORT
|
||||||
|
# ==========================================================
|
||||||
|
def get_nearby_tiles(self, rect):
|
||||||
|
tiles = []
|
||||||
|
|
||||||
|
# Determine tile range around player
|
||||||
|
start_x = max(0, rect.left // TILE_SIZE - 1)
|
||||||
|
end_x = min(self.width, rect.right // TILE_SIZE + 2)
|
||||||
|
|
||||||
|
start_y = max(0, rect.top // TILE_SIZE - 1)
|
||||||
|
end_y = min(self.height, rect.bottom // TILE_SIZE + 2)
|
||||||
|
|
||||||
|
for y in range(start_y, end_y):
|
||||||
|
for x in range(start_x, end_x):
|
||||||
|
|
||||||
|
tile_id = self.grid[y][x]
|
||||||
|
|
||||||
|
if tile_id != AIR:
|
||||||
|
tile_rect = pygame.Rect(
|
||||||
|
x * TILE_SIZE,
|
||||||
|
y * TILE_SIZE,
|
||||||
|
TILE_SIZE,
|
||||||
|
TILE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
tiles.append({
|
||||||
|
"rect": tile_rect,
|
||||||
|
"solid": get_tile(tile_id).collidable,
|
||||||
|
"id": tile_id,
|
||||||
|
"x": x,
|
||||||
|
"y": y
|
||||||
|
})
|
||||||
|
|
||||||
|
return tiles
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# BLOCK BREAKING
|
||||||
|
# ==========================================================
|
||||||
|
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)
|
||||||
|
|
||||||
|
if self.in_bounds(tile_x, tile_y):
|
||||||
|
tile_id = self.grid[tile_y][tile_x]
|
||||||
|
tile = get_tile(tile_id)
|
||||||
|
|
||||||
|
if tile_id != AIR:
|
||||||
|
if tile.drop:
|
||||||
|
inventory.add_item(tile.drop, 1)
|
||||||
|
self.grid[tile_y][tile_x] = AIR
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# BLOCK PLACING
|
||||||
|
# ==========================================================
|
||||||
|
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)
|
||||||
|
|
||||||
|
if self.in_bounds(tile_x, tile_y):
|
||||||
|
if self.grid[tile_y][tile_x] == AIR:
|
||||||
|
self.grid[tile_y][tile_x] = block_type
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# UTIL
|
||||||
|
# ==========================================================
|
||||||
|
def in_bounds(self, x, y):
|
||||||
|
return 0 <= x < self.width and 0 <= y < self.height
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# NEW: Get surface height for player spawn
|
||||||
|
# ==========================================================
|
||||||
|
def get_surface_height(self, x):
|
||||||
|
for y in range(self.height):
|
||||||
|
if self.grid[y][x] != AIR:
|
||||||
|
return y
|
||||||
|
return self.height - 1
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# TREE GENERATION
|
||||||
|
# ==========================================================
|
||||||
|
def generate_trees(self):
|
||||||
|
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(4, 7)
|
||||||
|
for i in range(height):
|
||||||
|
self.grid[surface_y - 1 - i][x] = WOOD
|
||||||
|
|
||||||
|
# leaves
|
||||||
|
leaf_start = surface_y - height - 1
|
||||||
|
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.25:
|
||||||
|
self.grid[y][lx] = LEAVES
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# ORE GENERATION
|
||||||
|
# ==========================================================
|
||||||
|
def generate_ores(self):
|
||||||
|
for _ in range(250):
|
||||||
|
x = random.randint(0, self.width - 1)
|
||||||
|
y = random.randint(SURFACE_LEVEL + 5, self.height - 1)
|
||||||
|
|
||||||
|
if self.grid[y][x] == STONE:
|
||||||
|
r = random.random()
|
||||||
|
if r < 0.5:
|
||||||
|
self.grid[y][x] = COAL_ORE
|
||||||
|
elif r < 0.75:
|
||||||
|
self.grid[y][x] = COPPER_ORE
|
||||||
|
elif r < 0.9:
|
||||||
|
self.grid[y][x] = IRON_ORE
|
||||||
|
else:
|
||||||
|
self.grid[y][x] = GOLD_ORE
|
||||||
Loading…
Reference in New Issue