Merge pull request 'feature/ai-but-cooler' (#35) from feature/ai-but-cooler into main

Есть куда стремиться, но для work-in-progress покатит. Потом с удобством использования поиграемся

Reviewed-on: #35
This commit is contained in:
PeaAshMeter 2026-04-17 00:31:10 +03:00
commit d33d6eedd6
3 changed files with 163 additions and 20 deletions

View File

@ -1,5 +1,10 @@
local easing = require "lib.utils.easing"
local pf = require "lib.pathfinder"
local utils = require "lib.utils.utils"
--- @alias AIAction fun(self: AIBehavior): Task<nil>
--- @return Character
local function closestCharacter(char)
local caster = Vec3 {}
char:try(Tree.behaviors.positioned, function(b)
@ -20,45 +25,173 @@ local function closestCharacter(char)
return charTarget
end
-- --- Возвращает все точки в радиусе в виде векторов (должен по крайней мере)
-- --- @param radius integer
-- --- @param center Vec3
-- --- @return Vec3[]
-- local function circleVectors(center, radius)
-- local vecs = {}
-- local res = {}
-- for t = 0, 2 * math.pi, EPSILON do
-- local x = math.cos(t) * radius + center.x
-- local y = math.sin(t) * radius + center.y
-- table.insert(vecs, Vec3 { math.floor(x), math.floor(y) })
-- end
-- for _, v in pairs(vecs) do
-- local i = 1
-- while i <= #res and (res[i].x ~= v.x or res[i].y ~= v.y) do
-- i = i + 1
-- end
-- if i == #res + 1 or #res == 0 then
-- table.insert(res, v)
-- print('[AI]: circle vecs:', v)
-- end
-- end
-- return res
-- end
--- Возвращает все точки в радиусе в виде векторов (должен по крайней мере)
--- @param radius integer
--- @param center Vec3
--- @return Vec3[]
local function circleVectors(center, radius)
local dx, dy, err = radius, 0, 1 - radius
local vecs, res = {}, {}
while dx >= dy do
table.insert(vecs, Vec3 { center.x + dx, center.y + dy })
table.insert(vecs, Vec3 { center.x - dx, center.y + dy })
table.insert(vecs, Vec3 { center.x + dx, center.y - dy })
table.insert(vecs, Vec3 { center.x - dx, center.y - dy })
table.insert(vecs, Vec3 { center.x + dy, center.y + dx })
table.insert(vecs, Vec3 { center.x - dy, center.y + dx })
table.insert(vecs, Vec3 { center.x + dy, center.y - dx })
table.insert(vecs, Vec3 { center.x - dy, center.y - dx })
dy = dy + 1
if err < 0 then
err = err + 2 * dy + 1
else
dx, err = dx - 1, err + 2 * (dy - dx) + 1
end
end
for _, v in pairs(vecs) do
local i = 1
while i <= #res and (res[i].x ~= v.x or res[i].y ~= v.y) and v.x >= 0 and v.y >= 0 do
i = i + 1
end
if i == #res + 1 or #res == 0 then
table.insert(res, v)
print('[AI]: circle vecs:', v)
end
end
return vecs
end
--- ищет пути к ближайшему персу в определённом радиусе
--- @param owner Character
--- @param radius integer здесь мы должны сами определять, сколько должны не доходить до персонажа (1 <= n)
--- @return Vec3|nil
local function pathToClosestCharacter(owner, radius)
local charTarget = closestCharacter(owner)
local targetPosition, ownerPosition = charTarget:has(Tree.behaviors.positioned), owner:has(Tree.behaviors.positioned)
if not targetPosition or not ownerPosition then return end
local circleVecs = circleVectors(targetPosition.position, radius)
local target = circleVecs[#circleVecs]
local path = pf(ownerPosition.position, target)
for i, c in ipairs(circleVecs) do
local newPath = pf(ownerPosition.position, c)
if newPath:size() < path:size() then
path = newPath
target = c
end
end
return target
end
--- @type table<Class, AIAction>
local aiNature = {
dev_warrior = function(self)
return function(callback) -- почему так, описано в Task
self.owner:try(Tree.behaviors.spellcaster, function(spellB)
self.target = pathToClosestCharacter(self.owner, 1)
local attackTarget = closestCharacter(self.owner):has(Tree.behaviors.positioned)
if not attackTarget then return end
local task1 = spellB.spellbook[1]:cast(self.owner, self.target)
if task1 then
task1(
function()
-- здесь мы оказываемся после того, как сходили в первый раз
print('[AI]: я походил')
local task2 = spellB.spellbook[3]:cast(self.owner, attackTarget.position)
if task2 then
-- дергаем функцию после завершения хода
print('[AI]: и ударил')
task2(callback)
else
print('[AI]: чёт не бьётся')
callback()
end
end
)
else
print('рот этого казино')
callback()
end
end)
end
end,
dev_mage = function(self)
return function(callback)
print("etoh... bleh")
callback()
end
end
}
--- @class AIBehavior : Behavior
--- @field target Vec3?
local behavior = {}
behavior.__index = behavior
behavior.id = "ai"
function behavior.new()
return setmetatable({}, behavior)
end
--- @return Task<nil>
function behavior:makeTurn()
function behavior:dev_warrior()
return function(callback) -- почему так, описано в Task
self.owner:try(Tree.behaviors.spellcaster, function(spellB)
local charTarget = closestCharacter(self.owner)
charTarget:try(Tree.behaviors.positioned, function(b)
self.target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти
end)
self.target = pathToClosestCharacter(self.owner, 1)
local attackTarget = closestCharacter(self.owner):has(Tree.behaviors.positioned)
if not attackTarget then return end
local task1 = spellB.spellbook[1]:cast(self.owner, self.target)
if task1 then
task1(
function()
-- здесь мы оказываемся после того, как сходили в первый раз
local newTarget = Vec3 { 1, 1 }
local task2 = spellB.spellbook[1]:cast(self.owner, newTarget)
print('[AI]: я походил')
local task2 = spellB.spellbook[3]:cast(self.owner, attackTarget.position)
if task2 then
-- дергаем функцию после завершения хода
print('[AI]: и ударил')
task2(callback)
else
print('[AI]: чёт не бьётся')
callback()
end
end
)
else
print('рот этого казино')
callback()
end
end)
end
end
--- @param class Class
function behavior.new(class)
return setmetatable({
makeTurn = aiNature[class]
}, behavior)
end
return behavior

View File

@ -1,7 +1,10 @@
--- @alias Class "dev_warrior"|"dev_mage"
--- @class StatsBehavior : Behavior
--- @field hp integer
--- @field mana integer
--- @field initiative integer
--- @field class Class
--- @field isInTurnOrder boolean
local behavior = {}
behavior.__index = behavior
@ -10,13 +13,15 @@ behavior.id = "stats"
--- @param hp? integer
--- @param mana? integer
--- @param initiative? integer
--- @param class? Class
--- @param isInTurnOrder? boolean
function behavior.new(hp, mana, initiative, isInTurnOrder)
function behavior.new(hp, mana, initiative, class, isInTurnOrder)
return setmetatable({
hp = hp or 20,
mana = mana or 10,
initiative = initiative or 10,
isInTurnOrder = isInTurnOrder or true
class = class or "dev_warrior",
isInTurnOrder = isInTurnOrder or true,
}, behavior)
end

View File

@ -19,7 +19,7 @@ function love.load()
:addBehavior {
Tree.behaviors.residentsleeper.new(),
Tree.behaviors.stats.new(nil, nil, 1),
Tree.behaviors.positioned.new(Vec3 { 3, 3 }),
Tree.behaviors.positioned.new(Vec3 { 1, 1 }),
Tree.behaviors.tiled.new(),
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
Tree.behaviors.shadowcaster.new(),
@ -29,7 +29,7 @@ function love.load()
:addBehavior {
Tree.behaviors.residentsleeper.new(),
Tree.behaviors.stats.new(nil, nil, 1),
Tree.behaviors.positioned.new(Vec3 { 4, 3 }),
Tree.behaviors.positioned.new(Vec3 { 3, 1 }),
Tree.behaviors.tiled.new(),
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
Tree.behaviors.shadowcaster.new(),
@ -39,7 +39,7 @@ function love.load()
:addBehavior {
Tree.behaviors.residentsleeper.new(),
Tree.behaviors.stats.new(nil, nil, 3),
Tree.behaviors.positioned.new(Vec3 { 5, 3 }),
Tree.behaviors.positioned.new(Vec3 { 7, 2 }),
Tree.behaviors.tiled.new(),
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
Tree.behaviors.shadowcaster.new(),
@ -54,7 +54,7 @@ function love.load()
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
Tree.behaviors.shadowcaster.new(),
Tree.behaviors.spellcaster.new(),
Tree.behaviors.ai.new()
Tree.behaviors.ai.new("dev_warrior") -- так мы вообще делать не должны, и он должен как-то подцеплять class из stats, но как я хз честно
},
character.spawn("BOAR")
:addBehavior {
@ -123,9 +123,14 @@ function love.draw()
love.graphics.setColor(1, 1, 1)
love.graphics.setFont(Tree.fonts:getTheme("Roboto_Mono"):getVariant("small"))
local mousePosX, mousePosY = love.mouse.getPosition()
local mousePos = Tree.level.camera:toWorldPosition(Vec3 { mousePosX, mousePosY }):floor()
local stats = "fps: " ..
love.timer.getFPS() ..
" lt: " .. lt .. " dt: " .. dt .. " mem: " .. string.format("%.2f MB", collectgarbage("count") / 1000)
" lt: " .. lt ..
" dt: " .. dt ..
" mem: " .. string.format("%.2f MB", collectgarbage("count") / 1000) ..
" mouse pos: " .. tostring(mousePos)
love.graphics.print(stats, 10, 10)
local t2 = love.timer.getTime()