Добавил систему задач для юнитов, она реализована в методе update класса Creature. Подчистил код, пофиксил по мелочам баги. Остался ещё техдолг Егору и задачи из main.

This commit is contained in:
shiva404
2026-03-05 16:35:08 +03:00
parent dafa95989f
commit 45f2c71cb8
6 changed files with 266 additions and 95 deletions

View File

@@ -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):

View File

@@ -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": {

View File

@@ -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
@@ -47,6 +47,7 @@ class Map:
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_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)) # Убираем занятые клетки
# # Находим свободные клетки
# # ★ МАССОВЫЙ СПАВН 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()

View File

@@ -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,21 +49,30 @@ 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)
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 # пикселей/кадр (настройте)
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
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
@@ -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

BIN
log.txt Normal file

Binary file not shown.

27
main.py
View File

@@ -11,8 +11,35 @@ if __name__ == "__main__":
# todo:
# прокрутка баг консоль и карта
# ОПТИМИЗАЦИИ
# перепроверять путь пореже, или только после столкновения, или локальный поиск
# очередь задач и задача рандомного патруля
# устроить краш тест поиску пути, запустив много объектов на маленьком поле, успел заметить баги
# добавить функцию движения за каким-то объектом
# сделать, чтобы в случае отменненого движения не телепортировался назад, а плавно
# приступаем к логике
# сделать по аналогии с текущей клеткой текущий объект
# посмотреть как в 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 в реплан, проверить
# совет ксюши - не считать коллизии с объектами, только при перемещении в клетку проверять