From e7f550ce22e24d3c38f65afd4fe371f4e9e95a49 Mon Sep 17 00:00:00 2001 From: Geralt <84459734+MathisMartin31@users.noreply.github.com> Date: Sat, 2 May 2026 23:28:52 +0200 Subject: [PATCH] [Feature] Implement options menu and reading/writing values from/to the SRAM (#449) * background * Implement Options button * Update main menu artwork * add options menu tiles * prepare options menu handling * incorporate main menu refactor * toto * add all menu interactions * do not reset selection_x in main_menu * options struct * fix rebase from ricfehr3's repo * Implement high contrast cards * Save/Load options values to SRAM * load options on game_init and validate data within the load_options function * Get changes from ricfehr3's PR * recover fix for glitched ace * clang format * clang format * clang format * Save 500B of image data by using only minimal set of tiles instead of keeping final window in source file * clang format * clang format * Implement Options button * Update main menu artwork * progress, leaving it alone for now until discussion * make game_vars an extern * Move options vars into their own struct * rollback previous commit * Implement save validation. Version check to come * implement data validity + version check for game saves * clang format * Save position in rng sequence for consistent run after load * fix rebase issues * fix rebase issue * remove unused/duplicate define * Oops, made a mistake when testing PR #424 * encode high contrast palette into an image, and load it at game_start * document save.c/h and correct typos * correct alignment * game speed and high contrast buttons working * small cleanup * sliders also work again now * clang format * Moved balatro_version extern declaration to version.h file * removed unsused variable * make showing ROM version a toggle * move ante, round and money to the GameVariables struct * Rename SaveCheckInfo struct to SaveHeader for clarity * Add SaveHeader struct explanation * clang format * Revert unnecessary tileset optimization * SFX now follows the sound volume slider * clang format * Music now follows volume slider * Changed volume slider increment from 5 to 20 to avoid having the music sound too rough under 20% volume * refactor main_menu.c to use SelectionGrid and Button structs * clang format * make a "new" save button so we don't save during the state transition * clang format * Reduced state transition flicker by only changing affine background to a different one * Fix loading game too late in game init sequence * First batch of suggestions from @MeirGavish * Remove unnecessary intermediary buffers for high contrast palette swapping * comment row warp hack for button input * Add some of Copilot's suggestions * rename balatro_version into gbalatro_version for rebranding reasons + version display erase screen fix * remove unnecessary NULL check on `on_selection_changed` method * implement small game speed arrow buttons animation * factorize some code * remove unnecessary data validation in load_game(), since we already checked the header * Implement @MeirGavish 's suggestion regarding splitting the huge `on_update` function * clang format * implement @ricfehr3 's suggestion about keeping last selected button instead of using an obscure `on_boot` boolean * Apply @ricfehr3's suggestions * fix clang-format flags --------- Co-authored-by: MathisMartin31 --- Doxyfile | 5 +- Makefile | 2 +- graphics/background_main_menu_gfx.png | Bin 13037 -> 11962 bytes graphics/background_options_menu_gfx.grit | 1 + graphics/background_options_menu_gfx.png | Bin 0 -> 2194 bytes graphics/deck_gfx.grit | 2 +- graphics/deck_gfx.png | Bin 25188 -> 5400 bytes graphics/high_contrast_deck_pal_gfx.grit | 1 + graphics/high_contrast_deck_pal_gfx.png | Bin 0 -> 568 bytes include/affine_background.h | 7 + include/audio_utils.h | 12 +- include/button.h | 2 +- include/card.h | 1 + include/def_state_info_table.h | 19 +- include/game.h | 18 +- include/game/common_ui.h | 1 + include/game/main_menu.h | 8 +- include/game/options_menu.h | 31 ++ include/game_variables.h | 33 +- include/save.h | 25 + include/timer.h | 2 +- include/version.h | 11 + scripts/get_hash.py | 2 +- source/affine_background.c | 9 +- source/audio_utils.c | 4 +- source/blind.c | 2 +- source/card.c | 15 +- source/game.c | 164 +++--- source/game/common_ui.c | 13 +- source/game/main_menu.c | 193 +++++-- source/game/options_menu.c | 624 ++++++++++++++++++++++ source/joker.c | 7 +- source/joker_effects.c | 14 +- source/save.c | 166 ++++++ source/selection_grid.c | 5 +- source/splash_screen.c | 6 +- source/sprite.c | 7 +- source/version.c | 4 +- 38 files changed, 1231 insertions(+), 185 deletions(-) create mode 100644 graphics/background_options_menu_gfx.grit create mode 100644 graphics/background_options_menu_gfx.png create mode 100644 graphics/high_contrast_deck_pal_gfx.grit create mode 100644 graphics/high_contrast_deck_pal_gfx.png create mode 100644 include/game/options_menu.h create mode 100644 include/save.h create mode 100644 include/version.h create mode 100644 source/game/options_menu.c create mode 100644 source/save.c 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 2543fa2b8c04bda618e041da641ec6f492544485..728a1aaea27dff0bcbbc6f4d6a213fc9500e62f6 100644 GIT binary patch delta 8410 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 lJsI-dM(ehLPpEdN@rQpPOse*qDveWm~a 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 0000000000000000000000000000000000000000..2cf5401b5c6567278c3022b11d806e61fa585144 GIT binary patch literal 2194 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 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 0000000000000000000000000000000000000000..7141ae47d54e9ad8e3171accc73f360f75e74fe5 GIT binary patch literal 568 zcmeAS@N?(olHy`uVBq!ia0vp^93afW3?x5a^xMh6z*v{*>>S|f?5t2wl%JNFlghxL zF|l@{wFk4Ki0k>hKZ=}U%LFz?II}zSi%DLSWW?bF#{ydjN_By^vOg=BS1v6xd&bXm*qv?C& zf_>|x6y`E3`zg(2dKvg4YU+KKbS<6knU3j(cFaWr0Yk83n$0!D~IfKP}kkpBPw|A7q0ZfBsJtfz}( z2*=FigoKo&w8Yfp^al@~JbL!<>Em<-ZUzRK7>3Fn6DJn{6*73b`njxgN@xNA@}T}9 literal 0 HcmV?d00001 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;