Compare commits

...

252 Commits
jokers ... main

Author SHA1 Message Date
MeirGavish
461584b674
Music - up to measure 46 (#322)
Some checks failed
Deploy Nightly / Run Build ROM Workflow (push) Has been cancelled
Deploy Nightly / Deploy nightly (push) Has been cancelled
* Music - Added portamento and note stops to the lead melody

* Added OpenMPT .bak files to .gitignore

* Music - Expanded the module 2x for more accurate quasi-triplet + added 2-3 measures

* Music - Added pitch bend and fixed decorative note

* Music - Added measures 31-32

* Fixed wrong G/D chord sample

* Music - added measures 37-38

* Music - Added echo quasi-square

* Music - layered the quasi-square with the keyboard

* Music - small reduction to quasi-square volume

* Music - more volume adjustments

* Music - added measures 39-32 (triad chords)

* Music - raised the triad chords one octave

* Added missing repetition measures 33-36

* Music - More repetition measures 43-46

* Music - fixed pitch bend in measure 30

* Music - Added tremolo to the lead synth

* Revert "Music - Added tremolo to the lead synth"

This reverts commit 3ffee46845.

* Music - reduced the triad chords by an octave

* Music - Added lead melody fade-out and reduced triad chords volume

* Music - Added channel 7, and unused channel 8

* Increased the triad chords volume back

* Music - Added panned backing chords (used all 8 channels)

* Music - Added all the panned backing chords

* Music - fixed last echo note

* Music - Added background fifth drown to triad chords

* Music - reduced panning of panned chords

* Updated music arrangement credits in README
2026-03-09 11:17:17 +02:00
MeirGavish
1d71be674d
Changed the capitalization of hand names and centered them (#391) 2026-03-08 15:31:19 +02:00
MeirGavish
45b2275b68
Fixed round counter not resetting on game over + removed unused variable (#394)
* Fixed round not resetting on game over + removed unused variable
2026-03-07 11:45:49 +02:00
Elliot Tester
2e6e033f1f
Cleaned up Disclaimers (#386)
Some checks failed
Deploy Nightly / Run Build ROM Workflow (push) Has been cancelled
Deploy Nightly / Deploy nightly (push) Has been cancelled
* Cleaned up Disclaimers

* Apply disclaimer wording suggestion from @MeirGavish


---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2026-03-02 19:42:16 +02:00
MeirGavish
25b706789d
Updated CONTRIBUTING.md with scope and art sections (#388)
* Updated CONTRIBUTING.md with scope and art sections

Added sections on project scope and art contribution guidelines.
2026-02-26 15:47:20 +02:00
MeirGavish
37a29440de
Sprite documentation addendum note (#381) 2026-02-11 23:42:10 -08:00
Rickey
0bc140b4e8
Document sprite.h w/ minor optimizations / fixes (#379)
* Rename Sprite to SpriteInfo

---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2026-02-11 16:55:56 +02:00
MeirGavish
b292d227f9
Fix round start top left blind panel animation background (#377)
* Refactored helper sides copying code in expand 3x3 code

* Implemented main_bg_se_copy_expand_3w_row()

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-31 08:13:54 +02:00
MeirGavish
0f9ebd56f6
Changed L and R keys to play and discard respectively (#374)
* Changed L and R keys to play and discard respectively, removed sorting key in favor of on-screen buttons

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 19:08:47 +02:00
Geralt
fe4820cf65
Fix cards in deck count shifting when too few cards remain (#370)
* fix

* fix deck size/max size text not getting erased

* add TODO for future deck size overflow fix

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2026-01-26 09:22:20 +02:00
Geralt
339d8dc79f
Fix selecting a card by accident when moving too fast on round start (#369)
* fix bug

* make timers ints instead of uints

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2026-01-26 05:36:31 +02:00
MeirGavish
97bf8b3efd
Added sort by rank/suit buttons (#351)
* Added definitions for sorting buttons (cherry pick, does not compile)

* Re-applied changes lost in merge

* Added sort buttons functionality (cherry-pick, has issues)

* Fixed sort buttons issue

* Removed stray code from merge

* clang-format

* Removed code from merge error

* Removed unused function from merge error

* Removed "E" typo from "SUIT" button

* Changed all the 2x5 S's in the UI to be 1 pixel wider at 3x5

* Revert "Changed all the 2x5 S's in the UI to be 1 pixel wider at 3x5"

This reverts commit f70e0d5ab7.

* Changed the width of the "SUIT" button to be consistent with the "RANK" button
2026-01-24 10:06:51 +02:00
Geralt
02799ddc54
Optimize "whole hand" conditions checks for Joker effects (#213)
* Introduce a ContainedHandTypes type

* Separate computing of hand_type and _contained_hand_types variables

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2026-01-22 14:22:48 +02:00
MeirGavish
8e1867d235
Switch example gameplay gif to updated video in README (#366) 2026-01-19 09:22:42 +02:00
MeirGavish
fd2235d2f6
Update control instructions in README.md (#365) 2026-01-19 09:22:09 +02:00
Emma
fc5767c96e
Reset selection_y after blind skip (#362) 2026-01-16 08:11:47 +02:00
MeirGavish
bb82ab69af
Encoded palette indexing in background pngs (#350) 2026-01-15 17:09:04 -08:00
MeirGavish
5300846f73
Refactored to use selection grid during round (#348)
* Added wrapping to selection grid

* Added return value to on_selection_changed to allow aborting on wrap

* Extracted button code to button.c/.h

* Bounds checking changes for selection_grid_move_selection_horz()

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Rickey <ric@rf3.xyz>
2026-01-15 20:32:09 +02:00
MeirGavish
b7cc3ec5d6
[Optimization] Slow score lerping from 60 to 30 FPS (#336) 2026-01-14 16:43:53 -08:00
Horváth Balázs
11dafe26ac
Fix Four Fingers joker effect persisting after game over (#359)
* Fix four fingers joker effect stuck after game over

* Fix game_over_on_exit() not removing owned jokers
2026-01-11 23:18:40 +02:00
MeirGavish
d56528bc5a
Changed to custom Balatro font by @MathisMartin31 (#340)
* Changed to custom Balatro font by @MathisMartin31

* Made tweaks to the font

* Tweaked F and y
2026-01-07 17:54:43 -08:00
MeirGavish
db9879b5b5
Added blind score requirement and reward in blind select screen (#282)
* Added blind select requirement and rewards printing

* Broke game_blind_select_print_blinds_reqs_and_rewards() into separate functions
2026-01-06 11:06:24 +02:00
MeirGavish
34889988a9
Added documentation to get_played/hand_distribution (#343)
* Added documentation to get_played/hand_distribution

* Documented get_played_distribution() behavior
2025-12-28 11:20:30 -08:00
MeirGavish
6575fc7f94
Update the truncate number function to use decimal point (#333)
Some checks failed
Deploy Nightly / Run Build ROM Workflow (push) Has been cancelled
Deploy Nightly / Deploy nightly (push) Has been cancelled
* Fixed to truncate as "1.234M" instead of "1234K"

* Optimized by switching to strings to avoid divisions

* Changed FP constants to strings

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 07:52:30 +02:00
MeirGavish
f76c25614c
Change selection grid to maintain relative horizontal position when switching rows (#329)
* Change selection grid to maintain relative horizontal position when switching rows

* clang-format

* Fixed joker selling index error

* Protect against division by 0 and removed redundant fixed point conversions.
2025-12-21 22:00:08 -08:00
Rickey
955a0942f1
Add contributing guide (#334)
* Add contributing guide

* small format changes for CONTRIBUTING.md

* Update CONTRIBUTING.md

Co-authored-by: MeirGavish <meir.gavish@gmail.com>

* Update CONTRIBUTING.md with more clang-format

* Move stuff around for CONTRIBUTING

* Update VScode instructions

* Remove recommendation for formatOnSave

* Update the clang-format thing again

* Update CONTRIBUTING.md

Co-authored-by: MeirGavish <meir.gavish@gmail.com>

---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-12-21 17:27:03 -08:00
MeirGavish
8fb0813cf5
Fixed incorrect swapped font mapping for .2 and .3 (#332) 2025-12-19 12:18:33 -08:00
Rickey
1da8e0332e
document splash_screen.h (#331) 2025-12-19 11:22:21 +02:00
Rickey
628c0c992d
Introduce decimal point font (#314)
* Introduce decimal point font

* Add png of font

* Add getting a decimal point str from int

* Move font out of utils

* Clang format

* set Readme.md as main page in docs

* Add font generator

* kind of fix font

* Fix image oddities

* Added pip install pillow to build_ci_workflow

* Added ensurepip

* Update Docker/CI for font generation

* Fix CI

* Fix CI forreal

* Another CI fix

* Fix directory name for rom

* Adjust directory

* Dummy commit for test

* Revert "Dummy commit for test"

This reverts commit b607085c13.

* dos2unix and update .gitignore

* Delete the generated font file

* Delete the generated font file

* remove dot from font

* Update include/font.h

Co-authored-by: MeirGavish <meir.gavish@gmail.com>

---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-12-18 18:19:01 -08:00
Geralt
faa75a4eea
Swap Cards in Hand (#280)
* card sorting

* Added refactor from @MeirGavish

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-18 15:14:28 +02:00
Rickey
9a2d128e87
Fix url for nightly (#323) 2025-12-18 05:36:33 +02:00
MeirGavish
34419a589f
Music - Fixed wrong G/D chord sample (#321)
* Fixed wrong G/D chord sample

* Added openmpt .bak files to .gitignore
2025-12-16 22:47:58 +02:00
Copilot
b146bb667b
Add parentheses to timer macro expressions (#319)
---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MeirGavish <34395426+MeirGavish@users.noreply.github.com>
2025-12-16 11:43:07 +02:00
MeirGavish
e90f23661d
Sound effects for score accumulation and blind menu pop-up (#313) 2025-12-16 10:32:04 +02:00
MeirGavish
177a3a9741
Fixed bug where the mult wasn't truncated (#315) 2025-12-16 09:53:50 +02:00
MeirGavish
f095bf7024
Added documentation for selection_grid.h (#309)
* Added documentation for selection_grid.h

* Fix clang-format

* Update include/selection_grid.h

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

* Added NULL-checks for selection_grid_move_selection_horz/vert()

---------

Co-authored-by: Rickey <ric@rf3.xyz>
2025-12-15 21:16:25 -08:00
MeirGavish
81e6b7ab31
Button sound effect (#312)
* Removed magic number for CARD_FOCUS_SFX_PITCH_OFFSET_RANGE
2025-12-13 16:10:15 +02:00
MeirGavish
8a9750b0b3
Used proper text centering for price printing and consolidated functions for printing and erasing (#308) 2025-12-13 09:03:06 +02:00
MeirGavish
ae5369b8fe
Added C standard gnu23 to the makefile (#288)
* Added C standard gnu23 to the makefile

* Added gnu23 standard to all test modue makefiles

* Added installation of latest gcc version to tests workflow

* Fixed typo in previous commit

* Made GCC 14 installation conditional and moved from the workflow to the script

* Extracted GCC version check to external script for CI and created tests README

* Added #!/bin/bash

* Make check_gcc_version.sh executable
2025-12-12 18:46:03 -08:00
MeirGavish
9bffe5c9a9
Refactor - remove selected from SpriteObject (#305) 2025-12-12 18:44:26 -08:00
Tygyh
440efe1ce1
Update .gitignore with .idea and codeQL directories (#310) 2025-12-12 12:26:33 +02:00
MeirGavish
16b24a0bf9
Updated splash screen with new organization URL (#306) 2025-12-10 23:00:46 -08:00
Marcel Schulz
f9ed858549
[Fixed] Joker shop price not properly erased on purchase (#286) 2025-12-11 08:39:09 +02:00
MeirGavish
1e1fe85b88
Merge pull request #227 from GBALATRO/music_relayer_chords
Music - Relayered chord samples
2025-12-11 07:02:20 +02:00
MeirGavish
0698d27ce9 Music - Switched to 44.1kHz chord samples 2025-12-11 07:00:38 +02:00
MeirGavish
a4e1fab100 Music - Switched chords to C-based samples (+reused some frames instead of copy-pasted ones) 2025-12-11 07:00:38 +02:00
MeirGavish
d22fcb0056 Music - Relayered chord samples 2025-12-11 07:00:38 +02:00
MeirGavish
d96bc9b1c2
Renamed GBLA_UNUSED -> GBAL_UNUSED and updated RowOnKeyTransitFunc documentation (#292) 2025-12-10 17:17:28 -08:00
MeirGavish
5a4856ec82
Refactor - removed tile_mem obj charblock index magic number (#293) 2025-12-10 17:13:59 -08:00
Rickey
2fb9e6d633
Clang format joker files and def files (#257)
* Clang format joker files and def files

* Fix blind usage of GBLA_UNUSED macro

* Add final files for clang-format (main.c and selection_grid.h)

---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-12-10 09:41:24 +02:00
Rickey
35ecbf33ae
Format and document utility files (#246)
Some checks failed
Deploy Nightly / Run Build ROM Workflow (push) Has been cancelled
Deploy Nightly / Deploy nightly (push) Has been cancelled
* Format and document utility files

* Add Doxyfile

* small updates for cleanup

* Final touches for util docs

* Replace `_` with `s_` for static functions

* update for clang-format changes

* Update include/audio_utils.h

Co-authored-by: MeirGavish <meir.gavish@gmail.com>

* Apply suggestions from code review

Co-authored-by: MeirGavish <meir.gavish@gmail.com>

* Update include/audio_utils.h

Co-authored-by: MeirGavish <meir.gavish@gmail.com>

* Update include/graphic_utils.h

Co-authored-by: MeirGavish <meir.gavish@gmail.com>

* Make clang-format happy :)

* Update gitignore for doxygen

* restor graphic_utils.c to main

* update graphics utils

* tmp

* tmp

* get it to compile

* fix some bad merge mistakes

* Fix clang format

* Apply suggestions from code review

Co-authored-by: MeirGavish <meir.gavish@gmail.com>

* make clang-format happy

---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-12-07 12:56:39 -08:00
MeirGavish
7bd510ee5f
Update README for consistency in capitalization 2025-12-07 15:37:28 +02:00
MeirGavish
cf746c597e
Update README with joker swap controls
Added controls for swapping jokers in the shop.
2025-12-07 15:36:43 +02:00
Geralt
bf66f96844
Add swapping order of Owned Jokers (#258)
* swap Jokers

* Hide score properly

* add key_released to inputs triggering selection_grid process input

* format code

* Add suggestions and bug fixes from @MeirGavish

* Add comment

* format selection_grid

* toto

* format

* format

* Turned clang-format off for selection grid rows

* revert reroll bug fix, already adressed

* format selection_grid.c

* Move conditions around in game.c

* typo

* Attempt at fixing braced initializer indent format

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-12-07 02:58:50 -08:00
Rickey
a82bd671de
Separate clang-format test into its own workflow (#283)
* Rework workflow dependencies
2025-12-06 23:59:55 +02:00
Geralt
687d77e18a
Improve Face Cards sprite art (#284)
* Tweaks by @MeirGavish

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2025-12-06 16:33:23 +02:00
MeirGavish
28ff6c81ef
Fixed bug where reroll button is triggered by any key (#279) 2025-12-05 10:06:43 +02:00
MeirGavish
9f170f298d
Restored removed joker shop availability check function (#276) 2025-12-04 18:21:10 -08:00
Rickey
291edaf4f2
Clang format and refactor game.h/c + update Bracket Alignment to BlockIndent (#259)
* Refactor and clang-format game.h/c

* Updated clang-format AlignAfterOpenBracket: BlockIndent and ColumnLimit: 100 and refactored all files accordingly

---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-03 13:49:31 +02:00
MeirGavish
43d837571e
Truncate Scores (#269)
* Fixed score truncation erasure, and centering for chips, mult, score and "temp score

* Added centering for money

* Added tests for truncate_uint_to_suffixed_str()

* Fixed u32 bug with get_digits

* Fixed score flames for large numbers

* Small change to update documentation

* Added 1 pixel on to score rect on the left to make room for full character tile + reset_top_left_panel_bottom_row() refactor

* Replaced directional defines with enums

* Updated ante_lut comment

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-02 08:28:36 +02:00
Geralt
07cab81ba1
Fix Wrathful Joker missing pixel (#272)
Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2025-12-01 09:34:30 +02:00
Geralt
10e75f18ae
remove triboulet from code (#266)
Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2025-11-30 18:11:48 +02:00
MeirGavish
2d7394f03e
Fixed corners for Banner joker (#271) 2025-11-30 17:57:05 +02:00
Tygyh
8bba449196
Convert BG_ID_* defines to enum BackgroundId (#265)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2025-11-30 08:53:51 +02:00
Geralt
0953a430c4
Implement Seltzer and Joker Expiration effect (#252)
* Add Seltzer art

* Implement Joker expiration

* Add exception to copying Jokers + Seltzer message fix

* Move all scoring logic to joker_object_score away from check_and_score_joker_for_event

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2025-11-30 08:52:42 +02:00
Geralt
435a9a0a1d
Refactor JokerEffect scoring (#245)
* Remove usage of memcmp for JokerEffect null check

* Added suggestions from @ricfehr3

* Add suggestions from @MeirGavish


---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-11-28 19:03:47 +02:00
Rickey
1cacabdff6
Clang format pool (#255)
* Update comment prefix + add github action

* Clang Format pool

* fix clang format version

* Fix pool clang format
2025-11-27 10:41:13 -08:00
Rickey
184ee719b3
Clang format card.c/h (#256) 2025-11-26 00:52:04 +02:00
Rickey
09dd996181
Clang Format Blinds (#253)
* Quick cleanup on bitset docs

* Clang format blind.h/c

* Replace `_` with `s_` for functions

* Fix newline size for macros.

* Fix format for ci

* Add macro for "unused" attribute
2025-11-25 13:47:44 -08:00
Rickey
2c32ff69e3
Clang format hand analysis (#261)
* Clang format hand analysis

* Update source/hand_analysis.c

Co-authored-by: MeirGavish <meir.gavish@gmail.com>

---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-11-25 13:45:07 -08:00
Rickey
e910aee184
Clang format list (#254)
* Finish clang-format on lists
2025-11-25 11:32:50 +02:00
Rickey
6db35b0f95
Clang format sprite (#262) 2025-11-24 23:26:47 +02:00
MeirGavish
4a5753c068
Merge pull request #260 from ricfehr3/clang-format-splash-screen
Clang format splash screen
2025-11-24 07:58:36 +02:00
Rickey Fehr
fcb2337fa2 Clang format splash screen 2025-11-23 18:43:45 -08:00
MeirGavish
f51380c93c
Added sound effects for chips, mult, and xmult (#229) 2025-11-22 07:27:43 +02:00
Geralt
408c264e85
Modify blueprint and brainstorm implementation to fix retrigger bug (#244)
---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2025-11-21 20:09:07 +02:00
MeirGavish
4567a8e1ad
Added credits to readme (#247)
* Added credits to README.md
2025-11-21 16:04:31 +02:00
Geralt
2fb41e533c
Make Chips, Mult and Score unsigned and protect against overflow (#212)
---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2025-11-20 12:39:15 +02:00
Rickey
47b1bac31f
Add list_swap() and list_insert() functions (#232)
* Add list_swap() and list_insert() functions

* Update docs for list_swap

* Add test case and fix list insert bug

* Fix silly bug with list length on insertion

* Initial changes for pr

* Add reverse iterator and update test for it

* Cleanup iterator next function

* update tests to cover list_get_at_idx
2025-11-18 18:19:00 -08:00
Rickey
a4caa44580
Add clang format and used it for bitset (#240)
* Add clang format file .clang-format

* Use clang format on bitset


---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-11-18 08:56:32 +02:00
MeirGavish
f3f375cef1
Banner Joker art update by @GreenNinja2525 (#242) 2025-11-16 20:18:14 +02:00
Geralt
1ac55692e9
Break up the played cards scoring loop into several functions (#224)
* rename PLAY_PLAYING state into PLAY_STARTING

* make discarded_card and sound_played global variables

* Broken up cards_score state update

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-11-16 17:33:01 +02:00
MeirGavish
557e591c31
Merge pull request #223 from cellos51/integrate_photograph
Integrated Photograph joker art by @MathisMartin31
2025-11-13 08:38:28 +02:00
Geralt
1f3d3bffbd
Fix bug occurring when a retriggered card has a non-scoring card to the left (#218)
---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2025-11-12 16:06:37 +02:00
MeirGavish
28043bd7ca Integrated Photograph joker art by @MathisMartin31 2025-11-12 14:11:16 +02:00
Geralt
4183f563ab
Implement cards held in hand effects (#205)
* Adapt existing Jokers to the Held Card mechanic + effect display location

* Add suggestions from @ricfehr3

* Add suggestions from @MeirGavish

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2025-11-12 13:18:53 +02:00
Rickey
e0cffab768
Update list implementation to work with memory pools (#168)
* Introduce indexed list implementation

* Fix CI tests for pool

* Take bitset out of pool

* Replace joker bitset interactions with wrappers

* Add bitset tests

* Add test to gitignore

---------

Co-authored-by: rfehr-idexx <ric-fehr@idexx.com>
2025-11-11 17:56:21 +02:00
MeirGavish
d5f59e904b
Fixed game stuck due to interest bug + cleanup refactor (#214)
Co-authored-by: MeirGavish <34395426+MeirGavish@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MeirGavish <34395426+MeirGavish@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2025-11-07 00:33:52 +02:00
MeirGavish
64dd586df8
Optimized score lerping by changing the number of steps to a power of 2 (#211) 2025-11-06 12:15:07 +02:00
Geralt
e7eaa82c9d
Add flaming effect when exceeding score requirements (#210)
* Fix grit palette swapping

* Add suggestions from @MeirGavish

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2025-11-04 12:31:09 +02:00
Rickey
ecd98d0626
Merge pull request #208 from ffrank89/patch-1
Add gba-dev pacman installation instructions to README.md
2025-11-03 22:13:06 -08:00
Geralt
df674a73a6
Iterate over Hanging Chad"s art by @MeirGavish (#209)
Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
2025-11-03 09:34:18 +02:00
MeirGavish
247d1339c0
Update Hanging Chad art (#207) 2025-11-03 08:42:10 +02:00
MeirGavish
ada2650c50
New Run Button (#206)
* New Run Button

* Refactor - replaced cashout copies with main_bg_se_copy_expand_3x3_rect() - WIP (Palette still messed)
2025-11-03 07:34:46 +02:00
ffrank89
d8acb99665
Add gba-dev pacman installation instructions to README.md
Rearranged installation steps for clarity and added a command to install gba-dev.

I ran into the following error before running the added command:

Makefile:9: /opt/devkitpro/devkitARM/gba_rules: No such file or directory
make: *** No rule to make target `/opt/devkitpro/devkitARM/gba_rules'.  Stop.
2025-11-02 22:38:40 -05:00
MeirGavish
fbf8cba9db
Merge pull request #203 from ricfehr3/hotfix/nightly-zip-creation
Junk directories on release zip
2025-11-02 08:34:15 +02:00
Rickey
438df88316
Add download button (#196)
* Add download button

* Update README.md
2025-11-02 08:29:40 +02:00
Rickey Fehr
73abe5b6ce Junk directories on release zip 2025-11-01 21:19:00 -07:00
theturtlemafia
03273de829
Adds Shortcut and Four Fingers (#95)
* adds shortcut joker

* adds Four Fingers joker, and combination support for Four Fingers + Shortcut

---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-01 16:28:40 +02:00
Geralt
eb815b7820
Refactor of scoring flow, Joker Callbacks and Card Retriggers (#160)
* Remove Baseball Card for now, will have to wait a future PR

* Removed cards held in hand joker scoring stage - should be in another PR

---------

Co-authored-by: MonteCrysto <mathis.martin31@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-11-01 15:50:52 +02:00
Rickey
2c532b5bd8
Merge pull request #197 from ricfehr3/hotfix/fix-nightly-permissions
Fix permissions issue for nightly workflow
2025-10-31 23:37:11 -07:00
Rickey Fehr
fa52350146 Fix permissions issue for nightly workflow 2025-10-31 23:22:33 -07:00
MeirGavish
dfe6edad88
Refactor - Replaced magic number copies with main_bg_se_copy_rect() (#192) 2025-11-01 08:12:20 +02:00
Rickey
9dc0905c93
Merge pull request #195 from ricfehr3/update-issue-template
Update issue template for bug reports
2025-10-31 22:30:37 -07:00
Rickey
d32d567c2b
Add nightly rom releases (#182)
* Add nightly rom releases

* Begin testing CI

* Add tests workflow

* Move release tag workflow

* Fix upload logic for intermediate artifact

* Fix variable add names

* Update build CI

* Update nightly upload location

* Add git hash to output

* Update upload location again

* Update name to have a date

* Adjust date format for consistency
2025-10-31 22:00:15 -07:00
Rickey Fehr
4af345c567 Update issue template for bug reports 2025-10-31 20:53:54 -07:00
Elliptical
8c00fbdeb1
Update issue templates 2025-10-31 21:53:52 -04:00
MeirGavish
b472ce2bad
Changed the order of suits to match the original Balatro (#189) 2025-10-30 11:18:36 +02:00
JustinL-12
c510e78704
Implement Interest (#144)
* Implemented Interest

* Very carefully applied changes from PR #144 to implement interest and fixed magic numbers

---------

Co-authored-by: JustinL-12 <jlong30@uic.edu>
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-29 20:43:26 +02:00
JustinL-12
b38433d191
Weight jokers by rarity in the shop (#152)
* Added weight for the rarity of jokers in the shop

* Includes manual conflict resolution

---------

Co-authored-by: JustinL-12 <jlong30@uic.edu>
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-27 19:18:48 +02:00
MeirGavish
5a3e891e43
Merge pull request #178 from cellos51/allow_additional_joker_testing
Added option for a second test joker
2025-10-25 15:22:52 +03:00
MeirGavish
b01e9e5f01 Added option for a second test joker 2025-10-25 15:18:23 +03:00
MeirGavish
a083d3de74
Moved pool file out of external definition and replaced it with a flag (#176)
* Moved pool file out of external definition and replaced it with a flag
2025-10-22 22:43:02 +03:00
MeirGavish
e45c1eeb22
Merge pull request #172 from cellos51/start_music_after_splashscreen
Changed music to start after Splash Screen
2025-10-22 06:41:29 +03:00
MeirGavish
9b568e8e1a Moved undefined game state to enum end (more xmacro idiomatic) 2025-10-21 12:50:18 +03:00
MeirGavish
9c78094399 Fixed invalid access to game state array 2025-10-21 12:49:02 +03:00
MeirGavish
0b848cd72e Merge branch 'main' into start_music_after_splashscreen 2025-10-21 12:25:07 +03:00
MeirGavish
8a4242e81e
Merge pull request #132 from ricfehr3/feature/replace-malloc-with-pools
Feature/replace malloc with pools
2025-10-21 12:03:52 +03:00
Rickey
52b1af576f
Update source/pool.c
Update for PR fix magic number

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-10-20 18:07:48 -07:00
Rickey Fehr
517d40c294 Cleanup for PR, return old sprite tracker 2025-10-19 15:52:36 -07:00
MeirGavish
462c040e9a Changed the check in game_change_state() to full bounds checking 2025-10-19 11:45:48 +03:00
Rickey Fehr
e40f3c989f Fix memory map script 2025-10-18 17:35:53 -07:00
Rickey Fehr
af3d69bcfb Rename def files 2025-10-18 17:28:35 -07:00
Rickey Fehr
a5c5f9cb15 Add timings to memory test 2025-10-18 17:23:59 -07:00
Rickey Fehr
ab07c9af04 Hotfix going out of bounds 2025-10-18 17:09:22 -07:00
Rickey Fehr
09f6c35496 Fix slight bug with false positives on pool test 2025-10-17 18:08:20 -07:00
Rickey Fehr
138792df48 Add memory pool test, fix bugs 2025-10-17 18:05:36 -07:00
Rickey Fehr
16059ab632 Update for PR
Readd sprite tracker to not reuse indices
check for capacity when returning pool entries
Update script for printing memory map
2025-10-17 16:55:09 -07:00
Rickey Fehr
d499abe2c4 Cleanup for PR 2025-10-17 16:55:09 -07:00
Rickey Fehr
d78e04c6b5 Fix style, tiny error, and typo 2025-10-17 16:55:09 -07:00
Rickey Fehr
5e2bd9a59a Update memory map script and cleanup 2025-10-17 16:55:09 -07:00
Rickey Fehr
a1e73be3fc Update github actions to print the memory map 2025-10-17 16:55:09 -07:00
Rickey Fehr
06cd13a17f Centralize location of memory pools
This commit centralizes the location of memory pools and adds an
associated script (scripts/get_memory_map.sh) to read the memory map.
2025-10-17 16:55:09 -07:00
Rickey Fehr
72c0a6c4a9 Optimize bit operations, style fix, cleanup 2025-10-17 16:55:07 -07:00
Rickey Fehr
e00c51502b Replace Malloc With Pools
This commit introduces an object pool and replaces instances of malloc with pools where sensible.
2025-10-17 16:54:09 -07:00
MeirGavish
53d2e53d78 Changed music to start after Splash Screen 2025-10-17 13:43:21 +03:00
MeirGavish
f725c5e1b8
Merge pull request #155 from drewjohnson2/stateRefactor
State refactor
2025-10-17 11:51:24 +03:00
Drew
d6ebda657a Additional PR feedback 2025-10-16 14:48:59 -05:00
Drew
f77a715845 Some PR feedback 2025-10-14 23:08:42 -05:00
Drew
3a6e004fc6 Rebase and some PR feedback 2025-10-14 10:44:21 -05:00
Drew
b9af95d770 Fixing some constant names after a rebase. 2025-10-14 10:26:09 -05:00
Drew
597dc62e24 Rebasing onto main. Switching to KEY_ANY to restart after a game over 2025-10-14 10:26:09 -05:00
Drew
0f154741ed Renaming a function, fixing a call to memcpy16. Replacing magic numbers with enum values 2025-10-14 10:26:09 -05:00
Drew
3071e512c4 Just some cleanup 2025-10-14 10:26:09 -05:00
Drew
dbef1dacdc Added the ability to restart the game after a game over.
The function that handles this is the on_exit of the game lost state. This function is not meant to
be permanent, but is just a placeholder while we decide what we actually want to do when game over is
reached.
2025-10-14 10:26:05 -05:00
Drew
6d66ccbb61 Replaced the substate switch statements with function tables. 2025-10-14 10:24:42 -05:00
Drew
4e1f418a8d Preparing for rebase 2025-10-14 10:20:43 -05:00
Drew
fb35397238 Cleanup 2025-10-14 10:20:43 -05:00
Drew
04ffb0d4dd A little bit of scope creep here.
Renamed some of the state functions to include the word 'on'. Also
replaced the switch statement in set_hand() with a lookup table
access. This lookup table contains the chips, mult and display name
for a given hand. Some may not like these changes, so we can easily
roll back if need be.
2025-10-14 10:20:43 -05:00
Drew
121bbc01d5 Refactored state machine.
Added a state info map which keeps track of a particular state's on_init, on_update and on_exit functions.
A few of the on_update functions have sub-states, which create extremely long switch statements.
Not sure if perhaps the best route would be to keep the switch statements but break the cases into individual
functions or if we should keep track of substates. The latter sounds like it could get complicated and really
messy quick. Problem for another day.
2025-10-14 10:20:40 -05:00
MeirGavish
6dce57e197
Merge pull request #145 from pansythoughts/main
kind of fixed how game_speed behaves
2025-10-14 13:57:40 +03:00
MeirGavish
b5b7c856d6
Merge pull request #133 from ricfehr3/refactor-blinds
Cleanup, Refactor blinds into a map
2025-10-14 13:46:27 +03:00
Rickey Fehr
ef048791c1 Separate out graphics and add ability change boss 2025-10-13 23:36:27 -07:00
Rickey Fehr
f885fa9164 Fix Iterating over every blind instead of 3 max 2025-10-13 23:36:27 -07:00
Rickey Fehr
54d955d1e7 Update for PR 2025-10-13 23:36:27 -07:00
Rickey Fehr
3c2dd0edba Rename blind enums to represent their type
This commit prefixes the enums `BlindType` and `BlindState` with their
types for clarity
2025-10-13 23:36:27 -07:00
Rickey Fehr
7ee07bb34c Refactor blind into a map 2025-10-13 23:36:27 -07:00
alex
f463ba99a0 changed how game_speed affects hand score animations 2025-10-13 20:28:52 -07:00
alex
767e21d5fa made the lerped temp score expression clearer 2025-10-13 20:26:20 -07:00
alex
d108836b6a changed how game_speed affects hand score animations 2025-10-13 20:24:20 -07:00
MeirGavish
f36fa5e998
Merge pull request #169 from cellos51/implement_joker_testing
Implement joker testing
2025-10-13 21:27:42 +03:00
MeirGavish
71de180871 Merge branch 'main' into implement_joker_testing 2025-10-13 20:59:17 +03:00
MeirGavish
8c25089966 Allow defining an ID for a joker to always appear in shop and be tested 2025-10-13 20:58:44 +03:00
alex
56c33b2eef made the lerped temp score expression clearer 2025-10-11 23:45:02 -07:00
alex
cf7c9dc7c3 fixed typo 2025-10-11 13:36:43 -07:00
MeirGavish
d7f64d3570
Merge pull request #166 from cellos51/integrate_joker_art
Added art for Jokers Bootstraps by @MathisMartin31 and @noctowlhoot and Pareidolia by @NoWhammies10 and @MeirGavish (jokers rearranged for palette optimization)
2025-10-09 23:54:56 +03:00
MeirGavish
9d1a51884f Added art for Jokers Bootstraps by @MathisMartin31 and @noctowlhoot and Pareidolia by @NoWhammies10 and @MeirGavish (jokers rearranged for palette optimization) 2025-10-09 23:42:07 +03:00
MeirGavish
609e1f19b6
Merge pull request #159 from MathisMartin31/main
Add Pareidolia and Acrobat Jokers
2025-10-09 20:43:36 +03:00
Geralt
ce5150bffb
Update joker_effects.c
Temporarily remove Pareidolia from shop until @MeirGavish can add the artwork for it
2025-10-09 18:57:10 +02:00
Geralt
ce6d2049f8
Update game.h
Fix missing `is_joker_present` rename into `is_joker_owned`
2025-10-09 14:52:41 +02:00
Geralt
f14f200cb0
Update joker_effects.c
Remove the "unused" attribute from joker effect functions that was added by mistake during merge
2025-10-09 14:50:18 +02:00
Geralt
c4f22be9b9
Update joker.h
Restoring the Brainstorm Joker ID, deleted by mistake while adding the Pareidolia Joker ID
2025-10-09 14:46:57 +02:00
MeirGavish
30638b65e2
Merge pull request #164 from ricfehr3/hotfix/hand-input-order
Fix softlock related to #163
2025-10-09 15:16:56 +03:00
MeirGavish
94cb898bb9
Merge pull request #134 from drewjohnson2/magic_numbers
Magic numbers
2025-10-09 11:18:02 +03:00
Rickey Fehr
73ec9a40d3 Fix softlock related to #163 2025-10-08 20:29:11 -07:00
Geralt
6787fab0c2
Merge branch 'main' into main 2025-10-08 23:25:24 +02:00
Drew
f752793caa Shop light specific macros for color 2025-10-08 16:16:23 -05:00
MeirGavish
9b6c77ea3f
Merge pull request #119 from Nanrech/pr-fix
Add blueprint and brainstorm jokers
2025-10-08 22:46:01 +03:00
MeirGavish
4b35b2f7d8 Added art for jokers Blueprint and Brainstorm by @NoWhammies10 and @GreenNinja2525 2025-10-08 22:40:15 +03:00
Nan
23a14cb76e
Update source/joker_effects.c
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-10-08 20:41:09 +02:00
Nan
8ae4968539 Remove duplicated entries 2025-10-08 18:52:10 +02:00
Nan
9ae53bddb8 Fix errors introduced by latest commits 2025-10-08 18:39:49 +02:00
Nan
af72fe5749 Fix conflicts
Attempt 1
2025-10-08 18:21:55 +02:00
Nan
7e17419811
Merge branch 'main' into pr-fix 2025-10-08 18:11:20 +02:00
Nan
fec243b482
Update source/joker_effects.c
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-08 17:59:27 +02:00
Nan
324478845d
Update source/joker_effects.c
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-10-08 17:58:11 +02:00
Nan
0dbde481d5
Update source/joker_effects.c
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-10-08 17:57:50 +02:00
Nan
bc44de0493
Update include/joker.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-08 17:57:20 +02:00
MeirGavish
f257ec03af
Merge pull request #150 from ricfehr3/docs/update-docs-docker
Update docs for docker
2025-10-08 10:32:11 +03:00
Drew
4e0d72ca8a PR Feedback 2025-10-08 01:27:15 -05:00
MonteCrysto
aa5ebd13fc Add suggestions from @MeirGavish 2025-10-08 08:14:54 +02:00
Rickey Fehr
cca015238d Update docs for docker 2025-10-07 20:30:11 -07:00
MeirGavish
296c489562
Merge pull request #162 from cellos51/integrate_joker_art
Integrate joker art
2025-10-08 00:45:09 +03:00
MeirGavish
ae40949008 Fixed Bull border corner transparency 2025-10-08 00:18:30 +03:00
MeirGavish
77f76bd4ef Removed "unused" attribute from integrated jokers 2025-10-08 00:05:37 +03:00
MeirGavish
a0f1ef7780 Added art for xmult hand jokers The Duo The Trio, The Family, The Order, and The Tribe by @MathisMartin31 2025-10-07 23:52:49 +03:00
MeirGavish
73c2db6b69
Merge pull request #143 from ricfehr3/feature/ci-release-on-tag
Add uploading ROM on tag push
2025-10-07 23:04:19 +03:00
MeirGavish
c529c4be69 Added art for jokers Abstract Joker by @MathisMartin31 and Bull by @NoWhammies10 and @GreenNinja2525 2025-10-07 21:26:43 +03:00
MonteCrysto
8cd86f1640 Merge remote-tracking branch 'upstream/main'
And add NULL Acrobat Joker effect
2025-10-07 20:18:41 +02:00
MeirGavish
46936c1878 Added art for jokers Raised Fist by @MathisMartin31 and Reserved Parking by @NoWhammies10 2025-10-07 21:13:31 +03:00
MeirGavish
22ddd83931
Merge pull request #138 from Seafoamm/main
Add macOS setup instructions
2025-10-07 16:20:01 +03:00
MeirGavish
f7c68934f7
Merge pull request #146 from noctowlhoot/main
Added more Jokers all outside of scope, some with art available already from MathisMartin31
2025-10-07 13:01:48 +03:00
MeirGavish
4fc0fbf5a8
Merge pull request #139 from mannodermaus/wrap-hand-input-on-oob-movement
Wrap around hand selection when going out of bounds horizontally
2025-10-07 12:56:53 +03:00
noctowlhoot
0187b5f78e
Update source/joker_effects.c
Co-authored-by: MeirGavish <meir.gavish@gmail.com>
2025-10-07 20:24:00 +10:30
MonteCrysto
0cfe0e5126 Added forgotten base price for the Acrobat Joker 2025-09-30 23:51:46 +02:00
MonteCrysto
b7a94fa085 Fix some issues (I actually forgot to compile) 2025-09-30 23:34:11 +02:00
MonteCrysto
de7c9d4d60 Add a "get_num_hands_remaining" function and implement the Acrobat Joker 2025-09-30 23:06:13 +02:00
MonteCrysto
c781ee9666 Add a "is_card_face" function to take into account if Pareidolia is present 2025-09-30 22:47:00 +02:00
Drew
1f40279a36 Created a define for a screen entry horizontal flip. 2025-09-30 00:09:53 -05:00
Drew
a6dd95ab0a Added a few more defines 2025-09-28 16:03:04 -05:00
Drew
cc95554db7 Added constants for timer values. The names on these might be bad. 2025-09-28 16:03:04 -05:00
Drew
34d885674c Finished adding palette memory indexs to an enum 2025-09-28 16:03:04 -05:00
Drew
99ecd424f4 Added enum values for some of the palette bank indexes 2025-09-28 16:03:04 -05:00
MeirGavish
f579c13ceb
Merge pull request #149 from GreenNinja2525/main
Fix Dynamic Badges (Hopefully) [Now Safe to Merge]
2025-09-27 21:44:27 +02:00
MeirGavish
8dbcadac59
Merge pull request #153 from ricfehr3/feature/add-version
Add git hash baked into ROM for bug reports
2025-09-27 21:41:09 +02:00
Wheeler
46944e5699
Merge branch 'cellos51:main' into main 2025-09-27 15:22:41 -04:00
Rickey Fehr
88726cbb39 Add script to get version 2025-09-24 20:49:01 -07:00
Rickey Fehr
36bcf0fa30 Move version into C file 2025-09-24 20:44:13 -07:00
noctowlhoot
b8a441f971
Update joker_effects.c
updated jokers into joker registry
2025-09-24 22:53:54 +09:30
Rickey Fehr
56bbc2ff9a Add defaults for GIT_HASH and GIT_DIRTY 2025-09-23 21:41:55 -07:00
Rickey Fehr
ebe789aa52 Add git hash baked into ROM for bug reports 2025-09-23 16:15:50 -07:00
alex
daba462575 fixed a division thingy 2025-09-23 12:45:09 -07:00
alex
ad9358547d main 2025-09-23 12:44:03 -07:00
alex
f57c37b900 fixed a divison thingy 2025-09-23 12:40:26 -07:00
alex
7bafc260dd fixed game_speed, now using fixed point and keeping game_speed static 2025-09-22 22:04:51 -07:00
Wheeler
3888a198c8
Change to Shields.io Badges
Hopefully this allows them to update properly. Shields.io is way more reliable and updates on its own. Hopefully this works!
2025-09-22 22:38:57 -04:00
Rickey Fehr
e58208e13d Add uploading ROM on tag push 2025-09-22 14:27:50 -07:00
MeirGavish
74739d87ed
Merge pull request #123 from drewjohnson2/main
Adding an x macro to cut down on some boilerplate.
2025-09-22 23:15:56 +02:00
Drew
e620697917 Renamed joker_table.h to def_joker_gfx_table.h. Removed an unnecessary constant 2025-09-22 16:06:45 -05:00
Wheeler
8d278b0c19
Fix Dynamic Badges (hopefully)
I think I fixed the dynamic badges, it just needed a maxAge parameter to properly tell GitHub to recache the data. I can't really test this as it is set to refresh each badge every 2 hours. I also fixed some minor formatting in the build instructions. I also added a discussions badge so people can read through them before contributing.
2025-09-22 12:44:20 -04:00
noctowlhoot
26db4e1aa0
Update joker_effects.c
fixed triboulet - was triggering for jacks also, rem
2025-09-22 21:42:15 +09:30
noctowlhoot
66c6b91222
Merge pull request #1 from noctowlhoot/noctowlhoot-patch-1
Update joker_effects.c
2025-09-22 20:54:44 +09:30
noctowlhoot
591485a5ac
Update joker_effects.c
I was unsure of the significance of the number immediately after joker rarity in the joker registry, so I haven't put the joker effects in there. If anyone can tell me what the significance of that number is I can add them in :)
2025-09-22 20:49:35 +09:30
alex
301f5a2a53 kind of fixed how game_speed behaves 2025-09-21 23:49:24 -07:00
alex
6e60cf5970 kind of fixed how game_speed behaves 2025-09-21 23:16:50 -07:00
Marcel Schnelle
a9f44e74d4 Wrap around hand selection when going out of bounds horizontally 2025-09-21 17:50:51 +09:00
Seafoamm
2c2cb17f74 Add macOS setup instructions 2025-09-21 00:25:51 -07:00
MeirGavish
9dc5f7ccc3
Merge pull request #116 from HeadedBranch/main
Add install instructions for linux
2025-09-18 21:44:30 +02:00
Drew
7c377fca94 Making joker_gfxTiles and joker_gfxPal static 2025-09-17 15:35:21 -05:00
Drew
c4e804234b Moving macro into pool definition 2025-09-17 15:01:26 -05:00
Drew
3dc959c17d Converting joker_gfxTiles and joker_gfxPal to pools 2025-09-17 14:35:00 -05:00
Drew
2c5bc3b5dd Adding an x macro to cut down on some boilerplate.
This should make adding additional jokers require a little less
typing. Additional jokers will need an entry in /include/joker_table.h
2025-09-15 21:36:20 -05:00
MeirGavish
27e91fb4a4
Merge pull request #121 from drewjohnson2/main
Removed redundant copy of shadow buffer into OAM.
2025-09-15 23:50:50 +03:00
Nan
2669afbae2 Merge branch 'main' of https://github.com/cellos51/balatro-gba 2025-09-15 21:46:48 +02:00
MeirGavish
a16065439e
Merge pull request #117 from ricfehr3/main
Add docker support
2025-09-15 22:30:21 +03:00
Rickey Fehr
5b76cbfad0 Add cache for docker compose container 2025-09-14 15:15:42 -07:00
Rickey Fehr
074f1956aa Update Github Actions CI to use Docker Compose 2025-09-14 15:15:10 -07:00
Drew
e8e945b17d Removed redundant copy of shadow buffer into OAM. 2025-09-14 13:50:38 -05:00
Nan
35c0d383c5 Add blueprint and brainstorm jokers
Implements blueprint and brainstorm to the game, but leaves the implementations commented out as they are still missing assets.
2025-09-13 22:18:31 +02:00
Rickey Fehr
b27c94e5bd Add docker support
Add docker support via a docker compose file with included instructions.
2025-09-11 12:59:52 -07:00
Isaac Hesslegrave
1562e4f9c7
Add step to activate correct shell variables 2025-09-11 08:36:37 +00:00
Isaac Hesslegrave
c2ef5b15d4
Add install instructions for linux 2025-09-11 08:27:42 +00:00
121 changed files with 15281 additions and 3892 deletions

56
.clang-format Normal file
View File

@ -0,0 +1,56 @@
BasedOnStyle: LLVM
IndentWidth: 4
UseTab: Never
# Allman brace style
BreakBeforeBraces: Allman
AllowShortBlocksOnASingleLine: false
AllowShortIfStatementsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortLoopsOnASingleLine: false
# Pointer alignment
PointerAlignment: Left
# Column limit
ColumnLimit: 100
# Case labels
IndentCaseLabels: true
# Include sorting
SortIncludes: CaseSensitive
IncludeBlocks: Regroup
ContinuationIndentWidth: 4
AlignArrayOfStructures: Left
PackConstructorInitializers: Never
SpaceBeforeParens: Custom
SpaceBeforeParensOptions:
AfterControlStatements: true
AfterFunctionDefinitionName: false
AlignOperands: AlignAfterOperator
BreakBeforeBinaryOperators: None
IndentPPDirectives: None
AlignConsecutiveMacros: Consecutive
BinPackArguments: false
BinPackParameters: false
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AlignAfterOpenBracket: BlockIndent
PenaltyBreakBeforeFirstCallParameter: 0
ReflowComments: true
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: 1
PenaltyReturnTypeOnItsOwnLine: 99999999
AlignEscapedNewlines: Left
BracedInitializerIndentWidth: 0

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees:
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
Any helpful screenshots or videos
**What version**
- **Nightly**: If you downloaded the ROM you can provide either the ROM name, or the name of the zipfile
- **Mainline Release**: Please provide the name of the release (**Joker Build**, **Itemless Build**, etc)
- **I Don't Know**: Please try to use the `build_bug_report.py` script in the `scripts/` directory. If there is no output from that, then try to recreate your bug with a new version.
**What Emulator or Platform were you playing on**
- e.g. mgba, flashcart
**Additional context**
Add any other context about the problem here.

View File

@ -2,60 +2,77 @@ name: Build CI
on:
push:
branches:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
inputs:
clear-cache:
description: 'Clear docker cache before running'
type: boolean
default: false
workflow_call:
inputs:
upload-build-artifact:
required: true
type: boolean
default: true
jobs:
build:
runs-on: ubuntu-latest
clang-format-job:
name: Run clang-format test
uses: ./.github/workflows/clang_format_ci_workflow.yml
unit-tests-job:
name: Run unit tests
uses: ./.github/workflows/unit_tests_ci_workflow.yml
build:
name: Build ROM
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set devkitPro environment variables
run: |
# Installing to home directory to avoid permission denied on caching
echo "DEVKITPRO=/home/runner/devkitpro" >> $GITHUB_ENV
echo "DEVKITARM=/home/runner/devkitpro/devkitARM" >> $GITHUB_ENV
export PATH=$DEVKITARM/bin:$DEVKITPRO/tools/bin:$PATH
# Caching to avoid excessive request to the devkitpro server from a runner because they
# eventually block and result in permission denied 403.
# If that happens it takes like 2 hours until you can make requests again
- name: Cache devkitPro toolchain
id: cache-devkitpro
uses: actions/checkout@v5
- name: Get docker image cache
uses: actions/cache@v4
with:
path: /home/runner/devkitpro
# Using a manual key version suffix - v{num}, if changes are made that make the
# cache stale and require a refresh, just bump the version number so there's
# a cache miss. The key needs to match in the "Save devkitPro to cache" stage
# so you need to bump the number there too...
key: devkitpro-${{ runner.os }}-v1
path: /tmp/docker-cache
key: docker-images-${{ hashFiles('docker-compose.yml') }}
- name: Set up devkitARM (if not cached)
if: steps.cache-devkitpro.outputs.cache-hit != 'true'
- name: Clear docker cache (if requested)
if: github.event.inputs.clear-cache == 'true'
run: |
sudo apt update
wget https://apt.devkitpro.org/install-devkitpro-pacman
chmod +x install-devkitpro-pacman
sudo ./install-devkitpro-pacman
mkdir -p "$DEVKITPRO"
sudo dkp-pacman -Sy --noconfirm gba-dev
# Copy from installed directory to cacheable directory
sudo cp -r /opt/devkitpro "/home/runner"
sudo chown -R $USER:$USER "/home/runner/devkitpro"
- name: Save devkitPro to cache
if: steps.cache-devkitpro.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: /home/runner/devkitpro
key: devkitpro-${{ runner.os }}-v1
echo "Clearing Docker cache..."
rm -rf /tmp/docker-cache
echo "Docker cache cleared"
- name: Load or pull images
run: |
if [ -f /tmp/docker-cache/gbalatro.tar ]; then
echo "Loading Image"
docker load -i /tmp/docker-cache/gbalatro.tar
else
echo "Pulling Image"
docker compose build gbalatro
mkdir -p /tmp/docker-cache
docker save $(docker compose config | grep image: | awk '{print $2}') -o /tmp/docker-cache/gbalatro.tar
fi
- name: Export UID/GID for Compose
run: |
echo "UID=$(id -u)" >> "$GITHUB_ENV"
echo "GID=$(id -g)" >> "$GITHUB_ENV"
- name: Build the project
run: |
make
run: docker compose -f docker-compose.yml run --rm gbalatro sh -c "make -j$(nproc) && ./scripts/get_memory_map.sh"
- name: 'Upload Artifact'
if: ${{ inputs.upload-build-artifact }}
uses: actions/upload-artifact@v4
with:
name: build-out
path: build
retention-days: 5

View File

@ -0,0 +1,20 @@
name: Run Clang Format Test
on:
workflow_dispatch:
workflow_call:
jobs:
run-test:
name: Verify code formatting
runs-on: ubuntu-latest
steps:
- name: Install clang-format
run: |
sudo apt-get update
sudo apt-get install -y clang-format
- name: Checkout repository
uses: actions/checkout@v5
- name: Run clang-format
run: clang-format --dry-run -Werror include/*.h source/*.c

View File

@ -0,0 +1,43 @@
name: Deploy Nightly
permissions:
contents: write
on:
schedule:
- cron: '0 2 * * *' # run at 2 AM UTC
workflow_dispatch:
jobs:
build-job:
name: Run Build ROM Workflow
uses: ./.github/workflows/build_ci_workflow.yml
with:
upload-build-artifact: true
nightly:
name: Deploy nightly
needs: build-job
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Download the build output
uses: actions/download-artifact@v5
with:
name: build-out
path: build
- name: Package Release
run: mv build/balatro-gba.gba build/balatro-gba_nightly_$(date +'%+4Y%m%d')_$(cat build/githash.txt).gba && zip -j release build/*.gba
- name: Deploy release
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/GBALATRO/balatro-gba/releases/258698380/assets{?name,label}
release_id: 258698380
asset_path: ./release.zip # path to archive to upload
asset_name: nightly-gbalatro-$$.zip # name to upload the release as, use $$ to insert date (YYYYMMDD) and 6 letter commit hash
asset_content_type: application/zip
max_releases: 7
ignore_hash: false # Only upload unique versions

View File

@ -0,0 +1,30 @@
name: Release On Version Tag
on:
push:
tags:
- "v*"
jobs:
build-job:
name: Run Build ROM Workflow
uses: ./.github/workflows/build_ci_workflow.yml
with:
upload-build-artifact: true
release:
name: Release Tagged Version
runs-on: ubuntu-latest
needs: build-job
steps:
- name: Download the build output
uses: actions/download-artifact@v5
with:
name: build-out
path: build
- uses: softprops/action-gh-release@v2
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
files: |
build/balatro-gba.gba

View File

@ -0,0 +1,21 @@
name: Run Unit Tests
on:
workflow_dispatch:
workflow_call:
jobs:
run-unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Check and install GCC version
run: ./ci_scripts/check_gcc_version.sh
- name: Run Unit Tests
run: cd tests && ./run_tests.sh

13
.gitignore vendored
View File

@ -1,6 +1,15 @@
/.vscode
/.idea
/build
/.vs
_codeql_detected_source_root
*.sln
*.vcxproj*
/balatro-gba/x64/**
*.vcxproj*
/balatro-gba/x64/**
/tests/bitset/build
/tests/list/build
/tests/pool/build
doc
tests/util/.vscode
tests/util/build
audio/*.bak

100
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,100 @@
# Developer Guide
Thank you for reading this. Below is a quick summary of expectations and tips to contribute.
## Scope
We are limiting the scope of the project in order to not compete with the original Balatro and avoid a takedown by Playstack.
We limited the scope to 52 jokers and reached that limit so currently there is no plan to add more jokers.
See [the scope discussion](https://github.com/GBALATRO/balatro-gba/discussions/355).
## Art
Before contributing art or if you need art for a code contribution check the [existing additional art thread](https://github.com/GBALATRO/balatro-gba/discussions/131) and the [joker art discussion](https://github.com/GBALATRO/balatro-gba/discussions/69) (though as said no more jokers are currently planned).
Note that there are color limitations for sprites and backgrounds resulting due to the GBA hardware. Sprites may not use more than 16 colors per sprite including transparency.
Backgrounds may use more colors but notice that their palette is encoded in their PNGs and new colors need to be added manually to the palette. [See relevant PR](https://github.com/GBALATRO/balatro-gba/pull/350).
## CI Checks
On pull-requests, various checks will be performed:
1. **Formatting**: `clang-format` will be ran on every `.c/.h` file with [this configuration](https://github.com/GBALATRO/balatro-gba/blob/main/.clang-format). Failures will cause the CI to fail.
2. **Unit Tests**: Unit tests are required to pass and are located in the repo [here](https://github.com/GBALATRO/balatro-gba/tree/main/tests).
3. **Rom Build**: The ROM must successfully build with the `make -j$(nproc)` command.
## Code Style
Besides the automatic formatting checks from `clang-format`, there is a looser set of code style rules. These are not strictly required, but is encouraged to be followed.
The following details the code style rules including the enforced clang-format style.
[Link in wiki](https://github.com/GBALATRO/balatro-gba/wiki/Code-Style-Guide)
## Building Documentation
Doxygen is used to build documentation that can be opened in browser.
[Link in wiki](https://github.com/GBALATRO/balatro-gba/wiki/Documentation-for-Developers)
## Tools
### clang-format
Running `clang-format` locally is recommended before submitting a PR as it will fail the **CI Checks** if not properly formatted. It is recommended either:
1. Run `clang-format` periodically and only commit formatted code.
2. Run `clang-format` as a separate commit on larger changes, and review each modified hunk.
Either way, just ensure you manually review automatic changes.
#### VSCode
The recommended setup for VSCode is to install the [**clangd**](https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.vscode-clangd) extension. It will provide helpful information in VSCode and can be used to format the code automatically according to the `.clang-format` file with **`Ctrl+Shift+I`**
There is an option to enable `"editor.formatOnSave"` in the VSCode `settings.json` file. You can also do this by opening the settings menu (`File->Preferences->Settings`) and searching `format on save`.
#### Manually
If installed locally and you'd prefer to use it in your shell. You can do the following
```sh
# List warnings
clang-format --dry-run -Werror include/*.h source/*.c
# Modify all files inplace
clang-format -i include/*.h source/*.c
# Or just one
clang-format -i include/blind.h
```
#### Disabling Formatting
Sometimes `clang-format` rules need to be broken, like in the case of the [joker registry](https://github.com/GBALATRO/balatro-gba/blob/8fb0813cf5f7235b6450dc9a76252dda4d9b4a27/source/joker_effects.c#L333) and other tables or maps. If it makes sense, you can wrap code in `// clang-format off` and `// clang-format on`:
- **Without clang-format**:
```c
// clang-format off
const table_of_tables =
{
{ TABLE_A, 1, tableAMap },
{ TABLE_B, 2, tableBMap },
{ TABLE_C, 3, tableCMap },
{ TABLE_D, 4, tableDMap },
{ TABLE_E, 5, tableEMap },
{ TABLE_F, 6, tableFMap },
{ TABLE_G, 7, tableGMap },
}
// clang-format on
```
- **With clang-format**:
```c
const table_of_tables =
{
{TABLE_A, 1, tableAMap}, {TABLE_B, 2, tableBMap}, {TABLE_C, 3, tableCMap},
{TABLE_D, 4, tableDMap}, {TABLE_E, 5, tableEMap}, {TABLE_F, 6, tableFMap},
{TABLE_G, 7, tableGMap},
}
```
### Custom Scripts
In the repo we use custom scripts located in the [`scripts`](https://github.com/GBALATRO/balatro-gba/tree/main/scripts) directory.
🟡 **Note**: `python3` and `bash` are required for these scripts.
- **get_hash.py**: Get git hash from ROM.
- **generate_font.py**: Generate a font manually.
- **get_memory_map.sh**: Print the memory map of the pre-allocated pools.
## Debugging
It's recommended to use [mGBA](https://mgba.io/) for ROM testing and debugging. As it provides a [`gdbserver`](https://en.wikipedia.org/wiki/Gdbserver) via the `-g` flag `mgba -g build/balatro-gba.gba`. You can connect via `gdb` or here is a [great guide for vscode](https://felixjones.co.uk/mgba_gdb/vscode.html).

6
Dockerfile.gbalatro Normal file
View File

@ -0,0 +1,6 @@
FROM devkitpro/devkitarm:20251117
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
python3-pil

3020
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@ -25,30 +25,37 @@ LIBTONC := $(DEVKITPRO)/libtonc
# the makefile is found
#
#---------------------------------------------------------------------------------
TARGET := $(notdir $(CURDIR))
BUILD := build
SOURCES := source
INCLUDES := include
DATA :=
MUSIC := audio
GRAPHICS := graphics
TARGET := $(notdir $(CURDIR))
BUILD := build
SOURCES := source
INCLUDES := include
DATA :=
MUSIC := audio
GRAPHICS := graphics
FONT := font
#---------------------------------------------------------------------------------
# options for code generation
#---------------------------------------------------------------------------------
ARCH := -mthumb -mthumb-interwork
CFLAGS := -g -O3 -Wall -Werror\
GIT_DIRTY := $(shell git diff-index --quiet HEAD -- || echo "-dirty")
GIT_HASH := $(shell git rev-parse --short HEAD || echo "undef")
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 \
$(ARCH)
CFLAGS += $(GIT_C_FLAGS)
CFLAGS += $(INCLUDE)
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions
ASFLAGS := -g $(ARCH)
LDFLAGS = -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LDFLAGS = -g $(ARCH) -Wl,-Map,$(notdir $*.map),--undefined=balatro_version
#---------------------------------------------------------------------------------
# any extra libraries we wish to link with the project
@ -76,7 +83,8 @@ export OUTPUT := $(CURDIR)/$(BUILD)/$(TARGET)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
$(foreach dir,$(DATA),$(CURDIR)/$(dir)) \
$(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir))
$(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir)) \
$(foreach dir,$(FONT),$(CURDIR)/$(dir)) \
export DEPSDIR := $(CURDIR)/$(BUILD)
@ -84,6 +92,7 @@ CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
PNGFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.png)))
FONTFILES := $(foreach dir,$(FONT),$(notdir $(wildcard $(dir)/*.png)))
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
ifneq ($(strip $(MUSIC)),)
@ -111,7 +120,9 @@ export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
export OFILES_GRAPHICS := $(PNGFILES:.png=.o)
export OFILES := $(OFILES_BIN) $(OFILES_SOURCES) $(OFILES_GRAPHICS)
export OFILES_FONT := $(FONTFILES:.png=.o)
export OFILES := $(OFILES_BIN) $(OFILES_SOURCES) $(OFILES_GRAPHICS) $(OFILES_FONT)
export HFILES := $(addsuffix .h,$(subst .,_,$(BINFILES))) $(PNGFILES:.png=.h)
@ -124,16 +135,22 @@ export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
.PHONY: $(BUILD) clean
#---------------------------------------------------------------------------------
$(BUILD):
$(BUILD): build/gbalatro_sys8.s
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
@echo "$(GIT_HASH)$(GIT_DIRTY)" > $@/githash.txt
#---------------------------------------------------------------------------------
build/%.s: $(FONT)/%.png
@echo Building font
@mkdir -p $(BUILD)
@python3 scripts/generate_font.py -i $< -o $@
#---------------------------------------------------------------------------------
clean:
@echo clean ...
@rm -fr $(BUILD) $(TARGET).elf $(TARGET).gba
#---------------------------------------------------------------------------------
all: $(BUILD)

123
README.md
View File

@ -1,21 +1,20 @@
# Balatro-GBA
[![Build Status](https://img.shields.io/github/actions/workflow/status/cellos51/balatro-gba/build_ci_workflow.yml?style=flat&logo=github&branch=main&label=Builds&labelColor=gray&color=default&v=1)](https://github.com/cellos51/balatro-gba/actions)
[![Open Issues](https://custom-icon-badges.demolab.com/github/issues/cellos51/balatro-gba?logo=bug&style=flat&label=Issues&labelColor=gray&color=red&v=2)](https://github.com/cellos51/balatro-gba/issues)
[![Pull Requests](https://custom-icon-badges.demolab.com/github/issues-pr/cellos51/balatro-gba?logo=git-pull-request&style=flat&label=Pull%20Requests&labelColor=gray&color=indigo&v=3)](https://github.com/cellos51/balatro-gba/pulls)
[![Build Status](https://img.shields.io/github/actions/workflow/status/cellos51/balatro-gba/build_ci_workflow.yml?style=flat&logo=github&branch=main&label=Builds&labelColor=gray&color=default&maxAge=7200)](https://github.com/cellos51/balatro-gba/actions)
[![Open Issues](https://img.shields.io/github/issues/cellos51/balatro-gba?style=flat&color=red&label=Issues&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ij48cGF0aCBmaWxsPSJ3aGl0ZSIgZD0iTTggOS41YTEuNSAxLjUgMCAxIDAgMC0zIDEuNSAxLjUgMCAwIDAgMCAzWiI+PC9wYXRoPjxwYXRoIGZpbGw9IndoaXRlIiBkPSJNOCAwYTggOCAwIDEgMSAwIDE2QTggOCAwIDAgMSA4IDBaTTEuNSA4YTYuNSA2LjUgMCAxIDAgMTMgMCA2LjUgNi41IDAgMCAwLTEzIDBaIj48L3BhdGg+PC9zdmc+)](https://github.com/cellos51/balatro-gba/issues)
[![Pull Requests](https://img.shields.io/github/issues-pr/cellos51/balatro-gba?style=flat&color=indigo&label=Pull%20Requests&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ij48cGF0aCBmaWxsPSJ3aGl0ZSIgZD0iTTEuNSAzLjI1YTIuMjUgMi4yNSAwIDEgMSAzIDIuMTIydjUuMjU2YTIuMjUxIDIuMjUxIDAgMSAxLTEuNSAwVjUuMzcyQTIuMjUgMi4yNSAwIDAgMSAxLjUgMy4yNVptNS42NzctLjE3N0w5LjU3My42NzdBLjI1LjI1IDAgMCAxIDEwIC44NTRWMi41aDFBMi41IDIuNSAwIDAgMSAxMy41IDV2NS42MjhhMi4yNTEgMi4yNTEgMCAxIDEtMS41IDBWNWExIDEgMCAwIDAtMS0xaC0xdjEuNjQ2YS4yNS4yNSAwIDAgMS0uNDI3LjE3N0w3LjE3NyAzLjQyN2EuMjUuMjUgMCAwIDEgMC0uMzU0Wk0zLjc1IDIuNWEuNzUuNzUgMCAxIDAgMCAxLjUuNzUuNzUgMCAwIDAgMC0xLjVabTAgOS41YS43NS43NSAwIDEgMCAwIDEuNS43NS43NSAwIDAgMCAwLTEuNVptOC4yNS43NWEuNzUuNzUgMCAxIDAgMS41IDAgLjc1Ljc1IDAgMCAwLTEuNSAwWiI+PC9wYXRoPjwvc3ZnPg==)](https://github.com/cellos51/balatro-gba/pulls)
[![Discussions](https://img.shields.io/github/discussions/cellos51/balatro-gba?style=flat&color=blue&label=Discussions&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ij48cGF0aCBmaWxsPSJ3aGl0ZSIgZD0iTTEuNzUgMWg4LjVjLjk2NiAwIDEuNzUuNzg0IDEuNzUgMS43NXY1LjVBMS43NSAxLjc1IDAgMCAxIDEwLjI1IDEwSDcuMDYxbC0yLjU3NCAyLjU3M0ExLjQ1OCAxLjQ1OCAwIDAgMSAyIDExLjU0M1YxMGgtLjI1QTEuNzUgMS43NSAwIDAgMSAwIDguMjV2LTUuNUMwIDEuNzg0Ljc4NCAxIDEuNzUgMVpNMS41IDIuNzV2NS41YzAgLjEzOC4xMTIuMjUuMjUuMjVoMWEuNzUuNzUgMCAwIDEgLjc1Ljc1djIuMTlsMi43Mi0yLjcyYS43NDkuNzQ5IDAgMCAxIC41My0uMjJoMy41YS4yNS4yNSAwIDAgMCAuMjUtLjI1di01LjVhLjI1LjI1IDAgMCAwLS4yNS0uMjVoLTguNWEuMjUuMjUgMCAwIDAtLjI1LjI1Wm0xMyAyYS4yNS4yNSAwIDAgMC0uMjUtLjI1aC0uNWEuNzUuNzUgMCAwIDEgMC0xLjVoLjVjLjk2NiAwIDEuNzUuNzg0IDEuNzUgMS43NXY1LjVBMS43NSAxLjc1IDAgMCAxIDE0LjI1IDEySDE0djEuNTQzYTEuNDU4IDEuNDU4IDAgMCAxLTIuNDg3IDEuMDNMOS4yMiAxMi4yOGEuNzQ5Ljc0OSAwIDAgMSAuMzI2LTEuMjc1Ljc0OS43NDkgMCAwIDEgLjczNC4yMTVsMi4yMiAyLjIydi0yLjE5YS43NS43NSAwIDAgMSAuNzUtLjc1aDFhLjI1LjI1IDAgMCAwIC4yNS0uMjVaIj48L3BhdGg+PC9zdmc+)](https://github.com/cellos51/balatro-gba/discussions)
This is an attempt to recreate the game **'Balatro'** as accurately as possible, including all of the visual effects that make Balatro feel satisfying to play.
This **tech-demo/proof of concept** is strictly limited in content to a minimal version of Balatro and will **NOT** recreate the full game. **This version is intended for people who already own and know how the official full game works.** Please refer to the Balatro Wiki if you need help understanding certain mechanics or abilities.
### Disclaimer
This project is a non-profit fan demake of Balatro for the Game Boy Advance, meant to recreate it as accurately as possible including all the visuals that make it satisfying to play. It is **not affiliated with or endorsed by Playstack or LocalThunk** and **it is not to be sold**. This version is a **minimal tech-demo**, intended for those who already own and know the official game. Refer to the Balatro wiki below for descriptions of game mechanics and joker effects. All rights remain with the original holders.
<a href="https://balatrowiki.org/">
<img src="https://custom-icon-badges.demolab.com/badge/Balatro%20Wiki-194c84?logo=bigjoker&logoColor=fff" alt="Balatro Wiki" width="155">
</a>
### Disclaimer: This project is NOT endorsed by or affiliated with Playstack or LocalThunk
#### This is a non-profit community fan project solely aimed to recreate a minimal version of Balatro on the Game Boy Advance as a tribute to the full Balatro and is not intended to infringe or draw sales away from the full game's release or any of the established works by Playstack and LocalThunk.
#### All rights are reserved to their respective holders.
### Please buy the official full version from these sources below:
[![Balatro on Steam](https://custom-icon-badges.demolab.com/badge/Balatro%20on%20Steam-194c84?logo=steam&logoColor=fff)](https://store.steampowered.com/app/2379780/Balatro/)
[![Balatro on Google Play](https://custom-icon-badges.demolab.com/badge/Balatro%20on%20Google%20Play-414141?logo=Google-play&logoColor=fff)](https://play.google.com/store/apps/details?id=com.playstack.balatro.android)
[![Balatro on Apple App Store](https://custom-icon-badges.demolab.com/badge/Balatro%20on%20Apple%20App%20Store-0D96F6?logo=app-store&logoColor=fff)](https://apps.apple.com/us/app/balatro/id6502453075)
@ -24,21 +23,73 @@ This **tech-demo/proof of concept** is strictly limited in content to a minimal
[![Balatro on Xbox](https://custom-icon-badges.demolab.com/badge/Balatro%20on%20Xbox-107C10.svg?logo=xbox&logoColor=white)](https://www.xbox.com/en-US/games/store/balatro/9PK087LNGJC5)
[![Balatro on Humble Bundle](https://img.shields.io/badge/Balatro%20on%20Humble%20Bundle-%23494F5C.svg?logo=HumbleBundle&logoColor=white)](https://www.humblebundle.com/store/balatro?srsltid=AfmBOoqS2De8T4kizzWxJS1pbvQosJ_bYCl4qvC6LA1YLPAh4sZ8vJqO)
<!-- The Gif is a little blurry but I think it looks fine -->
<img src="example.gif" alt="Example GIF" width="800">
---
<a href="https://github.com/cellos51/balatro-gba/releases/">
<img src="https://img.shields.io/badge/Download_ROMs_from_the_Releases_tab-8A2BE2?&logo=github" alt="Download ROM" width="500">
</a>
---
https://github.com/user-attachments/assets/54a9e2e9-1a02-48d5-bb9d-5ab257a7e03b
### Controls:
(D-Pad: Navigation)
(A: Pick Card/Make Selections)
(B: Deselect All Cards)
#### When on the hand row during round
(L: Play Hand)
(R: Discard Hand)
(B: Deselect All Cards)
#### When on the joker row in the shop or during round
(L: Sell Joker)
(R: Sort Suit/Rank)
(Hold A: Swap Owned Jokers or Playing Cards in the Shop or Round)
# Contributing
If you would like to contribute, please read CONTRIBUTING.md.
(D-Pad: Navigation)
# **Build Instructions:**
## **-Docker-**
A docker compose file is provided to build this project. It provides a standard build environment for this projects CI/CD and can also be used to build the ROM locally.
_Some users may find this option to be the easiest way to build locally._
- _This option **avoids** setting up the development environment as described below._
- _No additional software besides **docker desktop** is required._
### Step-by-Step
1.) Install [docker desktop](https://docs.docker.com/desktop/) for your operating system.
2.) Open a terminal to this project's directory:
- On **Linux** run `UID=$(id -u) GID=$(id -g) docker compose up`
- On **Windows** run `docker compose up`
<details>
<summary><i>How do I open a terminal in windows?</i></summary>
---
From the file explorer, you can open a folder in **powershell** (_a modern windows terminal_):
- **hold 'Shift'** and **Right Click** on the folder.
- Select **"Open PowerShell window here"** from the popup menu.
---
</details>
3.) Docker will build the project and the ROM will be in the same location as step 7 describes below.
## **-Windows-**
Video Tutorial: https://youtu.be/72Zzo1VDYzQ?si=UDmEdbST1Cx1zZV2
### With `Git` (not required)
@ -57,6 +108,36 @@ Video Tutorial: https://youtu.be/72Zzo1VDYzQ?si=UDmEdbST1Cx1zZV2
7.) After it completes, navigate through the `build` directory in the project folder and look for `balatro-gba.gba` and load it on an emulator or flashcart.
### Without `Git`
Disregard Steps 3-4 and instead click the green code button on the main repository page and press `Download Zip`. Unzip the folder and place it wherever you like. Then continue from Step 5.
## **-Linux-**
1.) Add the devkitPro repository using these instructions https://devkitpro.org/wiki/devkitPro_pacman
2.) Install devkitPro by running `sudo pacman -S gba-dev` and accepting all packages.
3.) Activate the devkitPro environment by running `source /etc/profile.d/devkit-env.sh` or opening a new shell.
4.) Follow instructions from the Windows tutorial starting from Step 3
## **-macOS-**
1.) Install devkitPro installer using: https://github.com/devkitPro/installer and following https://devkitpro.org/wiki/devkitPro_pacman#macOS.
> Note: You may have to install the installers directly from their url in a browser, as the installer script may not install correctly due to Cloudflare checks on their server. You can use one of the following urls:
> Apple Silicon: https://pkg.devkitpro.org/packages/macos-installers/devkitpro-pacman-installer.arm64.pkg
> Intel: https://pkg.devkitpro.org/packages/macos-installers/devkitpro-pacman-installer.x86_64.pkg
2.) Run `sudo dkp-pacman -S gba-dev`
3.) Verify that devkitPro is installed in '/opt/devkitpro'
4.) Add the following to your .bashrc or .zshrc (or export the variables in your shell session):
- export DEVKITPRO=/opt/devkitpro
- export DEVKITARM=$DEVKITPRO/devkitARM
- export PATH=$PATH:$DEVKITPRO/tools/bin:$DEVKITPRO/pacman/bin
5.) Follow instructions from Windows tutorial step 4
## **Common Issues:**
#### 1. **When I run `make` it errors out and won't compile!**
@ -70,3 +151,17 @@ Disregard Steps 3-4 and instead click the green code button on the main reposito
#### 4. **It says I don't have `Git` or `Make` installed!**
- Use `pacman -S git` (not required) or `pacman -S make` although make should already be installed if you followed the instructions correctly.
# **Credits:**
## **Game**
This GBA implementation is based on Balatro which is designed and created by LocalThunk and published by Playstack.
See repository contributors list for code contribution credits to this GBA implementation.
## **Music**
Music arrangement is made by @cellos51 and @MeirGavish based on original Balatro soundtrack by [LouisF](https://louisfmusic.com/) and [transcription by MrCrimit](https://musescore.com/user/8237321/scores/14590660).
## **Imagery**
Sprites and backgrounds are based on original Balatro imagery created by LocalThunk.
See [Joker Art Discussion](https://github.com/cellos51/balatro-gba/discussions/69) for full credits for each joker sprite.
## **Sounds**
For the mult and xmult sound effects: [Toy records#06-E3-02.wav by poissonmort](https://freesound.org/s/253249/) used under License: Attribution 4.0
All other sound effects were created by LocalThunk and are used under Creative Commons - CC0 license.

BIN
audio/button.wav Normal file

Binary file not shown.

BIN
audio/chips_accum.wav Normal file

Binary file not shown.

BIN
audio/chips_card.wav Normal file

Binary file not shown.

BIN
audio/chips_generic.wav Normal file

Binary file not shown.

Binary file not shown.

BIN
audio/mult.wav Normal file

Binary file not shown.

BIN
audio/pop.wav Normal file

Binary file not shown.

BIN
audio/xmult.wav Normal file

Binary file not shown.

17
ci_scripts/check_gcc_version.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash
# Check GCC version and install GCC 14 if needed
check_gcc_version() {
if command -v gcc &> /dev/null; then
gcc_version=$(gcc -dumpversion | cut -d. -f1)
if [ "$gcc_version" -lt 14 ]; then
echo "GCC version $gcc_version detected. Installing GCC 14..."
sudo apt-get update
sudo apt-get install -y gcc-14
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100
echo "GCC 14 installed and set as default."
fi
fi
}
check_gcc_version

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
services:
gbalatro:
image: gbalatro:dev
build:
context: .
dockerfile: Dockerfile.gbalatro
working_dir: /balatro-gba
volumes:
- ./:/balatro-gba
user: "${UID-1000}:${GID-1000}"
command: ["sh", "-c", "make -j$(nproc)"]

BIN
font/gbalatro_sys8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
graphics/big_blind_gfx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 B

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -p!

BIN
graphics/boss_blind_gfx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx17.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx18.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx19.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx20.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx21.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx22.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx23.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -m! -pn 16

BIN
graphics/joker_gfx25.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 961 B

After

Width:  |  Height:  |  Size: 1016 B

View File

@ -0,0 +1 @@
-gB4 -Mw4 -Mh4 -p!

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

View File

@ -1,26 +1,93 @@
/**
* @file affine_background.h
*
* @brief Utilities for affine background on GBA
*/
#ifndef AFFINE_BACKGROUND_H
#define AFFINE_BACKGROUND_H
#include <tonc.h>
#include "graphic_utils.h"
#define AFFINE_BG_IDX 2 // The index of the affine background BGCNT register etc.
#define AFFINE_BG_PAL_LEN 16
#define AFFINE_BG_PB (PAL_ROW_LEN * 10) // This isn't really a palette bank, just the starting index of the palette
#include <tonc.h>
/**
* @def AFFINE_BG_IDX
* @brief The index of the affine background BGCNT register etc.
*/
#define AFFINE_BG_IDX 2
/**
* @def AFFINE_BG_PAL_LEN
* @brief Number of u16 colors available in the palette.
*/
#define AFFINE_BG_PAL_LEN 16
/**
* @def AFFINE_BG_PB
* @brief The starting index of the background palette.
*/
#define AFFINE_BG_PB (PAL_ROW_LEN * 10)
/**
* @brief An ID to specify background rendering types.
*/
enum AffineBackgroundID
{
/**
* @brief Display background for main menu.
*
* Signifies which background to use and provides a higher quality affine
* mode to display when there is no game logic.
*/
AFFINE_BG_MAIN_MENU,
/**
* @brief Display background for game play.
*
* Signifies which background to use and provides a lower quality affine
* mode to keep resources down during play.
*/
AFFINE_BG_GAME,
};
/**
* @brief Initialize resources for affine background rendering
*/
void affine_background_init();
/**
* @brief Interrupt routine to update display on HBLANK
*/
IWRAM_CODE void affine_background_hblank();
/**
* @brief Per-frame update of the affine background
*/
IWRAM_CODE void affine_background_update();
/**
* @brief Update the affine background color
*
* @param color @ref COLOR to set
*/
void affine_background_set_color(COLOR color);
// Must be called with an array of size at least AFFINE_BG_PAL_LEN
void affine_background_load_palette(const u16 *src);
/**
* @brief Set the background palette
*
* Must be called with an array of size at least @ref AFFINE_BG_PAL_LEN
*
* @param src pointer to palette to set
*/
void affine_background_load_palette(const u16* src);
/**
* @brief Update the background id
*
* Update the background id by configuring display registers and loading
* the image and palette used for the specified id.
*
* @param new_bg @ref AffineBackgrounID to set
*/
void affine_background_change_background(enum AffineBackgroundID new_bg);
#endif // AFFINE_BACKGROUND_H

View File

@ -1,16 +1,54 @@
#ifndef AUDIO_UTILS_H
#define AUDIO_UTILS_H
#include <mm_types.h>
#define MM_FULL_VOLUME 255
#define MM_PAN_CENTER 128
#define MM_BASE_PITCH_RATE 1024
#define SFX_DEFAULT_VOLUME MM_FULL_VOLUME
#define SFX_DEFAULT_PAN MM_PAN_CENTER
#define SFX_DEFAULT_HANDLE 0
void play_sfx(mm_word id, mm_word rate);
#endif
/**
* @file audio_utils.h
*
* @brief Utilities for using maxmod to play sound effects
*/
#ifndef AUDIO_UTILS_H
#define AUDIO_UTILS_H
#include <mm_types.h>
/**
* @def MM_FULL_VOLUME
* @brief The maximum volume for maxmod mm_sound_effect.volume
*/
#define MM_FULL_VOLUME 255
/**
* @def MM_PAN_CENTER
* @brief The center pan of stereo audio for maxmod
*/
#define MM_PAN_CENTER 128
/**
* @def MM_BASE_PITCH_RATE
* @brief The default pitch rate for the sound effect played.
*
* Increasing the rate increases the pitch, while decreasing lowers the pitch.
*/
#define MM_BASE_PITCH_RATE 1024
/**
* @def SFX_DEFAULT_VOLUME
* @brief Default volume for sound effects
*/
#define SFX_DEFAULT_VOLUME MM_FULL_VOLUME
/**
* @def SFX_DEFAULT_PAN
* @brief The default stereo pan, will always be center pan.
*/
#define SFX_DEFAULT_PAN MM_PAN_CENTER
/**
* @brief Play a sound effect, wrapper for mmEffectEx()
* See https://maxmod.org/ref/functions/mm_sound_effect.html
* and https://maxmod.org/ref/functions/mmEffectEx.html
*
* @param id the sound id to play, from maxmod compiled soundbank.h header
* @param rate the pitch rate, the default value is @ref MM_BASE_PTCH_RATE
* @param volume the volume ranging from 0 (silent) to 255 (loudest)
*/
void play_sfx(mm_word id, mm_word rate, mm_byte volume);
#endif

207
include/bitset.h Normal file
View File

@ -0,0 +1,207 @@
/**
* @file bitset.h
*
* @brief A bitset for operating on flags
*/
#ifndef BITSET_H
#define BITSET_H
#include <stdbool.h>
#include <stdint.h>
/**
* @def BITSET_BITS_PER_WORD
* @brief Number of bits in a word for a bitset.
*
* Number of bits in a word for a bitset. Will always be 32 here.
*/
#define BITSET_BITS_PER_WORD 32
/**
* @def BITSET_ARRAY_SIZE
* @brief Number of words in a bitset
*
* Number of words in every bitset. This represents the maximum number and each
* bitset will always use this number of words, though it's capacity can be any length
* from `1` to `BITSET_BITS_PER_WORD * BITSET_ARRAY_SIZE`
*/
#define BITSET_ARRAY_SIZE 8
/**
* @def BITSET_MAX_BITS
* @brief Maximum number of bits in a bitset
*/
#define BITSET_MAX_BITS (BITSET_BITS_PER_WORD * BITSET_ARRAY_SIZE)
/**
* @brief A bitset spread across multiple `uint32_t` words
*/
typedef struct Bitset
{
/**
* @brief Word array of `uint32_t` to hold the bitset data
*/
uint32_t* w;
/**
* @brief Number of bits in a word, will be 32
*/
uint32_t nbits;
/**
* @brief Number of words int the `w` array
*/
uint32_t nwords;
/**
* @brief Number of actual flags (nbits * nwords)
*/
uint32_t cap;
} Bitset;
/**
* @brief An iterator into a @ref Bitset
*
* This iterator will parse and find the next index to a '1' bit as efficiently as possible.
*
* There is no implementation of the following (yet):
* - Reverse iteration
* - Bit-by-bit iteration
* - Iterating on offsets to '0' bits
*/
typedef struct
{
/**
* @brief @ref Bitset this is iterating through
*/
const Bitset* bitset;
/**
* @brief Current word the iterator is on
*/
int word;
/**
* @brief Current bit the iterator is on
*/
int bit;
/**
* @brief Number of bits that have been iterated through in total
*/
int itr;
} BitsetItr;
/**
* @brief Set a flag in a bitset to a value
*
* @param bitset A @ref Bitset to operate on
* @param idx the index of the flag to set
* @param on the value to set the flag to
*/
void bitset_set_idx(Bitset* bitset, int idx, bool on);
/**
* @brief Get the value of a flag
*
* @param bitset A @ref Bitset to operate on
* @param idx the index of the flag to get
*
* @return the value of the flag as `true` or `false`
*/
bool bitset_get_idx(Bitset* bitset, int idx);
/**
* @brief Set the next free index in the bitset and return the index value
*
* @param bitset A @ref Bitset to operate on
*
* @return The index of the bit that was set
*/
int bitset_set_next_free_idx(Bitset* bitset);
/**
* @brief Clear the bitset, all to 0
*
* @param bitset A @ref Bitset to operate on
*/
void bitset_clear(Bitset* bitset);
/**
* @brief Check if a bitset is empty (all 0's)
*
* @param bitset A @ref Bitset to operate on
*
* @return `true` if empty, `false` otherwise
*/
bool bitset_is_empty(Bitset* bitset);
/**
* @brief Count how many bits are set to `1` in a bitset
*
* @param bitset A @ref Bitset to operate on
*
* @return The number of flags set to `1` in a bitset
*/
int bitset_num_set_bits(Bitset* bitset);
/**
* @brief Find the index of the nth set bit
*
* Find the index of the nth flag set to `1`. This function is useful to get one value quickly,
* but does not operate iteratively well. Use a @BitsetItr for iterative access to a bitset.
*
* @param bitset A @ref Bitset to operate on
*
* @return The index of the nth flag set to `1` in the bitset
*/
int bitset_find_idx_of_nth_set(const Bitset* bitset, int n);
/**
* @brief Declare a @ref BitsetItr
*
* @param bitset A @ref Bitset to operate on
*
* @return A newly constructed BitsetItr
*/
BitsetItr bitset_itr_create(const Bitset* bitset);
/**
* @brief Get the index of the next set bit in the bitset from a @ref BitsetItr
*
* @param itr A @ref BitsetItr to operate on
*
* @return a positive number if successful, UNDEFINED otherwise (out-of-bounds)
*/
int bitset_itr_next(BitsetItr* itr);
/**
* @def BITSET_DEFINE
* @brief Make a standard bitset
*
* Make a bitset with a valid static array to store it's array of words.
*
* Use this to define bitsets in the code, specifically as a `static` scoped
* variable. The passed `name` will be the same name as the bitset.
*
* Usage example:
*
* ```c
* BITSET_DEFINE(_my_bitset, 128);
* // normal operation...
* bitset_clear(&_my_bitset);
* ```
*
* @param name the name of the bitset
* @param capacity the capacity of the bitset
*/
#define BITSET_DEFINE(name, capacity) \
static uint32_t name##_w[BITSET_ARRAY_SIZE] = {0}; \
static Bitset name = { \
.w = name##_w, \
.nbits = BITSET_BITS_PER_WORD, \
.nwords = BITSET_ARRAY_SIZE, \
.cap = capacity, \
};
#endif // BITSET_H

View File

@ -2,49 +2,81 @@
#define BLIND_H
#include "sprite.h"
#define MAX_ANTE 8 // The GBA's max uint value is around 4 billion, so we're going to not add endless mode for simplicity's sake
// +1 is added because we'll actually be indexing at 1, but if something causes you to go to ante 0, there will still be a value there.
static const int ante_lut[MAX_ANTE + 1] = {100, 300, 800, 2000, 5000, 11000, 20000, 35000, 50000};
#define MAX_BLINDS 3
#define SMALL_BLIND 0
#define BIG_BLIND 1
#define BOSS_BLIND 2
// The GBA's max uint value is around 4 billion, so we're going to not add endless mode for
// simplicity's sake
#define MAX_ANTE 8
#define SMALL_BLIND_PB 1
#define BIG_BLIND_PB 2
#define BOSS_BLIND_PB 3
#define BIG_BLIND_PB 2
#define BOSS_BLIND_PB 3
#define BLIND_SPRITE_OFFSET 16
#define SMALL_BLIND_TID 960
#define BIG_BLIND_TID (BLIND_SPRITE_OFFSET + SMALL_BLIND_TID)
#define BOSS_BLIND_TID (BLIND_SPRITE_OFFSET + BIG_BLIND_TID)
#define BLIND_SPRITE_OFFSET 16
#define BLIND_SPRITE_COPY_SIZE BLIND_SPRITE_OFFSET * 8 // 8 ints per tile
#define SMALL_BLIND_TID 960
#define BIG_BLIND_TID (BLIND_SPRITE_OFFSET + SMALL_BLIND_TID)
#define BOSS_BLIND_TID (BLIND_SPRITE_OFFSET + BIG_BLIND_TID)
#define BLIND_TEXT_COLOR_INDEX 1
#define BLIND_SHADOW_COLOR_INDEX 2
#define BLIND_HIGHLIGHT_COLOR_INDEX 3
#define BLIND_MAIN_COLOR_INDEX 4
#define BLIND_BACKGROUND_MAIN_COLOR_INDEX 5
#define BLIND_BACKGROUND_SECONDARY_COLOR_INDEX 6
#define BLIND_BACKGROUND_SHADOW_COLOR_INDEX 7
enum BlindColorIndex
{
BLIND_TEXT_COLOR_INDEX = 1,
BLIND_SHADOW_COLOR_INDEX = 2,
BLIND_HIGHLIGHT_COLOR_INDEX = 3,
BLIND_MAIN_COLOR_INDEX = 4,
BLIND_BACKGROUND_MAIN_COLOR_INDEX = 5,
BLIND_BACKGROUND_SECONDARY_COLOR_INDEX = 6,
BLIND_BACKGROUND_SHADOW_COLOR_INDEX = 7,
};
#define BLIND_TYPE_INFO_TABLE \
BLIND_INFO(SMALL, small, FIX_ONE, 3) \
BLIND_INFO(BIG, big, (FIX_ONE * 3) / 2, 4) \
BLIND_INFO(BOSS, boss, FIX_ONE * 2, 5)
// clang-format off
enum BlindType
{
#define BLIND_INFO(NAME, name, multi, reward) \
BLIND_TYPE_##NAME,
BLIND_TYPE_INFO_TABLE
#undef BLIND_INFO
BLIND_TYPE_MAX,
};
// clang-format on
enum BlindState
{
BLIND_CURRENT,
BLIND_UPCOMING,
BLIND_DEFEATED,
BLIND_SKIPPED
BLIND_STATE_CURRENT,
BLIND_STATE_UPCOMING,
BLIND_STATE_DEFEATED,
BLIND_STATE_SKIPPED,
BLIND_STATE_MAX,
};
// TODO: Move this to a common interface for other palettes
typedef struct
{
const unsigned int* tiles;
const u16* palette;
u32 tid;
u32 pb;
} BlindGfxInfo;
typedef struct
{
enum BlindType type;
BlindGfxInfo gfx_info;
FIXED score_req_multipler;
s32 reward;
} Blind;
void blind_init();
int blind_get_requirement(int type, int ante);
int blind_get_reward(int type);
u16 blind_get_color(int type, int index);
void blind_set_boss_graphics(const unsigned int* tiles, const u16* palette);
Sprite *blind_token_new(int type, int x, int y, int sprite_index);
u32 blind_get_requirement(enum BlindType type, int ante);
int blind_get_reward(enum BlindType type);
u16 blind_get_color(enum BlindType type, enum BlindColorIndex index);
#endif // BLIND_H
Sprite* blind_token_new(enum BlindType type, int x, int y, int sprite_index);
#endif // BLIND_H

76
include/button.h Normal file
View File

@ -0,0 +1,76 @@
/**
* @file button.h
*
* @brief A button structure containing the common button functionalities
*/
#ifndef BUTTON_H
#define BUTTON_H
#include <tonc_types.h>
#define BTN_HIGHLIGHT_COLOR 0xFFFF
#define BUTTON_SFX_VOLUME 154 // 60% of MM_FULL_VOLUME
/**
* @brief The function to be called when the button is pressed
*/
typedef void (*ButtonOnPressedFunc)(void);
/**
* @brief Returns true if the button should be activated when pressed.
*/
typedef bool (*ButtonCanBePressedFunc)(void);
/**
* @brief A button representation
*/
typedef struct
{
// Using u8 for the palette indices to make sure they don't overflow pal_bg_mem
/**
* @brief Palette index of the button border whose color is updated depending on highlight.
*/
u8 border_pal_idx;
/**
* @brief Palette index of the color of the button itself, used for the border when not
* highlighted.
*/
u8 button_pal_idx;
/**
* @brief Called when the button is pressed if @ref can_be_pressed, don't call this directly,
* call @ref button_press() instead.
* Should not be set to NULL.
*/
ButtonOnPressedFunc on_pressed;
/**
* @brief Returns true if the button should be activated when pressed.
* Can be NULL if button can always be pressed.
*/
ButtonCanBePressedFunc can_be_pressed;
} Button;
/**
* @brief Set the button highlight on or off.
* @param button The button to modify. No-op on NULL.
* @param highlight true to highlight, false to unhighlight.
*/
void button_set_highlight(Button* button, bool highlight);
/**
* @brief Execute a button press, by calling the button's @ref on_pressed function.
* This checks the button's @ref can_be_pressed function and will do nothing if
* returns false.
*
* Button presses should go through this function rather than directly call @ref on_pressed.
*
* @param button The button being pressed. No-op on NULL.
*/
void button_press(Button* button);
#endif // BUTTON_H

View File

@ -1,44 +1,47 @@
#ifndef CARD_H
#define CARD_H
#include <tonc.h>
#include <maxmod.h>
#include "sprite.h"
#define CARD_TID 0
#define CARD_SPRITE_OFFSET 16
#define CARD_PB 0
#include <maxmod.h>
#include <tonc.h>
#define MAX_CARDS (NUM_SUITS * NUM_RANKS)
#define MAX_CARDS_ON_SCREEN 16
#define CARD_TID 0
#define CARD_SPRITE_OFFSET 16
#define CARD_PB 0
#define CARD_STARTING_LAYER 0
// Card suits
#define HEARTS 0
#define CLUBS 1
#define DIAMONDS 2
#define SPADES 3
#define DIAMONDS 0
#define CLUBS 1
#define HEARTS 2
#define SPADES 3
#define NUM_SUITS 4
// Card ranks
#define TWO 0
#define THREE 1
#define FOUR 2
#define FIVE 3
#define SIX 4
#define SEVEN 5
#define EIGHT 6
#define NINE 7
#define TEN 8
#define JACK 9
#define QUEEN 10
#define KING 11
#define ACE 12
#define NUM_RANKS 13
#define TWO 0
#define THREE 1
#define FOUR 2
#define FIVE 3
#define SIX 4
#define SEVEN 5
#define EIGHT 6
#define NINE 7
#define TEN 8
#define JACK 9
#define QUEEN 10
#define KING 11
#define ACE 12
#define NUM_RANKS 13
#define RANK_OFFSET 2 // Because the first rank is 2 and ranks start at 0
#define IMPOSSIBLY_HIGH_CARD_VALUE 100
// Card types
typedef struct
typedef struct Card
{
u8 suit;
u8 rank;
@ -46,27 +49,28 @@ typedef struct
typedef struct CardObject
{
Card *card;
SpriteObject *sprite_object;
Card* card;
SpriteObject* sprite_object;
bool selected;
} CardObject;
// Card functions
void card_init();
// Card methods
Card *card_new(u8 suit, u8 rank);
void card_destroy(Card **card);
u8 card_get_value(Card *card);
Card* card_new(u8 suit, u8 rank);
void card_destroy(Card** card);
u8 card_get_value(Card* card);
// CardObject methods
CardObject *card_object_new(Card *card);
void card_object_destroy(CardObject **card_object);
void card_object_update(CardObject *card_object); // Update the card object position and scale
void card_object_set_sprite(CardObject *card_object, int layer);
CardObject* card_object_new(Card* card);
void card_object_destroy(CardObject** card_object);
void card_object_update(CardObject* card_object); // Update the card object position and scale
void card_object_set_sprite(CardObject* card_object, int layer);
void card_object_shake(CardObject* card_object, mm_word sound_id);
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);
#endif // CARD_H
#endif // CARD_H

View File

@ -0,0 +1,12 @@
#include "card.h"
#include "joker.h"
#include "list.h"
#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);
POOL_ENTRY(CardObject, MAX_CARDS_ON_SCREEN);
POOL_ENTRY(ListNode, MAX_LIST_NODES);

View File

@ -0,0 +1,26 @@
DEF_JOKER_GFX(0)
DEF_JOKER_GFX(1)
DEF_JOKER_GFX(2)
DEF_JOKER_GFX(3)
DEF_JOKER_GFX(4)
DEF_JOKER_GFX(5)
DEF_JOKER_GFX(6)
DEF_JOKER_GFX(7)
DEF_JOKER_GFX(8)
DEF_JOKER_GFX(9)
DEF_JOKER_GFX(10)
DEF_JOKER_GFX(11)
DEF_JOKER_GFX(12)
DEF_JOKER_GFX(13)
DEF_JOKER_GFX(14)
DEF_JOKER_GFX(15)
DEF_JOKER_GFX(16)
DEF_JOKER_GFX(17)
DEF_JOKER_GFX(18)
DEF_JOKER_GFX(19)
DEF_JOKER_GFX(20)
DEF_JOKER_GFX(21)
DEF_JOKER_GFX(22)
DEF_JOKER_GFX(23)
DEF_JOKER_GFX(24)
DEF_JOKER_GFX(25)

View File

@ -0,0 +1,11 @@
// clang-format off
// (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, noop)
DEF_STATE_INFO(GAME_STATE_PLAYING, game_round_on_init, game_playing_on_update, noop)
DEF_STATE_INFO(GAME_STATE_ROUND_END, noop, game_round_end_on_update, game_round_end_on_exit)
DEF_STATE_INFO(GAME_STATE_SHOP, noop, game_shop_on_update, game_shop_on_exit)
DEF_STATE_INFO(GAME_STATE_BLIND_SELECT, game_blind_select_on_init, game_blind_select_on_update, game_blind_select_on_exit)
DEF_STATE_INFO(GAME_STATE_LOSE, game_lose_on_init, game_lose_on_update, game_over_on_exit)
DEF_STATE_INFO(GAME_STATE_WIN, game_win_on_init, game_win_on_update, game_over_on_exit)
// clang-format on

73
include/font.h Normal file
View File

@ -0,0 +1,73 @@
/**
* @file font.h
*
* @brief Utility file to interact with custom font
*/
#ifndef FONT_H
#define FONT_H
/** @name Decimal Point Fonts
* @brief A set of macros to map the fonts decimal-point values (e.g. ".1")
* to their replaced characters.
*
* "FP#" -> Font "Point" "number"
*
*
* For example:
* ```c
* tte_printf("Testing " FP0_STR " Something!");
* ```
* prints "Testing .0 Something!"
*
* The map is the following:
*
* ```c
* '&' == '.0'
* '^' == '.1'
* '}' == '.2'
* '{' == '.3'
* '|' == '.4'
* '`' == '.5'
* '<' == '.6'
* '>' == '.7'
* '_' == '.8'
* ';' == '.9'
* ```
*
* @{
*/
#define FP0_STR "&" // .0
#define FP1_STR "^" // .1
#define FP2_STR "}" // .2
#define FP3_STR "{" // .3
#define FP4_STR "|" // .4
#define FP5_STR "`" // .5
#define FP6_STR "<" // .6
#define FP7_STR ">" // .7
#define FP8_STR "_" // .8
#define FP9_STR ";" // .9
/** @} */
/**
* @brief Get the decimal point string equivalent of 0-9 from integer param.
*
* @param val Value between 0-9 to get ".0" to ".9" equivalents. Note that
* if a value outside of range is passed it will perform '% 10'
* on that value and the last decimal digit will be used.
*
* @return A char* to the string values of '.0' to '.9'
*/
const char* get_font_point_str(int val);
/**
* @brief Get the decimal point char equivalent of 0-9 from char param.
*
* @param digit_char A char of a digit between '0'-'9' to get '.0' to '.9' equivalents. Note that
* if a char outside of range is passed it the return value will be within limits
* but is not defined with any relationship to the input.
*
* @return A char representing a value in the font within the range of '.0' to '.9'
*/
char digit_char_to_font_point(char digit_char);
#endif // FONT_H

View File

@ -1,48 +1,69 @@
#ifndef GAME_H
#define GAME_H
#define MAX_HAND_SIZE 16
#define MAX_DECK_SIZE 52
#define MAX_JOKERS_HELD_SIZE 5 // This doesn't account for negatives right now.
#define MAX_SHOP_JOKERS 2 // TODO: Make this dynamic and allow for other items besides jokers
#define MAX_SELECTION_SIZE 5
#define MAX_CARD_SCORE_DIGITS 2 // Current digit limit for score received from cards including mult etc. from jokers
#define MAX_CARD_SCORE_STR_LEN (MAX_CARD_SCORE_DIGITS + 1) // For the '+' or 'X'
#define FRAMES(x) (((x) + game_speed - 1) / game_speed)
#include <tonc.h>
// TODO: Turn into enum?
#define BG_ID_CARD_SELECTING 1
#define BG_ID_CARD_PLAYING 2
#define BG_ID_ROUND_END 3
#define BG_ID_SHOP 4
#define BG_ID_BLIND_SELECT 5
#define BG_ID_MAIN_MENU 6
#define MAX_HAND_SIZE 16
#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_SELECTION_SIZE 5
#define FRAMES(x) (((x) + game_speed - 1) / game_speed)
// TODO: Can make these dynamic to support interest-related jokers and vouchers
#define MAX_INTEREST 5
#define INTEREST_PER_5 1
// Input bindings
#define SELECT_CARD KEY_A
#define SELECT_CARD KEY_A
#define DESELECT_CARDS KEY_B
#define PEEK_DECK KEY_L // Not implemented
#define SORT_HAND KEY_R
#define PAUSE_GAME KEY_START // Not implemented
#define SELL_KEY KEY_L
#define PEEK_DECK KEY_L // Not implemented
#define SORT_HAND KEY_R
#define PAUSE_GAME KEY_START // Not implemented
#define SELL_KEY KEY_L
// Matching the position of the on-screen buttons
#define PLAY_HAND_KEY KEY_L
// Same value as SELL_KEY - activated on the joker row, while this is activated on the hand row
#define DISCARD_HAND_KEY KEY_R
struct List;
typedef struct List List;
// Utility functions for other files
typedef struct CardObject CardObject;
typedef struct Card Card;
typedef struct JokerObject JokerObject;
enum BackgroundId
{
BG_NONE,
BG_CARD_SELECTING,
BG_CARD_PLAYING,
BG_ROUND_END,
BG_SHOP,
BG_BLIND_SELECT,
BG_MAIN_MENU
};
// Enum value names in ../include/def_state_info_table.h
enum GameState
{
GAME_SPLASH_SCREEN,
GAME_MAIN_MENU,
GAME_PLAYING,
GAME_ROUND_END,
GAME_SHOP,
GAME_BLIND_SELECT,
GAME_LOSE,
GAME_WIN
#define DEF_STATE_INFO(stateEnum, on_init, on_update, on_exit) stateEnum,
#include "def_state_info_table.h"
#undef DEF_STATE_INFO
GAME_STATE_MAX,
GAME_STATE_UNDEFINED
};
enum HandState
{
HAND_DRAW,
HAND_SELECT,
HAND_SHUFFLING, // This is actually a misnomer because it's used for the deck, but it mechanically makes sense to be a state of the hand
// This is actually a misnomer because it's used for the deck
// but it mechanically makes sense to be a state of the hand
HAND_SHUFFLING,
HAND_DISCARD,
HAND_PLAY,
HAND_PLAYING
@ -50,8 +71,13 @@ enum HandState
enum PlayState
{
PLAY_PLAYING,
PLAY_SCORING,
PLAY_STARTING,
PLAY_BEFORE_SCORING,
PLAY_SCORING_CARDS,
PLAY_SCORING_CARD_JOKERS,
PLAY_SCORING_HELD_CARDS,
PLAY_SCORING_INDEPENDENT_JOKERS,
PLAY_SCORING_HAND_SCORED_END,
PLAY_ENDING,
PLAY_ENDED
};
@ -64,10 +90,10 @@ enum HandType
PAIR,
TWO_PAIR,
THREE_OF_A_KIND,
FOUR_OF_A_KIND,
STRAIGHT,
FLUSH,
FULL_HOUSE,
FOUR_OF_A_KIND,
STRAIGHT_FLUSH,
ROYAL_FLUSH,
FIVE_OF_A_KIND,
@ -75,27 +101,81 @@ enum HandType
FLUSH_FIVE
};
// clang-format off
// Store all contained hands to optimize "whole hand condition" Jokers
typedef struct ContainedHandTypes
{
union
{
struct
{
u16 HIGH_CARD : 1;
u16 PAIR : 1;
u16 TWO_PAIR : 1;
u16 THREE_OF_A_KIND : 1;
u16 STRAIGHT : 1;
u16 FLUSH : 1;
u16 FULL_HOUSE : 1;
u16 FOUR_OF_A_KIND : 1;
u16 STRAIGHT_FLUSH : 1;
u16 ROYAL_FLUSH : 1;
u16 FIVE_OF_A_KIND : 1;
u16 FLUSH_HOUSE : 1;
u16 FLUSH_FIVE : 1;
u16 : 3;
};
u16 value;
};
} ContainedHandTypes;
// clang-format on
typedef struct
{
int substate;
void (*on_init)();
void (*on_update)();
void (*on_exit)();
} StateInfo;
// Game functions
void game_init();
void game_update();
void game_set_state(enum GameState new_game_state);
void game_change_state(enum GameState new_game_state);
// Forward declaration
struct List;
typedef struct List List;
CardObject** get_hand_array(void);
int get_hand_top(void);
int hand_get_size(void);
CardObject** get_played_array(void);
int get_played_top(void);
int get_scored_card_index(void);
bool is_joker_owned(int joker_id);
bool card_is_face(Card* card);
List* get_jokers_list(void);
List* get_expired_jokers_list(void);
// Utility functions for other files
typedef struct CardObject CardObject; // forward declaration, actually declared in card.h
typedef struct JokerObject JokerObject;
CardObject** get_hand_array(void);
int get_hand_top(void);
int hand_get_size(void);
CardObject** get_played_array(void);
int get_played_top(void);
List* get_jokers(void);
ContainedHandTypes* get_contained_hands(void);
enum HandType* get_hand_type(void);
int get_deck_top(void);
int get_num_discards_remaining(void);
int get_num_hands_remaining(void);
u32 get_chips(void);
void set_chips(u32 new_chips);
void display_chips();
u32 get_mult(void);
void set_mult(u32 new_mult);
void display_mult();
int get_money(void);
void set_money(int new_money);
void display_money();
void set_retrigger(bool new_retrigger);
int get_game_speed(void);
void set_game_speed(int new_game_speed);
// joker specific functions
bool is_shortcut_joker_active(void);
int get_straight_and_flush_size(void);
#endif // GAME_H

9
include/gbalatro_sys8.h Normal file
View File

@ -0,0 +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

View File

@ -1,182 +1,360 @@
#ifndef GRAPHIC_UTILS_H
#define GRAPHIC_UTILS_H
#include <tonc_video.h>
/* This file contains general utils and wrappers that relate to
/**
* @file graphic_utils.h
*
* @brief Graphic utility functions
*
* This file contains general utils and wrappers that relate to
* graphics/video/vram and generally displaying things on the screen.
* Mostly wrappers and defines for using tonc.
*
* Note: the code here assumes we're working with a single screenblock
* which should be true for this entire game since a screenblock
*
* Note: the code here assumes we're working with a single screenblock
* which should be true for this entire game since a screenblock
* is enough to contain a full screen (and more)
* and there isn't any scrolling etc.
*/
#ifndef GRAPHIC_UTILS_H
#define GRAPHIC_UTILS_H
/* Reminder:
#include <tonc_math.h>
#include <tonc_video.h>
/**
* @name Graphics Utilities Constants
*
* Reminder:
* Screen Base Block is the base for the screenblock entries i.e. tilemap
* Character Base Block is the base for the tiles themselves
*
* @{
*/
#define MAIN_BG_SBB 31
#define MAIN_BG_CBB 1
#define TTE_SBB 30
#define TTE_CBB 0
#define MAIN_BG_SBB 31
#define MAIN_BG_CBB 1
#define TTE_SBB 30
#define TTE_CBB 0
#define AFFINE_BG_SBB 2
#define AFFINE_BG_CBB 2
#define PAL_ROW_LEN 16
#define NUM_PALETTES 16
#define PAL_ROW_LEN 16
#define NUM_PALETTES 16
/**
* @def TILE_SIZE
* @brief Tile size in pixels, both height and width as tiles are square
*/
#define TILE_SIZE 8
/**
* @def TILE_MEM_OBJ_CHARBLOCK0_IDX
* @brief The index for the first charblock of the object memory in tonc's tile_mem
*/
#define TILE_MEM_OBJ_CHARBLOCK0_IDX 4
/** @} */
/**
* @name TTE Palette constants
*
* @{
*/
/** @def TTE_BIT_UNPACK_OFFSET */
#define TTE_BIT_UNPACK_OFFSET 14
/** @def TTE_BIT_ON_CLR_IDX */
#define TTE_BIT_ON_CLR_IDX TTE_BIT_UNPACK_OFFSET + 1
#define TTE_YELLOW_PB 12 // 0xC
#define TTE_BLUE_PB 13 // 0xD
#define TTE_RED_PB 14 // 0xE
#define TTE_WHITE_PB 15 // 0xF
/** @def TTE_YELLOW_PB */
#define TTE_YELLOW_PB 12 // 0xC
#define TEXT_CLR_YELLOW RGB15(31, 20, 0) // 0x029F
#define TEXT_CLR_BLUE RGB15(0, 18, 31) // 0x7E40
#define TEXT_CLR_RED RGB15(31, 9, 8) // 0x213F
#define TEXT_CLR_WHITE CLR_WHITE
/** @def TTE_BLUE_PB */
#define TTE_BLUE_PB 13 // 0xD
/* Dimensions for a screenblock.
/** @def TTE_RED_PB */
#define TTE_RED_PB 14 // 0xE
/** @def TTE_WHITE_PB */
#define TTE_WHITE_PB 15 // 0xF
/** @def TTE_SPECIAL_PB_MULT_OFFSET */
#define TTE_SPECIAL_PB_MULT_OFFSET 0x1000
/**
* @def TTE_CHAR_SIZE
*
* @brief By default TTE characters occupy a single tile
*/
#define TTE_CHAR_SIZE TILE_SIZE
/** @} */
/**
* @name Text colors
*
* @{
*/
/** @def TEXT_CLR_YELLOW */
#define TEXT_CLR_YELLOW RGB15(31, 20, 0) // 0x029F
/** @def TEXT_CLR_BLUE */
#define TEXT_CLR_BLUE RGB15(0, 18, 31) // 0x7E40
/** @def TEXT_CLR_RED */
#define TEXT_CLR_RED RGB15(31, 9, 8) // 0x213F
/** @def TEXT_CLR_WHITE */
#define TEXT_CLR_WHITE CLR_WHITE
/** @} */
/**
* @name Dimensions for a screenblock.
*
* A 1024 size screenblock is arranged in a grid of 32x32 screen entries
* Interestingly since each block is 8x8 pixels, the 240x160 GBA screen
* is smaller than the screenblock, only the top left part of the screenblock
* is displayed on the screen.
*
* @{
*/
/** @def SE_ROW_LEN */
#define SE_ROW_LEN 32
/** @def SE_COL_LEN */
#define SE_COL_LEN 32
// Since y direction goes from the top of the screen to the bottom
#define SCREEN_UP -1
#define SCREEN_DOWN 1
#define SCREEN_LEFT -1
#define SCREEN_RIGHT 1
/** @} */
#define SE_UP SCREEN_UP
#define SE_DOWN SCREEN_DOWN
enum ScreenVertDir
{
SCREEN_UP = -1,
SCREEN_DOWN = 1
};
#define OVERFLOW_LEFT SCREEN_LEFT
#define OVERFLOW_RIGHT SCREEN_RIGHT
enum ScreenHorzDir
{
SCREEN_LEFT = -1,
SCREEN_RIGHT = 1
};
// Tile size in pixels, both height and width as tiles are square
#define TILE_SIZE 8
#define EFFECT_TEXT_SEPARATION_AMOUNT 32; // If we need to show multiple effects at once
enum OverflowDir
{
OVERFLOW_LEFT = SCREEN_LEFT,
OVERFLOW_RIGHT = SCREEN_RIGHT
};
// By default TTE characters occupy a single tile
#define TTE_CHAR_SIZE TILE_SIZE
/** @} */
// When making this, missed that it already exists in tonc_math.h
typedef RECT Rect;
/* Gets the screenblock entry for the given coordinates (x, y).
* x and y are in number of tiles.
* Returns the screenblock entry.
/**
* @brief Get the width of a rectangle
*
* @param rect a @ref Rect to measure
*
* @return The width of the rectangle.
*/
SE main_bg_se_get_se(BG_POINT pos);
INLINE int rect_width(const Rect* rect)
{
/* Extra parens to avoid issues in case compiler turns INLINE into macro
* Not sure if necessary, could be just paranoia
*/
return (((rect)->right) - ((rect)->left) + 1);
return max(0, rect->right - rect->left + 1);
}
/**
* @brief Get the height of a rectangle
*
* @param rect a @ref Rect to measure
*
* @return The height of the rectangle, or 0 if rect->right < rect->left
*/
INLINE int rect_height(const Rect* rect)
{
return (((rect)->bottom) - ((rect)->top) + 1);
return max(0, rect->bottom - rect->top + 1);
}
/* Copies an SE rect vertically in direction by a single tile.
/**
* @brief Copies an SE rect vertically in direction by a single tile.
*
* bg_sbb is the SBB of the background in which to move the rect
* direction must be either SE_UP or SE_DOWN.
* se_rect dimensions are in number of tiles.
*
*
* NOTE: This does not work with TTE_SBB, probably because it's 4BPP...
*
* If you are doing this operation you are probably doing this in the main
* background and you should use main_bg_se_copy_rect_1_tile_vert() instead.
*
* @param bg_sbb the SBB of the background in which to move the rect.
*
* @param se_rect dimensions are in number of tiles.
*
* @param direction must be either @ref SE_UP or @ref SE_DOWN.
*/
void bg_se_copy_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int direction);
void bg_se_copy_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, enum ScreenVertDir direction);
/* Clears a rect in the main background.
* The se_rect dimensions need to be in number of tiles.
/**
* @brief Clears a rect in the main background.
*
* @param se_rect dimensions need to be in number of tiles.
*/
void main_bg_se_clear_rect(Rect se_rect);
/* Copies a rect in the main background vertically in direction by a single tile.
* direction must be either SE_UP or SE_DOWN.
* se_rect dimensions are in number of tiles.
/**
* @brief Copies a rect in the main background vertically in direction by a single tile.
*
* @param se_rect dimensions are in number of tiles.
*
* @param direction must be either @ref SE_UP or @ref SE_DOWN.
*/
void main_bg_se_copy_rect_1_tile_vert(Rect se_rect, int direction);
void main_bg_se_copy_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction);
/* Copies a rect in the main background from se_rect to the position (x, y).
* se_rect dimensions are in number of tiles.
* x and y are the coordinates in number of tiles.
/**
* @brief Copies a rect in the main background from se_rect to the position (x, y).
*
* @param se_rect dimensions are in number of tiles.
*
* @param dest_pos x and y are the coordinates in number of tiles.
*/
void main_bg_se_copy_rect(Rect se_rect, BG_POINT pos);
void main_bg_se_copy_rect(Rect se_rect, BG_POINT dest_pos);
/* Copies a screen entry to a rect in the main background.
* se_rect dimensions are in number of tiles.
* The tile is copied to the top left corner of the rect.
*/
void main_bg_se_fill_rect_with_se(SE tile, Rect se_rect);
/* Copies a 3x3 rect into se_rect_dest, the 3x3 rect is stretched to fill se_rect_dest.
* The corners are copied, the sides are stretched, and the center is filled.
* The parameter se_rect_src_3x3_top_left points to the top left corner of the source
* 3x3 rect.
* Dest rect sides can be of length 2, then the sides are not copied, only the corners.
* But dest rect sides must be at least 2.
/**
* @brief Copies a 3x3 rect and expands it to fit a passed rect.
*
* Performs the following operation:
*
* 1. The 3x3 rect is stretched to fill se_rect_dest.
* 2. The corners are copied
* 3. The sides are stretched
* 4. The center is filled.
*
* @param se_rect_dest destination for the 3x3 copy, if rect sides are length 2, then the sides are
* not copied, and the center is not filled (in the case of both top and bottom sides),
* and only the corners are copied.
* **But dest rect sides must be at least 2.**
*
* @param se_rect_src_3x3_top_left points to the top left corner of the source 3x3 rect.
*/
void main_bg_se_copy_expand_3x3_rect(Rect se_rect_dest, BG_POINT se_rect_src_3x3_top_left);
/* Moves a rect in the main background vertically in direction by a single tile.
/**
* @brief Copies a 3-width SE rect and expands it to fit a passed rect.
*
* Performs the following operation:
*
* 1. The sides are stretched
* 2. The center is filled.
*
* @param se_rect_dest destination for the copy; if rect width is 2, the center is not
* filled and only the sides are copied.
* **But dest rect width must be at least 2.**
*
* @param src_row_left_pnt points to the leftmost tile of the source 3-width rect.
*/
void main_bg_se_copy_expand_3w_row(Rect se_dest_rect, BG_POINT src_row_left_pnt);
/**
* @brief Moves a rect in the main background vertically in direction by a single tile.
*
* Note that tiles in the previous location will be transparent (0x000)
* so maybe copy would be a better choice if you don't want to delete things
* direction must be either SE_UP or SE_DOWN.
* se_rect dimensions are in number of tiles.
*
* @param se_rect dimensions are in number of tiles.
* @param direction must be either @ref SE_UP or @ref SE_DOWN.
*/
void main_bg_se_move_rect_1_tile_vert(Rect se_rect, int direction);
void main_bg_se_move_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction);
// A wrapper for tte_erase_rect that would use the rect struct
/**
* @brief A wrapper for tte_erase_rect that would use the rect struct
*
* @param rect rectangle to erase
*/
void tte_erase_rect_wrapper(Rect rect);
/* Changes rect->left so it fits the digits of num exactly when right aligned to rect->right.
/**
* @brief Changes rect->left so it fits the digits of num exactly when right aligned to rect->right.
* Assumes num is not negative.
*
*
* overflow_direction determines the direction the number will overflow
* if it's too large to fit inside the rect.
* Should be either OVERFLOW_LEFT or OVERFLOW_RIGHT.
*
* The rect is in number of pixels but should be a multiple of TILE_SIZE
* so it's a whole number of tiles to fit TTE characters
*
* if it's too large to fit inside the rect.
*
* Note that both rect->left and rect-right need to be defined, top and bottom don't matter
*
* @param rect is in number of pixels but should be a multiple of TILE_SIZE, so it's a whole number
* of tiles to fit TTE characters
*
* @param num number to display
*
* @param overflow_direction either OVERFLOW_LEFT or OVERFLOW_RIGHT.
*/
void update_text_rect_to_right_align_num(Rect* rect, int num, int overflow_direction);
void update_text_rect_to_right_align_str(
Rect* rect,
const char* str,
enum OverflowDir overflow_direction
);
/*Copies 16 bit data from src to dst, applying a palette offset to the data.
/**
* @brief Updates a rect so a string is centered within it.
*
* @param rect The rect provided, the provided values are used to determine the center
* and it is then updated so the string starting in rect->left is centered
* The rect is in number of pixels but should be a multiple of TTE_CHAR_SIZE
* so it's a whole number of tiles to fit TTE characters.
*
* @param str The string, the center of the string will be at the center of the updated rect.
*
* @param bias_direction Which direction to bias when the string can't be evenly centered
* with respect to char tiles.
* Examples:
* | |S|T|R| | - Can be evenly centered, bias has no effect
* | | |S|T|R| | - Bias right
* | |S|T|R| | | - Bias left
* |A|B|C|D| | - Bias left
* | |A|B|C|D| - Bias right
*/
void update_text_rect_to_center_str(Rect* rect, const char* str, enum ScreenHorzDir bias_direction);
/**
* @brief Copies 16 bit data from src to dst, applying a palette offset to the data.
*
* This is intended solely for use with tile8/8bpp data for dst and src.
* The palette offset allows the tiles to use a different location in the palette
* memory
* This is useful because grit always loads the palette to the beginning of
* pal_bg_mem[]
* The palette offset allows the tiles to use a different location in the palette * memory
* This is useful because grit always loads the palette to the beginning of pal_bg_mem[]
*
* @param dst destination charblock
*
* @param src destination charblock
*
* @param wcount Number of words to copy
*
* @param palette_offset palette offset to shift to
*/
void memcpy16_tile8_with_palette_offset(u16* dst, const u16* src, uint hwcount, u8 palette_offset);
/*Copies 32 bit data from src to dst, applying a palette offset to the data.
/**
* @brief Copies 32 bit data from src to dst, applying a palette offset to the data.
*
* This is intended solely for use with tile8/8bpp data for dst and src.
* The palette offset allows the tiles to use a different location in the palette
* memory
* This is useful because grit always loads the palette to the beginning of
* pal_bg_mem[]
* The palette offset allows the tiles to use a different location in the palette memory
* This is useful because grit always loads the palette to the beginning of pal_bg_mem[]
*
* @param dst destination charblock
*
* @param src destination charblock
*
* @param wcount Number of words to copy
*
* @param palette_offset palette offset to shift to
*/
void memcpy32_tile8_with_palette_offset(u32* dst, const u32* src, uint wcount, u8 palette_offset);
/* Toggles the visibility of the window layers.
* win0 and win1 are the visibility states for the two windows.
/**
* @brief Toggles the visibility of the window layers.
*
* These windows are primarily used for the shadows on held jokers, consumables and cards.
*
* @param win0 the visibility state for window 0
* @param win1 the visibility state for window 1
*/
void toggle_windows(bool win0, bool win1);
#endif //GRAPHIC_UTILS_H
#endif // GRAPHIC_UTILS_H

View File

@ -1,16 +1,42 @@
#ifndef HAND_ANALYSIS_H
#define HAND_ANALYSIS_H
#include <tonc.h>
#include "card.h"
void get_hand_distribution(u8 *ranks_out, u8 *suits_out);
void get_played_distribution(u8 *ranks_out, u8 *suits_out);
#include <tonc.h>
u8 hand_contains_n_of_a_kind(u8 *ranks);
bool hand_contains_two_pair(u8 *ranks);
bool hand_contains_full_house(u8 *ranks);
bool hand_contains_straight(u8 *ranks);
bool hand_contains_flush(u8 *suits);
/**
* @brief Outputs the distribution of ranks and suits in the hand
* @param ranks_out output - updated such as ranks_out[rank] is the number of cards of rank in the
* hand. Must be of size NUM_RANKS.
* @param suits_out output - updated such as suits_out[suit] is the number of cards if suit in the
* hand Must be of size NUM_SUITS
*/
void get_hand_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS]);
#endif
/**
* @brief Outputs the distribution of ranks and suits in the played stack
* @param ranks_out output - updated such as ranks_out[rank] is the number of cards of rank in the
* played stack. Must be of size NUM_RANKS.
* @param suits_out output - updated such as suits_out[suit] is the number of cards if suit in the
* played stack. Must be of size NUM_SUITS
*/
void get_played_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS]);
u8 hand_contains_n_of_a_kind(u8* ranks);
bool hand_contains_two_pair(u8* ranks);
bool hand_contains_full_house(u8* ranks);
bool hand_contains_straight(u8* ranks);
bool hand_contains_flush(u8* suits);
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
);
void select_paired_cards_in_hand(CardObject** played, int top, bool* selection);
#endif

View File

@ -1,94 +1,182 @@
#ifndef JOKER_H
#define JOKER_H
#include <maxmod.h>
#include "sprite.h"
#include "card.h"
#include "game.h"
#include "graphic_utils.h"
#include "sprite.h"
#define JOKER_TID (MAX_HAND_SIZE + MAX_SELECTION_SIZE) * JOKER_SPRITE_OFFSET // Tile ID for the starting index in the tile memory
#include <maxmod.h>
// This won't be more than the number of jokers in your current deck
// plus the amount that can fit in the shop, 8 should be fine. For now...
#define MAX_ACTIVE_JOKERS 8
#define MAX_DEFINABLE_JOKERS 150
// Tile ID for the starting index in the tile memory
#define JOKER_TID (MAX_HAND_SIZE + MAX_SELECTION_SIZE) * JOKER_SPRITE_OFFSET
#define JOKER_SPRITE_OFFSET 16 // Offset for the joker sprites
#define JOKER_BASE_PB 4 // The starting palette index for the jokers
#define JOKER_LAST_PB (NUM_PALETTES - 1)
#define JOKER_BASE_PB 4 // The starting palette index for the jokers
#define JOKER_LAST_PB (NUM_PALETTES - 1)
// Currently allocating the rest of the palettes for the jokers.
// This number needs to be decreased once we need to allocated palettes for other sprites
// such as planet cards etc.
#define JOKER_STARTING_LAYER 27
#define BASE_EDITION 0
#define FOIL_EDITION 1
#define HOLO_EDITION 2
#define POLY_EDITION 3
#define BASE_EDITION 0
#define FOIL_EDITION 1
#define HOLO_EDITION 2
#define POLY_EDITION 3
#define NEGATIVE_EDITION 4
#define MAX_EDITIONS 5
#define COMMON_JOKER 0
#define UNCOMMON_JOKER 1
#define RARE_JOKER 2
#define COMMON_JOKER 0
#define UNCOMMON_JOKER 1
#define RARE_JOKER 2
#define LEGENDARY_JOKER 3
// Percent chance to get a joker of each rarity
// Note that this deviates slightly from the Balatro wiki to allow legendary
// jokers to appear without spectral cards implemented
#define COMMON_JOKER_CHANCE 70
#define UNCOMMON_JOKER_CHANCE 25
#define RARE_JOKER_CHANCE 5
#define LEGENDARY_JOKER_CHANCE 0
// These are the common Joker Events. Special Joker behaviour will be checked on a
// Joker per Joker basis (see if it's there, then do something, e.g. Pareidolia, Baseball Card)
enum JokerEvent
{
JOKER_EVENT_ON_JOKER_CREATED, // Triggers only once when the Joker is created, mainly used for
// data initialization
JOKER_EVENT_ON_HAND_PLAYED, // Triggers only once when the hand is played and before the cards
// are scored
JOKER_EVENT_ON_CARD_SCORED, // Triggers when a played card scores (e.g. Walkie Talkie,
// Fibonnacci...)
JOKER_EVENT_ON_CARD_SCORED_END, // Triggers after the card has finishd scoring (e.g. retrigger
// Jokers)
JOKER_EVENT_ON_CARD_HELD, // Triggers when going through held cards
JOKER_EVENT_INDEPENDENT, // Joker will trigger normally, when Jokers are scored (e.g. base
// Joker)
JOKER_EVENT_ON_HAND_SCORED_END, // Triggers when entire hand has finished scoring (e.g. food
// Jokers)
JOKER_EVENT_ON_HAND_DISCARDED, // Triggers when discarding a hand
JOKER_EVENT_ON_ROUND_END, // Triggers at the end of the round (e.g. Rocket)
JOKER_EVENT_ON_BLIND_SELECTED, // Triggers when selecting a blind (e.g. Dagger, Riff Raff,
// Madness..)
};
// These are flags that can be combined into a single u32 and returned by
// JokerEffect functions to indicate which fields of the output JokerEffect are valid
#define JOKER_EFFECT_FLAG_NONE 0
#define JOKER_EFFECT_FLAG_CHIPS (1 << 0)
#define JOKER_EFFECT_FLAG_MULT (1 << 1)
#define JOKER_EFFECT_FLAG_XMULT (1 << 2)
#define JOKER_EFFECT_FLAG_MONEY (1 << 3)
#define JOKER_EFFECT_FLAG_RETRIGGER (1 << 4)
#define JOKER_EFFECT_FLAG_EXPIRE (1 << 5)
#define JOKER_EFFECT_FLAG_MESSAGE (1 << 6)
#define MAX_JOKER_OBJECTS 32 // The maximum number of joker objects that can be created at once
#define DEFAULT_JOKER_ID 0
#define GREEDY_JOKER_ID 1 // This is just an example to show the patern of making joker IDs
#define JOKER_STENCIL_ID 16
// Jokers in the game
#define DEFAULT_JOKER_ID 0
#define GREEDY_JOKER_ID 1
#define STENCIL_JOKER_ID 16
#define SHORTCUT_JOKER_ID 26
#define PAREIDOLIA_JOKER_ID 30
#define BLUEPRINT_JOKER_ID 39
#define BRAINSTORM_JOKER_ID 40
#define FOUR_FINGERS_JOKER_ID 48
typedef struct
typedef struct
{
u8 id; // Unique ID for the joker, used to identify different jokers
u8 id; // Unique ID for the joker, used to identify different jokers
u8 modifier; // base, foil, holo, poly, negative
u8 value;
u8 rarity;
bool processed;
// General purpose values that are interpreted differently for each Joker (scaling, last
// retriggered card, etc...)
s32 scoring_state;
s32 persistent_state;
} Joker;
typedef struct JokerObject
{
Joker *joker;
Joker* joker;
SpriteObject* sprite_object;
} JokerObject;
typedef struct // These jokers are triggered after the played hand has finished scoring.
typedef struct // These jokers are triggered after the played hand has finished scoring.
{
int chips;
int mult;
int xmult;
u32 chips;
u32 mult;
u32 xmult;
int money;
bool retrigger; // Retrigger played hand (e.g. "Dusk" joker, even though on the wiki it says "On Scored" it makes more sense to have it here)
bool retrigger; // Retrigger played hand (e.g. "Dusk" joker, even though on the wiki it says "On
// Scored" it makes more sense to have it here)
bool expire; // Will make the Joker expire/destry itself if true (i.e. Bananas and fully
// consumed Food Jokers)
char* message; // Used to send custom messages e.g. "Extinct!" or "Again!"
} JokerEffect;
typedef JokerEffect (*JokerEffectFunc)(Joker *joker, Card *scored_card);
typedef struct {
// JokerEffectFuncs take in a joker that will be scored, a scored_card that is not NULL when related
// to the given joker_event, and output a joker_effect storing the effects of the scored joker They
// return a set of flags indicating what fields of the joker_effect are valid to access
typedef u32 (*JokerEffectFunc)(
Joker* joker,
Card* scored_card,
enum JokerEvent joker_event,
JokerEffect** joker_effect
);
typedef struct
{
u8 rarity;
u8 base_value;
JokerEffectFunc effect;
JokerEffectFunc joker_effect_func;
} JokerInfo;
const JokerInfo* get_joker_registry_entry(int joker_id);
size_t get_joker_registry_size(void);
void joker_init();
Joker *joker_new(u8 id);
void joker_destroy(Joker **joker);
Joker* joker_new(u8 id);
void joker_destroy(Joker** joker);
// Unique effects like "Four Fingers" or "Credit Card" will be hard coded into game.c with a conditional check for the joker ID from the players owned jokers
// game.c should probably be restructured so most of the variables in it are moved to some sort of global variable header file so they can be easily accessed and modified for the jokers
JokerEffect joker_get_score_effect(Joker *joker, Card *scored_card);
// Unique effects like "Four Fingers" or "Credit Card" will be hard coded into game.c with a
// conditional check for the joker ID from the players owned jokers game.c should probably be
// restructured so most of the variables in it are moved to some sort of global variable header file
// so they can be easily accessed and modified for the jokers
u32 joker_get_score_effect(
Joker* joker,
Card* scored_card,
enum JokerEvent joker_event,
JokerEffect** joker_effect
);
int joker_get_sell_value(const Joker* joker);
JokerObject *joker_object_new(Joker *joker);
void joker_object_destroy(JokerObject **joker_object);
void joker_object_update(JokerObject *joker_object);
void joker_object_shake(JokerObject *joker_object, mm_word sound_id); // This doesn't actually score anything, it just performs an animation and plays a sound effect
bool joker_object_score(JokerObject *joker_object, Card* scored_card, int *chips, int *mult, int *xmult, int *money, bool *retrigger); // This scores the joker and returns true if it was scored successfully (Card = NULL means the joker is independent and not scored by a card)
void joker_object_set_selected(JokerObject* joker_object, bool selected);
bool joker_object_is_selected(JokerObject* joker_object);
JokerObject* joker_object_new(Joker* joker);
void joker_object_destroy(JokerObject** joker_object);
void joker_object_update(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);
// This scores the joker and returns true if it was scored successfully
// card_object = NULL means the joker_event does not concern a particular Card, i.e. Independend or
// On_Blind_Selected as opposed to events that concern a particular card, i.e. On_Card_Scored or
// On_Card_Held
bool joker_object_score(
JokerObject* joker_object,
CardObject* card_object,
enum JokerEvent joker_event
);
Sprite* joker_object_get_sprite(JokerObject* joker_object);
int joker_get_random_rarity();
#endif // JOKER_H
#endif // JOKER_H

View File

@ -1,3 +1,4 @@
// clang-format off
#ifndef JOKER_GFX_H
#define JOKER_GFX_H
@ -16,5 +17,17 @@
#include "joker_gfx12.h"
#include "joker_gfx13.h"
#include "joker_gfx14.h"
#include "joker_gfx15.h"
#include "joker_gfx16.h"
#include "joker_gfx17.h"
#include "joker_gfx18.h"
#include "joker_gfx19.h"
#include "joker_gfx20.h"
#include "joker_gfx21.h"
#include "joker_gfx22.h"
#include "joker_gfx23.h"
#include "joker_gfx24.h"
#include "joker_gfx25.h"
#endif
#endif
// clang-format on

View File

@ -1,25 +1,271 @@
#ifndef LIST_H
#define LIST_H
#include <stdbool.h>
typedef struct List
{
void** _array;
int size;
int allocated_size;
} List;
List *list_new(int init_size);
void list_destroy(List **list);
bool list_append(List *list, void *value);
bool list_remove_by_idx(List *list, int index);
void* list_get(List *list, int index);
int list_get_size(List *list);
bool list_remove_by_value(List *list, void *value);
bool int_list_append(List *list, intptr_t value);
intptr_t int_list_get(List *list, int index);
bool int_list_remove_by_value(List *list, intptr_t value);
#endif
/**
* @file list.h
*
* @brief A doubly-linked list
*
* List Implementation
* ===================
*
* - This @ref List operates as a linked list @ref ListNodes. It operates as a regular
* doubly-linked list but doesn't allocate memory and rather gets @ref ListNodes from a pool.
*/
#ifndef LIST_H
#define LIST_H
#include <stdbool.h>
/**
* @def MAX_LIST_NODES
* @brief Number of reserved list nodes.
*
* Number of list nodes available from the pool of @ref ListNode . This should
* be set to to the maximum number of list nodes needed at once.
*/
#define MAX_LIST_NODES 128
typedef struct ListNode ListNode;
/**
* @brief A single entry in a @ref List
*/
struct ListNode
{
/**
* @brief The previous @ref ListNode in the associated @ref List, NULL if at the `head` of the
* list
*/
ListNode* prev;
/**
* @brief The next @ref ListNode in the associated @ref List, NULL if at the `tail` of the list
*/
ListNode* next;
/**
* @brief Pointer to generic data stored in this node
*/
void* data;
};
/**
* @brief A doubly-linked list
*/
typedef struct List
{
/**
* @brief The first entry in the list
*/
ListNode* head;
/**
* @brief The last entry in the list
*/
ListNode* tail;
/**
* @brief Number of elements in list
*/
int len;
} List;
/**
* @brief @ref ListItr direction
*/
enum ListItrDirection
{
LIST_ITR_FORWARD,
LIST_ITR_REVERSE,
};
/**
* @brief An iterator into a list
*/
typedef struct
{
/**
* @brief A pointer to the @ref List this is iterating through
*/
List* list;
/**
* @brief The next node in the list
*/
ListNode* next_node;
/**
* @brief The current node in the list iterator
*
* The node of the most recently returned data from @ref list_itr_next() .
*/
ListNode* current_node;
/**
* @brief The direction of the iterator
*/
enum ListItrDirection direction;
} ListItr;
/**
* Create a list.
*
* While this function does not allocate memory for the list itself, the list does allocate memory
* for each element. So every created list must be freed with @ref list_clear to ensure the list's
* nodes are deleted properly.
*
* @return A @ref List with head and tail reset.
*/
List list_create(void);
/**
* Clear a list.
*
* Go through the list and free each node and set the `head` and `tail` to `NULL`.
* Note, it doesn't "free" the data at the node.
*
* @param list pointer to a @ref List to clear
*/
void list_clear(List* list);
/**
* Check if a list is empty
*
* @param list pointer to a @ref List
*
* @return `true` if the `list` is empty, `false` otherwise.
*/
bool list_is_empty(const List* list);
/**
* Prepend an entry to the `head` of a @ref list
*
* @param list pointer to a @ref List
* @param data pointer to data to put into the @ref List
*/
void list_push_front(List* list, void* data);
/**
* Append an entry to the `tail` of a @ref list
*
* @param list pointer to a @ref List
* @param data pointer to data to put into the @ref List
*/
void list_push_back(List* list, void* data);
/**
* Insert data into a @ref List a specific index
*
* If the index specified is larger than the length of the list
* it will @ref list_push_back() the data instead;
*
* Performs the following operation:
*
*
* node
*
*
* idx-1 idx idx+1
*
*
* 1. Set new `node` `prev` to the node at idx - 1
* 2. Set new `node` `next` to the node at idx
* 3. Set node at idx - 1 `next` to new `node`
* 4. Set node at idx `prev` to the new `node`
*
* Result:
*
*
* idx-1 node idx idx+1
*
*
* Finally, the list is now updated with new `node` now at the labeled idx:
*
*
* idx-1 idx idx+1idx+2
*
*
* @param list pointer to a @ref List
* @param data pointer to data to put into the @ref List
* @param idx desired index to insert
*/
void list_insert(List* list, void* data, unsigned int idx);
/**
* Swap the data pointers at the specified indices of a @ref List
*
* If either indices are larger than the length of the list, return false.
*
* @param list pointer to a @ref List
* @param idx_a desired index to swap with idx_b
* @param idx_b desired index to swap with idx_a
*
* @return true if successful, false otherwise
*/
bool list_swap(List* list, unsigned int idx_a, unsigned int idx_b);
/**
* Get a List's node at the specified index
*
* @param list pointer to a @ref List
* @param idx index of the desired @ref ListNode in the list
*
* @return a pointer to the data at the index of the list, or NULL if out-of-bounds
*/
void* list_get_at_idx(List* list, unsigned int idx);
/**
* Remove a List's node at the specified index
*
* @param list pointer to a @ref List
* @param idx index of the desired @ref ListNode in the list
*
* @return `true` if successfully removed, `false` if out-of-bounds
*/
bool list_remove_at_idx(List* list, unsigned int idx);
/**
* Get the number of elements in a @ref List
*
* @param list pointer to a @ref List
*
* @return The number of elements in the list
*/
int list_get_len(const List* list);
/**
* Declare a @ref ListItr
*
* @param list pointer to a @ref List
*
* @return A new @ref ListItr
*/
ListItr list_itr_create(List* list);
/**
* Declare a reverse @ref ListItr
*
* @param list pointer to a @ref List
*
* @return A new reverse @ref ListItr
*/
ListItr rev_list_itr_create(List* list);
/**
* Get the next data entry in a @ref ListItr
*
* @param itr pointer to the @ref ListItr
*
* @return A pointer to the data pointer at the next @ref ListNode if valid, otherwise return NULL.
*/
void* list_itr_next(ListItr* itr);
/**
* Remove the current @ref ListNode from the iterator.
*
* The "current node" corresponds to the list node associated with the
* most recently returned valu from @ref list_itr_next()
*
* @param itr pointer to the @ref ListItr
*/
void list_itr_remove_current_node(ListItr* itr);
#endif

67
include/pool.h Normal file
View File

@ -0,0 +1,67 @@
#ifndef POOL_H
#define POOL_H
#include "bitset.h"
#include <stdbool.h>
#include <stdint.h>
#ifdef POOLS_TEST_ENV
#define POOLS_DEF_FILE "def_test_mempool.h"
#else
#define POOLS_DEF_FILE "def_balatro_mempool.h"
#endif
#define POOL_DECLARE_TYPE(type) \
typedef struct \
{ \
Bitset* bitset; \
type* objects; \
} type##Pool; \
type* pool_get_##type(); \
void pool_free_##type(type* obj); \
int pool_idx_##type(type* obj); \
type* pool_at_##type(int idx);
#define POOL_DEFINE_TYPE(type, capacity) \
BITSET_DEFINE(type##_bitset, capacity) \
static type type##_storage[capacity]; \
static type##Pool type##_pool = { \
.bitset = &type##_bitset, \
.objects = type##_storage, \
}; \
type* pool_get_##type() \
{ \
int free_offset = bitset_set_next_free_idx(type##_pool.bitset); \
if (free_offset == -1) \
return NULL; \
return &type##_pool.objects[free_offset]; \
} \
void pool_free_##type(type* entry) \
{ \
if (entry == NULL) \
return; \
int offset = entry - &type##_pool.objects[0]; \
bitset_set_idx(type##_pool.bitset, offset, false); \
} \
int pool_idx_##type(type* entry) \
{ \
return entry - &type##_pool.objects[0]; \
} \
type* pool_at_##type(int idx) \
{ \
if (idx < 0 || idx >= (type##_pool.bitset)->cap) \
return NULL; \
return &type##_pool.objects[idx]; \
}
#define POOL_GET(type) pool_get_##type()
#define POOL_FREE(type, obj) pool_free_##type(obj)
#define POOL_IDX(type, obj) pool_idx_##type(obj) // the index of the object
#define POOL_AT(type, idx) pool_at_##type(idx) // the object at
#define POOL_ENTRY(name, capacity) POOL_DECLARE_TYPE(name);
#include POOLS_DEF_FILE
#undef POOL_ENTRY
#endif // POOL_H

View File

@ -1,5 +1,18 @@
/**
* @file selection_grid.h
*
* @brief An implementation for a selection grid that handles directional selection.
*
* The selection grid is not the same vertically and horizontally.
* It is divided into rows and each row defines its own callbacks for size, directional changes,
* and selection input.
* The idea is that it would be a more fitting simple solution for this game since it tends
* to be more dynamic horizontally than vertically with differing sizes of hands, jokers, etc.
*/
#ifndef SELECTION_GRID_H
#define SELECTION_GRID_H
#include <tonc.h>
typedef POINT Selection;
@ -9,33 +22,138 @@ struct SelectionGrid;
typedef struct SelectionGridRow SelectionGridRow;
typedef struct SelectionGrid SelectionGrid;
// Called whenever there is a change in the selection cursor
// row_idx is the index of the row whose function is invoked - can be used to identify whether it is the previous or new selection row.
typedef void (*RowOnSelectionChangedFunc)(SelectionGrid* selection_grid, int row_idx, const Selection* prev_selection, const Selection* new_selection);
typedef int (*RowGetSizeFunc)();
// Called for any non-directional key hit
// The key will not be passed, the function will have to check key_hit() etc. for the key it wants to check
typedef void (*RowOnKeyHitFunc)(SelectionGrid* selection_grid, Selection* selection);
/**
* @brief Callback function type for handling selection changes in a SelectionGrid.
*
* This function is invoked whenever the selection cursor changes position within
* the grid. Used to perform custom actions based on selection changes,
* such as updating UI elements.
*
* @param selection_grid Pointer to the SelectionGrid instance where the change occurred
* @param row_idx Index of the row associated with this callback. This can be used to
* determine whether this is the previously selected row or the newly
* selected row
* @param prev_selection Pointer to the Selection state before the change occurred.
* Contains the previous cursor position
* @param new_selection Pointer to the Selection state after the change occurred.
* Contains the new cursor position
* @return false if the selection change needs to be aborted, true if can proceed.
*
*/
typedef bool (*RowOnSelectionChangedFunc)(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
);
/**
* @brief Function pointer type for retrieving the size of a row in a selection grid.
*
* This is useful to allow generic row lengths e.g. when it can depend on the number of cards
* in hand, items in the shop, etc.
*
* @return int The number of elements in the row.
*/
typedef int (*RowGetSizeFunc)();
/**
* @brief Callback function type for handling non-directional key presses in a selection grid row.
*
* This function is invoked whenever a non-directional key transitions
* (either hit down or release up).
* The specific key and the type of transition
* (press/release) are not passed as parameters to the callback. Instead, the implementation must
* query the key state using functions like key_hit(), key_released(), etc. to determine which key
* triggered the event and its current state.
*
* @param selection_grid Pointer to the SelectionGrid that contains the row receiving the key event
* @param selection Pointer to the Selection (row) that is handling the key transition.
*/
typedef void (*RowOnKeyTransitFunc)(SelectionGrid* selection_grid, Selection* selection);
/**
* @brief A set of attributes to the selection grid row affecting selection grid behavior.
*/
typedef struct
{
/**
* @brief Whether to wrap selection when it passes the end of the row.
*/
bool wrap;
} SelGridRowAttributes;
/**
* @brief A single row in the selection grid, defined by its callback functions.
*
* @var row_idx is used to easily identify the row within the callbacks.
*/
struct SelectionGridRow
{
int row_idx;
RowGetSizeFunc get_size;
RowOnSelectionChangedFunc on_selection_changed;
RowOnKeyHitFunc on_key_hit;
RowGetSizeFunc get_size;
RowOnSelectionChangedFunc on_selection_changed;
RowOnKeyTransitFunc on_key_transit;
SelGridRowAttributes attributes;
};
/**
* @brief The core selection grid struct, represents the grid itself and its state.
*
* It can be statically defined with an array of @ref SelectionGridRow to define its
* contents.
*/
struct SelectionGrid
{
SelectionGridRow* rows;
const SelectionGridRow* rows;
const int num_rows;
Selection selection;
};
};
/**
* @brief Processes user input for the selection grid.
*
* This function handles input events (such as directional controls and button presses)
* and updates the selection grid's state accordingly. It should be called each frame
* to respond to user interactions with the grid.
*
* @param selection_grid Pointer to the SelectionGrid structure to process input for.
* Must not be NULL. NULL-checks are in place and will return early.
*/
void selection_grid_process_input(SelectionGrid* selection_grid);
/**
* @brief Moves the selection horizontally within the selection grid.
*
* This function updates the current selection position by moving it horizontally
* based on the specified direction. This can be useful if some non-press event
* should update the selection grid.
*
* @param selection_grid Pointer to the SelectionGrid structure to operate on.
* Must not be NULL. NULL-checks are in place and will return early.
* @param direction_tribool Direction indicator for horizontal movement behaving like tonc's
* tribools:
* - Negative value: move left
* - Zero: no movement
* - Positive value: move right
*/
void selection_grid_move_selection_horz(SelectionGrid* selection_grid, int direction_tribool);
/**
* @brief Moves the selection vertically within the selection grid.
*
* This function updates the current selection position by moving it vertically
* based on the specified direction. This can be useful if some non-press event
* should update the selection grid.
*
* @param selection_grid Pointer to the SelectionGrid structure to operate on.
* Must not be NULL. NULL-checks are in place and will return early.
* @param direction_tribool Direction indicator for vertical movement behaving like tonc's tribools:
* - Negative value: move up
* - Zero: no movement
* - Positive value: move down
*/
void selection_grid_move_selection_vert(SelectionGrid* selection_grid, int direction_tribool);
#endif
#endif

View File

@ -1,13 +1,38 @@
/**
* @file splash_screen.h
*
* @brief Functions and constants for the splash screen intro
*
*/
#ifndef SPLASH_SCREEN_H
#define SPLASH_SCREEN_H
#include <tonc.h>
#define SPLASH_FPS 60
/** @name Splash screen timing variables
*
* Constants for timing the duration of the splash screen.
*
* @{
*/
#define SPLASH_FPS 60
#define SPLASH_DURATION_SECONDS 10
#define SPLASH_DURATION_FRAMES (SPLASH_FPS * SPLASH_DURATION_SECONDS)
#define SPLASH_DURATION_FRAMES (SPLASH_FPS * SPLASH_DURATION_SECONDS)
/** @} */
void splash_screen_init();
void splash_screen_update(uint timer);
/**
* @brief Initialize the splash screen by printing the splash screen text.
*/
void splash_screen_on_init(void);
#endif // SPLASH_SCREEN_H
/**
* @brief Update splash screen timers and print the remaining time accordingly.
*/
void splash_screen_on_update(void);
/**
* @brief Exit the splash screen
*/
void splash_screen_on_exit(void);
#endif // SPLASH_SCREEN_H

View File

@ -1,42 +1,332 @@
/**
* @file sprite.h
*
* @brief Sprite system for Gbalatro
*/
#ifndef SPRITE_H
#define SPRITE_H
#include <tonc.h>
#include <maxmod.h>
#include <tonc.h>
#define CARD_SPRITE_SIZE 32
/**
* @name Sprite system constants
* @{
*/
#define CARD_SPRITE_SIZE 32
#define MAX_AFFINES 32
#define MAX_SPRITES 128
#define MAX_SPRITE_OBJECTS 16
#define SPRITE_FOCUS_RAISE_PX 10
#define CARD_FOCUS_SFX_PITCH_OFFSET_RANGE 512
typedef struct
{
OBJ_ATTR *obj;
OBJ_AFFINE *aff;
POINT pos;
} Sprite;
/** @} */
// A sprite object is a sprite that is selectable and movable in animation
/**
* @brief Sprite struct for GBA hardware specifics
*/
typedef struct
{
/**
* @brief GBA sprite attribute registers info (A0-A2)
*/
OBJ_ATTR* obj;
/**
* @brief GBA sprite affine matrices registers info
*/
OBJ_AFFINE* aff;
/**
* @brief Sprite position on screen in pixels
*/
POINT pos;
/**
* @brief Sprite index in memory managed by GBAlatro
*/
int idx;
} Sprite;
/**
* @brief A sprite object is a sprite that is focusable and movable in animation
*/
typedef struct
{
/**
* @brief Sprite configuration info
*/
Sprite* sprite;
FIXED tx, ty; // target position
FIXED x, y; // position
FIXED vx, vy; // velocity
/**
* @brief Target position
*/
FIXED tx, ty;
/**
* @brief Current position
*/
FIXED x, y;
/**
* @brief Velocity
*/
FIXED vx, vy;
/**
* @brief Target Scale
*/
FIXED tscale;
/**
* @brief Current Scale, in units for tonc's `obj_aff_rotscale`
*/
FIXED scale;
/**
* @brief Scale velocity AKA the rate of change of scaling ops
*/
FIXED vscale;
FIXED trotation; // this never gets used so i might remove it later
/**
* @brief Target rotation
*/
FIXED trotation;
/**
* @brief Actual rotation, in units for tonc's `obj_aff_rotscale`
*/
FIXED rotation;
/**
* @brief Rotation velocity
*/
FIXED vrotation;
bool selected;
/**
* @brief Focused status (card specific, raise and lower card)
*/
bool focused;
} SpriteObject;
// Sprite methods
Sprite *sprite_new(u16 a0, u16 a1, u32 tid, u32 pb, int sprite_index);
Sprite *affine_sprite_new(u16 a0, u16 a1, u32 tid, u32 pb);
void sprite_destroy(Sprite **sprite);
int sprite_get_layer(Sprite *sprite);
INLINE void sprite_position(Sprite *sprite, int x, int y)
/**
* @brief Allocate and retrieve a pointer to a valid Sprite
*
* @param a0 attribute 0 of OBJ_ATTR
* @param a1 attribute 1 of OBJ_ATTR
* @param tid base tile index of sprite, part of attribute 2
* @param pb Palette-bank
* @param sprite_index index in memory
*
* @return Valid Sprite if allocations are successful.
* Otherwise, return **NULL**.
*/
Sprite* sprite_new(u16 a0, u16 a1, u32 tid, u32 pb, int sprite_index);
/**
* @brief Destroy Sprite
*
* @param sprite pointer to a pointer of Sprite to destroy. No action if **NULL**.
*/
void sprite_destroy(Sprite** sprite);
/**
* @brief Get index of Sprite in the GBA object buffer
*
* @param sprite pointer to Sprite, cannot be **NULL**
*
* @return Index of sprite in object buffer if `sprite` is valid, otherwise **UNDEFINED**.
*/
int sprite_get_layer(Sprite* sprite);
/**
* @brief Get a Sprite's width and height
*
* @param sprite pointer to Sprite, cannot be **NULL**
* @param width pointer to variable to be set, cannot be **NULL**
* @param height pointer to variable to be set, cannot be **NULL**
*
* @return **true** if successful, **false** if otherwise. Upon success,
* `width` and `height` contain valid data, otherwise, the
* variables are unchanged.
*/
bool sprite_get_dimensions(Sprite* sprite, int* width, int* height);
/**
* @brief Get a Sprites's height
*
* @param sprite pointer to Sprite, cannot be **NULL**
* @param height pointer to variable to be set, cannot be **NULL**
*
* @return **true** is successful, **false** if otherwise. Upon success,
* `height` contains valid data, otherwise, the variable is unchanged.
*/
bool sprite_get_height(Sprite* sprite, int* height);
/**
* @brief Get a Sprite's width
*
* @param sprite pointer to Sprite, cannot be **NULL**
* @param width pointer to variable to be set, cannot be **NULL**
*
* @return **true** is successful, **false** if otherwise. Upon success,
* `width` contains valid data, otherwise, the variable is unchanged.
*/
bool sprite_get_width(Sprite* sprite, int* width);
/**
* @brief Get the palette bank of a Sprite
*
* @param sprite pointer to extract associated palette bank. Cannot be **NULL**.
*
* @return The palette bank of the Sprite if successful, otherwise return **UNDEFINED**.
*/
int sprite_get_pb(const Sprite* sprite);
/**
* @brief Initialize GBAlatro sprite system
*/
void sprite_init(void);
/**
* @brief Draw Sprites to screen, to be called once per frame
*/
void sprite_draw(void);
/**
* @brief Allocate and retrieve a pointer to a valid SpriteObject
*
* @return A valid pointer to an newly allocated SpriteObject
* if successful, othewise return **NULL**.
*/
SpriteObject* sprite_object_new();
/**
* @brief Destroy SpriteObject
*
* Destroy a SpriteObject by freeing it back to the pool and releasing its
* associated resources
*
* @param sprite_object pointer to a pointer of SpriteObject to destroy.
* Cannot be **NULL**.
*/
void sprite_object_destroy(SpriteObject** sprite_object);
/**
* @brief Register a Sprite to an associated SpriteObject
*
* @param sprite_object pointer to SpriteObject to associate Sprite with.
* Cannot be **NULL**.
*
* @param sprite pointer to Sprite to associate SpriteObject with.
* Cannot be **NULL**.
*/
void sprite_object_set_sprite(SpriteObject* sprite_object, Sprite* sprite);
/**
* @brief Reset SpriteObject's transform back to default values.
*
* @param sprite_object pointer to SpriteObject to reset transform.
* Cannot be **NULL**.
*/
void sprite_object_reset_transform(SpriteObject* sprite_object);
/**
* @brief Update a SpriteObject, to be called once per frame per active SpriteObject
*
* @param sprite_object pointer to SpriteObject to update. Cannot be **NULL**.
*/
void sprite_object_update(SpriteObject* sprite_object);
/**
* @brief Shake SpriteObject on screen and play a sound
*
* @param SpriteObject pointer to SpriteObject to shake. Cannot be **NULL**.
* @param sound_id ID of sound from maxmod to play on executing shake. If **UNDEFINED**
* no sound will play.
*/
void sprite_object_shake(SpriteObject* sprite_object, mm_word sound_id);
/**
* @brief Get a SpriteObject's registered Sprite
*
* @param sprite_object pointer to SpriteObject's registered Sprite. Cannot be **NULL**.
*
* @return Sprite pointer registered to `sprite_object` if successful,
* otherwise return **NULL**. May be successful and **NULL** if there is no
* Sprite registered to the SpriteObject.
*/
Sprite* sprite_object_get_sprite(SpriteObject* sprite_object);
/**
* @brief Set the focus for SpriteObject
* Raises the object by SPRITE_FOCUS_RAISE_PX.
*
* Note: This is currently unused by CardObject as their focus is handled in
* cards_in_hand_update_loop() but we may want to extract it from there and refactor them use this
* instead.
*
* @param sprite_object pointer to SpriteObject to set the focus of. Cannot be **NULL**.
* @param focus **true** to focus, **false** to unfocus
*/
void sprite_object_set_focus(SpriteObject* sprite_object, bool focus);
/**
* @brief Get the width and height of SpriteObject's registered Sprite
*
* @param sprite_object pointer to SpriteObject to get the dimensions of. Cannot be **NULL**.
* @param width pointer to variable to be set, cannot be **NULL**
* @param height pointer to variable to be set, cannot be **NULL**
*
* @return **true** is successful, **false** if otherwise. Upon success,
* `width` and `height` contain valid data, otherwise, the
* variables are unchanged.
*/
bool sprite_object_get_dimensions(SpriteObject* sprite_object, int* width, int* height);
/**
* @brief Get a SpriteObject's height
*
* @param sprite_object pointer to SpriteObject to get the height of. Cannot be **NULL**.
* @param height pointer to variable to be set, cannot be **NULL**
*
* @return **true** is successful, **false** if otherwise. Upon success,
* `height` contains valid data, otherwise, the
* variable is unchanged.
*/
bool sprite_object_get_height(SpriteObject* sprite_object, int* height);
/**
* @brief Get a SpriteObject's width
*
* @param sprite_object pointer to SpriteObject to get the width of. Cannot be **NULL**.
* @param width pointer to variable to be set, cannot be **NULL**
*
* @return **true** is successful, **false** if otherwise. Upon success,
* `width` contains valid data, otherwise, the
* variable is unchanged.
*/
bool sprite_object_get_width(SpriteObject* sprite_object, int* width);
/**
* @brief Get the `focused` variable from a SpriteObject
*
* @param sprite_object valid pointer to SpriteObject to check
*
* @return `true` if the SpriteObject is focused, `false` otherwise
*/
bool sprite_object_is_focused(SpriteObject* sprite_object);
/**
* @brief Set sprite position. Inlined for efficiency
*
* @param sprite poitner to Sprite to adjust the position of. A **NULL** check is
* not performed, though the value cannot be **NULL**.
*
* @param x horizontal position in pixels
* @param y vertical position in pixels
*/
INLINE void sprite_position(Sprite* sprite, int x, int y)
{
sprite->pos.x = x;
sprite->pos.y = y;
@ -44,23 +334,4 @@ INLINE void sprite_position(Sprite *sprite, int x, int y)
obj_set_pos(sprite->obj, x, y);
}
// Sprite functions
void sprite_init();
void sprite_draw();
int sprite_get_pb(const Sprite* sprite);
// SpriteObject methods
SpriteObject *sprite_object_new();
void sprite_object_destroy(SpriteObject **sprite_object);
void sprite_object_set_sprite(SpriteObject* sprite_object, Sprite* sprite);
void sprite_object_reset_transform(SpriteObject* sprite_object);
void sprite_object_update(SpriteObject *sprite_object);
void sprite_object_shake(SpriteObject* sprite_object, mm_word sound_id);
void sprite_object_set_selected(SpriteObject* sprite_object, bool selected);
bool sprite_object_is_selected(SpriteObject* sprite_object);
Sprite* sprite_object_get_sprite(SpriteObject* sprite_object);
void sprite_object_set_focus(SpriteObject* sprite_object, bool focus);
bool sprite_object_is_focused(SpriteObject* sprite_object);
#endif // SPRITE_H
#endif // SPRITE_H

View File

@ -1,44 +1,145 @@
/**
* @file util.h
*
* @brief Utilities relating around number string representation and protected arithmatic helper
* functions
*/
#ifndef UTIL_H
#define UTIL_H
static inline int get_digits(int n) // https://stackoverflow.com/questions/1068849/how-do-i-determine-the-number-of-digits-of-an-integer-in-c
{
if (n < 10) return 1;
if (n < 100) return 2;
if (n < 1000) return 3;
if (n < 10000) return 4;
if (n < 100000) return 5;
if (n < 1000000) return 6;
if (n < 10000000) return 7;
if (n < 100000000) return 8;
if (n < 1000000000) return 9;
return 10;
}
#include <stdint.h>
static inline int get_digits_odd(int n)
{
if (n < 100) return 1;
if (n < 10000) return 2;
if (n < 1000000) return 3;
if (n < 100000000) return 4;
return 5;
}
static inline int get_digits_even(int n)
{
if (n < 10) return 1;
if (n < 1000) return 2;
if (n < 100000) return 3;
if (n < 10000000) return 4;
return 5;
}
/**
* @def GBAL_UNUSED
* @brief A friendly wrapper around the not so friendly looking __attribute__ syntax for ((unused))
*/
#define GBAL_UNUSED __attribute__((unused))
#define UNDEFINED -1
/**
* @def NUM_ELEM_IN_ARR
* @brief Get the number of elements in an array
*
* @param arr input array
*/
#define NUM_ELEM_IN_ARR(arr) (sizeof(arr) / sizeof((arr)[0]))
#define INT_MAX_DIGITS 10 // strlen(str(INT_MAX)) = strlen("2147483647")
#define INT_MAX_DIGITS 11 // strlen(str(INT_MAX)) = strlen("-2147483647")
#define UINT_MAX_DIGITS 10 // strlen(str(UINT32_MAX)) = strlen("4294967295")
#define UINT8_MAX_DIGITS 3 // strlen(str(UINT8_MAX)) = strlen("255")
int int_arr_max(int int_arr[], int size);
#define ONE_K 1000
#define ONE_M 1000000
#define ONE_B 1000000000
#endif // UTIL_H
#define ONE_K_ZEROS 3
#define ONE_M_ZEROS 6
#define ONE_B_ZEROS 9
// The suffix replaces everything past the third digit, e.g. "999K" -> "1M"
// so it needs at least this number of chars to be able to display any suffixed number
#define SUFFIXED_NUM_MIN_REQ_CHARS 4
/**
* @brief Avoid overflow when adding two u32 integers
*
* @param a left operator **a + b**
* @param b left operator **a + b**
*
* @return the result of **a + b** or **UINT32_MAX** in case of overflow
*/
uint32_t u32_protected_add(uint32_t a, uint32_t b);
/**
* @brief Avoid overflow when adding two u16 integers
*
* @param a left operator **a + b**
* @param b left operator **a + b**
*
* @return the result of **a + b** or **UINT16_MAX** in case of overflow
*/
uint16_t u16_protected_add(uint16_t a, uint16_t b);
/**
* @brief Avoid overflow when multiplying two u32 integers
*
* @param a left operator **a * b**
* @param b left operator **a * b**
*
* @return the result of **a * b** or **UINT32_MAX** in case of overflow
*/
uint32_t u32_protected_mult(uint32_t a, uint32_t b);
/**
* @brief Avoid overflow when multiplying two u16 integers
*
* @param a left operator **a * b**
* @param b left operator **a * b**
*
* @return the result of **a * b** or **UINT16_MAX** in case of overflow
*/
uint16_t u16_protected_mult(uint16_t a, uint16_t b);
/**
* @brief Truncate an unsigned number into a suffixed string representation e.g. 12000 -> "12K"
* The least significant digits are rounded down e.g. 12345 -> "12K", 12987 -> "12K"
*
* @param num The number to truncate, can be anything from 0 to UINT32_MAX.
*
* @param num_req_chars The number of characters to constrain the string to.
* The function will use up as much characters as it can
* in order to maintain as much accuracy as possible.
* So numbers are not fully truncated if not necessary,
* e.g. 123123000 -> "123123K" for example value 7,
* and if num_req_chars > u32_get_digits(num) the number will not
* be truncated at all.
* Passing less than SUFFIXED_NUM_MIN_REQ_CHARS may result in an
* output string longer than num_req_chars but
* can be done to truncate 1000s -> "1K", 2000 -> "2K" etc.
* which wouldn't be otherwise.
*
* @param out_str An output buffer to write the resulting string to.
* Must be of size UINT_MAX_DIGITS + 1. + 1 for null-terminator.
* At that size the suffix character will always be accounted for since
* a number with more digits than UINT_MAX_DIGITS will not be handled nor
* truncated.
*/
void truncate_uint_to_suffixed_str(
uint32_t num,
int num_req_chars,
char out_str_buff[UINT_MAX_DIGITS + 1]
);
/**
* @brief Get the number of digits in a 32-bit unsigned number
* https://stackoverflow.com/questions/1068849/how-do-i-determine-the-number-of-digits-of-an-integer-in-c
*
* @param n 32-bit unsigned value to find the number of decimal digits of
*
* @return the number of digits in a number
*/
static inline int u32_get_digits(uint32_t n)
{
if (n < 10)
return 1;
if (n < 100)
return 2;
if (n < 1000)
return 3;
if (n < 10000)
return 4;
if (n < 100000)
return 5;
if (n < 1000000)
return 6;
if (n < 10000000)
return 7;
if (n < 100000000)
return 8;
if (n < 1000000000)
return 9;
return 10;
}
#endif // UTIL_H

116
scripts/generate_font.py Normal file
View File

@ -0,0 +1,116 @@
#!/usr/bin/env python3
from PIL import Image, ImageOps
import argparse
CHAR_WIDTH = 8
CHAR_HEIGHT = 8
NUM_VERT_CHARS = 6
NUM_HORZ_CHARS = 16
WORD_SIZE = 32
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input", required=True, help="input file")
parser.add_argument("-o", "--output", required=True, help="output file")
args = parser.parse_args()
in_path = args.input
out_path = args.output
# The font is stored in a 4-bit colormap png and is made of a grid of
# 16x6 characters:
# |-------------16---------------|
# | [][][][][][][][][][][][][][][][]
# | [][][][][][][][][][][][][][][][]
# 6 [][][][][][][][][][][][][][][][]
# | [][][][][][][][][][][][][][][][]
# | [][][][][][][][][][][][][][][][]
# | [][][][][][][][][][][][][][][][]
#
# Each character is a bitmap of 8x8 pixels.
#
# For example, the ! :
#
# ___00___
# ___00___
# ___00___
# ___00___
# ___00___
# ___00___
# ________
# ___00___
#
# The font file is organized to go character-by-character and left-to-right
img = Image.open(in_path)
img = img.convert("P")
img = img.convert('1')
img = ImageOps.invert(img)
glyphs = []
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
pix_y = char_y * CHAR_HEIGHT
crop_area = (pix_x, pix_y, pix_x + CHAR_WIDTH, pix_y + CHAR_HEIGHT)
pixel_block_img = img.crop(crop_area)
pixels = list(pixel_block_img.getdata())
word0 = 0
word1 = 0
for i, pixel in enumerate(pixels):
bit = 1 if pixel > 0 else 0
if i < WORD_SIZE:
word0 = word0 | (bit << i)
else:
word1 = word1 | (bit << (i - WORD_SIZE))
glyphs.append((word0, word1))
NUM_WORDS_IN_ROW = 8
first = True
with open(out_path, "w") as out:
out.write("""
@{{BLOCK(gbalatro_sys8)
.section .rodata
.align 2
.global gbalatro_sys8Font
gbalatro_sys8Font:
.word gbalatro_sys8Glyphs, 0, 0
.hword 32, 96
.byte 8, 8
.byte 8, 8
.hword 8
.byte 1, 0
.section .rodata
.align 2
.global gbalatro_sys8Glyphs @ 768 bytes (192 unsigned ints)
gbalatro_sys8Glyphs:
""")
for i, glyph in enumerate(glyphs):
if first:
out.write(" .word ")
first = False
out.write(f"0x{glyph[0]:08X},0x{glyph[1]:08X}")
# This checks to see if we have reached NUM_WORDS_IN_ROW words, (divided by
# 2 because that's the number of entries in each char[])
if (((i + 1) % (NUM_WORDS_IN_ROW // 2)) == 0):
out.write("\n")
if i != (len(glyphs) - 1):
out.write(" .word ")
else:
out.write(",")
out.write("\n")
out.write("\n")
out.write('@}}BLOCK(gbalatro_sys8)\n')

27
scripts/get_hash.py Normal file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
import sys
import re
def grep_binary_offsets(path, pattern):
with open(path, "rb") as f:
data = f.read()
results = []
for match in re.finditer(rb"[ -~]{4,}", data):
s = match.group()
if re.search(pattern.encode(), s):
results.append((match.start(), s.decode("utf-8", errors="ignore")))
return results
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python get_hash.py <file>")
sys.exit(1)
file_path = sys.argv[1]
pattern = "GBALATRO_VERSION"
for offset, text in grep_binary_offsets(file_path, pattern):
version = text.split(":")[1]
print(f"git hash: {version}")

77
scripts/get_memory_map.sh Executable file
View File

@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
ELF_FILE="${ELF_FILE-./build/balatro-gba.elf}"
POOL_DEF_FILE="${POOL_DEF_FILE-./include/def_balatro_mempool.h}"
READELF="${READELF-/opt/devkitpro/devkitARM/bin/arm-none-eabi-readelf}"
TOTAL_BYTES=0
if [ ! -f "$POOL_DEF_FILE" ]; then
echo "Mempool definition file not found: $POOL_DEF_FILE"
echo "You can set your mempool definition file with:"
echo " POOL_DEF_FILE=<mempool-def-file> $(basename $0)"
exit 1
fi
if [ ! -f "$ELF_FILE" ]; then
echo "elf file not found: $ELF_FILE"
echo "You can set your elf file with:"
echo " ELF_FILE=<elf-file> $(basename $0)"
exit 1
fi
if [ ! -x "$READELF" ]; then
echo "ERROR: \"$READELF\" is not an executable file."
echo "You can override the file location for 'arm-none-eabi-readelf' with the READELF env variable."
echo " e.g. $ READELF=\"/my/custom/location/arm-none-eabi-readelf\" $(basename $0) <file>"
exit 1
fi
print_line_break() {
echo "--------------------------------------------------------------------"
}
get_pool_names() {
grep POOL_ENTRY "$POOL_DEF_FILE" | sed -n 's@.*(\(.*\)).*@\1@p' | sed 's@,@@g' | cut -d ' ' -f 1
}
print_line_break
printf "%-16s| %-10s | %-10s | %-10s | %-10s \n" "Object" "address" "pool size" "func size" "bitmap size"
print_line_break
for name in $(get_pool_names); do
output_pool="$( \
"$READELF" -sW "$ELF_FILE" | \
grep "${name}_" | \
grep OBJECT | \
grep storage | \
sed -E 's@ +@ @g; s@^ @@' \
)"
output_func="$( \
"$READELF" -sW "$ELF_FILE" | \
grep -E "pool_free|pool_get|pool_init" | \
grep -E "${name}$" | \
sed -E 's@ +@ @g; s@^ @@' | \
tr -d '\n' \
)"
output_bitset="$( \
"$READELF" -sW "$ELF_FILE" | \
grep -E "${name}_bitset_w" | \
grep OBJECT | \
sed -E 's@ +@ @g; s@^ @@' | \
tr -d '\n' \
)"
address="$(cut -d ' ' -f 2 <<< $output_pool)"
pool_size="$(cut -d ' ' -f 3 <<< $output_pool)"
func_size="$(cut -d ' ' -f 3 <<< $output_func)"
bitset_size="$(cut -d ' ' -f 3 <<< $output_bitset)"
TOTAL_BYTES=$(( TOTAL_BYTES + pool_size + func_size + bitset_size ))
printf "%-16s| 0x%8s | %-10u | %-10u | %-10u \n" "$name" "$address" "$pool_size" "$func_size" "$bitset_size"
done
print_line_break
echo Total bytes used: $TOTAL_BYTES

View File

@ -1,57 +1,28 @@
#include "affine_background.h"
#include "affine_background_gfx.h"
#include "affine_main_menu_background_gfx.h"
#include "graphic_utils.h"
#define ANIMATION_SPEED_DIVISOR 16
BG_AFFINE bgaff_arr[SCREEN_HEIGHT + 1];
// Prepare screen during VBLANK
// Pre-computes the affine matrices values for each scanline and stores in bgaff_arr. This is to be
// done in VBLANK so the HBLANK code can just fetch the values quickly.
static IWRAM_CODE void s_affine_background_prep_bgaff_arr();
AFF_SRC_EX asx = {0};
enum AffineBackgroundID background = AFFINE_BG_MAIN_MENU;
static uint timer = 0;
static BG_AFFINE _bgaff_arr[SCREEN_HEIGHT + 1];
static AFF_SRC_EX _asx = {0};
static enum AffineBackgroundID _background = AFFINE_BG_MAIN_MENU;
static uint _timer = 0;
void affine_background_init()
{
{
affine_background_update();
REG_BG_AFFINE[AFFINE_BG_IDX] = bg_aff_default;
}
// Pre-computes the affine matrices values for each scanline
// and stores in bgaff_arr.
// This is to be done in VBLANK so the HBLANK code
// can just fetch the values quickly.
IWRAM_CODE void affine_background_prep_bgaff_arr()
{
for (u16 vcount = 0; vcount < SCREEN_HEIGHT; vcount++)
{
const s32 timer_s32 = 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); // dividing the timer by 16 to make the animation slower
asx.scr_x = (SCREEN_WIDTH / 2); // 128 on x and y is an offset used to center the rotation
asx.scr_y = vcount_s16 - (SCREEN_HEIGHT / 2); // scr_y must equal vcount otherwise the background will have no vertical difference
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);
bg_rotscale_ex(&bgaff_arr[vcount], &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];
}
IWRAM_CODE void affine_background_hblank()
{
vu16 vcount = REG_VCOUNT;
@ -62,69 +33,110 @@ 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] = _bgaff_arr[vcount + 1];
}
void affine_background_update()
{
if (REG_IE & IRQ_HBLANK) // High quality mode with HBLANK interrupt
{
affine_background_prep_bgaff_arr();
s_affine_background_prep_bgaff_arr();
}
else // Low quality mode without HBLANK interrupt
{
asx.scr_x = 0;
asx.scr_y = 0;
asx.tex_x += 5;
asx.tex_y += 12;
asx.sx = ((lu_sin(timer * 100)) >> 8) + 256; // Scale the sine value to fit in a s16
asx.sy = ((lu_sin(timer * 100 + 0x4000)) >> 8) + 256; // Scale the sine value to fit in a s16
asx.alpha = 0;
_asx.scr_x = 0;
_asx.scr_y = 0;
_asx.tex_x += 5;
_asx.tex_y += 12;
// Scale the sine value to fit in a s16
_asx.sx = ((lu_sin(_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;
bg_rotscale_ex(&bgaff_arr[0], &asx);
REG_BG_AFFINE[AFFINE_BG_IDX] = bgaff_arr[0];
bg_rotscale_ex(&_bgaff_arr[0], &_asx);
REG_BG_AFFINE[AFFINE_BG_IDX] = _bgaff_arr[0];
}
timer++;
_timer++;
}
void affine_background_set_color(COLOR color)
{
affine_background_change_background(background); // Reload the palette to reset any previous color scaling
// Reload the palette to reset any previous color scaling
affine_background_change_background(_background);
for (int i = 0; i < AFFINE_BG_PAL_LEN; i++)
{
clr_rgbscale(&pal_bg_mem[AFFINE_BG_PB] + i, &pal_bg_mem[AFFINE_BG_PB] + i, 1, color);
}
}
void affine_background_load_palette(const u16 *src)
void affine_background_load_palette(const u16* src)
{
memcpy16(&pal_bg_mem[AFFINE_BG_PB], src, AFFINE_BG_PAL_LEN);
}
void affine_background_change_background(enum AffineBackgroundID new_bg)
{
background = new_bg;
_background = new_bg;
switch (background)
switch (_background)
{
case AFFINE_BG_MAIN_MENU:
REG_BG2CNT &= ~BG_AFF_32x32;
REG_BG2CNT |= BG_AFF_16x16;
REG_IE |= IRQ_HBLANK; // Enable HBLANK
case AFFINE_BG_MAIN_MENU:
REG_BG2CNT &= ~BG_AFF_32x32;
REG_BG2CNT |= BG_AFF_16x16;
REG_IE |= IRQ_HBLANK; // Enable HBLANK
memcpy32_tile8_with_palette_offset((u32*)&tile8_mem[AFFINE_BG_CBB], (const u32*)affine_main_menu_background_gfxTiles, affine_main_menu_background_gfxTilesLen/4, AFFINE_BG_PB);
GRIT_CPY(&se_mem[AFFINE_BG_SBB], affine_main_menu_background_gfxMap);
affine_background_load_palette(affine_main_menu_background_gfxPal);
break;
case AFFINE_BG_GAME:
REG_BG2CNT &= ~BG_AFF_16x16;
REG_BG2CNT |= BG_AFF_32x32;
REG_IE &= ~IRQ_HBLANK; // Disable HBLANK
memcpy32_tile8_with_palette_offset(
(u32*)&tile8_mem[AFFINE_BG_CBB],
(const u32*)affine_main_menu_background_gfxTiles,
affine_main_menu_background_gfxTilesLen / 4,
AFFINE_BG_PB
);
GRIT_CPY(&se_mem[AFFINE_BG_SBB], affine_main_menu_background_gfxMap);
affine_background_load_palette(affine_main_menu_background_gfxPal);
break;
case AFFINE_BG_GAME:
REG_BG2CNT &= ~BG_AFF_16x16;
REG_BG2CNT |= BG_AFF_32x32;
REG_IE &= ~IRQ_HBLANK; // Disable HBLANK
memcpy32_tile8_with_palette_offset((u32*)&tile8_mem[AFFINE_BG_CBB], (const u32*)affine_background_gfxTiles, affine_background_gfxTilesLen/4, AFFINE_BG_PB);
GRIT_CPY(&se_mem[AFFINE_BG_SBB], affine_background_gfxMap);
affine_background_load_palette(affine_background_gfxPal);
break;
memcpy32_tile8_with_palette_offset(
(u32*)&tile8_mem[AFFINE_BG_CBB],
(const u32*)affine_background_gfxTiles,
affine_background_gfxTilesLen / 4,
AFFINE_BG_PB
);
GRIT_CPY(&se_mem[AFFINE_BG_SBB], affine_background_gfxMap);
affine_background_load_palette(affine_background_gfxPal);
break;
}
}
}
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 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);
// 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);
bg_rotscale_ex(&_bgaff_arr[vcount], &_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];
}

View File

@ -1,8 +1,15 @@
#include "audio_utils.h"
#include <maxmod.h>
void play_sfx(mm_word id, mm_word rate)
{
mm_sound_effect sfx = { {id}, rate, 0, SFX_DEFAULT_VOLUME, SFX_DEFAULT_PAN, };
mmEffectEx(&sfx);
}
#include "audio_utils.h"
#include <maxmod.h>
void play_sfx(mm_word id, mm_word rate, mm_byte volume)
{
mm_sound_effect sfx = {
{id},
rate,
0,
volume,
SFX_DEFAULT_PAN,
};
mmEffectEx(&sfx);
}

171
source/bitset.c Normal file
View File

@ -0,0 +1,171 @@
#include "bitset.h"
#include "util.h"
void bitset_set_idx(Bitset* bitset, int idx, bool on)
{
uint32_t i = idx / BITSET_BITS_PER_WORD;
uint32_t b = idx % BITSET_BITS_PER_WORD;
// Below are the "fast" forms of the above operations, respectively.
// These are more efficient, but removed for readability
// See: https://github.com/cellos51/balatro-gba/pull/132#discussion_r2365966071
// Divide by 32 to get the word index
// uint32_t i = idx >> 5;
// Get last 5-bits, same as a modulo (% 32) operation on positive numbers
// uint32_t b = idx & 0x1F;
if (on)
{
bitset->w[i] |= (uint32_t)1 << b;
}
else
{
bitset->w[i] &= ~((uint32_t)1 << b);
}
}
int bitset_set_next_free_idx(Bitset* bitset)
{
for (uint32_t i = 0; i < bitset->nwords; i++)
{
uint32_t inv = ~bitset->w[i];
// guard so we don't call `ctz` with 0, since __builtin_ctz(0) is undefined
// https://gcc.gnu.org/onlinedocs/gcc/Bit-Operation-Builtins.html#index-_005f_005fbuiltin_005fctz
//
// By using the bitwise inverse of the word, you can skip words that are full
// quickly (where the value is 0 or 'false' since all bits are '1', or 'in use'). Any value
// greater than 0 indicates there is a free slot. Then, when counting the trailing 0's, you
// can test very quickly where the first free slot is. This operation prevents looping
// through every bit of filled flags, and will instead operate only on the first word with
// free slots.
if (inv)
{
int bit = __builtin_ctz(inv);
bitset->w[i] |= ((uint32_t)1 << bit);
int idx = i * BITSET_BITS_PER_WORD + bit;
return (idx < bitset->cap) ? idx : UNDEFINED;
}
}
return UNDEFINED;
}
void bitset_clear(Bitset* bitset)
{
for (int i = 0; i < bitset->nwords; i++)
{
bitset->w[i] = 0;
}
}
bool bitset_is_empty(Bitset* bitset)
{
for (int i = 0; i < bitset->nwords; i++)
{
if (bitset->w[i])
return false;
}
return true;
}
bool bitset_get_idx(Bitset* bitset, int idx)
{
uint32_t i = idx / BITSET_BITS_PER_WORD;
uint32_t b = idx % BITSET_BITS_PER_WORD;
return bitset->w[i] & (uint32_t)1 << b;
}
int bitset_num_set_bits(Bitset* bitset)
{
int sum = 0;
for (int i = 0; i < bitset->nwords; i++)
{
sum += __builtin_popcount(bitset->w[i]);
}
return sum;
}
int bitset_find_idx_of_nth_set(const Bitset* bitset, int n)
{
int tracker = 0;
int prev_tracker = 0;
for (int i = 0; i < bitset->nwords; i++)
{
tracker += __builtin_popcount(bitset->w[i]);
if (tracker > n)
{
// The index is here somewhere
// this one is to count the 1's not the offset, underflow to -1 is good for finding the
// 0 index
int base = prev_tracker - 1;
// this one is for the actual offset we want to map the id to
int offset = bitset->nbits * i;
for (int j = 0; j < bitset->nbits; j++)
{
if (base == n)
{
return offset - 1;
}
base += (bitset->w[i] >> j) & 0x01;
offset++;
}
break;
}
prev_tracker = tracker;
}
return UNDEFINED;
}
BitsetItr bitset_itr_create(const Bitset* bitset)
{
BitsetItr itr = {
.bitset = bitset,
.word = 0,
.bit = 0,
.itr = 0,
};
return itr;
}
int bitset_itr_next(BitsetItr* itr)
{
// So, worst case scenario for this is one bit at the end of the last
// word in the bitset. You would look (32 * 7) + 31 times!
// This can be sped up with by checking if the word is empty first.
// Then the worst enemy of this method would be something like a set bit at the end
// of every word. In that case you would need to loop 31 times maximum.
// So one last thing you could do is something like `bitset_allocate_idx` does with the
// __builtin_ctz function as well.
//
// The point being, this can be very slow, but it's simple and can be much faster.
for (; itr->word < itr->bitset->nwords; itr->word++)
{
for (; itr->bit < itr->bitset->nbits; itr->bit++)
{
itr->itr++;
if (itr->bitset->w[itr->word] & (1 << itr->bit))
{
// if itr->bit == nbits on the next run, the for loop will handle it
itr->bit++;
// above we always make it one more than it is
// it's so we can return without mutating the actual iterator
// once it gets here. Just subtract one
return itr->itr - 1;
}
}
itr->bit = 0;
}
itr->word = 0;
return UNDEFINED;
}

View File

@ -1,95 +1,118 @@
#include "blind.h"
#include "big_blind_gfx.h"
#include "boss_blind_gfx.h"
#include "graphic_utils.h"
#include "small_blind_gfx.h"
#include "util.h"
#include <tonc.h>
#include "blind.h"
#include "blinds_gfx.h"
#include "graphic_utils.h"
// 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};
// Palettes for the blinds (Transparency, Text Color, Shadow, Highlight, Main Color) Use this: http://www.budmelvin.com/dev/15bitconverter.html
static const u16 small_blind_token_palette[PAL_ROW_LEN] = {0x0000, 0x7FFF, 0x34A1, 0x5DCB, 0x5104, 0x55A0, 0x2D01, 0x34E0};
static const u16 big_blind_token_palette[PAL_ROW_LEN] = {0x0000, 0x2527, 0x15F5, 0x36FC, 0x1E9C, 0x01B4, 0x0D0A, 0x010E};
static const u16 boss_blind_token_palette[PAL_ROW_LEN] = {0x0000, 0x2CC9, 0x3D0D, 0x5E14, 0x5171, 0x4D0F, 0x2CC8, 0x3089}; // This variable is temporary, each boss blind will have its own unique palette
// Palettes for the blinds (Transparency, Text Color, Shadow, Highlight, Main Color) Use this:
// http://www.budmelvin.com/dev/15bitconverter.html
static const u16 small_blind_token_palette[PAL_ROW_LEN] =
{0x0000, 0x7FFF, 0x34A1, 0x5DCB, 0x5104, 0x55A0, 0x2D01, 0x34E0};
static const u16 big_blind_token_palette[PAL_ROW_LEN] =
{0x0000, 0x2527, 0x15F5, 0x36FC, 0x1E9C, 0x01B4, 0x0D0A, 0x010E};
// This variable is temporary, each boss blind will have its own unique palette
static const u16 boss_blind_token_palette[PAL_ROW_LEN] =
{0x0000, 0x2CC9, 0x3D0D, 0x5E14, 0x5171, 0x4D0F, 0x2CC8, 0x3089};
// clang-format off
static Blind _blind_type_map[BLIND_TYPE_MAX] = {
#define BLIND_INFO(NAME, name, multi, _reward) \
{ \
.type = BLIND_TYPE_##NAME, \
.gfx_info = \
{ \
.tiles = name##_blind_gfxTiles, \
.palette = name##_blind_token_palette, \
.tid = NAME##_BLIND_TID, \
.pb = NAME##_BLIND_PB, \
}, \
.score_req_multipler = multi, \
.reward = _reward, \
},
BLIND_TYPE_INFO_TABLE
#undef BLIND_INFO
};
// clang-format on
static void s_blind_gfx_init(enum BlindType type);
GBAL_UNUSED
void blind_set_boss_graphics(const unsigned int* tiles, const u16* palette)
{
// TODO: This function is unused and not fully fleshed out.
// We need to support more boss blind graphics in the future.
// The idea here is that we can call this function to set the boss up to
// render new tiles
//
// This will eventually be in it's own map mapping graphic data to
// boss types.
_blind_type_map[BLIND_TYPE_BOSS].gfx_info.tiles = tiles;
_blind_type_map[BLIND_TYPE_BOSS].gfx_info.palette = palette;
s_blind_gfx_init(BLIND_TYPE_BOSS);
}
void blind_init()
{
// Blind graphics (fighting grit every step of the way as usual)
GRIT_CPY(&tile_mem[4][SMALL_BLIND_TID], blinds_gfxTiles);
for (int i = 0; i < BLIND_TYPE_MAX; i++)
{
s_blind_gfx_init(i);
}
memcpy16(&pal_obj_mem[PAL_ROW_LEN * SMALL_BLIND_PB], &small_blind_token_palette, sizeof(small_blind_token_palette) / 2);
memcpy16(&pal_obj_mem[PAL_ROW_LEN * BIG_BLIND_PB], &big_blind_token_palette, sizeof(big_blind_token_palette) / 2);
// Boss Blind (This is temporary. Each boss blind is unique and will have to have its own graphics and palette which will probably be stored in some huge array)
memcpy16(&pal_obj_mem[PAL_ROW_LEN * BOSS_BLIND_PB], &boss_blind_token_palette, sizeof(boss_blind_token_palette) / 2);
return;
}
int blind_get_requirement(int type, int ante)
u32 blind_get_requirement(enum BlindType type, int ante)
{
// Ensure ante is within valid range
if (ante < 0 || ante > MAX_ANTE)
{
ante = 0; // Ensure ante is within valid range
}
ante = 0;
switch (type)
{
case SMALL_BLIND:
return ante_lut[ante];
case BIG_BLIND:
return (ante_lut[ante] * 3) / 2; // X1.5
case BOSS_BLIND:
return ante_lut[ante] * 2; // X2
default:
return 0; // Invalid type
}
return fx2int(_blind_type_map[type].score_req_multipler * ante_lut[ante]);
}
int blind_get_reward(int type)
int blind_get_reward(enum BlindType type)
{
switch (type)
{
case SMALL_BLIND:
return 3;
case BIG_BLIND:
return 4;
case BOSS_BLIND:
return 5;
default:
return 0; // Invalid type
}
return _blind_type_map[type].reward;
}
u16 blind_get_color(int type, int index)
u16 blind_get_color(enum BlindType type, enum BlindColorIndex index)
{
switch (type)
{
case SMALL_BLIND:
return small_blind_token_palette[index];
case BIG_BLIND:
return big_blind_token_palette[index];
case BOSS_BLIND:
return boss_blind_token_palette[index];
default:
return 0; // Invalid type
}
return _blind_type_map[type].gfx_info.palette[index];
}
Sprite *blind_token_new(int type, int x, int y, int sprite_index)
Sprite* blind_token_new(enum BlindType type, int x, int y, int sprite_index)
{
Sprite *sprite = NULL;
u16 a0 = ATTR0_SQUARE | ATTR0_4BPP;
u16 a1 = ATTR1_SIZE_32x32;
u32 tid = _blind_type_map[type].gfx_info.tid, pb = _blind_type_map[type].gfx_info.pb;
switch (type)
{
case SMALL_BLIND:
sprite = sprite_new(ATTR0_SQUARE | ATTR0_4BPP, ATTR1_SIZE_32x32, SMALL_BLIND_TID, SMALL_BLIND_PB, sprite_index);
break;
case BIG_BLIND:
sprite = sprite_new(ATTR0_SQUARE | ATTR0_4BPP, ATTR1_SIZE_32x32, BIG_BLIND_TID, BIG_BLIND_PB, sprite_index);
break;
case BOSS_BLIND:
sprite = sprite_new(ATTR0_SQUARE | ATTR0_4BPP, ATTR1_SIZE_32x32, BOSS_BLIND_TID, BOSS_BLIND_PB, sprite_index);
break;
default:
return NULL;
}
Sprite* sprite = sprite_new(a0, a1, tid, pb, sprite_index);
sprite_position(sprite, x, y);
return sprite;
}
static void s_blind_gfx_init(enum BlindType type)
{
// TODO: Re-add grit copy. You need to decouple the blind graphics first.
// This will allow this function to change the boss graphics info
// GRIT_CPY(&tile_mem[TILE_MEM_OBJ_CHARBLOCK0_IDX][_blind_type_map[type].pal_info.tid], tiles);
BlindGfxInfo* p_gfx = &_blind_type_map[type].gfx_info;
memcpy32(
&tile_mem[TILE_MEM_OBJ_CHARBLOCK0_IDX][p_gfx->tid],
p_gfx->tiles,
BLIND_SPRITE_COPY_SIZE
);
memcpy16(&pal_obj_bank[p_gfx->pb], p_gfx->palette, PAL_ROW_LEN);
}

29
source/button.c Normal file
View File

@ -0,0 +1,29 @@
#include "button.h"
#include "audio_utils.h"
#include "soundbank.h"
#include <tonc.h>
void button_set_highlight(Button* button, bool highlight)
{
if (button == NULL)
return;
u16 set_color = highlight ? BTN_HIGHLIGHT_COLOR : pal_bg_mem[button->button_pal_idx];
memset16(&pal_bg_mem[button->border_pal_idx], set_color, 1);
}
void button_press(Button* button)
{
if (button == NULL || button->on_pressed == NULL ||
(button->can_be_pressed != NULL && !button->can_be_pressed()))
{
return;
}
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
button->on_pressed();
}

View File

@ -1,17 +1,19 @@
#include "card.h"
#include <maxmod.h>
#include <stdlib.h>
#include "deck_gfx.h"
#include "graphic_utils.h"
#include <maxmod.h>
#include <stdlib.h>
// Audio
#include "pool.h"
#include "soundbank.h"
// 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] = {
{0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192},
// 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] = {
{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}
@ -23,9 +25,9 @@ void card_init()
}
// Card methods
Card *card_new(u8 suit, u8 rank)
Card* card_new(u8 suit, u8 rank)
{
Card *card = malloc(sizeof(Card));
Card* card = POOL_GET(Card);
card->suit = suit;
card->rank = rank;
@ -33,14 +35,13 @@ Card *card_new(u8 suit, u8 rank)
return card;
}
void card_destroy(Card **card)
void card_destroy(Card** card)
{
if (*card == NULL) return;
free(*card);
POOL_FREE(Card, *card);
*card = NULL;
}
u8 card_get_value(Card *card)
u8 card_get_value(Card* card)
{
if (card->rank == JACK || card->rank == QUEEN || card->rank == KING)
{
@ -59,36 +60,50 @@ u8 card_get_value(Card *card)
}
// CardObject methods
CardObject *card_object_new(Card *card)
CardObject* card_object_new(Card* card)
{
CardObject *card_object = malloc(sizeof(CardObject));
CardObject* card_object = POOL_GET(CardObject);
card_object->card = card;
card_object->sprite_object = sprite_object_new();
card_object->selected = false;
return card_object;
}
void card_object_destroy(CardObject **card_object)
void card_object_destroy(CardObject** card_object)
{
if (*card_object == NULL) return;
if (*card_object == NULL)
return;
sprite_object_destroy(&((*card_object)->sprite_object));
//card_destroy(&(*card_object)->card); // In practice, this is unnecessary because the card will be inserted into the discard pile and then back into the deck. If you need to destroy the card, you can do it manually before calling this function.
free(*card_object);
POOL_FREE(CardObject, *card_object);
*card_object = NULL;
}
void card_object_update(CardObject* card_object)
{
if (card_object == NULL) return;
if (card_object == NULL)
return;
sprite_object_update(card_object->sprite_object);
}
void card_object_set_sprite(CardObject *card_object, int layer)
void card_object_set_sprite(CardObject* card_object, int layer)
{
int tile_index = CARD_TID + (layer * CARD_SPRITE_OFFSET);
memcpy32(&tile_mem[4][tile_index], &deck_gfxTiles[card_sprite_lut[card_object->card->suit][card_object->card->rank] * TILE_SIZE], TILE_SIZE * CARD_SPRITE_OFFSET);
sprite_object_set_sprite(card_object->sprite_object, sprite_new(ATTR0_SQUARE | ATTR0_4BPP | ATTR0_AFF, ATTR1_SIZE_32, tile_index, 0, layer + CARD_STARTING_LAYER));
memcpy32(
&tile_mem[TILE_MEM_OBJ_CHARBLOCK0_IDX][tile_index],
&deck_gfxTiles
[_card_sprite_lut[card_object->card->suit][card_object->card->rank] * TILE_SIZE],
TILE_SIZE * CARD_SPRITE_OFFSET
);
Sprite* sprite = sprite_new(
ATTR0_SQUARE | ATTR0_4BPP | ATTR0_AFF,
ATTR1_SIZE_32,
tile_index,
0,
layer + CARD_STARTING_LAYER
);
sprite_object_set_sprite(card_object->sprite_object, sprite);
}
void card_object_shake(CardObject* card_object, mm_word sound_id)
@ -100,14 +115,14 @@ void card_object_set_selected(CardObject* card_object, bool selected)
{
if (card_object == NULL)
return;
sprite_object_set_selected(card_object->sprite_object, selected);
card_object->selected = selected;
}
bool card_object_is_selected(CardObject* card_object)
{
if (card_object == NULL)
return false;
return sprite_object_is_selected(card_object->sprite_object);
return card_object->selected;
}
Sprite* card_object_get_sprite(CardObject* card_object)

29
source/font.c Normal file
View File

@ -0,0 +1,29 @@
#include "font.h"
#include "util.h"
#include <stdlib.h>
static const char* s_font_point_lookup[] = {
FP0_STR,
FP1_STR,
FP2_STR,
FP3_STR,
FP4_STR,
FP5_STR,
FP6_STR,
FP7_STR,
FP8_STR,
FP9_STR,
};
const char* get_font_point_str(int val)
{
val = abs(val) % 10;
return s_font_point_lookup[val];
}
char digit_char_to_font_point(char digit_char)
{
return get_font_point_str(digit_char - '0')[0];
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,30 @@
#include <tonc_core.h>
#include <tonc_tte.h>
#include <tonc_math.h>
#include "util.h"
#include "graphic_utils.h"
const Rect FULL_SCREENBLOCK_RECT = { 0, 0, SE_ROW_LEN - 1, SE_COL_LEN - 1};
#include "util.h"
#include <string.h>
#include <tonc_core.h>
#include <tonc_math.h>
#include <tonc_tte.h>
const Rect FULL_SCREENBLOCK_RECT = {0, 0, SE_ROW_LEN - 1, SE_COL_LEN - 1};
static void clip_se_rect_to_screenblock(Rect* rect);
static void bg_se_copy_or_move_rect_1_tile_vert(
u16 bg_sbb,
Rect se_rect,
enum ScreenVertDir direction,
bool move
);
static void main_bg_se_copy_or_move_rect_1_tile_vert(
Rect se_rect,
enum ScreenVertDir direction,
bool move
);
// Clips a rect of screenblock entries to a specified rect
// The bounding rect is not required to be within screenblock boundaries
static void clip_se_rect_to_bounding_rect(Rect* rect, const Rect* bounding_rect)
static inline void clip_se_rect_to_bounding_rect(Rect* rect, const Rect* bounding_rect)
{
rect->right = min(rect->right, bounding_rect->right);
rect->bottom = min(rect->bottom, bounding_rect->bottom);
@ -24,46 +39,35 @@ static void clip_se_rect_to_screenblock(Rect* rect)
clip_se_rect_to_bounding_rect(rect, &FULL_SCREENBLOCK_RECT);
}
SE main_bg_se_get_se(BG_POINT pos)
{
return se_mat[MAIN_BG_SBB][pos.y][pos.x];
}
// Clips a rect of screenblock entries to be within one step of
// Clips a rect of screenblock entries to be within one step of
// screenblock boundaries vertically depending on direction.
static void clip_se_rect_within_step_of_full_screen_vert(Rect* se_rect, int direction)
static inline void clip_se_rect_within_step_of_full_screen_vert(
Rect* se_rect,
enum ScreenVertDir direction
)
{
Rect bounding_rect = FULL_SCREENBLOCK_RECT;
if (direction == SE_UP)
if (direction == SCREEN_UP)
{
bounding_rect.top += 1;
}
else if (direction == SE_DOWN)
else if (direction == SCREEN_DOWN)
{
bounding_rect.bottom -= 1;
}
clip_se_rect_to_bounding_rect(se_rect, &bounding_rect);
}
void main_bg_se_clear_rect(Rect se_rect)
{
if (se_rect.left > se_rect.right)
return;
// Clip to avoid screenblock overflow
clip_se_rect_to_screenblock(&se_rect);
for (int y = se_rect.top; y < se_rect.bottom; y++)
{
memset16(&(se_mat[MAIN_BG_SBB][y][se_rect.left]), 0x0000, rect_width(&se_rect));
}
}
// Internal static function to merge implementation of move/copy functions.
static void bg_se_copy_or_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int direction, bool move)
static void bg_se_copy_or_move_rect_1_tile_vert(
u16 bg_sbb,
Rect se_rect,
enum ScreenVertDir direction,
bool move
)
{
if (se_rect.left > se_rect.right
|| (direction != SE_UP && direction != SE_DOWN))
if (se_rect.left > se_rect.right || (direction != SCREEN_UP && direction != SCREEN_DOWN))
{
return;
}
@ -71,14 +75,16 @@ static void bg_se_copy_or_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int di
// Clip to avoid read/write overflow of the screenblock
clip_se_rect_within_step_of_full_screen_vert(&se_rect, direction);
int start = (direction == SE_UP) ? se_rect.top : se_rect.bottom;
int end = (direction == SE_UP) ? se_rect.bottom : se_rect.top;
int start = (direction == SCREEN_UP) ? se_rect.top : se_rect.bottom;
int end = (direction == SCREEN_UP) ? se_rect.bottom : se_rect.top;
for (int y = start; y != end - direction; y -= direction)
{
memcpy16(&(se_mat[bg_sbb][y + direction][se_rect.left]),
&se_mat[bg_sbb][y][se_rect.left],
rect_width(&se_rect));
memcpy16(
&(se_mat[bg_sbb][y + direction][se_rect.left]),
&se_mat[bg_sbb][y][se_rect.left],
rect_width(&se_rect)
);
}
if (move)
@ -87,32 +93,36 @@ static void bg_se_copy_or_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int di
}
}
static void main_bg_se_copy_or_move_rect_1_tile_vert(Rect se_rect, int direction, bool move)
static void main_bg_se_copy_or_move_rect_1_tile_vert(
Rect se_rect,
enum ScreenVertDir direction,
bool move
)
{
bg_se_copy_or_move_rect_1_tile_vert(MAIN_BG_SBB, se_rect, direction, move);
bg_se_copy_or_move_rect_1_tile_vert(MAIN_BG_SBB, se_rect, direction, move);
}
void bg_se_copy_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int direction)
void bg_se_copy_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, enum ScreenVertDir direction)
{
bg_se_copy_or_move_rect_1_tile_vert(MAIN_BG_SBB, se_rect, direction, false);
}
void bg_se_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int direction)
void bg_se_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, enum ScreenVertDir direction)
{
bg_se_copy_or_move_rect_1_tile_vert(MAIN_BG_SBB, se_rect, direction, true);
}
void main_bg_se_copy_rect_1_tile_vert(Rect se_rect, int direction)
void main_bg_se_copy_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction)
{
main_bg_se_copy_or_move_rect_1_tile_vert(se_rect, direction, false);
}
void main_bg_se_move_rect_1_tile_vert(Rect se_rect, int direction)
void main_bg_se_move_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction)
{
main_bg_se_copy_or_move_rect_1_tile_vert(se_rect, direction, true);
}
void main_bg_se_copy_rect(Rect se_rect, BG_POINT pos)
void main_bg_se_copy_rect(Rect se_rect, BG_POINT dest_pos)
{
if (se_rect.left > se_rect.right || se_rect.top > se_rect.bottom)
return;
@ -127,22 +137,18 @@ void main_bg_se_copy_rect(Rect se_rect, BG_POINT pos)
// Copy the rect to the tile map
for (int sy = 0; sy < height; sy++)
{
memcpy16(&tile_map[sy][0],
&se_mat[MAIN_BG_SBB][se_rect.top + sy][se_rect.left],
width);
memcpy16(&tile_map[sy][0], &se_mat[MAIN_BG_SBB][se_rect.top + sy][se_rect.left], width);
}
// TODO: Avoid overflow
// Copy the tilemap to the new rect position
for (int sy = 0; sy < height; sy++)
{
memcpy16(&se_mat[MAIN_BG_SBB][pos.y + sy][pos.x],
&tile_map[sy][0],
width);
memcpy16(&se_mat[MAIN_BG_SBB][dest_pos.y + sy][dest_pos.x], &tile_map[sy][0], width);
}
}
void main_bg_se_fill_rect_with_se(SE se, Rect se_rect)
static inline void main_bg_se_fill_rect_with_se(SE se, Rect se_rect)
{
if (se_rect.left > se_rect.right || se_rect.top > se_rect.bottom)
return;
@ -160,7 +166,12 @@ void main_bg_se_fill_rect_with_se(SE se, Rect se_rect)
}
// Helper: Copy the corners of a 3x3 tile block
static void main_bg_se_expand_3x3_copy_corners(const Rect* se_dest_rect, const BG_POINT* src_top_left_pnt, int dest_rect_width, int dest_rect_height)
static inline void main_bg_se_expand_3x3_copy_corners(
const Rect* se_dest_rect,
const BG_POINT* src_top_left_pnt,
int dest_rect_width,
int dest_rect_height
)
{
SE top_left_se = se_mat[MAIN_BG_SBB][src_top_left_pnt->y][src_top_left_pnt->x];
se_mat[MAIN_BG_SBB][se_dest_rect->top][se_dest_rect->left] = top_left_se;
@ -169,33 +180,54 @@ static void main_bg_se_expand_3x3_copy_corners(const Rect* se_dest_rect, const B
se_mat[MAIN_BG_SBB][se_dest_rect->top][se_dest_rect->left + dest_rect_width - 1] = top_right_se;
SE bottom_left_se = se_mat[MAIN_BG_SBB][src_top_left_pnt->y + 2][src_top_left_pnt->x];
se_mat[MAIN_BG_SBB][se_dest_rect->top + dest_rect_height - 1][se_dest_rect->left] = bottom_left_se;
se_mat[MAIN_BG_SBB][se_dest_rect->top + dest_rect_height - 1][se_dest_rect->left] =
bottom_left_se;
SE bottom_right_se = se_mat[MAIN_BG_SBB][src_top_left_pnt->y + 2][src_top_left_pnt->x + 2];
se_mat[MAIN_BG_SBB][se_dest_rect->top + dest_rect_height - 1][se_dest_rect->left + dest_rect_width - 1] = bottom_right_se;
se_mat[MAIN_BG_SBB][se_dest_rect->top + dest_rect_height - 1]
[se_dest_rect->left + dest_rect_width - 1] = bottom_right_se;
}
// Helper: Copy the top and bottom sides of a 3x3 tile block
static void main_bg_se_expand_3x3_copy_top_bottom(const Rect* se_dest_rect, const BG_POINT* src_top_left_pnt, int dest_rect_width)
static inline void main_bg_se_expand_3x3_copy_top_bottom(
const Rect* se_dest_rect,
const BG_POINT* src_top_left_pnt,
int dest_rect_width
)
{
if (dest_rect_width > 2)
{
SE top_middle_se = se_mat[MAIN_BG_SBB][src_top_left_pnt->y][src_top_left_pnt->x + 1];
SE bottom_middle_se = se_mat[MAIN_BG_SBB][src_top_left_pnt->y + 2][src_top_left_pnt->x + 1];
memset16(&se_mat[MAIN_BG_SBB][se_dest_rect->top][se_dest_rect->left + 1], top_middle_se, dest_rect_width - 2);
memset16(&se_mat[MAIN_BG_SBB][se_dest_rect->bottom][se_dest_rect->left + 1], bottom_middle_se, dest_rect_width - 2);
memset16(
&se_mat[MAIN_BG_SBB][se_dest_rect->top][se_dest_rect->left + 1],
top_middle_se,
dest_rect_width - 2
);
memset16(
&se_mat[MAIN_BG_SBB][se_dest_rect->bottom][se_dest_rect->left + 1],
bottom_middle_se,
dest_rect_width - 2
);
}
}
// Helper: Copy the left and right sides of a 3x3 tile block
static void main_bg_se_expand_3x3_copy_left_right(const Rect* se_dest_rect, const BG_POINT* src_top_left_pnt, int dest_rect_width, int dest_rect_height)
static inline void main_bg_se_3w_copy_expand_left_right_sides(
const Rect* se_dest_rect,
const BG_POINT* src_left_pnt
)
{
SE middle_left_se = se_mat[MAIN_BG_SBB][src_top_left_pnt->y + 1][src_top_left_pnt->x];
SE middle_right_se = se_mat[MAIN_BG_SBB][src_top_left_pnt->y + 1][src_top_left_pnt->x + 2];
for (int y = 1; y < dest_rect_height - 1; y++)
SE middle_left_se = se_mat[MAIN_BG_SBB][src_left_pnt->y][src_left_pnt->x];
// Assuming width 3 so the right side is + 2
SE middle_right_se = se_mat[MAIN_BG_SBB][src_left_pnt->y][src_left_pnt->x + 2];
int dest_rect_width = rect_width(se_dest_rect);
int dest_rect_height = rect_height(se_dest_rect);
for (int y = 0; y < dest_rect_height; y++)
{
se_mat[MAIN_BG_SBB][se_dest_rect->top + y][se_dest_rect->left] = middle_left_se;
se_mat[MAIN_BG_SBB][se_dest_rect->top + y][se_dest_rect->left + dest_rect_width - 1] = middle_right_se;
se_mat[MAIN_BG_SBB][se_dest_rect->top + y][se_dest_rect->left + dest_rect_width - 1] =
middle_right_se;
}
}
@ -213,19 +245,60 @@ void main_bg_se_copy_expand_3x3_rect(Rect se_dest_rect, BG_POINT src_top_left_pn
}
// Copy the corners
main_bg_se_expand_3x3_copy_corners(&se_dest_rect, &src_top_left_pnt, dest_rect_width, dest_rect_height);
main_bg_se_expand_3x3_copy_corners(
&se_dest_rect,
&src_top_left_pnt,
dest_rect_width,
dest_rect_height
);
// Copy top and bottom sides
main_bg_se_expand_3x3_copy_top_bottom(&se_dest_rect, &src_top_left_pnt, dest_rect_width);
BG_POINT src_middle_left_pnt = {src_top_left_pnt.x, src_top_left_pnt.y + 1};
// Avoid the corners when copying the sides
Rect dest_sides_rect = se_dest_rect;
dest_sides_rect.top += 1;
dest_sides_rect.bottom -= 1;
// Copy left and right sides
main_bg_se_expand_3x3_copy_left_right(&se_dest_rect, &src_top_left_pnt, dest_rect_width, dest_rect_height);
main_bg_se_3w_copy_expand_left_right_sides(&dest_sides_rect, &src_middle_left_pnt);
// Fill the center if needed
if (dest_rect_width > 2 && dest_rect_height > 2)
{
SE middle_fill_se = se_mat[MAIN_BG_SBB][src_top_left_pnt.y + 1][src_top_left_pnt.x + 1];
Rect dest_inner_fill_rect = {se_dest_rect.left + 1, se_dest_rect.top + 1, se_dest_rect.right - 1, se_dest_rect.bottom - 1};
Rect dest_inner_fill_rect = {
se_dest_rect.left + 1,
se_dest_rect.top + 1,
se_dest_rect.right - 1,
se_dest_rect.bottom - 1
};
main_bg_se_fill_rect_with_se(middle_fill_se, dest_inner_fill_rect);
}
}
void main_bg_se_copy_expand_3w_row(Rect se_dest_rect, BG_POINT src_row_left_pnt)
{
clip_se_rect_to_screenblock(&se_dest_rect);
int dest_rect_width = rect_width(&se_dest_rect);
if (dest_rect_width < 2)
{
return;
}
// Copy left and right sides
main_bg_se_3w_copy_expand_left_right_sides(&se_dest_rect, &src_row_left_pnt);
if (dest_rect_width > 2)
{
SE middle_fill_se = se_mat[MAIN_BG_SBB][src_row_left_pnt.y][src_row_left_pnt.x + 1];
Rect dest_inner_fill_rect = se_dest_rect;
// Avoid copying the sides when filling the rect.
dest_inner_fill_rect.left += 1;
dest_inner_fill_rect.right -= 1;
main_bg_se_fill_rect_with_se(middle_fill_se, dest_inner_fill_rect);
}
}
@ -235,22 +308,45 @@ void tte_erase_rect_wrapper(Rect rect)
tte_erase_rect(rect.left, rect.top, rect.right, rect.bottom);
}
void update_text_rect_to_right_align_num(Rect* rect, int num, int overflow_direction)
void update_text_rect_to_right_align_str(
Rect* rect,
const char* str,
enum OverflowDir overflow_direction
)
{
int num_digits = get_digits(num);
// TODO: Allow passing string length to avoid calling strlen()?
int str_len = strlen(str);
if (overflow_direction == OVERFLOW_LEFT)
{
rect->left = max(0, rect->right - num_digits * TILE_SIZE);
rect->left = max(0, rect->right - str_len * TTE_CHAR_SIZE);
}
else if (overflow_direction == OVERFLOW_RIGHT)
{
int num_fitting_digits = rect_width(rect) / TILE_SIZE;
if (num_digits < num_fitting_digits)
rect->left += (num_fitting_digits - num_digits) * TILE_SIZE;
//else nothing is to be updated, entire rect is filled and may overflow
int num_fitting_chars = rect_width(rect) / TTE_CHAR_SIZE;
if (str_len < num_fitting_chars)
rect->left += (num_fitting_chars - str_len) * TTE_CHAR_SIZE;
// else nothing is to be updated, entire rect is filled and may overflow
}
}
void update_text_rect_to_center_str(Rect* rect, const char* str, enum ScreenHorzDir bias_direction)
{
if (rect == NULL || str == NULL)
{
return;
}
int text_width_chars = strlen(str);
int rect_width_chars = rect_width(rect) / TTE_CHAR_SIZE;
bool bias_right = (bias_direction == SCREEN_RIGHT);
/* Adding bias_right makes sure that we round up when biased right
* but round down when biased left.
*/
rect->left += max(0, (rect_width_chars - text_width_chars + bias_right) / 2) * TTE_CHAR_SIZE;
}
void memcpy16_tile8_with_palette_offset(u16* dst, const u16* src, uint hwcount, u8 palette_offset)
{
const u16 offset = (((palette_offset) << 8) | (palette_offset));
@ -263,7 +359,8 @@ void memcpy16_tile8_with_palette_offset(u16* dst, const u16* src, uint hwcount,
void memcpy32_tile8_with_palette_offset(u32* dst, const u32* src, uint wcount, u8 palette_offset)
{
const u32 offset = (palette_offset << 24) | (palette_offset << 16) | (palette_offset << 8) | palette_offset;
const u32 offset =
(palette_offset << 24) | (palette_offset << 16) | (palette_offset << 8) | palette_offset;
for (int i = 0; i < wcount; i++)
{
// Copying u8 data 4 times across u32 data
@ -299,4 +396,17 @@ void toggle_windows(bool win0, bool win1)
{
REG_BLDCNT = 0;
}
}
}
void main_bg_se_clear_rect(Rect se_rect)
{
if (se_rect.left > se_rect.right)
return;
// Clip to avoid screenblock overflow
clip_se_rect_to_screenblock(&se_rect);
for (int y = se_rect.top; y < se_rect.bottom; y++)
{
memset16(&(se_mat[MAIN_BG_SBB][y][se_rect.left]), 0x0000, rect_width(&se_rect));
}
}

View File

@ -1,41 +1,68 @@
#include "hand_analysis.h"
#include "card.h"
#include "game.h"
static void get_distribution(CardObject **cards, int top, u8 *ranks_out, u8 *suits_out) {
for (int i = 0; i < NUM_RANKS; i++) ranks_out[i] = 0;
for (int i = 0; i < NUM_SUITS; i++) suits_out[i] = 0;
void get_hand_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS])
{
for (int i = 0; i < NUM_RANKS; i++)
ranks_out[i] = 0;
for (int i = 0; i < NUM_SUITS; i++)
suits_out[i] = 0;
for (int i = 0; i <= top; i++) {
if (cards[i] && card_object_is_selected(cards[i])) {
CardObject** cards = get_hand_array();
int top = get_hand_top();
for (int i = 0; i <= top; i++)
{
if (cards[i] && card_object_is_selected(cards[i]))
{
ranks_out[cards[i]->card->rank]++;
suits_out[cards[i]->card->suit]++;
}
}
}
void get_hand_distribution(u8 *ranks_out, u8 *suits_out) {
get_distribution(get_hand_array(), get_hand_top(), ranks_out, suits_out);
}
void get_played_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS])
{
for (int i = 0; i < NUM_RANKS; i++)
ranks_out[i] = 0;
for (int i = 0; i < NUM_SUITS; i++)
suits_out[i] = 0;
void get_played_distribution(u8 *ranks_out, u8 *suits_out) {
get_distribution(get_played_array(), get_played_top(), ranks_out, suits_out);
CardObject** played = get_played_array();
int top = get_played_top();
for (int i = 0; i <= top; i++)
{
/* The difference from get_hand_distribution() (not checking if card is selected)
* is in line Balatro behavior,
* see https://github.com/GBALATRO/balatro-gba/issues/341#issuecomment-3691363488
*/
if (!played[i])
continue;
ranks_out[played[i]->card->rank]++;
suits_out[played[i]->card->suit]++;
}
}
// Returns the highest N of a kind. So a full-house would return 3.
u8 hand_contains_n_of_a_kind(u8 *ranks) {
u8 hand_contains_n_of_a_kind(u8* ranks)
{
u8 highest_n = 0;
for (int i = 0; i < NUM_RANKS; i++) {
for (int i = 0; i < NUM_RANKS; i++)
{
if (ranks[i] > highest_n)
highest_n = ranks[i];
}
return highest_n;
}
bool hand_contains_two_pair(u8 *ranks) {
bool hand_contains_two_pair(u8* ranks)
{
bool contains_other_pair = false;
for (int i = 0; i < NUM_RANKS; i++) {
if (ranks[i] >= 2) {
for (int i = 0; i < NUM_RANKS; i++)
{
if (ranks[i] >= 2)
{
if (contains_other_pair)
return true;
contains_other_pair = true;
@ -44,14 +71,18 @@ bool hand_contains_two_pair(u8 *ranks) {
return false;
}
bool hand_contains_full_house(u8* ranks) {
bool hand_contains_full_house(u8* ranks)
{
int count_three = 0;
int count_pair = 0;
for (int i = 0; i < NUM_RANKS; i++) {
if (ranks[i] >= 3) {
for (int i = 0; i < NUM_RANKS; i++)
{
if (ranks[i] >= 3)
{
count_three++;
}
else if (ranks[i] >= 2) {
else if (ranks[i] >= 2)
{
count_pair++;
}
}
@ -63,26 +94,363 @@ bool hand_contains_full_house(u8* ranks) {
return (count_three >= 2 || (count_three && count_pair));
}
bool hand_contains_straight(u8 *ranks) {
for (int i = 0; i < NUM_RANKS - 4; i++)
// This is mostly from Google Gemini
bool hand_contains_straight(u8* ranks)
{
if (!is_shortcut_joker_active())
{
if (ranks[i] && ranks[i + 1] && ranks[i + 2] && ranks[i + 3] && ranks[i + 4])
return true;
int straight_size = get_straight_and_flush_size();
// This is the regular case of detecting straights
int run = 0;
for (int i = 0; i < NUM_RANKS; ++i)
{
if (ranks[i])
{
if (++run >= straight_size)
return true;
}
else
{
run = 0;
}
}
// Check for ace low straight
if (straight_size >= 2 && ranks[ACE])
{
// With A as low, the highest rank you can use is FIVE.
// -1 for inclusive integer distance and another -1 for the Ace e.g. need=5 -> need 2..5
int last_needed = TWO + (straight_size - 2);
if (last_needed <= FIVE)
{
bool ok = true;
for (int r = TWO; r <= last_needed; ++r)
{
if (!ranks[r])
{
ok = false;
break;
}
}
if (ok)
return true;
}
}
return false;
}
else
{
// Shortcut Joker is active, we have to detect straights where any card may "skip" 1 rank
// We do this with a dynamic programming algorithm that calculates
// the longest possible straight that can end on each rank
// and stopping when we find one that is {straight-size} cards long
u8 longest_short_cut_at[NUM_RANKS] = {0};
// A low ace can start a sequence. 'ace_low_len' is 1 if an ace is present,
// acting as a potential predecessor for TWO and THREE.
int ace_low_len = ranks[ACE] ? 1 : 0;
// Iterate through all ranks from TWO up to ACE.
for (int i = 0; i < NUM_RANKS; i++)
{
// No cards in this rank, no straight can end here, continue
if (ranks[i] == 0)
{
longest_short_cut_at[i] = 0;
continue;
}
int prev_len1 = 0;
int prev_len2 = 0;
// This logic handles the special connections for ace-low straights.
if (i == TWO)
{
// A TWO can be preceded by a low ACE (no skip).
prev_len1 = ace_low_len;
}
else if (i == THREE)
{
// A THREE can be preceded by a TWO (no skip) or a low ACE (skip).
prev_len1 = longest_short_cut_at[TWO];
prev_len2 = ace_low_len;
}
else if (i == ACE)
{
// An ACE (as the highest card) can be preceded by a KING or a QUEEN.
prev_len1 = longest_short_cut_at[KING];
prev_len2 = longest_short_cut_at[QUEEN];
}
else // For all other cards (FOUR through KING).
{
// A card can be preceded by the rank directly below or two ranks below.
prev_len1 = longest_short_cut_at[i - 1];
prev_len2 = longest_short_cut_at[i - 2];
}
// The length of the straight ending at rank 'i' is 1 (for the card itself)
// plus the length of the longest valid preceding straight.
longest_short_cut_at[i] = 1 + max(prev_len1, prev_len2);
// If we've formed a sequence of {straight-size} or more cards, we have a straight.
if (longest_short_cut_at[i] >= get_straight_and_flush_size())
{
return true;
}
}
}
// Check for ace low straight
if (ranks[ACE] && ranks[TWO] && ranks[THREE] && ranks[FOUR] && ranks[FIVE])
return true;
return false;
}
bool hand_contains_flush(u8 *suits) {
bool hand_contains_flush(u8* suits)
{
for (int i = 0; i < NUM_SUITS; i++)
{
if (suits[i] >= MAX_SELECTION_SIZE) // this allows MAX_SELECTION_SIZE - 1 for four fingers joker
if (suits[i] >= get_straight_and_flush_size())
{
return true;
}
}
return false;
}
// Returns the number of cards in the best flush found
// or 0 if no flush of min_len is found, and marks them in out_selection.
/**
* 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.
*
* @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.
* @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)
{
if (top < 0)
return 0;
for (int i = 0; i <= top; i++)
out_selection[i] = false;
int suit_counts[NUM_SUITS] = {0};
for (int i = 0; i <= top; i++)
{
if (played[i] && played[i]->card)
{
suit_counts[played[i]->card->suit]++;
}
}
int best_suit = -1;
int best_count = 0;
for (int i = 0; i < NUM_SUITS; i++)
{
if (suit_counts[i] > best_count)
{
best_count = suit_counts[i];
best_suit = i;
}
}
if (best_count >= min_len)
{
for (int i = 0; i <= top; i++)
{
if (played[i] && played[i]->card && played[i]->card->suit == best_suit)
{
out_selection[i] = true;
}
}
return best_count;
}
return 0;
}
// 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
)
{
if (top < 0)
return 0;
for (int i = 0; i <= top; i++)
out_selection[i] = false;
// --- Setup for Backtracking DP ---
u8 longest_straight_at[NUM_RANKS] = {0};
int parent[NUM_RANKS];
for (int i = 0; i < NUM_RANKS; i++)
parent[i] = -1;
u8 ranks[NUM_RANKS] = {0};
for (int i = 0; i <= top; i++)
{
if (played[i] && played[i]->card)
{
ranks[played[i]->card->rank]++;
}
}
// --- Run DP to find longest straight ---
// This is nearly identical to hand_contains_straight() logic
// TODO: Consolidate functions to avoid code duplication?
// Might cost performance because this does a little more
int ace_low_len = ranks[ACE] ? 1 : 0;
for (int i = 0; i < NUM_RANKS; i++)
{
if (ranks[i] > 0)
{
int prev1 = 0, prev2 = 0;
int parent1 = -1, parent2 = -1;
if (shortcut_active)
{
if (i == TWO)
{
prev1 = ace_low_len;
parent1 = ACE;
}
else if (i == THREE)
{
prev1 = longest_straight_at[TWO];
parent1 = TWO;
prev2 = ace_low_len;
parent2 = ACE;
}
else if (i == ACE)
{
prev1 = longest_straight_at[KING];
parent1 = KING;
prev2 = longest_straight_at[QUEEN];
parent2 = QUEEN;
}
else
{
prev1 = longest_straight_at[i - 1];
parent1 = i - 1;
if (i > 1)
{
prev2 = longest_straight_at[i - 2];
parent2 = i - 2;
}
}
}
else
{
if (i == TWO)
{
prev1 = ace_low_len;
parent1 = ACE;
}
else if (i == ACE)
{
prev1 = longest_straight_at[KING];
parent1 = KING;
}
else
{
prev1 = longest_straight_at[i - 1];
parent1 = i - 1;
}
}
// Parallels longest_short_cut_at[i] = 1 + max(prev_len1, prev_len2);
// in hand_contains_straight()
if (prev1 >= prev2)
{
longest_straight_at[i] = 1 + prev1;
parent[i] = parent1;
}
else
{
longest_straight_at[i] = 1 + prev2;
parent[i] = parent2;
}
}
}
// --- Find best straight and backtrack ---
int best_len = 0;
int end_rank = -1;
for (int i = 0; i < NUM_RANKS; i++)
{
if (longest_straight_at[i] >= best_len)
{
best_len = longest_straight_at[i];
end_rank = i;
}
}
if (best_len >= min_len)
{
u8 needed_ranks[NUM_RANKS] = {0};
int current_rank = end_rank;
while (current_rank != -1 && best_len > 0)
{
needed_ranks[current_rank]++;
current_rank = parent[current_rank];
best_len--;
}
for (int i = 0; i <= top; i++)
{
if (played[i] && played[i]->card && needed_ranks[played[i]->card->rank] > 0)
{
out_selection[i] = true;
needed_ranks[played[i]->card->rank]--;
}
}
int final_card_count = 0;
for (int i = 0; i <= top; i++)
{
if (out_selection[i])
final_card_count++;
}
return final_card_count;
}
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
bool rank_selected[NUM_RANKS] = {0};
bool any_selected_rank = false;
for (int i = 0; i <= played_top; i++)
{
if (selection[i] && played[i] && played[i]->card)
{
rank_selected[played[i]->card->rank] = true;
any_selected_rank = true;
}
}
// If no ranks were selected initially, nothing to do
if (!any_selected_rank)
return;
// Add any unselected card to the selection if if shares a rank with the selected ranks
for (int i = 0; i <= played_top; i++)
{
if (played[i] && played[i]->card && !selection[i])
{
if (rank_selected[played[i]->card->rank])
{
selection[i] = true;
}
}
}
}

View File

@ -1,24 +1,35 @@
#include <tonc.h>
#include "joker.h"
#include "joker_gfx.h"
#include "graphic_utils.h"
#include "card.h"
#include "graphic_utils.h"
#include "joker_gfx.h"
#include "pool.h"
#include "soundbank.h"
#include "util.h"
#include <maxmod.h>
#include <stdlib.h>
#include <string.h>
#include <tonc.h>
#define JOKER_SCORE_TEXT_Y 48
#define JOKER_SCORE_TEXT_Y 48
#define HELD_CARD_SCORE_TEXT_Y 108
#define MAX_CARD_SCORE_STR_LEN 2
#define NUM_JOKERS_PER_SPRITESHEET 2
const unsigned int **joker_gfxTiles;
const unsigned short **joker_gfxPal;
static const unsigned int* joker_gfxTiles[] = {
#define DEF_JOKER_GFX(idx) joker_gfx##idx##Tiles,
#include "../include/def_joker_gfx_table.h"
#undef DEF_JOKER_GFX
};
const static u8 edition_price_lut[MAX_EDITIONS] =
{
static const unsigned short* joker_gfxPal[] = {
#define DEF_JOKER_GFX(idx) joker_gfx##idx##Pal,
#include "def_joker_gfx_table.h"
#undef DEF_JOKER_GFX
};
const static u8 edition_price_lut[MAX_EDITIONS] = {
0, // BASE_EDITION
2, // FOIL_EDITION
3, // HOLO_EDITION
@ -28,163 +39,78 @@ const static u8 edition_price_lut[MAX_EDITIONS] =
/* So for the card objects, I needed them to be properly sorted
which is why they let you specify the layer index when creating a new card object.
Since the cards would overlap a lot in your hand, If they weren't sorted properly, it would look like a mess.
The joker objects are functionally identical to card objects, so they use the same 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.
Since the cards would overlap a lot in your hand, If they weren't sorted properly, it would look
like a mess. The joker objects are functionally identical to card objects, so they use the same
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 _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;
static int joker_pb_num_sprite_users[JOKER_LAST_PB - JOKER_BASE_PB + 1] = { 0 };
static int _joker_spritesheet_pb_map[(MAX_DEFINABLE_JOKERS + 1) / NUM_JOKERS_PER_SPRITESHEET];
static int _joker_pb_num_sprite_users[JOKER_LAST_PB - JOKER_BASE_PB + 1] = {0};
static int get_num_spritesheets()
{
return (get_joker_registry_size() + NUM_JOKERS_PER_SPRITESHEET - 1) / NUM_JOKERS_PER_SPRITESHEET;
}
static int joker_get_spritesheet_idx(u8 joker_id)
{
return joker_id / NUM_JOKERS_PER_SPRITESHEET;
}
// TODO: This should be generalized so any sprite can have dynamic swapping
static void joker_pb_add_sprite_user(int pb)
{
joker_pb_num_sprite_users[pb - JOKER_BASE_PB]++;
}
static void 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);
}
static int joker_pb_get_num_sprite_users(int joker_pb)
{
return joker_pb_num_sprite_users[joker_pb - JOKER_BASE_PB];
}
static int get_unused_joker_pb()
{
for (int i = 0; i < NUM_ELEM_IN_ARR(joker_pb_num_sprite_users); i++)
{
if (joker_pb_num_sprite_users[i] == 0)
{
return (i + JOKER_BASE_PB);
}
}
return UNDEFINED;
}
static int allocate_pb_if_needed(u8 joker_id)
{
int joker_spritesheet_idx = joker_get_spritesheet_idx(joker_id);
int joker_pb = joker_spritesheet_pb_map[joker_spritesheet_idx];
if (joker_pb != UNDEFINED)
{
// Already allocated
return joker_pb;
}
// Allocate a new palette
joker_pb = get_unused_joker_pb();
if (joker_pb == UNDEFINED)
{
// Ran out of palettes, default to base and pray
joker_pb = JOKER_BASE_PB;
}
else
{
joker_spritesheet_pb_map[joker_spritesheet_idx] = joker_pb;
memcpy16(&pal_obj_mem[PAL_ROW_LEN * joker_pb], joker_gfxPal[joker_spritesheet_idx], NUM_ELEM_IN_ARR(joker_gfx0Pal));
}
return joker_pb;
}
static int s_get_num_spritesheets(void);
static int s_joker_get_spritesheet_idx(u8 joker_id);
static void s_joker_pb_add_sprite_user(int pb);
static void s_joker_pb_remove_sprite_user(int pb);
static int s_joker_pb_get_num_sprite_users(int joker_pb);
static int s_get_unused_joker_pb(void);
static int s_allocate_pb_if_needed(u8 joker_id);
void joker_init()
{
// This should init once only so no need to free
int num_spritesheets = get_num_spritesheets();
joker_gfxTiles = (const unsigned int**)malloc((sizeof(unsigned int*) * num_spritesheets));
joker_gfxPal = (const unsigned short**)malloc((sizeof(unsigned int*) * num_spritesheets));
joker_spritesheet_pb_map = (int*)malloc(sizeof(int) * num_spritesheets);
// TODO: Automate this with the preprocessor somehow?
joker_gfxTiles[0] = joker_gfx0Tiles;
joker_gfxTiles[1] = joker_gfx1Tiles;
joker_gfxTiles[2] = joker_gfx2Tiles;
joker_gfxTiles[3] = joker_gfx3Tiles;
joker_gfxTiles[4] = joker_gfx4Tiles;
joker_gfxTiles[5] = joker_gfx5Tiles;
joker_gfxTiles[6] = joker_gfx6Tiles;
joker_gfxTiles[7] = joker_gfx7Tiles;
joker_gfxTiles[8] = joker_gfx8Tiles;
joker_gfxTiles[9] = joker_gfx9Tiles;
joker_gfxTiles[10] = joker_gfx10Tiles;
joker_gfxTiles[11] = joker_gfx11Tiles;
joker_gfxTiles[12] = joker_gfx12Tiles;
joker_gfxTiles[13] = joker_gfx13Tiles;
joker_gfxTiles[14] = joker_gfx14Tiles;
joker_gfxPal[0] = joker_gfx0Pal;
joker_gfxPal[1] = joker_gfx1Pal;
joker_gfxPal[2] = joker_gfx2Pal;
joker_gfxPal[3] = joker_gfx3Pal;
joker_gfxPal[4] = joker_gfx4Pal;
joker_gfxPal[5] = joker_gfx5Pal;
joker_gfxPal[6] = joker_gfx6Pal;
joker_gfxPal[7] = joker_gfx7Pal;
joker_gfxPal[8] = joker_gfx8Pal;
joker_gfxPal[9] = joker_gfx9Pal;
joker_gfxPal[10] = joker_gfx10Pal;
joker_gfxPal[11] = joker_gfx11Pal;
joker_gfxPal[12] = joker_gfx12Pal;
joker_gfxPal[13] = joker_gfx13Pal;
joker_gfxPal[14] = joker_gfx14Pal;
int num_spritesheets = s_get_num_spritesheets();
for (int i = 0; i < num_spritesheets; i++)
{
joker_spritesheet_pb_map[i] = UNDEFINED;
_joker_spritesheet_pb_map[i] = UNDEFINED;
}
}
Joker *joker_new(u8 id)
Joker* joker_new(u8 id)
{
if (id >= get_joker_registry_size()) return NULL;
if (id >= get_joker_registry_size())
return NULL;
Joker *joker = (Joker*)malloc(sizeof(Joker));
const JokerInfo *jinfo = get_joker_registry_entry(id);
Joker* joker = POOL_GET(Joker);
const JokerInfo* jinfo = get_joker_registry_entry(id);
joker->id = id;
joker->modifier = BASE_EDITION; // TODO: Make this a parameter
joker->value = jinfo->base_value + edition_price_lut[joker->modifier];
joker->rarity = jinfo->rarity;
joker->processed = false;
joker->scoring_state = 0;
joker->persistent_state = 0;
// initialize persistent Joker data if needed
JokerEffect* joker_effect = NULL;
jinfo->joker_effect_func(joker, NULL, JOKER_EVENT_ON_JOKER_CREATED, &joker_effect);
return joker;
}
void joker_destroy(Joker **joker)
void joker_destroy(Joker** joker)
{
if (*joker == NULL) return;
free(*joker);
POOL_FREE(Joker, *joker);
*joker = NULL;
}
JokerEffect joker_get_score_effect(Joker *joker, Card *scored_card)
u32 joker_get_score_effect(
Joker* joker,
Card* scored_card,
enum JokerEvent joker_event,
JokerEffect** joker_effect
)
{
const JokerInfo *jinfo = get_joker_registry_entry(joker->id);
if (!jinfo) return (JokerEffect){0};
const JokerInfo* jinfo = get_joker_registry_entry(joker->id);
if (!jinfo)
return JOKER_EFFECT_FLAG_NONE;
return jinfo->effect(joker, scored_card);
return jinfo->joker_effect_func(joker, scored_card, joker_event, joker_effect);
}
int joker_get_sell_value(const Joker* joker)
@ -194,21 +120,21 @@ int joker_get_sell_value(const Joker* joker)
return UNDEFINED;
}
return joker->value/2;
return joker->value / 2;
}
// JokerObject methods
JokerObject *joker_object_new(Joker *joker)
JokerObject* joker_object_new(Joker* joker)
{
JokerObject *joker_object = malloc(sizeof(JokerObject));
JokerObject* joker_object = POOL_GET(JokerObject);
int layer = 0;
for (int i = 0; i < MAX_JOKER_OBJECTS; i++)
{
if (!used_layers[i])
if (!_used_layers[i])
{
layer = i;
used_layers[i] = true; // Mark this layer as used
_used_layers[i] = true; // Mark this layer as used
break;
}
}
@ -217,137 +143,181 @@ JokerObject *joker_object_new(Joker *joker)
joker_object->sprite_object = sprite_object_new();
int tile_index = JOKER_TID + (layer * JOKER_SPRITE_OFFSET);
int joker_spritesheet_idx = joker_get_spritesheet_idx(joker->id);
int joker_spritesheet_idx = s_joker_get_spritesheet_idx(joker->id);
int joker_idx = joker->id % NUM_JOKERS_PER_SPRITESHEET;
int joker_pb = allocate_pb_if_needed(joker->id);
joker_pb_add_sprite_user(joker_pb);
int joker_pb = s_allocate_pb_if_needed(joker->id);
s_joker_pb_add_sprite_user(joker_pb);
memcpy32(&tile_mem[4][tile_index], &joker_gfxTiles[joker_spritesheet_idx][joker_idx * TILE_SIZE * JOKER_SPRITE_OFFSET], TILE_SIZE * JOKER_SPRITE_OFFSET);
memcpy32(
&tile_mem[TILE_MEM_OBJ_CHARBLOCK0_IDX][tile_index],
&joker_gfxTiles[joker_spritesheet_idx][joker_idx * TILE_SIZE * JOKER_SPRITE_OFFSET],
TILE_SIZE * JOKER_SPRITE_OFFSET
);
sprite_object_set_sprite
(
joker_object->sprite_object,
sprite_new
(
ATTR0_SQUARE | ATTR0_4BPP | ATTR0_AFF,
ATTR1_SIZE_32,
tile_index,
sprite_object_set_sprite(
joker_object->sprite_object,
sprite_new(
ATTR0_SQUARE | ATTR0_4BPP | ATTR0_AFF,
ATTR1_SIZE_32,
tile_index,
joker_pb,
JOKER_STARTING_LAYER + layer
)
);
return joker_object;
}
void joker_object_destroy(JokerObject **joker_object)
void joker_object_destroy(JokerObject** joker_object)
{
if (joker_object == NULL || *joker_object == NULL) return;
if (joker_object == NULL || *joker_object == NULL)
return;
int layer = sprite_get_layer(joker_object_get_sprite(*joker_object)) - JOKER_STARTING_LAYER;
used_layers[layer] = false;
joker_pb_remove_sprite_user(sprite_get_pb(joker_object_get_sprite(*joker_object)));
if (joker_pb_get_num_sprite_users((sprite_get_pb(joker_object_get_sprite(*joker_object)))) == 0)
_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[joker_get_spritesheet_idx((*joker_object)->joker->id)] = UNDEFINED;
_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
free(*joker_object);
joker_destroy(&(*joker_object)->joker); // Destroy the joker
POOL_FREE(JokerObject, *joker_object);
*joker_object = NULL;
}
void joker_object_update(JokerObject *joker_object)
void joker_object_update(JokerObject* joker_object)
{
CardObject *card_object = (CardObject *)joker_object;
CardObject* card_object = (CardObject*)joker_object;
card_object_update(card_object);
}
void joker_object_shake(JokerObject *joker_object, mm_word sound_id)
void joker_object_shake(JokerObject* joker_object, mm_word sound_id)
{
sprite_object_shake(joker_object->sprite_object, sound_id);
}
bool joker_object_score(JokerObject *joker_object, Card* scored_card, int *chips, int *mult, int *xmult, int *money, bool *retrigger)
void set_and_shift_text(char* str, int* cursor_pos_x, int* cursor_pos_y, int color_pb)
{
if (joker_object->joker->processed == true) return false; // If the joker has already been processed, return false
tte_set_pos(*cursor_pos_x, *cursor_pos_y);
tte_set_special(color_pb * TTE_SPECIAL_PB_MULT_OFFSET);
tte_write(str);
JokerEffect joker_effect = joker_get_score_effect(joker_object->joker, scored_card);
// + 1 For space
const int joker_score_display_offset_px = (MAX_CARD_SCORE_STR_LEN + 1) * TTE_CHAR_SIZE;
*cursor_pos_x += joker_score_display_offset_px;
}
if (memcmp(&joker_effect, &(JokerEffect){0}, sizeof(JokerEffect)) != 0)
bool joker_object_score(
JokerObject* joker_object,
CardObject* card_object,
enum JokerEvent joker_event
)
{
if (joker_object == NULL)
{
*chips += joker_effect.chips;
*mult += joker_effect.mult;
*mult *= joker_effect.xmult > 0 ? joker_effect.xmult : 1; // if xmult is zero, DO NOT multiply by it
*money += joker_effect.money;
// TODO: Retrigger
const int joker_score_display_offset_px = (MAX_CARD_SCORE_STR_LEN + 1)*TTE_CHAR_SIZE;
// + 1 For space
int cursorPosX = fx2int(joker_object->sprite_object->x) + 8; // Offset of 16 pixels to center the text on the card
if (joker_effect.chips > 0)
{
char score_buffer[INT_MAX_DIGITS + 2]; // For '+' and null terminator
tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y);
tte_set_special(0xD000); // Blue
snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.chips);
tte_write(score_buffer);
cursorPosX += joker_score_display_offset_px;
}
if (joker_effect.mult > 0)
{
char score_buffer[INT_MAX_DIGITS + 2];
tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y);
tte_set_special(0xE000); // Red
snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.mult);
tte_write(score_buffer);
cursorPosX += joker_score_display_offset_px;
}
if (joker_effect.xmult > 0)
{
char score_buffer[INT_MAX_DIGITS + 2];
tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y);
tte_set_special(0xE000); // Red
snprintf(score_buffer, sizeof(score_buffer), "X%d", joker_effect.xmult);
tte_write(score_buffer);
cursorPosX += joker_score_display_offset_px;
}
if (joker_effect.money > 0)
{
char score_buffer[INT_MAX_DIGITS + 2];
tte_set_pos(cursorPosX, JOKER_SCORE_TEXT_Y);
tte_set_special(0xC000); // Yellow
snprintf(score_buffer, sizeof(score_buffer), "+%d", joker_effect.money);
tte_write(score_buffer);
cursorPosX += joker_score_display_offset_px;
}
joker_object->joker->processed = true; // Mark the joker as processed
joker_object_shake(joker_object, SFX_CARD_SELECT); // TODO: Add a sound effect for scoring the joker
return true;
return false;
}
return false;
}
JokerEffect* joker_effect = NULL;
u32 effect_flags_ret =
joker_get_score_effect(joker_object->joker, card_object->card, joker_event, &joker_effect);
void joker_object_set_selected(JokerObject* joker_object, bool selected)
{
if (joker_object == NULL)
return;
sprite_object_set_selected(joker_object->sprite_object, selected);
}
bool joker_object_is_selected(JokerObject* joker_object)
{
if (joker_object == NULL)
if (effect_flags_ret == JOKER_EFFECT_FLAG_NONE)
{
return false;
return sprite_object_is_selected(joker_object->sprite_object);
}
u32 chips = get_chips();
u32 mult = get_mult();
int money = get_money();
if (effect_flags_ret & JOKER_EFFECT_FLAG_RETRIGGER)
{
set_retrigger(joker_effect->retrigger);
}
// joker_effect.message will have been set if the Joker had anything custom to say
int cursorPosX = TILE_SIZE; // Offset of one tile to better center the text on the card
int cursorPosY = 0;
if (joker_event == JOKER_EVENT_ON_CARD_HELD)
{
// 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);
cursorPosY = HELD_CARD_SCORE_TEXT_Y;
}
else
{
cursorPosX += fx2int(joker_object->sprite_object->x);
cursorPosY = JOKER_SCORE_TEXT_Y;
}
mm_word sfx_id;
if (effect_flags_ret & JOKER_EFFECT_FLAG_CHIPS)
{
chips = u32_protected_add(chips, joker_effect->chips);
char score_buffer[INT_MAX_DIGITS + 2]; // For '+' and null terminator
snprintf(score_buffer, sizeof(score_buffer), "+%lu", joker_effect->chips);
set_and_shift_text(score_buffer, &cursorPosX, &cursorPosY, TTE_BLUE_PB);
sfx_id = SFX_CHIPS_GENERIC; // The joker chips effect is "generic"
}
if (effect_flags_ret & JOKER_EFFECT_FLAG_MULT)
{
mult = u32_protected_add(mult, joker_effect->mult);
char score_buffer[INT_MAX_DIGITS + 2];
snprintf(score_buffer, sizeof(score_buffer), "+%lu", joker_effect->mult);
set_and_shift_text(score_buffer, &cursorPosX, &cursorPosY, TTE_RED_PB);
sfx_id = SFX_MULT;
}
// if xmult is zero, DO NOT multiply by it
if (effect_flags_ret & JOKER_EFFECT_FLAG_XMULT && joker_effect->xmult > 0)
{
mult = u32_protected_mult(mult, joker_effect->xmult);
char score_buffer[INT_MAX_DIGITS + 2];
snprintf(score_buffer, sizeof(score_buffer), "X%lu", joker_effect->xmult);
set_and_shift_text(score_buffer, &cursorPosX, &cursorPosY, TTE_RED_PB);
sfx_id = SFX_XMULT;
}
if (effect_flags_ret & JOKER_EFFECT_FLAG_MONEY)
{
money += joker_effect->money;
char score_buffer[INT_MAX_DIGITS + 2];
snprintf(score_buffer, sizeof(score_buffer), "%d$", joker_effect->money);
set_and_shift_text(score_buffer, &cursorPosX, &cursorPosY, TTE_YELLOW_PB);
// TODO: Money sound effect
}
// custom message for Jokers (including retriggers where Jokers will say "Again!")
// joker_effect->message will have been set if the Joker had anything custom to say
if (effect_flags_ret & JOKER_EFFECT_FLAG_MESSAGE)
{
set_and_shift_text(joker_effect->message, &cursorPosX, &cursorPosY, TTE_WHITE_PB);
}
// this will start the Joker expire animation
if (effect_flags_ret & JOKER_EFFECT_FLAG_EXPIRE && joker_effect->expire)
{
joker_object_shake(joker_object, UNDEFINED);
list_push_back(get_expired_jokers_list(), joker_object);
}
// Update values
set_chips(chips);
set_mult(mult);
set_money(money);
// Update displays
display_chips();
display_mult();
display_money();
joker_object_shake(joker_object, sfx_id);
return true;
}
Sprite* joker_object_get_sprite(JokerObject* joker_object)
@ -356,3 +326,99 @@ Sprite* joker_object_get_sprite(JokerObject* joker_object)
return NULL;
return sprite_object_get_sprite(joker_object->sprite_object);
}
int joker_get_random_rarity()
{
int joker_rarity = 0;
int rarity_roll = random() % 100;
if (rarity_roll < COMMON_JOKER_CHANCE)
{
joker_rarity = COMMON_JOKER;
}
else if (rarity_roll < COMMON_JOKER_CHANCE + UNCOMMON_JOKER_CHANCE)
{
joker_rarity = UNCOMMON_JOKER;
}
else if (rarity_roll < COMMON_JOKER_CHANCE + UNCOMMON_JOKER_CHANCE + RARE_JOKER_CHANCE)
{
joker_rarity = RARE_JOKER;
}
else if (rarity_roll < COMMON_JOKER_CHANCE + UNCOMMON_JOKER_CHANCE + RARE_JOKER_CHANCE +
LEGENDARY_JOKER_CHANCE)
{
joker_rarity = LEGENDARY_JOKER;
}
return joker_rarity;
}
static int s_get_num_spritesheets()
{
return (get_joker_registry_size() + NUM_JOKERS_PER_SPRITESHEET - 1) /
NUM_JOKERS_PER_SPRITESHEET;
}
static int s_joker_get_spritesheet_idx(u8 joker_id)
{
return joker_id / NUM_JOKERS_PER_SPRITESHEET;
}
static void s_joker_pb_add_sprite_user(int pb)
{
_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);
}
static int s_joker_pb_get_num_sprite_users(int joker_pb)
{
return _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++)
{
if (_joker_pb_num_sprite_users[i] == 0)
{
return (i + JOKER_BASE_PB);
}
}
return UNDEFINED;
}
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];
if (joker_pb != UNDEFINED)
{
// Already allocated
return joker_pb;
}
// Allocate a new palette
joker_pb = s_get_unused_joker_pb();
if (joker_pb == UNDEFINED)
{
// Ran out of palettes, default to base and pray
joker_pb = JOKER_BASE_PB;
}
else
{
_joker_spritesheet_pb_map[joker_spritesheet_idx] = joker_pb;
memcpy16(
&pal_obj_mem[PAL_ROW_LEN * joker_pb],
joker_gfxPal[joker_spritesheet_idx],
NUM_ELEM_IN_ARR(joker_gfx0Pal)
);
}
return joker_pb;
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More