diff --git a/Doxyfile b/Doxyfile index 7417526..fa775c7 100644 --- a/Doxyfile +++ b/Doxyfile @@ -572,7 +572,7 @@ EXTRACT_PACKAGE = NO # included in the documentation. # The default value is: NO. -EXTRACT_STATIC = NO +EXTRACT_STATIC = YES # If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined # locally in source files will be included in the documentation. If set to NO, @@ -588,7 +588,7 @@ EXTRACT_LOCAL_CLASSES = YES # included. # The default value is: NO. -EXTRACT_LOCAL_METHODS = NO +EXTRACT_LOCAL_METHODS = YES # If this flag is set to YES, the members of anonymous namespaces will be # extracted and appear in the documentation as a namespace called @@ -3023,3 +3023,4 @@ MSCFILE_DIRS = EXCLUDE = build tests graphics audio doc scripts USE_MDFILE_AS_MAINPAGE = CONTRIBUTING.md + diff --git a/Makefile b/Makefile index de9fadb..07c0df1 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ CFLAGS += $(INCLUDE) CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions ASFLAGS := -g $(ARCH) -LDFLAGS = -g $(ARCH) -Wl,-Map,$(notdir $*.map),--undefined=balatro_version +LDFLAGS = -g $(ARCH) -Wl,-Map,$(notdir $*.map),--undefined=gbalatro_version #--------------------------------------------------------------------------------- # any extra libraries we wish to link with the project diff --git a/graphics/background_main_menu_gfx.png b/graphics/background_main_menu_gfx.png index 2543fa2..728a1aa 100644 Binary files a/graphics/background_main_menu_gfx.png and b/graphics/background_main_menu_gfx.png differ diff --git a/graphics/background_options_menu_gfx.grit b/graphics/background_options_menu_gfx.grit new file mode 100644 index 0000000..f362168 --- /dev/null +++ b/graphics/background_options_menu_gfx.grit @@ -0,0 +1 @@ +-gB8 -mR8 -mLs -mRtf -pn 160 diff --git a/graphics/background_options_menu_gfx.png b/graphics/background_options_menu_gfx.png new file mode 100644 index 0000000..2cf5401 Binary files /dev/null and b/graphics/background_options_menu_gfx.png differ diff --git a/graphics/deck_gfx.grit b/graphics/deck_gfx.grit index f677be5..1c22f29 100644 --- a/graphics/deck_gfx.grit +++ b/graphics/deck_gfx.grit @@ -1 +1 @@ --gB4 -Mw4 -Mh4 -m! \ No newline at end of file +-gB4 -Mw4 -Mh4 -m! -pn 16 diff --git a/graphics/deck_gfx.png b/graphics/deck_gfx.png index 82848c2..da62ed3 100644 Binary files a/graphics/deck_gfx.png and b/graphics/deck_gfx.png differ diff --git a/graphics/high_contrast_deck_pal_gfx.grit b/graphics/high_contrast_deck_pal_gfx.grit new file mode 100644 index 0000000..85ddb07 --- /dev/null +++ b/graphics/high_contrast_deck_pal_gfx.grit @@ -0,0 +1 @@ +-gB4 -g! -m! -pn 16 diff --git a/graphics/high_contrast_deck_pal_gfx.png b/graphics/high_contrast_deck_pal_gfx.png new file mode 100644 index 0000000..7141ae4 Binary files /dev/null and b/graphics/high_contrast_deck_pal_gfx.png differ diff --git a/include/affine_background.h b/include/affine_background.h index c0e5e92..2898f66 100644 --- a/include/affine_background.h +++ b/include/affine_background.h @@ -33,6 +33,13 @@ */ enum AffineBackgroundID { + /** + * @brief Display background for the initial splash screen. + * + * Signifies that no background is currently in use. + */ + AFFINE_BG_NONE, + /** * @brief Display background for main menu. * diff --git a/include/audio_utils.h b/include/audio_utils.h index 24d49ae..367768a 100644 --- a/include/audio_utils.h +++ b/include/audio_utils.h @@ -9,10 +9,16 @@ #include /** - * @def MM_FULL_VOLUME + * @def MM_MODULE_FULL_VOLUME + * @brief The maximum volume for maxmod module volume (main theme) + */ +#define MM_MODULE_FULL_VOLUME 1024 + +/** + * @def MM_SFX_FULL_VOLUME * @brief The maximum volume for maxmod mm_sound_effect.volume */ -#define MM_FULL_VOLUME 255 +#define MM_SFX_FULL_VOLUME 255 /** * @def MM_PAN_CENTER @@ -32,7 +38,7 @@ * @def SFX_DEFAULT_VOLUME * @brief Default volume for sound effects */ -#define SFX_DEFAULT_VOLUME MM_FULL_VOLUME +#define SFX_DEFAULT_VOLUME MM_SFX_FULL_VOLUME /** * @def SFX_DEFAULT_PAN diff --git a/include/button.h b/include/button.h index b8ed615..945d8c9 100644 --- a/include/button.h +++ b/include/button.h @@ -11,7 +11,7 @@ #define BTN_HIGHLIGHT_COLOR 0xFFFF -#define BUTTON_SFX_VOLUME 154 // 60% of MM_FULL_VOLUME +#define BUTTON_SFX_VOLUME 154 // 60% of MM_SFX_FULL_VOLUME /** * @brief The function to be called when the button is pressed diff --git a/include/card.h b/include/card.h index 5b957ee..d716f48 100644 --- a/include/card.h +++ b/include/card.h @@ -56,6 +56,7 @@ typedef struct CardObject // Card functions void card_init(); +void toggle_high_contrast_cards(bool enable); // Card methods Card* card_new(u8 suit, u8 rank); diff --git a/include/def_state_info_table.h b/include/def_state_info_table.h index c7bfb74..cb79c4d 100644 --- a/include/def_state_info_table.h +++ b/include/def_state_info_table.h @@ -1,12 +1,13 @@ // clang-format off // (stateEnum, on_init, on_update, on_exit) -DEF_STATE_INFO(GAME_STATE_SPLASH_SCREEN, splash_screen_on_init, splash_screen_on_update, splash_screen_on_exit) -DEF_STATE_INFO(GAME_STATE_MAIN_MENU, game_main_menu_on_init, game_main_menu_on_update, game_main_menu_on_exit) -DEF_STATE_INFO(GAME_STATE_GAME_START, noop, game_start, noop) -DEF_STATE_INFO(GAME_STATE_PLAYING, game_round_on_init, game_playing_on_update, noop) -DEF_STATE_INFO(GAME_STATE_ROUND_END, noop, game_round_end_on_update, game_round_end_on_exit) -DEF_STATE_INFO(GAME_STATE_SHOP, noop, game_shop_on_update, game_shop_on_exit) -DEF_STATE_INFO(GAME_STATE_BLIND_SELECT, game_blind_select_on_init, game_blind_select_on_update, game_blind_select_on_exit) -DEF_STATE_INFO(GAME_STATE_LOSE, game_lose_on_init, game_lose_on_update, game_over_on_exit) -DEF_STATE_INFO(GAME_STATE_WIN, game_win_on_init, game_win_on_update, game_over_on_exit) +DEF_STATE_INFO(GAME_STATE_SPLASH_SCREEN, splash_screen_on_init, splash_screen_on_update, splash_screen_on_exit ) +DEF_STATE_INFO(GAME_STATE_MAIN_MENU, game_main_menu_on_init, game_main_menu_on_update, game_main_menu_on_exit ) +DEF_STATE_INFO(GAME_STATE_OPTIONS_MENU, game_options_menu_on_init, game_options_menu_on_update, game_options_menu_on_exit) +DEF_STATE_INFO(GAME_STATE_GAME_START, noop, game_start, noop ) +DEF_STATE_INFO(GAME_STATE_PLAYING, game_round_on_init, game_playing_on_update, noop ) +DEF_STATE_INFO(GAME_STATE_ROUND_END, noop, game_round_end_on_update, game_round_end_on_exit ) +DEF_STATE_INFO(GAME_STATE_SHOP, noop, game_shop_on_update, game_shop_on_exit ) +DEF_STATE_INFO(GAME_STATE_BLIND_SELECT, game_blind_select_on_init, game_blind_select_on_update, game_blind_select_on_exit) +DEF_STATE_INFO(GAME_STATE_LOSE, game_lose_on_init, game_lose_on_update, game_over_on_exit ) +DEF_STATE_INFO(GAME_STATE_WIN, game_win_on_init, game_win_on_update, game_over_on_exit ) // clang-format on diff --git a/include/game.h b/include/game.h index 857b868..107dd78 100644 --- a/include/game.h +++ b/include/game.h @@ -1,6 +1,7 @@ #ifndef GAME_H #define GAME_H +#include "blind.h" #include "game/common_ui.h" #include "game_variables.h" @@ -13,7 +14,7 @@ extern GameVariables g_game_vars; #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 FRAMES(x) (((x) + game_speed - 1) / game_speed) +#define FRAMES(x) (((x) + (g_game_vars.game_speed) - 1) / (g_game_vars.game_speed)) // TODO: Can make these dynamic to support interest-related jokers and vouchers #define MAX_INTEREST 5 @@ -123,7 +124,7 @@ typedef struct ContainedHandTypes } ContainedHandTypes; // clang-format on -typedef void (*GameStateCallback)(); +typedef void (*GameStateCallback)(void); typedef struct { @@ -158,17 +159,16 @@ int get_num_hands_remaining(void); u32 get_chips(void); void set_chips(u32 new_chips); -void display_chips(); +void display_chips(void); u32 get_mult(void); void set_mult(u32 new_mult); -void display_mult(); -int get_money(void); -void set_money(int new_money); -void display_money(); +void display_mult(void); +void display_money(void); void set_retrigger(bool new_retrigger); +enum BlindType get_current_blind(void); +enum BlindType get_next_boss_blind(void); -int get_game_speed(void); -void set_game_speed(int new_game_speed); +u32 get_rand(void); // joker specific functions bool is_shortcut_joker_active(void); diff --git a/include/game/common_ui.h b/include/game/common_ui.h index 0715250..1241383 100644 --- a/include/game/common_ui.h +++ b/include/game/common_ui.h @@ -17,6 +17,7 @@ enum BackgroundId BG_ROUND_END, BG_SHOP, BG_BLIND_SELECT, + BG_OPTIONS_MENU, BG_MAIN_MENU }; diff --git a/include/game/main_menu.h b/include/game/main_menu.h index 144f926..1d021bf 100644 --- a/include/game/main_menu.h +++ b/include/game/main_menu.h @@ -15,23 +15,17 @@ void game_main_menu_change_background(void); /** * @brief Main menu state initialization - * - * @param vars passed @ref GameVariables struct */ void game_main_menu_on_init(void); /** * @brief Main menu state update - * - * @param vars passed @ref GameVariables struct */ void game_main_menu_on_update(void); /** * @brief Main menu cleanup (called when transitioning to game start) - * - * @param vars passed @ref GameVariables struct */ void game_main_menu_on_exit(void); -#endif // GAME_MAIN_MENU_H +#endif // GAME_MAIN_MENU_H \ No newline at end of file diff --git a/include/game/options_menu.h b/include/game/options_menu.h new file mode 100644 index 0000000..5d44324 --- /dev/null +++ b/include/game/options_menu.h @@ -0,0 +1,31 @@ +/** + * @file options_menu.h + * + * @brief Options menu state functions. + */ +#ifndef GAME_OPTIONS_MENU_H +#define GAME_OPTIONS_MENU_H + +#include "game_variables.h" + +/** + * @brief Change the options menu background + */ +void game_options_menu_change_background(void); + +/** + * @brief Options menu state initialization + */ +void game_options_menu_on_init(void); + +/** + * @brief Options menu state update + */ +void game_options_menu_on_update(void); + +/** + * @brief Options menu cleanup (called when going back to main menu) + */ +void game_options_menu_on_exit(void); + +#endif // GAME_OPTIONS_MENU_H \ No newline at end of file diff --git a/include/game_variables.h b/include/game_variables.h index 5e002c8..83d2e2e 100644 --- a/include/game_variables.h +++ b/include/game_variables.h @@ -8,6 +8,19 @@ #include +#define GAME_SPEED_MIN 1 +#define GAME_SPEED_MAX 4 + +// Volume is stored from 0 to 5 but is an increment of 20 so 0 to 100 will be displayed +#define VOLUME_OPTION_MIN 0 +#define VOLUME_OPTION_MAX 5 +#define VOLUME_OPTION_INCREMENT 20 + +#define DEFAULT_GAME_SPEED 1 +#define DEFAULT_HIGH_CONTRAST false +#define DEFAULT_MUSIC_VOLUME VOLUME_OPTION_MAX +#define DEFAULT_SOUND_VOLUME VOLUME_OPTION_MAX + /** * @brief A central location for all game variables. * @@ -16,8 +29,26 @@ */ typedef struct { - s32 timer; + // Internal variables + + s32 timer; // This might already exist in libtonc but idk so i'm just making my own u32 rng_seed; + u32 rng_step; // Position in the rng sequence. + + // Variables visible by the player + + int round; + int ante; + int money; + + // Options variables + + // BY DEFAULT IS SET TO 1, but if changed to 2 or more, should speed up all (or most) of the + // game aspects that should be sped up by speed, as in the original game. + u8 game_speed; + bool high_contrast; + u8 music_volume; + u8 sound_volume; } GameVariables; extern GameVariables g_game_vars; diff --git a/include/save.h b/include/save.h new file mode 100644 index 0000000..fdfc4f8 --- /dev/null +++ b/include/save.h @@ -0,0 +1,25 @@ +/** + * @file save.h + * + * @brief Utils functions to save/load data structures from/to the SRAM. + */ +#ifndef SAVE_H +#define SAVE_H + +#include "game_variables.h" + +#include + +/** + * @brief Save game data to SRAM. + */ +void save_game(); + +/** + * @brief Load game data from SRAM. + * + * @sa save_game + */ +void load_game(); + +#endif // SAVE_H diff --git a/include/timer.h b/include/timer.h index 278aaf6..62457fd 100644 --- a/include/timer.h +++ b/include/timer.h @@ -9,4 +9,4 @@ #define TM_ZERO 0 -#endif // TIMER_H +#endif // TIMER_H \ No newline at end of file diff --git a/include/version.h b/include/version.h new file mode 100644 index 0000000..cc9c28c --- /dev/null +++ b/include/version.h @@ -0,0 +1,11 @@ +/** + * @file version.h + * + * @brief Game ROM version global var declaration. + */ +#ifndef VERSION_H +#define VERSION_H + +extern const char gbalatro_version[]; + +#endif // VERSION_H diff --git a/scripts/get_hash.py b/scripts/get_hash.py index 42c0b9c..e101705 100644 --- a/scripts/get_hash.py +++ b/scripts/get_hash.py @@ -20,7 +20,7 @@ if __name__ == "__main__": sys.exit(1) file_path = sys.argv[1] - pattern = "GBALATRO_VERSION" + pattern = "GBALATRO[_,-]VERSION" for offset, text in grep_binary_offsets(file_path, pattern): version = text.split(":")[1] diff --git a/source/affine_background.c b/source/affine_background.c index ae0268e..340bc86 100644 --- a/source/affine_background.c +++ b/source/affine_background.c @@ -13,7 +13,7 @@ static IWRAM_CODE void s_affine_background_prep_bgaff_arr(); static BG_AFFINE _bgaff_arr[SCREEN_HEIGHT + 1]; static AFF_SRC_EX _asx = {0}; -static enum AffineBackgroundID _background = AFFINE_BG_MAIN_MENU; +static enum AffineBackgroundID _background = AFFINE_BG_NONE; static uint _timer = 0; void affine_background_init() @@ -78,6 +78,11 @@ void affine_background_load_palette(const u16* src) void affine_background_change_background(enum AffineBackgroundID new_bg) { + if (_background == new_bg) + { + return; + } + _background = new_bg; switch (_background) @@ -110,6 +115,8 @@ void affine_background_change_background(enum AffineBackgroundID new_bg) GRIT_CPY(&se_mem[AFFINE_BG_SBB], affine_background_gfxMap); affine_background_load_palette(affine_background_gfxPal); break; + default: + break; } } diff --git a/source/audio_utils.c b/source/audio_utils.c index 6a4a484..a57060f 100644 --- a/source/audio_utils.c +++ b/source/audio_utils.c @@ -1,5 +1,7 @@ #include "audio_utils.h" +#include "game_variables.h" + #include void play_sfx(mm_word id, mm_word rate, mm_byte volume) @@ -8,7 +10,7 @@ void play_sfx(mm_word id, mm_word rate, mm_byte volume) {id}, rate, 0, - volume, + (volume * g_game_vars.sound_volume) / VOLUME_OPTION_MAX, SFX_DEFAULT_PAN, }; mmEffectEx(&sfx); diff --git a/source/blind.c b/source/blind.c index 13bea88..1155a78 100644 --- a/source/blind.c +++ b/source/blind.c @@ -127,7 +127,7 @@ enum BlindType roll_blind_type(bool showdown) } // roll a random blind among the unbeaten ones - int random_blind_idx = rand() % list_get_len(p_unbeaten_blinds); + int random_blind_idx = get_rand() % list_get_len(p_unbeaten_blinds); Blind* random_blind = list_get_at_idx(p_unbeaten_blinds, random_blind_idx); return random_blind->type; diff --git a/source/card.c b/source/card.c index 4f30c74..528146b 100644 --- a/source/card.c +++ b/source/card.c @@ -2,6 +2,7 @@ #include "deck_gfx.h" #include "graphic_utils.h" +#include "high_contrast_deck_pal_gfx.h" #include #include @@ -21,7 +22,19 @@ const static u16 _card_sprite_lut[NUM_SUITS][NUM_RANKS] = { void card_init() { - GRIT_CPY(&pal_obj_mem[CARD_PB], deck_gfxPal); + toggle_high_contrast_cards(false); +} + +void toggle_high_contrast_cards(bool enable) +{ + if (enable) + { + GRIT_CPY(&pal_obj_mem[CARD_PB], high_contrast_deck_pal_gfxPal); + } + else + { + GRIT_CPY(&pal_obj_mem[CARD_PB], deck_gfxPal); + } } // Card methods diff --git a/source/game.c b/source/game.c index 8b3fbc4..f7bc07f 100644 --- a/source/game.c +++ b/source/game.c @@ -8,16 +8,17 @@ #include "background_main_menu_gfx.h" #include "background_shop_gfx.h" #include "bitset.h" -#include "blind.h" #include "button.h" #include "card.h" #include "game/common_ui.h" #include "game/main_menu.h" +#include "game/options_menu.h" #include "game_variables.h" #include "graphic_utils.h" #include "hand_analysis.h" #include "joker.h" #include "list.h" +#include "save.h" #include "selection_grid.h" #include "soundbank.h" #include "splash_screen.h" @@ -64,6 +65,7 @@ #define CARD_UNFOCUSED_SEL_Y 15 #define CARD_FOCUSED_SEL_Y 20 +// Timer defs #define TM_RESET_STATIC_VARS 30 #define TM_END_POP_MENU_ANIM 13 #define TM_START_ROUND_END_REWARDS_ANIM 1 @@ -87,6 +89,7 @@ // TODO: Rename "PID" to "PAL_IDX" // Palette IDs + #define BOSS_BLIND_PRIMARY_PID 1 #define REROLL_BTN_PID 3 #define BLIND_SKIP_BTN_PID 5 @@ -428,9 +431,6 @@ static const BG_POINT HAND_PLAY_POS = {120, 70}; typedef void (*SubStateActionFn)(void); -// BY DEFAULT IS SET TO 1, but if changed to 2 or more, should speed up all (or most) of the game -// aspects that should be sped up by speed, as in the original game. -static int game_speed = 1; static enum BackgroundId background = BG_NONE; static StateInfo state_info[] = { @@ -552,6 +552,18 @@ static enum PlayState play_state = PLAY_STARTING; static enum HandType hand_type = NONE; static ContainedHandTypes _contained_hands = {0}; +// Initialization of the global var +// clang-format off +GameVariables g_game_vars = { + 0, 0, 0, + 0, 0, 0, + DEFAULT_GAME_SPEED, + DEFAULT_HIGH_CONTRAST, + DEFAULT_MUSIC_VOLUME, + DEFAULT_SOUND_VOLUME +}; +// clang-format on + // The sprite that displays the blind when in "GAME_PLAYING/GAME_ROUND_END" state static Sprite* playing_blind_token = NULL; @@ -586,9 +598,6 @@ static int max_discards = 4; static int hands = 0; static int discards = 0; -static int round = 0; -static int ante = 0; -static int money = 0; static u32 score = 0; static u32 temp_score = 0; // This is the score that shows in the same spot as the hand type. static bool score_flames_active = false; @@ -645,9 +654,6 @@ static int shortcut_joker_count = 0; static int four_fingers_joker_count = 0; -GBAL_UNUSED -GameVariables g_game_vars; - GBAL_UNUSED static inline bool is_shop_joker_avail(int joker_id) { @@ -729,7 +735,7 @@ static inline void jokers_available_to_shop_init(void) static void reroll_boss_blind(bool no_tiles) { // Showdown blinds only show up on ante 8, 16, etc... - next_boss_blind = roll_blind_type((ante % 8 == 0) && (ante > 0)); + next_boss_blind = roll_blind_type((g_game_vars.ante % 8 == 0) && (g_game_vars.ante > 0)); if (!no_tiles) { apply_blind_tiles(next_boss_blind, BOSS_BLIND_TOKEN_LAYER); @@ -781,6 +787,8 @@ void game_init() jokers_available_to_shop_init(); + load_game(); + hands = max_hands; discards = max_discards; g_game_vars.timer = TM_ZERO; @@ -788,10 +796,10 @@ void game_init() blinds_states[0] = BLIND_STATE_CURRENT; blinds_states[1] = BLIND_STATE_UPCOMING; blinds_states[2] = BLIND_STATE_UPCOMING; - ante = STARTING_ANTE; - money = STARTING_MONEY; + g_game_vars.ante = STARTING_ANTE; + g_game_vars.money = STARTING_MONEY; score = STARTING_SCORE; - round = 0; + g_game_vars.round = 0; // Initialize/reset unbeaten Boss/Showdown Blinds so they are all available init_unbeaten_blinds_list(false); @@ -918,6 +926,12 @@ void game_change_state(enum GameState new_game_state) } } +u32 get_rand() +{ + g_game_vars.rng_step++; + return rand(); +} + CardObject** get_hand_array(void) { return hand; @@ -1037,15 +1051,14 @@ int get_num_hands_remaining(void) return hands; } -int get_game_speed(void) +enum BlindType get_current_blind() { - return game_speed; + return current_blind; } -// for the future when a menu actually lets this variable be changed. -void set_game_speed(int new_game_speed) +enum BlindType get_next_boss_blind() { - game_speed = new_game_speed; + return next_boss_blind; } u32 get_chips(void) @@ -1068,16 +1081,6 @@ void set_mult(u32 new_mult) mult = new_mult; } -int get_money(void) -{ - return money; -} - -void set_money(int new_money) -{ - money = new_money; -} - void set_retrigger(bool new_retrigger) { retrigger = new_retrigger; @@ -1089,7 +1092,7 @@ void display_money() tte_erase_rect_wrapper(MONEY_TEXT_RECT); char money_str_buff[INT_MAX_DIGITS + 2]; // + 2 for null terminator and "$" sign - snprintf(money_str_buff, sizeof(money_str_buff), "$%d", money); + snprintf(money_str_buff, sizeof(money_str_buff), "$%d", g_game_vars.money); // Bias left so the number is centered and the "$" sign is on the left update_text_rect_to_center_str(&money_text_rect, money_str_buff, SCREEN_LEFT); @@ -1806,7 +1809,7 @@ static void display_score(u32 value) static void check_flaming_score(void) { u32 curr_score = u32_protected_mult(chips, mult); - u32 required_score = blind_get_requirement(current_blind, ante); + u32 required_score = blind_get_requirement(current_blind, g_game_vars.ante); if (curr_score >= required_score && !score_flames_active) { // start flaming score @@ -1834,7 +1837,7 @@ static void display_round(void) ROUND_TEXT_RECT.left, ROUND_TEXT_RECT.top, TTE_YELLOW_PB, - round + g_game_vars.round ); } @@ -1935,14 +1938,14 @@ static inline void deck_shuffle(void) { for (int i = deck_top; i > 0; i--) { - int j = rand() % (i + 1); + int j = get_rand() % (i + 1); Card* temp = deck[i]; deck[i] = deck[j]; deck[j] = temp; } } -static void game_round_on_init() +static void game_round_on_init(void) { hand_state = HAND_DRAW; cards_drawn = 0; @@ -1974,7 +1977,7 @@ static void game_round_on_init() } Rect blind_req_text_rect = BLIND_REQ_TEXT_RECT; - u32 blind_requirement = blind_get_requirement(current_blind, ante); + u32 blind_requirement = blind_get_requirement(current_blind, g_game_vars.ante); char blind_req_str_buff[UINT_MAX_DIGITS + 1]; @@ -2019,14 +2022,14 @@ static void game_over_init(void) main_bg_se_copy_rect(NEW_RUN_BTN_SRC_RECT, NEW_RUN_BTN_DEST_POS); } -static void game_lose_on_init() +static void game_lose_on_init(void) { game_over_init(); // Using the text color to match the "Game Over" text affine_background_set_color(TEXT_CLR_RED); } -static void game_win_on_init() +static void game_win_on_init(void) { game_over_init(); // Using the text color to match the "You Win" text @@ -2036,8 +2039,9 @@ static void game_win_on_init() // General functions static inline void set_seed(int seed) { + srand(seed); g_game_vars.rng_seed = seed; - srand(g_game_vars.rng_seed); + g_game_vars.rng_step = 0; } // Playing state functions @@ -2195,7 +2199,7 @@ static bool game_playing_hand_row_on_selection_changed( */ play_sfx( SFX_CARD_FOCUS, - MM_BASE_PITCH_RATE + rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, + MM_BASE_PITCH_RATE + get_rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, SFX_DEFAULT_VOLUME ); } @@ -2215,7 +2219,7 @@ static bool game_playing_hand_row_on_selection_changed( */ play_sfx( SFX_CARD_FOCUS, - MM_BASE_PITCH_RATE + rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, + MM_BASE_PITCH_RATE + get_rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, SFX_DEFAULT_VOLUME ); } @@ -2377,13 +2381,13 @@ static inline void game_playing_handle_round_over(void) { enum GameState next_state = GAME_STATE_ROUND_END; - if (score >= blind_get_requirement(current_blind, ante)) + if (score >= blind_get_requirement(current_blind, g_game_vars.ante)) { if (current_blind > BLIND_TYPE_BIG) { - if (ante < MAX_ANTE) + if (g_game_vars.ante < MAX_ANTE) { - display_ante(++ante); + display_ante(++g_game_vars.ante); // mark current boss blind as beaten and allow for reroll set_blind_beaten(next_boss_blind); @@ -2706,7 +2710,7 @@ static bool check_and_score_joker_for_event( static inline bool game_round_is_over(void) { - return hands == 0 || score >= blind_get_requirement(current_blind, ante); + return hands == 0 || score >= blind_get_requirement(current_blind, g_game_vars.ante); } // Basically a copy of HAND_DISCARD @@ -3156,7 +3160,7 @@ static inline void game_playing_process_input_and_state(void) display_chips(); static const int SCORE_CALC_SFX_PITCH_SHIFT = -102; // -10% OF MM_BASE_PITCH_RATE - static const int SCORE_CALC_SFX_VOLUME = 204; // 80% MM_FULL_VOLUME + static const int SCORE_CALC_SFX_VOLUME = 204; // 80% MM_SFX_FULL_VOLUME // The chips calculation SFX is the same as button play_sfx( @@ -3171,10 +3175,10 @@ static inline void game_playing_process_input_and_state(void) /* Using fixed point in case the score is lower than NUM_SCORE_LERP_STEPS and then * then the division rounds it down to 0 and it's never added to the total. * The operation is equivalent to - * fxdiv(int2fx(temp_score * get_game_speed()), int2fx(NUM_SCORE_LERP_STEPS)) + * fxdiv(int2fx(temp_score * g_game_vars.game_speed), int2fx(NUM_SCORE_LERP_STEPS)) */ - lerped_temp_score -= int2fx(temp_score * get_game_speed()) / NUM_SCORE_LERP_STEPS; - lerped_score += int2fx(temp_score * get_game_speed()) / NUM_SCORE_LERP_STEPS; + lerped_temp_score -= int2fx(temp_score * g_game_vars.game_speed) / NUM_SCORE_LERP_STEPS; + lerped_score += int2fx(temp_score * g_game_vars.game_speed) / NUM_SCORE_LERP_STEPS; if (lerped_temp_score > 0) { @@ -3504,7 +3508,7 @@ static inline void game_playing_process_flaming_score(void) } } -static void game_playing_on_update() +static void game_playing_on_update(void) { // Background logic (thissss might be moved to the card'ssss logic later. I'm a sssssnake) if (hand_state == HAND_DRAW || hand_state == HAND_DISCARD || hand_state == HAND_SELECT) @@ -3537,13 +3541,13 @@ static void game_playing_on_update() static int calculate_interest_reward(void) { - int reward = (money / 5) * INTEREST_PER_5; + int reward = (g_game_vars.money / 5) * INTEREST_PER_5; if (reward > MAX_INTEREST) reward = MAX_INTEREST; return reward; } -static void game_round_end_on_exit() +static void game_round_end_on_exit(void) { // Cleanup blind tokens from this round to avoid accumulating // allocated blind sprites each round @@ -3555,7 +3559,7 @@ static void game_round_end_on_exit() // TODO: Reuse sprites for blind selection? } -static void game_round_end_on_update() +static void game_round_end_on_update(void) { if (state_info[game_state].substate == ROUND_END_EXIT) { @@ -3606,7 +3610,7 @@ static void game_round_end_display_finished_blind() { obj_unhide(round_end_blind_token->obj, 0); - int current_ante = ante; + int current_ante = g_game_vars.ante; // Beating the boss blind increases the ante, so we need to display the previous ante value if (current_blind > BLIND_TYPE_BIG) @@ -3854,7 +3858,7 @@ static void game_round_end_display_rewards() static inline void game_round_end_cashout(void) { // Reward the player - money += hands + blind_get_reward(current_blind) + calculate_interest_reward(); + g_game_vars.money += hands + blind_get_reward(current_blind) + calculate_interest_reward(); display_money(); hands = max_hands; // Reset the hands to the maximum @@ -3969,7 +3973,7 @@ static inline int game_shop_get_rand_available_joker_id(void) return UNDEFINED; int matching_joker_ids[jokers_avail_size]; - int fallback_random_idx = random() % jokers_avail_size; + int fallback_random_idx = get_rand() % jokers_avail_size; int fallback_random_joker_id = UNDEFINED; int match_count = 0; @@ -3989,7 +3993,7 @@ static inline int game_shop_get_rand_available_joker_id(void) } int selected_joker_id = - (match_count > 0) ? matching_joker_ids[random() % match_count] : fallback_random_joker_id; + (match_count > 0) ? matching_joker_ids[get_rand() % match_count] : fallback_random_joker_id; return selected_joker_id; } @@ -4160,7 +4164,7 @@ static inline void game_sell_joker(int joker_idx) return; JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_owned_jokers_list, joker_idx); - money += joker_get_sell_value(joker_object->joker); + g_game_vars.money += joker_get_sell_value(joker_object->joker); display_money(); erase_price_under_sprite_object(joker_object->sprite_object); @@ -4216,8 +4220,8 @@ static inline void game_shop_buy_joker(int shop_joker_idx) { JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_shop_jokers_list, shop_joker_idx); - money -= joker_object->joker->value; // Deduct the money spent on the joker - display_money(); // Update the money display + g_game_vars.money -= joker_object->joker->value; // Deduct the money spent on the joker + display_money(); // Update the money display erase_price_under_sprite_object(joker_object->sprite_object); sprite_object_set_focus(joker_object->sprite_object, false); add_to_held_jokers(joker_object); @@ -4255,7 +4259,7 @@ static void shop_top_row_on_key_transit(SelectionGrid* selection_grid, Selection JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_shop_jokers_list, shop_joker_idx); if (joker_object == NULL || list_get_len(&_owned_jokers_list) >= MAX_JOKERS_HELD_SIZE || - money < joker_object->joker->value) + g_game_vars.money < joker_object->joker->value) { return; } @@ -4354,7 +4358,7 @@ static bool shop_reroll_row_on_selection_changed( static inline void game_shop_reroll(int* reroll_cost) { - money -= *reroll_cost; + g_game_vars.money -= *reroll_cost; display_money(); // Update the money display ListItr itr = list_itr_create(&_shop_jokers_list); @@ -4405,7 +4409,7 @@ static void shop_reroll_row_on_key_transit(SelectionGrid* selection_grid, Select return; } - if (money >= reroll_cost) + if (g_game_vars.money >= reroll_cost) { // TODO: Add money sound effect play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME); @@ -4503,7 +4507,7 @@ static inline void game_shop_lights_anim_frame(void) memcpy16(&pal_bg_mem[SHOP_LIGHTS_1_PID], &shifted_palette[3], 1); } -static void game_shop_on_update() +static void game_shop_on_update(void) { change_background(BG_SHOP); @@ -4536,7 +4540,7 @@ static void game_shop_on_update() shop_state_actions[substate](); } -static void game_shop_on_exit() +static void game_shop_on_exit(void) { ListItr itr = list_itr_create(&_shop_jokers_list); JokerObject* joker_object; @@ -4554,9 +4558,11 @@ static void game_shop_on_exit() list_clear(&_shop_jokers_list); increment_blind(BLIND_STATE_DEFEATED); // TODO: Move to game_round_end()? + + save_game(); } -static void game_blind_select_on_init() +static void game_blind_select_on_init(void) { change_background(BG_BLIND_SELECT); selection_x = 0; @@ -4565,7 +4571,7 @@ static void game_blind_select_on_init() play_sfx(SFX_POP, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME); } -static void game_blind_select_on_update() +static void game_blind_select_on_update(void) { if (state_info[game_state].substate == BLIND_SELECT_MAX) { @@ -4638,7 +4644,7 @@ static inline void game_blind_select_print_blind_req(enum BlindTokens blind) { Rect blind_req_score_rect = game_blind_select_get_req_score_rect(blind); - u32 blind_req = blind_get_requirement(get_blind_type_from_token(blind), ante); + u32 blind_req = blind_get_requirement(get_blind_type_from_token(blind), g_game_vars.ante); char blind_req_str_buff[UINT_MAX_DIGITS + 1]; truncate_uint_to_suffixed_str( @@ -4736,7 +4742,7 @@ static void game_blind_select_handle_input() play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME); state_info[game_state].substate = BLIND_SELECTED_ANIM_SEQ; g_game_vars.timer = TM_ZERO; - ++round; + ++g_game_vars.round; display_round(); } else if (current_blind <= BLIND_TYPE_BIG) @@ -4853,25 +4859,25 @@ static void game_blind_select_display_blind_panel() } } -static void game_blind_select_on_exit() +static void game_blind_select_on_exit(void) { selection_y = 0; background = UNDEFINED; } -void game_start() +void game_start(void) { - set_seed(g_game_vars.rng_seed); // set_seed(9); // 9 is a full house + set_seed(g_game_vars.rng_seed); affine_background_change_background(AFFINE_BG_GAME); - // Normally I would just cache these and hide/unhide but I didn't feel like dealing with - // defining a layer for it - hands = max_hands; discards = max_discards; + // Activate high contrast palette for cards if loaded settings tell us to + toggle_high_contrast_cards(g_game_vars.high_contrast); + // Fill the deck with all the cards. Later on this can be replaced with a more dynamic system // that allows for different decks and card types. for (int suit = 0; suit < NUM_SUITS; suit++) @@ -4912,7 +4918,7 @@ void game_start() ANTE_TEXT_RECT.left, ANTE_TEXT_RECT.top, TTE_YELLOW_PB, - ante, + g_game_vars.ante, TTE_WHITE_PB, MAX_ANTE ); // Ante @@ -4934,7 +4940,7 @@ static inline void game_over_process_user_input() } } -static void game_lose_on_update() +static void game_lose_on_update(void) { if (g_game_vars.timer < GAME_OVER_ANIM_FRAMES) { @@ -4956,7 +4962,7 @@ static void game_lose_on_update() // This function isn't set in stone. This is just a placeholder // allowing the player to restart the game. Thought it would be nice to have // util we decide what we want to do after a game over. -static void game_over_on_exit() +static void game_over_on_exit(void) { while (list_get_len(&_owned_jokers_list) > 0) { @@ -4996,7 +5002,7 @@ static void game_over_on_exit() ANTE_TEXT_RECT.left, ANTE_TEXT_RECT.top, TTE_YELLOW_PB, - ante, + g_game_vars.ante, TTE_WHITE_PB, MAX_ANTE ); @@ -5004,7 +5010,7 @@ static void game_over_on_exit() affine_background_load_palette(affine_background_gfxPal); } -static void game_win_on_update() +static void game_win_on_update(void) { if (g_game_vars.timer < GAME_OVER_ANIM_FRAMES) { diff --git a/source/game/common_ui.c b/source/game/common_ui.c index 2ebb69d..c28feac 100644 --- a/source/game/common_ui.c +++ b/source/game/common_ui.c @@ -2,6 +2,7 @@ #include "game.h" #include "game/main_menu.h" +#include "game/options_menu.h" typedef void (*BackgroundRenderCallback)(void); @@ -15,18 +16,18 @@ static const BackgroundRenderCallback bgCallbacks[] = { [BG_ROUND_END] = NULL, [BG_SHOP] = NULL, [BG_BLIND_SELECT] = NULL, + [BG_OPTIONS_MENU] = game_options_menu_change_background, [BG_MAIN_MENU] = game_main_menu_change_background, }; void change_background(enum BackgroundId id) { - if (id == BG_MAIN_MENU) + if (id != background && bgCallbacks[id] != NULL) { - if (id != background) - { - bgCallbacks[id](); - } + bgCallbacks[id](); } - change_background_legacy(id); background = id; + + // Can be removed once all states have their own "change_background" func + change_background_legacy(id); } diff --git a/source/game/main_menu.c b/source/game/main_menu.c index cea7af4..7796df0 100644 --- a/source/game/main_menu.c +++ b/source/game/main_menu.c @@ -9,29 +9,79 @@ #include "game/common_ui.h" #include "game_variables.h" #include "graphic_utils.h" +#include "selection_grid.h" #include "soundbank.h" #include "sprite.h" +#include "version.h" #include #include #include #include -static const u32 PLAY_BUTTON_OUTLINE_PID = 2; -static const u32 PLAY_BUTTON_MAIN_COLOR_PID = 5; +#define PLAY_BUTTON_MAIN_COLOR_PID 5 +#define PLAY_BUTTON_OUTLINE_PID 6 +#define OPTIONS_BUTTON_MAIN_COLOR_PID 7 +#define OPTIONS_BUTTON_OUTLINE_PID 1 -static const u32 IMPLEMENTED_BUTTONS = 1; // Remove this once all buttons are implemented -static const u32 PLAY_BTN_IDX = 0; +enum MainButtons +{ + PLAY_BTN_IDX, + OPTIONS_BTN_IDX, + MAIN_MENU_NB_BTN +}; // Pixel sizes #define MAIN_MENU_ACE_T_X 88 #define MAIN_MENU_ACE_T_Y 26 +// clang-format off +static const BG_POINT GBALATRO_VERSION_TEXT_POS = {0, 152}; +static const Rect GBALATRO_VERSION_TEXT_RECT = {0, 152, 240, 160}; +// clang-format on + +// Define SelectionGrid for the main menu buttons + +static int main_menu_return_row_size(void); +static bool main_menu_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +); +static void main_menu_on_key_transit(SelectionGrid* selection_grid, Selection* selection); + +static void play_on_pressed(void); +static void options_on_pressed(void); + +// clang-format off +static SelectionGridRow main_menu_selection_rows[] = { + { + 0, + main_menu_return_row_size, + main_menu_on_selection_changed, + main_menu_on_key_transit, + {.wrap = false} + }, +}; + +static Button main_menu_buttons[] = { + {PLAY_BUTTON_OUTLINE_PID, PLAY_BUTTON_MAIN_COLOR_PID, play_on_pressed, NULL}, + {OPTIONS_BUTTON_OUTLINE_PID, OPTIONS_BUTTON_MAIN_COLOR_PID, options_on_pressed, NULL}, +}; + +static SelectionGrid main_menu_selection_grid = { + main_menu_selection_rows, + 1, + {0, 0} +}; +// clang-format on + // Main menu sprite - the ace of spades static CardObject* main_menu_ace = NULL; -// Current selected button index -static int selection_x = 0; +// Keep track of last highlighted button +static enum MainButtons last_highlighted_button = PLAY_BTN_IDX; void game_main_menu_change_background(void) { @@ -42,11 +92,14 @@ void game_main_menu_change_background(void) GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_main_menu_gfxTiles); GRIT_CPY(&se_mem[MAIN_BG_SBB], background_main_menu_gfxMap); - // Disable the button highlight colors - memcpy16(&pal_bg_mem[PLAY_BUTTON_OUTLINE_PID], &pal_bg_mem[PLAY_BUTTON_MAIN_COLOR_PID], 1); + // Disable all button highlight colors + for (int i = 0; i < MAIN_MENU_NB_BTN; i++) + { + button_set_highlight(&main_menu_buttons[i], false); + } } -void game_main_menu_on_init() +void game_main_menu_on_init(void) { affine_background_change_background(AFFINE_BG_MAIN_MENU); change_background(BG_MAIN_MENU); @@ -58,10 +111,18 @@ void game_main_menu_on_init() main_menu_ace->sprite_object->ty = int2fx(MAIN_MENU_ACE_T_Y); main_menu_ace->sprite_object->y = main_menu_ace->sprite_object->ty; main_menu_ace->sprite_object->tscale = float2fx(0.8f); - selection_x = 0; + card_object_update(main_menu_ace); + + // Select last highlighted button, Play button by default. + // e.g. if we return from the options menu, we want the Options button to be highlighted. + Selection sel_init = {last_highlighted_button, 0}; + main_menu_selection_grid.selection = sel_init; + + // Highlight current button + button_set_highlight(&main_menu_buttons[main_menu_selection_grid.selection.x], true); } -void game_main_menu_on_update() +void game_main_menu_on_update(void) { card_object_update(main_menu_ace); main_menu_ace->sprite_object->trotation = lu_sin((g_game_vars.timer << 8) / 2) / 3; @@ -75,41 +136,91 @@ void game_main_menu_on_update() g_game_vars.rng_seed *= 2; } - if (key_hit(KEY_LEFT)) - { - if (selection_x > 0) - { - selection_x--; - } - } - else if (key_hit(KEY_RIGHT)) - { - if (selection_x < IMPLEMENTED_BUTTONS - 1) - { - selection_x++; - } - } - - if (selection_x == PLAY_BTN_IDX) - { - memset16(&pal_bg_mem[PLAY_BUTTON_OUTLINE_PID], BTN_HIGHLIGHT_COLOR, 1); - - if (key_hit(SELECT_CARD)) - { - play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME); - game_change_state(GAME_STATE_GAME_START); - } - } - else - { - memcpy16(&pal_bg_mem[PLAY_BUTTON_OUTLINE_PID], &pal_bg_mem[PLAY_BUTTON_MAIN_COLOR_PID], 1); - } + selection_grid_process_input(&main_menu_selection_grid); } -void game_main_menu_on_exit() +void game_main_menu_on_exit(void) { + // Save selected button + last_highlighted_button = main_menu_selection_grid.selection.x; + // Normally I would just cache these and hide/unhide but I didn't feel like dealing with // defining a layer for it card_destroy(&main_menu_ace->card); card_object_destroy(&main_menu_ace); } + +// Implement SelectionGrid handler functions + +static int main_menu_return_row_size(void) +{ + return MAIN_MENU_NB_BTN; +} + +static bool main_menu_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +) +{ + if (prev_selection->x >= 0 && prev_selection->x < MAIN_MENU_NB_BTN) + { + button_set_highlight(&main_menu_buttons[prev_selection->x], false); + } + + play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME); + button_set_highlight(&main_menu_buttons[new_selection->x], true); + + return true; +} + +static void main_menu_on_key_transit(SelectionGrid* selection_grid, Selection* selection) +{ + // DEBUG: toggle git hash display by pressing L+R+START+SELECT + static bool combo_pressed = false; + static bool show_version = false; + if (key_is_down(KEY_START) && key_is_down(KEY_SELECT) && key_is_down(KEY_L) && + key_is_down(KEY_R)) + { + if (!combo_pressed) + { + show_version = !show_version; + if (show_version) + { + tte_printf( + "#{P:%d,%d; cx:0x%X000}%s", + GBALATRO_VERSION_TEXT_POS.x, + GBALATRO_VERSION_TEXT_POS.y, + TTE_WHITE_PB, + gbalatro_version + ); + } + else + { + tte_erase_rect_wrapper(GBALATRO_VERSION_TEXT_RECT); + } + } + + combo_pressed = true; + } + else + { + combo_pressed = false; + } + + if (key_hit(SELECT_CARD)) + { + button_press(&main_menu_buttons[selection->x]); + } +} + +static void play_on_pressed(void) +{ + game_change_state(GAME_STATE_GAME_START); +} + +static void options_on_pressed(void) +{ + game_change_state(GAME_STATE_OPTIONS_MENU); +} diff --git a/source/game/options_menu.c b/source/game/options_menu.c new file mode 100644 index 0000000..ebc48ee --- /dev/null +++ b/source/game/options_menu.c @@ -0,0 +1,624 @@ +#include "game/options_menu.h" + +#include "affine_background.h" +#include "audio_utils.h" +#include "background_options_menu_gfx.h" +#include "button.h" +#include "game.h" +#include "game/common_ui.h" +#include "game_variables.h" +#include "graphic_utils.h" +#include "save.h" +#include "selection_grid.h" +#include "soundbank.h" +#include "util.h" + +#include +#include +#include +#include +#include + +// Button indices +enum OptionButtons +{ + GAME_SPEED_BTN_IDX, + HIGH_CONTRAST_BTN_IDX, + MUSIC_VOLUME_BTN_IDX, + SOUND_VOLUME_BTN_IDX, + BACK_BTN_IDX, + NB_OPTIONS_BUTTONS +}; +enum OptionSpeedButtons +{ + GAME_SPEED_DOWN_BTN_IDX, + GAME_SPEED_UP_BTN_IDX, + NB_GAME_SPEED_BUTTONS +}; + +// Color palette indices +#define MENU_BUTTON_MAIN_COLOR_PAL_IDX 1 +#define BACK_BUTTON_MAIN_COLOR_PAL_IDX 3 +#define SPEED_DOWN_BUTTON_OUTLINE_COLOR_PAL_IDX 4 +#define SPEED_BUTTON_OUTLINE_COLOR_PAL_IDX 5 +#define SPEED_UP_BUTTON_OUTLINE_COLOR_PAL_IDX 6 +#define CONTRAST_BUTTON_OUTLINE_COLOR_PAL_IDX 7 +#define MUSIC_BUTTON_OUTLINE_COLOR_PAL_IDX 8 +#define SOUND_BUTTON_OUTLINE_COLOR_PAL_IDX 9 +#define BACK_BUTTON_OUTLINE_COLOR_PAL_IDX 10 + +// Define selection grid for the menu buttons + +static void game_speed_down_on_pressed(void); +static void game_speed_up_on_pressed(void); +static void high_contrast_on_pressed(void); +static void back_on_pressed(void); +static int options_menu_return_row_size(void); +static void options_menu_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection); +static bool game_speed_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +); +static bool music_volume_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +); +static bool sound_volume_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +); +static bool regular_button_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +); + +// clang-format off +static SelectionGridRow options_menu_selection_rows[] = { + { + GAME_SPEED_BTN_IDX, + options_menu_return_row_size, + game_speed_row_on_selection_changed, + NULL, + {.wrap = true} // The wrapping is used to allow button activation with Left/Right arrows by + // putting logic into the `game_speed_row_on_selection_changed` function + }, + { + HIGH_CONTRAST_BTN_IDX, + options_menu_return_row_size, + regular_button_row_on_selection_changed, + options_menu_row_on_key_transit, + {.wrap = false} + }, + { + MUSIC_VOLUME_BTN_IDX, + options_menu_return_row_size, + music_volume_row_on_selection_changed, + NULL, + {.wrap = true} // Same trick as Game Speed button + }, + { + SOUND_VOLUME_BTN_IDX, + options_menu_return_row_size, + sound_volume_row_on_selection_changed, + NULL, + {.wrap = true} // Same trick as Game Speed button + }, + { + BACK_BTN_IDX, + options_menu_return_row_size, + regular_button_row_on_selection_changed, + options_menu_row_on_key_transit, + {.wrap = false} + } +}; + +static Button options_menu_buttons[] = { + {SPEED_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, NULL, NULL}, + {CONTRAST_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, high_contrast_on_pressed, NULL}, + {MUSIC_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, NULL, NULL}, + {SOUND_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, NULL, NULL}, + {BACK_BUTTON_OUTLINE_COLOR_PAL_IDX, BACK_BUTTON_MAIN_COLOR_PAL_IDX, back_on_pressed, NULL} +}; + +static Button game_speed_buttons[] = { + {SPEED_DOWN_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, game_speed_down_on_pressed, NULL}, + {SPEED_UP_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, game_speed_up_on_pressed, NULL}, +}; +// clang-format on + +const Selection OPTIONS_MENU_INIT_SEL = {0, 0}; + +static SelectionGrid options_menu_selection_grid = { + options_menu_selection_rows, + NB_OPTIONS_BUTTONS, + OPTIONS_MENU_INIT_SEL +}; + +// Positions/Rects used to construct and update the menu +// clang-format off + +// Values in tiles +static const Rect OPTIONS_SPEED_DOWN_ACTIVE_BTN_SRC_RECT = { 3, 20, 3, 21}; +static const Rect OPTIONS_SPEED_DOWN_DISABLED_BTN_SRC_RECT = { 9, 22, 9, 23}; +static const BG_POINT OPTIONS_SPEED_DOWN_BTN_DEST_POS = {11, 3}; +static const Rect OPTIONS_SPEED_UP_ACTIVE_BTN_SRC_RECT = {10, 20, 10, 21}; +static const Rect OPTIONS_SPEED_UP_DISABLED_BTN_SRC_RECT = {10, 22, 10, 23}; +static const BG_POINT OPTIONS_SPEED_UP_BTN_DEST_POS = {18, 3}; +static const Rect OPTIONS_SPEED_VALUES[GAME_SPEED_MAX] = { { 6, 20, 7, 21}, + { 3, 22, 4, 23}, + { 5, 22, 6, 23}, + { 7, 22, 8, 23} }; +static const BG_POINT OPTIONS_SPEED_VALUE_DEST_POS = {14, 3}; + +static const Rect OPTIONS_CONTRAST_VALUE_YES_SRC_RECT = { 4, 24, 7, 25}; +static const Rect OPTIONS_CONTRAST_VALUE_NO_SRC_RECT = { 9, 24, 12, 25}; +static const BG_POINT OPTIONS_CONTRAST_VALUE_DEST_POS = {13, 7}; + +static const Rect OPTIONS_MUSIC_SLIDER_FULL_SRC = {12, 20, 12, 20}; +static const Rect OPTIONS_MUSIC_SLIDER_MID_SRC = {13, 20, 13, 20}; +static const Rect OPTIONS_MUSIC_SLIDER_EMPTY_SRC = {14, 20, 14, 20}; +static const Rect OPTIONS_SOUND_SLIDER_FULL_SRC = {12, 22, 12, 22}; +static const Rect OPTIONS_SOUND_SLIDER_MID_SRC = {13, 22, 13, 22}; +static const Rect OPTIONS_SOUND_SLIDER_EMPTY_SRC = {14, 22, 14, 22}; +static const BG_POINT OPTIONS_MUSIC_SLIDER_START_POS = { 5, 11}; +static const BG_POINT OPTIONS_SOUND_SLIDER_START_POS = { 5, 14}; +static const BG_POINT OPTIONS_MUSIC_SLIDER_END_POS = {24, 11}; +static const BG_POINT OPTIONS_SOUND_SLIDER_END_POS = {24, 14}; +static const u8 OPTIONS_MUSIC_SLIDER_SEGMENT_LENGTH = (OPTIONS_MUSIC_SLIDER_END_POS.x - OPTIONS_MUSIC_SLIDER_START_POS.x + 1) + / VOLUME_OPTION_MAX; +static const u8 OPTIONS_SOUND_SLIDER_SEGMENT_LENGTH = (OPTIONS_SOUND_SLIDER_END_POS.x - OPTIONS_SOUND_SLIDER_START_POS.x + 1) + / VOLUME_OPTION_MAX; + +// Values in pixels +static const BG_POINT OPTIONS_GAME_SPEED_TEXT_POS = { 82, 16}; +static const BG_POINT OPTIONS_HIGH_CONTRAST_TEXT_POS = { 40, 48}; +static const BG_POINT OPTIONS_MUSIC_VOLUME_TEXT_POS = { 56, 80}; +static const BG_POINT OPTIONS_MUSIC_VALUE_TEXT_POS = {160, 80}; +static const BG_POINT OPTIONS_SOUND_VOLUME_TEXT_POS = { 56, 104}; +static const BG_POINT OPTIONS_SOUND_VALUE_TEXT_POS = {160, 104}; +static const BG_POINT OPTIONS_BACK_SAVE_TEXT_POS = { 72, 136}; +// clang-format on + +#define GAME_SPEED_ARROW_HIGHLIGHT_DURATION 10 +enum OptionSpeedButtons game_speed_arrow_highlight_button = GAME_SPEED_UP_BTN_IDX; +static s32 game_speed_arrow_highlight_start = UNDEFINED; + +static bool any_value_changed = false; +static bool back_btn_is_save_state = false; + +static void disable_all_game_speed_outlines_except_self(enum OptionSpeedButtons highlighted_btn) +{ + for (int i = 0; i < NB_GAME_SPEED_BUTTONS; i++) + { + button_set_highlight(&game_speed_buttons[i], i == highlighted_btn); + } +} + +static void disable_all_outlines_except_self(enum OptionButtons highlighted_btn) +{ + // These two get disabled no matter what here + disable_all_game_speed_outlines_except_self(NB_GAME_SPEED_BUTTONS); + + for (int i = 0; i < NB_OPTIONS_BUTTONS; i++) + { + button_set_highlight(&options_menu_buttons[i], i == highlighted_btn); + } +} + +static void update_game_speed_button_graphics() +{ + // check if need to disable game speed arrows + if (g_game_vars.game_speed == GAME_SPEED_MIN) + { + main_bg_se_copy_rect( + OPTIONS_SPEED_DOWN_DISABLED_BTN_SRC_RECT, + OPTIONS_SPEED_DOWN_BTN_DEST_POS + ); + } + else + { + main_bg_se_copy_rect( + OPTIONS_SPEED_DOWN_ACTIVE_BTN_SRC_RECT, + OPTIONS_SPEED_DOWN_BTN_DEST_POS + ); + } + + if (g_game_vars.game_speed == GAME_SPEED_MAX) + { + main_bg_se_copy_rect(OPTIONS_SPEED_UP_DISABLED_BTN_SRC_RECT, OPTIONS_SPEED_UP_BTN_DEST_POS); + } + else + { + main_bg_se_copy_rect(OPTIONS_SPEED_UP_ACTIVE_BTN_SRC_RECT, OPTIONS_SPEED_UP_BTN_DEST_POS); + } + + main_bg_se_copy_rect( + OPTIONS_SPEED_VALUES[g_game_vars.game_speed - 1], + OPTIONS_SPEED_VALUE_DEST_POS + ); + + any_value_changed = true; +} + +static void update_high_contrast_button_graphics() +{ + if (g_game_vars.high_contrast) + { + main_bg_se_copy_rect(OPTIONS_CONTRAST_VALUE_YES_SRC_RECT, OPTIONS_CONTRAST_VALUE_DEST_POS); + } + else + { + main_bg_se_copy_rect(OPTIONS_CONTRAST_VALUE_NO_SRC_RECT, OPTIONS_CONTRAST_VALUE_DEST_POS); + } + + any_value_changed = true; +} + +static void update_volume_slider_graphics(enum OptionButtons slider_idx) +{ + u8 slider_value; + BG_POINT slider_segment_dest; + int slider_segment_length; + Rect slider_segment_full; + Rect slider_segment_mid; + Rect slider_segment_empty; + BG_POINT slider_text_pos; + + switch (slider_idx) + { + case MUSIC_VOLUME_BTN_IDX: + slider_value = g_game_vars.music_volume; + slider_segment_dest = OPTIONS_MUSIC_SLIDER_START_POS; + slider_segment_length = OPTIONS_MUSIC_SLIDER_SEGMENT_LENGTH; + slider_segment_full = OPTIONS_MUSIC_SLIDER_FULL_SRC; + slider_segment_mid = OPTIONS_MUSIC_SLIDER_MID_SRC; + slider_segment_empty = OPTIONS_MUSIC_SLIDER_EMPTY_SRC; + slider_text_pos = OPTIONS_MUSIC_VALUE_TEXT_POS; + break; + case SOUND_VOLUME_BTN_IDX: + slider_value = g_game_vars.sound_volume; + slider_segment_dest = OPTIONS_SOUND_SLIDER_START_POS; + slider_segment_length = OPTIONS_SOUND_SLIDER_SEGMENT_LENGTH; + slider_segment_full = OPTIONS_SOUND_SLIDER_FULL_SRC; + slider_segment_mid = OPTIONS_SOUND_SLIDER_MID_SRC; + slider_segment_empty = OPTIONS_SOUND_SLIDER_EMPTY_SRC; + slider_text_pos = OPTIONS_SOUND_VALUE_TEXT_POS; + break; + // This function only takes in a volume slider + default: + return; + } + + int i = 0; + + // full part of the bar + for (; i < slider_value * slider_segment_length - 1; i++) + { + main_bg_se_copy_rect(slider_segment_full, slider_segment_dest); + slider_segment_dest.x++; + } + + // at exactly `slider_value` we either: + // - are in the middle of the bar, then we draw the frontier between full and empty tiles + // - are at the end, then draw a full segment + if (slider_value == VOLUME_OPTION_MAX) + { + main_bg_se_copy_rect(slider_segment_full, slider_segment_dest); + } + else + { + if (slider_value != VOLUME_OPTION_MIN) + { + // draw middle point + main_bg_se_copy_rect(slider_segment_mid, slider_segment_dest); + i++; + slider_segment_dest.x++; + } + + // empty part of the bar + for (; i < VOLUME_OPTION_MAX * slider_segment_length; i++) + { + main_bg_se_copy_rect(slider_segment_empty, slider_segment_dest); + slider_segment_dest.x++; + } + } + + tte_printf( + "#{P:%d,%d; cx:0x%X000}%3d", + slider_text_pos.x, + slider_text_pos.y, + TTE_WHITE_PB, + (slider_value * VOLUME_OPTION_INCREMENT) + ); + + any_value_changed = true; +} + +static void change_back_save_text(bool is_back) +{ + char* btn_text = is_back ? " Back " : "Save Changes"; + tte_printf( + "#{P:%d,%d; cx:0x%X000}%s", + OPTIONS_BACK_SAVE_TEXT_POS.x, + OPTIONS_BACK_SAVE_TEXT_POS.y, + TTE_WHITE_PB, + btn_text + ); +} + +void game_options_menu_change_background(void) +{ + tte_erase_screen(); + CBB_CLEAR(MAIN_BG_CBB); + + GRIT_CPY(pal_bg_mem, background_options_menu_gfxPal); + GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_options_menu_gfxTiles); + GRIT_CPY(&se_mem[MAIN_BG_SBB], background_options_menu_gfxMap); + + tte_printf( + "#{P:%d,%d; cx:0x%X000}Game Speed", + OPTIONS_GAME_SPEED_TEXT_POS.x, + OPTIONS_GAME_SPEED_TEXT_POS.y, + TTE_WHITE_PB + ); + tte_printf( + "#{P:%d,%d; cx:0x%X000}High Contrast Cards", + OPTIONS_HIGH_CONTRAST_TEXT_POS.x, + OPTIONS_HIGH_CONTRAST_TEXT_POS.y, + TTE_WHITE_PB + ); + tte_printf( + "#{P:%d,%d; cx:0x%X000}Music Volume", + OPTIONS_MUSIC_VOLUME_TEXT_POS.x, + OPTIONS_MUSIC_VOLUME_TEXT_POS.y, + TTE_WHITE_PB + ); + tte_printf( + "#{P:%d,%d; cx:0x%X000}Sound Volume", + OPTIONS_SOUND_VOLUME_TEXT_POS.x, + OPTIONS_SOUND_VOLUME_TEXT_POS.y, + TTE_WHITE_PB + ); + change_back_save_text(true); +} + +void game_options_menu_on_init(void) +{ + change_background(BG_OPTIONS_MENU); + + // Select game speed button by default + options_menu_selection_grid.selection = OPTIONS_MENU_INIT_SEL; + disable_all_outlines_except_self(GAME_SPEED_BTN_IDX); + + // Do an update on the first frame + update_game_speed_button_graphics(); + update_high_contrast_button_graphics(); + update_volume_slider_graphics(MUSIC_VOLUME_BTN_IDX); + update_volume_slider_graphics(SOUND_VOLUME_BTN_IDX); + + // Prevent "Back" button to become "Save Changes" when we first load the data, + // the player hasn't technically changed anything so it would be confusing. + any_value_changed = false; +} + +void game_options_menu_on_update(void) +{ + selection_grid_process_input(&options_menu_selection_grid); + + // game speed arrows small animation: they stay highlighted for a few frames + if (game_speed_arrow_highlight_start != UNDEFINED && + (g_game_vars.timer - game_speed_arrow_highlight_start) > + GAME_SPEED_ARROW_HIGHLIGHT_DURATION) + { + game_speed_arrow_highlight_start = UNDEFINED; + button_set_highlight(&game_speed_buttons[game_speed_arrow_highlight_button], false); + } + + // Except on the first update where it is forced to display the loaded values + // instead of the default ones, if any value was changed, edit the last button + // text to read "Save Changes" instead of "Back" + if (any_value_changed && !back_btn_is_save_state) + { + change_back_save_text(false); + back_btn_is_save_state = true; + } + + any_value_changed = false; +} + +void game_options_menu_on_exit(void) +{ + tte_erase_screen(); +} + +static void game_speed_down_on_pressed(void) +{ + g_game_vars.game_speed--; + game_speed_arrow_highlight_start = g_game_vars.timer; + game_speed_arrow_highlight_button = GAME_SPEED_DOWN_BTN_IDX; + disable_all_game_speed_outlines_except_self(GAME_SPEED_DOWN_BTN_IDX); + update_game_speed_button_graphics(); +} + +static void game_speed_up_on_pressed(void) +{ + g_game_vars.game_speed++; + game_speed_arrow_highlight_start = g_game_vars.timer; + game_speed_arrow_highlight_button = GAME_SPEED_UP_BTN_IDX; + disable_all_game_speed_outlines_except_self(GAME_SPEED_UP_BTN_IDX); + update_game_speed_button_graphics(); +} + +/** + * @brief Handles input for the high contrast card toggle button and nothing more. + */ +static void high_contrast_on_pressed(void) +{ + g_game_vars.high_contrast = (g_game_vars.high_contrast == 1) ? false : true; + update_high_contrast_button_graphics(); +} + +/** + * @brief Handles input for the Back/Save button. + */ +static void back_on_pressed(void) +{ + if (back_btn_is_save_state) + { + save_game(); + change_back_save_text(true); + back_btn_is_save_state = false; + } + else + { + game_change_state(GAME_STATE_MAIN_MENU); + } +} + +/** + * @brief Gives the width of options menu rows in selection grid. + * + * @returns Always 1, all rows contain only 1 button. Could have been a define + * but I had to make this a function so it could be given to the + * SelectionGrid constructor. + */ +static int options_menu_return_row_size(void) +{ + return 1; +} + +static void change_button_highlight( + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +) +{ + if (prev_selection->y == row_idx && prev_selection->x >= 0 && + prev_selection->x < NB_OPTIONS_BUTTONS) + { + button_set_highlight(&options_menu_buttons[row_idx], false); + } + + if (new_selection->y == row_idx) + { + play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME); + button_set_highlight(&options_menu_buttons[row_idx], true); + } +} + +/** + * @brief Handles left/Right inputs on the row of the Game Speed button. + * Is used to increased/decrease the speed value, instead of relying on a + * `on_key_transit` function because the latter does not take directional + * inputs. + */ +static bool game_speed_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +) +{ + change_button_highlight(row_idx, prev_selection, new_selection); + disable_all_game_speed_outlines_except_self(NB_GAME_SPEED_BUTTONS); + + // Can only change game speed by pressing left/right while staying on the button's row + if (prev_selection->y != new_selection->y) + return true; + + if (key_hit(KEY_LEFT) && g_game_vars.game_speed > GAME_SPEED_MIN) + { + button_press(&game_speed_buttons[GAME_SPEED_DOWN_BTN_IDX]); + } + else if (key_hit(KEY_RIGHT) && g_game_vars.game_speed < GAME_SPEED_MAX) + { + button_press(&game_speed_buttons[GAME_SPEED_UP_BTN_IDX]); + } + + return true; +} + +/** + * @brief Handles input for all SelectionGridRows in options menu, only actually does + * something on rows containing "normal" buttons (those for which the on_pressed + * method in options_menu_buttons is not NULL). + */ +static void options_menu_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection) +{ + if (key_hit(SELECT_CARD)) + { + button_press(&options_menu_buttons[selection->y]); + } +} + +static bool regular_button_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +) +{ + change_button_highlight(row_idx, prev_selection, new_selection); + return true; +} + +static bool music_volume_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +) +{ + change_button_highlight(row_idx, prev_selection, new_selection); + + if (prev_selection->y != new_selection->y) + return true; + + if (key_hit(KEY_LEFT) && g_game_vars.music_volume > VOLUME_OPTION_MIN) + { + g_game_vars.music_volume--; + } + else if (key_hit(KEY_RIGHT) && g_game_vars.music_volume < VOLUME_OPTION_MAX) + { + g_game_vars.music_volume++; + } + + update_volume_slider_graphics(MUSIC_VOLUME_BTN_IDX); + mmSetModuleVolume(MM_MODULE_FULL_VOLUME * g_game_vars.music_volume / VOLUME_OPTION_MAX); + + return true; +} + +static bool sound_volume_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +) +{ + change_button_highlight(row_idx, prev_selection, new_selection); + + if (prev_selection->y != new_selection->y) + return true; + + if (key_hit(KEY_LEFT) && g_game_vars.sound_volume > VOLUME_OPTION_MIN) + { + g_game_vars.sound_volume--; + } + else if (key_hit(KEY_RIGHT) && g_game_vars.sound_volume < VOLUME_OPTION_MAX) + { + g_game_vars.sound_volume++; + } + + update_volume_slider_graphics(SOUND_VOLUME_BTN_IDX); + + return true; +} diff --git a/source/joker.c b/source/joker.c index 2b976f8..3e47929 100644 --- a/source/joker.c +++ b/source/joker.c @@ -1,6 +1,7 @@ #include "joker.h" #include "card.h" +#include "game_variables.h" #include "graphic_utils.h" #include "joker_gfx.h" #include "pool.h" @@ -271,7 +272,7 @@ bool joker_object_score( u32 chips = get_chips(); u32 mult = get_mult(); - int money = get_money(); + int money = g_game_vars.money; if (effect_flags_ret & JOKER_EFFECT_FLAG_RETRIGGER) { @@ -345,7 +346,7 @@ bool joker_object_score( // Update values set_chips(chips); set_mult(mult); - set_money(money); + g_game_vars.money = money; // Update displays display_chips(); @@ -367,7 +368,7 @@ Sprite* joker_object_get_sprite(JokerObject* joker_object) int joker_get_random_rarity() { int joker_rarity = 0; - int rarity_roll = random() % 100; + int rarity_roll = get_rand() % 100; if (rarity_roll < COMMON_JOKER_CHANCE) { joker_rarity = COMMON_JOKER; diff --git a/source/joker_effects.c b/source/joker_effects.c index a14733f..b3c7f56 100644 --- a/source/joker_effects.c +++ b/source/joker_effects.c @@ -562,7 +562,7 @@ static u32 misprint_joker_effect( *joker_effect = &shared_joker_effect; - (*joker_effect)->mult = random() % (MISPRINT_MAX_MULT + 1); + (*joker_effect)->mult = get_rand() % (MISPRINT_MAX_MULT + 1); return JOKER_EFFECT_FLAG_MULT; } @@ -775,7 +775,7 @@ static u32 reserved_parking_joker_effect( u32 effect_flags_ret = JOKER_EFFECT_FLAG_NONE; - if ((random() % 2 == 0) && card_is_face(scored_card)) + if ((get_rand() % 2 == 0) && card_is_face(scored_card)) { *joker_effect = &shared_joker_effect; @@ -797,7 +797,7 @@ static u32 business_card_joker_effect( u32 effect_flags_ret = JOKER_EFFECT_FLAG_NONE; - if ((random() % 2 == 0) && card_is_face(scored_card)) + if ((get_rand() % 2 == 0) && card_is_face(scored_card)) { *joker_effect = &shared_joker_effect; @@ -885,11 +885,11 @@ static u32 bull_joker_effect( // The wiki says it does nothing if money is 0 or below // This allows us to avoid scoring negative Chips - if (get_money() > 0) + if (g_game_vars.money > 0) { *joker_effect = &shared_joker_effect; - (*joker_effect)->chips = get_money() * 2; + (*joker_effect)->chips = g_game_vars.money * 2; effect_flags_ret = JOKER_EFFECT_FLAG_CHIPS; } @@ -1154,11 +1154,11 @@ static u32 bootstraps_joker_effect( u32 effect_flags_ret = JOKER_EFFECT_FLAG_NONE; // Same protection as the Bull Joker - if (get_money() > 0) + if (g_game_vars.money > 0) { *joker_effect = &shared_joker_effect; - (*joker_effect)->mult = (get_money() / 5) * 2; + (*joker_effect)->mult = (g_game_vars.money / 5) * 2; effect_flags_ret = JOKER_EFFECT_FLAG_MULT; } diff --git a/source/save.c b/source/save.c new file mode 100644 index 0000000..af722b3 --- /dev/null +++ b/source/save.c @@ -0,0 +1,166 @@ +/** + * @file save.c + */ +#include "save.h" + +#include "audio_utils.h" +#include "game.h" +#include "joker.h" +#include "list.h" +#include "util.h" +#include "version.h" + +#include +#include + +// See https://gbadev.net/gbadoc/memory.html for more details on SRAM +// A few important pieces of info: +// - Read/Writes are limited to 8-bits words so they are done byte per byte +// - Memory is filled with 1s by default +// (at least in mgba, not sure about real HW) + +#define CHECK_BASE 0x0000 +#define GAME_BASE 0x0010 +#define LISTS_BASE 0x0060 + +#define CHECK_MAGIC 0x4C414247 // Spells GBAL, used to determine if the save data is junk +#define CHECK_HASH_SIZE 7 +#define GIT_HASH_START 17 // starts after "GBALATRO-VERSION:" in the gbalatro_version var + +// clang-format off +/** + * @brief SaveHeader for validation checks + * + * Structure holding save data header info to be packed and written to SRAM for validation. + * Defined in this discussion as follows: https://github.com/GBALATRO/balatro-gba/discussions/450 + * word | Byte 0 | Byte 1 | Byte 2 | Byte 3 | name | purpose + * -----|--------|--------|--------|--------|--------------|------------------------------------------------------------------ + * 0 | 0x47 | 0x42 | 0x41 | 0x4C | MAGIC | Identify if proceeding data is valid and not junk, spells "GBAL" + * 1 | Dirty | H[0] | H[1] | H[2] | GITHASH_LOW | Dirty flag, followed by the first 3 bytes of shortened git hash H + * 2 | H[3] | H[4] | H[5] | H[6] | GITHASH_HIGH | Last 4 bytes of shortened git hash H, with a dirty flag + */ +// clang-format on +typedef struct SaveHeader +{ + u32 magic; + bool dirty; + char githash[CHECK_HASH_SIZE]; +} SaveHeader; + +/** + * @brief Write raw binary data to SRAM + * + * @param sram_base address written to in the SRAM + * @param bytes pointer to the written data + * @param size number of bytes written + */ +static inline void write_sram(u32 sram_base, const u8* bytes, u32 size) +{ + if (sram_base + size > SRAM_SIZE) + return; + + for (u32 i = 0; i < size; i++) + { + sram_mem[sram_base + i] = bytes[i]; + } +} + +/** + * @brief Read raw binary data from SRAM + * + * @sa write_sram + */ +static inline void read_sram(u32 sram_base, u8* bytes, u32 size) +{ + if (sram_base + size > SRAM_SIZE) + return; + + for (u32 i = 0; i < size; i++) + { + bytes[i] = sram_mem[sram_base + i]; + } +} + +/** + * @brief Checks the 7 chars of gbalatro_version after the "GBALATRO_VERSION" prefix + * representing the git hash of the code the build is based on. + * + * @returns true if the git hash of the ROM is equal to the hash saved in SRAM. + * false if they are different. + */ +static inline bool check_hash(const char* prefix) +{ + for (u32 i = 0; i < CHECK_HASH_SIZE; i++) + { + if (gbalatro_version[GIT_HASH_START + i] != prefix[i]) + { + return false; + } + } + + return true; +} + +/** + * @brief Determines if the current build is considered "dirty" aka has uncommitted changes. + * This works because the gbalatro_version string has "-dirty" added at the end if it's + * dirty. + * + * @returns true if version is dirty, false otherwise. + */ +static inline bool is_version_dirty() +{ + return strlen(gbalatro_version) > GIT_HASH_START + CHECK_HASH_SIZE; +} + +/** + * @brief Writes a magic number and ROM version info to SRAM to signal that the + * save data exists and allow the game to determine if it is compatible. + */ +static inline void set_save_header() +{ + SaveHeader check = {}; + check.magic = CHECK_MAGIC; + check.dirty = is_version_dirty(); + memcpy(&(check.githash), gbalatro_version + GIT_HASH_START, CHECK_HASH_SIZE); + + write_sram(CHECK_BASE, (const u8*)&check, sizeof(check)); +} + +/** + * @brief Reads whether the save data exists and is valid. + * + * @sa set_save_valid + */ +static inline bool check_save_header() +{ + SaveHeader check; + read_sram(CHECK_BASE, (u8*)&check, sizeof(check)); + + bool is_valid = (check.magic == CHECK_MAGIC) && check.dirty == is_version_dirty() && + check_hash(check.githash); + + return is_valid; +} + +void save_game(void) +{ + set_save_header(); + write_sram(GAME_BASE, (const u8*)&g_game_vars, sizeof(g_game_vars)); +} + +void load_game(void) +{ + if (!check_save_header()) + return; + + read_sram(GAME_BASE, (u8*)&g_game_vars, sizeof(g_game_vars)); + + // return to where we were in the random sequence so that the run stays reproducible + for (u32 i = 0; i < g_game_vars.rng_step; i++) + { + (void)rand(); + } + + mmSetModuleVolume(MM_MODULE_FULL_VOLUME * g_game_vars.music_volume / VOLUME_OPTION_MAX); +} diff --git a/source/selection_grid.c b/source/selection_grid.c index 8c792a7..20542c2 100644 --- a/source/selection_grid.c +++ b/source/selection_grid.c @@ -125,6 +125,9 @@ void selection_grid_process_input(SelectionGrid* selection_grid) { // To make the next line shorter and more readable Selection* selection = &selection_grid->selection; - selection_grid->rows[selection->y].on_key_transit(selection_grid, selection); + if (selection_grid->rows[selection->y].on_key_transit != NULL) + { + selection_grid->rows[selection->y].on_key_transit(selection_grid, selection); + } } } diff --git a/source/splash_screen.c b/source/splash_screen.c index 358a05d..41fc049 100644 --- a/source/splash_screen.c +++ b/source/splash_screen.c @@ -11,7 +11,7 @@ static const Rect COUNTDOWN_TIMER_RECT = {208, 144, 240, 152}; static uint timer = 0; -void splash_screen_on_init() +void splash_screen_on_init(void) { timer = 0; @@ -25,7 +25,7 @@ void splash_screen_on_init() tte_printf("#{P:8,144; cx:0xF000}(Press any key to skip)"); } -void splash_screen_on_update() +void splash_screen_on_update(void) { timer++; @@ -49,7 +49,7 @@ void splash_screen_on_update() tte_erase_screen(); } -void splash_screen_on_exit() +void splash_screen_on_exit(void) { mmStart(MOD_MAIN_THEME, MM_PLAY_LOOP); } diff --git a/source/sprite.c b/source/sprite.c index 82a4bd5..b747a9d 100644 --- a/source/sprite.c +++ b/source/sprite.c @@ -2,6 +2,7 @@ #include "audio_utils.h" #include "game.h" +#include "game_variables.h" #include "pool.h" #include "soundbank.h" #include "util.h" @@ -209,8 +210,8 @@ void sprite_object_reset_transform(SpriteObject* sprite_object) void sprite_object_update(SpriteObject* sprite_object) { - sprite_object->vx += ((sprite_object->tx - sprite_object->x) * get_game_speed()) / 8; - sprite_object->vy += ((sprite_object->ty - sprite_object->y) * get_game_speed()) / 8; + sprite_object->vx += ((sprite_object->tx - sprite_object->x) * g_game_vars.game_speed) / 8; + sprite_object->vy += ((sprite_object->ty - sprite_object->y) * g_game_vars.game_speed) / 8; // Scale up the card when it's played sprite_object->vscale += (sprite_object->tscale - sprite_object->scale) / 8; @@ -310,7 +311,7 @@ void sprite_object_set_focus(SpriteObject* sprite_object, bool focus) play_sfx( SFX_CARD_FOCUS, - MM_BASE_PITCH_RATE + rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, + MM_BASE_PITCH_RATE + get_rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, SFX_DEFAULT_VOLUME ); sprite_object->ty = sprite_object->ty + int2fx((focus ? -1 : 1) * SPRITE_FOCUS_RAISE_PX); diff --git a/source/version.c b/source/version.c index 3a57fbb..17fc689 100644 --- a/source/version.c +++ b/source/version.c @@ -6,5 +6,5 @@ #define GIT_DIRTY "-dirty" #endif -__attribute__((section(".version"), used)) const char balatro_version[] = - "GBALATRO_VERSION:" GIT_HASH GIT_DIRTY; +__attribute__((section(".version"), used)) const char gbalatro_version[] = + "GBALATRO-VERSION:" GIT_HASH GIT_DIRTY;