#include "round_end.h" #include "affine_background.h" #include "affine_background_gfx.h" #include "game.h" #include "game_variables.h" #include "layout.h" #include "state_machine.h" #include "timer.h" #include "util.h" enum GameRoundEndStates { ROUND_END_START, START_EXPAND_POPUP, DISPLAY_FINISHED_BLIND, DISPLAY_SCORE_MIN, UPDATE_BLIND_REWARD, BLIND_PANEL_EXIT, DISPLAY_REWARDS, DISPLAY_CASHOUT, DISMISS_ROUND_END_PANEL, ROUND_END_STATES_MAX }; static const u32 TM_RESET_STATIC_VARS = 30; static const u32 TM_END_POP_MENU_ANIM = 13; static const u32 TM_START_ROUND_END_REWARDS_ANIM = 1; static const u32 TM_END_DISPLAY_FIN_BLIND = 30; static const u32 TM_END_DISPLAY_SCORE_MIN = 4; static const u32 TM_DISMISS_ROUND_END_TM = 20; static const u32 REWARD_PANEL_BORDER_PID = 19; static const u32 ROUND_END_BLACK_PANEL_INIT_BOTTOM_SE = 12; static const u32 ROUND_END_REWARD_TEXT_X = 88; static const u32 ROUND_END_REWARD_AMOUNT_X = 168; // clang-format off static const Rect ROUND_END_BLIND_REQ_RECT = {104, 96, 136, UNDEFINED}; static const Rect ROUND_END_BLIND_REWARD_RECT = {168, 96, UNDEFINED, UNDEFINED}; static const Rect CASHOUT_DEST_RECT = {10, 8, 23, 10 }; static const Rect CASHOUT_TEXT_RECT = {88, 72, UNDEFINED, UNDEFINED}; static const Rect BLIND_TOKEN_TEXT_RECT = {80, 72, 200, 160 }; static const Rect ROUND_END_MENU_RECT = {9, 7, 24, 20 }; static const BG_POINT CASHOUT_SRC_3X3_RECT_POS = {5, 29}; // clang-format on static int blind_reward = 0; static int hand_reward = 0; static int interest_reward = 0; static int interest_to_count = 0; static int interest_start_time = UNDEFINED; static int calculate_interest_reward(void); static void game_round_end_start(void); static void game_round_end_start_expand_popup(void); static void game_round_end_display_finished_blind(void); static void game_round_end_display_score_min(void); static void game_round_end_update_blind_reward(void); static void game_round_end_panel_exit(void); static void game_round_end_display_rewards(void); static void game_round_end_display_cashout(void); static void game_round_end_dismiss_round_end_panel(void); static void game_round_end_extend_black_panel_down(int black_panel_bottom); static StateInfo state_info[] = { STATE_INFO_UPDATE_FN_ONLY(game_round_end_start), STATE_INFO_UPDATE_FN_ONLY(game_round_end_start_expand_popup), STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_finished_blind), STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_score_min), STATE_INFO_UPDATE_FN_ONLY(game_round_end_update_blind_reward), STATE_INFO_UPDATE_FN_ONLY(game_round_end_panel_exit), STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_rewards), STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_cashout), STATE_INFO_UPDATE_FN_ONLY(game_round_end_dismiss_round_end_panel), }; static StateMachine round_end_sm = { .state_infos = &state_info[0], .num_infos = ROUND_END_STATES_MAX, }; static int calculate_interest_reward(void) { int reward = (g_game_vars.money / 5) * INTEREST_PER_5; if (reward > MAX_INTEREST) reward = MAX_INTEREST; return reward; } static void game_round_end_start(void) { // Reset static variables to default values upon re-entering the round end state if (g_game_vars.timer == TM_RESET_STATIC_VARS) { change_background(BG_ROUND_END, false); // Change the background to the round end background state_machine_change_state(&round_end_sm, START_EXPAND_POPUP); g_game_vars.timer = TM_ZERO; // Reset the timer blind_reward = blind_get_reward(g_game_vars.current_blind); hand_reward = g_game_vars.hands; interest_reward = calculate_interest_reward(); interest_to_count = interest_reward; interest_start_time = UNDEFINED; } } static void game_round_end_start_expand_popup(void) { main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP); if (g_game_vars.timer == TM_END_POP_MENU_ANIM) { state_machine_change_state(&round_end_sm, DISPLAY_FINISHED_BLIND); g_game_vars.timer = TM_ZERO; } } static void game_round_end_display_finished_blind(void) { obj_unhide(g_game_vars.round_end_blind_token->obj, 0); int current_ante = g_game_vars.ante; // Beating the boss blind increases the ante, so we need to display the previous ante value if (g_game_vars.current_blind > BLIND_TYPE_BIG) current_ante--; Rect blind_req_rect = ROUND_END_BLIND_REQ_RECT; u32 blind_req = blind_get_requirement(g_game_vars.current_blind, current_ante); /* Not bothering to truncate here because there are 8 tiles * and the blind requirement will not increase past ante 8 * so there's enough room for sure. */ char blind_req_str_buff[UINT_MAX_DIGITS + 1]; snprintf(blind_req_str_buff, sizeof(blind_req_str_buff), "%lu", blind_req); update_text_rect_to_right_align_str(&blind_req_rect, blind_req_str_buff, OVERFLOW_RIGHT); tte_printf( "#{P:%d,%d; cx:0x%X000}%s", blind_req_rect.left, blind_req_rect.top, TTE_RED_PB, blind_req_str_buff ); if (g_game_vars.timer == TM_START_ROUND_END_REWARDS_ANIM) { game_round_end_extend_black_panel_down(ROUND_END_BLACK_PANEL_INIT_BOTTOM_SE); } if (g_game_vars.timer >= TM_END_DISPLAY_FIN_BLIND) { state_machine_change_state(&round_end_sm, DISPLAY_SCORE_MIN); g_game_vars.timer = TM_ZERO; } } static void game_round_end_display_score_min(void) { const int timer_offset = g_game_vars.timer - 1; const int x_from = 0; const int y_from = 29; const int x_to = 13; const int y_to = 11; memcpy16( &se_mem[MAIN_BG_SBB][x_to + timer_offset + 32 * y_to], &se_mem[MAIN_BG_SBB][x_from + timer_offset + 32 * y_from], 1 ); if (g_game_vars.timer >= TM_END_DISPLAY_SCORE_MIN) { state_machine_change_state(&round_end_sm, UPDATE_BLIND_REWARD); g_game_vars.timer = TM_ZERO; } } static void game_round_end_update_blind_reward(void) { if (g_game_vars.timer % FRAMES(20) != 0) return; // TODO: Add sound effect here if (blind_reward > 0) { blind_reward--; tte_printf( "#{P:%d,%d; cx:0x%X000}$%d", BLIND_REWARD_RECT.left, BLIND_REWARD_RECT.top, TTE_YELLOW_PB, blind_reward ); tte_printf( "#{P:%d,%d; cx:0x%X000}$%d", ROUND_END_BLIND_REWARD_RECT.left, ROUND_END_BLIND_REWARD_RECT.top, TTE_YELLOW_PB, blind_get_reward(g_game_vars.current_blind) - blind_reward ); } else if (g_game_vars.timer > FRAMES(20)) { tte_erase_rect_wrapper(BLIND_REWARD_RECT); tte_erase_rect_wrapper(BLIND_REQ_TEXT_RECT); obj_hide(g_game_vars.playing_blind_token->obj); affine_background_load_palette(affine_background_gfxPal); state_machine_change_state(&round_end_sm, BLIND_PANEL_EXIT); g_game_vars.timer = TM_ZERO; } } static void game_round_end_panel_exit(void) { // TODO: make heads or tails of what's going on here and replace // magic numbers. if (g_game_vars.timer < 8) { main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SCREEN_UP); if (g_game_vars.timer == 1) { reset_top_left_panel_bottom_row(); } else if (g_game_vars.timer == 2) { int y = 5; memset16(&se_mem[MAIN_BG_SBB][32 * (y - 1)], 0x0001, 1); memset16(&se_mem[MAIN_BG_SBB][1 + 32 * (y - 1)], 0x0002, 7); memset16(&se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], 0x0401, 1); } } else if (g_game_vars.timer > FRAMES(20)) { memset16(&pal_bg_mem[REWARD_PANEL_BORDER_PID], 0x1483, 1); state_machine_change_state(&round_end_sm, DISPLAY_REWARDS); g_game_vars.timer = TM_ZERO; } } static inline void game_round_end_print_separator_ellipsis(void) { int x = (ROUND_END_REWARDS_ELLIPSIS_POS.x + g_game_vars.timer - TM_REWARDS_ELLIPSIS_PRINT_START) * TILE_SIZE; int y = (ROUND_END_REWARDS_ELLIPSIS_POS.y) * TILE_SIZE; tte_printf("#{P:%d,%d; cx:0x%X000}.", x, y, TTE_WHITE_PB); } // TODO: Allow for more generic rewards and consolidate with game_round_end_print_interest_reward() static inline void game_round_end_print_hand_reward(int hand_y_offset) { int hand_y = ROUND_END_REWARDS_ELLIPSIS_POS.y + hand_y_offset; if (g_game_vars.timer == TM_DISPLAY_REWARDS_CONT_WAIT) { game_round_end_extend_black_panel_down(hand_y); tte_printf( "#{P:%lu,%d; cx:0x%X000}%d #{cx:0x%X000}Hands", ROUND_END_REWARD_TEXT_X, hand_y * TILE_SIZE, TTE_BLUE_PB, hand_reward, TTE_WHITE_PB ); } // Increment the hand reward text until the hand reward variable is depleted else if (g_game_vars.timer > TM_HAND_REWARD_INCR_WAIT && g_game_vars.timer % FRAMES(TM_REWARD_INCREMENT_INTERVAL) == 0) { hand_reward--; tte_printf( "#{P:%lu, %d; cx:0x%X000}$%ld", ROUND_END_REWARD_AMOUNT_X, hand_y * TILE_SIZE, TTE_YELLOW_PB, g_game_vars.hands - hand_reward ); if (hand_reward == 0) { interest_start_time = g_game_vars.timer + TM_REWARD_DISPLAY_INTERVAL; } } } static inline void game_round_end_print_interest_reward(int interest_y_offset) { int interest_y = ROUND_END_REWARDS_ELLIPSIS_POS.y + interest_y_offset; if (g_game_vars.timer == interest_start_time) { game_round_end_extend_black_panel_down(interest_y); tte_printf( "#{P:%lu,%d; cx:0x%X000}%d #{cx:0x%X000}Interest", ROUND_END_REWARD_TEXT_X, interest_y * TILE_SIZE, TTE_YELLOW_PB, interest_reward, TTE_WHITE_PB ); } // Increment the interest reward text until the interest reward variable is depleted else if (g_game_vars.timer > interest_start_time + TM_REWARD_DISPLAY_INTERVAL && g_game_vars.timer % FRAMES(TM_REWARD_INCREMENT_INTERVAL) == 0) { interest_to_count--; tte_printf( "#{P:%lu, %d; cx:0x%X000}$%d", ROUND_END_REWARD_AMOUNT_X, interest_y * TILE_SIZE, TTE_YELLOW_PB, interest_reward - interest_to_count ); } } static void game_round_end_display_rewards(void) { int hand_y_offset = 0; int interest_y_offset = 0; if (g_game_vars.hands > 0) { hand_y_offset = 1; } else { interest_start_time = TM_DISPLAY_REWARDS_CONT_WAIT; } if (interest_reward > 0) { interest_y_offset = hand_y_offset + 1; } // Once all rewards are accounted for go to the next state if (hand_reward <= 0 && interest_to_count <= 0) { g_game_vars.timer = TM_ZERO; state_machine_change_state(&round_end_sm, DISPLAY_CASHOUT); } else if (g_game_vars.timer == TM_START_ROUND_END_REWARDS_ANIM) { game_round_end_extend_black_panel_down(ROUND_END_REWARDS_ELLIPSIS_POS.y); } else if (g_game_vars.timer < TM_REWARDS_ELLIPSIS_PRINT_END) { game_round_end_print_separator_ellipsis(); } else if (g_game_vars.timer >= TM_DISPLAY_REWARDS_CONT_WAIT && hand_reward > 0) { game_round_end_print_hand_reward(hand_y_offset); } else if (interest_start_time != UNDEFINED && g_game_vars.timer >= interest_start_time && interest_to_count > 0) { game_round_end_print_interest_reward(interest_y_offset); } } static inline void game_round_end_cashout(void) { // Reward the player g_game_vars.money += g_game_vars.hands + blind_get_reward(g_game_vars.current_blind) + calculate_interest_reward(); display_money(); g_game_vars.hands = MAX_HANDS; // Reset the hands to the maximum g_game_vars.discards = MAX_DISCARDS; // Reset the discards to the maximum // TODO: these can just be in one spot, passing global to global display_hands(); // Set the hands display display_discards(); // Set the discards display g_game_vars.score = 0; display_score(g_game_vars.score); // Set the score display } static void game_round_end_display_cashout() { if (g_game_vars.timer == FRAMES(40)) { // Put the "cash out" button onto the round end panel main_bg_se_copy_expand_3x3_rect(CASHOUT_DEST_RECT, CASHOUT_SRC_3X3_RECT_POS); int cashout_amount = g_game_vars.hands + blind_get_reward(g_game_vars.current_blind) + calculate_interest_reward(); bool omit_space = cashout_amount >= 10; tte_printf( "#{P:%d, %d; cx:0x%X000}Cash Out:%s$%d", CASHOUT_TEXT_RECT.left, CASHOUT_TEXT_RECT.top, TTE_WHITE_PB, omit_space ? "" : " ", cashout_amount ); } // Wait until the player presses A to cash out else if (g_game_vars.timer > FRAMES(40) && key_hit(SELECT_CARD)) { game_round_end_cashout(); state_machine_change_state(&round_end_sm, DISMISS_ROUND_END_PANEL); g_game_vars.timer = TM_ZERO; obj_hide(g_game_vars.round_end_blind_token->obj); // Hide the blind token object tte_erase_rect_wrapper(BLIND_TOKEN_TEXT_RECT); // Erase the blind token text } } static void game_round_end_dismiss_round_end_panel(void) { Rect round_end_down = ROUND_END_MENU_RECT; round_end_down.top--; main_bg_se_copy_rect_1_tile_vert(round_end_down, SCREEN_DOWN); if (g_game_vars.timer >= TM_DISMISS_ROUND_END_TM) { g_game_vars.timer = TM_ZERO; game_change_state(GAME_STATE_SHOP); } } static void game_round_end_extend_black_panel_down(int black_panel_bottom) { Rect single_line_rect = ROUND_END_MENU_RECT; single_line_rect.bottom = black_panel_bottom; single_line_rect.top = single_line_rect.bottom - 1; main_bg_se_copy_rect_1_tile_vert(single_line_rect, SCREEN_DOWN); } void game_round_end_change_background(void) { // Disable window 0 so it doesn't make the cashout menu transparent toggle_windows(false, true); main_bg_se_clear_rect(ROUND_END_MENU_RECT); tte_erase_rect_wrapper(HAND_SIZE_RECT); } void game_round_end_on_init(void) { g_game_vars.timer = 0; state_machine_register(&round_end_sm); state_machine_change_state(&round_end_sm, ROUND_END_START); } void game_round_end_on_update(void) { // Substate logic only } void game_round_end_on_exit(void) { // Cleanup blind tokens from this round to avoid accumulating // allocated blind sprites each round blind_reward = 0; hand_reward = 0; interest_reward = 0; sprite_destroy(&g_game_vars.playing_blind_token); sprite_destroy(&g_game_vars.round_end_blind_token); state_machine_remove(&round_end_sm); // TODO: Reuse sprites for blind selection? }