diff --git a/audio/game_lose.wav b/audio/game_lose.wav new file mode 100644 index 0000000..d323f21 Binary files /dev/null and b/audio/game_lose.wav differ diff --git a/audio/game_win.wav b/audio/game_win.wav new file mode 100644 index 0000000..724ff2e Binary files /dev/null and b/audio/game_win.wav differ diff --git a/include/audio_utils.h b/include/audio_utils.h index 367768a..7566457 100644 --- a/include/audio_utils.h +++ b/include/audio_utils.h @@ -6,6 +6,8 @@ #ifndef AUDIO_UTILS_H #define AUDIO_UTILS_H +#include "game_variables.h" + #include /** @@ -64,4 +66,25 @@ */ void play_sfx(mm_word id, mm_word rate, mm_byte volume); +/** + * @brief Play music at a low pitch and slow tempo (lose screen) + */ +void play_lose_music(void); + +/** + * @brief Play music at a normal pitch and tempo + */ +void play_regular_music(void); + +void set_volume(int volume); + +/** + * @brief Get MaxMod module audio value from VOLUME_OPTION value + * @param step VOLUME_OPTION between @ref VOLUME_OPTION_MIN and @ref VOLUME_OPTION_MAX + */ +inline int volume_module_step_to_val(unsigned char step) +{ + return MM_MODULE_FULL_VOLUME * step / VOLUME_OPTION_MAX; +} + #endif diff --git a/include/state_machine.h b/include/state_machine.h index 27d6eb8..882257f 100644 --- a/include/state_machine.h +++ b/include/state_machine.h @@ -66,6 +66,11 @@ typedef struct * @brief The current state of the state machine, the offset into state_infos */ int state; + + /** + * @brief Flag to determine if statemachine is already running it's active_update function + */ + bool registered; } StateMachine; /** @@ -102,6 +107,14 @@ void noop(void); // clang-format off #define STATE_INFO_UPDATE_FN_ONLY(fn) {.on_init = noop, .on_update = fn, .on_exit = noop} + +#define STATE_MACHINE_DEFINE(infos, num) \ +{ \ + .state_infos = &infos[0], \ + .num_infos = num, \ + .registered = false, \ +}; + // clang-format on #endif // STATE_MACHINE_H diff --git a/include/util.h b/include/util.h index ac99683..281bc42 100644 --- a/include/util.h +++ b/include/util.h @@ -17,6 +17,14 @@ #define UNDEFINED -1 +/** + * @def SIGN + * @brief Get the sign (signum) of an integer + * + * @return 1,-1,0 if the number is positive,negative, or 0, respectively. + */ +#define SIGN(x) ((x > 0) - (x < 0)) + /** * @def NUM_ELEM_IN_ARR * @brief Get the number of elements in an array diff --git a/source/audio_utils.c b/source/audio_utils.c index a57060f..1ad6144 100644 --- a/source/audio_utils.c +++ b/source/audio_utils.c @@ -1,17 +1,150 @@ +/** + * @file audio_utils.c + * + * @brief Audio utility functions implementation. + */ + #include "audio_utils.h" #include "game_variables.h" +#include "mgba_logger.h" +#include "state_machine.h" +#include "util.h" #include +#include + +typedef struct +{ + s32 target; + s32 stride; +} AudioParamReq; + +typedef struct +{ + s32 current; + AudioParamReq req; +} AudioParam; + +// clang-format off +#define AUDIO_PARAM_REQ_DEFAULT { .target = 0, .stride = 0 } +#define AUDIO_PARAM_DEFINE(init_val) { .current = init_val, .req = AUDIO_PARAM_REQ_DEFAULT } +// clang-format on + +typedef struct +{ + AudioParam pitch; + AudioParam tempo; + AudioParam volume; +} MusicPlayerState; + +static const u32 DEFAULT_PITCH = 0x400; +static const u32 DEFAULT_TEMPO = 0x400; +static const u32 DEFAULT_VOLUME = MM_MODULE_FULL_VOLUME; +static const u32 MUSIC_CHANGE_FRAMES = 75; +static const u32 VOLUME_CHANGE_FRAMES = MUSIC_CHANGE_FRAMES / 3; + +static void set_audio_param_req(AudioParam* param, s32 target, s32 steps); +static void speed_change_update(void); + +static MusicPlayerState music_player = { + .pitch = AUDIO_PARAM_DEFINE(DEFAULT_PITCH), + .tempo = AUDIO_PARAM_DEFINE(DEFAULT_TEMPO), + .volume = AUDIO_PARAM_DEFINE(DEFAULT_VOLUME), +}; + +static StateInfo state_info[] = { + STATE_INFO_UPDATE_FN_ONLY(speed_change_update), +}; + +static StateMachine song_speed_sm = STATE_MACHINE_DEFINE(state_info, 1); + +static void set_audio_param_req(AudioParam* param, s32 target, s32 steps) +{ + int offset = target - param->current; + + // if '0' set to '1' + steps |= !steps; + param->req.stride = offset / steps; + param->req.stride = !param->req.stride ? SIGN(offset) : param->req.stride; + param->req.target = target; +} + +/** + * @brief Update the @ref AudioParam for the music transition state machine for audio transitions + * + * @return true if target is reached, false otherwise + */ +static inline bool audio_param_update(AudioParam* param) +{ + if (abs(param->current - param->req.target) <= abs(param->req.stride)) + { + param->current = param->req.target; + return true; + } + param->current += param->req.stride; + return false; +} + +static void speed_change_update(void) +{ + bool tempo_reached = audio_param_update(&music_player.tempo); + bool pitch_reached = audio_param_update(&music_player.pitch); + bool volume_reached = audio_param_update(&music_player.volume); + + mmSetModuleTempo(music_player.tempo.current); + mmSetModulePitch(music_player.pitch.current); + set_volume(music_player.volume.current); + + if (tempo_reached && pitch_reached && volume_reached) + state_machine_remove(&song_speed_sm); +} void play_sfx(mm_word id, mm_word rate, mm_byte volume) { + int adj_volume = volume * g_game_vars.sound_volume / VOLUME_OPTION_MAX; mm_sound_effect sfx = { {id}, rate, 0, - (volume * g_game_vars.sound_volume) / VOLUME_OPTION_MAX, + adj_volume, SFX_DEFAULT_PAN, }; mmEffectEx(&sfx); } + +void play_lose_music(void) +{ + const u32 slow_music_speed = 0x200; + // Don't adjust the volume if already on the lowest setting, otherwise it's not audible + u32 vol = g_game_vars.music_volume; + // Don't divide the audio by half if it's one, just sounds bad + // This works too, not for negative numbers. It's neat. + // vol |= !((vol - 1) > 0); + vol = (vol == 1) ? 1 : vol / 2; + u32 target_vol = volume_module_step_to_val(vol); + + set_audio_param_req(&music_player.pitch, slow_music_speed, MUSIC_CHANGE_FRAMES); + set_audio_param_req(&music_player.tempo, slow_music_speed, MUSIC_CHANGE_FRAMES); + set_audio_param_req(&music_player.volume, target_vol, VOLUME_CHANGE_FRAMES); + + state_machine_register(&song_speed_sm); + state_machine_change_state(&song_speed_sm, 0); +} + +void play_regular_music(void) +{ + u32 target_vol = volume_module_step_to_val(g_game_vars.music_volume); + set_audio_param_req(&music_player.pitch, DEFAULT_PITCH, MUSIC_CHANGE_FRAMES); + set_audio_param_req(&music_player.tempo, DEFAULT_TEMPO, MUSIC_CHANGE_FRAMES); + set_audio_param_req(&music_player.volume, target_vol, VOLUME_CHANGE_FRAMES); + + state_machine_register(&song_speed_sm); + state_machine_change_state(&song_speed_sm, 0); +} + +void set_volume(int volume) +{ + music_player.volume.current = volume; + mmSetModuleVolume(volume); +} diff --git a/source/game.c b/source/game.c index a93da96..76eb690 100644 --- a/source/game.c +++ b/source/game.c @@ -211,10 +211,7 @@ static StateInfo state_info[] = { #undef DEF_STATE_INFO }; -static StateMachine game_sm = { - .state_infos = &state_info[0], - .num_infos = GAME_STATE_MAX, -}; +static StateMachine game_sm = STATE_MACHINE_DEFINE(state_info, GAME_STATE_MAX); // clang-format off static SelectionGridRow game_playing_selection_rows[] = { diff --git a/source/game/blind_select.c b/source/game/blind_select.c index bc99f84..388c812 100644 --- a/source/game/blind_select.c +++ b/source/game/blind_select.c @@ -56,10 +56,7 @@ static StateInfo state_info[] = { STATE_INFO_UPDATE_FN_ONLY(game_blind_select_exit), }; -static StateMachine blind_select_sm = { - .state_infos = &state_info[0], - .num_infos = BLIND_SELECT_MAX, -}; +static StateMachine blind_select_sm = STATE_MACHINE_DEFINE(state_info, BLIND_SELECT_MAX); // clang-format off // Points x y diff --git a/source/game/game_over.c b/source/game/game_over.c index 6c12d01..6ed9788 100644 --- a/source/game/game_over.c +++ b/source/game/game_over.c @@ -4,7 +4,6 @@ #include "audio_utils.h" #include "button.h" #include "game.h" -#include "game/main_menu.h" #include "graphic_utils.h" #include "layout.h" #include "random.h" @@ -63,11 +62,16 @@ static void game_over_init(enum EndCondition init_condition) void game_win_on_init(void) { + play_sfx(SFX_GAME_WIN, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME); + game_over_init(END_CONDITION_WIN); } void game_lose_on_init(void) { + play_sfx(SFX_GAME_LOSE, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME); + play_lose_music(); + game_over_init(END_CONDITION_LOSS); } @@ -127,6 +131,7 @@ void game_over_on_update(void) void game_over_on_exit(void) { + play_regular_music(); condition = END_CONDITION_NONE; game_reset(); rng_shuffle_seed(); diff --git a/source/game/options_menu.c b/source/game/options_menu.c index ee08e6a..e5baf63 100644 --- a/source/game/options_menu.c +++ b/source/game/options_menu.c @@ -612,7 +612,7 @@ static bool music_volume_row_on_selection_changed( } update_volume_slider_graphics(MUSIC_VOLUME_ROW_IDX); - mmSetModuleVolume(MM_MODULE_FULL_VOLUME * g_game_vars.music_volume / VOLUME_OPTION_MAX); + set_volume(volume_module_step_to_val(g_game_vars.music_volume)); return true; } diff --git a/source/game/round_end.c b/source/game/round_end.c index f33abc7..5d925d6 100644 --- a/source/game/round_end.c +++ b/source/game/round_end.c @@ -79,10 +79,7 @@ static StateInfo state_info[] = { STATE_INFO_UPDATE_FN_ONLY(game_round_end_dismiss_round_end_panel), }; -static StateMachine round_end_sm = { - .state_infos = &state_info[0], - .num_infos = ROUND_END_STATES_MAX, -}; +static StateMachine round_end_sm = STATE_MACHINE_DEFINE(state_info, ROUND_END_STATES_MAX); static int calculate_interest_reward(void) { diff --git a/source/game/shop.c b/source/game/shop.c index 4c09490..3d609af 100644 --- a/source/game/shop.c +++ b/source/game/shop.c @@ -81,10 +81,7 @@ static StateInfo shop_state_actions[] = { STATE_INFO_UPDATE_FN_ONLY(game_shop_outro), }; -static StateMachine shop_sm = { - .state_infos = &shop_state_actions[0], - .num_infos = GAME_SHOP_MAX, -}; +static StateMachine shop_sm = STATE_MACHINE_DEFINE(shop_state_actions, GAME_SHOP_MAX); // Shop SelectionGrid diff --git a/source/save.c b/source/save.c index 4cece11..c72fc78 100644 --- a/source/save.c +++ b/source/save.c @@ -7,6 +7,7 @@ #include "bitset.h" #include "card.h" #include "game.h" +#include "game_variables.h" #include "joker.h" #include "list.h" #include "util.h" @@ -326,10 +327,9 @@ void load_options(void) g_game_vars.music_volume = options.music_volume; g_game_vars.sound_volume = options.sound_volume; + set_volume(volume_module_step_to_val(g_game_vars.music_volume)); set_cards_high_contrast(options.cards_high_contrast); set_cards_more_readable(options.cards_more_readable); - - mmSetModuleVolume(MM_MODULE_FULL_VOLUME * g_game_vars.music_volume / VOLUME_OPTION_MAX); } void save_game(void) diff --git a/source/state_machine.c b/source/state_machine.c index 0286412..0b4f58c 100644 --- a/source/state_machine.c +++ b/source/state_machine.c @@ -15,9 +15,10 @@ void noop(void) {}; void state_machine_register(StateMachine* state_machine) { - // Always try to remove the state machine first. Only one can exist at a time - // So ensure that calling this function doesn't add two update functions - state_machine_remove(state_machine); + if (state_machine->registered) + return; + + state_machine->registered = true; state_machine->active_update = noop; state_machine->state = UNDEFINED; @@ -27,6 +28,10 @@ void state_machine_register(StateMachine* state_machine) void state_machine_remove(StateMachine* state_machine) { + if (!state_machine->registered) + return; + + state_machine->registered = false; list_remove_data(&update_cbs, &state_machine->active_update); } @@ -42,6 +47,9 @@ void state_machine_update(void) void state_machine_change_state(StateMachine* state_machine, int new_state) { + if (!state_machine->registered) + return; + if (state_machine->state >= 0 && state_machine->state < state_machine->num_infos) { state_machine->state_infos[state_machine->state].on_exit();