Compare commits
No commits in common. "main" and "feature/shadows" have entirely different histories.
main
...
feature/sh
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,47 +0,0 @@
|
||||
extern vec2 center;
|
||||
extern number time;
|
||||
extern number intensity;
|
||||
extern vec2 screenSize;
|
||||
|
||||
// Hexagon grid logic
|
||||
// Returns distance to nearest hex center
|
||||
float hexDist(vec2 p) {
|
||||
p = abs(p);
|
||||
float c = dot(p, normalize(vec2(1.0, 1.73)));
|
||||
c = max(c, p.x);
|
||||
return c;
|
||||
}
|
||||
|
||||
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
|
||||
// Normalize coordinates to -1..1, correcting for aspect ratio
|
||||
vec2 aspect = vec2(screenSize.x / screenSize.y, 1.0);
|
||||
vec2 uv = (screen_coords / screenSize.y) - (center / screenSize.y);
|
||||
|
||||
// Wave parameters
|
||||
float dist = length(uv);
|
||||
float speed = 5.0;
|
||||
float waveWidth = 0.1;
|
||||
float decay = 1.0 - smoothstep(0.0, 1.5, dist); // Decay over distance
|
||||
|
||||
// Calculate wave pulse
|
||||
float wavePhase = time * speed;
|
||||
float pulse = smoothstep(waveWidth, 0.0, abs(dist - mod(wavePhase, 2.0)));
|
||||
|
||||
// Hex grid pattern (visual only)
|
||||
vec2 hexUV = screen_coords * 0.05; // Scale grid
|
||||
// Basic hex grid approximation
|
||||
vec2 q = vec2(hexUV.x * 2.0/3.0, hexUV.y);
|
||||
|
||||
// Distortion
|
||||
vec2 distort = normalize(uv) * pulse * intensity * 0.02 * decay;
|
||||
vec2 finalUV = texture_coords - distort;
|
||||
|
||||
// Sample texture with distortion
|
||||
vec4 texColor = Texel(texture, finalUV);
|
||||
|
||||
// Add divine glow at the wavefront
|
||||
vec4 glowColor = vec4(1.0, 0.8, 0.4, 1.0); // Gold/Kitsune-fire color
|
||||
texColor += glowColor * pulse * decay * 0.5;
|
||||
|
||||
return texColor * color;
|
||||
}
|
||||
102
lib/animation_node.lua
Normal file
102
lib/animation_node.lua
Normal file
@ -0,0 +1,102 @@
|
||||
local easing = require "lib.utils.easing"
|
||||
|
||||
--- @alias voidCallback fun(): nil
|
||||
--- @alias animationRunner fun(node: AnimationNode)
|
||||
|
||||
--- Узел дерева анимаций.
|
||||
---
|
||||
--- Отслеживает завершение всех анимаций всех дочерних узлов и оповещает вышестоящий узел.
|
||||
---
|
||||
--- Дочерние узлы одного уровня запускают свою анимацию одновременно после завершения анимации родителя.
|
||||
--- Example:
|
||||
--- ```lua
|
||||
--- AnimationNode {
|
||||
--- function (node) residentsleeper:sleep(1000, node) end, -- must pass itself down as the parameter
|
||||
--- onEnd = function() print("completed") end,
|
||||
--- children = { -- children run in parallel after the parent animation is completed
|
||||
--- AnimationNode {
|
||||
--- function (node) sprite:animate("attack", node) end
|
||||
--- },
|
||||
--- AnimationNode {
|
||||
--- function (node) other_sprite:animate("hurt", node) end
|
||||
--- },
|
||||
--- }
|
||||
--- }:run()
|
||||
--- ```
|
||||
--- @class AnimationNode
|
||||
--- @field count integer
|
||||
--- @field run animationRunner
|
||||
--- @field parent AnimationNode?
|
||||
--- @field children AnimationNode[]
|
||||
--- @field finish voidCallback
|
||||
--- @field onEnd voidCallback?
|
||||
--- @field duration number продолжительность в миллисекундах
|
||||
--- @field easing ease функция смягчения
|
||||
--- @field t number прогресс анимации
|
||||
--- @field state "running" | "waiting" | "finished"
|
||||
local animation = {}
|
||||
animation.__index = animation
|
||||
|
||||
--- Регистрация завершения дочерней анимации
|
||||
function animation:bubbleUp()
|
||||
self.count = self.count - 1
|
||||
if self.count > 0 then return end
|
||||
self.state = "finished"
|
||||
if self.onEnd then self.onEnd() end
|
||||
if self.parent then self.parent:bubbleUp() end
|
||||
end
|
||||
|
||||
--- @param children AnimationNode[]
|
||||
--- Запланировать анимации после текущей, которые запустятся одновременно друг с другом
|
||||
function animation:chain(children)
|
||||
for _, child in ipairs(children) do
|
||||
child.parent = self
|
||||
table.insert(self.children, child)
|
||||
self.count = self.count + 1
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Возвращает текущий прогресс анимации с учетом смягчения
|
||||
function animation:getValue()
|
||||
return self.easing(self.t)
|
||||
end
|
||||
|
||||
function animation:update(dt)
|
||||
if self.state ~= "running" then return end
|
||||
|
||||
if self.t < 1 then
|
||||
self.t = self.t + dt * 1000 / self.duration -- в знаменателе продолжительность анимации в секундах
|
||||
else
|
||||
self.t = 1
|
||||
self:finish()
|
||||
end
|
||||
end
|
||||
|
||||
--- @param data {[1]: animationRunner?, onEnd?: voidCallback, duration: number?, easing: ease?, children?: AnimationNode[]}
|
||||
--- @return AnimationNode
|
||||
local function new(data)
|
||||
local t = setmetatable({}, animation)
|
||||
t.run = data[1] or function(self)
|
||||
self:finish()
|
||||
end
|
||||
t.onEnd = data.onEnd
|
||||
t.count = 1 -- своя анимация
|
||||
t.children = {}
|
||||
t:chain(data.children or {})
|
||||
t.duration = data.duration or 1000
|
||||
t.easing = data.easing or easing.linear
|
||||
t.t = 0
|
||||
t.state = "running"
|
||||
t.finish = function()
|
||||
if t.state ~= "running" then return end
|
||||
t.state = "waiting"
|
||||
t:bubbleUp()
|
||||
for _, anim in ipairs(t.children) do
|
||||
anim:run()
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
return new
|
||||
@ -8,6 +8,3 @@ 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"
|
||||
Tree.behaviors.ai = require "lib.character.behaviors.ai"
|
||||
|
||||
--- @alias voidCallback fun(): nil
|
||||
|
||||
@ -1,99 +0,0 @@
|
||||
local task = require "lib.utils.task"
|
||||
local ease = require "lib.utils.easing"
|
||||
|
||||
local EFFECTS_SUPPORTED = love.audio.isEffectsSupported()
|
||||
|
||||
--- @alias SourceFilter { type: "bandpass"|"highpass"|"lowpass", volume: number, highgain: number, lowgain: number }
|
||||
|
||||
--- @class Audio
|
||||
--- @field musicVolume number
|
||||
--- @field soundVolume number
|
||||
--- @field looped boolean
|
||||
--- @field from love.Source?
|
||||
--- @field to love.Source?
|
||||
audio = {}
|
||||
audio.__index = audio
|
||||
|
||||
--- здесь мы должны выгружать значения из файлика с сохранением настроек
|
||||
local function new(musicVolume, soundVolume)
|
||||
return setmetatable({
|
||||
musicVolume = musicVolume,
|
||||
soundVolume = soundVolume,
|
||||
looped = true
|
||||
}, audio)
|
||||
end
|
||||
|
||||
function audio:update(dt)
|
||||
if self.fader then
|
||||
local t = self.fader.value
|
||||
if self.from then self.from:setVolume(self.musicVolume * (1 - t)) end
|
||||
if self.to then self.to:setVolume(self.musicVolume * t) end
|
||||
end
|
||||
end
|
||||
|
||||
--- if from is nil, than we have fade in to;
|
||||
--- if to is nil, than we have fade out from
|
||||
---
|
||||
--- also we should guarantee, that from and to have the same volume
|
||||
--- @param from love.Source
|
||||
--- @param to love.Source
|
||||
--- @param ms number? in milliseconds
|
||||
function audio:crossfade(from, to, ms)
|
||||
print("[Audio]: Triggered crossfade")
|
||||
|
||||
-- Stop previous 'from' if it's dangling to avoid leaks
|
||||
if self.from and self.from ~= from and self.from ~= to then
|
||||
self.from:stop()
|
||||
end
|
||||
|
||||
self:play(to)
|
||||
to:setVolume(0)
|
||||
self.from = from
|
||||
self.to = to
|
||||
|
||||
-- Reuse fader object to allow task cancellation
|
||||
if not self.fader then self.fader = { value = 0 } end
|
||||
self.fader.value = 0
|
||||
|
||||
task.tween(self.fader, { value = 1 }, ms or 1000, ease.easeOutCubic)(function()
|
||||
if self.from then
|
||||
self.from:setVolume(0)
|
||||
self.from:stop()
|
||||
end
|
||||
if self.to then
|
||||
self.to:setVolume(self.musicVolume)
|
||||
end
|
||||
self.fader = nil
|
||||
print("[Audio]: Crossfade done")
|
||||
end)
|
||||
end
|
||||
|
||||
--- @param source love.Source
|
||||
--- @param settings SourceFilter?
|
||||
--- @param effectName string?
|
||||
function audio:play(source, settings, effectName)
|
||||
if source:getType() == "stream" then
|
||||
source:setLooping(self.looped)
|
||||
source:setVolume(self.musicVolume)
|
||||
source:play()
|
||||
else
|
||||
source:setVolume(self.soundVolume)
|
||||
source:play()
|
||||
end
|
||||
if settings and EFFECTS_SUPPORTED then
|
||||
source.setFilter(source, settings)
|
||||
end
|
||||
if effectName and EFFECTS_SUPPORTED then
|
||||
source:setEffect(effectName, true)
|
||||
end
|
||||
end
|
||||
|
||||
function audio:setMusicVolume(volume)
|
||||
self.musicVolume = volume
|
||||
end
|
||||
|
||||
function audio:setSoundVolume(volume)
|
||||
self.soundVolume = volume
|
||||
end
|
||||
|
||||
return { new = new }
|
||||
@ -1,64 +0,0 @@
|
||||
local easing = require "lib.utils.easing"
|
||||
|
||||
local function closestCharacter(char)
|
||||
local caster = Vec3 {}
|
||||
char:try(Tree.behaviors.positioned, function(b)
|
||||
caster = b.position
|
||||
end)
|
||||
local charTarget
|
||||
local minDist = 88005553535 -- spooky magic number
|
||||
for k, v in pairs(Tree.level.characters) do
|
||||
v:try(Tree.behaviors.positioned, function(b)
|
||||
local dist = ((caster.x - b.position.x) ^ 2 + (caster.y - b.position.y) ^ 2) ^ 0.5
|
||||
if dist < minDist and dist ~= 0 then
|
||||
minDist = dist
|
||||
charTarget = v
|
||||
end
|
||||
-- print(k, b.position)
|
||||
end)
|
||||
end
|
||||
return charTarget
|
||||
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()
|
||||
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
|
||||
|
||||
return behavior
|
||||
@ -1,10 +1,10 @@
|
||||
local task = require "lib.utils.task"
|
||||
|
||||
--- @class LightBehavior : Behavior
|
||||
--- @field intensity number
|
||||
--- @field color Vec3
|
||||
--- @field seed integer
|
||||
--- @field private animateColorTask? Task
|
||||
--- @field colorAnimationNode? AnimationNode
|
||||
--- @field targetColor? Vec3
|
||||
--- @field sourceColor? Vec3
|
||||
local behavior = {}
|
||||
behavior.__index = behavior
|
||||
behavior.id = "light"
|
||||
@ -20,12 +20,17 @@ function behavior.new(values)
|
||||
end
|
||||
|
||||
function behavior:update(dt)
|
||||
-- All logic moved to tasks
|
||||
if not self.colorAnimationNode then return end
|
||||
local delta = self.targetColor - self.sourceColor
|
||||
self.color = self.sourceColor + delta * self.colorAnimationNode:getValue()
|
||||
self.colorAnimationNode:update(dt)
|
||||
end
|
||||
|
||||
function behavior:animateColor(targetColor, duration, easing)
|
||||
-- If there's support for canceling tasks, we should do it here
|
||||
return task.tween(self, { color = targetColor }, duration or 800, easing)
|
||||
function behavior:animateColor(targetColor, animationNode)
|
||||
if self.colorAnimationNode then self.colorAnimationNode:finish() end
|
||||
self.colorAnimationNode = animationNode
|
||||
self.sourceColor = self.color
|
||||
self.targetColor = targetColor
|
||||
end
|
||||
|
||||
function behavior:draw()
|
||||
|
||||
@ -1,37 +1,21 @@
|
||||
--- Умеет асинхронно ждать какое-то время (для анимаций)
|
||||
--- @class ResidentSleeperBehavior : Behavior
|
||||
--- @field private t0 number?
|
||||
--- @field private sleepTime number?
|
||||
--- @field private callback voidCallback?
|
||||
--- @field private state 'running' | 'finished'
|
||||
--- @field animationNode? AnimationNode
|
||||
local behavior = {}
|
||||
behavior.__index = behavior
|
||||
behavior.id = "residentsleeper"
|
||||
|
||||
function behavior.new() return setmetatable({}, behavior) end
|
||||
|
||||
function behavior:update(_)
|
||||
if self.state ~= 'running' then return end
|
||||
|
||||
local t = love.timer.getTime()
|
||||
if t >= self.t0 + self.sleepTime then
|
||||
self.state = 'finished'
|
||||
self.callback()
|
||||
end
|
||||
function behavior:update(dt)
|
||||
if not self.animationNode then return end
|
||||
self.animationNode:update(dt)
|
||||
end
|
||||
|
||||
--- @return Task<nil>
|
||||
function behavior:sleep(ms)
|
||||
self.sleepTime = ms / 1000
|
||||
return function(callback)
|
||||
if self.state == 'running' then
|
||||
self.callback()
|
||||
end
|
||||
|
||||
self.t0 = love.timer.getTime()
|
||||
self.callback = callback
|
||||
self.state = 'running'
|
||||
end
|
||||
--- @param node AnimationNode
|
||||
function behavior:sleep(node)
|
||||
if self.animationNode then self.animationNode:finish() end
|
||||
self.animationNode = node
|
||||
end
|
||||
|
||||
return behavior
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
--- @class SpellcasterBehavior : Behavior
|
||||
--- @field spellbook Spell[] собственный набор спеллов персонажа
|
||||
--- @field cast Spell | nil ссылка на активный спелл из спеллбука
|
||||
--- @field cooldowns {[string]: integer} текущий кулдаун спеллов по тегам
|
||||
--- @field state "idle" | "casting" | "running"
|
||||
local behavior = {}
|
||||
behavior.__index = behavior
|
||||
behavior.id = "spellcaster"
|
||||
behavior.state = "idle"
|
||||
behavior.cooldowns = {}
|
||||
|
||||
---@param spellbook Spell[] | nil
|
||||
---@return SpellcasterBehavior
|
||||
@ -15,7 +13,6 @@ function behavior.new(spellbook)
|
||||
local spb = require "lib.spellbook" --- @todo временное добавление ходьбы (и читов) всем персонажам
|
||||
local t = {}
|
||||
t.spellbook = spellbook or spb.of { spb.walk, spb.regenerateMana, spb.attack }
|
||||
t.cooldowns = {}
|
||||
return setmetatable(t, behavior)
|
||||
end
|
||||
|
||||
@ -26,14 +23,6 @@ function behavior:endCast()
|
||||
Tree.level.selector:unlock()
|
||||
end
|
||||
|
||||
function behavior:processCooldowns()
|
||||
local cds = {}
|
||||
for tag, cd in pairs(self.cooldowns) do
|
||||
cds[tag] = (cd - 1) >= 0 and cd - 1 or 0
|
||||
end
|
||||
self.cooldowns = cds
|
||||
end
|
||||
|
||||
function behavior:update(dt)
|
||||
if Tree.level.selector:deselected() then
|
||||
self.state = "idle"
|
||||
|
||||
@ -69,21 +69,18 @@ function sprite:draw()
|
||||
)
|
||||
end
|
||||
|
||||
--- @return Task<nil>
|
||||
function sprite:animate(state)
|
||||
return function(callback)
|
||||
--- @param node AnimationNode
|
||||
function sprite:animate(state, node)
|
||||
if not self.animationGrid[state] then
|
||||
print("[SpriteBehavior]: no animation for '" .. state .. "'")
|
||||
callback()
|
||||
return print("[SpriteBehavior]: no animation for '" .. state .. "'")
|
||||
end
|
||||
self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED,
|
||||
function()
|
||||
self:loop("idle")
|
||||
callback()
|
||||
node:finish()
|
||||
end)
|
||||
self.state = state
|
||||
end
|
||||
end
|
||||
|
||||
function sprite:loop(state)
|
||||
if not self.animationGrid[state] then
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
local task = require "lib.utils.task"
|
||||
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
|
||||
@ -15,35 +20,29 @@ function behavior.new(size)
|
||||
end
|
||||
|
||||
--- @param path Deque
|
||||
--- @return Task<nil>
|
||||
function behavior:followPath(path)
|
||||
if path:is_empty() then return task.fromValue(nil) end
|
||||
|
||||
--- @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)
|
||||
|
||||
-- Рекурсивная функция для прохода по пути
|
||||
local function nextStep()
|
||||
if path:is_empty() then
|
||||
self.owner:try(Tree.behaviors.sprite, function(sprite)
|
||||
sprite:loop("idle")
|
||||
end)
|
||||
return task.fromValue(nil)
|
||||
end
|
||||
|
||||
local nextCell = path:pop_front()
|
||||
return task.chain(self:runTo(nextCell), nextStep)
|
||||
end
|
||||
|
||||
return nextStep()
|
||||
self.path = path;
|
||||
---@type Vec3
|
||||
local nextCell = path:peek_front()
|
||||
self:runTo(nextCell)
|
||||
path:pop_front()
|
||||
end
|
||||
|
||||
--- @param target Vec3
|
||||
--- @return Task<nil>
|
||||
function behavior:runTo(target)
|
||||
local positioned = self.owner:has(Tree.behaviors.positioned)
|
||||
if not positioned then return task.fromValue(nil) end
|
||||
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)
|
||||
@ -54,15 +53,31 @@ function behavior:runTo(target)
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
local distance = target:subtract(positioned.position):length()
|
||||
local duration = distance * 500 -- 500ms per unit
|
||||
|
||||
return task.tween(positioned, { position = target }, duration)
|
||||
end
|
||||
|
||||
function behavior:update(dt)
|
||||
-- Logic moved to tasks
|
||||
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
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
local Vec3 = require "lib.utils.vec3"
|
||||
local utils = require "lib.utils.utils"
|
||||
local task = require "lib.utils.task"
|
||||
local easing = require "lib.utils.easing"
|
||||
|
||||
local EPSILON = 0.001
|
||||
|
||||
@ -11,6 +9,9 @@ local EPSILON = 0.001
|
||||
--- @field speed number
|
||||
--- @field pixelsPerMeter integer
|
||||
--- @field scale number
|
||||
--- @field animationNode AnimationNode?
|
||||
--- @field animationEndPosition Vec3
|
||||
--- @field animationBeginPosition Vec3
|
||||
local camera = {
|
||||
position = Vec3 {},
|
||||
velocity = Vec3 {},
|
||||
@ -37,6 +38,12 @@ local controlMap = {
|
||||
}
|
||||
|
||||
function camera:update(dt)
|
||||
if self.animationNode and self.animationNode.state == "running" then
|
||||
self.animationNode:update(dt) -- тик анимации
|
||||
self.position = utils.lerp(self.animationBeginPosition, self.animationEndPosition, self.animationNode:getValue())
|
||||
return
|
||||
end
|
||||
|
||||
-------------------- зум на колесо ---------------------
|
||||
local y = Tree.controls.mouseWheelY
|
||||
if camera.scale > camera:getDefaultScale() * 5 and y > 0 then return end;
|
||||
@ -90,14 +97,14 @@ function camera:detach()
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
--- Плавно перемещает камеру к указанной точке.
|
||||
--- @param position Vec3
|
||||
--- @param duration number?
|
||||
--- @param easing function?
|
||||
--- @return Task
|
||||
function camera:animateTo(position, duration, easing)
|
||||
self.velocity = Vec3 {} -- Сбрасываем инерцию перед началом анимации
|
||||
return task.tween(self, { position = position }, duration or 1000, easing)
|
||||
--- @param animationNode AnimationNode
|
||||
function camera:animateTo(position, animationNode)
|
||||
if self.animationNode and self.animationNode.state ~= "finished" then self.animationNode:finish() end
|
||||
self.animationNode = animationNode
|
||||
self.animationEndPosition = position
|
||||
self.animationBeginPosition = self.position
|
||||
self.velocity = Vec3 {}
|
||||
end
|
||||
|
||||
--- @return Camera
|
||||
|
||||
@ -8,7 +8,7 @@ grid.__index = grid
|
||||
--- adds a value to the grid
|
||||
--- @param value any
|
||||
function grid:add(value)
|
||||
self.__grid[tostring(value.position)] = value
|
||||
grid[tostring(value.position)] = value
|
||||
end
|
||||
|
||||
--- @param position Vec3
|
||||
|
||||
@ -19,9 +19,6 @@ level.__index = level
|
||||
local function new(type, template)
|
||||
local size = Vec3 { 30, 30 } -- magic numbers for testing purposes only
|
||||
print(type, template, size)
|
||||
|
||||
Tree.audio:play(Tree.assets.files.audio.music.level1.battle)
|
||||
|
||||
return setmetatable({
|
||||
size = size,
|
||||
characters = {},
|
||||
@ -40,7 +37,6 @@ end
|
||||
function level:update(dt)
|
||||
utils.each(self.deadIds, function(id)
|
||||
self.characters[id] = nil
|
||||
self.turnOrder:remove(id)
|
||||
end)
|
||||
self.deadIds = {}
|
||||
|
||||
|
||||
@ -37,18 +37,10 @@ function selector:update(dt)
|
||||
if not selectedId then self:select(nil) end
|
||||
return
|
||||
end
|
||||
local task = b.cast:cast(char, mousePosition) -- в task функция, которая запускает анимацию спелла
|
||||
if not task then return end -- не получилось скастовать
|
||||
|
||||
if b.cast:cast(char, mousePosition) then
|
||||
self:lock()
|
||||
b.state = "running"
|
||||
|
||||
task(
|
||||
function(_) -- это коллбэк, который сработает по окончании анимации спелла
|
||||
b:endCast()
|
||||
if not char:has(Tree.behaviors.ai) then self:select(char.id) end -- выделяем персонажа обратно после того, как посмотрели на каст
|
||||
end
|
||||
)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
local PriorityQueue = require "lib.utils.priority_queue"
|
||||
local easing = require "lib.utils.easing"
|
||||
|
||||
local initiativeComparator = function(id_a, id_b)
|
||||
local res = Tree.level.characters[id_a]:try(Tree.behaviors.stats, function(astats)
|
||||
@ -30,47 +29,17 @@ end
|
||||
--- Перемещаем активного персонажа в очередь сходивших
|
||||
---
|
||||
--- Если в очереди на ход больше никого нет, заканчиваем раунд
|
||||
---
|
||||
--- Анимируем камеру к следующему персонажу. Если это ИИ, то активируем его логику.
|
||||
function turnOrder:next()
|
||||
self.actedQueue:insert(self.current)
|
||||
local next = self.pendingQueue:peek()
|
||||
if not next then self:endRound() else self.current = self.pendingQueue:pop() end
|
||||
|
||||
local char = Tree.level.characters[self.current]
|
||||
|
||||
Tree.level.selector:lock()
|
||||
char:try(Tree.behaviors.positioned, function(positioned)
|
||||
Tree.level.camera:animateTo(positioned.position, 1500, easing.easeInOutCubic)(
|
||||
function()
|
||||
if char:has(Tree.behaviors.ai) then
|
||||
char:has(Tree.behaviors.ai):makeTurn()(
|
||||
function()
|
||||
self:next()
|
||||
end)
|
||||
else
|
||||
Tree.level.selector:unlock()
|
||||
Tree.level.selector:select(self.current)
|
||||
end
|
||||
end
|
||||
)
|
||||
end)
|
||||
if not next then return self:endRound() end
|
||||
self.current = self.pendingQueue:pop()
|
||||
end
|
||||
|
||||
--- Производим действия в конце раунда
|
||||
---
|
||||
--- Меняем местами очередь сходивших и не сходивших (пустую)
|
||||
function turnOrder:endRound()
|
||||
assert(self.pendingQueue:size() == 0, "[TurnOrder]: tried to end the round before everyone had a turn")
|
||||
print("[TurnOrder]: end of the round")
|
||||
|
||||
for _, id in ipairs(self.actedQueue.data) do
|
||||
local char = Tree.level.characters[id]
|
||||
char:try(Tree.behaviors.spellcaster, function(spellcaster)
|
||||
spellcaster:processCooldowns()
|
||||
end)
|
||||
end
|
||||
|
||||
self.actedQueue, self.pendingQueue = self.pendingQueue, self.actedQueue
|
||||
self.current = self.pendingQueue:pop()
|
||||
end
|
||||
@ -139,29 +108,4 @@ function turnOrder:add(id)
|
||||
self.actedQueue:insert(id) -- новые персонажи по умолчанию попадают в очередь следующего хода
|
||||
end
|
||||
|
||||
--- Удалить персонажа из очереди хода (например, при смерти)
|
||||
--- @param id Id
|
||||
function turnOrder:remove(id)
|
||||
if self.current == id then
|
||||
self.current = self.pendingQueue:pop()
|
||||
if not self.current then
|
||||
self:endRound()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local function filterQueue(q, targetId)
|
||||
local newQ = PriorityQueue.new(initiativeComparator)
|
||||
for _, val in ipairs(q.data) do
|
||||
if val ~= targetId then
|
||||
newQ:insert(val)
|
||||
end
|
||||
end
|
||||
return newQ
|
||||
end
|
||||
|
||||
self.actedQueue = filterQueue(self.actedQueue, id)
|
||||
self.pendingQueue = filterQueue(self.pendingQueue, id)
|
||||
end
|
||||
|
||||
return { new = new }
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
-- --- @class Music
|
||||
-- --- @field source table<string, love.Source> audio streams, that supports multitrack (kind of)
|
||||
-- --- @field offset number
|
||||
-- music = {}
|
||||
-- music.__index = music
|
||||
|
||||
-- --- @param path string accepts path to dir with some music files (example: "main_ambient"; "player/theme1" and etc etc)
|
||||
-- local function new(path)
|
||||
-- local dir = Tree.assets.files.audio.music[path]
|
||||
-- --- @type table<string, love.Source>
|
||||
-- local source = {}
|
||||
-- print(dir)
|
||||
|
||||
-- for _, v in pairs(dir) do
|
||||
-- print(v.filename)
|
||||
-- source[v.filename] = v.source
|
||||
-- print(v.filename)
|
||||
-- end
|
||||
|
||||
-- print('[music]: new source: ', table.concat(source, ' '))
|
||||
|
||||
-- return setmetatable({ source = source, offset = 0 }, music)
|
||||
-- end
|
||||
|
||||
-- function music:update()
|
||||
-- for _, v in ipairs(self.source) do
|
||||
-- v:seek()
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- --- pause stemfile or music at all
|
||||
-- --- @param filename? string
|
||||
-- function music:pause(filename)
|
||||
-- if filename then
|
||||
-- self.source[filename]:pause()
|
||||
-- else
|
||||
-- for _, v in pairs(self.source) do
|
||||
-- v:pause()
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- --- play music stemfile by his name
|
||||
-- --- @param filename string
|
||||
-- --- @return boolean
|
||||
-- function music:play(filename)
|
||||
-- print('[music]: ', table.concat(self.source, ' '))
|
||||
-- self.source[filename]:seek(self.offset, "seconds")
|
||||
-- return self.source[filename]:play()
|
||||
-- end
|
||||
|
||||
-- return { new = new }
|
||||
@ -57,7 +57,7 @@ function barElement:draw()
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
--- текст поверх
|
||||
if self.drawText then
|
||||
local font = Tree.fonts:getDefaultTheme():getVariant("small")
|
||||
local font = Tree.fonts:getDefaultTheme():getVariant("medium")
|
||||
local t = love.graphics.newText(font, tostring(self.value) .. "/" .. tostring(self.maxValue))
|
||||
love.graphics.draw(t, math.floor(self.bounds.x + self.bounds.width / 2 - t:getWidth() / 2),
|
||||
math.floor(self.bounds.y + self.bounds.height / 2 - t:getHeight() / 2))
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
local task = require "lib.utils.task"
|
||||
local easing = require "lib.utils.easing"
|
||||
local AnimationNode = require "lib.animation_node"
|
||||
local Element = require "lib.simple_ui.element"
|
||||
local Rect = require "lib.simple_ui.rect"
|
||||
local SkillRow = require "lib.simple_ui.level.skill_row"
|
||||
@ -7,8 +7,7 @@ local Bars = require "lib.simple_ui.level.bottom_bars"
|
||||
local EndTurnButton = require "lib.simple_ui.level.end_turn"
|
||||
|
||||
--- @class CharacterPanel : UIElement
|
||||
--- @field animationTask Task
|
||||
--- @field alpha number
|
||||
--- @field animationNode AnimationNode
|
||||
--- @field state "show" | "idle" | "hide"
|
||||
--- @field skillRow SkillRow
|
||||
--- @field bars BottomBars
|
||||
@ -22,26 +21,41 @@ function characterPanel.new(characterId)
|
||||
t.skillRow = SkillRow(characterId)
|
||||
t.bars = Bars(characterId)
|
||||
t.endTurnButton = EndTurnButton {}
|
||||
t.alpha = 0 -- starts hidden/animating
|
||||
return setmetatable(t, characterPanel)
|
||||
end
|
||||
|
||||
function characterPanel:show()
|
||||
AnimationNode {
|
||||
function(animationNode)
|
||||
if self.animationNode then self.animationNode:finish() end
|
||||
self.animationNode = animationNode
|
||||
self.state = "show"
|
||||
self.animationTask = task.tween(self, { alpha = 1 }, 300, easing.easeOutCubic)
|
||||
self.animationTask(function() self.state = "idle" end)
|
||||
end,
|
||||
duration = 300,
|
||||
onEnd = function()
|
||||
self.state = "idle"
|
||||
end,
|
||||
easing = easing.easeOutCubic
|
||||
}:run()
|
||||
end
|
||||
|
||||
function characterPanel:hide()
|
||||
AnimationNode {
|
||||
function(animationNode)
|
||||
if self.animationNode then self.animationNode:finish() end
|
||||
self.animationNode = animationNode
|
||||
self.state = "hide"
|
||||
self.animationTask = task.tween(self, { alpha = 0 }, 300, easing.easeOutCubic)
|
||||
end,
|
||||
duration = 300,
|
||||
easing = easing.easeOutCubic
|
||||
}:run()
|
||||
end
|
||||
|
||||
--- @type love.Canvas
|
||||
local characterPanelCanvas;
|
||||
|
||||
function characterPanel:update(dt)
|
||||
-- Tasks update automatically via task.update(dt) in main.lua
|
||||
if self.animationNode then self.animationNode:update(dt) end
|
||||
self.skillRow:update(dt)
|
||||
self.bars.bounds = Rect {
|
||||
width = self.skillRow.bounds.width,
|
||||
@ -68,8 +82,14 @@ function characterPanel:update(dt)
|
||||
end
|
||||
|
||||
--- анимация появления
|
||||
local alpha = 1
|
||||
if self.state == "show" then
|
||||
alpha = self.animationNode:getValue()
|
||||
elseif self.state == "hide" then
|
||||
alpha = 1 - self.animationNode:getValue()
|
||||
end
|
||||
local revealShader = Tree.assets.files.shaders.reveal
|
||||
revealShader:send("t", self.alpha)
|
||||
revealShader:send("t", alpha)
|
||||
end
|
||||
|
||||
function characterPanel:draw()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
local Element = require "lib.simple_ui.element"
|
||||
local task = require "lib.utils.task"
|
||||
local AnimationNode = require "lib.animation_node"
|
||||
local easing = require "lib.utils.easing"
|
||||
|
||||
--- @class EndTurnButton : UIElement
|
||||
@ -22,7 +22,7 @@ function endTurnButton:update(dt)
|
||||
end
|
||||
|
||||
function endTurnButton:layout()
|
||||
local font = Tree.fonts:getDefaultTheme():getVariant("large")
|
||||
local font = Tree.fonts:getDefaultTheme():getVariant("headline")
|
||||
self.text = love.graphics.newText(font, "Завершить ход")
|
||||
self.bounds.width = self.text:getWidth() + 32
|
||||
self.bounds.height = self.text:getHeight() + 16
|
||||
@ -46,6 +46,19 @@ end
|
||||
|
||||
function endTurnButton:onClick()
|
||||
Tree.level.turnOrder:next()
|
||||
Tree.level.selector:select(nil)
|
||||
local cid = Tree.level.turnOrder.current
|
||||
local playing = Tree.level.characters[cid]
|
||||
if not playing:has(Tree.behaviors.positioned) then return end
|
||||
|
||||
AnimationNode {
|
||||
function(node)
|
||||
Tree.level.camera:animateTo(playing:has(Tree.behaviors.positioned).position, node)
|
||||
end,
|
||||
duration = 1500,
|
||||
easing = easing.easeInOutCubic,
|
||||
onEnd = function() Tree.level.selector:select(cid) end
|
||||
}:run()
|
||||
end
|
||||
|
||||
return function(values)
|
||||
|
||||
@ -8,7 +8,6 @@ local UI_SCALE = require "lib.simple_ui.level.scale"
|
||||
--- @field hovered boolean
|
||||
--- @field selected boolean
|
||||
--- @field onClick function?
|
||||
--- @field getCooldown function?
|
||||
--- @field icon? string
|
||||
local skillButton = setmetatable({}, Element)
|
||||
skillButton.__index = skillButton
|
||||
@ -19,11 +18,7 @@ function skillButton:update(dt)
|
||||
if self:hitTest(mx, my) then
|
||||
self.hovered = true
|
||||
if Tree.controls:isJustPressed("select") then
|
||||
local cd = self.getCooldown and self.getCooldown() or 0
|
||||
if cd == 0 then
|
||||
if self.onClick then self.onClick() end
|
||||
end
|
||||
|
||||
Tree.controls:consume("select")
|
||||
end
|
||||
else
|
||||
@ -34,7 +29,6 @@ end
|
||||
function skillButton:draw()
|
||||
love.graphics.setLineWidth(2)
|
||||
|
||||
local cd = self.getCooldown and self.getCooldown() or 0
|
||||
|
||||
if not self.icon then
|
||||
love.graphics.setColor(0.05, 0.05, 0.05)
|
||||
@ -59,25 +53,6 @@ function skillButton:draw()
|
||||
love.graphics.setColor(0.7, 1, 0.7, 0.5)
|
||||
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
|
||||
end
|
||||
|
||||
|
||||
if cd > 0 then
|
||||
love.graphics.setColor(0, 0, 0, 0.5)
|
||||
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
|
||||
|
||||
local font = Tree.fonts:getDefaultTheme():getVariant("headline")
|
||||
love.graphics.setColor(0, 0, 0)
|
||||
local t = love.graphics.newText(font, tostring(cd))
|
||||
love.graphics.draw(t, math.floor(self.bounds.x + 2 + self.bounds.width / 2 - t:getWidth() / 2),
|
||||
math.floor(self.bounds.y + 2 + self.bounds.height / 2 - t:getHeight() / 2))
|
||||
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
love.graphics.draw(t, math.floor(self.bounds.x + self.bounds.width / 2 - t:getWidth() / 2),
|
||||
math.floor(self.bounds.y + self.bounds.height / 2 - t:getHeight() / 2))
|
||||
else
|
||||
|
||||
end
|
||||
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
end
|
||||
|
||||
@ -117,9 +92,6 @@ function skillRow.new(characterId)
|
||||
behavior.cast = nil
|
||||
end
|
||||
end
|
||||
skb.getCooldown = function()
|
||||
return behavior.cooldowns[spell.tag] or 0
|
||||
end
|
||||
t.children[i] = skb
|
||||
end
|
||||
end)
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
-- --- @class Sound
|
||||
-- --- @field source love.Source just a sound
|
||||
-- sound = {}
|
||||
|
||||
-- local function new()
|
||||
-- return setmetatable({}, sound)
|
||||
-- end
|
||||
|
||||
-- return { new }
|
||||
@ -1,104 +0,0 @@
|
||||
local Query = require "lib.spell.target_query"
|
||||
local targetTest = require "lib.spell.target_test"
|
||||
local task = require "lib.utils.task"
|
||||
|
||||
--- @alias SpellPreview "default" Подсветка возможных целей
|
||||
--- | "path" Подсветка пути до цели
|
||||
|
||||
--- @class Spell
|
||||
--- @field tag string
|
||||
--- @field baseCost integer Базовые затраты маны на каст
|
||||
--- @field baseCooldown integer Базовый кулдаун в ходах
|
||||
--- @field targetQuery SpellTargetQuery Селектор возможных целей
|
||||
--- @field previewType SpellPreview Вид превью во время каста
|
||||
--- @field distance? integer Сторона квадрата с центром в позиции кастера, в пределах которого должна находиться цель, либо отсутствие ограничения
|
||||
--- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла
|
||||
--- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире
|
||||
--- @field cast fun(self: Spell, caster: Character, target: Vec3): Task<nil>? Вызывается в момент каста, изменяет мир.
|
||||
local spell = {}
|
||||
spell.__index = spell
|
||||
spell.tag = "spell_base"
|
||||
spell.baseCost = 1
|
||||
spell.baseCooldown = 1
|
||||
spell.targetQuery = Query(targetTest.any)
|
||||
spell.previewType = "default"
|
||||
|
||||
function spell:update(caster, dt)
|
||||
if self.previewType == "path" then
|
||||
local charPos = caster:has(Tree.behaviors.positioned).position:floor()
|
||||
--- @type Vec3
|
||||
local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor()
|
||||
if self.targetQuery.test(caster, mpos) then
|
||||
self.path = require "lib.pathfinder" (charPos, mpos)
|
||||
else
|
||||
self.path = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function spell:draw()
|
||||
if self.previewType == "path" then
|
||||
local path = self.path --[[@as Deque?]]
|
||||
if not path then return end
|
||||
--- Это отрисовка пути персонажа к мышке
|
||||
Tree.level.camera:attach()
|
||||
love.graphics.setCanvas(Tree.level.render.textures.overlayLayer)
|
||||
love.graphics.setColor(0.6, 0.75, 0.5)
|
||||
for p in path:values() do
|
||||
love.graphics.circle("fill", p.x + 0.45, p.y + 0.45, 0.1)
|
||||
end
|
||||
love.graphics.setCanvas()
|
||||
Tree.level.camera:detach()
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
function spell:cast(caster, target) return task.fromValue() end
|
||||
|
||||
--- Конструктор [Spell]
|
||||
--- @param data {tag: string, baseCost: integer, baseCooldown: integer, targetQuery: SpellTargetQuery?, previewType: SpellPreview?, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task<nil>?}
|
||||
--- @return Spell
|
||||
function spell.new(data)
|
||||
local newSpell = setmetatable({
|
||||
tag = data.tag,
|
||||
baseCost = data.baseCost,
|
||||
baseCooldown = data.baseCooldown,
|
||||
targetQuery = data.targetQuery,
|
||||
previewType = data.previewType,
|
||||
distance = data.distance
|
||||
}, spell)
|
||||
|
||||
newSpell.targetQuery = newSpell.distance
|
||||
and newSpell.targetQuery:intersect(Query(targetTest.distance(newSpell.distance)))
|
||||
or newSpell.targetQuery
|
||||
|
||||
function newSpell:cast(caster, target)
|
||||
if caster:try(Tree.behaviors.spellcaster, function(spellcaster) -- проверка на кулдаун
|
||||
return (spellcaster.cooldowns[self.tag] or 0) > 0
|
||||
end) then
|
||||
return
|
||||
end
|
||||
|
||||
if not self.targetQuery.test(caster, target) then return end -- проверка корректности цели
|
||||
|
||||
-- проверка на достаточное количество маны
|
||||
if caster:try(Tree.behaviors.stats, function(stats)
|
||||
return stats.mana < self.baseCost
|
||||
end) then
|
||||
return
|
||||
end
|
||||
|
||||
caster:try(Tree.behaviors.stats, function(stats)
|
||||
stats.mana = stats.mana - self.baseCost
|
||||
end)
|
||||
|
||||
caster:try(Tree.behaviors.spellcaster, function(spellcaster)
|
||||
spellcaster.cooldowns[self.tag] = self.baseCooldown
|
||||
end)
|
||||
return data.onCast(caster, target)
|
||||
end
|
||||
|
||||
return newSpell
|
||||
end
|
||||
|
||||
return spell
|
||||
@ -1,67 +0,0 @@
|
||||
--- Тип, отвечающий за выбор и фильтрацию подходящих тайлов как цели спелла
|
||||
--- теория множеств my beloved?
|
||||
--- @class SpellTargetQuery
|
||||
local query = {}
|
||||
query.__index = query
|
||||
|
||||
--- Проверяет координаты на соответствие внутреннему условию
|
||||
--- @param caster Character
|
||||
--- @param position Vec3
|
||||
--- @return boolean
|
||||
function query.test(caster, position)
|
||||
return true
|
||||
end
|
||||
|
||||
--- Объединение
|
||||
--- @param q SpellTargetQuery
|
||||
function query:join(q)
|
||||
return setmetatable({
|
||||
test = function(caster, pos)
|
||||
return self.test(caster, pos) or q.test(caster, pos)
|
||||
end
|
||||
}, query)
|
||||
end
|
||||
|
||||
--- Пересечение
|
||||
--- @param q SpellTargetQuery
|
||||
function query:intersect(q)
|
||||
return setmetatable({
|
||||
test = function(caster, pos)
|
||||
return self.test(caster, pos) and q.test(caster, pos)
|
||||
end
|
||||
}, query)
|
||||
end
|
||||
|
||||
--- Исключение (не коммутативное, "те, что есть в query, но нет в q")
|
||||
--- @param q SpellTargetQuery
|
||||
function query:exclude(q)
|
||||
return setmetatable({
|
||||
test = function(caster, pos)
|
||||
return self.test(caster, pos) and not q.test(caster, pos)
|
||||
end
|
||||
}, query)
|
||||
end
|
||||
|
||||
--- Находит все соответствующие условиям координаты тайлов и возвращает их в виде списка
|
||||
--- @param caster Character
|
||||
--- @return Vec3[]
|
||||
function query:asSet(caster)
|
||||
--- @TODO: оптимизировать и брать не всю карту для выборки
|
||||
local res = {}
|
||||
for _, tile in pairs(Tree.level.tileGrid) do
|
||||
if self.test(caster, tile.position) then
|
||||
table.insert(res, tile.position)
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
--- @param test SpellTargetTest
|
||||
local function new(test)
|
||||
return setmetatable({
|
||||
test = test
|
||||
}, query)
|
||||
end
|
||||
|
||||
return new
|
||||
@ -1,27 +0,0 @@
|
||||
--- @alias SpellTargetTest fun(caster: Character, targetPosition: Vec3) : boolean
|
||||
|
||||
return {
|
||||
-- любой тайл
|
||||
any = function() return true end,
|
||||
-- тайл, где находится кастующий
|
||||
caster = function(caster, targetPosition)
|
||||
local targetCharacterId = Tree.level.characterGrid:get(targetPosition)
|
||||
return caster.id == targetCharacterId
|
||||
end,
|
||||
-- тайл, где находится любой персонаж
|
||||
character = function(caster, targetPosition)
|
||||
local targetCharacterId = Tree.level.characterGrid:get(targetPosition)
|
||||
return not not targetCharacterId
|
||||
end,
|
||||
-- тайл в пределах окружности в нашей кривой метрике
|
||||
--- @param radius number
|
||||
distance = function(radius)
|
||||
return function(caster, targetPosition)
|
||||
return caster:try(Tree.behaviors.positioned, function(p)
|
||||
local dist = math.max(math.abs(p.position.x - targetPosition.x),
|
||||
math.abs(p.position.y - targetPosition.y))
|
||||
return dist <= radius
|
||||
end)
|
||||
end
|
||||
end
|
||||
}
|
||||
@ -7,80 +7,135 @@
|
||||
--- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла
|
||||
--- Да, это Future/Promise/await/async
|
||||
|
||||
local task = require 'lib.utils.task'
|
||||
local spell = require 'lib.spell.spell'
|
||||
local targetTest = require 'lib.spell.target_test'
|
||||
local Query = require "lib.spell.target_query"
|
||||
local AnimationNode = require "lib.animation_node"
|
||||
local easing = require "lib.utils.easing"
|
||||
|
||||
local walk = spell.new {
|
||||
tag = "dev_move",
|
||||
previewType = "path",
|
||||
baseCooldown = 1,
|
||||
baseCost = 2,
|
||||
targetQuery = Query(targetTest.any):exclude(Query(targetTest.character)),
|
||||
distance = 3,
|
||||
onCast = function(caster, target)
|
||||
local initialPos = caster:has(Tree.behaviors.positioned).position:floor()
|
||||
local path = require "lib.pathfinder" (initialPos, target)
|
||||
path:pop_front()
|
||||
if path:is_empty() then
|
||||
print("[Walk]: the path is empty", initialPos, target)
|
||||
return
|
||||
--- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell
|
||||
--- @field tag string
|
||||
--- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла
|
||||
--- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире
|
||||
--- @field cast fun(self: Spell, caster: Character, target: Vec3): boolean Вызывается в момент каста, изменяет мир. Возвращает bool в зависимости от того, получилось ли скастовать
|
||||
local spell = {}
|
||||
spell.__index = spell
|
||||
spell.tag = "base"
|
||||
|
||||
function spell:update(caster, dt) end
|
||||
|
||||
function spell:draw() end
|
||||
|
||||
function spell:cast(caster, target) return true end
|
||||
|
||||
local walk = setmetatable({
|
||||
--- @type Deque
|
||||
path = nil
|
||||
}, spell)
|
||||
walk.tag = "dev_move"
|
||||
|
||||
function walk:cast(caster, target)
|
||||
if not caster:try(Tree.behaviors.stats, function(stats)
|
||||
return stats.mana >= 2
|
||||
end) then
|
||||
return false
|
||||
end
|
||||
|
||||
local path = self.path
|
||||
path:pop_front()
|
||||
if path:is_empty() then return false end
|
||||
|
||||
for p in path:values() do print(p) end
|
||||
|
||||
caster:try(Tree.behaviors.stats, function(stats)
|
||||
stats.mana = stats.mana - 2
|
||||
print(stats.mana)
|
||||
end)
|
||||
|
||||
local sprite = caster:has(Tree.behaviors.sprite)
|
||||
assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE")
|
||||
if not sprite then
|
||||
return
|
||||
if not sprite then return true end
|
||||
AnimationNode {
|
||||
function(node) caster:has(Tree.behaviors.tiled):followPath(path, node) end,
|
||||
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
|
||||
}:run()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return caster:has(Tree.behaviors.tiled):followPath(path)
|
||||
function walk:update(caster, dt)
|
||||
local charPos = caster:has(Tree.behaviors.positioned).position:floor()
|
||||
--- @type Vec3
|
||||
local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor()
|
||||
self.path = require "lib.pathfinder" (charPos, mpos)
|
||||
end
|
||||
}
|
||||
|
||||
local regenerateMana = spell.new {
|
||||
tag = "dev_mana",
|
||||
baseCooldown = 2,
|
||||
baseCost = 0,
|
||||
targetQuery = Query(targetTest.caster),
|
||||
distance = 0,
|
||||
onCast = function(caster, target)
|
||||
function walk:draw()
|
||||
if not self.path then return end
|
||||
--- Это отрисовка пути персонажа к мышке
|
||||
Tree.level.camera:attach()
|
||||
love.graphics.setCanvas(Tree.level.render.textures.overlayLayer)
|
||||
love.graphics.setColor(0.6, 0.75, 0.5)
|
||||
for p in self.path:values() do
|
||||
love.graphics.circle("fill", p.x + 0.45, p.y + 0.45, 0.1)
|
||||
end
|
||||
love.graphics.setCanvas()
|
||||
Tree.level.camera:detach()
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
end
|
||||
|
||||
local regenerateMana = setmetatable({}, spell)
|
||||
regenerateMana.tag = "dev_mana"
|
||||
|
||||
function regenerateMana:cast(caster, target)
|
||||
caster:try(Tree.behaviors.stats, function(stats)
|
||||
stats.mana = 10
|
||||
stats.initiative = stats.initiative + 10
|
||||
end)
|
||||
|
||||
local sprite = caster:has(Tree.behaviors.sprite)
|
||||
if not sprite then return end
|
||||
print(caster.id, "has regenerated mana and gained initiative")
|
||||
local sprite = caster:has(Tree.behaviors.sprite)
|
||||
if not sprite then return true end
|
||||
|
||||
local light = require "lib/character/character".spawn("Light Effect")
|
||||
light:addBehavior {
|
||||
Tree.behaviors.light.new { color = Vec3 { 0.3, 0.3, 0.6 }, intensity = 4 },
|
||||
Tree.behaviors.light.new { color = Vec3 { 0.6, 0.3, 0.3 }, intensity = 4 },
|
||||
Tree.behaviors.residentsleeper.new(),
|
||||
Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }),
|
||||
}
|
||||
AnimationNode {
|
||||
function(node)
|
||||
sprite:animate("hurt", node)
|
||||
end,
|
||||
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end
|
||||
}:run()
|
||||
|
||||
return task.wait {
|
||||
task.chain(task.tween(light:has(Tree.behaviors.light) --[[@as LightBehavior]],
|
||||
{ intensity = 1, color = Vec3 {} }, 800, easing.easeInCubic), function()
|
||||
light:die()
|
||||
return task.fromValue()
|
||||
end),
|
||||
sprite:animate("hurt")
|
||||
}
|
||||
AnimationNode {
|
||||
function(node)
|
||||
light:has(Tree.behaviors.light):animateColor(Vec3 {}, node)
|
||||
end,
|
||||
easing = easing.easeInQuad,
|
||||
duration = 800,
|
||||
onEnd = function() light:die() end
|
||||
}:run()
|
||||
|
||||
return true
|
||||
end
|
||||
}
|
||||
|
||||
local attack = spell.new {
|
||||
tag = "dev_attack",
|
||||
baseCooldown = 1,
|
||||
baseCost = 2,
|
||||
targetQuery = Query(targetTest.character):exclude(Query(targetTest.caster)),
|
||||
distance = 1,
|
||||
onCast = function(caster, target)
|
||||
local attack = setmetatable({}, spell)
|
||||
attack.tag = "dev_attack"
|
||||
|
||||
function attack:cast(caster, target)
|
||||
if caster:try(Tree.behaviors.positioned, function(p)
|
||||
local dist = math.max(math.abs(p.position.x - target.x), math.abs(p.position.y - target.y))
|
||||
print("dist:", dist)
|
||||
return dist > 2
|
||||
end) then
|
||||
return false
|
||||
end
|
||||
|
||||
caster:try(Tree.behaviors.stats, function(stats)
|
||||
stats.mana = stats.mana - 2
|
||||
end)
|
||||
|
||||
--- @type Character
|
||||
local targetCharacterId = Tree.level.characterGrid:get(target)
|
||||
if not targetCharacterId or targetCharacterId == caster.id then return false end
|
||||
local targetCharacter = Tree.level.characters[targetCharacterId]
|
||||
targetCharacter:try(Tree.behaviors.stats, function(stats)
|
||||
stats.hp = stats.hp - 4
|
||||
@ -88,38 +143,36 @@ local attack = spell.new {
|
||||
|
||||
local sprite = caster:has(Tree.behaviors.sprite)
|
||||
local targetSprite = targetCharacter:has(Tree.behaviors.sprite)
|
||||
if not sprite or not targetSprite then return end
|
||||
if not sprite or not targetSprite then return true end
|
||||
|
||||
caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) end)
|
||||
|
||||
return
|
||||
task.wait {
|
||||
sprite:animate("attack"),
|
||||
task.wait {
|
||||
task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(500),
|
||||
function()
|
||||
local light = require "lib/character/character".spawn("Light Effect")
|
||||
light:addBehavior {
|
||||
Tree.behaviors.light.new { color = Vec3 { 0.6, 0.3, 0.3 }, intensity = 4 },
|
||||
Tree.behaviors.positioned.new(targetCharacter:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }),
|
||||
}
|
||||
return
|
||||
task.wait {
|
||||
task.chain(task.tween(light:has(Tree.behaviors.light) --[[@as LightBehavior]],
|
||||
{ intensity = 1, color = Vec3 {} }, 1000, easing.easeInCubic), function()
|
||||
light:die()
|
||||
return task.fromValue()
|
||||
end),
|
||||
targetSprite:animate("hurt")
|
||||
}
|
||||
AnimationNode {
|
||||
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
|
||||
children = {
|
||||
AnimationNode {
|
||||
function(node)
|
||||
sprite:animate("attack", node)
|
||||
end
|
||||
),
|
||||
},
|
||||
AnimationNode {
|
||||
function(node)
|
||||
targetCharacter:has(Tree.behaviors.residentsleeper):sleep(node)
|
||||
end,
|
||||
duration = 200,
|
||||
children = {
|
||||
AnimationNode {
|
||||
function(node)
|
||||
targetSprite:animate("hurt", node)
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}:run()
|
||||
|
||||
Tree.audio:play(Tree.assets.files.audio.sounds.hurt)
|
||||
}
|
||||
}
|
||||
return true
|
||||
end
|
||||
}
|
||||
|
||||
----------------------------------------
|
||||
local spellbook = {
|
||||
@ -133,7 +186,6 @@ local spellbook = {
|
||||
function spellbook.of(list)
|
||||
local spb = {}
|
||||
for i, sp in ipairs(list) do
|
||||
print(i)
|
||||
spb[i] = setmetatable({}, { __index = sp })
|
||||
end
|
||||
return spb
|
||||
|
||||
@ -9,11 +9,9 @@ Tree = {
|
||||
Tree.fonts = (require "lib.utils.font_manager"):load("WDXL_Lubrifont_TC"):loadTheme("Roboto_Mono") -- дефолтный шрифт
|
||||
Tree.panning = require "lib/panning"
|
||||
Tree.controls = require "lib.controls"
|
||||
Tree.audio = (require "lib.audio").new(1, 1)
|
||||
Tree.level = (require "lib.level.level").new("procedural", "flower_plains") -- для теста у нас только один уровень, который сразу же загружен
|
||||
|
||||
Tree.behaviors = (require "lib.utils.behavior_loader")("lib/character/behaviors") --- @todo написать нормальную загрузку поведений
|
||||
-- Tree.audio = (require "lib.audio").new(1, 1)
|
||||
-- Tree.behaviors.map = require "lib.character.behaviors.map"
|
||||
-- Tree.behaviors.spellcaster = require "lib.character.behaviors.spellcaster"
|
||||
-- Tree.behaviors.sprite = require "lib.character.behaviors.sprite"
|
||||
|
||||
@ -50,10 +50,6 @@ function AssetBundle.loadFile(path)
|
||||
return love.graphics.newShader(path);
|
||||
elseif (ext == "lua") then
|
||||
return require(string.gsub(path, ".lua", ""))
|
||||
elseif (ext == "ogg") and string.find(path, "sounds") then
|
||||
return love.audio.newSource(path, 'static')
|
||||
elseif (ext == "ogg") and string.find(path, "music") then
|
||||
return love.audio.newSource(path, 'stream')
|
||||
end
|
||||
return filedata
|
||||
end
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
--- @class Counter
|
||||
--- @field private count integer
|
||||
--- @field private onFinish fun(): nil
|
||||
--- @field private isAlive boolean
|
||||
--- @field push fun():nil добавить 1 к счетчику
|
||||
--- @field pop fun():nil убавить 1 у счетчика
|
||||
--- @field set fun(count: integer): nil установить значение на счетчике
|
||||
local counter = {}
|
||||
counter.__index = counter
|
||||
|
||||
|
||||
--- @private
|
||||
function counter:_push()
|
||||
self.count = self.count + 1
|
||||
end
|
||||
|
||||
--- @private
|
||||
function counter:_pop()
|
||||
self.count = self.count - 1
|
||||
if self.count == 0 and self.isAlive then
|
||||
self.isAlive = false
|
||||
self.onFinish()
|
||||
end
|
||||
end
|
||||
|
||||
--- @param onFinish fun(): nil
|
||||
local function new(onFinish)
|
||||
local t = {
|
||||
count = 0,
|
||||
onFinish = onFinish,
|
||||
isAlive = true,
|
||||
}
|
||||
t.push = function() counter._push(t) end
|
||||
t.pop = function() counter._pop(t) end
|
||||
t.set = function(count) t.count = count end
|
||||
|
||||
return setmetatable(t, counter)
|
||||
end
|
||||
|
||||
return new
|
||||
@ -4,11 +4,11 @@
|
||||
--- @field private _sizes {[FontVariant]: integer}
|
||||
local theme = {
|
||||
_sizes = {
|
||||
smallest = 12,
|
||||
small = 14,
|
||||
medium = 16,
|
||||
large = 22,
|
||||
headline = 32,
|
||||
smallest = 10,
|
||||
small = 12,
|
||||
medium = 14,
|
||||
large = 16,
|
||||
headline = 20,
|
||||
}
|
||||
}
|
||||
theme.__index = theme
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---@class PriorityQueue
|
||||
---@field data any[] внутренний массив-куча (индексация с 1)
|
||||
---@field private data any[] внутренний массив-куча (индексация с 1)
|
||||
---@field private cmp fun(a:any, b:any):boolean компаратор: true, если a выше по приоритету, чем b
|
||||
local PriorityQueue = {}
|
||||
PriorityQueue.__index = PriorityQueue
|
||||
|
||||
@ -1,142 +0,0 @@
|
||||
local easing_lib = require "lib.utils.easing"
|
||||
local lerp = require "lib.utils.utils".lerp
|
||||
|
||||
--- Обобщенная асинхронная функция (Task).
|
||||
--- По сути это функция, принимающая коллбэк: `fun(callback: fun(value: T): nil): nil`
|
||||
--- @generic T
|
||||
--- @alias Task fun(callback: fun(value: T): nil): nil
|
||||
|
||||
local task = {}
|
||||
local activeTweens = {} -- list of tweens
|
||||
-- We also need a way to track tweens by target to support cancellation/replacement
|
||||
-- Let's stick to a simple list for update, but maybe add a helper to find by target?
|
||||
-- Or, just allow multiple tweens per target (which is valid for different properties).
|
||||
-- But if we animate the SAME property, we have a conflict.
|
||||
-- For now, let's just add task.cancel(target) which kills ALL tweens for that target.
|
||||
|
||||
--- Обновление всех активных анимаций (твинов).
|
||||
--- Нужно вызывать в love.update(dt)
|
||||
function task.update(dt)
|
||||
for i = #activeTweens, 1, -1 do
|
||||
local t = activeTweens[i]
|
||||
if not t.cancelled then
|
||||
t.elapsed = t.elapsed + dt * 1000
|
||||
local progress = math.min(t.elapsed / t.duration, 1)
|
||||
local value = t.easing(progress)
|
||||
|
||||
for key, targetValue in pairs(t.properties) do
|
||||
t.target[key] = lerp(t.initial[key], targetValue, value)
|
||||
end
|
||||
|
||||
if progress >= 1 then
|
||||
t.completed = true
|
||||
table.remove(activeTweens, i)
|
||||
if t.resolve then t.resolve() end
|
||||
end
|
||||
else
|
||||
table.remove(activeTweens, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Возвращает Completer — объект, который позволяет вручную завершить таску.
|
||||
--- @generic T
|
||||
--- @return { complete: fun(val: T) }, Task<T> future
|
||||
function task.completer()
|
||||
local c = { completed = false, value = nil, cb = nil }
|
||||
function c:complete(val)
|
||||
if self.completed then return end
|
||||
self.completed = true
|
||||
self.value = val
|
||||
if self.cb then self.cb(val) end
|
||||
end
|
||||
|
||||
local future = function(callback)
|
||||
if c.completed then
|
||||
callback(c.value)
|
||||
else
|
||||
c.cb = callback
|
||||
end
|
||||
end
|
||||
return c, future
|
||||
end
|
||||
|
||||
--- Отменяет все активные твины для указанного объекта.
|
||||
--- @param target table
|
||||
function task.cancel(target)
|
||||
for _, t in ipairs(activeTweens) do
|
||||
if t.target == target then
|
||||
t.cancelled = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Создает таску, которая плавно меняет свойства объекта.
|
||||
--- Автоматически отменяет предыдущие твины для этого объекта (чтобы не было конфликтов).
|
||||
--- @param target table Объект, свойства которого меняем
|
||||
--- @param properties table Набор конечных значений { key = value }
|
||||
--- @param duration number Длительность в мс
|
||||
--- @param easing function? Функция смягчения (по умолчанию linear)
|
||||
--- @return Task<nil>
|
||||
function task.tween(target, properties, duration, easing)
|
||||
task.cancel(target) -- Cancel previous animations on this target
|
||||
|
||||
local initial = {}
|
||||
for k, _ in pairs(properties) do
|
||||
initial[k] = target[k]
|
||||
if type(initial[k]) == "table" and initial[k].copy then
|
||||
initial[k] = initial[k]:copy() -- Для Vec3
|
||||
end
|
||||
end
|
||||
|
||||
local comp, future = task.completer()
|
||||
table.insert(activeTweens, {
|
||||
target = target,
|
||||
initial = initial,
|
||||
properties = properties,
|
||||
duration = duration or 1000,
|
||||
easing = easing or easing_lib.linear,
|
||||
elapsed = 0,
|
||||
resolve = function() comp:complete() end
|
||||
})
|
||||
return future
|
||||
end
|
||||
|
||||
--- Возвращает таску, которая завершится сразу с переданным значением.
|
||||
function task.fromValue(val)
|
||||
return function(callback) callback(val) end
|
||||
end
|
||||
|
||||
--- Возвращает новый Task, который завершится после завершения всех переданных `tasks`.
|
||||
--- @param tasks Task[]
|
||||
--- @return Task<any[]>
|
||||
function task.wait(tasks)
|
||||
if #tasks == 0 then return task.fromValue({}) end
|
||||
local count = #tasks
|
||||
local results = {}
|
||||
|
||||
return function(callback)
|
||||
for i, t in ipairs(tasks) do
|
||||
t(function(result)
|
||||
results[i] = result
|
||||
count = count - 1
|
||||
if count == 0 then callback(results) end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Последовательно объединяет два `Task` в один.
|
||||
--- @param t Task
|
||||
--- @param onCompleted fun(value: any): Task
|
||||
--- @return Task
|
||||
function task.chain(t, onCompleted)
|
||||
return function(callback)
|
||||
t(function(value)
|
||||
local t2 = onCompleted(value)
|
||||
t2(callback)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return task
|
||||
33
main.lua
33
main.lua
@ -2,8 +2,6 @@
|
||||
|
||||
local character = require "lib/character/character"
|
||||
local testLayout
|
||||
local TestRunner = require "test.runner"
|
||||
TestRunner:register(require "test.task")
|
||||
|
||||
function love.conf(t)
|
||||
t.console = true
|
||||
@ -25,36 +23,15 @@ function love.load()
|
||||
Tree.behaviors.shadowcaster.new(),
|
||||
Tree.behaviors.spellcaster.new()
|
||||
},
|
||||
character.spawn("Foodor")
|
||||
:addBehavior {
|
||||
Tree.behaviors.residentsleeper.new(),
|
||||
Tree.behaviors.stats.new(nil, nil, 1),
|
||||
Tree.behaviors.positioned.new(Vec3 { 4, 3 }),
|
||||
Tree.behaviors.tiled.new(),
|
||||
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
||||
Tree.behaviors.shadowcaster.new(),
|
||||
Tree.behaviors.spellcaster.new()
|
||||
},
|
||||
character.spawn("Foodor")
|
||||
:addBehavior {
|
||||
Tree.behaviors.residentsleeper.new(),
|
||||
Tree.behaviors.stats.new(nil, nil, 3),
|
||||
Tree.behaviors.positioned.new(Vec3 { 5, 3 }),
|
||||
Tree.behaviors.tiled.new(),
|
||||
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
||||
Tree.behaviors.shadowcaster.new(),
|
||||
Tree.behaviors.spellcaster.new()
|
||||
},
|
||||
character.spawn("Baris")
|
||||
:addBehavior {
|
||||
Tree.behaviors.residentsleeper.new(),
|
||||
Tree.behaviors.stats.new(nil, nil, 2),
|
||||
Tree.behaviors.stats.new(nil, nil, 1),
|
||||
Tree.behaviors.positioned.new(Vec3 { 5, 5 }),
|
||||
Tree.behaviors.tiled.new(),
|
||||
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
||||
Tree.behaviors.shadowcaster.new(),
|
||||
Tree.behaviors.spellcaster.new(),
|
||||
Tree.behaviors.ai.new()
|
||||
Tree.behaviors.spellcaster.new()
|
||||
},
|
||||
}
|
||||
|
||||
@ -69,16 +46,12 @@ end
|
||||
|
||||
local lt = "0"
|
||||
function love.update(dt)
|
||||
TestRunner:update(dt) -- закомментировать для отключения тестов
|
||||
|
||||
local t1 = love.timer.getTime()
|
||||
require('lib.utils.task').update(dt)
|
||||
Tree.controls:poll()
|
||||
Tree.level.camera:update(dt) -- сначала логика камеры, потому что на нее завязан UI
|
||||
testLayout:update(dt) -- потом UI, потому что нужно перехватить жесты и не пустить их дальше
|
||||
Tree.panning:update(dt)
|
||||
Tree.level:update(dt)
|
||||
Tree.audio:update(dt)
|
||||
|
||||
Tree.controls:cache()
|
||||
|
||||
@ -111,7 +84,7 @@ function love.draw()
|
||||
testLayout:draw()
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
|
||||
love.graphics.setFont(Tree.fonts:getTheme("Roboto_Mono"):getVariant("small"))
|
||||
love.graphics.setFont(Tree.fonts:getTheme("Roboto_Mono"):getVariant("medium"))
|
||||
local stats = "fps: " ..
|
||||
love.timer.getFPS() ..
|
||||
" lt: " .. lt .. " dt: " .. dt .. " mem: " .. string.format("%.2f MB", collectgarbage("count") / 1000)
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
--- @class Test
|
||||
local test = {}
|
||||
function test:run(complete) end
|
||||
|
||||
function test:update(dt) end
|
||||
|
||||
--- @class TestRunner
|
||||
--- @field private tests Test[]
|
||||
--- @field private state "loading" | "running" | "completed"
|
||||
--- @field private completedCount integer
|
||||
local runner = {}
|
||||
runner.tests = {}
|
||||
runner.state = "loading"
|
||||
runner.completedCount = 0
|
||||
|
||||
--- глобальный update для тестов, нужен для тестирования фич, зависимых от времени
|
||||
function runner:update(dt)
|
||||
if self.state == "loading" then
|
||||
print("[TestRunner]: running " .. #self.tests .. " tests")
|
||||
|
||||
for _, t in ipairs(self.tests) do
|
||||
t:run(
|
||||
function()
|
||||
self.completedCount = self.completedCount + 1
|
||||
if self.completedCount == #self.tests then
|
||||
self.state = "completed"
|
||||
print("[TestRunner]: tests completed")
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
self.state = "running"
|
||||
end
|
||||
|
||||
for _, t in ipairs(self.tests) do
|
||||
if t.update then t:update(dt) end
|
||||
end
|
||||
end
|
||||
|
||||
--- добавляет тест для прохождения
|
||||
--- @param t Test
|
||||
function runner:register(t)
|
||||
table.insert(self.tests, t)
|
||||
end
|
||||
|
||||
return runner
|
||||
@ -1,75 +0,0 @@
|
||||
local task = require "lib.utils.task"
|
||||
|
||||
local test = {}
|
||||
|
||||
local t0
|
||||
local task1Start, task2Start
|
||||
local task1Callback, task2Callback
|
||||
|
||||
--- @return Task<number>
|
||||
local function task1()
|
||||
return function(callback)
|
||||
task1Start = love.timer.getTime()
|
||||
task1Callback = callback
|
||||
end
|
||||
end
|
||||
|
||||
--- @return Task<number>
|
||||
local function task2()
|
||||
return function(callback)
|
||||
task2Start = love.timer.getTime()
|
||||
task2Callback = callback
|
||||
end
|
||||
end
|
||||
|
||||
function test:run(complete)
|
||||
t0 = love.timer.getTime()
|
||||
|
||||
task.wait {
|
||||
task1(),
|
||||
task2()
|
||||
} (function(values)
|
||||
local tWait = love.timer.getTime()
|
||||
local dt = tWait - t0
|
||||
|
||||
local t1 = values[1]
|
||||
local t2 = values[2]
|
||||
|
||||
assert(type(t1) == "number" and type(t2) == "number")
|
||||
assert(t2 > t1)
|
||||
assert(dt >= 2, "dt = " .. dt)
|
||||
|
||||
print("task.wait completed in " .. dt .. " sec", "t1 = " .. t1 - t0, "t2 = " .. t2 - t0)
|
||||
|
||||
|
||||
t0 = love.timer.getTime()
|
||||
task.chain(task1(), function(value)
|
||||
t1 = value
|
||||
assert(t1 - t0 >= 1)
|
||||
return task2()
|
||||
end)(
|
||||
function(value)
|
||||
t2 = value
|
||||
assert(t2 - t0 >= 2)
|
||||
print("task.chain completed in " .. t2 - t0 .. " sec")
|
||||
|
||||
complete()
|
||||
end
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
function test:update(dt)
|
||||
local t = love.timer.getTime()
|
||||
if task1Start and t - task1Start >= 1 then
|
||||
task1Callback(t)
|
||||
task1Start = nil
|
||||
end
|
||||
|
||||
if task2Start and t - task2Start >= 2 then
|
||||
task2Callback(t)
|
||||
task2Start = nil
|
||||
end
|
||||
end
|
||||
|
||||
return test
|
||||
Loading…
x
Reference in New Issue
Block a user