Implement Seltzer and Joker Expiration effect (#252)

* Add Seltzer art

* Implement Joker expiration

* Add exception to copying Jokers + Seltzer message fix

* Move all scoring logic to joker_object_score away from check_and_score_joker_for_event

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
This commit is contained in:
Geralt
2025-11-30 07:52:42 +01:00
committed by GitHub
parent 435a9a0a1d
commit 0953a430c4
6 changed files with 184 additions and 48 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 12 KiB

+16 -1
View File
@@ -1,6 +1,8 @@
#ifndef GAME_H
#define GAME_H
#include <tonc.h>
#define MAX_HAND_SIZE 16
#define MAX_DECK_SIZE 52
#define MAX_JOKERS_HELD_SIZE 5 // This doesn't account for negatives right now.
@@ -56,6 +58,7 @@ enum PlayState
PLAY_SCORING_CARD_JOKERS,
PLAY_SCORING_HELD_CARDS,
PLAY_SCORING_INDEPENDENT_JOKERS,
PLAY_SCORING_HAND_SCORED_END,
PLAY_ENDING,
PLAY_ENDED
};
@@ -109,11 +112,23 @@ int get_scored_card_index(void);
bool is_joker_owned(int joker_id);
bool card_is_face(Card *card);
List* get_jokers_list(void);
List* get_expired_jokers_list(void);
int get_deck_top(void);
int get_num_discards_remaining(void);
int get_num_hands_remaining(void);
int get_money(void);
u32 get_chips(void);
void set_chips(u32 new_chips);
void display_chips();
u32 get_mult(void);
void set_mult(u32 new_mult);
void display_mult();
int get_money(void);
void set_money(int new_money);
void display_money();
void set_retrigger(bool new_retrigger);
int get_game_speed(void);
void set_game_speed(int new_game_speed);
+6 -2
View File
@@ -139,8 +139,12 @@ int joker_get_sell_value(const Joker* joker);
JokerObject *joker_object_new(Joker *joker);
void joker_object_destroy(JokerObject **joker_object);
void joker_object_update(JokerObject *joker_object);
void joker_object_shake(JokerObject *joker_object, mm_word sound_id); // This doesn't actually score anything, it just performs an animation and plays a sound effect
bool joker_object_score(JokerObject *joker_object, CardObject* card_object, enum JokerEvent joker_event, u32 *chips, u32 *mult, int *money, bool *retrigger); // This scores the joker and returns true if it was scored successfully (Card = NULL means the joker is independent and not scored by a card)
// This doesn't actually score anything, it just performs an animation and plays a sound effect
void joker_object_shake(JokerObject *joker_object, mm_word sound_id);
// This scores the joker and returns true if it was scored successfully
// card_object = NULL means the joker_event does not concern a particular Card, i.e. Independend or On_Blind_Selected
// as opposed to events that concern a particular card, i.e. On_Card_Scored or On_Card_Held
bool joker_object_score(JokerObject *joker_object, CardObject* card_object, enum JokerEvent joker_event);
void joker_object_set_selected(JokerObject* joker_object, bool selected);
bool joker_object_is_selected(JokerObject* joker_object);
+115 -21
View File
@@ -2,7 +2,6 @@
#include <maxmod.h>
#include <stdint.h>
#include <tonc.h>
#include <stdlib.h>
#include "bitset.h"
@@ -238,6 +237,7 @@ static bool sort_by_suit = false;
static List _owned_jokers_list;
static List _discarded_jokers_list;
static List _expired_jokers_list;
BITSET_DEFINE(_avail_jokers_bitset, MAX_DEFINABLE_JOKERS)
static List _shop_jokers_list;
@@ -392,6 +392,11 @@ List* get_jokers_list(void)
return &_owned_jokers_list;
}
List* get_expired_jokers_list(void)
{
return &_expired_jokers_list;
}
bool is_joker_owned(int joker_id)
{
ListItr itr = list_itr_create(&_owned_jokers_list);
@@ -478,11 +483,42 @@ void set_game_speed(int new_game_speed)
game_speed = new_game_speed;
}
u32 get_chips(void)
{
return chips;
}
void set_chips(u32 new_chips)
{
chips = new_chips;
}
u32 get_mult(void)
{
return mult;
}
void set_mult(u32 new_mult)
{
mult = new_mult;
}
int get_money(void)
{
return money;
}
void set_money(int new_money)
{
money = new_money;
}
void set_retrigger(bool new_retrigger)
{
retrigger = new_retrigger;
}
// Consts
@@ -1127,11 +1163,11 @@ void display_score(u32 value)
tte_printf("#{P:%d,48; cx:0x%X000}%lu%c", x_offset, TTE_WHITE_PB, display_value, score_suffix);
}
void display_money(int value)
void display_money()
{
int x_offset = 32 - get_digits_odd(value) * TILE_SIZE;
int x_offset = 32 - get_digits_odd(money) * TILE_SIZE;
tte_erase_rect_wrapper(MONEY_TEXT_RECT);
tte_printf("#{P:%d,%d; cx:0x%X000}$%d", x_offset, MONEY_TEXT_RECT.top, TTE_YELLOW_PB, value);
tte_printf("#{P:%d,%d; cx:0x%X000}$%d", x_offset, MONEY_TEXT_RECT.top, TTE_YELLOW_PB, money);
}
// Show/Hide flaming score effect if we will score
@@ -1474,6 +1510,7 @@ void game_init()
// Initialize all jokers list once
_owned_jokers_list = list_create();
_discarded_jokers_list = list_create();
_expired_jokers_list = list_create();
_shop_jokers_list = list_create();
// TODO: Move this to an initialization of the play scoring states
_joker_scored_itr = list_itr_create(&_owned_jokers_list);
@@ -1538,7 +1575,7 @@ void game_start()
display_hands(hands); // Hand
display_discards(discards); // Discard
display_money(money); // Set the money display
display_money(); // Set the money display
tte_printf("#{P:%d,%d; cx:0x%X000}%d#{cx:0x%X000}/%d", ANTE_TEXT_RECT.left, ANTE_TEXT_RECT.top, TTE_YELLOW_PB, ante, TTE_WHITE_PB, MAX_ANTE); // Ante
@@ -2191,12 +2228,8 @@ static bool check_and_score_joker_for_event(ListItr* starting_joker_itr, CardObj
while((joker = list_itr_next(starting_joker_itr)))
{
if (joker_object_score(joker, card_object, joker_event, &chips, &mult, &money, &retrigger))
if (joker_object_score(joker, card_object, joker_event))
{
display_chips();
display_mult();
display_money(money);
return true;
}
}
@@ -2241,7 +2274,7 @@ static bool play_before_scoring_cards_update()
}
// returns true if the scoring loop has returned early
static bool play_scoring_cards_update()
static bool play_scoring_cards_update(void)
{
if (timer % FRAMES(30) == 0 && timer > FRAMES(40))
{
@@ -2261,6 +2294,7 @@ static bool play_scoring_cards_update()
scored_card_index = hand_top;
play_state = PLAY_SCORING_HELD_CARDS;
return false;
}
@@ -2331,7 +2365,6 @@ static bool play_scoring_card_jokers_update()
// increment index to start seeking the next scoring card from the next card
scored_card_index++;
play_state = PLAY_SCORING_CARDS;
return false;
}
return false;
@@ -2356,7 +2389,7 @@ static bool play_scoring_held_cards_update(int played_idx)
}
scored_card_index = 0;
_joker_round_end_itr = list_itr_create(&_owned_jokers_list);
play_state = PLAY_SCORING_INDEPENDENT_JOKERS;
}
@@ -2377,15 +2410,29 @@ static bool play_scoring_independent_jokers_update(int played_idx)
return true;
}
// Trigger hand end effect for all jokers once they are done scoring
scored_card_index = played_top + 1; // Reset the scored card index to the top of the played stack
play_state = PLAY_SCORING_HAND_SCORED_END;
}
return false;
}
// Trigger hand end effect for all jokers once they are done scoring
static bool play_scoring_hand_scored_end_update(int played_idx)
{
if (played_idx == 0 && (timer % FRAMES(30) == 0) && timer > FRAMES(40))
{
tte_erase_rect_wrapper(PLAYED_CARDS_SCORES_RECT);
if (check_and_score_joker_for_event(&_joker_round_end_itr, NULL, JOKER_EVENT_ON_HAND_SCORED_END))
{
return true;
}
play_state = PLAY_ENDING;
timer = TM_ZERO;
scored_card_index = played_top + 1; // Reset the scored card index to the top of the played stack
play_state = PLAY_ENDING;
}
return false;
@@ -2528,6 +2575,14 @@ static void played_cards_update_loop()
return;
}
break;
case PLAY_SCORING_HAND_SCORED_END:
if (play_scoring_hand_scored_end_update(played_idx))
{
return;
}
break;
case PLAY_ENDING:
@@ -2641,7 +2696,7 @@ static int calculate_interest_reward()
static void game_round_end_cashout()
{
money += hands + blind_get_reward(current_blind) + calculate_interest_reward(); // Reward the player
display_money(money);
display_money();
hands = max_hands; // Reset the hands to the maximum
discards = max_discards; // Reset the discards to the maximum
@@ -3094,7 +3149,7 @@ static void game_shop_intro()
static void game_shop_reroll(int *reroll_cost)
{
money -= *reroll_cost;
display_money(money); // Update the money display
display_money(); // Update the money display
ListItr itr = list_itr_create(&_shop_jokers_list);
JokerObject* joker_object;
@@ -3173,7 +3228,7 @@ void game_sell_joker(int joker_idx)
JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_owned_jokers_list, joker_idx);
money += joker_get_sell_value(joker_object->joker);
display_money(money);
display_money();
erase_price_under_sprite_object(joker_object->sprite_object);
remove_owned_joker(joker_idx);
@@ -3208,7 +3263,7 @@ static void game_shop_buy_joker(int shop_joker_idx)
JokerObject *joker_object = (JokerObject*)list_get_at_idx(&_shop_jokers_list, shop_joker_idx);
money -= joker_object->joker->value; // Deduct the money spent on the joker
display_money(money); // Update the money display
display_money(); // Update the money display
erase_price_under_sprite_object(joker_object->sprite_object);
sprite_object_set_focus(joker_object->sprite_object, false);
add_to_held_jokers(joker_object);
@@ -3669,6 +3724,43 @@ static void game_main_menu_on_update()
}
}
#define EXPIRE_ANIMATION_FRAME_COUNT 3
static void expired_jokers_update_loop()
{
if(list_is_empty(&_expired_jokers_list)) {
return;
}
ListItr itr = list_itr_create(&_expired_jokers_list);
JokerObject* joker_object;
while((joker_object = list_itr_next(&itr)))
{
joker_object_update(joker_object);
// let just enough frames pass that we see it rotating and shrinking
if (timer % FRAMES(EXPIRE_ANIMATION_FRAME_COUNT) == 0)
{
// get joker idx
int expired_joker_idx = 0;
ListItr joker_itr = list_itr_create(&_owned_jokers_list);
JokerObject* expired_joker;
while ((expired_joker = list_itr_next(&joker_itr)) && expired_joker != joker_object)
{
expired_joker_idx++;
}
// Removing expired Jokers here, instead of immediately like ones we
// sell or discard allow us to have a small shrink animation without
// the other owned Jokers rearranging themselves to fill the newly
// freed space, therefore obscuring the animation
remove_owned_joker(expired_joker_idx);
list_itr_remove_current_node(&itr);
joker_object_destroy(&joker_object);
}
}
}
static void discarded_jokers_update_loop()
{
if(list_is_empty(&_discarded_jokers_list)) {
@@ -3720,6 +3812,7 @@ static void jokers_update_loop()
{
held_jokers_update_loop();
discarded_jokers_update_loop();
expired_jokers_update_loop();
}
static void game_over_anim_frame()
@@ -3767,6 +3860,7 @@ static void game_over_on_exit()
list_clear(&_owned_jokers_list);
list_clear(&_discarded_jokers_list);
list_clear(&_expired_jokers_list);
list_clear(&_shop_jokers_list);
game_init();
@@ -3777,7 +3871,7 @@ static void game_over_on_exit()
display_mult();
display_hands(hands);
display_discards(discards);
display_money(money);
display_money();
tte_printf(
"#{P:%d,%d; cx:0x%X000}%d#{cx:0x%X000}/%d",
ANTE_TEXT_RECT.left,
+25 -7
View File
@@ -265,7 +265,7 @@ void set_and_shift_text(char* str, int* cursor_pos_x, int* cursor_pos_y, int col
*cursor_pos_x += joker_score_display_offset_px;
}
bool joker_object_score(JokerObject *joker_object, CardObject* card_object, enum JokerEvent joker_event, u32 *chips, u32 *mult, int *money, bool *retrigger)
bool joker_object_score(JokerObject *joker_object, CardObject* card_object, enum JokerEvent joker_event)
{
if (joker_object == NULL)
{
@@ -280,11 +280,17 @@ bool joker_object_score(JokerObject *joker_object, CardObject* card_object, enum
return false;
}
u32 chips = get_chips();
u32 mult = get_mult();
int money = get_money();
if (effect_flags_ret & JOKER_EFFECT_FLAG_RETRIGGER)
{
*retrigger = joker_effect->retrigger;
set_retrigger(joker_effect->retrigger);
}
// joker_effect.message will have been set if the Joker had anything custom to say
int cursorPosX = TILE_SIZE; // Offset of one tile to better center the text on the card
int cursorPosY = 0;
if (joker_event == JOKER_EVENT_ON_CARD_HELD)
@@ -303,7 +309,7 @@ bool joker_object_score(JokerObject *joker_object, CardObject* card_object, enum
mm_word sfx_id;
if (effect_flags_ret & JOKER_EFFECT_FLAG_CHIPS)
{
*chips = u32_protected_add(*chips, joker_effect->chips);
chips = u32_protected_add(chips, joker_effect->chips);
char score_buffer[INT_MAX_DIGITS + 2]; // For '+' and null terminator
snprintf(score_buffer, sizeof(score_buffer), "+%lu", joker_effect->chips);
set_and_shift_text(score_buffer, &cursorPosX, &cursorPosY, TTE_BLUE_PB);
@@ -311,7 +317,7 @@ bool joker_object_score(JokerObject *joker_object, CardObject* card_object, enum
}
if (effect_flags_ret & JOKER_EFFECT_FLAG_MULT)
{
*mult = u32_protected_add(*mult, joker_effect->mult);
mult = u32_protected_add(mult, joker_effect->mult);
char score_buffer[INT_MAX_DIGITS + 2];
snprintf(score_buffer, sizeof(score_buffer), "+%lu", joker_effect->mult);
set_and_shift_text(score_buffer, &cursorPosX, &cursorPosY, TTE_RED_PB);
@@ -320,7 +326,7 @@ bool joker_object_score(JokerObject *joker_object, CardObject* card_object, enum
// if xmult is zero, DO NOT multiply by it
if (effect_flags_ret & JOKER_EFFECT_FLAG_XMULT && joker_effect->xmult > 0)
{
*mult = u32_protected_mult(*mult, joker_effect->xmult);
mult = u32_protected_mult(mult, joker_effect->xmult);
char score_buffer[INT_MAX_DIGITS + 2];
snprintf(score_buffer, sizeof(score_buffer), "X%lu", joker_effect->xmult);
set_and_shift_text(score_buffer, &cursorPosX, &cursorPosY, TTE_RED_PB);
@@ -328,7 +334,7 @@ bool joker_object_score(JokerObject *joker_object, CardObject* card_object, enum
}
if (effect_flags_ret & JOKER_EFFECT_FLAG_MONEY)
{
*money += joker_effect->money;
money += joker_effect->money;
char score_buffer[INT_MAX_DIGITS + 2];
snprintf(score_buffer, sizeof(score_buffer), "%d$", joker_effect->money);
set_and_shift_text(score_buffer, &cursorPosX, &cursorPosY, TTE_YELLOW_PB);
@@ -340,11 +346,23 @@ bool joker_object_score(JokerObject *joker_object, CardObject* card_object, enum
{
set_and_shift_text(joker_effect->message, &cursorPosX, &cursorPosY, TTE_WHITE_PB);
}
// this will start the Joker expire animation
if (effect_flags_ret & JOKER_EFFECT_FLAG_EXPIRE && joker_effect->expire)
{
// TODO make Jokers expire
joker_object_shake(joker_object, UNDEFINED);
list_push_back(get_expired_jokers_list(), joker_object);
}
// Update values
set_chips(chips);
set_mult(mult);
set_money(money);
// Update displays
display_chips();
display_mult();
display_money();
joker_object_shake(joker_object, sfx_id);
return true;
+22 -17
View File
@@ -976,7 +976,9 @@ static u32 blueprint_brainstorm_joker_effect(Joker *joker, Card *scored_card, en
// No need for this kind of init since these Jokers
// will have their data copied when needed
if (joker_event == JOKER_EVENT_ON_JOKER_CREATED || joker_event == JOKER_EVENT_ON_ROUND_END)
if (joker_event == JOKER_EVENT_ON_JOKER_CREATED ||
joker_event == JOKER_EVENT_ON_HAND_SCORED_END ||
joker_event == JOKER_EVENT_ON_ROUND_END)
{
return effect_flags_ret;
}
@@ -1090,8 +1092,6 @@ static u32 hack_joker_effect(Joker *joker, Card *scored_card, enum JokerEvent jo
}
// Note: Joker expiration is not yet implemented so Seltzer cannot be made active before it does.
GBLA_UNUSED
static u32 seltzer_joker_effect(Joker *joker, Card *scored_card, enum JokerEvent joker_event, JokerEffect **joker_effect)
{
u32 effect_flags_ret = JOKER_EFFECT_FLAG_NONE;
@@ -1110,28 +1110,33 @@ static u32 seltzer_joker_effect(Joker *joker, Card *scored_card, enum JokerEvent
break;
case JOKER_EVENT_ON_CARD_SCORED_END:
// Works the same way as Dusk, but checks if it can still trigger
if (*p_hands_left_until_exp > 0)
// Works the same way as Dusk
// No need to check for p_hands_left_until_exp because the Joker
// will be destroyed the moment we hit 0
*joker_effect = &shared_joker_effect;
(*joker_effect)->retrigger = ((*p_last_retriggered_idx) < get_scored_card_index());
if ((*joker_effect)->retrigger)
{
*joker_effect = &shared_joker_effect;
(*joker_effect)->retrigger = (*p_last_retriggered_idx < get_scored_card_index());
if ((*joker_effect)->retrigger)
{
*p_last_retriggered_idx = get_scored_card_index();
(*joker_effect)->message = "Again!";
effect_flags_ret = JOKER_EFFECT_FLAG_RETRIGGER | JOKER_EFFECT_FLAG_MESSAGE;
}
*p_last_retriggered_idx = get_scored_card_index();
(*joker_effect)->message = "Again!";
effect_flags_ret = JOKER_EFFECT_FLAG_RETRIGGER | JOKER_EFFECT_FLAG_MESSAGE;
}
break;
case JOKER_EVENT_ON_HAND_SCORED_END:
*joker_effect = &shared_joker_effect;
effect_flags_ret = JOKER_EFFECT_FLAG_MESSAGE;
(*p_hands_left_until_exp)--;
if (*p_hands_left_until_exp > 0)
{
*p_hands_left_until_exp -= 1;
(*joker_effect)->message = "-1";
// Need to do this for now because the message's memory can't really be allocated
// So we can't use snprintf to craft a message depending on the number of hands left
static const char* seltzer_messages[] = {
"1", "2", "3", "4", "5", "6", "7", "8", "9"
};
(*joker_effect)->message = (char*)seltzer_messages[(*p_hands_left_until_exp) - 1];
}
else
{
@@ -1244,12 +1249,12 @@ const JokerInfo joker_registry[] =
{ UNCOMMON_JOKER, 7, joker_effect_noop, }, // 48 Four Fingers
{ COMMON_JOKER, 4, scholar_joker_effect }, // 49
{ UNCOMMON_JOKER, 8, fibonnaci_joker_effect }, // 50
{ UNCOMMON_JOKER, 6, seltzer_joker_effect, }, // 51
// The following jokers don't have sprites yet,
// uncomment them when their sprites are added.
#if 0
{ UNCOMMON_JOKER, 6, seltzer_joker_effect, },
{ COMMON_JOKER, 5, shoot_the_moon_joker_effect, },
#endif
};