GBAlatro
A Demake of Balatro for the GBA
Loading...
Searching...
No Matches
round.c
Go to the documentation of this file.
1
6#include "game/round.h"
7
8#include "audio_utils.h"
9#include "background_gfx.h"
10#include "button.h"
11#include "game.h"
12#include "game/common_ui.h"
13#include "game/joker_row.h"
14#include "graphic_utils.h"
15#include "hand.h"
16#include "joker.h"
17#include "layout.h"
18#include "list.h"
19#include "selection_grid.h"
20#include "skip_tag.h"
21#include "soundbank.h"
22#include "timer.h"
23#include "util.h"
24
25#include <stdlib.h>
26#include <tonc.h>
27
28/*******************************************************************************
29 * CONSTS
30 ******************************************************************************/
31
32// Palette IDs
33#define PLAY_HAND_BTN_PAL_IDX 6
34#define PLAY_HAND_BTN_BORDER_PAL_IDX 7
35#define DISCARD_BTN_PAL_IDX 13
36#define DISCARD_BTN_BORDER_PAL_IDX 8
37#define SORT_BTNS_PAL_IDX 9
38#define SORT_BY_RANK_BTN_BORDER_PAL_IDX 22
39#define SORT_BY_SUIT_BTN_BORDER_PAL_IDX 23
40#define BLIND_BG_SHADOW_PAL_IDX 5
41#define BLIND_BG_SECONDARY_PAL_IDX 18
42#define BLIND_BG_PRIMARY_PAL_IDX 19
43
44// Naming the stage where cards return from the discard pile to the deck "undiscard"
45#define PITCH_STEP_DISCARD_SFX (-64)
46#define PITCH_STEP_DRAW_SFX 24
47#define PITCH_STEP_UNDISCARD_SFX (2 * PITCH_STEP_DRAW_SFX)
48
49/* This needs to stay a power of 2 and small enough
50 * for the lerping to be done before the next hand is drawn.
51 */
52#define NUM_SCORE_LERP_STEPS 16
53#define TM_SCORE_LERP_INTERVAL 2
54
55#define GAME_PLAYING_HAND_SEL_Y 1
56
57// Pixel sizes
58#define CARD_FOCUSED_UNSEL_Y 10
59#define CARD_UNFOCUSED_SEL_Y 15
60#define CARD_FOCUSED_SEL_Y 20
61#define SCORED_CARD_TEXT_Y 48
62#define JOKER_SCORE_TEXT_Y 48
63#define HELD_CARD_SCORE_TEXT_Y 108
64#define MAX_CARD_SCORE_STR_LEN 2
65
66// clang-format off
67
68// Flaming score animation frames
69#define SCORE_FLAMES_ANIM_FREQ 5 // animation will run at 12FPS
70#define NUM_SCORE_FLAMES_FRAMES 8 // Chips and Mult flame frames are next to one another
71#define SCORE_FLAME_FRAME_WIDTH 3 // so we only need to offset to get the next ones
72static const Rect SCORE_FLAME_RESET = {26, 20, 28, 20};
73static const Rect SCORE_FLAME_FRAMES_START = {26, 21, 28, 21};
74static const BG_POINT SCORE_FLAME_CHIPS_POS = {1, 9};
75static const BG_POINT SCORE_FLAME_MULT_POS = {5, 9};
76
77/* Contains the shop icon/current blind etc.
78 * The difference between TOP_LEFT_PANEL_ANIM_RECT and TOP_LEFT_PANEL_RECT
79 * is due to an overlap between the bottom of the top left panel
80 * and the top of the score panel in the tiles connecting them.
81 * TOP_LEFT_PANEL_ANIM_RECT should be used for animations,
82 * TOP_LEFT_PANEL_RECT for copies etc. but mind the overlap
83 */
84static const BG_POINT TOP_LEFT_BLIND_TITLE_POINT = {0, 21};
85static const Rect BIG_BLIND_TITLE_SRC_RECT = {0, 26, 8, 26 };
86static const Rect BOSS_BLIND_TITLE_SRC_RECT = {0, 27, 8, 27 };
87
88// Rects for TTE (in pixels)
89
90// The rect for popping menu animations (round end, shop, blinds)
91// - extends beyond the visible screen to the end of the screenblock
92// It includes both the target and source position rects.
93// This is because when popping, the target position is blank so we just animate
94// the whole rect so we don't have to track its position
95static const Rect HAND_BG_RECT_SELECTING = {9, 11, 24, 17 };
96static const Rect HAND_SIZE_RECT_SELECT = {120, 128, 160, 136 };
97static const Rect HAND_SIZE_RECT_PLAYING = {120, 152, 160, 160 };
98static const Rect PLAYED_CARDS_SCORES_RECT = {72, 48, 240, 56 };
99static const Rect HELD_CARDS_SCORES_RECT = {72, 108, 240, 116 };
100
101static const BG_POINT CARD_DRAW_POS = {208, 110};
102static const BG_POINT CARD_DISCARD_PNT = {240, 70};
103static const BG_POINT HAND_START_POS = {120, 90};
104static const BG_POINT HAND_PLAY_POS = {120, 70};
105// clang-format on
106
107/*******************************************************************************
108 * ROUND SELECTIONGRID
109 ******************************************************************************/
110
111static int round_button_row_get_size(void);
112static bool round_button_row_on_selection_changed(
113 SelectionGrid* selection_grid,
114 int row_idx,
115 const Selection* prev_selection,
116 const Selection* new_selection
117);
118static void round_button_row_on_key_hit(SelectionGrid* selection_grid, Selection* selection);
119
120static void round_hand_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection);
121
122static bool round_hand_row_on_selection_changed(
123 SelectionGrid* selection_grid,
124 int row_idx,
125 const Selection* prev_selection,
126 const Selection* new_selection
127);
128
129static int round_hand_row_get_size(void);
130
131static int hand_sel_idx_to_card_idx(int selection_index);
132
133// clang-format off
134static SelectionGridRow game_round_selection_rows[] = {
135 {
136 0,
140 {.wrap = false}
141 },
142 {
143 1,
144 round_hand_row_get_size,
145 round_hand_row_on_selection_changed,
146 round_hand_row_on_key_transit,
147 {.wrap = true}
148 },
149 {
150 2,
151 round_button_row_get_size,
152 round_button_row_on_selection_changed,
153 round_button_row_on_key_hit,
154 {.wrap = false}
155 }
156};
157// clang-format on
158
159static const Selection GAME_PLAYING_INIT_SEL = {0, 1};
160
161static SelectionGrid game_round_selection_grid = {
162 game_round_selection_rows,
163 NUM_ELEM_IN_ARR(game_round_selection_rows),
164 GAME_PLAYING_INIT_SEL
165};
166
167/*******************************************************************************
168 * ROUND BUTTONS
169 ******************************************************************************/
170
171static void round_discard_on_pressed(void);
172static void round_play_hand_on_pressed(void);
173static void round_sort_by_rank_on_pressed(void);
174static void round_sort_by_suit_on_pressed(void);
175
176static bool can_discard_hand(void);
177static bool can_play_hand(void);
178
179// clang-format off
180// Array of buttons by horizontal selection index (x)
181static Button game_round_buttons[] = {
182 {PLAY_HAND_BTN_BORDER_PAL_IDX, PLAY_HAND_BTN_PAL_IDX, round_play_hand_on_pressed, can_play_hand },
183 {SORT_BY_RANK_BTN_BORDER_PAL_IDX, SORT_BTNS_PAL_IDX, round_sort_by_rank_on_pressed, NULL },
184 {SORT_BY_SUIT_BTN_BORDER_PAL_IDX, SORT_BTNS_PAL_IDX, round_sort_by_suit_on_pressed, NULL },
185 {DISCARD_BTN_BORDER_PAL_IDX, DISCARD_BTN_PAL_IDX, round_discard_on_pressed, can_discard_hand},
186};
187// clang-format on
188
189/*******************************************************************************
190 * CARD PLAYING STATE
191 ******************************************************************************/
192
193enum PlayState
194{
195 PLAY_STARTING,
196 PLAY_BEFORE_SCORING,
197 PLAY_SCORING_CARDS,
198 PLAY_SCORING_CARD_JOKERS,
199 PLAY_SCORING_HELD_CARDS,
200 PLAY_SCORING_INDEPENDENT_JOKERS,
201 PLAY_SCORING_HAND_SCORED_END,
202 PLAY_ENDING,
203 PLAY_ENDED
204};
205
206static enum PlayState play_state = PLAY_STARTING;
207
208/*******************************************************************************
209 * INTERNAL VARIABLES
210 ******************************************************************************/
211
212// This is a stupid way to do this but I don't care
213static const int HAND_SPACING_LUT[MAX_HAND_SIZE] =
214 {28, 28, 28, 28, 27, 21, 18, 15, 13, 12, 10, 9, 9, 8, 8, 7};
215
216// Stacks
217static CardObject* s_played_hand[MAX_SELECTION_SIZE] = {NULL};
218static int s_played_top = -1;
219
220static Card* s_discard_pile[MAX_DECK_SIZE] = {NULL};
221static int s_discard_top = -1;
222
223// Keeping track of cards scored
224static int s_scored_card_index = 0;
225static bool s_retrigger = false;
226
227// Keeping track of what Jokers are scored at each step
228static ListItr s_joker_scored_itr;
229static ListItr s_joker_card_scored_end_itr;
230static ListItr s_joker_round_end_itr;
231
232static u32 s_temp_score = 0; // This is the score that shows in the same spot as the hand type.
233static bool s_score_flames_active = false;
234static FIXED s_lerped_score = 0;
235static FIXED s_lerped_temp_score = 0;
236
237// discarded cards specific
238static bool s_sound_played = false;
239static bool s_discarded_card = false;
240
241static int s_cards_drawn = 0;
242static int s_cards_discarded = 0;
243
244/*******************************************************************************
245 * UTILS FUNCTIONS
246 ******************************************************************************/
247
249{
250 return s_played_top;
251}
252
254{
255 return s_played_top + 1;
256}
257
263static inline void played_push(CardObject* card_object)
264{
265 if (s_played_top >= MAX_SELECTION_SIZE - 1)
266 return;
267 s_played_hand[++s_played_top] = card_object;
268}
269
271{
272 return s_discard_top;
273}
274
282static inline void discard_push(Card* card)
283{
284 if (s_discard_top >= MAX_DECK_SIZE - 1)
285 return;
286 s_discard_pile[++s_discard_top] = card;
287}
288
294static inline Card* discard_pop()
295{
296 if (s_discard_top < 0)
297 return NULL;
298 return s_discard_pile[s_discard_top--];
299}
300
302{
303 return s_scored_card_index;
304}
305
306void set_retrigger(bool new_retrigger)
307{
308 s_retrigger = new_retrigger;
309}
310
319static void get_played_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS])
320{
321 for (int i = 0; i < NUM_RANKS; i++)
322 ranks_out[i] = 0;
323 for (int i = 0; i < NUM_SUITS; i++)
324 suits_out[i] = 0;
325
326 for (int i = 0; i <= s_played_top; i++)
327 {
328 /* The difference from get_hand_distribution() (not checking if card is selected)
329 * is in line Balatro behavior,
330 * see https://github.com/GBALATRO/balatro-gba/issues/341#issuecomment-3691363488
331 */
332 if (!s_played_hand[i])
333 continue;
334 ranks_out[s_played_hand[i]->card->rank]++;
335 suits_out[s_played_hand[i]->card->suit]++;
336 }
337}
338
339/*******************************************************************************
340 * BACKGROUND MANIPULATION
341 ******************************************************************************/
342
344{
345 tte_erase_rect_wrapper(HAND_SIZE_RECT_PLAYING);
346 REG_WIN0V = (REG_WIN0V << 8) | 0x80; // Set window 0 top to 128
347
348 if (get_current_background() == BG_CARD_PLAYING)
349 {
350 int offset = 11;
351 memcpy16(
352 &se_mem[MAIN_BG_SBB][SE_ROW_LEN * offset],
353 &background_gfxMap[SE_ROW_LEN * offset],
354 SE_ROW_LEN * 8
355 );
356 }
357 else
358 {
359 toggle_windows(true, true); // Enable window 0 for the hand shadow
360
361 // Load the tiles and palette
362 // Background
363 GRIT_CPY(pal_bg_mem, background_gfxPal);
364 GRIT_CPY(&tile8_mem[MAIN_BG_CBB], background_gfxTiles);
365 GRIT_CPY(&se_mem[MAIN_BG_SBB], background_gfxMap);
366
367 if (g_game_vars.current_blind ==
368 BLIND_TYPE_BIG) // Change text and palette depending on blind type
369 {
370 main_bg_se_copy_rect(BIG_BLIND_TITLE_SRC_RECT, TOP_LEFT_BLIND_TITLE_POINT);
371 }
372 else if (g_game_vars.current_blind >= BLIND_TYPE_BOSS)
373 {
374 main_bg_se_copy_rect(BOSS_BLIND_TITLE_SRC_RECT, TOP_LEFT_BLIND_TITLE_POINT);
375 }
376
377 // Copies the Blind item into the top left panel
378 main_bg_se_copy_rect(TOP_LEFT_ITEM_SRC_RECT, TOP_LEFT_PANEL_POINT);
379
380 // This would change the palette of the background to match the blind, but the backgroun
381 // doesn't use the blind token's exact colors so a different approach is required
382 memset16(
383 &pal_bg_mem[BLIND_BG_PRIMARY_PAL_IDX],
384 blind_get_color(g_game_vars.current_blind, BLIND_BACKGROUND_MAIN_COLOR_INDEX),
385 1
386 );
387 memset16(
388 &pal_bg_mem[BLIND_BG_SECONDARY_PAL_IDX],
389 blind_get_color(g_game_vars.current_blind, BLIND_BACKGROUND_SECONDARY_COLOR_INDEX),
390 1
391 );
392 memset16(
393 &pal_bg_mem[BLIND_BG_SHADOW_PAL_IDX],
394 blind_get_color(g_game_vars.current_blind, BLIND_BACKGROUND_SHADOW_COLOR_INDEX),
395 1
396 );
397
398 for (int i = 0; i < NUM_ELEM_IN_ARR(game_round_buttons); i++)
399 {
400 button_set_highlight(&game_round_buttons[i], false);
401 }
402 }
403}
404
406{
407 if (get_current_background() != BG_CARD_SELECTING)
408 {
409 change_background(BG_CARD_SELECTING, false);
410 }
411
412 REG_WIN0V = (REG_WIN0V << 8) | 0xA0; // Set window 0 bottom to 160
413 toggle_windows(true, true);
414
415 for (int i = 0; i <= 2; i++)
416 {
417 main_bg_se_move_rect_1_tile_vert(HAND_BG_RECT_SELECTING, SCREEN_DOWN);
418 }
419
420 tte_erase_rect_wrapper(HAND_SIZE_RECT_SELECT);
421}
422
423/*******************************************************************************
424 * BUTTONS IMPLEMENTATION
425 ******************************************************************************/
426
432static bool can_discard_hand(void)
433{
434 return (
435 g_game_vars.discards > 0 && get_hand_state() == HAND_SELECT &&
437 );
438}
439
445static bool can_play_hand(void)
446{
447 return (
448 g_game_vars.hands > 0 && get_hand_state() == HAND_SELECT && hand_get_nb_selected_cards() > 0
449 );
450}
451
455static inline void round_execute_discard(void)
456{
457 if (!can_discard_hand())
458 return;
459
460 set_hand_state(HAND_DISCARD);
461 --g_game_vars.discards;
462 display_discards();
464}
465
470{
471 if (!can_discard_hand())
472 return;
473
475
476 // Move back to hand selection
477 selection_grid_move_selection_vert(&game_round_selection_grid, -1);
478}
479
484{
485 hand_change_sort(false);
486}
487
492{
493 hand_change_sort(true);
494}
495
499static inline void round_execute_play_hand(void)
500{
501 if (!can_play_hand())
502 return;
503
504 set_hand_state(HAND_PLAY);
505
506 --g_game_vars.hands;
507 display_hands();
508
509 g_game_vars.nb_played_hands[get_hand_type() - 1]++;
510}
511
516{
517 if (!can_play_hand())
518 return;
519
521
522 // Move back to hand selection
523 selection_grid_move_selection_vert(&game_round_selection_grid, -1);
524}
525
526static int round_hand_row_get_size(void)
527{
528 return hand_nb_held_cards();
529}
530
531/*******************************************************************************
532 * CARD MOVING LOGIC
533 ******************************************************************************/
534
535// true if and only if we are currently moving a card around
536static bool s_moving_card = false;
537
538// This will prevent us from moving cards around if we selected one
539// by moving too fast after pressing the A button
540static bool s_card_moved_too_fast = false;
541static bool s_card_selected_instead_of_moved = false;
542
543// After pressing A, if we press Left/Right too fast, we should select the card
544// and change focus to the next one, instead of swapping them
545// This should fix inputs sometimes not registering when quickly selecting cards
546static const int CARD_SWAP_TIME_THRESHOLD = 6;
547static int s_selection_hit_timer = UNDEFINED;
548
549static bool round_hand_row_on_selection_changed(
550 SelectionGrid* selection_grid,
551 int row_idx,
552 const Selection* prev_selection,
553 const Selection* new_selection
554)
555{
556 int prev_card_idx = UNDEFINED;
557 int next_card_idx = UNDEFINED;
558
559 // Do not use FRAMES(x) here as we are counting real frames ignoring game speed
560 s_card_moved_too_fast = (s_selection_hit_timer != UNDEFINED) &&
561 (g_game_vars.timer - s_selection_hit_timer) < CARD_SWAP_TIME_THRESHOLD;
562
563 if (prev_selection->y == GAME_PLAYING_HAND_SEL_Y)
564 {
565 prev_card_idx = hand_sel_idx_to_card_idx(prev_selection->x);
566 }
567
568 if (new_selection->y == GAME_PLAYING_HAND_SEL_Y)
569 {
570 next_card_idx = hand_sel_idx_to_card_idx(new_selection->x);
571 }
572
573 bool on_the_same_row = new_selection->y == prev_selection->y; // == GAME_PLAYING_HAND_SEL_Y
574
575 if (on_the_same_row && key_is_down(SELECT_CARD) && !s_card_moved_too_fast &&
576 !s_card_selected_instead_of_moved)
577 {
578 bool moved_by_one_tile = abs(new_selection->x - prev_selection->x) == 1;
579
580 // Avoid swapping when selection wraps
581 if (!moved_by_one_tile)
582 {
583 // Abort the selection if swapping so it doesn't wrap
584 return false;
585 }
586 else
587 {
588 swap_cards_in_hand(prev_card_idx, next_card_idx);
589 s_moving_card = true;
591
592 /* Not calling sprite_object_set_focus() because focus is handled by
593 * cards_in_hand_update_loop() based on the selection grid value...
594 */
595 play_sfx(
596 SFX_CARD_FOCUS,
597 MM_BASE_PITCH_RATE + rng_get_u32(RNG_SEQ_MISC) % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
599 );
600 }
601 }
602 else
603 {
604 // select current card if we tried moving it too fast
605 if (key_released(SELECT_CARD) || (s_card_moved_too_fast && !s_moving_card))
606 {
607 hand_select_card(prev_card_idx);
608 s_card_selected_instead_of_moved = true;
609 }
610 if (next_card_idx != UNDEFINED)
611 {
612 /* Not calling sprite_object_set_focus() because focus is handled by
613 * cards_in_hand_update_loop() based on the selection grid value...
614 */
615 play_sfx(
616 SFX_CARD_FOCUS,
617 MM_BASE_PITCH_RATE + rng_get_u32(RNG_SEQ_MISC) % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
619 );
620 }
621 }
622
623 return true;
624}
625
626static void round_hand_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection)
627{
628 if (key_hit(SELECT_CARD))
629 {
630 s_selection_hit_timer = g_game_vars.timer;
631 }
632 else if (key_released(SELECT_CARD))
633 {
634 if (!s_moving_card && !s_card_selected_instead_of_moved)
635 {
637 }
638 s_moving_card = false;
639 s_card_moved_too_fast = false;
640 s_card_selected_instead_of_moved = false;
641 s_selection_hit_timer = UNDEFINED;
642 }
643 else if (key_hit(DESELECT_CARDS))
644 {
647 }
648 else if (key_hit(PLAY_HAND_KEY))
649 {
651 }
652 else if (key_hit(DISCARD_HAND_KEY))
653 {
655 }
656}
657
658/*******************************************************************************
659 * OTHER SELECTIONGRID IMPLEMENTATION
660 ******************************************************************************/
661
662static int round_button_row_get_size(void)
663{
664 return NUM_ELEM_IN_ARR(game_round_buttons);
665}
666
667static inline void round_button_set_highlight(int btn_idx, bool highlight)
668{
669 button_set_highlight(&game_round_buttons[btn_idx], highlight);
670}
671
672static bool round_button_row_on_selection_changed(
673 SelectionGrid* selection_grid,
674 int row_idx,
675 const Selection* prev_selection,
676 const Selection* new_selection
677)
678{
679 // The selection grid system only guarantees that the new selection is within bounds
680 // but not the previous one...
681 // As of writing (PR #348), this check is not strictly needed for this row but it is
682 // left in, in case that ever changes. It can be reconsidered and removed.
683 if (prev_selection->y == row_idx && prev_selection->x >= 0 &&
684 prev_selection->x < round_button_row_get_size())
685 {
686 round_button_set_highlight(prev_selection->x, false);
687 }
688
689 if (new_selection->y == row_idx)
690 {
691 round_button_set_highlight(new_selection->x, true);
692 }
693
694 return true;
695}
696
697static void round_button_row_on_key_hit(SelectionGrid* selection_grid, Selection* selection)
698{
699 if (key_hit(SELECT_CARD))
700 {
701 button_press(&game_round_buttons[selection->x]);
702 }
703}
704
711static inline int hand_sel_idx_to_card_idx(int selection_index)
712{
713 // This is because the hand is drawn from right to left.
714 // There is no particular reason for why that was done, it's just how it was done.
715 // Maybe one day it can be reverted and made consistent so this conversion is not needed.
716 return hand_nb_held_cards() - selection_index - 1;
717}
718
719static inline void round_process_hand_select_input(void)
720{
721 selection_grid_process_input(&game_round_selection_grid);
722}
723
728static inline void round_handle_round_over(void)
729{
730 enum GameState next_state = GAME_STATE_ROUND_END;
731
732 if (g_game_vars.score >= blind_get_requirement(g_game_vars.current_blind, g_game_vars.ante))
733 {
734 if (g_game_vars.current_blind > BLIND_TYPE_BIG)
735 {
736 if (g_game_vars.ante < MAX_ANTE)
737 {
738 g_game_vars.ante++;
739 display_ante();
740
741 // mark current boss blind as beaten and allow for reroll
742 set_blind_beaten(g_game_vars.next_boss_blind);
743 }
744 else
745 {
746 next_state = GAME_STATE_WIN;
747 }
748 }
749 }
750 else if (g_game_vars.hands == 0)
751 {
752 next_state = GAME_STATE_LOSE;
753 }
754
755 game_change_state(next_state);
756}
757
758static inline void card_in_hand_loop_handle_discard_and_shuffling(
759 int card_idx,
760 FIXED* hand_x,
761 FIXED* hand_y,
762 bool* break_loop
763)
764{
765 if (get_hand_state() != HAND_DISCARD && get_hand_state() != HAND_SHUFFLING)
766 {
767 // Assumes hand_state is one of these
768 return;
769 }
770
771 CardObject** hand = get_hand_array();
772
773 *break_loop = false;
774 if (card_object_is_selected(hand[card_idx]) || get_hand_state() == HAND_SHUFFLING)
775 {
776 if (!s_discarded_card)
777 {
778 *hand_x = int2fx(CARD_DISCARD_PNT.x);
779 *hand_y = int2fx(CARD_DISCARD_PNT.y);
780
781 if (!s_sound_played)
782 {
783 play_sfx(
784 SFX_CARD_DRAW,
785 MM_BASE_PITCH_RATE + s_cards_discarded * PITCH_STEP_DISCARD_SFX,
787 );
788 s_sound_played = true;
789 }
790
791 if (hand[card_idx]->x >= *hand_x)
792 {
793 discard_push(hand[card_idx]->card);
794
795 // Remove discarded card from hand and shift the ones after it
796 card_object_destroy(&hand[card_idx]);
799
800 s_cards_discarded++;
801 s_sound_played = false;
802 g_game_vars.timer = TM_ZERO;
803
804 // Protect against an edge case where `card_idx == hand_top` before discarding. In
805 // that case, calling `reorder_card_sprites_layers` cannot shift the discarded card,
806 // and we end up with an element at `card_idx` that is still NULL.
807 if (hand[card_idx] != NULL)
808 {
809 *hand_y = hand[card_idx]->y;
810 *hand_x = hand[card_idx]->x;
811 }
812 }
813
814 s_discarded_card = true;
815 }
816 else
817 {
818 if (get_hand_state() == HAND_DISCARD)
819 {
820 // Don't raise the card if we're mass discarding, it looks stupid.
821 *hand_y -= int2fx(15);
822 }
823 else // hand_state == HAND_SHUFFLING
824 {
825 *hand_y += int2fx(24);
826 }
827 *hand_x = *hand_x + (int2fx(card_idx) - int2fx(get_hand_top()) / 2) *
828 -HAND_SPACING_LUT[get_hand_top()];
829 }
830 }
831 else
832 {
833 *hand_x = *hand_x + (int2fx(card_idx) - int2fx(get_hand_top()) / 2) *
834 -HAND_SPACING_LUT[get_hand_top()];
835 }
836
837 if (card_idx == 0 && s_discarded_card == false && g_game_vars.timer % FRAMES(10) == 0)
838 {
839 // This is never reached in the case of HAND_SHUFFLING. Not sure why but that's how it's
840 // supposed to be.
841 set_hand_state(HAND_DRAW);
842 s_sound_played = false;
843 s_cards_discarded = 0;
845 g_game_vars.timer = TM_ZERO;
846 *break_loop = true;
847 return;
848 };
849}
850
851static inline void select_flush_and_straight_cards_in_played_hand(void)
852{
853 // Special handling because Four Fingers might be active
854 bool final_selection[MAX_SELECTION_SIZE] = {false};
855
856 // Will be 4 if Four Fingers is in effect, otherwise 5
857 int min_len = get_straight_and_flush_size();
858
859 // if we have a flush in our hand
860 if (get_hand_type() == FLUSH || get_hand_type() == STRAIGHT_FLUSH ||
861 get_hand_type() == ROYAL_FLUSH)
862 {
863 bool flush_selection[MAX_HAND_SIZE] = {false};
864 find_flush_in_played_cards(s_played_hand, s_played_top, min_len, flush_selection);
865 // Add the results into the final selection
866 for (int i = 0; i <= s_played_top; i++)
867 {
868 final_selection[i] = flush_selection[i];
869 }
870 }
871
872 // If we have a straight in our hand
873 if (get_hand_type() == STRAIGHT || get_hand_type() == STRAIGHT_FLUSH ||
874 get_hand_type() == ROYAL_FLUSH)
875 {
876 bool straight_selection[MAX_HAND_SIZE] = {false};
877 find_straight_in_played_cards(s_played_hand, s_played_top, min_len, straight_selection);
878 // Add the results into the final selection
879 for (int i = 0; i <= s_played_top; i++)
880 {
881 final_selection[i] = final_selection[i] || straight_selection[i];
882 }
883 // If Four Fingers is active, pairs can happen in a valid straight
884 // If Four Fingers is not active, pairs are impossible so this will not affect things
885 select_paired_cards_in_hand(s_played_hand, s_played_top, final_selection);
886 }
887
888 // Finally, set mark the cards as selected based final_selection
889 for (int i = 0; i <= s_played_top; i++)
890 {
891 if (final_selection[i])
892 {
893 card_object_set_selected(s_played_hand[i], true);
894 }
895 }
896}
897
898static inline void select_all_five_cards_in_played_hand(void)
899{
900 for (int i = 0; i <= s_played_top; i++)
901 {
902 card_object_set_selected(s_played_hand[i], true);
903 }
904}
905
906static inline void select_four_of_a_kind_cards_in_played_hand(void)
907{
908 int played_size = get_played_size();
909
910 // find four cards with the same rank
911 // If there are 5 cards selected we just need to find the one card that doesn't match, and
912 // select the others
913 if (played_size >= 5)
914 {
915 int unmatched_index = -1;
916
917 for (int i = 0; i <= played_size; i++)
918 {
919 if (s_played_hand[i]->card->rank != s_played_hand[(i + 1) % played_size]->card->rank &&
920 s_played_hand[i]->card->rank != s_played_hand[(i + 2) % played_size]->card->rank)
921 {
922 unmatched_index = i;
923 break;
924 }
925 }
926
927 for (int i = 0; i <= played_size; i++)
928 {
929 if (i != unmatched_index)
930 {
931 card_object_set_selected(s_played_hand[i], true);
932 }
933 }
934 }
935 else // If there are only 4 cards selected we know they match
936 {
937 for (int i = 0; i <= played_size; i++)
938 {
939 card_object_set_selected(s_played_hand[i], true);
940 }
941 }
942}
943
944static inline void select_three_of_a_kind_cards_in_played_hand(void)
945{
946 // find three cards with the same rank
947 for (int i = 0; i <= s_played_top - 1; i++)
948 {
949 for (int j = i + 1; j <= s_played_top; j++)
950 {
951 if (s_played_hand[i]->card->rank == s_played_hand[j]->card->rank)
952 {
953 card_object_set_selected(s_played_hand[i], true);
954 card_object_set_selected(s_played_hand[j], true);
955
956 for (int k = j + 1; k <= s_played_top; k++)
957 {
958 if (s_played_hand[i]->card->rank == s_played_hand[k]->card->rank &&
959 !card_object_is_selected(s_played_hand[k]))
960 {
961 card_object_set_selected(s_played_hand[k], true);
962 break;
963 }
964 }
965
966 break;
967 }
968 }
969
970 if (card_object_is_selected(s_played_hand[i]))
971 break;
972 }
973}
974
975static inline void select_two_pair_cards_in_played_hand(void)
976{
977 // find two pairs of cards with the same rank
978 int i;
979
980 for (i = 0; i <= s_played_top - 1; i++)
981 {
982 for (int j = i + 1; j <= s_played_top; j++)
983 {
984 if (s_played_hand[i]->card->rank == s_played_hand[j]->card->rank)
985 {
986 card_object_set_selected(s_played_hand[i], true);
987 card_object_set_selected(s_played_hand[j], true);
988
989 break;
990 }
991 }
992
993 if (card_object_is_selected(s_played_hand[i]))
994 break;
995 }
996
997 for (; i <= s_played_top - 1; i++) // Find second pair
998 {
999 for (int j = i + 1; j <= s_played_top; j++)
1000 {
1001 if (s_played_hand[i]->card->rank == s_played_hand[j]->card->rank &&
1002 !card_object_is_selected(s_played_hand[i]) &&
1003 !card_object_is_selected(s_played_hand[j]))
1004 {
1005 card_object_set_selected(s_played_hand[i], true);
1006 card_object_set_selected(s_played_hand[j], true);
1007 break;
1008 }
1009 }
1010 }
1011}
1012
1013static inline void select_pair_cards_in_played_hand(void)
1014{
1015 // find two cards with the same rank
1016 for (int i = 0; i <= s_played_top - 1; i++)
1017 {
1018 for (int j = i + 1; j <= s_played_top; j++)
1019 {
1020 if (s_played_hand[i]->card->rank == s_played_hand[j]->card->rank)
1021 {
1022 card_object_set_selected(s_played_hand[i], true);
1023 card_object_set_selected(s_played_hand[j], true);
1024 break;
1025 }
1026 }
1027
1028 if (card_object_is_selected(s_played_hand[i]))
1029 break;
1030 }
1031}
1032
1033static inline void select_highcard_cards_in_played_hand(void)
1034{
1035 // find the card with the highest rank in the hand
1036 int highest_rank_index = 0;
1037
1038 for (int i = 0; i <= s_played_top; i++)
1039 {
1040 if (s_played_hand[i]->card->rank > s_played_hand[highest_rank_index]->card->rank)
1041 {
1042 highest_rank_index = i;
1043 }
1044 }
1045
1046 card_object_set_selected(s_played_hand[highest_rank_index], true);
1047}
1048
1054static inline bool round_is_over(void)
1055{
1056 return g_game_vars.hands == 0 ||
1057 g_game_vars.score >= blind_get_requirement(g_game_vars.current_blind, g_game_vars.ante);
1058}
1059
1060static inline void round_process_input_and_state(void)
1061{
1062 if (get_hand_state() == HAND_TAGS)
1063 {
1064 // We're checking for scoring Tags in a general way here, but only the Juggler can
1065 // really apply on round start
1066 if (skip_tag_process_get_proc_stage() == SKIP_TAG_PROCESS_STAGE_END)
1067 {
1068 set_hand_state(HAND_DRAW);
1069 g_game_vars.timer = TM_ZERO;
1070 }
1071 }
1072 else if (get_hand_state() == HAND_SELECT)
1073 {
1074 round_process_hand_select_input();
1075 }
1076 else if (play_state == PLAY_ENDING)
1077 {
1078 if (g_game_vars.mult > 0)
1079 {
1080 // protect against score overflow
1081 s_temp_score = u32_protected_mult(g_game_vars.chips, g_game_vars.mult);
1082 s_lerped_temp_score = int2fx(s_temp_score);
1083 s_lerped_score = int2fx(g_game_vars.score);
1084
1085 if (s_temp_score > g_game_vars.best_hand_score)
1086 g_game_vars.best_hand_score = s_temp_score;
1087
1088 display_temp_score(s_temp_score);
1089
1090 g_game_vars.chips = 0;
1091 g_game_vars.mult = 0;
1092 display_mult();
1093 display_chips();
1094
1095 static const int SCORE_CALC_SFX_PITCH_SHIFT = -102; // -10% OF MM_BASE_PITCH_RATE
1096 static const int SCORE_CALC_SFX_VOLUME = 204; // 80% MM_SFX_FULL_VOLUME
1097
1098 // The chips calculation SFX is the same as button
1099 play_sfx(
1100 SFX_BUTTON,
1101 MM_BASE_PITCH_RATE + SCORE_CALC_SFX_PITCH_SHIFT,
1102 SCORE_CALC_SFX_VOLUME
1103 );
1104 }
1105 }
1106 else if (play_state == PLAY_ENDED && g_game_vars.timer % FRAMES(TM_SCORE_LERP_INTERVAL) == 0)
1107 {
1108 /* Using fixed point in case the score is lower than NUM_SCORE_LERP_STEPS and then
1109 * then the division rounds it down to 0 and it's never added to the total.
1110 * The operation is equivalent to
1111 * fxdiv(int2fx(temp_score * g_game_vars.game_speed), int2fx(NUM_SCORE_LERP_STEPS))
1112 */
1113 s_lerped_temp_score -= int2fx(s_temp_score * g_game_vars.game_speed) / NUM_SCORE_LERP_STEPS;
1114 s_lerped_score += int2fx(s_temp_score * g_game_vars.game_speed) / NUM_SCORE_LERP_STEPS;
1115
1116 if (s_lerped_temp_score > 0)
1117 {
1118 // Set the score display first because it's more important
1119 // in case there isn't enough time within the frame to display both
1120 display_score(fx2uint(s_lerped_score));
1121 display_temp_score(fx2uint(s_lerped_temp_score));
1122 }
1123 else
1124 {
1125 g_game_vars.score = u32_protected_add(g_game_vars.score, s_temp_score);
1126 s_temp_score = 0;
1127 s_lerped_temp_score = 0;
1128 s_lerped_score = 0;
1129
1130 erase_temp_score();
1131 display_score(g_game_vars.score);
1132 }
1133 }
1134}
1135
1140static inline void card_draw(void)
1141{
1142 if (get_deck_top() < 0 || get_hand_top() >= g_game_vars.hand_size - 1 ||
1143 get_hand_top() >= MAX_HAND_SIZE - 1)
1144 return;
1145
1146 CardObject* card_object = card_object_new(deck_pop());
1147
1148 const FIXED deck_x = int2fx(CARD_DRAW_POS.x);
1149 const FIXED deck_y = int2fx(CARD_DRAW_POS.y);
1150
1151 card_object->x = deck_x;
1152 card_object->y = deck_y;
1153
1155 get_hand_array()[get_hand_top()] = card_object;
1156
1157 // Sort the hand after drawing a card
1158 sort_cards();
1159
1160 play_sfx(
1161 SFX_CARD_DRAW,
1162 MM_BASE_PITCH_RATE + s_cards_drawn * PITCH_STEP_DRAW_SFX,
1164 );
1165}
1166
1167static inline void round_process_card_draw(void)
1168{
1169 if (get_hand_state() == HAND_DRAW && s_cards_drawn < g_game_vars.hand_size)
1170 {
1171 if (g_game_vars.timer % FRAMES(10) == 0) // Draw a card every 10 frames
1172 {
1173 s_cards_drawn++;
1174 card_draw();
1175 }
1176 }
1177 else if (get_hand_state() == HAND_DRAW)
1178 {
1179 set_hand_state(HAND_SELECT); // Change the hand state to select after drawing all the cards
1180 s_cards_drawn = 0;
1181 g_game_vars.timer = TM_ZERO;
1182 }
1183}
1184
1185static inline void round_discarded_cards_loop(void)
1186{
1187 // Discarded cards loop (mainly for shuffling)
1188 if (hand_nb_held_cards() == 0 && get_hand_state() == HAND_SHUFFLING && s_discard_top >= -1 &&
1189 g_game_vars.timer > FRAMES(10))
1190 {
1191 // Change the background to the round end background. This is how it works in Balatro, so
1192 // I'm doing it this way too.
1193 change_background(BG_ROUND_END, false);
1194
1195 // We take each discarded card and put it back into the deck with a short animation
1196 static CardObject* discarded_card_object = NULL;
1197 if (s_discard_top >= 0 && discarded_card_object == NULL)
1198 {
1199 discarded_card_object = card_object_new(discard_pop());
1200
1201 // Set the sprite for the discarded card object. Reuse Id 0 to save a sprite
1202 card_object_set_sprite(discarded_card_object, 0);
1203 sprite_object_reset_transform((SpriteObject*)discarded_card_object);
1204
1205 discarded_card_object->tx = int2fx(204);
1206 discarded_card_object->ty = int2fx(112);
1207 discarded_card_object->x = int2fx(240);
1208 discarded_card_object->y = int2fx(80);
1209 }
1210 else
1211 {
1212 if (discarded_card_object->y >= discarded_card_object->ty)
1213 {
1214 deck_push(discarded_card_object->card); // Put the card back into the deck
1215 card_object_destroy(&discarded_card_object);
1216
1217 play_sfx(
1218 SFX_CARD_DRAW,
1219 MM_BASE_PITCH_RATE + PITCH_STEP_UNDISCARD_SFX,
1221 );
1222 }
1223 }
1224
1225 // If there are no more discarded cards, stop shuffling
1226 if (s_discard_top == -1 && discarded_card_object == NULL)
1227 {
1228 // After HAND_SHUFFLING the round is over
1230 }
1231 }
1232}
1233
1234static inline void select_cards_in_played_hand(void)
1235{
1236 switch (get_hand_type()) // select the cards that apply to the hand type
1237 {
1238 case NONE:
1239 break;
1240 case HIGH_CARD:
1241 select_highcard_cards_in_played_hand();
1242 break;
1243 case PAIR:
1244 select_pair_cards_in_played_hand();
1245 break;
1246 case TWO_PAIR:
1247 select_two_pair_cards_in_played_hand();
1248 break;
1249 case THREE_OF_A_KIND:
1250 select_three_of_a_kind_cards_in_played_hand();
1251 break;
1252 case FOUR_OF_A_KIND:
1253 select_four_of_a_kind_cards_in_played_hand();
1254 break;
1255 case STRAIGHT:
1256 /* FALL THROUGH */
1257 case FLUSH:
1258 /* FALL THROUGH */
1259 case STRAIGHT_FLUSH:
1260 /* FALL THROUGH */
1261 case ROYAL_FLUSH:
1262 select_flush_and_straight_cards_in_played_hand();
1263 break;
1264 case FULL_HOUSE:
1265 /* FALL THROUGH */
1266 case FIVE_OF_A_KIND:
1267 /* FALL THROUGH */
1268 case FLUSH_HOUSE:
1269 /* FALL THROUGH */
1270 case FLUSH_FIVE: // Select all played cards in the hand
1271 select_all_five_cards_in_played_hand();
1272 break;
1273 }
1274}
1275
1276static inline void cards_in_hand_update_loop(void)
1277{
1278 int selected_card_idx = hand_sel_idx_to_card_idx(game_round_selection_grid.selection.x);
1279
1280 // TODO: Break this function up into smaller ones, Gods be good
1281 // Start from the end of the hand and work backwards because that's how Balatro does it
1282 CardObject** hand = get_hand_array();
1283
1284 for (int i = get_hand_top(); i >= 0; i--)
1285 {
1286 if (hand[i] != NULL)
1287 {
1288 FIXED hand_x = int2fx(HAND_START_POS.x);
1289 FIXED hand_y = int2fx(HAND_START_POS.y);
1290
1291 switch (get_hand_state())
1292 {
1293 case HAND_TAGS:
1294 // Nothing to do here
1295 break;
1296 case HAND_DRAW:
1297 hand_x = hand_x + (int2fx(i) - int2fx(get_hand_top()) / 2) *
1298 -HAND_SPACING_LUT[get_hand_top()];
1299 break;
1300 case HAND_SELECT:
1301 bool is_focused =
1302 (i == selected_card_idx &&
1303 game_round_selection_grid.selection.y == GAME_PLAYING_HAND_SEL_Y);
1304
1305 if (is_focused && !card_object_is_selected(hand[i]))
1306 {
1307 hand_y -= int2fx(CARD_FOCUSED_UNSEL_Y);
1308 }
1309 else if (!is_focused && card_object_is_selected(hand[i]))
1310 {
1311 hand_y -= int2fx(CARD_UNFOCUSED_SEL_Y);
1312 }
1313 else if (is_focused && card_object_is_selected(hand[i]))
1314 {
1315 hand_y -= int2fx(CARD_FOCUSED_SEL_Y);
1316 }
1317 if (i != selected_card_idx && hand[i]->y > hand_y)
1318 {
1319 hand[i]->y = hand_y;
1320 // Set target y to match y. Ensures target is updated even when vy becomes
1321 // 0, preventing immediate snap back.
1322 hand[i]->ty = hand_y;
1323 hand[i]->vy = 0;
1324 }
1325
1326 hand_x = hand_x + (int2fx(i) - int2fx(get_hand_top()) / 2) *
1327 -HAND_SPACING_LUT[get_hand_top()]; // TODO: Change this
1328 // later to reference a
1329 // 2D LUT of positions
1330 break;
1331 case HAND_SHUFFLING:
1332 /* FALL THROUGH */
1333 case HAND_DISCARD: // TODO: Add sound
1334 bool break_loop;
1335 card_in_hand_loop_handle_discard_and_shuffling(
1336 i,
1337 &hand_x,
1338 &hand_y,
1339 &break_loop
1340 );
1341 if (break_loop)
1342 break;
1343
1344 break;
1345 case HAND_PLAY:
1346 hand_x = hand_x + (int2fx(i) - int2fx(get_hand_top()) / 2) *
1347 -HAND_SPACING_LUT[get_hand_top()];
1348 hand_y += int2fx(24);
1349
1350 if (card_object_is_selected(hand[i]) && s_discarded_card == false &&
1351 g_game_vars.timer % FRAMES(10) == 0)
1352 {
1353 card_object_set_selected(hand[i], false);
1354 played_push(hand[i]);
1355 sprite_destroy(&hand[i]->sprite);
1356 hand[i] = NULL;
1358
1359 play_sfx(
1360 SFX_CARD_DRAW,
1361 MM_BASE_PITCH_RATE + s_cards_drawn * PITCH_STEP_DISCARD_SFX,
1363 );
1364
1367 s_cards_drawn++;
1368
1369 s_discarded_card = true;
1370 }
1371
1372 if (i == 0 && s_discarded_card == false && g_game_vars.timer % FRAMES(10) == 0)
1373 {
1374 set_hand_state(HAND_PLAYING);
1375 s_cards_drawn = 0;
1377 g_game_vars.timer = TM_ZERO;
1378 s_scored_card_index = get_played_size();
1379
1380 select_cards_in_played_hand();
1381 }
1382
1383 break;
1384 case HAND_PLAYING:
1385 // Don't need to do anything here, just wait for the player to select cards
1386 hand_x = hand_x + (int2fx(i) - int2fx(get_hand_top()) / 2) *
1387 -HAND_SPACING_LUT[get_hand_top()];
1388 hand_y += int2fx(24);
1389 break;
1390 }
1391
1392 hand[i]->tx = hand_x;
1393 hand[i]->ty = hand_y;
1394 }
1395 }
1396}
1397
1398static inline void round_ui_text_update(void)
1399{
1400 static int s_last_hand_size = UNDEFINED;
1401 static int s_last_hand_max_size = UNDEFINED;
1402 static int s_last_deck_size = UNDEFINED;
1403
1404 if (g_game_vars.timer == 1 || s_last_hand_size != hand_nb_held_cards() ||
1405 s_last_hand_max_size != g_game_vars.hand_size || s_last_deck_size != deck_get_size())
1406 {
1407 // TODO: Clean this code up a bit
1408 // Print hand size/max size at correct height depending on background state
1409 switch (get_current_background())
1410 {
1411 case BG_CARD_SELECTING:
1412 tte_printf(
1413 "#{P:%d,%d; cx:0x%X000}%2d/%-2ld",
1414 HAND_SIZE_RECT_SELECT.left,
1415 HAND_SIZE_RECT_SELECT.top,
1416 TTE_WHITE_PB,
1418 g_game_vars.hand_size
1419 );
1420 break;
1421
1422 case BG_CARD_PLAYING:
1423 tte_printf(
1424 "#{P:%d,%d; cx:0x%X000}%2d/%-2ld",
1425 HAND_SIZE_RECT_PLAYING.left,
1426 HAND_SIZE_RECT_PLAYING.top,
1427 TTE_WHITE_PB,
1429 g_game_vars.hand_size
1430 );
1431 break;
1432
1433 default:
1434 break;
1435 }
1436
1437 // Deck size/max size
1438 display_deck_size_max();
1439
1440 s_last_hand_size = hand_nb_held_cards();
1441 s_last_hand_max_size = g_game_vars.hand_size;
1442 s_last_deck_size = deck_get_size();
1443 }
1444}
1445
1447{
1448 u32 curr_score = u32_protected_mult(g_game_vars.chips, g_game_vars.mult);
1449 u32 required_score = blind_get_requirement(g_game_vars.current_blind, g_game_vars.ante);
1450 if (curr_score >= required_score && !s_score_flames_active)
1451 {
1452 // start flaming score
1453 s_score_flames_active = true;
1454 return;
1455 }
1456 if (curr_score < required_score && s_score_flames_active)
1457 {
1458 // stop flaming score and clear rect
1459 s_score_flames_active = false;
1460
1461 Rect reset_rect = SCORE_FLAME_RESET;
1462 main_bg_se_copy_rect(reset_rect, SCORE_FLAME_CHIPS_POS);
1463 reset_rect.left += SCORE_FLAME_FRAME_WIDTH;
1464 reset_rect.right += SCORE_FLAME_FRAME_WIDTH;
1465 main_bg_se_copy_rect(reset_rect, SCORE_FLAME_MULT_POS);
1466 }
1467}
1468
1469static inline void round_process_flaming_score(void)
1470{
1471 static u8 flame_score_frame = 0;
1472
1473 if (s_score_flames_active)
1474 {
1475 if (g_game_vars.timer % SCORE_FLAMES_ANIM_FREQ == 0)
1476 {
1477 Rect frame_rect = SCORE_FLAME_FRAMES_START;
1478 flame_score_frame = (flame_score_frame + 1) % NUM_SCORE_FLAMES_FRAMES;
1479
1480 // chips flame (blue)
1481 frame_rect.top += flame_score_frame;
1482 frame_rect.bottom += flame_score_frame;
1483 main_bg_se_copy_rect(frame_rect, SCORE_FLAME_CHIPS_POS);
1484
1485 // mult flame (red)
1486 frame_rect.left += SCORE_FLAME_FRAME_WIDTH;
1487 frame_rect.right += SCORE_FLAME_FRAME_WIDTH;
1488 main_bg_se_copy_rect(frame_rect, SCORE_FLAME_MULT_POS);
1489 }
1490 }
1491}
1492
1493/*******************************************************************************
1494 * CARD/JOKER SCORING LOGIC
1495 ******************************************************************************/
1496
1497static void joker_scoring_increment_text_cursor(int* cursor_pos_x)
1498{
1499 GBAL_RETURN_IF_NULL(cursor_pos_x, RET_NONE);
1500
1501 // + 1 For space
1502 const int joker_score_display_offset_px = (MAX_CARD_SCORE_STR_LEN + 1) * TTE_CHAR_SIZE;
1503 *cursor_pos_x += joker_score_display_offset_px;
1504}
1505
1506// This scores the joker and returns true if it was scored successfully
1507// card_object = NULL means the joker_event does not concern a particular Card, i.e. Independend or
1508// On_Blind_Selected as opposed to events that concern a particular card, i.e. On_Card_Scored or
1509// On_Card_Held
1510static bool joker_object_score(
1511 JokerObject* joker_object,
1512 CardObject* card_object,
1513 enum JokerEvent joker_event
1514)
1515{
1516 GBAL_RETURN_IF_NULL(joker_object, false);
1517
1518 JokerEffect* joker_effect = NULL;
1519 u32 effect_flags_ret =
1520 joker_get_score_effect(joker_object->joker, card_object->card, joker_event, &joker_effect);
1521
1522 if (effect_flags_ret == JOKER_EFFECT_FLAG_NONE)
1523 {
1524 return false;
1525 }
1526
1527 if (effect_flags_ret & JOKER_EFFECT_FLAG_RETRIGGER)
1528 {
1529 set_retrigger(joker_effect->retrigger);
1530 }
1531
1532 int cursorPosX = TILE_SIZE; // Offset of one tile to better center the text on the card
1533 int cursorPosY = 0;
1534 if (joker_event == JOKER_EVENT_ON_CARD_HELD)
1535 {
1536 // display the text on top of the card instead of below the Joker for Held Cards effects
1537 // scored_card cannot be NULL here because of the joker event
1538 cursorPosX += fx2int(card_object->x);
1539 cursorPosY = HELD_CARD_SCORE_TEXT_Y;
1540 }
1541 else
1542 {
1543 cursorPosX += fx2int(joker_object->x);
1544 cursorPosY = JOKER_SCORE_TEXT_Y;
1545 }
1546
1547 mm_word sfx_id;
1548 if (effect_flags_ret & JOKER_EFFECT_FLAG_CHIPS)
1549 {
1550 g_game_vars.chips = u32_protected_add(g_game_vars.chips, joker_effect->chips);
1551 tte_printf(
1552 "#{P:%d,%d; cx:0x%X000;}+%lu",
1553 cursorPosX,
1554 cursorPosY,
1555 TTE_BLUE_PB,
1556 joker_effect->chips
1557 );
1558 joker_scoring_increment_text_cursor(&cursorPosX);
1559 sfx_id = SFX_CHIPS_GENERIC; // The joker chips effect is "generic"
1560 }
1561 if (effect_flags_ret & JOKER_EFFECT_FLAG_MULT)
1562 {
1563 g_game_vars.mult = u32_protected_add(g_game_vars.mult, joker_effect->mult);
1564 tte_printf(
1565 "#{P:%d,%d; cx:0x%X000;}+%lu",
1566 cursorPosX,
1567 cursorPosY,
1568 TTE_RED_PB,
1569 joker_effect->mult
1570 );
1571 joker_scoring_increment_text_cursor(&cursorPosX);
1572 sfx_id = SFX_MULT;
1573 }
1574 // if xmult is zero, DO NOT multiply by it
1575 if (effect_flags_ret & JOKER_EFFECT_FLAG_XMULT && joker_effect->xmult > 0)
1576 {
1577 g_game_vars.mult = u32_protected_mult(g_game_vars.mult, joker_effect->xmult);
1578 tte_printf(
1579 "#{P:%d,%d; cx:0x%X000;}X%lu",
1580 cursorPosX,
1581 cursorPosY,
1582 TTE_RED_PB,
1583 joker_effect->xmult
1584 );
1585 joker_scoring_increment_text_cursor(&cursorPosX);
1586 sfx_id = SFX_XMULT;
1587 }
1588 if (effect_flags_ret & JOKER_EFFECT_FLAG_MONEY)
1589 {
1590 g_game_vars.money += joker_effect->money;
1591 tte_printf(
1592 "#{P:%d,%d; cx:0x%X000;}%d$",
1593 cursorPosX,
1594 cursorPosY,
1595 TTE_YELLOW_PB,
1596 joker_effect->money
1597 );
1598 joker_scoring_increment_text_cursor(&cursorPosX);
1599 // TODO: Money sound effect
1600 }
1601 // custom message for Jokers (including retriggers where Jokers will say "Again!")
1602 // joker_effect->message will have been set if the Joker had anything custom to say
1603 if (effect_flags_ret & JOKER_EFFECT_FLAG_MESSAGE)
1604 {
1605 tte_printf(
1606 "#{P:%d,%d; cx:0x%X000;}%s",
1607 cursorPosX,
1608 cursorPosY,
1609 TTE_WHITE_PB,
1610 joker_effect->message
1611 );
1612 }
1613 // this will start the Joker expire animation
1614 if (effect_flags_ret & JOKER_EFFECT_FLAG_EXPIRE && joker_effect->expire)
1615 {
1616 joker_object_shake(joker_object, UNDEFINED);
1617 list_push_back(get_expired_jokers_list(), joker_object);
1618 }
1619
1620 // Update displays
1621 display_chips();
1622 display_mult();
1623 display_money();
1624
1625 joker_object_shake(joker_object, sfx_id);
1626
1627 return true;
1628}
1629
1641 ListItr* starting_joker_itr,
1642 CardObject* card_object,
1643 enum JokerEvent joker_event
1644)
1645{
1646 JokerObject* joker;
1647
1648 while ((joker = list_itr_next(starting_joker_itr)))
1649 {
1650 if (joker_object_score(joker, card_object, joker_event))
1651 {
1652 return true;
1653 }
1654 }
1655 return false;
1656}
1657
1658static inline void play_starting_played_cards_update(int played_idx)
1659{
1660 // Protect against out of bounds read in `s_played_hand` array
1661 bool card_selected =
1662 (s_played_top < s_scored_card_index)
1663 ? false
1664 : card_object_is_selected(s_played_hand[s_played_top - s_scored_card_index]);
1665
1666 if (played_idx == s_played_top && (g_game_vars.timer % FRAMES(10) == 0 || !card_selected) &&
1667 g_game_vars.timer > FRAMES(40))
1668 {
1669 s_scored_card_index--;
1670
1671 if (s_scored_card_index == 0)
1672 {
1673 s_joker_scored_itr = list_itr_create(get_jokers_list());
1674 g_game_vars.timer = TM_ZERO;
1675 play_state = PLAY_BEFORE_SCORING;
1676 }
1677 }
1678
1679 s_played_hand[played_idx]->tx =
1680 int2fx(HAND_PLAY_POS.x) +
1681 (int2fx(s_played_top - played_idx) - int2fx(s_played_top) / 2) * -27;
1682 s_played_hand[played_idx]->ty = int2fx(HAND_PLAY_POS.y);
1683
1684 card_selected = card_object_is_selected(s_played_hand[played_idx]);
1685 if (card_selected && s_played_top - played_idx >= s_scored_card_index)
1686 {
1687 s_played_hand[played_idx]->ty -= int2fx(10);
1688 }
1689}
1690
1698static inline bool play_before_scoring_cards_update(void)
1699{
1700 // Activate Jokers with an effect just before the hand is scored
1701 if (check_and_score_joker_for_event(&s_joker_scored_itr, NULL, JOKER_EVENT_ON_HAND_PLAYED))
1702 {
1703 return true;
1704 }
1705
1706 play_state = PLAY_SCORING_CARDS;
1707 return false;
1708}
1709
1715static inline bool play_scoring_cards_update(void)
1716{
1717 if (g_game_vars.timer % FRAMES(30) == 0 && g_game_vars.timer > FRAMES(40))
1718 {
1719 // We are about to score played Cards.
1720 // Start from the current card index
1721 // and seek the next scoring card
1722 while (s_scored_card_index <= s_played_top &&
1723 !card_object_is_selected(s_played_hand[s_scored_card_index]))
1724 {
1725 s_scored_card_index++;
1726 }
1727
1728 // go to the next state if there are no cards left to score
1729 if (s_scored_card_index > s_played_top)
1730 {
1731 // reuse these variables for held cards
1732 s_joker_scored_itr = list_itr_create(get_jokers_list());
1733 s_scored_card_index = get_hand_top();
1734
1735 play_state = PLAY_SCORING_HELD_CARDS;
1736
1737 return false;
1738 }
1739
1740 tte_erase_rect_wrapper(PLAYED_CARDS_SCORES_RECT);
1741
1742 CardObject* scored_card_object = s_played_hand[s_scored_card_index];
1743
1744 if (card_object_is_selected(scored_card_object))
1745 {
1746 // Offset of 1 tile to keep the text on the card
1747 tte_set_pos(fx2int(scored_card_object->x) + TILE_SIZE, SCORED_CARD_TEXT_Y);
1748
1749 // Set text color to blue from background memory
1750 tte_set_special(TTE_BLUE_PB * TTE_SPECIAL_PB_MULT_OFFSET);
1751
1752 u8 card_value = card_get_value(scored_card_object->card);
1753
1754 // Write the score to a character buffer variable
1755 char score_buffer[INT_MAX_DIGITS + 2]; // for '+' and null terminator
1756 snprintf(score_buffer, sizeof(score_buffer), "+%hhu", card_value);
1757 tte_write(score_buffer);
1758
1759 card_object_shake(scored_card_object, SFX_CHIPS_CARD);
1760
1761 // Relocated card scoring logic here
1762 g_game_vars.chips = u32_protected_add(g_game_vars.chips, card_value);
1763 display_chips();
1764
1765 // Allow Joker scoring
1766 s_joker_scored_itr = list_itr_create(get_jokers_list());
1767 s_joker_card_scored_end_itr = list_itr_create(get_jokers_list());
1768 }
1769
1770 play_state = PLAY_SCORING_CARD_JOKERS;
1771 return true;
1772 }
1773
1774 return false;
1775}
1776
1784static inline bool play_scoring_card_jokers_update(void)
1785{
1786 if (g_game_vars.timer % FRAMES(30) == 0 && g_game_vars.timer > FRAMES(40))
1787 {
1788 tte_erase_rect_wrapper(PLAYED_CARDS_SCORES_RECT);
1789
1790 // since we sought the next scoring card index in the previous state,
1791 // scored_card_index is guaranteed to be a scoring card
1793 &s_joker_scored_itr,
1794 s_played_hand[s_scored_card_index],
1795 JOKER_EVENT_ON_CARD_SCORED
1796 ))
1797 {
1798 return true;
1799 }
1800
1801 // Trigger all Jokers that have an effect when a card finishes scoring
1802 // (e.g. retriggers) after activating all the other scored_card Jokers normally
1804 &s_joker_card_scored_end_itr,
1805 s_played_hand[s_scored_card_index],
1806 JOKER_EVENT_ON_CARD_SCORED_END
1807 ))
1808 {
1809 // If we just scored a retrigger, return early and go back to the
1810 // previous state score the same card again without incrementing
1811 // scored_card_index to score the current card again
1812 if (s_retrigger)
1813 {
1814 s_retrigger = false;
1815 play_state = PLAY_SCORING_CARDS;
1816 }
1817 return true;
1818 }
1819
1820 // increment index to start seeking the next scoring card from the next card
1821 s_scored_card_index++;
1822 play_state = PLAY_SCORING_CARDS;
1823 }
1824
1825 return false;
1826}
1827
1834static inline bool play_scoring_held_cards_update(int played_idx)
1835{
1836 if (played_idx == 0 && (g_game_vars.timer % FRAMES(30) == 0) && g_game_vars.timer > FRAMES(40))
1837 {
1838 tte_erase_rect_wrapper(HELD_CARDS_SCORES_RECT);
1839
1840 CardObject** hand = get_hand_array();
1841
1842 // Go through all held cards and see if they activate Jokers
1843 for (; s_scored_card_index >= 0; s_scored_card_index--)
1844 {
1846 &s_joker_scored_itr,
1847 hand[s_scored_card_index],
1848 JOKER_EVENT_ON_CARD_HELD
1849 ))
1850 {
1851 card_object_shake(hand[s_scored_card_index], SFX_CARD_SELECT);
1852 return true;
1853 }
1854 s_joker_scored_itr = list_itr_create(get_jokers_list());
1855 }
1856
1857 s_scored_card_index = 0;
1858 s_joker_round_end_itr = list_itr_create(get_jokers_list());
1859 play_state = PLAY_SCORING_INDEPENDENT_JOKERS;
1860 }
1861
1862 return false;
1863}
1864
1871static inline bool play_scoring_independent_jokers_update(int played_idx)
1872{
1873 if (played_idx == 0 && (g_game_vars.timer % FRAMES(30) == 0) && g_game_vars.timer > FRAMES(40))
1874 {
1875
1876 tte_erase_rect_wrapper(PLAYED_CARDS_SCORES_RECT);
1877
1878 if (check_and_score_joker_for_event(&s_joker_scored_itr, NULL, JOKER_EVENT_INDEPENDENT))
1879 {
1880 return true;
1881 }
1882
1883 // Reset the scored card index to past the top of the played stack
1884 s_scored_card_index = get_played_size();
1885
1886 play_state = PLAY_SCORING_HAND_SCORED_END;
1887 }
1888
1889 return false;
1890}
1891
1898static inline bool play_scoring_hand_scored_end_update(int played_idx)
1899{
1900 if (played_idx == 0 && (g_game_vars.timer % FRAMES(30) == 0) && g_game_vars.timer > FRAMES(40))
1901 {
1902
1903 tte_erase_rect_wrapper(PLAYED_CARDS_SCORES_RECT);
1904
1905 bool scored = check_and_score_joker_for_event(
1906 &s_joker_round_end_itr,
1907 NULL,
1908 JOKER_EVENT_ON_HAND_SCORED_END
1909 );
1910
1911 if (scored)
1912 {
1913 return true;
1914 }
1915
1916 g_game_vars.timer = TM_ZERO;
1917 play_state = PLAY_ENDING;
1918 }
1919
1920 return false;
1921}
1922
1929static inline void play_ending_played_cards_update(int played_idx)
1930{
1931 // Same protection against out of bounds access as `play_starting_played_cards_update`
1932 bool card_selected =
1933 (s_played_top < s_scored_card_index)
1934 ? false
1935 : card_object_is_selected(s_played_hand[s_played_top - s_scored_card_index]);
1936
1937 if (played_idx == s_played_top && (g_game_vars.timer % FRAMES(10) == 0 || !card_selected) &&
1938 g_game_vars.timer > FRAMES(40))
1939 {
1940 s_scored_card_index--;
1941
1942 /* SFX_CHIPS_ACCUM has been pitch shifted to perserve high frequencies in downsampling.
1943 * Now it needs to be pitch shifted back to the original frequency.
1944 */
1945 int static const CHIPS_ACCUM_SFX_PITCH_RATIO = 2;
1946
1947 if (s_scored_card_index == 0)
1948 {
1949 play_sfx(
1950 SFX_CHIPS_ACCUM,
1951 CHIPS_ACCUM_SFX_PITCH_RATIO * MM_BASE_PITCH_RATE,
1953 );
1954 g_game_vars.timer = TM_ZERO;
1955 play_state = PLAY_ENDED;
1956 }
1957 }
1958
1959 if (card_object_is_selected(s_played_hand[played_idx]) &&
1960 s_played_top - played_idx >= s_scored_card_index)
1961 {
1962 s_played_hand[played_idx]->ty = int2fx(HAND_PLAY_POS.y);
1963 }
1964}
1965
1974static bool play_ended_played_cards_update(int played_idx)
1975{
1976 if (!s_discarded_card && g_game_vars.timer > FRAMES(40))
1977 {
1978 // play the sound only once per card, when it is pushed off-screen to the right
1979 if (!s_sound_played)
1980 {
1981 play_sfx(
1982 SFX_CARD_DRAW,
1983 MM_BASE_PITCH_RATE + s_cards_drawn * PITCH_STEP_DISCARD_SFX,
1985 );
1986 s_sound_played = true;
1987 }
1988
1989 // card has exited the screen, now discard it and set it to NULL
1990 if (s_played_hand[played_idx]->x >= int2fx(CARD_DISCARD_PNT.x))
1991 {
1992 discard_push(s_played_hand[played_idx]->card); // Push the card to the discard pile
1993 card_object_destroy(&s_played_hand[played_idx]);
1994
1995 s_cards_drawn++; // This technically isn't drawing cards, I'm just reusing the variable
1996 s_sound_played = false; // Allow for the sound for the next card to be played
1997
1998 // we reached hand_top, all cards have been discarded
1999 if (played_idx == s_played_top)
2000 {
2001 if (round_is_over())
2002 {
2003 set_hand_state(HAND_SHUFFLING);
2004 }
2005 else
2006 {
2007 set_hand_state(HAND_DRAW);
2008 }
2009
2010 play_state = PLAY_STARTING;
2011 s_cards_drawn = 0;
2013 s_played_top = -1; // Reset the played stack
2014 s_scored_card_index = 0;
2015 s_joker_scored_itr = list_itr_create(get_jokers_list());
2016 g_game_vars.timer = TM_ZERO;
2017 }
2018
2019 return true; // return early to avoid accessing played[played_idx] == NULL
2020 }
2021
2022 // put target X position off screen to the right
2023 s_played_hand[played_idx]->tx = int2fx(CARD_DISCARD_PNT.x);
2024 s_discarded_card = true;
2025 }
2026
2027 return false;
2028}
2029
2030static inline void played_cards_update_loop(void)
2031{
2032 // So this one is a bit fucking weird because I have to work kinda backwards for everything
2033 // because of the order of the pushed cards from the hand to the play stack (also crazy that the
2034 // company that published Balatro is called "Playstack" and this is a play stack, but I digress)
2035 for (int played_idx = 0; played_idx <= s_played_top; played_idx++)
2036 {
2037 if (s_played_hand[played_idx] == NULL)
2038 {
2039 continue;
2040 }
2041
2042 if (card_object_get_sprite(s_played_hand[played_idx]) == NULL)
2043 {
2044 // Offset the index so played cards don't overlap with the hand
2045 // and for example get overwritten when it the hand is reordered
2046 card_object_set_sprite(s_played_hand[played_idx], MAX_HAND_SIZE + played_idx);
2047 }
2048
2049 switch (play_state)
2050 {
2051 case PLAY_STARTING:
2052
2053 play_starting_played_cards_update(played_idx);
2054 break;
2055
2056 case PLAY_BEFORE_SCORING:
2057
2059 {
2060 return;
2061 }
2062 break;
2063
2064 case PLAY_SCORING_CARDS:
2065
2067 {
2068 return;
2069 }
2070 break;
2071
2072 case PLAY_SCORING_CARD_JOKERS:
2073
2075 {
2076 return;
2077 }
2078 break;
2079
2080 case PLAY_SCORING_HELD_CARDS:
2081
2082 if (play_scoring_held_cards_update(played_idx))
2083 {
2084 return;
2085 }
2086 break;
2087
2088 case PLAY_SCORING_INDEPENDENT_JOKERS:
2089
2091 {
2092 return;
2093 }
2094 break;
2095
2096 case PLAY_SCORING_HAND_SCORED_END:
2097
2099 {
2100 return;
2101 }
2102 break;
2103
2104 case PLAY_ENDING:
2105
2107 break;
2108
2109 case PLAY_ENDED:
2110
2111 if (play_ended_played_cards_update(played_idx))
2112 {
2113 // we continue here instead of returning for performance
2114 // to instantly go to the next card to discard at played_idx+1,
2115 // instead of starting over from index 0 and going up
2116 // to that card again
2117 continue;
2118 }
2119 break;
2120 }
2121
2122 s_played_hand[played_idx]->tscale = FIX_ONE;
2123 }
2124}
2125
2126/*******************************************************************************
2127 * FOUND STATE FUNCTIONS
2128 ******************************************************************************/
2129
2131{
2132 s_joker_scored_itr = list_itr_create(get_jokers_list());
2133
2134 set_hand_state(HAND_TAGS);
2135
2137 s_cards_drawn = 0;
2138
2139 sprite_destroy(&g_game_vars.playing_blind_token);
2140 g_game_vars.playing_blind_token = blind_token_new(
2141 g_game_vars.current_blind,
2142 CUR_BLIND_TOKEN_POS.x,
2143 CUR_BLIND_TOKEN_POS.y,
2144 PLAYING_BLIND_TOKEN_LAYER
2145 ); // Create the blind token sprite at the top left corner
2146 // TODO: Hide blind token and display it after sliding blind rect animation
2147 // if (g_game_vars.playing_blind_token != NULL)
2148 //{
2149 // sprite_hide(g_game_vars.playing_blind_token); // Hide the blind token sprite for now
2150 //}
2151 sprite_destroy(&g_game_vars.round_end_blind_token);
2152 g_game_vars.round_end_blind_token = blind_token_new(
2153 g_game_vars.current_blind,
2154 81,
2155 86,
2156 ROUND_END_BLIND_TOKEN_LAYER
2157 ); // Create the blind token sprite for round end
2158
2159 if (g_game_vars.round_end_blind_token != NULL)
2160 {
2161 sprite_hide(g_game_vars.round_end_blind_token); // Hide the blind token sprite for now
2162 }
2163
2164 Rect blind_req_text_rect = BLIND_REQ_TEXT_RECT;
2165 u32 blind_requirement = blind_get_requirement(g_game_vars.current_blind, g_game_vars.ante);
2166
2167 char blind_req_str_buff[UINT_MAX_DIGITS + 1];
2168
2170 blind_requirement,
2171 rect_width(&BLIND_REQ_TEXT_RECT) / TTE_CHAR_SIZE,
2172 blind_req_str_buff
2173 );
2174
2175 // Update text rect for right alignment AFTER shortening the number
2176 update_text_rect_to_right_align_str(&blind_req_text_rect, blind_req_str_buff, OVERFLOW_RIGHT);
2177
2178 tte_printf(
2179 "#{P:%d,%d; cx:0x%X000}%s",
2180 blind_req_text_rect.left,
2181 blind_req_text_rect.top,
2182 TTE_RED_PB,
2183 blind_req_str_buff
2184 );
2185 tte_printf(
2186 "#{P:%d,%d; cx:0x%X000}$%d",
2187 BLIND_REWARD_RECT.left,
2188 BLIND_REWARD_RECT.top,
2189 TTE_YELLOW_PB,
2190 blind_get_reward(g_game_vars.current_blind)
2191 ); // Blind reward
2192
2193 deck_shuffle(); // Shuffle the deck at the start of the round
2194
2195 skip_tag_process_start(SKIP_TAG_EVENT_ON_ROUND_START);
2196
2197 /* Note that since cards_in_hand_update_loop() handles card highlight there's no need
2198 * to call a selection changed callback to highlight the initial card, this wouldn't work
2199 * otherwise or for the buttons.
2200 */
2201 game_round_selection_grid.selection = GAME_PLAYING_INIT_SEL;
2202}
2203
2205{
2206 // Background logic (thissss might be moved to the card'ssss logic later. I'm a sssssnake)
2207 if (get_hand_state() == HAND_TAGS || get_hand_state() == HAND_DRAW ||
2208 get_hand_state() == HAND_DISCARD || get_hand_state() == HAND_SELECT)
2209 {
2210 change_background(BG_CARD_SELECTING, false);
2211 }
2212 else if (get_hand_state() != HAND_SHUFFLING)
2213 {
2214 change_background(BG_CARD_PLAYING, false);
2215 }
2216
2217 round_process_input_and_state();
2218
2219 // Card logic
2220
2221 round_process_card_draw();
2222
2223 round_discarded_cards_loop();
2224
2225 s_discarded_card = false;
2226
2227 cards_in_hand_update_loop();
2228 played_cards_update_loop();
2229
2230 round_ui_text_update();
2231
2232 // animate score flames if we exceed the score requirement
2233 round_process_flaming_score();
2234}
Utilities for using maxmod to play sound effects.
#define MM_BASE_PITCH_RATE
The default pitch rate for the sound effect played.
Definition audio_utils.h:37
void play_sfx(mm_word id, mm_word rate, mm_byte volume)
Play a sound effect, wrapper for mmEffectEx() See https://maxmod.org/ref/functions/mm_sound_effect....
#define SFX_DEFAULT_VOLUME
Default volume for sound effects.
Definition audio_utils.h:43
u16 blind_get_color(enum BlindType type, enum BlindColorIndex index)
Get the color associated with a given Blind.
Definition blind.c:213
u32 blind_get_requirement(enum BlindType type, int ante)
Get the score required to beat a certain blind (either the Small, Big, or any Boss/Showdown blind) at...
Definition blind.c:101
Sprite * blind_token_new(enum BlindType type, int x, int y, enum BlindTokenLayers sprite_index)
Create a new BlindToken sprite.
Definition blind.c:263
void set_blind_beaten(enum BlindType type)
Remove the given Blind from the corresponding List so that we don't roll it again in the future.
Definition blind.c:175
int blind_get_reward(enum BlindType type)
Get the amount of money gained from beating a certain Blind.
Definition blind.c:110
A button structure containing the common button functionalities Including highlight and pressing func...
void button_press(Button *button)
Execute a button press, by calling the button's on_pressed function. This checks the button's can_be_...
Definition button.c:18
void button_set_highlight(Button *button, bool highlight)
Set the button highlight on or off.
Definition button.c:8
Common functions to render UI elements.
void change_background(enum BackgroundId id, bool force_redraw)
Definition common_ui.c:35
Graphic utility functions.
void main_bg_se_move_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction)
Moves a rect in the main background vertically in direction by a single tile.
void main_bg_se_copy_rect(Rect se_rect, BG_POINT dest_pos)
Copies a rect in the main background from se_rect to the position (x, y).
void toggle_windows(bool win0, bool win1)
Toggles the visibility of the window layers.
void update_text_rect_to_right_align_str(Rect *rect, const char *str, enum OverflowDir overflow_direction)
Changes rect->left so it fits the digits of num exactly when right aligned to rect->right....
#define TILE_SIZE
Tile size in pixels, both height and width as tiles are square.
#define TTE_CHAR_SIZE
By default TTE characters occupy a single tile.
void tte_erase_rect_wrapper(Rect rect)
A wrapper for tte_erase_rect that would use the rect struct.
INLINE int rect_width(const Rect *rect)
Get the width of a rectangle.
Functions relative to manipulating and analyzing the contents of the Hand, a.k.a. the Cards we hold a...
void set_hand_state(enum HandState)
Set the hand state. Primarily used by the GAME_PLAYING game state.
Definition hand.c:96
void hand_set_nb_selected_cards(int new_selections)
Set the current number of selected Cards.
Definition hand.c:126
int find_flush_in_played_cards(CardObject **played, int top, int min_len, bool *out_selection)
Finds the best flush (set of cards with the same suit) in the given array of played cards.
Definition hand.c:545
enum HandType get_hand_type(void)
Get the current hand type.
Definition hand.c:131
int get_hand_top(void)
Get the position in hand array of the last card obtained.
Definition hand.c:106
enum HandState get_hand_state(void)
Get the hand state.
Definition hand.c:91
void swap_cards_in_hand(int idx_a, int idx_b)
Swaps the order of two cards in hand.
Definition hand.c:175
int find_straight_in_played_cards(CardObject **played, int top, int min_len, bool *out_selection)
Finds the best straight in the given array of played cards.
Definition hand.c:588
CardObject ** get_hand_array(void)
Get the hand array of Cards currently held in hand.
Definition hand.c:101
int hand_get_nb_selected_cards(void)
Get the current number of selected Cards.
Definition hand.c:121
void select_paired_cards_in_hand(CardObject **played, int top, bool *selection)
This is used for the special case in "Four Fingers" where you can add a pair into a straight (e....
Definition hand.c:732
void reorder_card_sprites_layers(void)
Destroy the sprites of the Cards held in hand and recreate then in the same order as the Cards in the...
Definition hand.c:245
void set_hand_top(int new_hand_top)
Set the position in hand array of the last card obtained.
Definition hand.c:111
void hand_select_card(int index)
Set the card at a given index in Hand as selected.
Definition hand.c:304
void hand_change_sort(bool to_sort_by_suit)
Switch to the given sort method. Can sort playing cards in two ways: by rank and suit....
Definition hand.c:295
void compute_hand_value_info(void)
Determine the HandType and ContainedHandTypes of the currently selected Cards, then print the Hand's ...
Definition hand.c:157
void sort_cards(void)
Sort the hand array according to the selected method to do that.
Definition hand.c:281
void hand_deselect_all_cards(void)
Deselect all cards in hand.
Definition hand.c:325
int hand_nb_held_cards(void)
Get the current number of Cards in hand.
Definition hand.c:116
Functions relative to the handling of Jokers.
All the functions used specifically to interact with Jokers. This includes browsing,...
bool jokers_sel_row_on_selection_changed(SelectionGrid *selection_grid, int row_idx, const Selection *prev_selection, const Selection *new_selection)
Handle directional inputs on the Jokers' row at the top of the game screen.
Definition joker_row.c:16
void jokers_sel_row_on_key_transit(SelectionGrid *selection_grid, Selection *selection)
Handle button inputs on the Jokers' row at the top of the game screen.
Definition joker_row.c:92
int jokers_sel_row_get_size(void)
Returns the size of the Jokers' row at the top of the game screen. Is equal to the number of Jokers o...
Definition joker_row.c:11
Header of shared rects and points needed for ui.
A doubly-linked list.
void * list_itr_next(ListItr *itr)
Definition list.c:281
void list_push_back(List *list, void *data)
Definition list.c:89
ListItr list_itr_create(List *list)
Definition list.c:257
u32 rng_get_u32(enum RngSequence key)
Get the next "randomly" generated number in the sequence corresponding to the given type.
Definition random.c:56
static void card_draw(void)
Draw the next card at the top of the Deck and play a little Sprite animation to position it in our ha...
Definition round.c:1140
static void discard_push(Card *card)
Push a new card to discard at the end of the associated array.
Definition round.c:282
static bool play_scoring_independent_jokers_update(int played_idx)
Score Jokers normally for event JOKER_EVENT_INDEPENDENT.
Definition round.c:1871
void round_change_background_playing(void)
Change to the round card playing background.
Definition round.c:405
void round_on_init(void)
Round state initialization.
Definition round.c:2130
int get_scored_card_index(void)
Get the index of the card that is currently being scored.
Definition round.c:301
static void played_push(CardObject *card_object)
Push a new card to play at the end of the associated array.
Definition round.c:263
void round_on_update(void)
Round state update.
Definition round.c:2204
int get_discard_top(void)
Get the index of the last discarded card.
Definition round.c:270
static bool play_before_scoring_cards_update(void)
Returns true if the Jokers scoring loop has returned early for event JOKER_EVENT_ON_HAND_PLAYED.
Definition round.c:1698
int get_played_size(void)
Get the number of cards played.
Definition round.c:253
void round_change_background_selecting(void)
Change to the round card selection background.
Definition round.c:343
static bool round_is_over(void)
Determines if the round is over, be it because we lost or won the round.
Definition round.c:1054
void set_retrigger(bool new_retrigger)
Set whether the current card should get triggered again.
Definition round.c:306
static bool check_and_score_joker_for_event(ListItr *starting_joker_itr, CardObject *card_object, enum JokerEvent joker_event)
Iterate over the Jokers List until we encounter one that scores for the specified event.
Definition round.c:1640
static void play_ending_played_cards_update(int played_idx)
This is the reverse of PLAY_STARTING. The cards get reset back to their neutral position sequentially...
Definition round.c:1929
static void round_sort_by_suit_on_pressed(void)
"Suit" sorting button implementation.
Definition round.c:491
static void round_play_hand_on_pressed(void)
"Play" button implementation.
Definition round.c:515
static void round_execute_discard(void)
Triggers the discard of the currently selected cards in hand.
Definition round.c:455
static Card * discard_pop()
Remove and recover the last discarded card.
Definition round.c:294
void toggle_flaming_score(void)
Checks whether the score that would result from the current Chips and Mult exceeds the current Blind'...
Definition round.c:1446
static void round_execute_play_hand(void)
Triggers the playing of the currently selected cards in hand.
Definition round.c:499
static bool can_play_hand(void)
Determines whether the "Play" button can be pressed or not.
Definition round.c:445
static bool play_scoring_card_jokers_update(void)
Activate Jokers for event JOKER_EVENT_ON_CARD_SCORED_END for the previous scored card,...
Definition round.c:1784
static bool can_discard_hand(void)
Determines whether the "Discard" button can be pressed or not.
Definition round.c:432
static bool play_scoring_held_cards_update(int played_idx)
Activate Jokers for event JOKER_EVENT_ON_CARD_HELD for the previous scored card, if any.
Definition round.c:1834
int get_played_top(void)
Get the index of the last played card.
Definition round.c:248
static int hand_sel_idx_to_card_idx(int selection_index)
Converts a selection index from the selection grid into a card index within the hand array.
Definition round.c:711
static bool play_scoring_hand_scored_end_update(int played_idx)
Trigger all Jokers for event JOKER_EVENT_ON_HAND_SCORED_END once they are done scoring.
Definition round.c:1898
static GBAL_UNUSED void get_played_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS])
Outputs the distribution of ranks and suits in the played stack.
Definition round.c:319
static void round_sort_by_rank_on_pressed(void)
"Rank" sorting button implementation.
Definition round.c:483
static void round_discard_on_pressed(void)
"Discard" button implementation.
Definition round.c:469
static void round_handle_round_over(void)
Evaluates if we have won or lost when we can no longer play so that we land on the correct Game Over ...
Definition round.c:728
static bool play_scoring_cards_update(void)
Score all played cards in order, then Jokers after each one.
Definition round.c:1715
static bool play_ended_played_cards_update(int played_idx)
Returns true if the card at index played_idx has been discarded. Basically a copy of HAND_DISCARD.
Definition round.c:1974
API relative to the Rounds we play.
An implementation for a selection grid that handles directional selection.
void selection_grid_move_selection_vert(SelectionGrid *selection_grid, int direction_tribool)
Moves the selection vertically within the selection grid.
void selection_grid_process_input(SelectionGrid *selection_grid)
Processes user input for the selection grid.
Data structures and functions related to the handling of Skip Tags.
void skip_tag_process_start(enum SkipTagEvent checked_tag_event)
Start Skip Tag processing for the specified event.
Definition skip_tag.c:344
enum SkipTagProcStage skip_tag_process_get_proc_stage(void)
Recover the current stage of the Tag processing.
Definition skip_tag.c:339
void sprite_object_reset_transform(SpriteObject *sprite_object)
Reset SpriteObject's transform back to default values.
Definition sprite.c:278
void sprite_destroy(Sprite **sprite)
Destroy Sprite.
Definition sprite.c:134
void sprite_hide(Sprite *sprite)
Hides the sprite by manipulating ATTR0_HIDE in OAM.
Definition sprite.c:222
A button representation.
Definition button.h:31
Definition card.h:46
An iterator into a list.
Definition list.h:91
The core selection grid struct, represents the grid itself and its state.
A sprite object is a sprite that is focusable and movable in animation.
Definition sprite.h:86
Timer constants.
Utilities relating around number string representation and protected arithmatic helper functions.
uint32_t u32_protected_add(uint32_t a, uint32_t b)
Avoid overflow when adding two u32 integers.
Definition util.c:175
#define NUM_ELEM_IN_ARR(arr)
Get the number of elements in an array.
Definition util.h:55
#define GBAL_RETURN_IF_NULL(param, ret_val)
Returns ret_val and prints a default error message if param is equal to NULL. Useful for checking arg...
Definition util.h:138
void truncate_uint_to_suffixed_str(uint32_t num, int num_req_chars, char out_str_buff[UINT_MAX_DIGITS+1])
Truncate an unsigned number into a suffixed string representation e.g. 12000 -> "12K" The least signi...
Definition util.c:116
#define GBAL_UNUSED
Wrapper around the "unused" attribute.
Definition util.h:25
#define RET_NONE
An empty return value for RETURN_IF macros when used in void functions Expands to nothing because mac...
Definition util.h:146
uint32_t u32_protected_mult(uint32_t a, uint32_t b)
Avoid overflow when multiplying two u32 integers.
Definition util.c:185