diff --git a/.gitignore b/.gitignore index 542edaa..be9866f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /build /.vs *.sln -*.vcxproj* \ No newline at end of file +*.vcxproj* +/balatro-gba/x64/** diff --git a/README.md b/README.md index 0270f47..1e8f868 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,26 @@ # Balatro GBA This is an 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 was available in the January 2024 Balatro Steam demo. -### **Disclaimer: This project is NOT endorsed or supported by Playstack or LocalThunk** -#### This project is solely aimed to recreate Balatro's old demo on the Game Boy Advance and is not meant to infringe or draw sales away from the full game's release or any of the established works by Playstack and LocalThunk. All rights are reserved to their respective holders. +This port is strictly limited in content to a minimal version of Balatro and will not recreate the full game. +### **Disclaimer: This project is NOT endorsed by or affiliated with Playstack or LocalThunk** +#### This is a non-profit community fan project solely aimed to recreate a minimal version of Balatro on the Game Boy Advance as a tribute to the full Balatro and is not meant to infringe or draw sales away from the full game's release or any of the established works by Playstack and LocalThunk. +#### All rights are reserved to their respective holders. +This version is meant for people who already own and know the official full game. + +Buy the official full version + +[Balatro on Steam](https://store.steampowered.com/app/2379780/Balatro/) + +[Balatro on Google Play](https://play.google.com/store/apps/details?id=com.playstack.balatro.android) + +[Balatro on Apple App Store](https://apps.apple.com/us/app/balatro/id6502453075) + +[Balatro on Switch](https://www.nintendo.com/us/store/products/balatro-switch/) + +[Balatro on PlayStation Store](https://store.playstation.com/en-us/concept/10010334) + +[Balatro on Xbox](https://www.xbox.com/games/store/balatro/9PK087LNGJC5) + ![Example](https://github.com/cellos51/balatro-gba/blob/main/example.gif) # **Build Instructions:** @@ -22,7 +39,7 @@ Example: `cd '/home/user/OneDrive/Documents/balatro-gba` or something like this. 9. 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-** -[Not Available] +[No Instructions Available] ### **Common Issues:** 1. **When I drag the folder into the window and press enter, it doesn't work!** @@ -36,5 +53,3 @@ Example: `cd '/home/user/OneDrive/Documents/balatro-gba` or something like this. 5. **It says I don't have `Git` or `Make` installed!** -Use `pacman -S git` or `pacman -S make` although it should already be installed if you followed the instructions correctly. -#### Purchasing the official game is highly recommended and almost mandatory in order for this project to have a chance of staying up due to copyright issues and potential legal action. -**Purchase the Steam Version here: https://store.steampowered.com/app/2379780/Balatro/** diff --git a/graphics/deck_gfx.png b/graphics/deck_gfx.png index 1b77526..98c61d9 100644 Binary files a/graphics/deck_gfx.png and b/graphics/deck_gfx.png differ diff --git a/graphics/joker_gfx10.grit b/graphics/joker_gfx10.grit new file mode 100644 index 0000000..a8ac3f6 --- /dev/null +++ b/graphics/joker_gfx10.grit @@ -0,0 +1 @@ +-gB4 -Mw4 -Mh4 -m! -pn 16 \ No newline at end of file diff --git a/graphics/joker_gfx10.png b/graphics/joker_gfx10.png new file mode 100644 index 0000000..c5b269d Binary files /dev/null and b/graphics/joker_gfx10.png differ diff --git a/graphics/joker_gfx11.grit b/graphics/joker_gfx11.grit new file mode 100644 index 0000000..a8ac3f6 --- /dev/null +++ b/graphics/joker_gfx11.grit @@ -0,0 +1 @@ +-gB4 -Mw4 -Mh4 -m! -pn 16 \ No newline at end of file diff --git a/graphics/joker_gfx11.png b/graphics/joker_gfx11.png new file mode 100644 index 0000000..07b537c Binary files /dev/null and b/graphics/joker_gfx11.png differ diff --git a/graphics/joker_gfx2.png b/graphics/joker_gfx2.png index b848ff0..bbdf59b 100644 Binary files a/graphics/joker_gfx2.png and b/graphics/joker_gfx2.png differ diff --git a/graphics/joker_gfx3.png b/graphics/joker_gfx3.png index 673aa12..9e84c1e 100644 Binary files a/graphics/joker_gfx3.png and b/graphics/joker_gfx3.png differ diff --git a/graphics/joker_gfx4.png b/graphics/joker_gfx4.png index e2074c6..76dd5b9 100644 Binary files a/graphics/joker_gfx4.png and b/graphics/joker_gfx4.png differ diff --git a/graphics/joker_gfx5.png b/graphics/joker_gfx5.png index c220cf3..bc4e112 100644 Binary files a/graphics/joker_gfx5.png and b/graphics/joker_gfx5.png differ diff --git a/graphics/joker_gfx6.png b/graphics/joker_gfx6.png index 7620341..2a86ee5 100644 Binary files a/graphics/joker_gfx6.png and b/graphics/joker_gfx6.png differ diff --git a/graphics/joker_gfx8.png b/graphics/joker_gfx8.png index 01dbbdd..eaf2fbc 100644 Binary files a/graphics/joker_gfx8.png and b/graphics/joker_gfx8.png differ diff --git a/graphics/joker_gfx9.grit b/graphics/joker_gfx9.grit new file mode 100644 index 0000000..a8ac3f6 --- /dev/null +++ b/graphics/joker_gfx9.grit @@ -0,0 +1 @@ +-gB4 -Mw4 -Mh4 -m! -pn 16 \ No newline at end of file diff --git a/graphics/joker_gfx9.png b/graphics/joker_gfx9.png new file mode 100644 index 0000000..d220413 Binary files /dev/null and b/graphics/joker_gfx9.png differ diff --git a/include/affine_background.h b/include/affine_background.h new file mode 100644 index 0000000..8e5f78f --- /dev/null +++ b/include/affine_background.h @@ -0,0 +1,17 @@ +#ifndef AFFINE_BACKGROUND_H +#define AFFINE_BACKGROUND_H + +#include + +#include "graphic_utils.h" + +#define AFFINE_BG_PAL_LEN 5 +#define AFFINE_BG_PB (PAL_ROW_LEN * 15) // This isn't really a palette bank, just the starting index of the palette + +void affine_background_init(); +void affine_background_update(); +void affine_background_set_color(COLOR color); +// Must be called with an array of size at least AFFINE_BG_PAL_LEN +void affine_background_load_palette(const u16 *src); + +#endif // AFFINE_BACKGROUND_H diff --git a/include/game.h b/include/game.h index 6fdaec1..55e8730 100644 --- a/include/game.h +++ b/include/game.h @@ -6,6 +6,8 @@ #define MAX_JOKERS_HELD_SIZE 5 // This doesn't account for negatives right now. #define MAX_SHOP_JOKERS 2 // TODO: Make this dynamic and allow for other items besides jokers #define MAX_SELECTION_SIZE 5 +#define MAX_CARD_SCORE_DIGITS 2 // Current digit limit for score received from cards including mult etc. from jokers +#define MAX_CARD_SCORE_STR_LEN (MAX_CARD_SCORE_DIGITS + 1) // For the '+' or 'X' #define FRAMES(x) (((x) + game_speed - 1) / game_speed) // TODO: Turn into enum? @@ -77,9 +79,12 @@ typedef struct CardObject CardObject; // forward declaration, actually declared typedef struct JokerObject JokerObject; CardObject** get_hand_array(void); int get_hand_top(void); +int hand_get_size(void); CardObject** get_played_array(void); int get_played_top(void); JokerObject** get_jokers(void); int get_jokers_top(void); +int get_num_discards_remaining(void); + #endif // GAME_H \ No newline at end of file diff --git a/include/graphic_utils.h b/include/graphic_utils.h index 6c169c7..2e80828 100644 --- a/include/graphic_utils.h +++ b/include/graphic_utils.h @@ -43,6 +43,9 @@ // Tile size in pixels, both height and width as tiles are square #define TILE_SIZE 8 +// By default TTE characters occupy a single tile +#define TTE_CHAR_SIZE TILE_SIZE + typedef struct { int left; @@ -117,4 +120,22 @@ void tte_erase_rect_wrapper(Rect rect); */ void update_text_rect_to_right_align_num(Rect* rect, int num, int overflow_direction); -#endif //GRAPHIC_UTILS_H \ No newline at end of file +/*Copies 16 bit data from src to dst, applying a palette offset to the data. + * This is intended solely for use with tile8/8bpp data for dst and src. + * The palette offset allows the tiles to use a different location in the palette + * memory + * This is useful because grit always loads the palette to the beginning of + * pal_bg_mem[] + */ +void memcpy16_tile8_with_palette_offset(u16* dst, const u16* src, uint hwcount, u8 palette_offset); + +/*Copies 32 bit data from src to dst, applying a palette offset to the data. + * This is intended solely for use with tile8/8bpp data for dst and src. + * The palette offset allows the tiles to use a different location in the palette + * memory + * This is useful because grit always loads the palette to the beginning of + * pal_bg_mem[] + */ +void memcpy32_tile8_with_palette_offset(u32* dst, const u32* src, uint wcount, u8 palette_offset); + +#endif //GRAPHIC_UTILS_H diff --git a/include/joker_gfx.h b/include/joker_gfx.h index 92c3edc..75cfbbc 100644 --- a/include/joker_gfx.h +++ b/include/joker_gfx.h @@ -10,5 +10,8 @@ #include "joker_gfx6.h" #include "joker_gfx7.h" #include "joker_gfx8.h" +#include "joker_gfx9.h" +#include "joker_gfx10.h" +#include "joker_gfx11.h" #endif \ No newline at end of file diff --git a/include/list.h b/include/list.h new file mode 100644 index 0000000..fd1c366 --- /dev/null +++ b/include/list.h @@ -0,0 +1,25 @@ +#ifndef LIST_H +#define LIST_H + +#include + +typedef struct List +{ + void** _array; + int size; + int allocated_size; +} List; + +List *list_new(int init_size); +void list_destroy(List **list); +bool list_append(List *list, void *value); +bool list_remove_by_idx(List *list, int index); +void* list_get(List *list, int index); +int list_get_size(List *list); +bool list_remove_by_value(List *list, void *value); + +bool int_list_append(List *list, intptr_t value); +intptr_t int_list_get(List *list, int index); +bool int_list_remove_by_value(List *list, intptr_t value); + +#endif diff --git a/include/selection_grid.h b/include/selection_grid.h new file mode 100644 index 0000000..9ddd2bf --- /dev/null +++ b/include/selection_grid.h @@ -0,0 +1,41 @@ +#ifndef SELECTION_GRID_H +#define SELECTION_GRID_H +#include + +typedef POINT Selection; + +struct SelectionGridRow; +struct SelectionGrid; +typedef struct SelectionGridRow SelectionGridRow; +typedef struct SelectionGrid SelectionGrid; + +// Called whenever there is a change in the selection cursor +// row_idx is the index of the row whose function is invoked - can be used to identify whether it is the previous or new selection row. +typedef void (*RowOnSelectionChangedFunc)(SelectionGrid* selection_grid, int row_idx, const Selection* prev_selection, const Selection* new_selection); +typedef int (*RowGetSizeFunc)(); +// Called for any non-directional key hit +// The key will not be passed, the function will have to check key_hit() etc. for the key it wants to check +typedef void (*RowOnKeyHitFunc)(SelectionGrid* selection_grid, Selection* selection); + +struct SelectionGridRow +{ + int row_idx; + RowGetSizeFunc get_size; + RowOnSelectionChangedFunc on_selection_changed; + RowOnKeyHitFunc on_key_hit; +}; + +struct SelectionGrid +{ + SelectionGridRow* rows; + const int num_rows; + Selection selection; + }; + + +void selection_grid_process_input(SelectionGrid* selection_grid); + +void selection_grid_move_selection_horz(SelectionGrid* selection_grid, int direction_tribool); +void selection_grid_move_selection_vert(SelectionGrid* selection_grid, int direction_tribool); + +#endif \ No newline at end of file diff --git a/include/sprite.h b/include/sprite.h index b4d6bca..b3f27b8 100644 --- a/include/sprite.h +++ b/include/sprite.h @@ -27,6 +27,8 @@ typedef struct FIXED rotation; FIXED vrotation; bool selected; + bool focused; + } SpriteObject; // Sprite methods @@ -58,6 +60,7 @@ void sprite_object_shake(SpriteObject* sprite_object, mm_word sound_id); void sprite_object_set_selected(SpriteObject* sprite_object, bool selected); bool sprite_object_is_selected(SpriteObject* sprite_object); Sprite* sprite_object_get_sprite(SpriteObject* sprite_object); - +void sprite_object_set_focus(SpriteObject* sprite_object, bool focus); +bool sprite_object_is_focused(SpriteObject* sprite_object); #endif // SPRITE_H \ No newline at end of file diff --git a/include/util.h b/include/util.h index 5b3160e..7d9b915 100644 --- a/include/util.h +++ b/include/util.h @@ -37,6 +37,8 @@ static inline int get_digits_even(int n) #define NUM_ELEM_IN_ARR(arr) (sizeof(arr) / sizeof((arr)[0])) +#define INT_MAX_DIGITS 10 // strlen(str(INT_MAX)) = strlen("2147483647") + int int_arr_max(int int_arr[], int size); #endif // UTIL_H \ No newline at end of file diff --git a/source/affine_background.c b/source/affine_background.c new file mode 100644 index 0000000..f9e15e7 --- /dev/null +++ b/source/affine_background.c @@ -0,0 +1,46 @@ +#include "affine_background.h" +#include "affine_background_gfx.h" + +#include "graphic_utils.h" + +BG_AFFINE bgaff; +AFF_SRC_EX asx = {32<<8, 64<<8, 120, 80, 0x0100, 0x0100, 0}; + +void affine_background_init() +{ + memcpy32_tile8_with_palette_offset((u32*)&tile8_mem[AFFINE_BG_CBB], (const u32*)affine_background_gfxTiles, affine_background_gfxTilesLen/4, AFFINE_BG_PB); + GRIT_CPY(&se_mem[AFFINE_BG_SBB], affine_background_gfxMap); + affine_background_load_palette(affine_background_gfxPal); + + bgaff = bg_aff_default; +} + +void affine_background_update() +{ + static uint timer = 0; + timer++; + + // These values are not permament, just the current configuration for the affine background + asx.tex_x += 5; + asx.tex_y += 12; + asx.sx = (lu_sin(timer * 100)) >> 8; // Scale the sine value to fit in a s16 + asx.sx += 256; // Add 256 to the sine value to make it positive + asx.sy = (lu_sin(timer * 100 + 0x4000)) >> 8; // Scale the sine value to fit in a s16 + asx.sy += 256; // Add 256 to the sine value to make it positive + bg_rotscale_ex(&bgaff, &asx); + REG_BG_AFFINE[2] = bgaff; +} + +void affine_background_set_color(COLOR color) +{ + memcpy16(&pal_bg_mem[AFFINE_BG_PB], affine_background_gfxPal, AFFINE_BG_PAL_LEN); + for (int i = 0; i < AFFINE_BG_PAL_LEN; i++) + { + clr_rgbscale(&pal_bg_mem[AFFINE_BG_PB] + i, &pal_bg_mem[AFFINE_BG_PB] + i, 1, color); + } +} + +void affine_background_load_palette(const u16 *src) +{ + memcpy16(&pal_bg_mem[AFFINE_BG_PB], src, AFFINE_BG_PAL_LEN); +} \ No newline at end of file diff --git a/source/game.c b/source/game.c index 03dad5a..0c3f9f0 100644 --- a/source/game.c +++ b/source/game.c @@ -11,16 +11,21 @@ #include "hand_analysis.h" #include "blind.h" #include "joker.h" +#include "affine_background.h" #include "graphic_utils.h" #include "tonc_video.h" #include "audio_utils.h" +#include "selection_grid.h" #include "background_gfx.h" #include "background_shop_gfx.h" #include "background_blind_select_gfx.h" +#include "affine_background_gfx.h" #include "soundbank.h" +#include "list.h" + // This seed system is temporary. static uint rng_seed = 2048; static bool seeded = false; @@ -29,7 +34,7 @@ static uint timer = 0; // This might already exist in libtonc but idk so i'm jus static int game_speed = 1; static int background = 0; -static enum GameState game_state = GAME_SHOP; // The current game state, this is used to determine what the game is doing at any given time +static enum GameState game_state = GAME_BLIND_SELECT; // The current game state, this is used to determine what the game is doing at any given time static enum HandState hand_state = HAND_DRAW; static enum PlayState play_state = PLAY_PLAYING; static int state = 0; // General state variable, used for switch statements in each game state related function @@ -148,6 +153,11 @@ int get_hand_top(void) { return hand_top; } +int hand_get_size(void) +{ + return hand_top + 1; +} + CardObject **get_played_array(void) { return played; } @@ -164,6 +174,8 @@ int get_jokers_top(void) { return jokers_top; } +List* jokers_available_to_shop; // List of joker IDs + // Consts // Rects left top right bottom @@ -228,17 +240,24 @@ static const Rect ROUND_END_NUM_HANDS_RECT = {88, 116, UNDEFINED, UNDEF static const Rect HAND_REWARD_RECT = {168, UNDEFINED, UNDEFINED, UNDEFINED }; static const Rect CASHOUT_RECT = {88, 72, UNDEFINED, UNDEFINED }; static const Rect SHOP_REROLL_RECT = {88, 96, UNDEFINED, UNDEFINED }; +#define ITEM_SHOP_Y 71 // TODO: Needs to be a rect? //TODO: Properly define and use #define MENU_POP_OUT_ANIM_FRAMES 20 #define SCORED_CARD_TEXT_Y 48 +#define HIGHLIGHT_COLOR 0xFFFF + #define PITCH_STEP_DISCARD_SFX (-64) #define PITCH_STEP_DRAW_SFX 24 #define PITCH_STEP_UNDISCARD_SFX 2*PITCH_STEP_DRAW_SFX // Naming the stage where cards return from the discard pile to the deck "undiscard" +int get_num_discards_remaining(void) { + return discards; +} + // General functions void set_seed(int seed) { @@ -428,6 +447,8 @@ void change_background(int id) else if (current_blind == BOSS_BLIND) { main_bg_se_copy_rect(BOSS_BLIND_TITLE_SRC_RECT, TOP_LEFT_BLIND_TITLE_POINT); + + affine_background_set_color(blind_get_color(BOSS_BLIND, BLIND_SHADOW_COLOR_INDEX)); } bg_copy_current_item_to_top_left_panel(); @@ -849,11 +870,6 @@ void hand_change_sort() sort_cards(); } -int hand_get_size() -{ - return hand_top + 1; -} - int hand_get_max_size() { return hand_size; @@ -990,8 +1006,21 @@ void game_set_state(enum GameState new_game_state) game_state = new_game_state; } +void jokers_available_to_shop_init() +{ + int num_defined_jokers = get_joker_registry_size(); + jokers_available_to_shop = list_new(num_defined_jokers); + for (intptr_t i = 0; i < num_defined_jokers; i++) + { + // Add all joker IDs sequentially + int_list_append(jokers_available_to_shop, i); + } +} + void game_init() { + jokers_available_to_shop_init(); + hands = max_hands; discards = max_discards; @@ -1551,7 +1580,7 @@ static void played_cards_update_loop(bool* discarded_card, int* played_selection FIXED played_x = int2fx(120); 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; @@ -1615,7 +1644,7 @@ static void played_cards_update_loop(bool* discarded_card, int* played_selection tte_set_special(0xD000); // Set text color to blue from background memory // Write the score to a character buffer variable - char score_buffer[5]; // Assuming the maximum score is 99, we need 4 characters (2 digits + null terminator) + char score_buffer[INT_MAX_DIGITS + 2]; // for '+' and null terminator snprintf(score_buffer, sizeof(score_buffer), "+%d", card_get_value(played[j]->card)); tte_write(score_buffer); @@ -1764,11 +1793,6 @@ static void held_jokers_update_loop() jokers[i]->sprite_object->tx = hand_x - int2fx(spacing_lut[jokers_top][i]); jokers[i]->sprite_object->ty = hand_y; - if (joker_object_is_selected(jokers[i])) - { - jokers[i]->sprite_object->ty -= int2fx(10); - } - joker_object_update(jokers[i]); } } @@ -1810,41 +1834,6 @@ static void game_round_end_cashout() 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() { // TODO: Blind rect sliding into view animation... @@ -1986,6 +1975,7 @@ void game_round_end() tte_erase_rect_wrapper(BLIND_REWARD_RECT); tte_erase_rect_wrapper(BLIND_REQ_TEXT_RECT); obj_hide(playing_blind_token->obj); + affine_background_load_palette(affine_background_gfxPal); state = 5; timer = 0; } @@ -2146,13 +2136,60 @@ void game_round_end() } } -void game_shop_intro(JokerObject* shop_jokers[]) +// Shop +static List *shop_jokers = NULL; +#define REROLL_BASE_COST 5 // Base cost for rerolling the shop items +static int reroll_cost = REROLL_BASE_COST; + +#define NEXT_ROUND_BTN_SEL_X 0 + +#define NEXT_ROUND_BTN_FRAME_PAL_IDX 5 +#define NEXT_ROUND_BTN_PAL_IDX 16 +#define REROLL_BTN_FRAME_PAL_IDX 7 +#define REROLL_BTN_PAL_IDX 3 + +static void game_shop_create_items() +{ + tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT); + if (list_get_size(jokers_available_to_shop) == 0) + { + // No jokers to create + return; + } + + shop_jokers = list_new(MAX_SHOP_JOKERS); + + for (int i = 0; i < MAX_SHOP_JOKERS; i++) + { + // TODO: weight the random choice by joker rarity + int joker_idx = random() % list_get_size(jokers_available_to_shop); + intptr_t joker_id = int_list_get(jokers_available_to_shop, joker_idx); + list_remove_by_idx(jokers_available_to_shop, joker_idx); + + JokerObject *joker_object = joker_object_new(joker_new(joker_id)); + + joker_object->sprite_object->x = int2fx(120 + i * CARD_SPRITE_SIZE); + joker_object->sprite_object->y = int2fx(160); + joker_object->sprite_object->tx = joker_object->sprite_object->x; + joker_object->sprite_object->ty = int2fx(ITEM_SHOP_Y); + + int x = fx2int(joker_object->sprite_object->tx) + TILE_SIZE - (get_digits_even(joker_object->joker->value) - 1) * TILE_SIZE; + int y = fx2int(joker_object->sprite_object->ty) + CARD_SPRITE_SIZE + TILE_SIZE; + tte_printf("#{P:%d,%d; cx:0xC000}$%d", x, y, joker_object->joker->value); + + sprite_position(joker_object_get_sprite(joker_object), fx2int(joker_object->sprite_object->x), fx2int(joker_object->sprite_object->y)); + list_append(shop_jokers, joker_object); + } +} + +// Intro sequence (menu and shop icon coming into frame) +static void game_shop_intro() { main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_UP); if (timer == 1) { - game_shop_create_items(shop_jokers, true); + game_shop_create_items(); } if (timer >= 7) // Shift the shop icon @@ -2179,237 +2216,323 @@ void game_shop_intro(JokerObject* shop_jokers[]) } } +static void game_shop_reroll(int *reroll_cost) +{ + money -= *reroll_cost; + display_money(money); // Update the money display + for (int i = 0; i < list_get_size(shop_jokers); i++) + { + JokerObject *joker_object = list_get(shop_jokers, i); + if (joker_object != NULL) + { + int_list_append(jokers_available_to_shop, joker_object->joker->id); + joker_object_destroy(&joker_object); // Destroy the joker object if it exists + } + } + + list_destroy(&shop_jokers); + + game_shop_create_items(); + + for (int i = 0; i < list_get_size(shop_jokers); i++) + { + JokerObject *joker_object = list_get(shop_jokers, i); + if (joker_object != NULL) + { + joker_object->sprite_object->y = joker_object->sprite_object->ty; // Set the y position to the target position + joker_object_shake(joker_object, UNDEFINED); // Give the joker a little wiggle animation + } + } + + (*reroll_cost)++; + tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, *reroll_cost); +} + +// Shop input +static int shop_top_row_get_size() +{ + return list_get_size(shop_jokers) + 1; // + 1 to account for next round button +} + +static void game_shop_buy_joker(int shop_joker_idx) +{ + JokerObject *joker_object = list_get(shop_jokers, shop_joker_idx); + joker_push(joker_object); + money -= joker_object->joker->value; // Deduct the money spent on the joker + display_money(money); // Update the money display + + Rect joker_price_rect; + joker_price_rect.left = fx2int(joker_object->sprite_object->tx); + // 2*TILE_SIZE because the item is highlighted and raised by 1 additional tile + joker_price_rect.top = fx2int(joker_object->sprite_object->ty) + CARD_SPRITE_SIZE + 2 * TILE_SIZE; + joker_price_rect.right = joker_price_rect.left + TILE_SIZE * 3; + joker_price_rect.bottom = joker_price_rect.top + TILE_SIZE; + + tte_erase_rect_wrapper(joker_price_rect); + + list_remove_by_idx(shop_jokers, shop_joker_idx); // Remove the joker from the shop +} + +static void shop_top_row_on_key_hit(SelectionGrid* selection_grid, Selection* selection) +{ + if (!key_hit(SELECT_CARD)) + return; + + if (selection->x == NEXT_ROUND_BTN_SEL_X) + { + // Go to next blind selection game state + state = 2; // Go to the outro sequence state + timer = 0; // Reset the timer + reroll_cost = REROLL_BASE_COST; + + memcpy16(&pal_bg_mem[NEXT_ROUND_BTN_FRAME_PAL_IDX], &pal_bg_mem[6], 1); + + // memcpy16(&pal_bg_mem[16], &pal_bg_mem[6], 1); + // This changes the color of the button to a dark red. + // However, it shares a palette with the shop icon, so it will change the color of the shop icon as well. + // And I don't care enough to fix it right now. + } + else + { + int shop_joker_idx = selection->x - 1; // - 1 to account for next round button + JokerObject *joker_object = list_get(shop_jokers, shop_joker_idx); + if (joker_object == NULL + || jokers_top >= MAX_JOKERS_HELD_SIZE - 1 + || money < joker_object->joker->value) + { + return; + } + + game_shop_buy_joker(shop_joker_idx); + + // In Balatro the selection actually stays on the purchased joker it's easier to just move it left + selection_grid_move_selection_horz(selection_grid, -1); + } +} + + + +static void shop_top_row_on_selection_changed(SelectionGrid* selection_grid, int row_idx, + const Selection* prev_selection, + const Selection* new_selection) +{ + // The selection grid system only guarantees that the new selection is within bounds + if (prev_selection->y == row_idx && prev_selection->x >= 0 && prev_selection->x < shop_top_row_get_size()) + { + if (prev_selection->x == NEXT_ROUND_BTN_SEL_X) + { + // Remove next round button highlight + memcpy16(&pal_bg_mem[NEXT_ROUND_BTN_FRAME_PAL_IDX], &pal_bg_mem[NEXT_ROUND_BTN_PAL_IDX], 1); + } + else + { + JokerObject *joker = list_get(shop_jokers, prev_selection->x - 1); + sprite_object_set_focus(joker->sprite_object, false); + // -1 to account for next round button + } + } + + if (new_selection->y == row_idx) + { + if (new_selection->x == NEXT_ROUND_BTN_SEL_X) + { + // Highlight next round button + memset16(&pal_bg_mem[NEXT_ROUND_BTN_FRAME_PAL_IDX], HIGHLIGHT_COLOR, 1); + } + else + { + JokerObject *joker = list_get(shop_jokers, new_selection->x - 1); + sprite_object_set_focus(joker->sprite_object, true); + // -1 to account for next round button + } + } +} + +static int shop_bottom_row_get_size() +{ + return 1; // Currently only the reroll button +} + +static void shop_bottom_row_on_selection_changed(SelectionGrid* selection_grid, int row_idx, const Selection* prev_selection, const Selection* new_selection) +{ + if (row_idx == prev_selection->y) + { + // Remove highlight + memcpy16(&pal_bg_mem[REROLL_BTN_FRAME_PAL_IDX], &pal_bg_mem[REROLL_BTN_PAL_IDX], 1); + } + else if (row_idx == new_selection->y) + { + memset16(&pal_bg_mem[REROLL_BTN_FRAME_PAL_IDX], HIGHLIGHT_COLOR, 1); + } +} + +static void shop_bottom_row_on_key_hit(SelectionGrid* selection_grid, Selection* selection) +{ + if (money >= reroll_cost) + { + game_shop_reroll(&reroll_cost); + } +} + +SelectionGridRow shop_selection_rows[] = +{ + {0, shop_top_row_get_size, shop_top_row_on_selection_changed, shop_top_row_on_key_hit}, + {1, shop_bottom_row_get_size, shop_bottom_row_on_selection_changed, shop_bottom_row_on_key_hit} +}; + +static const Selection SHOP_INIT_SEL = {-1, 0}; + +SelectionGrid shop_selection_grid = {shop_selection_rows, NUM_ELEM_IN_ARR(shop_selection_rows), SHOP_INIT_SEL}; + +// Shop menu input and selection +static void game_shop_process_user_input() +{ + if (timer == 1) + { + // The selection grid is initialized outside of bounds and moved + // to trigger the selection change so the initial selection is visible + shop_selection_grid.selection = SHOP_INIT_SEL; + selection_grid_move_selection_horz(&shop_selection_grid, 1); + tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, reroll_cost); + } + + // Shop input logic + selection_grid_process_input(&shop_selection_grid); +} + +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); +} + +// Outro sequence (menu and shop icon going out of frame) +static void game_shop_outro() +{ + // Shift the shop panel + main_bg_se_move_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_DOWN); + + main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SE_UP); + + if (timer == 1) + { + tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT); // Erase the shop prices text + + for (int i = 0; i < list_get_size(shop_jokers); i++) + { + JokerObject *joker_object = list_get(shop_jokers, i); + if (joker_object != NULL) + { + joker_object->sprite_object->ty = int2fx(160); + } + } + + int y = 6; + memset16(&se_mem[MAIN_BG_SBB][32 * (y - 1)], 0x0006, 1); + memset16(&se_mem[MAIN_BG_SBB][1 + 32 * (y - 1)], 0x0007, 2); + memset16(&se_mem[MAIN_BG_SBB][3 + 32 * (y - 1)], 0x0008, 1); + memset16(&se_mem[MAIN_BG_SBB][4 + 32 * (y - 1)], 0x0009, 4); + memset16(&se_mem[MAIN_BG_SBB][7 + 32 * (y - 1)], 0x000A, 1); + memset16(&se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], 0x0406, 1); + } + else if (timer == 2) + { + int y = 5; + memset16(&se_mem[MAIN_BG_SBB][32 * (y - 1)], 0x0001, 1); + memset16(&se_mem[MAIN_BG_SBB][1 + 32 * (y - 1)], 0x0002, 7); + memset16(&se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], 0x0401, 1); + } + + if (timer >= MENU_POP_OUT_ANIM_FRAMES) + { + state = 3; // Go to the next state + timer = 0; // Reset the timer + } +} + 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 != NULL) { - if (shop_jokers[i] != NULL) + for (int i = 0; i < list_get_size(shop_jokers); i++) { - joker_object_update(shop_jokers[i]); + JokerObject *joker_object = list_get(shop_jokers, i); + if (joker_object != NULL) + { + joker_object_update(joker_object); + } } } + - 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 - const ushort max_items_top = MAX_SHOP_JOKERS; - const ushort max_items_bottom = 0; - - if (timer % 20 == 0) // Shift palette around the border of the shop icon + if (timer % 20 == 0) { - 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); + 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: // Intro sequence (menu and shop icon coming into frame) + case 0: { - game_shop_intro(shop_jokers); + game_shop_intro(); break; } - case 1: // Shop menu input and selection + case 1: { - if (timer == 1) - { - tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, reroll_cost); - } - - // Shop input logic - if (key_hit(KEY_UP)) - { - selection_y = 0; - - if (selection_x > max_items_top) - { - selection_x = max_items_top; - } - } - else if (key_hit(KEY_DOWN)) - { - selection_y = 1; - - if (selection_x > max_items_bottom) - { - selection_x = max_items_bottom; - } - } - else if (key_hit(KEY_LEFT)) - { - if (selection_x > 0) - { - selection_x--; - } - } - else if (key_hit(KEY_RIGHT)) - { - if (selection_y == 0 && selection_x < max_items_top) - { - selection_x++; - } - else if (selection_y == 1 && selection_x < max_items_bottom) - { - selection_x++; - } - } - - memcpy16(&pal_bg_mem[7], &pal_bg_mem[3], 1); - memcpy16(&pal_bg_mem[5], &pal_bg_mem[16], 1); - - // Shop selection logic - if (selection_x == 0 && selection_y == 0) - { - memset16(&pal_bg_mem[5], 0xFFFF, 1); - - if (key_hit(SELECT_CARD)) - { - // Go to next blind selection game state - state = 2; // Go to the outro sequence state - timer = 0; // Reset the timer - - memcpy16(&pal_bg_mem[5], &pal_bg_mem[6], 1); - - // memcpy16(&pal_bg_mem[16], &pal_bg_mem[6], 1); - // This changes the color of the button to a dark red. - // However, it shares a palette with the shop icon, so it will change the color of the shop icon as well. - // And I don't care enough to fix it right now. - } - } - else if (selection_x == 0 && selection_y == 1) - { - memset16(&pal_bg_mem[7], 0xFFFF, 1); - - if (key_hit(SELECT_CARD) && money >= reroll_cost) - { - money -= 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++) - { - if (shop_jokers[i] != NULL) - { - if (i == selection_x - 1 && selection_y == 0) - { - shop_jokers[i]->sprite_object->ty = int2fx(61); - - if (key_hit(SELECT_CARD) && jokers_top < MAX_JOKERS_HELD_SIZE - 1 && money >= shop_jokers[i]->joker->value) - { - joker_push(shop_jokers[i]); - money -= shop_jokers[i]->joker->value; // Deduct the money spent on the joker - display_money(money); // Update the money display - - Rect joker_price_rect; - joker_price_rect.left = fx2int(shop_jokers[i]->sprite_object->tx); - // 2*TILE_SIZE because the item is highlighted and raised by 1 additional tile - joker_price_rect.top = fx2int(shop_jokers[i]->sprite_object->ty) + CARD_SPRITE_SIZE + 2*TILE_SIZE; - joker_price_rect.right = joker_price_rect.left + TILE_SIZE * 3; - joker_price_rect.bottom = joker_price_rect.top + TILE_SIZE; - - tte_erase_rect_wrapper(joker_price_rect); - - shop_jokers[i] = NULL; // Remove the joker from the shop - } - } - else - { - shop_jokers[i]->sprite_object->ty = int2fx(71); - } - } - } - + game_shop_process_user_input(); break; } - case 2: // Outro sequence (menu and shop icon going out of frame) + case 2: { - // Shift the shop panel - main_bg_se_move_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_DOWN); - - main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SE_UP); - - if (timer == 1) - { - tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT); // Erase the shop prices text - - for (int i = 0; i < MAX_SHOP_JOKERS; i++) - { - if (shop_jokers[i] != NULL) - { - shop_jokers[i]->sprite_object->ty = int2fx(160); - } - } - - int y = 6; - memset16(&se_mem[MAIN_BG_SBB][32 * (y - 1)], 0x0006, 1); - memset16(&se_mem[MAIN_BG_SBB][1 + 32 * (y - 1)], 0x0007, 2); - memset16(&se_mem[MAIN_BG_SBB][3 + 32 * (y - 1)], 0x0008, 1); - memset16(&se_mem[MAIN_BG_SBB][4 + 32 * (y - 1)], 0x0009, 4); - memset16(&se_mem[MAIN_BG_SBB][7 + 32 * (y - 1)], 0x000A, 1); - memset16(&se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], 0x0406, 1); - } - else if (timer == 2) - { - int y = 5; - memset16(&se_mem[MAIN_BG_SBB][32 * (y - 1)], 0x0001, 1); - memset16(&se_mem[MAIN_BG_SBB][1 + 32 * (y - 1)], 0x0002, 7); - memset16(&se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], 0x0401, 1); - } - - if (timer >= MENU_POP_OUT_ANIM_FRAMES) - { - state = 3; // Go to the next state - timer = 0; // Reset the timer - } - + game_shop_outro(); break; } default: state = 0; // Reset the state - reroll_cost = reroll_base_cost; - selection_x = 0; // Reset the selection - selection_y = 0; // Reset the selection - - for (int i = 0; i < MAX_SHOP_JOKERS; i++) + for (int i = 0; i < list_get_size(shop_jokers); i++) { - joker_object_destroy(&shop_jokers[i]); // Destroy the joker objects + JokerObject* joker_object = list_get(shop_jokers, i); + if (joker_object != NULL) + { + // Make the joker available back to shop + int_list_append(jokers_available_to_shop, (intptr_t)joker_object->joker->id); + } + joker_object_destroy(&joker_object); // Destroy the joker objects } - increment_blind(BLIND_DEFEATED); + list_destroy(&shop_jokers); + + 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 break; } } + + 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 diff --git a/source/graphic_utils.c b/source/graphic_utils.c index 5311711..b568442 100644 --- a/source/graphic_utils.c +++ b/source/graphic_utils.c @@ -168,3 +168,22 @@ void update_text_rect_to_right_align_num(Rect* rect, int num, int overflow_direc } } +void memcpy16_tile8_with_palette_offset(u16* dst, const u16* src, uint hwcount, u8 palette_offset) +{ + const u16 offset = (((palette_offset) << 8) | (palette_offset)); + for (int i = 0; i < hwcount; i++) + { + // Copying u8 data twice across u16 data + dst[i] = src[i] + offset; + } +} + +void memcpy32_tile8_with_palette_offset(u32* dst, const u32* src, uint wcount, u8 palette_offset) +{ + const u32 offset = (palette_offset << 24) | (palette_offset << 16) | (palette_offset << 8) | palette_offset; + for (int i = 0; i < wcount; i++) + { + // Copying u8 data 4 times across u32 data + dst[i] = src[i] + offset; + } +} \ No newline at end of file diff --git a/source/int_list.c b/source/int_list.c new file mode 100644 index 0000000..1a39dc4 --- /dev/null +++ b/source/int_list.c @@ -0,0 +1,100 @@ +#include +#include +#include "list.h" +#include "util.h" + +List *list_new(int init_size) { + List *list = (List *)malloc(sizeof(List)); + if (list == NULL) return NULL; + list->_array = (void **)malloc(sizeof(void*) * init_size); + if (!list->_array) + { + free(list); + return NULL; + } + list->size = 0; + list->allocated_size = init_size; + return list; +} + +void list_destroy(List **list) { + if (list == NULL || *list == NULL) + return; + { + free((*list)->_array); + free(*list); + } + + *list = NULL; +} + +bool int_list_append(List *list, intptr_t value) +{ + return list_append(list, (void*)value); +} + +bool list_append(List *list, void *value) +{ + if (list->size >= list->allocated_size) + { + int new_size = list->allocated_size * 2; + void **new_arr = (void **)realloc(list->_array, sizeof(void*) * 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 list_remove_by_idx(List *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 list_remove_by_value(List *list, void* value) +{ + for (int i = 0; i < list->size; i++) + { + if (list->_array[i] == value) + { + return list_remove_by_idx(list, i); + } + } + + return false; +} + +bool int_list_remove_by_value(List *list, intptr_t value) +{ + return list_remove_by_value(list, (void*)value); +} + +void* list_get(List *list, int index) +{ + if (index < 0 || index >= list->size) + return NULL; + return list->_array[index]; +} + +intptr_t int_list_get(List *list, int index) +{ + return (intptr_t)list_get(list, index); +} + +int list_get_size(List *list) +{ + if (list == NULL) + { + return UNDEFINED; + } + return list->size; +} diff --git a/source/joker.c b/source/joker.c index de376b0..6b30061 100644 --- a/source/joker.c +++ b/source/joker.c @@ -127,6 +127,9 @@ void joker_init() joker_gfxTiles[6] = joker_gfx6Tiles; joker_gfxTiles[7] = joker_gfx7Tiles; joker_gfxTiles[8] = joker_gfx8Tiles; + joker_gfxTiles[9] = joker_gfx9Tiles; + joker_gfxTiles[10] = joker_gfx10Tiles; + joker_gfxTiles[11] = joker_gfx11Tiles; joker_gfxPal[0] = joker_gfx0Pal; joker_gfxPal[1] = joker_gfx1Pal; @@ -137,6 +140,9 @@ void joker_init() joker_gfxPal[6] = joker_gfx6Pal; joker_gfxPal[7] = joker_gfx7Pal; joker_gfxPal[8] = joker_gfx8Pal; + joker_gfxPal[9] = joker_gfx9Pal; + joker_gfxPal[10] = joker_gfx10Pal; + joker_gfxPal[11] = joker_gfx11Pal; for (int i = 0; i < num_spritesheets; i++) { @@ -264,33 +270,47 @@ bool joker_object_score(JokerObject *joker_object, Card* scored_card, int *chips *money += joker_effect.money; // TODO: Retrigger - tte_set_pos(fx2int(joker_object->sprite_object->x) + 8, JOKER_SCORE_TEXT_Y); // Offset of 16 pixels to center the text on the card - - char score_buffer[12]; + const int joker_score_display_offset_px = (MAX_CARD_SCORE_STR_LEN + 1)*TTE_CHAR_SIZE; + // + 1 For space + int cursorPosX = fx2int(joker_object->sprite_object->x) + 8; // Offset of 16 pixels to center the text on the card if (joker_effect.chips > 0) { + char score_buffer[INT_MAX_DIGITS + 2]; // For '+' and null terminator + tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y); tte_set_special(0xD000); // Blue snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.chips); + tte_write(score_buffer); + cursorPosX += joker_score_display_offset_px; } - else if (joker_effect.mult > 0) + if (joker_effect.mult > 0) { + char score_buffer[INT_MAX_DIGITS + 2]; + tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y); tte_set_special(0xE000); // Red snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.mult); + tte_write(score_buffer); + cursorPosX += joker_score_display_offset_px; } - else if (joker_effect.xmult > 0) + if (joker_effect.xmult > 0) { + char score_buffer[INT_MAX_DIGITS + 2]; + tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y); tte_set_special(0xE000); // Red snprintf(score_buffer, sizeof(score_buffer), "X%d", joker_effect.xmult); + tte_write(score_buffer); + cursorPosX += joker_score_display_offset_px; } - else if (joker_effect.money > 0) + if (joker_effect.money > 0) { + char score_buffer[INT_MAX_DIGITS + 2]; + tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y); tte_set_special(0xC000); // Yellow snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.money); + tte_write(score_buffer); + cursorPosX += joker_score_display_offset_px; } - tte_write(score_buffer); - joker_object->joker->processed = true; // Mark the joker as processed joker_object_shake(joker_object, SFX_CARD_SELECT); // TODO: Add a sound effect for scoring the joker diff --git a/source/joker_effects.c b/source/joker_effects.c index 5e934ee..bab4512 100644 --- a/source/joker_effects.c +++ b/source/joker_effects.c @@ -1,3 +1,4 @@ +#include "game.h" #include "joker.h" #include "util.h" #include "hand_analysis.h" @@ -178,7 +179,8 @@ static JokerEffect half_joker_effect(Joker *joker, Card *scored_card) { if (scored_card != NULL) return effect; // if card != null, we are not at the end-phase of scoring yet - if (get_played_top() + 1 <= 3) // game.c has hand_get_size() but it's not accesible here + int played_size = get_played_top() + 1; + if (played_size <= 3) effect.mult = 20; return effect; @@ -190,12 +192,13 @@ static JokerEffect joker_stencil_effect(Joker *joker, Card *scored_card) { return effect; // if card != null, we are not at the end-phase of scoring yet // +1 xmult per empty joker slot... - int jokers_top = get_jokers_top(); - effect.xmult = (MAX_JOKERS_HELD_SIZE-1) - jokers_top; + int num_jokers = get_jokers_top() + 1; + + effect.xmult = (MAX_JOKERS_HELD_SIZE) - num_jokers; // ...and also each stencil_joker adds +1 xmult JokerObject** jokers = get_jokers(); - for (int i = 0; i < jokers_top; i++ ) + for (int i = 0; i < num_jokers; i++ ) { if (jokers[i]->joker->id == JOKER_STENCIL_ID) effect.xmult++; @@ -215,6 +218,78 @@ static JokerEffect misprint_joker_effect(Joker *joker, Card *scored_card) { return effect; } +static JokerEffect walkie_talkie_joker_effect(Joker *joker, Card *scored_card) { + JokerEffect effect = {0}; + if (scored_card == NULL) + return effect; + + if (scored_card->rank == TEN || scored_card->rank == FOUR) { + effect.chips = 10; + effect.mult = 4; + } + + return effect; +} + +static JokerEffect fibonnaci_joker_effect(Joker *joker, Card *scored_card) { + JokerEffect effect = {0}; + if (scored_card == NULL) + return effect; + + switch (scored_card->rank) { + case ACE: case TWO: case THREE: case FIVE: case EIGHT: + effect.mult = 8; + default: + break; + } + + return effect; +} + +static JokerEffect banner_joker_effect(Joker *joker, Card *scored_card) { + JokerEffect effect = {0}; + if (scored_card != NULL) + return effect; // if card != null, we are not at the end-phase of scoring yet + + effect.chips = 30 * get_num_discards_remaining(); + + return effect; +} + +static JokerEffect mystic_summit_joker_effect(Joker *joker, Card *scored_card) { + JokerEffect effect = {0}; + if (scored_card != NULL) + return effect; // if card != null, we are not at the end-phase of scoring yet + + if (get_num_discards_remaining() == 0) + effect.mult = 15; + + return effect; +} + +static JokerEffect blackboard_joker_effect(Joker *joker, Card *scored_card) { + JokerEffect effect = {0}; + if (scored_card != NULL) + return effect; // if card != null, we are not at the end-phase of scoring yet + + bool all_cards_are_spades_or_clubs = true; + CardObject** hand = get_hand_array(); + int hand_size = hand_get_size(); + for (int i = 0; i < hand_size; i++ ) + { + u8 suit = hand[i]->card->suit; + if (suit == HEARTS || suit == DIAMONDS) { + all_cards_are_spades_or_clubs = false; + break; + } + } + + if (all_cards_are_spades_or_clubs) + effect.xmult = 3; + + return effect; +} + const JokerInfo joker_registry[] = { { COMMON_JOKER, 2, default_joker_effect }, // DEFAULT_JOKER_ID = 0 { COMMON_JOKER, 5, greedy_joker_effect }, // GREEDY_JOKER_ID = 1 @@ -233,6 +308,11 @@ const JokerInfo joker_registry[] = { { COMMON_JOKER, 4, crafty_joker_effect }, { COMMON_JOKER, 5, half_joker_effect }, { UNCOMMON_JOKER, 8, joker_stencil_effect }, + { COMMON_JOKER, 5, banner_joker_effect }, + { COMMON_JOKER, 4, walkie_talkie_joker_effect }, + { UNCOMMON_JOKER, 8, fibonnaci_joker_effect }, + { UNCOMMON_JOKER, 6, blackboard_joker_effect }, + { COMMON_JOKER, 5, mystic_summit_joker_effect }, { COMMON_JOKER, 4, misprint_joker_effect }, }; diff --git a/source/main.c b/source/main.c index bd7b046..269fc09 100644 --- a/source/main.c +++ b/source/main.c @@ -7,6 +7,7 @@ #include "game.h" #include "blind.h" #include "joker.h" +#include "affine_background.h" #include "graphic_utils.h" // Graphics @@ -17,29 +18,6 @@ #include "soundbank.h" #include "soundbank_bin.h" -#define MAP_AFF_SIZE 0x0100 -BG_AFFINE bgaff; -AFF_SRC_EX asx = {32<<8, 64<<8, 120, 80, 0x0100, 0x0100, 0}; - - -void init_map() // Temp function derived from libtonc example -{ - // Grit is fucking garbage and doesn't allow you to have a palette offset, they all get loaded at 0x0000. I have to manually change each tile's palette bank - - unsigned int correctedTiles[affine_background_gfxTilesLen / 4]; - for (int i = 0; i < affine_background_gfxTilesLen / 4; i++) - { - correctedTiles[i] = affine_background_gfxTiles[i] | 0xF0F0F0F0; // I brute forced this. I have no idea how this would and does work, but it does. We'll just leave this here for now. - // Actually for future reference, each number seems to equate to an XY on the palette, so 0xF0F0F0F0 would just push everything down 15 colors, since the second number isn't set. - } - - memcpy(&tile8_mem[AFFINE_BG_CBB], correctedTiles, affine_background_gfxTilesLen); - GRIT_CPY(&se_mem[AFFINE_BG_SBB], affine_background_gfxMap); - memcpy16(&pal_bg_mem[16 * 15], affine_background_gfxPal, 5); - - bgaff = bg_aff_default; -} - void init() { irq_init(NULL); @@ -66,7 +44,7 @@ void init() // BG1 is the main background layer REG_BG1CNT = BG_PRIO(1) | BG_CBB(MAIN_BG_CBB) | BG_SBB(MAIN_BG_SBB) | BG_8BPP; // 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_top = 44; @@ -93,9 +71,8 @@ void init() REG_DISPCNT = DCNT_MODE1 | DCNT_OBJ_1D | DCNT_BG0 | DCNT_BG1 | DCNT_BG2 | DCNT_OBJ | DCNT_WIN0 | DCNT_WIN1; - init_map(); // Initialize the map for the background - // Initialize subsystems + affine_background_init(); sprite_init(); card_init(); blind_init(); @@ -103,24 +80,9 @@ void init() game_init(); } -void background_update() // This needs to be called before update() and draw() because setting REG_BG_AFFINE during an HBlank can cause issues -{ - static uint timer = 0; - timer++; - - // These values are not permament, just the current configuration for the affine background - asx.tex_x += 5; - asx.tex_y += 12; - asx.sx = (lu_sin(timer * 100)) >> 8; // Scale the sine value to fit in a s16 - asx.sx += 256; // Add 256 to the sine value to make it positive - asx.sy = (lu_sin(timer * 100 + 0x4000)) >> 8; // Scale the sine value to fit in a s16 - asx.sy += 256; // Add 256 to the sine value to make it positive - bg_rotscale_ex(&bgaff, &asx); - REG_BG_AFFINE[2]= bgaff; -} - void update() { + affine_background_update(); game_update(); } @@ -138,7 +100,6 @@ int main() VBlankIntrWait(); mmFrame(); key_poll(); - background_update(); update(); draw(); } diff --git a/source/selection_grid.c b/source/selection_grid.c new file mode 100644 index 0000000..8acc017 --- /dev/null +++ b/source/selection_grid.c @@ -0,0 +1,82 @@ +#include "selection_grid.h" + + +static void selection_grid_process_directional_input(SelectionGrid *selection_grid) +{ + int horz_tri_input = bit_tribool(key_hit(KEY_ANY), KI_RIGHT, KI_LEFT); + + if (horz_tri_input != 0) + { + selection_grid_move_selection_horz(selection_grid, horz_tri_input); + /* Avoid handling both vertical and horizontal input at the same time, + * it creates all sorts of difficult edge cases. + */ + return; + } + + int vert_tri_input = bit_tribool(key_hit(KEY_ANY), KI_DOWN, KI_UP); + + + if (vert_tri_input != 0) + { + selection_grid_move_selection_vert(selection_grid, vert_tri_input); + } + +} + +void selection_grid_move_selection_horz(SelectionGrid* selection_grid, + int direction_tribool) +{ + Selection new_selection = selection_grid->selection; + new_selection.x += direction_tribool; + if( selection_grid->selection.y >= 0 + && selection_grid->selection.y < selection_grid->num_rows + && new_selection.x >= 0 + && new_selection.x < selection_grid->rows[new_selection.y].get_size()) + { + selection_grid->rows[new_selection.y].on_selection_changed(selection_grid, new_selection.y, &selection_grid->selection, &new_selection); + selection_grid->selection = new_selection; + } +} + +void selection_grid_move_selection_vert(SelectionGrid *selection_grid, int direction_tribool) +{ + Selection selection = selection_grid->selection; + Selection new_selection = selection; + new_selection.y += direction_tribool; + + if (new_selection.y >= 0 + && new_selection.y < selection_grid->num_rows) + { + int new_row_size = selection_grid->rows[new_selection.y].get_size(); + if (selection.x >= new_row_size) + { + // TODO: Maintain relative horizontal position + // Clip selection to row size + new_selection.x = new_row_size - 1; + } + if (selection.y >= 0 && selection.y < selection_grid->num_rows) + { + selection_grid->rows[selection.y].on_selection_changed(selection_grid, selection.y, &selection, &new_selection); + } + selection_grid->rows[new_selection.y].on_selection_changed(selection_grid, new_selection.y, &selection, &new_selection); + selection_grid->selection = new_selection; + } +} + +void selection_grid_process_input(SelectionGrid *selection_grid) +{ + if (selection_grid == NULL || selection_grid->rows == NULL) + return; + + selection_grid_process_directional_input(selection_grid); + + u32 non_directional_key = KEY_ANY & ~KEY_DIR; + if (key_hit(non_directional_key)) + { + Selection* selection = &selection_grid->selection; // To make the next line shorter and more readable + selection_grid->rows[selection->y].on_key_hit(selection_grid, selection); + } +} + + diff --git a/source/sprite.c b/source/sprite.c index a93b953..90cb7b5 100644 --- a/source/sprite.c +++ b/source/sprite.c @@ -1,6 +1,7 @@ #include "sprite.h" #include "util.h" #include "audio_utils.h" +#include "soundbank.h" #include #include @@ -8,6 +9,7 @@ #define MAX_SPRITES 128 #define MAX_AFFINES 32 +#define SPRITE_FOCUS_RAISE_PX 10 OBJ_ATTR obj_buffer[MAX_SPRITES]; OBJ_AFFINE *obj_aff_buffer = (OBJ_AFFINE*)obj_buffer; @@ -145,9 +147,9 @@ void sprite_object_reset_transform(SpriteObject* sprite_object) sprite_object->y = 0; sprite_object->vx = 0; sprite_object->vy = 0; - sprite_object->tscale = float2fx(1.0f); // Target scale - sprite_object->scale = float2fx(1.0f); - sprite_object->vscale = float2fx(0.0f); + 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; @@ -163,7 +165,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 // 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) { sprite_object->vx = 0; @@ -211,8 +213,8 @@ void sprite_object_update(SpriteObject* sprite_object) 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->vrotation = float2fx(8.0f); // Rotate 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 if (sound_id == UNDEFINED) return; // If no sound ID is provided, do nothing @@ -239,3 +241,20 @@ Sprite* sprite_object_get_sprite(SpriteObject* sprite_object) return NULL; return sprite_object->sprite; } + +void sprite_object_set_focus(SpriteObject* sprite_object, bool focus) +{ + if (sprite_object->focused == focus) + { + return; + } + sprite_object->focused = focus; + + play_sfx(SFX_CARD_FOCUS , MM_BASE_PITCH_RATE + rand() % 512); + sprite_object->ty = sprite_object->ty + int2fx((focus ? -1 : 1) * SPRITE_FOCUS_RAISE_PX); +} + +bool sprite_object_is_focused(SpriteObject* sprite_object) +{ + return sprite_object->focused; +}