from common import os, json, uuid, deepcopy, dataclass, field 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) 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 #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 cell in 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"]) self.cells[line].append(final_cell) def move_obj(self, type, start, goal): """Перемещает объект типа 'terrain_obj', 'item_obj' или 'creature_obj' из клетки start=(row, col) в goal=(row, col)""" 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) if obj is None: return False setattr(source_cell, type, None) setattr(dest_cell, type, obj) 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): for j in range(len(self.cells)): for cell in self.cells[j]: if cell.creature_obj and cell.creature_obj.current_target: cell.creature_obj.update(time_delta, self.cell_size, self) # self! 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) @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) #print(easy_map.find_way((1, 1), (5, 5))) #print(easy_map.find_way((0, 0), (1, 1))) #print(easy_map.find_way((0, 0), (0, 1))) #print(easy_map.find_way((0, 0), (0, 0))) #sp = eb_objects.Sprite(self.sprites, "elf_watching") #gr = pygame.image.load(file_path).convert_alpha() 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 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") # 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_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, active_cell, 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"Current FPS: {current_fps:.2f}") pygame.quit()