Merge branch 'main' into hblank_affine_background_effects

This commit is contained in:
MeirGavish
2025-08-12 17:24:14 +03:00
6 changed files with 357 additions and 191 deletions
+1
View File
@@ -3,3 +3,4 @@
/.vs /.vs
*.sln *.sln
*.vcxproj* *.vcxproj*
/balatro-gba/x64/**
+25
View File
@@ -0,0 +1,25 @@
#ifndef LIST_H
#define LIST_H
#include <stdbool.h>
/* Defining an int list for simplicity
* If another list is needed, a generic void*
* list should be defined....
*/
typedef struct IntList
{
int* _array;
int size;
int allocated_size;
} IntList;
IntList *int_list_new(int init_size);
void int_list_destroy(IntList **list);
bool int_list_append(IntList *list, int value);
bool int_list_remove_by_idx(IntList *list, int index);
int int_list_get(IntList *list, int index);
int int_list_get_size(IntList *list);
bool int_list_remove_by_value(IntList *list, int value);
#endif
+144 -88
View File
@@ -23,6 +23,8 @@
#include "soundbank.h" #include "soundbank.h"
#include "list.h"
// This seed system is temporary. // This seed system is temporary.
static uint rng_seed = 2048; static uint rng_seed = 2048;
static bool seeded = false; static bool seeded = false;
@@ -171,6 +173,8 @@ int get_jokers_top(void) {
return jokers_top; return jokers_top;
} }
IntList* jokers_available_to_shop; // List of joker IDs
// Consts // Consts
// Rects left top right bottom // Rects left top right bottom
@@ -998,8 +1002,21 @@ void game_set_state(enum GameState new_game_state)
game_state = new_game_state; game_state = new_game_state;
} }
void jokers_avialable_to_shop_init()
{
int num_defined_jokers = get_joker_registry_size();
jokers_available_to_shop = int_list_new(num_defined_jokers);
for (int i = 0; i < num_defined_jokers; i++)
{
// Add all joker IDs sequentially
int_list_append(jokers_available_to_shop, i);
}
}
void game_init() void game_init()
{ {
jokers_avialable_to_shop_init();
hands = max_hands; hands = max_hands;
discards = max_discards; discards = max_discards;
@@ -1559,7 +1576,7 @@ static void played_cards_update_loop(bool* discarded_card, int* played_selection
FIXED played_x = int2fx(120); FIXED played_x = int2fx(120);
FIXED played_y = int2fx(70); FIXED played_y = int2fx(70);
FIXED played_scale = float2fx(1.0f); FIXED played_scale = FIX_ONE;
played_x = played_x + (int2fx(played_top - i) - int2fx(played_top) / 2) * -27; played_x = played_x + (int2fx(played_top - i) - int2fx(played_top) / 2) * -27;
@@ -1818,41 +1835,6 @@ static void game_round_end_cashout()
display_score(score); // Set the score display display_score(score); // Set the score display
} }
static void game_shop_create_items(JokerObject *shop_jokers[], bool first_time)
{
tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT);
for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{
if (shop_jokers[i] != NULL)
{
joker_object_destroy(&shop_jokers[i]); // Destroy the joker object if it exists
}
u8 joker_id = random() % get_joker_registry_size();
// TODO: don't pick a joker that we already own
// TODO: weight the random choice by joker rarity
shop_jokers[i] = joker_object_new(joker_new(joker_id));
shop_jokers[i]->sprite_object->x = int2fx(120 + i * 32);
shop_jokers[i]->sprite_object->y = int2fx(160);
shop_jokers[i]->sprite_object->tx = shop_jokers[i]->sprite_object->x;
shop_jokers[i]->sprite_object->ty = int2fx(71);
int x = fx2int(shop_jokers[i]->sprite_object->tx) + TILE_SIZE - (get_digits_even(shop_jokers[i]->joker->value) - 1) * TILE_SIZE;
int y = fx2int(shop_jokers[i]->sprite_object->ty) + CARD_SPRITE_SIZE + TILE_SIZE;
tte_printf("#{P:%d,%d; cx:0xC000}$%d", x, y, shop_jokers[i]->joker->value);
if (first_time == false)
{
shop_jokers[i]->sprite_object->y = shop_jokers[i]->sprite_object->ty; // If it's not the first time, set the y position to the target position
joker_object_shake(shop_jokers[i], UNDEFINED); // Give the joker a little wiggle animation
}
sprite_position(joker_object_get_sprite(shop_jokers[i]), fx2int(shop_jokers[i]->sprite_object->x), fx2int(shop_jokers[i]->sprite_object->y));
}
}
void game_playing() void game_playing()
{ {
// TODO: Blind rect sliding into view animation... // TODO: Blind rect sliding into view animation...
@@ -2155,13 +2137,44 @@ void game_round_end()
} }
} }
void game_shop_intro(JokerObject* shop_jokers[]) static void game_shop_create_items(JokerObject *shop_jokers[])
{
tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT);
if (int_list_get_size(jokers_available_to_shop) == 0)
{
// No jokers to create
return;
}
for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{
// TODO: weight the random choice by joker rarity
int joker_idx = random() % int_list_get_size(jokers_available_to_shop);
int joker_id = int_list_get(jokers_available_to_shop, joker_idx);
int_list_remove_by_idx(jokers_available_to_shop, joker_idx);
shop_jokers[i] = joker_object_new(joker_new(joker_id));
shop_jokers[i]->sprite_object->x = int2fx(120 + i * 32);
shop_jokers[i]->sprite_object->y = int2fx(160);
shop_jokers[i]->sprite_object->tx = shop_jokers[i]->sprite_object->x;
shop_jokers[i]->sprite_object->ty = int2fx(71);
int x = fx2int(shop_jokers[i]->sprite_object->tx) + TILE_SIZE - (get_digits_even(shop_jokers[i]->joker->value) - 1) * TILE_SIZE;
int y = fx2int(shop_jokers[i]->sprite_object->ty) + CARD_SPRITE_SIZE + TILE_SIZE;
tte_printf("#{P:%d,%d; cx:0xC000}$%d", x, y, shop_jokers[i]->joker->value);
sprite_position(joker_object_get_sprite(shop_jokers[i]), fx2int(shop_jokers[i]->sprite_object->x), fx2int(shop_jokers[i]->sprite_object->y));
}
}
// Intro sequence (menu and shop icon coming into frame)
static void game_shop_intro(JokerObject* shop_jokers[])
{ {
main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_UP); main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_UP);
if (timer == 1) if (timer == 1)
{ {
game_shop_create_items(shop_jokers, true); game_shop_create_items(shop_jokers);
} }
if (timer >= 7) // Shift the shop icon if (timer >= 7) // Shift the shop icon
@@ -2188,59 +2201,44 @@ void game_shop_intro(JokerObject* shop_jokers[])
} }
} }
void game_shop() static void game_shop_reroll(JokerObject** shop_jokers, int *reroll_cost)
{ {
change_background(BG_ID_SHOP); money -= *reroll_cost;
display_money(money); // Update the money display
for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{
if (shop_jokers[i] != NULL)
{
int_list_append(jokers_available_to_shop, shop_jokers[i]->joker->id);
joker_object_destroy(&shop_jokers[i]); // Destroy the joker object if it exists
}
}
static JokerObject *shop_jokers[MAX_SHOP_JOKERS] = {NULL}; game_shop_create_items(shop_jokers);
for (int i = 0; i < MAX_SHOP_JOKERS; i++) for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{ {
if (shop_jokers[i] != NULL) if (shop_jokers[i] != NULL)
{ {
joker_object_update(shop_jokers[i]); shop_jokers[i]->sprite_object->y = shop_jokers[i]->sprite_object->ty; // Set the y position to the target position
joker_object_shake(shop_jokers[i], UNDEFINED); // Give the joker a little wiggle animation
} }
} }
const int reroll_base_cost = 5; // Base cost for rerolling the shop items (*reroll_cost)++;
static int reroll_cost = reroll_base_cost; tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, *reroll_cost);
}
// Shop menu input and selection
static void game_shop_process_user_input(JokerObject** shop_jokers)
{
static const int REROLL_BASE_COST = 5; // Base cost for rerolling the shop items
static int reroll_cost = REROLL_BASE_COST;
// temp variables for future implementation // temp variables for future implementation
const ushort max_items_top = MAX_SHOP_JOKERS; const ushort max_items_top = MAX_SHOP_JOKERS;
const ushort max_items_bottom = 0; const ushort max_items_bottom = 0;
if (timer % 20 == 0) // Shift palette around the border of the shop icon
{
COLOR shifted_palette[4];
memcpy16(&shifted_palette[0], &pal_bg_mem[14], 1);
memcpy16(&shifted_palette[1], &pal_bg_mem[17], 1);
memcpy16(&shifted_palette[2], &pal_bg_mem[22], 1);
memcpy16(&shifted_palette[3], &pal_bg_mem[8], 1);
// Circularly shift the palette
int last = shifted_palette[3];
for (int i = 3; i > 0; --i) {
shifted_palette[i] = shifted_palette[i - 1];
}
shifted_palette[0] = last;
memcpy16(&pal_bg_mem[14], &shifted_palette[0], 1); // Copy the shifted palette to the next 4 slots
memcpy16(&pal_bg_mem[17], &shifted_palette[1], 1);
memcpy16(&pal_bg_mem[22], &shifted_palette[2], 1);
memcpy16(&pal_bg_mem[8], &shifted_palette[3], 1);
}
switch (state) // I'm only using magic numbers here for the sake of simplicity since it's just sequential, but you can replace them with named constants or enums if it makes it clearer
{
case 0: // Intro sequence (menu and shop icon coming into frame)
{
game_shop_intro(shop_jokers);
break;
}
case 1: // Shop menu input and selection
{
if (timer == 1) if (timer == 1)
{ {
tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, reroll_cost); tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, reroll_cost);
@@ -2274,6 +2272,7 @@ void game_shop()
} }
else if (key_hit(KEY_RIGHT)) else if (key_hit(KEY_RIGHT))
{ {
// TODO: Don't allow cursor on sold items
if (selection_y == 0 && selection_x < max_items_top) if (selection_y == 0 && selection_x < max_items_top)
{ {
selection_x++; selection_x++;
@@ -2288,7 +2287,7 @@ void game_shop()
memcpy16(&pal_bg_mem[5], &pal_bg_mem[16], 1); memcpy16(&pal_bg_mem[5], &pal_bg_mem[16], 1);
// Shop selection logic // Shop selection logic
if (selection_x == 0 && selection_y == 0) if (selection_x == 0 && selection_y == 0) // Next round button
{ {
memset16(&pal_bg_mem[5], 0xFFFF, 1); memset16(&pal_bg_mem[5], 0xFFFF, 1);
@@ -2297,6 +2296,7 @@ void game_shop()
// Go to next blind selection game state // Go to next blind selection game state
state = 2; // Go to the outro sequence state state = 2; // Go to the outro sequence state
timer = 0; // Reset the timer timer = 0; // Reset the timer
reroll_cost = REROLL_BASE_COST;
memcpy16(&pal_bg_mem[5], &pal_bg_mem[6], 1); memcpy16(&pal_bg_mem[5], &pal_bg_mem[6], 1);
@@ -2306,23 +2306,17 @@ void game_shop()
// And I don't care enough to fix it right now. // And I don't care enough to fix it right now.
} }
} }
else if (selection_x == 0 && selection_y == 1) else if (selection_x == 0 && selection_y == 1) // Reroll button
{ {
memset16(&pal_bg_mem[7], 0xFFFF, 1); memset16(&pal_bg_mem[7], 0xFFFF, 1);
if (key_hit(SELECT_CARD) && money >= reroll_cost) if (key_hit(SELECT_CARD) && money >= reroll_cost)
{ {
money -= reroll_cost; game_shop_reroll(shop_jokers, &reroll_cost);
display_money(money); // Update the money display
game_shop_create_items(shop_jokers, false);
reroll_cost++;
tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, reroll_cost);
} }
} }
for (int i = 0; i < MAX_SHOP_JOKERS; i++) for (int i = 0; i < MAX_SHOP_JOKERS; i++) // Item selection logic
{ {
if (shop_jokers[i] != NULL) if (shop_jokers[i] != NULL)
{ {
@@ -2354,7 +2348,63 @@ void game_shop()
} }
} }
} }
}
static void game_shop_lights_anim_frame()
{
// Shift palette around the border of the shop icon
COLOR shifted_palette[4];
memcpy16(&shifted_palette[0], &pal_bg_mem[14], 1);
memcpy16(&shifted_palette[1], &pal_bg_mem[17], 1);
memcpy16(&shifted_palette[2], &pal_bg_mem[22], 1);
memcpy16(&shifted_palette[3], &pal_bg_mem[8], 1);
// Circularly shift the palette
int last = shifted_palette[3];
for (int i = 3; i > 0; --i)
{
shifted_palette[i] = shifted_palette[i - 1];
}
shifted_palette[0] = last;
memcpy16(&pal_bg_mem[14], &shifted_palette[0], 1); // Copy the shifted palette to the next 4 slots
memcpy16(&pal_bg_mem[17], &shifted_palette[1], 1);
memcpy16(&pal_bg_mem[22], &shifted_palette[2], 1);
memcpy16(&pal_bg_mem[8], &shifted_palette[3], 1);
}
void game_shop()
{
change_background(BG_ID_SHOP);
static JokerObject *shop_jokers[MAX_SHOP_JOKERS] = {NULL};
for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{
if (shop_jokers[i] != NULL)
{
joker_object_update(shop_jokers[i]);
}
}
if (timer % 20 == 0)
{
game_shop_lights_anim_frame();
}
switch (state) // I'm only using magic numbers here for the sake of simplicity since it's just sequential, but you can replace them with named constants or enums if it makes it clearer
{
case 0:
{
game_shop_intro(shop_jokers);
break;
}
case 1:
{
game_shop_process_user_input(shop_jokers);
break; break;
} }
case 2: // Outro sequence (menu and shop icon going out of frame) case 2: // Outro sequence (menu and shop icon going out of frame)
@@ -2402,23 +2452,29 @@ void game_shop()
} }
default: default:
state = 0; // Reset the state state = 0; // Reset the state
reroll_cost = reroll_base_cost;
selection_x = 0; // Reset the selection selection_x = 0; // Reset the selection
selection_y = 0; // Reset the selection selection_y = 0; // Reset the selection
for (int i = 0; i < MAX_SHOP_JOKERS; i++) for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{ {
if (shop_jokers[i] != NULL)
{
// Make the joker available back to shop
int_list_append(jokers_available_to_shop, shop_jokers[i]->joker->id);
}
joker_object_destroy(&shop_jokers[i]); // Destroy the joker objects joker_object_destroy(&shop_jokers[i]); // Destroy the joker objects
} }
increment_blind(BLIND_DEFEATED); increment_blind(BLIND_DEFEATED); // TODO: Move to game_round_end()?
game_set_state(GAME_BLIND_SELECT); // If we reach here, we should go to the blind select state game_set_state(GAME_BLIND_SELECT); // If we reach here, we should go to the blind select state
break; break;
} }
} }
void game_blind_select() void game_blind_select()
{ {
switch (state) // I'm only using magic numbers here for the sake of simplicity since it's just sequential, but you can replace them with named constants or enums if it makes it clearer switch (state) // I'm only using magic numbers here for the sake of simplicity since it's just sequential, but you can replace them with named constants or enums if it makes it clearer
+84
View File
@@ -0,0 +1,84 @@
#include <stdlib.h>
#include <stdbool.h>
#include "list.h"
#include "util.h"
IntList *int_list_new(int init_size) {
IntList *list = (IntList *)malloc(sizeof(IntList));
if (list == NULL) return NULL;
list->_array = (int *)malloc(sizeof(int) * init_size);
if (!list->_array)
{
free(list);
return NULL;
}
list->size = 0;
list->allocated_size = init_size;
return list;
}
void int_list_destroy(IntList **list) {
if (list == NULL || *list == NULL)
return;
{
free((*list)->_array);
free(*list);
}
*list = NULL;
}
bool int_list_append(IntList *list, int value)
{
if (list->size >= list->allocated_size)
{
int new_size = list->allocated_size * 2;
int *new_arr = (int *)realloc(list->_array, sizeof(int) * new_size);
if (new_arr == NULL)
return false;
list->_array = new_arr;
list->allocated_size = new_size;
}
list->_array[list->size++] = value;
return true;
}
bool int_list_remove_by_idx(IntList *list, int index) {
if (index < 0 || index >= list->size)
return false;
for (int i = index; i < list->size - 1; ++i)
{
list->_array[i] = list->_array[i + 1];
}
list->size--;
return true;
}
bool int_list_remove_by_value(IntList *list, int value)
{
for (int i = 0; i < list->size; i++)
{
if (list->_array[i] == value)
{
return int_list_remove_by_idx(list, i);
}
}
return false;
}
int int_list_get(IntList *list, int index)
{
if (index < 0 || index >= list->size)
return 0;
return list->_array[index];
}
int int_list_get_size(IntList *list)
{
if (list == NULL)
{
return UNDEFINED;
}
return list->size;
}
+1 -1
View File
@@ -45,7 +45,7 @@ void init()
// BG1 is the main background layer // BG1 is the main background layer
REG_BG1CNT = BG_PRIO(1) | BG_CBB(MAIN_BG_CBB) | BG_SBB(MAIN_BG_SBB) | BG_8BPP; REG_BG1CNT = BG_PRIO(1) | BG_CBB(MAIN_BG_CBB) | BG_SBB(MAIN_BG_SBB) | BG_8BPP;
// BG2 is the affine background layer // BG2 is the affine background layer
REG_BG2CNT = BG_PRIO(2) | BG_CBB(AFFINE_BG_CBB) | BG_SBB(AFFINE_BG_CBB) | BG_8BPP | BG_WRAP | BG_AFF_32x32; REG_BG2CNT = BG_PRIO(2) | BG_CBB(AFFINE_BG_CBB) | BG_SBB(AFFINE_BG_SBB) | BG_8BPP | BG_WRAP | BG_AFF_32x32;
int win1_left = 72; int win1_left = 72;
int win1_top = 44; int win1_top = 44;
+6 -6
View File
@@ -145,9 +145,9 @@ void sprite_object_reset_transform(SpriteObject* sprite_object)
sprite_object->y = 0; sprite_object->y = 0;
sprite_object->vx = 0; sprite_object->vx = 0;
sprite_object->vy = 0; sprite_object->vy = 0;
sprite_object->tscale = float2fx(1.0f); // Target scale sprite_object->tscale = FIX_ONE; // Target scale
sprite_object->scale = float2fx(1.0f); sprite_object->scale = FIX_ONE;
sprite_object->vscale = float2fx(0.0f); sprite_object->vscale = 0;
sprite_object->trotation = 0; // Target rotation sprite_object->trotation = 0; // Target rotation
sprite_object->rotation = 0; sprite_object->rotation = 0;
sprite_object->vrotation = 0; sprite_object->vrotation = 0;
@@ -163,7 +163,7 @@ void sprite_object_update(SpriteObject* sprite_object)
sprite_object->vrotation += (sprite_object->trotation - sprite_object->rotation) / 8; // Rotate the card when it's played sprite_object->vrotation += (sprite_object->trotation - sprite_object->rotation) / 8; // Rotate the card when it's played
// set velocity to 0 if it's close enough to the target // set velocity to 0 if it's close enough to the target
const float epsilon = float2fx(0.01f); const FIXED epsilon = float2fx(0.01f);
if (sprite_object->vx < epsilon && sprite_object->vx > -epsilon && sprite_object->vy < epsilon && sprite_object->vy > -epsilon) if (sprite_object->vx < epsilon && sprite_object->vx > -epsilon && sprite_object->vy < epsilon && sprite_object->vy > -epsilon)
{ {
sprite_object->vx = 0; sprite_object->vx = 0;
@@ -211,8 +211,8 @@ void sprite_object_update(SpriteObject* sprite_object)
void sprite_object_shake(SpriteObject* sprite_object, mm_word sound_id) void sprite_object_shake(SpriteObject* sprite_object, mm_word sound_id)
{ {
sprite_object->vscale = float2fx(0.3f); // Scale down the card when it's scored sprite_object->vscale = float2fx(0.3f);
sprite_object->vrotation = float2fx(8.0f); // Rotate the card when it's scored 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 if (sound_id == UNDEFINED) return; // If no sound ID is provided, do nothing