From 46c7b46bd10a8dcb7ea42a8c2479f824a87642cd Mon Sep 17 00:00:00 2001 From: neckrat Date: Sun, 12 Apr 2026 23:10:20 +0300 Subject: [PATCH 1/9] added class stat --- lib/character/behaviors/stats.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 -- 2.47.2 From db8db450d05222b81c5b16fe92cc411f3597cead Mon Sep 17 00:00:00 2001 From: neckrat Date: Sun, 12 Apr 2026 23:21:50 +0300 Subject: [PATCH 2/9] ai maketurn, but in table --- lib/character/behaviors/ai.lua | 77 +++++++++++++++++++--------------- main.lua | 2 +- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index b5e7b82..18f58fe 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -20,45 +20,56 @@ local function closestCharacter(char) return charTarget end +---@type {[Class]: fun(self): Task} возможно где-то здесь на объявлении типа сломается типизация +local aiNature = { + ["dev_warrior"] = function(self) + 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) + + 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) + if task2 then + -- дергаем функцию после завершения хода + task2(callback) + else + callback() + end + end + ) + else + 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() - 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) - - 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) - if task2 then - -- дергаем функцию после завершения хода - task2(callback) - else - callback() - end - end - ) - else - callback() - end - end) - end +--- @param class Class +function behavior.new(class) + return setmetatable({ + makeTurn = aiNature[class] + }, behavior) end return behavior diff --git a/main.lua b/main.lua index 25db8f6..1097aeb 100644 --- a/main.lua +++ b/main.lua @@ -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, но как я хз честно }, } -- 2.47.2 From b6737e8f0b62505299dc734e1f414ea9c86803aa Mon Sep 17 00:00:00 2001 From: neckrat Date: Tue, 14 Apr 2026 19:25:00 +0300 Subject: [PATCH 3/9] some casual self typing --- lib/character/behaviors/ai.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index 18f58fe..64bc736 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -20,7 +20,7 @@ local function closestCharacter(char) return charTarget end ----@type {[Class]: fun(self): Task} возможно где-то здесь на объявлении типа сломается типизация +---@type {[Class]: fun(self: AIBehavior): Task} возможно где-то здесь на объявлении типа сломается типизация local aiNature = { ["dev_warrior"] = function(self) return function(callback) -- почему так, описано в Task -- 2.47.2 From bad4b494cd62126f01995485894cfc754d5a7bf2 Mon Sep 17 00:00:00 2001 From: neckrat Date: Wed, 15 Apr 2026 09:56:17 +0300 Subject: [PATCH 4/9] circleVectors function and some ai progress --- lib/character/behaviors/ai.lua | 54 ++++++++++++++++++++++++++++++---- main.lua | 6 ++-- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index 64bc736..4b94d69 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" +EPSILON = 0.01 + +--- @return Character local function closestCharacter(char) local caster = Vec3 {} char:try(Tree.behaviors.positioned, function(b) @@ -20,16 +25,54 @@ local function closestCharacter(char) return charTarget end +--- Возвращает все точки в радиусе в виде векторов +--- @param radius integer +local function circleVectors(radius) + local cam = Tree.level.camera + local vecs = {} + for t = 0, 2 * math.pi, EPSILON do + local x = math.sin(t) * radius + local y = math.cos(t) * radius + vecs[cam:toWorldPosition(Vec3 { x, y })] = true + end + return utils.keys(vecs) +end + +--- @param owner Character +--- @param space integer здесь мы должны сами определять, сколько должны не доходить до персонажа (1 <= n) +--- @return Vec3|nil +local function pathToClosestCharacter(owner, space) + 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 target = Vec3 {} + print(ownerPosition.position, targetPosition.position) + local path = pf(ownerPosition.position, targetPosition.position) + for c in path:values() do + print(c) + end + print(path) + space = math.min(space, path:size()) + print(space, path:size()) + for _ = 0, space - 1 do + path:pop_back() + end + if path:size() ~= 0 then + target = path:pop_back() + else + target = ownerPosition.position + end + print(target, targetPosition.position) + --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти + return target +end + ---@type {[Class]: fun(self: AIBehavior): Task} возможно где-то здесь на объявлении типа сломается типизация local aiNature = { ["dev_warrior"] = function(self) 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, 2) local task1 = spellB.spellbook[1]:cast(self.owner, self.target) if task1 then task1( @@ -46,6 +89,7 @@ local aiNature = { end ) else + print('рот этого казино') callback() end end) diff --git a/main.lua b/main.lua index 1097aeb..cedfd5d 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, 1 }), Tree.behaviors.tiled.new(), Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), Tree.behaviors.shadowcaster.new(), -- 2.47.2 From cdf68004da6191ed19ce4a513ff65206056f5932 Mon Sep 17 00:00:00 2001 From: neckrat Date: Wed, 15 Apr 2026 13:42:33 +0300 Subject: [PATCH 5/9] new circleVectors (midpoint circle algorithm) and pathToClosestCharacter function --- lib/character/behaviors/ai.lua | 100 +++++++++++++++++++++++---------- main.lua | 9 ++- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index 4b94d69..6258ea5 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -25,45 +25,85 @@ 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 -local function circleVectors(radius) - local cam = Tree.level.camera - local vecs = {} - for t = 0, 2 * math.pi, EPSILON do - local x = math.sin(t) * radius - local y = math.cos(t) * radius - vecs[cam:toWorldPosition(Vec3 { x, y })] = true +--- @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 - return utils.keys(vecs) + 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 vecs end --- @param owner Character ---- @param space integer здесь мы должны сами определять, сколько должны не доходить до персонажа (1 <= n) +--- @param radius integer здесь мы должны сами определять, сколько должны не доходить до персонажа (1 <= n) --- @return Vec3|nil -local function pathToClosestCharacter(owner, space) +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 target = Vec3 {} - print(ownerPosition.position, targetPosition.position) - local path = pf(ownerPosition.position, targetPosition.position) - for c in path:values() do - print(c) + + 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 - print(path) - space = math.min(space, path:size()) - print(space, path:size()) - for _ = 0, space - 1 do - path:pop_back() - end - if path:size() ~= 0 then - target = path:pop_back() - else - target = ownerPosition.position - end - print(target, targetPosition.position) - --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти return target end @@ -72,7 +112,7 @@ local aiNature = { ["dev_warrior"] = function(self) return function(callback) -- почему так, описано в Task self.owner:try(Tree.behaviors.spellcaster, function(spellB) - self.target = pathToClosestCharacter(self.owner, 2) + self.target = pathToClosestCharacter(self.owner, 1) local task1 = spellB.spellbook[1]:cast(self.owner, self.target) if task1 then task1( diff --git a/main.lua b/main.lua index cedfd5d..cdeae48 100644 --- a/main.lua +++ b/main.lua @@ -39,7 +39,7 @@ function love.load() :addBehavior { Tree.behaviors.residentsleeper.new(), Tree.behaviors.stats.new(nil, nil, 3), - Tree.behaviors.positioned.new(Vec3 { 7, 1 }), + Tree.behaviors.positioned.new(Vec3 { 7, 2 }), Tree.behaviors.tiled.new(), Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), Tree.behaviors.shadowcaster.new(), @@ -112,9 +112,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() -- 2.47.2 From a16b279e4447c839797a6812bb019c7a759eb2ae Mon Sep 17 00:00:00 2001 From: neckrat Date: Wed, 15 Apr 2026 14:53:07 +0300 Subject: [PATCH 6/9] i hate negative numbers --- lib/character/behaviors/ai.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index 6258ea5..b2bacad 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -75,7 +75,7 @@ local function circleVectors(center, radius) 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 + 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 @@ -112,7 +112,7 @@ local aiNature = { ["dev_warrior"] = function(self) return function(callback) -- почему так, описано в Task self.owner:try(Tree.behaviors.spellcaster, function(spellB) - self.target = pathToClosestCharacter(self.owner, 1) + self.target = pathToClosestCharacter(self.owner, 2) local task1 = spellB.spellbook[1]:cast(self.owner, self.target) if task1 then task1( -- 2.47.2 From 85883dfa7dbf6a7a3e9dbed56e487e0b1b228df1 Mon Sep 17 00:00:00 2001 From: neckrat Date: Wed, 15 Apr 2026 17:29:16 +0300 Subject: [PATCH 7/9] he's (ai) doing some stuff and i think its cool --- lib/character/behaviors/ai.lua | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index b2bacad..9a0dc47 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -86,6 +86,7 @@ local function circleVectors(center, radius) return vecs end +--- ищет пути к ближайшему персу в определённом радиусе --- @param owner Character --- @param radius integer здесь мы должны сами определять, сколько должны не доходить до персонажа (1 <= n) --- @return Vec3|nil @@ -112,18 +113,22 @@ local aiNature = { ["dev_warrior"] = function(self) return function(callback) -- почему так, описано в Task self.owner:try(Tree.behaviors.spellcaster, function(spellB) - self.target = pathToClosestCharacter(self.owner, 2) + 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 -- 2.47.2 From d1597d8ffa68f5cef1a950043ce242a6d1fba4fb Mon Sep 17 00:00:00 2001 From: neckrat Date: Thu, 16 Apr 2026 15:04:50 +0300 Subject: [PATCH 8/9] i dont need epsilon now --- lib/character/behaviors/ai.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index 9a0dc47..9402f8b 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -2,8 +2,6 @@ local easing = require "lib.utils.easing" local pf = require "lib.pathfinder" local utils = require "lib.utils.utils" -EPSILON = 0.01 - --- @return Character local function closestCharacter(char) local caster = Vec3 {} -- 2.47.2 From d7228cc3228bba5e635d35c975aa972036d1dc06 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Fri, 17 Apr 2026 00:29:53 +0300 Subject: [PATCH 9/9] slightly tweak the types in AIBehavior --- lib/character/behaviors/ai.lua | 41 +++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index 9402f8b..725d4a8 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -2,6 +2,8 @@ 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 {} @@ -106,9 +108,9 @@ local function pathToClosestCharacter(owner, radius) return target end ----@type {[Class]: fun(self: AIBehavior): Task} возможно где-то здесь на объявлении типа сломается типизация +--- @type table local aiNature = { - ["dev_warrior"] = function(self) + dev_warrior = function(self) return function(callback) -- почему так, описано в Task self.owner:try(Tree.behaviors.spellcaster, function(spellB) self.target = pathToClosestCharacter(self.owner, 1) @@ -138,7 +140,7 @@ local aiNature = { end) end end, - ["dev_mage"] = function(self) + dev_mage = function(self) return function(callback) print("etoh... bleh") callback() @@ -146,12 +148,45 @@ local aiNature = { end } + + --- @class AIBehavior : Behavior --- @field target Vec3? local behavior = {} behavior.__index = behavior behavior.id = "ai" +function behavior:dev_warrior() + 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 + --- @param class Class function behavior.new(class) return setmetatable({ -- 2.47.2