#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 #include #include #include // 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(sprite, UNDEFINED); return (s16)(sprite->obj - obj_buffer); } bool sprite_get_width(Sprite* sprite, int* width) { GBAL_RETURN_IF_NULL(sprite, false); GBAL_RETURN_IF_NULL(width, false); *width = obj_get_width(sprite->obj); return true; } bool sprite_get_height(Sprite* sprite, int* height) { GBAL_RETURN_IF_NULL(sprite, false); GBAL_RETURN_IF_NULL(height, false); *height = obj_get_height(sprite->obj); return true; } bool sprite_get_dimensions(Sprite* sprite, int* width, int* height) { GBAL_RETURN_IF_NULL(sprite, false); GBAL_RETURN_IF_NULL(width, false); GBAL_RETURN_IF_NULL(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(sprite, UNDEFINED); return (sprite->obj->attr2 & ATTR2_PALBANK_MASK) >> ATTR2_PALBANK_SHIFT; } void sprite_hide(Sprite* sprite) { GBAL_RETURN_IF_NULL(sprite, RET_NONE); obj_hide(sprite->obj); } void sprite_unhide(Sprite* sprite) { GBAL_RETURN_IF_NULL(sprite, RET_NONE); obj_unhide(sprite->obj, sprite->mode); } // SpriteObject methods void sprite_object_init(SpriteObject* sprite_object) { GBAL_RETURN_IF_NULL(sprite_object, RET_NONE); 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(sprite_object, RET_NONE); 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(sprite_object, RET_NONE); 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(sprite_object, RET_NONE); sprite_hide(sprite_object->sprite); } void sprite_object_unhide(SpriteObject* sprite_object) { GBAL_RETURN_IF_NULL(sprite_object, RET_NONE); sprite_unhide(sprite_object->sprite); } void sprite_object_reset_transform(SpriteObject* sprite_object) { GBAL_RETURN_IF_NULL(sprite_object, RET_NONE); 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(sprite_object, RET_NONE); 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(sprite_object, NULL); return sprite_object->sprite; } void sprite_object_set_focus(SpriteObject* sprite_object, bool focus) { GBAL_RETURN_IF_NULL(sprite_object, RET_NONE); 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(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(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(sprite_object, false); return sprite_get_dimensions(sprite_object->sprite, width, height); } bool sprite_object_is_focused(SpriteObject* sprite_object) { GBAL_RETURN_IF_NULL(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(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(sprite_object, RET_NONE); 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(sprite_object, RET_NONE); // + 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(sprite_object, RET_NONE); 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); }