Merge branch 'main' into start_at_blind_selection

This commit is contained in:
MeirGavish
2025-07-11 19:42:52 +03:00
2 changed files with 78 additions and 45 deletions
+30 -1
View File
@@ -1,6 +1,35 @@
# Balatro GBA # Balatro GBA
This is my attempt to recreate the game 'Balatro' as accurately as possible, including all of the visual effects that make Balatro feel satisfying to play. This is my attempt to recreate the game 'Balatro' as accurately as possible, including all of the visual effects that make Balatro feel satisfying to play.
This port is strictly limited to the content that is available in the Balatro Steam demo. This port is strictly limited to the content that was available in the Balatro Steam demo.
![Example](https://github.com/cellos51/balatro-gba/blob/main/example.gif) ![Example](https://github.com/cellos51/balatro-gba/blob/main/example.gif)
# **Build Instructions:**
## **-Windows-**
1. Install `devkitPro` from https://devkitpro.org/ and navigate to `Click Here for instructions`
2. Navigate down until you find the `Download the latest version of the graphical installer` and download the latest release from the Github page.
3. Execute it and follow the installation instructions, you only need to install the tools for the GBA development so ensure you have it selected and install it to the default directory.
4. Go to the Start Menu and search for `MSys2` and open it.
5. Install `Git` by typing this command: `pacman -S git`
6. Navigate to where you want to clone the project such as your desktop or Documents folder.
The easiest way to properly `cd` to the directory is to type `cd` and drag and drop the folder into the terminal window and press Enter.
Example: `cd '/home/user/OneDrive/Documents/Folder` or something like this.
7. Clone the project with `git clone --recursive https://github.com/cellos51/balatro-gba.git` in the terminal
8. Now you want to `cd` to the new folder that was created. Use `cd` and drag the new folder into the window to add the file path and press Enter.
9. Run the `make` command in the window to start building.
10. After it completes, navigate through the `build` directory in the project folder and look for `balatro-gba.gba` and load it on an emulator or flashcart.
## **-Linux-**
[Undocumented]
### **Common Issues:**
1. **When I drag the folder into the window and press enter, it doesn't work!**
-After dragging the folder, press the right or left arrow key until the text is no longer highlighted and then press enter.
2. **When I run `make` it errors out and won't compile!**
-Move the project folder to your desktop and then `cd` to it by dragging the folder into the terminal window after typing `cd` first.
3. **I can't find the build folder!**
-Look for a folder called `build` inside the project folder and then find `balatro-gba.gba`.
4. **The Game won't start!**
-Try a different emulator or if you are using original hardware, make sure the rom is not corrupted and try a different flashcart or SD Card. If this does not work, open an issue on the Github page because a recent commit may have broke the game.
+48 -44
View File
@@ -115,14 +115,17 @@ static inline Card *discard_pop()
// Screenblock rects // Screenblock rects
static const Rect ROUND_END_MENU_RECT = {9, 7, 24, 21 }; static const Rect ROUND_END_MENU_RECT = {9, 7, 24, 21 };
static const Rect POP_MENU_ANIM_RECT = {9, 6, 24, 32 }; static const Rect POP_MENU_ANIM_RECT_DOWN = {9, 6, 24, 32 };
// The rect for popping menu animations (round end, shop, blinds) - includes both the static const Rect POP_MENU_ANIM_RECT_UP = {9, 7, 24, 32 };
// target and source position rects. // The rects for popping menu animations (round end, shop, blinds)
// Extends beyond the visible screen to the end of the screenblock // - extends beyond the visible screen to the end of the screenblock
// This is because when popping, the target position is blank so we just animate the whole thing // It includes both the target and source position rects.
// so we don't have to track its position // This is because when popping, the target position is blank so we just animate
// // the whole rect so we don't have to track its position
// When unpopping we include another row above the menu assuming it's blank so it's copied into it //
// In the down version we include another row above the menu assuming it's blank so it's copied into it
static const Rect SINGLE_BLIND_SELECT_RECT = { 9, 7, 13, 32 };
static const Rect SHOP_ICON_RECT = { 0, 0, 8, 4 }; static const Rect SHOP_ICON_RECT = { 0, 0, 8, 4 };
@@ -452,13 +455,15 @@ void change_background(int id)
int x_from = 0; int x_from = 0;
int y_from = 27; int y_from = 27;
int x_to = 9 + (i * 5); Rect blind_rect = SINGLE_BLIND_SELECT_RECT;
int y_to = 31;
for (int y = 7; y < 40; y++) // + 1 to get to the start of the next blind, no gap between them
{ blind_rect.left += i * (SINGLE_BLIND_SELECT_RECT.right - SINGLE_BLIND_SELECT_RECT.left + 1);
memcpy16(&se_mem[MAIN_BG_SBB][x_to + 32 * (y - 1)], &se_mem[MAIN_BG_SBB][x_to + 32 * y], 5); blind_rect.right += i * (SINGLE_BLIND_SELECT_RECT.right - SINGLE_BLIND_SELECT_RECT.left + 1);
} main_bg_se_copy_rect_1_tile_vert(blind_rect, SE_UP);
int x_to = blind_rect.left;
int y_to = 31;
if (i == BIG_BLIND) if (i == BIG_BLIND)
{ {
@@ -1546,7 +1551,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
{ {
case 0: case 0:
{ {
if (timer == 30) if (timer == 30) // Reset static variables to default values upon re-entering the round end state
{ {
change_background(BG_ID_ROUND_END); // Change the background to the round end background change_background(BG_ID_ROUND_END); // Change the background to the round end background
state = 1; // Change the state to the next one state = 1; // Change the state to the next one
@@ -1557,7 +1562,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
} }
break; break;
} }
case 1: case 1: // This creates the top 16 by 7 tiles of the pop up. It places it in vram, moving it up one tile each frame, not clearing the previous row of tiles so they fill the blank space as it moves up.
{ {
const int bottom_of_screen = 19; const int bottom_of_screen = 19;
int y = bottom_of_screen - timer; int y = bottom_of_screen - timer;
@@ -1609,7 +1614,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
} }
break; break;
} }
case 2: case 2: // Display the beaten blind, expand the panel border down a tile and wait until a bit until going to the next state
{ {
obj_unhide(round_end_blind_token->obj, 0); obj_unhide(round_end_blind_token->obj, 0);
@@ -1630,7 +1635,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
} }
break; break;
} }
case 3: case 3: // Sequentially display the "score min" text over the next 4 frames
{ {
// "Score Min" text // "Score Min" text
const unsigned short tile_map3[4] = {0x003E, 0x003F, 0x0040, 0x0041}; const unsigned short tile_map3[4] = {0x003E, 0x003F, 0x0040, 0x0041};
@@ -1645,7 +1650,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
} }
break; break;
} }
case 4: case 4: // Every 20 frames, display the blind reward and update the text until it reaches 0
{ {
if (timer % FRAMES(20) != 0) break; if (timer % FRAMES(20) != 0) break;
@@ -1667,7 +1672,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
} }
break; break;
} }
case 5: case 5: // This is just an incredibly convoluted way to slide the "small blind" panel out of view. It was made this way because I didn't know the better way at the time AND because I was being a perfectionist and wanted to make the bottom row of tiles on the panel not connected to the "SCORE" panel.
{ {
if (timer < FRAMES(20)) break; if (timer < FRAMES(20)) break;
@@ -1675,18 +1680,18 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
{ {
int y = 5; int y = 5;
if (sequence_step == 1) if (sequence_step == 1) // Erase the score text from the small blind panel and move it all up by one tile
{ {
tte_erase_rect(40, 16, 64, 40); tte_erase_rect(40, 16, 64, 40);
obj_hide(playing_blind_token->obj);
const unsigned short tile_map1[10] = {0x0064, 0x0065, 0x0065, 0x0066, 0x0067, 0x0067, 0x0067, 0x0068, 0x0464, se_mem[MAIN_BG_SBB][32 * y + 9]}; const unsigned short tile_map1[10] = {0x0064, 0x0065, 0x0065, 0x0066, 0x0067, 0x0067, 0x0067, 0x0068, 0x0464, se_mem[MAIN_BG_SBB][32 * y + 9]}; // Swaps the top of the score panel tiles with ones that aren't connected to the small blind panel
memcpy(&se_mem[MAIN_BG_SBB][32 * y], &tile_map1, sizeof(tile_map1)); memcpy(&se_mem[MAIN_BG_SBB][32 * y], &tile_map1, sizeof(tile_map1));
y -= 1; y -= 1;
const unsigned short tile_map2[10] = {0x006F, 0x0070, 0x0070, 0x0070, 0x0070, 0x0070, 0x0070, 0x0070, 0x046F, se_mem[MAIN_BG_SBB][32 * y + 9]}; const unsigned short tile_map2[10] = {0x006F, 0x0070, 0x0070, 0x0070, 0x0070, 0x0070, 0x0070, 0x0070, 0x046F, se_mem[MAIN_BG_SBB][32 * y + 9]}; // Swaps the bottom of the small blind panel tiles with ones that aren't connected to the score panel
memcpy(&se_mem[MAIN_BG_SBB][32 * y], &tile_map2, sizeof(tile_map2)); memcpy(&se_mem[MAIN_BG_SBB][32 * y], &tile_map2, sizeof(tile_map2));
// These just move everything else up by one tile
y = 1; y = 1;
const unsigned short tile_map3[10] = {se_mem[MAIN_BG_SBB][32 * y], se_mem[MAIN_BG_SBB][32 * y + 1], se_mem[MAIN_BG_SBB][32 * y + 2], se_mem[MAIN_BG_SBB][32 * y + 3], se_mem[MAIN_BG_SBB][32 * y + 4], se_mem[MAIN_BG_SBB][32 * y + 5], se_mem[MAIN_BG_SBB][32 * y + 6], se_mem[MAIN_BG_SBB][32 * y + 7], se_mem[MAIN_BG_SBB][32 * y + 8], se_mem[MAIN_BG_SBB][32 * (y - 1) + 9]}; const unsigned short tile_map3[10] = {se_mem[MAIN_BG_SBB][32 * y], se_mem[MAIN_BG_SBB][32 * y + 1], se_mem[MAIN_BG_SBB][32 * y + 2], se_mem[MAIN_BG_SBB][32 * y + 3], se_mem[MAIN_BG_SBB][32 * y + 4], se_mem[MAIN_BG_SBB][32 * y + 5], se_mem[MAIN_BG_SBB][32 * y + 6], se_mem[MAIN_BG_SBB][32 * y + 7], se_mem[MAIN_BG_SBB][32 * y + 8], se_mem[MAIN_BG_SBB][32 * (y - 1) + 9]};
y -= 1; y -= 1;
@@ -1704,9 +1709,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
} }
else if (sequence_step == 2) else if (sequence_step == 2)
{ {
const unsigned short tile_map1[10] = {0x0064, 0x0065, 0x0065, 0x0066, 0x0067, 0x0067, 0x0067, 0x0068, 0x0464, se_mem[MAIN_BG_SBB][32 * y + 9]};
memcpy(&se_mem[MAIN_BG_SBB][32 * y], &tile_map1, sizeof(tile_map1));
y = 1; y = 1;
const unsigned short tile_map3[10] = {se_mem[MAIN_BG_SBB][32 * y], se_mem[MAIN_BG_SBB][32 * y + 1], se_mem[MAIN_BG_SBB][32 * y + 2], se_mem[MAIN_BG_SBB][32 * y + 3], se_mem[MAIN_BG_SBB][32 * y + 4], se_mem[MAIN_BG_SBB][32 * y + 5], se_mem[MAIN_BG_SBB][32 * y + 6], se_mem[MAIN_BG_SBB][32 * y + 7], se_mem[MAIN_BG_SBB][32 * y + 8], se_mem[MAIN_BG_SBB][32 * (y - 1) + 9]}; const unsigned short tile_map3[10] = {se_mem[MAIN_BG_SBB][32 * y], se_mem[MAIN_BG_SBB][32 * y + 1], se_mem[MAIN_BG_SBB][32 * y + 2], se_mem[MAIN_BG_SBB][32 * y + 3], se_mem[MAIN_BG_SBB][32 * y + 4], se_mem[MAIN_BG_SBB][32 * y + 5], se_mem[MAIN_BG_SBB][32 * y + 6], se_mem[MAIN_BG_SBB][32 * y + 7], se_mem[MAIN_BG_SBB][32 * y + 8], se_mem[MAIN_BG_SBB][32 * (y - 1) + 9]};
y -= 1; y -= 1;
@@ -1728,10 +1731,10 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
memcpy(&se_mem[MAIN_BG_SBB][32 * y], &tile_map6, sizeof(tile_map6)); memcpy(&se_mem[MAIN_BG_SBB][32 * y], &tile_map6, sizeof(tile_map6));
y = 4; y = 4;
const unsigned short tile_map2[10] = {0x046B, 0x001F, 0x001F, 0x001F, 0x001F, 0x001F, 0x001F, 0x001F, 0x006B, se_mem[MAIN_BG_SBB][32 * y + 9]}; const unsigned short tile_map2[10] = {0x046B, 0x001F, 0x001F, 0x001F, 0x001F, 0x001F, 0x001F, 0x001F, 0x006B, se_mem[MAIN_BG_SBB][32 * y + 9]}; // This sets the uncleared bottom row of the small blind panel to empty tiles that match the stats panel
memcpy(&se_mem[MAIN_BG_SBB][32 * y], &tile_map2, sizeof(tile_map2)); memcpy(&se_mem[MAIN_BG_SBB][32 * y], &tile_map2, sizeof(tile_map2));
} }
else else // The rest of this just moves the panel tiles up in vram until they're gone
{ {
y = 1; y = 1;
const unsigned short tile_map3[10] = {se_mem[MAIN_BG_SBB][32 * y], se_mem[MAIN_BG_SBB][32 * y + 1], se_mem[MAIN_BG_SBB][32 * y + 2], se_mem[MAIN_BG_SBB][32 * y + 3], se_mem[MAIN_BG_SBB][32 * y + 4], se_mem[MAIN_BG_SBB][32 * y + 5], se_mem[MAIN_BG_SBB][32 * y + 6], se_mem[MAIN_BG_SBB][32 * y + 7], se_mem[MAIN_BG_SBB][32 * y + 8], se_mem[MAIN_BG_SBB][32 * (y - 1) + 9]}; const unsigned short tile_map3[10] = {se_mem[MAIN_BG_SBB][32 * y], se_mem[MAIN_BG_SBB][32 * y + 1], se_mem[MAIN_BG_SBB][32 * y + 2], se_mem[MAIN_BG_SBB][32 * y + 3], se_mem[MAIN_BG_SBB][32 * y + 4], se_mem[MAIN_BG_SBB][32 * y + 5], se_mem[MAIN_BG_SBB][32 * y + 6], se_mem[MAIN_BG_SBB][32 * y + 7], se_mem[MAIN_BG_SBB][32 * y + 8], se_mem[MAIN_BG_SBB][32 * (y - 1) + 9]};
@@ -1764,7 +1767,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
} }
break; break;
} }
case 6: // TODO: implement interest case 6: // This state handles displaying the rewards earned from the completed round
{ {
if (timer < FRAMES(20)) break; if (timer < FRAMES(20)) break;
@@ -1776,12 +1779,13 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
hand_y = 1; hand_y = 1;
} }
// TODO: implement interest
// if (interest > 0) // if (interest > 0)
// { // {
// interest_y = 1 + hand_y; // interest_y = 1 + hand_y;
// } // }
if (sequence_step == 0) if (sequence_step == 0) // Expand the black part of the panel down by one tile
{ {
int y = 14; int y = 14;
@@ -1791,7 +1795,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
const unsigned short tile_map2[17] = {se_mem[MAIN_BG_SBB][8 + 32 * y], 0x002A, 0x0055, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0455, 0x042A}; const unsigned short tile_map2[17] = {se_mem[MAIN_BG_SBB][8 + 32 * y], 0x002A, 0x0055, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0056, 0x0455, 0x042A};
memcpy(&se_mem[MAIN_BG_SBB][8 + 32 * y], tile_map2, sizeof(tile_map2)); memcpy(&se_mem[MAIN_BG_SBB][8 + 32 * y], tile_map2, sizeof(tile_map2));
} }
else if (sequence_step < 15) else if (sequence_step < 15) // Use TTE to print '.' until the end of the panel width
{ {
// Print the separator dots // Print the separator dots
int x = (9 + sequence_step) * 8; int x = (9 + sequence_step) * 8;
@@ -1799,11 +1803,11 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
tte_printf("#{P:%d,%d; cx:0xF000}.", x, y); tte_printf("#{P:%d,%d; cx:0xF000}.", x, y);
} }
else if (sequence_step >= 30 && hand_reward > 0) else if (sequence_step >= 30 && hand_reward > 0) // Wait an additional 15 frames since the last sequenced action
{ {
if (sequence_step == 30) if (sequence_step == 30) // Expand the black part of the panel down by one tile again
{ {
int y = 14 + hand_y; int y = 14 + hand_y; // This was intended for modularity so that I can add interest rewards later
const unsigned short tile_map1[17] = {se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], 0x002A, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x042A}; const unsigned short tile_map1[17] = {se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], 0x002A, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x042A};
memcpy(&se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], tile_map1, sizeof(tile_map1)); memcpy(&se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], tile_map1, sizeof(tile_map1));
@@ -1813,7 +1817,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
tte_printf("#{P:%d,%d; cx:0xD000}%d #{cx:0xF000}Hands", ROUND_END_NUM_HANDS_RECT.left, ROUND_END_NUM_HANDS_RECT.top, hand_reward); // Print the hand reward tte_printf("#{P:%d,%d; cx:0xD000}%d #{cx:0xF000}Hands", ROUND_END_NUM_HANDS_RECT.left, ROUND_END_NUM_HANDS_RECT.top, hand_reward); // Print the hand reward
} }
else if (sequence_step > 45 && timer % FRAMES(20) == 0) else if (sequence_step > 45 && timer % FRAMES(20) == 0) // After 15 frames, every 20 frames, increment the hand reward text until the hand reward variable is depleted
{ {
int y = (13 + hand_y) * 8; int y = (13 + hand_y) * 8;
hand_reward--; hand_reward--;
@@ -1823,7 +1827,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
sequence_step++; sequence_step++;
if (hand_reward <= 0 && interest_reward <= 0) if (hand_reward <= 0 && interest_reward <= 0) // Once all rewards are accounted for go to the next state
{ {
sequence_step = 0; sequence_step = 0;
timer = 0; // Reset the timer timer = 0; // Reset the timer
@@ -1834,7 +1838,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
} }
case 7: case 7:
{ {
if (timer == FRAMES(40)) if (timer == FRAMES(40)) // Put the "cash out" button onto the round end panel
{ {
int y = 8; int y = 8;
@@ -1854,7 +1858,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
tte_printf("#{P:%d, %d; cx:0xF000}Cash Out: $%d", CASHOUT_RECT.left, CASHOUT_RECT.top, hands + blind_get_reward(current_blind)); // Print the cash out amount tte_printf("#{P:%d, %d; cx:0xF000}Cash Out: $%d", CASHOUT_RECT.left, CASHOUT_RECT.top, hands + blind_get_reward(current_blind)); // Print the cash out amount
} }
else if (timer > FRAMES(40) && key_hit(KEY_A)) else if (timer > FRAMES(40) && key_hit(KEY_A)) // Wait until the player presses A to cash out
{ {
state = 8; // Go to the next state state = 8; // Go to the next state
money += hands + blind_get_reward(current_blind); // Reward the player money += hands + blind_get_reward(current_blind); // Reward the player
@@ -1867,10 +1871,10 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa
break; break;
} }
case 8: case 8: // Shift the round end panel back out of view and go to the next state
{ {
sequence_step++; sequence_step++;
main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_DOWN); main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT_DOWN, SE_DOWN);
if (sequence_step >= 20) if (sequence_step >= 20)
{ {
@@ -1935,7 +1939,7 @@ void game_shop()
{ {
case 0: // Intro sequence (menu and shop icon coming into frame) case 0: // Intro sequence (menu and shop icon coming into frame)
{ {
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_UP, SE_UP);
if (timer >= 7) // Shift the shop icon if (timer >= 7) // Shift the shop icon
{ {
@@ -2034,7 +2038,7 @@ void game_shop()
case 2: // Outro sequence (menu and shop icon going out of frame) case 2: // Outro sequence (menu and shop icon going out of frame)
{ {
// Shift the shop panel // Shift the shop panel
main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_DOWN); main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT_DOWN, SE_DOWN);
main_bg_se_copy_rect_1_tile_vert(SHOP_ICON_RECT, SE_UP); main_bg_se_copy_rect_1_tile_vert(SHOP_ICON_RECT, SE_UP);
@@ -2090,7 +2094,7 @@ void game_blind_select()
{ {
case 0: // Intro sequence (menu coming into frame) case 0: // Intro sequence (menu coming into frame)
{ {
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_UP, SE_UP);
if (timer == 12) if (timer == 12)
{ {