From d5d5a6fd6ba3a49234bb6be2710c1517dddb2800 Mon Sep 17 00:00:00 2001 From: Geralt <84459734+MathisMartin31@users.noreply.github.com> Date: Sat, 16 May 2026 03:36:19 +0200 Subject: [PATCH] Refactor/shop (#475) * initial commit, DOES NOT COMPILE * Rename joker interactions file to `joker_row` * IT'S WORKIIING (now to make it pretty lol) * fixed rebase issues * fix rebase + undo state machine changes * Document shop state functions * clang format * fix reroll cost not appearing on shop init * Move generic SpriteObject text/price printing functions to sprite.c + some minor fixes * Resolve weird defines/include order issue * clang format * Implement suggestions from @ricfehr3 * clang format * clang format * fix rebase * Implement sugegstions from @ricfehr3 * Refactored shop to use the Button struct * missed some memsets --------- Co-authored-by: MathisMartin31 --- include/blind.h | 13 +- include/def_state_info_table.h | 2 +- include/game.h | 23 +- include/game/joker_row.h | 47 ++ include/game/shop.h | 29 ++ include/layout.h | 2 + include/sprite.h | 31 ++ source/blind.c | 12 + source/game.c | 880 +++------------------------------ source/game/common_ui.c | 3 +- source/game/joker_row.c | 128 +++++ source/game/round_end.c | 1 + source/game/shop.c | 714 ++++++++++++++++++++++++++ source/graphic_utils.c | 2 +- source/sprite.c | 50 ++ 15 files changed, 1090 insertions(+), 847 deletions(-) create mode 100644 include/game/joker_row.h create mode 100644 include/game/shop.h create mode 100644 source/game/joker_row.c create mode 100644 source/game/shop.c diff --git a/include/blind.h b/include/blind.h index 1a425fc..ad639dd 100644 --- a/include/blind.h +++ b/include/blind.h @@ -1,29 +1,20 @@ #ifndef BLIND_H #define BLIND_H -#include "game.h" #include "sprite.h" // The GBA's max uint value is around 4 billion, so we're going to not add endless mode for // simplicity's sake #define MAX_ANTE 8 -#define NORMAL_BLIND_PB 1 -#define BOSS_BLIND_PB 2 - -#define BLIND_BASE_LAYER (MAX_HAND_SIZE + MAX_SELECTION_SIZE) +// Sprite IDs of the various Blind Tokens used in the game, expressed as an offset +// relative to `BLIND_BASE_LAYER` #define PLAYING_BLIND_TOKEN_LAYER 0 #define ROUND_END_BLIND_TOKEN_LAYER 1 #define SMALL_BLIND_TOKEN_LAYER 2 #define BIG_BLIND_TOKEN_LAYER 3 #define BOSS_BLIND_TOKEN_LAYER 4 -#define BLIND_SPRITE_OFFSET 16 -#define BLIND_SPRITE_COPY_SIZE (BLIND_SPRITE_OFFSET * TILE_SIZE) - -#define BLIND_TOKENS_PER_SPRITESHEET 2 -#define BLIND_TOKEN_PALETTE_SIZE 8 - // The sprites that display the blinds when in "GAME_BLIND_SELECT" state // There are only 3 blinds per Ante, so we don't need more sprites than that enum BlindTokens diff --git a/include/def_state_info_table.h b/include/def_state_info_table.h index f4628cb..44cd5e7 100644 --- a/include/def_state_info_table.h +++ b/include/def_state_info_table.h @@ -6,7 +6,7 @@ DEF_STATE_INFO(GAME_STATE_OPTIONS_MENU, game_options_menu_on_init, game_options 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, game_round_end_on_init, 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_SHOP, game_shop_on_init, 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_over_on_update, game_over_on_exit ) DEF_STATE_INFO(GAME_STATE_WIN, game_win_on_init, game_over_on_update, game_over_on_exit ) diff --git a/include/game.h b/include/game.h index 0f397da..6e428ba 100644 --- a/include/game.h +++ b/include/game.h @@ -1,7 +1,9 @@ #ifndef GAME_H #define GAME_H +#include "bitset.h" #include "game/common_ui.h" +#include "game_variables.h" #include "graphic_utils.h" #include @@ -39,6 +41,9 @@ typedef struct CardObject CardObject; typedef struct Card Card; typedef struct JokerObject JokerObject; +typedef void (*GameStateCallback)(void); +typedef void (*SubStateActionFn)(void); + // Enum value names in ../include/def_state_info_table.h enum GameState { @@ -121,17 +126,6 @@ typedef struct ContainedHandTypes } ContainedHandTypes; // clang-format on -typedef void (*GameStateCallback)(void); -typedef void (*SubStateActionFn)(void); - -typedef struct -{ - int substate; - GameStateCallback on_init; - GameStateCallback on_update; - GameStateCallback on_exit; -} StateInfo; - // Game functions void game_init(); @@ -156,8 +150,14 @@ int get_played_top(void); int get_scored_card_index(void); bool is_joker_owned(int joker_id); bool card_is_face(Card* card); +void add_joker(JokerObject* joker_object); +void remove_owned_joker(int owned_joker_idx); List* get_jokers_list(void); List* get_expired_jokers_list(void); +List* get_discarded_jokers_list(void); +List* get_shop_jokers_list(void); +Bitset* get_avail_jokers_bitset(void); +void set_shop_joker_avail(int joker_id, bool avail); ContainedHandTypes* get_contained_hands(void); enum HandType* get_hand_type(void); @@ -189,7 +189,6 @@ void game_start(void); void change_background_legacy(enum BackgroundId id); void display_round(void); -void reset_top_left_panel_bottom_row(void); void reset_background(void); void display_hands(void); diff --git a/include/game/joker_row.h b/include/game/joker_row.h new file mode 100644 index 0000000..d4e5e8e --- /dev/null +++ b/include/game/joker_row.h @@ -0,0 +1,47 @@ +/** + * @file joker_row.h + * + * @brief All the functions used specifically to interact with Jokers. + * This includes browsing, swapping, selling, etc. + */ +#ifndef GAME_JOKER_ROW_H +#define GAME_JOKER_ROW_H + +#include "selection_grid.h" + +/** + * @brief Returns the size of the Jokers' row at the top of the game screen. + * Is equal to the number of Jokers owned. + * + * @return int + */ +int jokers_sel_row_get_size(void); + +/** + * @brief Handle directional inputs on the Jokers' row at the top of the game screen. + * + * @param selection_grid pointer to the SelectionGrid the row is a part of. + * @param row_idx index of the row this function is called on. + * @param prev_selection pointer to the coordinates of the previously selected button + * in `selection_grid` + * @param new_selection pointer to the coordinates of the newly selected button in + * `selection_grid` + * @return bool + */ +bool jokers_sel_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +); + +/** + * @brief Handle button inputs on the Jokers' row at the top of the game screen. + * + * @param selection_grid pointer to the SelectionGrid the row is a part of. + * @param selection pointer to the coordinates of the currently selected button in + * `selection_grid` + */ +void jokers_sel_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection); + +#endif // GAME_JOKER_ROW_H \ No newline at end of file diff --git a/include/game/shop.h b/include/game/shop.h new file mode 100644 index 0000000..9ed5ffd --- /dev/null +++ b/include/game/shop.h @@ -0,0 +1,29 @@ +/** + * @file shop.h + * + * @brief Shop state functions. + */ +#ifndef GAME_SHOP_H +#define GAME_SHOP_H + +/** + * @brief Change to the shop background + */ +void game_shop_change_background(void); + +/** + * @brief Shop state initialization + */ +void game_shop_on_init(void); + +/** + * @brief Shop state update + */ +void game_shop_on_update(void); + +/** + * @brief Shop cleanup + */ +void game_shop_on_exit(void); + +#endif // GAME_SHOP_H \ No newline at end of file diff --git a/include/layout.h b/include/layout.h index b79f4fe..3ebe28a 100644 --- a/include/layout.h +++ b/include/layout.h @@ -13,6 +13,8 @@ static const BG_POINT CUR_BLIND_TOKEN_POS = {8, 18}; static const BG_POINT TOP_LEFT_PANEL_POINT = {0, 0}; static const BG_POINT ROUND_END_REWARDS_ELLIPSIS_POS = {10, 13}; +static const BG_POINT JOKER_DISCARD_TARGET = {240, 30}; +static const BG_POINT HELD_JOKERS_POS = {108, 10}; // Rects left top right bottom static const Rect TOP_LEFT_PANEL_ANIM_RECT = {0, 0, 8, 4}; static const Rect POP_MENU_ANIM_RECT = {9, 7, 24, 31}; diff --git a/include/sprite.h b/include/sprite.h index ca10438..ba4dee4 100644 --- a/include/sprite.h +++ b/include/sprite.h @@ -317,6 +317,37 @@ bool sprite_object_get_width(SpriteObject* sprite_object, int* width); */ bool sprite_object_is_focused(SpriteObject* sprite_object); +/** + * @brief Print the given string directly beneath a SpriteObject. + * This is used only for Cards for now. + * + * @param sprite_object valid pointer to SpriteObject to check + * @param text the string to be printed below the sprite + */ +void sprite_object_print_text_under(SpriteObject* sprite_object, const char text[]); + +/** + * @brief Print the price string directly beneath a SpriteObject. + * More specialized version of sprite_object_print_text_under, + * automatically formats the price to `$%d`. + * + * @param sprite_object valid pointer to SpriteObject to check + * @param price the price of the card to be printed + * + * @sa sprite_object_print_text_under + */ +void sprite_object_print_price_under(SpriteObject* sprite_object, int price); + +/** + * @brief Erase the text within the Rect directly beneath a SpriteObject. + * This is used only for Cards for now. + * + * @param sprite_object valid pointer to SpriteObject to check + * + * @sa sprite_object_print_text_under + */ +void sprite_object_erase_text_under(SpriteObject* sprite_object); + /** * @brief Set sprite position. Inlined for efficiency * diff --git a/source/blind.c b/source/blind.c index 1155a78..49d1b47 100644 --- a/source/blind.c +++ b/source/blind.c @@ -1,6 +1,7 @@ #include "blind.h" #include "blind_gfx.h" +#include "game.h" #include "graphic_utils.h" #include "list.h" #include "stdbool.h" @@ -9,6 +10,17 @@ #include #include +#define NORMAL_BLIND_PB 1 +#define BOSS_BLIND_PB 2 + +#define BLIND_BASE_LAYER (MAX_HAND_SIZE + MAX_SELECTION_SIZE) + +#define BLIND_SPRITE_OFFSET 16 +#define BLIND_SPRITE_COPY_SIZE (BLIND_SPRITE_OFFSET * TILE_SIZE) + +#define BLIND_TOKENS_PER_SPRITESHEET 2 +#define BLIND_TOKEN_PALETTE_SIZE 8 + static const unsigned int* blind_gfxTiles[] = { #define DEF_BLIND_GFX(idx) blind_gfx##idx##Tiles, #include "../include/def_blind_gfx_table.h" diff --git a/source/game.c b/source/game.c index c8036c8..6fb8286 100644 --- a/source/game.c +++ b/source/game.c @@ -5,17 +5,16 @@ #include "audio_utils.h" #include "background_gfx.h" #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/blind_select.h" #include "game/common_ui.h" #include "game/game_over.h" +#include "game/joker_row.h" #include "game/main_menu.h" #include "game/options_menu.h" #include "game/round_end.h" +#include "game/shop.h" #include "game_variables.h" #include "graphic_utils.h" #include "hand_analysis.h" @@ -39,16 +38,10 @@ #define STRAIGHT_AND_FLUSH_SIZE_DEFAULT 5 // Pixel sizes -#define ITEM_SHOP_Y 71 #define SCORED_CARD_TEXT_Y 48 // SE sizes -#define SHOP_LIGHTS_1_CLR 0xFFFF -#define SHOP_LIGHTS_2_CLR 0x32BE -#define SHOP_LIGHTS_3_CLR 0x4B5F -#define SHOP_LIGHTS_4_CLR 0x5F9F - #define PITCH_STEP_DISCARD_SFX (-64) #define PITCH_STEP_DRAW_SFX 24 #define PITCH_STEP_UNDISCARD_SFX 2 * PITCH_STEP_DRAW_SFX @@ -62,38 +55,21 @@ #define CARD_UNFOCUSED_SEL_Y 15 #define CARD_FOCUSED_SEL_Y 20 -// Timer defs -#define TM_CREATE_SHOP_ITEMS_WAIT 1 -#define TM_SHIFT_SHOP_ICON_WAIT 7 -#define TM_END_GAME_SHOP_INTRO 12 -#define TM_SHOP_PRC_INPUT_START 1 - // TODO: Rename "PID" to "PAL_IDX" // Palette IDs -#define BOSS_BLIND_PRIMARY_PID 1 -#define REROLL_BTN_PID 3 -#define NEXT_ROUND_BTN_SELECTED_BORDER_PID 5 -#define BLIND_BG_SHADOW_PID 5 -#define SHOP_PANEL_SHADOW_PID 6 -#define BOSS_BLIND_SHADOW_PID 7 -#define REROLL_BTN_SELECTED_BORDER_PID 7 -#define SHOP_LIGHTS_1_PID 8 -#define SHOP_LIGHTS_2_PID 14 -#define NEXT_ROUND_BTN_PID 16 -#define SHOP_LIGHTS_3_PID 17 -#define BLIND_BG_SECONDARY_PID 18 -#define BLIND_BG_PRIMARY_PID 19 -#define SHOP_LIGHTS_4_PID 22 -#define SHOP_BOTTOM_PANEL_BORDER_PID 26 +#define BLIND_BG_SHADOW_PAL_IDX 5 +#define BLIND_BG_SECONDARY_PAL_IDX 18 +#define BLIND_BG_PRIMARY_PAL_IDX 19 +#define REWARD_PANEL_BORDER_PAL_IDX 19 -#define PLAY_HAND_BTN_PID 6 -#define PLAY_HAND_BTN_BORDER_PID 7 -#define DISCARD_BTN_PID 13 -#define DISCARD_BTN_BORDER_PID 8 -#define SORT_BTNS_PID 9 -#define SORT_BY_RANK_BTN_BORDER_PID 22 -#define SORT_BY_SUIT_BTN_BORDER_PID 23 +#define PLAY_HAND_BTN_PAL_IDX 6 +#define PLAY_HAND_BTN_BORDER_PAL_IDX 7 +#define DISCARD_BTN_PAL_IDX 13 +#define DISCARD_BTN_BORDER_PAL_IDX 8 +#define SORT_BTNS_PAL_IDX 9 +#define SORT_BY_RANK_BTN_BORDER_PAL_IDX 22 +#define SORT_BY_SUIT_BTN_BORDER_PAL_IDX 23 // Naming the stage where cards return from the discard pile to the deck "undiscard" @@ -103,28 +79,12 @@ #define NUM_SCORE_LERP_STEPS 16 #define TM_SCORE_LERP_INTERVAL 2 -// Shop -#define REROLL_BASE_COST 5 // Base cost for rerolling the shop items - -#define NEXT_ROUND_BTN_SEL_X 0 - #define GAME_PLAYING_HAND_SEL_Y 1 #define GAME_PLAYING_BUTTONS_SEL_Y 2 #define GAME_PLAYING_NUM_BOTTOM_BTNS 2 -#define REROLL_BTN_FRAME_PAL_IDX 7 -#define REROLL_BTN_PAL_IDX 3 - #define EXPIRE_ANIMATION_FRAME_COUNT 3 -enum GameShopStates -{ - GAME_SHOP_INTRO, - GAME_SHOP_ACTIVE, - GAME_SHOP_EXIT, - GAME_SHOP_MAX -}; - typedef struct { u32 chips; @@ -149,11 +109,6 @@ static void noop(void) // reviewer(s). static void game_round_on_init(void); static void game_playing_on_update(void); -static void game_shop_on_update(void); -static void game_shop_on_exit(void); -static void game_shop_intro(void); -static void game_shop_process_user_input(void); -static void game_shop_outro(void); static void sort_cards(void); static void display_temp_score(u32 value); @@ -197,37 +152,6 @@ static bool game_playing_hand_row_on_selection_changed( static int game_playing_hand_row_get_size(void); -static void shop_reroll_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection); -static bool shop_reroll_row_on_selection_changed( - SelectionGrid* selection_grid, - int row_idx, - const Selection* prev_selection, - const Selection* new_selection -); -static int shop_reroll_row_get_size(void); -static bool shop_top_row_on_selection_changed( - SelectionGrid* selection_grid, - int row_idx, - const Selection* prev_selection, - const Selection* new_selection -); -static void shop_top_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection); -static int shop_top_row_get_size(void); -static void jokers_sel_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection); -static bool jokers_sel_row_on_selection_changed( - SelectionGrid* selection_grid, - int row_idx, - const Selection* prev_selection, - const Selection* new_selection -); -static int jokers_sel_row_get_size(void); -static void game_shop_create_items(void); - -static void erase_price_under_sprite_object(SpriteObject* sprite_object); -static void print_price_under_sprite_object(SpriteObject* sprite_object, int price); - -static void remove_owned_joker(int owned_joker_idx); - static int hand_sel_idx_to_card_idx(int selection_index); static void hand_select_card(int index); static void hand_toggle_sort(void); @@ -282,7 +206,6 @@ static const Rect HELD_CARDS_SCORES_RECT = {72, 108, 240, 116 }; static const Rect MONEY_TEXT_RECT = {8, 120, 64, 128 }; static const Rect CHIPS_TEXT_RECT = {8, 80, 32, 88 }; static const Rect MULT_TEXT_RECT = {40, 80, 64, 88 }; -static const Rect SHOP_PRICES_TEXT_RECT = {72, 56, 192, 160 }; // Rects with UNDEFINED are only used in tte_printf, they need to be fully defined // to be used with tte_erase_rect_wrapper() @@ -291,10 +214,7 @@ static const Rect DISCARDS_TEXT_RECT = {48, 104, UNDEFINED, UNDEF static const Rect DECK_SIZE_RECT = {200, 152, 240, 160 }; static const Rect ROUND_TEXT_RECT = {48, 144, UNDEFINED, UNDEFINED }; static const Rect ANTE_TEXT_RECT = {8, 144, UNDEFINED, UNDEFINED }; -static const Rect SHOP_REROLL_RECT = {88, 96, UNDEFINED, UNDEFINED }; -static const BG_POINT HELD_JOKERS_POS = {108, 10}; -static const BG_POINT JOKER_DISCARD_TARGET = {240, 30}; static const BG_POINT CARD_DRAW_POS = {208, 110}; static const BG_POINT CARD_DISCARD_PNT = {240, 70}; static const BG_POINT HAND_START_POS = {120, 90}; @@ -305,13 +225,6 @@ static const BG_POINT HAND_PLAY_POS = {120, 70}; // variable and handling in common.c once the related refactor is finished static enum BackgroundId background_legacy = BG_NONE; -static StateInfo state_info[] = { -#define DEF_STATE_INFO(stateEnum, init_fn, update_fn, exit_fn) \ - {.on_init = init_fn, .on_update = update_fn, .on_exit = exit_fn, .substate = 0}, -#include "../include/def_state_info_table.h" -#undef DEF_STATE_INFO -}; - // clang-format off SelectionGridRow game_playing_selection_rows[] = { { @@ -348,24 +261,10 @@ SelectionGrid game_playing_selection_grid = { // Array of buttons by horizontal selection index (x) Button game_playing_buttons[] = { - {PLAY_HAND_BTN_BORDER_PID, PLAY_HAND_BTN_PID, game_playing_play_hand_on_pressed, can_play_hand }, - {SORT_BY_RANK_BTN_BORDER_PID, SORT_BTNS_PID, game_playing_sort_by_rank_on_pressed, NULL }, - {SORT_BY_SUIT_BTN_BORDER_PID, SORT_BTNS_PID, game_playing_sort_by_suit_on_pressed, NULL }, - {DISCARD_BTN_BORDER_PID, DISCARD_BTN_PID, game_playing_discard_on_pressed, can_discard_hand}, -}; - -SelectionGridRow shop_selection_rows[] = { - {0, jokers_sel_row_get_size, jokers_sel_row_on_selection_changed, jokers_sel_row_on_key_transit, {.wrap = false, .has_h_exit_idx = false, .h_exit_idx = 0}}, - {1, shop_top_row_get_size, shop_top_row_on_selection_changed, shop_top_row_on_key_transit, {.wrap = false, .has_h_exit_idx = false, .h_exit_idx = 0}}, - {2, shop_reroll_row_get_size, shop_reroll_row_on_selection_changed, shop_reroll_row_on_key_transit, {.wrap = false, .has_h_exit_idx = true, .h_exit_idx = 1} }, -}; - -static const Selection SHOP_INIT_SEL = {-1, 1}; - -SelectionGrid shop_selection_grid = { - shop_selection_rows, - NUM_ELEM_IN_ARR(shop_selection_rows), - SHOP_INIT_SEL + {PLAY_HAND_BTN_BORDER_PAL_IDX, PLAY_HAND_BTN_PAL_IDX, game_playing_play_hand_on_pressed, can_play_hand }, + {SORT_BY_RANK_BTN_BORDER_PAL_IDX, SORT_BTNS_PAL_IDX, game_playing_sort_by_rank_on_pressed, NULL }, + {SORT_BY_SUIT_BTN_BORDER_PAL_IDX, SORT_BTNS_PAL_IDX, game_playing_sort_by_suit_on_pressed, NULL }, + {DISCARD_BTN_BORDER_PAL_IDX, DISCARD_BTN_PAL_IDX, game_playing_discard_on_pressed, can_discard_hand}, }; // This is a stupid way to do this but I don't care @@ -389,14 +288,6 @@ static const HandValues hand_base_values[] = { {.chips = 160, .mult = 16, .display_name = "Flush 5"} // FLUSH_FIVE }; -static const SubStateActionFn shop_state_actions[] = { - game_shop_intro, - game_shop_process_user_input, - game_shop_outro -}; - -static int reroll_cost = REROLL_BASE_COST; - // The current game state, this is used to determine what the game is doing at any given time static enum GameState game_state = GAME_STATE_UNDEFINED; static enum HandState hand_state = HAND_DRAW; @@ -405,7 +296,7 @@ static enum PlayState play_state = PLAY_STARTING; static enum HandType hand_type = NONE; static ContainedHandTypes _contained_hands = {0}; -// Initialization of the global var +// Initialization of the global vars // clang-format off GameVariables g_game_vars = { .timer = 0, .rng_seed = 0, .rng_step = 0, @@ -435,6 +326,21 @@ GameVariables g_game_vars = { }; // clang-format on +typedef struct +{ + int substate; + GameStateCallback on_init; + GameStateCallback on_update; + GameStateCallback on_exit; +} StateInfo; + +StateInfo state_info[] = { +#define DEF_STATE_INFO(stateEnum, init_fn, update_fn, exit_fn) \ + {.on_init = init_fn, .on_update = update_fn, .on_exit = exit_fn, .substate = 0}, +#include "../include/def_state_info_table.h" +#undef DEF_STATE_INFO +}; + // The sprite that displays the blind when in "GAME_PLAYING/GAME_ROUND_END" state // The sprite that displays the blind when in "GAME_ROUND_END" state @@ -500,16 +406,6 @@ static inline bool is_shop_joker_avail(int joker_id) return bitset_get_idx(&_avail_jokers_bitset, joker_id); } -static inline void set_shop_joker_avail(int joker_id, bool avail) -{ - bitset_set_idx(&_avail_jokers_bitset, joker_id, avail); -} - -static inline int get_num_shop_jokers_avail(void) -{ - return bitset_num_set_bits(&_avail_jokers_bitset); -} - static inline void reset_shop_jokers(void) { int num_jokers = get_joker_registry_size(); @@ -520,11 +416,6 @@ static inline void reset_shop_jokers(void) } } -static inline bool no_avail_jokers(void) -{ - return bitset_is_empty(&_avail_jokers_bitset); -} - static inline void played_push(CardObject* card_object) { if (played_top >= MAX_SELECTION_SIZE - 1) @@ -824,6 +715,26 @@ List* get_expired_jokers_list(void) return &_expired_jokers_list; } +List* get_discarded_jokers_list(void) +{ + return &_discarded_jokers_list; +} + +List* get_shop_jokers_list(void) +{ + return &_shop_jokers_list; +} + +Bitset* get_avail_jokers_bitset(void) +{ + return &_avail_jokers_bitset; +} + +void set_shop_joker_avail(int joker_id, bool avail) +{ + bitset_set_idx(&_avail_jokers_bitset, joker_id, avail); +} + bool is_shortcut_joker_active(void) { return shortcut_joker_count > 0; @@ -835,7 +746,7 @@ int get_straight_and_flush_size(void) : STRAIGHT_AND_FLUSH_SIZE_DEFAULT; } -static void add_joker(JokerObject* joker_object) +void add_joker(JokerObject* joker_object) { list_push_back(&_owned_jokers_list, joker_object); @@ -853,7 +764,7 @@ static void add_joker(JokerObject* joker_object) } } -static void remove_owned_joker(int owned_joker_idx) +void remove_owned_joker(int owned_joker_idx) { // TODO: Extract to on_joker_removed() callback JokerObject* joker_object = list_get_at_idx(&_owned_jokers_list, owned_joker_idx); @@ -1315,17 +1226,17 @@ void change_background_legacy(enum BackgroundId id) // This would change the palette of the background to match the blind, but the backgroun // doesn't use the blind token's exact colors so a different approach is required memset16( - &pal_bg_mem[BLIND_BG_PRIMARY_PID], + &pal_bg_mem[BLIND_BG_PRIMARY_PAL_IDX], blind_get_color(g_game_vars.current_blind, BLIND_BACKGROUND_MAIN_COLOR_INDEX), 1 ); memset16( - &pal_bg_mem[BLIND_BG_SECONDARY_PID], + &pal_bg_mem[BLIND_BG_SECONDARY_PAL_IDX], blind_get_color(g_game_vars.current_blind, BLIND_BACKGROUND_SECONDARY_COLOR_INDEX), 1 ); memset16( - &pal_bg_mem[BLIND_BG_SHADOW_PID], + &pal_bg_mem[BLIND_BG_SHADOW_PAL_IDX], blind_get_color(g_game_vars.current_blind, BLIND_BACKGROUND_SHADOW_COLOR_INDEX), 1 ); @@ -1354,34 +1265,7 @@ void change_background_legacy(enum BackgroundId id) tte_erase_rect_wrapper(HAND_SIZE_RECT_SELECT); } - else if (id == BG_SHOP) - { - toggle_windows(false, true); - - GRIT_CPY(pal_bg_mem, background_shop_gfxPal); - GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_shop_gfxTiles); - GRIT_CPY(&se_mem[MAIN_BG_SBB], background_shop_gfxMap); - - // Set the outline colors for the shop background. This is used for the alternate shop - // palettes when opening packs - memset16(&pal_bg_mem[SHOP_BOTTOM_PANEL_BORDER_PID], 0x213D, 1); - memset16(&pal_bg_mem[SHOP_PANEL_SHADOW_PID], 0x10B4, 1); - - // Reset the shop lights to correct colors - memset16(&pal_bg_mem[SHOP_LIGHTS_2_PID], SHOP_LIGHTS_2_CLR, 1); - memset16(&pal_bg_mem[SHOP_LIGHTS_3_PID], SHOP_LIGHTS_3_CLR, 1); - memset16(&pal_bg_mem[SHOP_LIGHTS_4_PID], SHOP_LIGHTS_4_CLR, 1); - memset16(&pal_bg_mem[SHOP_LIGHTS_1_PID], SHOP_LIGHTS_1_CLR, 1); - - // Disable the button highlight colors - memcpy16(&pal_bg_mem[REROLL_BTN_SELECTED_BORDER_PID], &pal_bg_mem[REROLL_BTN_PID], 1); - memcpy16( - &pal_bg_mem[NEXT_ROUND_BTN_SELECTED_BORDER_PID], - &pal_bg_mem[NEXT_ROUND_BTN_PID], - 1 - ); - } - else if (id == BG_BLIND_SELECT || id == BG_MAIN_MENU || id == BG_ROUND_END) + else if (id == BG_MAIN_MENU || id == BG_BLIND_SELECT || id == BG_SHOP || id == BG_ROUND_END) { // do nothing, just don't return early! } @@ -3129,652 +3013,6 @@ static void game_playing_on_update(void) game_playing_process_flaming_score(); } -static Rect get_text_rect_under_sprite_object(SpriteObject* sprite_object) -{ - int height = 0; - int width = 0; - - if (sprite_object_get_dimensions(sprite_object, &width, &height) == false) - { - // fallback - height = CARD_SPRITE_SIZE; - width = CARD_SPRITE_SIZE; - } - - Rect ret_rect = {0}; - - ret_rect.left = fx2int(sprite_object->tx); - ret_rect.top = fx2int(sprite_object->ty) + height + TILE_SIZE; - ret_rect.right = ret_rect.left + width; - ret_rect.bottom = ret_rect.top + TTE_CHAR_SIZE; - - return ret_rect; -} - -static void print_price_under_sprite_object(SpriteObject* sprite_object, int price) -{ - Rect price_rect = get_text_rect_under_sprite_object(sprite_object); - - char price_str_buff[INT_MAX_DIGITS + 2]; // + 2 for null-terminator and "$" - - snprintf(price_str_buff, sizeof(price_str_buff), "$%d", price); - - update_text_rect_to_center_str(&price_rect, price_str_buff, SCREEN_LEFT); - - tte_printf("#{P:%d,%d; cx:0x%X000}$%d", price_rect.left, price_rect.top, TTE_YELLOW_PB, price); -} - -static void erase_price_under_sprite_object(SpriteObject* sprite_object) -{ - Rect price_rect = get_text_rect_under_sprite_object(sprite_object); - - // Add SPRITE_FOCUS_RAISE_PX to cover the focused case - price_rect.bottom = price_rect.bottom + SPRITE_FOCUS_RAISE_PX; - - tte_erase_rect_wrapper(price_rect); -} - -static inline int game_shop_get_rand_available_joker_id(void) -{ - // Roll for what rarity the joker will be - int joker_rarity = joker_get_random_rarity(); - - // Now determine how many jokers are available based on the rarity - int jokers_avail_size = get_num_shop_jokers_avail(); - - if (jokers_avail_size == 0) - return UNDEFINED; - - int matching_joker_ids[jokers_avail_size]; - int fallback_random_idx = get_rand() % jokers_avail_size; - int fallback_random_joker_id = UNDEFINED; - int match_count = 0; - - BitsetItr itr = bitset_itr_create(&_avail_jokers_bitset); - - int i = 0; - int joker_id = UNDEFINED; - while ((joker_id = bitset_itr_next(&itr)) != UNDEFINED) - { - if (i++ == fallback_random_idx) - fallback_random_joker_id = joker_id; - const JokerInfo* info = get_joker_registry_entry(joker_id); - if (info->rarity == joker_rarity) - { - matching_joker_ids[match_count++] = joker_id; - } - } - - int selected_joker_id = - (match_count > 0) ? matching_joker_ids[get_rand() % match_count] : fallback_random_joker_id; - - return selected_joker_id; -} - -static void game_shop_create_items(void) -{ - tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT); - - if (no_avail_jokers()) - return; - - list_clear(&_shop_jokers_list); - _shop_jokers_list = list_create(); - - for (int i = 0; i < MAX_SHOP_JOKERS; i++) - { - int joker_id = 0; -#ifdef TEST_JOKER_ID0 // Allow defining an ID for a joker to always appear in shop and be tested - if (is_shop_joker_avail(TEST_JOKER_ID0)) - { - joker_id = TEST_JOKER_ID0; - } - else -#endif -#ifdef TEST_JOKER_ID1 - if (is_shop_joker_avail(TEST_JOKER_ID1)) - { - joker_id = TEST_JOKER_ID1; - } - else -#endif - { - joker_id = game_shop_get_rand_available_joker_id(); - } - - // If for some reason only no joker is left, don't make another - if (joker_id == UNDEFINED) - break; - - set_shop_joker_avail(joker_id, false); - - JokerObject* joker_object = joker_object_new(joker_new(joker_id)); - - joker_object->sprite_object->x = int2fx(120 + i * CARD_SPRITE_SIZE); - joker_object->sprite_object->y = int2fx(160); - joker_object->sprite_object->tx = joker_object->sprite_object->x; - joker_object->sprite_object->ty = int2fx(ITEM_SHOP_Y); - - print_price_under_sprite_object(joker_object->sprite_object, joker_object->joker->value); - - sprite_position( - joker_object_get_sprite(joker_object), - fx2int(joker_object->sprite_object->x), - fx2int(joker_object->sprite_object->y) - ); - - list_push_back(&_shop_jokers_list, joker_object); - } -} - -// Intro sequence (menu and shop icon coming into frame) -static void game_shop_intro() -{ - main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP); - - if (g_game_vars.timer == TM_CREATE_SHOP_ITEMS_WAIT) - { - game_shop_create_items(); - } - - if (g_game_vars.timer >= TM_SHIFT_SHOP_ICON_WAIT) // Shift the shop icon - { - int timer_offset = g_game_vars.timer - 6; - - // TODO: Extract to generic function? - for (int y = 0; y < timer_offset; y++) - { - int y_from = 26 + y - timer_offset; - int y_to = 0 + y; - - Rect from = {0, y_from, 8, y_from}; - BG_POINT to = {0, y_to}; - - main_bg_se_copy_rect(from, to); - } - } - - if (g_game_vars.timer == TM_END_GAME_SHOP_INTRO) - { - state_info[game_state].substate = GAME_SHOP_ACTIVE; - g_game_vars.timer = TM_ZERO; // Reset the timer - } -} - -static int jokers_sel_row_get_size(void) -{ - return list_get_len(&_owned_jokers_list); -} - -static bool jokers_sel_row_on_selection_changed( - SelectionGrid* selection_grid, - int row_idx, - const Selection* prev_selection, - const Selection* new_selection -) -{ - // swap Jokers if the A button is held down and all Jokers are on the same row - bool swapping = - key_is_down(SELECT_CARD) && new_selection->y == row_idx && prev_selection->y == row_idx; - - if (prev_selection->y == row_idx) - { - JokerObject* joker_object = - (JokerObject*)list_get_at_idx(&_owned_jokers_list, prev_selection->x); - // Don't change focus from current Joker if swapping - if (joker_object != NULL && !swapping) - { - erase_price_under_sprite_object(joker_object->sprite_object); - sprite_object_set_focus(joker_object->sprite_object, false); - } - } - - if (new_selection->y == row_idx) - { - JokerObject* joker_object = - (JokerObject*)list_get_at_idx(&_owned_jokers_list, new_selection->x); - if (joker_object != NULL) - { - if (!swapping) - { - sprite_object_set_focus(joker_object->sprite_object, true); - } - // If we land on this row while the A button is being held, we are in swapping mode - // This means that we need to hide the price, whether we were already - // on this row or if we come from another - if (!key_is_down(SELECT_CARD)) - { - print_price_under_sprite_object( - joker_object->sprite_object, - joker_get_sell_value(joker_object->joker) - ); - } - } - } - - if (swapping) - { - list_swap( - &_owned_jokers_list, - (unsigned int)prev_selection->x, - (unsigned int)new_selection->x - ); - } - - return true; -} - -static inline void joker_start_discard_animation(JokerObject* joker_object) -{ - joker_object->sprite_object->tx = int2fx(JOKER_DISCARD_TARGET.x); - joker_object->sprite_object->ty = int2fx(JOKER_DISCARD_TARGET.y); - list_push_back(&_discarded_jokers_list, joker_object); -} - -static inline void game_sell_joker(int joker_idx) -{ - if (joker_idx < 0 || joker_idx >= list_get_len(&_owned_jokers_list)) - return; - - JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_owned_jokers_list, joker_idx); - g_game_vars.money += joker_get_sell_value(joker_object->joker); - display_money(); - erase_price_under_sprite_object(joker_object->sprite_object); - - remove_owned_joker(joker_idx); - - joker_start_discard_animation(joker_object); -} - -static void jokers_sel_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection) -{ - JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_owned_jokers_list, selection->x); - if (joker_object != NULL) - { - if (key_hit(SELECT_CARD)) - { - erase_price_under_sprite_object(joker_object->sprite_object); - } - else if (key_released(SELECT_CARD)) - { - print_price_under_sprite_object( - joker_object->sprite_object, - joker_get_sell_value(joker_object->joker) - ); - } - } - - if (key_hit(SELL_KEY)) - { - int sold_joker_idx = selection->x; - - // Move the selection away from the jokers so it doesn't point to an invalid place - // Do this before selling the joker so valid row sizes are used - selection_grid_move_selection_vert(selection_grid, SCREEN_DOWN); - - game_sell_joker(sold_joker_idx); - } -} - -// Shop input -static int shop_top_row_get_size(void) -{ - // + 1 to account for next round button - return list_get_len(&_shop_jokers_list) + 1; -} - -static inline void add_to_held_jokers(JokerObject* joker_object) -{ - joker_object->sprite_object->ty = int2fx(HELD_JOKERS_POS.y); - add_joker(joker_object); -} - -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); - - 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); - list_remove_at_idx(&_shop_jokers_list, shop_joker_idx); // Remove the joker from the shop -} - -static void shop_top_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection) -{ - if (!key_hit(SELECT_CARD)) - return; - - if (selection->x == NEXT_ROUND_BTN_SEL_X) - { - play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME); - - // Go to next blind selection game state - state_info[game_state].substate = GAME_SHOP_EXIT; // Go to the outro sequence state - g_game_vars.timer = TM_ZERO; // Reset the timer - reroll_cost = REROLL_BASE_COST; - - memcpy16( - &pal_bg_mem[NEXT_ROUND_BTN_SELECTED_BORDER_PID], - &pal_bg_mem[SHOP_PANEL_SHADOW_PID], - 1 - ); - - // memcpy16(&pal_bg_mem[16], &pal_bg_mem[6], 1); - // This changes the color of the button to a dark red. - // However, it shares a palette with the shop icon, so it will change the color of the shop - // icon as well. And I don't care enough to fix it right now. - } - else - { - int shop_joker_idx = selection->x - 1; // - 1 to account for next round button - JokerObject* joker_object = - (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 || - g_game_vars.money < joker_object->joker->value) - { - return; - } - - game_shop_buy_joker(shop_joker_idx); - - // In Balatro the selection actually stays on the purchased joker it's easier to just move - // it left - selection_grid_move_selection_horz(selection_grid, -1); - } -} - -static bool shop_top_row_on_selection_changed( - SelectionGrid* selection_grid, - int row_idx, - const Selection* prev_selection, - const Selection* new_selection -) -{ - // Guard if we move down while on jokers - if (new_selection->y > row_idx && prev_selection->x > 0) - return false; - - // The selection grid system only guarantees that the new selection is within bounds - // but not the previous one... - // This allows using INIT_SEL = {-1, 1} and move to set the initial selection in a hacky way... - if (prev_selection->y == row_idx && prev_selection->x >= 0 && - prev_selection->x < shop_top_row_get_size()) - { - if (prev_selection->x == NEXT_ROUND_BTN_SEL_X) - { - // Remove next round button highlight - memcpy16( - &pal_bg_mem[NEXT_ROUND_BTN_SELECTED_BORDER_PID], - &pal_bg_mem[NEXT_ROUND_BTN_PID], - 1 - ); - } - else - { - int idx = prev_selection->x - 1; // -1 to account for next round button - JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_shop_jokers_list, idx); - sprite_object_set_focus(joker_object->sprite_object, false); - } - } - - if (new_selection->y == row_idx) - { - if (new_selection->x == NEXT_ROUND_BTN_SEL_X) - { - // Highlight next round button - memset16(&pal_bg_mem[NEXT_ROUND_BTN_SELECTED_BORDER_PID], BTN_HIGHLIGHT_COLOR, 1); - } - else - { - int idx = new_selection->x - 1; // -1 to account for next round button - JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_shop_jokers_list, idx); - sprite_object_set_focus(joker_object->sprite_object, true); - } - } - - return true; -} - -static int shop_reroll_row_get_size() -{ - return 1; // Only the reroll button -} - -static bool shop_reroll_row_on_selection_changed( - SelectionGrid* selection_grid, - int row_idx, - const Selection* prev_selection, - const Selection* new_selection -) -{ - if (row_idx == prev_selection->y) - { - // Remove highlight - memcpy16(&pal_bg_mem[REROLL_BTN_SELECTED_BORDER_PID], &pal_bg_mem[REROLL_BTN_PID], 1); - - if (new_selection->x != NEXT_ROUND_BTN_SEL_X) - { - int idx = new_selection->x - 1; - JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_shop_jokers_list, idx); - sprite_object_set_focus(joker_object->sprite_object, true); - } - } - else if (row_idx == new_selection->y) - { - memset16(&pal_bg_mem[REROLL_BTN_SELECTED_BORDER_PID], BTN_HIGHLIGHT_COLOR, 1); - } - - return true; -} - -static inline void game_shop_reroll(int* reroll_cost) -{ - g_game_vars.money -= *reroll_cost; - display_money(); // Update the money display - - ListItr itr = list_itr_create(&_shop_jokers_list); - JokerObject* joker_object; - - while ((joker_object = list_itr_next(&itr))) - { - if (joker_object != NULL) - { - set_shop_joker_avail(joker_object->joker->id, true); - joker_object_destroy(&joker_object); // Destroy the joker object if it exists - } - } - - list_clear(&_shop_jokers_list); - _shop_jokers_list = list_create(); - - game_shop_create_items(); - - itr = list_itr_create(&_shop_jokers_list); - - while ((joker_object = list_itr_next(&itr))) - { - if (joker_object != NULL) - { - // Set the y position to the target position - joker_object->sprite_object->y = joker_object->sprite_object->ty; - - // Give the joker a little wiggle animation - joker_object_shake(joker_object, UNDEFINED); - } - } - - (*reroll_cost)++; - tte_printf( - "#{P:%d,%d; cx:0x%X000}$%d", - SHOP_REROLL_RECT.left, - SHOP_REROLL_RECT.top, - TTE_WHITE_PB, - *reroll_cost - ); -} - -static void shop_reroll_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection) -{ - if (!key_hit(SELECT_CARD)) - { - return; - } - - if (g_game_vars.money >= reroll_cost) - { - // TODO: Add money sound effect - play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME); - game_shop_reroll(&reroll_cost); - } -} - -// Shop menu input and selection -static void game_shop_process_user_input() -{ - if (g_game_vars.timer == TM_SHOP_PRC_INPUT_START) - { - // TODO: Move to on_init? - // The selection grid is initialized outside of bounds and moved - // to trigger the selection change so the initial selection is visible - shop_selection_grid.selection = SHOP_INIT_SEL; - selection_grid_move_selection_horz(&shop_selection_grid, 1); - tte_printf( - "#{P:%d,%d; cx:0x%X000}$%d", - SHOP_REROLL_RECT.left, - SHOP_REROLL_RECT.top, - TTE_WHITE_PB, - reroll_cost - ); - } - - // Shop input logic - selection_grid_process_input(&shop_selection_grid); -} - -// Outro sequence (menu and shop icon going out of frame) -static void game_shop_outro() -{ - // Shift the shop panel - main_bg_se_move_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_DOWN); - - main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SCREEN_UP); - - // TODO: make heads or tails of what's going on here and replace - // magic numbers. - if (g_game_vars.timer == 1) - { - tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT); // Erase the shop prices text - - ListItr itr = list_itr_create(&_shop_jokers_list); - JokerObject* joker_object; - while ((joker_object = list_itr_next(&itr))) - { - if (joker_object != NULL) - { - joker_object->sprite_object->ty = int2fx(160); - } - } - - reset_top_left_panel_bottom_row(); - } - else if (g_game_vars.timer == 2) - { - int y = 5; - memset16(&se_mat[MAIN_BG_SBB][y - 1][0], 0x0001, 1); - memset16(&se_mat[MAIN_BG_SBB][y - 1][1], 0x0002, 7); - memset16(&se_mat[MAIN_BG_SBB][y - 1][8], SE_HFLIP | 0x0001, 1); - } - - if (g_game_vars.timer >= MENU_POP_OUT_ANIM_FRAMES) - { - state_info[game_state].substate = GAME_SHOP_MAX; // Go to the next state - g_game_vars.timer = TM_ZERO; // Reset the timer - } -} - -static inline void game_shop_lights_anim_frame(void) -{ - // Shift palette around the border of the shop icon - COLOR shifted_palette[4]; - memcpy16(&shifted_palette[0], &pal_bg_mem[SHOP_LIGHTS_2_PID], 1); - memcpy16(&shifted_palette[1], &pal_bg_mem[SHOP_LIGHTS_3_PID], 1); - memcpy16(&shifted_palette[2], &pal_bg_mem[SHOP_LIGHTS_4_PID], 1); - memcpy16(&shifted_palette[3], &pal_bg_mem[SHOP_LIGHTS_1_PID], 1); - - // Circularly shift the palette - int last = shifted_palette[3]; - - for (int i = 3; i > 0; --i) - { - shifted_palette[i] = shifted_palette[i - 1]; - } - - shifted_palette[0] = last; - - // Copy the shifted palette to the next 4 slots - memcpy16(&pal_bg_mem[SHOP_LIGHTS_2_PID], &shifted_palette[0], 1); - memcpy16(&pal_bg_mem[SHOP_LIGHTS_3_PID], &shifted_palette[1], 1); - memcpy16(&pal_bg_mem[SHOP_LIGHTS_4_PID], &shifted_palette[2], 1); - memcpy16(&pal_bg_mem[SHOP_LIGHTS_1_PID], &shifted_palette[3], 1); -} - -static void game_shop_on_update(void) -{ - change_background(BG_SHOP, false); - - if (!list_is_empty(&_shop_jokers_list)) - { - ListItr itr = list_itr_create(&_shop_jokers_list); - JokerObject* joker_object; - while ((joker_object = list_itr_next(&itr))) - { - if (joker_object != NULL) - { - joker_object_update(joker_object); - } - } - } - - if (g_game_vars.timer % 20 == 0) - { - game_shop_lights_anim_frame(); - } - - if (state_info[game_state].substate == GAME_SHOP_MAX) - { - game_change_state(GAME_STATE_BLIND_SELECT); - return; - } - - int substate = state_info[game_state].substate; - - shop_state_actions[substate](); -} - -static void game_shop_on_exit(void) -{ - ListItr itr = list_itr_create(&_shop_jokers_list); - JokerObject* joker_object; - - while ((joker_object = list_itr_next(&itr))) - { - if (joker_object != NULL) - { - // Make the joker available back to shop - set_shop_joker_avail(joker_object->joker->id, true); - } - joker_object_destroy(&joker_object); // Destroy the joker objects - } - - list_clear(&_shop_jokers_list); - - increment_blind(BLIND_STATE_DEFEATED); // TODO: Move to game_round_end()? - - save_game(); -} - void game_start(void) { // set_seed(9); // 9 is a full house diff --git a/source/game/common_ui.c b/source/game/common_ui.c index bb5b44d..368350b 100644 --- a/source/game/common_ui.c +++ b/source/game/common_ui.c @@ -5,6 +5,7 @@ #include "game/main_menu.h" #include "game/options_menu.h" #include "game/round_end.h" +#include "game/shop.h" typedef void (*BackgroundRenderCallback)(void); @@ -16,7 +17,7 @@ static const BackgroundRenderCallback bgCallbacks[] = { [BG_CARD_SELECTING] = NULL, [BG_CARD_PLAYING] = NULL, [BG_ROUND_END] = game_round_end_change_background, - [BG_SHOP] = NULL, + [BG_SHOP] = game_shop_change_background, [BG_BLIND_SELECT] = game_blind_select_change_background, [BG_OPTIONS_MENU] = game_options_menu_change_background, [BG_MAIN_MENU] = game_main_menu_change_background, diff --git a/source/game/joker_row.c b/source/game/joker_row.c new file mode 100644 index 0000000..eb17771 --- /dev/null +++ b/source/game/joker_row.c @@ -0,0 +1,128 @@ +#include "game/joker_row.h" + +#include "game.h" +#include "game_variables.h" +#include "joker.h" +#include "layout.h" +#include "list.h" +#include "sprite.h" +#include "util.h" + +int jokers_sel_row_get_size(void) +{ + return list_get_len(get_jokers_list()); +} + +bool jokers_sel_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +) +{ + List* owned_jokers_list = get_jokers_list(); + + // swap Jokers if the A button is held down and all Jokers are on the same row + bool swapping = + key_is_down(SELECT_CARD) && new_selection->y == row_idx && prev_selection->y == row_idx; + + if (prev_selection->y == row_idx) + { + JokerObject* joker_object = + (JokerObject*)list_get_at_idx(owned_jokers_list, prev_selection->x); + // Don't change focus from current Joker if swapping + if (joker_object != NULL && !swapping) + { + sprite_object_erase_text_under(joker_object->sprite_object); + sprite_object_set_focus(joker_object->sprite_object, false); + } + } + + if (new_selection->y == row_idx) + { + JokerObject* joker_object = + (JokerObject*)list_get_at_idx(owned_jokers_list, new_selection->x); + if (joker_object != NULL) + { + if (!swapping) + { + sprite_object_set_focus(joker_object->sprite_object, true); + } + // If we land on this row while the A button is being held, we are in swapping mode + // This means that we need to hide the price, whether we were already + // on this row or if we come from another + if (!key_is_down(SELECT_CARD)) + { + sprite_object_print_price_under( + joker_object->sprite_object, + joker_object->joker->value + ); + } + } + } + + if (swapping) + { + list_swap( + owned_jokers_list, + (unsigned int)prev_selection->x, + (unsigned int)new_selection->x + ); + } + + return true; +} + +static inline void joker_start_discard_animation(JokerObject* joker_object) +{ + joker_object->sprite_object->tx = int2fx(JOKER_DISCARD_TARGET.x); + joker_object->sprite_object->ty = int2fx(JOKER_DISCARD_TARGET.y); + list_push_back(get_discarded_jokers_list(), joker_object); +} + +static inline void game_sell_joker(int joker_idx) +{ + List* owned_jokers_list = get_jokers_list(); + + if (joker_idx < 0 || joker_idx >= list_get_len(owned_jokers_list)) + return; + + JokerObject* joker_object = (JokerObject*)list_get_at_idx(owned_jokers_list, joker_idx); + g_game_vars.money += joker_get_sell_value(joker_object->joker); + display_money(); + sprite_object_erase_text_under(joker_object->sprite_object); + + remove_owned_joker(joker_idx); + + joker_start_discard_animation(joker_object); +} + +void jokers_sel_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection) +{ + JokerObject* joker_object = (JokerObject*)list_get_at_idx(get_jokers_list(), selection->x); + if (joker_object != NULL) + { + if (key_hit(SELECT_CARD)) + { + sprite_object_erase_text_under(joker_object->sprite_object); + } + else if (key_released(SELECT_CARD)) + { + sprite_object_print_price_under( + joker_object->sprite_object, + joker_object->joker->value + ); + } + } + + if (key_hit(SELL_KEY)) + { + int sold_joker_idx = selection->x; + + // Move the selection away from the jokers so it doesn't point to an invalid place + // Do this before selling the joker so valid row sizes are used + selection_grid_move_selection_vert(selection_grid, SCREEN_DOWN); + + game_sell_joker(sold_joker_idx); + } +} \ No newline at end of file diff --git a/source/game/round_end.c b/source/game/round_end.c index 8f4dbab..120821a 100644 --- a/source/game/round_end.c +++ b/source/game/round_end.c @@ -2,6 +2,7 @@ #include "affine_background.h" #include "affine_background_gfx.h" +#include "game.h" #include "game_variables.h" #include "layout.h" #include "timer.h" diff --git a/source/game/shop.c b/source/game/shop.c new file mode 100644 index 0000000..8f1e06c --- /dev/null +++ b/source/game/shop.c @@ -0,0 +1,714 @@ +/** + * @file shop.c + * + * @brief Shop state functions implementation. + */ + +#include "game/shop.h" + +#include "audio_utils.h" +#include "background_shop_gfx.h" +#include "bitset.h" +#include "button.h" +#include "game.h" +#include "game/blind_select.h" +#include "game/joker_row.h" +#include "game_variables.h" +#include "joker.h" +#include "layout.h" +#include "list.h" +#include "save.h" +#include "soundbank.h" +#include "timer.h" +#include "util.h" + +// Timer defs +#define TM_END_GAME_SHOP_INTRO 12 +#define TM_CREATE_SHOP_ITEMS_WAIT 1 +#define TM_SHIFT_SHOP_ICON_WAIT 7 + +// Pixel sized +#define ITEM_SHOP_Y 71 + +// Shop +#define REROLL_BASE_COST 5 // Base cost for rerolling the shop items +#define NEXT_ROUND_BTN_SEL_X 0 + +// Palette IDs +#define REROLL_BTN_PAL_IDX 3 +#define NEXT_ROUND_BTN_SELECTED_BORDER_PAL_IDX 5 +#define SHOP_PANEL_SHADOW_PAL_IDX 6 +#define REROLL_BTN_SELECTED_BORDER_PAL_IDX 7 +#define SHOP_LIGHTS_1_PAL_IDX 8 +#define SHOP_LIGHTS_2_PAL_IDX 14 +#define NEXT_ROUND_BTN_PAL_IDX 16 +#define SHOP_LIGHTS_3_PAL_IDX 17 +#define SHOP_LIGHTS_4_PAL_IDX 22 +#define SHOP_BOTTOM_PANEL_BORDER_PAL_IDX 26 + +#define SHOP_LIGHTS_1_CLR 0xFFFF +#define SHOP_LIGHTS_2_CLR 0x32BE +#define SHOP_LIGHTS_3_CLR 0x4B5F +#define SHOP_LIGHTS_4_CLR 0x5F9F + +// clang-format off +static const Rect SHOP_ICON_FROM_RECT = { 0, 26, 8, 26 }; +static const BG_POINT SHOP_ICON_TO_POS = { 0, 0 }; +static const Rect SHOP_PRICES_TEXT_RECT = { 72, 56, 192, 160 }; +static const Rect SHOP_REROLL_RECT = { 88, 96, UNDEFINED, UNDEFINED }; + +// Positions in pixels +static const BG_POINT SHOP_JOKER_SPRITES_INIT_POS = {120, 160}; +// clang-format on + +// Shop Substates + +enum GameShopStates +{ + GAME_SHOP_INTRO, + GAME_SHOP_ACTIVE, + GAME_SHOP_EXIT, + GAME_SHOP_MAX +}; + +static void game_shop_intro(); +static void game_shop_process_user_input(); +static void game_shop_outro(); + +static const SubStateActionFn shop_state_actions[] = { + game_shop_intro, + game_shop_process_user_input, + game_shop_outro +}; + +// Shop SelectionGrid + +static int shop_top_row_get_size(void); +static bool shop_top_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +); +static void shop_top_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection); +static int shop_reroll_row_get_size(void); +static bool shop_reroll_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +); +static void shop_reroll_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection); + +static SelectionGridRow shop_selection_rows[] = { + {0, jokers_sel_row_get_size, jokers_sel_row_on_selection_changed, jokers_sel_row_on_key_transit, {.wrap = false, .has_h_exit_idx = false, .h_exit_idx = 0}}, + {1, shop_top_row_get_size, shop_top_row_on_selection_changed, shop_top_row_on_key_transit, {.wrap = false, .has_h_exit_idx = false, .h_exit_idx = 0}}, + {2, shop_reroll_row_get_size, shop_reroll_row_on_selection_changed, shop_reroll_row_on_key_transit, {.wrap = false, .has_h_exit_idx = true, .h_exit_idx = 1} }, +}; + +static const Selection SHOP_INIT_SEL = {-1, 1}; + +static SelectionGrid shop_selection_grid = { + shop_selection_rows, + NUM_ELEM_IN_ARR(shop_selection_rows), + SHOP_INIT_SEL +}; + +// Shop Buttons + +static void next_round_on_pressed(void); +static void reroll_on_pressed(void); +static bool reroll_can_be_pressed(void); +static Button next_round_button = + {NEXT_ROUND_BTN_SELECTED_BORDER_PAL_IDX, NEXT_ROUND_BTN_PAL_IDX, next_round_on_pressed, NULL}; +static Button reroll_button = { + REROLL_BTN_SELECTED_BORDER_PAL_IDX, + REROLL_BTN_PAL_IDX, + reroll_on_pressed, + reroll_can_be_pressed +}; + +// Shop internal variables + +static int timer; +static enum GameShopStates substate; + +static int reroll_cost = REROLL_BASE_COST; + +void game_shop_change_background(void) +{ + toggle_windows(false, true); + + GRIT_CPY(pal_bg_mem, background_shop_gfxPal); + GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_shop_gfxTiles); + GRIT_CPY(&se_mem[MAIN_BG_SBB], background_shop_gfxMap); + + // Set the outline colors for the shop background. This is used for the alternate shop + // palettes when opening packs + pal_bg_mem[SHOP_BOTTOM_PANEL_BORDER_PAL_IDX] = 0x213D; + pal_bg_mem[SHOP_PANEL_SHADOW_PAL_IDX] = 0x10B4; + + // Reset the shop lights to correct colors + pal_bg_mem[SHOP_LIGHTS_2_PAL_IDX] = SHOP_LIGHTS_2_CLR; + pal_bg_mem[SHOP_LIGHTS_3_PAL_IDX] = SHOP_LIGHTS_3_CLR; + pal_bg_mem[SHOP_LIGHTS_4_PAL_IDX] = SHOP_LIGHTS_4_CLR; + pal_bg_mem[SHOP_LIGHTS_1_PAL_IDX] = SHOP_LIGHTS_1_CLR; + + // Disable the button highlight colors + pal_bg_mem[REROLL_BTN_SELECTED_BORDER_PAL_IDX] = pal_bg_mem[REROLL_BTN_PAL_IDX]; + pal_bg_mem[NEXT_ROUND_BTN_SELECTED_BORDER_PAL_IDX] = pal_bg_mem[NEXT_ROUND_BTN_PAL_IDX]; +} + +void game_shop_on_init(void) +{ + game_shop_change_background(); + + timer = TM_ZERO; + substate = GAME_SHOP_INTRO; + + // The selection grid is initialized outside of bounds and moved + // to trigger the selection change so the initial selection is visible + shop_selection_grid.selection = SHOP_INIT_SEL; + selection_grid_move_selection_horz(&shop_selection_grid, 1); +} + +/** + * @brief Computes the number of Jokers we can currently roll in the Shop. + * The Jokers we own is taken into account and can't be rolled again. + */ +static inline int get_num_shop_jokers_avail(void) +{ + return bitset_num_set_bits(get_avail_jokers_bitset()); +} + +/** + * @brief Rolls a random Joker among the available ones + */ +static inline int game_shop_get_rand_available_joker_id(void) +{ + // Roll for what rarity the joker will be + int joker_rarity = joker_get_random_rarity(); + + // Now determine how many jokers are available based on the rarity + int jokers_avail_size = get_num_shop_jokers_avail(); + + if (jokers_avail_size == 0) + return UNDEFINED; + + int matching_joker_ids[jokers_avail_size]; + int fallback_random_idx = get_rand() % jokers_avail_size; + int fallback_random_joker_id = UNDEFINED; + int match_count = 0; + + BitsetItr itr = bitset_itr_create(get_avail_jokers_bitset()); + + int i = 0; + int joker_id = UNDEFINED; + while ((joker_id = bitset_itr_next(&itr)) != UNDEFINED) + { + if (i++ == fallback_random_idx) + fallback_random_joker_id = joker_id; + const JokerInfo* info = get_joker_registry_entry(joker_id); + if (info->rarity == joker_rarity) + { + matching_joker_ids[match_count++] = joker_id; + } + } + + int selected_joker_id = + (match_count > 0) ? matching_joker_ids[get_rand() % match_count] : fallback_random_joker_id; + + return selected_joker_id; +} + +/** + * @brief Returns true if we can't roll any Joker + */ +static inline bool no_avail_jokers(void) +{ + return bitset_is_empty(get_avail_jokers_bitset()); +} + +/** + * @brief Setup for the lists of items we can purchase in the Shop. + * Only Jokers are available for now, but this is where consumables and + * booster packs will be rolled when they are implemented. + */ +static void game_shop_create_items(void) +{ + tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT); + + if (no_avail_jokers()) + return; + + List* shop_jokers_list = get_shop_jokers_list(); + + list_clear(shop_jokers_list); + *shop_jokers_list = list_create(); + + for (int i = 0; i < MAX_SHOP_JOKERS; i++) + { + int joker_id = 0; +#ifdef TEST_JOKER_ID0 // Allow defining an ID for a joker to always appear in shop and be tested + if (is_shop_joker_avail(TEST_JOKER_ID0)) + { + joker_id = TEST_JOKER_ID0; + } + else +#endif +#ifdef TEST_JOKER_ID1 + if (is_shop_joker_avail(TEST_JOKER_ID1)) + { + joker_id = TEST_JOKER_ID1; + } + else +#endif + { + joker_id = game_shop_get_rand_available_joker_id(); + } + + // If for some reason only no joker is left, don't make another + if (joker_id == UNDEFINED) + break; + + set_shop_joker_avail(joker_id, false); + + JokerObject* joker_object = joker_object_new(joker_new(joker_id)); + + joker_object->sprite_object->x = + int2fx(SHOP_JOKER_SPRITES_INIT_POS.x + i * CARD_SPRITE_SIZE); + joker_object->sprite_object->y = int2fx(SHOP_JOKER_SPRITES_INIT_POS.y); + joker_object->sprite_object->tx = joker_object->sprite_object->x; + joker_object->sprite_object->ty = int2fx(ITEM_SHOP_Y); + + sprite_object_print_price_under(joker_object->sprite_object, joker_object->joker->value); + + sprite_position( + joker_object_get_sprite(joker_object), + fx2int(joker_object->sprite_object->x), + fx2int(joker_object->sprite_object->y) + ); + + list_push_back(shop_jokers_list, joker_object); + } +} + +/** + * @brief Intro sequence (menu and shop icon coming into frame) + */ +static void game_shop_intro() +{ + main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP); + + if (timer == TM_CREATE_SHOP_ITEMS_WAIT) + { + game_shop_create_items(); + } + + if (timer >= TM_SHIFT_SHOP_ICON_WAIT) // Shift the shop icon + { + int timer_offset = timer - 6; + + // TODO: Extract to generic function? + for (int y = 0; y < timer_offset; y++) + { + Rect from = SHOP_ICON_FROM_RECT; + from.top += y - timer_offset; + from.bottom += y - timer_offset; + + BG_POINT to = SHOP_ICON_TO_POS; + to.y = y; + + main_bg_se_copy_rect(from, to); + } + } + + if (timer == TM_END_GAME_SHOP_INTRO) + { + substate = GAME_SHOP_ACTIVE; + timer = TM_ZERO; // Reset the timer + + // print initial reroll cost only when the panel is in place + tte_printf( + "#{P:%d,%d; cx:0x%X000}$%d", + SHOP_REROLL_RECT.left, + SHOP_REROLL_RECT.top, + TTE_WHITE_PB, + reroll_cost + ); + } +} + +/** + * @brief Computes the number of Jokers up for sale, aka the number of + * "buttons" there are on the top row of the Shop. + */ +static int shop_top_row_get_size(void) +{ + // + 1 to account for next round button + return list_get_len(get_shop_jokers_list()) + 1; +} + +/** + * @brief Add a newly purchased Joker to the list of owned Jokers. + */ +static inline void add_to_held_jokers(JokerObject* joker_object) +{ + joker_object->sprite_object->ty = int2fx(HELD_JOKERS_POS.y); + add_joker(joker_object); +} + +/** + * @brief Called when pressing A on a Shop Joker to buy it. + */ +static inline void game_shop_buy_joker(int shop_joker_idx) +{ + List* shop_jokers_list = get_shop_jokers_list(); + JokerObject* joker_object = (JokerObject*)list_get_at_idx(shop_jokers_list, shop_joker_idx); + + g_game_vars.money -= joker_object->joker->value; + display_money(); + sprite_object_erase_text_under(joker_object->sprite_object); + sprite_object_set_focus(joker_object->sprite_object, false); + add_to_held_jokers(joker_object); + list_remove_at_idx(shop_jokers_list, shop_joker_idx); // Remove the joker from the shop +} + +/** + * @brief Handle button inputs for the "Next Round" button and shop Jokers. + */ +static void shop_top_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection) +{ + if (!key_hit(SELECT_CARD)) + return; + + if (selection->x == NEXT_ROUND_BTN_SEL_X) + { + button_press(&next_round_button); + } + else + { + int shop_joker_idx = selection->x - 1; // - 1 to account for next round button + JokerObject* joker_object = + (JokerObject*)list_get_at_idx(get_shop_jokers_list(), shop_joker_idx); + if (joker_object == NULL || list_get_len(get_jokers_list()) >= MAX_JOKERS_HELD_SIZE || + g_game_vars.money < joker_object->joker->value) + { + return; + } + + game_shop_buy_joker(shop_joker_idx); + selection_grid_move_selection_horz(selection_grid, -1); + } +} + +/** + * @brief Handle d-pad inputs for the "Next Round" button and shop Jokers' row. + */ +static bool shop_top_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +) +{ + List* shop_jokers_list = get_shop_jokers_list(); + // Guard if we move down while on jokers + if (new_selection->y > row_idx && prev_selection->x > 0) + return false; + + // The selection grid system only guarantees that the new selection is within bounds + // but not the previous one... + // This allows using INIT_SEL = {-1, 1} and move to set the initial selection in a hacky way... + if (prev_selection->y == row_idx && prev_selection->x >= 0 && + prev_selection->x < shop_top_row_get_size()) + { + if (prev_selection->x == NEXT_ROUND_BTN_SEL_X) + { + button_set_highlight(&next_round_button, false); + } + else + { + int idx = prev_selection->x - 1; // -1 to account for next round button + JokerObject* joker_object = (JokerObject*)list_get_at_idx(shop_jokers_list, idx); + sprite_object_set_focus(joker_object->sprite_object, false); + } + } + + if (new_selection->y == row_idx) + { + if (new_selection->x == NEXT_ROUND_BTN_SEL_X) + { + button_set_highlight(&next_round_button, true); + } + else + { + int idx = new_selection->x - 1; // -1 to account for next round button + JokerObject* joker_object = (JokerObject*)list_get_at_idx(shop_jokers_list, idx); + sprite_object_set_focus(joker_object->sprite_object, true); + } + } + + return true; +} + +/** + * @brief Get size of the row with the "Reroll" button + * + * @returns Always 1, because there is only the "Reroll" button on that row. + */ +static int shop_reroll_row_get_size() +{ + return 1; +} + +/** + * @brief Handle d-pad inputs for the "Reroll" button's row. + */ +static bool shop_reroll_row_on_selection_changed( + SelectionGrid* selection_grid, + int row_idx, + const Selection* prev_selection, + const Selection* new_selection +) +{ + if (row_idx == prev_selection->y) + { + button_set_highlight(&reroll_button, false); + + if (new_selection->x != NEXT_ROUND_BTN_SEL_X) + { + int idx = new_selection->x - 1; + JokerObject* joker_object = (JokerObject*)list_get_at_idx(get_shop_jokers_list(), idx); + sprite_object_set_focus(joker_object->sprite_object, true); + } + } + else if (row_idx == new_selection->y) + { + button_set_highlight(&reroll_button, true); + } + + return true; +} + +/** + * @brief Reroll items up for sale in the Shop. + * Reroll cost will go up by a rate that increases by 1 each reroll. + */ +static inline void game_shop_reroll(int* reroll_cost) +{ + g_game_vars.money -= *reroll_cost; + display_money(); // Update the money display + + List* shop_jokers_list = get_shop_jokers_list(); + ListItr itr = list_itr_create(shop_jokers_list); + JokerObject* joker_object; + + while ((joker_object = list_itr_next(&itr))) + { + if (joker_object != NULL) + { + set_shop_joker_avail(joker_object->joker->id, true); + joker_object_destroy(&joker_object); // Destroy the joker object if it exists + } + } + + list_clear(shop_jokers_list); + *shop_jokers_list = list_create(); + + game_shop_create_items(); + + itr = list_itr_create(shop_jokers_list); + + while ((joker_object = list_itr_next(&itr))) + { + if (joker_object != NULL) + { + // Set the y position to the target position + joker_object->sprite_object->y = joker_object->sprite_object->ty; + + // Give the joker a little wiggle animation + joker_object_shake(joker_object, UNDEFINED); + } + } + + (*reroll_cost)++; + tte_printf( + "#{P:%d,%d; cx:0x%X000}$%d", + SHOP_REROLL_RECT.left, + SHOP_REROLL_RECT.top, + TTE_WHITE_PB, + *reroll_cost + ); +} + +/** + * @brief Handle button inputs for the "Reroll" button's row. + */ +static void shop_reroll_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection) +{ + if (!key_hit(SELECT_CARD)) + { + return; + } + + button_press(&reroll_button); +} + +static void next_round_on_pressed(void) +{ + // Go to next blind selection game state + substate = GAME_SHOP_EXIT; // Go to the outro sequence state + timer = TM_ZERO; + reroll_cost = REROLL_BASE_COST; + + pal_bg_mem[NEXT_ROUND_BTN_SELECTED_BORDER_PAL_IDX] = pal_bg_mem[SHOP_PANEL_SHADOW_PAL_IDX]; +} + +static void reroll_on_pressed(void) +{ + // TODO: Add money sound effect + game_shop_reroll(&reroll_cost); +} + +static bool reroll_can_be_pressed(void) +{ + return g_game_vars.money >= reroll_cost; +} + +/** + * @brief Handle user inputs logic in the Shop though a SelectionGrid. + */ +static void game_shop_process_user_input() +{ + selection_grid_process_input(&shop_selection_grid); +} + +/** + * @brief Outro sequence substate update. + * This makes the menu and shop icon go out of frame. + */ +static void game_shop_outro() +{ + // Shift the shop panel + main_bg_se_move_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_DOWN); + + main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SCREEN_UP); + + // TODO: make heads or tails of what's going on here and replace + // magic numbers. + if (timer == 1) + { + tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT); // Erase the shop prices text + + ListItr itr = list_itr_create(get_shop_jokers_list()); + JokerObject* joker_object; + while ((joker_object = list_itr_next(&itr))) + { + if (joker_object != NULL) + { + joker_object->sprite_object->ty = int2fx(160); + } + } + + reset_top_left_panel_bottom_row(); + } + else if (timer == 2) + { + int y = 5; + memset16(&se_mat[MAIN_BG_SBB][y - 1][0], 0x0001, 1); + memset16(&se_mat[MAIN_BG_SBB][y - 1][1], 0x0002, 7); + memset16(&se_mat[MAIN_BG_SBB][y - 1][8], SE_HFLIP | 0x0001, 1); + } + + if (timer >= MENU_POP_OUT_ANIM_FRAMES) + { + substate = GAME_SHOP_MAX; // Go to the next state + timer = TM_ZERO; // Reset the timer + } +} + +/** + * @brief Cycling shop lights animation substate update. + */ +static inline void game_shop_lights_anim_frame(void) +{ + // Shift palette around the border of the shop icon + COLOR shifted_palette[4]; + shifted_palette[0] = pal_bg_mem[SHOP_LIGHTS_2_PAL_IDX]; + shifted_palette[1] = pal_bg_mem[SHOP_LIGHTS_3_PAL_IDX]; + shifted_palette[2] = pal_bg_mem[SHOP_LIGHTS_4_PAL_IDX]; + shifted_palette[3] = pal_bg_mem[SHOP_LIGHTS_1_PAL_IDX]; + + // Circularly shift the palette + int last = shifted_palette[3]; + + for (int i = 3; i > 0; --i) + { + shifted_palette[i] = shifted_palette[i - 1]; + } + + shifted_palette[0] = last; + + // Copy the shifted palette to the next 4 slots + pal_bg_mem[SHOP_LIGHTS_2_PAL_IDX] = shifted_palette[0]; + pal_bg_mem[SHOP_LIGHTS_3_PAL_IDX] = shifted_palette[1]; + pal_bg_mem[SHOP_LIGHTS_4_PAL_IDX] = shifted_palette[2]; + pal_bg_mem[SHOP_LIGHTS_1_PAL_IDX] = shifted_palette[3]; +} + +void game_shop_on_update(void) +{ + timer++; + + List* shop_jokers_list = get_shop_jokers_list(); + + if (!list_is_empty(shop_jokers_list)) + { + ListItr itr = list_itr_create(shop_jokers_list); + JokerObject* joker_object; + while ((joker_object = list_itr_next(&itr))) + { + if (joker_object != NULL) + { + joker_object_update(joker_object); + } + } + } + + if (timer % 20 == 0) + { + game_shop_lights_anim_frame(); + } + + if (substate == GAME_SHOP_MAX) + { + game_change_state(GAME_STATE_BLIND_SELECT); + return; + } + + shop_state_actions[substate](); +} + +void game_shop_on_exit(void) +{ + List* shop_jokers_list = get_shop_jokers_list(); + ListItr itr = list_itr_create(shop_jokers_list); + JokerObject* joker_object; + + while ((joker_object = list_itr_next(&itr))) + { + if (joker_object != NULL) + { + // Make the joker available back to shop + set_shop_joker_avail(joker_object->joker->id, true); + } + joker_object_destroy(&joker_object); // Destroy the joker objects + } + + list_clear(shop_jokers_list); + + increment_blind(BLIND_STATE_DEFEATED); // TODO: Move to game_round_end()? + + save_game(); +} \ No newline at end of file diff --git a/source/graphic_utils.c b/source/graphic_utils.c index eae8237..defaa8c 100644 --- a/source/graphic_utils.c +++ b/source/graphic_utils.c @@ -412,7 +412,7 @@ void main_bg_se_clear_rect(Rect se_rect) } } -void reset_top_left_panel_bottom_row() +void reset_top_left_panel_bottom_row(void) { BG_POINT top_left_panel_bottom_row_pos = TOP_LEFT_PANEL_POINT; // Use the source rect height to offset to the bottom row point diff --git a/source/sprite.c b/source/sprite.c index b747a9d..72072f9 100644 --- a/source/sprite.c +++ b/source/sprite.c @@ -3,6 +3,7 @@ #include "audio_utils.h" #include "game.h" #include "game_variables.h" +#include "graphic_utils.h" #include "pool.h" #include "soundbank.h" #include "util.h" @@ -351,3 +352,52 @@ bool sprite_object_is_focused(SpriteObject* sprite_object) { return sprite_object->focused; } + +static Rect sprite_object_get_text_rect_under(SpriteObject* sprite_object) +{ + int height = 0; + int width = 0; + + if (sprite_object_get_dimensions(sprite_object, &width, &height) == false) + { + // fallback + height = CARD_SPRITE_SIZE; + width = CARD_SPRITE_SIZE; + } + + Rect ret_rect = {0}; + + ret_rect.left = fx2int(sprite_object->tx); + ret_rect.top = fx2int(sprite_object->ty) + height + TILE_SIZE; + ret_rect.right = ret_rect.left + width; + ret_rect.bottom = ret_rect.top + TTE_CHAR_SIZE; + + return ret_rect; +} + +void sprite_object_print_text_under(SpriteObject* sprite_object, const char text[]) +{ + Rect text_rect = sprite_object_get_text_rect_under(sprite_object); + + update_text_rect_to_center_str(&text_rect, text, SCREEN_LEFT); + + tte_printf("#{P:%d,%d; cx:0x%X000}%s", text_rect.left, text_rect.top, TTE_YELLOW_PB, text); +} + +void sprite_object_print_price_under(SpriteObject* sprite_object, int price) +{ + // + 2 for null-terminator and "$" + char price_str_buff[INT_MAX_DIGITS + 2]; + snprintf(price_str_buff, sizeof(price_str_buff), "$%d", price); + sprite_object_print_text_under(sprite_object, price_str_buff); +} + +void sprite_object_erase_text_under(SpriteObject* sprite_object) +{ + Rect text_rect = sprite_object_get_text_rect_under(sprite_object); + + // Add SPRITE_FOCUS_RAISE_PX to cover the focused case + text_rect.bottom = text_rect.bottom + SPRITE_FOCUS_RAISE_PX; + + tte_erase_rect_wrapper(text_rect); +}