add PositionedBehavior

This commit is contained in:
PeaAshMeter 2026-01-17 14:49:08 +03:00
parent 4aa470f443
commit 7ff7e47a90
13 changed files with 173 additions and 156 deletions

View File

@ -1,8 +1,10 @@
--- @meta _ --- @meta _
Tree.behaviors.map = require "lib.character.behaviors.map"
Tree.behaviors.spellcaster = require "lib.character.behaviors.spellcaster" Tree.behaviors.spellcaster = require "lib.character.behaviors.spellcaster"
Tree.behaviors.sprite = require "lib.character.behaviors.sprite" Tree.behaviors.sprite = require "lib.character.behaviors.sprite"
Tree.behaviors.stats = require "lib.character.behaviors.stats" Tree.behaviors.stats = require "lib.character.behaviors.stats"
Tree.behaviors.residentsleeper = require "lib.character.behaviors.residentsleeper" Tree.behaviors.residentsleeper = require "lib.character.behaviors.residentsleeper"
Tree.behaviors.shadowcaster = require "lib.character.behaviors.shadowcaster" Tree.behaviors.shadowcaster = require "lib.character.behaviors.shadowcaster"
Tree.behaviors.light = require "character.behaviors.light" Tree.behaviors.light = require "character.behaviors.light"
Tree.behaviors.positioned = require "character.behaviors.positioned"
Tree.behaviors.tiled = require "character.behaviors.tiled"
Tree.behaviors.cursor = require "character.behaviors.cursor"

View File

@ -0,0 +1,19 @@
--- Добавляет следование за курсором мыши
--- @class CursorBehavior : Behavior
local behavior = {}
behavior.__index = behavior
behavior.id = "cursor"
---@return CursorBehavior
function behavior.new()
return setmetatable({}, behavior)
end
function behavior:update()
self.owner:try(Tree.behaviors.positioned, function(b)
local mx, my = love.mouse.getX(), love.mouse.getY()
b.position = Tree.level.camera:toWorldPosition(Vec3 { mx, my })
end)
end
return behavior

View File

@ -1,38 +1,36 @@
--- @class LightBehavior : Behavior --- @class LightBehavior : Behavior
--- @field intensity number --- @field intensity number
--- @field color Vec3 --- @field color Vec3
--- @field position Vec3
--- @field seed integer --- @field seed integer
local behavior = {} local behavior = {}
behavior.__index = behavior behavior.__index = behavior
behavior.id = "light" behavior.id = "light"
---@param values {intensity: number?, color: Vec3?, position: Vec3?, seed: integer?} ---@param values {intensity: number?, color: Vec3?, seed: integer?}
---@return LightBehavior ---@return LightBehavior
function behavior.new(values) function behavior.new(values)
return setmetatable({ return setmetatable({
intensity = values.intensity or 1, intensity = values.intensity or 1,
color = values.color or Vec3 { 1, 1, 1 }, color = values.color or Vec3 { 1, 1, 1 },
position = values.position or Vec3 {},
seed = values.seed or math.random(math.pow(2, 16)) seed = values.seed or math.random(math.pow(2, 16))
}, behavior) }, behavior)
end end
function behavior:update() function behavior:update()
local mx, my = love.mouse.getX(), love.mouse.getY()
self.position = Tree.level.camera:toWorldPosition(Vec3 { mx, my })
end end
function behavior:draw() function behavior:draw()
local positioned = self.owner:has(Tree.behaviors.positioned)
if not positioned then return end
Tree.level.camera:attach() Tree.level.camera:attach()
love.graphics.setCanvas(Tree.level.render.textures.lightLayer) love.graphics.setCanvas(Tree.level.render.textures.lightLayer)
local shader = Tree.assets.files.shaders.light local shader = Tree.assets.files.shaders.light
shader:send("color", { self.color.x, self.color.y, self.color.z }) shader:send("color", { self.color.x, self.color.y, self.color.z })
shader:send("time", love.timer.getTime() + self.seed) shader:send("time", love.timer.getTime() + self.seed)
love.graphics.setShader(shader) love.graphics.setShader(shader)
-- love.graphics.setBlendMode("add") love.graphics.draw(Tree.assets.files.masks.circle128, positioned.position.x - self.intensity / 2,
love.graphics.draw(Tree.assets.files.masks.circle128, self.position.x - self.intensity / 2, positioned.position.y - self.intensity / 2, 0, self.intensity / 128,
self.position.y - self.intensity / 2, 0, self.intensity / 128,
self.intensity / 128) self.intensity / 128)
love.graphics.setBlendMode("alpha") love.graphics.setBlendMode("alpha")

View File

@ -1,91 +0,0 @@
local utils = require "lib.utils.utils"
--- Отвечает за размещение и перемещение по локации
--- @class MapBehavior : Behavior
--- @field position Vec3
--- @field runTarget Vec3 точка, в которую в данный момент бежит персонаж
--- @field displayedPosition Vec3 точка, в которой персонаж отображается
--- @field t0 number время начала движения для анимациии
--- @field path Deque путь, по которому сейчас бежит персонаж
--- @field animationNode? AnimationNode AnimationNode, с которым связана анимация перемещения
--- @field size Vec3
local mapBehavior = {}
mapBehavior.__index = mapBehavior
mapBehavior.id = "map"
--- @param position? Vec3
--- @param size? Vec3
function mapBehavior.new(position, size)
return setmetatable({
position = position or Vec3({}),
displayedPosition = position or Vec3({}),
size = size or Vec3({ 1, 1 }),
}, mapBehavior)
end
--- @param position Vec3
function mapBehavior:lookAt(position)
self.owner:try(Tree.behaviors.sprite,
function(sprite)
if position.x > self.displayedPosition.x then sprite.side = sprite.RIGHT end
-- (sic!)
if position.x < self.displayedPosition.x then sprite.side = sprite.LEFT end
end
)
end
--- @param path Deque
--- @param animationNode AnimationNode
function mapBehavior:followPath(path, animationNode)
if path:is_empty() then return animationNode:finish() end
self.animationNode = animationNode
self.position = self.displayedPosition
self.owner:try(Tree.behaviors.sprite, function(sprite)
sprite:loop("run")
end)
self.path = path;
---@type Vec3
local nextCell = path:peek_front()
self:runTo(nextCell)
path:pop_front()
end
--- @param target Vec3
function mapBehavior:runTo(target)
self.t0 = love.timer.getTime()
self.runTarget = target
self.owner:try(Tree.behaviors.sprite,
function(sprite)
if target.x < self.position.x then
sprite.side = Tree.behaviors.sprite.LEFT
elseif target.x > self.position.x then
sprite.side = Tree.behaviors.sprite.RIGHT
end
end
)
end
function mapBehavior:update(dt)
if self.runTarget then
local delta = love.timer.getTime() - self.t0 or love.timer.getTime()
local fraction = delta /
(0.5 * self.runTarget:subtract(self.position):length()) -- бежим одну клетку за 500 мс, по диагонали больше
if fraction >= 1 then -- анимация перемещена завершена
self.position = self.runTarget
if not self.path:is_empty() then -- еще есть, куда бежать
self:runTo(self.path:pop_front())
else -- мы добежали до финальной цели
self.owner:try(Tree.behaviors.sprite, function(sprite)
sprite:loop("idle")
end)
self.runTarget = nil
if self.animationNode then self.animationNode:finish() end
end
else -- анимация перемещения не завершена
self.displayedPosition = utils.lerp(self.position, self.runTarget, fraction) -- линейный интерполятор
end
end
end
return mapBehavior

View File

@ -0,0 +1,25 @@
--- Отвечает за размещение на уровне
--- @class PositionedBehavior : Behavior
--- @field position Vec3
local behavior = {}
behavior.__index = behavior
behavior.id = "positioned"
--- @param position? Vec3
function behavior.new(position)
return setmetatable({
position = position or Vec3({}),
}, behavior)
end
--- @param position Vec3
function behavior:lookAt(position)
self.owner:try(Tree.behaviors.sprite,
function(sprite)
if position.x > self.position.x then sprite.side = sprite.RIGHT end
if position.x < self.position.x then sprite.side = sprite.LEFT end
end
)
end
return behavior

View File

@ -8,11 +8,11 @@ function behavior.new() return setmetatable({}, behavior) end
function behavior:draw() function behavior:draw()
local sprite = self.owner:has(Tree.behaviors.sprite) local sprite = self.owner:has(Tree.behaviors.sprite)
local map = self.owner:has(Tree.behaviors.map) local positioned = self.owner:has(Tree.behaviors.positioned)
if not map then return end if not positioned then return end
local ppm = Tree.level.camera.pixelsPerMeter local ppm = Tree.level.camera.pixelsPerMeter
local position = map.displayedPosition + Vec3 { 0.5, 0.5 } local position = positioned.position + Vec3 { 0.5, 0.5 }
local lightIds = Tree.level.lightGrid:query(position, 5) local lightIds = Tree.level.lightGrid:query(position, 5)
--- @type Character[] --- @type Character[]
@ -37,7 +37,7 @@ function behavior:draw()
love.graphics.setCanvas(Tree.level.render.textures.spriteLightLayer) love.graphics.setCanvas(Tree.level.render.textures.spriteLightLayer)
love.graphics.setBlendMode("add") love.graphics.setBlendMode("add")
for _, light in ipairs(lights) do for _, light in ipairs(lights) do
local lightPos = light:has(Tree.behaviors.light).position local lightPos = light:has(Tree.behaviors.positioned).position
local lightVec = lightPos - position local lightVec = lightPos - position
local lightColor = light:has(Tree.behaviors.light).color local lightColor = light:has(Tree.behaviors.light).color

View File

@ -41,10 +41,10 @@ end
function sprite:draw() function sprite:draw()
if not self.animationTable[self.state] or not Tree.assets.files.sprites.character[self.state] then return end if not self.animationTable[self.state] or not Tree.assets.files.sprites.character[self.state] then return end
self.owner:try(Tree.behaviors.map, self.owner:try(Tree.behaviors.positioned,
function(map) function(pos)
local ppm = Tree.level.camera.pixelsPerMeter local ppm = Tree.level.camera.pixelsPerMeter
local position = map.displayedPosition + Vec3 { 0.5, 0.5 } local position = pos.position + Vec3 { 0.5, 0.5 }
love.graphics.setCanvas(Tree.level.render.textures.spriteLayer) love.graphics.setCanvas(Tree.level.render.textures.spriteLayer)
Tree.level.camera:attach() Tree.level.camera:attach()

View File

@ -0,0 +1,83 @@
local utils = require "lib.utils.utils"
--- Отвечает за перемещение по тайлам
--- @class TiledBehavior : Behavior
--- @field private runSource? Vec3 точка, из которой бежит персонаж
--- @field private runTarget? Vec3 точка, в которую в данный момент бежит персонаж
--- @field private path? Deque путь, по которому сейчас бежит персонаж
--- @field private animationNode? AnimationNode AnimationNode, с которым связана анимация перемещения
--- @field private t0 number время начала движения
--- @field size Vec3
local behavior = {}
behavior.__index = behavior
behavior.id = "tiled"
--- @param size? Vec3
function behavior.new(size)
return setmetatable({
size = size or Vec3({ 1, 1 }),
}, behavior)
end
--- @param path Deque
--- @param animationNode AnimationNode
function behavior:followPath(path, animationNode)
if path:is_empty() then return animationNode:finish() end
self.animationNode = animationNode
self.owner:try(Tree.behaviors.sprite, function(sprite)
sprite:loop("run")
end)
self.path = path;
---@type Vec3
local nextCell = path:peek_front()
self:runTo(nextCell)
path:pop_front()
end
--- @param target Vec3
function behavior:runTo(target)
local positioned = self.owner:has(Tree.behaviors.positioned)
if not positioned then return end
self.t0 = love.timer.getTime()
self.runTarget = target
self.runSource = positioned.position
self.owner:try(Tree.behaviors.sprite,
function(sprite)
if target.x < positioned.position.x then
sprite.side = Tree.behaviors.sprite.LEFT
elseif target.x > positioned.position.x then
sprite.side = Tree.behaviors.sprite.RIGHT
end
end
)
end
function behavior:update(dt)
if self.runTarget then
local positioned = self.owner:has(Tree.behaviors.positioned)
if not positioned then return end
local delta = love.timer.getTime() - self.t0 or love.timer.getTime()
local fraction = delta /
(0.5 * self.runTarget:subtract(self.runSource):length()) -- бежим одну клетку за 500 мс, по диагонали больше
if fraction >= 1 then -- анимация перемещена завершена
positioned.position = self.runTarget
if not self.path:is_empty() then -- еще есть, куда бежать
self:runTo(self.path:pop_front())
else -- мы добежали до финальной цели
self.owner:try(Tree.behaviors.sprite, function(sprite)
sprite:loop("idle")
end)
self.runTarget = nil
if self.animationNode then self.animationNode:finish() end
end
else -- анимация перемещения не завершена
positioned.position = utils.lerp(self.runSource, self.runTarget, fraction) -- линейный интерполятор
end
end
end
return behavior

View File

@ -12,12 +12,15 @@ function grid:add(id)
local character = Tree.level.characters[id] local character = Tree.level.characters[id]
if not character then return end if not character then return end
local mapB = character:has(Tree.behaviors.map) local positioned = character:has(Tree.behaviors.positioned)
if not mapB then return end if not positioned then return end
local centerX, centerY = math.floor(mapB.displayedPosition.x + 0.5), local tiled = character:has(Tree.behaviors.tiled)
math.floor(mapB.displayedPosition.y + 0.5) if not tiled then return end
local sizeX, sizeY = mapB.size.x, mapB.size.y
local centerX, centerY = math.floor(positioned.position.x + 0.5),
math.floor(positioned.position.y + 0.5)
local sizeX, sizeY = tiled.size.x, tiled.size.y
for y = centerY, centerY + sizeY - 1 do for y = centerY, centerY + sizeY - 1 do
for x = centerX, centerX + sizeX - 1 do for x = centerX, centerX + sizeX - 1 do
@ -30,11 +33,7 @@ end
--- @param b Character --- @param b Character
local function drawCmp(a, b) local function drawCmp(a, b)
--- @TODO: это захардкожено, надо разделить поведения --- @TODO: это захардкожено, надо разделить поведения
return (a:has(Tree.behaviors.map) and a:has(Tree.behaviors.map).displayedPosition.y or return a:has(Tree.behaviors.positioned).position.y < b:has(Tree.behaviors.positioned).position.y
a:has(Tree.behaviors.light).position.y)
<
(b:has(Tree.behaviors.map) and b:has(Tree.behaviors.map).displayedPosition.y or
b:has(Tree.behaviors.light).position.y)
end end
--- fills the grid with the actual data --- fills the grid with the actual data

View File

@ -15,9 +15,10 @@ function grid:add(id)
local lightB = character:has(Tree.behaviors.light) local lightB = character:has(Tree.behaviors.light)
if not lightB then return end if not lightB then return end
local positioned = character:has(Tree.behaviors.positioned)
if not positioned then return end
local key = tostring(Vec3 { positioned.position.x, positioned.position.y }:floor())
local key = tostring(Vec3 { lightB.position.x, lightB.position.y }:floor())
if not self.__grid[key] then self.__grid[key] = {} end if not self.__grid[key] then self.__grid[key] = {} end
table.insert(self.__grid[key], character.id) table.insert(self.__grid[key], character.id)
end end

View File

@ -49,11 +49,11 @@ function endTurnButton:onClick()
Tree.level.selector:select(nil) Tree.level.selector:select(nil)
local cid = Tree.level.turnOrder.current local cid = Tree.level.turnOrder.current
local playing = Tree.level.characters[cid] local playing = Tree.level.characters[cid]
if not playing:has(Tree.behaviors.map) then return end if not playing:has(Tree.behaviors.positioned) then return end
AnimationNode { AnimationNode {
function(node) function(node)
Tree.level.camera:animateTo(playing:has(Tree.behaviors.map).displayedPosition, node) Tree.level.camera:animateTo(playing:has(Tree.behaviors.positioned).position, node)
end, end,
duration = 1500, duration = 1500,
easing = easing.easeInOutCubic, easing = easing.easeInOutCubic,

View File

@ -51,7 +51,7 @@ function walk:cast(caster, target)
local sprite = caster:has(Tree.behaviors.sprite) local sprite = caster:has(Tree.behaviors.sprite)
if not sprite then return true end if not sprite then return true end
AnimationNode { AnimationNode {
function(node) caster:has(Tree.behaviors.map):followPath(path, node) end, function(node) caster:has(Tree.behaviors.tiled):followPath(path, node) end,
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end, onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
}:run() }:run()
@ -59,7 +59,7 @@ function walk:cast(caster, target)
end end
function walk:update(caster, dt) function walk:update(caster, dt)
local charPos = caster:has(Tree.behaviors.map).position:floor() local charPos = caster:has(Tree.behaviors.positioned).position:floor()
--- @type Vec3 --- @type Vec3
local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor() local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor()
self.path = require "lib.pathfinder" (charPos, mpos) self.path = require "lib.pathfinder" (charPos, mpos)
@ -104,8 +104,8 @@ local attack = setmetatable({}, spell)
attack.tag = "dev_attack" attack.tag = "dev_attack"
function attack:cast(caster, target) function attack:cast(caster, target)
if caster:try(Tree.behaviors.map, function(map) if caster:try(Tree.behaviors.positioned, function(p)
local dist = math.max(math.abs(map.position.x - target.x), math.abs(map.position.y - target.y)) local dist = math.max(math.abs(p.position.x - target.x), math.abs(p.position.y - target.y))
print("dist:", dist) print("dist:", dist)
return dist > 2 return dist > 2
end) then end) then
@ -128,7 +128,7 @@ function attack:cast(caster, target)
local targetSprite = targetCharacter:has(Tree.behaviors.sprite) local targetSprite = targetCharacter:has(Tree.behaviors.sprite)
if not sprite or not targetSprite then return true end if not sprite or not targetSprite then return true end
caster:try(Tree.behaviors.map, function(map) map:lookAt(target) end) caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) end)
AnimationNode { AnimationNode {
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end, onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,

View File

@ -17,7 +17,8 @@ function love.load()
:addBehavior { :addBehavior {
Tree.behaviors.residentsleeper.new(), Tree.behaviors.residentsleeper.new(),
Tree.behaviors.stats.new(nil, nil, 1), Tree.behaviors.stats.new(nil, nil, 1),
Tree.behaviors.map.new(), Tree.behaviors.positioned.new(Vec3 { 3, 3 }),
Tree.behaviors.tiled.new(),
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
Tree.behaviors.shadowcaster.new(), Tree.behaviors.shadowcaster.new(),
Tree.behaviors.spellcaster.new() Tree.behaviors.spellcaster.new()
@ -25,26 +26,9 @@ function love.load()
character.spawn("Baris") character.spawn("Baris")
:addBehavior { :addBehavior {
Tree.behaviors.residentsleeper.new(), Tree.behaviors.residentsleeper.new(),
Tree.behaviors.stats.new(nil, nil, 2), Tree.behaviors.stats.new(nil, nil, 1),
Tree.behaviors.map.new(Vec3 { 3, 3 }), Tree.behaviors.positioned.new(Vec3 { 5, 5 }),
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), Tree.behaviors.tiled.new(),
Tree.behaviors.shadowcaster.new(),
Tree.behaviors.spellcaster.new()
},
character.spawn("Foodor Jr")
:addBehavior {
Tree.behaviors.residentsleeper.new(),
Tree.behaviors.stats.new(nil, nil, 3),
Tree.behaviors.map.new(Vec3 { 0, 3 }),
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
Tree.behaviors.shadowcaster.new(),
Tree.behaviors.spellcaster.new()
},
character.spawn("Baris Jr")
:addBehavior {
Tree.behaviors.residentsleeper.new(),
Tree.behaviors.stats.new(nil, nil, 4),
Tree.behaviors.map.new(Vec3 { 0, 6 }),
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
Tree.behaviors.shadowcaster.new(), Tree.behaviors.shadowcaster.new(),
Tree.behaviors.spellcaster.new() Tree.behaviors.spellcaster.new()
@ -55,15 +39,12 @@ function love.load()
Tree.level.turnOrder:add(id) Tree.level.turnOrder:add(id)
end end
for i = 1, 1, 1 do
for j = 1, 1, 1 do
character.spawn("My Light") character.spawn("My Light")
:addBehavior { :addBehavior {
Tree.behaviors.light.new { color = Vec3 { 88, 34, 13 }, position = Vec3 { i, j } * 3, intensity = 10 } Tree.behaviors.light.new { color = Vec3 { 88, 34, 13 }, intensity = 10 },
Tree.behaviors.positioned.new(),
Tree.behaviors.cursor.new()
} }
end
end
Tree.level.turnOrder:endRound() Tree.level.turnOrder:endRound()
print("Now playing:", Tree.level.turnOrder.current) print("Now playing:", Tree.level.turnOrder.current)