diff --git a/common.py b/common.py index a892229..73f0083 100644 --- a/common.py +++ b/common.py @@ -4,9 +4,7 @@ import uuid import random from dataclasses import dataclass, field from copy import deepcopy - -from collections import defaultdict -from heapq import heappush, heappop +from functools import partial import pygame import pygame_gui @@ -15,12 +13,10 @@ from classes import Rock from pathfinding.core.grid import Grid from pathfinding.finder.a_star import AStarFinder - from collections import defaultdict from heapq import heappush, heappop from math import sqrt, inf - def path_exists(data, path): current = data for key in path: @@ -371,7 +367,7 @@ def find_way(cells, start, goal): queue[queue_size] = [nr, nc] queue_size += 1 - print(f"Путь не найден: {start} -> {goal}") + #print(f"Путь не найден: {start} -> {goal}") return None def can_move_diagonal(r, c, nr, nc, rocks_only, rows, cols): diff --git a/def_map.json b/def_map.json index 4b2f92c..a3a1bad 100644 --- a/def_map.json +++ b/def_map.json @@ -20,11 +20,7 @@ "sprite_name": "grass_small" }, "item_obj": {}, - "creature_obj": { - "id": "1", - "name": "2", - "sprite_name": "elf_watching" - } + "creature_obj": {} }, { "terrain_obj": { @@ -926,11 +922,7 @@ "sprite_name": "grass_small" }, "item_obj": {}, - "creature_obj": { - "id": "1", - "name": "2", - "sprite_name": "elf_watching" - } + "creature_obj": {} }, { "terrain_obj": { diff --git a/eb_engine.py b/eb_engine.py index 56e8cdb..75819bc 100644 --- a/eb_engine.py +++ b/eb_engine.py @@ -1,5 +1,5 @@ from common import os, json, uuid, deepcopy, random -from common import dataclass, field +from common import dataclass, field, partial from common import pygame, pygame_gui import eb_objects import eb_terrain_objects @@ -46,7 +46,8 @@ class Map: cam_x: int = 0 cam_y: int = 0 cell_dist: int = 1 - + + #effects[] #action_time_multiplier def __post_init__(self): @@ -55,7 +56,7 @@ class Map: buff = json.load(file) for line in range(len(buff)): self.cells[line] = [] - for cell in buff[str(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"]: @@ -63,6 +64,7 @@ class Map: 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) @@ -90,6 +92,7 @@ class Map: setattr(source_cell, type, None) setattr(dest_cell, type, obj) + #obj.grid_pos = goal return True def get_cell_at_mouse(self, mouse_pos): @@ -113,7 +116,7 @@ class Map: 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: + if cell.creature_obj: cell.creature_obj.update(time_delta, self.cell_size, self) # self! def draw_map(self, screen, current_frame, grid=True): @@ -293,8 +296,17 @@ class Engine: mem_before = process.memory_info().rss / 1024 - NUM_ELVES = 1000 + NUM_ELVES = 2000 elf_count = 0 + space_pressed = False + spawn = False + + def spawn_elf(): + elf = eb_objects.Creature(id=f"elf_{random.randint(1000,9999)}", name="Elf", sprite_name="elf_watching", grid_pos = (0, 0)) + 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, r_move_short, r_move_short, r_move_short, r_move_short, r_move_long]) + easy_map.cells[0][0].creature_obj = elf while running: @@ -305,12 +317,29 @@ class Engine: 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_elf() + elf_count += 1 + # 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 @@ -372,41 +401,39 @@ class Engine: 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 keys[pygame.K_SPACE] and elf_count < NUM_ELVES: - # Находим свободные клетки - # ★ МАССОВЫЙ СПАВН 50 эльфов ★ - free_cells = [] - for j in range(len(easy_map.cells)): # ★ ЛИМИТ строк ★ - for i in range(len(easy_map.cells[j])): # ★ ЛИМИТ колонок ★ - cell = easy_map.cells[j][i] - if (cell.creature_obj is None and - cell.terrain_obj and - cell.terrain_obj.sprite_name == "grass_small"): - free_cells.append((j, i)) - - spawn_count = min(50, len(free_cells)) # ★ Не больше свободных клеток ★ - for _ in range(spawn_count): - row, col = random.choice(free_cells) - elf = eb_objects.Creature(id=f"elf_{random.randint(1000,9999)}", name="Elf", sprite_name="elf_watching") - easy_map.cells[row][col].creature_obj = elf - - # ★ БЕЗОПАСНЫЙ выбор цели ★ - possible_targets = [c for c in free_cells if c != (row, col)] - if possible_targets: - target_cell = random.choice(possible_targets) - elf.move(easy_map.cells, (row, col), target_cell) - - free_cells.remove((row, col)) # Убираем занятые клетки + 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() + + # # Находим свободные клетки + # # ★ МАССОВЫЙ СПАВН 50 эльфов ★ + # free_cells = [] + # for j in range(len(easy_map.cells)): # ★ ЛИМИТ строк ★ + # for i in range(len(easy_map.cells[j])): # ★ ЛИМИТ колонок ★ + # cell = easy_map.cells[j][i] + # if (cell.creature_obj is None and + # cell.terrain_obj and + # cell.terrain_obj.sprite_name == "grass_small"): + # free_cells.append((j, i)) +# + # spawn_count = min(50, len(free_cells)) # ★ Не больше свободных клеток ★ + # for _ in range(spawn_count): + # row, col = random.choice(free_cells) + # elf = eb_objects.Creature(id=f"elf_{random.randint(1000,9999)}", name="Elf", sprite_name="elf_watching") + # easy_map.cells[row][col].creature_obj = elf +# + # # ★ БЕЗОПАСНЫЙ выбор цели ★ + # possible_targets = [c for c in free_cells if c != (row, col)] + # if possible_targets: + # target_cell = random.choice(possible_targets) + # elf.move(easy_map.cells, (row, col), target_cell) +# + # free_cells.remove((row, col)) # Убираем занятые клетки @@ -437,7 +464,7 @@ class Engine: 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) + easy_map.cells, cell_coords) if keys[pygame.K_ESCAPE]: running = False @@ -453,6 +480,6 @@ class Engine: if global_counter % 10 == 0: current_fps = clock.get_fps() - print(f"Current FPS: {current_fps:.2f}") + print(f"Elves count: {elf_count} - Current FPS: {current_fps:.2f}") pygame.quit() \ No newline at end of file diff --git a/eb_objects.py b/eb_objects.py index c24e56c..d1dd457 100644 --- a/eb_objects.py +++ b/eb_objects.py @@ -1,4 +1,21 @@ -from common import deepcopy, dataclass, field +from common import deepcopy, dataclass, field, random + +@dataclass +class Action: + sprite_name: str + func: function + duration: float + progress: float = 0.0 + #status: str = "pending" + #взаимодействие с инвентарем + #взять предмет + #бросить предмет + #передать предмет + #собрать ресурс + #активное действие с оружием + #колдовство + +#class Task @dataclass class Object: @@ -6,6 +23,7 @@ class Object: name: str sprite_name: str sprite_state: int = 0 + grid_pos: tuple = None # current_map # pos # weight @@ -31,22 +49,31 @@ class Terrain(Object): @dataclass class Creature(Object): - waypoints: list = field(default_factory = list) - quick_actions: list = field(default_factory = list) - tasks: list = field(default_factory = list) - inventory: dict = field(default_factory = dict) - - move_progress: float = 0.0 # 0.0 = старт клетки, 1.0 = конец клетки - current_target: tuple = None # (row, col) следующая клетка - final_goal: tuple = None - move_speed: float = 0.02 # пикселей/кадр (настройте) - render_offset: tuple = (0.0, 0.0) - start_pos: tuple = None # (row, col) начальная позиция сегмента пути - - replan_counter: int = 0 - REPLAN_INTERVAL: int = 300 + direction: int = 0 # tuple? + move_progress: float = 0.0 # 0.0 = старт клетки, 1.0 = конец клетки + current_target: tuple = None # (row, col) следующая клетка + final_goal: tuple = None + move_speed: float = 0.02 # пикселей/кадр + render_offset: tuple = (0.0, 0.0) + start_pos: tuple = None # (row, col) начальная позиция сегмента пути + replan_counter: int = 0 + REPLAN_INTERVAL: int = 30000 + waypoints: list = field(default_factory = list) + inventory: dict = field(default_factory = dict) + quick_actions: list = field(default_factory = list) + tasks: list = field(default_factory = list) + + action: Action = None + action_time: float = 0.0 + action_counter: int = 0 + task_counter: int = 0 + + new_task: list = field(default_factory = list) + interrupt_task: list = field(default_factory = list) + interrupt_action_status: str = "completed" + def replan(self, cells, pos): from common import find_way path = find_way(cells, pos, self.final_goal) @@ -56,21 +83,9 @@ class Creature(Object): else: self.waypoints.clear() self.current_target = None + self.final_goal = None - def move(self, cells, start, goal): - from common import find_way - self.final_goal = goal - 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): + def calc_step(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 @@ -98,28 +113,143 @@ class Creature(Object): if self.move_progress >= 1.0: map_obj.move_obj('creature_obj', self.start_pos, self.current_target) + self.grid_pos = self.current_target if self.waypoints: self.waypoints.pop(0) if self.waypoints: - self.start_pos = self.current_target # Новая клетка как старт + self.start_pos = self.current_target self.current_target = self.waypoints[0] self.move_progress = 0.0 - self.render_offset = (0.0, 0.0) # ← ДОБАВИТЬ! + self.render_offset = (0.0, 0.0) else: + #print(111111111111111) self.current_target = None + self.final_goal = None self.render_offset = (0.0, 0.0) return - - # ★ ТОЛЬКО интерполяция offset ★ - start_row, start_col = self.start_pos #or (0, 0) + if self.current_target is None: self.render_offset = (0.0, 0.0) return + + start_row, start_col = self.start_pos 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) + def move(self, cells, goal): + from common import find_way + self.final_goal = goal + path = find_way(cells, self.grid_pos, 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 = self.grid_pos + self.render_offset = (0.0, 0.0) + else: + self.final_goal = None + + def create_task(self): + self.new_task = [] + + #add actions_default_durations dict {func1: dur1, ...} + def add_action(self, sprite_name: str, func: function, duration: float): + self.new_task.append(Action(sprite_name, func, duration)) + + def add_task(self): + self.tasks.append(self.new_task) + + def add_interr_task(self): + pass + #param resume_on_interrupt, Bool + #if True + # save goal to buffer + #if False + # action/task_counter = 0 + #clear goal/wp + + def update(self, time_delta, cell_size, map_obj): + #quick_actions? here? + #print(self.waypoints, self.final_goal, self.action_counter, self.task_counter) + if self.final_goal is not None: + #print(2) + self.calc_step(time_delta, cell_size, map_obj) + return + + if self.interrupt_task and self.interrupt_action_status == "completed": + #print(3) + self.action_time = 0.0 + self.action = self.interrupt_task.pop(0) + self.interrupt_action_status = "active" + + #print(f" DEBUG: tasks={len(self.tasks)}, " + # f"task_len={len(self.tasks[self.task_counter]) if self.tasks else 0}, " + # f"action={self.action is not None}") + + if self.action: + #print(self.action_counter, self.task_counter) + self.action_time += time_delta + self.action.progress = min(1.0, self.action_time / self.action.duration) + + if self.action_time >= self.action.duration: + self.action.func() + if self.interrupt_action_status == "active": + self.interrupt_action_status == "completed" + #if not self.inter_task and goal: move to buff_goal from self.pos (add to Object) + self.action = None + self.action_time = 0.0 + + + elif self.tasks: + #print(6) + if self.action_counter < len(self.tasks[self.task_counter]): + self.action = self.tasks[self.task_counter][self.action_counter] + self.action_counter += 1 + else: + self.task_counter += 1 + self.action_counter = 0 + if self.task_counter == len(self.tasks): + self.task_counter = 0 + + self.action = self.tasks[self.task_counter][self.action_counter] + self.action_counter += 1 + self.action_time = 0.0 + + + #elif self.tasks: + # if self.action_counter >= len(self.tasks[self.task_counter]): + # self.task_counter += 1 + # self.action_counter = 0 + # if self.task_counter >= len(self.tasks): + # self.task_counter = 0 +# + # self.action = self.tasks[self.task_counter][self.action_counter] + # self.action_counter += 1 + # self.action_time = 0.0 + + def patrol(self, cells, area): + goal = (random.randint(self.grid_pos[0] - area, + self.grid_pos[0] + area), + random.randint(self.grid_pos[1] - area, + self.grid_pos[1] + area)) + while goal == self.grid_pos: + goal = (random.randint(self.grid_pos[0] - area, + self.grid_pos[0] + area), + random.randint(self.grid_pos[1] - area, + self.grid_pos[1] + area)) + self.move(cells, goal) + + + def move_rand(self, cells, area_start, area_end): + goal = (random.randint(area_start, area_end), random.randint(area_start, area_end)) + while goal == self.grid_pos: + goal = (random.randint(area_start, area_end), random.randint(area_start, area_end)) + self.move(cells, goal) + + + @dataclass class Item(Object): @@ -132,7 +262,6 @@ class Container(Item): # content = {} pass - @dataclass class Building(Object): - pass + pass \ No newline at end of file diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..f8a8771 Binary files /dev/null and b/log.txt differ diff --git a/main.py b/main.py index 4b0d6d7..02c7c4f 100644 --- a/main.py +++ b/main.py @@ -11,8 +11,35 @@ if __name__ == "__main__": # todo: # прокрутка баг консоль и карта # ОПТИМИЗАЦИИ + # перепроверять путь пореже, или только после столкновения, или локальный поиск # очередь задач и задача рандомного патруля # устроить краш тест поиску пути, запустив много объектов на маленьком поле, успел заметить баги # добавить функцию движения за каким-то объектом # сделать, чтобы в случае отменненого движения не телепортировался назад, а плавно - # приступаем к логике \ No newline at end of file + # приступаем к логике + # сделать по аналогии с текущей клеткой текущий объект + + + + + # посмотреть как в clock = pygame.time.Clock() работает фпс + # перемещать оъект в другую клетку при половине офсета + + # техдолг Егору + # убрать cells и mapobject creature - перенести нужную логику в методы Map + # система имен спрайтов и Action - реализовать + # рисовать группой спрайтов как в перпл + # нужен ли теперь start_pos? grid_pos? + # class Task с проверками выполнения экшонов + + #вернуть назад апдейт. вернул - работает, сравнить с новым и решить + #final_goal = None - check all! + #рефактор goal = (random.randint(self.grid_pos[0] - area, + # self.grid_pos[0] + area), + # random.randint(self.grid_pos[1] - area, + # self.grid_pos[1] + area)) + + # отловить баг - иногда всё равно встают - похоже из-за реплана и коллизий с эльфами + # вроде починил, добавив final_goal = None в реплан, проверить + + # совет ксюши - не считать коллизии с объектами, только при перемещении в клетку проверять \ No newline at end of file