Compare commits

..

No commits in common. "main" and "feature/ai-but-cooler" have entirely different histories.

61 changed files with 215 additions and 1043 deletions

3
.gitattributes vendored
View File

@ -1,3 +0,0 @@
*.ogg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text

View File

@ -6,6 +6,5 @@
"love.filesystem.load": "loadfile" "love.filesystem.load": "loadfile"
}, },
"workspace.ignoreDir": ["dev_utils"], "workspace.ignoreDir": ["dev_utils"],
"diagnostics.ignoredFiles": "Disable", "diagnostics.ignoredFiles": "Disable"
"hint.enable": true
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/audio/sounds/meow.ogg (Stored with Git LFS)

Binary file not shown.

BIN
assets/boar.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
assets/overlay_icons/atlas.png (Stored with Git LFS)

Binary file not shown.

View File

@ -1,6 +0,0 @@
return {
tileSize = 32,
["dev_target"] = { 0 },
["dev_path"] = { 1 },
["dev_path_closed"] = { 2 },
}

View File

@ -1,6 +0,0 @@
vec4 effect(vec4 color, Image tex, vec2 uv, vec2 px)
{
vec4 c = Texel(tex, uv);
float gray = dot(c.rgb, vec3(0.299, 0.587, 0.114)); // магические числа человеческого восприятия
return vec4(vec3(gray), c.a);
}

View File

@ -1,5 +0,0 @@
vec4 effect(vec4 color, Image tex, vec2 uv, vec2 px)
{
vec4 c = Texel(tex, uv);
return vec4(vec3(1.0) - c.rgb, c.a);
}

View File

@ -1,85 +0,0 @@
#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);
}

View File

@ -1,9 +0,0 @@
return {
width = 32,
height = 32,
base = {
x = 14,
y = 26
},
size = 0.6 -- условная доля тайла, которую занимает спрайт
}

BIN
assets/sprites/boar/sheets/idle.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/sprites/boar/sheets/run.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,9 +0,0 @@
return {
width = 96,
height = 64,
base = {
x = 38,
y = 47
},
size = 0.4 -- условная доля тайла, которую занимает спрайт
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/sprites/character/sheets/attack.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/sprites/character/sheets/hurt.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/sprites/character/sheets/idle.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/sprites/character/sheets/run.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -9,6 +9,5 @@ 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

View File

@ -1,7 +1,6 @@
local easing = require "lib.utils.easing" local easing = require "lib.utils.easing"
local pf = require "lib.pathfinder" local pf = require "lib.pathfinder"
local utils = require "lib.utils.utils" local utils = require "lib.utils.utils"
local task = require "lib.utils.task"
--- @alias AIAction fun(self: AIBehavior): Task<nil> --- @alias AIAction fun(self: AIBehavior): Task<nil>
@ -157,14 +156,35 @@ local behavior = {}
behavior.__index = behavior behavior.__index = behavior
behavior.id = "ai" behavior.id = "ai"
--- Заставляет ИИ сделать ход function behavior:dev_warrior()
--- return function(callback) -- почему так, описано в Task
--- По умолчанию ничего не делает self.owner:try(Tree.behaviors.spellcaster, function(spellB)
--- @return Task<nil> self.target = pathToClosestCharacter(self.owner, 1)
function behavior:makeTurn() local attackTarget = closestCharacter(self.owner):has(Tree.behaviors.positioned)
return function(callback) 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() callback()
end end
end
)
else
print('рот этого казино')
callback()
end
end)
end
end end
--- @param class Class --- @param class Class

View File

@ -1,187 +0,0 @@
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]: удаляем стаки!!")
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

View File

@ -32,19 +32,21 @@ 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
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.setShader()
love.graphics.setBlendMode("alpha") love.graphics.setBlendMode("alpha")
end)
love.graphics.setShader()
love.graphics.setCanvas()
Tree.level.camera:detach()
end end
return behavior return behavior

View File

@ -10,7 +10,6 @@ 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 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 }
@ -22,14 +21,43 @@ 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, 0.2, 0.2 * math.cos(math.pi / 4))
love.graphics.pop() love.graphics.pop()
end)
if not sprite then
love.graphics.setCanvas()
return
end
love.graphics.setCanvas(Tree.level.render.textures.spriteLightLayer)
love.graphics.setBlendMode("add")
for _, light in ipairs(lights) do
local lightPos = light:has(Tree.behaviors.positioned).position
local lightVec = lightPos - position
local lightColor = light:has(Tree.behaviors.light).color
if lightPos.y > position.y then
love.graphics.setColor(lightColor.x, lightColor.y, lightColor.z,
1 - 0.3 * lightVec:length())
elseif position.y - lightPos.y < 3 then
love.graphics.setColor(lightColor.x, lightColor.y, lightColor.z,
(1 - easing.easeInSine((position.y - lightPos.y))) - 0.3 * lightVec:length())
end
sprite.animationTable[sprite.state]:draw(Tree.assets.files.sprites.character[sprite.state],
position.x,
position.y, nil, 1 / ppm * sprite.side, 1 / ppm, 38, 47)
end
love.graphics.setBlendMode("alpha")
Tree.level.camera:detach()
love.graphics.setColor(1, 1, 1)
love.graphics.setCanvas()
end end
return behavior return behavior

View File

@ -20,9 +20,6 @@ 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()

View File

@ -3,8 +3,6 @@ local anim8 = require "lib.utils.anim8"
--- @class SpriteBehavior : Behavior --- @class SpriteBehavior : Behavior
--- @field animationTable table<string, table> --- @field animationTable table<string, table>
--- @field animationGrid table --- @field animationGrid table
--- @field manifest table
--- @field sheets table
--- @field state "idle"|"run"|"hurt"|"attack" --- @field state "idle"|"run"|"hurt"|"attack"
--- @field side 1|-1 --- @field side 1|-1
local sprite = {} local sprite = {}
@ -19,12 +17,11 @@ function sprite.new(spriteDir)
local anim = setmetatable({}, sprite) local anim = setmetatable({}, sprite)
anim.animationTable = {} anim.animationTable = {}
anim.animationGrid = {} anim.animationGrid = {}
anim.manifest = spriteDir.manifest
anim.sheets = spriteDir.sheets
-- n: name; i: image -- n: name; i: image
for n, i in pairs(spriteDir.sheets) do for n, i in pairs(spriteDir) do
local aGrid = anim8.newGrid(anim.manifest.width, anim.manifest.height, i:getWidth(), i:getHeight()) local aGrid = anim8.newGrid(96, 64, i:getWidth(), i:getHeight())
local tiles = '1-' .. math.ceil(i:getWidth() / anim.manifest.width) local tiles = '1-' .. math.ceil(i:getWidth() / 96)
anim.animationGrid[n] = aGrid(tiles, 1) anim.animationGrid[n] = aGrid(tiles, 1)
end end
@ -42,78 +39,32 @@ function sprite:update(dt)
end end
function sprite:draw() function sprite:draw()
if not self.animationTable[self.state] or not self.sheets[self.state] then return end if not self.animationTable[self.state] or not Tree.assets.files.sprites.character[self.state] then return end
self.owner:try(Tree.behaviors.positioned, self.owner:try(Tree.behaviors.positioned,
function(pos) function(pos)
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 }
Tree.level.render:enqueue(Tree.level.render.LAYERS.SPRITE, position.y, function() love.graphics.setCanvas(Tree.level.render.textures.spriteLayer)
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 texW, texH = Tree.assets.files.sprites.character[self.state]:getWidth(),
local queryRadius = 12 -- Увеличенный радиус для плавности Tree.assets.files.sprites.character[self.state]:getHeight()
local lightIds = Tree.level.lightGrid:query(position, queryRadius) local shader = Tree.assets.files.shaders.outline
local lightsData = {} shader:send("texSize", { texW, texH })
shader:send("time", love.timer:getTime())
for _, id in ipairs(lightIds) do love.graphics.setShader(shader)
local lightChar = Tree.level.characters[id]
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
end self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state],
-- Сортируем по дистанции, чтобы выбрать 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.x,
position.y, nil, 1 / ppm * self.side, 1 / ppm, self.manifest.base.x, self.manifest.base.y) position.y, nil, 1 / ppm * self.side, 1 / ppm, 38, 47)
love.graphics.setShader() love.graphics.setShader()
end) Tree.level.camera:detach()
love.graphics.setCanvas()
end end
) )
end end
@ -123,7 +74,7 @@ function sprite:animate(state)
return function(callback) return function(callback)
if not self.animationGrid[state] then if not self.animationGrid[state] then
print("[SpriteBehavior]: no animation for '" .. state .. "'") print("[SpriteBehavior]: no animation for '" .. state .. "'")
return callback() callback()
end end
self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED, self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED,
function() function()
@ -139,9 +90,6 @@ function sprite:loop(state)
return print("[SpriteBehavior]: no animation for '" .. state .. "'") return print("[SpriteBehavior]: no animation for '" .. state .. "'")
end end
self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED) self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED)
if state == 'idle' then
self.animationTable[state]:gotoFrame(love.math.random(#self.animationTable[state].frames))
end
self.state = state self.state = state
end end

View File

@ -6,27 +6,10 @@
--- @field initiative integer --- @field initiative integer
--- @field class Class --- @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
@ -39,7 +22,6 @@ function behavior.new(hp, mana, initiative, class, isInTurnOrder)
initiative = initiative or 10, initiative = initiative or 10,
class = class or "dev_warrior", class = class or "dev_warrior",
isInTurnOrder = isInTurnOrder or true, isInTurnOrder = isInTurnOrder or true,
amIAlive = true
}, behavior) }, behavior)
end end

View File

@ -1,123 +0,0 @@
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 -----------------
--- @alias EffectSumFunc fun(owner: Character, effect1: EffectTag, effect2: EffectTag): boolean
--- Принимает таблицу, в ключах которых тэги эффектов, которые мы хотим просуммировать, и в значениях которых функция,
--- возвращающая булево значение: применять ли эффект после суммирования.
--- @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

View File

@ -1,6 +1,8 @@
local utils = require "lib.utils.utils" local utils = require "lib.utils.utils"
local pQueue = require "lib.utils.priority_queue"
--- @class CharacterGrid : Grid --- @class CharacterGrid : Grid
--- @field __grid {string: Id|nil} --- @field __grid {string: Id|nil}
--- @field yOrderQueue PriorityQueue<Character> очередь отрисовки сверху вниз
local grid = setmetatable({}, require "lib.level.grid.base") local grid = setmetatable({}, require "lib.level.grid.base")
grid.__index = grid grid.__index = grid
@ -27,13 +29,22 @@ function grid:add(id)
end end
end end
--- @param a Character
--- @param b Character
local function drawCmp(a, b)
--- @TODO: это захардкожено, надо разделить поведения
return a:has(Tree.behaviors.positioned).position.y < b:has(Tree.behaviors.positioned).position.y
end
--- fills the grid with the actual data --- fills the grid with the actual data
--- ---
--- should be called as early as possible during every tick --- should be called as early as possible during every tick
function grid:reload() function grid:reload()
self:reset() self:reset()
self.yOrderQueue = pQueue.new(drawCmp)
utils.each(Tree.level.characters, function(c) utils.each(Tree.level.characters, function(c)
self:add(c.id) self:add(c.id)
self.yOrderQueue:insert(c)
end) end)
end end

View File

@ -9,32 +9,17 @@ 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)
local grid = setmetatable({ __grid = tMap }, map) return 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()
if not self.batch then return end love.graphics.setCanvas(Tree.level.render.textures.floorLayer)
Tree.level.render:enqueue(Tree.level.render.LAYERS.FLOOR, 0, function() Tree.level.camera:attach()
love.graphics.draw(self.batch) utils.each(self.__grid, function(el)
el:draw()
end) end)
Tree.level.camera:detach()
love.graphics.setCanvas()
end end
return { new = new } return { new = new }

View File

@ -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.progressive_plains) Tree.audio:play(Tree.assets.files.audio.music.level1.battle)
return setmetatable({ return setmetatable({
size = size, size = size,
@ -33,7 +33,7 @@ local function new(type, template)
camera = (require "lib.level.camera").new(), camera = (require "lib.level.camera").new(),
turnOrder = (require "lib.level.turn_order").new(), turnOrder = (require "lib.level.turn_order").new(),
render = (require "lib.level.render").new {}, render = (require "lib.level.render").new {},
weather = (require "lib.level.weather").new { ambientLight = Vec3 { 0.5, 0.5, 0.5 }, skyLight = Vec3 { 0.85, 0.55, 0.40 } } weather = (require "lib.level.weather").new { ambientLight = Vec3 { 0.36, 0.42, 0.6 }, skyLight = Vec3 {} }
}, level) }, level)
end end
@ -56,9 +56,9 @@ end
function level:draw() function level:draw()
self.render:clear() self.render:clear()
self.tileGrid:draw() self.tileGrid:draw()
utils.each(self.characters, function(char) while not self.characterGrid.yOrderQueue:is_empty() do -- по сути это сортировка кучей за n log n
char:draw() self.characterGrid.yOrderQueue:pop():draw()
end) end
self.render:draw() self.render:draw()
end end

View File

@ -1,30 +1,18 @@
--- @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)
@ -33,10 +21,6 @@ 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()
@ -73,102 +57,50 @@ function render:applyBlur(input, radius)
return self.textures.tmp2 return self.textures.tmp2
end end
function render:draw()
-- пол -> тени -> спрайты -> свет -> оверлей
local weather = Tree.level.weather
local txs = self.textures
love.graphics.setCanvas(txs.lightLayer)
love.graphics.draw(self:applyBlur(txs.shadowLayer, 4 * Tree.level.camera.scale))
love.graphics.setCanvas()
-- self.lightLayer:newImageData():encode("png", "lightLayer.png")
-- os.exit(0)
local lightShader = Tree.assets.files.shaders.light_postprocess
lightShader:send("scene", txs.floorLayer)
lightShader:send("light", self:applyBlur(txs.lightLayer, 2))
lightShader:send("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z })
love.graphics.setShader(lightShader)
love.graphics.draw(txs.floorLayer)
lightShader:send("scene", txs.spriteLayer)
lightShader:send("light", txs.spriteLightLayer)
love.graphics.draw(txs.spriteLayer)
love.graphics.setShader()
love.graphics.draw(txs.overlayLayer)
end
---@param params {w: number?, h: number?} ---@param params {w: number?, h: number?}
---@return table|Render ---@return table|Render
local function new(params) local function new(params)
local w = params.w or love.graphics.getWidth() local w = params.w or love.graphics.getWidth()
local h = params.h or love.graphics.getHeight() local h = params.h or love.graphics.getHeight()
local lowResScale = 0.5
return setmetatable({ return setmetatable({
lowResScale = lowResScale,
queue = {},
textures = { textures = {
shadowLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale), shadowLayer = love.graphics.newCanvas(w, h),
spriteLayer = love.graphics.newCanvas(w, h), spriteLayer = love.graphics.newCanvas(w, h),
spriteLightLayer = love.graphics.newCanvas(w, h),
floorLayer = love.graphics.newCanvas(w, h), floorLayer = love.graphics.newCanvas(w, h),
overlayLayer = love.graphics.newCanvas(w, h), overlayLayer = love.graphics.newCanvas(w, h),
lightLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale), lightLayer = love.graphics.newCanvas(w, h),
tmp1 = love.graphics.newCanvas(w * lowResScale, h * lowResScale), tmp1 = love.graphics.newCanvas(w, h),
tmp2 = love.graphics.newCanvas(w * lowResScale, h * lowResScale), tmp2 = love.graphics.newCanvas(w, h),
} }
}, { __index = render }) }, { __index = render })
end end
function render:draw()
local weather = Tree.level.weather
local txs = self.textures
-- 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()
-- 3. Пост-процессинг и композиция
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
lightShader:send("scene", txs.floorLayer)
lightShader:send("light", self:applyBlur(txs.lightLayer, 2 * self.lowResScale))
lightShader:send("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z })
love.graphics.setShader(lightShader)
love.graphics.draw(txs.floorLayer)
love.graphics.setShader()
love.graphics.draw(txs.overlayLayer)
-- Спрайты уже полностью освещены в SpriteBehavior (с учетом ambient и point lights)
-- Поэтому рисуем их "как есть"
love.graphics.draw(txs.spriteLayer)
end
return { new = new } return { new = new }

View File

@ -43,15 +43,6 @@ 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()
@ -78,9 +69,6 @@ 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

View File

@ -112,7 +112,6 @@ function skillRow.new(characterId)
if not behavior.cast then if not behavior.cast then
behavior.cast = behavior.spellbook[i] behavior.cast = behavior.spellbook[i]
behavior.state = "casting" behavior.state = "casting"
behavior.spellbook[i]:onSelected(char)
else else
behavior.state = "idle" behavior.state = "idle"
behavior.cast = nil behavior.cast = nil

View File

@ -1,169 +0,0 @@
local utils = require "lib.utils.utils"
local taskUtils = require "lib.utils.task"
--- Некоторое свойство, что можно наложить на персонажа. Позволяет реализовать такие вещи как 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
}
end
function effect:update(dt) end
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 }

View File

@ -1,8 +1,6 @@
local Query = require "lib.spell.target_query" local Query = require "lib.spell.target_query"
local targetTest = require "lib.spell.target_test" local targetTest = require "lib.spell.target_test"
local task = require "lib.utils.task" local task = require "lib.utils.task"
local easing = require "lib.utils.easing"
local pf = require "lib.pathfinder"
--- @alias SpellPreview "default" Подсветка возможных целей --- @alias SpellPreview "default" Подсветка возможных целей
--- | "path" Подсветка пути до цели --- | "path" Подсветка пути до цели
@ -12,7 +10,7 @@ local pf = require "lib.pathfinder"
--- @field baseCost integer Базовые затраты маны на каст --- @field baseCost integer Базовые затраты маны на каст
--- @field baseCooldown integer Базовый кулдаун в ходах --- @field baseCooldown integer Базовый кулдаун в ходах
--- @field targetQuery SpellTargetQuery Селектор возможных целей --- @field targetQuery SpellTargetQuery Селектор возможных целей
--- @field targetType SpellPreview Вид превью во время каста --- @field previewType SpellPreview Вид превью во время каста
--- @field distance? integer Сторона квадрата с центром в позиции кастера, в пределах которого должна находиться цель, либо отсутствие ограничения --- @field distance? integer Сторона квадрата с центром в позиции кастера, в пределах которого должна находиться цель, либо отсутствие ограничения
--- @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 Рисует превью каста, ничего не должна изменять в идеальном мире
@ -23,19 +21,10 @@ spell.tag = "spell_base"
spell.baseCost = 1 spell.baseCost = 1
spell.baseCooldown = 1 spell.baseCooldown = 1
spell.targetQuery = Query(targetTest.any) spell.targetQuery = Query(targetTest.any)
spell.targetType = "default" spell.previewType = "default"
--- Вызывается, когда игрок выбирает спелл на панели заклинаний
--- @param caster Character
function spell:onSelected(caster)
self.targets = self.targetQuery:asSet(caster)
self.tSize = 0.67 -- анимация появления таргетов
task.tween(self, { tSize = 1 }, 200, easing.easeOutQuad)
end
function spell:update(caster, dt) function spell:update(caster, dt)
if self.targetType == "path" then if self.previewType == "path" then
local charPos = caster:has(Tree.behaviors.positioned).position:floor() local charPos = caster:has(Tree.behaviors.positioned).position:floor()
--- @type Vec3 --- @type Vec3
local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor() local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor()
@ -47,42 +36,27 @@ function spell:update(caster, dt)
end end
end end
local icons = require("lib.utils.sprite_atlas").load(Tree.assets.files.overlay_icons)
function spell:draw() function spell:draw()
if self.targetType == "path" then if self.previewType == "path" then
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.render:enqueue(Tree.level.render.LAYERS.OVERLAY, 0, function() Tree.level.camera:attach()
local i = 0 love.graphics.setCanvas(Tree.level.render.textures.overlayLayer)
path:pop_front() love.graphics.setColor(0.6, 0.75, 0.5)
for p in path:values() do for p in path:values() do
i = i + 1 love.graphics.circle("fill", p.x + 0.45, p.y + 0.45, 0.1)
local s = 1 / Tree.level.camera.pixelsPerMeter
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)
end end
love.graphics.setCanvas()
Tree.level.camera:detach()
love.graphics.setColor(1, 1, 1) love.graphics.setColor(1, 1, 1)
end)
else
Tree.level.render:enqueue(Tree.level.render.LAYERS.OVERLAY, 0, function()
love.graphics.setColor(1, 1, 1, 0.5)
for _, p in pairs(self.targets) do
local s = self.tSize / Tree.level.camera.pixelsPerMeter
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)
end
love.graphics.setShader()
love.graphics.setColor(1, 1, 1)
end)
end end
end end
function spell:cast(caster, target) return task.fromValue() end function spell:cast(caster, target) return task.fromValue() end
--- Конструктор [Spell] --- Конструктор [Spell]
--- @param data {tag: string, baseCost: integer, baseCooldown: integer, targetQuery: SpellTargetQuery?, targetType: SpellPreview?, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task<nil>?} --- @param data {tag: string, baseCost: integer, baseCooldown: integer, targetQuery: SpellTargetQuery?, previewType: SpellPreview?, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task<nil>?}
--- @return Spell --- @return Spell
function spell.new(data) function spell.new(data)
local newSpell = setmetatable({ local newSpell = setmetatable({
@ -90,11 +64,11 @@ function spell.new(data)
baseCost = data.baseCost, baseCost = data.baseCost,
baseCooldown = data.baseCooldown, baseCooldown = data.baseCooldown,
targetQuery = data.targetQuery, targetQuery = data.targetQuery,
targetType = data.targetType, previewType = data.previewType,
distance = data.distance distance = data.distance
}, spell) }, spell)
newSpell.targetQuery = (newSpell.distance and newSpell.targetType ~= "path") newSpell.targetQuery = newSpell.distance
and newSpell.targetQuery:intersect(Query(targetTest.distance(newSpell.distance))) and newSpell.targetQuery:intersect(Query(targetTest.distance(newSpell.distance)))
or newSpell.targetQuery or newSpell.targetQuery
@ -106,15 +80,6 @@ function spell.new(data)
end end
if not self.targetQuery.test(caster, target) then return end -- проверка корректности цели if not self.targetQuery.test(caster, target) then return end -- проверка корректности цели
if self.targetType == "path" then
-- дополнительное условие для спеллов с путями (количество шагов)
if not caster:has(Tree.behaviors.tiled) or not caster:has(Tree.behaviors.positioned) then return end
local i = 1
for _ in pf(caster:has(Tree.behaviors.positioned).position, target):values() do
if i > self.distance + 1 then return end -- учитывается начальная точка, где находится кастер
i = i + 1
end
end
-- проверка на достаточное количество маны -- проверка на достаточное количество маны
if caster:try(Tree.behaviors.stats, function(stats) if caster:try(Tree.behaviors.stats, function(stats)
@ -123,13 +88,6 @@ 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)
@ -137,8 +95,6 @@ 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

View File

@ -48,7 +48,7 @@ end
function query:asSet(caster) function query:asSet(caster)
--- @TODO: оптимизировать и брать не всю карту для выборки --- @TODO: оптимизировать и брать не всю карту для выборки
local res = {} local res = {}
for _, tile in pairs(Tree.level.tileGrid.__grid) do for _, tile in pairs(Tree.level.tileGrid) do
if self.test(caster, tile.position) then if self.test(caster, tile.position) then
table.insert(res, tile.position) table.insert(res, tile.position)
end end

View File

@ -15,7 +15,7 @@ local easing = require "lib.utils.easing"
local walk = spell.new { local walk = spell.new {
tag = "dev_move", tag = "dev_move",
targetType = "path", previewType = "path",
baseCooldown = 1, baseCooldown = 1,
baseCost = 2, baseCost = 2,
targetQuery = Query(targetTest.any):exclude(Query(targetTest.character)), targetQuery = Query(targetTest.any):exclude(Query(targetTest.character)),
@ -52,8 +52,7 @@ local regenerateMana = spell.new {
end) end)
local sprite = caster:has(Tree.behaviors.sprite) local sprite = caster:has(Tree.behaviors.sprite)
local effects = caster:has(Tree.behaviors.effects) if not sprite then return end
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")
@ -68,8 +67,7 @@ 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
} }
@ -88,10 +86,9 @@ 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 or not targetEffects then return end -- проверять на эффект может и не стоит if not sprite or not targetSprite 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)
@ -106,7 +103,6 @@ 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]],
@ -114,11 +110,12 @@ 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

View File

@ -74,19 +74,4 @@ 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

View File

@ -15,17 +15,16 @@ function love.load()
testLayout = require "lib.simple_ui.level.layout" testLayout = require "lib.simple_ui.level.layout"
local chars = { local chars = {
-- 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 { 1, 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") character.spawn("Foodor")
:addBehavior { :addBehavior {
Tree.behaviors.residentsleeper.new(), Tree.behaviors.residentsleeper.new(),
@ -34,8 +33,7 @@ function love.load()
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") character.spawn("Foodor")
:addBehavior { :addBehavior {
@ -45,47 +43,25 @@ function love.load()
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("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("dev_warrior") -- так мы вообще делать не должны, и он должен как-то подцеплять class из stats, но как я хз честно
}, },
-- 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)
@ -154,5 +130,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 = w, h = h } Tree.level.render = (require "lib.level.render").new { w, h }
end end