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 "state_machine.h"
9#include "timer.h"
10#include "util.h"
11
12enum GameRoundEndStates
13{
14 ROUND_END_START,
15 START_EXPAND_POPUP,
16 DISPLAY_FINISHED_BLIND,
17 DISPLAY_SCORE_MIN,
18 UPDATE_BLIND_REWARD,
19 BLIND_PANEL_EXIT,
20 DISPLAY_REWARDS,
21 DISPLAY_CASHOUT,
22 DISMISS_ROUND_END_PANEL,
23 ROUND_END_STATES_MAX
24};
25
26static const u32 TM_RESET_STATIC_VARS = 30;
27static const u32 TM_END_POP_MENU_ANIM = 13;
28static const u32 TM_START_ROUND_END_REWARDS_ANIM = 1;
29static const u32 TM_END_DISPLAY_FIN_BLIND = 30;
30static const u32 TM_END_DISPLAY_SCORE_MIN = 4;
31static const u32 TM_DISMISS_ROUND_END_TM = 20;
32
33static const u32 REWARD_PANEL_BORDER_PID = 19;
34
35static const u32 ROUND_END_BLACK_PANEL_INIT_BOTTOM_SE = 12;
36static const u32 ROUND_END_REWARD_TEXT_X = 88;
37static const u32 ROUND_END_REWARD_AMOUNT_X = 168;
38
39// clang-format off
40static const Rect ROUND_END_BLIND_REQ_RECT = {104, 96, 136, UNDEFINED};
41static const Rect ROUND_END_BLIND_REWARD_RECT = {168, 96, UNDEFINED, UNDEFINED};
42static const Rect CASHOUT_DEST_RECT = {10, 8, 23, 10 };
43static const Rect CASHOUT_TEXT_RECT = {88, 72, UNDEFINED, UNDEFINED};
44static const Rect BLIND_TOKEN_TEXT_RECT = {80, 72, 200, 160 };
45static const Rect ROUND_END_MENU_RECT = {9, 7, 24, 20 };
46
47static const BG_POINT CASHOUT_SRC_3X3_RECT_POS = {5, 29};
48// clang-format on
49
50static int blind_reward = 0;
51static int hand_reward = 0;
52static int interest_reward = 0;
53static int interest_to_count = 0;
54static int interest_start_time = UNDEFINED;
55
56static int calculate_interest_reward(void);
57
58static void game_round_end_start(void);
59static void game_round_end_start_expand_popup(void);
60static void game_round_end_display_finished_blind(void);
61static void game_round_end_display_score_min(void);
62static void game_round_end_update_blind_reward(void);
63static void game_round_end_panel_exit(void);
64static void game_round_end_display_rewards(void);
65static void game_round_end_display_cashout(void);
66static void game_round_end_dismiss_round_end_panel(void);
67
68static void game_round_end_extend_black_panel_down(int black_panel_bottom);
69
70static StateInfo state_info[] = {
71 STATE_INFO_UPDATE_FN_ONLY(game_round_end_start),
72 STATE_INFO_UPDATE_FN_ONLY(game_round_end_start_expand_popup),
73 STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_finished_blind),
74 STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_score_min),
75 STATE_INFO_UPDATE_FN_ONLY(game_round_end_update_blind_reward),
76 STATE_INFO_UPDATE_FN_ONLY(game_round_end_panel_exit),
77 STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_rewards),
78 STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_cashout),
79 STATE_INFO_UPDATE_FN_ONLY(game_round_end_dismiss_round_end_panel),
80};
81
82static StateMachine round_end_sm = STATE_MACHINE_DEFINE(state_info, ROUND_END_STATES_MAX);
83
84static int calculate_interest_reward(void)
85{
86 int reward = (g_game_vars.money / 5) * INTEREST_PER_5;
87 if (reward > MAX_INTEREST)
88 reward = MAX_INTEREST;
89 return reward;
90}
91
92static void game_round_end_start(void)
93{
94 // Reset static variables to default values upon re-entering the round end state
95 if (g_game_vars.timer == TM_RESET_STATIC_VARS)
96 {
97 change_background(BG_ROUND_END, false); // Change the background to the round end background
98 state_machine_change_state(&round_end_sm, START_EXPAND_POPUP);
99 g_game_vars.timer = TM_ZERO; // Reset the timer
100 blind_reward = blind_get_reward(g_game_vars.current_blind);
101 hand_reward = g_game_vars.hands;
102 interest_reward = calculate_interest_reward();
103 interest_to_count = interest_reward;
104 interest_start_time = UNDEFINED;
105 }
106}
107
108static void game_round_end_start_expand_popup(void)
109{
110 main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP);
111
112 if (g_game_vars.timer == TM_END_POP_MENU_ANIM)
113 {
114 state_machine_change_state(&round_end_sm, DISPLAY_FINISHED_BLIND);
115 g_game_vars.timer = TM_ZERO;
116 }
117}
118
119static void game_round_end_display_finished_blind(void)
120{
121 sprite_unhide(g_game_vars.round_end_blind_token);
122
123 int current_ante = g_game_vars.ante;
124
125 // Beating the boss blind increases the ante, so we need to display the previous ante value
126 if (g_game_vars.current_blind > BLIND_TYPE_BIG)
127 current_ante--;
128
129 Rect blind_req_rect = ROUND_END_BLIND_REQ_RECT;
130 u32 blind_req = blind_get_requirement(g_game_vars.current_blind, current_ante);
131
132 /* Not bothering to truncate here because there are 8 tiles
133 * and the blind requirement will not increase past ante 8
134 * so there's enough room for sure.
135 */
136 char blind_req_str_buff[UINT_MAX_DIGITS + 1];
137 snprintf(blind_req_str_buff, sizeof(blind_req_str_buff), "%lu", blind_req);
138
139 update_text_rect_to_right_align_str(&blind_req_rect, blind_req_str_buff, OVERFLOW_RIGHT);
140
141 tte_printf(
142 "#{P:%d,%d; cx:0x%X000}%s",
143 blind_req_rect.left,
144 blind_req_rect.top,
145 TTE_RED_PB,
146 blind_req_str_buff
147 );
148
149 if (g_game_vars.timer == TM_START_ROUND_END_REWARDS_ANIM)
150 {
151 game_round_end_extend_black_panel_down(ROUND_END_BLACK_PANEL_INIT_BOTTOM_SE);
152 }
153
154 if (g_game_vars.timer >= TM_END_DISPLAY_FIN_BLIND)
155 {
156 state_machine_change_state(&round_end_sm, DISPLAY_SCORE_MIN);
157 g_game_vars.timer = TM_ZERO;
158 }
159}
160
161static void game_round_end_display_score_min(void)
162{
163 const int timer_offset = g_game_vars.timer - 1;
164 const int x_from = 0;
165 const int y_from = 29;
166 const int x_to = 13;
167 const int y_to = 11;
168
169 memcpy16(
170 &se_mem[MAIN_BG_SBB][x_to + timer_offset + 32 * y_to],
171 &se_mem[MAIN_BG_SBB][x_from + timer_offset + 32 * y_from],
172 1
173 );
174
175 if (g_game_vars.timer >= TM_END_DISPLAY_SCORE_MIN)
176 {
177 state_machine_change_state(&round_end_sm, UPDATE_BLIND_REWARD);
178 g_game_vars.timer = TM_ZERO;
179 }
180}
181
182static void game_round_end_update_blind_reward(void)
183{
184 if (g_game_vars.timer % FRAMES(20) != 0)
185 return;
186
187 // TODO: Add sound effect here
188
189 if (blind_reward > 0)
190 {
191 blind_reward--;
192 tte_printf(
193 "#{P:%d,%d; cx:0x%X000}$%d",
194 BLIND_REWARD_RECT.left,
195 BLIND_REWARD_RECT.top,
196 TTE_YELLOW_PB,
197 blind_reward
198 );
199 tte_printf(
200 "#{P:%d,%d; cx:0x%X000}$%d",
201 ROUND_END_BLIND_REWARD_RECT.left,
202 ROUND_END_BLIND_REWARD_RECT.top,
203 TTE_YELLOW_PB,
204 blind_get_reward(g_game_vars.current_blind) - blind_reward
205 );
206 }
207 else if (g_game_vars.timer > FRAMES(20))
208 {
209 tte_erase_rect_wrapper(BLIND_REWARD_RECT);
210 tte_erase_rect_wrapper(BLIND_REQ_TEXT_RECT);
211 sprite_hide(g_game_vars.playing_blind_token);
212 affine_background_load_palette(affine_background_gfxPal);
213 state_machine_change_state(&round_end_sm, BLIND_PANEL_EXIT);
214 g_game_vars.timer = TM_ZERO;
215 }
216}
217
218static void game_round_end_panel_exit(void)
219{
220 // TODO: make heads or tails of what's going on here and replace
221 // magic numbers.
222 if (g_game_vars.timer < 8)
223 {
224 main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SCREEN_UP);
225
226 if (g_game_vars.timer == 1)
227 {
229 }
230 else if (g_game_vars.timer == 2)
231 {
232 int y = 5;
233 memset16(&se_mem[MAIN_BG_SBB][32 * (y - 1)], 0x0001, 1);
234 memset16(&se_mem[MAIN_BG_SBB][1 + 32 * (y - 1)], 0x0002, 7);
235 memset16(&se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], 0x0401, 1);
236 }
237 }
238 else if (g_game_vars.timer > FRAMES(20))
239 {
240 memset16(&pal_bg_mem[REWARD_PANEL_BORDER_PID], 0x1483, 1);
241 state_machine_change_state(&round_end_sm, DISPLAY_REWARDS);
242 g_game_vars.timer = TM_ZERO;
243 }
244}
245
246static inline void game_round_end_print_separator_ellipsis(void)
247{
248 int x =
249 (ROUND_END_REWARDS_ELLIPSIS_POS.x + g_game_vars.timer - TM_REWARDS_ELLIPSIS_PRINT_START) *
250 TILE_SIZE;
251 int y = (ROUND_END_REWARDS_ELLIPSIS_POS.y) * TILE_SIZE;
252
253 tte_printf("#{P:%d,%d; cx:0x%X000}.", x, y, TTE_WHITE_PB);
254}
255
256// TODO: Allow for more generic rewards and consolidate with game_round_end_print_interest_reward()
257static inline void game_round_end_print_hand_reward(int hand_y_offset)
258{
259 int hand_y = ROUND_END_REWARDS_ELLIPSIS_POS.y + hand_y_offset;
260 if (g_game_vars.timer == TM_DISPLAY_REWARDS_CONT_WAIT)
261 {
262 game_round_end_extend_black_panel_down(hand_y);
263
264 tte_printf(
265 "#{P:%lu,%d; cx:0x%X000}%d #{cx:0x%X000}Hands",
266 ROUND_END_REWARD_TEXT_X,
267 hand_y * TILE_SIZE,
268 TTE_BLUE_PB,
269 hand_reward,
270 TTE_WHITE_PB
271 );
272 }
273 // Increment the hand reward text until the hand reward variable is depleted
274 else if (g_game_vars.timer > TM_HAND_REWARD_INCR_WAIT &&
275 g_game_vars.timer % FRAMES(TM_REWARD_INCREMENT_INTERVAL) == 0)
276 {
277 hand_reward--;
278 tte_printf(
279 "#{P:%lu, %d; cx:0x%X000}$%ld",
280 ROUND_END_REWARD_AMOUNT_X,
281 hand_y * TILE_SIZE,
282 TTE_YELLOW_PB,
283 g_game_vars.hands - hand_reward
284 );
285 if (hand_reward == 0)
286 {
287 interest_start_time = g_game_vars.timer + TM_REWARD_DISPLAY_INTERVAL;
288 }
289 }
290}
291
292static inline void game_round_end_print_interest_reward(int interest_y_offset)
293{
294 int interest_y = ROUND_END_REWARDS_ELLIPSIS_POS.y + interest_y_offset;
295
296 if (g_game_vars.timer == interest_start_time)
297 {
298 game_round_end_extend_black_panel_down(interest_y);
299
300 tte_printf(
301 "#{P:%lu,%d; cx:0x%X000}%d #{cx:0x%X000}Interest",
302 ROUND_END_REWARD_TEXT_X,
303 interest_y * TILE_SIZE,
304 TTE_YELLOW_PB,
305 interest_reward,
306 TTE_WHITE_PB
307 );
308 }
309 // Increment the interest reward text until the interest reward variable is depleted
310 else if (g_game_vars.timer > interest_start_time + TM_REWARD_DISPLAY_INTERVAL &&
311 g_game_vars.timer % FRAMES(TM_REWARD_INCREMENT_INTERVAL) == 0)
312 {
313 interest_to_count--;
314 tte_printf(
315 "#{P:%lu, %d; cx:0x%X000}$%d",
316 ROUND_END_REWARD_AMOUNT_X,
317 interest_y * TILE_SIZE,
318 TTE_YELLOW_PB,
319 interest_reward - interest_to_count
320 );
321 }
322}
323
324static void game_round_end_display_rewards(void)
325{
326 int hand_y_offset = 0;
327 int interest_y_offset = 0;
328
329 if (g_game_vars.hands > 0)
330 {
331 hand_y_offset = 1;
332 }
333 else
334 {
335 interest_start_time = TM_DISPLAY_REWARDS_CONT_WAIT;
336 }
337
338 if (interest_reward > 0)
339 {
340 interest_y_offset = hand_y_offset + 1;
341 }
342
343 // Once all rewards are accounted for go to the next state
344 if (hand_reward <= 0 && interest_to_count <= 0)
345 {
346 g_game_vars.timer = TM_ZERO;
347 state_machine_change_state(&round_end_sm, DISPLAY_CASHOUT);
348 }
349 else if (g_game_vars.timer == TM_START_ROUND_END_REWARDS_ANIM)
350 {
351 game_round_end_extend_black_panel_down(ROUND_END_REWARDS_ELLIPSIS_POS.y);
352 }
353 else if (g_game_vars.timer < TM_REWARDS_ELLIPSIS_PRINT_END)
354 {
355 game_round_end_print_separator_ellipsis();
356 }
357 else if (g_game_vars.timer >= TM_DISPLAY_REWARDS_CONT_WAIT && hand_reward > 0)
358 {
359 game_round_end_print_hand_reward(hand_y_offset);
360 }
361 else if (interest_start_time != UNDEFINED && g_game_vars.timer >= interest_start_time &&
362 interest_to_count > 0)
363 {
364 game_round_end_print_interest_reward(interest_y_offset);
365 }
366}
367
368static inline void game_round_end_cashout(void)
369{
370 // Reward the player
371 g_game_vars.money += g_game_vars.hands + blind_get_reward(g_game_vars.current_blind) +
372 calculate_interest_reward();
373 display_money();
374
375 g_game_vars.hands = MAX_HANDS; // Reset the hands to the maximum
376 g_game_vars.discards = MAX_DISCARDS; // Reset the discards to the maximum
377 // TODO: these can just be in one spot, passing global to global
378 display_hands(); // Set the hands display
379 display_discards(); // Set the discards display
380
381 g_game_vars.score = 0;
382 display_score(g_game_vars.score); // Set the score display
383}
384
385static void game_round_end_display_cashout()
386{
387 if (g_game_vars.timer == FRAMES(40))
388 {
389 // Put the "cash out" button onto the round end panel
390 main_bg_se_copy_expand_3x3_rect(CASHOUT_DEST_RECT, CASHOUT_SRC_3X3_RECT_POS);
391
392 int cashout_amount = g_game_vars.hands + blind_get_reward(g_game_vars.current_blind) +
393 calculate_interest_reward();
394
395 bool omit_space = cashout_amount >= 10;
396 tte_printf(
397 "#{P:%d, %d; cx:0x%X000}Cash Out:%s$%d",
398 CASHOUT_TEXT_RECT.left,
399 CASHOUT_TEXT_RECT.top,
400 TTE_WHITE_PB,
401 omit_space ? "" : " ",
402 cashout_amount
403 );
404 }
405
406 // Wait until the player presses A to cash out
407 else if (g_game_vars.timer > FRAMES(40) && key_hit(SELECT_CARD))
408 {
409 game_round_end_cashout();
410
411 state_machine_change_state(&round_end_sm, DISMISS_ROUND_END_PANEL);
412 g_game_vars.timer = TM_ZERO;
413
414 sprite_hide(g_game_vars.round_end_blind_token); // Hide the blind token object
415 tte_erase_rect_wrapper(BLIND_TOKEN_TEXT_RECT); // Erase the blind token text
416 }
417}
418
419static void game_round_end_dismiss_round_end_panel(void)
420{
421 Rect round_end_down = ROUND_END_MENU_RECT;
422 round_end_down.top--;
423 main_bg_se_copy_rect_1_tile_vert(round_end_down, SCREEN_DOWN);
424
425 if (g_game_vars.timer >= TM_DISMISS_ROUND_END_TM)
426 {
427 g_game_vars.timer = TM_ZERO;
428 game_change_state(GAME_STATE_SHOP);
429 }
430}
431
432static void game_round_end_extend_black_panel_down(int black_panel_bottom)
433{
434 Rect single_line_rect = ROUND_END_MENU_RECT;
435 single_line_rect.bottom = black_panel_bottom;
436 single_line_rect.top = single_line_rect.bottom - 1;
437 main_bg_se_copy_rect_1_tile_vert(single_line_rect, SCREEN_DOWN);
438}
439
441{
442 // Disable window 0 so it doesn't make the cashout menu transparent
443 toggle_windows(false, true);
444
445 main_bg_se_clear_rect(ROUND_END_MENU_RECT);
446 tte_erase_rect_wrapper(HAND_SIZE_RECT);
447}
448
450{
451 g_game_vars.timer = 0;
452 state_machine_register(&round_end_sm);
453 state_machine_change_state(&round_end_sm, ROUND_END_START);
454}
455
457{
458 // Substate logic only
459}
460
462{
463 // Cleanup blind tokens from this round to avoid accumulating
464 // allocated blind sprites each round
465 blind_reward = 0;
466 hand_reward = 0;
467 interest_reward = 0;
468 sprite_destroy(&g_game_vars.playing_blind_token);
469 sprite_destroy(&g_game_vars.round_end_blind_token);
470 state_machine_remove(&round_end_sm);
471 // TODO: Reuse sprites for blind selection?
472}
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:106
int blind_get_reward(enum BlindType type)
Get the amount of money gained from beating a certain Blind.
Definition blind.c:115
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 game_round_end_on_exit(void)
Round end cleanup.
Definition round_end.c:461
void game_round_end_on_init(void)
Round end state initialization.
Definition round_end.c:449
void game_round_end_on_update(void)
Round end state update.
Definition round_end.c:456
void game_round_end_change_background(void)
Change to the round end background.
Definition round_end.c:440
void sprite_destroy(Sprite **sprite)
Destroy Sprite.
Definition sprite.c:93
void sprite_hide(Sprite *sprite)
Hides the sprite by manipulating ATTR0_HIDE in OAM.
Definition sprite.c:171
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:178
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.
State machine callbacks.
State machine instance.
Timer constants.
Utilities relating around number string representation and protected arithmatic helper functions.