Compare commits
No commits in common. "main" and "feature-fonts" have entirely different histories.
main
...
feature-fo
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.
Binary file not shown.
|
Before Width: | Height: | Size: 572 B |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
@ -1,18 +0,0 @@
|
||||
extern vec2 direction; // (1.0, 0.0) для X, (0.0, 1.0) для Y
|
||||
extern number radius; // радиус размытия
|
||||
|
||||
vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords)
|
||||
{
|
||||
vec4 sum = vec4(0.0);
|
||||
float weightTotal = 0.0;
|
||||
|
||||
for (int i = -10; i <= 10; i++) {
|
||||
float offset = float(i);
|
||||
float weight = exp(-offset * offset / (2.0 * radius * radius));
|
||||
vec2 shift = direction * offset / love_ScreenSize.xy;
|
||||
sum += Texel(tex, texture_coords + shift) * weight;
|
||||
weightTotal += weight;
|
||||
}
|
||||
|
||||
return sum / weightTotal;
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
extern vec3 color;
|
||||
extern number time;
|
||||
|
||||
vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords)
|
||||
{
|
||||
vec4 texColor = Texel(tex, texture_coords);
|
||||
|
||||
float mask = texColor.r;
|
||||
|
||||
vec2 uv = texture_coords - 0.5;
|
||||
float dist = length(uv * 2.0);
|
||||
|
||||
float t = time;
|
||||
|
||||
float wave = sin((uv.x + uv.y) * 6.0 + t * 1.5) * 0.03;
|
||||
float ripple = sin(length(uv) * 20.0 - t * 2.0) * 0.02;
|
||||
float flicker = sin(t * 2.5) * 0.02;
|
||||
|
||||
dist += wave + ripple + flicker;
|
||||
|
||||
float intensity = 1.0 - smoothstep(0.0, 1.0, dist);
|
||||
intensity = pow(intensity, 2.0);
|
||||
|
||||
float colorShift = sin(t * 3.0) * 0.1;
|
||||
vec3 flickerColor = color + vec3(colorShift, colorShift * 0.5, -colorShift * 0.3);
|
||||
|
||||
vec3 finalColor = flickerColor * intensity * mask;
|
||||
|
||||
return vec4(finalColor, mask * intensity);
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
extern Image scene;
|
||||
extern Image light;
|
||||
|
||||
extern vec3 ambient;
|
||||
|
||||
vec4 effect(vec4 vcolor, Image unused, vec2 uv, vec2 px)
|
||||
{
|
||||
vec4 s = Texel(scene, uv);
|
||||
vec3 l = Texel(light, uv).rgb;
|
||||
|
||||
l = clamp(l, 0.0, 1.0);
|
||||
vec3 a = clamp(ambient, 0.0, 1.0);
|
||||
|
||||
// Канальный множитель: от ambient до 1 в зависимости от света
|
||||
vec3 m = a + (vec3(1.0) - a) * l;
|
||||
|
||||
vec3 rgb = s.rgb * m;
|
||||
|
||||
return vec4(rgb, s.a);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
local easing = require "lib.utils.easing"
|
||||
|
||||
--- @alias voidCallback fun(): nil
|
||||
--- @alias animationRunner fun(node: AnimationNode)
|
||||
|
||||
--- Узел дерева анимаций.
|
||||
@ -22,7 +23,6 @@ local easing = require "lib.utils.easing"
|
||||
--- }
|
||||
--- }:run()
|
||||
--- ```
|
||||
--- @deprecated
|
||||
--- @class AnimationNode
|
||||
--- @field count integer
|
||||
--- @field run animationRunner
|
||||
@ -73,7 +73,6 @@ function animation:update(dt)
|
||||
end
|
||||
end
|
||||
|
||||
--- @deprecated
|
||||
--- @param data {[1]: animationRunner?, onEnd?: voidCallback, duration: number?, easing: ease?, children?: AnimationNode[]}
|
||||
--- @return AnimationNode
|
||||
local function new(data)
|
||||
@ -90,7 +89,6 @@ local function new(data)
|
||||
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
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
--- @meta _
|
||||
Tree.behaviors.map = require "lib.character.behaviors.map"
|
||||
Tree.behaviors.spellcaster = require "lib.character.behaviors.spellcaster"
|
||||
Tree.behaviors.sprite = require "lib.character.behaviors.sprite"
|
||||
Tree.behaviors.stats = require "lib.character.behaviors.stats"
|
||||
Tree.behaviors.residentsleeper = require "lib.character.behaviors.residentsleeper"
|
||||
Tree.behaviors.shadowcaster = require "lib.character.behaviors.shadowcaster"
|
||||
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,91 +0,0 @@
|
||||
local ease = require "lib.utils.easing"
|
||||
local AnimationNode = require "lib.animation_node"
|
||||
|
||||
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 animationNode AnimationNode?
|
||||
--- @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 not self.animationNode then return end
|
||||
self.from:setVolume(self.musicVolume - self.animationNode:getValue() * self.musicVolume)
|
||||
self.to:setVolume(self.animationNode:getValue() * self.musicVolume)
|
||||
self.animationNode:update(dt)
|
||||
-- print(self.animationNode.t)
|
||||
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")
|
||||
self:play(to)
|
||||
to:setVolume(0)
|
||||
self.from = from
|
||||
self.to = to
|
||||
self.animationNode = AnimationNode {
|
||||
function(node) end,
|
||||
onEnd = function()
|
||||
self.from:setVolume(0)
|
||||
self.to:setVolume(self.musicVolume)
|
||||
self.from:stop()
|
||||
self.animationNode = nil
|
||||
print("[Audio]: Crossfade done")
|
||||
end,
|
||||
duration = ms or 1000,
|
||||
easing = ease.easeOutCubic,
|
||||
}
|
||||
self.animationNode:run()
|
||||
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,59 +0,0 @@
|
||||
local AnimationNode = require "lib.animation_node"
|
||||
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 animationNode AnimationNode?
|
||||
--- @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)
|
||||
|
||||
spellB.spellbook[1]:cast(self.owner, self.target)(function()
|
||||
-- здесь мы оказываемся после того, как сходили в первый раз
|
||||
print("[AI]: finished move 1")
|
||||
local newTarget = Vec3 { 1, 1 }
|
||||
-- поэтому позиция персонажа для нового каста пересчитается динамически
|
||||
spellB.spellbook[1]:cast(self.owner, newTarget)(function()
|
||||
print("[AI]: finished move 2")
|
||||
-- дергаем функцию после завершения хода
|
||||
callback()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return behavior
|
||||
@ -11,11 +11,6 @@ behavior.id = "behavior"
|
||||
|
||||
function behavior.new() return setmetatable({}, behavior) end
|
||||
|
||||
--- это деструктор с крутым названием
|
||||
function behavior:die()
|
||||
|
||||
end
|
||||
|
||||
function behavior:update(dt) end
|
||||
|
||||
function behavior:draw() end
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
--- Добавляет следование за курсором мыши
|
||||
--- @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
|
||||
@ -1,74 +0,0 @@
|
||||
local AnimationNode = require "lib.animation_node"
|
||||
local easing = require "lib.utils.easing"
|
||||
|
||||
--- @class LightBehavior : Behavior
|
||||
--- @field intensity number
|
||||
--- @field color Vec3
|
||||
--- @field seed integer
|
||||
--- @field colorAnimationNode? AnimationNode
|
||||
--- @field private animateColorCallback? fun(): nil
|
||||
--- @field targetColor? Vec3
|
||||
--- @field sourceColor? Vec3
|
||||
local behavior = {}
|
||||
behavior.__index = behavior
|
||||
behavior.id = "light"
|
||||
|
||||
---@param values {intensity: number?, color: Vec3?, seed: integer?}
|
||||
---@return LightBehavior
|
||||
function behavior.new(values)
|
||||
return setmetatable({
|
||||
intensity = values.intensity or 1,
|
||||
color = values.color or Vec3 { 1, 1, 1 },
|
||||
seed = values.seed or math.random(math.pow(2, 16))
|
||||
}, behavior)
|
||||
end
|
||||
|
||||
function behavior:update(dt)
|
||||
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
|
||||
|
||||
--- @TODO: refactor
|
||||
function behavior:animateColor(targetColor)
|
||||
if self.colorAnimationNode then self.colorAnimationNode:finish() end
|
||||
self.colorAnimationNode = AnimationNode {
|
||||
function(_) end,
|
||||
easing = easing.easeInQuad,
|
||||
duration = 800,
|
||||
onEnd = function()
|
||||
if self.animateColorCallback then self.animateColorCallback() end
|
||||
end
|
||||
}
|
||||
self.colorAnimationNode:run()
|
||||
self.sourceColor = self.color
|
||||
self.targetColor = targetColor
|
||||
|
||||
return function(callback)
|
||||
self.animateColorCallback = callback
|
||||
end
|
||||
end
|
||||
|
||||
function behavior:draw()
|
||||
local positioned = self.owner:has(Tree.behaviors.positioned)
|
||||
if not positioned then return end
|
||||
|
||||
Tree.level.camera:attach()
|
||||
love.graphics.setCanvas(Tree.level.render.textures.lightLayer)
|
||||
local shader = Tree.assets.files.shaders.light
|
||||
shader:send("color", { self.color.x, self.color.y, self.color.z })
|
||||
shader:send("time", love.timer.getTime() + self.seed)
|
||||
love.graphics.setShader(shader)
|
||||
love.graphics.draw(Tree.assets.files.masks.circle128, positioned.position.x - self.intensity / 2,
|
||||
positioned.position.y - self.intensity / 2, 0, self.intensity / 128,
|
||||
self.intensity / 128)
|
||||
|
||||
love.graphics.setBlendMode("alpha")
|
||||
|
||||
love.graphics.setShader()
|
||||
love.graphics.setCanvas()
|
||||
Tree.level.camera:detach()
|
||||
end
|
||||
|
||||
return behavior
|
||||
91
lib/character/behaviors/map.lua
Normal file
91
lib/character/behaviors/map.lua
Normal file
@ -0,0 +1,91 @@
|
||||
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
|
||||
@ -1,25 +0,0 @@
|
||||
--- Отвечает за размещение на уровне
|
||||
--- @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
|
||||
@ -1,37 +1,28 @@
|
||||
--- Умеет асинхронно ждать какое-то время (для анимаций)
|
||||
--- @class ResidentSleeperBehavior : Behavior
|
||||
--- @field private t0 number?
|
||||
--- @field private sleepTime number?
|
||||
--- @field private callback voidCallback?
|
||||
--- @field private state 'running' | 'finished'
|
||||
--- @field animationNode? AnimationNode
|
||||
--- @field endsAt? number
|
||||
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()
|
||||
function behavior:update(dt)
|
||||
if not self.animationNode then return end
|
||||
if love.timer.getTime() >= self.endsAt then
|
||||
self.animationNode:finish()
|
||||
self.animationNode = nil
|
||||
self.endsAt = nil
|
||||
end
|
||||
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 ms number time to wait in milliseconds
|
||||
--- @param node AnimationNode
|
||||
function behavior:sleep(ms, node)
|
||||
if self.animationNode then node:finish() end
|
||||
self.animationNode = node
|
||||
self.endsAt = love.timer.getTime() + ms / 1000
|
||||
end
|
||||
|
||||
return behavior
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
local easing = require "lib.utils.easing"
|
||||
--- @class ShadowcasterBehavior : Behavior
|
||||
local behavior = {}
|
||||
behavior.id = "shadowcaster"
|
||||
behavior.__index = behavior
|
||||
|
||||
function behavior.new() return setmetatable({}, behavior) end
|
||||
|
||||
function behavior:draw()
|
||||
local sprite = self.owner:has(Tree.behaviors.sprite)
|
||||
local positioned = self.owner:has(Tree.behaviors.positioned)
|
||||
if not positioned then return end
|
||||
|
||||
local ppm = Tree.level.camera.pixelsPerMeter
|
||||
local position = positioned.position + Vec3 { 0.5, 0.5 }
|
||||
|
||||
local lightIds = Tree.level.lightGrid:query(position, 5)
|
||||
--- @type Character[]
|
||||
local lights = {}
|
||||
for _, id in ipairs(lightIds) do
|
||||
table.insert(lights, Tree.level.characters[id])
|
||||
end
|
||||
|
||||
Tree.level.camera:attach()
|
||||
love.graphics.setCanvas(Tree.level.render.textures.shadowLayer)
|
||||
love.graphics.push()
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.translate(position.x, position.y)
|
||||
love.graphics.ellipse("fill", 0, 0, 0.2, 0.2 * math.cos(math.pi / 4))
|
||||
love.graphics.pop()
|
||||
|
||||
if not sprite then
|
||||
love.graphics.setCanvas()
|
||||
return
|
||||
end
|
||||
|
||||
love.graphics.setCanvas(Tree.level.render.textures.spriteLightLayer)
|
||||
love.graphics.setBlendMode("add")
|
||||
for _, light in ipairs(lights) do
|
||||
local lightPos = light:has(Tree.behaviors.positioned).position
|
||||
local lightVec = lightPos - position
|
||||
|
||||
local lightColor = light:has(Tree.behaviors.light).color
|
||||
if lightPos.y > position.y then
|
||||
love.graphics.setColor(lightColor.x, lightColor.y, lightColor.z,
|
||||
1 - 0.3 * lightVec:length())
|
||||
elseif position.y - lightPos.y < 3 then
|
||||
love.graphics.setColor(lightColor.x, lightColor.y, lightColor.z,
|
||||
(1 - easing.easeInSine((position.y - lightPos.y))) - 0.3 * lightVec:length())
|
||||
end
|
||||
|
||||
sprite.animationTable[sprite.state]:draw(Tree.assets.files.sprites.character[sprite.state],
|
||||
position.x,
|
||||
position.y, nil, 1 / ppm * sprite.side, 1 / ppm, 38, 47)
|
||||
end
|
||||
love.graphics.setBlendMode("alpha")
|
||||
|
||||
Tree.level.camera:detach()
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
love.graphics.setCanvas()
|
||||
end
|
||||
|
||||
return behavior
|
||||
@ -41,15 +41,10 @@ end
|
||||
function sprite:draw()
|
||||
if not self.animationTable[self.state] or not Tree.assets.files.sprites.character[self.state] then return end
|
||||
|
||||
self.owner:try(Tree.behaviors.positioned,
|
||||
function(pos)
|
||||
self.owner:try(Tree.behaviors.map,
|
||||
function(map)
|
||||
local ppm = Tree.level.camera.pixelsPerMeter
|
||||
local position = pos.position + Vec3 { 0.5, 0.5 }
|
||||
|
||||
love.graphics.setCanvas(Tree.level.render.textures.spriteLayer)
|
||||
Tree.level.camera:attach()
|
||||
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
local position = map.displayedPosition
|
||||
if Tree.level.selector.id == self.owner.id then
|
||||
local texW, texH = Tree.assets.files.sprites.character[self.state]:getWidth(),
|
||||
Tree.assets.files.sprites.character[self.state]:getHeight()
|
||||
@ -58,31 +53,27 @@ function sprite:draw()
|
||||
shader:send("time", love.timer:getTime())
|
||||
love.graphics.setShader(shader)
|
||||
end
|
||||
self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state],
|
||||
position.x,
|
||||
position.y, nil, 1 / ppm * self.side, 1 / ppm, 38, 47)
|
||||
|
||||
self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state],
|
||||
position.x + 0.5,
|
||||
position.y + 0.5, nil, 1 / ppm * self.side, 1 / ppm, 38, 47)
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
love.graphics.setShader()
|
||||
Tree.level.camera:detach()
|
||||
love.graphics.setCanvas()
|
||||
end
|
||||
)
|
||||
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)
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
local utils = require "lib.utils.utils"
|
||||
|
||||
--- Отвечает за перемещение по тайлам
|
||||
--- @class TiledBehavior : Behavior
|
||||
--- @field private runSource? Vec3 точка, из которой бежит персонаж
|
||||
--- @field private runTarget? Vec3 точка, в которую в данный момент бежит персонаж
|
||||
--- @field private path? Deque путь, по которому сейчас бежит персонаж
|
||||
--- @field private followPathCallback? fun()
|
||||
--- @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
|
||||
--- @return Task<nil>
|
||||
function behavior:followPath(path)
|
||||
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()
|
||||
|
||||
return function(callback)
|
||||
self.followPathCallback = callback
|
||||
end
|
||||
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.followPathCallback then
|
||||
self.followPathCallback()
|
||||
end
|
||||
end
|
||||
else -- анимация перемещения не завершена
|
||||
positioned.position = utils.lerp(self.runSource, self.runTarget, fraction) -- линейный интерполятор
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return behavior
|
||||
@ -13,7 +13,11 @@ character.__index = character
|
||||
|
||||
--- Создаёт персонажа, которым будет управлять или игрок или компьютер
|
||||
--- @param name string
|
||||
local function spawn(name)
|
||||
--- @param spriteDir table
|
||||
--- @param position? Vec3
|
||||
--- @param size? Vec3
|
||||
--- @param initiative? integer
|
||||
local function spawn(name, spriteDir, position, size, initiative)
|
||||
local char = {}
|
||||
|
||||
char = setmetatable(char, character)
|
||||
@ -22,6 +26,14 @@ local function spawn(name)
|
||||
char.behaviors = {}
|
||||
char._behaviorsIdx = {}
|
||||
|
||||
char:addBehavior {
|
||||
Tree.behaviors.residentsleeper.new(),
|
||||
Tree.behaviors.stats.new(nil, nil, initiative),
|
||||
Tree.behaviors.map.new(position, size),
|
||||
Tree.behaviors.sprite.new(spriteDir),
|
||||
Tree.behaviors.spellcaster.new()
|
||||
}
|
||||
|
||||
Tree.level.characters[char.id] = char
|
||||
return char
|
||||
end
|
||||
@ -80,18 +92,6 @@ function character:addBehavior(behaviors)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Добавляет персонажа в очередь на удаление.
|
||||
--- В конце фрейма он умирает. Ужасной смертью.
|
||||
---
|
||||
--- Ещё этот метод должен освобождать ресурсы в поведениях. Мы против утечек памяти!
|
||||
function character:die()
|
||||
for _, b in ipairs(self.behaviors) do
|
||||
if b.die then b:die() end
|
||||
end
|
||||
|
||||
table.insert(Tree.level.deadIds, self.id)
|
||||
end
|
||||
|
||||
function character:update(dt)
|
||||
for _, b in ipairs(self.behaviors) do
|
||||
if b.update then b:update(dt) end
|
||||
|
||||
@ -17,7 +17,7 @@ local camera = {
|
||||
velocity = Vec3 {},
|
||||
acceleration = 0.2,
|
||||
speed = 5,
|
||||
pixelsPerMeter = 32,
|
||||
pixelsPerMeter = 24,
|
||||
}
|
||||
|
||||
function camera:getDefaultScale()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -12,15 +12,12 @@ function grid:add(id)
|
||||
local character = Tree.level.characters[id]
|
||||
if not character then return end
|
||||
|
||||
local positioned = character:has(Tree.behaviors.positioned)
|
||||
if not positioned then return end
|
||||
local mapB = character:has(Tree.behaviors.map)
|
||||
if not mapB then return end
|
||||
|
||||
local tiled = character:has(Tree.behaviors.tiled)
|
||||
if not tiled then return end
|
||||
|
||||
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
|
||||
local centerX, centerY = math.floor(mapB.displayedPosition.x + 0.5),
|
||||
math.floor(mapB.displayedPosition.y + 0.5)
|
||||
local sizeX, sizeY = mapB.size.x, mapB.size.y
|
||||
|
||||
for y = centerY, centerY + sizeY - 1 do
|
||||
for x = centerX, centerX + sizeX - 1 do
|
||||
@ -32,8 +29,10 @@ end
|
||||
--- @param a Character
|
||||
--- @param b Character
|
||||
local function drawCmp(a, b)
|
||||
--- @TODO: это захардкожено, надо разделить поведения
|
||||
return a:has(Tree.behaviors.positioned).position.y < b:has(Tree.behaviors.positioned).position.y
|
||||
-- здесь персонажи гарантированно имеют нужное поведение
|
||||
return a:has(Tree.behaviors.map).displayedPosition.y
|
||||
<
|
||||
b:has(Tree.behaviors.map).displayedPosition.y
|
||||
end
|
||||
|
||||
--- fills the grid with the actual data
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
local utils = require "lib.utils.utils"
|
||||
--- Пометровая сетка источников света, чтобы быстро искать ближайшие для некоторого объекта
|
||||
--- @class LightGrid : Grid
|
||||
--- @field __grid {string: [Id]}
|
||||
local grid = setmetatable({}, require "lib.level.grid.base")
|
||||
grid.__index = grid
|
||||
|
||||
--- Adds a character id to the grid
|
||||
--- @private
|
||||
--- @param id Id
|
||||
function grid:add(id)
|
||||
local character = Tree.level.characters[id]
|
||||
if not character then return end
|
||||
|
||||
local lightB = character:has(Tree.behaviors.light)
|
||||
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())
|
||||
if not self.__grid[key] then self.__grid[key] = {} end
|
||||
table.insert(self.__grid[key], character.id)
|
||||
end
|
||||
|
||||
--- fills the grid with the actual data
|
||||
---
|
||||
--- should be called as early as possible during every tick
|
||||
function grid:reload()
|
||||
self:reset()
|
||||
utils.each(Tree.level.characters, function(c)
|
||||
self:add(c.id)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Возвращает все источники света, которые находятся в пределах круга с диаметром [distance] в [метрике Чебышёва](https://ru.wikipedia.org/wiki/Расстояние_Чебышёва)
|
||||
--- @param position Vec3
|
||||
--- @param distance integer
|
||||
function grid:query(position, distance)
|
||||
--- @type Id[]
|
||||
local res = {}
|
||||
local topLeft = position:subtract(Vec3 { distance / 2, distance / 2 }):floor()
|
||||
for i = 0, distance, 1 do
|
||||
for j = 0, distance, 1 do
|
||||
--- @type Id[]?
|
||||
local lights = self:get(topLeft:add(Vec3 { i, j }))
|
||||
if lights then
|
||||
for _, lightChar in ipairs(lights) do
|
||||
table.insert(res, lightChar)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- Generates an empty grid
|
||||
--- @return LightGrid
|
||||
local function new()
|
||||
return setmetatable({
|
||||
__grid = {}
|
||||
}, grid)
|
||||
end
|
||||
|
||||
return { new = new }
|
||||
@ -13,13 +13,9 @@ local function new(type, template, size)
|
||||
end
|
||||
|
||||
function map:draw()
|
||||
love.graphics.setCanvas(Tree.level.render.textures.floorLayer)
|
||||
Tree.level.camera:attach()
|
||||
utils.each(self.__grid, function(el)
|
||||
el:draw()
|
||||
end)
|
||||
Tree.level.camera:detach()
|
||||
love.graphics.setCanvas()
|
||||
end
|
||||
|
||||
return { new = new }
|
||||
|
||||
@ -3,49 +3,34 @@ local utils = require "lib.utils.utils"
|
||||
--- @class Level
|
||||
--- @field size Vec3
|
||||
--- @field characters Character[]
|
||||
--- @field deadIds Id[]
|
||||
--- @field characterGrid CharacterGrid
|
||||
--- @field lightGrid LightGrid
|
||||
--- @field selector Selector
|
||||
--- @field camera Camera
|
||||
--- @field tileGrid TileGrid
|
||||
--- @field turnOrder TurnOrder
|
||||
--- @field render Render
|
||||
local level = {}
|
||||
level.__index = level
|
||||
|
||||
local path = nil
|
||||
|
||||
--- @param type "procedural"|"handmaded"
|
||||
--- @param template Procedural|Handmaded
|
||||
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 = {},
|
||||
deadIds = {},
|
||||
characterGrid = (require "lib.level.grid.character_grid").new(),
|
||||
lightGrid = (require "lib.level.grid.light_grid").new(),
|
||||
tileGrid = (require "lib.level.grid.tile_grid").new(type, template, size),
|
||||
selector = (require "lib.level.selector").new(),
|
||||
camera = (require "lib.level.camera").new(),
|
||||
turnOrder = (require "lib.level.turn_order").new(),
|
||||
render = (require "lib.level.render").new {},
|
||||
weather = (require "lib.level.weather").new { ambientLight = Vec3 { 0.36, 0.42, 0.6 }, skyLight = Vec3 {} }
|
||||
}, level)
|
||||
end
|
||||
|
||||
function level:update(dt)
|
||||
utils.each(self.deadIds, function(id)
|
||||
self.characters[id] = nil
|
||||
self.turnOrder:remove(id)
|
||||
end)
|
||||
self.deadIds = {}
|
||||
|
||||
self.characterGrid:reload()
|
||||
self.lightGrid:reload()
|
||||
utils.each(self.characters, function(el)
|
||||
el:update(dt)
|
||||
end)
|
||||
@ -54,13 +39,10 @@ function level:update(dt)
|
||||
end
|
||||
|
||||
function level:draw()
|
||||
self.render:clear()
|
||||
self.tileGrid:draw()
|
||||
while not self.characterGrid.yOrderQueue:is_empty() do -- по сути это сортировка кучей за n log n
|
||||
self.characterGrid.yOrderQueue:pop():draw()
|
||||
end
|
||||
|
||||
self.render:draw()
|
||||
end
|
||||
|
||||
return {
|
||||
|
||||
@ -1,106 +0,0 @@
|
||||
--- @class Render
|
||||
--- @field textures table<string, love.Canvas>
|
||||
local render = {
|
||||
textures = {}
|
||||
}
|
||||
|
||||
function render:clear()
|
||||
local weather = Tree.level.weather
|
||||
local txs = self.textures
|
||||
love.graphics.setCanvas(txs.shadowLayer)
|
||||
love.graphics.clear()
|
||||
love.graphics.setCanvas(txs.spriteLayer)
|
||||
love.graphics.clear()
|
||||
love.graphics.setCanvas(txs.spriteLightLayer)
|
||||
love.graphics.clear(weather.skyLight.x, weather.skyLight.y, weather.skyLight.z)
|
||||
love.graphics.setCanvas(txs.floorLayer)
|
||||
love.graphics.clear()
|
||||
love.graphics.setCanvas(txs.lightLayer)
|
||||
love.graphics.clear(weather.skyLight.x, weather.skyLight.y, weather.skyLight.z)
|
||||
love.graphics.setCanvas(txs.overlayLayer)
|
||||
love.graphics.clear()
|
||||
end
|
||||
|
||||
function render:free()
|
||||
for _, tx in pairs(self.textures) do
|
||||
tx:release()
|
||||
end
|
||||
self.textures = nil
|
||||
end
|
||||
|
||||
--- TODO: это используется для блюра, должно кэшироваться и поддерживать ресайз
|
||||
|
||||
function render:applyBlur(input, radius)
|
||||
local blurShader = Tree.assets.files.shaders.blur
|
||||
|
||||
-- Горизонтальный проход
|
||||
blurShader:send("direction", { 1.0, 0.0 })
|
||||
blurShader:send("radius", radius)
|
||||
|
||||
self.textures.tmp1:renderTo(function()
|
||||
love.graphics.clear()
|
||||
love.graphics.setShader(blurShader)
|
||||
love.graphics.draw(input)
|
||||
love.graphics.setShader()
|
||||
end)
|
||||
|
||||
-- Вертикальный проход
|
||||
self.textures.tmp2:renderTo(
|
||||
function()
|
||||
love.graphics.clear()
|
||||
love.graphics.setShader(blurShader)
|
||||
blurShader:send("direction", { 0.0, 1.0 })
|
||||
love.graphics.draw(self.textures.tmp1)
|
||||
love.graphics.setShader()
|
||||
end
|
||||
)
|
||||
return self.textures.tmp2
|
||||
end
|
||||
|
||||
function render:draw()
|
||||
-- пол -> тени -> спрайты -> свет -> оверлей
|
||||
local weather = Tree.level.weather
|
||||
local txs = self.textures
|
||||
love.graphics.setCanvas(txs.lightLayer)
|
||||
love.graphics.draw(self:applyBlur(txs.shadowLayer, 4 * Tree.level.camera.scale))
|
||||
love.graphics.setCanvas()
|
||||
|
||||
-- self.lightLayer:newImageData():encode("png", "lightLayer.png")
|
||||
-- os.exit(0)
|
||||
|
||||
local lightShader = Tree.assets.files.shaders.light_postprocess
|
||||
lightShader:send("scene", txs.floorLayer)
|
||||
lightShader:send("light", self:applyBlur(txs.lightLayer, 2))
|
||||
lightShader:send("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z })
|
||||
love.graphics.setShader(lightShader)
|
||||
love.graphics.draw(txs.floorLayer)
|
||||
|
||||
|
||||
lightShader:send("scene", txs.spriteLayer)
|
||||
lightShader:send("light", txs.spriteLightLayer)
|
||||
love.graphics.draw(txs.spriteLayer)
|
||||
love.graphics.setShader()
|
||||
|
||||
love.graphics.draw(txs.overlayLayer)
|
||||
end
|
||||
|
||||
---@param params {w: number?, h: number?}
|
||||
---@return table|Render
|
||||
local function new(params)
|
||||
local w = params.w or love.graphics.getWidth()
|
||||
local h = params.h or love.graphics.getHeight()
|
||||
return setmetatable({
|
||||
textures = {
|
||||
shadowLayer = love.graphics.newCanvas(w, h),
|
||||
spriteLayer = love.graphics.newCanvas(w, h),
|
||||
spriteLightLayer = love.graphics.newCanvas(w, h),
|
||||
floorLayer = love.graphics.newCanvas(w, h),
|
||||
overlayLayer = love.graphics.newCanvas(w, h),
|
||||
lightLayer = love.graphics.newCanvas(w, h),
|
||||
tmp1 = love.graphics.newCanvas(w, h),
|
||||
tmp2 = love.graphics.newCanvas(w, h),
|
||||
}
|
||||
}, { __index = render })
|
||||
end
|
||||
|
||||
return { new = new }
|
||||
@ -37,16 +37,9 @@ function selector:update(dt)
|
||||
if not selectedId then self:select(nil) end
|
||||
return
|
||||
end
|
||||
local task = b.cast:cast(char, mousePosition) -- в task функция, которая запускает анимацию спелла
|
||||
if task then
|
||||
if b.cast:cast(char, mousePosition) then
|
||||
self:lock()
|
||||
b.state = "running"
|
||||
|
||||
task(
|
||||
function(_) -- это коллбэк, который сработает по окончании анимации спелла
|
||||
b:endCast()
|
||||
end
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@ -34,15 +34,6 @@ function turnOrder:next()
|
||||
local next = self.pendingQueue:peek()
|
||||
if not next then return self:endRound() end
|
||||
self.current = self.pendingQueue:pop()
|
||||
|
||||
local char = Tree.level.characters[self.current]
|
||||
char:try(Tree.behaviors.ai, function(ai)
|
||||
Tree.level.selector:lock()
|
||||
ai:makeTurn()(function()
|
||||
Tree.level.selector:unlock()
|
||||
self:next()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Меняем местами очередь сходивших и не сходивших (пустую)
|
||||
@ -117,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,12 +0,0 @@
|
||||
--- @class Weather
|
||||
--- @field skyLight Vec3
|
||||
--- @field ambientLight Vec3
|
||||
local weather = {}
|
||||
|
||||
--- @param proto Weather
|
||||
--- @return Weather
|
||||
local function new(proto)
|
||||
return setmetatable(proto, { __index = weather })
|
||||
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,10 +57,10 @@ function barElement:draw()
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
--- текст поверх
|
||||
if self.drawText then
|
||||
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))
|
||||
local font = Tree.fonts:getDefaultTheme():getVariant("small")
|
||||
love.graphics.setFont(font)
|
||||
love.graphics.printf(tostring(self.value) .. "/" .. tostring(self.maxValue), self.bounds.x,
|
||||
self.bounds.y, self.bounds.width, "center")
|
||||
end
|
||||
|
||||
self:drawBorder("inner")
|
||||
|
||||
@ -49,15 +49,15 @@ function endTurnButton:onClick()
|
||||
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
|
||||
if not playing:has(Tree.behaviors.map) then return end
|
||||
|
||||
AnimationNode {
|
||||
function(node)
|
||||
Tree.level.camera:animateTo(playing:has(Tree.behaviors.positioned).position, node)
|
||||
Tree.level.camera:animateTo(playing:has(Tree.behaviors.map).displayedPosition, node)
|
||||
end,
|
||||
duration = 1500,
|
||||
easing = easing.easeInOutCubic,
|
||||
onEnd = function() if not playing:has(Tree.behaviors.ai) then Tree.level.selector:select(cid) end end
|
||||
onEnd = function() Tree.level.selector:select(cid) end
|
||||
}:run()
|
||||
end
|
||||
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
-- --- @class Sound
|
||||
-- --- @field source love.Source just a sound
|
||||
-- sound = {}
|
||||
|
||||
-- local function new()
|
||||
-- return setmetatable({}, sound)
|
||||
-- end
|
||||
|
||||
-- return { new }
|
||||
@ -7,13 +7,13 @@
|
||||
--- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла
|
||||
--- Да, это Future/Promise/await/async
|
||||
|
||||
local task = require 'lib.utils.task'
|
||||
local AnimationNode = require "lib.animation_node"
|
||||
|
||||
--- @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): Task<nil> | nil Вызывается в момент каста, изменяет мир.
|
||||
--- @field cast fun(self: Spell, caster: Character, target: Vec3): boolean Вызывается в момент каста, изменяет мир. Возвращает bool в зависимости от того, получилось ли скастовать
|
||||
local spell = {}
|
||||
spell.__index = spell
|
||||
spell.tag = "base"
|
||||
@ -22,7 +22,7 @@ function spell:update(caster, dt) end
|
||||
|
||||
function spell:draw() end
|
||||
|
||||
function spell:cast(caster, target) return end
|
||||
function spell:cast(caster, target) return true end
|
||||
|
||||
local walk = setmetatable({
|
||||
--- @type Deque
|
||||
@ -34,32 +34,32 @@ function walk:cast(caster, target)
|
||||
if not caster:try(Tree.behaviors.stats, function(stats)
|
||||
return stats.mana >= 2
|
||||
end) then
|
||||
return
|
||||
return false
|
||||
end
|
||||
|
||||
local initialPos = caster:has(Tree.behaviors.positioned).position:floor()
|
||||
local path = require "lib.pathfinder" (initialPos, target)
|
||||
local path = self.path
|
||||
path:pop_front()
|
||||
if path:is_empty() then
|
||||
print("[Walk]: the path is empty", initialPos, target)
|
||||
return
|
||||
end
|
||||
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
|
||||
end
|
||||
if not sprite then return true end
|
||||
AnimationNode {
|
||||
function(node) caster:has(Tree.behaviors.map):followPath(path, node) end,
|
||||
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
|
||||
}:run()
|
||||
|
||||
return caster:has(Tree.behaviors.tiled):followPath(path)
|
||||
return true
|
||||
end
|
||||
|
||||
function walk:update(caster, dt)
|
||||
local charPos = caster:has(Tree.behaviors.positioned).position:floor()
|
||||
local charPos = caster:has(Tree.behaviors.map).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)
|
||||
@ -68,14 +68,10 @@ end
|
||||
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
|
||||
|
||||
@ -87,34 +83,29 @@ function regenerateMana:cast(caster, target)
|
||||
stats.mana = 10
|
||||
stats.initiative = stats.initiative + 10
|
||||
end)
|
||||
|
||||
local sprite = caster:has(Tree.behaviors.sprite)
|
||||
if not sprite then return nil end
|
||||
print(caster.id, "has regenerated mana and gained initiative")
|
||||
local sprite = caster:has(Tree.behaviors.sprite)
|
||||
if not sprite then return true end
|
||||
AnimationNode {
|
||||
function(node)
|
||||
sprite:animate("hurt", node)
|
||||
end,
|
||||
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end
|
||||
}:run()
|
||||
|
||||
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.residentsleeper.new(),
|
||||
Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }),
|
||||
}
|
||||
|
||||
return task.wait {
|
||||
light:has(Tree.behaviors.light):animateColor(Vec3 {}),
|
||||
sprite:animate("hurt")
|
||||
}
|
||||
return true
|
||||
end
|
||||
|
||||
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))
|
||||
if caster:try(Tree.behaviors.map, function(map)
|
||||
local dist = math.max(math.abs(map.position.x - target.x), math.abs(map.position.y - target.y))
|
||||
print("dist:", dist)
|
||||
return dist > 2
|
||||
end) then
|
||||
return
|
||||
return false
|
||||
end
|
||||
|
||||
caster:try(Tree.behaviors.stats, function(stats)
|
||||
@ -123,7 +114,7 @@ function attack:cast(caster, target)
|
||||
|
||||
--- @type Character
|
||||
local targetCharacterId = Tree.level.characterGrid:get(target)
|
||||
if not targetCharacterId or targetCharacterId == caster.id then return end
|
||||
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
|
||||
@ -131,20 +122,34 @@ function attack:cast(caster, target)
|
||||
|
||||
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)
|
||||
caster:try(Tree.behaviors.map, function(map) map:lookAt(target) end)
|
||||
|
||||
return
|
||||
task.wait {
|
||||
sprite:animate("attack"),
|
||||
task.wait {
|
||||
task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200),
|
||||
function() return targetSprite:animate("hurt") end
|
||||
),
|
||||
Tree.audio:play(Tree.assets.files.audio.sounds.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(200, node)
|
||||
end,
|
||||
children = {
|
||||
AnimationNode {
|
||||
function(node)
|
||||
targetSprite:animate("hurt", node)
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}:run()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
@ -159,7 +164,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
|
||||
@ -1,83 +0,0 @@
|
||||
--- Обобщенная асинхронная функция
|
||||
---
|
||||
--- Использование в общих чертах выглядит так:
|
||||
--- ```lua
|
||||
--- local multiplyByTwoCallback = nil
|
||||
--- local n = nil
|
||||
--- local function multiplyByTwoAsync(number)
|
||||
--- -- императивно сохраняем/обрабатываем параметр
|
||||
--- n = number
|
||||
--- return function(callback) -- это функция, которая запускает задачу
|
||||
--- multiplyByTwoCallback = callback
|
||||
--- end
|
||||
--- end
|
||||
---
|
||||
--- local function update(dt)
|
||||
--- --- ждем нужного момента времени...
|
||||
---
|
||||
--- if multiplyByTwoCallback then -- завершаем вычисление
|
||||
--- local result = n * 2
|
||||
--- multiplyByTwoCallback(result) -- результат асинхронного вычисления идет в параметр коллбека!
|
||||
--- multiplyByTwoCallback = nil
|
||||
--- end
|
||||
--- end
|
||||
---
|
||||
---
|
||||
--- --- потом это можно вызывать так:
|
||||
--- local task = multiplyByTwoAsync(21)
|
||||
--- -- это ленивое вычисление, так что в этот момент ничего не произойдет
|
||||
--- -- запускаем
|
||||
--- task(
|
||||
--- function(result) print(result) end -- выведет 42 после завершения вычисления, т.е. аналогично `task.then((res) => print(res))` на JS
|
||||
--- )
|
||||
---
|
||||
--- ```
|
||||
--- @generic T
|
||||
--- @alias Task<T> fun(callback: fun(value: T): nil): nil
|
||||
|
||||
--- Возвращает новый Task, который завершится после завершения всех переданных `tasks`.
|
||||
---
|
||||
--- Значение созданного Task будет содержать список значений `tasks` в том же порядке.
|
||||
---
|
||||
--- См. также https://api.dart.dev/dart-async/Future/wait.html
|
||||
--- @generic T
|
||||
--- @param tasks Task[]
|
||||
--- @return Task<T[]>
|
||||
local function wait(tasks)
|
||||
local count = #tasks
|
||||
local results = {}
|
||||
|
||||
return function(callback)
|
||||
for i, task in ipairs(tasks) do
|
||||
task(
|
||||
function(result)
|
||||
results[i] = result
|
||||
|
||||
count = count - 1
|
||||
if count == 0 then callback(results) end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Последовательно объединяет два `Task` в один.
|
||||
--- @generic T
|
||||
--- @generic R
|
||||
--- @param task Task<T> `Task`, который выполнится первым
|
||||
--- @param onCompleted fun(value: T): Task<R> Конструктор второго `Task`. Принимает результат выполнения первого `Task`
|
||||
--- @return Task<R>
|
||||
local function chain(task, onCompleted)
|
||||
return function(callback)
|
||||
task(function(value)
|
||||
local task2 = onCompleted(value)
|
||||
task2(callback)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
wait = wait,
|
||||
chain = chain
|
||||
}
|
||||
75
main.lua
75
main.lua
@ -1,83 +1,34 @@
|
||||
-- CameraLoader = require 'lib/camera'
|
||||
|
||||
local character = require "lib/character/character"
|
||||
local testLayout
|
||||
local TestRunner = require "test.runner"
|
||||
TestRunner:register(require "test.task")
|
||||
require "lib/tree"
|
||||
local testLayout = require "lib.simple_ui.level.layout"
|
||||
|
||||
function love.conf(t)
|
||||
t.console = true
|
||||
end
|
||||
|
||||
function love.load()
|
||||
love.window.setMode(1280, 720, { resizable = true, msaa = 4, vsync = true })
|
||||
require "lib/tree" -- важно это сделать после настройки окна
|
||||
testLayout = require "lib.simple_ui.level.layout"
|
||||
|
||||
local chars = {
|
||||
character.spawn("Foodor")
|
||||
:addBehavior {
|
||||
Tree.behaviors.residentsleeper.new(),
|
||||
Tree.behaviors.stats.new(nil, nil, 1),
|
||||
Tree.behaviors.positioned.new(Vec3 { 3, 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, 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.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()
|
||||
},
|
||||
}
|
||||
|
||||
for id, _ in pairs(chars) do
|
||||
character.spawn("Foodor", Tree.assets.files.sprites.character, nil, nil, 1)
|
||||
character.spawn("Baris", Tree.assets.files.sprites.character, Vec3 { 3, 3 }, nil, 2)
|
||||
character.spawn("Foodor Jr", Tree.assets.files.sprites.character, Vec3 { 0, 3 }, nil, 3)
|
||||
character.spawn("Baris Jr", Tree.assets.files.sprites.character, Vec3 { 0, 6 }, nil, 4)
|
||||
for id, _ in pairs(Tree.level.characters) do
|
||||
Tree.level.turnOrder:add(id)
|
||||
end
|
||||
|
||||
|
||||
Tree.level.turnOrder:endRound()
|
||||
print("Now playing:", Tree.level.turnOrder.current)
|
||||
love.window.setMode(1280, 720, { resizable = true, msaa = 4, vsync = true })
|
||||
end
|
||||
|
||||
local lt = "0"
|
||||
function love.update(dt)
|
||||
TestRunner:update(dt) -- закомментировать для отключения тестов
|
||||
|
||||
local t1 = love.timer.getTime()
|
||||
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()
|
||||
|
||||
@ -105,8 +56,11 @@ function love.draw()
|
||||
love.graphics.draw(Tree.assets.files.cats, 0, 0)
|
||||
love.graphics.pop()
|
||||
|
||||
Tree.level.camera:attach()
|
||||
|
||||
Tree.level:draw()
|
||||
|
||||
Tree.level.camera:detach()
|
||||
testLayout:draw()
|
||||
love.graphics.setColor(1, 1, 1)
|
||||
|
||||
@ -119,10 +73,3 @@ function love.draw()
|
||||
local t2 = love.timer.getTime()
|
||||
dt = string.format("%.3f", (t2 - t1) * 1000)
|
||||
end
|
||||
|
||||
function love.resize(w, h)
|
||||
local render = Tree.level.render
|
||||
if not render then return end
|
||||
render:free()
|
||||
Tree.level.render = (require "lib.level.render").new { w, h }
|
||||
end
|
||||
|
||||
@ -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