diff --git a/README.md b/README.md index ded4287..135cb1b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,35 @@ # 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 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) + +# **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. diff --git a/source/game.c b/source/game.c index d84a99f..9fc8bc9 100644 --- a/source/game.c +++ b/source/game.c @@ -115,14 +115,17 @@ static inline Card *discard_pop() // Screenblock rects static const Rect ROUND_END_MENU_RECT = {9, 7, 24, 21 }; -static const Rect POP_MENU_ANIM_RECT = {9, 6, 24, 32 }; -// The rect for popping menu animations (round end, shop, blinds) - includes both the -// target and source position rects. -// 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 -// 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 +static const Rect POP_MENU_ANIM_RECT_DOWN = {9, 6, 24, 32 }; +static const Rect POP_MENU_ANIM_RECT_UP = {9, 7, 24, 32 }; +// The rects for popping menu animations (round end, shop, blinds) +// - extends beyond the visible screen to the end of the screenblock +// It includes both the target and source position rects. +// 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 +// +// 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 }; @@ -452,13 +455,15 @@ void change_background(int id) int x_from = 0; int y_from = 27; - int x_to = 9 + (i * 5); - int y_to = 31; + Rect blind_rect = SINGLE_BLIND_SELECT_RECT; - for (int y = 7; y < 40; y++) - { - memcpy16(&se_mem[MAIN_BG_SBB][x_to + 32 * (y - 1)], &se_mem[MAIN_BG_SBB][x_to + 32 * y], 5); - } + // + 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); + 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) { @@ -1546,7 +1551,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa { 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 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; } - 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; 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; } - 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); @@ -1630,7 +1635,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa } break; } - case 3: + case 3: // Sequentially display the "score min" text over the next 4 frames { // "Score Min" text 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; } - 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; @@ -1667,7 +1672,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa } 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; @@ -1675,18 +1680,18 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa { 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); - 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)); 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)); + // These just move everything else up by one tile 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]}; 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) { - 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; 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; @@ -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)); 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)); } - else + else // The rest of this just moves the panel tiles up in vram until they're gone { 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]}; @@ -1764,7 +1767,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa } break; } - case 6: // TODO: implement interest + case 6: // This state handles displaying the rewards earned from the completed round { 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; } + // TODO: implement interest // if (interest > 0) // { // 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; @@ -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}; 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 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); } - 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}; 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 } - 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; hand_reward--; @@ -1823,7 +1827,7 @@ void game_round_end() // Writing this kind a made me want to kms. If somewone wa 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; 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: { - if (timer == FRAMES(40)) + if (timer == FRAMES(40)) // Put the "cash out" button onto the round end panel { 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 } - 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 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; } - case 8: + case 8: // Shift the round end panel back out of view and go to the next state { 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) { @@ -1935,7 +1939,7 @@ void game_shop() { 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 { @@ -2034,7 +2038,7 @@ void game_shop() case 2: // Outro sequence (menu and shop icon going out of frame) { // 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); @@ -2090,7 +2094,7 @@ void game_blind_select() { 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) {