Compare commits
38 Commits
turn-order
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 284f2ac204 | |||
| 970047bded | |||
| 47dea647a0 | |||
| af00e8abd4 | |||
| 4161044dcc | |||
| 5c1a0b0c19 | |||
| 75550148f7 | |||
| 29e001e20f | |||
| 00f3db4ff9 | |||
| f5e5bce3ef | |||
| 9ce9b85dfa | |||
| e952d22d7f | |||
| 586ea68d2b | |||
| 2012035eb6 | |||
| 615738d06a | |||
| 7394249cb8 | |||
| 3d0d52438f | |||
| bc730ef48c | |||
| 73ba99734c | |||
| bc1c6cfd6a | |||
| f2169d333c | |||
| bcc376030c | |||
| c61c1875e7 | |||
| 411c435e7a | |||
| a9bb7df188 | |||
| 1376cf7041 | |||
| cd8d2768e0 | |||
| de24808a82 | |||
| aecc9acde0 | |||
| 187b8b3c74 | |||
| e7e4071931 | |||
| 59f1227033 | |||
| 123885b2b3 | |||
| c566d1669e | |||
| 331aefb0f6 | |||
| cdffff59c3 | |||
| c16870102b | |||
| 538bd1df33 |
19
assets/dev_icons/LICENSE.md
Normal file
19
assets/dev_icons/LICENSE.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Created by [Marvyra](https://www.gamedevmarket.net/member/marvyra)
|
||||||
|
|
||||||
|
4.1. A "Licence" means that the Seller grants to GDN (purely for the purpose of sub-licensing to the Purchaser) and GDN grants (by way of sub-licence thereof) to the Purchaser a non-exclusive perpetual licence to;
|
||||||
|
|
||||||
|
(a) use the Licensed Asset to create Derivative Works; and
|
||||||
|
|
||||||
|
(b) use the Licensed Asset and any Derivative Works as part of both Non-Monetized Media Products and Monetized Media Products, with no restriction on the number of projects the Licensed Asset may be used in. In either case, the Licensed Assets can be used in Media Products that are either:
|
||||||
|
|
||||||
|
i) used for the Purchaser's own personal use; and/or
|
||||||
|
|
||||||
|
ii) used for the Purchaser’s commercial use in which case it may be distributed, sold and supplied by the Purchaser for any fee that the Purchaser may determine.
|
||||||
|
|
||||||
|
4.2. A Licence does not allow the Purchaser to:
|
||||||
|
|
||||||
|
(a) Use the Licensed Asset or Derivative Works in a logo, trademark or service mark;
|
||||||
|
|
||||||
|
(b) Use, sell, share, transfer, give away, sublicense or redistribute the Licensed Asset or Derivate Works other than as part of the relevant Media Product; or
|
||||||
|
|
||||||
|
(c) Allow the user of the Media Product to extract the Licensed Asset or Derivative Works and use them outside of the relevant Media Product.
|
||||||
BIN
assets/dev_icons/atlas.png
Normal file
BIN
assets/dev_icons/atlas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
6
assets/dev_icons/manifest.lua
Normal file
6
assets/dev_icons/manifest.lua
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
return {
|
||||||
|
tileSize = 16,
|
||||||
|
["dev_attack"] = { 44 },
|
||||||
|
["dev_mana"] = { 42 },
|
||||||
|
["dev_move"] = { 51 },
|
||||||
|
}
|
||||||
91
assets/fonts/Roboto_Mono/OFL.txt
Normal file
91
assets/fonts/Roboto_Mono/OFL.txt
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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.
|
||||||
BIN
assets/fonts/Roboto_Mono/font.ttf
Normal file
BIN
assets/fonts/Roboto_Mono/font.ttf
Normal file
Binary file not shown.
94
assets/fonts/WDXL_Lubrifont_TC/OFL.txt
Normal file
94
assets/fonts/WDXL_Lubrifont_TC/OFL.txt
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
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.
|
||||||
BIN
assets/fonts/WDXL_Lubrifont_TC/font.ttf
Normal file
BIN
assets/fonts/WDXL_Lubrifont_TC/font.ttf
Normal file
Binary file not shown.
BIN
assets/masks/rrect32.png
Normal file
BIN
assets/masks/rrect32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/masks/squircle.png
Normal file
BIN
assets/masks/squircle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
8
assets/shaders/alpha_mask.glsl
Normal file
8
assets/shaders/alpha_mask.glsl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
24
assets/shaders/reveal.glsl
Normal file
24
assets/shaders/reveal.glsl
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
17
assets/shaders/soft_uniform_noise.glsl
Normal file
17
assets/shaders/soft_uniform_noise.glsl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
local easing = require "lib.utils.easing"
|
||||||
|
|
||||||
--- @alias voidCallback fun(): nil
|
--- @alias voidCallback fun(): nil
|
||||||
--- @alias animationRunner fun(node: AnimationNode)
|
--- @alias animationRunner fun(node: AnimationNode)
|
||||||
|
|
||||||
@ -28,6 +30,10 @@
|
|||||||
--- @field children AnimationNode[]
|
--- @field children AnimationNode[]
|
||||||
--- @field finish voidCallback
|
--- @field finish voidCallback
|
||||||
--- @field onEnd voidCallback?
|
--- @field onEnd voidCallback?
|
||||||
|
--- @field duration number продолжительность в миллисекундах
|
||||||
|
--- @field easing ease функция смягчения
|
||||||
|
--- @field t number прогресс анимации
|
||||||
|
--- @field state "running" | "waiting" | "finished"
|
||||||
local animation = {}
|
local animation = {}
|
||||||
animation.__index = animation
|
animation.__index = animation
|
||||||
|
|
||||||
@ -35,6 +41,7 @@ animation.__index = animation
|
|||||||
function animation:bubbleUp()
|
function animation:bubbleUp()
|
||||||
self.count = self.count - 1
|
self.count = self.count - 1
|
||||||
if self.count > 0 then return end
|
if self.count > 0 then return end
|
||||||
|
self.state = "finished"
|
||||||
if self.onEnd then self.onEnd() end
|
if self.onEnd then self.onEnd() end
|
||||||
if self.parent then self.parent:bubbleUp() end
|
if self.parent then self.parent:bubbleUp() end
|
||||||
end
|
end
|
||||||
@ -50,7 +57,23 @@ function animation:chain(children)
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param data {[1]: animationRunner?, onEnd?: voidCallback, children?: AnimationNode[]}
|
--- Возвращает текущий прогресс анимации с учетом смягчения
|
||||||
|
function animation:getValue()
|
||||||
|
return self.easing(self.t)
|
||||||
|
end
|
||||||
|
|
||||||
|
function animation:update(dt)
|
||||||
|
if self.state ~= "running" then return end
|
||||||
|
|
||||||
|
if self.t < 1 then
|
||||||
|
self.t = self.t + dt * 1000 / self.duration -- в знаменателе продолжительность анимации в секундах
|
||||||
|
else
|
||||||
|
self.t = 1
|
||||||
|
self:finish()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param data {[1]: animationRunner?, onEnd?: voidCallback, duration: number?, easing: ease?, children?: AnimationNode[]}
|
||||||
--- @return AnimationNode
|
--- @return AnimationNode
|
||||||
local function new(data)
|
local function new(data)
|
||||||
local t = setmetatable({}, animation)
|
local t = setmetatable({}, animation)
|
||||||
@ -61,7 +84,12 @@ local function new(data)
|
|||||||
t.count = 1 -- своя анимация
|
t.count = 1 -- своя анимация
|
||||||
t.children = {}
|
t.children = {}
|
||||||
t:chain(data.children or {})
|
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.finish = function()
|
||||||
|
t.state = "waiting"
|
||||||
t:bubbleUp()
|
t:bubbleUp()
|
||||||
for _, anim in ipairs(t.children) do
|
for _, anim in ipairs(t.children) do
|
||||||
anim:run()
|
anim:run()
|
||||||
|
|||||||
@ -24,6 +24,17 @@ function mapBehavior.new(position, size)
|
|||||||
}, mapBehavior)
|
}, mapBehavior)
|
||||||
end
|
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 path Deque
|
||||||
--- @param animationNode AnimationNode
|
--- @param animationNode AnimationNode
|
||||||
function mapBehavior:followPath(path, animationNode)
|
function mapBehavior:followPath(path, animationNode)
|
||||||
|
|||||||
@ -24,6 +24,10 @@ function behavior:endCast()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function behavior:update(dt)
|
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
|
if self.cast and self.state == "casting" then self.cast:update(self.owner, dt) end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
local utils = require "lib.utils.utils"
|
|
||||||
|
|
||||||
--- @alias Device "mouse" | "key" | "pad"
|
--- @alias Device "mouse" | "key" | "pad"
|
||||||
|
|
||||||
--- @param device Device
|
--- @param device Device
|
||||||
@ -17,6 +15,7 @@ controls.keymap = {
|
|||||||
cameraMoveRight = control("key", "d"),
|
cameraMoveRight = control("key", "d"),
|
||||||
cameraMoveDown = control("key", "s"),
|
cameraMoveDown = control("key", "s"),
|
||||||
cameraMoveScroll = control("mouse", "3"),
|
cameraMoveScroll = control("mouse", "3"),
|
||||||
|
cameraAnimateTo = control('key', 't'),
|
||||||
fullMana = control("key", "m"),
|
fullMana = control("key", "m"),
|
||||||
select = control("mouse", "1"),
|
select = control("mouse", "1"),
|
||||||
endTurnTest = control("key", "e"),
|
endTurnTest = control("key", "e"),
|
||||||
@ -26,6 +25,12 @@ controls.keymap = {
|
|||||||
local currentKeys = {}
|
local currentKeys = {}
|
||||||
local cachedKeys = {}
|
local cachedKeys = {}
|
||||||
|
|
||||||
|
--- @type number
|
||||||
|
controls.mouseWheelY = 0
|
||||||
|
love.wheelmoved = function(x, y)
|
||||||
|
controls.mouseWheelY = y
|
||||||
|
end
|
||||||
|
|
||||||
--- polling controls in O(n)
|
--- polling controls in O(n)
|
||||||
--- should be called at the beginning of every frame
|
--- should be called at the beginning of every frame
|
||||||
function controls:poll()
|
function controls:poll()
|
||||||
@ -49,6 +54,7 @@ function controls:cache()
|
|||||||
for k, v in pairs(currentKeys) do
|
for k, v in pairs(currentKeys) do
|
||||||
cachedKeys[k] = v
|
cachedKeys[k] = v
|
||||||
end
|
end
|
||||||
|
controls.mouseWheelY = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
--- marks a control consumed for the current frame
|
--- marks a control consumed for the current frame
|
||||||
|
|||||||
@ -9,12 +9,15 @@ local EPSILON = 0.001
|
|||||||
--- @field speed number
|
--- @field speed number
|
||||||
--- @field pixelsPerMeter integer
|
--- @field pixelsPerMeter integer
|
||||||
--- @field scale number
|
--- @field scale number
|
||||||
|
--- @field animationNode AnimationNode?
|
||||||
|
--- @field animationEndPosition Vec3
|
||||||
|
--- @field animationBeginPosition Vec3
|
||||||
local camera = {
|
local camera = {
|
||||||
position = Vec3 {},
|
position = Vec3 {},
|
||||||
velocity = Vec3 {},
|
velocity = Vec3 {},
|
||||||
acceleration = 0.2,
|
acceleration = 0.2,
|
||||||
speed = 5,
|
speed = 5,
|
||||||
pixelsPerMeter = 24,
|
pixelsPerMeter = 32,
|
||||||
}
|
}
|
||||||
|
|
||||||
function camera:getDefaultScale()
|
function camera:getDefaultScale()
|
||||||
@ -27,12 +30,6 @@ 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 = {
|
local controlMap = {
|
||||||
cameraMoveUp = Vec3({ 0, -1 }),
|
cameraMoveUp = Vec3({ 0, -1 }),
|
||||||
cameraMoveLeft = Vec3({ -1 }),
|
cameraMoveLeft = Vec3({ -1 }),
|
||||||
@ -41,6 +38,19 @@ local controlMap = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function camera:update(dt)
|
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
|
local ps = Tree.panning
|
||||||
if ps.delta:length() > 0 then
|
if ps.delta:length() > 0 then
|
||||||
local worldDelta = ps.delta:scale(1 / (self.pixelsPerMeter * self.scale)):scale(dt):scale(self.speed)
|
local worldDelta = ps.delta:scale(1 / (self.pixelsPerMeter * self.scale)):scale(dt):scale(self.speed)
|
||||||
@ -87,6 +97,16 @@ function camera:detach()
|
|||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
end
|
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
|
--- @return Camera
|
||||||
local function new()
|
local function new()
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
|
|||||||
@ -35,7 +35,6 @@ function level:update(dt)
|
|||||||
el:update(dt)
|
el:update(dt)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
self.camera:update(dt)
|
|
||||||
self.selector:update(dt)
|
self.selector:update(dt)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
--- @class Selector
|
--- @class Selector
|
||||||
--- @field id Id | nil
|
--- @field id Id | nil
|
||||||
|
--- @field lastId Id | nil
|
||||||
--- @field locked boolean
|
--- @field locked boolean
|
||||||
local selector = {}
|
local selector = {}
|
||||||
selector.__index = selector
|
selector.__index = selector
|
||||||
@ -15,6 +16,7 @@ function selector:select(characterId)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function selector:update(dt)
|
function selector:update(dt)
|
||||||
|
self.lastId = self.id
|
||||||
if self.locked or not Tree.controls:isJustPressed("select") then return end
|
if self.locked or not Tree.controls:isJustPressed("select") then return end
|
||||||
|
|
||||||
local mousePosition = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor()
|
local mousePosition = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor()
|
||||||
@ -32,11 +34,8 @@ function selector:update(dt)
|
|||||||
|
|
||||||
char:try(Tree.behaviors.spellcaster, function(b)
|
char:try(Tree.behaviors.spellcaster, function(b)
|
||||||
if not b.cast then
|
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
|
end
|
||||||
if b.cast:cast(char, mousePosition) then
|
if b.cast:cast(char, mousePosition) then
|
||||||
self:lock()
|
self:lock()
|
||||||
@ -57,6 +56,16 @@ function selector:unlock()
|
|||||||
self.locked = false
|
self.locked = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- If a character was selected during this tick, returns its Id
|
||||||
|
function selector:selected()
|
||||||
|
if self.id and self.id ~= self.lastId then return self.id end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- If a character was **de**selected during this tick, returns its Id
|
||||||
|
function selector:deselected()
|
||||||
|
if not self.id and self.lastId then return self.lastId end
|
||||||
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
new = new
|
new = new
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,6 @@ end
|
|||||||
---
|
---
|
||||||
--- Если в очереди на ход больше никого нет, заканчиваем раунд
|
--- Если в очереди на ход больше никого нет, заканчиваем раунд
|
||||||
function turnOrder:next()
|
function turnOrder:next()
|
||||||
Tree.level.selector.id = nil
|
|
||||||
self.actedQueue:insert(self.current)
|
self.actedQueue:insert(self.current)
|
||||||
local next = self.pendingQueue:peek()
|
local next = self.pendingQueue:peek()
|
||||||
if not next then return self:endRound() end
|
if not next then return self:endRound() end
|
||||||
|
|||||||
20
lib/simple_ui/color.lua
Normal file
20
lib/simple_ui/color.lua
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
--- @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
|
||||||
109
lib/simple_ui/element.lua
Normal file
109
lib/simple_ui/element.lua
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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 Общий градиент поверх элемента (интерполированный меш)
|
||||||
|
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
|
||||||
|
|
||||||
|
function uiElement:draw() end
|
||||||
|
|
||||||
|
function uiElement:hitTest(screenX, screenY)
|
||||||
|
return self.bounds:hasPoint(screenX, screenY)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @generic T : UIElement
|
||||||
|
--- @param values table
|
||||||
|
--- @param self T
|
||||||
|
--- @return T
|
||||||
|
function uiElement.new(self, values)
|
||||||
|
values.bounds = values.bounds or Rect {}
|
||||||
|
values.overlayGradientMesh = values.overlayGradientMesh or uiElement.overlayGradientMesh;
|
||||||
|
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
|
||||||
71
lib/simple_ui/level/bar.lua
Normal file
71
lib/simple_ui/level/bar.lua
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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
|
||||||
86
lib/simple_ui/level/bottom_bars.lua
Normal file
86
lib/simple_ui/level/bottom_bars.lua
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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
|
||||||
128
lib/simple_ui/level/cpanel.lua
Normal file
128
lib/simple_ui/level/cpanel.lua
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
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
|
||||||
66
lib/simple_ui/level/end_turn.lua
Normal file
66
lib/simple_ui/level/end_turn.lua
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
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
|
||||||
23
lib/simple_ui/level/layout.lua
Normal file
23
lib/simple_ui/level/layout.lua
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
local CPanel = require "lib.simple_ui.level.cpanel"
|
||||||
|
|
||||||
|
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)
|
||||||
|
elseif Tree.level.selector:deselected() then
|
||||||
|
self.characterPanel:hide()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function layout:draw()
|
||||||
|
if self.characterPanel then self.characterPanel:draw() end
|
||||||
|
end
|
||||||
|
|
||||||
|
return layout
|
||||||
2
lib/simple_ui/level/scale.lua
Normal file
2
lib/simple_ui/level/scale.lua
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
local UI_SCALE = 0.75 -- выдуманное значение для dependency injection, надо подбирать так, чтобы UI_SCALE * 64 было целым числом
|
||||||
|
return UI_SCALE
|
||||||
184
lib/simple_ui/level/skill_row.lua
Normal file
184
lib/simple_ui/level/skill_row.lua
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
local icons = require("lib.utils.sprite_atlas").load(Tree.assets.files.dev_icons)
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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 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()
|
||||||
|
|
||||||
|
self:drawBorder("inner")
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
love.graphics.setColor(1, 1, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
--- @class SkillRow : UIElement
|
||||||
|
--- @field characterId Id
|
||||||
|
--- @field selected SkillButton?
|
||||||
|
--- @field children SkillButton[]
|
||||||
|
local skillRow = setmetatable({}, Element)
|
||||||
|
skillRow.__index = skillRow
|
||||||
|
|
||||||
|
--- @param characterId Id
|
||||||
|
--- @return SkillRow
|
||||||
|
function skillRow.new(characterId)
|
||||||
|
local t = {
|
||||||
|
characterId = characterId,
|
||||||
|
children = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(t, skillRow)
|
||||||
|
|
||||||
|
local char = Tree.level.characters[characterId]
|
||||||
|
char:try(Tree.behaviors.spellcaster, function(behavior)
|
||||||
|
for i, spell in ipairs(behavior.spellbook) do
|
||||||
|
local skb = skillButton:new { icon = spell.tag }
|
||||||
|
skb.onClick = function()
|
||||||
|
skb.selected = not skb.selected
|
||||||
|
if t.selected then t.selected.selected = false end
|
||||||
|
t.selected = skb
|
||||||
|
|
||||||
|
if not behavior.cast then
|
||||||
|
behavior.cast = behavior.spellbook[i]
|
||||||
|
behavior.state = "casting"
|
||||||
|
else
|
||||||
|
behavior.state = "idle"
|
||||||
|
behavior.cast = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
t.children[i] = skb
|
||||||
|
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:update(dt)
|
||||||
|
local iconSize = math.floor(64 * UI_SCALE)
|
||||||
|
local screenW, screenH = love.graphics.getDimensions()
|
||||||
|
local padding, margin = 8, 4
|
||||||
|
local count = #self.children -- слоты под скиллы
|
||||||
|
|
||||||
|
self.bounds = Rect {
|
||||||
|
width = iconSize * count + (count + 1) * margin,
|
||||||
|
height = iconSize + 2 * margin,
|
||||||
|
}
|
||||||
|
self.bounds.y = screenH - self.bounds.height - padding -- отступ снизу
|
||||||
|
self.bounds.x = screenW / 2 - self.bounds.width / 2
|
||||||
|
|
||||||
|
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: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)
|
||||||
|
|
||||||
|
-- сначала иконки скиллов
|
||||||
|
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
|
||||||
24
lib/simple_ui/rect.lua
Normal file
24
lib/simple_ui/rect.lua
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
--- @class Rect
|
||||||
|
--- @field x number
|
||||||
|
--- @field y number
|
||||||
|
--- @field width number
|
||||||
|
--- @field height number
|
||||||
|
local rect = {}
|
||||||
|
rect.__index = rect
|
||||||
|
|
||||||
|
--- @param table {x: number, y: number, width: number, height: number}
|
||||||
|
function rect.new(table)
|
||||||
|
local r = {
|
||||||
|
x = table.x or 0,
|
||||||
|
y = table.y or 0,
|
||||||
|
width = table.width or 0,
|
||||||
|
height = table.height or 0
|
||||||
|
}
|
||||||
|
return setmetatable(r, rect)
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
return rect.new
|
||||||
@ -10,11 +10,13 @@
|
|||||||
local AnimationNode = require "lib.animation_node"
|
local AnimationNode = require "lib.animation_node"
|
||||||
|
|
||||||
--- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell
|
--- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell
|
||||||
|
--- @field tag string
|
||||||
--- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла
|
--- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла
|
||||||
--- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире
|
--- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире
|
||||||
--- @field cast fun(self: Spell, caster: Character, target: Vec3): boolean Вызывается в момент каста, изменяет мир. Возвращает bool в зависимости от того, получилось ли скастовать
|
--- @field cast fun(self: Spell, caster: Character, target: Vec3): boolean Вызывается в момент каста, изменяет мир. Возвращает bool в зависимости от того, получилось ли скастовать
|
||||||
local spell = {}
|
local spell = {}
|
||||||
spell.__index = spell
|
spell.__index = spell
|
||||||
|
spell.tag = "base"
|
||||||
|
|
||||||
function spell:update(caster, dt) end
|
function spell:update(caster, dt) end
|
||||||
|
|
||||||
@ -26,6 +28,7 @@ local walk = setmetatable({
|
|||||||
--- @type Deque
|
--- @type Deque
|
||||||
path = nil
|
path = nil
|
||||||
}, spell)
|
}, spell)
|
||||||
|
walk.tag = "dev_move"
|
||||||
|
|
||||||
function walk:cast(caster, target)
|
function walk:cast(caster, target)
|
||||||
if not caster:try(Tree.behaviors.stats, function(stats)
|
if not caster:try(Tree.behaviors.stats, function(stats)
|
||||||
@ -73,6 +76,7 @@ function walk:draw()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local regenerateMana = setmetatable({}, spell)
|
local regenerateMana = setmetatable({}, spell)
|
||||||
|
regenerateMana.tag = "dev_mana"
|
||||||
|
|
||||||
function regenerateMana:cast(caster, target)
|
function regenerateMana:cast(caster, target)
|
||||||
caster:try(Tree.behaviors.stats, function(stats)
|
caster:try(Tree.behaviors.stats, function(stats)
|
||||||
@ -93,6 +97,7 @@ function regenerateMana:cast(caster, target)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local attack = setmetatable({}, spell)
|
local attack = setmetatable({}, spell)
|
||||||
|
attack.tag = "dev_attack"
|
||||||
|
|
||||||
function attack:cast(caster, target)
|
function attack:cast(caster, target)
|
||||||
if caster:try(Tree.behaviors.map, function(map)
|
if caster:try(Tree.behaviors.map, function(map)
|
||||||
@ -119,6 +124,8 @@ function attack:cast(caster, target)
|
|||||||
local targetSprite = targetCharacter:has(Tree.behaviors.sprite)
|
local targetSprite = targetCharacter:has(Tree.behaviors.sprite)
|
||||||
if not sprite or not targetSprite then return true end
|
if not sprite or not targetSprite then return true end
|
||||||
|
|
||||||
|
caster:try(Tree.behaviors.map, function(map) map:lookAt(target) end)
|
||||||
|
|
||||||
AnimationNode {
|
AnimationNode {
|
||||||
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
|
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
|
||||||
children = {
|
children = {
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
Tree = {
|
Tree = {
|
||||||
assets = (require "lib.utils.asset_bundle"):load()
|
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.panning = require "lib/panning"
|
||||||
Tree.controls = require "lib.controls"
|
Tree.controls = require "lib.controls"
|
||||||
Tree.level = (require "lib.level.level").new("procedural", "flower_plains") -- для теста у нас только один уровень, который сразу же загружен
|
Tree.level = (require "lib.level.level").new("procedural", "flower_plains") -- для теста у нас только один уровень, который сразу же загружен
|
||||||
|
|||||||
569
lib/ui/core.lua
569
lib/ui/core.lua
@ -1,569 +0,0 @@
|
|||||||
local controls = require "lib.controls"
|
|
||||||
|
|
||||||
---@class UIConstraints
|
|
||||||
---@field min_w number
|
|
||||||
---@field min_h number
|
|
||||||
---@field max_w number
|
|
||||||
---@field max_h number
|
|
||||||
|
|
||||||
local ui = {}
|
|
||||||
|
|
||||||
-- =============== Constraints helpers ===============
|
|
||||||
|
|
||||||
local function make_constraints(min_w, min_h, max_w, max_h)
|
|
||||||
return {
|
|
||||||
min_w = min_w or 0,
|
|
||||||
min_h = min_h or 0,
|
|
||||||
max_w = max_w or math.huge,
|
|
||||||
max_h = max_h or math.huge,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function clamp(v, lo, hi)
|
|
||||||
if v < lo then return lo end
|
|
||||||
if v > hi then return hi end
|
|
||||||
return v
|
|
||||||
end
|
|
||||||
|
|
||||||
local function clamp_size(w, h, c)
|
|
||||||
return clamp(w, c.min_w, c.max_w), clamp(h, c.min_h, c.max_h)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function loosen(c)
|
|
||||||
-- "loose" constraints (0..max), удобно для wrap-контейнеров/Align
|
|
||||||
return make_constraints(0, 0, c.max_w, c.max_h)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function tighten(c, w, h)
|
|
||||||
-- exact (tight) constraints — зажимают размер ребёнка
|
|
||||||
w, h = clamp_size(w, h, c)
|
|
||||||
return make_constraints(w, h, w, h)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- =============== Base elements ===============
|
|
||||||
|
|
||||||
--- @class Element
|
|
||||||
--- @field parent Element|nil
|
|
||||||
--- @field children Element[]
|
|
||||||
--- @field child Element|nil
|
|
||||||
--- @field origin Vec3
|
|
||||||
--- @field size Vec3
|
|
||||||
--- @field state table
|
|
||||||
local Element = {}
|
|
||||||
Element.__index = Element
|
|
||||||
|
|
||||||
function Element:new(props)
|
|
||||||
local o = setmetatable({}, self)
|
|
||||||
o.parent = nil
|
|
||||||
o.children = {}
|
|
||||||
o.origin = Vec3 { 0, 0, 0 }
|
|
||||||
o.size = Vec3 { 0, 0, 0 }
|
|
||||||
o.state = {}
|
|
||||||
if props then
|
|
||||||
-- Копируем "публичные" поля; Таблицы клонируем, если можно
|
|
||||||
for k, v in pairs(props) do
|
|
||||||
if type(v) == "table" then
|
|
||||||
o[k] = v.copy and v:copy() or v
|
|
||||||
else
|
|
||||||
o[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param dt number
|
|
||||||
function Element:update(dt)
|
|
||||||
-- По умолчанию спускаем update вниз
|
|
||||||
for _, ch in ipairs(self.children or {}) do
|
|
||||||
ch:update(dt)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param constraints UIConstraints
|
|
||||||
function Element:layout(constraints)
|
|
||||||
-- Базовое поведение: размер = объединённый bbox детей (wrap-content)
|
|
||||||
local max_w, max_h = 0, 0
|
|
||||||
for _, ch in ipairs(self.children) do
|
|
||||||
ch:layout(loosen(constraints)) -- по умолчанию не заставляем детей растягиваться
|
|
||||||
max_w = math.max(max_w, ch.size.x)
|
|
||||||
max_h = math.max(max_h, ch.size.y)
|
|
||||||
end
|
|
||||||
local w, h = clamp_size(max_w, max_h, constraints)
|
|
||||||
self.size.x, self.size.y = w, h
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param origin Vec3
|
|
||||||
function Element:arrange(origin)
|
|
||||||
-- Базово: ставим себя, а детей кладём в (0,0) внутри нас
|
|
||||||
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
|
||||||
for _, ch in ipairs(self.children) do
|
|
||||||
ch:arrange(Vec3 { self.origin.x, self.origin.y, self.origin.z })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Element:draw()
|
|
||||||
-- По умолчанию — только рисуем детей
|
|
||||||
for _, ch in ipairs(self.children) do
|
|
||||||
ch:draw()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- calls the callback if clicked inside own bounding box (rectangular)
|
|
||||||
--- @param callback function
|
|
||||||
function Element:onTap(callback)
|
|
||||||
local mx, my = love.mouse.getPosition()
|
|
||||||
if mx > self.origin.x and mx < self.origin.x + self.size.x
|
|
||||||
and my > self.origin.y and my < self.origin.y + self.size.y
|
|
||||||
then
|
|
||||||
if controls:isJustPressed("select") then
|
|
||||||
controls:consume("select")
|
|
||||||
callback()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- =============== SingleChild / MultiChild базовые ===============
|
|
||||||
|
|
||||||
--- @class SingleChildElement: Element
|
|
||||||
local SingleChildElement = setmetatable({}, { __index = Element })
|
|
||||||
SingleChildElement.__index = SingleChildElement
|
|
||||||
|
|
||||||
function SingleChildElement:new(props)
|
|
||||||
local o = Element.new(self, props)
|
|
||||||
if o.child then
|
|
||||||
o.child.parent = o
|
|
||||||
o.children = { o.child }
|
|
||||||
end
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class MultiChildElement: Element
|
|
||||||
local MultiChildElement = setmetatable({}, { __index = Element })
|
|
||||||
MultiChildElement.__index = MultiChildElement
|
|
||||||
|
|
||||||
function MultiChildElement:new(props)
|
|
||||||
local o = Element.new(self, props)
|
|
||||||
o.children = o.children or o.children or {}
|
|
||||||
o.children = o.children -- ensure array
|
|
||||||
if props and props.children then
|
|
||||||
for _, ch in ipairs(props.children) do
|
|
||||||
ch.parent = o
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
-- =============== Root ===============
|
|
||||||
|
|
||||||
---@class Root: SingleChildElement
|
|
||||||
local Root = setmetatable({}, { __index = SingleChildElement })
|
|
||||||
Root.__index = Root
|
|
||||||
|
|
||||||
function Root:new(props)
|
|
||||||
return SingleChildElement.new(self, props)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Root:update(dt)
|
|
||||||
-- Root может делать глобальные обновления, но главное — спустить вниз
|
|
||||||
if self.child then self.child:update(dt) end
|
|
||||||
|
|
||||||
-- На каждом кадре делаем полный проход раскладки:
|
|
||||||
local w, h = love.graphics.getWidth(), love.graphics.getHeight()
|
|
||||||
self.size = Vec3 { w, h, 0 }
|
|
||||||
local constraints = make_constraints(w, h, w, h) -- tight: размер окна
|
|
||||||
if self.child then
|
|
||||||
-- layout с точными ограничениями родителя
|
|
||||||
self.child:layout(loosen(constraints)) -- разрешаем контенту быть меньше окна
|
|
||||||
-- arrange кладём ребёнка в начало (0,0), а Align/др. уже выровняет внутри себя
|
|
||||||
self.child:arrange(Vec3 { 0, 0, 0 })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Root:draw()
|
|
||||||
if self.child then self.child:draw() end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- =============== Rectangle (leaf) ===============
|
|
||||||
|
|
||||||
---@class Rectangle: Element
|
|
||||||
---@field color number[] @ {r,g,b} 0..1
|
|
||||||
---@field size Vec3
|
|
||||||
local Rectangle = setmetatable({}, { __index = Element })
|
|
||||||
Rectangle.__index = Rectangle
|
|
||||||
|
|
||||||
function Rectangle:new(props)
|
|
||||||
local o = Element.new(self, props)
|
|
||||||
--- @cast o Rectangle
|
|
||||||
o.color = o.color or { 1, 1, 1 }
|
|
||||||
-- size должен быть задан; если нет — size = 0,0
|
|
||||||
o.size = o.size or Vec3 { 0, 0, 0 }
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
function Rectangle:layout(constraints)
|
|
||||||
local w = self.size.x or 0
|
|
||||||
local h = self.size.y or 0
|
|
||||||
w, h = clamp_size(w, h, constraints)
|
|
||||||
self.size.x, self.size.y = w, h
|
|
||||||
end
|
|
||||||
|
|
||||||
function Rectangle:draw()
|
|
||||||
love.graphics.setColor(self.color[1], self.color[2], self.color[3], 1)
|
|
||||||
love.graphics.rectangle("fill", self.origin.x, self.origin.y, self.size.x, self.size.y)
|
|
||||||
-- Сброс цвета — по вкусу
|
|
||||||
end
|
|
||||||
|
|
||||||
-- =============== Align (single child) ===============
|
|
||||||
|
|
||||||
---@class Align: SingleChildElement
|
|
||||||
---@field alignment string "center_left" | "center" | "center_right" | "top_left" | "top_center" | "top_right" | "bottom_left" | "bottom_center" | "bottom_right"
|
|
||||||
---@field expand boolean если true — занимает все доступное от родителя
|
|
||||||
local Align = setmetatable({}, { __index = SingleChildElement })
|
|
||||||
Align.__index = Align
|
|
||||||
|
|
||||||
function Align:new(props)
|
|
||||||
local o = SingleChildElement.new(self, props)
|
|
||||||
---@cast o Align
|
|
||||||
o.alignment = o.alignment or "center"
|
|
||||||
o.expand = (o.expand ~= nil) and o.expand or true -- по умолчанию растягиваемся под родителя
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
function Align:layout(constraints)
|
|
||||||
if self.child then
|
|
||||||
-- Ребёнка считаем "loose" — пусть занимает сколько хочет
|
|
||||||
self.child:layout(loosen(constraints))
|
|
||||||
if self.expand then
|
|
||||||
-- Сам Align займет максимум
|
|
||||||
self.size.x, self.size.y = clamp_size(constraints.max_w, constraints.max_h, constraints)
|
|
||||||
else
|
|
||||||
-- Или же wrap по ребёнку
|
|
||||||
local w, h = self.child.size.x, self.child.size.y
|
|
||||||
self.size.x, self.size.y = clamp_size(w, h, constraints)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
self.size.x, self.size.y = clamp_size(0, 0, constraints)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Align:arrange(origin)
|
|
||||||
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
|
||||||
if not self.child then return end
|
|
||||||
|
|
||||||
local pw, ph = self.size.x, self.size.y
|
|
||||||
local cw, ch = self.child.size.x, self.child.size.y
|
|
||||||
local x, y = self.origin.x, self.origin.y
|
|
||||||
|
|
||||||
--- @todo сделать красиво
|
|
||||||
if self.alignment == "center" then
|
|
||||||
x = x + (pw - cw) / 2
|
|
||||||
y = y + (ph - ch) / 2
|
|
||||||
elseif self.alignment == "center_left" then
|
|
||||||
y = y + (ph - ch) / 2
|
|
||||||
elseif self.alignment == "center_right" then
|
|
||||||
x = x + (pw - cw)
|
|
||||||
y = y + (ph - ch) / 2
|
|
||||||
elseif self.alignment == "top_left" then
|
|
||||||
-- x,y остаются
|
|
||||||
elseif self.alignment == "top_center" then
|
|
||||||
x = x + (pw - cw) / 2
|
|
||||||
elseif self.alignment == "top_right" then
|
|
||||||
x = x + (pw - cw)
|
|
||||||
elseif self.alignment == "bottom_left" then
|
|
||||||
y = y + (ph - ch)
|
|
||||||
elseif self.alignment == "bottom_center" then
|
|
||||||
y = y + (ph - ch)
|
|
||||||
x = x + (pw - cw) / 2
|
|
||||||
elseif self.alignment == "bottom_right" then
|
|
||||||
x = x + (pw - cw)
|
|
||||||
y = y + (ph - ch)
|
|
||||||
else
|
|
||||||
-- неизвестное — по центру
|
|
||||||
x = x + (pw - cw) / 2
|
|
||||||
y = y + (ph - ch) / 2
|
|
||||||
end
|
|
||||||
|
|
||||||
self.child:arrange(Vec3 { x, y, self.origin.z })
|
|
||||||
end
|
|
||||||
|
|
||||||
-- =============== Row / Column (multi) ===============
|
|
||||||
|
|
||||||
-- helper: прочитать flex у ребёнка (если есть)
|
|
||||||
local function get_flex_of(ch)
|
|
||||||
return (ch.get_flex and ch:get_flex()) or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class Row: MultiChildElement
|
|
||||||
---@field gap number расстояние между детьми
|
|
||||||
local Row = setmetatable({}, { __index = MultiChildElement })
|
|
||||||
Row.__index = Row
|
|
||||||
|
|
||||||
function Row:new(props)
|
|
||||||
local o = MultiChildElement.new(self, props)
|
|
||||||
---@cast o Row
|
|
||||||
o.gap = o.gap or 0
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
function Row:layout(constraints)
|
|
||||||
local total_gap = (#self.children > 1) and (self.gap * (#self.children - 1)) or 0
|
|
||||||
|
|
||||||
local fixed_w, max_h = 0, 0
|
|
||||||
local flex_children, total_flex = {}, 0
|
|
||||||
local loose = loosen(constraints)
|
|
||||||
|
|
||||||
-- проход 1: меряем нефлекс-детей, собираем флекс-список
|
|
||||||
for _, ch in ipairs(self.children) do
|
|
||||||
local f = get_flex_of(ch)
|
|
||||||
if f > 0 then
|
|
||||||
total_flex = total_flex + f
|
|
||||||
flex_children[#flex_children + 1] = { node = ch, flex = f }
|
|
||||||
else
|
|
||||||
ch:layout(loose)
|
|
||||||
fixed_w = fixed_w + ch.size.x
|
|
||||||
if ch.size.y > max_h then max_h = ch.size.y end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local min_row_w = fixed_w + total_gap
|
|
||||||
local wants_expand = total_flex > 0
|
|
||||||
|
|
||||||
-- КЛЮЧ: если есть флекс → растягиваемся до доступного max_w
|
|
||||||
local candidate = wants_expand and constraints.max_w or min_row_w
|
|
||||||
local target_w = clamp(candidate, math.max(min_row_w, constraints.min_w), constraints.max_w)
|
|
||||||
local remaining = math.max(0, target_w - min_row_w)
|
|
||||||
|
|
||||||
-- проход 2: раздаём остаток флекс-детям (tight по ширине)
|
|
||||||
if total_flex > 0 and remaining > 0 then
|
|
||||||
local acc = 0
|
|
||||||
for i, item in ipairs(flex_children) do
|
|
||||||
local alloc = (i == #flex_children)
|
|
||||||
and (remaining - acc) -- последний добирает остаток для суммирования в точности в target_w
|
|
||||||
or (remaining * item.flex / total_flex)
|
|
||||||
acc = acc + alloc
|
|
||||||
local c = make_constraints(alloc, 0, alloc, loose.max_h)
|
|
||||||
item.node:layout(c)
|
|
||||||
if item.node.size.y > max_h then max_h = item.node.size.y end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- даже если remaining==0, флекс-детей всё равно надо промерить (tight нулевой шириной)
|
|
||||||
for _, item in ipairs(flex_children) do
|
|
||||||
local c = make_constraints(0, 0, 0, loose.max_h)
|
|
||||||
item.node:layout(c)
|
|
||||||
if item.node.size.y > max_h then max_h = item.node.size.y end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.size.x, self.size.y = target_w, clamp(max_h, constraints.min_h, constraints.max_h)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Row:arrange(origin)
|
|
||||||
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
|
||||||
local cursor_x = self.origin.x
|
|
||||||
local y_base = self.origin.y
|
|
||||||
|
|
||||||
for i, ch in ipairs(self.children) do
|
|
||||||
-- Вертикальное выравнивание: по базовой линии (top). Можно прокачать до "center" / "bottom".
|
|
||||||
ch:arrange(Vec3 { cursor_x, y_base, self.origin.z })
|
|
||||||
cursor_x = cursor_x + ch.size.x + (i < #self.children and self.gap or 0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class Column: MultiChildElement
|
|
||||||
---@field gap number
|
|
||||||
local Column = setmetatable({}, { __index = MultiChildElement })
|
|
||||||
Column.__index = Column
|
|
||||||
|
|
||||||
function Column:new(props)
|
|
||||||
local o = MultiChildElement.new(self, props)
|
|
||||||
--- @cast o Column
|
|
||||||
o.gap = o.gap or 0
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
function Column:layout(constraints)
|
|
||||||
local total_gap = (#self.children > 1) and (self.gap * (#self.children - 1)) or 0
|
|
||||||
|
|
||||||
local fixed_h, max_w = 0, 0
|
|
||||||
local flex_children, total_flex = {}, 0
|
|
||||||
local loose = loosen(constraints)
|
|
||||||
|
|
||||||
for _, ch in ipairs(self.children) do
|
|
||||||
local f = get_flex_of(ch)
|
|
||||||
if f > 0 then
|
|
||||||
total_flex = total_flex + f
|
|
||||||
flex_children[#flex_children + 1] = { node = ch, flex = f }
|
|
||||||
else
|
|
||||||
ch:layout(loose)
|
|
||||||
fixed_h = fixed_h + ch.size.y
|
|
||||||
if ch.size.x > max_w then max_w = ch.size.x end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local min_col_h = fixed_h + total_gap
|
|
||||||
local wants_expand = (total_flex > 0)
|
|
||||||
local candidate = wants_expand and constraints.max_h or min_col_h
|
|
||||||
local target_h = clamp(candidate, math.max(min_col_h, constraints.min_h), constraints.max_h)
|
|
||||||
local remaining = math.max(0, target_h - min_col_h)
|
|
||||||
|
|
||||||
if total_flex > 0 and remaining > 0 then
|
|
||||||
local acc = 0
|
|
||||||
for i, item in ipairs(flex_children) do
|
|
||||||
local alloc = (i == #flex_children)
|
|
||||||
and (remaining - acc)
|
|
||||||
or (remaining * item.flex / total_flex)
|
|
||||||
acc = acc + alloc
|
|
||||||
local c = make_constraints(0, alloc, loose.max_w, alloc)
|
|
||||||
item.node:layout(c)
|
|
||||||
if item.node.size.x > max_w then max_w = item.node.size.x end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
for _, item in ipairs(flex_children) do
|
|
||||||
local c = make_constraints(0, 0, loose.max_w, 0)
|
|
||||||
item.node:layout(c)
|
|
||||||
if item.node.size.x > max_w then max_w = item.node.size.x end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.size.x, self.size.y = clamp(max_w, constraints.min_w, constraints.max_w), target_h
|
|
||||||
end
|
|
||||||
|
|
||||||
function Column:arrange(origin)
|
|
||||||
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
|
||||||
local x_base = self.origin.x
|
|
||||||
local cursor_y = self.origin.y
|
|
||||||
|
|
||||||
for i, ch in ipairs(self.children) do
|
|
||||||
ch:arrange(Vec3 { x_base, cursor_y, self.origin.z })
|
|
||||||
cursor_y = cursor_y + ch.size.y + (i < #self.children and self.gap or 0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
|
|
||||||
---@class Expanded: SingleChildElement
|
|
||||||
---@field flex integer -- коэффициент флекса (>=1)
|
|
||||||
local Expanded = setmetatable({}, { __index = SingleChildElement })
|
|
||||||
Expanded.__index = Expanded
|
|
||||||
|
|
||||||
---@param props { flex?: integer, child?: Element }|nil
|
|
||||||
---@return Expanded
|
|
||||||
function Expanded:new(props)
|
|
||||||
local o = SingleChildElement.new(self, props)
|
|
||||||
---@cast o Expanded
|
|
||||||
o.flex = (o.flex and math.max(1, math.floor(o.flex))) or 1
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
function Expanded:get_flex()
|
|
||||||
return self.flex
|
|
||||||
end
|
|
||||||
|
|
||||||
function Expanded:layout(constraints)
|
|
||||||
if self.child then
|
|
||||||
self.child:layout(constraints)
|
|
||||||
-- Становимся размером с ребёнка с учётом ограничений
|
|
||||||
local w, h = self.child.size.x, self.child.size.y
|
|
||||||
w, h = clamp_size(w, h, constraints)
|
|
||||||
self.size.x, self.size.y = w, h
|
|
||||||
else
|
|
||||||
-- Пустой Expanded — просто занимает выделенное родителем место (можно для Spacer)
|
|
||||||
local w, h = clamp_size(0, 0, constraints)
|
|
||||||
self.size.x, self.size.y = w, h
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Expanded:arrange(origin)
|
|
||||||
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
|
||||||
if self.child then
|
|
||||||
self.child:arrange(self.origin)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- =============== Padding (single child) ===============
|
|
||||||
|
|
||||||
---@class Padding: SingleChildElement
|
|
||||||
---@field padding {left?: number, right?: number, top?: number, bottom?: number}
|
|
||||||
local Padding = setmetatable({}, { __index = SingleChildElement })
|
|
||||||
Padding.__index = Padding
|
|
||||||
|
|
||||||
---@param props { padding?: {left?: number, right?: number, top?: number, bottom?: number}, child?: Element }|nil
|
|
||||||
---@return Padding
|
|
||||||
function Padding:new(props)
|
|
||||||
local o = SingleChildElement.new(self, props)
|
|
||||||
---@cast o Padding
|
|
||||||
local p = o.padding or {}
|
|
||||||
o.padding = {
|
|
||||||
left = tonumber(p.left) or 0,
|
|
||||||
right = tonumber(p.right) or 0,
|
|
||||||
top = tonumber(p.top) or 0,
|
|
||||||
bottom = tonumber(p.bottom) or 0,
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
function Padding:layout(constraints)
|
|
||||||
local L = self.padding.left
|
|
||||||
local R = self.padding.right
|
|
||||||
local T = self.padding.top
|
|
||||||
local B = self.padding.bottom
|
|
||||||
local hp = L + R
|
|
||||||
local vp = T + B
|
|
||||||
|
|
||||||
-- Вычитаем паддинги из ограничений для ребёнка
|
|
||||||
local child_min_w = math.max(0, constraints.min_w - hp)
|
|
||||||
local child_min_h = math.max(0, constraints.min_h - vp)
|
|
||||||
local child_max_w = math.max(0, constraints.max_w - hp)
|
|
||||||
local child_max_h = math.max(0, constraints.max_h - vp)
|
|
||||||
local child_c = make_constraints(child_min_w, child_min_h, child_max_w, child_max_h)
|
|
||||||
|
|
||||||
if self.child then
|
|
||||||
self.child:layout(child_c)
|
|
||||||
local w = self.child.size.x + hp
|
|
||||||
local h = self.child.size.y + vp
|
|
||||||
self.size.x, self.size.y = clamp_size(w, h, constraints)
|
|
||||||
else
|
|
||||||
-- Нет ребёнка: размер равен сумме паддингов (с учётом ограничений)
|
|
||||||
self.size.x, self.size.y = clamp_size(hp, vp, constraints)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Padding:arrange(origin)
|
|
||||||
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
|
||||||
if not self.child then return end
|
|
||||||
local L = self.padding.left
|
|
||||||
local T = self.padding.top
|
|
||||||
self.child:arrange(Vec3 { self.origin.x + L, self.origin.y + T, self.origin.z })
|
|
||||||
end
|
|
||||||
|
|
||||||
-- =============== Public constructors (callable) ===============
|
|
||||||
|
|
||||||
local function mk_ctor(class)
|
|
||||||
-- Сохраняем существующую метатаблицу (в ней уже есть __index на родителя!)
|
|
||||||
local mt = getmetatable(class) or {}
|
|
||||||
mt.__call = function(_, props)
|
|
||||||
return class:new(props or {})
|
|
||||||
end
|
|
||||||
setmetatable(class, mt)
|
|
||||||
return class
|
|
||||||
end
|
|
||||||
|
|
||||||
ui.Element = Element
|
|
||||||
ui.Root = mk_ctor(Root)
|
|
||||||
ui.Align = mk_ctor(Align)
|
|
||||||
ui.Row = mk_ctor(Row)
|
|
||||||
ui.Column = mk_ctor(Column)
|
|
||||||
ui.Rectangle = mk_ctor(Rectangle)
|
|
||||||
ui.Expanded = mk_ctor(Expanded)
|
|
||||||
ui.Padding = mk_ctor(Padding)
|
|
||||||
|
|
||||||
-- Экспорт вспомогательных, если пригодится
|
|
||||||
ui.constraints = {
|
|
||||||
make = make_constraints,
|
|
||||||
loosen = loosen,
|
|
||||||
tighten = tighten,
|
|
||||||
clamp_size = clamp_size,
|
|
||||||
}
|
|
||||||
|
|
||||||
return ui
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
local Vec3 = require "lib.utils.vec3"
|
|
||||||
local ui = require "lib.ui.core"
|
|
||||||
|
|
||||||
|
|
||||||
--- @class SkillButton : Rectangle
|
|
||||||
--- @field owner Character
|
|
||||||
--- @field spellId number
|
|
||||||
local SkillButton = ui.Rectangle {
|
|
||||||
size = Vec3 { 100, 100 },
|
|
||||||
color = { 1, 0, 0 },
|
|
||||||
}
|
|
||||||
function SkillButton:update(dt)
|
|
||||||
ui.Rectangle.update(self, dt)
|
|
||||||
self.owner:try(Tree.behaviors.spellcaster, function(spellcaster)
|
|
||||||
self.color = spellcaster.state == "casting" and { 0, 1, 0 } or { 1, 0, 0 }
|
|
||||||
self:onTap(function()
|
|
||||||
if not spellcaster.cast then
|
|
||||||
spellcaster.cast = spellcaster.spellbook
|
|
||||||
[self.spellId]
|
|
||||||
spellcaster.state = "casting"
|
|
||||||
else
|
|
||||||
spellcaster.state = "idle"
|
|
||||||
spellcaster.cast = nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local skillRows = {}
|
|
||||||
|
|
||||||
local layout = {}
|
|
||||||
function layout:build()
|
|
||||||
return ui.Root {
|
|
||||||
child = ui.Align {
|
|
||||||
alignment = "bottom_center",
|
|
||||||
child =
|
|
||||||
--- для каждого персонажа строим свой ряд скиллов, сохраняем его на потом и возвращаем
|
|
||||||
--- если персонаж не выделен, не возвращаем ничего
|
|
||||||
(function()
|
|
||||||
local id = Tree.level.selector.id
|
|
||||||
if not id then return nil end
|
|
||||||
if skillRows[id] then return skillRows[id] end
|
|
||||||
local r =
|
|
||||||
ui.Row {
|
|
||||||
children = {
|
|
||||||
ui.Padding { padding = { left = 4, right = 4 }, child = setmetatable({ owner = Tree.level.characters[id], spellId = 1 }, { __index = SkillButton }) },
|
|
||||||
ui.Padding { padding = { left = 4, right = 4 }, child = setmetatable({ owner = Tree.level.characters[id], spellId = 2 }, { __index = SkillButton }) },
|
|
||||||
ui.Padding { padding = { left = 4, right = 4 }, child = setmetatable({ owner = Tree.level.characters[id], spellId = 3 }, { __index = SkillButton }) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
skillRows[id] = r
|
|
||||||
return r
|
|
||||||
end)()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
return layout
|
|
||||||
@ -51,7 +51,7 @@ function AssetBundle.loadFile(path)
|
|||||||
elseif (ext == "lua") then
|
elseif (ext == "lua") then
|
||||||
return require(string.gsub(path, ".lua", ""))
|
return require(string.gsub(path, ".lua", ""))
|
||||||
end
|
end
|
||||||
return nil
|
return filedata
|
||||||
end
|
end
|
||||||
|
|
||||||
function AssetBundle.cutExtension(filename)
|
function AssetBundle.cutExtension(filename)
|
||||||
|
|||||||
57
lib/utils/easing.lua
Normal file
57
lib/utils/easing.lua
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
---- Функции смягчения, нормализованные к [0, 1]
|
||||||
|
|
||||||
|
---@alias ease fun(x: number): number
|
||||||
|
|
||||||
|
local easing = {}
|
||||||
|
|
||||||
|
--- @type ease
|
||||||
|
function easing.linear(x)
|
||||||
|
return x
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type ease
|
||||||
|
function easing.easeInSine(x)
|
||||||
|
return 1 - math.cos((x * math.pi) / 2);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type ease
|
||||||
|
function easing.easeOutSine(x)
|
||||||
|
return math.sin((x * math.pi) / 2);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type ease
|
||||||
|
function easing.easeInOutSine(x)
|
||||||
|
return -(math.cos(x * math.pi) - 1) / 2;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type ease
|
||||||
|
function easing.easeInQuad(x)
|
||||||
|
return x * x
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type ease
|
||||||
|
function easing.easeOutQuad(x)
|
||||||
|
return 1 - (1 - x) * (1 - x)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type ease
|
||||||
|
function easing.easeInOutQuad(x)
|
||||||
|
return x < 0.5 and 2 * x * x or 1 - math.pow(-2 * x + 2, 2) / 2;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type ease
|
||||||
|
function easing.easeInCubic(x)
|
||||||
|
return x * x * x
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type ease
|
||||||
|
function easing.easeOutCubic(x)
|
||||||
|
return 1 - math.pow(1 - x, 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type ease
|
||||||
|
function easing.easeInOutCubic(x)
|
||||||
|
return x < 0.5 and 4 * x * x * x or 1 - math.pow(-2 * x + 2, 3) / 2;
|
||||||
|
end
|
||||||
|
|
||||||
|
return easing
|
||||||
94
lib/utils/font_manager.lua
Normal file
94
lib/utils/font_manager.lua
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
--- @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
|
||||||
28
main.lua
28
main.lua
@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
local character = require "lib/character/character"
|
local character = require "lib/character/character"
|
||||||
require "lib/tree"
|
require "lib/tree"
|
||||||
local layout = require "lib.ui.layout"
|
local testLayout = require "lib.simple_ui.level.layout"
|
||||||
|
|
||||||
|
|
||||||
function love.conf(t)
|
function love.conf(t)
|
||||||
t.console = true
|
t.console = true
|
||||||
@ -19,29 +18,18 @@ function love.load()
|
|||||||
end
|
end
|
||||||
Tree.level.turnOrder:endRound()
|
Tree.level.turnOrder:endRound()
|
||||||
print("Now playing:", Tree.level.turnOrder.current)
|
print("Now playing:", Tree.level.turnOrder.current)
|
||||||
love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true })
|
love.window.setMode(1280, 720, { resizable = true, msaa = 4, vsync = true })
|
||||||
end
|
end
|
||||||
|
|
||||||
local lt = "0"
|
local lt = "0"
|
||||||
function love.update(dt)
|
function love.update(dt)
|
||||||
local t1 = love.timer.getTime()
|
local t1 = love.timer.getTime()
|
||||||
Tree.controls:poll()
|
Tree.controls:poll()
|
||||||
Widgets = layout:build()
|
Tree.level.camera:update(dt) -- сначала логика камеры, потому что на нее завязан UI
|
||||||
Widgets:update(dt) -- логика UI-слоя должна отработать раньше всех, потому что нужно перехватить жесты и не пустить их дальше
|
testLayout:update(dt) -- потом UI, потому что нужно перехватить жесты и не пустить их дальше
|
||||||
Tree.panning:update(dt)
|
Tree.panning:update(dt)
|
||||||
Tree.level:update(dt)
|
Tree.level: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()
|
Tree.controls:cache()
|
||||||
|
|
||||||
local t2 = love.timer.getTime()
|
local t2 = love.timer.getTime()
|
||||||
@ -73,11 +61,13 @@ function love.draw()
|
|||||||
Tree.level:draw()
|
Tree.level:draw()
|
||||||
|
|
||||||
Tree.level.camera:detach()
|
Tree.level.camera:detach()
|
||||||
|
testLayout:draw()
|
||||||
Widgets:draw()
|
|
||||||
love.graphics.setColor(1, 1, 1)
|
love.graphics.setColor(1, 1, 1)
|
||||||
|
|
||||||
local stats = "fps: " .. love.timer.getFPS() .. " lt: " .. lt .. " dt: " .. dt
|
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)
|
||||||
love.graphics.print(stats, 10, 10)
|
love.graphics.print(stats, 10, 10)
|
||||||
|
|
||||||
local t2 = love.timer.getTime()
|
local t2 = love.timer.getTime()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user