Cardanim - Oinite12/balatro-modding-modules GitHub Wiki

Cardanim is a system that streamlines the definition of frame sequences on cards, and allows automatic generation of frames via macros, while remaining relatively performant. This file should be loaded after Jokers/Centers are registered.

Acknowledgements

The development of this module and documentation would not be possible without initial implementations by Cryptid, Potassium, and Ascensio. Additionally, further development of this module is motivated by the encouragement of MarioFan597.

Configuring Cardanim

The Cardanim configuration table (local cardanim_cfg) contains three fields that can be modified to meet specific needs:

  • macro_directory STRING - The directory (relative to the mod folder) containing macro files, which the Cardanim file will automatically load.
  • card_layers TABLE - Definitions of card layers - custom card layers (like Cryptid's soul_pos.extra) will need to be defined here; then the keys of this table can be used in the animation property. Each card layer requires the following functions:
    • coords takes an entry from G.P_CENTER, and refers to the table in a Center's register that contains coordinates on an atlas.
    • child takes the children property of a Card object, and refers to the Card's child that holds the sprite for a card's specific layer. This child should contain a set_sprite_pos method.
  • granularity NUMBER integer - This value specifies how many frames can actually fit within 0.1 seconds. It is used to determine the minimum value of each frame's t, as well as "good" values of t.
    • For the sake of clean division, it is recommended that this value be either 1 or a number with only 2 and 5 as its factors, for example 2, 4, 5, 8, 10, and 20, and not 3, 6, 7, and 15.

Note that this table is also added to G via the property G[SMODS.current_mod.prefix .. "_cardanim_cfg"]; this can then be used in macros.

Using Cardanim

On the Center register (i.e. SMODS.Joker), animations are defined with the animation property. The animation property can take one of two properties:

  • frames TABLE
  • macro TABLE

frames

This property takes a TABLE with keys corresponding to the layers of a card (all optional); these layers are defined onto cardanim_cfg.card_layers. In vanilla Balatro, these layers are pos and soul_pos. Other mods may add other card layers, such as Cryptid (soul_pos.extra); these will need to be added to Cardanim's configuration table.

The values of frames are each SEQUENCEs of TABLEs of the form {x=#,y=#,t=#}, each of which defines a single frame in the sequence:

  • x and y are coordinates on an atlas.
  • t OPTIONAL (default=1) is the duration of the frame; a frame persists for t*0.1 seconds (that is, animations run at a rate of 10 fps, or 0.1 spf).
    • The minimum value of t is 1/granularity, and good values of t are multiples of this minimum value. For example, if the granularity is 4 (4 frames per 0.1 seconds), the minimum value is 0.25, and good values include 0.25, 0.5, 0.75, 1, 1.25...

Each layer of frames do not need to have equal sums of t; they can assume different period lengths.

Example

SMODS.Joker {
  ...
  animation = {
    frames = {
      pos = {
        { x=1, y=1, t=1},
        { x=2, y=1, t=1},
        { x=3, y=1, t=5},
        { x=1, y=2, t=1},
        { x=2, y=2, t=1},
        { x=3, y=2, t=5},
      },
      soul_pos = {
        { x=1, y=3, t=1},
        { x=2, y=3, t=1},
        { x=3, y=3, t=1},
      },
    },
  },
  ...
}

macro

Alternatively, instead of manually writing out frame sequences, macros can be defined to automatically generate frame sequences via a function. The property macro must always take a property type, which is a STRING containing the file name of the macro (excluding extension) (CASE SENSITIVE). Any further properties are dependent on the macro itself, but all go under macro.

For more information on macros, please see the Macro section.

Example

SMODS.Joker {
  ...
  animation = {
    macro = {
      type = "skim",
      ...
    },
  },
  ...
}

Macros

Macros are file-separated functions that automatically generate frame sequences. They are loaded via the main Cardanim file, after which cards can define an animation using a macro. Macros can be written to generate sequences from other different animation sequence file formats, or with very specific durations depending on the animation period.

The directory to add macro files depends on the specified directory in the Cardanim confgiuration table.

List of macros

Writing macros

Macros should be written so that the file itself returns the macro function.

  • The macro function takes a TABLE (usually named macro_obj), which is the same TABLE as <card center>.animation.macro.
  • The return value should be a TABLE with the same format as <P_CENTER entry>.animation.frames (Using Cardanim > frames > example).

It is recommended to specify default values for optional parameters, and throw errors with specific messages for non-optional/limited value parameters, for easier debugging.

Hint: To iterate through each card layer, G[SMODS.current_mod.prefix .. "_cardanim_cfg"].card_layers can be used.

Example

return function(macro_obj)
  local example_variable = macro_obj.example_variable or 1
  ...
  return {pos = {
    { x=1, y=1, t=2},
    { x=2, y=1, t=4},
  }}
end

Motive for development

Cardanim was initially developed for Ascensio by Oinite after the mod began adding more animated cards, starting with the Joker "Fortunae Risus". Ascensio's old animation system was based on Potassium's implementation of card animation for their Hologram retexture, which in turn was based on Cryptid's implementation of card animation for Jimball and Glowing Deck:

-- animate hologram sprite
-- code adapted from Cryptid's jimball
banana_hologram_dt = 0
local _game_update = Game.update
function Game:update(dt)
    _game_update(self, dt)
    banana_hologram_dt = banana_hologram_dt + dt -- cryptid has a check here but im not sure what it's for
	if G.P_CENTERS and G.P_CENTERS.j_hologram and banana_hologram_dt > 0.05 then
		banana_hologram_dt = banana_hologram_dt - 0.05
		local hologramobj = G.P_CENTERS.j_hologram
		if hologramobj.soul_pos.x == 11 and hologramobj.soul_pos.y == 9 then
			hologramobj.soul_pos.x = 0
			hologramobj.soul_pos.y = 0
		elseif hologramobj.soul_pos.x < 11 then
			hologramobj.soul_pos.x = hologramobj.soul_pos.x + 1
		elseif hologramobj.soul_pos.y < 9 then
			hologramobj.soul_pos.x = 0
			hologramobj.soul_pos.y = hologramobj.soul_pos.y + 1
		end
        -- oh my god i hate this so much but ARGH
        -- note that this can't use find_card because it also needs to work in the collection
        -- unless there's some other way you can do it
        for _, card in pairs(G.I.CARD) do
            if card and card.config.center == hologramobj then
                card.children.floating_sprite:set_sprite_pos(hologramobj.soul_pos)
            end
        end
	end
    [...]
end

While this implementation sufficed for one or two cards, it became less sufficient for Ascensio, which had more animated Jokers being added to the mod. This code (or modifications of it) was added for each Joker - this is a problem because that would mean each animated Joker would constantly be checking all cards on screen to see if they needed updating. It's not a problem for 5 or 10 Jokers, but because we are dealing with a Cryptid add-on, the case of high numbers of Jokers needs to be taken into account, a case that would suffer under the old animation implementation.

Additionally, the current method of defining animations is quite tedious; it worked for Fortunae Risus, which only has eight frames for its one animated layer, but contributers were beginning to introduce more lengthy animations, and animations involving multiple layers.

Cardanim resolves both issues - it only needs to check all cards on screen once (per mod with a Cardanim file), and features a sequence syntax that is not only easier to read than if-else chains, but can also be automatically generated with macros.

⚠️ **GitHub.com Fallback** ⚠️