Refactored to use selection grid during round (#348)

* Added wrapping to selection grid

* Added return value to on_selection_changed to allow aborting on wrap

* Extracted button code to button.c/.h

* Bounds checking changes for selection_grid_move_selection_horz()

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Rickey <ric@rf3.xyz>
This commit is contained in:
MeirGavish
2026-01-15 20:32:09 +02:00
committed by GitHub
co-authored by Copilot Rickey
parent b7cc3ec5d6
commit 5300846f73
5 changed files with 553 additions and 322 deletions
+76
View File
@@ -0,0 +1,76 @@
/**
* @file button.h
*
* @brief A button structure containing the common button functionalities
*/
#ifndef BUTTON_H
#define BUTTON_H
#include <tonc_types.h>
#define BTN_HIGHLIGHT_COLOR 0xFFFF
#define BUTTON_SFX_VOLUME 154 // 60% of MM_FULL_VOLUME
/**
* @brief The function to be called when the button is pressed
*/
typedef void (*ButtonOnPressedFunc)(void);
/**
* @brief Returns true if the button should be activated when pressed.
*/
typedef bool (*ButtonCanBePressedFunc)(void);
/**
* @brief A button representation
*/
typedef struct
{
// Using u8 for the palette indices to make sure they don't overflow pal_bg_mem
/**
* @brief Palette index of the button border whose color is updated depending on highlight.
*/
u8 border_pal_idx;
/**
* @brief Palette index of the color of the button itself, used for the border when not
* highlighted.
*/
u8 button_pal_idx;
/**
* @brief Called when the button is pressed if @ref can_be_pressed, don't call this directly,
* call @ref button_press() instead.
* Should not be set to NULL.
*/
ButtonOnPressedFunc on_pressed;
/**
* @brief Returns true if the button should be activated when pressed.
* Can be NULL if button can always be pressed.
*/
ButtonCanBePressedFunc can_be_pressed;
} Button;
/**
* @brief Set the button highlight on or off.
* @param button The button to modify. No-op on NULL.
* @param highlight true to highlight, false to unhighlight.
*/
void button_set_highlight(Button* button, bool highlight);
/**
* @brief Execute a button press, by calling the button's @ref on_pressed function.
* This checks the button's @ref can_be_pressed function and will do nothing if
* returns false.
*
* Button presses should go through this function rather than directly call @ref on_pressed.
*
* @param button The button being pressed. No-op on NULL.
*/
void button_press(Button* button);
#endif // BUTTON_H
+15 -1
View File
@@ -37,9 +37,10 @@ typedef struct SelectionGrid SelectionGrid;
* Contains the previous cursor position * Contains the previous cursor position
* @param new_selection Pointer to the Selection state after the change occurred. * @param new_selection Pointer to the Selection state after the change occurred.
* Contains the new cursor position * Contains the new cursor position
* @return false if the selection change needs to be aborted, true if can proceed.
* *
*/ */
typedef void (*RowOnSelectionChangedFunc)( typedef bool (*RowOnSelectionChangedFunc)(
SelectionGrid* selection_grid, SelectionGrid* selection_grid,
int row_idx, int row_idx,
const Selection* prev_selection, const Selection* prev_selection,
@@ -71,6 +72,18 @@ typedef int (*RowGetSizeFunc)();
*/ */
typedef void (*RowOnKeyTransitFunc)(SelectionGrid* selection_grid, Selection* selection); typedef void (*RowOnKeyTransitFunc)(SelectionGrid* selection_grid, Selection* selection);
/**
* @brief A set of attributes to the selection grid row affecting selection grid behavior.
*/
typedef struct
{
/**
* @brief Whether to wrap selection when it passes the end of the row.
*/
bool wrap;
} SelGridRowAttributes;
/** /**
* @brief A single row in the selection grid, defined by its callback functions. * @brief A single row in the selection grid, defined by its callback functions.
* *
@@ -82,6 +95,7 @@ struct SelectionGridRow
RowGetSizeFunc get_size; RowGetSizeFunc get_size;
RowOnSelectionChangedFunc on_selection_changed; RowOnSelectionChangedFunc on_selection_changed;
RowOnKeyTransitFunc on_key_transit; RowOnKeyTransitFunc on_key_transit;
SelGridRowAttributes attributes;
}; };
/** /**
+29
View File
@@ -0,0 +1,29 @@
#include "button.h"
#include "audio_utils.h"
#include "soundbank.h"
#include <tonc.h>
void button_set_highlight(Button* button, bool highlight)
{
if (button == NULL)
return;
u16 set_color = highlight ? BTN_HIGHLIGHT_COLOR : pal_bg_mem[button->button_pal_idx];
memset16(&pal_bg_mem[button->border_pal_idx], set_color, 1);
}
void button_press(Button* button)
{
if (button == NULL || button->on_pressed == NULL ||
(button->can_be_pressed != NULL && !button->can_be_pressed()))
{
return;
}
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
button->on_pressed();
}
+401 -317
View File
@@ -9,6 +9,7 @@
#include "background_shop_gfx.h" #include "background_shop_gfx.h"
#include "bitset.h" #include "bitset.h"
#include "blind.h" #include "blind.h"
#include "button.h"
#include "card.h" #include "card.h"
#include "graphic_utils.h" #include "graphic_utils.h"
#include "hand_analysis.h" #include "hand_analysis.h"
@@ -45,7 +46,6 @@
#define MENU_POP_OUT_ANIM_FRAMES 20 #define MENU_POP_OUT_ANIM_FRAMES 20
#define GAME_OVER_ANIM_FRAMES 15 #define GAME_OVER_ANIM_FRAMES 15
#define HIGHLIGHT_COLOR 0xFFFF
#define SHOP_LIGHTS_1_CLR 0xFFFF #define SHOP_LIGHTS_1_CLR 0xFFFF
#define SHOP_LIGHTS_2_CLR 0x32BE #define SHOP_LIGHTS_2_CLR 0x32BE
#define SHOP_LIGHTS_3_CLR 0x4B5F #define SHOP_LIGHTS_3_CLR 0x4B5F
@@ -55,8 +55,6 @@
#define PITCH_STEP_DRAW_SFX 24 #define PITCH_STEP_DRAW_SFX 24
#define PITCH_STEP_UNDISCARD_SFX 2 * PITCH_STEP_DRAW_SFX #define PITCH_STEP_UNDISCARD_SFX 2 * PITCH_STEP_DRAW_SFX
#define BUTTON_SFX_VOLUME 154 // 60% of MM_FULL_VOLUME
#define STARTING_ROUND 0 #define STARTING_ROUND 0
#define STARTING_ANTE 1 #define STARTING_ANTE 1
#define STARTING_MONEY 4 #define STARTING_MONEY 4
@@ -129,11 +127,8 @@
#define NEXT_ROUND_BTN_SEL_X 0 #define NEXT_ROUND_BTN_SEL_X 0
#define GAME_PLAYING_HAND_SEL_Y 0 #define GAME_PLAYING_HAND_SEL_Y 1
#define GAME_PLAYING_BUTTONS_SEL_Y 1 #define GAME_PLAYING_BUTTONS_SEL_Y 2
#define GAME_PLAYING_PLAY_BTN_SEL_X 0
#define GAME_PLAYING_DISCARD_BTN_SEL_X 1
#define GAME_PLAYING_NUM_SEL_ROWS 2
#define GAME_PLAYING_NUM_BOTTOM_BTNS 2 #define GAME_PLAYING_NUM_BOTTOM_BTNS 2
#define REROLL_BTN_FRAME_PAL_IDX 7 #define REROLL_BTN_FRAME_PAL_IDX 7
@@ -242,8 +237,6 @@ static void display_round(int value);
static void display_hands(int value); static void display_hands(int value);
static void display_discards(int value); static void display_discards(int value);
static void set_hand(void); static void set_hand(void);
static void hand_set_focus(int index);
static bool hand_can_discard(void);
static int deck_get_size(void); static int deck_get_size(void);
static int deck_get_max_size(void); static int deck_get_max_size(void);
static void increment_blind(enum BlindState increment_reason); static void increment_blind(enum BlindState increment_reason);
@@ -255,15 +248,41 @@ static bool check_and_score_joker_for_event(
); );
static int calculate_interest_reward(void); static int calculate_interest_reward(void);
static void game_over_anim_frame(void); static void game_over_anim_frame(void);
static void game_playing_discard_on_pressed(void);
static void game_playing_play_hand_on_pressed(void);
static int game_playing_button_row_get_size(void);
static bool game_playing_button_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
);
static void game_playing_button_row_on_key_hit(SelectionGrid* selection_grid, Selection* selection);
static void game_playing_hand_row_on_key_transit(
SelectionGrid* selection_grid,
Selection* selection
);
static bool game_playing_hand_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
);
static int game_playing_hand_row_get_size(void);
static void shop_reroll_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection); static void shop_reroll_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection);
static void shop_reroll_row_on_selection_changed( static bool shop_reroll_row_on_selection_changed(
SelectionGrid* selection_grid, SelectionGrid* selection_grid,
int row_idx, int row_idx,
const Selection* prev_selection, const Selection* prev_selection,
const Selection* new_selection const Selection* new_selection
); );
static int shop_reroll_row_get_size(void); static int shop_reroll_row_get_size(void);
static void shop_top_row_on_selection_changed( static bool shop_top_row_on_selection_changed(
SelectionGrid* selection_grid, SelectionGrid* selection_grid,
int row_idx, int row_idx,
const Selection* prev_selection, const Selection* prev_selection,
@@ -272,7 +291,7 @@ static void shop_top_row_on_selection_changed(
static void shop_top_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection); static void shop_top_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection);
static int shop_top_row_get_size(void); static int shop_top_row_get_size(void);
static void jokers_sel_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection); static void jokers_sel_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection);
static void jokers_sel_row_on_selection_changed( static bool jokers_sel_row_on_selection_changed(
SelectionGrid* selection_grid, SelectionGrid* selection_grid,
int row_idx, int row_idx,
const Selection* prev_selection, const Selection* prev_selection,
@@ -280,11 +299,20 @@ static void jokers_sel_row_on_selection_changed(
); );
static int jokers_sel_row_get_size(void); static int jokers_sel_row_get_size(void);
static void game_shop_create_items(void); static void game_shop_create_items(void);
static void erase_price_under_sprite_object(SpriteObject* sprite_object); static void erase_price_under_sprite_object(SpriteObject* sprite_object);
static void print_price_under_sprite_object(SpriteObject* sprite_object, int price); static void print_price_under_sprite_object(SpriteObject* sprite_object, int price);
static void game_round_end_extend_black_panel_down(int black_panel_bottom); static void game_round_end_extend_black_panel_down(int black_panel_bottom);
static void remove_owned_joker(int owned_joker_idx); 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_change_sort(void);
static void hand_deselect_all_cards(void);
static bool can_play_hand(void);
static bool can_discard_hand(void);
// Consts // Consts
// clang-format off // clang-format off
@@ -404,10 +432,50 @@ static StateInfo state_info[] = {
#undef DEF_STATE_INFO #undef DEF_STATE_INFO
}; };
// clang-format off
SelectionGridRow game_playing_selection_rows[] = {
{
0,
jokers_sel_row_get_size,
jokers_sel_row_on_selection_changed,
jokers_sel_row_on_key_transit,
{.wrap = false}
},
{
1,
game_playing_hand_row_get_size,
game_playing_hand_row_on_selection_changed,
game_playing_hand_row_on_key_transit,
{.wrap = true}
},
{
2,
game_playing_button_row_get_size,
game_playing_button_row_on_selection_changed,
game_playing_button_row_on_key_hit,
{.wrap = false}
}
};
// clang-format on
static const Selection GAME_PLAYING_INIT_SEL = {0, 1};
SelectionGrid game_playing_selection_grid = {
game_playing_selection_rows,
NUM_ELEM_IN_ARR(game_playing_selection_rows),
GAME_PLAYING_INIT_SEL
};
// 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 },
{DISCARD_BTN_BORDER_PID, DISCARD_BTN_PID, game_playing_discard_on_pressed, can_discard_hand},
};
SelectionGridRow shop_selection_rows[] = { SelectionGridRow shop_selection_rows[] = {
{0, jokers_sel_row_get_size, jokers_sel_row_on_selection_changed, jokers_sel_row_on_key_transit }, {0, jokers_sel_row_get_size, jokers_sel_row_on_selection_changed, jokers_sel_row_on_key_transit, {.wrap = false}},
{1, shop_top_row_get_size, shop_top_row_on_selection_changed, shop_top_row_on_key_transit }, {1, shop_top_row_get_size, shop_top_row_on_selection_changed, shop_top_row_on_key_transit, {.wrap = false}},
{2, shop_reroll_row_get_size, shop_reroll_row_on_selection_changed, shop_reroll_row_on_key_transit} {2, shop_reroll_row_get_size, shop_reroll_row_on_selection_changed, shop_reroll_row_on_key_transit, {.wrap = false}}
}; };
static const Selection SHOP_INIT_SEL = {-1, 1}; static const Selection SHOP_INIT_SEL = {-1, 1};
@@ -1051,6 +1119,17 @@ void display_mult(void)
check_flaming_score(); check_flaming_score();
} }
static inline void display_ante(int value)
{
tte_printf(
"#{P:%d,%d; cx:0xC000}%d#{cx:0xF000}/%d",
ANTE_TEXT_RECT.left,
ANTE_TEXT_RECT.top,
value,
MAX_ANTE
);
}
// idx_a and idx_b are assumed to be valid indexes within the hand array // idx_a and idx_b are assumed to be valid indexes within the hand array
// no checks will be performed here for performance's sake // no checks will be performed here for performance's sake
static inline void swap_cards_in_hand(int idx_a, int idx_b) static inline void swap_cards_in_hand(int idx_a, int idx_b)
@@ -1256,9 +1335,10 @@ static void change_background(enum BackgroundId id)
1 1
); );
// Copy the Play Hand and Discard button colors to their selection highlights for (int i = 0; i < NUM_ELEM_IN_ARR(game_playing_buttons); i++)
memcpy16(&pal_bg_mem[PLAY_HAND_BTN_BORDER_PID], &pal_bg_mem[PLAY_HAND_BTN_PID], 1); {
memcpy16(&pal_bg_mem[DISCARD_BTN_BORDER_PID], &pal_bg_mem[DISCARD_BTN_PID], 1); button_set_highlight(&game_playing_buttons[i], false);
}
} }
} }
else if (id == BG_CARD_PLAYING) else if (id == BG_CARD_PLAYING)
@@ -1725,37 +1805,9 @@ static void set_hand(void)
display_mult(); display_mult();
} }
static void hand_set_focus(int index) static bool can_discard_hand(void)
{ {
if (hand_state != HAND_SELECT) return (discards > 0 && hand_state == HAND_SELECT && hand_selections > 0);
return;
// Wrap around to the other side of the hand when going out of bounds on either side
if (index < 0)
{
selection_x = hand_top;
}
else if (index > hand_top)
{
selection_x = 0;
}
else
{
selection_x = index;
}
play_sfx(
SFX_CARD_FOCUS,
MM_BASE_PITCH_RATE + rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
SFX_DEFAULT_VOLUME
);
}
static bool hand_can_discard(void)
{
if (hand_state != HAND_SELECT || hand_selections == 0)
return false;
return true;
} }
static int deck_get_size(void) static int deck_get_size(void)
@@ -1856,6 +1908,12 @@ static void game_round_on_init()
); // Blind reward ); // Blind reward
deck_shuffle(); // Shuffle the deck at the start of the round deck_shuffle(); // Shuffle the deck at the start of the round
/* Note that since cards_in_hand_update_loop() handles card highlight there's no need
* to call a selection changed callback to highlight the initial card, this wouldn't work
* otherwise or for the buttons.
*/
game_playing_selection_grid.selection = GAME_PLAYING_INIT_SEL;
} }
static void game_main_menu_on_init() static void game_main_menu_on_init()
@@ -1902,22 +1960,207 @@ static inline void set_seed(int seed)
srand(rng_seed); srand(rng_seed);
} }
static inline void hand_toggle_card_selection(void) // Playing state functions
static void game_playing_discard_on_pressed(void)
{ {
if (hand_state != HAND_SELECT || hand[selection_x] == NULL) if (!can_discard_hand())
return; return;
if (card_object_is_selected(hand[selection_x])) hand_state = HAND_DISCARD;
display_hands(--discards);
set_hand();
tte_printf(
"#{P:%d,%d; cx:0x%X000}%d",
DISCARDS_TEXT_RECT.left,
DISCARDS_TEXT_RECT.top,
TTE_RED_PB,
discards
);
// Move back to hand selection
selection_grid_move_selection_vert(&game_playing_selection_grid, -1);
}
static void game_playing_play_hand_on_pressed(void)
{
if (!can_play_hand())
return;
hand_state = HAND_PLAY;
display_hands(--hands);
// Move back to hand selection
selection_grid_move_selection_vert(&game_playing_selection_grid, -1);
}
static int game_playing_hand_row_get_size(void)
{
return hand_get_size();
}
// card moving logic
// true if and only if we are currently moving a card around
static bool moving_card = false;
// This will prevent us from moving cards around if we selected one
// by moving too fast after pressing the A button
static bool card_moved_too_fast = false;
static bool card_selected_instead_of_moved = false;
// After pressing A, if we press Left/Right too fast, we should select the card
// and change focus to the next one, instead of swapping them
// This should fix inputs sometimes not registering when quickly selecting cards
static const int card_swap_time_threshold = 6;
static uint selection_hit_timer = TM_ZERO;
static bool game_playing_hand_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
)
{
int prev_card_idx = UNDEFINED;
int next_card_idx = UNDEFINED;
// Do not use FRAMES(x) here as we are counting real frames ignoring game speed
card_moved_too_fast = (timer - selection_hit_timer) < card_swap_time_threshold;
if (prev_selection->y == GAME_PLAYING_HAND_SEL_Y)
{ {
card_object_set_selected(hand[selection_x], false); prev_card_idx = hand_sel_idx_to_card_idx(prev_selection->x);
hand_selections--;
play_sfx(SFX_CARD_DESELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
} }
else if (hand_selections < MAX_SELECTION_SIZE)
if (new_selection->y == GAME_PLAYING_HAND_SEL_Y)
{ {
card_object_set_selected(hand[selection_x], true); next_card_idx = hand_sel_idx_to_card_idx(new_selection->x);
hand_selections++; }
play_sfx(SFX_CARD_SELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
bool on_the_same_row = new_selection->y == prev_selection->y; // == GAME_PLAYING_HAND_SEL_Y
if (on_the_same_row && key_is_down(SELECT_CARD) && !card_moved_too_fast &&
!card_selected_instead_of_moved)
{
bool moved_by_one_tile = abs(new_selection->x - prev_selection->x) == 1;
// Avoid swapping when selection wraps
if (!moved_by_one_tile)
{
// Abort the selection if swapping so it doesn't wrap
return false;
}
else
{
swap_cards_in_hand(prev_card_idx, next_card_idx);
moving_card = true;
reorder_card_sprites_layers();
/* Not calling sprite_object_set_focus() because focus is handled by
* cards_in_hand_update_loop() based on the selection grid value...
*/
play_sfx(
SFX_CARD_FOCUS,
MM_BASE_PITCH_RATE + rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
SFX_DEFAULT_VOLUME
);
}
}
else
{
// select current card if we tried moving it too fast
if (key_released(SELECT_CARD) || (card_moved_too_fast && !moving_card))
{
hand_select_card(prev_card_idx);
card_selected_instead_of_moved = true;
}
if (next_card_idx != UNDEFINED)
{
/* Not calling sprite_object_set_focus() because focus is handled by
* cards_in_hand_update_loop() based on the selection grid value...
*/
play_sfx(
SFX_CARD_FOCUS,
MM_BASE_PITCH_RATE + rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
SFX_DEFAULT_VOLUME
);
}
}
return true;
}
static void game_playing_hand_row_on_key_transit(
SelectionGrid* selection_grid,
Selection* selection
)
{
if (key_hit(SELECT_CARD))
{
selection_hit_timer = timer;
}
else if (key_released(SELECT_CARD))
{
if (!moving_card && !card_selected_instead_of_moved)
{
hand_select_card(hand_sel_idx_to_card_idx(selection->x));
}
moving_card = false;
card_moved_too_fast = false;
card_selected_instead_of_moved = false;
selection_hit_timer = TM_ZERO;
}
else if (key_hit(DESELECT_CARDS))
{
hand_deselect_all_cards();
set_hand();
}
else if (key_hit(SORT_HAND))
{
hand_change_sort();
}
}
static int game_playing_button_row_get_size(void)
{
return NUM_ELEM_IN_ARR(game_playing_buttons);
}
static inline void game_playing_button_set_highlight(int btn_idx, bool highlight)
{
button_set_highlight(&game_playing_buttons[btn_idx], highlight);
}
static bool game_playing_button_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
)
{
// The selection grid system only guarantees that the new selection is within bounds
// but not the previous one...
// As of writing (PR #348), this check is not strictly needed for this row but it is
// left in, in case that ever changes. It can be reconsidered and removed.
if (prev_selection->y == row_idx && prev_selection->x >= 0 &&
prev_selection->x < game_playing_button_row_get_size())
{
game_playing_button_set_highlight(prev_selection->x, false);
}
if (new_selection->y == row_idx)
{
game_playing_button_set_highlight(new_selection->x, true);
}
return true;
}
static void game_playing_button_row_on_key_hit(SelectionGrid* selection_grid, Selection* selection)
{
if (key_hit(SELECT_CARD))
{
button_press(&game_playing_buttons[selection->x]);
} }
} }
@@ -1940,220 +2183,56 @@ static void hand_deselect_all_cards(void)
} }
} }
static inline void hand_change_sort(void) static void hand_change_sort(void)
{ {
sort_by_suit = !sort_by_suit; sort_by_suit = !sort_by_suit;
sort_cards(); sort_cards();
} }
static inline bool hand_can_play(void) static bool can_play_hand(void)
{ {
if (hand_state != HAND_SELECT || hand_selections == 0) if (hand_state != HAND_SELECT || hand_selections == 0)
return false; return false;
return true; return true;
} }
static inline void select_current_card(void) /**
* @brief Converts a selection index from the selection grid into a card index within the hand array
* @param selection_index The selection index from the selection grid.
* @return The index within the hand stack array.
* Note that the result is not valid if hand size is 0.
*/
static inline int hand_sel_idx_to_card_idx(int selection_index)
{ {
hand_toggle_card_selection(); // This is because the hand is drawn from right to left.
// There is no particular reason for why that was done, it's just how it was done.
// Maybe one day it can be reverted and made consistent so this conversion is not needed.
return hand_get_size() - selection_index - 1;
}
static void hand_select_card(int index)
{
if (index < 0 || index >= hand_get_size() || hand_state != HAND_SELECT || hand[index] == NULL)
return;
if (card_object_is_selected(hand[index]))
{
card_object_set_selected(hand[index], false);
hand_selections--;
play_sfx(SFX_CARD_DESELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
}
else if (hand_selections < MAX_SELECTION_SIZE)
{
card_object_set_selected(hand[index], true);
hand_selections++;
play_sfx(SFX_CARD_SELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
}
set_hand(); set_hand();
} }
// card moving logic
// true if and only if we are currently moving a card around
static bool moving_card = false;
// This will prevent us from moving cards around if we selected one
// by moving too fast after pressing the A button
static bool card_moved_too_fast = false;
static bool card_selected_instead_of_moved = false;
// After pressing A, if we press Left/Right too fast, we should select the card
// and change focus to the next one, instead of swapping them
// This should fix inputs sometimes not registering when quickly selecting cards
static const int card_swap_time_threshold = 6;
static uint selection_hit_timer = TM_ZERO;
static inline void game_playing_apply_card_movement_input(enum ScreenHorzDir move_dir)
{
// The reason why this adds +1 (-SCREEN_LEFT) is because the hand is drawn from right to left.
// There is no particular reason for this, it's just how I did it.
int next_card = selection_x - move_dir;
// Do not use FRAMES(x) here as we are counting real frames ignoring game speed
card_moved_too_fast = (timer - selection_hit_timer) < card_swap_time_threshold;
// swap cards around if A is held down when pressing D-pad keys
if (key_is_down(SELECT_CARD) && !card_moved_too_fast && !card_selected_instead_of_moved)
{
bool selection_not_at_border =
(move_dir == SCREEN_LEFT) ? selection_x < hand_top : selection_x > 0;
if (selection_not_at_border)
{
swap_cards_in_hand(selection_x, next_card);
moving_card = true;
reorder_card_sprites_layers();
hand_set_focus(next_card);
}
}
else
{
// select current card if we tried moving it too fast
if (key_released(SELECT_CARD) || (card_moved_too_fast && !moving_card))
{
select_current_card();
card_selected_instead_of_moved = true;
}
hand_set_focus(next_card);
}
}
static inline void game_playing_highlight_play_btn(void)
{
memset16(&pal_bg_mem[PLAY_HAND_BTN_BORDER_PID], HIGHLIGHT_COLOR, 1);
memcpy16(&pal_bg_mem[DISCARD_BTN_BORDER_PID], &pal_bg_mem[DISCARD_BTN_PID], 1);
}
static inline void game_playing_highlight_discard_btn(void)
{
memcpy16(&pal_bg_mem[PLAY_HAND_BTN_BORDER_PID], &pal_bg_mem[PLAY_HAND_BTN_PID], 1);
memset16(&pal_bg_mem[DISCARD_BTN_BORDER_PID], HIGHLIGHT_COLOR, 1);
}
static inline void game_playing_unhighlight_buttons(void)
{
memcpy16(&pal_bg_mem[PLAY_HAND_BTN_BORDER_PID], &pal_bg_mem[PLAY_HAND_BTN_PID], 1);
memcpy16(&pal_bg_mem[DISCARD_BTN_BORDER_PID], &pal_bg_mem[DISCARD_BTN_PID], 1);
}
static inline void game_playing_execute_hand_discard(void)
{
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
hand_state = HAND_DISCARD;
selection_x = 0;
selection_y = 0;
display_hands(--discards);
set_hand();
tte_printf(
"#{P:%d,%d; cx:0x%X000}%d",
DISCARDS_TEXT_RECT.left,
DISCARDS_TEXT_RECT.top,
TTE_RED_PB,
discards
);
}
static inline void game_playing_execute_hand_play(void)
{
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
hand_state = HAND_PLAY;
selection_x = 0;
selection_y = 0;
display_hands(--hands);
}
static inline void game_playing_process_hand_select_input(void) static inline void game_playing_process_hand_select_input(void)
{ {
// true = play button highlighted, false = discard button highlighted selection_grid_process_input(&game_playing_selection_grid);
static bool discard_button_highlighted = false;
if (key_hit(KEY_LEFT))
{
if (selection_y == GAME_PLAYING_HAND_SEL_Y)
{
game_playing_apply_card_movement_input(SCREEN_LEFT);
}
else if (selection_y == GAME_PLAYING_BUTTONS_SEL_Y)
{
discard_button_highlighted = false;
}
}
else if (key_hit(KEY_RIGHT))
{
if (selection_y == GAME_PLAYING_HAND_SEL_Y)
{
game_playing_apply_card_movement_input(SCREEN_RIGHT);
}
else if (selection_y == GAME_PLAYING_BUTTONS_SEL_Y)
{
discard_button_highlighted = true;
}
}
else if (key_hit(KEY_UP) && selection_y == GAME_PLAYING_BUTTONS_SEL_Y)
{
selection_y = GAME_PLAYING_HAND_SEL_Y;
}
else if (key_hit(KEY_DOWN) && selection_y == GAME_PLAYING_HAND_SEL_Y)
{
selection_y = GAME_PLAYING_BUTTONS_SEL_Y;
if (selection_x > hand_top / 2)
{
discard_button_highlighted = false; // Play button
}
else
{
discard_button_highlighted = true; // Discard button
}
}
else if (selection_y == GAME_PLAYING_BUTTONS_SEL_Y)
{
if (discard_button_highlighted == false) // Play button logic
{
game_playing_highlight_play_btn();
if (key_hit(SELECT_CARD) && hands > 0 && hand_can_play())
{
game_playing_execute_hand_play();
}
}
else
{
game_playing_highlight_discard_btn();
if (key_hit(SELECT_CARD) && discards > 0 && hand_can_discard())
{
game_playing_execute_hand_discard();
}
}
}
else if (selection_y == GAME_PLAYING_HAND_SEL_Y)
{
game_playing_unhighlight_buttons();
// Register timer when we hit A to pick a card
// If we try to move the picked card before card_swap_time_threshold frames,
// We will select it instead of moving it
if (key_hit(SELECT_CARD))
{
selection_hit_timer = timer;
}
// select card if we were not moving it around
else if (key_released(SELECT_CARD))
{
if (!moving_card && !card_selected_instead_of_moved)
{
select_current_card();
}
moving_card = false;
card_moved_too_fast = false;
card_selected_instead_of_moved = false;
selection_hit_timer = TM_ZERO;
}
if (key_hit(DESELECT_CARDS))
{
hand_deselect_all_cards();
set_hand();
}
}
if (key_hit(SORT_HAND))
{
hand_change_sort();
}
} }
static inline void card_draw(void) static inline void card_draw(void)
@@ -2181,17 +2260,6 @@ static inline void card_draw(void)
); );
} }
static inline void display_ante(int value)
{
tte_printf(
"#{P:%d,%d; cx:0xC000}%d#{cx:0xF000}/%d",
ANTE_TEXT_RECT.left,
ANTE_TEXT_RECT.top,
value,
MAX_ANTE
);
}
static inline void game_playing_handle_round_over(void) static inline void game_playing_handle_round_over(void)
{ {
enum GameState next_state = GAME_STATE_ROUND_END; enum GameState next_state = GAME_STATE_ROUND_END;
@@ -3087,8 +3155,52 @@ static inline void game_playing_discarded_cards_loop(void)
} }
} }
static inline void select_cards_in_played_hand()
{
switch (hand_type) // select the cards that apply to the hand type
{
case NONE:
break;
case HIGH_CARD:
select_highcard_cards_in_played_hand();
break;
case PAIR:
select_pair_cards_in_played_hand();
break;
case TWO_PAIR:
select_two_pair_cards_in_played_hand();
break;
case THREE_OF_A_KIND:
select_three_of_a_kind_cards_in_played_hand();
break;
case FOUR_OF_A_KIND:
select_four_of_a_kind_cards_in_played_hand();
break;
case STRAIGHT:
/* FALL THROUGH */
case FLUSH:
/* FALL THROUGH */
case STRAIGHT_FLUSH:
/* FALL THROUGH */
case ROYAL_FLUSH:
select_flush_and_straight_cards_in_played_hand();
break;
case FULL_HOUSE:
/* FALL THROUGH */
case FIVE_OF_A_KIND:
/* FALL THROUGH */
case FLUSH_HOUSE:
/* FALL THROUGH */
case FLUSH_FIVE: // Select all played cards in the hand
select_all_five_cards_in_played_hand();
break;
}
}
static inline void cards_in_hand_update_loop(void) static inline void cards_in_hand_update_loop(void)
{ {
int selected_card_idx = hand_sel_idx_to_card_idx(game_playing_selection_grid.selection.x);
// TODO: Break this function up into smaller ones, Gods be good // TODO: Break this function up into smaller ones, Gods be good
// Start from the end of the hand and work backwards because that's how Balatro does it // Start from the end of the hand and work backwards because that's how Balatro does it
for (int i = hand_top + 1; i >= 0; i--) for (int i = hand_top + 1; i >= 0; i--)
@@ -3105,7 +3217,9 @@ static inline void cards_in_hand_update_loop(void)
hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -HAND_SPACING_LUT[hand_top]; hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -HAND_SPACING_LUT[hand_top];
break; break;
case HAND_SELECT: case HAND_SELECT:
bool is_focused = (i == selection_x && selection_y == 0); bool is_focused =
(i == selected_card_idx &&
game_playing_selection_grid.selection.y == GAME_PLAYING_HAND_SEL_Y);
if (is_focused && !card_object_is_selected(hand[i])) if (is_focused && !card_object_is_selected(hand[i]))
{ {
@@ -3120,7 +3234,7 @@ static inline void cards_in_hand_update_loop(void)
hand_y -= int2fx(CARD_FOCUSED_SEL_Y); hand_y -= int2fx(CARD_FOCUSED_SEL_Y);
} }
if (i != selection_x && hand[i]->sprite_object->y > hand_y) if (i != selected_card_idx && hand[i]->sprite_object->y > hand_y)
{ {
hand[i]->sprite_object->y = hand_y; hand[i]->sprite_object->y = hand_y;
hand[i]->sprite_object->vy = 0; hand[i]->sprite_object->vy = 0;
@@ -3180,45 +3294,7 @@ static inline void cards_in_hand_update_loop(void)
timer = TM_ZERO; timer = TM_ZERO;
scored_card_index = played_top + 1; scored_card_index = played_top + 1;
switch (hand_type) // select the cards that apply to the hand type select_cards_in_played_hand();
{
case NONE:
break;
case HIGH_CARD:
select_highcard_cards_in_played_hand();
break;
case PAIR:
select_pair_cards_in_played_hand();
break;
case TWO_PAIR:
select_two_pair_cards_in_played_hand();
break;
case THREE_OF_A_KIND:
select_three_of_a_kind_cards_in_played_hand();
break;
case FOUR_OF_A_KIND:
select_four_of_a_kind_cards_in_played_hand();
break;
case STRAIGHT:
/* FALL THROUGH */
case FLUSH:
/* FALL THROUGH */
case STRAIGHT_FLUSH:
/* FALL THROUGH */
case ROYAL_FLUSH:
select_flush_and_straight_cards_in_played_hand();
break;
// ELSE FALL THROUGH
case FULL_HOUSE:
/* FALL THROUGH */
case FIVE_OF_A_KIND:
/* FALL THROUGH */
case FLUSH_HOUSE:
/* FALL THROUGH */
case FLUSH_FIVE: // Select all played cards in the hand
select_all_five_cards_in_played_hand();
break;
}
} }
break; break;
@@ -3892,7 +3968,7 @@ static int jokers_sel_row_get_size(void)
return list_get_len(&_owned_jokers_list); return list_get_len(&_owned_jokers_list);
} }
static void jokers_sel_row_on_selection_changed( static bool jokers_sel_row_on_selection_changed(
SelectionGrid* selection_grid, SelectionGrid* selection_grid,
int row_idx, int row_idx,
const Selection* prev_selection, const Selection* prev_selection,
@@ -3946,6 +4022,8 @@ static void jokers_sel_row_on_selection_changed(
(unsigned int)new_selection->x (unsigned int)new_selection->x
); );
} }
return true;
} }
static inline void joker_start_discard_animation(JokerObject* joker_object) static inline void joker_start_discard_animation(JokerObject* joker_object)
@@ -4069,7 +4147,7 @@ static void shop_top_row_on_key_transit(SelectionGrid* selection_grid, Selection
} }
} }
static void shop_top_row_on_selection_changed( static bool shop_top_row_on_selection_changed(
SelectionGrid* selection_grid, SelectionGrid* selection_grid,
int row_idx, int row_idx,
const Selection* prev_selection, const Selection* prev_selection,
@@ -4077,6 +4155,8 @@ static void shop_top_row_on_selection_changed(
) )
{ {
// The selection grid system only guarantees that the new selection is within bounds // 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 && if (prev_selection->y == row_idx && prev_selection->x >= 0 &&
prev_selection->x < shop_top_row_get_size()) prev_selection->x < shop_top_row_get_size())
{ {
@@ -4094,7 +4174,6 @@ static void shop_top_row_on_selection_changed(
int idx = prev_selection->x - 1; // -1 to account for next round button 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); JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_shop_jokers_list, idx);
sprite_object_set_focus(joker_object->sprite_object, false); sprite_object_set_focus(joker_object->sprite_object, false);
// -1 to account for next round button
} }
} }
@@ -4103,7 +4182,7 @@ static void shop_top_row_on_selection_changed(
if (new_selection->x == NEXT_ROUND_BTN_SEL_X) if (new_selection->x == NEXT_ROUND_BTN_SEL_X)
{ {
// Highlight next round button // Highlight next round button
memset16(&pal_bg_mem[NEXT_ROUND_BTN_SELECTED_BORDER_PID], HIGHLIGHT_COLOR, 1); memset16(&pal_bg_mem[NEXT_ROUND_BTN_SELECTED_BORDER_PID], BTN_HIGHLIGHT_COLOR, 1);
} }
else else
{ {
@@ -4112,6 +4191,8 @@ static void shop_top_row_on_selection_changed(
sprite_object_set_focus(joker_object->sprite_object, true); sprite_object_set_focus(joker_object->sprite_object, true);
} }
} }
return true;
} }
static int shop_reroll_row_get_size() static int shop_reroll_row_get_size()
@@ -4119,7 +4200,7 @@ static int shop_reroll_row_get_size()
return 1; // Only the reroll button return 1; // Only the reroll button
} }
static void shop_reroll_row_on_selection_changed( static bool shop_reroll_row_on_selection_changed(
SelectionGrid* selection_grid, SelectionGrid* selection_grid,
int row_idx, int row_idx,
const Selection* prev_selection, const Selection* prev_selection,
@@ -4133,8 +4214,10 @@ static void shop_reroll_row_on_selection_changed(
} }
else if (row_idx == new_selection->y) else if (row_idx == new_selection->y)
{ {
memset16(&pal_bg_mem[REROLL_BTN_SELECTED_BORDER_PID], HIGHLIGHT_COLOR, 1); memset16(&pal_bg_mem[REROLL_BTN_SELECTED_BORDER_PID], BTN_HIGHLIGHT_COLOR, 1);
} }
return true;
} }
static inline void game_shop_reroll(int* reroll_cost) static inline void game_shop_reroll(int* reroll_cost)
@@ -4203,6 +4286,7 @@ static void game_shop_process_user_input()
{ {
if (timer == TM_SHOP_PRC_INPUT_START) if (timer == TM_SHOP_PRC_INPUT_START)
{ {
// TODO: Move to on_init?
// The selection grid is initialized outside of bounds and moved // The selection grid is initialized outside of bounds and moved
// to trigger the selection change so the initial selection is visible // to trigger the selection change so the initial selection is visible
shop_selection_grid.selection = SHOP_INIT_SEL; shop_selection_grid.selection = SHOP_INIT_SEL;
@@ -4343,6 +4427,8 @@ static void game_shop_on_exit()
static void game_blind_select_on_init() static void game_blind_select_on_init()
{ {
change_background(BG_BLIND_SELECT); change_background(BG_BLIND_SELECT);
selection_x = 0;
selection_y = 0;
play_sfx(SFX_POP, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME); play_sfx(SFX_POP, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
} }
@@ -4533,7 +4619,6 @@ static void game_blind_select_handle_input()
if (selection_y == 0) if (selection_y == 0)
{ {
// 5 is the multiplier palette color and the skip button color
memset16(&pal_bg_mem[BLIND_SELECT_BTN_SELECTED_BORDER_PID], 0xFFFF, 1); memset16(&pal_bg_mem[BLIND_SELECT_BTN_SELECTED_BORDER_PID], 0xFFFF, 1);
memcpy16( memcpy16(
&pal_bg_mem[BLIND_SKIP_BTN_SELECTED_BORDER_PID], &pal_bg_mem[BLIND_SKIP_BTN_SELECTED_BORDER_PID],
@@ -4543,7 +4628,6 @@ static void game_blind_select_handle_input()
} }
else else
{ {
// 15 is the select button color
memcpy16( memcpy16(
&pal_bg_mem[BLIND_SELECT_BTN_SELECTED_BORDER_PID], &pal_bg_mem[BLIND_SELECT_BTN_SELECTED_BORDER_PID],
&pal_bg_mem[BLIND_SELECT_BTN_PID], &pal_bg_mem[BLIND_SELECT_BTN_PID],
@@ -4725,7 +4809,7 @@ static void game_main_menu_on_update()
if (selection_x == MAIN_MENU_PLAY_BTN_IDX) if (selection_x == MAIN_MENU_PLAY_BTN_IDX)
{ {
memset16(&pal_bg_mem[MAIN_MENU_PLAY_BUTTON_OUTLINE_PID], HIGHLIGHT_COLOR, 1); memset16(&pal_bg_mem[MAIN_MENU_PLAY_BUTTON_OUTLINE_PID], BTN_HIGHLIGHT_COLOR, 1);
if (key_hit(SELECT_CARD)) if (key_hit(SELECT_CARD))
{ {
+35 -7
View File
@@ -23,23 +23,36 @@ static void selection_grid_process_directional_input(SelectionGrid* selection_gr
void selection_grid_move_selection_horz(SelectionGrid* selection_grid, int direction_tribool) void selection_grid_move_selection_horz(SelectionGrid* selection_grid, int direction_tribool)
{ {
if (selection_grid == NULL) if (selection_grid == NULL || selection_grid->selection.y < 0 ||
selection_grid->selection.y >= selection_grid->num_rows)
{
return; return;
}
Selection new_selection = selection_grid->selection; Selection new_selection = selection_grid->selection;
new_selection.x += direction_tribool; new_selection.x += direction_tribool;
if (selection_grid->selection.y >= 0 && int row_size = selection_grid->rows[new_selection.y].get_size();
selection_grid->selection.y < selection_grid->num_rows && new_selection.x >= 0 && bool wrap_enabled = selection_grid->rows[new_selection.y].attributes.wrap;
new_selection.x < selection_grid->rows[new_selection.y].get_size())
if (wrap_enabled)
{ {
selection_grid->rows[new_selection.y].on_selection_changed( new_selection.x = wrap(new_selection.x, 0, row_size);
}
if (wrap_enabled || (new_selection.x >= 0 && new_selection.x < row_size))
{
bool proceed_selection = selection_grid->rows[new_selection.y].on_selection_changed(
selection_grid, selection_grid,
new_selection.y, new_selection.y,
&selection_grid->selection, &selection_grid->selection,
&new_selection &new_selection
); );
if (proceed_selection)
{
selection_grid->selection = new_selection; selection_grid->selection = new_selection;
} }
}
} }
void selection_grid_move_selection_vert(SelectionGrid* selection_grid, int direction_tribool) void selection_grid_move_selection_vert(SelectionGrid* selection_grid, int direction_tribool)
@@ -66,15 +79,30 @@ void selection_grid_move_selection_vert(SelectionGrid* selection_grid, int direc
// The operations are equivalent to fixed point if all the numbers were converted // The operations are equivalent to fixed point if all the numbers were converted
new_selection.x = fx2int(selection.x * ((int2fx(new_row_size) / old_row_size))); new_selection.x = fx2int(selection.x * ((int2fx(new_row_size) / old_row_size)));
bool proceed_selection = true;
if (selection.y >= 0 && selection.y < selection_grid->num_rows) if (selection.y >= 0 && selection.y < selection_grid->num_rows)
{ {
proceed_selection =
selection_grid->rows[selection.y] selection_grid->rows[selection.y]
.on_selection_changed(selection_grid, selection.y, &selection, &new_selection); .on_selection_changed(selection_grid, selection.y, &selection, &new_selection);
} }
selection_grid->rows[new_selection.y]
.on_selection_changed(selection_grid, new_selection.y, &selection, &new_selection); if (proceed_selection)
{
proceed_selection = selection_grid->rows[new_selection.y].on_selection_changed(
selection_grid,
new_selection.y,
&selection,
&new_selection
);
}
if (proceed_selection)
{
selection_grid->selection = new_selection; selection_grid->selection = new_selection;
} }
}
} }
void selection_grid_process_input(SelectionGrid* selection_grid) void selection_grid_process_input(SelectionGrid* selection_grid)