Files
balatro-gba/source/sprite.c
Geralt 0eb7042468
Some checks failed
Build and Deploy Doxygen Docs / docs (push) Has been cancelled
Deploy Nightly / Run Build ROM Workflow (push) Has been cancelled
Deploy Nightly / Deploy nightly (push) Has been cancelled
[Bug] Implement independent RNG sequences to avoid interferences (#599)
* Implement our own Xorshift32 RNG algo to manage independent number sequences

* Fix issues with seed 0

* adress @MeirGavish's review

* Apply suggestions from @MeirGavish's code review

Co-authored-by: MeirGavish <meir.gavish@gmail.com>

* adress the rest of @MeirGavish's review

* adress @MeirGavish's review

* Fix last issues

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
Co-authored-by: MonteCrysto <monte.crysto31@gmail.com>
2026-08-05 12:37:06 +03:00

468 lines
13 KiB
C

#include "sprite.h"
#include "audio_utils.h"
#include "game.h"
#include "game_variables.h"
#include "graphic_utils.h"
#include "item.h"
#include "mgba_logger.h"
#include "pool.h"
#include "random.h"
#include "soundbank.h"
#include "util.h"
#include <maxmod.h>
#include <stdlib.h>
#include <tonc.h>
#include <tonc_oam.h>
// Damping Constants: SPRING_DAMP_NUMERATOR/2^SPRING_DAMP_DENOM_SHIFT ~ 0.699 damping factor
//
// SPRING_DAMP_ROUNDING = 2^(SPRING_DAMP_DENOM_SHIFT - 1) rounding for fixed-point arithmetic by
// adding half the denominator to round instead of truncating
#define SPRING_DAMP_NUMERATOR 179
#define SPRING_DAMP_DENOM_SHIFT 8
#define SPRING_DAMP_ROUNDING (1 << (SPRING_DAMP_DENOM_SHIFT - 1))
OBJ_ATTR obj_buffer[MAX_SPRITES];
OBJ_AFFINE* obj_aff_buffer = (OBJ_AFFINE*)obj_buffer;
static Sprite* free_sprites[MAX_SPRITES] = {NULL};
static bool free_affines[MAX_AFFINES] = {false};
static List sprite_objects_list = LIST_DEFAULT;
// Sprite methods
Sprite* sprite_new(u16 a0, u16 a1, u32 tid, u32 pb, s16 sprite_index)
{
Sprite* sprite = POOL_GET(Sprite);
sprite->obj = NULL;
sprite->aff = NULL;
if (!free_sprites[sprite_index])
{
free_sprites[sprite_index] = sprite;
}
else
{
POOL_FREE(Sprite, sprite);
return NULL;
}
if (a0 & ATTR0_AFF)
{
int aff_index = MAX_AFFINES;
for (int i = 0; i < MAX_AFFINES; i++)
{
if (!free_affines[i])
{
free_affines[i] = true;
aff_index = i;
break;
}
}
if (aff_index == MAX_AFFINES)
{
POOL_FREE(Sprite, sprite);
return NULL;
}
a1 = a1 | ATTR1_AFF_ID(aff_index);
sprite->obj = &obj_buffer[sprite_index];
sprite->aff = &obj_aff_buffer[aff_index];
obj_set_attr(sprite->obj, a0, a1, ATTR2_PALBANK(pb) | tid);
obj_aff_identity(&obj_aff_buffer[aff_index]);
}
else
{
sprite->obj = &obj_buffer[sprite_index];
obj_set_attr(sprite->obj, a0, a1, ATTR2_PALBANK(pb) | tid);
}
sprite->idx = sprite_index;
sprite->mode = a0 & ATTR0_MODE_MASK;
return sprite;
}
void sprite_destroy(Sprite** sprite)
{
if (*sprite == NULL)
return;
obj_hide((*sprite)->obj);
if ((*sprite)->aff != NULL)
{
free_affines[(*sprite)->aff - obj_aff_buffer] = false;
}
free_sprites[(*sprite)->idx] = NULL;
POOL_FREE(Sprite, *sprite);
*sprite = NULL;
}
/* The following functions don't check sprite->obj, assuming it shouldn't be NULL
* if sprite != NULL since it's set in the constructor
*/
s16 sprite_get_layer(Sprite* sprite)
{
GBAL_RETURN_IF_NULL_RET(sprite, UNDEFINED);
return (s16)(sprite->obj - obj_buffer);
}
bool sprite_get_width(Sprite* sprite, int* width)
{
GBAL_RETURN_IF_NULL_RET(sprite, false);
GBAL_RETURN_IF_NULL_RET(width, false);
*width = obj_get_width(sprite->obj);
return true;
}
bool sprite_get_height(Sprite* sprite, int* height)
{
GBAL_RETURN_IF_NULL_RET(sprite, false);
GBAL_RETURN_IF_NULL_RET(height, false);
*height = obj_get_height(sprite->obj);
return true;
}
bool sprite_get_dimensions(Sprite* sprite, int* width, int* height)
{
GBAL_RETURN_IF_NULL_RET(sprite, false);
GBAL_RETURN_IF_NULL_RET(width, false);
GBAL_RETURN_IF_NULL_RET(height, false);
const u8* size = obj_get_size(sprite->obj);
*width = size[0];
*height = size[1];
return true;
}
// Sprite functions
void sprite_init()
{
oam_init(obj_buffer, MAX_SPRITES);
}
void sprite_draw()
{
oam_copy(oam_mem, obj_buffer, MAX_SPRITES);
}
int sprite_get_pb(const Sprite* sprite)
{
GBAL_RETURN_IF_NULL_RET(sprite, UNDEFINED);
return (sprite->obj->attr2 & ATTR2_PALBANK_MASK) >> ATTR2_PALBANK_SHIFT;
}
void sprite_hide(Sprite* sprite)
{
GBAL_RETURN_IF_NULL_VOID(sprite);
obj_hide(sprite->obj);
}
void sprite_unhide(Sprite* sprite)
{
GBAL_RETURN_IF_NULL_VOID(sprite);
obj_unhide(sprite->obj, sprite->mode);
}
// SpriteObject methods
void sprite_object_init(SpriteObject* sprite_object)
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
sprite_object->sprite = NULL;
sprite_object_reset_transform(sprite_object);
sprite_object->focused = false;
list_push_back(&sprite_objects_list, sprite_object);
}
void sprite_object_destroy(SpriteObject* sprite_object)
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
list_remove_data(&sprite_objects_list, sprite_object);
sprite_destroy(&sprite_object->sprite);
}
void sprite_object_set_sprite(SpriteObject* sprite_object, Sprite* sprite)
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
sprite_destroy(&sprite_object->sprite); // Destroy the old sprite if it exists
sprite_object->sprite = sprite;
}
void sprite_object_hide(SpriteObject* sprite_object)
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
sprite_hide(sprite_object->sprite);
}
void sprite_object_unhide(SpriteObject* sprite_object)
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
sprite_unhide(sprite_object->sprite);
}
void sprite_object_reset_transform(SpriteObject* sprite_object)
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
sprite_object_position(sprite_object, 0, 0); // Target position
sprite_object->vx = 0;
sprite_object->vy = 0;
sprite_object->tscale = FIX_ONE; // Target scale
sprite_object->scale = FIX_ONE;
sprite_object->vscale = 0;
sprite_object->trotation = 0; // Target rotation
sprite_object->rotation = 0;
sprite_object->vrotation = 0;
}
/* The following functions are in the SpriteObject update loop which is called each frame
* so they avoid argument NULL-checks for efficiency.
*/
static inline bool sprite_object_has_velocity(const SpriteObject* sprite_object)
{
return sprite_object->vx != 0 || sprite_object->vy != 0 || sprite_object->vscale != 0 ||
sprite_object->vrotation != 0;
}
static inline bool sprite_object_at_target(const SpriteObject* s)
{
return s->x == s->tx && s->y == s->ty && s->scale == s->tscale && s->rotation == s->trotation;
}
static inline bool is_sprite_object_static(const SpriteObject* sprite_object)
{
return !sprite_object_has_velocity(sprite_object) && sprite_object_at_target(sprite_object);
}
static inline IWRAM_CODE void update_sprite_position(SpriteObject* sprite_object)
{
sprite_object->vx += ((sprite_object->tx - sprite_object->x) * g_game_vars.game_speed) / 8;
sprite_object->vy += ((sprite_object->ty - sprite_object->y) * g_game_vars.game_speed) / 8;
// Scale up the card when it's played
sprite_object->vscale += (sprite_object->tscale - sprite_object->scale) / 8;
// Rotate the card when it's played
sprite_object->vrotation += (sprite_object->trotation - sprite_object->rotation) / 8;
const FIXED epsilon = (FIX_ONE >> 6); // = 1/2^6 = 0.015625
// Snap to target position when velocity is negligible to avoid infinite approach
if (abs(sprite_object->vx) < epsilon && abs(sprite_object->vy) < epsilon)
{
sprite_object->vx = 0;
sprite_object->vy = 0;
sprite_object->x = sprite_object->tx;
sprite_object->y = sprite_object->ty;
}
else
{
sprite_object->vx = (sprite_object->vx * SPRING_DAMP_NUMERATOR + SPRING_DAMP_ROUNDING) >>
SPRING_DAMP_DENOM_SHIFT;
sprite_object->vy = (sprite_object->vy * SPRING_DAMP_NUMERATOR + SPRING_DAMP_ROUNDING) >>
SPRING_DAMP_DENOM_SHIFT;
sprite_object->x += sprite_object->vx;
sprite_object->y += sprite_object->vy;
}
// Set scale to 0 if it's close enough to the target
if (abs(sprite_object->vscale) < epsilon)
{
sprite_object->vscale = 0;
sprite_object->scale = sprite_object->tscale;
}
else
{
sprite_object->vscale =
(sprite_object->vscale * SPRING_DAMP_NUMERATOR + SPRING_DAMP_ROUNDING) >>
SPRING_DAMP_DENOM_SHIFT;
sprite_object->scale += sprite_object->vscale;
}
// For rotation, prioritize snapping to target if close enough, then zero velocity.
if (abs(sprite_object->vrotation) < epsilon)
{
sprite_object->vrotation = 0;
sprite_object->rotation = sprite_object->trotation;
}
else // Apply damping and update rotation if not yet settled
{
sprite_object->vrotation =
(sprite_object->vrotation * SPRING_DAMP_NUMERATOR + SPRING_DAMP_ROUNDING) >>
SPRING_DAMP_DENOM_SHIFT;
sprite_object->rotation += sprite_object->vrotation;
}
// Apply rotation and scale to the sprite
obj_aff_rotscale(
sprite_object->sprite->aff,
sprite_object->scale,
sprite_object->scale,
-sprite_object->vx + sprite_object->rotation
);
}
IWRAM_CODE void sprite_object_update(SpriteObject* sprite_object)
{
if (!is_sprite_object_static(sprite_object))
update_sprite_position(sprite_object);
sprite_position(sprite_object->sprite, fx2int(sprite_object->x), fx2int(sprite_object->y));
}
void sprite_object_update_all(void)
{
SpriteObject* sprite_object = NULL;
ListItr itr = list_itr_create(&sprite_objects_list);
while ((sprite_object = list_itr_next(&itr)))
{
sprite_object_update(sprite_object);
}
}
void sprite_object_shake(SpriteObject* sprite_object, mm_word sound_id)
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
sprite_object->vscale = float2fx(0.3f);
sprite_object->vrotation = float2fx(8.0f); // Rotate the card when it's scored
if (sound_id == UNDEFINED)
return; // If no sound ID is provided, do nothing
play_sfx(sound_id, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
}
Sprite* sprite_object_get_sprite(SpriteObject* sprite_object)
{
GBAL_RETURN_IF_NULL_RET(sprite_object, NULL);
return sprite_object->sprite;
}
void sprite_object_set_focus(SpriteObject* sprite_object, bool focus)
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
if (sprite_object->focused == focus)
{
return;
}
sprite_object->focused = focus;
play_sfx(
SFX_CARD_FOCUS,
MM_BASE_PITCH_RATE + rng_get_u32(RNG_SEQ_MISC) % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
SFX_DEFAULT_VOLUME
);
sprite_object->ty = sprite_object->ty + int2fx((focus ? -1 : 1) * SPRITE_FOCUS_RAISE_PX);
}
bool sprite_object_get_width(SpriteObject* sprite_object, int* width)
{
GBAL_RETURN_IF_NULL_RET(sprite_object, false);
return sprite_get_width(sprite_object->sprite, width);
}
bool sprite_object_get_height(SpriteObject* sprite_object, int* height)
{
GBAL_RETURN_IF_NULL_RET(sprite_object, false);
return sprite_get_height(sprite_object->sprite, height);
}
bool sprite_object_get_dimensions(SpriteObject* sprite_object, int* width, int* height)
{
GBAL_RETURN_IF_NULL_RET(sprite_object, false);
return sprite_get_dimensions(sprite_object->sprite, width, height);
}
bool sprite_object_is_focused(SpriteObject* sprite_object)
{
GBAL_RETURN_IF_NULL_RET(sprite_object, false);
return sprite_object->focused;
}
static Rect sprite_object_get_text_rect_under(SpriteObject* sprite_object)
{
int height = 0;
int width = 0;
Rect ret_rect = {0};
GBAL_RETURN_IF_NULL_RET(sprite_object, ret_rect);
if (sprite_object_get_dimensions(sprite_object, &width, &height) == false)
{
// fallback
height = CARD_SPRITE_SIZE;
width = CARD_SPRITE_SIZE;
}
ret_rect.left = fx2int(sprite_object->tx);
ret_rect.top = fx2int(sprite_object->ty) + height + TILE_SIZE;
ret_rect.right = ret_rect.left + width;
ret_rect.bottom = ret_rect.top + TTE_CHAR_SIZE;
return ret_rect;
}
void sprite_object_print_text_under(SpriteObject* sprite_object, const char text[])
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
Rect text_rect = sprite_object_get_text_rect_under(sprite_object);
update_text_rect_to_center_str(&text_rect, text, SCREEN_LEFT);
tte_printf("#{P:%d,%d; cx:0x%X000}%s", text_rect.left, text_rect.top, TTE_YELLOW_PB, text);
}
void sprite_object_print_price_under(SpriteObject* sprite_object, int price)
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
// + 2 for null-terminator and "$"
char price_str_buff[INT_MAX_DIGITS + 2];
snprintf(price_str_buff, sizeof(price_str_buff), "$%d", price);
sprite_object_print_text_under(sprite_object, price_str_buff);
}
void sprite_object_erase_text_under(SpriteObject* sprite_object)
{
GBAL_RETURN_IF_NULL_VOID(sprite_object);
Rect text_rect = sprite_object_get_text_rect_under(sprite_object);
// Add SPRITE_FOCUS_RAISE_PX to cover the focused case
text_rect.bottom = text_rect.bottom + SPRITE_FOCUS_RAISE_PX;
tte_erase_rect_wrapper(text_rect);
}