Merge branch 'main' into game_over_screen

This commit is contained in:
MeirGavish
2025-08-16 12:00:32 +03:00
32 changed files with 902 additions and 316 deletions
+1
View File
@@ -3,3 +3,4 @@
/.vs /.vs
*.sln *.sln
*.vcxproj* *.vcxproj*
/balatro-gba/x64/**
+21 -6
View File
@@ -1,9 +1,26 @@
# Balatro GBA # Balatro GBA
This is an attempt to recreate the game 'Balatro' as accurately as possible, including all of the visual effects that make Balatro feel satisfying to play. This is an attempt to recreate the game 'Balatro' as accurately as possible, including all of the visual effects that make Balatro feel satisfying to play.
This port is strictly limited to the content that was available in the January 2024 Balatro Steam demo. This port is strictly limited in content to a minimal version of Balatro and will not recreate the full game.
### **Disclaimer: This project is NOT endorsed or supported by Playstack or LocalThunk** ### **Disclaimer: This project is NOT endorsed by or affiliated with Playstack or LocalThunk**
#### This project is solely aimed to recreate Balatro's old demo on the Game Boy Advance and is not meant to infringe or draw sales away from the full game's release or any of the established works by Playstack and LocalThunk. All rights are reserved to their respective holders. #### This is a non-profit community fan project solely aimed to recreate a minimal version of Balatro on the Game Boy Advance as a tribute to the full Balatro and is not meant to infringe or draw sales away from the full game's release or any of the established works by Playstack and LocalThunk.
#### All rights are reserved to their respective holders.
This version is meant for people who already own and know the official full game.
Buy the official full version
[Balatro on Steam](https://store.steampowered.com/app/2379780/Balatro/)
[Balatro on Google Play](https://play.google.com/store/apps/details?id=com.playstack.balatro.android)
[Balatro on Apple App Store](https://apps.apple.com/us/app/balatro/id6502453075)
[Balatro on Switch](https://www.nintendo.com/us/store/products/balatro-switch/)
[Balatro on PlayStation Store](https://store.playstation.com/en-us/concept/10010334)
[Balatro on Xbox](https://www.xbox.com/games/store/balatro/9PK087LNGJC5)
![Example](https://github.com/cellos51/balatro-gba/blob/main/example.gif) ![Example](https://github.com/cellos51/balatro-gba/blob/main/example.gif)
# **Build Instructions:** # **Build Instructions:**
@@ -22,7 +39,7 @@ Example: `cd '/home/user/OneDrive/Documents/balatro-gba` or something like this.
9. After it completes, navigate through the `build` directory in the project folder and look for `balatro-gba.gba` and load it on an emulator or flashcart. 9. After it completes, navigate through the `build` directory in the project folder and look for `balatro-gba.gba` and load it on an emulator or flashcart.
## **-Linux-** ## **-Linux-**
[Not Available] [No Instructions Available]
### **Common Issues:** ### **Common Issues:**
1. **When I drag the folder into the window and press enter, it doesn't work!** 1. **When I drag the folder into the window and press enter, it doesn't work!**
@@ -36,5 +53,3 @@ Example: `cd '/home/user/OneDrive/Documents/balatro-gba` or something like this.
5. **It says I don't have `Git` or `Make` installed!** 5. **It says I don't have `Git` or `Make` installed!**
-Use `pacman -S git` or `pacman -S make` although it should already be installed if you followed the instructions correctly. -Use `pacman -S git` or `pacman -S make` although it should already be installed if you followed the instructions correctly.
#### Purchasing the official game is highly recommended and almost mandatory in order for this project to have a chance of staying up due to copyright issues and potential legal action.
**Purchase the Steam Version here: https://store.steampowered.com/app/2379780/Balatro/**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+1
View File
@@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16
Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 B

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16
Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

+17
View File
@@ -0,0 +1,17 @@
#ifndef AFFINE_BACKGROUND_H
#define AFFINE_BACKGROUND_H
#include <tonc.h>
#include "graphic_utils.h"
#define AFFINE_BG_PAL_LEN 5
#define AFFINE_BG_PB (PAL_ROW_LEN * 15) // This isn't really a palette bank, just the starting index of the palette
void affine_background_init();
void affine_background_update();
void affine_background_set_color(COLOR color);
// Must be called with an array of size at least AFFINE_BG_PAL_LEN
void affine_background_load_palette(const u16 *src);
#endif // AFFINE_BACKGROUND_H
+5
View File
@@ -6,6 +6,8 @@
#define MAX_JOKERS_HELD_SIZE 5 // This doesn't account for negatives right now. #define MAX_JOKERS_HELD_SIZE 5 // This doesn't account for negatives right now.
#define MAX_SHOP_JOKERS 2 // TODO: Make this dynamic and allow for other items besides jokers #define MAX_SHOP_JOKERS 2 // TODO: Make this dynamic and allow for other items besides jokers
#define MAX_SELECTION_SIZE 5 #define MAX_SELECTION_SIZE 5
#define MAX_CARD_SCORE_DIGITS 2 // Current digit limit for score received from cards including mult etc. from jokers
#define MAX_CARD_SCORE_STR_LEN (MAX_CARD_SCORE_DIGITS + 1) // For the '+' or 'X'
#define FRAMES(x) (((x) + game_speed - 1) / game_speed) #define FRAMES(x) (((x) + game_speed - 1) / game_speed)
// TODO: Turn into enum? // TODO: Turn into enum?
@@ -77,9 +79,12 @@ typedef struct CardObject CardObject; // forward declaration, actually declared
typedef struct JokerObject JokerObject; typedef struct JokerObject JokerObject;
CardObject** get_hand_array(void); CardObject** get_hand_array(void);
int get_hand_top(void); int get_hand_top(void);
int hand_get_size(void);
CardObject** get_played_array(void); CardObject** get_played_array(void);
int get_played_top(void); int get_played_top(void);
JokerObject** get_jokers(void); JokerObject** get_jokers(void);
int get_jokers_top(void); int get_jokers_top(void);
int get_num_discards_remaining(void);
#endif // GAME_H #endif // GAME_H
+21
View File
@@ -43,6 +43,9 @@
// Tile size in pixels, both height and width as tiles are square // Tile size in pixels, both height and width as tiles are square
#define TILE_SIZE 8 #define TILE_SIZE 8
// By default TTE characters occupy a single tile
#define TTE_CHAR_SIZE TILE_SIZE
typedef struct typedef struct
{ {
int left; int left;
@@ -117,4 +120,22 @@ void tte_erase_rect_wrapper(Rect rect);
*/ */
void update_text_rect_to_right_align_num(Rect* rect, int num, int overflow_direction); void update_text_rect_to_right_align_num(Rect* rect, int num, int overflow_direction);
/*Copies 16 bit data from src to dst, applying a palette offset to the data.
* This is intended solely for use with tile8/8bpp data for dst and src.
* The palette offset allows the tiles to use a different location in the palette
* memory
* This is useful because grit always loads the palette to the beginning of
* pal_bg_mem[]
*/
void memcpy16_tile8_with_palette_offset(u16* dst, const u16* src, uint hwcount, u8 palette_offset);
/*Copies 32 bit data from src to dst, applying a palette offset to the data.
* This is intended solely for use with tile8/8bpp data for dst and src.
* The palette offset allows the tiles to use a different location in the palette
* memory
* This is useful because grit always loads the palette to the beginning of
* pal_bg_mem[]
*/
void memcpy32_tile8_with_palette_offset(u32* dst, const u32* src, uint wcount, u8 palette_offset);
#endif //GRAPHIC_UTILS_H #endif //GRAPHIC_UTILS_H
+3
View File
@@ -10,5 +10,8 @@
#include "joker_gfx6.h" #include "joker_gfx6.h"
#include "joker_gfx7.h" #include "joker_gfx7.h"
#include "joker_gfx8.h" #include "joker_gfx8.h"
#include "joker_gfx9.h"
#include "joker_gfx10.h"
#include "joker_gfx11.h"
#endif #endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef LIST_H
#define LIST_H
#include <stdbool.h>
typedef struct List
{
void** _array;
int size;
int allocated_size;
} List;
List *list_new(int init_size);
void list_destroy(List **list);
bool list_append(List *list, void *value);
bool list_remove_by_idx(List *list, int index);
void* list_get(List *list, int index);
int list_get_size(List *list);
bool list_remove_by_value(List *list, void *value);
bool int_list_append(List *list, intptr_t value);
intptr_t int_list_get(List *list, int index);
bool int_list_remove_by_value(List *list, intptr_t value);
#endif
+41
View File
@@ -0,0 +1,41 @@
#ifndef SELECTION_GRID_H
#define SELECTION_GRID_H
#include <tonc.h>
typedef POINT Selection;
struct SelectionGridRow;
struct SelectionGrid;
typedef struct SelectionGridRow SelectionGridRow;
typedef struct SelectionGrid SelectionGrid;
// Called whenever there is a change in the selection cursor
// row_idx is the index of the row whose function is invoked - can be used to identify whether it is the previous or new selection row.
typedef void (*RowOnSelectionChangedFunc)(SelectionGrid* selection_grid, int row_idx, const Selection* prev_selection, const Selection* new_selection);
typedef int (*RowGetSizeFunc)();
// Called for any non-directional key hit
// The key will not be passed, the function will have to check key_hit() etc. for the key it wants to check
typedef void (*RowOnKeyHitFunc)(SelectionGrid* selection_grid, Selection* selection);
struct SelectionGridRow
{
int row_idx;
RowGetSizeFunc get_size;
RowOnSelectionChangedFunc on_selection_changed;
RowOnKeyHitFunc on_key_hit;
};
struct SelectionGrid
{
SelectionGridRow* rows;
const int num_rows;
Selection selection;
};
void selection_grid_process_input(SelectionGrid* selection_grid);
void selection_grid_move_selection_horz(SelectionGrid* selection_grid, int direction_tribool);
void selection_grid_move_selection_vert(SelectionGrid* selection_grid, int direction_tribool);
#endif
+4 -1
View File
@@ -27,6 +27,8 @@ typedef struct
FIXED rotation; FIXED rotation;
FIXED vrotation; FIXED vrotation;
bool selected; bool selected;
bool focused;
} SpriteObject; } SpriteObject;
// Sprite methods // Sprite methods
@@ -58,6 +60,7 @@ void sprite_object_shake(SpriteObject* sprite_object, mm_word sound_id);
void sprite_object_set_selected(SpriteObject* sprite_object, bool selected); void sprite_object_set_selected(SpriteObject* sprite_object, bool selected);
bool sprite_object_is_selected(SpriteObject* sprite_object); bool sprite_object_is_selected(SpriteObject* sprite_object);
Sprite* sprite_object_get_sprite(SpriteObject* sprite_object); Sprite* sprite_object_get_sprite(SpriteObject* sprite_object);
void sprite_object_set_focus(SpriteObject* sprite_object, bool focus);
bool sprite_object_is_focused(SpriteObject* sprite_object);
#endif // SPRITE_H #endif // SPRITE_H
+2
View File
@@ -37,6 +37,8 @@ static inline int get_digits_even(int n)
#define NUM_ELEM_IN_ARR(arr) (sizeof(arr) / sizeof((arr)[0])) #define NUM_ELEM_IN_ARR(arr) (sizeof(arr) / sizeof((arr)[0]))
#define INT_MAX_DIGITS 10 // strlen(str(INT_MAX)) = strlen("2147483647")
int int_arr_max(int int_arr[], int size); int int_arr_max(int int_arr[], int size);
#endif // UTIL_H #endif // UTIL_H
+46
View File
@@ -0,0 +1,46 @@
#include "affine_background.h"
#include "affine_background_gfx.h"
#include "graphic_utils.h"
BG_AFFINE bgaff;
AFF_SRC_EX asx = {32<<8, 64<<8, 120, 80, 0x0100, 0x0100, 0};
void affine_background_init()
{
memcpy32_tile8_with_palette_offset((u32*)&tile8_mem[AFFINE_BG_CBB], (const u32*)affine_background_gfxTiles, affine_background_gfxTilesLen/4, AFFINE_BG_PB);
GRIT_CPY(&se_mem[AFFINE_BG_SBB], affine_background_gfxMap);
affine_background_load_palette(affine_background_gfxPal);
bgaff = bg_aff_default;
}
void affine_background_update()
{
static uint timer = 0;
timer++;
// These values are not permament, just the current configuration for the affine background
asx.tex_x += 5;
asx.tex_y += 12;
asx.sx = (lu_sin(timer * 100)) >> 8; // Scale the sine value to fit in a s16
asx.sx += 256; // Add 256 to the sine value to make it positive
asx.sy = (lu_sin(timer * 100 + 0x4000)) >> 8; // Scale the sine value to fit in a s16
asx.sy += 256; // Add 256 to the sine value to make it positive
bg_rotscale_ex(&bgaff, &asx);
REG_BG_AFFINE[2] = bgaff;
}
void affine_background_set_color(COLOR color)
{
memcpy16(&pal_bg_mem[AFFINE_BG_PB], affine_background_gfxPal, AFFINE_BG_PAL_LEN);
for (int i = 0; i < AFFINE_BG_PAL_LEN; i++)
{
clr_rgbscale(&pal_bg_mem[AFFINE_BG_PB] + i, &pal_bg_mem[AFFINE_BG_PB] + i, 1, color);
}
}
void affine_background_load_palette(const u16 *src)
{
memcpy16(&pal_bg_mem[AFFINE_BG_PB], src, AFFINE_BG_PAL_LEN);
}
+324 -201
View File
@@ -11,16 +11,21 @@
#include "hand_analysis.h" #include "hand_analysis.h"
#include "blind.h" #include "blind.h"
#include "joker.h" #include "joker.h"
#include "affine_background.h"
#include "graphic_utils.h" #include "graphic_utils.h"
#include "tonc_video.h" #include "tonc_video.h"
#include "audio_utils.h" #include "audio_utils.h"
#include "selection_grid.h"
#include "background_gfx.h" #include "background_gfx.h"
#include "background_shop_gfx.h" #include "background_shop_gfx.h"
#include "background_blind_select_gfx.h" #include "background_blind_select_gfx.h"
#include "affine_background_gfx.h"
#include "soundbank.h" #include "soundbank.h"
#include "list.h"
// This seed system is temporary. // This seed system is temporary.
static uint rng_seed = 2048; static uint rng_seed = 2048;
static bool seeded = false; static bool seeded = false;
@@ -29,7 +34,7 @@ static uint timer = 0; // This might already exist in libtonc but idk so i'm jus
static int game_speed = 1; static int game_speed = 1;
static int background = 0; static int background = 0;
static enum GameState game_state = GAME_SHOP; // The current game state, this is used to determine what the game is doing at any given time static enum GameState game_state = GAME_BLIND_SELECT; // The current game state, this is used to determine what the game is doing at any given time
static enum HandState hand_state = HAND_DRAW; static enum HandState hand_state = HAND_DRAW;
static enum PlayState play_state = PLAY_PLAYING; static enum PlayState play_state = PLAY_PLAYING;
static int state = 0; // General state variable, used for switch statements in each game state related function static int state = 0; // General state variable, used for switch statements in each game state related function
@@ -148,6 +153,11 @@ int get_hand_top(void) {
return hand_top; return hand_top;
} }
int hand_get_size(void)
{
return hand_top + 1;
}
CardObject **get_played_array(void) { CardObject **get_played_array(void) {
return played; return played;
} }
@@ -164,6 +174,8 @@ int get_jokers_top(void) {
return jokers_top; return jokers_top;
} }
List* jokers_available_to_shop; // List of joker IDs
// Consts // Consts
// Rects left top right bottom // Rects left top right bottom
@@ -228,17 +240,24 @@ static const Rect ROUND_END_NUM_HANDS_RECT = {88, 116, UNDEFINED, UNDEF
static const Rect HAND_REWARD_RECT = {168, UNDEFINED, UNDEFINED, UNDEFINED }; static const Rect HAND_REWARD_RECT = {168, UNDEFINED, UNDEFINED, UNDEFINED };
static const Rect CASHOUT_RECT = {88, 72, UNDEFINED, UNDEFINED }; static const Rect CASHOUT_RECT = {88, 72, UNDEFINED, UNDEFINED };
static const Rect SHOP_REROLL_RECT = {88, 96, UNDEFINED, UNDEFINED }; static const Rect SHOP_REROLL_RECT = {88, 96, UNDEFINED, UNDEFINED };
#define ITEM_SHOP_Y 71 // TODO: Needs to be a rect?
//TODO: Properly define and use //TODO: Properly define and use
#define MENU_POP_OUT_ANIM_FRAMES 20 #define MENU_POP_OUT_ANIM_FRAMES 20
#define SCORED_CARD_TEXT_Y 48 #define SCORED_CARD_TEXT_Y 48
#define HIGHLIGHT_COLOR 0xFFFF
#define PITCH_STEP_DISCARD_SFX (-64) #define PITCH_STEP_DISCARD_SFX (-64)
#define PITCH_STEP_DRAW_SFX 24 #define PITCH_STEP_DRAW_SFX 24
#define PITCH_STEP_UNDISCARD_SFX 2*PITCH_STEP_DRAW_SFX #define PITCH_STEP_UNDISCARD_SFX 2*PITCH_STEP_DRAW_SFX
// Naming the stage where cards return from the discard pile to the deck "undiscard" // Naming the stage where cards return from the discard pile to the deck "undiscard"
int get_num_discards_remaining(void) {
return discards;
}
// General functions // General functions
void set_seed(int seed) void set_seed(int seed)
{ {
@@ -428,6 +447,8 @@ void change_background(int id)
else if (current_blind == BOSS_BLIND) else if (current_blind == BOSS_BLIND)
{ {
main_bg_se_copy_rect(BOSS_BLIND_TITLE_SRC_RECT, TOP_LEFT_BLIND_TITLE_POINT); main_bg_se_copy_rect(BOSS_BLIND_TITLE_SRC_RECT, TOP_LEFT_BLIND_TITLE_POINT);
affine_background_set_color(blind_get_color(BOSS_BLIND, BLIND_SHADOW_COLOR_INDEX));
} }
bg_copy_current_item_to_top_left_panel(); bg_copy_current_item_to_top_left_panel();
@@ -849,11 +870,6 @@ void hand_change_sort()
sort_cards(); sort_cards();
} }
int hand_get_size()
{
return hand_top + 1;
}
int hand_get_max_size() int hand_get_max_size()
{ {
return hand_size; return hand_size;
@@ -990,8 +1006,21 @@ void game_set_state(enum GameState new_game_state)
game_state = new_game_state; game_state = new_game_state;
} }
void jokers_available_to_shop_init()
{
int num_defined_jokers = get_joker_registry_size();
jokers_available_to_shop = list_new(num_defined_jokers);
for (intptr_t i = 0; i < num_defined_jokers; i++)
{
// Add all joker IDs sequentially
int_list_append(jokers_available_to_shop, i);
}
}
void game_init() void game_init()
{ {
jokers_available_to_shop_init();
hands = max_hands; hands = max_hands;
discards = max_discards; discards = max_discards;
@@ -1551,7 +1580,7 @@ static void played_cards_update_loop(bool* discarded_card, int* played_selection
FIXED played_x = int2fx(120); FIXED played_x = int2fx(120);
FIXED played_y = int2fx(70); FIXED played_y = int2fx(70);
FIXED played_scale = float2fx(1.0f); FIXED played_scale = FIX_ONE;
played_x = played_x + (int2fx(played_top - i) - int2fx(played_top) / 2) * -27; played_x = played_x + (int2fx(played_top - i) - int2fx(played_top) / 2) * -27;
@@ -1615,7 +1644,7 @@ static void played_cards_update_loop(bool* discarded_card, int* played_selection
tte_set_special(0xD000); // Set text color to blue from background memory tte_set_special(0xD000); // Set text color to blue from background memory
// Write the score to a character buffer variable // Write the score to a character buffer variable
char score_buffer[5]; // Assuming the maximum score is 99, we need 4 characters (2 digits + null terminator) char score_buffer[INT_MAX_DIGITS + 2]; // for '+' and null terminator
snprintf(score_buffer, sizeof(score_buffer), "+%d", card_get_value(played[j]->card)); snprintf(score_buffer, sizeof(score_buffer), "+%d", card_get_value(played[j]->card));
tte_write(score_buffer); tte_write(score_buffer);
@@ -1764,11 +1793,6 @@ static void held_jokers_update_loop()
jokers[i]->sprite_object->tx = hand_x - int2fx(spacing_lut[jokers_top][i]); jokers[i]->sprite_object->tx = hand_x - int2fx(spacing_lut[jokers_top][i]);
jokers[i]->sprite_object->ty = hand_y; jokers[i]->sprite_object->ty = hand_y;
if (joker_object_is_selected(jokers[i]))
{
jokers[i]->sprite_object->ty -= int2fx(10);
}
joker_object_update(jokers[i]); joker_object_update(jokers[i]);
} }
} }
@@ -1810,41 +1834,6 @@ static void game_round_end_cashout()
display_score(score); // Set the score display display_score(score); // Set the score display
} }
static void game_shop_create_items(JokerObject *shop_jokers[], bool first_time)
{
tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT);
for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{
if (shop_jokers[i] != NULL)
{
joker_object_destroy(&shop_jokers[i]); // Destroy the joker object if it exists
}
u8 joker_id = random() % get_joker_registry_size();
// TODO: don't pick a joker that we already own
// TODO: weight the random choice by joker rarity
shop_jokers[i] = joker_object_new(joker_new(joker_id));
shop_jokers[i]->sprite_object->x = int2fx(120 + i * 32);
shop_jokers[i]->sprite_object->y = int2fx(160);
shop_jokers[i]->sprite_object->tx = shop_jokers[i]->sprite_object->x;
shop_jokers[i]->sprite_object->ty = int2fx(71);
int x = fx2int(shop_jokers[i]->sprite_object->tx) + TILE_SIZE - (get_digits_even(shop_jokers[i]->joker->value) - 1) * TILE_SIZE;
int y = fx2int(shop_jokers[i]->sprite_object->ty) + CARD_SPRITE_SIZE + TILE_SIZE;
tte_printf("#{P:%d,%d; cx:0xC000}$%d", x, y, shop_jokers[i]->joker->value);
if (first_time == false)
{
shop_jokers[i]->sprite_object->y = shop_jokers[i]->sprite_object->ty; // If it's not the first time, set the y position to the target position
joker_object_shake(shop_jokers[i], UNDEFINED); // Give the joker a little wiggle animation
}
sprite_position(joker_object_get_sprite(shop_jokers[i]), fx2int(shop_jokers[i]->sprite_object->x), fx2int(shop_jokers[i]->sprite_object->y));
}
}
void game_playing() void game_playing()
{ {
// TODO: Blind rect sliding into view animation... // TODO: Blind rect sliding into view animation...
@@ -1986,6 +1975,7 @@ void game_round_end()
tte_erase_rect_wrapper(BLIND_REWARD_RECT); tte_erase_rect_wrapper(BLIND_REWARD_RECT);
tte_erase_rect_wrapper(BLIND_REQ_TEXT_RECT); tte_erase_rect_wrapper(BLIND_REQ_TEXT_RECT);
obj_hide(playing_blind_token->obj); obj_hide(playing_blind_token->obj);
affine_background_load_palette(affine_background_gfxPal);
state = 5; state = 5;
timer = 0; timer = 0;
} }
@@ -2146,13 +2136,60 @@ void game_round_end()
} }
} }
void game_shop_intro(JokerObject* shop_jokers[]) // Shop
static List *shop_jokers = NULL;
#define REROLL_BASE_COST 5 // Base cost for rerolling the shop items
static int reroll_cost = REROLL_BASE_COST;
#define NEXT_ROUND_BTN_SEL_X 0
#define NEXT_ROUND_BTN_FRAME_PAL_IDX 5
#define NEXT_ROUND_BTN_PAL_IDX 16
#define REROLL_BTN_FRAME_PAL_IDX 7
#define REROLL_BTN_PAL_IDX 3
static void game_shop_create_items()
{
tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT);
if (list_get_size(jokers_available_to_shop) == 0)
{
// No jokers to create
return;
}
shop_jokers = list_new(MAX_SHOP_JOKERS);
for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{
// TODO: weight the random choice by joker rarity
int joker_idx = random() % list_get_size(jokers_available_to_shop);
intptr_t joker_id = int_list_get(jokers_available_to_shop, joker_idx);
list_remove_by_idx(jokers_available_to_shop, joker_idx);
JokerObject *joker_object = joker_object_new(joker_new(joker_id));
joker_object->sprite_object->x = int2fx(120 + i * CARD_SPRITE_SIZE);
joker_object->sprite_object->y = int2fx(160);
joker_object->sprite_object->tx = joker_object->sprite_object->x;
joker_object->sprite_object->ty = int2fx(ITEM_SHOP_Y);
int x = fx2int(joker_object->sprite_object->tx) + TILE_SIZE - (get_digits_even(joker_object->joker->value) - 1) * TILE_SIZE;
int y = fx2int(joker_object->sprite_object->ty) + CARD_SPRITE_SIZE + TILE_SIZE;
tte_printf("#{P:%d,%d; cx:0xC000}$%d", x, y, joker_object->joker->value);
sprite_position(joker_object_get_sprite(joker_object), fx2int(joker_object->sprite_object->x), fx2int(joker_object->sprite_object->y));
list_append(shop_jokers, joker_object);
}
}
// Intro sequence (menu and shop icon coming into frame)
static void game_shop_intro()
{ {
main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_UP); main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_UP);
if (timer == 1) if (timer == 1)
{ {
game_shop_create_items(shop_jokers, true); game_shop_create_items();
} }
if (timer >= 7) // Shift the shop icon if (timer >= 7) // Shift the shop icon
@@ -2179,29 +2216,193 @@ void game_shop_intro(JokerObject* shop_jokers[])
} }
} }
void game_shop() static void game_shop_reroll(int *reroll_cost)
{ {
change_background(BG_ID_SHOP); money -= *reroll_cost;
display_money(money); // Update the money display
static JokerObject *shop_jokers[MAX_SHOP_JOKERS] = {NULL}; for (int i = 0; i < list_get_size(shop_jokers); i++)
for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{ {
if (shop_jokers[i] != NULL) JokerObject *joker_object = list_get(shop_jokers, i);
if (joker_object != NULL)
{ {
joker_object_update(shop_jokers[i]); int_list_append(jokers_available_to_shop, joker_object->joker->id);
joker_object_destroy(&joker_object); // Destroy the joker object if it exists
} }
} }
const int reroll_base_cost = 5; // Base cost for rerolling the shop items list_destroy(&shop_jokers);
static int reroll_cost = reroll_base_cost;
// temp variables for future implementation game_shop_create_items();
const ushort max_items_top = MAX_SHOP_JOKERS;
const ushort max_items_bottom = 0;
if (timer % 20 == 0) // Shift palette around the border of the shop icon for (int i = 0; i < list_get_size(shop_jokers); i++)
{ {
JokerObject *joker_object = list_get(shop_jokers, i);
if (joker_object != NULL)
{
joker_object->sprite_object->y = joker_object->sprite_object->ty; // Set the y position to the target position
joker_object_shake(joker_object, UNDEFINED); // Give the joker a little wiggle animation
}
}
(*reroll_cost)++;
tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, *reroll_cost);
}
// Shop input
static int shop_top_row_get_size()
{
return list_get_size(shop_jokers) + 1; // + 1 to account for next round button
}
static void game_shop_buy_joker(int shop_joker_idx)
{
JokerObject *joker_object = list_get(shop_jokers, shop_joker_idx);
joker_push(joker_object);
money -= joker_object->joker->value; // Deduct the money spent on the joker
display_money(money); // Update the money display
Rect joker_price_rect;
joker_price_rect.left = fx2int(joker_object->sprite_object->tx);
// 2*TILE_SIZE because the item is highlighted and raised by 1 additional tile
joker_price_rect.top = fx2int(joker_object->sprite_object->ty) + CARD_SPRITE_SIZE + 2 * TILE_SIZE;
joker_price_rect.right = joker_price_rect.left + TILE_SIZE * 3;
joker_price_rect.bottom = joker_price_rect.top + TILE_SIZE;
tte_erase_rect_wrapper(joker_price_rect);
list_remove_by_idx(shop_jokers, shop_joker_idx); // Remove the joker from the shop
}
static void shop_top_row_on_key_hit(SelectionGrid* selection_grid, Selection* selection)
{
if (!key_hit(SELECT_CARD))
return;
if (selection->x == NEXT_ROUND_BTN_SEL_X)
{
// Go to next blind selection game state
state = 2; // Go to the outro sequence state
timer = 0; // Reset the timer
reroll_cost = REROLL_BASE_COST;
memcpy16(&pal_bg_mem[NEXT_ROUND_BTN_FRAME_PAL_IDX], &pal_bg_mem[6], 1);
// memcpy16(&pal_bg_mem[16], &pal_bg_mem[6], 1);
// This changes the color of the button to a dark red.
// However, it shares a palette with the shop icon, so it will change the color of the shop icon as well.
// And I don't care enough to fix it right now.
}
else
{
int shop_joker_idx = selection->x - 1; // - 1 to account for next round button
JokerObject *joker_object = list_get(shop_jokers, shop_joker_idx);
if (joker_object == NULL
|| jokers_top >= MAX_JOKERS_HELD_SIZE - 1
|| money < joker_object->joker->value)
{
return;
}
game_shop_buy_joker(shop_joker_idx);
// In Balatro the selection actually stays on the purchased joker it's easier to just move it left
selection_grid_move_selection_horz(selection_grid, -1);
}
}
static void shop_top_row_on_selection_changed(SelectionGrid* selection_grid, int row_idx,
const Selection* prev_selection,
const Selection* new_selection)
{
// The selection grid system only guarantees that the new selection is within bounds
if (prev_selection->y == row_idx && prev_selection->x >= 0 && prev_selection->x < shop_top_row_get_size())
{
if (prev_selection->x == NEXT_ROUND_BTN_SEL_X)
{
// Remove next round button highlight
memcpy16(&pal_bg_mem[NEXT_ROUND_BTN_FRAME_PAL_IDX], &pal_bg_mem[NEXT_ROUND_BTN_PAL_IDX], 1);
}
else
{
JokerObject *joker = list_get(shop_jokers, prev_selection->x - 1);
sprite_object_set_focus(joker->sprite_object, false);
// -1 to account for next round button
}
}
if (new_selection->y == row_idx)
{
if (new_selection->x == NEXT_ROUND_BTN_SEL_X)
{
// Highlight next round button
memset16(&pal_bg_mem[NEXT_ROUND_BTN_FRAME_PAL_IDX], HIGHLIGHT_COLOR, 1);
}
else
{
JokerObject *joker = list_get(shop_jokers, new_selection->x - 1);
sprite_object_set_focus(joker->sprite_object, true);
// -1 to account for next round button
}
}
}
static int shop_bottom_row_get_size()
{
return 1; // Currently only the reroll button
}
static void shop_bottom_row_on_selection_changed(SelectionGrid* selection_grid, int row_idx, const Selection* prev_selection, const Selection* new_selection)
{
if (row_idx == prev_selection->y)
{
// Remove highlight
memcpy16(&pal_bg_mem[REROLL_BTN_FRAME_PAL_IDX], &pal_bg_mem[REROLL_BTN_PAL_IDX], 1);
}
else if (row_idx == new_selection->y)
{
memset16(&pal_bg_mem[REROLL_BTN_FRAME_PAL_IDX], HIGHLIGHT_COLOR, 1);
}
}
static void shop_bottom_row_on_key_hit(SelectionGrid* selection_grid, Selection* selection)
{
if (money >= reroll_cost)
{
game_shop_reroll(&reroll_cost);
}
}
SelectionGridRow shop_selection_rows[] =
{
{0, shop_top_row_get_size, shop_top_row_on_selection_changed, shop_top_row_on_key_hit},
{1, shop_bottom_row_get_size, shop_bottom_row_on_selection_changed, shop_bottom_row_on_key_hit}
};
static const Selection SHOP_INIT_SEL = {-1, 0};
SelectionGrid shop_selection_grid = {shop_selection_rows, NUM_ELEM_IN_ARR(shop_selection_rows), SHOP_INIT_SEL};
// Shop menu input and selection
static void game_shop_process_user_input()
{
if (timer == 1)
{
// The selection grid is initialized outside of bounds and moved
// to trigger the selection change so the initial selection is visible
shop_selection_grid.selection = SHOP_INIT_SEL;
selection_grid_move_selection_horz(&shop_selection_grid, 1);
tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, reroll_cost);
}
// Shop input logic
selection_grid_process_input(&shop_selection_grid);
}
static void game_shop_lights_anim_frame()
{
// Shift palette around the border of the shop icon
COLOR shifted_palette[4]; COLOR shifted_palette[4];
memcpy16(&shifted_palette[0], &pal_bg_mem[14], 1); memcpy16(&shifted_palette[0], &pal_bg_mem[14], 1);
memcpy16(&shifted_palette[1], &pal_bg_mem[17], 1); memcpy16(&shifted_palette[1], &pal_bg_mem[17], 1);
@@ -2211,7 +2412,8 @@ void game_shop()
// Circularly shift the palette // Circularly shift the palette
int last = shifted_palette[3]; int last = shifted_palette[3];
for (int i = 3; i > 0; --i) { for (int i = 3; i > 0; --i)
{
shifted_palette[i] = shifted_palette[i - 1]; shifted_palette[i] = shifted_palette[i - 1];
} }
@@ -2223,132 +2425,8 @@ void game_shop()
memcpy16(&pal_bg_mem[8], &shifted_palette[3], 1); memcpy16(&pal_bg_mem[8], &shifted_palette[3], 1);
} }
switch (state) // I'm only using magic numbers here for the sake of simplicity since it's just sequential, but you can replace them with named constants or enums if it makes it clearer // Outro sequence (menu and shop icon going out of frame)
{ static void game_shop_outro()
case 0: // Intro sequence (menu and shop icon coming into frame)
{
game_shop_intro(shop_jokers);
break;
}
case 1: // Shop menu input and selection
{
if (timer == 1)
{
tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, reroll_cost);
}
// Shop input logic
if (key_hit(KEY_UP))
{
selection_y = 0;
if (selection_x > max_items_top)
{
selection_x = max_items_top;
}
}
else if (key_hit(KEY_DOWN))
{
selection_y = 1;
if (selection_x > max_items_bottom)
{
selection_x = max_items_bottom;
}
}
else if (key_hit(KEY_LEFT))
{
if (selection_x > 0)
{
selection_x--;
}
}
else if (key_hit(KEY_RIGHT))
{
if (selection_y == 0 && selection_x < max_items_top)
{
selection_x++;
}
else if (selection_y == 1 && selection_x < max_items_bottom)
{
selection_x++;
}
}
memcpy16(&pal_bg_mem[7], &pal_bg_mem[3], 1);
memcpy16(&pal_bg_mem[5], &pal_bg_mem[16], 1);
// Shop selection logic
if (selection_x == 0 && selection_y == 0)
{
memset16(&pal_bg_mem[5], 0xFFFF, 1);
if (key_hit(SELECT_CARD))
{
// Go to next blind selection game state
state = 2; // Go to the outro sequence state
timer = 0; // Reset the timer
memcpy16(&pal_bg_mem[5], &pal_bg_mem[6], 1);
// memcpy16(&pal_bg_mem[16], &pal_bg_mem[6], 1);
// This changes the color of the button to a dark red.
// However, it shares a palette with the shop icon, so it will change the color of the shop icon as well.
// And I don't care enough to fix it right now.
}
}
else if (selection_x == 0 && selection_y == 1)
{
memset16(&pal_bg_mem[7], 0xFFFF, 1);
if (key_hit(SELECT_CARD) && money >= reroll_cost)
{
money -= reroll_cost;
display_money(money); // Update the money display
game_shop_create_items(shop_jokers, false);
reroll_cost++;
tte_printf("#{P:%d,%d; cx:0xF000}$%d", SHOP_REROLL_RECT.left, SHOP_REROLL_RECT.top, reroll_cost);
}
}
for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{
if (shop_jokers[i] != NULL)
{
if (i == selection_x - 1 && selection_y == 0)
{
shop_jokers[i]->sprite_object->ty = int2fx(61);
if (key_hit(SELECT_CARD) && jokers_top < MAX_JOKERS_HELD_SIZE - 1 && money >= shop_jokers[i]->joker->value)
{
joker_push(shop_jokers[i]);
money -= shop_jokers[i]->joker->value; // Deduct the money spent on the joker
display_money(money); // Update the money display
Rect joker_price_rect;
joker_price_rect.left = fx2int(shop_jokers[i]->sprite_object->tx);
// 2*TILE_SIZE because the item is highlighted and raised by 1 additional tile
joker_price_rect.top = fx2int(shop_jokers[i]->sprite_object->ty) + CARD_SPRITE_SIZE + 2*TILE_SIZE;
joker_price_rect.right = joker_price_rect.left + TILE_SIZE * 3;
joker_price_rect.bottom = joker_price_rect.top + TILE_SIZE;
tte_erase_rect_wrapper(joker_price_rect);
shop_jokers[i] = NULL; // Remove the joker from the shop
}
}
else
{
shop_jokers[i]->sprite_object->ty = int2fx(71);
}
}
}
break;
}
case 2: // Outro sequence (menu and shop icon going out of frame)
{ {
// Shift the shop panel // Shift the shop panel
main_bg_se_move_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_DOWN); main_bg_se_move_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_DOWN);
@@ -2359,11 +2437,12 @@ void game_shop()
{ {
tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT); // Erase the shop prices text tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT); // Erase the shop prices text
for (int i = 0; i < MAX_SHOP_JOKERS; i++) for (int i = 0; i < list_get_size(shop_jokers); i++)
{ {
if (shop_jokers[i] != NULL) JokerObject *joker_object = list_get(shop_jokers, i);
if (joker_object != NULL)
{ {
shop_jokers[i]->sprite_object->ty = int2fx(160); joker_object->sprite_object->ty = int2fx(160);
} }
} }
@@ -2388,28 +2467,72 @@ void game_shop()
state = 3; // Go to the next state state = 3; // Go to the next state
timer = 0; // Reset the timer timer = 0; // Reset the timer
} }
}
void game_shop()
{
change_background(BG_ID_SHOP);
if (shop_jokers != NULL)
{
for (int i = 0; i < list_get_size(shop_jokers); i++)
{
JokerObject *joker_object = list_get(shop_jokers, i);
if (joker_object != NULL)
{
joker_object_update(joker_object);
}
}
}
if (timer % 20 == 0)
{
game_shop_lights_anim_frame();
}
switch (state) // I'm only using magic numbers here for the sake of simplicity since it's just sequential, but you can replace them with named constants or enums if it makes it clearer
{
case 0:
{
game_shop_intro();
break;
}
case 1:
{
game_shop_process_user_input();
break;
}
case 2:
{
game_shop_outro();
break; break;
} }
default: default:
state = 0; // Reset the state state = 0; // Reset the state
reroll_cost = reroll_base_cost;
selection_x = 0; // Reset the selection for (int i = 0; i < list_get_size(shop_jokers); i++)
selection_y = 0; // Reset the selection
for (int i = 0; i < MAX_SHOP_JOKERS; i++)
{ {
joker_object_destroy(&shop_jokers[i]); // Destroy the joker objects JokerObject* joker_object = list_get(shop_jokers, i);
if (joker_object != NULL)
{
// Make the joker available back to shop
int_list_append(jokers_available_to_shop, (intptr_t)joker_object->joker->id);
}
joker_object_destroy(&joker_object); // Destroy the joker objects
} }
increment_blind(BLIND_DEFEATED); list_destroy(&shop_jokers);
increment_blind(BLIND_DEFEATED); // TODO: Move to game_round_end()?
game_set_state(GAME_BLIND_SELECT); // If we reach here, we should go to the blind select state game_set_state(GAME_BLIND_SELECT); // If we reach here, we should go to the blind select state
break; break;
} }
} }
void game_blind_select() void game_blind_select()
{ {
switch (state) // I'm only using magic numbers here for the sake of simplicity since it's just sequential, but you can replace them with named constants or enums if it makes it clearer switch (state) // I'm only using magic numbers here for the sake of simplicity since it's just sequential, but you can replace them with named constants or enums if it makes it clearer
+19
View File
@@ -168,3 +168,22 @@ void update_text_rect_to_right_align_num(Rect* rect, int num, int overflow_direc
} }
} }
void memcpy16_tile8_with_palette_offset(u16* dst, const u16* src, uint hwcount, u8 palette_offset)
{
const u16 offset = (((palette_offset) << 8) | (palette_offset));
for (int i = 0; i < hwcount; i++)
{
// Copying u8 data twice across u16 data
dst[i] = src[i] + offset;
}
}
void memcpy32_tile8_with_palette_offset(u32* dst, const u32* src, uint wcount, u8 palette_offset)
{
const u32 offset = (palette_offset << 24) | (palette_offset << 16) | (palette_offset << 8) | palette_offset;
for (int i = 0; i < wcount; i++)
{
// Copying u8 data 4 times across u32 data
dst[i] = src[i] + offset;
}
}
+100
View File
@@ -0,0 +1,100 @@
#include <stdlib.h>
#include <stdbool.h>
#include "list.h"
#include "util.h"
List *list_new(int init_size) {
List *list = (List *)malloc(sizeof(List));
if (list == NULL) return NULL;
list->_array = (void **)malloc(sizeof(void*) * init_size);
if (!list->_array)
{
free(list);
return NULL;
}
list->size = 0;
list->allocated_size = init_size;
return list;
}
void list_destroy(List **list) {
if (list == NULL || *list == NULL)
return;
{
free((*list)->_array);
free(*list);
}
*list = NULL;
}
bool int_list_append(List *list, intptr_t value)
{
return list_append(list, (void*)value);
}
bool list_append(List *list, void *value)
{
if (list->size >= list->allocated_size)
{
int new_size = list->allocated_size * 2;
void **new_arr = (void **)realloc(list->_array, sizeof(void*) * new_size);
if (new_arr == NULL)
return false;
list->_array = new_arr;
list->allocated_size = new_size;
}
list->_array[list->size++] = value;
return true;
}
bool list_remove_by_idx(List *list, int index) {
if (index < 0 || index >= list->size)
return false;
for (int i = index; i < list->size - 1; ++i)
{
list->_array[i] = list->_array[i + 1];
}
list->size--;
return true;
}
bool list_remove_by_value(List *list, void* value)
{
for (int i = 0; i < list->size; i++)
{
if (list->_array[i] == value)
{
return list_remove_by_idx(list, i);
}
}
return false;
}
bool int_list_remove_by_value(List *list, intptr_t value)
{
return list_remove_by_value(list, (void*)value);
}
void* list_get(List *list, int index)
{
if (index < 0 || index >= list->size)
return NULL;
return list->_array[index];
}
intptr_t int_list_get(List *list, int index)
{
return (intptr_t)list_get(list, index);
}
int list_get_size(List *list)
{
if (list == NULL)
{
return UNDEFINED;
}
return list->size;
}
+28 -8
View File
@@ -127,6 +127,9 @@ void joker_init()
joker_gfxTiles[6] = joker_gfx6Tiles; joker_gfxTiles[6] = joker_gfx6Tiles;
joker_gfxTiles[7] = joker_gfx7Tiles; joker_gfxTiles[7] = joker_gfx7Tiles;
joker_gfxTiles[8] = joker_gfx8Tiles; joker_gfxTiles[8] = joker_gfx8Tiles;
joker_gfxTiles[9] = joker_gfx9Tiles;
joker_gfxTiles[10] = joker_gfx10Tiles;
joker_gfxTiles[11] = joker_gfx11Tiles;
joker_gfxPal[0] = joker_gfx0Pal; joker_gfxPal[0] = joker_gfx0Pal;
joker_gfxPal[1] = joker_gfx1Pal; joker_gfxPal[1] = joker_gfx1Pal;
@@ -137,6 +140,9 @@ void joker_init()
joker_gfxPal[6] = joker_gfx6Pal; joker_gfxPal[6] = joker_gfx6Pal;
joker_gfxPal[7] = joker_gfx7Pal; joker_gfxPal[7] = joker_gfx7Pal;
joker_gfxPal[8] = joker_gfx8Pal; joker_gfxPal[8] = joker_gfx8Pal;
joker_gfxPal[9] = joker_gfx9Pal;
joker_gfxPal[10] = joker_gfx10Pal;
joker_gfxPal[11] = joker_gfx11Pal;
for (int i = 0; i < num_spritesheets; i++) for (int i = 0; i < num_spritesheets; i++)
{ {
@@ -264,32 +270,46 @@ bool joker_object_score(JokerObject *joker_object, Card* scored_card, int *chips
*money += joker_effect.money; *money += joker_effect.money;
// TODO: Retrigger // TODO: Retrigger
tte_set_pos(fx2int(joker_object->sprite_object->x) + 8, JOKER_SCORE_TEXT_Y); // Offset of 16 pixels to center the text on the card const int joker_score_display_offset_px = (MAX_CARD_SCORE_STR_LEN + 1)*TTE_CHAR_SIZE;
// + 1 For space
char score_buffer[12];
int cursorPosX = fx2int(joker_object->sprite_object->x) + 8; // Offset of 16 pixels to center the text on the card
if (joker_effect.chips > 0) if (joker_effect.chips > 0)
{ {
char score_buffer[INT_MAX_DIGITS + 2]; // For '+' and null terminator
tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y);
tte_set_special(0xD000); // Blue tte_set_special(0xD000); // Blue
snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.chips); snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.chips);
tte_write(score_buffer);
cursorPosX += joker_score_display_offset_px;
} }
else if (joker_effect.mult > 0) if (joker_effect.mult > 0)
{ {
char score_buffer[INT_MAX_DIGITS + 2];
tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y);
tte_set_special(0xE000); // Red tte_set_special(0xE000); // Red
snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.mult); snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.mult);
tte_write(score_buffer);
cursorPosX += joker_score_display_offset_px;
} }
else if (joker_effect.xmult > 0) if (joker_effect.xmult > 0)
{ {
char score_buffer[INT_MAX_DIGITS + 2];
tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y);
tte_set_special(0xE000); // Red tte_set_special(0xE000); // Red
snprintf(score_buffer, sizeof(score_buffer), "X%d", joker_effect.xmult); snprintf(score_buffer, sizeof(score_buffer), "X%d", joker_effect.xmult);
tte_write(score_buffer);
cursorPosX += joker_score_display_offset_px;
} }
else if (joker_effect.money > 0) if (joker_effect.money > 0)
{ {
char score_buffer[INT_MAX_DIGITS + 2];
tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y);
tte_set_special(0xC000); // Yellow tte_set_special(0xC000); // Yellow
snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.money); snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.money);
}
tte_write(score_buffer); tte_write(score_buffer);
cursorPosX += joker_score_display_offset_px;
}
joker_object->joker->processed = true; // Mark the joker as processed joker_object->joker->processed = true; // Mark the joker as processed
joker_object_shake(joker_object, SFX_CARD_SELECT); // TODO: Add a sound effect for scoring the joker joker_object_shake(joker_object, SFX_CARD_SELECT); // TODO: Add a sound effect for scoring the joker
+84 -4
View File
@@ -1,3 +1,4 @@
#include "game.h"
#include "joker.h" #include "joker.h"
#include "util.h" #include "util.h"
#include "hand_analysis.h" #include "hand_analysis.h"
@@ -178,7 +179,8 @@ static JokerEffect half_joker_effect(Joker *joker, Card *scored_card) {
if (scored_card != NULL) if (scored_card != NULL)
return effect; // if card != null, we are not at the end-phase of scoring yet return effect; // if card != null, we are not at the end-phase of scoring yet
if (get_played_top() + 1 <= 3) // game.c has hand_get_size() but it's not accesible here int played_size = get_played_top() + 1;
if (played_size <= 3)
effect.mult = 20; effect.mult = 20;
return effect; return effect;
@@ -190,12 +192,13 @@ static JokerEffect joker_stencil_effect(Joker *joker, Card *scored_card) {
return effect; // if card != null, we are not at the end-phase of scoring yet return effect; // if card != null, we are not at the end-phase of scoring yet
// +1 xmult per empty joker slot... // +1 xmult per empty joker slot...
int jokers_top = get_jokers_top(); int num_jokers = get_jokers_top() + 1;
effect.xmult = (MAX_JOKERS_HELD_SIZE-1) - jokers_top;
effect.xmult = (MAX_JOKERS_HELD_SIZE) - num_jokers;
// ...and also each stencil_joker adds +1 xmult // ...and also each stencil_joker adds +1 xmult
JokerObject** jokers = get_jokers(); JokerObject** jokers = get_jokers();
for (int i = 0; i < jokers_top; i++ ) for (int i = 0; i < num_jokers; i++ )
{ {
if (jokers[i]->joker->id == JOKER_STENCIL_ID) if (jokers[i]->joker->id == JOKER_STENCIL_ID)
effect.xmult++; effect.xmult++;
@@ -215,6 +218,78 @@ static JokerEffect misprint_joker_effect(Joker *joker, Card *scored_card) {
return effect; return effect;
} }
static JokerEffect walkie_talkie_joker_effect(Joker *joker, Card *scored_card) {
JokerEffect effect = {0};
if (scored_card == NULL)
return effect;
if (scored_card->rank == TEN || scored_card->rank == FOUR) {
effect.chips = 10;
effect.mult = 4;
}
return effect;
}
static JokerEffect fibonnaci_joker_effect(Joker *joker, Card *scored_card) {
JokerEffect effect = {0};
if (scored_card == NULL)
return effect;
switch (scored_card->rank) {
case ACE: case TWO: case THREE: case FIVE: case EIGHT:
effect.mult = 8;
default:
break;
}
return effect;
}
static JokerEffect banner_joker_effect(Joker *joker, Card *scored_card) {
JokerEffect effect = {0};
if (scored_card != NULL)
return effect; // if card != null, we are not at the end-phase of scoring yet
effect.chips = 30 * get_num_discards_remaining();
return effect;
}
static JokerEffect mystic_summit_joker_effect(Joker *joker, Card *scored_card) {
JokerEffect effect = {0};
if (scored_card != NULL)
return effect; // if card != null, we are not at the end-phase of scoring yet
if (get_num_discards_remaining() == 0)
effect.mult = 15;
return effect;
}
static JokerEffect blackboard_joker_effect(Joker *joker, Card *scored_card) {
JokerEffect effect = {0};
if (scored_card != NULL)
return effect; // if card != null, we are not at the end-phase of scoring yet
bool all_cards_are_spades_or_clubs = true;
CardObject** hand = get_hand_array();
int hand_size = hand_get_size();
for (int i = 0; i < hand_size; i++ )
{
u8 suit = hand[i]->card->suit;
if (suit == HEARTS || suit == DIAMONDS) {
all_cards_are_spades_or_clubs = false;
break;
}
}
if (all_cards_are_spades_or_clubs)
effect.xmult = 3;
return effect;
}
const JokerInfo joker_registry[] = { const JokerInfo joker_registry[] = {
{ COMMON_JOKER, 2, default_joker_effect }, // DEFAULT_JOKER_ID = 0 { COMMON_JOKER, 2, default_joker_effect }, // DEFAULT_JOKER_ID = 0
{ COMMON_JOKER, 5, greedy_joker_effect }, // GREEDY_JOKER_ID = 1 { COMMON_JOKER, 5, greedy_joker_effect }, // GREEDY_JOKER_ID = 1
@@ -233,6 +308,11 @@ const JokerInfo joker_registry[] = {
{ COMMON_JOKER, 4, crafty_joker_effect }, { COMMON_JOKER, 4, crafty_joker_effect },
{ COMMON_JOKER, 5, half_joker_effect }, { COMMON_JOKER, 5, half_joker_effect },
{ UNCOMMON_JOKER, 8, joker_stencil_effect }, { UNCOMMON_JOKER, 8, joker_stencil_effect },
{ COMMON_JOKER, 5, banner_joker_effect },
{ COMMON_JOKER, 4, walkie_talkie_joker_effect },
{ UNCOMMON_JOKER, 8, fibonnaci_joker_effect },
{ UNCOMMON_JOKER, 6, blackboard_joker_effect },
{ COMMON_JOKER, 5, mystic_summit_joker_effect },
{ COMMON_JOKER, 4, misprint_joker_effect }, { COMMON_JOKER, 4, misprint_joker_effect },
}; };
+4 -43
View File
@@ -7,6 +7,7 @@
#include "game.h" #include "game.h"
#include "blind.h" #include "blind.h"
#include "joker.h" #include "joker.h"
#include "affine_background.h"
#include "graphic_utils.h" #include "graphic_utils.h"
// Graphics // Graphics
@@ -17,29 +18,6 @@
#include "soundbank.h" #include "soundbank.h"
#include "soundbank_bin.h" #include "soundbank_bin.h"
#define MAP_AFF_SIZE 0x0100
BG_AFFINE bgaff;
AFF_SRC_EX asx = {32<<8, 64<<8, 120, 80, 0x0100, 0x0100, 0};
void init_map() // Temp function derived from libtonc example
{
// Grit is fucking garbage and doesn't allow you to have a palette offset, they all get loaded at 0x0000. I have to manually change each tile's palette bank
unsigned int correctedTiles[affine_background_gfxTilesLen / 4];
for (int i = 0; i < affine_background_gfxTilesLen / 4; i++)
{
correctedTiles[i] = affine_background_gfxTiles[i] | 0xF0F0F0F0; // I brute forced this. I have no idea how this would and does work, but it does. We'll just leave this here for now.
// Actually for future reference, each number seems to equate to an XY on the palette, so 0xF0F0F0F0 would just push everything down 15 colors, since the second number isn't set.
}
memcpy(&tile8_mem[AFFINE_BG_CBB], correctedTiles, affine_background_gfxTilesLen);
GRIT_CPY(&se_mem[AFFINE_BG_SBB], affine_background_gfxMap);
memcpy16(&pal_bg_mem[16 * 15], affine_background_gfxPal, 5);
bgaff = bg_aff_default;
}
void init() void init()
{ {
irq_init(NULL); irq_init(NULL);
@@ -66,7 +44,7 @@ void init()
// BG1 is the main background layer // BG1 is the main background layer
REG_BG1CNT = BG_PRIO(1) | BG_CBB(MAIN_BG_CBB) | BG_SBB(MAIN_BG_SBB) | BG_8BPP; REG_BG1CNT = BG_PRIO(1) | BG_CBB(MAIN_BG_CBB) | BG_SBB(MAIN_BG_SBB) | BG_8BPP;
// BG2 is the affine background layer // BG2 is the affine background layer
REG_BG2CNT = BG_PRIO(2) | BG_CBB(AFFINE_BG_CBB) | BG_SBB(AFFINE_BG_CBB) | BG_8BPP | BG_WRAP | BG_AFF_32x32; REG_BG2CNT = BG_PRIO(2) | BG_CBB(AFFINE_BG_CBB) | BG_SBB(AFFINE_BG_SBB) | BG_8BPP | BG_WRAP | BG_AFF_32x32;
int win1_left = 72; int win1_left = 72;
int win1_top = 44; int win1_top = 44;
@@ -93,9 +71,8 @@ void init()
REG_DISPCNT = DCNT_MODE1 | DCNT_OBJ_1D | DCNT_BG0 | DCNT_BG1 | DCNT_BG2 | DCNT_OBJ | DCNT_WIN0 | DCNT_WIN1; REG_DISPCNT = DCNT_MODE1 | DCNT_OBJ_1D | DCNT_BG0 | DCNT_BG1 | DCNT_BG2 | DCNT_OBJ | DCNT_WIN0 | DCNT_WIN1;
init_map(); // Initialize the map for the background
// Initialize subsystems // Initialize subsystems
affine_background_init();
sprite_init(); sprite_init();
card_init(); card_init();
blind_init(); blind_init();
@@ -103,24 +80,9 @@ void init()
game_init(); game_init();
} }
void background_update() // This needs to be called before update() and draw() because setting REG_BG_AFFINE during an HBlank can cause issues
{
static uint timer = 0;
timer++;
// These values are not permament, just the current configuration for the affine background
asx.tex_x += 5;
asx.tex_y += 12;
asx.sx = (lu_sin(timer * 100)) >> 8; // Scale the sine value to fit in a s16
asx.sx += 256; // Add 256 to the sine value to make it positive
asx.sy = (lu_sin(timer * 100 + 0x4000)) >> 8; // Scale the sine value to fit in a s16
asx.sy += 256; // Add 256 to the sine value to make it positive
bg_rotscale_ex(&bgaff, &asx);
REG_BG_AFFINE[2]= bgaff;
}
void update() void update()
{ {
affine_background_update();
game_update(); game_update();
} }
@@ -138,7 +100,6 @@ int main()
VBlankIntrWait(); VBlankIntrWait();
mmFrame(); mmFrame();
key_poll(); key_poll();
background_update();
update(); update();
draw(); draw();
} }
+82
View File
@@ -0,0 +1,82 @@
#include "selection_grid.h"
static void selection_grid_process_directional_input(SelectionGrid *selection_grid)
{
int horz_tri_input = bit_tribool(key_hit(KEY_ANY), KI_RIGHT, KI_LEFT);
if (horz_tri_input != 0)
{
selection_grid_move_selection_horz(selection_grid, horz_tri_input);
/* Avoid handling both vertical and horizontal input at the same time,
* it creates all sorts of difficult edge cases.
*/
return;
}
int vert_tri_input = bit_tribool(key_hit(KEY_ANY), KI_DOWN, KI_UP);
if (vert_tri_input != 0)
{
selection_grid_move_selection_vert(selection_grid, vert_tri_input);
}
}
void selection_grid_move_selection_horz(SelectionGrid* selection_grid,
int direction_tribool)
{
Selection new_selection = selection_grid->selection;
new_selection.x += direction_tribool;
if( selection_grid->selection.y >= 0
&& selection_grid->selection.y < selection_grid->num_rows
&& new_selection.x >= 0
&& new_selection.x < selection_grid->rows[new_selection.y].get_size())
{
selection_grid->rows[new_selection.y].on_selection_changed(selection_grid, new_selection.y, &selection_grid->selection, &new_selection);
selection_grid->selection = new_selection;
}
}
void selection_grid_move_selection_vert(SelectionGrid *selection_grid, int direction_tribool)
{
Selection selection = selection_grid->selection;
Selection new_selection = selection;
new_selection.y += direction_tribool;
if (new_selection.y >= 0
&& new_selection.y < selection_grid->num_rows)
{
int new_row_size = selection_grid->rows[new_selection.y].get_size();
if (selection.x >= new_row_size)
{
// TODO: Maintain relative horizontal position
// Clip selection to row size
new_selection.x = new_row_size - 1;
}
if (selection.y >= 0 && selection.y < selection_grid->num_rows)
{
selection_grid->rows[selection.y].on_selection_changed(selection_grid, selection.y, &selection, &new_selection);
}
selection_grid->rows[new_selection.y].on_selection_changed(selection_grid, new_selection.y, &selection, &new_selection);
selection_grid->selection = new_selection;
}
}
void selection_grid_process_input(SelectionGrid *selection_grid)
{
if (selection_grid == NULL || selection_grid->rows == NULL)
return;
selection_grid_process_directional_input(selection_grid);
u32 non_directional_key = KEY_ANY & ~KEY_DIR;
if (key_hit(non_directional_key))
{
Selection* selection = &selection_grid->selection; // To make the next line shorter and more readable
selection_grid->rows[selection->y].on_key_hit(selection_grid, selection);
}
}
+24 -5
View File
@@ -1,6 +1,7 @@
#include "sprite.h" #include "sprite.h"
#include "util.h" #include "util.h"
#include "audio_utils.h" #include "audio_utils.h"
#include "soundbank.h"
#include <tonc.h> #include <tonc.h>
#include <stdlib.h> #include <stdlib.h>
@@ -8,6 +9,7 @@
#define MAX_SPRITES 128 #define MAX_SPRITES 128
#define MAX_AFFINES 32 #define MAX_AFFINES 32
#define SPRITE_FOCUS_RAISE_PX 10
OBJ_ATTR obj_buffer[MAX_SPRITES]; OBJ_ATTR obj_buffer[MAX_SPRITES];
OBJ_AFFINE *obj_aff_buffer = (OBJ_AFFINE*)obj_buffer; OBJ_AFFINE *obj_aff_buffer = (OBJ_AFFINE*)obj_buffer;
@@ -145,9 +147,9 @@ void sprite_object_reset_transform(SpriteObject* sprite_object)
sprite_object->y = 0; sprite_object->y = 0;
sprite_object->vx = 0; sprite_object->vx = 0;
sprite_object->vy = 0; sprite_object->vy = 0;
sprite_object->tscale = float2fx(1.0f); // Target scale sprite_object->tscale = FIX_ONE; // Target scale
sprite_object->scale = float2fx(1.0f); sprite_object->scale = FIX_ONE;
sprite_object->vscale = float2fx(0.0f); sprite_object->vscale = 0;
sprite_object->trotation = 0; // Target rotation sprite_object->trotation = 0; // Target rotation
sprite_object->rotation = 0; sprite_object->rotation = 0;
sprite_object->vrotation = 0; sprite_object->vrotation = 0;
@@ -163,7 +165,7 @@ void sprite_object_update(SpriteObject* sprite_object)
sprite_object->vrotation += (sprite_object->trotation - sprite_object->rotation) / 8; // Rotate the card when it's played sprite_object->vrotation += (sprite_object->trotation - sprite_object->rotation) / 8; // Rotate the card when it's played
// set velocity to 0 if it's close enough to the target // set velocity to 0 if it's close enough to the target
const float epsilon = float2fx(0.01f); const FIXED epsilon = float2fx(0.01f);
if (sprite_object->vx < epsilon && sprite_object->vx > -epsilon && sprite_object->vy < epsilon && sprite_object->vy > -epsilon) if (sprite_object->vx < epsilon && sprite_object->vx > -epsilon && sprite_object->vy < epsilon && sprite_object->vy > -epsilon)
{ {
sprite_object->vx = 0; sprite_object->vx = 0;
@@ -211,7 +213,7 @@ void sprite_object_update(SpriteObject* sprite_object)
void sprite_object_shake(SpriteObject* sprite_object, mm_word sound_id) void sprite_object_shake(SpriteObject* sprite_object, mm_word sound_id)
{ {
sprite_object->vscale = float2fx(0.3f); // Scale down the card when it's scored sprite_object->vscale = float2fx(0.3f);
sprite_object->vrotation = float2fx(8.0f); //Rotate the card when it's scored sprite_object->vrotation = float2fx(8.0f); //Rotate the card when it's scored
if (sound_id == UNDEFINED) return; // If no sound ID is provided, do nothing if (sound_id == UNDEFINED) return; // If no sound ID is provided, do nothing
@@ -239,3 +241,20 @@ Sprite* sprite_object_get_sprite(SpriteObject* sprite_object)
return NULL; return NULL;
return sprite_object->sprite; return sprite_object->sprite;
} }
void sprite_object_set_focus(SpriteObject* sprite_object, bool focus)
{
if (sprite_object->focused == focus)
{
return;
}
sprite_object->focused = focus;
play_sfx(SFX_CARD_FOCUS , MM_BASE_PITCH_RATE + rand() % 512);
sprite_object->ty = sprite_object->ty + int2fx((focus ? -1 : 1) * SPRITE_FOCUS_RAISE_PX);
}
bool sprite_object_is_focused(SpriteObject* sprite_object)
{
return sprite_object->focused;
}