Compare commits

..

No commits in common. "main" and "hp-bar-the-dumb-way" have entirely different histories.

62 changed files with 534 additions and 1986 deletions

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.

View File

@ -1,91 +0,0 @@
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@ -1,94 +0,0 @@
Copyright 2025 The WDXL Lubrifont Project Authors (https://github.com/NightFurySL2001/WD-XL-font)
Copyright 2018-2020 The ZCOOL QingKe HuangYou Project Authors (https://www.github.com/googlefonts/zcool-qingke-huangyou)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -4,9 +4,9 @@ extern float blockSize;
// hash-функция для шума по целочисленным координатам блока
float hash(vec2 p) {
p = vec2(
dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3))
);
dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3))
);
return fract(sin(p.x + p.y) * 43758.5453123);
}

View File

@ -10,8 +10,8 @@ vec4 effect(vec4 color, Image tex, vec2 uv, vec2 px)
{
vec2 cell = floor(px / 2.0); // тут можно размер зерна менять
float n = hash(cell).x; // 0..1
float v = 0.9 + n * 0.1; // 0.9..1.0
float n = hash(cell).x; // 0..1
float v = 0.9 + n * 0.1; // 0.9..1.0
return vec4(v, v, v, 1.0);
}

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
if not self.animationGrid[state] then
print("[SpriteBehavior]: no animation for '" .. state .. "'")
callback()
end
self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED,
function()
self:loop("idle")
callback()
end)
self.state = state
--- @param node AnimationNode
function sprite:animate(state, node)
if not self.animationGrid[state] then
return print("[SpriteBehavior]: no animation for '" .. state .. "'")
end
self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED,
function()
self:loop("idle")
node:finish()
end)
self.state = state
end
function sprite:loop(state)

View File

@ -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

View File

@ -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

View File

@ -17,7 +17,7 @@ local camera = {
velocity = Vec3 {},
acceleration = 0.2,
speed = 5,
pixelsPerMeter = 32,
pixelsPerMeter = 24,
}
function camera:getDefaultScale()
@ -38,7 +38,7 @@ local controlMap = {
}
function camera:update(dt)
if self.animationNode and self.animationNode.state == "running" then
if self.animationNode and not (self.animationNode.t >= 1) then
self.animationNode:update(dt) -- тик анимации
self.position = utils.lerp(self.animationBeginPosition, self.animationEndPosition, self.animationNode:getValue())
return
@ -100,7 +100,7 @@ end
--- @param position Vec3
--- @param animationNode AnimationNode
function camera:animateTo(position, animationNode)
if self.animationNode and self.animationNode.state ~= "finished" then self.animationNode:finish() end
if self.animationNode then self.animationNode:finish() end
self.animationNode = animationNode
self.animationEndPosition = position
self.animationBeginPosition = self.position

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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 }

View File

@ -3,64 +3,47 @@ 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)
self.camera:update(dt)
self.selector: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 {

View File

@ -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 }

View File

@ -34,19 +34,15 @@ function selector:update(dt)
char:try(Tree.behaviors.spellcaster, function(b)
if not b.cast then
if not selectedId then self:select(nil) end
return
-- тут какая-то страшная дичь, я даже не уверен что оно работает
-- зато я точно уверен, что это надо было писать не так
if not selectedId then self:select(selectedId) end
if selectedId ~= Tree.level.turnOrder.current and Tree.level.turnOrder.isTurnsEnabled then return end
return self:select(selectedId)
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

View File

@ -30,19 +30,11 @@ end
---
--- Если в очереди на ход больше никого нет, заканчиваем раунд
function turnOrder:next()
Tree.level.selector:select(nil)
self.actedQueue:insert(self.current)
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 +109,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 }

View File

@ -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 }

View File

@ -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 }

View File

@ -39,11 +39,7 @@ end
--- Рисует границу вокруг элемента (с псевдо-затенением)
--- @param type "outer" | "inner"
--- @param width? number
function uiElement:drawBorder(type, width)
local w = width or 4
love.graphics.setLineWidth(w)
function uiElement:drawBorder(type)
if type == "inner" then
love.graphics.setColor(0.2, 0.2, 0.2)
love.graphics.line({
@ -59,37 +55,18 @@ function uiElement:drawBorder(type, width)
self.bounds.x, self.bounds.y + self.bounds.height,
})
else
love.graphics.setColor(0.3, 0.3, 0.3)
-- love.graphics.line({
-- self.bounds.x, self.bounds.y + self.bounds.height,
-- self.bounds.x, self.bounds.y,
-- self.bounds.x + self.bounds.width, self.bounds.y,
-- })
love.graphics.line({
self.bounds.x, self.bounds.y + self.bounds.height - w,
self.bounds.x, self.bounds.y + w,
})
love.graphics.line({
self.bounds.x + w, self.bounds.y,
self.bounds.x + self.bounds.width - w, self.bounds.y,
})
love.graphics.setColor(0.2, 0.2, 0.2)
-- love.graphics.line({
-- self.bounds.x + self.bounds.width, self.bounds.y,
-- self.bounds.x + self.bounds.width, self.bounds.y + self.bounds.height,
-- self.bounds.x, self.bounds.y + self.bounds.height,
-- })
love.graphics.line({
self.bounds.x + self.bounds.width, self.bounds.y + w,
self.bounds.x + self.bounds.width, self.bounds.y + self.bounds.height - w,
self.bounds.x + self.bounds.width, self.bounds.y,
self.bounds.x + self.bounds.width, self.bounds.y + self.bounds.height,
self.bounds.x, self.bounds.y + self.bounds.height,
})
love.graphics.setColor(0.3, 0.3, 0.3)
love.graphics.line({
self.bounds.x + self.bounds.width - w, self.bounds.y + self.bounds.height,
self.bounds.x + w, self.bounds.y + self.bounds.height,
self.bounds.x, self.bounds.y + self.bounds.height,
self.bounds.x, self.bounds.y,
self.bounds.x + self.bounds.width, self.bounds.y,
})
end

View File

@ -1,71 +0,0 @@
local Element = require "lib.simple_ui.element"
--- @class BarElement : UIElement
--- @field getter fun() : number
--- @field value number
--- @field maxValue number
--- @field color Color
--- @field useDividers boolean
--- @field drawText boolean
local barElement = setmetatable({}, Element)
barElement.__index = barElement
barElement.useDividers = false
barElement.drawText = false
function barElement:update(dt)
local val = self.getter()
self.value = val < 0 and 0 or val > self.maxValue and self.maxValue or val
end
function barElement:draw()
local valueWidth = self.bounds.width * self.value / self.maxValue
local emptyWidth = self.bounds.width - valueWidth
--- шум
love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setShader()
--- закраска пустой части
love.graphics.setColor(0.05, 0.05, 0.05)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x + valueWidth, self.bounds.y, emptyWidth,
self.bounds.height)
love.graphics.setBlendMode("alpha")
--- закраска значимой части её цветом
love.graphics.setColor(self.color.r, self.color.g, self.color.b)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, valueWidth,
self.bounds.height)
love.graphics.setBlendMode("alpha")
--- мерки
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255)
if self.useDividers then
local count = self.maxValue - 1
local measureWidth = self.bounds.width / self.maxValue
for i = 1, count, 1 do
love.graphics.line(self.bounds.x + i * measureWidth, self.bounds.y, self.bounds.x + i * measureWidth,
self.bounds.y + self.bounds.height)
end
end
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))
end
self:drawBorder("inner")
self:drawGradientOverlay()
end
return function(values) return barElement:new(values) end

View File

@ -1,86 +0,0 @@
local Element = require "lib.simple_ui.element"
local Rect = require "lib.simple_ui.rect"
local Color = require "lib.simple_ui.color"
local Bar = require "lib.simple_ui.level.bar"
--- @class BottomBars : UIElement
--- @field hpBar BarElement
--- @field manaBar BarElement
local bottomBars = setmetatable({}, Element)
bottomBars.__index = bottomBars;
--- @param cid Id
function bottomBars.new(cid)
local t = setmetatable({}, bottomBars)
t.hpBar =
Bar {
getter = function()
local char = Tree.level.characters[cid]
return char:try(Tree.behaviors.stats, function(stats)
return stats.hp or 0
end)
end,
color = Color { r = 130 / 255, g = 8 / 255, b = 8 / 255 },
drawText = true,
maxValue = 20
}
t.manaBar =
Bar {
getter = function()
local char = Tree.level.characters[cid]
return char:try(Tree.behaviors.stats, function(stats)
return stats.mana or 0
end)
end,
color = Color { r = 51 / 255, g = 105 / 255, b = 30 / 255 },
useDividers = true,
maxValue = 10
}
return t
end
function bottomBars:update(dt)
local height = 16
local margin = 2
self.bounds.height = height
self.bounds.y = self.bounds.y - height
self.hpBar.bounds = Rect {
width = -2 * margin + self.bounds.width / 2,
height = height - margin,
x = self.bounds.x + margin,
y = self.bounds.y + margin
}
self.manaBar.bounds = Rect {
width = -2 * margin + self.bounds.width / 2,
height = height - margin,
x = self.bounds.x + margin + self.bounds.width / 2,
y = self.bounds.y + margin
}
self.hpBar:update(dt)
self.manaBar:update(dt)
end
function bottomBars:draw()
-- шум
love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setShader()
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setBlendMode("alpha")
self.hpBar:draw()
self.manaBar:draw()
end
return bottomBars.new

View File

@ -1,128 +0,0 @@
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"
local Bars = require "lib.simple_ui.level.bottom_bars"
local EndTurnButton = require "lib.simple_ui.level.end_turn"
--- @class CharacterPanel : UIElement
--- @field animationNode AnimationNode
--- @field state "show" | "idle" | "hide"
--- @field skillRow SkillRow
--- @field bars BottomBars
--- @field endTurnButton EndTurnButton
local characterPanel = setmetatable({}, Element)
characterPanel.__index = characterPanel
function characterPanel.new(characterId)
local t = {}
t.state = "show"
t.skillRow = SkillRow(characterId)
t.bars = Bars(characterId)
t.endTurnButton = EndTurnButton {}
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"
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"
end,
duration = 300,
easing = easing.easeOutCubic
}:run()
end
--- @type love.Canvas
local characterPanelCanvas;
function characterPanel:update(dt)
if self.animationNode then self.animationNode:update(dt) end
self.skillRow:update(dt)
self.bars.bounds = Rect {
width = self.skillRow.bounds.width,
x = self.skillRow.bounds.x,
y = self.skillRow.bounds.y
}
self.bars:update(dt)
self.bounds = Rect {
x = self.bars.bounds.x,
y = self.bars.bounds.y,
width = self.bars.bounds.width,
height = self.bars.bounds.height + self.skillRow.bounds.height
}
self.endTurnButton:layout()
self.endTurnButton.bounds.x = self.bounds.x + self.bounds.width + 32
self.endTurnButton.bounds.y = self.bounds.y + self.bounds.height / 2 - self.endTurnButton.bounds.height / 2
self.endTurnButton:update(dt)
if not characterPanelCanvas then
characterPanelCanvas = love.graphics.newCanvas(self.bounds.width, self.bounds.height)
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", alpha)
end
function characterPanel:draw()
self.skillRow:draw()
--- @TODO: переписать этот ужас с жонглированием координатами, а то слишком хардкод (skillRow рисуется относительно нуля и не закрывает канвас)
love.graphics.push()
local canvas = love.graphics.getCanvas()
love.graphics.translate(0, self.bars.bounds.height)
love.graphics.setCanvas(characterPanelCanvas)
love.graphics.clear()
love.graphics.draw(canvas)
love.graphics.pop()
love.graphics.push()
love.graphics.translate(-self.bounds.x, -self.bounds.y)
self.bars:draw()
self:drawBorder("outer")
love.graphics.pop()
--- рисуем текстуру шейдером появления
love.graphics.setCanvas()
love.graphics.setShader(Tree.assets.files.shaders.reveal)
love.graphics.setColor(1, 1, 1, 1)
self.endTurnButton:draw()
love.graphics.push()
love.graphics.translate(self.bounds.x, self.bounds.y)
love.graphics.draw(characterPanelCanvas)
love.graphics.setColor(1, 1, 1)
love.graphics.pop()
love.graphics.setShader()
end
return characterPanel.new

View File

@ -1,66 +0,0 @@
local Element = require "lib.simple_ui.element"
local AnimationNode = require "lib.animation_node"
local easing = require "lib.utils.easing"
--- @class EndTurnButton : UIElement
--- @field hovered boolean
--- @field onClick function?
local endTurnButton = setmetatable({}, Element)
endTurnButton.__index = endTurnButton
function endTurnButton:update(dt)
local mx, my = love.mouse.getPosition()
if self:hitTest(mx, my) then
self.hovered = true
if Tree.controls:isJustPressed("select") then
if self.onClick then self.onClick() end
Tree.controls:consume("select")
end
else
self.hovered = false
end
end
function endTurnButton:layout()
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
end
function endTurnButton:draw()
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255, 0.9)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
if self.hovered then
love.graphics.setColor(0.1, 0.1, 0.1)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
end
love.graphics.setColor(0.95, 0.95, 0.95)
love.graphics.draw(self.text, self.bounds.x + 16, self.bounds.y + 8)
self:drawBorder("outer")
love.graphics.setColor(1, 1, 1)
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() if not playing:has(Tree.behaviors.ai) then Tree.level.selector:select(cid) end end
}:run()
end
return function(values)
return endTurnButton:new(values)
end

View File

@ -1,19 +1,254 @@
local CPanel = require "lib.simple_ui.level.cpanel"
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"
local Color = require "lib.simple_ui.color"
local build
--- @class BarElement : UIElement
--- @field getter fun() : number
--- @field value number
--- @field maxValue number
--- @field color Color
--- @field useDividers boolean
--- @field drawText boolean
local barElement = setmetatable({}, Element)
barElement.__index = barElement
barElement.useDividers = false
barElement.drawText = false
function barElement:update(dt)
local val = self.getter()
self.value = val < 0 and 0 or val > self.maxValue and self.maxValue or val
end
function barElement:draw()
local valueWidth = self.bounds.width * self.value / self.maxValue
local emptyWidth = self.bounds.width - valueWidth
--- шум
love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setShader()
--- закраска пустой части
love.graphics.setColor(0.05, 0.05, 0.05)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x + valueWidth, self.bounds.y, emptyWidth,
self.bounds.height)
love.graphics.setBlendMode("alpha")
--- закраска значимой части её цветом
love.graphics.setColor(self.color.r, self.color.g, self.color.b)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, valueWidth,
self.bounds.height)
love.graphics.setBlendMode("alpha")
--- мерки
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255)
if self.useDividers then
local count = self.maxValue - 1
local measureWidth = self.bounds.width / self.maxValue
for i = 1, count, 1 do
love.graphics.line(self.bounds.x + i * measureWidth, self.bounds.y, self.bounds.x + i * measureWidth,
self.bounds.y + self.bounds.height)
end
end
love.graphics.setColor(1, 1, 1)
--- текст поверх
if self.drawText then
love.graphics.printf(tostring(self.value) .. "/" .. tostring(self.maxValue), self.bounds.x,
self.bounds.y, self.bounds.width, "center")
end
self:drawGradientOverlay()
end
--- @class BottomBars : UIElement
--- @field hpBar BarElement
--- @field manaBar BarElement
local bottomBars = setmetatable({}, Element)
bottomBars.__index = bottomBars;
--- @param cid Id
function bottomBars.new(cid)
local t = setmetatable({}, bottomBars)
t.hpBar =
barElement:new {
getter = function()
local char = Tree.level.characters[cid]
return char:try(Tree.behaviors.stats, function(stats)
return stats.hp or 0
end)
end,
color = Color { r = 130 / 255, g = 8 / 255, b = 8 / 255 },
drawText = true,
maxValue = 20
}
t.manaBar =
barElement:new {
getter = function()
local char = Tree.level.characters[cid]
return char:try(Tree.behaviors.stats, function(stats)
return stats.mana or 0
end)
end,
color = Color { r = 51 / 255, g = 105 / 255, b = 30 / 255 },
useDividers = true,
maxValue = 10
}
return t
end
function bottomBars:update(dt)
local height = 14
local margin = 2
self.bounds.height = height
self.bounds.y = self.bounds.y - height
self.hpBar.bounds = Rect {
width = -2 * margin + self.bounds.width / 2,
height = height,
x = self.bounds.x + margin,
y = self.bounds.y
}
self.manaBar.bounds = Rect {
width = -2 * margin + self.bounds.width / 2,
height = height,
x = self.bounds.x + margin + self.bounds.width / 2,
y = self.bounds.y
}
self.hpBar:update(dt)
self.manaBar:update(dt)
end
function bottomBars:draw()
-- шум
love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setShader()
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setBlendMode("alpha")
self.hpBar:draw()
self.manaBar:draw()
end
local c = love.graphics.newCanvas(1280, 720) --- @TODO: выставлять канвасу правильный размер в зависимости от окна
--- @class CharacterPanel : UIElement
--- @field animationNode AnimationNode
--- @field state "show" | "idle" | "hide"
--- @field skillRow SkillRow
--- @field bars BottomBars
local characterPanel = setmetatable({}, Element)
characterPanel.__index = characterPanel
function characterPanel.new(characterId)
local t = {}
t.state = "show"
t.skillRow = SkillRow(characterId)
t.bars = bottomBars.new(characterId)
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"
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"
end,
duration = 300,
easing = easing.easeOutCubic
}:run()
end
function characterPanel:update(dt)
if self.animationNode then self.animationNode:update(dt) end
self.skillRow:update(dt)
self.bars.bounds = Rect {
width = self.skillRow.bounds.width,
x = self.skillRow.bounds.x,
y = self.skillRow.bounds.y
}
self.bars:update(dt)
self.bounds = Rect {
x = self.bars.bounds.x,
y = self.bars.bounds.y,
width = self.bars.bounds.width,
height = self.bars.bounds.height + self.skillRow.bounds.height
}
--- анимация появления
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", alpha)
end
function characterPanel:draw()
love.graphics.setCanvas(c)
love.graphics.clear()
self.skillRow:draw()
self.bars:draw()
self:drawBorder("outer")
--- рисуем текстуру шейдером появления
love.graphics.setCanvas()
love.graphics.setShader(Tree.assets.files.shaders.reveal)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(c)
love.graphics.setColor(1, 1, 1)
love.graphics.setShader()
end
-----------------------------------
local layout = {}
function layout:update(dt)
if self.characterPanel then self.characterPanel:update(dt) end
local cid = Tree.level.selector:selected()
if cid then
self.characterPanel = CPanel(cid)
self.characterPanel = characterPanel.new(cid)
self.characterPanel:show()
self.characterPanel:update(dt)
elseif Tree.level.selector:deselected() then
self.characterPanel:hide()
end
if self.characterPanel then self.characterPanel:update(dt) end
end
function layout:draw()

View File

@ -1,2 +1,2 @@
local UI_SCALE = 0.75 -- выдуманное значение для dependency injection, надо подбирать так, чтобы UI_SCALE * 64 было целым числом
local UI_SCALE = 0.8 -- выдуманное значение для dependency injection
return UI_SCALE

View File

@ -103,11 +103,8 @@ function skillRow.new(characterId)
return t
end
--- @type love.Canvas
local c;
function skillRow:update(dt)
local iconSize = math.floor(64 * UI_SCALE)
local iconSize = 64 * UI_SCALE
local screenW, screenH = love.graphics.getDimensions()
local padding, margin = 8, 4
local count = #self.children -- слоты под скиллы
@ -124,61 +121,54 @@ function skillRow:update(dt)
y = self.bounds.y + margin, height = iconSize, width = iconSize }
skb:update(dt)
end
if not c then
c = love.graphics.newCanvas(self.bounds.width, self.bounds.height)
end
end
local c = love.graphics.newCanvas(1280, 720) --- @TODO: выставлять канвасу правильный размер в зависимости от окна
function skillRow:draw()
local oldCanvas = love.graphics.getCanvas()
love.graphics.setCanvas({ c, stencil = true })
love.graphics.clear()
love.graphics.setColor(1, 1, 1)
do
--- рисуем в локальных координатах текстурки
love.graphics.push()
love.graphics.translate(-self.bounds.x, -self.bounds.y)
-- сначала иконки скиллов
for _, skb in ipairs(self.children) do
skb:draw()
end
-- маска для вырезов под иконки
love.graphics.setShader(Tree.assets.files.shaders.alpha_mask)
love.graphics.stencil(function()
local mask = Tree.assets.files.masks.rrect32
local maskSize = mask:getWidth()
for _, skb in ipairs(self.children) do
love.graphics.draw(mask, skb.bounds.x, skb.bounds.y, 0,
skb.bounds.width / maskSize, skb.bounds.height / maskSize)
end
end, "replace", 1)
love.graphics.setShader()
-- дальше рисуем панель, перекрывая иконки
love.graphics.setStencilTest("less", 1)
-- шум
love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setShader()
-- фон
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setBlendMode("alpha")
love.graphics.setStencilTest()
--затенение
self:drawGradientOverlay()
love.graphics.pop()
-- сначала иконки скиллов
for _, skb in ipairs(self.children) do
skb:draw()
end
-- маска для вырезов под иконки
love.graphics.setShader(Tree.assets.files.shaders.alpha_mask)
love.graphics.stencil(function()
local mask = Tree.assets.files.masks.rrect32
local maskSize = mask:getWidth()
for _, skb in ipairs(self.children) do
love.graphics.draw(mask, skb.bounds.x, skb.bounds.y, 0,
skb.bounds.width / maskSize, skb.bounds.height / maskSize)
end
end, "replace", 1)
love.graphics.setShader()
-- дальше рисуем панель, перекрывая иконки
love.graphics.setStencilTest("less", 1)
-- шум
love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setShader()
-- фон
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setBlendMode("alpha")
love.graphics.setStencilTest()
--затенение
self:drawGradientOverlay()
love.graphics.setColor(1, 1, 1)
love.graphics.setCanvas(oldCanvas)
love.graphics.draw(c)
end
return skillRow.new

View File

@ -1,9 +0,0 @@
-- --- @class Sound
-- --- @field source love.Source just a sound
-- sound = {}
-- local function new()
-- return setmetatable({}, sound)
-- end
-- return { new }

View File

@ -7,22 +7,22 @@
--- --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 Вызывается в момент каста, изменяет мир.
local spell = {}
--- @field cast fun(self: Spell, caster: Character, target: Vec3): boolean Вызывается в момент каста, изменяет мир. Возвращает bool в зависимости от того, получилось ли скастовать
local spell = {}
spell.__index = spell
spell.tag = "base"
spell.tag = "base"
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

View File

@ -6,14 +6,11 @@
Tree = {
assets = (require "lib.utils.asset_bundle"):load()
}
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"

View File

@ -50,12 +50,8 @@ 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
return nil
end
function AssetBundle.cutExtension(filename)

View File

@ -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

View File

@ -1,94 +0,0 @@
--- @alias FontVariant "smallest" | "small" | "medium" | "large" | "headline"
--- @class TextTheme
--- @field private _sizes {[FontVariant]: integer}
local theme = {
_sizes = {
smallest = 10,
small = 12,
medium = 14,
large = 16,
headline = 20,
}
}
theme.__index = theme
--- @param loader fun(size: integer): love.Font?
function theme.new(loader)
local t = {}
for tag, size in pairs(theme._sizes) do
local f = loader(size)
if not f then return nil end
t[tag] = f
end
return setmetatable(t, theme)
end
--- @param variant FontVariant
--- @return love.Font
function theme:getVariant(variant)
return self[variant]
end
----------------------------------------------------------
--- A singleton to handle fonts
--- @class FontManager
--- @field private _themes table
--- @field defaultTheme string?
local FontManager = {
_themes = {}
}
--- @param name string
--- @param size integer
function FontManager.newFont(name, size)
local err = function()
print("[FontManager]: font " .. name .. " not found!")
end
local fontDir = Tree.assets.files.fonts[name]
if not fontDir then return err() end
local font = fontDir.font
if not font then return err() end
return love.graphics.newFont(font, size)
end
--- @param name string
--- @return FontManager
function FontManager:loadTheme(name)
self._themes[name] = theme.new(
function(size)
return self.newFont(name, size)
end
)
return self
end
--- @param name string
--- @return TextTheme?
function FontManager:getTheme(name)
return self._themes[name]
end
--- @return TextTheme
function FontManager:getDefaultTheme()
return self._themes[self.defaultTheme]
end
--- initial setup
--- @param defaultFontName string
function FontManager:load(defaultFontName)
local t = self:loadTheme(defaultFontName):getTheme(defaultFontName)
if not t then
print("[FontManager]: default font " .. defaultFontName .. " is missing")
return self
end
self.defaultTheme = defaultFontName
local f = t:getVariant("medium")
love.graphics.setFont(f)
return self
end
return FontManager

View File

@ -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
}

View File

@ -1,83 +1,44 @@
-- 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 = false, msaa = 0, 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, потому что нужно перехватить жесты и не пустить их дальше
testLayout:update(dt) -- логика UI-слоя должна отработать раньше всех, потому что нужно перехватить жесты и не пустить их дальше
Tree.panning:update(dt)
Tree.level:update(dt)
Tree.audio:update(dt)
-- для тестов очереди ходов
-- удалить как только появится ui для людей
if Tree.controls:isJustPressed("endTurnTest") then
Tree.level.turnOrder:next()
print("Now playing:", Tree.level.turnOrder.current)
end
if Tree.controls:isJustPressed("toggleTurns") then
print('toggle turns')
Tree.level.turnOrder:toggleTurns()
end
Tree.controls:cache()
@ -105,24 +66,17 @@ 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)
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)
local stats = "fps: " .. love.timer.getFPS() .. " lt: " .. lt .. " dt: " .. dt
love.graphics.print(stats, 10, 10)
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

View File

@ -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

View File

@ -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