GBAlatro
A Demake of Balatro for the GBA
Loading...
Searching...
No Matches
round_end.c
1#include "round_end.h"
2
3#include "affine_background.h"
4#include "affine_background_gfx.h"
5#include "game.h"
6#include "game_variables.h"
7#include "layout.h"
8#include "skip_tag.h"
9#include "state_machine.h"
10#include "timer.h"
11#include "util.h"
12
13enum GameRoundEndStates
14{
15 ROUND_END_START,
16 START_EXPAND_POPUP,
17 DISPLAY_FINISHED_BLIND,
18 DISPLAY_SCORE_MIN,
19 UPDATE_BLIND_REWARD,
20 BLIND_PANEL_EXIT,
21 DISPLAY_REWARDS,
22 DISPLAY_CASHOUT,
23 DISMISS_ROUND_END_PANEL,
24 ROUND_END_STATES_MAX
25};
26
27enum RewardType
28{
29 REWARD_TYPE_HAND,
30 REWARD_TYPE_INVESTMENT,
31 REWARD_TYPE_INTEREST,
32 REWARD_TYPE_MAX
33};
34
35static const u32 TM_RESET_STATIC_VARS = 30;
36static const u32 TM_END_POP_MENU_ANIM = 13;
37static const u32 TM_START_ROUND_END_REWARDS_ANIM = 1;
38static const u32 TM_END_DISPLAY_FIN_BLIND = 30;
39static const u32 TM_END_DISPLAY_SCORE_MIN = 4;
40static const u32 TM_DISMISS_ROUND_END_TM = 20;
41
42static const u32 REWARD_PANEL_BORDER_PID = 19;
43
44static const u32 ROUND_END_BLACK_PANEL_INIT_BOTTOM_SE = 12;
45static const u32 ROUND_END_REWARD_TEXT_X = 88;
46static const u32 ROUND_END_REWARD_AMOUNT_X = 168;
47
48// clang-format off
49static const Rect ROUND_END_BLIND_REQ_RECT = {104, 96, 136, UNDEFINED};
50static const Rect ROUND_END_BLIND_REWARD_RECT = {168, 96, UNDEFINED, UNDEFINED};
51static const Rect CASHOUT_DEST_RECT = {10, 8, 23, 10 };
52static const Rect CASHOUT_TEXT_RECT = {88, 72, UNDEFINED, UNDEFINED};
53static const Rect BLIND_TOKEN_TEXT_RECT = {80, 72, 200, 160 };
54static const Rect ROUND_END_MENU_RECT = {9, 7, 24, 20 };
55
56static const BG_POINT CASHOUT_SRC_3X3_RECT_POS = {5, 29};
57// clang-format on
58
65typedef struct RewardTypeData
66{
70 const char* text;
71
75 const u8 text_color;
76
85 const int multiplier;
86
90 int count;
91
97
98// clang-format off
99static RewardTypeData s_reward_data[] = {
100 [REWARD_TYPE_HAND] = {
101 .text = "Hands",
102 .text_color = TTE_BLUE_PB,
103 .multiplier = 1,
104 .count = 0,
105 .remaining = 0
106 },
107 [REWARD_TYPE_INVESTMENT] = {
108 .text = "Boss",
109 .text_color = TTE_YELLOW_PB,
110 .multiplier = INVESTMENT_TAG_REWARD,
111 .count = 0,
112 .remaining = 0
113 },
114 [REWARD_TYPE_INTEREST] = {
115 .text = "Interest",
116 .text_color = TTE_YELLOW_PB,
117 .multiplier = 1,
118 .count = 0,
119 .remaining = 0
120 }
121};
122// clang-format on
123
124static enum RewardType s_current_reward = 0;
125static u32 s_current_reward_start_time = TM_DISPLAY_REWARDS_CONT_WAIT;
126static int s_reward_y_offset = 1;
127
128static int s_blind_reward = 0;
129static int s_cashout = 0;
130
131static int calculate_interest_reward(void);
132
133static void round_end_start(void);
134static void round_end_start_expand_popup(void);
135static void round_end_display_finished_blind(void);
136static void round_end_display_score_min(void);
137static void round_end_update_blind_reward(void);
138static void round_end_panel_exit(void);
139static void round_end_display_rewards(void);
140static void round_end_display_cashout(void);
141static void round_end_dismiss_round_end_panel(void);
142
143static void round_end_extend_black_panel_down(int black_panel_bottom);
144
145// clang-format off
146static StateInfo state_info[] =
147{
148 [ROUND_END_START] = STATE_INFO_UPDATE_FN_ONLY(round_end_start),
149 [START_EXPAND_POPUP] = STATE_INFO_UPDATE_FN_ONLY(round_end_start_expand_popup),
150 [DISPLAY_FINISHED_BLIND] = STATE_INFO_UPDATE_FN_ONLY(round_end_display_finished_blind),
151 [DISPLAY_SCORE_MIN] = STATE_INFO_UPDATE_FN_ONLY(round_end_display_score_min),
152 [UPDATE_BLIND_REWARD] = STATE_INFO_UPDATE_FN_ONLY(round_end_update_blind_reward),
153 [BLIND_PANEL_EXIT] = STATE_INFO_UPDATE_FN_ONLY(round_end_panel_exit),
154 [DISPLAY_REWARDS] = STATE_INFO_UPDATE_FN_ONLY(round_end_display_rewards),
155 [DISPLAY_CASHOUT] = STATE_INFO_UPDATE_FN_ONLY(round_end_display_cashout),
156 [DISMISS_ROUND_END_PANEL] = STATE_INFO_UPDATE_FN_ONLY(round_end_dismiss_round_end_panel),
157};
158// clang-format on
159
160static StateMachine round_end_sm = STATE_MACHINE_DEFINE(state_info, ROUND_END_STATES_MAX);
161
162static int calculate_interest_reward(void)
163{
164 int reward = (g_game_vars.money / 5) * INTEREST_PER_5;
165 if (reward > MAX_INTEREST)
166 reward = MAX_INTEREST;
167 return reward;
168}
169
170static void round_end_start(void)
171{
172 // Reset static variables to default values upon re-entering the round end state
173 if (g_game_vars.timer == TM_RESET_STATIC_VARS)
174 {
175 change_background(BG_ROUND_END, false); // Change the background to the round end background
176
177 g_game_vars.timer = TM_ZERO; // Reset the timer
178 state_machine_change_state(&round_end_sm, START_EXPAND_POPUP);
179
180 s_current_reward = 0;
181 s_current_reward_start_time = TM_DISPLAY_REWARDS_CONT_WAIT;
182 s_reward_y_offset = 1;
183
184 s_blind_reward = blind_get_reward(g_game_vars.current_blind);
185 s_reward_data[REWARD_TYPE_HAND].count = g_game_vars.hands;
186 s_reward_data[REWARD_TYPE_INVESTMENT].count = g_game_vars.current_blind >= BLIND_TYPE_BOSS
187 ? skip_tag_count(SKIP_TAG_TYPE_INVESTMENT)
188 : 0;
189 s_reward_data[REWARD_TYPE_INTEREST].count = calculate_interest_reward();
190
191 s_cashout = blind_get_reward(g_game_vars.current_blind);
192 for (enum RewardType i = 0; i < REWARD_TYPE_MAX; i++)
193 {
194 s_reward_data[i].remaining = s_reward_data[i].count;
195 s_cashout += s_reward_data[i].count * s_reward_data[i].multiplier;
196 }
197 }
198}
199
200static void round_end_start_expand_popup(void)
201{
202 main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP);
203
204 if (g_game_vars.timer == TM_END_POP_MENU_ANIM)
205 {
206 state_machine_change_state(&round_end_sm, DISPLAY_FINISHED_BLIND);
207 g_game_vars.timer = TM_ZERO;
208 }
209}
210
211static void round_end_display_finished_blind(void)
212{
213 sprite_unhide(g_game_vars.round_end_blind_token);
214
215 int current_ante = g_game_vars.ante;
216
217 // Beating the boss blind increases the ante, so we need to display the previous ante value
218 if (g_game_vars.current_blind > BLIND_TYPE_BIG)
219 current_ante--;
220
221 Rect blind_req_rect = ROUND_END_BLIND_REQ_RECT;
222 u32 blind_req = blind_get_requirement(g_game_vars.current_blind, current_ante);
223
224 /* Not bothering to truncate here because there are 8 tiles
225 * and the blind requirement will not increase past ante 8
226 * so there's enough room for sure.
227 */
228 char blind_req_str_buff[UINT_MAX_DIGITS + 1];
229 snprintf(blind_req_str_buff, sizeof(blind_req_str_buff), "%lu", blind_req);
230
231 update_text_rect_to_right_align_str(&blind_req_rect, blind_req_str_buff, OVERFLOW_RIGHT);
232
233 tte_printf(
234 "#{P:%d,%d; cx:0x%X000}%s",
235 blind_req_rect.left,
236 blind_req_rect.top,
237 TTE_RED_PB,
238 blind_req_str_buff
239 );
240
241 if (g_game_vars.timer == TM_START_ROUND_END_REWARDS_ANIM)
242 {
243 round_end_extend_black_panel_down(ROUND_END_BLACK_PANEL_INIT_BOTTOM_SE);
244 }
245
246 if (g_game_vars.timer >= TM_END_DISPLAY_FIN_BLIND)
247 {
248 state_machine_change_state(&round_end_sm, DISPLAY_SCORE_MIN);
249 g_game_vars.timer = TM_ZERO;
250 }
251}
252
253static void round_end_display_score_min(void)
254{
255 const int timer_offset = g_game_vars.timer - 1;
256 const int x_from = 0;
257 const int y_from = 29;
258 const int x_to = 13;
259 const int y_to = 11;
260
261 memcpy16(
262 &se_mem[MAIN_BG_SBB][x_to + timer_offset + 32 * y_to],
263 &se_mem[MAIN_BG_SBB][x_from + timer_offset + 32 * y_from],
264 1
265 );
266
267 if (g_game_vars.timer >= TM_END_DISPLAY_SCORE_MIN)
268 {
269 state_machine_change_state(&round_end_sm, UPDATE_BLIND_REWARD);
270 g_game_vars.timer = TM_ZERO;
271 }
272}
273
274static void round_end_update_blind_reward(void)
275{
276 if (g_game_vars.timer % FRAMES(20) != 0)
277 return;
278
279 // TODO: Add sound effect here
280
281 if (s_blind_reward > 0)
282 {
283 s_blind_reward--;
284 tte_printf(
285 "#{P:%d,%d; cx:0x%X000}$%d",
286 BLIND_REWARD_RECT.left,
287 BLIND_REWARD_RECT.top,
288 TTE_YELLOW_PB,
289 s_blind_reward
290 );
291 tte_printf(
292 "#{P:%d,%d; cx:0x%X000}$%d",
293 ROUND_END_BLIND_REWARD_RECT.left,
294 ROUND_END_BLIND_REWARD_RECT.top,
295 TTE_YELLOW_PB,
296 blind_get_reward(g_game_vars.current_blind) - s_blind_reward
297 );
298 }
299 else if (g_game_vars.timer > FRAMES(20))
300 {
301 tte_erase_rect_wrapper(BLIND_REWARD_RECT);
302 tte_erase_rect_wrapper(BLIND_REQ_TEXT_RECT);
303 sprite_hide(g_game_vars.playing_blind_token);
304 affine_background_load_palette(affine_background_gfxPal);
305 state_machine_change_state(&round_end_sm, BLIND_PANEL_EXIT);
306 g_game_vars.timer = TM_ZERO;
307 }
308}
309
310static void round_end_panel_exit(void)
311{
312 // TODO: make heads or tails of what's going on here and replace
313 // magic numbers.
314 if (g_game_vars.timer < 8)
315 {
316 main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SCREEN_UP);
317
318 if (g_game_vars.timer == 1)
319 {
321 }
322 else if (g_game_vars.timer == 2)
323 {
324 int y = 5;
325 memset16(&se_mem[MAIN_BG_SBB][32 * (y - 1)], 0x0001, 1);
326 memset16(&se_mem[MAIN_BG_SBB][1 + 32 * (y - 1)], 0x0002, 7);
327 memset16(&se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], 0x0401, 1);
328 }
329 }
330 else if (g_game_vars.timer > FRAMES(20))
331 {
332 memset16(&pal_bg_mem[REWARD_PANEL_BORDER_PID], 0x1483, 1);
333 state_machine_change_state(&round_end_sm, DISPLAY_REWARDS);
334 g_game_vars.timer = TM_ZERO;
335 }
336}
337
338static inline void round_end_print_separator_ellipsis(void)
339{
340 int x =
341 (ROUND_END_REWARDS_ELLIPSIS_POS.x + g_game_vars.timer - TM_REWARDS_ELLIPSIS_PRINT_START) *
342 TILE_SIZE;
343 int y = (ROUND_END_REWARDS_ELLIPSIS_POS.y) * TILE_SIZE;
344
345 tte_printf("#{P:%d,%d; cx:0x%X000}.", x, y, TTE_WHITE_PB);
346}
347
348static inline bool s_should_increment_curr_reward(void)
349{
350 switch (s_current_reward)
351 {
352 // For Hands and Interest rewards, updating the values on screen
353 // depends only on the timer
354 case REWARD_TYPE_HAND:
355 case REWARD_TYPE_INTEREST:
356 return g_game_vars.timer % FRAMES(TM_REWARD_INCREMENT_INTERVAL) == 0;
357
358 // For the Investment Tag reward, updating the values on screen will
359 // rely on the Tag processing stage
360 case REWARD_TYPE_INVESTMENT:
361 return skip_tag_process_get_proc_stage() == SKIP_TAG_PROCESS_STAGE_TRIGGER;
362
363 default:
364 return true;
365 }
366}
367
368static inline void round_end_print_current_reward(void)
369{
370 int reward_y = ROUND_END_REWARDS_ELLIPSIS_POS.y + s_reward_y_offset;
371 if (g_game_vars.timer == s_current_reward_start_time)
372 {
373 round_end_extend_black_panel_down(reward_y);
374
375 tte_printf(
376 "#{P:%lu,%d; cx:0x%X000}%d #{cx:0x%X000}%s",
377 ROUND_END_REWARD_TEXT_X,
378 reward_y * TILE_SIZE,
379 s_reward_data[s_current_reward].text_color,
380 s_reward_data[s_current_reward].count,
381 TTE_WHITE_PB,
382 s_reward_data[s_current_reward].text
383 );
384
385 if (s_current_reward == REWARD_TYPE_INVESTMENT)
386 skip_tag_process_start(SKIP_TAG_EVENT_ON_ROUND_END);
387 }
388
389 // Increment the reward text until the reward variable is depleted
390 else if (g_game_vars.timer > s_current_reward_start_time + TM_REWARD_DISPLAY_INTERVAL)
391 {
392 if (!s_should_increment_curr_reward())
393 return;
394
395 s_reward_data[s_current_reward].remaining--;
396 int accumulated_reward =
397 s_reward_data[s_current_reward].count - s_reward_data[s_current_reward].remaining;
398
399 tte_printf(
400 "#{P:%lu, %d; cx:0x%X000}$%d",
401 ROUND_END_REWARD_AMOUNT_X,
402 reward_y * TILE_SIZE,
403 TTE_YELLOW_PB,
404 accumulated_reward * s_reward_data[s_current_reward].multiplier
405 );
406
407 if (s_reward_data[s_current_reward].remaining <= 0)
408 {
409 s_current_reward++;
410 s_reward_y_offset++;
411 s_current_reward_start_time = g_game_vars.timer + TM_REWARD_INCREMENT_INTERVAL;
412 }
413 }
414}
415
416static void round_end_display_rewards(void)
417{
418 // Once all rewards are accounted for go to the next state
419 if (s_current_reward >= REWARD_TYPE_MAX)
420 {
421 g_game_vars.timer = TM_ZERO;
422 state_machine_change_state(&round_end_sm, DISPLAY_CASHOUT);
423 return;
424 }
425
426 if (g_game_vars.timer == TM_START_ROUND_END_REWARDS_ANIM)
427 {
428 round_end_extend_black_panel_down(ROUND_END_REWARDS_ELLIPSIS_POS.y);
429 }
430 else if (g_game_vars.timer < TM_REWARDS_ELLIPSIS_PRINT_END)
431 {
432 round_end_print_separator_ellipsis();
433 }
434 else if (g_game_vars.timer >= TM_DISPLAY_REWARDS_CONT_WAIT)
435 {
436 // Go to the next reward if the current one has no remaining money to give
437 while (s_current_reward < REWARD_TYPE_MAX && s_reward_data[s_current_reward].remaining <= 0)
438 s_current_reward++;
439
440 if (s_current_reward < REWARD_TYPE_MAX)
441 round_end_print_current_reward();
442 }
443}
444
445static inline void round_end_cashout(void)
446{
447 // Reward the player
448 g_game_vars.money += s_cashout;
449 display_money();
450
451 g_game_vars.hands = MAX_HANDS; // Reset the hands to the maximum
452 g_game_vars.discards = MAX_DISCARDS; // Reset the discards to the maximum
453 // TODO: these can just be in one spot, passing global to global
454 display_hands(); // Set the hands display
455 display_discards(); // Set the discards display
456
457 g_game_vars.score = 0;
458 display_score(g_game_vars.score); // Set the score display
459}
460
461static void round_end_display_cashout()
462{
463 if (g_game_vars.timer == FRAMES(40))
464 {
465 // Put the "cash out" button onto the round end panel
466 main_bg_se_copy_expand_3x3_rect(CASHOUT_DEST_RECT, CASHOUT_SRC_3X3_RECT_POS);
467
468 bool omit_space = s_cashout >= 10;
469 tte_printf(
470 "#{P:%d, %d; cx:0x%X000}Cash Out:%s$%d",
471 CASHOUT_TEXT_RECT.left,
472 CASHOUT_TEXT_RECT.top,
473 TTE_WHITE_PB,
474 omit_space ? "" : " ",
475 s_cashout
476 );
477 }
478
479 // Wait until the player presses A to cash out
480 else if (g_game_vars.timer > FRAMES(40) && key_hit(SELECT_CARD))
481 {
482 round_end_cashout();
483
484 state_machine_change_state(&round_end_sm, DISMISS_ROUND_END_PANEL);
485 g_game_vars.timer = TM_ZERO;
486
487 sprite_hide(g_game_vars.round_end_blind_token); // Hide the blind token object
488 tte_erase_rect_wrapper(BLIND_TOKEN_TEXT_RECT); // Erase the blind token text
489 }
490}
491
492static void round_end_dismiss_round_end_panel(void)
493{
494 Rect round_end_down = ROUND_END_MENU_RECT;
495 round_end_down.top--;
496 main_bg_se_copy_rect_1_tile_vert(round_end_down, SCREEN_DOWN);
497
498 if (g_game_vars.timer >= TM_DISMISS_ROUND_END_TM)
499 {
500 g_game_vars.timer = TM_ZERO;
501 game_change_state(GAME_STATE_SHOP);
502 }
503}
504
505static void round_end_extend_black_panel_down(int black_panel_bottom)
506{
507 Rect single_line_rect = ROUND_END_MENU_RECT;
508 single_line_rect.bottom = black_panel_bottom;
509 single_line_rect.top = single_line_rect.bottom - 1;
510 main_bg_se_copy_rect_1_tile_vert(single_line_rect, SCREEN_DOWN);
511}
512
514{
515 // Disable window 0 so it doesn't make the cashout menu transparent
516 toggle_windows(false, true);
517
518 main_bg_se_clear_rect(ROUND_END_MENU_RECT);
519 tte_erase_rect_wrapper(HAND_SIZE_RECT);
520}
521
523{
524 // Reset hand size in case it was increased by the Juggle Tag
525 g_game_vars.hand_size = DEFAULT_HAND_SIZE;
526 g_game_vars.timer = 0;
527 g_game_vars.nb_unused_discards += g_game_vars.discards;
528 state_machine_register(&round_end_sm);
529 state_machine_change_state(&round_end_sm, ROUND_END_START);
530}
531
533{
534 // Substate logic only
535}
536
538{
539 for (enum RewardType i = 0; i < REWARD_TYPE_MAX; i++)
540 {
541 s_reward_data[i].count = 0;
542 s_reward_data[i].remaining = 0;
543 }
544 s_blind_reward = 0;
545 s_cashout = 0;
546
547 // Cleanup blind tokens from this round to avoid accumulating
548 // allocated blind sprites each round
549 sprite_destroy(&g_game_vars.playing_blind_token);
550 sprite_destroy(&g_game_vars.round_end_blind_token);
551 state_machine_remove(&round_end_sm);
552 // TODO: Reuse sprites for blind selection?
553}
Utilities for affine background on GBA.
void affine_background_load_palette(const u16 *src)
Set the background palette.
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
int blind_get_reward(enum BlindType type)
Get the amount of money gained from beating a certain Blind.
Definition blind.c:110
void change_background(enum BackgroundId id, bool force_redraw)
Definition common_ui.c:35
void reset_top_left_panel_bottom_row(void)
Restores the bottom row of the top-left panel from the background map.
Definition common_ui.c:48
Game global game variables struct definition.
void main_bg_se_copy_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction)
Copies a rect in the main background vertically in direction by a single tile.
void main_bg_se_clear_rect(Rect se_rect)
Clears a rect in the main background.
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.
void main_bg_se_copy_expand_3x3_rect(Rect se_rect_dest, BG_POINT se_rect_src_3x3_top_left)
Copies a 3x3 rect and expands it to fit a passed rect. Special case of the 9-patch.
void tte_erase_rect_wrapper(Rect rect)
A wrapper for tte_erase_rect that would use the rect struct.
Header of shared rects and points needed for ui.
Round end game state functions.
void round_end_change_background(void)
Change to the round end background.
Definition round_end.c:513
void round_end_on_exit(void)
Round end cleanup.
Definition round_end.c:537
void round_end_on_init(void)
Round end state initialization.
Definition round_end.c:522
void round_end_on_update(void)
Round end state update.
Definition round_end.c:532
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
int skip_tag_count(enum SkipTagTypes tag_type)
Counts how many tags of a certain type we own.
Definition skip_tag.c:213
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
void sprite_unhide(Sprite *sprite)
Unhides the sprite by manipulating ATTR0_HIDE in OAM. The sprite's ATTR0_MODE is maintained from the ...
Definition sprite.c:229
State Machine.
void state_machine_remove(StateMachine *state_machine)
Remove a statemachine's update function.
void state_machine_register(StateMachine *state_machine)
Register a statemachine to run it's update function once per frame.
void state_machine_change_state(StateMachine *state_machine, int new_state)
Calls the current state's on_exit, the new state's on_init, and sets the active update fn.
Data struct for the different types of rewards.
Definition round_end.c:66
const char * text
the name of the reward (i.e. "Hands")
Definition round_end.c:70
const int multiplier
how many dollars the player recieves every time a certain type of reward is counted towards the total...
Definition round_end.c:85
int remaining
how many times the reward still needs to be counted for this End of Round
Definition round_end.c:95
const u8 text_color
the color the name of the reward is printed with
Definition round_end.c:75
int count
how many times the reward was earned by the player during the last round
Definition round_end.c:90
State machine callbacks.
State machine instance.
Timer constants.
Utilities relating around number string representation and protected arithmatic helper functions.