feature/effects #37
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||||
BIN
assets/audio/music/level1/progressive_plains.ogg
(Stored with Git LFS)
Normal file
BIN
assets/audio/sounds/meow.ogg
(Stored with Git LFS)
Normal file
BIN
assets/boar.png
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 130 B |
BIN
assets/cats.png
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 572 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 368 B After Width: | Height: | Size: 128 B |
85
assets/shaders/sprite_light.glsl
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
#define MAX_LIGHTS 8
|
||||||
|
|
||||||
|
struct Light {
|
||||||
|
vec2 position;
|
||||||
|
vec3 color;
|
||||||
|
float radius;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern Light lights[MAX_LIGHTS];
|
||||||
|
extern int num_lights;
|
||||||
|
extern vec2 sprite_pos; // Мировая позиция спрайта (в метрах)
|
||||||
|
extern vec3 ambient; // Эмбиентное освещение
|
||||||
|
extern vec3 sky; // Цвет неба
|
||||||
|
|
||||||
|
// Параметры выделения
|
||||||
|
extern bool is_selected;
|
||||||
|
extern vec2 tex_size;
|
||||||
|
extern float time;
|
||||||
|
|
||||||
|
// Функция для имитации easing.easeInSine
|
||||||
|
float easeInSine(float x) {
|
||||||
|
return 1.0 - cos((x * 3.14159) / 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords)
|
||||||
|
{
|
||||||
|
vec4 texColor = Texel(tex, texture_coords);
|
||||||
|
|
||||||
|
// ЛОГИКА ОБВОДКИ (Outline)
|
||||||
|
// Мы выполняем ее до расчетов освещения. Если пиксель прозрачный и объект выбран,
|
||||||
|
// проверяем соседей, чтобы понять, не граница ли это.
|
||||||
|
// Обводка рисуется "самосветящейся", чтобы выделение было видно даже в полной темноте.
|
||||||
|
if (is_selected && texColor.a <= 0.0) {
|
||||||
|
float maxAlpha = 0.0;
|
||||||
|
|
||||||
|
// Проверяем соседние пиксели (квадратом 3x3)
|
||||||
|
for (float x = -1.0; x <= 1.0; x++) {
|
||||||
|
for (float y = -1.0; y <= 1.0; y++) {
|
||||||
|
if (x == 0.0 && y == 0.0) continue;
|
||||||
|
vec2 offset = vec2(x, y) / tex_size;
|
||||||
|
maxAlpha = max(maxAlpha, Texel(tex, texture_coords + offset).a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxAlpha > 0.0) {
|
||||||
|
// Эффект пульсации и "бегущей волны" из оригинального шейдера outline.glsl
|
||||||
|
float modY = (0.75 + sin(time) * 0.25) * (0.5 + cos(texture_coords.y * 10.0 + time * 2.0) * 0.5);
|
||||||
|
return vec4(vec3(modY, 0.2 * sin(time) + 0.5, 0.5), 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (texColor.a == 0.0) {
|
||||||
|
return vec4(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Расчет базового освещения мира: персонаж освещается смесью неба (Sky) и отраженного света (Ambient).
|
||||||
|
vec3 baseLight = ambient + (vec3(1.0) - ambient) * sky;
|
||||||
|
|
||||||
|
// Десатурация базового света на 30%.
|
||||||
|
float luma = dot(baseLight, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
vec3 characterBaseLight = mix(baseLight, vec3(luma), 0.3);
|
||||||
|
|
||||||
|
vec3 pointLight = vec3(0.0);
|
||||||
|
|
||||||
|
for (int i = 0; i < num_lights; i++) {
|
||||||
|
vec2 lightPos = lights[i].position;
|
||||||
|
float radius = lights[i].radius;
|
||||||
|
vec2 lightVec = lightPos - sprite_pos;
|
||||||
|
float dist = length(lightVec);
|
||||||
|
|
||||||
|
// Плавное затухание света по радиусу.
|
||||||
|
float radiusFalloff = smoothstep(radius, radius * 0.2, dist);
|
||||||
|
|
||||||
|
// Реализация псевдо-проекции (3D-эффект):
|
||||||
|
float frontWeight = smoothstep(-2.0, 0.5, lightVec.y);
|
||||||
|
|
||||||
|
float attenuation = radiusFalloff * frontWeight;
|
||||||
|
pointLight += lights[i].color * attenuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
pointLight = clamp(pointLight, 0.0, 1.0);
|
||||||
|
|
||||||
|
// Финальный расчет цвета:
|
||||||
|
return vec4(texColor.rgb * (characterBaseLight + pointLight), texColor.a);
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 130 B |
@ -9,5 +9,6 @@ Tree.behaviors.positioned = require "character.behaviors.positioned"
|
|||||||
Tree.behaviors.tiled = require "character.behaviors.tiled"
|
Tree.behaviors.tiled = require "character.behaviors.tiled"
|
||||||
Tree.behaviors.cursor = require "character.behaviors.cursor"
|
Tree.behaviors.cursor = require "character.behaviors.cursor"
|
||||||
Tree.behaviors.ai = require "lib.character.behaviors.ai"
|
Tree.behaviors.ai = require "lib.character.behaviors.ai"
|
||||||
|
Tree.behaviors.effects = require "lib.character.behaviors.effects"
|
||||||
|
|
||||||
--- @alias voidCallback fun(): nil
|
--- @alias voidCallback fun(): nil
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
local easing = require "lib.utils.easing"
|
local easing = require "lib.utils.easing"
|
||||||
|
local pf = require "lib.pathfinder"
|
||||||
|
local utils = require "lib.utils.utils"
|
||||||
|
|
||||||
|
--- @alias AIAction fun(self: AIBehavior): Task<nil>
|
||||||
|
|
||||||
|
--- @return Character
|
||||||
local function closestCharacter(char)
|
local function closestCharacter(char)
|
||||||
local caster = Vec3 {}
|
local caster = Vec3 {}
|
||||||
char:try(Tree.behaviors.positioned, function(b)
|
char:try(Tree.behaviors.positioned, function(b)
|
||||||
@ -20,45 +25,142 @@ local function closestCharacter(char)
|
|||||||
return charTarget
|
return charTarget
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- --- Возвращает все точки в радиусе в виде векторов (должен по крайней мере)
|
||||||
|
-- --- @param radius integer
|
||||||
|
-- --- @param center Vec3
|
||||||
|
-- --- @return Vec3[]
|
||||||
|
-- local function circleVectors(center, radius)
|
||||||
|
-- local vecs = {}
|
||||||
|
-- local res = {}
|
||||||
|
-- for t = 0, 2 * math.pi, EPSILON do
|
||||||
|
-- local x = math.cos(t) * radius + center.x
|
||||||
|
-- local y = math.sin(t) * radius + center.y
|
||||||
|
-- table.insert(vecs, Vec3 { math.floor(x), math.floor(y) })
|
||||||
|
-- end
|
||||||
|
-- for _, v in pairs(vecs) do
|
||||||
|
-- local i = 1
|
||||||
|
-- while i <= #res and (res[i].x ~= v.x or res[i].y ~= v.y) do
|
||||||
|
-- i = i + 1
|
||||||
|
-- end
|
||||||
|
-- if i == #res + 1 or #res == 0 then
|
||||||
|
-- table.insert(res, v)
|
||||||
|
-- print('[AI]: circle vecs:', v)
|
||||||
|
-- end
|
||||||
|
-- end
|
||||||
|
-- return res
|
||||||
|
-- end
|
||||||
|
|
||||||
|
--- Возвращает все точки в радиусе в виде векторов (должен по крайней мере)
|
||||||
|
--- @param radius integer
|
||||||
|
--- @param center Vec3
|
||||||
|
--- @return Vec3[]
|
||||||
|
local function circleVectors(center, radius)
|
||||||
|
local dx, dy, err = radius, 0, 1 - radius
|
||||||
|
local vecs, res = {}, {}
|
||||||
|
while dx >= dy do
|
||||||
|
table.insert(vecs, Vec3 { center.x + dx, center.y + dy })
|
||||||
|
table.insert(vecs, Vec3 { center.x - dx, center.y + dy })
|
||||||
|
table.insert(vecs, Vec3 { center.x + dx, center.y - dy })
|
||||||
|
table.insert(vecs, Vec3 { center.x - dx, center.y - dy })
|
||||||
|
table.insert(vecs, Vec3 { center.x + dy, center.y + dx })
|
||||||
|
table.insert(vecs, Vec3 { center.x - dy, center.y + dx })
|
||||||
|
table.insert(vecs, Vec3 { center.x + dy, center.y - dx })
|
||||||
|
table.insert(vecs, Vec3 { center.x - dy, center.y - dx })
|
||||||
|
dy = dy + 1
|
||||||
|
if err < 0 then
|
||||||
|
err = err + 2 * dy + 1
|
||||||
|
else
|
||||||
|
dx, err = dx - 1, err + 2 * (dy - dx) + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, v in pairs(vecs) do
|
||||||
|
local i = 1
|
||||||
|
while i <= #res and (res[i].x ~= v.x or res[i].y ~= v.y) and v.x >= 0 and v.y >= 0 do
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
if i == #res + 1 or #res == 0 then
|
||||||
|
table.insert(res, v)
|
||||||
|
print('[AI]: circle vecs:', v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return vecs
|
||||||
|
end
|
||||||
|
|
||||||
|
--- ищет пути к ближайшему персу в определённом радиусе
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param radius integer здесь мы должны сами определять, сколько должны не доходить до персонажа (1 <= n)
|
||||||
|
--- @return Vec3|nil
|
||||||
|
local function pathToClosestCharacter(owner, radius)
|
||||||
|
local charTarget = closestCharacter(owner)
|
||||||
|
local targetPosition, ownerPosition = charTarget:has(Tree.behaviors.positioned), owner:has(Tree.behaviors.positioned)
|
||||||
|
if not targetPosition or not ownerPosition then return end
|
||||||
|
|
||||||
|
local circleVecs = circleVectors(targetPosition.position, radius)
|
||||||
|
local target = circleVecs[#circleVecs]
|
||||||
|
local path = pf(ownerPosition.position, target)
|
||||||
|
for i, c in ipairs(circleVecs) do
|
||||||
|
local newPath = pf(ownerPosition.position, c)
|
||||||
|
if newPath:size() < path:size() then
|
||||||
|
path = newPath
|
||||||
|
target = c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return target
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type table<Class, AIAction>
|
||||||
|
local aiNature = {
|
||||||
|
dev_warrior = function(self)
|
||||||
|
return function(callback) -- почему так, описано в Task
|
||||||
|
self.owner:try(Tree.behaviors.spellcaster, function(spellB)
|
||||||
|
self.target = pathToClosestCharacter(self.owner, 1)
|
||||||
|
local attackTarget = closestCharacter(self.owner):has(Tree.behaviors.positioned)
|
||||||
|
if not attackTarget then return end
|
||||||
|
local task1 = spellB.spellbook[1]:cast(self.owner, self.target)
|
||||||
|
if task1 then
|
||||||
|
task1(
|
||||||
|
function()
|
||||||
|
-- здесь мы оказываемся после того, как сходили в первый раз
|
||||||
|
print('[AI]: я походил')
|
||||||
|
local task2 = spellB.spellbook[3]:cast(self.owner, attackTarget.position)
|
||||||
|
if task2 then
|
||||||
|
-- дергаем функцию после завершения хода
|
||||||
|
print('[AI]: и ударил')
|
||||||
|
task2(callback)
|
||||||
|
else
|
||||||
|
print('[AI]: чёт не бьётся')
|
||||||
|
callback()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
else
|
||||||
|
print('рот этого казино')
|
||||||
|
callback()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
dev_mage = function(self)
|
||||||
|
return function(callback)
|
||||||
|
print("etoh... bleh")
|
||||||
|
callback()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--- @class AIBehavior : Behavior
|
--- @class AIBehavior : Behavior
|
||||||
--- @field target Vec3?
|
--- @field target Vec3?
|
||||||
local behavior = {}
|
local behavior = {}
|
||||||
behavior.__index = behavior
|
behavior.__index = behavior
|
||||||
behavior.id = "ai"
|
behavior.id = "ai"
|
||||||
|
|
||||||
function behavior.new()
|
--- @param class Class
|
||||||
return setmetatable({}, behavior)
|
function behavior.new(class)
|
||||||
end
|
return setmetatable({
|
||||||
|
makeTurn = aiNature[class]
|
||||||
--- @return Task<nil>
|
}, behavior)
|
||||||
function behavior:makeTurn()
|
|
||||||
return function(callback) -- почему так, описано в Task
|
|
||||||
self.owner:try(Tree.behaviors.spellcaster, function(spellB)
|
|
||||||
local charTarget = closestCharacter(self.owner)
|
|
||||||
charTarget:try(Tree.behaviors.positioned, function(b)
|
|
||||||
self.target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти
|
|
||||||
end)
|
|
||||||
|
|
||||||
local task1 = spellB.spellbook[1]:cast(self.owner, self.target)
|
|
||||||
if task1 then
|
|
||||||
task1(
|
|
||||||
function()
|
|
||||||
-- здесь мы оказываемся после того, как сходили в первый раз
|
|
||||||
local newTarget = Vec3 { 1, 1 }
|
|
||||||
local task2 = spellB.spellbook[1]:cast(self.owner, newTarget)
|
|
||||||
if task2 then
|
|
||||||
-- дергаем функцию после завершения хода
|
|
||||||
task2(callback)
|
|
||||||
else
|
|
||||||
callback()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
)
|
|
||||||
else
|
|
||||||
callback()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return behavior
|
return behavior
|
||||||
|
|||||||
187
lib/character/behaviors/effects.lua
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
local task = require "lib.utils.task"
|
||||||
|
local efb = require "lib.effectbook"
|
||||||
|
local book = efb.book
|
||||||
|
|
||||||
|
--- ===========ЛОГИКА ЭФФЕКТОВ И ЧТО С ЭТИМ ЕДЯТ===========
|
||||||
|
--- читать здесь: https://docs.google.com/document/d/1Hxa5dOLaeRpLQOs5H-oIDDuLLhKbDw40lR9d62Zb4Tg/edit?usp=sharing
|
||||||
|
--- и здесь: https://docs.google.com/document/d/1jvhuM3mxqYSQTEM8m-WL-uUSie9QRsZOCCUEiw9ZqzM/edit?tab=t.0
|
||||||
|
|
||||||
|
--- behavior thats holds all effects that we applied
|
||||||
|
--- @class EffectsBehavior : Behavior
|
||||||
|
--- @field effectsPriority EffectTag[] хранит эффекты в порядке их применения
|
||||||
|
--- @field effectsProperties table<EffectTag, { stacks: integer, intensity: integer }> хранит характеристики эффектов
|
||||||
|
local behavior = {}
|
||||||
|
behavior.__index = behavior
|
||||||
|
behavior.id = "effects"
|
||||||
|
|
||||||
|
--- @return EffectsBehavior
|
||||||
|
function behavior.new()
|
||||||
|
return setmetatable({
|
||||||
|
effectsPriority = {},
|
||||||
|
effectsProperties = {},
|
||||||
|
}, behavior)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- проверяет, можно ли наложить эффект и при наложении его применяет
|
||||||
|
--- @param effect EffectTag
|
||||||
|
--- @param stacks integer
|
||||||
|
--- @param intensity integer
|
||||||
|
function behavior:addEffect(effect, stacks, intensity)
|
||||||
|
local task1, birthStatement = book[effect]:beforeBirth(self.owner, intensity)
|
||||||
|
if task1 then
|
||||||
|
task1(function() end)
|
||||||
|
end
|
||||||
|
if not birthStatement then return end
|
||||||
|
|
||||||
|
-- проверка на сумму, и её применение
|
||||||
|
for i, ef in ipairs(self.effectsPriority) do
|
||||||
|
|
|||||||
|
if efb.sums[effect] then
|
||||||
|
if efb.sums[effect][ef] then
|
||||||
|
if not efb.sums[effect][ef](self.owner, effect, ef) then return end
|
||||||
|
end
|
||||||
|
elseif efb.sums[ef] then
|
||||||
|
if efb.sums[ef][effect] then
|
||||||
|
if not efb.sums[ef][effect](self.owner, ef, effect) then return end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
book[effect]:onBirth(self.owner, stacks, intensity)
|
||||||
|
|
||||||
|
local task3 = book[effect]:afterBirth(self.owner, intensity)
|
||||||
|
if task3 then
|
||||||
|
task3(function()
|
||||||
|
print("[Effects]: мы применили эффект!!")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Удаляет один эффект по порядку
|
||||||
|
--- @param effect EffectTag
|
||||||
|
function behavior:deleteEffect(effect)
|
||||||
|
self.effectsProperties[effect] = nil
|
||||||
|
for i, ef in ipairs(self.effectsPriority) do
|
||||||
|
if ef == effect then
|
||||||
|
table.remove(self.effectsPriority, i)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- О ДААА ЭТА ФУНКЦИЯ МЕНЯЕТ СОСТОЯНИЕ О ДАААААА О ДАААААААААА
|
||||||
|
--- @param effect EffectTag
|
||||||
|
--- @param amount integer
|
||||||
|
function behavior:deleteStacks(effect, amount)
|
||||||
|
print("[Effects]: удаляем стаки!!")
|
||||||
|
PeaAshMeter
commented
ладно, это ужас, но не ужас-ужас ладно, это ужас, но не ужас-ужас
neckrat
commented
по академически я должен был написать линейный поиск через цикл while, но я необучаемое быдло 🤡 по академически я должен был написать линейный поиск через цикл while, но я необучаемое быдло 🤡
PeaAshMeter
commented
академически у тебя не должно быть линейных поисков вообще, в этом-то и суть академически у тебя не должно быть линейных поисков вообще, в этом-то и суть
PeaAshMeter
commented
а у тебя получается внутри линейного а у тебя получается внутри линейного `for i, ef in ipairs` линейный `table.remove`. Это не ужас-ужас, потому что ты гарантируешь срабатывание `table.remove` не более одного раза
neckrat
commented
этот моментик я бы тоже переписал этот моментик я бы тоже переписал
PeaAshMeter
commented
типа красиво-алгоритмически - это все на тегах и все за O(1), кроме последовательного срабатывания всех эффектов (очевидно) типа красиво-алгоритмически - это все на тегах и все за O(1), **кроме** последовательного срабатывания всех эффектов (очевидно)
neckrat
commented
думаю, хотя бы для этого система тэгов не пригодится (надеюсь) думаю, хотя бы для этого система тэгов не пригодится (надеюсь)
PeaAshMeter
commented
ну если я правильно собираю в голове ход мыслей, то нужны адекватные теги (уникальные; один тег к одному эффекту), и отдельно группы тегов (один ко многим). Например, чтобы объединить 15 версий невидимости в одну группу "невидимость" и не давать выбрать таких персонажей в таргет ну если я правильно собираю в голове ход мыслей, то нужны адекватные теги (уникальные; один тег к одному эффекту), и отдельно группы тегов (один ко многим). Например, чтобы объединить 15 версий невидимости в одну группу "невидимость" и не давать выбрать таких персонажей в таргет
PeaAshMeter
commented
это переусложнение, которое уже ближе к реальному геймдизайну, где, как обычно, боссы могут иметь немного другие версии тех же самых эффектов это переусложнение, которое уже ближе к реальному геймдизайну, где, как обычно, боссы могут иметь немного другие версии тех же самых эффектов
neckrat
commented
нам же эти же тэги пригодятся для спеллов в конце концов, для их систематизации нам же эти же тэги пригодятся для спеллов в конце концов, для их систематизации
PeaAshMeter
commented
не вижу необходимости иметь одну общую систему тегов для спеллов и для эффектов не вижу необходимости иметь одну общую систему тегов для спеллов и для эффектов
neckrat
commented
не думаю, что это такое уж переусложнение, мы же не только ради эффектов это делаем в конце концов не думаю, что это такое уж переусложнение, мы же не только ради эффектов это делаем в конце концов
PeaAshMeter
commented
типа можно, но пока не знаю, зачем > не вижу необходимости иметь одну общую систему тегов для спеллов и для эффектов
типа можно, но пока не знаю, зачем
neckrat
commented
вай нот? dry и всё такое
вай нот? dry и всё такое
> не вижу необходимости иметь одну общую систему тегов для спеллов и для эффектов
PeaAshMeter
commented
тогда будет нейминг "spell_myspell" и "effect_myeffect" тогда будет нейминг "spell_myspell" и "effect_myeffect"
PeaAshMeter
commented
так-то у нас не стоит задача по тегу понимать еще и тип объекта, у нас для этого инструменты языка есть, не на бейсике пишем так-то у нас не стоит задача по тегу понимать еще и тип объекта, у нас для этого инструменты языка есть, не на бейсике пишем
neckrat
commented
справедливо справедливо
PeaAshMeter
commented
фактически у нас как будто есть таблицы в реляционной СУБД Microsoft Access: spells и effects. фактически у нас как будто есть таблицы в реляционной СУБД Microsoft Access: spells и effects.
мы и так понимаем, что это разные таблицы. В рамках разных таблиц айдишники не обязаны быть уникальными
PeaAshMeter
commented
типа другое дело что у нас у characters айдишники номерные, а у статических данных - строковые. Но это по историческим причинам и потому что, как бы, статические данные типа спеллов и эффектов мы как раз хотим находить по удобному идентификатору, а динамические типа персонажей - не хотим типа другое дело что у нас у characters айдишники номерные, а у статических данных - строковые. Но это по историческим причинам и потому что, как бы, статические данные типа спеллов и эффектов мы как раз хотим находить по удобному идентификатору, а динамические типа персонажей - не хотим
neckrat
commented
ну да, да, очевидно ну да, да, очевидно
neckrat
commented
у нас персонажи вообще в роли энтити, там со строковыми тэгами не выйдет у нас персонажи вообще в роли энтити, там со строковыми тэгами не выйдет
PeaAshMeter
commented
короче нормальная архитектура вроде короче нормальная архитектура вроде
|
|||||||
|
self.effectsProperties[effect].stacks = self.effectsProperties[effect].stacks -
|
||||||
|
amount -- !!!!!!!!!!!!!!!! <<<<< 21+ only
|
||||||
|
if self.effectsProperties[effect].stacks <= 0 then
|
||||||
|
print("[Effects]:", effect, "ДОЛЖЕН БЫТЬ СТЁРТ")
|
||||||
|
self:deleteEffect(effect)
|
||||||
|
print("[Effects]:", effect, "СТЁРТ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- должна вызываться перед смертью персонажа;
|
||||||
|
---
|
||||||
|
--- возвращает, убивать ли персонажа
|
||||||
|
--- @return boolean
|
||||||
|
function behavior:beforeDeath()
|
||||||
|
for i, ef in ipairs(self.effectsPriority) do
|
||||||
|
local task1, deathStatement = book[ef]:beforeDeath(self.owner, self.effectsProperties[ef].intensity)
|
||||||
|
if task1 then
|
||||||
|
task1(function() end)
|
||||||
|
end
|
||||||
|
if deathStatement == false then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- должна вызываться после смерти персонажа (может ли такая ситуация возникнуть вообще?)
|
||||||
|
function behavior:afterDeath()
|
||||||
|
for i, ef in ipairs(self.effectsPriority) do
|
||||||
|
local task1 = book[ef]:afterDeath(self.owner, self.effectsProperties[ef].intensity)
|
||||||
|
if task1 then
|
||||||
|
task1(function() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- должен вызываться в начале хода
|
||||||
|
---
|
||||||
|
--- возвращает, может ли персонаж сделать ход?
|
||||||
|
--- @return boolean
|
||||||
|
function behavior:beforeTurn()
|
||||||
|
for i, ef in ipairs(self.effectsPriority) do
|
||||||
|
local task1, turnStatement = book[ef]:beforeTurn(self.owner, self.effectsProperties[ef].intensity)
|
||||||
|
if task1 then
|
||||||
|
task1(function() end)
|
||||||
|
end
|
||||||
|
if turnStatement == false then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- должен вызываться в конце хода
|
||||||
|
function behavior:afterTurn()
|
||||||
|
for i, ef in ipairs(self.effectsPriority) do
|
||||||
|
local task1 = book[ef]:afterTurn(self.owner, self.effectsProperties[ef].intensity)
|
||||||
|
if task1 then
|
||||||
|
task1(function() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- должен вызываться перед кастом спелла
|
||||||
|
---
|
||||||
|
--- возвращает, может ли персонаж скастовать спелл?
|
||||||
|
--- @return boolean
|
||||||
|
function behavior:beforeCast()
|
||||||
|
for i, ef in ipairs(self.effectsPriority) do
|
||||||
|
local task1, castStatement = book[ef]:beforeCast(self.owner, self.effectsProperties[ef].intensity)
|
||||||
|
if task1 then
|
||||||
|
task1(function() end)
|
||||||
|
end
|
||||||
|
if castStatement == false then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- должен вызываться после каста спелла
|
||||||
|
function behavior:afterCast()
|
||||||
|
for i, ef in ipairs(self.effectsPriority) do
|
||||||
|
local task1 = book[ef]:afterCast(self.owner, self.effectsProperties[ef].intensity)
|
||||||
|
if task1 then
|
||||||
|
task1(function() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- должен вызываться перед получением урона
|
||||||
|
---
|
||||||
|
--- возвращает получаемый урон
|
||||||
|
--- @return integer
|
||||||
|
function behavior:beforeDamage(damage)
|
||||||
|
local totalDamage = damage
|
||||||
|
for i, ef in ipairs(self.effectsPriority) do
|
||||||
|
local task1
|
||||||
|
task1, totalDamage = book[ef]:beforeDamage(self.owner, self.effectsProperties[ef].intensity,
|
||||||
|
totalDamage or damage)
|
||||||
|
if task1 then
|
||||||
|
task1(function() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return totalDamage or damage
|
||||||
|
end
|
||||||
|
|
||||||
|
--- должен вызываться после получения урона
|
||||||
|
function behavior:afterDamage()
|
||||||
|
for i, ef in ipairs(self.effectsPriority) do
|
||||||
|
local task1 = book[ef]:afterDamage(self.owner, self.effectsProperties[ef].intensity)
|
||||||
|
if task1 then
|
||||||
|
task1(function() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return behavior
|
||||||
@ -32,22 +32,19 @@ function behavior:draw()
|
|||||||
local positioned = self.owner:has(Tree.behaviors.positioned)
|
local positioned = self.owner:has(Tree.behaviors.positioned)
|
||||||
if not positioned then return end
|
if not positioned then return end
|
||||||
|
|
||||||
love.graphics.setBlendMode("add", "premultiplied")
|
Tree.level.render:enqueue(Tree.level.render.LAYERS.LIGHT, positioned.position.y, function()
|
||||||
Tree.level.camera:attach()
|
love.graphics.setBlendMode("add", "premultiplied")
|
||||||
love.graphics.setCanvas(Tree.level.render.textures.lightLayer)
|
local shader = Tree.assets.files.shaders.light
|
||||||
local shader = Tree.assets.files.shaders.light
|
shader:send("color", { self.color.x, self.color.y, self.color.z })
|
||||||
shader:send("color", { self.color.x, self.color.y, self.color.z })
|
shader:send("time", love.timer:getTime() + self.seed)
|
||||||
shader:send("time", love.timer.getTime() + self.seed)
|
love.graphics.setShader(shader)
|
||||||
love.graphics.setShader(shader)
|
love.graphics.draw(Tree.assets.files.masks.circle128, positioned.position.x - self.intensity / 2,
|
||||||
love.graphics.draw(Tree.assets.files.masks.circle128, positioned.position.x - self.intensity / 2,
|
positioned.position.y - self.intensity / 2, 0, self.intensity / 128,
|
||||||
positioned.position.y - self.intensity / 2, 0, self.intensity / 128,
|
self.intensity / 128)
|
||||||
self.intensity / 128)
|
|
||||||
|
|
||||||
love.graphics.setBlendMode("alpha")
|
love.graphics.setShader()
|
||||||
|
love.graphics.setBlendMode("alpha")
|
||||||
love.graphics.setShader()
|
end)
|
||||||
love.graphics.setCanvas()
|
|
||||||
Tree.level.camera:detach()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return behavior
|
return behavior
|
||||||
|
|||||||
@ -10,10 +10,7 @@ function behavior:draw()
|
|||||||
local sprite = self.owner:has(Tree.behaviors.sprite)
|
local sprite = self.owner:has(Tree.behaviors.sprite)
|
||||||
local positioned = self.owner:has(Tree.behaviors.positioned)
|
local positioned = self.owner:has(Tree.behaviors.positioned)
|
||||||
if not positioned then return end
|
if not positioned then return end
|
||||||
if not sprite then
|
if not sprite then return end
|
||||||
love.graphics.setCanvas()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local ppm = Tree.level.camera.pixelsPerMeter
|
local ppm = Tree.level.camera.pixelsPerMeter
|
||||||
local position = positioned.position + Vec3 { 0.5, 0.5 }
|
local position = positioned.position + Vec3 { 0.5, 0.5 }
|
||||||
@ -25,41 +22,14 @@ function behavior:draw()
|
|||||||
table.insert(lights, Tree.level.characters[id])
|
table.insert(lights, Tree.level.characters[id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- 1. Эллипс тени
|
||||||
Tree.level.camera:attach()
|
Tree.level.render:enqueue(Tree.level.render.LAYERS.SHADOW, position.y, function()
|
||||||
love.graphics.setCanvas(Tree.level.render.textures.shadowLayer)
|
love.graphics.push()
|
||||||
love.graphics.push()
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.translate(position.x, position.y)
|
||||||
love.graphics.translate(position.x, position.y)
|
love.graphics.ellipse("fill", 0, 0, sprite.manifest.size / 2, sprite.manifest.size / 2 * math.cos(math.pi / 4))
|
||||||
love.graphics.ellipse("fill", 0, 0, sprite.manifest.size / 2, sprite.manifest.size / 2 * math.cos(math.pi / 4))
|
love.graphics.pop()
|
||||||
love.graphics.pop()
|
end)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
love.graphics.setCanvas(Tree.level.render.textures.spriteLightLayer)
|
|
||||||
love.graphics.setBlendMode("add")
|
|
||||||
for _, light in ipairs(lights) do
|
|
||||||
local lightPos = light:has(Tree.behaviors.positioned).position
|
|
||||||
local lightVec = lightPos - position
|
|
||||||
|
|
||||||
local lightColor = light:has(Tree.behaviors.light).color
|
|
||||||
if lightPos.y > position.y then
|
|
||||||
love.graphics.setColor(lightColor.x, lightColor.y, lightColor.z,
|
|
||||||
1 - 0.3 * lightVec:length())
|
|
||||||
elseif position.y - lightPos.y < 3 then
|
|
||||||
love.graphics.setColor(lightColor.x, lightColor.y, lightColor.z,
|
|
||||||
(1 - easing.easeInSine((position.y - lightPos.y))) - 0.3 * lightVec:length())
|
|
||||||
end
|
|
||||||
|
|
||||||
sprite.animationTable[sprite.state]:draw(sprite.sheets[sprite.state],
|
|
||||||
position.x,
|
|
||||||
position.y, nil, 1 / ppm * sprite.side, 1 / ppm, sprite.manifest.base.x, sprite.manifest.base.y)
|
|
||||||
end
|
|
||||||
love.graphics.setBlendMode("alpha")
|
|
||||||
|
|
||||||
Tree.level.camera:detach()
|
|
||||||
love.graphics.setColor(1, 1, 1)
|
|
||||||
love.graphics.setCanvas()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return behavior
|
return behavior
|
||||||
|
|||||||
@ -20,6 +20,9 @@ function behavior.new(spellbook)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function behavior:endCast()
|
function behavior:endCast()
|
||||||
|
self.owner:try(Tree.behaviors.effects, function(effects)
|
||||||
|
effects:afterCast()
|
||||||
|
end)
|
||||||
self.state = "idle"
|
self.state = "idle"
|
||||||
self.cast = nil
|
self.cast = nil
|
||||||
Tree.level.turnOrder:reorder()
|
Tree.level.turnOrder:reorder()
|
||||||
|
|||||||
@ -49,25 +49,71 @@ function sprite:draw()
|
|||||||
local ppm = Tree.level.camera.pixelsPerMeter
|
local ppm = Tree.level.camera.pixelsPerMeter
|
||||||
local position = pos.position + Vec3 { 0.5, 0.5 }
|
local position = pos.position + Vec3 { 0.5, 0.5 }
|
||||||
|
|
||||||
love.graphics.setCanvas(Tree.level.render.textures.spriteLayer)
|
Tree.level.render:enqueue(Tree.level.render.LAYERS.SPRITE, position.y, function()
|
||||||
Tree.level.camera:attach()
|
love.graphics.setColor(1, 1, 1)
|
||||||
|
|
||||||
love.graphics.setColor(1, 1, 1)
|
-- Собираем источники света для шейдера
|
||||||
if Tree.level.selector.id == self.owner.id then
|
local queryRadius = 12 -- Увеличенный радиус для плавности
|
||||||
local texW, texH = self.sheets[self.state]:getWidth(),
|
local lightIds = Tree.level.lightGrid:query(position, queryRadius)
|
||||||
self.sheets[self.state]:getHeight()
|
local lightsData = {}
|
||||||
local shader = Tree.assets.files.shaders.outline
|
|
||||||
shader:send("texSize", { texW, texH })
|
|
||||||
shader:send("time", love.timer:getTime())
|
|
||||||
love.graphics.setShader(shader)
|
|
||||||
end
|
|
||||||
self.animationTable[self.state]:draw(self.sheets[self.state],
|
|
||||||
position.x,
|
|
||||||
position.y, nil, 1 / ppm * self.side, 1 / ppm, self.manifest.base.x, self.manifest.base.y)
|
|
||||||
|
|
||||||
love.graphics.setShader()
|
for _, id in ipairs(lightIds) do
|
||||||
Tree.level.camera:detach()
|
local lightChar = Tree.level.characters[id]
|
||||||
love.graphics.setCanvas()
|
local b = lightChar:has(Tree.behaviors.light) --[[@as LightBehavior]]
|
||||||
|
local lPos = lightChar:has(Tree.behaviors.positioned).position
|
||||||
|
local dist = (lPos - position):length()
|
||||||
|
|
||||||
|
-- Берем только те, что могут дотянуться до нас своим радиусом
|
||||||
|
if dist < b.intensity + 2 then
|
||||||
|
table.insert(lightsData, {
|
||||||
|
x = lPos.x,
|
||||||
|
y = lPos.y,
|
||||||
|
r = b.color.x,
|
||||||
|
g = b.color.y,
|
||||||
|
b = b.color.z,
|
||||||
|
radius = b.intensity,
|
||||||
|
dist = dist
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Сортируем по дистанции, чтобы выбрать 8 самых влиятельных
|
||||||
|
table.sort(lightsData, function(a, b) return a.dist < b.dist end)
|
||||||
|
|
||||||
|
local lightShader = Tree.assets.files.shaders.sprite_light
|
||||||
|
local weather = Tree.level.weather
|
||||||
|
local numLights = math.min(#lightsData, 8)
|
||||||
|
local isSelected = (Tree.level.selector.id == self.owner.id)
|
||||||
|
|
||||||
|
lightShader:send("sprite_pos", { position.x, position.y })
|
||||||
|
lightShader:send("sky", { weather.skyLight.x, weather.skyLight.y, weather.skyLight.z })
|
||||||
|
lightShader:send("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z })
|
||||||
|
lightShader:send("num_lights", numLights)
|
||||||
|
|
||||||
|
lightShader:send("is_selected", isSelected)
|
||||||
|
if isSelected then
|
||||||
|
local sheet = self.sheets[self.state]
|
||||||
|
lightShader:send("tex_size", { sheet:getWidth(), sheet:getHeight() })
|
||||||
|
lightShader:send("time", love.timer:getTime())
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, numLights do
|
||||||
|
local l = lightsData[i]
|
||||||
|
local idx = i - 1
|
||||||
|
lightShader:send(string.format("lights[%d].position", idx), { l.x, l.y })
|
||||||
|
lightShader:send(string.format("lights[%d].color", idx), { l.r, l.g, l.b })
|
||||||
|
lightShader:send(string.format("lights[%d].radius", idx), l.radius)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
love.graphics.setShader(lightShader)
|
||||||
|
|
||||||
|
self.animationTable[self.state]:draw(self.sheets[self.state],
|
||||||
|
position.x,
|
||||||
|
position.y, nil, 1 / ppm * self.side, 1 / ppm, self.manifest.base.x, self.manifest.base.y)
|
||||||
|
|
||||||
|
love.graphics.setShader()
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,22 +1,45 @@
|
|||||||
|
--- @alias Class "dev_warrior"|"dev_mage"
|
||||||
|
|
||||||
--- @class StatsBehavior : Behavior
|
--- @class StatsBehavior : Behavior
|
||||||
--- @field hp integer
|
--- @field hp integer
|
||||||
--- @field mana integer
|
--- @field mana integer
|
||||||
--- @field initiative integer
|
--- @field initiative integer
|
||||||
|
--- @field class Class
|
||||||
--- @field isInTurnOrder boolean
|
--- @field isInTurnOrder boolean
|
||||||
|
--- @field amIAlive boolean
|
||||||
local behavior = {}
|
local behavior = {}
|
||||||
behavior.__index = behavior
|
behavior.__index = behavior
|
||||||
behavior.id = "stats"
|
behavior.id = "stats"
|
||||||
|
|
||||||
|
--- план прост, если что-то не так, то мы просто убиваем бехавиор (по крайней мере так должно было быть, но пиаш мне запретил :sob:)
|
||||||
|
function behavior:checkStats()
|
||||||
|
-- if self.hp <= 0 then behavior:die() end
|
||||||
|
if self.hp <= 0 then
|
||||||
|
self.amIAlive = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param damage integer
|
||||||
|
function behavior:dealDamage(damage)
|
||||||
|
local effects = self.owner:has(Tree.behaviors.effects)
|
||||||
|
if effects then damage = effects:beforeDamage(damage) end
|
||||||
|
self.hp = self.hp - damage
|
||||||
|
self:checkStats()
|
||||||
|
end
|
||||||
|
|
||||||
--- @param hp? integer
|
--- @param hp? integer
|
||||||
--- @param mana? integer
|
--- @param mana? integer
|
||||||
--- @param initiative? integer
|
--- @param initiative? integer
|
||||||
|
--- @param class? Class
|
||||||
--- @param isInTurnOrder? boolean
|
--- @param isInTurnOrder? boolean
|
||||||
function behavior.new(hp, mana, initiative, isInTurnOrder)
|
function behavior.new(hp, mana, initiative, class, isInTurnOrder)
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
hp = hp or 20,
|
hp = hp or 20,
|
||||||
mana = mana or 10,
|
mana = mana or 10,
|
||||||
initiative = initiative or 10,
|
initiative = initiative or 10,
|
||||||
isInTurnOrder = isInTurnOrder or true
|
isInTurnOrder = isInTurnOrder or true,
|
||||||
|
class = "dev_warrior",
|
||||||
|
amIAlive = true
|
||||||
}, behavior)
|
}, behavior)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
123
lib/effectbook.lua
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
local task = require "lib.utils.task"
|
||||||
|
local effect = require "lib.spell.effect"
|
||||||
|
local easing = require "lib.utils.easing"
|
||||||
|
|
||||||
|
--- некое уникальное строковое значение
|
||||||
|
--- @alias EffectTag string
|
||||||
|
|
||||||
|
--- Кровотечение.
|
||||||
|
---
|
||||||
|
--- Наносит `intensity` урона перед началом каждого хода.
|
||||||
|
local bleeding = effect.new({
|
||||||
|
tag = "bleeding"
|
||||||
|
})
|
||||||
|
|
||||||
|
function bleeding:afterBirth(owner, intensity)
|
||||||
|
local light = require "lib/character/character".spawn("Bleeding Light Effect")
|
||||||
|
light:addBehavior {
|
||||||
|
Tree.behaviors.light.new { color = Vec3 { 1, 0., 0. }, intensity = 4 },
|
||||||
|
Tree.behaviors.positioned.new(owner:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }),
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.wait({ task.chain(task.tween(light:has(Tree.behaviors.light) --[[@as LightBehavior]],
|
||||||
|
{ intensity = 1, color = Vec3 { 0, 0., 0. } }, 800, easing.easeInCubic), function()
|
||||||
|
light:die()
|
||||||
|
return task.fromValue()
|
||||||
|
end) })
|
||||||
|
end
|
||||||
|
|
||||||
|
function bleeding:beforeTurn(owner, intensity)
|
||||||
|
local stats = owner:has(Tree.behaviors.stats)
|
||||||
|
local sprite = owner:has(Tree.behaviors.sprite)
|
||||||
|
if not stats or not sprite then return end
|
||||||
|
stats:dealDamage(intensity)
|
||||||
|
return task.wait({ sprite:animate("hurt") }), true
|
||||||
|
end
|
||||||
|
|
||||||
|
function bleeding:afterTurn(owner, intensity)
|
||||||
|
local behavior = owner:has(Tree.behaviors.effects)
|
||||||
|
if not behavior then
|
||||||
|
print('[EffectBook]: yo man what the hell wheres your behavior how thats possible please stop thats not normal')
|
||||||
|
else
|
||||||
|
behavior:deleteStacks("bleeding", 1)
|
||||||
|
end
|
||||||
|
return task.wait {}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- meow
|
||||||
|
function bleeding:afterCast(owner, intensity)
|
||||||
|
Tree.audio:play(Tree.assets.files.audio.sounds.meow)
|
||||||
|
return task.wait {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function bleeding:beforeCast(owner, intensity)
|
||||||
|
Tree.audio:play(Tree.assets.files.audio.sounds.meow)
|
||||||
|
return task.wait {}, true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Отвращение к смерти.
|
||||||
|
---
|
||||||
|
--- Спасает от смертельного урона, оставляя одно очко здоровья. Персонаж не может ходить до тех пор, пока эффект не сработает.
|
||||||
|
local aversionToDeath = effect.new {
|
||||||
|
tag = "aversionToDeath"
|
||||||
|
}
|
||||||
|
|
||||||
|
function aversionToDeath:beforeDamage(owner, intensity, damage)
|
||||||
|
local stats = owner:has(Tree.behaviors.stats)
|
||||||
|
local effects = owner:has(Tree.behaviors.effects)
|
||||||
|
if not stats or not effects then return end
|
||||||
|
if stats.hp <= damage then
|
||||||
|
effects:deleteStacks("aversionToDeath", 1)
|
||||||
|
-- тут должен быть какой-нибудь классный спецэффект, но я не умею в шейдеры
|
||||||
|
return task.wait({}), stats.hp - 1
|
||||||
|
end
|
||||||
|
return task.wait {}, damage
|
||||||
|
end
|
||||||
|
|
||||||
|
function aversionToDeath:beforeTurn(owner, intensity)
|
||||||
|
local sprite = owner:has(Tree.behaviors.sprite)
|
||||||
|
if not sprite then
|
||||||
|
return task.wait {}, false
|
||||||
|
end
|
||||||
|
return task.wait {
|
||||||
|
sprite:animate("hurt")
|
||||||
|
}, false
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------- Effectbook & Sum -----------------
|
||||||
|
PeaAshMeter
commented
лучше алиас сделать для гигафункции лучше алиас сделать для гигафункции
|
|||||||
|
|
||||||
|
--- @alias EffectSumFunc fun(owner: Character, effect1: EffectTag, effect2: EffectTag): boolean
|
||||||
|
|
||||||
|
--- Принимает таблицу, в ключах которых тэги эффектов, которые мы хотим просуммировать, и в значениях которых функция,
|
||||||
|
--- возвращающая булево значение: применять ли эффект после суммирования.
|
||||||
|
PeaAshMeter
commented
Этот алиас имеет слишком общее название, у аннотаций глобальная область видимости 👎 Этот алиас имеет слишком общее название, у аннотаций глобальная область видимости 👎
neckrat
commented
чорт чорт
|
|||||||
|
--- @type table<EffectTag, table<EffectTag, EffectSumFunc>>
|
||||||
|
local sums = {}
|
||||||
|
|
||||||
|
--- Сумма кровотечения и отвращения к смерти, (в целях разработки) удаляет оба эффекта, не позволяя дальше применять эффект
|
||||||
|
sums.bleeding = {
|
||||||
|
aversionToDeath = function(owner, effect1, effect2)
|
||||||
|
print("[EffectBook]: применяем сумму, удаляем оба эффекта")
|
||||||
|
local behaviorEffect = owner:has(Tree.behaviors.effects)
|
||||||
|
if not behaviorEffect then
|
||||||
|
print(
|
||||||
|
"[EffectBook]: yo man what the hell wheres your behavior how thats possible please stop thats not normal")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
behaviorEffect:deleteEffect(effect1)
|
||||||
|
behaviorEffect:deleteEffect(effect2)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
--- @class EffectBook
|
||||||
|
--- @field sums table<EffectTag, table<EffectTag, EffectSumFunc>>
|
||||||
|
--- @field book table<EffectTag, Effect>
|
||||||
|
local effectbook = {
|
||||||
|
sums = sums,
|
||||||
|
book = {
|
||||||
|
bleeding = bleeding,
|
||||||
|
aversionToDeath = aversionToDeath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return effectbook
|
||||||
@ -9,17 +9,32 @@ map.__index = map
|
|||||||
--- @param size? Vec3
|
--- @param size? Vec3
|
||||||
local function new(type, template, size)
|
local function new(type, template, size)
|
||||||
local tMap = require('lib.level.' .. type).new(template, size)
|
local tMap = require('lib.level.' .. type).new(template, size)
|
||||||
return setmetatable({ __grid = tMap }, map)
|
local grid = setmetatable({ __grid = tMap }, map)
|
||||||
|
grid:refreshBatch()
|
||||||
|
return grid
|
||||||
|
end
|
||||||
|
|
||||||
|
function map:refreshBatch()
|
||||||
|
-- Находим атлас первого попавшегося тайла (предполагаем, что он один для всех)
|
||||||
|
local _, firstTile = next(self.__grid)
|
||||||
|
if not firstTile then return end
|
||||||
|
|
||||||
|
local atlas = firstTile.atlasData.atlas
|
||||||
|
local count = 0
|
||||||
|
for _ in pairs(self.__grid) do count = count + 1 end
|
||||||
|
|
||||||
|
self.batch = love.graphics.newSpriteBatch(atlas, count)
|
||||||
|
for _, tile in pairs(self.__grid) do
|
||||||
|
-- 1/32 это масштаб, так как размер тайла в мире 1x1 метр, а в атласе 32x32 пикселя
|
||||||
|
self.batch:add(tile.atlasData.quad, tile.position.x, tile.position.y, 0, 1 / 32, 1 / 32)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function map:draw()
|
function map:draw()
|
||||||
love.graphics.setCanvas(Tree.level.render.textures.floorLayer)
|
if not self.batch then return end
|
||||||
Tree.level.camera:attach()
|
Tree.level.render:enqueue(Tree.level.render.LAYERS.FLOOR, 0, function()
|
||||||
utils.each(self.__grid, function(el)
|
love.graphics.draw(self.batch)
|
||||||
el:draw()
|
|
||||||
end)
|
end)
|
||||||
Tree.level.camera:detach()
|
|
||||||
love.graphics.setCanvas()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return { new = new }
|
return { new = new }
|
||||||
|
|||||||
@ -20,7 +20,7 @@ local function new(type, template)
|
|||||||
local size = Vec3 { 30, 30 } -- magic numbers for testing purposes only
|
local size = Vec3 { 30, 30 } -- magic numbers for testing purposes only
|
||||||
print(type, template, size)
|
print(type, template, size)
|
||||||
|
|
||||||
Tree.audio:play(Tree.assets.files.audio.music.level1.battle)
|
Tree.audio:play(Tree.assets.files.audio.music.level1.progressive_plains)
|
||||||
|
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
size = size,
|
size = size,
|
||||||
@ -56,9 +56,9 @@ end
|
|||||||
function level:draw()
|
function level:draw()
|
||||||
self.render:clear()
|
self.render:clear()
|
||||||
self.tileGrid:draw()
|
self.tileGrid:draw()
|
||||||
while not self.characterGrid.yOrderQueue:is_empty() do -- по сути это сортировка кучей за n log n
|
utils.each(self.characters, function(char)
|
||||||
self.characterGrid.yOrderQueue:pop():draw()
|
char:draw()
|
||||||
end
|
end)
|
||||||
|
|
||||||
self.render:draw()
|
self.render:draw()
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,18 +1,30 @@
|
|||||||
--- @class Render
|
--- @class Render
|
||||||
--- @field textures table<string, love.Canvas>
|
--- @field textures table<string, love.Canvas>
|
||||||
|
--- @field queue table[]
|
||||||
|
--- @field lowResScale number
|
||||||
|
--- @field LAYERS table<string, integer>
|
||||||
local render = {
|
local render = {
|
||||||
textures = {}
|
textures = {},
|
||||||
|
queue = {},
|
||||||
|
lowResScale = 1.0,
|
||||||
|
LAYERS = {
|
||||||
|
FLOOR = 1,
|
||||||
|
SHADOW = 2,
|
||||||
|
LIGHT = 3,
|
||||||
|
SPRITE = 4,
|
||||||
|
OVERLAY = 5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function render:clear()
|
function render:clear()
|
||||||
local weather = Tree.level.weather
|
local weather = Tree.level.weather
|
||||||
local txs = self.textures
|
local txs = self.textures
|
||||||
|
self.queue = {}
|
||||||
|
|
||||||
love.graphics.setCanvas(txs.shadowLayer)
|
love.graphics.setCanvas(txs.shadowLayer)
|
||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
love.graphics.setCanvas(txs.spriteLayer)
|
love.graphics.setCanvas(txs.spriteLayer)
|
||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
love.graphics.setCanvas(txs.spriteLightLayer)
|
|
||||||
love.graphics.clear(weather.skyLight.x, weather.skyLight.y, weather.skyLight.z)
|
|
||||||
love.graphics.setCanvas(txs.floorLayer)
|
love.graphics.setCanvas(txs.floorLayer)
|
||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
love.graphics.setCanvas(txs.lightLayer)
|
love.graphics.setCanvas(txs.lightLayer)
|
||||||
@ -21,6 +33,10 @@ function render:clear()
|
|||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function render:enqueue(layer, z, func)
|
||||||
|
table.insert(self.queue, { layer = layer, z = z, func = func })
|
||||||
|
end
|
||||||
|
|
||||||
function render:free()
|
function render:free()
|
||||||
for _, tx in pairs(self.textures) do
|
for _, tx in pairs(self.textures) do
|
||||||
tx:release()
|
tx:release()
|
||||||
@ -57,50 +73,102 @@ function render:applyBlur(input, radius)
|
|||||||
return self.textures.tmp2
|
return self.textures.tmp2
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param params {w: number?, h: number?}
|
||||||
|
---@return table|Render
|
||||||
|
local function new(params)
|
||||||
|
local w = params.w or love.graphics.getWidth()
|
||||||
|
local h = params.h or love.graphics.getHeight()
|
||||||
|
local lowResScale = 0.5
|
||||||
|
|
||||||
|
return setmetatable({
|
||||||
|
lowResScale = lowResScale,
|
||||||
|
queue = {},
|
||||||
|
textures = {
|
||||||
|
shadowLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
|
||||||
|
spriteLayer = love.graphics.newCanvas(w, h),
|
||||||
|
floorLayer = love.graphics.newCanvas(w, h),
|
||||||
|
overlayLayer = love.graphics.newCanvas(w, h),
|
||||||
|
lightLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
|
||||||
|
tmp1 = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
|
||||||
|
tmp2 = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
|
||||||
|
}
|
||||||
|
}, { __index = render })
|
||||||
|
end
|
||||||
|
|
||||||
function render:draw()
|
function render:draw()
|
||||||
local weather = Tree.level.weather
|
local weather = Tree.level.weather
|
||||||
local txs = self.textures
|
local txs = self.textures
|
||||||
love.graphics.setCanvas(txs.lightLayer)
|
|
||||||
love.graphics.draw(self:applyBlur(txs.shadowLayer, 4 * Tree.level.camera.scale))
|
-- 1. Сортировка очереди
|
||||||
|
table.sort(self.queue, function(a, b)
|
||||||
|
if a.layer ~= b.layer then
|
||||||
|
return a.layer < b.layer
|
||||||
|
end
|
||||||
|
return a.z < b.z
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- 2. Рендеринг очереди в соответствующие Canvas
|
||||||
|
local currentLayer = nil
|
||||||
|
for _, entry in ipairs(self.queue) do
|
||||||
|
if entry.layer ~= currentLayer then
|
||||||
|
if currentLayer then
|
||||||
|
Tree.level.camera:detach()
|
||||||
|
local wasLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.LIGHT
|
||||||
|
if wasLowRes then
|
||||||
|
love.graphics.pop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
currentLayer = entry.layer
|
||||||
|
local isLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.LIGHT
|
||||||
|
|
||||||
|
if currentLayer == self.LAYERS.FLOOR then
|
||||||
|
love.graphics.setCanvas(txs.floorLayer)
|
||||||
|
elseif currentLayer == self.LAYERS.SHADOW then
|
||||||
|
love.graphics.setCanvas(txs.shadowLayer)
|
||||||
|
elseif currentLayer == self.LAYERS.LIGHT then
|
||||||
|
love.graphics.setCanvas(txs.lightLayer)
|
||||||
|
elseif currentLayer == self.LAYERS.SPRITE then
|
||||||
|
love.graphics.setCanvas(txs.spriteLayer)
|
||||||
|
elseif currentLayer == self.LAYERS.OVERLAY then
|
||||||
|
love.graphics.setCanvas(txs.overlayLayer)
|
||||||
|
end
|
||||||
|
|
||||||
|
if isLowRes then
|
||||||
|
love.graphics.push()
|
||||||
|
love.graphics.scale(self.lowResScale)
|
||||||
|
end
|
||||||
|
Tree.level.camera:attach()
|
||||||
|
end
|
||||||
|
entry.func()
|
||||||
|
end
|
||||||
|
if currentLayer then
|
||||||
|
Tree.level.camera:detach()
|
||||||
|
local wasLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.LIGHT
|
||||||
|
if wasLowRes then
|
||||||
|
love.graphics.pop()
|
||||||
|
end
|
||||||
|
end
|
||||||
love.graphics.setCanvas()
|
love.graphics.setCanvas()
|
||||||
|
|
||||||
-- self.lightLayer:newImageData():encode("png", "lightLayer.png")
|
-- 3. Пост-процессинг и композиция
|
||||||
-- os.exit(0)
|
love.graphics.setCanvas(txs.lightLayer)
|
||||||
|
-- Радиус блюра тоже масштабируем, так как текстура меньше
|
||||||
|
love.graphics.draw(self:applyBlur(txs.shadowLayer, 4 * Tree.level.camera.scale * self.lowResScale))
|
||||||
|
love.graphics.setCanvas()
|
||||||
|
|
||||||
local lightShader = Tree.assets.files.shaders.light_postprocess
|
local lightShader = Tree.assets.files.shaders.light_postprocess
|
||||||
lightShader:send("scene", txs.floorLayer)
|
lightShader:send("scene", txs.floorLayer)
|
||||||
lightShader:send("light", self:applyBlur(txs.lightLayer, 2))
|
lightShader:send("light", self:applyBlur(txs.lightLayer, 2 * self.lowResScale))
|
||||||
lightShader:send("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z })
|
lightShader:send("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z })
|
||||||
love.graphics.setShader(lightShader)
|
love.graphics.setShader(lightShader)
|
||||||
love.graphics.draw(txs.floorLayer)
|
love.graphics.draw(txs.floorLayer)
|
||||||
|
|
||||||
love.graphics.setShader()
|
love.graphics.setShader()
|
||||||
love.graphics.draw(txs.overlayLayer)
|
love.graphics.draw(txs.overlayLayer)
|
||||||
love.graphics.setShader(lightShader)
|
|
||||||
|
|
||||||
lightShader:send("scene", txs.spriteLayer)
|
-- Спрайты уже полностью освещены в SpriteBehavior (с учетом ambient и point lights)
|
||||||
lightShader:send("light", txs.spriteLightLayer)
|
-- Поэтому рисуем их "как есть"
|
||||||
love.graphics.draw(txs.spriteLayer)
|
love.graphics.draw(txs.spriteLayer)
|
||||||
love.graphics.setShader()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param params {w: number?, h: number?}
|
|
||||||
---@return table|Render
|
|
||||||
local function new(params)
|
|
||||||
local w = params.w or love.graphics.getWidth()
|
|
||||||
local h = params.h or love.graphics.getHeight()
|
|
||||||
return setmetatable({
|
|
||||||
textures = {
|
|
||||||
shadowLayer = love.graphics.newCanvas(w, h),
|
|
||||||
spriteLayer = love.graphics.newCanvas(w, h),
|
|
||||||
spriteLightLayer = love.graphics.newCanvas(w, h),
|
|
||||||
floorLayer = love.graphics.newCanvas(w, h),
|
|
||||||
overlayLayer = love.graphics.newCanvas(w, h),
|
|
||||||
lightLayer = love.graphics.newCanvas(w, h),
|
|
||||||
tmp1 = love.graphics.newCanvas(w, h),
|
|
||||||
tmp2 = love.graphics.newCanvas(w, h),
|
|
||||||
}
|
|
||||||
}, { __index = render })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return { new = new }
|
return { new = new }
|
||||||
|
|||||||
@ -43,6 +43,15 @@ function turnOrder:next()
|
|||||||
char:try(Tree.behaviors.positioned, function(positioned)
|
char:try(Tree.behaviors.positioned, function(positioned)
|
||||||
Tree.level.camera:animateTo(positioned.position, 1500, easing.easeInOutCubic)(
|
Tree.level.camera:animateTo(positioned.position, 1500, easing.easeInOutCubic)(
|
||||||
function()
|
function()
|
||||||
|
-- проверяем, позволяют ли эффекты нам сходить
|
||||||
|
if char:try(Tree.behaviors.effects, function(effects)
|
||||||
|
-- print("[TurnOrder]: ну мы пытаемся применить эффект к", char.id)
|
||||||
|
return effects:beforeTurn()
|
||||||
|
end) == false then
|
||||||
|
self:next()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if char:has(Tree.behaviors.ai) then
|
if char:has(Tree.behaviors.ai) then
|
||||||
char:has(Tree.behaviors.ai):makeTurn()(
|
char:has(Tree.behaviors.ai):makeTurn()(
|
||||||
function()
|
function()
|
||||||
@ -69,6 +78,9 @@ function turnOrder:endRound()
|
|||||||
char:try(Tree.behaviors.spellcaster, function(spellcaster)
|
char:try(Tree.behaviors.spellcaster, function(spellcaster)
|
||||||
spellcaster:processCooldowns()
|
spellcaster:processCooldowns()
|
||||||
end)
|
end)
|
||||||
|
char:try(Tree.behaviors.effects, function(effects)
|
||||||
|
effects:afterTurn()
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
self.actedQueue, self.pendingQueue = self.pendingQueue, self.actedQueue
|
self.actedQueue, self.pendingQueue = self.pendingQueue, self.actedQueue
|
||||||
|
|||||||
169
lib/spell/effect.lua
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
local utils = require "lib.utils.utils"
|
||||||
|
local taskUtils = require "lib.utils.task"
|
||||||
|
|
||||||
|
PeaAshMeter
commented
Почему это вообще в папке со спеллами? Почему это вообще в папке со спеллами?
neckrat
commented
я не придумал куда в другое место это запихать, не в либ же кидать 🥵 я не придумал куда в другое место это запихать, не в либ же кидать 🥵
|
|||||||
|
--- Некоторое свойство, что можно наложить на персонажа. Позволяет реализовать такие вещи как DOT'ы
|
||||||
|
--- и вообще, что душа поживает.
|
||||||
|
---
|
||||||
|
--- У каждого эффекта есть тэг и функции триггеры (например, `beforeTurn`, что срабатывает перед началом хода персонажа и так далее).
|
||||||
|
--- Каждая функция триггер делится на два типа, `before...` и `after...`. Каждая из них возвращает `task`, для того чтобы
|
||||||
|
--- проиграть анимацию, например. Функции типа `before...` также возвращают по мимо таска некоторое значение, зависящее от
|
||||||
|
--- конкретной функции.
|
||||||
|
--- @class Effect
|
||||||
|
--- @field tag string
|
||||||
|
local effect = {}
|
||||||
|
effect.__index = effect
|
||||||
|
|
||||||
|
--- Предполагается, что в каждую функцию будет передаваться `Character` (владелец эффекта) и параметр `intensity`, который отвечает за силу эффекта
|
||||||
|
--- @alias EffectFunc fun(owner: Character, intensity: integer): Task<nil>, nil бред конечно, но иначе всё в жёлтом
|
||||||
|
--- @alias EffectStatementFunc fun(owner: Character, intensity: integer): Task<nil>, boolean
|
||||||
|
--- @alias EffectDamageFunc fun(owner: Character, intensity: integer, damage: integer): Task<nil>, integer
|
||||||
|
--- @alias EffectRegenFunc fun(owner: Character, intensity: integer, amountHp: integer): Task<nil>, integer
|
||||||
|
--- @alias EffectData { tag: string }
|
||||||
|
|
||||||
|
--- Срабатывает перед применением эффекта
|
||||||
|
---
|
||||||
|
--- Возвращает, а можно ли применить эффект?
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>, boolean
|
||||||
|
function effect:beforeBirth(owner, intensity) return taskUtils.fromValue(), true end
|
||||||
|
|
||||||
|
--- Срабатывает после применения эффекта
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>
|
||||||
|
function effect:afterBirth(owner, intensity) return taskUtils.fromValue() end
|
||||||
|
|
||||||
|
--- Срабатывает перед смертью владельца эффекта
|
||||||
|
---
|
||||||
|
--- Возвращает, умирает ли персонаж?
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>, boolean
|
||||||
|
function effect:beforeDeath(owner, intensity) return taskUtils.fromValue(), true end
|
||||||
|
|
||||||
|
--- Срабатывает после смерти владельца эффекта
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>
|
||||||
|
function effect:afterDeath(owner, intensity) return taskUtils.fromValue() end
|
||||||
|
|
||||||
|
--- Срабатывает перед ходом владельца эффекта
|
||||||
|
---
|
||||||
|
--- Возвращает, будет ли персонаж ходить?
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>, boolean
|
||||||
|
function effect:beforeTurn(owner, intensity) return taskUtils.fromValue(), true end
|
||||||
|
|
||||||
|
--- Срабатывает после хода владельца эффекта
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>
|
||||||
|
function effect:afterTurn(owner, intensity) return taskUtils.fromValue() end
|
||||||
|
|
||||||
|
--- Срабатывает перед кастом заклинания владельцем эффекта
|
||||||
|
---
|
||||||
|
--- Возвращает, произойдёт ли каст?
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>, boolean
|
||||||
|
function effect:beforeCast(owner, intensity) return taskUtils.fromValue(), true end
|
||||||
|
|
||||||
|
--- Срабатывает после каста заклинания владельцем эффекта
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>
|
||||||
|
function effect:afterCast(owner, intensity) return taskUtils.fromValue() end
|
||||||
|
|
||||||
|
--- Срабатывает перед нанесением урона владельцем эффекта
|
||||||
|
---
|
||||||
|
--- Возвращает урон, который собираются нанести
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @param damage integer
|
||||||
|
--- @return Task<nil>, integer
|
||||||
|
function effect:beforeAttack(owner, intensity, damage) return taskUtils.fromValue(), damage end
|
||||||
|
|
||||||
|
--- Срабатывает после нанесения урона владельцем эффекта
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>
|
||||||
|
function effect:afterAttack(owner, intensity) return taskUtils.fromValue() end
|
||||||
|
|
||||||
|
--- Срабатывает перед получением урона владельцем эффекта
|
||||||
|
---
|
||||||
|
--- Возвращает урон, который должны получить
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @param damage integer
|
||||||
|
--- @return Task<nil>, integer
|
||||||
|
function effect:beforeDamage(owner, intensity, damage) return taskUtils.fromValue(), damage end
|
||||||
|
|
||||||
|
--- Срабатывает после получения урона владельцем эффекта
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>
|
||||||
|
function effect:afterDamage(owner, intensity) return taskUtils.fromValue() end
|
||||||
|
|
||||||
|
--- Срабатывает перед регенерацией здоровья владельцем эффекта
|
||||||
|
---
|
||||||
|
--- Возвращает количество здоровья, которое должно быть восстановлено
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @param amountHp integer кол-во хп для регена
|
||||||
|
--- @return Task<nil>, integer
|
||||||
|
function effect:beforeRegeneration(owner, intensity, amountHp) return taskUtils.fromValue(), amountHp end
|
||||||
|
|
||||||
|
--- Срабатывает после регенерации здоровья владельцем эффекта
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param intensity integer
|
||||||
|
--- @return Task<nil>
|
||||||
|
function effect:afterRegeneration(owner, intensity) return taskUtils.fromValue() end
|
||||||
|
|
||||||
|
--- Функция, что задаёт правила присвоения эффекта
|
||||||
|
--- @param owner Character
|
||||||
|
--- @param stacks integer
|
||||||
|
--- @param intensity integer
|
||||||
|
function effect:onBirth(owner, stacks, intensity)
|
||||||
|
local effects = owner:has(Tree.behaviors.effects)
|
||||||
|
if not effects then return end
|
||||||
|
-- проверяем на наличие такого же эффекта
|
||||||
|
if effects.effectsProperties[self.tag] then
|
||||||
|
local i = 1
|
||||||
|
while i < #effects.effectsPriority and effects.effectsPriority[i] ~= self.tag do
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
local ef = table.remove(effects.effectsPriority, i)
|
||||||
|
effects.effectsPriority[#effects.effectsPriority + 1] = ef
|
||||||
|
else
|
||||||
|
effects.effectsPriority[#effects.effectsPriority + 1] = self.tag
|
||||||
|
end
|
||||||
|
effects.effectsProperties[self.tag] = {
|
||||||
|
stacks = stacks,
|
||||||
|
intensity = intensity
|
||||||
|
PeaAshMeter
commented
ок, это интересный способ создать пустой таск. Я даже не знал, что так можно 🥵 ок, это интересный способ создать пустой таск. Я даже не знал, что так можно 🥵
вообще семантически правильный способ это сделать - `task.fromValue()`
neckrat
commented
даже так даже так
|
|||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function effect:update(dt) end
|
||||||
|
|
||||||
|
PeaAshMeter
commented
оно явно не должно так дублироваться. оно явно не должно так дублироваться.
вообще-то дефолтные методы ты и так определил в `effect`.
хотя логичнее было бы сделать "если обработчика события нет, то не обрабатываем событие".
neckrat
commented
это итог того, что я отказался от нилов в функциях это итог того, что я отказался от нилов в функциях
хотя полагаю это можно сократить, я не тыкал этот момент
|
|||||||
|
function effect:draw() end
|
||||||
|
|
||||||
|
--- дип сравнение эффектов
|
||||||
|
--- @param other Effect
|
||||||
|
--- @return boolean
|
||||||
|
function effect:__eq(other)
|
||||||
|
return utils.deepComparison(self, other)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param data EffectData
|
||||||
|
--- @return Effect
|
||||||
|
local function new(data)
|
||||||
|
local newEffect = setmetatable({
|
||||||
|
tag = data.tag,
|
||||||
|
}, effect)
|
||||||
|
|
||||||
|
return newEffect
|
||||||
|
end
|
||||||
|
|
||||||
|
return { new = new }
|
||||||
@ -54,33 +54,28 @@ function spell:draw()
|
|||||||
local path = self.path --[[@as Deque?]]
|
local path = self.path --[[@as Deque?]]
|
||||||
if not path then return end
|
if not path then return end
|
||||||
--- Это отрисовка пути персонажа к мышке
|
--- Это отрисовка пути персонажа к мышке
|
||||||
Tree.level.camera:attach()
|
Tree.level.render:enqueue(Tree.level.render.LAYERS.OVERLAY, 0, function()
|
||||||
love.graphics.setCanvas(Tree.level.render.textures.overlayLayer)
|
local i = 0
|
||||||
local i = 0
|
path:pop_front()
|
||||||
path:pop_front()
|
for p in path:values() do
|
||||||
for p in path:values() do
|
i = i + 1
|
||||||
i = i + 1
|
local s = 1 / Tree.level.camera.pixelsPerMeter
|
||||||
local s = 1 / Tree.level.camera.pixelsPerMeter
|
local quad = i > self.distance and icons:pickQuad('dev_path_closed') or icons:pickQuad('dev_path')
|
||||||
local quad = i > self.distance and icons:pickQuad('dev_path_closed') or icons:pickQuad('dev_path')
|
love.graphics.draw(icons.atlas, quad, p.x, p.y, 0, s, s)
|
||||||
love.graphics.draw(icons.atlas, quad, p.x, p.y, 0, s, s)
|
end
|
||||||
end
|
love.graphics.setColor(1, 1, 1)
|
||||||
love.graphics.setCanvas()
|
end)
|
||||||
Tree.level.camera:detach()
|
|
||||||
love.graphics.setColor(1, 1, 1)
|
|
||||||
else
|
else
|
||||||
Tree.level.camera:attach()
|
Tree.level.render:enqueue(Tree.level.render.LAYERS.OVERLAY, 0, function()
|
||||||
love.graphics.setCanvas(Tree.level.render.textures.overlayLayer)
|
love.graphics.setColor(1, 1, 1, 0.5)
|
||||||
love.graphics.setColor(1, 1, 1, 0.5)
|
for _, p in pairs(self.targets) do
|
||||||
for _, p in pairs(self.targets) do
|
local s = self.tSize / Tree.level.camera.pixelsPerMeter
|
||||||
local s = self.tSize / Tree.level.camera.pixelsPerMeter
|
local quad = icons:pickQuad('dev_target')
|
||||||
local quad = icons:pickQuad('dev_target')
|
love.graphics.draw(icons.atlas, quad, p.x + 0.5 - self.tSize / 2, p.y + 0.5 - self.tSize / 2, 0, s, s)
|
||||||
love.graphics.draw(icons.atlas, quad, p.x + 0.5 - self.tSize / 2, p.y + 0.5 - self.tSize / 2, 0, s, s)
|
end
|
||||||
end
|
love.graphics.setShader()
|
||||||
love.graphics.setShader()
|
love.graphics.setColor(1, 1, 1)
|
||||||
|
end)
|
||||||
love.graphics.setCanvas()
|
|
||||||
Tree.level.camera:detach()
|
|
||||||
love.graphics.setColor(1, 1, 1)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -128,6 +123,13 @@ function spell.new(data)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- проверка на возможность каста
|
||||||
|
if not caster:try(Tree.behaviors.effects, function(effects)
|
||||||
|
return effects:beforeCast()
|
||||||
|
end) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
caster:try(Tree.behaviors.stats, function(stats)
|
caster:try(Tree.behaviors.stats, function(stats)
|
||||||
stats.mana = stats.mana - self.baseCost
|
stats.mana = stats.mana - self.baseCost
|
||||||
end)
|
end)
|
||||||
@ -135,6 +137,8 @@ function spell.new(data)
|
|||||||
caster:try(Tree.behaviors.spellcaster, function(spellcaster)
|
caster:try(Tree.behaviors.spellcaster, function(spellcaster)
|
||||||
spellcaster.cooldowns[self.tag] = self.baseCooldown
|
spellcaster.cooldowns[self.tag] = self.baseCooldown
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
||||||
return data.onCast(caster, target)
|
return data.onCast(caster, target)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -52,7 +52,8 @@ local regenerateMana = spell.new {
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
local sprite = caster:has(Tree.behaviors.sprite)
|
local sprite = caster:has(Tree.behaviors.sprite)
|
||||||
if not sprite then return end
|
local effects = caster:has(Tree.behaviors.effects)
|
||||||
|
if not sprite or not effects then return end -- и тут возможно на эффекты проверять не стоит
|
||||||
print(caster.id, "has regenerated mana and gained initiative")
|
print(caster.id, "has regenerated mana and gained initiative")
|
||||||
|
|
||||||
local light = require "lib/character/character".spawn("Light Effect")
|
local light = require "lib/character/character".spawn("Light Effect")
|
||||||
@ -67,7 +68,8 @@ local regenerateMana = spell.new {
|
|||||||
light:die()
|
light:die()
|
||||||
return task.fromValue()
|
return task.fromValue()
|
||||||
end),
|
end),
|
||||||
sprite:animate("hurt")
|
sprite:animate("hurt"),
|
||||||
|
effects:addEffect("aversionToDeath", 1, 1),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
@ -86,9 +88,10 @@ local attack = spell.new {
|
|||||||
stats.hp = stats.hp - 4
|
stats.hp = stats.hp - 4
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local targetEffects = targetCharacter:has(Tree.behaviors.effects)
|
||||||
local sprite = caster:has(Tree.behaviors.sprite)
|
local sprite = caster:has(Tree.behaviors.sprite)
|
||||||
local targetSprite = targetCharacter:has(Tree.behaviors.sprite)
|
local targetSprite = targetCharacter:has(Tree.behaviors.sprite)
|
||||||
if not sprite or not targetSprite then return end
|
if not sprite or not targetSprite or not targetEffects then return end -- проверять на эффект может и не стоит
|
||||||
|
|
||||||
caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) end)
|
caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) end)
|
||||||
|
|
||||||
@ -103,6 +106,7 @@ local attack = spell.new {
|
|||||||
Tree.behaviors.light.new { color = Vec3 { 0.6, 0.3, 0.3 }, intensity = 4 },
|
Tree.behaviors.light.new { color = Vec3 { 0.6, 0.3, 0.3 }, intensity = 4 },
|
||||||
Tree.behaviors.positioned.new(targetCharacter:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }),
|
Tree.behaviors.positioned.new(targetCharacter:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }),
|
||||||
}
|
}
|
||||||
|
Tree.audio:play(Tree.assets.files.audio.sounds.hurt)
|
||||||
return
|
return
|
||||||
task.wait {
|
task.wait {
|
||||||
task.chain(task.tween(light:has(Tree.behaviors.light) --[[@as LightBehavior]],
|
task.chain(task.tween(light:has(Tree.behaviors.light) --[[@as LightBehavior]],
|
||||||
@ -110,12 +114,11 @@ local attack = spell.new {
|
|||||||
light:die()
|
light:die()
|
||||||
return task.fromValue()
|
return task.fromValue()
|
||||||
end),
|
end),
|
||||||
targetSprite:animate("hurt")
|
targetSprite:animate("hurt"),
|
||||||
|
targetEffects:addEffect("bleeding", 3, 3)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
),
|
),
|
||||||
|
|
||||||
Tree.audio:play(Tree.assets.files.audio.sounds.hurt)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@ -74,4 +74,19 @@ function P.lerp(from, to, t)
|
|||||||
return from + (to - from) * t
|
return from + (to - from) * t
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Compares two tables by their fields
|
||||||
|
--- @param t1 table
|
||||||
|
--- @param t2 table
|
||||||
|
--- @return boolean
|
||||||
|
function P.deepComparison(t1, t2)
|
||||||
|
for k, v in pairs(t1) do
|
||||||
|
if type(v) == "table" and type(t2[k]) == "table" then
|
||||||
|
if not P.deepComparison(v, t2[k]) then return false end
|
||||||
|
elseif t2[k] ~= v then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
return P
|
return P
|
||||||
|
|||||||
92
main.lua
@ -15,64 +15,77 @@ function love.load()
|
|||||||
testLayout = require "lib.simple_ui.level.layout"
|
testLayout = require "lib.simple_ui.level.layout"
|
||||||
|
|
||||||
local chars = {
|
local chars = {
|
||||||
|
-- character.spawn("Foodor")
|
||||||
|
-- :addBehavior {
|
||||||
|
-- Tree.behaviors.residentsleeper.new(),
|
||||||
|
-- Tree.behaviors.stats.new(nil, nil, 1),
|
||||||
|
-- Tree.behaviors.positioned.new(Vec3 { 3, 3 }),
|
||||||
|
-- Tree.behaviors.tiled.new(),
|
||||||
|
-- Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
||||||
|
-- Tree.behaviors.shadowcaster.new(),
|
||||||
|
-- Tree.behaviors.spellcaster.new(),
|
||||||
|
-- Tree.behaviors.effects.new()
|
||||||
|
-- },
|
||||||
character.spawn("Foodor")
|
character.spawn("Foodor")
|
||||||
:addBehavior {
|
:addBehavior {
|
||||||
Tree.behaviors.residentsleeper.new(),
|
Tree.behaviors.residentsleeper.new(),
|
||||||
Tree.behaviors.stats.new(nil, nil, 1),
|
Tree.behaviors.stats.new(nil, nil, 1),
|
||||||
Tree.behaviors.positioned.new(Vec3 { 3, 3 }),
|
Tree.behaviors.positioned.new(Vec3 { 3, 1 }),
|
||||||
Tree.behaviors.tiled.new(),
|
Tree.behaviors.tiled.new(),
|
||||||
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
||||||
Tree.behaviors.shadowcaster.new(),
|
Tree.behaviors.shadowcaster.new(),
|
||||||
Tree.behaviors.spellcaster.new()
|
Tree.behaviors.spellcaster.new(),
|
||||||
},
|
Tree.behaviors.effects.new()
|
||||||
character.spawn("Foodor")
|
|
||||||
:addBehavior {
|
|
||||||
Tree.behaviors.residentsleeper.new(),
|
|
||||||
Tree.behaviors.stats.new(nil, nil, 1),
|
|
||||||
Tree.behaviors.positioned.new(Vec3 { 4, 3 }),
|
|
||||||
Tree.behaviors.tiled.new(),
|
|
||||||
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
|
||||||
Tree.behaviors.shadowcaster.new(),
|
|
||||||
Tree.behaviors.spellcaster.new()
|
|
||||||
},
|
},
|
||||||
character.spawn("Foodor")
|
character.spawn("Foodor")
|
||||||
:addBehavior {
|
:addBehavior {
|
||||||
Tree.behaviors.residentsleeper.new(),
|
Tree.behaviors.residentsleeper.new(),
|
||||||
Tree.behaviors.stats.new(nil, nil, 3),
|
Tree.behaviors.stats.new(nil, nil, 3),
|
||||||
Tree.behaviors.positioned.new(Vec3 { 5, 3 }),
|
Tree.behaviors.positioned.new(Vec3 { 7, 2 }),
|
||||||
Tree.behaviors.tiled.new(),
|
|
||||||
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
|
||||||
Tree.behaviors.shadowcaster.new(),
|
|
||||||
Tree.behaviors.spellcaster.new()
|
|
||||||
},
|
|
||||||
character.spawn("Baris")
|
|
||||||
:addBehavior {
|
|
||||||
Tree.behaviors.residentsleeper.new(),
|
|
||||||
Tree.behaviors.stats.new(nil, nil, 2),
|
|
||||||
Tree.behaviors.positioned.new(Vec3 { 5, 5 }),
|
|
||||||
Tree.behaviors.tiled.new(),
|
Tree.behaviors.tiled.new(),
|
||||||
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
||||||
Tree.behaviors.shadowcaster.new(),
|
Tree.behaviors.shadowcaster.new(),
|
||||||
Tree.behaviors.spellcaster.new(),
|
Tree.behaviors.spellcaster.new(),
|
||||||
Tree.behaviors.ai.new()
|
Tree.behaviors.effects.new()
|
||||||
},
|
|
||||||
character.spawn("BOAR")
|
|
||||||
:addBehavior {
|
|
||||||
Tree.behaviors.residentsleeper.new(),
|
|
||||||
Tree.behaviors.stats.new(nil, nil, 2),
|
|
||||||
Tree.behaviors.positioned.new(Vec3 { 7, 7 }),
|
|
||||||
Tree.behaviors.tiled.new(),
|
|
||||||
Tree.behaviors.sprite.new(Tree.assets.files.sprites.boar),
|
|
||||||
Tree.behaviors.shadowcaster.new(),
|
|
||||||
Tree.behaviors.spellcaster.new(),
|
|
||||||
Tree.behaviors.ai.new()
|
|
||||||
},
|
},
|
||||||
|
-- character.spawn("Baris")
|
||||||
|
-- :addBehavior {
|
||||||
|
-- Tree.behaviors.residentsleeper.new(),
|
||||||
|
-- Tree.behaviors.stats.new(nil, nil, 2),
|
||||||
|
-- Tree.behaviors.positioned.new(Vec3 { 5, 5 }),
|
||||||
|
-- Tree.behaviors.tiled.new(),
|
||||||
|
-- Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
|
||||||
|
-- Tree.behaviors.shadowcaster.new(),
|
||||||
|
-- Tree.behaviors.spellcaster.new(),
|
||||||
|
-- Tree.behaviors.ai.new(),
|
||||||
|
-- Tree.behaviors.effects.new()
|
||||||
|
-- },
|
||||||
|
-- character.spawn("BOAR")
|
||||||
|
-- :addBehavior {
|
||||||
|
-- Tree.behaviors.residentsleeper.new(),
|
||||||
|
-- Tree.behaviors.stats.new(nil, nil, 2),
|
||||||
|
-- Tree.behaviors.positioned.new(Vec3 { 7, 7 }),
|
||||||
|
-- Tree.behaviors.tiled.new(),
|
||||||
|
-- Tree.behaviors.sprite.new(Tree.assets.files.sprites.boar),
|
||||||
|
-- Tree.behaviors.shadowcaster.new(),
|
||||||
|
-- Tree.behaviors.spellcaster.new(),
|
||||||
|
-- Tree.behaviors.ai.new(),
|
||||||
|
-- Tree.behaviors.effects.new()
|
||||||
|
-- },
|
||||||
}
|
}
|
||||||
|
|
||||||
for id, _ in pairs(chars) do
|
for id, _ in pairs(chars) do
|
||||||
Tree.level.turnOrder:add(id)
|
Tree.level.turnOrder:add(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- --- Это тестовый источник света, привязанный к мышке, и я очень прошу его не трогать
|
||||||
|
-- character.spawn("light")
|
||||||
|
-- :addBehavior {
|
||||||
|
-- Tree.behaviors.positioned.new(),
|
||||||
|
-- Tree.behaviors.cursor.new(),
|
||||||
|
-- Tree.behaviors.light.new { color = Vec3 { 0.5, 0.5, 0.5 }, intensity = 5 }
|
||||||
|
-- }
|
||||||
|
|
||||||
|
|
||||||
Tree.level.turnOrder:endRound()
|
Tree.level.turnOrder:endRound()
|
||||||
print("Now playing:", Tree.level.turnOrder.current)
|
print("Now playing:", Tree.level.turnOrder.current)
|
||||||
@ -123,9 +136,14 @@ function love.draw()
|
|||||||
love.graphics.setColor(1, 1, 1)
|
love.graphics.setColor(1, 1, 1)
|
||||||
|
|
||||||
love.graphics.setFont(Tree.fonts:getTheme("Roboto_Mono"):getVariant("small"))
|
love.graphics.setFont(Tree.fonts:getTheme("Roboto_Mono"):getVariant("small"))
|
||||||
|
local mousePosX, mousePosY = love.mouse.getPosition()
|
||||||
|
local mousePos = Tree.level.camera:toWorldPosition(Vec3 { mousePosX, mousePosY }):floor()
|
||||||
local stats = "fps: " ..
|
local stats = "fps: " ..
|
||||||
love.timer.getFPS() ..
|
love.timer.getFPS() ..
|
||||||
" lt: " .. lt .. " dt: " .. dt .. " mem: " .. string.format("%.2f MB", collectgarbage("count") / 1000)
|
" lt: " .. lt ..
|
||||||
|
" dt: " .. dt ..
|
||||||
|
" mem: " .. string.format("%.2f MB", collectgarbage("count") / 1000) ..
|
||||||
|
" mouse pos: " .. tostring(mousePos)
|
||||||
love.graphics.print(stats, 10, 10)
|
love.graphics.print(stats, 10, 10)
|
||||||
|
|
||||||
local t2 = love.timer.getTime()
|
local t2 = love.timer.getTime()
|
||||||
@ -136,5 +154,5 @@ function love.resize(w, h)
|
|||||||
local render = Tree.level.render
|
local render = Tree.level.render
|
||||||
if not render then return end
|
if not render then return end
|
||||||
render:free()
|
render:free()
|
||||||
Tree.level.render = (require "lib.level.render").new { w, h }
|
Tree.level.render = (require "lib.level.render").new { w = w, h = h }
|
||||||
end
|
end
|
||||||
|
|||||||
Всё таки позволяет через наложение сильного короткого эффекта поверх слабого длинного получить и сильный, и длинный эффект одновременно?
да, мы вроде так и договорились по итогу
Так вроде смысл реферата по DOS сводился к тому, что так делать не надо? Мы сделали разные выводы?
Так вроде смысл реферата по DOS сводился к тому, что так делать не надо? Мы сделали разные выводы?
получается, ты сделал
видимо ¯(ツ)/¯, я предположил так сделать из-за всех этих скрытых механик, где ты переопределяешь бесконечный эффект конечным и тд
а поддержка бесконечных эффектов же не присутствует в каком-то дополнительном виде, кроме как не указывать эффекту момент потери стака?
да, можно добавить просто какое-то магическое значение по типу -1 ради ui, ну или сделать флаг для этого