Multi‐layer cards - Oinite12/tiwmig-mod GitHub Wiki
By default, cards in Balatro only have two layers: the main sprite, and the "soul" sprite, which is usually an icon that "floats" in front of the main sprite, and is exempt from edition shaders. TIWMIG adds a system that allows for more layers to be added:
- above the main sprite, and before the "soul" sprite - These layers are subject to edition shaders
- above the "soul" sprite - These layers are not subject to edition shaders
The system is implemented entirely in items/multi-layer cards
.
When defining a Joker (or some other card), the pos
table can take an additional key, layers
, which is an ordered table that contains keyed tables of the form {x=#,y=#}
, similar to the usual pos
syntax. These layers are ordered from bottom to top.
The same principle applies to soul_pos
.
The atlas defined in atlas
will be used by all layers.
DISCLAIMER: I dont have the fullest fuckin' idea on what most of this does. This is just my understanding of how the system works.
When setting the layer sprites, Card:set_sprites()
, or an appending of it, is called on a card (hereby referred to as self
). self
contains a table children
, which contains Sprite objects associated with self
.
The appending to Card:set_sprites()
adds several Sprites to children
with keys of the form tiwmig_<type>_sprite_<id>
, where id
is an integer, and <type>
is either "main" or "soul". (This key is defined by G_TWMG.layer_name(type, id)
- thus from now on these Sprites will be referred to as such.) Specifically for each layer in pos.layers
and soul_pos.layers
, a sprite is added. And then something about role.draw_major
and all that stuff, idk what they do but it's based on what's coded for vanilla background and soul sprites, so...
{...}
for i,coords in pairs(_center.<pos/soul_pos>.layers) do if i <= max_layers then
self.children[G_TWMG.layer_name(type, i)] = Sprite(
self.T.x, self.T.y, self.T.w, self.T.h,
G.ASSET_ATLAS[_center.atlas or _center.set],
coords
)
self.children[G_TWMG.layer_name(type, i)].role.draw_major = self
self.children[G_TWMG.layer_name(type, i)].states.hover.can = false
self.children[G_TWMG.layer_name(type, i)].states.click.can = false
end end
{...}
This alone does not suffice to render the additional layers; the layers may not follow the card itself, or the layers are rendered twice, with one instance not following the card itself; not to mention, the layers do not have that floaty animation that all cards have.
To resolve this, a DrawStep is added for main and soul layers. Both DrawSteps target each G_TWMG.layer_name(type, id)
on self
. More specifically, shaders are drawn for each G_TWMG.layer_name(type, id)
that, at minimum, give the layers the floaty animation. The layer index (or #
) is used in the calculation of the scale and rotation modifier (responsible for the floaty animation) so that each layer does not strictly follow the animation of the background sprite. For main layers, an additional shader is drawn for editions, while for soul layers, edition shaders are only drawn if the edition shader can be applied to floating sprites (i.e. Edition.apply_to_float
is true).
SMODS.DrawStep({
{...}
local sprite = self.children[G_TWMG.layer_name(type, id)]
-- incorporate i to offset layers a bit so it's not all static
local scale_mod = 0.03 + (i/100)/2 + 0.02*math.sin(1.8*G.TIMERS.REAL)
local rotate_mod = 0.02*math.sin(1.219*G.TIMERS.REAL + (i-1)/2)
-- For soul layers, 0.03 -> 0.07, and 0.02 -> 0.05
sprite:draw_shader("dissolve", 0, nil, nil, self.children.center, scale_mod, rotate_mod, nil, 0.1 + 0.03*math.sin(1.8*G.TIMERS.REAL), nil, 0.6)
-- not sure why "dissolve" is called twice, but it's like that in vanilla
sprite:draw_shader("dissolve", nil, nil, nil, self.children.center, scale_mod, rotate_mod)
if self.edition then for k, v in pairs(G.P_CENTER_POOLS.Edition) do
if self.edition[v.key:sub(3)] then
sprite:draw_shader(v.shader, nil, nil, nil, self.children.center, scale_mod, rotate_mod)
end
end end
{...}
})
Finally, each G_TWMG.layer_name(type, id)
are added to the SMODS table SMODS.draw_ignore_keys
. I'm not sure exactly what this table is for, but it does ultimately fix the non-following sprite problem.
for i = 1,max_layers do
SMODS.draw_ignore_keys[G_TWMG.layer_name(type, id)] = true
end
This very step, however, requires each key to be specifically defined, and is not dynamic, i.e. dependent on the maximum number of layers of some Joker card. Because of this, a specific layer count limit needs to be defined, which, for TIWMIG, is G_TWMG.max_card_layers
.
Much of the code responsible for multi-layer cards is based on the code for three-layer Jokers in lib/ui.lua
and lib/misc.lua
for the Cryptid mod, which are used in Exotic Jokers. This served as inspiration for me to create a system that adds an arbitrary number of layers to a card.
In turn, their code (and my code) is based on pre-existing game code from the vanilla game, used to render the main and soul sprites.