Files
Elvenbane/eb_engine.py

565 lines
23 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from common import os, json, uuid, deepcopy, random
from common import dataclass, field, partial
from common import pygame, pygame_gui
import eb_objects
import eb_terrain_objects
import eb_creature_objects
#from pympler import muppy, summary
import gc, psutil, os
cell_classes = {"grass_small": eb_terrain_objects.Ground,
"sword_default": eb_objects.Item, "elf_watching": eb_creature_objects.Unit,
"rock_small": eb_terrain_objects.Rock}
main_dir = os.path.dirname(os.path.abspath(__file__))
sprites_dir = os.path.join(main_dir, "res", "sprites")
#class Render
#class ObjectManager
#class MapManager
#class Event
#class EventManager
#class Control
@dataclass
class Cell:
terrain_obj: None = None
item_obj: None = None
creature_obj: None = None
is_target: bool = False
render_offset: tuple = (0.0, 0.0)
@dataclass
class Map:
name: str
sprites: dict
sprites_refresh: int = 60
cells: dict = field(default_factory = dict)
walkable_matrix: list = field(default_factory = list)
rocks_matrix: list = field(default_factory = list)
color: str = "gray57"
target_color: str = "gold"
cell_size: int = 150
bord: int = 3
scale: float = 1
cam_x: int = 0
cam_y: int = 0
cell_dist: int = 1
#effects[]
#action_time_multiplier
def __post_init__(self):
self.cells = {}
with open(self.name, 'r') as file:
buff = json.load(file)
for line in range(len(buff)):
self.cells[line] = []
for col, cell in enumerate(buff[str(line)]):
final_cell = Cell(cell_classes[cell["terrain_obj"]["sprite_name"]](**cell["terrain_obj"]))
if cell["item_obj"]:
final_cell.item_obj = cell_classes[cell["item_obj"]["sprite_name"]](**cell["item_obj"])
if cell["creature_obj"]:
final_cell.creature_obj = cell_classes[cell["creature_obj"]["sprite_name"]](**cell["creature_obj"])
final_cell.creature_obj.grid_pos = (line, col)
self.cells[line].append(final_cell)
self.compute_walkable_rocks()
for j in range(len(self.cells)):
for cell in self.cells[j]:
if cell.creature_obj:
cell.creature_obj.walkable_matrix = self.walkable_matrix
cell.creature_obj.rocks_matrix = self.rocks_matrix
def move_obj(self, type, start, goal):
"""Перемещает объект типа 'terrain_obj', 'item_obj' или 'creature_obj'
из клетки start=(row, col) в goal=(row, col)"""
if goal is None:
return False
s_y, s_x = start
d_y, d_x = goal
# Проверка границ
if (s_y >= len(self.cells) or s_x >= len(self.cells[s_y]) or
d_y >= len(self.cells) or d_x >= len(self.cells[d_y])):
return False
source_cell = self.cells[s_y][s_x]
dest_cell = self.cells[d_y][d_x]
obj = getattr(source_cell, type)
check = getattr(dest_cell, type)
if obj is None or check is not None:
return False
setattr(source_cell, type, None)
setattr(dest_cell, type, obj)
#obj.grid_pos = goal
return True
def get_cell_at_mouse(self, mouse_pos):
"""Возвращает индексы клетки (row, col) по позиции мыши или None если вне карты"""
mx, my = mouse_pos
# Переводим экранные координаты в координаты карты с учетом камеры и масштаба
map_x = (mx - self.cam_x * self.scale) / self.scale / self.cell_size
map_y = (my - self.cam_y * self.scale) / self.scale / self.cell_size
# Получаем индексы ближайшей клетки
col = int(map_x)
row = int(map_y)
# Проверяем границы карты
if (row < 0 or row >= len(self.cells) or
col < 0 or col >= len(self.cells[row])):
return None
return (row, col)
def update_map(self, time_delta):
self.compute_walkable_rocks()
for j in range(len(self.cells)):
for cell in self.cells[j]:
if cell.creature_obj:
cell.creature_obj.update(time_delta, self.cell_size, self)
def compute_walkable_rocks(self):
"""Вычисляет матрицы walkable и rocks_only БЕЗ учета стартовой позиции"""
rows = len(self.cells)
if rows == 0:
return None, None
cols = len(self.cells[0])
# ★ ИНИЦИАЛИЗАЦИЯ ★
self.walkable_matrix = [[True] * cols for _ in range(rows)]
self.rocks_matrix = [[False] * cols for _ in range(rows)]
# ★ ПОЛНЫЙ ПРОХОД ПО КАРТЕ
for r in range(rows):
for c in range(cols):
cell = self.cells[r][c]
# Запрещаем ВСЕ существа (включая стартовое!)
if cell.creature_obj or \
(cell.terrain_obj and cell.terrain_obj.sprite_name == "rock_small"):
self.walkable_matrix[r][c] = False
# Отмечаем маленькие камни
if cell.terrain_obj and cell.terrain_obj.sprite_name == "rock_small":
self.rocks_matrix[r][c] = True
# def draw_map(self, screen, current_frame, grid=True):
# terrain_list = []
# creature_list = []
#
# # ★ 1 ПАСС: собираем terrain и creatures ★
# for j in range(len(self.cells)):
# for i, cell in enumerate(self.cells[j]):
# base_x = i * self.cell_size + self.cam_x
# base_y = j * self.cell_size + self.cam_y
#
# # Terrain данные
# terrain_dd = {
# "x": int(base_x * self.scale), "y": int(base_y * self.scale),
# "w": int(self.cell_size * self.scale - self.cell_dist),
# "h": int(self.cell_size * self.scale - self.cell_dist),
# "spr_up": current_frame % self.sprites_refresh,
# "sprites": self.sprites, "scale": self.scale, "screen": screen
# }
# terrain_list.append((cell.terrain_obj, terrain_dd))
#
# # Creature данные (если есть)
# if cell.creature_obj:
# offset_x, offset_y = cell.creature_obj.render_offset
# creature_dd = terrain_dd.copy()
# creature_dd["x"] = int((base_x + offset_x) * self.scale)
# creature_dd["y"] = int((base_y + offset_y) * self.scale)
# creature_list.append((cell.creature_obj, creature_dd))
#
# # ★ 2 ПАСС: рисуем terrain ★
# for obj, dd in terrain_list:
# obj.draw(dd)
#
# # ★ 3 ПАСС: рисуем ВСЕХ creatures ПОСЛЕ terrain ★
# for obj, dd in creature_list:
# obj.draw(dd)
#
# # ★ 4 ПАСС: grid поверх всего ★
# for j in range(len(self.cells)):
# for i, cell in enumerate(self.cells[j]):
# base_x = i * self.cell_size + self.cam_x
# base_y = j * self.cell_size + self.cam_y
# grid_rect = pygame.Rect(
# int(base_x * self.scale), int(base_y * self.scale),
# int(self.cell_size * self.scale - self.cell_dist),
# int(self.cell_size * self.scale - self.cell_dist)
# )
# color = self.target_color if cell.is_target else self.color
# pygame.draw.rect(screen, color, grid_rect, self.bord)
def draw_map(self, screen, current_frame, grid=True):
screen_rect = screen.get_rect()
terrain_list = []
creature_list = []
# Вычисляем видимую область в координатах карты (аналогично get_cell_at_mouse)
left_map = (self.cam_x * self.scale) / self.scale / self.cell_size
top_map = (self.cam_y * self.scale) / self.scale / self.cell_size
right_map = ((self.cam_x * self.scale + screen_rect.width) / self.scale / self.cell_size)
bottom_map = ((self.cam_y * self.scale + screen_rect.height) / self.scale / self.cell_size)
min_row = max(0, int(top_map - 1)) # -1 для буфера
max_row = min(len(self.cells), int(bottom_map + 2)) # +2 для буфера
min_col = max(0, int(left_map - 1))
max_col = min(len(self.cells[0]) if self.cells and self.cells[0] else 0, int(right_map + 2))
# ★ 1 ПАСС: собираем только видимые terrain и creatures ★
for j in range(min_row, max_row):
if j not in self.cells: continue
row_cells = self.cells[j]
for i in range(min_col, min(max_col, len(row_cells))):
cell = row_cells[i]
base_x = i * self.cell_size + self.cam_x
base_y = j * self.cell_size + self.cam_y
# Terrain данные
terrain_dd = {
"x": int(base_x * self.scale), "y": int(base_y * self.scale),
"w": int(self.cell_size * self.scale - self.cell_dist),
"h": int(self.cell_size * self.scale - self.cell_dist),
"spr_up": current_frame % self.sprites_refresh,
"sprites": self.sprites, "scale": self.scale, "screen": screen
}
terrain_list.append((cell.terrain_obj, terrain_dd))
# Creature данные (если есть)
if cell.creature_obj:
offset_x, offset_y = cell.creature_obj.render_offset
creature_dd = terrain_dd.copy()
creature_dd["x"] = int((base_x + offset_x) * self.scale)
creature_dd["y"] = int((base_y + offset_y) * self.scale)
creature_list.append((cell.creature_obj, creature_dd))
# ★ 2 ПАСС: рисуем terrain ★
for obj, dd in terrain_list:
obj.draw(dd)
# ★ 3 ПАСС: рисуем ВСЕХ creatures ПОСЛЕ terrain ★
for obj, dd in creature_list:
obj.draw(dd)
# ★ 4 ПАСС: grid только для видимых ячеек ★
for j in range(min_row, max_row):
if j not in self.cells: continue
row_cells = self.cells[j]
for i in range(min_col, min(max_col, len(row_cells))):
cell = row_cells[i]
base_x = i * self.cell_size + self.cam_x
base_y = j * self.cell_size + self.cam_y
grid_rect = pygame.Rect(
int(base_x * self.scale), int(base_y * self.scale),
int(self.cell_size * self.scale - self.cell_dist),
int(self.cell_size * self.scale - self.cell_dist) # ИСПРАВЛЕНО
)
color = self.target_color if cell.is_target else self.color
pygame.draw.rect(screen, color, grid_rect, self.bord)
@dataclass
class Engine:
sprites: dict = field(default_factory = dict)
cached_sprites: dict = field(default_factory = dict)
screen: pygame.Surface = ((1, 1))
width: int = 1600
height: int = 800
camera_step: int = 10
scale_step: float = 0.01
spr_scale: float = 1
def __post_init__(self):
self.sprites = {}
pygame.init()
pygame.display.set_caption('Elvenbane')
self.screen = pygame.display.set_mode((self.width, self.height), pygame.HWSURFACE | pygame.DOUBLEBUF)
print("The engine has started. Sprites were successfully loaded.\n")
self.load_sprites()
print("Sprites were successfully loaded.\n")
self.cached_sprites = deepcopy(self.sprites)
print("Sprites were successfully cached.\n")
def load_sprites(self, folder_path = sprites_dir):
self.sprites = {}
files = [f for f in os.listdir(folder_path) if f.lower().endswith('.png')]
#TOTAL SLOR - REWRITE THIS FUNC PLS
groups = {}
for f in files:
name = os.path.splitext(f)[0]
if '_' in name and name.rsplit('_', 1)[0].count('_') >= 1:
prefix = name.rsplit('_', 1)[0]
num = int(name.rsplit('_', 1)[1])
groups.setdefault(prefix, []).append((num, f))
for prefix, items in groups.items():
items.sort()
self.sprites[prefix] = [
pygame.image.load(os.path.join(folder_path, f)).convert_alpha()
for num, f in items
]
def scale_image(self, image):
orig_size = image.get_size()
new_size = (int(orig_size[0] * self.spr_scale), int(orig_size[1] * self.spr_scale))
return pygame.transform.smoothscale(image, new_size)
def scale_sprites(self):
for sp_name, sprite_list in self.sprites.items():
for i, sprite in enumerate(sprite_list):
scaled = self.scale_image(sprite)
self.cached_sprites[sp_name][i] = scaled
def create_console(self, manager):
console_window = pygame_gui.elements.UIWindow(
pygame.Rect(100, 100, 400, 300),
manager=manager,
window_display_title='Console',
resizable=True
)
input_entry = pygame_gui.elements.UITextEntryLine(
relative_rect=pygame.Rect(10, 250, 380, 30),
container=console_window,
manager=manager
)
# ★ UIScrollingContainer ★
scroll_container = pygame_gui.elements.UIScrollingContainer(
relative_rect=pygame.Rect(10, 10, 380, 230),
container=console_window,
manager=manager,
allow_scroll_x=False
)
# ★ UITextBox ВНУТРИ контейнера ★
output_box = pygame_gui.elements.UITextBox(
html_text=">>> hlwrld1\n",
relative_rect=pygame.Rect(0, 0, 380, 1000), # ← Увеличьте высоту!
container=scroll_container,
manager=manager
)
scroll_container.set_scrollable_area_dimensions((380, 2000))
return console_window, input_entry, scroll_container, output_box
def main_loop(self):
easy_map = Map("def_map.json", self.cached_sprites)
background = pygame.Surface((1600, 800))
background.fill("chartreuse4")
manager = pygame_gui.UIManager((1600, 800))
#hello_button = pygame_gui.elements.UIButton(relative_rect=pygame.Rect((350, 275), (100, 50)),
# text='Say Hello',
# manager=manager)
console_window, input_entry, scroll_container, output_box = self.create_console(manager)
output_log = ">>> hlwrld1\n"
console_active = False
clock = pygame.time.Clock()
running = True
unlock = True
current_frame = 0
max_fps = 60
global_counter = 0
global_counter_cap = 100000
active_cell = None
# profiling
process = psutil.Process(os.getpid())
gc.collect()
mem_before = process.memory_info().rss / 1024
NUM_ELVES = 2000
elf_count = 0
space_pressed = False
spawn = False
def spawn_elves():
spawn_positions = [(0,0), (0,99), (99,0), (99,99), (50,50)]
for pos in spawn_positions:
row, col = pos
if (row < len(easy_map.cells) and
col < len(easy_map.cells[row]) and
easy_map.cells[row][col].creature_obj is None):
elf = eb_objects.Creature(id=f"elf_{random.randint(1000,9999)}",
name="Elf", sprite_name="elf_watching",
grid_pos = pos,
walkable_matrix = easy_map.walkable_matrix,
rocks_matrix = easy_map.rocks_matrix)
r_move_short = eb_objects.Action(sprite_name="elf_watching", func=partial(elf.patrol, easy_map.cells, 3), duration=0.01)
r_move_long = eb_objects.Action(sprite_name="elf_watching", func=partial(elf.move_rand, easy_map.cells, 0, 99), duration=0.01)
elf.tasks.append([r_move_short]*5 + [r_move_long])
easy_map.cells[row][col].creature_obj = elf
while running:
time_delta = clock.tick(60)/1000.0
#pygame.event.clear()
if global_counter % 1000 == 0:
gc.collect()
mem_after = process.memory_info().rss / 1024
print(f"Leak: {mem_after - mem_before:.1f} KB per 1000 frames")
if global_counter % 100 == 0 and spawn == True:
spawn_elves()
elf_count += 5
# poll for events
# pygame.QUIT event means the user clicked X to close your window
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE and elf_count < NUM_ELVES and not space_pressed:
#spawn_elf()
#elf_count += 1
if spawn == False:
spawn = True
else: spawn = False
space_pressed = True
if event.type == pygame.KEYUP:
if event.key == pygame.K_SPACE:
space_pressed = False
if event.type == pygame.MOUSEWHEEL:
scroll_y = event.y
if scroll_y > 0:
easy_map.scale += self.scale_step * 5
self.spr_scale += self.scale_step * 5
self.scale_sprites()
elif scroll_y < 0 and easy_map.scale >= self.scale_step:
easy_map.scale -= self.scale_step * 5
self.spr_scale -= self.scale_step * 5
self.scale_sprites()
if event.type == pygame_gui.UI_TEXT_ENTRY_FINISHED and event.ui_element == input_entry:
user_text = input_entry.get_text()
exec(user_text)
if user_text.strip():
output_log += f">>> {user_text}\n"
output_box.set_text(output_log)
input_entry.set_text("")
user_text = ""
console_active = False
console_active = bool(input_entry.get_text().strip())
if event.type == pygame.VIDEORESIZE:
new_size = event.size
self.screen = pygame.display.set_mode(new_size, pygame.RESIZABLE)
manager.clear_and_set_new_size((new_size[0], new_size[1]))
# ★ ПЕРЕСОЗДАНИЕ ★
console_window.kill()
input_entry.kill()
scroll_container.kill()
output_box.kill()
console_window, input_entry, scroll_container, output_box = self.create_console(manager)
manager.process_events(event)
manager.update(time_delta)
# fill the screen with a color to wipe away anything from last frame
#self.screen.fill("chartreuse4")
self.screen.blit(background, (0, 0))
easy_map.update_map(time_delta)
easy_map.draw_map(self.screen, current_frame + 1)
manager.draw_ui(self.screen)
if not console_active:
keys = pygame.key.get_pressed()
if keys[pygame.K_w]:
easy_map.cam_y += self.camera_step
if keys[pygame.K_s]:
easy_map.cam_y -= self.camera_step
if keys[pygame.K_a]:
easy_map.cam_x += self.camera_step
if keys[pygame.K_d]:
easy_map.cam_x -= self.camera_step
if keys[pygame.K_q]:
easy_map.scale += self.scale_step
self.spr_scale += self.scale_step
self.scale_sprites()
if keys[pygame.K_e] and easy_map.scale >= self.scale_step:
easy_map.scale -= self.scale_step
self.spr_scale -= self.scale_step
self.scale_sprites()
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mouse_pos = pygame.mouse.get_pos()
console_rect = console_window.get_abs_rect()
if not console_rect.collidepoint(mouse_pos):
cell_coords = easy_map.get_cell_at_mouse(mouse_pos)
if cell_coords:
# Сбрасываем предыдущую цель
if active_cell is not None:
row, col = active_cell
if row in easy_map.cells and col < len(easy_map.cells[row]):
easy_map.cells[row][col].is_target = False
active_cell = cell_coords # Теперь кортеж (row, col)
row, col = active_cell
easy_map.cells[row][col].is_target = True
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 3:
mouse_pos = pygame.mouse.get_pos()
console_rect = console_window.get_abs_rect()
if (not console_rect.collidepoint(mouse_pos) and
active_cell is not None and
easy_map.cells[active_cell[0]][active_cell[1]].creature_obj is not None):
cell_coords = easy_map.get_cell_at_mouse(mouse_pos)
if cell_coords:
#print(f"Движение: {active_cell} -> {cell_coords}")
easy_map.cells[active_cell[0]][active_cell[1]].creature_obj.move(easy_map.cells, cell_coords)
if keys[pygame.K_ESCAPE]:
running = False
current_frame = (current_frame + 1) % max_fps
if global_counter < global_counter_cap:
global_counter += 1
else:
global_counter = 0
# flip() the display to put your work on screen
pygame.display.update()
if global_counter % 10 == 0:
current_fps = clock.get_fps()
print(f"Elves count: {elf_count} - Current FPS: {current_fps:.2f}")
pygame.quit()