Compare commits

..

25 Commits

Author SHA1 Message Date
6a15c6ed83 Fix AnimationNode:finish() called twice 2025-11-08 01:28:40 +03:00
a3853ceac8 Refactor layout 2025-11-08 01:28:01 +03:00
f32393b978 Make transparent elements untargetable 2025-11-08 01:27:43 +03:00
eb45ffade4 Make the AnimationNode handle its own state 2025-11-07 09:19:27 +03:00
0eb06dce3f Add linear easing (aka no easing) 2025-11-07 09:04:23 +03:00
c741bf3952 implement a generic UIElement constructor 2025-11-07 09:04:02 +03:00
12d57892be Move SkillButtton animations to the SkillRow. Implement naive state
management
2025-11-05 01:32:16 +03:00
2ddc32c430 Merge branch 'feature/simple_ui' of https://gitea.peaashmeter.dev/ArcMutex/heroes-of-nerevelon into feature/simple_ui 2025-11-04 07:01:45 +03:00
d6a57a9727 Introduce Rect class and UIElement base class for layout
Refactor skill button and row to use UIElement with Rect bounds and
transform Update layout references and coordinate calculations
accordingly
2025-11-04 07:01:12 +03:00
da2f6d03a3 Improve skill button animation timing and selection logic 2025-11-04 07:01:12 +03:00
99fe4c0556 Add easing functions normalized to [0, 1] range 2025-11-04 07:01:12 +03:00
2802570a50 Remove deprecated UI system 2025-11-04 07:01:12 +03:00
35a7a69bf7 Add naive ui v2 implementation 2025-11-04 07:01:12 +03:00
660edc5ef8 Add tag field to Spell and assign tags to spells 2025-11-04 07:01:12 +03:00
f1d181fb64 Add selection tracking to selector with selected and deselected methods 2025-11-04 07:01:12 +03:00
b9d2b469c8 add test icons 2025-11-04 07:01:12 +03:00
d4e351b080 Introduce Rect class and UIElement base class for layout
Refactor skill button and row to use UIElement with Rect bounds and
transform Update layout references and coordinate calculations
accordingly
2025-11-04 06:59:03 +03:00
175062a452 Improve skill button animation timing and selection logic 2025-11-04 04:43:52 +03:00
a5c9ca93f6 Add easing functions normalized to [0, 1] range 2025-11-04 02:03:34 +03:00
5ba653509a Remove deprecated UI system 2025-11-04 01:29:45 +03:00
14225002e2 Add naive ui v2 implementation 2025-11-04 01:16:29 +03:00
21dbf99435 Add tag field to Spell and assign tags to spells 2025-11-04 01:15:49 +03:00
72eb93baf7 Add selection tracking to selector with selected and deselected methods 2025-11-04 01:15:42 +03:00
a8c188b24e Add pickQuad method to spriteAtlas for random quad selection 2025-11-02 05:48:09 +03:00
55787a6643 add test icons 2025-10-26 02:37:15 +03:00
35 changed files with 123 additions and 1116 deletions

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: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,8 +0,0 @@
vec4 effect(vec4 color, Image tex, vec2 texCoord, vec2 screenCoord)
{
vec4 px = Texel(tex, texCoord);
if (px.a == 0.0) {
discard;
}
return vec4(1.0);
}

View File

@ -1,24 +0,0 @@
extern float t;
extern float blockSize;
// hash-функция для шума по целочисленным координатам блока
float hash(vec2 p) {
p = vec2(
dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3))
);
return fract(sin(p.x + p.y) * 43758.5453123);
}
vec4 effect(vec4 color, Image tex, vec2 texCoord, vec2 screenCoord)
{
float blockSize = 4.0;
vec2 cell = floor(screenCoord / blockSize);
float n = hash(cell); // [0..1]
float mask = 1.0 - step(t, n);
vec4 base = Texel(tex, texCoord) * color;
base.a *= mask;
return base;
}

View File

@ -1,17 +0,0 @@
#pragma language glsl3
vec2 hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 34.345);
return fract(vec2(p.x * p.y, p.x + p.y));
}
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
return vec4(v, v, v, 1.0);
}

View File

@ -33,7 +33,7 @@ local easing = require "lib.utils.easing"
--- @field duration number продолжительность в миллисекундах
--- @field easing ease функция смягчения
--- @field t number прогресс анимации
--- @field state "running" | "waiting" | "finished"
--- @field finished boolean
local animation = {}
animation.__index = animation
@ -41,7 +41,7 @@ animation.__index = animation
function animation:bubbleUp()
self.count = self.count - 1
if self.count > 0 then return end
self.state = "finished"
self.finished = true
if self.onEnd then self.onEnd() end
if self.parent then self.parent:bubbleUp() end
end
@ -63,7 +63,7 @@ function animation:getValue()
end
function animation:update(dt)
if self.state ~= "running" then return end
if self.finished then return end
if self.t < 1 then
self.t = self.t + dt * 1000 / self.duration -- в знаменателе продолжительность анимации в секундах
@ -82,14 +82,14 @@ local function new(data)
end
t.onEnd = data.onEnd
t.count = 1 -- своя анимация
t.finished = false
t.children = {}
t:chain(data.children or {})
t.duration = data.duration or 1000
t.easing = data.easing or easing.linear
t.t = 0
t.state = "running"
t.finish = function()
t.state = "waiting"
if t.finished then return end
t:bubbleUp()
for _, anim in ipairs(t.children) do
anim:run()

View File

@ -24,17 +24,6 @@ function mapBehavior.new(position, size)
}, 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)

View File

@ -19,15 +19,10 @@ end
function behavior:endCast()
self.state = "idle"
self.cast = nil
Tree.level.turnOrder:reorder()
Tree.level.selector:unlock()
end
function behavior:update(dt)
if Tree.level.selector:deselected() then
self.state = "idle"
self.cast = nil
end
if self.cast and self.state == "casting" then self.cast:update(self.owner, dt) end
end

View File

@ -1,22 +1,16 @@
--- @class StatsBehavior : Behavior
--- @field hp integer
--- @field mana integer
--- @field initiative integer
--- @field isInTurnOrder boolean
local behavior = {}
behavior.__index = behavior
behavior.id = "stats"
--- @param hp? integer
--- @param mana? integer
--- @param initiative? integer
--- @param isInTurnOrder? boolean
function behavior.new(hp, mana, initiative, isInTurnOrder)
function behavior.new(hp, mana)
return setmetatable({
hp = hp or 20,
mana = mana or 10,
initiative = initiative or 10,
isInTurnOrder = isInTurnOrder or true
mana = mana or 10
}, behavior)
end

View File

@ -16,8 +16,8 @@ character.__index = character
--- @param spriteDir table
--- @param position? Vec3
--- @param size? Vec3
--- @param initiative? integer
local function spawn(name, spriteDir, position, size, initiative)
--- @param level? integer
local function spawn(name, spriteDir, position, size, level)
local char = {}
char = setmetatable(char, character)
@ -28,7 +28,7 @@ local function spawn(name, spriteDir, position, size, initiative)
char:addBehavior {
Tree.behaviors.residentsleeper.new(),
Tree.behaviors.stats.new(nil, nil, initiative),
Tree.behaviors.stats.new(),
Tree.behaviors.map.new(position, size),
Tree.behaviors.sprite.new(spriteDir),
Tree.behaviors.spellcaster.new()

View File

@ -1,3 +1,5 @@
local utils = require "lib.utils.utils"
--- @alias Device "mouse" | "key" | "pad"
--- @param device Device
@ -15,22 +17,13 @@ controls.keymap = {
cameraMoveRight = control("key", "d"),
cameraMoveDown = control("key", "s"),
cameraMoveScroll = control("mouse", "3"),
cameraAnimateTo = control('key', 't'),
fullMana = control("key", "m"),
select = control("mouse", "1"),
endTurnTest = control("key", "e"),
toggleTurns = control("key", "r"),
select = control("mouse", "1")
}
local currentKeys = {}
local cachedKeys = {}
--- @type number
controls.mouseWheelY = 0
love.wheelmoved = function(x, y)
controls.mouseWheelY = y
end
--- polling controls in O(n)
--- should be called at the beginning of every frame
function controls:poll()
@ -54,7 +47,6 @@ function controls:cache()
for k, v in pairs(currentKeys) do
cachedKeys[k] = v
end
controls.mouseWheelY = 0
end
--- marks a control consumed for the current frame

View File

@ -9,15 +9,12 @@ local EPSILON = 0.001
--- @field speed number
--- @field pixelsPerMeter integer
--- @field scale number
--- @field animationNode AnimationNode?
--- @field animationEndPosition Vec3
--- @field animationBeginPosition Vec3
local camera = {
position = Vec3 {},
velocity = Vec3 {},
acceleration = 0.2,
speed = 5,
pixelsPerMeter = 32,
pixelsPerMeter = 24,
}
function camera:getDefaultScale()
@ -30,6 +27,12 @@ camera.scale = camera:getDefaultScale()
---------------------------------------------------
love.wheelmoved = function(x, y)
if camera.scale > camera:getDefaultScale() * 5 and y > 0 then return end;
if camera.scale < camera:getDefaultScale() / 5 and y < 0 then return end;
camera.scale = camera.scale + (camera.scale * 0.1 * y)
end
local controlMap = {
cameraMoveUp = Vec3({ 0, -1 }),
cameraMoveLeft = Vec3({ -1 }),
@ -38,19 +41,6 @@ local controlMap = {
}
function camera:update(dt)
if self.animationNode and self.animationNode.state == "running" then
self.animationNode:update(dt) -- тик анимации
self.position = utils.lerp(self.animationBeginPosition, self.animationEndPosition, self.animationNode:getValue())
return
end
-------------------- зум на колесо ---------------------
local y = Tree.controls.mouseWheelY
if camera.scale > camera:getDefaultScale() * 5 and y > 0 then return end;
if camera.scale < camera:getDefaultScale() / 5 and y < 0 then return end;
camera.scale = camera.scale + (camera.scale * 0.1 * y)
--------------------------------------------------------
local ps = Tree.panning
if ps.delta:length() > 0 then
local worldDelta = ps.delta:scale(1 / (self.pixelsPerMeter * self.scale)):scale(dt):scale(self.speed)
@ -97,16 +87,6 @@ function camera:detach()
love.graphics.pop()
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
self.animationNode = animationNode
self.animationEndPosition = position
self.animationBeginPosition = self.position
self.velocity = Vec3 {}
end
--- @return Camera
local function new()
return setmetatable({

View File

@ -7,7 +7,6 @@ local utils = require "lib.utils.utils"
--- @field selector Selector
--- @field camera Camera
--- @field tileGrid TileGrid
--- @field turnOrder TurnOrder
local level = {}
level.__index = level
@ -25,7 +24,6 @@ local function new(type, template)
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(),
}, level)
end
@ -35,6 +33,7 @@ function level:update(dt)
el:update(dt)
end)
self.camera:update(dt)
self.selector:update(dt)
end

View File

@ -27,14 +27,13 @@ function selector:update(dt)
local selectedId = Tree.level.characterGrid:get(Vec3 { mousePosition.x, mousePosition.y })
if not self.id then
if selectedId ~= Tree.level.turnOrder.current and Tree.level.turnOrder.isTurnsEnabled then return end
return self:select(selectedId)
else
local char = Tree.level.characters[self.id]
char:try(Tree.behaviors.spellcaster, function(b)
if not b.cast then
if not selectedId then self:select(nil) end
self:select(selectedId)
return
end
if b.cast:cast(char, mousePosition) then

View File

@ -1,111 +0,0 @@
local PriorityQueue = require "lib.utils.priority_queue"
local initiativeComparator = function(id_a, id_b)
local res = Tree.level.characters[id_a]:try(Tree.behaviors.stats, function(astats)
local res = Tree.level.characters[id_b]:try(Tree.behaviors.stats, function(bstats)
return astats.initiative > bstats.initiative
end)
return res
end)
return res or false
end
--- @class TurnOrder
--- @field actedQueue PriorityQueue Очередь тех, кто сделал ход в текущем раунде
--- @field pendingQueue PriorityQueue Очередь тех, кто ждет своего хода в текущем раунде
--- @field current? Id Считаем того, кто сейчас ходит, отдельно, т.к. он ВСЕГДА первый в списке
--- @field isTurnsEnabled boolean
local turnOrder = {}
turnOrder.__index = turnOrder
local function new()
return setmetatable({
actedQueue = PriorityQueue.new(initiativeComparator),
pendingQueue = PriorityQueue.new(initiativeComparator),
isTurnsEnabled = true,
}, turnOrder)
end
--- Перемещаем активного персонажа в очередь сходивших
---
--- Если в очереди на ход больше никого нет, заканчиваем раунд
function turnOrder:next()
self.actedQueue:insert(self.current)
local next = self.pendingQueue:peek()
if not next then return self:endRound() end
self.current = self.pendingQueue:pop()
end
--- Меняем местами очередь сходивших и не сходивших (пустую)
function turnOrder:endRound()
assert(self.pendingQueue:size() == 0, "[TurnOrder]: tried to end the round before everyone had a turn")
print("[TurnOrder]: end of the round")
self.actedQueue, self.pendingQueue = self.pendingQueue, self.actedQueue
self.current = self.pendingQueue:pop()
end
--- Пересчитать очередность хода
function turnOrder:reorder()
local _acted, _pending = PriorityQueue.new(initiativeComparator), PriorityQueue.new(initiativeComparator)
--- сортировка отдельно кучи не ходивших и ходивших
while self.pendingQueue:peek() do
_pending:insert(self.pendingQueue:pop())
end
while self.actedQueue:peek() do
_acted:insert(self.actedQueue:pop())
end
self.actedQueue, self.pendingQueue = _acted, _pending
local t = {}
for id in self:getOrder(10) do
table.insert(t, id)
end
print("[TurnOrder]: next 10 turns")
print(table.concat(t, ", "))
end
--- Итератор по бесконечной цикличной очереди хода
--- @param count integer?
function turnOrder:getOrder(count)
local order = { self.current }
local _acted, _pending = self.actedQueue:copy(), self.pendingQueue:copy()
local i, j = 0, 0
local nextTurn = false -- если вышли за пределы текущего хода, то сортируем список, чтобы поставить активного персонажа на свое место
return function()
-------------------- Очередь этого хода: активный + не сходившие
i = i + 1
if count and count < 1 then return nil end
if count and i > count then return nil end
if i == 1 then return self.current end
if _pending:peek() then
local next = _pending:pop()
table.insert(order, next)
return next
end
-------------------- Очередь следующих ходов: цикл по всем персонажам в порядке инициативы
if not nextTurn then
while _acted:peek() do
table.insert(order, _acted:pop())
end
table.sort(order, initiativeComparator)
nextTurn = true
end
j = j + 1
if j % #order == 0 then return order[#order] end
return order[j % #order]
end
end
--- @param id Id
function turnOrder:add(id)
self.actedQueue:insert(id) -- новые персонажи по умолчанию попадают в очередь следующего хода
end
return { new = new }

View File

@ -1,20 +0,0 @@
--- @class Color
--- @field r number
--- @field g number
--- @field b number
--- @field a number
local color = {
r = 1,
g = 1,
b = 1,
a = 1
}
color.__index = color
--- @param rgba {r?: number, g?: number, b?: number, a?: number}
--- @return Color
function color.new(rgba)
return setmetatable(rgba, color)
end
return color.new

View File

@ -1,22 +1,9 @@
local Rect = require "lib.simple_ui.rect"
local function makeGradientMesh(w, h, topColor, bottomColor)
local vertices = {
{ 0, 0, 0, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- левый верх
{ w, 0, 1, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- правый верх
{ w, h, 1, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- правый низ
{ 0, h, 0, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- левый низ
}
local mesh = love.graphics.newMesh(vertices, "fan", "static")
return mesh
end
--- @class UIElement
--- @field bounds Rect Прямоугольник, в границах которого размещается элемент. Размеры и положение в экранных координатах
--- @field overlayGradientMesh love.Mesh Общий градиент поверх элемента (интерполированный меш)
--- @field bounds Rect Прямоугольник, в границах которого размещается элемент. Размеры и положение в *локальных* координатах
--- @field transform love.Transform Преобразование из локальных координат элемента (bounds) в экранные координаты
local uiElement = {}
uiElement.bounds = Rect {}
uiElement.overlayGradientMesh = makeGradientMesh(1, 1, { 0, 0, 0, 0 }, { 0, 0, 0, 0.4 });
uiElement.__index = uiElement
function uiElement:update(dt) end
@ -24,7 +11,10 @@ function uiElement:update(dt) end
function uiElement:draw() end
function uiElement:hitTest(screenX, screenY)
return self.bounds:hasPoint(screenX, screenY)
local r, g, b, a = love.graphics.getColor()
if a == 0 then return false end
local lx, ly = self.transform:inverseTransformPoint(screenX, screenY)
return self.bounds:hasPoint(lx, ly)
end
--- @generic T : UIElement
@ -33,77 +23,8 @@ end
--- @return T
function uiElement.new(self, values)
values.bounds = values.bounds or Rect {}
values.overlayGradientMesh = values.overlayGradientMesh or uiElement.overlayGradientMesh;
values.transform = values.transform or love.math.newTransform()
return setmetatable(values, self)
end
--- Рисует границу вокруг элемента (с псевдо-затенением)
--- @param type "outer" | "inner"
--- @param width? number
function uiElement:drawBorder(type, width)
local w = width or 4
love.graphics.setLineWidth(w)
if type == "inner" then
love.graphics.setColor(0.2, 0.2, 0.2)
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.setColor(0.3, 0.3, 0.3)
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,
})
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,
})
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,
})
end
love.graphics.setColor(1, 1, 1)
end
--- рисует градиент поверх элемента
function uiElement:drawGradientOverlay()
love.graphics.push()
love.graphics.translate(self.bounds.x, self.bounds.y)
love.graphics.scale(self.bounds.width, self.bounds.height)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(self.overlayGradientMesh)
love.graphics.pop()
end
return uiElement

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.map) then return end
AnimationNode {
function(node)
Tree.level.camera:animateTo(playing:has(Tree.behaviors.map).displayedPosition, node)
end,
duration = 1500,
easing = easing.easeInOutCubic,
onEnd = function() Tree.level.selector:select(cid) end
}:run()
end
return function(values)
return endTurnButton:new(values)
end

View File

@ -1,23 +1,24 @@
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 build
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:show()
self.characterPanel:update(dt)
self.skillRow = SkillRow(cid)
self.skillRow:show()
elseif Tree.level.selector:deselected() then
self.characterPanel:hide()
self.skillRow:hide()
end
if self.skillRow then self.skillRow:update(dt) end
end
function layout:draw()
if self.characterPanel then self.characterPanel:draw() end
if self.skillRow then self.skillRow:draw() end
end
return layout

View File

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

View File

@ -1,19 +1,18 @@
local icons = require("lib.utils.sprite_atlas").load(Tree.assets.files.dev_icons)
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 UI_SCALE = require "lib.simple_ui.level.scale"
--- @class SkillButton : UIElement
--- @field hovered boolean
--- @field selected boolean
--- @field onClick function?
--- @field icon? string
--- @field icon string
local skillButton = setmetatable({}, Element)
skillButton.__index = skillButton
function skillButton:update(dt)
if not self.icon then return end
local mx, my = love.mouse.getPosition()
if self:hitTest(mx, my) then
self.hovered = true
@ -27,33 +26,22 @@ function skillButton:update(dt)
end
function skillButton:draw()
love.graphics.setLineWidth(2)
if not self.icon then
love.graphics.setColor(0.05, 0.05, 0.05)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
self:drawBorder("inner")
return
end
local quad = icons:pickQuad(self.icon)
love.graphics.push()
love.graphics.translate(self.bounds.x, self.bounds.y)
love.graphics.scale(self.bounds.width / icons.tileSize, self.bounds.height / icons.tileSize)
love.graphics.draw(icons.atlas, quad)
love.graphics.pop()
love.graphics.applyTransform(self.transform)
self:drawBorder("inner")
local r, g, b, a = love.graphics.getColor()
if self.selected then
love.graphics.setColor(0.3, 1, 0.3, 0.5)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setColor(0.3, 1, 0.3, a)
elseif self.hovered then
love.graphics.setColor(0.7, 1, 0.7, 0.5)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setColor(0.7, 1, 0.7, a)
else
love.graphics.setColor(1, 1, 1, a)
end
love.graphics.setColor(1, 1, 1)
love.graphics.translate(0, self.bounds.y)
love.graphics.draw(icons.atlas, icons:pickQuad(self.icon))
love.graphics.pop()
end
--------------------------------------------------------------------------------
@ -61,6 +49,8 @@ end
--- @class SkillRow : UIElement
--- @field characterId Id
--- @field selected SkillButton?
--- @field animationNode AnimationNode
--- @field state "show" | "idle" | "hide"
--- @field children SkillButton[]
local skillRow = setmetatable({}, Element)
skillRow.__index = skillRow
@ -70,6 +60,7 @@ skillRow.__index = skillRow
function skillRow.new(characterId)
local t = {
characterId = characterId,
state = "show",
children = {}
}
@ -96,89 +87,75 @@ function skillRow.new(characterId)
end
end)
for i = #t.children + 1, 7, 1 do
t.children[i] = skillButton:new {}
end
return t
end
--- @type love.Canvas
local c;
function skillRow: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 skillRow: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 skillRow:update(dt)
local iconSize = math.floor(64 * UI_SCALE)
if self.animationNode then self.animationNode:update(dt) end
local iconSize = icons.tileSize
local scale = (64 / iconSize)
local screenW, screenH = love.graphics.getDimensions()
local padding, margin = 8, 4
local count = #self.children -- слоты под скиллы
local padding = 8
local count = #self.children
self.bounds = Rect {
width = iconSize * count + (count + 1) * margin,
height = iconSize + 2 * margin,
width = count * icons.tileSize + (count - 1) * padding,
height = iconSize,
y = self.state == "show" and 10 * (1 - self.animationNode:getValue()) or 0
}
self.bounds.y = screenH - self.bounds.height - padding -- отступ снизу
self.bounds.x = screenW / 2 - self.bounds.width / 2
self.transform = love.math.newTransform():translate(screenW / 2,
screenH - 16):scale(scale, scale):translate(-self.bounds.width / 2, -iconSize)
for i, skb in ipairs(self.children) do
skb.bounds = Rect { x = self.bounds.x + margin + (i - 1) * (iconSize + margin), -- друг за другом, включая первый отступ от границы
y = self.bounds.y + margin, height = iconSize, width = iconSize }
skb.bounds = Rect { height = iconSize, width = iconSize }
skb.transform = self.transform:clone():translate(self.bounds.x + (i - 1) * iconSize +
(i - 1) *
padding, -- левый край ряда + размер предыдущих иконок + размер предыдущих отступов
self.bounds.y -- высота не меняется
)
skb:update(dt)
end
if not c then
c = love.graphics.newCanvas(self.bounds.width, self.bounds.height)
end
end
function skillRow:draw()
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)
-- сначала иконки скиллов
local alpha = 1
if self.state == "show" then
alpha = self.animationNode:getValue()
elseif self.state == "hide" then
alpha = 1 - self.animationNode:getValue()
end
love.graphics.setColor(1, 1, 1, alpha)
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()
end
love.graphics.setColor(1, 1, 1)
end
return skillRow.new

View File

@ -18,7 +18,7 @@ function rect.new(table)
end
function rect:hasPoint(x, y)
return x >= self.x and x < self.x + self.width and y >= self.y and y < self.y + self.height
return x >= self.x and x < self.width and y >= self.y and y < self.height
end
return rect.new

View File

@ -81,9 +81,8 @@ regenerateMana.tag = "dev_mana"
function regenerateMana:cast(caster, target)
caster:try(Tree.behaviors.stats, function(stats)
stats.mana = 10
stats.initiative = stats.initiative + 10
end)
print(caster.id, "has regenerated mana and gained initiative")
print(caster.id, "has regenerated mana")
local sprite = caster:has(Tree.behaviors.sprite)
if not sprite then return true end
AnimationNode {
@ -124,8 +123,6 @@ function attack:cast(caster, target)
local targetSprite = targetCharacter:has(Tree.behaviors.sprite)
if not sprite or not targetSprite then return true end
caster:try(Tree.behaviors.map, function(map) map:lookAt(target) end)
AnimationNode {
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
children = {

View File

@ -6,7 +6,6 @@
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.level = (require "lib.level.level").new("procedural", "flower_plains") -- для теста у нас только один уровень, который сразу же загружен

View File

@ -51,7 +51,7 @@ function AssetBundle.loadFile(path)
elseif (ext == "lua") then
return require(string.gsub(path, ".lua", ""))
end
return filedata
return nil
end
function AssetBundle.cutExtension(filename)

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,11 +1,11 @@
---@class PriorityQueue
---@field private data any[] внутренний массив-куча (индексация с 1)
---@field private cmp fun(a:any, b:any):boolean компаратор: true, если a выше по приоритету, чем b
---@field private data any[] # внутренний массив-куча (индексация с 1)
---@field private cmp fun(a:any, b:any):boolean # компаратор: true, если a выше по приоритету, чем b
local PriorityQueue = {}
PriorityQueue.__index = PriorityQueue
---Создать очередь с приоритетом.
---@param cmp fun(a:any, b:any):boolean|nil если nil, используется a < b (мин-куча)
---@param cmp fun(a:any, b:any):boolean|nil # если nil, используется a < b (мин-куча)
---@return PriorityQueue
function PriorityQueue.new(cmp)
local self = setmetatable({}, PriorityQueue)
@ -16,8 +16,8 @@ end
-- ===== Внутренние утилиты =====
---@param i integer индекс узла
---@param j integer индекс узла
---@param i integer @индекс узла
---@param j integer @индекс узла
function PriorityQueue:_swap(i, j)
self.data[i], self.data[j] = self.data[j], self.data[i]
end
@ -103,16 +103,4 @@ function PriorityQueue:is_empty()
return #self.data == 0
end
--- Shallow-копирование очереди
function PriorityQueue:copy()
local _data = {}
for i, v in ipairs(self.data) do
_data[i] = v
end
return setmetatable({
data = _data,
cmp = self.cmp
}, PriorityQueue)
end
return PriorityQueue

View File

@ -9,27 +9,18 @@ function love.conf(t)
end
function love.load()
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 })
character.spawn("Foodor", Tree.assets.files.sprites.character)
character.spawn("Baris", Tree.assets.files.sprites.character, Vec3 { 3, 3 })
love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true })
end
local lt = "0"
function love.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.controls:cache()
local t2 = love.timer.getTime()
@ -64,10 +55,7 @@ function love.draw()
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()