diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index b5e7b82..725d4a8 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -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 + +--- @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 +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 -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 diff --git a/lib/character/behaviors/stats.lua b/lib/character/behaviors/stats.lua index 965d290..d377882 100644 --- a/lib/character/behaviors/stats.lua +++ b/lib/character/behaviors/stats.lua @@ -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 diff --git a/main.lua b/main.lua index a657dc8..1295cbd 100644 --- a/main.lua +++ b/main.lua @@ -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()