diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7236a22 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/assets/audio/music/level1/bass/bass.ogg b/assets/audio/music/level1/bass/bass.ogg index 6e2b785..0df10c9 100644 Binary files a/assets/audio/music/level1/bass/bass.ogg and b/assets/audio/music/level1/bass/bass.ogg differ diff --git a/assets/audio/music/level1/battle.ogg b/assets/audio/music/level1/battle.ogg index 0984bb9..71eb18e 100644 Binary files a/assets/audio/music/level1/battle.ogg and b/assets/audio/music/level1/battle.ogg differ diff --git a/assets/audio/music/level1/choral.ogg b/assets/audio/music/level1/choral.ogg index eea732b..8b54a57 100644 Binary files a/assets/audio/music/level1/choral.ogg and b/assets/audio/music/level1/choral.ogg differ diff --git a/assets/audio/music/level1/drums.ogg b/assets/audio/music/level1/drums.ogg index 731cb32..64102a8 100644 Binary files a/assets/audio/music/level1/drums.ogg and b/assets/audio/music/level1/drums.ogg differ diff --git a/assets/audio/music/level1/flute.ogg b/assets/audio/music/level1/flute.ogg index 777de17..d056ff6 100644 Binary files a/assets/audio/music/level1/flute.ogg and b/assets/audio/music/level1/flute.ogg differ diff --git a/assets/audio/music/level1/guitar.ogg b/assets/audio/music/level1/guitar.ogg index 47ce20b..57b4595 100644 Binary files a/assets/audio/music/level1/guitar.ogg and b/assets/audio/music/level1/guitar.ogg differ diff --git a/assets/audio/music/level1/progressive_plains.ogg b/assets/audio/music/level1/progressive_plains.ogg new file mode 100644 index 0000000..c04e176 --- /dev/null +++ b/assets/audio/music/level1/progressive_plains.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2566dc9c054936218d5a26669e1469d9807d91612ac53a8e549b60a6d873885 +size 3103510 diff --git a/assets/audio/music/level1/violin.ogg b/assets/audio/music/level1/violin.ogg index d216526..4802257 100644 Binary files a/assets/audio/music/level1/violin.ogg and b/assets/audio/music/level1/violin.ogg differ diff --git a/assets/audio/sounds/hurt.ogg b/assets/audio/sounds/hurt.ogg index 7b921cf..90e9c1d 100644 Binary files a/assets/audio/sounds/hurt.ogg and b/assets/audio/sounds/hurt.ogg differ diff --git a/assets/audio/sounds/meow.ogg b/assets/audio/sounds/meow.ogg new file mode 100644 index 0000000..3a7cc9c --- /dev/null +++ b/assets/audio/sounds/meow.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa52fa051a07aad736458e0f0497eabfdcb66685484fba338209291cd484c83f +size 30606 diff --git a/assets/boar.png b/assets/boar.png index f1cab84..160d76e 100644 Binary files a/assets/boar.png and b/assets/boar.png differ diff --git a/assets/cats.png b/assets/cats.png index 7190b42..224a698 100644 Binary files a/assets/cats.png and b/assets/cats.png differ diff --git a/assets/dev_icons/atlas.png b/assets/dev_icons/atlas.png index f6dc572..8ae6961 100644 Binary files a/assets/dev_icons/atlas.png and b/assets/dev_icons/atlas.png differ diff --git a/assets/fonts/Roboto_Mono/font.ttf b/assets/fonts/Roboto_Mono/font.ttf index 3806bfb..b6ecc09 100644 Binary files a/assets/fonts/Roboto_Mono/font.ttf and b/assets/fonts/Roboto_Mono/font.ttf differ diff --git a/assets/fonts/WDXL_Lubrifont_TC/font.ttf b/assets/fonts/WDXL_Lubrifont_TC/font.ttf index a98f355..9e962b0 100644 Binary files a/assets/fonts/WDXL_Lubrifont_TC/font.ttf and b/assets/fonts/WDXL_Lubrifont_TC/font.ttf differ diff --git a/assets/masks/circle128.png b/assets/masks/circle128.png index 007a29d..fb9f877 100644 Binary files a/assets/masks/circle128.png and b/assets/masks/circle128.png differ diff --git a/assets/masks/gradientCircle256.png b/assets/masks/gradientCircle256.png index d93611c..b622a46 100644 Binary files a/assets/masks/gradientCircle256.png and b/assets/masks/gradientCircle256.png differ diff --git a/assets/masks/rrect32.png b/assets/masks/rrect32.png index dff5102..aa6faba 100644 Binary files a/assets/masks/rrect32.png and b/assets/masks/rrect32.png differ diff --git a/assets/masks/squircle.png b/assets/masks/squircle.png index 8bd5a6d..342378e 100644 Binary files a/assets/masks/squircle.png and b/assets/masks/squircle.png differ diff --git a/assets/overlay_icons/atlas.png b/assets/overlay_icons/atlas.png index 1443205..abed22d 100644 Binary files a/assets/overlay_icons/atlas.png and b/assets/overlay_icons/atlas.png differ diff --git a/assets/shaders/sprite_light.glsl b/assets/shaders/sprite_light.glsl new file mode 100644 index 0000000..f55b5ae --- /dev/null +++ b/assets/shaders/sprite_light.glsl @@ -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); +} diff --git a/assets/sprites/boar/sheets/idle.png b/assets/sprites/boar/sheets/idle.png index defff43..419fb33 100644 Binary files a/assets/sprites/boar/sheets/idle.png and b/assets/sprites/boar/sheets/idle.png differ diff --git a/assets/sprites/boar/sheets/run.png b/assets/sprites/boar/sheets/run.png index 904212d..9839871 100644 Binary files a/assets/sprites/boar/sheets/run.png and b/assets/sprites/boar/sheets/run.png differ diff --git a/assets/sprites/character/sheets/attack.png b/assets/sprites/character/sheets/attack.png index 8e9b7b0..b18632c 100644 Binary files a/assets/sprites/character/sheets/attack.png and b/assets/sprites/character/sheets/attack.png differ diff --git a/assets/sprites/character/sheets/hurt.png b/assets/sprites/character/sheets/hurt.png index 533cd55..eacceec 100644 Binary files a/assets/sprites/character/sheets/hurt.png and b/assets/sprites/character/sheets/hurt.png differ diff --git a/assets/sprites/character/sheets/idle.png b/assets/sprites/character/sheets/idle.png index 2068a96..ce3154e 100644 Binary files a/assets/sprites/character/sheets/idle.png and b/assets/sprites/character/sheets/idle.png differ diff --git a/assets/sprites/character/sheets/run.png b/assets/sprites/character/sheets/run.png index 9ca77fa..6d99592 100644 Binary files a/assets/sprites/character/sheets/run.png and b/assets/sprites/character/sheets/run.png differ diff --git a/assets/sprites/test.png b/assets/sprites/test.png index 0685450..bb87a65 100644 Binary files a/assets/sprites/test.png and b/assets/sprites/test.png differ diff --git a/assets/tiles/grass/atlas.png b/assets/tiles/grass/atlas.png index dd5d66c..717c855 100644 Binary files a/assets/tiles/grass/atlas.png and b/assets/tiles/grass/atlas.png differ diff --git a/lib/annotations.lua b/lib/annotations.lua index a6be927..00ae451 100644 --- a/lib/annotations.lua +++ b/lib/annotations.lua @@ -9,5 +9,6 @@ Tree.behaviors.positioned = require "character.behaviors.positioned" Tree.behaviors.tiled = require "character.behaviors.tiled" Tree.behaviors.cursor = require "character.behaviors.cursor" Tree.behaviors.ai = require "lib.character.behaviors.ai" +Tree.behaviors.effects = require "lib.character.behaviors.effects" --- @alias voidCallback fun(): nil diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index b5e7b82..c3ac027 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -1,5 +1,10 @@ local easing = require "lib.utils.easing" +local pf = require "lib.pathfinder" +local utils = require "lib.utils.utils" +--- @alias AIAction fun(self: AIBehavior): Task + +--- @return Character local function closestCharacter(char) local caster = Vec3 {} char:try(Tree.behaviors.positioned, function(b) @@ -20,45 +25,142 @@ local function closestCharacter(char) return charTarget 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 +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 --- @field target Vec3? local behavior = {} behavior.__index = behavior behavior.id = "ai" -function behavior.new() - return setmetatable({}, behavior) -end - ---- @return Task -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 +--- @param class Class +function behavior.new(class) + return setmetatable({ + makeTurn = aiNature[class] + }, behavior) end return behavior diff --git a/lib/character/behaviors/effects.lua b/lib/character/behaviors/effects.lua new file mode 100644 index 0000000..3099652 --- /dev/null +++ b/lib/character/behaviors/effects.lua @@ -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 хранит характеристики эффектов +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 diff --git a/lib/character/behaviors/light.lua b/lib/character/behaviors/light.lua index 1f8b9d8..dd26adc 100644 --- a/lib/character/behaviors/light.lua +++ b/lib/character/behaviors/light.lua @@ -32,22 +32,19 @@ function behavior:draw() local positioned = self.owner:has(Tree.behaviors.positioned) if not positioned then return end - love.graphics.setBlendMode("add", "premultiplied") - Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.textures.lightLayer) - local shader = Tree.assets.files.shaders.light - shader:send("color", { self.color.x, self.color.y, self.color.z }) - shader:send("time", love.timer.getTime() + self.seed) - love.graphics.setShader(shader) - love.graphics.draw(Tree.assets.files.masks.circle128, positioned.position.x - self.intensity / 2, - positioned.position.y - self.intensity / 2, 0, self.intensity / 128, - self.intensity / 128) + Tree.level.render:enqueue(Tree.level.render.LAYERS.LIGHT, positioned.position.y, function() + love.graphics.setBlendMode("add", "premultiplied") + local shader = Tree.assets.files.shaders.light + shader:send("color", { self.color.x, self.color.y, self.color.z }) + shader:send("time", love.timer:getTime() + self.seed) + love.graphics.setShader(shader) + love.graphics.draw(Tree.assets.files.masks.circle128, positioned.position.x - self.intensity / 2, + positioned.position.y - self.intensity / 2, 0, self.intensity / 128, + self.intensity / 128) - love.graphics.setBlendMode("alpha") - - love.graphics.setShader() - love.graphics.setCanvas() - Tree.level.camera:detach() + love.graphics.setShader() + love.graphics.setBlendMode("alpha") + end) end return behavior diff --git a/lib/character/behaviors/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua index c343cf8..0b2daeb 100644 --- a/lib/character/behaviors/shadowcaster.lua +++ b/lib/character/behaviors/shadowcaster.lua @@ -10,10 +10,7 @@ function behavior:draw() local sprite = self.owner:has(Tree.behaviors.sprite) local positioned = self.owner:has(Tree.behaviors.positioned) if not positioned then return end - if not sprite then - love.graphics.setCanvas() - return - end + if not sprite then return end local ppm = Tree.level.camera.pixelsPerMeter local position = positioned.position + Vec3 { 0.5, 0.5 } @@ -25,41 +22,14 @@ function behavior:draw() table.insert(lights, Tree.level.characters[id]) end - - Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.textures.shadowLayer) - love.graphics.push() - love.graphics.setColor(0, 0, 0, 1) - love.graphics.translate(position.x, position.y) - love.graphics.ellipse("fill", 0, 0, sprite.manifest.size / 2, sprite.manifest.size / 2 * math.cos(math.pi / 4)) - love.graphics.pop() - - - - 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() + -- 1. Эллипс тени + Tree.level.render:enqueue(Tree.level.render.LAYERS.SHADOW, position.y, function() + love.graphics.push() + love.graphics.setColor(0, 0, 0, 1) + 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.pop() + end) end return behavior diff --git a/lib/character/behaviors/spellcaster.lua b/lib/character/behaviors/spellcaster.lua index aa0ad60..8974c39 100644 --- a/lib/character/behaviors/spellcaster.lua +++ b/lib/character/behaviors/spellcaster.lua @@ -20,6 +20,9 @@ function behavior.new(spellbook) end function behavior:endCast() + self.owner:try(Tree.behaviors.effects, function(effects) + effects:afterCast() + end) self.state = "idle" self.cast = nil Tree.level.turnOrder:reorder() diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 0325a4b..318f2ed 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -49,25 +49,71 @@ function sprite:draw() local ppm = Tree.level.camera.pixelsPerMeter local position = pos.position + Vec3 { 0.5, 0.5 } - love.graphics.setCanvas(Tree.level.render.textures.spriteLayer) - Tree.level.camera:attach() + Tree.level.render:enqueue(Tree.level.render.LAYERS.SPRITE, position.y, function() + love.graphics.setColor(1, 1, 1) - love.graphics.setColor(1, 1, 1) - if Tree.level.selector.id == self.owner.id then - local texW, texH = self.sheets[self.state]:getWidth(), - self.sheets[self.state]:getHeight() - 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) + -- Собираем источники света для шейдера + local queryRadius = 12 -- Увеличенный радиус для плавности + local lightIds = Tree.level.lightGrid:query(position, queryRadius) + local lightsData = {} - love.graphics.setShader() - Tree.level.camera:detach() - love.graphics.setCanvas() + for _, id in ipairs(lightIds) do + 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 + + -- Сортируем по дистанции, чтобы выбрать 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 diff --git a/lib/character/behaviors/stats.lua b/lib/character/behaviors/stats.lua index 965d290..990a7a6 100644 --- a/lib/character/behaviors/stats.lua +++ b/lib/character/behaviors/stats.lua @@ -1,22 +1,45 @@ +--- @alias Class "dev_warrior"|"dev_mage" + --- @class StatsBehavior : Behavior --- @field hp integer --- @field mana integer --- @field initiative integer +--- @field class Class --- @field isInTurnOrder boolean +--- @field amIAlive boolean local behavior = {} behavior.__index = behavior 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 mana? integer --- @param initiative? integer +--- @param class? Class --- @param isInTurnOrder? boolean -function behavior.new(hp, mana, initiative, isInTurnOrder) +function behavior.new(hp, mana, initiative, class, isInTurnOrder) return setmetatable({ hp = hp or 20, mana = mana or 10, initiative = initiative or 10, - isInTurnOrder = isInTurnOrder or true + isInTurnOrder = isInTurnOrder or true, + class = "dev_warrior", + amIAlive = true }, behavior) end diff --git a/lib/effectbook.lua b/lib/effectbook.lua new file mode 100644 index 0000000..3f6f3f8 --- /dev/null +++ b/lib/effectbook.lua @@ -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 ----------------- + +--- @alias EffectSumFunc fun(owner: Character, effect1: EffectTag, effect2: EffectTag): boolean + +--- Принимает таблицу, в ключах которых тэги эффектов, которые мы хотим просуммировать, и в значениях которых функция, +--- возвращающая булево значение: применять ли эффект после суммирования. +--- @type table> +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> +--- @field book table +local effectbook = { + sums = sums, + book = { + bleeding = bleeding, + aversionToDeath = aversionToDeath + } +} + +return effectbook diff --git a/lib/level/grid/tile_grid.lua b/lib/level/grid/tile_grid.lua index 1a89a10..8982abb 100644 --- a/lib/level/grid/tile_grid.lua +++ b/lib/level/grid/tile_grid.lua @@ -9,17 +9,32 @@ map.__index = map --- @param size? Vec3 local function new(type, 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 function map:draw() - love.graphics.setCanvas(Tree.level.render.textures.floorLayer) - Tree.level.camera:attach() - utils.each(self.__grid, function(el) - el:draw() + if not self.batch then return end + Tree.level.render:enqueue(Tree.level.render.LAYERS.FLOOR, 0, function() + love.graphics.draw(self.batch) end) - Tree.level.camera:detach() - love.graphics.setCanvas() end return { new = new } diff --git a/lib/level/level.lua b/lib/level/level.lua index 67945eb..5148a04 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -20,7 +20,7 @@ local function new(type, template) local size = Vec3 { 30, 30 } -- magic numbers for testing purposes only print(type, template, size) - Tree.audio:play(Tree.assets.files.audio.music.level1.battle) + Tree.audio:play(Tree.assets.files.audio.music.level1.progressive_plains) return setmetatable({ size = size, @@ -56,9 +56,9 @@ end function level:draw() self.render:clear() self.tileGrid:draw() - while not self.characterGrid.yOrderQueue:is_empty() do -- по сути это сортировка кучей за n log n - self.characterGrid.yOrderQueue:pop():draw() - end + utils.each(self.characters, function(char) + char:draw() + end) self.render:draw() end diff --git a/lib/level/render.lua b/lib/level/render.lua index b0901d8..8cf636b 100644 --- a/lib/level/render.lua +++ b/lib/level/render.lua @@ -1,18 +1,30 @@ --- @class Render --- @field textures table +--- @field queue table[] +--- @field lowResScale number +--- @field LAYERS table local render = { - textures = {} + textures = {}, + queue = {}, + lowResScale = 1.0, + LAYERS = { + FLOOR = 1, + SHADOW = 2, + LIGHT = 3, + SPRITE = 4, + OVERLAY = 5 + } } function render:clear() local weather = Tree.level.weather local txs = self.textures + self.queue = {} + love.graphics.setCanvas(txs.shadowLayer) love.graphics.clear() love.graphics.setCanvas(txs.spriteLayer) love.graphics.clear() - love.graphics.setCanvas(txs.spriteLightLayer) - love.graphics.clear(weather.skyLight.x, weather.skyLight.y, weather.skyLight.z) love.graphics.setCanvas(txs.floorLayer) love.graphics.clear() love.graphics.setCanvas(txs.lightLayer) @@ -21,6 +33,10 @@ function render:clear() love.graphics.clear() end +function render:enqueue(layer, z, func) + table.insert(self.queue, { layer = layer, z = z, func = func }) +end + function render:free() for _, tx in pairs(self.textures) do tx:release() @@ -57,50 +73,102 @@ function render:applyBlur(input, radius) return self.textures.tmp2 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() 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)) + + -- 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() - -- self.lightLayer:newImageData():encode("png", "lightLayer.png") - -- os.exit(0) + -- 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)) + 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) - love.graphics.setShader(lightShader) - lightShader:send("scene", txs.spriteLayer) - lightShader:send("light", txs.spriteLightLayer) + -- Спрайты уже полностью освещены в SpriteBehavior (с учетом ambient и point lights) + -- Поэтому рисуем их "как есть" 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 return { new = new } diff --git a/lib/level/turn_order.lua b/lib/level/turn_order.lua index 08e9eec..a166e62 100644 --- a/lib/level/turn_order.lua +++ b/lib/level/turn_order.lua @@ -43,6 +43,15 @@ function turnOrder:next() char:try(Tree.behaviors.positioned, function(positioned) Tree.level.camera:animateTo(positioned.position, 1500, easing.easeInOutCubic)( 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 char:has(Tree.behaviors.ai):makeTurn()( function() @@ -69,6 +78,9 @@ function turnOrder:endRound() char:try(Tree.behaviors.spellcaster, function(spellcaster) spellcaster:processCooldowns() end) + char:try(Tree.behaviors.effects, function(effects) + effects:afterTurn() + end) end self.actedQueue, self.pendingQueue = self.pendingQueue, self.actedQueue diff --git a/lib/spell/effect.lua b/lib/spell/effect.lua new file mode 100644 index 0000000..863f474 --- /dev/null +++ b/lib/spell/effect.lua @@ -0,0 +1,169 @@ +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 бред конечно, но иначе всё в жёлтом +--- @alias EffectStatementFunc fun(owner: Character, intensity: integer): Task, boolean +--- @alias EffectDamageFunc fun(owner: Character, intensity: integer, damage: integer): Task, integer +--- @alias EffectRegenFunc fun(owner: Character, intensity: integer, amountHp: integer): Task, integer +--- @alias EffectData { tag: string } + +--- Срабатывает перед применением эффекта +--- +--- Возвращает, а можно ли применить эффект? +--- @param owner Character +--- @param intensity integer +--- @return Task, boolean +function effect:beforeBirth(owner, intensity) return taskUtils.fromValue(), true end + +--- Срабатывает после применения эффекта +--- @param owner Character +--- @param intensity integer +--- @return Task +function effect:afterBirth(owner, intensity) return taskUtils.fromValue() end + +--- Срабатывает перед смертью владельца эффекта +--- +--- Возвращает, умирает ли персонаж? +--- @param owner Character +--- @param intensity integer +--- @return Task, boolean +function effect:beforeDeath(owner, intensity) return taskUtils.fromValue(), true end + +--- Срабатывает после смерти владельца эффекта +--- @param owner Character +--- @param intensity integer +--- @return Task +function effect:afterDeath(owner, intensity) return taskUtils.fromValue() end + +--- Срабатывает перед ходом владельца эффекта +--- +--- Возвращает, будет ли персонаж ходить? +--- @param owner Character +--- @param intensity integer +--- @return Task, boolean +function effect:beforeTurn(owner, intensity) return taskUtils.fromValue(), true end + +--- Срабатывает после хода владельца эффекта +--- @param owner Character +--- @param intensity integer +--- @return Task +function effect:afterTurn(owner, intensity) return taskUtils.fromValue() end + +--- Срабатывает перед кастом заклинания владельцем эффекта +--- +--- Возвращает, произойдёт ли каст? +--- @param owner Character +--- @param intensity integer +--- @return Task, boolean +function effect:beforeCast(owner, intensity) return taskUtils.fromValue(), true end + +--- Срабатывает после каста заклинания владельцем эффекта +--- @param owner Character +--- @param intensity integer +--- @return Task +function effect:afterCast(owner, intensity) return taskUtils.fromValue() end + +--- Срабатывает перед нанесением урона владельцем эффекта +--- +--- Возвращает урон, который собираются нанести +--- @param owner Character +--- @param intensity integer +--- @param damage integer +--- @return Task, integer +function effect:beforeAttack(owner, intensity, damage) return taskUtils.fromValue(), damage end + +--- Срабатывает после нанесения урона владельцем эффекта +--- @param owner Character +--- @param intensity integer +--- @return Task +function effect:afterAttack(owner, intensity) return taskUtils.fromValue() end + +--- Срабатывает перед получением урона владельцем эффекта +--- +--- Возвращает урон, который должны получить +--- @param owner Character +--- @param intensity integer +--- @param damage integer +--- @return Task, integer +function effect:beforeDamage(owner, intensity, damage) return taskUtils.fromValue(), damage end + +--- Срабатывает после получения урона владельцем эффекта +--- @param owner Character +--- @param intensity integer +--- @return Task +function effect:afterDamage(owner, intensity) return taskUtils.fromValue() end + +--- Срабатывает перед регенерацией здоровья владельцем эффекта +--- +--- Возвращает количество здоровья, которое должно быть восстановлено +--- @param owner Character +--- @param intensity integer +--- @param amountHp integer кол-во хп для регена +--- @return Task, integer +function effect:beforeRegeneration(owner, intensity, amountHp) return taskUtils.fromValue(), amountHp end + +--- Срабатывает после регенерации здоровья владельцем эффекта +--- @param owner Character +--- @param intensity integer +--- @return Task +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 } diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua index 5ef80f0..11e3e45 100644 --- a/lib/spell/spell.lua +++ b/lib/spell/spell.lua @@ -54,33 +54,28 @@ function spell:draw() local path = self.path --[[@as Deque?]] if not path then return end --- Это отрисовка пути персонажа к мышке - Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.textures.overlayLayer) - local i = 0 - path:pop_front() - for p in path:values() do - i = i + 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 - love.graphics.setCanvas() - Tree.level.camera:detach() - love.graphics.setColor(1, 1, 1) + Tree.level.render:enqueue(Tree.level.render.LAYERS.OVERLAY, 0, function() + local i = 0 + path:pop_front() + for p in path:values() do + i = i + 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 + love.graphics.setColor(1, 1, 1) + end) else - Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.textures.overlayLayer) - 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.setCanvas() - Tree.level.camera:detach() - love.graphics.setColor(1, 1, 1) + 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 @@ -128,6 +123,13 @@ function spell.new(data) return end + -- проверка на возможность каста + if not caster:try(Tree.behaviors.effects, function(effects) + return effects:beforeCast() + end) then + return + end + caster:try(Tree.behaviors.stats, function(stats) stats.mana = stats.mana - self.baseCost end) @@ -135,6 +137,8 @@ function spell.new(data) caster:try(Tree.behaviors.spellcaster, function(spellcaster) spellcaster.cooldowns[self.tag] = self.baseCooldown end) + + return data.onCast(caster, target) end diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 52bf978..4c13c0a 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -52,7 +52,8 @@ local regenerateMana = spell.new { end) 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") local light = require "lib/character/character".spawn("Light Effect") @@ -67,7 +68,8 @@ local regenerateMana = spell.new { light:die() return task.fromValue() end), - sprite:animate("hurt") + sprite:animate("hurt"), + effects:addEffect("aversionToDeath", 1, 1), } end } @@ -86,9 +88,10 @@ local attack = spell.new { stats.hp = stats.hp - 4 end) + local targetEffects = targetCharacter:has(Tree.behaviors.effects) local sprite = caster:has(Tree.behaviors.sprite) local targetSprite = targetCharacter:has(Tree.behaviors.sprite) - if not sprite or not targetSprite then return end + if not sprite or not targetSprite or not targetEffects then return 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.positioned.new(targetCharacter:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }), } + Tree.audio:play(Tree.assets.files.audio.sounds.hurt) return task.wait { task.chain(task.tween(light:has(Tree.behaviors.light) --[[@as LightBehavior]], @@ -110,12 +114,11 @@ local attack = spell.new { light:die() return task.fromValue() end), - targetSprite:animate("hurt") + targetSprite:animate("hurt"), + targetEffects:addEffect("bleeding", 3, 3) } end ), - - Tree.audio:play(Tree.assets.files.audio.sounds.hurt) } } end diff --git a/lib/utils/utils.lua b/lib/utils/utils.lua index b5c0362..7920e87 100644 --- a/lib/utils/utils.lua +++ b/lib/utils/utils.lua @@ -74,4 +74,19 @@ function P.lerp(from, to, t) return from + (to - from) * t 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 diff --git a/main.lua b/main.lua index a657dc8..76792bc 100644 --- a/main.lua +++ b/main.lua @@ -15,64 +15,77 @@ function love.load() testLayout = require "lib.simple_ui.level.layout" local chars = { + -- character.spawn("Foodor") + -- :addBehavior { + -- Tree.behaviors.residentsleeper.new(), + -- Tree.behaviors.stats.new(nil, nil, 1), + -- Tree.behaviors.positioned.new(Vec3 { 3, 3 }), + -- Tree.behaviors.tiled.new(), + -- Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), + -- Tree.behaviors.shadowcaster.new(), + -- Tree.behaviors.spellcaster.new(), + -- Tree.behaviors.effects.new() + -- }, character.spawn("Foodor") :addBehavior { Tree.behaviors.residentsleeper.new(), 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.sprite.new(Tree.assets.files.sprites.character), Tree.behaviors.shadowcaster.new(), - Tree.behaviors.spellcaster.new() - }, - character.spawn("Foodor") - :addBehavior { - Tree.behaviors.residentsleeper.new(), - Tree.behaviors.stats.new(nil, nil, 1), - Tree.behaviors.positioned.new(Vec3 { 4, 3 }), - Tree.behaviors.tiled.new(), - Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), - Tree.behaviors.shadowcaster.new(), - Tree.behaviors.spellcaster.new() + Tree.behaviors.spellcaster.new(), + Tree.behaviors.effects.new() }, character.spawn("Foodor") :addBehavior { Tree.behaviors.residentsleeper.new(), Tree.behaviors.stats.new(nil, nil, 3), - Tree.behaviors.positioned.new(Vec3 { 5, 3 }), - Tree.behaviors.tiled.new(), - Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), - Tree.behaviors.shadowcaster.new(), - Tree.behaviors.spellcaster.new() - }, - character.spawn("Baris") - :addBehavior { - Tree.behaviors.residentsleeper.new(), - Tree.behaviors.stats.new(nil, nil, 2), - Tree.behaviors.positioned.new(Vec3 { 5, 5 }), + Tree.behaviors.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(), - Tree.behaviors.ai.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() }, + -- 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 Tree.level.turnOrder:add(id) 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() print("Now playing:", Tree.level.turnOrder.current) @@ -123,9 +136,14 @@ function love.draw() love.graphics.setColor(1, 1, 1) 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: " .. 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) local t2 = love.timer.getTime() @@ -136,5 +154,5 @@ function love.resize(w, h) local render = Tree.level.render if not render then return end render:free() - Tree.level.render = (require "lib.level.render").new { w, h } + Tree.level.render = (require "lib.level.render").new { w = w, h = h } end