From faab9e7111c6e188145f0be9bfa2da31998816e5 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Mon, 22 Sep 2025 23:00:57 +0200 Subject: [PATCH 01/14] Added `StateSprite` +Title, example shall follow --- lsp_def/classes/atlas.lua | 2 + lsp_def/vanilla.lua | 5 ++ src/game_object.lua | 144 +++++++++++++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/lsp_def/classes/atlas.lua b/lsp_def/classes/atlas.lua index c9238bd04..5df5bf443 100644 --- a/lsp_def/classes/atlas.lua +++ b/lsp_def/classes/atlas.lua @@ -8,6 +8,8 @@ ---@field path? string Name of the image file, including extension. ---@field atlas_table? "ASSET_ATLAS"|"ANIMATION_ATLAS"|"ASSET_IMAGES"|string Type of atlas. `ASSET_ATLAS`: non-animated sprites, `ANIMATION_ATLAS`: animated sprites, `ASSET_IMAGES`: anything else. ---@field frames? number Number of frames in the animation. +---@field columns? number Number of columns (= sprites horizontally). +---@field rows? number Number of rows (= sprites vertically). ---@field raw_key? boolean Sets whether the mod prefix is added to atlas key. Used for overriding vanilla sprites. ---@field language? string Key to a language. Restricts the atlas to only when this language is enabled. ---@field disable_mipmap? boolean Sets if the sprite is affected by the mipmap. diff --git a/lsp_def/vanilla.lua b/lsp_def/vanilla.lua index cb6c02ad0..4e060ac8d 100644 --- a/lsp_def/vanilla.lua +++ b/lsp_def/vanilla.lua @@ -80,6 +80,11 @@ function Sprite:__call(...) return self end AnimatedSprite = {} function AnimatedSprite:__call(...) return self end +---@class StateSprite: AnimatedSprite +---@overload fun(...: any): StateSprite|table +StateSprite = {} +function StateSprite:__call(...) return self end + ---@class Blind: Moveable ---@overload fun(...: any): Blind|table Blind = {} diff --git a/src/game_object.lua b/src/game_object.lua index ac256f251..402104502 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -399,6 +399,12 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. ----- API CODE GameObject.Atlas ------------------------------------------------------------------------------------------------- + local atlas_table_map = { + ASSET_ATLAS = "ASSET_ATLAS", + ANIMATION_ATLAS = "ANIMATION_ATLAS", + STATE_ATLAS = "ANIMATION_ATLAS", + } + SMODS.Atlases = {} SMODS.Atlas = SMODS.GameObject:extend { obj_table = SMODS.Atlases, @@ -440,7 +446,11 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. ('Failed to initialize image data for Atlas %s'):format(self.key)) self.image = love.graphics.newImage(self.image_data, { mipmaps = true, dpiscale = G.SETTINGS.GRAPHICS.texture_scaling }) - G[self.atlas_table][self.key_noloc or self.key] = self + + self.columns = self.image:get_width() / self.px + self.rows = self.image:get_height() / self.py + + G[atlas_table_map[self.atlas_table]][self.key_noloc or self.key] = self local mipmap_level = SMODS.config.graphics_mipmap_level_options[SMODS.config.graphics_mipmap_level] if not self.disable_mipmap and mipmap_level and mipmap_level > 0 then @@ -3806,6 +3816,136 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. } + ------------------------------------------------------------------------------------------------- + ------- API CODE Object.Node.Moveable.Sprite.AnimatedSprite.StateSprite + ------------------------------------------------------------------------------------------------- + + + StateSprite = AnimatedSprite:extend() + + -- Form of param [states] is; + -- { [state_name] = { start_pos = { x/y = [0..n-1 for n columns/rows in sprite atlas] }, (frames = [amount of frames] |OR| end_pos = { [same as start_pos] }), (optional) flipped_h/flipped_v = true }, ...} + -- Example; + --[[ + { + sleepy = { + start_pos = {x = 0, y = 0}, + end_pos = {x = 3} (y is set to start_pos.y) + }, + wakey = { + start_pos = {x = 4}, (y is set to 0) + frames = 4 (end_pos is set to start_pos with .x + frames) + }, + lookey = { + flipped_h = true, (start_pos is set to {x = 0, y = 0}, end_pos is set to start_pos => this state is a single frame "animation" at x = 0, y = 0, and flipped horizontally and vertically) + flipped_v = true + } + } + ]] + -- To change state, call StateSprite:set_state(state_name) + function StateSprite:init(X, Y, W, H, new_sprite_atlas, states, start_state, states_offset) + AnimatedSprite.init(self, X, Y, W, H, new_sprite_atlas, {x=0, y=0}) + + self.states_offset = states_offset and {x = states_offset.x or 0, y = states_offset.y or 0} or {x = 0, y = 0} + + self:load_states(states) + self:set_state(start_state) + self.state_name = start_state + + self.flipped_h = false + self.flipped_v = false + + if getmetatable(self) == StateSprite then + table.insert(G.I.SPRITE, self) + end + end + + function StateSprite:set_state(state) + local a_state = self.a_states[state] + if a_state and self.state ~= a_state then + self.state = a_state + self:set_sprite_pos({x = self.state.start_pos.x + self.states_offset.x, y = self.state.start_pos.y + self.states_offset.y}) + self.flipped_h = self.state.flipped_h + self.flipped_v = self.state.flipped_v + end + end + + function StateSprite:load_states(states) + self.a_states = {} + for key, state in ipairs(states) do + state.start_pos = state.start_pos and {x = state.start_pos.x or 0, y = state.start_pos.y or 0} or {x = 0, y = 0} + state.end_pos = state.end_pos and {x = state.end_pos.x or state.start_pos.x + (state.frames or 0), y = state.end_pos.y or state.start_pos.y} or state.start_pos + self.a_states[key] = state + end + end + + function StateSprite:animate() + local new_frame = self.state.start_pos.x + math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames + local _x = new_frame % self.atlas.columns + local _y = math.floor(new_frame / self.atlas.columns) + if new_frame ~= self.current_animation.current then + self.current_animation.current = new_frame + -- self.frame_offset = math.floor(self.animation.w*(self.current_animation.current)) + self.sprite:setViewport( + _x, + self.animation.h*self.animation.y + _y, -- Equivalent to; self.state.start_pos.y + _y (I think at least) + self.animation.w, + self.animation.h) + end + if self.float then + self.T.r = 0.02*math.sin(2*G.TIMERS.REAL+self.T.x) + self.offset.y = -(1+0.3*math.sin(0.666*G.TIMERS.REAL+self.T.y))*self.shadow_parrallax.y + self.offset.x = -(0.7+0.2*math.sin(0.666*G.TIMERS.REAL+self.T.x))*self.shadow_parrallax.x + end + end + + function StateSprite:set_sprite_pos(sprite_pos) + self.animation = { + x = sprite_pos and sprite_pos.x or 0, + y = sprite_pos and sprite_pos.y or 0, + frames = self.state and (self.state.end_pos.x - self.state.start_pos.x) or 1, current = 0, + w = self.scale.x, h = self.scale.y + } + + self.frame_offset = 0 -- Unused + + self.current_animation = { + current = 0, + frames = self.animation.frames, + w = self.animation.w, + h = self.animation.h} + + self.image_dims = self.image_dims or {} + self.image_dims[1], self.image_dims[2] = self.atlas.image:getDimensions() + + self.sprite = love.graphics.newQuad( + 0, + self.animation.h*self.animation.y, + self.animation.w, + self.animation.h, + self.image_dims[1], self.image_dims[2] + ) + self.offset_seconds = G.TIMERS.REAL + end + + function StateSprite:draw_self() + if not self.states.visible then return end + + prep_draw(self, 1) + love.graphics.scale(1/self.scale_mag) + love.graphics.setColor(G.C.WHITE) + love.graphics.draw( + self.atlas.image, + self.sprite, + 0 ,0, + 0, + self.VT.w/(self.T.w) * (self.flipped_h and -1 or 1), + self.VT.h/(self.T.h) * (self.flipped_v and -1 or 1) + ) + love.graphics.pop() + end + + ------------------------------------------------------------------------------------------------- ----- API IMPORT GameObject.DrawStep ------------------------------------------------------------------------------------------------- @@ -3830,4 +3970,4 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end end } -end +end \ No newline at end of file From a907434ab8f55b67dc1f42804d2547c4f4ca692c Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Thu, 25 Sep 2025 21:38:00 +0200 Subject: [PATCH 02/14] Added support for `state.frame_order`, defining the order the frames are played in. +Title, currently includes `linear`, `random`, or a table/list with manual frame-index-to-frame order. --- src/game_object.lua | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index 402104502..f947b8773 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -3824,7 +3824,17 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. StateSprite = AnimatedSprite:extend() -- Form of param [states] is; - -- { [state_name] = { start_pos = { x/y = [0..n-1 for n columns/rows in sprite atlas] }, (frames = [amount of frames] |OR| end_pos = { [same as start_pos] }), (optional) flipped_h/flipped_v = true }, ...} + --[[ + { + [state_name] = { + start_pos = { x/y = [0..n-1 for n columns/rows in sprite atlas] }, + (frames = [amount of frames] |OR| end_pos = { [same as start_pos] }), + frame_order = "linear" |OR| "random" |OR| {1: x, 2: y, .. n: m} + (optional) flipped_h/flipped_v = true + }, + ... + } + ]] -- Example; --[[ { @@ -3875,12 +3885,35 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. for key, state in ipairs(states) do state.start_pos = state.start_pos and {x = state.start_pos.x or 0, y = state.start_pos.y or 0} or {x = 0, y = 0} state.end_pos = state.end_pos and {x = state.end_pos.x or state.start_pos.x + (state.frames or 0), y = state.end_pos.y or state.start_pos.y} or state.start_pos + if type(state.frame_order) == "string" then + local keymap = { + linear=true, + random=true + } + if not keymap[state.frame_order:lower()] then + state.frame_order = "linear" + end + elseif type(state.frame_order) == "table" then + if not state.frame_order[1] then + state.frame_order = "linear" + end + else + state.frame_order = "linear" + end self.a_states[key] = state end end function StateSprite:animate() - local new_frame = self.state.start_pos.x + math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames + local new_frame + if type(self.state.frame_order) == "table" then + self.current_animation.frame_index = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames + new_frame = self.state.frame_order[self.current_animation.frame_index] or self.current_animation.current + elseif self.state.frame_order == "linear" then + new_frame = self.state.start_pos.x + math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames + elseif self.state.frame_order == "random" then + new_frame = math.random(0, self.current_animation.frames - 1) + end local _x = new_frame % self.atlas.columns local _y = math.floor(new_frame / self.atlas.columns) if new_frame ~= self.current_animation.current then From c3f223df1011630338d7b05dc6d4cb43d0edd1b9 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Oct 2025 22:47:41 +0200 Subject: [PATCH 03/14] Incorrect height/width function name *Title --- src/game_object.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index f947b8773..4b8410dbd 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -447,8 +447,8 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. self.image = love.graphics.newImage(self.image_data, { mipmaps = true, dpiscale = G.SETTINGS.GRAPHICS.texture_scaling }) - self.columns = self.image:get_width() / self.px - self.rows = self.image:get_height() / self.py + self.columns = self.image:getWidth() / self.px + self.rows = self.image:getHeight() / self.py G[atlas_table_map[self.atlas_table]][self.key_noloc or self.key] = self From b7259bbb83f46af4dd7f9c1e0b82a2e94d56b333 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Oct 2025 22:55:29 +0200 Subject: [PATCH 04/14] Added `self.state == nil` check *Title --- src/game_object.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/game_object.lua b/src/game_object.lua index 4b8410dbd..f2895a9bf 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -3905,6 +3905,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end function StateSprite:animate() + if not self.state then return end local new_frame if type(self.state.frame_order) == "table" then self.current_animation.frame_index = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames From f5f5565e2ab4218e68b87e1a3cf2a476fa4a0398 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Thu, 9 Apr 2026 01:49:29 +0200 Subject: [PATCH 05/14] Moved code to own file *TItle */+Also WIP tried to allow default args for StateSprite creation --- lsp_def/classes/atlas.lua | 2 +- src/game_object.lua | 162 +--------------------------- src/game_objects/state_sprite.lua | 170 ++++++++++++++++++++++++++++++ src/utils.lua | 11 +- 4 files changed, 181 insertions(+), 164 deletions(-) create mode 100644 src/game_objects/state_sprite.lua diff --git a/lsp_def/classes/atlas.lua b/lsp_def/classes/atlas.lua index 14a551d44..3e1cfd0c7 100644 --- a/lsp_def/classes/atlas.lua +++ b/lsp_def/classes/atlas.lua @@ -6,7 +6,7 @@ ---@field px? string|number Width of individual sprites using this atlas. ---@field py? string|number Height of individual sprite using this atlas. ---@field path? string Name of the image file, including extension. ----@field atlas_table? "ASSET_ATLAS"|"ANIMATION_ATLAS"|"ASSET_IMAGES"|string Type of atlas. `ASSET_ATLAS`: non-animated sprites, `ANIMATION_ATLAS`: animated sprites, `ASSET_IMAGES`: anything other image, e.g. logos. +---@field atlas_table? "ASSET_ATLAS"|"ANIMATION_ATLAS"|"ASSET_IMAGES"|"STATE_ATLAS"|string Type of atlas. `ASSET_ATLAS`: non-animated sprites, `ANIMATION_ATLAS`: animated sprites, `STATE_ATLAS`: StateSprites, `ASSET_IMAGES`: anything other image, e.g. logos. ---@field frames? number Number of frames in the animation. ---@field columns? number Number of columns (= sprites horizontally). ---@field rows? number Number of rows (= sprites vertically). diff --git a/src/game_object.lua b/src/game_object.lua index 869fe96f7..61a047874 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -3998,168 +3998,10 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. ------------------------------------------------------------------------------------------------- - ------- API CODE Object.Node.Moveable.Sprite.AnimatedSprite.StateSprite + ----- API IMPORT Object.Node.Moveable.Sprite.AnimatedSprite.StateSprite ------------------------------------------------------------------------------------------------- - - StateSprite = AnimatedSprite:extend() - - -- Form of param [states] is; - --[[ - { - [state_name] = { - start_pos = { x/y = [0..n-1 for n columns/rows in sprite atlas] }, - (frames = [amount of frames] |OR| end_pos = { [same as start_pos] }), - frame_order = "linear" |OR| "random" |OR| {1: x, 2: y, .. n: m} - (optional) flipped_h/flipped_v = true - }, - ... - } - ]] - -- Example; - --[[ - { - sleepy = { - start_pos = {x = 0, y = 0}, - end_pos = {x = 3} (y is set to start_pos.y) - }, - wakey = { - start_pos = {x = 4}, (y is set to 0) - frames = 4 (end_pos is set to start_pos with .x + frames) - }, - lookey = { - flipped_h = true, (start_pos is set to {x = 0, y = 0}, end_pos is set to start_pos => this state is a single frame "animation" at x = 0, y = 0, and flipped horizontally and vertically) - flipped_v = true - } - } - ]] - -- To change state, call StateSprite:set_state(state_name) - function StateSprite:init(X, Y, W, H, new_sprite_atlas, states, start_state, states_offset) - AnimatedSprite.init(self, X, Y, W, H, new_sprite_atlas, {x=0, y=0}) - - self.states_offset = states_offset and {x = states_offset.x or 0, y = states_offset.y or 0} or {x = 0, y = 0} - - self:load_states(states) - self:set_state(start_state) - self.state_name = start_state - - self.flipped_h = false - self.flipped_v = false - - if getmetatable(self) == StateSprite then - table.insert(G.I.SPRITE, self) - end - end - - function StateSprite:set_state(state) - local a_state = self.a_states[state] - if a_state and self.state ~= a_state then - self.state = a_state - self:set_sprite_pos({x = self.state.start_pos.x + self.states_offset.x, y = self.state.start_pos.y + self.states_offset.y}) - self.flipped_h = self.state.flipped_h - self.flipped_v = self.state.flipped_v - end - end - - function StateSprite:load_states(states) - self.a_states = {} - for key, state in ipairs(states) do - state.start_pos = state.start_pos and {x = state.start_pos.x or 0, y = state.start_pos.y or 0} or {x = 0, y = 0} - state.end_pos = state.end_pos and {x = state.end_pos.x or state.start_pos.x + (state.frames or 0), y = state.end_pos.y or state.start_pos.y} or state.start_pos - if type(state.frame_order) == "string" then - local keymap = { - linear=true, - random=true - } - if not keymap[state.frame_order:lower()] then - state.frame_order = "linear" - end - elseif type(state.frame_order) == "table" then - if not state.frame_order[1] then - state.frame_order = "linear" - end - else - state.frame_order = "linear" - end - self.a_states[key] = state - end - end - - function StateSprite:animate() - if not self.state then return end - local new_frame - if type(self.state.frame_order) == "table" then - self.current_animation.frame_index = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames - new_frame = self.state.frame_order[self.current_animation.frame_index] or self.current_animation.current - elseif self.state.frame_order == "linear" then - new_frame = self.state.start_pos.x + math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames - elseif self.state.frame_order == "random" then - new_frame = math.random(0, self.current_animation.frames - 1) - end - local _x = new_frame % self.atlas.columns - local _y = math.floor(new_frame / self.atlas.columns) - if new_frame ~= self.current_animation.current then - self.current_animation.current = new_frame - -- self.frame_offset = math.floor(self.animation.w*(self.current_animation.current)) - self.sprite:setViewport( - _x, - self.animation.h*self.animation.y + _y, -- Equivalent to; self.state.start_pos.y + _y (I think at least) - self.animation.w, - self.animation.h) - end - if self.float then - self.T.r = 0.02*math.sin(2*G.TIMERS.REAL+self.T.x) - self.offset.y = -(1+0.3*math.sin(0.666*G.TIMERS.REAL+self.T.y))*self.shadow_parrallax.y - self.offset.x = -(0.7+0.2*math.sin(0.666*G.TIMERS.REAL+self.T.x))*self.shadow_parrallax.x - end - end - - function StateSprite:set_sprite_pos(sprite_pos) - self.animation = { - x = sprite_pos and sprite_pos.x or 0, - y = sprite_pos and sprite_pos.y or 0, - frames = self.state and (self.state.end_pos.x - self.state.start_pos.x) or 1, current = 0, - w = self.scale.x, h = self.scale.y - } - - self.frame_offset = 0 -- Unused - - self.current_animation = { - current = 0, - frames = self.animation.frames, - w = self.animation.w, - h = self.animation.h} - - self.image_dims = self.image_dims or {} - self.image_dims[1], self.image_dims[2] = self.atlas.image:getDimensions() - - self.sprite = love.graphics.newQuad( - 0, - self.animation.h*self.animation.y, - self.animation.w, - self.animation.h, - self.image_dims[1], self.image_dims[2] - ) - self.offset_seconds = G.TIMERS.REAL - end - - function StateSprite:draw_self() - if not self.states.visible then return end - - prep_draw(self, 1) - love.graphics.scale(1/self.scale_mag) - love.graphics.setColor(G.C.WHITE) - love.graphics.draw( - self.atlas.image, - self.sprite, - 0 ,0, - 0, - self.VT.w/(self.T.w) * (self.flipped_h and -1 or 1), - self.VT.h/(self.T.h) * (self.flipped_v and -1 or 1) - ) - love.graphics.pop() - end - + assert(load(NFS.read(SMODS.path..'src/game_objects/state_sprite.lua'), ('=[SMODS _ "src/game_objects/state_sprite.lua"]')))() ------------------------------------------------------------------------------------------------- ----- API IMPORT GameObject.DrawStep diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua new file mode 100644 index 000000000..af655921c --- /dev/null +++ b/src/game_objects/state_sprite.lua @@ -0,0 +1,170 @@ +StateSprite = AnimatedSprite:extend() + +-- Form of param [states] is; +--[[ +{ + [state_name] = { + start_pos = { x/y = [0..n-1 for n columns/rows in sprite atlas] }, + (frames = [amount of frames] |OR| end_pos = { [same as start_pos] }), + frame_order = "linear" |OR| "random" |OR| {1: x, 2: y, .. n: m} + (optional) flipped_h/flipped_v = true + }, + ... +} +]] +-- Example; +--[[ +{ + sleepy = { + start_pos = {x = 0, y = 0}, + end_pos = {x = 3} (y is set to start_pos.y) + }, + wakey = { + start_pos = {x = 4}, (y is set to 0) + frames = 4 (end_pos is set to start_pos with .x + frames) + }, + lookey = { + flipped_h = true, (start_pos is set to {x = 0, y = 0}, end_pos is set to start_pos => this state is a single frame "animation" at x = 0, y = 0, and flipped horizontally and vertically) + flipped_v = true + } +} +]] +-- To change state, call StateSprite:set_state(state_name) +function StateSprite:init(X, Y, W, H, new_sprite_atlas, states_offset, states, start_state) + AnimatedSprite.init(self, X, Y, W, H, new_sprite_atlas, {x=0, y=0}) + + self.states_offset = states_offset and {x = states_offset.x or 0, y = states_offset.y or 0} or {x = 0, y = 0} + + if states then + self:load_states(states) + self:set_state(start_state) + self.state_name = start_state + end + + self.flipped_h = false + self.flipped_v = false + + if getmetatable(self) == StateSprite then + table.insert(G.I.SPRITE, self) + end +end + +function StateSprite:set_state(state) + local a_state = self.a_states[state] + if a_state and self.state ~= a_state then + self.state = a_state + self:set_sprite_pos({x = self.state.start_pos.x + self.states_offset.x, y = self.state.start_pos.y + self.states_offset.y}) + self.flipped_h = self.state.flipped_h + self.flipped_v = self.state.flipped_v + return true + end + return false +end + +function StateSprite:load_states(states) + self.a_states = {} + for key, state in ipairs(states) do + state.start_pos = state.start_pos and {x = state.start_pos.x or 0, y = state.start_pos.y or 0} or {x = 0, y = 0} + state.end_pos = state.end_pos and {x = state.end_pos.x or state.start_pos.x + (state.frames or 0), y = state.end_pos.y or state.start_pos.y} or state.start_pos + if type(state.frame_order) == "string" then + local keymap = { + linear=true, + random=true + } + if not keymap[state.frame_order:lower()] then + state.frame_order = "linear" + end + elseif type(state.frame_order) == "table" then + if not state.frame_order[1] then + state.frame_order = "linear" + end + else + state.frame_order = "linear" + end + self.a_states[key] = state + end +end + +function StateSprite:animate() + if not self.state then return end + local new_frame + if type(self.state.frame_order) == "table" then + self.current_animation.frame_index = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames + new_frame = self.state.frame_order[self.current_animation.frame_index] or self.current_animation.current + elseif self.state.frame_order == "linear" then + new_frame = self.state.start_pos.x + math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames + elseif self.state.frame_order == "random" then + new_frame = math.random(0, self.current_animation.frames - 1) + end + local _x = new_frame % self.atlas.columns + local _y = math.floor(new_frame / self.atlas.columns) + if new_frame ~= self.current_animation.current then + self.current_animation.current = new_frame + -- self.frame_offset = math.floor(self.animation.w*(self.current_animation.current)) + self.sprite:setViewport( + _x, + self.animation.h*self.animation.y + _y, -- Equivalent to; self.state.start_pos.y + _y (I think at least) + self.animation.w, + self.animation.h) + end + if self.float then + self.T.r = 0.02*math.sin(2*G.TIMERS.REAL+self.T.x) + self.offset.y = -(1+0.3*math.sin(0.666*G.TIMERS.REAL+self.T.y))*self.shadow_parrallax.y + self.offset.x = -(0.7+0.2*math.sin(0.666*G.TIMERS.REAL+self.T.x))*self.shadow_parrallax.x + end +end + +function StateSprite:set_sprite_pos(sprite_pos) + self.animation = { + x = sprite_pos and sprite_pos.x or 0, + y = sprite_pos and sprite_pos.y or 0, + frames = self.state and (self.state.end_pos.x - self.state.start_pos.x) or 1, current = 0, + w = self.scale.x, h = self.scale.y + } + + self.frame_offset = 0 -- Unused + + self.current_animation = { + current = 0, + frames = self.animation.frames, + w = self.animation.w, + h = self.animation.h} + + self.image_dims = self.image_dims or {} + self.image_dims[1], self.image_dims[2] = self.atlas.image:getDimensions() + + self.sprite = love.graphics.newQuad( + 0, + self.animation.h*self.animation.y, + self.animation.w, + self.animation.h, + self.image_dims[1], self.image_dims[2] + ) + self.offset_seconds = G.TIMERS.REAL +end + +function StateSprite:draw_self() + if not self.states.visible then return end + + prep_draw(self, 1) + love.graphics.scale(1/self.scale_mag) + love.graphics.setColor(G.C.WHITE) + love.graphics.draw( + self.atlas.image, + self.sprite, + 0 ,0, + 0, + self.VT.w/(self.T.w) * (self.flipped_h and -1 or 1), + self.VT.h/(self.T.h) * (self.flipped_v and -1 or 1) + ) + love.graphics.pop() +end + + +function Card:set_sprite_state(new_state) + if self.children.center:is(StateSprite) then + return self.children.center:set_state(new_state) + else + sendWarnMessage("Card:card_set_sprite_state() called on card with no StateSprite", "utils") + end +end \ No newline at end of file diff --git a/src/utils.lua b/src/utils.lua index fd5117350..fb72c4676 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -3583,7 +3583,7 @@ end function SMODS.get_atlas(atlas_key) - return G.ASSET_ATLAS[atlas_key] or G.ANIMATION_ATLAS[atlas_key] + return G.ASSET_ATLAS[atlas_key] or G.ANIMATION_ATLAS[atlas_key] -- atlas.atlas_table = STATE_ATLAS -> also stored in G.ANIMATION_ATLAS end function SMODS.get_atlas_sprite_class(atlas_key) @@ -3591,15 +3591,20 @@ function SMODS.get_atlas_sprite_class(atlas_key) local class_map = { ASSET_ATLAS = Sprite, ANIMATION_ATLAS = AnimatedSprite, + STATE_ATLAS = StateSprite, } return class_map[atlas.atlas_table] or Sprite end -function SMODS.create_sprite(X, Y, W, H, atlas, pos) +function SMODS.create_sprite(X, Y, W, H, atlas, pos, ...) local atlas_key = (type(atlas) == "string" and atlas) or (type(atlas) == "table" and (atlas.key or atlas.name)) atlas = SMODS.get_atlas(atlas_key) assert(atlas, "SMODS.create_sprite called with invalid atlas key: "..atlas_key) - return SMODS.get_atlas_sprite_class(atlas_key)(X, Y, W, H, atlas, pos) + local sprite_class = SMODS.get_atlas_sprite_class(atlas_key) + if sprite_class == StateSprite then + return sprite_class(X, Y, W, H, atlas, pos, ...) + end + return sprite_class(X, Y, W, H, atlas, pos) end local animate = AnimatedSprite.animate From 87ad08619b405a754bfb9f5ab218c634b6d97495 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Fri, 10 Apr 2026 22:58:47 +0200 Subject: [PATCH 06/14] Added (untested) support for `sprite_args` field/param for Center sprite creation +/*Title --- lovely/blind.toml | 2 +- lovely/create_sprite.toml | 4 +-- src/game_object.lua | 9 +++--- src/game_objects/state_sprite.lua | 50 ++++++++++++++++++++++++------- src/overrides.lua | 14 ++++----- src/utils.lua | 4 +-- 6 files changed, 56 insertions(+), 27 deletions(-) diff --git a/lovely/blind.toml b/lovely/blind.toml index 9d1771fa1..4c5476e9b 100644 --- a/lovely/blind.toml +++ b/lovely/blind.toml @@ -47,7 +47,7 @@ position = 'before' match_indent = true payload = ''' local obj = self.config.blind -self.children.animatedSprite = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, obj.atlas or 'blind_chips', obj.pos or G.P_BLINDS.bl_small.pos) +self.children.animatedSprite = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, obj.atlas or 'blind_chips', obj.pos or G.P_BLINDS.bl_small.pos, obj.sprite_args) self.children.animatedSprite.states = self.states ''' diff --git a/lovely/create_sprite.toml b/lovely/create_sprite.toml index fb427dbca..9001785ed 100644 --- a/lovely/create_sprite.toml +++ b/lovely/create_sprite.toml @@ -60,7 +60,7 @@ pattern = ''' ''' position = 'at' payload = ''' -local tag_sprite = SMODS.create_sprite(0, 0, _size*1, _size*1, SMODS.get_atlas((not self.hide_ability) and G.P_TAGS[self.key].atlas or "tags"), (self.hide_ability) and G.tag_undiscovered.pos or self.pos) +local tag_sprite = SMODS.create_sprite(0, 0, _size*1, _size*1, SMODS.get_atlas((not self.hide_ability) and G.P_TAGS[self.key].atlas or "tags"), (self.hide_ability) and G.tag_undiscovered.pos or self.pos, self.sprite_args) ''' match_indent = true # functions/common_events.lua @@ -72,7 +72,7 @@ pattern = ''' ''' position = 'at' payload = ''' -local blind_sprite = SMODS.create_sprite(0, 0, 1.2, 1.2, obj.atlas or 'blind_chips', copy_table(G.GAME.blind.pos)) +local blind_sprite = SMODS.create_sprite(0, 0, 1.2, 1.2, obj.atlas or 'blind_chips', copy_table(G.GAME.blind.pos), obj.sprite_args) ''' match_indent = true [[patches]] diff --git a/src/game_object.lua b/src/game_object.lua index 61a047874..4f8b7d331 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -713,7 +713,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. if not self.injected then -- Sticker sprites (stake_ prefix is removed for vanilla compatiblity) if self.sticker_pos ~= nil then - G.shared_stickers[self.key:sub(7)] = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, SMODS.get_atlas(self.sticker_atlas) or SMODS.get_atlas("stickers"), self.sticker_pos) + G.shared_stickers[self.key:sub(7)] = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, SMODS.get_atlas(self.sticker_atlas) or SMODS.get_atlas("stickers"), self.sticker_pos, self.sprite_args) G.sticker_map[self.key] = self.key:sub(7) else G.sticker_map[self.key] = nil @@ -1788,7 +1788,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. process_loc_text = function() end, inject = function(self) if self.overlay_pos then - self.overlay_sprite = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, self.atlas, self.overlay_pos) + self.overlay_sprite = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, self.atlas, self.overlay_pos, self.sprite_args) self.no_overlay = true end end, @@ -1896,7 +1896,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. }, inject = function(self) G.P_SEALS[self.key] = self - G.shared_seals[self.key] = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, SMODS.get_atlas(self.atlas) or SMODS.get_atlas('centers'), self.pos) + G.shared_seals[self.key] = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, SMODS.get_atlas(self.atlas) or SMODS.get_atlas('centers'), self.pos, self.sprite_args) self.badge_to_key[self.key:lower() .. '_seal'] = self.key SMODS.insert_pool(G.P_CENTER_POOLS[self.set], self) self.rng_buffer[#self.rng_buffer + 1] = self.key @@ -3136,7 +3136,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. self.order = #self.obj_buffer end, inject = function(self) - self.sticker_sprite = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, self.atlas, self.pos) + self.sticker_sprite = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, self.atlas, self.pos, self.sprite_args) G.shared_stickers[self.key] = self.sticker_sprite end, -- relocating sticker checks to here, so if the sticker has different checks than default @@ -3271,6 +3271,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. set = 'Enhanced', class_prefix = 'm', atlas = 'centers', + sprite_args = nil, -- Used by StateSprite (when the atlas' atlas_table == "STATE_ATLAS"), {states=..., states_offset=..., default_state=...} pos = { x = 0, y = 0 }, required_params = { 'key', diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua index af655921c..1956cfc7f 100644 --- a/src/game_objects/state_sprite.lua +++ b/src/game_objects/state_sprite.lua @@ -30,15 +30,14 @@ StateSprite = AnimatedSprite:extend() } ]] -- To change state, call StateSprite:set_state(state_name) -function StateSprite:init(X, Y, W, H, new_sprite_atlas, states_offset, states, start_state) +function StateSprite:init(X, Y, W, H, new_sprite_atlas, args) AnimatedSprite.init(self, X, Y, W, H, new_sprite_atlas, {x=0, y=0}) - self.states_offset = states_offset and {x = states_offset.x or 0, y = states_offset.y or 0} or {x = 0, y = 0} - - if states then - self:load_states(states) - self:set_state(start_state) - self.state_name = start_state + if args and next(args.states) then + self.sprite_args = args + self.states_offset = args.states_offset and {x = args.states_offset.x or 0, y = args.states_offset.y or 0} or {x = 0, y = 0} + self:load_states(args.states) + self:set_state(args.default_state or next(args.states)) end self.flipped_h = false @@ -96,14 +95,14 @@ function StateSprite:animate() elseif self.state.frame_order == "random" then new_frame = math.random(0, self.current_animation.frames - 1) end - local _x = new_frame % self.atlas.columns - local _y = math.floor(new_frame / self.atlas.columns) + local _x = self.animation.w * (new_frame % self.atlas.columns) + local _y = self.animation.h * math.floor(new_frame / self.atlas.columns) if new_frame ~= self.current_animation.current then self.current_animation.current = new_frame -- self.frame_offset = math.floor(self.animation.w*(self.current_animation.current)) self.sprite:setViewport( _x, - self.animation.h*self.animation.y + _y, -- Equivalent to; self.state.start_pos.y + _y (I think at least) + _y, self.animation.w, self.animation.h) end @@ -167,4 +166,33 @@ function Card:set_sprite_state(new_state) else sendWarnMessage("Card:card_set_sprite_state() called on card with no StateSprite", "utils") end -end \ No newline at end of file +end + + +SMODS.Atlas { + key = "test", + path = "", + px = 71, + py = 95, + atlas_table = "STATE_ATLAS" +} + + +SMODS.Joker { + key = "test", + atlas = "test", + pos = {0, 0}, + sprite_args = { + states = { + red = { + start_pos = { x = 0, y = 0}, + frames = 2 + }, + blue = { + start_pos = { x = 2, y = 0}, + frames = 2 + } + }, + default_state = "red" + } +} \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index c38afa161..94a143452 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -132,7 +132,7 @@ function create_UIBox_your_collection_blinds(exit) local row, col = 1, 1 for k, v in ipairs(blind_tab) do local atlas_key = v.discovered and v.atlas or 'blind_chips' - local temp_blind = SMODS.create_sprite(G.your_collection[row].T.x + G.your_collection[row].T.w/2, G.your_collection[row].T.y, 1.3, 1.3, atlas_key, v.discovered and v.pos or G.b_undiscovered.pos) + local temp_blind = SMODS.create_sprite(G.your_collection[row].T.x + G.your_collection[row].T.w/2, G.your_collection[row].T.y, 1.3, 1.3, atlas_key, v.discovered and v.pos or G.b_undiscovered.pos, v.sprite_args) temp_blind.states.click.can = false temp_blind.states.drag.can = false temp_blind.states.hover.can = true @@ -322,7 +322,7 @@ function G.FUNCS.your_collection_blinds_page(args) local row, col = 1, 1 for k, v in ipairs(blind_tab) do local atlas_key = v.discovered and v.atlas or 'blind_chips' - local temp_blind = SMODS.create_sprite(G.your_collection[row].T.x + G.your_collection[row].T.w/2, G.your_collection[row].T.y, 1.3, 1.3, atlas_key, v.discovered and v.pos or G.b_undiscovered.pos) + local temp_blind = SMODS.create_sprite(G.your_collection[row].T.x + G.your_collection[row].T.w/2, G.your_collection[row].T.y, 1.3, 1.3, atlas_key, v.discovered and v.pos or G.b_undiscovered.pos, v.sprite_args) temp_blind.states.click.can = false temp_blind.states.drag.can = false temp_blind.states.hover.can = true @@ -1798,7 +1798,7 @@ function Card:set_sprites(_center, _front) if _front then local _atlas, _pos = get_front_spriteinfo(_front) if self.children.front then self.children.front:remove() end - self.children.front = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, _atlas, _pos) + self.children.front = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, _atlas, _pos, _center.sprite_args) self.children.front.states.hover = self.states.hover self.children.front.states.click = self.states.click self.children.front.states.drag = self.states.drag @@ -1833,9 +1833,9 @@ function Card:set_sprites(_center, _front) self.children.center = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, atlas, pos) elseif _center.set == 'Joker' or _center.consumeable or _center.set == 'Voucher' then local atlas_key = _center[G.SETTINGS.colourblind_option and 'hc_atlas' or 'lc_atlas'] or _center.atlas or _center.set - self.children.center = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, atlas_key, _center.pos or {x=0, y=0}) + self.children.center = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, atlas_key, _center.pos or {x=0, y=0}, _center.sprite_args) else - self.children.center = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, _center.atlas or 'centers', _center.pos) + self.children.center = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, _center.atlas or 'centers', _center.pos, _center.sprite_args) end self.children.center.states.hover = self.states.hover self.children.center.states.click = self.states.click @@ -1862,7 +1862,7 @@ function Card:set_sprites(_center, _front) if _center.soul_pos or _center[G.SETTINGS.colourblind_option and 'hc_soul_atlas' or 'lc_soul_atlas'] or _center.soul_atlas then if self.children.floating_sprite then self.children.floating_sprite:remove() end local atlas_key = _center[G.SETTINGS.colourblind_option and 'hc_soul_atlas' or 'lc_soul_atlas'] or _center.soul_atlas or _center[G.SETTINGS.colourblind_option and 'hc_atlas' or 'lc_atlas'] or _center.atlas or _center.set - self.children.floating_sprite = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, atlas_key, _center.soul_pos or { x = 0, y = 0 }) + self.children.floating_sprite = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, atlas_key, _center.soul_pos or { x = 0, y = 0 }, _center.sprite_args) self.children.floating_sprite.role.draw_major = self self.children.floating_sprite.states.hover.can = false self.children.floating_sprite.states.click.can = false @@ -1870,7 +1870,7 @@ function Card:set_sprites(_center, _front) if self.children.back then self.children.back:remove() end local atlas_key = (G.GAME.viewed_back or G.GAME.selected_back) and ((G.GAME.viewed_back or G.GAME.selected_back)[G.SETTINGS.colourblind_option and 'hc_atlas' or 'lc_atlas'] or (G.GAME.viewed_back or G.GAME.selected_back).atlas) or 'centers' - self.children.back = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, atlas_key, self.params.bypass_back or (self.playing_card and G.GAME[self.back].pos or G.P_CENTERS['b_red'].pos)) + self.children.back = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, atlas_key, self.params.bypass_back or (self.playing_card and G.GAME[self.back].pos or G.P_CENTERS['b_red'].pos), _center.sprite_args) self.children.back.states.hover = self.states.hover self.children.back.states.click = self.states.click self.children.back.states.drag = self.states.drag diff --git a/src/utils.lua b/src/utils.lua index fb72c4676..2b0940eeb 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -3596,13 +3596,13 @@ function SMODS.get_atlas_sprite_class(atlas_key) return class_map[atlas.atlas_table] or Sprite end -function SMODS.create_sprite(X, Y, W, H, atlas, pos, ...) +function SMODS.create_sprite(X, Y, W, H, atlas, pos, sprite_args) local atlas_key = (type(atlas) == "string" and atlas) or (type(atlas) == "table" and (atlas.key or atlas.name)) atlas = SMODS.get_atlas(atlas_key) assert(atlas, "SMODS.create_sprite called with invalid atlas key: "..atlas_key) local sprite_class = SMODS.get_atlas_sprite_class(atlas_key) if sprite_class == StateSprite then - return sprite_class(X, Y, W, H, atlas, pos, ...) + return sprite_class(X, Y, W, H, atlas, pos, sprite_args) end return sprite_class(X, Y, W, H, atlas, pos) end From d553f7b5d26ec17897fde6f8f01d62737eadd047 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Fri, 10 Apr 2026 22:59:33 +0200 Subject: [PATCH 07/14] Marked Todo *Title --- src/game_objects/state_sprite.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua index 1956cfc7f..225b1acaa 100644 --- a/src/game_objects/state_sprite.lua +++ b/src/game_objects/state_sprite.lua @@ -84,6 +84,7 @@ function StateSprite:load_states(states) end end +-- Todo : check this bs function StateSprite:animate() if not self.state then return end local new_frame From a3f3e6bb830f0e7807fc582466a78d3d205ed8d2 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 11 Apr 2026 00:14:10 +0200 Subject: [PATCH 08/14] Tested `StateSprite` and made 'em functional *Title, yipee! --- assets/1x/test.png | Bin 0 -> 1047 bytes assets/2x/test.png | Bin 0 -> 1626 bytes src/game_objects/state_sprite.lua | 37 +++++++++++++++++++----------- src/overrides.lua | 2 +- 4 files changed, 24 insertions(+), 15 deletions(-) create mode 100644 assets/1x/test.png create mode 100644 assets/2x/test.png diff --git a/assets/1x/test.png b/assets/1x/test.png new file mode 100644 index 0000000000000000000000000000000000000000..b42a2da6d9e23e1a015cb1cde97ea4ea3d90b5d0 GIT binary patch literal 1047 zcmeAS@N?(olHy`uVBq!ia0y~yV3Yx}<2l%Xq?LcgG$6%T9OUlAu-^OVJ?}*3tzIa(mdF3r#q{>HE0#8!osD0~%)7ZzvXt-qn+rW*E$Kfu zh680ZZB_$iMCM%u%5a^3b)hG%E&b)h&B?-6F6!213!O_gL79nRCDUXp!gHLSzIfB+ zyFzAe>_*P1i#KbVCAyVNlPNFHaXR|qO`q=yiMe+-a*8hAY;E>1t7Mt{Hm@9~r7uD{ zeOHLg)dK2R6s&6Y(5qyb+}5ZZr`8uC-M%XX=4JtPEDF{&dwBDOk9Ee?8!dA$u9TX+ z!PqJ^S-8w?c~;ILlX}u`e)-2JzWYuxCHywUoZ?t@OdwzLWKhuZRr=Pyt zl;`y9)Js(IP<2>eEAx__A#d|utEH@&gGT;&cWPrngNeWR?$^6GwZr|cmkNiio3rk< z+L{>4*z3MO=1xt1$dJjx?xAeqFl+fg+gpEs`bK<|S^oR;>&vCl?;!Th53aPns zH*$t9{_K;}cM~Y%w%jSlY3qxS4&N1GbGbHhUS0G#C#O#oDC4?3D935)ix6Ps3eEKb z>RD8*YIc0p0~yvu!G>muwW%$UuP&>wYELt!)w zK%G}Vtl{z1DE_mV;jX1Y`Z=bO=LhCka$lIsT>MPOL9&q%d(P8%tM0aEQ>a{@cs)pg Mr>mdKI;Vst0Jx~P1aTS~NE^y;2_z$!w(Iw4@O)COj;BUTCO9xoj=U+HO|@=n^H{`=RQN^Y0^ z2d_?#ud8GP8b%k;P_z7AedEPB@p0!&|NlL?|K+5*{d+@e&EwbIoLt`~0}e{(dgd(r0po84!{T+`(@19dJIgD86PYL|P}PEAeA+$Gaz zPnvYu4bHigq3V8+k>L@~)@|=LMuC)^k6e3a%9J_J*M#MHd7Uc`(>7lk=+)Q6%y2|9 zDfMph*P`H{qIa9Y27Hg)3sU-fO*diVo z28F&xpr>Abt6q6~cJk6YKt+~er$9PjoY&DZpdd~LISi!!+{68ockO+deJxsMyLs)| zs_w_%maUJ>FT8W_%hgw6;9vyVF2jth32cdB0YigD``*9@e`jwz^GUAnd;T$_)72LD z_Di1DdH=BDf11wL_YXmFj!Q2aFzn#o!>8zZ$rt&Gx=Ks=zv-Xm2exmOT+$eohOlyfkd*Ud^3HS8o0eRW4apt8LjIxfPU1kFSXWrRU@@U2|2{ z=Z5Z6XLEz|&mXSvqP*a|b-}2l^{zWH;7pi#ZO5d~6N`%FRi7(AeP-u%&i&Nc+9`AT zt6&D))r#K>Op50tQOPfQ5Fe&@O#Rw()vK@Q^06#c_nn!^TY<@X%f_gal1rgBEWCEj z_!2P1Z|?sBlzO8KGUvC4zU5+=4NqRJGOWHBnDcAK-WM8XwP)f=L}uHi>02!pn>`5> zDl?N>uS$TVlBH0|GeJ3C;9PeqG;DD>(1d4Kb_10azlsH#VEI+gYBSV?&WKFit73Ak zX}()q-4{j%=ROR~xE2y!5yW*?3>ZqZ1PuZInT)bF?cA^K8UwP&)78&qol`;+0MiDK Az5oCK literal 0 HcmV?d00001 diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua index 225b1acaa..d21101e41 100644 --- a/src/game_objects/state_sprite.lua +++ b/src/game_objects/state_sprite.lua @@ -30,10 +30,13 @@ StateSprite = AnimatedSprite:extend() } ]] -- To change state, call StateSprite:set_state(state_name) -function StateSprite:init(X, Y, W, H, new_sprite_atlas, args) +function StateSprite:init(X, Y, W, H, new_sprite_atlas, _pos, args) AnimatedSprite.init(self, X, Y, W, H, new_sprite_atlas, {x=0, y=0}) + args = args or {} - if args and next(args.states) then + if not args.states or not next(args.states) then + sendWarnMessage(string.format("StateSprite initialized without states, atlas = '%s'", new_sprite_atlas.name), "utils") + else self.sprite_args = args self.states_offset = args.states_offset and {x = args.states_offset.x or 0, y = args.states_offset.y or 0} or {x = 0, y = 0} self:load_states(args.states) @@ -50,7 +53,9 @@ end function StateSprite:set_state(state) local a_state = self.a_states[state] - if a_state and self.state ~= a_state then + if not a_state then + sendWarnMessage(string.format("StateSprite:set_state() called with invalid state '%s'", state), "utils") + elseif self.state ~= a_state then self.state = a_state self:set_sprite_pos({x = self.state.start_pos.x + self.states_offset.x, y = self.state.start_pos.y + self.states_offset.y}) self.flipped_h = self.state.flipped_h @@ -62,9 +67,9 @@ end function StateSprite:load_states(states) self.a_states = {} - for key, state in ipairs(states) do + for key, state in pairs(states) do state.start_pos = state.start_pos and {x = state.start_pos.x or 0, y = state.start_pos.y or 0} or {x = 0, y = 0} - state.end_pos = state.end_pos and {x = state.end_pos.x or state.start_pos.x + (state.frames or 0), y = state.end_pos.y or state.start_pos.y} or state.start_pos + state.frames = state.frames or ((state.end_pos or state.start_pos).x - state.start_pos.x + ((state.end_pos.y or state.start_pos).y - state.start_pos.y) * self.atlas.columns + 1) if type(state.frame_order) == "string" then local keymap = { linear=true, @@ -92,15 +97,14 @@ function StateSprite:animate() self.current_animation.frame_index = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames new_frame = self.state.frame_order[self.current_animation.frame_index] or self.current_animation.current elseif self.state.frame_order == "linear" then - new_frame = self.state.start_pos.x + math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames + new_frame = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames elseif self.state.frame_order == "random" then new_frame = math.random(0, self.current_animation.frames - 1) end - local _x = self.animation.w * (new_frame % self.atlas.columns) - local _y = self.animation.h * math.floor(new_frame / self.atlas.columns) + local _x = self.animation.w * ((self.states_offset.x + self.state.start_pos.x + new_frame) % self.atlas.columns) + local _y = self.animation.h * (self.states_offset.y + self.state.start_pos.y + math.floor(new_frame / self.atlas.columns)) if new_frame ~= self.current_animation.current then self.current_animation.current = new_frame - -- self.frame_offset = math.floor(self.animation.w*(self.current_animation.current)) self.sprite:setViewport( _x, _y, @@ -118,7 +122,7 @@ function StateSprite:set_sprite_pos(sprite_pos) self.animation = { x = sprite_pos and sprite_pos.x or 0, y = sprite_pos and sprite_pos.y or 0, - frames = self.state and (self.state.end_pos.x - self.state.start_pos.x) or 1, current = 0, + frames = self.state and self.state.frames or 1, current = 0, w = self.scale.x, h = self.scale.y } @@ -134,7 +138,7 @@ function StateSprite:set_sprite_pos(sprite_pos) self.image_dims[1], self.image_dims[2] = self.atlas.image:getDimensions() self.sprite = love.graphics.newQuad( - 0, + self.animation.w*self.animation.x, self.animation.h*self.animation.y, self.animation.w, self.animation.h, @@ -172,7 +176,7 @@ end SMODS.Atlas { key = "test", - path = "", + path = "test.png", px = 71, py = 95, atlas_table = "STATE_ATLAS" @@ -182,7 +186,7 @@ SMODS.Atlas { SMODS.Joker { key = "test", atlas = "test", - pos = {0, 0}, + pos = {x=0, y=0}, sprite_args = { states = { red = { @@ -195,5 +199,10 @@ SMODS.Joker { } }, default_state = "red" - } + }, + calculate = function (self, card, context) + if context.bababooey then + card.children.center:set_state(context.state) + end + end } \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index 94a143452..579a77a45 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -1798,7 +1798,7 @@ function Card:set_sprites(_center, _front) if _front then local _atlas, _pos = get_front_spriteinfo(_front) if self.children.front then self.children.front:remove() end - self.children.front = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, _atlas, _pos, _center.sprite_args) + self.children.front = SMODS.create_sprite(self.T.x, self.T.y, self.T.w, self.T.h, _atlas, _pos, _center and _center.sprite_args) self.children.front.states.hover = self.states.hover self.children.front.states.click = self.states.click self.children.front.states.drag = self.states.drag From 82355ae16c96fd027b5dacde0b2cded0f7de1592 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 11 Apr 2026 00:15:22 +0200 Subject: [PATCH 09/14] Removed testing stuff -Title --- assets/1x/test.png | Bin 1047 -> 0 bytes assets/2x/test.png | Bin 1626 -> 0 bytes src/game_objects/state_sprite.lua | 37 +----------------------------- 3 files changed, 1 insertion(+), 36 deletions(-) delete mode 100644 assets/1x/test.png delete mode 100644 assets/2x/test.png diff --git a/assets/1x/test.png b/assets/1x/test.png deleted file mode 100644 index b42a2da6d9e23e1a015cb1cde97ea4ea3d90b5d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1047 zcmeAS@N?(olHy`uVBq!ia0y~yV3Yx}<2l%Xq?LcgG$6%T9OUlAu-^OVJ?}*3tzIa(mdF3r#q{>HE0#8!osD0~%)7ZzvXt-qn+rW*E$Kfu zh680ZZB_$iMCM%u%5a^3b)hG%E&b)h&B?-6F6!213!O_gL79nRCDUXp!gHLSzIfB+ zyFzAe>_*P1i#KbVCAyVNlPNFHaXR|qO`q=yiMe+-a*8hAY;E>1t7Mt{Hm@9~r7uD{ zeOHLg)dK2R6s&6Y(5qyb+}5ZZr`8uC-M%XX=4JtPEDF{&dwBDOk9Ee?8!dA$u9TX+ z!PqJ^S-8w?c~;ILlX}u`e)-2JzWYuxCHywUoZ?t@OdwzLWKhuZRr=Pyt zl;`y9)Js(IP<2>eEAx__A#d|utEH@&gGT;&cWPrngNeWR?$^6GwZr|cmkNiio3rk< z+L{>4*z3MO=1xt1$dJjx?xAeqFl+fg+gpEs`bK<|S^oR;>&vCl?;!Th53aPns zH*$t9{_K;}cM~Y%w%jSlY3qxS4&N1GbGbHhUS0G#C#O#oDC4?3D935)ix6Ps3eEKb z>RD8*YIc0p0~yvu!G>muwW%$UuP&>wYELt!)w zK%G}Vtl{z1DE_mV;jX1Y`Z=bO=LhCka$lIsT>MPOL9&q%d(P8%tM0aEQ>a{@cs)pg Mr>mdKI;Vst0Jx~P1aTS~NE^y;2_z$!w(Iw4@O)COj;BUTCO9xoj=U+HO|@=n^H{`=RQN^Y0^ z2d_?#ud8GP8b%k;P_z7AedEPB@p0!&|NlL?|K+5*{d+@e&EwbIoLt`~0}e{(dgd(r0po84!{T+`(@19dJIgD86PYL|P}PEAeA+$Gaz zPnvYu4bHigq3V8+k>L@~)@|=LMuC)^k6e3a%9J_J*M#MHd7Uc`(>7lk=+)Q6%y2|9 zDfMph*P`H{qIa9Y27Hg)3sU-fO*diVo z28F&xpr>Abt6q6~cJk6YKt+~er$9PjoY&DZpdd~LISi!!+{68ockO+deJxsMyLs)| zs_w_%maUJ>FT8W_%hgw6;9vyVF2jth32cdB0YigD``*9@e`jwz^GUAnd;T$_)72LD z_Di1DdH=BDf11wL_YXmFj!Q2aFzn#o!>8zZ$rt&Gx=Ks=zv-Xm2exmOT+$eohOlyfkd*Ud^3HS8o0eRW4apt8LjIxfPU1kFSXWrRU@@U2|2{ z=Z5Z6XLEz|&mXSvqP*a|b-}2l^{zWH;7pi#ZO5d~6N`%FRi7(AeP-u%&i&Nc+9`AT zt6&D))r#K>Op50tQOPfQ5Fe&@O#Rw()vK@Q^06#c_nn!^TY<@X%f_gal1rgBEWCEj z_!2P1Z|?sBlzO8KGUvC4zU5+=4NqRJGOWHBnDcAK-WM8XwP)f=L}uHi>02!pn>`5> zDl?N>uS$TVlBH0|GeJ3C;9PeqG;DD>(1d4Kb_10azlsH#VEI+gYBSV?&WKFit73Ak zX}()q-4{j%=ROR~xE2y!5yW*?3>ZqZ1PuZInT)bF?cA^K8UwP&)78&qol`;+0MiDK Az5oCK diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua index d21101e41..437f988d0 100644 --- a/src/game_objects/state_sprite.lua +++ b/src/game_objects/state_sprite.lua @@ -89,7 +89,6 @@ function StateSprite:load_states(states) end end --- Todo : check this bs function StateSprite:animate() if not self.state then return end local new_frame @@ -171,38 +170,4 @@ function Card:set_sprite_state(new_state) else sendWarnMessage("Card:card_set_sprite_state() called on card with no StateSprite", "utils") end -end - - -SMODS.Atlas { - key = "test", - path = "test.png", - px = 71, - py = 95, - atlas_table = "STATE_ATLAS" -} - - -SMODS.Joker { - key = "test", - atlas = "test", - pos = {x=0, y=0}, - sprite_args = { - states = { - red = { - start_pos = { x = 0, y = 0}, - frames = 2 - }, - blue = { - start_pos = { x = 2, y = 0}, - frames = 2 - } - }, - default_state = "red" - }, - calculate = function (self, card, context) - if context.bababooey then - card.children.center:set_state(context.state) - end - end -} \ No newline at end of file +end \ No newline at end of file From 2211b0dea29a2a52d9d9865380f406f3bd498849 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 11 Apr 2026 00:51:31 +0200 Subject: [PATCH 10/14] Added `exit_to` and `frame_durations` fields to `StateSprite` states +Title, `exit_to` should allow automatically entering a new state once a previous one has finished one iteration, `frame_durations` allows stretching individual frames to be longer (or faster, I think?). ! This is untested + `frame_durations` required tracking individual frames' progress instead of the animation overall, shouldn't be too bad but who knows. --- src/game_objects/state_sprite.lua | 49 +++++++++++++++++++------------ 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua index 437f988d0..9e233042b 100644 --- a/src/game_objects/state_sprite.lua +++ b/src/game_objects/state_sprite.lua @@ -7,7 +7,9 @@ StateSprite = AnimatedSprite:extend() start_pos = { x/y = [0..n-1 for n columns/rows in sprite atlas] }, (frames = [amount of frames] |OR| end_pos = { [same as start_pos] }), frame_order = "linear" |OR| "random" |OR| {1: x, 2: y, .. n: m} - (optional) flipped_h/flipped_v = true + (optional) flipped_h/flipped_v = true, + (optional) exit_to = [state], + (optional) frame_durations = {1: 2.0, 2:...} }, ... } @@ -17,15 +19,17 @@ StateSprite = AnimatedSprite:extend() { sleepy = { start_pos = {x = 0, y = 0}, - end_pos = {x = 3} (y is set to start_pos.y) + end_pos = {x = 3} (y is set to start_pos.y) }, wakey = { - start_pos = {x = 4}, (y is set to 0) - frames = 4 (end_pos is set to start_pos with .x + frames) + start_pos = {x = 4}, (y is set to 0) + frames = 4, (end_pos is set to start_pos with .x + frames) + exit_to = "lookey", (after one iteration, sets state to this value) }, lookey = { flipped_h = true, (start_pos is set to {x = 0, y = 0}, end_pos is set to start_pos => this state is a single frame "animation" at x = 0, y = 0, and flipped horizontally and vertically) - flipped_v = true + flipped_v = true, + frame_durations = {[1] = 3.0} (the first frame lasts three times longer) } } ]] @@ -91,24 +95,29 @@ end function StateSprite:animate() if not self.state then return end - local new_frame - if type(self.state.frame_order) == "table" then - self.current_animation.frame_index = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames - new_frame = self.state.frame_order[self.current_animation.frame_index] or self.current_animation.current - elseif self.state.frame_order == "linear" then - new_frame = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % self.current_animation.frames - elseif self.state.frame_order == "random" then - new_frame = math.random(0, self.current_animation.frames - 1) + if self.state.exit_to and self.current_animation.elapsed >= self.current_animation.frames then + self:set_state(self.state.exit_to) end - local _x = self.animation.w * ((self.states_offset.x + self.state.start_pos.x + new_frame) % self.atlas.columns) - local _y = self.animation.h * (self.states_offset.y + self.state.start_pos.y + math.floor(new_frame / self.atlas.columns)) - if new_frame ~= self.current_animation.current then - self.current_animation.current = new_frame + local frame_finished = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % (self.state.frame_durations or {})[self.animation.current] or 1.0 + if frame_finished then + local new_frame + if type(self.state.frame_order) == "table" then + self.current_animation.frame_index = (self.current_animation.frame_index + 1) % self.current_animation.frames + new_frame = self.state.frame_order[self.current_animation.frame_index] or self.current_animation.current + elseif self.state.frame_order == "random" then + new_frame = math.random(0, self.current_animation.frames - 1) + end + local _x = self.animation.w * ((self.states_offset.x + self.state.start_pos.x + new_frame) % self.atlas.columns) + local _y = self.animation.h * (self.states_offset.y + self.state.start_pos.y + math.floor(new_frame / self.atlas.columns)) + self.current_animation.current = new_frame or ((self.current_animation.current + 1) % self.current_animation.frames) + self.current_animation.elapsed = self.current_animation.elapsed + 1 self.sprite:setViewport( _x, _y, self.animation.w, - self.animation.h) + self.animation.h + ) + self.offset_seconds = G.TIMERS.REAL end if self.float then self.T.r = 0.02*math.sin(2*G.TIMERS.REAL+self.T.x) @@ -131,7 +140,9 @@ function StateSprite:set_sprite_pos(sprite_pos) current = 0, frames = self.animation.frames, w = self.animation.w, - h = self.animation.h} + h = self.animation.h, + elapsed = 0, + } self.image_dims = self.image_dims or {} self.image_dims[1], self.image_dims[2] = self.atlas.image:getDimensions() From 120d18a3b07e816375c6160a66cff3e0751aab21 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 11 Apr 2026 00:54:59 +0200 Subject: [PATCH 11/14] 2 Oversights 2 Fixed *Title --- src/game_objects/state_sprite.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua index 9e233042b..3075985b2 100644 --- a/src/game_objects/state_sprite.lua +++ b/src/game_objects/state_sprite.lua @@ -98,7 +98,7 @@ function StateSprite:animate() if self.state.exit_to and self.current_animation.elapsed >= self.current_animation.frames then self:set_state(self.state.exit_to) end - local frame_finished = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % (self.state.frame_durations or {})[self.animation.current] or 1.0 + local frame_finished = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % (self.state.frame_durations or {})[self.current_animation.current] or 1.0 if frame_finished then local new_frame if type(self.state.frame_order) == "table" then @@ -107,10 +107,10 @@ function StateSprite:animate() elseif self.state.frame_order == "random" then new_frame = math.random(0, self.current_animation.frames - 1) end - local _x = self.animation.w * ((self.states_offset.x + self.state.start_pos.x + new_frame) % self.atlas.columns) - local _y = self.animation.h * (self.states_offset.y + self.state.start_pos.y + math.floor(new_frame / self.atlas.columns)) self.current_animation.current = new_frame or ((self.current_animation.current + 1) % self.current_animation.frames) self.current_animation.elapsed = self.current_animation.elapsed + 1 + local _x = self.animation.w * ((self.states_offset.x + self.state.start_pos.x + self.current_animation.current) % self.atlas.columns) + local _y = self.animation.h * (self.states_offset.y + self.state.start_pos.y + math.floor(self.current_animation.current / self.atlas.columns)) self.sprite:setViewport( _x, _y, From d2fc84e1ac98692a27153bd1e4f3b7c267cc9a26 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sun, 12 Apr 2026 20:00:43 +0200 Subject: [PATCH 12/14] Refactor / fix *Title, a bit cleaner now I think. --- src/game_objects/state_sprite.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua index 3075985b2..47a22fad6 100644 --- a/src/game_objects/state_sprite.lua +++ b/src/game_objects/state_sprite.lua @@ -9,7 +9,7 @@ StateSprite = AnimatedSprite:extend() frame_order = "linear" |OR| "random" |OR| {1: x, 2: y, .. n: m} (optional) flipped_h/flipped_v = true, (optional) exit_to = [state], - (optional) frame_durations = {1: 2.0, 2:...} + (optional) frame_durations = {1: 2, 2:...} }, ... } @@ -29,7 +29,7 @@ StateSprite = AnimatedSprite:extend() lookey = { flipped_h = true, (start_pos is set to {x = 0, y = 0}, end_pos is set to start_pos => this state is a single frame "animation" at x = 0, y = 0, and flipped horizontally and vertically) flipped_v = true, - frame_durations = {[1] = 3.0} (the first frame lasts three times longer) + frame_durations = {[1] = 3} (the first frame lasts three times longer) } } ]] @@ -98,7 +98,7 @@ function StateSprite:animate() if self.state.exit_to and self.current_animation.elapsed >= self.current_animation.frames then self:set_state(self.state.exit_to) end - local frame_finished = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds)) % (self.state.frame_durations or {})[self.current_animation.current] or 1.0 + local frame_finished = (math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds) / (self.current_animation.frame_duration or 1))) > 0 if frame_finished then local new_frame if type(self.state.frame_order) == "table" then @@ -109,6 +109,7 @@ function StateSprite:animate() end self.current_animation.current = new_frame or ((self.current_animation.current + 1) % self.current_animation.frames) self.current_animation.elapsed = self.current_animation.elapsed + 1 + self.current_animation.frame_duration = (self.state.frame_durations or {})[self.current_animation.current] or 1 local _x = self.animation.w * ((self.states_offset.x + self.state.start_pos.x + self.current_animation.current) % self.atlas.columns) local _y = self.animation.h * (self.states_offset.y + self.state.start_pos.y + math.floor(self.current_animation.current / self.atlas.columns)) self.sprite:setViewport( @@ -142,6 +143,7 @@ function StateSprite:set_sprite_pos(sprite_pos) w = self.animation.w, h = self.animation.h, elapsed = 0, + frame_duration = (self.state.frame_durations or {})[0] or 1 } self.image_dims = self.image_dims or {} From 7240c73a52451934d865e0c7afd6e237e0ad7679 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Fri, 22 May 2026 16:34:45 +0200 Subject: [PATCH 13/14] Added `state.default_frame_duration` +Title, should make uniform FPS changes easier. --- src/game_objects/state_sprite.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua index 47a22fad6..5d2cf0e34 100644 --- a/src/game_objects/state_sprite.lua +++ b/src/game_objects/state_sprite.lua @@ -9,7 +9,8 @@ StateSprite = AnimatedSprite:extend() frame_order = "linear" |OR| "random" |OR| {1: x, 2: y, .. n: m} (optional) flipped_h/flipped_v = true, (optional) exit_to = [state], - (optional) frame_durations = {1: 2, 2:...} + (optional) frame_durations = {1: 2, 2:...}, (in Frames according to G.ANIMATION_FPS) + (optional) default_frame_duration = 1, (in Frames according to G.ANIMATION_FPS) }, ... } @@ -24,6 +25,7 @@ StateSprite = AnimatedSprite:extend() wakey = { start_pos = {x = 4}, (y is set to 0) frames = 4, (end_pos is set to start_pos with .x + frames) + default_frame_duration = 3, exit_to = "lookey", (after one iteration, sets state to this value) }, lookey = { @@ -98,7 +100,7 @@ function StateSprite:animate() if self.state.exit_to and self.current_animation.elapsed >= self.current_animation.frames then self:set_state(self.state.exit_to) end - local frame_finished = (math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds) / (self.current_animation.frame_duration or 1))) > 0 + local frame_finished = (math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds) / (self.current_animation.frame_duration or self.state.default_frame_duration or 1))) > 0 if frame_finished then local new_frame if type(self.state.frame_order) == "table" then @@ -109,7 +111,7 @@ function StateSprite:animate() end self.current_animation.current = new_frame or ((self.current_animation.current + 1) % self.current_animation.frames) self.current_animation.elapsed = self.current_animation.elapsed + 1 - self.current_animation.frame_duration = (self.state.frame_durations or {})[self.current_animation.current] or 1 + self.current_animation.frame_duration = (self.state.frame_durations or {})[self.current_animation.current] or self.state.default_frame_duration or 1 local _x = self.animation.w * ((self.states_offset.x + self.state.start_pos.x + self.current_animation.current) % self.atlas.columns) local _y = self.animation.h * (self.states_offset.y + self.state.start_pos.y + math.floor(self.current_animation.current / self.atlas.columns)) self.sprite:setViewport( @@ -143,7 +145,7 @@ function StateSprite:set_sprite_pos(sprite_pos) w = self.animation.w, h = self.animation.h, elapsed = 0, - frame_duration = (self.state.frame_durations or {})[0] or 1 + frame_duration = (self.state.frame_durations or {})[0] or self.state.default_frame_duration or 1 } self.image_dims = self.image_dims or {} From cfb1566687c88d0ef78ef2c2df5a793bb615ed3a Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Fri, 22 May 2026 16:51:18 +0200 Subject: [PATCH 14/14] Added support for `state.exit_to` being a function + stored `default_state` arg +/*Title --- src/game_objects/state_sprite.lua | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua index 5d2cf0e34..7a4f377de 100644 --- a/src/game_objects/state_sprite.lua +++ b/src/game_objects/state_sprite.lua @@ -8,7 +8,7 @@ StateSprite = AnimatedSprite:extend() (frames = [amount of frames] |OR| end_pos = { [same as start_pos] }), frame_order = "linear" |OR| "random" |OR| {1: x, 2: y, .. n: m} (optional) flipped_h/flipped_v = true, - (optional) exit_to = [state], + (optional) exit_to = [state] |OR [function(state_table, sprite), returning a state], (optional) frame_durations = {1: 2, 2:...}, (in Frames according to G.ANIMATION_FPS) (optional) default_frame_duration = 1, (in Frames according to G.ANIMATION_FPS) }, @@ -25,7 +25,7 @@ StateSprite = AnimatedSprite:extend() wakey = { start_pos = {x = 4}, (y is set to 0) frames = 4, (end_pos is set to start_pos with .x + frames) - default_frame_duration = 3, + default_frame_duration = 3, (all frames last 3 times longer (0.3 seconds with default G.ANIMATION_FPS == 10)) exit_to = "lookey", (after one iteration, sets state to this value) }, lookey = { @@ -45,8 +45,9 @@ function StateSprite:init(X, Y, W, H, new_sprite_atlas, _pos, args) else self.sprite_args = args self.states_offset = args.states_offset and {x = args.states_offset.x or 0, y = args.states_offset.y or 0} or {x = 0, y = 0} + self.default_state = args.default_state or next(args.states) self:load_states(args.states) - self:set_state(args.default_state or next(args.states)) + self:set_state(self.default_state) end self.flipped_h = false @@ -76,6 +77,7 @@ function StateSprite:load_states(states) for key, state in pairs(states) do state.start_pos = state.start_pos and {x = state.start_pos.x or 0, y = state.start_pos.y or 0} or {x = 0, y = 0} state.frames = state.frames or ((state.end_pos or state.start_pos).x - state.start_pos.x + ((state.end_pos.y or state.start_pos).y - state.start_pos.y) * self.atlas.columns + 1) + state.key = key if type(state.frame_order) == "string" then local keymap = { linear=true, @@ -98,7 +100,11 @@ end function StateSprite:animate() if not self.state then return end if self.state.exit_to and self.current_animation.elapsed >= self.current_animation.frames then - self:set_state(self.state.exit_to) + if type(self.state.exit_to) == "function" then + self:set_state(self.state:exit_to(self) or self.default_state) + else + self:set_state(self.state.exit_to) + end end local frame_finished = (math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds) / (self.current_animation.frame_duration or self.state.default_frame_duration or 1))) > 0 if frame_finished then