diff --git a/assets/masks/circle128.png b/assets/masks/circle128.png new file mode 100644 index 0000000..007a29d Binary files /dev/null and b/assets/masks/circle128.png differ diff --git a/assets/masks/gradientCircle256.png b/assets/masks/gradientCircle256.png new file mode 100644 index 0000000..d93611c Binary files /dev/null and b/assets/masks/gradientCircle256.png differ diff --git a/assets/shaders/blur.glsl b/assets/shaders/blur.glsl new file mode 100644 index 0000000..1ab89e1 --- /dev/null +++ b/assets/shaders/blur.glsl @@ -0,0 +1,18 @@ +extern vec2 direction; // (1.0, 0.0) для X, (0.0, 1.0) для Y +extern number radius; // радиус размытия + +vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords) +{ + vec4 sum = vec4(0.0); + float weightTotal = 0.0; + + for (int i = -10; i <= 10; i++) { + float offset = float(i); + float weight = exp(-offset * offset / (2.0 * radius * radius)); + vec2 shift = direction * offset / love_ScreenSize.xy; + sum += Texel(tex, texture_coords + shift) * weight; + weightTotal += weight; + } + + return sum / weightTotal; +} diff --git a/assets/shaders/light.glsl b/assets/shaders/light.glsl new file mode 100644 index 0000000..3b28fb3 --- /dev/null +++ b/assets/shaders/light.glsl @@ -0,0 +1,30 @@ +extern vec3 color; +extern number time; + +vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords) +{ + vec4 texColor = Texel(tex, texture_coords); + + float mask = texColor.r; + + vec2 uv = texture_coords - 0.5; + float dist = length(uv * 2.0); + + float t = time; + + float wave = sin((uv.x + uv.y) * 6.0 + t * 1.5) * 0.03; + float ripple = sin(length(uv) * 20.0 - t * 2.0) * 0.02; + float flicker = sin(t * 2.5) * 0.02; + + dist += wave + ripple + flicker; + + float intensity = 1.0 - smoothstep(0.0, 1.0, dist); + intensity = pow(intensity, 2.0); + + float colorShift = sin(t * 3.0) * 0.1; + vec3 flickerColor = color + vec3(colorShift, colorShift * 0.5, -colorShift * 0.3); + + vec3 finalColor = flickerColor * intensity * mask; + + return vec4(finalColor, mask * intensity); +} diff --git a/assets/shaders/light_postprocess.glsl b/assets/shaders/light_postprocess.glsl new file mode 100644 index 0000000..7e046f7 --- /dev/null +++ b/assets/shaders/light_postprocess.glsl @@ -0,0 +1,20 @@ +extern Image scene; +extern Image light; + +extern vec3 ambient; + +vec4 effect(vec4 vcolor, Image unused, vec2 uv, vec2 px) +{ + vec4 s = Texel(scene, uv); + vec3 l = Texel(light, uv).rgb; + + l = clamp(l, 0.0, 1.0); + vec3 a = clamp(ambient, 0.0, 1.0); + + // Канальный множитель: от ambient до 1 в зависимости от света + vec3 m = a + (vec3(1.0) - a) * l; + + vec3 rgb = s.rgb * m; + + return vec4(rgb, s.a); +} diff --git a/lib/animation_node.lua b/lib/animation_node.lua index b8309e7..e3f5aff 100644 --- a/lib/animation_node.lua +++ b/lib/animation_node.lua @@ -89,6 +89,7 @@ local function new(data) t.t = 0 t.state = "running" t.finish = function() + if t.state ~= "running" then return end t.state = "waiting" t:bubbleUp() for _, anim in ipairs(t.children) do diff --git a/lib/annotations.lua b/lib/annotations.lua index 9753b5d..6a0d688 100644 --- a/lib/annotations.lua +++ b/lib/annotations.lua @@ -1,6 +1,10 @@ --- @meta _ -Tree.behaviors.map = require "lib.character.behaviors.map" Tree.behaviors.spellcaster = require "lib.character.behaviors.spellcaster" Tree.behaviors.sprite = require "lib.character.behaviors.sprite" Tree.behaviors.stats = require "lib.character.behaviors.stats" Tree.behaviors.residentsleeper = require "lib.character.behaviors.residentsleeper" +Tree.behaviors.shadowcaster = require "lib.character.behaviors.shadowcaster" +Tree.behaviors.light = require "character.behaviors.light" +Tree.behaviors.positioned = require "character.behaviors.positioned" +Tree.behaviors.tiled = require "character.behaviors.tiled" +Tree.behaviors.cursor = require "character.behaviors.cursor" diff --git a/lib/character/behaviors/behavior.lua b/lib/character/behaviors/behavior.lua index 37e6775..0dec6bb 100644 --- a/lib/character/behaviors/behavior.lua +++ b/lib/character/behaviors/behavior.lua @@ -11,6 +11,11 @@ behavior.id = "behavior" function behavior.new() return setmetatable({}, behavior) end +--- это деструктор с крутым названием +function behavior:die() + +end + function behavior:update(dt) end function behavior:draw() end diff --git a/lib/character/behaviors/cursor.lua b/lib/character/behaviors/cursor.lua new file mode 100644 index 0000000..873115e --- /dev/null +++ b/lib/character/behaviors/cursor.lua @@ -0,0 +1,19 @@ +--- Добавляет следование за курсором мыши +--- @class CursorBehavior : Behavior +local behavior = {} +behavior.__index = behavior +behavior.id = "cursor" + +---@return CursorBehavior +function behavior.new() + return setmetatable({}, behavior) +end + +function behavior:update() + self.owner:try(Tree.behaviors.positioned, function(b) + local mx, my = love.mouse.getX(), love.mouse.getY() + b.position = Tree.level.camera:toWorldPosition(Vec3 { mx, my }) + end) +end + +return behavior diff --git a/lib/character/behaviors/light.lua b/lib/character/behaviors/light.lua new file mode 100644 index 0000000..1c629c6 --- /dev/null +++ b/lib/character/behaviors/light.lua @@ -0,0 +1,57 @@ +--- @class LightBehavior : Behavior +--- @field intensity number +--- @field color Vec3 +--- @field seed integer +--- @field colorAnimationNode? AnimationNode +--- @field targetColor? Vec3 +--- @field sourceColor? Vec3 +local behavior = {} +behavior.__index = behavior +behavior.id = "light" + +---@param values {intensity: number?, color: Vec3?, seed: integer?} +---@return LightBehavior +function behavior.new(values) + return setmetatable({ + intensity = values.intensity or 1, + color = values.color or Vec3 { 1, 1, 1 }, + seed = values.seed or math.random(math.pow(2, 16)) + }, behavior) +end + +function behavior:update(dt) + if not self.colorAnimationNode then return end + local delta = self.targetColor - self.sourceColor + self.color = self.sourceColor + delta * self.colorAnimationNode:getValue() + self.colorAnimationNode:update(dt) +end + +function behavior:animateColor(targetColor, animationNode) + if self.colorAnimationNode then self.colorAnimationNode:finish() end + self.colorAnimationNode = animationNode + self.sourceColor = self.color + self.targetColor = targetColor +end + +function behavior:draw() + local positioned = self.owner:has(Tree.behaviors.positioned) + if not positioned then return end + + 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) + + love.graphics.setBlendMode("alpha") + + love.graphics.setShader() + love.graphics.setCanvas() + Tree.level.camera:detach() +end + +return behavior diff --git a/lib/character/behaviors/map.lua b/lib/character/behaviors/map.lua deleted file mode 100644 index d22804d..0000000 --- a/lib/character/behaviors/map.lua +++ /dev/null @@ -1,91 +0,0 @@ -local utils = require "lib.utils.utils" - ---- Отвечает за размещение и перемещение по локации ---- @class MapBehavior : Behavior ---- @field position Vec3 ---- @field runTarget Vec3 точка, в которую в данный момент бежит персонаж ---- @field displayedPosition Vec3 точка, в которой персонаж отображается ---- @field t0 number время начала движения для анимациии ---- @field path Deque путь, по которому сейчас бежит персонаж ---- @field animationNode? AnimationNode AnimationNode, с которым связана анимация перемещения ---- @field size Vec3 -local mapBehavior = {} -mapBehavior.__index = mapBehavior -mapBehavior.id = "map" - - ---- @param position? Vec3 ---- @param size? Vec3 -function mapBehavior.new(position, size) - return setmetatable({ - position = position or Vec3({}), - displayedPosition = position or Vec3({}), - size = size or Vec3({ 1, 1 }), - }, mapBehavior) -end - ---- @param position Vec3 -function mapBehavior:lookAt(position) - self.owner:try(Tree.behaviors.sprite, - function(sprite) - if position.x > self.displayedPosition.x then sprite.side = sprite.RIGHT end - -- (sic!) - if position.x < self.displayedPosition.x then sprite.side = sprite.LEFT end - end - ) -end - ---- @param path Deque ---- @param animationNode AnimationNode -function mapBehavior:followPath(path, animationNode) - if path:is_empty() then return animationNode:finish() end - self.animationNode = animationNode - self.position = self.displayedPosition - self.owner:try(Tree.behaviors.sprite, function(sprite) - sprite:loop("run") - end) - self.path = path; - ---@type Vec3 - local nextCell = path:peek_front() - self:runTo(nextCell) - path:pop_front() -end - ---- @param target Vec3 -function mapBehavior:runTo(target) - self.t0 = love.timer.getTime() - self.runTarget = target - self.owner:try(Tree.behaviors.sprite, - function(sprite) - if target.x < self.position.x then - sprite.side = Tree.behaviors.sprite.LEFT - elseif target.x > self.position.x then - sprite.side = Tree.behaviors.sprite.RIGHT - end - end - ) -end - -function mapBehavior:update(dt) - if self.runTarget then - local delta = love.timer.getTime() - self.t0 or love.timer.getTime() - local fraction = delta / - (0.5 * self.runTarget:subtract(self.position):length()) -- бежим одну клетку за 500 мс, по диагонали больше - if fraction >= 1 then -- анимация перемещена завершена - self.position = self.runTarget - if not self.path:is_empty() then -- еще есть, куда бежать - self:runTo(self.path:pop_front()) - else -- мы добежали до финальной цели - self.owner:try(Tree.behaviors.sprite, function(sprite) - sprite:loop("idle") - end) - self.runTarget = nil - if self.animationNode then self.animationNode:finish() end - end - else -- анимация перемещения не завершена - self.displayedPosition = utils.lerp(self.position, self.runTarget, fraction) -- линейный интерполятор - end - end -end - -return mapBehavior diff --git a/lib/character/behaviors/positioned.lua b/lib/character/behaviors/positioned.lua new file mode 100644 index 0000000..a48cc01 --- /dev/null +++ b/lib/character/behaviors/positioned.lua @@ -0,0 +1,25 @@ +--- Отвечает за размещение на уровне +--- @class PositionedBehavior : Behavior +--- @field position Vec3 +local behavior = {} +behavior.__index = behavior +behavior.id = "positioned" + +--- @param position? Vec3 +function behavior.new(position) + return setmetatable({ + position = position or Vec3({}), + }, behavior) +end + +--- @param position Vec3 +function behavior:lookAt(position) + self.owner:try(Tree.behaviors.sprite, + function(sprite) + if position.x > self.position.x then sprite.side = sprite.RIGHT end + if position.x < self.position.x then sprite.side = sprite.LEFT end + end + ) +end + +return behavior diff --git a/lib/character/behaviors/residentsleeper.lua b/lib/character/behaviors/residentsleeper.lua index 6c5af5f..c46b701 100644 --- a/lib/character/behaviors/residentsleeper.lua +++ b/lib/character/behaviors/residentsleeper.lua @@ -1,7 +1,6 @@ --- Умеет асинхронно ждать какое-то время (для анимаций) --- @class ResidentSleeperBehavior : Behavior --- @field animationNode? AnimationNode ---- @field endsAt? number local behavior = {} behavior.__index = behavior behavior.id = "residentsleeper" @@ -10,19 +9,13 @@ function behavior.new() return setmetatable({}, behavior) end function behavior:update(dt) if not self.animationNode then return end - if love.timer.getTime() >= self.endsAt then - self.animationNode:finish() - self.animationNode = nil - self.endsAt = nil - end + self.animationNode:update(dt) end ---- @param ms number time to wait in milliseconds --- @param node AnimationNode -function behavior:sleep(ms, node) - if self.animationNode then node:finish() end +function behavior:sleep(node) + if self.animationNode then self.animationNode:finish() end self.animationNode = node - self.endsAt = love.timer.getTime() + ms / 1000 end return behavior diff --git a/lib/character/behaviors/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua new file mode 100644 index 0000000..3407143 --- /dev/null +++ b/lib/character/behaviors/shadowcaster.lua @@ -0,0 +1,63 @@ +local easing = require "lib.utils.easing" +--- @class ShadowcasterBehavior : Behavior +local behavior = {} +behavior.id = "shadowcaster" +behavior.__index = behavior + +function behavior.new() return setmetatable({}, behavior) end + +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 + + local ppm = Tree.level.camera.pixelsPerMeter + local position = positioned.position + Vec3 { 0.5, 0.5 } + + local lightIds = Tree.level.lightGrid:query(position, 5) + --- @type Character[] + local lights = {} + for _, id in ipairs(lightIds) do + 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, 0.2, 0.2 * math.cos(math.pi / 4)) + love.graphics.pop() + + 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 + +return behavior diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 0922c4d..33f076b 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -41,10 +41,15 @@ end function sprite:draw() if not self.animationTable[self.state] or not Tree.assets.files.sprites.character[self.state] then return end - self.owner:try(Tree.behaviors.map, - function(map) + self.owner:try(Tree.behaviors.positioned, + function(pos) local ppm = Tree.level.camera.pixelsPerMeter - local position = map.displayedPosition + local position = pos.position + Vec3 { 0.5, 0.5 } + + love.graphics.setCanvas(Tree.level.render.textures.spriteLayer) + Tree.level.camera:attach() + + 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(), Tree.assets.files.sprites.character[self.state]:getHeight() @@ -53,12 +58,13 @@ function sprite:draw() shader:send("time", love.timer:getTime()) love.graphics.setShader(shader) end - self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state], - position.x + 0.5, - position.y + 0.5, nil, 1 / ppm * self.side, 1 / ppm, 38, 47) - love.graphics.setColor(1, 1, 1) + position.x, + position.y, nil, 1 / ppm * self.side, 1 / ppm, 38, 47) + love.graphics.setShader() + Tree.level.camera:detach() + love.graphics.setCanvas() end ) end diff --git a/lib/character/behaviors/tiled.lua b/lib/character/behaviors/tiled.lua new file mode 100644 index 0000000..3c8fbc5 --- /dev/null +++ b/lib/character/behaviors/tiled.lua @@ -0,0 +1,83 @@ +local utils = require "lib.utils.utils" + +--- Отвечает за перемещение по тайлам +--- @class TiledBehavior : Behavior +--- @field private runSource? Vec3 точка, из которой бежит персонаж +--- @field private runTarget? Vec3 точка, в которую в данный момент бежит персонаж +--- @field private path? Deque путь, по которому сейчас бежит персонаж +--- @field private animationNode? AnimationNode AnimationNode, с которым связана анимация перемещения +--- @field private t0 number время начала движения +--- @field size Vec3 +local behavior = {} +behavior.__index = behavior +behavior.id = "tiled" + +--- @param size? Vec3 +function behavior.new(size) + return setmetatable({ + size = size or Vec3({ 1, 1 }), + }, behavior) +end + +--- @param path Deque +--- @param animationNode AnimationNode +function behavior:followPath(path, animationNode) + if path:is_empty() then return animationNode:finish() end + self.animationNode = animationNode + self.owner:try(Tree.behaviors.sprite, function(sprite) + sprite:loop("run") + end) + self.path = path; + ---@type Vec3 + local nextCell = path:peek_front() + self:runTo(nextCell) + path:pop_front() +end + +--- @param target Vec3 +function behavior:runTo(target) + local positioned = self.owner:has(Tree.behaviors.positioned) + if not positioned then return end + + self.t0 = love.timer.getTime() + self.runTarget = target + + self.runSource = positioned.position + + self.owner:try(Tree.behaviors.sprite, + function(sprite) + if target.x < positioned.position.x then + sprite.side = Tree.behaviors.sprite.LEFT + elseif target.x > positioned.position.x then + sprite.side = Tree.behaviors.sprite.RIGHT + end + end + ) +end + +function behavior:update(dt) + if self.runTarget then + local positioned = self.owner:has(Tree.behaviors.positioned) + if not positioned then return end + + local delta = love.timer.getTime() - self.t0 or love.timer.getTime() + local fraction = delta / + (0.5 * self.runTarget:subtract(self.runSource):length()) -- бежим одну клетку за 500 мс, по диагонали больше + if fraction >= 1 then -- анимация перемещена завершена + positioned.position = self.runTarget + if not self.path:is_empty() then -- еще есть, куда бежать + self:runTo(self.path:pop_front()) + else -- мы добежали до финальной цели + self.owner:try(Tree.behaviors.sprite, function(sprite) + sprite:loop("idle") + end) + self.runTarget = nil + if self.animationNode then self.animationNode:finish() end + end + else -- анимация перемещения не завершена + positioned.position = utils.lerp(self.runSource, self.runTarget, fraction) -- линейный интерполятор + end + end +end + +return behavior diff --git a/lib/character/character.lua b/lib/character/character.lua index bde0c2c..b9d9a50 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -13,11 +13,7 @@ character.__index = character --- Создаёт персонажа, которым будет управлять или игрок или компьютер --- @param name string ---- @param spriteDir table ---- @param position? Vec3 ---- @param size? Vec3 ---- @param initiative? integer -local function spawn(name, spriteDir, position, size, initiative) +local function spawn(name) local char = {} char = setmetatable(char, character) @@ -26,14 +22,6 @@ local function spawn(name, spriteDir, position, size, initiative) char.behaviors = {} char._behaviorsIdx = {} - char:addBehavior { - Tree.behaviors.residentsleeper.new(), - Tree.behaviors.stats.new(nil, nil, initiative), - Tree.behaviors.map.new(position, size), - Tree.behaviors.sprite.new(spriteDir), - Tree.behaviors.spellcaster.new() - } - Tree.level.characters[char.id] = char return char end @@ -92,6 +80,18 @@ function character:addBehavior(behaviors) return self end +--- Добавляет персонажа в очередь на удаление. +--- В конце фрейма он умирает. Ужасной смертью. +--- +--- Ещё этот метод должен освобождать ресурсы в поведениях. Мы против утечек памяти! +function character:die() + for _, b in ipairs(self.behaviors) do + if b.die then b:die() end + end + + table.insert(Tree.level.deadIds, self.id) +end + function character:update(dt) for _, b in ipairs(self.behaviors) do if b.update then b:update(dt) end diff --git a/lib/level/grid/character_grid.lua b/lib/level/grid/character_grid.lua index b21772b..292f475 100644 --- a/lib/level/grid/character_grid.lua +++ b/lib/level/grid/character_grid.lua @@ -12,12 +12,15 @@ function grid:add(id) local character = Tree.level.characters[id] if not character then return end - local mapB = character:has(Tree.behaviors.map) - if not mapB then return end + local positioned = character:has(Tree.behaviors.positioned) + if not positioned then return end - local centerX, centerY = math.floor(mapB.displayedPosition.x + 0.5), - math.floor(mapB.displayedPosition.y + 0.5) - local sizeX, sizeY = mapB.size.x, mapB.size.y + local tiled = character:has(Tree.behaviors.tiled) + if not tiled then return end + + local centerX, centerY = math.floor(positioned.position.x + 0.5), + math.floor(positioned.position.y + 0.5) + local sizeX, sizeY = tiled.size.x, tiled.size.y for y = centerY, centerY + sizeY - 1 do for x = centerX, centerX + sizeX - 1 do @@ -29,10 +32,8 @@ end --- @param a Character --- @param b Character local function drawCmp(a, b) - -- здесь персонажи гарантированно имеют нужное поведение - return a:has(Tree.behaviors.map).displayedPosition.y - < - b:has(Tree.behaviors.map).displayedPosition.y + --- @TODO: это захардкожено, надо разделить поведения + return a:has(Tree.behaviors.positioned).position.y < b:has(Tree.behaviors.positioned).position.y end --- fills the grid with the actual data diff --git a/lib/level/grid/light_grid.lua b/lib/level/grid/light_grid.lua new file mode 100644 index 0000000..9b7042b --- /dev/null +++ b/lib/level/grid/light_grid.lua @@ -0,0 +1,65 @@ +local utils = require "lib.utils.utils" +--- Пометровая сетка источников света, чтобы быстро искать ближайшие для некоторого объекта +--- @class LightGrid : Grid +--- @field __grid {string: [Id]} +local grid = setmetatable({}, require "lib.level.grid.base") +grid.__index = grid + +--- Adds a character id to the grid +--- @private +--- @param id Id +function grid:add(id) + local character = Tree.level.characters[id] + if not character then return end + + local lightB = character:has(Tree.behaviors.light) + if not lightB then return end + + local positioned = character:has(Tree.behaviors.positioned) + if not positioned then return end + + local key = tostring(Vec3 { positioned.position.x, positioned.position.y }:floor()) + if not self.__grid[key] then self.__grid[key] = {} end + table.insert(self.__grid[key], character.id) +end + +--- fills the grid with the actual data +--- +--- should be called as early as possible during every tick +function grid:reload() + self:reset() + utils.each(Tree.level.characters, function(c) + self:add(c.id) + end) +end + +--- Возвращает все источники света, которые находятся в пределах круга с диаметром [distance] в [метрике Чебышёва](https://ru.wikipedia.org/wiki/Расстояние_Чебышёва) +--- @param position Vec3 +--- @param distance integer +function grid:query(position, distance) + --- @type Id[] + local res = {} + local topLeft = position:subtract(Vec3 { distance / 2, distance / 2 }):floor() + for i = 0, distance, 1 do + for j = 0, distance, 1 do + --- @type Id[]? + local lights = self:get(topLeft:add(Vec3 { i, j })) + if lights then + for _, lightChar in ipairs(lights) do + table.insert(res, lightChar) + end + end + end + end + return res +end + +--- Generates an empty grid +--- @return LightGrid +local function new() + return setmetatable({ + __grid = {} + }, grid) +end + +return { new = new } diff --git a/lib/level/grid/tile_grid.lua b/lib/level/grid/tile_grid.lua index 776b2e0..1a89a10 100644 --- a/lib/level/grid/tile_grid.lua +++ b/lib/level/grid/tile_grid.lua @@ -13,9 +13,13 @@ local function new(type, template, size) end function map:draw() + love.graphics.setCanvas(Tree.level.render.textures.floorLayer) + Tree.level.camera:attach() utils.each(self.__grid, function(el) el:draw() 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 16991e9..17ddb34 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -3,16 +3,17 @@ local utils = require "lib.utils.utils" --- @class Level --- @field size Vec3 --- @field characters Character[] +--- @field deadIds Id[] --- @field characterGrid CharacterGrid +--- @field lightGrid LightGrid --- @field selector Selector --- @field camera Camera --- @field tileGrid TileGrid --- @field turnOrder TurnOrder +--- @field render Render local level = {} level.__index = level -local path = nil - --- @param type "procedural"|"handmaded" --- @param template Procedural|Handmaded local function new(type, template) @@ -21,16 +22,26 @@ local function new(type, template) return setmetatable({ size = size, characters = {}, + deadIds = {}, characterGrid = (require "lib.level.grid.character_grid").new(), + lightGrid = (require "lib.level.grid.light_grid").new(), tileGrid = (require "lib.level.grid.tile_grid").new(type, template, size), selector = (require "lib.level.selector").new(), camera = (require "lib.level.camera").new(), turnOrder = (require "lib.level.turn_order").new(), + render = (require "lib.level.render").new {}, + weather = (require "lib.level.weather").new { ambientLight = Vec3 { 0.36, 0.42, 0.6 }, skyLight = Vec3 {} } }, level) end function level:update(dt) + utils.each(self.deadIds, function(id) + self.characters[id] = nil + end) + self.deadIds = {} + self.characterGrid:reload() + self.lightGrid:reload() utils.each(self.characters, function(el) el:update(dt) end) @@ -39,10 +50,13 @@ function level:update(dt) 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 + + self.render:draw() end return { diff --git a/lib/level/render.lua b/lib/level/render.lua new file mode 100644 index 0000000..5888888 --- /dev/null +++ b/lib/level/render.lua @@ -0,0 +1,106 @@ +--- @class Render +--- @field textures table +local render = { + textures = {} +} + +function render:clear() + local weather = Tree.level.weather + local txs = self.textures + 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) + love.graphics.clear(weather.skyLight.x, weather.skyLight.y, weather.skyLight.z) + love.graphics.setCanvas(txs.overlayLayer) + love.graphics.clear() +end + +function render:free() + for _, tx in pairs(self.textures) do + tx:release() + end + self.textures = nil +end + +--- TODO: это используется для блюра, должно кэшироваться и поддерживать ресайз + +function render:applyBlur(input, radius) + local blurShader = Tree.assets.files.shaders.blur + + -- Горизонтальный проход + blurShader:send("direction", { 1.0, 0.0 }) + blurShader:send("radius", radius) + + self.textures.tmp1:renderTo(function() + love.graphics.clear() + love.graphics.setShader(blurShader) + love.graphics.draw(input) + love.graphics.setShader() + end) + + -- Вертикальный проход + self.textures.tmp2:renderTo( + function() + love.graphics.clear() + love.graphics.setShader(blurShader) + blurShader:send("direction", { 0.0, 1.0 }) + love.graphics.draw(self.textures.tmp1) + love.graphics.setShader() + end + ) + return self.textures.tmp2 +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?} +---@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/weather.lua b/lib/level/weather.lua new file mode 100644 index 0000000..13f04d0 --- /dev/null +++ b/lib/level/weather.lua @@ -0,0 +1,12 @@ +--- @class Weather +--- @field skyLight Vec3 +--- @field ambientLight Vec3 +local weather = {} + +--- @param proto Weather +--- @return Weather +local function new(proto) + return setmetatable(proto, { __index = weather }) +end + +return { new = new } diff --git a/lib/simple_ui/level/end_turn.lua b/lib/simple_ui/level/end_turn.lua index e1cb679..8e36e29 100644 --- a/lib/simple_ui/level/end_turn.lua +++ b/lib/simple_ui/level/end_turn.lua @@ -49,11 +49,11 @@ function endTurnButton:onClick() Tree.level.selector:select(nil) local cid = Tree.level.turnOrder.current local playing = Tree.level.characters[cid] - if not playing:has(Tree.behaviors.map) then return end + if not playing:has(Tree.behaviors.positioned) then return end AnimationNode { function(node) - Tree.level.camera:animateTo(playing:has(Tree.behaviors.map).displayedPosition, node) + Tree.level.camera:animateTo(playing:has(Tree.behaviors.positioned).position, node) end, duration = 1500, easing = easing.easeInOutCubic, diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 7f22b1e..51b0fa6 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -8,15 +8,16 @@ --- Да, это Future/Promise/await/async local AnimationNode = require "lib.animation_node" +local easing = require "lib.utils.easing" --- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell --- @field tag string --- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла --- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире --- @field cast fun(self: Spell, caster: Character, target: Vec3): boolean Вызывается в момент каста, изменяет мир. Возвращает bool в зависимости от того, получилось ли скастовать -local spell = {} -spell.__index = spell -spell.tag = "base" +local spell = {} +spell.__index = spell +spell.tag = "base" function spell:update(caster, dt) end @@ -51,7 +52,7 @@ function walk:cast(caster, target) local sprite = caster:has(Tree.behaviors.sprite) if not sprite then return true end AnimationNode { - function(node) caster:has(Tree.behaviors.map):followPath(path, node) end, + function(node) caster:has(Tree.behaviors.tiled):followPath(path, node) end, onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end, }:run() @@ -59,7 +60,7 @@ function walk:cast(caster, target) end function walk:update(caster, dt) - local charPos = caster:has(Tree.behaviors.map).position:floor() + local charPos = caster:has(Tree.behaviors.positioned).position:floor() --- @type Vec3 local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor() self.path = require "lib.pathfinder" (charPos, mpos) @@ -68,10 +69,14 @@ end function walk:draw() if not self.path then return end --- Это отрисовка пути персонажа к мышке + Tree.level.camera:attach() + love.graphics.setCanvas(Tree.level.render.textures.overlayLayer) love.graphics.setColor(0.6, 0.75, 0.5) for p in self.path:values() do love.graphics.circle("fill", p.x + 0.45, p.y + 0.45, 0.1) end + love.graphics.setCanvas() + Tree.level.camera:detach() love.graphics.setColor(1, 1, 1) end @@ -86,6 +91,13 @@ function regenerateMana:cast(caster, target) print(caster.id, "has regenerated mana and gained initiative") local sprite = caster:has(Tree.behaviors.sprite) if not sprite then return true end + + local light = require "lib/character/character".spawn("Light Effect") + light:addBehavior { + Tree.behaviors.light.new { color = Vec3 { 0.6, 0.3, 0.3 }, intensity = 4 }, + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }), + } AnimationNode { function(node) sprite:animate("hurt", node) @@ -93,6 +105,15 @@ function regenerateMana:cast(caster, target) onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end }:run() + AnimationNode { + function(node) + light:has(Tree.behaviors.light):animateColor(Vec3 {}, node) + end, + easing = easing.easeInQuad, + duration = 800, + onEnd = function() light:die() end + }:run() + return true end @@ -100,8 +121,8 @@ local attack = setmetatable({}, spell) attack.tag = "dev_attack" function attack:cast(caster, target) - if caster:try(Tree.behaviors.map, function(map) - local dist = math.max(math.abs(map.position.x - target.x), math.abs(map.position.y - target.y)) + if caster:try(Tree.behaviors.positioned, function(p) + local dist = math.max(math.abs(p.position.x - target.x), math.abs(p.position.y - target.y)) print("dist:", dist) return dist > 2 end) then @@ -124,7 +145,7 @@ function attack:cast(caster, target) local targetSprite = targetCharacter:has(Tree.behaviors.sprite) if not sprite or not targetSprite then return true end - caster:try(Tree.behaviors.map, function(map) map:lookAt(target) end) + caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) end) AnimationNode { onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end, @@ -136,8 +157,9 @@ function attack:cast(caster, target) }, AnimationNode { function(node) - targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200, node) + targetCharacter:has(Tree.behaviors.residentsleeper):sleep(node) end, + duration = 200, children = { AnimationNode { function(node) diff --git a/main.lua b/main.lua index bd2a7a8..26d002d 100644 --- a/main.lua +++ b/main.lua @@ -1,24 +1,47 @@ -- CameraLoader = require 'lib/camera' local character = require "lib/character/character" -require "lib/tree" -local testLayout = require "lib.simple_ui.level.layout" +local testLayout function love.conf(t) t.console = true end function love.load() - character.spawn("Foodor", Tree.assets.files.sprites.character, nil, nil, 1) - character.spawn("Baris", Tree.assets.files.sprites.character, Vec3 { 3, 3 }, nil, 2) - character.spawn("Foodor Jr", Tree.assets.files.sprites.character, Vec3 { 0, 3 }, nil, 3) - character.spawn("Baris Jr", Tree.assets.files.sprites.character, Vec3 { 0, 6 }, nil, 4) - for id, _ in pairs(Tree.level.characters) do + love.window.setMode(1280, 720, { resizable = true, msaa = 4, vsync = true }) + require "lib/tree" -- важно это сделать после настройки окна + 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() + }, + character.spawn("Baris") + :addBehavior { + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.stats.new(nil, nil, 1), + 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() + }, + } + + for id, _ in pairs(chars) do Tree.level.turnOrder:add(id) end + + Tree.level.turnOrder:endRound() print("Now playing:", Tree.level.turnOrder.current) - love.window.setMode(1280, 720, { resizable = true, msaa = 4, vsync = true }) end local lt = "0" @@ -56,11 +79,8 @@ function love.draw() love.graphics.draw(Tree.assets.files.cats, 0, 0) love.graphics.pop() - Tree.level.camera:attach() - Tree.level:draw() - Tree.level.camera:detach() testLayout:draw() love.graphics.setColor(1, 1, 1) @@ -73,3 +93,10 @@ function love.draw() local t2 = love.timer.getTime() dt = string.format("%.3f", (t2 - t1) * 1000) end + +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 } +end