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/lsp_def/classes/atlas.lua b/lsp_def/classes/atlas.lua index 8e16d64ff..a2159d54a 100644 --- a/lsp_def/classes/atlas.lua +++ b/lsp_def/classes/atlas.lua @@ -5,11 +5,13 @@ ---@field super? SMODS.GameObject|table Parent class. ---@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 path? string Name of the image file, including extension. ---@field path_mod? Mod|table The mod this object's `path` belongs to, if this is not the same mod it was created by. ----@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 fps? number Speed of animation based on frames per second. Default: 10 or G.ANIMATION_FPS. +---@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 8a67ce360..58bf20267 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 ca499b2bd..88aae9ed1 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -425,6 +425,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, @@ -466,7 +472,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:getWidth() / self.px + self.rows = self.image:getHeight() / 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 @@ -703,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 @@ -1777,7 +1787,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, @@ -1894,7 +1904,7 @@ SMODS.UndiscoveredCompat = { }, 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 @@ -3142,7 +3152,7 @@ SMODS.UndiscoveredCompat = { 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 @@ -3277,6 +3287,7 @@ SMODS.UndiscoveredCompat = { 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', @@ -4014,6 +4025,12 @@ SMODS.UndiscoveredCompat = { } + ------------------------------------------------------------------------------------------------- + ----- API IMPORT Object.Node.Moveable.Sprite.AnimatedSprite.StateSprite + ------------------------------------------------------------------------------------------------- + + assert(load(NFS.read(SMODS.path..'src/game_objects/state_sprite.lua'), ('=[SMODS _ "src/game_objects/state_sprite.lua"]')))() + ------------------------------------------------------------------------------------------------- ----- API IMPORT GameObject.DrawStep ------------------------------------------------------------------------------------------------- @@ -4038,4 +4055,4 @@ SMODS.UndiscoveredCompat = { end end } -end +end \ No newline at end of file diff --git a/src/game_objects/state_sprite.lua b/src/game_objects/state_sprite.lua new file mode 100644 index 000000000..7a4f377de --- /dev/null +++ b/src/game_objects/state_sprite.lua @@ -0,0 +1,194 @@ +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, + (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) + }, + ... +} +]] +-- 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) + 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 = { + 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} (the first frame lasts three times longer) + } +} +]] +-- To change state, call StateSprite:set_state(state_name) +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 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.default_state = args.default_state or next(args.states) + self:load_states(args.states) + self:set_state(self.default_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 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 + 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 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, + 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 + if self.state.exit_to and self.current_animation.elapsed >= self.current_animation.frames then + 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 + 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 + 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 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( + _x, + _y, + self.animation.w, + 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) + 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.frames 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, + elapsed = 0, + frame_duration = (self.state.frame_durations or {})[0] or self.state.default_frame_duration or 1 + } + + self.image_dims = self.image_dims or {} + self.image_dims[1], self.image_dims[2] = self.atlas.image:getDimensions() + + self.sprite = love.graphics.newQuad( + self.animation.w*self.animation.x, + 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/overrides.lua b/src/overrides.lua index 50583a8ff..688076b08 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 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 @@ -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 1ac017f7c..c36320a5c 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -3631,7 +3631,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) @@ -3639,15 +3639,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, 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) - 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, sprite_args) + end + return sprite_class(X, Y, W, H, atlas, pos) end local animate = AnimatedSprite.animate