Compare commits

...

10 Commits

24 changed files with 90108 additions and 117 deletions

Binary file not shown.

Binary file not shown.

1
classes.py Normal file
View File

@@ -0,0 +1 @@
from eb_terrain_objects import Rock

221
common.py
View File

@@ -3,13 +3,14 @@ import json
import uuid import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from copy import deepcopy from copy import deepcopy
from collections import defaultdict
from heapq import heappush, heappop
import pygame import pygame
import pygame_gui
from classes import Rock
def scale_image(image, n):
orig_size = image.get_size()
new_size = (int(orig_size[0] * n), int(orig_size[1] * n))
return pygame.transform.smoothscale(image, new_size)
def path_exists(data, path): def path_exists(data, path):
current = data current = data
@@ -19,3 +20,213 @@ def path_exists(data, path):
else: else:
return False return False
return True return True
#def find_way(cells, start, goal):
# """Находит путь от start=(row, col) к goal=(row, col). row=y (строка), col=x (столбец)"""
# rows = len(cells)
# if rows == 0:
# return None
# cols = len(cells[0])
# def is_passable(cell):
# # Проходимо, если НЕ Rock в terrain И creature_obj отсутствует
# return (cell.terrain_obj is None or not isinstance(cell.terrain_obj, Rock))
# # Проверка границ и проходимости старт/гол
# s_row, s_col = start
# g_row, g_col = goal
# if not (0 <= s_row < rows and 0 <= s_col < cols and 0 <= g_row < rows and 0 <= g_col < cols):
# return None
# start_cell = cells[s_row][s_col]
# goal_cell = cells[g_row][g_col]
# if not is_passable(start_cell) or not is_passable(goal_cell):
# print(f"Старт/гол непроходимы: start={is_passable(start_cell)}, goal={is_passable(goal_cell)}")
# return None
# # A* поиск (используем прямые row,col вместо ID для простоты)
# directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] # вверх, вниз, лево, право
# open_set = []
# # f_score = g + h (h=манхэттен)
# h = abs(s_row - g_row) + abs(s_col - g_col)
# heappush(open_set, (h, 0, s_row, s_col)) # f, g, row, col
# came_from = {}
# g_score = defaultdict(lambda: float('inf'))
# g_score[(s_row, s_col)] = 0
# while open_set:
# _, _, row, col = heappop(open_set)
# if (row, col) == (g_row, g_col):
# # Восстанавливаем путь
# path = []
# current = (row, col)
# while current in came_from:
# path.append(current)
# current = came_from[current]
# path.append(start)
# return path[::-1]
# for dr, dc in directions:
# nr, nc = row + dr, col + dc
# if 0 <= nr < rows and 0 <= nc < cols:
# if is_passable(cells[nr][nc]):
# tentative_g = g_score[(row, col)] + 1
# pos = (nr, nc)
# if tentative_g < g_score[pos]:
# came_from[pos] = (row, col)
# g_score[pos] = tentative_g
# f = tentative_g + abs(nr - g_row) + abs(nc - g_col)
# heappush(open_set, (f, tentative_g, nr, nc))
# print("Путь не найден (нет связи)")
# return None
#def find_way(cells, start, goal):
# """Находит путь от start=(row, col) к goal=(row, col) с диагональным движением"""
# rows = len(cells)
# if rows == 0:
# return None
# cols = len(cells[0])
#
# def is_passable(cell):
# return (cell.terrain_obj is None or not isinstance(cell.terrain_obj, Rock))
#
# s_row, s_col = start
# g_row, g_col = goal
# if not (0 <= s_row < rows and 0 <= s_col < cols and 0 <= g_row < rows and 0 <= g_col < cols):
# return None
#
# start_cell = cells[s_row][s_col]
# goal_cell = cells[g_row][g_col]
# if not is_passable(start_cell) or not is_passable(goal_cell):
# print(f"Старт/гол непроходимы: start={is_passable(start_cell)}, goal={is_passable(goal_cell)}")
# return None
#
# # ★ 8 НАПРАВЛЕНИЙ: 4 основных + 4 диагональных ★
# directions = [
# (-1, 0), (1, 0), (0, -1), (0, 1), # вверх, вниз, лево, право (стоимость 1.0)
# (-1, -1), (-1, 1), (1, -1), (1, 1) # диагонали (стоимость √2 ≈ 1.414)
# ]
#
# open_set = []
# h = abs(s_row - g_row) + abs(s_col - g_col) # эвристика манхэттен
# heappush(open_set, (h, 0, s_row, s_col)) # f, g, row, col
#
# came_from = {}
# g_score = defaultdict(lambda: float('inf'))
# g_score[(s_row, s_col)] = 0
#
# while open_set:
# _, _, row, col = heappop(open_set)
# if (row, col) == (g_row, g_col):
# # Восстанавливаем путь
# path = []
# current = (row, col)
# while current in came_from:
# path.append(current)
# current = came_from[current]
# path.append(start)
# return path[::-1]
#
# for dr, dc in directions:
# nr, nc = row + dr, col + dc
# if 0 <= nr < rows and 0 <= nc < cols:
# if is_passable(cells[nr][nc]):
# # ★ РАЗНЫЕ СТОИМОСТИ ДЛЯ ДИАГОНАЛЕЙ ★
# if abs(dr) + abs(dc) == 2: # диагональ
# move_cost = 1.414 # √2
# else: # ортогональ
# move_cost = 1.0
#
# tentative_g = g_score[(row, col)] + move_cost
# pos = (nr, nc)
#
# if tentative_g < g_score[pos]:
# came_from[pos] = (row, col)
# g_score[pos] = tentative_g
#
# # ★ ЧЕБЫШЕВ для диагоналей лучше манхэттена ★
# h = max(abs(nr - g_row), abs(nc - g_col))
# f = tentative_g + h
# heappush(open_set, (f, tentative_g, nr, nc))
#
# print("Путь не найден (нет связи)")
# return None
def find_way(cells, start, goal):
"""Находит путь с диагональным движением, но БЕЗ прохода через углы"""
rows = len(cells)
if rows == 0:
return None
cols = len(cells[0])
def is_passable(cell):
return (cell.terrain_obj is None or not isinstance(cell.terrain_obj, Rock))
s_row, s_col = start
g_row, g_col = goal
if not (0 <= s_row < rows and 0 <= s_col < cols and 0 <= g_row < rows and 0 <= g_col < cols):
return None
start_cell = cells[s_row][s_col]
goal_cell = cells[g_row][g_col]
if not is_passable(start_cell) or not is_passable(goal_cell):
print(f"Старт/гол непроходимы")
return None
directions = [
(-1, 0), (1, 0), (0, -1), (0, 1), # ортогональ (1.0)
(-1, -1), (-1, 1), (1, -1), (1, 1) # диагональ (1.414)
]
def can_move_diagonal(current_r, current_c, dr, dc):
"""Проверяет, можно ли двигаться по диагонали (НЕ через угол)"""
nr, nc = current_r + dr, current_c + dc
# Ортогональные соседи ДОЛЖНЫ быть проходимыми для диагонального хода
ortho1_r, ortho1_c = current_r + dr, current_c # вертикальный сосед
ortho2_r, ortho2_c = current_r, current_c + dc # горизонтальный сосед
# Проверяем границы для ортососедей
if not (0 <= ortho1_r < rows and 0 <= ortho1_c < cols):
return False
if not (0 <= ortho2_r < rows and 0 <= ortho2_c < cols):
return False
return (is_passable(cells[ortho1_r][ortho1_c]) and
is_passable(cells[ortho2_r][ortho2_c]))
open_set = []
h = max(abs(s_row - g_row), abs(s_col - g_col))
heappush(open_set, (h, 0, s_row, s_col))
came_from = {}
g_score = defaultdict(lambda: float('inf'))
g_score[(s_row, s_col)] = 0
while open_set:
_, _, row, col = heappop(open_set)
if (row, col) == (g_row, g_col):
path = []
current = (row, col)
while current in came_from:
path.append(current)
current = came_from[current]
path.append(start)
return path[::-1]
for dr, dc in directions:
nr, nc = row + dr, col + dc
if 0 <= nr < rows and 0 <= nc < cols and is_passable(cells[nr][nc]):
# ★ ПРОВЕРКА ДИАГОНАЛИ ★
if abs(dr) + abs(dc) == 2: # диагональное движение
if not can_move_diagonal(row, col, dr, dc):
continue # пропускаем запрещённую диагональ
move_cost = 1.414 if abs(dr) + abs(dc) == 2 else 1.0
tentative_g = g_score[(row, col)] + move_cost
pos = (nr, nc)
if tentative_g < g_score[pos]:
came_from[pos] = (row, col)
g_score[pos] = tentative_g
h = max(abs(nr - g_row), abs(nc - g_col))
f = tentative_g + h
heappush(open_set, (f, tentative_g, nr, nc))
print("Путь не найден")
return None

89468
def_map.json

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,35 @@
from common import pygame, os, json, uuid, deepcopy, dataclass, field from common import os, json, uuid, deepcopy, dataclass, field
from common import pygame, pygame_gui
import eb_objects import eb_objects
import eb_terrain_objects import eb_terrain_objects
import eb_creature_objects import eb_creature_objects
#from pympler import muppy, summary
import gc, psutil, os
cell_classes = {"grass_small": eb_terrain_objects.Ground, cell_classes = {"grass_small": eb_terrain_objects.Ground,
"sword_default": eb_objects.Item} "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__)) main_dir = os.path.dirname(os.path.abspath(__file__))
sprites_dir = os.path.join(main_dir, "res", "sprites") sprites_dir = os.path.join(main_dir, "res", "sprites")
#class Render
#class ObjectManager
#class MapManager
#class Event
#class EventManager
#class Control
@dataclass @dataclass
class Cell: class Cell:
terrain_obj: any terrain_obj: None = None
item_obj: None = None item_obj: None = None
creature_obj: None = None creature_obj: None = None
is_target: bool = False is_target: bool = False
render_offset: tuple = (0.0, 0.0)
@dataclass @dataclass
class Map: class Map:
@@ -24,13 +39,15 @@ class Map:
cells: dict = field(default_factory = dict) cells: dict = field(default_factory = dict)
color: str = "gray57" color: str = "gray57"
target_color: str = "gold" target_color: str = "gold"
size: int = 150 cell_size: int = 150
bord: int = 3 bord: int = 3
scale: float = 1 scale: float = 1
cam_x: int = 0 cam_x: int = 0
cam_y: int = 0 cam_y: int = 0
cell_dist: int = 1 cell_dist: int = 1
#action_time_multiplier
def __post_init__(self): def __post_init__(self):
self.cells = {} self.cells = {}
with open(self.name, 'r') as file: with open(self.name, 'r') as file:
@@ -38,49 +55,135 @@ class Map:
for line in range(len(buff)): for line in range(len(buff)):
self.cells[line] = [] self.cells[line] = []
for cell in buff[str(line)]: for cell in buff[str(line)]:
final_cell = deepcopy(Cell(cell_classes[cell["terrain_obj"]["sprite_name"]](**cell["terrain_obj"]))) final_cell = Cell(cell_classes[cell["terrain_obj"]["sprite_name"]](**cell["terrain_obj"]))
if cell["item_obj"]: if cell["item_obj"]:
final_cell.item_obj = deepcopy(cell_classes[cell["item_obj"]["sprite_name"]](**cell["item_obj"])) final_cell.item_obj = cell_classes[cell["item_obj"]["sprite_name"]](**cell["item_obj"])
self.cells[line].append(deepcopy(final_cell)) if cell["creature_obj"]:
final_cell.creature_obj = cell_classes[cell["creature_obj"]["sprite_name"]](**cell["creature_obj"])
def draw_map(self, screen, current_frame, grid = True): 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 j in range(len(self.cells)):
for i, cell in enumerate(self.cells[j]): for i, cell in enumerate(self.cells[j]):
dd = {"x": int((i * self.size + self.cam_x) * self.scale), base_x = i * self.cell_size + self.cam_x
"y": int((j * self.size + self.cam_y) * self.scale), base_y = j * self.cell_size + self.cam_y
"w": int(self.size * self.scale - self.cell_dist),
"h": int(self.size * self.scale - self.cell_dist), # 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, "spr_up": current_frame % self.sprites_refresh,
"sprites": self.sprites, "scale": self.scale, "sprites": self.sprites, "scale": self.scale, "screen": screen
"screen": screen} }
terrain_list.append((cell.terrain_obj, terrain_dd))
cell.terrain_obj.draw(dd) # Creature данные (если есть)
if cell.item_obj:
cell.item_obj.draw(dd)
if cell.creature_obj: if cell.creature_obj:
cell.creature_obj.draw(dd) 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)
if grid:
pygame.draw.rect(screen, self.color, pygame.Rect(dd["x"], dd["y"], dd["w"], dd["h"]), self.bord)
@dataclass @dataclass
class Engine: class Engine:
sprites: dict = field(default_factory = dict) sprites: dict = field(default_factory = dict)
cached_sprites: dict = field(default_factory = dict)
screen: pygame.Surface = ((1, 1)) screen: pygame.Surface = ((1, 1))
width: int = 1600 width: int = 1600
height: int = 800 height: int = 800
camera_step: int = 10 camera_step: int = 10
scale_step: float = 0.01 scale_step: float = 0.01
spr_scale: float = 1
def __post_init__(self): def __post_init__(self):
self.sprites = {} self.sprites = {}
pygame.init() pygame.init()
pygame.display.set_caption('Elvenbane') pygame.display.set_caption('Elvenbane')
self.screen = pygame.display.set_mode((self.width, self.height)) self.screen = pygame.display.set_mode((self.width, self.height), pygame.HWSURFACE | pygame.DOUBLEBUF)
self.load_sprites()
print("The engine has started. Sprites were successfully loaded.\n") 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): def load_sprites(self, folder_path = sprites_dir):
self.sprites = {} self.sprites = {}
@@ -101,11 +204,73 @@ class Engine:
for num, f in items 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): def main_loop(self):
easy_map = Map("def_map.json", self.sprites) 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") #sp = eb_objects.Sprite(self.sprites, "elf_watching")
#gr = pygame.image.load(file_path).convert_alpha() #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() clock = pygame.time.Clock()
running = True running = True
unlock = True unlock = True
@@ -114,21 +279,67 @@ class Engine:
global_counter = 0 global_counter = 0
global_counter_cap = 100000 global_counter_cap = 100000
active_cell = None
# profiling
process = psutil.Process(os.getpid())
gc.collect()
mem_before = process.memory_info().rss / 1024
while running: 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 # poll for events
# pygame.QUIT event means the user clicked X to close your window # pygame.QUIT event means the user clicked X to close your window
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
running = False 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 # fill the screen with a color to wipe away anything from last frame
self.screen.fill("chartreuse4") #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) easy_map.draw_map(self.screen, current_frame + 1)
#print(easy_map.cells[0][0].item_obj.sprite_cache) manager.draw_ui(self.screen)
if unlock: if not console_active:
keys = pygame.key.get_pressed() keys = pygame.key.get_pressed()
if keys[pygame.K_w]: if keys[pygame.K_w]:
easy_map.cam_y += self.camera_step easy_map.cam_y += self.camera_step
if keys[pygame.K_s]: if keys[pygame.K_s]:
@@ -137,10 +348,45 @@ class Engine:
easy_map.cam_x += self.camera_step easy_map.cam_x += self.camera_step
if keys[pygame.K_d]: if keys[pygame.K_d]:
easy_map.cam_x -= self.camera_step easy_map.cam_x -= self.camera_step
if keys[pygame.K_q]: if keys[pygame.K_q]:
easy_map.scale += self.scale_step 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: if keys[pygame.K_e] and easy_map.scale >= self.scale_step:
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]: if keys[pygame.K_ESCAPE]:
running = False running = False
@@ -151,8 +397,10 @@ class Engine:
global_counter = 0 global_counter = 0
# flip() the display to put your work on screen # flip() the display to put your work on screen
pygame.display.flip() pygame.display.update()
# limits FPS to 60
clock.tick(max_fps) if global_counter % 10 == 0:
current_fps = clock.get_fps()
print(f"Current FPS: {current_fps:.2f}")
pygame.quit() pygame.quit()

View File

@@ -1,4 +1,4 @@
from common import deepcopy, dataclass, field, scale_image, path_exists from common import deepcopy, dataclass, field
@dataclass @dataclass
class Object: class Object:
@@ -6,40 +6,24 @@ class Object:
name: str name: str
sprite_name: str sprite_name: str
sprite_state: int = 0 sprite_state: int = 0
#sprite_scale: int = 1 # current_map
#sprite_cache: dict = field(default_factory = dict) # pos
#sprite_cache_upd: int = 100 # weight
# # effects = {}
#def cache_sprite(self, sprites):
# if self.sprite_name not in self.sprite_cache:
# self.sprite_cache[self.sprite_name] = {}
# self.sprite_cache[self.sprite_name][self.sprite_state] = deepcopy(sprites[self.sprite_name][self.sprite_state])
#
#def scale_cached(self, draw_data):
# if self.sprite_scale != draw_data["scale"]:
# self.sprite_scale = draw_data["scale"]
# self.sprite_cache[self.sprite_name][self.sprite_state] = deepcopy(scale_image(draw_data["sprites"][self.sprite_name][self.sprite_state], draw_data["scale"]))
def draw(self, draw_data): def draw(self, draw_data):
#if draw_data["global_counter"] > self.sprite_cache_upd:
# self.sprite_cache = {}
if draw_data["spr_up"] == 0: if draw_data["spr_up"] == 0:
if self.sprite_state == len(draw_data["sprites"][self.sprite_name]) - 1: if self.sprite_state == len(draw_data["sprites"][self.sprite_name]) - 1:
self.sprite_state = 0 self.sprite_state = 0
else: else:
self.sprite_state += 1 self.sprite_state += 1
#if path_exists(self.sprite_cache, [self.sprite_name, self.sprite_state]): sp = draw_data["sprites"][self.sprite_name][self.sprite_state]
# self.scale_cached(draw_data)
#else:
# self.cache_sprite(draw_data["sprites"])
sp = scale_image(draw_data["sprites"][self.sprite_name][self.sprite_state], draw_data["scale"])
rect = sp.get_rect(center = (draw_data["x"] + draw_data["w"] /2, draw_data["y"] + draw_data["h"]/ 2)) rect = sp.get_rect(center = (draw_data["x"] + draw_data["w"] /2, draw_data["y"] + draw_data["h"]/ 2))
draw_data["screen"].blit(sp, rect) draw_data["screen"].blit(sp, rect)
def update(self):
pass
@dataclass @dataclass
class Terrain(Object): class Terrain(Object):
@@ -47,16 +31,72 @@ class Terrain(Object):
@dataclass @dataclass
class Creature(Object): class Creature(Object):
pass waypoints: list = field(default_factory = list)
#status quick_actions: list = field(default_factory = list)
#actions tasks: list = field(default_factory = list)
#tasks inventory: dict = field(default_factory = dict)
#items
move_progress: float = 0.0 # 0.0 = старт клетки, 1.0 = конец клетки
current_target: tuple = None # (row, col) следующая клетка
move_speed: float = 0.02 # пикселей/кадр (настройте)
render_offset: tuple = (0.0, 0.0)
start_pos: tuple = None # (row, col) начальная позиция сегмента пути
def move(self, cells, start, goal):
from common import find_way
self.start_pos = start
path = find_way(cells, start, goal)
if path and len(path) > 1:
self.waypoints = path[1:] # Убираем текущую позицию
self.current_target = self.waypoints[0]
self.move_progress = 0.0
self.start_pos = start # ★ ТУТ - текущая позиция как стартовая для первого шага ★
self.render_offset = (0.0, 0.0)
def update(self, time_delta, cell_size, map_obj):
if self.current_target is None or not self.waypoints:
self.render_offset = (0.0, 0.0)
return
self.move_progress += self.move_speed * time_delta * 60
self.move_progress = min(self.move_progress, 1.0)
if self.move_progress >= 1.0:
map_obj.move_obj('creature_obj', self.start_pos, self.current_target)
self.waypoints.pop(0)
if self.waypoints:
self.start_pos = self.current_target # Новая клетка как старт
self.current_target = self.waypoints[0]
self.move_progress = 0.0
self.render_offset = (0.0, 0.0) # ← ДОБАВИТЬ!
else:
self.current_target = None
self.render_offset = (0.0, 0.0)
return
# ★ ТОЛЬКО интерполяция offset ★
start_row, start_col = self.start_pos #or (0, 0)
target_row, target_col = self.current_target
offset_x = (target_col - start_col) * cell_size * self.move_progress
offset_y = (target_row - start_row) * cell_size * self.move_progress
self.render_offset = (offset_x, offset_y)
@dataclass @dataclass
class Item(Object): class Item(Object):
# passive_abilities = {}
# active_abilities = {}
pass pass
@dataclass
class Container(Item):
# content = {}
pass
@dataclass @dataclass
class Building(Object): class Building(Object):
pass pass

27
main.py
View File

@@ -5,31 +5,4 @@ def main():
e.main_loop() e.main_loop()
if __name__ == "__main__": if __name__ == "__main__":
# pydantic instead of dataclasses?
# Отрисовка голой сетки, прокрутка, масштаб +
# Отрисовка спрайтов:
# - сделать масштабирование в соотв. с клеткой +
# - посмотреть класс спрайта или сделать свой +
# - добавить отрисовку существ и предметов с анимацией +
# почитать про Surface, Display, доку к pygame-gui
# Начало гуя:
# - общая идея гуя
# - кнопка отключить сетку
# - строка ввода
# - клик
# Поиск пути, очередь задач для существ
# Редактор карты
# Охотник -> деревня
# Простой, но основательный гуй внизу экрана, глобальная карта и перемещение
# деревня на соседской локации и торговля с ней
# перемещение по воде, течение
#техдолг:
#проверить дефолтдикт field и None = None
#не взлетело кэширование - потом доделать
# проверить у ллм на ошибки - РЕГУЛЯРНАЯ АКТИВНОСТЬ:
# - deepcopy +
# - общие +
main() main()
#техдолг - draw_data to dd

View File

@@ -35,3 +35,53 @@ scaled = scale_image(sprites[cell.terrain_obj.sprite], self.scale) # KeyError!
7. Масштабирование каждый кадр 7. Масштабирование каждый кадр
scale_image() вызывается 150×150=22,500 раз в секунду при 60 FPS. Кэшируйте масштабированные спрайты. scale_image() вызывается 150×150=22,500 раз в секунду при 60 FPS. Кэшируйте масштабированные спрайты.
=========================================================================================================
#
#техдолг:
# pydantic instead of dataclasses?
# почитать про Surface, Display, доку к pygame-gui
# проверить дефолтдикт field и None = None
# изучить pypmler
# настроить логирование всего
# SLOP: load_sprites
# проверить у ллм на ошибки - РЕГУЛЯРНАЯ АКТИВНОСТЬ:
# - deepcopy +
# - общие +
# !!! ДОБАВИТЬ ПРОКРУТКУ И МАСШТАБ КАРТЫ ДЛЯ МЫШИ !!!
#
# ДОДЕЛАТЬ move для Creature - хранить pos в объекте ???
#
# ПРОВЕРИТЬ МЕНЯЕТСЯ ЛИ ПЕРЕДАННЫЙ В ОБЪЕКТ cells и если да,
# перенести всё взаимодействие с картой в объекты, карта только хранит cells
# и готовит данные для отрисовки Render'ом
#
# ИГРОВОЙ ТАКТ? или только для действий их длительность?
#
# ПОСМОТРЕТЬ ПО КОММИТАМ ЗАЧЕМ БЫЛ НУЖЕН path_exists, удалить?
#
# добавил гуй, динамическая консоль, всё работает, но:
# - слоп, почистить
# - мини-баг - если первые вводимые буквы совпадают с клавишами управления, один раз успевает проскочить до лока. некритично.
# - при вводе текста нет прокрутки к концу
# - плавающий баг - если повводить текст, а потом закрыть консоль, игра не закроется по эскейпу.
#
#
# в дальнейшем вся отрисовка переедет в класс рендер,
# карта будет только вовзращать поверхность для отрисовки или даже просто Cells
# active_cell переедет в класс Control
#
# НАЙТИ В КОДЕ ГДЕ Я ТАК НЕ СДЕЛАЛ И ИСПРАВИТЬ - НАШЕЛ ОДНУ, ПОИСКАТЬ ЕЩЕ
#if a is None:
# print("a это точно None")
#
# Альтернатива
#if a is not None:
# print("a не None")
#
# Встреча с Егором:
#
# сборщик данных в цикле и перекладчик
# модуль автоинпута, принимает поток данных и переводит их в команды движка

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,8 +1,8 @@
import json import json
from copy import deepcopy from copy import deepcopy
width = 10 width = 100
height = 8 height = 100
grass_def = {"id": "1", "name": "2", "sprite_name": "grass_small"} grass_def = {"id": "1", "name": "2", "sprite_name": "grass_small"}
cell_def = {"terrain_obj": grass_def, "item_obj": {}, "creature_obj": {}} cell_def = {"terrain_obj": grass_def, "item_obj": {}, "creature_obj": {}}

BIN
res/rocks-2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB