From b259aa3839fa6795a995ae833017f7f6911712bc Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sat, 27 Dec 2025 03:35:56 +0300 Subject: [PATCH 01/14] introduce basic offscreen render stack --- lib/character/behaviors/shadowcaster.lua | 0 lib/character/behaviors/sprite.lua | 33 +++++++++++++++++--- lib/level/grid/tile_grid.lua | 2 ++ lib/level/level.lua | 7 +++++ lib/level/render.lua | 39 ++++++++++++++++++++++++ lib/spellbook.lua | 2 ++ main.lua | 3 -- 7 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 lib/character/behaviors/shadowcaster.lua create mode 100644 lib/level/render.lua diff --git a/lib/character/behaviors/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua new file mode 100644 index 0000000..e69de29 diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 0922c4d..61d409b 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -38,13 +38,24 @@ function sprite:update(dt) anim:update(dt) end +local function makeGradientMesh(w, h, topColor, bottomColor) + local vertices = { + { 0, 0, 0, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- левый верх + { w, 0, 1, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- правый верх + { w + w * 0.1, h, 1, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- правый низ + { 0 - w * 0.1, h, 0, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- левый низ + } + local mesh = love.graphics.newMesh(vertices, "fan", "static") + return mesh +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) local ppm = Tree.level.camera.pixelsPerMeter - local position = map.displayedPosition + local position = map.displayedPosition + Vec3 { 0.5, 0.5 } 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() @@ -54,11 +65,25 @@ function sprite:draw() 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.setCanvas(Tree.level.render.shadowLayer) + love.graphics.setColor(0, 0, 0, 0.5) + love.graphics.ellipse("fill", position.x, position.y, 0.2, 0.2 * math.cos(math.pi / 4)) + local mesh = makeGradientMesh(0.4, 1.5, { 0, 0, 0, 0.5 }, { 0, 0, 0, 0 }) + love.graphics.push() + love.graphics.translate(position.x, position.y) + love.graphics.rotate(-3 * math.pi / 4) + love.graphics.translate(-0.2, 0) + love.graphics.draw(mesh) + love.graphics.pop() + + love.graphics.setCanvas(Tree.level.render.spriteLayer) love.graphics.setColor(1, 1, 1) + self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state], + position.x, + position.y, nil, 1 / ppm * self.side, 1 / ppm, 38, 47) love.graphics.setShader() + love.graphics.setCanvas() end ) end diff --git a/lib/level/grid/tile_grid.lua b/lib/level/grid/tile_grid.lua index 776b2e0..2625b5b 100644 --- a/lib/level/grid/tile_grid.lua +++ b/lib/level/grid/tile_grid.lua @@ -13,9 +13,11 @@ local function new(type, template, size) end function map:draw() + love.graphics.setCanvas(Tree.level.render.floorLayer) utils.each(self.__grid, function(el) el:draw() end) + love.graphics.setCanvas() end return { new = new } diff --git a/lib/level/level.lua b/lib/level/level.lua index 16991e9..4d79815 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -8,6 +8,7 @@ local utils = require "lib.utils.utils" --- @field camera Camera --- @field tileGrid TileGrid --- @field turnOrder TurnOrder +--- @field render Render local level = {} level.__index = level @@ -26,6 +27,7 @@ local function new(type, template) 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() }, level) end @@ -39,10 +41,15 @@ function level:update(dt) end function level:draw() + self.camera:attach() + self.render:clear() self.tileGrid:draw() while not self.characterGrid.yOrderQueue:is_empty() do -- по сути это сортировка кучей за n log n self.characterGrid.yOrderQueue:pop():draw() end + self.camera:detach() + + self.render:draw() end return { diff --git a/lib/level/render.lua b/lib/level/render.lua new file mode 100644 index 0000000..78d8299 --- /dev/null +++ b/lib/level/render.lua @@ -0,0 +1,39 @@ +--- @class Render +--- @field shadowLayer love.Canvas +--- @field spriteLayer love.Canvas +--- @field floorLayer love.Canvas +--- @field overlayLayer love.Canvas +local render = {} +render.__index = render + +function render:clear() + love.graphics.setCanvas(self.shadowLayer) + love.graphics.clear(1, 1, 1, 0) + love.graphics.setCanvas(self.spriteLayer) + love.graphics.clear() + love.graphics.setCanvas(self.floorLayer) + love.graphics.clear() + love.graphics.setCanvas(self.overlayLayer) + love.graphics.clear() +end + +function render:draw() + -- пол -> тени -> спрайты -> оверлей + love.graphics.draw(self.floorLayer) + love.graphics.setBlendMode("multiply", "premultiplied") + love.graphics.draw(self.shadowLayer) + love.graphics.setBlendMode("alpha") + love.graphics.draw(self.spriteLayer) + love.graphics.draw(self.overlayLayer) +end + +local function new() + return setmetatable({ + shadowLayer = love.graphics.newCanvas(1280, 720), + spriteLayer = love.graphics.newCanvas(1280, 720), + floorLayer = love.graphics.newCanvas(1280, 720), + overlayLayer = love.graphics.newCanvas(1280, 720), + }, render) +end + +return { new = new } diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 7f22b1e..7130f25 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -68,10 +68,12 @@ end function walk:draw() if not self.path then return end --- Это отрисовка пути персонажа к мышке + love.graphics.setCanvas(Tree.level.render.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() love.graphics.setColor(1, 1, 1) end diff --git a/main.lua b/main.lua index bd2a7a8..e77cdba 100644 --- a/main.lua +++ b/main.lua @@ -56,11 +56,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) -- 2.47.2 From 0c1332afb3844217b6c9bf887a06bced96c03518 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sat, 27 Dec 2025 04:37:32 +0300 Subject: [PATCH 02/14] add simple cone shadows (blurred) --- assets/shaders/blur.glsl | 18 +++++++++++++++++ lib/character/behaviors/sprite.lua | 2 +- lib/level/render.lua | 32 ++++++++++++++++++++++++++---- 3 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 assets/shaders/blur.glsl 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/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 61d409b..5e4c0bf 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -72,7 +72,7 @@ function sprite:draw() local mesh = makeGradientMesh(0.4, 1.5, { 0, 0, 0, 0.5 }, { 0, 0, 0, 0 }) love.graphics.push() love.graphics.translate(position.x, position.y) - love.graphics.rotate(-3 * math.pi / 4) + love.graphics.rotate(love.timer.getTime()) love.graphics.translate(-0.2, 0) love.graphics.draw(mesh) love.graphics.pop() diff --git a/lib/level/render.lua b/lib/level/render.lua index 78d8299..826a36f 100644 --- a/lib/level/render.lua +++ b/lib/level/render.lua @@ -8,7 +8,7 @@ render.__index = render function render:clear() love.graphics.setCanvas(self.shadowLayer) - love.graphics.clear(1, 1, 1, 0) + love.graphics.clear() love.graphics.setCanvas(self.spriteLayer) love.graphics.clear() love.graphics.setCanvas(self.floorLayer) @@ -17,13 +17,37 @@ function render:clear() love.graphics.clear() end +local function applyBlur(input, radius) + local blurShader = Tree.assets.files.shaders.blur + local tmp = love.graphics.newCanvas(1280, 720) + + -- Горизонтальный проход + blurShader:send("direction", { 1.0, 0.0 }) + blurShader:send("radius", radius) + + tmp:renderTo(function() + love.graphics.setShader(blurShader) + love.graphics.draw(input) + love.graphics.setShader() + end) + + -- Вертикальный проход + love.graphics.setShader(blurShader) + blurShader:send("direction", { 0.0, 1.0 }) + love.graphics.draw(tmp) + love.graphics.setShader() +end + function render:draw() -- пол -> тени -> спрайты -> оверлей love.graphics.draw(self.floorLayer) - love.graphics.setBlendMode("multiply", "premultiplied") - love.graphics.draw(self.shadowLayer) - love.graphics.setBlendMode("alpha") + + applyBlur(self.shadowLayer, 10) + -- love.graphics.draw(self.shadowLayer) + love.graphics.setShader() + love.graphics.draw(self.spriteLayer) + love.graphics.draw(self.overlayLayer) end -- 2.47.2 From 24cda32e81c41ae6195398b687813211faa5b86f Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 28 Dec 2025 05:16:01 +0300 Subject: [PATCH 03/14] temporary enshittification of the rendering --- lib/character/behaviors/sprite.lua | 36 +++++++++++++++++------------- lib/level/level.lua | 2 ++ lib/level/render.lua | 30 +++++++++++++++++-------- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 5e4c0bf..0cfe73c 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -56,6 +56,27 @@ function sprite:draw() function(map) local ppm = Tree.level.camera.pixelsPerMeter local position = map.displayedPosition + Vec3 { 0.5, 0.5 } + + Tree.level.camera:detach() + + love.graphics.push() + love.graphics.scale(ppm) + love.graphics.setCanvas(Tree.level.render.shadowLayer) + love.graphics.setColor(0, 0, 0, 0.5) + love.graphics.translate(position.x, position.y) + love.graphics.ellipse("fill", 0, 0, 0.2, 0.2 * math.cos(math.pi / 4)) + + love.graphics.rotate(math.pi / 4) + + self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state], + 0, + 0, nil, 1 / ppm * self.side, 1.2 / ppm, 38, 47) + love.graphics.pop() + + Tree.level.camera:attach() + + love.graphics.setCanvas(Tree.level.render.spriteLayer) + 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() @@ -64,21 +85,6 @@ function sprite:draw() shader:send("time", love.timer:getTime()) love.graphics.setShader(shader) end - - - love.graphics.setCanvas(Tree.level.render.shadowLayer) - love.graphics.setColor(0, 0, 0, 0.5) - love.graphics.ellipse("fill", position.x, position.y, 0.2, 0.2 * math.cos(math.pi / 4)) - local mesh = makeGradientMesh(0.4, 1.5, { 0, 0, 0, 0.5 }, { 0, 0, 0, 0 }) - love.graphics.push() - love.graphics.translate(position.x, position.y) - love.graphics.rotate(love.timer.getTime()) - love.graphics.translate(-0.2, 0) - love.graphics.draw(mesh) - love.graphics.pop() - - love.graphics.setCanvas(Tree.level.render.spriteLayer) - love.graphics.setColor(1, 1, 1) self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state], position.x, position.y, nil, 1 / ppm * self.side, 1 / ppm, 38, 47) diff --git a/lib/level/level.lua b/lib/level/level.lua index 4d79815..2f92129 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -48,6 +48,8 @@ function level:draw() self.characterGrid.yOrderQueue:pop():draw() end self.camera:detach() + --self.render.shadowLayer:newImageData():encode("png", "shadowLayer.png") + -- os.exit(0) self.render:draw() end diff --git a/lib/level/render.lua b/lib/level/render.lua index 826a36f..3259e48 100644 --- a/lib/level/render.lua +++ b/lib/level/render.lua @@ -19,33 +19,45 @@ end local function applyBlur(input, radius) local blurShader = Tree.assets.files.shaders.blur - local tmp = love.graphics.newCanvas(1280, 720) + local tmp1 = love.graphics.newCanvas(1280, 720) + local tmp2 = love.graphics.newCanvas(1280, 720) -- Горизонтальный проход blurShader:send("direction", { 1.0, 0.0 }) blurShader:send("radius", radius) - tmp:renderTo(function() + tmp1:renderTo(function() love.graphics.setShader(blurShader) love.graphics.draw(input) love.graphics.setShader() end) -- Вертикальный проход - love.graphics.setShader(blurShader) - blurShader:send("direction", { 0.0, 1.0 }) - love.graphics.draw(tmp) - love.graphics.setShader() + tmp2:renderTo( + function() + love.graphics.setShader(blurShader) + blurShader:send("direction", { 0.0, 1.0 }) + love.graphics.draw(tmp1) + love.graphics.setShader() + end + ) + return tmp2 end function render:draw() -- пол -> тени -> спрайты -> оверлей love.graphics.draw(self.floorLayer) + love.graphics.push() - applyBlur(self.shadowLayer, 10) - -- love.graphics.draw(self.shadowLayer) - love.graphics.setShader() + local blurred = applyBlur(self.shadowLayer, 2) + local wc, hc = love.graphics.getWidth() / 2, love.graphics.getHeight() / 2 + love.graphics.translate(wc, hc) + love.graphics.scale(Tree.level.camera.scale, Tree.level.camera.scale) + love.graphics.translate(-Tree.level.camera.position.x * Tree.level.camera.pixelsPerMeter, + -Tree.level.camera.position.y * Tree.level.camera.pixelsPerMeter) + love.graphics.draw(blurred) + love.graphics.pop() love.graphics.draw(self.spriteLayer) love.graphics.draw(self.overlayLayer) -- 2.47.2 From 198c322ec4501e4ab28d798589fc66f01895a87e Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 28 Dec 2025 05:21:07 +0300 Subject: [PATCH 04/14] performance update --- lib/level/render.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/level/render.lua b/lib/level/render.lua index 3259e48..dd4425f 100644 --- a/lib/level/render.lua +++ b/lib/level/render.lua @@ -17,16 +17,18 @@ function render:clear() love.graphics.clear() end +--- TODO: это используется для блюра, должно кэшироваться и поддерживать ресайз +local tmp1 = love.graphics.newCanvas(1280, 720) +local tmp2 = love.graphics.newCanvas(1280, 720) local function applyBlur(input, radius) local blurShader = Tree.assets.files.shaders.blur - local tmp1 = love.graphics.newCanvas(1280, 720) - local tmp2 = love.graphics.newCanvas(1280, 720) -- Горизонтальный проход blurShader:send("direction", { 1.0, 0.0 }) blurShader:send("radius", radius) tmp1:renderTo(function() + love.graphics.clear() love.graphics.setShader(blurShader) love.graphics.draw(input) love.graphics.setShader() @@ -35,6 +37,7 @@ local function applyBlur(input, radius) -- Вертикальный проход tmp2:renderTo( function() + love.graphics.clear() love.graphics.setShader(blurShader) blurShader:send("direction", { 0.0, 1.0 }) love.graphics.draw(tmp1) -- 2.47.2 From 767ddc459c1af1a2e09c26755044a7b64e17714e Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Mon, 29 Dec 2025 02:16:09 +0300 Subject: [PATCH 05/14] shadows v1.5 --- lib/character/behaviors/sprite.lua | 54 +++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 0cfe73c..2a9ac84 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -58,7 +58,6 @@ function sprite:draw() local position = map.displayedPosition + Vec3 { 0.5, 0.5 } Tree.level.camera:detach() - love.graphics.push() love.graphics.scale(ppm) love.graphics.setCanvas(Tree.level.render.shadowLayer) @@ -66,15 +65,62 @@ function sprite:draw() love.graphics.translate(position.x, position.y) love.graphics.ellipse("fill", 0, 0, 0.2, 0.2 * math.cos(math.pi / 4)) - love.graphics.rotate(math.pi / 4) + local r = love.timer.getTime() % (2 * math.pi) + local doWeUseFakeShadow = function(r) + local pi = math.pi + if r <= pi / 4 then + -- 1 + return false, 1 + end + if r <= pi / 2 then + -- 2 + return true, 1 - (r - pi / 4) / (pi / 4) + end + if r <= 3 * pi / 4 then + -- 3 + return true, (r - pi / 2) / (pi / 4) + end + if r <= 5 * pi / 4 then + -- 4 + return false, 1 + end + if r <= 3 * pi / 2 then + -- 5 + return true, 1 - (r - 5 * pi / 4) / (pi / 4) + end + if r <= 7 * pi / 4 then + -- 6 + return true, (r - 3 * pi / 2) / (pi / 4) + end + -- 1 + return false, 1 + end + + local drawFakeShadow, opacity = doWeUseFakeShadow(r) + local nangle = (math.pi + r) % (2 * math.pi) + love.graphics.rotate(nangle) + + love.graphics.setColor(0, 0, 0, math.min(opacity * opacity, 0.5)) self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state], 0, - 0, nil, 1 / ppm * self.side, 1.2 / ppm, 38, 47) + 0, nil, ((nangle >= math.pi / 2 and nangle < (3 * math.pi / 2)) and -1 or 1) / ppm * self.side, + 1.2 / ppm, + 38, 47) + + if drawFakeShadow then + love.graphics.setColor(0, 0, 0, 1) + local mesh = makeGradientMesh(0.4, 1, { 0, 0, 0, 0.15 }, + { 0, 0, 0, 0.0 }) + love.graphics.push() + love.graphics.rotate(math.pi) + love.graphics.translate(-0.2, 0) + love.graphics.draw(mesh) + love.graphics.pop() + end love.graphics.pop() Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.spriteLayer) love.graphics.setColor(1, 1, 1) if Tree.level.selector.id == self.owner.id then -- 2.47.2 From 03070cfff72734e4e7282aa19e0b95fc1c56fa89 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 4 Jan 2026 02:18:02 +0300 Subject: [PATCH 06/14] move shadows-related logic into its own behavior --- lib/annotations.lua | 1 + lib/character/behaviors/shadowcaster.lua | 98 ++++++++++++++++++++++++ lib/character/behaviors/sprite.lua | 79 +------------------ lib/character/character.lua | 1 + lib/level/grid/tile_grid.lua | 2 + lib/level/level.lua | 4 - lib/spellbook.lua | 2 + 7 files changed, 108 insertions(+), 79 deletions(-) diff --git a/lib/annotations.lua b/lib/annotations.lua index 9753b5d..944093b 100644 --- a/lib/annotations.lua +++ b/lib/annotations.lua @@ -4,3 +4,4 @@ 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" diff --git a/lib/character/behaviors/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua index e69de29..687078e 100644 --- a/lib/character/behaviors/shadowcaster.lua +++ b/lib/character/behaviors/shadowcaster.lua @@ -0,0 +1,98 @@ +--- @class ShadowcasterBehavior : Behavior +local behavior = {} +behavior.id = "shadowcaster" +behavior.__index = behavior + +function behavior.new() return setmetatable({}, behavior) end + +local function makeGradientMesh(w, h, topColor, bottomColor) + local vertices = { + { 0, 0, 0, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- левый верх + { w, 0, 1, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- правый верх + { w + w * 0.1, h, 1, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- правый низ + { 0 - w * 0.1, h, 0, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- левый низ + } + local mesh = love.graphics.newMesh(vertices, "fan", "static") + return mesh +end + +local function getFakeShadow(r) + local pi = math.pi + + if r <= pi / 4 then + -- 1 + return false, 1 + end + if r <= pi / 2 then + -- 2 + return true, 1 - (r - pi / 4) / (pi / 4) + end + if r <= 3 * pi / 4 then + -- 3 + return true, (r - pi / 2) / (pi / 4) + end + if r <= 5 * pi / 4 then + -- 4 + return false, 1 + end + if r <= 3 * pi / 2 then + -- 5 + return true, 1 - (r - 5 * pi / 4) / (pi / 4) + end + if r <= 7 * pi / 4 then + -- 6 + return true, (r - 3 * pi / 2) / (pi / 4) + end + -- 1 + return false, 1 +end + +function behavior:draw() + local sprite = self.owner:has(Tree.behaviors.sprite) + local map = self.owner:has(Tree.behaviors.map) + if not map then return end + + local ppm = Tree.level.camera.pixelsPerMeter + local position = map.displayedPosition + Vec3 { 0.5, 0.5 } + + love.graphics.setCanvas(Tree.level.render.shadowLayer) + love.graphics.push() + love.graphics.scale(ppm) + love.graphics.setColor(0, 0, 0, 0.5) + love.graphics.translate(position.x, position.y) + love.graphics.ellipse("fill", 0, 0, 0.2, 0.2 * math.cos(math.pi / 4)) + + if not sprite then + love.graphics.pop() + return + end + + local r = love.timer.getTime() % (2 * math.pi) + + local drawFakeShadow, opacity = getFakeShadow(r) + local nangle = (math.pi + r) % (2 * math.pi) + love.graphics.rotate(nangle) + + love.graphics.setColor(0, 0, 0, math.min(opacity * opacity, 0.5)) + sprite.animationTable[sprite.state]:draw(Tree.assets.files.sprites.character[sprite.state], + 0, + 0, nil, ((nangle >= math.pi / 2 and nangle < (3 * math.pi / 2)) and -1 or 1) / ppm * sprite.side, + 1.2 / ppm, + 38, 47) + + if drawFakeShadow then + love.graphics.setColor(0, 0, 0, 1) + local mesh = makeGradientMesh(0.4, 1, { 0, 0, 0, 0.15 }, + { 0, 0, 0, 0.0 }) + love.graphics.push() + love.graphics.rotate(math.pi) + love.graphics.translate(-0.2, 0) + love.graphics.draw(mesh) + love.graphics.pop() + end + love.graphics.pop() + 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 2a9ac84..32ac8e0 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -38,17 +38,6 @@ function sprite:update(dt) anim:update(dt) end -local function makeGradientMesh(w, h, topColor, bottomColor) - local vertices = { - { 0, 0, 0, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- левый верх - { w, 0, 1, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- правый верх - { w + w * 0.1, h, 1, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- правый низ - { 0 - w * 0.1, h, 0, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- левый низ - } - local mesh = love.graphics.newMesh(vertices, "fan", "static") - return mesh -end - function sprite:draw() if not self.animationTable[self.state] or not Tree.assets.files.sprites.character[self.state] then return end @@ -57,71 +46,9 @@ function sprite:draw() local ppm = Tree.level.camera.pixelsPerMeter local position = map.displayedPosition + Vec3 { 0.5, 0.5 } - Tree.level.camera:detach() - love.graphics.push() - love.graphics.scale(ppm) - love.graphics.setCanvas(Tree.level.render.shadowLayer) - love.graphics.setColor(0, 0, 0, 0.5) - love.graphics.translate(position.x, position.y) - love.graphics.ellipse("fill", 0, 0, 0.2, 0.2 * math.cos(math.pi / 4)) - - local r = love.timer.getTime() % (2 * math.pi) - local doWeUseFakeShadow = function(r) - local pi = math.pi - - if r <= pi / 4 then - -- 1 - return false, 1 - end - if r <= pi / 2 then - -- 2 - return true, 1 - (r - pi / 4) / (pi / 4) - end - if r <= 3 * pi / 4 then - -- 3 - return true, (r - pi / 2) / (pi / 4) - end - if r <= 5 * pi / 4 then - -- 4 - return false, 1 - end - if r <= 3 * pi / 2 then - -- 5 - return true, 1 - (r - 5 * pi / 4) / (pi / 4) - end - if r <= 7 * pi / 4 then - -- 6 - return true, (r - 3 * pi / 2) / (pi / 4) - end - -- 1 - return false, 1 - end - - local drawFakeShadow, opacity = doWeUseFakeShadow(r) - local nangle = (math.pi + r) % (2 * math.pi) - love.graphics.rotate(nangle) - - love.graphics.setColor(0, 0, 0, math.min(opacity * opacity, 0.5)) - self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state], - 0, - 0, nil, ((nangle >= math.pi / 2 and nangle < (3 * math.pi / 2)) and -1 or 1) / ppm * self.side, - 1.2 / ppm, - 38, 47) - - if drawFakeShadow then - love.graphics.setColor(0, 0, 0, 1) - local mesh = makeGradientMesh(0.4, 1, { 0, 0, 0, 0.15 }, - { 0, 0, 0, 0.0 }) - love.graphics.push() - love.graphics.rotate(math.pi) - love.graphics.translate(-0.2, 0) - love.graphics.draw(mesh) - love.graphics.pop() - end - love.graphics.pop() - - Tree.level.camera:attach() love.graphics.setCanvas(Tree.level.render.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(), @@ -134,7 +61,9 @@ function sprite:draw() self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state], position.x, position.y, nil, 1 / ppm * self.side, 1 / ppm, 38, 47) + love.graphics.setShader() + Tree.level.camera:detach() love.graphics.setCanvas() end ) diff --git a/lib/character/character.lua b/lib/character/character.lua index bde0c2c..b5344ba 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -31,6 +31,7 @@ local function spawn(name, spriteDir, position, size, initiative) Tree.behaviors.stats.new(nil, nil, initiative), Tree.behaviors.map.new(position, size), Tree.behaviors.sprite.new(spriteDir), + Tree.behaviors.shadowcaster.new(), Tree.behaviors.spellcaster.new() } diff --git a/lib/level/grid/tile_grid.lua b/lib/level/grid/tile_grid.lua index 2625b5b..2294e33 100644 --- a/lib/level/grid/tile_grid.lua +++ b/lib/level/grid/tile_grid.lua @@ -14,9 +14,11 @@ end function map:draw() love.graphics.setCanvas(Tree.level.render.floorLayer) + Tree.level.camera:attach() utils.each(self.__grid, function(el) el:draw() end) + Tree.level.camera:detach() love.graphics.setCanvas() end diff --git a/lib/level/level.lua b/lib/level/level.lua index 2f92129..1d8cf96 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -41,15 +41,11 @@ function level:update(dt) end function level:draw() - self.camera:attach() self.render:clear() self.tileGrid:draw() while not self.characterGrid.yOrderQueue:is_empty() do -- по сути это сортировка кучей за n log n self.characterGrid.yOrderQueue:pop():draw() end - self.camera:detach() - --self.render.shadowLayer:newImageData():encode("png", "shadowLayer.png") - -- os.exit(0) self.render:draw() end diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 7130f25..11160f1 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -68,12 +68,14 @@ end function walk:draw() if not self.path then return end --- Это отрисовка пути персонажа к мышке + Tree.level.camera:attach() love.graphics.setCanvas(Tree.level.render.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 -- 2.47.2 From cd5c844c5245196a6551e6d4ae6a0e72b4d05847 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 4 Jan 2026 02:23:52 +0300 Subject: [PATCH 07/14] Fix variable naming from r to phi in shadow calculation function --- lib/character/behaviors/shadowcaster.lua | 30 +++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/character/behaviors/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua index 687078e..d1c833a 100644 --- a/lib/character/behaviors/shadowcaster.lua +++ b/lib/character/behaviors/shadowcaster.lua @@ -16,32 +16,34 @@ local function makeGradientMesh(w, h, topColor, bottomColor) return mesh end -local function getFakeShadow(r) +--- @param phi number угол, под которым падает свет +--- @return boolean, number: рисуем ли тень * её прозрачность +local function getFakeShadow(phi) local pi = math.pi - if r <= pi / 4 then + if phi <= pi / 4 then -- 1 return false, 1 end - if r <= pi / 2 then + if phi <= pi / 2 then -- 2 - return true, 1 - (r - pi / 4) / (pi / 4) + return true, 1 - (phi - pi / 4) / (pi / 4) end - if r <= 3 * pi / 4 then + if phi <= 3 * pi / 4 then -- 3 - return true, (r - pi / 2) / (pi / 4) + return true, (phi - pi / 2) / (pi / 4) end - if r <= 5 * pi / 4 then + if phi <= 5 * pi / 4 then -- 4 return false, 1 end - if r <= 3 * pi / 2 then + if phi <= 3 * pi / 2 then -- 5 - return true, 1 - (r - 5 * pi / 4) / (pi / 4) + return true, 1 - (phi - 5 * pi / 4) / (pi / 4) end - if r <= 7 * pi / 4 then + if phi <= 7 * pi / 4 then -- 6 - return true, (r - 3 * pi / 2) / (pi / 4) + return true, (phi - 3 * pi / 2) / (pi / 4) end -- 1 return false, 1 @@ -67,10 +69,10 @@ function behavior:draw() return end - local r = love.timer.getTime() % (2 * math.pi) + local phi = love.timer.getTime() % (2 * math.pi) - local drawFakeShadow, opacity = getFakeShadow(r) - local nangle = (math.pi + r) % (2 * math.pi) + local drawFakeShadow, opacity = getFakeShadow(phi) + local nangle = (math.pi + phi) % (2 * math.pi) love.graphics.rotate(nangle) love.graphics.setColor(0, 0, 0, math.min(opacity * opacity, 0.5)) -- 2.47.2 From 82fdd0d609b1141569439fc49dd7a2274b287668 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Thu, 8 Jan 2026 12:45:59 +0300 Subject: [PATCH 08/14] light source test --- assets/masks/circle128.png | Bin 0 -> 572 bytes assets/masks/gradientCircle256.png | Bin 0 -> 11445 bytes assets/shaders/light.glsl | 30 +++++++ assets/shaders/light_postprocess.glsl | 20 +++++ lib/annotations.lua | 1 + lib/character/behaviors/light.lua | 45 +++++++++++ lib/character/behaviors/shadowcaster.lua | 99 +++++++---------------- lib/character/character.lua | 15 +--- lib/level/grid/character_grid.lua | 8 +- lib/level/grid/light_grid.lua | 64 +++++++++++++++ lib/level/level.lua | 5 +- lib/level/render.lua | 34 +++++--- main.lua | 56 +++++++++++-- 13 files changed, 275 insertions(+), 102 deletions(-) create mode 100644 assets/masks/circle128.png create mode 100644 assets/masks/gradientCircle256.png create mode 100644 assets/shaders/light.glsl create mode 100644 assets/shaders/light_postprocess.glsl create mode 100644 lib/character/behaviors/light.lua create mode 100644 lib/level/grid/light_grid.lua diff --git a/assets/masks/circle128.png b/assets/masks/circle128.png new file mode 100644 index 0000000000000000000000000000000000000000..007a29dc7d8c9b866700ab51aa3127e9fcb312d8 GIT binary patch literal 572 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%0A(RP0i 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 BP;CGJ literal 0 HcmV?d00001 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/annotations.lua b/lib/annotations.lua index 944093b..63bfea7 100644 --- a/lib/annotations.lua +++ b/lib/annotations.lua @@ -5,3 +5,4 @@ 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" diff --git a/lib/character/behaviors/light.lua b/lib/character/behaviors/light.lua new file mode 100644 index 0000000..6f40dd4 --- /dev/null +++ b/lib/character/behaviors/light.lua @@ -0,0 +1,45 @@ +--- @class LightBehavior : Behavior +--- @field intensity number +--- @field color Vec3 +--- @field position Vec3 +--- @field seed integer +local behavior = {} +behavior.__index = behavior +behavior.id = "light" + +---@param values {intensity: number?, color: Vec3?, position: Vec3?, seed: integer?} +---@return LightBehavior +function behavior.new(values) + return setmetatable({ + intensity = values.intensity or 1, + color = values.color or Vec3 { 1, 1, 1 }, + position = values.position or Vec3 {}, + seed = values.seed or math.random(math.pow(2, 16)) + }, behavior) +end + +function behavior:update() + local mx, my = love.mouse.getX(), love.mouse.getY() + self.position = Tree.level.camera:toWorldPosition(Vec3 { mx, my }) +end + +function behavior:draw() + Tree.level.camera:attach() + love.graphics.setCanvas(Tree.level.render.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.setBlendMode("add") + love.graphics.draw(Tree.assets.files.masks.circle128, self.position.x - self.intensity / 2, + self.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/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua index d1c833a..dbbf915 100644 --- a/lib/character/behaviors/shadowcaster.lua +++ b/lib/character/behaviors/shadowcaster.lua @@ -1,3 +1,4 @@ +local easing = require "lib.utils.easing" --- @class ShadowcasterBehavior : Behavior local behavior = {} behavior.id = "shadowcaster" @@ -5,50 +6,6 @@ behavior.__index = behavior function behavior.new() return setmetatable({}, behavior) end -local function makeGradientMesh(w, h, topColor, bottomColor) - local vertices = { - { 0, 0, 0, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- левый верх - { w, 0, 1, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- правый верх - { w + w * 0.1, h, 1, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- правый низ - { 0 - w * 0.1, h, 0, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- левый низ - } - local mesh = love.graphics.newMesh(vertices, "fan", "static") - return mesh -end - ---- @param phi number угол, под которым падает свет ---- @return boolean, number: рисуем ли тень * её прозрачность -local function getFakeShadow(phi) - local pi = math.pi - - if phi <= pi / 4 then - -- 1 - return false, 1 - end - if phi <= pi / 2 then - -- 2 - return true, 1 - (phi - pi / 4) / (pi / 4) - end - if phi <= 3 * pi / 4 then - -- 3 - return true, (phi - pi / 2) / (pi / 4) - end - if phi <= 5 * pi / 4 then - -- 4 - return false, 1 - end - if phi <= 3 * pi / 2 then - -- 5 - return true, 1 - (phi - 5 * pi / 4) / (pi / 4) - end - if phi <= 7 * pi / 4 then - -- 6 - return true, (phi - 3 * pi / 2) / (pi / 4) - end - -- 1 - return false, 1 -end - function behavior:draw() local sprite = self.owner:has(Tree.behaviors.sprite) local map = self.owner:has(Tree.behaviors.map) @@ -57,42 +14,48 @@ function behavior:draw() local ppm = Tree.level.camera.pixelsPerMeter local position = map.displayedPosition + 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.shadowLayer) love.graphics.push() - love.graphics.scale(ppm) - love.graphics.setColor(0, 0, 0, 0.5) + 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.pop() + love.graphics.setCanvas() return end - local phi = love.timer.getTime() % (2 * math.pi) + love.graphics.setCanvas(Tree.level.render.spriteLightLayer) + love.graphics.setBlendMode("add") + for _, light in ipairs(lights) do + local lightPos = light:has(Tree.behaviors.light).position + local lightVec = lightPos - position - local drawFakeShadow, opacity = getFakeShadow(phi) - local nangle = (math.pi + phi) % (2 * math.pi) - love.graphics.rotate(nangle) + 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 - love.graphics.setColor(0, 0, 0, math.min(opacity * opacity, 0.5)) - sprite.animationTable[sprite.state]:draw(Tree.assets.files.sprites.character[sprite.state], - 0, - 0, nil, ((nangle >= math.pi / 2 and nangle < (3 * math.pi / 2)) and -1 or 1) / ppm * sprite.side, - 1.2 / ppm, - 38, 47) - - if drawFakeShadow then - love.graphics.setColor(0, 0, 0, 1) - local mesh = makeGradientMesh(0.4, 1, { 0, 0, 0, 0.15 }, - { 0, 0, 0, 0.0 }) - love.graphics.push() - love.graphics.rotate(math.pi) - love.graphics.translate(-0.2, 0) - love.graphics.draw(mesh) - love.graphics.pop() + 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.pop() + love.graphics.setBlendMode("alpha") + + Tree.level.camera:detach() love.graphics.setColor(1, 1, 1) love.graphics.setCanvas() end diff --git a/lib/character/character.lua b/lib/character/character.lua index b5344ba..d64f9d0 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,15 +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.shadowcaster.new(), - Tree.behaviors.spellcaster.new() - } - Tree.level.characters[char.id] = char return char end diff --git a/lib/level/grid/character_grid.lua b/lib/level/grid/character_grid.lua index b21772b..e04c357 100644 --- a/lib/level/grid/character_grid.lua +++ b/lib/level/grid/character_grid.lua @@ -29,10 +29,12 @@ end --- @param a Character --- @param b Character local function drawCmp(a, b) - -- здесь персонажи гарантированно имеют нужное поведение - return a:has(Tree.behaviors.map).displayedPosition.y + --- @TODO: это захардкожено, надо разделить поведения + return (a:has(Tree.behaviors.map) and a:has(Tree.behaviors.map).displayedPosition.y or + a:has(Tree.behaviors.light).position.y) < - b:has(Tree.behaviors.map).displayedPosition.y + (b:has(Tree.behaviors.map) and b:has(Tree.behaviors.map).displayedPosition.y or + b:has(Tree.behaviors.light).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..ace03d7 --- /dev/null +++ b/lib/level/grid/light_grid.lua @@ -0,0 +1,64 @@ +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 key = tostring(Vec3 { lightB.position.x, lightB.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/level.lua b/lib/level/level.lua index 1d8cf96..00c425c 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -4,6 +4,7 @@ local utils = require "lib.utils.utils" --- @field size Vec3 --- @field characters Character[] --- @field characterGrid CharacterGrid +--- @field lightGrid LightGrid --- @field selector Selector --- @field camera Camera --- @field tileGrid TileGrid @@ -12,8 +13,6 @@ local utils = require "lib.utils.utils" local level = {} level.__index = level -local path = nil - --- @param type "procedural"|"handmaded" --- @param template Procedural|Handmaded local function new(type, template) @@ -23,6 +22,7 @@ local function new(type, template) size = size, characters = {}, 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(), @@ -33,6 +33,7 @@ end function level:update(dt) self.characterGrid:reload() + self.lightGrid:reload() utils.each(self.characters, function(el) el:update(dt) end) diff --git a/lib/level/render.lua b/lib/level/render.lua index dd4425f..27d77b3 100644 --- a/lib/level/render.lua +++ b/lib/level/render.lua @@ -1,7 +1,9 @@ --- @class Render --- @field shadowLayer love.Canvas --- @field spriteLayer love.Canvas +--- @field spriteLightLayer love.Canvas --- @field floorLayer love.Canvas +--- @field lightLayer love.Canvas --- @field overlayLayer love.Canvas local render = {} render.__index = render @@ -11,8 +13,12 @@ function render:clear() love.graphics.clear() love.graphics.setCanvas(self.spriteLayer) love.graphics.clear() + love.graphics.setCanvas(self.spriteLightLayer) + love.graphics.clear() love.graphics.setCanvas(self.floorLayer) love.graphics.clear() + love.graphics.setCanvas(self.lightLayer) + love.graphics.clear() love.graphics.setCanvas(self.overlayLayer) love.graphics.clear() end @@ -48,20 +54,26 @@ local function applyBlur(input, radius) end function render:draw() - -- пол -> тени -> спрайты -> оверлей + -- пол -> тени -> спрайты -> свет -> оверлей + love.graphics.setCanvas(self.lightLayer) + love.graphics.draw(applyBlur(self.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", self.floorLayer) + lightShader:send("light", self.lightLayer) + lightShader:send("ambient", { 0.24, 0.28, 0.40 }) + love.graphics.setShader(lightShader) love.graphics.draw(self.floorLayer) - love.graphics.push() - local blurred = applyBlur(self.shadowLayer, 2) - local wc, hc = love.graphics.getWidth() / 2, love.graphics.getHeight() / 2 - love.graphics.translate(wc, hc) - love.graphics.scale(Tree.level.camera.scale, Tree.level.camera.scale) - love.graphics.translate(-Tree.level.camera.position.x * Tree.level.camera.pixelsPerMeter, - -Tree.level.camera.position.y * Tree.level.camera.pixelsPerMeter) - love.graphics.draw(blurred) - love.graphics.pop() + lightShader:send("scene", self.spriteLayer) + lightShader:send("light", self.spriteLightLayer) love.graphics.draw(self.spriteLayer) + love.graphics.setShader() love.graphics.draw(self.overlayLayer) end @@ -70,8 +82,10 @@ local function new() return setmetatable({ shadowLayer = love.graphics.newCanvas(1280, 720), spriteLayer = love.graphics.newCanvas(1280, 720), + spriteLightLayer = love.graphics.newCanvas(1280, 720), floorLayer = love.graphics.newCanvas(1280, 720), overlayLayer = love.graphics.newCanvas(1280, 720), + lightLayer = love.graphics.newCanvas(1280, 720) }, render) end diff --git a/main.lua b/main.lua index e77cdba..506dbb1 100644 --- a/main.lua +++ b/main.lua @@ -9,13 +9,59 @@ function love.conf(t) 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 + local chars = { + character.spawn("Foodor") + :addBehavior { + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.stats.new(nil, nil, 1), + Tree.behaviors.map.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.map.new(Vec3 { 3, 3 }), + Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), + Tree.behaviors.shadowcaster.new(), + Tree.behaviors.spellcaster.new() + }, + character.spawn("Foodor Jr") + :addBehavior { + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.stats.new(nil, nil, 3), + Tree.behaviors.map.new(Vec3 { 0, 3 }), + Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), + Tree.behaviors.shadowcaster.new(), + Tree.behaviors.spellcaster.new() + }, + character.spawn("Baris Jr") + :addBehavior { + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.stats.new(nil, nil, 4), + Tree.behaviors.map.new(Vec3 { 0, 6 }), + 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 + + for i = 1, 1, 1 do + for j = 1, 1, 1 do + character.spawn("My Light") + :addBehavior { + Tree.behaviors.light.new { color = Vec3 { 88, 34, 13 }, position = Vec3 { i, j } * 3, intensity = 10 } + } + end + end + + Tree.level.turnOrder:endRound() print("Now playing:", Tree.level.turnOrder.current) love.window.setMode(1280, 720, { resizable = true, msaa = 4, vsync = true }) -- 2.47.2 From e6754048f69574f0357182f7a371244518c5afe8 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 14 Jan 2026 21:00:17 +0300 Subject: [PATCH 09/14] add weather to level --- lib/character/behaviors/light.lua | 2 +- lib/level/level.lua | 3 ++- lib/level/render.lua | 8 +++++--- lib/level/weather.lua | 12 ++++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 lib/level/weather.lua diff --git a/lib/character/behaviors/light.lua b/lib/character/behaviors/light.lua index 6f40dd4..c282f7f 100644 --- a/lib/character/behaviors/light.lua +++ b/lib/character/behaviors/light.lua @@ -30,7 +30,7 @@ function behavior:draw() 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.setBlendMode("add") + -- love.graphics.setBlendMode("add") love.graphics.draw(Tree.assets.files.masks.circle128, self.position.x - self.intensity / 2, self.position.y - self.intensity / 2, 0, self.intensity / 128, self.intensity / 128) diff --git a/lib/level/level.lua b/lib/level/level.lua index 00c425c..2023691 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -27,7 +27,8 @@ local function new(type, template) 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() + render = (require "lib.level.render").new(), + weather = (require "lib.level.weather").new { ambientLight = Vec3 { 0.36, 0.42, 0.6 }, skyLight = Vec3 { 0.9, 0.9, 0.9 } } }, level) end diff --git a/lib/level/render.lua b/lib/level/render.lua index 27d77b3..a88ea2e 100644 --- a/lib/level/render.lua +++ b/lib/level/render.lua @@ -9,16 +9,17 @@ local render = {} render.__index = render function render:clear() + local weather = Tree.level.weather love.graphics.setCanvas(self.shadowLayer) love.graphics.clear() love.graphics.setCanvas(self.spriteLayer) love.graphics.clear() love.graphics.setCanvas(self.spriteLightLayer) - love.graphics.clear() + love.graphics.clear(weather.skyLight.x, weather.skyLight.y, weather.skyLight.z) love.graphics.setCanvas(self.floorLayer) love.graphics.clear() love.graphics.setCanvas(self.lightLayer) - love.graphics.clear() + love.graphics.clear(weather.skyLight.x, weather.skyLight.y, weather.skyLight.z) love.graphics.setCanvas(self.overlayLayer) love.graphics.clear() end @@ -55,6 +56,7 @@ end function render:draw() -- пол -> тени -> спрайты -> свет -> оверлей + local weather = Tree.level.weather love.graphics.setCanvas(self.lightLayer) love.graphics.draw(applyBlur(self.shadowLayer, 4 * Tree.level.camera.scale)) love.graphics.setCanvas() @@ -65,7 +67,7 @@ function render:draw() local lightShader = Tree.assets.files.shaders.light_postprocess lightShader:send("scene", self.floorLayer) lightShader:send("light", self.lightLayer) - lightShader:send("ambient", { 0.24, 0.28, 0.40 }) + lightShader:send("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z }) love.graphics.setShader(lightShader) love.graphics.draw(self.floorLayer) 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 } -- 2.47.2 From 4aa470f443a54d4188ef6ee1d670b6761a41eea0 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 14 Jan 2026 21:35:23 +0300 Subject: [PATCH 10/14] allow window resizing TODO: get rid of fullscreen rendering --- lib/character/behaviors/light.lua | 2 +- lib/character/behaviors/shadowcaster.lua | 4 +- lib/character/behaviors/sprite.lua | 2 +- lib/level/grid/tile_grid.lua | 2 +- lib/level/level.lua | 4 +- lib/level/render.lua | 88 ++++++++++++++---------- lib/spellbook.lua | 2 +- main.lua | 15 +++- 8 files changed, 70 insertions(+), 49 deletions(-) diff --git a/lib/character/behaviors/light.lua b/lib/character/behaviors/light.lua index c282f7f..9e08c35 100644 --- a/lib/character/behaviors/light.lua +++ b/lib/character/behaviors/light.lua @@ -25,7 +25,7 @@ end function behavior:draw() Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.lightLayer) + 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) diff --git a/lib/character/behaviors/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua index dbbf915..7e3f0eb 100644 --- a/lib/character/behaviors/shadowcaster.lua +++ b/lib/character/behaviors/shadowcaster.lua @@ -22,7 +22,7 @@ function behavior:draw() end Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.shadowLayer) + 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) @@ -34,7 +34,7 @@ function behavior:draw() return end - love.graphics.setCanvas(Tree.level.render.spriteLightLayer) + love.graphics.setCanvas(Tree.level.render.textures.spriteLightLayer) love.graphics.setBlendMode("add") for _, light in ipairs(lights) do local lightPos = light:has(Tree.behaviors.light).position diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 32ac8e0..bc8fe9a 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -46,7 +46,7 @@ function sprite:draw() local ppm = Tree.level.camera.pixelsPerMeter local position = map.displayedPosition + Vec3 { 0.5, 0.5 } - love.graphics.setCanvas(Tree.level.render.spriteLayer) + love.graphics.setCanvas(Tree.level.render.textures.spriteLayer) Tree.level.camera:attach() love.graphics.setColor(1, 1, 1) diff --git a/lib/level/grid/tile_grid.lua b/lib/level/grid/tile_grid.lua index 2294e33..1a89a10 100644 --- a/lib/level/grid/tile_grid.lua +++ b/lib/level/grid/tile_grid.lua @@ -13,7 +13,7 @@ local function new(type, template, size) end function map:draw() - love.graphics.setCanvas(Tree.level.render.floorLayer) + love.graphics.setCanvas(Tree.level.render.textures.floorLayer) Tree.level.camera:attach() utils.each(self.__grid, function(el) el:draw() diff --git a/lib/level/level.lua b/lib/level/level.lua index 2023691..13be418 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -27,8 +27,8 @@ local function new(type, template) 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 { 0.9, 0.9, 0.9 } } + render = (require "lib.level.render").new {}, + weather = (require "lib.level.weather").new { ambientLight = Vec3 { 0.36, 0.42, 0.6 }, skyLight = Vec3 {} } }, level) end diff --git a/lib/level/render.lua b/lib/level/render.lua index a88ea2e..5888888 100644 --- a/lib/level/render.lua +++ b/lib/level/render.lua @@ -1,40 +1,43 @@ --- @class Render ---- @field shadowLayer love.Canvas ---- @field spriteLayer love.Canvas ---- @field spriteLightLayer love.Canvas ---- @field floorLayer love.Canvas ---- @field lightLayer love.Canvas ---- @field overlayLayer love.Canvas -local render = {} -render.__index = render +--- @field textures table +local render = { + textures = {} +} function render:clear() local weather = Tree.level.weather - love.graphics.setCanvas(self.shadowLayer) + local txs = self.textures + love.graphics.setCanvas(txs.shadowLayer) love.graphics.clear() - love.graphics.setCanvas(self.spriteLayer) + love.graphics.setCanvas(txs.spriteLayer) love.graphics.clear() - love.graphics.setCanvas(self.spriteLightLayer) + love.graphics.setCanvas(txs.spriteLightLayer) love.graphics.clear(weather.skyLight.x, weather.skyLight.y, weather.skyLight.z) - love.graphics.setCanvas(self.floorLayer) + love.graphics.setCanvas(txs.floorLayer) love.graphics.clear() - love.graphics.setCanvas(self.lightLayer) + love.graphics.setCanvas(txs.lightLayer) love.graphics.clear(weather.skyLight.x, weather.skyLight.y, weather.skyLight.z) - love.graphics.setCanvas(self.overlayLayer) + 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: это используется для блюра, должно кэшироваться и поддерживать ресайз -local tmp1 = love.graphics.newCanvas(1280, 720) -local tmp2 = love.graphics.newCanvas(1280, 720) -local function applyBlur(input, radius) + +function render:applyBlur(input, radius) local blurShader = Tree.assets.files.shaders.blur -- Горизонтальный проход blurShader:send("direction", { 1.0, 0.0 }) blurShader:send("radius", radius) - tmp1:renderTo(function() + self.textures.tmp1:renderTo(function() love.graphics.clear() love.graphics.setShader(blurShader) love.graphics.draw(input) @@ -42,53 +45,62 @@ local function applyBlur(input, radius) end) -- Вертикальный проход - tmp2:renderTo( + self.textures.tmp2:renderTo( function() love.graphics.clear() love.graphics.setShader(blurShader) blurShader:send("direction", { 0.0, 1.0 }) - love.graphics.draw(tmp1) + love.graphics.draw(self.textures.tmp1) love.graphics.setShader() end ) - return tmp2 + return self.textures.tmp2 end function render:draw() -- пол -> тени -> спрайты -> свет -> оверлей local weather = Tree.level.weather - love.graphics.setCanvas(self.lightLayer) - love.graphics.draw(applyBlur(self.shadowLayer, 4 * Tree.level.camera.scale)) + 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", self.floorLayer) - lightShader:send("light", self.lightLayer) + 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(self.floorLayer) + love.graphics.draw(txs.floorLayer) - lightShader:send("scene", self.spriteLayer) - lightShader:send("light", self.spriteLightLayer) - love.graphics.draw(self.spriteLayer) + lightShader:send("scene", txs.spriteLayer) + lightShader:send("light", txs.spriteLightLayer) + love.graphics.draw(txs.spriteLayer) love.graphics.setShader() - love.graphics.draw(self.overlayLayer) + love.graphics.draw(txs.overlayLayer) end -local function new() +---@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({ - shadowLayer = love.graphics.newCanvas(1280, 720), - spriteLayer = love.graphics.newCanvas(1280, 720), - spriteLightLayer = love.graphics.newCanvas(1280, 720), - floorLayer = love.graphics.newCanvas(1280, 720), - overlayLayer = love.graphics.newCanvas(1280, 720), - lightLayer = love.graphics.newCanvas(1280, 720) - }, render) + 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/spellbook.lua b/lib/spellbook.lua index 11160f1..311e5a7 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -69,7 +69,7 @@ function walk:draw() if not self.path then return end --- Это отрисовка пути персонажа к мышке Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.overlayLayer) + 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) diff --git a/main.lua b/main.lua index 506dbb1..681f988 100644 --- a/main.lua +++ b/main.lua @@ -1,14 +1,17 @@ -- 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() + 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 { @@ -64,7 +67,6 @@ function love.load() 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" @@ -116,3 +118,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 -- 2.47.2 From 7ff7e47a907c943f55a3679e18d52aa8e3897cb7 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sat, 17 Jan 2026 14:49:08 +0300 Subject: [PATCH 11/14] add PositionedBehavior --- lib/annotations.lua | 4 +- lib/character/behaviors/cursor.lua | 19 +++++ lib/character/behaviors/light.lua | 14 ++-- lib/character/behaviors/map.lua | 91 ------------------------ lib/character/behaviors/positioned.lua | 25 +++++++ lib/character/behaviors/shadowcaster.lua | 8 +-- lib/character/behaviors/sprite.lua | 6 +- lib/character/behaviors/tiled.lua | 83 +++++++++++++++++++++ lib/level/grid/character_grid.lua | 19 +++-- lib/level/grid/light_grid.lua | 5 +- lib/simple_ui/level/end_turn.lua | 4 +- lib/spellbook.lua | 10 +-- main.lua | 41 +++-------- 13 files changed, 173 insertions(+), 156 deletions(-) create mode 100644 lib/character/behaviors/cursor.lua delete mode 100644 lib/character/behaviors/map.lua create mode 100644 lib/character/behaviors/positioned.lua create mode 100644 lib/character/behaviors/tiled.lua diff --git a/lib/annotations.lua b/lib/annotations.lua index 63bfea7..6a0d688 100644 --- a/lib/annotations.lua +++ b/lib/annotations.lua @@ -1,8 +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/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 index 9e08c35..8d6b93c 100644 --- a/lib/character/behaviors/light.lua +++ b/lib/character/behaviors/light.lua @@ -1,38 +1,36 @@ --- @class LightBehavior : Behavior --- @field intensity number --- @field color Vec3 ---- @field position Vec3 --- @field seed integer local behavior = {} behavior.__index = behavior behavior.id = "light" ----@param values {intensity: number?, color: Vec3?, position: Vec3?, seed: integer?} +---@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 }, - position = values.position or Vec3 {}, seed = values.seed or math.random(math.pow(2, 16)) }, behavior) end function behavior:update() - local mx, my = love.mouse.getX(), love.mouse.getY() - self.position = Tree.level.camera:toWorldPosition(Vec3 { mx, my }) 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.setBlendMode("add") - love.graphics.draw(Tree.assets.files.masks.circle128, self.position.x - self.intensity / 2, - self.position.y - self.intensity / 2, 0, self.intensity / 128, + 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") 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/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua index 7e3f0eb..3407143 100644 --- a/lib/character/behaviors/shadowcaster.lua +++ b/lib/character/behaviors/shadowcaster.lua @@ -8,11 +8,11 @@ function behavior.new() return setmetatable({}, behavior) end function behavior:draw() local sprite = self.owner:has(Tree.behaviors.sprite) - local map = self.owner:has(Tree.behaviors.map) - if not map then return end + local positioned = self.owner:has(Tree.behaviors.positioned) + if not positioned then return end local ppm = Tree.level.camera.pixelsPerMeter - local position = map.displayedPosition + Vec3 { 0.5, 0.5 } + local position = positioned.position + Vec3 { 0.5, 0.5 } local lightIds = Tree.level.lightGrid:query(position, 5) --- @type Character[] @@ -37,7 +37,7 @@ function behavior:draw() love.graphics.setCanvas(Tree.level.render.textures.spriteLightLayer) love.graphics.setBlendMode("add") for _, light in ipairs(lights) do - local lightPos = light:has(Tree.behaviors.light).position + local lightPos = light:has(Tree.behaviors.positioned).position local lightVec = lightPos - position local lightColor = light:has(Tree.behaviors.light).color diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index bc8fe9a..33f076b 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -41,10 +41,10 @@ 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 + Vec3 { 0.5, 0.5 } + local position = pos.position + Vec3 { 0.5, 0.5 } love.graphics.setCanvas(Tree.level.render.textures.spriteLayer) Tree.level.camera:attach() 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/level/grid/character_grid.lua b/lib/level/grid/character_grid.lua index e04c357..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 @@ -30,11 +33,7 @@ end --- @param b Character local function drawCmp(a, b) --- @TODO: это захардкожено, надо разделить поведения - return (a:has(Tree.behaviors.map) and a:has(Tree.behaviors.map).displayedPosition.y or - a:has(Tree.behaviors.light).position.y) - < - (b:has(Tree.behaviors.map) and b:has(Tree.behaviors.map).displayedPosition.y or - b:has(Tree.behaviors.light).position.y) + 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 index ace03d7..9b7042b 100644 --- a/lib/level/grid/light_grid.lua +++ b/lib/level/grid/light_grid.lua @@ -15,9 +15,10 @@ function grid:add(id) 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 { lightB.position.x, lightB.position.y }:floor()) + 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 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 311e5a7..2149541 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -51,7 +51,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 +59,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) @@ -104,8 +104,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 @@ -128,7 +128,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, diff --git a/main.lua b/main.lua index 681f988..b291035 100644 --- a/main.lua +++ b/main.lua @@ -17,7 +17,8 @@ function love.load() :addBehavior { Tree.behaviors.residentsleeper.new(), Tree.behaviors.stats.new(nil, nil, 1), - Tree.behaviors.map.new(), + 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() @@ -25,26 +26,9 @@ function love.load() character.spawn("Baris") :addBehavior { Tree.behaviors.residentsleeper.new(), - Tree.behaviors.stats.new(nil, nil, 2), - Tree.behaviors.map.new(Vec3 { 3, 3 }), - Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), - Tree.behaviors.shadowcaster.new(), - Tree.behaviors.spellcaster.new() - }, - character.spawn("Foodor Jr") - :addBehavior { - Tree.behaviors.residentsleeper.new(), - Tree.behaviors.stats.new(nil, nil, 3), - Tree.behaviors.map.new(Vec3 { 0, 3 }), - Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), - Tree.behaviors.shadowcaster.new(), - Tree.behaviors.spellcaster.new() - }, - character.spawn("Baris Jr") - :addBehavior { - Tree.behaviors.residentsleeper.new(), - Tree.behaviors.stats.new(nil, nil, 4), - Tree.behaviors.map.new(Vec3 { 0, 6 }), + 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() @@ -55,15 +39,12 @@ function love.load() Tree.level.turnOrder:add(id) end - for i = 1, 1, 1 do - for j = 1, 1, 1 do - character.spawn("My Light") - :addBehavior { - Tree.behaviors.light.new { color = Vec3 { 88, 34, 13 }, position = Vec3 { i, j } * 3, intensity = 10 } - } - end - end - + character.spawn("My Light") + :addBehavior { + Tree.behaviors.light.new { color = Vec3 { 88, 34, 13 }, intensity = 10 }, + Tree.behaviors.positioned.new(), + Tree.behaviors.cursor.new() + } Tree.level.turnOrder:endRound() print("Now playing:", Tree.level.turnOrder.current) -- 2.47.2 From e088fddf48b64938a93b37e549327c1c28370883 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sat, 17 Jan 2026 17:43:29 +0300 Subject: [PATCH 12/14] implement character:die() --- lib/character/behaviors/behavior.lua | 5 +++++ lib/character/character.lua | 12 ++++++++++++ lib/level/level.lua | 7 +++++++ main.lua | 7 +++++++ 4 files changed, 31 insertions(+) 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/character.lua b/lib/character/character.lua index d64f9d0..b9d9a50 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -80,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/level.lua b/lib/level/level.lua index 13be418..17ddb34 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -3,6 +3,7 @@ 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 @@ -21,6 +22,7 @@ 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), @@ -33,6 +35,11 @@ local function new(type, template) 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) diff --git a/main.lua b/main.lua index b291035..846cac0 100644 --- a/main.lua +++ b/main.lua @@ -61,6 +61,13 @@ function love.update(dt) Tree.controls:cache() + if t1 > 2 then + local foodor = Tree.level.characters[1] + if not foodor then return end + print("Killing Foodor") + foodor:die() + end + local t2 = love.timer.getTime() lt = string.format("%.3f", (t2 - t1) * 1000) end -- 2.47.2 From ab37f6816de1ff3d1bb268f41f38b59b2f8f80c7 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sat, 17 Jan 2026 18:26:13 +0300 Subject: [PATCH 13/14] add demo dynamic light on dev_mana cast --- lib/animation_node.lua | 1 + lib/character/behaviors/residentsleeper.lua | 13 +++-------- lib/spellbook.lua | 25 +++++++++++++++++---- main.lua | 13 ----------- 4 files changed, 25 insertions(+), 27 deletions(-) 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/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/spellbook.lua b/lib/spellbook.lua index 2149541..e50fc82 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 @@ -90,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) @@ -97,6 +105,14 @@ function regenerateMana:cast(caster, target) onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end }:run() + AnimationNode { + function(node) + light:has(Tree.behaviors.residentsleeper):sleep(node) + end, + duration = 400, + onEnd = function() light:die() end + }:run() + return true end @@ -140,8 +156,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 846cac0..26d002d 100644 --- a/main.lua +++ b/main.lua @@ -39,12 +39,6 @@ function love.load() Tree.level.turnOrder:add(id) end - character.spawn("My Light") - :addBehavior { - Tree.behaviors.light.new { color = Vec3 { 88, 34, 13 }, intensity = 10 }, - Tree.behaviors.positioned.new(), - Tree.behaviors.cursor.new() - } Tree.level.turnOrder:endRound() print("Now playing:", Tree.level.turnOrder.current) @@ -61,13 +55,6 @@ function love.update(dt) Tree.controls:cache() - if t1 > 2 then - local foodor = Tree.level.characters[1] - if not foodor then return end - print("Killing Foodor") - foodor:die() - end - local t2 = love.timer.getTime() lt = string.format("%.3f", (t2 - t1) * 1000) end -- 2.47.2 From 26633db3c37b6b6e1bc67e90ac9902df7bfd1cf5 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 18 Jan 2026 01:03:35 +0300 Subject: [PATCH 14/14] implement LightBehavior:animateColor (to show that animations do work, actually) --- lib/character/behaviors/light.lua | 16 +++++++++++++++- lib/spellbook.lua | 5 +++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/character/behaviors/light.lua b/lib/character/behaviors/light.lua index 8d6b93c..1c629c6 100644 --- a/lib/character/behaviors/light.lua +++ b/lib/character/behaviors/light.lua @@ -2,6 +2,9 @@ --- @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" @@ -16,7 +19,18 @@ function behavior.new(values) }, behavior) end -function behavior:update() +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() diff --git a/lib/spellbook.lua b/lib/spellbook.lua index e50fc82..51b0fa6 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -107,9 +107,10 @@ function regenerateMana:cast(caster, target) AnimationNode { function(node) - light:has(Tree.behaviors.residentsleeper):sleep(node) + light:has(Tree.behaviors.light):animateColor(Vec3 {}, node) end, - duration = 400, + easing = easing.easeInQuad, + duration = 800, onEnd = function() light:die() end }:run() -- 2.47.2