Compare commits

...
10 Commits
Author SHA1 Message Date
wuxu 7c62cfc950 Add a main-menu Joker gallery
Expose every registered Joker from a paged review screen entered with Start or Select. Reuse the gameplay card art and description renderers, support controller navigation, and clean up the preview object when returning to the menu.
2026-07-19 11:36:38 +08:00
wuxu c1632590c6 Localize Joker descriptions in Chinese
Translate every registered Joker description while preserving names and gameplay effects. Encode the text through the extended TTE font and center explicit UTF-8 lines without counting formatting tags.
2026-07-19 11:36:01 +08:00
wuxu c7ef3c0047 Add Chinese text rendering support
Extend the bitmap font pipeline with the Chinese Joker glyph subset and encode supported UTF-8 text for Tonc TTE. Move the affine map clear of the expanded font, add a compile-time VRAM overlap guard, and cover every custom glyph with host tests.
2026-07-19 11:35:14 +08:00
MeirGavishandCopilot Autofix powered by AI 7a5a4a1276 Updated all static variables to have the s_ prefix and constants to upper case (#591)
* Updated all static variables to have the s_ prefix and constants to upper case

* Renamed missed sneaky functions in round.c (+ clang-format...)

* Renamed a few more missed constants

* Updated rng_update() documentation

* Apply suggestions from Copilot's code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Added missed suggestion

* Updated rng_update() comment

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-16 20:21:18 +03:00
GeraltandMathisMartin31 6e7d8a8399 Document hand analysis functions in hand.h (#572)
* doxygen

* clang format

* Update round.cFix call to `find_straight_in_played_cards`

* Update straight/flush functions docs in hand.h

* typo

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2026-07-10 17:46:05 -07:00
d24b0b5619 Refactor shop code to use generic Item (#536)
* Turned SpriteObject into a member of JokerObject and CardObject and expiremented with casting

* Extracted shop code to use generic `SpriteObject`

* Extracted shop joker generation to a function

* Renamed object -> item

* Added setters and getters for `SpriteObject` members and updated all accesses to use them with casting (AI used for tedious work and reviewed - model Raptor mini)

* Added sprite_object_set_vx() because sprite_object_set_vy() exists and reordered the function order to have x,y parameters first, then targets

* Added Item object to act as a purchasable interface for the shop - AI used (model Raptor mini)

* Added -fms-extensions and switched to anonymous inheritance

* Fixed anonymous inheritance for joker and added direct access for that too

* fix build error from previous commit + clang-format

* Fixed build errors + clang-format for real this time (I hope...)

* clang-format one last time, I don't understand it anymore

* clang-format is annoying

* Added uses of CHECK_NULL_ARG macros

* Merged item_defs.h into item.h and fixed build error from previous commit

* clang-format...?

* Changed sprite_object_new() into sprite_object_init() and made it accept a parameter instead of returning one to make it less awkward

* Updated sprite_object_init() documentation

* Updated sprite_object_destroy documentation

* More clang-format

* Renamed add_to_inventory to acquire and updated documentation

* Added function documentations

* Update documentation + swap order of consumable and playing card types

* Added documentation for new files (+ small addition to button.h)

* clang-format

* Used typedef

* A bit more documentation

* Changed sho pcode to use generic Item instead of SpriteObject

* clang-format + fixed NULL-check error

* Revert weird clang-format...?

* clang-format for real

* Fixed errors from rebase on log_function_name

* Fixed build errors from rebase on main

* Added some more documentation about first member struct inheritance

* typo fix

* typo fix for real

* Removed SpriteObject field getters/setters (AI used - model Raptor mini)

* Renamed item_acquire -> item_on_acquired

* Added item_can_acquire()

* clang-format

* Fixed some sprite issues but not all...

* Fixed some more issues

* Cleanup JokerObject some more JokerObjects in the shop

* Added `item_destroy()`

* Added item_set_available_to_shop, CHECK_ITEM_TYPE macros + small documentation updates

* Another small JokerObject->Item replacement

* Removed item_set_available_to_shop and replaced with item_roll_new() and item_dispose(), also changed "available_to_shop" semantics to "rollable"

* Re-applied changes from game.c to round.c after rebase on main

* Added partial implementations of item functions for playing cards

* Line endings fix...

* Macro renames, function documentations

* Updated documentations

* Apply fixes from Copilot review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix card_object_get_buy_price type checking

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Avoid double pointer cast on dispose

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from @ricfehr3's code review

Co-authored-by: Rickey <ric@rf3.xyz>

* Remove redundant comments

* Moved error checking before joker_get_random_rarity

Co-authored-by: Rickey <ric@rf3.xyz>

* Fixed build error from rebase

* clang-format

* Added doxygen documentation for Item explaining inheritance

* Removed unnecessary comment

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Rickey <ric@rf3.xyz>
2026-07-09 09:13:51 +03:00
GeraltandMathisMartin31 369c680cb7 [Enhancement] Game Over screen rework (#548)
* WIP

* getting somewhere

* clean up everything real good

* doc

* clang format

* Fix round.c after merge

* Remove extra deck_pop declaration

* Remove unnecessary comment

* address MeirGavish's review

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2026-07-08 03:48:31 +03:00
MeirGavish da16e6a486 [Refactor] Moved reset_top_left_panel_bottom_row() to common_ui.c/.h (#581) 2026-07-06 06:26:18 +03:00
MeirGavish 9a4aaca399 Fixed #575 incorrect joker sell value displayed after displaying description (#576) 2026-07-02 13:26:00 +03:00
4d26f13c26 Add logging macros that include the function name (#557)
* Added function name logging to mgba_logger

* Addendum to no-op defines

* Fixed compile errors

* Added "()" to function name in log to make it clearer it's a function

* Corrected documentation

* clang-format...?

* Removed MGBA_LOG_LEVEL_MASK magic number

* Added CHECK_NULL_ARG macros

* clang-format...?

* clang-format for real

* Added documentation for CHECK_NULL_ARG_VOID and CHECK_NULL_ARG_RET

* Fixed typo

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fixed macro safety by wrappinh with do-while

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fixed format string cut risk in mgba_func_printf

* clang-format

* clang-format some more

* clang-format for real

* Fixed DEBUG -> INFO in comment

Co-authored-by: Rickey <ric@rf3.xyz>

* Moved NULL-check macros to util.h and renamed them

* Small additions - macro rename + clang-format

* Truncate the string instead of the function name

* Cleaned up code from previous commit

* clang-format

* Added mgba_logger_available check to mgba_func_printf

* Updated documentation for latest change

* Changed CHECK_NULL_ARG functions to generic RETURN_ON_ERROR_VAL functions.

* clang-format

* Changed back to specifically check NULL

* clang-format

* Changed "=" into "==" in the error message

* clang-format

* Fixed forgotten "=="

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Rickey <ric@rf3.xyz>
2026-06-28 06:54:47 +03:00
60 changed files with 2947 additions and 1237 deletions
+1
View File
@@ -46,6 +46,7 @@ GIT_C_FLAGS := -DGIT_HASH=\"$(GIT_HASH)\" -DGIT_DIRTY=\"$(GIT_DIRTY)\"
CFLAGS := -g -O3 -Wall -Werror -std=gnu23 \
-mcpu=arm7tdmi -mtune=arm7tdmi \
-ffast-math -fomit-frame-pointer -funroll-loops \
-fms-extensions \
$(ARCH)
CFLAGS += $(GIT_C_FLAGS)
+96
View File
@@ -0,0 +1,96 @@
Fusion Pixel Font
https://github.com/TakWolf/fusion-pixel-font
Copyright (c) 2022, TakWolf (https://takwolf.com).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

+1
View File
@@ -0,0 +1 @@
-gB8 -mR8 -mLs -mRtf -pn 64
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -2,6 +2,7 @@
* @file button.h
*
* @brief A button structure containing the common button functionalities
* Including highlight and pressing functionalities.
*/
#ifndef BUTTON_H
+6 -3
View File
@@ -2,12 +2,12 @@
#define CARD_H
#include "deck_types.h"
#include "sprite.h"
#include "item.h"
#include <maxmod.h>
#include <tonc.h>
#define MAX_CARDS (NUM_SUITS * NUM_RANKS)
#define MAX_CARDS 128 // Arbitrary high value so we don't have issues
#define MAX_CARDS_ON_SCREEN 16
#define CARD_TID 0
@@ -54,8 +54,8 @@ typedef struct Card
typedef struct CardObject
{
Item; // First member struct inheritance
Card* card;
SpriteObject* sprite_object;
bool selected;
} CardObject;
@@ -83,4 +83,7 @@ void card_object_set_selected(CardObject* card_object, bool selected);
bool card_object_is_selected(CardObject* card_object);
Sprite* card_object_get_sprite(CardObject* card_object);
int card_object_get_buy_price(Item* card_object);
void card_object_dispose(Item** card_object);
#endif // CARD_H
+21
View File
@@ -0,0 +1,21 @@
/**
* @file chinese_text.h
* @brief UTF-8 conversion helpers for the extended TTE font.
*/
#ifndef CHINESE_TEXT_H
#define CHINESE_TEXT_H
#include <stddef.h>
/**
* @brief Map supported UTF-8 Chinese text to valid UTF-8 sequences for custom TTE glyphs.
*
* ASCII bytes, including TTE formatting tags, are copied unchanged. Unsupported Unicode
* characters are replaced with '?'. Custom glyph IDs are emitted as UTF-8 because Tonc TTE
* decodes every byte above 0x7F as part of a UTF-8 sequence.
*
* @return Number of encoded bytes, excluding the null terminator.
*/
size_t chinese_text_encode(const char* utf8, char* output, size_t output_size);
#endif // CHINESE_TEXT_H
-1
View File
@@ -4,7 +4,6 @@
#include "sprite.h"
POOL_ENTRY(Sprite, MAX_SPRITES);
POOL_ENTRY(SpriteObject, MAX_SPRITE_OBJECTS);
POOL_ENTRY(Joker, MAX_ACTIVE_JOKERS);
POOL_ENTRY(JokerObject, MAX_ACTIVE_JOKERS);
POOL_ENTRY(Card, MAX_CARDS);
+124
View File
@@ -0,0 +1,124 @@
// Ordered glyphs appended after ASCII code point 127 in gbalatro_sys8.png.
CHINESE_GLYPH(0x5C0F) // 小
CHINESE_GLYPH(0x4E11) // 丑
CHINESE_GLYPH(0x724C) // 牌
CHINESE_GLYPH(0x6C49) // 汉
CHINESE_GLYPH(0x5316) // 化
CHINESE_GLYPH(0x9884) // 预
CHINESE_GLYPH(0x89C8) // 览
CHINESE_GLYPH(0x534A) // 半
CHINESE_GLYPH(0x5F20) // 张
CHINESE_GLYPH(0x6050) // 恐
CHINESE_GLYPH(0x6016) // 怖
CHINESE_GLYPH(0x9762) // 面
CHINESE_GLYPH(0x5B54) // 孔
CHINESE_GLYPH(0x5982) // 如
CHINESE_GLYPH(0x679C) // 果
CHINESE_GLYPH(0x51FA) // 出
CHINESE_GLYPH(0x4E0D) // 不
CHINESE_GLYPH(0x8D85) // 超
CHINESE_GLYPH(0x8FC7) // 过
CHINESE_GLYPH(0x6253) // 打
CHINESE_GLYPH(0x7684) // 的
CHINESE_GLYPH(0x4EBA) // 人
CHINESE_GLYPH(0x5934) // 头
CHINESE_GLYPH(0x8BA1) // 计
CHINESE_GLYPH(0x5206) // 分
CHINESE_GLYPH(0x65F6) // 时
CHINESE_GLYPH(0x7ED9) // 给
CHINESE_GLYPH(0x4E88) // 予
CHINESE_GLYPH(0x500D) // 倍
CHINESE_GLYPH(0x7387) // 率
CHINESE_GLYPH(0x7B79) // 筹
CHINESE_GLYPH(0x7801) // 码
CHINESE_GLYPH(0x5DE6) // 左
CHINESE_GLYPH(0x53F3) // 右
CHINESE_GLYPH(0x5207) // 切
CHINESE_GLYPH(0x6362) // 换
CHINESE_GLYPH(0x52A8) // 动
CHINESE_GLYPH(0x753B) // 画
CHINESE_GLYPH(0x4E3B) // 主
CHINESE_GLYPH(0x83DC) // 菜
CHINESE_GLYPH(0x5355) // 单
CHINESE_GLYPH(0x4E00) // 一
CHINESE_GLYPH(0x4E09) // 三
CHINESE_GLYPH(0x4E0B) // 下
CHINESE_GLYPH(0x4E24) // 两
CHINESE_GLYPH(0x4E2A) // 个
CHINESE_GLYPH(0x4E2D) // 中
CHINESE_GLYPH(0x4E3A) // 为
CHINESE_GLYPH(0x4E8E) // 于
CHINESE_GLYPH(0x4F4D) // 位
CHINESE_GLYPH(0x4F4E) // 低
CHINESE_GLYPH(0x4F59) // 余
CHINESE_GLYPH(0x4F8B) // 例
CHINESE_GLYPH(0x4FA7) // 侧
CHINESE_GLYPH(0x5076) // 偶
CHINESE_GLYPH(0x518D) // 再
CHINESE_GLYPH(0x51E0) // 几
CHINESE_GLYPH(0x5230) // 到
CHINESE_GLYPH(0x5236) // 制
CHINESE_GLYPH(0x524D) // 前
CHINESE_GLYPH(0x5269) // 剩
CHINESE_GLYPH(0x529B) // 力
CHINESE_GLYPH(0x52A0) // 加
CHINESE_GLYPH(0x5305) // 包
CHINESE_GLYPH(0x53D1) // 发
CHINESE_GLYPH(0x53EF) // 可
CHINESE_GLYPH(0x5408) // 合
CHINESE_GLYPH(0x540C) // 同
CHINESE_GLYPH(0x540E) // 后
CHINESE_GLYPH(0x542B) // 含
CHINESE_GLYPH(0x548C) // 和
CHINESE_GLYPH(0x56DB) // 四
CHINESE_GLYPH(0x56DE) // 回
CHINESE_GLYPH(0x5747) // 均
CHINESE_GLYPH(0x590D) // 复
CHINESE_GLYPH(0x5916) // 外
CHINESE_GLYPH(0x5947) // 奇
CHINESE_GLYPH(0x5B50) // 子
CHINESE_GLYPH(0x5BF9) // 对
CHINESE_GLYPH(0x5C06) // 将
CHINESE_GLYPH(0x5F03) // 弃
CHINESE_GLYPH(0x5F53) // 当
CHINESE_GLYPH(0x6210) // 成
CHINESE_GLYPH(0x6216) // 或
CHINESE_GLYPH(0x6240) // 所
CHINESE_GLYPH(0x624B) // 手
CHINESE_GLYPH(0x62E5) // 拥
CHINESE_GLYPH(0x62EC) // 括
CHINESE_GLYPH(0x63A5) // 接
CHINESE_GLYPH(0x6570) // 数
CHINESE_GLYPH(0x65B9) // 方
CHINESE_GLYPH(0x65E0) // 无
CHINESE_GLYPH(0x6700) // 最
CHINESE_GLYPH(0x6709) // 有
CHINESE_GLYPH(0x672C) // 本
CHINESE_GLYPH(0x673A) // 机
CHINESE_GLYPH(0x6761) // 条
CHINESE_GLYPH(0x6765) // 来
CHINESE_GLYPH(0x6843) // 桃
CHINESE_GLYPH(0x6885) // 梅
CHINESE_GLYPH(0x69FD) // 槽
CHINESE_GLYPH(0x6B21) // 次
CHINESE_GLYPH(0x6BCF) // 每
CHINESE_GLYPH(0x70B9) // 点
CHINESE_GLYPH(0x7247) // 片
CHINESE_GLYPH(0x7528) // 用
CHINESE_GLYPH(0x7531) // 由
CHINESE_GLYPH(0x7A7A) // 空
CHINESE_GLYPH(0x7B2C) // 第
CHINESE_GLYPH(0x7EA2) // 红
CHINESE_GLYPH(0x7EC4) // 组
CHINESE_GLYPH(0x80FD) // 能
CHINESE_GLYPH(0x8272) // 色
CHINESE_GLYPH(0x82B1) // 花
CHINESE_GLYPH(0x89C6) // 视
CHINESE_GLYPH(0x89E6) // 触
CHINESE_GLYPH(0x95F4) // 间
CHINESE_GLYPH(0x968F) // 随
CHINESE_GLYPH(0x9694) // 隔
CHINESE_GLYPH(0x987A) // 顺
CHINESE_GLYPH(0x989D) // 额
CHINESE_GLYPH(0x9ED1) // 黑
CHINESE_GLYPH(0x3001) // 、
+6
View File
@@ -2,6 +2,12 @@
// (stateEnum, on_init, on_update, on_exit)
DEF_STATE_INFO(GAME_STATE_SPLASH_SCREEN, splash_screen_on_init, splash_screen_on_update, splash_screen_on_exit )
DEF_STATE_INFO(GAME_STATE_MAIN_MENU, game_main_menu_on_init, game_main_menu_on_update, game_main_menu_on_exit )
DEF_STATE_INFO(
GAME_STATE_JOKER_GALLERY,
game_joker_gallery_on_init,
game_joker_gallery_on_update,
game_joker_gallery_on_exit
)
DEF_STATE_INFO(GAME_STATE_OPTIONS_MENU, game_options_menu_on_init, game_options_menu_on_update, game_options_menu_on_exit)
DEF_STATE_INFO(GAME_STATE_RUN_SETUP , game_run_setup_on_init, game_run_setup_on_update, game_run_setup_on_exit )
DEF_STATE_INFO(GAME_STATE_GAME_START, noop, game_start, noop )
+3 -1
View File
@@ -4,12 +4,13 @@
#include "game/common_ui.h"
#include "game_variables.h"
#include "graphic_utils.h"
#include "item.h"
#include <tonc.h>
#define MAX_DECK_SIZE 52
#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_ITEMS 2 // TODO: Make this dynamic
#define MAX_SELECTION_SIZE 5
#define FRAMES(x) (((x) + (g_game_vars.game_speed) - 1) / (g_game_vars.game_speed))
@@ -69,6 +70,7 @@ void game_change_state(enum GameState new_game_state);
enum GameState game_get_state(void);
bool is_joker_owned(int joker_id);
bool joker_object_can_acquire(Item* item);
bool card_is_face(Card* card);
void add_joker(JokerObject* joker_object);
void remove_owned_joker(int owned_joker_idx);
+5
View File
@@ -33,4 +33,9 @@ enum BackgroundId get_current_background(void);
*/
void change_background(enum BackgroundId id, bool force_redraw);
/**
* @brief Restores the bottom row of the top-left panel from the background map.
*/
void reset_top_left_panel_bottom_row(void);
#endif // COMMON_UI_H
+24
View File
@@ -0,0 +1,24 @@
/**
* @file joker_gallery.h
*
* @brief Main-menu Joker description gallery state functions.
*/
#ifndef GAME_JOKER_GALLERY_H
#define GAME_JOKER_GALLERY_H
/**
* @brief Initialize the Joker gallery and display the selected Joker.
*/
void game_joker_gallery_on_init(void);
/**
* @brief Process Joker gallery navigation input.
*/
void game_joker_gallery_on_update(void);
/**
* @brief Destroy gallery resources before leaving the state.
*/
void game_joker_gallery_on_exit(void);
#endif // GAME_JOKER_GALLERY_H
-8
View File
@@ -23,14 +23,6 @@ void game_shop_reset(void);
*/
JokerObject* game_shop_get_description_card(void);
/**
* @brief Set whether a Joker can appear in the shop.
*
* @param avail - true to make it available to the shop to appear in
* false to make it unavailable.
*/
void game_shop_set_joker_avail(int joker_id, bool avail);
/**
* @brief Change to the shop background
*/
+4
View File
@@ -7,6 +7,7 @@
#define GAME_VARIABLES_H
#include "blind.h"
#include "hand.h"
#include "random.h"
#include <tonc.h>
@@ -49,6 +50,9 @@ typedef struct
s32 hand_size;
s32 deck;
u32 best_hand_score;
u32 nb_played_hands[HAND_TYPE_MAX];
// Blind variables
enum BlindType current_blind;
+9 -9
View File
@@ -1,9 +1,9 @@
#ifndef GBALATRO_SYS8_FONT_H
#define GBALATRO_SYS8_FONT_H
#include <tonc.h>
extern const TFont gbalatro_sys8Font;
extern const unsigned int gbalatro_sys8Glyphs[192];
#endif
#ifndef GBALATRO_SYS8_H
#define GBALATRO_SYS8_H
#include <tonc.h>
extern const TFont gbalatro_sys8Font;
extern const unsigned int gbalatro_sys8Glyphs[];
#endif
+1 -6
View File
@@ -32,7 +32,7 @@
#define MAIN_BG_CBB 1
#define TTE_SBB 30
#define TTE_CBB 0
#define AFFINE_BG_SBB 2
#define AFFINE_BG_SBB 4
#define AFFINE_BG_CBB 2
#define PAL_ROW_LEN 16
#define NUM_PALETTES 16
@@ -510,11 +510,6 @@ void memcpy32_tile8_with_palette_offset(u32* dst, const u32* src, uint wcount, u
*/
void toggle_windows(bool win0, bool win1);
/**
* @brief Restores the bottom row of the top-left panel from the background map.
*/
void reset_top_left_panel_bottom_row(void);
/**
* @brief Justify a text with custom formatting tags according to the given
* justification and bias direction tags
+45 -11
View File
@@ -40,7 +40,8 @@ enum HandType
ROYAL_FLUSH,
FIVE_OF_A_KIND,
FLUSH_HOUSE,
FLUSH_FIVE
FLUSH_FIVE,
HAND_TYPE_MAX = FLUSH_FIVE
};
// clang-format off
@@ -71,6 +72,10 @@ typedef struct ContainedHandTypes
} ContainedHandTypes;
// clang-format on
// Misc Hand Functions
const char* get_hand_type_name(enum HandType hand_type);
// Hand Structure Manipulation
/**
@@ -211,24 +216,53 @@ void sort_cards(void);
// Hand Contents Analysis
/**
* Finds the largest flush (set of cards with the same suit) in the given array of played cards.
* Marks the cards belonging to the best flush in the out_selection array.
* @brief Finds the best flush (set of cards with the same suit) in the given array of played
* cards.
*
* Normally a Flush is made of 5 cards of the same suit, but this function takes into account
* the Four Fingers joker which allows for Flushes made of 4 cards only.
*
* The cards belonging to that flush will be marked as selected in the out_selection array.
*
* @param played Array of pointers to CardObject representing played cards.
* @param top Index of the top of the played stack.
* @param min_len Minimum number of cards required for a flush.
* @param out_selection Output array of bools; set to true for cards in the best flush, false
* otherwise.
* otherwise.
*
* @return The number of cards in the best flush found, or 0 if no flush meets min_len.
*/
int find_flush_in_played_cards(CardObject** played, int top, int min_len, bool* out_selection);
int find_straight_in_played_cards(
CardObject** played,
int top,
bool shortcut_active,
int min_len,
bool* out_selection
);
/**
* @brief Finds the best straight in the given array of played cards.
*
* Normally, a Straight is a set of 5 cards with sequential ranks, but this function takes into
* account the Four Fingers and Shortcut jokers, which respectively allow for Straights made of
* 4 cards and with gaps of 1 rank between two cards.
*
* The cards belonging to that straight will be marked as selected in the out_selection array.
*
* @param played Array of pointers to CardObject representing played cards.
* @param top Index of the top of the played stack.
* @param min_len Minimum number of cards required for a straight.
* @param out_selection Output array of bools; set to true for cards in the best straight, false
* otherwise.
*
* @return The number of cards in the best straight found, or 0 if no straight meets
* min_len.
*/
int find_straight_in_played_cards(CardObject** played, int top, int min_len, bool* out_selection);
/**
* @brief This is used for the special case in "Four Fingers" where you can add a pair into a
* straight (e.g. AA234 should score all 5 cards)
*
* @param played Array of pointers to CardObject representing played cards.
* @param top Index of the top of the played stack.
* @param selection In/Out array of bools; cards from a straight are already set to true, so we
* add to those the leftover ones that form a pair with an already selected card.
*/
void select_paired_cards_in_hand(CardObject** played, int top, bool* selection);
#endif
+173
View File
@@ -0,0 +1,173 @@
/**
* @file item.h
*
* @brief The core structure for items in the shop and inventory.
* Provides a common API for the shop and inventory to handle all types of items.
* Uses struct inheritance so all inherited items can implement an is-a relationship with Item.
* This means that pointers to structs that inherit Item using first member struct inheritance
* can and should be cast to Item* so code that expects an Item* can use them.
*/
#ifndef ITEM_H
#define ITEM_H
#include "mgba_logger.h"
#include "sprite.h"
// TODO: Merge these with GBAL_RETURN_IF_NULL macros?
/**
* @brief Checks if @p item is of type @p expected_type - logs error message and returns otherwise
*
* This version is for a function that returns a value, while
* @ref ITEM_RETURN_IF_UNEXPECTED_TYPE_VOID is for a void function.
*/
#define ITEM_RETURN_IF_UNEXPECTED_TYPE_RET(item, expected_type, ret_val) \
do \
{ \
if ((item)->type != expected_type) \
{ \
MGBA_FUNC_ERROR("Unexpected %s->type != %s", #item, #expected_type); \
return (ret_val); \
} \
} while (0)
/**
* @brief Checks if @p item is of type @p expected_type - logs error message and returns otherwise
*
* This version is for a void function, while @ref ITEM_RETURN_IF_UNEXPECTED_TYPE_RET is for one
* with a return value.
*/
#define ITEM_RETURN_IF_UNEXPECTED_TYPE_VOID(item, expected_type) \
do \
{ \
if ((item)->type != expected_type) \
{ \
MGBA_FUNC_ERROR("Unexpected %s->type != %s", #item, #expected_type); \
return; \
} \
} while (0)
enum ItemType
{
ITEM_TYPE_JOKER,
ITEM_TYPE_PLAYING_CARD,
// Future planned item types
// ITEM_TYPE_CONSUMABLE, // Expand to PLANET, TAROT, and SPECTRAL?
// ITEM_TYPE_VOUCHER,
// ITEM_TYPE_PACK
ITEM_NUM_TYPES
};
/**
* @brief A generic interface for all items that can appear in the shop or be in the inventory.
* This uses first member struct inheritance - other structs are meant to inherit it by
* making their first member field Item.
* Then casts from inheriting structs to Item* are allowed and intentional and this allows for
* generic code that uses polymorphism.
* The -fms-extensions compile flag allows for anonymous members making it behave fully
* as inheritance. It makes all member fields be fully inherited so any struct
* that inherits Item for example will have all its fields accessible directly,
* e.g. `JokerObject joker_object; joker_object.type = ITEM_TYPE_JOKER`
*/
typedef struct Item
{
/**
* @brief First member struct inheritance
* all items that can appear in the shop are SpriteObjects.
* Note that this is an anonymous member.
*/
SpriteObject;
/**
* @brief The item type - used to dispatch the function implementations for inheriting types.
*/
enum ItemType type;
} Item;
/**
* @brief The set of functions that each item type implements.
*/
typedef struct ItemFuncs
{
/**
* All items must implement the following since they are called by the shop and all items
* must be capable of appearing in the shop.
*/
Item* (*roll_new)(void);
int (*get_buy_price)(Item* item);
bool (*can_acquire)(Item* item);
void (*acquire)(Item* item);
void (*dispose)(Item** item);
// TODO: void (*print_description)(Item* item); // or something of the form
// Optional implementation functions will be added here
} ItemFuncs;
/**
* @brief Rolls a random item of type @p item_type and returns a newly created one.
* Manages rollable items set if necessary (i.e. not rolling items already in inventory)
* To be used when rolling new items for the shop or packs.
*
* Matches @ref ItemFuncs.roll_new()
*
* @param item_type The type of the item to roll
*
* @return The newly created randomly rolled item
*/
Item* item_roll_new(enum ItemType item_type);
/**
* @brief Returns the buy price of the item.
*
* Matches @ref ItemFuncs.get_buy_price()
*
* @param item The item whose price to return.
*
* @return UNDEFINED in case of error, the item's buy price otherwise.
*/
int item_get_buy_price(Item* item);
/**
* @brief Acquires the item, adding to inventory if applicable.
* Called when it is purchased from the shop, note that it does not
* perform the purchase operation of decrementing the player's money,
* that should be handled by the shop code.
* For packs this can be to just open the pack,
* for vouchers, this will apply their effect.
*
* Matches @ref ItemFuncs.acquire()
*
* @param item The item to acquire
*/
void item_acquire(Item* item);
/**
* @brief Returns true if the item can be acquired, i.e. added to inventory.
* Does not check if the player has enough money to buy the item, that is the shop's job,
* as this will be used both when purchasing and when selecting in a pack.
*
* Matches @ref ItemFuncs.can_acquire()
*
* @param item The item to check
*/
bool item_can_acquire(Item* item);
/**
* @brief Destroys an item, freeing underlying resources, and manages rollable items sets if needed.
* To be used when destroying items from the inventory, shop, or packs.
*
* @param item A pointer to an item for destruction.
*/
void item_dispose(Item** item);
/**
* @brief Prints the buy price under the item
* Relies on the fact item is a SpriteObject
*
* @param item The item to print under
*/
void item_print_buy_price_under(Item* item);
#endif // ITEM_H
+26
View File
@@ -0,0 +1,26 @@
/**
* @file item_funcs.h
*
* @brief An API for the item functions defined in item.h
* Separated module from item.c/.h so item_funcs.c can include joker.h and card.h etc.
* so the item.h doesn't depend on implementations such as jokers and cards.
*/
#ifndef ITEM_FUNCS_H
#define ITEM_FUNCS_H
#include "item.h"
/**
* @brief Returns the function table for a given item type.
*
* Looks up the @ref ItemFuncs dispatch table corresponding to @p type.
*
* @param type The item type whose function table to retrieve.
*
* @return Pointer to the @ref ItemFuncs table for @p type,
* or NULL if @p type is invalid or has no implemented functions.
*/
ItemFuncs* get_item_type_funcs(enum ItemType type);
#endif // ITEM_FUNCS_H
+47 -2
View File
@@ -9,7 +9,7 @@
#include "card.h"
#include "game.h"
#include "graphic_utils.h"
#include "sprite.h"
#include "item.h"
#include <maxmod.h>
@@ -112,8 +112,8 @@ typedef struct
typedef struct JokerObject
{
Item; // First member struct inheritance
Joker* joker;
SpriteObject* sprite_object;
} JokerObject;
typedef struct // These jokers are triggered after the played hand has finished scoring.
@@ -197,6 +197,51 @@ JokerObject* joker_object_new(Joker* joker);
void joker_object_destroy(JokerObject** joker_object);
// 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);
/**
* @brief Returns the buy price of the joker object.
*
* @param joker_object the joker object whose price to return.
*
* @return UNDEFINED in case of error, the buy price of the joker otherwise.
*/
int joker_object_get_buy_price(Item* joker_object);
// TODO: Move to an owned_jokers.c/.h file?
/**
* @brief Add a Joker to the list of owned Jokers and place it in the joker row.
*
* @param joker_object The JokerObject to add cast to Item*
*/
void joker_object_add_to_owned(Item* joker_object);
/**
* @brief Destroy a JokerObject item, free its resources, and make it available to be rolled.
*
* @param joker_object Pointer to the JokerObject Item* to destroy; set to NULL.
*/
void joker_object_dispose(Item** joker_object);
/**
* @brief Set whether a Joker is available to be rolled for the shop, packs, etc.
*
* @param joker_id The ID of the joker whose availability to set.
* @param rollable true to make it rollable, false otherwise.
*/
void joker_set_rollable(int joker_id, bool rollable);
/**
* @brief Reset rollable jokers to include all jokers in the registry.
*/
void joker_reset_rollable_jokers(void);
/**
* @brief Roll and create a new JokerObject item.
*
* @return Newly created `Item*` (JokerObject) or NULL if none available.
*/
Item* joker_object_roll_new(void);
// 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
+16 -15
View File
@@ -9,21 +9,22 @@
#include "graphic_utils.h"
// clang-format off
// Points x y
static const BG_POINT CUR_BLIND_TOKEN_POS = {8, 18};
static const BG_POINT TOP_LEFT_PANEL_POINT = {0, 0};
static const BG_POINT ROUND_END_REWARDS_ELLIPSIS_POS = {10, 13};
static const BG_POINT JOKER_DISCARD_TARGET = {240, 30};
static const BG_POINT HELD_JOKERS_POS = {108, 10};
// Rects left top right bottom
static const Rect TOP_LEFT_PANEL_ANIM_RECT = {0, 0, 8, 4};
static const Rect POP_MENU_ANIM_RECT = {9, 7, 24, 31};
static const Rect DECK_ANIM_RECT = {25, 14, 28, 23}; // Can be used for the Shop and Blind Select screen
static const Rect TOP_LEFT_ITEM_SRC_RECT = {0, 20, 8, 25};
static const Rect BLIND_REWARD_RECT = {40, 32, 64, 40};
static const Rect BLIND_REQ_TEXT_RECT = {32, 24, 64, 32};
static const Rect PLAYING_SCREEN_RECT = {72, 0, 240, 160};
static const Rect HAND_SIZE_RECT = {128, 128, 152, 160}; // Seems to include both SELECT and PLAYING
// Points x y
static const BG_POINT CUR_BLIND_TOKEN_POS = {8, 18};
static const BG_POINT TOP_LEFT_PANEL_POINT = {0, 0};
static const BG_POINT ROUND_END_REWARDS_ELLIPSIS_POS = {10, 13};
static const BG_POINT JOKER_DISCARD_TARGET = {240, 30};
static const BG_POINT HELD_JOKERS_POS = {108, 10};
// Rects left top right bottom
static const Rect TOP_LEFT_PANEL_ANIM_RECT = {0, 0, 8, 4};
static const Rect POP_MENU_ANIM_RECT = {9, 7, 24, 31};
static const Rect DECK_ANIM_RECT = {25, 14, 28, 23}; // Can be used for the Shop and Blind Select screen
static const Rect TOP_LEFT_ITEM_SRC_RECT = {0, 20, 8, 25};
static const Rect TOP_LEFT_PANEL_BOTTOM_ROW_RESET_RECT = {0, 28, 8, 28};
static const Rect BLIND_REWARD_RECT = {40, 32, 64, 40};
static const Rect BLIND_REQ_TEXT_RECT = {32, 24, 64, 32};
static const Rect PLAYING_SCREEN_RECT = {72, 0, 240, 160};
static const Rect HAND_SIZE_RECT = {128, 128, 152, 160}; // Seems to include both SELECT and PLAYING
// clang-format on
#endif // LAYOUT_H
+32 -1
View File
@@ -17,7 +17,7 @@
* mgba -l 14 game.rom # INFO, WARN, and ERROR
* ```
*
* @note You have to fight with other logs in mgba and DEBUG can get messy.
* @note You have to fight with other logs in mgba and INFO can get messy.
*
* @note FATAL does kill the game. Use with care.
*/
@@ -54,19 +54,50 @@ bool mgba_logger_init(void);
*/
void mgba_printf(MgbaLogLevel level, const char* fmt, ...);
/**
* @brief Print to mgba log with a format string and function name
*
* @param level
* @param func_name Function name - prepended to the log string "<func_name>(): <log_string>"
* @param fmt Format string
* @param ... variadic arguments
*
* @note for all logs, it's cutoff at the hard mgba limit of 0x100
*/
void mgba_func_printf(MgbaLogLevel level, const char* func_name, const char* fmt, ...);
// clang-format off
#ifdef MGBA_LOGGING
#define MGBA_FATAL(...) mgba_printf(MGBA_LOG_FATAL, __VA_ARGS__)
#define MGBA_ERROR(...) mgba_printf(MGBA_LOG_ERROR, __VA_ARGS__)
#define MGBA_WARN(...) mgba_printf(MGBA_LOG_WARN, __VA_ARGS__)
#define MGBA_INFO(...) mgba_printf(MGBA_LOG_INFO, __VA_ARGS__)
#define MGBA_DEBUG(...) mgba_printf(MGBA_LOG_DEBUG, __VA_ARGS__)
#define MGBA_FUNC_LOG(level, ...) mgba_func_printf(level, __func__, __VA_ARGS__)
#define MGBA_FUNC_FATAL(...) MGBA_FUNC_LOG(MGBA_LOG_FATAL, __VA_ARGS__)
#define MGBA_FUNC_ERROR(...) MGBA_FUNC_LOG(MGBA_LOG_ERROR, __VA_ARGS__)
#define MGBA_FUNC_WARN(...) MGBA_FUNC_LOG(MGBA_LOG_WARN, __VA_ARGS__)
#define MGBA_FUNC_INFO(...) MGBA_FUNC_LOG(MGBA_LOG_INFO, __VA_ARGS__)
#define MGBA_FUNC_DEBUG(...) MGBA_FUNC_LOG(MGBA_LOG_DEBUG, __VA_ARGS__)
#else
#define MGBA_FATAL(...) ((void)0)
#define MGBA_ERROR(...) ((void)0)
#define MGBA_WARN(...) ((void)0)
#define MGBA_INFO(...) ((void)0)
#define MGBA_DEBUG(...) ((void)0)
#define MGBA_FUNC_LOG(level, ...) ((void)0)
#define MGBA_FUNC_FATAL(...) ((void)0)
#define MGBA_FUNC_ERROR(...) ((void)0)
#define MGBA_FUNC_WARN(...) ((void)0)
#define MGBA_FUNC_INFO(...) ((void)0)
#define MGBA_FUNC_DEBUG(...) ((void)0)
#endif
// clang-format on
+1 -1
View File
@@ -30,7 +30,7 @@ typedef struct
void rng_init(void);
/**
* @brief Update the CPU cycles counter and accumulate it into a bigger u32 `timer_acc` variable.
* @brief Accumulates CPU cycles for RNG seed generation, call exaclty once per frame.
*/
void rng_update(void);
+9 -8
View File
@@ -194,23 +194,24 @@ void sprite_init(void);
void sprite_draw(void);
/**
* @brief Allocate and retrieve a pointer to a valid SpriteObject
* @brief Initialize a SpriteObject to a default state.
* Must be called only once per SpriteObject when it is created.
*
* @return A valid pointer to an newly allocated SpriteObject
* if successful, othewise return **NULL**.
* @param sprite_object - The SpriteObject to initialize
*/
SpriteObject* sprite_object_new();
void sprite_object_init(SpriteObject* sprite_object);
/**
* @brief Destroy SpriteObject
*
* Destroy a SpriteObject by freeing it back to the pool and releasing its
* associated resources
* Destroy a SpriteObject by releasing its associated resources (e.g. the sprite).
* This invalidates the SpriteObject and it should not be used after destroyed,
* a new one should be created instead.
*
* @param sprite_object pointer to a pointer of SpriteObject to destroy.
* @param sprite_object pointer to a SpriteObject to destroy.
* Cannot be **NULL**.
*/
void sprite_object_destroy(SpriteObject** sprite_object);
void sprite_object_destroy(SpriteObject* sprite_object);
/**
* @brief Register a Sprite to an associated SpriteObject
+46
View File
@@ -8,6 +8,9 @@
#define UTIL_H
#include <stdint.h>
#ifdef MGBA_LOGGING
#include "mgba_logger.h"
#endif
/**
* @def GBAL_UNUSED
@@ -56,6 +59,49 @@
// so it needs at least this number of chars to be able to display any suffixed number
#define SUFFIXED_NUM_MIN_REQ_CHARS 4
#ifdef MGBA_LOGGING
#define LOG_ERROR(...) MGBA_FUNC_ERROR(__VA_ARGS__)
#else
// TODO: Add a define to conditionally compile print error to console and add it to the tests?
#define LOG_ERROR(...) ((void)(0))
#endif
/**
* @brief Checks if @p param is NULL and prints error message and returns in case it is.
* Useful for checking arguments to a function or errors during control flow.
*
* This version is for a void function, while @ref GBAL_RETURN_ON_ERROR_VAL_RET is for one with
* a return value.
*/
#define GBAL_RETURN_IF_NULL_VOID(param) \
do \
{ \
if ((param) == NULL) \
{ \
LOG_ERROR("Unexpected value: %s == NULL", #param); \
return; \
} \
} while (0)
/**
* @brief Checks if @p param is equal to NULL
* and prints error message and returns in case it is.
* Useful for checking arguments to a function or errors during control flow.
* @param ret_val The value to return in case @p param is equal to NULL.
*
* This version is for a function that returns a value while @ref GBAL_RETURN_ON_ERROR_VAL_VOID
* is for a void function.
*/
#define GBAL_RETURN_IF_NULL_RET(param, ret_val) \
do \
{ \
if ((param) == NULL) \
{ \
LOG_ERROR("Unexpected value: %s == NULL", #param); \
return (ret_val); \
} \
} while (0)
/**
* @brief Avoid overflow when adding two u32 integers
*
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Append the Chinese Joker-description glyph subset to the project's 8x8 ASCII font."""
from argparse import ArgumentParser
from pathlib import Path
import re
from PIL import Image, ImageDraw, ImageFont
ASCII_FONT_HEIGHT = 48
CELL_SIZE = 8
GLYPHS_PER_ROW = 16
def read_codepoints(table_path: Path) -> list[int]:
table = table_path.read_text(encoding="utf-8")
return [int(value, 16) for value in re.findall(r"CHINESE_GLYPH\(0x([0-9A-Fa-f]+)\)", table)]
def main() -> None:
parser = ArgumentParser()
parser.add_argument("--font", required=True, help="Fusion Pixel Font 8px zh_hans TTF")
parser.add_argument("--table", default="include/def_chinese_glyph_table.h")
parser.add_argument("--image", default="font/gbalatro_sys8.png")
args = parser.parse_args()
image_path = Path(args.image)
base = Image.open(image_path).convert("RGB").crop((0, 0, 128, ASCII_FONT_HEIGHT))
codepoints = read_codepoints(Path(args.table))
extra_rows = (len(codepoints) + GLYPHS_PER_ROW - 1) // GLYPHS_PER_ROW
output = Image.new("RGB", (128, ASCII_FONT_HEIGHT + extra_rows * CELL_SIZE), "white")
output.paste(base, (0, 0))
draw = ImageDraw.Draw(output)
font = ImageFont.truetype(args.font, CELL_SIZE)
for index, codepoint in enumerate(codepoints):
x = (index % GLYPHS_PER_ROW) * CELL_SIZE
y = ASCII_FONT_HEIGHT + (index // GLYPHS_PER_ROW) * CELL_SIZE
draw.text((x, y - 1), chr(codepoint), font=font, fill="black")
output.save(image_path, optimize=True)
print(f"Appended {len(codepoints)} Chinese glyphs to {image_path}")
if __name__ == "__main__":
main()
+10 -5
View File
@@ -5,7 +5,6 @@ import argparse
CHAR_WIDTH = 8
CHAR_HEIGHT = 8
NUM_VERT_CHARS = 6
NUM_HORZ_CHARS = 16
WORD_SIZE = 32
@@ -48,8 +47,14 @@ img = img.convert("P")
img = img.convert('1')
img = ImageOps.invert(img)
if img.width != NUM_HORZ_CHARS * CHAR_WIDTH or img.height % CHAR_HEIGHT != 0:
raise ValueError("font image must be 128px wide and a multiple of 8px high")
num_vert_chars = img.height // CHAR_HEIGHT
num_chars = num_vert_chars * NUM_HORZ_CHARS
glyphs = []
for char_y in range(NUM_VERT_CHARS):
for char_y in range(num_vert_chars):
for char_x in range(NUM_HORZ_CHARS):
# Get the 8x8 crop area at our current character in the image
pix_x = char_x * CHAR_WIDTH
@@ -86,7 +91,7 @@ with open(out_path, "w") as out:
.global gbalatro_sys8Font
gbalatro_sys8Font:
.word gbalatro_sys8Glyphs, 0, 0
.hword 32, 96
.hword 32, %d
.byte 8, 8
.byte 8, 8
.hword 8
@@ -94,9 +99,9 @@ gbalatro_sys8Font:
.section .rodata
.align 2
.global gbalatro_sys8Glyphs @ 768 bytes (192 unsigned ints)
.global gbalatro_sys8Glyphs
gbalatro_sys8Glyphs:
""")
""" % num_chars)
for i, glyph in enumerate(glyphs):
if first:
+29 -29
View File
@@ -11,10 +11,10 @@
// done in VBLANK so the HBLANK code can just fetch the values quickly.
static IWRAM_CODE void s_affine_background_prep_bgaff_arr();
static BG_AFFINE _bgaff_arr[SCREEN_HEIGHT + 1];
static AFF_SRC_EX _asx = {0};
static enum AffineBackgroundID _background = AFFINE_BG_NONE;
static uint _timer = 0;
static BG_AFFINE s_bgaff_arr[SCREEN_HEIGHT + 1];
static AFF_SRC_EX s_asx = {0};
static enum AffineBackgroundID s_background = AFFINE_BG_NONE;
static uint s_timer = 0;
void affine_background_init()
{
@@ -33,7 +33,7 @@ IWRAM_CODE void affine_background_hblank()
}
// See comment in affine_background_prep_bgaff_arr()
REG_BG_AFFINE[AFFINE_BG_IDX] = _bgaff_arr[vcount + 1];
REG_BG_AFFINE[AFFINE_BG_IDX] = s_bgaff_arr[vcount + 1];
}
void affine_background_update()
@@ -44,21 +44,21 @@ void affine_background_update()
}
else // Low quality mode without HBLANK interrupt
{
_asx.scr_x = 0;
_asx.scr_y = 0;
_asx.tex_x += 5;
_asx.tex_y += 12;
s_asx.scr_x = 0;
s_asx.scr_y = 0;
s_asx.tex_x += 5;
s_asx.tex_y += 12;
// Scale the sine value to fit in a s16
_asx.sx = ((lu_sin(_timer * 100)) >> 8) + 256;
s_asx.sx = ((lu_sin(s_timer * 100)) >> 8) + 256;
// Scale the sine value to fit in a s16
_asx.sy = ((lu_sin(_timer * 100 + 0x4000)) >> 8) + 256;
_asx.alpha = 0;
s_asx.sy = ((lu_sin(s_timer * 100 + 0x4000)) >> 8) + 256;
s_asx.alpha = 0;
bg_rotscale_ex(&_bgaff_arr[0], &_asx);
REG_BG_AFFINE[AFFINE_BG_IDX] = _bgaff_arr[0];
bg_rotscale_ex(&s_bgaff_arr[0], &s_asx);
REG_BG_AFFINE[AFFINE_BG_IDX] = s_bgaff_arr[0];
}
_timer++;
s_timer++;
}
void affine_background_set_color(COLOR color)
@@ -66,7 +66,7 @@ void affine_background_set_color(COLOR color)
// Reload the palette to reset any previous color scaling.
// Take source color directly from `affine_background_gfxPal` to avoid excessive
// darkening in case this function is called twice by mistake.
affine_background_change_background(_background);
affine_background_change_background(s_background);
for (int i = 0; i < AFFINE_BG_PAL_LEN; i++)
{
clr_rgbscale(&pal_bg_mem[AFFINE_BG_PB] + i, affine_background_gfxPal + i, 1, color);
@@ -80,14 +80,14 @@ void affine_background_load_palette(const u16* src)
void affine_background_change_background(enum AffineBackgroundID new_bg)
{
if (_background == new_bg)
if (s_background == new_bg)
{
return;
}
_background = new_bg;
s_background = new_bg;
switch (_background)
switch (s_background)
{
case AFFINE_BG_MAIN_MENU:
REG_BG2CNT &= ~BG_AFF_32x32;
@@ -126,26 +126,26 @@ static IWRAM_CODE void s_affine_background_prep_bgaff_arr()
{
for (u16 vcount = 0; vcount < SCREEN_HEIGHT; vcount++)
{
const s32 timer_s32 = _timer << 8;
const s32 timer_s32 = s_timer << 8;
const s32 vcount_s32 = vcount << 8;
const s16 vcount_s16 = vcount;
const s32 vcount_sine = lu_sin(vcount_s32 + timer_s32 / ANIMATION_SPEED_DIVISOR);
_asx.scr_x = (SCREEN_WIDTH / 2);
s_asx.scr_x = (SCREEN_WIDTH / 2);
// scr_y must equal vcount otherwise the background will have no vertical difference
_asx.scr_y = vcount_s16 - (SCREEN_HEIGHT / 2);
_asx.tex_x = (1000 * 1000) + (vcount_sine);
_asx.tex_y = (1000 * 1000);
_asx.sx = 128;
_asx.sy = 128;
_asx.alpha = vcount_sine + (timer_s32 / ANIMATION_SPEED_DIVISOR);
s_asx.scr_y = vcount_s16 - (SCREEN_HEIGHT / 2);
s_asx.tex_x = (1000 * 1000) + (vcount_sine);
s_asx.tex_y = (1000 * 1000);
s_asx.sx = 128;
s_asx.sy = 128;
s_asx.alpha = vcount_sine + (timer_s32 / ANIMATION_SPEED_DIVISOR);
bg_rotscale_ex(&_bgaff_arr[vcount], &_asx);
bg_rotscale_ex(&s_bgaff_arr[vcount], &s_asx);
}
/* HBLANK occurs after the scanline so REG_VCOUNT represents the
* the scanline that just passed, so when it's SCREEN_HEIGHT we will
* actually be updating the first line
*/
_bgaff_arr[SCREEN_HEIGHT] = _bgaff_arr[0];
s_bgaff_arr[SCREEN_HEIGHT] = s_bgaff_arr[0];
}
+12 -12
View File
@@ -36,16 +36,16 @@ static const unsigned short* blind_gfxPal[] = {
};
// Bitfields storing blinds we have yet to beat during the current run
static List unbeaten_boss_blinds;
static List unbeaten_showdown_blinds;
static List s_unbeaten_boss_blinds;
static List s_unbeaten_showdown_blinds;
// Maps the ante number to the base blind requirement for that ante.
// The game starts at ante 1 which is at index 1 for base requirement 300.
// Ante 0 is also there in case it is ever reached.
static const u32 ante_lut[] = {100, 300, 800, 2000, 5000, 11000, 20000, 35000, 50000};
static const u32 ANTE_LUT[] = {100, 300, 800, 2000, 5000, 11000, 20000, 35000, 50000};
// clang-format off
static Blind _blind_type_map[BLIND_TYPE_MAX] = {
static Blind s_blind_type_map[BLIND_TYPE_MAX] = {
{BLIND_TYPE_SMALL, FIX_ONE, 3},
{BLIND_TYPE_BIG, (FIX_ONE * 3) / 2, 4},
{BLIND_TYPE_HOOK, FIX_ONE * 2, 5},
@@ -94,12 +94,12 @@ u32 blind_get_requirement(enum BlindType type, int ante)
if (ante < 0 || ante > MAX_ANTE)
ante = 0;
return fx2int(_blind_type_map[type].score_req_multipler * ante_lut[ante]);
return fx2int(s_blind_type_map[type].score_req_multipler * ANTE_LUT[ante]);
}
int blind_get_reward(enum BlindType type)
{
return _blind_type_map[type].reward;
return s_blind_type_map[type].reward;
}
// Fills the unbeaten boss blinds lists
@@ -111,11 +111,11 @@ void init_unbeaten_blinds_list(bool showdown)
if (!init)
{
init = true;
unbeaten_showdown_blinds = list_init();
unbeaten_boss_blinds = list_init();
s_unbeaten_showdown_blinds = list_init();
s_unbeaten_boss_blinds = list_init();
}
List* p_unbeaten_blinds = showdown ? &unbeaten_showdown_blinds : &unbeaten_boss_blinds;
List* p_unbeaten_blinds = showdown ? &s_unbeaten_showdown_blinds : &s_unbeaten_boss_blinds;
// empty the list just to be sure
list_clear(p_unbeaten_blinds);
@@ -125,13 +125,13 @@ void init_unbeaten_blinds_list(bool showdown)
for (int i = lower_blind; i <= upper_blind; i++)
{
list_push_back(p_unbeaten_blinds, &_blind_type_map[i]);
list_push_back(p_unbeaten_blinds, &s_blind_type_map[i]);
}
}
enum BlindType roll_blind_type(bool showdown)
{
List* p_unbeaten_blinds = showdown ? &unbeaten_showdown_blinds : &unbeaten_boss_blinds;
List* p_unbeaten_blinds = showdown ? &s_unbeaten_showdown_blinds : &s_unbeaten_boss_blinds;
// Fill the list with all blinds if it is empty
// (happens on startup or if we have beaten all blinds)
@@ -150,7 +150,7 @@ enum BlindType roll_blind_type(bool showdown)
void set_blind_beaten(enum BlindType type)
{
bool showdown = (type >= BLIND_TYPE_SHOWDOWN);
List* p_unbeaten_blinds = showdown ? &unbeaten_showdown_blinds : &unbeaten_boss_blinds;
List* p_unbeaten_blinds = showdown ? &s_unbeaten_showdown_blinds : &s_unbeaten_boss_blinds;
// find the beaten blind idx in the list
int beaten_idx = 0;
+41 -19
View File
@@ -1,6 +1,8 @@
#include "card.h"
#include "graphic_utils.h"
#include "item.h"
#include "util.h"
#include <maxmod.h>
#include <stdlib.h>
@@ -17,17 +19,17 @@
// Card sprites lookup table. First index is the suit, second index is the rank. The value is the
// tile index.
const static u16 _card_sprite_lut[NUM_SUITS][NUM_RANKS] = {
const static u16 CARD_SPRITE_LUT[NUM_SUITS][NUM_RANKS] = {
{0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192},
{208, 224, 240, 256, 272, 288, 304, 320, 336, 352, 368, 384, 400},
{416, 432, 448, 464, 480, 496, 512, 528, 544, 560, 576, 592, 608},
{624, 640, 656, 672, 688, 704, 720, 736, 752, 768, 784, 800, 816}
};
// Deck sprites lookup table. Index is the deck Id. The value is the tile index.
const static u16 _deck_sprite_lut[DECK_TYPE_MAX] = {0, 16, 32, 48, 64, 80};
const static u16 DECK_SPRITE_LUT[DECK_TYPE_MAX] = {0, 16, 32, 48, 64, 80};
bool high_contrast = DEFAULT_HIGH_CONTRAST;
bool more_readable = DEFAULT_MORE_READABLE;
static bool s_high_contrast = DEFAULT_HIGH_CONTRAST;
static bool s_more_readable = DEFAULT_MORE_READABLE;
void card_init()
{
@@ -36,8 +38,8 @@ void card_init()
void set_cards_high_contrast(bool enable)
{
high_contrast = enable;
if (high_contrast)
s_high_contrast = enable;
if (s_high_contrast)
{
GRIT_CPY(&pal_obj_mem[CARD_PB * PAL_ROW_LEN], high_contrast_deck_pal_gfxPal);
}
@@ -49,17 +51,17 @@ void set_cards_high_contrast(bool enable)
void set_cards_more_readable(bool enable)
{
more_readable = enable;
s_more_readable = enable;
}
bool get_cards_high_contrast(void)
{
return high_contrast;
return s_high_contrast;
}
bool get_cards_more_readable(void)
{
return more_readable;
return s_more_readable;
}
// Card methods
@@ -101,9 +103,10 @@ u8 card_get_value(Card* card)
CardObject* card_object_new(Card* card)
{
CardObject* card_object = POOL_GET(CardObject);
sprite_object_init((SpriteObject*)card_object);
card_object->card = card;
card_object->sprite_object = sprite_object_new();
card_object->type = ITEM_TYPE_PLAYING_CARD;
card_object->selected = false;
return card_object;
@@ -113,7 +116,7 @@ void card_object_destroy(CardObject** card_object)
{
if (*card_object == NULL)
return;
sprite_object_destroy(&((*card_object)->sprite_object));
sprite_object_destroy((SpriteObject*)(*card_object));
POOL_FREE(CardObject, *card_object);
*card_object = NULL;
}
@@ -121,10 +124,10 @@ void card_object_destroy(CardObject** card_object)
void card_object_set_sprite(CardObject* card_object, int layer)
{
int tile_index = CARD_TID + (layer * CARD_SPRITE_OFFSET);
const unsigned int* card_tiles = more_readable ? deck_big_gfxTiles : deck_gfxTiles;
const unsigned int* card_tiles = s_more_readable ? deck_big_gfxTiles : deck_gfxTiles;
memcpy32(
&tile_mem[TILE_MEM_OBJ_CHARBLOCK0_IDX][tile_index],
&card_tiles[_card_sprite_lut[card_object->card->suit][card_object->card->rank] * TILE_SIZE],
&card_tiles[CARD_SPRITE_LUT[card_object->card->suit][card_object->card->rank] * TILE_SIZE],
TILE_SIZE * CARD_SPRITE_OFFSET
);
Sprite* sprite = sprite_new(
@@ -134,7 +137,7 @@ void card_object_set_sprite(CardObject* card_object, int layer)
CARD_PB,
layer + CARD_STARTING_LAYER
);
sprite_object_set_sprite(card_object->sprite_object, sprite);
sprite_object_set_sprite((SpriteObject*)card_object, sprite);
}
void card_object_set_sprite_face_down(CardObject* card_object, enum DeckType deck, int layer)
@@ -142,7 +145,7 @@ void card_object_set_sprite_face_down(CardObject* card_object, enum DeckType dec
int tile_index = CARD_TID + (layer * CARD_SPRITE_OFFSET);
memcpy32(
&tile_mem[TILE_MEM_OBJ_CHARBLOCK0_IDX][tile_index],
&decks_face_down_gfxTiles[_deck_sprite_lut[deck] * TILE_SIZE],
&decks_face_down_gfxTiles[DECK_SPRITE_LUT[deck] * TILE_SIZE],
TILE_SIZE * CARD_SPRITE_OFFSET
);
Sprite* sprite = sprite_new(
@@ -152,12 +155,12 @@ void card_object_set_sprite_face_down(CardObject* card_object, enum DeckType dec
DECK_SPRITES_PB,
layer + CARD_STARTING_LAYER
);
sprite_object_set_sprite(card_object->sprite_object, sprite);
sprite_object_set_sprite((SpriteObject*)card_object, sprite);
}
void card_object_shake(CardObject* card_object, mm_word sound_id)
{
sprite_object_shake(card_object->sprite_object, sound_id);
sprite_object_shake((SpriteObject*)card_object, sound_id);
}
void card_object_set_selected(CardObject* card_object, bool selected)
@@ -178,5 +181,24 @@ Sprite* card_object_get_sprite(CardObject* card_object)
{
if (card_object == NULL)
return NULL;
return sprite_object_get_sprite(card_object->sprite_object);
return sprite_object_get_sprite((SpriteObject*)card_object);
}
int card_object_get_buy_price(Item* card_object)
{
GBAL_RETURN_IF_NULL_RET(card_object, UNDEFINED);
ITEM_RETURN_IF_UNEXPECTED_TYPE_RET(card_object, ITEM_TYPE_PLAYING_CARD, UNDEFINED);
return 1;
}
void card_object_dispose(Item** card_object_item)
{
GBAL_RETURN_IF_NULL_VOID(card_object_item);
GBAL_RETURN_IF_NULL_VOID(*card_object_item);
ITEM_RETURN_IF_UNEXPECTED_TYPE_VOID(*card_object_item, ITEM_TYPE_PLAYING_CARD);
CardObject* card_object = (CardObject*)(*card_object_item);
card_object_destroy(&card_object);
*card_object_item = NULL;
}
+113
View File
@@ -0,0 +1,113 @@
#include "chinese_text.h"
#include <stdint.h>
#define CHINESE_GLYPH_FIRST 128
static const uint32_t s_chinese_codepoints[] = {
#define CHINESE_GLYPH(codepoint) codepoint,
#include "def_chinese_glyph_table.h"
#undef CHINESE_GLYPH
};
_Static_assert(
sizeof(s_chinese_codepoints) / sizeof(s_chinese_codepoints[0]) <= 256 - CHINESE_GLYPH_FIRST,
"Chinese glyph table exceeds the single-byte encoding range"
);
static uint32_t decode_utf8_codepoint(const unsigned char** cursor)
{
const unsigned char* text = *cursor;
if (text[0] < 0x80)
{
*cursor += 1;
return text[0];
}
if ((text[0] & 0xE0) == 0xC0 && (text[1] & 0xC0) == 0x80)
{
*cursor += 2;
return ((uint32_t)(text[0] & 0x1F) << 6) | (text[1] & 0x3F);
}
if ((text[0] & 0xF0) == 0xE0 && (text[1] & 0xC0) == 0x80 && (text[2] & 0xC0) == 0x80)
{
*cursor += 3;
return ((uint32_t)(text[0] & 0x0F) << 12) | ((uint32_t)(text[1] & 0x3F) << 6) |
(text[2] & 0x3F);
}
*cursor += 1;
return '?';
}
static uint32_t codepoint_to_glyph(uint32_t codepoint)
{
for (size_t i = 0; i < sizeof(s_chinese_codepoints) / sizeof(s_chinese_codepoints[0]); i++)
{
if (s_chinese_codepoints[i] == codepoint)
{
return CHINESE_GLYPH_FIRST + i;
}
}
return '?';
}
static size_t encode_tte_codepoint(uint32_t codepoint, char* output, size_t output_size)
{
if (codepoint < 0x80)
{
if (output_size < 1)
{
return 0;
}
output[0] = (char)codepoint;
return 1;
}
if (codepoint <= 0x7FF)
{
if (output_size < 2)
{
return 0;
}
output[0] = (char)(0xC0 | (codepoint >> 6));
output[1] = (char)(0x80 | (codepoint & 0x3F));
return 2;
}
return 0;
}
size_t chinese_text_encode(const char* utf8, char* output, size_t output_size)
{
if (utf8 == NULL || output == NULL || output_size == 0)
{
return 0;
}
const unsigned char* cursor = (const unsigned char*)utf8;
size_t output_len = 0;
while (*cursor != '\0')
{
uint32_t codepoint = decode_utf8_codepoint(&cursor);
uint32_t tte_codepoint = codepoint < 0x80 ? codepoint : codepoint_to_glyph(codepoint);
size_t encoded_size =
encode_tte_codepoint(tte_codepoint, output + output_len, output_size - output_len - 1);
if (encoded_size == 0)
{
break;
}
output_len += encoded_size;
}
output[output_len] = '\0';
return output_len;
}
+2 -2
View File
@@ -14,7 +14,7 @@
/**
* @brief List of DeckType names, all formatted to be centered in a string of 13 characters.
*/
static const char deck_names[DECK_TYPE_MAX][DECK_NAME_LENGTH] = {
static const char DECK_NAMES[DECK_TYPE_MAX][DECK_NAME_LENGTH] = {
" Red Deck ",
" Blue Deck ",
" Yellow Deck ",
@@ -42,7 +42,7 @@ static const PrintDescCallback deck_description_functions[DECK_TYPE_MAX] = {
void print_deck_name(enum DeckType deck, BG_POINT pos)
{
tte_printf("#{P:%d,%d; cx:0x%X000}%s", pos.x, pos.y, TTE_WHITE_PB, deck_names[deck]);
tte_printf("#{P:%d,%d; cx:0x%X000}%s", pos.x, pos.y, TTE_WHITE_PB, DECK_NAMES[deck]);
}
void print_deck_description(enum DeckType deck, BG_POINT pos)
+2 -2
View File
@@ -4,7 +4,7 @@
#include <stdlib.h>
static const char* s_font_point_lookup[] = {
static const char* FONT_POINT_LOOKUP[] = {
FP0_STR,
FP1_STR,
FP2_STR,
@@ -20,7 +20,7 @@ static const char* s_font_point_lookup[] = {
const char* get_font_point_str(int val)
{
val = abs(val) % 10;
return s_font_point_lookup[val];
return FONT_POINT_LOOKUP[val];
}
char digit_char_to_font_point(char digit_char)
+71 -53
View File
@@ -9,6 +9,7 @@
#include "game/blind_select.h"
#include "game/common_ui.h"
#include "game/game_over.h"
#include "game/joker_gallery.h"
#include "game/joker_row.h"
#include "game/main_menu.h"
#include "game/options_menu.h"
@@ -36,6 +37,7 @@
#include <maxmod.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define STRAIGHT_AND_FLUSH_SIZE_FOUR_FINGERS 4
#define STRAIGHT_AND_FLUSH_SIZE_DEFAULT 5
@@ -95,11 +97,15 @@ static StateMachine game_sm = STATE_MACHINE_DEFINE(state_info, GAME_STATE_MAX);
// Initialization of the global vars
// clang-format off
GameVariables g_game_vars = {
.timer = 0, .rng_info = {0, 0},
.timer = 0,
// Setting the seed to an invalid value so that the Run Setup screen knows we're not reusing a previous Run's seed
.rng_info = {UNDEFINED, 0},
.round = 0, .ante = 0, .money = 0, .hand_size = DEFAULT_HAND_SIZE,
.deck = DECK_TYPE_RED,
.best_hand_score = 0, .nb_played_hands = {0},
.current_blind = BLIND_TYPE_SMALL,
.next_boss_blind = BLIND_TYPE_BIG,
.blinds_states =
@@ -124,31 +130,31 @@ GameVariables g_game_vars = {
};
// clang-format on
static List _owned_jokers_list;
static List _discarded_jokers_list;
static List _expired_jokers_list;
static List s_owned_jokers_list;
static List s_discarded_jokers_list;
static List s_expired_jokers_list;
// Stacks
static Card* deck[MAX_DECK_SIZE] = {NULL};
static int deck_top = -1;
static Card* s_deck[MAX_DECK_SIZE] = {NULL};
static int s_deck_top = -1;
// Joker Special Variables
static int shortcut_joker_count = 0;
static int s_shortcut_joker_count = 0;
static int four_fingers_joker_count = 0;
static int s_four_fingers_joker_count = 0;
void deck_push(Card* card)
{
if (deck_top >= MAX_DECK_SIZE - 1)
if (s_deck_top >= MAX_DECK_SIZE - 1)
return;
deck[++deck_top] = card;
s_deck[++s_deck_top] = card;
}
Card* deck_pop(void)
{
if (deck_top < 0)
if (s_deck_top < 0)
return NULL;
return deck[deck_top--];
return s_deck[s_deck_top--];
}
void display_ante(void)
@@ -169,9 +175,9 @@ void game_init()
state_machine_remove(&game_sm);
state_machine_register(&game_sm);
// Initialize all jokers list once
_owned_jokers_list = list_init();
_discarded_jokers_list = list_init();
_expired_jokers_list = list_init();
s_owned_jokers_list = list_init();
s_discarded_jokers_list = list_init();
s_expired_jokers_list = list_init();
// TODO: Move this to an initialization of the play scoring states
game_shop_reset();
@@ -186,10 +192,15 @@ void game_init()
g_game_vars.ante = STARTING_ANTE;
g_game_vars.money = STARTING_MONEY;
g_game_vars.score = STARTING_SCORE;
g_game_vars.round = 0;
g_game_vars.chips = 0;
g_game_vars.mult = 0;
g_game_vars.round = STARTING_ROUND;
g_game_vars.best_hand_score = 0;
for (int i = 0; i < HAND_TYPE_MAX; i++)
g_game_vars.nb_played_hands[i] = 0;
// Initialize/reset unbeaten Boss/Showdown Blinds so they are all available
init_unbeaten_blinds_list(false);
init_unbeaten_blinds_list(true);
@@ -197,9 +208,9 @@ void game_init()
void game_reset()
{
while (list_get_len(&_owned_jokers_list) > 0)
while (list_get_len(&s_owned_jokers_list) > 0)
{
JokerObject* joker_object = list_get_at_idx(&_owned_jokers_list, 0);
JokerObject* joker_object = list_get_at_idx(&s_owned_jokers_list, 0);
remove_owned_joker(0);
joker_object_destroy(&joker_object);
}
@@ -212,9 +223,9 @@ void game_reset()
sprite_destroy(&g_game_vars.playing_blind_token);
sprite_destroy(&g_game_vars.round_end_blind_token);
list_clear(&_owned_jokers_list);
list_clear(&_discarded_jokers_list);
list_clear(&_expired_jokers_list);
list_clear(&s_owned_jokers_list);
list_clear(&s_discarded_jokers_list);
list_clear(&s_expired_jokers_list);
game_init();
@@ -233,20 +244,20 @@ void game_reset()
static inline void discarded_jokers_update_loop(void)
{
if (list_is_empty(&_discarded_jokers_list))
if (list_is_empty(&s_discarded_jokers_list))
{
return;
}
ListItr itr = list_itr_create(&_discarded_jokers_list);
ListItr itr = list_itr_create(&s_discarded_jokers_list);
JokerObject* joker_object;
while ((joker_object = list_itr_next(&itr)))
{
if (joker_object->sprite_object->x == joker_object->sprite_object->tx &&
joker_object->sprite_object->y == joker_object->sprite_object->ty)
if (joker_object->x == joker_object->tx && joker_object->y == joker_object->ty)
{
list_itr_remove_current_node(&itr);
// TODO: joker_object_dispose() instead?
joker_object_destroy(&joker_object);
}
}
@@ -254,7 +265,7 @@ static inline void discarded_jokers_update_loop(void)
static inline void held_jokers_update_loop(void)
{
static const int spacing_lut[MAX_JOKERS_HELD_SIZE][MAX_JOKERS_HELD_SIZE] = {
static const int SPACING_LUT[MAX_JOKERS_HELD_SIZE][MAX_JOKERS_HELD_SIZE] = {
{0, 0, 0, 0, 0 },
{13, -13, 0, 0, 0 },
{26, 0, -26, 0, 0 },
@@ -264,27 +275,33 @@ static inline void held_jokers_update_loop(void)
FIXED hand_x = int2fx(HELD_JOKERS_POS.x);
ListItr itr = list_itr_create(&_owned_jokers_list);
ListItr itr = list_itr_create(&s_owned_jokers_list);
JokerObject* joker;
int jokers_top = list_get_len(&_owned_jokers_list) - 1;
int jokers_top = list_get_len(&s_owned_jokers_list) - 1;
int i = 0;
while ((joker = list_itr_next(&itr)))
{
// Let the Shop handle the position of this Joker
if (joker != game_shop_get_description_card())
joker->sprite_object->tx = hand_x - int2fx(spacing_lut[jokers_top][i]);
joker->tx = hand_x - int2fx(SPACING_LUT[jokers_top][i]);
i++;
}
}
bool joker_object_can_acquire(Item* joker_object)
{
GBAL_RETURN_IF_NULL_RET(joker_object, false);
return (list_get_len(get_jokers_list()) < MAX_JOKERS_HELD_SIZE);
}
static inline void expired_jokers_update_loop(void)
{
if (list_is_empty(&_expired_jokers_list))
if (list_is_empty(&s_expired_jokers_list))
{
return;
}
ListItr itr = list_itr_create(&_expired_jokers_list);
ListItr itr = list_itr_create(&s_expired_jokers_list);
JokerObject* joker_object;
while ((joker_object = list_itr_next(&itr)))
@@ -294,7 +311,7 @@ static inline void expired_jokers_update_loop(void)
{
// get joker idx
int expired_joker_idx = 0;
ListItr joker_itr = list_itr_create(&_owned_jokers_list);
ListItr joker_itr = list_itr_create(&s_owned_jokers_list);
JokerObject* expired_joker;
while ((expired_joker = list_itr_next(&joker_itr)) && expired_joker != joker_object)
{
@@ -346,7 +363,7 @@ enum GameState game_get_state(void)
bool is_joker_owned(int joker_id)
{
ListItr itr = list_itr_create(&_owned_jokers_list);
ListItr itr = list_itr_create(&s_owned_jokers_list);
JokerObject* joker;
while ((joker = list_itr_next(&itr)))
@@ -361,71 +378,72 @@ bool is_joker_owned(int joker_id)
List* get_jokers_list(void)
{
return &_owned_jokers_list;
return &s_owned_jokers_list;
}
List* get_expired_jokers_list(void)
{
return &_expired_jokers_list;
return &s_expired_jokers_list;
}
List* get_discarded_jokers_list(void)
{
return &_discarded_jokers_list;
return &s_discarded_jokers_list;
}
bool is_shortcut_joker_active(void)
{
return shortcut_joker_count > 0;
return s_shortcut_joker_count > 0;
}
int get_straight_and_flush_size(void)
{
return four_fingers_joker_count > 0 ? STRAIGHT_AND_FLUSH_SIZE_FOUR_FINGERS
: STRAIGHT_AND_FLUSH_SIZE_DEFAULT;
return s_four_fingers_joker_count > 0 ? STRAIGHT_AND_FLUSH_SIZE_FOUR_FINGERS
: STRAIGHT_AND_FLUSH_SIZE_DEFAULT;
}
void add_joker(JokerObject* joker_object)
{
list_push_back(&_owned_jokers_list, joker_object);
list_push_back(&s_owned_jokers_list, joker_object);
// TODO: Extract to on_joker_added() callback
// In case the player gets multiple Four Fingers Jokers,
// only change size when the first one is added
if (joker_object->joker->id == FOUR_FINGERS_JOKER_ID)
{
four_fingers_joker_count++;
s_four_fingers_joker_count++;
}
if (joker_object->joker->id == SHORTCUT_JOKER_ID)
{
shortcut_joker_count++;
s_shortcut_joker_count++;
}
}
void remove_owned_joker(int owned_joker_idx)
{
// TODO: Extract to on_joker_removed() callback
JokerObject* joker_object = list_get_at_idx(&_owned_jokers_list, owned_joker_idx);
JokerObject* joker_object = list_get_at_idx(&s_owned_jokers_list, owned_joker_idx);
// In case the player gets multiple Four Fingers Jokers,
// and only reset the size when all of them have been removed
if (joker_object->joker->id == FOUR_FINGERS_JOKER_ID)
{
four_fingers_joker_count--;
s_four_fingers_joker_count--;
}
if (joker_object->joker->id == SHORTCUT_JOKER_ID)
{
shortcut_joker_count--;
s_shortcut_joker_count--;
}
game_shop_set_joker_avail(joker_object->joker->id, true);
list_remove_at_idx(&_owned_jokers_list, owned_joker_idx);
// TODO: Move to site of joker_destroy()?
joker_set_rollable(joker_object->joker->id, true);
list_remove_at_idx(&s_owned_jokers_list, owned_joker_idx);
}
int get_deck_top(void)
{
return deck_top;
return s_deck_top;
}
int get_num_discards_remaining(void)
@@ -618,23 +636,23 @@ void display_discards(void)
int deck_get_size(void)
{
return deck_top + 1;
return s_deck_top + 1;
}
static int deck_get_max_size(void)
{
// This is the max amount of cards that the player currently has in their possession
return get_hand_top() + get_played_top() + deck_top + get_discard_top() + 4;
return get_hand_top() + get_played_top() + s_deck_top + get_discard_top() + 4;
}
void deck_shuffle(void)
{
for (int i = deck_top; i > 0; i--)
for (int i = s_deck_top; i > 0; i--)
{
int j = rng_get_u32() % (i + 1);
Card* temp = deck[i];
deck[i] = deck[j];
deck[j] = temp;
Card* temp = s_deck[i];
s_deck[i] = s_deck[j];
s_deck[j] = temp;
}
}
+15 -15
View File
@@ -25,7 +25,7 @@ static const u32 BLIND_SELECT_BTN_SELECTED_BORDER_PID = 18;
static const u32 TM_DISP_BLIND_PANEL_FINISH = 7;
static const u32 TM_DISP_BLIND_PANEL_START = 1;
static int timer;
static int s_timer;
static void game_blind_select_start_anim_seq(void);
static void game_blind_select_handle_input(void);
@@ -97,11 +97,11 @@ static void game_blind_select_start_anim_seq()
);
}
if (timer == TM_END_ANIM_SEQ)
if (s_timer == TM_END_ANIM_SEQ)
{
game_blind_select_print_blinds_reqs_and_rewards();
state_machine_change_state(&blind_select_sm, BLIND_SELECT);
timer = TM_ZERO; // Reset the timer
s_timer = TM_ZERO; // Reset the timer
}
}
@@ -172,7 +172,7 @@ static inline void highlight_skip_button(void)
static void game_blind_select_handle_input()
{
if (timer == TM_BLIND_SELECT_START && g_game_vars.current_blind == BLIND_TYPE_BOSS)
if (s_timer == TM_BLIND_SELECT_START && g_game_vars.current_blind == BLIND_TYPE_BOSS)
{
selection_y = BLIND_ROW;
}
@@ -197,7 +197,7 @@ static void game_blind_select_handle_input()
case BLIND_ROW:
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
state_machine_change_state(&blind_select_sm, BLIND_SELECTED_ANIM_SEQ);
timer = TM_ZERO;
s_timer = TM_ZERO;
++g_game_vars.round;
display_round();
break;
@@ -230,7 +230,7 @@ static void game_blind_select_handle_input()
game_blind_select_print_blinds_reqs_and_rewards();
highlight_select_button();
timer = TM_ZERO;
s_timer = TM_ZERO;
}
break;
default:
@@ -241,7 +241,7 @@ static void game_blind_select_handle_input()
static void game_blind_select_selected_anim_seq()
{
if (timer < 15)
if (s_timer < 15)
{
Rect blinds_rect = POP_MENU_ANIM_RECT;
blinds_rect.top -= 1; // Because of the raised blind
@@ -256,28 +256,28 @@ static void game_blind_select_selected_anim_seq()
);
}
}
else if (timer >= MENU_POP_OUT_ANIM_FRAMES)
else if (s_timer >= MENU_POP_OUT_ANIM_FRAMES)
{
for (int i = 0; i < NUM_BLINDS_PER_ANTE; i++)
{
obj_hide(blind_select_tokens[i]->obj);
}
timer = TM_ZERO;
s_timer = TM_ZERO;
state_machine_change_state(&blind_select_sm, DISPLAY_BLIND_PANEL);
}
}
static void game_blind_select_display_blind_panel()
{
if (timer >= TM_DISP_BLIND_PANEL_FINISH)
if (s_timer >= TM_DISP_BLIND_PANEL_FINISH)
{
state_machine_change_state(&blind_select_sm, BLIND_SELECT_EXIT);
return;
}
// Switches to the selecting background and clears the blind panel area
if (timer == TM_DISP_BLIND_PANEL_START)
if (s_timer == TM_DISP_BLIND_PANEL_START)
{
change_background(BG_CARD_SELECTING, false);
@@ -295,9 +295,9 @@ static void game_blind_select_display_blind_panel()
}
// Shift the blind panel down onto screen
for (int y = 0; y < timer; y++)
for (int y = 0; y < s_timer; y++)
{
int y_from = 26 + y - timer;
int y_from = 26 + y - s_timer;
int y_to = 0 + y;
Rect from = {0, y_from, 8, y_from};
@@ -451,7 +451,7 @@ static void blind_tokens_init()
void game_blind_select_on_init(void)
{
timer = TM_ZERO;
s_timer = TM_ZERO;
state_machine_register(&blind_select_sm);
state_machine_change_state(&blind_select_sm, START_ANIM_SEQ);
@@ -472,7 +472,7 @@ void game_blind_select_on_init(void)
void game_blind_select_on_update(void)
{
timer++;
s_timer++;
}
void game_blind_select_on_exit(void)
+14 -5
View File
@@ -8,10 +8,11 @@
#include "game/round_end.h"
#include "game/run_setup.h"
#include "game/shop.h"
#include "layout.h"
typedef void (*BackgroundRenderCallback)(void);
static enum BackgroundId background = BG_NONE;
static enum BackgroundId s_background = BG_NONE;
// Map to fill in for refactor
static const BackgroundRenderCallback bgCallbacks[] = {
@@ -28,18 +29,26 @@ static const BackgroundRenderCallback bgCallbacks[] = {
enum BackgroundId get_current_background(void)
{
return background;
return s_background;
}
void change_background(enum BackgroundId id, bool force_redraw)
{
if (force_redraw)
{
background = BG_NONE;
s_background = BG_NONE;
}
if (id != background && bgCallbacks[id] != NULL)
if (id != s_background && bgCallbacks[id] != NULL)
{
bgCallbacks[id]();
}
background = id;
s_background = id;
}
void reset_top_left_panel_bottom_row(void)
{
BG_POINT top_left_panel_bottom_row_pos = TOP_LEFT_PANEL_POINT;
// Use the source rect height to offset to the bottom row point
top_left_panel_bottom_row_pos.y += rect_height(&TOP_LEFT_ITEM_SRC_RECT) - 1;
main_bg_se_copy_rect(TOP_LEFT_PANEL_BOTTOM_ROW_RESET_RECT, top_left_panel_bottom_row_pos);
}
+297 -75
View File
@@ -2,17 +2,30 @@
#include "affine_background.h"
#include "audio_utils.h"
#include "background_game_over_gfx.h"
#include "button.h"
#include "card.h"
#include "game.h"
#include "graphic_utils.h"
#include "hand.h"
#include "joker.h"
#include "layout.h"
#include "list.h"
#include "random.h"
#include "selection_grid.h"
#include "soundbank.h"
#include "timer.h"
#include "util.h"
#include <tonc.h>
#define RED_BTN_MAIN_COLOR_PAL_IDX 6
#define BLUE_BTN_MAIN_COLOR_PAL_IDX 7
#define REUSE_SEED_BTN_OUTLINE_PAL_IDX 8
#define NEW_RUN_BTN_OUTLINE_PAL_IDX 9
#define MAIN_MENU_BTN_OUTLINE_PAL_IDX 10
#define ENDLESS_BTN_OUTLINE_PAL_IDX 11
enum EndCondition
{
END_CONDITION_NONE,
@@ -21,52 +34,158 @@ enum EndCondition
};
// clang-format off
static const BG_POINT GAME_OVER_SRC_RECT_3X3_POS = { 25, 29};
static const Rect GAME_OVER_DIALOG_DEST_RECT = { 11, 21, 23, 28};
static const Rect GAME_OVER_ANIM_RECT = { 11, 8, 23, 28};
static const Rect GAME_LOSE_MSG_TEXT_RECT = {104, 72, UNDEFINED, UNDEFINED};
// 1 character to the right of GAME_LOSE
static const Rect GAME_WIN_MSG_TEXT_RECT = {112, 72, UNDEFINED, UNDEFINED};
static const BG_POINT NEW_RUN_BTN_DEST_POS = { 15, 26};
static const Rect NEW_RUN_BTN_SRC_RECT = { 0, 30, 4, 31};
static const Rect GAME_OVER_ANIM_RECT = { 3, 5, 26, 31};
static const Rect GAME_OVER_TEXT_TILES_SRC_RECT_1 = { 27, 24, 31, 25};
static const Rect GAME_OVER_TEXT_TILES_SRC_RECT_2 = { 27, 26, 31, 27};
static const BG_POINT GAME_OVER_TEXT_TILES_DEST_POS_1 = { 10, 21};
static const BG_POINT GAME_OVER_TEXT_TILES_DEST_POS_2 = { 15, 21};
static const Rect DEFEATED_BY_TEXT_SRC_RECT = { 28, 30, 31, 31};
static const BG_POINT DEFEATED_BY_TEXT_DEST_POS = { 22, 24};
static const BG_POINT DEFEATED_BY_TOKEN_INIT_POS = {180, 207};
static const Rect REUSE_SEED_BTN_OFF_SRC_RECT = { 28, 28, 29, 29};
static const Rect REUSE_SEED_BTN_ON_SRC_RECT = { 30, 28, 31, 29};
static const BG_POINT REUSE_SEED_BTN_DEST_POS = { 4, 11};
static const Rect BEST_HAND_SCORE_RECT = {126, 64, 166, 72};
static const Rect MOST_PLAYED_HAND_RECT = {112, 80, 168, 88};
static const BG_POINT SEED_VALUE_POS = {112, 96};
static const BG_POINT NEW_RUN_BTN_TEXT_POS = { 48, 112};
static const BG_POINT MAIN_MENU_BTN_TEXT_POS = {120, 112};
// clang-format on
static const u32 GAME_OVER_ANIM_FRAMES = 15;
static const u32 GAME_OVER_ANIM_FRAMES = 17;
static const mm_byte GAME_OVER_SFX_VOL = 178; // 70% of MM_SFX_FULL_VOLUME
static const mm_word GAME_OVER_WHOOSH_RATE = 922; // 90% of MM_BASE_PITCH_RATE
static enum EndCondition condition = END_CONDITION_NONE;
static u32 timer = TM_ZERO;
// Selection Grid
static void game_over_init(enum EndCondition init_condition)
enum GameOverRows
{
condition = init_condition;
// Clears the round end menu
main_bg_se_clear_rect(POP_MENU_ANIM_RECT);
main_bg_se_copy_expand_3x3_rect(GAME_OVER_DIALOG_DEST_RECT, GAME_OVER_SRC_RECT_3X3_POS);
main_bg_se_copy_rect(NEW_RUN_BTN_SRC_RECT, NEW_RUN_BTN_DEST_POS);
GAME_OVER_SEED_ROW,
GAME_OVER_RUN_MENU_ROW,
GAME_OVER_ROW_MAX
};
// Using the text color to match the Win/Loss condition
switch (condition)
#define GAME_OVER_SEED_BTN_COL 0
#define GAME_OVER_NEW_RUN_BTN_COL 0
#define GAME_OVER_MAIN_MENU_BTN_COL 1
static int game_over_get_row_size1(void)
{
return 1;
}
static int game_over_get_row_size2(void)
{
return 2;
}
static void game_over_on_key_transit(SelectionGrid* selection_grid, Selection* selection);
static bool game_over_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
);
static const SelectionGridRow game_over_selection_rows[] = {
{GAME_OVER_SEED_ROW, game_over_get_row_size1, game_over_on_selection_changed, game_over_on_key_transit, {.wrap = false}},
{GAME_OVER_RUN_MENU_ROW, game_over_get_row_size2, game_over_on_selection_changed, game_over_on_key_transit, {.wrap = false}}
};
static const Selection GAME_OVER_INIT_SEL = {0, GAME_OVER_RUN_MENU_ROW};
static SelectionGrid game_over_selection_grid = {
game_over_selection_rows,
GAME_OVER_ROW_MAX,
GAME_OVER_INIT_SEL
};
// Buttons
static void reuse_seed_on_pressed(void);
static void new_run_on_pressed(void);
static void main_menu_on_pressed(void);
static Button* game_over_get_button_from_sel(const Selection* selection);
// clang-format off
static Button game_over_buttons[GAME_OVER_ROW_MAX][2] = {
// GAME_OVER_SEED_ROW
{
case END_CONDITION_WIN:
affine_background_set_color(TEXT_CLR_BLUE);
break;
case END_CONDITION_LOSS:
affine_background_set_color(TEXT_CLR_RED);
break;
default:
break;
{ REUSE_SEED_BTN_OUTLINE_PAL_IDX, RED_BTN_MAIN_COLOR_PAL_IDX, reuse_seed_on_pressed, NULL }
},
// GAME_OVER_RUN_MENU_ROW
{
{ NEW_RUN_BTN_OUTLINE_PAL_IDX, RED_BTN_MAIN_COLOR_PAL_IDX, new_run_on_pressed, NULL },
{ MAIN_MENU_BTN_OUTLINE_PAL_IDX, RED_BTN_MAIN_COLOR_PAL_IDX, main_menu_on_pressed, NULL }
}
};
// clang-format on
// Internal Variables
static enum EndCondition s_end_condition = END_CONDITION_NONE;
static u32 s_timer = TM_ZERO;
static bool s_reuse_seed = false;
static bool s_continue_run = false;
static void game_over_common_init(enum EndCondition init_condition)
{
s_end_condition = init_condition;
s_timer = TM_ZERO;
s_reuse_seed = false;
s_continue_run = false;
// Hide all Joker sprites
JokerObject* joker_object = NULL;
ListItr itr = list_itr_create(get_jokers_list());
while ((joker_object = list_itr_next(&itr)))
{
obj_hide(joker_object->sprite->obj);
}
timer = TM_ZERO;
// Destroy any preexisting cards in the deck, which will be present if we restart a run
Card* card = NULL;
while (get_deck_top() >= 0)
{
card = deck_pop();
card_destroy(&card);
}
// Clears the round end menu
toggle_windows(false, false);
GRIT_CPY(pal_bg_mem, background_game_over_gfxPal);
GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_game_over_gfxTiles);
GRIT_CPY(&se_mem[MAIN_BG_SBB], background_game_over_gfxMap);
// Move blind token offscreen at initial position before moving it up
sprite_position(
g_game_vars.playing_blind_token,
DEFEATED_BY_TOKEN_INIT_POS.x,
DEFEATED_BY_TOKEN_INIT_POS.y
);
// Highlight New Run button
game_over_selection_grid.selection = GAME_OVER_INIT_SEL;
button_set_highlight(&game_over_buttons[GAME_OVER_SEED_ROW][GAME_OVER_SEED_BTN_COL], false);
button_set_highlight(
&game_over_buttons[GAME_OVER_RUN_MENU_ROW][GAME_OVER_NEW_RUN_BTN_COL],
true
);
button_set_highlight(
&game_over_buttons[GAME_OVER_RUN_MENU_ROW][GAME_OVER_MAIN_MENU_BTN_COL],
false
);
}
void game_win_on_init(void)
{
play_sfx(SFX_GAME_WIN, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
game_over_init(END_CONDITION_WIN);
game_over_common_init(END_CONDITION_WIN);
affine_background_set_color(TEXT_CLR_BLUE);
}
void game_lose_on_init(void)
@@ -76,67 +195,170 @@ void game_lose_on_init(void)
play_lose_music();
game_over_init(END_CONDITION_LOSS);
}
game_over_common_init(END_CONDITION_LOSS);
affine_background_set_color(TEXT_CLR_RED);
/**
* @brief Makes the message background frame move 1 tile up, called until it is in its place.
*/
static inline void game_over_anim_frame(void)
{
main_bg_se_move_rect_1_tile_vert(GAME_OVER_ANIM_RECT, SCREEN_UP);
}
// Change top text to "GAME OVER"
main_bg_se_copy_rect(GAME_OVER_TEXT_TILES_SRC_RECT_1, GAME_OVER_TEXT_TILES_DEST_POS_1);
main_bg_se_copy_rect(GAME_OVER_TEXT_TILES_SRC_RECT_2, GAME_OVER_TEXT_TILES_DEST_POS_2);
/**
* @brief Polls user input to detect "Retry" button press.
*/
static inline void game_over_process_user_input()
{
if (key_hit(SELECT_CARD))
{
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
game_change_state(GAME_STATE_BLIND_SELECT);
}
// Change blind frame text
main_bg_se_copy_rect(DEFEATED_BY_TEXT_SRC_RECT, DEFEATED_BY_TEXT_DEST_POS);
}
void game_over_on_update(void)
{
timer++;
if (timer < GAME_OVER_ANIM_FRAMES)
s_timer++;
// Need to clear text here or the number of cards remaining in deck stays for some reason
if (s_timer == 1)
tte_erase_screen();
// Move whole panel and Blind Token sprite up by a tile each frame
if (s_timer < GAME_OVER_ANIM_FRAMES)
{
game_over_anim_frame();
main_bg_se_move_rect_1_tile_vert(GAME_OVER_ANIM_RECT, SCREEN_UP);
sprite_position(
g_game_vars.playing_blind_token,
g_game_vars.playing_blind_token->pos.x,
g_game_vars.playing_blind_token->pos.y - TILE_SIZE
);
}
else if (timer == GAME_OVER_ANIM_FRAMES)
// Print values and buttons text
else if (s_timer == GAME_OVER_ANIM_FRAMES)
{
switch (condition)
char best_hand_str[UINT_MAX_DIGITS + 1];
truncate_uint_to_suffixed_str(
g_game_vars.best_hand_score,
rect_width(&BEST_HAND_SCORE_RECT) / TTE_CHAR_SIZE,
best_hand_str
);
Rect best_hand_rect = BEST_HAND_SCORE_RECT;
update_text_rect_to_center_str(&best_hand_rect, best_hand_str, SCREEN_LEFT);
tte_printf(
"#{P:%d,%d; cx:0x%X000}%s",
best_hand_rect.left,
best_hand_rect.top,
TTE_RED_PB,
best_hand_str
);
// Most played hand
enum HandType most_played_hand = HIGH_CARD;
for (enum HandType hand_type = PAIR; hand_type <= HAND_TYPE_MAX; hand_type++)
{
case END_CONDITION_WIN:
tte_printf(
"#{P:%d,%d; cx:0x%X000}YOU WIN",
GAME_WIN_MSG_TEXT_RECT.left,
GAME_WIN_MSG_TEXT_RECT.top,
TTE_BLUE_PB
);
break;
case END_CONDITION_LOSS:
tte_printf(
"#{P:%d,%d; cx:0x%X000}GAME OVER",
GAME_LOSE_MSG_TEXT_RECT.left,
GAME_LOSE_MSG_TEXT_RECT.top,
TTE_RED_PB
);
break;
default:
break;
if (g_game_vars.nb_played_hands[hand_type - 1] >
g_game_vars.nb_played_hands[most_played_hand - 1])
most_played_hand = hand_type;
}
const char* hand_name_str = get_hand_type_name(most_played_hand);
Rect hand_type_rect = MOST_PLAYED_HAND_RECT;
update_text_rect_to_center_str(&hand_type_rect, hand_name_str, SCREEN_LEFT);
tte_printf(
"#{P:%d,%d; cx:0x%X000}%s",
hand_type_rect.left,
hand_type_rect.top,
TTE_WHITE_PB,
hand_name_str
);
// Run's seed
char seed_str[BASE36_MAX_DIGITS + 1] = {'\0'};
u32_to_base36(g_game_vars.rng_info.seed, seed_str);
tte_printf(
"#{P:%d,%d; cx:0x%X000}%s",
SEED_VALUE_POS.x,
SEED_VALUE_POS.y,
TTE_WHITE_PB,
seed_str
);
// Buttons' text
tte_printf(
"#{P:%d,%d; cx:0x%X000}New Run",
NEW_RUN_BTN_TEXT_POS.x,
NEW_RUN_BTN_TEXT_POS.y,
TTE_WHITE_PB
);
tte_printf(
"#{P:%d,%d; cx:0x%X000}Main Menu",
MAIN_MENU_BTN_TEXT_POS.x,
MAIN_MENU_BTN_TEXT_POS.y,
TTE_WHITE_PB
);
}
game_over_process_user_input();
if (s_timer >= GAME_OVER_ANIM_FRAMES)
selection_grid_process_input(&game_over_selection_grid);
}
void game_over_on_exit(void)
{
toggle_windows(false, false);
play_regular_music();
condition = END_CONDITION_NONE;
game_reset();
rng_shuffle_seed();
s_end_condition = END_CONDITION_NONE;
u32 previous_seed = g_game_vars.rng_info.seed;
if (!s_continue_run)
game_reset();
if (s_reuse_seed)
rng_set_seed(previous_seed);
else
g_game_vars.rng_info.seed = UNDEFINED;
}
// SelectionGrid Implementation
static Button* game_over_get_button_from_sel(const Selection* selection)
{
return &game_over_buttons[selection->y][selection->x];
}
static void game_over_on_key_transit(SelectionGrid* selection_grid, Selection* selection)
{
if (key_hit(SELECT_CARD))
button_press(&game_over_buttons[selection->y][selection->x]);
}
static bool game_over_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
)
{
if (row_idx == prev_selection->y)
button_set_highlight(game_over_get_button_from_sel(prev_selection), false);
if (row_idx == new_selection->y)
button_set_highlight(game_over_get_button_from_sel(new_selection), true);
return true;
}
// Buttons
static void reuse_seed_on_pressed(void)
{
s_reuse_seed = !s_reuse_seed;
main_bg_se_copy_rect(
s_reuse_seed ? REUSE_SEED_BTN_ON_SRC_RECT : REUSE_SEED_BTN_OFF_SRC_RECT,
REUSE_SEED_BTN_DEST_POS
);
}
static void new_run_on_pressed(void)
{
game_change_state(GAME_STATE_RUN_SETUP);
}
static void main_menu_on_pressed(void)
{
game_change_state(GAME_STATE_MAIN_MENU);
}
+146
View File
@@ -0,0 +1,146 @@
#include "game/joker_gallery.h"
#include "affine_background.h"
#include "game.h"
#include "game/common_ui.h"
#include "graphic_utils.h"
#include "joker.h"
#include <string.h>
#include <tonc.h>
#define SHOP_DESC_RARITY_MAIN_COLOR_PAL_IDX 27
#define SHOP_DESC_RARITY_SHADOW_COLOR_PAL_IDX 28
// clang-format off
static const Rect VISIBLE_SCREEN_TILE_RECT = { 0, 0, 29, 19};
static const BG_POINT CLEAR_TILE_SOURCE = {30, 1};
static const Rect CARD_DESC_PANEL_RECT = { 9, 6, 28, 18};
static const NinePatchRect CARD_DESC_PANEL_SOURCE = {
.patch_rect = {27, 25, 31, 31},
.margins = { 2, 3, 2, 3}
};
static const int CARD_DESC_MAX_TEXT_HEIGHT = CARD_DESC_PANEL_RECT.bottom -
CARD_DESC_PANEL_RECT.top + 1 -
CARD_DESC_PANEL_SOURCE.margins.top -
CARD_DESC_PANEL_SOURCE.margins.bottom;
static const Rect CARD_DESC_TEXT_RECT = {11, 9, 26, 18};
static const Rect CARD_NAME_TEXT_RECT = {10, 7, 27, 7};
static const BG_POINT JOKER_SPRITE_POS = {135, 9};
// clang-format on
static JokerObject* s_gallery_joker = NULL;
static size_t s_joker_index = 0;
static void game_joker_gallery_render(void)
{
size_t joker_count = get_joker_registry_size();
const JokerInfo* info = get_joker_registry_entry(s_joker_index);
tte_erase_screen();
main_bg_se_copy_expand_tile(VISIBLE_SCREEN_TILE_RECT, CLEAR_TILE_SOURCE);
if (joker_count == 0 || info == NULL)
{
return;
}
s_gallery_joker = joker_object_new(joker_new(s_joker_index));
if (s_gallery_joker == NULL)
{
return;
}
sprite_object_position((SpriteObject*)s_gallery_joker, JOKER_SPRITE_POS.x, JOKER_SPRITE_POS.y);
int desc_bottom_offset = CARD_DESC_MAX_TEXT_HEIGHT -
info->joker_print_desc(s_gallery_joker->joker, CARD_DESC_TEXT_RECT);
desc_bottom_offset = max(0, desc_bottom_offset);
pal_bg_mem[SHOP_DESC_RARITY_MAIN_COLOR_PAL_IDX] = joker_get_rarity_color(info->rarity, true);
pal_bg_mem[SHOP_DESC_RARITY_SHADOW_COLOR_PAL_IDX] = joker_get_rarity_color(info->rarity, false);
Rect panel_rect = CARD_DESC_PANEL_RECT;
panel_rect.bottom -= desc_bottom_offset;
main_bg_se_copy_expand_9_patch(panel_rect, &CARD_DESC_PANEL_SOURCE);
tte_printf(
TTE_WHITE_TAG "#{P:%d,%d}%*s%s",
CARD_NAME_TEXT_RECT.left * TILE_SIZE,
CARD_NAME_TEXT_RECT.top * TILE_SIZE,
(rect_width(&CARD_NAME_TEXT_RECT) - strlen(info->name)) / 2,
"",
info->name
);
const char* rarity = joker_get_rarity_string(info->rarity);
tte_printf(
TTE_WHITE_TAG "#{P:%d,%d}%*s%s",
CARD_DESC_TEXT_RECT.left * TILE_SIZE,
(CARD_DESC_TEXT_RECT.bottom - desc_bottom_offset - 1) * TILE_SIZE,
(rect_width(&CARD_DESC_TEXT_RECT) - strlen(rarity)) / 2,
"",
rarity
);
tte_printf(TTE_WHITE_TAG "#{P:8,16}JOKER\n#{P:8,24}GALLERY");
tte_printf(TTE_YELLOW_TAG "#{P:8,48}%02d/%02d", (int)s_joker_index + 1, (int)joker_count);
tte_printf(TTE_WHITE_TAG "#{P:8,80}L PREV\n#{P:8,96}R NEXT\n#{P:8,120}B BACK");
}
static void game_joker_gallery_change_page(int direction)
{
size_t joker_count = get_joker_registry_size();
if (joker_count == 0)
{
return;
}
joker_object_destroy(&s_gallery_joker);
if (direction < 0)
{
s_joker_index = (s_joker_index + joker_count - 1) % joker_count;
}
else
{
s_joker_index = (s_joker_index + 1) % joker_count;
}
game_joker_gallery_render();
}
void game_joker_gallery_on_init(void)
{
affine_background_change_background(AFFINE_BG_MAIN_MENU);
change_background(BG_SHOP, true);
toggle_windows(false, false);
tte_colors_setup();
if (s_joker_index >= get_joker_registry_size())
{
s_joker_index = 0;
}
game_joker_gallery_render();
}
void game_joker_gallery_on_update(void)
{
if (key_hit(KEY_LEFT | KEY_L))
{
game_joker_gallery_change_page(-1);
}
else if (key_hit(KEY_RIGHT | KEY_R | KEY_A))
{
game_joker_gallery_change_page(1);
}
else if (key_hit(KEY_B))
{
game_change_state(GAME_STATE_MAIN_MENU);
}
}
void game_joker_gallery_on_exit(void)
{
joker_object_destroy(&s_gallery_joker);
tte_erase_screen();
}
+9 -9
View File
@@ -33,8 +33,8 @@ bool jokers_sel_row_on_selection_changed(
// Don't change focus from current Joker if swapping
if (joker_object != NULL && !swapping)
{
sprite_object_erase_text_under(joker_object->sprite_object);
sprite_object_set_focus(joker_object->sprite_object, false);
sprite_object_erase_text_under((SpriteObject*)joker_object);
sprite_object_set_focus((SpriteObject*)joker_object, false);
}
}
@@ -46,7 +46,7 @@ bool jokers_sel_row_on_selection_changed(
{
if (!swapping)
{
sprite_object_set_focus(joker_object->sprite_object, true);
sprite_object_set_focus((SpriteObject*)joker_object, true);
}
// If we land on this row while the A button is being held, we are in swapping mode
// This means that we need to hide the price, whether we were already
@@ -54,7 +54,7 @@ bool jokers_sel_row_on_selection_changed(
if (!key_is_down(SELECT_CARD))
{
sprite_object_print_price_under(
joker_object->sprite_object,
(SpriteObject*)joker_object,
joker_get_sell_value(joker_object->joker)
);
}
@@ -75,8 +75,8 @@ bool jokers_sel_row_on_selection_changed(
static inline void joker_start_discard_animation(JokerObject* joker_object)
{
joker_object->sprite_object->tx = int2fx(JOKER_DISCARD_TARGET.x);
joker_object->sprite_object->ty = int2fx(JOKER_DISCARD_TARGET.y);
joker_object->tx = int2fx(JOKER_DISCARD_TARGET.x);
joker_object->ty = int2fx(JOKER_DISCARD_TARGET.y);
list_push_back(get_discarded_jokers_list(), joker_object);
}
@@ -90,7 +90,7 @@ static inline void game_sell_joker(int joker_idx)
JokerObject* joker_object = (JokerObject*)list_get_at_idx(owned_jokers_list, joker_idx);
g_game_vars.money += joker_get_sell_value(joker_object->joker);
display_money();
sprite_object_erase_text_under(joker_object->sprite_object);
sprite_object_erase_text_under((SpriteObject*)joker_object);
remove_owned_joker(joker_idx);
@@ -104,12 +104,12 @@ void jokers_sel_row_on_key_transit(SelectionGrid* selection_grid, Selection* sel
{
if (key_hit(SELECT_CARD))
{
sprite_object_erase_text_under(joker_object->sprite_object);
sprite_object_erase_text_under((SpriteObject*)joker_object);
}
else if (key_released(SELECT_CARD))
{
sprite_object_print_price_under(
joker_object->sprite_object,
(SpriteObject*)joker_object,
joker_get_sell_value(joker_object->joker)
);
}
+29 -12
View File
@@ -78,10 +78,11 @@ static SelectionGrid main_menu_selection_grid = {
// clang-format on
// Main menu sprite - the ace of spades
static CardObject* main_menu_ace = NULL;
static CardObject* s_main_menu_ace = NULL;
// Keep track of last highlighted button
static enum MainButtons last_highlighted_button = PLAY_BTN_IDX;
static enum MainButtons s_last_highlighted_button = PLAY_BTN_IDX;
static bool s_gallery_prompt_drawn = false;
void game_main_menu_change_background(void)
{
@@ -103,15 +104,17 @@ void game_main_menu_on_init(void)
{
affine_background_change_background(AFFINE_BG_MAIN_MENU);
change_background(BG_MAIN_MENU, true);
main_menu_ace = card_object_new(card_new(SPADES, ACE));
card_object_set_sprite(main_menu_ace, 0);
main_menu_ace->sprite_object->sprite->obj->attr0 |= ATTR0_AFF_DBL;
main_menu_ace->sprite_object->tscale = float2fx(0.8f);
sprite_object_position(main_menu_ace->sprite_object, MAIN_MENU_ACE_T_X, MAIN_MENU_ACE_T_Y);
s_gallery_prompt_drawn = false;
s_main_menu_ace = card_object_new(card_new(SPADES, ACE));
card_object_set_sprite(s_main_menu_ace, 0);
// TODO: NULL-check sprite
s_main_menu_ace->sprite->obj->attr0 |= ATTR0_AFF_DBL;
s_main_menu_ace->tscale = float2fx(0.8f);
sprite_object_position((SpriteObject*)s_main_menu_ace, MAIN_MENU_ACE_T_X, MAIN_MENU_ACE_T_Y);
// Select last highlighted button, Play button by default.
// e.g. if we return from the options menu, we want the Options button to be highlighted.
Selection sel_init = {last_highlighted_button, 0};
Selection sel_init = {s_last_highlighted_button, 0};
main_menu_selection_grid.selection = sel_init;
// Highlight current button
@@ -120,7 +123,14 @@ void game_main_menu_on_init(void)
void game_main_menu_on_update(void)
{
main_menu_ace->sprite_object->trotation = lu_sin((g_game_vars.timer << 8) / 2) / 3;
s_main_menu_ace->trotation = lu_sin((g_game_vars.timer << 8) / 2) / 3;
// The splash state clears TTE after transitioning, so draw this on the first menu update.
if (!s_gallery_prompt_drawn)
{
tte_printf(TTE_WHITE_TAG "#{P:128,152}START: JOKERS");
s_gallery_prompt_drawn = true;
}
selection_grid_process_input(&main_menu_selection_grid);
}
@@ -128,12 +138,12 @@ void game_main_menu_on_update(void)
void game_main_menu_on_exit(void)
{
// Save selected button
last_highlighted_button = main_menu_selection_grid.selection.x;
s_last_highlighted_button = main_menu_selection_grid.selection.x;
// Normally I would just cache these and hide/unhide but I didn't feel like dealing with
// defining a layer for it
card_destroy(&main_menu_ace->card);
card_object_destroy(&main_menu_ace);
card_destroy(&s_main_menu_ace->card);
card_object_destroy(&s_main_menu_ace);
}
// Implement SelectionGrid handler functions
@@ -185,6 +195,7 @@ static void main_menu_on_key_transit(SelectionGrid* selection_grid, Selection* s
else
{
tte_erase_rect_wrapper(GBALATRO_VERSION_TEXT_RECT);
s_gallery_prompt_drawn = false;
}
}
@@ -195,6 +206,12 @@ static void main_menu_on_key_transit(SelectionGrid* selection_grid, Selection* s
combo_pressed = false;
}
if ((key_hit(KEY_START) || key_hit(KEY_SELECT)) && !key_is_down(KEY_L) && !key_is_down(KEY_R))
{
game_change_state(GAME_STATE_JOKER_GALLERY);
return;
}
if (key_hit(SELECT_CARD))
{
button_press(&main_menu_buttons[selection->x]);
+245 -243
View File
File diff suppressed because it is too large Load Diff
+64 -26
View File
@@ -18,6 +18,7 @@
#include "state_machine.h"
#include "util.h"
#include <string.h>
#include <tonc.h>
// Palette Indices
@@ -29,13 +30,13 @@
#define BLUE_DISABLED_BTN_MAIN_COLOR_PAL_IDX 13
#define BACK_BTN_MAIN_COLOR_PAL_IDX 14
#define NEW_RUN_BTN_OUTLINE_COLOR_PAL_IDX 30
#define RESUME_BTN_OUTLINE_COLOR_PAL_IDX 31
#define CHANGE_DECK_BTN_OUTINE_COLOR_PAL_IDX 32
#define SEED_CHECK_BTN_OUTLINE_COLOR_PAL_IDX 33
#define SEED_DECK_BTN_OUTLINE_COLOR_PAL_IDX 34
#define PLAY_BTN_OUTLINE_COLOR_PAL_IDX 35
#define BACK_BTN_OUTLINE_COLOR_PAL_IDX 36
#define NEW_RUN_BTN_OUTLINE_COLOR_PAL_IDX 30
#define RESUME_BTN_OUTLINE_COLOR_PAL_IDX 31
#define CHANGE_DECK_BTN_OUTLINE_COLOR_PAL_IDX 32
#define SEED_CHECK_BTN_OUTLINE_COLOR_PAL_IDX 33
#define SEED_DECK_BTN_OUTLINE_COLOR_PAL_IDX 34
#define PLAY_BTN_OUTLINE_COLOR_PAL_IDX 35
#define BACK_BTN_OUTLINE_COLOR_PAL_IDX 36
#define KEYBOARD_1_BTN_OUTLINE_COLOR_PAL_IDX 40
#define KEYBOARD_2_BTN_OUTLINE_COLOR_PAL_IDX 41
@@ -292,7 +293,7 @@ static SelectionGrid choose_deck_selection_grid = {
static void change_deck_on_pressed(void);
static Button change_deck_button = {
CHANGE_DECK_BTN_OUTINE_COLOR_PAL_IDX,
CHANGE_DECK_BTN_OUTLINE_COLOR_PAL_IDX,
CHANGE_DECK_BTN_MAIN_COLOR_PAL_IDX,
change_deck_on_pressed,
NULL
@@ -527,7 +528,7 @@ static Button choose_seed_bottom_buttons[2] = {
}
};
static const char keyboard_buttons_to_char[KEYBOARD_HEIGHT * KEYBOARD_WIDTH] = {
static const char KEYBOARD_BUTTONS_TO_CHAR[KEYBOARD_HEIGHT * KEYBOARD_WIDTH] = {
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
@@ -536,8 +537,8 @@ static const char keyboard_buttons_to_char[KEYBOARD_HEIGHT * KEYBOARD_WIDTH] = {
// clang-format on
// Size BASE36_MAX_DIGITS + 1 to always have '\0' at the end
static char seed_str[BASE36_MAX_DIGITS + 1] = {'\0'};
static u8 seed_cursor_pos = 0;
static char s_seed_str[BASE36_MAX_DIGITS + 1] = {'\0'};
static u8 s_seed_cursor_pos = 0;
#pragma endregion
@@ -566,6 +567,7 @@ enum RunSetupResumeRows
void game_run_setup_change_background(void)
{
tte_erase_screen();
GRIT_CPY(pal_bg_mem, background_run_setup_gfxPal);
GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_run_setup_gfxTiles);
GRIT_CPY(&se_mem[MAIN_BG_SBB], background_run_setup_gfxMap);
@@ -576,6 +578,22 @@ void game_run_setup_on_init(void)
state_machine_register(&run_setup_sm);
game_run_setup_change_background();
// Apply the current use_seed value if seed is UNDEFINED
if (g_game_vars.rng_info.seed == UNDEFINED)
{
// Make the string empty instead of showing all zeroes
memset(s_seed_str, '\0', BASE36_MAX_DIGITS + 1);
s_seed_cursor_pos = 0;
}
// Or use previous Run's seed if it hasn't been reset
else
{
u32_to_base36(g_game_vars.rng_info.seed, s_seed_str);
s_seed_cursor_pos = BASE36_MAX_DIGITS;
use_seed = true;
}
toggle_seed_enabled(use_seed);
// Rank doesn't matter, won't see it
run_setup_deck = card_object_new(card_new(SPADES, ACE));
@@ -584,7 +602,7 @@ void game_run_setup_on_init(void)
card_object_set_sprite_face_down(run_setup_deck, g_game_vars.deck, 0);
sprite_object_position(
run_setup_deck->sprite_object,
(SpriteObject*)run_setup_deck,
RUN_SETUP_DECK_SPRITE_T_X,
RUN_SETUP_DECK_SPRITE_T_Y
);
@@ -632,7 +650,13 @@ void game_run_setup_on_exit(void)
static void choose_deck_substate_init(void)
{
// Show Deck sprite, name and TODO: description
obj_unhide(run_setup_deck->sprite_object->sprite->obj, ATTR0_AFF);
{
Sprite* deck_sprite = sprite_object_get_sprite((SpriteObject*)run_setup_deck);
if (deck_sprite != NULL)
{
obj_unhide(deck_sprite->obj, ATTR0_AFF);
}
}
print_deck_name(g_game_vars.deck, RUN_SETUP_DECK_NAME_TEXT_POS);
print_deck_description(g_game_vars.deck, RUN_SETUP_DECK_DESC_TEXT_POS);
@@ -660,9 +684,9 @@ static void choose_deck_substate_init(void)
// Set button highlights
button_set_highlight(&change_deck_button, true);
button_set_highlight(&choose_deck_bottom_buttons[RUN_SETUP_DECK_BB_USE_SEED], false);
toggle_seed_enabled(use_seed); // This just re-applies the current value
button_set_highlight(&choose_deck_bottom_buttons[RUN_SETUP_DECK_BB_PLAY], false);
button_set_highlight(&back_button, false);
toggle_seed_enabled(use_seed);
// Print button text
tte_printf(
@@ -784,7 +808,7 @@ static inline void update_seed_text(void)
RUN_SETUP_SEED_FIELD_TEXT_POS.y,
TTE_BLACK_PB,
BASE36_MAX_DIGITS + 1,
seed_str
s_seed_str
);
}
@@ -797,7 +821,13 @@ static void seed_keyboard_substate_init(void)
tte_erase_rect_wrapper(RUN_SETUP_DECK_NAME_DESC_RECT);
// Hide Deck card sprite
obj_hide(run_setup_deck->sprite_object->sprite->obj);
{
Sprite* deck_sprite = sprite_object_get_sprite((SpriteObject*)run_setup_deck);
if (deck_sprite != NULL)
{
obj_hide(deck_sprite->obj);
}
}
// Clean deck swap screen with frame BG color
main_bg_se_copy_expand_tile(
@@ -890,9 +920,9 @@ static inline void reroll_seed_str(void)
rng_shuffle_seed();
u32 new_seed = rng_get_u32();
rng_set_seed(new_seed);
u32_to_base36(new_seed, seed_str);
u32_to_base36(new_seed, s_seed_str);
update_seed_text();
seed_cursor_pos = BASE36_MAX_DIGITS;
s_seed_cursor_pos = BASE36_MAX_DIGITS;
}
/**
@@ -900,10 +930,10 @@ static inline void reroll_seed_str(void)
*/
static inline void delete_seed_char(void)
{
if (seed_cursor_pos == 0)
if (s_seed_cursor_pos == 0)
return;
seed_str[--seed_cursor_pos] = '\0';
s_seed_str[--s_seed_cursor_pos] = '\0';
update_seed_text();
}
@@ -914,10 +944,10 @@ static inline void delete_seed_char(void)
*/
static inline void type_seed_char(enum RunSetupKeyboardButtons key)
{
if (seed_cursor_pos >= BASE36_MAX_DIGITS)
if (s_seed_cursor_pos >= BASE36_MAX_DIGITS)
return;
seed_str[seed_cursor_pos++] = keyboard_buttons_to_char[key];
s_seed_str[s_seed_cursor_pos++] = KEYBOARD_BUTTONS_TO_CHAR[key];
update_seed_text();
}
@@ -946,8 +976,8 @@ static void keyboard_button_on_pressed(void)
// The cursor position is unsigned so always positive, but we still need to
// ensure it doersn't go out of bounds by more than 1 so that we can always
// substract 1 from it when erasing a character from the seed string.
if (seed_cursor_pos > BASE36_MAX_DIGITS)
seed_cursor_pos = BASE36_MAX_DIGITS;
if (s_seed_cursor_pos > BASE36_MAX_DIGITS)
s_seed_cursor_pos = BASE36_MAX_DIGITS;
if (key_hit(DESELECT_CARDS))
delete_seed_char();
@@ -1092,7 +1122,13 @@ static void resume_substate_init(void)
tab_set_highlight(RUN_SETUP_TAB_RESUME);
// Show Deck card sprite
obj_unhide(run_setup_deck->sprite_object->sprite->obj, ATTR0_AFF);
{
Sprite* deck_sprite = sprite_object_get_sprite((SpriteObject*)run_setup_deck);
if (deck_sprite != NULL)
{
obj_unhide(deck_sprite->obj, ATTR0_AFF);
}
}
}
// COMMON BUTTONS
@@ -1157,10 +1193,12 @@ static void play_on_pressed(void)
{
// Apply provided Seed if enabled
if (use_seed)
rng_set_seed(base36_to_u32(seed_str));
rng_set_seed(base36_to_u32(s_seed_str));
else
rng_shuffle_seed();
use_seed = false;
game_change_state(GAME_STATE_GAME_START);
}
+159 -270
View File
@@ -17,6 +17,7 @@
#include "joker.h"
#include "layout.h"
#include "list.h"
#include "mgba_logger.h"
#include "random.h"
#include "save.h"
#include "soundbank.h"
@@ -87,8 +88,7 @@ static const Rect SHOP_PRICES_TEXT_RECT = { 72, 56, 192, 160 };
static const Rect SHOP_REROLL_RECT = { 88, 96, UNDEFINED, UNDEFINED };
// clang-format on
static List s_shop_jokers_list = LIST_DEFAULT;
BITSET_DEFINE(s_avail_jokers_bitset, MAX_DEFINABLE_JOKERS)
static List s_shop_items_list = LIST_DEFAULT;
enum GameShopStates
{
@@ -165,45 +165,28 @@ static Button reroll_button = {
// Shop internal variables
static int timer;
static int s_timer;
static int reroll_cost = REROLL_BASE_COST;
static int s_reroll_cost = REROLL_BASE_COST;
// Variables relative to the Card we are showing the description of
static JokerObject* description_card = NULL;
static FIXED description_card_original_x_pos = UNDEFINED;
static FIXED description_card_original_y_pos = UNDEFINED;
static List* description_card_original_list = NULL;
// TODO: Change this to item once it has description printing API.
static JokerObject* s_description_card = NULL;
static FIXED s_description_card_original_x_pos = UNDEFINED;
static FIXED s_description_card_original_y_pos = UNDEFINED;
static List* s_description_card_original_list = NULL;
JokerObject* game_shop_get_description_card(void)
{
return description_card;
}
static inline void reset_shop_jokers(void)
{
int num_jokers = get_joker_registry_size();
bitset_clear(&s_avail_jokers_bitset);
for (int i = 0; i < num_jokers; i++)
{
bitset_set_idx(&s_avail_jokers_bitset, i, true);
}
return s_description_card;
}
void game_shop_reset(void)
{
list_clear(&s_shop_jokers_list);
s_shop_jokers_list = list_init();
reset_shop_jokers();
}
/**
* @brief Set whether a Joker can appear in the shop.
*/
void game_shop_set_joker_avail(int joker_id, bool avail)
{
bitset_set_idx(&s_avail_jokers_bitset, joker_id, avail);
list_clear(&s_shop_items_list);
s_shop_items_list = list_init();
joker_reset_rollable_jokers();
}
void game_shop_change_background(void)
@@ -234,7 +217,7 @@ void game_shop_on_init(void)
{
game_shop_change_background();
timer = TM_ZERO;
s_timer = TM_ZERO;
state_machine_register(&shop_sm);
state_machine_change_state(&shop_sm, GAME_SHOP_INTRO);
@@ -246,123 +229,51 @@ void game_shop_on_init(void)
}
/**
* @brief Computes the number of Jokers we can currently roll in the Shop.
* The Jokers we own is taken into account and can't be rolled again.
* @brief Create a shop top row item - jokers, consumables, and possibly playing cards
* Currently only jokers are implemented.
*/
static inline int get_num_shop_jokers_avail(void)
static Item* game_shop_create_top_row_item(void)
{
return bitset_num_set_bits(&s_avail_jokers_bitset);
// TODO: Randomize item type when consumables are implemented
return item_roll_new(ITEM_TYPE_JOKER);
}
/**
* @brief Rolls a random Joker among the available ones
* @brief Setup for the lists of items we can purchase in the top row of the Shop.
* i.e. Jokers and consumables and possibly playing cards.
*/
static inline int game_shop_get_rand_available_joker_id(void)
{
// Roll for what rarity the joker will be
int joker_rarity = joker_get_random_rarity();
// Now determine how many jokers are available based on the rarity
int jokers_avail_size = get_num_shop_jokers_avail();
if (jokers_avail_size == 0)
return UNDEFINED;
int matching_joker_ids[jokers_avail_size];
int fallback_random_idx = rng_get_u32() % jokers_avail_size;
int fallback_random_joker_id = UNDEFINED;
int match_count = 0;
BitsetItr itr = bitset_itr_create(&s_avail_jokers_bitset);
int i = 0;
int joker_id = UNDEFINED;
while ((joker_id = bitset_itr_next(&itr)) != UNDEFINED)
{
if (i++ == fallback_random_idx)
fallback_random_joker_id = joker_id;
const JokerInfo* info = get_joker_registry_entry(joker_id);
if (info->rarity == joker_rarity)
{
matching_joker_ids[match_count++] = joker_id;
}
}
int selected_joker_id = (match_count > 0) ? matching_joker_ids[rng_get_u32() % match_count]
: fallback_random_joker_id;
return selected_joker_id;
}
/**
* @brief Returns true if we can't roll any Joker
*/
static inline bool no_avail_jokers(void)
{
return bitset_is_empty(&s_avail_jokers_bitset);
}
GBAL_UNUSED
static inline bool is_shop_joker_avail(int joker_id)
{
return bitset_get_idx(&s_avail_jokers_bitset, joker_id);
}
/**
* @brief Setup for the lists of items we can purchase in the Shop.
* Only Jokers are available for now, but this is where consumables and
* booster packs will be rolled when they are implemented.
*/
static void game_shop_create_items(void)
static void game_shop_create_top_row_items(void)
{
tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT);
if (no_avail_jokers())
return;
List* shop_items_list = &s_shop_items_list;
List* shop_jokers_list = &s_shop_jokers_list;
list_clear(shop_items_list);
*shop_items_list = list_init();
list_clear(shop_jokers_list);
*shop_jokers_list = list_init();
for (int i = 0; i < MAX_SHOP_JOKERS; i++)
for (int i = 0; i < MAX_SHOP_ITEMS; i++)
{
int joker_id = 0;
#ifdef TEST_JOKER_ID0 // Allow defining an ID for a joker to always appear in shop and be tested
if (is_shop_joker_avail(TEST_JOKER_ID0))
Item* item = game_shop_create_top_row_item();
if (item == NULL)
{
joker_id = TEST_JOKER_ID0;
}
else
#endif
#ifdef TEST_JOKER_ID1
if (is_shop_joker_avail(TEST_JOKER_ID1))
{
joker_id = TEST_JOKER_ID1;
}
else
#endif
{
joker_id = game_shop_get_rand_available_joker_id();
MGBA_FUNC_WARN("Could not create shop item");
// TODO: Decide how to handle this case gracefully
// If the issue for example is that we can't generate any more jokers,
// because for example the user owns all of them,
// maybe we need to generate consumables instead.
return;
}
// If for some reason only no joker is left, don't make another
if (joker_id == UNDEFINED)
break;
item->x = int2fx(SHOP_JOKER_SPRITES_INIT_POS.x + i * CARD_SPRITE_SIZE);
item->y = int2fx(SHOP_JOKER_SPRITES_INIT_POS.y);
item->tx = item->x;
item->ty = int2fx(ITEM_SHOP_Y);
game_shop_set_joker_avail(joker_id, false);
item_print_buy_price_under(item);
JokerObject* joker_object = joker_object_new(joker_new(joker_id));
joker_object->sprite_object->x =
int2fx(SHOP_JOKER_SPRITES_INIT_POS.x + i * CARD_SPRITE_SIZE);
joker_object->sprite_object->y = int2fx(SHOP_JOKER_SPRITES_INIT_POS.y);
joker_object->sprite_object->tx = joker_object->sprite_object->x;
joker_object->sprite_object->ty = int2fx(ITEM_SHOP_Y);
sprite_object_print_price_under(joker_object->sprite_object, joker_object->joker->value);
list_push_back(shop_jokers_list, joker_object);
list_push_back(shop_items_list, item);
}
}
@@ -373,14 +284,14 @@ static void game_shop_intro()
{
main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP);
if (timer == TM_CREATE_SHOP_ITEMS_WAIT)
if (s_timer == TM_CREATE_SHOP_ITEMS_WAIT)
{
game_shop_create_items();
game_shop_create_top_row_items();
}
if (timer >= TM_SHIFT_SHOP_ICON_WAIT) // Shift the shop icon
if (s_timer >= TM_SHIFT_SHOP_ICON_WAIT) // Shift the shop icon
{
int timer_offset = timer - 6;
int timer_offset = s_timer - 6;
// TODO: Extract to generic function?
for (int y = 0; y < timer_offset; y++)
@@ -396,10 +307,10 @@ static void game_shop_intro()
}
}
if (timer == TM_END_GAME_SHOP_INTRO)
if (s_timer == TM_END_GAME_SHOP_INTRO)
{
state_machine_change_state(&shop_sm, GAME_SHOP_ACTIVE);
timer = TM_ZERO; // Reset the timer
s_timer = TM_ZERO; // Reset the timer
// print initial reroll cost only when the panel is in place
tte_printf(
@@ -407,7 +318,7 @@ static void game_shop_intro()
SHOP_REROLL_RECT.left,
SHOP_REROLL_RECT.top,
TTE_WHITE_PB,
reroll_cost
s_reroll_cost
);
}
}
@@ -419,32 +330,23 @@ static void game_shop_intro()
static int shop_top_row_get_size(void)
{
// + 1 to account for next round button
return list_get_len(&s_shop_jokers_list) + 1;
}
/**
* @brief Add a newly purchased Joker to the list of owned Jokers.
*/
static inline void add_to_held_jokers(JokerObject* joker_object)
{
joker_object->sprite_object->ty = int2fx(HELD_JOKERS_POS.y);
add_joker(joker_object);
return list_get_len(&s_shop_items_list) + 1;
}
/**
* @brief Called when pressing A on a Shop Joker to buy it.
*/
static inline void game_shop_buy_joker(int shop_joker_idx)
static inline void game_shop_buy_item(int shop_item_idx)
{
List* shop_jokers_list = &s_shop_jokers_list;
JokerObject* joker_object = (JokerObject*)list_get_at_idx(shop_jokers_list, shop_joker_idx);
List* shop_items_list = &s_shop_items_list;
Item* item = (Item*)list_get_at_idx(shop_items_list, shop_item_idx);
g_game_vars.money -= joker_object->joker->value;
g_game_vars.money -= item_get_buy_price(item);
display_money();
sprite_object_erase_text_under(joker_object->sprite_object);
sprite_object_set_focus(joker_object->sprite_object, false);
add_to_held_jokers(joker_object);
list_remove_at_idx(shop_jokers_list, shop_joker_idx); // Remove the joker from the shop
sprite_object_erase_text_under((SpriteObject*)item);
sprite_object_set_focus((SpriteObject*)item, false);
item_acquire(item);
list_remove_at_idx(shop_items_list, shop_item_idx); // Remove the joker from the shop
}
/**
@@ -461,16 +363,14 @@ static void shop_top_row_on_key_transit(SelectionGrid* selection_grid, Selection
}
else
{
int shop_joker_idx = selection->x - 1; // - 1 to account for next round button
JokerObject* joker_object =
(JokerObject*)list_get_at_idx(&s_shop_jokers_list, shop_joker_idx);
if (joker_object == NULL || list_get_len(get_jokers_list()) >= MAX_JOKERS_HELD_SIZE ||
g_game_vars.money < joker_object->joker->value)
int shop_item_idx = selection->x - 1; // - 1 to account for next round button
Item* item = (Item*)list_get_at_idx(&s_shop_items_list, shop_item_idx);
if (!item_can_acquire(item) || g_game_vars.money < item_get_buy_price(item))
{
return;
}
game_shop_buy_joker(shop_joker_idx);
game_shop_buy_item(shop_item_idx);
selection_grid_move_selection_horz(selection_grid, -1);
}
}
@@ -485,7 +385,7 @@ static bool shop_top_row_on_selection_changed(
const Selection* new_selection
)
{
List* shop_jokers_list = &s_shop_jokers_list;
List* shop_items_list = &s_shop_items_list;
// Guard if we move down while on jokers
if (new_selection->y > row_idx && prev_selection->x > 0)
return false;
@@ -503,8 +403,8 @@ static bool shop_top_row_on_selection_changed(
else
{
int idx = prev_selection->x - 1; // -1 to account for next round button
JokerObject* joker_object = (JokerObject*)list_get_at_idx(shop_jokers_list, idx);
sprite_object_set_focus(joker_object->sprite_object, false);
SpriteObject* sprite_object = (SpriteObject*)list_get_at_idx(shop_items_list, idx);
sprite_object_set_focus(sprite_object, false);
}
}
@@ -517,8 +417,8 @@ static bool shop_top_row_on_selection_changed(
else
{
int idx = new_selection->x - 1; // -1 to account for next round button
JokerObject* joker_object = (JokerObject*)list_get_at_idx(shop_jokers_list, idx);
sprite_object_set_focus(joker_object->sprite_object, true);
SpriteObject* sprite_object = (SpriteObject*)list_get_at_idx(shop_items_list, idx);
sprite_object_set_focus(sprite_object, true);
}
}
@@ -552,8 +452,8 @@ static bool shop_reroll_row_on_selection_changed(
if (new_selection->x != NEXT_ROUND_BTN_SEL_X)
{
int idx = new_selection->x - 1;
JokerObject* joker_object = (JokerObject*)list_get_at_idx(&s_shop_jokers_list, idx);
sprite_object_set_focus(joker_object->sprite_object, true);
SpriteObject* sprite_object = (SpriteObject*)list_get_at_idx(&s_shop_items_list, idx);
sprite_object_set_focus(sprite_object, true);
}
}
else if (row_idx == new_selection->y)
@@ -570,48 +470,46 @@ static bool shop_reroll_row_on_selection_changed(
*/
static inline void game_shop_reroll(void)
{
g_game_vars.money -= reroll_cost;
g_game_vars.money -= s_reroll_cost;
display_money(); // Update the money display
List* shop_jokers_list = &s_shop_jokers_list;
ListItr itr = list_itr_create(shop_jokers_list);
JokerObject* joker_object;
List* shop_items_list = &s_shop_items_list;
ListItr itr = list_itr_create(shop_items_list);
Item* item;
while ((joker_object = list_itr_next(&itr)))
while ((item = list_itr_next(&itr)))
{
if (joker_object != NULL)
if (item != NULL && item->type == ITEM_TYPE_JOKER)
{
game_shop_set_joker_avail(joker_object->joker->id, true);
joker_object_destroy(&joker_object); // Destroy the joker object if it exists
item_dispose(&item);
}
}
list_clear(shop_jokers_list);
*shop_jokers_list = list_init();
list_clear(shop_items_list);
*shop_items_list = list_init();
game_shop_create_items();
game_shop_create_top_row_items();
itr = list_itr_create(shop_jokers_list);
itr = list_itr_create(shop_items_list);
while ((joker_object = list_itr_next(&itr)))
SpriteObject* item_sprite_object;
while ((item_sprite_object = list_itr_next(&itr)))
{
if (joker_object != NULL)
if (item_sprite_object != NULL)
{
// Set the y position to the target position
joker_object->sprite_object->y = joker_object->sprite_object->ty;
item_sprite_object->y = item_sprite_object->ty;
// Give the joker a little wiggle animation
joker_object_shake(joker_object, UNDEFINED);
sprite_object_shake(item_sprite_object, UNDEFINED);
}
}
reroll_cost++;
s_reroll_cost++;
tte_printf(
"#{P:%d,%d; cx:0x%X000}$%d",
SHOP_REROLL_RECT.left,
SHOP_REROLL_RECT.top,
TTE_WHITE_PB,
reroll_cost
s_reroll_cost
);
}
@@ -632,8 +530,8 @@ static void next_round_on_pressed(void)
{
// Go to next blind selection game state
state_machine_change_state(&shop_sm, GAME_SHOP_EXIT);
timer = TM_ZERO;
reroll_cost = REROLL_BASE_COST;
s_timer = TM_ZERO;
s_reroll_cost = REROLL_BASE_COST;
pal_bg_mem[NEXT_ROUND_BTN_SELECTED_BORDER_PAL_IDX] = pal_bg_mem[SHOP_PANEL_SHADOW_PAL_IDX];
}
@@ -646,7 +544,7 @@ static void reroll_on_pressed(void)
static bool reroll_can_be_pressed(void)
{
return g_game_vars.money >= reroll_cost;
return g_game_vars.money >= s_reroll_cost;
}
/**
@@ -664,7 +562,7 @@ static void game_shop_process_user_input(void)
// Owned Joker
case 0:
{
description_card_original_list = get_jokers_list();
s_description_card_original_list = get_jokers_list();
tmp_card = list_get_at_idx(get_jokers_list(), shop_selection_grid.selection.x);
break;
}
@@ -672,9 +570,9 @@ static void game_shop_process_user_input(void)
// Jokers for sale
case 1:
{
description_card_original_list = &s_shop_jokers_list;
s_description_card_original_list = &s_shop_items_list;
tmp_card = (shop_selection_grid.selection.x > 0)
? list_get_at_idx(&s_shop_jokers_list, shop_selection_grid.selection.x - 1)
? list_get_at_idx(&s_shop_items_list, shop_selection_grid.selection.x - 1)
: NULL;
break;
}
@@ -683,7 +581,7 @@ static void game_shop_process_user_input(void)
default:
{
description_card_original_list = NULL;
s_description_card_original_list = NULL;
tmp_card = NULL;
break;
}
@@ -692,14 +590,13 @@ static void game_shop_process_user_input(void)
// Show description of selected card when pressing B.
// Always wait for the card in question to be immobile to avoid accumulating
// errors when pressing and releasing B in quick succession.
if (tmp_card != NULL && tmp_card->sprite_object->vx == 0 && tmp_card->sprite_object->vy == 0 &&
key_held(DESELECT_CARDS))
if (tmp_card != NULL && tmp_card->vx == 0 && tmp_card->vy == 0 && key_held(DESELECT_CARDS))
{
description_card = tmp_card;
description_card_original_x_pos = description_card->sprite_object->x;
description_card_original_y_pos = description_card->sprite_object->y;
s_description_card = tmp_card;
s_description_card_original_x_pos = s_description_card->x;
s_description_card_original_y_pos = s_description_card->y;
timer = TM_ZERO;
s_timer = TM_ZERO;
state_machine_change_state(&shop_sm, GAME_SHOP_SHOW_CARD_DESC);
}
}
@@ -707,7 +604,7 @@ static void game_shop_process_user_input(void)
static void game_shop_show_card_desc(void)
{
// Anim start
if (timer == 1)
if (s_timer == 1)
{
// Erase shop text and disable transparency window
@@ -722,29 +619,29 @@ static void game_shop_show_card_desc(void)
ListItr itr = list_itr_create(get_jokers_list());
while ((joker_object = list_itr_next(&itr)))
{
if (joker_object != description_card)
joker_object->sprite_object->ty -= int2fx(OWNED_CARDS_HIDE_Y_OFFSET);
if (joker_object != s_description_card)
joker_object->ty -= int2fx(OWNED_CARDS_HIDE_Y_OFFSET);
}
// Shop Jokers
itr = list_itr_create(&s_shop_jokers_list);
itr = list_itr_create(&s_shop_items_list);
while ((joker_object = list_itr_next(&itr)))
{
if (joker_object != description_card)
joker_object->sprite_object->ty = int2fx(SHOP_JOKER_SPRITES_INIT_POS.y + TILE_SIZE);
if (joker_object != s_description_card)
joker_object->ty = int2fx(SHOP_JOKER_SPRITES_INIT_POS.y + TILE_SIZE);
}
// Set description_card new target position
description_card->sprite_object->tx = int2fx(CARD_DESCRIPTION_SPRITE_POS.x);
description_card->sprite_object->ty = int2fx(CARD_DESCRIPTION_SPRITE_POS.y);
s_description_card->tx = int2fx(CARD_DESCRIPTION_SPRITE_POS.x);
s_description_card->ty = int2fx(CARD_DESCRIPTION_SPRITE_POS.y);
}
// First 12 anim frames
if (timer <= TM_SHOW_CARD_DESC_WAIT)
if (s_timer <= TM_SHOW_CARD_DESC_WAIT)
{
// Hide Deck (last 5 frames only)
if (TM_SHOW_CARD_DESC_WAIT - timer < 5)
if (TM_SHOW_CARD_DESC_WAIT - s_timer < 5)
main_bg_se_move_rect_1_tile_vert(DECK_ANIM_RECT, SCREEN_DOWN);
// Hide shop panel
main_bg_se_move_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_DOWN);
@@ -753,13 +650,13 @@ static void game_shop_show_card_desc(void)
}
// Anim end
else if (timer == TM_SHOW_CARD_DESC_WAIT + 1)
else if (s_timer == TM_SHOW_CARD_DESC_WAIT + 1)
{
// Compute needed space for the description
const JokerInfo* info = get_joker_registry_entry(description_card->joker->id);
const JokerInfo* info = get_joker_registry_entry(s_description_card->joker->id);
int desc_bottom_offset =
CARD_DESC_MAX_TEXT_HEIGHT -
info->joker_print_desc(description_card->joker, CARD_DESC_TEXT_RECT);
info->joker_print_desc(s_description_card->joker, CARD_DESC_TEXT_RECT);
// Print Rarity and change color or the panel
// Do it before drawing the panel so the color is already set
@@ -795,10 +692,10 @@ static void game_shop_show_card_desc(void)
// Actively wait for the B button to be released, but only if the described card has stopped
// moving
else if (description_card->sprite_object->vx == 0 && description_card->sprite_object->vy == 0 &&
else if (s_description_card->vx == 0 && s_description_card->vy == 0 &&
!key_held(DESELECT_CARDS))
{
timer = TM_ZERO;
s_timer = TM_ZERO;
state_machine_change_state(&shop_sm, GAME_SHOP_HIDE_CARD_DESC);
}
}
@@ -809,7 +706,7 @@ static void game_shop_hide_card_desc(void)
static bool owned_joker_price_printed = false;
// Anim start
if (timer == 1)
if (s_timer == 1)
{
// Erase shop text and Joker Description frame
tte_erase_rect_wrapper(PLAYING_SCREEN_RECT);
@@ -832,52 +729,49 @@ static void game_shop_hide_card_desc(void)
ListItr itr = list_itr_create(get_jokers_list());
while ((joker_object = list_itr_next(&itr)))
{
if (joker_object != description_card)
joker_object->sprite_object->ty = int2fx(HELD_JOKERS_POS.y);
if (joker_object != s_description_card)
joker_object->ty = int2fx(HELD_JOKERS_POS.y);
}
// Shop Jokers
itr = list_itr_create(&s_shop_jokers_list);
itr = list_itr_create(&s_shop_items_list);
while ((joker_object = list_itr_next(&itr)))
{
if (joker_object != description_card)
joker_object->sprite_object->ty = int2fx(ITEM_SHOP_Y);
if (joker_object != s_description_card)
joker_object->ty = int2fx(ITEM_SHOP_Y);
}
description_card->sprite_object->tx = description_card_original_x_pos;
description_card->sprite_object->ty = description_card_original_y_pos;
s_description_card->tx = s_description_card_original_x_pos;
s_description_card->ty = s_description_card_original_y_pos;
}
// First 12 anim frames
if (timer <= TM_SHOW_CARD_DESC_WAIT)
if (s_timer <= TM_SHOW_CARD_DESC_WAIT)
{
// Show Deck (last 5 frames only)
if (TM_SHOW_CARD_DESC_WAIT - timer < 5)
if (TM_SHOW_CARD_DESC_WAIT - s_timer < 5)
main_bg_se_move_rect_1_tile_vert(DECK_ANIM_RECT, SCREEN_UP);
// Show shop panel
main_bg_se_move_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP);
}
// Last anim frame (no need to wait for the Joker to have stopped for this):
else if (timer == TM_SHOW_CARD_DESC_WAIT + 1)
else if (s_timer == TM_SHOW_CARD_DESC_WAIT + 1)
{
// Need to account for the description_card being selected if it came from the shop.
if (description_card_original_list == &s_shop_jokers_list)
description_card->sprite_object->ty += int2fx(TILE_SIZE);
if (s_description_card_original_list == &s_shop_items_list)
s_description_card->ty += int2fx(TILE_SIZE);
// Print price under shop Jokers
JokerObject* joker_object = NULL;
ListItr itr = list_itr_create(&s_shop_jokers_list);
while ((joker_object = list_itr_next(&itr)))
Item* item = NULL;
ListItr itr = list_itr_create(&s_shop_items_list);
while ((item = list_itr_next(&itr)))
{
sprite_object_print_price_under(
joker_object->sprite_object,
joker_object->joker->value
);
item_print_buy_price_under(item);
}
if (description_card_original_list == &s_shop_jokers_list)
description_card->sprite_object->ty -= int2fx(TILE_SIZE);
if (s_description_card_original_list == &s_shop_items_list)
s_description_card->ty -= int2fx(TILE_SIZE);
// Print Reroll prince
tte_printf(
@@ -885,7 +779,7 @@ static void game_shop_hide_card_desc(void)
SHOP_REROLL_RECT.left,
SHOP_REROLL_RECT.top,
TTE_WHITE_PB,
reroll_cost
s_reroll_cost
);
// Print Deck size that was erased
@@ -893,23 +787,23 @@ static void game_shop_hide_card_desc(void)
}
// Cleanup and change state
else if (description_card->sprite_object->vx == 0 && description_card->sprite_object->vy == 0)
else if (s_description_card->vx == 0 && s_description_card->vy == 0)
{
owned_joker_price_printed = false;
description_card = NULL;
timer = TM_ZERO;
s_description_card = NULL;
s_timer = TM_ZERO;
state_machine_change_state(&shop_sm, GAME_SHOP_ACTIVE);
}
// At any point after the other prices have been printed, and while the card is still moving,
// if we are NOT pressing A, print the price under it.
else if (!owned_joker_price_printed && !key_held(SELECT_CARD) &&
description_card_original_list == get_jokers_list())
s_description_card_original_list == get_jokers_list())
{
owned_joker_price_printed = true;
sprite_object_print_price_under(
description_card->sprite_object,
description_card->joker->value
(SpriteObject*)s_description_card,
joker_get_sell_value(s_description_card->joker)
);
}
}
@@ -925,33 +819,33 @@ static void game_shop_outro(void)
main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SCREEN_UP);
// TODO: make heads or tails of what's going on here and replace
// magic numbers.
if (timer == 1)
if (s_timer == 1)
{
tte_erase_rect_wrapper(SHOP_PRICES_TEXT_RECT); // Erase the shop prices text
ListItr itr = list_itr_create(&s_shop_jokers_list);
JokerObject* joker_object;
while ((joker_object = list_itr_next(&itr)))
ListItr itr = list_itr_create(&s_shop_items_list);
SpriteObject* shop_item;
while ((shop_item = list_itr_next(&itr)))
{
if (joker_object != NULL)
if (shop_item != NULL)
{
joker_object->sprite_object->ty = int2fx(160);
shop_item->ty = int2fx(160);
}
}
reset_top_left_panel_bottom_row();
}
else if (timer == 2)
else if (s_timer == 2)
{
// TODO: make heads or tails of what's going on here and replace
// magic numbers.
int y = 5;
memset16(&se_mat[MAIN_BG_SBB][y - 1][0], 0x0001, 1);
memset16(&se_mat[MAIN_BG_SBB][y - 1][1], 0x0002, 7);
memset16(&se_mat[MAIN_BG_SBB][y - 1][8], SE_HFLIP | 0x0001, 1);
}
if (timer >= MENU_POP_OUT_ANIM_FRAMES)
if (s_timer >= MENU_POP_OUT_ANIM_FRAMES)
{
game_change_state(GAME_STATE_BLIND_SELECT);
}
@@ -988,9 +882,9 @@ static inline void game_shop_lights_anim_frame(void)
void game_shop_on_update(void)
{
timer++;
s_timer++;
if (timer % 20 == 0)
if (s_timer % 20 == 0)
{
game_shop_lights_anim_frame();
}
@@ -998,21 +892,16 @@ void game_shop_on_update(void)
void game_shop_on_exit(void)
{
List* shop_jokers_list = &s_shop_jokers_list;
ListItr itr = list_itr_create(shop_jokers_list);
JokerObject* joker_object;
List* shop_items_list = &s_shop_items_list;
ListItr itr = list_itr_create(shop_items_list);
Item* item;
while ((joker_object = list_itr_next(&itr)))
while ((item = list_itr_next(&itr)))
{
if (joker_object != NULL)
{
// Make the joker available back to shop
game_shop_set_joker_avail(joker_object->joker->id, true);
}
joker_object_destroy(&joker_object); // Destroy the joker objects
item_dispose(&item);
}
list_clear(shop_jokers_list);
list_clear(shop_items_list);
increment_blind(BLIND_STATE_DEFEATED); // TODO: Move to game_round_end()?
-9
View File
@@ -9,7 +9,6 @@
#include <tonc_tte.h>
static const Rect FULL_SCREENBLOCK_RECT = {0, 0, SE_ROW_LEN - 1, SE_COL_LEN - 1};
static const Rect TOP_LEFT_PANEL_BOTTOM_ROW_RESET_RECT = {0, 28, 8, 28};
static void clip_se_rect_to_screenblock(Rect* rect);
static void bg_se_copy_or_move_rect_1_tile_vert(
@@ -501,14 +500,6 @@ void main_bg_se_clear_rect(Rect se_rect)
}
}
void reset_top_left_panel_bottom_row(void)
{
BG_POINT top_left_panel_bottom_row_pos = TOP_LEFT_PANEL_POINT;
// Use the source rect height to offset to the bottom row point
top_left_panel_bottom_row_pos.y += rect_height(&TOP_LEFT_ITEM_SRC_RECT) - 1;
main_bg_se_copy_rect(TOP_LEFT_PANEL_BOTTOM_ROW_RESET_RECT, top_left_panel_bottom_row_pos);
}
// Width of the screen, in nb of tiles
#define MAX_LINE_TEXT_LENGTH 30
+78 -75
View File
@@ -24,7 +24,7 @@ typedef struct
char* display_name;
} HandValues;
static const HandValues hand_base_values[] = {
static const HandValues HAND_BASE_VALUES[] = {
{.chips = 0, .mult = 0, .display_name = NULL }, // NONE
{.chips = 5, .mult = 1, .display_name = "Hi-Card"}, // HIGH_CARD
{.chips = 10, .mult = 2, .display_name = "Pair" }, // PAIR
@@ -61,7 +61,7 @@ typedef struct Hand
bool sort_by_suit;
} Hand;
static Hand hand = {
static Hand s_hand = {
.cards = {NULL},
.hand_top = -1,
.hand_selections = 0,
@@ -75,56 +75,66 @@ static Hand hand = {
static ContainedHandTypes compute_contained_hand_types(void);
static enum HandType compute_hand_type(struct ContainedHandTypes contained_types);
// Misc Hand Functions
const char* get_hand_type_name(enum HandType hand_type)
{
if (hand_type <= NONE || hand_type > FLUSH_FIVE)
return NULL;
return HAND_BASE_VALUES[hand_type].display_name;
}
// Hand Struct Manipulation
enum HandState get_hand_state(void)
{
return hand.state;
return s_hand.state;
}
void set_hand_state(enum HandState new_hand_state)
{
hand.state = new_hand_state;
s_hand.state = new_hand_state;
}
CardObject** get_hand_array(void)
{
return hand.cards;
return s_hand.cards;
}
int get_hand_top(void)
{
return hand.hand_top;
return s_hand.hand_top;
}
void set_hand_top(int new_hand_top)
{
hand.hand_top = new_hand_top;
s_hand.hand_top = new_hand_top;
}
int hand_nb_held_cards(void)
{
return hand.hand_top + 1;
return s_hand.hand_top + 1;
}
int hand_get_nb_selected_cards(void)
{
return hand.hand_selections;
return s_hand.hand_selections;
}
void hand_set_nb_selected_cards(int new_selections)
{
hand.hand_selections = new_selections;
s_hand.hand_selections = new_selections;
}
enum HandType get_hand_type(void)
{
return hand.hand_type;
return s_hand.hand_type;
}
ContainedHandTypes* get_contained_hands(void)
{
return &hand.contained_hands;
return &s_hand.contained_hands;
}
static void print_hand_type(const char* hand_type_str)
@@ -146,10 +156,10 @@ static void print_hand_type(const char* hand_type_str)
void compute_hand_value_info(void)
{
tte_erase_rect_wrapper(HAND_TYPE_RECT);
hand.contained_hands = compute_contained_hand_types();
hand.hand_type = compute_hand_type(hand.contained_hands);
s_hand.contained_hands = compute_contained_hand_types();
s_hand.hand_type = compute_hand_type(s_hand.contained_hands);
HandValues hand_values = hand_base_values[hand.hand_type];
HandValues hand_values = HAND_BASE_VALUES[s_hand.hand_type];
g_game_vars.chips = hand_values.chips;
g_game_vars.mult = hand_values.mult;
@@ -163,22 +173,22 @@ void compute_hand_value_info(void)
// no checks will be performed here for performance's sake
void swap_cards_in_hand(int idx_a, int idx_b)
{
CardObject* temp = hand.cards[idx_a];
hand.cards[idx_a] = hand.cards[idx_b];
hand.cards[idx_b] = temp;
CardObject* temp = s_hand.cards[idx_a];
s_hand.cards[idx_a] = s_hand.cards[idx_b];
s_hand.cards[idx_b] = temp;
}
static inline void sort_hand_by_suit(void)
{
for (int idx_a = 0; idx_a < hand.hand_top; idx_a++)
for (int idx_a = 0; idx_a < s_hand.hand_top; idx_a++)
{
for (int idx_b = idx_a + 1; idx_b <= hand.hand_top; idx_b++)
for (int idx_b = idx_a + 1; idx_b <= s_hand.hand_top; idx_b++)
{
if (hand.cards[idx_a] == NULL ||
(hand.cards[idx_b] != NULL &&
(hand.cards[idx_a]->card->suit > hand.cards[idx_b]->card->suit ||
(hand.cards[idx_a]->card->suit == hand.cards[idx_b]->card->suit &&
hand.cards[idx_a]->card->rank > hand.cards[idx_b]->card->rank))))
if (s_hand.cards[idx_a] == NULL ||
(s_hand.cards[idx_b] != NULL &&
(s_hand.cards[idx_a]->card->suit > s_hand.cards[idx_b]->card->suit ||
(s_hand.cards[idx_a]->card->suit == s_hand.cards[idx_b]->card->suit &&
s_hand.cards[idx_a]->card->rank > s_hand.cards[idx_b]->card->rank))))
{
swap_cards_in_hand(idx_a, idx_b);
}
@@ -188,13 +198,13 @@ static inline void sort_hand_by_suit(void)
static inline void sort_hand_by_rank(void)
{
for (int idx_a = 0; idx_a < hand.hand_top; idx_a++)
for (int idx_a = 0; idx_a < s_hand.hand_top; idx_a++)
{
for (int idx_b = idx_a + 1; idx_b <= hand.hand_top; idx_b++)
for (int idx_b = idx_a + 1; idx_b <= s_hand.hand_top; idx_b++)
{
if (hand.cards[idx_a] == NULL ||
(hand.cards[idx_b] != NULL &&
hand.cards[idx_a]->card->rank > hand.cards[idx_b]->card->rank))
if (s_hand.cards[idx_a] == NULL ||
(s_hand.cards[idx_b] != NULL &&
s_hand.cards[idx_a]->card->rank > s_hand.cards[idx_b]->card->rank))
{
swap_cards_in_hand(idx_a, idx_b);
}
@@ -207,25 +217,25 @@ static inline bool shift_null_card_to_end(int null_card_idx)
// Start by searching any non NULL cards after the NULL one
// don't start at null_card_idx+1 to avoid potential illegal array access
int non_null_card_idx = null_card_idx;
for (; non_null_card_idx <= hand.hand_top; non_null_card_idx++)
for (; non_null_card_idx <= s_hand.hand_top; non_null_card_idx++)
{
if (hand.cards[non_null_card_idx] != NULL)
if (s_hand.cards[non_null_card_idx] != NULL)
{
break;
}
}
// return false if there are no non-NULL cards left/there are no more sprites to destroy
if (non_null_card_idx > hand.hand_top)
if (non_null_card_idx > s_hand.hand_top)
{
return false;
}
// If there is one, shift it and all the cards that follow forward
// This way we close the gap and ensure the next card is not NULL
for (int j = 0; j <= hand.hand_top - non_null_card_idx; j++)
for (int j = 0; j <= s_hand.hand_top - non_null_card_idx; j++)
{
hand.cards[null_card_idx + j] = hand.cards[non_null_card_idx + j];
s_hand.cards[null_card_idx + j] = s_hand.cards[non_null_card_idx + j];
}
return true;
@@ -235,11 +245,11 @@ void reorder_card_sprites_layers(void)
{
// Update the sprites in the hand by destroying them and creating new ones in the correct order
// (This feels like a diabolical solution but like literally how else would you do this)
for (int i = 0; i <= hand.hand_top; i++)
for (int i = 0; i <= s_hand.hand_top; i++)
{
// a NULL card will only happen if we rearrange the sprites without having sorted them
// before. Any NULL CardObject will be sent to the end by shifting all elements forward
if (hand.cards[i] == NULL)
if (s_hand.cards[i] == NULL)
{
if (!shift_null_card_to_end(i))
{
@@ -248,20 +258,20 @@ void reorder_card_sprites_layers(void)
}
// card_object_get_sprite() will not work here since we need the address
sprite_destroy(&(hand.cards[i]->sprite_object->sprite));
sprite_destroy(&(s_hand.cards[i]->sprite));
}
// Recreate the sprites for the remaining non NULL cards, in order
for (int i = 0; i <= hand.hand_top; i++)
for (int i = 0; i <= s_hand.hand_top; i++)
{
if (hand.cards[i] != NULL)
if (s_hand.cards[i] != NULL)
{
// Set the sprite for the card object
card_object_set_sprite(hand.cards[i], i);
card_object_set_sprite(s_hand.cards[i], i);
sprite_position(
card_object_get_sprite(hand.cards[i]),
fx2int(hand.cards[i]->sprite_object->x),
fx2int(hand.cards[i]->sprite_object->y)
card_object_get_sprite(s_hand.cards[i]),
fx2int(s_hand.cards[i]->x),
fx2int(s_hand.cards[i]->y)
);
}
}
@@ -269,7 +279,7 @@ void reorder_card_sprites_layers(void)
void sort_cards(void)
{
if (hand.sort_by_suit)
if (s_hand.sort_by_suit)
{
sort_hand_by_suit();
}
@@ -283,29 +293,29 @@ void sort_cards(void)
void hand_change_sort(bool to_sort_by_suit)
{
if (to_sort_by_suit != hand.sort_by_suit)
if (to_sort_by_suit != s_hand.sort_by_suit)
{
hand.sort_by_suit = to_sort_by_suit;
s_hand.sort_by_suit = to_sort_by_suit;
sort_cards();
}
}
void hand_select_card(int index)
{
if (index < 0 || index >= hand_nb_held_cards() || hand.state != HAND_SELECT ||
hand.cards[index] == NULL)
if (index < 0 || index >= hand_nb_held_cards() || s_hand.state != HAND_SELECT ||
s_hand.cards[index] == NULL)
return;
if (card_object_is_selected(hand.cards[index]))
if (card_object_is_selected(s_hand.cards[index]))
{
card_object_set_selected(hand.cards[index], false);
hand.hand_selections--;
card_object_set_selected(s_hand.cards[index], false);
s_hand.hand_selections--;
play_sfx(SFX_CARD_DESELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
}
else if (hand.hand_selections < MAX_SELECTION_SIZE)
else if (s_hand.hand_selections < MAX_SELECTION_SIZE)
{
card_object_set_selected(hand.cards[index], true);
hand.hand_selections++;
card_object_set_selected(s_hand.cards[index], true);
s_hand.hand_selections++;
play_sfx(SFX_CARD_SELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
}
compute_hand_value_info();
@@ -314,12 +324,12 @@ void hand_select_card(int index)
void hand_deselect_all_cards(void)
{
bool any_cards_deselected = false;
for (int i = 0; i <= hand.hand_top; i++)
for (int i = 0; i <= s_hand.hand_top; i++)
{
if (card_object_is_selected(hand.cards[i]))
if (card_object_is_selected(s_hand.cards[i]))
{
card_object_set_selected(hand.cards[i], false);
hand.hand_selections--;
card_object_set_selected(s_hand.cards[i], false);
s_hand.hand_selections--;
any_cards_deselected = true;
}
}
@@ -346,13 +356,13 @@ static void get_hand_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUIT
for (int i = 0; i < NUM_SUITS; i++)
suits_out[i] = 0;
int top = hand.hand_top;
int top = s_hand.hand_top;
for (int i = 0; i <= top; i++)
{
if (hand.cards[i] && card_object_is_selected(hand.cards[i]))
if (s_hand.cards[i] && card_object_is_selected(s_hand.cards[i]))
{
ranks_out[hand.cards[i]->card->rank]++;
suits_out[hand.cards[i]->card->suit]++;
ranks_out[s_hand.cards[i]->card->rank]++;
suits_out[s_hand.cards[i]->card->suit]++;
}
}
}
@@ -573,14 +583,8 @@ int find_flush_in_played_cards(CardObject** played, int top, int min_len, bool*
}
// Returns the number of cards in the best straight or 0 if no straight of min_len is found, marks
// as true them in out_selection[]. This is mostly from Google Gemini
int find_straight_in_played_cards(
CardObject** played,
int top,
bool shortcut_active,
int min_len,
bool* out_selection
)
// them as true in out_selection[]. This is mostly from Google Gemini
int find_straight_in_played_cards(CardObject** played, int top, int min_len, bool* out_selection)
{
if (top < 0)
return 0;
@@ -607,6 +611,7 @@ int find_straight_in_played_cards(
// TODO: Consolidate functions to avoid code duplication?
// Might cost performance because this does a little more
int ace_low_len = ranks[ACE] ? 1 : 0;
bool is_shortcut_active = is_shortcut_joker_active();
for (int i = 0; i < NUM_RANKS; i++)
{
if (ranks[i] > 0)
@@ -614,7 +619,7 @@ int find_straight_in_played_cards(
int prev1 = 0, prev2 = 0;
int parent1 = -1, parent2 = -1;
if (shortcut_active)
if (is_shortcut_active)
{
if (i == TWO)
{
@@ -723,8 +728,6 @@ int find_straight_in_played_cards(
return 0;
}
// This is used for the special case in "Four Fingers" where you can add a pair into a straight
// (e.g. AA234 should score all 5 cards)
void select_paired_cards_in_hand(CardObject** played, int played_top, bool* selection)
{
// Build a set of ranks that are already selected
@@ -762,7 +765,7 @@ static ContainedHandTypes compute_contained_hand_types(void)
ContainedHandTypes hand_types = {0};
// Idk if this is how Balatro does it but this is how I'm doing it
if (hand.hand_selections == 0 || hand.state == HAND_DISCARD)
if (s_hand.hand_selections == 0 || s_hand.state == HAND_DISCARD)
{
return hand_types;
}
+71
View File
@@ -0,0 +1,71 @@
#include "item.h"
#include "item_funcs.h"
#include "mgba_logger.h"
#include "util.h"
#include <tonc.h>
Item* item_roll_new(enum ItemType item_type)
{
if ((int)item_type < 0 || item_type >= ITEM_NUM_TYPES)
{
MGBA_FUNC_ERROR("Undefined type %d", item_type);
return NULL;
}
ItemFuncs* item_funcs = get_item_type_funcs(item_type);
GBAL_RETURN_IF_NULL_RET(item_funcs, NULL);
GBAL_RETURN_IF_NULL_RET(item_funcs->roll_new, NULL);
return item_funcs->roll_new();
}
int item_get_buy_price(Item* item)
{
GBAL_RETURN_IF_NULL_RET(item, UNDEFINED);
ItemFuncs* item_funcs = get_item_type_funcs(item->type);
GBAL_RETURN_IF_NULL_RET(item_funcs, UNDEFINED);
GBAL_RETURN_IF_NULL_RET(item_funcs->get_buy_price, UNDEFINED);
return item_funcs->get_buy_price(item);
}
void item_acquire(Item* item)
{
GBAL_RETURN_IF_NULL_VOID(item);
ItemFuncs* item_funcs = get_item_type_funcs(item->type);
GBAL_RETURN_IF_NULL_VOID(item_funcs);
GBAL_RETURN_IF_NULL_VOID(item_funcs->acquire);
item_funcs->acquire(item);
}
bool item_can_acquire(Item* item)
{
GBAL_RETURN_IF_NULL_RET(item, false);
ItemFuncs* item_funcs = get_item_type_funcs(item->type);
GBAL_RETURN_IF_NULL_RET(item_funcs, false);
GBAL_RETURN_IF_NULL_RET(item_funcs->can_acquire, false);
return item_funcs->can_acquire(item);
}
void item_dispose(Item** item)
{
GBAL_RETURN_IF_NULL_VOID(item);
GBAL_RETURN_IF_NULL_VOID(*item);
ItemFuncs* item_funcs = get_item_type_funcs((*item)->type);
GBAL_RETURN_IF_NULL_VOID(item_funcs);
GBAL_RETURN_IF_NULL_VOID(item_funcs->dispose);
item_funcs->dispose(item);
}
void item_print_buy_price_under(Item* item)
{
GBAL_RETURN_IF_NULL_VOID(item);
sprite_object_print_price_under((SpriteObject*)item, item_get_buy_price(item));
}
+65
View File
@@ -0,0 +1,65 @@
#include "item_funcs.h"
#include "card.h"
#include "game.h"
#include "game/shop.h"
#include "joker.h"
#include "util.h"
static Item* item_roll_new_unimplemented();
static void item_acquire_unimplemented(Item* item);
static bool item_always_can_acquire(Item* item);
// clang-format off
ItemFuncs item_func_table[] = {
[ITEM_TYPE_JOKER] = {
.roll_new = joker_object_roll_new,
.get_buy_price = joker_object_get_buy_price,
.acquire = joker_object_add_to_owned,
.can_acquire = joker_object_can_acquire,
.dispose = joker_object_dispose
},
/* Currently playing cards have partial implementations since Magic Trick is not implemented
* and playing cards can't appear in the shop.
* If the Magic Trick voucher is implemented, these need to be completed.
* Playing cards must stay the only exception, the rest of the item types must implement
* all the basic functions to appear in the shop.
*/
[ITEM_TYPE_PLAYING_CARD] = {
.roll_new = item_roll_new_unimplemented,
.get_buy_price = card_object_get_buy_price,
.acquire = item_acquire_unimplemented,
.can_acquire = item_always_can_acquire,
.dispose = card_object_dispose
}
};
// clang-format on
ItemFuncs* get_item_type_funcs(enum ItemType type)
{
if ((int)type < 0 || type >= NUM_ELEM_IN_ARR(item_func_table))
{
MGBA_FUNC_ERROR("Invalid type %d", type);
return NULL;
}
return &item_func_table[type];
}
static Item* item_roll_new_unimplemented()
{
MGBA_FUNC_ERROR("Unimplemented roll_new function called");
return NULL;
}
static void item_acquire_unimplemented(Item* item)
{
GBAL_RETURN_IF_NULL_VOID(item);
MGBA_FUNC_ERROR("Unimplemented acquire function called for item type type %d", (item)->type);
}
static bool item_always_can_acquire(Item* item)
{
return true;
}
+195 -38
View File
@@ -4,6 +4,8 @@
#include "game/round.h"
#include "game_variables.h"
#include "graphic_utils.h"
#include "layout.h"
#include "mgba_logger.h"
#include "pool.h"
#include "random.h"
#include "soundbank.h"
@@ -11,6 +13,7 @@
// Tiles and palettes
#include "card_rarity_pal_gfx.h"
#include "item.h"
#include "joker_gfx.h"
#include <maxmod.h>
@@ -35,7 +38,8 @@ static const unsigned short* joker_gfxPal[] = {
#undef DEF_JOKER_GFX
};
const static u8 edition_price_lut[MAX_EDITIONS] = {
// TODO: Removed unplanned editions...
const static u8 EDITION_PRICE_LUT[MAX_EDITIONS] = {
0, // BASE_EDITION
2, // FOIL_EDITION
3, // HOLO_EDITION
@@ -50,20 +54,22 @@ const static u8 edition_price_lut[MAX_EDITIONS] = {
logic. But I'm going to use a simpler approach for the joker objects since I'm lazy and sorting
them wouldn't look good enough to warrant the effort.
*/
static bool used_layers[MAX_JOKER_OBJECTS] = {false}; // Track used layers for joker sprites
static bool s_used_layers[MAX_JOKER_OBJECTS] = {false}; // Track used layers for joker sprites
// TODO: Refactor sorting into SpriteObject?
// Maps the spritesheet index to the palette bank index allocated to it.
// Spritesheets that were not allocated are
static int joker_spritesheet_pb_map[MAX_NUM_JOKERS_SPRITESHEETS];
static int joker_pb_num_sprite_users[JOKER_LAST_PB - JOKER_BASE_PB + 1] = {0};
static int s_joker_spritesheet_pb_map[MAX_NUM_JOKERS_SPRITESHEETS];
static int s_joker_pb_num_sprite_users[JOKER_LAST_PB - JOKER_BASE_PB + 1] = {0};
BITSET_DEFINE(s_rollable_jokers_bitset, MAX_DEFINABLE_JOKERS)
// See linked issue for context of maps
// https://github.com/GBALATRO/balatro-gba/issues/274#issue-3685075538
// clang-format off
// Map of Joker ID -> Spritesheet idx
static int joker_id_to_sprite_map[] = {
static const int JOKER_ID_TO_SPRITE_MAP[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1,
2, 2,
@@ -88,13 +94,13 @@ static int joker_id_to_sprite_map[] = {
// This is used to determine the sprite index of a Joker's sprite
// within its spritesheet by substracting its ID to this starting ID.
// Notice how spritesheets with only one Joker have sequential starting IDs.
static int spritesheet_idx_to_starting_joker_id[] = {
static const int SPRITESHEET_IDX_TO_STARTING_JOKER_ID[] = {
0, 18, 20, 22, 27, 32, 36, 40, 42, 44,
45, 46, 47, 48, 49, 50, 51, 52
};
// Lookup table of Joker Rarity strings. Used to display at the bottom of the description screen.
static const char* joker_rarity_strings_lut[MAX_RARITIES] = {
static const char* JOKER_RARITY_STRINGS_LUT[MAX_RARITIES] = {
"Common", "Uncommon", "Rare", "Legendary"
};
// clang-format on
@@ -115,7 +121,7 @@ void joker_init()
for (int i = 0; i < num_spritesheets; i++)
{
joker_spritesheet_pb_map[i] = UNDEFINED;
s_joker_spritesheet_pb_map[i] = UNDEFINED;
}
}
@@ -129,7 +135,7 @@ Joker* joker_new(u8 id)
joker->id = id;
joker->modifier = BASE_EDITION; // TODO: Make this a parameter
joker->value = jinfo->base_value + edition_price_lut[joker->modifier];
joker->value = jinfo->base_value + EDITION_PRICE_LUT[joker->modifier];
joker->rarity = jinfo->rarity;
joker->scoring_state = 0;
joker->persistent_state = 0;
@@ -166,7 +172,7 @@ const char* joker_get_rarity_string(u8 rarity)
if (rarity >= MAX_RARITIES)
return NULL;
return joker_rarity_strings_lut[rarity];
return JOKER_RARITY_STRINGS_LUT[rarity];
}
u16 joker_get_rarity_color(u8 rarity, bool main_color)
@@ -179,12 +185,16 @@ u16 joker_get_rarity_color(u8 rarity, bool main_color)
return card_rarity_pal_gfxPal[1 + 2 * rarity + (main_color ? 0 : 1)];
}
int joker_get_buy_price(const Joker* joker)
{
GBAL_RETURN_IF_NULL_RET(joker, UNDEFINED);
return joker->value;
}
int joker_get_sell_value(const Joker* joker)
{
if (joker == NULL)
{
return UNDEFINED;
}
GBAL_RETURN_IF_NULL_RET(joker, UNDEFINED);
return joker->value / 2;
}
@@ -194,19 +204,22 @@ JokerObject* joker_object_new(Joker* joker)
{
JokerObject* joker_object = POOL_GET(JokerObject);
sprite_object_init((SpriteObject*)joker_object);
int layer = 0;
for (int i = 0; i < MAX_JOKER_OBJECTS; i++)
{
if (!used_layers[i])
if (!s_used_layers[i])
{
layer = i;
used_layers[i] = true; // Mark this layer as used
s_used_layers[i] = true; // Mark this layer as used
break;
}
}
joker_object->joker = joker;
joker_object->sprite_object = sprite_object_new();
joker_object->type = ITEM_TYPE_JOKER;
int tile_index = JOKER_TID + layer * JOKER_SPRITE_OFFSET;
@@ -222,7 +235,7 @@ JokerObject* joker_object_new(Joker* joker)
);
sprite_object_set_sprite(
joker_object->sprite_object,
(SpriteObject*)joker_object,
sprite_new(
ATTR0_SQUARE | ATTR0_4BPP | ATTR0_AFF,
ATTR1_SIZE_32,
@@ -241,27 +254,171 @@ void joker_object_destroy(JokerObject** joker_object)
return;
int layer = sprite_get_layer(joker_object_get_sprite(*joker_object)) - JOKER_STARTING_LAYER;
used_layers[layer] = false;
s_used_layers[layer] = false;
s_joker_pb_remove_sprite_user(sprite_get_pb(joker_object_get_sprite(*joker_object)));
if (s_joker_pb_get_num_sprite_users((sprite_get_pb(joker_object_get_sprite(*joker_object)))) ==
0)
{
joker_spritesheet_pb_map[s_joker_get_spritesheet_idx((*joker_object)->joker->id)] =
s_joker_spritesheet_pb_map[s_joker_get_spritesheet_idx((*joker_object)->joker->id)] =
UNDEFINED;
}
sprite_object_destroy(&(*joker_object)->sprite_object); // Destroy the sprite
joker_destroy(&(*joker_object)->joker); // Destroy the joker
sprite_object_destroy((SpriteObject*)(*joker_object));
joker_destroy(&(*joker_object)->joker);
POOL_FREE(JokerObject, *joker_object);
*joker_object = NULL;
}
void joker_object_shake(JokerObject* joker_object, mm_word sound_id)
void joker_object_dispose(Item** joker_object_item)
{
sprite_object_shake(joker_object->sprite_object, sound_id);
GBAL_RETURN_IF_NULL_VOID(joker_object_item);
GBAL_RETURN_IF_NULL_VOID(*joker_object_item);
ITEM_RETURN_IF_UNEXPECTED_TYPE_VOID(*joker_object_item, ITEM_TYPE_JOKER);
JokerObject* joker_object = (JokerObject*)(*joker_object_item);
GBAL_RETURN_IF_NULL_VOID(joker_object->joker);
joker_set_rollable(joker_object->joker->id, true);
joker_object_destroy(&joker_object);
*joker_object_item = NULL;
}
void set_and_shift_text(char* str, int* cursor_pos_x, int* cursor_pos_y, int color_pb)
void joker_object_shake(JokerObject* joker_object, mm_word sound_id)
{
sprite_object_shake((SpriteObject*)joker_object, sound_id);
}
int joker_object_get_buy_price(Item* joker_object)
{
GBAL_RETURN_IF_NULL_RET(joker_object, UNDEFINED);
ITEM_RETURN_IF_UNEXPECTED_TYPE_RET(joker_object, ITEM_TYPE_JOKER, UNDEFINED);
return ((JokerObject*)joker_object)->joker->value;
}
void joker_object_add_to_owned(Item* joker_object)
{
GBAL_RETURN_IF_NULL_VOID(joker_object);
ITEM_RETURN_IF_UNEXPECTED_TYPE_VOID(joker_object, ITEM_TYPE_JOKER);
joker_object->ty = int2fx(HELD_JOKERS_POS.y);
add_joker((JokerObject*)joker_object);
}
void joker_set_rollable(int joker_id, bool rollable)
{
bitset_set_idx(&s_rollable_jokers_bitset, joker_id, rollable);
}
/**
* @brief Computes the number of Jokers we can currently roll in the Shop.
* The Jokers we own is taken into account and can't be rolled again.
*/
static inline int get_num_rollable_jokers(void)
{
return bitset_num_set_bits(&s_rollable_jokers_bitset);
}
/**
* @brief Returns true if we can't roll any Joker
*/
static inline bool no_rollable_jokers(void)
{
return bitset_is_empty(&s_rollable_jokers_bitset);
}
GBAL_UNUSED
static inline bool joker_is_rollable(int joker_id)
{
return bitset_get_idx(&s_rollable_jokers_bitset, joker_id);
}
void joker_reset_rollable_jokers(void)
{
int num_jokers = get_joker_registry_size();
bitset_clear(&s_rollable_jokers_bitset);
for (int i = 0; i < num_jokers; i++)
{
bitset_set_idx(&s_rollable_jokers_bitset, i, true);
}
}
/**
* @brief Rolls a random Joker among the available ones
*/
static int joker_roll_id(void)
{
// Now determine how many jokers are available based on the rarity
int jokers_avail_size = get_num_rollable_jokers();
if (jokers_avail_size == 0)
return UNDEFINED;
// Roll for what rarity the joker will be
int joker_rarity = joker_get_random_rarity();
int matching_joker_ids[jokers_avail_size];
int fallback_random_idx = rng_get_u32() % jokers_avail_size;
int fallback_random_joker_id = UNDEFINED;
int match_count = 0;
BitsetItr itr = bitset_itr_create(&s_rollable_jokers_bitset);
int i = 0;
int joker_id = UNDEFINED;
while ((joker_id = bitset_itr_next(&itr)) != UNDEFINED)
{
if (i++ == fallback_random_idx)
fallback_random_joker_id = joker_id;
const JokerInfo* info = get_joker_registry_entry(joker_id);
if (info->rarity == joker_rarity)
{
matching_joker_ids[match_count++] = joker_id;
}
}
int selected_joker_id = (match_count > 0) ? matching_joker_ids[rng_get_u32() % match_count]
: fallback_random_joker_id;
return selected_joker_id;
}
Item* joker_object_roll_new(void)
{
if (no_rollable_jokers())
return NULL;
int joker_id = 0;
#ifdef TEST_JOKER_ID0 // Allow defining an ID for a joker to always appear in shop and be tested
if (joker_is_rollable(TEST_JOKER_ID0))
{
joker_id = TEST_JOKER_ID0;
}
else
#endif
#ifdef TEST_JOKER_ID1
if (joker_is_rollable(TEST_JOKER_ID1))
{
joker_id = TEST_JOKER_ID1;
}
else
#endif
{
joker_id = joker_roll_id();
}
// If for some reason only no joker is left, don't make another
if (joker_id == UNDEFINED)
return NULL;
joker_set_rollable(joker_id, false);
return (Item*)joker_object_new(joker_new(joker_id));
}
static void set_and_shift_text(char* str, int* cursor_pos_x, int* cursor_pos_y, int color_pb)
{
tte_set_pos(*cursor_pos_x, *cursor_pos_y);
tte_set_special(color_pb * TTE_SPECIAL_PB_MULT_OFFSET);
@@ -305,12 +462,12 @@ bool joker_object_score(
{
// display the text on top of the card instead of below the Joker for Held Cards effects
// scored_card cannot be NULL here because of the joker event
cursorPosX += fx2int(card_object->sprite_object->x);
cursorPosX += fx2int(card_object->x);
cursorPosY = HELD_CARD_SCORE_TEXT_Y;
}
else
{
cursorPosX += fx2int(joker_object->sprite_object->x);
cursorPosX += fx2int(joker_object->x);
cursorPosY = JOKER_SCORE_TEXT_Y;
}
@@ -375,7 +532,7 @@ Sprite* joker_object_get_sprite(JokerObject* joker_object)
{
if (joker_object == NULL)
return NULL;
return sprite_object_get_sprite(joker_object->sprite_object);
return sprite_object_get_sprite((SpriteObject*)joker_object);
}
int joker_get_random_rarity()
@@ -410,35 +567,35 @@ static int s_get_num_spritesheets()
static int s_joker_get_spritesheet_idx(u8 joker_id)
{
return joker_id_to_sprite_map[joker_id];
return JOKER_ID_TO_SPRITE_MAP[joker_id];
}
static int s_joker_get_sprite_idx_in_sheet(u8 joker_id, int spritesheet_idx)
{
return joker_id - spritesheet_idx_to_starting_joker_id[joker_id_to_sprite_map[joker_id]];
return joker_id - SPRITESHEET_IDX_TO_STARTING_JOKER_ID[JOKER_ID_TO_SPRITE_MAP[joker_id]];
}
static void s_joker_pb_add_sprite_user(int pb)
{
joker_pb_num_sprite_users[pb - JOKER_BASE_PB]++;
s_joker_pb_num_sprite_users[pb - JOKER_BASE_PB]++;
}
static void s_joker_pb_remove_sprite_user(int pb)
{
int num_sprite_users = joker_pb_num_sprite_users[pb - JOKER_BASE_PB];
joker_pb_num_sprite_users[pb - JOKER_BASE_PB] = max(0, num_sprite_users - 1);
int num_sprite_users = s_joker_pb_num_sprite_users[pb - JOKER_BASE_PB];
s_joker_pb_num_sprite_users[pb - JOKER_BASE_PB] = max(0, num_sprite_users - 1);
}
static int s_joker_pb_get_num_sprite_users(int joker_pb)
{
return joker_pb_num_sprite_users[joker_pb - JOKER_BASE_PB];
return s_joker_pb_num_sprite_users[joker_pb - JOKER_BASE_PB];
}
static int s_get_unused_joker_pb()
{
for (int i = 0; i < NUM_ELEM_IN_ARR(joker_pb_num_sprite_users); i++)
for (int i = 0; i < NUM_ELEM_IN_ARR(s_joker_pb_num_sprite_users); i++)
{
if (joker_pb_num_sprite_users[i] == 0)
if (s_joker_pb_num_sprite_users[i] == 0)
{
return (i + JOKER_BASE_PB);
}
@@ -450,7 +607,7 @@ static int s_get_unused_joker_pb()
static int s_allocate_pb_if_needed(u8 joker_id)
{
int joker_spritesheet_idx = s_joker_get_spritesheet_idx(joker_id);
int joker_pb = joker_spritesheet_pb_map[joker_spritesheet_idx];
int joker_pb = s_joker_spritesheet_pb_map[joker_spritesheet_idx];
if (joker_pb != UNDEFINED)
{
// Already allocated
@@ -467,7 +624,7 @@ static int s_allocate_pb_if_needed(u8 joker_id)
}
else
{
joker_spritesheet_pb_map[joker_spritesheet_idx] = joker_pb;
s_joker_spritesheet_pb_map[joker_spritesheet_idx] = joker_pb;
memcpy16(
&pal_obj_mem[PAL_ROW_LEN * joker_pb],
joker_gfxPal[joker_spritesheet_idx],
+316 -241
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -25,6 +25,31 @@
#include "soundbank.h"
#include "soundbank_bin.h"
enum
{
TTE_ASCII_GLYPH_COUNT = 96,
FONT_TILES_PER_ROW = 16,
CHARBLOCK_4BPP_TILE_COUNT = 512,
SCREENBLOCK_4BPP_TILE_COUNT = 64,
CHINESE_GLYPH_COUNT = 0
#define CHINESE_GLYPH(codepoint) +1
#include "def_chinese_glyph_table.h"
#undef CHINESE_GLYPH
,
TTE_FONT_TILE_COUNT =
TTE_ASCII_GLYPH_COUNT +
((CHINESE_GLYPH_COUNT + FONT_TILES_PER_ROW - 1) / FONT_TILES_PER_ROW) * FONT_TILES_PER_ROW,
TTE_FONT_FIRST_TILE = TTE_CBB * CHARBLOCK_4BPP_TILE_COUNT,
TTE_FONT_END_TILE = TTE_FONT_FIRST_TILE + TTE_FONT_TILE_COUNT,
AFFINE_BG_MAP_FIRST_TILE = AFFINE_BG_SBB * SCREENBLOCK_4BPP_TILE_COUNT,
AFFINE_BG_MAP_END_TILE = AFFINE_BG_MAP_FIRST_TILE + SCREENBLOCK_4BPP_TILE_COUNT,
};
_Static_assert(
TTE_FONT_END_TILE <= AFFINE_BG_MAP_FIRST_TILE || AFFINE_BG_MAP_END_TILE <= TTE_FONT_FIRST_TILE,
"TTE font tiles overlap the affine background map"
);
void init()
{
rng_init();
+37 -9
View File
@@ -16,28 +16,53 @@ static const u32 MGBA_ENABLE_MAGIC = 0xC0DE;
static const u32 MGBA_ENABLE_OK = 0x1DEA;
static const u32 MGBA_LOG_SEND = 0x100;
static const u32 MGBA_LOG_BUFFER_SIZE = 0x100;
static const u16 MGBA_LOG_LEVEL_MASK = 0x7;
static bool mgba_logger_available = false;
static bool s_mgba_logger_available = false;
bool mgba_logger_init(void)
{
*MGBA_REG_DEBUG_ENABLE = MGBA_ENABLE_MAGIC;
mgba_logger_available = (*MGBA_REG_DEBUG_ENABLE == MGBA_ENABLE_OK);
return mgba_logger_available;
s_mgba_logger_available = (*MGBA_REG_DEBUG_ENABLE == MGBA_ENABLE_OK);
return s_mgba_logger_available;
}
static void mgba_vprintf(MgbaLogLevel level, const char* fmt, va_list args)
{
if (!s_mgba_logger_available || fmt == NULL)
return;
vsnprintf(MGBA_REG_DEBUG_STRING, MGBA_LOG_BUFFER_SIZE, fmt, args);
*MGBA_REG_DEBUG_FLAGS = ((uint16_t)level & MGBA_LOG_LEVEL_MASK) | MGBA_LOG_SEND;
}
void mgba_printf(MgbaLogLevel level, const char* fmt, ...)
{
if (!mgba_logger_available || fmt == NULL)
return;
va_list args;
va_start(args, fmt);
vsnprintf(MGBA_REG_DEBUG_STRING, MGBA_LOG_BUFFER_SIZE, fmt, args);
mgba_vprintf(level, fmt, args);
va_end(args);
*MGBA_REG_DEBUG_FLAGS = ((uint16_t)level & 0x7) | MGBA_LOG_SEND;
}
void mgba_func_printf(MgbaLogLevel level, const char* func_name, const char* fmt, ...)
{
if (!s_mgba_logger_available || func_name == NULL || fmt == NULL)
{
// The one place where we can't log the error.
return;
}
char printed_str_buff[MGBA_LOG_BUFFER_SIZE];
// Expand the format first so the full string is truncated in case it's too long
va_list args;
va_start(args, fmt);
vsnprintf(printed_str_buff, sizeof(printed_str_buff), fmt, args);
va_end(args);
mgba_printf(level, "%s(): %s", func_name, printed_str_buff);
}
#else
// Noop stubs
@@ -50,4 +75,7 @@ void mgba_printf(MgbaLogLevel level, const char* fmt, ...)
{
}
void mgba_func_printf(MgbaLogLevel level, const char* func_name, const char* fmt, ...)
{
}
#endif
+3 -3
View File
@@ -7,7 +7,7 @@
#include <tonc.h>
// Accumulate timer 1 into a bigger variable so we can generate more diverse seeds
static u32 timer_acc = 0;
static u32 s_timer_acc = 0;
// Timers usage docs: https://gbadev.net/tonc/timers.html
void rng_init(void)
@@ -18,7 +18,7 @@ void rng_init(void)
void rng_update(void)
{
timer_acc += (u32)REG_TM1D;
s_timer_acc += (u32)REG_TM1D;
}
void rng_set_seed(u32 seed)
@@ -30,7 +30,7 @@ void rng_set_seed(u32 seed)
void rng_shuffle_seed(void)
{
rng_set_seed(timer_acc);
rng_set_seed(s_timer_acc);
}
u32 rng_get_u32(void)
+5 -5
View File
@@ -9,11 +9,11 @@
#include <tonc.h>
static const Rect COUNTDOWN_TIMER_RECT = {208, 144, 240, 152};
static uint timer = 0;
static uint s_timer = 0;
void splash_screen_on_init(void)
{
timer = 0;
s_timer = 0;
tte_printf("#{P:72,8; cx:0x%X000}DISCLAIMER", TTE_WHITE_PB);
tte_printf(
@@ -29,9 +29,9 @@ void splash_screen_on_init(void)
void splash_screen_on_update(void)
{
timer++;
s_timer++;
if (timer < SPLASH_DURATION_FRAMES)
if (s_timer < SPLASH_DURATION_FRAMES)
{
tte_erase_rect_wrapper(COUNTDOWN_TIMER_RECT);
tte_printf(
@@ -39,7 +39,7 @@ void splash_screen_on_update(void)
COUNTDOWN_TIMER_RECT.left,
COUNTDOWN_TIMER_RECT.top,
TTE_WHITE_PB,
1 + (SPLASH_DURATION_FRAMES - timer) / SPLASH_FPS
1 + (SPLASH_DURATION_FRAMES - s_timer) / SPLASH_FPS
);
if (!key_hit(KEY_ANY))
+9 -10
View File
@@ -4,6 +4,8 @@
#include "game.h"
#include "game_variables.h"
#include "graphic_utils.h"
#include "item.h"
#include "mgba_logger.h"
#include "pool.h"
#include "random.h"
#include "soundbank.h"
@@ -169,28 +171,25 @@ int sprite_get_pb(const Sprite* sprite)
}
// SpriteObject methods
SpriteObject* sprite_object_new()
void sprite_object_init(SpriteObject* sprite_object)
{
SpriteObject* sprite_object = POOL_GET(SpriteObject);
GBAL_RETURN_IF_NULL_VOID(sprite_object);
sprite_object->sprite = NULL;
sprite_object_reset_transform(sprite_object);
sprite_object->focused = false;
list_push_back(&sprite_objects_list, sprite_object);
return sprite_object;
}
void sprite_object_destroy(SpriteObject** sprite_object)
void sprite_object_destroy(SpriteObject* sprite_object)
{
if (*sprite_object == NULL)
if (sprite_object == NULL)
return;
list_remove_data(&sprite_objects_list, *sprite_object);
list_remove_data(&sprite_objects_list, sprite_object);
sprite_destroy(&(*sprite_object)->sprite);
POOL_FREE(SpriteObject, *sprite_object);
*sprite_object = NULL;
sprite_destroy(&sprite_object->sprite);
}
void sprite_object_set_sprite(SpriteObject* sprite_object, Sprite* sprite)
+4 -4
View File
@@ -4,7 +4,7 @@
#include "list.h"
#include "util.h"
static List update_cbs = LIST_DEFAULT;
static List s_update_cbs = LIST_DEFAULT;
// Used as a No Operation for game states that have no init and/or exit function.
// ricfehr3 did the work of determining whether a noop or a NULL check was more
@@ -23,7 +23,7 @@ void state_machine_register(StateMachine* state_machine)
state_machine->active_update = noop;
state_machine->state = UNDEFINED;
list_push_back(&update_cbs, &state_machine->active_update);
list_push_back(&s_update_cbs, &state_machine->active_update);
}
void state_machine_remove(StateMachine* state_machine)
@@ -32,12 +32,12 @@ void state_machine_remove(StateMachine* state_machine)
return;
state_machine->registered = false;
list_remove_data(&update_cbs, &state_machine->active_update);
list_remove_data(&s_update_cbs, &state_machine->active_update);
}
void state_machine_update(void)
{
ListItr itr = list_itr_create(&update_cbs);
ListItr itr = list_itr_create(&s_update_cbs);
StateCallback* cb;
while ((cb = list_itr_next(&itr)))
{
+15
View File
@@ -0,0 +1,15 @@
CC := gcc
CFLAGS := -I../../include -I. \
-g -O3 -std=gnu23 -Wall -Werror
SRC := chinese_text_test.c \
../../source/chinese_text.c
OUT := build/chinese_text_test
$(OUT): $(SRC) | build
$(CC) $(CFLAGS) -o $@ $^
build:
mkdir -p build
clean:
rm -f $(OUT)
+120
View File
@@ -0,0 +1,120 @@
#include "chinese_text.h"
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static const uint32_t CHINESE_CODEPOINTS[] = {
#define CHINESE_GLYPH(codepoint) codepoint,
#include "def_chinese_glyph_table.h"
#undef CHINESE_GLYPH
};
static size_t encode_utf8_codepoint(uint32_t codepoint, char output[4])
{
if (codepoint <= 0x7F)
{
output[0] = (char)codepoint;
return 1;
}
if (codepoint <= 0x7FF)
{
output[0] = (char)(0xC0 | (codepoint >> 6));
output[1] = (char)(0x80 | (codepoint & 0x3F));
return 2;
}
output[0] = (char)(0xE0 | (codepoint >> 12));
output[1] = (char)(0x80 | ((codepoint >> 6) & 0x3F));
output[2] = (char)(0x80 | (codepoint & 0x3F));
return 3;
}
static uint32_t decode_like_tonc(const unsigned char encoded[2])
{
return ((uint32_t)(encoded[0] & 0x1F) << 6) | (encoded[1] & 0x3F);
}
static void test_ascii_is_unchanged(void)
{
char output[32];
size_t output_len = chinese_text_encode("A: +20", output, sizeof(output));
assert(output_len == strlen("A: +20"));
assert(strcmp(output, "A: +20") == 0);
}
static void test_supported_chinese_uses_extended_glyphs(void)
{
char output[8];
size_t output_len = chinese_text_encode("小丑牌", output, sizeof(output));
assert(output_len == 6);
assert((unsigned char)output[0] == 0xC2);
assert((unsigned char)output[1] == 0x80);
assert((unsigned char)output[2] == 0xC2);
assert((unsigned char)output[3] == 0x81);
assert((unsigned char)output[4] == 0xC2);
assert((unsigned char)output[5] == 0x82);
assert(output[6] == '\0');
}
static void test_unsupported_unicode_becomes_question_mark(void)
{
char output[8];
size_t output_len = chinese_text_encode("龘", output, sizeof(output));
assert(output_len == 1);
assert(strcmp(output, "?") == 0);
}
static void test_all_custom_glyphs_round_trip_through_tonc_utf8(void)
{
size_t glyph_count = sizeof(CHINESE_CODEPOINTS) / sizeof(CHINESE_CODEPOINTS[0]);
assert(glyph_count <= 128);
for (size_t i = 0; i < glyph_count; i++)
{
char input[4] = {'\0'};
size_t input_len = encode_utf8_codepoint(CHINESE_CODEPOINTS[i], input);
input[input_len] = '\0';
char output[4] = {'\0'};
size_t output_len = chinese_text_encode(input, output, sizeof(output));
uint32_t expected_glyph_codepoint = 128 + i;
assert(output_len == 2);
assert(((unsigned char)output[0] & 0xE0) == 0xC0);
assert(((unsigned char)output[1] & 0xC0) == 0x80);
assert(decode_like_tonc((const unsigned char*)output) == expected_glyph_codepoint);
assert(output[2] == '\0');
}
}
static void test_small_buffer_does_not_split_utf8_sequence(void)
{
char output[4] = {'x', 'x', 'x', 'x'};
size_t output_len = chinese_text_encode("小丑牌", output, sizeof(output));
assert(output_len == 2);
assert((unsigned char)output[0] == 0xC2);
assert((unsigned char)output[1] == 0x80);
assert(output[2] == '\0');
}
int main(void)
{
test_ascii_is_unchanged();
test_supported_chinese_uses_extended_glyphs();
test_unsupported_unicode_becomes_question_mark();
test_all_custom_glyphs_round_trip_through_tonc_utf8();
test_small_buffer_does_not_split_utf8_sequence();
printf("Chinese text tests passed.\n");
return 0;
}
+1
View File
@@ -17,6 +17,7 @@ run_test() {
}
run_test bitset
run_test chinese_text
run_test list
run_test pool
run_test util