Feature/game over audio (#494)

* initial additions

* Move current_blind to game vars

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* Compile but is broken

* temp fix for token location

* tmp

* Working, lets clean up

* remove todo

* Remove odd reroll blind variable

* fix skipping blind screen

* fix reroll

* remove magic for blind token locations

* clang format

* Cleanup button highlight in blind menu

* some cleanup

* Add layout file for global UI rects

* remove dead code

* Clang format

* clang format it up

* Move reset top left funct to gfx utils

* make note of soon-to-be removed vars

* Cleanup gamevars declaration

* Cleanup layout file

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Add include for stdbool.h in common_ui.h

* Cleanup rect declaration

* Start moving state machine stuff to new file

* quick tmp

* oops, add new files

* working call list

* fixed pointer stuff

* working as per norm

* working replacement

* move defines to static const

* fix compile issue

* Add some missing "void"s

* Working new substates

* add statemachine to blind select

* Fix game restart statemachine leak

* clang-format

* Clang format

* Rework shop exit logic

* Use new state machine in shop

* Remove substates entirely

* Remove debug stuff

* Clang format

* document state machine better

* update list tests

* clang-format

* Update for PR

* Claaaaang format

* another clang-format

* Remove extra state in round_end

* cleanup docs for removals in lists

* clang-format... again

* update doxygen format

* Clang format

* tmp

* Update for Pr

* clang-format

* Cleanup for only supporting losing

* bug fix, thanks logs!

* Fix dangling pointer bug

The control of removing statemachines may call issues if starting the
function is done from another state machine call. To remedy this, add a
flag to determine if a state machine is registered and return early
instead of blindly removing it to re-add.

* Fix bug with audio iterations possibly going in reverse

* Remove extra includes

* Clang-format

* Add sound effects for game win/lose

* adjust audio steps

* tmp

* Nice sounding and cleaned up

* Fix and cleanup

* clang-format

* clang-format

* clang format again

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Rickey
2026-05-27 21:57:30 -07:00
committed by GitHub
parent d2e453c256
commit 63b6584706
14 changed files with 202 additions and 24 deletions
Binary file not shown.
Binary file not shown.
+23
View File
@@ -6,6 +6,8 @@
#ifndef AUDIO_UTILS_H #ifndef AUDIO_UTILS_H
#define AUDIO_UTILS_H #define AUDIO_UTILS_H
#include "game_variables.h"
#include <mm_types.h> #include <mm_types.h>
/** /**
@@ -64,4 +66,25 @@
*/ */
void play_sfx(mm_word id, mm_word rate, mm_byte volume); 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 #endif
+13
View File
@@ -66,6 +66,11 @@ typedef struct
* @brief The current state of the state machine, the offset into state_infos * @brief The current state of the state machine, the offset into state_infos
*/ */
int state; int state;
/**
* @brief Flag to determine if statemachine is already running it's active_update function
*/
bool registered;
} StateMachine; } StateMachine;
/** /**
@@ -102,6 +107,14 @@ void noop(void);
// clang-format off // clang-format off
#define STATE_INFO_UPDATE_FN_ONLY(fn) {.on_init = noop, .on_update = fn, .on_exit = noop} #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 // clang-format on
#endif // STATE_MACHINE_H #endif // STATE_MACHINE_H
+8
View File
@@ -17,6 +17,14 @@
#define UNDEFINED -1 #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 * @def NUM_ELEM_IN_ARR
* @brief Get the number of elements in an array * @brief Get the number of elements in an array
+134 -1
View File
@@ -1,17 +1,150 @@
/**
* @file audio_utils.c
*
* @brief Audio utility functions implementation.
*/
#include "audio_utils.h" #include "audio_utils.h"
#include "game_variables.h" #include "game_variables.h"
#include "mgba_logger.h"
#include "state_machine.h"
#include "util.h"
#include <maxmod.h> #include <maxmod.h>
#include <stdlib.h>
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) 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 = { mm_sound_effect sfx = {
{id}, {id},
rate, rate,
0, 0,
(volume * g_game_vars.sound_volume) / VOLUME_OPTION_MAX, adj_volume,
SFX_DEFAULT_PAN, SFX_DEFAULT_PAN,
}; };
mmEffectEx(&sfx); 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);
}
+1 -4
View File
@@ -211,10 +211,7 @@ static StateInfo state_info[] = {
#undef DEF_STATE_INFO #undef DEF_STATE_INFO
}; };
static StateMachine game_sm = { static StateMachine game_sm = STATE_MACHINE_DEFINE(state_info, GAME_STATE_MAX);
.state_infos = &state_info[0],
.num_infos = GAME_STATE_MAX,
};
// clang-format off // clang-format off
static SelectionGridRow game_playing_selection_rows[] = { static SelectionGridRow game_playing_selection_rows[] = {
+1 -4
View File
@@ -56,10 +56,7 @@ static StateInfo state_info[] = {
STATE_INFO_UPDATE_FN_ONLY(game_blind_select_exit), STATE_INFO_UPDATE_FN_ONLY(game_blind_select_exit),
}; };
static StateMachine blind_select_sm = { static StateMachine blind_select_sm = STATE_MACHINE_DEFINE(state_info, BLIND_SELECT_MAX);
.state_infos = &state_info[0],
.num_infos = BLIND_SELECT_MAX,
};
// clang-format off // clang-format off
// Points x y // Points x y
+6 -1
View File
@@ -4,7 +4,6 @@
#include "audio_utils.h" #include "audio_utils.h"
#include "button.h" #include "button.h"
#include "game.h" #include "game.h"
#include "game/main_menu.h"
#include "graphic_utils.h" #include "graphic_utils.h"
#include "layout.h" #include "layout.h"
#include "random.h" #include "random.h"
@@ -63,11 +62,16 @@ static void game_over_init(enum EndCondition init_condition)
void game_win_on_init(void) void game_win_on_init(void)
{ {
play_sfx(SFX_GAME_WIN, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
game_over_init(END_CONDITION_WIN); game_over_init(END_CONDITION_WIN);
} }
void game_lose_on_init(void) 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); game_over_init(END_CONDITION_LOSS);
} }
@@ -127,6 +131,7 @@ void game_over_on_update(void)
void game_over_on_exit(void) void game_over_on_exit(void)
{ {
play_regular_music();
condition = END_CONDITION_NONE; condition = END_CONDITION_NONE;
game_reset(); game_reset();
rng_shuffle_seed(); rng_shuffle_seed();
+1 -1
View File
@@ -612,7 +612,7 @@ static bool music_volume_row_on_selection_changed(
} }
update_volume_slider_graphics(MUSIC_VOLUME_ROW_IDX); 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; return true;
} }
+1 -4
View File
@@ -79,10 +79,7 @@ static StateInfo state_info[] = {
STATE_INFO_UPDATE_FN_ONLY(game_round_end_dismiss_round_end_panel), STATE_INFO_UPDATE_FN_ONLY(game_round_end_dismiss_round_end_panel),
}; };
static StateMachine round_end_sm = { static StateMachine round_end_sm = STATE_MACHINE_DEFINE(state_info, ROUND_END_STATES_MAX);
.state_infos = &state_info[0],
.num_infos = ROUND_END_STATES_MAX,
};
static int calculate_interest_reward(void) static int calculate_interest_reward(void)
{ {
+1 -4
View File
@@ -81,10 +81,7 @@ static StateInfo shop_state_actions[] = {
STATE_INFO_UPDATE_FN_ONLY(game_shop_outro), STATE_INFO_UPDATE_FN_ONLY(game_shop_outro),
}; };
static StateMachine shop_sm = { static StateMachine shop_sm = STATE_MACHINE_DEFINE(shop_state_actions, GAME_SHOP_MAX);
.state_infos = &shop_state_actions[0],
.num_infos = GAME_SHOP_MAX,
};
// Shop SelectionGrid // Shop SelectionGrid
+2 -2
View File
@@ -7,6 +7,7 @@
#include "bitset.h" #include "bitset.h"
#include "card.h" #include "card.h"
#include "game.h" #include "game.h"
#include "game_variables.h"
#include "joker.h" #include "joker.h"
#include "list.h" #include "list.h"
#include "util.h" #include "util.h"
@@ -326,10 +327,9 @@ void load_options(void)
g_game_vars.music_volume = options.music_volume; g_game_vars.music_volume = options.music_volume;
g_game_vars.sound_volume = options.sound_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_high_contrast(options.cards_high_contrast);
set_cards_more_readable(options.cards_more_readable); 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) void save_game(void)
+11 -3
View File
@@ -15,9 +15,10 @@ void noop(void) {};
void state_machine_register(StateMachine* state_machine) void state_machine_register(StateMachine* state_machine)
{ {
// Always try to remove the state machine first. Only one can exist at a time if (state_machine->registered)
// So ensure that calling this function doesn't add two update functions return;
state_machine_remove(state_machine);
state_machine->registered = true;
state_machine->active_update = noop; state_machine->active_update = noop;
state_machine->state = UNDEFINED; state_machine->state = UNDEFINED;
@@ -27,6 +28,10 @@ void state_machine_register(StateMachine* state_machine)
void state_machine_remove(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); 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) 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) if (state_machine->state >= 0 && state_machine->state < state_machine->num_infos)
{ {
state_machine->state_infos[state_machine->state].on_exit(); state_machine->state_infos[state_machine->state].on_exit();