From f80aa4942d398d11371890370508474144f09a2f Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 16 Sep 2024 00:27:15 +0000 Subject: [PATCH 01/54] Add chart file processing --- tools/assets_preprocessor/README.md | 6 ++ tools/assets_preprocessor/makefile | 2 +- .../src/assets_preprocessor.c | 21 ++-- .../assets_preprocessor/src/chart_processor.c | 101 ++++++++++++++++++ .../assets_preprocessor/src/chart_processor.h | 6 ++ .../assets_preprocessor/src/heatshrink_util.c | 3 +- .../assets_preprocessor/src/image_processor.c | 4 +- tools/assets_preprocessor/src/raw_processor.c | 9 +- 8 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 tools/assets_preprocessor/src/chart_processor.c create mode 100644 tools/assets_preprocessor/src/chart_processor.h diff --git a/tools/assets_preprocessor/README.md b/tools/assets_preprocessor/README.md index 7cda7eed5..c505952db 100644 --- a/tools/assets_preprocessor/README.md +++ b/tools/assets_preprocessor/README.md @@ -47,3 +47,9 @@ No processing is done on `.txt` files. They are copied from the input directory ### `.mid`, `.midi` The notes on the first track from the MIDI file are converted to buzzer format (frequency, duration), and compressed with Heatshrink compression. The buzzer is single channel, single voice, so if a note starts while another is playing, the first note is stopped. + +### `.chart` + +Generate Clone Hero charts [using this tool](https://efhiii.github.io/midi-ch/), see https://github.com/EFHIII/midi-ch. + +[.chart file format spec](https://github.com/TheNathannator/GuitarGame_ChartFormats/blob/main/doc/FileFormats/.chart/Core%20Infrastructure.md). diff --git a/tools/assets_preprocessor/makefile b/tools/assets_preprocessor/makefile index 34954201d..52963e3b0 100644 --- a/tools/assets_preprocessor/makefile +++ b/tools/assets_preprocessor/makefile @@ -154,7 +154,7 @@ clean: -@rm -f $(OBJECTS) $(EXECUTABLE) format: - clang-format -i -style=file $(SOURCES_TO_FORMAT) + clang-format-17 -i -style=file $(SOURCES_TO_FORMAT) ################################################################################ # Makefile Debugging diff --git a/tools/assets_preprocessor/src/assets_preprocessor.c b/tools/assets_preprocessor/src/assets_preprocessor.c index 6d9c357a9..311d7212a 100644 --- a/tools/assets_preprocessor/src/assets_preprocessor.c +++ b/tools/assets_preprocessor/src/assets_preprocessor.c @@ -10,6 +10,7 @@ #include #include +#include "chart_processor.h" #include "image_processor.h" #include "font_processor.h" #include "json_processor.h" @@ -23,9 +24,9 @@ * */ static const char* rawFileTypes[][2] = { - { "mid", "mid"}, - { "midi", "mid"}, - { "raw", "raw"}, + {"mid", "mid"}, + {"midi", "mid"}, + {"raw", "raw"}, }; const char* outDirName = NULL; @@ -81,6 +82,10 @@ static int processFile(const char* fpath, const struct stat* st __attribute__((u { process_image(fpath, outDirName); } + else if (endsWith(fpath, ".chart")) + { + process_chart(fpath, outDirName); + } else if (endsWith(fpath, ".json")) { process_json(fpath, outDirName); @@ -176,13 +181,11 @@ int main(int argc, char** argv) struct stat st = {0}; if (stat(outDirName, &st) == -1) { -#if defined(WINDOWS) || defined(__WINDOWS__) || defined(_WINDOWS) \ - || defined(WIN32) || defined(WIN64) \ - || defined(_WIN32) || defined(_WIN64) \ - || defined(__WIN32__) || defined(__TOS_WIN__) \ - || defined(_MSC_VER) +#if defined(WINDOWS) || defined(__WINDOWS__) || defined(_WINDOWS) || defined(WIN32) || defined(WIN64) \ + || defined(_WIN32) || defined(_WIN64) || defined(__WIN32__) || defined(__TOS_WIN__) || defined(_MSC_VER) mkdir(outDirName); -#elif defined(__linux) || defined(__linux__) || defined(linux) || defined(__LINUX__) || defined(__CYGWIN__) || defined(__APPLE__) +#elif defined(__linux) || defined(__linux__) || defined(linux) || defined(__LINUX__) || defined(__CYGWIN__) \ + || defined(__APPLE__) mkdir(outDirName, 0777); #endif } diff --git a/tools/assets_preprocessor/src/chart_processor.c b/tools/assets_preprocessor/src/chart_processor.c new file mode 100644 index 000000000..f226e9270 --- /dev/null +++ b/tools/assets_preprocessor/src/chart_processor.c @@ -0,0 +1,101 @@ +#include +#include +#include +#include + +#include "chart_processor.h" +#include "fileUtils.h" + +typedef enum +{ + CS_NONE, + CS_SONG, + CS_SYNC_TRACK, + CS_NOTES, +} charSection_t; + +void process_chart(const char* infile, const char* outdir) +{ + /* Determine if the output file already exists */ + char outFilePath[128] = {0}; + strcat(outFilePath, outdir); + strcat(outFilePath, "/"); + strcat(outFilePath, get_filename(infile)); + + char* dotptr = strrchr(outFilePath, '.'); + snprintf(&dotptr[1], strlen(dotptr), "cch"); + + if (doesFileExist(outFilePath)) + { + // printf("Output for %s already exists\n", infile); + // return; + } + + charSection_t section = CS_NONE; + bool inSection = false; + + /* Read input file */ + FILE* fp = fopen(infile, "rb"); + + if (fp) + { + FILE* outFile = fopen(outFilePath, "wb"); + + if (outFile) + { + char line[512] = {0}; + char sectionName[512] = {0}; + while (NULL != fgets(line, sizeof(line), fp)) + { + int tick, note, length; + + if (sscanf(line, "[%s]", sectionName)) + { + sectionName[strlen(sectionName) - 1] = 0; + + if (strstr(sectionName, "Song")) + { + section = CS_SONG; + } + else if (strstr(sectionName, "SyncTrack")) + { + section = CS_SYNC_TRACK; + } + else + { + section = CS_NOTES; + } + } + else if (strstr(line, "{")) + { + inSection = true; + } + else if (strstr(line, "}")) + { + inSection = false; + } + else if (inSection && (CS_NOTES == section) && // + sscanf(line, "%d = N %d %d", &tick, ¬e, &length)) + { + fputc((tick >> 24) & 0xFF, outFile); + fputc((tick >> 16) & 0xFF, outFile); + fputc((tick >> 8) & 0xFF, outFile); + fputc((tick >> 0) & 0xFF, outFile); + + if (length) + { + fputc(note & 0x80, outFile); + fputc((length >> 8) & 0xFF, outFile); + fputc((length >> 0) & 0xFF, outFile); + } + else + { + fputc(note, outFile); + } + } + } + fclose(outFile); + } + fclose(fp); + } +} diff --git a/tools/assets_preprocessor/src/chart_processor.h b/tools/assets_preprocessor/src/chart_processor.h new file mode 100644 index 000000000..0508149d0 --- /dev/null +++ b/tools/assets_preprocessor/src/chart_processor.h @@ -0,0 +1,6 @@ +#ifndef _CHART_PROCESSOR_H_ +#define _CHART_PROCESSOR_H_ + +void process_chart(const char* infile, const char* outdir); + +#endif \ No newline at end of file diff --git a/tools/assets_preprocessor/src/heatshrink_util.c b/tools/assets_preprocessor/src/heatshrink_util.c index 616671e86..d8531d6bf 100644 --- a/tools/assets_preprocessor/src/heatshrink_util.c +++ b/tools/assets_preprocessor/src/heatshrink_util.c @@ -127,7 +127,8 @@ void writeHeatshrinkFile(uint8_t* input, uint32_t len, const char* outFilePath) /* Write a compressed file */ shrunkFile = fopen(outFilePath, "wb"); - if (shrunkFile == NULL) { + if (shrunkFile == NULL) + { perror("Error occurred while writing file.\n"); } /* First four bytes are decompresed size */ diff --git a/tools/assets_preprocessor/src/image_processor.c b/tools/assets_preprocessor/src/image_processor.c index 76c32deaa..203c46427 100644 --- a/tools/assets_preprocessor/src/image_processor.c +++ b/tools/assets_preprocessor/src/image_processor.c @@ -10,8 +10,8 @@ #pragma GCC diagnostic push #endif #ifdef __GNUC__ -#pragma GCC diagnostic ignored "-Wcast-qual" -#pragma GCC diagnostic ignored "-Wmissing-prototypes" + #pragma GCC diagnostic ignored "-Wcast-qual" + #pragma GCC diagnostic ignored "-Wmissing-prototypes" #endif #define STB_IMAGE_IMPLEMENTATION diff --git a/tools/assets_preprocessor/src/raw_processor.c b/tools/assets_preprocessor/src/raw_processor.c index 366d83959..e954a6986 100644 --- a/tools/assets_preprocessor/src/raw_processor.c +++ b/tools/assets_preprocessor/src/raw_processor.c @@ -28,8 +28,8 @@ void process_raw(const char* inFile, const char* outDir, const char* outExt) // Read input file const char* errdesc = NULL; - errno = 0; - FILE* fp = fopen(inFile, "rb"); + errno = 0; + FILE* fp = fopen(inFile, "rb"); if (!fp) { errdesc = strerror(errno); @@ -48,12 +48,13 @@ void process_raw(const char* inFile, const char* outDir, const char* outExt) return; } - errno = 0; + errno = 0; int readlen = fread(byteString, 1, sz, fp); if (readlen < sz) { errdesc = (errno == 0) ? "Read too small" : strerror(errno); - fprintf(stderr, "ERR: raw_processor.c: Failed to read file %s: %d - %s\n", inFile, readlen, errdesc ? errdesc : "Unknown"); + fprintf(stderr, "ERR: raw_processor.c: Failed to read file %s: %d - %s\n", inFile, readlen, + errdesc ? errdesc : "Unknown"); free(byteString); fclose(fp); From 1e675e8b0064e32f68c49611cc08b66816f5b831 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 16 Sep 2024 00:28:08 +0000 Subject: [PATCH 02/54] Update submodules and make a target to do so automatically --- emulator/src-lib/rawdraw | 2 +- makefile | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/emulator/src-lib/rawdraw b/emulator/src-lib/rawdraw index a53ea30df..2dd1b18b0 160000 --- a/emulator/src-lib/rawdraw +++ b/emulator/src-lib/rawdraw @@ -1 +1 @@ -Subproject commit a53ea30df86f5876197873975efee3dbc983a942 +Subproject commit 2dd1b18b0d7cc69c34bd7c47b7fa9567e4ae01ba diff --git a/makefile b/makefile index 0ba928d2f..a05882b98 100644 --- a/makefile +++ b/makefile @@ -70,6 +70,8 @@ SOURCES = $(sort $(shell $(FIND) $(SRC_DIRS) -maxdepth 1 -iname "*.[c]") $(SRC # The emulator doesn't build components, but there is a target for formatting them ALL_FILES = $(shell $(FIND) components $(SRC_DIRS_RECURSIVE) -iname "*.[c|h]") +SUBMODULES = $(shell git config --file .gitmodules --name-only --get-regexp path | sed -nr 's/submodule.(.*).path/\1/p') + ################################################################################ # Includes ################################################################################ @@ -311,7 +313,7 @@ EXECUTABLE = swadge_emulator ################################################################################ # This list of targets do not build files which match their name -.PHONY: all assets bundle clean docs format cppcheck firmware clean-firmware $(CNFS_FILE) print-% +.PHONY: all assets bundle clean docs format cppcheck firmware clean-firmware $(CNFS_FILE) update-submodules print-% # Build the executable all: $(EXECUTABLE) @@ -477,5 +479,12 @@ gen-coverage: genhtml ./coverage.info --output-directory ./coverage firefox ./coverage/index.html & +update-submodules: + for submodule in $(SUBMODULES) ; do \ + echo Updating $$submodule to latest ; \ + git -C $$submodule fetch --prune ; \ + git -C $$submodule checkout origin/HEAD ; \ + done + # Print any value from this makefile print-% : ; @echo $* = $($*) From c3530cc77055d43b656368e8d56366f6a5e44745 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 16 Sep 2024 02:25:17 +0000 Subject: [PATCH 03/54] Write number of notes in compressed chart Fix hold bit --- .../assets_preprocessor/src/chart_processor.c | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/tools/assets_preprocessor/src/chart_processor.c b/tools/assets_preprocessor/src/chart_processor.c index f226e9270..ca26b8bfa 100644 --- a/tools/assets_preprocessor/src/chart_processor.c +++ b/tools/assets_preprocessor/src/chart_processor.c @@ -43,6 +43,10 @@ void process_chart(const char* infile, const char* outdir) if (outFile) { + uint8_t* tmpSpace = malloc(1024 * 1024); + uint32_t tmpIdx = 0; + uint32_t noteCount = 0; + char line[512] = {0}; char sectionName[512] = {0}; while (NULL != fgets(line, sizeof(line), fp)) @@ -72,28 +76,40 @@ void process_chart(const char* infile, const char* outdir) } else if (strstr(line, "}")) { + if(CS_NOTES == section) + { + fputc((noteCount >> 8) & 0xFF, outFile); + fputc((noteCount >> 0) & 0xFF, outFile); + noteCount = 0; + fwrite(tmpSpace, tmpIdx, 1, outFile); + tmpIdx = 0; + } inSection = false; } else if (inSection && (CS_NOTES == section) && // sscanf(line, "%d = N %d %d", &tick, ¬e, &length)) { - fputc((tick >> 24) & 0xFF, outFile); - fputc((tick >> 16) & 0xFF, outFile); - fputc((tick >> 8) & 0xFF, outFile); - fputc((tick >> 0) & 0xFF, outFile); + noteCount++; + + tmpSpace[tmpIdx++] = (tick >> 24) & 0xFF; + tmpSpace[tmpIdx++] = (tick >> 16) & 0xFF; + tmpSpace[tmpIdx++] = (tick >> 8) & 0xFF; + tmpSpace[tmpIdx++] = (tick >> 0) & 0xFF; if (length) { - fputc(note & 0x80, outFile); - fputc((length >> 8) & 0xFF, outFile); - fputc((length >> 0) & 0xFF, outFile); + tmpSpace[tmpIdx++] = note | 0x80; + + tmpSpace[tmpIdx++] = (length >> 8) & 0xFF; + tmpSpace[tmpIdx++] = (length >> 0) & 0xFF; } else { - fputc(note, outFile); + tmpSpace[tmpIdx++] = note; } } } + free(tmpSpace); fclose(outFile); } fclose(fp); From 97f331ef09bfc18d67781ae0c69212ebe50dc435 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 16 Sep 2024 02:25:29 +0000 Subject: [PATCH 04/54] Add basic SSR mode that barely works --- assets/credits.chart | 302 ++++++++++++++++++++++++++ main/modes/games/ssr/mode_ssr.c | 232 ++++++++++++++++++++ main/modes/games/ssr/mode_ssr.h | 5 + main/modes/system/mainMenu/mainMenu.c | 6 + 4 files changed, 545 insertions(+) create mode 100644 assets/credits.chart create mode 100644 main/modes/games/ssr/mode_ssr.c create mode 100644 main/modes/games/ssr/mode_ssr.h diff --git a/assets/credits.chart b/assets/credits.chart new file mode 100644 index 000000000..17471ecb9 --- /dev/null +++ b/assets/credits.chart @@ -0,0 +1,302 @@ +[Song] +{ + Name = " "SWADGE Credits New FINAL Sync3"" + Charter = "Edward Haas" + Year = "Unknown" + Offset = 0 + Resolution = 960 + Player2 = bass + Difficulty = 4 + PreviewStart = 0 + PreviewEnd = 10000 + Genre = "rock" + MediaType = "cd" + MusicStream = "song.ogg" +} +[SyncTrack] +{ + 0 = TS 4 + 0 = B 120000 + 0 = TS 4 + 0 = B 125000 +} +[ExpertSingle] +{ + 4040 = N 1 0 + 4280 = N 1 0 + 4540 = N 1 0 + 5540 = N 1 0 + 5800 = N 1 0 + 6060 = N 1 0 + 6560 = N 1 0 + 8100 = N 2 0 + 9100 = N 0 0 + 9320 = N 1 0 + 9580 = N 2 0 + 9780 = N 3 0 + 10080 = N 4 0 + 10600 = N 2 0 + 10740 = N 3 0 + 10840 = N 2 0 + 11100 = N 1 0 + 11600 = N 0 0 + 12100 = N 4 0 + 12900 = N 2 0 + 13160 = N 2 1460 + 16140 = N 1 0 + 16640 = N 1 0 + 16900 = N 2 0 + 17140 = N 3 0 + 17900 = N 4 0 + 18160 = N 0 0 + 19160 = N 4 0 + 19420 = N 3 0 + 19660 = N 2 0 + 19940 = N 0 0 + 20160 = N 1 0 + 20940 = N 2 0 + 21180 = N 3 1480 + 23200 = N 2 0 + 23700 = N 1 0 + 24200 = N 0 0 + 24720 = N 0 0 + 24960 = N 1 0 + 25220 = N 2 0 + 25960 = N 3 0 + 26220 = N 2 0 + 26720 = N 1 0 + 27220 = N 4 0 + 27740 = N 2 0 + 28240 = N 2 0 + 28740 = N 1 0 + 28880 = N 2 0 + 29000 = N 1 0 + 29240 = N 0 0 + 29740 = N 1 0 + 30260 = N 2 0 + 31260 = N 3 0 + 32020 = N 4 0 + 32260 = N 2 1040 + 33780 = N 4 0 + 33780 = E Bad_Too_High + 34040 = N 3 0 + 34280 = N 2 0 + 34780 = N 1 0 + 34960 = N 2 0 + 35120 = N 1 0 + 35300 = N 0 0 + 35800 = N 1 0 + 36300 = N 3 1060 + 37840 = N 4 0 + 38100 = N 3 0 + 38320 = N 2 0 + 40360 = N 1 0 + 41360 = N 0 0 + 41580 = N 1 0 + 41860 = N 2 0 + 42060 = N 3 0 + 42360 = N 4 0 + 42860 = N 2 0 + 42980 = N 3 0 + 43120 = N 2 0 + 43360 = N 1 0 + 43860 = N 0 0 + 44360 = N 4 0 + 45180 = N 2 0 + 45420 = N 2 1740 + 48400 = N 1 0 + 48900 = N 1 0 + 49160 = N 2 0 + 49420 = N 3 0 + 50160 = N 4 0 + 50420 = N 0 0 + 51440 = N 4 0 + 51680 = N 3 0 + 51940 = N 2 0 + 52200 = N 0 0 + 52440 = N 1 0 + 53220 = N 3 0 + 53440 = N 4 1480 + 55460 = N 3 0 + 55980 = N 2 0 + 56480 = N 1 0 + 56980 = N 1 0 + 57240 = N 2 0 + 57480 = N 3 0 + 58240 = N 4 0 + 58480 = N 1 0 + 59000 = N 0 0 + 59500 = N 4 0 + 60000 = N 3 0 + 60500 = N 3 0 + 61000 = N 4 0 + 61140 = N 3 0 + 61260 = N 2 0 + 61520 = N 0 0 + 62020 = N 1 0 + 62520 = N 2 0 + 63520 = N 3 0 + 64280 = N 4 0 + 64540 = N 1 1040 + 66060 = N 4 0 + 66060 = E Bad_Too_High + 66300 = N 3 0 + 66560 = N 2 0 + 67060 = N 1 0 + 67220 = N 2 0 + 67400 = N 1 0 + 67560 = N 0 0 + 68060 = N 4 0 + 68580 = N 3 1220 + 71600 = N 2 0 + 71680 = N 3 0 + 72100 = N 1 0 + 72600 = N 0 0 + 73360 = N 1 0 + 73620 = N 3 1020 + 75120 = N 0 0 + 75380 = N 1 0 + 75640 = N 3 0 + 75880 = N 4 0 + 76140 = N 0 0 + 76140 = E Bad_Too_Low + 76380 = N 4 0 + 76640 = N 2 0 + 77400 = N 1 0 + 77640 = N 2 0 + 77780 = N 3 1380 + 79660 = N 3 0 + 80180 = N 3 0 + 80680 = N 4 0 + 81680 = N 0 0 + 81940 = N 1 0 + 82180 = N 2 0 + 82440 = N 4 0 + 82680 = N 3 0 + 83200 = N 0 0 + 83640 = N 3 0 + 84460 = N 2 0 + 84700 = N 1 2240 + 87740 = N 4 0 + 87740 = E Bad_Too_High + 88240 = N 3 0 + 88740 = N 2 0 + 89740 = N 0 0 + 90000 = N 1 0 + 90260 = N 2 0 + 90500 = N 4 0 + 90760 = N 3 0 + 91260 = N 1 0 + 91440 = N 2 0 + 91600 = N 1 0 + 91760 = N 0 0 + 92260 = N 1 0 + 92780 = N 3 0 + 93540 = N 4 0 + 93780 = N 4 1460 + 95800 = N 1 0 + 96300 = N 3 0 + 96740 = N 3 0 + 98320 = N 2 0 + 98480 = N 4 0 + 98660 = N 3 0 + 98820 = N 2 0 + 99840 = N 1 0 + 100840 = N 2 0 + 101860 = N 1 0 + 102360 = N 2 0 + 102600 = N 1 0 + 102860 = N 0 1380 + 104900 = N 2 0 + 105900 = N 0 0 + 106120 = N 1 0 + 106380 = N 2 0 + 106600 = N 3 0 + 106900 = N 4 0 + 107400 = N 2 0 + 107520 = N 3 0 + 107640 = N 2 0 + 107900 = N 1 0 + 108400 = N 0 0 + 108900 = N 4 0 + 109660 = N 2 0 + 109920 = N 2 1740 + 112940 = N 1 0 + 113440 = N 1 0 + 113700 = N 2 0 + 113940 = N 3 0 + 114700 = N 4 0 + 114960 = N 0 0 + 115980 = N 4 0 + 116220 = N 3 0 + 116480 = N 2 0 + 116740 = N 0 0 + 116980 = N 1 0 + 117740 = N 3 0 + 117980 = N 4 1480 + 120000 = N 3 0 + 120500 = N 2 0 + 121020 = N 1 0 + 121520 = N 1 0 + 121760 = N 2 0 + 122020 = N 3 0 + 122780 = N 4 0 + 123020 = N 1 0 + 123520 = N 0 0 + 124040 = N 3 0 + 124540 = N 2 0 + 125040 = N 2 0 + 125540 = N 3 0 + 125720 = N 2 0 + 125880 = N 1 0 + 126060 = N 0 0 + 126560 = N 1 0 + 127060 = N 2 0 + 128060 = N 3 0 + 128820 = N 4 0 + 129080 = N 1 1040 + 130600 = N 4 0 + 130600 = E Bad_Too_High + 130760 = N 4 0 + 130840 = N 3 0 + 131100 = N 2 0 + 131600 = N 1 0 + 132100 = N 0 0 + 132600 = N 1 0 + 133100 = N 2 1040 + 134620 = N 3 0 + 134880 = N 2 0 + 135120 = N 1 0 + 136020 = N 3 0 + 136140 = N 4 0 + 137140 = N 1 0 + 137140 = E Bad_Too_Low + 138180 = N 1 0 + 138640 = N 0 0 + 138880 = N 1 0 + 139140 = N 2 0 + 139640 = N 3 0 + 140180 = N 4 0 + 140700 = N 3 0 + 141180 = N 4 0 + 142180 = N 4 0 + 142680 = N 0 0 + 142940 = N 1 0 + 143200 = N 2 0 + 143700 = N 3 0 + 144200 = N 4 0 + 144700 = N 3 0 + 144700 = E Bad_Too_Low + 145220 = N 3 0 + 145380 = N 4 2360 + 148240 = N 0 0 + 148480 = N 1 0 + 148740 = N 2 0 + 149000 = N 3 0 + 149240 = N 4 0 + 150260 = N 0 0 + 150760 = N 0 0 + 151020 = N 0 0 + 151260 = N 0 0 + 153260 = N 0 0 +} diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c new file mode 100644 index 000000000..dd9ad9cd7 --- /dev/null +++ b/main/modes/games/ssr/mode_ssr.c @@ -0,0 +1,232 @@ +#include "limits.h" +#include "menu.h" +#include "mode_ssr.h" + +typedef struct +{ + int32_t tick; + int32_t note; + int32_t hold; +} ssrNote_t; + +typedef struct +{ + font_t ibm; + midiFile_t credits; + ssrNote_t* notes; + int32_t tDiv; + int32_t tempo; + int32_t cNote; + int32_t tElapsedMs; + int32_t draw; +} ssrVars_t; + +ssrVars_t* ssr; + +static void ssrEnterMode(void); +static void ssrExitMode(void); +static void ssrMainLoop(int64_t elapsedUs); +static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); +static void loadTrackData(ssrVars_t* ssr, const uint8_t* data, size_t size); + +// static void ssrMenuCb(const char*, bool selected, uint32_t settingVal); + +static const char ssrName[] = "Swadge Swadge Rebellion"; + +swadgeMode_t ssrMode = { + .modeName = ssrName, + .wifiMode = ESP_NOW, + .overrideUsb = false, + .usesAccelerometer = true, + .usesThermometer = false, + .overrideSelectBtn = false, + .fnEnterMode = ssrEnterMode, + .fnExitMode = ssrExitMode, + .fnMainLoop = ssrMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = ssrBackgroundDrawCallback, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +/** + * This function is called when this mode is started. It should initialize + * variables and start the mode. + */ +static void ssrEnterMode(void) +{ + ssr = calloc(1, sizeof(ssrVars_t)); + loadFont("ibm_vga8.font", &ssr->ibm, false); + + size_t sz = 0; + const uint8_t* notes = cnfsGetFile("credits.cch", &sz); + loadTrackData(ssr, notes, sz); + + loadMidiFile("credits.mid", &ssr->credits, false); + + globalMidiPlayerPlaySongCb(&ssr->credits, MIDI_BGM, NULL); + + ssr->tDiv = ssr->credits.timeDivision; + ssr->tempo = globalMidiPlayerGet(MIDI_BGM)->tempo; + printf("%d @ %d\n", ssr->tDiv, ssr->tempo); +} + +/** + * This function is called when the mode is exited. It should free any allocated memory. + */ +static void ssrExitMode(void) +{ + unloadMidiFile(&ssr->credits); + free(ssr->notes); + freeFont(&ssr->ibm); + free(ssr); +} + +/** + * This function is called from the main loop. It's pretty quick, but the + * timing may be inconsistent. + * + * @param elapsedUs The time elapsed since the last time this function was called. Use this value to determine when + * it's time to do things + */ +static void ssrMainLoop(int64_t elapsedUs) +{ + // What a hack! + // ssr->tDiv = ssr->credits.timeDivision; + // ssr->tempo = globalMidiPlayerGet(MIDI_BGM). / 1000; + + + ssr->tElapsedMs += (elapsedUs / 1000); + + // int nextMs = (ssr->notes[ssr->cNote].tick * ssr->tempo) / (ssr->tDiv); + // if (ssr->tElapsedMs >= nextMs) + // { + // ssr->draw = ssr->notes[ssr->cNote].note; + // printf("Note %d (%d, %d)\n", ssr->notes[ssr->cNote].note, nextMs, ssr->tElapsedMs); + // ssr->cNote++; + // } + + midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); + int ticks = SAMPLES_TO_MIDI_TICKS(player->sampleCount, player->tempo, player->reader.division); + int nextTick = ssr->notes[ssr->cNote].tick; + if (ticks >= nextTick) + { + ssr->draw = ssr->notes[ssr->cNote].note; + // printf("Note %d (%d, %d)\n", ssr->notes[ssr->cNote].note, nextMs, ssr->tElapsedMs); + ssr->cNote++; + } + + clearPxTft(); + + paletteColor_t colors[] = {c020, c400, c550, c004, c420}; + // for(int i = 0; i < 5; i++) + // { + // fillDisplayArea(i * (TFT_WIDTH / 5), 0, (i + 1) * (TFT_WIDTH / 5), TFT_HEIGHT, colors[i]); + // } + + fillDisplayArea(ssr->draw * (TFT_WIDTH / 5), 0, (ssr->draw + 1) * (TFT_WIDTH / 5), TFT_HEIGHT, colors[ssr->draw]); + + // Process button events + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + ; // DO SOMETHING + } + + // ssr->credits.tracks + + // Check for analog touch + // int32_t centerVal, intensityVal; + // if (getTouchCentroid(¢erVal, &intensityVal)) + // { + // printf("touch center: %" PRId32 ", intensity: %" PRId32 "\n", centerVal, intensityVal); + // } + // else + // { + // printf("no touch\n"); + // } + + // // Get the acceleration + // int16_t a_x, a_y, a_z; + // accelGetAccelVec(&a_x, &a_y, &a_z); + + // Set LEDs + led_t leds[CONFIG_NUM_LEDS] = {0}; + // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) + // { + // leds[i].r = (255 * ((i + 0) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // leds[i].g = (255 * ((i + 3) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // leds[i].b = (255 * ((i + 6) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // } + setLeds(leds, CONFIG_NUM_LEDS); +} + +/** + * This function is called when the display driver wishes to update a + * section of the display. + * + * @param x the x coordinate that should be updated + * @param y the x coordinate that should be updated + * @param w the width of the rectangle to be updated + * @param h the height of the rectangle to be updated + * @param up update number + * @param numUp update number denominator + */ +static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +{ + // fillDisplayArea(x, y, x + w, y + h, c555); +} + +/** + * @brief Callback for when menu items are selected + * + * @param label The menu item that was selected or moved to + * @param selected true if the item was selected, false if it was moved to + * @param settingVal The value of the setting, if the menu item is a settings item + */ +// static void ssrMenuCb(const char* label, bool selected, uint32_t settingVal) +// { +// printf("%s %s\n", label, selected ? "selected" : "scrolled to"); + +// if (selected) +// { +// } +// } + +/** + * @brief TODO + * + * @param data + * @param size + */ +static void loadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) +{ + uint32_t dIdx = 0; + uint16_t numNotes = (data[dIdx++] << 8); + numNotes |= (data[dIdx++]); + + ssrv->notes = calloc(numNotes, sizeof(ssrNote_t)); + + for (int32_t nIdx = 0; nIdx < numNotes; nIdx++) + { + ssrv->notes[nIdx].tick = (data[dIdx + 0] << 24) | // + (data[dIdx + 1] << 16) | // + (data[dIdx + 2] << 8) | // + (data[dIdx + 3] << 0); + dIdx += 4; + + ssrv->notes[nIdx].note = data[dIdx++]; + + if (0x80 & ssrv->notes[nIdx].note) + { + printf("HOLD!\n"); + ssrv->notes[nIdx].note &= 0x7F; + ssrv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // + (data[dIdx + 1] << 0); + dIdx += 2; + } + + printf("%d %d %d\n", ssrv->notes[nIdx].tick, ssrv->notes[nIdx].note, ssrv->notes[nIdx].hold); + } +} diff --git a/main/modes/games/ssr/mode_ssr.h b/main/modes/games/ssr/mode_ssr.h new file mode 100644 index 000000000..f130b6638 --- /dev/null +++ b/main/modes/games/ssr/mode_ssr.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +extern swadgeMode_t ssrMode; \ No newline at end of file diff --git a/main/modes/system/mainMenu/mainMenu.c b/main/modes/system/mainMenu/mainMenu.c index 83ae4eb0a..1f22cd3c2 100644 --- a/main/modes/system/mainMenu/mainMenu.c +++ b/main/modes/system/mainMenu/mainMenu.c @@ -18,6 +18,7 @@ #include "mode_credits.h" #include "mode_pinball.h" #include "mode_bigbug.h" +#include "mode_ssr.h" #include "mode_synth.h" #include "ultimateTTT.h" #include "touchTest.h" @@ -150,6 +151,7 @@ static void mainMenuEnterMode(void) // Add single items mainMenu->menu = startSubMenu(mainMenu->menu, "Games"); + addSingleItemToMenu(mainMenu->menu, ssrMode.modeName); addSingleItemToMenu(mainMenu->menu, tttMode.modeName); addSingleItemToMenu(mainMenu->menu, pinballMode.modeName); addSingleItemToMenu(mainMenu->menu, bigbugMode.modeName); @@ -366,6 +368,10 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) { switchToSwadgeMode(&tttMode); } + else if (label == ssrMode.modeName) + { + switchToSwadgeMode(&ssrMode); + } else if (label == timerMode.modeName) { switchToSwadgeMode(&timerMode); From 9735b88da328c4b8e807272e01122688371f200a Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Tue, 17 Sep 2024 00:53:09 +0000 Subject: [PATCH 05/54] Get what looks like a rhythm game running --- main/midi/midiPlayer.h | 7 ++ main/modes/games/ssr/mode_ssr.c | 164 +++++++++++++++++++++++--------- 2 files changed, 127 insertions(+), 44 deletions(-) diff --git a/main/midi/midiPlayer.h b/main/midi/midiPlayer.h index 8751d9072..e82b852d8 100644 --- a/main/midi/midiPlayer.h +++ b/main/midi/midiPlayer.h @@ -94,9 +94,16 @@ /// @brief Convert the sample count to MIDI ticks #define SAMPLES_TO_MIDI_TICKS(n, tempo, div) ((n) * 1000000 * (div) / DAC_SAMPLE_RATE_HZ / (tempo)) +/// @brief Convert samples to milliseconds +#define SAMPLES_TO_MS(samp) (((samp) * 1000) / DAC_SAMPLE_RATE_HZ) + /// @brief Calculate the number of DAC samples in the given number of milliseconds #define MS_TO_SAMPLES(ms) ((ms) * DAC_SAMPLE_RATE_HZ / 1000) +/// @brief Convert MIDI ticks to milliseconds +#define MIDI_TICKS_TO_MS(ticks, tempo, div) \ + (int64_t)((int64_t)((int64_t)(ticks) * (int64_t)(tempo)) / (int64_t)((int64_t)1000 * (int64_t)(div))) + /// @brief Callback function used to provide feedback when a song finishes playing typedef void (*songFinishedCbFn)(void); diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index dd9ad9cd7..dda59f3e9 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -2,6 +2,9 @@ #include "menu.h" #include "mode_ssr.h" +#define HIT_BAR 16 +#define ICON_RADIUS 8 + typedef struct { int32_t tick; @@ -11,14 +14,26 @@ typedef struct typedef struct { + int32_t note; + int32_t timer; + int32_t posY; +} ssrNoteIcon_t; + +typedef struct +{ + // Font font_t ibm; + + // Song being played midiFile_t credits; + + // Track data + int32_t numNotes; ssrNote_t* notes; - int32_t tDiv; - int32_t tempo; int32_t cNote; - int32_t tElapsedMs; - int32_t draw; + + // Drawing data + list_t icons; } ssrVars_t; ssrVars_t* ssr; @@ -56,20 +71,19 @@ swadgeMode_t ssrMode = { */ static void ssrEnterMode(void) { + // 60FPS please + setFrameRateUs(16667); + ssr = calloc(1, sizeof(ssrVars_t)); loadFont("ibm_vga8.font", &ssr->ibm, false); - size_t sz = 0; + size_t sz = 0; const uint8_t* notes = cnfsGetFile("credits.cch", &sz); loadTrackData(ssr, notes, sz); loadMidiFile("credits.mid", &ssr->credits, false); globalMidiPlayerPlaySongCb(&ssr->credits, MIDI_BGM, NULL); - - ssr->tDiv = ssr->credits.timeDivision; - ssr->tempo = globalMidiPlayerGet(MIDI_BGM)->tempo; - printf("%d @ %d\n", ssr->tDiv, ssr->tempo); } /** @@ -79,6 +93,11 @@ static void ssrExitMode(void) { unloadMidiFile(&ssr->credits); free(ssr->notes); + void* val; + while ((val = pop(&ssr->icons))) + { + free(val); + } freeFont(&ssr->ibm); free(ssr); } @@ -92,40 +111,102 @@ static void ssrExitMode(void) */ static void ssrMainLoop(int64_t elapsedUs) { - // What a hack! - // ssr->tDiv = ssr->credits.timeDivision; - // ssr->tempo = globalMidiPlayerGet(MIDI_BGM). / 1000; + // This doesn't need to be calculated per-loop... + int32_t travelTimeMs = 2000; + int32_t travelUsPerPx = (1000 * travelTimeMs) / (TFT_HEIGHT - HIT_BAR + (2 * ICON_RADIUS)); + // If there are any notes left + if (ssr->cNote < ssr->numNotes) + { + // Get a reference to the player + midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); - ssr->tElapsedMs += (elapsedUs / 1000); + // Get the position of the song and when the next event is, in ms + int32_t songMs = SAMPLES_TO_MS(player->sampleCount); - // int nextMs = (ssr->notes[ssr->cNote].tick * ssr->tempo) / (ssr->tDiv); - // if (ssr->tElapsedMs >= nextMs) - // { - // ssr->draw = ssr->notes[ssr->cNote].note; - // printf("Note %d (%d, %d)\n", ssr->notes[ssr->cNote].note, nextMs, ssr->tElapsedMs); - // ssr->cNote++; - // } + // Check events until one hasn't happened yet + while (true) + { + // When the next event occurs + int32_t nextEventMs = MIDI_TICKS_TO_MS(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); + + // Check if the icon should be spawned now to reach the hit bar in time + if (songMs + travelTimeMs >= nextEventMs) + { + // Spawn an icon + ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); + ni->note = ssr->notes[ssr->cNote].note; + ni->posY = TFT_HEIGHT + (ICON_RADIUS * 2); + ni->timer = 0; + push(&ssr->icons, ni); + + // Increment the track data + ssr->cNote++; + } + else + { + break; + } + } + } + + // Clear the display + clearPxTft(); - midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); - int ticks = SAMPLES_TO_MIDI_TICKS(player->sampleCount, player->tempo, player->reader.division); - int nextTick = ssr->notes[ssr->cNote].tick; - if (ticks >= nextTick) + // Draw the target area + drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); + for (int32_t i = 0; i < 5; i++) { - ssr->draw = ssr->notes[ssr->cNote].note; - // printf("Note %d (%d, %d)\n", ssr->notes[ssr->cNote].note, nextMs, ssr->tElapsedMs); - ssr->cNote++; + int32_t xOffset = ((i * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); + drawCircle(xOffset, HIT_BAR, ICON_RADIUS + 2, c555); } - clearPxTft(); + // Draw all the icons + node_t* iconNode = ssr->icons.first; + while (iconNode) + { + // Draw the icon + ssrNoteIcon_t* icon = iconNode->val; + int32_t xOffset = ((icon->note * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); + paletteColor_t colors[] = {c020, c400, c550, c004, c420}; + drawCircleFilled(xOffset, icon->posY, ICON_RADIUS, colors[icon->note]); + + // Highlight the target if the timing is good enough + if (HIT_BAR - 4 <= icon->posY && icon->posY <= HIT_BAR + 4) + { + drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[icon->note]); + } - paletteColor_t colors[] = {c020, c400, c550, c004, c420}; - // for(int i = 0; i < 5; i++) - // { - // fillDisplayArea(i * (TFT_WIDTH / 5), 0, (i + 1) * (TFT_WIDTH / 5), TFT_HEIGHT, colors[i]); - // } + // Track if this icon gets removed + bool removed = false; - fillDisplayArea(ssr->draw * (TFT_WIDTH / 5), 0, (ssr->draw + 1) * (TFT_WIDTH / 5), TFT_HEIGHT, colors[ssr->draw]); + // Run this icon's timer + icon->timer += elapsedUs; + while (icon->timer >= travelUsPerPx) + { + icon->timer -= travelUsPerPx; + icon->posY--; + + // If it's off screen + if (icon->posY < -ICON_RADIUS) + { + // Remove this icon + node_t* nodeToRemove = iconNode; + iconNode = iconNode->next; + free(nodeToRemove->val); + removeEntry(&ssr->icons, nodeToRemove); + removed = true; + break; + } + } + + // If this icon wasn't removed + if (!removed) + { + // Iterate to the next + iconNode = iconNode->next; + } + } // Process button events buttonEvt_t evt = {0}; @@ -134,8 +215,6 @@ static void ssrMainLoop(int64_t elapsedUs) ; // DO SOMETHING } - // ssr->credits.tracks - // Check for analog touch // int32_t centerVal, intensityVal; // if (getTouchCentroid(¢erVal, &intensityVal)) @@ -202,13 +281,13 @@ static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h */ static void loadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) { - uint32_t dIdx = 0; - uint16_t numNotes = (data[dIdx++] << 8); - numNotes |= (data[dIdx++]); + uint32_t dIdx = 0; + ssrv->numNotes = (data[dIdx++] << 8); + ssrv->numNotes |= (data[dIdx++]); - ssrv->notes = calloc(numNotes, sizeof(ssrNote_t)); + ssrv->notes = calloc(ssrv->numNotes, sizeof(ssrNote_t)); - for (int32_t nIdx = 0; nIdx < numNotes; nIdx++) + for (int32_t nIdx = 0; nIdx < ssrv->numNotes; nIdx++) { ssrv->notes[nIdx].tick = (data[dIdx + 0] << 24) | // (data[dIdx + 1] << 16) | // @@ -220,13 +299,10 @@ static void loadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) if (0x80 & ssrv->notes[nIdx].note) { - printf("HOLD!\n"); ssrv->notes[nIdx].note &= 0x7F; ssrv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // (data[dIdx + 1] << 0); dIdx += 2; } - - printf("%d %d %d\n", ssrv->notes[nIdx].tick, ssrv->notes[nIdx].note, ssrv->notes[nIdx].hold); } } From 9662c3a85a839d98ffbff2deef4bb832fbbe4049 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Tue, 17 Sep 2024 01:09:37 +0000 Subject: [PATCH 06/54] Fix crash at end of song --- main/modes/games/ssr/mode_ssr.c | 55 ++++++++++++++++----------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index dda59f3e9..bc9828537 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -115,38 +115,35 @@ static void ssrMainLoop(int64_t elapsedUs) int32_t travelTimeMs = 2000; int32_t travelUsPerPx = (1000 * travelTimeMs) / (TFT_HEIGHT - HIT_BAR + (2 * ICON_RADIUS)); - // If there are any notes left - if (ssr->cNote < ssr->numNotes) - { - // Get a reference to the player - midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); + // Get a reference to the player + midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); - // Get the position of the song and when the next event is, in ms - int32_t songMs = SAMPLES_TO_MS(player->sampleCount); + // Get the position of the song and when the next event is, in ms + int32_t songMs = SAMPLES_TO_MS(player->sampleCount); - // Check events until one hasn't happened yet - while (true) - { - // When the next event occurs - int32_t nextEventMs = MIDI_TICKS_TO_MS(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); + // Check events until one hasn't happened yet or the song ends + while (ssr->cNote < ssr->numNotes) + { + // When the next event occurs + int32_t nextEventMs = MIDI_TICKS_TO_MS(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); - // Check if the icon should be spawned now to reach the hit bar in time - if (songMs + travelTimeMs >= nextEventMs) - { - // Spawn an icon - ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); - ni->note = ssr->notes[ssr->cNote].note; - ni->posY = TFT_HEIGHT + (ICON_RADIUS * 2); - ni->timer = 0; - push(&ssr->icons, ni); - - // Increment the track data - ssr->cNote++; - } - else - { - break; - } + // Check if the icon should be spawned now to reach the hit bar in time + if (songMs + travelTimeMs >= nextEventMs) + { + // Spawn an icon + ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); + ni->note = ssr->notes[ssr->cNote].note; + ni->posY = TFT_HEIGHT + (ICON_RADIUS * 2); + ni->timer = 0; + push(&ssr->icons, ni); + + // Increment the track data + ssr->cNote++; + } + else + { + // Nothing more to be spawned right now + break; } } From cad8bc77aeca6ab45f86df5bfd1cdcfd0b45a892 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Wed, 18 Sep 2024 11:10:22 +0000 Subject: [PATCH 07/54] Fix firmware compilation --- main/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b399a88c3..be2853599 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -35,6 +35,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "modes/games/pinball/pinball_physics.c" "modes/games/pinball/pinball_test.c" "modes/games/pinball/pinball_zones.c" + "modes/games/ssr/mode_ssr.c" "modes/games/ultimateTTT/ultimateTTT.c" "modes/games/ultimateTTT/ultimateTTTgame.c" "modes/games/ultimateTTT/ultimateTTThowTo.c" @@ -105,6 +106,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./modes/games" "./modes/games/pinball" "./modes/games/bigbug" + "./modes/games/ssr" "./modes/games/ultimateTTT" "./modes/music" "./modes/music/colorchord" From c233f13aca81b8414963edb9637733f85085a5dd Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Wed, 18 Sep 2024 11:11:02 +0000 Subject: [PATCH 08/54] Add lead-in to SSR Cleanup --- main/midi/midiPlayer.h | 30 ++++++----- main/modes/games/ssr/mode_ssr.c | 93 ++++++++++++++++++++++++++------- 2 files changed, 91 insertions(+), 32 deletions(-) diff --git a/main/midi/midiPlayer.h b/main/midi/midiPlayer.h index e82b852d8..d08b6d033 100644 --- a/main/midi/midiPlayer.h +++ b/main/midi/midiPlayer.h @@ -97,12 +97,14 @@ /// @brief Convert samples to milliseconds #define SAMPLES_TO_MS(samp) (((samp) * 1000) / DAC_SAMPLE_RATE_HZ) +/// @brief Convert samples to microseconds +#define SAMPLES_TO_US(samp) (((samp) * 1000000) / DAC_SAMPLE_RATE_HZ) + /// @brief Calculate the number of DAC samples in the given number of milliseconds #define MS_TO_SAMPLES(ms) ((ms) * DAC_SAMPLE_RATE_HZ / 1000) -/// @brief Convert MIDI ticks to milliseconds -#define MIDI_TICKS_TO_MS(ticks, tempo, div) \ - (int64_t)((int64_t)((int64_t)(ticks) * (int64_t)(tempo)) / (int64_t)((int64_t)1000 * (int64_t)(div))) +/// @brief Convert MIDI ticks to microseconds +#define MIDI_TICKS_TO_US(ticks, tempo, div) (int64_t)((int64_t)((int64_t)(ticks) * (int64_t)(tempo)) / ((int64_t)(div))) /// @brief Callback function used to provide feedback when a song finishes playing typedef void (*songFinishedCbFn)(void); @@ -203,24 +205,24 @@ typedef enum LOW_AGOGO = 68, CABASA = 69, MARACAS = 70, - /// @brief This note supersdes any ::SHORT_WHISTLE or ::LONG_WHISTLE notes playing + /// @brief This note supersedes any ::SHORT_WHISTLE or ::LONG_WHISTLE notes playing SHORT_WHISTLE = 71, - /// @brief This note supersdes any ::SHORT_WHISTLE or ::LONG_WHISTLE notes playing + /// @brief This note supersedes any ::SHORT_WHISTLE or ::LONG_WHISTLE notes playing LONG_WHISTLE = 72, - /// @brief This note supersdes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing + /// @brief This note supersedes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing SHORT_GUIRO = 73, - /// @brief This note supersdes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing + /// @brief This note supersedes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing LONG_GUIRO = 74, CLAVES = 75, HIGH_WOODBLOCK = 76, LOW_WOODBLOCK = 77, - /// @brief This note supersdes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing + /// @brief This note supersedes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing MUTE_CUICA = 78, - /// @brief This note supersdes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing + /// @brief This note supersedes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing OPEN_CUICA = 79, - /// @brief This note supersdes any ::MUTE_TRIANGLE or ::OPEN_TRIANGLE notes playing + /// @brief This note supersedes any ::MUTE_TRIANGLE or ::OPEN_TRIANGLE notes playing MUTE_TRIANGLE = 80, - /// @brief This note supersdes any ::MUTE_TRIANGLE or ::OPEN_TRIANGLE notes playing + /// @brief This note supersedes any ::MUTE_TRIANGLE or ::OPEN_TRIANGLE notes playing OPEN_TRIANGLE = 81, // Roland GS Extensions /*SHAKER = 82, @@ -483,7 +485,7 @@ typedef struct oscillatorShape_t shape; }; - /// @brief The ASDR characterstics of this timbre + /// @brief The ASDR characteristics of this timbre envelope_t envelope; /// @brief Various effects applied to this timbre. May be ignored by percussion timbres @@ -984,13 +986,13 @@ void globalMidiPlayerSetVolume(uint8_t trackType, int32_t volumeSetting); void globalMidiPlayerPauseAll(void); /** - * @brief Resume all songs currently being played by the system-widi MIDI players + * @brief Resume all songs currently being played by the system-wide MIDI players * */ void globalMidiPlayerResumeAll(void); /** - * @brief Stop all songs currently bein gplayed by the system-widi MIDI players, + * @brief Stop all songs currently being played by the system-wide MIDI players, * optionally resetting their state to the beginning of the song * * @param reset if true, the players will be reset to the beginning of the song diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index bc9828537..07ac3233e 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -1,10 +1,23 @@ -#include "limits.h" -#include "menu.h" +//============================================================================== +// Defines +//============================================================================== + #include "mode_ssr.h" +//============================================================================== +// Defines +//============================================================================== + #define HIT_BAR 16 #define ICON_RADIUS 8 +#define TRAVEL_TIME_US 2000000 +#define TRAVEL_US_PER_PX ((TRAVEL_TIME_US) / (TFT_HEIGHT - HIT_BAR + (2 * ICON_RADIUS))) + +//============================================================================== +// Structs +//============================================================================== + typedef struct { int32_t tick; @@ -26,6 +39,7 @@ typedef struct // Song being played midiFile_t credits; + int32_t leadInUs; // Track data int32_t numNotes; @@ -36,16 +50,21 @@ typedef struct list_t icons; } ssrVars_t; -ssrVars_t* ssr; +//============================================================================== +// Function Declarations +//============================================================================== static void ssrEnterMode(void); static void ssrExitMode(void); static void ssrMainLoop(int64_t elapsedUs); static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); -static void loadTrackData(ssrVars_t* ssr, const uint8_t* data, size_t size); - +static void ssrLoadTrackData(ssrVars_t* ssr, const uint8_t* data, size_t size); // static void ssrMenuCb(const char*, bool selected, uint32_t settingVal); +//============================================================================== +// Variables +//============================================================================== + static const char ssrName[] = "Swadge Swadge Rebellion"; swadgeMode_t ssrMode = { @@ -65,6 +84,12 @@ swadgeMode_t ssrMode = { .fnAdvancedUSB = NULL, }; +ssrVars_t* ssr; + +//============================================================================== +// Functions +//============================================================================== + /** * This function is called when this mode is started. It should initialize * variables and start the mode. @@ -74,16 +99,23 @@ static void ssrEnterMode(void) // 60FPS please setFrameRateUs(16667); + // Allocate mode memory ssr = calloc(1, sizeof(ssrVars_t)); + + // Load a font loadFont("ibm_vga8.font", &ssr->ibm, false); - size_t sz = 0; - const uint8_t* notes = cnfsGetFile("credits.cch", &sz); - loadTrackData(ssr, notes, sz); + // Load the track data + size_t sz = 0; + ssrLoadTrackData(ssr, cnfsGetFile("credits.cch", &sz), sz); + // Load the MIDI file loadMidiFile("credits.mid", &ssr->credits, false); + globalMidiPlayerPlaySong(&ssr->credits, MIDI_BGM); + globalMidiPlayerPauseAll(); - globalMidiPlayerPlaySongCb(&ssr->credits, MIDI_BGM, NULL); + // Set the lead-in timer + ssr->leadInUs = TRAVEL_TIME_US; } /** @@ -91,14 +123,23 @@ static void ssrEnterMode(void) */ static void ssrExitMode(void) { + // Free MIDI data unloadMidiFile(&ssr->credits); + + // Free track data free(ssr->notes); + + // Free UI data void* val; while ((val = pop(&ssr->icons))) { free(val); } + + // Free the font freeFont(&ssr->ibm); + + // Free mode memory free(ssr); } @@ -111,24 +152,40 @@ static void ssrExitMode(void) */ static void ssrMainLoop(int64_t elapsedUs) { - // This doesn't need to be calculated per-loop... - int32_t travelTimeMs = 2000; - int32_t travelUsPerPx = (1000 * travelTimeMs) / (TFT_HEIGHT - HIT_BAR + (2 * ICON_RADIUS)); + // Run a lead-in timer to allow notes to spawn before the song starts playing + if (ssr->leadInUs > 0) + { + ssr->leadInUs -= elapsedUs; + + if (ssr->leadInUs <= 0) + { + globalMidiPlayerResumeAll(); + ssr->leadInUs = 0; + } + } // Get a reference to the player midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); // Get the position of the song and when the next event is, in ms - int32_t songMs = SAMPLES_TO_MS(player->sampleCount); + int32_t songUs; + if (ssr->leadInUs > 0) + { + songUs = -ssr->leadInUs; + } + else + { + songUs = SAMPLES_TO_US(player->sampleCount); + } // Check events until one hasn't happened yet or the song ends while (ssr->cNote < ssr->numNotes) { // When the next event occurs - int32_t nextEventMs = MIDI_TICKS_TO_MS(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); + int32_t nextEventUs = MIDI_TICKS_TO_US(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); // Check if the icon should be spawned now to reach the hit bar in time - if (songMs + travelTimeMs >= nextEventMs) + if (songUs + TRAVEL_TIME_US >= nextEventUs) { // Spawn an icon ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); @@ -179,9 +236,9 @@ static void ssrMainLoop(int64_t elapsedUs) // Run this icon's timer icon->timer += elapsedUs; - while (icon->timer >= travelUsPerPx) + while (icon->timer >= TRAVEL_US_PER_PX) { - icon->timer -= travelUsPerPx; + icon->timer -= TRAVEL_US_PER_PX; icon->posY--; // If it's off screen @@ -276,7 +333,7 @@ static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h * @param data * @param size */ -static void loadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) +static void ssrLoadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) { uint32_t dIdx = 0; ssrv->numNotes = (data[dIdx++] << 8); From 7bc22100dc7113c5ce01645c6067762975d91b30 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Wed, 18 Sep 2024 11:27:46 +0000 Subject: [PATCH 09/54] Start button input --- main/modes/games/ssr/mode_ssr.c | 90 +++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index 07ac3233e..e9c7aa4d2 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -48,6 +48,7 @@ typedef struct // Drawing data list_t icons; + buttonBit_t btnState; } ssrVars_t; //============================================================================== @@ -86,6 +87,35 @@ swadgeMode_t ssrMode = { ssrVars_t* ssr; +static const paletteColor_t colors[] = {c020, c400, c550, c004, c420}; + +static const int32_t btnMap[][2] = { + { + PB_LEFT, + 0, + }, + { + PB_DOWN, + 1, + }, + { + PB_UP, + 2, + }, + { + PB_RIGHT, + 3, + }, + { + PB_B, + 4, + }, + { + PB_A, + 5, + }, +}; + //============================================================================== // Functions //============================================================================== @@ -152,6 +182,39 @@ static void ssrExitMode(void) */ static void ssrMainLoop(int64_t elapsedUs) { + // Process button events + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + ssr->btnState = evt.state; + for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(btnMap); bIdx++) + { + if (evt.button & btnMap[bIdx][0]) + { + printf("%d %s\n", btnMap[bIdx][1], evt.down ? "Down" : "Up"); + } + } + + // if (evt.down) + // { + // switch (evt.button) + // { + // case PB_UP: + // case PB_DOWN: + // case PB_LEFT: + // case PB_RIGHT: + // case PB_A: + // case PB_B: + // case PB_START: + // case PB_SELECT: + // default: + // { + // break; + // } + // } + // } + } + // Run a lead-in timer to allow notes to spawn before the song starts playing if (ssr->leadInUs > 0) { @@ -220,16 +283,16 @@ static void ssrMainLoop(int64_t elapsedUs) while (iconNode) { // Draw the icon - ssrNoteIcon_t* icon = iconNode->val; - int32_t xOffset = ((icon->note * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); - paletteColor_t colors[] = {c020, c400, c550, c004, c420}; + ssrNoteIcon_t* icon = iconNode->val; + int32_t xOffset = ((icon->note * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); drawCircleFilled(xOffset, icon->posY, ICON_RADIUS, colors[icon->note]); + // TODO figure out how to draw holds // Highlight the target if the timing is good enough - if (HIT_BAR - 4 <= icon->posY && icon->posY <= HIT_BAR + 4) - { - drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[icon->note]); - } + // if (HIT_BAR - 4 <= icon->posY && icon->posY <= HIT_BAR + 4) + // { + // drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[icon->note]); + // } // Track if this icon gets removed bool removed = false; @@ -262,11 +325,14 @@ static void ssrMainLoop(int64_t elapsedUs) } } - // Process button events - buttonEvt_t evt = {0}; - while (checkButtonQueueWrapper(&evt)) + // Draw indicators that the button is pressed + for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(btnMap); bIdx++) { - ; // DO SOMETHING + if (ssr->btnState & btnMap[bIdx][0]) + { + int32_t xOffset = ((bIdx * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); + drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[bIdx]); + } } // Check for analog touch @@ -354,6 +420,8 @@ static void ssrLoadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) if (0x80 & ssrv->notes[nIdx].note) { ssrv->notes[nIdx].note &= 0x7F; + + // TODO figure out how to draw holds ssrv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // (data[dIdx + 1] << 0); dIdx += 2; From 8e2e0f3268d75b850792593b8610e5a075960702 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Wed, 18 Sep 2024 22:53:58 +0000 Subject: [PATCH 10/54] Draw hold notes --- main/modes/games/ssr/mode_ssr.c | 48 +++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index e9c7aa4d2..bcb39497e 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -29,7 +29,8 @@ typedef struct { int32_t note; int32_t timer; - int32_t posY; + int32_t headPosY; + int32_t tailPosY; } ssrNoteIcon_t; typedef struct @@ -253,8 +254,28 @@ static void ssrMainLoop(int64_t elapsedUs) // Spawn an icon ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); ni->note = ssr->notes[ssr->cNote].note; - ni->posY = TFT_HEIGHT + (ICON_RADIUS * 2); - ni->timer = 0; + ni->headPosY = TFT_HEIGHT + (ICON_RADIUS * 2); + + // If this is a hold note + if (ssr->notes[ssr->cNote].hold) + { + // Figure out at what microsecond the tail ends + int32_t tailUs = MIDI_TICKS_TO_US(ssr->notes[ssr->cNote].hold, player->tempo, player->reader.division); + // Convert the time to a number of pixels + int32_t tailPx = tailUs / TRAVEL_US_PER_PX; + // Add the length pixels to the head to get the tail + ni->tailPosY = ni->headPosY + tailPx; + } + else + { + // No tail + ni->tailPosY = -1; + } + + // Start the timer at zero + ni->timer = 0; + + // Push into the list of icons push(&ssr->icons, ni); // Increment the track data @@ -285,8 +306,13 @@ static void ssrMainLoop(int64_t elapsedUs) // Draw the icon ssrNoteIcon_t* icon = iconNode->val; int32_t xOffset = ((icon->note * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); - drawCircleFilled(xOffset, icon->posY, ICON_RADIUS, colors[icon->note]); - // TODO figure out how to draw holds + drawCircleFilled(xOffset, icon->headPosY, ICON_RADIUS, colors[icon->note]); + // If there is a tail + if (icon->tailPosY >= 0) + { + // Draw the tail + fillDisplayArea(xOffset - 2, icon->headPosY, xOffset + 3, icon->tailPosY, colors[icon->note]); + } // Highlight the target if the timing is good enough // if (HIT_BAR - 4 <= icon->posY && icon->posY <= HIT_BAR + 4) @@ -302,10 +328,16 @@ static void ssrMainLoop(int64_t elapsedUs) while (icon->timer >= TRAVEL_US_PER_PX) { icon->timer -= TRAVEL_US_PER_PX; - icon->posY--; + + // Move the whole icon up + icon->headPosY--; + if (icon->tailPosY >= 0) + { + icon->tailPosY--; + } // If it's off screen - if (icon->posY < -ICON_RADIUS) + if (icon->headPosY < -ICON_RADIUS && (icon->tailPosY < 0)) { // Remove this icon node_t* nodeToRemove = iconNode; @@ -421,7 +453,7 @@ static void ssrLoadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) { ssrv->notes[nIdx].note &= 0x7F; - // TODO figure out how to draw holds + // Use the hold time to see when this note ends ssrv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // (data[dIdx + 1] << 0); dIdx += 2; From d94802f3e5237ed718de649698b9facce4c3844b Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Thu, 19 Sep 2024 00:23:32 +0000 Subject: [PATCH 11/54] Add note hits and tail holds --- main/modes/games/ssr/mode_ssr.c | 368 ++++++++++++++++++++++---------- 1 file changed, 250 insertions(+), 118 deletions(-) diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index bcb39497e..50c995b80 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -31,6 +31,7 @@ typedef struct int32_t timer; int32_t headPosY; int32_t tailPosY; + bool held; } ssrNoteIcon_t; typedef struct @@ -62,6 +63,9 @@ static void ssrMainLoop(int64_t elapsedUs); static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); static void ssrLoadTrackData(ssrVars_t* ssr, const uint8_t* data, size_t size); // static void ssrMenuCb(const char*, bool selected, uint32_t settingVal); +static uint32_t btnToNote(buttonBit_t btn); +static void ssrRunTimers(ssrVars_t* ssrv, uint32_t elapsedUs); +static void ssrDrawGame(ssrVars_t* ssrv); //============================================================================== // Variables @@ -88,34 +92,8 @@ swadgeMode_t ssrMode = { ssrVars_t* ssr; -static const paletteColor_t colors[] = {c020, c400, c550, c004, c420}; - -static const int32_t btnMap[][2] = { - { - PB_LEFT, - 0, - }, - { - PB_DOWN, - 1, - }, - { - PB_UP, - 2, - }, - { - PB_RIGHT, - 3, - }, - { - PB_B, - 4, - }, - { - PB_A, - 5, - }, -}; +static const paletteColor_t colors[] = {c020, c400, c550, c004, c420, c222}; +static const buttonBit_t noteToBtn[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; //============================================================================== // Functions @@ -188,32 +166,87 @@ static void ssrMainLoop(int64_t elapsedUs) while (checkButtonQueueWrapper(&evt)) { ssr->btnState = evt.state; - for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(btnMap); bIdx++) + + if (evt.down) { - if (evt.button & btnMap[bIdx][0]) + // Iterate through all currently shown icons + node_t* iconNode = ssr->icons.first; + while (iconNode) { - printf("%d %s\n", btnMap[bIdx][1], evt.down ? "Down" : "Up"); + ssrNoteIcon_t* icon = iconNode->val; + + // If the icon matches the button + if (icon->note == btnToNote(evt.button)) + { + // Find how off the timing is + int32_t pxOff = ABS(HIT_BAR - icon->headPosY); + int32_t usOff = pxOff * TRAVEL_US_PER_PX; + printf("%" PRId32 " us off\n", usOff); + + // Check if this button hit a note + bool iconHit = false; + + // Classify the time off + if (usOff < 21500) + { + printf(" Fantastic\n"); + iconHit = true; + } + else if (usOff < 43000) + { + printf(" Marvelous\n"); + iconHit = true; + } + else if (usOff < 102000) + { + printf(" Great\n"); + iconHit = true; + } + else if (usOff < 135000) + { + printf(" Decent\n"); + iconHit = true; + } + else if (usOff < 180000) + { + printf(" Way Off\n"); + iconHit = true; + } + else + { + printf(" MISS\n"); + } + + // If it was close enough to hit + if (iconHit) + { + if (icon->tailPosY >= 0) + { + // There is a tail, don't remove the note yet + icon->headPosY = HIT_BAR; + icon->held = true; + } + else + { + // No tail, remove the icon + node_t* nextNode = iconNode->next; + removeEntry(&ssr->icons, iconNode); + iconNode = nextNode; + } + } + + // the button was matched to an icon, break the loop + break; + } + + // Iterate to the next icon + iconNode = iconNode->next; } } - - // if (evt.down) - // { - // switch (evt.button) - // { - // case PB_UP: - // case PB_DOWN: - // case PB_LEFT: - // case PB_RIGHT: - // case PB_A: - // case PB_B: - // case PB_START: - // case PB_SELECT: - // default: - // { - // break; - // } - // } - // } + else + { + // TODO handle ups when holding tails + } } // Run a lead-in timer to allow notes to spawn before the song starts playing @@ -288,40 +321,52 @@ static void ssrMainLoop(int64_t elapsedUs) } } - // Clear the display - clearPxTft(); + ssrRunTimers(ssr, elapsedUs); + ssrDrawGame(ssr); - // Draw the target area - drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); - for (int32_t i = 0; i < 5; i++) - { - int32_t xOffset = ((i * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); - drawCircle(xOffset, HIT_BAR, ICON_RADIUS + 2, c555); - } + // Check for analog touch + // int32_t centerVal, intensityVal; + // if (getTouchCentroid(¢erVal, &intensityVal)) + // { + // printf("touch center: %" PRId32 ", intensity: %" PRId32 "\n", centerVal, intensityVal); + // } + // else + // { + // printf("no touch\n"); + // } - // Draw all the icons - node_t* iconNode = ssr->icons.first; + // // Get the acceleration + // int16_t a_x, a_y, a_z; + // accelGetAccelVec(&a_x, &a_y, &a_z); + + // Set LEDs + led_t leds[CONFIG_NUM_LEDS] = {0}; + // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) + // { + // leds[i].r = (255 * ((i + 0) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // leds[i].g = (255 * ((i + 3) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // leds[i].b = (255 * ((i + 6) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // } + setLeds(leds, CONFIG_NUM_LEDS); +} + +/** + * @brief TODO + * + * @param ssrv + * @param elapsedUs + */ +static void ssrRunTimers(ssrVars_t* ssrv, uint32_t elapsedUs) +{ + // Track if an icon was removed + bool removed = false; + + // Run all the icon timers + node_t* iconNode = ssrv->icons.first; while (iconNode) { - // Draw the icon + // Get a reference ssrNoteIcon_t* icon = iconNode->val; - int32_t xOffset = ((icon->note * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); - drawCircleFilled(xOffset, icon->headPosY, ICON_RADIUS, colors[icon->note]); - // If there is a tail - if (icon->tailPosY >= 0) - { - // Draw the tail - fillDisplayArea(xOffset - 2, icon->headPosY, xOffset + 3, icon->tailPosY, colors[icon->note]); - } - - // Highlight the target if the timing is good enough - // if (HIT_BAR - 4 <= icon->posY && icon->posY <= HIT_BAR + 4) - // { - // drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[icon->note]); - // } - - // Track if this icon gets removed - bool removed = false; // Run this icon's timer icon->timer += elapsedUs; @@ -329,68 +374,114 @@ static void ssrMainLoop(int64_t elapsedUs) { icon->timer -= TRAVEL_US_PER_PX; + bool shouldRemove = false; + // Move the whole icon up - icon->headPosY--; - if (icon->tailPosY >= 0) + if (!icon->held) + { + icon->headPosY--; + if (icon->tailPosY >= 0) + { + icon->tailPosY--; + } + + // If it's off screen + if (icon->headPosY < -ICON_RADIUS && (icon->tailPosY < 0)) + { + // Mark it for removal + shouldRemove = true; + } + } + else // The icon is being held { - icon->tailPosY--; + // Only move the tail position + if (icon->tailPosY >= HIT_BAR) + { + icon->tailPosY--; + } + + // If the tail finished + if (icon->tailPosY < HIT_BAR) + { + // Mark it for removal + shouldRemove = true; + } } - // If it's off screen - if (icon->headPosY < -ICON_RADIUS && (icon->tailPosY < 0)) + // If the icon should be removed + if (shouldRemove) { // Remove this icon - node_t* nodeToRemove = iconNode; - iconNode = iconNode->next; - free(nodeToRemove->val); - removeEntry(&ssr->icons, nodeToRemove); + free(iconNode->val); + removeEntry(&ssrv->icons, iconNode); + + // Stop the while timer loop removed = true; break; } } - // If this icon wasn't removed - if (!removed) + // If an icon was removed + if (removed) + { + // Stop iterating through notes + break; + } + else { // Iterate to the next iconNode = iconNode->next; } } +} + +/** + * @brief TODO + * + * @param ssrv + */ +static void ssrDrawGame(ssrVars_t* ssrv) +{ + // Clear the display + clearPxTft(); + + // Draw the target area + drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); + for (int32_t i = 0; i < ARRAY_SIZE(noteToBtn); i++) + { + int32_t xOffset = ((i * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); + drawCircle(xOffset, HIT_BAR, ICON_RADIUS + 2, c555); + } + + // Draw all the icons + node_t* iconNode = ssrv->icons.first; + while (iconNode) + { + // Draw the icon + ssrNoteIcon_t* icon = iconNode->val; + int32_t xOffset = ((icon->note * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); + drawCircleFilled(xOffset, icon->headPosY, ICON_RADIUS, colors[icon->note]); + + // If there is a tail + if (icon->tailPosY >= 0) + { + // Draw the tail + fillDisplayArea(xOffset - 2, icon->headPosY, xOffset + 3, icon->tailPosY, colors[icon->note]); + } + + // Iterate + iconNode = iconNode->next; + } // Draw indicators that the button is pressed - for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(btnMap); bIdx++) + for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(noteToBtn); bIdx++) { - if (ssr->btnState & btnMap[bIdx][0]) + if (ssrv->btnState & noteToBtn[bIdx]) { - int32_t xOffset = ((bIdx * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); + int32_t xOffset = ((bIdx * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[bIdx]); } } - - // Check for analog touch - // int32_t centerVal, intensityVal; - // if (getTouchCentroid(¢erVal, &intensityVal)) - // { - // printf("touch center: %" PRId32 ", intensity: %" PRId32 "\n", centerVal, intensityVal); - // } - // else - // { - // printf("no touch\n"); - // } - - // // Get the acceleration - // int16_t a_x, a_y, a_z; - // accelGetAccelVec(&a_x, &a_y, &a_z); - - // Set LEDs - led_t leds[CONFIG_NUM_LEDS] = {0}; - // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) - // { - // leds[i].r = (255 * ((i + 0) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // leds[i].g = (255 * ((i + 3) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // leds[i].b = (255 * ((i + 6) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // } - setLeds(leds, CONFIG_NUM_LEDS); } /** @@ -460,3 +551,44 @@ static void ssrLoadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) } } } + +/** + * @brief TODO + * + * @param btn + * @return uint32_t + */ +static uint32_t btnToNote(buttonBit_t btn) +{ + switch (btn) + { + case PB_LEFT: + { + return 0; + } + case PB_DOWN: + { + return 1; + } + case PB_UP: + { + return 2; + } + case PB_RIGHT: + { + return 3; + } + case PB_B: + { + return 4; + } + case PB_A: + { + return 5; + } + default: + { + return -1; + } + } +} From 88df763943ed9035ea22236b5cb0f837f4adb2f6 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 16 Sep 2024 00:27:15 +0000 Subject: [PATCH 12/54] Add chart file processing --- tools/assets_preprocessor/README.md | 6 ++ tools/assets_preprocessor/makefile | 2 +- .../src/assets_preprocessor.c | 21 ++-- .../assets_preprocessor/src/chart_processor.c | 101 ++++++++++++++++++ .../assets_preprocessor/src/chart_processor.h | 6 ++ .../assets_preprocessor/src/heatshrink_util.c | 3 +- .../assets_preprocessor/src/image_processor.c | 4 +- tools/assets_preprocessor/src/raw_processor.c | 9 +- 8 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 tools/assets_preprocessor/src/chart_processor.c create mode 100644 tools/assets_preprocessor/src/chart_processor.h diff --git a/tools/assets_preprocessor/README.md b/tools/assets_preprocessor/README.md index 7cda7eed5..c505952db 100644 --- a/tools/assets_preprocessor/README.md +++ b/tools/assets_preprocessor/README.md @@ -47,3 +47,9 @@ No processing is done on `.txt` files. They are copied from the input directory ### `.mid`, `.midi` The notes on the first track from the MIDI file are converted to buzzer format (frequency, duration), and compressed with Heatshrink compression. The buzzer is single channel, single voice, so if a note starts while another is playing, the first note is stopped. + +### `.chart` + +Generate Clone Hero charts [using this tool](https://efhiii.github.io/midi-ch/), see https://github.com/EFHIII/midi-ch. + +[.chart file format spec](https://github.com/TheNathannator/GuitarGame_ChartFormats/blob/main/doc/FileFormats/.chart/Core%20Infrastructure.md). diff --git a/tools/assets_preprocessor/makefile b/tools/assets_preprocessor/makefile index 34954201d..52963e3b0 100644 --- a/tools/assets_preprocessor/makefile +++ b/tools/assets_preprocessor/makefile @@ -154,7 +154,7 @@ clean: -@rm -f $(OBJECTS) $(EXECUTABLE) format: - clang-format -i -style=file $(SOURCES_TO_FORMAT) + clang-format-17 -i -style=file $(SOURCES_TO_FORMAT) ################################################################################ # Makefile Debugging diff --git a/tools/assets_preprocessor/src/assets_preprocessor.c b/tools/assets_preprocessor/src/assets_preprocessor.c index 6d9c357a9..311d7212a 100644 --- a/tools/assets_preprocessor/src/assets_preprocessor.c +++ b/tools/assets_preprocessor/src/assets_preprocessor.c @@ -10,6 +10,7 @@ #include #include +#include "chart_processor.h" #include "image_processor.h" #include "font_processor.h" #include "json_processor.h" @@ -23,9 +24,9 @@ * */ static const char* rawFileTypes[][2] = { - { "mid", "mid"}, - { "midi", "mid"}, - { "raw", "raw"}, + {"mid", "mid"}, + {"midi", "mid"}, + {"raw", "raw"}, }; const char* outDirName = NULL; @@ -81,6 +82,10 @@ static int processFile(const char* fpath, const struct stat* st __attribute__((u { process_image(fpath, outDirName); } + else if (endsWith(fpath, ".chart")) + { + process_chart(fpath, outDirName); + } else if (endsWith(fpath, ".json")) { process_json(fpath, outDirName); @@ -176,13 +181,11 @@ int main(int argc, char** argv) struct stat st = {0}; if (stat(outDirName, &st) == -1) { -#if defined(WINDOWS) || defined(__WINDOWS__) || defined(_WINDOWS) \ - || defined(WIN32) || defined(WIN64) \ - || defined(_WIN32) || defined(_WIN64) \ - || defined(__WIN32__) || defined(__TOS_WIN__) \ - || defined(_MSC_VER) +#if defined(WINDOWS) || defined(__WINDOWS__) || defined(_WINDOWS) || defined(WIN32) || defined(WIN64) \ + || defined(_WIN32) || defined(_WIN64) || defined(__WIN32__) || defined(__TOS_WIN__) || defined(_MSC_VER) mkdir(outDirName); -#elif defined(__linux) || defined(__linux__) || defined(linux) || defined(__LINUX__) || defined(__CYGWIN__) || defined(__APPLE__) +#elif defined(__linux) || defined(__linux__) || defined(linux) || defined(__LINUX__) || defined(__CYGWIN__) \ + || defined(__APPLE__) mkdir(outDirName, 0777); #endif } diff --git a/tools/assets_preprocessor/src/chart_processor.c b/tools/assets_preprocessor/src/chart_processor.c new file mode 100644 index 000000000..f226e9270 --- /dev/null +++ b/tools/assets_preprocessor/src/chart_processor.c @@ -0,0 +1,101 @@ +#include +#include +#include +#include + +#include "chart_processor.h" +#include "fileUtils.h" + +typedef enum +{ + CS_NONE, + CS_SONG, + CS_SYNC_TRACK, + CS_NOTES, +} charSection_t; + +void process_chart(const char* infile, const char* outdir) +{ + /* Determine if the output file already exists */ + char outFilePath[128] = {0}; + strcat(outFilePath, outdir); + strcat(outFilePath, "/"); + strcat(outFilePath, get_filename(infile)); + + char* dotptr = strrchr(outFilePath, '.'); + snprintf(&dotptr[1], strlen(dotptr), "cch"); + + if (doesFileExist(outFilePath)) + { + // printf("Output for %s already exists\n", infile); + // return; + } + + charSection_t section = CS_NONE; + bool inSection = false; + + /* Read input file */ + FILE* fp = fopen(infile, "rb"); + + if (fp) + { + FILE* outFile = fopen(outFilePath, "wb"); + + if (outFile) + { + char line[512] = {0}; + char sectionName[512] = {0}; + while (NULL != fgets(line, sizeof(line), fp)) + { + int tick, note, length; + + if (sscanf(line, "[%s]", sectionName)) + { + sectionName[strlen(sectionName) - 1] = 0; + + if (strstr(sectionName, "Song")) + { + section = CS_SONG; + } + else if (strstr(sectionName, "SyncTrack")) + { + section = CS_SYNC_TRACK; + } + else + { + section = CS_NOTES; + } + } + else if (strstr(line, "{")) + { + inSection = true; + } + else if (strstr(line, "}")) + { + inSection = false; + } + else if (inSection && (CS_NOTES == section) && // + sscanf(line, "%d = N %d %d", &tick, ¬e, &length)) + { + fputc((tick >> 24) & 0xFF, outFile); + fputc((tick >> 16) & 0xFF, outFile); + fputc((tick >> 8) & 0xFF, outFile); + fputc((tick >> 0) & 0xFF, outFile); + + if (length) + { + fputc(note & 0x80, outFile); + fputc((length >> 8) & 0xFF, outFile); + fputc((length >> 0) & 0xFF, outFile); + } + else + { + fputc(note, outFile); + } + } + } + fclose(outFile); + } + fclose(fp); + } +} diff --git a/tools/assets_preprocessor/src/chart_processor.h b/tools/assets_preprocessor/src/chart_processor.h new file mode 100644 index 000000000..0508149d0 --- /dev/null +++ b/tools/assets_preprocessor/src/chart_processor.h @@ -0,0 +1,6 @@ +#ifndef _CHART_PROCESSOR_H_ +#define _CHART_PROCESSOR_H_ + +void process_chart(const char* infile, const char* outdir); + +#endif \ No newline at end of file diff --git a/tools/assets_preprocessor/src/heatshrink_util.c b/tools/assets_preprocessor/src/heatshrink_util.c index 616671e86..d8531d6bf 100644 --- a/tools/assets_preprocessor/src/heatshrink_util.c +++ b/tools/assets_preprocessor/src/heatshrink_util.c @@ -127,7 +127,8 @@ void writeHeatshrinkFile(uint8_t* input, uint32_t len, const char* outFilePath) /* Write a compressed file */ shrunkFile = fopen(outFilePath, "wb"); - if (shrunkFile == NULL) { + if (shrunkFile == NULL) + { perror("Error occurred while writing file.\n"); } /* First four bytes are decompresed size */ diff --git a/tools/assets_preprocessor/src/image_processor.c b/tools/assets_preprocessor/src/image_processor.c index 76c32deaa..203c46427 100644 --- a/tools/assets_preprocessor/src/image_processor.c +++ b/tools/assets_preprocessor/src/image_processor.c @@ -10,8 +10,8 @@ #pragma GCC diagnostic push #endif #ifdef __GNUC__ -#pragma GCC diagnostic ignored "-Wcast-qual" -#pragma GCC diagnostic ignored "-Wmissing-prototypes" + #pragma GCC diagnostic ignored "-Wcast-qual" + #pragma GCC diagnostic ignored "-Wmissing-prototypes" #endif #define STB_IMAGE_IMPLEMENTATION diff --git a/tools/assets_preprocessor/src/raw_processor.c b/tools/assets_preprocessor/src/raw_processor.c index 366d83959..e954a6986 100644 --- a/tools/assets_preprocessor/src/raw_processor.c +++ b/tools/assets_preprocessor/src/raw_processor.c @@ -28,8 +28,8 @@ void process_raw(const char* inFile, const char* outDir, const char* outExt) // Read input file const char* errdesc = NULL; - errno = 0; - FILE* fp = fopen(inFile, "rb"); + errno = 0; + FILE* fp = fopen(inFile, "rb"); if (!fp) { errdesc = strerror(errno); @@ -48,12 +48,13 @@ void process_raw(const char* inFile, const char* outDir, const char* outExt) return; } - errno = 0; + errno = 0; int readlen = fread(byteString, 1, sz, fp); if (readlen < sz) { errdesc = (errno == 0) ? "Read too small" : strerror(errno); - fprintf(stderr, "ERR: raw_processor.c: Failed to read file %s: %d - %s\n", inFile, readlen, errdesc ? errdesc : "Unknown"); + fprintf(stderr, "ERR: raw_processor.c: Failed to read file %s: %d - %s\n", inFile, readlen, + errdesc ? errdesc : "Unknown"); free(byteString); fclose(fp); From 18a6092767c69b45a5e053bc90255a1006615cd9 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 16 Sep 2024 00:28:08 +0000 Subject: [PATCH 13/54] Update submodules and make a target to do so automatically --- emulator/src-lib/rawdraw | 2 +- makefile | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/emulator/src-lib/rawdraw b/emulator/src-lib/rawdraw index a53ea30df..2dd1b18b0 160000 --- a/emulator/src-lib/rawdraw +++ b/emulator/src-lib/rawdraw @@ -1 +1 @@ -Subproject commit a53ea30df86f5876197873975efee3dbc983a942 +Subproject commit 2dd1b18b0d7cc69c34bd7c47b7fa9567e4ae01ba diff --git a/makefile b/makefile index bede280b0..21b136095 100644 --- a/makefile +++ b/makefile @@ -71,6 +71,8 @@ SOURCES := $(filter-out main/utils/cnfs.c, $(SOURCES)) # The emulator doesn't build components, but there is a target for formatting them ALL_FILES = $(shell $(FIND) components $(SRC_DIRS_RECURSIVE) -iname "*.[c|h]") +SUBMODULES = $(shell git config --file .gitmodules --name-only --get-regexp path | sed -nr 's/submodule.(.*).path/\1/p') + ################################################################################ # Includes ################################################################################ @@ -312,7 +314,7 @@ EXECUTABLE = swadge_emulator ################################################################################ # This list of targets do not build files which match their name -.PHONY: all assets bundle clean docs format cppcheck firmware clean-firmware $(CNFS_FILE) print-% +.PHONY: all assets bundle clean docs format cppcheck firmware clean-firmware $(CNFS_FILE) update-submodules print-% # Build the executable all: $(EXECUTABLE) @@ -478,5 +480,12 @@ gen-coverage: genhtml ./coverage.info --output-directory ./coverage firefox ./coverage/index.html & +update-submodules: + for submodule in $(SUBMODULES) ; do \ + echo Updating $$submodule to latest ; \ + git -C $$submodule fetch --prune ; \ + git -C $$submodule checkout origin/HEAD ; \ + done + # Print any value from this makefile print-% : ; @echo $* = $($*) From be8391b8d07c7164df3e4efef3537b46145f9034 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 16 Sep 2024 02:25:17 +0000 Subject: [PATCH 14/54] Write number of notes in compressed chart Fix hold bit --- .../assets_preprocessor/src/chart_processor.c | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/tools/assets_preprocessor/src/chart_processor.c b/tools/assets_preprocessor/src/chart_processor.c index f226e9270..ca26b8bfa 100644 --- a/tools/assets_preprocessor/src/chart_processor.c +++ b/tools/assets_preprocessor/src/chart_processor.c @@ -43,6 +43,10 @@ void process_chart(const char* infile, const char* outdir) if (outFile) { + uint8_t* tmpSpace = malloc(1024 * 1024); + uint32_t tmpIdx = 0; + uint32_t noteCount = 0; + char line[512] = {0}; char sectionName[512] = {0}; while (NULL != fgets(line, sizeof(line), fp)) @@ -72,28 +76,40 @@ void process_chart(const char* infile, const char* outdir) } else if (strstr(line, "}")) { + if(CS_NOTES == section) + { + fputc((noteCount >> 8) & 0xFF, outFile); + fputc((noteCount >> 0) & 0xFF, outFile); + noteCount = 0; + fwrite(tmpSpace, tmpIdx, 1, outFile); + tmpIdx = 0; + } inSection = false; } else if (inSection && (CS_NOTES == section) && // sscanf(line, "%d = N %d %d", &tick, ¬e, &length)) { - fputc((tick >> 24) & 0xFF, outFile); - fputc((tick >> 16) & 0xFF, outFile); - fputc((tick >> 8) & 0xFF, outFile); - fputc((tick >> 0) & 0xFF, outFile); + noteCount++; + + tmpSpace[tmpIdx++] = (tick >> 24) & 0xFF; + tmpSpace[tmpIdx++] = (tick >> 16) & 0xFF; + tmpSpace[tmpIdx++] = (tick >> 8) & 0xFF; + tmpSpace[tmpIdx++] = (tick >> 0) & 0xFF; if (length) { - fputc(note & 0x80, outFile); - fputc((length >> 8) & 0xFF, outFile); - fputc((length >> 0) & 0xFF, outFile); + tmpSpace[tmpIdx++] = note | 0x80; + + tmpSpace[tmpIdx++] = (length >> 8) & 0xFF; + tmpSpace[tmpIdx++] = (length >> 0) & 0xFF; } else { - fputc(note, outFile); + tmpSpace[tmpIdx++] = note; } } } + free(tmpSpace); fclose(outFile); } fclose(fp); From 036e51a666dd8a1d289fcd7cac2db6d145e24732 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 16 Sep 2024 02:25:29 +0000 Subject: [PATCH 15/54] Add basic SSR mode that barely works --- assets/credits.chart | 302 ++++++++++++++++++++++++++ main/modes/games/ssr/mode_ssr.c | 232 ++++++++++++++++++++ main/modes/games/ssr/mode_ssr.h | 5 + main/modes/system/mainMenu/mainMenu.c | 6 + 4 files changed, 545 insertions(+) create mode 100644 assets/credits.chart create mode 100644 main/modes/games/ssr/mode_ssr.c create mode 100644 main/modes/games/ssr/mode_ssr.h diff --git a/assets/credits.chart b/assets/credits.chart new file mode 100644 index 000000000..17471ecb9 --- /dev/null +++ b/assets/credits.chart @@ -0,0 +1,302 @@ +[Song] +{ + Name = " "SWADGE Credits New FINAL Sync3"" + Charter = "Edward Haas" + Year = "Unknown" + Offset = 0 + Resolution = 960 + Player2 = bass + Difficulty = 4 + PreviewStart = 0 + PreviewEnd = 10000 + Genre = "rock" + MediaType = "cd" + MusicStream = "song.ogg" +} +[SyncTrack] +{ + 0 = TS 4 + 0 = B 120000 + 0 = TS 4 + 0 = B 125000 +} +[ExpertSingle] +{ + 4040 = N 1 0 + 4280 = N 1 0 + 4540 = N 1 0 + 5540 = N 1 0 + 5800 = N 1 0 + 6060 = N 1 0 + 6560 = N 1 0 + 8100 = N 2 0 + 9100 = N 0 0 + 9320 = N 1 0 + 9580 = N 2 0 + 9780 = N 3 0 + 10080 = N 4 0 + 10600 = N 2 0 + 10740 = N 3 0 + 10840 = N 2 0 + 11100 = N 1 0 + 11600 = N 0 0 + 12100 = N 4 0 + 12900 = N 2 0 + 13160 = N 2 1460 + 16140 = N 1 0 + 16640 = N 1 0 + 16900 = N 2 0 + 17140 = N 3 0 + 17900 = N 4 0 + 18160 = N 0 0 + 19160 = N 4 0 + 19420 = N 3 0 + 19660 = N 2 0 + 19940 = N 0 0 + 20160 = N 1 0 + 20940 = N 2 0 + 21180 = N 3 1480 + 23200 = N 2 0 + 23700 = N 1 0 + 24200 = N 0 0 + 24720 = N 0 0 + 24960 = N 1 0 + 25220 = N 2 0 + 25960 = N 3 0 + 26220 = N 2 0 + 26720 = N 1 0 + 27220 = N 4 0 + 27740 = N 2 0 + 28240 = N 2 0 + 28740 = N 1 0 + 28880 = N 2 0 + 29000 = N 1 0 + 29240 = N 0 0 + 29740 = N 1 0 + 30260 = N 2 0 + 31260 = N 3 0 + 32020 = N 4 0 + 32260 = N 2 1040 + 33780 = N 4 0 + 33780 = E Bad_Too_High + 34040 = N 3 0 + 34280 = N 2 0 + 34780 = N 1 0 + 34960 = N 2 0 + 35120 = N 1 0 + 35300 = N 0 0 + 35800 = N 1 0 + 36300 = N 3 1060 + 37840 = N 4 0 + 38100 = N 3 0 + 38320 = N 2 0 + 40360 = N 1 0 + 41360 = N 0 0 + 41580 = N 1 0 + 41860 = N 2 0 + 42060 = N 3 0 + 42360 = N 4 0 + 42860 = N 2 0 + 42980 = N 3 0 + 43120 = N 2 0 + 43360 = N 1 0 + 43860 = N 0 0 + 44360 = N 4 0 + 45180 = N 2 0 + 45420 = N 2 1740 + 48400 = N 1 0 + 48900 = N 1 0 + 49160 = N 2 0 + 49420 = N 3 0 + 50160 = N 4 0 + 50420 = N 0 0 + 51440 = N 4 0 + 51680 = N 3 0 + 51940 = N 2 0 + 52200 = N 0 0 + 52440 = N 1 0 + 53220 = N 3 0 + 53440 = N 4 1480 + 55460 = N 3 0 + 55980 = N 2 0 + 56480 = N 1 0 + 56980 = N 1 0 + 57240 = N 2 0 + 57480 = N 3 0 + 58240 = N 4 0 + 58480 = N 1 0 + 59000 = N 0 0 + 59500 = N 4 0 + 60000 = N 3 0 + 60500 = N 3 0 + 61000 = N 4 0 + 61140 = N 3 0 + 61260 = N 2 0 + 61520 = N 0 0 + 62020 = N 1 0 + 62520 = N 2 0 + 63520 = N 3 0 + 64280 = N 4 0 + 64540 = N 1 1040 + 66060 = N 4 0 + 66060 = E Bad_Too_High + 66300 = N 3 0 + 66560 = N 2 0 + 67060 = N 1 0 + 67220 = N 2 0 + 67400 = N 1 0 + 67560 = N 0 0 + 68060 = N 4 0 + 68580 = N 3 1220 + 71600 = N 2 0 + 71680 = N 3 0 + 72100 = N 1 0 + 72600 = N 0 0 + 73360 = N 1 0 + 73620 = N 3 1020 + 75120 = N 0 0 + 75380 = N 1 0 + 75640 = N 3 0 + 75880 = N 4 0 + 76140 = N 0 0 + 76140 = E Bad_Too_Low + 76380 = N 4 0 + 76640 = N 2 0 + 77400 = N 1 0 + 77640 = N 2 0 + 77780 = N 3 1380 + 79660 = N 3 0 + 80180 = N 3 0 + 80680 = N 4 0 + 81680 = N 0 0 + 81940 = N 1 0 + 82180 = N 2 0 + 82440 = N 4 0 + 82680 = N 3 0 + 83200 = N 0 0 + 83640 = N 3 0 + 84460 = N 2 0 + 84700 = N 1 2240 + 87740 = N 4 0 + 87740 = E Bad_Too_High + 88240 = N 3 0 + 88740 = N 2 0 + 89740 = N 0 0 + 90000 = N 1 0 + 90260 = N 2 0 + 90500 = N 4 0 + 90760 = N 3 0 + 91260 = N 1 0 + 91440 = N 2 0 + 91600 = N 1 0 + 91760 = N 0 0 + 92260 = N 1 0 + 92780 = N 3 0 + 93540 = N 4 0 + 93780 = N 4 1460 + 95800 = N 1 0 + 96300 = N 3 0 + 96740 = N 3 0 + 98320 = N 2 0 + 98480 = N 4 0 + 98660 = N 3 0 + 98820 = N 2 0 + 99840 = N 1 0 + 100840 = N 2 0 + 101860 = N 1 0 + 102360 = N 2 0 + 102600 = N 1 0 + 102860 = N 0 1380 + 104900 = N 2 0 + 105900 = N 0 0 + 106120 = N 1 0 + 106380 = N 2 0 + 106600 = N 3 0 + 106900 = N 4 0 + 107400 = N 2 0 + 107520 = N 3 0 + 107640 = N 2 0 + 107900 = N 1 0 + 108400 = N 0 0 + 108900 = N 4 0 + 109660 = N 2 0 + 109920 = N 2 1740 + 112940 = N 1 0 + 113440 = N 1 0 + 113700 = N 2 0 + 113940 = N 3 0 + 114700 = N 4 0 + 114960 = N 0 0 + 115980 = N 4 0 + 116220 = N 3 0 + 116480 = N 2 0 + 116740 = N 0 0 + 116980 = N 1 0 + 117740 = N 3 0 + 117980 = N 4 1480 + 120000 = N 3 0 + 120500 = N 2 0 + 121020 = N 1 0 + 121520 = N 1 0 + 121760 = N 2 0 + 122020 = N 3 0 + 122780 = N 4 0 + 123020 = N 1 0 + 123520 = N 0 0 + 124040 = N 3 0 + 124540 = N 2 0 + 125040 = N 2 0 + 125540 = N 3 0 + 125720 = N 2 0 + 125880 = N 1 0 + 126060 = N 0 0 + 126560 = N 1 0 + 127060 = N 2 0 + 128060 = N 3 0 + 128820 = N 4 0 + 129080 = N 1 1040 + 130600 = N 4 0 + 130600 = E Bad_Too_High + 130760 = N 4 0 + 130840 = N 3 0 + 131100 = N 2 0 + 131600 = N 1 0 + 132100 = N 0 0 + 132600 = N 1 0 + 133100 = N 2 1040 + 134620 = N 3 0 + 134880 = N 2 0 + 135120 = N 1 0 + 136020 = N 3 0 + 136140 = N 4 0 + 137140 = N 1 0 + 137140 = E Bad_Too_Low + 138180 = N 1 0 + 138640 = N 0 0 + 138880 = N 1 0 + 139140 = N 2 0 + 139640 = N 3 0 + 140180 = N 4 0 + 140700 = N 3 0 + 141180 = N 4 0 + 142180 = N 4 0 + 142680 = N 0 0 + 142940 = N 1 0 + 143200 = N 2 0 + 143700 = N 3 0 + 144200 = N 4 0 + 144700 = N 3 0 + 144700 = E Bad_Too_Low + 145220 = N 3 0 + 145380 = N 4 2360 + 148240 = N 0 0 + 148480 = N 1 0 + 148740 = N 2 0 + 149000 = N 3 0 + 149240 = N 4 0 + 150260 = N 0 0 + 150760 = N 0 0 + 151020 = N 0 0 + 151260 = N 0 0 + 153260 = N 0 0 +} diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c new file mode 100644 index 000000000..dd9ad9cd7 --- /dev/null +++ b/main/modes/games/ssr/mode_ssr.c @@ -0,0 +1,232 @@ +#include "limits.h" +#include "menu.h" +#include "mode_ssr.h" + +typedef struct +{ + int32_t tick; + int32_t note; + int32_t hold; +} ssrNote_t; + +typedef struct +{ + font_t ibm; + midiFile_t credits; + ssrNote_t* notes; + int32_t tDiv; + int32_t tempo; + int32_t cNote; + int32_t tElapsedMs; + int32_t draw; +} ssrVars_t; + +ssrVars_t* ssr; + +static void ssrEnterMode(void); +static void ssrExitMode(void); +static void ssrMainLoop(int64_t elapsedUs); +static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); +static void loadTrackData(ssrVars_t* ssr, const uint8_t* data, size_t size); + +// static void ssrMenuCb(const char*, bool selected, uint32_t settingVal); + +static const char ssrName[] = "Swadge Swadge Rebellion"; + +swadgeMode_t ssrMode = { + .modeName = ssrName, + .wifiMode = ESP_NOW, + .overrideUsb = false, + .usesAccelerometer = true, + .usesThermometer = false, + .overrideSelectBtn = false, + .fnEnterMode = ssrEnterMode, + .fnExitMode = ssrExitMode, + .fnMainLoop = ssrMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = ssrBackgroundDrawCallback, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +/** + * This function is called when this mode is started. It should initialize + * variables and start the mode. + */ +static void ssrEnterMode(void) +{ + ssr = calloc(1, sizeof(ssrVars_t)); + loadFont("ibm_vga8.font", &ssr->ibm, false); + + size_t sz = 0; + const uint8_t* notes = cnfsGetFile("credits.cch", &sz); + loadTrackData(ssr, notes, sz); + + loadMidiFile("credits.mid", &ssr->credits, false); + + globalMidiPlayerPlaySongCb(&ssr->credits, MIDI_BGM, NULL); + + ssr->tDiv = ssr->credits.timeDivision; + ssr->tempo = globalMidiPlayerGet(MIDI_BGM)->tempo; + printf("%d @ %d\n", ssr->tDiv, ssr->tempo); +} + +/** + * This function is called when the mode is exited. It should free any allocated memory. + */ +static void ssrExitMode(void) +{ + unloadMidiFile(&ssr->credits); + free(ssr->notes); + freeFont(&ssr->ibm); + free(ssr); +} + +/** + * This function is called from the main loop. It's pretty quick, but the + * timing may be inconsistent. + * + * @param elapsedUs The time elapsed since the last time this function was called. Use this value to determine when + * it's time to do things + */ +static void ssrMainLoop(int64_t elapsedUs) +{ + // What a hack! + // ssr->tDiv = ssr->credits.timeDivision; + // ssr->tempo = globalMidiPlayerGet(MIDI_BGM). / 1000; + + + ssr->tElapsedMs += (elapsedUs / 1000); + + // int nextMs = (ssr->notes[ssr->cNote].tick * ssr->tempo) / (ssr->tDiv); + // if (ssr->tElapsedMs >= nextMs) + // { + // ssr->draw = ssr->notes[ssr->cNote].note; + // printf("Note %d (%d, %d)\n", ssr->notes[ssr->cNote].note, nextMs, ssr->tElapsedMs); + // ssr->cNote++; + // } + + midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); + int ticks = SAMPLES_TO_MIDI_TICKS(player->sampleCount, player->tempo, player->reader.division); + int nextTick = ssr->notes[ssr->cNote].tick; + if (ticks >= nextTick) + { + ssr->draw = ssr->notes[ssr->cNote].note; + // printf("Note %d (%d, %d)\n", ssr->notes[ssr->cNote].note, nextMs, ssr->tElapsedMs); + ssr->cNote++; + } + + clearPxTft(); + + paletteColor_t colors[] = {c020, c400, c550, c004, c420}; + // for(int i = 0; i < 5; i++) + // { + // fillDisplayArea(i * (TFT_WIDTH / 5), 0, (i + 1) * (TFT_WIDTH / 5), TFT_HEIGHT, colors[i]); + // } + + fillDisplayArea(ssr->draw * (TFT_WIDTH / 5), 0, (ssr->draw + 1) * (TFT_WIDTH / 5), TFT_HEIGHT, colors[ssr->draw]); + + // Process button events + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + ; // DO SOMETHING + } + + // ssr->credits.tracks + + // Check for analog touch + // int32_t centerVal, intensityVal; + // if (getTouchCentroid(¢erVal, &intensityVal)) + // { + // printf("touch center: %" PRId32 ", intensity: %" PRId32 "\n", centerVal, intensityVal); + // } + // else + // { + // printf("no touch\n"); + // } + + // // Get the acceleration + // int16_t a_x, a_y, a_z; + // accelGetAccelVec(&a_x, &a_y, &a_z); + + // Set LEDs + led_t leds[CONFIG_NUM_LEDS] = {0}; + // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) + // { + // leds[i].r = (255 * ((i + 0) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // leds[i].g = (255 * ((i + 3) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // leds[i].b = (255 * ((i + 6) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // } + setLeds(leds, CONFIG_NUM_LEDS); +} + +/** + * This function is called when the display driver wishes to update a + * section of the display. + * + * @param x the x coordinate that should be updated + * @param y the x coordinate that should be updated + * @param w the width of the rectangle to be updated + * @param h the height of the rectangle to be updated + * @param up update number + * @param numUp update number denominator + */ +static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +{ + // fillDisplayArea(x, y, x + w, y + h, c555); +} + +/** + * @brief Callback for when menu items are selected + * + * @param label The menu item that was selected or moved to + * @param selected true if the item was selected, false if it was moved to + * @param settingVal The value of the setting, if the menu item is a settings item + */ +// static void ssrMenuCb(const char* label, bool selected, uint32_t settingVal) +// { +// printf("%s %s\n", label, selected ? "selected" : "scrolled to"); + +// if (selected) +// { +// } +// } + +/** + * @brief TODO + * + * @param data + * @param size + */ +static void loadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) +{ + uint32_t dIdx = 0; + uint16_t numNotes = (data[dIdx++] << 8); + numNotes |= (data[dIdx++]); + + ssrv->notes = calloc(numNotes, sizeof(ssrNote_t)); + + for (int32_t nIdx = 0; nIdx < numNotes; nIdx++) + { + ssrv->notes[nIdx].tick = (data[dIdx + 0] << 24) | // + (data[dIdx + 1] << 16) | // + (data[dIdx + 2] << 8) | // + (data[dIdx + 3] << 0); + dIdx += 4; + + ssrv->notes[nIdx].note = data[dIdx++]; + + if (0x80 & ssrv->notes[nIdx].note) + { + printf("HOLD!\n"); + ssrv->notes[nIdx].note &= 0x7F; + ssrv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // + (data[dIdx + 1] << 0); + dIdx += 2; + } + + printf("%d %d %d\n", ssrv->notes[nIdx].tick, ssrv->notes[nIdx].note, ssrv->notes[nIdx].hold); + } +} diff --git a/main/modes/games/ssr/mode_ssr.h b/main/modes/games/ssr/mode_ssr.h new file mode 100644 index 000000000..f130b6638 --- /dev/null +++ b/main/modes/games/ssr/mode_ssr.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +extern swadgeMode_t ssrMode; \ No newline at end of file diff --git a/main/modes/system/mainMenu/mainMenu.c b/main/modes/system/mainMenu/mainMenu.c index 9c6ac3bdd..42f91f7a0 100644 --- a/main/modes/system/mainMenu/mainMenu.c +++ b/main/modes/system/mainMenu/mainMenu.c @@ -18,6 +18,7 @@ #include "mode_credits.h" #include "mode_pinball.h" #include "mode_bigbug.h" +#include "mode_ssr.h" #include "mode_synth.h" #include "ultimateTTT.h" #include "touchTest.h" @@ -151,6 +152,7 @@ static void mainMenuEnterMode(void) // Add single items mainMenu->menu = startSubMenu(mainMenu->menu, "Games"); + addSingleItemToMenu(mainMenu->menu, ssrMode.modeName); addSingleItemToMenu(mainMenu->menu, tttMode.modeName); addSingleItemToMenu(mainMenu->menu, pinballMode.modeName); addSingleItemToMenu(mainMenu->menu, t48Mode.modeName); @@ -368,6 +370,10 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) { switchToSwadgeMode(&tttMode); } + else if (label == ssrMode.modeName) + { + switchToSwadgeMode(&ssrMode); + } else if (label == timerMode.modeName) { switchToSwadgeMode(&timerMode); From 39e6e4a2aa6ec1a9046dfeb75043ec071d9715f1 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Tue, 17 Sep 2024 00:53:09 +0000 Subject: [PATCH 16/54] Get what looks like a rhythm game running --- main/midi/midiPlayer.h | 7 ++ main/modes/games/ssr/mode_ssr.c | 164 +++++++++++++++++++++++--------- 2 files changed, 127 insertions(+), 44 deletions(-) diff --git a/main/midi/midiPlayer.h b/main/midi/midiPlayer.h index 8751d9072..e82b852d8 100644 --- a/main/midi/midiPlayer.h +++ b/main/midi/midiPlayer.h @@ -94,9 +94,16 @@ /// @brief Convert the sample count to MIDI ticks #define SAMPLES_TO_MIDI_TICKS(n, tempo, div) ((n) * 1000000 * (div) / DAC_SAMPLE_RATE_HZ / (tempo)) +/// @brief Convert samples to milliseconds +#define SAMPLES_TO_MS(samp) (((samp) * 1000) / DAC_SAMPLE_RATE_HZ) + /// @brief Calculate the number of DAC samples in the given number of milliseconds #define MS_TO_SAMPLES(ms) ((ms) * DAC_SAMPLE_RATE_HZ / 1000) +/// @brief Convert MIDI ticks to milliseconds +#define MIDI_TICKS_TO_MS(ticks, tempo, div) \ + (int64_t)((int64_t)((int64_t)(ticks) * (int64_t)(tempo)) / (int64_t)((int64_t)1000 * (int64_t)(div))) + /// @brief Callback function used to provide feedback when a song finishes playing typedef void (*songFinishedCbFn)(void); diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index dd9ad9cd7..dda59f3e9 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -2,6 +2,9 @@ #include "menu.h" #include "mode_ssr.h" +#define HIT_BAR 16 +#define ICON_RADIUS 8 + typedef struct { int32_t tick; @@ -11,14 +14,26 @@ typedef struct typedef struct { + int32_t note; + int32_t timer; + int32_t posY; +} ssrNoteIcon_t; + +typedef struct +{ + // Font font_t ibm; + + // Song being played midiFile_t credits; + + // Track data + int32_t numNotes; ssrNote_t* notes; - int32_t tDiv; - int32_t tempo; int32_t cNote; - int32_t tElapsedMs; - int32_t draw; + + // Drawing data + list_t icons; } ssrVars_t; ssrVars_t* ssr; @@ -56,20 +71,19 @@ swadgeMode_t ssrMode = { */ static void ssrEnterMode(void) { + // 60FPS please + setFrameRateUs(16667); + ssr = calloc(1, sizeof(ssrVars_t)); loadFont("ibm_vga8.font", &ssr->ibm, false); - size_t sz = 0; + size_t sz = 0; const uint8_t* notes = cnfsGetFile("credits.cch", &sz); loadTrackData(ssr, notes, sz); loadMidiFile("credits.mid", &ssr->credits, false); globalMidiPlayerPlaySongCb(&ssr->credits, MIDI_BGM, NULL); - - ssr->tDiv = ssr->credits.timeDivision; - ssr->tempo = globalMidiPlayerGet(MIDI_BGM)->tempo; - printf("%d @ %d\n", ssr->tDiv, ssr->tempo); } /** @@ -79,6 +93,11 @@ static void ssrExitMode(void) { unloadMidiFile(&ssr->credits); free(ssr->notes); + void* val; + while ((val = pop(&ssr->icons))) + { + free(val); + } freeFont(&ssr->ibm); free(ssr); } @@ -92,40 +111,102 @@ static void ssrExitMode(void) */ static void ssrMainLoop(int64_t elapsedUs) { - // What a hack! - // ssr->tDiv = ssr->credits.timeDivision; - // ssr->tempo = globalMidiPlayerGet(MIDI_BGM). / 1000; + // This doesn't need to be calculated per-loop... + int32_t travelTimeMs = 2000; + int32_t travelUsPerPx = (1000 * travelTimeMs) / (TFT_HEIGHT - HIT_BAR + (2 * ICON_RADIUS)); + // If there are any notes left + if (ssr->cNote < ssr->numNotes) + { + // Get a reference to the player + midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); - ssr->tElapsedMs += (elapsedUs / 1000); + // Get the position of the song and when the next event is, in ms + int32_t songMs = SAMPLES_TO_MS(player->sampleCount); - // int nextMs = (ssr->notes[ssr->cNote].tick * ssr->tempo) / (ssr->tDiv); - // if (ssr->tElapsedMs >= nextMs) - // { - // ssr->draw = ssr->notes[ssr->cNote].note; - // printf("Note %d (%d, %d)\n", ssr->notes[ssr->cNote].note, nextMs, ssr->tElapsedMs); - // ssr->cNote++; - // } + // Check events until one hasn't happened yet + while (true) + { + // When the next event occurs + int32_t nextEventMs = MIDI_TICKS_TO_MS(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); + + // Check if the icon should be spawned now to reach the hit bar in time + if (songMs + travelTimeMs >= nextEventMs) + { + // Spawn an icon + ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); + ni->note = ssr->notes[ssr->cNote].note; + ni->posY = TFT_HEIGHT + (ICON_RADIUS * 2); + ni->timer = 0; + push(&ssr->icons, ni); + + // Increment the track data + ssr->cNote++; + } + else + { + break; + } + } + } + + // Clear the display + clearPxTft(); - midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); - int ticks = SAMPLES_TO_MIDI_TICKS(player->sampleCount, player->tempo, player->reader.division); - int nextTick = ssr->notes[ssr->cNote].tick; - if (ticks >= nextTick) + // Draw the target area + drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); + for (int32_t i = 0; i < 5; i++) { - ssr->draw = ssr->notes[ssr->cNote].note; - // printf("Note %d (%d, %d)\n", ssr->notes[ssr->cNote].note, nextMs, ssr->tElapsedMs); - ssr->cNote++; + int32_t xOffset = ((i * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); + drawCircle(xOffset, HIT_BAR, ICON_RADIUS + 2, c555); } - clearPxTft(); + // Draw all the icons + node_t* iconNode = ssr->icons.first; + while (iconNode) + { + // Draw the icon + ssrNoteIcon_t* icon = iconNode->val; + int32_t xOffset = ((icon->note * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); + paletteColor_t colors[] = {c020, c400, c550, c004, c420}; + drawCircleFilled(xOffset, icon->posY, ICON_RADIUS, colors[icon->note]); + + // Highlight the target if the timing is good enough + if (HIT_BAR - 4 <= icon->posY && icon->posY <= HIT_BAR + 4) + { + drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[icon->note]); + } - paletteColor_t colors[] = {c020, c400, c550, c004, c420}; - // for(int i = 0; i < 5; i++) - // { - // fillDisplayArea(i * (TFT_WIDTH / 5), 0, (i + 1) * (TFT_WIDTH / 5), TFT_HEIGHT, colors[i]); - // } + // Track if this icon gets removed + bool removed = false; - fillDisplayArea(ssr->draw * (TFT_WIDTH / 5), 0, (ssr->draw + 1) * (TFT_WIDTH / 5), TFT_HEIGHT, colors[ssr->draw]); + // Run this icon's timer + icon->timer += elapsedUs; + while (icon->timer >= travelUsPerPx) + { + icon->timer -= travelUsPerPx; + icon->posY--; + + // If it's off screen + if (icon->posY < -ICON_RADIUS) + { + // Remove this icon + node_t* nodeToRemove = iconNode; + iconNode = iconNode->next; + free(nodeToRemove->val); + removeEntry(&ssr->icons, nodeToRemove); + removed = true; + break; + } + } + + // If this icon wasn't removed + if (!removed) + { + // Iterate to the next + iconNode = iconNode->next; + } + } // Process button events buttonEvt_t evt = {0}; @@ -134,8 +215,6 @@ static void ssrMainLoop(int64_t elapsedUs) ; // DO SOMETHING } - // ssr->credits.tracks - // Check for analog touch // int32_t centerVal, intensityVal; // if (getTouchCentroid(¢erVal, &intensityVal)) @@ -202,13 +281,13 @@ static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h */ static void loadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) { - uint32_t dIdx = 0; - uint16_t numNotes = (data[dIdx++] << 8); - numNotes |= (data[dIdx++]); + uint32_t dIdx = 0; + ssrv->numNotes = (data[dIdx++] << 8); + ssrv->numNotes |= (data[dIdx++]); - ssrv->notes = calloc(numNotes, sizeof(ssrNote_t)); + ssrv->notes = calloc(ssrv->numNotes, sizeof(ssrNote_t)); - for (int32_t nIdx = 0; nIdx < numNotes; nIdx++) + for (int32_t nIdx = 0; nIdx < ssrv->numNotes; nIdx++) { ssrv->notes[nIdx].tick = (data[dIdx + 0] << 24) | // (data[dIdx + 1] << 16) | // @@ -220,13 +299,10 @@ static void loadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) if (0x80 & ssrv->notes[nIdx].note) { - printf("HOLD!\n"); ssrv->notes[nIdx].note &= 0x7F; ssrv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // (data[dIdx + 1] << 0); dIdx += 2; } - - printf("%d %d %d\n", ssrv->notes[nIdx].tick, ssrv->notes[nIdx].note, ssrv->notes[nIdx].hold); } } From 8247e48858c517056f76ce9ec955af53f46c8db8 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Tue, 17 Sep 2024 01:09:37 +0000 Subject: [PATCH 17/54] Fix crash at end of song --- main/modes/games/ssr/mode_ssr.c | 55 ++++++++++++++++----------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index dda59f3e9..bc9828537 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -115,38 +115,35 @@ static void ssrMainLoop(int64_t elapsedUs) int32_t travelTimeMs = 2000; int32_t travelUsPerPx = (1000 * travelTimeMs) / (TFT_HEIGHT - HIT_BAR + (2 * ICON_RADIUS)); - // If there are any notes left - if (ssr->cNote < ssr->numNotes) - { - // Get a reference to the player - midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); + // Get a reference to the player + midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); - // Get the position of the song and when the next event is, in ms - int32_t songMs = SAMPLES_TO_MS(player->sampleCount); + // Get the position of the song and when the next event is, in ms + int32_t songMs = SAMPLES_TO_MS(player->sampleCount); - // Check events until one hasn't happened yet - while (true) - { - // When the next event occurs - int32_t nextEventMs = MIDI_TICKS_TO_MS(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); + // Check events until one hasn't happened yet or the song ends + while (ssr->cNote < ssr->numNotes) + { + // When the next event occurs + int32_t nextEventMs = MIDI_TICKS_TO_MS(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); - // Check if the icon should be spawned now to reach the hit bar in time - if (songMs + travelTimeMs >= nextEventMs) - { - // Spawn an icon - ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); - ni->note = ssr->notes[ssr->cNote].note; - ni->posY = TFT_HEIGHT + (ICON_RADIUS * 2); - ni->timer = 0; - push(&ssr->icons, ni); - - // Increment the track data - ssr->cNote++; - } - else - { - break; - } + // Check if the icon should be spawned now to reach the hit bar in time + if (songMs + travelTimeMs >= nextEventMs) + { + // Spawn an icon + ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); + ni->note = ssr->notes[ssr->cNote].note; + ni->posY = TFT_HEIGHT + (ICON_RADIUS * 2); + ni->timer = 0; + push(&ssr->icons, ni); + + // Increment the track data + ssr->cNote++; + } + else + { + // Nothing more to be spawned right now + break; } } From 9b1046ae936ee787575627d797f4a0c77a0d7cdd Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Wed, 18 Sep 2024 11:10:22 +0000 Subject: [PATCH 18/54] Fix firmware compilation --- main/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 548295324..e6d61cd0e 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -35,6 +35,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "modes/games/pinball/pinball_physics.c" "modes/games/pinball/pinball_test.c" "modes/games/pinball/pinball_zones.c" + "modes/games/ssr/mode_ssr.c" "modes/games/ultimateTTT/ultimateTTT.c" "modes/games/ultimateTTT/ultimateTTTgame.c" "modes/games/ultimateTTT/ultimateTTThowTo.c" @@ -108,6 +109,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./modes/games" "./modes/games/pinball" "./modes/games/bigbug" + "./modes/games/ssr" "./modes/games/ultimateTTT" "./modes/games/2048/" "./modes/music" From 1a9b35dfeff274295e2ba027ee25debf78fb1625 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Wed, 18 Sep 2024 11:11:02 +0000 Subject: [PATCH 19/54] Add lead-in to SSR Cleanup --- main/midi/midiPlayer.h | 30 ++++++----- main/modes/games/ssr/mode_ssr.c | 93 ++++++++++++++++++++++++++------- 2 files changed, 91 insertions(+), 32 deletions(-) diff --git a/main/midi/midiPlayer.h b/main/midi/midiPlayer.h index e82b852d8..d08b6d033 100644 --- a/main/midi/midiPlayer.h +++ b/main/midi/midiPlayer.h @@ -97,12 +97,14 @@ /// @brief Convert samples to milliseconds #define SAMPLES_TO_MS(samp) (((samp) * 1000) / DAC_SAMPLE_RATE_HZ) +/// @brief Convert samples to microseconds +#define SAMPLES_TO_US(samp) (((samp) * 1000000) / DAC_SAMPLE_RATE_HZ) + /// @brief Calculate the number of DAC samples in the given number of milliseconds #define MS_TO_SAMPLES(ms) ((ms) * DAC_SAMPLE_RATE_HZ / 1000) -/// @brief Convert MIDI ticks to milliseconds -#define MIDI_TICKS_TO_MS(ticks, tempo, div) \ - (int64_t)((int64_t)((int64_t)(ticks) * (int64_t)(tempo)) / (int64_t)((int64_t)1000 * (int64_t)(div))) +/// @brief Convert MIDI ticks to microseconds +#define MIDI_TICKS_TO_US(ticks, tempo, div) (int64_t)((int64_t)((int64_t)(ticks) * (int64_t)(tempo)) / ((int64_t)(div))) /// @brief Callback function used to provide feedback when a song finishes playing typedef void (*songFinishedCbFn)(void); @@ -203,24 +205,24 @@ typedef enum LOW_AGOGO = 68, CABASA = 69, MARACAS = 70, - /// @brief This note supersdes any ::SHORT_WHISTLE or ::LONG_WHISTLE notes playing + /// @brief This note supersedes any ::SHORT_WHISTLE or ::LONG_WHISTLE notes playing SHORT_WHISTLE = 71, - /// @brief This note supersdes any ::SHORT_WHISTLE or ::LONG_WHISTLE notes playing + /// @brief This note supersedes any ::SHORT_WHISTLE or ::LONG_WHISTLE notes playing LONG_WHISTLE = 72, - /// @brief This note supersdes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing + /// @brief This note supersedes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing SHORT_GUIRO = 73, - /// @brief This note supersdes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing + /// @brief This note supersedes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing LONG_GUIRO = 74, CLAVES = 75, HIGH_WOODBLOCK = 76, LOW_WOODBLOCK = 77, - /// @brief This note supersdes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing + /// @brief This note supersedes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing MUTE_CUICA = 78, - /// @brief This note supersdes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing + /// @brief This note supersedes any ::SHORT_GUIRO or ::LONG_GUIRO notes playing OPEN_CUICA = 79, - /// @brief This note supersdes any ::MUTE_TRIANGLE or ::OPEN_TRIANGLE notes playing + /// @brief This note supersedes any ::MUTE_TRIANGLE or ::OPEN_TRIANGLE notes playing MUTE_TRIANGLE = 80, - /// @brief This note supersdes any ::MUTE_TRIANGLE or ::OPEN_TRIANGLE notes playing + /// @brief This note supersedes any ::MUTE_TRIANGLE or ::OPEN_TRIANGLE notes playing OPEN_TRIANGLE = 81, // Roland GS Extensions /*SHAKER = 82, @@ -483,7 +485,7 @@ typedef struct oscillatorShape_t shape; }; - /// @brief The ASDR characterstics of this timbre + /// @brief The ASDR characteristics of this timbre envelope_t envelope; /// @brief Various effects applied to this timbre. May be ignored by percussion timbres @@ -984,13 +986,13 @@ void globalMidiPlayerSetVolume(uint8_t trackType, int32_t volumeSetting); void globalMidiPlayerPauseAll(void); /** - * @brief Resume all songs currently being played by the system-widi MIDI players + * @brief Resume all songs currently being played by the system-wide MIDI players * */ void globalMidiPlayerResumeAll(void); /** - * @brief Stop all songs currently bein gplayed by the system-widi MIDI players, + * @brief Stop all songs currently being played by the system-wide MIDI players, * optionally resetting their state to the beginning of the song * * @param reset if true, the players will be reset to the beginning of the song diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index bc9828537..07ac3233e 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -1,10 +1,23 @@ -#include "limits.h" -#include "menu.h" +//============================================================================== +// Defines +//============================================================================== + #include "mode_ssr.h" +//============================================================================== +// Defines +//============================================================================== + #define HIT_BAR 16 #define ICON_RADIUS 8 +#define TRAVEL_TIME_US 2000000 +#define TRAVEL_US_PER_PX ((TRAVEL_TIME_US) / (TFT_HEIGHT - HIT_BAR + (2 * ICON_RADIUS))) + +//============================================================================== +// Structs +//============================================================================== + typedef struct { int32_t tick; @@ -26,6 +39,7 @@ typedef struct // Song being played midiFile_t credits; + int32_t leadInUs; // Track data int32_t numNotes; @@ -36,16 +50,21 @@ typedef struct list_t icons; } ssrVars_t; -ssrVars_t* ssr; +//============================================================================== +// Function Declarations +//============================================================================== static void ssrEnterMode(void); static void ssrExitMode(void); static void ssrMainLoop(int64_t elapsedUs); static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); -static void loadTrackData(ssrVars_t* ssr, const uint8_t* data, size_t size); - +static void ssrLoadTrackData(ssrVars_t* ssr, const uint8_t* data, size_t size); // static void ssrMenuCb(const char*, bool selected, uint32_t settingVal); +//============================================================================== +// Variables +//============================================================================== + static const char ssrName[] = "Swadge Swadge Rebellion"; swadgeMode_t ssrMode = { @@ -65,6 +84,12 @@ swadgeMode_t ssrMode = { .fnAdvancedUSB = NULL, }; +ssrVars_t* ssr; + +//============================================================================== +// Functions +//============================================================================== + /** * This function is called when this mode is started. It should initialize * variables and start the mode. @@ -74,16 +99,23 @@ static void ssrEnterMode(void) // 60FPS please setFrameRateUs(16667); + // Allocate mode memory ssr = calloc(1, sizeof(ssrVars_t)); + + // Load a font loadFont("ibm_vga8.font", &ssr->ibm, false); - size_t sz = 0; - const uint8_t* notes = cnfsGetFile("credits.cch", &sz); - loadTrackData(ssr, notes, sz); + // Load the track data + size_t sz = 0; + ssrLoadTrackData(ssr, cnfsGetFile("credits.cch", &sz), sz); + // Load the MIDI file loadMidiFile("credits.mid", &ssr->credits, false); + globalMidiPlayerPlaySong(&ssr->credits, MIDI_BGM); + globalMidiPlayerPauseAll(); - globalMidiPlayerPlaySongCb(&ssr->credits, MIDI_BGM, NULL); + // Set the lead-in timer + ssr->leadInUs = TRAVEL_TIME_US; } /** @@ -91,14 +123,23 @@ static void ssrEnterMode(void) */ static void ssrExitMode(void) { + // Free MIDI data unloadMidiFile(&ssr->credits); + + // Free track data free(ssr->notes); + + // Free UI data void* val; while ((val = pop(&ssr->icons))) { free(val); } + + // Free the font freeFont(&ssr->ibm); + + // Free mode memory free(ssr); } @@ -111,24 +152,40 @@ static void ssrExitMode(void) */ static void ssrMainLoop(int64_t elapsedUs) { - // This doesn't need to be calculated per-loop... - int32_t travelTimeMs = 2000; - int32_t travelUsPerPx = (1000 * travelTimeMs) / (TFT_HEIGHT - HIT_BAR + (2 * ICON_RADIUS)); + // Run a lead-in timer to allow notes to spawn before the song starts playing + if (ssr->leadInUs > 0) + { + ssr->leadInUs -= elapsedUs; + + if (ssr->leadInUs <= 0) + { + globalMidiPlayerResumeAll(); + ssr->leadInUs = 0; + } + } // Get a reference to the player midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); // Get the position of the song and when the next event is, in ms - int32_t songMs = SAMPLES_TO_MS(player->sampleCount); + int32_t songUs; + if (ssr->leadInUs > 0) + { + songUs = -ssr->leadInUs; + } + else + { + songUs = SAMPLES_TO_US(player->sampleCount); + } // Check events until one hasn't happened yet or the song ends while (ssr->cNote < ssr->numNotes) { // When the next event occurs - int32_t nextEventMs = MIDI_TICKS_TO_MS(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); + int32_t nextEventUs = MIDI_TICKS_TO_US(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); // Check if the icon should be spawned now to reach the hit bar in time - if (songMs + travelTimeMs >= nextEventMs) + if (songUs + TRAVEL_TIME_US >= nextEventUs) { // Spawn an icon ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); @@ -179,9 +236,9 @@ static void ssrMainLoop(int64_t elapsedUs) // Run this icon's timer icon->timer += elapsedUs; - while (icon->timer >= travelUsPerPx) + while (icon->timer >= TRAVEL_US_PER_PX) { - icon->timer -= travelUsPerPx; + icon->timer -= TRAVEL_US_PER_PX; icon->posY--; // If it's off screen @@ -276,7 +333,7 @@ static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h * @param data * @param size */ -static void loadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) +static void ssrLoadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) { uint32_t dIdx = 0; ssrv->numNotes = (data[dIdx++] << 8); From 350f94add955eb9ed527fdafb712c29f08ee3ffd Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Wed, 18 Sep 2024 11:27:46 +0000 Subject: [PATCH 20/54] Start button input --- main/modes/games/ssr/mode_ssr.c | 90 +++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index 07ac3233e..e9c7aa4d2 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -48,6 +48,7 @@ typedef struct // Drawing data list_t icons; + buttonBit_t btnState; } ssrVars_t; //============================================================================== @@ -86,6 +87,35 @@ swadgeMode_t ssrMode = { ssrVars_t* ssr; +static const paletteColor_t colors[] = {c020, c400, c550, c004, c420}; + +static const int32_t btnMap[][2] = { + { + PB_LEFT, + 0, + }, + { + PB_DOWN, + 1, + }, + { + PB_UP, + 2, + }, + { + PB_RIGHT, + 3, + }, + { + PB_B, + 4, + }, + { + PB_A, + 5, + }, +}; + //============================================================================== // Functions //============================================================================== @@ -152,6 +182,39 @@ static void ssrExitMode(void) */ static void ssrMainLoop(int64_t elapsedUs) { + // Process button events + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + ssr->btnState = evt.state; + for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(btnMap); bIdx++) + { + if (evt.button & btnMap[bIdx][0]) + { + printf("%d %s\n", btnMap[bIdx][1], evt.down ? "Down" : "Up"); + } + } + + // if (evt.down) + // { + // switch (evt.button) + // { + // case PB_UP: + // case PB_DOWN: + // case PB_LEFT: + // case PB_RIGHT: + // case PB_A: + // case PB_B: + // case PB_START: + // case PB_SELECT: + // default: + // { + // break; + // } + // } + // } + } + // Run a lead-in timer to allow notes to spawn before the song starts playing if (ssr->leadInUs > 0) { @@ -220,16 +283,16 @@ static void ssrMainLoop(int64_t elapsedUs) while (iconNode) { // Draw the icon - ssrNoteIcon_t* icon = iconNode->val; - int32_t xOffset = ((icon->note * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); - paletteColor_t colors[] = {c020, c400, c550, c004, c420}; + ssrNoteIcon_t* icon = iconNode->val; + int32_t xOffset = ((icon->note * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); drawCircleFilled(xOffset, icon->posY, ICON_RADIUS, colors[icon->note]); + // TODO figure out how to draw holds // Highlight the target if the timing is good enough - if (HIT_BAR - 4 <= icon->posY && icon->posY <= HIT_BAR + 4) - { - drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[icon->note]); - } + // if (HIT_BAR - 4 <= icon->posY && icon->posY <= HIT_BAR + 4) + // { + // drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[icon->note]); + // } // Track if this icon gets removed bool removed = false; @@ -262,11 +325,14 @@ static void ssrMainLoop(int64_t elapsedUs) } } - // Process button events - buttonEvt_t evt = {0}; - while (checkButtonQueueWrapper(&evt)) + // Draw indicators that the button is pressed + for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(btnMap); bIdx++) { - ; // DO SOMETHING + if (ssr->btnState & btnMap[bIdx][0]) + { + int32_t xOffset = ((bIdx * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); + drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[bIdx]); + } } // Check for analog touch @@ -354,6 +420,8 @@ static void ssrLoadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) if (0x80 & ssrv->notes[nIdx].note) { ssrv->notes[nIdx].note &= 0x7F; + + // TODO figure out how to draw holds ssrv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // (data[dIdx + 1] << 0); dIdx += 2; From 784b6be6ac91ac392e96e35194249fc7f656dc5b Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Wed, 18 Sep 2024 22:53:58 +0000 Subject: [PATCH 21/54] Draw hold notes --- main/modes/games/ssr/mode_ssr.c | 48 +++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index e9c7aa4d2..bcb39497e 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -29,7 +29,8 @@ typedef struct { int32_t note; int32_t timer; - int32_t posY; + int32_t headPosY; + int32_t tailPosY; } ssrNoteIcon_t; typedef struct @@ -253,8 +254,28 @@ static void ssrMainLoop(int64_t elapsedUs) // Spawn an icon ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); ni->note = ssr->notes[ssr->cNote].note; - ni->posY = TFT_HEIGHT + (ICON_RADIUS * 2); - ni->timer = 0; + ni->headPosY = TFT_HEIGHT + (ICON_RADIUS * 2); + + // If this is a hold note + if (ssr->notes[ssr->cNote].hold) + { + // Figure out at what microsecond the tail ends + int32_t tailUs = MIDI_TICKS_TO_US(ssr->notes[ssr->cNote].hold, player->tempo, player->reader.division); + // Convert the time to a number of pixels + int32_t tailPx = tailUs / TRAVEL_US_PER_PX; + // Add the length pixels to the head to get the tail + ni->tailPosY = ni->headPosY + tailPx; + } + else + { + // No tail + ni->tailPosY = -1; + } + + // Start the timer at zero + ni->timer = 0; + + // Push into the list of icons push(&ssr->icons, ni); // Increment the track data @@ -285,8 +306,13 @@ static void ssrMainLoop(int64_t elapsedUs) // Draw the icon ssrNoteIcon_t* icon = iconNode->val; int32_t xOffset = ((icon->note * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); - drawCircleFilled(xOffset, icon->posY, ICON_RADIUS, colors[icon->note]); - // TODO figure out how to draw holds + drawCircleFilled(xOffset, icon->headPosY, ICON_RADIUS, colors[icon->note]); + // If there is a tail + if (icon->tailPosY >= 0) + { + // Draw the tail + fillDisplayArea(xOffset - 2, icon->headPosY, xOffset + 3, icon->tailPosY, colors[icon->note]); + } // Highlight the target if the timing is good enough // if (HIT_BAR - 4 <= icon->posY && icon->posY <= HIT_BAR + 4) @@ -302,10 +328,16 @@ static void ssrMainLoop(int64_t elapsedUs) while (icon->timer >= TRAVEL_US_PER_PX) { icon->timer -= TRAVEL_US_PER_PX; - icon->posY--; + + // Move the whole icon up + icon->headPosY--; + if (icon->tailPosY >= 0) + { + icon->tailPosY--; + } // If it's off screen - if (icon->posY < -ICON_RADIUS) + if (icon->headPosY < -ICON_RADIUS && (icon->tailPosY < 0)) { // Remove this icon node_t* nodeToRemove = iconNode; @@ -421,7 +453,7 @@ static void ssrLoadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) { ssrv->notes[nIdx].note &= 0x7F; - // TODO figure out how to draw holds + // Use the hold time to see when this note ends ssrv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // (data[dIdx + 1] << 0); dIdx += 2; From 8ecfe7bb097d6bb690e1483b3823296b9e0b18ad Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Thu, 19 Sep 2024 00:23:32 +0000 Subject: [PATCH 22/54] Add note hits and tail holds --- main/modes/games/ssr/mode_ssr.c | 368 ++++++++++++++++++++++---------- 1 file changed, 250 insertions(+), 118 deletions(-) diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/ssr/mode_ssr.c index bcb39497e..50c995b80 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/ssr/mode_ssr.c @@ -31,6 +31,7 @@ typedef struct int32_t timer; int32_t headPosY; int32_t tailPosY; + bool held; } ssrNoteIcon_t; typedef struct @@ -62,6 +63,9 @@ static void ssrMainLoop(int64_t elapsedUs); static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); static void ssrLoadTrackData(ssrVars_t* ssr, const uint8_t* data, size_t size); // static void ssrMenuCb(const char*, bool selected, uint32_t settingVal); +static uint32_t btnToNote(buttonBit_t btn); +static void ssrRunTimers(ssrVars_t* ssrv, uint32_t elapsedUs); +static void ssrDrawGame(ssrVars_t* ssrv); //============================================================================== // Variables @@ -88,34 +92,8 @@ swadgeMode_t ssrMode = { ssrVars_t* ssr; -static const paletteColor_t colors[] = {c020, c400, c550, c004, c420}; - -static const int32_t btnMap[][2] = { - { - PB_LEFT, - 0, - }, - { - PB_DOWN, - 1, - }, - { - PB_UP, - 2, - }, - { - PB_RIGHT, - 3, - }, - { - PB_B, - 4, - }, - { - PB_A, - 5, - }, -}; +static const paletteColor_t colors[] = {c020, c400, c550, c004, c420, c222}; +static const buttonBit_t noteToBtn[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; //============================================================================== // Functions @@ -188,32 +166,87 @@ static void ssrMainLoop(int64_t elapsedUs) while (checkButtonQueueWrapper(&evt)) { ssr->btnState = evt.state; - for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(btnMap); bIdx++) + + if (evt.down) { - if (evt.button & btnMap[bIdx][0]) + // Iterate through all currently shown icons + node_t* iconNode = ssr->icons.first; + while (iconNode) { - printf("%d %s\n", btnMap[bIdx][1], evt.down ? "Down" : "Up"); + ssrNoteIcon_t* icon = iconNode->val; + + // If the icon matches the button + if (icon->note == btnToNote(evt.button)) + { + // Find how off the timing is + int32_t pxOff = ABS(HIT_BAR - icon->headPosY); + int32_t usOff = pxOff * TRAVEL_US_PER_PX; + printf("%" PRId32 " us off\n", usOff); + + // Check if this button hit a note + bool iconHit = false; + + // Classify the time off + if (usOff < 21500) + { + printf(" Fantastic\n"); + iconHit = true; + } + else if (usOff < 43000) + { + printf(" Marvelous\n"); + iconHit = true; + } + else if (usOff < 102000) + { + printf(" Great\n"); + iconHit = true; + } + else if (usOff < 135000) + { + printf(" Decent\n"); + iconHit = true; + } + else if (usOff < 180000) + { + printf(" Way Off\n"); + iconHit = true; + } + else + { + printf(" MISS\n"); + } + + // If it was close enough to hit + if (iconHit) + { + if (icon->tailPosY >= 0) + { + // There is a tail, don't remove the note yet + icon->headPosY = HIT_BAR; + icon->held = true; + } + else + { + // No tail, remove the icon + node_t* nextNode = iconNode->next; + removeEntry(&ssr->icons, iconNode); + iconNode = nextNode; + } + } + + // the button was matched to an icon, break the loop + break; + } + + // Iterate to the next icon + iconNode = iconNode->next; } } - - // if (evt.down) - // { - // switch (evt.button) - // { - // case PB_UP: - // case PB_DOWN: - // case PB_LEFT: - // case PB_RIGHT: - // case PB_A: - // case PB_B: - // case PB_START: - // case PB_SELECT: - // default: - // { - // break; - // } - // } - // } + else + { + // TODO handle ups when holding tails + } } // Run a lead-in timer to allow notes to spawn before the song starts playing @@ -288,40 +321,52 @@ static void ssrMainLoop(int64_t elapsedUs) } } - // Clear the display - clearPxTft(); + ssrRunTimers(ssr, elapsedUs); + ssrDrawGame(ssr); - // Draw the target area - drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); - for (int32_t i = 0; i < 5; i++) - { - int32_t xOffset = ((i * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); - drawCircle(xOffset, HIT_BAR, ICON_RADIUS + 2, c555); - } + // Check for analog touch + // int32_t centerVal, intensityVal; + // if (getTouchCentroid(¢erVal, &intensityVal)) + // { + // printf("touch center: %" PRId32 ", intensity: %" PRId32 "\n", centerVal, intensityVal); + // } + // else + // { + // printf("no touch\n"); + // } - // Draw all the icons - node_t* iconNode = ssr->icons.first; + // // Get the acceleration + // int16_t a_x, a_y, a_z; + // accelGetAccelVec(&a_x, &a_y, &a_z); + + // Set LEDs + led_t leds[CONFIG_NUM_LEDS] = {0}; + // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) + // { + // leds[i].r = (255 * ((i + 0) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // leds[i].g = (255 * ((i + 3) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // leds[i].b = (255 * ((i + 6) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // } + setLeds(leds, CONFIG_NUM_LEDS); +} + +/** + * @brief TODO + * + * @param ssrv + * @param elapsedUs + */ +static void ssrRunTimers(ssrVars_t* ssrv, uint32_t elapsedUs) +{ + // Track if an icon was removed + bool removed = false; + + // Run all the icon timers + node_t* iconNode = ssrv->icons.first; while (iconNode) { - // Draw the icon + // Get a reference ssrNoteIcon_t* icon = iconNode->val; - int32_t xOffset = ((icon->note * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); - drawCircleFilled(xOffset, icon->headPosY, ICON_RADIUS, colors[icon->note]); - // If there is a tail - if (icon->tailPosY >= 0) - { - // Draw the tail - fillDisplayArea(xOffset - 2, icon->headPosY, xOffset + 3, icon->tailPosY, colors[icon->note]); - } - - // Highlight the target if the timing is good enough - // if (HIT_BAR - 4 <= icon->posY && icon->posY <= HIT_BAR + 4) - // { - // drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[icon->note]); - // } - - // Track if this icon gets removed - bool removed = false; // Run this icon's timer icon->timer += elapsedUs; @@ -329,68 +374,114 @@ static void ssrMainLoop(int64_t elapsedUs) { icon->timer -= TRAVEL_US_PER_PX; + bool shouldRemove = false; + // Move the whole icon up - icon->headPosY--; - if (icon->tailPosY >= 0) + if (!icon->held) + { + icon->headPosY--; + if (icon->tailPosY >= 0) + { + icon->tailPosY--; + } + + // If it's off screen + if (icon->headPosY < -ICON_RADIUS && (icon->tailPosY < 0)) + { + // Mark it for removal + shouldRemove = true; + } + } + else // The icon is being held { - icon->tailPosY--; + // Only move the tail position + if (icon->tailPosY >= HIT_BAR) + { + icon->tailPosY--; + } + + // If the tail finished + if (icon->tailPosY < HIT_BAR) + { + // Mark it for removal + shouldRemove = true; + } } - // If it's off screen - if (icon->headPosY < -ICON_RADIUS && (icon->tailPosY < 0)) + // If the icon should be removed + if (shouldRemove) { // Remove this icon - node_t* nodeToRemove = iconNode; - iconNode = iconNode->next; - free(nodeToRemove->val); - removeEntry(&ssr->icons, nodeToRemove); + free(iconNode->val); + removeEntry(&ssrv->icons, iconNode); + + // Stop the while timer loop removed = true; break; } } - // If this icon wasn't removed - if (!removed) + // If an icon was removed + if (removed) + { + // Stop iterating through notes + break; + } + else { // Iterate to the next iconNode = iconNode->next; } } +} + +/** + * @brief TODO + * + * @param ssrv + */ +static void ssrDrawGame(ssrVars_t* ssrv) +{ + // Clear the display + clearPxTft(); + + // Draw the target area + drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); + for (int32_t i = 0; i < ARRAY_SIZE(noteToBtn); i++) + { + int32_t xOffset = ((i * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); + drawCircle(xOffset, HIT_BAR, ICON_RADIUS + 2, c555); + } + + // Draw all the icons + node_t* iconNode = ssrv->icons.first; + while (iconNode) + { + // Draw the icon + ssrNoteIcon_t* icon = iconNode->val; + int32_t xOffset = ((icon->note * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); + drawCircleFilled(xOffset, icon->headPosY, ICON_RADIUS, colors[icon->note]); + + // If there is a tail + if (icon->tailPosY >= 0) + { + // Draw the tail + fillDisplayArea(xOffset - 2, icon->headPosY, xOffset + 3, icon->tailPosY, colors[icon->note]); + } + + // Iterate + iconNode = iconNode->next; + } // Draw indicators that the button is pressed - for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(btnMap); bIdx++) + for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(noteToBtn); bIdx++) { - if (ssr->btnState & btnMap[bIdx][0]) + if (ssrv->btnState & noteToBtn[bIdx]) { - int32_t xOffset = ((bIdx * TFT_WIDTH) / 5) + (TFT_WIDTH / 10); + int32_t xOffset = ((bIdx * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[bIdx]); } } - - // Check for analog touch - // int32_t centerVal, intensityVal; - // if (getTouchCentroid(¢erVal, &intensityVal)) - // { - // printf("touch center: %" PRId32 ", intensity: %" PRId32 "\n", centerVal, intensityVal); - // } - // else - // { - // printf("no touch\n"); - // } - - // // Get the acceleration - // int16_t a_x, a_y, a_z; - // accelGetAccelVec(&a_x, &a_y, &a_z); - - // Set LEDs - led_t leds[CONFIG_NUM_LEDS] = {0}; - // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) - // { - // leds[i].r = (255 * ((i + 0) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // leds[i].g = (255 * ((i + 3) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // leds[i].b = (255 * ((i + 6) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // } - setLeds(leds, CONFIG_NUM_LEDS); } /** @@ -460,3 +551,44 @@ static void ssrLoadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) } } } + +/** + * @brief TODO + * + * @param btn + * @return uint32_t + */ +static uint32_t btnToNote(buttonBit_t btn) +{ + switch (btn) + { + case PB_LEFT: + { + return 0; + } + case PB_DOWN: + { + return 1; + } + case PB_UP: + { + return 2; + } + case PB_RIGHT: + { + return 3; + } + case PB_B: + { + return 4; + } + case PB_A: + { + return 5; + } + default: + { + return -1; + } + } +} From 9eedcf8063e23c9752d351e52d54fb9335d5fb19 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Thu, 19 Sep 2024 10:55:12 +0000 Subject: [PATCH 23/54] Merged main into SSR --- .../games => attic}/pinball/mode_pinball.c | 97 +-- attic/pinball/mode_pinball.h | 28 + attic/pinball/pinball_circle.c | 80 ++ attic/pinball/pinball_circle.h | 10 + attic/pinball/pinball_draw.c | 153 ++++ attic/pinball/pinball_draw.h | 7 + attic/pinball/pinball_flipper.c | 106 +++ attic/pinball/pinball_flipper.h | 9 + attic/pinball/pinball_game.c | 500 +++++++++++ attic/pinball/pinball_game.h | 19 + attic/pinball/pinball_line.c | 140 ++++ attic/pinball/pinball_line.h | 8 + attic/pinball/pinball_physics.c | 461 +++++++++++ attic/pinball/pinball_physics.h | 6 + attic/pinball/pinball_point.c | 25 + attic/pinball/pinball_point.h | 6 + attic/pinball/pinball_rectangle.c | 65 ++ attic/pinball/pinball_rectangle.h | 9 + attic/pinball/pinball_triangle.c | 45 + attic/pinball/pinball_triangle.h | 7 + attic/pinball/pinball_typedef.h | 165 ++++ emulator/src/extensions/modes/ext_modes.c | 2 - main/CMakeLists.txt | 22 +- main/modes/games/pinball/mode_pinball.h | 111 --- main/modes/games/pinball/pinball_draw.c | 134 --- main/modes/games/pinball/pinball_draw.h | 6 - main/modes/games/pinball/pinball_physics.c | 771 ----------------- main/modes/games/pinball/pinball_physics.h | 16 - main/modes/games/pinball/pinball_test.c | 316 ------- main/modes/games/pinball/pinball_test.h | 9 - main/modes/games/pinball/pinball_zones.c | 178 ---- main/modes/games/pinball/pinball_zones.h | 9 - main/modes/music/jukebox/jukebox.c | 1 - main/modes/system/mainMenu/mainMenu.c | 6 - main/utils/fl_math/vectorFl2d.c | 17 +- main/utils/fl_math/vectorFl2d.h | 1 + main/utils/macros.h | 29 +- makefile | 2 +- tools/svg-to-pinball/.gitignore | 4 + tools/svg-to-pinball/.vscode/launch.json | 15 + tools/svg-to-pinball/.vscode/settings.json | 7 + tools/svg-to-pinball/README.md | 55 ++ tools/svg-to-pinball/gentable.sh | 8 + tools/svg-to-pinball/pinball.svg | 778 ++++++++++++++++++ tools/svg-to-pinball/requirements.txt | 1 + tools/svg-to-pinball/svg-to-pinball.py | 487 +++++++++++ 46 files changed, 3282 insertions(+), 1649 deletions(-) rename {main/modes/games => attic}/pinball/mode_pinball.c (53%) create mode 100644 attic/pinball/mode_pinball.h create mode 100644 attic/pinball/pinball_circle.c create mode 100644 attic/pinball/pinball_circle.h create mode 100644 attic/pinball/pinball_draw.c create mode 100644 attic/pinball/pinball_draw.h create mode 100644 attic/pinball/pinball_flipper.c create mode 100644 attic/pinball/pinball_flipper.h create mode 100644 attic/pinball/pinball_game.c create mode 100644 attic/pinball/pinball_game.h create mode 100644 attic/pinball/pinball_line.c create mode 100644 attic/pinball/pinball_line.h create mode 100644 attic/pinball/pinball_physics.c create mode 100644 attic/pinball/pinball_physics.h create mode 100644 attic/pinball/pinball_point.c create mode 100644 attic/pinball/pinball_point.h create mode 100644 attic/pinball/pinball_rectangle.c create mode 100644 attic/pinball/pinball_rectangle.h create mode 100644 attic/pinball/pinball_triangle.c create mode 100644 attic/pinball/pinball_triangle.h create mode 100644 attic/pinball/pinball_typedef.h delete mode 100644 main/modes/games/pinball/mode_pinball.h delete mode 100644 main/modes/games/pinball/pinball_draw.c delete mode 100644 main/modes/games/pinball/pinball_draw.h delete mode 100644 main/modes/games/pinball/pinball_physics.c delete mode 100644 main/modes/games/pinball/pinball_physics.h delete mode 100644 main/modes/games/pinball/pinball_test.c delete mode 100644 main/modes/games/pinball/pinball_test.h delete mode 100644 main/modes/games/pinball/pinball_zones.c delete mode 100644 main/modes/games/pinball/pinball_zones.h create mode 100755 tools/svg-to-pinball/.gitignore create mode 100755 tools/svg-to-pinball/.vscode/launch.json create mode 100755 tools/svg-to-pinball/.vscode/settings.json create mode 100755 tools/svg-to-pinball/README.md create mode 100755 tools/svg-to-pinball/gentable.sh create mode 100755 tools/svg-to-pinball/pinball.svg create mode 100755 tools/svg-to-pinball/requirements.txt create mode 100755 tools/svg-to-pinball/svg-to-pinball.py diff --git a/main/modes/games/pinball/mode_pinball.c b/attic/pinball/mode_pinball.c similarity index 53% rename from main/modes/games/pinball/mode_pinball.c rename to attic/pinball/mode_pinball.c index 49f4a05df..2a41750dc 100644 --- a/main/modes/games/pinball/mode_pinball.c +++ b/attic/pinball/mode_pinball.c @@ -6,10 +6,19 @@ #include #include "mode_pinball.h" -#include "pinball_zones.h" +#include "pinball_game.h" #include "pinball_physics.h" #include "pinball_draw.h" -#include "pinball_test.h" + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + pbScene_t scene; + font_t ibm; +} pinball_t; //============================================================================== // Function Prototypes @@ -60,38 +69,10 @@ static void pinEnterMode(void) // Allocate all the memory pinball = calloc(sizeof(pinball_t), 1); - pinball->balls = heap_caps_calloc(MAX_NUM_BALLS, sizeof(pbCircle_t), MALLOC_CAP_SPIRAM); - pinball->bumpers = heap_caps_calloc(MAX_NUM_BUMPERS, sizeof(pbCircle_t), MALLOC_CAP_SPIRAM); - pinball->walls = heap_caps_calloc(MAX_NUM_WALLS, sizeof(pbLine_t), MALLOC_CAP_SPIRAM); - pinball->flippers = heap_caps_calloc(MAX_NUM_FLIPPERS, sizeof(pbFlipper_t), MALLOC_CAP_SPIRAM); - - pinball->ballsTouching = heap_caps_calloc(MAX_NUM_BALLS, sizeof(pbTouchRef_t*), MALLOC_CAP_SPIRAM); - for (uint32_t i = 0; i < MAX_NUM_BALLS; i++) - { - pinball->ballsTouching[i] = heap_caps_calloc(MAX_NUM_TOUCHES, sizeof(pbTouchRef_t), MALLOC_CAP_SPIRAM); - } - - // Split the table into zones - createTableZones(pinball); - - // Create random balls - createRandomBalls(pinball, 0); - pbCreateBall(pinball, 6, 114); - pbCreateBall(pinball, 274, 114); - pbCreateBall(pinball, 135, 10); + loadFont("ibm_vga8.font", &pinball->ibm, false); - // Create random bumpers - createRandomBumpers(pinball, 0); - - // Create random walls - createRandomWalls(pinball, 0); - - // Create flippers - createFlipper(pinball, TFT_WIDTH / 2 - 50, 200, true); - createFlipper(pinball, TFT_WIDTH / 2 + 50, 200, false); - - // Load font - loadFont("ibm_vga8.font", &pinball->ibm_vga8, false); + pbSceneInit(&pinball->scene); + pbStartBall(&pinball->scene); } /** @@ -100,19 +81,8 @@ static void pinEnterMode(void) */ static void pinExitMode(void) { - for (uint32_t i = 0; i < MAX_NUM_BALLS; i++) - { - free(pinball->ballsTouching[i]); - } - free(pinball->ballsTouching); - - free(pinball->balls); - free(pinball->walls); - free(pinball->bumpers); - free(pinball->flippers); - // Free font - freeFont(&pinball->ibm_vga8); - // Free the rest of the state + freeFont(&pinball->ibm); + pbSceneDestroy(&pinball->scene); free(pinball); } @@ -123,37 +93,25 @@ static void pinExitMode(void) */ static void pinMainLoop(int64_t elapsedUs) { - // Make a local copy for speed - pinball_t* p = pinball; - - // Check all queued button events - buttonEvt_t evt; + // Handle inputs + buttonEvt_t evt = {0}; while (checkButtonQueueWrapper(&evt)) { - if (PB_RIGHT == evt.button) + if (evt.down && PB_START == evt.button) { - p->flippers[1].buttonHeld = evt.down; + pbSceneDestroy(&pinball->scene); + pbSceneInit(&pinball->scene); } - else if (PB_LEFT == evt.button) + else { - p->flippers[0].buttonHeld = evt.down; + pbButtonPressed(&pinball->scene, &evt); } } - // Only check physics once per frame - p->frameTimer += elapsedUs; - while (p->frameTimer >= PIN_US_PER_FRAME) - { - p->frameTimer -= PIN_US_PER_FRAME; - updatePinballPhysicsFrame(pinball); - } - - // Always draw foreground to prevent flicker - pinballDrawForeground(pinball); - - // Log frame time for FPS - p->frameTimesIdx = (p->frameTimesIdx + 1) % NUM_FRAME_TIMES; - p->frameTimes[p->frameTimesIdx] = esp_timer_get_time(); + pbSimulate(&pinball->scene, elapsedUs); + pbGameTimers(&pinball->scene, elapsedUs); + pbAdjustCamera(&pinball->scene); + pbSceneDraw(&pinball->scene, &pinball->ibm); } /** @@ -168,5 +126,4 @@ static void pinMainLoop(int64_t elapsedUs) */ static void pinBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) { - pinballDrawBackground(pinball, x, y, w, h); } diff --git a/attic/pinball/mode_pinball.h b/attic/pinball/mode_pinball.h new file mode 100644 index 000000000..e170e8c86 --- /dev/null +++ b/attic/pinball/mode_pinball.h @@ -0,0 +1,28 @@ +#pragma once + +//============================================================================== +// Includes +//============================================================================== + +#include +#include "swadge2024.h" + +//============================================================================== +// Defines +//============================================================================== + +#define PIN_US_PER_FRAME 16667 + +//============================================================================== +// Enums +//============================================================================== + +//============================================================================== +// Structs +//============================================================================== + +//============================================================================== +// Extern variables +//============================================================================== + +extern swadgeMode_t pinballMode; diff --git a/attic/pinball/pinball_circle.c b/attic/pinball/pinball_circle.c new file mode 100644 index 000000000..a606799d6 --- /dev/null +++ b/attic/pinball/pinball_circle.c @@ -0,0 +1,80 @@ +#include "pinball_circle.h" +#include "pinball_rectangle.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return uint32_t + */ +uint32_t readCircleFromFile(uint8_t* tableData, pbScene_t* scene) +{ + pbCircle_t* circle = &scene->circles[scene->numCircles++]; + + uint32_t dIdx = 0; + circle->id = readInt16(tableData, &dIdx); + circle->groupId = readInt8(tableData, &dIdx); + circle->group = addToGroup(scene, circle, circle->groupId); + circle->pos.x = readInt16(tableData, &dIdx); + circle->pos.y = readInt16(tableData, &dIdx); + circle->radius = readInt8(tableData, &dIdx); + circle->type = readInt8(tableData, &dIdx); + circle->pushVel = readInt8(tableData, &dIdx); + + return dIdx; +} + +/** + * @brief Simulate a ball's motion + * + * @param ball + * @param dt + * @param scene + */ +void pbBallSimulate(pbBall_t* ball, int32_t elapsedUs, float dt, pbScene_t* scene) +{ + if (ball->scoopTimer <= 0) + { + ball->vel = addVecFl2d(ball->vel, mulVecFl2d(scene->gravity, dt)); + ball->pos = addVecFl2d(ball->pos, mulVecFl2d(ball->vel, dt)); + } + else + { + ball->scoopTimer -= elapsedUs; + + if (ball->scoopTimer <= 0) + { + // Respawn in the launch tube + for (int32_t pIdx = 0; pIdx < scene->numPoints; pIdx++) + { + pbPoint_t* point = &scene->points[pIdx]; + if (PB_BALL_SPAWN == point->type) + { + ball->pos = point->pos; + break; + } + } + + pbOpenLaunchTube(scene, true); + + // Give the ball initial velocity + ball->vel.x = 0; + ball->vel.y = MAX_LAUNCHER_VELOCITY; + } + } +} + +/** + * @brief TODO + * + * @param circle + * @param elapsedUs + */ +void pbCircleTimer(pbCircle_t* circle, int32_t elapsedUs) +{ + if (circle->litTimer > 0) + { + circle->litTimer -= elapsedUs; + } +} diff --git a/attic/pinball/pinball_circle.h b/attic/pinball/pinball_circle.h new file mode 100644 index 000000000..53132f731 --- /dev/null +++ b/attic/pinball/pinball_circle.h @@ -0,0 +1,10 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +#define PINBALL_RADIUS 8 + +uint32_t readCircleFromFile(uint8_t* tableData, pbScene_t* scene); +void pbBallSimulate(pbBall_t* ball, int32_t elapsedUs, float dt, pbScene_t* scene); +void pbCircleTimer(pbCircle_t* circle, int32_t elapsedUs); diff --git a/attic/pinball/pinball_draw.c b/attic/pinball/pinball_draw.c new file mode 100644 index 000000000..155741306 --- /dev/null +++ b/attic/pinball/pinball_draw.c @@ -0,0 +1,153 @@ +#include "hdw-tft.h" +#include "shapes.h" + +#include "pinball_draw.h" +#include "pinball_line.h" +#include "pinball_circle.h" +#include "pinball_rectangle.h" +#include "pinball_flipper.h" + +/** + * @brief TODO doc + * + * @param scene + */ +void pbAdjustCamera(pbScene_t* scene) +{ + // No balls? No camera adjustment! + if (0 == scene->balls.length) + { + return; + } + + // Find the ball lowest on the table + float lowestBallX = 0; + float lowestBallY = 0; + + node_t* ballNode = scene->balls.first; + while (ballNode) + { + pbBall_t* ball = ballNode->val; + if (ball->pos.y > lowestBallY) + { + lowestBallX = ball->pos.x; + lowestBallY = ball->pos.y; + } + ballNode = ballNode->next; + } + + // Adjust the lowest ball's position to screen coordinates + lowestBallY -= scene->cameraOffset.y; + +#define PIN_CAMERA_BOUND_UPPER ((TFT_HEIGHT) / 4) +#define PIN_CAMERA_BOUND_LOWER ((3 * TFT_HEIGHT) / 4) + + // If the lowest ball is lower than the boundary + if (lowestBallY > PIN_CAMERA_BOUND_LOWER) + { + // Pan the camera down + if (scene->cameraOffset.y < scene->tableDim.y - TFT_HEIGHT) + { + scene->cameraOffset.y += (lowestBallY - PIN_CAMERA_BOUND_LOWER); + } + } + // If the lowest ball is higher than the other boundary + else if (lowestBallY < PIN_CAMERA_BOUND_UPPER) + { + // Pan the camera up + if (scene->cameraOffset.y > 0) + { + scene->cameraOffset.y -= (PIN_CAMERA_BOUND_UPPER - lowestBallY); + } + } + + // Pan in the X direction to view the launch tube + int16_t xEnd = lowestBallX + PINBALL_RADIUS + 40; + if (xEnd > TFT_WIDTH) + { + scene->cameraOffset.x = xEnd - TFT_WIDTH; + } + else + { + scene->cameraOffset.x = 0; + } +} + +/** + * @brief TODO doc + * + * @param scene + */ +void pbSceneDraw(pbScene_t* scene, font_t* font) +{ + clearPxTft(); + + // Draw an indicator for the ball save + if (scene->saveTimer > 0) + { + const char text[] = "SAVE"; + int16_t tWidth = textWidth(font, text); + drawText(font, c555, text, ((280 - tWidth) / 2) - scene->cameraOffset.x, 400 - scene->cameraOffset.y); + } + + // Triangle indicators + for (int32_t i = 0; i < scene->numTriangles; i++) + { + pbTriangle_t* tri = &scene->triangles[i]; + drawTriangleOutlined(tri->p1.x - scene->cameraOffset.x, tri->p1.y - scene->cameraOffset.y, // + tri->p2.x - scene->cameraOffset.x, tri->p2.y - scene->cameraOffset.y, // + tri->p3.x - scene->cameraOffset.x, tri->p3.y - scene->cameraOffset.y, // + tri->isOn ? c550 : cTransparent, c220); + } + + // Lines + for (int32_t i = 0; i < scene->numLines; i++) + { + pinballDrawLine(&scene->lines[i], &scene->cameraOffset); + } + + // balls + node_t* bNode = scene->balls.first; + while (bNode) + { + pbBall_t* ball = bNode->val; + + // Don't draw when scooped + if (ball->scoopTimer <= 0) + { + vecFl_t* pos = &ball->pos; + drawCircleFilled(pos->x - scene->cameraOffset.x, pos->y - scene->cameraOffset.y, ball->radius, c500); + } + + bNode = bNode->next; + } + + // circles + for (int32_t i = 0; i < scene->numCircles; i++) + { + if (PB_BUMPER == scene->circles[i].type) + { + vecFl_t* pos = &scene->circles[i].pos; + drawCircleFilled(pos->x - scene->cameraOffset.x, pos->y - scene->cameraOffset.y, scene->circles[i].radius, + (scene->circles[i].litTimer > 0) ? c252 : c131); + } + } + + // flippers + for (int32_t i = 0; i < scene->numFlippers; i++) + { + pinballDrawFlipper(&scene->flippers[i], &scene->cameraOffset); + } + + // launchers + for (int32_t i = 0; i < scene->numLaunchers; i++) + { + pbLauncher_t* l = &scene->launchers[i]; + int compression = l->height * l->impulse; + vec_t offsetPos = { + .x = l->pos.x - scene->cameraOffset.x, + .y = l->pos.y + compression - scene->cameraOffset.y, + }; + drawRect(offsetPos.x, offsetPos.y, offsetPos.x + l->width, offsetPos.y + l->height - compression, c330); + } +} diff --git a/attic/pinball/pinball_draw.h b/attic/pinball/pinball_draw.h new file mode 100644 index 000000000..848c2645b --- /dev/null +++ b/attic/pinball/pinball_draw.h @@ -0,0 +1,7 @@ +#pragma once + +#include "font.h" +#include "pinball_typedef.h" + +void pbAdjustCamera(pbScene_t* scene); +void pbSceneDraw(pbScene_t* scene, font_t* font); diff --git a/attic/pinball/pinball_flipper.c b/attic/pinball/pinball_flipper.c new file mode 100644 index 000000000..04d007d9b --- /dev/null +++ b/attic/pinball/pinball_flipper.c @@ -0,0 +1,106 @@ +#include + +#include "macros.h" +#include "shapes.h" + +#include "pinball_flipper.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return uint32_t + */ +uint32_t readFlipperFromFile(uint8_t* tableData, pbScene_t* scene) +{ + pbFlipper_t* flipper = &scene->flippers[scene->numFlippers++]; + uint32_t dIdx = 0; + + flipper->pos.x = readInt16(tableData, &dIdx); + flipper->pos.y = readInt16(tableData, &dIdx); + flipper->radius = readInt8(tableData, &dIdx); + flipper->length = readInt8(tableData, &dIdx); + flipper->facingRight = readInt8(tableData, &dIdx) != 0; + + flipper->maxRotation = 1.0f; + flipper->restAngle = 0.523599f; // 30 degrees + flipper->angularVelocity = 20.0f; + + if (!flipper->facingRight) + { + flipper->restAngle = M_PI - flipper->restAngle; + flipper->maxRotation = -flipper->maxRotation; + } + flipper->sign = (flipper->maxRotation >= 0) ? -1 : 1; + flipper->maxRotation = ABS(flipper->maxRotation); + + // changing + flipper->rotation = 0; + flipper->currentAngularVelocity = 0; + flipper->buttonHeld = false; + + return dIdx; +} + +/** + * @brief TODO doc + * + * @param flipper + * @param dt + */ +void pbFlipperSimulate(pbFlipper_t* flipper, float dt) +{ + float prevRotation = flipper->rotation; + + if (flipper->buttonHeld) + { + flipper->rotation = flipper->rotation + dt * flipper->angularVelocity; + if (flipper->rotation > flipper->maxRotation) + { + flipper->rotation = flipper->maxRotation; + } + } + else + { + flipper->rotation = flipper->rotation - dt * flipper->angularVelocity; + if (flipper->rotation < 0) + { + flipper->rotation = 0; + } + } + flipper->currentAngularVelocity = flipper->sign * (flipper->rotation - prevRotation) / dt; +} + +/** + * @brief TODO doc + * + * @param flipper + * @return vecFl_t + */ +vecFl_t pbFlipperGetTip(pbFlipper_t* flipper) +{ + float angle = flipper->restAngle + flipper->sign * flipper->rotation; + vecFl_t dir = {.x = cosf(angle), .y = sinf(angle)}; + return addVecFl2d(flipper->pos, mulVecFl2d(dir, flipper->length)); +} + +/** + * @brief TODO doc + * + * @param flipper + */ +void pinballDrawFlipper(pbFlipper_t* flipper, vec_t* cameraOffset) +{ + vecFl_t pos = { + .x = flipper->pos.x - cameraOffset->x, + .y = flipper->pos.y - cameraOffset->y, + }; + drawCircleFilled(pos.x, pos.y, flipper->radius, c115); + vecFl_t tip = pbFlipperGetTip(flipper); + tip.x -= cameraOffset->x; + tip.y -= cameraOffset->y; + drawCircleFilled(tip.x, tip.y, flipper->radius, c115); + drawLine(pos.x, pos.y + flipper->radius, tip.x, tip.y + flipper->radius, c115, 0); + drawLine(pos.x, pos.y - flipper->radius, tip.x, tip.y - flipper->radius, c115, 0); +} diff --git a/attic/pinball/pinball_flipper.h b/attic/pinball/pinball_flipper.h new file mode 100644 index 000000000..d52429deb --- /dev/null +++ b/attic/pinball/pinball_flipper.h @@ -0,0 +1,9 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +uint32_t readFlipperFromFile(uint8_t* tableData, pbScene_t* scene); +void pinballDrawFlipper(pbFlipper_t* flipper, vec_t* cameraOffset); +void pbFlipperSimulate(pbFlipper_t* flipper, float dt); +vecFl_t pbFlipperGetTip(pbFlipper_t* flipper); diff --git a/attic/pinball/pinball_game.c b/attic/pinball/pinball_game.c new file mode 100644 index 000000000..1b7bb4267 --- /dev/null +++ b/attic/pinball/pinball_game.c @@ -0,0 +1,500 @@ +#include +#include +#include +#include +#include "heatshrink_helper.h" + +#include "pinball_game.h" + +#include "pinball_line.h" +#include "pinball_circle.h" +#include "pinball_rectangle.h" +#include "pinball_flipper.h" +#include "pinball_triangle.h" +#include "pinball_point.h" + +/** + * @brief TODO doc + * + * @param data + * @param idx + * @return uint8_t + */ +uint8_t readInt8(uint8_t* data, uint32_t* idx) +{ + return data[(*idx)++]; +} + +/** + * @brief TODO doc + * + * @param data + * @param idx + * @return uint16_t + */ +uint16_t readInt16(uint8_t* data, uint32_t* idx) +{ + int16_t ret = (data[*idx] << 8) | (data[(*idx) + 1]); + (*idx) += 2; + return ret; +} + +/** + * @brief TODO + * + * @param scene + * @param obj + * @param groupId + * @return list_t* + */ +list_t* addToGroup(pbScene_t* scene, void* obj, uint8_t groupId) +{ + push(&scene->groups[groupId], obj); + return &scene->groups[groupId]; +} + +/** + * @brief TODO doc + * + * @param scene + */ +void pbSceneInit(pbScene_t* scene) +{ + scene->gravity.x = 0; + scene->gravity.y = 180; + scene->score = 0; + scene->paused = false; + + uint32_t decompressedSize = 0; + uint8_t* tableData = (uint8_t*)readHeatshrinkFile("pinball.raw", &decompressedSize, true); + uint32_t dIdx = 0; + + // Allocate groups + scene->numGroups = readInt8(tableData, &dIdx) + 1; + scene->groups = (list_t*)calloc(scene->numGroups, sizeof(list_t)); + + uint16_t linesInFile = readInt16(tableData, &dIdx); + scene->lines = calloc(linesInFile, sizeof(pbLine_t)); + scene->numLines = 0; + for (uint16_t lIdx = 0; lIdx < linesInFile; lIdx++) + { + dIdx += readLineFromFile(&tableData[dIdx], scene); + pbLine_t* newLine = &scene->lines[scene->numLines - 1]; + + // Record the table dimension + float maxX = MAX(newLine->p1.x, newLine->p2.x); + float maxY = MAX(newLine->p1.y, newLine->p2.y); + + if (maxX > scene->tableDim.x) + { + scene->tableDim.x = maxX; + } + if (maxY > scene->tableDim.y) + { + scene->tableDim.y = maxY; + } + } + + uint16_t circlesInFile = readInt16(tableData, &dIdx); + scene->circles = calloc(circlesInFile, sizeof(pbCircle_t)); + scene->numCircles = 0; + for (uint16_t cIdx = 0; cIdx < circlesInFile; cIdx++) + { + dIdx += readCircleFromFile(&tableData[dIdx], scene); + } + + uint16_t rectanglesInFile = readInt16(tableData, &dIdx); + scene->launchers = calloc(1, sizeof(pbLauncher_t)); + scene->numLaunchers = 0; + for (uint16_t rIdx = 0; rIdx < rectanglesInFile; rIdx++) + { + dIdx += readRectangleFromFile(&tableData[dIdx], scene); + } + + uint16_t flippersInFile = readInt16(tableData, &dIdx); + scene->flippers = calloc(flippersInFile, sizeof(pbFlipper_t)); + scene->numFlippers = 0; + for (uint16_t fIdx = 0; fIdx < flippersInFile; fIdx++) + { + dIdx += readFlipperFromFile(&tableData[dIdx], scene); + } + + uint16_t trianglesInFile = readInt16(tableData, &dIdx); + scene->triangles = calloc(trianglesInFile, sizeof(pbTriangle_t)); + scene->numTriangles = 0; + for (uint16_t tIdx = 0; tIdx < trianglesInFile; tIdx++) + { + dIdx += readTriangleFromFile(&tableData[dIdx], scene); + } + + uint16_t pointsInFile = readInt16(tableData, &dIdx); + scene->points = calloc(pointsInFile, sizeof(pbPoint_t)); + scene->numPoints = 0; + for (uint16_t pIdx = 0; pIdx < pointsInFile; pIdx++) + { + dIdx += readPointFromFile(&tableData[dIdx], scene); + } + + free(tableData); + + // Reset the camera + scene->cameraOffset.x = 0; + scene->cameraOffset.y = 0; + + // Start with three balls + scene->ballCount = 3; +} + +/** + * @brief TODO + * + * @param scene + */ +void pbStartBall(pbScene_t* scene) +{ + // Set the state + pbSetState(scene, PBS_WAIT_TO_LAUNCH); + + // Clear loop history + memset(scene->loopHistory, 0, sizeof(scene->loopHistory)); + + // Reset targets + for (uint16_t lIdx = 0; lIdx < scene->numLines; lIdx++) + { + pbLine_t* line = &scene->lines[lIdx]; + if (PB_DROP_TARGET == line->type) + { + line->isUp = true; + } + } + + // Open the launch tube + pbOpenLaunchTube(scene, true); + + clear(&scene->balls); + for (uint16_t pIdx = 0; pIdx < scene->numPoints; pIdx++) + { + if (PB_BALL_SPAWN == scene->points[pIdx].type) + { + pbBall_t* ball = calloc(1, sizeof(pbBall_t)); + ball->pos = scene->points[pIdx].pos; + ball->vel.x = 0; + ball->vel.y = 0; + ball->radius = PINBALL_RADIUS; + ball->mass = M_PI * 4.0f * 4.0f; + ball->restitution = 0.2f; + push(&scene->balls, ball); + return; + } + } +} + +/** + * @brief + * + * @param scene + */ +void pbStartMultiball(pbScene_t* scene) +{ + // Don't start multiball if there are already three balls + if (3 == scene->balls.length) + { + return; + } + + // Ignore the first spawn point (tube) + bool ignoreFirst = true; + + // For each point + for (uint16_t pIdx = 0; pIdx < scene->numPoints; pIdx++) + { + // If this is a spawn point + if (PB_BALL_SPAWN == scene->points[pIdx].type) + { + // Ignore the first + if (ignoreFirst) + { + ignoreFirst = false; + } + else + { + // Spawn a ball here + // TODO check if space is empty first + pbBall_t* ball = calloc(1, sizeof(pbBall_t)); + ball->pos = scene->points[pIdx].pos; + ball->vel.x = 0; + ball->vel.y = 0; + ball->radius = PINBALL_RADIUS; + ball->mass = M_PI * 4.0f * 4.0f; + ball->restitution = 0.2f; + push(&scene->balls, ball); + + // All balls spawned + if (3 == scene->balls.length) + { + return; + } + } + } + } +} + +/** + * @brief TODO + * + * @param scene + */ +void pbSceneDestroy(pbScene_t* scene) +{ + if (scene->groups) + { + // Free the rest of the state + free(scene->lines); + free(scene->circles); + free(scene->launchers); + free(scene->flippers); + free(scene->triangles); + free(scene->points); + + node_t* bNode = scene->balls.first; + while (bNode) + { + free(bNode->val); + bNode = bNode->next; + } + clear(&scene->balls); + + for (int32_t gIdx = 0; gIdx < scene->numGroups; gIdx++) + { + clear(&scene->groups[gIdx]); + } + free(scene->groups); + scene->groups = NULL; + } +} + +// ------------------------ user interaction --------------------------- + +/** + * @brief TODO doc + * + * @param scene + * @param event + */ +void pbButtonPressed(pbScene_t* scene, buttonEvt_t* event) +{ + if (event->down) + { + switch (event->button) + { + case PB_LEFT: + { + for (int32_t fIdx = 0; fIdx < scene->numFlippers; fIdx++) + { + if (scene->flippers[fIdx].facingRight) + { + scene->flippers[fIdx].buttonHeld = true; + } + } + break; + } + case PB_RIGHT: + { + for (int32_t fIdx = 0; fIdx < scene->numFlippers; fIdx++) + { + if (!scene->flippers[fIdx].facingRight) + { + scene->flippers[fIdx].buttonHeld = true; + } + } + for (int32_t rIdx = 0; rIdx < scene->numLaunchers; rIdx++) + { + scene->launchers[rIdx].buttonHeld = true; + } + break; + } + default: + { + break; + } + } + } + else + { + switch (event->button) + { + case PB_LEFT: + { + for (int32_t fIdx = 0; fIdx < scene->numFlippers; fIdx++) + { + if (scene->flippers[fIdx].facingRight) + { + scene->flippers[fIdx].buttonHeld = false; + } + } + break; + } + case PB_RIGHT: + { + for (int32_t fIdx = 0; fIdx < scene->numFlippers; fIdx++) + { + if (!scene->flippers[fIdx].facingRight) + { + scene->flippers[fIdx].buttonHeld = false; + } + } + for (int32_t rIdx = 0; rIdx < scene->numLaunchers; rIdx++) + { + scene->launchers[rIdx].buttonHeld = false; + } + break; + } + default: + { + break; + } + } + } +} + +/** + * @brief + * + * @param ball + * @param scene + */ +void pbRemoveBall(pbBall_t* ball, pbScene_t* scene) +{ + // Clear loop history + memset(scene->loopHistory, 0, sizeof(scene->loopHistory)); + + // If the save timer is running + if (scene->saveTimer > 0 && 1 == scene->balls.length) + { + // Save the ball by scooping it back + printf("Ball Saved\n"); + ball->scoopTimer = 2000000; + } + else + { + // Find the ball in the list + node_t* bNode = scene->balls.first; + while (bNode) + { + if (ball == bNode->val) + { + // Remove the ball from the list + free(bNode->val); + removeEntry(&scene->balls, bNode); + break; + } + bNode = bNode->next; + } + + // If there are no active balls left + if (0 == scene->balls.length) + { + // Decrement the overall ball count + scene->ballCount--; + + // If there are balls left + if (0 < scene->ballCount) + { + pbSetState(scene, PBS_BALL_OVER); + // TODO show bonus set up for next ball, etc. + pbStartBall(scene); + } + else + { + // No balls left + pbSetState(scene, PBS_GAME_OVER); + } + } + } +} + +/** + * @brief TODO + * + * @param scene + * @param elapsedUs + */ +void pbGameTimers(pbScene_t* scene, int32_t elapsedUs) +{ + if (scene->saveTimer > 0) + { + scene->saveTimer -= elapsedUs; + } +} + +/** + * @brief TODO + * + * @param scene + * @param open + */ +void pbOpenLaunchTube(pbScene_t* scene, bool open) +{ + if (open != scene->launchTubeClosed) + { + scene->launchTubeClosed = open; + + if (!open) + { + // Start a 15s timer to save the ball when the door closes + scene->saveTimer = 15000000; + pbSetState(scene, PBS_GAME_NO_EVENT); + } + + for (int32_t lIdx = 0; lIdx < scene->numLines; lIdx++) + { + pbLine_t* line = &scene->lines[lIdx]; + if (PB_LAUNCH_DOOR == line->type) + { + line->isUp = !open; + } + } + } +} + +/** + * @brief TODO + * + * @param scene + * @param state + */ +void pbSetState(pbScene_t* scene, pbGameState_t state) +{ + if (scene->state != state) + { + scene->state = state; + switch (state) + { + case PBS_WAIT_TO_LAUNCH: + { + printf("Ball Start\n"); + break; + } + case PBS_GAME_NO_EVENT: + { + printf("Event Finished\n"); + break; + } + case PBS_GAME_EVENT: + { + printf("Event Started\n"); + break; + } + case PBS_BALL_OVER: + { + printf("Ball Lost\n"); + break; + } + case PBS_GAME_OVER: + { + printf("Game Over\n"); + break; + } + } + } +} diff --git a/attic/pinball/pinball_game.h b/attic/pinball/pinball_game.h new file mode 100644 index 000000000..78cfc0867 --- /dev/null +++ b/attic/pinball/pinball_game.h @@ -0,0 +1,19 @@ +#pragma once + +#include "hdw-btn.h" +#include "macros.h" +#include "pinball_typedef.h" + +uint8_t readInt8(uint8_t* data, uint32_t* idx); +uint16_t readInt16(uint8_t* data, uint32_t* idx); +list_t* addToGroup(pbScene_t* scene, void* obj, uint8_t groupId); + +void pbSceneInit(pbScene_t* scene); +void pbSceneDestroy(pbScene_t* scene); +void pbButtonPressed(pbScene_t* scene, buttonEvt_t* event); +void pbRemoveBall(pbBall_t* ball, pbScene_t* scene); +void pbStartBall(pbScene_t* scene); +void pbGameTimers(pbScene_t* scene, int32_t elapsedUs); +void pbOpenLaunchTube(pbScene_t* scene, bool open); +void pbStartMultiball(pbScene_t* scene); +void pbSetState(pbScene_t* scene, pbGameState_t state); diff --git a/attic/pinball/pinball_line.c b/attic/pinball/pinball_line.c new file mode 100644 index 000000000..10e163c3f --- /dev/null +++ b/attic/pinball/pinball_line.c @@ -0,0 +1,140 @@ +#include "shapes.h" +#include "palette.h" + +#include "pinball_line.h" +#include "pinball_physics.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return int32_t + */ +int32_t readLineFromFile(uint8_t* tableData, pbScene_t* scene) +{ + pbLine_t* line = &scene->lines[scene->numLines++]; + uint32_t dIdx = 0; + line->id = readInt16(tableData, &dIdx); + line->groupId = readInt8(tableData, &dIdx); + line->group = addToGroup(scene, line, line->groupId); + line->p1.x = readInt16(tableData, &dIdx); + line->p1.y = readInt16(tableData, &dIdx); + line->p2.x = readInt16(tableData, &dIdx); + line->p2.y = readInt16(tableData, &dIdx); + line->type = readInt8(tableData, &dIdx); + line->pushVel = readInt8(tableData, &dIdx); + line->isUp = readInt8(tableData, &dIdx); + + return dIdx; +} + +/** + * @brief TODO doc + * + * @param line + */ +void pinballDrawLine(pbLine_t* line, vec_t* cameraOffset) +{ + paletteColor_t color = c555; + switch (line->type) + { + case PB_WALL: + case PB_BALL_LOST: + case PB_LAUNCH_DOOR: + { + if (!line->isUp) + { + return; + } + color = c555; + break; + } + case PB_SLINGSHOT: + { + color = line->litTimer > 0 ? c500 : c300; + break; + } + case PB_DROP_TARGET: + { + if (line->isUp) + { + color = c050; + } + else + { + color = c010; + } + break; + } + case PB_STANDUP_TARGET: + { + color = line->litTimer > 0 ? c004 : c002; + break; + } + case PB_SPINNER: + { + color = c123; + break; + } + case PB_SCOOP: + { + color = c202; + break; + } + } + + drawLine(line->p1.x - cameraOffset->x, line->p1.y - cameraOffset->y, line->p2.x - cameraOffset->x, + line->p2.y - cameraOffset->y, color, 0); +} + +/** + * @brief TODO + * + * @param line + * @param elapsedUs + */ +void pbLineTimer(pbLine_t* line, int32_t elapsedUs, pbScene_t* scene) +{ + // Decrement the lit timer + if (line->litTimer > 0) + { + line->litTimer -= elapsedUs; + } + + // Decrement the reset timer + if (line->resetTimer > 0) + { + line->resetTimer -= elapsedUs; + + if (line->resetTimer <= 0) + { + // Make sure the line isn't intersecting a ball before popping up + bool intersecting = false; + + node_t* bNode = scene->balls.first; + while (bNode) + { + pbBall_t* ball = bNode->val; + if (ballLineIntersection(ball, line)) + { + intersecting = true; + break; + } + bNode = bNode->next; + } + + // If there are no intersections + if (!intersecting) + { + // Raise the target + line->isUp = true; + } + else + { + // Try next frame + line->resetTimer = 1; + } + } + } +} diff --git a/attic/pinball/pinball_line.h b/attic/pinball/pinball_line.h new file mode 100644 index 000000000..bb22992bf --- /dev/null +++ b/attic/pinball/pinball_line.h @@ -0,0 +1,8 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +int32_t readLineFromFile(uint8_t* tableData, pbScene_t* scene); +void pinballDrawLine(pbLine_t* line, vec_t* cameraOffset); +void pbLineTimer(pbLine_t* line, int32_t elapsedUs, pbScene_t* scene); diff --git a/attic/pinball/pinball_physics.c b/attic/pinball/pinball_physics.c new file mode 100644 index 000000000..e99939999 --- /dev/null +++ b/attic/pinball/pinball_physics.c @@ -0,0 +1,461 @@ +#include +#include +#include +#include "pinball_line.h" +#include "pinball_circle.h" +#include "pinball_rectangle.h" +#include "pinball_flipper.h" +#include "pinball_triangle.h" +#include "pinball_physics.h" + +static void handleBallBallCollision(pbBall_t* ball1, pbBall_t* ball2); +static void handleBallCircleCollision(pbScene_t* scene, pbBall_t* ball, pbCircle_t* circle); +static void handleBallFlipperCollision(pbBall_t* ball, pbFlipper_t* flipper); +static bool handleBallLineCollision(pbBall_t* ball, pbScene_t* scene); +static void handleBallLauncherCollision(pbLauncher_t* launcher, pbBall_t* ball, float dt); + +/** + * @brief TODO + * + * @param scene + * @param elapsedUs + */ +void pbSimulate(pbScene_t* scene, int32_t elapsedUs) +{ + float elapsedUsFl = elapsedUs / 1000000.0f; + + for (int32_t i = 0; i < scene->numFlippers; i++) + { + pbFlipperSimulate(&scene->flippers[i], elapsedUsFl); + } + + for (int32_t i = 0; i < scene->numLaunchers; i++) + { + pbLauncherSimulate(&scene->launchers[i], &scene->balls, elapsedUsFl); + } + + node_t* bNode = scene->balls.first; + while (bNode) + { + pbBall_t* ball = bNode->val; + + pbBallSimulate(ball, elapsedUs, elapsedUsFl, scene); + + node_t* bNode2 = bNode->next; + while (bNode2) + { + pbBall_t* ball2 = bNode2->val; + handleBallBallCollision(ball, ball2); + bNode2 = bNode2->next; + } + + for (int32_t cIdx = 0; cIdx < scene->numCircles; cIdx++) + { + handleBallCircleCollision(scene, ball, &scene->circles[cIdx]); + } + + for (int32_t fIdx = 0; fIdx < scene->numFlippers; fIdx++) + { + handleBallFlipperCollision(ball, &scene->flippers[fIdx]); + } + + for (int32_t lIdx = 0; lIdx < scene->numLaunchers; lIdx++) + { + handleBallLauncherCollision(&scene->launchers[lIdx], ball, elapsedUs); + } + + // Collide ball with lines + if (handleBallLineCollision(ball, scene)) + { + // Iterate to the next ball node + bNode = bNode->next; + + // Then remove the ball + pbRemoveBall(ball, scene); + } + else + { + // Iterate to the next ball + bNode = bNode->next; + } + } + + for (int32_t cIdx = 0; cIdx < scene->numCircles; cIdx++) + { + pbCircleTimer(&scene->circles[cIdx], elapsedUs); + } + + for (int32_t tIdx = 0; tIdx < scene->numTriangles; tIdx++) + { + pbTriangleTimer(&scene->triangles[tIdx], elapsedUs); + } + + for (int32_t lIdx = 0; lIdx < scene->numLines; lIdx++) + { + pbLineTimer(&scene->lines[lIdx], elapsedUs, scene); + } +} + +/** + * @brief Find the closest point to point p on a line segment between a and b + * + * @param p A point + * @param a One end of a line segment + * @param b The other end of a line segment + * @return A point on the line segment closest to p + */ +static vecFl_t closestPointOnSegment(vecFl_t p, vecFl_t a, vecFl_t b) +{ + vecFl_t ab = subVecFl2d(b, a); + float t = sqMagVecFl2d(ab); + + if (t == 0.0f) + { + return a; + } + + t = (dotVecFl2d(p, ab) - dotVecFl2d(a, ab)) / t; + if (t > 1) + { + t = 1; + } + else if (t < 0) + { + t = 0; + } + + return addVecFl2d(a, mulVecFl2d(ab, t)); +} + +/** + * @brief TODO doc + * + * @param ball1 + * @param ball2 + */ +static void handleBallBallCollision(pbBall_t* ball1, pbBall_t* ball2) +{ + float restitution = MIN(ball1->restitution, ball2->restitution); + vecFl_t dir = subVecFl2d(ball2->pos, ball1->pos); + float d = magVecFl2d(dir); + if (0 == d || d > (ball1->radius + ball2->radius)) + { + return; + } + + dir = divVecFl2d(dir, d); + + float corr = (ball1->radius + ball2->radius - d) / 2.0f; + ball1->pos = addVecFl2d(ball1->pos, mulVecFl2d(dir, -corr)); + ball2->pos = addVecFl2d(ball2->pos, mulVecFl2d(dir, corr)); + + float v1 = dotVecFl2d(ball1->vel, dir); + float v2 = dotVecFl2d(ball2->vel, dir); + + float m1 = ball1->mass; + float m2 = ball2->mass; + + float newV1 = (m1 * v1 + m2 * v2 - m2 * (v1 - v2) * restitution) / (m1 + m2); + float newV2 = (m1 * v1 + m2 * v2 - m1 * (v2 - v1) * restitution) / (m1 + m2); + + ball1->vel = addVecFl2d(ball1->vel, mulVecFl2d(dir, newV1 - v1)); + ball2->vel = addVecFl2d(ball2->vel, mulVecFl2d(dir, newV2 - v2)); +} + +/** + * @brief TODO doc + * + * @param scene + * @param ball + * @param circle + */ +static void handleBallCircleCollision(pbScene_t* scene, pbBall_t* ball, pbCircle_t* circle) +{ + vecFl_t dir = subVecFl2d(ball->pos, circle->pos); + float d = magVecFl2d(dir); + if (d == 0.0 || d > (ball->radius + circle->radius)) + { + if (circle->id == scene->touchedLoopId) + { + scene->touchedLoopId = PIN_INVALID_ID; + } + return; + } + + if (PB_BUMPER == circle->type) + { + // Normalize the direction + dir = divVecFl2d(dir, d); + + // Move ball backwards to not clip + float corr = ball->radius + circle->radius - d; + ball->pos = addVecFl2d(ball->pos, mulVecFl2d(dir, corr)); + + // Adjust the velocity + float v = dotVecFl2d(ball->vel, dir); + ball->vel = addVecFl2d(ball->vel, mulVecFl2d(dir, circle->pushVel - v)); + + circle->litTimer = 250000; + } + else if (PB_ROLLOVER == circle->type) + { + if (circle->id != scene->touchedLoopId) + { + scene->touchedLoopId = circle->id; + + memmove(&scene->loopHistory[1], &scene->loopHistory[0], + sizeof(scene->loopHistory) - sizeof(scene->loopHistory[0])); + scene->loopHistory[0] = circle->id; + + if (scene->loopHistory[0] + 1 == scene->loopHistory[1] + && scene->loopHistory[1] + 1 == scene->loopHistory[2]) + { + printf("Loop Counter Clockwise\n"); + } + else if (scene->loopHistory[2] + 1 == scene->loopHistory[1] + && scene->loopHistory[1] + 1 == scene->loopHistory[0]) + { + printf("Loop Clockwise\n"); + } + } + // Group two rollovers should close the launch tube + // TODO hardcoding a group ID is gross + if (5 == circle->groupId) + { + pbOpenLaunchTube(scene, false); + } + } +} + +/** + * @brief TODO doc + * + * @param ball + * @param flipper + */ +static void handleBallFlipperCollision(pbBall_t* ball, pbFlipper_t* flipper) +{ + vecFl_t closest = closestPointOnSegment(ball->pos, flipper->pos, pbFlipperGetTip(flipper)); + vecFl_t dir = subVecFl2d(ball->pos, closest); + float d = magVecFl2d(dir); + if (d == 0.0 || d > ball->radius + flipper->radius) + { + return; + } + + dir = divVecFl2d(dir, d); + + float corr = (ball->radius + flipper->radius - d); + ball->pos = addVecFl2d(ball->pos, mulVecFl2d(dir, corr)); + + // update velocity + + vecFl_t radius = closest; + radius = addVecFl2d(radius, mulVecFl2d(dir, flipper->radius)); + radius = subVecFl2d(radius, flipper->pos); + vecFl_t surfaceVel = perpendicularVecFl2d(radius); + surfaceVel = mulVecFl2d(surfaceVel, flipper->currentAngularVelocity); + + float v = dotVecFl2d(ball->vel, dir); + float vNew = dotVecFl2d(surfaceVel, dir); + + ball->vel = addVecFl2d(ball->vel, mulVecFl2d(dir, vNew - v)); +} + +/** + * @brief TODO + * + * @param ball + * @param line + * @return true + * @return false + */ +bool ballLineIntersection(pbBall_t* ball, pbLine_t* line) +{ + // Get the line segment from the list of walls + vecFl_t a = line->p1; + vecFl_t b = line->p2; + // Get the closest point on the segment to the center of the ball + vecFl_t c = closestPointOnSegment(ball->pos, a, b); + // Find the distance between the center of the ball and the closest point on the line + vecFl_t d = subVecFl2d(ball->pos, c); + float dist = magVecFl2d(d); + // If the distance is less than the radius, and the distance is less + // than the minimum distance, its the best collision + return (dist < ball->radius); +} + +/** + * @brief TODO doc + * + * @param ball + * @param lines + * @param true if the ball should be deleted, false if not + */ +static bool handleBallLineCollision(pbBall_t* ball, pbScene_t* scene) +{ + // find closest segment; + vecFl_t ballToClosest; + vecFl_t ab; + vecFl_t normal; + float minDist = FLT_MAX; + pbLine_t* cLine = NULL; + + // For each segment of the wall + for (int32_t i = 0; i < scene->numLines; i++) + { + pbLine_t* line = &scene->lines[i]; + + if (line->isUp) + { + // Get the line segment from the list of walls + vecFl_t a = line->p1; + vecFl_t b = line->p2; + // Get the closest point on the segment to the center of the ball + vecFl_t c = closestPointOnSegment(ball->pos, a, b); + // Find the distance between the center of the ball and the closest point on the line + vecFl_t d = subVecFl2d(ball->pos, c); + float dist = magVecFl2d(d); + // If the distance is less than the radius, and the distance is less + // than the minimum distance, its the best collision + if ((dist < ball->radius) && (dist < minDist)) + { + minDist = dist; + ballToClosest = d; + ab = subVecFl2d(b, a); + normal = perpendicularVecFl2d(ab); + cLine = line; + } + } + } + + // Check if there were any collisions + if (NULL == cLine) + { + return false; + } + + // push out to not clip + if (0 == minDist) + { + ballToClosest = normal; + minDist = magVecFl2d(normal); + } + ballToClosest = divVecFl2d(ballToClosest, minDist); + ball->pos = addVecFl2d(ball->pos, mulVecFl2d(ballToClosest, ball->radius - minDist)); // TODO epsilon here? + + float v = dotVecFl2d(ball->vel, ballToClosest); + if (cLine->pushVel) + { + // Adjust the velocity + ball->vel = addVecFl2d(ball->vel, mulVecFl2d(ballToClosest, cLine->pushVel - v)); + } + else + { + // update velocity + float vNew = ABS(v) * ball->restitution; // TODO care about wall's restitution? + ball->vel = addVecFl2d(ball->vel, mulVecFl2d(ballToClosest, vNew - v)); + } + + switch (cLine->type) + { + default: + case PB_WALL: + case PB_SPINNER: + case PB_LAUNCH_DOOR: + { + break; + } + case PB_STANDUP_TARGET: + { + pbSetState(scene, PBS_GAME_EVENT); + } + // Fall through + case PB_SLINGSHOT: + { + cLine->litTimer = 250000; + break; + } + case PB_DROP_TARGET: + { + cLine->isUp = false; + + // Check if all targets in the group are hit + bool someLineUp = false; + list_t* group = cLine->group; + node_t* node = group->first; + while (NULL != node) + { + pbLine_t* groupLine = node->val; + if (groupLine->isUp) + { + someLineUp = true; + break; + } + node = node->next; + } + + // If all lines are down + if (!someLineUp) + { + // Reset them + // TODO start a timer for this? Make sure a ball isn't touching the line before resetting? + node = group->first; + while (NULL != node) + { + // Start a timer to reset the target + ((pbLine_t*)node->val)->resetTimer = 3000000; + node = node->next; + } + } + break; + } + case PB_SCOOP: + { + // Count the scoop + scene->scoopCount++; + printf("Ball %" PRId32 " locked\n", scene->scoopCount); + if (3 == scene->scoopCount) + { + printf("Multiball!!!\n"); + pbStartMultiball(scene); + } + ball->scoopTimer = 2000000; + break; + } + case PB_BALL_LOST: + { + return true; + } + } + return false; +} + +/** + * @brief TODO + * + * @param launcher + * @param balls + * @param dt + */ +static void handleBallLauncherCollision(pbLauncher_t* launcher, pbBall_t* ball, float dt) +{ + if (ball->vel.y >= 0) + { + // Get the compressed Y level + float posY = launcher->pos.y + (launcher->impulse * launcher->height); + + // Check Y + if ((ball->pos.y + ball->radius > posY) && (ball->pos.y - ball->radius < posY)) + { + // Check X + if ((ball->pos.x > launcher->pos.x) && (ball->pos.x < launcher->pos.x + launcher->width)) + { + // Collision, set the position to be slightly touching + ball->pos.y = posY - ball->radius + 0.1f; + // Bounce a little + ball->vel = mulVecFl2d(ball->vel, -0.3f); + } + } + } +} diff --git a/attic/pinball/pinball_physics.h b/attic/pinball/pinball_physics.h new file mode 100644 index 000000000..8b03470c0 --- /dev/null +++ b/attic/pinball/pinball_physics.h @@ -0,0 +1,6 @@ +#pragma once + +#include "pinball_typedef.h" + +void pbSimulate(pbScene_t* scene, int32_t elapsedUs); +bool ballLineIntersection(pbBall_t* ball, pbLine_t* line); diff --git a/attic/pinball/pinball_point.c b/attic/pinball/pinball_point.c new file mode 100644 index 000000000..5ffa19c20 --- /dev/null +++ b/attic/pinball/pinball_point.c @@ -0,0 +1,25 @@ +#include "shapes.h" +#include "palette.h" + +#include "pinball_point.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return int32_t + */ +int32_t readPointFromFile(uint8_t* tableData, pbScene_t* scene) +{ + pbPoint_t* point = &scene->points[scene->numPoints++]; + uint32_t dIdx = 0; + point->id = readInt16(tableData, &dIdx); + point->groupId = readInt8(tableData, &dIdx); + point->group = addToGroup(scene, point, point->groupId); + point->pos.x = readInt16(tableData, &dIdx); + point->pos.y = readInt16(tableData, &dIdx); + point->type = readInt8(tableData, &dIdx); + + return dIdx; +} diff --git a/attic/pinball/pinball_point.h b/attic/pinball/pinball_point.h new file mode 100644 index 000000000..d79dc6c9d --- /dev/null +++ b/attic/pinball/pinball_point.h @@ -0,0 +1,6 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +int32_t readPointFromFile(uint8_t* tableData, pbScene_t* scene); diff --git a/attic/pinball/pinball_rectangle.c b/attic/pinball/pinball_rectangle.c new file mode 100644 index 000000000..89df5560d --- /dev/null +++ b/attic/pinball/pinball_rectangle.c @@ -0,0 +1,65 @@ +#include +#include "geometryFl.h" + +#include "pinball_rectangle.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return uint32_t + */ +uint32_t readRectangleFromFile(uint8_t* tableData, pbScene_t* scene) +{ + uint32_t dIdx = 0; + pbLauncher_t* launcher = &scene->launchers[scene->numLaunchers++]; + launcher->id = readInt16(tableData, &dIdx); + launcher->groupId = readInt8(tableData, &dIdx); + launcher->group = addToGroup(scene, launcher, launcher->groupId); + launcher->pos.x = readInt16(tableData, &dIdx); + launcher->pos.y = readInt16(tableData, &dIdx); + launcher->width = readInt16(tableData, &dIdx); + launcher->height = readInt16(tableData, &dIdx); + launcher->buttonHeld = false; + launcher->impulse = 0; + return dIdx; +} + +/** + * @brief TODO doc + * + * @param launcher + * @param balls + * @param dt + */ +void pbLauncherSimulate(pbLauncher_t* launcher, list_t* balls, float dt) +{ + if (launcher->buttonHeld) + { + launcher->impulse += (dt / 3); + if (launcher->impulse > 0.99f) + { + launcher->impulse = 0.99f; + } + } + else if (launcher->impulse) + { + rectangleFl_t r = {.pos = launcher->pos, .width = launcher->width, .height = launcher->height}; + // If touching a ball, transfer to a ball + node_t* bNode = balls->first; + while (bNode) + { + pbBall_t* ball = bNode->val; + circleFl_t b = {.pos = ball->pos, .radius = ball->radius}; + if (circleRectFlIntersection(b, r, NULL)) + { + ball->vel.y = (MAX_LAUNCHER_VELOCITY * launcher->impulse); + } + + bNode = bNode->next; + } + + launcher->impulse = 0; + } +} \ No newline at end of file diff --git a/attic/pinball/pinball_rectangle.h b/attic/pinball/pinball_rectangle.h new file mode 100644 index 000000000..dda040bfa --- /dev/null +++ b/attic/pinball/pinball_rectangle.h @@ -0,0 +1,9 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +#define MAX_LAUNCHER_VELOCITY -600 + +uint32_t readRectangleFromFile(uint8_t* tableData, pbScene_t* scene); +void pbLauncherSimulate(pbLauncher_t* launcher, list_t* balls, float dt); diff --git a/attic/pinball/pinball_triangle.c b/attic/pinball/pinball_triangle.c new file mode 100644 index 000000000..0f7011f77 --- /dev/null +++ b/attic/pinball/pinball_triangle.c @@ -0,0 +1,45 @@ +#include +#include "pinball_triangle.h" + +/** + * @brief TODO doc + * + * @param tableData + * @param scene + * @return uint32_t + */ +uint32_t readTriangleFromFile(uint8_t* tableData, pbScene_t* scene) +{ + pbTriangle_t* triangle = &scene->triangles[scene->numTriangles++]; + + uint32_t dIdx = 0; + triangle->id = readInt16(tableData, &dIdx); + triangle->groupId = readInt8(tableData, &dIdx); + triangle->group = addToGroup(scene, triangle, triangle->groupId); + triangle->p1.x = readInt16(tableData, &dIdx); + triangle->p1.y = readInt16(tableData, &dIdx); + triangle->p2.x = readInt16(tableData, &dIdx); + triangle->p2.y = readInt16(tableData, &dIdx); + triangle->p3.x = readInt16(tableData, &dIdx); + triangle->p3.y = readInt16(tableData, &dIdx); + + triangle->blinkTimer = 0; + triangle->isOn = false; + triangle->isBlinking = true; + + return dIdx; +} + +/** + * @brief + * + * @param tri + * @param elapsedUs + */ +void pbTriangleTimer(pbTriangle_t* tri, int32_t elapsedUs) +{ + if (tri->isBlinking) + { + RUN_TIMER_EVERY(tri->blinkTimer, 333333, elapsedUs, tri->isOn = !tri->isOn;); + } +} diff --git a/attic/pinball/pinball_triangle.h b/attic/pinball/pinball_triangle.h new file mode 100644 index 000000000..162a25cc1 --- /dev/null +++ b/attic/pinball/pinball_triangle.h @@ -0,0 +1,7 @@ +#pragma once + +#include "pinball_typedef.h" +#include "pinball_game.h" + +uint32_t readTriangleFromFile(uint8_t* tableData, pbScene_t* scene); +void pbTriangleTimer(pbTriangle_t* tri, int32_t elapsedUs); diff --git a/attic/pinball/pinball_typedef.h b/attic/pinball/pinball_typedef.h new file mode 100644 index 000000000..840f54168 --- /dev/null +++ b/attic/pinball/pinball_typedef.h @@ -0,0 +1,165 @@ +#pragma once + +#include +#include +#include +#include + +#include "vector2d.h" +#include "vectorFl2d.h" +#include "macros.h" +#include "linked_list.h" + +#define PIN_INVALID_ID 0xFFFF + +typedef enum +{ + PBS_WAIT_TO_LAUNCH, + PBS_GAME_NO_EVENT, + PBS_GAME_EVENT, + PBS_BALL_OVER, + PBS_GAME_OVER, +} pbGameState_t; + +typedef enum +{ + PB_WALL, + PB_SLINGSHOT, + PB_DROP_TARGET, + PB_STANDUP_TARGET, + PB_SPINNER, + PB_SCOOP, + PB_BALL_LOST, + PB_LAUNCH_DOOR, +} pbLineType_t; + +typedef enum +{ + PB_BUMPER, + PB_ROLLOVER +} pbCircleType_t; + +typedef enum +{ + PB_BALL_SPAWN, + PB_ITEM_SPAWN, +} pbPointType_t; + +typedef struct +{ + uint16_t id; + uint8_t groupId; + list_t* group; + pbPointType_t type; + vecFl_t pos; +} pbPoint_t; + +typedef struct +{ + uint16_t id; + uint8_t groupId; + list_t* group; + pbLineType_t type; + vecFl_t p1; + vecFl_t p2; + float pushVel; + bool isUp; + int32_t litTimer; + int32_t resetTimer; +} pbLine_t; + +typedef struct +{ + // fixed + float radius; + vecFl_t pos; + float length; + float restAngle; + float maxRotation; + float sign; + float angularVelocity; + // changing + float rotation; + float currentAngularVelocity; + bool buttonHeld; + bool facingRight; +} pbFlipper_t; + +// TODO merge these +typedef struct +{ + float radius; + float mass; + float restitution; + vecFl_t pos; + vecFl_t vel; + int32_t scoopTimer; +} pbBall_t; + +typedef struct +{ + uint16_t id; + uint8_t groupId; + list_t* group; + float radius; + vecFl_t pos; + pbCircleType_t type; + float pushVel; + int32_t litTimer; +} pbCircle_t; + +typedef struct +{ + uint16_t id; + uint8_t groupId; + list_t* group; + vecFl_t p1; + vecFl_t p2; + vecFl_t p3; + bool isBlinking; + bool isOn; + int32_t blinkTimer; +} pbTriangle_t; + +typedef struct +{ + uint16_t id; + uint8_t groupId; + list_t* group; + vecFl_t pos; + float width; + float height; + bool buttonHeld; + float impulse; +} pbLauncher_t; + +typedef struct +{ + vecFl_t gravity; + int32_t score; + bool paused; + int32_t numGroups; + list_t* groups; + pbLine_t* lines; + int32_t numLines; + list_t balls; + pbCircle_t* circles; + int32_t numCircles; + pbFlipper_t* flippers; + int32_t numFlippers; + pbLauncher_t* launchers; + int32_t numLaunchers; + pbTriangle_t* triangles; + int32_t numTriangles; + pbPoint_t* points; + int32_t numPoints; + vec_t cameraOffset; + vecFl_t tableDim; + bool launchTubeClosed; + uint16_t touchedLoopId; + uint16_t loopHistory[3]; + int32_t saveTimer; + int32_t scoopCount; + int32_t ballCount; + pbGameState_t state; +} pbScene_t; \ No newline at end of file diff --git a/emulator/src/extensions/modes/ext_modes.c b/emulator/src/extensions/modes/ext_modes.c index c1b072b05..8bc83d92a 100644 --- a/emulator/src/extensions/modes/ext_modes.c +++ b/emulator/src/extensions/modes/ext_modes.c @@ -29,7 +29,6 @@ #include "mode_2048.h" #include "mode_bigbug.h" #include "mode_credits.h" -#include "mode_pinball.h" #include "mode_synth.h" #include "touchTest.h" #include "tunernome.h" @@ -71,7 +70,6 @@ static swadgeMode_t* allSwadgeModes[] = { &keebTestMode, &mainMenuMode, &modeCredits, - &pinballMode, &synthMode, &t48Mode, &timerMode, diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index e6d61cd0e..6293289de 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,10 +1,10 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" - "asset_loaders/heatshrink_decoder.c" - "asset_loaders/heatshrink_helper.c" "asset_loaders/fs_font.c" "asset_loaders/fs_json.c" "asset_loaders/fs_txt.c" "asset_loaders/fs_wsg.c" + "asset_loaders/heatshrink_decoder.c" + "asset_loaders/heatshrink_helper.c" "colorchord/DFT32.c" "colorchord/embeddedNf.c" "colorchord/embeddedOut.c" @@ -30,17 +30,12 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "modes/games/bigbug/soundManager_bigbug.c" "modes/games/bigbug/tilemap_bigbug.c" "modes/games/bigbug/pathfinding_bigbug.c" - "modes/games/pinball/mode_pinball.c" - "modes/games/pinball/pinball_draw.c" - "modes/games/pinball/pinball_physics.c" - "modes/games/pinball/pinball_test.c" - "modes/games/pinball/pinball_zones.c" "modes/games/ssr/mode_ssr.c" "modes/games/ultimateTTT/ultimateTTT.c" "modes/games/ultimateTTT/ultimateTTTgame.c" "modes/games/ultimateTTT/ultimateTTThowTo.c" - "modes/games/ultimateTTT/ultimateTTTp2p.c" "modes/games/ultimateTTT/ultimateTTTmarkerSelect.c" + "modes/games/ultimateTTT/ultimateTTTp2p.c" "modes/games/ultimateTTT/ultimateTTTresult.c" "modes/games/2048/2048_game.c" "modes/games/2048/2048_menus.c" @@ -58,16 +53,16 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "modes/system/quickSettings/quickSettings.c" "modes/test/accelTest/accelTest.c" "modes/test/factoryTest/factoryTest.c" - "modes/test/touchTest/touchTest.c" "modes/test/keebTest/keebTest.c" + "modes/test/touchTest/touchTest.c" "modes/utilities/dance/dance.c" "modes/utilities/dance/portableDance.c" "modes/utilities/gamepad/gamepad.c" "modes/utilities/timer/modeTimer.c" "swadge2024.c" - "utils/color_utils.c" "utils/cnfs.c" "utils/cnfs_image.c" + "utils/color_utils.c" "utils/dialogBox.c" "utils/fl_math/geometryFl.c" "utils/fl_math/vectorFl2d.c" @@ -98,7 +93,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" crashwrap REQUIRES esp_timer spi_flash - INCLUDE_DIRS "." + INCLUDE_DIRS "./" "./asset_loaders" "./asset_loaders/common" "./colorchord" @@ -107,7 +102,6 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./midi" "./modes" "./modes/games" - "./modes/games/pinball" "./modes/games/bigbug" "./modes/games/ssr" "./modes/games/ultimateTTT" @@ -115,8 +109,8 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./modes/music" "./modes/music/colorchord" "./modes/music/jukebox" - "./modes/music/usbsynth" "./modes/music/tunernome" + "./modes/music/usbsynth" "./modes/system" "./modes/system/credits" "./modes/system/intro" @@ -125,8 +119,8 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./modes/test" "./modes/test/accelTest" "./modes/test/factoryTest" - "./modes/test/touchTest" "./modes/test/keebTest" + "./modes/test/touchTest" "./modes/utilities" "./modes/utilities/dance" "./modes/utilities/gamepad" diff --git a/main/modes/games/pinball/mode_pinball.h b/main/modes/games/pinball/mode_pinball.h deleted file mode 100644 index 20546c649..000000000 --- a/main/modes/games/pinball/mode_pinball.h +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -//============================================================================== -// Includes -//============================================================================== - -#include -#include "swadge2024.h" - -//============================================================================== -// Defines -//============================================================================== - -#define PIN_US_PER_FRAME 16667 -#define NUM_ZONES 32 - -#define MAX_NUM_BALLS 512 -#define MAX_NUM_WALLS 1024 -#define MAX_NUM_BUMPERS 10 -#define MAX_NUM_TOUCHES 16 -#define MAX_NUM_FLIPPERS 6 - -#define NUM_FRAME_TIMES 60 - -//============================================================================== -// Enums -//============================================================================== - -typedef enum -{ - PIN_NO_SHAPE, - PIN_CIRCLE, - PIN_LINE, - PIN_RECT, - PIN_FLIPPER, -} pbShapeType_t; - -//============================================================================== -// Structs -//============================================================================== - -typedef struct -{ - circleFl_t c; - vecFl_t vel; // Velocity is in pixels per frame (@ 60fps, so pixels per 16.7ms) - vecFl_t accel; // Acceleration is pixels per frame squared - vecFl_t lastPos; // The previous postion, used to compare actual positional change to velocity - bool bounce; // true if the ball bounced this frame, false otherwise - uint32_t zoneMask; - paletteColor_t color; - bool filled; -} pbCircle_t; - -typedef struct -{ - lineFl_t l; - float length; - uint32_t zoneMask; - paletteColor_t color; -} pbLine_t; - -typedef struct -{ - rectangleFl_t r; - uint32_t zoneMask; - paletteColor_t color; -} pbRect_t; - -typedef struct -{ - pbCircle_t cPivot; ///< The circle that the flipper pivots on - pbCircle_t cTip; ///< The circle at the tip of the flipper - pbLine_t sideL; ///< The left side of the flipper when pointing upward - pbLine_t sideR; ///< The right side of the flipper when pointing upward - int32_t length; ///< The length of the flipper, from pivot center to tip center - float angle; ///< The current angle of the flipper - bool facingRight; ///< True if the flipper is facing right, false if left - bool buttonHeld; ///< True if the button is being held down, false if it is released - uint32_t zoneMask; ///< The zones this flipper is in -} pbFlipper_t; - -typedef struct -{ - const void* obj; - pbShapeType_t type; -} pbTouchRef_t; - -typedef struct -{ - pbCircle_t* balls; - uint32_t numBalls; - pbTouchRef_t** ballsTouching; - pbLine_t* walls; - uint32_t numWalls; - pbCircle_t* bumpers; - uint32_t numBumpers; - pbFlipper_t* flippers; - uint32_t numFlippers; - int32_t frameTimer; - pbRect_t zones[NUM_ZONES]; - font_t ibm_vga8; - - uint32_t frameTimes[NUM_FRAME_TIMES]; - uint32_t frameTimesIdx; -} pinball_t; - -//============================================================================== -// Extern variables -//============================================================================== - -extern swadgeMode_t pinballMode; diff --git a/main/modes/games/pinball/pinball_draw.c b/main/modes/games/pinball/pinball_draw.c deleted file mode 100644 index 0cdd62265..000000000 --- a/main/modes/games/pinball/pinball_draw.c +++ /dev/null @@ -1,134 +0,0 @@ -//============================================================================== -// Includes -//============================================================================== - -#include "pinball_draw.h" - -//============================================================================== -// Function Declarations -//============================================================================== - -static void drawPinCircle(pbCircle_t* c); -static void drawPinLine(pbLine_t* l); -// static void drawPinRect(pbRect_t* r); -static void drawPinFlipper(pbFlipper_t* f); - -//============================================================================== -// Functions -//============================================================================== - -/** - * @brief Draw a section of the background - * - * @param p Pinball state - * @param x The x coordinate of the background to draw - * @param y The y coordinate of the background to draw - * @param w The width of the background to draw - * @param h The height of the background to draw - */ -void pinballDrawBackground(pinball_t* p, int16_t x, int16_t y, int16_t w, int16_t h) -{ - // Fill with black - fillDisplayArea(x, y, x + w, y + h, c000); -} - -/** - * @brief Draw the foreground - * - * @param p Pinball state - */ -void pinballDrawForeground(pinball_t* p) -{ - // Draw walls - for (uint32_t wIdx = 0; wIdx < p->numWalls; wIdx++) - { - drawPinLine(&p->walls[wIdx]); - } - - // Draw bumpers - for (uint32_t uIdx = 0; uIdx < p->numBumpers; uIdx++) - { - drawPinCircle(&p->bumpers[uIdx]); - } - - // Draw balls - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - drawPinCircle(&p->balls[bIdx]); - } - - // Draw flippers - for (uint32_t fIdx = 0; fIdx < p->numFlippers; fIdx++) - { - drawPinFlipper(&p->flippers[fIdx]); - } - - // Debug draw zones - // for (int32_t i = 0; i < NUM_ZONES; i++) - // { - // drawPinRect(&p->zones[i]); - // } - - // Calculate and draw FPS - int32_t startIdx = (p->frameTimesIdx + 1) % NUM_FRAME_TIMES; - uint32_t tElapsed = p->frameTimes[p->frameTimesIdx] - p->frameTimes[startIdx]; - if (0 != tElapsed) - { - uint32_t fps = (1000000 * NUM_FRAME_TIMES) / tElapsed; - - char tmp[16]; - snprintf(tmp, sizeof(tmp) - 1, "%" PRIu32, fps); - drawText(&p->ibm_vga8, c555, tmp, 35, 2); - } -} - -/** - * @brief Draw a pinball circle - * - * @param c The circle to draw - */ -void drawPinCircle(pbCircle_t* circ) -{ - if (circ->filled) - { - drawCircleFilled((circ->c.pos.x), (circ->c.pos.y), (circ->c.radius), circ->color); - } - else - { - drawCircle((circ->c.pos.x), (circ->c.pos.y), (circ->c.radius), circ->color); - } -} - -/** - * @brief Draw a pinball line - * - * @param l The line to draw - */ -void drawPinLine(pbLine_t* line) -{ - drawLineFast((line->l.p1.x), (line->l.p1.y), (line->l.p2.x), (line->l.p2.y), line->color); -} - -/** - * @brief Draw a pinball rectangle - * - * @param r The rectangle to draw - */ -// void drawPinRect(pbRect_t* rect) -// { -// drawRect(rect->r.pos.x, rect->r.pos.y, rect->r.pos.x + rect->r.width, rect->r.pos.y + rect->r.height, -// rect->color); -// } - -/** - * @brief Draw a pinball flipper - * - * @param f The flipper to draw - */ -void drawPinFlipper(pbFlipper_t* f) -{ - drawPinCircle(&f->cPivot); - drawPinCircle(&f->cTip); - drawPinLine(&f->sideL); - drawPinLine(&f->sideR); -} diff --git a/main/modes/games/pinball/pinball_draw.h b/main/modes/games/pinball/pinball_draw.h deleted file mode 100644 index 2e10f138e..000000000 --- a/main/modes/games/pinball/pinball_draw.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include "mode_pinball.h" - -void pinballDrawBackground(pinball_t* p, int16_t x, int16_t y, int16_t w, int16_t h); -void pinballDrawForeground(pinball_t* p); diff --git a/main/modes/games/pinball/pinball_physics.c b/main/modes/games/pinball/pinball_physics.c deleted file mode 100644 index 54568988f..000000000 --- a/main/modes/games/pinball/pinball_physics.c +++ /dev/null @@ -1,771 +0,0 @@ -//============================================================================== -// Includes -//============================================================================== - -#include -#include "pinball_physics.h" -#include "pinball_zones.h" - -//============================================================================== -// Function Declarations -//============================================================================== - -bool checkBallPbCircleCollision(pbCircle_t* ball, pbCircle_t* circle, pbTouchRef_t* touchRef); -bool checkBallPbLineCollision(pbCircle_t* ball, pbLine_t* line, pbTouchRef_t* touchRef); - -void checkBallBallCollisions(pinball_t* p); -void checkBallStaticCollision(pinball_t* p); -void sweepCheckFlippers(pinball_t* p); - -void moveBalls(pinball_t* p); - -void checkBallsNotTouching(pinball_t* p); -void setBallTouching(pbTouchRef_t* ballTouching, const void* obj, pbShapeType_t type); -pbShapeType_t ballIsTouching(pbTouchRef_t* ballTouching, const void* obj); - -void checkBallsAtRest(pinball_t* p); - -void moveBallBackFromLine(pbCircle_t* ball, pbLine_t* line, vecFl_t* collisionVec); -void moveBallBackFromCircle(pbCircle_t* ball, pbCircle_t* fixed); - -//============================================================================== -// Functions -//============================================================================== - -/** - * @brief TODO - * - * @param p - */ -void updatePinballPhysicsFrame(pinball_t* p) -{ - // Move balls along new vectors - moveBalls(p); - - // Move flippers rotationally - sweepCheckFlippers(p); - - // If there are multiple balls - if (1 < p->numBalls) - { - // Check for ball-ball collisions - checkBallBallCollisions(p); - } - - // Check for collisions between balls and static objects - checkBallStaticCollision(p); - - // Check if balls are actually at rest - checkBallsAtRest(p); - - // Clear references to balls touching things after moving - checkBallsNotTouching(p); -} - -/** - * @brief TODO - * - * @param p - */ -void checkBallBallCollisions(pinball_t* p) -{ - // For each ball, check collisions with other balls - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - pbCircle_t* ball = &p->balls[bIdx]; - for (uint32_t obIdx = bIdx + 1; obIdx < p->numBalls; obIdx++) - { - pbCircle_t* otherBall = &p->balls[obIdx]; - vecFl_t centerToCenter; - // Check for a new collision - if ((ball->zoneMask & otherBall->zoneMask) // In the same zone - && circleCircleFlIntersection(ball->c, otherBall->c, NULL, ¢erToCenter)) // and intersecting - { - // Move balls backwards equally from the midpoint to not clip - float halfDistM = (ball->c.radius + otherBall->c.radius - EPSILON) / 2.0f; - vecFl_t midwayPoint = divVecFl2d(addVecFl2d(ball->c.pos, otherBall->c.pos), 2.0f); - vecFl_t vecFromMid = mulVecFl2d(normVecFl2d(centerToCenter), halfDistM); - - // Move both balls - ball->c.pos = addVecFl2d(midwayPoint, vecFromMid); - otherBall->c.pos = subVecFl2d(midwayPoint, vecFromMid); - - // If the balls aren't touching yet, adjust velocities (bounce) - if (PIN_NO_SHAPE == ballIsTouching(p->ballsTouching[bIdx], otherBall)) - { - // Math for the first ball - vecFl_t v1 = ball->vel; - vecFl_t x1 = ball->c.pos; - vecFl_t v2 = otherBall->vel; - vecFl_t x2 = otherBall->c.pos; - vecFl_t x1_x2 = subVecFl2d(x1, x2); - vecFl_t v1_v2 = subVecFl2d(v1, v2); - float xSqMag = sqMagVecFl2d(x1_x2); - vecFl_t ballNewVel = ball->vel; - if (xSqMag > 0) - { - ballNewVel = subVecFl2d(v1, mulVecFl2d(x1_x2, (dotVecFl2d(v1_v2, x1_x2) / xSqMag))); - } - - // Flip everything for the other ball - v1 = otherBall->vel; - x1 = otherBall->c.pos; - v2 = ball->vel; - x2 = ball->c.pos; - x1_x2 = subVecFl2d(x1, x2); - v1_v2 = subVecFl2d(v1, v2); - xSqMag = sqMagVecFl2d(x1_x2); - if (xSqMag > 0) - { - otherBall->vel - = subVecFl2d(v1, mulVecFl2d(x1_x2, (dotVecFl2d(v1_v2, x1_x2) / sqMagVecFl2d(x1_x2)))); - } - - // Set the new velocity for the first ball after finding the second's - ball->vel = ballNewVel; - - // The balls are touching each other - setBallTouching(p->ballsTouching[bIdx], otherBall, PIN_CIRCLE); - setBallTouching(p->ballsTouching[obIdx], ball, PIN_CIRCLE); - - // Mark both balls as bounced - ball->bounce = true; - otherBall->bounce = true; - } - } - } - } -} - -/** - * @brief TODO - * - * @param ball - * @param circle - * @param touchRef - * @return true - * @return false - */ -bool checkBallPbCircleCollision(pbCircle_t* ball, pbCircle_t* circle, pbTouchRef_t* touchRef) -{ - bool bounced = false; - vecFl_t collisionVec; - - // Check for a collision - if ((ball->zoneMask & circle->zoneMask) // In the same zone - && circleCircleFlIntersection(ball->c, circle->c, NULL, &collisionVec)) // and intersecting - { - // Find the normalized vector along the collision normal - vecFl_t reflVec = normVecFl2d(collisionVec); - - // If the ball isn't already touching the circle - if (PIN_NO_SHAPE == ballIsTouching(touchRef, circle)) - { - // Bounced on a circle - ball->bounce = true; - bounced = true; - // Reflect the velocity vector along the normal between the two radii - // See http://www.sunshine2k.de/articles/coding/vectorreflection/vectorreflection.html - ball->vel = subVecFl2d(ball->vel, mulVecFl2d(reflVec, (2 * dotVecFl2d(ball->vel, reflVec)))); - // Lose some speed on the bounce - ball->vel = mulVecFl2d(ball->vel, WALL_BOUNCINESS); - // printf("%d,%.4f,%.4f\n", __LINE__, ball->vel.x, ball->vel.y); - // Mark this circle as being touched to not double-bounce - setBallTouching(touchRef, circle, PIN_CIRCLE); - } - - // Move ball back to not clip into the circle - moveBallBackFromCircle(ball, circle); - // ball->c.pos = addVecFl2d(circle->c.pos, mulVecFl2d(reflVec, ball->c.radius + circle->c.radius - EPSILON)); - } - return bounced; -} - -/** - * @brief TODO - * - * @param ball - * @param line - * @param touchRef - * @return true - * @return false - */ -bool checkBallPbLineCollision(pbCircle_t* ball, pbLine_t* line, pbTouchRef_t* touchRef) -{ - bool bounced = false; - vecFl_t collisionVec; - vecFl_t cpOnLine; - // Check for a collision - if ((ball->zoneMask & line->zoneMask) // In the same zone - && circleLineFlIntersection(ball->c, line->l, true, &cpOnLine, &collisionVec)) // and intersecting - { - /* TODO this reflection can have bad results when colliding with the tip of a line. - * The center-center vector can get weird if the ball moves fast and clips into the tip. - * The solution is probably to binary-search-move the ball as far as it'll go without clipping - */ - - // Find the normalized vector along the collision normal - vecFl_t reflVec = normVecFl2d(collisionVec); - - // If the ball isn't already touching the line - if (PIN_NO_SHAPE == ballIsTouching(touchRef, line)) - { - // The dot product with the collision normal is how much Y velocity component there is. - // If this value is small the ball should slide down the line (i.e. don't lose velocity on the bounce) - float velDotColNorm = dotVecFl2d(ball->vel, reflVec); - - // Bounce it by reflecting across the collision normal - ball->vel = subVecFl2d(ball->vel, mulVecFl2d(reflVec, (2 * velDotColNorm))); - - // Check if the ball should slide (i.e. not lose velocity) or bounce (i.e. lose velocity) - // 0 means the ball's velocity is parallel to the wall (slide) - // -mag(vel) means the ball's velocity is perpendicular to the wall (bounce) - if (velDotColNorm < -0.2f) - { - // Lose some speed on the bounce. - ball->vel = mulVecFl2d(ball->vel, WALL_BOUNCINESS); - } - - // Mark this line as being touched to not double-bounce - setBallTouching(touchRef, line, PIN_LINE); - - // Bounced off a line - ball->bounce = true; - bounced = true; - } - - // Move ball back to not clip into the bumper - // TODO accommodate line end collisions (circle) - moveBallBackFromLine(ball, line, &reflVec); - // ball->c.pos = addVecFl2d(cpOnLine, mulVecFl2d(reflVec, ball->c.radius - EPSILON)); - } - return bounced; -} - -/** - * @brief TODO - * - * @param p - */ -void checkBallStaticCollision(pinball_t* p) -{ - // For each ball, check collisions with static objects - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - // Reference and integer representation - pbCircle_t* ball = &p->balls[bIdx]; - - // Iterate over all bumpers - for (uint32_t uIdx = 0; uIdx < p->numBumpers; uIdx++) - { - checkBallPbCircleCollision(ball, &p->bumpers[uIdx], p->ballsTouching[bIdx]); - } - - // Iterate over all walls - for (uint32_t wIdx = 0; wIdx < p->numWalls; wIdx++) - { - checkBallPbLineCollision(ball, &p->walls[wIdx], p->ballsTouching[bIdx]); - } - } -} - -/** - * @brief TODO - * - * @param p - */ -void sweepCheckFlippers(pinball_t* p) -{ - // For each flipper - for (uint32_t fIdx = 0; fIdx < p->numFlippers; fIdx++) - { - pbFlipper_t* flipper = &p->flippers[fIdx]; - - // Check if the flipper is moving up or down - float angularVel = 0; - if (flipper->buttonHeld) - { - angularVel = FLIPPER_UP_DEGREES_PER_FRAME; - } - else - { - angularVel = -FLIPPER_DOWN_DEGREES_PER_FRAME; - } - - // Find the bounds for the flipper depending on the direction it's facing - float lBound = 0; - float uBound = 0; - if (flipper->facingRight) - { - lBound = M_PI_2 - FLIPPER_UP_ANGLE; - uBound = M_PI_2 + FLIPPER_DOWN_ANGLE; - // Flip velocity if facing right - angularVel *= -1; - } - else - { - lBound = (M_PI + M_PI_2) - FLIPPER_DOWN_ANGLE; - uBound = (M_PI + M_PI_2) + FLIPPER_UP_ANGLE; - } - - // Flipper starts here - float sweepStart = flipper->angle; - // Flipper ends here, bounded - float sweepEnd = flipper->angle + angularVel; - sweepEnd = CLAMP((sweepEnd), lBound, uBound); - - // Find sweep steps if in motion - float sweepStep = 0.0f; - int numSteps = 0; - if (sweepStart == sweepEnd) - { - // Flipper not in motion - angularVel = 0; - sweepStep = 0; - numSteps = 1; - } - else - { - // Flipper in motion - // TODO large sweep steps kill framerate.... - numSteps = 8; - sweepStep = (sweepEnd - sweepStart) / (float)numSteps; - } - - // Move the flipper a little, then check for collisions - for (int32_t step = 0; step < numSteps; step++) - { - // Sweep the flipper a little - flipper->angle += sweepStep; - updateFlipperPos(flipper); - - // Normal collision checks - // For each ball, check collisions with flippers objects - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - // Reference and integer representation - pbCircle_t* ball = &p->balls[bIdx]; - pbTouchRef_t* touchRef = p->ballsTouching[bIdx]; - - if (ball->zoneMask & flipper->zoneMask) - { - // Check if the ball is touching any part of the flipper - bool touching = false; - vecFl_t colPoint, colVec; - if (circleLineFlIntersection(ball->c, flipper->sideL.l, false, &colPoint, &colVec)) - { - // Move ball back to not clip into the flipper - colVec = normVecFl2d(colVec); - moveBallBackFromLine(ball, &flipper->sideL, &colVec); - // ball->c.pos = addVecFl2d(colPoint, mulVecFl2d(normVecFl2d(colVec), ball->c.radius - - // EPSILON)); - touching = true; - } - if (circleLineFlIntersection(ball->c, flipper->sideR.l, false, &colPoint, &colVec)) - { - // Move ball back to not clip into the flipper - colVec = normVecFl2d(colVec); - moveBallBackFromLine(ball, &flipper->sideR, &colVec); - // ball->c.pos = addVecFl2d(colPoint, mulVecFl2d(normVecFl2d(colVec), ball->c.radius - - // EPSILON)); - touching = true; - } - if (circleCircleFlIntersection(ball->c, flipper->cPivot.c, &colPoint, &colVec)) - { - // Move ball back to not clip into the flipper - moveBallBackFromCircle(ball, &flipper->cPivot); - // ball->c.pos = addVecFl2d(colPoint, mulVecFl2d(normVecFl2d(colVec), ball->c.radius - - // EPSILON)); - touching = true; - } - if (circleCircleFlIntersection(ball->c, flipper->cTip.c, &colPoint, &colVec)) - { - // Move ball back to not clip into the flipper - moveBallBackFromCircle(ball, &flipper->cTip); - // ball->c.pos = addVecFl2d(colPoint, mulVecFl2d(normVecFl2d(colVec), ball->c.radius - - // EPSILON)); - touching = true; - } - - // If the ball is touching the flipper for the first time - if (touching && (PIN_NO_SHAPE == ballIsTouching(touchRef, flipper))) - { - // Mark them as in contact - setBallTouching(touchRef, flipper, PIN_FLIPPER); - - // Bounce the ball - vecFl_t reflVec = normVecFl2d(colVec); - ball->vel = subVecFl2d(ball->vel, mulVecFl2d(reflVec, (2 * dotVecFl2d(ball->vel, reflVec)))); - - // If the flipper is in motion - if (0 != angularVel) - { - // Get the distance between the pivot and the ball - float dist = magVecFl2d(subVecFl2d(flipper->cPivot.c.pos, ball->c.pos)); - // Convert angular velocity of the flipper to linear velocity at that point - float impulseMag = (ABS(angularVel) * dist); - - // Impart an impulse on the ball along the collision normal - ball->vel = addVecFl2d(ball->vel, mulVecFl2d(reflVec, impulseMag)); - } - } - } - } - } - - // Make sure the final angle is correct - flipper->angle = sweepEnd; - } -} - -/** - * @brief TODO - * - * @param p - */ -void moveBalls(pinball_t* p) -{ - // For each ball, check collisions with static objects - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - pbCircle_t* ball = &p->balls[bIdx]; - - // Acceleration changes velocity - // TODO adjust gravity vector when on top of a line - ball->vel = addVecFl2d(ball->vel, ball->accel); - // printf("%d,%.4f,%.4f\n", __LINE__, ball->vel.x, ball->vel.y); - - // Save the last position to check if the ball is at rest - ball->lastPos = ball->c.pos; - - // Move the ball - ball->c.pos.x += (ball->vel.x); - ball->c.pos.y += (ball->vel.y); - - // Update zone mask - // TODO update this after nudging ball too? - ball->zoneMask = pinZoneCircle(p, *ball); - } -} - -/** - * @brief TODO - * - * @param p - */ -void checkBallsNotTouching(pinball_t* p) -{ - // For each ball - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - pbCircle_t* ball = &p->balls[bIdx]; - // For each thing it could be touching - for (uint32_t tIdx = 0; tIdx < MAX_NUM_TOUCHES; tIdx++) - { - pbTouchRef_t* tr = &p->ballsTouching[bIdx][tIdx]; - // If it's touching a thing - if (NULL != tr->obj) - { - bool setNotTouching = false; - switch (tr->type) - { - case PIN_CIRCLE: - { - const pbCircle_t* other = (const pbCircle_t*)tr->obj; - if ((0 == (ball->zoneMask & other->zoneMask)) // Not in the same zone - || !circleCircleFlIntersection(ball->c, other->c, NULL, NULL)) // or not touching - { - setNotTouching = true; - } - break; - } - case PIN_LINE: - { - const pbLine_t* other = (const pbLine_t*)tr->obj; - if ((0 == (ball->zoneMask & other->zoneMask)) // Not in the same zone - || !circleLineFlIntersection(ball->c, other->l, true, NULL, NULL)) // or not touching - { - setNotTouching = true; - } - break; - } - case PIN_RECT: - { - const pbRect_t* other = (const pbRect_t*)tr->obj; - if ((0 == (ball->zoneMask & other->zoneMask)) // Not in the same zone - || !circleRectFlIntersection(ball->c, other->r, NULL)) // or not touching - { - setNotTouching = true; - } - break; - } - case PIN_FLIPPER: - { - const pbFlipper_t* flipper = (const pbFlipper_t*)tr->obj; - if ((0 == (ball->zoneMask & flipper->zoneMask)) // Not in the same zone - || !(circleCircleFlIntersection(ball->c, flipper->cPivot.c, NULL, NULL) // or not touching - || circleCircleFlIntersection(ball->c, flipper->cTip.c, NULL, NULL) - || circleLineFlIntersection(ball->c, flipper->sideL.l, false, NULL, NULL) - || circleLineFlIntersection(ball->c, flipper->sideR.l, false, NULL, NULL))) - { - setNotTouching = true; - } - break; - } - default: - case PIN_NO_SHAPE: - { - // Not touching anything... - break; - } - } - - // If the object is no longer touching - if (setNotTouching) - { - // Clear the reference - tr->obj = NULL; - tr->type = PIN_NO_SHAPE; - } - } - } - } -} - -/** - * @brief TODO - * - * @param ballTouching - * @param obj - * @param type - */ -void setBallTouching(pbTouchRef_t* ballTouching, const void* obj, pbShapeType_t type) -{ - for (uint32_t i = 0; i < MAX_NUM_TOUCHES; i++) - { - if (NULL == ballTouching->obj) - { - ballTouching->obj = obj; - ballTouching->type = type; - return; - } - } -} - -/** - * @brief TODO - * - * @param ballTouching - * @param obj - * @return pbShapeType_t - */ -pbShapeType_t ballIsTouching(pbTouchRef_t* ballTouching, const void* obj) -{ - for (uint32_t i = 0; i < MAX_NUM_TOUCHES; i++) - { - if (ballTouching->obj == obj) - { - return ballTouching->type; - } - } - return PIN_NO_SHAPE; -} - -/** - * @brief TODO - * - * @param p - */ -void checkBallsAtRest(pinball_t* p) -{ - // For each ball - for (uint32_t bIdx = 0; bIdx < p->numBalls; bIdx++) - { - pbCircle_t* ball = &p->balls[bIdx]; - - // If the ball didn't bounce this frame (which can adjust position to not clip) - if (false == ball->bounce) - { - // And the ball is traveling downward - if (ball->vel.y > 0) - { - // See how far the ball actually traveled - float velM = sqMagVecFl2d(ball->vel); - - // If the ball is moving slowly - if (velM < 1.0f) - { - // And it didn't move as much as it should have - float posDeltaM = sqMagVecFl2d(subVecFl2d(ball->c.pos, ball->lastPos)); - if ((velM - posDeltaM) > 0.01f) - { - // Stop the ball altogether to not accumulate velocity - ball->vel.x = 0; - ball->vel.y = 0; - } - } - } - } - else - { - // Clear the bounce flag - ball->bounce = false; - } - } -} - -/** - * @brief Update the position of a flipper's tip circle and line walls depending on the flipper's angle. The pivot - * circle never changes. - * - * @param f The flipper to update - */ -void updateFlipperPos(pbFlipper_t* f) -{ - // Make sure the angle is between 0 and 360 - while (f->angle < 0) - { - f->angle += (2 * M_PI); - } - while (f->angle >= (2 * M_PI)) - { - f->angle -= (2 * M_PI); - } - - // This is the set of points to rotate - vecFl_t points[] = { - { - // Center of the tip of the flipper - .x = 0, - .y = -f->length, - }, - { - // Bottom point of the right side - .x = f->cPivot.c.radius, - .y = 0, - }, - { - // Top point of the right side - .x = f->cTip.c.radius, - .y = -f->length, - }, - { - // Bottom point of the left side - .x = -f->cPivot.c.radius, - .y = 0, - }, - { - // Top point of the left side - .x = -f->cTip.c.radius, - .y = -f->length, - }, - }; - - // This is where to write the rotated points - vecFl_t* dests[] = { - &f->cTip.c.pos, &f->sideR.l.p1, &f->sideR.l.p2, &f->sideL.l.p1, &f->sideL.l.p2, - }; - - // Get the trig values for all rotations, just once - float sinA = sinf(f->angle); - float cosA = cosf(f->angle); - - // For each point - for (int32_t idx = 0; idx < ARRAY_SIZE(points); idx++) - { - // Rotate the point - float oldX = points[idx].x; - float oldY = points[idx].y; - float newX = (oldX * cosA) - (oldY * sinA); - float newY = (oldX * sinA) + (oldY * cosA); - - // Translate relative to the pivot point - dests[idx]->x = f->cPivot.c.pos.x + newX; - dests[idx]->y = f->cPivot.c.pos.y + newY; - } -} - -/** - * @brief TODO - * - * see - * https://github.com/AEFeinstein/Super-2024-Swadge-FW/blob/4d7d41d9ab0e3a7670a967a0a4cd72364a8c39ac/main/modes/pinball/pinball_physics.c - * - * @param ball - * @param line - * @param collisionVec - */ -void moveBallBackFromLine(pbCircle_t* ball, pbLine_t* line, vecFl_t* collisionNorm) -{ - // Do a bunch of work to adjust the ball's position to not clip into this line. - // First create a copy of the line - lineFl_t barrierLine = line->l; - - // Then find the normal vector to the barrier, pointed towards the ball - vecFl_t barrierOffset = mulVecFl2d(*collisionNorm, ball->c.radius); - - // Translate the along the normal vector, the distance of the radius - // This creates a line parallel to the wall where the ball's center could be - barrierLine.p1 = addVecFl2d(barrierLine.p1, barrierOffset); - barrierLine.p2 = addVecFl2d(barrierLine.p2, barrierOffset); - - // Create a line for the ball's motion - lineFl_t ballLine = { - .p1 = ball->c.pos, - .p2 = addVecFl2d(ball->c.pos, ball->vel), - }; - - // Find the intersection between where the ball's center could be and the ball's trajectory. - // Set the ball's position to that point - ball->c.pos = infLineIntersectionPoint(barrierLine, ballLine); -} - -/** - * @brief TODO - * - * @param ball - * @param fixed - */ -void moveBallBackFromCircle(pbCircle_t* ball, pbCircle_t* fixed) -{ - // Create a barrier circle around the fixed that the ball's center can't pass through - circleFl_t barrier = fixed->c; - barrier.radius += ball->c.radius; - - // Create a line for the ball's motion - lineFl_t ballLine = { - .p1 = ball->c.pos, - .p2 = addVecFl2d(ball->c.pos, ball->vel), - }; - - vecFl_t intersection_1; - vecFl_t intersection_2; - switch (circleLineFlIntersectionPoints(barrier, ballLine, &intersection_1, &intersection_2)) - { - default: - case 0: - { - // No intersection? - break; - } - case 1: - { - ball->c.pos = intersection_1; - break; - } - case 2: - { - // Two intersection points, use the one closer to ball->c.pos - float diff1 = sqMagVecFl2d(subVecFl2d(ball->c.pos, intersection_1)); - float diff2 = sqMagVecFl2d(subVecFl2d(ball->c.pos, intersection_2)); - if (diff1 < diff2) - { - ball->c.pos = intersection_1; - } - else - { - ball->c.pos = intersection_2; - } - } - } -} \ No newline at end of file diff --git a/main/modes/games/pinball/pinball_physics.h b/main/modes/games/pinball/pinball_physics.h deleted file mode 100644 index ac33bb964..000000000 --- a/main/modes/games/pinball/pinball_physics.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "mode_pinball.h" - -#define PINBALL_GRAVITY (1 / 60.0f) ///< Gravitational constant - -#define WALL_BOUNCINESS 0.5f - -#define FLIPPER_UP_DEGREES_PER_FRAME 0.296705972839036f ///< Number of degrees (17) to move a flipper up per 60fps frame -#define FLIPPER_DOWN_DEGREES_PER_FRAME \ - 0.174532925199433f ///< Number of degrees (10) to move a flipper down per 60fps frame -#define FLIPPER_UP_ANGLE 0.349065850398866f ///< Angle of a flipper (20) when actuated -#define FLIPPER_DOWN_ANGLE 0.523598775598299f ///< Angle of a flipper (30) when idle - -void updateFlipperPos(pbFlipper_t* f); -void updatePinballPhysicsFrame(pinball_t* p); diff --git a/main/modes/games/pinball/pinball_test.c b/main/modes/games/pinball/pinball_test.c deleted file mode 100644 index 3af6cb4d4..000000000 --- a/main/modes/games/pinball/pinball_test.c +++ /dev/null @@ -1,316 +0,0 @@ -//============================================================================== -// Includes -//============================================================================== - -#include -#include "pinball_test.h" -#include "pinball_zones.h" -#include "pinball_physics.h" - -//============================================================================== -// Functions -//============================================================================== - -/** - * @brief TODO - * - * @param p - * @param x - * @param y - */ -void pbCreateBall(pinball_t* p, float x, float y) -{ - pbCircle_t* ball = &p->balls[p->numBalls++]; -#define BALL_RAD 5 - ball->c.radius = (BALL_RAD); - ball->c.pos.x = x; - ball->c.pos.y = y; - ball->lastPos.x = x; - ball->lastPos.y = y; - // #define MAX_VEL 128 - ball->vel.x = 0; - ball->vel.y = 0; - ball->accel.x = 0; - ball->accel.y = PINBALL_GRAVITY; - ball->color = c500; - ball->filled = true; -} - -/** - * @brief Create balls with random positions and velocities - * - * @param p The pinball state - * @param numBalls The number of balls to create - */ -void createRandomBalls(pinball_t* p, int32_t numBalls) -{ - // Don't overflow - if (numBalls > MAX_NUM_BALLS) - { - numBalls = MAX_NUM_BALLS; - } - p->numBalls = 0; - - // Make some balls - for (int32_t i = 0; i < numBalls; i++) - { - pbCircle_t* ball = &p->balls[p->numBalls++]; -#define BALL_RAD 5 - ball->c.radius = (BALL_RAD); - ball->c.pos.x = ((BALL_RAD + 1) + (esp_random() % (TFT_WIDTH - 2 * (BALL_RAD + 1)))); - ball->c.pos.y = ((BALL_RAD + 1) + (esp_random() % (TFT_HEIGHT - 2 * (BALL_RAD + 1)))); - ball->lastPos.x = ball->c.pos.x; - ball->lastPos.y = ball->c.pos.x; - ball->vel.x = 0; - ball->vel.y = 5 / 60.0f; - ball->accel.x = 0; - ball->accel.y = PINBALL_GRAVITY; - ball->color = esp_random() % cTransparent; - ball->filled = true; - } -} - -/** - * @brief TODO - * - * @param p - * @param numBumpers - */ -void createRandomBumpers(pinball_t* p, int32_t numBumpers) -{ - int fixedBumpersPlaced = 0; - vecFl_t fixedBumpers[] = { - { - .x = 140, - .y = 120, - }, - { - .x = 100, - .y = 80, - }, - { - .x = 180, - .y = 80, - }, - }; - numBumpers += ARRAY_SIZE(fixedBumpers); - - // Don't overflow - if (numBumpers > MAX_NUM_BUMPERS) - { - numBumpers = MAX_NUM_BUMPERS; - } - p->numBumpers = 0; - - // Make some balls - while (numBumpers > p->numBumpers) - { - pbCircle_t bumper = {0}; -#define BUMPER_RAD 10 - bumper.c.radius = (BUMPER_RAD); - if (fixedBumpersPlaced < ARRAY_SIZE(fixedBumpers)) - { - bumper.c.pos = fixedBumpers[fixedBumpersPlaced]; - fixedBumpersPlaced++; - } - else - { - bumper.c.pos.x = ((BUMPER_RAD + 1) + (esp_random() % (TFT_WIDTH - 2 * (BUMPER_RAD + 1)))); - bumper.c.pos.y = ((BUMPER_RAD + 1) + (esp_random() % (TFT_HEIGHT - 2 * (BUMPER_RAD + 1)))); - } - bumper.color = c050; - bumper.filled = false; - bumper.zoneMask = pinZoneCircle(p, bumper); - - bool intersection = false; - for (int32_t ol = 0; ol < p->numWalls; ol++) - { - if (circleLineFlIntersection(bumper.c, p->walls[ol].l, true, NULL, NULL)) - { - intersection = true; - break; - } - } - - for (int32_t ob = 0; ob < p->numBumpers; ob++) - { - if (circleCircleFlIntersection(bumper.c, p->bumpers[ob].c, NULL, NULL)) - { - intersection = true; - break; - } - } - - if (!intersection) - { - memcpy(&p->bumpers[p->numBumpers], &bumper, sizeof(pbCircle_t)); - p->numBumpers++; - } - } -} - -/** - * @brief Create random static walls - * - * @param p The pinball state - * @param numWalls The number of walls to create - */ -void createRandomWalls(pinball_t* p, int32_t numWalls) -{ - // Always Create a boundary - lineFl_t corners[] = { - { - .p1 = {.x = (0), .y = (0)}, - .p2 = {.x = (TFT_WIDTH - 1), .y = (0)}, - }, - { - .p1 = {.x = (TFT_WIDTH - 1), .y = (0)}, - .p2 = {.x = (TFT_WIDTH - 1), .y = (TFT_HEIGHT - 1)}, - }, - { - .p1 = {.x = (TFT_WIDTH - 1), .y = (TFT_HEIGHT - 1)}, - .p2 = {.x = (0), .y = (TFT_HEIGHT - 1)}, - }, - { - .p1 = {.x = (0), .y = (TFT_HEIGHT - 1)}, - .p2 = {.x = (0), .y = (0)}, - }, - // { - // .p1 = {.x = 0, .y = 90}, - // .p2 = {.x = 50, .y = 110}, - // }, - // { - // .p1 = {.x = 140, .y = 70}, - // .p2 = {.x = 210, .y = 80}, - // }, - - { - .p1 = {.x = 0, .y = 120}, - .p2 = {.x = 94, .y = 188}, - }, - { - .p1 = {.x = 279, .y = 120}, - .p2 = {.x = 186, .y = 188}, - }, - }; - - // Don't overflow - if (numWalls > MAX_NUM_WALLS - ARRAY_SIZE(corners)) - { - numWalls = MAX_NUM_WALLS - ARRAY_SIZE(corners); - } - p->numWalls = 0; - - for (int32_t i = 0; i < ARRAY_SIZE(corners); i++) - { - pbLine_t* pbl = &p->walls[p->numWalls++]; - pbl->l.p1.x = corners[i].p1.x; - pbl->l.p1.y = corners[i].p1.y; - pbl->l.p2.x = corners[i].p2.x; - pbl->l.p2.y = corners[i].p2.y; - vecFl_t delta = { - .x = pbl->l.p2.x - pbl->l.p1.x, - .y = pbl->l.p2.y - pbl->l.p1.y, - }; - pbl->length = magVecFl2d(delta); - pbl->color = c555; - pbl->zoneMask = pinZoneLine(p, *pbl); - } - - // Make a bunch of random lines - while (numWalls > p->numWalls) - { - pbLine_t pbl = {0}; // = &p->walls[p->numWalls++]; - -#define L_LEN 12 - - pbl.l.p1.x = (L_LEN + (esp_random() % (TFT_WIDTH - (L_LEN * 2)))); - pbl.l.p1.y = (L_LEN + (esp_random() % (TFT_HEIGHT - (L_LEN * 2)))); - pbl.l.p2.x = pbl.l.p1.x + ((esp_random() % (L_LEN * 2)) - L_LEN); - pbl.l.p2.y = pbl.l.p1.y + ((esp_random() % (L_LEN * 2)) - L_LEN); - vecFl_t delta = { - .x = pbl.l.p2.x - pbl.l.p1.x, - .y = pbl.l.p2.y - pbl.l.p1.y, - }; - pbl.length = magVecFl2d(delta); - pbl.color = c005; // esp_random() % cTransparent; - - if (pbl.l.p1.x == pbl.l.p2.x && pbl.l.p1.y == pbl.l.p2.y) - { - if (esp_random() % 2) - { - pbl.l.p2.x = (pbl.l.p2.x) + ((1)); - } - else - { - pbl.l.p2.y = (pbl.l.p2.y) + ((1)); - } - } - - pbl.zoneMask = pinZoneLine(p, pbl); - - bool intersection = false; - for (int32_t ol = 0; ol < p->numWalls; ol++) - { - if (lineLineFlIntersection(pbl.l, p->walls[ol].l)) - { - intersection = true; - } - } - - for (int32_t ob = 0; ob < p->numBumpers; ob++) - { - if (circleLineFlIntersection(p->bumpers[ob].c, pbl.l, true, NULL, NULL)) - { - intersection = true; - } - } - - if (!intersection) - { - memcpy(&p->walls[p->numWalls], &pbl, sizeof(pbLine_t)); - p->numWalls++; - } - } -} - -/** - * @brief Create a Flipper - * - * @param p The pinball state - * @param pivot_x - * @param pivot_y - * @param facingRight - */ -void createFlipper(pinball_t* p, int32_t pivot_x, int32_t pivot_y, bool facingRight) -{ - pbFlipper_t* f = &p->flippers[p->numFlippers]; - - f->cPivot.color = c505; - f->cTip.color = c505; - f->sideL.color = c505; - f->sideR.color = c505; - - f->cPivot.c.pos.x = pivot_x; - f->cPivot.c.pos.y = pivot_y; - f->cPivot.c.radius = 10; - f->length = 40; - f->cTip.c.radius = 5; - f->facingRight = facingRight; - - f->zoneMask = pinZoneFlipper(p, f); - - // Update angle and position after setting zone - if (f->facingRight) - { - f->angle = M_PI_2 + FLIPPER_DOWN_ANGLE; - } - else - { - f->angle = M_PI + M_PI_2 - FLIPPER_DOWN_ANGLE; - } - updateFlipperPos(f); - - // Update flipper count - p->numFlippers++; -} diff --git a/main/modes/games/pinball/pinball_test.h b/main/modes/games/pinball/pinball_test.h deleted file mode 100644 index 75903a64f..000000000 --- a/main/modes/games/pinball/pinball_test.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "mode_pinball.h" - -void pbCreateBall(pinball_t* p, float x, float y); -void createRandomBalls(pinball_t* p, int32_t numBalls); -void createRandomWalls(pinball_t* p, int32_t numWalls); -void createRandomBumpers(pinball_t* p, int32_t numBumpers); -void createFlipper(pinball_t* p, int32_t pivot_x, int32_t pivot_y, bool facingRight); diff --git a/main/modes/games/pinball/pinball_zones.c b/main/modes/games/pinball/pinball_zones.c deleted file mode 100644 index a6ea07ba7..000000000 --- a/main/modes/games/pinball/pinball_zones.c +++ /dev/null @@ -1,178 +0,0 @@ -//============================================================================== -// Includes -//============================================================================== - -#include -#include "pinball_zones.h" -#include "pinball_physics.h" - -//============================================================================== -// Functions -//============================================================================== - -/** - * @brief Split a table up into zones. Each object is assigned to one or more zones for a very quick first-pass - * collision check. - * - * @param p The pinball state - */ -void createTableZones(pinball_t* p) -{ - // Split the space into zones. Start with one big rectangle - int32_t splitOffset = (NUM_ZONES >> 1); - p->zones[0].r.pos.x = 0; - p->zones[0].r.pos.y = 0; - p->zones[0].r.width = (TFT_WIDTH); - p->zones[0].r.height = (TFT_HEIGHT); - p->zones[0].color = c505; - - // While more zoning needs to happen - while (splitOffset) - { - // Iterate over current zones, back to front - for (int32_t i = NUM_ZONES - 1; i >= 0; i--) - { - // If this is a real zone - if (0 < p->zones[i].r.height) - { - // Split it either vertically or horizontally, depending on which is larger - if (p->zones[i].r.height > p->zones[i].r.width) - { - // Split vertically - int32_t newHeight_1 = p->zones[i].r.height / 2; - int32_t newHeight_2 = p->zones[i].r.height - newHeight_1; - - // Shrink the original zone - p->zones[i].r.height = newHeight_1; - - // Create the new zone - p->zones[i + splitOffset].r.height = newHeight_2; - p->zones[i + splitOffset].r.pos.y = p->zones[i].r.pos.y + p->zones[i].r.height; - - p->zones[i + splitOffset].r.width = p->zones[i].r.width; - p->zones[i + splitOffset].r.pos.x = p->zones[i].r.pos.x; - } - else - { - // Split horizontally - int32_t newWidth_1 = p->zones[i].r.width / 2; - int32_t newWidth_2 = p->zones[i].r.width - newWidth_1; - - // Shrink the original zone - p->zones[i].r.width = newWidth_1; - - // Create the new zone - p->zones[i + splitOffset].r.width = newWidth_2; - p->zones[i + splitOffset].r.pos.x = p->zones[i].r.pos.x + p->zones[i].r.width; - - p->zones[i + splitOffset].r.height = p->zones[i].r.height; - p->zones[i + splitOffset].r.pos.y = p->zones[i].r.pos.y; - } - - // Give it a random color, just because - p->zones[i + splitOffset].color = esp_random() % cTransparent; - } - } - - // Half the split offset - splitOffset /= 2; - } -} - -/** - * @brief Determine which table zones a rectangle is in - * - * @param p The pinball state - * @param r The rectangle to zone - * @return A bitmask of the zones the rectangle is in - */ -uint32_t pinZoneRect(pinball_t* p, pbRect_t rect) -{ - uint32_t zoneMask = 0; - for (int16_t z = 0; z < NUM_ZONES; z++) - { - if (rectRectFlIntersection(p->zones[z].r, rect.r, NULL)) - { - zoneMask |= (1 << z); - } - } - return zoneMask; -} - -/** - * @brief Determine which table zones a line is in - * - * @param p The pinball state - * @param l The line to zone - * @return A bitmask of the zones the line is in - */ -uint32_t pinZoneLine(pinball_t* p, pbLine_t line) -{ - uint32_t zoneMask = 0; - for (int16_t z = 0; z < NUM_ZONES; z++) - { - if (rectLineFlIntersection(p->zones[z].r, line.l, NULL)) - { - zoneMask |= (1 << z); - } - } - return zoneMask; -} - -/** - * @brief Determine which table zones a circle is in - * - * @param p The pinball state - * @param r The circle to zone - * @return A bitmask of the zones the circle is in - */ -uint32_t pinZoneCircle(pinball_t* p, pbCircle_t circ) -{ - uint32_t zoneMask = 0; - for (int16_t z = 0; z < NUM_ZONES; z++) - { - if (circleRectFlIntersection(circ.c, p->zones[z].r, NULL)) - { - zoneMask |= (1 << z); - } - } - return zoneMask; -} - -/** - * @brief Determine which table zones a flipper is in. Note, this function will modify the flipper's angle - * - * @param p The pinball state - * @param f The flipper to zone - * @return A bitmask of the zones the circle is in - */ -uint32_t pinZoneFlipper(pinball_t* p, pbFlipper_t* f) -{ - pbRect_t boundingBox = {0}; - if (f->facingRight) - { - // Record the X position - boundingBox.r.pos.x = (f->cPivot.c.pos.x - f->cPivot.c.radius); - } - else - { - // Record the X position - boundingBox.r.pos.x = (f->cPivot.c.pos.x - f->length - f->cTip.c.radius); - } - - // Width is the same when facing left and right - boundingBox.r.width = (f->length + f->cPivot.c.radius + f->cTip.c.radius); - - // Height is the same too. Move the flipper up and record the Y start - f->angle = M_PI_2 - FLIPPER_UP_ANGLE; - updateFlipperPos(f); - boundingBox.r.pos.y = (f->cTip.c.pos.y - f->cTip.c.radius); - - // Move the flipper down and record the Y end - f->angle = M_PI_2 + FLIPPER_DOWN_ANGLE; - updateFlipperPos(f); - boundingBox.r.height = (f->cTip.c.pos.y + f->cTip.c.radius) - boundingBox.r.pos.y; - - // Return the zones of the bounding box - return pinZoneRect(p, boundingBox); -} \ No newline at end of file diff --git a/main/modes/games/pinball/pinball_zones.h b/main/modes/games/pinball/pinball_zones.h deleted file mode 100644 index c080c60ae..000000000 --- a/main/modes/games/pinball/pinball_zones.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "mode_pinball.h" - -void createTableZones(pinball_t* p); -uint32_t pinZoneRect(pinball_t* p, pbRect_t r); -uint32_t pinZoneLine(pinball_t* p, pbLine_t l); -uint32_t pinZoneCircle(pinball_t* p, pbCircle_t c); -uint32_t pinZoneFlipper(pinball_t* p, pbFlipper_t* f); diff --git a/main/modes/music/jukebox/jukebox.c b/main/modes/music/jukebox/jukebox.c index 4288dbe0c..6f8957037 100644 --- a/main/modes/music/jukebox/jukebox.c +++ b/main/modes/music/jukebox/jukebox.c @@ -32,7 +32,6 @@ #include "mainMenu.h" #include "modeTimer.h" #include "mode_credits.h" -#include "mode_pinball.h" #include "touchTest.h" #include "tunernome.h" #include "midiPlayer.h" diff --git a/main/modes/system/mainMenu/mainMenu.c b/main/modes/system/mainMenu/mainMenu.c index 42f91f7a0..bba6e7269 100644 --- a/main/modes/system/mainMenu/mainMenu.c +++ b/main/modes/system/mainMenu/mainMenu.c @@ -16,7 +16,6 @@ #include "mainMenu.h" #include "modeTimer.h" #include "mode_credits.h" -#include "mode_pinball.h" #include "mode_bigbug.h" #include "mode_ssr.h" #include "mode_synth.h" @@ -154,7 +153,6 @@ static void mainMenuEnterMode(void) mainMenu->menu = startSubMenu(mainMenu->menu, "Games"); addSingleItemToMenu(mainMenu->menu, ssrMode.modeName); addSingleItemToMenu(mainMenu->menu, tttMode.modeName); - addSingleItemToMenu(mainMenu->menu, pinballMode.modeName); addSingleItemToMenu(mainMenu->menu, t48Mode.modeName); addSingleItemToMenu(mainMenu->menu, bigbugMode.modeName); mainMenu->menu = endSubMenu(mainMenu->menu); @@ -362,10 +360,6 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) { switchToSwadgeMode(&bigbugMode); } - else if (label == pinballMode.modeName) - { - switchToSwadgeMode(&pinballMode); - } else if (label == tttMode.modeName) { switchToSwadgeMode(&tttMode); diff --git a/main/utils/fl_math/vectorFl2d.c b/main/utils/fl_math/vectorFl2d.c index 10aba55c0..bc5a0b938 100644 --- a/main/utils/fl_math/vectorFl2d.c +++ b/main/utils/fl_math/vectorFl2d.c @@ -134,4 +134,19 @@ vecFl_t normVecFl2d(vecFl_t in) .y = in.y / len, }; return norm; -} \ No newline at end of file +} + +/** + * @brief Return a vector perpendicular to the input + * + * @param in The input vector + * @return The perpendicular vector + */ +vecFl_t perpendicularVecFl2d(vecFl_t in) +{ + vecFl_t perp = { + .x = -in.y, + .y = in.x, + }; + return perp; +} diff --git a/main/utils/fl_math/vectorFl2d.h b/main/utils/fl_math/vectorFl2d.h index b4cc03248..fe75eced2 100644 --- a/main/utils/fl_math/vectorFl2d.h +++ b/main/utils/fl_math/vectorFl2d.h @@ -49,5 +49,6 @@ vecFl_t rotateVecFl2d(vecFl_t vector, float radians); float magVecFl2d(vecFl_t vector); float sqMagVecFl2d(vecFl_t vector); vecFl_t normVecFl2d(vecFl_t in); +vecFl_t perpendicularVecFl2d(vecFl_t in); #endif \ No newline at end of file diff --git a/main/utils/macros.h b/main/utils/macros.h index 34793c526..23026f2a5 100644 --- a/main/utils/macros.h +++ b/main/utils/macros.h @@ -47,9 +47,9 @@ */ #ifdef __APPLE__ /* Force a compilation error if condition is true, but also produce a - result (of value 0 and type size_t), so the expression can be used - e.g. in a structure initializer (or where-ever else comma expressions - aren't permitted). */ + result (of value 0 and type size_t), so the expression can be used + e.g. in a structure initializer (or where-ever else comma expressions + aren't permitted). */ #define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int : -!!(e); })) /// Helper macro to determine the number of elements in an array. Should not be used directly @@ -79,4 +79,25 @@ */ #define POS_MODULO_ADD(a, b, d) ((a + (b % d) + d) % d) -#endif \ No newline at end of file +/** + * @brief Run timer_code every period, using tracking it with timer + * + * @param timer The accumulator variable, must persist between calls + * @param period The period at which timer_code should be run + * @param elapsed The time elapsed since this was last called + * @param timer_code The code to execute every period + */ +#define RUN_TIMER_EVERY(timer, period, elapsed, timer_code) \ + do \ + { \ + timer += elapsed; \ + while (timer > period) \ + { \ + timer -= period; \ + { \ + timer_code \ + } \ + } \ + } while (0) + +#endif diff --git a/makefile b/makefile index 21b136095..dbaf6eb27 100644 --- a/makefile +++ b/makefile @@ -324,7 +324,7 @@ assets: ./tools/assets_preprocessor/assets_preprocessor -i ./assets/ -o ./assets_image/ # To build the main file, you have to compile the objects -$(EXECUTABLE): $(OBJECTS) +$(EXECUTABLE): $(CNFS_FILE) $(OBJECTS) $(CC) $(OBJECTS) $(LIBRARY_FLAGS) -o $@ # This compiles each c file into an o file diff --git a/tools/svg-to-pinball/.gitignore b/tools/svg-to-pinball/.gitignore new file mode 100755 index 000000000..dfe5670df --- /dev/null +++ b/tools/svg-to-pinball/.gitignore @@ -0,0 +1,4 @@ +*.xml +*.xsd +*.svg +*.bin \ No newline at end of file diff --git a/tools/svg-to-pinball/.vscode/launch.json b/tools/svg-to-pinball/.vscode/launch.json new file mode 100755 index 000000000..2f59b07d4 --- /dev/null +++ b/tools/svg-to-pinball/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: svg-to-pinball.py", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/svg-to-pinball.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/tools/svg-to-pinball/.vscode/settings.json b/tools/svg-to-pinball/.vscode/settings.json new file mode 100755 index 000000000..1c83c6cb0 --- /dev/null +++ b/tools/svg-to-pinball/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "Bezier", + "Subpaths", + "svgelements" + ] +} \ No newline at end of file diff --git a/tools/svg-to-pinball/README.md b/tools/svg-to-pinball/README.md new file mode 100755 index 000000000..2798c4065 --- /dev/null +++ b/tools/svg-to-pinball/README.md @@ -0,0 +1,55 @@ +If objects aren't where they're supposed to be, svgelements may not be applying transforms correctly. Use this Inkscape plugin to apply transforms to objects before saving the SVG: https://github.com/Klowner/inkscape-applytransforms + +## File Format + +1. Number of Groups +1. Number of lines + * Line objects +1. Number of circles + * circles objects +1. Number of rectangles + * rectangles objects +1. Number of flippers + * flippers objects + +### Line +**Byte**|**Size**|**Value** +:-----:|:-----:|:-----: +0|2|ID +2|1|Group ID +3|2|p1.x +5|2|p1.y +7|2|p2.x +9|2|p2.y +11|1|Type +12|1|Push Velocity +13|1|Is Solid + +### Circle +**Byte**|**Size**|**Value** +:-----:|:-----:|:-----: +0|2|ID +2|1|Group ID +3|2|x +5|2|y +7|1|radius +8|1|Push Velocity + +### Rectangle +**Byte**|**Size**|**Value** +:-----:|:-----:|:-----: +0|2|ID +2|1|Group ID +3|2|x +5|2|y +7|2|width +9|2|height + +### Flipper +**Byte**|**Size**|**Value** +:-----:|:-----:|:-----: +0|2|x +2|2|y +4|1|Radius +5|1|Length +6|1|Facing Right \ No newline at end of file diff --git a/tools/svg-to-pinball/gentable.sh b/tools/svg-to-pinball/gentable.sh new file mode 100755 index 000000000..38168d84c --- /dev/null +++ b/tools/svg-to-pinball/gentable.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +find ../../ -iname cnfs_image* -delete +find ../../ -iname table.bin -delete +find ../../ -iname pinball.raw -delete +make -C ../cnfs/ clean +python svg-to-pinball.py +cp table.bin ../../assets/pinball.raw diff --git a/tools/svg-to-pinball/pinball.svg b/tools/svg-to-pinball/pinball.svg new file mode 100755 index 000000000..88b9b5383 --- /dev/null +++ b/tools/svg-to-pinball/pinball.svg @@ -0,0 +1,778 @@ + + + + diff --git a/tools/svg-to-pinball/requirements.txt b/tools/svg-to-pinball/requirements.txt new file mode 100755 index 000000000..56a41c393 --- /dev/null +++ b/tools/svg-to-pinball/requirements.txt @@ -0,0 +1 @@ +svgelements==1.9.6 diff --git a/tools/svg-to-pinball/svg-to-pinball.py b/tools/svg-to-pinball/svg-to-pinball.py new file mode 100755 index 000000000..5f48dd7a3 --- /dev/null +++ b/tools/svg-to-pinball/svg-to-pinball.py @@ -0,0 +1,487 @@ +from svgelements import SVG +from svgelements import Group +from svgelements import Path +from svgelements import Point +from svgelements import Circle +from svgelements import Rect +from math import sqrt, pow +from enum import Enum + +groups = [] + + +def getIntGroupId(gId: str) -> int: + try: + if gId.startswith("group_"): + gInt = int(gId.split("_")[1]) + if gInt not in groups: + groups.append(gInt) + return gInt + return 0 + except: + return 0 + + +def getIntId(id: str) -> int: + try: + return int(id) + except: + return 0 + + +class LineType(Enum): + JS_WALL = 0 + JS_SLINGSHOT = 1 + JS_DROP_TARGET = 2 + JS_STANDUP_TARGET = 3 + JS_SPINNER = 4 + JS_SCOOP = 5 + JS_BALL_LOST = 6 + JS_LAUNCH_DOOR = 7 + + +class CircleType(Enum): + JS_BUMPER = 0 + JS_ROLLOVER = 1 + + +class PointType(Enum): + JS_BALL_SPAWN = 0 + JS_ITEM_SPAWN = 1 + + +class xyPoint: + def __init__(self, p: Point = None, x: int = 0, y: int = 0) -> None: + if p is not None: + self.x: int = int(p.x) + self.y: int = int(p.y) + else: + self.x: int = int(x) + self.y: int = int(y) + + def toBytes(self) -> bytearray: + return bytearray( + [(self.x >> 8) & 0xFF, self.x & 0xFF, (self.y >> 8) & 0xFF, self.y & 0xFF] + ) + + def __eq__(self, other: object) -> bool: + return self.x == other.x and self.y == other.y + + +class pbPoint: + def __init__(self, pos: xyPoint, type: LineType, gId: str, id: str) -> None: + self.pos = pos + self.type: int = type.value + self.gId: int = getIntGroupId(gId) + self.id: int = getIntId(id) + + def toBytes(self) -> bytearray: + b = bytearray([(self.id >> 8), self.id & 0xFF, self.gId]) + b.extend(self.pos.toBytes()) + b.append(self.type) + return b + + +class pbLine: + def __init__( + self, p1: xyPoint, p2: xyPoint, type: LineType, gId: str, id: str + ) -> None: + self.p1 = p1 + self.p2 = p2 + self.type: int = type.value + self.gId: int = getIntGroupId(gId) + self.id: int = getIntId(id) + + match (type): + case LineType.JS_WALL: + self.isSolid = True + self.pushVel = 0 + pass + case LineType.JS_SLINGSHOT: + self.isSolid = True + self.pushVel = 80 + pass + case LineType.JS_DROP_TARGET: + self.isSolid = True + self.pushVel = 40 + pass + case LineType.JS_STANDUP_TARGET: + self.isSolid = True + self.pushVel = 0 + pass + case LineType.JS_SPINNER: + self.isSolid = False + self.pushVel = 0 + pass + case LineType.JS_SCOOP: + self.isSolid = True + self.pushVel = 0 + pass + case LineType.JS_BALL_LOST: + self.isSolid = True + self.pushVel = 0 + pass + case LineType.JS_LAUNCH_DOOR: + self.isSolid = False + self.pushVel = 0 + + def __str__(self) -> str: + return "{.p1 = {.x = %d, .y = %d}, .p2 = {.x = %d, .y = %d}}," % ( + self.p1.x, + self.p1.y, + self.p2.x, + self.p2.y, + ) + + def toBytes(self) -> bytearray: + b = bytearray([(self.id >> 8), self.id & 0xFF, self.gId]) + b.extend(self.p1.toBytes()) + b.extend(self.p2.toBytes()) + b.append(self.type) + b.append(self.pushVel) + b.append(self.isSolid) + # print(' '.join(['%02X' % x for x in b])) + return b + + +class pbCircle: + def __init__( + self, + pos: xyPoint, + radius: int, + type: CircleType, + pushVel: int, + gId: str, + id: str, + ) -> None: + self.position = pos + self.radius = int(radius) + self.type = type.value + self.gId: int = getIntGroupId(gId) + self.id: int = getIntId(id) + self.pushVel = int(pushVel) + + def __str__(self) -> str: + return "{.pos = {.x = %d, .y = %d}, .radius = %d}," % ( + self.position.x, + self.position.y, + self.radius, + ) + + def toBytes(self) -> bytearray: + b = bytearray([(self.id >> 8), self.id & 0xFF, self.gId]) + b.extend(self.position.toBytes()) + b.append(self.radius) + b.append(self.type) + b.append(self.pushVel) + # print(' '.join(['%02X' % x for x in b])) + return b + + +class pbRectangle: + def __init__(self, position: xyPoint, size: xyPoint, gId: str, id: str) -> None: + self.position = position + self.size = size + self.gId: int = getIntGroupId(gId) + self.id: int = getIntId(id) + + def __str__(self) -> str: + return "{.pos = {.x = %d, .y = %d}, .width = %d, .height = %d}," % ( + self.position.x, + self.position.y, + self.size.x, + self.size.y, + ) + + def toBytes(self) -> bytearray: + b = bytearray([(self.id >> 8), self.id & 0xFF, self.gId]) + b.extend(self.position.toBytes()) + b.extend(self.size.toBytes()) + # print(' '.join(['%02X' % x for x in b])) + return b + + +class pbTriangle: + def __init__(self, vertices: list[xyPoint], gId: str, id: str) -> None: + self.vertices = vertices + self.gId: int = getIntGroupId(gId) + self.id: int = getIntId(id) + + def toBytes(self) -> bytearray: + b = bytearray([(self.id >> 8), self.id & 0xFF, self.gId]) + for point in self.vertices: + b.extend(point.toBytes()) + return b + + +class pbFlipper: + def __init__( + self, pivot: xyPoint, radius: int, length: int, facingRight: bool + ) -> None: + self.pivot = pivot + self.radius = int(radius) + self.length = int(length) + self.facingRight = bool(facingRight) + + def __str__(self) -> str: + return ( + "{.cPivot = {.pos = {.x = %d, .y = %d}, .radius = %d}, .len = %d, .facingRight = %s}," + % ( + self.pivot.x, + self.pivot.y, + self.radius, + self.length, + "true" if self.facingRight else "false", + ) + ) + + def toBytes(self) -> bytearray: + b = bytearray() + b.extend(self.pivot.toBytes()) + b.append(self.radius) + b.append(self.length) + b.append(self.facingRight) + # print(' '.join(['%02X' % x for x in b])) + return b + + +def extractCircles(gs: list, type: CircleType, gId: str) -> list[pbCircle]: + """Recursively extract all circles from this list of SVG things + + Args: + gs (list): A list that contains Group and Circle + + Returns: + list[str]: A list of C strings for the circles + """ + circles = [] + for g in gs: + if isinstance(g, Circle): + circles.append( + pbCircle( + xyPoint(x=g.cx, y=g.cy), (g.rx + g.ry) / 2, type, 120, gId, g.id + ) + ) + elif isinstance(g, Group): + circles.extend(extractCircles(g, type, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Circles") + return circles + + +def extractPoints(gs: list, type: PointType, gId: str) -> list[pbPoint]: + """Recursively extract all points from this list of SVG things + + Args: + gs (list): A list that contains Group and Point + + Returns: + list[str]: A list of C strings for the points + """ + points = [] + for g in gs: + if isinstance(g, Circle): + points.append(pbPoint(xyPoint(x=g.cx, y=g.cy), type, gId, g.id)) + elif isinstance(g, Group): + points.extend(extractPoints(g, type, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Points") + return points + + +def extractRectangles(gs: list, gId: str) -> list[pbRectangle]: + """Recursively extract all circles from this list of SVG things + + Args: + gs (list): A list that contains Group and Circle + + Returns: + list[str]: A list of C strings for the circles + """ + rectangles = [] + for g in gs: + if isinstance(g, Rect): + rectangles.append( + pbRectangle( + xyPoint(x=g.x, y=g.y), xyPoint(x=g.width, y=g.height), gId, g.id + ) + ) + elif isinstance(g, Group): + rectangles.extend(extractRectangles(g, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Rects") + return rectangles + + +def extractPaths(gs: list, lineType: LineType, gId: str) -> list[pbLine]: + """Recursively extract all paths from this list of SVG things + + Args: + gs (list): A list that contains Group and Path + + Returns: + list[str]: A list of C strings for the path segments + """ + lines = [] + for g in gs: + if isinstance(g, Path): + lastPoint: Point = None + point: Point + for point in g.as_points(): + if lastPoint is not None and lastPoint != point: + lines.append( + pbLine( + xyPoint(p=lastPoint), xyPoint(p=point), lineType, gId, g.id + ) + ) + lastPoint = point + elif isinstance(g, Group): + lines.extend(extractPaths(g, lineType, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Paths") + return lines + + +def extractTriangles(gs: list, gId: str) -> list[pbTriangle]: + """Recursively extract all triangles from this list of SVG things + + Args: + gs (list): A list that contains Group and Path + + Returns: + list[str]: A list of C strings for the path segments + """ + triangles = [] + vertices = [] + for g in gs: + if isinstance(g, Path): + point: Point + for point in g.as_points(): + pbp = xyPoint(p=point) + + if 3 == len(vertices) and pbp == vertices[0]: + # Save the triangle + triangles.append(pbTriangle(vertices, gId, g.id)) + # Start a new one + vertices = [] + elif len(vertices) == 0 or pbp != vertices[-1]: + vertices.append(pbp) + elif isinstance(g, Group): + triangles.extend(extractTriangles(g, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Triangles") + return triangles + + +def extractFlippers(gs: list, gId: str) -> list[pbFlipper]: + """Recursively extract all flippers (groups of circles and paths) from this list of SVG things + + Args: + gs (list): A list that contains stuff + + Returns: + list[str]: A list of C strings for the path segments + """ + lines = [] + flipperParts: list[Circle] = [] + for g in gs: + if isinstance(g, Circle): + flipperParts.append(g) + elif isinstance(g, Path): + pass + elif isinstance(g, Group): + lines.extend(extractFlippers(g, g.id)) + else: + print("Found " + str(type(g)) + " when extracting Flippers") + + if 2 == len(flipperParts): + if "pivot" in flipperParts[0].id.lower(): + pivot = flipperParts[0] + tip = flipperParts[1] + else: + pivot = flipperParts[1] + tip = flipperParts[0] + + if pivot.cx < tip.cx: + facingRight = True + else: + facingRight = False + + flipperLen = sqrt(pow(pivot.cx - tip.cx, 2) + pow(pivot.cy - tip.cy, 2)) + + lines.append( + pbFlipper( + xyPoint(x=pivot.cx, y=pivot.cy), pivot.rx, flipperLen, facingRight + ) + ) + + return lines + + +def addLength(tableData: bytearray, array: int): + length = len(array) + b = [(length >> 8) & 0xFF, (length) & 0xFF] + tableData.extend(b) + # print(' '.join(['%02X' % x for x in b])) + + +def main(): + # Load the SVG + g: Group = SVG().parse("pinball.svg") + + lines: list[pbLine] = [] + lines.extend(extractPaths(g.objects["350_Walls"], LineType.JS_WALL, None)) + lines.extend(extractPaths(g.objects["300_Scoops"], LineType.JS_SCOOP, None)) + lines.extend(extractPaths(g.objects["250_Slingshots"], LineType.JS_SLINGSHOT, None)) + lines.extend(extractPaths(g.objects["200_Drop_Targets"], LineType.JS_DROP_TARGET, None)) + lines.extend( + extractPaths(g.objects["150_Standup_Targets"], LineType.JS_STANDUP_TARGET, None) + ) + lines.extend(extractPaths(g.objects["400_Ball_Lost"], LineType.JS_BALL_LOST, None)) + lines.extend(extractPaths(g.objects["650_Launch_Door"], LineType.JS_LAUNCH_DOOR, None)) + + circles: list[pbCircle] = [] + circles.extend(extractCircles(g.objects["450_Rollovers"], CircleType.JS_ROLLOVER, None)) + circles.extend(extractCircles(g.objects["500_Bumpers"], CircleType.JS_BUMPER, None)) + + points: list[pbPoint] = [] + points.extend(extractPoints(g.objects["050_Ball_Spawn"], PointType.JS_BALL_SPAWN, None)) + points.extend(extractPoints(g.objects["100_Item_Spawn"], PointType.JS_ITEM_SPAWN, None)) + + launchers = extractRectangles(g.objects["600_Launchers"], None) + flippers = extractFlippers(g.objects["550_Flippers"], None) + triangles = extractTriangles(g.objects["000_Indicators"], None) + + tableData: bytearray = bytearray() + tableData.append(max(groups)) + + addLength(tableData, lines) + for line in sorted(lines, key=lambda x: x.id): + tableData.extend(line.toBytes()) + + addLength(tableData, circles) + for circle in sorted(circles, key=lambda x: x.id): + tableData.extend(circle.toBytes()) + + addLength(tableData, launchers) + for launcher in sorted(launchers, key=lambda x: x.id): + tableData.extend(launcher.toBytes()) + + addLength(tableData, flippers) + for flipper in flippers: + tableData.extend(flipper.toBytes()) + + addLength(tableData, triangles) + for triangle in sorted(triangles, key=lambda x: x.id): + tableData.extend(triangle.toBytes()) + + addLength(tableData, points) + for point in sorted(points, key=lambda x: x.id): + tableData.extend(point.toBytes()) + + with open("table.bin", "wb") as outFile: + outFile.write(tableData) + + +if __name__ == "__main__": + main() From 05538d2bfcf9f9b8ec1d9fece415f81540dabf95 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Thu, 19 Sep 2024 10:57:41 +0000 Subject: [PATCH 24/54] Add SSR to emulator modes list --- emulator/src/extensions/modes/ext_modes.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/emulator/src/extensions/modes/ext_modes.c b/emulator/src/extensions/modes/ext_modes.c index 8bc83d92a..8916edfbf 100644 --- a/emulator/src/extensions/modes/ext_modes.c +++ b/emulator/src/extensions/modes/ext_modes.c @@ -29,6 +29,7 @@ #include "mode_2048.h" #include "mode_bigbug.h" #include "mode_credits.h" +#include "mode_ssr.h" #include "mode_synth.h" #include "touchTest.h" #include "tunernome.h" @@ -70,6 +71,7 @@ static swadgeMode_t* allSwadgeModes[] = { &keebTestMode, &mainMenuMode, &modeCredits, + &ssrMode, &synthMode, &t48Mode, &timerMode, From a12d8fc711a7d387c2f8b5003ed4472a44191a26 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Thu, 19 Sep 2024 23:48:47 +0000 Subject: [PATCH 25/54] Rename to Swadge Hero --- emulator/src/extensions/modes/ext_modes.c | 4 +- main/CMakeLists.txt | 4 +- .../mode_swadgeHero.c} | 154 +++++++++--------- .../mode_swadgeHero.h} | 2 +- main/modes/system/mainMenu/mainMenu.c | 8 +- 5 files changed, 86 insertions(+), 86 deletions(-) rename main/modes/games/{ssr/mode_ssr.c => swadgeHero/mode_swadgeHero.c} (78%) rename main/modes/games/{ssr/mode_ssr.h => swadgeHero/mode_swadgeHero.h} (52%) diff --git a/emulator/src/extensions/modes/ext_modes.c b/emulator/src/extensions/modes/ext_modes.c index 8916edfbf..970b6d0b7 100644 --- a/emulator/src/extensions/modes/ext_modes.c +++ b/emulator/src/extensions/modes/ext_modes.c @@ -29,7 +29,7 @@ #include "mode_2048.h" #include "mode_bigbug.h" #include "mode_credits.h" -#include "mode_ssr.h" +#include "mode_swadgeHero.h" #include "mode_synth.h" #include "touchTest.h" #include "tunernome.h" @@ -71,7 +71,7 @@ static swadgeMode_t* allSwadgeModes[] = { &keebTestMode, &mainMenuMode, &modeCredits, - &ssrMode, + &swadgeHeroMode, &synthMode, &t48Mode, &timerMode, diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 6293289de..d2c5d3c14 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -30,7 +30,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "modes/games/bigbug/soundManager_bigbug.c" "modes/games/bigbug/tilemap_bigbug.c" "modes/games/bigbug/pathfinding_bigbug.c" - "modes/games/ssr/mode_ssr.c" + "modes/games/swadgeHero/mode_swadgeHero.c" "modes/games/ultimateTTT/ultimateTTT.c" "modes/games/ultimateTTT/ultimateTTTgame.c" "modes/games/ultimateTTT/ultimateTTThowTo.c" @@ -103,7 +103,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./modes" "./modes/games" "./modes/games/bigbug" - "./modes/games/ssr" + "./modes/games/swadgeHero" "./modes/games/ultimateTTT" "./modes/games/2048/" "./modes/music" diff --git a/main/modes/games/ssr/mode_ssr.c b/main/modes/games/swadgeHero/mode_swadgeHero.c similarity index 78% rename from main/modes/games/ssr/mode_ssr.c rename to main/modes/games/swadgeHero/mode_swadgeHero.c index 50c995b80..aab7c0774 100644 --- a/main/modes/games/ssr/mode_ssr.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -2,7 +2,7 @@ // Defines //============================================================================== -#include "mode_ssr.h" +#include "mode_swadgeHero.h" //============================================================================== // Defines @@ -23,7 +23,7 @@ typedef struct int32_t tick; int32_t note; int32_t hold; -} ssrNote_t; +} shNote_t; typedef struct { @@ -32,7 +32,7 @@ typedef struct int32_t headPosY; int32_t tailPosY; bool held; -} ssrNoteIcon_t; +} shNoteIcon_t; typedef struct { @@ -45,52 +45,52 @@ typedef struct // Track data int32_t numNotes; - ssrNote_t* notes; + shNote_t* notes; int32_t cNote; // Drawing data list_t icons; buttonBit_t btnState; -} ssrVars_t; +} shVars_t; //============================================================================== // Function Declarations //============================================================================== -static void ssrEnterMode(void); -static void ssrExitMode(void); -static void ssrMainLoop(int64_t elapsedUs); -static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); -static void ssrLoadTrackData(ssrVars_t* ssr, const uint8_t* data, size_t size); -// static void ssrMenuCb(const char*, bool selected, uint32_t settingVal); +static void shEnterMode(void); +static void shExitMode(void); +static void shMainLoop(int64_t elapsedUs); +static void shBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); +static void shLoadTrackData(shVars_t* sh, const uint8_t* data, size_t size); +// static void shMenuCb(const char*, bool selected, uint32_t settingVal); static uint32_t btnToNote(buttonBit_t btn); -static void ssrRunTimers(ssrVars_t* ssrv, uint32_t elapsedUs); -static void ssrDrawGame(ssrVars_t* ssrv); +static void shRunTimers(shVars_t* shv, uint32_t elapsedUs); +static void shDrawGame(shVars_t* shv); //============================================================================== // Variables //============================================================================== -static const char ssrName[] = "Swadge Swadge Rebellion"; +static const char shName[] = "Swadge Hero"; -swadgeMode_t ssrMode = { - .modeName = ssrName, +swadgeMode_t swadgeHeroMode = { + .modeName = shName, .wifiMode = ESP_NOW, .overrideUsb = false, .usesAccelerometer = true, .usesThermometer = false, .overrideSelectBtn = false, - .fnEnterMode = ssrEnterMode, - .fnExitMode = ssrExitMode, - .fnMainLoop = ssrMainLoop, + .fnEnterMode = shEnterMode, + .fnExitMode = shExitMode, + .fnMainLoop = shMainLoop, .fnAudioCallback = NULL, - .fnBackgroundDrawCallback = ssrBackgroundDrawCallback, + .fnBackgroundDrawCallback = shBackgroundDrawCallback, .fnEspNowRecvCb = NULL, .fnEspNowSendCb = NULL, .fnAdvancedUSB = NULL, }; -ssrVars_t* ssr; +shVars_t* sh; static const paletteColor_t colors[] = {c020, c400, c550, c004, c420, c222}; static const buttonBit_t noteToBtn[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; @@ -103,53 +103,53 @@ static const buttonBit_t noteToBtn[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, * This function is called when this mode is started. It should initialize * variables and start the mode. */ -static void ssrEnterMode(void) +static void shEnterMode(void) { // 60FPS please setFrameRateUs(16667); // Allocate mode memory - ssr = calloc(1, sizeof(ssrVars_t)); + sh = calloc(1, sizeof(shVars_t)); // Load a font - loadFont("ibm_vga8.font", &ssr->ibm, false); + loadFont("ibm_vga8.font", &sh->ibm, false); // Load the track data size_t sz = 0; - ssrLoadTrackData(ssr, cnfsGetFile("credits.cch", &sz), sz); + shLoadTrackData(sh, cnfsGetFile("credits.cch", &sz), sz); // Load the MIDI file - loadMidiFile("credits.mid", &ssr->credits, false); - globalMidiPlayerPlaySong(&ssr->credits, MIDI_BGM); + loadMidiFile("credits.mid", &sh->credits, false); + globalMidiPlayerPlaySong(&sh->credits, MIDI_BGM); globalMidiPlayerPauseAll(); // Set the lead-in timer - ssr->leadInUs = TRAVEL_TIME_US; + sh->leadInUs = TRAVEL_TIME_US; } /** * This function is called when the mode is exited. It should free any allocated memory. */ -static void ssrExitMode(void) +static void shExitMode(void) { // Free MIDI data - unloadMidiFile(&ssr->credits); + unloadMidiFile(&sh->credits); // Free track data - free(ssr->notes); + free(sh->notes); // Free UI data void* val; - while ((val = pop(&ssr->icons))) + while ((val = pop(&sh->icons))) { free(val); } // Free the font - freeFont(&ssr->ibm); + freeFont(&sh->ibm); // Free mode memory - free(ssr); + free(sh); } /** @@ -159,21 +159,21 @@ static void ssrExitMode(void) * @param elapsedUs The time elapsed since the last time this function was called. Use this value to determine when * it's time to do things */ -static void ssrMainLoop(int64_t elapsedUs) +static void shMainLoop(int64_t elapsedUs) { // Process button events buttonEvt_t evt = {0}; while (checkButtonQueueWrapper(&evt)) { - ssr->btnState = evt.state; + sh->btnState = evt.state; if (evt.down) { // Iterate through all currently shown icons - node_t* iconNode = ssr->icons.first; + node_t* iconNode = sh->icons.first; while (iconNode) { - ssrNoteIcon_t* icon = iconNode->val; + shNoteIcon_t* icon = iconNode->val; // If the icon matches the button if (icon->note == btnToNote(evt.button)) @@ -230,7 +230,7 @@ static void ssrMainLoop(int64_t elapsedUs) { // No tail, remove the icon node_t* nextNode = iconNode->next; - removeEntry(&ssr->icons, iconNode); + removeEntry(&sh->icons, iconNode); iconNode = nextNode; } } @@ -250,14 +250,14 @@ static void ssrMainLoop(int64_t elapsedUs) } // Run a lead-in timer to allow notes to spawn before the song starts playing - if (ssr->leadInUs > 0) + if (sh->leadInUs > 0) { - ssr->leadInUs -= elapsedUs; + sh->leadInUs -= elapsedUs; - if (ssr->leadInUs <= 0) + if (sh->leadInUs <= 0) { globalMidiPlayerResumeAll(); - ssr->leadInUs = 0; + sh->leadInUs = 0; } } @@ -266,9 +266,9 @@ static void ssrMainLoop(int64_t elapsedUs) // Get the position of the song and when the next event is, in ms int32_t songUs; - if (ssr->leadInUs > 0) + if (sh->leadInUs > 0) { - songUs = -ssr->leadInUs; + songUs = -sh->leadInUs; } else { @@ -276,24 +276,24 @@ static void ssrMainLoop(int64_t elapsedUs) } // Check events until one hasn't happened yet or the song ends - while (ssr->cNote < ssr->numNotes) + while (sh->cNote < sh->numNotes) { // When the next event occurs - int32_t nextEventUs = MIDI_TICKS_TO_US(ssr->notes[ssr->cNote].tick, player->tempo, player->reader.division); + int32_t nextEventUs = MIDI_TICKS_TO_US(sh->notes[sh->cNote].tick, player->tempo, player->reader.division); // Check if the icon should be spawned now to reach the hit bar in time if (songUs + TRAVEL_TIME_US >= nextEventUs) { // Spawn an icon - ssrNoteIcon_t* ni = calloc(1, sizeof(ssrNoteIcon_t)); - ni->note = ssr->notes[ssr->cNote].note; + shNoteIcon_t* ni = calloc(1, sizeof(shNoteIcon_t)); + ni->note = sh->notes[sh->cNote].note; ni->headPosY = TFT_HEIGHT + (ICON_RADIUS * 2); // If this is a hold note - if (ssr->notes[ssr->cNote].hold) + if (sh->notes[sh->cNote].hold) { // Figure out at what microsecond the tail ends - int32_t tailUs = MIDI_TICKS_TO_US(ssr->notes[ssr->cNote].hold, player->tempo, player->reader.division); + int32_t tailUs = MIDI_TICKS_TO_US(sh->notes[sh->cNote].hold, player->tempo, player->reader.division); // Convert the time to a number of pixels int32_t tailPx = tailUs / TRAVEL_US_PER_PX; // Add the length pixels to the head to get the tail @@ -309,10 +309,10 @@ static void ssrMainLoop(int64_t elapsedUs) ni->timer = 0; // Push into the list of icons - push(&ssr->icons, ni); + push(&sh->icons, ni); // Increment the track data - ssr->cNote++; + sh->cNote++; } else { @@ -321,8 +321,8 @@ static void ssrMainLoop(int64_t elapsedUs) } } - ssrRunTimers(ssr, elapsedUs); - ssrDrawGame(ssr); + shRunTimers(sh, elapsedUs); + shDrawGame(sh); // Check for analog touch // int32_t centerVal, intensityVal; @@ -353,20 +353,20 @@ static void ssrMainLoop(int64_t elapsedUs) /** * @brief TODO * - * @param ssrv + * @param shv * @param elapsedUs */ -static void ssrRunTimers(ssrVars_t* ssrv, uint32_t elapsedUs) +static void shRunTimers(shVars_t* shv, uint32_t elapsedUs) { // Track if an icon was removed bool removed = false; // Run all the icon timers - node_t* iconNode = ssrv->icons.first; + node_t* iconNode = shv->icons.first; while (iconNode) { // Get a reference - ssrNoteIcon_t* icon = iconNode->val; + shNoteIcon_t* icon = iconNode->val; // Run this icon's timer icon->timer += elapsedUs; @@ -413,7 +413,7 @@ static void ssrRunTimers(ssrVars_t* ssrv, uint32_t elapsedUs) { // Remove this icon free(iconNode->val); - removeEntry(&ssrv->icons, iconNode); + removeEntry(&shv->icons, iconNode); // Stop the while timer loop removed = true; @@ -438,9 +438,9 @@ static void ssrRunTimers(ssrVars_t* ssrv, uint32_t elapsedUs) /** * @brief TODO * - * @param ssrv + * @param shv */ -static void ssrDrawGame(ssrVars_t* ssrv) +static void shDrawGame(shVars_t* shv) { // Clear the display clearPxTft(); @@ -454,11 +454,11 @@ static void ssrDrawGame(ssrVars_t* ssrv) } // Draw all the icons - node_t* iconNode = ssrv->icons.first; + node_t* iconNode = shv->icons.first; while (iconNode) { // Draw the icon - ssrNoteIcon_t* icon = iconNode->val; + shNoteIcon_t* icon = iconNode->val; int32_t xOffset = ((icon->note * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); drawCircleFilled(xOffset, icon->headPosY, ICON_RADIUS, colors[icon->note]); @@ -476,7 +476,7 @@ static void ssrDrawGame(ssrVars_t* ssrv) // Draw indicators that the button is pressed for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(noteToBtn); bIdx++) { - if (ssrv->btnState & noteToBtn[bIdx]) + if (shv->btnState & noteToBtn[bIdx]) { int32_t xOffset = ((bIdx * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[bIdx]); @@ -495,7 +495,7 @@ static void ssrDrawGame(ssrVars_t* ssrv) * @param up update number * @param numUp update number denominator */ -static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +static void shBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) { // fillDisplayArea(x, y, x + w, y + h, c555); } @@ -507,7 +507,7 @@ static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h * @param selected true if the item was selected, false if it was moved to * @param settingVal The value of the setting, if the menu item is a settings item */ -// static void ssrMenuCb(const char* label, bool selected, uint32_t settingVal) +// static void shMenuCb(const char* label, bool selected, uint32_t settingVal) // { // printf("%s %s\n", label, selected ? "selected" : "scrolled to"); @@ -522,30 +522,30 @@ static void ssrBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h * @param data * @param size */ -static void ssrLoadTrackData(ssrVars_t* ssrv, const uint8_t* data, size_t size) +static void shLoadTrackData(shVars_t* shv, const uint8_t* data, size_t size) { uint32_t dIdx = 0; - ssrv->numNotes = (data[dIdx++] << 8); - ssrv->numNotes |= (data[dIdx++]); + shv->numNotes = (data[dIdx++] << 8); + shv->numNotes |= (data[dIdx++]); - ssrv->notes = calloc(ssrv->numNotes, sizeof(ssrNote_t)); + shv->notes = calloc(shv->numNotes, sizeof(shNote_t)); - for (int32_t nIdx = 0; nIdx < ssrv->numNotes; nIdx++) + for (int32_t nIdx = 0; nIdx < shv->numNotes; nIdx++) { - ssrv->notes[nIdx].tick = (data[dIdx + 0] << 24) | // + shv->notes[nIdx].tick = (data[dIdx + 0] << 24) | // (data[dIdx + 1] << 16) | // (data[dIdx + 2] << 8) | // (data[dIdx + 3] << 0); dIdx += 4; - ssrv->notes[nIdx].note = data[dIdx++]; + shv->notes[nIdx].note = data[dIdx++]; - if (0x80 & ssrv->notes[nIdx].note) + if (0x80 & shv->notes[nIdx].note) { - ssrv->notes[nIdx].note &= 0x7F; + shv->notes[nIdx].note &= 0x7F; // Use the hold time to see when this note ends - ssrv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // + shv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // (data[dIdx + 1] << 0); dIdx += 2; } diff --git a/main/modes/games/ssr/mode_ssr.h b/main/modes/games/swadgeHero/mode_swadgeHero.h similarity index 52% rename from main/modes/games/ssr/mode_ssr.h rename to main/modes/games/swadgeHero/mode_swadgeHero.h index f130b6638..d328e338e 100644 --- a/main/modes/games/ssr/mode_ssr.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -2,4 +2,4 @@ #include -extern swadgeMode_t ssrMode; \ No newline at end of file +extern swadgeMode_t swadgeHeroMode; \ No newline at end of file diff --git a/main/modes/system/mainMenu/mainMenu.c b/main/modes/system/mainMenu/mainMenu.c index bba6e7269..efe0b4d7e 100644 --- a/main/modes/system/mainMenu/mainMenu.c +++ b/main/modes/system/mainMenu/mainMenu.c @@ -17,7 +17,7 @@ #include "modeTimer.h" #include "mode_credits.h" #include "mode_bigbug.h" -#include "mode_ssr.h" +#include "mode_swadgeHero.h" #include "mode_synth.h" #include "ultimateTTT.h" #include "touchTest.h" @@ -151,7 +151,7 @@ static void mainMenuEnterMode(void) // Add single items mainMenu->menu = startSubMenu(mainMenu->menu, "Games"); - addSingleItemToMenu(mainMenu->menu, ssrMode.modeName); + addSingleItemToMenu(mainMenu->menu, swadgeHeroMode.modeName); addSingleItemToMenu(mainMenu->menu, tttMode.modeName); addSingleItemToMenu(mainMenu->menu, t48Mode.modeName); addSingleItemToMenu(mainMenu->menu, bigbugMode.modeName); @@ -364,9 +364,9 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) { switchToSwadgeMode(&tttMode); } - else if (label == ssrMode.modeName) + else if (label == swadgeHeroMode.modeName) { - switchToSwadgeMode(&ssrMode); + switchToSwadgeMode(&swadgeHeroMode); } else if (label == timerMode.modeName) { From 1489823e207db929cc1317d7aa75ee5bea53514f Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Fri, 20 Sep 2024 01:17:30 +0000 Subject: [PATCH 26/54] Start work on difficulties --- assets/credits.chart | 302 ------------------ assets/credits.mid | Bin 5769 -> 5403 bytes assets/credits.mid.bak | Bin 0 -> 5769 bytes assets/credits_e.chart | 140 ++++++++ assets/credits_h.chart | 289 +++++++++++++++++ assets/credits_m.chart | 217 +++++++++++++ main/modes/games/swadgeHero/mode_swadgeHero.c | 66 ++-- 7 files changed, 693 insertions(+), 321 deletions(-) delete mode 100644 assets/credits.chart create mode 100644 assets/credits.mid.bak create mode 100644 assets/credits_e.chart create mode 100644 assets/credits_h.chart create mode 100644 assets/credits_m.chart diff --git a/assets/credits.chart b/assets/credits.chart deleted file mode 100644 index 17471ecb9..000000000 --- a/assets/credits.chart +++ /dev/null @@ -1,302 +0,0 @@ -[Song] -{ - Name = " "SWADGE Credits New FINAL Sync3"" - Charter = "Edward Haas" - Year = "Unknown" - Offset = 0 - Resolution = 960 - Player2 = bass - Difficulty = 4 - PreviewStart = 0 - PreviewEnd = 10000 - Genre = "rock" - MediaType = "cd" - MusicStream = "song.ogg" -} -[SyncTrack] -{ - 0 = TS 4 - 0 = B 120000 - 0 = TS 4 - 0 = B 125000 -} -[ExpertSingle] -{ - 4040 = N 1 0 - 4280 = N 1 0 - 4540 = N 1 0 - 5540 = N 1 0 - 5800 = N 1 0 - 6060 = N 1 0 - 6560 = N 1 0 - 8100 = N 2 0 - 9100 = N 0 0 - 9320 = N 1 0 - 9580 = N 2 0 - 9780 = N 3 0 - 10080 = N 4 0 - 10600 = N 2 0 - 10740 = N 3 0 - 10840 = N 2 0 - 11100 = N 1 0 - 11600 = N 0 0 - 12100 = N 4 0 - 12900 = N 2 0 - 13160 = N 2 1460 - 16140 = N 1 0 - 16640 = N 1 0 - 16900 = N 2 0 - 17140 = N 3 0 - 17900 = N 4 0 - 18160 = N 0 0 - 19160 = N 4 0 - 19420 = N 3 0 - 19660 = N 2 0 - 19940 = N 0 0 - 20160 = N 1 0 - 20940 = N 2 0 - 21180 = N 3 1480 - 23200 = N 2 0 - 23700 = N 1 0 - 24200 = N 0 0 - 24720 = N 0 0 - 24960 = N 1 0 - 25220 = N 2 0 - 25960 = N 3 0 - 26220 = N 2 0 - 26720 = N 1 0 - 27220 = N 4 0 - 27740 = N 2 0 - 28240 = N 2 0 - 28740 = N 1 0 - 28880 = N 2 0 - 29000 = N 1 0 - 29240 = N 0 0 - 29740 = N 1 0 - 30260 = N 2 0 - 31260 = N 3 0 - 32020 = N 4 0 - 32260 = N 2 1040 - 33780 = N 4 0 - 33780 = E Bad_Too_High - 34040 = N 3 0 - 34280 = N 2 0 - 34780 = N 1 0 - 34960 = N 2 0 - 35120 = N 1 0 - 35300 = N 0 0 - 35800 = N 1 0 - 36300 = N 3 1060 - 37840 = N 4 0 - 38100 = N 3 0 - 38320 = N 2 0 - 40360 = N 1 0 - 41360 = N 0 0 - 41580 = N 1 0 - 41860 = N 2 0 - 42060 = N 3 0 - 42360 = N 4 0 - 42860 = N 2 0 - 42980 = N 3 0 - 43120 = N 2 0 - 43360 = N 1 0 - 43860 = N 0 0 - 44360 = N 4 0 - 45180 = N 2 0 - 45420 = N 2 1740 - 48400 = N 1 0 - 48900 = N 1 0 - 49160 = N 2 0 - 49420 = N 3 0 - 50160 = N 4 0 - 50420 = N 0 0 - 51440 = N 4 0 - 51680 = N 3 0 - 51940 = N 2 0 - 52200 = N 0 0 - 52440 = N 1 0 - 53220 = N 3 0 - 53440 = N 4 1480 - 55460 = N 3 0 - 55980 = N 2 0 - 56480 = N 1 0 - 56980 = N 1 0 - 57240 = N 2 0 - 57480 = N 3 0 - 58240 = N 4 0 - 58480 = N 1 0 - 59000 = N 0 0 - 59500 = N 4 0 - 60000 = N 3 0 - 60500 = N 3 0 - 61000 = N 4 0 - 61140 = N 3 0 - 61260 = N 2 0 - 61520 = N 0 0 - 62020 = N 1 0 - 62520 = N 2 0 - 63520 = N 3 0 - 64280 = N 4 0 - 64540 = N 1 1040 - 66060 = N 4 0 - 66060 = E Bad_Too_High - 66300 = N 3 0 - 66560 = N 2 0 - 67060 = N 1 0 - 67220 = N 2 0 - 67400 = N 1 0 - 67560 = N 0 0 - 68060 = N 4 0 - 68580 = N 3 1220 - 71600 = N 2 0 - 71680 = N 3 0 - 72100 = N 1 0 - 72600 = N 0 0 - 73360 = N 1 0 - 73620 = N 3 1020 - 75120 = N 0 0 - 75380 = N 1 0 - 75640 = N 3 0 - 75880 = N 4 0 - 76140 = N 0 0 - 76140 = E Bad_Too_Low - 76380 = N 4 0 - 76640 = N 2 0 - 77400 = N 1 0 - 77640 = N 2 0 - 77780 = N 3 1380 - 79660 = N 3 0 - 80180 = N 3 0 - 80680 = N 4 0 - 81680 = N 0 0 - 81940 = N 1 0 - 82180 = N 2 0 - 82440 = N 4 0 - 82680 = N 3 0 - 83200 = N 0 0 - 83640 = N 3 0 - 84460 = N 2 0 - 84700 = N 1 2240 - 87740 = N 4 0 - 87740 = E Bad_Too_High - 88240 = N 3 0 - 88740 = N 2 0 - 89740 = N 0 0 - 90000 = N 1 0 - 90260 = N 2 0 - 90500 = N 4 0 - 90760 = N 3 0 - 91260 = N 1 0 - 91440 = N 2 0 - 91600 = N 1 0 - 91760 = N 0 0 - 92260 = N 1 0 - 92780 = N 3 0 - 93540 = N 4 0 - 93780 = N 4 1460 - 95800 = N 1 0 - 96300 = N 3 0 - 96740 = N 3 0 - 98320 = N 2 0 - 98480 = N 4 0 - 98660 = N 3 0 - 98820 = N 2 0 - 99840 = N 1 0 - 100840 = N 2 0 - 101860 = N 1 0 - 102360 = N 2 0 - 102600 = N 1 0 - 102860 = N 0 1380 - 104900 = N 2 0 - 105900 = N 0 0 - 106120 = N 1 0 - 106380 = N 2 0 - 106600 = N 3 0 - 106900 = N 4 0 - 107400 = N 2 0 - 107520 = N 3 0 - 107640 = N 2 0 - 107900 = N 1 0 - 108400 = N 0 0 - 108900 = N 4 0 - 109660 = N 2 0 - 109920 = N 2 1740 - 112940 = N 1 0 - 113440 = N 1 0 - 113700 = N 2 0 - 113940 = N 3 0 - 114700 = N 4 0 - 114960 = N 0 0 - 115980 = N 4 0 - 116220 = N 3 0 - 116480 = N 2 0 - 116740 = N 0 0 - 116980 = N 1 0 - 117740 = N 3 0 - 117980 = N 4 1480 - 120000 = N 3 0 - 120500 = N 2 0 - 121020 = N 1 0 - 121520 = N 1 0 - 121760 = N 2 0 - 122020 = N 3 0 - 122780 = N 4 0 - 123020 = N 1 0 - 123520 = N 0 0 - 124040 = N 3 0 - 124540 = N 2 0 - 125040 = N 2 0 - 125540 = N 3 0 - 125720 = N 2 0 - 125880 = N 1 0 - 126060 = N 0 0 - 126560 = N 1 0 - 127060 = N 2 0 - 128060 = N 3 0 - 128820 = N 4 0 - 129080 = N 1 1040 - 130600 = N 4 0 - 130600 = E Bad_Too_High - 130760 = N 4 0 - 130840 = N 3 0 - 131100 = N 2 0 - 131600 = N 1 0 - 132100 = N 0 0 - 132600 = N 1 0 - 133100 = N 2 1040 - 134620 = N 3 0 - 134880 = N 2 0 - 135120 = N 1 0 - 136020 = N 3 0 - 136140 = N 4 0 - 137140 = N 1 0 - 137140 = E Bad_Too_Low - 138180 = N 1 0 - 138640 = N 0 0 - 138880 = N 1 0 - 139140 = N 2 0 - 139640 = N 3 0 - 140180 = N 4 0 - 140700 = N 3 0 - 141180 = N 4 0 - 142180 = N 4 0 - 142680 = N 0 0 - 142940 = N 1 0 - 143200 = N 2 0 - 143700 = N 3 0 - 144200 = N 4 0 - 144700 = N 3 0 - 144700 = E Bad_Too_Low - 145220 = N 3 0 - 145380 = N 4 2360 - 148240 = N 0 0 - 148480 = N 1 0 - 148740 = N 2 0 - 149000 = N 3 0 - 149240 = N 4 0 - 150260 = N 0 0 - 150760 = N 0 0 - 151020 = N 0 0 - 151260 = N 0 0 - 153260 = N 0 0 -} diff --git a/assets/credits.mid b/assets/credits.mid index f4a8c8ed246ce50dffbcbe46dd9568890fb45766..5ae95c3b4c2a48810c3e68b5a68a6826621eb751 100644 GIT binary patch literal 5403 zcmaKwYg1d-6^2h@Ptv6JOgd@Yl4+(C4~!}#j&8;BfF*^C!4|QN5td@Ef`e^tLgMP1 zoC+}Zv|rXt|7QP#`@H)|n53C}fW6N?d#~%e-nEZ@C_M{2&-={##Jluq<%iPlCQsjc zA1?i^yJx2S{n*5K_vr3gu)e?7eQ)ik`}UoC-%oYVyr`|Fd%O?Rmp-5IybqPjmp}dU z@815mzq83R*#K& zT{gaQeAC9gD>imN#X_Hrr*k~B<8|vA3s-GqkI9I~{=%{jw}x$^+A7(Ecie4-m4Kz3 z6)Nxkz~tSa-C8T#Ew5FzTjd6ml;x|fFD#Fs!otZv*wbetvqc2r!0gG0Ecb1^$ewZU zTN^7gk=DYD1mhUvkk@Os#w$oW`N|4UWJH&Z9KQPwDaX@RSZlD-nzZ~%8AVdP6?AOS z1RK`zA&-}Z;S<@JvysKtmmKf1;p*|%HjIYsG%?D0^7`p&$lR2AWoOpb;Nc1W;K8G?F z`?UalDI3|WNMF?oRe68KMygWNYXwp*wfQyYhjO7AimuyOiKvgUd3=c{m%1evM9)T+ zP9^Z|bs=Jk?Re=BkryI*2rPmAvCfOD7ZK5#MGO|*&$%d!r;DMLAtDglMg(G)(S#Mg zAW`i=qRbLusk~Qg&OQec%AN93u7~NU68t>=0Exe|r?(9W@H~5RuLFrDTDydZ5Ql{s zMHb5)B2pI-ArC$x;vvCtKoug2Lc}(R_@xQ!&IE@DnLzLto5aciqPqhJ*eHYF7c3ROv=78zw@p)P!Q!ozhrNP(`APxg|N(%YB3e7|P-ojzXT;-MggF}WbH zoJZ{rZ!~c6c*AnbS|LxnXE)hWx6A@@$)Mr}5WIo8!?R7cqOzoz_SxtJDIdj_i7b-| z-i*tLaZfe#q)0T1c6*0EZCP+gr*IpAFGM{XG|6tS4b3$H9EqCYxGRK5P0j$x4cy`kWfq7{wbHJ}4Lwtht6}uN zvBKyZR-m8>C=x#x?M^O7wkvk$;N4eDSecjyI1|MB&T7C4HFIaVmx*V0gSR!itK*Xk zd0gnVyK@y52*7#}`X5jy_+PT|DxwJX*EX?o+F*?et{%tG4qKqyp<`Y~vyiXS4D#N| zcdGwRN4SgI=k>3SAk*Rhhd9&j2r)EtIs*B1_3t`@cDVZgrj4~_@^Z2Maq(kIcwgY% zS@&VLS?5o*-`&$?1?ry~%( zVVaPmkB{~LSBLoQbVK!DQvJtpobvm>elS)pyP$>j53&OFA__CCf0%n&%pKPMul9pL z{os#EZVv=N{}tTlQY^sejE>M99{|3P54K5pTPg(56A}Oof!cTNt914dqu`TL>l@C) z?Ii%=GFLj8BXbqIUCMY2@xzu1i4&1~^*hUPe3YOp6|GnCv6=q~M`ri6w^tFP^m zE)hyNb6d6CQ1UIPui_FzxC`)qoz9!h0hQY)-iywguC zvq#P{*p#W|!*MUiSmDL-5fkr2=N|U^%x~YrxR+UmYgmSY$?O*y`Qwt6@gR}RwxpLW zLvMC3xQ1oKY?-5mc3efj^j_HEM5Pd=6L;CDksDox8{N+~eiY@DNmSbv!jgfNGmWY6 zgcHT-LrFO8!i2OYH|tusgMtXTk&Ak|Ha~7ez~AWfBLIoJj8H8Bh6l zm}640B)6#~I(=B(S2wzej+>mwGG;k3h>k&prmfpw%d@d;k!sN*2#6y&uE@k2JjjOl zx>k7D3>b43JIGs%6EWP0At{DCF}V{v;w8q$j_=BWC;Ba;}tD1i5<2L zdnG%HmUvaRgkq7drw~rXB)1w!?`3Q<_F0l-6UM8SBxOwc%%^D+zghFQ-K1oZ)d~S3 zNF{>BI8e{q0AUzl!vOXStTq{GWCPE0Ojvx*CN@U5D;AaI(StG*gh%O3th^*kWofCl z)5WF3Un6&(Ce8pJxR3ct=$`B#DA0=%oMt_(b+X{2C9^BAAu6*j^l_ifIA9XwZM^|q zZ}?LfQq8i^^{u6_)S-(Err41ra7pDpiRms3$t_J-C6vNjI~rBHSt|ppyk%L0;;gk5hZj z3@Xc^oT#SZXPZi2t;Mv~}TyC`1BiJmS6Yv&8d?>Z59j1BwM{iMc!@AxseY zgpw6bur-0vZcDwvi0keMyEX!zbGR4CsacC0tTF*)k)sq7R@oU5D@2aken*hbUQF56 zmWD+dT0lQI3=KL`wcn7x1VM~YYzVJeYOfRp3t#|j<^J`iXSo~|`D83sNoI*2RzlPZLOB%7cNVM(}7gzWFQuu{nc zfYiWT+hGOxOJRWyyoM{JZr~>o2FV*}iXbXB@KSG}ghS;YlcO&L2)bhw_$cvF>QTt( zho|}_Bb*?ke-Bz^N9t)1CmSjwm_%w;{@YBaAuB(AM-f6cxc<%!5Y{NUUk>G{A zvu3Gt-Vy6!*)Mp9)mLf*gg0d2S$HRacXDms$-_Hkcn7SX^G;X@vE`6E+Pp(MqZeJM zYaSf$fX81D4|x1J@zBE@->7f#&EGGyg@i)~U*#(-B%Hx8JP?iRU+09w2bB-=3~W;J zu93 zYuU}ECRy&XETh~VWE|uc>TNpzzbDP-0wv5Ns*Cx;uGrm;g)rN~g;%S`)oY~~_BPqMRw2_QF!PdQFg`|>Ax`qyrM-C7A8$N8P}8|Tuc|9sur zS>spHd4K6o@oO^=zN*x!@yVU#V0CXdesB3z{PxtnuNv{0*PBa)YtH-GSnRh~zi{49 zU;6!wUxNOF@hkKh^>34NV*G>Xc4&J1CWv66{k2}4h6PU4PKgO10%HJK-*>Oze z8&)cgYpPEMd%dbNVk&=avQRSBLcniMbPu*!7%>yO2op{}X37ipc0Xlmi~Uxw<_sF9 z`~<6IY?No;`3Uc}&CO+8yXo{-%*_V}K2p<+FCMge<0vaXIx2ChWG24tpj3f;d(>0_ zS55UUhpPBgd4M3(2ZMvr0hLnbMJCeV=%Y!3ivv76-*%Ntv7O$4K45wy8WLK{gK$0?rJsm9RGJr|B3yg`6*Yo|N=HcPnw#qu zS|s`mwyR_;LW+E$8jnF;gp@{v6!jSQBcPClkAPBPr6T+D0=n z*(-fZl1j4NvOQWdlQqsv;#94OP-Ckq530^!&s3gvNKVJx-s<#j>&PS94D8hA)fIy; zO>MG+KydXZ$MIg>IjEb`p8&oZZv!MA*?*gQQTB=Xe4%&`DXixvZdDw3bEIRuds*wY@p za-Wq7D-+Mr0KIKf>JD34iF)?Ae$%o`oUpYhPue8)%cdrNtvQETGj%I~D=;?o5>}nk zQms9>!e(Hmmhfpxtu?h!LU7ET;BB^dM_{f$3W%j-?#vPI9n92MIc#$g&`so`WvYvZ zS!KRxYERy%HOb-PU;(edj6ATIY1^!hz}!@0+&SXV|C%ZnxB`V|Q-yL{s?by+>Kgk~b zm2+fKRyzQFW?}0@kAwpVs0BW3fhE|2wvM8Se!CTfjodmGoH^ZidRU>^1`X3#8hnZ5 z0%z{k@Tt)Tat$zhPh+--JPYAQ|B&)}kIt3t%v5b9^vjz)Sj+MQWvcSdb8-6FBm z@biqCa|Ug#twE#L!qELCR_?3yXTRyEdb1*rhvKt`Qt?nD^dTu}@BgdVYdeEI)7WKG z4mEeJ8znQX6{}r>w9jg5);Wg#cKa>Zk0AZcAQE=1aGAZsS(Z*Z#(z12aP|(ooc#Pd zkar<>XkDL%_xC#dMTs7GzQZff-xyku`*x34pf85xkgr4ap+C5BdMMP`MC0(99+r_x zDIk?+r_ZUhc289jYp4?6UJS3Ds@&Jg@iu3MTRRduCa+6*y=3xxM3Pq|`Sk#S?BzvN zc`BHD-Rb4zNRF^`T+8hR{C#uPa ztzj&O()X>FigpwkCl>ydMF4NIcdr% z0Hkf>ZL>yhTgKZMO70T7E?V7IBxJUOUPeJ>bSQ&+8MRwRQ#8}DdlEXVbWCD{TM5-a z@zP3|Njwi&;!*8wv4p*qsHksZCajo*&==~hFAEPV-mdj2Fgd+{1cidO3ffYgJk0q! z2=v5?@mJL@DDo|&RRg(VG5KRhknEcL53Gcc#P1N26grQhoRXT;p&XIe?Bv#mASKTN z=`cwv=Ef$cZ{SFA6W8?hHL8_S1YaxaXz#voD^M*&=t}c^3$dsQP}Oad=3SIVb$TVj zWQ0HhiX>qW(h1?YJH(*A7n0mdzQ9_Nj*0|fP{m17dVk5X1l`t!Ol|(UK?uRF0&1O2XfKG>i!ZQQ#GfqBx~!tl zeQyB>HRpb`m=prhK)1(+MJHN!9~G`txH|y?Xi4mIK7sngYobjMZGvJYbT7g5Nl1GF z-av@cO<@E;dCN+e@#scd0gp;r0ZOY2E-B;l85dIFe3T{c8CF7Yu7*^za??TH7e0F#}v}pwh5Gnj~#S}JK0TEQa%OM)8 zWpY2EGwiG%nh5C3L8_MR&oP7Y;)$m`{tXhAZ=Q?l_DmNcqatK};uH*2H<=$qJVcuL zo81hA8%sf50Y?a8f+s+n#Yuwh7L<}odQz$)2~|mEI_%AydKQwx^&ReCclu>>U2lQw z(v)PFB`uke58%=@UgUAxcuUdv@kFBX*qgJ_h+&`d{lNLF0=*>HjFpLk7VK~+Lx1IZr|8=3on*f@TU#zKk%%fCoyr7&?2 znVqSpgf`eCwT{WG@tg$~FsvrTXBjO9qls75EuUnx5{wpg%LPWWGqwFOqtVv;xbP99 z37Hn6@u8pOb~`0grYDt7|7q$Q+r3m)=OgrgGV=%|EIt?0WzE4)kUoGF>KR#d5k1WhVe cThJ!#tWSoJH<{j{0KHhkcJ;~NUi@VEFCwg1aR2}S diff --git a/assets/credits.mid.bak b/assets/credits.mid.bak new file mode 100644 index 0000000000000000000000000000000000000000..f4a8c8ed246ce50dffbcbe46dd9568890fb45766 GIT binary patch literal 5769 zcmb`L+j0}x6^1)Ol}x3$;KCPN>Ax`qyrM-C7A8$N8P}8|Tuc|9sur zS>spHd4K6o@oO^=zN*x!@yVU#V0CXdesB3z{PxtnuNv{0*PBa)YtH-GSnRh~zi{49 zU;6!wUxNOF@hkKh^>34NV*G>Xc4&J1CWv66{k2}4h6PU4PKgO10%HJK-*>Oze z8&)cgYpPEMd%dbNVk&=avQRSBLcniMbPu*!7%>yO2op{}X37ipc0Xlmi~Uxw<_sF9 z`~<6IY?No;`3Uc}&CO+8yXo{-%*_V}K2p<+FCMge<0vaXIx2ChWG24tpj3f;d(>0_ zS55UUhpPBgd4M3(2ZMvr0hLnbMJCeV=%Y!3ivv76-*%Ntv7O$4K45wy8WLK{gK$0?rJsm9RGJr|B3yg`6*Yo|N=HcPnw#qu zS|s`mwyR_;LW+E$8jnF;gp@{v6!jSQBcPClkAPBPr6T+D0=n z*(-fZl1j4NvOQWdlQqsv;#94OP-Ckq530^!&s3gvNKVJx-s<#j>&PS94D8hA)fIy; zO>MG+KydXZ$MIg>IjEb`p8&oZZv!MA*?*gQQTB=Xe4%&`DXixvZdDw3bEIRuds*wY@p za-Wq7D-+Mr0KIKf>JD34iF)?Ae$%o`oUpYhPue8)%cdrNtvQETGj%I~D=;?o5>}nk zQms9>!e(Hmmhfpxtu?h!LU7ET;BB^dM_{f$3W%j-?#vPI9n92MIc#$g&`so`WvYvZ zS!KRxYERy%HOb-PU;(edj6ATIY1^!hz}!@0+&SXV|C%ZnxB`V|Q-yL{s?by+>Kgk~b zm2+fKRyzQFW?}0@kAwpVs0BW3fhE|2wvM8Se!CTfjodmGoH^ZidRU>^1`X3#8hnZ5 z0%z{k@Tt)Tat$zhPh+--JPYAQ|B&)}kIt3t%v5b9^vjz)Sj+MQWvcSdb8-6FBm z@biqCa|Ug#twE#L!qELCR_?3yXTRyEdb1*rhvKt`Qt?nD^dTu}@BgdVYdeEI)7WKG z4mEeJ8znQX6{}r>w9jg5);Wg#cKa>Zk0AZcAQE=1aGAZsS(Z*Z#(z12aP|(ooc#Pd zkar<>XkDL%_xC#dMTs7GzQZff-xyku`*x34pf85xkgr4ap+C5BdMMP`MC0(99+r_x zDIk?+r_ZUhc289jYp4?6UJS3Ds@&Jg@iu3MTRRduCa+6*y=3xxM3Pq|`Sk#S?BzvN zc`BHD-Rb4zNRF^`T+8hR{C#uPa ztzj&O()X>FigpwkCl>ydMF4NIcdr% z0Hkf>ZL>yhTgKZMO70T7E?V7IBxJUOUPeJ>bSQ&+8MRwRQ#8}DdlEXVbWCD{TM5-a z@zP3|Njwi&;!*8wv4p*qsHksZCajo*&==~hFAEPV-mdj2Fgd+{1cidO3ffYgJk0q! z2=v5?@mJL@DDo|&RRg(VG5KRhknEcL53Gcc#P1N26grQhoRXT;p&XIe?Bv#mASKTN z=`cwv=Ef$cZ{SFA6W8?hHL8_S1YaxaXz#voD^M*&=t}c^3$dsQP}Oad=3SIVb$TVj zWQ0HhiX>qW(h1?YJH(*A7n0mdzQ9_Nj*0|fP{m17dVk5X1l`t!Ol|(UK?uRF0&1O2XfKG>i!ZQQ#GfqBx~!tl zeQyB>HRpb`m=prhK)1(+MJHN!9~G`txH|y?Xi4mIK7sngYobjMZGvJYbT7g5Nl1GF z-av@cO<@E;dCN+e@#scd0gp;r0ZOY2E-B;l85dIFe3T{c8CF7Yu7*^za??TH7e0F#}v}pwh5Gnj~#S}JK0TEQa%OM)8 zWpY2EGwiG%nh5C3L8_MR&oP7Y;)$m`{tXhAZ=Q?l_DmNcqatK};uH*2H<=$qJVcuL zo81hA8%sf50Y?a8f+s+n#Yuwh7L<}odQz$)2~|mEI_%AydKQwx^&ReCclu>>U2lQw z(v)PFB`uke58%=@UgUAxcuUdv@kFBX*qgJ_h+&`d{lNLF0=*>HjFpLk7VK~+Lx1IZr|8=3on*f@TU#zKk%%fCoyr7&?2 znVqSpgf`eCwT{WG@tg$~FsvrTXBjO9qls75EuUnx5{wpg%LPWWGqwFOqtVv;xbP99 z37Hn6@u8pOb~`0grYDt7|7q$Q+r3m)=OgrgGV=%|EIt?0WzE4)kUoGF>KR#d5k1WhVe cThJ!#tWSoJH<{j{0KHhkcJ;~NUi@VEFCwg1aR2}S literal 0 HcmV?d00001 diff --git a/assets/credits_e.chart b/assets/credits_e.chart new file mode 100644 index 000000000..4304414d1 --- /dev/null +++ b/assets/credits_e.chart @@ -0,0 +1,140 @@ +[Song] +{ + Name = " "SWADGE Credits New FINAL Sync3"" + Charter = "Edward Haas" + Year = "Unknown" + Offset = 0 + Resolution = 600 + Player2 = bass + Difficulty = 4 + PreviewStart = 4000 + PreviewEnd = 14000 + Genre = "rock" + MediaType = "cd" + MusicStream = "song.ogg" +} +[SyncTrack] +{ + 0 = TS 4 + 0 = B 120000 + 2400 = TS 4 + 2400 = B 125000 + 98187 = B 125000 +} +[ExpertSingle] +{ + 4925 = N 1 0 + 5863 = N 1 0 + 6500 = N 1 0 + 7463 = N 2 0 + 8088 = N 0 0 + 9025 = N 3 0 + 9650 = N 2 0 + 10463 = N 2 0 + 12488 = N 1 0 + 13113 = N 2 0 + 13750 = N 0 0 + 14538 = N 3 0 + 15488 = N 2 0 + 16900 = N 2 0 + 17525 = N 1 0 + 18162 = N 2 0 + 18788 = N 0 0 + 19413 = N 3 0 + 20050 = N 1 0 + 20675 = N 0 0 + 21313 = N 2 0 + 21938 = N 3 0 + 23513 = N 2 0 + 24250 = N 0 0 + 25088 = N 1 662 + 26050 = N 2 0 + 27625 = N 1 0 + 28250 = N 0 0 + 28875 = N 3 0 + 29500 = N 2 0 + 30125 = N 3 0 + 30788 = N 2 1087 + 32650 = N 1 0 + 33288 = N 2 0 + 33913 = N 0 0 + 34550 = N 3 0 + 35175 = N 2 0 + 35800 = N 3 925 + 37063 = N 2 0 + 37700 = N 1 0 + 38325 = N 2 0 + 38950 = N 0 0 + 39588 = N 3 0 + 40213 = N 2 0 + 40850 = N 0 0 + 41475 = N 2 0 + 42100 = N 3 0 + 42738 = N 1 650 + 43688 = N 3 0 + 43688 = E Bad_Too_High + 44313 = N 2 0 + 45263 = N 2 762 + 47150 = N 1 0 + 47775 = N 0 0 + 48413 = N 2 637 + 49350 = N 0 0 + 49988 = N 2 0 + 50775 = N 3 0 + 52188 = N 2 0 + 52188 = E Bad_Too_Low + 52825 = N 3 0 + 53612 = N 0 0 + 54400 = N 1 0 + 55188 = N 3 0 + 57238 = N 2 0 + 57863 = N 1 0 + 58488 = N 0 0 + 59125 = N 2 0 + 60062 = N 0 0 + 60863 = N 3 0 + 62275 = N 1 0 + 62900 = N 3 650 + 63850 = N 2 0 + 64800 = N 1 0 + 65425 = N 2 0 + 66063 = N 1 0 + 67963 = N 1 0 + 68588 = N 0 0 + 69213 = N 2 0 + 70150 = N 1 0 + 70938 = N 1 0 + 72988 = N 0 0 + 73613 = N 1 0 + 74250 = N 0 0 + 74887 = N 3 0 + 75513 = N 1 0 + 77400 = N 2 0 + 78038 = N 1 0 + 78663 = N 2 0 + 79288 = N 0 0 + 79925 = N 3 0 + 80863 = N 3 0 + 81500 = N 2 0 + 82438 = N 3 0 + 83075 = N 2 650 + 84025 = N 1 0 + 84650 = N 0 0 + 85275 = N 0 0 + 86538 = N 0 0 + 86538 = E Bad_Too_Low + 87413 = N 1 0 + 88113 = N 2 0 + 88763 = N 2 0 + 89675 = N 3 0 + 90338 = N 2 0 + 91263 = N 0 0 + 91263 = E Bad_Too_Low + 91900 = N 1 0 + 92525 = N 2 0 + 93163 = N 3 0 + 95050 = N 1 0 + 95675 = N 2 0 + 96313 = N 0 0 + 96938 = N 0 0 +} diff --git a/assets/credits_h.chart b/assets/credits_h.chart new file mode 100644 index 000000000..f16cd8baa --- /dev/null +++ b/assets/credits_h.chart @@ -0,0 +1,289 @@ +[Song] +{ + Name = " "SWADGE Credits New FINAL Sync3"" + Charter = "Edward Haas" + Year = "Unknown" + Offset = 0 + Resolution = 600 + Player2 = bass + Difficulty = 4 + PreviewStart = 4000 + PreviewEnd = 14000 + Genre = "rock" + MediaType = "cd" + MusicStream = "song.ogg" +} +[SyncTrack] +{ + 0 = TS 4 + 0 = B 120000 + 2400 = TS 4 + 2400 = B 125000 + 98187 = B 125000 +} +[ExpertSingle] +{ + 4925 = N 1 0 + 5075 = N 1 0 + 5237 = N 1 0 + 5863 = N 1 0 + 6025 = N 1 0 + 6188 = N 1 0 + 6500 = N 1 0 + 7463 = N 2 0 + 8088 = N 0 0 + 8225 = N 1 0 + 8388 = N 2 0 + 8513 = N 4 0 + 8700 = N 5 0 + 9025 = N 3 0 + 9175 = N 3 0 + 9338 = N 2 0 + 9650 = N 1 0 + 9963 = N 5 0 + 10463 = N 2 0 + 10625 = N 2 913 + 12488 = N 1 0 + 12800 = N 1 0 + 12963 = N 2 0 + 13113 = N 3 0 + 13588 = N 4 0 + 13750 = N 0 0 + 14375 = N 5 0 + 14538 = N 4 0 + 14688 = N 2 0 + 14863 = N 0 0 + 15000 = N 1 0 + 15488 = N 2 0 + 15638 = N 3 925 + 16900 = N 2 0 + 17213 = N 1 0 + 17525 = N 0 0 + 17850 = N 0 0 + 18000 = N 1 0 + 18162 = N 2 0 + 18625 = N 4 0 + 18788 = N 1 0 + 19100 = N 0 0 + 19413 = N 4 0 + 19738 = N 3 0 + 20050 = N 3 0 + 20363 = N 2 0 + 20525 = N 2 0 + 20675 = N 1 0 + 20988 = N 2 0 + 21313 = N 3 0 + 21938 = N 4 0 + 22413 = N 5 0 + 22563 = N 2 650 + 23513 = N 5 0 + 23513 = E Bad_Too_High + 23675 = N 4 0 + 23825 = N 3 0 + 24138 = N 2 0 + 24250 = N 3 0 + 24350 = N 2 0 + 24463 = N 1 0 + 24775 = N 2 0 + 25088 = N 4 662 + 26050 = N 5 0 + 26213 = N 4 0 + 26350 = N 3 0 + 27625 = N 2 0 + 28250 = N 0 0 + 28388 = N 1 0 + 28563 = N 2 0 + 28688 = N 4 0 + 28875 = N 5 0 + 29188 = N 4 0 + 29350 = N 4 0 + 29500 = N 3 0 + 29813 = N 2 0 + 30125 = N 5 0 + 30638 = N 3 0 + 30788 = N 3 1087 + 32650 = N 1 0 + 32963 = N 1 0 + 33125 = N 2 0 + 33288 = N 3 0 + 33750 = N 4 0 + 33913 = N 0 0 + 34550 = N 5 0 + 34700 = N 4 0 + 34863 = N 3 0 + 35025 = N 2 0 + 35175 = N 3 0 + 35663 = N 4 0 + 35800 = N 5 925 + 37063 = N 4 0 + 37388 = N 3 0 + 37700 = N 2 0 + 38013 = N 2 0 + 38175 = N 3 0 + 38325 = N 4 0 + 38800 = N 5 0 + 38950 = N 1 0 + 39275 = N 0 0 + 39588 = N 4 0 + 39900 = N 3 0 + 40213 = N 3 0 + 40525 = N 4 0 + 40688 = N 2 0 + 40850 = N 1 0 + 41163 = N 2 0 + 41475 = N 3 0 + 42100 = N 4 0 + 42575 = N 5 0 + 42738 = N 4 650 + 43688 = N 3 0 + 43838 = N 2 0 + 44000 = N 1 0 + 44313 = N 0 0 + 44413 = N 2 0 + 44525 = N 1 0 + 44625 = N 0 0 + 44938 = N 5 0 + 45263 = N 4 762 + 47150 = N 3 0 + 47463 = N 2 0 + 47775 = N 1 0 + 48250 = N 2 0 + 48413 = N 5 637 + 49350 = N 0 0 + 49513 = N 1 0 + 49675 = N 2 0 + 49825 = N 3 0 + 49988 = N 4 0 + 50138 = N 5 0 + 50300 = N 3 0 + 50775 = N 2 0 + 50925 = N 3 0 + 52188 = N 4 0 + 52513 = N 4 0 + 52825 = N 5 0 + 53450 = N 1 0 + 53612 = N 2 0 + 53763 = N 3 0 + 53925 = N 5 0 + 54075 = N 4 0 + 54400 = N 2 0 + 54675 = N 5 0 + 55188 = N 4 0 + 55338 = N 3 1400 + 57238 = N 5 0 + 57238 = E Bad_Too_High + 57550 = N 4 0 + 57863 = N 3 0 + 58488 = N 0 0 + 58650 = N 1 0 + 58813 = N 3 0 + 58963 = N 5 0 + 59125 = N 4 0 + 59438 = N 3 0 + 59550 = N 4 0 + 59650 = N 3 0 + 59750 = N 2 0 + 60062 = N 3 0 + 60388 = N 4 0 + 60863 = N 5 0 + 61013 = N 5 912 + 62275 = N 1 0 + 62588 = N 3 0 + 62863 = N 3 0 + 63850 = N 2 0 + 63950 = N 4 0 + 64063 = N 3 0 + 64163 = N 2 0 + 64800 = N 1 0 + 65425 = N 2 0 + 66063 = N 1 0 + 66375 = N 2 0 + 66525 = N 1 0 + 66687 = N 0 863 + 67963 = N 2 0 + 68588 = N 0 0 + 68725 = N 1 0 + 68887 = N 2 0 + 69025 = N 4 0 + 69213 = N 5 0 + 69525 = N 4 0 + 69675 = N 4 0 + 69838 = N 3 0 + 70150 = N 2 0 + 70463 = N 5 0 + 70938 = N 3 0 + 71100 = N 3 1088 + 72988 = N 1 0 + 73300 = N 1 0 + 73463 = N 2 0 + 73613 = N 3 0 + 74088 = N 4 0 + 74250 = N 0 0 + 74887 = N 5 0 + 75038 = N 4 0 + 75200 = N 3 0 + 75363 = N 2 0 + 75513 = N 3 0 + 75988 = N 4 0 + 76138 = N 5 925 + 77400 = N 4 0 + 77713 = N 3 0 + 78038 = N 2 0 + 78350 = N 2 0 + 78500 = N 3 0 + 78663 = N 4 0 + 79138 = N 5 0 + 79288 = N 1 0 + 79600 = N 0 0 + 79925 = N 3 0 + 80238 = N 2 0 + 80550 = N 2 0 + 80863 = N 3 0 + 80975 = N 2 0 + 81075 = N 1 0 + 81188 = N 0 0 + 81500 = N 1 0 + 81813 = N 2 0 + 82438 = N 3 0 + 82913 = N 5 0 + 83075 = N 4 650 + 84025 = N 3 0 + 84125 = N 3 0 + 84338 = N 2 0 + 84650 = N 1 0 + 84963 = N 0 0 + 85275 = N 1 0 + 85588 = N 2 650 + 86538 = N 3 0 + 86700 = N 2 0 + 86850 = N 0 0 + 87413 = N 1 0 + 88113 = N 2 0 + 88763 = N 2 0 + 89050 = N 1 0 + 89200 = N 2 0 + 89363 = N 3 0 + 89675 = N 4 0 + 90013 = N 5 0 + 90338 = N 0 0 + 90638 = N 1 0 + 91263 = N 1 0 + 91575 = N 0 0 + 91738 = N 1 0 + 91900 = N 2 0 + 92213 = N 3 0 + 92525 = N 4 0 + 92838 = N 4 0 + 92838 = E Bad_Too_Low + 93163 = N 4 0 + 93263 = N 5 1475 + 95050 = N 1 0 + 95200 = N 2 0 + 95363 = N 3 0 + 95525 = N 4 0 + 95675 = N 5 0 + 96313 = N 0 0 + 96625 = N 0 0 + 96788 = N 0 0 + 96938 = N 0 0 +} diff --git a/assets/credits_m.chart b/assets/credits_m.chart new file mode 100644 index 000000000..e468c74a1 --- /dev/null +++ b/assets/credits_m.chart @@ -0,0 +1,217 @@ +[Song] +{ + Name = " "SWADGE Credits New FINAL Sync3"" + Charter = "Edward Haas" + Year = "Unknown" + Offset = 0 + Resolution = 600 + Player2 = bass + Difficulty = 4 + PreviewStart = 4000 + PreviewEnd = 14000 + Genre = "rock" + MediaType = "cd" + MusicStream = "song.ogg" +} +[SyncTrack] +{ + 0 = TS 4 + 0 = B 120000 + 2400 = TS 4 + 2400 = B 125000 + 98187 = B 125000 +} +[ExpertSingle] +{ + 4925 = N 1 0 + 5237 = N 1 0 + 5863 = N 1 0 + 6188 = N 1 0 + 6500 = N 1 0 + 7463 = N 2 0 + 8088 = N 0 0 + 8388 = N 3 0 + 8700 = N 4 0 + 9025 = N 3 0 + 9338 = N 2 0 + 9650 = N 1 0 + 9963 = N 4 0 + 10463 = N 1 0 + 12488 = N 0 0 + 12800 = N 0 0 + 13113 = N 2 0 + 13588 = N 3 0 + 14375 = N 4 0 + 14688 = N 2 0 + 15000 = N 0 0 + 15488 = N 2 0 + 16900 = N 2 0 + 17213 = N 1 0 + 17525 = N 0 0 + 17850 = N 0 0 + 18162 = N 2 0 + 18625 = N 3 0 + 19100 = N 0 0 + 19413 = N 3 0 + 19738 = N 2 0 + 20050 = N 2 0 + 20363 = N 1 0 + 20675 = N 0 0 + 20988 = N 1 0 + 21313 = N 2 0 + 21938 = N 3 0 + 22413 = N 4 0 + 23513 = N 3 0 + 23825 = N 2 0 + 24138 = N 1 0 + 24350 = N 1 0 + 24775 = N 1 0 + 25088 = N 3 662 + 26050 = N 4 0 + 26350 = N 2 0 + 27625 = N 1 0 + 28250 = N 0 0 + 28563 = N 1 0 + 28875 = N 4 0 + 29188 = N 3 0 + 29500 = N 2 0 + 29813 = N 1 0 + 30125 = N 4 0 + 30638 = N 1 0 + 32650 = N 0 0 + 32963 = N 0 0 + 33288 = N 2 0 + 33750 = N 3 0 + 34550 = N 4 0 + 34863 = N 2 0 + 35175 = N 0 0 + 35663 = N 2 0 + 37063 = N 2 0 + 37388 = N 1 0 + 37700 = N 0 0 + 38013 = N 0 0 + 38325 = N 2 0 + 38800 = N 3 0 + 39275 = N 0 0 + 39588 = N 3 0 + 39900 = N 2 0 + 40213 = N 2 0 + 40525 = N 3 0 + 40850 = N 0 0 + 41163 = N 1 0 + 41475 = N 2 0 + 42100 = N 3 0 + 42575 = N 4 0 + 43688 = N 3 0 + 44000 = N 2 0 + 44313 = N 1 0 + 44525 = N 1 0 + 44938 = N 2 0 + 45263 = N 1 762 + 47150 = N 2 0 + 47150 = E Bad_Too_High + 47463 = N 1 0 + 47775 = N 0 0 + 48250 = N 1 0 + 49350 = N 0 0 + 49675 = N 1 0 + 49988 = N 3 0 + 50300 = N 4 0 + 50775 = N 0 0 + 51013 = N 1 862 + 52188 = N 1 0 + 52513 = N 1 0 + 52825 = N 2 0 + 53450 = N 0 0 + 53763 = N 1 0 + 54075 = N 3 0 + 54400 = N 2 0 + 54675 = N 4 0 + 55188 = N 3 0 + 57238 = N 2 0 + 57550 = N 1 0 + 57863 = N 0 0 + 58488 = N 0 0 + 58488 = E Bad_Too_High + 58813 = N 1 0 + 59125 = N 2 0 + 59438 = N 1 0 + 59650 = N 1 0 + 60062 = N 1 0 + 60388 = N 3 0 + 60863 = N 4 0 + 62275 = N 0 0 + 62588 = N 3 0 + 62863 = N 3 0 + 63850 = N 2 0 + 64063 = N 2 0 + 64800 = N 1 0 + 65425 = N 2 0 + 66063 = N 1 0 + 66375 = N 2 0 + 66687 = N 0 863 + 67963 = N 1 0 + 68588 = N 0 0 + 68887 = N 1 0 + 69213 = N 3 0 + 69525 = N 2 0 + 69838 = N 1 0 + 70150 = N 0 0 + 70463 = N 4 0 + 70938 = N 1 0 + 72988 = N 0 0 + 73300 = N 0 0 + 73613 = N 2 0 + 74088 = N 3 0 + 74887 = N 4 0 + 75200 = N 2 0 + 75513 = N 0 0 + 75988 = N 2 0 + 77400 = N 2 0 + 77713 = N 1 0 + 78038 = N 0 0 + 78350 = N 0 0 + 78663 = N 1 0 + 79138 = N 2 0 + 79600 = N 0 0 + 79925 = N 3 0 + 80238 = N 2 0 + 80550 = N 2 0 + 80863 = N 3 0 + 81075 = N 1 0 + 81500 = N 1 0 + 81813 = N 2 0 + 82438 = N 3 0 + 82913 = N 4 0 + 84025 = N 3 0 + 84338 = N 2 0 + 84650 = N 1 0 + 84963 = N 0 0 + 85275 = N 1 0 + 85588 = N 2 650 + 86538 = N 3 0 + 86850 = N 0 0 + 87413 = N 1 0 + 88113 = N 2 0 + 88763 = N 2 0 + 89050 = N 0 0 + 89363 = N 1 0 + 89675 = N 2 0 + 90013 = N 3 0 + 90338 = N 2 0 + 90638 = N 4 0 + 91263 = N 4 0 + 91575 = N 3 0 + 91900 = N 1 0 + 91900 = E Bad_Too_Low + 92213 = N 2 0 + 92525 = N 3 0 + 92838 = N 4 0 + 93163 = N 4 0 + 95050 = N 1 0 + 95363 = N 2 0 + 95675 = N 3 0 + 96313 = N 0 0 + 96625 = N 0 0 + 96938 = N 0 0 +} diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index aab7c0774..0c59b1dc4 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -18,6 +18,14 @@ // Structs //============================================================================== +typedef struct +{ + char* midi; + char* easy; + char* med; + char* hard; +} shSong_t; + typedef struct { int32_t tick; @@ -47,6 +55,7 @@ typedef struct int32_t numNotes; shNote_t* notes; int32_t cNote; + int32_t numTracks; // Drawing data list_t icons; @@ -61,7 +70,7 @@ static void shEnterMode(void); static void shExitMode(void); static void shMainLoop(int64_t elapsedUs); static void shBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); -static void shLoadTrackData(shVars_t* sh, const uint8_t* data, size_t size); +static uint32_t shLoadTrackData(shVars_t* shv, const uint8_t* data, size_t size); // static void shMenuCb(const char*, bool selected, uint32_t settingVal); static uint32_t btnToNote(buttonBit_t btn); static void shRunTimers(shVars_t* shv, uint32_t elapsedUs); @@ -95,6 +104,15 @@ shVars_t* sh; static const paletteColor_t colors[] = {c020, c400, c550, c004, c420, c222}; static const buttonBit_t noteToBtn[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; +static const shSong_t shSongList[] = { + { + .midi = "credits.mid", + .easy = "credits_e.cch", + .med = "credits_m.cch", + .hard = "credits_h.cch", + }, +}; + //============================================================================== // Functions //============================================================================== @@ -116,10 +134,10 @@ static void shEnterMode(void) // Load the track data size_t sz = 0; - shLoadTrackData(sh, cnfsGetFile("credits.cch", &sz), sz); + sh->numTracks = 1 + shLoadTrackData(sh, cnfsGetFile(shSongList[0].hard, &sz), sz); // Load the MIDI file - loadMidiFile("credits.mid", &sh->credits, false); + loadMidiFile(shSongList[0].midi, &sh->credits, false); globalMidiPlayerPlaySong(&sh->credits, MIDI_BGM); globalMidiPlayerPauseAll(); @@ -286,8 +304,8 @@ static void shMainLoop(int64_t elapsedUs) { // Spawn an icon shNoteIcon_t* ni = calloc(1, sizeof(shNoteIcon_t)); - ni->note = sh->notes[sh->cNote].note; - ni->headPosY = TFT_HEIGHT + (ICON_RADIUS * 2); + ni->note = sh->notes[sh->cNote].note; + ni->headPosY = TFT_HEIGHT + (ICON_RADIUS * 2); // If this is a hold note if (sh->notes[sh->cNote].hold) @@ -447,9 +465,9 @@ static void shDrawGame(shVars_t* shv) // Draw the target area drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); - for (int32_t i = 0; i < ARRAY_SIZE(noteToBtn); i++) + for (int32_t i = 0; i < shv->numTracks; i++) { - int32_t xOffset = ((i * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); + int32_t xOffset = ((i * TFT_WIDTH) / shv->numTracks) + (TFT_WIDTH / (2 * shv->numTracks)); drawCircle(xOffset, HIT_BAR, ICON_RADIUS + 2, c555); } @@ -459,7 +477,7 @@ static void shDrawGame(shVars_t* shv) { // Draw the icon shNoteIcon_t* icon = iconNode->val; - int32_t xOffset = ((icon->note * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); + int32_t xOffset = ((icon->note * TFT_WIDTH) / shv->numTracks) + (TFT_WIDTH / (2 * shv->numTracks)); drawCircleFilled(xOffset, icon->headPosY, ICON_RADIUS, colors[icon->note]); // If there is a tail @@ -474,11 +492,11 @@ static void shDrawGame(shVars_t* shv) } // Draw indicators that the button is pressed - for (int32_t bIdx = 0; bIdx < ARRAY_SIZE(noteToBtn); bIdx++) + for (int32_t bIdx = 0; bIdx < shv->numTracks; bIdx++) { if (shv->btnState & noteToBtn[bIdx]) { - int32_t xOffset = ((bIdx * TFT_WIDTH) / ARRAY_SIZE(noteToBtn)) + (TFT_WIDTH / 10); + int32_t xOffset = ((bIdx * TFT_WIDTH) / shv->numTracks) + (TFT_WIDTH / (2 * shv->numTracks)); drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[bIdx]); } } @@ -518,13 +536,16 @@ static void shBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, /** * @brief TODO - * - * @param data - * @param size + * + * @param shv + * @param data + * @param size + * @return uint32_t */ -static void shLoadTrackData(shVars_t* shv, const uint8_t* data, size_t size) +static uint32_t shLoadTrackData(shVars_t* shv, const uint8_t* data, size_t size) { - uint32_t dIdx = 0; + uint32_t maxTrack = 0; + uint32_t dIdx = 0; shv->numNotes = (data[dIdx++] << 8); shv->numNotes |= (data[dIdx++]); @@ -533,23 +554,30 @@ static void shLoadTrackData(shVars_t* shv, const uint8_t* data, size_t size) for (int32_t nIdx = 0; nIdx < shv->numNotes; nIdx++) { shv->notes[nIdx].tick = (data[dIdx + 0] << 24) | // - (data[dIdx + 1] << 16) | // - (data[dIdx + 2] << 8) | // - (data[dIdx + 3] << 0); + (data[dIdx + 1] << 16) | // + (data[dIdx + 2] << 8) | // + (data[dIdx + 3] << 0); dIdx += 4; shv->notes[nIdx].note = data[dIdx++]; + if((shv->notes[nIdx].note & 0x7F) > maxTrack) + { + maxTrack = shv->notes[nIdx].note & 0x7F; + } + if (0x80 & shv->notes[nIdx].note) { shv->notes[nIdx].note &= 0x7F; // Use the hold time to see when this note ends shv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // - (data[dIdx + 1] << 0); + (data[dIdx + 1] << 0); dIdx += 2; } } + + return maxTrack; } /** From c626ef88659c91523297e5d4e3851c617a8c3237 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Fri, 20 Sep 2024 11:33:59 +0000 Subject: [PATCH 27/54] Add Swadge Hero Menu Organize into files --- main/modes/games/swadgeHero/mode_swadgeHero.c | 603 ++++-------------- main/modes/games/swadgeHero/mode_swadgeHero.h | 96 ++- main/modes/games/swadgeHero/swadgeHero_game.c | 464 ++++++++++++++ main/modes/games/swadgeHero/swadgeHero_game.h | 11 + main/modes/games/swadgeHero/swadgeHero_menu.c | 190 ++++++ main/modes/games/swadgeHero/swadgeHero_menu.h | 8 + 6 files changed, 888 insertions(+), 484 deletions(-) create mode 100644 main/modes/games/swadgeHero/swadgeHero_game.c create mode 100644 main/modes/games/swadgeHero/swadgeHero_game.h create mode 100644 main/modes/games/swadgeHero/swadgeHero_menu.c create mode 100644 main/modes/games/swadgeHero/swadgeHero_menu.h diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index 0c59b1dc4..6b4aec1f8 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -1,66 +1,10 @@ //============================================================================== -// Defines +// Includes //============================================================================== #include "mode_swadgeHero.h" - -//============================================================================== -// Defines -//============================================================================== - -#define HIT_BAR 16 -#define ICON_RADIUS 8 - -#define TRAVEL_TIME_US 2000000 -#define TRAVEL_US_PER_PX ((TRAVEL_TIME_US) / (TFT_HEIGHT - HIT_BAR + (2 * ICON_RADIUS))) - -//============================================================================== -// Structs -//============================================================================== - -typedef struct -{ - char* midi; - char* easy; - char* med; - char* hard; -} shSong_t; - -typedef struct -{ - int32_t tick; - int32_t note; - int32_t hold; -} shNote_t; - -typedef struct -{ - int32_t note; - int32_t timer; - int32_t headPosY; - int32_t tailPosY; - bool held; -} shNoteIcon_t; - -typedef struct -{ - // Font - font_t ibm; - - // Song being played - midiFile_t credits; - int32_t leadInUs; - - // Track data - int32_t numNotes; - shNote_t* notes; - int32_t cNote; - int32_t numTracks; - - // Drawing data - list_t icons; - buttonBit_t btnState; -} shVars_t; +#include "swadgeHero_game.h" +#include "swadgeHero_menu.h" //============================================================================== // Function Declarations @@ -70,18 +14,17 @@ static void shEnterMode(void); static void shExitMode(void); static void shMainLoop(int64_t elapsedUs); static void shBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); -static uint32_t shLoadTrackData(shVars_t* shv, const uint8_t* data, size_t size); -// static void shMenuCb(const char*, bool selected, uint32_t settingVal); -static uint32_t btnToNote(buttonBit_t btn); -static void shRunTimers(shVars_t* shv, uint32_t elapsedUs); -static void shDrawGame(shVars_t* shv); //============================================================================== -// Variables +// Const Variables //============================================================================== static const char shName[] = "Swadge Hero"; +//============================================================================== +// Variables +//============================================================================== + swadgeMode_t swadgeHeroMode = { .modeName = shName, .wifiMode = ESP_NOW, @@ -99,24 +42,22 @@ swadgeMode_t swadgeHeroMode = { .fnAdvancedUSB = NULL, }; -shVars_t* sh; - -static const paletteColor_t colors[] = {c020, c400, c550, c004, c420, c222}; -static const buttonBit_t noteToBtn[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; - -static const shSong_t shSongList[] = { - { - .midi = "credits.mid", - .easy = "credits_e.cch", - .med = "credits_m.cch", - .hard = "credits_h.cch", - }, -}; +shVars_t* shv; //============================================================================== // Functions //============================================================================== +/** + * @brief TODO + * + * @return + */ +shVars_t* getShVars(void) +{ + return shv; +} + /** * This function is called when this mode is started. It should initialize * variables and start the mode. @@ -127,22 +68,15 @@ static void shEnterMode(void) setFrameRateUs(16667); // Allocate mode memory - sh = calloc(1, sizeof(shVars_t)); + shv = calloc(1, sizeof(shVars_t)); // Load a font - loadFont("ibm_vga8.font", &sh->ibm, false); - - // Load the track data - size_t sz = 0; - sh->numTracks = 1 + shLoadTrackData(sh, cnfsGetFile(shSongList[0].hard, &sz), sz); + loadFont("ibm_vga8.font", &shv->ibm, false); + loadFont("righteous_150.font", &shv->righteous, false); + loadFont("rodin_eb.font", &shv->rodin, false); - // Load the MIDI file - loadMidiFile(shSongList[0].midi, &sh->credits, false); - globalMidiPlayerPlaySong(&sh->credits, MIDI_BGM); - globalMidiPlayerPauseAll(); - - // Set the lead-in timer - sh->leadInUs = TRAVEL_TIME_US; + // Show initial menu + shChangeScreen(shv, SH_MENU); } /** @@ -150,24 +84,16 @@ static void shEnterMode(void) */ static void shExitMode(void) { - // Free MIDI data - unloadMidiFile(&sh->credits); - - // Free track data - free(sh->notes); - - // Free UI data - void* val; - while ((val = pop(&sh->icons))) - { - free(val); - } + // Free the screen + shChangeScreen(shv, SH_NONE); - // Free the font - freeFont(&sh->ibm); + // Free the fonts + freeFont(&shv->ibm); + freeFont(&shv->rodin); + freeFont(&shv->righteous); // Free mode memory - free(sh); + free(shv); } /** @@ -183,321 +109,48 @@ static void shMainLoop(int64_t elapsedUs) buttonEvt_t evt = {0}; while (checkButtonQueueWrapper(&evt)) { - sh->btnState = evt.state; - - if (evt.down) + switch (shv->screen) { - // Iterate through all currently shown icons - node_t* iconNode = sh->icons.first; - while (iconNode) + case SH_GAME: { - shNoteIcon_t* icon = iconNode->val; - - // If the icon matches the button - if (icon->note == btnToNote(evt.button)) - { - // Find how off the timing is - int32_t pxOff = ABS(HIT_BAR - icon->headPosY); - int32_t usOff = pxOff * TRAVEL_US_PER_PX; - printf("%" PRId32 " us off\n", usOff); - - // Check if this button hit a note - bool iconHit = false; - - // Classify the time off - if (usOff < 21500) - { - printf(" Fantastic\n"); - iconHit = true; - } - else if (usOff < 43000) - { - printf(" Marvelous\n"); - iconHit = true; - } - else if (usOff < 102000) - { - printf(" Great\n"); - iconHit = true; - } - else if (usOff < 135000) - { - printf(" Decent\n"); - iconHit = true; - } - else if (usOff < 180000) - { - printf(" Way Off\n"); - iconHit = true; - } - else - { - printf(" MISS\n"); - } - - // If it was close enough to hit - if (iconHit) - { - if (icon->tailPosY >= 0) - { - // There is a tail, don't remove the note yet - icon->headPosY = HIT_BAR; - icon->held = true; - } - else - { - // No tail, remove the icon - node_t* nextNode = iconNode->next; - removeEntry(&sh->icons, iconNode); - iconNode = nextNode; - } - } - - // the button was matched to an icon, break the loop - break; - } - - // Iterate to the next icon - iconNode = iconNode->next; + shGameInput(shv, &evt); + break; } - } - else - { - // TODO handle ups when holding tails - } - } - - // Run a lead-in timer to allow notes to spawn before the song starts playing - if (sh->leadInUs > 0) - { - sh->leadInUs -= elapsedUs; - - if (sh->leadInUs <= 0) - { - globalMidiPlayerResumeAll(); - sh->leadInUs = 0; - } - } - - // Get a reference to the player - midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); - - // Get the position of the song and when the next event is, in ms - int32_t songUs; - if (sh->leadInUs > 0) - { - songUs = -sh->leadInUs; - } - else - { - songUs = SAMPLES_TO_US(player->sampleCount); - } - - // Check events until one hasn't happened yet or the song ends - while (sh->cNote < sh->numNotes) - { - // When the next event occurs - int32_t nextEventUs = MIDI_TICKS_TO_US(sh->notes[sh->cNote].tick, player->tempo, player->reader.division); - - // Check if the icon should be spawned now to reach the hit bar in time - if (songUs + TRAVEL_TIME_US >= nextEventUs) - { - // Spawn an icon - shNoteIcon_t* ni = calloc(1, sizeof(shNoteIcon_t)); - ni->note = sh->notes[sh->cNote].note; - ni->headPosY = TFT_HEIGHT + (ICON_RADIUS * 2); - - // If this is a hold note - if (sh->notes[sh->cNote].hold) + case SH_MENU: { - // Figure out at what microsecond the tail ends - int32_t tailUs = MIDI_TICKS_TO_US(sh->notes[sh->cNote].hold, player->tempo, player->reader.division); - // Convert the time to a number of pixels - int32_t tailPx = tailUs / TRAVEL_US_PER_PX; - // Add the length pixels to the head to get the tail - ni->tailPosY = ni->headPosY + tailPx; + shMenuInput(shv, &evt); + break; } - else + case SH_GAME_END: + case SH_HIGH_SCORES: + case SH_NONE: + default: { - // No tail - ni->tailPosY = -1; + break; } - - // Start the timer at zero - ni->timer = 0; - - // Push into the list of icons - push(&sh->icons, ni); - - // Increment the track data - sh->cNote++; - } - else - { - // Nothing more to be spawned right now - break; } } - shRunTimers(sh, elapsedUs); - shDrawGame(sh); - - // Check for analog touch - // int32_t centerVal, intensityVal; - // if (getTouchCentroid(¢erVal, &intensityVal)) - // { - // printf("touch center: %" PRId32 ", intensity: %" PRId32 "\n", centerVal, intensityVal); - // } - // else - // { - // printf("no touch\n"); - // } - - // // Get the acceleration - // int16_t a_x, a_y, a_z; - // accelGetAccelVec(&a_x, &a_y, &a_z); - - // Set LEDs - led_t leds[CONFIG_NUM_LEDS] = {0}; - // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) - // { - // leds[i].r = (255 * ((i + 0) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // leds[i].g = (255 * ((i + 3) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // leds[i].b = (255 * ((i + 6) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // } - setLeds(leds, CONFIG_NUM_LEDS); -} - -/** - * @brief TODO - * - * @param shv - * @param elapsedUs - */ -static void shRunTimers(shVars_t* shv, uint32_t elapsedUs) -{ - // Track if an icon was removed - bool removed = false; - - // Run all the icon timers - node_t* iconNode = shv->icons.first; - while (iconNode) + // Run logic and outputs + switch (shv->screen) { - // Get a reference - shNoteIcon_t* icon = iconNode->val; - - // Run this icon's timer - icon->timer += elapsedUs; - while (icon->timer >= TRAVEL_US_PER_PX) - { - icon->timer -= TRAVEL_US_PER_PX; - - bool shouldRemove = false; - - // Move the whole icon up - if (!icon->held) - { - icon->headPosY--; - if (icon->tailPosY >= 0) - { - icon->tailPosY--; - } - - // If it's off screen - if (icon->headPosY < -ICON_RADIUS && (icon->tailPosY < 0)) - { - // Mark it for removal - shouldRemove = true; - } - } - else // The icon is being held - { - // Only move the tail position - if (icon->tailPosY >= HIT_BAR) - { - icon->tailPosY--; - } - - // If the tail finished - if (icon->tailPosY < HIT_BAR) - { - // Mark it for removal - shouldRemove = true; - } - } - - // If the icon should be removed - if (shouldRemove) - { - // Remove this icon - free(iconNode->val); - removeEntry(&shv->icons, iconNode); - - // Stop the while timer loop - removed = true; - break; - } - } - - // If an icon was removed - if (removed) + case SH_GAME: { - // Stop iterating through notes + shRunTimers(shv, elapsedUs); + shDrawGame(shv); break; } - else + case SH_MENU: { - // Iterate to the next - iconNode = iconNode->next; - } - } -} - -/** - * @brief TODO - * - * @param shv - */ -static void shDrawGame(shVars_t* shv) -{ - // Clear the display - clearPxTft(); - - // Draw the target area - drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); - for (int32_t i = 0; i < shv->numTracks; i++) - { - int32_t xOffset = ((i * TFT_WIDTH) / shv->numTracks) + (TFT_WIDTH / (2 * shv->numTracks)); - drawCircle(xOffset, HIT_BAR, ICON_RADIUS + 2, c555); - } - - // Draw all the icons - node_t* iconNode = shv->icons.first; - while (iconNode) - { - // Draw the icon - shNoteIcon_t* icon = iconNode->val; - int32_t xOffset = ((icon->note * TFT_WIDTH) / shv->numTracks) + (TFT_WIDTH / (2 * shv->numTracks)); - drawCircleFilled(xOffset, icon->headPosY, ICON_RADIUS, colors[icon->note]); - - // If there is a tail - if (icon->tailPosY >= 0) - { - // Draw the tail - fillDisplayArea(xOffset - 2, icon->headPosY, xOffset + 3, icon->tailPosY, colors[icon->note]); + shMenuDraw(shv, elapsedUs); + break; } - - // Iterate - iconNode = iconNode->next; - } - - // Draw indicators that the button is pressed - for (int32_t bIdx = 0; bIdx < shv->numTracks; bIdx++) - { - if (shv->btnState & noteToBtn[bIdx]) + case SH_GAME_END: + case SH_HIGH_SCORES: + case SH_NONE: + default: { - int32_t xOffset = ((bIdx * TFT_WIDTH) / shv->numTracks) + (TFT_WIDTH / (2 * shv->numTracks)); - drawCircleOutline(xOffset, HIT_BAR, ICON_RADIUS + 8, 4, colors[bIdx]); + break; } } } @@ -518,105 +171,89 @@ static void shBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, // fillDisplayArea(x, y, x + w, y + h, c555); } -/** - * @brief Callback for when menu items are selected - * - * @param label The menu item that was selected or moved to - * @param selected true if the item was selected, false if it was moved to - * @param settingVal The value of the setting, if the menu item is a settings item - */ -// static void shMenuCb(const char* label, bool selected, uint32_t settingVal) -// { -// printf("%s %s\n", label, selected ? "selected" : "scrolled to"); - -// if (selected) -// { -// } -// } - /** * @brief TODO - * - * @param shv - * @param data - * @param size - * @return uint32_t + * + * @param sh + * @param newScreen */ -static uint32_t shLoadTrackData(shVars_t* shv, const uint8_t* data, size_t size) +void shChangeScreen(shVars_t* sh, shScreen_t newScreen) { - uint32_t maxTrack = 0; - uint32_t dIdx = 0; - shv->numNotes = (data[dIdx++] << 8); - shv->numNotes |= (data[dIdx++]); - - shv->notes = calloc(shv->numNotes, sizeof(shNote_t)); - - for (int32_t nIdx = 0; nIdx < shv->numNotes; nIdx++) + // Cleanup + switch (sh->screen) { - shv->notes[nIdx].tick = (data[dIdx + 0] << 24) | // - (data[dIdx + 1] << 16) | // - (data[dIdx + 2] << 8) | // - (data[dIdx + 3] << 0); - dIdx += 4; - - shv->notes[nIdx].note = data[dIdx++]; - - if((shv->notes[nIdx].note & 0x7F) > maxTrack) + case SH_MENU: { - maxTrack = shv->notes[nIdx].note & 0x7F; + shTeardownMenu(shv); + break; } - - if (0x80 & shv->notes[nIdx].note) + case SH_GAME: { - shv->notes[nIdx].note &= 0x7F; + // Free MIDI data + unloadMidiFile(&shv->midiSong); + + // Free chart data + free(shv->chartNotes); - // Use the hold time to see when this note ends - shv->notes[nIdx].hold = (data[dIdx + 0] << 8) | // - (data[dIdx + 1] << 0); - dIdx += 2; + // Free UI data + void* val; + while ((val = pop(&shv->gameNotes))) + { + free(val); + } + break; + } + case SH_GAME_END: + case SH_HIGH_SCORES: + case SH_NONE: + default: + { + break; } } - return maxTrack; -} + sh->screen = newScreen; -/** - * @brief TODO - * - * @param btn - * @return uint32_t - */ -static uint32_t btnToNote(buttonBit_t btn) -{ - switch (btn) + // Setup + switch (sh->screen) { - case PB_LEFT: - { - return 0; - } - case PB_DOWN: - { - return 1; - } - case PB_UP: - { - return 2; - } - case PB_RIGHT: - { - return 3; - } - case PB_B: + case SH_GAME: { - return 4; + // Load the chart data + const char* chartFile; + switch (sh->difficulty) + { + default: + case SH_EASY: + { + chartFile = sh->menuSong->easy; + break; + } + case SH_MEDIUM: + { + chartFile = sh->menuSong->med; + break; + } + case SH_HARD: + { + chartFile = sh->menuSong->hard; + break; + } + } + shLoadSong(sh, sh->menuSong->midi, chartFile); + break; } - case PB_A: + case SH_MENU: { - return 5; + shSetupMenu(shv); + break; } + case SH_GAME_END: + case SH_HIGH_SCORES: + case SH_NONE: default: { - return -1; + break; } } } diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index d328e338e..d02e70c16 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -1,5 +1,99 @@ #pragma once +//============================================================================== +// Includes +//============================================================================== + #include -extern swadgeMode_t swadgeHeroMode; \ No newline at end of file +//============================================================================== +// Enums +//============================================================================== + +typedef enum +{ + SH_NONE, + SH_MENU, + SH_GAME, + SH_GAME_END, + SH_HIGH_SCORES, +} shScreen_t; + +typedef enum +{ + SH_EASY, + SH_MEDIUM, + SH_HARD, +} shDifficulty_t; + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + char* name; + char* midi; + char* easy; + char* med; + char* hard; +} shSong_t; + +typedef struct +{ + int32_t tick; + int32_t note; + int32_t hold; +} shChartNote_t; + +typedef struct +{ + int32_t note; + int32_t timer; + int32_t headPosY; + int32_t tailPosY; + bool held; +} shGameNote_t; + +typedef struct +{ + // Font and menu + font_t ibm; + font_t righteous; + font_t rodin; + menu_t* menu; + menuManiaRenderer_t* renderer; + const char* submenu; + const shSong_t* menuSong; + shDifficulty_t difficulty; + + // Currently shown screen + shScreen_t screen; + + // Song being played + midiFile_t midiSong; + int32_t leadInUs; + + // Chart data + int32_t numChartNotes; + shChartNote_t* chartNotes; + int32_t currentChartNote; + + // Drawing data + list_t gameNotes; + buttonBit_t btnState; + int32_t numFrets; +} shVars_t; + +//============================================================================== +// Extern variables +//============================================================================== + +extern swadgeMode_t swadgeHeroMode; + +//============================================================================== +// Function Declarations +//============================================================================== + +shVars_t* getShVars(void); +void shChangeScreen(shVars_t* sh, shScreen_t newScreen); diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c new file mode 100644 index 000000000..4ca15a0c6 --- /dev/null +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -0,0 +1,464 @@ +//============================================================================== +// Includes +//============================================================================== + +#include "swadgeHero_game.h" + +//============================================================================== +// Defines +//============================================================================== + +#define HIT_BAR 16 +#define GAME_NOTE_RADIUS 8 + +#define TRAVEL_US_PER_PX ((TRAVEL_TIME_US) / (TFT_HEIGHT - HIT_BAR + (2 * GAME_NOTE_RADIUS))) + +//============================================================================== +// Const Variables +//============================================================================== + +static const paletteColor_t colors[] = {c020, c400, c550, c004, c420, c222}; +static const buttonBit_t noteToBtn[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; + +//============================================================================== +// Function Declarations +//============================================================================== + +static uint32_t btnToNote(buttonBit_t btn); + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief TODO + * + * @param sh + * @param midi + * @param chart + */ +void shLoadSong(shVars_t* sh, const char* midi, const char* chart) +{ + size_t sz = 0; + sh->numFrets = 1 + shLoadChartData(sh, cnfsGetFile(chart, &sz), sz); + + // Load the MIDI file + loadMidiFile(midi, &sh->midiSong, false); + globalMidiPlayerPlaySong(&sh->midiSong, MIDI_BGM); + globalMidiPlayerPauseAll(); + + // Set the lead-in timer + sh->leadInUs = TRAVEL_TIME_US; +} + +/** + * @brief TODO + * + * @param sh + * @param data + * @param size + * @return uint32_t + */ +uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size) +{ + uint32_t maxFret = 0; + uint32_t dIdx = 0; + sh->numChartNotes = (data[dIdx++] << 8); + sh->numChartNotes |= (data[dIdx++]); + + sh->chartNotes = calloc(sh->numChartNotes, sizeof(shChartNote_t)); + + for (int32_t nIdx = 0; nIdx < sh->numChartNotes; nIdx++) + { + sh->chartNotes[nIdx].tick = (data[dIdx + 0] << 24) | // + (data[dIdx + 1] << 16) | // + (data[dIdx + 2] << 8) | // + (data[dIdx + 3] << 0); + dIdx += 4; + + sh->chartNotes[nIdx].note = data[dIdx++]; + + if ((sh->chartNotes[nIdx].note & 0x7F) > maxFret) + { + maxFret = sh->chartNotes[nIdx].note & 0x7F; + } + + if (0x80 & sh->chartNotes[nIdx].note) + { + sh->chartNotes[nIdx].note &= 0x7F; + + // Use the hold time to see when this note ends + sh->chartNotes[nIdx].hold = (data[dIdx + 0] << 8) | // + (data[dIdx + 1] << 0); + dIdx += 2; + } + } + + return maxFret; +} + +/** + * @brief TODO + * + * @param sh + * @param elapsedUs + */ +void shRunTimers(shVars_t* sh, uint32_t elapsedUs) +{ + // Run a lead-in timer to allow notes to spawn before the song starts playing + if (sh->leadInUs > 0) + { + sh->leadInUs -= elapsedUs; + + if (sh->leadInUs <= 0) + { + globalMidiPlayerResumeAll(); + sh->leadInUs = 0; + } + } + + // Get a reference to the player + midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); + + // Get the position of the song and when the next event is, in ms + int32_t songUs; + if (sh->leadInUs > 0) + { + songUs = -sh->leadInUs; + } + else + { + songUs = SAMPLES_TO_US(player->sampleCount); + } + + // Check events until one hasn't happened yet or the song ends + while (sh->currentChartNote < sh->numChartNotes) + { + // When the next event occurs + int32_t nextEventUs + = MIDI_TICKS_TO_US(sh->chartNotes[sh->currentChartNote].tick, player->tempo, player->reader.division); + + // Check if the game note should be spawned now to reach the hit bar in time + if (songUs + TRAVEL_TIME_US >= nextEventUs) + { + // Spawn an game note + shGameNote_t* ni = calloc(1, sizeof(shGameNote_t)); + ni->note = sh->chartNotes[sh->currentChartNote].note; + ni->headPosY = TFT_HEIGHT + (GAME_NOTE_RADIUS * 2); + + // If this is a hold note + if (sh->chartNotes[sh->currentChartNote].hold) + { + // Figure out at what microsecond the tail ends + int32_t tailUs = MIDI_TICKS_TO_US(sh->chartNotes[sh->currentChartNote].hold, player->tempo, + player->reader.division); + // Convert the time to a number of pixels + int32_t tailPx = tailUs / TRAVEL_US_PER_PX; + // Add the length pixels to the head to get the tail + ni->tailPosY = ni->headPosY + tailPx; + } + else + { + // No tail + ni->tailPosY = -1; + } + + // Start the timer at zero + ni->timer = 0; + + // Push into the list of game notes + push(&sh->gameNotes, ni); + + // Increment the chart data + sh->currentChartNote++; + } + else + { + // Nothing more to be spawned right now + break; + } + } + + // Track if an game note was removed + bool removed = false; + + // Run all the game note timers + node_t* gameNoteNode = sh->gameNotes.first; + while (gameNoteNode) + { + // Get a reference + shGameNote_t* gameNote = gameNoteNode->val; + + // Run this game note's timer + gameNote->timer += elapsedUs; + while (gameNote->timer >= TRAVEL_US_PER_PX) + { + gameNote->timer -= TRAVEL_US_PER_PX; + + bool shouldRemove = false; + + // Move the whole game note up + if (!gameNote->held) + { + gameNote->headPosY--; + if (gameNote->tailPosY >= 0) + { + gameNote->tailPosY--; + } + + // If it's off screen + if (gameNote->headPosY < -GAME_NOTE_RADIUS && (gameNote->tailPosY < 0)) + { + // Mark it for removal + shouldRemove = true; + } + } + else // The game note is being held + { + // Only move the tail position + if (gameNote->tailPosY >= HIT_BAR) + { + gameNote->tailPosY--; + } + + // If the tail finished + if (gameNote->tailPosY < HIT_BAR) + { + // Mark it for removal + shouldRemove = true; + } + } + + // If the game note should be removed + if (shouldRemove) + { + // Remove this game note + free(gameNoteNode->val); + removeEntry(&sh->gameNotes, gameNoteNode); + + // Stop the while timer loop + removed = true; + break; + } + } + + // If an game note was removed + if (removed) + { + // Stop iterating through notes + break; + } + else + { + // Iterate to the next + gameNoteNode = gameNoteNode->next; + } + } +} + +/** + * @brief TODO + * + * @param sh + */ +void shDrawGame(shVars_t* sh) +{ + // Clear the display + clearPxTft(); + + // Draw the target area + drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); + for (int32_t i = 0; i < sh->numFrets; i++) + { + int32_t xOffset = ((i * TFT_WIDTH) / sh->numFrets) + (TFT_WIDTH / (2 * sh->numFrets)); + drawCircle(xOffset, HIT_BAR, GAME_NOTE_RADIUS + 2, c555); + } + + // Draw all the game notes + node_t* gameNoteNode = sh->gameNotes.first; + while (gameNoteNode) + { + // Draw the game note + shGameNote_t* gameNote = gameNoteNode->val; + int32_t xOffset = ((gameNote->note * TFT_WIDTH) / sh->numFrets) + (TFT_WIDTH / (2 * sh->numFrets)); + drawCircleFilled(xOffset, gameNote->headPosY, GAME_NOTE_RADIUS, colors[gameNote->note]); + + // If there is a tail + if (gameNote->tailPosY >= 0) + { + // Draw the tail + fillDisplayArea(xOffset - 2, gameNote->headPosY, xOffset + 3, gameNote->tailPosY, colors[gameNote->note]); + } + + // Iterate + gameNoteNode = gameNoteNode->next; + } + + // Draw indicators that the button is pressed + for (int32_t bIdx = 0; bIdx < sh->numFrets; bIdx++) + { + if (sh->btnState & noteToBtn[bIdx]) + { + int32_t xOffset = ((bIdx * TFT_WIDTH) / sh->numFrets) + (TFT_WIDTH / (2 * sh->numFrets)); + drawCircleOutline(xOffset, HIT_BAR, GAME_NOTE_RADIUS + 8, 4, colors[bIdx]); + } + } + + // Set LEDs + led_t leds[CONFIG_NUM_LEDS] = {0}; + // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) + // { + // leds[i].r = (255 * ((i + 0) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // leds[i].g = (255 * ((i + 3) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // leds[i].b = (255 * ((i + 6) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); + // } + setLeds(leds, CONFIG_NUM_LEDS); +} + +/** + * @brief TODO + * + * @param sh + * @param evt + */ +void shGameInput(shVars_t* sh, buttonEvt_t* evt) +{ + sh->btnState = evt->state; + + if (evt->down) + { + // Iterate through all currently shown game notes + node_t* gameNoteNode = sh->gameNotes.first; + while (gameNoteNode) + { + shGameNote_t* gameNote = gameNoteNode->val; + + // If the game note matches the button + if (gameNote->note == btnToNote(evt->button)) + { + // Find how off the timing is + int32_t pxOff = ABS(HIT_BAR - gameNote->headPosY); + int32_t usOff = pxOff * TRAVEL_US_PER_PX; + printf("%" PRId32 " us off\n", usOff); + + // Check if this button hit a note + bool gameNoteHit = false; + + // Classify the time off + if (usOff < 21500) + { + printf(" Fantastic\n"); + gameNoteHit = true; + } + else if (usOff < 43000) + { + printf(" Marvelous\n"); + gameNoteHit = true; + } + else if (usOff < 102000) + { + printf(" Great\n"); + gameNoteHit = true; + } + else if (usOff < 135000) + { + printf(" Decent\n"); + gameNoteHit = true; + } + else if (usOff < 180000) + { + printf(" Way Off\n"); + gameNoteHit = true; + } + else + { + printf(" MISS\n"); + } + + // If it was close enough to hit + if (gameNoteHit) + { + if (gameNote->tailPosY >= 0) + { + // There is a tail, don't remove the note yet + gameNote->headPosY = HIT_BAR; + gameNote->held = true; + } + else + { + // No tail, remove the game note + node_t* nextNode = gameNoteNode->next; + removeEntry(&sh->gameNotes, gameNoteNode); + gameNoteNode = nextNode; + } + } + + // the button was matched to an game note, break the loop + break; + } + + // Iterate to the next game note + gameNoteNode = gameNoteNode->next; + } + } + else + { + // TODO handle ups when holding tails + } + + // Check for analog touch + // int32_t centerVal, intensityVal; + // if (getTouchCentroid(¢erVal, &intensityVal)) + // { + // printf("touch center: %" PRId32 ", intensity: %" PRId32 "\n", centerVal, intensityVal); + // } + // else + // { + // printf("no touch\n"); + // } + + // // Get the acceleration + // int16_t a_x, a_y, a_z; + // accelGetAccelVec(&a_x, &a_y, &a_z); +} + +/** + * @brief TODO + * + * @param btn + * @return uint32_t + */ +static uint32_t btnToNote(buttonBit_t btn) +{ + switch (btn) + { + case PB_LEFT: + { + return 0; + } + case PB_DOWN: + { + return 1; + } + case PB_UP: + { + return 2; + } + case PB_RIGHT: + { + return 3; + } + case PB_B: + { + return 4; + } + case PB_A: + { + return 5; + } + default: + { + return -1; + } + } +} diff --git a/main/modes/games/swadgeHero/swadgeHero_game.h b/main/modes/games/swadgeHero/swadgeHero_game.h new file mode 100644 index 000000000..a692b5e35 --- /dev/null +++ b/main/modes/games/swadgeHero/swadgeHero_game.h @@ -0,0 +1,11 @@ +#pragma once + +#include "mode_swadgeHero.h" + +#define TRAVEL_TIME_US 2000000 + +void shLoadSong(shVars_t* sh, const char* midi, const char* chart); +uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size); +void shGameInput(shVars_t* sh, buttonEvt_t* evt); +void shRunTimers(shVars_t* sh, uint32_t elapsedUs); +void shDrawGame(shVars_t* sh); diff --git a/main/modes/games/swadgeHero/swadgeHero_menu.c b/main/modes/games/swadgeHero/swadgeHero_menu.c new file mode 100644 index 000000000..4285887df --- /dev/null +++ b/main/modes/games/swadgeHero/swadgeHero_menu.c @@ -0,0 +1,190 @@ +//============================================================================== +// Includes +//============================================================================== + +#include "swadgeHero_menu.h" +#include "mainMenu.h" + +//============================================================================== +// Function Declarations +//============================================================================== + +static void shMenuCb(const char*, bool selected, uint32_t settingVal); + +//============================================================================== +// Const Variables +//============================================================================== + +static const shSong_t shSongList[] = { + { + .name = "GShip Credits", + .midi = "credits.mid", + .easy = "credits_e.cch", + .med = "credits_m.cch", + .hard = "credits_h.cch", + }, +}; + +static const char strSongSelect[] = "Song Select"; +static const char strEasy[] = "Easy"; +static const char strMedium[] = "Medium"; +static const char strHard[] = "Hard"; +static const char strHighScores[] = "High Scores"; +static const char strSettings[] = "Settings"; +static const char strExit[] = "Exit"; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief TODO + * + * @param sh + */ +void shSetupMenu(shVars_t* sh) +{ + // Allocate the menu + sh->menu = initMenu(swadgeHeroMode.modeName, shMenuCb); + sh->renderer = initMenuManiaRenderer(&sh->righteous, NULL, &sh->rodin); + setManiaLedsOn(sh->renderer, true); + + // Add songs to play + sh->menu = startSubMenu(sh->menu, strSongSelect); + for (int32_t sIdx = 0; sIdx < ARRAY_SIZE(shSongList); sIdx++) + { + // Add difficulties for each song + sh->menu = startSubMenu(sh->menu, shSongList[sIdx].name); + addSingleItemToMenu(sh->menu, strEasy); + addSingleItemToMenu(sh->menu, strMedium); + addSingleItemToMenu(sh->menu, strHard); + sh->menu = endSubMenu(sh->menu); + } + sh->menu = endSubMenu(sh->menu); + + // Add high score entries per-song + sh->menu = startSubMenu(sh->menu, strHighScores); + for (int32_t sIdx = 0; sIdx < ARRAY_SIZE(shSongList); sIdx++) + { + addSingleItemToMenu(sh->menu, shSongList[sIdx].name); + } + sh->menu = endSubMenu(sh->menu); + + // TODO add settings + sh->menu = startSubMenu(sh->menu, strSettings); + sh->menu = endSubMenu(sh->menu); + + // Add exit + addSingleItemToMenu(sh->menu, strExit); +} + +/** + * @brief TODO + * + * @param sh + */ +void shTeardownMenu(shVars_t* sh) +{ + setManiaLedsOn(sh->renderer, false); + deinitMenuManiaRenderer(sh->renderer); + deinitMenu(sh->menu); +} + +/** + * @brief TODO + * + * @param sh + * @param btn + */ +void shMenuInput(shVars_t* sh, buttonEvt_t* btn) +{ + sh->menu = menuButton(sh->menu, *btn); +} + +/** + * @brief TODO + * + * @param sh + */ +void shMenuDraw(shVars_t* sh, int32_t elapsedUs) +{ + drawMenuMania(sh->menu, sh->renderer, elapsedUs); +} + +/** + * @brief Callback for when menu items are selected + * + * @param label The menu item that was selected or moved to + * @param selected true if the item was selected, false if it was moved to + * @param settingVal The value of the setting, if the menu item is a settings item + */ +static void shMenuCb(const char* label, bool selected, uint32_t settingVal) +{ + // Get a reference to the game state because it isn't a callback arg + shVars_t* sh = getShVars(); + + if (selected) + { + if (strSongSelect == label) + { + // Note we're on the song select submenu + sh->submenu = strSongSelect; + } + else if (strHighScores == label) + { + // Note we're on the high score submenu + sh->submenu = strHighScores; + } + else if (strSettings == label) + { + // Note we're on the settings submenu + sh->submenu = strSettings; + } + else if (strExit == label) + { + // Exit to the main menu + switchToSwadgeMode(&mainMenuMode); + } + else if (strEasy == label) + { + printf("Play %s (Easy)\n", sh->menuSong->midi); + sh->difficulty = SH_EASY; + shChangeScreen(sh, SH_GAME); + } + else if (strMedium == label) + { + printf("Play %s (Medium)\n", sh->menuSong->midi); + sh->difficulty = SH_MEDIUM; + shChangeScreen(sh, SH_GAME); + } + else if (strHard == label) + { + printf("Play %s (Hard)\n", sh->menuSong->midi); + sh->difficulty = SH_HARD; + shChangeScreen(sh, SH_GAME); + } + else if (sh->submenu == strSongSelect || sh->submenu == strHighScores) + { + // These submenus have lists of songs. Find the song by the label + for (int32_t sIdx = 0; sIdx < ARRAY_SIZE(shSongList); sIdx++) + { + // If the name matches + if (label == shSongList[sIdx].name) + { + // Save the song + sh->menuSong = &shSongList[sIdx]; + + if (sh->submenu == strHighScores) + { + // Show high scores + printf("HS %s\n", shSongList[sIdx].midi); + } + // If playing a song, difficulty select is next + + // Found the song, so break + break; + } + } + } + } +} diff --git a/main/modes/games/swadgeHero/swadgeHero_menu.h b/main/modes/games/swadgeHero/swadgeHero_menu.h new file mode 100644 index 000000000..87cc43a96 --- /dev/null +++ b/main/modes/games/swadgeHero/swadgeHero_menu.h @@ -0,0 +1,8 @@ +#pragma once + +#include "mode_swadgeHero.h" + +void shSetupMenu(shVars_t* sh); +void shTeardownMenu(shVars_t* sh); +void shMenuInput(shVars_t* sh, buttonEvt_t* btn); +void shMenuDraw(shVars_t* sh, int32_t elapsedUs); From 981df96ab3c6ffd8c0016046afad5c36a81c3a6d Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Fri, 20 Sep 2024 21:38:46 +0000 Subject: [PATCH 28/54] Fix charts --- assets/credits.mid | Bin 5403 -> 3923 bytes assets/credits.mid.bak | Bin 5769 -> 0 bytes assets/credits_e.chart | 251 +++++++++---------- assets/credits_h.chart | 534 ++++++++++++++++++++--------------------- assets/credits_m.chart | 394 +++++++++++++++--------------- 5 files changed, 590 insertions(+), 589 deletions(-) delete mode 100644 assets/credits.mid.bak diff --git a/assets/credits.mid b/assets/credits.mid index 5ae95c3b4c2a48810c3e68b5a68a6826621eb751..c21fa5c6397e840870b9dda138c54fb35a671e23 100644 GIT binary patch literal 3923 zcmdT`>u%fF5uU_yn06r;+Z1Sx0tH$?tt?rhZnb65vPEA1vY@=Kf&S?}%05CLr4P_|NN47#-7T7;y8#Nce?F4uGBans`R4Gm#_J9MJb_2> z_|acJYYck;k_7J$LZQb$e+uspABO&yyo|UpIB?uL7-6BZ($kV~s&O8l#qa9=o2!5xXD;lWU!NZaUUwx`}Jr#WQ zm8*hOXuAcpd=i)%1dIBfqNBIHz*Zn|OI5c7Qt=g8)#_wHsZ$(kDafj1^}^NA-}GTg zhqTruhlO*mfTsJxfue$1u-L)o+`*}^02Rvj zCV(0K7y^o~=u~=35Wqv3{c(b?WzuonnUk`0lmxoG^o%Y4hwdq01MhQ zP&N2utK)5fzj+m8H?h#R&Xg@I-{E~+gGlbg&4IMuR@On?+;cavHX9V+bHjZOl96`} zkV@SkTf&XS3#x-{JF*Q6#>pQn17xG+87PeBgyr))qYTTN5w>#9R+?m`eCn3b7B4NG zBF)X}$uDbHv%MNTD{r4v;iokH>S4)1ITgEJ5i6JLS3`@W+K!tCNolfb>twgM&!u(6 zC#LHDwSLvm;fZ!dm0mEpBA>Hn&xB?xTS^7g%H(0G0%_woFbxP-Os@idJPs zmwN%TRQV)QhJ_c+>I+ybTh%fw8lAvK)w`v3U}F|s1A{VPSwq!nD6}(@pmxlpwjY)X zsNH|*HL`XX(O}^Nz2+lJ{onVRJ41ox54D>AgG&EJ3G1r4VqgPkatHpWzQYai1HGof zy{5qh(U|S&@N&D6--eKZN00{Z~%=0;u){M5MhnD&)4;&_!YaMKXHOHX;MPbk}-4E~0rY=D|~=q_G!lr>BO8<)F^3~IEkL_tlA zy#)9P7jb4KEG2+&(qBG%6p1 zTFf5jvk*!j=hG09`++G#c=Z5{`H2p@+7~FiAMj&!%3j_Orrvld@UxDW1wY-#V@h+J zcxenM?eIf(=(O@RVbr8kMH`54O%P{R?20}_ z-WX?=+(m=N(_PAOwyR`ud`u&`As2=v7!-xUb(49=489c%-Y|nxW^jt3 z`{^5T4kj)AhNe#BhPwrOFC^?B9Q|E}}U2rB}Gj(Ii@G2paHsExQ z5Ro)Nm&>HNOq%J&LCTp_Zt2@OX}3(;EtA$`(&Qc`H!)-w|uh8)&XgOwC$Q8MpG zZ-kTcZ*9#BOSFk2QdV|C+071E61SY!FGXJ8vi`kq^)F9FuC6&(EyCn2%D_*4Da7!W z#qfJVPPiZQG3|Gs#{5VO>am;O#$Y8r;W8nd-NZ*Lad?@zT*-j8KJ?bXUmqdPtazVR z$u8%8?$%_t0-HQ>!RWX$>ewVTXC2lME9`$tL$d_SM~KM(j)v5@kWG*BVa30ndXhcL z`hQzJLO1_VPvDA9nwXMY#qF>*4a_wNr@8qgrqn-T4y$g;61!x|UJDdl(tB=7s#@!~ zYaq>k5<7vZKzO6&Zs1Uhq`uao>0FD7RA$z*9bz}vXL%B$Zco`>n-*oRxi>N3R*#K& zT{gaQeAC9gD>imN#X_Hrr*k~B<8|vA3s-GqkI9I~{=%{jw}x$^+A7(Ecie4-m4Kz3 z6)Nxkz~tSa-C8T#Ew5FzTjd6ml;x|fFD#Fs!otZv*wbetvqc2r!0gG0Ecb1^$ewZU zTN^7gk=DYD1mhUvkk@Os#w$oW`N|4UWJH&Z9KQPwDaX@RSZlD-nzZ~%8AVdP6?AOS z1RK`zA&-}Z;S<@JvysKtmmKf1;p*|%HjIYsG%?D0^7`p&$lR2AWoOpb;Nc1W;K8G?F z`?UalDI3|WNMF?oRe68KMygWNYXwp*wfQyYhjO7AimuyOiKvgUd3=c{m%1evM9)T+ zP9^Z|bs=Jk?Re=BkryI*2rPmAvCfOD7ZK5#MGO|*&$%d!r;DMLAtDglMg(G)(S#Mg zAW`i=qRbLusk~Qg&OQec%AN93u7~NU68t>=0Exe|r?(9W@H~5RuLFrDTDydZ5Ql{s zMHb5)B2pI-ArC$x;vvCtKoug2Lc}(R_@xQ!&IE@DnLzLto5aciqPqhJ*eHYF7c3ROv=78zw@p)P!Q!ozhrNP(`APxg|N(%YB3e7|P-ojzXT;-MggF}WbH zoJZ{rZ!~c6c*AnbS|LxnXE)hWx6A@@$)Mr}5WIo8!?R7cqOzoz_SxtJDIdj_i7b-| z-i*tLaZfe#q)0T1c6*0EZCP+gr*IpAFGM{XG|6tS4b3$H9EqCYxGRK5P0j$x4cy`kWfq7{wbHJ}4Lwtht6}uN zvBKyZR-m8>C=x#x?M^O7wkvk$;N4eDSecjyI1|MB&T7C4HFIaVmx*V0gSR!itK*Xk zd0gnVyK@y52*7#}`X5jy_+PT|DxwJX*EX?o+F*?et{%tG4qKqyp<`Y~vyiXS4D#N| zcdGwRN4SgI=k>3SAk*Rhhd9&j2r)EtIs*B1_3t`@cDVZgrj4~_@^Z2Maq(kIcwgY% zS@&VLS?5o*-`&$?1?ry~%( zVVaPmkB{~LSBLoQbVK!DQvJtpobvm>elS)pyP$>j53&OFA__CCf0%n&%pKPMul9pL z{os#EZVv=N{}tTlQY^sejE>M99{|3P54K5pTPg(56A}Oof!cTNt914dqu`TL>l@C) z?Ii%=GFLj8BXbqIUCMY2@xzu1i4&1~^*hUPe3YOp6|GnCv6=q~M`ri6w^tFP^m zE)hyNb6d6CQ1UIPui_FzxC`)qoz9!h0hQY)-iywguC zvq#P{*p#W|!*MUiSmDL-5fkr2=N|U^%x~YrxR+UmYgmSY$?O*y`Qwt6@gR}RwxpLW zLvMC3xQ1oKY?-5mc3efj^j_HEM5Pd=6L;CDksDox8{N+~eiY@DNmSbv!jgfNGmWY6 zgcHT-LrFO8!i2OYH|tusgMtXTk&Ak|Ha~7ez~AWfBLIoJj8H8Bh6l zm}640B)6#~I(=B(S2wzej+>mwGG;k3h>k&prmfpw%d@d;k!sN*2#6y&uE@k2JjjOl zx>k7D3>b43JIGs%6EWP0At{DCF}V{v;w8q$j_=BWC;Ba;}tD1i5<2L zdnG%HmUvaRgkq7drw~rXB)1w!?`3Q<_F0l-6UM8SBxOwc%%^D+zghFQ-K1oZ)d~S3 zNF{>BI8e{q0AUzl!vOXStTq{GWCPE0Ojvx*CN@U5D;AaI(StG*gh%O3th^*kWofCl z)5WF3Un6&(Ce8pJxR3ct=$`B#DA0=%oMt_(b+X{2C9^BAAu6*j^l_ifIA9XwZM^|q zZ}?LfQq8i^^{u6_)S-(Err41ra7pDpiRms3$t_J-C6vNjI~rBHSt|ppyk%L0;;gk5hZj z3@Xc^oT#SZXPZi2t;Mv~}TyC`1BiJmS6Yv&8d?>Z59j1BwM{iMc!@AxseY zgpw6bur-0vZcDwvi0keMyEX!zbGR4CsacC0tTF*)k)sq7R@oU5D@2aken*hbUQF56 zmWD+dT0lQI3=KL`wcn7x1VM~YYzVJeYOfRp3t#|j<^J`iXSo~|`D83sNoI*2RzlPZLOB%7cNVM(}7gzWFQuu{nc zfYiWT+hGOxOJRWyyoM{JZr~>o2FV*}iXbXB@KSG}ghS;YlcO&L2)bhw_$cvF>QTt( zho|}_Bb*?ke-Bz^N9t)1CmSjwm_%w;{@YBaAuB(AM-f6cxc<%!5Y{NUUk>G{A zvu3Gt-Vy6!*)Mp9)mLf*gg0d2S$HRacXDms$-_Hkcn7SX^G;X@vE`6E+Pp(MqZeJM zYaSf$fX81D4|x1J@zBE@->7f#&EGGyg@i)~U*#(-B%Hx8JP?iRU+09w2bB-=3~W;J zu93 zYuU}ECRy&XETh~VWE|uc>TNpzzbDP-0wv5Ns*Cx;uGrm;g)rN~g;%S`)oY~~_BPqMRw2_QF!PdQFg`|>Ax`qyrM-C7A8$N8P}8|Tuc|9sur zS>spHd4K6o@oO^=zN*x!@yVU#V0CXdesB3z{PxtnuNv{0*PBa)YtH-GSnRh~zi{49 zU;6!wUxNOF@hkKh^>34NV*G>Xc4&J1CWv66{k2}4h6PU4PKgO10%HJK-*>Oze z8&)cgYpPEMd%dbNVk&=avQRSBLcniMbPu*!7%>yO2op{}X37ipc0Xlmi~Uxw<_sF9 z`~<6IY?No;`3Uc}&CO+8yXo{-%*_V}K2p<+FCMge<0vaXIx2ChWG24tpj3f;d(>0_ zS55UUhpPBgd4M3(2ZMvr0hLnbMJCeV=%Y!3ivv76-*%Ntv7O$4K45wy8WLK{gK$0?rJsm9RGJr|B3yg`6*Yo|N=HcPnw#qu zS|s`mwyR_;LW+E$8jnF;gp@{v6!jSQBcPClkAPBPr6T+D0=n z*(-fZl1j4NvOQWdlQqsv;#94OP-Ckq530^!&s3gvNKVJx-s<#j>&PS94D8hA)fIy; zO>MG+KydXZ$MIg>IjEb`p8&oZZv!MA*?*gQQTB=Xe4%&`DXixvZdDw3bEIRuds*wY@p za-Wq7D-+Mr0KIKf>JD34iF)?Ae$%o`oUpYhPue8)%cdrNtvQETGj%I~D=;?o5>}nk zQms9>!e(Hmmhfpxtu?h!LU7ET;BB^dM_{f$3W%j-?#vPI9n92MIc#$g&`so`WvYvZ zS!KRxYERy%HOb-PU;(edj6ATIY1^!hz}!@0+&SXV|C%ZnxB`V|Q-yL{s?by+>Kgk~b zm2+fKRyzQFW?}0@kAwpVs0BW3fhE|2wvM8Se!CTfjodmGoH^ZidRU>^1`X3#8hnZ5 z0%z{k@Tt)Tat$zhPh+--JPYAQ|B&)}kIt3t%v5b9^vjz)Sj+MQWvcSdb8-6FBm z@biqCa|Ug#twE#L!qELCR_?3yXTRyEdb1*rhvKt`Qt?nD^dTu}@BgdVYdeEI)7WKG z4mEeJ8znQX6{}r>w9jg5);Wg#cKa>Zk0AZcAQE=1aGAZsS(Z*Z#(z12aP|(ooc#Pd zkar<>XkDL%_xC#dMTs7GzQZff-xyku`*x34pf85xkgr4ap+C5BdMMP`MC0(99+r_x zDIk?+r_ZUhc289jYp4?6UJS3Ds@&Jg@iu3MTRRduCa+6*y=3xxM3Pq|`Sk#S?BzvN zc`BHD-Rb4zNRF^`T+8hR{C#uPa ztzj&O()X>FigpwkCl>ydMF4NIcdr% z0Hkf>ZL>yhTgKZMO70T7E?V7IBxJUOUPeJ>bSQ&+8MRwRQ#8}DdlEXVbWCD{TM5-a z@zP3|Njwi&;!*8wv4p*qsHksZCajo*&==~hFAEPV-mdj2Fgd+{1cidO3ffYgJk0q! z2=v5?@mJL@DDo|&RRg(VG5KRhknEcL53Gcc#P1N26grQhoRXT;p&XIe?Bv#mASKTN z=`cwv=Ef$cZ{SFA6W8?hHL8_S1YaxaXz#voD^M*&=t}c^3$dsQP}Oad=3SIVb$TVj zWQ0HhiX>qW(h1?YJH(*A7n0mdzQ9_Nj*0|fP{m17dVk5X1l`t!Ol|(UK?uRF0&1O2XfKG>i!ZQQ#GfqBx~!tl zeQyB>HRpb`m=prhK)1(+MJHN!9~G`txH|y?Xi4mIK7sngYobjMZGvJYbT7g5Nl1GF z-av@cO<@E;dCN+e@#scd0gp;r0ZOY2E-B;l85dIFe3T{c8CF7Yu7*^za??TH7e0F#}v}pwh5Gnj~#S}JK0TEQa%OM)8 zWpY2EGwiG%nh5C3L8_MR&oP7Y;)$m`{tXhAZ=Q?l_DmNcqatK};uH*2H<=$qJVcuL zo81hA8%sf50Y?a8f+s+n#Yuwh7L<}odQz$)2~|mEI_%AydKQwx^&ReCclu>>U2lQw z(v)PFB`uke58%=@UgUAxcuUdv@kFBX*qgJ_h+&`d{lNLF0=*>HjFpLk7VK~+Lx1IZr|8=3on*f@TU#zKk%%fCoyr7&?2 znVqSpgf`eCwT{WG@tg$~FsvrTXBjO9qls75EuUnx5{wpg%LPWWGqwFOqtVv;xbP99 z37Hn6@u8pOb~`0grYDt7|7q$Q+r3m)=OgrgGV=%|EIt?0WzE4)kUoGF>KR#d5k1WhVe cThJ!#tWSoJH<{j{0KHhkcJ;~NUi@VEFCwg1aR2}S diff --git a/assets/credits_e.chart b/assets/credits_e.chart index 4304414d1..073f1c0e4 100644 --- a/assets/credits_e.chart +++ b/assets/credits_e.chart @@ -1,14 +1,14 @@ [Song] { - Name = " "SWADGE Credits New FINAL Sync3"" + Name = "" Charter = "Edward Haas" Year = "Unknown" Offset = 0 - Resolution = 600 + Resolution = 480 Player2 = bass Difficulty = 4 - PreviewStart = 4000 - PreviewEnd = 14000 + PreviewStart = 0 + PreviewEnd = 10000 Genre = "rock" MediaType = "cd" MusicStream = "song.ogg" @@ -17,124 +17,133 @@ { 0 = TS 4 0 = B 120000 - 2400 = TS 4 - 2400 = B 125000 - 98187 = B 125000 + 0 = TS 4 + 0 = B 113999 } [ExpertSingle] { - 4925 = N 1 0 - 5863 = N 1 0 - 6500 = N 1 0 - 7463 = N 2 0 - 8088 = N 0 0 - 9025 = N 3 0 - 9650 = N 2 0 - 10463 = N 2 0 - 12488 = N 1 0 - 13113 = N 2 0 - 13750 = N 0 0 - 14538 = N 3 0 - 15488 = N 2 0 - 16900 = N 2 0 - 17525 = N 1 0 - 18162 = N 2 0 - 18788 = N 0 0 - 19413 = N 3 0 - 20050 = N 1 0 - 20675 = N 0 0 - 21313 = N 2 0 - 21938 = N 3 0 - 23513 = N 2 0 - 24250 = N 0 0 - 25088 = N 1 662 - 26050 = N 2 0 - 27625 = N 1 0 - 28250 = N 0 0 - 28875 = N 3 0 - 29500 = N 2 0 - 30125 = N 3 0 - 30788 = N 2 1087 - 32650 = N 1 0 - 33288 = N 2 0 - 33913 = N 0 0 - 34550 = N 3 0 - 35175 = N 2 0 - 35800 = N 3 925 - 37063 = N 2 0 - 37700 = N 1 0 - 38325 = N 2 0 - 38950 = N 0 0 - 39588 = N 3 0 - 40213 = N 2 0 - 40850 = N 0 0 - 41475 = N 2 0 - 42100 = N 3 0 - 42738 = N 1 650 - 43688 = N 3 0 - 43688 = E Bad_Too_High - 44313 = N 2 0 - 45263 = N 2 762 - 47150 = N 1 0 - 47775 = N 0 0 - 48413 = N 2 637 - 49350 = N 0 0 - 49988 = N 2 0 - 50775 = N 3 0 - 52188 = N 2 0 - 52188 = E Bad_Too_Low - 52825 = N 3 0 - 53612 = N 0 0 - 54400 = N 1 0 - 55188 = N 3 0 - 57238 = N 2 0 - 57863 = N 1 0 - 58488 = N 0 0 - 59125 = N 2 0 - 60062 = N 0 0 - 60863 = N 3 0 - 62275 = N 1 0 - 62900 = N 3 650 - 63850 = N 2 0 - 64800 = N 1 0 - 65425 = N 2 0 - 66063 = N 1 0 - 67963 = N 1 0 - 68588 = N 0 0 - 69213 = N 2 0 - 70150 = N 1 0 - 70938 = N 1 0 - 72988 = N 0 0 - 73613 = N 1 0 - 74250 = N 0 0 - 74887 = N 3 0 - 75513 = N 1 0 - 77400 = N 2 0 - 78038 = N 1 0 - 78663 = N 2 0 - 79288 = N 0 0 - 79925 = N 3 0 - 80863 = N 3 0 - 81500 = N 2 0 - 82438 = N 3 0 - 83075 = N 2 650 - 84025 = N 1 0 - 84650 = N 0 0 - 85275 = N 0 0 - 86538 = N 0 0 - 86538 = E Bad_Too_Low - 87413 = N 1 0 - 88113 = N 2 0 - 88763 = N 2 0 - 89675 = N 3 0 - 90338 = N 2 0 - 91263 = N 0 0 - 91263 = E Bad_Too_Low - 91900 = N 1 0 - 92525 = N 2 0 - 93163 = N 3 0 - 95050 = N 1 0 - 95675 = N 2 0 - 96313 = N 0 0 - 96938 = N 0 0 + 1920 = N 1 0 + 2640 = N 1 0 + 3120 = N 1 0 + 3840 = N 2 0 + 4320 = N 1 0 + 4800 = N 3 0 + 5280 = N 2 0 + 5760 = N 3 0 + 6240 = N 2 911 + 7680 = N 1 0 + 8160 = N 2 0 + 8640 = N 0 0 + 9120 = N 3 0 + 9600 = N 2 0 + 10080 = N 3 720 + 11040 = N 2 0 + 11520 = N 1 0 + 12000 = N 2 0 + 12480 = N 0 0 + 12960 = N 3 0 + 13440 = N 1 0 + 13920 = N 0 0 + 14400 = N 2 0 + 14880 = N 3 0 + 15360 = N 2 480 + 16080 = N 1 0 + 16560 = N 0 0 + 17040 = N 0 0 + 18000 = N 2 0 + 19200 = N 1 0 + 19680 = N 0 0 + 20160 = N 3 0 + 20640 = N 2 0 + 21120 = N 3 0 + 21600 = N 2 911 + 23040 = N 1 0 + 23520 = N 2 0 + 24000 = N 0 0 + 24480 = N 3 0 + 24960 = N 2 0 + 25440 = N 3 720 + 26400 = N 2 0 + 26880 = N 1 0 + 27360 = N 2 0 + 27840 = N 0 0 + 28320 = N 3 0 + 28800 = N 1 0 + 29280 = N 0 0 + 29760 = N 2 0 + 30240 = N 3 0 + 30720 = N 2 480 + 31440 = N 1 0 + 31920 = N 0 0 + 32400 = N 2 0 + 34080 = N 1 0 + 34560 = N 0 0 + 35040 = N 2 480 + 35760 = N 0 0 + 36240 = N 2 0 + 36840 = N 3 0 + 37920 = N 1 0 + 37920 = E Bad_Too_Low + 38400 = N 2 0 + 38880 = N 0 0 + 39360 = N 1 0 + 39840 = N 3 0 + 40320 = N 1 911 + 41760 = N 3 0 + 41760 = E Bad_Too_High + 42240 = N 1 0 + 42720 = N 0 0 + 43200 = N 2 0 + 43680 = N 0 0 + 44160 = N 1 0 + 44640 = N 3 720 + 45600 = N 1 0 + 46080 = N 3 0 + 46840 = N 2 0 + 47520 = N 1 0 + 48000 = N 2 0 + 48480 = N 1 0 + 48960 = N 0 720 + 49920 = N 1 0 + 50400 = N 0 0 + 50880 = N 3 0 + 51360 = N 2 0 + 51840 = N 3 0 + 52320 = N 2 911 + 53760 = N 1 0 + 54240 = N 2 0 + 54720 = N 0 0 + 55200 = N 3 0 + 55680 = N 2 0 + 56160 = N 3 720 + 57120 = N 2 0 + 57600 = N 1 0 + 58080 = N 2 0 + 58560 = N 0 0 + 59040 = N 3 0 + 59520 = N 1 0 + 60000 = N 0 0 + 60480 = N 2 0 + 60960 = N 3 0 + 61440 = N 2 480 + 62160 = N 1 0 + 62640 = N 0 0 + 63120 = N 0 0 + 64080 = N 1 0 + 64740 = N 2 0 + 65280 = N 0 0 + 65280 = E Bad_Too_Low + 65760 = N 0 0 + 66240 = N 1 0 + 66720 = N 2 0 + 67200 = N 3 0 + 67680 = N 3 0 + 68160 = N 1 0 + 68160 = E Bad_Too_Low + 68640 = N 2 0 + 69120 = N 3 0 + 70560 = N 1 0 + 71040 = N 2 0 + 71520 = N 0 0 + 72000 = N 0 0 } diff --git a/assets/credits_h.chart b/assets/credits_h.chart index f16cd8baa..3b64e443f 100644 --- a/assets/credits_h.chart +++ b/assets/credits_h.chart @@ -1,14 +1,14 @@ [Song] { - Name = " "SWADGE Credits New FINAL Sync3"" + Name = "" Charter = "Edward Haas" Year = "Unknown" Offset = 0 - Resolution = 600 + Resolution = 480 Player2 = bass Difficulty = 4 - PreviewStart = 4000 - PreviewEnd = 14000 + PreviewStart = 0 + PreviewEnd = 10000 Genre = "rock" MediaType = "cd" MusicStream = "song.ogg" @@ -17,273 +17,267 @@ { 0 = TS 4 0 = B 120000 - 2400 = TS 4 - 2400 = B 125000 - 98187 = B 125000 + 0 = TS 4 + 0 = B 113999 } [ExpertSingle] { - 4925 = N 1 0 - 5075 = N 1 0 - 5237 = N 1 0 - 5863 = N 1 0 - 6025 = N 1 0 - 6188 = N 1 0 - 6500 = N 1 0 - 7463 = N 2 0 - 8088 = N 0 0 - 8225 = N 1 0 - 8388 = N 2 0 - 8513 = N 4 0 - 8700 = N 5 0 - 9025 = N 3 0 - 9175 = N 3 0 - 9338 = N 2 0 - 9650 = N 1 0 - 9963 = N 5 0 - 10463 = N 2 0 - 10625 = N 2 913 - 12488 = N 1 0 - 12800 = N 1 0 - 12963 = N 2 0 - 13113 = N 3 0 - 13588 = N 4 0 - 13750 = N 0 0 - 14375 = N 5 0 - 14538 = N 4 0 - 14688 = N 2 0 - 14863 = N 0 0 - 15000 = N 1 0 - 15488 = N 2 0 - 15638 = N 3 925 - 16900 = N 2 0 - 17213 = N 1 0 - 17525 = N 0 0 - 17850 = N 0 0 - 18000 = N 1 0 - 18162 = N 2 0 - 18625 = N 4 0 - 18788 = N 1 0 - 19100 = N 0 0 - 19413 = N 4 0 - 19738 = N 3 0 - 20050 = N 3 0 - 20363 = N 2 0 - 20525 = N 2 0 - 20675 = N 1 0 - 20988 = N 2 0 - 21313 = N 3 0 - 21938 = N 4 0 - 22413 = N 5 0 - 22563 = N 2 650 - 23513 = N 5 0 - 23513 = E Bad_Too_High - 23675 = N 4 0 - 23825 = N 3 0 - 24138 = N 2 0 - 24250 = N 3 0 - 24350 = N 2 0 - 24463 = N 1 0 - 24775 = N 2 0 - 25088 = N 4 662 - 26050 = N 5 0 - 26213 = N 4 0 - 26350 = N 3 0 - 27625 = N 2 0 - 28250 = N 0 0 - 28388 = N 1 0 - 28563 = N 2 0 - 28688 = N 4 0 - 28875 = N 5 0 - 29188 = N 4 0 - 29350 = N 4 0 - 29500 = N 3 0 - 29813 = N 2 0 - 30125 = N 5 0 - 30638 = N 3 0 - 30788 = N 3 1087 - 32650 = N 1 0 - 32963 = N 1 0 - 33125 = N 2 0 - 33288 = N 3 0 - 33750 = N 4 0 - 33913 = N 0 0 - 34550 = N 5 0 - 34700 = N 4 0 - 34863 = N 3 0 - 35025 = N 2 0 - 35175 = N 3 0 - 35663 = N 4 0 - 35800 = N 5 925 - 37063 = N 4 0 - 37388 = N 3 0 - 37700 = N 2 0 - 38013 = N 2 0 - 38175 = N 3 0 - 38325 = N 4 0 - 38800 = N 5 0 - 38950 = N 1 0 - 39275 = N 0 0 - 39588 = N 4 0 - 39900 = N 3 0 - 40213 = N 3 0 - 40525 = N 4 0 - 40688 = N 2 0 - 40850 = N 1 0 - 41163 = N 2 0 - 41475 = N 3 0 - 42100 = N 4 0 - 42575 = N 5 0 - 42738 = N 4 650 - 43688 = N 3 0 - 43838 = N 2 0 - 44000 = N 1 0 - 44313 = N 0 0 - 44413 = N 2 0 - 44525 = N 1 0 - 44625 = N 0 0 - 44938 = N 5 0 - 45263 = N 4 762 - 47150 = N 3 0 - 47463 = N 2 0 - 47775 = N 1 0 - 48250 = N 2 0 - 48413 = N 5 637 - 49350 = N 0 0 - 49513 = N 1 0 - 49675 = N 2 0 - 49825 = N 3 0 - 49988 = N 4 0 - 50138 = N 5 0 - 50300 = N 3 0 - 50775 = N 2 0 - 50925 = N 3 0 - 52188 = N 4 0 - 52513 = N 4 0 - 52825 = N 5 0 - 53450 = N 1 0 - 53612 = N 2 0 - 53763 = N 3 0 - 53925 = N 5 0 - 54075 = N 4 0 - 54400 = N 2 0 - 54675 = N 5 0 - 55188 = N 4 0 - 55338 = N 3 1400 - 57238 = N 5 0 - 57238 = E Bad_Too_High - 57550 = N 4 0 - 57863 = N 3 0 - 58488 = N 0 0 - 58650 = N 1 0 - 58813 = N 3 0 - 58963 = N 5 0 - 59125 = N 4 0 - 59438 = N 3 0 - 59550 = N 4 0 - 59650 = N 3 0 - 59750 = N 2 0 - 60062 = N 3 0 - 60388 = N 4 0 - 60863 = N 5 0 - 61013 = N 5 912 - 62275 = N 1 0 - 62588 = N 3 0 - 62863 = N 3 0 - 63850 = N 2 0 - 63950 = N 4 0 - 64063 = N 3 0 - 64163 = N 2 0 - 64800 = N 1 0 - 65425 = N 2 0 - 66063 = N 1 0 - 66375 = N 2 0 - 66525 = N 1 0 - 66687 = N 0 863 - 67963 = N 2 0 - 68588 = N 0 0 - 68725 = N 1 0 - 68887 = N 2 0 - 69025 = N 4 0 - 69213 = N 5 0 - 69525 = N 4 0 - 69675 = N 4 0 - 69838 = N 3 0 - 70150 = N 2 0 - 70463 = N 5 0 - 70938 = N 3 0 - 71100 = N 3 1088 - 72988 = N 1 0 - 73300 = N 1 0 - 73463 = N 2 0 - 73613 = N 3 0 - 74088 = N 4 0 - 74250 = N 0 0 - 74887 = N 5 0 - 75038 = N 4 0 - 75200 = N 3 0 - 75363 = N 2 0 - 75513 = N 3 0 - 75988 = N 4 0 - 76138 = N 5 925 - 77400 = N 4 0 - 77713 = N 3 0 - 78038 = N 2 0 - 78350 = N 2 0 - 78500 = N 3 0 - 78663 = N 4 0 - 79138 = N 5 0 - 79288 = N 1 0 - 79600 = N 0 0 - 79925 = N 3 0 - 80238 = N 2 0 - 80550 = N 2 0 - 80863 = N 3 0 - 80975 = N 2 0 - 81075 = N 1 0 - 81188 = N 0 0 - 81500 = N 1 0 - 81813 = N 2 0 - 82438 = N 3 0 - 82913 = N 5 0 - 83075 = N 4 650 - 84025 = N 3 0 - 84125 = N 3 0 - 84338 = N 2 0 - 84650 = N 1 0 - 84963 = N 0 0 - 85275 = N 1 0 - 85588 = N 2 650 - 86538 = N 3 0 - 86700 = N 2 0 - 86850 = N 0 0 - 87413 = N 1 0 - 88113 = N 2 0 - 88763 = N 2 0 - 89050 = N 1 0 - 89200 = N 2 0 - 89363 = N 3 0 - 89675 = N 4 0 - 90013 = N 5 0 - 90338 = N 0 0 - 90638 = N 1 0 - 91263 = N 1 0 - 91575 = N 0 0 - 91738 = N 1 0 - 91900 = N 2 0 - 92213 = N 3 0 - 92525 = N 4 0 - 92838 = N 4 0 - 92838 = E Bad_Too_Low - 93163 = N 4 0 - 93263 = N 5 1475 - 95050 = N 1 0 - 95200 = N 2 0 - 95363 = N 3 0 - 95525 = N 4 0 - 95675 = N 5 0 - 96313 = N 0 0 - 96625 = N 0 0 - 96788 = N 0 0 - 96938 = N 0 0 + 1920 = N 1 0 + 2040 = N 1 0 + 2160 = N 1 0 + 2640 = N 1 0 + 2760 = N 1 0 + 2880 = N 1 0 + 3120 = N 1 0 + 3840 = N 2 0 + 4320 = N 0 0 + 4440 = N 1 0 + 4560 = N 2 0 + 4640 = N 4 0 + 4800 = N 5 0 + 5040 = N 4 0 + 5115 = N 5 0 + 5280 = N 3 0 + 5520 = N 2 0 + 5760 = N 5 0 + 6120 = N 3 0 + 6240 = N 3 911 + 7680 = N 1 0 + 7920 = N 1 0 + 8040 = N 2 0 + 8160 = N 3 0 + 8520 = N 4 0 + 8640 = N 0 0 + 9120 = N 5 0 + 9240 = N 4 0 + 9360 = N 2 0 + 9480 = N 1 0 + 9600 = N 2 0 + 9960 = N 3 0 + 10080 = N 4 720 + 11040 = N 3 0 + 11280 = N 2 0 + 11520 = N 1 0 + 11760 = N 1 0 + 11880 = N 2 0 + 12000 = N 3 0 + 12360 = N 4 0 + 12480 = N 1 0 + 12720 = N 0 0 + 12960 = N 4 0 + 13200 = N 3 0 + 13440 = N 3 0 + 13680 = N 2 0 + 13800 = N 2 0 + 13920 = N 1 0 + 14160 = N 2 0 + 14400 = N 3 0 + 14880 = N 4 0 + 15240 = N 5 0 + 15360 = N 4 480 + 16080 = N 3 0 + 16200 = N 2 0 + 16320 = N 1 0 + 16560 = N 0 0 + 16635 = N 2 0 + 16800 = N 1 0 + 17040 = N 2 0 + 17280 = N 4 480 + 18000 = N 5 0 + 18120 = N 4 0 + 18240 = N 3 0 + 19200 = N 2 0 + 19680 = N 0 0 + 19800 = N 1 0 + 19920 = N 2 0 + 20000 = N 4 0 + 20160 = N 5 0 + 20400 = N 4 0 + 20520 = N 4 0 + 20640 = N 3 0 + 20880 = N 2 0 + 21120 = N 5 0 + 21480 = N 3 0 + 21600 = N 3 911 + 23040 = N 1 0 + 23280 = N 1 0 + 23400 = N 2 0 + 23520 = N 3 0 + 23880 = N 4 0 + 24000 = N 0 0 + 24480 = N 5 0 + 24600 = N 4 0 + 24720 = N 2 0 + 24840 = N 1 0 + 24960 = N 2 0 + 25320 = N 3 0 + 25440 = N 4 720 + 26400 = N 3 0 + 26640 = N 2 0 + 26880 = N 1 0 + 27120 = N 1 0 + 27240 = N 2 0 + 27360 = N 3 0 + 27720 = N 4 0 + 27840 = N 1 0 + 28080 = N 0 0 + 28320 = N 5 0 + 28560 = N 4 0 + 28800 = N 4 0 + 29040 = N 5 0 + 29160 = N 2 0 + 29280 = N 1 0 + 29520 = N 2 0 + 29760 = N 3 0 + 30240 = N 4 0 + 30600 = N 5 0 + 30720 = N 2 480 + 31440 = N 5 0 + 31440 = E Bad_Too_High + 31560 = N 4 0 + 31680 = N 3 0 + 31920 = N 2 0 + 32000 = N 3 0 + 32080 = N 2 0 + 32160 = N 1 0 + 32400 = N 4 0 + 32640 = N 3 0 + 34080 = N 2 0 + 34320 = N 1 0 + 34560 = N 0 0 + 34920 = N 1 0 + 35040 = N 3 480 + 35760 = N 0 0 + 35880 = N 1 0 + 36000 = N 2 0 + 36120 = N 3 0 + 36240 = N 4 0 + 36360 = N 5 0 + 36480 = N 2 0 + 36840 = N 1 0 + 36960 = N 3 0 + 37920 = N 4 0 + 38160 = N 4 0 + 38400 = N 5 0 + 38880 = N 0 0 + 39000 = N 1 0 + 39120 = N 2 0 + 39240 = N 5 0 + 39360 = N 4 0 + 39600 = N 2 0 + 39810 = N 5 0 + 40200 = N 4 0 + 40320 = N 3 911 + 41760 = N 4 0 + 41760 = E Bad_Too_High + 42000 = N 3 0 + 42240 = N 2 0 + 42720 = N 0 0 + 42840 = N 1 0 + 42960 = N 2 0 + 43080 = N 4 0 + 43200 = N 3 0 + 43440 = N 2 0 + 43530 = N 3 0 + 43620 = N 2 0 + 43920 = N 2 0 + 44160 = N 4 0 + 44520 = N 5 0 + 44640 = N 5 720 + 45600 = N 4 0 + 45840 = N 5 0 + 46080 = N 5 0 + 46840 = N 4 0 + 46960 = N 4 0 + 47040 = N 3 0 + 47520 = N 2 0 + 48000 = N 3 0 + 48480 = N 1 0 + 48720 = N 2 0 + 48840 = N 1 0 + 48960 = N 0 720 + 49920 = N 2 0 + 50400 = N 0 0 + 50520 = N 1 0 + 50680 = N 2 0 + 50880 = N 5 0 + 51120 = N 4 0 + 51240 = N 4 0 + 51360 = N 3 0 + 51600 = N 2 0 + 51840 = N 5 0 + 52200 = N 3 0 + 52320 = N 3 911 + 53760 = N 1 0 + 54000 = N 1 0 + 54120 = N 2 0 + 54240 = N 3 0 + 54600 = N 4 0 + 54720 = N 0 0 + 55200 = N 5 0 + 55320 = N 4 0 + 55440 = N 2 0 + 55560 = N 1 0 + 55680 = N 2 0 + 56040 = N 3 0 + 56160 = N 4 720 + 57120 = N 3 0 + 57360 = N 2 0 + 57600 = N 1 0 + 57840 = N 1 0 + 57960 = N 2 0 + 58080 = N 3 0 + 58440 = N 4 0 + 58560 = N 1 0 + 58800 = N 0 0 + 59040 = N 5 0 + 59280 = N 4 0 + 59520 = N 4 0 + 59760 = N 5 0 + 59880 = N 2 0 + 60000 = N 1 0 + 60240 = N 2 0 + 60480 = N 3 0 + 60960 = N 4 0 + 61320 = N 5 0 + 61440 = N 4 480 + 62160 = N 3 0 + 62235 = N 3 0 + 62400 = N 2 0 + 62640 = N 1 0 + 62880 = N 0 0 + 63120 = N 1 0 + 63360 = N 2 480 + 64080 = N 3 0 + 64200 = N 2 0 + 64320 = N 0 0 + 64740 = N 1 0 + 65280 = N 2 0 + 65760 = N 2 0 + 66000 = N 1 0 + 66120 = N 2 0 + 66240 = N 3 0 + 66480 = N 4 0 + 66720 = N 5 0 + 66960 = N 0 0 + 67200 = N 1 0 + 67680 = N 1 0 + 67920 = N 0 0 + 68040 = N 1 0 + 68160 = N 2 0 + 68400 = N 3 0 + 68640 = N 4 0 + 68880 = N 4 0 + 68880 = E Bad_Too_Low + 69120 = N 4 0 + 69200 = N 5 1120 + 70560 = N 1 0 + 70680 = N 2 0 + 70800 = N 3 0 + 70920 = N 4 0 + 71040 = N 5 0 + 71520 = N 0 0 + 71760 = N 0 0 + 71880 = N 0 0 + 72000 = N 0 0 } diff --git a/assets/credits_m.chart b/assets/credits_m.chart index e468c74a1..dd3236b04 100644 --- a/assets/credits_m.chart +++ b/assets/credits_m.chart @@ -1,14 +1,14 @@ [Song] { - Name = " "SWADGE Credits New FINAL Sync3"" + Name = "" Charter = "Edward Haas" Year = "Unknown" Offset = 0 - Resolution = 600 + Resolution = 480 Player2 = bass Difficulty = 4 - PreviewStart = 4000 - PreviewEnd = 14000 + PreviewStart = 0 + PreviewEnd = 10000 Genre = "rock" MediaType = "cd" MusicStream = "song.ogg" @@ -17,201 +17,199 @@ { 0 = TS 4 0 = B 120000 - 2400 = TS 4 - 2400 = B 125000 - 98187 = B 125000 + 0 = TS 4 + 0 = B 113999 } [ExpertSingle] { - 4925 = N 1 0 - 5237 = N 1 0 - 5863 = N 1 0 - 6188 = N 1 0 - 6500 = N 1 0 - 7463 = N 2 0 - 8088 = N 0 0 - 8388 = N 3 0 - 8700 = N 4 0 - 9025 = N 3 0 - 9338 = N 2 0 - 9650 = N 1 0 - 9963 = N 4 0 - 10463 = N 1 0 - 12488 = N 0 0 - 12800 = N 0 0 - 13113 = N 2 0 - 13588 = N 3 0 - 14375 = N 4 0 - 14688 = N 2 0 - 15000 = N 0 0 - 15488 = N 2 0 - 16900 = N 2 0 - 17213 = N 1 0 - 17525 = N 0 0 - 17850 = N 0 0 - 18162 = N 2 0 - 18625 = N 3 0 - 19100 = N 0 0 - 19413 = N 3 0 - 19738 = N 2 0 - 20050 = N 2 0 - 20363 = N 1 0 - 20675 = N 0 0 - 20988 = N 1 0 - 21313 = N 2 0 - 21938 = N 3 0 - 22413 = N 4 0 - 23513 = N 3 0 - 23825 = N 2 0 - 24138 = N 1 0 - 24350 = N 1 0 - 24775 = N 1 0 - 25088 = N 3 662 - 26050 = N 4 0 - 26350 = N 2 0 - 27625 = N 1 0 - 28250 = N 0 0 - 28563 = N 1 0 - 28875 = N 4 0 - 29188 = N 3 0 - 29500 = N 2 0 - 29813 = N 1 0 - 30125 = N 4 0 - 30638 = N 1 0 - 32650 = N 0 0 - 32963 = N 0 0 - 33288 = N 2 0 - 33750 = N 3 0 - 34550 = N 4 0 - 34863 = N 2 0 - 35175 = N 0 0 - 35663 = N 2 0 - 37063 = N 2 0 - 37388 = N 1 0 - 37700 = N 0 0 - 38013 = N 0 0 - 38325 = N 2 0 - 38800 = N 3 0 - 39275 = N 0 0 - 39588 = N 3 0 - 39900 = N 2 0 - 40213 = N 2 0 - 40525 = N 3 0 - 40850 = N 0 0 - 41163 = N 1 0 - 41475 = N 2 0 - 42100 = N 3 0 - 42575 = N 4 0 - 43688 = N 3 0 - 44000 = N 2 0 - 44313 = N 1 0 - 44525 = N 1 0 - 44938 = N 2 0 - 45263 = N 1 762 - 47150 = N 2 0 - 47150 = E Bad_Too_High - 47463 = N 1 0 - 47775 = N 0 0 - 48250 = N 1 0 - 49350 = N 0 0 - 49675 = N 1 0 - 49988 = N 3 0 - 50300 = N 4 0 - 50775 = N 0 0 - 51013 = N 1 862 - 52188 = N 1 0 - 52513 = N 1 0 - 52825 = N 2 0 - 53450 = N 0 0 - 53763 = N 1 0 - 54075 = N 3 0 - 54400 = N 2 0 - 54675 = N 4 0 - 55188 = N 3 0 - 57238 = N 2 0 - 57550 = N 1 0 - 57863 = N 0 0 - 58488 = N 0 0 - 58488 = E Bad_Too_High - 58813 = N 1 0 - 59125 = N 2 0 - 59438 = N 1 0 - 59650 = N 1 0 - 60062 = N 1 0 - 60388 = N 3 0 - 60863 = N 4 0 - 62275 = N 0 0 - 62588 = N 3 0 - 62863 = N 3 0 - 63850 = N 2 0 - 64063 = N 2 0 - 64800 = N 1 0 - 65425 = N 2 0 - 66063 = N 1 0 - 66375 = N 2 0 - 66687 = N 0 863 - 67963 = N 1 0 - 68588 = N 0 0 - 68887 = N 1 0 - 69213 = N 3 0 - 69525 = N 2 0 - 69838 = N 1 0 - 70150 = N 0 0 - 70463 = N 4 0 - 70938 = N 1 0 - 72988 = N 0 0 - 73300 = N 0 0 - 73613 = N 2 0 - 74088 = N 3 0 - 74887 = N 4 0 - 75200 = N 2 0 - 75513 = N 0 0 - 75988 = N 2 0 - 77400 = N 2 0 - 77713 = N 1 0 - 78038 = N 0 0 - 78350 = N 0 0 - 78663 = N 1 0 - 79138 = N 2 0 - 79600 = N 0 0 - 79925 = N 3 0 - 80238 = N 2 0 - 80550 = N 2 0 - 80863 = N 3 0 - 81075 = N 1 0 - 81500 = N 1 0 - 81813 = N 2 0 - 82438 = N 3 0 - 82913 = N 4 0 - 84025 = N 3 0 - 84338 = N 2 0 - 84650 = N 1 0 - 84963 = N 0 0 - 85275 = N 1 0 - 85588 = N 2 650 - 86538 = N 3 0 - 86850 = N 0 0 - 87413 = N 1 0 - 88113 = N 2 0 - 88763 = N 2 0 - 89050 = N 0 0 - 89363 = N 1 0 - 89675 = N 2 0 - 90013 = N 3 0 - 90338 = N 2 0 - 90638 = N 4 0 - 91263 = N 4 0 - 91575 = N 3 0 - 91900 = N 1 0 - 91900 = E Bad_Too_Low - 92213 = N 2 0 - 92525 = N 3 0 - 92838 = N 4 0 - 93163 = N 4 0 - 95050 = N 1 0 - 95363 = N 2 0 - 95675 = N 3 0 - 96313 = N 0 0 - 96625 = N 0 0 - 96938 = N 0 0 + 1920 = N 1 0 + 2160 = N 1 0 + 2640 = N 1 0 + 2880 = N 1 0 + 3120 = N 1 0 + 3840 = N 2 0 + 4320 = N 1 0 + 4560 = N 2 0 + 4800 = N 4 0 + 5040 = N 3 0 + 5280 = N 2 0 + 5520 = N 1 0 + 5760 = N 4 0 + 6120 = N 1 0 + 7680 = N 0 0 + 7920 = N 0 0 + 8160 = N 2 0 + 8520 = N 3 0 + 9120 = N 4 0 + 9360 = N 2 0 + 9600 = N 0 0 + 9960 = N 2 0 + 11040 = N 2 0 + 11280 = N 1 0 + 11520 = N 0 0 + 11760 = N 0 0 + 12000 = N 2 0 + 12360 = N 3 0 + 12720 = N 0 0 + 12960 = N 3 0 + 13200 = N 2 0 + 13440 = N 2 0 + 13680 = N 1 0 + 13920 = N 0 0 + 14160 = N 1 0 + 14400 = N 2 0 + 14880 = N 3 0 + 15240 = N 4 0 + 16080 = N 3 0 + 16320 = N 2 0 + 16560 = N 1 0 + 16800 = N 0 0 + 17040 = N 1 0 + 17280 = N 3 480 + 18000 = N 4 0 + 18240 = N 2 0 + 19200 = N 1 0 + 19680 = N 0 0 + 19920 = N 1 0 + 20160 = N 4 0 + 20400 = N 3 0 + 20640 = N 2 0 + 20880 = N 1 0 + 21120 = N 4 0 + 21480 = N 1 0 + 23040 = N 0 0 + 23280 = N 0 0 + 23520 = N 2 0 + 23880 = N 3 0 + 24480 = N 4 0 + 24720 = N 2 0 + 24960 = N 0 0 + 25320 = N 2 0 + 26400 = N 2 0 + 26640 = N 1 0 + 26880 = N 0 0 + 27120 = N 0 0 + 27360 = N 2 0 + 27720 = N 3 0 + 28080 = N 0 0 + 28320 = N 4 0 + 28560 = N 3 0 + 28800 = N 3 0 + 29040 = N 4 0 + 29280 = N 0 0 + 29520 = N 1 0 + 29760 = N 2 0 + 30240 = N 3 0 + 30600 = N 4 0 + 31440 = N 3 0 + 31680 = N 2 0 + 31920 = N 1 0 + 32080 = N 1 0 + 32400 = N 2 0 + 32640 = N 1 0 + 34080 = N 3 0 + 34080 = E Bad_Too_High + 34320 = N 2 0 + 34560 = N 1 0 + 34920 = N 2 0 + 35760 = N 0 0 + 36000 = N 1 0 + 36240 = N 3 0 + 36480 = N 4 0 + 36840 = N 1 0 + 37000 = N 3 680 + 37920 = N 3 0 + 38160 = N 3 0 + 38400 = N 4 0 + 38880 = N 0 0 + 39120 = N 1 0 + 39360 = N 3 0 + 39600 = N 2 0 + 39810 = N 4 0 + 40200 = N 4 0 + 40200 = E Bad_Too_High + 41760 = N 3 0 + 42000 = N 2 0 + 42240 = N 1 0 + 42720 = N 0 0 + 42960 = N 1 0 + 43200 = N 2 0 + 43440 = N 1 0 + 43620 = N 1 0 + 43920 = N 1 0 + 44160 = N 3 0 + 44520 = N 4 0 + 45600 = N 2 0 + 45840 = N 4 0 + 46080 = N 4 0 + 46840 = N 3 0 + 47040 = N 2 0 + 47520 = N 1 0 + 48000 = N 2 0 + 48480 = N 1 0 + 48720 = N 2 0 + 48960 = N 1 720 + 49920 = N 2 0 + 50400 = N 0 0 + 50680 = N 1 0 + 50880 = N 3 0 + 51120 = N 2 0 + 51360 = N 1 0 + 51600 = N 0 0 + 51840 = N 4 0 + 52200 = N 1 0 + 53760 = N 0 0 + 54000 = N 0 0 + 54240 = N 2 0 + 54600 = N 3 0 + 55200 = N 4 0 + 55440 = N 2 0 + 55680 = N 0 0 + 56040 = N 2 0 + 57120 = N 2 0 + 57360 = N 1 0 + 57600 = N 0 0 + 57840 = N 0 0 + 58080 = N 2 0 + 58440 = N 3 0 + 58800 = N 0 0 + 59040 = N 4 0 + 59280 = N 3 0 + 59520 = N 3 0 + 59760 = N 4 0 + 60000 = N 0 0 + 60240 = N 1 0 + 60480 = N 2 0 + 60960 = N 3 0 + 61320 = N 4 0 + 62160 = N 3 0 + 62400 = N 2 0 + 62640 = N 1 0 + 62880 = N 0 0 + 63120 = N 1 0 + 63360 = N 2 480 + 64080 = N 3 0 + 64320 = N 0 0 + 64740 = N 1 0 + 65280 = N 2 0 + 65760 = N 2 0 + 66000 = N 1 0 + 66240 = N 2 0 + 66480 = N 3 0 + 66720 = N 4 0 + 66960 = N 0 0 + 67200 = N 1 0 + 67680 = N 1 0 + 67920 = N 0 0 + 68160 = N 1 0 + 68400 = N 2 0 + 68640 = N 3 0 + 68880 = N 4 0 + 69120 = N 4 0 + 70560 = N 1 0 + 70800 = N 2 0 + 71040 = N 3 0 + 71520 = N 0 0 + 71760 = N 0 0 + 72000 = N 0 0 } From 7c8e820f62b810b67471e07876b086541d280669 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sat, 21 Sep 2024 00:01:22 +0000 Subject: [PATCH 29/54] Use correct colors and buttons for difficulties Fix firmware build --- main/CMakeLists.txt | 2 + main/modes/games/swadgeHero/mode_swadgeHero.h | 3 + main/modes/games/swadgeHero/swadgeHero_game.c | 86 ++++++++----------- 3 files changed, 39 insertions(+), 52 deletions(-) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index d2c5d3c14..ae3404e99 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -31,6 +31,8 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "modes/games/bigbug/tilemap_bigbug.c" "modes/games/bigbug/pathfinding_bigbug.c" "modes/games/swadgeHero/mode_swadgeHero.c" + "modes/games/swadgeHero/swadgeHero_game.c" + "modes/games/swadgeHero/swadgeHero_menu.c" "modes/games/ultimateTTT/ultimateTTT.c" "modes/games/ultimateTTT/ultimateTTTgame.c" "modes/games/ultimateTTT/ultimateTTThowTo.c" diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index d02e70c16..d65f46576 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -78,6 +78,9 @@ typedef struct int32_t numChartNotes; shChartNote_t* chartNotes; int32_t currentChartNote; + paletteColor_t const* colors; + buttonBit_t const* noteToBtn; + int32_t const* btnToNote; // Drawing data list_t gameNotes; diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 4ca15a0c6..9a7ae1759 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -17,14 +17,17 @@ // Const Variables //============================================================================== -static const paletteColor_t colors[] = {c020, c400, c550, c004, c420, c222}; -static const buttonBit_t noteToBtn[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; +static const paletteColor_t colors_e[] = {c020, c004, c420, c222}; +static const buttonBit_t noteToBtn_e[] = {PB_LEFT, PB_RIGHT, PB_B, PB_A}; +static const int32_t btnToNote_e[] = {-1, -1, 0, 1, 3, 2}; -//============================================================================== -// Function Declarations -//============================================================================== +static const paletteColor_t colors_m[] = {c020, c400, c550, c004, c420, c222}; +static const buttonBit_t noteToBtn_m[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; +static const int32_t btnToNote_m[] = {2, 1, 0, 3, 5, 4}; -static uint32_t btnToNote(buttonBit_t btn); +static const paletteColor_t colors_h[] = {c020, c400, c550, c004, c420, c222}; +static const buttonBit_t noteToBtn_h[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; +static const int32_t btnToNote_h[] = {2, 1, 0, 3, 5, 4}; //============================================================================== // Functions @@ -42,6 +45,25 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) size_t sz = 0; sh->numFrets = 1 + shLoadChartData(sh, cnfsGetFile(chart, &sz), sz); + if (6 == sh->numFrets) + { + sh->btnToNote = btnToNote_h; + sh->noteToBtn = noteToBtn_h; + sh->colors = colors_h; + } + else if (5 == sh->numFrets) + { + sh->btnToNote = btnToNote_m; + sh->noteToBtn = noteToBtn_m; + sh->colors = colors_m; + } + else if (4 == sh->numFrets) + { + sh->btnToNote = btnToNote_e; + sh->noteToBtn = noteToBtn_e; + sh->colors = colors_e; + } + // Load the MIDI file loadMidiFile(midi, &sh->midiSong, false); globalMidiPlayerPlaySong(&sh->midiSong, MIDI_BGM); @@ -281,13 +303,13 @@ void shDrawGame(shVars_t* sh) // Draw the game note shGameNote_t* gameNote = gameNoteNode->val; int32_t xOffset = ((gameNote->note * TFT_WIDTH) / sh->numFrets) + (TFT_WIDTH / (2 * sh->numFrets)); - drawCircleFilled(xOffset, gameNote->headPosY, GAME_NOTE_RADIUS, colors[gameNote->note]); + drawCircleFilled(xOffset, gameNote->headPosY, GAME_NOTE_RADIUS, sh->colors[gameNote->note]); // If there is a tail if (gameNote->tailPosY >= 0) { // Draw the tail - fillDisplayArea(xOffset - 2, gameNote->headPosY, xOffset + 3, gameNote->tailPosY, colors[gameNote->note]); + fillDisplayArea(xOffset - 2, gameNote->headPosY, xOffset + 3, gameNote->tailPosY, sh->colors[gameNote->note]); } // Iterate @@ -297,10 +319,10 @@ void shDrawGame(shVars_t* sh) // Draw indicators that the button is pressed for (int32_t bIdx = 0; bIdx < sh->numFrets; bIdx++) { - if (sh->btnState & noteToBtn[bIdx]) + if (sh->btnState & sh->noteToBtn[bIdx]) { int32_t xOffset = ((bIdx * TFT_WIDTH) / sh->numFrets) + (TFT_WIDTH / (2 * sh->numFrets)); - drawCircleOutline(xOffset, HIT_BAR, GAME_NOTE_RADIUS + 8, 4, colors[bIdx]); + drawCircleOutline(xOffset, HIT_BAR, GAME_NOTE_RADIUS + 8, 4, sh->colors[bIdx]); } } @@ -334,7 +356,7 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) shGameNote_t* gameNote = gameNoteNode->val; // If the game note matches the button - if (gameNote->note == btnToNote(evt->button)) + if (gameNote->note == sh->btnToNote[31 - __builtin_clz(evt->button)]) { // Find how off the timing is int32_t pxOff = ABS(HIT_BAR - gameNote->headPosY); @@ -388,6 +410,7 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) { // No tail, remove the game note node_t* nextNode = gameNoteNode->next; + free(gameNoteNode->val); removeEntry(&sh->gameNotes, gameNoteNode); gameNoteNode = nextNode; } @@ -421,44 +444,3 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // int16_t a_x, a_y, a_z; // accelGetAccelVec(&a_x, &a_y, &a_z); } - -/** - * @brief TODO - * - * @param btn - * @return uint32_t - */ -static uint32_t btnToNote(buttonBit_t btn) -{ - switch (btn) - { - case PB_LEFT: - { - return 0; - } - case PB_DOWN: - { - return 1; - } - case PB_UP: - { - return 2; - } - case PB_RIGHT: - { - return 3; - } - case PB_B: - { - return 4; - } - case PB_A: - { - return 5; - } - default: - { - return -1; - } - } -} From 5c2dd857d1821742919c326ef6b13cba3a0c0a1e Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sat, 21 Sep 2024 15:54:12 +0000 Subject: [PATCH 30/54] Increase CONFIG_ESP_MAIN_TASK_STACK_SIZE Move all menu allocations to SPIRAM --- main/menu/menu.c | 19 ++++++++++--------- main/menu/menuManiaRenderer.c | 2 +- .../quickSettings/menuQuickSettingsRenderer.c | 8 +++++--- main/utils/wheel_menu.c | 6 ++++-- sdkconfig | 4 ++-- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/main/menu/menu.c b/main/menu/menu.c index 1047cd9f4..9819592d7 100644 --- a/main/menu/menu.c +++ b/main/menu/menu.c @@ -4,6 +4,7 @@ #include #include +#include #include "menu.h" #include "macros.h" @@ -47,11 +48,11 @@ static void deinitSubMenu(menu_t* menu); */ menu_t* initMenu(const char* title, menuCb cbFunc) { - menu_t* menu = calloc(1, sizeof(menu_t)); + menu_t* menu = heap_caps_calloc(1, sizeof(menu_t), MALLOC_CAP_SPIRAM); menu->title = title; menu->cbFunc = cbFunc; menu->currentItem = NULL; - menu->items = calloc(1, sizeof(list_t)); + menu->items = heap_caps_calloc(1, sizeof(list_t), MALLOC_CAP_SPIRAM); menu->parentMenu = NULL; menu->showBattery = false; return menu; @@ -130,15 +131,15 @@ static void deinitSubMenu(menu_t* menu) menu_t* startSubMenu(menu_t* menu, const char* label) { // Allocate a submenu - menu_t* subMenu = calloc(1, sizeof(menu_t)); + menu_t* subMenu = heap_caps_calloc(1, sizeof(menu_t), MALLOC_CAP_SPIRAM); subMenu->title = label; subMenu->cbFunc = menu->cbFunc; subMenu->currentItem = NULL; - subMenu->items = calloc(1, sizeof(list_t)); + subMenu->items = heap_caps_calloc(1, sizeof(list_t), MALLOC_CAP_SPIRAM); subMenu->parentMenu = menu; // Allocate a new menu item - menuItem_t* newItem = calloc(1, sizeof(menuItem_t)); + menuItem_t* newItem = heap_caps_calloc(1, sizeof(menuItem_t), MALLOC_CAP_SPIRAM); newItem->label = label; newItem->options = NULL; newItem->numOptions = 0; @@ -191,7 +192,7 @@ menu_t* endSubMenu(menu_t* menu) */ menuItem_t* addSingleItemToMenu(menu_t* menu, const char* label) { - menuItem_t* newItem = calloc(1, sizeof(menuItem_t)); + menuItem_t* newItem = heap_caps_calloc(1, sizeof(menuItem_t), MALLOC_CAP_SPIRAM); newItem->label = label; newItem->options = NULL; newItem->numOptions = 0; @@ -260,7 +261,7 @@ void removeSingleItemFromMenu(menu_t* menu, const char* label) */ void addMultiItemToMenu(menu_t* menu, const char* const* labels, uint8_t numLabels, uint8_t currentLabel) { - menuItem_t* newItem = calloc(1, sizeof(menuItem_t)); + menuItem_t* newItem = heap_caps_calloc(1, sizeof(menuItem_t), MALLOC_CAP_SPIRAM); newItem->label = NULL; newItem->options = labels; newItem->numOptions = numLabels; @@ -327,7 +328,7 @@ void removeMultiItemFromMenu(menu_t* menu, const char* const* labels) */ void addSettingsItemToMenu(menu_t* menu, const char* label, const settingParam_t* bounds, int32_t val) { - menuItem_t* newItem = calloc(1, sizeof(menuItem_t)); + menuItem_t* newItem = heap_caps_calloc(1, sizeof(menuItem_t), MALLOC_CAP_SPIRAM); newItem->label = label; newItem->minSetting = bounds->min; newItem->maxSetting = bounds->max; @@ -404,7 +405,7 @@ void addSettingsOptionsItemToMenu(menu_t* menu, const char* settingLabel, const const int32_t* optionValues, uint8_t numOptions, const settingParam_t* bounds, int32_t currentValue) { - menuItem_t* newItem = calloc(1, sizeof(menuItem_t)); + menuItem_t* newItem = heap_caps_calloc(1, sizeof(menuItem_t), MALLOC_CAP_SPIRAM); newItem->label = settingLabel; newItem->options = optionLabels; newItem->settingVals = optionValues; diff --git a/main/menu/menuManiaRenderer.c b/main/menu/menuManiaRenderer.c index 0f1c6931a..531c28330 100644 --- a/main/menu/menuManiaRenderer.c +++ b/main/menu/menuManiaRenderer.c @@ -89,7 +89,7 @@ static void drawMenuText(menuManiaRenderer_t* renderer, const char* text, int16_ */ menuManiaRenderer_t* initMenuManiaRenderer(font_t* titleFont, font_t* titleFontOutline, font_t* menuFont) { - menuManiaRenderer_t* renderer = calloc(1, sizeof(menuManiaRenderer_t)); + menuManiaRenderer_t* renderer = heap_caps_calloc(1, sizeof(menuManiaRenderer_t), MALLOC_CAP_SPIRAM); // Save or allocate title font if (NULL == titleFont) diff --git a/main/modes/system/quickSettings/menuQuickSettingsRenderer.c b/main/modes/system/quickSettings/menuQuickSettingsRenderer.c index 45ac9fdf4..e2e74a822 100644 --- a/main/modes/system/quickSettings/menuQuickSettingsRenderer.c +++ b/main/modes/system/quickSettings/menuQuickSettingsRenderer.c @@ -9,7 +9,9 @@ #include "font.h" #include "menu.h" #include "palette.h" -#include "esp_log.h" + +#include +#include #include #include @@ -109,7 +111,7 @@ static const quickSettingsItemInfo_t* getInfoForLabel(menuQuickSettingsRenderer_ */ menuQuickSettingsRenderer_t* initMenuQuickSettingsRenderer(font_t* font) { - menuQuickSettingsRenderer_t* renderer = calloc(1, sizeof(menuQuickSettingsRenderer_t)); + menuQuickSettingsRenderer_t* renderer = heap_caps_calloc(1, sizeof(menuQuickSettingsRenderer_t), MALLOC_CAP_SPIRAM); renderer->font = font; return renderer; @@ -243,7 +245,7 @@ void drawMenuQuickSettings(menu_t* menu, menuQuickSettingsRenderer_t* renderer, void quickSettingsRendererCustomizeOption(menuQuickSettingsRenderer_t* renderer, const char* label, const wsg_t* onWsg, const wsg_t* offWsg, const char* maxLabel, const char* minLabel) { - quickSettingsItemInfo_t* info = calloc(1, sizeof(quickSettingsItemInfo_t)); + quickSettingsItemInfo_t* info = heap_caps_calloc(1, sizeof(quickSettingsItemInfo_t), MALLOC_CAP_SPIRAM); info->label = label; info->onWsg = onWsg; diff --git a/main/utils/wheel_menu.c b/main/utils/wheel_menu.c index 2d34fbb04..18097c016 100644 --- a/main/utils/wheel_menu.c +++ b/main/utils/wheel_menu.c @@ -14,6 +14,8 @@ #include "macros.h" #include "geometry.h" +#include + #include #include @@ -73,7 +75,7 @@ static int cmpDrawInfo(const void* a, const void* b); */ wheelMenuRenderer_t* initWheelMenu(const font_t* font, uint16_t anchorAngle, const rectangle_t* textBox) { - wheelMenuRenderer_t* renderer = calloc(1, sizeof(wheelMenuRenderer_t)); + wheelMenuRenderer_t* renderer = heap_caps_calloc(1, sizeof(wheelMenuRenderer_t), MALLOC_CAP_SPIRAM); renderer->font = font; renderer->anchorAngle = anchorAngle; @@ -1050,7 +1052,7 @@ static wheelItemInfo_t* findOrAddInfo(wheelMenuRenderer_t* renderer, const char* if (NULL == info) { - info = calloc(1, sizeof(wheelItemInfo_t)); + info = heap_caps_calloc(1, sizeof(wheelItemInfo_t), MALLOC_CAP_SPIRAM); info->label = label; // Defaults for colors diff --git a/sdkconfig b/sdkconfig index e720e1850..b672d7f78 100644 --- a/sdkconfig +++ b/sdkconfig @@ -1016,7 +1016,7 @@ CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP=y CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32 CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304 -CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=4608 CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y # CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set CONFIG_ESP_MAIN_TASK_AFFINITY=0x0 @@ -1965,7 +1965,7 @@ CONFIG_ESP32S2_ALLOW_RTC_FAST_MEM_AS_HEAP=y # CONFIG_ESP32S2_MEMPROT_FEATURE is not set CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 -CONFIG_MAIN_TASK_STACK_SIZE=3584 +CONFIG_MAIN_TASK_STACK_SIZE=4608 CONFIG_CONSOLE_UART_DEFAULT=y # CONFIG_CONSOLE_UART_CUSTOM is not set # CONFIG_CONSOLE_UART_NONE is not set From cbb3c88c172955729f65ada6d0a81539d6bde2eb Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sat, 21 Sep 2024 15:58:56 +0000 Subject: [PATCH 31/54] Allocate SwadgeHero in SPIRAM --- main/modes/games/swadgeHero/mode_swadgeHero.h | 1 + main/modes/games/swadgeHero/swadgeHero_game.c | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index d65f46576..0147059e9 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -4,6 +4,7 @@ // Includes //============================================================================== +#include #include //============================================================================== diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 9a7ae1759..28ce1ccd1 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -65,7 +65,7 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) } // Load the MIDI file - loadMidiFile(midi, &sh->midiSong, false); + loadMidiFile(midi, &sh->midiSong, true); globalMidiPlayerPlaySong(&sh->midiSong, MIDI_BGM); globalMidiPlayerPauseAll(); @@ -88,7 +88,7 @@ uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size) sh->numChartNotes = (data[dIdx++] << 8); sh->numChartNotes |= (data[dIdx++]); - sh->chartNotes = calloc(sh->numChartNotes, sizeof(shChartNote_t)); + sh->chartNotes = heap_caps_calloc(sh->numChartNotes, sizeof(shChartNote_t), MALLOC_CAP_SPIRAM); for (int32_t nIdx = 0; nIdx < sh->numChartNotes; nIdx++) { @@ -164,7 +164,7 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) if (songUs + TRAVEL_TIME_US >= nextEventUs) { // Spawn an game note - shGameNote_t* ni = calloc(1, sizeof(shGameNote_t)); + shGameNote_t* ni = heap_caps_calloc(1, sizeof(shGameNote_t), MALLOC_CAP_SPIRAM); ni->note = sh->chartNotes[sh->currentChartNote].note; ni->headPosY = TFT_HEIGHT + (GAME_NOTE_RADIUS * 2); @@ -309,7 +309,8 @@ void shDrawGame(shVars_t* sh) if (gameNote->tailPosY >= 0) { // Draw the tail - fillDisplayArea(xOffset - 2, gameNote->headPosY, xOffset + 3, gameNote->tailPosY, sh->colors[gameNote->note]); + fillDisplayArea(xOffset - 2, gameNote->headPosY, xOffset + 3, gameNote->tailPosY, + sh->colors[gameNote->note]); } // Iterate From baf0aecd1fa95c4da92c493970d8126bb43590f5 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sun, 22 Sep 2024 12:04:56 +0000 Subject: [PATCH 32/54] Squash merge origin/main into ssr --- .../workflows/build-firmware-and-emulator.yml | 10 + Doxyfile | 1 + assets/fonts/pango-fw.font.png | Bin 0 -> 1294 bytes assets/pango/levels/mockup.bin | Bin 0 -> 274 bytes assets/pango/levels/preset.bin | Bin 0 -> 274 bytes assets/pango/sounds/bgmCastle.mid | Bin 0 -> 2217 bytes assets/pango/sounds/bgmDeMAGio.mid | Bin 0 -> 2217 bytes assets/pango/sounds/bgmGameOver.mid | Bin 0 -> 209 bytes assets/pango/sounds/bgmGameStart.mid | Bin 0 -> 200 bytes assets/pango/sounds/bgmIntro.mid | Bin 0 -> 233 bytes assets/pango/sounds/bgmNameEntry.mid | Bin 0 -> 648 bytes assets/pango/sounds/bgmSmooth.mid | Bin 0 -> 2889 bytes assets/pango/sounds/bgmUnderground.mid | Bin 0 -> 4569 bytes assets/pango/sounds/snd1up.mid | Bin 0 -> 152 bytes assets/pango/sounds/sndBlockStop.mid | Bin 0 -> 168 bytes assets/pango/sounds/sndCheckpoint.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndCoin.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndDie.mid | Bin 0 -> 184 bytes assets/pango/sounds/sndHit.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndHurt.mid | Bin 0 -> 176 bytes assets/pango/sounds/sndJump1.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndJump2.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndJump3.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndLevelClearA.mid | Bin 0 -> 209 bytes assets/pango/sounds/sndLevelClearB.mid | Bin 0 -> 185 bytes assets/pango/sounds/sndLevelClearC.mid | Bin 0 -> 161 bytes assets/pango/sounds/sndLevelClearD.mid | Bin 0 -> 161 bytes assets/pango/sounds/sndLevelClearS.mid | Bin 0 -> 241 bytes assets/pango/sounds/sndMenuConfirm.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndMenuDeny.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndMenuSelect.mid | Bin 0 -> 144 bytes assets/pango/sounds/sndOutOfTime.mid | Bin 0 -> 248 bytes assets/pango/sounds/sndPause.mid | Bin 0 -> 160 bytes assets/pango/sounds/sndPowerUp.mid | Bin 0 -> 184 bytes assets/pango/sounds/sndSlide.mid | Bin 0 -> 176 bytes assets/pango/sounds/sndSpawn.mid | Bin 0 -> 176 bytes assets/pango/sounds/sndSquish.mid | Bin 0 -> 152 bytes assets/pango/sounds/sndWarp.mid | Bin 0 -> 200 bytes assets/pango/sounds/sndWaveBall.mid | Bin 0 -> 160 bytes assets/pango/sprites/blockfragment.png | Bin 0 -> 998 bytes assets/pango/sprites/break-000.png | Bin 0 -> 1019 bytes assets/pango/sprites/break-001.png | Bin 0 -> 1028 bytes assets/pango/sprites/break-002.png | Bin 0 -> 1034 bytes assets/pango/sprites/break-003.png | Bin 0 -> 1034 bytes assets/pango/sprites/kr-000.png | Bin 0 -> 1079 bytes assets/pango/sprites/kr-001.png | Bin 0 -> 1077 bytes assets/pango/sprites/kr-002.png | Bin 0 -> 1066 bytes assets/pango/sprites/kr-003.png | Bin 0 -> 1061 bytes assets/pango/sprites/kr-004.png | Bin 0 -> 1058 bytes assets/pango/sprites/kr-005.png | Bin 0 -> 1071 bytes assets/pango/sprites/kr-006.png | Bin 0 -> 1061 bytes assets/pango/sprites/kr-007.png | Bin 0 -> 1080 bytes assets/pango/sprites/kr-008.png | Bin 0 -> 1085 bytes assets/pango/sprites/kr-009.png | Bin 0 -> 1065 bytes assets/pango/sprites/kr-010.png | Bin 0 -> 1079 bytes assets/pango/sprites/kr-011.png | Bin 0 -> 1065 bytes assets/pango/sprites/kr-012.png | Bin 0 -> 1066 bytes assets/pango/sprites/kr-013.png | Bin 0 -> 1087 bytes assets/pango/sprites/kr-014.png | Bin 0 -> 1070 bytes assets/pango/sprites/kr-015.png | Bin 0 -> 1042 bytes assets/pango/sprites/pa-000.png | Bin 0 -> 1081 bytes assets/pango/sprites/pa-001.png | Bin 0 -> 1079 bytes assets/pango/sprites/pa-002.png | Bin 0 -> 1084 bytes assets/pango/sprites/pa-003.png | Bin 0 -> 1082 bytes assets/pango/sprites/pa-004.png | Bin 0 -> 1067 bytes assets/pango/sprites/pa-005.png | Bin 0 -> 1075 bytes assets/pango/sprites/pa-006.png | Bin 0 -> 1071 bytes assets/pango/sprites/pa-007.png | Bin 0 -> 1079 bytes assets/pango/sprites/pa-008.png | Bin 0 -> 1077 bytes assets/pango/sprites/pa-009.png | Bin 0 -> 1091 bytes assets/pango/sprites/pa-010.png | Bin 0 -> 1084 bytes assets/pango/sprites/pa-011.png | Bin 0 -> 1081 bytes assets/pango/sprites/pa-012.png | Bin 0 -> 1070 bytes assets/pango/sprites/pa-013.png | Bin 0 -> 1083 bytes assets/pango/sprites/pa-014.png | Bin 0 -> 1082 bytes assets/pango/sprites/pa-015.png | Bin 0 -> 1047 bytes assets/pango/sprites/pa-100.png | Bin 0 -> 314 bytes assets/pango/sprites/pa-101.png | Bin 0 -> 323 bytes assets/pango/sprites/pa-102.png | Bin 0 -> 308 bytes assets/pango/sprites/pa-103.png | Bin 0 -> 324 bytes assets/pango/sprites/pa-104.png | Bin 0 -> 290 bytes assets/pango/sprites/pa-105.png | Bin 0 -> 310 bytes assets/pango/sprites/pa-106.png | Bin 0 -> 305 bytes assets/pango/sprites/pa-107.png | Bin 0 -> 338 bytes assets/pango/sprites/pa-108.png | Bin 0 -> 324 bytes assets/pango/sprites/pa-109.png | Bin 0 -> 331 bytes assets/pango/sprites/pa-110.png | Bin 0 -> 310 bytes assets/pango/sprites/pa-111.png | Bin 0 -> 322 bytes assets/pango/sprites/pa-112.png | Bin 0 -> 318 bytes assets/pango/sprites/pa-113.png | Bin 0 -> 325 bytes assets/pango/sprites/pa-114.png | Bin 0 -> 319 bytes assets/pango/sprites/pa-en-000.png | Bin 0 -> 1077 bytes assets/pango/sprites/pa-en-001.png | Bin 0 -> 1081 bytes assets/pango/sprites/pa-en-002.png | Bin 0 -> 1079 bytes assets/pango/sprites/pa-en-003.png | Bin 0 -> 1077 bytes assets/pango/sprites/pa-en-004.png | Bin 0 -> 1073 bytes assets/pango/sprites/pa-en-005.png | Bin 0 -> 1070 bytes assets/pango/sprites/pa-en-006.png | Bin 0 -> 1069 bytes assets/pango/sprites/pa-en-007.png | Bin 0 -> 1073 bytes assets/pango/sprites/pa-en-008.png | Bin 0 -> 1062 bytes assets/pango/sprites/po-000.png | Bin 0 -> 349 bytes assets/pango/sprites/po-001.png | Bin 0 -> 359 bytes assets/pango/sprites/po-002.png | Bin 0 -> 283 bytes assets/pango/sprites/po-003.png | Bin 0 -> 300 bytes assets/pango/sprites/po-004.png | Bin 0 -> 317 bytes assets/pango/sprites/po-005.png | Bin 0 -> 343 bytes assets/pango/sprites/po-006.png | Bin 0 -> 318 bytes assets/pango/sprites/po-007.png | Bin 0 -> 361 bytes assets/pango/sprites/po-008.png | Bin 0 -> 350 bytes assets/pango/sprites/po-009.png | Bin 0 -> 279 bytes assets/pango/sprites/po-010.png | Bin 0 -> 295 bytes assets/pango/sprites/po-011.png | Bin 0 -> 358 bytes assets/pango/sprites/po-012.png | Bin 0 -> 338 bytes assets/pango/sprites/po-013.png | Bin 0 -> 353 bytes assets/pango/sprites/po-014.png | Bin 0 -> 318 bytes assets/pango/sprites/px-000.png | Bin 0 -> 287 bytes assets/pango/sprites/px-001.png | Bin 0 -> 288 bytes assets/pango/sprites/px-002.png | Bin 0 -> 303 bytes assets/pango/sprites/px-003.png | Bin 0 -> 292 bytes assets/pango/sprites/px-004.png | Bin 0 -> 291 bytes assets/pango/sprites/px-005.png | Bin 0 -> 316 bytes assets/pango/sprites/px-006.png | Bin 0 -> 304 bytes assets/pango/sprites/px-007.png | Bin 0 -> 341 bytes assets/pango/sprites/px-008.png | Bin 0 -> 321 bytes assets/pango/sprites/px-009.png | Bin 0 -> 303 bytes assets/pango/sprites/px-010.png | Bin 0 -> 293 bytes assets/pango/sprites/px-011.png | Bin 0 -> 317 bytes assets/pango/sprites/px-012.png | Bin 0 -> 327 bytes assets/pango/sprites/px-013.png | Bin 0 -> 331 bytes assets/pango/sprites/px-014.png | Bin 0 -> 308 bytes assets/pango/tiles/.DS_Store | Bin 0 -> 6148 bytes assets/pango/tiles/pa-tile-000.png | Bin 0 -> 975 bytes assets/pango/tiles/pa-tile-001.png | Bin 0 -> 1005 bytes assets/pango/tiles/pa-tile-002.png | Bin 0 -> 992 bytes assets/pango/tiles/pa-tile-003.png | Bin 0 -> 1009 bytes assets/pango/tiles/pa-tile-004.png | Bin 0 -> 984 bytes assets/pango/tiles/pa-tile-005.png | Bin 0 -> 1011 bytes assets/pango/tiles/pa-tile-006.png | Bin 0 -> 1016 bytes assets/pango/tiles/pa-tile-007.png | Bin 0 -> 992 bytes assets/pango/tiles/pa-tile-008.png | Bin 0 -> 985 bytes assets/pango/tiles/pa-tile-009.png | Bin 0 -> 1022 bytes assets/pango/tiles/pa-tile-010.png | Bin 0 -> 1021 bytes assets/pango/tiles/pa-tile-011.png | Bin 0 -> 1022 bytes assets/pango/tiles/pa-tile-012.png | Bin 0 -> 1023 bytes assets/pango/tiles/pa-tile-013.png | Bin 0 -> 1030 bytes assets/pango/tiles/pa-tile-014.png | Bin 0 -> 1031 bytes assets/pango/tiles/pa-tile-015.png | Bin 0 -> 1031 bytes docs/EMULATOR.md | 17 +- docs/MIDI.md | 102 + emulator/resources/SwadgeEmulator.desktop | 11 + emulator/resources/install.sh | 57 + emulator/src/extensions/modes/ext_modes.c | 4 +- emulator/src/idf/midi_device.c | 5 + main/CMakeLists.txt | 19 +- main/display/wsg.c | 14 +- main/display/wsg.h | 7 +- main/display/wsgPalette.c | 404 ++++ main/display/wsgPalette.h | 91 + main/menu/menuManiaRenderer.c | 4 +- main/midi/midiFileParser.c | 54 +- main/midi/midiPlayer.c | 493 ++++- main/midi/midiPlayer.h | 39 + main/modes/games/pango/paEntity.c | 1547 +++++++++++++++ main/modes/games/pango/paEntity.h | 191 ++ main/modes/games/pango/paEntityManager.c | 439 ++++ main/modes/games/pango/paEntityManager.h | 68 + main/modes/games/pango/paGameData.c | 282 +++ main/modes/games/pango/paGameData.h | 86 + main/modes/games/pango/paLeveldef.h | 19 + main/modes/games/pango/paSoundManager.c | 97 + main/modes/games/pango/paSoundManager.h | 65 + main/modes/games/pango/paSprite.h | 20 + main/modes/games/pango/paTables.h | 69 + main/modes/games/pango/paTilemap.c | 429 ++++ main/modes/games/pango/paTilemap.h | 111 ++ main/modes/games/pango/paWsgManager.c | 295 +++ main/modes/games/pango/paWsgManager.h | 147 ++ main/modes/games/pango/pango.c | 1757 +++++++++++++++++ main/modes/games/pango/pango.h | 47 + main/modes/games/pango/pango_typedef.h | 71 + main/modes/music/usbsynth/mode_synth.c | 88 +- main/modes/system/mainMenu/mainMenu.c | 6 + main/swadge2024.c | 3 + tools/pango_editor/mockup.bin | Bin 0 -> 274 bytes tools/pango_editor/mockup.tmx | 23 + tools/pango_editor/pa-tileset.png | Bin 0 -> 1209 bytes tools/pango_editor/pa-tileset.tsx | 4 + tools/pango_editor/pango-editor.js | 111 ++ tools/pango_editor/pango-tiles.tsx | 4 + tools/pango_editor/pango.tiled-project | 11 + tools/pango_editor/pango.tiled-session | 65 + tools/pango_editor/preset.tmx | 23 + tools/sandbox_test/usbhid_test/sandbox.c | 2 +- tools/sandbox_test/usbhid_test/test/Makefile | 12 +- tools/sandbox_test/usbhid_test/test/hidtest.c | 21 +- .../usbhid_test/test/winbuild.bat | 1 + 196 files changed, 7368 insertions(+), 78 deletions(-) create mode 100644 assets/fonts/pango-fw.font.png create mode 100644 assets/pango/levels/mockup.bin create mode 100644 assets/pango/levels/preset.bin create mode 100644 assets/pango/sounds/bgmCastle.mid create mode 100644 assets/pango/sounds/bgmDeMAGio.mid create mode 100644 assets/pango/sounds/bgmGameOver.mid create mode 100644 assets/pango/sounds/bgmGameStart.mid create mode 100644 assets/pango/sounds/bgmIntro.mid create mode 100644 assets/pango/sounds/bgmNameEntry.mid create mode 100644 assets/pango/sounds/bgmSmooth.mid create mode 100644 assets/pango/sounds/bgmUnderground.mid create mode 100644 assets/pango/sounds/snd1up.mid create mode 100644 assets/pango/sounds/sndBlockStop.mid create mode 100644 assets/pango/sounds/sndCheckpoint.mid create mode 100644 assets/pango/sounds/sndCoin.mid create mode 100644 assets/pango/sounds/sndDie.mid create mode 100644 assets/pango/sounds/sndHit.mid create mode 100644 assets/pango/sounds/sndHurt.mid create mode 100644 assets/pango/sounds/sndJump1.mid create mode 100644 assets/pango/sounds/sndJump2.mid create mode 100644 assets/pango/sounds/sndJump3.mid create mode 100644 assets/pango/sounds/sndLevelClearA.mid create mode 100644 assets/pango/sounds/sndLevelClearB.mid create mode 100644 assets/pango/sounds/sndLevelClearC.mid create mode 100644 assets/pango/sounds/sndLevelClearD.mid create mode 100644 assets/pango/sounds/sndLevelClearS.mid create mode 100644 assets/pango/sounds/sndMenuConfirm.mid create mode 100644 assets/pango/sounds/sndMenuDeny.mid create mode 100644 assets/pango/sounds/sndMenuSelect.mid create mode 100644 assets/pango/sounds/sndOutOfTime.mid create mode 100644 assets/pango/sounds/sndPause.mid create mode 100644 assets/pango/sounds/sndPowerUp.mid create mode 100644 assets/pango/sounds/sndSlide.mid create mode 100644 assets/pango/sounds/sndSpawn.mid create mode 100644 assets/pango/sounds/sndSquish.mid create mode 100644 assets/pango/sounds/sndWarp.mid create mode 100644 assets/pango/sounds/sndWaveBall.mid create mode 100644 assets/pango/sprites/blockfragment.png create mode 100644 assets/pango/sprites/break-000.png create mode 100644 assets/pango/sprites/break-001.png create mode 100644 assets/pango/sprites/break-002.png create mode 100644 assets/pango/sprites/break-003.png create mode 100644 assets/pango/sprites/kr-000.png create mode 100644 assets/pango/sprites/kr-001.png create mode 100644 assets/pango/sprites/kr-002.png create mode 100644 assets/pango/sprites/kr-003.png create mode 100644 assets/pango/sprites/kr-004.png create mode 100644 assets/pango/sprites/kr-005.png create mode 100644 assets/pango/sprites/kr-006.png create mode 100644 assets/pango/sprites/kr-007.png create mode 100644 assets/pango/sprites/kr-008.png create mode 100644 assets/pango/sprites/kr-009.png create mode 100644 assets/pango/sprites/kr-010.png create mode 100644 assets/pango/sprites/kr-011.png create mode 100644 assets/pango/sprites/kr-012.png create mode 100644 assets/pango/sprites/kr-013.png create mode 100644 assets/pango/sprites/kr-014.png create mode 100644 assets/pango/sprites/kr-015.png create mode 100644 assets/pango/sprites/pa-000.png create mode 100644 assets/pango/sprites/pa-001.png create mode 100644 assets/pango/sprites/pa-002.png create mode 100644 assets/pango/sprites/pa-003.png create mode 100644 assets/pango/sprites/pa-004.png create mode 100644 assets/pango/sprites/pa-005.png create mode 100644 assets/pango/sprites/pa-006.png create mode 100644 assets/pango/sprites/pa-007.png create mode 100644 assets/pango/sprites/pa-008.png create mode 100644 assets/pango/sprites/pa-009.png create mode 100644 assets/pango/sprites/pa-010.png create mode 100644 assets/pango/sprites/pa-011.png create mode 100644 assets/pango/sprites/pa-012.png create mode 100644 assets/pango/sprites/pa-013.png create mode 100644 assets/pango/sprites/pa-014.png create mode 100644 assets/pango/sprites/pa-015.png create mode 100644 assets/pango/sprites/pa-100.png create mode 100644 assets/pango/sprites/pa-101.png create mode 100644 assets/pango/sprites/pa-102.png create mode 100644 assets/pango/sprites/pa-103.png create mode 100644 assets/pango/sprites/pa-104.png create mode 100644 assets/pango/sprites/pa-105.png create mode 100644 assets/pango/sprites/pa-106.png create mode 100644 assets/pango/sprites/pa-107.png create mode 100644 assets/pango/sprites/pa-108.png create mode 100644 assets/pango/sprites/pa-109.png create mode 100644 assets/pango/sprites/pa-110.png create mode 100644 assets/pango/sprites/pa-111.png create mode 100644 assets/pango/sprites/pa-112.png create mode 100644 assets/pango/sprites/pa-113.png create mode 100644 assets/pango/sprites/pa-114.png create mode 100644 assets/pango/sprites/pa-en-000.png create mode 100644 assets/pango/sprites/pa-en-001.png create mode 100644 assets/pango/sprites/pa-en-002.png create mode 100644 assets/pango/sprites/pa-en-003.png create mode 100644 assets/pango/sprites/pa-en-004.png create mode 100644 assets/pango/sprites/pa-en-005.png create mode 100644 assets/pango/sprites/pa-en-006.png create mode 100644 assets/pango/sprites/pa-en-007.png create mode 100644 assets/pango/sprites/pa-en-008.png create mode 100644 assets/pango/sprites/po-000.png create mode 100644 assets/pango/sprites/po-001.png create mode 100644 assets/pango/sprites/po-002.png create mode 100644 assets/pango/sprites/po-003.png create mode 100644 assets/pango/sprites/po-004.png create mode 100644 assets/pango/sprites/po-005.png create mode 100644 assets/pango/sprites/po-006.png create mode 100644 assets/pango/sprites/po-007.png create mode 100644 assets/pango/sprites/po-008.png create mode 100644 assets/pango/sprites/po-009.png create mode 100644 assets/pango/sprites/po-010.png create mode 100644 assets/pango/sprites/po-011.png create mode 100644 assets/pango/sprites/po-012.png create mode 100644 assets/pango/sprites/po-013.png create mode 100644 assets/pango/sprites/po-014.png create mode 100644 assets/pango/sprites/px-000.png create mode 100644 assets/pango/sprites/px-001.png create mode 100644 assets/pango/sprites/px-002.png create mode 100644 assets/pango/sprites/px-003.png create mode 100644 assets/pango/sprites/px-004.png create mode 100644 assets/pango/sprites/px-005.png create mode 100644 assets/pango/sprites/px-006.png create mode 100644 assets/pango/sprites/px-007.png create mode 100644 assets/pango/sprites/px-008.png create mode 100644 assets/pango/sprites/px-009.png create mode 100644 assets/pango/sprites/px-010.png create mode 100644 assets/pango/sprites/px-011.png create mode 100644 assets/pango/sprites/px-012.png create mode 100644 assets/pango/sprites/px-013.png create mode 100644 assets/pango/sprites/px-014.png create mode 100644 assets/pango/tiles/.DS_Store create mode 100644 assets/pango/tiles/pa-tile-000.png create mode 100644 assets/pango/tiles/pa-tile-001.png create mode 100644 assets/pango/tiles/pa-tile-002.png create mode 100644 assets/pango/tiles/pa-tile-003.png create mode 100644 assets/pango/tiles/pa-tile-004.png create mode 100644 assets/pango/tiles/pa-tile-005.png create mode 100644 assets/pango/tiles/pa-tile-006.png create mode 100644 assets/pango/tiles/pa-tile-007.png create mode 100644 assets/pango/tiles/pa-tile-008.png create mode 100644 assets/pango/tiles/pa-tile-009.png create mode 100644 assets/pango/tiles/pa-tile-010.png create mode 100644 assets/pango/tiles/pa-tile-011.png create mode 100644 assets/pango/tiles/pa-tile-012.png create mode 100644 assets/pango/tiles/pa-tile-013.png create mode 100644 assets/pango/tiles/pa-tile-014.png create mode 100644 assets/pango/tiles/pa-tile-015.png create mode 100644 docs/MIDI.md create mode 100644 emulator/resources/SwadgeEmulator.desktop create mode 100755 emulator/resources/install.sh create mode 100644 main/display/wsgPalette.c create mode 100644 main/display/wsgPalette.h create mode 100644 main/modes/games/pango/paEntity.c create mode 100644 main/modes/games/pango/paEntity.h create mode 100644 main/modes/games/pango/paEntityManager.c create mode 100644 main/modes/games/pango/paEntityManager.h create mode 100644 main/modes/games/pango/paGameData.c create mode 100644 main/modes/games/pango/paGameData.h create mode 100644 main/modes/games/pango/paLeveldef.h create mode 100644 main/modes/games/pango/paSoundManager.c create mode 100644 main/modes/games/pango/paSoundManager.h create mode 100644 main/modes/games/pango/paSprite.h create mode 100644 main/modes/games/pango/paTables.h create mode 100644 main/modes/games/pango/paTilemap.c create mode 100644 main/modes/games/pango/paTilemap.h create mode 100644 main/modes/games/pango/paWsgManager.c create mode 100644 main/modes/games/pango/paWsgManager.h create mode 100644 main/modes/games/pango/pango.c create mode 100644 main/modes/games/pango/pango.h create mode 100644 main/modes/games/pango/pango_typedef.h create mode 100644 tools/pango_editor/mockup.bin create mode 100644 tools/pango_editor/mockup.tmx create mode 100644 tools/pango_editor/pa-tileset.png create mode 100644 tools/pango_editor/pa-tileset.tsx create mode 100644 tools/pango_editor/pango-editor.js create mode 100644 tools/pango_editor/pango-tiles.tsx create mode 100644 tools/pango_editor/pango.tiled-project create mode 100644 tools/pango_editor/pango.tiled-session create mode 100644 tools/pango_editor/preset.tmx create mode 100644 tools/sandbox_test/usbhid_test/test/winbuild.bat diff --git a/.github/workflows/build-firmware-and-emulator.yml b/.github/workflows/build-firmware-and-emulator.yml index 850739ef8..4f42db1a7 100644 --- a/.github/workflows/build-firmware-and-emulator.yml +++ b/.github/workflows/build-firmware-and-emulator.yml @@ -55,6 +55,9 @@ jobs: emu_artifacts: - swadge_emulator - version.txt + - install.sh + - icon.png + - SwadgeEmulator.desktop idf_install: ~/.espressif runs-on: ${{ matrix.runner }} @@ -169,6 +172,13 @@ jobs: run: | make SwadgeEmulator.app + - name: Create Linux FreeDesktop files + if: matrix.emulator && matrix.family == 'linux' + run: | + cp emulator/resources/install.sh . + cp emulator/resources/icon.png . + cp emulator/resources/SwadgeEmulator.desktop . + - name: Create Emulator zip if: matrix.emulator && matrix.family != 'windows' run: | diff --git a/Doxyfile b/Doxyfile index b403154b7..a564bf95b 100644 --- a/Doxyfile +++ b/Doxyfile @@ -956,6 +956,7 @@ INPUT = ./main \ ./docs/CODE_OF_CONDUCT.md \ ./docs/PORTING.md \ ./docs/EMULATOR.md \ + ./docs/MIDI.md \ ./docs/SERIAL_DEBUG.md # This tag can be used to specify the character encoding of the source files diff --git a/assets/fonts/pango-fw.font.png b/assets/fonts/pango-fw.font.png new file mode 100644 index 0000000000000000000000000000000000000000..c3de0ffb5f9a292a32c86b572d735cc8660648b2 GIT binary patch literal 1294 zcmV+p1@ZccP)Px#2~bQ_MF0Q*00030|Nn#C&(Z(@00VSVPE!E?|3pyMApigbq)9|URA_{P*#%eYAsq>rbvJvr0LI)6%qOS^>F=M7JB8sLEwXT_FF3g;;mht zMi`3-cZ{d-UQE9PS$)Oe00IF?Lk@3^KfyRr}gwlLzR@#oXe8-p`63GtFfdf}_vXCJ^a%dirsM|X*-4TE! zybvuTEQ@-p1X9~9j6i{mh_iW;xkHeMEGsXkPGmk31@eMgBy8Zt-%^F&%- ziNc!x;8%?(6!jR_Kn(4qHz?&)4E`ib_iH<{jU*I5_8)Z(3yKIj*^Nj^5QeA;3mb{| zXQB}L_Jwu*s0w4YQ3(}5$4U@Hlbra@c|F7NP8DV?ZaQoA4qeQ{;-Rz;7oe!_G{6Ss z2sN76YGZ#Sx)IJ9q!fh0X!abAbqi%WwTxkHq))PO>WwcFB^ir>_s70BMSAXXqXHJVlcj_;OmkW zt3MWEtAr9ml&3;BB-3%^HIKnk>tD>?K~cbg^g=eWOArl^zleJ$WUVBS?JV(Eq9CK$X#!BMewAnxZ#45IFraI4@y@F?!6*tmg;egYCvOZk zU0qHoeXH5rADrZrC6!#Zxt^#zor%Kjyfsq@CCpxzhPR?% zG~+}Re6+%bJR4^yOe_k@Na;@sB`?=1|7fghMb24-Hla^Fcaite{iD&%V5Xg!En!lz zkVx|ix#V#&_Zxo|24Hl?a55=RHWaNor9+>_Mih!c#PH5YF?KT)_3OjPebcmnW(s1q zk;7|}M@YuO`cxE5*>3V1kP^OsA_^7wTcUsiiJ}lG#zSlk&E4p+C^*De&76RmYCqIl zI8Eo{{nQ=}$M-0d4g!M=-t)DpS2yF?5O&DwNYh~XZ{xPEh}?Ch%<+CMxWg+2Ngrja zOgH3rnNdBOUM~+qY8)&-g^iHf$c+c4Ij0^7nQpoLFCYTH7looh04ur>JMH^U6iDIA zSA@%=sucON6Lhem0kr0RiWF=?xezlPlBu{o2kh!-GhL(6H+NuUFor2Ci=O$PBTr#i zXZ0^8@teLPoHI|8Jw)~nMZ?Ajyy%l!PML8z1_LSp9is!JlO7h{>TGld@8x$CTWw#H z--37acqLI31_BD6gRc+Ide*c4()s}{dA85?**@F&2PCRd0S9uv;Q9^q8 z^-b(IuA}XiZCu)3ef-9mIdf*lqrvQbEFuT;OezuuvrivTHCevd-`{hpvaHM6_DYtM z%JaA1wx;SwSyrln-}7IOzkHU(%Z@Bw#PVI>JCOx>7YDVS?CD`#6yqg_9Ve~FUQ&l= z@C=^76L<`d;VC?YNARfgm3)7WYw&pk|8sc0iGMooU!CX2PU3eY9l!w`!XXS`07q~H zLm0vdoWMJH2j_8=%wf5{IO#oc_ry(OH<@n-_iVP0?RblE2%&Lv*bHf+PJ4oM%;k3Z=lTIXke`Mi0( z>p7O!&hnne!vnN^7^BDNDV##PhxMsXTpzpDGeT=0vpv#! zbiVGUU*vtn!?;`4~t@=8HvsK-hzD8f8fA4oW|2}zi z{!^&?VfqSvh0f2xKHt^9M;_hF5b8T&x`o#7pY>-u&vO1<^5~q$Q1{$)6Rq=pN*C`( zxjvSiQ`tV2_4m2Iz9)02zbB?|&^Ku7`*ttO`g`Pe_?`3ESBLjwwXX&L5B3V|`k&(m DP=c9* literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/bgmDeMAGio.mid b/assets/pango/sounds/bgmDeMAGio.mid new file mode 100644 index 0000000000000000000000000000000000000000..e50939584ba30457e31616f74a444c1927eb3ce7 GIT binary patch literal 2217 zcmd6pyGjFL5QWDJBIpDBosEb_ml&1AOVk*zfus;DEG;YqZS)ED5%y7h!}x{ui*Z3v zNMUuD%Rh7G%xuWu?&&ci7Gfr5<6&_3_KdX>!?}pz{rvpw>f#sP&Mm#Z$FLq{+Tu$T z*&yXvMA7S}ya)E1Der@gPRbkLWh>>Ek%~?`r!z!T7SC4R;G#QXH^fg@gf;2yXRu7lU$HSr7Z0{a|12jw;Gx#YJ?pWTW6 z@{*@{>Lnj}JC}JG^)lwj+RNZFSnfle)J;9r+q%}#U$xCME-&+}TeZzA`={}~F~+xg_}p`unHfyr2B)D?jtTe^*g>U;53r&$Rkme~WpyCgyEpx3M?DO;Dcd>zwXI ze#ZaX{=I+qukH7s-?`kobLp?@Zyo*SRsGhP_WrAV{@UOD%TGP7*!vZGxBd^L&!X88)OcFl6a`fV2Y=?F{5Q0~NyLp?sjz0QChpPXGV_ literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/bgmIntro.mid b/assets/pango/sounds/bgmIntro.mid new file mode 100644 index 0000000000000000000000000000000000000000..8c79b83c185cbd987ff1919da6577964ab08332a GIT binary patch literal 233 zcmYk%u?m7v6vpu{m1h1X747Ius4jBq0$N4bcOfN4T%h-aFa{1~vUS zoImGs>;0t#FvbW5ntK1Za+;wxf&O4w#$x(&YffHs^Z~xv8@gC4DF7WFZ3*G%n8q}v zDa~3X8TZ9)=*qK_lJl~$W%=K49QL-(c0*qthpy_BRP6J+lALDGVW09|$~kE~Noag< W`7U&HawVcc=Se{Qd?h}=65s>ji8yiq literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/bgmNameEntry.mid b/assets/pango/sounds/bgmNameEntry.mid new file mode 100644 index 0000000000000000000000000000000000000000..817d7e476ca1e42a74c61997530c3c6c0ba4ef40 GIT binary patch literal 648 zcmb`FJ4ypl7=_PF!X!i&U}qyCxxzw@8@&%H3)-9sTMQKVRR+uhS6bg8l?mACbJeAWEbn_BDbOJ%A!_S`+k{Tp%{a>xB<-_w2n?Vq!E&R&1#JstcGzI&JO y61MN$U;cpnpmJa57WfN%&&Ti>w(s}2=T!aP->0unpWmCo8QepA=+8a*rM>|$X_is| literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/bgmSmooth.mid b/assets/pango/sounds/bgmSmooth.mid new file mode 100644 index 0000000000000000000000000000000000000000..a56157d7ea53b1ab8e1a3bc4e97f56db11bbaa2d GIT binary patch literal 2889 zcmd^>zit{q5Qo>sCKg66Qe73LNbQ_$A!Cf?!(hY-pD_p<=u&K?Na>=uzenIvDDx;O z^Csk5&OdNgL9!xqO{z~b`)6i$W@k4UezC@wqS-P9^C=mA`-)UH^YPBkcCcsWAI+0) z-^}}k-5)Z_)*7?I{v)Aoh&kzy!ab;?t4Whw5h&`q6}X7~5hxh9l_u+&#R9L+Jhueb|RR=>99W@D|>{8`y>JzY;@NmmTQp b?;2jiCUkXoWp~r7Xlu>?b$gB8{fU1Aw|G*7 literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/bgmUnderground.mid b/assets/pango/sounds/bgmUnderground.mid new file mode 100644 index 0000000000000000000000000000000000000000..92aa5f2734fa7e2943d0f614f7b6a9c96983e756 GIT binary patch literal 4569 zcmc(j!A{#i5Qdjx0is-5<&a~~T#7(Zq69&ROB8~lC{YAOh^Plws_3n1?>s^tpbwEp zs`?`7zji;_UfYTtsCrucyPkh%X8+k8c{KYH7-M$Kmf1F+Mze2Ug$~T>qvyS<@0rzm z^LOsGSxvTg_kYJzZ+@E9_TK2~!_~X-kMCx=-!;qK!2B{AKO3_Y?{ep$)coQ!D6%T(&V$3JbB(@6KY<6rgPeK3JJb4^)-N<(XuQ;TnZQHgVa_~uO|3Vz-qzUG$UK;R z;CI(gWOtHdUlb3#te?vMCF1Xir^q70!NdQCdYkv`s5%EL%@BiWtH&guppU)btrqV@H+y_2lxfPgCnvbOSH` zz|$6w)!nuBz1F@b!d3Ky9Xv&O`Pcb%bp>|yM;>~L)9RJ{UJBc~B`@R`ow>S0SMEA| z9r0N`uj50P@PWxIevnu4JPy22xAz+Fb#D3C+d$V|sOvlN-iddl(ds;ny@c+|`4QKJ z7uLmtcpt<&(P--=jlB)(#tZ9aF21?=rW$P>rLmW&tGs=S{m9R+S)}vtEjTak7jy&W zoVm~Q{ml9u*KN6cqmygp!PfPy##@c~*xR7)%kknzzK))+tLpbko}QoY$ENz`p5i?O zXYZ{%J!f?i-)rUc9p8K9bcMd%dk>7h!RSA~&a?Fv-tg&jQPiF>=D_n@Koprt;irIEf1zrd{XZeRybOFY(osC|dp_gMH? zxFOyoKY53bb^ODwFP^@5*vA+4g^$F0BwqSU;>Axe>+oP#lo#@ZJ@?|v=Qrx-xF524 zj`B!8*X5hO0p0MudoJ{CeciV^VpE7QfTAuy4ZN_xFy}5bD5BdvV`)yKxE-!UhHqYe$|D9j8daZh%)ZtY5wS6J=;_?*fA*#>) z#baO=ww52e)OUpE1(!jgcPHdZ{P+wWz+*er4C!ogeQY_pEcy1^-jF3g+!U Fnm&!X88)N>`6)~c8`ywiU^XL!tp*ea^MN!Q55ofnpgbc3!+`*X c2^sY^4H*m!6T*OW7?2LGuV@Gc8m-R&0L+jk7LMf_?y7ZS^F3BXZ1KQO%F$y%yJ>d6 id8hr9Wmqzz-oXc6ZQMLt*~muLvX+&sWGPF%>i7Z?Bq&t? literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndCheckpoint.mid b/assets/pango/sounds/sndCheckpoint.mid new file mode 100644 index 0000000000000000000000000000000000000000..866cec1414b61c6e58029acee80cbffcc8ed23b4 GIT binary patch literal 144 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25Q~dzLe|-k1bTq?; zN}x1Q$p$V5h7If~3>&!X88)N>`6)~c8`ywiU^XL!tp*ea^MN!Q55ofnpgbc3!+`*X X2_f}14IvB+69Vfi8Ulev>N5ZUsT?F> literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndCoin.mid b/assets/pango/sounds/sndCoin.mid new file mode 100644 index 0000000000000000000000000000000000000000..10fb39a7def6dd3eb13f092fb308d77fd6bf59ee GIT binary patch literal 144 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0I+|fa zB~TiuWCNE2!v^*gh7H{H3>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} Vgy4FchF~BaR$tK&1~gKi0RVz2Bgp^& literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndDie.mid b/assets/pango/sounds/sndDie.mid new file mode 100644 index 0000000000000000000000000000000000000000..dba1e7513b0a133499e46e602898612844455009 GIT binary patch literal 184 zcmXZVK?;IU6oujcsl1|^7cHQvD^Np3aNr#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} U1dn=~1`i-@1Eg($M(Q&F0DVa#LjV8( literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndHurt.mid b/assets/pango/sounds/sndHurt.mid new file mode 100644 index 0000000000000000000000000000000000000000..530006503534c6470a06c712372daed648cc408c GIT binary patch literal 176 zcmXZVu?@m75QX92PGk_!gBXA|3pk0(G%1i$Fo2Pe=;#ndG6JJ`1V+gw<|$Bp_wJo^ z{cwwb!(h$a50AUjo?Jk_l%+ka$>+iyCvwHVevzK4b&@uLw2GwdNV+OVOLE#Rv-!;@ i?UyXwjv4g^KJX5spMq>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} U1dn=~1`i#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} U1fP1F1|J~p52XEpM(Q&F0Dk!+aR2}S literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndJump3.mid b/assets/pango/sounds/sndJump3.mid new file mode 100644 index 0000000000000000000000000000000000000000..941b35664039fc6833f822a29505818fc92a1010 GIT binary patch literal 144 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0I+|fa zB~TiuWCNE2!v^*gh7H{H3>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} U1pj)Q27e$O0;EHLM(Q&F0Dq|?fdBvi literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndLevelClearA.mid b/assets/pango/sounds/sndLevelClearA.mid new file mode 100644 index 0000000000000000000000000000000000000000..5faca34edb0907e3ae0dd2c07c2530339e1ca25e GIT binary patch literal 209 zcmYk0F$%&!6hvo}u!`C%9>6viNU@M2AtXd0g$Gz6U?Wyq?RtY8z(f2ac$C~^9ioEO zn>X`|-*mkNprH_{vFWa7+8QAS!ZAsr%^cwn>#avf@tvQLJ7=EA!a!ygvUHFW-9`}2 zZp!MDckCx8^$H`nE8w-qkhz=BfWeT4%*p)EOL|GK$(sCmK`-bPS&^-KTzt!)QoI3? Cr#Wi? literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndLevelClearB.mid b/assets/pango/sounds/sndLevelClearB.mid new file mode 100644 index 0000000000000000000000000000000000000000..f56147fbee0fb9f731177be247d2aa37393b01c5 GIT binary patch literal 185 zcmYk0F$%&!6hz-9VHLGkJb+XgBcM(dX^0%b_pCLM+Ad&z;=KE3pz?`?!1x{qhEU(J_7i literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndLevelClearC.mid b/assets/pango/sounds/sndLevelClearC.mid new file mode 100644 index 0000000000000000000000000000000000000000..887708f9508c0a1875c37c1625a42e50d2a1212b GIT binary patch literal 161 zcmXYqF$%&!6hz-{!YXR7cmUg6VIfV51Qb$ufCz#Xl1i(MH^@=;2p%OjS%+x#X6AkP zO?&MChrybuY5SYnmXtv{=egZ2NvF*12U5Xb--$12oy3(VE;`~m5HISTB|2?$Hott( gK6lY=n2pSr!t&_N_hzn0#2jWHDTcXo8XY#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} Ugphihh7cg_0i->EM(Q&F0Dp-ia{vGU literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndMenuDeny.mid b/assets/pango/sounds/sndMenuDeny.mid new file mode 100644 index 0000000000000000000000000000000000000000..586cc825a2bec9fa6d36925590d86e20ad035aee GIT binary patch literal 144 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0I+|fa zB~TiuWCNE2!v^*gh7H{H3>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} Q1cQ2;1_K5gC=Id#0DM^^Hvj+t literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndMenuSelect.mid b/assets/pango/sounds/sndMenuSelect.mid new file mode 100644 index 0000000000000000000000000000000000000000..2be02c48a2acb3c0765500cf01d54165f3a05df3 GIT binary patch literal 144 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0I+|fa zB~TiuWCNE2!v^*gh7H{H3>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} U1dn=~1`i-@1Eg($M(Q&F0DVa#LjV8( literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndOutOfTime.mid b/assets/pango/sounds/sndOutOfTime.mid new file mode 100644 index 0000000000000000000000000000000000000000..31b1d5b93b6ea1c9abc96f02997a5ba744021b6b GIT binary patch literal 248 zcmZ{fK?=e!6a^<~h@$TG1a7^87bqwMDqTkq+_|c&_6P+pk|PLSBsYn_HFc${_urc# zkkVZ|U{PolOLx0UTayY%Cu8(xPC6Fm5lK5Tei811MG1>NVde?TmT;EtHNna@-s*F2 sWgokwT@ewk;CX0z*EXGlzLKmYYss2nXJVOuo$O66+5h*>uYLH+8_8ZtM*si- literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndPause.mid b/assets/pango/sounds/sndPause.mid new file mode 100644 index 0000000000000000000000000000000000000000..5712c1e37813a032f44e20f46fbd03a3e4321404 GIT binary patch literal 160 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0I-6lb zB~TiuWCNE2!v^*gh7H{H3>#8`{1hgJ4QxO$Fq;v=Rs)KI`9PYDhv5MOP@a*2;Q#}} Xgy?#khG+(x2@ya#0!X9sK~@3){LLw0 literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndPowerUp.mid b/assets/pango/sounds/sndPowerUp.mid new file mode 100644 index 0000000000000000000000000000000000000000..4763e03dfbdfd5742aaee903d9bc662259106843 GIT binary patch literal 184 zcmYk0F$%&!6hz-9VHLGkJb{kKj>qlXZxKR&VCb zFMivdBj9mlZfv{jMQu&WAsve%+ssLa+;0ccp6~p??wWOCi-64{w(PN!ddx5{H)Z$9 jJNc=Le#MA(2QPS*{cR}4O02{n2C)|FKi{Y~Kd)EbH(fBc literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndSlide.mid b/assets/pango/sounds/sndSlide.mid new file mode 100644 index 0000000000000000000000000000000000000000..ed1d66270f894043d6259285832069ae128e8eb2 GIT binary patch literal 176 zcmXZVF$%&!5QX7?lduuB*GjBxa|KhT2tf!zNReg*!Ol)B^ax(T8_W?rN^Y_~qSZHV z-oVuDIRG9<;>NnYUX)hE6yh<g~M;B4R2w8y*JOZO+6LSg_RPWy1 zmww|e0Vpt7Gd1pZm2F5Fq*I>T?Sgd7%GZum^PBI)L90iQDXsKX~6~{Ksb1JO-#Ib^8_VO$}hoE=wzKBr#Cb2=U4qR z0s)72bJf3JYDZEA>6YjIRFK*%oJUg0s_)oGS|_%>Vw;HV2JE38Jr=Yr+5PiH`_jd* XXGY$^7p8hRjJ4Q^jo68uUYGm;p?4)` literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndWarp.mid b/assets/pango/sounds/sndWarp.mid new file mode 100644 index 0000000000000000000000000000000000000000..f4a5e6e66729ee34419152ee493896b20f39d680 GIT binary patch literal 200 zcmeYb$w*;fU|?flWME=^;2Tnu4djV4{AXrh_#eT-!X&}L@IR25O{#(6zdi$0dNRX? zN}x1Q$p$V5h7If~3>&!X88)N>`6)~c8`ywiU^XL!tp*ea^MN!Q55ofnpgbc3!vO|{ Z310O!4PHRn8Av+=X$K(fK)N{4X#gwGGtK}2 literal 0 HcmV?d00001 diff --git a/assets/pango/sounds/sndWaveBall.mid b/assets/pango/sounds/sndWaveBall.mid new file mode 100644 index 0000000000000000000000000000000000000000..f04eb8b265a82dd8ece8d19517d5be819535fe46 GIT binary patch literal 160 zcmXYqF$%&!6hz-9VIyj(NNAM`Q$vQ-jKD+wtc-MlR@|duQg%`PiSkldXlp%YmeY(e`l4_vypm{A2##9-aQ>GxvGA zwV|`$%h%GQ!`<=0CXXh$6jDmncQd&uOlhhvo6B9{N>_Eag*+6Y3{?+X%2ScbR5g~B zycDG@RYRLVVw7Hr+#Zgbg;|$d>7}K;<=YbRM>0Ygn#oQx2!>a|ugL0&2dbNIN(H-SkUTp_%VGn*_ z0E&@_CPcs*n(%@Mn}G%!Fs5mz&I2bSs;xvJi@9e;wX+C@2j#4&Hkw$-0poERYw;5E za0`3z0|QWuL`Z`LYiPm?B5Vd4Y`~bNojMPk>ddebg)HV?9U=J>I0+Le84?;P5eqqB zJWgXRUSbC<8O9F`Krs>_4Hm4S2``AS8ECKpW14pAJaG8kz1shH#pmM7bh1yw|9L*# zeLF$q+GzOpU_L(0X2*K+?9I*k^Rf1Rz24EEmBIMkDBt7MR$~E zd9@w5g+2Iz0VqZynh*hNXu=C3Yz7)^z?i0;IuD$TsJ0S?EasjW)y^Uq9+b19+Gt`S z2aLyQti?;r!!7K=4-7yt5+My1tf2`nh_D%GumNM5cIrHEsx!k%6tb9mb%f+k;3Q0> zWJqYFL@eZh@i>jOc!?dbWEej%0L4gzG+3~PCcGfRW}v|ajA`1b^T6SEcXE3DC!cS3 zC*vs%|L3{(=fN99o*xdbtS`Oz_;hEV&MwUSc(ill?d@Bazutei_rk)v6Ql3%H}B0I rJhyf1cpm+9Q?t)r|9yV(@TJYqXO>=#?)-ka@c|GGSH^?w<&CX>SWrft literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/break-001.png b/assets/pango/sprites/break-001.png new file mode 100644 index 0000000000000000000000000000000000000000..421f27d67cf23a48b5106d522a7fbc04a9f7545a GIT binary patch literal 1028 zcmds#J!_Ov5QUEl8Wut#2#TbzG8PKrG=d;x-H@#4vh1$n2S`AeKcE3C3qe6GtRhUK zg``cLRu!Wt4t!S&kN*JHqpW%h%<=&Y&b3XRvaIiMtIo**o-(T;I_%0s&%^l(Y zxeE(pK8GHS*1Fo6E&Y@ZzaHM0tnz4*OChCH?QSMFg(*$d%I0!cxYAXv+d>|SP==}v zTgp?B%2YKhD|snOS*iwY0*O(2DRMhFZWd-~R&6nNcMG?4tHy(}hecS1Rij0>r$t(( zRb$Dpmql5YRU?fESVI$D5MeW`zy^$I+Ntxv3HNlbrjW(lBRs>a35EydNYC_Y`k_U4 zlxKN0J8%no@B;%-j6^gc0@l!k7ev?$G}wSKO*?fSI2lpRN))n~duCK~7QyhKoE6oK zCKhtQc$~&syu>`*!XEs<02Ct;(qO?Fn(%@Mn}G%!Fs5mz&I6}fW>|?r7IUu_A^8(H z2@@$95*jHH3prpsPGc=zVh1c4#t#fYF%lsS7ObHOFNm-iXs`iens(|uaQNMw9pC%G z=j-FqU`)gRd2auC@&S?Ees5)RYU}x%m5wgX?ft5S?^mvUdNJF3b!=;Le&hHwZ|&cG zdvpKG$!`xgmp<=)JahTZ^54a))2GAxCoa94?QTApE)Rac-#&UBH1$^py`ApHvwzI$ BPJRFY literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/break-002.png b/assets/pango/sprites/break-002.png new file mode 100644 index 0000000000000000000000000000000000000000..dab3fc1d0523afabbe56f36ec58fcbeff17e787e GIT binary patch literal 1034 zcmds#PfOHc5XWb;z$7W6Ai{HW5yW`~$%-z-58YiN>LO-ysDpy5U+cJHem(c_I<9TN0JKsP1YN=V6oVY$AB9ooPc9-v^)4%aC z{@av}mg=dInnfBBu!bhQAi`!=fejebv{UDS<7Q!&rI5wk-7Va*1jB=}hecSH zerVC{X_1y?2X0{xeqaEKk%%Tlz#5wHf(V;|1{*M@X{XKuC)~5FL?Mg0M|hUA2!;pc zNY65wSjYk6aT;s!67z5id+-ATP>e)Kg9U47!V4m71{!R@n5LaN51g#buo8tV=AIQH z`4cz^6Db)I8YvMAIbb|aV=Z1{2P_%J4-7yt5+My1tf2`nh_D%GumNM5cIrHE_}$&; z-TT95__W*fX!t+Ros;LE5xLlD-|Am`|8Vp0yv&S`t>?MjX8A??(dgjug|qLj{@m}! zaQ5=h=^d5sYEOiHY5RA!fvpz62U1~P%&w>6GbbHgH=!q z3-b#EwXhq6Ei42@gCGVAEyOb8llwEgki6VGGiT1n-dP?jbUTMTlDbQa{UP6jyMO!k z@c-oL10z0D*M|#p+I)XxTbjPRe12t~N0VF%DWz(6Gr1{DX{uH>m%GB1u4>&D@=$~_ zRBhN&o{Chas$p5lOHs;FHE0t^jM7Vy+wO6*FiW#)i@Cd7xTRY)9+W*S!ZNHHExJ7| z(lV_YONPBH%Cf8)X+*#pn(%@Mn^^@mU`*3aod-_1r+YPpEao2J8D32=JSazcrdQJs zExMyT%d6ReTiAmi7=U6Vq6rbOh9=kORi!G}huJ=HV9h;0Fev7>SSu3)awt7ev?$G}wSKO*?fSIMp)4N))n~d$kD3 zpTJ3&NXd}UNQqd;0poERYw;30V979kU;v7d2x+ij4NZ7Kgv~&M4H(n3Q|E!h@9xCt z;txI_Zwv<`8vf67;qTg0L@qA%XIFX;uRNIQ=-B@0Uv>1)*~xI`RB!wHJAJtI^33Y` zi&yu*OvayPHwO2%c78wE+IaKr__ep)Lsy^m&OKk-S$#BlJ^s0OrZfJ0^YPcqU}|Z8 K(BGW9bo(D1l~khu literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-000.png b/assets/pango/sprites/kr-000.png new file mode 100644 index 0000000000000000000000000000000000000000..1cfc09624873b009cc18d9e36529fda92150efa9 GIT binary patch literal 1079 zcmds#%}dl_5XWb&Ko<-{4-&l?QFNa=L|~{Zwh*$c5@isb1|#Z_C7n8~r>MX~K@)W< zItZ;0bn6ggMleX8B6=e!D7sY?beTRqe?|}JkLQ`0?|fhOa$|g~ySlk5BHi^vwI<&l z{`x!D@qg*kwLw0WndaEAv{$x#73sLrI6OJZql%b_i-bs)yQ-LqtAt9HvYMEQn}kW0 zZgnvicL|p)!x~~C9ugs0ur$R|JS9@HK&ybnD7_T9{X3@Ws-c=?F?Ta{(=g4#gR;50 zYq(~iMYn}|XoO~A$*`q*YNTc%jR;sn6J8KuGpoP`jA`1b^T2VlFw3Hl#oXO3+_DIU z2W1b7uq^taMYpF#S{6HS3w!Vb15k`aG$8`k(1aI6*bFq-fH6%wbsjk3p2bQOvY2~> zXK@z6@Sq&&S&SwYa=>_;##+3@Jlw(_{J;PdBN5VI!5W(If(V;|1{*M@X{XKuCrf5n zi9!~0&k`Z|6F3PIDH#$PDG>`fU_4G^EnZ>=EE&cR3_vjwAq^I+p$RXDuo-Bu0b`nW z>O64x-R+*3`oib+`R4cp4gcpk^6TPVL|&-Zh9>)OoS9$U^kkrOneA@O?r0drB@u@K$Rm4PGBt){rs$wdx5-M5JYGNjC z5++%y)x})gC0w#JYlww-NQ7j;(iBValt{?}tpXCQ)RH9ncTCk)Lp94_Vl#EqFwMe) zw7I%#xMrb6wS{_Ugl1t$v!!}!q-G(F2v|cCUJzk2qre7?Zpz8?z;UxM%c79M#O@Yu zSp>s_w1-7l7X8qo+S4K}iygRyJ@|nEC`KZh5CLmw!V4ly1{!R@=%$=J51eq%VkHV0 zOdR1^oJBA^NJn}Wqge}kz<8X-TD-(O+`=CGzyK5@5z=768k+Ee2$O*Z8!);lC(i>X z%g(S8g$yRnvO@OH!r3q@Wrr+{EwL8%fblquwRnjgu%sD3FaX6!gfv*Nh9h9E^p95OcfUATYrmW6dOYx8xwn4*&+Nw8=DC;69hEIN zDuY)JE$;mKt#7fmZf5$|wYNjpZ@*p`JaFmM%$wTw$q$_qpNAKwzOQfm?%p+fcImQJ P`1gu5$6EEy=$ZMy(|}&n literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-002.png b/assets/pango/sprites/kr-002.png new file mode 100644 index 0000000000000000000000000000000000000000..1a7b6d967c5cc79062f46840ed85d9beef75ff66 GIT binary patch literal 1066 zcmds#y=&895XUc7u!IK1#iACR9I`mvCZcUfX;USXq{bJdbUKUTREL5n;-tiif)2O1 zh`1G<99+aYh#ZBggB1k9K?N5x6bIq_`1~0@(EfOyyZhevCAVhVQ?=^es)*ED)6EXw zUHpv>Z{z>#Yb!_jRF*nZ4Ov~^|3hS`KYLZW0ug$HGG zb=PpsLW^z-_0R~-!jfT2_0&ksLK+dUh9W?_~^A&a@YTexKr z3=hg47GYWRLyK-ti?l3u;1>4a2L_-RiD*Iutf2`nh_D%GumNM5cIrHE!aa+XC}c7B z2+!gyg5g0q(z6&%EaZUkIE}SpaG*TiKa=>_;##+3@4p=gb9~gjQBtjZ2SVI$D5MeXWU<1Z9 z?bLbT@Vh(MJ^P){=H*ViOT+(pdRxnn5V_cDPRxxySh=%NdpSPbICFDiGoIC-oPT%m z)BQtNs}=d!=pT8rTsgIU?Rsr+>}P#pUi!<=*B%~z y`>o@Do>qF(pX*y2W8F(LBVTIc@Ar+pxNv&U$ydGG^Itz4XQ#9#+s)O+!i~RxIADeV literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-003.png b/assets/pango/sprites/kr-003.png new file mode 100644 index 0000000000000000000000000000000000000000..7ffd299518bfb8da8462d1e4e88b1f5a4b0ad32e GIT binary patch literal 1061 zcmds#J8P6t5QZlSNLY<{2^hp8Hd+}2gEr!#%f`4OOWch@v=N0Nb_OkkK!_>4purRh z4i<_wBACL)!X`ox#qolLAXauJMFhcsHsj;@86HSJ&Y78a-pgJeX%6>PwpK)>r*XL6 z;=6&rzV0smFHX%?`E*=p4cBD;{kA2M&Kn~~M~8S+5fgEd5Xo{^6;pAQP{~qO6Eks> zFv-%bF6QDc;gV%oLoCEYA|wlzrdW!nL`oKD6_6OEmm;@+$5dT4RI@DRZl-P;rdfDU zHdl8I*DSQ?wong^&@3z&wp34z)GVYC0c&W&3nFZ071)3=O*?fSIBphZSroFEySs&3 z7QyhK>|qg>ML)FY_OwXLVh3(v4}M?(ijjyWM8F!F@PY`Nfd(5erfH|n11H?GScyUw zbC2*W&LS8dlp{Th(ZoUy7?0Cfi`P%83zqk%x AdjJ3c literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-004.png b/assets/pango/sprites/kr-004.png new file mode 100644 index 0000000000000000000000000000000000000000..06c303ea35be57970b890118b02243eb13c89709 GIT binary patch literal 1058 zcmds#&xh1;7{{M!$h0+-6=O@sB9?gWF=NK=Fvgf&$r5{d=vr;6Lux6ti4G-?s6*L^ zmgv}_Wk@_a?5R~+me?*?9Y%+EdiD7;`mp=;`8?0_e%?Rk?w~(YpO~5uk$QKwJ>+|k zzecUf|IzjPr} z;hKdO-4^Ph5t@Z1!-Gb9c9J%OV&a zlszoMvgn5v-JTX{S?s_q?7WJqYFL@eZh@i>jOc!?dbWEej%0L4gzG+3~PCcGfRW}v|ajA`1b z^T6SEcYJPPhtKAvVSkQ>|MQ#~UwMwm#csPf-*~k2VEyp>Q?=H)JI&2_bMn}M)#;bJ zlk1Ob`(8IHPj-4!=eI`9<+GC;^~)c|E4v%lp1wG=w0f&uJ@Kn^;r8dBqw$Tcy!rOq oyUN3>wO4z;KAZjW=g4S(?_+hLGMN7UdwZK*((UxyYpuoQzv%m5*#H0l literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-005.png b/assets/pango/sprites/kr-005.png new file mode 100644 index 0000000000000000000000000000000000000000..03cc9a66b28f02d41746a6993b48f6b528301294 GIT binary patch literal 1071 zcmds#%}dl_5XWcrvKUgAK(`Lkp-w?CV7EY7*`%~(6%W-$P|QK-(wlp!gAf=MLFHk> zTL*6&9ipI%frns+JV!4O5g24b2QLQGr{~Y;0sZkjGxMGA%ie8HHwFi410pgwd9vQ( zyN|!2YCr$iZ$CJ}r*f^;7?8CCjjeScr#2NER$ju@p~H zg5g2g!y+t;erVC{X_1!24&1^X{J;PdBN0uAfHgGX1ras_4K`p*(@vcSPPk{W5``@0 z9^qM>MKC-lM|u{ciG>_69;dMuFEJ0dum?Xd0L4gzG+3~PCcGfRW}v|ajA`1b^T5fH z8CIf@#oV(*Nd5#)!bD1jghoolLJk;@(^!j_*a1t1@dE=;j6_I-1#4)+3nFX=8f?Ir zrky$u9Da9)XU=Z&`Eb28JwwC)dFFOH&k%WKvOYFDw0z^?+sfLpYG3=^*ysB%YD=|S zPa5aCwWZ-neKZ|-!@ug15Z{j0y)mED_Xc33Qv6VvtX_{GJ) D83|{o literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-006.png b/assets/pango/sprites/kr-006.png new file mode 100644 index 0000000000000000000000000000000000000000..e90a0a0adb50511c706e86994e9dc019c98bf4ba GIT binary patch literal 1061 zcmds#y=&895XVm)ER6*_2th;;r4A}8hm&BnB*du1kXDC4i&D7J7h2K94jGhElq!fE zxD^+N48@^1l_&+v;ou^IvnYa_os{$A^Jn-#`{Q};?t9;tEVf&Xk>S0=A~MoEU+eJQ z&0lqB2mjwKJUYW?+gztHA^i{gzKd*MYG0bF^Qa;w;vylE<*q8G;wqt%rK~1q;wE8| zrCVLh#a+TB%dm!6h=)W-7A#G%6io*JoHNFxH)(1aI6*vu-h0b`nW>O64VEX=YfWHEPl3%4wS z;X&EMA}ou3XwmIyk(R{{+`=CGzyK5@5lx7IH8kM`5jF!2HegKCPMrr%xM#5vg)HVC z;aQwTFgz$ndKROJg&Z&*r?D0ejs#M9wyA=ccO9rkCH3%E_U(AD1gHb+h~Uz>&e_ z{U;vG-Dq~(zgE}n_`}}T+2^;vyqdXwv-};Gq*1I^m tcJ%(AgI6}zD~F$K%=Xs?zVr8kxI%(Ehwf@Al-d{jNUDN;o literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-007.png b/assets/pango/sprites/kr-007.png new file mode 100644 index 0000000000000000000000000000000000000000..0de9825c5cea305ffac7cfba72b4ccd6ef3b40f1 GIT binary patch literal 1080 zcmds#%S+W!5XUDYFfW27fmomrxex?thzd$lG!^1@@r4CO5DYD(7DgmgWDDs*50HdG z+SEEBsJ69m5jh0WqFNM4EsAgvQOl-J=g;VX{y1l5zVm(D>$U1&V?%3$h%^ol^^fpv z;;*HCBmY+}-|FGBVP<5oPnKWr_$E?!vvy=`fJYTE5f=%OEO%8g6;}zBEM+w@6E_Ky zEZyp2F76U8S%x*lLOdiwvS4Y7rFcrDWPw%ziBWnfa{G5o)m1|^%VO?k>ZW0ug$HGG zb=PpsLW^z-_0R~-!jfT2_0&ksLK+dUh9W?_~^A&a@YTexKr z3=hg47GYWRLyK-ti?l3u;1>4a2L_-RiD*Iutf2`nh_D%GumNM5cIrHE!aa+XC}c7B z2+!gyg5g0q(z6&%EaZUkIE}SpaG*TiKa=>_;##+3@4p=gb9~gjQBtjZ2SVI$D5MeXWU<1Z9 z?bLbT@Vo06J^q!?hjSyxQy_I`}Voh{8> zZL>dGYn7qHYp-tq`L%0yrtQ$v%2diTrFbD?8yda`|K z@3kl2YwKtC&!1lTd4FQ=^uqk9t}VOwJXn0!y!8C@`kl$u#Y*MM&VA3S?LC*apBO(g RzUeItWO$(3zub57>R(-eWM=>X literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-008.png b/assets/pango/sprites/kr-008.png new file mode 100644 index 0000000000000000000000000000000000000000..9513601ecfed84a107303b5e0ee3f29211b96f25 GIT binary patch literal 1085 zcmds#%S+Z#5QfLZ5J4!SC=qcHE~20sCS8n=HhT)m#n^P{wcC)Zt!Temq!&b5f=%OEO%8g6;}zBEM+w@6E_Ky zEZyp2F76U8S%x*lLOdiwvS4Y7rFcrDWPw%ziBWnfa{G5o)m1|^%VO?k>ZW0ug$HGG zb=PpsLW^z-_0R~-!jfT2_0&ksLK+dUh9W?_~^A&a@YTexKr z3=hg47GYWRLyK-ti?l3u;1>4a2L_-RiD*Iutf2`nh_D%GumNM5cIrHE!aa+XC}c7B z2+!gyg5g0q(z6&%EaZUkIE}SpaG*TiKa=>_;##+3@4p=gb9~gjQBtjZ2SVI$D5MeXWU<1Z9 z?bLbT@Vjdp8QtLXd2)DQgogj~9RGcF1(8#gp3dsdr3(vlTVJ)-cb~k~`Fz*J((VT@ zPP~8e{nvp*-)0`I^)0rZK2o#y!`+7Zwz}oDPeU{PXMP+zxNml3~W6hf%ug^c5ZLhA6w_I78oZfS1`^T3LFV&5mUwnIc^vk_JZ)#e$-P{;@y4>-# Y_WIDd!#gVXhoB*q-hrOguJLPs0b~zmssI20 literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-009.png b/assets/pango/sprites/kr-009.png new file mode 100644 index 0000000000000000000000000000000000000000..6aaf31c3535573779cc3a45e6e3bd4925bb4030f GIT binary patch literal 1065 zcmds%KWh_U5XLW!&_7U7R4Ae-v?xeH;XqsztSM=vgpia%>(W8aj)IDI5p+`tx+t9J zAfh-(a8eu!f+C`aag&ZBNc97Vbvb{1KZY+PFZbTvbI{##+3@4p`ER9~gjQBtjZ2SVI$D5MeUVU;{=s z<>Yzb@Vh(ESzPB@yV!1aDEL3m{O>D|5qYjrJ2^LYw|l3u_09gF>PBnd$Nt~vyB<87 zJTt%JRR8J3{l?6~@Uh`99|r$yU#YBCmnR0lu8r?py0qSXbb9v4__u2J&`SUE*{Sz0 zq`u|lwaM4@-sdMH<-H3>7r)=!82I(1b8qo@ZvH7qW4c-EO)Xvj2NBa< Aw*UYD literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-010.png b/assets/pango/sprites/kr-010.png new file mode 100644 index 0000000000000000000000000000000000000000..377c86f745ba34ff0a81ddc7f3d8b87a40b93782 GIT binary patch literal 1079 zcmds%%}W$u5XQ&ETm&KNvh+#N2StZsvP&RnYqk`Ns0DTMkVEtVK^h?yWT2yh@w-M4iQX0-#?=lc3UFxbPRikOItgh-ZHRZPWILM2ODP0Yki z!X!(zx|oZ*giDrY4Y3dpiI6N~wHd8kZ(=0ql zo2$EqYZh8mTd0RdXcm?~7(f zMKC-_dsu{J(GM-EJuT9**nwNvgC7`xVkDvo5wM0Pydc74puq-=Zpz8?zzO#(R-%x> z#1WpwSp>s_bfjl7nzgV8jK^uL#Y@b?E$qP$3_vjwAq^I+p$RXDFd1mD0i&C8@;q>| z>Zk z!04u&JP#axcbf)|eCPT&(%%@M;Qu^_exG}Y$kBSOYp{Cv)U8YHwe^)fho4+N`|{@K zo#u_-ZXf&Fa(u_c8@wn#;4Y9-_-fT2R|?X#YjXGB47=NYI2&fA?2x6gCDy_oFdnC|7B8^_mNerB2A~*;kOm9Z(1aI6m<%-7 zfYD7kc^)|Y?nY+MZg8#LsG74B{GaDcZ(#+I^Yz+zYxv>S?$WOIzU}?pu?ORC=HB0z zOV4}D$A3Nj(>~q!Hu`#S?ckGY>*(UZv+l9#+Q!Y*6OYDjPxt)Dfqb!lPpiGWFn{lg zeLcGVedJ5~)2sClkI!9P?Yup7`16jxgTGJB{p_pGH!l0i%gw*J)F+#@&cubqf0spL A1ONa4 literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-012.png b/assets/pango/sprites/kr-012.png new file mode 100644 index 0000000000000000000000000000000000000000..858714176ac3775aa3e31280eed98b9cb1aeb285 GIT binary patch literal 1066 zcmds#%}dl_5XZ+BLn_kAW*1lU_sNT=g;T?{qa0A^PTU@F3q$WeZ51yBGT8Ks<-)8 z`5Wk|@c-4N>xcPt&9xg7vhuFBDYEU_%(2cSk1Ap!E)pVH?y6!ct`aI)%4%XJZW1P0 zy4A&8+$CJH3~Pvmcu0g~!O|2<@svo(0<8iPqx4eb_V1XgtA=Wp#oW!*O~W(`56b51 zuHl-67Tp%=p%I#eCBv5Lsgat6G$LRPO?W|s&8z|&Fs5mz&I8BI!YqqI7ISyEaLXbX z9+W*S!m{Xx7Tul}X<6*RE$qP$3_vjw(S!(CLla&QVKdNR1I9G%)Op~9dloBE$YSmh zp2b-N!-I09XEB;s$N}SV8f)q-02Fq(m&_fblquwRnjguw)oNFaX6!gfv*Nh9fnI{K;Z%^w4Ve==}8e>Yp7)I&+t&1{?L^ z-|I{32hZJ^e*U_9`OW6-1CMJ%pZAZC?HwHX^tCen?fCqow`Xs$Q<{^l`pU%VtA7D* C5nz4* literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-013.png b/assets/pango/sprites/kr-013.png new file mode 100644 index 0000000000000000000000000000000000000000..e3d0017b6de6cf8ae52ce4c726827fb385d2c614 GIT binary patch literal 1087 zcmds#y-U?$6vxjs2$3vnib{xrz*?d+L{MU)+|;p42S_2HR&m5mGgo16Z$ZTfZg@cEfW z!~3?+kGyKyEbX-eyFRT9^pDGinU=|=g|SnwM;jNreol>CU%XY{{itiYqrG5gi48m=;csiZxlgu zHHc2zp(yGmNDvG{m);1vbf{woqq(ai%#wD($r$-$go?rY5JycvKM+agh+oa#s~oag|WXQdSc)ag#8~ z(ycD$;x6HmWmrQj#6uz^3znu>il;`PmR+_PATLKbt6 z@GQuH6W{oJJlkxvX!t+R(VyoYBJxzdHZt9JfBw!=b-2H~a{Tetg{QaHduuCS z2M6A6UAygFL(eOdTUw_(+XsfP&yCz@&s=`izw^VT+1^dR9xX4QZ!X@o7aM=v+x7d< zlY#wvKOOE|Ti^Ys>-~dk+unSc&D9HYCkNlHyj literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/kr-015.png b/assets/pango/sprites/kr-015.png new file mode 100644 index 0000000000000000000000000000000000000000..60a3c832219b123043e00e9f3081cee26b2b637b GIT binary patch literal 1042 zcmds#ziZQB5XP_SP>P|olSn7U;v$H*Ikh3R1~h~uD3*!^^`>^x!P!YkK@bFQuHYaP z97J#saVQ9fAc#_MC=L>*x~P+$pWdI*7uqlHdw0)0Kk}g4S!_%kn-Gyk`$DV7cWUo% z{2>2Nou2IT8N1zEY|7?`nI9qthTV&U1s+wzL|i09vhJ#4Dy|YLS!Fdb6E_KytZsEN z7k3GltYHnY5D$rvEG$j26iD>JM_A&a?Z zMM(YxPQpY=hJ;2+#6k`jkJDI-m)HSIhVcUfP>e)Kg9U47!V4m71{!R@n5LaN4;+4X zv;CzJpY1!nPM?PTc`pCHyMf4VyEQ*J`grZ($0Kjgj5n{{pWj}8vGm>_E&u)bZn$&x z*Y(lsn^QZ-cZY{pE^RbEoj5nMI(T++?Cg!*(HnU=yLG6u^>psv_g7b@zkYji{`2Im U)aPH8r~iO0?S)Qjv$=BbKM~wmD*ylh literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-000.png b/assets/pango/sprites/pa-000.png new file mode 100644 index 0000000000000000000000000000000000000000..d9ba2a2a3c5473fdb622fb1f5d5563758b0fc7ae GIT binary patch literal 1081 zcmds#zf0F)5XUbKl5a@Xl9+DgP)l)cQK%^<#20^IAcawMLbw?z4IvUyXd*_$VM|D> zLw`VFxMY_?&=9nU4v7%7WKGwr=g;T?{qQ_@_rCX!?~}f2Pkp7SA|my@m%9e|Ht^S2 zSIhsk2Tw2ZIWRHM(DmGjWqJ$yg>k?Y-@` zrqh+3wUfKc9lsa)_jgAxoM?Er*uHjXY2(@6`uOOp$6H74wM?#FpBVmj?tI(Mv8!+A Pp&-58)vlG!8#8|aGg4}O literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-001.png b/assets/pango/sprites/pa-001.png new file mode 100644 index 0000000000000000000000000000000000000000..7566fd22e44d6a7f9f9d0566bb01d5aca5b76d4a GIT binary patch literal 1079 zcmds#&r8%{5XNWgBF2K95(%M8bW(7lTY3n8LQhfcQ8T*`tnS|lVAq==%34inwN zLl>(<6haW@A!tYo*JoHNFxH)(1aI6*vu-h0b`nW>O64VEX=YfWHEPl3%4wS z;X&EMA}ou3XwmIyk(R{{+`=CGzyK5@5lx7IH8kM`5jF!2HegKCPMrr%xM#5vg)HVC z;aQwTFgz$ndKROJg&Z&*r?D0`6NhGx7V7PQONLXZweIrHGf+VP+jv#~}hiC|q5Gn(Q9FB;7 zIXH>XBvCY2G+aeOLC_YImQXYlA#}ZZ{)`^b56^RV?|c9FUhVJeX>8cqAR>*`LtO)W zH}lt2spJ3R)La6E!w+g3zsulFAr>gG{JOvFV(B+Fe@OvP0~B}-XN%*0K? zBulrtn2WoFOO{~`u@Dc5kSti5Vkw>yDOsRZKw^|$iroGkQ+3r)&9a!gnYwA1X5m5E zT-`NXv(Td3LOnD>v#?~?Qav?Nvyesvtf2`nh_IPeU<1Z9?bLbTxLKHGQOIKM?iOxY z1jB=}hecQx{m`P@(;_X49k_)(_<;c^Mk1OJ0c&W&3nFX=8f?Irrky$uoN&)#B??*0 zJ;Jj%i(q(Aj`S=>6AL+DJWgXRUSb|@VGn*_0E&?aX|P}oO?W|s%|L?<7}K;<=Yf+Y zGps}*i@9fsko*apgo%_435}G9g&Z&*r?D0+P2;|B(y7>SSu3)awt7ev?$G}wSK zO*?fSIQ;Hf28WmVydNFt8>Hd?JjZ@se1OPH)vg0WP17S&Z?-($UOD*N=2xFjOb%A> zE$T&oleaRTjr8z3qEzhVSg$xaq_4WW$c5qswj0FUQu-_}Ye%XD3d7-SwpY>fL0t**G5Eux;Y%+V0c<~BXk$Wwul|jrR$cP?X0)wIl5z!(P z#%+th$VF`;T0{tKCZT06qSeJHC?cW$YHzjFD`K|UMKHO59{dA09@$i~}K_30{)Dq;hKdO-4^Ph5t@Z1!-Gb9c9J z%OV&alszoMvgn5v-JTX{S?s_q?7WJqYFL@eZh@i>jOc!?dbWEej%0L4gzG+3~PCcGfRW}v|a zjA`1b^T6SEx4U_KozL6zjmahr|K~aSy>%avt?|*r)4lVj@4ngkV&~?O->+vrEp%^x zFwuG7^V<5&p336KUjy}ymyc%749zdw!?RZ|4)$ML?AUj2;M=8o&)V?w{@O}k-<~I5 zYn$G+u1{3kQzz!yOQ(+f96q*K-!@dAZGY)Mx$tM{$7=ii?Ea47U9B6C3pc1}o&W#< literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-004.png b/assets/pango/sprites/pa-004.png new file mode 100644 index 0000000000000000000000000000000000000000..9c00bde30ebd9f4522e8b783bce65761fb5644c6 GIT binary patch literal 1067 zcmds#y-U?$6vt0iP}+rPD6yg;P#PNI5N(EbFLp_8cD)2S>EN+wNouI&4hlhKP|$@A z5>(P8IJGnscW69B5Dn2P*$cIZEe)qn&!5o)`r~=dIp6brxpyWSW0l^4UJ=4-HY#iH4t*2pxi>N1s`02ICgLI?lI5-{rs680lBKLBX5uDc zlBHW+%*9>8CCjjeScr#2NER$ju@p~H zg5g2g!y+t;erVC{X_1!24&1^X{J;PdBN0uAfHgGX1ras_4K`p*(@vcSPPk{W5``@0 z9^qM>MKC-lM|u{ciG>_69;dMuFEJ0dum?Xd0L4gzG+3~PCcGfRW}v|ajA`1b^T5fH z8CIf@#oV(*Nd5#)!bD1jghoolLJk;@(^!j_*a1t1@dE=;j6_I-1#4)+3nFX=8f?Ir zrky$u9Da9$lT%;$e7ezWOw#awo{PV4t|0PCy*kqB@60~9uuo3xIJ5a=@zwIdU-SEq z&#tfDdbFooJ8*m8?oe(0WM|{Y={Ljvv-{^oWoF_0<8#k1Kb?EO@_lCWlz;xxIl6Id zb+Eg&Tee>B9Dlgf_p%3P7Y4?;6YAWYDf}gNfTnr4w?eLm0gyr1`vxzcWpRtMJ(ib!>=IWo?7 z6@RsX<@|qjuD6R%Womr1F0=2}e-l}DwY|5~;88_P#6?0R%UxAW#Z^KjOIc0K#7)8^ zOSih1i@StNmSGLC5D$rvELfUiDV`E3S)f%wVw7Hr-2NR?b=6SKvY5M>x@nkZ;X&D4 z-8EdZ(4yNyJv2hIuw>X$JvCCZkVXWop$RXDu$fh01I9G%)Op~zS(s%}$YSp97H(Mt z!-KMiMOYU7(4yPZA}xy@xP?9VfdME+BAO5ZYiPm?B5Vd4Y`~bNojMPkaL-~T3R%oO z!m~JwV0ciD^ejdb3prpsPGc=zVjgZ`4}M?(ijfFuuwV^MctM2CK!Xh!)3j6Pfs-XO ztVAJ;xo3%x{0W?diIfZpjg*Lm955cIu@*0}1C|Wq2L_-RiI4^h*3g6(MA!^8*nlxj zJ9Qp7{O&eS9Q?{>;pBL0f`S#3`D6Zw_WR$D@0;21 zX=wHI>y2Av_`RQBGe7+D%UomX=%!=gOWhTX`Ac{0PwnEF%Jtm(`EqaD4VKGTqct*H HKXl*n2M`}N|v&kn2DQ& zNtSMPF&B3Umn_2?Vj&(9Az83A#Zo*aQnEm+fW#=h6uJF7rs}Gpnq@I}Gj-E2&BBAS zxw>n(W}!v5g?ea&W?{*&rFv?lW+9CTSVI$D5MeW`zy^$I+NtxvakDVXqL9Vh-7Va* z2!;n`4~wuY`k_U)r$t&8J8%no@B;%-j6^gc0@l!k7ev?$G}wSKO*?fSIN_efN))n~ zdxU3k7QyhK9O+q%CKhtQc$~&syu>`*!XEs<02Ct;(qO?Fn(%@Mn}G%!Fs5mz&I2b) zW>|?r7IV)MA^8(H2@@$95*jHH3prpsPGc=zVh1c4#t#fYF%lsS7ObHOFNm-iXs`ie zns(|uaQNK~PF~vL^JTWxoTTCZJQufbFC+5Sc>T;&|MTgm?>pMV-CcKX4FB4Au>bI@ z>YmG6LnC`P+Y2vu|2V$cs2%IOzw!3q>7&b)Z-*)ewioyHF0EGQKJ}ldF3L=M_1R$M z^~l6^H1m6d0X1)9r)e3{`ttM>h*gMSuEp?W__i0 GW&STm7GM4V literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-007.png b/assets/pango/sprites/pa-007.png new file mode 100644 index 0000000000000000000000000000000000000000..5fc066b581effd50f9fea1412ca1ba4eab129663 GIT binary patch literal 1079 zcmds#zf0F)5XVm?FyRY@SSl5iU_-w-1&0t6MZZ!aUoAq4q~NGl6OzE7MI@vetd53; zprF8^p~c0*w#%u-g0>)`r3NSKdiDGnJ)j?+=kDJ3{_$NH8y%`NwKs`Kr8+!Nz%CI)#_5fgEd5Xo{^6;pAQP{~qO6Eks>Fv-%b zF6QDc;gV%oLoCEYA|wlzrdW!nL`oKD6_6OEmm;@+$5dT4RI@DRZl-P;rdfDUHdl8I z*DSQ?wong^&@3z&wp34z)GVYC0c&W&3nFZ071)3=O*?fSIBphZSroFEySs&37QyhK z>|qg>ML)FY_OwXLVh3(v4}M?(ijjyWM8F!F@PY`Nfd(5erfH|n11H?GScyUwbC2*W z&LS8dlp{Th(ZoUy7?0Cfi@M#pLRKhOECTh9?WQyu7=XkD7Q|E02eva#X!$EmN2mA03s>${dl zT0X7x^wr72x0e0G!|QWvdp-YZYzI~zl(bln@H(s22 zdhKdw$DyOMZ#K@pZSQ;aY~|2cbU{&M}})pIM|+b0e@={mmq%KOc2 O2uO8sbYQuE>h53QrD3-K literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-008.png b/assets/pango/sprites/pa-008.png new file mode 100644 index 0000000000000000000000000000000000000000..0e6ee046c4c3fa15f01f7db115f78f906b747337 GIT binary patch literal 1077 zcmds#%S+W!5Xa}*<{}B|CW!)}h;|WUxhs-uX30hL(!wHyU|PA+Bt?iY5GcfOB1KUnW;Y2Mi^A}#$VYD0Wm z`Rk}|q-02Fq(m&_fblquwRnjguw)oNFaX6!gfv*Nh9inHwlS{XsSf_PW8!s+=e|l)Gdwq89&8^3|)IRpEvLz?~T-n=weEPuIJyWwUSug#) K_1a?3=#TDzM6&#q3l^Jy5Vb$7_z($*D`Ra5=FgIzqTh>5sJh-A5|imA9tsAMUtiJ7=b zm}Kcz7jto!aLF>PAr|5x5t0Q-fT2R|?X#YjXGB47wXAuk!%8{PMXksA;jK^uL#Y@b?E$qP$3_vjwAq^I+p$RXDuo-Bu0b`nW>O63= zWQLU}WHI+F5t2WFlQ5BzA)%2Hv5*7C<22UdC3e7)Vf?@V6eAJRV8I%i@PY`Nfd(5e zrfH|n1Bc(;u7TlYKJQ0s)d3p*&vWAU#e0aH=;=H%*fewY=F7^$_Qs=Emg*m;D~GD* z+xurnVWdf)w1eG`wys!wXWe_n4{+Pi18GQKdrt#xF!=X3qs dyG9>dzASIHv_1Ipz4HKMq^GOeIoDCY_7`iwYmEQ^ literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-010.png b/assets/pango/sprites/pa-010.png new file mode 100644 index 0000000000000000000000000000000000000000..3dcdad74040a5f6a9ab32f951badff3fedcb2f37 GIT binary patch literal 1084 zcmds#-)qld6vxlD#JXZ><-RhVDZh{d_m_ z*Ho$F|Eo*m2l&*S@9%1p>4ojzMQU&K9UkoDQAJF|MM5OYT~$oQRYE08SxwBuO~NEg zx4M{%yM#-YVGXem4~dX0Sejxfo)RfppjAL(lwOM5{vA_w)lki{n7f&}X_#i=LD^j0 zHC(gMqT50}G(xkmWY|(YHBz&XMg**(2``ASnN?r|#x(8JdEmHNm}ODOV(#u1ZdnAw zgR+N3SQh=zqTACVEsGtvg+2Iz0VqZynh*hNXu=C3Yz7)^z?i0;IuD$1&tfGCS>igNoH$5MYPMlePcj@%v>dv<{M_Si@^sbCder&EAdOCIP_Li}Uh2<~1?$7_O ztURl}8>t=bdwl%sgKe8e+s7+As*6vCSNBy$H?%x#x;(dh>cwRB^UIdX+=;2@(lfJv U-LcJ|?!iL3J9|5(+fH8n3-PdSh5!Hn literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-011.png b/assets/pango/sprites/pa-011.png new file mode 100644 index 0000000000000000000000000000000000000000..b4379d94750e5f700de15471289a8bff437725b2 GIT binary patch literal 1081 zcmds#OH0*Z5XL7(FhMX-Fo`-)Nv(>8q=iMAtSb|VYKi`kh3;O4MXXcsba;}XG*V@~9+C-#%=vcMR zw}Zd#)@J@MUAcLP&xWabZ9o>^Z}~3LbYtZB*dUK8Vj?aQB3bULVk)i@Dp|^EVkT}9 zCRw`G#a!GaT(S&nh=q7agk-_e6ie}xNXY`N0urP2QslONOx0CGHOpe|X6mM4nuP~t zb9L8n%|eTA3-!Q^?1gxP6FNm-iXs`iens(|uaKb%{l_+E} z_XyA8EP~-dInuKjO)TVq@i>jOc!_zqg+2Iz0VqZyq``tUG~opiHUkYdU`*3aod-^q z%&-!LEasjiLh>hY5++hIBs5YY7IMINoW@$b#12?8j2{?)VkANuELcMmUJzk3&|m|` zH0{)R;PAWa9c_H$^Z9&zc$9|!^Ng=vyobo?q3V&b?%6Z9KW=KSv>yFy50}4O-F$z? zuD2(iue_;MUN>@k=bol>{^a-B*~3efwafBhX5*i`a}DX}-`dqOHu0kC(a&mAXV1>@ zU-PFQ>!dB;X?gWPcAq@xr(XV;>$|WvGw^ib`hn?NU;CdeT>7-UGPT;f&ENG+?%hAX P4+=6gI9y#En3(woc>!oR literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-012.png b/assets/pango/sprites/pa-012.png new file mode 100644 index 0000000000000000000000000000000000000000..35878a50e3a40006566e2f31e62a88a394258328 GIT binary patch literal 1070 zcmds#-)qis6vxl@q-J?UT&yP-qqrGS=f({&n|VrWJ$txN%7spn8;vp-ERx4~|q+BRm>_RKGl;h>|XZY0i?ejV3ywCf`b7^RBpxRmM6p?CuPhW%Y z3jVq}D*S(O=E`aO9Mg%;fw>Y)*ug(bt5>Zy^Mg)|~y4NZ7Kgw3o18!)D6r_KY%&B82;LKbs(w{XiM z7#@^8EW)zrhZfzQ7HL`Rz%A^-4-7yt648VRSVI$D5MeXWU<1Z9?bLbTgnJe%QOIKM z5uU|a1jB=Jq-QajSjYk6aT;s!67z5id+-ATP>e)Kg9U47!V4m71{!R@n5LaN51cHS zVI>M#%soqlDl;YvSb9>6Nm!qcVDb%jpNtd%i8} zp1wG)x0WouxvhQt=ZE(%KX1NXJ9@1)v#UFQADR2I@MqPl>b?1ytp}Hn?Os3qwmCVq z>D}?$7h0>o)+#-Z_SNSfF;E57#ZWO^$4QGyCN1VfISBf3UCB Hd+6L>x%g+p literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-013.png b/assets/pango/sprites/pa-013.png new file mode 100644 index 0000000000000000000000000000000000000000..98dbe63d8e25052dc0e6a4654927816f6a9df4e7 GIT binary patch literal 1083 zcmds#-)Glh6vxjtGGjD~$ymFINUT$ANo;MlC10~mzLgQpA*q|9Ey`rlOf~6;6J3?* zBI_bWrHe?S(^N~-O&4ACsff&QK$ zzN`6buCL?&^D`HA@~JsK)YmQZ3oYM8YG(%bjP&xTA|~P@A(G{;DyHHpp^~MnCT8L$ zVUnd=UChN@!X?YFhFFM)L`W7aO|cYDiIgnRDj+dRFGX(uj;Xq8sAgHr-AvsyOtbKy zY_9Gau32c&ZJ{0-p;=fmY^k0asaZ%P0@l!k7ev_1DzE`#ns(|uaNI1+vM6LRcXtc7 zEP~-d*~20%i+*U)?P-yg#SYxU9{j)n6eAH$h=4UT;RO*k0}VD{Ow&%C2Tr(Wu@Z$W z<{sf$oJBA^C`Wo0qltwaFdnC|7B4Xmx3C94FaX6!gfv*Nh9e)Kg9U47!V4m71{!R@ zn5LaN4;+4X8;19PC{?N+4H=n96J4RdPzFr(%YTsRZb8^+#<8xOQ51jw}b9(ER2bCR*<5O!V zAAMMJdsA2QoohYEw$1f#oa%g1eLHsP$n?bPdn+efmtQn3-Tm@w@#*y&tsOIm|I9Ae XY<}1NAffg*Ig) zw5nB+ijE#6(;qM6%pf#Z+7+RI-%S#7x{I zOtN&Vi@CTk1tdo4rO55yF;!O$)hvs-o2i?IX%-%o z&DCAQH481eE!0CJGz&|HE!9&aH4AA(z#5wHf(V;g1vX$z(@vcSj+=#97KJS4?r!0h zMKC-ldsu{J(GM-UJuT9**nwNvgC7`xVkDvo5wM0Pydc76puq-=Y1*mtzzO#(R-%x_ z+#@`Tvj~O<Yckj{&{A{rHM}~9UYaax95gOZoNCPbMfQt`j>k%hn5fSZ+j&R500OD z@n(1L``@ci&R%}@e4_p5+JQY|=a()_Um5p5Q#TI(T4-zJv^|`By*|I(xp=jyUe%X_ Rqbq}OkZO0WGTU`#>@U7WZ`PmR4?Z^D&yBTCt?*f`z$^X}RC;o9Dj z7@eM3d$IN8;Bs?wVf*;WUq2pSZJpWN`rLXmy!K%>9(}*_<;e4$*OQ&qzb95M-hcIS b-<6546Vq>}hPP(d53@kJonCvRdHMD~5)W4y literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-100.png b/assets/pango/sprites/pa-100.png new file mode 100644 index 0000000000000000000000000000000000000000..9eff408b95feb5eb0a38f954c9c007ed9a12cebc GIT binary patch literal 314 zcmV-A0mc4_P)Px#^hrcPR5*>TRND>2FbFeIH@eKk%~WQ}PTWVE6iP_@ah58jV2ni`gb9?Pvp5MT z#NHNXBJ`$cC06dL#JQHu^_r4dd+I#aF4I(1);h!rR2rn=08KA{dpo}D-*7kJ7~;Jz zQH5#?>kefo38?fPc(U$_Pi|ffwim5l?2m?B7H#fxQ8r*dgGPx#{YgYYR5*>LRKbnJFbHI<>*!n)TT`gXThrVl!Dq0YjkHLS9GHQD;D~%_SUIV} zCD>+s$%Gwwhr=h*oxubpOM~unjWOqx?08zapF^aPwEH|zid1w^>HRnU>pR-{?A|Xd zRruqFToggC;a695cP22Nm zphw@(XDuL&1E>HhmI|^8m@{s9g` VgR1FtPx#?ny*JR5*>5RKbzNFbI>}(rr!jbkyXYbdRhP7I1Q_nam_ulEDxY4P;Okr-LQ- zU-8a_oruE}SW4uMEczDxB>H~$#AD6C z8s^Q~=m@uM>^6`=(v+s<)2Px#{z*hZR5*>Tl+kU*APj`>P)Ev4>{QRhPS=M7Vr=tERck3yLcX(oI0X1(uv%1y zO4u^rqTnPxajZn_bq?9y01aF#E8`_u4FCw#sp7H%)|*&;Kz=iQV$ZfH zih^n)8&_4Q0U8}JOVtBMrq;V7%{)BYKZi6V4QX@%kotSg=Ns#qEWLPZkJqY}EPRj2 z+++N0kXQL1c`dwc-!UW%sT_4mk|)CLoLt;A$M0Z?V}wJO5|n?VP<7)!R`?+NxA*~D WUbBIiE*bU!0000Px#+(|@1R5*>5R7)1aAP5}tdZT(KdZv0N??g9I0l^wpdFXtcpoE}=A-+cy6zvV0 zPlzbn2mr*hEp_5FiDA=38Q2qCJ9`n>3tfJJSU@ZxFsiYugBSI0n~hId++N;Nszhaw znG7^o+aBLa)q~9uLMP0*qf!BYMu;cJ@Em~q3rp3SsoOyc)tMK9Xij1W{p>meW=P{j zXg`VF!6wXC2!3}!oPiPb4UuMT_Bps+=m3p_zg6pDh<2A#(1AyHBwILr7xPQs oM1Xmzo~H~&j0`b literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-105.png b/assets/pango/sprites/pa-105.png new file mode 100644 index 0000000000000000000000000000000000000000..974d38649752944ba6ba4882de9d9db1cd6dc64b GIT binary patch literal 310 zcmV-60m=S}P)Px#@JU2LR5*>5Q^^s-FbG^c=_sm+)MVA9p2$lQpe5t*^2qGLB_Lr6%Wu*lESEn1 zLnKuq0Eq5q>l^1u0-GnQz?tCsWsiZg(3O9P8bpl(S*)xHKGg5AA3p7Hdu2s{_WE|J85~NWPE>10tpk9pCO#=6a{y@yNAzB)$3>#*ss{nv6YgNv?t7pL8+r&G zn;2B414m>w0?8tnz);RT;vx|eK1NmGokr;Q;T{laaSB1X=sqK(7qiHw{j`n1E_o9S zv+4p_f9B!6LSi~&Hurq7qlttP{Ev!w<Px#>q$gGR5*>5lU){sAP9w_(~ib9F>A^-*_!S=bSMxdho?ySxctcUVi1D$xe_~n z&y0-%Rv!2tjvKnT<#o=8^)Lgk1X8- zuEgPKcm3MmB>tzM@GzJK;hEoOOKD7l`$YL$SOES2pu2R)H{WH+00000NkvXXu0mjf DHWr8j literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-107.png b/assets/pango/sprites/pa-107.png new file mode 100644 index 0000000000000000000000000000000000000000..516e4482983a356b6dd81869921671c822ccb329 GIT binary patch literal 338 zcmV-Y0j>UtP)Px$3`s;mR5*>5R6(}HAP7tP=0?Mr?3v1$yc2)XqNuc;pN)zV0-{8K6&Mwa>$vZG zZl{79Civ#;d`tSY`35S;*%(P!I-luy)YDlA?v$`tBZ+l-RFILl8YlUrjzfn=@GL2AFnl7x^yDRuPaJT kqDHJK%z3`xe>F((2MGs%1nqDh!2kdN07*qoM6N<$f~*vc&j0`b literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-108.png b/assets/pango/sprites/pa-108.png new file mode 100644 index 0000000000000000000000000000000000000000..617dd368d424d50d68cf0a52f82b96e1a03a1d9d GIT binary patch literal 324 zcmV-K0lWT*P)Px#{z*hZR5*>DR8ba$AP5|K-6+q*o=MJRXJS8E2@SQke3Y6q&LBisgfcW*CjkMt z9{&Js{suIQhXPMReDkS1#gc$R3y~6HH4<-Qsvsh%YAVSVf^%t+xd6G`3RrFLK028M zP%R)z&>SmLhHef({S`IwNsR@=8Jm{t>nYcQ{NBtrNnJx%NCvKPx{XPog&zc!q)B2F zBkp=CoIjwqKwCH6}Cu|E_5P*j1q z;~oG3)r1ozY6wh;YnHGxUBzA&Qrp%|yUxlRK8Y#*H>_e=i7OTFPxX0nYF{ask$nN7 Wu!SP4YZf~I0000Px$1xZ9fR5*>DRMD-(FbK1w8!0n!GaWOvGx0vsL=;Hx^pN^Nij7S~003r(nH`27 z!`Cwc$1^^IAU{l_{ zM0{Zy0I(}PN$ix)L>d^KVR5!Yyw$~n-5{$0 zE(=lCQ>4Y){%|Tbit;Y}C(0M;QnYU2X^2C?VafX?kpNij%f!%w*TYc(;O^e7G^_6D dtY7A$_74UBph}(xvylJ*002ovPDHLkV1jiaj^zLV literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-110.png b/assets/pango/sprites/pa-110.png new file mode 100644 index 0000000000000000000000000000000000000000..6e7a7f81626c2604a1da81dd5a78c80ed266c8ac GIT binary patch literal 310 zcmV-60m=S}P)Px#@JU2LR5*>LR9gPP^#6DV50;JM5q)Mq{8^{azGgyJzp(R-| zKLI=O&V-eSz4lO9^HIQwn2CuKIp9`Ft4fRFhqe~%LpSxgr3HD_Gap_~R5ucPdFVwT7%Adh*1{CF3UzX<$p z_$rEBsc+*ivAvD9h~*h@Tt*f+u>b%EnGF2oF01E!06G+%mW5Fx761SM07*qo IM6N<$f_^1}>i_@% literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-111.png b/assets/pango/sprites/pa-111.png new file mode 100644 index 0000000000000000000000000000000000000000..5ab490066b20588cb5f75511b147084bc4fc01bb GIT binary patch literal 322 zcmV-I0lof-P)Px#{7FPXR5*>Llu>d7AqYgfQ#q1nB4=98WX{z2n25UI?50AMAH%>CS`_%F@$^Fm zrC`tTM?hFtfaEiLz`|4RXw$VOuaL5W0ULlSdDutgqzN?LCPr-XEIq&4{a8V1kAAK7 z8^CAmG(>dX_D%<+4zd9NS*d?l1=)5$RG@w-&z7NP8Ria{T^fw!t(C&qJVD=Y;kgR; zeUOfzq5N0dt10e9umDH~Nqt`ishia2jHfw}U8^$s)L}qM4d@dVEqqS_*&OP>B8K;H zZNsXg{1e~+N`UIo0QOT_{c#skX(qs+SGREoG|s^|%*FHhpYiLv(CU}#9`GjQ2OJcb U@7{BR2mk;807*qoM6N<$f_vYOYXATM literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-112.png b/assets/pango/sprites/pa-112.png new file mode 100644 index 0000000000000000000000000000000000000000..e75ee5ea72298b5cc5c7ea591384e2d8d48e9023 GIT binary patch literal 318 zcmV-E0m1%>P)Px#_(?=TR5*>LR6!DhFbG^6e`MZ7-(=oI-$V~hA{dCBDJKo=vV>LO&!AOO89D)~ z$52*m3s<>G!BouGtJbT$lWZ%A8Q8`|a3@)T0bqB9O+K-L8qlf;n_1vp_UL}fb?_=q z=@v;&yHW9ENVd>bxYPx#|4BqaR5*>Llv}RDFbG6vtS-rGa@SPWS~dkm)Gk0q|xHh01Bt0DI)Dcg#WWE4VUqD9{-MEeu|S3W2FX*h=MVZr|De z4$%Q^*2txkJ(_Yg``R_Xh#sOkkL+Y$r-FSObwH0y&kl?cQ;Ebrk#Ml6I^*iS1?;kA znh9pWZ)IE;(F9ebG1aNGhGoBmSiC0i?Cxy;ntN`$U^vQ^;S7j$nT~lGumbG;?BD&a zPhu|BwY?em<9lFbmw-Fw3eqrD^9}RDU(eYFFtNMN*|Px#`AI}UR5*>DR6!2IAPjR*e>C2dKb1FSZ}JW+5FlmMEVUvuc1#)}_yH;y?Ms0G z_`d!jw)hEYhsA*x5$$>`t63?K@CcC;Q8p2OjID!6q`H}V(a2PgwD05BR}3RzWG!^u zRTjx0ZCTF+Q;SZEZ3XZR71DawB@3;$UUC6VJF}Edyoz-P!7*qG$pw-NX4Kp+@GeW0 zDnip`zd5;*#$R`n4Au20u2L*AlcqR%g5+slRpa$bfzw6@GpOJ|s#VW%9ufkyzu|ka zYhp;=;cOEsf8Pgnkd7xwJMnav@Ps@F4CHAfmN>jnjANGX&*obS6Mn(ve*jn-r-3&L RefIzW002ovPDHLkV1j-te|G=? literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-000.png b/assets/pango/sprites/pa-en-000.png new file mode 100644 index 0000000000000000000000000000000000000000..9e2dc8fe5ae0fb585ad67867672372a93a21ea55 GIT binary patch literal 1077 zcmds#-)qis6vxkFlX*0)?ZR4=3wA?Er=+we*7LNj*3;UCtkjTGF4P!tLs?1EiiSE@ zF1T>B=R)E_nv1#7Nh`II8|7kLkPC4k9WS3h!>6`ypU*kxecnHwn`6zP&T3y(L^?-? z>*IX8`0MRxGq zHwlw0-Rfd4?h-CphBd@OJS0N0U}=h_cuJ&XfmQ*DQF$d>7}K;<=Yiv9VU|T9i@Cd7 zxMdLx56T`EVOjJ;i*8Sgv@CYu7WUu=2A~*;XhH<6p$RXDuo-Bu0b`nW>O645J&Tnn zWHI*$&*ChC;XyglvlvY*|z<8X-TD-&#STc+s7=U6VLK-YsLla&QVKdNR z1I9G%)Oq0WyX&8r`poD3h4JPD4gcpk^7HaTM9z-X4^8&oyMDW}`PKH0wm(OgR`S<@ zU9ZpX-g|6kU$bjwapA{?_SM#tEsrm@*G6~T-&PquDZjg}y{L9ie|&m(&(fp8p8e|| zbkzHozxQ;{RaS>jw4H7YOigWk^YY5!=hK&3-_E}~{_NnsPiyl76Saj?i<|DOoV)S$ R%-RRm%SfYHZw*e*{RLAfVtW7p literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-001.png b/assets/pango/sprites/pa-en-001.png new file mode 100644 index 0000000000000000000000000000000000000000..a3775116fa6172a9b15e6fa28f79ccdecf03b2ff GIT binary patch literal 1081 zcmds#%}do$5QayukcfsWyNE%w2waa?i;9waF*8KeER-OGhH52b)T)IPWFHWv!9dcY zO|+1R3W_3w7KVeZleT^BT8UCHJ)J+J1N!5fnR(~^xO2lp{cSCsEh5r3aHKZEx1GOg zbA|sOuG~Jzr{Tg#f1fO^b^R1+yg7Vqtj?o~n23vnNS3>*n2M`}N|v&kn2DQ&NtSMP zF&B3Umn_2?Vj&(9Az83A#Zo*aQnEm+fW#=h6uJF7rs}Gpnq@I}Gj-E2&BBASxw>n( zW}!v5g?ea&W?{*&rFv?lW+9CTSVI$D5MeW`zy^$I+NtxvakDVXqL9Vh-7Va*2!;n` z4~wuY`k_U)r$t&8J8%no@B;%-j6^gc0@l!k7ev?$G}wSKO*?fSIN_efN))n~dxU3k z7QyhK9O+q%CKhtQc$~&syu>`*!XEs<02Ct;(qO?Fn(%@Mn}G%!Fs5mz&I2b)W>|?r z7IV)MA^8(H2@@$95*jHH3prpsPGc=zVh1c4#t#fYF%lsS7ObHOFNm-iXs`iens(|u zaQNNr8J+mS=kv_S&?pW6=NaF){0Nbk25N`LstecdG;Ckq)m+&+@oqVPFTOaldUER7 z`JM;$_fPI^Zo9s5^wW;%nWpvH%Etc2nY|sut-ZhA%vPJ`?|%7=-qj&mG-V7MAyZ7uhgVKQlJOql%b_i-bs)yQ-LqtAt9HvYMEQn}kW0 zZgnvicL|p)!x~~C9ugs0ur$R|JS9@HK&ybnD7_T9{X3@Ws-c=?F?Ta{(=g4#gR;50 zYq(~iMYn}|XoO~A$*`q*YNTc%jR;sn6J8KuGpoP`jA`1b^T2VlFw3Hl#oXO3+_DIU z2W1b7uq^taMYpF#S{6HS3w!Vb15k`aG$8`k(1aI6*bFq-fH6%wbsjk3p2bQOvY2~> zXK@z6@Sq&&S&SwYa=>_;##+3@Jlw(_{J;PdBN5VI!5W(If(V;|1{*M@X{XKuCrf5n zi9!~0&k`Z|6F3PIDH#$PDG>`fU_4G^EnZ>=EE&cR3_vjwAq^I+p$RXDuo-Bu0b`nW z>O64x-5nggu+C@oR--mb!~c2C{l5DWk+(-GC&zl8zMB28z1i13xH{AQ?*7LkTRyG) z+1Y<~-`vE~?C{m&ZToNRXgu1}wYK%dmuu6TPrqK&H_yI3@9G_IwW>ev$YkIA(Yf}A z^@~4yS8V;#ME(4x_13RgZoZ%D^#18XR~DxZf37sEYvY%%9~gYwz3c0X$@yCAR22d; MGE}Q93`{)u3ti@BMF0Q* literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-003.png b/assets/pango/sprites/pa-en-003.png new file mode 100644 index 0000000000000000000000000000000000000000..3a8531bed94950b881e72bdfb749668b2e60795e GIT binary patch literal 1077 zcmds#zf0F)5Xa9P3dvGJ5_5il;`PmR+_PATLKbt6 z@GQpT1X z=l!*>OP#Y1-fq3PI@8zGdF{&h)UL_8$<7!1_V0dkW^R4u^s|wb->ZK0t=&O4z^(VK)!%OXtI$qzrvpB!;Q1#REwb>uFr^DNuw~d@Tb?ew+ O)=Ragzk8u_V(KrWZEEWP literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-004.png b/assets/pango/sprites/pa-en-004.png new file mode 100644 index 0000000000000000000000000000000000000000..8106e846285606c62928ca3deb886c7ed1d00d5c GIT binary patch literal 1073 zcmds#%}do$5Qe8T5aW^%ftUrl&>#?Dprxo?F(XXwl@_gpU~pGafo6*3s-y@hXm&*? zv`DCB6fLqAF}SFpY|$4KwP@?2g6Zk}86D6c=giDI@5jA7Hac8sZfh2i%E+N=jc*Ho ztxXO5Uz@wy$7jo#+VG&PuDAaX*}6D(WPFH66)_PP36U&!RWTJ;36(5mH8B%636m_{ z>S8YL5-wSWHN-+ZBto)aX^N$IN~C0gRso4odMR@IcTCk)Lp94{?q=$yVVZ>pWpj1c zaLq!CZVUC$2+hKhVN3PYNXnXfblquwRnknxP?9VfdME+BBa5BH8kM`5jF!2HegKCPMrr%mdvmc zg)HWtB|`Eia1tg`G9)xoA{KJMc$~&syu=PzGK?P>fMO&<8Z1~t6J8KuGtgiI#x(8J zdEoH7>#86B&S&FnZM06q|9OsWPTfW1$&u>7cj*^e_cEA z=)=J5x0c@rUo^a4c|O(NcK%7%!{vAP`;XpUx$&p|dGCX{H#;VKrh9uD7f!QUMutYK JtAi7l{sLQPWSRf~ literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-005.png b/assets/pango/sprites/pa-en-005.png new file mode 100644 index 0000000000000000000000000000000000000000..59a33a43afa4478509d1ba2c143fadfcb9551204 GIT binary patch literal 1070 zcmds#OH0*Z5XQ#~WWs?60+AxlqM-EzL5nEyk|0Mpo{A(Q3ML8{ffWW;>ZU@XT13o3 zI^9;0-=L5Rl9)=uMHk%0=>yazYWnqlj9$<`?>jTkJePB0tTt5X?(G(l%J9MdI^RwF z^>lUc|Miud`}nj?)rb0I>HXI4BJB%fBaHzbRm4PGBt){@RmD_XB~-GM)x=EPBuuh& ztBbj~OSohi)({KvkO;|wr74!;DUp%|S_LFV>7~f+-!WBJ4b?1*xtpn*hG`Zal+D#$ z!!-*nx-Ha0BQy(3hAq`oBQ*) z6tb9mmI%q8z)6@$$&k=UiCD-1<8c~m@e(^=$uNFk0E&?aX|P}oO?W|s%|L?<7}K;< z=YhlTZs+(!i_hot_1ZWM|K~aUbNUe?Cx`nFGD1-fSKl7ajb8jPyJvO7uI1-%r~kZb4t{<4MW$BH9Bu5qw~f6rJW%Uj>N_$2 E7rU%w5C8xG literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-006.png b/assets/pango/sprites/pa-en-006.png new file mode 100644 index 0000000000000000000000000000000000000000..3ac23f5d4031bd901fd7931e522fb0c243d10aa7 GIT binary patch literal 1069 zcmds#%}dl_5XZ-aGQ+k4!K6cWNW!HK16^u`Y|R#8+jQyBC7~J!4+YyHiaK~63eCPYJ_gl@Wf&Srs5gBNlu21vb z#oth0FaO`)oge4Z)0v){knV?lKSj1YXq{o*JoHNFxH)(1aI6*vu-h0b`nW>O64VEX=YfWHEPl3%4wS z;X&EMA}ou3XwmIyk(R{{+`=CGzyK5@5lx7IH8kM`5jF!2HegKCPMrr%xM#5vg)HVC z;aQwTFgz$ndKROJg&Z&*r?D03FF8tv0>Be+(hKB$1od0$6IU?s8^%L!(rTK*&JJ)G4>x;GJE8Wq>yH!Nmh6ZaR z+&lSnRJZZ{eE!B!u9ov71AWqb-T6tRvNU{Zte-~}F%cIDku0&Qn2M`}N|v;mn2DQ& zNtSALF&B3Umn_X1Vj&(9Az83A#Zo*aQnEm+fJ7^`B#HhVQ+3r)%`%wSOx-k0v+y8o zuI?JHS!hvhp&lBcSy<9+sh%3CSx6%S*3g6(M3~GdumPi+a`HTI+$_wpC}c3PyMKrs>_4Hm4S2``8+8ECKp zqnmQ_JaG8kb&Za1a&63w)JG}!KhNpk7w;kR+)(ZKSjWnRn@er7w|Zi=egE90^`}3! z&mXwe*nCv2%-sDjeg9eSo#vat-a`wQZ_i#kI5^O?Ftt{hd2#kjqi3S~&+^yqj}NZ4 zukHEv_VB)*<&AgmpG+SEex6w5e=0K6 LU#~U$8dv@TnsH}D literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/pa-en-008.png b/assets/pango/sprites/pa-en-008.png new file mode 100644 index 0000000000000000000000000000000000000000..f6b2b2ad7b974339ee4908cea5f62290b65ff63f GIT binary patch literal 1062 zcmds#ziZQB5XP^mlu`-mpj2@YOQsIOfs+W0DK%JQO+y7i;vk1gHytd3wIYI+>f~^n z=pu?49Gw&tm!ni1v>-Gnif$s}rsv1^&+vuz%lqElbI*_5oM}x}M|O>fNVR#m(dN6A zztQ1A{=dAka*)r!h4xfkdaJv?iBy(nraO~7s)&iWNQh*)tBR?(N~mNhtBIMoNtk5m zRu^+|mvG54tRWWSArX=VOH(YxQz9h`vEsJ1y zQ1-A0%c37zbbDH)Ww8Udum?Xd0L4f|6Cz*@O?W|s%|L?<7}K;<=YbRMS*%1Mi@8U5 z7H1I*56Y3A#b{z72aLyQti?;r!!7K=4-7yt5+My1tf2`nh_D%GumNM5cIrHEvSfyp zC}c7BED@4Ffs-(ik|Cjy60wj2#^W^B;w5&#l41P702Ct;(qO?Fn(%@Mn}G%!Fs5mz z&I5(T>6o@q8FI-_@A-CnB7p5ejucl|x`P$Ou6GOGfUn>2vO=Cwp```b(wQtAT v!p_ZC=eLb-^tJx|z{f*BUQCZaU3_$8%bPo$`Fq{&Ird3&veoF-Pha~BHN0PN literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-000.png b/assets/pango/sprites/po-000.png new file mode 100644 index 0000000000000000000000000000000000000000..83a43918638b81307306b0ff7d1c2c7783e69968 GIT binary patch literal 349 zcmV-j0iyniP)Px$7fD1xR5*>5R6(}HAP5V6y^(i)6Mr#e zX69rp6(2$XzMJuo%{!kgJnNBnRRlAGs>ZjfPT!k_3Pc1m+u{|ag8yJE;8LSw?-5rv z-WB}V%mbh81V5#W@)+Nck1>KO**%NwqvS=lJ&5sL<{+ZiZXGyqG_S$>Tj+aP)JufiFB*NL(`Yu`Cx00000NkvXXu0mjf;Q5sz literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-001.png b/assets/pango/sprites/po-001.png new file mode 100644 index 0000000000000000000000000000000000000000..be3c8da018f52d0994969c6b6babd64dcbb90814 GIT binary patch literal 359 zcmV-t0hs=YP)Px$AxT6*R5*>5RMD-(FbFeGw^4Q?Gr>;G#QW$$xrAJGNR>*8jh%!NO89^Sz#N=w zVv04$K=J;!eyrdwkKiGm1d&8XF@@^hc?AQwq~yXdh-r2W5+92qyB5?Yy&8Yu0$^s$ zMupm>RFEo$-36*(?)|IkzKOvYVE42M5rK%bWSa#5aCZ<Px#)k#D_R5*>TRKX3zAPjS=Zp2P7Q?e5?TXA8go(BOdP+#45i-%k*UBa_#DCOyU~I)8J*mG}dHQ|7Z` zo;GS&+VOWPg_KzVfJzy}oDxc{gMsHi9-}pxrTh65lv!H>;N~W_6e2S0+i!DfU|(md hpjO-0xZ&@M^Z`w#Znr`9yE^~?002ovPDHLkV1n}zc8~x7 literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-003.png b/assets/pango/sprites/po-003.png new file mode 100644 index 0000000000000000000000000000000000000000..c6f90fff98ac4933313469b7199a32e145568b14 GIT binary patch literal 300 zcmV+{0n`48P)Px#=1D|BR5*>LR6!EMAP8JK{gHoyH{qXr6FnptVkEKc&N$HwyR4uR0<6HW0BRtg zzg7r$=mN~#vs2qfwvMr>11ks7O77sDk>huqM^Ie2HEneoEQJ98wbss~F(?z6k%5~U zIANTRnATv0_~>92ml^=)K1YoEPX%@ub1z&PJ8k0%+)Wqwh8#uyEuzIO*zV*ptH%OA zw2_(3RJV+(hk*awD-HnAGQUA2LZ%85p;H%3)c`jES~{!&Td`@29QPx#_en%SR5*>DRMD}+FbEUxx{-H+nP4Z+q<*xfjzi+y(@*Ri2oVtk6SO(!yTeey zn&$bhj3@%DcM%~6g6Jl9gcSq;*l>_;@Xw)jc#m6kr;4h|Q*?od5LM;vH+Vww+6B3L zp3Tm#JXRngNfr^=`j%j=Iwu3bZ40w}lH_bR*}gfnYSg_ zsVfPpl5AgU%X3FuDxZ;^2Jei`F(l!u=KVkUG4 z-C@1|s|AQznOxcUnyO|SUsVSypH66k4P*m%3+9q_U31dw*vI=|B^-DIPtkM(;Gct< P00000NkvXXu0mjf&P;}X literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-005.png b/assets/pango/sprites/po-005.png new file mode 100644 index 0000000000000000000000000000000000000000..81920aa4b6fb94c9d59077bc7d13a7b8cad188d2 GIT binary patch literal 343 zcmV-d0jU0oP)Px$5lKWrR5*>5RKc|aAqbq=myYZSYJ#4ui5wD*y14#7mtCGP1A-6>q*!#LR9N-Z z1+1Wm1OU8lrw3>eNI@65RtJOrj9Q>Ywq)*30zeFmT?zL2*_6?UxgNCIPH=aMNZd;J zFOHXJX8m7Pr!#lwpEDrY$=tn{ek90LNBgmaG5v^W-}o|ts`nCBg&n9$AZcSmYv-6n z4<>uuk>HeFN{|J&Oh$D8xWX*L>~Z>*GA7@vT}B4?`x{tEK96b0;7izT5t*`3Rahx` z7a9?V;R!Qntyy*imO-K_MI>UJ|0H;d=}HB$TOLhq>`vPe(P)Px#_(?=TR5*>DRM8cKFbF*Qx)D3UOt4dCq90X^Bx>6GiHUFrc#%*bBGK=yc@0S@ z3*N(N;~}V$C`&%SYQ8t6OUWARP8l4GQ|SsjHr%GIC<~@)bq;qJTKEO-PU8?KFgw~& z)&DCXq9k*7S|+HfdoqBD4+{G7BVu1;W13gcE8Kxp>9K`cekRk`Lxa+j2aFk{hg&Jk zqCWt>jueq|;x-E+LYO5`ShZ3ZiZAHk7#mW{N` QzW@LL07*qoM6N<$g7c_@^#A|> literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-007.png b/assets/pango/sprites/po-007.png new file mode 100644 index 0000000000000000000000000000000000000000..a9054c7e42f897096e6944734193376902ca9444 GIT binary patch literal 361 zcmV-v0ha!WP)Px$BS}O-R5*=|RM8d0AP5V6w~;%+Ot2F(aUZRoBJDLl^g%IQ1^{V%OR$jTKr{q7z>Ql8~W{3vh!G}vJee#Bx zftevymbbC&MlLOw88I_db<0&%pgGWd^uG~gCHPx$7)eAyR5*=|RMD}-AP8JC*O5J8P0*7y@jeV5DxPm=k`aX!781e>W;RtNW;WlG zrZ|Bb2ta>DSb^2T!_G)5WUQaNA<|hWb2C51xTWG(%eQiPAHTYK+ z092KLL<0a2kka1A{NoP zI$nV%k-ZDkrr&xiK~*WX(Ru~vNmSMCd=Q-a?;Y?AGLivi9(6Lat%&FDlkp74jKG8a znMOHdTuYErK=0Z_x&~IdfVWTi)qs3vs8*T?9JTOptgWRazJ7r3?#cZ!PWCW4q5AKHP=-tR_H2?qr07*qoM6N<$f?FewQvd(} literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-009.png b/assets/pango/sprites/po-009.png new file mode 100644 index 0000000000000000000000000000000000000000..ea7e5c247c6169e237ba643f676ec16b9919806e GIT binary patch literal 279 zcmV+y0qFjTP)Px#(Md!>R5*>@lu-`DAPhqd>W#QldZutE&Xj$uVWD*?llC=Bl?t*S76JeOrfQ~Y zSk&M@ehh$_s#(GOJNc7C0l>;ygVa918vuyNd)Vp{!2PvWY!iBK#O7QV01GmZD<4&S zFNPJYT*ofYkg-+P$d)&bBb d8^0sW`vOi-i@)}@8}9%B002ovPDHLkV1j1Ba)|%{ literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-010.png b/assets/pango/sprites/po-010.png new file mode 100644 index 0000000000000000000000000000000000000000..8dd9d1ca1b82e814a1db58e421e3e9977d0c149c GIT binary patch literal 295 zcmV+?0oeYDP)Px#;Ymb6R5*>bRND>2APjS=Zj_y1CbCmz;y!3-pgLuO<=04d1Y z+Y`bHLjltA&0>!o073}Gx(Y~v%qJ0OWXT(<}tVpA=x|(JysVrxV8~_yi zjer-qgOP!MZQ~ozsB>0|w>}j3vIdu3c#38h-jcn@Wf4W@=U_BuC71)cb8}d)`I`&S zwP$GZx17=$x%QOZ<=7s7A7x t?eBA`@96J)U!H$2NI@3?Kj`CG&JXE3fKN-Q0wDkZ002ovPDHLkV1nz(cJcrK literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-011.png b/assets/pango/sprites/po-011.png new file mode 100644 index 0000000000000000000000000000000000000000..b79b1184daa861bbac1d6d2c60ebb2be5e66bab5 GIT binary patch literal 358 zcmV-s0h#`ZP)Px$AW1|)R5*=|RKXF%AP8J;t|NPbnxH3Z;vNP?qnGTAGcmfbiwfZYFK_^m!nZZ8 z*1x}}02yY3z|4@sJqSH12vBY0RY_OCMF*i&Ajy=%;4-U;V4q#KAf2pu8Flzx@(7Tj zq86Z3Pz`mB99bn>TeIA(U?#6P7Ll1JTdLZ!i}bmOygSAWGlRQ>hyZ{-Z;6q4MOWb3 zypzbyR>%hccaQx**u2OwI+)p+*#nI~gYxEo7P1NepsNUvMO0P5>{tW1s^<~EyYyr) zTiGg(06Q{9eS+-F?2`o&Ay;*&^%nfPiN4IoP0SA}9@3_86ZtW~3Hfc@P9c-c>~zc& zPy2X`S`Av@Pip2OR#0>z-87k174WhrwuZCEap7%{A1sUDU_>eUB>(^b07*qoM6N<$ Ef}OFLX#fBK literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-012.png b/assets/pango/sprites/po-012.png new file mode 100644 index 0000000000000000000000000000000000000000..777b9486dd5a607e893997af0be52cb87e132699 GIT binary patch literal 338 zcmV-Y0j>UtP)Px$3`s;mR5*=|RND>2FbFfJ+bBE1Ot4dC;yxN$^6XivTBWfufk0Tm3w*VnA%y_| zloz?^O;pH{Y-RvJTLF{`t+tIE_-w<}GHuRi4xSVuZ-G9$HZB5GhZI5!0Jtbn>d65C zz<2eNnE~v{8I!<8?xFgmCDK=Ce~1eZQ787dz!)|12ZlxpF!R$;L&QO8?I1@ufkfyv zh0*>^rmF>xk#Z^{!sR63s!k$ZId^3=GlHwS>vf6R`+k_&JGB4I?BVW1B9zE`n1NdK zxJW?jgAgGR@@99{k*hQ4s*qBd2vTcapBpKUvC|yL(I*H0kVm{Wl9|zIs4V9}CdYRc k$#UHpP+fQCYRt#pAF=0=7#2ZT+5i9m07*qoM6N<$f}d%QUjP6A literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/po-013.png b/assets/pango/sprites/po-013.png new file mode 100644 index 0000000000000000000000000000000000000000..50a17a28c854d87b7503a5602d1219a4a4d82ee1 GIT binary patch literal 353 zcmV-n0iOPeP)Px$8%ab#R5*>5RN<|LFbF)18?h741Uq#m-j7N}TK#;Nn3R@-D@A|;XLx)f;v(We zUpvFAKUH$94;BD`nL$LL%`Q^Acw3EFAtJYc6}zxHxWUY*9-3Jwn*xZ4qcKQWyKHVZ znQs`u8e?A`d_iLXXA!fC|B$U6w8bKyt149W(axN$ZUl&EBCUKMpV8e%cB1Kl1~ve2 zRd?-mn6jGPg3Xya(E+%tx_9qxFL1VY@Ia^YfQVcT5As(;l2M(I zmJxT(nJXS&q4vRp3fi;rq@P)Px#_(?=TR5*>LR6!C0AqWed{>VS!o8V9Tru5L!MReWmG$$P-guo~WU;*(EazFs! z^)%s<=vM``=$b%_eUr-pJA9k$?mc%VxFE@uyMiXW!p6u(32>S?c#u`>;O+td?_4#) z%pf8liCQb%y`LNY9^)+m$*yKNTWB|4RF$gAJFFBC0IU=M82Zc*k#2rFbViz?9_tA- z=OZQ$WK&sHr&X60aQCUY*|}V?rKqG6WagN>Bbi!Q^|>RR9yc|s#{iHA@Y5!y(Q+b) zUR><~ld|(aSQxoN_UN(&cNZ&VX)4Xw*1reyf7m_@nh`#>6JxSUj*sr*AJ%`efr07f QoB#j-07*qoM6N<$g1A+NT>t<8 literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-000.png b/assets/pango/sprites/px-000.png new file mode 100644 index 0000000000000000000000000000000000000000..5eded032a41093f28e57e9be7b93ff90b36f7618 GIT binary patch literal 287 zcmV+)0pR|LP)Px#*-1n}R5*>Tl*2p0m8_Dv81 zF?oX2>&=TC3^!{lC7_GNW!+js zw$zC{gaUn3AyU9<#4Wt{x#IO)+rn2J+;-vI10YNxvD1)85ChPx#+DSw~R5*>LRNDcAAPl^?lJ3-*x)aWHeiZRYg4i~H;61_-1pX}31wr;7&F=OA zU{Qd<%tW*9Q;0z{cPL$Bm*CdHs-d`@L=W`B+}Jt^R=%4cBIgeRGjb-fq=+uMWG$5o z0H1g&(JT-3R^VyH4Bq=a;MWeOT{!nt007ARIxR`F&!7@*kGeXY+Ql`>&UGeI zuh+f`(LXYhYyx_|%w3&zDkHLthE_CIHq-PP!~b9dh)IZEM8HEbFqlLSYWu~)5y!8w m*cn#+4ULcwiX>6MEZ_t54QsZo4BJWo0000Px#=}AOER5*>LR9g;%FbuQNZZs~-oywWKkG3>TQZ}(jNDyM@Q8$D!L9*Wj>3!Wl z-lItfm<|jWW|=+chLDzR%A5ha&sMV8{{+FpB^V{PG4*3x@g(-!4$)Td$d&}A*JlLQ z?oT3S_G0q0h&uR2vI4IwSjmc!WST+G%rh3&eX&IkE(5H$CNKbKpSAE>6dQmm8!ara zQ(`7|0|2S@^$SUh>6pKzJcig9$-0H?Fo8Neg0yjf86xRKve)~7z;01Iis1GH4eY~oYG+6)u002ovPDHLkV1g@O BezyPs literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-003.png b/assets/pango/sprites/px-003.png new file mode 100644 index 0000000000000000000000000000000000000000..d4fdb71cd850b4c8bf369f77307183387775c580 GIT binary patch literal 292 zcmV+<0o(qGP)Px#-bqA3R5*>DRKXF$APkdw-6-tTnYt61NqsaBfq}H~C&8R#$ruO=LGm?0_PLTj z|6@o9m;wwKjw<({1tFcSsXPLfoGW?wK0z>Z3Pz4L8b5BOC-Jp6(G}c^DS>&tN8qtO ziCDQGroI<(4z5UM;JJd?STPa}d%!&k8|4B)GY1Cf!R@>ZHkjAPr*wL(3YOlxU?v~{ zatrFdD+Svz(j=e1)iiL@{6UdaZcFS#s^fjpAV^0uB}jVb?>a-ZfW=?8kvoChb}0}P q>Gd|gwl!(Uc!dPwcvTwo*BXZa0000Px#-AP12R5*>5RND>2APlpsZgk!0X6jCKCf`S2AsBj%gaigVCPWY^1SzfwGUF$< z&wWVHc^Gk?IdfD$4qOBPK)G=Mz*1+b)QtUFEZGNu$&m?yr6v0$hU|zv7xRO6002ovPDHLkV1j_CckBQF literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-005.png b/assets/pango/sprites/px-005.png new file mode 100644 index 0000000000000000000000000000000000000000..8c8fde498f4208125d8d916256716297c2533130 GIT binary patch literal 316 zcmV-C0mJ@@P)Px#_DMuRR5*>5RKXF$APkdoY*cpYOxcOdq&^zJ7(-g$aR)Y%g)tD$1j()m()+9L zJC99*>Soxv$0W;sZ)gAjKPx#>PbXFR5*>5l-(JFFbGA@?dL{1Q#(_4iZjuNHU=VzmL~-cT=FLpGtB(Ika~ah zDJxLx7*><<-=16guD^r>0H8EK0Gn5LEk71>@&O(rSO}Ffe9jxd8JMY9lE4|ZkPs}4 zAf#DmNnnFD@Xkc<{ZPLPy!RY@Z3+r*Dfv%aJBhuD+I?CejMA;Eq;UZ=vp%BIer&Z+ zPx$4@pEpR5*==RNJk^APlpsv(aUz&eWagOuiow2;osnl?uUmIY2lF5<&X4x0}uO z3GzaK86$b;baK$n1rS_1W!w-X=4rdzlfXjTA3}x%Ek9(0zAW=!*FO?VL)T3ddKcur zLL7qZng~`k$uUVmgOlGePoWnXinS7E6`s7W|NiTaD|=f)i*zUOPGeT?e{#S(t@X9& z@w{KS8HeU!M6*?uNp}~#_JT(F>Q^U}hL(Yv%ii5MaRXA%{Ay&NH|RtT_D>a7Q0o3K zx=|y^ujrnPx#`$YWLZ3jNfRU+6J!(< zP6l8Ia_dIO2rz4e#|;Vui6Bq{Rr{kH016~%`1<(}8v!$tw1nyx0p}g9J543XC98rY zpsOE~n3-6gSkh*{6pXtdU8a1#Imvq)2o?thfY;^|zxS!I)Pt(*C~5Zd-vUiV?|c~j zCt9OL5MYVjZ^m-l=$s9}9aB{~MwYaI6TA6kXQCiIzc4L9f$lO}V{ehaw)Z9W;I8$%w4c}?FU_+w T{q03^00000NkvXXu0mjf^P+@& literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-009.png b/assets/pango/sprites/px-009.png new file mode 100644 index 0000000000000000000000000000000000000000..c2307dba1dd0a2479aaf0f2911f4342426c153f5 GIT binary patch literal 303 zcmV+~0nq-5P)Px#=}AOER5*=|R9muzFbGSWv!m<&S5ViK`(ag{R(?9A2?Xc>0MImiG>c~>lH`i) z@Xisa1Su5CjAtWBOSG3r4#4NQfCM4Px#-$_J4R5*>LQ&ASfAP7tPb|ZVIo@sZoXJQ`}hah#Y<4+(VLrQ*C literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-011.png b/assets/pango/sprites/px-011.png new file mode 100644 index 0000000000000000000000000000000000000000..5c18daf2c002e5d4a0314e74c3ab564b44694260 GIT binary patch literal 317 zcmV-D0mA-?P)Px#_en%SR5*>Llj{w`FbIV2qHbjGl$pAdJJa)E|YkOx5X&MrWd@yt@@Jjy~#GFM=kg<`+a6Q~A<3-J`-SFt~I@mNog;kc0I zo4{sg>&qC8ARvGw2y%NWTs6G5QHD%%ZteRMi}!2BJP6x P00000NkvXXu0mjfk*$NG literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-012.png b/assets/pango/sprites/px-012.png new file mode 100644 index 0000000000000000000000000000000000000000..ff16dba3b5ef21e1155ac6c10dfdc8da9f416cde GIT binary patch literal 327 zcmV-N0l5B&P)Px$0ZBwbR5*>5l+lrdKnO%%Z*4}J)5UZblg8Bdq3poujI~r*Is#2YqcAXIk9^rQ zbKZdnsh$VH5jc83WH6$x?F?O+1N0z~=(XYYEH24$vEh6!zFFc^at3(M`cCNcrXQ=H z2E@JM89NVV&?AztjnrZvRp58O?MgqZ0A9D=FKo8Tc1*8h5!Gh^Eg6amxqmoiteI#3 zDNK7i^aC$5C=#7dpKk#0;<^((KyR1in?kRkWo-W1urlpwk;eEy6@u+PI9O=c=d*5Q zwxc^Qey*PW2}K1j20=)eX8`S=pj7uFW1k?7{U2@2T!lk#Y1biNT*(T9e#?q4&t=0g Zhkqu-)Ey&vXKVlf002ovPDHLkV1n*jikbib literal 0 HcmV?d00001 diff --git a/assets/pango/sprites/px-013.png b/assets/pango/sprites/px-013.png new file mode 100644 index 0000000000000000000000000000000000000000..768fb507a3abde86d3cf7cd9b8e9b0d3290f12e6 GIT binary patch literal 331 zcmV-R0kr;!P)Px$1xZ9fR5*>5RNE26AP8JC*HP@rnyx2P)7}Tc0BVxknM?#>d58iD1exWm^Z3k^ z1St>@9Q3vpi0FUZ2#zMt2nC`nIfAsH*1f_`Xo2Twpw;a^)2|&}7OM{p1HrU}Cs+dk zU|Xbt5G%_La{@s7`y!t};8UYjLxjWfHY%$zz0yNxTg1cHsc8rO+xfz)!Fy4emr z$Ps3Hc3LDEO6Z5WD#BB1Z(P&z3}h0?qKq?rcNc(gAG*P4IT_c0f^=K|aR$JnT|(U* z!3*a=QumkvL9S{Ka3$!2(m?JUyPU|Zf#!Iv&(u9ho_EX839u1RLtlScKUaS2Px#?ny*JR5*>5R9g%`%G@(JXOebGB-d2olbkK=O=He~ z8Y}f}uho}#-i=*-pcVGr1AR73lW8;zY`)49b+MZ2F7P)c?59S$(2Do~00006G?Wc0*F)$pG!s>%RsUjPaSAw{$U?0yJlo`bi8@6H}l)s{0L zgwS1Of41Xa+Gqvl9BgMUS>)~Xtbx5~J64K8HRRmewjy)S9)aej7w z%%e#zg_KftY$i8_DNWUBbGa*A>8h@_kcT3aq3UK!c`8zws>ZUCm!g!VYG@Nkv{Flw z=2Y4)-x%d%?Hh=4UT;RO*UGYV|L z=%$=J51epM_iBX&HJ@|nEC`KZh5CLmw!V4ly z1{!R@=%$=J51fpswi1O5CeDm%XAuk!(pgb$G;3iG7?0Cfih($ literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-001.png b/assets/pango/tiles/pa-tile-001.png new file mode 100644 index 0000000000000000000000000000000000000000..e16af24ce1063e0f0a570ea71cb0b3ace00dbb21 GIT binary patch literal 1005 zcmds#y=s(E5QblWimJMYha9u`OA(fyI6@%+Vf$#-Y-w{?sE2T!(Fd~Up7 z9v$l9>fs;h=9l5+@r*~4TnZ_r>bsfT6s9y)m(As_aHXrd+d>|SP=>09E#;|5WvUv> zN?wXmma3smATdfWMQ)qpW?`0Q)r+~iTeziLH4n-j7GW7yO^a?%i?mFuX34OZMOl_r zlSTxrp$RXDu$fh01I9G%)Op~9d%9OEWHI*$&+ux&@Sq&&nO?0QT69NwmRH+>TiAmi z7=U6Vq6rbOh9_;##+3@ zJlw(_{J;PdBN5VI!5W(If(V;|1{*M@X{XKur#dsNL?Mg0S4T+x1Wv+4N`{0+O2k4A z7?0CfiQL=5+7V%%e#zg_KhD-Arx@Q<|#F=5klK(pBAUArD0;L)F8U@>HZURgGmO zFGVR!)zBu87^RmYx7Bg8FiW%Q#oXO3+|sR@2W1b7uneoFMYpF#TBcRAWZ281EX%4% zBLddYgcn5E%qp+}W14pAJaEE2-K!O{n0th0c(q`7P>%FWuhtJOx}!YHtL?xo?7i$%6ki+KTEc1adwS$0E;Q~_Z+D=n-P*=o7Jy|uR`X?t-nTk{>={0(pOfAaMHhR?0H z>;0+De?R&w4Zf^i9nN_)$)%7|s=k}aO<_t?b=h3*3Rk+SyDj9Q2xX{x*ixQ~RHmx2 ztmLIAWvLq41QMh4Qsj1X+$_w}ta>qbcMG?4tL8!3!y+uhs%g>fX_1y`)hrqIvM9^4 zYSM^+H8kM`5jL|5Y`~bNojMPka8LJYg)HVC;Tc{n7#@@(J=3f8LyPVx&+=+Ja0`3z z0|QWuL^L4+*3g6(MA!^8*nlxjJ9Qp78BuK|3R%oOGpe0MFgz${MYYkyLJk;@(^!j_ zn1@@~gC7`xVkANuELcMmUJzk3&|m|`H0{)R;8bUZl_+E}_v#4ApTJ3&NXd}UNQqd; z0poERYw;30V979kU;v7d2x+ij4NZ7Kgv~&M4H(n3Q|E!h@9xRw_z$0}ckAVbhX3;% zUBCa1$mfgM%fsETuTOTzFYXOjccz~-AO09jK7SibPRAD?pIu&PXZmpXbELD!ccou% UMmrzI4=x$iV!oW6Pft$&0jsY(9RL6T literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-004.png b/assets/pango/tiles/pa-tile-004.png new file mode 100644 index 0000000000000000000000000000000000000000..dbe2cc0fffce656e278b599a706bd79234a101ff GIT binary patch literal 984 zcmds#y-M6s5XFz$h#=Z1B9bN`Ha5-^L{`=XS!7pIXd{U80!d+=q!DY2DaA?)>%2g^ zLDD9jmF5M~jz8xyyr6&XotZP|V=oWK!^MU51xbse{lSFq^4s719RIhrm!^DXzD$O@ zdU)LUC(T|R9vtoQXp&1IrBr=4lbgbnrs}e}+!d~LRd-v+LlMeQ^{}Np6{$>BV_C^d zQOZ&^v7~f+?YLQ(rCIf2?(Py|+tVT~)2dlA>}64wW!0n+ z0c&W&3nFZ071)3=O*?fSIN_e|)e2e6J;F1*S};5)M|!4L>xUNIQJ&@1cHkEF;0Fev z7>Q^?1gxP6FNm-iXs`iens(|ua5AFWN))n~duCKSi(q(A&WdWIiG>_69;dMuFEJ0d zum?Xd0L4gzG+3~PCcGfRW}v|ajA`1b^T4Uj3@cH{V(!%ul0SiyFp-iWp^*}?kORi! zG}huJcEFNh{J;PdBN5VI!5W(If(V;|1{*M@X{XKuhu_`i^!SC(pRbegl!o_tKL5YC uMda#e@absn`t0Y2zrUC7SMGml?&FE{biQ+Q_v86H9UAS82M@ca-(CTC&^X@! literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-005.png b/assets/pango/tiles/pa-tile-005.png new file mode 100644 index 0000000000000000000000000000000000000000..20e4f663523cd80b3e58bf76e6808d687e2d665b GIT binary patch literal 1011 zcmds$KZ_Gk5X2`ZNC+MVIYC6wN^C4-YYWeWTtGrdf`XO*SZ%Ddu+&(IwZ(RV{o$t& zi$%1!!rnqVD@)hk?Z@yz^4Pb#voo{Fo8@A*J32p-v^zhTthmR=pWz9UE`^j*b!;X#g(*$dX>++NTDBt7MRk;Cd9@w5g+2Iz z0VqZynh*hNXu=C3Oa>Zk!04u&JP(|VsJ0S?3?|NuYG)A)57JptZ8U3P4;YWrSc{jK zhg;Z#9~gjQBtjZ2SVI$D5MeUVU;{=s<>YzbRPPKcQOIE8>J_qo7S4uQDLZ6oY>Bn7 z2aLyQti?<0fF;fNfdME+BBa5BH8kM`5heo-Hehs9PM!x2zq`xpyFa678n=;*=A!T93z{ov-Mz0b36+fR?b?rfKjUM)YKIk`D| a>NS7lAHTUL{CH<;KIYLRmqJRZ`fesSg(*$dWplYJTc!mME!@(rng?YMi?9rMOvm+vt-!I zqAbg*Nh1Q*(1aI6*vu-h0b`nW>O645J>9DnvY2~>XLz+>cuU-^96n{Ll& z_&?9B-w$3Q^6F%?Hb49H?fW+aT^ug2ec!+EcYp8alk0~|N7ruej&HtRES{ZtzVl+` j?x`D_i(kWcuYUabtgVm3)Ax?P%t6p(ygfQt-+lNG0d7dU literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-007.png b/assets/pango/tiles/pa-tile-007.png new file mode 100644 index 0000000000000000000000000000000000000000..3a5038eb186cd12b734c1ee242001c08ca1cb2fd GIT binary patch literal 992 zcmds$F-ja@5QSe9LLAe$9kkY(95DXfIRTtEw}6oL>5*jY>yi0Q3U zdVoQ+^agr?w3Bb=82+&PGylxIdGF2c++NTlZdN8rOg;SCzyK5@5z=768k+Ee2$O*Z8!);lC(i?i-`&pa=$Y&3bUL0<@H)@e ze`kLXxjhj%e^ym=6vkS;b5UNeSKO|r~jZg;(PJrZ|V&H zZ{5By=5uOwv@oZgpWQ#w=~u&tlX)IZaw(*gs@=`xrZA3!3ZW`<`5Bkoa@As6|QtuS6j$K5z0_? zv!y&0sZ3R4S;o9yM40!4C{TF%lsS7ObHOFNiQ1Xs`jJn{x6zaH@BPl_+E|arFw>KMQBW ztdt$HG`7TA*aODnG}huJcEFNm{J;PdBN5VI!5W(If(Vm=1{*NCDJRbZhu__y@ulxv zU$2iA#}xdZ=fcK~r-(c`9Lz57een9;%k4V4MAo_#WzeanAN8qO~c*5@wY`UlfhOmP4J literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-011.png b/assets/pango/tiles/pa-tile-011.png new file mode 100644 index 0000000000000000000000000000000000000000..b06a00e8695f2107ad02d6a8d62e4ee82cb763ce GIT binary patch literal 1022 zcmds#ziO0G5XDcvz!E}&+KE~$7LrC0rnOkZhGZdGmfa??cV^C8jRkArD0;L)C^Y z<*7(zsv4G+ycDG@Rf9Hx#3;QKxt$+33$rwdL(4sra zv%H!exP?9VfdME+BAO5ZYiPm?B5Vd4Y`~bNojMPkjHqTM3R%oOGpad@V0ci@ifTp^ z3prpsPGc=zVjgZ`4}M?(ijfFuuwV^MctM2CK!Xh!)3j6Pfm1CrtVAJ;xmSyj{0W?d ziIfZpjg*Lm955cIu@*0}1C|Wq2L_-RiI4^h*3g6(MA!^8*nlxjJ9Qp7{O)ed?)~6% z{CGN^(eQts!@p17Bl6B@cz1sJ)&0fd_MqDsEMC4)|5CrsPJiuP?;Wq*+V5XI=)eBb q^&WJ+qi*nK@cHf8SAG99IeGVI<>QC>@8#^_2{;<<<{yivPfm!p2xSKDj@`3(3p9GjryA?2FCO`a=IwU(&+xZa3z8{^alUDgG~C zKR4ks_hh`js>7qp-=*Hm&5i9f9!+v7q?D@N&E%#qrKwukTXLvQi@Sq&&nO;plwCIlV zEU#tBdS@6LKbt+jB3sz7#@_fqMFgf zLJk;@(^!j_n1@@~gC7`xVkANuELcMmUJzk3&|m|`H0{)R;8e>DD^bW|?$shBe*!0A zA|*pYBPC)X2aLyQti?<0fF;BDfdME+BBa5BH8kM`5jF!2HegKCPMrr1zq_T${cn7} z?ulcF}$$*=iZq)b3XE9V>apcPV^-8r#D7(zDE!K*1G&Z ze{rzj)44yNtZVP<$$jb2(~X6jDmn?q+gRn9@|OY%X_&D_zyPE##pHWvJS) zr92g>OjX0Ol9!^CrE1V7kQk+xBDaI%W?`0Q)fRJiw{T0hYCI@=ScGL*HCl9gTBK!K zHI@u}S(Ig2HPVQHH8kM`5jL|5Y`~bNojMPka8LJY3R%oO!ZW;@V0ciD^h~d&A6j%r zd6rkR1GlgTKQI8rNJJANU=2-pL4?gfgAEwdv{UDSlM&UdL?Mg0XGS$=5eyH?Sy9bs zVj%~N$7!s^OU%P9?7_4Hm4S2``AS8ECKpW14pAJaDRIhLtE}G52Z_l0Siy zFp-iWp^*}?kORi!G}huJcEFNh{J;PdBN5VI!5W(If(V;|1{*M@X{XKuhu__~#qHmG zes0fa3mX2fi6+X*!;b_SQEa{{wub BN@f56 literal 0 HcmV?d00001 diff --git a/assets/pango/tiles/pa-tile-014.png b/assets/pango/tiles/pa-tile-014.png new file mode 100644 index 0000000000000000000000000000000000000000..75362619c1f5b71dcfad4b268608564e9675eb25 GIT binary patch literal 1031 zcmds#y=v4^5XDan!6mXP*r-^DMM$&NG$IJP?2@d=5_W?{u&{9o!A31aw6TjyQUx-d zAhuiN305OwF=%aN8(+XGGX8QO!wbouduQg%`Pl7^@mhEJ^0K7vXnio@d;a9Fv&jF| z-nl8Cg{PCXzGmOA{FY9=-MG6s9erK?)Eg*+6Y3{@Mp zl&2zlZ+G6hR7H;WQjR$29i?9rYh94a2L_-RiD*Iutf2`nh_D%GumNM5cIrHEGNPK5C}c7B%&6upg5g0qE2paG*TiKa=>_;##+3@4p=gb9~gjQBtjZ2SVI$D5MeXWU<1Z9?bLbT@Vi@?ZXNOY z`D`+t((r$tdw-sPK;&>VxV?FC@9~dMU0v-g&S&#GJKw%sTe{GDxxTylX7AzGv+tj5 zpSgZx+PSoP`o;du`J-{%+By1oxbGeDl$H~Kf?>j%e^ym=6vk>%4F1CIKLpNJ6;}4`JOub>&)}N z*FQPqGxvBpTGGz<3%{fzuUBrb4tX@mrI1ppb~lro!jz_JWplYJT+P2;|B(y7>SSu3)awt7ev?$G}wSKO*?fSIQ;GwXKVX> zem$?voadTsT`}5k1t-Zf@bF0 7E 7F 09 01 F7 | Enables General MIDI Mode. All channels are fully reset, and set to use Bank 0, Program 0 ("Acoustic Grand Piano"), and Channel 10 only set to Percussion mode. | +| GM Disable |
F0 7E 7F 09 00 F7
| Disables General MIDI Mode. All channels are fully reset, and set to use Bank 1. Channels 1 through 9 are set to use Programs 0 through 8, respectively. Channel 10 is set to Percussion mode, and set to Bank 0 (General MIDI Compatible Drum kit). Channel 11 is set to Percussion mode, and set to Bank 1 (Donut Swadge Drum kit). Channels 12 through 16 **are currently** set to Bank 0, Program 0 ("Acoustic Grand Piano"), but **note** that this may change in the future and is not a guarantee. | diff --git a/emulator/resources/SwadgeEmulator.desktop b/emulator/resources/SwadgeEmulator.desktop new file mode 100644 index 000000000..1a7481498 --- /dev/null +++ b/emulator/resources/SwadgeEmulator.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=Swadge Emulator +GenericName=Swadge Emulator +Comment=Emulate the MAGFest Swadge +Exec=$ROOT/bin/swadge_emulator %U +Terminal=false +Type=Application +StartupNotify=true +MimeType=audio/midi;audio/x-midi; +Icon=$ROOT/share/icons/SwadgeEmulator/icon.png +Categories=Game;Emulator;AudioVideo;Audio;Midi;Player;Development; diff --git a/emulator/resources/install.sh b/emulator/resources/install.sh new file mode 100755 index 000000000..cdbc336ea --- /dev/null +++ b/emulator/resources/install.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -e +# Uncomment this to print all script lines +# set -x + +if [ -e swadge_emulator ]; then + BIN_SRC="swadge_emulator" +elif [ -e ../../swadge_emulator ]; then + BIN_SRC="../../swadge_emulator" +else + echo "Error: install file 'swadge_emulator' not found! Are you running this script from the correct directory?" + exit 1 +fi + +if [ -e SwadgeEmulator.desktop ]; then + DESKTOP_SRC="SwadgeEmulator.desktop" +elif [ -e emulator/resources/SwadgeEmulator.desktop ]; then + DESKTOP_SRC="emulator/resources/SwadgeEmulator.desktop" +else + echo "Error: install file 'SwadgeEmulator.desktop' not found! Are you running this script from the correct directory?" + exit 1 +fi + +if [ -e icon.png ]; then + ICON_SRC="icon.png" +elif [ -e emulator/resources/icon.png ]; then + ICON_SRC="emulator/resources/icon.png" +else + echo "Error: install file 'ico.png' not found! Are you running this script from the correct directory?" +fi + +INSTALL_DIR="${HOME}/.local" + +if [ "$#" -gt "0" ]; then + INSTALL_DIR="${1}" +fi + +echo "Installing the Swadge Emulator to: ${INSTALL_DIR}" + +mkdir -p "${INSTALL_DIR}/bin" "${INSTALL_DIR}/share/icons/SwadgeEmulator" "${INSTALL_DIR}/share/applications" + +cp "${BIN_SRC}" "${INSTALL_DIR}/bin/swadge_emulator" +cp "${ICON_SRC}" "${INSTALL_DIR}/share/icons/SwadgeEmulator/icon.png" +sed "s,\$ROOT,${INSTALL_DIR},g" "${DESKTOP_SRC}" > "${INSTALL_DIR}/share/applications/SwadgeEmulator.desktop" + +if ! [[ ":$PATH:" == *":$INSTALL_DIR/bin:"* ]]; then + echo "WARNING: ${INSTALL_DIR}/bin is not in your PATH!" + echo " If you want to be able to start the Swadge Emulator from the command-line," + echo " you can run this command to update your PATH for this session," + echo " or add it to a file like ~/.bash_profile:" + echo + echo " export PATH=\"\$PATH:${INSTALL_DIR}/bin\"" + echo +fi + +echo +echo "Installation complete!" diff --git a/emulator/src/extensions/modes/ext_modes.c b/emulator/src/extensions/modes/ext_modes.c index 970b6d0b7..1efd93fbf 100644 --- a/emulator/src/extensions/modes/ext_modes.c +++ b/emulator/src/extensions/modes/ext_modes.c @@ -25,12 +25,13 @@ #include "jukebox.h" #include "keebTest.h" #include "mainMenu.h" -#include "modeTimer.h" #include "mode_2048.h" #include "mode_bigbug.h" #include "mode_credits.h" #include "mode_swadgeHero.h" #include "mode_synth.h" +#include "modeTimer.h" +#include "pango.h" #include "touchTest.h" #include "tunernome.h" #include "ultimateTTT.h" @@ -72,6 +73,7 @@ static swadgeMode_t* allSwadgeModes[] = { &mainMenuMode, &modeCredits, &swadgeHeroMode, + &pangoMode, &synthMode, &t48Mode, &timerMode, diff --git a/emulator/src/idf/midi_device.c b/emulator/src/idf/midi_device.c index 798d45a27..257b7cd1d 100644 --- a/emulator/src/idf/midi_device.c +++ b/emulator/src/idf/midi_device.c @@ -25,6 +25,9 @@ #include #include +// Un-comment to enable printing all received MIDI packets +// #define DEBUG_MIDI_PACKETS + static uint8_t runningStatus = 0; static struct platform_midi_driver* midiDriver = NULL; @@ -66,12 +69,14 @@ bool tud_midi_n_packet_read(uint8_t itf, uint8_t packet[4]) int read = midiDriver ? platform_midi_read(midiDriver, real_packet, sizeof(real_packet)) : 0; if (read > 0) { +#ifdef DEBUG_MIDI_PACKETS printf("Packet: "); for (int i = 0; i < read; i++) { printf("%hhx, ", real_packet[i]); } printf("\n"); +#endif // Normally start reading after the status byte int dataOffset = 1; diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ae3404e99..afcbbc22a 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -12,6 +12,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "display/font.c" "display/shapes.c" "display/wsg.c" + "display/wsgPalette.c" "menu/menu.c" "menu/menuManiaRenderer.c" "menu/menu_utils.c" @@ -22,26 +23,33 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "midi/midiUsb.c" "midi/midiUtil.c" "midi/waveTables.c" + "modes/games/2048/2048_game.c" + "modes/games/2048/2048_menus.c" + "modes/games/2048/mode_2048.c" "modes/games/bigbug/aabb_utils_bigbug.c" "modes/games/bigbug/entity_bigbug.c" "modes/games/bigbug/entityManager_bigbug.c" "modes/games/bigbug/gameData_bigbug.c" "modes/games/bigbug/mode_bigbug.c" + "modes/games/bigbug/pathfinding_bigbug.c" "modes/games/bigbug/soundManager_bigbug.c" "modes/games/bigbug/tilemap_bigbug.c" - "modes/games/bigbug/pathfinding_bigbug.c" "modes/games/swadgeHero/mode_swadgeHero.c" "modes/games/swadgeHero/swadgeHero_game.c" "modes/games/swadgeHero/swadgeHero_menu.c" + "modes/games/pango/paEntity.c" + "modes/games/pango/paEntityManager.c" + "modes/games/pango/paGameData.c" + "modes/games/pango/pango.c" + "modes/games/pango/paSoundManager.c" + "modes/games/pango/paTilemap.c" + "modes/games/pango/paWsgManager.c" "modes/games/ultimateTTT/ultimateTTT.c" "modes/games/ultimateTTT/ultimateTTTgame.c" "modes/games/ultimateTTT/ultimateTTThowTo.c" "modes/games/ultimateTTT/ultimateTTTmarkerSelect.c" "modes/games/ultimateTTT/ultimateTTTp2p.c" "modes/games/ultimateTTT/ultimateTTTresult.c" - "modes/games/2048/2048_game.c" - "modes/games/2048/2048_menus.c" - "modes/games/2048/mode_2048.c" "modes/music/colorchord/colorchord.c" "modes/music/jukebox/jukebox.c" "modes/music/tunernome/tunernome.c" @@ -104,10 +112,11 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./midi" "./modes" "./modes/games" + "./modes/games/2048" "./modes/games/bigbug" + "./modes/games/pango" "./modes/games/swadgeHero" "./modes/games/ultimateTTT" - "./modes/games/2048/" "./modes/music" "./modes/music/colorchord" "./modes/music/jukebox" diff --git a/main/display/wsg.c b/main/display/wsg.c index ba4e12a76..9617cf415 100644 --- a/main/display/wsg.c +++ b/main/display/wsg.c @@ -10,12 +10,6 @@ #include "fill.h" #include "wsg.h" -//============================================================================== -// Function Prototypes -//============================================================================== - -static void rotatePixel(int16_t* x, int16_t* y, int16_t rotateDeg, int16_t width, int16_t height); - //============================================================================== // Functions //============================================================================== @@ -25,12 +19,12 @@ static void rotatePixel(int16_t* x, int16_t* y, int16_t rotateDeg, int16_t width * then reflection over Y axis, then reflection over X axis, then translation * * @param x The x coordinate of the pixel location to transform - * @param y The y coordinate of the pixel location to trasform + * @param y The y coordinate of the pixel location to transform * @param rotateDeg The number of degrees to rotate clockwise, must be 0-359 * @param width The width of the image * @param height The height of the image */ -static void rotatePixel(int16_t* x, int16_t* y, int16_t rotateDeg, int16_t width, int16_t height) +void rotatePixel(int16_t* x, int16_t* y, int16_t rotateDeg, int16_t width, int16_t height) { // This function has been micro optimized by cnlohr on 2022-09-07, using gcc version 8.4.0 (crosstool-NG // esp-2021r2-patch3) @@ -253,7 +247,7 @@ void drawWsg(const wsg_t* wsg, int16_t xOff, int16_t yOff, bool flipLR, bool fli uint32_t dstY = srcY + yOff; // It is too complicated to detect both directions and backoff correctly, so we just do this here. - // It does slow things down a "tiny" bit. People in the future could optimze out this check. + // It does slow things down a "tiny" bit. People in the future could optimize out this check. if (dstY >= TFT_HEIGHT) { continue; @@ -494,4 +488,4 @@ void drawWsgTile(const wsg_t* wsg, int32_t xOff, int32_t yOff) pxDisp += dWidth; pxWsg += wWidth; } -} +} \ No newline at end of file diff --git a/main/display/wsg.h b/main/display/wsg.h index 5e8856693..a1fceabb2 100644 --- a/main/display/wsg.h +++ b/main/display/wsg.h @@ -17,13 +17,16 @@ * * \section wsg_usage Usage * - * There are three ways to draw a WSG to the display each with varying complexity and speed + * There are five ways to draw a WSG to the display each with varying complexity and speed * - drawWsg(): Draw a WSG to the display with transparency, rotation, and flipping over horizontal or vertical axes. * This is the slowest option. * - drawWsgSimple(): Draw a WSG to the display with transparency. This is the medium speed option and should be used if * the WSG is not rotated or flipped. * - drawWsgTile(): Draw a WSG to the display without transparency. Any transparent pixels will be an indeterminate * color. This is the fastest option, and best for background tiles or images. + * - drawWsgSimpleScaled(): Draw a WSG to the display with transparency at a specified scale. Scales are integer + * values, so 2x, 3x, 4x... are the valid options. + * - drawWsgSimpleHalf(): Draw a WSG to the display with transparency at half the original resolution. * * \section wsg_example Example * @@ -45,6 +48,7 @@ #include #include +#include /** * @brief A sprite using paletteColor_t colors that can be drawn to the display @@ -56,6 +60,7 @@ typedef struct uint16_t h; ///< The height of the image } wsg_t; +void rotatePixel(int16_t* x, int16_t* y, int16_t rotateDeg, int16_t width, int16_t height); void drawWsg(const wsg_t* wsg, int16_t xOff, int16_t yOff, bool flipLR, bool flipUD, int16_t rotateDeg); void drawWsgSimple(const wsg_t* wsg, int16_t xOff, int16_t yOff); void drawWsgSimpleScaled(const wsg_t* wsg, int16_t xOff, int16_t yOff, int16_t xScale, int16_t yScale); diff --git a/main/display/wsgPalette.c b/main/display/wsgPalette.c new file mode 100644 index 000000000..9e96ba102 --- /dev/null +++ b/main/display/wsgPalette.c @@ -0,0 +1,404 @@ +/** + * @file wsgPalette.c + * @author Jeremy Stintzcum (jeremy.stintzcum@gmail.com) + * @brief Provides palette swap functionality for Swadge + * @version 1.0.0 + * @date 2024-09-20 + * + * @copyright Copyright (c) 2024 + * + */ + +//============================================================================== +// Includes +//============================================================================== + +#include "wsgPalette.h" +#include "hdw-tft.h" +#include "trigonometry.h" +#include "macros.h" +#include "fill.h" + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Draw a WSG to the display utilizing a palette + * + * @param wsg The WSG to draw to the display + * @param xOff The x offset to draw the WSG at + * @param yOff The y offset to draw the WSG at + * @param palette The new palette used to translate the colors + * @param flipLR true to flip the image across the Y axis + * @param flipUD true to flip the image across the X axis + * @param rotateDeg The number of degrees to rotate clockwise, must be 0-359 + */ +void drawWsgPalette(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette, bool flipLR, bool flipUD, + int16_t rotateDeg) +{ + // This function has been micro optimized by cnlohr on 2022-09-08, using gcc version 8.4.0 (crosstool-NG + // esp-2021r2-patch3) + + if (NULL == wsg->px) + { + return; + } + + if (rotateDeg) + { + SETUP_FOR_TURBO(); + uint32_t wsgw = wsg->w; + uint32_t wsgh = wsg->h; + for (int32_t srcY = 0; srcY < wsgh; srcY++) + { + int32_t usey = srcY; + + // Reflect over X axis? + if (flipUD) + { + usey = wsg->h - 1 - usey; + } + + const paletteColor_t* linein = &wsg->px[usey * wsgw]; + + // Reflect over Y axis? + uint32_t readX = 0; + uint32_t advanceX = 1; + if (flipLR) + { + readX = wsgw - 1; + advanceX = -1; + } + + int32_t localX = 0; + for (int32_t srcX = 0; srcX != wsgw; srcX++) + { + // Draw if not transparent + uint8_t color = palette->newColors[linein[srcX]]; + if (cTransparent != color) + { + uint16_t tx = localX; + uint16_t ty = srcY; + + rotatePixel((int16_t*)&tx, (int16_t*)&ty, rotateDeg, wsgw, wsgh); + tx += xOff; + ty += yOff; + TURBO_SET_PIXEL_BOUNDS(tx, ty, color); + } + localX++; + readX += advanceX; + } + } + } + else + { + // Draw the image's pixels (no rotation or transformation) + uint32_t w = TFT_WIDTH; + paletteColor_t* px = getPxTftFramebuffer(); + + uint16_t wsgw = wsg->w; + uint16_t wsgh = wsg->h; + + int32_t xstart = 0; + int16_t xend = wsgw; + int32_t xinc = 1; + + // Reflect over Y axis? + if (flipLR) + { + xstart = wsgw - 1; + xend = -1; + xinc = -1; + } + + if (xOff < 0) + { + if (xinc > 0) + { + xstart -= xOff; + if (xstart >= xend) + { + return; + } + } + else + { + xstart += xOff; + if (xend >= xstart) + { + return; + } + } + xOff = 0; + } + + if (xOff + wsgw > w) + { + int32_t peelBack = (xOff + wsgw) - w; + if (xinc > 0) + { + xend -= peelBack; + if (xstart >= xend) + { + return; + } + } + else + { + xend += peelBack; + if (xend >= xstart) + { + return; + } + } + } + + for (int16_t srcY = 0; srcY < wsgh; srcY++) + { + int32_t usey = srcY; + + // Reflect over X axis? + if (flipUD) + { + usey = wsgh - 1 - usey; + } + + const paletteColor_t* linein = &wsg->px[usey * wsgw]; + + // Transform this pixel's draw location as necessary + uint32_t dstY = srcY + yOff; + + // It is too complicated to detect both directions and backoff correctly, so we just do this here. + // It does slow things down a "tiny" bit. People in the future could optimize out this check. + if (dstY >= TFT_HEIGHT) + { + continue; + } + + int32_t lineOffset = dstY * w; + int32_t dstx = xOff + lineOffset; + + for (int32_t srcX = xstart; srcX != xend; srcX += xinc) + { + // Get colors from remap + uint8_t color = palette->newColors[linein[srcX]]; + + // Draw if not transparent + if (cTransparent != color) + { + px[dstx] = color; + } + dstx++; + } + } + } +} + +/** + * @brief Draw a WSG to the display without flipping or rotation + * + * @param wsg The WSG to draw to the display + * @param xOff The x offset to draw the WSG at + * @param yOff The y offset to draw the WSG at + * @param palette Color Map to use + */ +void drawWsgPaletteSimple(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette) +{ + // This function has been micro optimized by cnlohr on 2022-09-07, using gcc version 8.4.0 (crosstool-NG + // esp-2021r2-patch3) + + if (NULL == wsg->px) + { + return; + } + + // Only draw in bounds + int dWidth = TFT_WIDTH; + int wWidth = wsg->w; + int xMin = CLAMP(xOff, 0, dWidth); + int xMax = CLAMP(xOff + wWidth, 0, dWidth); + int yMin = CLAMP(yOff, 0, TFT_HEIGHT); + int yMax = CLAMP(yOff + wsg->h, 0, TFT_HEIGHT); + paletteColor_t* px = getPxTftFramebuffer(); + int numX = xMax - xMin; + int wsgY = (yMin - yOff); + int wsgX = (xMin - xOff); + paletteColor_t* lineout = &px[(yMin * dWidth) + xMin]; + const paletteColor_t* linein = &wsg->px[wsgY * wWidth + wsgX]; + + // Draw each pixel + for (int y = yMin; y < yMax; y++) + { + for (int x = 0; x < numX; x++) + { + uint8_t color = palette->newColors[linein[x]]; + if (color != cTransparent) + { + lineout[x] = color; + } + } + lineout += dWidth; + linein += wWidth; + wsgY++; + } +} + +/** + * @brief Draw a WSG to the display without flipping or rotation + * + * @param wsg The WSG to draw to the display + * @param xOff The x offset to draw the WSG at + * @param yOff The y offset to draw the WSG at + * @param palette Color Map to use + * @param xScale The amount to scale the image horizontally + * @param yScale The amount to scale the image vertically + */ +void drawWsgPaletteSimpleScaled(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette, int16_t xScale, + int16_t yScale) +{ + // This function has been micro optimized by cnlohr on 2022-09-07, using gcc version 8.4.0 (crosstool-NG + // esp-2021r2-patch3) + + if (NULL == wsg->px) + { + return; + } + + // Only draw in bounds + int dWidth = TFT_WIDTH; + int dHeight = TFT_HEIGHT; + int wWidth = wsg->w; + int xMax = CLAMP(xOff + wWidth * xScale, 0, dWidth); + int yMax = CLAMP(yOff + wsg->h * yScale, 0, dHeight); + const paletteColor_t* linein = wsg->px; + + int x1; + int y1; + + // Draw each pixel, scaled + for (int y = yOff, iy = 0; y < yMax && iy < wsg->h; y += yScale, iy++) + { + if (y >= TFT_HEIGHT) + { + return; + } + + y1 = y + yScale; + + // Entire "pixel" is off-screen + if (y1 <= 0) + { + linein += wWidth; + continue; + } + + for (int x = xOff, ix = 0; x < xMax && ix < wsg->w; x += xScale, ix++) + { + if (x >= TFT_WIDTH) + { + // next line + break; + } + + x1 = x + xScale; + + if (x1 <= 0) + { + // next pixel + continue; + } + + uint8_t color = palette->newColors[linein[ix]]; + if (color != cTransparent) + { + fillDisplayArea(MAX(x, 0), MAX(y, 0), MIN(x1, dWidth), MIN(y1, dHeight), color); + } + } + linein += wWidth; + } +} + +/** + * @brief Draw a WSG to the display without flipping or rotation at half size + * + * @param wsg The WSG to draw to the display + * @param xOff The x offset to draw the WSG at + * @param yOff The y offset to draw the WSG at + * @param palette Color Map to use + */ +void drawWsgPaletteSimpleHalf(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette) +{ + // This function has been micro optimized by cnlohr on 2022-09-07, using gcc version 8.4.0 (crosstool-NG + // esp-2021r2-patch3) + + if (NULL == wsg->px) + { + return; + } + + // Only draw in bounds + int dWidth = TFT_WIDTH; + int wWidth = wsg->w; + int xMin = CLAMP(xOff, 0, dWidth); + int xMax = CLAMP(xOff + (wWidth / 2), 0, dWidth); + int yMin = CLAMP(yOff, 0, TFT_HEIGHT); + int yMax = CLAMP(yOff + (wsg->h / 2), 0, TFT_HEIGHT); + paletteColor_t* px = getPxTftFramebuffer(); + int numX = xMax - xMin; + int wsgY = (yMin - yOff); + int wsgX = (xMin - xOff); + paletteColor_t* lineout = &px[(yMin * dWidth) + xMin]; + const paletteColor_t* linein = &wsg->px[wsgY * wWidth + wsgX]; + + // Draw each pixel + for (int y = yMin; y < yMax; y++) + { + for (int x = 0; x < numX; x++) + { + uint8_t color = palette->newColors[linein[x * 2]]; + if (color != cTransparent) + { + lineout[x] = color; + } + } + lineout += dWidth; + linein += (2 * wWidth); + wsgY++; + } +} + +/** + * @brief Resets the palette to initial state + * + * @param palette Color map to modify + */ +void wsgPaletteReset(wsgPalette_t* palette) +{ + // Reset the palette + for (int32_t i = 0; i < 217; i++) + { + palette->newColors[i] = i; + } +} + +/** + * @brief Sets a single color to the provided palette + * + * @param palette Color map to modify + * @param replacedColor Color to be replaced + * @param newColor Color that will replace the previous + */ +void wsgPaletteSet(wsgPalette_t* palette, paletteColor_t replacedColor, paletteColor_t newColor) +{ + palette->newColors[replacedColor] = newColor; +} + +void wsgPaletteSetGroup(wsgPalette_t* palette, paletteColor_t* replacedColors, paletteColor_t* newColors, + uint8_t arrSize) +{ + for (int32_t i = 0; i < arrSize; i++) + { + wsgPaletteSet(palette, replacedColors[i], newColors[i]); + } +} \ No newline at end of file diff --git a/main/display/wsgPalette.h b/main/display/wsgPalette.h new file mode 100644 index 000000000..190ca062d --- /dev/null +++ b/main/display/wsgPalette.h @@ -0,0 +1,91 @@ +/** + * @file wsgPalette.h + * @author Jeremy Stintzcum (jeremy.stintzcum@gmail.com) + * @brief Provides palette swap functionality for Swadge + * @version 1.0.0 + * @date 2024-09-20 + * + * @copyright Copyright (c) 2024 + * + */ + +/*! \file wsgPalette.h + * + * \section wsgPalette_design Design Philosophy + * + * Provides functionality to WSGs to use a palette a la the NES. See wsg.h for how these are implemented. + * + * Clones all the current options for drawing WSGs, but all of them require a new parameter, 'palette' which is a array + of paletteColor_t objects. The functions largely just intercepts the color given by the WSG and converts it based on + the newColor map. + * + * \section wsgPalette_usage Usage + * + * There are three setup functions: + * - wsgPaletteReset(): Resets the provided palette to draw the default colors + * - wsgPaletteSet(): Provided the palette, a color to overwrite and a new color to use, sets the color + * - wsgPaletteSetGroup(): Does the same as above, but using lists to make generation easier + * + * If wsgPaletteReset() isn't called for the palette being used, all colors not specifically assigned will be black. + * + * There are four drawing functions provided with the palette + * - drawWsgPalette(): Draws the WSG with the appropriate palette + * - drawWsgPaletteSimple(): Draws the WSG with palette, but can't be rotated or flipped. + * - drawWsgPaletteSimpleScaled(): Draws the WSG with palette at a larger size set by the provided scale (integer + values, 2x, 3x, 4x...). + * - drawWsgPaletteSimpleHalf(): Draws the WSG at half scale with the included palette. + * + * \section wsgPalette_example Example + * + * \code{.c} + * // In modeData_t + * { + * wsgPalette_t pal; + * } + + * // In modeEnter + * { + * // Palette setup + * wsgPaletteReset(&pal); + * wsgPaletteSet(&pal, c000, c555); + * } + * + * // Where the WSG is drawn + * { + * drawWsgPalette(&wsg, x, y, &pal, vertFlip, HorFlip, rotation); + * } + * \endcode + */ +#pragma once + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "wsg.h" + +//============================================================================== +// Struct +//============================================================================== + +typedef struct +{ + paletteColor_t newColors[217]; ///< Color map +} wsgPalette_t; + +//============================================================================== +// Functions +//============================================================================== + +void drawWsgPalette(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette, bool flipLR, bool flipUD, + int16_t rotateDeg); +void drawWsgPaletteSimple(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette); +void drawWsgPaletteSimpleScaled(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette, int16_t xScale, + int16_t yScale); +void drawWsgPaletteSimpleHalf(const wsg_t* wsg, int16_t xOff, int16_t yOff, wsgPalette_t* palette); +void wsgPaletteReset(wsgPalette_t* palette); +void wsgPaletteSet(wsgPalette_t* palette, paletteColor_t replaced, paletteColor_t newColor); +void wsgPaletteSetGroup(wsgPalette_t* palette, paletteColor_t* replacedColors, paletteColor_t* newColors, + uint8_t arrSize); \ No newline at end of file diff --git a/main/menu/menuManiaRenderer.c b/main/menu/menuManiaRenderer.c index 531c28330..db03705a6 100644 --- a/main/menu/menuManiaRenderer.c +++ b/main/menu/menuManiaRenderer.c @@ -113,8 +113,8 @@ menuManiaRenderer_t* initMenuManiaRenderer(font_t* titleFont, font_t* titleFontO } else { - renderer->titleFont = titleFont; - renderer->titleFontAllocated = false; + renderer->titleFontOutline = titleFont; + renderer->titleFontOutlineAllocated = false; } // Save or allocate menu font diff --git a/main/midi/midiFileParser.c b/main/midi/midiFileParser.c index 7d308e681..eebe407a2 100644 --- a/main/midi/midiFileParser.c +++ b/main/midi/midiFileParser.c @@ -516,26 +516,6 @@ static bool trackParseNext(midiFileReader_t* reader, midiTrackState_t* track) // A sysex event in a MIDI file is different from one in streaming mode (i.e. USB), because there is no // length byte transmitted in streaming mode, so we need to check the manufacturer length there - // TODO: Move this to the streaming parser - /* - // We'll need to read at least one more byte - //if (!TRK_REMAIN()) { ERR(); } - uint16_t manufacturer = *(track->cur++); - if (!manufacturer) - { - if (TRK_REMAIN() < 2) { ERR(); } - // A manufacturer ID of 0 means the ID is actually in the next 2 bytes - manufacturer = *(track->cur++); - manufacturer <<= 7; - manufacturer |= *(track->cur++); - } - else - { - // Technically 0x00 0x00 0x41 is considered a different manufacturer from the single-byte value 0x41 - // So in that case just put a 1 in the 15th bit that's otherwise unused - manufacturer |= (1 << 15); - }*/ - uint32_t sysexLength; read = readVariableLength(track->cur, TRK_REMAIN(), &sysexLength); if (!read) @@ -554,10 +534,38 @@ static bool trackParseNext(midiFileReader_t* reader, midiTrackState_t* track) track->nextEvent.sysex.data = track->cur; // If the status is 0xF0, the 0xF0 should be prefixed to the data. track->nextEvent.sysex.prefix = (status == 0xF0) ? 0xF0 : 0x00; - // TODO - track->nextEvent.sysex.manufacturerId = 0; - track->cur += sysexLength; + // Store the pointer to the end of data so we don't need to do math later + const uint8_t* sysexEnd = (track->cur + sysexLength); + + // TODO: Move this to the streaming parser + // We'll need to read at least one more byte + if (!TRK_REMAIN()) + { + ERR(); + } + uint16_t manufacturer = *(track->cur++); + if (!manufacturer) + { + if (TRK_REMAIN() < 2) + { + ERR(); + } + // A manufacturer ID of 0 means the ID is actually in the next 2 bytes + manufacturer = *(track->cur++); + manufacturer <<= 7; + manufacturer |= *(track->cur++); + } + else + { + // Technically 0x00 0x00 0x41 is considered a different manufacturer from the single-byte value 0x41 + // So in that case just put a 1 in the 15th bit that's otherwise unused + manufacturer |= (1 << 15); + } + + track->nextEvent.sysex.manufacturerId = manufacturer; + + track->cur = sysexEnd; } else { diff --git a/main/midi/midiPlayer.c b/main/midi/midiPlayer.c index 8cb9ff3ae..848c1d39b 100644 --- a/main/midi/midiPlayer.c +++ b/main/midi/midiPlayer.c @@ -14,6 +14,9 @@ #include "macros.h" #include "cnfs.h" +// Uncomment to enable logging SysEx commands in detail +// #define DEBUG_SYSEX 1 + #define OSC_DITHER //============================================================================== @@ -48,6 +51,26 @@ static const uint8_t oscDither[] = { * ((int)(voice)->transitionTicksTotal - (int)(voice)->transitionTicks) \ / (int)(voice)->transitionTicksTotal)) +/// @brief Set only the MSB of a 14-bit value +#define SET_MSB(target, val) \ + do \ + { \ + uint16_t new14bitVal = ((val) & 0x7F); \ + new14bitVal <<= 7; \ + new14bitVal |= ((target) & 0x7F); \ + (target) = new14bitVal; \ + } while (0) + +/// @brief Set only the LSB of a 14-bit value +#define SET_LSB(target, val) \ + do \ + { \ + uint16_t new14bitVal = ((target) >> 7) & 0x7F; \ + new14bitVal <<= 7; \ + new14bitVal |= ((val) & 0x7F); \ + (target) = new14bitVal; \ + } while (0) + // Values for the percussion special states bitmap #define SHIFT_HI_HAT (0) #define SHIFT_WHISTLE (6) @@ -807,6 +830,251 @@ static void handleSysexEvent(midiPlayer_t* player, const midiSysexEvent_t* sysex // Actually we can assign a non-registered control to R, G, and B // I think there's enough for every LED too assuming there's still like, 7 or so // AND: if possible have a sysex command (hmm) that sets all the LEDs to individual values at once + + // GM Enable (01) + // GM Disable (02) + // GM2 Enable (03) + // GM Disable (00) (incorrect but I bet people are doing it because teragonaudio says to) + uint8_t mfrLen = 1; + +#ifdef DEBUG_SYSEX + uint8_t mfrHex[3] = {0, 0, 0}; +#endif + + const uint8_t* end = sysex->data + sysex->length; + + // Determine the length of the manufacturer ID so we know how many bytes to skip for the real data + if (sysex->manufacturerId & (1 << 15)) + { + // This is a 1-byte manufacturer ID + mfrLen = 1; +#ifdef DEBUG_SYSEX + mfrHex[0] = sysex->manufacturerId & ~(1 << 15); +#endif + } + else + { + // This is a 3-byte manufacturer ID + mfrLen = 3; +#ifdef DEBUG_SYSEX + mfrHex[1] = (sysex->manufacturerId >> 7) & 0x7F; + mfrHex[2] = (sysex->manufacturerId & 0x7F); +#endif + } + + const uint8_t* dataPtr = sysex->data + mfrLen; + +#ifdef DEBUG_SYSEX + printf("Got SysEx event length=%" PRIu32 ":\n", sysex->length); + printf("Manufacturer: "); + + for (int i = 0; i < mfrLen; i++) + { + printf("%02" PRIx8 " ", mfrHex[i]); + } + printf("\n"); + + for (uint32_t i = 0; i < sysex->length; i++) + { + if (i % 8 == 0) + { + printf("\n%04" PRIx32 " ", i); + } + + printf("%02" PRIx8 " ", sysex->data[i]); + } + + printf("\n"); +#endif + + switch (sysex->manufacturerId) + { + case MMFR_EDUCATIONAL_USE: + break; + + case MMFR_UNIVERSAL_NON_REAL_TIME: + case MMFR_UNIVERSAL_REAL_TIME: + { + bool realTime = (sysex->manufacturerId == MMFR_UNIVERSAL_REAL_TIME); + + // Universal SysEx messages have 127 "channel" values, with 0x7F meaning "Disregard Channel" + // uint8_t sysexChannel = *dataPtr++; + + if (dataPtr >= end) + { + // Err + return; + } + + uint8_t subId = *dataPtr++; + + if (dataPtr >= end) + { + // Err + return; + } + + uint8_t subId2 = *dataPtr++; + + if (realTime) + { + // Real Time Universal SysEx + switch (subId) + { + // 0: UNUSED + case 0x0: + // MIDI Time Code + case 0x1: + // MIDI Show Control + case 0x2: + // Notation Information + case 0x3: + break; + + // Device Control + case 0x4: + { + switch (subId2) + { + // Master Volume + case 1: + { + break; + } + + // Master Balance + case 2: + break; + + // Master Fine Tuning + case 3: + { + break; + } + + // Master Coarse Tuning + case 4: + { + break; + } + + case 5: + default: + break; + } + + break; + } + + // Real Time MTC Cueing + case 0x5: + // MIDI Machine Control Commands + case 0x6: + // MIDI Machine Control Responses + case 0x7: + // MIDI Tuning Standard (Real Time) + case 0x8: + // Controller Destination Setting + case 0x9: + // Key-based Instrument Control + case 0xA: + // Scalable Polyphony MIDI MIP Message + case 0xB: + // Mobile Phone Control Message + case 0xC: + default: + break; + } + } + else + { + // Non-Real Time Universal SysEx + switch (subId) + { + // 0: UNUSED + case 0x0: + // Sample Dump + case 0x1: + // Sample Data Packet + case 0x2: + // Sample Dump Request + case 0x3: + // MIDI Time Code + case 0x4: + // Sample Dump Extensions + case 0x5: + // General Information + case 0x6: + // File Dump + case 0x7: + // MIDI Tuning Standard + case 0x8: + break; + + // General MIDI + case 0x9: + { + switch (subId2) + { + case 0: + { + // NOTE: This value is NOT defined by the MIDI spec + // However, some resources incorrectly claim that a value of `0` for sub-ID 2 is the + // value for a "GM Off" event (with `1` being "GM On"), even though a value of `2` is + // specified for `GM Off`, so it is possible that a `0` value will be sent with the + // intent to disable GM. + midiGmOff(player); + break; + } + + // General MIDI 1 On + case 1: + { + midiGmOn(player); + break; + } + + // General MIDI Off + case 2: + { + midiGmOff(player); + break; + } + + // General MIDI 2 On (Unsupported) + case 3: + default: + break; + } + break; + } + + // Downloadable Sounds + case 0xA: + // File Reference Message + case 0xB: + // MIDI Visual Control + case 0xC: + // MIDI Capability Inquiry + case 0xD: + // End of File + case 0x7B: + // Wait + case 0x7C: + // Cancel + case 0x7D: + // NAK + case 0x7E: + // ACK + case 0x7F: + default: + break; + } + } + + break; + } + } } /** @@ -894,7 +1162,7 @@ void midiPlayerInit(midiPlayer_t* player) void midiPlayerReset(midiPlayer_t* player) { midiAllSoundOff(player); - midiGmOn(player); + midiGmOff(player); // We need the tempo to not be zero, so set it to the default of 120BPM until we get a tempo event // 120 BPM == 500,000 microseconds per quarter note @@ -1121,11 +1389,14 @@ void midiResetChannelControllers(midiPlayer_t* player, uint8_t channel) { midiChannel_t* chan = &player->channels[channel]; midiSustain(player, channel, MIDI_FALSE); - chan->volume = UINT14_MAX; - chan->pitchBend = PITCH_BEND_CENTER; - chan->program = 0; - chan->held = 0; - chan->sustenuto = 0; + chan->volume = UINT14_MAX; + chan->pitchBend = PITCH_BEND_CENTER; + chan->registeredParameter = true; + // Set selected RPN to "RPN Reset" which means none currently selected + chan->selectedParameter = UINT14_MAX; + chan->program = 0; + chan->held = 0; + chan->sustenuto = 0; initTimbre(&chan->timbre, getTimbreForProgram(chan->percussion, chan->bank, chan->program)); } @@ -1153,8 +1424,44 @@ void midiGmOff(midiPlayer_t* player) midiResetChannelControllers(player, chanIdx); // Channel 10 (index 9) is reserved for percussion. - chan->percussion = (9 == chanIdx); - chan->bank = 1; + // Also enable percussion on channel 11 (index 10) with an alternate drum kit + chan->percussion = (9 == chanIdx || 10 == chanIdx); + // Set bank 1 (MAGFest sounds) for everything except the first drumkit on 10 + chan->bank = (9 == chanIdx) ? 0 : 1; + + switch (chanIdx) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + { + chan->program = chanIdx; + break; + } + + case 9: + case 10: + { + chan->program = 0; + break; + } + + case 11: + case 12: + case 13: + case 14: + case 15: + { + chan->program = 0; + break; + } + } initTimbre(&chan->timbre, getTimbreForProgram(chan->percussion, chan->bank, chan->program)); } @@ -1597,19 +1904,35 @@ void midiControlChange(midiPlayer_t* player, uint8_t channel, midiControl_t cont { case MCC_BANK_MSB: { - uint16_t newBank = (val & 0x7F); - newBank <<= 7; - newBank |= (player->channels[channel].bank & 0x7F); - player->channels[channel].bank = newBank; + SET_MSB(player->channels[channel].bank, val); break; } case MCC_BANK_LSB: { - uint16_t newBank = (player->channels[channel].bank >> 7) & 0x7F; - newBank <<= 7; - newBank |= (val & 0x7F); - player->channels[channel].bank = newBank; + SET_LSB(player->channels[channel].bank, val); + break; + } + + // Data Entry MSB (6) + case MCC_DATA_ENTRY_MSB: + { + uint16_t curVal = midiGetParameterValue(player, channel, player->channels[channel].registeredParameter, + player->channels[channel].selectedParameter); + SET_MSB(curVal, val); + midiSetParameter(player, channel, player->channels[channel].registeredParameter, + player->channels[channel].selectedParameter, curVal); + break; + } + + // Data Entry LSB (38) + case MCC_DATA_ENTRY_LSB: + { + uint16_t curVal = midiGetParameterValue(player, channel, player->channels[channel].registeredParameter, + player->channels[channel].selectedParameter); + SET_LSB(curVal, val); + midiSetParameter(player, channel, player->channels[channel].registeredParameter, + player->channels[channel].selectedParameter, curVal); break; } @@ -1672,6 +1995,56 @@ void midiControlChange(midiPlayer_t* player, uint8_t channel, midiControl_t cont break; } + // Data Button Increment (96) + case MCC_DATA_BUTTON_INC: + // Data Button Decrement (97) + case MCC_DATA_BUTTON_DEC: + { + bool inc = (control == MCC_DATA_BUTTON_INC); + uint16_t param = player->channels[channel].selectedParameter; + uint16_t curVal + = midiGetParameterValue(player, channel, player->channels[channel].registeredParameter, param); + + // Prevent rollover + if ((inc && curVal < UINT14_MAX) || (!inc && curVal > 0)) + { + midiSetParameter(player, channel, player->channels[channel].registeredParameter, param, curVal); + } + break; + } + + // Non-registered Parameter Number LSB (98) + case MCC_NON_REGISTERED_PARAM_LSB: + { + player->channels[channel].registeredParameter = false; + SET_LSB(player->channels[channel].selectedParameter, val); + break; + } + + // Non-registered Parameter Number MSB (99) + case MCC_NON_REGISTERED_PARAM_MSB: + { + player->channels[channel].registeredParameter = false; + SET_MSB(player->channels[channel].selectedParameter, val); + break; + } + + // Registered Parameter Number LSB (100) + case MCC_REGISTERED_PARAM_LSB: + { + player->channels[channel].registeredParameter = true; + SET_LSB(player->channels[channel].selectedParameter, val); + break; + } + + // Registered Parameter Number MSB (101) + case MCC_REGISTERED_PARAM_MSB: + { + player->channels[channel].registeredParameter = true; + SET_MSB(player->channels[channel].selectedParameter, val); + break; + } + // All sounds off (120) case MCC_ALL_SOUND_OFF: { @@ -1763,6 +2136,94 @@ uint16_t midiGetControlValue14bit(midiPlayer_t* player, uint8_t channel, midiCon return result; } +void midiSetParameter(midiPlayer_t* player, uint8_t channel, bool registeredParam, uint16_t param, uint16_t value) +{ + if (registeredParam) + { + switch (param) + { + // Pitch Bend Range + case 0x0000: + { + // Not supported (yet?) + break; + } + + // Master Fine Tuning + case 0x0001: + { + // Not supported + break; + } + + // Master Coarse Tuning + case 0x0002: + { + // Not supported + break; + } + + // "Not Set" + case 0x3FFF: + default: + { + // No action necessary + break; + } + } + } + else + { + switch (param) + { + case 10: + { + // Set Percussion + if ((value != 0) != player->channels[channel].percussion) + { + player->channels[channel].percussion = (value != 0); + // Necessary to actually configure the channel for its new percussion state + midiSetProgram(player, channel, player->channels[channel].program); + } + break; + } + + default: + { + // Ignore all others + break; + } + } + } +} + +uint16_t midiGetParameterValue(midiPlayer_t* player, uint8_t channel, bool registered, uint16_t param) +{ + if (registered) + { + switch (param) + { + default: + return 0; + } + } + else + { + switch (param) + { + case 10: + { + return player->channels[channel].percussion ? 1 : 0; + } + + default: + { + return 0; + } + } + } +} + void midiPitchWheel(midiPlayer_t* player, uint8_t channel, uint16_t value) { // Save the pitch bend value diff --git a/main/midi/midiPlayer.h b/main/midi/midiPlayer.h index d08b6d033..7cff75efa 100644 --- a/main/midi/midiPlayer.h +++ b/main/midi/midiPlayer.h @@ -343,6 +343,16 @@ typedef enum MCC_POLY_OPERATION = 127, } midiControl_t; +/** + * @brief Values that can be directly compared against midiSysexEvent_t::manufacturerId + */ +typedef enum +{ + MMFR_EDUCATIONAL_USE = 0x807D, + MMFR_UNIVERSAL_NON_REAL_TIME = 0x807E, + MMFR_UNIVERSAL_REAL_TIME = 0x807F, +} midiManufacturerId_t; + //============================================================================== // Structs //============================================================================== @@ -593,6 +603,12 @@ typedef struct /// @brief The ID of the program (timbre) set for this channel uint8_t program; + /// @brief Whether selectedParameter represents a registered or non-registered parameter + bool registeredParameter; + + /// @brief The ID of the currently selected registered or non-registered parameter + uint16_t selectedParameter; + /// @brief The actual current timbre definition which the program ID corresponds to midiTimbre_t timbre; @@ -879,6 +895,29 @@ uint8_t midiGetControlValue(midiPlayer_t* player, uint8_t channel, midiControl_t */ uint16_t midiGetControlValue14bit(midiPlayer_t* player, uint8_t channel, midiControl_t control); +/** + * @brief Set a registered or non-registered parameter value + * + * @param player The MIDI player + * @param channel The channel to set the parameter on + * @param registered true if param refers to a registered parameter number and false if it refers to a non-registered + * @param param The registered or non-registered MIDI parameter to set the value of + * @param value The 14-bit value to set the parameter to + */ +void midiSetParameter(midiPlayer_t* player, uint8_t channel, bool registered, uint16_t param, uint16_t value); + +/** + * @brief Get the value of a registered or non-registered parameter + * + * @param player The MIDI player + * @param channel The channel to retrieve the parameter from + * @param registered true if param refers to a registered parameter number and false if it refers to a non-registered + * @param param The registered or non-registered MIDI parameter number to retrieve the value for + * @return The current 14-bit value of the given registered or non-registered parameter, or 0 if the parameter is + * unsupported + */ +uint16_t midiGetParameterValue(midiPlayer_t* player, uint8_t channel, bool registered, uint16_t param); + /** * @brief Set the pitch wheel value on a given MIDI channel * diff --git a/main/modes/games/pango/paEntity.c b/main/modes/games/pango/paEntity.c new file mode 100644 index 000000000..a33ca8a9c --- /dev/null +++ b/main/modes/games/pango/paEntity.c @@ -0,0 +1,1547 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include "paEntity.h" +#include "paEntityManager.h" +#include "paTilemap.h" +#include "paGameData.h" +#include "soundFuncs.h" +#include "hdw-btn.h" +#include "esp_random.h" +// #include "aabb_utils.h" +#include "trigonometry.h" +#include +#include "soundFuncs.h" + +//============================================================================== +// Constants +//============================================================================== +#define SUBPIXEL_RESOLUTION 4 +#define PA_TILE_SIZE_IN_POWERS_OF_2 4 +#define PA_TILE_SIZE 16 +#define PA_HALF_TILESIZE 8 +#define DESPAWN_THRESHOLD 64 + +#define SIGNOF(x) ((x > 0) - (x < 0)) +#define PA_TO_TILECOORDS(x) ((x) >> PA_TILE_SIZE_IN_POWERS_OF_2) +#define PA_GET_TAXICAB_DISTANCE(x1, y1, x2, y2) (abs(x1 - x2) + abs(y1 - y2)) +// #define TO_PIXEL_COORDS(x) ((x) >> SUBPIXEL_RESOLUTION) +// #define TO_SUBPIXEL_COORDS(x) ((x) << SUBPIXEL_RESOLUTION) + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeEntity(paEntity_t* self, paEntityManager_t* entityManager, paTilemap_t* tilemap, + paGameData_t* gameData, paSoundManager_t* soundManager) +{ + self->active = false; + self->tilemap = tilemap; + self->gameData = gameData; + self->soundManager = soundManager; + self->homeTileX = 0; + self->homeTileY = 0; + self->gravity = false; + self->falling = false; + self->entityManager = entityManager; + self->fallOffTileHandler = &defaultFallOffTileHandler; + self->spriteFlipHorizontal = false; + self->spriteFlipVertical = false; + self->facingDirection = PA_DIRECTION_SOUTH; + self->stateTimer = -1; + self->tempStateTimer = -1; + self->baseSpeed = 0; + self->stateFlag = false; + + // Fields not explicitly initialized + // self->type = 0; + // self->updateFunction = NULL; + // self->x = 0; + // self->y = 0; + // self->xspeed = 0; + // self->yspeed = 0; + // self->xMaxSpeed = 0; + // self->yMaxSpeed = 0; + // self->xDamping = 0; + // self->yDamping = 0; + // self->gravityEnabled = false; + // self->spriteIndex = 0; + // self->animationTimer = 0; + // self->jumpPower = 0; + // self->visible = false; + // self->hp = 0; + // self->invincibilityFrames = 0; + // self->scoreValue = 0; + // self->collisionHandler = NULL; + // self->tileCollisionHandler = NULL; + // self->overlapTileHandler = NULL; +} + +void pa_updatePlayer(paEntity_t* self) +{ + switch (self->state) + { + case PA_PL_ST_NORMAL: + default: + { + if (self->gameData->btnState & PB_LEFT) + { + self->xspeed -= 4; + + if (self->xspeed < -16) + { + self->xspeed = -16; + } + } + else if (self->gameData->btnState & PB_RIGHT) + { + self->xspeed += 4; + + if (self->xspeed > 16) + { + self->xspeed = 16; + } + } + + if (self->gameData->btnState & PB_UP) + { + self->yspeed -= 4; + + if (self->yspeed < -16) + { + self->yspeed = -16; + } + } + else if (self->gameData->btnState & PB_DOWN) + { + self->yspeed += 4; + + if (self->yspeed > 16) + { + self->yspeed = 16; + } + } + + if (self->animationTimer > 0) + { + self->animationTimer--; + } + + if (((self->gameData->btnState & PB_START) && !(self->gameData->prevBtnState & PB_START))) + { + self->gameData->changeState = PA_ST_PAUSE; + } + + /* + if(self->xspeed){ + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION) + SIGNOF(self->xspeed); + + if(!self->yspeed){ + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION); + } + } + + if(self->yspeed){ + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION) + SIGNOF(self->yspeed); + + if(!self->xspeed){ + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION); + } + } + */ + + switch (self->facingDirection) + { + case PA_DIRECTION_WEST: + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION) - 1; + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION); + break; + case PA_DIRECTION_EAST: + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION) + 1; + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION); + break; + case PA_DIRECTION_NORTH: + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION); + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION) - 1; + break; + case PA_DIRECTION_SOUTH: + default: + self->targetTileX = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION); + self->targetTileY = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION) + 1; + break; + } + + if (self->gameData->btnState & PB_A && !(self->gameData->prevBtnState & PB_A)) + { + uint8_t t = pa_getTile(self->tilemap, self->targetTileX, self->targetTileY); + if (t == PA_TILE_BLOCK || t == PA_TILE_SPAWN_BLOCK_0 || t == PA_TILE_BONUS_BLOCK_0) + { + paEntity_t* newHitBlock = createHitBlock( + self->entityManager, (self->targetTileX << SUBPIXEL_RESOLUTION) + PA_HALF_TILESIZE, + (self->targetTileY << SUBPIXEL_RESOLUTION) + PA_HALF_TILESIZE); + + if (newHitBlock != NULL) + { + pa_setTile(self->tilemap, self->targetTileX, self->targetTileY, PA_TILE_EMPTY); + newHitBlock->jumpPower = t; + switch (self->facingDirection) + { + case PA_DIRECTION_WEST: + newHitBlock->xspeed = -64; + break; + case PA_DIRECTION_EAST: + newHitBlock->xspeed = 64; + break; + case PA_DIRECTION_NORTH: + newHitBlock->yspeed = -64; + break; + case PA_DIRECTION_SOUTH: + default: + newHitBlock->yspeed = 64; + break; + } + soundPlaySfx(&(self->soundManager->sndSquish), BZR_LEFT); + } + } + + self->state = PA_PL_ST_PUSHING; + self->stateTimer = 8; + + switch (self->facingDirection) + { + case PA_DIRECTION_WEST: + self->spriteIndex = PA_SP_PLAYER_PUSH_SIDE_1; + break; + case PA_DIRECTION_EAST: + self->spriteIndex = PA_SP_PLAYER_PUSH_SIDE_1; + break; + case PA_DIRECTION_NORTH: + self->spriteIndex = PA_SP_PLAYER_PUSH_NORTH_1; + break; + case PA_DIRECTION_SOUTH: + default: + self->spriteIndex = PA_SP_PLAYER_PUSH_SOUTH_1; + break; + } + + break; + } + animatePlayer(self); + break; + } + case PA_PL_ST_PUSHING: + { + self->stateTimer--; + + if (self->stateTimer < 0) + { + self->state = PA_PL_ST_NORMAL; + break; + } + + if (self->stateTimer == 2) + { + self->spriteIndex++; + } + + break; + } + } + + pa_moveEntityWithTileCollisions(self); + applyDamping(self); + pa_detectEntityCollisions(self); +} + +void updateCrabdozer(paEntity_t* self) +{ + switch (self->state) + { + case PA_EN_ST_STUN: + self->stateTimer--; + if (self->stateTimer < 0) + { + self->facingDirection = PA_DIRECTION_NONE; + + /*if(self->stateFlag){ + self->state = PA_EN_ST_AGGRESSIVE; + self->stateTimer = 32767; //effectively always aggressive + self->entityManager->aggroEnemies++; + } else*/ + { + self->state = PA_EN_ST_NORMAL; + self->stateTimer = (300 + esp_random() % 600); // Min 5 seconds, max 15 seconds + } + } + else + { + if (self->gameData->frameCount % ((self->stateTimer >> 1) + 1) == 0) + { + self->spriteIndex = PA_SP_ENEMY_STUN; + self->spriteFlipHorizontal = !self->spriteFlipHorizontal; + } + } + + pa_detectEntityCollisions(self); + break; + case PA_EN_ST_NORMAL: + case PA_EN_ST_AGGRESSIVE: + case PA_EN_ST_RUNAWAY: + { + self->stateTimer--; + if (self->stateTimer < 0 || self->entityManager->aggroEnemies < self->gameData->minAggroEnemies) + { + if (self->state == PA_EN_ST_RUNAWAY) + { + killEnemy(self); + break; + } + else if (self->state == PA_EN_ST_NORMAL + && (self->entityManager->aggroEnemies < self->gameData->maxAggroEnemies)) + { + self->state = PA_EN_ST_AGGRESSIVE; + self->entityManager->aggroEnemies++; + self->baseSpeed += 2; + self->stateTimer = (300 + esp_random() % 300); // Min 5 seconds, max 10 seconds + } + else if (self->state == PA_EN_ST_AGGRESSIVE) + { + self->state = PA_EN_ST_NORMAL; + self->entityManager->aggroEnemies--; + self->baseSpeed -= 2; + self->stateTimer = (300 + esp_random() % 300); // Min 5 seconds, max 10 seconds + } + } + + if (self->state != PA_EN_ST_RUNAWAY && self->entityManager->activeEnemies == 1 + && self->gameData->remainingEnemies == 0) + { + self->state = PA_EN_ST_RUNAWAY; + self->entityManager->aggroEnemies = 1; + self->baseSpeed = 20; + self->stateTimer = 480; // 8 seconds + + self->targetTileX = (esp_random() % 2) ? 1 : 15; + self->targetTileY = (esp_random() % 2) ? 1 : 13; + } + + uint8_t tx = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION); + uint8_t ty = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION); + + uint8_t t1, t2, t3 = 0; + uint8_t distT1, distT2, distT3; + + if (self->state != PA_EN_ST_RUNAWAY) + { + self->targetTileX = PA_TO_TILECOORDS(self->entityManager->playerEntity->x >> SUBPIXEL_RESOLUTION); + self->targetTileY = PA_TO_TILECOORDS(self->entityManager->playerEntity->y >> SUBPIXEL_RESOLUTION); + } + + int16_t hcof = (((self->x >> SUBPIXEL_RESOLUTION) % PA_TILE_SIZE) - PA_HALF_TILESIZE); + int16_t vcof = (((self->y >> SUBPIXEL_RESOLUTION) % PA_TILE_SIZE) - PA_HALF_TILESIZE); + + bool doAgression = (self->state == PA_EN_ST_AGGRESSIVE) /*? esp_random() % 2 : false*/; + + switch (self->facingDirection) + { + case PA_DIRECTION_WEST: + if (hcof) + { + break; + } + + t1 = pa_getTile(self->tilemap, tx - 1, ty); + t2 = pa_getTile(self->tilemap, tx, ty - 1); + t3 = pa_getTile(self->tilemap, tx, ty + 1); + + distT1 = PA_GET_TAXICAB_DISTANCE(tx - 1, ty, self->targetTileX, self->targetTileY); + distT2 = PA_GET_TAXICAB_DISTANCE(tx, ty - 1, self->targetTileX, self->targetTileY); + distT3 = PA_GET_TAXICAB_DISTANCE(tx, ty + 1, self->targetTileX, self->targetTileY); + + if ((!t2 || doAgression) && distT2 < distT1 && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_NORTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_NORTH, self->baseSpeed); + } + break; + } + + if ((!t3 || doAgression) && distT3 < distT1) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_SOUTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_SOUTH, self->baseSpeed); + } + break; + } + + if (t1) + { + if (doAgression && t1 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_WEST, self->baseSpeed >> 1, tx, ty); + break; + } + + if ((!t2 || doAgression) && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_NORTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_NORTH, self->baseSpeed); + } + break; + } + + if (!t3 || doAgression) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_SOUTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_SOUTH, self->baseSpeed); + } + break; + } + + pa_enemyChangeDirection(self, PA_DIRECTION_EAST, self->baseSpeed); + break; + } + + break; + case PA_DIRECTION_EAST: + if (hcof) + { + break; + } + + t1 = pa_getTile(self->tilemap, tx + 1, ty); + t2 = pa_getTile(self->tilemap, tx, ty - 1); + t3 = pa_getTile(self->tilemap, tx, ty + 1); + + distT1 = PA_GET_TAXICAB_DISTANCE(tx + 1, ty, self->targetTileX, self->targetTileY); + distT2 = PA_GET_TAXICAB_DISTANCE(tx, ty - 1, self->targetTileX, self->targetTileY); + distT3 = PA_GET_TAXICAB_DISTANCE(tx, ty + 1, self->targetTileX, self->targetTileY); + + if ((!t2 || doAgression) && distT2 < distT1 && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_NORTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_NORTH, self->baseSpeed); + } + break; + } + if ((!t3 || doAgression) && distT3 < distT1) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_SOUTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_SOUTH, self->baseSpeed); + } + break; + } + if (t1) + { + if (doAgression && t1 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_EAST, self->baseSpeed >> 1, tx, ty); + break; + } + + if ((!t2 || doAgression) && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_NORTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_NORTH, self->baseSpeed); + } + break; + } + + if (!t3 || doAgression) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_SOUTH, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_SOUTH, self->baseSpeed); + } + break; + } + + pa_enemyChangeDirection(self, PA_DIRECTION_WEST, self->baseSpeed); + break; + } + + break; + case PA_DIRECTION_NORTH: + if (vcof) + { + break; + } + + t1 = pa_getTile(self->tilemap, tx, ty - 1); + t2 = pa_getTile(self->tilemap, tx - 1, ty); + t3 = pa_getTile(self->tilemap, tx + 1, ty); + + distT1 = PA_GET_TAXICAB_DISTANCE(tx, ty - 1, self->targetTileX, self->targetTileY); + distT2 = PA_GET_TAXICAB_DISTANCE(tx - 1, ty, self->targetTileX, self->targetTileY); + distT3 = PA_GET_TAXICAB_DISTANCE(tx + 1, ty, self->targetTileX, self->targetTileY); + + if ((!t2 || doAgression) && distT2 < distT1 && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_WEST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_WEST, self->baseSpeed); + } + break; + } + + if ((!t3 || doAgression) && distT3 < distT1) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_EAST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_EAST, self->baseSpeed); + } + break; + } + + if (t1) + { + if (doAgression && t1 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_NORTH, self->baseSpeed >> 1, tx, ty); + break; + } + + if ((!t2 || doAgression) && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_WEST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_WEST, self->baseSpeed); + } + break; + } + + if (!t3 || doAgression) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_EAST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_EAST, self->baseSpeed); + } + + break; + } + + pa_enemyChangeDirection(self, PA_DIRECTION_SOUTH, self->baseSpeed); + break; + } + + break; + case PA_DIRECTION_NONE: + default: + pa_enemyChangeDirection(self, 1 >> (esp_random() % 3), self->baseSpeed); + break; + case PA_DIRECTION_SOUTH: + if (vcof) + { + break; + } + + t1 = pa_getTile(self->tilemap, tx, ty + 1); + t2 = pa_getTile(self->tilemap, tx - 1, ty); + t3 = pa_getTile(self->tilemap, tx + 1, ty); + + distT1 = PA_GET_TAXICAB_DISTANCE(tx, ty + 1, self->targetTileX, self->targetTileY); + distT2 = PA_GET_TAXICAB_DISTANCE(tx - 1, ty, self->targetTileX, self->targetTileY); + distT3 = PA_GET_TAXICAB_DISTANCE(tx + 1, ty, self->targetTileX, self->targetTileY); + + if ((!t2 || doAgression) && distT2 < distT1 && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_WEST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_WEST, self->baseSpeed); + } + break; + } + + if ((!t3 || doAgression) && distT3 < distT1) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_EAST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_EAST, self->baseSpeed); + } + break; + } + + if (t1) + { + if (doAgression && t1 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_SOUTH, self->baseSpeed >> 1, tx, ty); + break; + } + + if ((!t2 || doAgression) && (t3 || distT2 < distT3)) + { + if (doAgression && t2 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_WEST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_WEST, self->baseSpeed); + } + break; + } + + if (!t3 || doAgression) + { + if (doAgression && t3 == PA_TILE_BLOCK) + { + pa_enemyBreakBlock(self, PA_DIRECTION_EAST, self->baseSpeed >> 1, tx, ty); + } + else + { + pa_enemyChangeDirection(self, PA_DIRECTION_EAST, self->baseSpeed); + } + break; + } + + pa_enemyChangeDirection(self, PA_DIRECTION_NORTH, self->baseSpeed); + break; + } + + break; + } + + pa_animateEnemy(self); + despawnWhenOffscreen(self); + if (self->state != PA_EN_ST_BREAK_BLOCK) + { + // Need to skip this if enemy has just changed to breaking block state + // or else enemy will be stopped + pa_moveEntityWithTileCollisions(self); + } + pa_detectEntityCollisions(self); + + break; + } + case PA_EN_ST_BREAK_BLOCK: + { + /*//Need to force a speed value because + //tile collision will stop the enemy before we get here + switch(self->facingDirection){ + case PA_DIRECTION_WEST: + self->xspeed = -8; + break; + case PA_DIRECTION_EAST: + self->xspeed = 8; + break; + case PA_DIRECTION_NORTH: + self->yspeed = -8; + break; + case PA_DIRECTION_SOUTH: + self->yspeed = 8; + break; + default: + break; + }*/ + + self->x += self->xspeed; + self->y += self->yspeed; + + self->stateTimer--; + if (self->stateTimer < 0) + { + self->state = PA_EN_ST_AGGRESSIVE; + self->xspeed *= 2; + self->yspeed *= 2; + self->stateTimer = self->tempStateTimer; + } + + pa_animateEnemy(self); + break; + } + default: + { + break; + } + } +} + +void pa_enemyChangeDirection(paEntity_t* self, uint16_t newDirection, int16_t speed) +{ + switch (newDirection) + { + case PA_DIRECTION_WEST: + self->yspeed = 0; + self->xspeed = -speed; + break; + case PA_DIRECTION_EAST: + self->yspeed = 0; + self->xspeed = speed; + break; + case PA_DIRECTION_NORTH: + self->xspeed = 0; + self->yspeed = -speed; + break; + case PA_DIRECTION_NONE: + default: + self->xspeed = 0; + self->yspeed = 0; + break; + case PA_DIRECTION_SOUTH: + self->xspeed = 0; + self->yspeed = speed; + break; + } + + self->facingDirection = newDirection; +} + +void pa_enemyBreakBlock(paEntity_t* self, uint16_t newDirection, int16_t speed, uint8_t tx, uint8_t ty) +{ + switch (newDirection) + { + case PA_DIRECTION_WEST: + pa_createBreakBlock(self->entityManager, ((tx - 1) << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE, + (ty << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE); + break; + case PA_DIRECTION_EAST: + pa_createBreakBlock(self->entityManager, ((tx + 1) << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE, + (ty << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE); + break; + case PA_DIRECTION_NORTH: + pa_createBreakBlock(self->entityManager, (tx << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE, + ((ty - 1) << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE); + break; + case PA_DIRECTION_NONE: + default: + break; + case PA_DIRECTION_SOUTH: + pa_createBreakBlock(self->entityManager, (tx << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE, + ((ty + 1) << SUBPIXEL_RESOLUTION) + PA_HALF_TILE_SIZE); + break; + } + + self->state = PA_EN_ST_BREAK_BLOCK; + self->tempStateTimer = self->stateTimer; + self->stateTimer = 16; + pa_enemyChangeDirection(self, newDirection, speed); +} + +void pa_animateEnemy(paEntity_t* self) +{ + if (self->xspeed != 0) + { + if ((self->xspeed < 0) || (self->xspeed > 0)) + { + // Running + self->spriteFlipHorizontal = (self->xspeed > 0) ? 0 : 1; + + if (self->gameData->frameCount % 5 == 0) + { + self->spriteIndex + = PA_SP_ENEMY_SIDE_1 + ((self->spriteIndex + 1) % 2) + ((self->state != PA_EN_ST_NORMAL) ? 4 : 0); + self->facingDirection = self->spriteFlipHorizontal ? PA_DIRECTION_WEST : PA_DIRECTION_EAST; + } + } + else + { + // self->spriteIndex = SP_PLAYER_SLIDE; + } + } + else if (self->yspeed > 0) + { + if (self->yspeed > 0) + { + if (self->gameData->frameCount % 5 == 0) + { + self->spriteIndex = PA_SP_ENEMY_SOUTH + ((self->state != PA_EN_ST_NORMAL) ? 4 : 0); + self->spriteFlipHorizontal = (self->gameData->frameCount >> 1) % 2; + self->facingDirection = PA_DIRECTION_SOUTH; + } + } + } + else if (self->yspeed < 0) + { + if (self->yspeed < 0) + { + if (self->gameData->frameCount % 5 == 0) + { + self->spriteIndex = PA_SP_ENEMY_NORTH + ((self->state != PA_EN_ST_NORMAL) ? 4 : 0); + self->spriteFlipHorizontal = (self->gameData->frameCount >> 1) % 2; + self->facingDirection = PA_DIRECTION_NORTH; + } + } + } + else + { + self->facingDirection = PA_DIRECTION_NONE; + } +} + +void updateHitBlock(paEntity_t* self) +{ + self->animationTimer++; + + if (self->homeTileY > self->tilemap->mapHeight) + { + pa_destroyEntity(self, false); + return; + } + + pa_moveEntityWithTileCollisions(self); +} + +void pa_moveEntityWithTileCollisions(paEntity_t* self) +{ + uint16_t newX = self->x; + uint16_t newY = self->y; + uint8_t tx = PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION); + uint8_t ty = PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION); + // bool collision = false; + + // Are we inside a block? Push self out of block + uint8_t t = pa_getTile(self->tilemap, tx, ty); + self->overlapTileHandler(self, t, tx, ty); + + if (pa_isSolid(t)) + { + if (self->xspeed == 0 && self->yspeed == 0) + { + newX += (self->spriteFlipHorizontal) ? 16 : -16; + } + else + { + if (self->yspeed != 0) + { + self->yspeed = -self->yspeed; + } + else + { + self->xspeed = -self->xspeed; + } + } + } + else + { + if (self->yspeed != 0) + { + int16_t hcof = (((self->x >> SUBPIXEL_RESOLUTION) % PA_TILE_SIZE) - PA_HALF_TILESIZE); + + // Handle halfway though tile + uint8_t at = pa_getTile(self->tilemap, tx + SIGNOF(hcof), ty); + + if (pa_isSolid(at)) + { + // collision = true; + newX = ((tx + 1) * PA_TILE_SIZE - PA_HALF_TILESIZE) << SUBPIXEL_RESOLUTION; + } + + uint8_t newTy = PA_TO_TILECOORDS(((self->y + self->yspeed) >> SUBPIXEL_RESOLUTION) + + SIGNOF(self->yspeed) * PA_HALF_TILESIZE); + + if (newTy != ty) + { + uint8_t newVerticalTile = pa_getTile(self->tilemap, tx, newTy); + + // if (newVerticalTile > PA_TILE_UNUSED_29 && newVerticalTile < PA_TILE_BG_GOAL_ZONE) + { + if (self->tileCollisionHandler(self, newVerticalTile, tx, newTy, 2 << (self->yspeed > 0))) + { + newY = ((newTy + ((ty < newTy) ? -1 : 1)) * PA_TILE_SIZE + PA_HALF_TILESIZE) + << SUBPIXEL_RESOLUTION; + } + } + } + } + + if (self->xspeed != 0) + { + int16_t vcof = (((self->y >> SUBPIXEL_RESOLUTION) % PA_TILE_SIZE) - PA_HALF_TILESIZE); + + // Handle halfway though tile + uint8_t att = pa_getTile(self->tilemap, tx, ty + SIGNOF(vcof)); + + if (pa_isSolid(att)) + { + // collision = true; + newY = ((ty + 1) * PA_TILE_SIZE - PA_HALF_TILESIZE) << SUBPIXEL_RESOLUTION; + } + + // Handle outside of tile + uint8_t newTx = PA_TO_TILECOORDS(((self->x + self->xspeed) >> SUBPIXEL_RESOLUTION) + + SIGNOF(self->xspeed) * PA_HALF_TILESIZE); + + if (newTx != tx) + { + uint8_t newHorizontalTile = pa_getTile(self->tilemap, newTx, ty); + + // if (newHorizontalTile > PA_TILE_UNUSED_29 && newHorizontalTile < PA_TILE_BG_GOAL_ZONE) + { + if (self->tileCollisionHandler(self, newHorizontalTile, newTx, ty, (self->xspeed > 0))) + { + newX = ((newTx + ((tx < newTx) ? -1 : 1)) * PA_TILE_SIZE + PA_HALF_TILESIZE) + << SUBPIXEL_RESOLUTION; + } + } + + if (!self->falling) + { + uint8_t newBelowTile = pa_getTile(self->tilemap, tx, ty + 1); + + if ((self->gravityEnabled + && !pa_isSolid( + newBelowTile)) /*(|| (!self->gravityEnabled && newBelowTile != PA_TILE_LADDER)*/) + { + self->fallOffTileHandler(self); + } + } + } + } + } + + self->x = newX + self->xspeed; + self->y = newY + self->yspeed; +} + +void defaultFallOffTileHandler(paEntity_t* self) +{ + self->falling = true; +} + +void applyDamping(paEntity_t* self) +{ + if (self->xspeed > 0) + { + self->xspeed -= self->xDamping; + + if (self->xspeed < 0) + { + self->xspeed = 0; + } + } + else if (self->xspeed < 0) + { + self->xspeed += self->xDamping; + + if (self->xspeed > 0) + { + self->xspeed = 0; + } + } + + if (self->yspeed > 0) + { + self->yspeed -= self->yDamping; + + if (self->yspeed < 0) + { + self->yspeed = 0; + } + } + else if (self->yspeed < 0) + { + self->yspeed += self->yDamping; + + if (self->yspeed > 0) + { + self->yspeed = 0; + } + } +} + +void applyGravity(paEntity_t* self) +{ + if (!self->gravityEnabled || !self->falling) + { + return; + } + + self->yspeed += self->gravity; + + if (self->yspeed > self->yMaxSpeed) + { + self->yspeed = self->yMaxSpeed; + } +} + +void despawnWhenOffscreen(paEntity_t* self) +{ + if ((self->x >> SUBPIXEL_RESOLUTION) < (self->tilemap->mapOffsetX - DESPAWN_THRESHOLD) + || (self->x >> SUBPIXEL_RESOLUTION) + > (self->tilemap->mapOffsetX + PA_TILE_MAP_DISPLAY_WIDTH_PIXELS + DESPAWN_THRESHOLD)) + { + pa_destroyEntity(self, true); + } + + if (self->y > 63616) + { + return; + } + + if ((self->y >> SUBPIXEL_RESOLUTION) < (self->tilemap->mapOffsetY - (DESPAWN_THRESHOLD << 2)) + || (self->y >> SUBPIXEL_RESOLUTION) + > (self->tilemap->mapOffsetY + PA_TILE_MAP_DISPLAY_HEIGHT_PIXELS + DESPAWN_THRESHOLD)) + { + pa_destroyEntity(self, true); + } +} + +void pa_destroyEntity(paEntity_t* self, bool respawn) +{ + /*if (respawn && !(self->homeTileX == 0 && self->homeTileY == 0)) + { + self->tilemap->map[self->homeTileY * self->tilemap->mapWidth + self->homeTileX] = self->type + 128; + }*/ + + // self->entityManager->activeEntities--; + self->active = false; +} + +void animatePlayer(paEntity_t* self) +{ + if (abs(self->xspeed) > abs(self->yspeed)) + { + if (((self->gameData->btnState & PB_LEFT) && self->xspeed < 0) + || ((self->gameData->btnState & PB_RIGHT) && self->xspeed > 0)) + { + // Running + self->spriteFlipHorizontal = (self->xspeed > 0) ? 0 : 1; + self->facingDirection = self->spriteFlipHorizontal ? PA_DIRECTION_WEST : PA_DIRECTION_EAST; + + if (self->gameData->frameCount % 7 == 0) + { + self->spriteIndex = PA_SP_PLAYER_SIDE + ((self->spriteIndex + 1) % 3); + } + } + else + { + // self->spriteIndex = SP_PLAYER_SLIDE; + } + } + else if (self->yspeed > 0) + { + if ((self->gameData->btnState & PB_DOWN) && self->yspeed > 0) + { + self->facingDirection = PA_DIRECTION_SOUTH; + + if (self->gameData->frameCount % 7 == 0) + { + self->spriteIndex = PA_SP_PLAYER_SOUTH + ((self->spriteIndex + 1) % 2); + self->spriteFlipHorizontal = (self->gameData->frameCount >> 1) % 2; + } + } + } + else if (self->yspeed < 0) + { + if ((self->gameData->btnState & PB_UP) && self->yspeed < 0) + { + self->facingDirection = PA_DIRECTION_NORTH; + + if (self->gameData->frameCount % 7 == 0) + { + self->spriteIndex = PA_SP_PLAYER_NORTH + ((self->spriteIndex + 1) % 2); + self->spriteFlipHorizontal = (self->gameData->frameCount >> 1) % 2; + } + } + } + else + { + // Standing + // self->spriteIndex = PA_SP_PLAYER_SOUTH; + } +} + +void pa_detectEntityCollisions(paEntity_t* self) +{ + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + paEntity_t* checkEntity = &(self->entityManager->entities[i]); + if (checkEntity->active && checkEntity != self) + { + uint32_t dist = abs(self->x - checkEntity->x) + abs(self->y - checkEntity->y); + + if (dist < 200) + { + self->collisionHandler(self, checkEntity); + } + } + } +} + +void pa_playerCollisionHandler(paEntity_t* self, paEntity_t* other) +{ + switch (other->type) + { + case PA_ENTITY_CRABDOZER: + { + other->xspeed = -other->xspeed; + + /*if (self->y < other->y || self->yspeed > 0) + { + pa_scorePoints(self->gameData, other->scoreValue); + + killEnemy(other); + soundPlaySfx(&(self->soundManager->sndSquish), BZR_LEFT); + + self->yspeed = -180; + self->jumpPower = 64 + ((abs(self->xspeed) + 16) >> 3); + self->falling = true; + } + else*/ + if (self->invincibilityFrames <= 0 && other->state != PA_EN_ST_STUN) + { + self->hp--; + pa_updateLedsHpMeter(self->entityManager, self->gameData); + self->gameData->comboTimer = 0; + + if (!self->gameData->debugMode && self->hp == 0) + { + self->updateFunction = &updateEntityDead; + self->type = ENTITY_DEAD; + self->xspeed = 0; + self->yspeed = -60; + self->spriteIndex = PA_SP_PLAYER_HURT; + self->gameData->changeState = PA_ST_DEAD; + self->gravityEnabled = true; + self->falling = true; + } + else + { + self->xspeed = 0; + self->yspeed = 0; + self->jumpPower = 0; + self->invincibilityFrames = 120; + soundPlaySfx(&(self->soundManager->sndHurt), BZR_LEFT); + } + } + + break; + } + case ENTITY_HIT_BLOCK: + { + if (self->x < other->x) + { + self->x = other->x - (PA_TILE_SIZE << SUBPIXEL_RESOLUTION); + self->xspeed = 0; + } + else if (self->x > other->x) + { + self->x = other->x + (PA_TILE_SIZE << SUBPIXEL_RESOLUTION); + self->xspeed = 0; + } + else if (self->y < other->y) + { + self->y = other->y - (PA_TILE_SIZE << SUBPIXEL_RESOLUTION); + self->yspeed = 0; + } + else if (self->y > other->y) + { + self->y = other->y + (PA_TILE_SIZE << SUBPIXEL_RESOLUTION); + self->yspeed = 0; + } + break; + } + default: + { + break; + } + } +} + +void pa_enemyCollisionHandler(paEntity_t* self, paEntity_t* other) +{ + switch (other->type) + { + case PA_ENTITY_CRABDOZER: + if ((self->xspeed > 0 && self->x < other->x) || (self->xspeed < 0 && self->x > other->x)) + { + self->xspeed = -self->xspeed; + // self->spriteFlipHorizontal = -self->spriteFlipHorizontal; + } + + if ((self->yspeed > 0 && self->y < other->y) || (self->yspeed < 0 && self->y > other->y)) + { + self->yspeed = -self->yspeed; + // self->spriteFlipHorizontal = -self->spriteFlipHorizontal; + } + break; + case ENTITY_HIT_BLOCK: + self->xspeed = other->xspeed * 2; + self->yspeed = other->yspeed * 2; + pa_scorePoints(self->gameData, self->scoreValue); + soundPlaySfx(&(self->soundManager->sndHurt), 2); + killEnemy(self); + break; + default: + { + break; + } + } +} + +void pa_dummyCollisionHandler(paEntity_t* self, paEntity_t* other) +{ + return; +} + +bool pa_playerTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + /*switch (tileId) + { + case PA_TILE_COIN_1 ... PA_TILE_COIN_3: + { + pa_setTile(self->tilemap, tx, ty, PA_TILE_EMPTY); + addCoins(self->gameData, 1); + pa_scorePoints(self->gameData, 50); + break; + } + case PA_TILE_LADDER: + { + self->gravityEnabled = false; + self->falling = false; + break; + } + default: + { + break; + } + }*/ + + if (pa_isSolid(tileId)) + { + switch (direction) + { + case 0: // LEFT + self->xspeed = 0; + break; + case 1: // RIGHT + self->xspeed = 0; + break; + case 2: // UP + self->yspeed = 0; + break; + case 4: // DOWN + // Landed on platform + self->falling = false; + self->yspeed = 0; + break; + default: // Should never hit + return false; + } + // trigger tile collision resolution + return true; + } + + return false; +} + +bool pa_enemyTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + /*switch (tileId) + { + case PA_TILE_BOUNCE_BLOCK: + { + switch (direction) + { + case 0: + // hitBlock->xspeed = -64; + if (tileId == PA_TILE_BOUNCE_BLOCK) + { + self->xspeed = 48; + } + break; + case 1: + // hitBlock->xspeed = 64; + if (tileId == PA_TILE_BOUNCE_BLOCK) + { + self->xspeed = -48; + } + break; + case 2: + // hitBlock->yspeed = -128; + if (tileId == PA_TILE_BOUNCE_BLOCK) + { + self->yspeed = 48; + } + break; + case 4: + // hitBlock->yspeed = (tileId == PA_TILE_BRICK_BLOCK) ? 32 : 64; + if (tileId == PA_TILE_BOUNCE_BLOCK) + { + self->yspeed = -48; + } + break; + default: + break; + } + break; + } + default: + { + break; + } + }*/ + + if (pa_isSolid(tileId)) + { + switch (direction) + { + case 0: // LEFT + self->xspeed = 0; + break; + case 1: // RIGHT + self->xspeed = 0; + break; + case 2: // UP + self->yspeed = 0; + break; + case 4: // DOWN + // Landed on platform + self->falling = false; + self->yspeed = 0; + break; + default: // Should never hit + return false; + } + // trigger tile collision resolution + return true; + } + + return false; +} + +bool pa_dummyTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + return false; +} + +bool pa_hitBlockTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + if (pa_isSolid(tileId)) + { + soundPlaySfx(&(self->soundManager->sndHit), 1); + pa_destroyEntity(self, false); + + if (PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION) == self->homeTileX + && PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION) == self->homeTileY) + { + pa_createBreakBlock(self->entityManager, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + if (self->jumpPower == PA_TILE_SPAWN_BLOCK_0) + { + self->entityManager->gameData->remainingEnemies--; + } + } + else + { + self->tilemap->map[PA_TO_TILECOORDS(self->y >> SUBPIXEL_RESOLUTION) * self->tilemap->mapWidth + + PA_TO_TILECOORDS(self->x >> SUBPIXEL_RESOLUTION)] + = self->jumpPower; + } + + return true; + } + return false; +} + +void dieWhenFallingOffScreen(paEntity_t* self) +{ + uint16_t deathBoundary = (self->tilemap->mapOffsetY + PA_TILE_MAP_DISPLAY_HEIGHT_PIXELS + DESPAWN_THRESHOLD); + if (((self->y >> SUBPIXEL_RESOLUTION) > deathBoundary) + && ((self->y >> SUBPIXEL_RESOLUTION) < deathBoundary + DESPAWN_THRESHOLD)) + { + self->hp = 0; + pa_updateLedsHpMeter(self->entityManager, self->gameData); + self->gameData->changeState = PA_ST_DEAD; + pa_destroyEntity(self, true); + } +} + +void pa_updateDummy(paEntity_t* self) +{ + // Do nothing, because that's what dummies do! +} + +void pa_updateBreakBlock(paEntity_t* self) +{ + if (self->gameData->frameCount % 4 == 0) + { + self->spriteIndex++; + + if (self->spriteIndex > PA_SP_BREAK_BLOCK_3) + { + pa_createBlockFragment(self->entityManager, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + pa_createBlockFragment(self->entityManager, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + pa_createBlockFragment(self->entityManager, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + pa_createBlockFragment(self->entityManager, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + pa_destroyEntity(self, false); + } + } +} + +void updateEntityDead(paEntity_t* self) +{ + applyGravity(self); + self->x += self->xspeed; + self->y += self->yspeed; + + despawnWhenOffscreen(self); +} + +void pa_updateBlockFragment(paEntity_t* self) +{ + self->animationTimer++; + if (self->animationTimer > 8) + { + pa_destroyEntity(self, false); + return; + } + + self->x += self->xspeed; + self->y += self->yspeed; + + applyGravity(self); + despawnWhenOffscreen(self); +} + +void killEnemy(paEntity_t* target) +{ + target->homeTileX = 0; + target->homeTileY = 0; + target->gravityEnabled = true; + target->falling = true; + target->type = ENTITY_DEAD; + target->spriteFlipVertical = true; + target->updateFunction = &updateEntityDead; + + target->entityManager->activeEnemies--; + if (target->state == PA_EN_ST_AGGRESSIVE) + { + target->entityManager->aggroEnemies--; + } + + if (target->entityManager->activeEnemies == 0 && target->entityManager->gameData->remainingEnemies == 0) + { + target->gameData->changeState = PA_ST_LEVEL_CLEAR; + target->entityManager->playerEntity->spriteIndex = PA_SP_PLAYER_WIN; + target->entityManager->playerEntity->updateFunction = &pa_updateDummy; + } +} + +void pa_playerOverlapTileHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty) +{ + /*switch (tileId) + { + case PA_TILE_COIN_1 ... PA_TILE_COIN_3: + { + pa_setTile(self->tilemap, tx, ty, PA_TILE_EMPTY); + addCoins(self->gameData, 1); + pa_scorePoints(self->gameData, 50); + break; + } + case PA_TILE_LADDER: + { + if (self->gravityEnabled) + { + self->gravityEnabled = false; + self->xspeed = 0; + } + break; + } + default: + { + break; + } + } + + if (!self->gravityEnabled && tileId != PA_TILE_LADDER) + { + self->gravityEnabled = true; + self->falling = true; + if (self->yspeed < 0) + { + self->yspeed = -32; + } + }*/ +} + +void pa_defaultOverlapTileHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty) +{ + // Nothing to do. +} + +void killPlayer(paEntity_t* self) +{ + self->hp = 0; + pa_updateLedsHpMeter(self->entityManager, self->gameData); + + self->updateFunction = &updateEntityDead; + self->type = ENTITY_DEAD; + self->xspeed = 0; + self->yspeed = -60; + self->spriteIndex = PA_SP_PLAYER_HURT; + self->gameData->changeState = PA_ST_DEAD; + self->falling = true; +} + +void drawEntityTargetTile(paEntity_t* self) +{ + drawRect((self->targetTileX << PA_TILE_SIZE_IN_POWERS_OF_2) - self->tilemap->mapOffsetX, + self->targetTileY << PA_TILE_SIZE_IN_POWERS_OF_2, + (self->targetTileX << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_TILE_SIZE - self->tilemap->mapOffsetX, + (self->targetTileY << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_TILE_SIZE, esp_random() % 216); +} diff --git a/main/modes/games/pango/paEntity.h b/main/modes/games/pango/paEntity.h new file mode 100644 index 000000000..dc89f4587 --- /dev/null +++ b/main/modes/games/pango/paEntity.h @@ -0,0 +1,191 @@ +#ifndef _PA_ENTITY_H_ +#define _PA_ENTITY_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "pango_typedef.h" +#include "paTilemap.h" +#include "paGameData.h" +#include "paSoundManager.h" +#include "shapes.h" + +//============================================================================== +// Enums +//============================================================================== + +typedef enum +{ + ENTITY_PLAYER, + PA_ENTITY_CRABDOZER, + PA_ENTITY_BREAK_BLOCK, + PA_ENTITY_BLOCK_FRAGMENT, + ENTITY_HIT_BLOCK, + ENTITY_DEAD +} paEntityIndex_t; + +typedef enum +{ + PA_DIRECTION_NONE, + PA_DIRECTION_NORTH, + PA_DIRECTION_SOUTH, + PA_DIRECTION_NULL_3, + PA_DIRECTION_WEST, + PA_DIRECTION_NORTHWEST, + PA_DIRECTION_SOUTHWEST, + PA_DIRECTION_NULL_7, + PA_DIRECTION_EAST, + PA_DIRECTION_NORTHEAST, + PA_DIRECTION_SOUTHEAST +} paCompassDirection_t; + +typedef enum +{ + PA_EN_ST_SPAWNING, + PA_EN_ST_STUN, + PA_EN_ST_NORMAL, + PA_EN_ST_AGGRESSIVE, + PA_EN_ST_RUNAWAY, + PA_EN_ST_BREAK_BLOCK, +} paEnemyState_t; + +typedef enum +{ + PA_PL_ST_NORMAL, + PA_PL_ST_PUSHING +} paPlayerState_t; + +//============================================================================== +// Structs +//============================================================================== + +typedef void (*pa_updateFunction_t)(struct paEntity_t* self); +typedef void (*pa_collisionHandler_t)(struct paEntity_t* self, struct paEntity_t* other); +typedef bool (*PA_TILE_CollisionHandler_t)(struct paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, + uint8_t direction); +typedef void (*pa_fallOffTileHandler_t)(struct paEntity_t* self); +typedef void (*pa_overlapTileHandler_t)(struct paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty); + +struct paEntity_t +{ + bool active; + // bool important; + + uint8_t type; + pa_updateFunction_t updateFunction; + + uint16_t x; + uint16_t y; + + int16_t xspeed; + int16_t yspeed; + + int16_t xMaxSpeed; + int16_t yMaxSpeed; + + int16_t xDamping; + int16_t yDamping; + + bool gravityEnabled; + int16_t gravity; + bool falling; + + uint16_t facingDirection; + + uint8_t spriteIndex; + bool spriteFlipHorizontal; + bool spriteFlipVertical; + uint8_t animationTimer; + + paTilemap_t* tilemap; + paGameData_t* gameData; + paSoundManager_t* soundManager; + + uint8_t homeTileX; + uint8_t homeTileY; + + int16_t jumpPower; + + bool visible; + uint8_t hp; + int8_t invincibilityFrames; + uint16_t scoreValue; + + uint8_t targetTileX; + uint8_t targetTileY; + uint16_t state; + bool stateFlag; + int16_t stateTimer; + int16_t tempStateTimer; + int16_t baseSpeed; + + // paEntity_t *entities; + paEntityManager_t* entityManager; + + pa_collisionHandler_t collisionHandler; + PA_TILE_CollisionHandler_t tileCollisionHandler; + pa_fallOffTileHandler_t fallOffTileHandler; + pa_overlapTileHandler_t overlapTileHandler; +}; + +//============================================================================== +// Prototypes +//============================================================================== +void pa_initializeEntity(paEntity_t* self, paEntityManager_t* entityManager, paTilemap_t* tilemap, + paGameData_t* gameData, paSoundManager_t* soundManager); + +void pa_updatePlayer(paEntity_t* self); +void updateCrabdozer(paEntity_t* self); +void pa_enemyChangeDirection(paEntity_t* self, uint16_t newDirection, int16_t speed); +void pa_enemyBreakBlock(paEntity_t* self, uint16_t newDirection, int16_t speed, uint8_t tx, uint8_t ty); +void pa_animateEnemy(paEntity_t* self); +void updateHitBlock(paEntity_t* self); + +void pa_moveEntityWithTileCollisions(paEntity_t* self); +void defaultFallOffTileHandler(paEntity_t* self); + +void despawnWhenOffscreen(paEntity_t* self); + +void pa_destroyEntity(paEntity_t* self, bool respawn); + +void applyDamping(paEntity_t* self); + +void applyGravity(paEntity_t* self); + +void animatePlayer(paEntity_t* self); + +void pa_detectEntityCollisions(paEntity_t* self); + +void pa_playerCollisionHandler(paEntity_t* self, paEntity_t* other); +void pa_enemyCollisionHandler(paEntity_t* self, paEntity_t* other); +void pa_dummyCollisionHandler(paEntity_t* self, paEntity_t* other); + +bool pa_playerTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); +bool pa_enemyTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); +bool pa_dummyTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); + +void dieWhenFallingOffScreen(paEntity_t* self); + +void pa_updateDummy(paEntity_t* self); + +void updateEntityDead(paEntity_t* self); + +void killEnemy(paEntity_t* target); + +void turnAroundAtEdgeOfTileHandler(paEntity_t* self); + +void pa_playerOverlapTileHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty); +void pa_defaultOverlapTileHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty); + +void killPlayer(paEntity_t* self); + +void drawEntityTargetTile(paEntity_t* self); + +bool pa_hitBlockTileCollisionHandler(paEntity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); +void pa_updateBreakBlock(paEntity_t* self); +void pa_updateBlockFragment(paEntity_t* self); + +#endif diff --git a/main/modes/games/pango/paEntityManager.c b/main/modes/games/pango/paEntityManager.c new file mode 100644 index 000000000..de3c819bf --- /dev/null +++ b/main/modes/games/pango/paEntityManager.c @@ -0,0 +1,439 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include + +#include "paEntityManager.h" +#include "esp_random.h" +#include "palette.h" +#include "soundFuncs.h" + +#include "cnfs.h" +#include "fs_wsg.h" + +//============================================================================== +// Constants +//============================================================================== +#define SUBPIXEL_RESOLUTION 4 +#define PA_TO_TILECOORDS(x) ((x) >> PA_TILE_SIZE_IN_POWERS_OF_2) + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeEntityManager(paEntityManager_t* entityManager, paWsgManager_t* wsgManager, paTilemap_t* tilemap, + paGameData_t* gameData, paSoundManager_t* soundManager) +{ + entityManager->wsgManager = wsgManager; + entityManager->entities = calloc(MAX_ENTITIES, sizeof(paEntity_t)); + + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + pa_initializeEntity(&(entityManager->entities[i]), entityManager, tilemap, gameData, soundManager); + } + + entityManager->activeEntities = 0; + entityManager->tilemap = tilemap; + entityManager->gameData = gameData; + entityManager->soundManager = soundManager; + + // entityManager->viewEntity = pa_createPlayer(entityManager, entityManager->tilemap->warps[0].x * 16, + // entityManager->tilemap->warps[0].y * 16); + entityManager->playerEntity = entityManager->viewEntity; + + // entityManager->activeEnemies = 0; + // entityManager->maxEnemies = 3; +} + +void pa_updateEntities(paEntityManager_t* entityManager) +{ + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + if (entityManager->entities[i].active) + { + entityManager->entities[i].updateFunction(&(entityManager->entities[i])); + + /*if (&(entityManager->entities[i]) == entityManager->viewEntity) + { + pa_viewFollowEntity(entityManager->tilemap, &(entityManager->entities[i])); + }*/ + } + } +} + +void pa_deactivateAllEntities(paEntityManager_t* entityManager, bool excludePlayer) +{ + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + paEntity_t* currentEntity = &(entityManager->entities[i]); + currentEntity->active = false; + + // clear out invisible block tiles that are placed for every Break Block object + // if(currentEntity->type == PA_ENTITY_BREAK_BLOCK){ + // pa_setTile(currentEntity->tilemap, PA_TO_TILECOORDS(currentEntity->x >> SUBPIXEL_RESOLUTION), + // PA_TO_TILECOORDS(currentEntity->y >> SUBPIXEL_RESOLUTION), PA_TILE_EMPTY); + // } + + if (currentEntity->type == ENTITY_HIT_BLOCK && currentEntity->jumpPower == PA_TILE_SPAWN_BLOCK_0) + { + entityManager->gameData->remainingEnemies--; + } + + if (excludePlayer && currentEntity == entityManager->playerEntity) + { + currentEntity->active = true; + } + } + + entityManager->activeEntities = 0; + entityManager->aggroEnemies = 0; +} + +void pa_drawEntities(paEntityManager_t* entityManager) +{ + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + paEntity_t currentEntity = entityManager->entities[i]; + + if (currentEntity.active && currentEntity.visible) + { + drawWsg(entityManager->wsgManager->sprites[currentEntity.spriteIndex].wsg, + (currentEntity.x >> SUBPIXEL_RESOLUTION) + - entityManager->wsgManager->sprites[currentEntity.spriteIndex].originX + - entityManager->tilemap->mapOffsetX, + (currentEntity.y >> SUBPIXEL_RESOLUTION) - entityManager->tilemap->mapOffsetY + - entityManager->wsgManager->sprites[currentEntity.spriteIndex].originY, + currentEntity.spriteFlipHorizontal, currentEntity.spriteFlipVertical, 0); + } + } +} + +paEntity_t* pa_findInactiveEntity(paEntityManager_t* entityManager) +{ + if (entityManager->activeEntities == MAX_ENTITIES) + { + return NULL; + }; + + uint8_t entityIndex = 0; + + while (entityManager->entities[entityIndex].active) + { + entityIndex++; + + // Extra safeguard to make sure we don't get stuck here + if (entityIndex > MAX_ENTITIES) + { + return NULL; + } + } + + return &(entityManager->entities[entityIndex]); +} + +void pa_viewFollowEntity(paTilemap_t* tilemap, paEntity_t* entity) +{ + int16_t moveViewByX = (entity->x) >> SUBPIXEL_RESOLUTION; + int16_t moveViewByY = (entity->y > 63616) ? 0 : (entity->y) >> SUBPIXEL_RESOLUTION; + + int16_t centerOfViewX = tilemap->mapOffsetX + 140; + int16_t centerOfViewY = tilemap->mapOffsetY + 120; + + // if(centerOfViewX != moveViewByX) { + moveViewByX -= centerOfViewX; + //} + + // if(centerOfViewY != moveViewByY) { + moveViewByY -= centerOfViewY; + //} + + // if(moveViewByX && moveViewByY){ + pa_scrollTileMap(tilemap, moveViewByX, moveViewByY); + //} +} + +paEntity_t* pa_createEntity(paEntityManager_t* entityManager, uint8_t objectIndex, uint16_t x, uint16_t y) +{ + // if(entityManager->activeEntities == MAX_ENTITIES){ + // return NULL; + // } + + paEntity_t* createdEntity; + + switch (objectIndex) + { + case ENTITY_PLAYER: + createdEntity = pa_createPlayer(entityManager, x, y); + break; + case PA_ENTITY_CRABDOZER: + createdEntity = createCrabdozer(entityManager, x, y); + break; + case ENTITY_HIT_BLOCK: + createdEntity = createHitBlock(entityManager, x, y); + break; + default: + createdEntity = NULL; + } + + // if(createdEntity != NULL) { + // entityManager->activeEntities++; + // } + + return createdEntity; +} + +paEntity_t* pa_createPlayer(paEntityManager_t* entityManager, uint16_t x, uint16_t y) +{ + paEntity_t* entity = pa_findInactiveEntity(entityManager); + + if (entity == NULL) + { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->xMaxSpeed = 40; // 72; Walking + entity->yMaxSpeed = 64; // 72; + entity->xDamping = 2; + entity->yDamping = 2; + entity->gravityEnabled = false; + entity->gravity = 4; + entity->falling = false; + entity->jumpPower = 0; + entity->spriteFlipVertical = false; + entity->hp = 1; + entity->animationTimer = 0; // Used as a cooldown for shooting square wave balls + entity->state = PA_PL_ST_NORMAL; + entity->stateTimer = -1; + + entity->type = ENTITY_PLAYER; + entity->spriteIndex = PA_SP_PLAYER_SOUTH; + entity->updateFunction = &pa_updatePlayer; + entity->collisionHandler = &pa_playerCollisionHandler; + entity->tileCollisionHandler = &pa_playerTileCollisionHandler; + entity->fallOffTileHandler = &defaultFallOffTileHandler; + entity->overlapTileHandler = &pa_playerOverlapTileHandler; + return entity; +} + +paEntity_t* createCrabdozer(paEntityManager_t* entityManager, uint16_t x, uint16_t y) +{ + paEntity_t* entity = pa_findInactiveEntity(entityManager); + + if (entity == NULL) + { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->xMaxSpeed = 132; + entity->yMaxSpeed = 132; + entity->gravityEnabled = false; + entity->gravity = 0; + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + entity->scoreValue = 100; + entity->stateTimer = -1; + entity->tempStateTimer = -1; + entity->stateFlag = false; + entity->baseSpeed = entityManager->gameData->enemyInitialSpeed; + + entity->type = PA_ENTITY_CRABDOZER; + entity->spriteIndex = PA_SP_ENEMY_SOUTH; + entity->facingDirection = PA_DIRECTION_NONE; + entity->state = PA_EN_ST_NORMAL; + entity->stateTimer = 300 + (esp_random() % 600); // Min 5 seconds, max 15 seconds + entity->updateFunction = &updateCrabdozer; + entity->collisionHandler = &pa_enemyCollisionHandler; + entity->tileCollisionHandler = &pa_enemyTileCollisionHandler; + entity->fallOffTileHandler = &defaultFallOffTileHandler; + entity->overlapTileHandler = &pa_defaultOverlapTileHandler; + + return entity; +} + +paEntity_t* pa_createBreakBlock(paEntityManager_t* entityManager, uint16_t x, uint16_t y) +{ + paEntity_t* entity = pa_findInactiveEntity(entityManager); + + if (entity == NULL) + { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->xMaxSpeed = 132; + entity->yMaxSpeed = 132; + entity->gravityEnabled = false; + entity->gravity = 0; + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + entity->scoreValue = 100; + entity->animationTimer = 0; + entity->type = PA_ENTITY_BREAK_BLOCK; + entity->spriteIndex = PA_SP_BREAK_BLOCK; + entity->facingDirection = PA_DIRECTION_NONE; + entity->updateFunction = &pa_updateBreakBlock; + entity->collisionHandler = &pa_dummyCollisionHandler; + entity->tileCollisionHandler = &pa_dummyTileCollisionHandler; + entity->fallOffTileHandler = &defaultFallOffTileHandler; + entity->overlapTileHandler = &pa_defaultOverlapTileHandler; + + pa_setTile(entityManager->tilemap, PA_TO_TILECOORDS(x), PA_TO_TILECOORDS(y), PA_TILE_EMPTY); + + return entity; +} + +paEntity_t* pa_createBlockFragment(paEntityManager_t* entityManager, uint16_t x, uint16_t y) +{ + paEntity_t* entity = pa_findInactiveEntity(entityManager); + + if (entity == NULL) + { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = -64 + (esp_random() % 128); + entity->yspeed = -64 + (esp_random() % 128); + entity->xMaxSpeed = 132; + entity->yMaxSpeed = 132; + entity->gravityEnabled = true; + entity->gravity = 0; + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + entity->scoreValue = 100; + entity->animationTimer = 0; + entity->type = PA_ENTITY_BLOCK_FRAGMENT; + entity->spriteIndex = PA_SP_BLOCK_FRAGMENT; + entity->facingDirection = PA_DIRECTION_NONE; + entity->updateFunction = &pa_updateBlockFragment; + entity->collisionHandler = &pa_dummyCollisionHandler; + entity->tileCollisionHandler = &pa_dummyTileCollisionHandler; + entity->fallOffTileHandler = &defaultFallOffTileHandler; + entity->overlapTileHandler = &pa_defaultOverlapTileHandler; + + return entity; +} + +paEntity_t* createHitBlock(paEntityManager_t* entityManager, uint16_t x, uint16_t y) +{ + paEntity_t* entity = pa_findInactiveEntity(entityManager); + + if (entity == NULL) + { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + entity->homeTileX = PA_TO_TILECOORDS(x); + entity->homeTileY = PA_TO_TILECOORDS(y); + + entity->xspeed = 0; + entity->yspeed = 0; + entity->yDamping = 0; + entity->xMaxSpeed = 132; + entity->yMaxSpeed = 132; + entity->gravityEnabled = true; + entity->gravity = 4; + + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + + entity->type = ENTITY_HIT_BLOCK; + entity->spriteIndex = PA_SP_BLOCK; + entity->animationTimer = 0; + entity->updateFunction = &updateHitBlock; + entity->collisionHandler = &pa_dummyCollisionHandler; + entity->tileCollisionHandler = &pa_hitBlockTileCollisionHandler; + entity->overlapTileHandler = &pa_defaultOverlapTileHandler; + + return entity; +} + +void pa_freeEntityManager(paEntityManager_t* self) +{ + free(self->entities); +} + +paEntity_t* pa_spawnEnemyFromSpawnBlock(paEntityManager_t* entityManager) +{ + paEntity_t* newEnemy = NULL; + + if (entityManager->gameData->remainingEnemies > 0 + && entityManager->activeEnemies < entityManager->gameData->maxActiveEnemies) + { + uint16_t iterations = 0; + while (newEnemy == NULL && iterations < 2) + { + for (uint16_t ty = 1; ty < 14; ty++) + { + for (uint16_t tx = 1; tx < 16; tx++) + { + uint8_t t = pa_getTile(entityManager->tilemap, tx, ty); + + if (t == PA_TILE_SPAWN_BLOCK_0 + && (iterations > 0 || !(esp_random() % entityManager->gameData->remainingEnemies))) + { + newEnemy + = createCrabdozer(entityManager, (tx << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE, + (ty << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE); + + if (newEnemy != NULL) + { + // pa_setTile(entityManager->tilemap, tx, ty, PA_TILE_EMPTY); + newEnemy->state = PA_EN_ST_STUN; + newEnemy->stateTimer = 120; + /*if(entityManager->activeEnemies == 0 || entityManager->gameData->remainingEnemies == 1){ + //The first and last enemies are permanently angry + newEnemy->stateFlag = true; + }*/ + + paEntity_t* newBreakBlock = pa_createBreakBlock( + entityManager, (tx << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE, + (ty << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE); + if (newBreakBlock != NULL) + { + soundPlaySfx(&(entityManager->soundManager->sndSpawn), 3); + } + + entityManager->activeEnemies++; + entityManager->gameData->remainingEnemies--; + return newEnemy; + } + } + } + } + iterations++; + } + } + + return newEnemy; +} \ No newline at end of file diff --git a/main/modes/games/pango/paEntityManager.h b/main/modes/games/pango/paEntityManager.h new file mode 100644 index 000000000..48412900f --- /dev/null +++ b/main/modes/games/pango/paEntityManager.h @@ -0,0 +1,68 @@ +#ifndef _PA_ENTITYMANAGER_H_ +#define _PA_ENTITYMANAGER_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "pango_typedef.h" +#include "paEntity.h" +#include "paTilemap.h" +#include "paGameData.h" +#include "hdw-tft.h" +#include "paSprite.h" +#include "paSoundManager.h" +#include "paWsgManager.h" + +//============================================================================== +// Constants +//============================================================================== +#define MAX_ENTITIES 32 +#define SPRITESET_SIZE 32 + +//============================================================================== +// Structs +//============================================================================== + +struct paEntityManager_t +{ + paEntity_t* entities; + uint8_t activeEntities; + + int16_t activeEnemies; + int16_t aggroEnemies; + + paGameData_t* gameData; + + paEntity_t* viewEntity; + paEntity_t* playerEntity; + + paWsgManager_t* wsgManager; + paTilemap_t* tilemap; + + paSoundManager_t* soundManager; +}; + +//============================================================================== +// Prototypes +//============================================================================== +void pa_initializeEntityManager(paEntityManager_t* entityManager, paWsgManager_t* wsgManager, paTilemap_t* tilemap, + paGameData_t* gameData, paSoundManager_t* soundManager); +void pa_updateEntities(paEntityManager_t* entityManager); +void pa_deactivateAllEntities(paEntityManager_t* entityManager, bool excludePlayer); +void pa_drawEntities(paEntityManager_t* entityManager); +paEntity_t* pa_findInactiveEntity(paEntityManager_t* entityManager); + +void pa_viewFollowEntity(paTilemap_t* tilemap, paEntity_t* entity); +paEntity_t* pa_createEntity(paEntityManager_t* entityManager, uint8_t objectIndex, uint16_t x, uint16_t y); +paEntity_t* pa_createPlayer(paEntityManager_t* entityManager, uint16_t x, uint16_t y); +paEntity_t* createCrabdozer(paEntityManager_t* entityManager, uint16_t x, uint16_t y); +paEntity_t* createHitBlock(paEntityManager_t* entityManager, uint16_t x, uint16_t y); +void pa_freeEntityManager(paEntityManager_t* entityManager); +paEntity_t* pa_spawnEnemyFromSpawnBlock(paEntityManager_t* entityManager); +paEntity_t* pa_createBreakBlock(paEntityManager_t* entityManager, uint16_t x, uint16_t y); +paEntity_t* pa_createBlockFragment(paEntityManager_t* entityManager, uint16_t x, uint16_t y); + +#endif diff --git a/main/modes/games/pango/paGameData.c b/main/modes/games/pango/paGameData.c new file mode 100644 index 000000000..cf394f013 --- /dev/null +++ b/main/modes/games/pango/paGameData.c @@ -0,0 +1,282 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include "paGameData.h" +#include "paTables.h" +#include "paEntityManager.h" +#include "esp_random.h" +#include "hdw-btn.h" +#include "soundFuncs.h" + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeGameData(paGameData_t* gameData, paSoundManager_t* soundManager) +{ + gameData->gameState = 0; + gameData->btnState = 0; + gameData->score = 0; + gameData->lives = 3; + gameData->countdown = 000; + gameData->world = 1; + gameData->level = 1; + gameData->frameCount = 0; + gameData->coins = 0; + gameData->combo = 0; + gameData->comboTimer = 0; + gameData->bgColor = c000; + gameData->initials[0] = 'A'; + gameData->initials[1] = 'A'; + gameData->initials[2] = 'A'; + gameData->rank = 5; + gameData->extraLifeCollected = false; + gameData->checkpoint = 0; + gameData->levelDeaths = 0; + gameData->initialHp = 1; + gameData->debugMode = false; + gameData->continuesUsed = false; + gameData->inGameTimer = 0; + gameData->soundManager = soundManager; +} + +void pa_initializeGameDataFromTitleScreen(paGameData_t* gameData, uint16_t levelIndex) +{ + gameData->gameState = 0; + gameData->btnState = 0; + gameData->score = 0; + gameData->lives = 3; + gameData->countdown = 000; + gameData->frameCount = 0; + gameData->coins = 0; + gameData->combo = 0; + gameData->comboTimer = 0; + gameData->bgColor = c000; + gameData->extraLifeCollected = false; + gameData->checkpoint = 0; + gameData->levelDeaths = 0; + gameData->currentBgm = 0; + gameData->changeBgm = 0; + gameData->initialHp = 1; + gameData->continuesUsed = (gameData->world == 1 && gameData->level == 1) ? false : true; + gameData->inGameTimer = 0; + + pa_setDifficultyLevel(gameData, levelIndex); + + pa_resetGameDataLeds(gameData); +} + +void pa_updateLedsHpMeter(paEntityManager_t* entityManager, paGameData_t* gameData) +{ + if (entityManager->playerEntity == NULL) + { + return; + } + + uint8_t hp = entityManager->playerEntity->hp; + if (hp > 3) + { + hp = 3; + } + + // HP meter led pairs: + // 3 4 + // 2 5 + // 1 6 + for (int32_t i = 1; i < 7; i++) + { + gameData->leds[i].r = 0x80; + gameData->leds[i].g = 0x00; + gameData->leds[i].b = 0x00; + } + + for (int32_t i = 1; i < 1 + hp; i++) + { + gameData->leds[i].r = 0x00; + gameData->leds[i].g = 0x80; + + gameData->leds[7 - i].r = 0x00; + gameData->leds[7 - i].g = 0x80; + } + + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_scorePoints(paGameData_t* gameData, uint16_t points) +{ + gameData->combo++; + + uint32_t comboPoints = points * gameData->combo; + + gameData->score += comboPoints; + gameData->comboScore = comboPoints; + + gameData->comboTimer = (gameData->levelDeaths < 3) ? 240 : 1; +} + +void addCoins(paGameData_t* gameData, uint8_t coins) +{ + gameData->coins += coins; + if (gameData->coins > 99) + { + gameData->lives++; + soundPlaySfx(&(gameData->soundManager->snd1up), BZR_LEFT); + gameData->coins = 0; + } + else + { + soundPlaySfx(&(gameData->soundManager->sndCoin), BZR_LEFT); + } +} + +void updateComboTimer(paGameData_t* gameData) +{ + gameData->comboTimer--; + + if (gameData->comboTimer < 0) + { + gameData->comboTimer = 0; + gameData->combo = 0; + } +} + +void pa_resetGameDataLeds(paGameData_t* gameData) +{ + for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + gameData->leds[i].r = 0; + gameData->leds[i].g = 0; + gameData->leds[i].b = 0; + } + + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_updateLedsShowHighScores(paGameData_t* gameData) +{ + if (((gameData->frameCount) % 10) == 0) + { + for (int32_t i = 0; i < 8; i++) + { + if (((gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) + { + gameData->leds[i].r = 0xF0; + gameData->leds[i].g = 0xF0; + gameData->leds[i].b = 0x00; + } + + if (gameData->leds[i].r > 0) + { + gameData->leds[i].r -= 0x05; + } + + if (gameData->leds[i].g > 0) + { + gameData->leds[i].g -= 0x10; + } + + if (gameData->leds[i].b > 0) + { + gameData->leds[i].b = 0x00; + } + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_updateLedsGameOver(paGameData_t* gameData) +{ + if (((gameData->frameCount) % 10) == 0) + { + for (int32_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + if (((gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) + { + gameData->leds[i].r = 0xF0; + gameData->leds[i].g = 0x00; + gameData->leds[i].b = 0x00; + } + + gameData->leds[i].r -= 0x10; + gameData->leds[i].g = 0x00; + gameData->leds[i].b = 0x00; + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_updateLedsLevelClear(paGameData_t* gameData) +{ + if (((gameData->frameCount) % 10) == 0) + { + for (int32_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + if (((gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) + { + gameData->leds[i].g = (esp_random() % 24) * (10); + gameData->leds[i].b = (esp_random() % 24) * (10); + } + + if (gameData->leds[i].r > 0) + { + gameData->leds[i].r -= 0x10; + } + + if (gameData->leds[i].g > 0) + { + gameData->leds[i].g -= 0x10; + } + + if (gameData->leds[i].b > 0) + { + gameData->leds[i].b -= 0x10; + } + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_updateLedsGameClear(paGameData_t* gameData) +{ + if (((gameData->frameCount) % 10) == 0) + { + for (int32_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + if (((gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) + { + gameData->leds[i].r = (esp_random() % 24) * (10); + gameData->leds[i].g = (esp_random() % 24) * (10); + gameData->leds[i].b = (esp_random() % 24) * (10); + } + + if (gameData->leds[i].r > 0) + { + gameData->leds[i].r -= 0x10; + } + + if (gameData->leds[i].g > 0) + { + gameData->leds[i].g -= 0x10; + } + + if (gameData->leds[i].b > 0) + { + gameData->leds[i].b -= 0x10; + } + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void pa_setDifficultyLevel(paGameData_t* gameData, uint16_t levelIndex) +{ + gameData->remainingEnemies + = masterDifficulty[(levelIndex * MASTER_DIFFICULTY_TABLE_ROW_LENGTH) + TOTAL_ENEMIES_LOOKUP_OFFSET]; + gameData->maxActiveEnemies + = masterDifficulty[(levelIndex * MASTER_DIFFICULTY_TABLE_ROW_LENGTH) + MAX_ACTIVE_ENEMIES_LOOKUP_OFFSET]; + gameData->enemyInitialSpeed + = masterDifficulty[(levelIndex * MASTER_DIFFICULTY_TABLE_ROW_LENGTH) + ENEMY_INITIAL_SPEED_LOOKUP_OFFSET]; + gameData->minAggroEnemies = 1; + gameData->maxAggroEnemies = 1; +} \ No newline at end of file diff --git a/main/modes/games/pango/paGameData.h b/main/modes/games/pango/paGameData.h new file mode 100644 index 000000000..2a93f4547 --- /dev/null +++ b/main/modes/games/pango/paGameData.h @@ -0,0 +1,86 @@ +#ifndef _PA_GAMEDATA_H_ +#define _PA_GAMEDATA_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include "hdw-led.h" +#include "pango_typedef.h" +#include "palette.h" +#include "paSoundManager.h" + +//============================================================================== +// Constants +//============================================================================== + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + int16_t btnState; + int16_t prevBtnState; + uint8_t gameState; + uint8_t changeState; + + uint32_t score; + uint8_t lives; + uint8_t coins; + int16_t countdown; + uint16_t frameCount; + + uint8_t world; + uint8_t level; + + uint16_t combo; + int16_t comboTimer; + uint32_t comboScore; + + bool extraLifeCollected; + uint8_t checkpoint; + uint8_t levelDeaths; + uint8_t initialHp; + + led_t leds[CONFIG_NUM_LEDS]; + + paletteColor_t bgColor; + + char initials[3]; + uint8_t rank; + bool debugMode; + + uint8_t changeBgm; + uint8_t currentBgm; + + bool continuesUsed; + uint32_t inGameTimer; + + int16_t maxActiveEnemies; + int16_t remainingEnemies; + int16_t enemyInitialSpeed; + int16_t minAggroEnemies; + int16_t maxAggroEnemies; + + paSoundManager_t* soundManager; +} paGameData_t; + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeGameData(paGameData_t* gameData, paSoundManager_t* soundManager); +void pa_initializeGameDataFromTitleScreen(paGameData_t* gameData, uint16_t levelIndex); +void pa_updateLedsHpMeter(paEntityManager_t* entityManager, paGameData_t* gameData); +void pa_scorePoints(paGameData_t* gameData, uint16_t points); +void addCoins(paGameData_t* gameData, uint8_t coins); +void updateComboTimer(paGameData_t* gameData); +void pa_resetGameDataLeds(paGameData_t* gameData); +void pa_updateLedsShowHighScores(paGameData_t* gameData); +void pa_updateLedsLevelClear(paGameData_t* gameData); +void pa_updateLedsGameClear(paGameData_t* gameData); +void pa_updateLedsGameOver(paGameData_t* gameData); +void pa_setDifficultyLevel(paGameData_t* gameData, uint16_t levelIndex); + +#endif \ No newline at end of file diff --git a/main/modes/games/pango/paLeveldef.h b/main/modes/games/pango/paLeveldef.h new file mode 100644 index 000000000..b75733b85 --- /dev/null +++ b/main/modes/games/pango/paLeveldef.h @@ -0,0 +1,19 @@ +#ifndef _LEVELDEF_H_ +#define _LEVELDEF_H_ + +//============================================================================== +// Includes +//============================================================================== +#include + +//============================================================================== +// Structs +//============================================================================== +typedef struct +{ + char filename[16]; + uint16_t timeLimit; + uint16_t checkpointTimeLimit; +} paLeveldef_t; + +#endif diff --git a/main/modes/games/pango/paSoundManager.c b/main/modes/games/pango/paSoundManager.c new file mode 100644 index 000000000..9344b388c --- /dev/null +++ b/main/modes/games/pango/paSoundManager.c @@ -0,0 +1,97 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include "paSoundManager.h" + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeSoundManager(paSoundManager_t* self) +{ + loadMidiFile("bgmCastle.mid", &self->bgmCastle, false); + // self->bgmCastle.shouldLoop = true; + + loadMidiFile("bgmDeMAGio.mid", &self->bgmDemagio, false); + // self->bgmDemagio.shouldLoop = true; + + loadMidiFile("bgmGameStart.mid", &self->bgmGameStart, false); + loadMidiFile("bgmIntro.mid", &self->bgmIntro, false); + loadMidiFile("bgmNameEntry.mid", &self->bgmNameEntry, false); + // self->bgmNameEntry.shouldLoop = true; + + loadMidiFile("bgmSmooth.mid", &self->bgmSmooth, false); + // self->bgmSmooth.shouldLoop = true; + + loadMidiFile("bgmUnderground.mid", &self->bgmUnderground, false); + // self->bgmUnderground.shouldLoop = true; + + loadMidiFile("snd1up.mid", &self->snd1up, false); + // loadMidiFile("sndBreak.mid", &self->sndBreak, false); + loadMidiFile("sndCheckpoint.mid", &self->sndCheckpoint, false); + loadMidiFile("sndCoin.mid", &self->sndCoin, false); + loadMidiFile("sndDie.mid", &self->sndDie, false); + loadMidiFile("bgmGameOver.mid", &self->bgmGameOver, false); + loadMidiFile("sndBlockStop.mid", &self->sndHit, false); + loadMidiFile("sndSquish.mid", &self->sndHurt, false); + loadMidiFile("sndJump1.mid", &self->sndJump1, false); + loadMidiFile("sndJump2.mid", &self->sndJump2, false); + loadMidiFile("sndJump3.mid", &self->sndJump3, false); + loadMidiFile("sndLevelClearA.mid", &self->sndLevelClearA, false); + loadMidiFile("sndLevelClearB.mid", &self->sndLevelClearB, false); + loadMidiFile("sndLevelClearC.mid", &self->sndLevelClearC, false); + loadMidiFile("sndLevelClearD.mid", &self->sndLevelClearD, false); + loadMidiFile("sndLevelClearS.mid", &self->sndLevelClearS, false); + loadMidiFile("sndMenuConfirm.mid", &self->sndMenuConfirm, false); + loadMidiFile("sndMenuDeny.mid", &self->sndMenuDeny, false); + loadMidiFile("sndMenuSelect.mid", &self->sndMenuSelect, false); + loadMidiFile("sndOutOfTime.mid", &self->sndOuttaTime, false); + loadMidiFile("sndPause.mid", &self->sndPause, false); + loadMidiFile("sndPowerUp.mid", &self->sndPowerUp, false); + loadMidiFile("sndSlide.mid", &self->sndSquish, false); + // loadMidiFile("sndTally.mid", &self->sndTally, false); + loadMidiFile("sndWarp.mid", &self->sndWarp, false); + loadMidiFile("sndWaveBall.mid", &self->sndWaveBall, false); + + loadMidiFile("sndSpawn.mid", &self->sndSpawn, false); +} + +void pa_freeSoundManager(paSoundManager_t* self) +{ + unloadMidiFile(&self->bgmCastle); + unloadMidiFile(&self->bgmDemagio); + unloadMidiFile(&self->bgmGameStart); + unloadMidiFile(&self->bgmIntro); + unloadMidiFile(&self->bgmNameEntry); + unloadMidiFile(&self->bgmSmooth); + unloadMidiFile(&self->bgmUnderground); + unloadMidiFile(&self->snd1up); + unloadMidiFile(&self->sndBreak); + unloadMidiFile(&self->sndCheckpoint); + unloadMidiFile(&self->sndCoin); + unloadMidiFile(&self->sndDie); + unloadMidiFile(&self->bgmGameOver); + unloadMidiFile(&self->sndHit); + unloadMidiFile(&self->sndHurt); + unloadMidiFile(&self->sndJump1); + unloadMidiFile(&self->sndJump2); + unloadMidiFile(&self->sndJump3); + unloadMidiFile(&self->sndLevelClearA); + unloadMidiFile(&self->sndLevelClearB); + unloadMidiFile(&self->sndLevelClearC); + unloadMidiFile(&self->sndLevelClearD); + unloadMidiFile(&self->sndLevelClearS); + unloadMidiFile(&self->sndMenuConfirm); + unloadMidiFile(&self->sndMenuDeny); + unloadMidiFile(&self->sndMenuSelect); + unloadMidiFile(&self->sndOuttaTime); + unloadMidiFile(&self->sndPause); + unloadMidiFile(&self->sndPowerUp); + unloadMidiFile(&self->sndSquish); + unloadMidiFile(&self->sndTally); + unloadMidiFile(&self->sndWarp); + unloadMidiFile(&self->sndWaveBall); + + unloadMidiFile(&self->sndSpawn); +} \ No newline at end of file diff --git a/main/modes/games/pango/paSoundManager.h b/main/modes/games/pango/paSoundManager.h new file mode 100644 index 000000000..48c625ac5 --- /dev/null +++ b/main/modes/games/pango/paSoundManager.h @@ -0,0 +1,65 @@ +#ifndef _PA_SOUNDMANAGER_H_ +#define _PA_SOUNDMANAGER_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include + +//============================================================================== +// Constants +//============================================================================== + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + midiFile_t bgmDemagio; + midiFile_t bgmIntro; + midiFile_t bgmSmooth; + midiFile_t bgmUnderground; + midiFile_t bgmCastle; + midiFile_t bgmGameStart; + midiFile_t sndDie; + midiFile_t sndMenuSelect; + midiFile_t sndMenuConfirm; + midiFile_t sndMenuDeny; + midiFile_t sndPause; + midiFile_t sndHit; + midiFile_t sndSquish; + midiFile_t sndBreak; + midiFile_t sndCoin; + midiFile_t sndPowerUp; + midiFile_t sndJump1; + midiFile_t sndJump2; + midiFile_t sndJump3; + midiFile_t sndWarp; + midiFile_t sndHurt; + midiFile_t sndWaveBall; + midiFile_t snd1up; + midiFile_t sndCheckpoint; + midiFile_t sndLevelClearD; + midiFile_t sndLevelClearC; + midiFile_t sndLevelClearB; + midiFile_t sndLevelClearA; + midiFile_t sndLevelClearS; + midiFile_t sndTally; + midiFile_t bgmNameEntry; + midiFile_t bgmGameOver; + midiFile_t sndOuttaTime; + + midiFile_t sndSpawn; +} paSoundManager_t; + +//============================================================================== +// Functions +//============================================================================== +void pa_initializeSoundManager(paSoundManager_t* self); +void pa_freeSoundManager(paSoundManager_t* self); + +#endif \ No newline at end of file diff --git a/main/modes/games/pango/paSprite.h b/main/modes/games/pango/paSprite.h new file mode 100644 index 000000000..5898a8ee5 --- /dev/null +++ b/main/modes/games/pango/paSprite.h @@ -0,0 +1,20 @@ +#ifndef _PA_SPRITE_H_ +#define _PA_SPRITE_H_ + +//============================================================================== +// Includes +//============================================================================== +#include +#include "wsg.h" + +//============================================================================== +// Structs +//============================================================================== +typedef struct +{ + wsg_t* wsg; + int16_t originX; + int16_t originY; +} paSprite_t; + +#endif diff --git a/main/modes/games/pango/paTables.h b/main/modes/games/pango/paTables.h new file mode 100644 index 000000000..ebdd6c82c --- /dev/null +++ b/main/modes/games/pango/paTables.h @@ -0,0 +1,69 @@ +#ifndef _PA_TABLES_H_ +#define _PA_TABLES_H_ + +//============================================================================== +// Includes +//============================================================================== +#include + +//============================================================================== +// Look Up Tables +//============================================================================== + +#define DEFAULT_ENEMY_SPAWN_LOCATION_TABLE_LENGTH 20 + +#define DEFAULT_ENEMY_SPAWN_LOCATION_TX_LOOKUP_OFFSET 0 +#define DEFAULT_ENEMY_SPAWN_LOCATION_TY_LOOKUP_OFFSET 1 +#define DEFAULT_ENEMY_SPAWN_LOCATION_ROW_LENGTH 2 + +static const uint8_t + defaultEnemySpawnLocations[DEFAULT_ENEMY_SPAWN_LOCATION_TABLE_LENGTH * DEFAULT_ENEMY_SPAWN_LOCATION_ROW_LENGTH] + = { + + // tx,ty + 1, 1, 15, 1, 1, 13, 15, 13, 8, 1, 15, 7, 8, 13, 1, 7, 4, 1, 12, 1, + 15, 4, 15, 10, 12, 13, 4, 13, 1, 10, 1, 4, 6, 1, 10, 1, 6, 13, 10, 13}; + +/*#define MASTER_DIFFICULTY2_TABLE_LENGTH 6 + +#define TOTAL_ENEMIES_LOOKUP_OFFSET 0 +#define INITIAL_ACTIVE_ENEMIES_LOOKUP_OFFSET 1 +#define MAX_ACTIVE_ENEMIES_LOOKUP_OFFSET 2 +#define INITIAL_AGGRESSIVE_ENEMIES_LOOKUP_OFFET 3 +#define MAX_AGGRESSIVE_ENEMIES_LOOKUP_OFFSET 4 +#define INITIAL_ENEMY_SPEED 5 +#define MAX_ENEMY_SPEED 6 +#define MASTER_DIFFICULTY2_TABLE_ROW_LENGTH 7 + +static const int16_t masterDifficulty2[MASTER_DIFFICULTY2_TABLE_LENGTH * MASTER_DIFFICULTY2_TABLE_ROW_LENGTH] = { + +//In-level difficulty curve: +//Starting at 15 seconds, the leftmost parameter will increment every 10 seconds until it reaches the max, +//Then the same will happen with the next parameter in the table until all reach the end-state for the current level. + +// Total initial max initial max initial max +// enemies, active, active, aggressive, aggressive, speed, speed + 5, 1, 2, 0, 1, 12, 12, + 5, 2, 3, 0, 1, 12, 12, + 6, 3, 3, 0, 2, 12, 14, + 6, 3, 3, 0, 3, 12, 14, + 7, 3, 3, 0, 3, 13, 15, + 7, 4, 4, 0, 1, 8, 10, +};*/ + +#define MASTER_DIFFICULTY_TABLE_LENGTH 16 + +#define TOTAL_ENEMIES_LOOKUP_OFFSET 0 +#define MAX_ACTIVE_ENEMIES_LOOKUP_OFFSET 1 +#define ENEMY_INITIAL_SPEED_LOOKUP_OFFSET 2 +#define MASTER_DIFFICULTY_TABLE_ROW_LENGTH 3 + +static const int16_t masterDifficulty[MASTER_DIFFICULTY_TABLE_LENGTH * MASTER_DIFFICULTY_TABLE_ROW_LENGTH] = { + + // Total max min max + // enemies, active, speed aggro, aggro, + 5, 2, 12, 5, 3, 12, 6, 3, 13, 7, 4, 10, 8, 3, 13, 8, 3, 14, 8, 3, 15, 7, 2, 16, + 8, 3, 15, 8, 3, 16, 9, 3, 16, 8, 4, 12, 9, 3, 16, 8, 3, 17, 10, 4, 14, 12, 1, 18, +}; + +#endif \ No newline at end of file diff --git a/main/modes/games/pango/paTilemap.c b/main/modes/games/pango/paTilemap.c new file mode 100644 index 000000000..92808b766 --- /dev/null +++ b/main/modes/games/pango/paTilemap.c @@ -0,0 +1,429 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include +#include + +#include "fs_wsg.h" +#include "paTilemap.h" +#include "paLeveldef.h" +#include "esp_random.h" + +#include "cnfs.h" + +//============================================================================== +// Function Prototypes +//============================================================================== + +// bool isInteractive(uint8_t tileId); + +//============================================================================== +// Functions +//============================================================================== + +void pa_initializeTileMap(paTilemap_t* tilemap, paWsgManager_t* wsgManager) +{ + tilemap->mapOffsetX = 0; + tilemap->mapOffsetY = 0; + + tilemap->tileSpawnEnabled = false; + tilemap->executeTileSpawnColumn = -1; + tilemap->executeTileSpawnRow = -1; + + tilemap->animationFrame = 0; + tilemap->animationTimer = 23; + + tilemap->wsgManager = wsgManager; +} + +void pa_drawTileMap(paTilemap_t* tilemap) +{ + tilemap->animationTimer--; + if (tilemap->animationTimer < 0) + { + tilemap->animationFrame = ((tilemap->animationFrame + 1) % 3); + tilemap->animationTimer = 23; + } + + for (int32_t y = (tilemap->mapOffsetY >> PA_TILE_SIZE_IN_POWERS_OF_2); + y < (tilemap->mapOffsetY >> PA_TILE_SIZE_IN_POWERS_OF_2) + PA_TILE_MAP_DISPLAY_HEIGHT_TILES; y++) + { + if (y >= tilemap->mapHeight) + { + break; + } + + for (int32_t x = (tilemap->mapOffsetX >> PA_TILE_SIZE_IN_POWERS_OF_2); + x < (tilemap->mapOffsetX >> PA_TILE_SIZE_IN_POWERS_OF_2) + PA_TILE_MAP_DISPLAY_WIDTH_TILES; x++) + { + if (x >= tilemap->mapWidth) + { + break; + } + else if (x < 0) + { + continue; + } + + uint8_t tile = tilemap->map[(y * tilemap->mapWidth) + x]; + + if (tile < PA_TILE_WALL_0 || tile == PA_TILE_INVISIBLE_BLOCK) + { + continue; + } + + // Test animated tiles + if (tile == PA_TILE_SPAWN_BLOCK_0 || tile == PA_TILE_BONUS_BLOCK_0) + { + tile += tilemap->animationFrame; + } + + // Draw only non-garbage tiles + if (tile > 0 && tile < 13) + { + if (pa_needsTransparency(tile)) + { + // drawWsgSimpleFast(&tilemap->tiles[tile - 32], x * PA_TILE_SIZE - tilemap->mapOffsetX, y * + // PA_TILE_SIZE - tilemap->mapOffsetY); + drawWsgSimple(tilemap->wsgManager->tiles[tile - 1], x * PA_TILE_SIZE - tilemap->mapOffsetX, + y * PA_TILE_SIZE - tilemap->mapOffsetY); + } + else + { + drawWsgTile(tilemap->wsgManager->tiles[tile - 1], x * PA_TILE_SIZE - tilemap->mapOffsetX, + y * PA_TILE_SIZE - tilemap->mapOffsetY); + } + } + else if (tile > 127 && tilemap->tileSpawnEnabled + && (tilemap->executeTileSpawnColumn == x || tilemap->executeTileSpawnRow == y + || tilemap->executeTileSpawnAll)) + { + pa_tileSpawnEntity(tilemap, tile - 128, x, y); + } + } + } + + tilemap->executeTileSpawnAll = 0; +} + +void pa_scrollTileMap(paTilemap_t* tilemap, int16_t x, int16_t y) +{ + if (x != 0) + { + uint8_t oldTx = tilemap->mapOffsetX >> PA_TILE_SIZE_IN_POWERS_OF_2; + tilemap->mapOffsetX = CLAMP(tilemap->mapOffsetX + x, tilemap->minMapOffsetX, tilemap->maxMapOffsetX); + uint8_t newTx = tilemap->mapOffsetX >> PA_TILE_SIZE_IN_POWERS_OF_2; + + if (newTx > oldTx) + { + tilemap->executeTileSpawnColumn = oldTx + PA_TILE_MAP_DISPLAY_WIDTH_TILES; + } + else if (newTx < oldTx) + { + tilemap->executeTileSpawnColumn = newTx; + } + else + { + tilemap->executeTileSpawnColumn = -1; + } + } + + if (y != 0) + { + uint8_t oldTy = tilemap->mapOffsetY >> PA_TILE_SIZE_IN_POWERS_OF_2; + tilemap->mapOffsetY = CLAMP(tilemap->mapOffsetY + y, tilemap->minMapOffsetY, tilemap->maxMapOffsetY); + uint8_t newTy = tilemap->mapOffsetY >> PA_TILE_SIZE_IN_POWERS_OF_2; + + if (newTy > oldTy) + { + tilemap->executeTileSpawnRow = oldTy + PA_TILE_MAP_DISPLAY_HEIGHT_TILES; + } + else if (newTy < oldTy) + { + tilemap->executeTileSpawnRow = newTy; + } + else + { + tilemap->executeTileSpawnRow = -1; + } + } +} + +bool pa_loadMapFromFile(paTilemap_t* tilemap, const char* name) +{ + if (tilemap->map != NULL) + { + free(tilemap->map); + } + + size_t sz; + uint8_t* buf = cnfsReadFile(name, &sz, false); + + if (NULL == buf) + { + ESP_LOGE("MAP", "Failed to read %s", name); + return false; + } + + uint8_t width = buf[0]; + uint8_t height = buf[1]; + + tilemap->map = (uint8_t*)heap_caps_calloc(width * height, sizeof(uint8_t), MALLOC_CAP_SPIRAM); + memcpy(tilemap->map, &buf[2], width * height); + + tilemap->mapWidth = width; + tilemap->mapHeight = height; + + tilemap->minMapOffsetX = 0; + tilemap->maxMapOffsetX = width * PA_TILE_SIZE - PA_TILE_MAP_DISPLAY_WIDTH_PIXELS; + + tilemap->minMapOffsetY = 0; + tilemap->maxMapOffsetY = height * PA_TILE_SIZE - PA_TILE_MAP_DISPLAY_HEIGHT_PIXELS; + + /*for (uint16_t i = 0; i < 16; i++) + { + tilemap->warps[i].x = buf[2 + width * height + i * 2]; + tilemap->warps[i].y = buf[2 + width * height + i * 2 + 1]; + }*/ + + free(buf); + + return true; +} + +void pa_tileSpawnEntity(paTilemap_t* tilemap, uint8_t objectIndex, uint8_t tx, uint8_t ty) +{ + paEntity_t* entityCreated + = pa_createEntity(tilemap->entityManager, objectIndex, (tx << PA_TILE_SIZE_IN_POWERS_OF_2) + 8, + (ty << PA_TILE_SIZE_IN_POWERS_OF_2) + 8); + + if (entityCreated != NULL) + { + entityCreated->homeTileX = tx; + entityCreated->homeTileY = ty; + tilemap->map[ty * tilemap->mapWidth + tx] = 0; + } +} + +uint8_t pa_getTile(paTilemap_t* tilemap, uint8_t tx, uint8_t ty) +{ + // ty = CLAMP(ty, 0, tilemap->mapHeight - 1); + + if (/*ty < 0 ||*/ ty >= tilemap->mapHeight) + { + // ty = 0; + return 0; + } + + if (/*tx < 0 ||*/ tx >= tilemap->mapWidth) + { + return 0; + } + + return tilemap->map[ty * tilemap->mapWidth + tx]; +} + +void pa_setTile(paTilemap_t* tilemap, uint8_t tx, uint8_t ty, uint8_t newTileId) +{ + // ty = CLAMP(ty, 0, tilemap->mapHeight - 1); + + if (ty >= tilemap->mapHeight || tx >= tilemap->mapWidth) + { + return; + } + + tilemap->map[ty * tilemap->mapWidth + tx] = newTileId; +} + +bool pa_isSolid(uint8_t tileId) +{ + switch (tileId) + { + case PA_TILE_EMPTY: + return false; + break; + default: + return true; + } +} + +// bool isInteractive(uint8_t tileId) +// { +// return tileId > PA_TILE_INVISIBLE_BLOCK && tileId < PA_TILE_BG_GOAL_ZONE; +// } + +void pa_unlockScrolling(paTilemap_t* tilemap) +{ + tilemap->minMapOffsetX = 0; + tilemap->maxMapOffsetX = tilemap->mapWidth * PA_TILE_SIZE - PA_TILE_MAP_DISPLAY_WIDTH_PIXELS; + + tilemap->minMapOffsetY = 0; + tilemap->maxMapOffsetY = tilemap->mapHeight * PA_TILE_SIZE - PA_TILE_MAP_DISPLAY_HEIGHT_PIXELS; +} + +bool pa_needsTransparency(uint8_t tileId) +{ + switch (tileId) + { + /*case PA_TILE_BOUNCE_BLOCK: + case PA_TILE_GIRDER: + case PA_TILE_CONTAINER_1 ... PA_TILE_CONTAINER_3: + case PA_TILE_COIN_1 ... PA_TILE_COIN_3: + case PA_TILE_LADDER: + case PA_TILE_BG_GOAL_ZONE ... PA_TILE_BG_CLOUD_D: + return true; + case PA_TILE_BG_CLOUD: + return false; + case PA_TILE_BG_TALL_GRASS ... PA_TILE_BG_MOUNTAIN_R: + return true; + case PA_TILE_BG_MOUNTAIN ... PA_TILE_BG_METAL: + return false; + case PA_TILE_BG_CHAINS: + return true; + case PA_TILE_BG_WALL: + return false;*/ + default: + return true; + } +} + +void pa_freeTilemap(paTilemap_t* tilemap) +{ + free(tilemap->map); +} + +void pa_generateMaze(paTilemap_t* tilemap) +{ + int32_t tx = 1; + int32_t ty = 13; + pa_setTile(tilemap, tx, ty, PA_TILE_EMPTY); + + while (ty > 1) + { + tx = 1; + while (tx < 15) + { + if (!pa_getTile(tilemap, tx, ty) && !pa_genPathContinue(tilemap, tx, ty)) + { + pa_genMakePath(tilemap, tx, ty); + } + tx += 2; + } + ty -= 2; + } +} + +bool pa_genPathContinue(paTilemap_t* tilemap, uint32_t x, uint32_t y) +{ + if (pa_getTile(tilemap, x, y - 2)) + { + return false; + } + if (pa_getTile(tilemap, x, y + 2)) + { + return false; + } + if (pa_getTile(tilemap, x + 2, y)) + { + return false; + } + if (pa_getTile(tilemap, x - 2, y)) + { + return false; + } + + return true; +} + +void pa_genMakePath(paTilemap_t* tilemap, uint32_t x, uint32_t y) +{ + bool done = 0; + uint32_t nx = x; + uint32_t ny = y; + + while (!done) + { + uint32_t r = esp_random() % 4; + + switch (r) + { + case 0: + if (pa_getTile(tilemap, nx, ny - 2)) + { + pa_setTile(tilemap, nx, ny - 1, 0); + pa_setTile(tilemap, nx, ny - 2, 0); + ny -= 2; + } + break; + case 1: + if (pa_getTile(tilemap, nx, ny + 2)) + { + pa_setTile(tilemap, nx, ny + 1, 0); + pa_setTile(tilemap, nx, ny + 2, 0); + ny += 2; + } + break; + case 2: + if (pa_getTile(tilemap, nx - 2, ny)) + { + pa_setTile(tilemap, nx - 1, ny, 0); + pa_setTile(tilemap, nx - 2, ny, 0); + nx -= 2; + } + break; + case 3: + if (pa_getTile(tilemap, nx + 2, ny)) + { + pa_setTile(tilemap, nx + 1, ny, 0); + pa_setTile(tilemap, nx + 2, ny, 0); + nx += 2; + } + break; + } + + done = pa_genPathContinue(tilemap, nx, ny); + } +} + +void pa_placeEnemySpawns(paTilemap_t* tilemap) +{ + int16_t enemySpawnsToPlace = tilemap->entityManager->gameData->remainingEnemies; + int16_t enemiesPlaced = 0; + bool previouslyPlaced = false; + int16_t iterations = 0; + + // Place enemy spawn blocks + while (enemySpawnsToPlace > 0 && iterations < 16) + { + for (uint16_t ty = 1; ty < 13; ty++) + { + for (uint16_t tx = 1; tx < 15; tx++) + { + if (enemySpawnsToPlace <= 0) + { + break; + } + + uint8_t t = pa_getTile(tilemap, tx, ty); + + if (t == PA_TILE_BLOCK && !previouslyPlaced && !(esp_random() % 15)) + { + pa_setTile(tilemap, tx, ty, PA_TILE_SPAWN_BLOCK_0); + enemySpawnsToPlace--; + enemiesPlaced++; + previouslyPlaced = true; + } + else + { + previouslyPlaced = false; + } + } + } + iterations++; + } + + tilemap->entityManager->gameData->remainingEnemies = enemiesPlaced; +} \ No newline at end of file diff --git a/main/modes/games/pango/paTilemap.h b/main/modes/games/pango/paTilemap.h new file mode 100644 index 000000000..66ea92770 --- /dev/null +++ b/main/modes/games/pango/paTilemap.h @@ -0,0 +1,111 @@ +#ifndef _PA_TILE_MAP_H_ +#define _PA_TILE_MAP_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "wsg.h" +#include "paWsgManager.h" +#include "macros.h" +#include "pango_typedef.h" +#include "paEntityManager.h" + +//============================================================================== +// Constants +//============================================================================== +#define PA_TILE_MAP_DISPLAY_WIDTH_PIXELS 280 // The screen size +#define PA_TILE_MAP_DISPLAY_HEIGHT_PIXELS 240 // The screen size +#define PA_TILE_MAP_DISPLAY_WIDTH_TILES 19 // The screen size in tiles + 1 +#define PA_TILE_MAP_DISPLAY_HEIGHT_TILES 16 // The screen size in tiles + 1 + +#define PA_TILE_SIZE 16 +#define PA_HALF_TILE_SIZE 8 +#define PA_TILE_SIZE_IN_POWERS_OF_2 4 + +#define PA_TILE_SET_SIZE 15 + +//============================================================================== +// Enums +//============================================================================== +typedef enum +{ + PA_TILE_EMPTY, + PA_TILE_WALL_0, + PA_TILE_WALL_1, + PA_TILE_WALL_2, + PA_TILE_WALL_3, + PA_TILE_WALL_4, + PA_TILE_WALL_5, + PA_TILE_WALL_6, + PA_TILE_WALL_7, + PA_TILE_BLOCK, + PA_TILE_SPAWN_BLOCK_0, + PA_TILE_SPAWN_BLOCK_1, + PA_TILE_SPAWN_BLOCK_2, + PA_TILE_BONUS_BLOCK_0, + PA_TILE_BONUS_BLOCK_1, + PA_TILE_BONUS_BLOCK_2, + PA_TILE_INVISIBLE_BLOCK +} PA_TILE_Index_t; + +//============================================================================== +// Structs +//============================================================================== +typedef struct +{ + uint8_t x; + uint8_t y; +} pa_warp_t; +struct paTilemap_t +{ + paWsgManager_t* wsgManager; + + uint8_t* map; + uint8_t mapWidth; + uint8_t mapHeight; + + pa_warp_t warps[16]; + + int16_t mapOffsetX; + int16_t mapOffsetY; + + int16_t minMapOffsetX; + int16_t maxMapOffsetX; + int16_t minMapOffsetY; + int16_t maxMapOffsetY; + + bool tileSpawnEnabled; + int16_t executeTileSpawnColumn; + int16_t executeTileSpawnRow; + bool executeTileSpawnAll; + + paEntityManager_t* entityManager; + + uint8_t animationFrame; + int16_t animationTimer; +}; + +//============================================================================== +// Prototypes +//============================================================================== +void pa_initializeTileMap(paTilemap_t* tilemap, paWsgManager_t* wsgManager); +void pa_drawTileMap(paTilemap_t* tilemap); +void pa_scrollTileMap(paTilemap_t* tilemap, int16_t x, int16_t y); +void pa_drawTile(paTilemap_t* tilemap, uint8_t tileId, int16_t x, int16_t y); +bool pa_loadMapFromFile(paTilemap_t* tilemap, const char* name); +void pa_tileSpawnEntity(paTilemap_t* tilemap, uint8_t objectIndex, uint8_t tx, uint8_t ty); +uint8_t pa_getTile(paTilemap_t* tilemap, uint8_t tx, uint8_t ty); +void pa_setTile(paTilemap_t* tilemap, uint8_t tx, uint8_t ty, uint8_t newTileId); +bool pa_isSolid(uint8_t tileId); +void pa_unlockScrolling(paTilemap_t* tilemap); +bool pa_needsTransparency(uint8_t tileId); +void pa_freeTilemap(paTilemap_t* tilemap); +void pa_generateMaze(paTilemap_t* tilemap); +bool pa_genPathContinue(paTilemap_t* tilemap, uint32_t x, uint32_t y); +void pa_genMakePath(paTilemap_t* tilemap, uint32_t x, uint32_t y); +void pa_placeEnemySpawns(paTilemap_t* tilemap); + +#endif diff --git a/main/modes/games/pango/paWsgManager.c b/main/modes/games/pango/paWsgManager.c new file mode 100644 index 000000000..6ba690e25 --- /dev/null +++ b/main/modes/games/pango/paWsgManager.c @@ -0,0 +1,295 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "fs_wsg.h" +#include "paWsgManager.h" + +//============================================================================== +// Functions +//============================================================================== + +void pa_initializeWsgManager(paWsgManager_t* self) +{ + pa_loadWsgs(self); + pa_initializeSprites(self); + pa_initializeTiles(self); +} + +void pa_freeWsgManager(paWsgManager_t* self) +{ + for (uint16_t i = 0; i < PA_WSGS_SIZE; i++) + { + freeWsg(&self->wsgs[i]); + } +} + +void pa_loadWsgs(paWsgManager_t* self) +{ + loadWsg("pa-100.wsg", &self->wsgs[PA_WSG_PANGO_SOUTH], false); + loadWsg("pa-101.wsg", &self->wsgs[PA_WSG_PANGO_WALK_SOUTH], false); + loadWsg("pa-102.wsg", &self->wsgs[PA_WSG_PANGO_NORTH], false); + loadWsg("pa-103.wsg", &self->wsgs[PA_WSG_PANGO_WALK_NORTH], false); + loadWsg("pa-104.wsg", &self->wsgs[PA_WSG_PANGO_SIDE], false); + loadWsg("pa-106.wsg", &self->wsgs[PA_WSG_PANGO_WALK_SIDE_1], false); + loadWsg("pa-105.wsg", &self->wsgs[PA_WSG_PANGO_WALK_SIDE_2], false); + loadWsg("pa-107.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_SOUTH_1], false); + loadWsg("pa-108.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_SOUTH_2], false); + loadWsg("pa-109.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_NORTH_1], false); + loadWsg("pa-110.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_NORTH_2], false); + loadWsg("pa-111.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_SIDE_1], false); + loadWsg("pa-112.wsg", &self->wsgs[PA_WSG_PANGO_PUSH_SIDE_2], false); + loadWsg("pa-113.wsg", &self->wsgs[PA_WSG_PANGO_HURT], false); + loadWsg("pa-114.wsg", &self->wsgs[PA_WSG_PANGO_WIN], false); + loadWsg("pa-015.wsg", &self->wsgs[PA_WSG_PANGO_ICON], false); + loadWsg("po-000.wsg", &self->wsgs[PA_WSG_PO_SOUTH], false); + loadWsg("po-001.wsg", &self->wsgs[PA_WSG_PO_WALK_SOUTH], false); + loadWsg("po-002.wsg", &self->wsgs[PA_WSG_PO_NORTH], false); + loadWsg("po-003.wsg", &self->wsgs[PA_WSG_PO_WALK_NORTH], false); + loadWsg("po-004.wsg", &self->wsgs[PA_WSG_PO_SIDE], false); + loadWsg("po-006.wsg", &self->wsgs[PA_WSG_PO_WALK_SIDE_1], false); + loadWsg("po-005.wsg", &self->wsgs[PA_WSG_PO_WALK_SIDE_2], false); + loadWsg("po-007.wsg", &self->wsgs[PA_WSG_PO_PUSH_SOUTH_1], false); + loadWsg("po-008.wsg", &self->wsgs[PA_WSG_PO_PUSH_SOUTH_2], false); + loadWsg("po-009.wsg", &self->wsgs[PA_WSG_PO_PUSH_NORTH_1], false); + loadWsg("po-010.wsg", &self->wsgs[PA_WSG_PO_PUSH_NORTH_2], false); + loadWsg("po-011.wsg", &self->wsgs[PA_WSG_PO_PUSH_SIDE_1], false); + loadWsg("po-012.wsg", &self->wsgs[PA_WSG_PO_PUSH_SIDE_2], false); + loadWsg("po-013.wsg", &self->wsgs[PA_WSG_PO_HURT], false); + loadWsg("po-014.wsg", &self->wsgs[PA_WSG_PO_WIN], false); + loadWsg("pa-015.wsg", &self->wsgs[PA_WSG_PO_ICON], false); + loadWsg("px-000.wsg", &self->wsgs[PA_WSG_PIXEL_SOUTH], false); + loadWsg("px-001.wsg", &self->wsgs[PA_WSG_PIXEL_WALK_SOUTH], false); + loadWsg("px-002.wsg", &self->wsgs[PA_WSG_PIXEL_NORTH], false); + loadWsg("px-003.wsg", &self->wsgs[PA_WSG_PIXEL_WALK_NORTH], false); + loadWsg("px-004.wsg", &self->wsgs[PA_WSG_PIXEL_SIDE], false); + loadWsg("px-006.wsg", &self->wsgs[PA_WSG_PIXEL_WALK_SIDE_1], false); + loadWsg("px-005.wsg", &self->wsgs[PA_WSG_PIXEL_WALK_SIDE_2], false); + loadWsg("px-007.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_SOUTH_1], false); + loadWsg("px-008.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_SOUTH_2], false); + loadWsg("px-009.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_NORTH_1], false); + loadWsg("px-010.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_NORTH_2], false); + loadWsg("px-011.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_SIDE_1], false); + loadWsg("px-012.wsg", &self->wsgs[PA_WSG_PIXEL_PUSH_SIDE_2], false); + loadWsg("px-013.wsg", &self->wsgs[PA_WSG_PIXEL_HURT], false); + loadWsg("px-014.wsg", &self->wsgs[PA_WSG_PIXEL_WIN], false); + loadWsg("px-015.wsg", &self->wsgs[PA_WSG_PIXEL_ICON], false); + loadWsg("kr-000.wsg", &self->wsgs[PA_WSG_GIRL_SOUTH], false); + loadWsg("kr-001.wsg", &self->wsgs[PA_WSG_GIRL_WALK_SOUTH], false); + loadWsg("kr-002.wsg", &self->wsgs[PA_WSG_GIRL_NORTH], false); + loadWsg("kr-003.wsg", &self->wsgs[PA_WSG_GIRL_WALK_NORTH], false); + loadWsg("kr-004.wsg", &self->wsgs[PA_WSG_GIRL_SIDE], false); + loadWsg("kr-006.wsg", &self->wsgs[PA_WSG_GIRL_WALK_SIDE_1], false); + loadWsg("kr-005.wsg", &self->wsgs[PA_WSG_GIRL_WALK_SIDE_2], false); + loadWsg("kr-007.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_SOUTH_1], false); + loadWsg("kr-008.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_SOUTH_2], false); + loadWsg("kr-009.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_NORTH_1], false); + loadWsg("kr-010.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_NORTH_2], false); + loadWsg("kr-011.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_SIDE_1], false); + loadWsg("kr-012.wsg", &self->wsgs[PA_WSG_GIRL_PUSH_SIDE_2], false); + loadWsg("kr-013.wsg", &self->wsgs[PA_WSG_GIRL_HURT], false); + loadWsg("kr-014.wsg", &self->wsgs[PA_WSG_GIRL_WIN], false); + loadWsg("kr-015.wsg", &self->wsgs[PA_WSG_GIRL_ICON], false); + // loadWsg("pa-tile-009.wsg", &self->wsgs[PA_WSG_BLOCK], false); + // loadWsg("pa-tile-013.wsg", &self->wsgs[PA_WSG_BONUS_BLOCK], false); + loadWsg("pa-en-004.wsg", &self->wsgs[PA_WSG_ENEMY_SOUTH], false); + loadWsg("pa-en-006.wsg", &self->wsgs[PA_WSG_ENEMY_NORTH], false); + loadWsg("pa-en-000.wsg", &self->wsgs[PA_WSG_ENEMY_SIDE_1], false); + loadWsg("pa-en-001.wsg", &self->wsgs[PA_WSG_ENEMY_SIDE_2], false); + loadWsg("pa-en-005.wsg", &self->wsgs[PA_WSG_ENEMY_DRILL_SOUTH], false); + loadWsg("pa-en-007.wsg", &self->wsgs[PA_WSG_ENEMY_DRILL_NORTH], false); + loadWsg("pa-en-008.wsg", &self->wsgs[PA_WSG_ENEMY_STUN], false); + loadWsg("pa-en-002.wsg", &self->wsgs[PA_WSG_ENEMY_DRILL_SIDE_1], false); + loadWsg("pa-en-003.wsg", &self->wsgs[PA_WSG_ENEMY_DRILL_SIDE_2], false); + loadWsg("break-000.wsg", &self->wsgs[PA_WSG_BREAK_BLOCK], false); + loadWsg("break-001.wsg", &self->wsgs[PA_WSG_BREAK_BLOCK_1], false); + loadWsg("break-002.wsg", &self->wsgs[PA_WSG_BREAK_BLOCK_2], false); + loadWsg("break-003.wsg", &self->wsgs[PA_WSG_BREAK_BLOCK_3], false); + loadWsg("blockfragment.wsg", &self->wsgs[PA_WSG_BLOCK_FRAGMENT], false); + + loadWsg("pa-tile-001.wsg", &self->wsgs[PA_WSG_WALL_0], false); + loadWsg("pa-tile-002.wsg", &self->wsgs[PA_WSG_WALL_1], false); + loadWsg("pa-tile-003.wsg", &self->wsgs[PA_WSG_WALL_2], false); + loadWsg("pa-tile-004.wsg", &self->wsgs[PA_WSG_WALL_3], false); + loadWsg("pa-tile-005.wsg", &self->wsgs[PA_WSG_WALL_4], false); + loadWsg("pa-tile-006.wsg", &self->wsgs[PA_WSG_WALL_5], false); + loadWsg("pa-tile-007.wsg", &self->wsgs[PA_WSG_WALL_6], false); + loadWsg("pa-tile-008.wsg", &self->wsgs[PA_WSG_WALL_7], false); + loadWsg("pa-tile-009.wsg", &self->wsgs[PA_WSG_BLOCK], false); + loadWsg("pa-tile-010.wsg", &self->wsgs[PA_WSG_SPAWN_BLOCK_0], false); + loadWsg("pa-tile-011.wsg", &self->wsgs[PA_WSG_SPAWN_BLOCK_1], false); + loadWsg("pa-tile-012.wsg", &self->wsgs[PA_WSG_SPAWN_BLOCK_2], false); + loadWsg("pa-tile-013.wsg", &self->wsgs[PA_WSG_BONUS_BLOCK_0], false); + loadWsg("pa-tile-014.wsg", &self->wsgs[PA_WSG_BONUS_BLOCK_1], false); + loadWsg("pa-tile-015.wsg", &self->wsgs[PA_WSG_BONUS_BLOCK_2], false); +} + +void pa_initializeSprites(paWsgManager_t* self) +{ + self->sprites[PA_SP_PLAYER_SOUTH].wsg = &self->wsgs[PA_WSG_PANGO_SOUTH]; + self->sprites[PA_SP_PLAYER_SOUTH].originX = 8; + self->sprites[PA_SP_PLAYER_SOUTH].originY = 16; + + self->sprites[PA_SP_PLAYER_WALK_SOUTH].wsg = &self->wsgs[PA_WSG_PANGO_WALK_SOUTH]; + self->sprites[PA_SP_PLAYER_WALK_SOUTH].originX = 8; + self->sprites[PA_SP_PLAYER_WALK_SOUTH].originY = 16; + + self->sprites[PA_SP_PLAYER_NORTH].wsg = &self->wsgs[PA_WSG_PANGO_NORTH]; + self->sprites[PA_SP_PLAYER_NORTH].originX = 8; + self->sprites[PA_SP_PLAYER_NORTH].originY = 16; + + self->sprites[PA_SP_PLAYER_WALK_NORTH].wsg = &self->wsgs[PA_WSG_PANGO_WALK_NORTH]; + self->sprites[PA_SP_PLAYER_WALK_NORTH].originX = 8; + self->sprites[PA_SP_PLAYER_WALK_NORTH].originY = 16; + + self->sprites[PA_SP_PLAYER_SIDE].wsg = &self->wsgs[PA_WSG_PANGO_SIDE]; + self->sprites[PA_SP_PLAYER_SIDE].originX = 8; + self->sprites[PA_SP_PLAYER_SIDE].originY = 16; + + self->sprites[PA_SP_PLAYER_WALK_SIDE_1].wsg = &self->wsgs[PA_WSG_PANGO_WALK_SIDE_1]; + self->sprites[PA_SP_PLAYER_WALK_SIDE_1].originX = 8; + self->sprites[PA_SP_PLAYER_WALK_SIDE_1].originY = 16; + + self->sprites[PA_SP_PLAYER_WALK_SIDE_2].wsg = &self->wsgs[PA_WSG_PANGO_WALK_SIDE_2]; + self->sprites[PA_SP_PLAYER_WALK_SIDE_2].originX = 8; + self->sprites[PA_SP_PLAYER_WALK_SIDE_2].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_1].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_SOUTH_1]; + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_1].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_1].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_2].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_SOUTH_2]; + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_2].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_SOUTH_2].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_NORTH_1].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_NORTH_1]; + self->sprites[PA_SP_PLAYER_PUSH_NORTH_1].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_NORTH_1].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_NORTH_2].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_NORTH_2]; + self->sprites[PA_SP_PLAYER_PUSH_NORTH_2].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_NORTH_2].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_SIDE_1].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_SIDE_1]; + self->sprites[PA_SP_PLAYER_PUSH_SIDE_1].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_SIDE_1].originY = 16; + + self->sprites[PA_SP_PLAYER_PUSH_SIDE_2].wsg = &self->wsgs[PA_WSG_PANGO_PUSH_SIDE_2]; + self->sprites[PA_SP_PLAYER_PUSH_SIDE_2].originX = 8; + self->sprites[PA_SP_PLAYER_PUSH_SIDE_2].originY = 16; + + self->sprites[PA_SP_PLAYER_HURT].wsg = &self->wsgs[PA_WSG_PANGO_HURT]; + self->sprites[PA_SP_PLAYER_HURT].originX = 8; + self->sprites[PA_SP_PLAYER_HURT].originY = 16; + + self->sprites[PA_SP_PLAYER_WIN].wsg = &self->wsgs[PA_SP_PLAYER_WIN]; + self->sprites[PA_SP_PLAYER_WIN].originX = 8; + self->sprites[PA_SP_PLAYER_WIN].originY = 16; + + self->sprites[PA_SP_PLAYER_ICON].wsg = &self->wsgs[PA_SP_PLAYER_ICON]; + self->sprites[PA_SP_PLAYER_ICON].originX = 8; + self->sprites[PA_SP_PLAYER_ICON].originY = 16; + + self->sprites[PA_SP_BLOCK].wsg = &self->wsgs[PA_WSG_BLOCK]; + self->sprites[PA_SP_BLOCK].originX = 8; + self->sprites[PA_SP_BLOCK].originY = 8; + + self->sprites[PA_SP_BONUS_BLOCK].wsg = &self->wsgs[PA_WSG_BONUS_BLOCK_0]; + self->sprites[PA_SP_BONUS_BLOCK].originX = 8; + self->sprites[PA_SP_BONUS_BLOCK].originY = 8; + + self->sprites[PA_SP_ENEMY_SOUTH].wsg = &self->wsgs[PA_WSG_ENEMY_SOUTH]; + self->sprites[PA_SP_ENEMY_SOUTH].originX = 8; + self->sprites[PA_SP_ENEMY_SOUTH].originY = 16; + + self->sprites[PA_SP_ENEMY_NORTH].wsg = &self->wsgs[PA_WSG_ENEMY_NORTH]; + self->sprites[PA_SP_ENEMY_NORTH].originX = 8; + self->sprites[PA_SP_ENEMY_NORTH].originY = 16; + + self->sprites[PA_SP_ENEMY_SIDE_1].wsg = &self->wsgs[PA_WSG_ENEMY_SIDE_1]; + self->sprites[PA_SP_ENEMY_SIDE_1].originX = 8; + self->sprites[PA_SP_ENEMY_SIDE_1].originY = 16; + + self->sprites[PA_SP_ENEMY_SIDE_2].wsg = &self->wsgs[PA_WSG_ENEMY_SIDE_2]; + self->sprites[PA_SP_ENEMY_SIDE_2].originX = 8; + self->sprites[PA_SP_ENEMY_SIDE_2].originY = 16; + + self->sprites[PA_SP_ENEMY_DRILL_SOUTH].wsg = &self->wsgs[PA_WSG_ENEMY_DRILL_SOUTH]; + self->sprites[PA_SP_ENEMY_DRILL_SOUTH].originX = 8; + self->sprites[PA_SP_ENEMY_DRILL_SOUTH].originY = 16; + + self->sprites[PA_SP_ENEMY_DRILL_NORTH].wsg = &self->wsgs[PA_WSG_ENEMY_DRILL_NORTH]; + self->sprites[PA_SP_ENEMY_DRILL_NORTH].originX = 8; + self->sprites[PA_SP_ENEMY_DRILL_NORTH].originY = 16; + + self->sprites[PA_SP_ENEMY_STUN].wsg = &self->wsgs[PA_WSG_ENEMY_STUN]; + self->sprites[PA_SP_ENEMY_STUN].originX = 8; + self->sprites[PA_SP_ENEMY_STUN].originY = 16; + + self->sprites[PA_SP_ENEMY_DRILL_SIDE_1].wsg = &self->wsgs[PA_WSG_ENEMY_DRILL_SIDE_1]; + self->sprites[PA_SP_ENEMY_DRILL_SIDE_1].originX = 8; + self->sprites[PA_SP_ENEMY_DRILL_SIDE_1].originY = 16; + + self->sprites[PA_SP_ENEMY_DRILL_SIDE_2].wsg = &self->wsgs[PA_WSG_ENEMY_DRILL_SIDE_2]; + self->sprites[PA_SP_ENEMY_DRILL_SIDE_2].originX = 8; + self->sprites[PA_SP_ENEMY_DRILL_SIDE_2].originY = 16; + + self->sprites[PA_SP_BREAK_BLOCK].wsg = &self->wsgs[PA_WSG_BREAK_BLOCK]; + self->sprites[PA_SP_BREAK_BLOCK].originX = 8; + self->sprites[PA_SP_BREAK_BLOCK].originY = 8; + + self->sprites[PA_SP_BREAK_BLOCK_1].wsg = &self->wsgs[PA_WSG_BREAK_BLOCK_1]; + self->sprites[PA_SP_BREAK_BLOCK_1].originX = 8; + self->sprites[PA_SP_BREAK_BLOCK_1].originY = 8; + + self->sprites[PA_SP_BREAK_BLOCK_2].wsg = &self->wsgs[PA_WSG_BREAK_BLOCK_2]; + self->sprites[PA_SP_BREAK_BLOCK_2].originX = 8; + self->sprites[PA_SP_BREAK_BLOCK_2].originY = 8; + + self->sprites[PA_SP_BREAK_BLOCK_3].wsg = &self->wsgs[PA_WSG_BREAK_BLOCK_3]; + self->sprites[PA_SP_BREAK_BLOCK_3].originX = 8; + self->sprites[PA_SP_BREAK_BLOCK_3].originY = 8; + + self->sprites[PA_SP_BLOCK_FRAGMENT].wsg = &self->wsgs[PA_WSG_BLOCK_FRAGMENT]; + self->sprites[PA_SP_BLOCK_FRAGMENT].originX = 3; + self->sprites[PA_SP_BLOCK_FRAGMENT].originY = 3; +} + +void pa_initializeTiles(paWsgManager_t* self) +{ + self->tiles[0] = &self->wsgs[PA_WSG_WALL_0]; + self->tiles[1] = &self->wsgs[PA_WSG_WALL_1]; + self->tiles[2] = &self->wsgs[PA_WSG_WALL_2]; + self->tiles[3] = &self->wsgs[PA_WSG_WALL_3]; + self->tiles[4] = &self->wsgs[PA_WSG_WALL_4]; + self->tiles[5] = &self->wsgs[PA_WSG_WALL_5]; + self->tiles[6] = &self->wsgs[PA_WSG_WALL_6]; + self->tiles[7] = &self->wsgs[PA_WSG_WALL_7]; + self->tiles[8] = &self->wsgs[PA_WSG_BLOCK]; + self->tiles[9] = &self->wsgs[PA_WSG_SPAWN_BLOCK_0]; + self->tiles[10] = &self->wsgs[PA_WSG_SPAWN_BLOCK_1]; + self->tiles[11] = &self->wsgs[PA_WSG_SPAWN_BLOCK_2]; + self->tiles[12] = &self->wsgs[PA_WSG_BONUS_BLOCK_0]; + self->tiles[13] = &self->wsgs[PA_WSG_BONUS_BLOCK_1]; + self->tiles[14] = &self->wsgs[PA_WSG_BONUS_BLOCK_2]; +} + +void pa_remapWsgToSprite(paWsgManager_t* self, uint16_t spriteIndex, uint16_t wsgIndex) +{ + self->sprites[spriteIndex].wsg = &self->wsgs[wsgIndex]; +} + +void pa_remapWsgToTile(paWsgManager_t* self, uint16_t tileIndex, uint16_t wsgIndex) +{ + self->tiles[tileIndex] = &self->wsgs[wsgIndex]; +} + +void pa_remapPlayerCharacter(paWsgManager_t* self, uint16_t newBaseIndex) +{ + for (uint16_t i = 0; i < (PA_SP_PLAYER_ICON + 1); i++) + { + pa_remapWsgToSprite(self, i, newBaseIndex + i); + } +} diff --git a/main/modes/games/pango/paWsgManager.h b/main/modes/games/pango/paWsgManager.h new file mode 100644 index 000000000..a6bbe9f4a --- /dev/null +++ b/main/modes/games/pango/paWsgManager.h @@ -0,0 +1,147 @@ +#ifndef _PA_WSGMANAGER_H_ +#define _PA_WSGMANAGER_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include "wsg.h" +#include "paSprite.h" +#include "pango_typedef.h" + +//============================================================================== +// Constants +//============================================================================== +#define PA_WSGS_SIZE 93 +#define PA_SPRITESET_SIZE 32 +#define PA_TILE_SET_SIZE 15 + +//============================================================================== +// Enums +//============================================================================== +typedef enum +{ + PA_WSG_PANGO_SOUTH, + PA_WSG_PANGO_WALK_SOUTH, + PA_WSG_PANGO_NORTH, + PA_WSG_PANGO_WALK_NORTH, + PA_WSG_PANGO_SIDE, + PA_WSG_PANGO_WALK_SIDE_1, + PA_WSG_PANGO_WALK_SIDE_2, + PA_WSG_PANGO_PUSH_SOUTH_1, + PA_WSG_PANGO_PUSH_SOUTH_2, + PA_WSG_PANGO_PUSH_NORTH_1, + PA_WSG_PANGO_PUSH_NORTH_2, + PA_WSG_PANGO_PUSH_SIDE_1, + PA_WSG_PANGO_PUSH_SIDE_2, + PA_WSG_PANGO_HURT, + PA_WSG_PANGO_WIN, + PA_WSG_PANGO_ICON, + PA_WSG_PO_SOUTH, + PA_WSG_PO_WALK_SOUTH, + PA_WSG_PO_NORTH, + PA_WSG_PO_WALK_NORTH, + PA_WSG_PO_SIDE, + PA_WSG_PO_WALK_SIDE_1, + PA_WSG_PO_WALK_SIDE_2, + PA_WSG_PO_PUSH_SOUTH_1, + PA_WSG_PO_PUSH_SOUTH_2, + PA_WSG_PO_PUSH_NORTH_1, + PA_WSG_PO_PUSH_NORTH_2, + PA_WSG_PO_PUSH_SIDE_1, + PA_WSG_PO_PUSH_SIDE_2, + PA_WSG_PO_HURT, + PA_WSG_PO_WIN, + PA_WSG_PO_ICON, + PA_WSG_PIXEL_SOUTH, + PA_WSG_PIXEL_WALK_SOUTH, + PA_WSG_PIXEL_NORTH, + PA_WSG_PIXEL_WALK_NORTH, + PA_WSG_PIXEL_SIDE, + PA_WSG_PIXEL_WALK_SIDE_1, + PA_WSG_PIXEL_WALK_SIDE_2, + PA_WSG_PIXEL_PUSH_SOUTH_1, + PA_WSG_PIXEL_PUSH_SOUTH_2, + PA_WSG_PIXEL_PUSH_NORTH_1, + PA_WSG_PIXEL_PUSH_NORTH_2, + PA_WSG_PIXEL_PUSH_SIDE_1, + PA_WSG_PIXEL_PUSH_SIDE_2, + PA_WSG_PIXEL_HURT, + PA_WSG_PIXEL_WIN, + PA_WSG_PIXEL_ICON, + PA_WSG_GIRL_SOUTH, + PA_WSG_GIRL_WALK_SOUTH, + PA_WSG_GIRL_NORTH, + PA_WSG_GIRL_WALK_NORTH, + PA_WSG_GIRL_SIDE, + PA_WSG_GIRL_WALK_SIDE_1, + PA_WSG_GIRL_WALK_SIDE_2, + PA_WSG_GIRL_PUSH_SOUTH_1, + PA_WSG_GIRL_PUSH_SOUTH_2, + PA_WSG_GIRL_PUSH_NORTH_1, + PA_WSG_GIRL_PUSH_NORTH_2, + PA_WSG_GIRL_PUSH_SIDE_1, + PA_WSG_GIRL_PUSH_SIDE_2, + PA_WSG_GIRL_HURT, + PA_WSG_GIRL_WIN, + PA_WSG_GIRL_ICON, + // PA_WSG_BLOCK, + // PA_WSG_BONUS_BLOCK, + PA_WSG_ENEMY_SOUTH, + PA_WSG_ENEMY_NORTH, + PA_WSG_ENEMY_SIDE_1, + PA_WSG_ENEMY_SIDE_2, + PA_WSG_ENEMY_DRILL_SOUTH, + PA_WSG_ENEMY_DRILL_NORTH, + PA_WSG_ENEMY_DRILL_SIDE_1, + PA_WSG_ENEMY_DRILL_SIDE_2, + PA_WSG_ENEMY_STUN, + PA_WSG_BREAK_BLOCK, + PA_WSG_BREAK_BLOCK_1, + PA_WSG_BREAK_BLOCK_2, + PA_WSG_BREAK_BLOCK_3, + PA_WSG_BLOCK_FRAGMENT, + PA_WSG_WALL_0, + PA_WSG_WALL_1, + PA_WSG_WALL_2, + PA_WSG_WALL_3, + PA_WSG_WALL_4, + PA_WSG_WALL_5, + PA_WSG_WALL_6, + PA_WSG_WALL_7, + PA_WSG_BLOCK, + PA_WSG_SPAWN_BLOCK_0, + PA_WSG_SPAWN_BLOCK_1, + PA_WSG_SPAWN_BLOCK_2, + PA_WSG_BONUS_BLOCK_0, + PA_WSG_BONUS_BLOCK_1, + PA_WSG_BONUS_BLOCK_2 +} paWsgIndex_t; + +//============================================================================== +// Structs +//============================================================================== +typedef struct +{ + wsg_t wsgs[PA_WSGS_SIZE]; + paSprite_t sprites[PA_SPRITESET_SIZE]; + wsg_t* tiles[PA_TILE_SET_SIZE]; +} paWsgManager_t; + +//============================================================================== +// Function Definitions +//============================================================================== +void pa_initializeWsgManager(paWsgManager_t* self); +void pa_freeWsgManager(paWsgManager_t* self); + +void pa_loadWsgs(paWsgManager_t* self); +void pa_initializeSprites(paWsgManager_t* self); +void pa_initializeTiles(paWsgManager_t* tiles); + +void pa_remapWsgToSprite(paWsgManager_t* self, uint16_t spriteIndex, uint16_t wsgIndex); +void pa_remapWsgToTile(paWsgManager_t* self, uint16_t tileIndex, uint16_t wsgIndex); + +void pa_remapPlayerCharacter(paWsgManager_t* self, uint16_t newBaseIndex); + +#endif \ No newline at end of file diff --git a/main/modes/games/pango/pango.c b/main/modes/games/pango/pango.c new file mode 100644 index 000000000..b50a2cbd9 --- /dev/null +++ b/main/modes/games/pango/pango.c @@ -0,0 +1,1757 @@ +/** + * @file pango.c + * @author J.Vega (JVeg199X) + * @brief Pango + * @date 2024-05-04 + * + */ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include + +#include "esp_log.h" +#include "esp_timer.h" + +#include "pango.h" +#include "esp_random.h" + +#include "pango_typedef.h" +#include "paWsgManager.h" +#include "paTilemap.h" +#include "paGameData.h" +#include "paEntityManager.h" +#include "paLeveldef.h" + +#include "hdw-led.h" +#include "palette.h" +#include "hdw-nvs.h" +#include "paSoundManager.h" +#include +#include "mainMenu.h" +#include "fill.h" +#include "paTables.h" + +//============================================================================== +// Constants +//============================================================================== +#define BIG_SCORE 4000000UL +#define BIGGER_SCORE 10000000UL +#define FAST_TIME 1500 // 25 minutes + +const char pangoName[] = "Pango"; + +static const paletteColor_t highScoreNewEntryColors[4] = {c050, c055, c005, c055}; + +static const paletteColor_t redColors[4] = {c501, c540, c550, c540}; +static const paletteColor_t yellowColors[4] = {c550, c331, c550, c555}; +static const paletteColor_t greenColors[4] = {c555, c051, c030, c051}; +static const paletteColor_t cyanColors[4] = {c055, c455, c055, c033}; +static const paletteColor_t purpleColors[4] = {c213, c535, c555, c535}; +static const paletteColor_t rgbColors[4] = {c500, c050, c005, c050}; + +static const int16_t cheatCode[11] + = {PB_UP, PB_UP, PB_DOWN, PB_DOWN, PB_LEFT, PB_RIGHT, PB_LEFT, PB_RIGHT, PB_B, PB_A, PB_START}; + +//============================================================================== +// Functions Prototypes +//============================================================================== + +void pangoEnterMode(void); +void pangoExitMode(void); +void pangoMainLoop(int64_t elapsedUs); + +//============================================================================== +// Structs +//============================================================================== + +typedef void (*pa_gameUpdateFuncton_t)(pango_t* self, int64_t elapsedUs); +struct pango_t +{ + font_t radiostars; + + paWsgManager_t wsgManager; + paTilemap_t tilemap; + paEntityManager_t entityManager; + paGameData_t gameData; + + paSoundManager_t soundManager; + + uint8_t menuState; + uint8_t menuSelection; + uint8_t cheatCodeIdx; + + int16_t btnState; + int16_t prevBtnState; + + int32_t frameTimer; + + pangoHighScores_t highScores; + pangoUnlockables_t unlockables; + bool easterEgg; + + pa_gameUpdateFuncton_t update; + + menuManiaRenderer_t* menuRenderer; + menu_t* menu; + menuItem_t* levelSelectMenuItem; +}; + +//============================================================================== +// Function Prototypes +//============================================================================== +void drawPangoHud(font_t* font, paGameData_t* gameData); +void drawPangoTitleScreen(font_t* font, paGameData_t* gameData); +void pangoBuildMainMenu(pango_t* self); +static void pangoUpdateMainMenu(pango_t* self, int64_t elapsedUs); +void changeStateReadyScreen(pango_t* self); +void updateReadyScreen(pango_t* self, int64_t elapsedUs); +void drawReadyScreen(font_t* font, paGameData_t* gameData); +void changeStateGame(pango_t* self); +void detectGameStateChange(pango_t* self); +void detectBgmChange(pango_t* self); +void changeStateDead(pango_t* self); +void updateDead(pango_t* self, int64_t elapsedUs); +void changeStateGameOver(pango_t* self); +void updateGameOver(pango_t* self, int64_t elapsedUs); +void drawGameOver(font_t* font, paGameData_t* gameData); +void changeStateTitleScreen(pango_t* self); +void changeStateLevelClear(pango_t* self); +void updateLevelClear(pango_t* self, int64_t elapsedUs); +void drawLevelClear(font_t* font, paGameData_t* gameData); +void changeStateGameClear(pango_t* self); +void updateGameClear(pango_t* self, int64_t elapsedUs); +void drawGameClear(font_t* font, paGameData_t* gameData); +void pangoInitializeHighScores(pango_t* self); +void loadPangoHighScores(pango_t* self); +void pangoSaveHighScores(pango_t* self); +void pangoInitializeUnlockables(pango_t* self); +void loadPangoUnlockables(pango_t* self); +void pangoSaveUnlockables(pango_t* self); +void drawPangoHighScores(font_t* font, pangoHighScores_t* highScores, paGameData_t* gameData); +uint8_t getHighScoreRank(pangoHighScores_t* highScores, uint32_t newScore); +void insertScoreIntoHighScores(pangoHighScores_t* highScores, uint32_t newScore, char newInitials[], uint8_t rank); +void changeStateNameEntry(pango_t* self); +void updateNameEntry(pango_t* self, int64_t elapsedUs); +void drawNameEntry(font_t* font, paGameData_t* gameData, uint8_t currentInitial); +void pangoChangeStateShowHighScores(pango_t* self); +void updateShowHighScores(pango_t* self, int64_t elapsedUs); +void drawShowHighScores(font_t* font, uint8_t menuState); +void changeStatePause(pango_t* self); +void updatePause(pango_t* self, int64_t elapsedUs); +void drawPause(font_t* font); +uint16_t getLevelIndex(uint8_t world, uint8_t level); +void pangoChangeStateMainMenu(pango_t* self); + +//============================================================================== +// Variables +//============================================================================== + +pango_t* pango = NULL; + +swadgeMode_t pangoMode = {.modeName = pangoName, + .wifiMode = NO_WIFI, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .fnEnterMode = pangoEnterMode, + .fnExitMode = pangoExitMode, + .fnMainLoop = pangoMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = NULL, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL}; + +#define NUM_LEVELS 16 + +static const paLeveldef_t leveldef[17] = {{.filename = "level1-1.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "dac01.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level1-3.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level1-4.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level2-1.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "dac03.bin", .timeLimit = 220, .checkpointTimeLimit = 90}, + {.filename = "level2-3.bin", .timeLimit = 200, .checkpointTimeLimit = 90}, + {.filename = "level2-4.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level3-1.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "dac02.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level3-3.bin", .timeLimit = 180, .checkpointTimeLimit = 90}, + {.filename = "level3-4.bin", .timeLimit = 220, .checkpointTimeLimit = 110}, + {.filename = "level4-1.bin", .timeLimit = 270, .checkpointTimeLimit = 90}, + {.filename = "level4-2.bin", .timeLimit = 240, .checkpointTimeLimit = 90}, + {.filename = "level4-3.bin", .timeLimit = 240, .checkpointTimeLimit = 90}, + {.filename = "level4-4.bin", .timeLimit = 240, .checkpointTimeLimit = 90}, + {.filename = "debug.bin", .timeLimit = 180, .checkpointTimeLimit = 90}}; + +led_t platLeds[CONFIG_NUM_LEDS]; + +static const char str_ready[] = "Ready?"; +static const char str_set[] = "Set..."; +static const char str_pango[] = "PANGO!"; +static const char str_time_up[] = "-Time Up!-"; +static const char str_game_over[] = "Game Over"; +static const char str_well_done[] = "Nice Clear!"; +static const char str_congrats[] = "Congratulations!"; +static const char str_initials[] = "Enter your initials!"; +static const char str_hbd[] = "Happy Birthday, Evelyn!"; +static const char str_registrated[] = "Your name registrated."; +static const char str_do_your_best[] = "Do your best!"; +static const char str_pause[] = "-Pause-"; + +static const char pangoMenuNewGame[] = "New Game"; +static const char pangoMenuContinue[] = "Continue - Lv"; +static const char pangoMenuCharacter[] = "Character"; +static const char pangoMenuHighScores[] = "High Scores"; +static const char pangoMenuResetScores[] = "Reset Scores"; +static const char pangoMenuResetProgress[] = "Reset Progress"; +static const char pangoMenuExit[] = "Exit"; +static const char pangoMenuSaveAndExit[] = "Save & Exit"; + +static const char KEY_SCORES[] = "pf_scores"; +static const char KEY_UNLOCKS[] = "pf_unlocks"; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief TODO + * + */ +void pangoEnterMode(void) +{ + // Allocate memory for this mode + pango = (pango_t*)calloc(1, sizeof(pango_t)); + memset(pango, 0, sizeof(pango_t)); + + pango->menuState = 0; + pango->menuSelection = 0; + pango->btnState = 0; + pango->prevBtnState = 0; + + loadPangoHighScores(pango); + loadPangoUnlockables(pango); + if (pango->highScores.initials[0][0] == 'E' && pango->highScores.initials[0][1] == 'F' + && pango->highScores.initials[0][2] == 'V') + { + pango->easterEgg = true; + } + + loadFont("pango-fw.font", &pango->radiostars, false); + pango->menuRenderer = initMenuManiaRenderer(&pango->radiostars, &pango->radiostars, &pango->radiostars); + + pa_initializeWsgManager(&(pango->wsgManager)); + + pa_initializeTileMap(&(pango->tilemap), &(pango->wsgManager)); + pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + pa_generateMaze(&(pango->tilemap)); + pango->tilemap.mapOffsetX = -4; + + pa_initializeSoundManager(&(pango->soundManager)); + + pa_initializeGameData(&(pango->gameData), &(pango->soundManager)); + pa_initializeEntityManager(&(pango->entityManager), &(pango->wsgManager), &(pango->tilemap), &(pango->gameData), + &(pango->soundManager)); + + pango->tilemap.entityManager = &(pango->entityManager); + pango->tilemap.tileSpawnEnabled = true; + + setFrameRateUs(16666); + + pango->menu = NULL; + changeStateTitleScreen(pango); +} + +/** + * @brief TODO + * + */ +void pangoExitMode(void) +{ + // deinitMenu can't set menu pointer to NULL, + // so this is the only way to know that the menu has not been previously freed. + if (pango->update == &pangoUpdateMainMenu) + { + // Deinitialize the menu. + // This will also free the "level select" menu item. + deinitMenu(pango->menu); + } + deinitMenuManiaRenderer(pango->menuRenderer); + + freeFont(&pango->radiostars); + pa_freeWsgManager(&(pango->wsgManager)); + pa_freeTilemap(&(pango->tilemap)); + pa_freeSoundManager(&(pango->soundManager)); + pa_freeEntityManager(&(pango->entityManager)); + free(pango); +} + +/** + * @brief This callback function is called when an item is selected from the menu + * + * @param label The item that was selected from the menu + * @param selected True if the item was selected with the A button, false if this is a multi-item which scrolled to + * @param settingVal The value of the setting, if the menu item is a settings item + */ +static void pangoMenuCb(const char* label, bool selected, uint32_t settingVal) +{ + if (selected) + { + if (label == pangoMenuNewGame) + { + pango->gameData.world = 1; + pango->gameData.level = 1; + pango->entityManager.activeEnemies = 0; + pa_initializeGameDataFromTitleScreen(&(pango->gameData), 0); + pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + pa_generateMaze(&(pango->tilemap)); + pa_placeEnemySpawns(&(pango->tilemap)); + + changeStateReadyScreen(pango); + deinitMenu(pango->menu); + } + else if (label == pangoMenuContinue) + { + pango->gameData.world = 1; + pango->gameData.level = settingVal; + pa_initializeGameDataFromTitleScreen(&(pango->gameData), settingVal); + pango->entityManager.activeEnemies = 0; + pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + pa_generateMaze(&(pango->tilemap)); + pa_placeEnemySpawns(&(pango->tilemap)); + + changeStateReadyScreen(pango); + deinitMenu(pango->menu); + } + else if (label == pangoMenuCharacter) + { + pa_remapPlayerCharacter(&(pango->wsgManager), 16 * settingVal); + soundPlaySfx(&(pango->soundManager.sndMenuConfirm), BZR_STEREO); + } + else if (label == pangoMenuHighScores) + { + pangoChangeStateShowHighScores(pango); + pango->gameData.btnState = 0; + deinitMenu(pango->menu); + } + else if (label == pangoMenuResetScores) + { + pangoInitializeHighScores(pango); + // soundPlaySfx(&(pango->soundManager.detonate), BZR_STEREO); + } + else if (label == pangoMenuResetProgress) + { + pangoInitializeUnlockables(pango); + // soundPlaySfx(&(pango->soundManager.die), BZR_STEREO); + } + else if (label == pangoMenuSaveAndExit) + { + pangoSaveHighScores(pango); + pangoSaveUnlockables(pango); + switchToSwadgeMode(&mainMenuMode); + } + else if (label == pangoMenuExit) + { + switchToSwadgeMode(&mainMenuMode); + } + } + else + { + // soundPlaySfx(&(pango->soundManager.hit3), BZR_STEREO); + } +} + +/** + * @brief TODO + * + * @param elapsedUs + */ +void pangoMainLoop(int64_t elapsedUs) +{ + // Check inputs + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Save the button state + pango->btnState = evt.state; + pango->gameData.btnState = evt.state; + + if (pango->update == &pangoUpdateMainMenu) + { + // Pass button events to the menu + pango->menu = menuButton(pango->menu, evt); + } + } + + pango->update(pango, elapsedUs); + + pango->prevBtnState = pango->btnState; + pango->gameData.prevBtnState = pango->prevBtnState; +} + +void pangoChangeStateMainMenu(pango_t* self) +{ + self->gameData.frameCount = 0; + self->update = &pangoUpdateMainMenu; + pangoBuildMainMenu(pango); +} + +void pangoBuildMainMenu(pango_t* self) +{ + // Initialize the menu + pango->menu = initMenu(pangoName, pangoMenuCb); + addSingleItemToMenu(pango->menu, pangoMenuNewGame); + + /* + Manually allocate and build "level select" menu item + because the max setting will have to change as levels are unlocked + */ + if (pango->unlockables.maxLevelIndexUnlocked > 1 || pango->gameData.debugMode) + { + pango->levelSelectMenuItem = calloc(1, sizeof(menuItem_t)); + pango->levelSelectMenuItem->label = pangoMenuContinue; + pango->levelSelectMenuItem->minSetting = 1; + pango->levelSelectMenuItem->maxSetting + = (pango->gameData.debugMode) ? NUM_LEVELS - 1 : pango->unlockables.maxLevelIndexUnlocked; + pango->levelSelectMenuItem->currentSetting + = (pango->gameData.level == 1) ? pango->levelSelectMenuItem->maxSetting : pango->gameData.level; + pango->levelSelectMenuItem->options = NULL; + pango->levelSelectMenuItem->subMenu = NULL; + + push(pango->menu->items, pango->levelSelectMenuItem); + } + + settingParam_t characterSettingBounds = { + .def = 0, + .min = 0, + .max = 3, + .key = NULL, + }; + addSettingsItemToMenu(pango->menu, pangoMenuCharacter, &characterSettingBounds, 0); + + addSingleItemToMenu(pango->menu, pangoMenuHighScores); + + if (pango->gameData.debugMode) + { + addSingleItemToMenu(pango->menu, pangoMenuResetProgress); + addSingleItemToMenu(pango->menu, pangoMenuResetScores); + addSingleItemToMenu(pango->menu, pangoMenuSaveAndExit); + } + else + { + addSingleItemToMenu(pango->menu, pangoMenuExit); + } +} + +static void pangoUpdateMainMenu(pango_t* self, int64_t elapsedUs) +{ + // Draw the menu + drawMenuMania(pango->menu, pango->menuRenderer, elapsedUs); +} + +void updateGame(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, self->gameData.bgColor); + + pa_updateEntities(&(self->entityManager)); + + pa_drawTileMap(&(self->tilemap)); + pa_drawEntities(&(self->entityManager)); + + // drawEntityTargetTile(self->entityManager.playerEntity); + + detectGameStateChange(self); + detectBgmChange(self); + + // self->gameData.coins = self->gameData.remainingEnemies; + self->gameData.coins = self->entityManager.aggroEnemies; + drawPangoHud(&(self->radiostars), &(self->gameData)); + + self->gameData.frameCount++; + if (self->gameData.frameCount > 59) + { + self->gameData.frameCount = 0; + self->gameData.countdown--; + self->gameData.inGameTimer++; + + if (self->gameData.countdown < 10) + { + soundPlayBgm(&(self->soundManager.sndOuttaTime), BZR_STEREO); + } + + if (self->gameData.countdown < 0) + { + killPlayer(self->entityManager.playerEntity); + } + + pa_spawnEnemyFromSpawnBlock(&(self->entityManager)); + } + + updateComboTimer(&(self->gameData)); +} + +void drawPangoHud(font_t* font, paGameData_t* gameData) +{ + char coinStr[8]; + snprintf(coinStr, sizeof(coinStr) - 1, "C:%02d", gameData->coins); + + char scoreStr[32]; + snprintf(scoreStr, sizeof(scoreStr) - 1, "%06" PRIu32, gameData->score); + + char levelStr[15]; + snprintf(levelStr, sizeof(levelStr) - 1, "Level %d-%d", gameData->world, gameData->level); + + char livesStr[8]; + snprintf(livesStr, sizeof(livesStr) - 1, "x%d", gameData->lives); + + char timeStr[10]; + snprintf(timeStr, sizeof(timeStr) - 1, "T:%03d", gameData->countdown); + + if (gameData->frameCount > 29) + { + drawText(font, c500, "1UP", 24, 2); + } + + drawText(font, c553, livesStr, 56, 2); + // drawText(font, c553, coinStr, 160, 224); + drawText(font, c553, scoreStr, 112, 2); + drawText(font, c553, levelStr, 32, 226); + drawText(font, (gameData->countdown > 30) ? c553 : redColors[(gameData->frameCount >> 3) % 4], timeStr, 200, 226); + + if (gameData->comboTimer == 0) + { + return; + } + + snprintf(scoreStr, sizeof(scoreStr) - 1, "+%" PRIu32 /*" (x%d)"*/, gameData->comboScore /*, gameData->combo*/); + drawText(font, (gameData->comboTimer < 60) ? c030 : greenColors[(pango->gameData.frameCount >> 3) % 4], scoreStr, + 190, 2); +} + +void updateTitleScreen(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, self->gameData.bgColor); + + self->gameData.frameCount++; + + if (self->gameData.frameCount > 600) + { + // resetGameDataLeds(&(self->gameData)); + pango->menuSelection = 0; + pangoChangeStateShowHighScores(self); + + return; + } + + if ((self->gameData.btnState & cheatCode[pango->menuSelection]) + && !(self->gameData.prevBtnState & cheatCode[pango->menuSelection])) + { + pango->menuSelection++; + + if (pango->menuSelection > 8) + { + pango->menuSelection = 0; + pango->menuState = 1; + pango->gameData.debugMode = true; + // soundPlaySfx(&(pango->soundManager.levelClear), BZR_STEREO); + } + else + { + // soundPlaySfx(&(pango->soundManager.hit3), BZR_STEREO); + } + } + + if ((((self->gameData.btnState & PB_A) && !(self->gameData.prevBtnState & PB_A)) + || ((self->gameData.btnState & PB_START) && !(self->gameData.prevBtnState & PB_START)))) + { + self->gameData.btnState = 0; + pango->menuSelection = 0; + + if (!pango->gameData.debugMode) + { + // soundPlaySfx(&(pango->soundManager.launch), BZR_STEREO); + } + + pangoChangeStateMainMenu(self); + return; + } + + // Handle inputs + /*switch (pango->menuState) + { + case 0: + { + if (self->gameData.frameCount > 600) + { + pa_resetGameDataLeds(&(self->gameData)); + changeStateShowHighScores(self); + } + + if ((self->gameData.btnState & cheatCode[pango->cheatCodeIdx]) + && !(self->gameData.prevBtnState & cheatCode[pango->cheatCodeIdx])) + { + pango->cheatCodeIdx++; + + if (pango->cheatCodeIdx > 10) + { + pango->cheatCodeIdx = 0; + pango->menuState = 1; + pango->gameData.debugMode = true; + soundPlaySfx(&(pango->soundManager.sndLevelClearS), BZR_STEREO); + break; + } + else + { + soundPlaySfx(&(pango->soundManager.sndMenuSelect), BZR_STEREO); + break; + } + } + else + { + if (!(self->gameData.frameCount % 150)) + { + pango->cheatCodeIdx = 0; + } + } + + if (((self->gameData.btnState & PB_START) && !(self->gameData.prevBtnState & PB_START)) + || ((self->gameData.btnState & PB_A) && !(self->gameData.prevBtnState & PB_A))) + { + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + pango->menuState = 1; + pango->menuSelection = 0; + } + + break; + } + case 1: + { + if (((self->gameData.btnState & PB_START) && !(self->gameData.prevBtnState & PB_START)) + || ((self->gameData.btnState & PB_A) && !(self->gameData.prevBtnState & PB_A))) + { + switch (self->menuSelection) + { + case 0 ... 1: + { + uint16_t levelIndex = getLevelIndex(self->gameData.world, self->gameData.level); + if ((levelIndex >= NUM_LEVELS) + || (!self->gameData.debugMode && levelIndex > self->unlockables.maxLevelIndexUnlocked)) + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + break; + } + + //if(self->menuSelection == 0){ + // self->gameData.world = 1; + // self->gameData.level = 1; + // } + + pa_initializeGameDataFromTitleScreen(&(self->gameData), levelIndex); + self->entityManager.activeEnemies = 0; + pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + pa_generateMaze(&(pango->tilemap)); + pa_placeEnemySpawns(&(pango->tilemap)); + + changeStateReadyScreen(self); + break; + } + case 2: + { + if (self->gameData.debugMode) + { + // Reset Progress + pangoInitializeUnlockables(self); + soundPlaySfx(&(self->soundManager.sndBreak), BZR_STEREO); + } + else + { + // Show High Scores + self->menuSelection = 0; + self->menuState = 0; + + changeStateShowHighScores(self); + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + } + break; + } + case 3: + { + if (self->gameData.debugMode) + { + // Reset High Scores + pangoInitializeHighScores(self); + soundPlaySfx(&(self->soundManager.sndBreak), BZR_STEREO); + } + else + { + // Show Achievements + self->menuSelection = 0; + self->menuState = 2; + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + } + break; + } + case 4: + { + if (self->gameData.debugMode) + { + // Save & Quit + pangoSaveHighScores(self); + pangoSaveUnlockables(self); + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + switchToSwadgeMode(&mainMenuMode); + } + else + { + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + switchToSwadgeMode(&mainMenuMode); + } + break; + } + default: + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + self->menuSelection = 0; + } + } + } + else if ((self->gameData.btnState & PB_UP && !(self->gameData.prevBtnState & PB_UP))) + { + if (pango->menuSelection > 0) + { + pango->menuSelection--; + + if (!self->gameData.debugMode && pango->menuSelection == 1 + && self->unlockables.maxLevelIndexUnlocked == 0) + { + pango->menuSelection--; + } + + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + } + else if ((self->gameData.btnState & PB_DOWN && !(self->gameData.prevBtnState & PB_DOWN))) + { + if (pango->menuSelection < 4) + { + pango->menuSelection++; + + if (!self->gameData.debugMode && pango->menuSelection == 1 + && self->unlockables.maxLevelIndexUnlocked == 0) + { + pango->menuSelection++; + } + + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + else + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + } + } + else if ((self->gameData.btnState & PB_LEFT && !(self->gameData.prevBtnState & PB_LEFT))) + { + if (pango->menuSelection == 1) + { + if (pango->gameData.level == 1 && pango->gameData.world == 1) + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + } + else + { + pango->gameData.level--; + if (pango->gameData.level < 1) + { + pango->gameData.level = 4; + if (pango->gameData.world > 1) + { + pango->gameData.world--; + } + } + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + } + } + else if ((self->gameData.btnState & PB_RIGHT && !(self->gameData.prevBtnState & PB_RIGHT))) + { + if (pango->menuSelection == 1) + { + if ((pango->gameData.level == 4 && pango->gameData.world == 4) + || (!pango->gameData.debugMode + && getLevelIndex(pango->gameData.world, pango->gameData.level + 1) + > pango->unlockables.maxLevelIndexUnlocked)) + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + } + else + { + pango->gameData.level++; + if (pango->gameData.level > 4) + { + pango->gameData.level = 1; + if (pango->gameData.world < 8) + { + pango->gameData.world++; + } + } + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + } + } + else if ((self->gameData.btnState & PB_B && !(self->gameData.prevBtnState & PB_B))) + { + self->gameData.frameCount = 0; + pango->menuState = 0; + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + } + break; + } + case 2: + { + if ((self->gameData.btnState & PB_B && !(self->gameData.prevBtnState & PB_B))) + { + self->gameData.frameCount = 0; + pango->menuState = 1; + soundPlaySfx(&(self->soundManager.sndMenuConfirm), BZR_STEREO); + } + break; + } + default: + pango->menuState = 0; + soundPlaySfx(&(pango->soundManager.sndMenuDeny), BZR_STEREO); + break; + }*/ + + // pa_scrollTileMap(&(pango->tilemap), 1, 0); + // if (self->tilemap.mapOffsetX >= self->tilemap.maxMapOffsetX && self->gameData.frameCount > 58) + //{ + // self->tilemap.mapOffsetX = 0; + // } + + drawPangoTitleScreen(&(self->radiostars), &(self->gameData)); + + if (((self->gameData.frameCount) % 10) == 0) + { + for (int32_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + // self->gameData.leds[i].r = (( (self->gameData.frameCount >> 4) % NUM_LEDS) == i) ? 0xFF : 0x00; + + platLeds[i].r += (esp_random() % 1); + platLeds[i].g += (esp_random() % 8); + platLeds[i].b += (esp_random() % 8); + } + } + setLeds(platLeds, CONFIG_NUM_LEDS); +} + +void drawPangoTitleScreen(font_t* font, paGameData_t* gameData) +{ + // pa_drawTileMap(&(pango->tilemap)); + + drawText(font, c555, "P A N G O", 96, 32); + + if (pango->gameData.debugMode) + { + drawText(font, c555, "Debug Mode", 80, 48); + } + + switch (pango->menuState) + { + case 0: + { + if ((gameData->frameCount % 60) < 30) + { + drawText(font, c555, "- Press START button -", 20, 128); + } + break; + } + + case 1: + { + drawText(font, c555, "Start Game", 48, 128); + + if (pango->gameData.debugMode || pango->unlockables.maxLevelIndexUnlocked > 0) + { + char levelStr[24]; + snprintf(levelStr, sizeof(levelStr) - 1, "Level Select: %d-%d", gameData->world, gameData->level); + drawText(font, c555, levelStr, 48, 144); + } + + if (pango->gameData.debugMode) + { + drawText(font, c555, "Reset Progress", 48, 160); + drawText(font, c555, "Reset High Scores", 48, 176); + drawText(font, c555, "Save & Exit to Menu", 48, 192); + } + else + { + drawText(font, c555, "High Scores", 48, 160); + drawText(font, c555, "Achievements", 48, 176); + drawText(font, c555, "Exit to Menu", 48, 192); + } + + drawText(font, c555, "->", 24, 128 + pango->menuSelection * 16); + + break; + } + + case 2: + { + if (pango->unlockables.gameCleared) + { + drawText(font, redColors[(gameData->frameCount >> 3) % 4], "Beat the game!", 48, 80); + } + + if (pango->unlockables.oneCreditCleared) + { + drawText(font, yellowColors[(gameData->frameCount >> 3) % 4], "1 Credit Clear!", 48, 96); + } + + if (pango->unlockables.bigScore) + { + drawText(font, greenColors[(gameData->frameCount >> 3) % 4], "Got 4 million points!", 48, 112); + } + + if (pango->unlockables.biggerScore) + { + drawText(font, cyanColors[(gameData->frameCount >> 3) % 4], "Got 10 million points!", 48, 128); + } + + if (pango->unlockables.fastTime) + { + drawText(font, purpleColors[(gameData->frameCount >> 3) % 4], "Beat within 25 min!", 48, 144); + } + + if (pango->unlockables.gameCleared && pango->unlockables.oneCreditCleared && pango->unlockables.bigScore + && pango->unlockables.biggerScore && pango->unlockables.fastTime) + { + drawText(font, rgbColors[(gameData->frameCount >> 3) % 4], "100% 100% 100%", 48, 160); + } + + drawText(font, c555, "Press B to Return", 48, 192); + break; + } + + default: + break; + } +} + +void changeStateReadyScreen(pango_t* self) +{ + self->gameData.frameCount = 0; + + soundPlayBgm(&(self->soundManager.bgmIntro), BZR_STEREO); + + pa_resetGameDataLeds(&(self->gameData)); + + self->update = &updateReadyScreen; +} + +void updateReadyScreen(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + self->gameData.frameCount++; + if (self->gameData.frameCount > 179) + { + soundStop(true); + changeStateGame(self); + } + + drawReadyScreen(&(self->radiostars), &(self->gameData)); +} + +void drawReadyScreen(font_t* font, paGameData_t* gameData) +{ + drawPangoHud(font, gameData); + + drawText(font, c555, str_ready, 80, 96); + + if (gameData->frameCount > 60) + { + drawText(font, c555, str_set, 112, 128); + } + + if (gameData->frameCount > 120) + { + drawText(font, c555, str_pango, 144, 160); + } + + /*if (getLevelIndex(gameData->world, gameData->level) == 0) + { + drawText(font, c555, "A: Jump", xOff, 128 + (font->height + 3) * 3); + drawText(font, c555, "B: Run / Fire", xOff, 128 + (font->height + 3) * 4); + }*/ +} + +void changeStateGame(pango_t* self) +{ + self->gameData.frameCount = 0; + self->gameData.currentBgm = 0; + pa_resetGameDataLeds(&(self->gameData)); + + pa_deactivateAllEntities(&(self->entityManager), false); + + uint16_t levelIndex = getLevelIndex(self->gameData.world, self->gameData.level); + // pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + // pa_generateMaze(&(pango->tilemap)); + // pa_placeEnemySpawns(&(pango->tilemap)); + + self->gameData.countdown = leveldef[levelIndex].timeLimit; + + paEntityManager_t* entityManager = &(self->entityManager); + entityManager->viewEntity = pa_createPlayer(entityManager, (9 << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE, + (7 << PA_TILE_SIZE_IN_POWERS_OF_2) + PA_HALF_TILE_SIZE); + entityManager->playerEntity = entityManager->viewEntity; + entityManager->playerEntity->hp = self->gameData.initialHp; + + if (entityManager->activeEnemies == 0) + { + for (uint16_t i = 0; i < self->gameData.maxActiveEnemies; i++) + { + pa_spawnEnemyFromSpawnBlock(&(self->entityManager)); + } + } + else + { + // uint16_t randomAggroEnemy = esp_random() % self->gameData.maxActiveEnemies; + + int16_t skippedEnemyRespawnCount = 0; + + for (uint16_t i = 0; i < entityManager->activeEnemies; i++) + { + if (i >= DEFAULT_ENEMY_SPAWN_LOCATION_TABLE_LENGTH) + { + skippedEnemyRespawnCount++; + continue; + } + + uint8_t spawnTx = defaultEnemySpawnLocations[i * DEFAULT_ENEMY_SPAWN_LOCATION_ROW_LENGTH + + DEFAULT_ENEMY_SPAWN_LOCATION_TX_LOOKUP_OFFSET]; + uint8_t spawnTy = defaultEnemySpawnLocations[i * DEFAULT_ENEMY_SPAWN_LOCATION_ROW_LENGTH + + DEFAULT_ENEMY_SPAWN_LOCATION_TY_LOOKUP_OFFSET]; + uint8_t tileAtSpawn = pa_getTile(&(self->tilemap), spawnTx, spawnTy); + + switch (tileAtSpawn) + { + default: + break; + case PA_TILE_BLOCK: + pa_setTile(&(self->tilemap), spawnTx, spawnTy, PA_TILE_EMPTY); + break; + case PA_TILE_SPAWN_BLOCK_0: + skippedEnemyRespawnCount++; + continue; + break; + } + + /*paEntity_t* newEnemy = */ createCrabdozer(&(self->entityManager), + (spawnTx << PA_TILE_SIZE_IN_POWERS_OF_2) + 8, + (spawnTy << PA_TILE_SIZE_IN_POWERS_OF_2) + 8); + + /*if(newEnemy != NULL && i == randomAggroEnemy){ + newEnemy->stateFlag = true; + newEnemy->state = PA_EN_ST_STUN; + newEnemy->stateTimer = 1; + }*/ + } + + entityManager->activeEnemies -= skippedEnemyRespawnCount; + } + + // pa_viewFollowEntity(&(self->tilemap), entityManager->playerEntity); + + pa_updateLedsHpMeter(&(self->entityManager), &(self->gameData)); + + self->tilemap.executeTileSpawnAll = true; + + self->update = &updateGame; +} + +void detectGameStateChange(pango_t* self) +{ + if (!self->gameData.changeState) + { + return; + } + + switch (self->gameData.changeState) + { + case PA_ST_DEAD: + changeStateDead(self); + break; + + case PA_ST_READY_SCREEN: + changeStateReadyScreen(self); + break; + + case PA_ST_LEVEL_CLEAR: + changeStateLevelClear(self); + break; + + case PA_ST_PAUSE: + changeStatePause(self); + break; + + default: + break; + } + + self->gameData.changeState = 0; +} + +void detectBgmChange(pango_t* self) +{ + if (!self->gameData.changeBgm) + { + return; + } + + switch (self->gameData.changeBgm) + { + case PA_BGM_NULL: + if (self->gameData.currentBgm != PA_BGM_NULL) + { + soundStop(true); + } + break; + + case PA_BGM_MAIN: + if (self->gameData.currentBgm != PA_BGM_MAIN) + { + soundPlayBgm(&(self->soundManager.bgmDemagio), BZR_STEREO); + } + break; + + case PA_BGM_ATHLETIC: + if (self->gameData.currentBgm != PA_BGM_ATHLETIC) + { + soundPlayBgm(&(self->soundManager.bgmSmooth), BZR_STEREO); + } + break; + + case PA_BGM_UNDERGROUND: + if (self->gameData.currentBgm != PA_BGM_UNDERGROUND) + { + soundPlayBgm(&(self->soundManager.bgmUnderground), BZR_STEREO); + } + break; + + case PA_BGM_FORTRESS: + if (self->gameData.currentBgm != PA_BGM_FORTRESS) + { + soundPlayBgm(&(self->soundManager.bgmCastle), BZR_STEREO); + } + break; + + default: + break; + } + + self->gameData.currentBgm = self->gameData.changeBgm; + self->gameData.changeBgm = 0; +} + +void changeStateDead(pango_t* self) +{ + self->gameData.frameCount = 0; + self->gameData.lives--; + self->gameData.levelDeaths++; + self->gameData.combo = 0; + self->gameData.comboTimer = 0; + self->gameData.initialHp = 1; + + soundStop(true); + soundPlayBgm(&(self->soundManager.sndDie), BZR_STEREO); + + self->update = &updateDead; +} + +void updateDead(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, self->gameData.bgColor); + + self->gameData.frameCount++; + if (self->gameData.frameCount > 179) + { + if (self->gameData.lives > 0) + { + changeStateReadyScreen(self); + } + else + { + changeStateGameOver(self); + } + } + + pa_updateEntities(&(self->entityManager)); + pa_drawTileMap(&(self->tilemap)); + pa_drawEntities(&(self->entityManager)); + drawPangoHud(&(self->radiostars), &(self->gameData)); + + if (self->gameData.countdown < 0) + { + drawText(&(self->radiostars), c555, str_time_up, (TFT_WIDTH - textWidth(&(self->radiostars), str_time_up)) / 2, + 128); + } +} + +void updateGameOver(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + self->gameData.frameCount++; + if (self->gameData.frameCount > 179) + { + // Handle unlockables + + if (self->gameData.score >= BIG_SCORE) + { + self->unlockables.bigScore = true; + } + + if (self->gameData.score >= BIGGER_SCORE) + { + self->unlockables.biggerScore = true; + } + + if (!self->gameData.debugMode) + { + pangoSaveUnlockables(self); + } + + changeStateNameEntry(self); + } + + drawGameOver(&(self->radiostars), &(self->gameData)); + pa_updateLedsGameOver(&(self->gameData)); +} + +void changeStateGameOver(pango_t* self) +{ + self->gameData.frameCount = 0; + pa_resetGameDataLeds(&(self->gameData)); + soundPlayBgm(&(self->soundManager.bgmGameOver), BZR_STEREO); + self->update = &updateGameOver; +} + +void drawGameOver(font_t* font, paGameData_t* gameData) +{ + drawPangoHud(font, gameData); + drawText(font, c555, str_game_over, (TFT_WIDTH - textWidth(font, str_game_over)) / 2, 128); +} + +void changeStateTitleScreen(pango_t* self) +{ + self->gameData.frameCount = 0; + self->gameData.gameState = PA_ST_TITLE_SCREEN; + self->update = &updateTitleScreen; +} + +void changeStateLevelClear(pango_t* self) +{ + self->gameData.frameCount = 0; + self->gameData.checkpoint = 0; + self->gameData.levelDeaths = 0; + self->gameData.initialHp = self->entityManager.playerEntity->hp; + self->gameData.extraLifeCollected = false; + pa_resetGameDataLeds(&(self->gameData)); + self->update = &updateLevelClear; +} + +void updateLevelClear(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, self->gameData.bgColor); + + self->gameData.frameCount++; + + if (self->gameData.frameCount > 60) + { + if (self->gameData.countdown > 0) + { + self->gameData.countdown--; + + if (self->gameData.countdown % 2) + { + soundPlayBgm(&(self->soundManager.sndTally), BZR_STEREO); + } + + uint16_t comboPoints = 50 * self->gameData.combo; + + self->gameData.score += comboPoints; + self->gameData.comboScore = comboPoints; + + if (self->gameData.combo > 1) + { + self->gameData.combo--; + } + } + else if (self->gameData.frameCount % 60 == 0) + { + // Hey look, it's a frame rule! + + uint16_t levelIndex = getLevelIndex(self->gameData.world, self->gameData.level); + + if (levelIndex >= NUM_LEVELS - 1) + { + // Game Cleared! + + if (!self->gameData.debugMode) + { + // Determine achievements + self->unlockables.gameCleared = true; + + if (!self->gameData.continuesUsed) + { + self->unlockables.oneCreditCleared = true; + + if (self->gameData.inGameTimer < FAST_TIME) + { + self->unlockables.fastTime = true; + } + } + + if (self->gameData.score >= BIG_SCORE) + { + self->unlockables.bigScore = true; + } + + if (self->gameData.score >= BIGGER_SCORE) + { + self->unlockables.biggerScore = true; + } + } + + changeStateGameClear(self); + } + else + { + // Advance to the next level + self->gameData.level++; + if (self->gameData.level > 4) + { + self->gameData.world++; + self->gameData.level = 1; + } + + // Unlock the next level + levelIndex++; + if (levelIndex > self->unlockables.maxLevelIndexUnlocked) + { + self->unlockables.maxLevelIndexUnlocked = levelIndex; + } + + pa_setDifficultyLevel(&(pango->gameData), getLevelIndex(self->gameData.world, self->gameData.level)); + pa_loadMapFromFile(&(pango->tilemap), "preset.bin"); + pa_generateMaze(&(pango->tilemap)); + pa_placeEnemySpawns(&(pango->tilemap)); + + self->entityManager.activeEnemies = 0; + + changeStateReadyScreen(self); + } + + if (!self->gameData.debugMode) + { + pangoSaveUnlockables(self); + } + + return; + } + } + + pa_updateEntities(&(self->entityManager)); + pa_drawTileMap(&(self->tilemap)); + pa_drawEntities(&(self->entityManager)); + drawPangoHud(&(self->radiostars), &(self->gameData)); + drawLevelClear(&(self->radiostars), &(self->gameData)); + pa_updateLedsLevelClear(&(self->gameData)); +} + +void drawLevelClear(font_t* font, paGameData_t* gameData) +{ + drawPangoHud(font, gameData); + drawText(font, c000, str_well_done, (TFT_WIDTH - textWidth(font, str_well_done) + 1) >> 1, 129); + drawText(font, c553, str_well_done, (TFT_WIDTH - textWidth(font, str_well_done)) >> 1, 128); +} + +void changeStateGameClear(pango_t* self) +{ + self->gameData.frameCount = 0; + self->update = &updateGameClear; + pa_resetGameDataLeds(&(self->gameData)); + soundPlayBgm(&(self->soundManager.bgmSmooth), BZR_STEREO); +} + +void updateGameClear(pango_t* self, int64_t elapsedUs) +{ + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + self->gameData.frameCount++; + + if (self->gameData.frameCount > 450) + { + if (self->gameData.lives > 0) + { + if (self->gameData.frameCount % 60 == 0) + { + self->gameData.lives--; + self->gameData.score += 200000; + soundPlaySfx(&(self->soundManager.snd1up), BZR_STEREO); + } + } + else if (self->gameData.frameCount % 960 == 0) + { + changeStateGameOver(self); + } + } + + drawPangoHud(&(self->radiostars), &(self->gameData)); + drawGameClear(&(self->radiostars), &(self->gameData)); + pa_updateLedsGameClear(&(self->gameData)); +} + +void drawGameClear(font_t* font, paGameData_t* gameData) +{ + drawPangoHud(font, gameData); + + char timeStr[32]; + snprintf(timeStr, sizeof(timeStr) - 1, "in %06" PRIu32 " seconds!", gameData->inGameTimer); + + drawText(font, yellowColors[(gameData->frameCount >> 3) % 4], str_congrats, + (TFT_WIDTH - textWidth(font, str_congrats)) / 2, 48); + + if (gameData->frameCount > 120) + { + drawText(font, c555, "You've completed your", 8, 80); + drawText(font, c555, "trip across Swadge Land", 8, 96); + } + + if (gameData->frameCount > 180) + { + drawText(font, (gameData->inGameTimer < FAST_TIME) ? cyanColors[(gameData->frameCount >> 3) % 4] : c555, + timeStr, (TFT_WIDTH - textWidth(font, timeStr)) / 2, 112); + } + + if (gameData->frameCount > 300) + { + drawText(font, c555, "The Swadge staff", 8, 144); + drawText(font, c555, "thanks you for playing!", 8, 160); + } + + if (gameData->frameCount > 420) + { + drawText(font, (gameData->lives > 0) ? highScoreNewEntryColors[(gameData->frameCount >> 3) % 4] : c555, + "Bonus 200000pts per life!", (TFT_WIDTH - textWidth(font, "Bonus 100000pts per life!")) / 2, 192); + } + + /* + drawText(font, c555, "Thanks for playing.", 24, 48); + drawText(font, c555, "Many more battle scenes", 8, 96); + drawText(font, c555, "will soon be available!", 8, 112); + drawText(font, c555, "Bonus 100000pts per life!", 8, 160); + */ +} + +void pangoInitializeHighScores(pango_t* self) +{ + self->highScores.scores[0] = 100000; + self->highScores.scores[1] = 80000; + self->highScores.scores[2] = 40000; + self->highScores.scores[3] = 20000; + self->highScores.scores[4] = 10000; + + for (uint8_t i = 0; i < NUM_PLATFORMER_HIGH_SCORES; i++) + { + self->highScores.initials[i][0] = 'J' + i; + self->highScores.initials[i][1] = 'P' - i; + self->highScores.initials[i][2] = 'V' + i; + } +} + +void loadPangoHighScores(pango_t* self) +{ + size_t size = sizeof(pangoHighScores_t); + // Try reading the value + if (false == readNvsBlob(KEY_SCORES, &(self->highScores), &(size))) + { + // Value didn't exist, so write the default + pangoInitializeHighScores(self); + } +} + +void pangoSaveHighScores(pango_t* self) +{ + size_t size = sizeof(pangoHighScores_t); + writeNvsBlob(KEY_SCORES, &(self->highScores), size); +} + +void pangoInitializeUnlockables(pango_t* self) +{ + self->unlockables.maxLevelIndexUnlocked = 0; + self->unlockables.gameCleared = false; + self->unlockables.oneCreditCleared = false; + self->unlockables.bigScore = false; + self->unlockables.fastTime = false; + self->unlockables.biggerScore = false; +} + +void loadPangoUnlockables(pango_t* self) +{ + size_t size = sizeof(pangoUnlockables_t); + // Try reading the value + if (false == readNvsBlob(KEY_UNLOCKS, &(self->unlockables), &(size))) + { + // Value didn't exist, so write the default + pangoInitializeHighScores(self); + } +} + +void pangoSaveUnlockables(pango_t* self) +{ + size_t size = sizeof(pangoUnlockables_t); + writeNvsBlob(KEY_UNLOCKS, &(self->unlockables), size); +} + +void drawPangoHighScores(font_t* font, pangoHighScores_t* highScores, paGameData_t* gameData) +{ + drawText(font, c555, "RANK SCORE NAME", 48, 96); + for (uint8_t i = 0; i < NUM_PLATFORMER_HIGH_SCORES; i++) + { + char rowStr[32]; + snprintf(rowStr, sizeof(rowStr) - 1, "%d %06" PRIu32 " %c%c%c", i + 1, highScores->scores[i], + highScores->initials[i][0], highScores->initials[i][1], highScores->initials[i][2]); + drawText(font, (gameData->rank == i) ? highScoreNewEntryColors[(gameData->frameCount >> 3) % 4] : c555, rowStr, + 60, 128 + i * 16); + } +} + +uint8_t getHighScoreRank(pangoHighScores_t* highScores, uint32_t newScore) +{ + uint8_t i; + for (i = 0; i < NUM_PLATFORMER_HIGH_SCORES; i++) + { + if (highScores->scores[i] < newScore) + { + break; + } + } + + return i; +} + +void insertScoreIntoHighScores(pangoHighScores_t* highScores, uint32_t newScore, char newInitials[], uint8_t rank) +{ + if (rank >= NUM_PLATFORMER_HIGH_SCORES) + { + return; + } + + for (uint8_t i = NUM_PLATFORMER_HIGH_SCORES - 1; i > rank; i--) + { + highScores->scores[i] = highScores->scores[i - 1]; + highScores->initials[i][0] = highScores->initials[i - 1][0]; + highScores->initials[i][1] = highScores->initials[i - 1][1]; + highScores->initials[i][2] = highScores->initials[i - 1][2]; + } + + highScores->scores[rank] = newScore; + highScores->initials[rank][0] = newInitials[0]; + highScores->initials[rank][1] = newInitials[1]; + highScores->initials[rank][2] = newInitials[2]; +} + +void changeStateNameEntry(pango_t* self) +{ + self->gameData.frameCount = 0; + uint8_t rank = getHighScoreRank(&(self->highScores), self->gameData.score); + self->gameData.rank = rank; + self->menuState = 0; + + pa_resetGameDataLeds(&(self->gameData)); + + if (rank >= NUM_PLATFORMER_HIGH_SCORES || self->gameData.debugMode) + { + self->menuSelection = 0; + self->gameData.rank = NUM_PLATFORMER_HIGH_SCORES; + pangoChangeStateShowHighScores(self); + return; + } + + soundPlayBgm(&(self->soundManager.bgmNameEntry), BZR_STEREO); + self->menuSelection = self->gameData.initials[0]; + self->update = &updateNameEntry; +} + +void updateNameEntry(pango_t* self, int64_t elapsedUs) +{ + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + self->gameData.frameCount++; + + if (self->gameData.btnState & PB_LEFT && !(self->gameData.prevBtnState & PB_LEFT)) + { + self->menuSelection--; + + if (self->menuSelection < 32) + { + self->menuSelection = 90; + } + + self->gameData.initials[self->menuState] = self->menuSelection; + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + else if (self->gameData.btnState & PB_RIGHT && !(self->gameData.prevBtnState & PB_RIGHT)) + { + self->menuSelection++; + + if (self->menuSelection > 90) + { + self->menuSelection = 32; + } + + self->gameData.initials[self->menuState] = self->menuSelection; + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + else if (self->gameData.btnState & PB_B && !(self->gameData.prevBtnState & PB_B)) + { + if (self->menuState > 0) + { + self->menuState--; + self->menuSelection = self->gameData.initials[self->menuState]; + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + else + { + soundPlaySfx(&(self->soundManager.sndMenuDeny), BZR_STEREO); + } + } + else if (self->gameData.btnState & PB_A && !(self->gameData.prevBtnState & PB_A)) + { + self->menuState++; + + if (self->menuState > 2) + { + insertScoreIntoHighScores(&(self->highScores), self->gameData.score, self->gameData.initials, + self->gameData.rank); + pangoSaveHighScores(self); + pangoChangeStateShowHighScores(self); + soundPlaySfx(&(self->soundManager.sndPowerUp), BZR_STEREO); + } + else + { + self->menuSelection = self->gameData.initials[self->menuState]; + soundPlaySfx(&(self->soundManager.sndMenuSelect), BZR_STEREO); + } + } + + drawNameEntry(&(self->radiostars), &(self->gameData), self->menuState); + pa_updateLedsShowHighScores(&(self->gameData)); +} + +void drawNameEntry(font_t* font, paGameData_t* gameData, uint8_t currentInitial) +{ + drawText(font, greenColors[(pango->gameData.frameCount >> 3) % 4], str_initials, + (TFT_WIDTH - textWidth(font, str_initials)) / 2, 64); + + char rowStr[32]; + snprintf(rowStr, sizeof(rowStr) - 1, "%d %06" PRIu32, gameData->rank + 1, gameData->score); + drawText(font, c555, rowStr, 64, 128); + + for (uint8_t i = 0; i < 3; i++) + { + snprintf(rowStr, sizeof(rowStr) - 1, "%c", gameData->initials[i]); + drawText(font, (currentInitial == i) ? highScoreNewEntryColors[(gameData->frameCount >> 3) % 4] : c555, rowStr, + 192 + 16 * i, 128); + } +} + +void pangoChangeStateShowHighScores(pango_t* self) +{ + self->gameData.frameCount = 0; + self->update = &updateShowHighScores; +} + +void updateShowHighScores(pango_t* self, int64_t elapsedUs) +{ + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c000); + + self->gameData.frameCount++; + + if ((self->gameData.frameCount > 300) + || (((self->gameData.btnState & PB_START) && !(self->gameData.prevBtnState & PB_START)) + || ((self->gameData.btnState & PB_A) && !(self->gameData.prevBtnState & PB_A)))) + { + self->menuState = 0; + self->menuSelection = 0; + soundStop(true); + changeStateTitleScreen(self); + } + + drawShowHighScores(&(self->radiostars), self->menuState); + drawPangoHighScores(&(self->radiostars), &(self->highScores), &(self->gameData)); + + pa_updateLedsShowHighScores(&(self->gameData)); +} + +void drawShowHighScores(font_t* font, uint8_t menuState) +{ + if (pango->easterEgg) + { + drawText(font, highScoreNewEntryColors[(pango->gameData.frameCount >> 3) % 4], str_hbd, + (TFT_WIDTH - textWidth(font, str_hbd)) / 2, 32); + } + else if (menuState == 3) + { + drawText(font, redColors[(pango->gameData.frameCount >> 3) % 4], str_registrated, + (TFT_WIDTH - textWidth(font, str_registrated)) / 2, 32); + } + else + { + drawText(font, c555, str_do_your_best, (TFT_WIDTH - textWidth(font, str_do_your_best)) / 2, 32); + } +} + +void changeStatePause(pango_t* self) +{ + soundStop(true); + soundPlaySfx(&(self->soundManager.sndPause), BZR_STEREO); + self->update = &updatePause; +} + +void updatePause(pango_t* self, int64_t elapsedUs) +{ + if (((self->gameData.btnState & PB_START) && !(self->gameData.prevBtnState & PB_START))) + { + soundPlaySfx(&(self->soundManager.sndPause), BZR_STEREO); + self->gameData.changeBgm = self->gameData.currentBgm; + self->gameData.currentBgm = PA_BGM_NULL; + self->update = &updateGame; + } + + pa_drawTileMap(&(self->tilemap)); + pa_drawEntities(&(self->entityManager)); + drawPangoHud(&(self->radiostars), &(self->gameData)); + drawPause(&(self->radiostars)); +} + +void drawPause(font_t* font) +{ + drawText(font, c000, str_pause, (TFT_WIDTH - textWidth(font, str_pause) + 2) >> 1, 129); + drawText(font, c553, str_pause, (TFT_WIDTH - textWidth(font, str_pause)) >> 1, 128); +} + +uint16_t getLevelIndex(uint8_t world, uint8_t level) +{ + return (world - 1) * 4 + (level - 1); +} \ No newline at end of file diff --git a/main/modes/games/pango/pango.h b/main/modes/games/pango/pango.h new file mode 100644 index 000000000..74d86ca46 --- /dev/null +++ b/main/modes/games/pango/pango.h @@ -0,0 +1,47 @@ +#ifndef _MODE_PLATFORMER_H_ +#define _MODE_PLATFORMER_H_ +//============================================================================== +// Includes +//============================================================================== + +#include "pango_typedef.h" +#include "swadge2024.h" + +/*============================================================================== + * Constants + *============================================================================*/ + +#define NUM_PLATFORMER_HIGH_SCORES 5 + +extern const char pangoName[]; + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + uint32_t scores[NUM_PLATFORMER_HIGH_SCORES]; + char initials[NUM_PLATFORMER_HIGH_SCORES][3]; +} pangoHighScores_t; + +typedef struct +{ + uint8_t maxLevelIndexUnlocked; + bool gameCleared; + bool oneCreditCleared; + bool bigScore; + bool fastTime; + bool biggerScore; +} pangoUnlockables_t; + +//============================================================================== +// Prototypes +//============================================================================== + +void updateGame(pango_t* pango, int64_t elapsedUs); +void updateTitleScreen(pango_t* pango, int64_t elapsedUs); + +extern swadgeMode_t pangoMode; + +#endif diff --git a/main/modes/games/pango/pango_typedef.h b/main/modes/games/pango/pango_typedef.h new file mode 100644 index 000000000..151211633 --- /dev/null +++ b/main/modes/games/pango/pango_typedef.h @@ -0,0 +1,71 @@ +#ifndef PANGO_COMMON_TYPEDEF_INCLUDED +#define PANGO_COMMON_TYPEDEF_INCLUDED + +typedef struct pango_t pango_t; +typedef struct paEntityManager_t paEntityManager_t; +typedef struct paTilemap_t paTilemap_t; +typedef struct paEntity_t paEntity_t; + +typedef enum +{ + PA_ST_NULL, + PA_ST_TITLE_SCREEN, + PA_ST_READY_SCREEN, + PA_ST_GAME, + PA_ST_DEAD, + PA_ST_LEVEL_CLEAR, + PA_ST_WORLD_CLEAR, + PA_ST_GAME_CLEAR, + PA_ST_GAME_OVER, + PA_ST_HIGH_SCORE_ENTRY, + PA_ST_HIGH_SCORE_TABLE, + PA_ST_PAUSE +} pa_gameStateEnum_t; + +typedef enum +{ + PA_BGM_NO_CHANGE, + PA_BGM_MAIN, + PA_BGM_ATHLETIC, + PA_BGM_UNDERGROUND, + PA_BGM_FORTRESS, + PA_BGM_NULL +} pa_bgmEnum_t; + +typedef enum +{ + PA_SP_PLAYER_SOUTH, + PA_SP_PLAYER_WALK_SOUTH, + PA_SP_PLAYER_NORTH, + PA_SP_PLAYER_WALK_NORTH, + PA_SP_PLAYER_SIDE, + PA_SP_PLAYER_WALK_SIDE_1, + PA_SP_PLAYER_WALK_SIDE_2, + PA_SP_PLAYER_PUSH_SOUTH_1, + PA_SP_PLAYER_PUSH_SOUTH_2, + PA_SP_PLAYER_PUSH_NORTH_1, + PA_SP_PLAYER_PUSH_NORTH_2, + PA_SP_PLAYER_PUSH_SIDE_1, + PA_SP_PLAYER_PUSH_SIDE_2, + PA_SP_PLAYER_HURT, + PA_SP_PLAYER_WIN, + PA_SP_PLAYER_ICON, + PA_SP_BLOCK, + PA_SP_BONUS_BLOCK, + PA_SP_ENEMY_SOUTH, + PA_SP_ENEMY_NORTH, + PA_SP_ENEMY_SIDE_1, + PA_SP_ENEMY_SIDE_2, + PA_SP_ENEMY_DRILL_SOUTH, + PA_SP_ENEMY_DRILL_NORTH, + PA_SP_ENEMY_DRILL_SIDE_1, + PA_SP_ENEMY_DRILL_SIDE_2, + PA_SP_ENEMY_STUN, + PA_SP_BREAK_BLOCK, + PA_SP_BREAK_BLOCK_1, + PA_SP_BREAK_BLOCK_2, + PA_SP_BREAK_BLOCK_3, + PA_SP_BLOCK_FRAGMENT +} pa_spriteDef_t; + +#endif \ No newline at end of file diff --git a/main/modes/music/usbsynth/mode_synth.c b/main/modes/music/usbsynth/mode_synth.c index da238c3d0..5bdb34af9 100644 --- a/main/modes/music/usbsynth/mode_synth.c +++ b/main/modes/music/usbsynth/mode_synth.c @@ -18,6 +18,8 @@ #include "cnfs_image.h" #include "ctype.h" +#include + #include "midiPlayer.h" #include "midiFileParser.h" #include "midiUsb.h" @@ -283,6 +285,7 @@ typedef struct lfsrState_t shuffleState; int32_t shufflePos; int32_t headroom; + bool gmMode; wsg_t instrumentImages[16]; wsg_t percussionImage; @@ -1038,6 +1041,7 @@ static const char* menuItemIgnore = "Enabled: "; static const char* menuItemBank = "Bank Select: "; static const char* menuItemInstrument = "Instrument: "; static const char* menuItemResetAll = "Reset All Channels"; +static const char* menuItemGm = "General MIDI: "; static const char* menuItemReset = "Reset"; static const char* menuItemControls = "Controllers"; @@ -1057,6 +1061,7 @@ static const char* const nvsKeyIgnoreChan = "synth_ignorech"; static const char* const nvsKeyChanPerc = "synth_chpercus"; static const char* const nvsKeySynthConf = "synth_confblob"; static const char* const nvsKeySynthControlConf = "synth_ctrlconf"; +static const char* const nvsKeyGmEnabled = "synth_gmmode"; static const char* const menuItemModeOptions[] = { "Streaming", @@ -1190,6 +1195,11 @@ static const int32_t menuItemChannelsValues[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, }; +static const int32_t menuItemGmValues[] = { + 0, + 1, +}; + static settingParam_t menuItemModeBounds = { .def = 0, .min = 0, @@ -1288,6 +1298,13 @@ static settingParam_t menuItemChannelsBounds = { .key = NULL, }; +static settingParam_t menuItemGmBounds = { + .def = 0, + .min = 0, + .max = 1, + .key = nvsKeyGmEnabled, +}; + static const synthConfig_t defaultSynthConfig = { .ignoreChannelMask = 0, .percChannelMask = 0x0200, // Channel 10 set only @@ -1306,6 +1323,24 @@ static const synthConfig_t defaultSynthConfig = { .controlCounts = 0, }; +static const synthConfig_t nonGmSynthConfig = { + .ignoreChannelMask = 0, + .percChannelMask = 0x0600, // Channel 10 and 11 set + .programs = { + 0, 1, 2, 3, + 4, 5, 6, 7, + 8, 0, 0, 0, + 0, 0, 0, 0, + }, + .banks = { + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 0, 1, 0, + 0, 0, 0, 0, + }, + .controlCounts = 0, +}; + const char synthModeName[] = "MIDI Player"; static const char intermissionMsg[] = "SWADGAOKE!"; @@ -1351,7 +1386,7 @@ static synthData_t* sd; static void synthEnterMode(void) { - sd = calloc(1, sizeof(synthData_t)); + sd = heap_caps_calloc(1, sizeof(synthData_t), MALLOC_CAP_SPIRAM); loadFont("ibm_vga8.font", &sd->font, true); loadFont("sonic.font", &sd->betterFont, true); makeOutlineFont(&sd->betterFont, &sd->betterOutline, true); @@ -1428,6 +1463,12 @@ static void synthEnterMode(void) sd->headroom = nvsRead; sd->midiPlayer.headroom = sd->headroom; + if (!readNvs32(nvsKeyGmEnabled, &nvsRead)) + { + nvsRead = 0; + } + sd->gmMode = nvsRead ? true : false; + bool useDefaultConfig = true; size_t configBlobLen; if (readNvsBlob(nvsKeySynthConf, NULL, &configBlobLen)) @@ -1473,7 +1514,8 @@ static void synthEnterMode(void) { for (int blobIdx = 0; blobIdx < sd->synthConfig.controlCounts; blobIdx++) { - synthControlConfig_t* copy = (synthControlConfig_t*)malloc(sizeof(synthControlConfig_t)); + synthControlConfig_t* copy + = (synthControlConfig_t*)heap_caps_malloc(sizeof(synthControlConfig_t), MALLOC_CAP_SPIRAM); if (copy) { memcpy(copy, &configs[blobIdx], sizeof(synthControlConfig_t)); @@ -1490,7 +1532,7 @@ static void synthEnterMode(void) size_t savedNameLen; if (readNvsBlob(nvsKeyLastSong, NULL, &savedNameLen)) { - sd->filenameBuf = malloc(savedNameLen < 128 ? 128 : savedNameLen + 1); + sd->filenameBuf = heap_caps_malloc(savedNameLen < 128 ? 128 : savedNameLen + 1, MALLOC_CAP_SPIRAM); if (readNvsBlob(nvsKeyLastSong, sd->filenameBuf, &savedNameLen)) { @@ -1986,6 +2028,15 @@ static void synthApplyConfig(void) { sd->midiPlayer.headroom = sd->headroom; + if (sd->gmMode) + { + midiGmOn(&sd->midiPlayer); + } + else + { + midiGmOff(&sd->midiPlayer); + } + for (int i = 0; i < 16; i++) { uint16_t channelBit = (1 << i); @@ -2044,7 +2095,7 @@ static void synthSaveControl(uint8_t channel, uint8_t control, uint8_t value) } // Not found, add one - synthControlConfig_t* conf = calloc(1, sizeof(synthControlConfig_t)); + synthControlConfig_t* conf = heap_caps_calloc(1, sizeof(synthControlConfig_t), MALLOC_CAP_SPIRAM); if (conf) { conf->control = control; @@ -2351,6 +2402,11 @@ static void addChannelsMenu(menu_t* menu, const synthConfig_t* config) addSingleItemToMenu(menu, menuItemResetAll); wheelMenuSetItemInfo(sd->wheelMenu, menuItemResetAll, &sd->resetImage, rotTopMenu++, NO_SCROLL); + + addSettingsOptionsItemToMenu(menu, menuItemGm, menuItemOffOnOptions, menuItemGmValues, ARRAY_SIZE(menuItemGmValues), + &menuItemGmBounds, sd->gmMode); + wheelMenuSetItemInfo(sd->wheelMenu, menuItemGm, NULL, rotTopMenu++, SCROLL_HORIZ_R); + wheelMenuSetItemTextIcon(sd->wheelMenu, menuItemGm, "GM"); } static void synthSetupMenu(bool forceReset) @@ -2521,7 +2577,7 @@ static void preloadLyrics(karaokeInfo_t* karInfo, const midiFile_t* midiFile) // TODO we could save a couple bytes if we parsed the file an additional time to check how many events // there are in total... - midiTextInfo_t* info = (midiTextInfo_t*)malloc(sizeof(midiTextInfo_t)); + midiTextInfo_t* info = (midiTextInfo_t*)heap_caps_malloc(sizeof(midiTextInfo_t), MALLOC_CAP_SPIRAM); if (info) { @@ -3956,7 +4012,7 @@ static void midiTextCallback(metaEventType_t type, const char* text, uint32_t le return; } - midiTextInfo_t* info = (midiTextInfo_t*)malloc(sizeof(midiTextInfo_t)); + midiTextInfo_t* info = (midiTextInfo_t*)heap_caps_malloc(sizeof(midiTextInfo_t), MALLOC_CAP_SPIRAM); if (info) { @@ -4124,7 +4180,7 @@ static void synthMenuCb(const char* label, bool selected, uint32_t value) sd->screen = SS_FILE_SELECT; if (!sd->filenameBuf) { - sd->filenameBuf = calloc(1, 128); + sd->filenameBuf = heap_caps_calloc(1, 128, MALLOC_CAP_SPIRAM); } if (!sd->filenameBuf) @@ -4233,6 +4289,24 @@ static void synthMenuCb(const char* label, bool selected, uint32_t value) sd->menuSelectedChannel = value; sd->updateMenu = true; } + else if (label == menuItemGm) + { + if (value != sd->gmMode) + { + // Also reset all channels + memcpy(&sd->synthConfig, (value ? &defaultSynthConfig : &nonGmSynthConfig), sizeof(synthConfig_t)); + synthControlConfig_t* control; + while (NULL != (control = pop(&sd->controllerSettings))) + { + free(control); + } + + sd->gmMode = value; + writeNvs32(nvsKeyGmEnabled, value); + synthApplyConfig(); + sd->updateMenu = true; + } + } else { // Mapped menu items diff --git a/main/modes/system/mainMenu/mainMenu.c b/main/modes/system/mainMenu/mainMenu.c index efe0b4d7e..ec9848bee 100644 --- a/main/modes/system/mainMenu/mainMenu.c +++ b/main/modes/system/mainMenu/mainMenu.c @@ -20,6 +20,7 @@ #include "mode_swadgeHero.h" #include "mode_synth.h" #include "ultimateTTT.h" +#include "pango.h" #include "touchTest.h" #include "tunernome.h" #include "keebTest.h" @@ -153,6 +154,7 @@ static void mainMenuEnterMode(void) mainMenu->menu = startSubMenu(mainMenu->menu, "Games"); addSingleItemToMenu(mainMenu->menu, swadgeHeroMode.modeName); addSingleItemToMenu(mainMenu->menu, tttMode.modeName); + addSingleItemToMenu(mainMenu->menu, pangoMode.modeName); addSingleItemToMenu(mainMenu->menu, t48Mode.modeName); addSingleItemToMenu(mainMenu->menu, bigbugMode.modeName); mainMenu->menu = endSubMenu(mainMenu->menu); @@ -368,6 +370,10 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) { switchToSwadgeMode(&swadgeHeroMode); } + else if (label == pangoMode.modeName) + { + switchToSwadgeMode(&pangoMode); + } else if (label == timerMode.modeName) { switchToSwadgeMode(&timerMode); diff --git a/main/swadge2024.c b/main/swadge2024.c index 2e57c7544..e350f4798 100644 --- a/main/swadge2024.c +++ b/main/swadge2024.c @@ -27,6 +27,9 @@ * If you just want to run the Swadge emulator without setting up a development environment, see the \ref emulator * for an installation guide and usage instructions. * + * If you want to learn about creating MIDI song files for the Swadge, see the \ref MIDI guide. See also the + * \ref emulator which you can use to listen to MIDI files. + * * If you're just starting Swadge development, you're already at the right place to start! Here's a good sequence of * pages to read from here. * diff --git a/tools/pango_editor/mockup.bin b/tools/pango_editor/mockup.bin new file mode 100644 index 0000000000000000000000000000000000000000..b5519a2b6c80ef11660094c55880c696256c3797 GIT binary patch literal 274 zcmZvX+YW#r3`6S`#*2yn|FN#riHkNNETcOe=P}J$3n~EfL-Afw&Ac+I-`;sSZV%tl r + + + + +2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,0, +9,0,0,0,10,0,0,0,0,0,0,0,10,0,0,0,5,0, +9,10,10,0,10,0,10,10,10,10,10,0,10,0,10,10,5,0, +9,0,10,0,0,0,0,0,0,0,10,0,10,0,0,0,5,0, +9,0,10,10,10,0,11,10,10,0,10,0,10,0,10,10,5,0, +9,0,0,0,0,0,0,0,10,0,0,0,0,0,10,0,5,0, +9,10,10,10,10,10,10,0,10,0,10,10,10,10,10,0,5,0, +9,0,10,0,0,0,10,0,0,0,0,0,11,0,0,0,5,0, +9,0,10,0,10,0,10,10,10,10,10,0,10,0,10,10,5,0, +9,0,10,0,10,0,10,0,0,0,0,0,0,0,10,0,5,0, +9,0,11,10,10,0,10,0,10,0,10,10,10,10,10,0,5,0, +9,0,10,0,0,0,10,0,10,0,10,0,10,0,0,0,5,0, +9,0,10,0,10,10,10,0,10,0,10,0,10,0,10,10,5,0, +9,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,5,0, +7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,6,0 + + + diff --git a/tools/pango_editor/pa-tileset.png b/tools/pango_editor/pa-tileset.png new file mode 100644 index 0000000000000000000000000000000000000000..a836e3057b6d9131ddcdd9fb2956c414f9365557 GIT binary patch literal 1209 zcmds#*-OEgh-%>V~h0QdPyTLq9!GoOEuI((I&LeMM_Muh)S}F zOv#}1LF}Qx2qY1TnnEIjx&)CA78WgrLPFE0`7;{OA2a8i@A)pby*#`*5T6_`B7w5f zk_x`#{4I@(<$w9*n4f%Nj#g|glEIfLpG6jQly9vH@u)sov`r3&YS}MJ^lQrn4GGt9p_%>XCB^GP@gop`qtv2Uz2Hvw$z2Yrq@QF zd~J%O`}Uo=NLxZ&ATE$`^X`MQ zeZ4ofjWt(PjjbpMC8oO8lPYy|s7+cLf_q1YGWHZ7ShPRySYq?l;;E-KiQQ={uNUt4 z-4JZN6xrSq_i9PUpWd_$;}i2Er(91YYh?ae=CbaBUHS3B&z literal 0 HcmV?d00001 diff --git a/tools/pango_editor/pa-tileset.tsx b/tools/pango_editor/pa-tileset.tsx new file mode 100644 index 000000000..75765c532 --- /dev/null +++ b/tools/pango_editor/pa-tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tools/pango_editor/pango-editor.js b/tools/pango_editor/pango-editor.js new file mode 100644 index 000000000..ffe438efd --- /dev/null +++ b/tools/pango_editor/pango-editor.js @@ -0,0 +1,111 @@ +tiled.registerMapFormat("Pango", { + name: "Pango map format", + extension: "bin", + read: (fileName) => { + var file = new BinaryFile(fileName, BinaryFile.ReadOnly); + var filePath = FileInfo.path(fileName); + var buffer = file.read(file.size); + var view = new Uint8Array(buffer); + const tileDataOffset = 2; + const tileSizeInPixels = 16; + + var map = new TileMap(); + + //The first two bytes contain the width and height of the tilemap in tiles + var tileDataLength = view[0] * view[1]; + map.setSize(view[0], view[1]); + map.setTileSize(tileSizeInPixels, tileSizeInPixels); + + var tileset = tiled.open(filePath + '/pa-tileset.tsx'); + + var layer = new TileLayer(); + + map.addTileset(tileset); + + layer.width = map.width; + layer.height = map.height; + layer.name = 'Main'; + + var layerEdit = layer.edit(); + var importTileX = 0; + var importTileY = 0; + + + //Import tile data + for(let i = 0; i < tileDataLength; i++){ + let tileId = view[i + tileDataOffset]; + layerEdit.setTile(importTileX, importTileY, tileset.tile(tileId)); + + importTileX++; + if(importTileX >= map.width){ + importTileY++; + importTileX=0; + } + } + + layerEdit.apply(); + + map.addLayer(layer); + file.close(); + return map; + }, + write: (map, fileName) => { + for (let i = 0; i < map.layerCount; ++i) { + const layer = map.layerAt(i); + + if (!layer.isTileLayer) { + continue; + } + + let file = new BinaryFile(fileName, BinaryFile.WriteOnly); + let buffer = new ArrayBuffer(2 + layer.width * layer.height + 2); //Buffer sized to lenth byte + width byte + length * width of level bytes + 2 total target tile bytes + let view = new Uint8Array(buffer); + + //The first two bytes contain the width and height of the tilemap in tiles + view[0]=layer.width; + view[1]=layer.height; + let writePosition = 2; + let totalTargetBlockTiles = 0; + + for (let y = 0; y < layer.height; ++y) { + const row = []; + + for (let x = 0; x < layer.width; ++x) { + const tile = layer.tileAt(x, y); + if(!tile){ + //file.write(0); + view[writePosition] = 0; + writePosition++; + continue; + } + + const tileId = tile.id; + + //Handle "target block tiles" + //These are the blocks that the player must break to complete the level. + //if(tileId >= 16 && tileId <= 127) { + // totalTargetBlockTiles++; + //} + + //Handling every tile + view[writePosition]=tileId; + + writePosition++; + } + } + + //The last 2 bytes hold the total number of "target block tiles" + //Forced into a (hopefully) unsigned 16 bit integer, little endian + //There's probably a better way to do this... + //let totalTargetBlockTilesLowerByte = totalTargetBlockTiles & 255; + //let totalTargetBlockTilesUpperByte = (totalTargetBlockTiles >> 8) & 255; + //view[writePosition] = totalTargetBlockTilesLowerByte; + //writePosition++; + //view[writePosition] = totalTargetBlockTilesUpperByte; + //writePosition++; + + file.write(buffer); + file.commit(); + } + }, +}); \ No newline at end of file diff --git a/tools/pango_editor/pango-tiles.tsx b/tools/pango_editor/pango-tiles.tsx new file mode 100644 index 000000000..ed777d450 --- /dev/null +++ b/tools/pango_editor/pango-tiles.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tools/pango_editor/pango.tiled-project b/tools/pango_editor/pango.tiled-project new file mode 100644 index 000000000..d58954aef --- /dev/null +++ b/tools/pango_editor/pango.tiled-project @@ -0,0 +1,11 @@ +{ + "automappingRulesFile": "", + "commands": [ + ], + "extensionsPath": "extensions", + "folders": [ + "." + ], + "propertyTypes": [ + ] +} diff --git a/tools/pango_editor/pango.tiled-session b/tools/pango_editor/pango.tiled-session new file mode 100644 index 000000000..eca5100ff --- /dev/null +++ b/tools/pango_editor/pango.tiled-session @@ -0,0 +1,65 @@ +{ + "Map/SizeTest": { + "height": 4300, + "width": 2 + }, + "activeFile": "mockup.tmx", + "expandedProjectPaths": [ + ], + "file.lastUsedOpenFilter": "Pango map format (*.bin)", + "fileStates": { + "mockup.bin": { + "scale": 2.3914930555555554, + "selectedLayer": 0, + "viewCenter": { + "x": 144.47041742286754, + "y": 120.63593466424685 + } + }, + "mockup.tmx": { + "scale": 2.39, + "selectedLayer": 0, + "viewCenter": { + "x": 144.14225941422592, + "y": 120.2928870292887 + } + }, + "pa-tileset.tsx": { + "scaleInDock": 1, + "scaleInEditor": 1 + }, + "preset.tmx": { + "scale": 2.39, + "selectedLayer": 0, + "viewCenter": { + "x": 144.14225941422592, + "y": 120.2928870292887 + } + } + }, + "last.exportedFilePath": "/Users/jvega/Development/Swadge/esp32/2025/Swadge-IDF-5.0/tools/pango_editor", + "last.imagePath": "/Users/jvega/Development/Swadge/esp32/2025/Swadge-IDF-5.0/tools/pango_editor", + "map.height": 15, + "map.lastUsedExportFilter": "Pango map format (*.bin)", + "map.lastUsedFormat": "tmx", + "map.tileHeight": 16, + "map.tileWidth": 16, + "map.width": 18, + "openFiles": [ + "pa-tileset.tsx", + "mockup.tmx", + "preset.tmx" + ], + "project": "pango.tiled-project", + "recentFiles": [ + "pa-tileset.tsx", + "preset.tmx", + "mockup.tmx", + "mockup.bin" + ], + "tileset.lastUsedFormat": "tsx", + "tileset.tileSize": { + "height": 16, + "width": 16 + } +} diff --git a/tools/pango_editor/preset.tmx b/tools/pango_editor/preset.tmx new file mode 100644 index 000000000..9933afcd7 --- /dev/null +++ b/tools/pango_editor/preset.tmx @@ -0,0 +1,23 @@ + + + + + +2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,5,0, +7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,6,0 + + + diff --git a/tools/sandbox_test/usbhid_test/sandbox.c b/tools/sandbox_test/usbhid_test/sandbox.c index cdc6bb66d..b7f177c00 100644 --- a/tools/sandbox_test/usbhid_test/sandbox.c +++ b/tools/sandbox_test/usbhid_test/sandbox.c @@ -124,7 +124,7 @@ int16_t sandboxAdvancedUSB(uint8_t* buffer, uint16_t length, uint8_t isGet) buffer[1] = 0x55; buffer[2] = 0xaa; buffer[3] = 0x55; - return 64; + return 254; } else { diff --git a/tools/sandbox_test/usbhid_test/test/Makefile b/tools/sandbox_test/usbhid_test/test/Makefile index ac4a4fb25..448ff94cf 100644 --- a/tools/sandbox_test/usbhid_test/test/Makefile +++ b/tools/sandbox_test/usbhid_test/test/Makefile @@ -1,7 +1,17 @@ all : hidtest +ifeq ($(shell uname), Linux) +CFLAGS:=-g -O0 +LDFLAGS:=-ludev +CC:=gcc +else +CFLAGS:=-Os -s +CC:=gcc +LDFLAGS:=C:/windows/system32/setupapi.dll +endif + hidtest : hidtest.c - gcc -o $@ $^ -ludev + $(CC) -o $@ $^ $(CFLAGS) $(LDFLAGS) clean : rm -rf hidtest diff --git a/tools/sandbox_test/usbhid_test/test/hidtest.c b/tools/sandbox_test/usbhid_test/test/hidtest.c index 37e23b9a4..0b29082cf 100644 --- a/tools/sandbox_test/usbhid_test/test/hidtest.c +++ b/tools/sandbox_test/usbhid_test/test/hidtest.c @@ -1,6 +1,5 @@ #include #include -#include #include #include @@ -12,9 +11,12 @@ #define PID 0x4269 #ifdef WIN32 -const int reg_packet_length = 65; +const int reg_packet_length = 255; +const int reg_back_rest = 256; #else -const int reg_packet_length = 64; +#include +const int reg_packet_length = 254; +const int reg_back_rest = 254; #endif hid_device * hd; @@ -28,7 +30,7 @@ int main( int argc, char ** argv ) if( !hd ) { fprintf( stderr, "Could not open USB\n" ); return -94; } // Disable tick. - uint8_t rdata[65] = { 0 }; + uint8_t rdata[256] = { 0 }; rdata[0] = 173; r = hid_get_feature_report( hd, rdata, reg_packet_length ); printf( "Got data: %d bytes\n", r ); @@ -43,13 +45,14 @@ int main( int argc, char ** argv ) for( i = 0; i < 1024; i++ ) { r = hid_get_feature_report( hd, rdata, reg_packet_length ); - if( r != reg_packet_length ) + rdata[0] = 173; + if( r != reg_back_rest ) { fprintf( stderr, "Error reading message (%d)\n", r ); } } double dEnd = OGGetAbsoluteTime(); - printf( "Reads: %5.0f/sec / %3.2f kB/s\n", 1024.0/(dEnd - dStart), (63)/(dEnd - dStart)); + printf( "Reads: %5.0f/sec / %3.2f kB/s\n", 1024.0/(dEnd - dStart), (reg_packet_length)/(dEnd - dStart)); dStart = OGGetAbsoluteTime(); for( i = 0; i < 1024; i++ ) @@ -59,11 +62,11 @@ int main( int argc, char ** argv ) r = hid_send_feature_report( hd, rdata, reg_packet_length ); if( r != reg_packet_length ) { - fprintf( stderr, "Error reading message (%d)\n", r ); + fprintf( stderr, "Error writing message (%d)\n", r ); } } dEnd = OGGetAbsoluteTime(); - printf( "Writes: %5.0f/sec / %3.2f kB/s\n", 1024.0/(dEnd - dStart), (63)/(dEnd - dStart) ); + printf( "Writes: %5.0f/sec / %3.2f kB/s\n", 1024.0/(dEnd - dStart), (reg_packet_length)/(dEnd - dStart) ); rdata[0] = 173; rdata[1] = 0x00; @@ -78,7 +81,7 @@ int main( int argc, char ** argv ) int f; for( f = 0; f < 10; f++ ) { - for( y = 0; y < 240; y++ ) + for( y = 0; y < 240; y+=10 ) { for( x = 0; x < 280; x += 56 ) { diff --git a/tools/sandbox_test/usbhid_test/test/winbuild.bat b/tools/sandbox_test/usbhid_test/test/winbuild.bat new file mode 100644 index 000000000..0011c1a9c --- /dev/null +++ b/tools/sandbox_test/usbhid_test/test/winbuild.bat @@ -0,0 +1 @@ +tcc hidtest.c C:/windows/system32/setupapi.dll \ No newline at end of file From 957d7b5506ec90cfd8276b88e315faba831a662a Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sun, 22 Sep 2024 18:20:19 +0000 Subject: [PATCH 33/54] Squash merge origin/main into ssr --- assets/soko/levels/SK_LEVEL_LIST.txt | 22 + assets/soko/levels/classic/sck_c_threes.tmx | 26 + assets/soko/levels/classic/sk_c_alignthat.tmx | 27 + assets/soko/levels/classic/sk_c_cosms.tmx | 30 + assets/soko/levels/classic/sk_c_files.tmx | 23 + assets/soko/levels/classic/sk_c_fours.tmx | 27 + assets/soko/levels/classic/sk_c_plus.tmx | 23 + assets/soko/levels/classic/sk_c_test2.tmx | 29 + assets/soko/levels/euler/sk_e_a-frame.tmx | 25 + assets/soko/levels/euler/sk_e_alkatraz.tmx | 38 + assets/soko/levels/euler/sk_e_apollo.tmx | 34 + assets/soko/levels/euler/sk_e_camera.tmx | 25 + assets/soko/levels/euler/sk_e_casette.tmx | 34 + assets/soko/levels/euler/sk_e_copymachine.tmx | 50 + assets/soko/levels/euler/sk_e_curlingiron.tmx | 26 + assets/soko/levels/euler/sk_e_doubleblock.tmx | 25 + assets/soko/levels/euler/sk_e_feint.tmx | 23 + assets/soko/levels/euler/sk_e_groundwork.tmx | 23 + assets/soko/levels/euler/sk_e_harmonica.tmx | 25 + assets/soko/levels/euler/sk_e_mouse.tmx | 22 + assets/soko/levels/euler/sk_e_spine.tmx | 28 + assets/soko/levels/euler/sk_e_spiral.tmx | 23 + assets/soko/levels/euler/sk_e_start.tmx | 23 + .../soko/levels/euler/sk_e_steeringwheel.tmx | 21 + assets/soko/levels/euler/sk_e_threestep.tmx | 42 + assets/soko/levels/euler/sk_e_threestep2.tmx | 38 + assets/soko/levels/euler/sk_e_throughput.tmx | 34 + assets/soko/levels/euler/sk_e_tunnels.tmx | 23 + assets/soko/levels/euler/sk_e_waterwheel.tmx | 69 + assets/soko/levels/sk_e_overworld.tmx | 154 ++ assets/soko/levels/sk_overworld.tmx | 134 ++ assets/soko/sprites/pango/sk_pango_back1.png | Bin 0 -> 183 bytes assets/soko/sprites/pango/sk_pango_back2.png | Bin 0 -> 181 bytes assets/soko/sprites/pango/sk_pango_fwd1.png | Bin 0 -> 207 bytes assets/soko/sprites/pango/sk_pango_fwd2.png | Bin 0 -> 208 bytes assets/soko/sprites/pango/sk_pango_side1.png | Bin 0 -> 211 bytes assets/soko/sprites/pango/sk_pango_side2.png | Bin 0 -> 284 bytes assets/soko/sprites/pixel/sk_pixel_back.png | Bin 0 -> 257 bytes assets/soko/sprites/pixel/sk_pixel_front.png | Bin 0 -> 297 bytes assets/soko/sprites/pixel/sk_pixel_left.png | Bin 0 -> 276 bytes assets/soko/sprites/pixel/sk_pixel_right.png | Bin 0 -> 370 bytes assets/soko/sprites/sk_crate.png | Bin 0 -> 1476 bytes assets/soko/sprites/sk_crate_2.png | Bin 0 -> 1448 bytes assets/soko/sprites/sk_crate_ongoal.png | Bin 0 -> 218 bytes assets/soko/sprites/sk_dog.png | Bin 0 -> 1515 bytes assets/soko/sprites/sk_e_crate.png | Bin 0 -> 183 bytes assets/soko/sprites/sk_goal.png | Bin 0 -> 421 bytes assets/soko/sprites/sk_grass.png | Bin 0 -> 1776 bytes assets/soko/sprites/sk_portal_complete.png | Bin 0 -> 141 bytes assets/soko/sprites/sk_portal_incomplete.png | Bin 0 -> 143 bytes assets/soko/sprites/sk_ring.png | Bin 0 -> 1529 bytes assets/soko/sprites/sk_sticky_crate.png | Bin 0 -> 268 bytes assets/soko/sprites/sk_sticky_trail_crate.png | Bin 0 -> 248 bytes docs/soko/readme.md | 43 + docs/soko/soko_levels.md | 96 ++ emulator/src/extensions/modes/ext_modes.c | 4 +- main/CMakeLists.txt | 13 +- main/modes/games/soko/soko.c | 364 +++++ main/modes/games/soko/soko.h | 279 ++++ main/modes/games/soko/soko_consts.h | 13 + main/modes/games/soko/soko_game.c | 313 +++++ main/modes/games/soko/soko_game.h | 12 + main/modes/games/soko/soko_gamerules.c | 1240 +++++++++++++++++ main/modes/games/soko/soko_gamerules.h | 49 + main/modes/games/soko/soko_input.c | 179 +++ main/modes/games/soko/soko_input.h | 40 + main/modes/games/soko/soko_save.c | 680 +++++++++ main/modes/games/soko/soko_save.h | 5 + main/modes/games/soko/soko_undo.c | 124 ++ main/modes/games/soko/soko_undo.h | 17 + main/modes/system/mainMenu/mainMenu.c | 6 + makefile | 6 + tools/soko/plugin/sokoban_tiled_importer.js | 21 + .../sokobon_binary_conversion_script.lua | 91 ++ tools/soko/soko_tmx_preprocessor.py | 68 + tools/soko/templateTiledProject/README.md | 62 + .../entitySprites/button16.png | Bin 0 -> 192 bytes .../entitySprites/crate16.png | Bin 0 -> 181 bytes .../entitySprites/ghostblock16.png | Bin 0 -> 176 bytes .../entitySprites/laser90Right16.png | Bin 0 -> 252 bytes .../entitySprites/laserEmitUp16.png | Bin 0 -> 184 bytes .../entitySprites/laserReceiveOmni16.png | Bin 0 -> 242 bytes .../entitySprites/laserReceiveUp16.png | Bin 0 -> 317 bytes .../entitySprites/player16.png | Bin 0 -> 255 bytes .../entitySprites/warpexternal16.png | Bin 0 -> 220 bytes .../entitySprites/warpinternal16.png | Bin 0 -> 241 bytes .../entitySprites/warpinternalexit16.png | Bin 0 -> 257 bytes .../extensions/export-to-soko.js | 391 ++++++ tools/soko/templateTiledProject/objLayers.tsx | 79 ++ .../templateTiledProject/sk_binOverworld.bin | Bin 0 -> 107 bytes .../templateTiledProject/sk_binOverworld.tmx | 55 + .../templateTiledProject/sk_laserTest.bin | Bin 0 -> 123 bytes .../templateTiledProject/sk_laserTest.tmx | 63 + .../templateTiledProject/soko_entities.tsx | 79 ++ .../soko/templateTiledProject/templateMap.bin | Bin 0 -> 99 bytes .../soko/templateTiledProject/templateMap.tmx | 71 + .../templateProject.tiled-project | 14 + .../templateProject.tiled-session | 42 + .../tileSprites/tilesheet.png | Bin 0 -> 792 bytes tools/soko/templateTiledProject/tilesheet.tsx | 8 + tools/soko/templateTiledProject/warehouse.bin | Bin 0 -> 106 bytes tools/soko/templateTiledProject/warehouse.tmx | 37 + tools/soko/tmx_to_binary.py | 222 +++ 103 files changed, 5928 insertions(+), 4 deletions(-) create mode 100644 assets/soko/levels/SK_LEVEL_LIST.txt create mode 100644 assets/soko/levels/classic/sck_c_threes.tmx create mode 100644 assets/soko/levels/classic/sk_c_alignthat.tmx create mode 100644 assets/soko/levels/classic/sk_c_cosms.tmx create mode 100644 assets/soko/levels/classic/sk_c_files.tmx create mode 100644 assets/soko/levels/classic/sk_c_fours.tmx create mode 100644 assets/soko/levels/classic/sk_c_plus.tmx create mode 100644 assets/soko/levels/classic/sk_c_test2.tmx create mode 100644 assets/soko/levels/euler/sk_e_a-frame.tmx create mode 100644 assets/soko/levels/euler/sk_e_alkatraz.tmx create mode 100644 assets/soko/levels/euler/sk_e_apollo.tmx create mode 100644 assets/soko/levels/euler/sk_e_camera.tmx create mode 100644 assets/soko/levels/euler/sk_e_casette.tmx create mode 100644 assets/soko/levels/euler/sk_e_copymachine.tmx create mode 100644 assets/soko/levels/euler/sk_e_curlingiron.tmx create mode 100644 assets/soko/levels/euler/sk_e_doubleblock.tmx create mode 100644 assets/soko/levels/euler/sk_e_feint.tmx create mode 100644 assets/soko/levels/euler/sk_e_groundwork.tmx create mode 100644 assets/soko/levels/euler/sk_e_harmonica.tmx create mode 100644 assets/soko/levels/euler/sk_e_mouse.tmx create mode 100644 assets/soko/levels/euler/sk_e_spine.tmx create mode 100644 assets/soko/levels/euler/sk_e_spiral.tmx create mode 100644 assets/soko/levels/euler/sk_e_start.tmx create mode 100644 assets/soko/levels/euler/sk_e_steeringwheel.tmx create mode 100644 assets/soko/levels/euler/sk_e_threestep.tmx create mode 100644 assets/soko/levels/euler/sk_e_threestep2.tmx create mode 100644 assets/soko/levels/euler/sk_e_throughput.tmx create mode 100644 assets/soko/levels/euler/sk_e_tunnels.tmx create mode 100644 assets/soko/levels/euler/sk_e_waterwheel.tmx create mode 100644 assets/soko/levels/sk_e_overworld.tmx create mode 100644 assets/soko/levels/sk_overworld.tmx create mode 100644 assets/soko/sprites/pango/sk_pango_back1.png create mode 100644 assets/soko/sprites/pango/sk_pango_back2.png create mode 100644 assets/soko/sprites/pango/sk_pango_fwd1.png create mode 100644 assets/soko/sprites/pango/sk_pango_fwd2.png create mode 100644 assets/soko/sprites/pango/sk_pango_side1.png create mode 100644 assets/soko/sprites/pango/sk_pango_side2.png create mode 100644 assets/soko/sprites/pixel/sk_pixel_back.png create mode 100644 assets/soko/sprites/pixel/sk_pixel_front.png create mode 100644 assets/soko/sprites/pixel/sk_pixel_left.png create mode 100644 assets/soko/sprites/pixel/sk_pixel_right.png create mode 100644 assets/soko/sprites/sk_crate.png create mode 100644 assets/soko/sprites/sk_crate_2.png create mode 100644 assets/soko/sprites/sk_crate_ongoal.png create mode 100644 assets/soko/sprites/sk_dog.png create mode 100644 assets/soko/sprites/sk_e_crate.png create mode 100644 assets/soko/sprites/sk_goal.png create mode 100644 assets/soko/sprites/sk_grass.png create mode 100644 assets/soko/sprites/sk_portal_complete.png create mode 100644 assets/soko/sprites/sk_portal_incomplete.png create mode 100644 assets/soko/sprites/sk_ring.png create mode 100644 assets/soko/sprites/sk_sticky_crate.png create mode 100644 assets/soko/sprites/sk_sticky_trail_crate.png create mode 100644 docs/soko/readme.md create mode 100644 docs/soko/soko_levels.md create mode 100644 main/modes/games/soko/soko.c create mode 100644 main/modes/games/soko/soko.h create mode 100644 main/modes/games/soko/soko_consts.h create mode 100644 main/modes/games/soko/soko_game.c create mode 100644 main/modes/games/soko/soko_game.h create mode 100644 main/modes/games/soko/soko_gamerules.c create mode 100644 main/modes/games/soko/soko_gamerules.h create mode 100644 main/modes/games/soko/soko_input.c create mode 100644 main/modes/games/soko/soko_input.h create mode 100644 main/modes/games/soko/soko_save.c create mode 100644 main/modes/games/soko/soko_save.h create mode 100644 main/modes/games/soko/soko_undo.c create mode 100644 main/modes/games/soko/soko_undo.h create mode 100644 tools/soko/plugin/sokoban_tiled_importer.js create mode 100644 tools/soko/plugin/sokobon_binary_conversion_script.lua create mode 100644 tools/soko/soko_tmx_preprocessor.py create mode 100644 tools/soko/templateTiledProject/README.md create mode 100644 tools/soko/templateTiledProject/entitySprites/button16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/crate16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/ghostblock16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/laser90Right16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/laserEmitUp16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/laserReceiveOmni16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/laserReceiveUp16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/player16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/warpexternal16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/warpinternal16.png create mode 100644 tools/soko/templateTiledProject/entitySprites/warpinternalexit16.png create mode 100644 tools/soko/templateTiledProject/extensions/export-to-soko.js create mode 100644 tools/soko/templateTiledProject/objLayers.tsx create mode 100644 tools/soko/templateTiledProject/sk_binOverworld.bin create mode 100644 tools/soko/templateTiledProject/sk_binOverworld.tmx create mode 100644 tools/soko/templateTiledProject/sk_laserTest.bin create mode 100644 tools/soko/templateTiledProject/sk_laserTest.tmx create mode 100644 tools/soko/templateTiledProject/soko_entities.tsx create mode 100644 tools/soko/templateTiledProject/templateMap.bin create mode 100644 tools/soko/templateTiledProject/templateMap.tmx create mode 100644 tools/soko/templateTiledProject/templateProject.tiled-project create mode 100644 tools/soko/templateTiledProject/templateProject.tiled-session create mode 100644 tools/soko/templateTiledProject/tileSprites/tilesheet.png create mode 100644 tools/soko/templateTiledProject/tilesheet.tsx create mode 100644 tools/soko/templateTiledProject/warehouse.bin create mode 100644 tools/soko/templateTiledProject/warehouse.tmx create mode 100644 tools/soko/tmx_to_binary.py diff --git a/assets/soko/levels/SK_LEVEL_LIST.txt b/assets/soko/levels/SK_LEVEL_LIST.txt new file mode 100644 index 000000000..a82eb3fe3 --- /dev/null +++ b/assets/soko/levels/SK_LEVEL_LIST.txt @@ -0,0 +1,22 @@ +0:sk_e_overworld.bin: +1:sk_e_start.bin: +2:sk_e_tunnels.bin: +3:sk_e_groundwork.bin: +4:sk_e_tunnels.bin: +5:sk_e_camera.bin: +6:sk_e_doubleblock.bin: +7:sk_e_mouse.bin: +30:sk_e_threestep.bin: +31:sk_e_harmonica.bin: +32:sk_e_spine.bin: +33:sk_e_a-frame.bin: +34:sk_e_curlingiron.bin: +35:sk_e_copymachine.bin: +36:sk_e_spiral.bin: +37:sk_e_steeringwheel.bin: +38:sk_e_casette.bin: +101:sk_e_apollo.bin: +102:sk_e_waterwheel.bin: +103:sk_e_feint.bin: +104:sk_e_throughput.bin: +105:sk_e_alkatraz.bin: diff --git a/assets/soko/levels/classic/sck_c_threes.tmx b/assets/soko/levels/classic/sck_c_threes.tmx new file mode 100644 index 000000000..62cce165b --- /dev/null +++ b/assets/soko/levels/classic/sck_c_threes.tmx @@ -0,0 +1,26 @@ + + + + + + +0,0,1,1,1,1,1,1, +0,1,1,2,2,2,2,1, +0,1,2,2,2,3,2,1, +1,1,2,2,3,3,1,1, +1,2,2,2,2,2,1,0, +1,2,2,2,1,1,1,0, +1,1,1,1,1,0,0,0 + + + + + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_alignthat.tmx b/assets/soko/levels/classic/sk_c_alignthat.tmx new file mode 100644 index 000000000..fc50e7c08 --- /dev/null +++ b/assets/soko/levels/classic/sk_c_alignthat.tmx @@ -0,0 +1,27 @@ + + + + + + +0,0,1,1,1,1,0,0,0, +0,1,1,2,2,1,0,0,0, +0,1,2,2,2,1,1,1,1, +1,1,2,2,3,3,3,3,1, +1,2,2,2,2,1,2,2,1, +1,2,2,2,2,1,2,2,1, +1,1,1,1,1,1,1,1,1 + + + + + + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_cosms.tmx b/assets/soko/levels/classic/sk_c_cosms.tmx new file mode 100644 index 000000000..f5acc76db --- /dev/null +++ b/assets/soko/levels/classic/sk_c_cosms.tmx @@ -0,0 +1,30 @@ + + + + + + +0,1,1,1,1,1,1,1,0, +0,1,2,2,1,2,2,1,0, +0,1,2,3,2,3,2,1,0, +0,1,2,3,1,3,2,1,0, +1,1,2,3,1,3,2,1,1, +1,2,2,2,2,2,2,2,1, +1,2,2,2,1,2,2,2,1, +1,1,1,1,1,1,1,1,1 + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_files.tmx b/assets/soko/levels/classic/sk_c_files.tmx new file mode 100644 index 000000000..adc216733 --- /dev/null +++ b/assets/soko/levels/classic/sk_c_files.tmx @@ -0,0 +1,23 @@ + + + + + + +0,1,1,1,1,1,1, +0,1,2,2,1,2,1, +0,1,2,3,2,2,1, +0,1,2,3,1,2,1, +1,1,2,3,1,2,1, +1,2,2,2,2,2,1, +1,2,2,2,2,1,1, +1,1,1,1,1,1,0 + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_fours.tmx b/assets/soko/levels/classic/sk_c_fours.tmx new file mode 100644 index 000000000..21b6f2ce1 --- /dev/null +++ b/assets/soko/levels/classic/sk_c_fours.tmx @@ -0,0 +1,27 @@ + + + + + + +0,0,0,0,0,0,0,1,1,1,1,0,0,0, +1,1,1,1,1,1,1,1,2,2,1,0,0,0, +1,2,2,2,2,2,2,2,2,2,2,1,1,1, +1,2,2,2,2,2,1,1,2,2,2,3,3,1, +1,2,2,2,2,2,2,1,1,2,2,3,3,1, +1,2,2,2,2,2,2,2,2,2,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,0,0,0 + + + + + + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_plus.tmx b/assets/soko/levels/classic/sk_c_plus.tmx new file mode 100644 index 000000000..e4dd0a82b --- /dev/null +++ b/assets/soko/levels/classic/sk_c_plus.tmx @@ -0,0 +1,23 @@ + + + + + + +2,1,1,1,1,1,1,1, +1,2,2,2,2,2,2,1, +1,2,2,3,2,3,2,1, +1,1,2,2,2,2,2,1, +1,1,2,3,2,3,2,1, +1,1,2,2,2,2,2,1, +1,1,1,1,1,1,1,1 + + + + + + + + + + diff --git a/assets/soko/levels/classic/sk_c_test2.tmx b/assets/soko/levels/classic/sk_c_test2.tmx new file mode 100644 index 000000000..50b640c3f --- /dev/null +++ b/assets/soko/levels/classic/sk_c_test2.tmx @@ -0,0 +1,29 @@ + + + + + + +0,0,0,0,0,0,0,0,0,0, +0,12,12,12,12,12,12,12,12,0, +0,12,14,12,12,12,12,12,12,0, +0,12,13,14,12,13,13,13,12,0, +0,12,13,13,12,13,13,13,12,0, +0,12,13,13,12,13,13,13,12,0, +0,12,13,13,14,13,13,13,12,0, +0,12,13,13,13,13,13,13,12,0, +0,12,12,12,12,12,12,12,12,0, +0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_a-frame.tmx b/assets/soko/levels/euler/sk_e_a-frame.tmx new file mode 100644 index 000000000..a26061b99 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_a-frame.tmx @@ -0,0 +1,25 @@ + + + + + + +2,2,2,2,2, +2,2,0,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,0,2,2, +2,2,2,2,2, +2,2,0,2,2 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_alkatraz.tmx b/assets/soko/levels/euler/sk_e_alkatraz.tmx new file mode 100644 index 000000000..dbfb05da8 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_alkatraz.tmx @@ -0,0 +1,38 @@ + + + + + + +0,2,2,2,2,2,2,2,2,2, +0,2,1,2,2,2,2,2,1,2, +0,2,2,2,2,2,1,2,1,2, +0,2,1,1,1,1,1,2,1,2, +0,2,1,1,2,2,2,2,1,2, +0,2,1,2,2,2,2,2,2,2, +2,2,1,2,2,2,2,2,0,0, +2,2,2,2,2,2,2,2,0,0, +2,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_apollo.tmx b/assets/soko/levels/euler/sk_e_apollo.tmx new file mode 100644 index 000000000..fdbcdb133 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_apollo.tmx @@ -0,0 +1,34 @@ + + + + + + +2,2,2,2,2,0, +2,2,2,0,2,0, +2,2,2,2,2,0, +2,2,0,2,2,0, +2,2,0,2,2,0, +2,2,2,2,2,0, +2,2,2,2,2,0, +0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_camera.tmx b/assets/soko/levels/euler/sk_e_camera.tmx new file mode 100644 index 000000000..53b301eb0 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_camera.tmx @@ -0,0 +1,25 @@ + + + + + + +0,0,0,0,2,2, +2,2,2,2,2,2, +2,2,2,2,2,2, +2,2,2,2,2,2, +2,2,2,2,2,2 + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_casette.tmx b/assets/soko/levels/euler/sk_e_casette.tmx new file mode 100644 index 000000000..6bf91dd41 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_casette.tmx @@ -0,0 +1,34 @@ + + + + + + +2,2,2,2,2,2, +2,2,2,2,0,2, +2,2,2,2,2,2, +2,2,2,2,2,0, +2,2,2,2,2,0, +2,2,2,2,2,0, +2,2,0,2,2,0, +2,2,2,2,2,0 + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_copymachine.tmx b/assets/soko/levels/euler/sk_e_copymachine.tmx new file mode 100644 index 000000000..3299ae6d7 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_copymachine.tmx @@ -0,0 +1,50 @@ + + + + + + +0,0,0,0,0,0,2,2,0,0, +0,0,0,2,2,2,2,2,2,2, +0,0,0,2,2,2,2,1,2,2, +0,0,2,2,2,2,2,1,2,2, +0,0,2,2,2,2,2,2,2,2, +0,0,2,2,2,1,1,1,1,2, +0,0,0,0,2,2,2,2,2,2, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_curlingiron.tmx b/assets/soko/levels/euler/sk_e_curlingiron.tmx new file mode 100644 index 000000000..17285aeba --- /dev/null +++ b/assets/soko/levels/euler/sk_e_curlingiron.tmx @@ -0,0 +1,26 @@ + + + + + + +0,2,2,2,0,0,2,2,0,0,2,2,0, +2,2,2,2,2,2,2,2,2,2,2,2,2, +2,2,2,2,2,2,2,2,2,2,2,2,2, +0,2,2,2,0,0,2,2,0,0,2,2,0 + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_doubleblock.tmx b/assets/soko/levels/euler/sk_e_doubleblock.tmx new file mode 100644 index 000000000..593c29e14 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_doubleblock.tmx @@ -0,0 +1,25 @@ + + + + + + +2,2,2,2,2, +2,2,2,2,2, +0,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +0,2,2,2,2, +0,2,0,0,0 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_feint.tmx b/assets/soko/levels/euler/sk_e_feint.tmx new file mode 100644 index 000000000..14d4295f6 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_feint.tmx @@ -0,0 +1,23 @@ + + + + + + +2,2,2,2,2, +2,2,2,2,2, +0,0,2,2,2, +0,0,2,2,2, +0,0,0,2,2 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_groundwork.tmx b/assets/soko/levels/euler/sk_e_groundwork.tmx new file mode 100644 index 000000000..84892ef9e --- /dev/null +++ b/assets/soko/levels/euler/sk_e_groundwork.tmx @@ -0,0 +1,23 @@ + + + + + + +0,2,2,2,2, +0,2,1,1,2, +0,2,1,2,2, +0,2,2,2,2, +0,2,2,2,2 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_harmonica.tmx b/assets/soko/levels/euler/sk_e_harmonica.tmx new file mode 100644 index 000000000..90d11cb8e --- /dev/null +++ b/assets/soko/levels/euler/sk_e_harmonica.tmx @@ -0,0 +1,25 @@ + + + + + + +0,2,2,2,2,2,2,2,2,2,2,2,2,2,2, +2,2,2,2,2,1,2,2,2,1,2,2,2,2,2, +2,2,2,1,2,1,2,1,2,1,2,1,2,2,2, +2,2,2,1,2,2,2,1,2,2,2,1,2,2,2, +2,2,2,2,2,2,2,2,2,2,2,2,2,2,0 + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_mouse.tmx b/assets/soko/levels/euler/sk_e_mouse.tmx new file mode 100644 index 000000000..23e5e7402 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_mouse.tmx @@ -0,0 +1,22 @@ + + + + + + +0,0,2,2,2,2,2, +0,0,2,0,0,0,2, +0,0,2,0,2,2,2, +2,2,2,2,2,0,0, +0,2,2,2,0,0,0, +0,2,2,2,0,0,0 + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_spine.tmx b/assets/soko/levels/euler/sk_e_spine.tmx new file mode 100644 index 000000000..3289a33b8 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_spine.tmx @@ -0,0 +1,28 @@ + + + + + + +0,2,2,0,0,0, +2,2,2,2,0,0, +2,2,2,2,0,0, +0,2,2,2,0,0, +0,2,2,2,2,2, +0,2,2,2,2,2, +0,2,2,2,0,0, +0,0,2,2,0,0 + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_spiral.tmx b/assets/soko/levels/euler/sk_e_spiral.tmx new file mode 100644 index 000000000..09ef2d6da --- /dev/null +++ b/assets/soko/levels/euler/sk_e_spiral.tmx @@ -0,0 +1,23 @@ + + + + + + +2,2,2,2,2,2,2, +2,2,0,0,2,2,2, +2,2,2,2,2,2,2, +2,2,2,2,2,2,2, +2,2,2,2,2,2,2, +2,2,2,0,0,2,2, +2,2,2,2,2,2,2 + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_start.tmx b/assets/soko/levels/euler/sk_e_start.tmx new file mode 100644 index 000000000..fb75544ec --- /dev/null +++ b/assets/soko/levels/euler/sk_e_start.tmx @@ -0,0 +1,23 @@ + + + + + + +2,0,0,2,2,0, +2,2,2,2,2,2, +1,1,1,1,2,2, +0,2,2,2,2,2, +0,2,2,2,2,2 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_steeringwheel.tmx b/assets/soko/levels/euler/sk_e_steeringwheel.tmx new file mode 100644 index 000000000..306a556ef --- /dev/null +++ b/assets/soko/levels/euler/sk_e_steeringwheel.tmx @@ -0,0 +1,21 @@ + + + + + + +0,2,2,2,0,2,2,2,0, +2,2,0,2,2,2,0,2,2, +2,0,0,2,2,2,2,0,2, +2,2,0,2,2,2,0,2,2, +0,2,2,2,2,2,2,2,0 + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_threestep.tmx b/assets/soko/levels/euler/sk_e_threestep.tmx new file mode 100644 index 000000000..1354cb252 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_threestep.tmx @@ -0,0 +1,42 @@ + + + + + + +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +0,0,0,0,2, +0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_threestep2.tmx b/assets/soko/levels/euler/sk_e_threestep2.tmx new file mode 100644 index 000000000..5e42e2e54 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_threestep2.tmx @@ -0,0 +1,38 @@ + + + + + + +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +2,2,2,2,2, +0,0,0,0,2 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_throughput.tmx b/assets/soko/levels/euler/sk_e_throughput.tmx new file mode 100644 index 000000000..525cc3836 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_throughput.tmx @@ -0,0 +1,34 @@ + + + + + + +2,2,2,2,2,2,2,2,2,2, +2,2,2,2,2,0,0,0,0,0, +2,2,2,2,2,2,2,2,2,0, +2,2,2,2,2,0,0,0,0,0, +2,2,2,2,2,2,2,2,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_tunnels.tmx b/assets/soko/levels/euler/sk_e_tunnels.tmx new file mode 100644 index 000000000..100325e86 --- /dev/null +++ b/assets/soko/levels/euler/sk_e_tunnels.tmx @@ -0,0 +1,23 @@ + + + + + + +2,2,2,2,2, +2,2,1,1,1, +2,2,2,2,2, +2,1,2,2,2, +2,2,2,2,2 + + + + + + + + + + + + diff --git a/assets/soko/levels/euler/sk_e_waterwheel.tmx b/assets/soko/levels/euler/sk_e_waterwheel.tmx new file mode 100644 index 000000000..bc5e8a79e --- /dev/null +++ b/assets/soko/levels/euler/sk_e_waterwheel.tmx @@ -0,0 +1,69 @@ + + + + + + +0,0,0,0,0,2,0,0,0, +0,0,0,0,0,2,0,0,0, +0,0,2,2,2,2,2,0,0, +2,2,2,2,2,2,2,0,0, +0,0,2,2,2,2,2,2,2, +0,0,2,2,2,2,2,0,0, +0,0,2,2,2,2,2,0,0, +0,0,0,2,0,0,0,0,0, +0,0,0,2,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/sk_e_overworld.tmx b/assets/soko/levels/sk_e_overworld.tmx new file mode 100644 index 000000000..056463744 --- /dev/null +++ b/assets/soko/levels/sk_e_overworld.tmx @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + +0,12,12,12,12,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,76,13,13,13,76,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,13,13,13,77,77,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,13,13,13,77,77,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,13,13,13,77,77,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,13,76,76,13,77,76,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +12,13,77,76,77,77,77,76,76,76,76,76,76,76,76,76,76,76,76,0,0,0,0, +12,13,13,76,77,77,13,13,13,13,13,13,13,13,13,13,13,77,76,76,0,0,0, +12,13,13,76,77,77,77,76,76,13,13,13,13,13,13,13,13,77,77,76,76,0,0, +12,13,13,76,76,76,77,77,76,77,77,13,13,13,13,13,13,77,77,77,76,76,0, +12,13,13,13,13,76,77,77,77,77,77,77,13,13,13,13,13,77,77,77,77,76,76, +12,13,13,13,13,76,77,77,77,77,77,76,76,76,76,76,76,76,76,76,76,76,76, +12,13,13,13,13,76,77,77,77,77,77,77,77,76,77,77,77,77,77,77,77,77,76, +12,12,13,13,13,76,13,13,77,77,77,77,77,77,77,77,77,77,77,77,77,77,76, +0,76,12,76,76,76,13,13,77,77,77,77,77,77,76,76,76,76,76,77,77,76,76, +0,0,0,0,76,13,13,13,13,13,13,77,77,77,76,77,77,77,77,77,76,76,0, +0,0,0,0,12,13,13,13,13,13,13,13,13,77,77,77,77,77,76,76,76,0,0, +0,0,0,0,12,12,12,12,12,12,12,12,12,12,76,76,76,76,76,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/levels/sk_overworld.tmx b/assets/soko/levels/sk_overworld.tmx new file mode 100644 index 000000000..4764283fc --- /dev/null +++ b/assets/soko/levels/sk_overworld.tmx @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + +76,12,12,12,12,12,12,12,12,12,12,12,12,0,0,0,0,0, +12,13,13,13,13,13,13,13,13,13,13,13,12,0,0,0,0,0, +12,13,13,13,13,13,13,13,13,13,13,13,13,12,0,0,0,0, +12,13,13,13,13,13,13,13,13,13,13,13,13,13,12,0,0,0, +12,13,13,13,13,13,13,13,13,12,13,13,13,13,13,12,0,0, +12,13,13,13,13,13,13,13,13,13,12,13,13,13,13,13,12,0, +12,13,13,13,13,13,13,13,13,13,13,12,12,12,12,12,12,12, +12,13,13,13,13,12,13,13,13,13,13,13,13,13,13,13,13,12, +12,13,13,13,13,12,12,13,13,13,13,13,13,13,13,13,13,12, +12,13,13,13,13,12,12,12,13,13,13,13,13,13,13,13,13,12, +12,13,13,13,13,12,12,12,12,13,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,12,12,12,12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,12,12,12,12,12,12,12,12,12,12,12, +12,12,13,13,13,13,13,13,12,12,12,12,76,76,0,0,0,0, +0,0,12,13,13,13,13,13,13,13,13,77,77,76,0,0,0,0, +0,0,0,12,13,13,13,13,13,13,13,13,12,76,0,0,0,0, +0,0,0,0,12,13,13,13,13,13,13,13,13,12,0,0,0,0, +0,0,0,0,12,12,12,12,12,12,12,12,12,12,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/soko/sprites/pango/sk_pango_back1.png b/assets/soko/sprites/pango/sk_pango_back1.png new file mode 100644 index 0000000000000000000000000000000000000000..eeb0ec16081940cde36adccf691f1e115f5eb928 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`b)GJcAr-f_P7V}0Ai(2t_vG9= zK$ICBwQI%(yIC{O2z;B!@;vA819!F~_xu$m|6pNaWQd-#fqhSP9n+C?!`8?-$$Qdo z?{fXO?MK^oX@i#J*WVtlX5@_WI<@$f-}z**>4qEHdrDV)=6KOy$STV4-ua_e%7hSR hHi>sj{x9ItJ3OCf&Z<~TeV|JiJYD@<);T3K0RS`kNf-bC literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pango/sk_pango_back2.png b/assets/soko/sprites/pango/sk_pango_back2.png new file mode 100644 index 0000000000000000000000000000000000000000..f4e8ce13ebff142c81e94cfd5451b9e76cc9577d GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`HJ&bxAr-f_P7dT_5ae;GRxd9C zqUCyJvwb*C<+W$JT(OaLO0h2yG7ae#XO29>7%RIc7@&3H|3{Q2h?V0sk>K*6& zcN6UQd06wF-F8i?!@5A%c}bu(gPWlL`(hL3kZlYZ6VJV8C~kSp?Go8y=da0sQ9zh^ c!M?A|d1gFw)^}$u0lI_1)78&qol`;+0PH$D;{X5v literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pango/sk_pango_fwd1.png b/assets/soko/sprites/pango/sk_pango_fwd1.png new file mode 100644 index 0000000000000000000000000000000000000000..674d392d0079d37cf1e4912dc6eb1b3b20e5657e GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`(>z@qLn>~~oy^GBpupj}cXIhF zzdN1Q6K4r<33c~|zWd+aF*T_+}rLn?0dPG;n5P~dRgJGuOo z-SJn>uy4tzYvD8#6>o~_}=B3_Q57q*+1CVvoIW%KHP!oRrp4bwRsC3u82OL{mC zdA?CldvN8~*E6v!ZoBWh@jci+L00&-)3!^(U%n{$FVHhL2(_}_!4{DZbRvVNtDnm{ Hr-UW|$+1t^ literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pango/sk_pango_side1.png b/assets/soko/sprites/pango/sk_pango_side1.png new file mode 100644 index 0000000000000000000000000000000000000000..35bbf3d7e6b1028fba61e2fe7da0fbe5198224f7 GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`vpiiKLn>~q4Zbbdpum%?Y#(z- z|9HakcSroDP8V3%T6aVF_`{hdTaH><-e54D;pz6lenS82xLqggot7CE3tzZXlEnLG zoAUSUkfht0@h)X=BDU*pO_bCnZ???KUE2IC4;A{ KpUXO@geCxZB2n}J literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pango/sk_pango_side2.png b/assets/soko/sprites/pango/sk_pango_side2.png new file mode 100644 index 0000000000000000000000000000000000000000..3869df3a880db519c24eb8474b461d156e1925cf GIT binary patch literal 284 zcmV+%0ptFOP)Px#)=5M`R5*>LQ^^g(Fbt!D?$d`dL|+{vp2?ZXnn``wQsg5AiV83i*d#^D3Gio8 zc-J5C%MC3Dzi|8H+q}A;ytwh0000((% literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pixel/sk_pixel_back.png b/assets/soko/sprites/pixel/sk_pixel_back.png new file mode 100644 index 0000000000000000000000000000000000000000..bce5ac7c8ef100517ecc23ae2161da2678e8e03c GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`>pWc?Ln?0d2456Bs=!ga{9pO( z<9>HFt$(&Gau428G+lTii_SI?Bh4V|2OS%YHhWz9I$^ckf!=4_oqJCxYzla)(WG&y z`RkvXvqF>iKcB_He&lD3T~!QE?qlxfn_PJ8&-uVKri|pd`&cUu zHOqW^HG4wJ)u(nFc5gd!rsT+C4-vyfvwuBi75Z{!*A1Z+Eef0VFxlsSJ$|PA?ZsXH zLi%crw(k@8$((Ak=d!!mm&rhv^QA_3ruq6ZXaU(AKnwzxfzopr07$uN As{jB1 literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/pixel/sk_pixel_front.png b/assets/soko/sprites/pixel/sk_pixel_front.png new file mode 100644 index 0000000000000000000000000000000000000000..77517ad271c00cb67b997f8e4bcdbd3bcc283955 GIT binary patch literal 297 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`*F0SuLn?0d1|1Y+GUQky7JcG= zUdo)0$Jz_n$p-E>S|us`>ll z$QQhC66YTLTl4h#(=eA$Td&M{(-w;$1 zU)pN!B;r^T#Qt*ftdrWIo7x}5=3G+U`wRy@CmrrnVu(fqq z9k^Dvq&W1(ja3)7{JgwIHgS@+?f<7c4HPx$EJ;K`R5*==lPgjKK@f(&Nu`Q2ASq1ztij|ONE97M0VcOVVWv3?h!{kATsQ;{ z5p*Lm8FsUi_p0}b20Q)H7-05cB06v$}v=8T1fEfcguXgVhNS?5O_l-;jaM3Y1gmD=rhX=9b z+=u&`tsmH~H#M)R0@8b|Xb{AI9y2BIXd!vs*4IdlW2*~X^w?mMi*AKZVZo;+Q5 QrT_o{07*qoM6N<$f*Vku(f|Me literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_crate.png b/assets/soko/sprites/sk_crate.png new file mode 100644 index 0000000000000000000000000000000000000000..65a07ff576aef87225defe11c3e3c6a4dcf19e7d GIT binary patch literal 1476 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|_);T0(|mmy zw18|5AO?X;!IOa`XMsm#F$061G6*wPEVVCVU|_Dx42dX-@b$4u&d=3LOvz75)vL%Y z0PC`;umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3 znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8 zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Uod0?Yb6da36%JYk|ZS*0kQB8q}q8e_akHsA} zAm3X>2Bj9~=ahoN-_F>;zy=&9D58j%far+8ssmXRT}MDhen~zsWff&6d*+p-78Mi$ zQyJJsBdC42RiK-Nusk#`v&0T&0Gcq8E~Hqpf(Bd&I7Tpo6Q@$Bagd-zO0kga0FDM= zez4=R(TC?PJFdSueG9f=ykk|MjF7~HWiQ}unjoB~mIyvU>cd^g@k!HsE)`@-7NuOtD-|U&GF>CVa z!=HFo%`zdj33Z#B^&)}DFh%jrhwf$JuRfyl`l;DryJMwsNec)oZ2oC@oOeKJ~T~^V))(t=AB{u ze);+*qN@+EMz#vI+|tSD%$sse%H>zzgb63!EcVP1H#|1U?XQyEVhyneSI#UgegDgt zb3*F5@6r=zS|^+k4qGjfv3Sn(4R;!CeWbFMCfT{G>P)*J`$Igu zc+(B{&lT6QRd21FT6yZoPqx2Xk6k$U&1&lgF&4Yu(V>xV9;;n6b`IOMC`0T-(Tj6| ztUH_L_h%-Vn@n3S=)Iyn{>v9TrS80nm15x;<|`{gmxjfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3 znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8 zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Uod0?Yb6da36%JYk|ZS*0kQB8q}q8e_akHsA} zAm3X>2Bj9~=ahoN-_F>;zy=&9D58j%far+8ssmXRT}MDhen~zsWff&6d*+p-78Mi$ zQyJJsBdC42RiK-Nusk#`v&0T&0Gcq8E~Hqpf(Bd&I7Tpo6Q@$Bagd-zO0kga0FDM= zez4=R(TC?PJFdSueGj@7e3|u zbNA4B%K-m{68eUQijSO_ts9vWe0+{%h#W5Yy((?l+=t2fX}ea%?OhihyH;Z>-?eT< zpSeXl*9LX1*_t!kE&YXRqfGv!BbN`qJ;9%_SMb8cchhc&n{VH;?a}3PZmkCv>9V>B zY6@s7GI)ibwlkHVr_}W0_Ny-^U6x-u{rbYu)vNdIde6^uSj2Dou1hneo<935*IRci zvpkKT^Fy*^!-8r`}jSMkS9BCdi zCeJGEW|_BP^4};kChZm;{ZeCw!_HA#PEQq>n|z??O@>{WEMvIX=_6%XA3y8}+Y5>5QS;RF3Lju?zSMwU@+*sGg4$x_thp_&51w V55Av2tO6<}Jzf1=);T3K0RVIv_tXFY literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_crate_ongoal.png b/assets/soko/sprites/sk_crate_ongoal.png new file mode 100644 index 0000000000000000000000000000000000000000..43273405a806945c57985f2b8463fcf979f74b1e GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|rg*wIhFJ72 zogB#5qQK*_{D}0<^hH2;%XV^|0%PH=7gqKv7rrm>6=2Z#xtp;q{ba!Jj*tlXL%OWq z`*+H$W{Qw~V189nX!Es09PbpF)Mh@sSClk6owK>RyE&m$C3)TFnNc59yb_gm{GJqW zq3T5)_zm<jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3 znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8 zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Uod0?Yb6da36%JYk|ZS*0kQB8q}q8e_akHsA} zAm3X>2Bj9~=ahoN-_F>;zy=&9D58j%far+8ssmXRT}MDhen~zsWff&6d*+p-78Mi$ zQyJJsBdC42RiK-Nusk#`v&0T&0Gcq8E~Hqpf(Bd&I7Tpo6Q@$Bagd-zO0kga0FDM= zez4=R(TC?PJFdSueG!wu;@i6-Mt)Jr#Fd5>uxf1?2<6;Jbv1 zvP9bR>iggKRT^{K7^^&}R$DsJ@5r(}H~S-&tUrX zPob7aN|RGh@0Lb}W{0)7rqT z7kYEooq1g~S3rvOl|}UPgx4!YSk~WF{-7KwmE-JsrDDHekI>I!X8H5pUax+Xz4Nf! zp|6``Sa|Nus1ZK+?c=mVzmDw=SQql4^s`Km*VQfllE*z321cr0EO~w4dW_{<-ixh) z0t-t5-L|^rt^4hhV$2$&_2=A^e-g8oJ>;G%I#nrL<@qYD=bvW2FR=0EH+Y^u{iEj9 mn5il0GP`V+K2`jCy literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_e_crate.png b/assets/soko/sprites/sk_e_crate.png new file mode 100644 index 0000000000000000000000000000000000000000..5b7c207dfdc8602d0394da422a5cb032ab698c6f GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|3O!vMLo9la z20QW{P~gz+uK()0Z`xd+4FVmBCvAebmM(bFwCLkJK1l{Qfyqt98hs)u%9dxe1ZFEt zo^e{m@5Hx0_Z3gL#f#+%()*%~J{%PLr~km_hH+Z-2@#D~))pTe6K+l{R_5OnT@dj_ fd|h37HMjn=cPx$Ur9tkR5*==lTA(nK@f$%8s`YgNQ_>hjfY~zPLJTym3L7#Dv6+hL_L8&gU%35 zG%kpL^IXi(&6UiJC@tZuudZo4JtoMM2Lm53y%kv`l4Ac>+7gY>TQ;W54UMeFMc zgTYhD>7v~dQ-saa4`qyzFcx9}x4knfa}rH>ODZsn`1$gJkQ;HjI1x1@QE3@rx8 z^WndN3lkWj1fi0NDh6J-FpJ9b3hehTiHkyB!dQl+KXMi_h@yxr9|k>J)bUxonu6G_ zBuK)dnaXqeIG#kmcS#IJ`fwXiUM6AoECwx|!rUh@mBawLXU9sjmj%k_BrOWIHfl7Q zyX8ADGdAn1BuUL@&axp%vWA)Ql;zad6Q3Z^_u&AMqyDRdE?9N&$;udYI$ycORaTgxE#hhr!n!(mRvl+< zjf!Sap>Vk75BZ3Lkp&w%LJA5C1YFn!@6NH(+>udnZ+gfrrF+Mp{yffy2bbzGx zHztjj(n}Cz)4NjFZf(ACZ~oQr=|^Q=1)fP1R+H!o2yA3weOUkJa-V&lZc%tb zfL=e7&M6m9LE%kDK3`avC}dQ~;rMF8j^fvK{A+)o%*}l-IwQB>oc^s_PRtf_7f!x< z`&I>ugcL*GD<9Jk_g4$QJu5o!o6}$hQ`>uu&56$rDVA5)^Sr2j_$Qg?gDg|o)X(dc z&%Kpuo!_Yamt9;lYfmtn<7r?#@TEq0ruq6ZXaU(AKnwzxf+qt>&H|6fVg?31We{ep zSZZI!z`$IW84^(v;p=0SoS&h?X&sHg;q@=(~ zU%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8s6w~6GOr}DLN~8i8Da>`9GBGM~RsBTId_ z|A5Z7NlCUU$t=l91qU45Kj08_%qc+?1*r!GK~5$pWUX=%^U`gVDs)p)(-KQ_N|fwE z^T0->C^#0Cl;;;^+vr17qnZK{MK#<;AB#I|K)$z%3`#A|&nX3kzn!sxfekoJP(%?i z0nrhGRR^*tx{iR1{E~cN$|}lC_RK3uEh;DirZTXNMo{~3t3WpkVR>j?W{Dll05oAF zT}ZKH1r4|maExFECr+hM;~+tclwu*-0UQm${9wmrqYuwpc3gjR`XqoQXRW7;V~B-d z?4&@i!vO-W@s|}uLb6yFDsZ_@{PlFdc7b%0b%Ex(Bep-5D{zPf{t9eqNDvWy<-gKW z*uR5MW%8LbbF0(uy`B>{N5{STut}|!P+5%$UwqBp_UEqWeq4RKr1SH{i4yC+&2IAi zIg^3;Rhjuek#SJ7$vad0+kZ9V!ddCjds7*7mS|Te&U|ZoC3%TLVax`@LubFnKA1UE`D=ux z!^FoPm%iORy=U^i&o)mEy|Q)Cvb=f2>Q&B(85(CfCMGqrW?UBcEdH^@NqbPq=jrO_vd$@?2>_bN Bjne=C literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_portal_complete.png b/assets/soko/sprites/sk_portal_complete.png new file mode 100644 index 0000000000000000000000000000000000000000..1dd3ca145f57ef997875b3af3b0a0252428180a5 GIT binary patch literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|+&x_!Lo9le zP3BpCwr6H-=oC830|Yt-$0FMzRWeP|Je*A%8i63g_>zE6*It9H11lJLcwT54^0sRw i@ffJNn=^~M literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_ring.png b/assets/soko/sprites/sk_ring.png new file mode 100644 index 0000000000000000000000000000000000000000..d57803713c17efe18d63ad34e2c4e7e25c6def83 GIT binary patch literal 1529 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|_);T0(|mmy zw18|5AO?X;!IOa`XMsm#F$061G6*wPEVVCVU|_Dx42dX-@b$4u&d=3LOvz75)vL%Y z0PC`;umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3 znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8 zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Uod0?Yb6da36%JYk|ZS*0kQB8q}q8e_akHsA} zAm3X>2Bj9~=ahoN-_F>;zy=&9D58j%far+8ssmXRT}MDhen~zsWff&6d*+p-78Mi$ zQyJJsBdC42RiK-Nusk#`v&0T&0Gcq8E~Hqpf(Bd&I7Tpo6Q@$Bagd-zO0kga0FDM= zez4=R(TC?PJFdSueGF4rpEn8W!vg1wOBrY?4#l0&Y zxXxd=h4rhck`k+avFeIkhYzZIKAloG;9YX^-Lyx(nVn+)kKQ$|E54<3%#`o+yN74a zJpwPa>td%&Ry0e+5*9o1fmb z{29aThm&<`K1^HaYrXHwpU=ED42|a=9eBdZz`g8XaO&fY4QGt^btyIqrTaC;P5F3p zSSJz(O71piTcRYPnJyVh4f=PS}o^8Slr zJC}WsY>*4Qlqzi&^(uVl2k&!7DY&MA4v;Ki%(q3=2iwidIir8spb39@C>;FIJ)-xwnpHSwGDFPMg44$rjF6*2UngDdm BB#{6B literal 0 HcmV?d00001 diff --git a/assets/soko/sprites/sk_sticky_crate.png b/assets/soko/sprites/sk_sticky_crate.png new file mode 100644 index 0000000000000000000000000000000000000000..c1d4307252c18e35ee2a37c6c603555583d5de86 GIT binary patch literal 268 zcmV+n0rUQeP)Px##z{m$R5*>Llj{wGFbsshL~P{Fl+HwE!ZW2moG1jFQmK}Z5I!Gwek1^)G?pj; zD6eGsqvz{_Qff*a+mh60q>GAhw3C84 zw!0=q*uTfvS9umhpgVG*w9Q`ZSk{fauJ766Jg(eX!+#mcxCy7kEX1;ZKj#m}-c+(t SSX#0G0000Px#vPnciR5*>DQ_Bs-APh688>KTbRAy>s;vEuNnxr5-pO9i(CZ7TTgx2vw0{{rl zqV>z_{9*~X6hiBwCJ-%QV-4oZSx+J`31~3R$pAtNO{AyyxEpd(4WAr$@Ae;4< y`Lsymc-WP5DNQ$0qi`0CJlS5T#?JNsbN&JHuRZ8Mz*r&x0000 :{id}:filename.bin: + +The id's do not need to packed. A 'levelIndices' int array is created which maps the provided id's to a clean loopable array. + +*Text is parsed by sokoExtractLevelNamesAndIndeces in soko.c. Importing is done by sokoLoadBinLevel in soko.c.* + +### Number of Levels +Level index, save data, and other level arrays are all pre-allocated to the 'SOKO_LEVEL_COUNT' in soko_consts.h. This constant to be increased to the number of levels, including overworld, as appropriate. + +### Adding to the Overworld + +Overworld is a map with all puzzles in them. 'portal' objects are used to transition into level. + In the overworld map file, the object has a custom property called 'target_id'. That gets set to the index of of the level. Everything else is handled by the engine. + +See [soko_levels.md](soko_levels) for more details on how levels work. + + + \ No newline at end of file diff --git a/docs/soko/soko_levels.md b/docs/soko/soko_levels.md new file mode 100644 index 000000000..4814a6436 --- /dev/null +++ b/docs/soko/soko_levels.md @@ -0,0 +1,96 @@ +# Soko Binary File Format + +The tools/soko folder contains a pre-processor that converts the [tiled](https://www.mapeditor.org/) tmx map files into a custom binary format (.bin). + +### How The Levels Work +The levels in the game are split into two elements. There are tiles. These are background items, such as 'Floor' or 'Wall'. There must be one at every location on the map (although "EMPTY" is an option). + +A collision matrix defined in 'soko_gamerules.c' determines what entities can walk on what tiles. + +Entities are stored in their own array, and represent anything that can move, basically. Entities can be (but mostly aren't) located at the same location as other entities. Player, Crate, WarpExternal, LaserEmitter, etc. are entities. + +In the tiled editor, there are two layers: an 'Object Layer' called entities and a 'Tiles Layer' layer called tiles. These correspond to the internal structure of the level. There should **not** be more layers than this, as the converter is fragile and poorly written. + +Each tile in the tilesheet has an id, and this is used when converting/loading the level to figure out what tile/object is where. + +#### Defining the Game Mode +The player entity should contain a 'gamemode' custom property, set to one of the following options: + +- SOKO_OVERWORLD +- SOKO_CLASSIC +- SOKO_EULER +- SOKO_LASERBOUNCE + +#### Configuring the Overworld +The overworld level is where we will add connections between levels. The structure of the game is flat: the player must return to the overworld after they complete a level. There are not multiple overworlds (zones, world 2-2, etc). + +The overworld uses an entity with the class 'warpexternal', with ID 3. This object contains a custom property 'target_id', which corresponds to the level ID value that should get loaded. + +#### Setting the Levels +First, save the level tmx file in the appropriate folder in assets. + +Then, edit the 'SK_LEVEL_LIST.txt' file. +Add a line for your level with the following syntax: + +*:id:filename.bin:* + +That's a colon, then a chosen whole-integer id value that doesn't conflict with others. They do not have to be sequenced or defined in order. Then another colon, then the filename. THis will be the same as the .tmx file,except with the .bin extension. + +Then another colon at the end of the line. + +The level is now ready to be loaded. Add it to the overworld map as described above. + +--- + +### Preprocessor +"soko_tmx_preprocessor.py" scans a directory (recursively) for these files and puts them flat inside the spiffs_image output folder. + +Because there is no folder structure on output, sokoban levels should follow a consistent naming scheme to avoid name conflicts. + +### Converstion to Binary +tmx files are xml based. Each map should contain a tiles layer (called 'tileset') which gets read as the static tileset, and an objects layer called 'entities'. + +### The Binary Format +The format is a packed sequence of bytes. + +First, 3 bytes of header information: +1. Width +2. Height +3. Gamemode + +Ignoring entities and compression, the next is a tight "grid" of tile data, left to right, top to bottom, like a book. Each byte is the id of the tile at that position, defined in the 'soko_bin_t' enum in soko.h. The values don't necesarily correspond to the enum values for the tiles or entity structs; but instead the soko_bin_t enum. Which is only used for parsing the file. + +#### Entities +As the data is parsed, the position of the last-parsed tile is kept. If the parser encounters a 'SKB_OBJSTART' byte (201), it does not 'advance' the position of the tiles, but instead creates an entity. + +Entities are 'SKB_OBJSTART', then a byte defining some number of data pieces, then a 'SKB_OBJEND' byte. Each entity is at least 3 bytes. The position of the entity is the last parsed tile position, and the type of entity is determined by it's second byte. + +If, after the modes design has been finished and all entity type sizes are known, we can remove the 'SKB_OBJEND' byte. For now, we need it. + +The rest of the bytes depend on the entity. The WARPEXTERNAL entity has one additional byte, defining the level ID to jump to. Warp internal works the same way. + +All Entity data is stored between bytes 200 and 255. + +### Compression +After the file is created, it gets compressed. + +The compression scheme is one of the compressing byte, then the 'SKB_COMPRESS' byte, followed by some number of times to repeat that previous byte. + +"Floor, Floor, Floor" could become "Floor, Compress, 2". +"Wall, Wall, Wall, Wall, Wall, Wall, Wall, Wall" would become "Wall, Compress, 7" + +Because the data is stores in horizontal rows, this only compresses contiguous horizontal sections of tiles, including when "word wrapped". Regardless, it won't hurt. *Getting around 73% ratio on 5/13/24. Mostly in overworld.* + +#### Entity Binary Encoding Schemes +*START = 'SKB_OBJSTART', END = 'SKB_OBJEND', and 'SKB_' prefix ignored.* + +- START, PLAYER, END +- START, CRATE, [StickyFlag] END + - not stick, not trail = 0 + - sticky, not trail = 1 + - not stick, trail = 2 + - sticky, trail ST = 3 +- START, WARPEXTERNAL, [Target ID], END + - TargetID should match SK_LVEL_LIST.txt data. + +*Note: Todo as I re-write the converter in python* \ No newline at end of file diff --git a/emulator/src/extensions/modes/ext_modes.c b/emulator/src/extensions/modes/ext_modes.c index 1efd93fbf..9e0926671 100644 --- a/emulator/src/extensions/modes/ext_modes.c +++ b/emulator/src/extensions/modes/ext_modes.c @@ -32,6 +32,7 @@ #include "mode_synth.h" #include "modeTimer.h" #include "pango.h" +#include "soko.h" #include "touchTest.h" #include "tunernome.h" #include "ultimateTTT.h" @@ -72,8 +73,9 @@ static swadgeMode_t* allSwadgeModes[] = { &keebTestMode, &mainMenuMode, &modeCredits, - &swadgeHeroMode, &pangoMode, + &sokoMode, + &swadgeHeroMode, &synthMode, &t48Mode, &timerMode, diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index afcbbc22a..1932487b3 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -34,9 +34,6 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "modes/games/bigbug/pathfinding_bigbug.c" "modes/games/bigbug/soundManager_bigbug.c" "modes/games/bigbug/tilemap_bigbug.c" - "modes/games/swadgeHero/mode_swadgeHero.c" - "modes/games/swadgeHero/swadgeHero_game.c" - "modes/games/swadgeHero/swadgeHero_menu.c" "modes/games/pango/paEntity.c" "modes/games/pango/paEntityManager.c" "modes/games/pango/paGameData.c" @@ -44,6 +41,15 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "modes/games/pango/paSoundManager.c" "modes/games/pango/paTilemap.c" "modes/games/pango/paWsgManager.c" + "modes/games/soko/soko.c" + "modes/games/soko/soko_game.c" + "modes/games/soko/soko_gamerules.c" + "modes/games/soko/soko_input.c" + "modes/games/soko/soko_save.c" + "modes/games/soko/soko_undo.c" + "modes/games/swadgeHero/mode_swadgeHero.c" + "modes/games/swadgeHero/swadgeHero_game.c" + "modes/games/swadgeHero/swadgeHero_menu.c" "modes/games/ultimateTTT/ultimateTTT.c" "modes/games/ultimateTTT/ultimateTTTgame.c" "modes/games/ultimateTTT/ultimateTTThowTo.c" @@ -115,6 +121,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "./modes/games/2048" "./modes/games/bigbug" "./modes/games/pango" + "./modes/games/soko" "./modes/games/swadgeHero" "./modes/games/ultimateTTT" "./modes/music" diff --git a/main/modes/games/soko/soko.c b/main/modes/games/soko/soko.c new file mode 100644 index 000000000..0e54f57fa --- /dev/null +++ b/main/modes/games/soko/soko.c @@ -0,0 +1,364 @@ +#include + +#include "soko.h" +#include "soko_game.h" +#include "soko_gamerules.h" +#include "soko_save.h" + +static void sokoMainLoop(int64_t elapsedUs); +static void sokoEnterMode(void); +static void sokoExitMode(void); +static void sokoMenuCb(const char* label, bool selected, uint32_t settingVal); +static void sokoBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); +static void sokoExtractLevelNamesAndIndices(soko_abs_t* self); + +// strings +static const char sokoModeName[] = "Sokobanabokabon"; +static const char sokoResumeGameLabel[] = "returnitytoit"; +static const char sokoNewGameLabel[] = "startsyfreshy"; + +// create the mode +swadgeMode_t sokoMode = { + .modeName = sokoModeName, + .wifiMode = NO_WIFI, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .overrideSelectBtn = false, + .fnEnterMode = sokoEnterMode, + .fnExitMode = sokoExitMode, + .fnMainLoop = sokoMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = sokoBackgroundDrawCallback, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +// soko_t* soko=NULL; +soko_abs_t* soko = NULL; + +static void sokoEnterMode(void) +{ + soko = calloc(1, sizeof(soko_abs_t)); + // Load a font + loadFont("ibm_vga8.font", &soko->ibm, false); + + // load sprite assets + // set pointer + soko->currentTheme = &soko->sokoDefaultTheme; + soko->sokoDefaultTheme.wallColor = c111; + soko->sokoDefaultTheme.floorColor = c444; + soko->sokoDefaultTheme.altFloorColor = c444; + soko->background = SKBG_FORREST; + // load or set themes... + // Default Theme + loadWsg("sk_pixel_front.wsg", &soko->sokoDefaultTheme.playerDownWSG, false); + loadWsg("sk_pixel_back.wsg", &soko->sokoDefaultTheme.playerUpWSG, false); + loadWsg("sk_pixel_left.wsg", &soko->sokoDefaultTheme.playerLeftWSG, false); + loadWsg("sk_pixel_right.wsg", &soko->sokoDefaultTheme.playerRightWSG, false); + loadWsg("sk_crate_2.wsg", &soko->sokoDefaultTheme.crateWSG, false); + loadWsg("sk_crate_ongoal.wsg", &soko->sokoDefaultTheme.crateOnGoalWSG, false); + loadWsg("sk_sticky_crate.wsg", &soko->sokoDefaultTheme.stickyCrateWSG, false); + loadWsg("sk_portal_complete.wsg", &soko->sokoDefaultTheme.portal_completeWSG, false); + loadWsg("sk_portal_incomplete.wsg", &soko->sokoDefaultTheme.portal_incompleteWSG, false); + loadWsg("sk_goal.wsg", &soko->sokoDefaultTheme.goalWSG, false); + + // we check against 0,0 as an invalid start location, and use file location instead. + soko->overworld_playerX = 0; + soko->overworld_playerY = 0; + + // Overworld Theme + soko->overworldTheme.playerDownWSG = soko->sokoDefaultTheme.playerDownWSG; + soko->overworldTheme.playerUpWSG = soko->sokoDefaultTheme.playerUpWSG; + soko->overworldTheme.playerLeftWSG = soko->sokoDefaultTheme.playerLeftWSG; + soko->overworldTheme.playerRightWSG = soko->sokoDefaultTheme.playerRightWSG; + soko->overworldTheme.crateWSG = soko->sokoDefaultTheme.crateWSG; + soko->overworldTheme.goalWSG = soko->sokoDefaultTheme.goalWSG; + soko->overworldTheme.crateOnGoalWSG = soko->sokoDefaultTheme.crateOnGoalWSG; + soko->overworldTheme.stickyCrateWSG = soko->sokoDefaultTheme.stickyCrateWSG; + soko->overworldTheme.portal_completeWSG = soko->sokoDefaultTheme.portal_completeWSG; + soko->overworldTheme.portal_incompleteWSG = soko->sokoDefaultTheme.portal_incompleteWSG; + soko->overworldTheme.wallColor = c111; + soko->overworldTheme.floorColor = c444; + + // Euler Theme + soko->eulerTheme.playerDownWSG = soko->sokoDefaultTheme.playerDownWSG; + soko->eulerTheme.playerUpWSG = soko->sokoDefaultTheme.playerUpWSG; + soko->eulerTheme.playerLeftWSG = soko->sokoDefaultTheme.playerLeftWSG; + soko->eulerTheme.playerRightWSG = soko->sokoDefaultTheme.playerRightWSG; + soko->eulerTheme.goalWSG = soko->sokoDefaultTheme.goalWSG; + + loadWsg("sk_e_crate.wsg", &soko->eulerTheme.crateWSG, false); + loadWsg("sk_sticky_trail_crate.wsg", &soko->eulerTheme.crateOnGoalWSG, false); + soko->eulerTheme.stickyCrateWSG = soko->sokoDefaultTheme.stickyCrateWSG; + soko->eulerTheme.portal_completeWSG = soko->sokoDefaultTheme.portal_completeWSG; + soko->eulerTheme.portal_incompleteWSG = soko->sokoDefaultTheme.portal_incompleteWSG; + soko->eulerTheme.wallColor = c000; + soko->eulerTheme.floorColor = c555; + soko->eulerTheme.altFloorColor = c433; // painted tiles color. + + // Initialize the menu + soko->menu = initMenu(sokoModeName, sokoMenuCb); + soko->menuManiaRenderer = initMenuManiaRenderer(&soko->ibm, NULL, NULL); + + addSingleItemToMenu(soko->menu, sokoResumeGameLabel); + addSingleItemToMenu(soko->menu, sokoNewGameLabel); + + // Set the mode to menu mode + soko->screen = SOKO_MENU; + soko->state = SKS_INIT; + + // load up the level list. + soko->levelFileText = loadTxt("SK_LEVEL_LIST.txt", true); + sokoExtractLevelNamesAndIndices(soko); + + // load level solved state. + sokoLoadLevelSolvedState(soko); +} + +static void sokoExitMode(void) +{ + // Deinitialize the menu + deinitMenu(soko->menu); + deinitMenuManiaRenderer(soko->menuManiaRenderer); + + // Free the font + freeFont(&soko->ibm); + + // free the level name file + freeTxt(soko->levelFileText); + + // free sprites + // default + freeWsg(&soko->sokoDefaultTheme.playerUpWSG); + freeWsg(&soko->sokoDefaultTheme.playerDownWSG); + freeWsg(&soko->sokoDefaultTheme.playerLeftWSG); + freeWsg(&soko->sokoDefaultTheme.playerRightWSG); + freeWsg(&soko->sokoDefaultTheme.crateWSG); + freeWsg(&soko->sokoDefaultTheme.crateOnGoalWSG); + freeWsg(&soko->sokoDefaultTheme.stickyCrateWSG); + freeWsg(&soko->sokoDefaultTheme.portal_completeWSG); + freeWsg(&soko->sokoDefaultTheme.portal_incompleteWSG); + freeWsg(&soko->sokoDefaultTheme.goalWSG); + // euler + freeWsg(&soko->eulerTheme.crateWSG); + freeWsg(&soko->eulerTheme.crateOnGoalWSG); + free(soko->levelBinaryData); // TODO is this the best place to free? + // Free everything else + free(soko); +} + +static void sokoMenuCb(const char* label, bool selected, uint32_t settingVal) +{ + if (selected) + { + // placeholder. + if (label == sokoResumeGameLabel) + { + int32_t data; + readNvs32("sk_data", &data); + // bitshift, etc, as needed. + uint16_t lastSaved = (uint16_t)data; + sokoLoadGameplay(soko, lastSaved, false); + sokoInitGameBin(soko); + soko->screen = SOKO_LEVELPLAY; + } + else if (label == sokoNewGameLabel) + { + // load level. + // we probably shouldn't have a new game option; just an overworld option. + sokoLoadGameplay(soko, 0, true); + sokoInitGameBin(soko); + soko->screen = SOKO_LEVELPLAY; + } + } +} + +static void sokoMainLoop(int64_t elapsedUs) +{ + // Pick what runs and draws depending on the screen being displayed + switch (soko->screen) + { + case SOKO_MENU: + { + // Process button events + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Pass button events to the menu + soko->menu = menuButton(soko->menu, evt); + } + + // Draw the menu + drawMenuMania(soko->menu, soko->menuManiaRenderer, elapsedUs); + break; + } + case SOKO_LEVELPLAY: + { + // pass along to other gameplay, in other file + // Always process button events, regardless of control scheme, so the main menu button can be captured + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Save the button state + soko->input.btnState = evt.state; + } + + // process input functions in input. + // Input will turn state into function calls into the game code, and handle complexities. + sokoPreProcessInput(&soko->input, elapsedUs); + // background had been drawn, input has been processed and functions called. Now do followup logic and draw + // level. gameplay loop + soko->gameLoopFunc(soko, elapsedUs); + break; + } + case SOKO_LOADNEWLEVEL: + { + sokoLoadGameplay(soko, soko->loadNewLevelIndex, soko->loadNewLevelFlag); + sokoInitNewLevel(soko, soko->currentLevel.gameMode); + printf("Go to gameplay\n"); + soko->loadNewLevelFlag = false; // reset flag. + soko->screen = SOKO_LEVELPLAY; + } + } +} + +// void freeEntity(soko_abs_t* self, sokoEntity_t* entity) // Free internal entity structures +// { +// if (entity->propFlag) +// { +// if (entity->properties->targetCount) +// { +// free(entity->properties->targetX); +// free(entity->properties->targetY); +// } +// free(entity->properties); +// entity->propFlag = false; +// } +// self->currentLevel.entityCount -= 1; +// } + +// placeholder. +static void sokoBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +{ + // Use TURBO drawing mode to draw individual pixels fast + SETUP_FOR_TURBO(); + uint16_t shiftReg = 0xACE1u; + uint16_t bit = 0; + switch (soko->background) + { + case SKBG_GRID: + { + for (int16_t yp = y; yp < y + h; yp++) + { + for (int16_t xp = x; xp < x + w; xp++) + { + if ((0 == xp % 20) || (0 == yp % 20)) + { + TURBO_SET_PIXEL(xp, yp, c002); + } + else + { + TURBO_SET_PIXEL(xp, yp, c001); + } + } + } + break; + } + case SKBG_BLACK: + { + for (int16_t yp = y; yp < y + h; yp++) + { + for (int16_t xp = x; xp < x + w; xp++) + { + TURBO_SET_PIXEL(xp, yp, c000); + } + } + break; + } + case SKBG_FORREST: + { + for (int16_t yp = y; yp < y + h; yp += 8) + { + for (int16_t xp = x; xp < x + w; xp += 8) + { + // not random enough but im going to leave it as is. + // LFSR + bit = ((shiftReg >> 0) ^ (shiftReg >> 2) ^ (shiftReg >> 3) ^ (shiftReg >> 5)) & 1u; + shiftReg = (shiftReg >> 1) | (bit << 15); + shiftReg = shiftReg + yp + xp * 3 + 1; + + for (int16_t ypp = yp; ypp < yp + 8; ypp++) + { + for (int16_t xpp = xp; xpp < xp + 8; xpp++) + { + if ((shiftReg & 3) == 0) + { + TURBO_SET_PIXEL(xpp, ypp, c020); + } + else + { + TURBO_SET_PIXEL(xpp, ypp, c121); + } + } + } + } + } + break; + } + default: + { + break; + } + } +} +// todo: move to soko_save +static void sokoExtractLevelNamesAndIndices(soko_abs_t* self) +{ + printf("Loading Level List...!\n"); + // printf("%s\n", self->levelFileText); + // printf("%d\n", (int)strlen(self->levelFileText)); + // char* a = strstr(self->levelFileText,":"); + // char* b = strstr(a,".bin:"); + // printf("%d",(int)((int)b-(int)a)); + // char* stringPtrs[30]; + // memset(stringPtrs,0,30*sizeof(char*)); + char** stringPtrs = soko->levelNames; + memset(stringPtrs, 0, sizeof(soko->levelNames)); + memset(soko->levelIndices, 0, sizeof(soko->levelIndices)); + int intInd = 0; + int ind = 0; + char* storageStr = strtok(self->levelFileText, ":"); + while (storageStr != NULL) + { + // strtol(storageStr, NULL, 10) && + if (!(strstr(storageStr, ".bin"))) // Make sure you're not accidentally reading a number from a filename + { + soko->levelIndices[intInd] = (int)strtol(storageStr, NULL, 10); + // printf("NumberThing: %s :: %d\n",storageStr,(int)strtol(storageStr,NULL,10)); + intInd++; + } + else + { + if (!strpbrk(storageStr, "\n\t\r ") && (strstr(storageStr, ".bin"))) + { + // int tokLen = strlen(storageStr); + // char* tempPtr = calloc((tokLen + 1), sizeof(char)); // Length plus null teminator + // strcpy(tempPtr,storageStr); + // stringPtrs[ind] = tempPtr; + stringPtrs[ind] = storageStr; + // printf("%s\n",storageStr); + ind++; + } + } + // printf("This guy!\n"); + storageStr = strtok(NULL, ":"); + } + printf("Strings: %d, Ints: %d\n", ind, intInd); + printf("Levels and indices:\n"); + for (int i = ind - 1; i > -1; i--) + { + printf("Index: %d : %d : %s\n", i, soko->levelIndices[i], stringPtrs[i]); + } +} diff --git a/main/modes/games/soko/soko.h b/main/modes/games/soko/soko.h new file mode 100644 index 000000000..e073be957 --- /dev/null +++ b/main/modes/games/soko/soko.h @@ -0,0 +1,279 @@ +#ifndef _SOKO_MODE_H_ +#define _SOKO_MODE_H_ + +#include "swadge2024.h" +#include "soko_input.h" +#include "soko_consts.h" + +extern swadgeMode_t sokoMode; + +typedef enum +{ + SOKO_OVERWORLD = 0, + SOKO_CLASSIC = 1, + SOKO_EULER = 2, + SOKO_LASERBOUNCE = 3 +} soko_var_t; + +typedef enum +{ + SOKO_MENU, + SOKO_LEVELPLAY, + SOKO_LOADNEWLEVEL +} sokoScreen_t; + +typedef enum +{ + SKBG_GRID = 0, + SKBG_BLACK = 1, + SKBG_FORREST = 2, +} sokoBackground_t; + +typedef enum +{ + SKB_EMPTY = 0, + SKB_WALL = 1, + SKB_FLOOR = 2, + SKB_GOAL = 3, + SKB_NO_WALK = 4, + SKB_OBJSTART = 201, // Object and Signal Bytes are over 200 + SKB_COMPRESS = 202, + SKB_PLAYER = 203, + SKB_CRATE = 204, + SKB_WARPINTERNAL = 205, + SKB_WARPINTERNALEXIT = 206, + SKB_WARPEXTERNAL = 207, + SKB_BUTTON = 208, + SKB_LASEREMITTER = 209, + SKB_LASERRECEIVEROMNI = 210, + SKB_LASERRECEIVER = 211, + SKB_LASER90ROTATE = 212, + SKB_GHOSTBLOCK = 213, + SKB_OBJEND = 230 +} soko_bin_t; // Binary file byte value decode list +typedef struct soko_portal_s +{ + uint8_t x; + uint8_t y; + uint8_t index; + bool levelCompleted; // use this to show completed levels later +} soko_portal_t; + +typedef struct soko_goal_s +{ + uint8_t x; + uint8_t y; +} soko_goal_t; + +typedef enum +{ + SKS_INIT, ///< meta enum used for edge cases + SKS_GAMEPLAY, + SKS_VICTORY, +} sokoGameState_t; + +/* +typedef enum +{ + SOKO_OVERWORLD = 0, + SOKO_CLASSIC = 1, + SOKO_EULER = 2 +} soko_var_t; +*/ + +typedef enum +{ + SKE_NONE = 0, + SKE_PLAYER = 1, + SKE_CRATE = 2, + SKE_LASER_90 = 3, + SKE_STICKY_CRATE = 4, + SKE_STICKY_TRAIL_CRATE = 5, + SKE_WARP = 11, + SKE_BUTTON = 6, + SKE_LASER_EMIT_UP = 7, + SKE_LASER_RECEIVE_OMNI = 8, + SKE_LASER_RECEIVE = 9, + SKE_GHOST = 10 +} sokoEntityType_t; + +typedef enum +{ + SKT_EMPTY = 0, + SKT_FLOOR = 1, + SKT_WALL = 2, + SKT_GOAL = 3, + SKT_NO_WALK = 4, + SKT_PORTAL = 5, + SKT_LASER_EMIT = 6, // To Be Removed + SKT_LASER_RECEIVE = 7, // To Be Removed + SKT_FLOOR_WALKED = 8 +} sokoTile_t; + +typedef struct +{ + bool sticky; // For Crates, this determines if crates stick to players. For Buttons, this determines if the button + // stays down. + bool trail; // Crates leave Euler trails + bool players; // For Crates, allow player push. For Button, allow player press. + bool crates; // For Buttons, allow crate push. For Portals, allow crate transport. + bool inverted; // For Buttons, invert default state of affected blocks. For ghost blocks, inverts default + // tangibility. Button and Ghostblock with both cancel. + uint8_t* targetX; + uint8_t* targetY; + uint8_t targetCount; + uint8_t hp; +} sokoEntityProperties_t; // this is a separate type so that it can be allocated as several different types with a void + // pointer and some aggressive casting. + +typedef struct +{ + sokoEntityType_t type; + uint16_t x; + uint16_t y; + sokoDirection_t facing; + sokoEntityProperties_t properties; + bool propFlag; +} sokoEntity_t; + +typedef struct sokoVec_s +{ + int16_t x; + int16_t y; +} sokoVec_t; +typedef struct +{ + uint16_t moveID; + bool isEntity; + sokoEntity_t* entity; + sokoTile_t tile; + uint16_t x; + uint16_t y; + sokoDirection_t facing; +} sokoUndoMove_t; + +typedef struct sokoCollision_s +{ + uint16_t x; + uint16_t y; + uint16_t entityFlag; + uint16_t entityIndex; + +} sokoCollision_t; + +typedef struct +{ + wsg_t playerWSG; + wsg_t playerUpWSG; + wsg_t playerRightWSG; + wsg_t playerLeftWSG; + wsg_t playerDownWSG; + wsg_t goalWSG; + wsg_t crateWSG; + wsg_t crateOnGoalWSG; + wsg_t stickyCrateWSG; + wsg_t portal_incompleteWSG; + wsg_t portal_completeWSG; + paletteColor_t wallColor; + paletteColor_t floorColor; + paletteColor_t altFloorColor; +} sokoTheme_t; + +typedef struct +{ + uint16_t levelScale; + uint8_t width; + uint8_t height; + uint8_t entityCount; + uint16_t playerIndex; // we could have multiple players... + sokoTile_t tiles[SOKO_MAX_LEVELSIZE][SOKO_MAX_LEVELSIZE]; + sokoEntity_t entities[SOKO_MAX_ENTITY_COUNT]; // todo: pointer and runtime array size + soko_var_t gameMode; +} sokoLevel_t; + +typedef struct soko_abs_s soko_abs_t; + +typedef struct soko_abs_s +{ + // meta + menu_t* menu; ///< The menu structure + menuManiaRenderer_t* menuManiaRenderer; ///< Renderer for the menu + font_t ibm; ///< The font used in the menu and game + sokoScreen_t screen; ///< The screen being displayed + + char* levelFileText; + char* levelNames[SOKO_LEVEL_COUNT]; + uint16_t levelIndices[SOKO_LEVEL_COUNT]; + bool levelSolved[SOKO_LEVEL_COUNT]; + + // game settings + uint16_t maxPush; ///< Maximum number of crates the player can push. Use 0 for no limit. + sokoGameState_t state; + + // theme settings + sokoTheme_t* currentTheme; ///< Points to one of the other themes. + sokoTheme_t overworldTheme; + sokoTheme_t eulerTheme; + sokoTheme_t sokoDefaultTheme; + sokoBackground_t background; + + // level + char* levels[SOKO_LEVEL_COUNT]; ///< List of wsg filenames. not comitted to storing level data like this, but idk if + ///< I need level names like picross. + // wsg_t levelWSG; ///< Current level + uint8_t* levelBinaryData; + + soko_portal_t portals[SOKO_MAX_PORTALS]; + uint8_t portalCount; + + soko_goal_t goals[SOKO_MAX_GOALS]; + uint8_t goalCount; + + // input + sokoGameplayInput_t input; + + // current level + uint16_t currentLevelIndex; + sokoLevel_t currentLevel; + + // undo ring buffer + sokoUndoMove_t history[SOKO_UNDO_BUFFER_SIZE]; // if >255, change index to uint16. + uint8_t historyBufferTail; + uint8_t historyCurrent; + bool historyNewMove; + + // todo: rename to 'isVictory' + bool allCratesOnGoal; + uint16_t moveCount; + uint16_t undoCount; + + // camera features + bool camEnabled; + uint16_t camX; + uint16_t camY; + uint16_t camPadExtentX; + uint16_t camPadExtentY; + uint16_t camWidth; + uint16_t camHeight; + + // game loop functions //Functions are moved into game struct so engine can support different game rules + void (*gameLoopFunc)(soko_abs_t* self, int64_t elapsedUs); + void (*sokoTryPlayerMovementFunc)(soko_abs_t* self); + bool (*sokoTryMoveEntityInDirectionFunc)(soko_abs_t* self, sokoEntity_t* entity, int dx, int dy, uint16_t push); + void (*drawTilesFunc)(soko_abs_t* self, sokoLevel_t* level); + bool (*isVictoryConditionFunc)(soko_abs_t* self); + sokoTile_t (*sokoGetTileFunc)(soko_abs_t* self, int x, int y); + + // Player Convenience Pointer + sokoEntity_t* soko_player; + // overworld enter/exit data + uint16_t overworld_playerX; + uint16_t overworld_playerY; + + bool loadNewLevelFlag; + uint8_t loadNewLevelIndex; + soko_var_t loadNewLevelVariant; + +} soko_abs_t; + +#endif diff --git a/main/modes/games/soko/soko_consts.h b/main/modes/games/soko/soko_consts.h new file mode 100644 index 000000000..d3da0a065 --- /dev/null +++ b/main/modes/games/soko/soko_consts.h @@ -0,0 +1,13 @@ +#ifndef SOKO_CONSTS_H +#define SOKO_CONSTS_H + +#define SOKO_LEVEL_COUNT 30 +#define SOKO_MAX_LEVELSIZE 30 +#define SOKO_MAX_ENTITY_COUNT 15 +#define SOKO_MAX_PORTALS 25 +#define SOKO_MAX_GOALS 20 +#define SOKO_MAX_REDIRECTS 15 // Should be equal to MAX_ENTITY until I find an edge case +#define SOKO_VICTORY_TIMER_US 1000000 +#define SOKO_UNDO_BUFFER_SIZE 255 + +#endif // SOKO_CONSTS_H \ No newline at end of file diff --git a/main/modes/games/soko/soko_game.c b/main/modes/games/soko/soko_game.c new file mode 100644 index 000000000..a93022eb2 --- /dev/null +++ b/main/modes/games/soko/soko_game.c @@ -0,0 +1,313 @@ +#include "soko_game.h" +#include "soko.h" +#include "soko_gamerules.h" + +/* +void sokoTryPlayerMovement(void); +sokoTile_t sokoGetTile(int, int); +bool sokoTryMoveEntityInDirection(sokoEntity_t*, int, int,uint16_t); +bool allCratesOnGoal(void); + +*/ +// sokoDirection_t sokoDirectionFromDelta(int, int); + +// soko_t* s; +// sokoEntity_t* player; + +soko_abs_t* soko_s; + +void sokoInitGameBin(soko_abs_t* soko) +{ + printf("init sokoban game from bin file"); + + soko_s = soko; + soko_s->soko_player = &soko_s->currentLevel.entities[soko_s->currentLevel.playerIndex]; + + soko->camX = soko_s->soko_player->x; + soko->camY = soko_s->soko_player->y; + + sokoInitInput(&soko_s->input); + + soko->state = SKS_GAMEPLAY; + + sokoConfigGamemode(soko, soko->currentLevel.gameMode); +} + +void sokoInitGame(soko_abs_t* soko) +{ + printf("init sokobon game.\n"); + + // Configure conveninence pointers. + soko_s = soko; + soko_s->soko_player = &soko_s->currentLevel.entities[soko_s->currentLevel.playerIndex]; + + // reset camera + soko->camX = soko_s->soko_player->x; + soko->camY = soko_s->soko_player->y; + + sokoInitInput(&soko_s->input); + + soko->state = SKS_GAMEPLAY; + + sokoConfigGamemode(soko, SOKO_OVERWORLD); + + // sokoConfigGamemode(soko,SOKO_EULER); +} + +void sokoInitNewLevel(soko_abs_t* soko, soko_var_t variant) +{ + printf("Init New Level.\n"); + + soko_s = soko; + soko_s->soko_player = &soko_s->currentLevel.entities[soko_s->currentLevel.playerIndex]; + sokoInitInput(&soko_s->input); + + // set gameplay settings from default settings, if we want powerups or whatever that adjusts them, or have a state + // machine. + soko_s->maxPush = 0; // set to 1 for "traditional" sokoban. + + soko->state = SKS_GAMEPLAY; + + sokoConfigGamemode(soko, variant); +} + +/* +void gameLoop(int64_t elapsedUs) +{ + if(s->state == SKS_GAMEPLAY) + { + //logic + sokoTryPlayerMovement(); + + //victory status. stored separate from gamestate because of future gameplay ideas/remixes. + s->allCratesOnGoal = allCratesOnGoal(); + if(s->allCratesOnGoal){ + s->state = SKS_VICTORY; + } + //draw level + drawTiles(&s->currentLevel); + + }else if(s->state == SKS_VICTORY) + { + //check for input for exit/next level. + drawTiles(&s->currentLevel); + } + + + //DEBUG PLACEHOLDER: + // Render the time to a string + char str[16] = {0}; + int16_t tWidth; + if(!s->allCratesOnGoal) + { + snprintf(str, sizeof(str) - 1, "sokoban"); + // Measure the width of the time string + tWidth = textWidth(&s->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&s->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + }else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&s->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&s->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + +} + + +//Gameplay Logic +void sokoTryPlayerMovement() +{ + + if(s->input.playerInputDeltaX == 0 && s->input.playerInputDeltaY == 0) + { + return; + } + + sokoTryMoveEntityInDirection(player,s->input.playerInputDeltaX,s->input.playerInputDeltaY,0); +} + + +bool sokoTryMoveEntityInDirection(sokoEntity_t* entity, int dx, int dy, uint16_t push) +{ + //prevent infitnite loop where you push yourself nowhere. + if(dx == 0 && dy == 0 ) + { + return false; + } + + //maxiumum number of crates we can push. Traditional sokoban has a limit of one. I prefer infinite for challenges. + if(s->maxPush != 0 && push>s->maxPush) + { + return false; + } + + int px = entity->x+dx; + int py = entity->y+dy; + sokoTile_t nextTile = sokoGetTile(px,py); + + if(nextTile == SKT_FLOOR || nextTile == SKT_GOAL || nextTile == SKT_EMPTY) + { + //Is there an entity at this position? + for (size_t i = 0; i < s->currentLevel.entityCount; i++) + { + //is pushable. + if(s->currentLevel.entities[i].type == SKE_CRATE) + { + if(s->currentLevel.entities[i].x == px && s->currentLevel.entities[i].y == py) + { + if(sokoTryMoveEntityInDirection(&s->currentLevel.entities[i],dx,dy,push+1)) + { + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx,dy); + return true; + }else{ + //can't push? can't move. + return false; + } + + } + } + + } + + //No wall in front of us and nothing to push, we can move. + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx,dy); + return true; + } + + return false; +} + +//draw the tiles (and entities, for now) of the level. +void drawTiles(sokoLevel_t* level) +{ + SETUP_FOR_TURBO(); + uint16_t scale = level->levelScale; + uint16_t ox = (TFT_WIDTH/2)-((level->width)*scale/2); + uint16_t oy = (TFT_HEIGHT/2)-((level->height)*scale/2); + + for (size_t x = 0; x < level->width; x++) + { + for (size_t y = 0; y < level->height; y++) + { + paletteColor_t color = cTransparent; + switch (level->tiles[x][y]) + { + case SKT_FLOOR: + color = c444; + break; + case SKT_WALL: + color = c111; + break; + case SKT_GOAL: + color = c141; + break; + case SKT_EMPTY: + color = cTransparent; + default: + break; + } + + //Draw a square. + //none of this matters it's all getting replaced with drawwsg later. + if(color != cTransparent){ + for (size_t xd = ox+x*scale; xd < ox+x*scale+scale; xd++) + { + for (size_t yd = oy+y*scale; yd < oy+y*scale+scale; yd++) + { + TURBO_SET_PIXEL(xd, yd, color); + } + } + } + //draw outline around the square. + //drawRect(ox+x*s,oy+y*s,ox+x*s+s,oy+y*s+s,color); + } + } + + for (size_t i = 0; i < level->entityCount; i++) + { + switch (level->entities[i].type) + { + case SKE_PLAYER: + switch(level->entities[i].facing){ + case SKD_UP: + drawWsg(&s->playerUpWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + case SKD_RIGHT: + drawWsg(&s->playerRightWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + case SKD_LEFT: + drawWsg(&s->playerLeftWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + case SKD_DOWN: + default: + drawWsg(&s->playerDownWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + } + + break; + case SKE_CRATE: + drawWsg(&s->crateWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + case SKE_NONE: + default: + break; + } + } + +} + +bool allCratesOnGoal() +{ + for (size_t i = 0; i < s->currentLevel.entityCount; i++) + { + if(s->currentLevel.entities[i].type == SKE_CRATE) + { + if(s->currentLevel.tiles[s->currentLevel.entities[i].x][s->currentLevel.entities[i].y] != SKT_GOAL) + { + return false; + } + } + } + + return true; +} + + +sokoDirection_t sokoDirectionFromDelta(int dx,int dy) +{ + if(dx > 0 && dy == 0) + { + return SKD_RIGHT; + }else if(dx < 0 && dy == 0) + { + return SKD_LEFT; + }else if(dx == 0 && dy < 0) + { + return SKD_UP; + }else if(dx == 0 && dy > 0) + { + return SKD_DOWN; + } + + return SKD_NONE; +} +sokoTile_t sokoGetTile(int x, int y) +{ + if(x<0 || x >= s->currentLevel.width) + { + return SKT_WALL; + } + if(y<0 || y >= s->currentLevel.height) + { + return SKT_WALL; + } + + return s->currentLevel.tiles[x][y]; +} +*/ \ No newline at end of file diff --git a/main/modes/games/soko/soko_game.h b/main/modes/games/soko/soko_game.h new file mode 100644 index 000000000..41c60aaf0 --- /dev/null +++ b/main/modes/games/soko/soko_game.h @@ -0,0 +1,12 @@ +#ifndef SOKO_GAME_H +#define SOKO_GAME_H + +#include "soko.h" + +void sokoInitGame(soko_abs_t*); +void sokoInitGameBin(soko_abs_t*); +void sokoInitNewLevel(soko_abs_t* soko, soko_var_t variant); +void gameLoop(int64_t); +void drawTiles(sokoLevel_t*); + +#endif // SOKO_GAME_H \ No newline at end of file diff --git a/main/modes/games/soko/soko_gamerules.c b/main/modes/games/soko/soko_gamerules.c new file mode 100644 index 000000000..625a1591d --- /dev/null +++ b/main/modes/games/soko/soko_gamerules.c @@ -0,0 +1,1240 @@ +#include "soko_game.h" +#include "soko.h" +#include "soko_gamerules.h" +#include "soko_save.h" +#include "shapes.h" +#include "soko_undo.h" + +// clang-format off +// True if the entity CANNOT go on the tile +bool sokoEntityTileCollision[6][9] = { + // Empty, //floor//wall//goal//noWalk//portal //l-emit //l-receive //walked + {true, false, true, false, true,false, false, false, false}, // SKE_NONE + {true, false, true, false, true,false, false, false, true}, // PLAYER + {true, false, true, false, true,false, false, false, false}, // CRATE + {true, false, true, false, true,false, false, false, false}, // LASER + {true, false, true, false, true,false, false, false, false}, // STICKY CRATE + {true, false, true, false, true,false, false, false, true} // STICKY_TRAIL_CRATE +}; +// clang-format on + +uint64_t victoryDanceTimer; + +void sokoConfigGamemode( + soko_abs_t* soko, + soko_var_t variant) // This should be called when you reload a level to make sure game rules are correct +{ + soko->currentTheme = &soko->sokoDefaultTheme; + soko->background = SKBG_GRID; + + if (variant == SOKO_CLASSIC) // standard gamemode. Check 'variant' variable + { + printf("Config Soko to Classic\n"); + soko->maxPush = 1; // set to 1 for "traditional" sokoban. + soko->gameLoopFunc = absSokoGameLoop; + soko->sokoTryPlayerMovementFunc = absSokoTryPlayerMovement; + soko->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + soko->drawTilesFunc = absSokoDrawTiles; + soko->isVictoryConditionFunc = absSokoAllCratesOnGoal; + soko->sokoGetTileFunc = absSokoGetTile; + } + else if (variant == SOKO_EULER) // standard gamemode. Check 'variant' variable + { + printf("Config Soko to Euler\n"); + soko->maxPush = 0; // set to 0 for infinite push. + soko->gameLoopFunc = absSokoGameLoop; + soko->sokoTryPlayerMovementFunc = eulerSokoTryPlayerMovement; + soko->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + soko->drawTilesFunc = absSokoDrawTiles; + soko->isVictoryConditionFunc = eulerNoUnwalkedFloors; + soko->currentTheme = &soko->eulerTheme; + soko->background = SKBG_BLACK; + + // Initialze spaces below the player and sticky. + for (size_t i = 0; i < soko->currentLevel.entityCount; i++) + { + if (soko->currentLevel.entities[i].type == SKE_PLAYER + || soko->currentLevel.entities[i].type == SKE_STICKY_TRAIL_CRATE) + { + soko->currentLevel.tiles[soko->currentLevel.entities[i].x][soko->currentLevel.entities[i].y] + = SKT_FLOOR_WALKED; + } + } + } + else if (variant == SOKO_OVERWORLD) + { + printf("Config Soko to Overworld\n"); + soko->maxPush = 0; // set to 0 for infinite push. + soko->gameLoopFunc = overworldSokoGameLoop; + soko->sokoTryPlayerMovementFunc = absSokoTryPlayerMovement; + soko->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + soko->drawTilesFunc = absSokoDrawTiles; + soko->isVictoryConditionFunc = overworldPortalEntered; + soko->sokoGetTileFunc = absSokoGetTile; + soko->currentTheme = &soko->overworldTheme; + // set position to previous overworld positon when re-entering the overworld + // but like... not an infinite loop? + soko->soko_player->x = soko->overworld_playerX; + soko->soko_player->y = soko->overworld_playerY; + soko->background = SKBG_FORREST; + + for (size_t i = 0; i < soko->portalCount; i++) + { + if (soko->portals[i].index < SOKO_LEVEL_COUNT) + { + soko->portals[i].levelCompleted = soko->levelSolved[soko->portals[i].index]; + } + } + } + else if (variant == SOKO_LASERBOUNCE) + { + printf("Config Soko to Laser Bounce\n"); + soko->maxPush = 0; // set to 0 for infinite push. + soko->gameLoopFunc = laserBounceSokoGameLoop; + soko->sokoTryPlayerMovementFunc = absSokoTryPlayerMovement; + soko->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + soko->drawTilesFunc = absSokoDrawTiles; + soko->isVictoryConditionFunc = absSokoAllCratesOnGoal; + soko->sokoGetTileFunc = absSokoGetTile; + } + else + { + printf("invalid gamemode."); + } + + // add conditional for alternative variants + sokoInitHistory(soko); +} + +void laserBounceSokoGameLoop(soko_abs_t* self, int64_t elapsedUs) +{ + if (self->state == SKS_GAMEPLAY) + { + // logic + self->sokoTryPlayerMovementFunc(self); + + // victory status. stored separate from gamestate because of future gameplay ideas/remixes. + // todo: rename to isVictory or such. + self->allCratesOnGoal = self->isVictoryConditionFunc(self); + if (self->allCratesOnGoal) + { + self->state = SKS_VICTORY; + victoryDanceTimer = 0; + } + // draw level + self->drawTilesFunc(self, &self->currentLevel); + drawLaserFromEntity(self, self->soko_player); + } + else if (self->state == SKS_VICTORY) + { + // check for input for exit/next level. + self->drawTilesFunc(self, &self->currentLevel); + victoryDanceTimer += elapsedUs; + if (victoryDanceTimer > SOKO_VICTORY_TIMER_US) + { + sokoSolveCurrentLevel(self); + self->loadNewLevelIndex = 0; + self->loadNewLevelFlag = true; + self->screen = SOKO_LOADNEWLEVEL; + } + } + + // DEBUG PLACEHOLDER: + // Render the time to a string + char str[16] = {0}; + int16_t tWidth; + if (!self->allCratesOnGoal) + { + // snprintf(buffer, buflen - 1, "%s%s", item->label, item->options[item->currentOpt]); + snprintf(str, sizeof(str) - 1, "%s", self->levelNames[self->currentLevelIndex]); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + sharedGameLoop(self); +} + +void absSokoGameLoop(soko_abs_t* soko, int64_t elapsedUs) +{ + if (soko->state == SKS_GAMEPLAY) + { + // logic + soko->sokoTryPlayerMovementFunc(soko); + + // undo check + if (soko->input.undo) + { + sokoUndo(soko); + } + + // victory status. stored separate from gamestate because of future gameplay ideas/remixes. + // todo: rename to isVictory or such. + soko->allCratesOnGoal = soko->isVictoryConditionFunc(soko); + if (soko->allCratesOnGoal) + { + soko->state = SKS_VICTORY; + victoryDanceTimer = 0; + } + // draw level + soko->drawTilesFunc(soko, &soko->currentLevel); + } + else if (soko->state == SKS_VICTORY) + { + // check for input for exit/next level. + soko->drawTilesFunc(soko, &soko->currentLevel); + victoryDanceTimer += elapsedUs; + if (victoryDanceTimer > SOKO_VICTORY_TIMER_US) + { + sokoSolveCurrentLevel(soko); + soko->loadNewLevelIndex = 0; + soko->loadNewLevelFlag = true; + soko->screen = SOKO_LOADNEWLEVEL; + } + } + + // DEBUG PLACEHOLDER: + char str[16] = {0}; + int16_t tWidth; + if (!soko->allCratesOnGoal) + { + snprintf(str, sizeof(str) - 1, "%s", soko->levelNames[soko->currentLevelIndex]); + // Measure the width of the time string + tWidth = textWidth(&soko->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&soko->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&soko->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&soko->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + sharedGameLoop(soko); +} + +void sharedGameLoop(soko_abs_t* self) +{ + if (self->input.restartLevel) + { + restartCurrentLevel(self); + } + else if (self->input.exitToOverworld) + { + exitToOverworld(self); + } +} + +// Gameplay Logic +void absSokoTryPlayerMovement(soko_abs_t* soko) +{ + if (soko->input.playerInputDeltaX == 0 && soko->input.playerInputDeltaY == 0) + { + return; + } + + bool b = soko->sokoTryMoveEntityInDirectionFunc(soko, soko->soko_player, soko->input.playerInputDeltaX, + soko->input.playerInputDeltaY, 0); + sokoHistoryTurnOver(soko); + if (b) + { + soko->moveCount++; + } +} + +bool absSokoTryMoveEntityInDirection(soko_abs_t* self, sokoEntity_t* entity, int dx, int dy, uint16_t push) +{ + // prevent infitnite loop where you push yourself nowhere. + if (dx == 0 && dy == 0) + { + return false; + } + + // maxiumum number of crates we can push. Traditional sokoban has a limit of one. Euler is infinity. + if (self->maxPush != 0 && push > self->maxPush) + { + return false; + } + + int px = entity->x + dx; + int py = entity->y + dy; + sokoTile_t nextTile = self->sokoGetTileFunc(self, px, py); + + // when this is false, we CAN move. True for Collision. + if (!sokoEntityTileCollision[entity->type][nextTile]) + { + // Is there an entity at this position? + for (size_t i = 0; i < self->currentLevel.entityCount; i++) + { + // is pushable. + if (self->currentLevel.entities[i].type == SKE_CRATE + || self->currentLevel.entities[i].type == SKE_STICKY_CRATE) + { + if (self->currentLevel.entities[i].x == px && self->currentLevel.entities[i].y == py) + { + if (self->sokoTryMoveEntityInDirectionFunc(self, &self->currentLevel.entities[i], dx, dy, push + 1)) + { + sokoAddEntityMoveToHistory(self, entity, entity->x, entity->y, entity->facing); + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx, dy); + return true; // if entities overlap, we should not break here? + } + else + { + // can't push? can't move. + return false; + } + } + } + else if (self->currentLevel.entities[i].type == SKE_STICKY_TRAIL_CRATE) + { + // previous + // for euler. todo: make EulerTryMoveEntityInDirection instead of an if statement. + if (self->currentLevel.entities[i].x == px && self->currentLevel.entities[i].y == py) + { + if (self->sokoTryMoveEntityInDirectionFunc(self, &self->currentLevel.entities[i], dx, dy, push + 1)) + { + sokoAddEntityMoveToHistory(self, entity, entity->x, entity->y, entity->facing); + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx, dy); + return true; + } + else + { + // can't push? can't move. + return false; + } + } + } + } + + // todo: this is a hack, we should have separate absSokoTryMoveEntityInDirection functions. + if (self->currentLevel.gameMode == SOKO_EULER && entity->propFlag && entity->properties.trail) + { + if (self->currentLevel.tiles[entity->x + dx][entity->y + dy] == SKT_FLOOR) + { + sokoAddTileMoveToHistory(self, entity->x + dx, entity->y + dy, SKT_FLOOR); + self->currentLevel.tiles[entity->x + dx][entity->y + dy] = SKT_FLOOR_WALKED; + } + + if (self->currentLevel.tiles[entity->x][entity->y] == SKT_FLOOR) + { + sokoAddTileMoveToHistory(self, entity->x, entity->y, SKT_FLOOR); + self->currentLevel.tiles[entity->x][entity->y] = SKT_FLOOR_WALKED; + } + } + // No wall in front of us and nothing to push, we can move. + // we assume the player never gets pushed for undo here, so if it's the player moving, thats a new move. + sokoAddEntityMoveToHistory(self, entity, entity->x, entity->y, entity->facing); + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx, dy); + return true; + } + // all other floor types invalid. Be careful when we add tile types in different rule sets. + + return false; +} + +// draw the tiles (and entities, for now) of the level. +void absSokoDrawTiles(soko_abs_t* self, sokoLevel_t* level) +{ + uint16_t scale = level->levelScale; + // These are in level space (not pixels) and must be within bounds of currentLevel.tiles. + int16_t screenMinX, screenMaxX, screenMinY, screenMaxY; + // offsets. + uint16_t ox, oy; + + // Recalculate Camera Position + // todo: extract to a function if we end up with different draw functions. Part of future pointer refactor. + if (self->camEnabled) + { + // calculate camera position. Shift if needed. Cam position was initiated to player position. + if (self->soko_player->x > self->camX + self->camPadExtentX) + { + self->camX = self->soko_player->x - self->camPadExtentX; + } + else if (self->soko_player->x < self->camX - self->camPadExtentX) + { + self->camX = self->soko_player->x + self->camPadExtentX; + } + else if (self->soko_player->y > self->camY + self->camPadExtentY) + { + self->camY = self->soko_player->y - self->camPadExtentY; + } + else if (self->soko_player->y < self->camY - self->camPadExtentY) + { + self->camY = self->soko_player->y + self->camPadExtentY; + } + + // calculate offsets + ox = -self->camX * scale + (TFT_WIDTH / 2); + oy = -self->camY * scale + (TFT_HEIGHT / 2); + + // calculate out of bounds draws. todo: make tenery operators. + screenMinX = self->camX - self->camWidth / 2 - 1; + if (screenMinX < 0) + { + screenMinX = 0; + } + screenMaxX = self->camX + self->camWidth / 2 + 1; + if (screenMaxX > level->width) + { + screenMaxX = level->width; + } + screenMinY = self->camY - self->camHeight / 2 - 1; + if (screenMinY < 0) + { + screenMinY = 0; + } + screenMaxY = self->camY + self->camHeight / 2 + 1; + if (screenMaxY > level->height) + { + screenMaxY = level->height; + } + } + else + { // no camera + // calculate offsets to center the level. + ox = (TFT_WIDTH / 2) - ((level->width) * scale / 2); + oy = (TFT_HEIGHT / 2) - ((level->height) * scale / 2); + + // bounds are just the level. + screenMinX = 0; + screenMaxX = level->width; + screenMinY = 0; + screenMaxY = level->height; + } + + SETUP_FOR_TURBO(); + + // Tile Drawing (bg layer) + for (size_t x = screenMinX; x < screenMaxX; x++) + { + for (size_t y = screenMinY; y < screenMaxY; y++) + { + paletteColor_t color = cTransparent; + switch (level->tiles[x][y]) + { + case SKT_FLOOR: + { + color = self->currentTheme->floorColor; + break; + } + case SKT_WALL: + { + color = self->currentTheme->wallColor; + break; + } + case SKT_GOAL: + { + color = self->currentTheme->floorColor; + break; + } + case SKT_FLOOR_WALKED: + { + color = self->currentTheme->altFloorColor; + break; + } + case SKT_EMPTY: + { + color = cTransparent; + break; + } + case SKT_PORTAL: + { // todo: draw completed or not completed. + color = c441; + // color = self->currentTheme->floorColor; + break; + } + default: + { + break; + } + } + + // Draw a square. + // none of this matters it's all getting replaced with drawwsg later. + if (color != cTransparent) + { + for (size_t xd = ox + x * scale; xd < ox + x * scale + scale; xd++) + { + for (size_t yd = oy + y * scale; yd < oy + y * scale + scale; yd++) + { + TURBO_SET_PIXEL(xd, yd, color); + } + } + } + + if (level->tiles[x][y] == SKT_GOAL) + { + drawWsg(&self->currentTheme->goalWSG, ox + x * scale, oy + y * scale, false, false, 0); + } + + // DEBUG_DRAW_COUNT++; + // draw outline around the square. + // drawRect(ox+x*s,oy+y*s,ox+x*s+s,oy+y*s+s,color); + } + } + + // draw portal in overworld before entities. + // hypothetically, we can get rid of the overworld check, and there just won't be other portals? but there could be? + // sprint("a\n"); + if (self->currentLevel.gameMode == SOKO_OVERWORLD) + { + for (int i = 0; i < self->portalCount; i++) + { + if (self->portals[i].x >= screenMinX && self->portals[i].x <= screenMaxX && self->portals[i].y >= screenMinY + && self->portals[i].y <= screenMaxY) + { + if (self->portals[i].levelCompleted) + { + drawWsg(&self->currentTheme->portal_completeWSG, ox + self->portals[i].x * scale, + oy + self->portals[i].y * scale, false, false, 0); + } + else + { + drawWsg(&self->currentTheme->portal_incompleteWSG, ox + self->portals[i].x * scale, + oy + self->portals[i].y * scale, false, false, 0); + } + } + } + } + + // draw entities + for (size_t i = 0; i < level->entityCount; i++) + { + // don't bother drawing off screen + if (level->entities[i].x >= screenMinX && level->entities[i].x <= screenMaxX + && level->entities[i].y >= screenMinY && level->entities[i].y <= screenMaxY) + { + switch (level->entities[i].type) + { + case SKE_PLAYER: + { + switch (level->entities[i].facing) + { + case SKD_UP: + { + drawWsg(&self->currentTheme->playerUpWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + case SKD_RIGHT: + { + drawWsg(&self->currentTheme->playerRightWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + case SKD_LEFT: + { + drawWsg(&self->currentTheme->playerLeftWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + case SKD_DOWN: + default: + { + drawWsg(&self->currentTheme->playerDownWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + } + + break; + } + case SKE_CRATE: + { + if (self->currentLevel.tiles[self->currentLevel.entities[i].x][self->currentLevel.entities[i].y] + == SKT_GOAL) + { + drawWsg(&self->currentTheme->crateOnGoalWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + } + else + { + drawWsg(&self->currentTheme->crateWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + } + break; + } + case SKE_STICKY_CRATE: + { + drawWsg(&self->currentTheme->stickyCrateWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + case SKE_STICKY_TRAIL_CRATE: + { + drawWsg(&self->currentTheme->crateOnGoalWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + case SKE_NONE: + default: + { + break; + } + } + } + } +} + +bool absSokoAllCratesOnGoal(soko_abs_t* soko) +{ + for (size_t i = 0; i < soko->currentLevel.entityCount; i++) + { + if (soko->currentLevel.entities[i].type == SKE_CRATE) + { + if (soko->currentLevel.tiles[soko->currentLevel.entities[i].x][soko->currentLevel.entities[i].y] + != SKT_GOAL) + { + return false; + } + } + } + return true; +} + +sokoTile_t absSokoGetTile(soko_abs_t* self, int x, int y) +{ + if (x < 0 || x >= self->currentLevel.width) + { + return SKT_WALL; + } + if (y < 0 || y >= self->currentLevel.height) + { + return SKT_WALL; + } + + return self->currentLevel.tiles[x][y]; +} + +sokoDirection_t sokoDirectionFromDelta(int dx, int dy) +{ + if (dx > 0 && dy == 0) + { + return SKD_RIGHT; + } + else if (dx < 0 && dy == 0) + { + return SKD_LEFT; + } + else if (dx == 0 && dy < 0) + { + return SKD_UP; + } + else if (dx == 0 && dy > 0) + { + return SKD_DOWN; + } + + return SKD_NONE; +} + +sokoVec_t sokoGridToPix(soko_abs_t* self, sokoVec_t grid) // Convert grid position to screen pixel position +{ + sokoVec_t retVec; + uint16_t scale + = self->currentLevel + .levelScale; //@todo These should be in constants, but too lazy to change all references at the moment. + uint16_t ox = (TFT_WIDTH / 2) - ((self->currentLevel.width) * scale / 2); + uint16_t oy = (TFT_HEIGHT / 2) - ((self->currentLevel.height) * scale / 2); + retVec.x = ox + scale * grid.x + scale / 2; + retVec.y = oy + scale * grid.y + scale / 2; + return retVec; +} + +void drawLaserFromEntity(soko_abs_t* self, sokoEntity_t* emitter) +{ + sokoCollision_t impactSpot = sokoBeamImpact(self, self->soko_player); + // printf("Player Pos: x:%d,y:%d Facing:%d Impact Result: x:%d,y:%d, Flag:%d + // Index:%d\n",self->soko_player->x,self->soko_player->y,self->soko_player->facing,impactSpot.x,impactSpot.y,impactSpot.entityFlag,impactSpot.entityIndex); + sokoVec_t playerGrid, impactGrid; + playerGrid.x = emitter->x; + playerGrid.y = emitter->y; + impactGrid.x = impactSpot.x; + impactGrid.y = impactSpot.y; + sokoVec_t playerPix = sokoGridToPix(self, playerGrid); + sokoVec_t impactPix = sokoGridToPix(self, impactGrid); + drawLine(playerPix.x, playerPix.y, impactPix.x, impactPix.y, c500, 0); +} + +// void sokoDoBeam(soko_abs_t* self) +// { +// // bool receiverImpact; +// // for (int entInd = 0; entInd < self->currentLevel.entityCount; entInd++) +// // { +// // if (self->currentLevel.entities[entInd].type == SKE_LASER_EMIT_UP) +// // { +// // self->currentLevel.entities[entInd].properties->targetCount = 0; +// // receiverImpact = sokoBeamImpactRecursive( +// // self, self->currentLevel.entities[entInd].x, self->currentLevel.entities[entInd].y, +// // self->currentLevel.entities[entInd].type, &self->currentLevel.entities[entInd]); +// // } +// // } +// } + +bool sokoLaserTileCollision(sokoTile_t testTile) +{ + switch (testTile) + { + case SKT_EMPTY: + { + return false; + } + case SKT_FLOOR: + { + return false; + } + case SKT_WALL: + { + return true; + } + case SKT_GOAL: + { + return false; + } + case SKT_PORTAL: + { + return false; + } + case SKT_FLOOR_WALKED: + { + return false; + } + case SKT_NO_WALK: + { + return false; + } + default: + { + return false; + } + } +} + +bool sokoLaserEntityCollision(sokoEntityType_t testEntity) +{ + switch (testEntity) // Anything that doesn't unconditionally pass should return true + { + case SKE_NONE: + { + return false; + } + case SKE_PLAYER: + { + return false; + } + case SKE_CRATE: + { + return true; + } + case SKE_LASER_90: + { + return true; + } + case SKE_STICKY_CRATE: + { + return true; + } + case SKE_WARP: + { + return false; + } + case SKE_BUTTON: + { + return false; + } + case SKE_LASER_EMIT_UP: + { + return true; + } + case SKE_LASER_RECEIVE_OMNI: + { + return true; + } + case SKE_LASER_RECEIVE: + { + return true; + } + case SKE_GHOST: + { + return true; + } + default: + { + return false; + } + } +} + +sokoDirection_t sokoRedirectDir(sokoDirection_t emitterDir, bool inverted) +{ + switch (emitterDir) + { + case SKD_UP: + { + return inverted ? SKD_LEFT : SKD_RIGHT; + } + case SKD_DOWN: + { + return inverted ? SKD_RIGHT : SKD_LEFT; + } + case SKD_RIGHT: + { + return inverted ? SKD_DOWN : SKD_UP; + } + case SKD_LEFT: + { + return inverted ? SKD_UP : SKD_DOWN; + } + default: + { + return SKD_NONE; + } + } +} + +int sokoBeamImpactRecursive(soko_abs_t* self, int emitter_x, int emitter_y, sokoDirection_t emitterDir, + sokoEntity_t* rootEmitter) +{ + sokoDirection_t dir = emitterDir; + sokoVec_t projVec = {0, 0}; + sokoVec_t emitVec = {emitter_x, emitter_y}; + switch (dir) + { + case SKD_DOWN: + { + projVec.y = 1; + break; + } + case SKD_UP: + { + projVec.y = -1; + break; + } + case SKD_LEFT: + { + projVec.x = -1; + break; + } + case SKD_RIGHT: + { + projVec.x = 1; + break; + } + default: + { + projVec.y = -1; + break; + // return base entity position + } + } + + // Iterate over tiles in ray to edge of level + sokoVec_t testPos = sokoAddCoord(emitVec, projVec); + int entityCount = self->currentLevel.entityCount; + // todo: make first pass pack a statically allocated array with only the entities in the path of the laser. + + int16_t possibleSquares = 0; + if (dir == SKD_RIGHT) // move these checks into the switch statement + { + possibleSquares = self->currentLevel.width - emitVec.x; // Up to and including far wall + } + if (dir == SKD_LEFT) + { + possibleSquares = emitVec.x + 1; + } + if (dir == SKD_UP) + { + possibleSquares = emitVec.y + 1; + } + if (dir == SKD_DOWN) + { + possibleSquares = self->currentLevel.height - emitVec.y; + } + + int tileCollFlag, entCollFlag, entCollInd; + tileCollFlag = entCollFlag = entCollInd = 0; + + bool retVal; + // printf("emitVec(%d,%d)",emitVec.x,emitVec.y); + // printf("projVec:(%d,%d) possibleSquares:%d ",projVec.x,projVec.y,possibleSquares); + + for (int n = 0; n < possibleSquares; n++) + { + sokoTile_t posTile = absSokoGetTile(self, testPos.x, testPos.y); + // printf("|n:%d,posTile:(%d,%d):%d|",n,testPos.x,testPos.y,posTile); + if (sokoLaserTileCollision(posTile)) + { + tileCollFlag = 1; + break; + } + for (int m = 0; m < entityCount; m++) // iterate over tiles/entities to check for laser collision. First pass + // finds everything in the path of the + { + sokoEntity_t candidateEntity = self->currentLevel.entities[m]; + // printf("|m:%d;CE:(%d,%d)%d",m,candidateEntity.x,candidateEntity.y,candidateEntity.type); + if (candidateEntity.x == testPos.x && candidateEntity.y == testPos.y) + { + // printf(";POSMATCH;Coll:%d",entityCollision[candidateEntity.type]); + if (sokoLaserEntityCollision(candidateEntity.type)) + { + entCollFlag = 1; + entCollInd = m; + // printf("|"); + break; + } + } + // printf("|"); + } + sokoEntityProperties_t* entProps = &rootEmitter->properties; + if (tileCollFlag) + { + entProps->targetX[entProps->targetCount] = testPos.x; // Pack target properties with every impacted + // position. + entProps->targetY[entProps->targetCount] = testPos.y; + entProps->targetCount++; + } + if (entCollFlag) + { + sokoEntityType_t entType = self->currentLevel.entities[entCollInd].type; + + entProps->targetX[entProps->targetCount] = testPos.x; // Pack target properties with every impacted entity. + entProps->targetY[entProps->targetCount] + = testPos.y; // If there's a redirect, it will be added after this one. + entProps->targetCount++; + if (entType == SKE_LASER_90) + { + sokoDirection_t redirectDir + = sokoRedirectDir(emitterDir, self->currentLevel.entities[entCollInd].facing); // SKD_UP or SKD_DOWN + sokoBeamImpactRecursive(self, testPos.x, testPos.y, redirectDir, rootEmitter); + } + + break; + } + testPos = sokoAddCoord(testPos, projVec); + } + retVal = self->currentLevel.entities[entCollInd].properties.targetCount; + // printf("\n"); + // retVal.x = testPos.x; + // retVal.y = testPos.y; + // retVal.entityIndex = entCollInd; + // retVal.entityFlag = entCollFlag; + // printf("impactPoint:(%d,%d)\n",testPos.x,testPos.y); + return retVal; +} + +sokoCollision_t sokoBeamImpact(soko_abs_t* self, sokoEntity_t* emitter) +{ + sokoDirection_t dir = emitter->facing; + sokoVec_t projVec = {0, 0}; + sokoVec_t emitVec = {emitter->x, emitter->y}; + switch (dir) + { + case SKD_DOWN: + { + projVec.y = 1; + break; + } + case SKD_UP: + { + projVec.y = -1; + break; + } + case SKD_LEFT: + { + projVec.x = -1; + break; + } + case SKD_RIGHT: + { + projVec.x = 1; + break; + } + default: + { // return base entity position + } + } + + // Iterate over tiles in ray to edge of level + sokoVec_t testPos = sokoAddCoord(emitVec, projVec); + int entityCount = self->currentLevel.entityCount; + // todo: make first pass pack a statically allocated array with only the entities in the path of the laser. + + uint8_t tileCollision[] + = {0, 0, 1, 0, 0, 1, 1}; // There should be a pointer internal to the game state so this can vary with game mode + uint8_t entityCollision[] = {0, 0, 1, 1}; + + int16_t possibleSquares = 0; + if (dir == SKD_RIGHT) // move these checks into the switch statement + { + possibleSquares = self->currentLevel.width - emitVec.x; // Up to and including far wall + } + if (dir == SKD_LEFT) + { + possibleSquares = emitVec.x + 1; + } + if (dir == SKD_UP) + { + possibleSquares = emitVec.y + 1; + } + if (dir == SKD_DOWN) + { + possibleSquares = self->currentLevel.height - emitVec.y; + } + + // int tileCollFlag = 0; + int entCollFlag = 0; + int entCollInd = 0; + + sokoCollision_t retVal; + // printf("emitVec(%d,%d)",emitVec.x,emitVec.y); + // printf("projVec:(%d,%d) possibleSquares:%d ",projVec.x,projVec.y,possibleSquares); + + for (int n = 0; n < possibleSquares; n++) + { + sokoTile_t posTile = absSokoGetTile(self, testPos.x, testPos.y); + // printf("|n:%d,posTile:(%d,%d):%d|",n,testPos.x,testPos.y,posTile); + if (tileCollision[posTile]) + { + // tileCollFlag = 1; + break; + } + for (int m = 0; m < entityCount; m++) // iterate over tiles/entities to check for laser collision. First pass + // finds everything in the path of the + { + sokoEntity_t candidateEntity = self->currentLevel.entities[m]; + // printf("|m:%d;CE:(%d,%d)%d",m,candidateEntity.x,candidateEntity.y,candidateEntity.type); + if (candidateEntity.x == testPos.x && candidateEntity.y == testPos.y) + { + // printf(";POSMATCH;Coll:%d",entityCollision[candidateEntity.type]); + if (entityCollision[candidateEntity.type]) + { + entCollFlag = 1; + entCollInd = m; + // printf("|"); + break; + } + } + // printf("|"); + } + + if (entCollFlag) + { + break; + } + testPos = sokoAddCoord(testPos, projVec); + } + // printf("\n"); + retVal.x = testPos.x; + retVal.y = testPos.y; + retVal.entityIndex = entCollInd; + retVal.entityFlag = entCollFlag; + // printf("impactPoint:(%d,%d)\n",testPos.x,testPos.y); + return retVal; +} + +sokoVec_t sokoAddCoord(sokoVec_t op1, sokoVec_t op2) +{ + sokoVec_t retVal; + retVal.x = op1.x + op2.x; + retVal.y = op1.y + op2.y; + return retVal; +} + +// Euler Game Modes +void eulerSokoTryPlayerMovement(soko_abs_t* self) +{ + if (self->input.playerInputDeltaX == 0 && self->input.playerInputDeltaY == 0) + { + return; + } + + uint16_t x = self->soko_player->x; + uint16_t y = self->soko_player->y; + bool moved = self->sokoTryMoveEntityInDirectionFunc(self, self->soko_player, self->input.playerInputDeltaX, + self->input.playerInputDeltaY, 0); + + if (moved) + { + // Paint Floor + + // previous + if (self->currentLevel.tiles[x][y] == SKT_FLOOR) + { + sokoAddTileMoveToHistory(self, x, y, SKT_FLOOR); + self->currentLevel.tiles[x][y] = SKT_FLOOR_WALKED; + } + if (self->currentLevel.tiles[self->soko_player->x][self->soko_player->y] == SKT_FLOOR) + { + sokoAddTileMoveToHistory(self, self->soko_player->x, self->soko_player->y, SKT_FLOOR); + self->currentLevel.tiles[self->soko_player->x][self->soko_player->y] = SKT_FLOOR_WALKED; + } + + // Try Sticky Blocks + // Loop through all entities is probably not really slower than sampling? We usually have <5 entities. + for (size_t i = 0; i < self->currentLevel.entityCount; i++) + { + if (self->currentLevel.entities[i].type == SKE_STICKY_CRATE) + { + if (self->currentLevel.entities[i].x == x && self->currentLevel.entities[i].y == y + 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + else if (self->currentLevel.entities[i].x == x && self->currentLevel.entities[i].y == y - 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + else if (self->currentLevel.entities[i].y == y && self->currentLevel.entities[i].x == x + 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + else if (self->currentLevel.entities[i].y == y && self->currentLevel.entities[i].x == x - 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + } + } + sokoHistoryTurnOver(self); + } +} + +bool eulerNoUnwalkedFloors(soko_abs_t* self) +{ + for (size_t x = 0; x < self->currentLevel.width; x++) + { + for (size_t y = 0; y < self->currentLevel.height; y++) + { + if (self->currentLevel.tiles[x][y] == SKT_FLOOR) + { + return false; + } + } + } + + return true; +} + +void overworldSokoGameLoop(soko_abs_t* self, int64_t elapsedUs) +{ + if (self->state == SKS_GAMEPLAY) + { + // logic + + // by saving this before we move, we lag by one position. The final movement onto a portal doesn't get saved, as + // this loopin't entered again then we return to the position we were at before the last loop. + self->overworld_playerX = self->soko_player->x; + self->overworld_playerY = self->soko_player->y; + + self->sokoTryPlayerMovementFunc(self); + + // victory status. stored separate from gamestate because of future gameplay ideas/remixes. + // todo: rename 'allCrates' to isVictory or such. + self->allCratesOnGoal = self->isVictoryConditionFunc(self); + if (self->allCratesOnGoal) + { + self->state = SKS_VICTORY; + + printf("Player at %d,%d\n", self->soko_player->x, self->soko_player->y); + victoryDanceTimer = 0; + } + // draw level + self->drawTilesFunc(self, &self->currentLevel); + } + else if (self->state == SKS_VICTORY) + { + self->drawTilesFunc(self, &self->currentLevel); + + // check for input for exit/next level. + uint8_t targetWorldIndex = 0; + for (int i = 0; i < self->portalCount; i++) + { + if (self->soko_player->x == self->portals[i].x && self->soko_player->y == self->portals[i].y) + { + targetWorldIndex = self->portals[i].index; + break; + } + } + + self->loadNewLevelIndex = targetWorldIndex; + self->loadNewLevelFlag = false; // load saved data. + self->screen = SOKO_LOADNEWLEVEL; + } + + // DEBUG PLACEHOLDER: + // Render the time to a string + char str[16] = {0}; + int16_t tWidth; + if (!self->allCratesOnGoal) + { + snprintf(str, sizeof(str) - 1, "sokoban"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } +} + +bool overworldPortalEntered(soko_abs_t* self) +{ + for (uint8_t i = 0; i < self->portalCount; i++) + { + if (self->soko_player->x == self->portals[i].x && self->soko_player->y == self->portals[i].y) + { + return true; + } + } + return false; +} + +void restartCurrentLevel(soko_abs_t* self) +{ + // assumed this is set already? + // self->loadNewLevelIndex = self->loadNewLevelIndex; + + // todo: what can we do about screen flash when restarting? + self->loadNewLevelFlag = true; + self->screen = SOKO_LOADNEWLEVEL; +} + +void exitToOverworld(soko_abs_t* soko) +{ + printf("Exit to Overworld\n"); + // save. todo: skip if victory. + if (soko->currentLevel.gameMode == SOKO_EULER) + { + // sokoSaveEulerTiles(soko); + } + // sokoSaveCurrentLevelEntities(soko); + + soko->loadNewLevelIndex = 0; + soko->loadNewLevelFlag = true; + // self->state = SKS_GAMEPLAY; + soko->screen = SOKO_LOADNEWLEVEL; +} diff --git a/main/modes/games/soko/soko_gamerules.h b/main/modes/games/soko/soko_gamerules.h new file mode 100644 index 000000000..4588d201a --- /dev/null +++ b/main/modes/games/soko/soko_gamerules.h @@ -0,0 +1,49 @@ +#ifndef SOKO_GAMERULES_H +#define SOKO_GAMERULES_H + +/// @brief call [entity][tile] to get a bool that is true if that entity can NOT walk (or get pushed onto) that tile. +// bool sokoEntityTileCollision[4][8]; + +sokoTile_t sokoGetTile(int, int); +void sokoConfigGamemode(soko_abs_t* gamestate, soko_var_t variant); + +// utility/shared functions. +void sharedGameLoop(soko_abs_t* self); +sokoDirection_t sokoDirectionFromDelta(int, int); + +// entity pushing. +void sokoTryPlayerMovement(void); +bool sokoTryMoveEntityInDirection(sokoEntity_t*, int, int, uint16_t); + +// classic and default +void absSokoGameLoop(soko_abs_t* self, int64_t elapsedUs); +void absSokoTryPlayerMovement(soko_abs_t* self); +bool absSokoTryMoveEntityInDirection(soko_abs_t* self, sokoEntity_t* entity, int dx, int dy, uint16_t push); +void absSokoDrawTiles(soko_abs_t* self, sokoLevel_t* level); +bool absSokoAllCratesOnGoal(soko_abs_t* self); +sokoTile_t absSokoGetTile(soko_abs_t* self, int x, int y); +bool allCratesOnGoal(void); + +// euler +void eulerSokoTryPlayerMovement(soko_abs_t* self); +bool eulerNoUnwalkedFloors(soko_abs_t* self); + +// lasers +sokoCollision_t sokoBeamImpact(soko_abs_t* self, sokoEntity_t* emitter); +int sokoBeamImpactRecursive(soko_abs_t* self, int emitter_x, int emitter_y, sokoDirection_t emitterDir, + sokoEntity_t* rootEmitter); +sokoDirection_t sokoRedirectDir(sokoDirection_t emitterDir, bool inverted); +bool sokoLaserEntityCollision(sokoEntityType_t testEntity); +bool sokoLaserTileCollision(sokoTile_t testTile); +void laserBounceSokoGameLoop(soko_abs_t* self, int64_t elapsedUs); +sokoVec_t sokoGridToPix(soko_abs_t* self, sokoVec_t grid); +void drawLaserFromEntity(soko_abs_t* self, sokoEntity_t* emitter); +sokoVec_t sokoAddCoord(sokoVec_t op1, sokoVec_t op2); + +// overworld +void overworldSokoGameLoop(soko_abs_t* self, int64_t elapsedUs); +bool overworldPortalEntered(soko_abs_t* self); +void restartCurrentLevel(soko_abs_t* self); +void exitToOverworld(soko_abs_t* self); + +#endif // SOKO_GAMERULES_H \ No newline at end of file diff --git a/main/modes/games/soko/soko_input.c b/main/modes/games/soko/soko_input.c new file mode 100644 index 000000000..ab8fa4127 --- /dev/null +++ b/main/modes/games/soko/soko_input.c @@ -0,0 +1,179 @@ +#include "soko_input.h" + +/** + * @brief Initialize Input. Does this for every puzzle start, to reset button state. + * Also where config like dastime is set. + * + * @param input + */ +void sokoInitInput(sokoGameplayInput_t* input) +{ + input->dasTime = 100000; + input->firstDASTime = 500000; + input->DASActive = false; + input->prevHoldingDir = SKD_NONE; + input->prevBtnState = 0; + input->playerInputDeltaX = 0; + input->playerInputDeltaY = 0; + input->restartLevel = false; + input->exitToOverworld = false; + input->undo = false; +} +/** + * @brief Input preprocessing turns btnstate into game-logic usable data. + * Input variables only set on press, as appropriate. + * Handles DAS, settings, etc. + * Called once a frame before game loop. + * + * @param input + */ +void sokoPreProcessInput(sokoGameplayInput_t* input, int64_t elapsedUs) +{ + uint16_t btn = input->btnState; + // reset output data. + input->playerInputDeltaY = 0; + input->playerInputDeltaX = 0; + + // Non directional buttons + if ((btn & PB_B) && !(input->prevBtnState & PB_B)) + { + input->restartLevel = true; + } + else + { + input->restartLevel = false; + } + + if ((btn & PB_A) && !(input->prevBtnState & PB_A)) + { + input->undo = true; + } + else + { + input->undo = false; + } + + if ((btn & PB_START) && !(input->prevBtnState & PB_START)) + { + input->exitToOverworld = true; + } + else + { + input->exitToOverworld = false; + } + + // update holding direction + if ((btn & PB_UP) && !(btn & 0b1110)) + { + input->holdingDir = SKD_UP; + } + else if ((btn & PB_DOWN) && !(btn & 0b1101)) + { + input->holdingDir = SKD_DOWN; + } + else if ((btn & PB_LEFT) && !(btn & 0b1011)) + { + input->holdingDir = SKD_LEFT; + } + else if ((btn & PB_RIGHT) && !(btn & 0b0111)) + { + input->holdingDir = SKD_RIGHT; + } + else + { + input->holdingDir = SKD_NONE; + input->DASActive = false; + input->timeHeldDirection = 0; // reset when buttons change or multiple buttons. + } + + // going from one button to another without letting go could cheese DAS. + if (input->holdingDir != input->prevHoldingDir) + { + input->DASActive = false; + input->timeHeldDirection = 0; + } + + // increment DAS time. + if (input->holdingDir != SKD_NONE) + { + input->timeHeldDirection += elapsedUs; + } + + // two cases when DAS gets triggered: initial and every one after the initial. + bool triggerDAS = false; + if (input->DASActive == false && input->timeHeldDirection > input->firstDASTime) + { + triggerDAS = true; + input->DASActive = true; + } + + if (input->DASActive == true && input->timeHeldDirection > input->dasTime) + { + triggerDAS = true; + } + + if (triggerDAS) + { + // reset timer + input->timeHeldDirection = 0; + + // trigger movement + // todo: in sokogame i had to write delta to direction. This is basically directionenum to delta, which could be + // extracted too. + switch (input->holdingDir) + { + case SKD_RIGHT: + { + input->playerInputDeltaX = 1; + break; + } + case SKD_LEFT: + { + input->playerInputDeltaX = -1; + break; + } + case SKD_UP: + { + input->playerInputDeltaY = -1; + break; + } + case SKD_DOWN: + { + input->playerInputDeltaY = 1; + break; + } + case SKD_NONE: + default: + { + break; + } + } + } + else + { // if !trigger DAS + + // holdingDir is ONLY holding one button. So we use normal buttonstate for taps so we can tap button two before + // releasing button one. + if (input->btnState & PB_UP && !(input->prevBtnState & PB_UP)) + { + input->playerInputDeltaY = -1; + } + else if (input->btnState & PB_DOWN && !(input->prevBtnState & PB_DOWN)) + { + input->playerInputDeltaY = 1; + } + else if (input->btnState & PB_LEFT && !(input->prevBtnState & PB_LEFT)) + { + input->playerInputDeltaX = -1; + } + else if (input->btnState & PB_RIGHT && !(input->prevBtnState & PB_RIGHT)) + { + input->playerInputDeltaX = 1; + } + + } // end !triggerDAS + + // do this last + input->prevBtnState = btn; + input->prevHoldingDir = input->holdingDir; +} diff --git a/main/modes/games/soko/soko_input.h b/main/modes/games/soko/soko_input.h new file mode 100644 index 000000000..0ed23478c --- /dev/null +++ b/main/modes/games/soko/soko_input.h @@ -0,0 +1,40 @@ +#include "swadge2024.h" + +// there is a way to set clever ints here such that we can super quickly convert to dx and dy with bit ops. I'll think +// it through eventually. +typedef enum +{ + SKD_UP, + SKD_DOWN, + SKD_RIGHT, + SKD_LEFT, + SKD_NONE +} sokoDirection_t; + +typedef struct +{ + // input input data. + uint16_t btnState; ///< The button state. Provided to input For PreProcess. + + // input meta data. Used by PreProcess. + uint16_t prevBtnState; ///< The button state from the previous frame. + sokoDirection_t holdingDir; ///< What direction we are holding down. + sokoDirection_t prevHoldingDir; ///< What direction we are holding down. + uint64_t timeHeldDirection; ///< The amount of time we have been holding a single button down. Used for DAS. + bool DASActive; ///< If DAS has begun. User may be holding before first DAS, this is false. After first, it becomes + ///< true. + uint64_t dasTime; ///< How many microseconds before DAS starts + uint64_t firstDASTime; ///< how many microseconds after DAS has started before the next DAS + + // input output data. ie: usable Gameplay data. + // todo: use Direction in input + int playerInputDeltaX; + int playerInputDeltaY; + bool undo; + bool restartLevel; + bool exitToOverworld; + +} sokoGameplayInput_t; + +void sokoInitInput(sokoGameplayInput_t*); +void sokoPreProcessInput(sokoGameplayInput_t*, int64_t); diff --git a/main/modes/games/soko/soko_save.c b/main/modes/games/soko/soko_save.c new file mode 100644 index 000000000..0df4d9c10 --- /dev/null +++ b/main/modes/games/soko/soko_save.c @@ -0,0 +1,680 @@ +#include "soko.h" +#include "soko_save.h" + +static void sokoLoadCurrentLevelEntities(soko_abs_t* soko); +static void sokoSetLevelSolvedState(soko_abs_t* soko, uint16_t levelIndex, bool solved); +static void sokoLoadBinTiles(soko_abs_t* soko, int byteCount); +static int sokoFindIndex(soko_abs_t* self, int targetIndex); +void sokoSaveEulerTiles(soko_abs_t* soko); +void sokoLoadEulerTiles(soko_abs_t* soko); +void sokoSaveCurrentLevelEntities(soko_abs_t* soko); + +/// @brief Called on 'resume' from the menu. +/// @param soko +void sokoLoadGameplay(soko_abs_t* soko, uint16_t levelIndex, bool loadNew) +{ + // save previous level if needed. + sokoSaveGameplay(soko); + + // load current level + int32_t data = 0; + readNvs32("sk_data", &data); + // bitshift, etc, as needed. + uint16_t lastSaved = (uint16_t)data; + + sokoLoadBinLevel(soko, levelIndex); + if (levelIndex == lastSaved && !loadNew) + { + printf("Load Saved Data for level %i\n", lastSaved); + // current level entity positions + sokoLoadCurrentLevelEntities(soko); + + if (soko->currentLevel.gameMode == SOKO_EULER) + { + sokoLoadEulerTiles(soko); + } + } +} + +void sokoSaveGameplay(soko_abs_t* soko) +{ + printf("Save Gameplay\n"); + + // save current level + if (soko->currentLevelIndex == 0) + { + // overworld gets saved separately. + return; + } + int current = soko->currentLevelIndex; + // current level entity positions + uint32_t data = current; + // what other data gets encoded? we can also save the sk_tiles count. + writeNvs32("sk_data", data); + + sokoSaveCurrentLevelEntities(soko); + + if (soko->currentLevel.gameMode == SOKO_EULER) + { + sokoSaveEulerTiles(soko); + } +} + +void sokoLoadLevelSolvedState(soko_abs_t* soko) +{ + // todo: automatically split for >32, >64 levels using 2 loops. + + int32_t lvs = 0; + readNvs32("sklv1", &lvs); + // i<32... + for (size_t i = 0; i < SOKO_LEVEL_COUNT; i++) + { + soko->levelSolved[i] = (1 & lvs >> i) == 1; + } + // now the next 32 bytes! + // readNvs32("sklv2",&lvs); + // for (size_t i = 32; i < SOKO_LEVEL_COUNT || i < 64; i++) + // { + // soko->levelSolved[i] = (1 & lvs>>i) == 1; + // } + + // etc. Probably won't bother cleaning it into nested loop until over 32*4 levels... + // so .. never? +} + +void sokoSetLevelSolvedState(soko_abs_t* soko, uint16_t levelIndex, bool solved) +{ + printf("save level solved status %d\n", levelIndex); + // todo: changes a single levels bool in the sokoSolved array, + soko->levelSolved[levelIndex] = true; + + int section = levelIndex / 32; + int index = levelIndex; + int32_t lvs = 0; + + if (section == 0) + { + readNvs32("sklv1", &lvs); + } + else if (section == 1) + { + readNvs32("sklv2", &lvs); + index -= 32; + } // else, 64, + + // write the bit. + if (solved) + { + // set bit + lvs = lvs | (1 << index); + } + else + { + // clear bit + lvs = lvs & ~(1 << index); + } + + // write the bit out to data. + if (section == 0) + { + writeNvs32("sklv1", lvs); + } + else if (section == 1) + { + writeNvs32("sklv2", lvs); + } +} + +void sokoSolveCurrentLevel(soko_abs_t* soko) +{ + if (soko->currentLevelIndex == 0) + { + // overworld level. + return; + } + else + { + sokoSetLevelSolvedState(soko, soko->currentLevelIndex, true); + } +} + +// Saving Progress +// soko->overworldX +// soko->overworldY +// current level? or just stick on overworld? + +// current level progress (all entitity positions/data, entities array. non-entities comes from file.) +// euler encoding? (do like picross level?) + +void sokoSaveCurrentLevelEntities(soko_abs_t* soko) +{ + // todo: the overworld will have >max entities... and they never need to be serialized... + // so maybe just make a separate array for portals that is entities of size maxLevelCount... + // and then treat it completely separately in the game loops. + + // sort of feels like we should do something similar to the blob packing of the levels. + // Then write a function that's like "get entity from bytes" where we pass it an array-slice of bytes, and get back + // some entity object. except, we have to include x,y data here... so it would be different... + + // instead, we can have our own binary encoding. Some entities never move, and can be loaded from the disk. + // after they are loaded, we save "Index, X, Y, Extra" binary sets, and replace the values for the entities at the + // index position. I think it will work such that, for a level, entities will always have the same index position in + // the entities array... this is ONLY true if we never actually 'destroy' or 'CREATE' entities, but just flip some + // 'dead' flag. + + // if each entity is 4 bytes, then we can save (adjust) all entities as a single blob, always, since it's a + // pre-allocated array. + char* entities = calloc(soko->currentLevel.entityCount * 4, sizeof(char)); + + for (int i = 0; i < soko->currentLevel.entityCount; i++) + { + entities[i * 4] = i; + // todo: facing... + // sokoentityproperties? will these ever change at runtime? there is an "hp" that was made for laserbounce... + // do we need the propflag? + entities[i * 4 + 1] = soko->currentLevel.entities[i].x; + entities[i * 4 + 2] = soko->currentLevel.entities[i].y; + entities[i * 4 + 3] = soko->currentLevel.entities[i].facing; + } + size_t size = sizeof(char) * (soko->currentLevel.entityCount) * 4; + writeNvsBlob("sk_ents", entities, size); + free(entities); +} +// todo: there is no clean place to return to the main menu right now, so gotta write that function/flow so this can get +// called. + +/// @brief After loading the level into currentLevel, this updates the entity array with saved +/// @param soko +void sokoLoadCurrentLevelEntities(soko_abs_t* soko) +{ + printf("loading current level entities.\n"); + + char* entities = calloc(soko->currentLevel.entityCount * 4, sizeof(char)); + size_t size = sizeof(char) * (soko->currentLevel.entityCount * 4); + readNvsBlob("sk_ents", entities, &size); + + for (int i = 0; i < soko->currentLevel.entityCount; i++) + { + // todo: wait, if all entities are the same length, we don't actually need to save the index... + soko->currentLevel.entities[i].x = entities[i * 4 + 1]; + soko->currentLevel.entities[i].y = entities[i * 4 + 2]; + soko->currentLevel.entities[i].facing = entities[i * 4 + 3]; + } + free(entities); +} + +void sokoSaveEulerTiles(soko_abs_t* soko) +{ + printf("encoding euler tiles.\n"); + + sokoTile_t prevTile = SKT_FLOOR; + int w = soko->currentLevel.width; + uint16_t i = 0; + char* blops = (char*)calloc(255, sizeof(char)); + for (uint16_t y = 0; y < soko->currentLevel.height; y++) + { + for (uint16_t x = 0; x < w; x++) + { + sokoTile_t t = soko->currentLevel.tiles[x][y]; + if (t == SKT_FLOOR || t == SKT_FLOOR_WALKED) + { + if (t == prevTile) + { + blops[i] = blops[i] + 1; + } + else + { + prevTile = t; + i++; + blops[i] = blops[i] + 1; + if (i > 255) + { + printf("ERROR This level is too big to save for euler???\n"); + break; + } + } + } + } + } + i++; + writeNvsBlob("sk_e_t_c", &i, sizeof(uint16_t)); + writeNvsBlob("sk_e_ts", blops, sizeof(char) * i); + + free(blops); +} + +void sokoLoadEulerTiles(soko_abs_t* soko) +{ + printf("Load Euler Tiles\n"); + sokoTile_t runningTile = SKT_FLOOR; + uint16_t w = soko->currentLevel.width; + uint16_t total = 0; + // i don't think i need to calloc before reading the blob? + + size_t size = sizeof(uint16_t); + readNvsBlob("sk_e_t_c", &total, &size); + + char* blops = calloc(total, sizeof(char)); + size = sizeof(char) * total; + readNvsBlob("sk_e_ts", blops, &size); + + uint16_t bi = 0; + if (blops[0] == 0) + { + // pre-flip, basically... + runningTile = SKT_FLOOR_WALKED; + bi = 1; // doesn't mess up our count, because 0 counts for 0 tiles. + } + for (size_t y = 0; y < soko->currentLevel.height; y++) + { + for (size_t x = 0; x < w; x++) + { + sokoTile_t t = soko->currentLevel.tiles[x][y]; + if (t == SKT_FLOOR || t == SKT_FLOOR_WALKED) + { + soko->currentLevel.tiles[x][y] = runningTile; + blops[bi] = blops[bi] - 1; + + if (blops[bi] == 0) + { + bi++; + // flop + if (runningTile == SKT_FLOOR) + { + runningTile = SKT_FLOOR_WALKED; + } + else if (runningTile == SKT_FLOOR_WALKED) + { + runningTile = SKT_FLOOR; + } + } + } + } + } + free(blops); +} + +// Level loading +void sokoLoadBinLevel(soko_abs_t* soko, uint16_t levelIndex) +{ + printf("load bin level %d, %s\n", levelIndex, soko->levelNames[levelIndex]); + soko->state = SKS_INIT; + size_t fileSize; + if (soko->levelBinaryData) + { + free(soko->levelBinaryData); + } + soko->levelBinaryData + = cnfsReadFile(soko->levelNames[levelIndex], &fileSize, true); // Heap CAPS malloc/calloc allocation for SPI RAM + + // The pointer returned by spiffsReadFile can be freed with free() with no additional steps. + soko->currentLevel.width = soko->levelBinaryData[0]; // first two bytes of a level's data always describe the + // bounding width and height of the tilemap. + soko->currentLevel.height = soko->levelBinaryData[1]; // Max Theoretical Level Bounding Box Size is 255x255, though + // you'll likely run into issues with entities first. + soko->currentLevel.gameMode = (soko_var_t)soko->levelBinaryData[2]; + // for(int i = 0; i < fileSize; i++) + //{ + // printf("%d, ",soko->levelBinaryData[i]); + // } + // printf("\n"); + soko->currentLevelIndex = levelIndex; + soko->currentLevel.levelScale = 16; + soko->camWidth = TFT_WIDTH / (soko->currentLevel.levelScale); + soko->camHeight = TFT_HEIGHT / (soko->currentLevel.levelScale); + soko->camEnabled = soko->camWidth < soko->currentLevel.width || soko->camHeight < soko->currentLevel.height; + soko->camPadExtentX = soko->camWidth * 0.6 * 0.5; + soko->camPadExtentY = soko->camHeight * 0.6 * 0.5; + + // incremented by loadBinTiles. + soko->currentLevel.entityCount = 0; + soko->portalCount = 0; + + sokoLoadBinTiles(soko, (int)fileSize); + + if (levelIndex == 0) + { + if (soko->overworld_playerX == 0 && soko->overworld_playerY == 0) + { + printf("resetting player position from loaded entity\n"); + soko->overworld_playerX = soko->soko_player->x; + soko->overworld_playerY = soko->soko_player->y; + } + } + + printf("Loaded level w: %i, h %i, entities: %i\n", soko->currentLevel.width, soko->currentLevel.height, + soko->currentLevel.entityCount); +} + +// todo: rename self to soko +void sokoLoadBinTiles(soko_abs_t* self, int byteCount) +{ + const int HEADER_BYTE_OFFSET = 3; // width,height,mode + // int totalTiles = self->currentLevel.width * self->currentLevel.height; + int tileIndex = 0; + int prevTileType = 0; + self->currentLevel.entityCount = 0; + self->goalCount = 0; + + for (int i = HEADER_BYTE_OFFSET; i < byteCount; i++) + { + // Objects in level data should be of the form + // SKB_OBJSTART, SKB_[Object Type], [Data Bytes] , SKB_OBJEND + if (self->levelBinaryData[i] == SKB_OBJSTART) + { + int objX = (tileIndex - 1) % (self->currentLevel.width); // Look at the previous + int objY = (tileIndex - 1) / (self->currentLevel.width); + uint8_t flagByte, direction; + bool players, crates, sticky, trail, inverted; + int hp; //, targetX, targetY; + // printf("reading object byte after start: %i,%i:%i\n",objX,objY,self->levelBinaryData[i+1]); + + switch (self->levelBinaryData[i + 1]) // On creating entities, index should be advanced to the SKB_OBJEND + // byte so the post-increment moves to the next tile. + { + case SKB_COMPRESS: + { + i += 2; + // we should not have dound this, we are inside of an object! + break; // Not yet implemented + } + case SKB_PLAYER: + { // moved gamemode to bit 3 of level data in header. + // self->currentLevel.gameMode = self->levelBinaryData[i + 2]; + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_PLAYER; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->soko_player = &self->currentLevel.entities[self->currentLevel.playerIndex]; + self->currentLevel.playerIndex = self->currentLevel.entityCount; + self->currentLevel.entityCount += 1; + i += 2; // start, player, end. + break; + } + case SKB_CRATE: + { + flagByte = self->levelBinaryData[i + 2]; + sticky = !!(flagByte & (0x1 << 0)); + trail = !!(flagByte & (0x1 << 1)); + if (sticky && trail) + { + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_STICKY_TRAIL_CRATE; + } + else if (sticky) + { + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_STICKY_CRATE; + } + else + { + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_CRATE; + } + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.sticky = sticky; + self->currentLevel.entities[self->currentLevel.entityCount].properties.trail = trail; + self->currentLevel.entityCount += 1; + i += 3; + break; + } + case SKB_WARPINTERNAL: //[type][flags][hp][destx][desty] + { + flagByte = self->levelBinaryData[i + 2]; + crates = !!(flagByte & (0x1 << 0)); + hp = self->levelBinaryData[i + 3]; + // targetX = self->levelBinaryData[i + 4]; + // targetY = self->levelBinaryData[i + 5]; + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_WARP; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.crates = crates; + self->currentLevel.entities[self->currentLevel.entityCount].properties.hp = hp; + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetX + = malloc(sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetY + = malloc(sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetCount = 1; + self->currentLevel.entityCount += 1; + i += 6; + break; + } + case SKB_WARPINTERNALEXIT: + { + flagByte = self->levelBinaryData[i + 2]; + + i += 2; // No data or properties in this object. + break; // Can be used later on for verifying valid warps from save files. + } + case SKB_WARPEXTERNAL: //[typep][flags][index] + { // todo implement extraction of index value and which values should be used for auto-indexed portals + self->currentLevel.tiles[objX][objY] = SKT_PORTAL; + flagByte = self->levelBinaryData[i + 2]; // destination + self->portals[self->portalCount].index + = sokoFindIndex(self, flagByte); // For basic test, 1 indexed with levels, but multi-room + // overworld needs more sophistication to keep indices correct. + self->portals[self->portalCount].x = objX; + self->portals[self->portalCount].y = objY; + self->portalCount += 1; + i += 3; + break; + } + case SKB_BUTTON: //[type][flag][numTargets][targetx][targety]... + { + flagByte = self->levelBinaryData[i + 2]; + crates = !!(flagByte & (0x1 << 0)); + players = !!(flagByte & (0x1 << 1)); + inverted = !!(flagByte & (0x1 << 2)); + sticky = !!(flagByte & (0x1 << 3)); + hp = self->levelBinaryData[i + 3]; + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_BUTTON; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetX + = malloc(sizeof(uint8_t) * hp); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetY + = malloc(sizeof(uint8_t) * hp); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetCount = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.crates = crates; + self->currentLevel.entities[self->currentLevel.entityCount].properties.players = players; + self->currentLevel.entities[self->currentLevel.entityCount].properties.inverted = inverted; + self->currentLevel.entities[self->currentLevel.entityCount].properties.sticky = sticky; + for (int j = 0; j < hp; j++) + { + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetX[j] + = self->levelBinaryData[3 + 2 * j + 1]; + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetY[j] + = self->levelBinaryData[3 + 2 * (j + 1)]; + } + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetCount = hp; + self->currentLevel.entities[self->currentLevel.entityCount].properties.players = players; + self->currentLevel.entities[self->currentLevel.entityCount].properties.crates = crates; + self->currentLevel.entities[self->currentLevel.entityCount].properties.inverted = inverted; + self->currentLevel.entities[self->currentLevel.entityCount].properties.sticky = sticky; + self->currentLevel.entityCount += 1; + i += (4 + 2 * hp); + break; + } + case SKB_LASEREMITTER: //[type][flag] + { + flagByte = self->levelBinaryData[i + 2]; + direction = (flagByte & (0x3 << 6)) >> 6; // flagbyte stores direction in 0bDD0000P0 Where D is + // direction bits and P is player push + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_EMIT_UP; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].facing = direction; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.players = players; + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetX + = calloc(SOKO_MAX_ENTITY_COUNT, sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetX + = calloc(SOKO_MAX_ENTITY_COUNT, sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties.targetCount = 0; + self->currentLevel.entityCount += 1; + i += 3; + break; + } + case SKB_LASERRECEIVEROMNI: + { + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_RECEIVE_OMNI; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entityCount += 1; + i += 2; + break; + } + case SKB_LASERRECEIVER: + { + flagByte = self->levelBinaryData[i + 2]; + direction = (flagByte & (0x3 << 6)) >> 6; // flagbyte stores direction in 0bDD0000P0 Where D is + // direction bits and P is player push + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_RECEIVE; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].facing = direction; + self->currentLevel.entityCount += 1; + i += 3; + break; + } + case SKB_LASER90ROTATE: + { + flagByte = self->levelBinaryData[i + 2]; + direction = !!(flagByte & (0x1 < 0)); + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_90; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].facing = direction; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.players = players; + self->currentLevel.entityCount += 1; + i += 3; + break; + } + case SKB_GHOSTBLOCK: + { + flagByte = self->levelBinaryData[i + 2]; + inverted = !!(flagByte & (0x1 < 2)); + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_GHOST; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties.players = players; + self->currentLevel.entityCount += 1; + i += 3; + break; + } + case SKB_OBJEND: + { + i += 1; + break; + } + default: // Make the best of an undefined object type and try to skip it by finding its end byte + { + bool objEndFound = false; + int undefinedObjectLength = 0; + while (!objEndFound) + { + undefinedObjectLength += 1; + if (self->levelBinaryData[i + undefinedObjectLength] == SKB_OBJEND) + { + objEndFound = true; + } + } + i += undefinedObjectLength; // Move to the completion byte of an undefined object type and hope it + // doesn't have two end bytes. + break; + } + } + } + else + { + int tileX = (tileIndex) % (self->currentLevel.width); + int tileY = (tileIndex) / (self->currentLevel.width); + // self->currentLevel.tiles[tileX][tileY] = self->levelBinaryData[i]; + int tileType = 0; + switch (self->levelBinaryData[i]) // This is a bit easier to read than two arrays + { + case SKB_EMPTY: + { + tileType = SKT_EMPTY; + break; + } + case SKB_WALL: + { + tileType = SKT_WALL; + break; + } + case SKB_FLOOR: + { + tileType = SKT_FLOOR; + break; + } + case SKB_NO_WALK: + { + tileType = SKT_FLOOR; //@todo Add No-Walk floors that can only accept crates or pass lasers + break; + } + case SKB_GOAL: + { + tileType = SKT_GOAL; + self->goals[self->goalCount].x = tileX; + self->goals[self->goalCount].y = tileY; + self->goalCount++; + break; + } + case SKB_COMPRESS: + { + tileType = prevTileType; + // decrement the next one + if (self->levelBinaryData[i + 1] > 1) + { + self->levelBinaryData[i + 1] -= 1; + i -= 1; // unloop the loop! deloop! Cursed loops! + } + else + { + i += 1; + } + + break; + } + default: + { + tileType = SKT_EMPTY; + break; + } + } + self->currentLevel.tiles[tileX][tileY] = tileType; + prevTileType = tileType; + // printf("BinData@%d: %d Tile: %d at (%d,%d) + // index:%d\n",i,self->levelBinaryData[i],tileType,tileX,tileY,tileIndex); + tileIndex++; + } + } +} + +static int sokoFindIndex(soko_abs_t* self, int targetIndex) +{ + // Filenames are formatted like '1:sk_level.bin:' + int retVal = -1; + for (int i = 0; i < SOKO_LEVEL_COUNT; i++) + { + if (self->levelIndices[i] == targetIndex) + { + retVal = i; + break; + } + } + return retVal; +} diff --git a/main/modes/games/soko/soko_save.h b/main/modes/games/soko/soko_save.h new file mode 100644 index 000000000..f24d0d72b --- /dev/null +++ b/main/modes/games/soko/soko_save.h @@ -0,0 +1,5 @@ +void sokoLoadGameplay(soko_abs_t* soko, uint16_t levelIndex, bool loadNew); +void sokoSaveGameplay(soko_abs_t* soko); +void sokoLoadLevelSolvedState(soko_abs_t* soko); +void sokoSolveCurrentLevel(soko_abs_t* soko); +void sokoLoadBinLevel(soko_abs_t* soko, uint16_t levelIndex); diff --git a/main/modes/games/soko/soko_undo.c b/main/modes/games/soko/soko_undo.c new file mode 100644 index 000000000..2bb262874 --- /dev/null +++ b/main/modes/games/soko/soko_undo.c @@ -0,0 +1,124 @@ +#include "soko_undo.h" + +void sokoInitHistory(soko_abs_t* soko) +{ + soko->historyCurrent = 0; + soko->historyBufferTail = 0; + soko->history[0].moveID = 0; + soko->historyNewMove = true; +} + +void sokoHistoryTurnOver(soko_abs_t* soko) +{ + soko->historyNewMove = true; +} + +void sokoAddTileMoveToHistory(soko_abs_t* soko, uint16_t tileX, uint16_t tileY, sokoTile_t oldTileType) +{ + uint16_t moveID = soko->history[soko->historyCurrent].moveID; + if (soko->historyNewMove) + { + moveID += 1; + soko->historyNewMove = false; + } + // i think this should basically always be false for tiles. + + soko->historyCurrent++; + if (soko->historyCurrent >= SOKO_UNDO_BUFFER_SIZE) + { + soko->historyCurrent = 0; + } + if (soko->historyCurrent == soko->historyBufferTail) + { + soko->historyBufferTail++; + if (soko->historyBufferTail >= SOKO_UNDO_BUFFER_SIZE) + { + soko->historyBufferTail = 0; + } + } + sokoUndoMove_t* move = &soko->history[soko->historyCurrent]; + + move->moveID = moveID; + move->isEntity = false; + move->tile = oldTileType; + move->x = tileX; + move->y = tileY; +} + +void sokoAddEntityMoveToHistory(soko_abs_t* soko, sokoEntity_t* entity, uint16_t oldX, uint16_t oldY, + sokoDirection_t oldFacing) +{ + uint16_t moveID = soko->history[soko->historyCurrent].moveID; + // should basically only be true for the player... + if (soko->historyNewMove) + { + moveID += 1; + soko->historyNewMove = false; + // printf("first invalid move (oldest) %i\n",soko->historyOldestValidMoveID); + } + + soko->historyCurrent++; + if (soko->historyCurrent >= SOKO_UNDO_BUFFER_SIZE) + { + soko->historyCurrent = 0; + } + if (soko->historyCurrent == soko->historyBufferTail) + { + soko->historyBufferTail++; + if (soko->historyBufferTail >= SOKO_UNDO_BUFFER_SIZE) + { + soko->historyBufferTail = 0; + } + } + + sokoUndoMove_t* move = &soko->history[soko->historyCurrent]; + move->moveID = moveID; + move->isEntity = true; + move->entity = entity; + move->x = oldX; + move->y = oldY; + move->facing = oldFacing; +} + +void sokoUndo(soko_abs_t* soko) +{ + // HistoryCurrent points to the last added move. + uint16_t undoMoveId = soko->history[soko->historyCurrent].moveID; + + // nope! can't undo! out of history. + if (undoMoveId == soko->history[soko->historyBufferTail].moveID) + { + return; + } + + while (soko->history[soko->historyCurrent].moveID == undoMoveId) + { + // history can partially overwrite the oldest move in the buffer. + // we can fix that by uh... storing the last move we overwrote in a 'invalidUndo' and stopping undoes of it? + sokoUndoMove_t* m = &soko->history[soko->historyCurrent]; + // undo this move. + if (m->isEntity) + { + // undo the entity + m->entity->x = m->x; + m->entity->y = m->y; + m->entity->facing = m->facing; + // todo: facing + } + else + { + // undo the tile + soko->currentLevel.tiles[m->x][m->y] = m->tile; + } + // ring buffer + if (soko->historyCurrent > 0) + { + soko->historyCurrent--; + } + else + { + soko->historyCurrent = SOKO_UNDO_BUFFER_SIZE - 1; + } + } + soko->undoCount++; +} diff --git a/main/modes/games/soko/soko_undo.h b/main/modes/games/soko/soko_undo.h new file mode 100644 index 000000000..0fc70572c --- /dev/null +++ b/main/modes/games/soko/soko_undo.h @@ -0,0 +1,17 @@ +#ifndef SOKO_UNDO_H + #define SOKO_UNDO_H + + #include "swadge2024.h" + #include "soko.h" + +// if isEntity is true, then x and y are the position to return the entity at entityindex to, rest is ignored. +// if isentity is false, then set the tile at position x,y to tile. rest is ignored. + +#endif + +void sokoHistoryTurnOver(soko_abs_t* soko); +void sokoAddTileMoveToHistory(soko_abs_t* soko, uint16_t tileX, uint16_t tileY, sokoTile_t oldTileType); +void sokoAddEntityMoveToHistory(soko_abs_t* soko, sokoEntity_t* entity, uint16_t oldX, uint16_t oldY, + sokoDirection_t oldFacing); +void sokoUndo(soko_abs_t* soko); +void sokoInitHistory(soko_abs_t* soko); \ No newline at end of file diff --git a/main/modes/system/mainMenu/mainMenu.c b/main/modes/system/mainMenu/mainMenu.c index ec9848bee..f0bd851f7 100644 --- a/main/modes/system/mainMenu/mainMenu.c +++ b/main/modes/system/mainMenu/mainMenu.c @@ -21,6 +21,7 @@ #include "mode_synth.h" #include "ultimateTTT.h" #include "pango.h" +#include "soko.h" #include "touchTest.h" #include "tunernome.h" #include "keebTest.h" @@ -157,6 +158,7 @@ static void mainMenuEnterMode(void) addSingleItemToMenu(mainMenu->menu, pangoMode.modeName); addSingleItemToMenu(mainMenu->menu, t48Mode.modeName); addSingleItemToMenu(mainMenu->menu, bigbugMode.modeName); + addSingleItemToMenu(mainMenu->menu, sokoMode.modeName); mainMenu->menu = endSubMenu(mainMenu->menu); mainMenu->menu = startSubMenu(mainMenu->menu, "Music"); @@ -362,6 +364,10 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) { switchToSwadgeMode(&bigbugMode); } + else if (label == sokoMode.modeName) + { + switchToSwadgeMode(&sokoMode); + } else if (label == tttMode.modeName) { switchToSwadgeMode(&tttMode); diff --git a/makefile b/makefile index dbaf6eb27..6a90b21e7 100644 --- a/makefile +++ b/makefile @@ -334,10 +334,16 @@ $(EXECUTABLE): $(CNFS_FILE) $(OBJECTS) # To create the c file with assets, run these tools $(CNFS_FILE): +# Sokoban .tmx to bin preprocessor + python ./tools/soko/soko_tmx_preprocessor.py ./assets/soko/ ./assets_image/ + $(MAKE) -C ./tools/assets_preprocessor/ ./tools/assets_preprocessor/assets_preprocessor -i ./assets/ -o ./assets_image/ $(MAKE) -C ./tools/cnfs/ ./tools/cnfs/cnfs_gen assets_image/ main/utils/cnfs_image.c main/utils/cnfs_image.h + + + bundle: SwadgeEmulator.app diff --git a/tools/soko/plugin/sokoban_tiled_importer.js b/tools/soko/plugin/sokoban_tiled_importer.js new file mode 100644 index 000000000..c9aece315 --- /dev/null +++ b/tools/soko/plugin/sokoban_tiled_importer.js @@ -0,0 +1,21 @@ +//import "classic" sokoban text levels via tiled. +//http://www.sokobano.de/wiki/index.php?title=Level_format +//these levels are plaintext, and use the following scheme: +// # for walls +// @ for the player. + for player on goal +// . for the goal +// $ for a box, * for box on goal +// space for floor. + +//while our game treats empty and wall the same, proper sokoban levels must be enclosed by a wall + +// local d = Dialog("Paste Text as level") +// :label{id="lab1",label="",text="Import tiles."} +// :text{id="text1"} +// :label{id="lab3", label="",text="Max supported tilemap size: 255x255"} +// :separator{} +// :button{id="ok",text="&OK",focus=true} +// :button{text="&Cancel" } +// :show() + +tiled.register \ No newline at end of file diff --git a/tools/soko/plugin/sokobon_binary_conversion_script.lua b/tools/soko/plugin/sokobon_binary_conversion_script.lua new file mode 100644 index 000000000..b6e69191a --- /dev/null +++ b/tools/soko/plugin/sokobon_binary_conversion_script.lua @@ -0,0 +1,91 @@ +-- Script to export tilemap data as a binary file. +-- Original script by Zeltrix (https://pastebin.com/mQGiKAgR) +-- Export to binary by JVeg199X +-- Note: This script only works with tilemaps of 255x255 tiles or less + +-- DO NOT USE. WIP for original binary +-- Check .asp file and .config file + +if TilesetMode == nil then return app.alert "Use Aseprite 1.3" end +local spr = app.activeSprite + +if not spr then return end + +-- TODO Add Multi-File Selection for multiple levels and config files + +local d = Dialog("Export Tilemap as .bin File") +d:label{id="lab1",label="",text="Export Tilemap as .bin File for your own GameEngine"} + :file{id = "path", label="Export Path", filename="",open=false,filetypes={"bin"}, save=true, focus=true} + :label{id="lab3", label="",text="Max supported tilemap size: 255x255"} + :separator{} + :label{id="lab2", label="",text="In the last row of the tilemap-layer there has to be at least one Tile \"colored\" to fully export the whole Tilemap"} + :button{id="ok",text="&OK",focus=true} + :button{text="&Cancel" } + :show() + + + +--Initialize warp data array +local warps = {} +for i=0, 15 do + warps[i] = {} + warps[i][0] = 0; + warps[i][1] = 0; +end + +local data = d.data +if not data.ok then return end + local lay = app.activeLayer + if(#data.path<=0)then app.alert("No path selected") end + if not lay.isTilemap then return app.alert("Layer is not tilemap") end + pc = app.pixelColor + mapFile = io.open(data.path,"w") + + for _,c in ipairs(lay.cels) do + local img = c.image + + --The first two bytes contain the width and height of the tilemap in tiles + mapFile:write(string.char(img.width)) + mapFile:write(string.char(img.height)) + + --The next section of bytes is the tilemap itself + for p in img:pixels() do + if(p ~= nil) then + local tileId = p() + + --if(tileId == 130) then + -- local d2 = Dialog(tileId) + -- d2:show() + --end + + if(tileId > 0 and tileId < 17) then + --warp tiles + + tileBelowCurrentTile = img:getPixel(p.x, p.y+1) + if(tileBelowCurrentTile == 34 or tileBelowCurrentTile == 64 or tileBelowCurrentTile == 158) then + --if tile below warp tile is brick block or container or checkpoint, write it like normal + mapFile:write(string.char(tileId)) + else + --otherwise store it in warps array and don't write it into the file just yet + warps[tileId-1][0] = p.x + warps[tileId-1][1] = p.y + mapFile:write(string.char(0)) + end + + else + --every other tile + mapFile:write(string.char(tileId)) + end + + end + end + + --The last 32 bytes are warp x and y locations + for i=0, 15 do + mapFile:write(string.char(warps[i][0])) + mapFile:write(string.char(warps[i][1])) + end + end + + mapFile:close() + \ No newline at end of file diff --git a/tools/soko/soko_tmx_preprocessor.py b/tools/soko/soko_tmx_preprocessor.py new file mode 100644 index 000000000..19e45e293 --- /dev/null +++ b/tools/soko/soko_tmx_preprocessor.py @@ -0,0 +1,68 @@ +import sys +import os +from tmx_to_binary import convertTMX +count = 0 +total = 0 +raw_total = 0 +comp_total = 0 +def main(): + print("Starting soko tmx conversion") + + inputdir = sys.argv[1] + # check if output is real directory and create it if it does not exist. + outputdir = sys.argv[2] + if not os.path.exists(outputdir): + os.makedirs(outputdir) + + if not os.path.exists(outputdir): + print("oh no! input directory for soko tmx preprocessor doesn't exist!") + return + + + # todo: automatically check and move SK_LEVEL_LIST.txt, it doesn't update automatically. + + # todo: ensure output ends in a trailing slash. + convertDir(inputdir,outputdir) + print("Completed soko tmx converstion. "+str(count)+ "/"+str(total)+" tmx files converted. "+str(comp_total)+"/"+str(raw_total)+" - "+str(raw_total-comp_total)+" (of converted) bytes saved with compression.") + + +def convertDir(dir,output): + global count,total, raw_total, comp_total + # todo: check file modification dates. + # lol no + for file in os.scandir(dir): + if os.path.isfile(file): + name, ext = os.path.splitext(file) + if ext == '.tmx': + lastMod = os.path.getmtime(file) + fname = getNameFromPath(file) + out_file = output+fname+".bin" + if(os.path.isfile(out_file)): + lastOutMod = os.path.getmtime(out_file) + if(lastMod < lastOutMod): + #print("skipping "+fname) + total+=1 + continue + convertAndSave(file.path,output) + count+=1 + total+=1 + elif os.path.isdir(file): + convertDir(file,output) + +def convertAndSave(filepath,output): + global raw_total, comp_total + rawbytes, r,c = convertTMX(filepath) + raw_total += r + comp_total += c + fname = getNameFromPath(filepath) + outfile_file = output+fname+".bin" + with open(outfile_file,"wb") as binary_file: + binary_file.write(rawbytes) + +def getNameFromPath(p): + base = os.path.basename(p) + fp = base.split(".") + fname = fp[len(fp)-2] + return fname + +main() \ No newline at end of file diff --git a/tools/soko/templateTiledProject/README.md b/tools/soko/templateTiledProject/README.md new file mode 100644 index 000000000..291705d94 --- /dev/null +++ b/tools/soko/templateTiledProject/README.md @@ -0,0 +1,62 @@ +Open the project 'templateProject.tiled-project' using the most recent version of Tiled tilemap editor. + +## IF YOUR OBJECTS DO NOT SNAP TO THE CENTER OF THE GRID TILES, GO TO EDIT>PREFERENCE>FINE GRID DIVISIONS AND SET IT TO 2. + +### All tiles should go in the 'tiles' tilemap layer. Use the 'tilesheet' tileset to place walls, floors, and goals. +### All entities should go in the 'entities' object layer. Use the 'objLayers' tileset to place objects with baked-in data. + +### Level List File +The game uses an overworld for level selection. In order to designate the level to be loaded, an index number should be provided. Please prefix your level binary 'sk_' and end it with '.bin'. The former prevents filename collisions and the latter is mandatory to be properly copied into system memory. The 'SK_LEVEL_LIST.txt' file should be edited to include the desired index and name of your level. The level list file is formatted as such: +``` +1:sk_overworld.bin: +7:sk_test1.bin: +8:sk_test2.bin: +9:sk_test3.bin: +2:sk_warehouse.bin: +``` +## Entities: + +### Player: + Be sure to set the 'gamemode' property. + Valid values are: + SOKO_OVERWORLD, + SOKO_CLASSIC, + SOKO_EULER, + SOKO_LASERBOUNCE + +### Crate: + The 'sticky' property indicates whether the crate will stick to a player's sprite. + The 'trail' property indicates whether a crate will leave its own trail in a SOKO_EULER puzzle. + +### Button: + The 'playerPress' property indicates whether a player can depress the button. + The 'cratePress' property indicates whether a crate can depress the button. + The 'invertAction' property inverts the button's effects on all of its target blocks. For instance, all non-inverted Ghost Blocks targeted by the Button will start intangible. + The 'stayDownOnPress' property indicates whether the button will remain depressed after its first press after resets once players or crates are removed. + To target a ghostblock, find the Object ID of the target in Tiled and populate the target#id property with that ID (start at target1id and count up). + Be sure to set the 'numTargets' property to the number of targeted blocks. + +### Ghost Block: + Target a Ghost Block with a Button. + The 'playerMove' property indicates whether a player can move the ghost block like a crate while in its tangible state. + The 'inverted' property indicates whether a Ghost block will start intangible (unless the button targeting it is intangible). + +### Internal Warp and Internal Warp Exit: + The 'hp' property indicates how many times a Warp can be entered. + The 'allow_crates' property indicates whether an Internal Warp may pass crates to their destination. Note that the destination will be blocked by a Crate on its destination. + To target another internal warp, find the Object ID of the target in Tiled and populate the 'target_id' field with that ID. + Warps may only target Internal Warp and Internal Warp Exit blocks. Internal Warp Exits have no function in gameplay and serve only as destination markers for Internal Warps. To make a 2-Way Portal, have two Internal Warps target one another's IDs. + +### External Warps: + External warps are used in the overworld for level selection. When the player steps on an External Warp, the level pointed to by the associated index (See Level List File) will be loaded. When the player completes the loaded puzzle, they will automatically reload the overworld level they came from. + The 'manuallyIndexed' property, when true, indicates that the game should check the 'target_id' value to find the appropriate level index. When false, this property indicates that the game may use this warp to point to a level which is not already attached to another external warp. Automatically indexed external warps will be assigned the lowest unused level index from the Level List File. + +### Laser Emitter/Receiver: + Be sure to set the 'emitDirection' property. + Valid values are: + UP, + DOWN, + RIGHT, + LEFT + The 'playerMove' property indicates where the Laser Emitter can be pushed by players. + diff --git a/tools/soko/templateTiledProject/entitySprites/button16.png b/tools/soko/templateTiledProject/entitySprites/button16.png new file mode 100644 index 0000000000000000000000000000000000000000..2065777b120913be172e63fcada53749955c02a2 GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|sytmBLo9l) zPB_hbK!L}ZH)(N+npN;j&NMH6jRg)zmrq|c&BEbPy3m8{ncozdY>Z@jm&Y9On0(|5 zOBciEXA>QwZ*G}z|GVVhzm^J96+cO=EAkX||0y};DaX9V1zoF^Dz<&zwlR)jmg+OX p2JXb{PW3%(94W3z?->4H;oZ8~ylV$DLkrLq44$rjF6*2UngE!lM*#o; literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/crate16.png b/tools/soko/templateTiledProject/entitySprites/crate16.png new file mode 100644 index 0000000000000000000000000000000000000000..aac13c44399de403c4b930f79c2ece24b2fc5228 GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|@;zM~Lo9li zPP)k3V8G$x|Jd#BO6!+CMw9C@1-Rri<9!cTTTY5Tw>N&qj8ZnXhPRa_LK8d~7?j-o zw!ho=Ls(?Z<)S8rXDn>n(w{ZSTZju3=qNngxPYPBQScu}$>E3_>Kca{R(_hD5)ipv dSaroY-r34Z%I?cFV}VvPc)I$ztaD0e0swuxJyZYy literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/ghostblock16.png b/tools/soko/templateTiledProject/entitySprites/ghostblock16.png new file mode 100644 index 0000000000000000000000000000000000000000..cef0a40c90b1a009cfe909092a7458f3e31553e8 GIT binary patch literal 176 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|vOHZJLo9li zUOvftK!Jz#qN11kyUsmdKI;Vst0FB2z(f|Me literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/laser90Right16.png b/tools/soko/templateTiledProject/entitySprites/laser90Right16.png new file mode 100644 index 0000000000000000000000000000000000000000..f620ba50097d4f24adc476a96eabb3c1a65a9214 GIT binary patch literal 252 zcmVPx#wn;=mR5*>DlR*;0AP4~0On$_l;7#}^-b4@4Bu#Z{%S8{YuquTXqMlNXl?ZjB zS^?Mr)IFs(PIgV8!t635CED9O!hsHhm(E$yXYJ~c^-=E;P z^Z;(qw%YaxhIWZD0A&IrSHAQEMhf35_ZTXy!efOAJkxwrm{q<%=~UsDwm%M--S#Yk zOKIFVdQ&MBb@076?rIsgCw literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/laserReceiveOmni16.png b/tools/soko/templateTiledProject/entitySprites/laserReceiveOmni16.png new file mode 100644 index 0000000000000000000000000000000000000000..a12ad9b3279804c5678b55af43631665e6cba50f GIT binary patch literal 242 zcmVPx#tVu*cR5*>Llid-6FbsnQ*NwE3m?_!GGo=qCt>gT(*NO)Y(5E;y0X7m)1eBd; zFR4&z8*p^~)SJ!jJK00|x?bGrVVkUAu<+b3bo!61hLx;(yv}HfA*{#@o(A}kR{jbG zfI~nrrBLb(D%SvM=mkMAUwA}_^=fY;d=~Qeue=qN3Z&mp<2T3o2_wi#Y91PhoB#j-07*qoM6N<$f)>VOqW}N^ literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/laserReceiveUp16.png b/tools/soko/templateTiledProject/entitySprites/laserReceiveUp16.png new file mode 100644 index 0000000000000000000000000000000000000000..1a7283a84755c130ebce1e73c762dcac520f6cff GIT binary patch literal 317 zcmV-D0mA-?P)Px#_en%SR5*=wliRVxAPhqfJGUeCWY-jWa!uS1UJlDTlT1i#NfxFEHAYAQKn)6j zK?A7tHGqj0p_N;N+Faz~o^4HtsJ_2VvO!$t%L1`yB}GhS@8asdh(DSA7C{_qu;*!= zKzv?*?Sbn3zJkFNGl@4qlAc4OBo{;XN{SBf)rLJpl7ANGD5kzup=91Dv^oIHzPu1Z z;kyNeuOyA@*3P=Zw}ObCEXDF7)}!k`;i`Kv(XJlMp0^_Swb{Fc!G0R{3uJotfj$to z;?su=GTYvBP#s+c=mwGp?e8q8?yr+K`dF;luLyMjDV8KttmB;%Zy^Q$6K~PLBx}xqW`)Kz7T=Z!-(^Uy3ocCPo4}^d ztZUP_(^t69a97#$P_N5cKc2kL;h$^%Xlmz1#^kLNAJ2GwoU1&cu}iZ3U{>*@rk%NA zKiq2!Z!mmepPYAndJe;bcbp8ij|`$x-sc^t?ic@Gd!a+(?HyZT3)%AXk^4&Y-vymB z?OgO~x~RmizApdd`;rWg&98QTQS;-y(bBQ~j{YpB#83H_(Px03VDNPHb6Mw<&;$S= C7iHxD literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/warpexternal16.png b/tools/soko/templateTiledProject/entitySprites/warpexternal16.png new file mode 100644 index 0000000000000000000000000000000000000000..d622ba0f6523e5c263150d6aa23c32c49b7383d6 GIT binary patch literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|rg^$JhFJ72 z4Zh8L*np$ugtV-2rN()ev_i5=tK6|g}(eG*b51Cm%ShOF% SQ@$JMLIzJ)KbLh*2~7YET2;aT literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/warpinternal16.png b/tools/soko/templateTiledProject/entitySprites/warpinternal16.png new file mode 100644 index 0000000000000000000000000000000000000000..3ac464870a64f295f4cec4c81b3ce3ac2b1405fb GIT binary patch literal 241 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|R(rZQhFJ7Y zoqSUCumX?EQ=4)V+dvW1NYk6_98$U#`c@}9e%|^}$eHKaRk9?V+(_VSC oJEf5G!-Tzi^Ai6nhkj(&TJCc*WWDfQptBh~UHx3vIVCg!0QzWJYybcN literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/entitySprites/warpinternalexit16.png b/tools/soko/templateTiledProject/entitySprites/warpinternalexit16.png new file mode 100644 index 0000000000000000000000000000000000000000..b785f3666eabe4c6450a47d1bc047eb8577bd61c GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|c6+)whFJ7Y zoqSUCumX?E@yeGORY8HTJWVG$Z@9p8j(gt?vCopaF-Igcx-a}XJ>Bur_t)|t_3B*N z%L7{78&;^Q^Q}l)?GNRt)6tPir(vIzXS^>t5pRr-NDIiD>LZ7pFuuu<6hhb{MQ z$1G^bjVpISC literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/extensions/export-to-soko.js b/tools/soko/templateTiledProject/extensions/export-to-soko.js new file mode 100644 index 000000000..78a111d0f --- /dev/null +++ b/tools/soko/templateTiledProject/extensions/export-to-soko.js @@ -0,0 +1,391 @@ +var customMapFormat = { + name: "Swadge Sokobon Level Format", + extension: "bin", + write: + + function(p_map,p_fileName) { + + //Special Characters + var sokoSigs = + { + stackInPlace: 201, + compress: 202, + player: 203, + crate: 204, + warpinternal: 205, + warpinternalexit: 206, + warpexternal: 207, + button: 208, + laserEmitUp: 209, + laserReceiveOmni: 210, + laserReceiveUp: 211, + laser90Right: 212, + ghostblock: 213, + stackObjEnd: 230 + } + + var m = { + width: p_map.width, + height: p_map.height, + layers: [] + }; + + var sokoTileLayer, sokoObjectLayer; + var objArr = []; + //tiled.time("Export completed in"); + for (var i = 0; i < p_map.layerCount; ++i) + { + var layer = p_map.layerAt(i); + if(layer.isTileLayer) + { + sokoTileLayer = layer; + tiled.log("Layer " + i + " is Tile Layer"); + } + if(layer.isObjectLayer) + { + sokoObjectLayer = layer; + tiled.log("Layer " + i + " is Object Layer"); + } + } + sokoObjectLayer.objects.forEach( function(arrItem, ind) + { + tiled.log(ind); + + tiled.log(arrItem.tile.className); + var xval = Math.round(arrItem.x / arrItem.width); + var yval = Math.round(arrItem.y / arrItem.height - 1); + var posit = xval + yval * sokoTileLayer.width; + + tiled.log("(" + xval + "," + yval + ") Pos: " + posit + "(Width: " + sokoTileLayer.width + ")"); + var props = arrItem.resolvedProperties(); + tiled.log(JSON.stringify(props)) + tiled.log("-------------"); + var objItem = + { + obj: arrItem, + pos: posit, + x: xval, + y: yval, + index: ind + }; + objArr.push(objItem); + } + + + + ) + objArr.sort((a,b)=>(a.pos > b.pos) ? -1 : 1); //Sort by index in descending order so we can just split and insert stacked objects + objArr.forEach( function(arrItem) + { + tiled.log("Index:" + arrItem.index + ";Pos:" + arrItem.pos + ":(" + arrItem.x + ","+arrItem.y + ")"); + } + ) + for (var i = 0; i < p_map.layerCount; ++i) + { + var layer = p_map.layerAt(i); + + if(!layer.isTileLayer) + { + continue; + } + + data = []; + if(layer.isTileLayer) { + data.push(layer.width); + data.push(layer.height); + var rows = []; + for (y = 0; y < layer.height; ++y) { + var row = []; + for (x = 0; x < layer.width; ++x) + { + row.push(layer.cellAt(x,y).tileId); + data.push(layer.cellAt(x,y).tileId+1); + } + rows.push(row); + } + + //PackInObjects + if(1) + { + objArr.forEach(function(objItem, ind, objArr) + { + headerOffset = 2; + var objClassName = objItem.obj.tile.className; + tiled.log(sokoSigs[objClassName]); + var propertyVals = [111]; + propertyVals = propExtract(objItem, objArr); + insertionData = [sokoSigs.stackInPlace, sokoSigs[objClassName]].concat(propertyVals).concat([sokoSigs.stackObjEnd]); + tiled.log("DataBefore: " + data.slice(0,objItem.pos+headerOffset+1)); + tiled.log("InsertionData: " + insertionData); + tiled.log("DataAfter: " + data.slice(objItem.pos+headerOffset+1)); + data = data.slice(0,objItem.pos+headerOffset+1).concat(insertionData).concat(data.slice(objItem.pos+headerOffset+1)); + } + + ) + } + m.layers.push(rows); + tiled.log(m.layers); + //var file = new TextFile(fileName, TextFile.WriteOnly); + tiled.log("Export to " + p_fileName); + let view = Uint8Array.from(data); + let fileHand = new BinaryFile(p_fileName, BinaryFile.WriteOnly); + let buffer = view.buffer.slice(view.byteOffset, view.byteLength + view.byteOffset); + //let buffer = view.buffer; + tiled.log(view); + fileHand.write(buffer); + fileHand.commit(); + tiled.log(buffer); + } + } + + + } + + +} + +function findObjCoordById(objArr,id) +{ + var loopArgs = + { + id: id, + retVal: { + x: 0, + y: 0, + valid: false, + index: 0 + } + } + objArr.forEach( function(objEntry, ind, arr){ + //tiled.log("Target ID: " + this.id + " Entry ID: " + objEntry.obj.id + " Pos:(" + objEntry.x + "," + objEntry.y + ")"); + + if(this.id == objEntry.obj.id) + { + //tiled.log("MATCH!"); + this.retVal = { + x: objEntry.x, + y: objEntry.y, + valid: true, + index: ind + }; + } + + } , loopArgs + ) + return loopArgs.retVal; +} + +function propExtract(objItem, objArr) +{ + + soko_direction = + { + UP: 0, + DOWN: 1, + RIGHT: 2, + LEFT: 3 + }; + soko_player_gamemodes = + { + SOKO_OVERWORLD: 0, + SOKO_CLASSIC: 1, + SOKO_EULER: 2, + SOKO_LASERBOUNCE: 3 + }; + soko_crate_properties = + { + sticky: 0b1, + trail: 0b10 + }; + soko_warpinternal_properties = + { + allow_crates: 0b1 + }; + soko_warpexternal_properties = + { + manualIndex: 0b1 + }; + soko_laser90Right_properties = + { + emitDirection: 0b1, + playerMove: 0b10 + }; + soko_laserEmitUp_properties = + { + playerMove: 0b10 + }; + soko_button_properties = + { + cratePress: 0b1, + playerPress: 0b10, + invertAction: 0b100, + stayDownOnPress: 0b1000 + }; + soko_ghostblock_properties = + { + inverted: 0b100, + playerMove: 0b10 + }; + + var properties = objItem.obj.resolvedProperties(); + + retVal = []; + + switch(objItem.obj.tile.className) + { + case "player": + retVal.push(soko_player_gamemodes[properties.gamemode]); + break; + case "crate": + var variant = 0b0; + if(properties.sticky) + { + variant = variant | soko_crate_properties.sticky; + } + if(properties.trail) + { + variant = variant | soko_crate_properties.trail; + } + retVal.push(variant); + break; + case "laser90Right": + var variant = 0b0; + if(properties.emitDirection) + { + variant = variant | soko_laser90Right_properties.emitDirection; + //tiled.log("laser90Right:emitDirection:" + properties.emitDirection); + } + if(properties.playerMove) + { + variant = variant | soko_laser90Right_properties.playerMove; + //tiled.log("laser90Right:emitDirection:" + properties.playerMove); + } + retVal.push(variant); + break; + case "laserEmitUp": + var variant = 0b0; + if(properties.playerMove) + { + variant = variant | soko_laserEmitUp_properties.playerMove; + } + //tiled.log("" + properties.emitDirection + " " + soko_direction[properties.emitDirection]); + variant = variant | ((soko_direction[properties.emitDirection] & 0b11) << 6); + retVal.push(variant); + break; + case "laserReceiveUp": + var variant = 0b0; + tiled.log("" + properties.emitDirection + " " + soko_direction[properties.emitDirection]); + variant = variant | ((soko_direction[properties.emitDirection] & 0b11) << 6); + retVal.push(variant); + break + case "warpinternal": + var variant = 0b0; + if(properties.allow_crates) + { + variant = variant | soko_warpinternal_properties.allow_crates; + //tiled.log("laser90Right:emitDirection:" + properties.emitDirection); + } + retVal.push(variant); + retVal.push(properties.hp); + var targetCoord = findObjCoordById(objArr,properties.target_id); + //if(targetCoord.valid) + //{ + //tiled.log("className === warpinternalexit || className === warpinternal: " + ((objArr[targetCoord.index].obj.tile.className === "warpinternalexit") || (objArr[targetCoord.index].obj.tile.className === "warpinternal"))); + //} + if(!targetCoord.valid) + { + tiled.log("No Valid Warp Exit at target_id"); + } + if(targetCoord.valid && ((objArr[targetCoord.index].obj.tile.className === "warpinternalexit") || (objArr[targetCoord.index].obj.tile.className === "warpinternal"))){ + tiled.log("Warp Valid ID: " + properties.target_id + " at Coord(" + targetCoord.x + "," + targetCoord.y + ")"); + //retVal.push(properties[idString]); + retVal.push(targetCoord.x); + retVal.push(targetCoord.y); + } + break; + case "warpexternal": + var variant = 0b0; + var target_id = properties.target_id; + if(properties.manualIndex) + { + variant = variant | soko_warpexternal_properties.manualIndex; + //tiled.log("laser90Right:emitDirection:" + properties.emitDirection); + } + retVal.push(variant); + retVal.push(target_id); + break; + case "button": + var variant = 0b0; + if(properties.cratePress) + { + variant = variant | soko_button_properties.cratePress; + } + if(properties.invertAction) + { + variant = variant | soko_button_properties.invertAction; + } + if(properties.playerPress) + { + variant = variant | soko_button_properties.playerPress; + } + if(properties.stayDownOnPress) + { + variant = variant | soko_button_properties.stayDownOnPress; + } + var numTarg = (properties.numTargets & 0b111); + variant = variant | (numTarg << 5); //store the number of targets in the upper 5 bits (up to 7 targets per button) + retVal.push(variant); + for(var i = 0; i < numTarg; ++i) + { + idString = "target" + (i+1) + "id"; + var targetCoord = findObjCoordById(objArr,properties[idString]); + if(targetCoord.valid){ + tiled.log("Valid ID:" + properties[idString] + " at Coord(" + targetCoord.x + "," + targetCoord.y + ")"); + //retVal.push(properties[idString]); + retVal.push(targetCoord.x); + retVal.push(targetCoord.y); + } + else + { + numTarg -= 1; //discard invalid target ID, reduce target count by 1 + variant = variant & 0b11111; + variant = variant | (numTarg << 5); + retVal[0] = variant; + } + } + break; + case "ghostblock": + var variant = 0b0; + if(properties.inverted) + { + variant = variant | soko_ghostblock_properties.inverted; + } + if(properties.playerMove) + { + variant = variant | soko_ghostblock_properties.playerMove; + } + + } + return retVal; +} + +tiled.log("Registering Soko Map Export"); +//tiled.log(tiled.activeAsset.layers[0].cellAt(3,1)); +//map = tiled.activeAsset; +//dat = []; +//dat.push(map.width); +//dat.push(map.height); +//for (var y = 0; y < map.height; ++y) +/* +{ + for(var x = 0; x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/sk_binOverworld.bin b/tools/soko/templateTiledProject/sk_binOverworld.bin new file mode 100644 index 0000000000000000000000000000000000000000..9aab206e2aa6ebc100fa5b8095345f6b9b083c66 GIT binary patch literal 107 qcmd<&WP}4I2w-GHFixIlV0gyFgoQ#@1yq0XG)Ns2nkZNVW + + + + + + + + + + + + + + +12,12,12,12,12,12,12,12,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,12,12,12,12,12,12,12,12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/sk_laserTest.bin b/tools/soko/templateTiledProject/sk_laserTest.bin new file mode 100644 index 0000000000000000000000000000000000000000..b0208cc4b6e9d4248b446d8deb1df5a054aa43fc GIT binary patch literal 123 zcmd<&WP}5zlb0QyF@XUiBZOuGGfrM%0E(Zy!U!ZG0%t%1K%vvj&wv6D8JH@N6te!4 P7aJgouP}jSVD + + + + + + + + + + + + + + +12,12,12,12,12,12,12,12,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,12,12,12,12,12,12,12,12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/soko_entities.tsx b/tools/soko/templateTiledProject/soko_entities.tsx new file mode 100644 index 000000000..9d0caa3ac --- /dev/null +++ b/tools/soko/templateTiledProject/soko_entities.tsx @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/templateMap.bin b/tools/soko/templateTiledProject/templateMap.bin new file mode 100644 index 0000000000000000000000000000000000000000..cc7f267ef3b78593c935178de507ec1f7694f6d6 GIT binary patch literal 99 zcmXv`+X;X$5WA#OT*WOM&Gil+RMZtRhj)eMBOwnmR}9ckvr4g${;cXA5J11gL}j$8a?MjXQc literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/templateMap.tmx b/tools/soko/templateTiledProject/templateMap.tmx new file mode 100644 index 000000000..be9e1a588 --- /dev/null +++ b/tools/soko/templateTiledProject/templateMap.tmx @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + +12,12,12,12,12,12,12,12, +12,13,13,14,13,13,13,12, +12,13,13,13,13,13,13,12, +12,13,13,13,13,13,15,12, +12,13,13,13,13,13,13,12, +12,12,12,12,12,12,12,12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/templateProject.tiled-project b/tools/soko/templateTiledProject/templateProject.tiled-project new file mode 100644 index 000000000..d0eb59206 --- /dev/null +++ b/tools/soko/templateTiledProject/templateProject.tiled-project @@ -0,0 +1,14 @@ +{ + "automappingRulesFile": "", + "commands": [ + ], + "compatibilityVersion": 1100, + "extensionsPath": "extensions", + "folders": [ + "." + ], + "properties": [ + ], + "propertyTypes": [ + ] +} diff --git a/tools/soko/templateTiledProject/templateProject.tiled-session b/tools/soko/templateTiledProject/templateProject.tiled-session new file mode 100644 index 000000000..ea98e1286 --- /dev/null +++ b/tools/soko/templateTiledProject/templateProject.tiled-session @@ -0,0 +1,42 @@ +{ + "activeFile": "templateMap.tmx", + "expandedProjectPaths": [ + "." + ], + "file.lastUsedOpenFilter": "All Files (*)", + "fileStates": { + "": { + "scaleInEditor": 1 + }, + "objLayers.tsx": { + "dynamicWrapping": true, + "scaleInDock": 1, + "scaleInEditor": 1 + }, + "templateMap.tmx": { + "scale": 4.492708333333334, + "selectedLayer": 1, + "viewCenter": { + "x": 58.094134013447714, + "y": 48.18919545559935 + } + }, + "templateMap.tmx#tilesheet": { + "dynamicWrapping": false, + "scaleInDock": 1 + } + }, + "last.exportedFilePath": "C:/Users/grana/repos/Swadge-IDF-5.0/tools/soko/templateTiledProject", + "last.imagePath": "C:/Users/grana/repos/Swadge-IDF-5.0/tools/soko/templateTiledProject/tileSprites", + "map.lastUsedExportFilter": "All Files (*)", + "openFiles": [ + "templateMap.tmx", + "objLayers.tsx" + ], + "project": "templateProject.tiled-project", + "property.type": "bool", + "recentFiles": [ + "objLayers.tsx", + "templateMap.tmx" + ] +} diff --git a/tools/soko/templateTiledProject/tileSprites/tilesheet.png b/tools/soko/templateTiledProject/tileSprites/tilesheet.png new file mode 100644 index 0000000000000000000000000000000000000000..90c89a827e95d5ec8d788723291ac1339242a266 GIT binary patch literal 792 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>V7lb#;uumf z=k3kJywwH*ZWqsomOZiUy*}mUrGx^CJ@%rQad(!{M z@A#KrW_js`y!BL-mDjBPAE@QOz4`ya%{SK6q!!9*cQ3oSepT4A^`g~vfBB0mST^)B z9PR&g?O$D%r{>q&QM0dp7M-lF>hCq_Sxd+H^Y30?uBn_`^)2ju-Ln6JzvW6x>RGle zn^L5?>2>Kk-!p5T2AQZkiEsZ`vvkUp^DifI2-cMQ&%XXYVET)Tr&S7ePTTu;_rv;< zB^^hed(UED_2W+W?g!PHEAx$| z+JBf{yYeihaY=sHfx>nDDiV_wpQjcafB5g_zoVwR6XXAWj7m-Wyz!_xKa0tvBeS+^ zaM<5{r(__MWEga3P7s41`@7p2r?wnlxnKq#`(_E&2WOp`e!M-{x8CN##A(V0ZhAg= zr@77WLY!Au?(h3I`$G?;_%TdB>)5}%Az7%$b>Vg4aK=eR*}j*uK8goBY?=99S?F0q zhQFgm>@S7y{q?^4Jh-m@-YCJU^M85hfwjIYSrY}c*Q|W2$@Kf%B>DMP{}!CSW&FR+ zQX?}?G{=_Z)n1uDTAKd0d$Jc*zg8sD`PU@nTG`tFZY=SaazzcozV$bjRqk7U zYKBSrS|%mVCC3-mT}*3VIsJXKT-~NAn{UmVCwoj&`5v3x+n?Xlzdzr@d&YA0lu1e- zCOymAe~0V%zld+Ev)0Q0H#1nfm+zjk(8BX`!zUg1T + + + + + + + diff --git a/tools/soko/templateTiledProject/warehouse.bin b/tools/soko/templateTiledProject/warehouse.bin new file mode 100644 index 0000000000000000000000000000000000000000..216b974867b2a214e09281942a452bb49adee519 GIT binary patch literal 106 zcmX|&%MAb^2t(m<(fy|~ItOz|F|mh2e`A`tAO`9NYkRVeWo!8op{zs)_2UFuwGqvq JRHSkh9xm>s6i@&F literal 0 HcmV?d00001 diff --git a/tools/soko/templateTiledProject/warehouse.tmx b/tools/soko/templateTiledProject/warehouse.tmx new file mode 100644 index 000000000..328d85892 --- /dev/null +++ b/tools/soko/templateTiledProject/warehouse.tmx @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + +0,0,12,12,12,12,12,0, +12,12,12,13,13,13,12,0, +12,14,13,13,13,13,12,0, +12,12,12,13,13,14,12,0, +12,14,12,12,13,13,12,0, +12,13,12,13,14,13,12,12, +12,13,13,14,13,13,14,12, +12,13,13,13,14,13,13,12, +12,12,12,12,12,12,12,12 + + + + + + + + + + + + + diff --git a/tools/soko/tmx_to_binary.py b/tools/soko/tmx_to_binary.py new file mode 100644 index 000000000..6141fb703 --- /dev/null +++ b/tools/soko/tmx_to_binary.py @@ -0,0 +1,222 @@ +import math +from itertools import groupby +from xml.dom.minidom import parse,parseString +import os + +SIG_BYTE_SIMPLE_SEQUENCE = 200 #This byte will capture long strings of bytes in compact form. Example [12][12][12]...[12] => [200][#][12] where # is number of 12 tiles + +OBJECT_START_BYTE = 201 +SKB_EMPTY = 0 +SKB_WALL = 1 +SKB_FLOOR = 2 +SKB_GOAL = 3 +SKB_NO_WALK = 4 +SKB_OBJSTART = 201 +SKB_COMPRESS = 202 +SKB_PLAYER = 203 +SKB_CRATE = 204 +SKB_WARPINTERNAL = 205 +SKB_WARPINTERNALEXIT = 206 +SKB_WARPEXTERNAL = 207 +SKB_BUTTON = 208 +SKB_LASEREMITTER = 209 +SKB_LASERRECEIVEROMNI = 210 +SKB_LASERRECEIVER = 211 +SKB_LASER90ROTATE = 212 +SKB_GHOSTBLOCK = 213 +SKB_OBJEND = 230 + +classToID = { + "wall": SKB_WALL, + "wal": SKB_WALL, + "block": SKB_WALL, + "floor": SKB_FLOOR, + "ground": SKB_FLOOR, + "goal":SKB_GOAL, + "floornowalk":SKB_NO_WALK, + "nowalk":SKB_NO_WALK, + "nowalkfloor":SKB_NO_WALK, + "empty":SKB_EMPTY, + "nothing":SKB_EMPTY, + "player":SKB_PLAYER, + "crate":SKB_CRATE, + "warpexternal":SKB_WARPEXTERNAL, + "portal":SKB_WARPEXTERNAL, + "button":SKB_BUTTON, +} + +def insert_position(position, sourceList, insertionList): + return sourceList[:position] + insertionList + sourceList[position:] + +# root = tk.Tk() +# root.withdraw() + +def convertTMX(file_path): + print("convert "+file_path) + document = parse(file_path) + entities = {} + mapHeaderWidth = int(document.getElementsByTagName("map")[0].attributes.getNamedItem("width").nodeValue) + mapHeaderHeight = int(document.getElementsByTagName("map")[0].attributes.getNamedItem("height").nodeValue) + mode = "SOKO_UNDEFINED" + + + allProps = document.getElementsByTagName("property") + for prop in allProps: + if(prop.getAttribute("name") == "gamemode"): + mode=prop.getAttribute("value") + break + if(mode == "SOKO_UNDEFINED"): + print("Preprocessor Warning. "+file_path+" has no properly set gamemode. setting gamemode to SOKO_CLASSIC") + mode = "SOKO_CLASSIC" + modeInt = getModeInt(mode) + + #get firstgid of tilesheet + tileLookups = {} #offset from the tilesheet + tilesets = document.getElementsByTagName("tileset") + for tileset in tilesets: + x =(int((tileset.getAttribute("firstgid")))) + source = tileset.getAttribute("source") + if(source != ""): + current_dir = os.path.dirname(file_path) + tpath = os.path.normpath(current_dir+"/"+source) + if(os.path.splitext(tpath)[1] == ".tsx"): + doc = parse(tpath) + tileLookups[x] = loadTilesetLookup(doc) + else: + tileLookups[x] = loadTilesetLookup(tileset) + + # populate entities dictionary + + # loop through entities and add values. + objectlayers = document.getElementsByTagName("objectgroup") + entityContainer = objectlayers[0] + if(entityContainer.getAttribute("name") != "entities"): #todo: get length of container. number children, i guess? + print("Warning, object layer not called 'entities' or there is more than one object layer. there should be just one, called entities.") + + for entity in entityContainer.getElementsByTagName("object"): + ebytes = getEntityBytesFromEntity(entity,tileLookups) + x = int(float(entity.getAttribute("x"))/16) + y = int(float(entity.getAttribute("y"))/16) + #print(str(x)+","+str(y)+" = "+str(ebytes)) + entities[str(x)+","+str(y)] = ebytes + + + + dataText = document.getElementsByTagName("data")[0].firstChild.nodeValue + scrub = "".join(dataText.split()) #Remove all residual whitespace in data block + scrub = [(int(i)) for i in scrub.split(",")] #Convert all tileIDs to int. + + # fisrt, our HEADER data: width, height, modeint + output = [mapHeaderWidth,mapHeaderHeight,modeInt] + for i in range(len(scrub)): + x = (i-1)%mapHeaderWidth + y = ((i-1)//mapHeaderWidth)+1 #todo: figure out why this is +1 + keypos = str(x)+","+str(y) + #print("playing with "+key) + if(keypos in entities): + #append each byte of the entity data. + for b in entities[keypos]: + output.append(b) + output.append(int(getTile(scrub[i],tileLookups))) + + # output now is a list of tiles. + + output2 = compress(output) + #output2 = output + rawsize = len(output) + compsize = len(output2) + rawBytesc = bytearray(output2) + rawBytesImmut = bytes(rawBytesc) + return rawBytesImmut, rawsize, compsize + # outfile_path = "".join([file_path.split(".")[0],".bin"]) + # with open(outfile_path,"wb") as binary_file: + # binary_file.write(rawBytesImmut) + +def compress(bytes): + res = [] + for k,i in groupby(bytes): + run = list(i) + if(len(run)>3): + res.extend([k,SKB_COMPRESS,len(run)-1]) + else: + res.extend(run) + return res + +# These need to match the enum int casts in soko.h +def getModeInt(mode): + mode = mode.upper() + if(mode == "SOKO_OVERWORLD" or mode == "OVERWORLD"): + return 0 + elif mode == "SOKO_CLASSIC" or mode == "CLASSIC": + return 1 + elif mode == "SOKO_EULER" or mode == "EULER": + return 2 + elif mode == "SOKO_LASER" or mode == "LASER" or mode == "SOKO_LASERBOUNCE" or mode == "LASERBOUNCE": + return 3 + + +def getEntityBytesFromEntity(entity,lookups): + #todo: look up data in the tsx. which we have loaded? I think? + #SKB_OBJSTART, SKB_[Object Type], [Data Bytes] , SKB_OBJEND + gid = int(entity.getAttribute("gid")) + tid = getTile(gid,lookups) + + otype = 0 + if(tid == SKB_PLAYER): + return [SKB_OBJSTART,SKB_PLAYER,SKB_OBJEND] + elif(tid == SKB_WARPEXTERNAL): + # index of destination or x,y? + id = int(getEntityPropValue(entity,"target_id",None)) + return [SKB_OBJSTART,SKB_WARPEXTERNAL,id,SKB_OBJEND] + elif(tid == SKB_CRATE): + # bit 0 is sticky ob01 + # bit 1 is trail ob10 + sticky = 0 + trail = 0 + if getEntityPropValue(entity,"sticky","false") == "true": + sticky = 1 + if getEntityPropValue(entity,"trail","false") == "true": + trail = 2 + flag = trail+sticky + return [SKB_OBJSTART,SKB_CRATE,flag,SKB_OBJEND] + # etc + print("could not get entity..."+str(gid)); + return [] + return [SKB_OBJSTART,SKB_CRATE,SKB_OBJEND] + +def getTile(i,lookups): + if(i == 0): + # empty from tiled + return 0 # whatever our empty is. + for k,v in lookups.items(): + ix = i-k + if(k > i): + continue + if ix in v: + s = v[ix] + if s in classToID: + x = classToID[s] + return x + else: + print("what's the byte for "+str(s)) + print("uh oh"+str(i)+"-"+str(k)+"-"+str(lookups)) + return i + +def loadTilesetLookup(doc): + # turn root object into dictonary of id's->classnames. + tiles = doc.getElementsByTagName("tile") + lookup = {} + for tile in tiles: + lookup[int(tile.getAttribute("id"))] = tile.getAttribute("type") + + return lookup + +def getEntityPropValue(entity, property, default=0): + props = entity.getElementsByTagName("property") + for prop in props: + if prop.getAttribute("name") == property: + return prop.getAttribute("value") + + return default + + \ No newline at end of file From 3bd1e99cdba40628bf4999613072162b6c33d190 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 23 Sep 2024 01:06:29 +0000 Subject: [PATCH 34/54] Add hardware type config Specify pins when initializing DAC Add DAC shutdown function Fix GPIO headers to match current IDF --- components/hdw-battmon/include/hdw-battmon.h | 2 +- components/hdw-dac/hdw-dac.c | 38 ++++++++++- components/hdw-dac/include/hdw-dac.h | 6 +- components/hdw-esp-now/include/hdw-esp-now.h | 2 +- components/hdw-imu/include/hdw-imu.h | 1 + components/hdw-led/include/hdw-led.h | 2 +- components/hdw-mic/include/hdw-mic.h | 2 +- components/hdw-tft/include/hdw-tft.h | 2 +- emulator/idf-inc/driver/dac_continuous.h | 13 ++++ emulator/idf-inc/driver/gpio.h | 1 + emulator/idf-inc/esp_sleep.h | 2 +- emulator/idf-inc/hal/gpio_types.h | 49 -------------- emulator/idf-inc/soc/gpio_num.h | 50 ++++++++++++++ emulator/src/components/hdw-dac/hdw-dac.c | 24 ++++++- main/Kconfig.projbuild | 19 ++++++ main/swadge2024.c | 68 ++++++++++++++------ makefile | 1 + sdkconfig | 3 + 18 files changed, 204 insertions(+), 81 deletions(-) create mode 100644 emulator/idf-inc/driver/dac_continuous.h create mode 100644 emulator/idf-inc/soc/gpio_num.h diff --git a/components/hdw-battmon/include/hdw-battmon.h b/components/hdw-battmon/include/hdw-battmon.h index cbc67d36b..725e5e1a0 100644 --- a/components/hdw-battmon/include/hdw-battmon.h +++ b/components/hdw-battmon/include/hdw-battmon.h @@ -29,7 +29,7 @@ * \endcode */ -#include +#include void initBattmon(gpio_num_t gpio); void deinitBattmon(void); diff --git a/components/hdw-dac/hdw-dac.c b/components/hdw-dac/hdw-dac.c index 4d34fcde9..35eb5557f 100644 --- a/components/hdw-dac/hdw-dac.c +++ b/components/hdw-dac/hdw-dac.c @@ -5,7 +5,6 @@ #include #include "freertos/FreeRTOS.h" #include "freertos/queue.h" -#include "driver/dac_continuous.h" #include "hdw-dac.h" //============================================================================== @@ -31,6 +30,9 @@ static fnDacCallback_t dacCb = NULL; /** A temporary buffer for the application to fill with audio samples before */ static uint8_t tmpDacBuf[DAC_BUF_SIZE] = {0}; +/** The GPIO which controls amplifier shutdown */ +static gpio_num_t shdnGpio; + //============================================================================== // Functions //============================================================================== @@ -62,16 +64,18 @@ static bool IRAM_ATTR dac_on_convert_done_callback(dac_continuous_handle_t handl /** * @brief Initialize the DAC * + * @param channel The output channel (pin) for the ADC + * @param shdn_gpio The GPIO that controls the amplifier's shutdown * @param cb A callback function which will be called to request samples from the application */ -void initDac(fnDacCallback_t cb) +void initDac(dac_channel_mask_t channel, gpio_num_t shdn_gpio, fnDacCallback_t cb) { /* Save the callback */ dacCb = cb; /* Configure the DAC */ dac_continuous_config_t cont_cfg = { - .chan_mask = DAC_CHANNEL_MASK_CH0, // This is GPIO_NUM_17 + .chan_mask = channel, // DAC_CHANNEL_MASK_CH0 is GPIO_NUM_17, DAC_CHANNEL_MASK_CH1 is GPIO_NUM_18 .desc_num = DMA_DESCRIPTORS, // The number of DMA descriptors .buf_size = DAC_BUF_SIZE, // The size of each MDA buffer .freq_hz = DAC_SAMPLE_RATE_HZ, // The frequency of DAC conversion @@ -93,6 +97,15 @@ void initDac(fnDacCallback_t cb) .on_stop = NULL, }; ESP_ERROR_CHECK(dac_continuous_register_event_callback(dac_handle, &cbs, dacIsrQueue)); + + /* Initialize the GPIO of shutdown pin */ + shdnGpio = shdn_gpio; + gpio_config_t shdn_gpio_config = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << shdn_gpio, + }; + ESP_ERROR_CHECK(gpio_config(&shdn_gpio_config)); + ESP_ERROR_CHECK(gpio_set_level(shdn_gpio, 0)); } /** @@ -153,3 +166,22 @@ void dacPoll(void) } } } + +/** + * @brief Set the shutdown state of the DAC + * + * @param shutdown true to shut down the DAC, false to enable it + */ +void setDacShutdown(bool shutdown) +{ + if (shutdown) + { + ESP_ERROR_CHECK(gpio_set_level(shdnGpio, 1)); + dacStop(); + } + else + { + ESP_ERROR_CHECK(gpio_set_level(shdnGpio, 0)); + dacStart(); + } +} \ No newline at end of file diff --git a/components/hdw-dac/include/hdw-dac.h b/components/hdw-dac/include/hdw-dac.h index 59695ab68..f47d38a06 100644 --- a/components/hdw-dac/include/hdw-dac.h +++ b/components/hdw-dac/include/hdw-dac.h @@ -67,6 +67,9 @@ //============================================================================== #include +#include +#include "driver/dac_continuous.h" +#include "driver/gpio.h" //============================================================================== // Defines @@ -94,8 +97,9 @@ typedef void (*fnDacCallback_t)(uint8_t* samples, int16_t len); // Function Declarations //============================================================================== -void initDac(fnDacCallback_t cb); +void initDac(dac_channel_mask_t channel, gpio_num_t shdn_gpio, fnDacCallback_t cb); void deinitDac(void); void dacPoll(void); void dacStart(void); void dacStop(void); +void setDacShutdown(bool shutdown); diff --git a/components/hdw-esp-now/include/hdw-esp-now.h b/components/hdw-esp-now/include/hdw-esp-now.h index 189693cc3..ce7388236 100644 --- a/components/hdw-esp-now/include/hdw-esp-now.h +++ b/components/hdw-esp-now/include/hdw-esp-now.h @@ -85,7 +85,7 @@ #include #include -#include +#include #include //============================================================================== diff --git a/components/hdw-imu/include/hdw-imu.h b/components/hdw-imu/include/hdw-imu.h index 74d3d0350..6a4480b57 100644 --- a/components/hdw-imu/include/hdw-imu.h +++ b/components/hdw-imu/include/hdw-imu.h @@ -46,6 +46,7 @@ #include #include +#include #include #include "quaternions.h" diff --git a/components/hdw-led/include/hdw-led.h b/components/hdw-led/include/hdw-led.h index 6096d082a..085c34ffd 100644 --- a/components/hdw-led/include/hdw-led.h +++ b/components/hdw-led/include/hdw-led.h @@ -55,7 +55,7 @@ #include #include -#include +#include /// @brief The maximum LED brightness setting #define MAX_LED_BRIGHTNESS 8 diff --git a/components/hdw-mic/include/hdw-mic.h b/components/hdw-mic/include/hdw-mic.h index b6f6f7d3d..ace97356d 100644 --- a/components/hdw-mic/include/hdw-mic.h +++ b/components/hdw-mic/include/hdw-mic.h @@ -72,7 +72,7 @@ #include -#include +#include //============================================================================== // Defines diff --git a/components/hdw-tft/include/hdw-tft.h b/components/hdw-tft/include/hdw-tft.h index fc6fc5a81..17982313c 100644 --- a/components/hdw-tft/include/hdw-tft.h +++ b/components/hdw-tft/include/hdw-tft.h @@ -78,7 +78,7 @@ #include #include -#include +#include #include #include #include diff --git a/emulator/idf-inc/driver/dac_continuous.h b/emulator/idf-inc/driver/dac_continuous.h new file mode 100644 index 000000000..8402d348c --- /dev/null +++ b/emulator/idf-inc/driver/dac_continuous.h @@ -0,0 +1,13 @@ +#pragma once + +#define BIT(nr) (1UL << (nr)) + +/** + * @brief DAC channel mask + * + */ +typedef enum { + DAC_CHANNEL_MASK_CH0 = BIT(0), /*!< DAC channel 0 is GPIO25(ESP32) / GPIO17(ESP32S2) */ + DAC_CHANNEL_MASK_CH1 = BIT(1), /*!< DAC channel 1 is GPIO26(ESP32) / GPIO18(ESP32S2) */ + DAC_CHANNEL_MASK_ALL = BIT(0) | BIT(1), /*!< Both DAC channel 0 and channel 1 */ +} dac_channel_mask_t; diff --git a/emulator/idf-inc/driver/gpio.h b/emulator/idf-inc/driver/gpio.h index b6096793e..74fe22c3a 100644 --- a/emulator/idf-inc/driver/gpio.h +++ b/emulator/idf-inc/driver/gpio.h @@ -7,3 +7,4 @@ #pragma once #include "hal/gpio_types.h" +#include "soc/gpio_num.h" \ No newline at end of file diff --git a/emulator/idf-inc/esp_sleep.h b/emulator/idf-inc/esp_sleep.h index f759ae4d3..acb6c3381 100644 --- a/emulator/idf-inc/esp_sleep.h +++ b/emulator/idf-inc/esp_sleep.h @@ -11,7 +11,7 @@ #include "esp_err.h" #include "touch_pad.h" -#include "hal/gpio_types.h" +#include "soc/gpio_num.h" // #include "soc/soc_caps.h" diff --git a/emulator/idf-inc/hal/gpio_types.h b/emulator/idf-inc/hal/gpio_types.h index c44b89e20..9a71647c0 100644 --- a/emulator/idf-inc/hal/gpio_types.h +++ b/emulator/idf-inc/hal/gpio_types.h @@ -6,55 +6,6 @@ #pragma once -typedef enum -{ - GPIO_NUM_NC = -1, /*!< Use to signal not connected to S/W */ - GPIO_NUM_0 = 0, /*!< GPIO0, input and output */ - GPIO_NUM_1 = 1, /*!< GPIO1, input and output */ - GPIO_NUM_2 = 2, /*!< GPIO2, input and output */ - GPIO_NUM_3 = 3, /*!< GPIO3, input and output */ - GPIO_NUM_4 = 4, /*!< GPIO4, input and output */ - GPIO_NUM_5 = 5, /*!< GPIO5, input and output */ - GPIO_NUM_6 = 6, /*!< GPIO6, input and output */ - GPIO_NUM_7 = 7, /*!< GPIO7, input and output */ - GPIO_NUM_8 = 8, /*!< GPIO8, input and output */ - GPIO_NUM_9 = 9, /*!< GPIO9, input and output */ - GPIO_NUM_10 = 10, /*!< GPIO10, input and output */ - GPIO_NUM_11 = 11, /*!< GPIO11, input and output */ - GPIO_NUM_12 = 12, /*!< GPIO12, input and output */ - GPIO_NUM_13 = 13, /*!< GPIO13, input and output */ - GPIO_NUM_14 = 14, /*!< GPIO14, input and output */ - GPIO_NUM_15 = 15, /*!< GPIO15, input and output */ - GPIO_NUM_16 = 16, /*!< GPIO16, input and output */ - GPIO_NUM_17 = 17, /*!< GPIO17, input and output */ - GPIO_NUM_18 = 18, /*!< GPIO18, input and output */ - GPIO_NUM_19 = 19, /*!< GPIO19, input and output */ - GPIO_NUM_20 = 20, /*!< GPIO20, input and output */ - GPIO_NUM_21 = 21, /*!< GPIO21, input and output */ - GPIO_NUM_26 = 26, /*!< GPIO26, input and output */ - GPIO_NUM_27 = 27, /*!< GPIO27, input and output */ - GPIO_NUM_28 = 28, /*!< GPIO28, input and output */ - GPIO_NUM_29 = 29, /*!< GPIO29, input and output */ - GPIO_NUM_30 = 30, /*!< GPIO30, input and output */ - GPIO_NUM_31 = 31, /*!< GPIO31, input and output */ - GPIO_NUM_32 = 32, /*!< GPIO32, input and output */ - GPIO_NUM_33 = 33, /*!< GPIO33, input and output */ - GPIO_NUM_34 = 34, /*!< GPIO34, input and output */ - GPIO_NUM_35 = 35, /*!< GPIO35, input and output */ - GPIO_NUM_36 = 36, /*!< GPIO36, input and output */ - GPIO_NUM_37 = 37, /*!< GPIO37, input and output */ - GPIO_NUM_38 = 38, /*!< GPIO38, input and output */ - GPIO_NUM_39 = 39, /*!< GPIO39, input and output */ - GPIO_NUM_40 = 40, /*!< GPIO40, input and output */ - GPIO_NUM_41 = 41, /*!< GPIO41, input and output */ - GPIO_NUM_42 = 42, /*!< GPIO42, input and output */ - GPIO_NUM_43 = 43, /*!< GPIO43, input and output */ - GPIO_NUM_44 = 44, /*!< GPIO44, input and output */ - GPIO_NUM_45 = 45, /*!< GPIO45, input and output */ - GPIO_NUM_46 = 46, /*!< GPIO46, input mode only */ - GPIO_NUM_MAX, -} gpio_num_t; - typedef enum { GPIO_PULLUP_DISABLE = 0x0, /*!< Disable GPIO pull-up resistor */ diff --git a/emulator/idf-inc/soc/gpio_num.h b/emulator/idf-inc/soc/gpio_num.h new file mode 100644 index 000000000..38c8e3ec8 --- /dev/null +++ b/emulator/idf-inc/soc/gpio_num.h @@ -0,0 +1,50 @@ +#pragma once + +typedef enum +{ + GPIO_NUM_NC = -1, /*!< Use to signal not connected to S/W */ + GPIO_NUM_0 = 0, /*!< GPIO0, input and output */ + GPIO_NUM_1 = 1, /*!< GPIO1, input and output */ + GPIO_NUM_2 = 2, /*!< GPIO2, input and output */ + GPIO_NUM_3 = 3, /*!< GPIO3, input and output */ + GPIO_NUM_4 = 4, /*!< GPIO4, input and output */ + GPIO_NUM_5 = 5, /*!< GPIO5, input and output */ + GPIO_NUM_6 = 6, /*!< GPIO6, input and output */ + GPIO_NUM_7 = 7, /*!< GPIO7, input and output */ + GPIO_NUM_8 = 8, /*!< GPIO8, input and output */ + GPIO_NUM_9 = 9, /*!< GPIO9, input and output */ + GPIO_NUM_10 = 10, /*!< GPIO10, input and output */ + GPIO_NUM_11 = 11, /*!< GPIO11, input and output */ + GPIO_NUM_12 = 12, /*!< GPIO12, input and output */ + GPIO_NUM_13 = 13, /*!< GPIO13, input and output */ + GPIO_NUM_14 = 14, /*!< GPIO14, input and output */ + GPIO_NUM_15 = 15, /*!< GPIO15, input and output */ + GPIO_NUM_16 = 16, /*!< GPIO16, input and output */ + GPIO_NUM_17 = 17, /*!< GPIO17, input and output */ + GPIO_NUM_18 = 18, /*!< GPIO18, input and output */ + GPIO_NUM_19 = 19, /*!< GPIO19, input and output */ + GPIO_NUM_20 = 20, /*!< GPIO20, input and output */ + GPIO_NUM_21 = 21, /*!< GPIO21, input and output */ + GPIO_NUM_26 = 26, /*!< GPIO26, input and output */ + GPIO_NUM_27 = 27, /*!< GPIO27, input and output */ + GPIO_NUM_28 = 28, /*!< GPIO28, input and output */ + GPIO_NUM_29 = 29, /*!< GPIO29, input and output */ + GPIO_NUM_30 = 30, /*!< GPIO30, input and output */ + GPIO_NUM_31 = 31, /*!< GPIO31, input and output */ + GPIO_NUM_32 = 32, /*!< GPIO32, input and output */ + GPIO_NUM_33 = 33, /*!< GPIO33, input and output */ + GPIO_NUM_34 = 34, /*!< GPIO34, input and output */ + GPIO_NUM_35 = 35, /*!< GPIO35, input and output */ + GPIO_NUM_36 = 36, /*!< GPIO36, input and output */ + GPIO_NUM_37 = 37, /*!< GPIO37, input and output */ + GPIO_NUM_38 = 38, /*!< GPIO38, input and output */ + GPIO_NUM_39 = 39, /*!< GPIO39, input and output */ + GPIO_NUM_40 = 40, /*!< GPIO40, input and output */ + GPIO_NUM_41 = 41, /*!< GPIO41, input and output */ + GPIO_NUM_42 = 42, /*!< GPIO42, input and output */ + GPIO_NUM_43 = 43, /*!< GPIO43, input and output */ + GPIO_NUM_44 = 44, /*!< GPIO44, input and output */ + GPIO_NUM_45 = 45, /*!< GPIO45, input and output */ + GPIO_NUM_46 = 46, /*!< GPIO46, input mode only */ + GPIO_NUM_MAX, +} gpio_num_t; diff --git a/emulator/src/components/hdw-dac/hdw-dac.c b/emulator/src/components/hdw-dac/hdw-dac.c index 4013b8be3..c52f26c5c 100644 --- a/emulator/src/components/hdw-dac/hdw-dac.c +++ b/emulator/src/components/hdw-dac/hdw-dac.c @@ -16,6 +16,7 @@ //============================================================================== fnDacCallback_t dacCb = NULL; +static bool shdn = false; //============================================================================== // Functions @@ -24,9 +25,11 @@ fnDacCallback_t dacCb = NULL; /** * @brief Initialize the DAC * + * @param channel + * @param shdn_gpio * @param cb */ -void initDac(fnDacCallback_t cb) +void initDac(dac_channel_mask_t channel, gpio_num_t shdn_gpio, fnDacCallback_t cb) { // TODO dacCb = cb; @@ -93,7 +96,14 @@ void dacHandleSoundOutput(short* out, int framesp, short numChannels) // Copy the same sample to each channel for (int j = 0; j < numChannels; j++) { - out[i * numChannels + j] = samp; + if (shdn) + { + out[i * numChannels + j] = 0; + } + else + { + out[i * numChannels + j] = samp; + } } } } @@ -104,3 +114,13 @@ void dacHandleSoundOutput(short* out, int framesp, short numChannels) } } } + +/** + * @brief Set the shutdown state of the DAC + * + * @param shutdown true to shut down the DAC, false to enable it + */ +void setDacShutdown(bool shutdown) +{ + shdn = shutdown; +} diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index bab1cadb1..46411021c 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -1,4 +1,23 @@ menu "Swadge Configuration" + + choice BUILD_HARDWARE + prompt "Select the hardware" + default HARDWARE_GUNSHIP + help + Select the hardware being built for + config HARDWARE_WAVEBIRD + bool "HARDWARE_WAVEBIRD" + help + Build for the Squarewavebird Swadge (2023) + config HARDWARE_GUNSHIP + bool "HARDWARE_GUNSHIP" + help + Build for the Gunship Swadge (2024) + config HARDWARE_HOTDOG + bool "DEBUG_OUTPUT_UART" + help + Build for the Hotdog Swadge (2025) + endchoice choice DEBUG_OUTPUT_TYPE prompt "Select Debug Output" default DEBUG_OUTPUT_USB diff --git a/main/swadge2024.c b/main/swadge2024.c index e350f4798..3192a8ea3 100644 --- a/main/swadge2024.c +++ b/main/swadge2024.c @@ -174,6 +174,7 @@ #include #include #include +#include #include "advanced_usb_control.h" #include "shapes.h" @@ -194,6 +195,28 @@ #define RTC_DATA_ATTR #endif +// Define hardware-specific GPIOs +#if defined(CONFIG_HARDWARE_WAVEBIRD) || defined(CONFIG_HARDWARE_GUNSHIP) + #define GPIO_SAO_1 GPIO_NUM_17 + #define GPIO_SAO_2 GPIO_NUM_18 + + #define GPIO_BTN_UP GPIO_NUM_0 + #define GPIO_BTN_DOWN GPIO_NUM_4 + #define GPIO_BTN_LEFT GPIO_NUM_2 + #define GPIO_BTN_RIGHT GPIO_NUM_1 + +#elif defined(CONFIG_HARDWARE_HOTDOG) + #define GPIO_SAO_1 GPIO_NUM_40 + #define GPIO_SAO_2 GPIO_NUM_42 + + #define GPIO_BTN_UP GPIO_NUM_1 + #define GPIO_BTN_DOWN GPIO_NUM_4 + #define GPIO_BTN_LEFT GPIO_NUM_0 + #define GPIO_BTN_RIGHT GPIO_NUM_2 +#else + #error "Define what hardware is being built for" +#endif + //============================================================================== // Variables //============================================================================== @@ -237,10 +260,14 @@ static void dacCallback(uint8_t* samples, int16_t len); */ void app_main(void) { + // Make sure there isn't a pin conflict + if (GPIO_SAO_1 != GPIO_NUM_17) + { #ifdef CONFIG_DEBUG_OUTPUT_UART_SAO - // Redirect UART if configured to do so - uart_set_pin(UART_NUM_0, GPIO_NUM_18, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + // Redirect UART if configured and able + uart_set_pin(UART_NUM_0, GPIO_SAO_1, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); #endif + } // Init NVS. Do this first to get test mode status and crashwrap logs initNvs(true); @@ -296,14 +323,14 @@ void app_main(void) // Init buttons and touch pads gpio_num_t pushButtons[] = { - GPIO_NUM_0, // Up - GPIO_NUM_4, // Down - GPIO_NUM_2, // Left - GPIO_NUM_1, // Right - GPIO_NUM_16, // A - GPIO_NUM_15, // B - GPIO_NUM_8, // Start - GPIO_NUM_5 // Select + GPIO_BTN_UP, // Up + GPIO_BTN_DOWN, // Down + GPIO_BTN_LEFT, // Left + GPIO_BTN_RIGHT, // Right + GPIO_NUM_16, // A + GPIO_NUM_15, // B + GPIO_NUM_8, // Start + GPIO_NUM_5 // Select }; touch_pad_t touchPads[] = { TOUCH_PAD_NUM9, // GPIO_NUM_9 @@ -332,13 +359,15 @@ void app_main(void) initShapes(); // Initialize the RGB LEDs - initLeds(GPIO_NUM_39, -#ifdef CONFIG_DEBUG_OUTPUT_UART_SAO - GPIO_NUM_NC, -#else - GPIO_NUM_18, + gpio_num_t ledMirrorGpio = GPIO_NUM_NC; +#ifndef CONFIG_DEBUG_OUTPUT_UART_SAO + if (GPIO_SAO_2 != GPIO_NUM_18) + { + ledMirrorGpio = GPIO_SAO_2; + } #endif - getLedBrightnessSetting()); + + initLeds(GPIO_NUM_39, ledMirrorGpio, getLedBrightnessSetting()); // Initialize optional peripherals, depending on the mode's requests initOptionalPeripherals(); @@ -529,13 +558,12 @@ static void initOptionalPeripherals(void) #if defined(CONFIG_SOUND_OUTPUT_SPEAKER) // Initialize the speaker. The DAC uses the same DMA controller for continuous output, // so it can't be initialized at the same time as the microphone - initDac(dacCallback); + initDac(DAC_CHANNEL_MASK_CH0, // GPIO_NUM_17 + GPIO_NUM_18, dacCallback); dacStart(); initGlobalMidiPlayer(); #elif defined(CONFIG_SOUND_OUTPUT_BUZZER) - // Init buzzer. This must be called before initMic() - initBuzzer(GPIO_NUM_40, LEDC_TIMER_0, LEDC_CHANNEL_0, // - GPIO_NUM_42, LEDC_TIMER_1, LEDC_CHANNEL_1, getBgmVolumeSetting(), getSfxVolumeSetting()); + #error "Buzzer is no longer supported, get with the times!" #endif } diff --git a/makefile b/makefile index 6a90b21e7..bb5a38e30 100644 --- a/makefile +++ b/makefile @@ -196,6 +196,7 @@ DEFINES_LIST = \ CONFIG_ESP_SYSTEM_PANIC=y\ CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME=y\ CONFIG_DEBUG_OUTPUT_USB=y\ + CONFIG_HARDWARE_GUNSHIP=y \ CONFIG_IDF_TARGET_ESP32S2=y \ SOC_RMT_CHANNELS_PER_GROUP=4 \ SOC_TOUCH_SENSOR_NUM=15 \ diff --git a/sdkconfig b/sdkconfig index b672d7f78..0f2bc6a7d 100644 --- a/sdkconfig +++ b/sdkconfig @@ -453,6 +453,9 @@ CONFIG_PARTITION_TABLE_MD5=y # # Swadge Configuration # +# CONFIG_HARDWARE_WAVEBIRD is not set +CONFIG_HARDWARE_GUNSHIP=y +# CONFIG_HARDWARE_HOTDOG is not set CONFIG_DEBUG_OUTPUT_USB=y # CONFIG_DEBUG_OUTPUT_UART is not set # CONFIG_DEBUG_OUTPUT_UART_SAO is not set From d213798c03f89ce83051828f3a441eaac43bc88c Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 23 Sep 2024 01:23:27 +0000 Subject: [PATCH 35/54] Fix warnings --- emulator/src-lib/gifenc.c | 11 +++++++++++ emulator/src/components/hdw-nvs/hdw-nvs.c | 1 + emulator/src/emu_cnfs.c | 1 + emulator/src/emu_main.c | 1 + emulator/src/emu_utils.c | 5 ++++- emulator/src/extensions/emu_args.c | 6 ------ emulator/src/extensions/emu_ext.h | 16 ++++++++++++++++ emulator/src/extensions/replay/ext_replay.c | 7 ++++++- 8 files changed, 40 insertions(+), 8 deletions(-) diff --git a/emulator/src-lib/gifenc.c b/emulator/src-lib/gifenc.c index a0cb1d01b..1be3c39f6 100644 --- a/emulator/src-lib/gifenc.c +++ b/emulator/src-lib/gifenc.c @@ -12,6 +12,13 @@ #include #endif +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic push +#endif +#ifdef __GNUC__ + #pragma GCC diagnostic ignored "-Wunused-result" +#endif + /* helper to write a little-endian 16-bit number portably */ #define write_num(fd, n) write((fd), (uint8_t []) {(n) & 0xFF, (n) >> 8}, 2) @@ -321,3 +328,7 @@ ge_close_gif(ge_GIF* gif) close(gif->fd); free(gif); } + +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic pop +#endif diff --git a/emulator/src/components/hdw-nvs/hdw-nvs.c b/emulator/src/components/hdw-nvs/hdw-nvs.c index 5ecc8461a..df216025c 100644 --- a/emulator/src/components/hdw-nvs/hdw-nvs.c +++ b/emulator/src/components/hdw-nvs/hdw-nvs.c @@ -14,6 +14,7 @@ #include #include "hdw-nvs.h" +#include "hdw-nvs_emu.h" #include "cJSON.h" #include "emu_main.h" #include "emu_utils.h" diff --git a/emulator/src/emu_cnfs.c b/emulator/src/emu_cnfs.c index b2f3b723d..5eb46406c 100644 --- a/emulator/src/emu_cnfs.c +++ b/emulator/src/emu_cnfs.c @@ -8,6 +8,7 @@ #include "esp_heap_caps.h" #include "esp_log.h" +#include "cnfs.h" #include "cnfs_image.h" //============================================================================== diff --git a/emulator/src/emu_main.c b/emulator/src/emu_main.c index 67bc055c3..73dbc26e4 100644 --- a/emulator/src/emu_main.c +++ b/emulator/src/emu_main.c @@ -66,6 +66,7 @@ #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" #pragma GCC diagnostic ignored "-Wjump-misses-init" #pragma GCC diagnostic ignored "-Wundef" + #pragma GCC diagnostic ignored "-Wredundant-decls" #endif // Make it so we don't need to include any other C files in our build. diff --git a/emulator/src/emu_utils.c b/emulator/src/emu_utils.c index 293d432e4..ffc5e97a7 100644 --- a/emulator/src/emu_utils.c +++ b/emulator/src/emu_utils.c @@ -83,7 +83,10 @@ bool makeDirs(const char* path) char tmp[1024]; strncpy(tmp, buffer, sizeof(tmp)); tmp[sizeof(tmp) - 1] = '\0'; - readlink(tmp, buffer, sizeof(buffer) - strlen(buffer) - 1); + if (-1 == readlink(tmp, buffer, sizeof(buffer) - strlen(buffer) - 1)) + { + printf("readlink error: %s", strerror(errno)); + } // printf("Symbolic Link: %s --> %s\n", tmp, buffer); } #endif diff --git a/emulator/src/extensions/emu_args.c b/emulator/src/extensions/emu_args.c index 28ec60ea1..c2c20ff8b 100644 --- a/emulator/src/extensions/emu_args.c +++ b/emulator/src/extensions/emu_args.c @@ -393,12 +393,6 @@ static bool handleArgument(const char* optName, const char* arg, int optVal) printf("ERR: Invalid integer value '%s'\n", arg); return false; } - - if (emulatorArgs.seed < 0) - { - printf("ERR: Seed value must be greater than or equal to 0\n"); - return false; - } } } else if (argShowFps == optName) diff --git a/emulator/src/extensions/emu_ext.h b/emulator/src/extensions/emu_ext.h index e27bfb5de..da3008ac6 100644 --- a/emulator/src/extensions/emu_ext.h +++ b/emulator/src/extensions/emu_ext.h @@ -70,8 +70,24 @@ #include #include #include "emu_args.h" + +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic push +#endif +#ifdef __GNUC__ + #pragma GCC diagnostic ignored "-Wmissing-prototypes" + #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" + #pragma GCC diagnostic ignored "-Wjump-misses-init" + #pragma GCC diagnostic ignored "-Wundef" + #pragma GCC diagnostic ignored "-Wredundant-decls" +#endif + #include "CNFG.h" +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic pop +#endif + //============================================================================== // Structs //============================================================================== diff --git a/emulator/src/extensions/replay/ext_replay.c b/emulator/src/extensions/replay/ext_replay.c index 2fbddd970..1d1b7bb30 100644 --- a/emulator/src/extensions/replay/ext_replay.c +++ b/emulator/src/extensions/replay/ext_replay.c @@ -794,8 +794,13 @@ void startRecording(const char* filename) replayEntry_t modeEntry = { .type = SET_MODE, .time = 0, - .modeName = emulatorArgs.startMode, + .modeName = NULL, }; + + char* tmpStr = malloc(strlen(emulatorArgs.startMode) + 1); + strncpy(tmpStr, emulatorArgs.startMode, strlen(emulatorArgs.startMode) + 1); + modeEntry.modeName = tmpStr; + writeEntry(&modeEntry); } From 99635104a1ab274d205b726cf9b51f28682e581e Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Thu, 26 Sep 2024 11:40:04 +0000 Subject: [PATCH 36/54] Work on more precise timings --- main/modes/games/swadgeHero/mode_swadgeHero.h | 3 +- main/modes/games/swadgeHero/swadgeHero_game.c | 143 ++++++++++-------- 2 files changed, 80 insertions(+), 66 deletions(-) diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index 0147059e9..f68bee99a 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -50,7 +50,8 @@ typedef struct typedef struct { int32_t note; - int32_t timer; + int32_t headTimeUs; + int32_t tailTimeUs; int32_t headPosY; int32_t tailPosY; bool held; diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 28ce1ccd1..8c87c52fa 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -11,8 +11,6 @@ #define HIT_BAR 16 #define GAME_NOTE_RADIUS 8 -#define TRAVEL_US_PER_PX ((TRAVEL_TIME_US) / (TFT_HEIGHT - HIT_BAR + (2 * GAME_NOTE_RADIUS))) - //============================================================================== // Const Variables //============================================================================== @@ -163,21 +161,29 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) // Check if the game note should be spawned now to reach the hit bar in time if (songUs + TRAVEL_TIME_US >= nextEventUs) { + // TODO math is getting weird somewhere, maybe use ms, not us? + printf("EVT: %d\n", nextEventUs); + // Spawn an game note shGameNote_t* ni = heap_caps_calloc(1, sizeof(shGameNote_t), MALLOC_CAP_SPIRAM); ni->note = sh->chartNotes[sh->currentChartNote].note; - ni->headPosY = TFT_HEIGHT + (GAME_NOTE_RADIUS * 2); + + // Start the game note offscreen + ni->headPosY = TFT_HEIGHT; + + // Save when the note should be hit + ni->headTimeUs = nextEventUs; // If this is a hold note if (sh->chartNotes[sh->currentChartNote].hold) { - // Figure out at what microsecond the tail ends - int32_t tailUs = MIDI_TICKS_TO_US(sh->chartNotes[sh->currentChartNote].hold, player->tempo, - player->reader.division); - // Convert the time to a number of pixels - int32_t tailPx = tailUs / TRAVEL_US_PER_PX; - // Add the length pixels to the head to get the tail - ni->tailPosY = ni->headPosY + tailPx; + // Start the tail offscreen too + ni->tailPosY = TFT_HEIGHT; + + // Save when the tail ends + int32_t tailTick + = sh->chartNotes[sh->currentChartNote].tick + sh->chartNotes[sh->currentChartNote].hold; + ni->tailTimeUs = MIDI_TICKS_TO_US(tailTick, player->tempo, player->reader.division); } else { @@ -185,9 +191,6 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) ni->tailPosY = -1; } - // Start the timer at zero - ni->timer = 0; - // Push into the list of game notes push(&sh->gameNotes, ni); @@ -211,58 +214,69 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) // Get a reference shGameNote_t* gameNote = gameNoteNode->val; - // Run this game note's timer - gameNote->timer += elapsedUs; - while (gameNote->timer >= TRAVEL_US_PER_PX) + // Update note position + gameNote->headPosY = (((TFT_HEIGHT - HIT_BAR) * (gameNote->headTimeUs - songUs)) / TRAVEL_TIME_US) + HIT_BAR; + // Update tail position if there is a hold + if (-1 != gameNote->tailPosY) { - gameNote->timer -= TRAVEL_US_PER_PX; - - bool shouldRemove = false; - - // Move the whole game note up - if (!gameNote->held) - { - gameNote->headPosY--; - if (gameNote->tailPosY >= 0) - { - gameNote->tailPosY--; - } - - // If it's off screen - if (gameNote->headPosY < -GAME_NOTE_RADIUS && (gameNote->tailPosY < 0)) - { - // Mark it for removal - shouldRemove = true; - } - } - else // The game note is being held - { - // Only move the tail position - if (gameNote->tailPosY >= HIT_BAR) - { - gameNote->tailPosY--; - } - - // If the tail finished - if (gameNote->tailPosY < HIT_BAR) - { - // Mark it for removal - shouldRemove = true; - } - } + gameNote->tailPosY + = (((TFT_HEIGHT - HIT_BAR) * (gameNote->tailTimeUs - songUs)) / TRAVEL_TIME_US) + HIT_BAR; + } - // If the game note should be removed - if (shouldRemove) - { - // Remove this game note - free(gameNoteNode->val); - removeEntry(&sh->gameNotes, gameNoteNode); + // TODO remove note - // Stop the while timer loop - removed = true; - break; - } - } + // Run this game note's timer + // gameNote->timer += elapsedUs; + // while (gameNote->timer >= TRAVEL_US_PER_PX) + // { + // gameNote->timer -= TRAVEL_US_PER_PX; + + // bool shouldRemove = false; + + // // Move the whole game note up + // if (!gameNote->held) + // { + // gameNote->headPosY--; + // if (gameNote->tailPosY >= 0) + // { + // gameNote->tailPosY--; + // } + + // // If it's off screen + // if (gameNote->headPosY < -GAME_NOTE_RADIUS && (gameNote->tailPosY < 0)) + // { + // // Mark it for removal + // shouldRemove = true; + // } + // } + // else // The game note is being held + // { + // // Only move the tail position + // if (gameNote->tailPosY >= HIT_BAR) + // { + // gameNote->tailPosY--; + // } + + // // If the tail finished + // if (gameNote->tailPosY < HIT_BAR) + // { + // // Mark it for removal + // shouldRemove = true; + // } + // } + + // // If the game note should be removed + // if (shouldRemove) + // { + // // Remove this game note + // free(gameNoteNode->val); + // removeEntry(&sh->gameNotes, gameNoteNode); + + // // Stop the while timer loop + // removed = true; + // break; + // } + // } // If an game note was removed if (removed) @@ -361,7 +375,7 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) { // Find how off the timing is int32_t pxOff = ABS(HIT_BAR - gameNote->headPosY); - int32_t usOff = pxOff * TRAVEL_US_PER_PX; + int32_t usOff = 0; // TODO pxOff * TRAVEL_US_PER_PX; printf("%" PRId32 " us off\n", usOff); // Check if this button hit a note @@ -437,8 +451,7 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // printf("touch center: %" PRId32 ", intensity: %" PRId32 "\n", centerVal, intensityVal); // } // else - // { - // printf("no touch\n"); + // {hold // } // // Get the acceleration From ab3cc2a55deeab5aafb48e31da7295bed8db45b6 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Fri, 27 Sep 2024 00:41:43 +0000 Subject: [PATCH 37/54] Add fret lines based on MIDI tempo Add status and timing text Fix note input timings --- main/modes/games/swadgeHero/mode_swadgeHero.c | 4 + main/modes/games/swadgeHero/mode_swadgeHero.h | 14 + main/modes/games/swadgeHero/swadgeHero_game.c | 281 ++++++++++++------ 3 files changed, 214 insertions(+), 85 deletions(-) diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index 6b4aec1f8..c51b6ebbd 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -201,6 +201,10 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) { free(val); } + while ((val = pop(&shv->fretLines))) + { + free(val); + } break; } case SH_GAME_END: diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index f68bee99a..f114bd929 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -57,6 +57,12 @@ typedef struct bool held; } shGameNote_t; +typedef struct +{ + int32_t headTimeUs; + int32_t headPosY; +} shFretLine_t; + typedef struct { // Font and menu @@ -83,11 +89,19 @@ typedef struct paletteColor_t const* colors; buttonBit_t const* noteToBtn; int32_t const* btnToNote; + int32_t tempo; + + // Fret line data + list_t fretLines; + int32_t lastFretLineUs; // Drawing data list_t gameNotes; buttonBit_t btnState; int32_t numFrets; + const char* hitText; + const char* timingText; + int32_t textTimerUs; } shVars_t; //============================================================================== diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 8c87c52fa..3c2a6de53 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -11,6 +11,8 @@ #define HIT_BAR 16 #define GAME_NOTE_RADIUS 8 +#define SH_TEXT_TIME 500000 + //============================================================================== // Const Variables //============================================================================== @@ -19,14 +21,24 @@ static const paletteColor_t colors_e[] = {c020, c004, c420, c222}; static const buttonBit_t noteToBtn_e[] = {PB_LEFT, PB_RIGHT, PB_B, PB_A}; static const int32_t btnToNote_e[] = {-1, -1, 0, 1, 3, 2}; -static const paletteColor_t colors_m[] = {c020, c400, c550, c004, c420, c222}; -static const buttonBit_t noteToBtn_m[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; -static const int32_t btnToNote_m[] = {2, 1, 0, 3, 5, 4}; +static const paletteColor_t colors_m[] = {c020, c550, c004, c420, c222}; +static const buttonBit_t noteToBtn_m[] = {PB_LEFT, PB_UP, PB_RIGHT, PB_B, PB_A}; +static const int32_t btnToNote_m[] = {1, -1, 0, 2, 4, 3}; static const paletteColor_t colors_h[] = {c020, c400, c550, c004, c420, c222}; static const buttonBit_t noteToBtn_h[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; static const int32_t btnToNote_h[] = {2, 1, 0, 3, 5, 4}; +static const char hit_fantastic[] = "Fantastic"; +static const char hit_marvelous[] = "Marvelous"; +static const char hit_great[] = "Great"; +static const char hit_decent[] = "Decent"; +static const char hit_way_off[] = "Way Off"; +static const char hit_miss[] = "Miss"; + +static const char hit_early[] = "Early"; +static const char hit_late[] = "Late"; + //============================================================================== // Functions //============================================================================== @@ -65,10 +77,26 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) // Load the MIDI file loadMidiFile(midi, &sh->midiSong, true); globalMidiPlayerPlaySong(&sh->midiSong, MIDI_BGM); + + // Seek to load the tempo + midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); + midiSeek(player, 1000); + sh->tempo = player->tempo; + + // Return to the beginning of the song globalMidiPlayerPauseAll(); + midiSeek(player, 0); // Set the lead-in timer sh->leadInUs = TRAVEL_TIME_US; + + // Start with one fret line at t=0 + sh->lastFretLineUs = 0; + + shFretLine_t* fretLine = heap_caps_calloc(1, sizeof(shFretLine_t), MALLOC_CAP_SPIRAM); + fretLine->headPosY = TFT_HEIGHT + 1; + fretLine->headTimeUs = sh->lastFretLineUs; + push(&sh->fretLines, fretLine); } /** @@ -107,7 +135,7 @@ uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size) { sh->chartNotes[nIdx].note &= 0x7F; - // Use the hold time to see when this note ends + // Use the hold time to see how long this note is held sh->chartNotes[nIdx].hold = (data[dIdx + 0] << 8) | // (data[dIdx + 1] << 0); dIdx += 2; @@ -137,6 +165,12 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) } } + // Run a timer for pop-up text + if (sh->textTimerUs > 0) + { + sh->textTimerUs -= elapsedUs; + } + // Get a reference to the player midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); @@ -151,6 +185,18 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) songUs = SAMPLES_TO_US(player->sampleCount); } + // Generate fret lines based on tempo + int32_t nextFretLineUs = sh->lastFretLineUs + sh->tempo; + if (songUs + TRAVEL_TIME_US >= nextFretLineUs) + { + sh->lastFretLineUs += sh->tempo; + + shFretLine_t* fretLine = heap_caps_calloc(1, sizeof(shFretLine_t), MALLOC_CAP_SPIRAM); + fretLine->headPosY = TFT_HEIGHT + 1; + fretLine->headTimeUs = sh->lastFretLineUs; + push(&sh->fretLines, fretLine); + } + // Check events until one hasn't happened yet or the song ends while (sh->currentChartNote < sh->numChartNotes) { @@ -161,15 +207,12 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) // Check if the game note should be spawned now to reach the hit bar in time if (songUs + TRAVEL_TIME_US >= nextEventUs) { - // TODO math is getting weird somewhere, maybe use ms, not us? - printf("EVT: %d\n", nextEventUs); - // Spawn an game note shGameNote_t* ni = heap_caps_calloc(1, sizeof(shGameNote_t), MALLOC_CAP_SPIRAM); ni->note = sh->chartNotes[sh->currentChartNote].note; // Start the game note offscreen - ni->headPosY = TFT_HEIGHT; + ni->headPosY = TFT_HEIGHT + (GAME_NOTE_RADIUS / 2); // Save when the note should be hit ni->headTimeUs = nextEventUs; @@ -178,17 +221,18 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) if (sh->chartNotes[sh->currentChartNote].hold) { // Start the tail offscreen too - ni->tailPosY = TFT_HEIGHT; + ni->tailPosY = TFT_HEIGHT + (GAME_NOTE_RADIUS / 2); // Save when the tail ends - int32_t tailTick - = sh->chartNotes[sh->currentChartNote].tick + sh->chartNotes[sh->currentChartNote].hold; + int32_t tailTick = sh->chartNotes[sh->currentChartNote].tick + // + sh->chartNotes[sh->currentChartNote].hold; ni->tailTimeUs = MIDI_TICKS_TO_US(tailTick, player->tempo, player->reader.division); } else { // No tail - ni->tailPosY = -1; + ni->tailPosY = -1; + ni->tailTimeUs = -1; } // Push into the list of game notes @@ -204,10 +248,7 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) } } - // Track if an game note was removed - bool removed = false; - - // Run all the game note timers + // Update note positions based on song position node_t* gameNoteNode = sh->gameNotes.first; while (gameNoteNode) { @@ -215,7 +256,18 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) shGameNote_t* gameNote = gameNoteNode->val; // Update note position - gameNote->headPosY = (((TFT_HEIGHT - HIT_BAR) * (gameNote->headTimeUs - songUs)) / TRAVEL_TIME_US) + HIT_BAR; + if (gameNote->held) + { + // Held notes stick at the hit bar + gameNote->headPosY = HIT_BAR; + } + else + { + // Moving notes follow this formula + gameNote->headPosY + = (((TFT_HEIGHT - HIT_BAR) * (gameNote->headTimeUs - songUs)) / TRAVEL_TIME_US) + HIT_BAR; + } + // Update tail position if there is a hold if (-1 != gameNote->tailPosY) { @@ -223,66 +275,48 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) = (((TFT_HEIGHT - HIT_BAR) * (gameNote->tailTimeUs - songUs)) / TRAVEL_TIME_US) + HIT_BAR; } - // TODO remove note - - // Run this game note's timer - // gameNote->timer += elapsedUs; - // while (gameNote->timer >= TRAVEL_US_PER_PX) - // { - // gameNote->timer -= TRAVEL_US_PER_PX; - - // bool shouldRemove = false; - - // // Move the whole game note up - // if (!gameNote->held) - // { - // gameNote->headPosY--; - // if (gameNote->tailPosY >= 0) - // { - // gameNote->tailPosY--; - // } - - // // If it's off screen - // if (gameNote->headPosY < -GAME_NOTE_RADIUS && (gameNote->tailPosY < 0)) - // { - // // Mark it for removal - // shouldRemove = true; - // } - // } - // else // The game note is being held - // { - // // Only move the tail position - // if (gameNote->tailPosY >= HIT_BAR) - // { - // gameNote->tailPosY--; - // } - - // // If the tail finished - // if (gameNote->tailPosY < HIT_BAR) - // { - // // Mark it for removal - // shouldRemove = true; - // } - // } - - // // If the game note should be removed - // if (shouldRemove) - // { - // // Remove this game note - // free(gameNoteNode->val); - // removeEntry(&sh->gameNotes, gameNoteNode); - - // // Stop the while timer loop - // removed = true; - // break; - // } - // } - - // If an game note was removed - if (removed) + // Check if the note should be removed + bool shouldRemove = false; + if (-1 != gameNote->tailPosY) { - // Stop iterating through notes - break; + // There is a tail + if (gameNote->held) + { + if (gameNote->tailPosY < HIT_BAR) + { + // Note is currently being held, tail reached the bar + shouldRemove = true; + } + } + else if (gameNote->tailPosY < 0) + { + // Note is not held, tail is offscreen + shouldRemove = true; + } + } + else if (gameNote->headPosY < -GAME_NOTE_RADIUS) + { + // There is no tail and the whole note is offscreen + shouldRemove = true; + } + + // If the game note should be removed + if (shouldRemove) + { + // Save the node to remove before iterating + node_t* toRemove = gameNoteNode; + + // Iterate to the next + gameNoteNode = gameNoteNode->next; + + // Remove the game note + free(toRemove->val); + removeEntry(&sh->gameNotes, toRemove); + + // Note that it was missed + sh->hitText = hit_miss; + // Set a timer to not show the text forever + sh->textTimerUs = SH_TEXT_TIME; } else { @@ -290,6 +324,30 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) gameNoteNode = gameNoteNode->next; } } + + // Iterate through fret lines + node_t* fretLineNode = sh->fretLines.first; + while (fretLineNode) + { + shFretLine_t* fretLine = fretLineNode->val; + + // Update positions + fretLine->headPosY = (((TFT_HEIGHT - HIT_BAR) * (fretLine->headTimeUs - songUs)) / TRAVEL_TIME_US) + HIT_BAR; + + // Remove if off screen + if (fretLine->headPosY < 0) + { + node_t* toRemove = fretLineNode; + fretLineNode = fretLineNode->next; + free(toRemove->val); + removeEntry(&sh->fretLines, toRemove); + } + else + { + // Iterate normally + fretLineNode = fretLineNode->next; + } + } } /** @@ -302,6 +360,15 @@ void shDrawGame(shVars_t* sh) // Clear the display clearPxTft(); + // Draw fret lines first + node_t* fretLineNode = sh->fretLines.first; + while (fretLineNode) + { + shFretLine_t* fretLine = fretLineNode->val; + drawLineFast(0, fretLine->headPosY, TFT_WIDTH, fretLine->headPosY, c111); + fretLineNode = fretLineNode->next; + } + // Draw the target area drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); for (int32_t i = 0; i < sh->numFrets; i++) @@ -341,6 +408,20 @@ void shDrawGame(shVars_t* sh) } } + // Draw text + if (sh->textTimerUs > 0) + { + int16_t tWidth = textWidth(&sh->ibm, sh->hitText); + drawText(&sh->ibm, c555, sh->hitText, (TFT_WIDTH - tWidth) / 2, 100); + + if ((hit_fantastic != sh->hitText) && (hit_miss != sh->hitText)) + { + tWidth = textWidth(&sh->ibm, sh->timingText); + drawText(&sh->ibm, sh->timingText == hit_early ? c335 : c533, sh->timingText, (TFT_WIDTH - tWidth) / 2, + 100 + sh->ibm.height + 16); + } + } + // Set LEDs led_t leds[CONFIG_NUM_LEDS] = {0}; // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) @@ -360,10 +441,24 @@ void shDrawGame(shVars_t* sh) */ void shGameInput(shVars_t* sh, buttonEvt_t* evt) { + // Get the position of the song and when the next event is, in ms + int32_t songUs; + if (sh->leadInUs > 0) + { + songUs = -sh->leadInUs; + } + else + { + songUs = SAMPLES_TO_US(globalMidiPlayerGet(MIDI_BGM)->sampleCount); + } + sh->btnState = evt->state; if (evt->down) { + // See if the button press matches a note + bool noteMatch = false; + // Iterate through all currently shown game notes node_t* gameNoteNode = sh->gameNotes.first; while (gameNoteNode) @@ -373,10 +468,16 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // If the game note matches the button if (gameNote->note == sh->btnToNote[31 - __builtin_clz(evt->button)]) { + noteMatch = true; + // Find how off the timing is - int32_t pxOff = ABS(HIT_BAR - gameNote->headPosY); - int32_t usOff = 0; // TODO pxOff * TRAVEL_US_PER_PX; - printf("%" PRId32 " us off\n", usOff); + int32_t usOff = (songUs - gameNote->headTimeUs); + + // Set either early or late + sh->timingText = (usOff > 0) ? hit_late : hit_early; + + // Find the absolute difference + usOff = ABS(usOff); // Check if this button hit a note bool gameNoteHit = false; @@ -384,34 +485,37 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // Classify the time off if (usOff < 21500) { - printf(" Fantastic\n"); + sh->hitText = hit_fantastic; gameNoteHit = true; } else if (usOff < 43000) { - printf(" Marvelous\n"); + sh->hitText = hit_marvelous; gameNoteHit = true; } else if (usOff < 102000) { - printf(" Great\n"); + sh->hitText = hit_great; gameNoteHit = true; } else if (usOff < 135000) { - printf(" Decent\n"); + sh->hitText = hit_decent; gameNoteHit = true; } else if (usOff < 180000) { - printf(" Way Off\n"); + sh->hitText = hit_way_off; gameNoteHit = true; } else { - printf(" MISS\n"); + sh->hitText = hit_miss; } + // Set a timer to not show the text forever + sh->textTimerUs = SH_TEXT_TIME; + // If it was close enough to hit if (gameNoteHit) { @@ -438,6 +542,13 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // Iterate to the next game note gameNoteNode = gameNoteNode->next; } + + if (false == noteMatch) + { + // Total miss + sh->hitText = hit_miss; + // TODO ignore buttons not used for this difficulty + } } else { From e2a40897ec516d31a7c1e58bb6623957e218cac0 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Fri, 27 Sep 2024 00:45:13 +0000 Subject: [PATCH 38/54] Squashed commit of the following: commit 426906c3dc3ebcb7a718082c71128a46a44cfc8c Author: gelakinetic Date: Thu Sep 26 22:38:01 2024 +0000 Fix HARDWARE_HOTDOG config commit bec74512044becb1cf8ab0e37c0fd3d7f196ac11 Author: gelakinetic Date: Sun Sep 22 21:44:55 2024 -0400 Add Initial Swadge Hero (#297) Add hardware type config Fix or suppress warnings commit 45147c245fa35357658e949ec609b611f8a5906f Author: Dylan Whichard Date: Sun Sep 22 18:33:53 2024 -0700 Add direct MIDI-opening support for OSX (#284) --- emulator/resources/Info.plist | 25 ++ emulator/src/extensions/midi/ext_midi.c | 315 +++++++++++++++++++++++- main/Kconfig.projbuild | 2 +- makefile | 1 + tools/mac_events_test/.gitignore | 4 + tools/mac_events_test/Info.plist | 55 +++++ tools/mac_events_test/main.c | 274 +++++++++++++++++++++ tools/mac_events_test/makefile | 218 ++++++++++++++++ 8 files changed, 883 insertions(+), 11 deletions(-) create mode 100644 tools/mac_events_test/.gitignore create mode 100644 tools/mac_events_test/Info.plist create mode 100644 tools/mac_events_test/main.c create mode 100644 tools/mac_events_test/makefile diff --git a/emulator/resources/Info.plist b/emulator/resources/Info.plist index 9afd51a71..17ab3507d 100644 --- a/emulator/resources/Info.plist +++ b/emulator/resources/Info.plist @@ -31,5 +31,30 @@ DISPLAY :0 + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + midi + mid + kar + + CFBundleTypeIconFile + SwadgeEmulator.icns + CFBundleTypeNames + MIDI File + CFBundleTypeOSTypes + + Midi + + LSItemContentTypes + + public.midi-audio + + CFBundleTypeRole + Viewer + + diff --git a/emulator/src/extensions/midi/ext_midi.c b/emulator/src/extensions/midi/ext_midi.c index ca39ac292..bb6e8a4e7 100644 --- a/emulator/src/extensions/midi/ext_midi.c +++ b/emulator/src/extensions/midi/ext_midi.c @@ -1,6 +1,7 @@ #include "ext_midi.h" #include "emu_ext.h" #include "emu_main.h" +#include "emu_utils.h" #include "hdw-nvs_emu.h" #include "emu_cnfs.h" @@ -8,12 +9,50 @@ #include "mode_synth.h" #include +#include +#include + +#ifdef EMU_MACOS + // Used to handle DocumentOpen event that OSX uses instead of Just Putting It In Argv + #include +#endif + +//============================================================================== +// Types +//============================================================================== + +#ifdef EMU_MACOS +typedef void (*MacOpenFileCb)(const char* path); + +typedef struct +{ + EventHandlerUPP globalEventHandler; + AEEventHandlerUPP appleEventHandler; + EventHandlerRef globalEventHandlerRef; + MacOpenFileCb openFileCallback; +} MacOpenFileHandler; +#endif //============================================================================== // Function Prototypes //============================================================================== static bool midiInitCb(emuArgs_t* emuArgs); +static void midiPreFrameCb(uint64_t frame); +static bool midiInjectFile(const char* path); + +#ifdef EMU_MACOS +// Exists but isn't declared in the headers +extern Boolean ConvertEventRefToEventRecord(EventRef, EventRecord*); + +bool installMacOpenFileHandler(MacOpenFileHandler* handlerRef, MacOpenFileCb callback); +void checkForEventsMacOpenFileHandler(MacOpenFileHandler* handlerRef, uint32_t millis); +void uninstallMacOpenFileHandler(MacOpenFileHandler* handlerRef); + +static pascal OSErr handleOpenDocumentEvent(const AppleEvent* event, AppleEvent* reply, SRefCon handlerRef); +static OSStatus globalEventHandler(EventHandlerCallRef handler, EventRef event, void* data); +static void doFileOpenCb(const char* path); +#endif //============================================================================== // Variables @@ -22,7 +61,7 @@ static bool midiInitCb(emuArgs_t* emuArgs); emuExtension_t midiEmuExtension = { .name = "midi", .fnInitCb = midiInitCb, - .fnPreFrameCb = NULL, + .fnPreFrameCb = midiPreFrameCb, .fnPostFrameCb = NULL, .fnKeyCb = NULL, .fnMouseMoveCb = NULL, @@ -30,6 +69,17 @@ emuExtension_t midiEmuExtension = { .fnRenderCb = NULL, }; +static char midiPathBuffer[1024]; +static const char* midiFile = NULL; + +#ifdef EMU_MACOS +static const EventTypeSpec eventTypes[] = {{.eventClass = kEventClassAppleEvent, .eventKind = kEventAppleEvent}}; + +static bool handlerInstalled = false; +static bool emulatorStarted = false; +static MacOpenFileHandler macOpenFileHandler; +#endif + //============================================================================== // Functions //============================================================================== @@ -38,14 +88,20 @@ static bool midiInitCb(emuArgs_t* emuArgs) { if (emuArgs->midiFile) { - printf("Opening MIDI file: %s\n", emuArgs->midiFile); - if (emuCnfsInjectFile(emuArgs->midiFile, emuArgs->midiFile)) - { - emuInjectNvs32("storage", "synth_playmode", 1); - emuInjectNvsBlob("storage", "synth_lastsong", strlen(emuArgs->midiFile), emuArgs->midiFile); - emulatorSetSwadgeModeByName(synthMode.modeName); - } - else + midiFile = emuArgs->midiFile; + } + +#ifdef EMU_MACOS + handlerInstalled = installMacOpenFileHandler(&macOpenFileHandler, doFileOpenCb); + // Wait up to 100ms for an event at startup + checkForEventsMacOpenFileHandler(&macOpenFileHandler, 100); + emulatorStarted = true; +#endif + + if (midiFile) + { + printf("Opening MIDI file: %s\n", midiFile); + if (!midiInjectFile(midiFile)) { printf("Could not read MIDI file!\n"); emulatorQuit(); @@ -56,4 +112,243 @@ static bool midiInitCb(emuArgs_t* emuArgs) } return false; -} \ No newline at end of file +} + +void midiPreFrameCb(uint64_t frame) +{ +#ifdef EMU_MACOS + if (handlerInstalled) + { + // Wait up to 5ms for an event, is that enough? + checkForEventsMacOpenFileHandler(&macOpenFileHandler, 5); + } +#endif +} + +static bool midiInjectFile(const char* path) +{ + if (emuCnfsInjectFile(midiFile, midiFile)) + { + emuInjectNvs32("storage", "synth_playmode", 1); + emuInjectNvsBlob("storage", "synth_lastsong", strlen(midiFile), midiFile); + emulatorSetSwadgeModeByName(synthMode.modeName); + + return true; + } + else + { + return false; + } +} + +#ifdef EMU_MACOS +static void doFileOpenCb(const char* path) +{ + strncpy(midiPathBuffer, path, sizeof(midiPathBuffer)); + midiFile = midiPathBuffer; + + if (emulatorStarted) + { + if (!midiInjectFile(path)) + { + printf("Error: could not read MIDI file %s!\n", path); + } + } +} + +bool installMacOpenFileHandler(MacOpenFileHandler* handlerRef, MacOpenFileCb callback) +{ + // Init handler + handlerRef->appleEventHandler = NULL; + handlerRef->globalEventHandler = NULL; + handlerRef->openFileCallback = callback; + + // Install handler + handlerRef->appleEventHandler = NewAEEventHandlerUPP(handleOpenDocumentEvent); + OSStatus result = AEInstallEventHandler(kCoreEventClass, kAEOpenDocuments, handlerRef->appleEventHandler, + (SRefCon)handlerRef, false); + + if (result != noErr) + { + printf("Failed to install OpenDocument handler\n"); + uninstallMacOpenFileHandler(handlerRef); + return false; + } + + // Install the application-level handler + handlerRef->globalEventHandler = NewEventHandlerUPP(globalEventHandler); + result = InstallApplicationEventHandler(handlerRef->globalEventHandler, 1, eventTypes, NULL, + &handlerRef->globalEventHandlerRef); + + if (result != noErr) + { + printf("Failed to install global event handler\n"); + uninstallMacOpenFileHandler(handlerRef); + return false; + } + + // Handler successfully installed + return true; +} + +// Runs the event loop and waits for up to the specified time +// The callback will be called for any events that are recevied +void checkForEventsMacOpenFileHandler(MacOpenFileHandler* handlerRef, uint32_t millis) +{ + EventTimeout timeout = millis / 1000.0; + while (1) + { + EventRef eventRef; + + OSErr result = ReceiveNextEvent(1, eventTypes, timeout, kEventRemoveFromQueue, &eventRef); + + if (result == eventLoopTimedOutErr) + { + // printf("No event received after timeout\n"); + break; + } + else if (result == noErr) + { + result = SendEventToEventTarget(eventRef, GetEventDispatcherTarget()); + ReleaseEvent(eventRef); + if (result != noErr) + { + if (result == eventNotHandledErr) + { + // printf("Got eventNotHandledErr from SendEventToEventTarget()\n"); + } + else + { + printf("Error in SendEventToEventTarget(): %d %s\n", result, strerror(result)); + break; + } + } + } + else + { + printf("Error in ReceiveNextEvent()\n"); + break; + } + } +} + +// Uninstalls and deletes +void uninstallMacOpenFileHandler(MacOpenFileHandler* handlerRef) +{ + if (handlerRef != NULL) + { + if (handlerRef->appleEventHandler != NULL) + { + DisposeAEEventHandlerUPP(handlerRef->appleEventHandler); + handlerRef->appleEventHandler = NULL; + } + + if (handlerRef->globalEventHandler != NULL) + { + DisposeEventHandlerUPP(handlerRef->globalEventHandler); + handlerRef->globalEventHandler = NULL; + } + handlerRef->openFileCallback = NULL; + } +} + +static pascal OSErr handleOpenDocumentEvent(const AppleEvent* event, AppleEvent* reply, SRefCon handlerRefArg) +{ + MacOpenFileHandler* handlerRef = (MacOpenFileHandler*)handlerRefArg; + + AEDescList docList; + OSErr result = AEGetParamDesc(event, keyDirectObject, typeAEList, &docList); + + if (result != noErr) + { + return result; + } + + long docCount = 0; + result = AECountItems(&docList, &docCount); + if (result != noErr) + { + return result; + } + + char buffer[2048]; + + // Yup, it's zero-indexed. Weird. + for (long i = 1; i <= docCount; i++) + { + AEKeyword keyword; + DescType docType; + Size docSize; + + result = AEGetNthPtr(&docList, i, typeFileURL, &keyword, &docType, &buffer, sizeof(buffer), &docSize); + + if (result != noErr) + { + return result; + } + + CFURLRef docUrlRef = CFURLCreateWithBytes(NULL, (UInt8*)buffer, docSize, kCFStringEncodingUTF8, NULL); + + if (docUrlRef != NULL) + { + CFStringRef docStringRef = CFURLCopyFileSystemPath(docUrlRef, kCFURLPOSIXPathStyle); + if (docStringRef != NULL) + { + char pathBuffer[1024]; + if (CFStringGetFileSystemRepresentation(docStringRef, pathBuffer, sizeof(pathBuffer))) + { + handlerRef->openFileCallback(pathBuffer); + } + CFRelease(docStringRef); + } + CFRelease(docUrlRef); + } + } + + return AEDisposeDesc(&docList); +} + +static OSStatus globalEventHandler(EventHandlerCallRef handler, EventRef event, void* data) +{ + bool inQueue = IsEventInQueue(GetMainEventQueue(), event); + + if (inQueue) + { + RetainEvent(event); + RemoveEventFromQueue(GetMainEventQueue(), event); + } + + EventRecord record; + ConvertEventRefToEventRecord(event, &record); + char messageStr[5] = { + (char)((record.message >> 24) & 0xff), + (char)((record.message >> 16) & 0xff), + (char)((record.message >> 8) & 0xff), + (char)((record.message) & 0xff), + 0, + }; + printf("globalEventHandler() what=%hu, message=%s\n", record.what, messageStr); + OSStatus result = AEProcessAppleEvent(&record); + + if (result == errAEEventNotHandled) + { + printf("errAEEventNotHandled in globalEventHandler()\n"); + } + else if (result != noErr) + { + printf("globalEventHandler() AEProcessAppleEvent() returned ERROR: %d (%s)\n", result, strerror(result)); + } + else + { + printf("globalEventHandler() AEProcessAppleEvent() success!\n"); + } + + if (inQueue) + { + ReleaseEvent(event); + } + + return noErr; +} + +#endif diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 46411021c..5ae5769f8 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -14,7 +14,7 @@ menu "Swadge Configuration" help Build for the Gunship Swadge (2024) config HARDWARE_HOTDOG - bool "DEBUG_OUTPUT_UART" + bool "HARDWARE_HOTDOG" help Build for the Hotdog Swadge (2025) endchoice diff --git a/makefile b/makefile index bb5a38e30..83837a17b 100644 --- a/makefile +++ b/makefile @@ -282,6 +282,7 @@ LIBRARY_FLAGS += \ -static-libstdc++ else LIBRARY_FLAGS += \ + -framework Carbon \ -framework Foundation \ -framework CoreFoundation \ -framework CoreMIDI \ diff --git a/tools/mac_events_test/.gitignore b/tools/mac_events_test/.gitignore new file mode 100644 index 000000000..5e53c4401 --- /dev/null +++ b/tools/mac_events_test/.gitignore @@ -0,0 +1,4 @@ +test +test.icns +test.app + diff --git a/tools/mac_events_test/Info.plist b/tools/mac_events_test/Info.plist new file mode 100644 index 000000000..3484970e6 --- /dev/null +++ b/tools/mac_events_test/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + test + CFBundleGetInfoString + Public Domain + CFBundleIconFile + test.icns + CFBundleIdentifier + com.dylwhich.osxargtest + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + test + CFBundleVersion + 1.0 + NSHumanReadableCopyright + Public Domain + LSMinimumSystemVersion + 10 + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + midi + mid + kar + + CFBundleTypeIconFile + test.icns + CFBundleTypeNames + MIDI File + CFBundleTypeOSTypes + + Midi + + LSItemContentTypes + + public.midi-audio + + CFBundleTypeRole + Viewer + + + + diff --git a/tools/mac_events_test/main.c b/tools/mac_events_test/main.c new file mode 100644 index 000000000..4d886ef3f --- /dev/null +++ b/tools/mac_events_test/main.c @@ -0,0 +1,274 @@ +#include +#include +#include +#include +#include + +#include + +extern Boolean ConvertEventRefToEventRecord(EventRef, EventRecord*); + +#define LOG_FILE "/Users/dylwhich/events.log" + +#ifdef LOG_FILE +#define LOG(...) fprintf(logFile, __VA_ARGS__) +#else +#define LOG printf +#endif + +typedef void (*MacOpenFileCb)(const char* path); + +typedef struct +{ + EventHandlerUPP globalEventHandler; + AEEventHandlerUPP appleEventHandler; + MacOpenFileCb openFileCallback; + EventHandlerRef globalEventHandlerRef; +} MacOpenFileHandler; + +bool installMacOpenFileHandler(MacOpenFileHandler* handlerRef, MacOpenFileCb callback); +void checkForEventsMacOpenFileHandler(MacOpenFileHandler* handlerRef, uint32_t millis); +void uninstallMacOpenFileHandler(MacOpenFileHandler* handlerRef); + +static pascal OSErr handleOpenDocumentEvent(const AppleEvent* event, AppleEvent* reply, SRefCon handlerRef); +static OSStatus globalEventHandler(EventHandlerCallRef handler, EventRef event, void* data); +static void doOpenCb(const char* path); + +FILE* logFile; + +const EventTypeSpec eventTypes[] = {{.eventClass = kEventClassAppleEvent, .eventKind = kEventAppleEvent}}; + +static void doOpenCb(const char* path) +{ + LOG("Got file: %s\n", path); +} + +int main(int argc, char** argv) +{ +#ifdef LOG_FILE + logFile = fopen(LOG_FILE, "a"); +#endif + + LOG("\nStarting up\n"); + + LOG("Installing event handler...\n"); + MacOpenFileHandler handler; + + bool installed = installMacOpenFileHandler(&handler, doOpenCb); + + if (installed) + { + LOG("OK!\n"); + checkForEventsMacOpenFileHandler(&handler, 500); + uninstallMacOpenFileHandler(&handler); + LOG("Done processing events\n"); + } + else + { + LOG("FAILED!"); + return 1; + } + + return 0; +} + +bool installMacOpenFileHandler(MacOpenFileHandler* handlerRef, MacOpenFileCb callback) +{ + // Init handler + handlerRef->appleEventHandler = NULL; + handlerRef->globalEventHandler = NULL; + handlerRef->openFileCallback = callback; + + // Install handler + handlerRef->appleEventHandler = NewAEEventHandlerUPP(handleOpenDocumentEvent); + OSStatus result = AEInstallEventHandler(kCoreEventClass, kAEOpenDocuments, handlerRef->appleEventHandler, (SRefCon)handlerRef, false); + + if (result != noErr) + { + LOG("Failed to install OpenDocument handler\n"); + uninstallMacOpenFileHandler(handlerRef); + return false; + } + + // Install the application-level handler + handlerRef->globalEventHandler = NewEventHandlerUPP(globalEventHandler); + result = InstallApplicationEventHandler(handlerRef->globalEventHandler, 1, eventTypes, NULL, &handlerRef->globalEventHandlerRef); + + if (result != noErr) + { + LOG("Failed to install global event handler\n"); + uninstallMacOpenFileHandler(handlerRef); + return false; + } + + // Handler successfully installed + return true; +} + +// Runs the event loop and waits for up to the specified time +// The callback will be called for any events that are recevied +void checkForEventsMacOpenFileHandler(MacOpenFileHandler* handlerRef, uint32_t millis) +{ + EventTimeout timeout = millis / 1000.0; + while (1) + { + EventRef eventRef; + + OSErr result = ReceiveNextEvent(1, eventTypes, timeout, kEventRemoveFromQueue, &eventRef); + + if (result == eventLoopTimedOutErr) + { + LOG("No event received after timeout\n"); + break; + } + else if (result == noErr) + { + LOG("Got an event!\n"); + result = SendEventToEventTarget(eventRef, GetEventDispatcherTarget()); + ReleaseEvent(eventRef); + if (result != noErr) + { + if (result == eventNotHandledErr) + { + LOG("Got eventNotHandledErr from SendEventToEventTarget()\n"); + } + else + { + LOG("Error in SendEventToEventTarget(): %d %s\n", result, strerror(result)); + break; + } + } + } + else + { + LOG("Error in ReceiveNextEvent()\n"); + break; + } + } +} + +// Uninstalls and deletes +void uninstallMacOpenFileHandler(MacOpenFileHandler* handlerRef) +{ + if (handlerRef != NULL) + { + if (handlerRef->appleEventHandler != NULL) + { + DisposeAEEventHandlerUPP(handlerRef->appleEventHandler); + handlerRef->appleEventHandler = NULL; + } + + if (handlerRef->globalEventHandler != NULL) + { + DisposeEventHandlerUPP(handlerRef->globalEventHandler); + handlerRef->globalEventHandler = NULL; + } + handlerRef->openFileCallback = NULL; + } +} + +static pascal OSErr handleOpenDocumentEvent(const AppleEvent* event, AppleEvent* reply, SRefCon handlerRefArg) +{ + LOG("Hey, handleOpenDocumentEvent() got called!\n"); + MacOpenFileHandler* handlerRef = (MacOpenFileHandler*)handlerRefArg; + + AEDescList docList; + OSErr result = AEGetParamDesc(event, keyDirectObject, typeAEList, &docList); + + if (result != noErr) + { + return result; + } + + long docCount = 0; + result = AECountItems(&docList, &docCount); + if (result != noErr) + { + return result; + } + + char buffer[2048]; + + // Yup, it's zero-indexed. Weird. + for (long i = 1; i <= docCount; i++) + { + AEKeyword keyword; + DescType docType; + Size docSize; + + result = AEGetNthPtr(&docList, i, typeFileURL, &keyword, &docType, &buffer, sizeof(buffer), &docSize); + + if (result != noErr) + { + LOG("Error getting event doc index %ld\n", i); + return result; + } + + CFURLRef docUrlRef = CFURLCreateWithBytes(NULL, (UInt8*)buffer, docSize, kCFStringEncodingUTF8, NULL); + + if (docUrlRef != NULL) + { + CFStringRef docStringRef = CFURLCopyFileSystemPath(docUrlRef, kCFURLPOSIXPathStyle); + if (docStringRef != NULL) + { + char pathBuffer[1024]; + if (CFStringGetFileSystemRepresentation(docStringRef, pathBuffer, sizeof(pathBuffer))) + { + handlerRef->openFileCallback(pathBuffer); + LOG("Successfully handled event?\n"); + } + CFRelease(docStringRef); + } + CFRelease(docUrlRef); + } + } + + return AEDisposeDesc(&docList); +} + +static OSStatus globalEventHandler(EventHandlerCallRef handler, EventRef event, void* data) +{ + LOG("globalEventHandler()!!!!!!\n"); + + bool inQueue = IsEventInQueue(GetMainEventQueue(), event); + + if (inQueue) + { + RetainEvent(event); + RemoveEventFromQueue(GetMainEventQueue(), event); + } + + EventRecord record; + ConvertEventRefToEventRecord(event, &record); + char messageStr[5] = + { + (char)((record.message >> 24) & 0xff), + (char)((record.message >> 16) & 0xff), + (char)((record.message >> 8) & 0xff), + (char)((record.message) & 0xff), + 0, + }; + LOG("globalEventHandler() what=%hu, message=%s\n", record.what, messageStr); + OSStatus result = AEProcessAppleEvent(&record); + + if (result == errAEEventNotHandled) + { + LOG("errAEEventNotHandled in globalEventHandler()\n"); + } + else if (result != noErr) + { + LOG("globalEventHandler() AEProcessAppleEvent() returned ERROR: %d (%s)\n", result, strerror(result)); + } + else + { + LOG("globalEventHandler() AEProcessAppleEvent() success!\n"); + } + + if (inQueue) + { + ReleaseEvent(event); + } + + return noErr; +} + diff --git a/tools/mac_events_test/makefile b/tools/mac_events_test/makefile new file mode 100644 index 000000000..00225e3ad --- /dev/null +++ b/tools/mac_events_test/makefile @@ -0,0 +1,218 @@ +# Makefile by Adam, 2022 + +################################################################################ +# What OS we're compiling on +################################################################################ + +IS_WSL := 0 +ifeq ($(OS),Windows_NT) + HOST_OS = Windows +else + UNAME_S := $(shell uname -s) + ifeq ($(UNAME_S),Linux) + HOST_OS = Linux + # Check if this is WSL. 0 for not WSL, 1 for WSL + IS_WSL := $(shell uname -a | grep -i WSL | wc -l) + else ifeq ($(UNAME_S),Darwin) + HOST_OS = Darwin + endif +endif + +################################################################################ +# Programs to use +################################################################################ + +CC = gcc + +FIND:=find + + +################################################################################ +# Source Files +################################################################################ + +SOURCES := main.c + +################################################################################ +# Includes +################################################################################ + +################################################################################ +# Compiler Flags +################################################################################ + +# These are flags for the compiler, all files +CFLAGS = \ + -c \ + -g \ + -fdiagnostics-color=always \ + -ffunction-sections \ + -fdata-sections \ + -gdwarf-4 \ + -ggdb \ + -O2 \ + -fno-jump-tables \ + -finline-functions \ + -std=gnu17 + +# Required for OpenGL and some other libraries +CFLAGS += \ + -mmacosx-version-min=10.0 + +# These are warning flags that the IDF uses +CFLAGS_WARNINGS = \ + -Wall \ + -Werror=all \ + -Wno-error=unused-function \ + -Wno-error=unused-variable \ + -Wno-error=deprecated-declarations \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-sign-compare \ + -Wno-enum-conversion \ + -Wno-error=unused-but-set-variable + +# These are warning flags that I like +CFLAGS_WARNINGS_EXTRA = \ + -Wundef \ + -Wformat=2 \ + -Winvalid-pch \ + -Wmissing-format-attribute \ + -Wmissing-include-dirs \ + -Wpointer-arith \ + -Wunused-local-typedefs \ + -Wuninitialized \ + -Wshadow \ + -Wredundant-decls \ + -Wswitch \ + -Wcast-align \ + -Wformat-nonliteral \ + -Wno-switch-default \ + -Wunused \ + -Wunused-macros \ + -Wmissing-declarations \ + -Wmissing-prototypes \ + -Wcast-qual \ + -Wno-switch \ + -Wunused-result \ +# -Wstrict-prototypes \ +# -Wpedantic \ +# -Wconversion \ +# -Wsign-conversion \ +# -Wdouble-promotion + +################################################################################ +# Defines +################################################################################ + +# Create a variable with the git hash and branch name +GIT_HASH = \"$(shell git rev-parse --short=7 HEAD)\" + +# Used by the ESP SDK +DEFINES_LIST = \ + _GNU_SOURCE \ + _POSIX_READER_WRITER_LOCKS + +DEFINES = $(patsubst %, -D%, $(DEFINES_LIST)) + +################################################################################ +# Output Objects +################################################################################ + +# This is the directory in which object files will be stored +OBJ_DIR = obj + +# This is a list of objects to build +OBJECTS = $(patsubst %.c, $(OBJ_DIR)/%.o, $(SOURCES)) + +################################################################################ +# Linker options +################################################################################ + +# This is a list of libraries to include. Order doesn't matter + +#LIBS = m X11 GL pthread Xext Xinerama +LIBS = + +# These are directories to look for library files in +LIB_DIRS = + +# On MacOS we need to ensure that X11 is added for OpenGL and some others +#ifeq ($(HOST_OS),Darwin) +# LIB_DIRS = /opt/X11/lib +#endif + +# This combines the flags for the linker to find and use libraries +LIBRARY_FLAGS = $(patsubst %, -L%, $(LIB_DIRS)) $(patsubst %, -l%, $(LIBS)) \ + -ggdb + +LIBRARY_FLAGS += \ + -framework Carbon + +################################################################################ +# Build Filenames +################################################################################ + +# These are the files to build +EXECUTABLE = test +BUNDLE = test.app +ICONS = test.icns + +################################################################################ +# Targets for Building +################################################################################ + +# This list of targets do not build files which match their name +.PHONY: all bundle clean print-% + +# Build the executable +all: $(EXECUTABLE) + +# To build the main file, you have to compile the objects +$(EXECUTABLE): $(OBJECTS) + $(CC) $(OBJECTS) $(LIBRARY_FLAGS) -o $@ + +# This compiles each c file into an o file +./$(OBJ_DIR)/%.o: ./%.c + @mkdir -p $(@D) # This creates a directory before building an object in it. + $(CC) $(CFLAGS) $(CFLAGS_WARNINGS) $(CFLAGS_WARNINGS_EXTRA) $(DEFINES) $(INC) $< -o $@ + +bundle: $(BUNDLE) + +$(BUNDLE): $(EXECUTABLE) $(ICONS) Info.plist + rm -rf $(BUNDLE) + mkdir -p $(BUNDLE)/Contents/{MacOS,Resources,libs} + cp Info.plist $(BUNDLE)/Contents/Info.plist + echo "APPLTest" > $(BUNDLE)/Contents/PkgInfo + cp $(ICONS) $(BUNDLE)/Contents/Resources/ + vtool -set-build-version macos 10.0 10.0 -replace -output $(BUNDLE)/Contents/MacOS/test $(EXECUTABLE) + dylibbundler -od -b -x ./$(BUNDLE)/Contents/MacOS/test -d ./$(BUNDLE)/Contents/libs/ + + +$(ICONS): ../../emulator/resources/icon.png + rm -rf test.iconset + mkdir -p test.iconset + sips -z 16 16 $< --out test.iconset/icon_16x16.png + sips -z 32 32 $< --out test.iconset/icon_16x16@2x.png + sips -z 32 32 $< --out test.iconset/icon_32x32.png + sips -z 64 64 $< --out test.iconset/icon_32x32@2x.png + sips -z 128 128 $< --out test.iconset/icon_128x128.png + sips -z 256 256 $< --out test.iconset/icon_128x128@2x.png + sips -z 256 256 $< --out test.iconset/icon_256x256.png + sips -z 512 512 $< --out test.iconset/icon_256x256@2x.png + sips -z 512 512 $< --out test.iconset/icon_512x512.png + sips -z 1024 1024 $< --out test.iconset/icon_512x512@2x.png + iconutil -c icns -o $(ICONS) test.iconset + rm -r test.iconset + +# This cleans emulator files +clean: + -@rm -rf $(OBJECTS) $(EXECUTABLE) $(ICONS) test.iconset $(BUNDLE) + + +################################################################################ +# Firmware targets +################################################################################ + +# Print any value from this makefile +print-% : ; @echo $* = $($*) From 777cde95a0d5f99c7ffaa03e36e45940542741f3 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sat, 28 Sep 2024 23:20:31 +0000 Subject: [PATCH 39/54] Replace circles with icons --- assets/swadgeHero/sh_a.png | Bin 0 -> 699 bytes assets/swadgeHero/sh_b.png | Bin 0 -> 740 bytes assets/swadgeHero/sh_down.png | Bin 0 -> 632 bytes assets/swadgeHero/sh_left.png | Bin 0 -> 683 bytes assets/swadgeHero/sh_right.png | Bin 0 -> 677 bytes assets/swadgeHero/sh_up.png | Bin 0 -> 635 bytes main/modes/games/swadgeHero/mode_swadgeHero.c | 11 ++++ main/modes/games/swadgeHero/mode_swadgeHero.h | 2 + main/modes/games/swadgeHero/swadgeHero_game.c | 54 +++++++++++------- 9 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 assets/swadgeHero/sh_a.png create mode 100644 assets/swadgeHero/sh_b.png create mode 100644 assets/swadgeHero/sh_down.png create mode 100644 assets/swadgeHero/sh_left.png create mode 100644 assets/swadgeHero/sh_right.png create mode 100644 assets/swadgeHero/sh_up.png diff --git a/assets/swadgeHero/sh_a.png b/assets/swadgeHero/sh_a.png new file mode 100644 index 0000000000000000000000000000000000000000..b1f21fc59a604584f46eabaac0821a0684b4f078 GIT binary patch literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^5-yI%Y+bmbd&Q$8kAgjd)s@sA9)2h+0bs&RhsUulC*358WhAJsRuO!8T3ylX<}nwWc+TANa=*S)@> z6*y&2(9WQ*Ax$1ztrUBgd2MOgcAv9p-TleE`wSMG2{3cYGnwbJKdtzpqS&2D>*_Wy zlm2*M+U>VDW%|@V9+uXc)e_f;l9a@fRIB8o zR3OD*WME{XYhbBsWD#OyWMyh(Wn!*vU|?lnFh?iK3q?b2eoAIqC2kFG3m<(0YS4h& zP?DLOT3nKtTY#>|7-&4i5)tni$v{1lAU(nPX(i=}MX3zs<>h*rdD+Fui3O>8`9g@Dy(uaG?Cj<? zeLii`dHwa@hgm;%9XNkf-c8l~b{yB^5TGMfOI#yLQW8s2t&)pUffR$0fsu)>fu*jI zMTn7+m8p@HiMh6cft7*59GxgH6b-rgDVb@NxHY^jeDn>dK?80>NoHW*m&5C5{#PvHc$CKrRYcz2QPJMmFK@~JC3Q#YEF&y!<87<H=O_7|KoVx|nU+m_@4h1xw`977~7m!5EEY6=iwxfr(bsz2M&2Fs?7 z@AmVA(#`h7iW;P-97`6R!ErjMaiv0Nf|`fu4DKLSv8E8l;El4^joVC)70d39`=IT3 zLgSmKeV+u|*F&4OdF{A2;kNAM+Twx?8R2;+j4yjz7v$c3_ov!s_p#NJpQg9gP2NC6cwc)I$ztaD0e0syz@ B-UR>v literal 0 HcmV?d00001 diff --git a/assets/swadgeHero/sh_left.png b/assets/swadgeHero/sh_left.png new file mode 100644 index 0000000000000000000000000000000000000000..beb86a1bbc30561dec289c5331e1fcf2139c796d GIT binary patch literal 683 zcmeAS@N?(olHy`uVBq!ia0vp^5=brF`9e-#phY$b%^5y^k|1mu7cYsc0O!9VjvA0V$CL5S}-C z{-6HzP`%8*;`}~I1>d&RVxoMbr169%G`Pv!x)!Rzu@u7rLK`hh>?+% zsgae5xwe6Um4U$=ohUC94Y~O#nQ4`{HM}i+^bM#%18ze}W^QV6Nn&mRx*lVo@eoTy zyk{f>^+>t*I;7bhncr0V4trO$q6BL!5%;OXk;vd$@?2>=-p B@*DsF literal 0 HcmV?d00001 diff --git a/assets/swadgeHero/sh_right.png b/assets/swadgeHero/sh_right.png new file mode 100644 index 0000000000000000000000000000000000000000..43e384a3e14cf493c2c1d1a813dae4fc18aa98f6 GIT binary patch literal 677 zcmeAS@N?(olHy`uVBq!ia0vp^5gIhokm zney|cXU#IPwG{;_EG^Yow8&-QLKjCzX*M?I%1Yx^t3p<;3~_Z;VrOTrueaZ{DP`lv z6b}y#c6OH5R_|TAN)|6p2}ZG1=SOoP&dH`t-a@ zmzG?(u-MPfcFvrtTer5{xUo4f(3z8yebJ(}M~{v~Mfq`abFNu4_0_8@FJE5fC&-ZzrOtc|6eit7|?$Vj7i?^F7|fG2Bkm_XMsm#F#`kNVGw3K zp1&dmD9B#o>Fdh=f>TD!lwo$;l3buri>HfYh{Wa6ldfV+fjll3ouoP5y}Phu_uKl& zIoslAev!TE-C!Wtbu2_7Pi0bASF1_tkyEoA`aZF0PHS?ATp6-7?7O*C)MnMUedZTS zzx;Ay{@$`M*(9vLv-(iT?6XJXDulgLw~AFi&dG}QmXVOF8 z`RlpKR=z3W{pnFtHTq8rLK`hh>?+%sgae5 zxwe6Um4U$=ohUC94Y~O#nQ4`{HM}i+^bM#%18ze}W^QV6Nn&mRx*lVo@eoTyyk{f> x^+>t*I;7bhncr0V4trO$q6BL!5%;OXk;vd$@?2>`#U^P>O& literal 0 HcmV?d00001 diff --git a/assets/swadgeHero/sh_up.png b/assets/swadgeHero/sh_up.png new file mode 100644 index 0000000000000000000000000000000000000000..6c8db5f54ef6e3a5b222bd612ff7b08c72d1b017 GIT binary patch literal 635 zcmeAS@N?(olHy`uVBq!ia0vp^5lTSsJCK zOzi9~ix!#G)-rQ&Ftf24S5}6sTIJZ>%)-g3;NYO->dMT{ZeL%YvT2iVXD2Hsr<${K z^2UuR8#iirc(Aaud$+ch?An#OaG{QyTk+1FEr$*jEM3aM&28%E*L>i>lvAgcT)MR7 z)~zFt9$k6$N>Et%$)`^zu3r7}>(~GP|KESPa2n_i#w2fd7lUanMpdfpRr>`sf3r-m^Q-;}XOLBoi?Vc`$q6F8^y|JeN(|ME_rb9{MZ9d^G(oosqaSL&x_~)nV|JlestSkgqRoI#QY~?fC`m~yNwrEYN(E93Mg~SEx(1fIMiwDP zMpmXqRwm}!1_o9J26J?xyihdc=BH$)RpQq0w(!w6pau=N4JDbmsl_FUxdrHYjDf~O zED`aZkqp!$3DOgspH@mmtT}V`<;yxP!WTttDnm{r-UW| D$5h*O literal 0 HcmV?d00001 diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index c51b6ebbd..1b5a1715b 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -75,6 +75,12 @@ static void shEnterMode(void) loadFont("righteous_150.font", &shv->righteous, false); loadFont("rodin_eb.font", &shv->rodin, false); + const char* icons[] = {"sh_left.wsg", "sh_down.wsg", "sh_up.wsg", "sh_right.wsg", "sh_b.wsg", "sh_a.wsg"}; + for (int32_t i = 0; i < ARRAY_SIZE(shv->icons); i++) + { + loadWsg(icons[i], &shv->icons[i], true); + } + // Show initial menu shChangeScreen(shv, SH_MENU); } @@ -92,6 +98,11 @@ static void shExitMode(void) freeFont(&shv->rodin); freeFont(&shv->righteous); + for (int32_t i = 0; i < ARRAY_SIZE(shv->icons); i++) + { + freeWsg(&shv->icons[i]); + } + // Free mode memory free(shv); } diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index f114bd929..8a097b3e3 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -89,6 +89,7 @@ typedef struct paletteColor_t const* colors; buttonBit_t const* noteToBtn; int32_t const* btnToNote; + int32_t const* noteToIcon; int32_t tempo; // Fret line data @@ -102,6 +103,7 @@ typedef struct const char* hitText; const char* timingText; int32_t textTimerUs; + wsg_t icons[6]; } shVars_t; //============================================================================== diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 3c2a6de53..8f53d4028 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -9,7 +9,8 @@ //============================================================================== #define HIT_BAR 16 -#define GAME_NOTE_RADIUS 8 +#define GAME_NOTE_HEIGHT 16 +#define GAME_NOTE_WIDTH 24 #define SH_TEXT_TIME 500000 @@ -17,17 +18,20 @@ // Const Variables //============================================================================== -static const paletteColor_t colors_e[] = {c020, c004, c420, c222}; +static const paletteColor_t colors_e[] = {c500, c330, c005, c303}; static const buttonBit_t noteToBtn_e[] = {PB_LEFT, PB_RIGHT, PB_B, PB_A}; static const int32_t btnToNote_e[] = {-1, -1, 0, 1, 3, 2}; +static const int32_t noteToIcon_e[] = {0, 3, 4, 5}; -static const paletteColor_t colors_m[] = {c020, c550, c004, c420, c222}; +static const paletteColor_t colors_m[] = {c500, c033, c330, c005, c303}; static const buttonBit_t noteToBtn_m[] = {PB_LEFT, PB_UP, PB_RIGHT, PB_B, PB_A}; static const int32_t btnToNote_m[] = {1, -1, 0, 2, 4, 3}; +static const int32_t noteToIcon_m[] = {0, 2, 3, 4, 5}; -static const paletteColor_t colors_h[] = {c020, c400, c550, c004, c420, c222}; +static const paletteColor_t colors_h[] = {c500, c050, c033, c330, c005, c303}; static const buttonBit_t noteToBtn_h[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_B, PB_A}; static const int32_t btnToNote_h[] = {2, 1, 0, 3, 5, 4}; +static const int32_t noteToIcon_h[] = {0, 1, 2, 3, 4, 5}; static const char hit_fantastic[] = "Fantastic"; static const char hit_marvelous[] = "Marvelous"; @@ -57,21 +61,24 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) if (6 == sh->numFrets) { - sh->btnToNote = btnToNote_h; - sh->noteToBtn = noteToBtn_h; - sh->colors = colors_h; + sh->btnToNote = btnToNote_h; + sh->noteToBtn = noteToBtn_h; + sh->colors = colors_h; + sh->noteToIcon = noteToIcon_h; } else if (5 == sh->numFrets) { - sh->btnToNote = btnToNote_m; - sh->noteToBtn = noteToBtn_m; - sh->colors = colors_m; + sh->btnToNote = btnToNote_m; + sh->noteToBtn = noteToBtn_m; + sh->colors = colors_m; + sh->noteToIcon = noteToIcon_m; } else if (4 == sh->numFrets) { - sh->btnToNote = btnToNote_e; - sh->noteToBtn = noteToBtn_e; - sh->colors = colors_e; + sh->btnToNote = btnToNote_e; + sh->noteToBtn = noteToBtn_e; + sh->colors = colors_e; + sh->noteToIcon = noteToIcon_e; } // Load the MIDI file @@ -212,7 +219,7 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) ni->note = sh->chartNotes[sh->currentChartNote].note; // Start the game note offscreen - ni->headPosY = TFT_HEIGHT + (GAME_NOTE_RADIUS / 2); + ni->headPosY = TFT_HEIGHT + (GAME_NOTE_HEIGHT / 2); // Save when the note should be hit ni->headTimeUs = nextEventUs; @@ -221,7 +228,7 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) if (sh->chartNotes[sh->currentChartNote].hold) { // Start the tail offscreen too - ni->tailPosY = TFT_HEIGHT + (GAME_NOTE_RADIUS / 2); + ni->tailPosY = TFT_HEIGHT + (GAME_NOTE_HEIGHT / 2); // Save when the tail ends int32_t tailTick = sh->chartNotes[sh->currentChartNote].tick + // @@ -294,7 +301,7 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) shouldRemove = true; } } - else if (gameNote->headPosY < -GAME_NOTE_RADIUS) + else if (gameNote->headPosY < -(GAME_NOTE_HEIGHT / 2)) { // There is no tail and the whole note is offscreen shouldRemove = true; @@ -373,18 +380,18 @@ void shDrawGame(shVars_t* sh) drawLineFast(0, HIT_BAR, TFT_WIDTH - 1, HIT_BAR, c555); for (int32_t i = 0; i < sh->numFrets; i++) { + int32_t margin = 4; int32_t xOffset = ((i * TFT_WIDTH) / sh->numFrets) + (TFT_WIDTH / (2 * sh->numFrets)); - drawCircle(xOffset, HIT_BAR, GAME_NOTE_RADIUS + 2, c555); + drawRect(xOffset - (GAME_NOTE_WIDTH / 2) - margin, HIT_BAR - (GAME_NOTE_HEIGHT / 2) - margin, // + xOffset + (GAME_NOTE_WIDTH / 2) + margin, HIT_BAR + (GAME_NOTE_HEIGHT / 2) + margin, c555); } // Draw all the game notes node_t* gameNoteNode = sh->gameNotes.first; while (gameNoteNode) { - // Draw the game note shGameNote_t* gameNote = gameNoteNode->val; int32_t xOffset = ((gameNote->note * TFT_WIDTH) / sh->numFrets) + (TFT_WIDTH / (2 * sh->numFrets)); - drawCircleFilled(xOffset, gameNote->headPosY, GAME_NOTE_RADIUS, sh->colors[gameNote->note]); // If there is a tail if (gameNote->tailPosY >= 0) @@ -394,6 +401,10 @@ void shDrawGame(shVars_t* sh) sh->colors[gameNote->note]); } + // Draw the game note + drawWsgTile(&sh->icons[sh->noteToIcon[gameNote->note]], xOffset - (GAME_NOTE_WIDTH / 2), + gameNote->headPosY - (GAME_NOTE_HEIGHT / 2)); + // Iterate gameNoteNode = gameNoteNode->next; } @@ -403,8 +414,11 @@ void shDrawGame(shVars_t* sh) { if (sh->btnState & sh->noteToBtn[bIdx]) { + int32_t margin = 8; int32_t xOffset = ((bIdx * TFT_WIDTH) / sh->numFrets) + (TFT_WIDTH / (2 * sh->numFrets)); - drawCircleOutline(xOffset, HIT_BAR, GAME_NOTE_RADIUS + 8, 4, sh->colors[bIdx]); + drawRect(xOffset - (GAME_NOTE_WIDTH / 2) - margin, HIT_BAR - (GAME_NOTE_HEIGHT / 2) - margin, // + xOffset + (GAME_NOTE_WIDTH / 2) + margin, HIT_BAR + (GAME_NOTE_HEIGHT / 2) + margin, + sh->colors[bIdx]); } } From 10f0e82a8be3a7deb4c5271c794cac4618319943 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sun, 29 Sep 2024 00:25:08 +0000 Subject: [PATCH 40/54] Add combo, score, and fail --- main/modes/games/swadgeHero/mode_swadgeHero.h | 5 ++ main/modes/games/swadgeHero/swadgeHero_game.c | 70 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index 8a097b3e3..b1de22b2f 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -92,6 +92,11 @@ typedef struct int32_t const* noteToIcon; int32_t tempo; + // Score data + int32_t score; + int32_t failMeter; + int32_t combo; + // Fret line data list_t fretLines; int32_t lastFretLineUs; diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 8f53d4028..958953d7c 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -43,6 +43,12 @@ static const char hit_miss[] = "Miss"; static const char hit_early[] = "Early"; static const char hit_late[] = "Late"; +//============================================================================== +// Function Declarations +//============================================================================== + +static int32_t getMultiplier(shVars_t* sh); + //============================================================================== // Functions //============================================================================== @@ -104,6 +110,11 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) fretLine->headPosY = TFT_HEIGHT + 1; fretLine->headTimeUs = sh->lastFretLineUs; push(&sh->fretLines, fretLine); + + // Set score, combo, and fail + sh->score = 0; + sh->combo = 0; + sh->failMeter = 50; } /** @@ -305,6 +316,10 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) { // There is no tail and the whole note is offscreen shouldRemove = true; + + // Break the combo + sh->combo = 0; + sh->failMeter = MAX(0, sh->failMeter - 2); } // If the game note should be removed @@ -436,6 +451,28 @@ void shDrawGame(shVars_t* sh) } } + int32_t textMargin = 12; + int32_t failBarHeight = 8; + + // Draw combo when 10+ + if (sh->combo >= 10) + { + char comboText[32] = {0}; + snprintf(comboText, sizeof(comboText) - 1, "Combo: %" PRId32, sh->combo); + int16_t tWidth = textWidth(&sh->rodin, comboText); + drawText(&sh->rodin, c555, comboText, (TFT_WIDTH - tWidth - textMargin), + TFT_HEIGHT - failBarHeight - sh->rodin.height); + } + + // Always draw score + char scoreText[32] = {0}; + snprintf(scoreText, sizeof(scoreText) - 1, "%" PRId32, sh->score); + drawText(&sh->rodin, c555, scoreText, textMargin, TFT_HEIGHT - failBarHeight - sh->rodin.height); + + // Draw fail meter bar + int32_t failMeterWidth = (sh->failMeter * TFT_WIDTH) / 100; + fillDisplayArea(0, TFT_HEIGHT - failBarHeight, failMeterWidth, TFT_HEIGHT - 1, c440); + // Set LEDs led_t leds[CONFIG_NUM_LEDS] = {0}; // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) @@ -496,43 +533,60 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // Check if this button hit a note bool gameNoteHit = false; + int32_t baseScore = 0; + // Classify the time off if (usOff < 21500) { sh->hitText = hit_fantastic; gameNoteHit = true; + baseScore = 5; } else if (usOff < 43000) { sh->hitText = hit_marvelous; gameNoteHit = true; + baseScore = 4; } else if (usOff < 102000) { sh->hitText = hit_great; gameNoteHit = true; + baseScore = 3; } else if (usOff < 135000) { sh->hitText = hit_decent; gameNoteHit = true; + baseScore = 2; } else if (usOff < 180000) { sh->hitText = hit_way_off; gameNoteHit = true; + baseScore = 1; } else { sh->hitText = hit_miss; + // Break the combo + sh->combo = 0; + sh->failMeter = MAX(0, sh->failMeter - 2); } + // Increment the score + sh->score += getMultiplier(sh) * baseScore; + // Set a timer to not show the text forever sh->textTimerUs = SH_TEXT_TIME; // If it was close enough to hit if (gameNoteHit) { + // Increment the combo + sh->combo++; + sh->failMeter = MIN(100, sh->failMeter + 1); + if (gameNote->tailPosY >= 0) { // There is a tail, don't remove the note yet @@ -561,6 +615,11 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) { // Total miss sh->hitText = hit_miss; + + // Break the combo + sh->combo = 0; + sh->failMeter = MAX(0, sh->failMeter - 2); + // TODO ignore buttons not used for this difficulty } } @@ -583,3 +642,14 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // int16_t a_x, a_y, a_z; // accelGetAccelVec(&a_x, &a_y, &a_z); } + +/** + * @brief TODO + * + * @param sh + * @return int32_t + */ +static int32_t getMultiplier(shVars_t* sh) +{ + return MIN(4, (sh->combo + 10) / 10); +} \ No newline at end of file From 62e5e9359525860cfaafd0e8abdcef60c4354ca5 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sun, 29 Sep 2024 23:42:20 +0000 Subject: [PATCH 41/54] Add high score display to menu (not a separate view) Add settings to menu --- main/modes/games/swadgeHero/mode_swadgeHero.c | 20 +++- main/modes/games/swadgeHero/mode_swadgeHero.h | 4 +- main/modes/games/swadgeHero/swadgeHero_menu.c | 112 +++++++++++++++++- main/modes/games/swadgeHero/swadgeHero_menu.h | 3 + 4 files changed, 132 insertions(+), 7 deletions(-) diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index 1b5a1715b..0e058d760 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -133,7 +133,10 @@ static void shMainLoop(int64_t elapsedUs) break; } case SH_GAME_END: - case SH_HIGH_SCORES: + { + // TODO implement game end + break; + } case SH_NONE: default: { @@ -157,7 +160,10 @@ static void shMainLoop(int64_t elapsedUs) break; } case SH_GAME_END: - case SH_HIGH_SCORES: + { + // TODO implement game end + break; + } case SH_NONE: default: { @@ -219,7 +225,10 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) break; } case SH_GAME_END: - case SH_HIGH_SCORES: + { + // TODO implement game end + break; + } case SH_NONE: default: { @@ -264,7 +273,10 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) break; } case SH_GAME_END: - case SH_HIGH_SCORES: + { + // TODO implement game end + break; + } case SH_NONE: default: { diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index b1de22b2f..e77964dfc 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -17,7 +17,6 @@ typedef enum SH_MENU, SH_GAME, SH_GAME_END, - SH_HIGH_SCORES, } shScreen_t; typedef enum @@ -109,6 +108,9 @@ typedef struct const char* timingText; int32_t textTimerUs; wsg_t icons[6]; + + // High score display + list_t hsStrs; } shVars_t; //============================================================================== diff --git a/main/modes/games/swadgeHero/swadgeHero_menu.c b/main/modes/games/swadgeHero/swadgeHero_menu.c index 4285887df..182381b8a 100644 --- a/main/modes/games/swadgeHero/swadgeHero_menu.c +++ b/main/modes/games/swadgeHero/swadgeHero_menu.c @@ -5,6 +5,12 @@ #include "swadgeHero_menu.h" #include "mainMenu.h" +//============================================================================== +// Defines +//============================================================================== + +#define HS_STR_LEN 32 + //============================================================================== // Function Declarations //============================================================================== @@ -33,6 +39,16 @@ static const char strHighScores[] = "High Scores"; static const char strSettings[] = "Settings"; static const char strExit[] = "Exit"; +const char* shs_fail_key = "shs_fail"; +const char* shs_fail_label = "Song Fail: "; +const char* shs_fail_opts[] = {"On", "Off"}; +const int32_t shs_fail_vals[] = {true, false}; + +const char* shs_speed_key = "shs_speed"; +const char* shs_speed_label = "Scroll: "; +const char* shs_speed_opts[] = {"Slow", "Medium", "Fast"}; +const int32_t shs_speed_vals[] = {0, 1, 2}; + //============================================================================== // Functions //============================================================================== @@ -66,12 +82,52 @@ void shSetupMenu(shVars_t* sh) sh->menu = startSubMenu(sh->menu, strHighScores); for (int32_t sIdx = 0; sIdx < ARRAY_SIZE(shSongList); sIdx++) { - addSingleItemToMenu(sh->menu, shSongList[sIdx].name); + sh->menu = startSubMenu(sh->menu, shSongList[sIdx].name); + + // Allocate and print high score strings + // Save them all to a linked list to free later + + // TODO get high scores from NVS + + char* easyStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); + snprintf(easyStr, HS_STR_LEN - 1, "Easy: %" PRId32, 0x7FFFFFFF); + addSingleItemToMenu(sh->menu, easyStr); + push(&sh->hsStrs, easyStr); + + char* mediumStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); + snprintf(mediumStr, HS_STR_LEN - 1, "Medium: %" PRId32, 0x7FFFFFFF); + addSingleItemToMenu(sh->menu, mediumStr); + push(&sh->hsStrs, mediumStr); + + char* hardStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); + snprintf(hardStr, HS_STR_LEN - 1, "Hard: %" PRId32, 0x7FFFFFFF); + addSingleItemToMenu(sh->menu, hardStr); + push(&sh->hsStrs, hardStr); + + sh->menu = endSubMenu(sh->menu); } sh->menu = endSubMenu(sh->menu); - // TODO add settings + // Add settings sh->menu = startSubMenu(sh->menu, strSettings); + + // Setting for song fail + settingParam_t failBounds = { + .min = shs_fail_vals[0], + .max = shs_fail_vals[ARRAY_SIZE(shs_fail_vals) - 1], + }; + addSettingsOptionsItemToMenu(sh->menu, shs_fail_label, shs_fail_opts, shs_fail_vals, ARRAY_SIZE(shs_fail_vals), + &failBounds, shGetSettingFail()); + + // Setting for note speed + settingParam_t speedBounds = { + .min = shs_speed_vals[0], + .max = shs_speed_vals[ARRAY_SIZE(shs_speed_vals) - 1], + }; + addSettingsOptionsItemToMenu(sh->menu, shs_speed_label, shs_speed_opts, shs_speed_vals, ARRAY_SIZE(shs_speed_vals), + &speedBounds, shGetSettingSpeed()); + + // End settings sh->menu = endSubMenu(sh->menu); // Add exit @@ -85,6 +141,11 @@ void shSetupMenu(shVars_t* sh) */ void shTeardownMenu(shVars_t* sh) { + void* toFree; + while ((toFree = pop(&sh->hsStrs))) + { + free(toFree); + } setManiaLedsOn(sh->renderer, false); deinitMenuManiaRenderer(sh->renderer); deinitMenu(sh->menu); @@ -187,4 +248,51 @@ static void shMenuCb(const char* label, bool selected, uint32_t settingVal) } } } + else + { + if (label == shs_fail_label) + { + writeNvs32(shs_fail_key, settingVal); + } + if (label == shs_speed_label) + { + writeNvs32(shs_speed_key, settingVal); + } + } +} + +/** + * @brief TODO + * + * TODO use this somewhere + * + * @return true + * @return false + */ +bool shGetSettingFail(void) +{ + int32_t failSetting; + if (readNvs32(shs_fail_key, &failSetting)) + { + return failSetting; + } + return true; } + +/** + * @brief TODO + * + * TODO use this somewhere + * + * @return true + * @return false + */ +int32_t shGetSettingSpeed(void) +{ + int32_t speedSetting; + if (readNvs32(shs_speed_key, &speedSetting)) + { + return speedSetting; + } + return 0; +} \ No newline at end of file diff --git a/main/modes/games/swadgeHero/swadgeHero_menu.h b/main/modes/games/swadgeHero/swadgeHero_menu.h index 87cc43a96..f3109f1a6 100644 --- a/main/modes/games/swadgeHero/swadgeHero_menu.h +++ b/main/modes/games/swadgeHero/swadgeHero_menu.h @@ -6,3 +6,6 @@ void shSetupMenu(shVars_t* sh); void shTeardownMenu(shVars_t* sh); void shMenuInput(shVars_t* sh, buttonEvt_t* btn); void shMenuDraw(shVars_t* sh, int32_t elapsedUs); + +bool shGetSettingFail(void); +int32_t shGetSettingSpeed(void); From b2962c1c9d26ec055bf93a26aa504a2667be7944 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Mon, 30 Sep 2024 00:59:24 +0000 Subject: [PATCH 42/54] Add skeleton for game end screen Show game end when song ends or fail out Fix firmware build --- main/CMakeLists.txt | 1 + main/modes/games/swadgeHero/mode_swadgeHero.c | 10 +++--- main/modes/games/swadgeHero/swadgeHero_game.c | 33 +++++++++++++++--- .../games/swadgeHero/swadgeHero_gameEnd.c | 34 +++++++++++++++++++ .../games/swadgeHero/swadgeHero_gameEnd.h | 6 ++++ main/modes/games/swadgeHero/swadgeHero_menu.c | 7 ++-- 6 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 main/modes/games/swadgeHero/swadgeHero_gameEnd.c create mode 100644 main/modes/games/swadgeHero/swadgeHero_gameEnd.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 1932487b3..54fd21aed 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -49,6 +49,7 @@ idf_component_register(SRCS "asset_loaders/common/heatshrink_encoder.c" "modes/games/soko/soko_undo.c" "modes/games/swadgeHero/mode_swadgeHero.c" "modes/games/swadgeHero/swadgeHero_game.c" + "modes/games/swadgeHero/swadgeHero_gameEnd.c" "modes/games/swadgeHero/swadgeHero_menu.c" "modes/games/ultimateTTT/ultimateTTT.c" "modes/games/ultimateTTT/ultimateTTTgame.c" diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index 0e058d760..c92db9b11 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -4,6 +4,7 @@ #include "mode_swadgeHero.h" #include "swadgeHero_game.h" +#include "swadgeHero_gameEnd.h" #include "swadgeHero_menu.h" //============================================================================== @@ -134,7 +135,7 @@ static void shMainLoop(int64_t elapsedUs) } case SH_GAME_END: { - // TODO implement game end + shGameEndInput(shv, &evt); break; } case SH_NONE: @@ -161,7 +162,7 @@ static void shMainLoop(int64_t elapsedUs) } case SH_GAME_END: { - // TODO implement game end + shGameEndDraw(shv, elapsedUs); break; } case SH_NONE: @@ -207,6 +208,7 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) case SH_GAME: { // Free MIDI data + globalMidiPlayerStop(true); unloadMidiFile(&shv->midiSong); // Free chart data @@ -226,7 +228,7 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) } case SH_GAME_END: { - // TODO implement game end + // Nothing to free break; } case SH_NONE: @@ -274,7 +276,7 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) } case SH_GAME_END: { - // TODO implement game end + // Nothing to set up break; } case SH_NONE: diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 958953d7c..559987d1c 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -3,6 +3,7 @@ //============================================================================== #include "swadgeHero_game.h" +#include "swadgeHero_menu.h" //============================================================================== // Defines @@ -48,6 +49,7 @@ static const char hit_late[] = "Late"; //============================================================================== static int32_t getMultiplier(shVars_t* sh); +static void shSongOver(void); //============================================================================== // Functions @@ -89,7 +91,7 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) // Load the MIDI file loadMidiFile(midi, &sh->midiSong, true); - globalMidiPlayerPlaySong(&sh->midiSong, MIDI_BGM); + globalMidiPlayerPlaySongCb(&sh->midiSong, MIDI_BGM, shSongOver); // Seek to load the tempo midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); @@ -320,6 +322,10 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) // Break the combo sh->combo = 0; sh->failMeter = MAX(0, sh->failMeter - 2); + if (0 == sh->failMeter && shGetSettingFail()) + { + shChangeScreen(sh, SH_GAME_END); + } } // If the game note should be removed @@ -469,9 +475,11 @@ void shDrawGame(shVars_t* sh) snprintf(scoreText, sizeof(scoreText) - 1, "%" PRId32, sh->score); drawText(&sh->rodin, c555, scoreText, textMargin, TFT_HEIGHT - failBarHeight - sh->rodin.height); - // Draw fail meter bar - int32_t failMeterWidth = (sh->failMeter * TFT_WIDTH) / 100; - fillDisplayArea(0, TFT_HEIGHT - failBarHeight, failMeterWidth, TFT_HEIGHT - 1, c440); + if (shGetSettingFail()) + { // Draw fail meter bar + int32_t failMeterWidth = (sh->failMeter * TFT_WIDTH) / 100; + fillDisplayArea(0, TFT_HEIGHT - failBarHeight, failMeterWidth, TFT_HEIGHT - 1, c440); + } // Set LEDs led_t leds[CONFIG_NUM_LEDS] = {0}; @@ -572,6 +580,10 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // Break the combo sh->combo = 0; sh->failMeter = MAX(0, sh->failMeter - 2); + if (0 == sh->failMeter && shGetSettingFail()) + { + shChangeScreen(sh, SH_GAME_END); + } } // Increment the score @@ -619,6 +631,10 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // Break the combo sh->combo = 0; sh->failMeter = MAX(0, sh->failMeter - 2); + if (0 == sh->failMeter && shGetSettingFail()) + { + shChangeScreen(sh, SH_GAME_END); + } // TODO ignore buttons not used for this difficulty } @@ -652,4 +668,13 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) static int32_t getMultiplier(shVars_t* sh) { return MIN(4, (sh->combo + 10) / 10); +} + +/** + * @brief TODO + * + */ +static void shSongOver(void) +{ + shChangeScreen(getShVars(), SH_GAME_END); } \ No newline at end of file diff --git a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c new file mode 100644 index 000000000..812df8266 --- /dev/null +++ b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c @@ -0,0 +1,34 @@ +//============================================================================== +// Includes +//============================================================================== + +#include "swadgeHero_gameEnd.h" + +//============================================================================== +// Defines +//============================================================================== + +//============================================================================== +// Const Variables +//============================================================================== + +//============================================================================== +// Function Declarations +//============================================================================== + +//============================================================================== +// Functions +//============================================================================== + +void shGameEndInput(shVars_t* sh, buttonEvt_t* evt) +{ + if (evt->down) + { + shChangeScreen(sh, SH_MENU); + } +} + +void shGameEndDraw(shVars_t* sh, int32_t elapsedUs) +{ + clearPxTft(); +} \ No newline at end of file diff --git a/main/modes/games/swadgeHero/swadgeHero_gameEnd.h b/main/modes/games/swadgeHero/swadgeHero_gameEnd.h new file mode 100644 index 000000000..7ef8efdf7 --- /dev/null +++ b/main/modes/games/swadgeHero/swadgeHero_gameEnd.h @@ -0,0 +1,6 @@ +#pragma once + +#include "mode_swadgeHero.h" + +void shGameEndInput(shVars_t* sh, buttonEvt_t* evt); +void shGameEndDraw(shVars_t* sh, int32_t elapsedUs); \ No newline at end of file diff --git a/main/modes/games/swadgeHero/swadgeHero_menu.c b/main/modes/games/swadgeHero/swadgeHero_menu.c index 182381b8a..8be56a8ae 100644 --- a/main/modes/games/swadgeHero/swadgeHero_menu.c +++ b/main/modes/games/swadgeHero/swadgeHero_menu.c @@ -88,19 +88,20 @@ void shSetupMenu(shVars_t* sh) // Save them all to a linked list to free later // TODO get high scores from NVS + int32_t maxScore = 0x7FFFFFFF; char* easyStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); - snprintf(easyStr, HS_STR_LEN - 1, "Easy: %" PRId32, 0x7FFFFFFF); + snprintf(easyStr, HS_STR_LEN - 1, "Easy: %" PRId32, maxScore); addSingleItemToMenu(sh->menu, easyStr); push(&sh->hsStrs, easyStr); char* mediumStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); - snprintf(mediumStr, HS_STR_LEN - 1, "Medium: %" PRId32, 0x7FFFFFFF); + snprintf(mediumStr, HS_STR_LEN - 1, "Medium: %" PRId32, maxScore); addSingleItemToMenu(sh->menu, mediumStr); push(&sh->hsStrs, mediumStr); char* hardStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); - snprintf(hardStr, HS_STR_LEN - 1, "Hard: %" PRId32, 0x7FFFFFFF); + snprintf(hardStr, HS_STR_LEN - 1, "Hard: %" PRId32, maxScore); addSingleItemToMenu(sh->menu, hardStr); push(&sh->hsStrs, hardStr); From 09fb79b490451f5c49188ec16346f8c7502c4145 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Tue, 1 Oct 2024 01:15:01 +0000 Subject: [PATCH 43/54] Added option to seek to end of midi Safer exit from Swadge Hero game mode Read settings to RAM when game starts Use settings in gameplay Set up polling to display the fail meter at the end of the song Draw score on game end screen Add more speed options --- main/midi/midiPlayer.c | 17 +-- main/midi/midiPlayer.h | 3 +- main/modes/games/swadgeHero/mode_swadgeHero.c | 6 +- main/modes/games/swadgeHero/mode_swadgeHero.h | 7 ++ main/modes/games/swadgeHero/swadgeHero_game.c | 118 ++++++++++++------ main/modes/games/swadgeHero/swadgeHero_game.h | 4 +- .../games/swadgeHero/swadgeHero_gameEnd.c | 32 ++++- main/modes/games/swadgeHero/swadgeHero_menu.c | 4 +- 8 files changed, 134 insertions(+), 57 deletions(-) diff --git a/main/midi/midiPlayer.c b/main/midi/midiPlayer.c index 848c1d39b..2d74de2b7 100644 --- a/main/midi/midiPlayer.c +++ b/main/midi/midiPlayer.c @@ -2351,15 +2351,18 @@ void midiSeek(midiPlayer_t* player, uint32_t ticks) player->sampleCount = TICKS_TO_SAMPLES(curTick, player->tempo, player->reader.division); - stopped = !player->eventAvailable - && !(player->eventAvailable = midiNextEvent(&player->reader, &player->pendingEvent)); - if (stopped) + if ((uint32_t)-1 != ticks) { - midiSongEnd(player); - - if (endCb) + stopped = !player->eventAvailable + && !(player->eventAvailable = midiNextEvent(&player->reader, &player->pendingEvent)); + if (stopped) { - endCb(); + midiSongEnd(player); + + if (endCb) + { + endCb(); + } } } diff --git a/main/midi/midiPlayer.h b/main/midi/midiPlayer.h index 7cff75efa..2aeb9b4a1 100644 --- a/main/midi/midiPlayer.h +++ b/main/midi/midiPlayer.h @@ -965,7 +965,8 @@ void midiPause(midiPlayer_t* player, bool pause); * large MIDI files. * * @param player The MIDI player to seek on - * @param ticks The absolute number of MIDI ticks to seek to. + * @param ticks The absolute number of MIDI ticks to seek to. If this is -1, it + * will seek to the end of the song */ void midiSeek(midiPlayer_t* player, uint32_t ticks); diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index c92db9b11..f6ba56a1b 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -151,8 +151,10 @@ static void shMainLoop(int64_t elapsedUs) { case SH_GAME: { - shRunTimers(shv, elapsedUs); - shDrawGame(shv); + if (shRunTimers(shv, elapsedUs)) + { + shDrawGame(shv); + } break; } case SH_MENU: diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index e77964dfc..ebdb1a33e 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -90,11 +90,18 @@ typedef struct int32_t const* btnToNote; int32_t const* noteToIcon; int32_t tempo; + int32_t failSampleInterval; + int32_t failSampleTimer; + + // Setting data + bool failOn; + int32_t scrollTime; // Score data int32_t score; int32_t failMeter; int32_t combo; + bool gameEnd; // Fret line data list_t fretLines; diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 559987d1c..679fca5bb 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -15,6 +15,8 @@ #define SH_TEXT_TIME 500000 +#define NUM_FAIL_METER_SAMPLES 200 + //============================================================================== // Const Variables //============================================================================== @@ -50,6 +52,7 @@ static const char hit_late[] = "Late"; static int32_t getMultiplier(shVars_t* sh); static void shSongOver(void); +static void shMissNote(shVars_t* sh); //============================================================================== // Functions @@ -64,6 +67,11 @@ static void shSongOver(void); */ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) { + // Load settings + sh->failOn = shGetSettingFail(); + sh->scrollTime = shGetSettingSpeed(); + + // Load song size_t sz = 0; sh->numFrets = 1 + shLoadChartData(sh, cnfsGetFile(chart, &sz), sz); @@ -89,21 +97,29 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) sh->noteToIcon = noteToIcon_e; } + // Make sure MIDI player is initialized + initGlobalMidiPlayer(); + midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); + // Load the MIDI file loadMidiFile(midi, &sh->midiSong, true); - globalMidiPlayerPlaySongCb(&sh->midiSong, MIDI_BGM, shSongOver); - // Seek to load the tempo - midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); - midiSeek(player, 1000); - sh->tempo = player->tempo; + // Set the file, but don't play yet + midiPause(player, true); + player->sampleCount = 0; + midiSetFile(player, &sh->midiSong); - // Return to the beginning of the song - globalMidiPlayerPauseAll(); - midiSeek(player, 0); + // Seek to load the tempo and length, then reset + midiSeek(player, -1); + sh->tempo = player->tempo; + int32_t songLenUs = SAMPLES_TO_US(player->sampleCount); + globalMidiPlayerStop(true); + + sh->failSampleInterval = songLenUs / NUM_FAIL_METER_SAMPLES; + sh->failSampleTimer = 0; // Set the lead-in timer - sh->leadInUs = TRAVEL_TIME_US; + sh->leadInUs = sh->scrollTime; // Start with one fret line at t=0 sh->lastFretLineUs = 0; @@ -170,9 +186,17 @@ uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size) * * @param sh * @param elapsedUs + * @return true + * @return false */ -void shRunTimers(shVars_t* sh, uint32_t elapsedUs) +bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) { + if (sh->gameEnd) + { + shChangeScreen(sh, SH_GAME_END); + return false; + } + // Run a lead-in timer to allow notes to spawn before the song starts playing if (sh->leadInUs > 0) { @@ -180,10 +204,20 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) if (sh->leadInUs <= 0) { - globalMidiPlayerResumeAll(); + globalMidiPlayerPlaySongCb(&sh->midiSong, MIDI_BGM, shSongOver); sh->leadInUs = 0; } } + else if (sh->failOn) + { + sh->failSampleTimer += elapsedUs; + if (sh->failSampleTimer > -sh->failSampleInterval) + { + sh->failSampleTimer -= sh->failSampleInterval; + // TODO save these values somewhere, display on game over screen + printf("Fail meter: %d\n", sh->failMeter); + } + } // Run a timer for pop-up text if (sh->textTimerUs > 0) @@ -207,7 +241,7 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) // Generate fret lines based on tempo int32_t nextFretLineUs = sh->lastFretLineUs + sh->tempo; - if (songUs + TRAVEL_TIME_US >= nextFretLineUs) + if (songUs + sh->scrollTime >= nextFretLineUs) { sh->lastFretLineUs += sh->tempo; @@ -225,7 +259,7 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) = MIDI_TICKS_TO_US(sh->chartNotes[sh->currentChartNote].tick, player->tempo, player->reader.division); // Check if the game note should be spawned now to reach the hit bar in time - if (songUs + TRAVEL_TIME_US >= nextEventUs) + if (songUs + sh->scrollTime >= nextEventUs) { // Spawn an game note shGameNote_t* ni = heap_caps_calloc(1, sizeof(shGameNote_t), MALLOC_CAP_SPIRAM); @@ -285,14 +319,14 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) { // Moving notes follow this formula gameNote->headPosY - = (((TFT_HEIGHT - HIT_BAR) * (gameNote->headTimeUs - songUs)) / TRAVEL_TIME_US) + HIT_BAR; + = (((TFT_HEIGHT - HIT_BAR) * (gameNote->headTimeUs - songUs)) / sh->scrollTime) + HIT_BAR; } // Update tail position if there is a hold if (-1 != gameNote->tailPosY) { gameNote->tailPosY - = (((TFT_HEIGHT - HIT_BAR) * (gameNote->tailTimeUs - songUs)) / TRAVEL_TIME_US) + HIT_BAR; + = (((TFT_HEIGHT - HIT_BAR) * (gameNote->tailTimeUs - songUs)) / sh->scrollTime) + HIT_BAR; } // Check if the note should be removed @@ -320,12 +354,7 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) shouldRemove = true; // Break the combo - sh->combo = 0; - sh->failMeter = MAX(0, sh->failMeter - 2); - if (0 == sh->failMeter && shGetSettingFail()) - { - shChangeScreen(sh, SH_GAME_END); - } + shMissNote(sh); } // If the game note should be removed @@ -360,7 +389,7 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) shFretLine_t* fretLine = fretLineNode->val; // Update positions - fretLine->headPosY = (((TFT_HEIGHT - HIT_BAR) * (fretLine->headTimeUs - songUs)) / TRAVEL_TIME_US) + HIT_BAR; + fretLine->headPosY = (((TFT_HEIGHT - HIT_BAR) * (fretLine->headTimeUs - songUs)) / sh->scrollTime) + HIT_BAR; // Remove if off screen if (fretLine->headPosY < 0) @@ -376,6 +405,7 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) fretLineNode = fretLineNode->next; } } + return true; } /** @@ -475,8 +505,9 @@ void shDrawGame(shVars_t* sh) snprintf(scoreText, sizeof(scoreText) - 1, "%" PRId32, sh->score); drawText(&sh->rodin, c555, scoreText, textMargin, TFT_HEIGHT - failBarHeight - sh->rodin.height); - if (shGetSettingFail()) - { // Draw fail meter bar + if (sh->failOn) + { + // Draw fail meter bar int32_t failMeterWidth = (sh->failMeter * TFT_WIDTH) / 100; fillDisplayArea(0, TFT_HEIGHT - failBarHeight, failMeterWidth, TFT_HEIGHT - 1, c440); } @@ -578,12 +609,7 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) { sh->hitText = hit_miss; // Break the combo - sh->combo = 0; - sh->failMeter = MAX(0, sh->failMeter - 2); - if (0 == sh->failMeter && shGetSettingFail()) - { - shChangeScreen(sh, SH_GAME_END); - } + shMissNote(sh); } // Increment the score @@ -627,14 +653,7 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) { // Total miss sh->hitText = hit_miss; - - // Break the combo - sh->combo = 0; - sh->failMeter = MAX(0, sh->failMeter - 2); - if (0 == sh->failMeter && shGetSettingFail()) - { - shChangeScreen(sh, SH_GAME_END); - } + shMissNote(sh); // TODO ignore buttons not used for this difficulty } @@ -676,5 +695,24 @@ static int32_t getMultiplier(shVars_t* sh) */ static void shSongOver(void) { - shChangeScreen(getShVars(), SH_GAME_END); -} \ No newline at end of file + // Set a flag, end the game synchronously + getShVars()->gameEnd = true; +} + +/** + * @brief TODO + * + * @param sh + */ +static void shMissNote(shVars_t* sh) +{ + sh->combo = 0; + if (sh->failOn) + { + sh->failMeter = MAX(0, sh->failMeter - 2); + if (0 == sh->failMeter) + { + sh->gameEnd = true; + } + } +} diff --git a/main/modes/games/swadgeHero/swadgeHero_game.h b/main/modes/games/swadgeHero/swadgeHero_game.h index a692b5e35..8f94b94e8 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.h +++ b/main/modes/games/swadgeHero/swadgeHero_game.h @@ -2,10 +2,8 @@ #include "mode_swadgeHero.h" -#define TRAVEL_TIME_US 2000000 - void shLoadSong(shVars_t* sh, const char* midi, const char* chart); uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size); void shGameInput(shVars_t* sh, buttonEvt_t* evt); -void shRunTimers(shVars_t* sh, uint32_t elapsedUs); +bool shRunTimers(shVars_t* sh, uint32_t elapsedUs); void shDrawGame(shVars_t* sh); diff --git a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c index 812df8266..6443453a4 100644 --- a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c +++ b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c @@ -20,15 +20,43 @@ // Functions //============================================================================== +/** + * @brief TODO + * + * @param sh + * @param evt + */ void shGameEndInput(shVars_t* sh, buttonEvt_t* evt) { if (evt->down) { - shChangeScreen(sh, SH_MENU); + switch (evt->button) + { + case PB_A: + { + shChangeScreen(sh, SH_MENU); + break; + } + default: + { + break; + } + } } } +/** + * @brief TODO + * + * @param sh + * @param elapsedUs + */ void shGameEndDraw(shVars_t* sh, int32_t elapsedUs) { clearPxTft(); -} \ No newline at end of file + + char scoreStr[32]; + snprintf(scoreStr, sizeof(scoreStr) - 1, "%" PRId32, sh->score); + int16_t tWidth = textWidth(&sh->rodin, scoreStr); + drawText(&sh->rodin, c555, scoreStr, (TFT_WIDTH - tWidth) / 2, (TFT_HEIGHT - sh->rodin.height) / 2); +} diff --git a/main/modes/games/swadgeHero/swadgeHero_menu.c b/main/modes/games/swadgeHero/swadgeHero_menu.c index 8be56a8ae..30aa6164b 100644 --- a/main/modes/games/swadgeHero/swadgeHero_menu.c +++ b/main/modes/games/swadgeHero/swadgeHero_menu.c @@ -46,8 +46,8 @@ const int32_t shs_fail_vals[] = {true, false}; const char* shs_speed_key = "shs_speed"; const char* shs_speed_label = "Scroll: "; -const char* shs_speed_opts[] = {"Slow", "Medium", "Fast"}; -const int32_t shs_speed_vals[] = {0, 1, 2}; +const char* shs_speed_opts[] = {"Slow", "Normal", "Fast", "Turbo"}; +const int32_t shs_speed_vals[] = {4000000, 3000000, 2000000, 1000000}; //============================================================================== // Functions From 45f01b53cc26710235adc0e7b1448a6d82aa83b1 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Tue, 1 Oct 2024 11:45:36 +0000 Subject: [PATCH 44/54] Add fail meter charting --- main/modes/games/swadgeHero/mode_swadgeHero.c | 5 ++- main/modes/games/swadgeHero/mode_swadgeHero.h | 6 ++- main/modes/games/swadgeHero/swadgeHero_game.c | 31 ++++++++------- main/modes/games/swadgeHero/swadgeHero_game.h | 2 + .../games/swadgeHero/swadgeHero_gameEnd.c | 38 +++++++++++++++++++ 5 files changed, 65 insertions(+), 17 deletions(-) diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index f6ba56a1b..23aeef014 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -230,12 +230,15 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) } case SH_GAME_END: { - // Nothing to free + // Free fail samples + clear(&shv->failSamples); break; } case SH_NONE: default: { + // Clear this on exit, just in case + clear(&shv->failSamples); break; } } diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index ebdb1a33e..c64d2c990 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -90,8 +90,11 @@ typedef struct int32_t const* btnToNote; int32_t const* noteToIcon; int32_t tempo; + + // Fail meter tracking + int32_t failMeter; int32_t failSampleInterval; - int32_t failSampleTimer; + list_t failSamples; // Setting data bool failOn; @@ -99,7 +102,6 @@ typedef struct // Score data int32_t score; - int32_t failMeter; int32_t combo; bool gameEnd; diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 679fca5bb..7c9afe3ae 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -15,8 +15,6 @@ #define SH_TEXT_TIME 500000 -#define NUM_FAIL_METER_SAMPLES 200 - //============================================================================== // Const Variables //============================================================================== @@ -67,6 +65,9 @@ static void shMissNote(shVars_t* sh); */ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) { + // Don't immediately quit + sh->gameEnd = false; + // Load settings sh->failOn = shGetSettingFail(); sh->scrollTime = shGetSettingSpeed(); @@ -112,11 +113,13 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) // Seek to load the tempo and length, then reset midiSeek(player, -1); sh->tempo = player->tempo; - int32_t songLenUs = SAMPLES_TO_US(player->sampleCount); + int32_t songLenUs = SAMPLES_TO_US(player->sampleCount); // TODO this is incorrect??? globalMidiPlayerStop(true); + // TODO not capturing NUM_FAIL_METER_SAMPLES points?? sh->failSampleInterval = songLenUs / NUM_FAIL_METER_SAMPLES; - sh->failSampleTimer = 0; + + printf("Song len us: %d, sample interval %d\n", songLenUs, sh->failSampleInterval); // Set the lead-in timer sh->leadInUs = sh->scrollTime; @@ -208,16 +211,6 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) sh->leadInUs = 0; } } - else if (sh->failOn) - { - sh->failSampleTimer += elapsedUs; - if (sh->failSampleTimer > -sh->failSampleInterval) - { - sh->failSampleTimer -= sh->failSampleInterval; - // TODO save these values somewhere, display on game over screen - printf("Fail meter: %d\n", sh->failMeter); - } - } // Run a timer for pop-up text if (sh->textTimerUs > 0) @@ -239,6 +232,16 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) songUs = SAMPLES_TO_US(player->sampleCount); } + if (sh->failOn && songUs >= 0) + { + while (sh->failSampleInterval * sh->failSamples.length <= songUs) + { + printf("sample at %d\n", songUs); + // Save the sample to display on the game over screen + push(&sh->failSamples, sh->failMeter); + } + } + // Generate fret lines based on tempo int32_t nextFretLineUs = sh->lastFretLineUs + sh->tempo; if (songUs + sh->scrollTime >= nextFretLineUs) diff --git a/main/modes/games/swadgeHero/swadgeHero_game.h b/main/modes/games/swadgeHero/swadgeHero_game.h index 8f94b94e8..7bdc32b0a 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.h +++ b/main/modes/games/swadgeHero/swadgeHero_game.h @@ -2,6 +2,8 @@ #include "mode_swadgeHero.h" +#define NUM_FAIL_METER_SAMPLES 200 + void shLoadSong(shVars_t* sh, const char* midi, const char* chart); uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size); void shGameInput(shVars_t* sh, buttonEvt_t* evt); diff --git a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c index 6443453a4..12f37bce7 100644 --- a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c +++ b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c @@ -3,6 +3,7 @@ //============================================================================== #include "swadgeHero_gameEnd.h" +#include "swadgeHero_game.h" //============================================================================== // Defines @@ -55,8 +56,45 @@ void shGameEndDraw(shVars_t* sh, int32_t elapsedUs) { clearPxTft(); + // Draw the score char scoreStr[32]; snprintf(scoreStr, sizeof(scoreStr) - 1, "%" PRId32, sh->score); int16_t tWidth = textWidth(&sh->rodin, scoreStr); drawText(&sh->rodin, c555, scoreStr, (TFT_WIDTH - tWidth) / 2, (TFT_HEIGHT - sh->rodin.height) / 2); + + // Draw a graph of the fail meter + if (sh->failOn) + { +#define Y_MARGIN 20 +#define Y_HEIGHT 50 +#define X_MARGIN ((TFT_WIDTH - NUM_FAIL_METER_SAMPLES) / 2) +#define BOX_MARGIN 2 + + drawRect(X_MARGIN - BOX_MARGIN, TFT_HEIGHT - Y_MARGIN - Y_HEIGHT - BOX_MARGIN, // + TFT_WIDTH - X_MARGIN + BOX_MARGIN, TFT_HEIGHT - Y_MARGIN + BOX_MARGIN, c220); + + // Setup the first point + int32_t xOff = (TFT_WIDTH - NUM_FAIL_METER_SAMPLES) / 2; + vec_t lastPoint = { + .x = xOff, + .y = TFT_HEIGHT - Y_MARGIN - 25, + }; + + // Iterate through all points + node_t* failNode = sh->failSamples.first; + while (failNode) + { + // Draw a line from the last point to this one + xOff++; + vec_t cPoint = { + .x = xOff, + .y = TFT_HEIGHT - Y_MARGIN - ((intptr_t)failNode->val) / 2, + }; + drawLineFast(lastPoint.x, lastPoint.y, cPoint.x, cPoint.y, c440); + + // Increment to the next + lastPoint = cPoint; + failNode = failNode->next; + } + } } From e8e8c88fc6c1530a5fdd04a59257a8a5a57f8e48 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Wed, 2 Oct 2024 01:08:42 +0000 Subject: [PATCH 45/54] Fix bug when seeking to end of MIDI file (fixes fail meter chart) Fix memory leak Implement high score saving and display Add note scoring for hold notes Ignore buttons not used in the mode --- main/midi/midiPlayer.c | 26 +- main/modes/games/swadgeHero/mode_swadgeHero.c | 45 +++- main/modes/games/swadgeHero/mode_swadgeHero.h | 2 + main/modes/games/swadgeHero/swadgeHero_game.c | 252 +++++++++++------- .../games/swadgeHero/swadgeHero_gameEnd.c | 4 +- main/modes/games/swadgeHero/swadgeHero_menu.c | 42 +-- 6 files changed, 237 insertions(+), 134 deletions(-) diff --git a/main/midi/midiPlayer.c b/main/midi/midiPlayer.c index 2d74de2b7..b34332042 100644 --- a/main/midi/midiPlayer.c +++ b/main/midi/midiPlayer.c @@ -2338,7 +2338,14 @@ void midiSeek(midiPlayer_t* player, uint32_t ticks) if (!player->eventAvailable || player->pendingEvent.absTime > ticks) { ESP_LOGD("MIDI", "No more events between start and end time\n"); - curTick = ticks; + if ((META_EVENT == player->pendingEvent.type) && (END_OF_TRACK == player->pendingEvent.meta.type)) + { + curTick = player->pendingEvent.absTime; + } + else + { + curTick = ticks; + } break; } @@ -2351,18 +2358,15 @@ void midiSeek(midiPlayer_t* player, uint32_t ticks) player->sampleCount = TICKS_TO_SAMPLES(curTick, player->tempo, player->reader.division); - if ((uint32_t)-1 != ticks) + stopped = !player->eventAvailable + && !(player->eventAvailable = midiNextEvent(&player->reader, &player->pendingEvent)); + if (stopped && (uint32_t)-1 != ticks) { - stopped = !player->eventAvailable - && !(player->eventAvailable = midiNextEvent(&player->reader, &player->pendingEvent)); - if (stopped) - { - midiSongEnd(player); + midiSongEnd(player); - if (endCb) - { - endCb(); - } + if (endCb) + { + endCb(); } } diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index 23aeef014..cde463ed6 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -50,7 +50,7 @@ shVars_t* shv; //============================================================================== /** - * @brief TODO + * @brief TODO doc * * @return */ @@ -192,7 +192,7 @@ static void shBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, } /** - * @brief TODO + * @brief TODO doc * * @param sh * @param newScreen @@ -237,8 +237,7 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) case SH_NONE: default: { - // Clear this on exit, just in case - clear(&shv->failSamples); + // Nothing tear down break; } } @@ -287,7 +286,45 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) case SH_NONE: default: { + // Clear this on exit, just in case + clear(&shv->failSamples); + break; + } + } +} + +/** + * @brief TODO doc + * + * @param songName + * @param difficulty + * @param key Must be at least 9 bytes + */ +void shGetNvsKey(const char* songName, shDifficulty_t difficulty, char* key) +{ + int32_t toCopy = MIN(strlen(songName), 7); + memcpy(key, songName, toCopy); + + char dChar; + switch (difficulty) + { + default: + case SH_EASY: + { + dChar = 'e'; + break; + } + case SH_MEDIUM: + { + dChar = 'm'; + break; + } + case SH_HARD: + { + dChar = 'h'; break; } } + key[toCopy] = dChar; + key[toCopy + 1] = 0; } diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index c64d2c990..a1b7dc6b3 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -80,6 +80,7 @@ typedef struct // Song being played midiFile_t midiSong; int32_t leadInUs; + char hsKey[16]; // Chart data int32_t numChartNotes; @@ -134,3 +135,4 @@ extern swadgeMode_t swadgeHeroMode; shVars_t* getShVars(void); void shChangeScreen(shVars_t* sh, shScreen_t newScreen); +void shGetNvsKey(const char* songName, shDifficulty_t difficulty, char* key); diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 7c9afe3ae..2e0b6e018 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -51,13 +51,14 @@ static const char hit_late[] = "Late"; static int32_t getMultiplier(shVars_t* sh); static void shSongOver(void); static void shMissNote(shVars_t* sh); +static void shHitNote(shVars_t* sh, int32_t baseScore); //============================================================================== // Functions //============================================================================== /** - * @brief TODO + * @brief TODO doc * * @param sh * @param midi @@ -76,28 +77,35 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) size_t sz = 0; sh->numFrets = 1 + shLoadChartData(sh, cnfsGetFile(chart, &sz), sz); - if (6 == sh->numFrets) + shDifficulty_t difficulty; + if (4 <= sh->numFrets) { - sh->btnToNote = btnToNote_h; - sh->noteToBtn = noteToBtn_h; - sh->colors = colors_h; - sh->noteToIcon = noteToIcon_h; + difficulty = SH_EASY; + sh->btnToNote = btnToNote_e; + sh->noteToBtn = noteToBtn_e; + sh->colors = colors_e; + sh->noteToIcon = noteToIcon_e; } else if (5 == sh->numFrets) { + difficulty = SH_MEDIUM; sh->btnToNote = btnToNote_m; sh->noteToBtn = noteToBtn_m; sh->colors = colors_m; sh->noteToIcon = noteToIcon_m; } - else if (4 == sh->numFrets) + else // >= 6 { - sh->btnToNote = btnToNote_e; - sh->noteToBtn = noteToBtn_e; - sh->colors = colors_e; - sh->noteToIcon = noteToIcon_e; + difficulty = SH_HARD; + sh->btnToNote = btnToNote_h; + sh->noteToBtn = noteToBtn_h; + sh->colors = colors_h; + sh->noteToIcon = noteToIcon_h; } + // Save the key for the score + shGetNvsKey(midi, difficulty, sh->hsKey); + // Make sure MIDI player is initialized initGlobalMidiPlayer(); midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); @@ -113,14 +121,12 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) // Seek to load the tempo and length, then reset midiSeek(player, -1); sh->tempo = player->tempo; - int32_t songLenUs = SAMPLES_TO_US(player->sampleCount); // TODO this is incorrect??? + int32_t songLenUs = SAMPLES_TO_US(player->sampleCount); globalMidiPlayerStop(true); - // TODO not capturing NUM_FAIL_METER_SAMPLES points?? + // Figure out how often to sample the fail meter for the chart after the song sh->failSampleInterval = songLenUs / NUM_FAIL_METER_SAMPLES; - printf("Song len us: %d, sample interval %d\n", songLenUs, sh->failSampleInterval); - // Set the lead-in timer sh->leadInUs = sh->scrollTime; @@ -139,7 +145,7 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) } /** - * @brief TODO + * @brief TODO doc * * @param sh * @param data @@ -185,7 +191,7 @@ uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size) } /** - * @brief TODO + * @brief TODO doc * * @param sh * @param elapsedUs @@ -196,6 +202,21 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) { if (sh->gameEnd) { + // Save score to NVS + int32_t oldHs = 0; + if (!readNvs32(sh->hsKey, &oldHs)) + { + // No score saved yet, assume 0 + oldHs = 0; + } + + // Write the high score to NVS if it's larger + if (sh->score > oldHs) + { + writeNvs32(sh->hsKey, sh->score); + } + + // Switch to the game end screen shChangeScreen(sh, SH_GAME_END); return false; } @@ -236,9 +257,8 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) { while (sh->failSampleInterval * sh->failSamples.length <= songUs) { - printf("sample at %d\n", songUs); // Save the sample to display on the game over screen - push(&sh->failSamples, sh->failMeter); + push(&sh->failSamples, (void*)((intptr_t)sh->failMeter)); } } @@ -343,6 +363,9 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) { // Note is currently being held, tail reached the bar shouldRemove = true; + + // Note was held all the way, so score it. + shHitNote(sh, 5); } } else if (gameNote->tailPosY < 0) @@ -412,7 +435,7 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) } /** - * @brief TODO + * @brief TODO doc * * @param sh */ @@ -527,7 +550,7 @@ void shDrawGame(shVars_t* sh) } /** - * @brief TODO + * @brief TODO doc * * @param sh * @param evt @@ -547,7 +570,8 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) sh->btnState = evt->state; - if (evt->down) + int32_t notePressed = sh->btnToNote[31 - __builtin_clz(evt->button)]; + if (-1 != notePressed) { // See if the button press matches a note bool noteMatch = false; @@ -559,90 +583,98 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) shGameNote_t* gameNote = gameNoteNode->val; // If the game note matches the button - if (gameNote->note == sh->btnToNote[31 - __builtin_clz(evt->button)]) + if (gameNote->note == notePressed) { + // Button event matches a note on screen somewhere noteMatch = true; - // Find how off the timing is - int32_t usOff = (songUs - gameNote->headTimeUs); - - // Set either early or late - sh->timingText = (usOff > 0) ? hit_late : hit_early; - - // Find the absolute difference - usOff = ABS(usOff); - - // Check if this button hit a note - bool gameNoteHit = false; - - int32_t baseScore = 0; - - // Classify the time off - if (usOff < 21500) + // Button was pressed + if (evt->down) { - sh->hitText = hit_fantastic; - gameNoteHit = true; - baseScore = 5; - } - else if (usOff < 43000) - { - sh->hitText = hit_marvelous; - gameNoteHit = true; - baseScore = 4; - } - else if (usOff < 102000) - { - sh->hitText = hit_great; - gameNoteHit = true; - baseScore = 3; - } - else if (usOff < 135000) - { - sh->hitText = hit_decent; - gameNoteHit = true; - baseScore = 2; - } - else if (usOff < 180000) - { - sh->hitText = hit_way_off; - gameNoteHit = true; - baseScore = 1; - } - else - { - sh->hitText = hit_miss; - // Break the combo - shMissNote(sh); - } + // Find how off the timing is + int32_t usOff = (songUs - gameNote->headTimeUs); - // Increment the score - sh->score += getMultiplier(sh) * baseScore; + // Set either early or late + sh->timingText = (usOff > 0) ? hit_late : hit_early; - // Set a timer to not show the text forever - sh->textTimerUs = SH_TEXT_TIME; + // Find the absolute difference + usOff = ABS(usOff); - // If it was close enough to hit - if (gameNoteHit) - { - // Increment the combo - sh->combo++; - sh->failMeter = MIN(100, sh->failMeter + 1); + // Check if this button hit a note + bool gameNoteHit = false; - if (gameNote->tailPosY >= 0) + int32_t baseScore = 0; + + // Classify the time off + if (usOff < 21500) + { + sh->hitText = hit_fantastic; + gameNoteHit = true; + baseScore = 5; + } + else if (usOff < 43000) + { + sh->hitText = hit_marvelous; + gameNoteHit = true; + baseScore = 4; + } + else if (usOff < 102000) + { + sh->hitText = hit_great; + gameNoteHit = true; + baseScore = 3; + } + else if (usOff < 135000) + { + sh->hitText = hit_decent; + gameNoteHit = true; + baseScore = 2; + } + else if (usOff < 180000) { - // There is a tail, don't remove the note yet - gameNote->headPosY = HIT_BAR; - gameNote->held = true; + sh->hitText = hit_way_off; + gameNoteHit = true; + baseScore = 1; } else { - // No tail, remove the game note - node_t* nextNode = gameNoteNode->next; - free(gameNoteNode->val); - removeEntry(&sh->gameNotes, gameNoteNode); - gameNoteNode = nextNode; + sh->hitText = hit_miss; + // Break the combo + shMissNote(sh); + } + + // Set a timer to not show the text forever + sh->textTimerUs = SH_TEXT_TIME; + + // If it was close enough to hit + if (gameNoteHit) + { + shHitNote(sh, baseScore); + + if (gameNote->tailPosY >= 0) + { + // There is a tail, don't remove the note yet + gameNote->headPosY = HIT_BAR; + gameNote->held = true; + } + else + { + // No tail, remove the game note + node_t* nextNode = gameNoteNode->next; + free(gameNoteNode->val); + removeEntry(&sh->gameNotes, gameNoteNode); + gameNoteNode = nextNode; + } } } + else if (gameNote->held) + { + // A held note was released. Remove it! + node_t* nextNode = gameNoteNode->next; + free(gameNoteNode->val); + removeEntry(&sh->gameNotes, gameNoteNode); + gameNoteNode = nextNode; + } // the button was matched to an game note, break the loop break; @@ -651,20 +683,16 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // Iterate to the next game note gameNoteNode = gameNoteNode->next; } + // Done iterating through notes - if (false == noteMatch) + // Check if a button down didn't have a matching note on screen + if (false == noteMatch && evt->down) { // Total miss sh->hitText = hit_miss; shMissNote(sh); - - // TODO ignore buttons not used for this difficulty } } - else - { - // TODO handle ups when holding tails - } // Check for analog touch // int32_t centerVal, intensityVal; @@ -682,7 +710,7 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) } /** - * @brief TODO + * @brief TODO doc * * @param sh * @return int32_t @@ -693,7 +721,7 @@ static int32_t getMultiplier(shVars_t* sh) } /** - * @brief TODO + * @brief TODO doc * */ static void shSongOver(void) @@ -703,7 +731,27 @@ static void shSongOver(void) } /** - * @brief TODO + * @brief TODO doc + * + * @param sh + * @param baseScore + */ +static void shHitNote(shVars_t* sh, int32_t baseScore) +{ + // Increment the score + sh->score += getMultiplier(sh) * baseScore; + + // Increment the combo & fail meter + sh->combo++; + + if (sh->failOn) + { + sh->failMeter = MIN(100, sh->failMeter + 1); + } +} + +/** + * @brief TODO doc * * @param sh */ diff --git a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c index 12f37bce7..843275a6a 100644 --- a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c +++ b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c @@ -22,7 +22,7 @@ //============================================================================== /** - * @brief TODO + * @brief TODO doc * * @param sh * @param evt @@ -47,7 +47,7 @@ void shGameEndInput(shVars_t* sh, buttonEvt_t* evt) } /** - * @brief TODO + * @brief TODO doc * * @param sh * @param elapsedUs diff --git a/main/modes/games/swadgeHero/swadgeHero_menu.c b/main/modes/games/swadgeHero/swadgeHero_menu.c index 30aa6164b..41db20e8c 100644 --- a/main/modes/games/swadgeHero/swadgeHero_menu.c +++ b/main/modes/games/swadgeHero/swadgeHero_menu.c @@ -54,7 +54,7 @@ const int32_t shs_speed_vals[] = {4000000, 3000000, 2000000, 1000000}; //============================================================================== /** - * @brief TODO + * @brief TODO doc * * @param sh */ @@ -87,21 +87,37 @@ void shSetupMenu(shVars_t* sh) // Allocate and print high score strings // Save them all to a linked list to free later - // TODO get high scores from NVS - int32_t maxScore = 0x7FFFFFFF; + // Variables for reading from NVS + int32_t tmpScore; + char nvsKey[16]; + shGetNvsKey(shSongList[sIdx].midi, SH_EASY, nvsKey); + if (!readNvs32(nvsKey, &tmpScore)) + { + tmpScore = 0; + } char* easyStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); - snprintf(easyStr, HS_STR_LEN - 1, "Easy: %" PRId32, maxScore); + snprintf(easyStr, HS_STR_LEN - 1, "Easy: %" PRId32, tmpScore); addSingleItemToMenu(sh->menu, easyStr); push(&sh->hsStrs, easyStr); + shGetNvsKey(shSongList[sIdx].name, SH_MEDIUM, nvsKey); + if (!readNvs32(nvsKey, &tmpScore)) + { + tmpScore = 0; + } char* mediumStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); - snprintf(mediumStr, HS_STR_LEN - 1, "Medium: %" PRId32, maxScore); + snprintf(mediumStr, HS_STR_LEN - 1, "Medium: %" PRId32, tmpScore); addSingleItemToMenu(sh->menu, mediumStr); push(&sh->hsStrs, mediumStr); + shGetNvsKey(shSongList[sIdx].name, SH_HARD, nvsKey); + if (!readNvs32(nvsKey, &tmpScore)) + { + tmpScore = 0; + } char* hardStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); - snprintf(hardStr, HS_STR_LEN - 1, "Hard: %" PRId32, maxScore); + snprintf(hardStr, HS_STR_LEN - 1, "Hard: %" PRId32, tmpScore); addSingleItemToMenu(sh->menu, hardStr); push(&sh->hsStrs, hardStr); @@ -136,7 +152,7 @@ void shSetupMenu(shVars_t* sh) } /** - * @brief TODO + * @brief TODO doc * * @param sh */ @@ -153,7 +169,7 @@ void shTeardownMenu(shVars_t* sh) } /** - * @brief TODO + * @brief TODO doc * * @param sh * @param btn @@ -164,7 +180,7 @@ void shMenuInput(shVars_t* sh, buttonEvt_t* btn) } /** - * @brief TODO + * @brief TODO doc * * @param sh */ @@ -263,9 +279,7 @@ static void shMenuCb(const char* label, bool selected, uint32_t settingVal) } /** - * @brief TODO - * - * TODO use this somewhere + * @brief TODO doc * * @return true * @return false @@ -281,9 +295,7 @@ bool shGetSettingFail(void) } /** - * @brief TODO - * - * TODO use this somewhere + * @brief TODO doc * * @return true * @return false From c41c9255e1e7e826516a0a302ea54bc8fcc522a3 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Thu, 3 Oct 2024 00:59:21 +0000 Subject: [PATCH 46/54] Add ability to recolor the mania menu Recolor Swadge Hero menu --- main/menu/menuManiaRenderer.c | 104 ++++++++++++------ main/menu/menuManiaRenderer.h | 15 +++ main/modes/games/swadgeHero/swadgeHero_menu.c | 8 ++ 3 files changed, 94 insertions(+), 33 deletions(-) diff --git a/main/menu/menuManiaRenderer.c b/main/menu/menuManiaRenderer.c index db03705a6..a3c6fe6e9 100644 --- a/main/menu/menuManiaRenderer.c +++ b/main/menu/menuManiaRenderer.c @@ -34,16 +34,6 @@ #define UP_ARROW_HEIGHT 10 #define UP_ARROW_MARGIN 2 -#define TITLE_BG_COLOR c115 -#define TITLE_TEXT_COLOR c542 -#define TEXT_OUTLINE_COLOR c000 -#define BG_COLOR c540 -#define OUTER_RING_COLOR c243 -#define INNER_RING_COLOR c531 -#define ROW_COLOR c000 -#define ROW_TEXT_COLOR c555 -// #define ROW_TEXT_SELECTED_COLOR c533 - #define ORBIT_RING_RADIUS_1 26 #define ORBIT_RING_RADIUS_2 18 #define RING_STROKE_THICKNESS 8 @@ -55,7 +45,7 @@ //============================================================================== /// @brief Colors to cycle through for the selected drop shadow -static const paletteColor_t selectedShadowColors[] = { +static const paletteColor_t defaultShadowColors[] = { c500, c511, c522, c533, c544, c555, c544, c533, c522, c511, }; @@ -91,6 +81,18 @@ menuManiaRenderer_t* initMenuManiaRenderer(font_t* titleFont, font_t* titleFontO { menuManiaRenderer_t* renderer = heap_caps_calloc(1, sizeof(menuManiaRenderer_t), MALLOC_CAP_SPIRAM); + // Default colors + renderer->titleBgColor = c115; + renderer->titleTextColor = c542; + renderer->textOutlineColor = c000; + renderer->bgColor = c540; + renderer->outerRingColor = c243; + renderer->innerRingColor = c531; + renderer->rowColor = c000; + renderer->rowTextColor = c555; + renderer->shadowColors = defaultShadowColors; + renderer->shadowColorsLen = ARRAY_SIZE(defaultShadowColors); + // Save or allocate title font if (NULL == titleFont) { @@ -141,8 +143,8 @@ menuManiaRenderer_t* initMenuManiaRenderer(font_t* titleFont, font_t* titleFontO // Initialize Rings const paletteColor_t ringColors[] = { - INNER_RING_COLOR, - OUTER_RING_COLOR, + renderer->innerRingColor, + renderer->outerRingColor, }; int32_t ringMinSpeed = 15000; int32_t ringMaxSpeed = 20000; @@ -215,7 +217,7 @@ static void drawMenuText(menuManiaRenderer_t* renderer, const char* text, int16_ bool leftArrow, bool rightArrow, bool doubleArrows) { // Pick colors based on selection - paletteColor_t textColor = ROW_TEXT_COLOR; + paletteColor_t textColor = renderer->rowTextColor; if (isSelected) { // Draw drop shadow for selected item @@ -225,7 +227,7 @@ static void drawMenuText(menuManiaRenderer_t* renderer, const char* text, int16_ y + rows + DROP_SHADOW_OFFSET, // x + PARALLELOGRAM_HEIGHT - rows - 1 + PARALLELOGRAM_WIDTH + DROP_SHADOW_OFFSET, // y + rows + DROP_SHADOW_OFFSET, // - selectedShadowColors[renderer->selectedShadowIdx]); + renderer->shadowColors[renderer->selectedShadowIdx]); } // Bounce the item @@ -239,7 +241,7 @@ static void drawMenuText(menuManiaRenderer_t* renderer, const char* text, int16_ y + rows, // x + PARALLELOGRAM_HEIGHT - rows - 1 + PARALLELOGRAM_WIDTH, // y + rows, // - ROW_COLOR); + renderer->rowColor); } // Draw the text @@ -328,12 +330,13 @@ static void drawMenuText(menuManiaRenderer_t* renderer, const char* text, int16_ * * @param radius The radius of the ring * @param angle The angle of the ring for orbiting circles - * @param color The color of the ring + * @param ringColor The color of the ring + * @param bgColor The color of the background */ -static void drawManiaRing(int16_t radius, int16_t angle, paletteColor_t color) +static void drawManiaRing(int16_t radius, int16_t angle, paletteColor_t ringColor, paletteColor_t bgColor) { // Draw the ring - drawCircleOutline(TFT_WIDTH / 2, TFT_HEIGHT / 2, radius, RING_STROKE_THICKNESS, color); + drawCircleOutline(TFT_WIDTH / 2, TFT_HEIGHT / 2, radius, RING_STROKE_THICKNESS, ringColor); // Draw the the smaller ring on the orbit (two filled circles) vec_t circlePos = { @@ -341,14 +344,14 @@ static void drawManiaRing(int16_t radius, int16_t angle, paletteColor_t color) .y = -radius + (RING_STROKE_THICKNESS / 2), }; circlePos = rotateVec2d(circlePos, angle); - drawCircleFilled((TFT_WIDTH / 2) + circlePos.x, (TFT_HEIGHT / 2) + circlePos.y, ORBIT_RING_RADIUS_1, color); + drawCircleFilled((TFT_WIDTH / 2) + circlePos.x, (TFT_HEIGHT / 2) + circlePos.y, ORBIT_RING_RADIUS_1, ringColor); drawCircleFilled((TFT_WIDTH / 2) + circlePos.x, (TFT_HEIGHT / 2) + circlePos.y, - ORBIT_RING_RADIUS_1 - RING_STROKE_THICKNESS, BG_COLOR); + ORBIT_RING_RADIUS_1 - RING_STROKE_THICKNESS, bgColor); // Draw an opposite filled circle circlePos.x = -circlePos.x; circlePos.y = -circlePos.y; - drawCircleFilled((TFT_WIDTH / 2) + circlePos.x, (TFT_HEIGHT / 2) + circlePos.y, ORBIT_RING_RADIUS_2, color); + drawCircleFilled((TFT_WIDTH / 2) + circlePos.x, (TFT_HEIGHT / 2) + circlePos.y, ORBIT_RING_RADIUS_2, ringColor); } /** @@ -379,7 +382,7 @@ void drawMenuMania(menu_t* menu, menuManiaRenderer_t* renderer, int64_t elapsedU while (renderer->ledExciteTimer >= 40000 * 8) { renderer->ledExciteTimer -= 40000 * 8; - uint32_t ledColor = paletteToRGB(BG_COLOR); + uint32_t ledColor = paletteToRGB(renderer->bgColor); renderer->leds[renderer->currentLed].r = ((ledColor >> 16) & 0xFF) / 2; renderer->leds[renderer->currentLed].g = ((ledColor >> 8) & 0xFF) / 2; renderer->leds[renderer->currentLed].b = ((ledColor >> 0) & 0xFF) / 2; @@ -443,7 +446,7 @@ void drawMenuMania(menu_t* menu, menuManiaRenderer_t* renderer, int64_t elapsedU while (renderer->selectedShadowTimer > (100000)) { renderer->selectedShadowTimer -= (100000); - renderer->selectedShadowIdx = (renderer->selectedShadowIdx + 1) % ARRAY_SIZE(selectedShadowColors); + renderer->selectedShadowIdx = (renderer->selectedShadowIdx + 1) % renderer->shadowColorsLen; } // Run a timer to bounce the selected item, when transitioned to @@ -468,7 +471,7 @@ void drawMenuMania(menu_t* menu, menuManiaRenderer_t* renderer, int64_t elapsedU renderer->selectedMarqueeTimer += elapsedUs; // Clear the background - fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, BG_COLOR); + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, renderer->bgColor); // Draw the rings for (int16_t i = 0; i < ARRAY_SIZE(renderer->rings); i++) @@ -476,7 +479,7 @@ void drawMenuMania(menu_t* menu, menuManiaRenderer_t* renderer, int64_t elapsedU maniaRing_t* ring = &renderer->rings[i]; int16_t ringRadius = (MIN_RING_RADIUS + MAX_RING_RADIUS) / 2 + (((MAX_RING_RADIUS - MIN_RING_RADIUS) * getSin1024(ring->diameterAngle)) / 1024); - drawManiaRing(ringRadius, ring->orbitAngle, ring->color); + drawManiaRing(ringRadius, ring->orbitAngle, ring->color, renderer->bgColor); } // Find the start of the 'page' @@ -513,18 +516,18 @@ void drawMenuMania(menu_t* menu, menuManiaRenderer_t* renderer, int64_t elapsedU int16_t titleBgX1 = (TFT_WIDTH + tWidth) / 2 + 6; int16_t titleBgY0 = y; int16_t titleBgY1 = y + TITLE_BG_HEIGHT; - fillDisplayArea(titleBgX0, titleBgY0, titleBgX1, titleBgY1, TITLE_BG_COLOR); + fillDisplayArea(titleBgX0, titleBgY0, titleBgX1, titleBgY1, renderer->titleBgColor); drawTriangleOutlined(titleBgX0, titleBgY0, titleBgX0, titleBgY1, titleBgX0 - (TITLE_BG_HEIGHT / 2), - (titleBgY0 + titleBgY1) / 2, TITLE_BG_COLOR, TITLE_BG_COLOR); + (titleBgY0 + titleBgY1) / 2, renderer->titleBgColor, renderer->titleBgColor); drawTriangleOutlined(titleBgX1, titleBgY0, titleBgX1, titleBgY1, titleBgX1 + (TITLE_BG_HEIGHT / 2), - (titleBgY0 + titleBgY1) / 2, TITLE_BG_COLOR, TITLE_BG_COLOR); + (titleBgY0 + titleBgY1) / 2, renderer->titleBgColor, renderer->titleBgColor); // Draw a title y += (TITLE_BG_HEIGHT - renderer->titleFont->height) / 2; // Draw the menu text - drawText(renderer->titleFont, TITLE_TEXT_COLOR, menu->title, (TFT_WIDTH - tWidth) / 2, y); + drawText(renderer->titleFont, renderer->titleTextColor, menu->title, (TFT_WIDTH - tWidth) / 2, y); // Outline the menu text - drawText(renderer->titleFontOutline, TEXT_OUTLINE_COLOR, menu->title, (TFT_WIDTH - tWidth) / 2, y); + drawText(renderer->titleFontOutline, renderer->textOutlineColor, menu->title, (TFT_WIDTH - tWidth) / 2, y); // Move to drawing the rows y = titleBgY1 + Y_SECTION_MARGIN; @@ -537,7 +540,7 @@ void drawMenuMania(menu_t* menu, menuManiaRenderer_t* renderer, int64_t elapsedU { drawLineFast(PARALLELOGRAM_X_OFFSET + PARALLELOGRAM_HEIGHT - t + (UP_ARROW_HEIGHT * 2 - 1) / 2, y + t, PARALLELOGRAM_X_OFFSET + PARALLELOGRAM_HEIGHT + t + (UP_ARROW_HEIGHT * 2 - 1) / 2, y + t, - ROW_COLOR); + renderer->rowColor); } y += (UP_ARROW_HEIGHT); } @@ -598,7 +601,7 @@ void drawMenuMania(menu_t* menu, menuManiaRenderer_t* renderer, int64_t elapsedU { drawLineFast(PARALLELOGRAM_X_OFFSET + PARALLELOGRAM_WIDTH - t - (UP_ARROW_HEIGHT * 2) / 2, y - t, PARALLELOGRAM_X_OFFSET + PARALLELOGRAM_WIDTH + t - (UP_ARROW_HEIGHT * 2) / 2, y - t, - ROW_COLOR); + renderer->rowColor); } } @@ -644,3 +647,38 @@ void setManiaLedsOn(menuManiaRenderer_t* renderer, bool ledsOn) setLeds(renderer->leds, CONFIG_NUM_LEDS); } } + +/** + * @brief Recolor a menu renderer + * + * @param renderer The menu renderer to recolor + * @param titleBgColor The color of the title background + * @param titleTextColor The color of the title text + * @param textOutlineColor The color of the title text outline + * @param bgColor The color of the screen background + * @param outerRingColor The color of the outer rotating ring + * @param innerRingColor The color of the inner rotating ring + * @param rowColor The color of the row background + * @param rowTextColor The color of the row text + * @param shadowColors The colors cycled through as the selected shadow + * @param shadowColorsLen The number of selected shadow colors to cycle through + */ +void recolorMenuManiaRenderer(menuManiaRenderer_t* renderer, paletteColor_t titleBgColor, paletteColor_t titleTextColor, + paletteColor_t textOutlineColor, paletteColor_t bgColor, paletteColor_t outerRingColor, + paletteColor_t innerRingColor, paletteColor_t rowColor, paletteColor_t rowTextColor, + const paletteColor_t* shadowColors, int32_t shadowColorsLen) +{ + renderer->titleBgColor = titleBgColor; + renderer->titleTextColor = titleTextColor; + renderer->textOutlineColor = textOutlineColor; + renderer->bgColor = bgColor; + renderer->outerRingColor = outerRingColor; + renderer->innerRingColor = innerRingColor; + renderer->rings[0].color = outerRingColor; + renderer->rings[1].color = innerRingColor; + renderer->rowColor = rowColor; + renderer->rowTextColor = rowTextColor; + renderer->shadowColors = shadowColors; + renderer->shadowColorsLen = shadowColorsLen; + renderer->selectedShadowIdx = 0; +} diff --git a/main/menu/menuManiaRenderer.h b/main/menu/menuManiaRenderer.h index c4d911d42..e24d618e8 100644 --- a/main/menu/menuManiaRenderer.h +++ b/main/menu/menuManiaRenderer.h @@ -83,11 +83,26 @@ typedef struct int32_t selectedBounceTimer; ///< The timer to bounce the offset for the selected item int32_t selectedValue; ///< The option index or setting value to tell when it changes int32_t selectedMarqueeTimer; ///< The timer for marquee-ing the selected item text, if too long to fit + + paletteColor_t titleBgColor; ///< The color of the title background + paletteColor_t titleTextColor; ///< The color of the title text + paletteColor_t textOutlineColor; ///< The color of the title text outline + paletteColor_t bgColor; ///< The color of the screen background + paletteColor_t outerRingColor; ///< The color of the outer rotating ring + paletteColor_t innerRingColor; ///< The color of the inner rotating ring + paletteColor_t rowColor; ///< The color of the row background + paletteColor_t rowTextColor; ///< The color of the row text + const paletteColor_t* shadowColors; ///< The colors cycled through as the selected shadow + int32_t shadowColorsLen; ///< The number of selected shadow colors to cycle through } menuManiaRenderer_t; menuManiaRenderer_t* initMenuManiaRenderer(font_t* titleFont, font_t* titleFontOutline, font_t* menuFont); void deinitMenuManiaRenderer(menuManiaRenderer_t* renderer); void drawMenuMania(menu_t* menu, menuManiaRenderer_t* renderer, int64_t elapsedUs); void setManiaLedsOn(menuManiaRenderer_t* renderer, bool ledsOn); +void recolorMenuManiaRenderer(menuManiaRenderer_t* renderer, paletteColor_t titleBgColor, paletteColor_t titleTextColor, + paletteColor_t textOutlineColor, paletteColor_t bgColor, paletteColor_t outerRingColor, + paletteColor_t innerRingColor, paletteColor_t rowColor, paletteColor_t rowTextColor, + const paletteColor_t* shadowColors, int32_t shadowColorsLen); #endif \ No newline at end of file diff --git a/main/modes/games/swadgeHero/swadgeHero_menu.c b/main/modes/games/swadgeHero/swadgeHero_menu.c index 41db20e8c..f8449f5ab 100644 --- a/main/modes/games/swadgeHero/swadgeHero_menu.c +++ b/main/modes/games/swadgeHero/swadgeHero_menu.c @@ -63,6 +63,14 @@ void shSetupMenu(shVars_t* sh) // Allocate the menu sh->menu = initMenu(swadgeHeroMode.modeName, shMenuCb); sh->renderer = initMenuManiaRenderer(&sh->righteous, NULL, &sh->rodin); + + static const paletteColor_t shadowColors[] = {c110, c210, c220, c320, c330, c430, c330, c320, c220, c210}; + recolorMenuManiaRenderer(sh->renderer, // + c431, c100, c100, // Title colors (bg, text, outline) + c111, // Background + c200, c210, // Rings + c000, c444, // Rows + shadowColors, ARRAY_SIZE(shadowColors)); setManiaLedsOn(sh->renderer, true); // Add songs to play From 6ab0a46dab26408bf3d9404f6daa5d6ca8fe821f Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Thu, 3 Oct 2024 02:47:02 +0000 Subject: [PATCH 47/54] Draw name, grade, combo, and histogram on game over screen --- main/modes/games/swadgeHero/mode_swadgeHero.c | 22 +- main/modes/games/swadgeHero/mode_swadgeHero.h | 6 + main/modes/games/swadgeHero/swadgeHero_game.c | 203 +++++++++++------- main/modes/games/swadgeHero/swadgeHero_game.h | 10 +- .../games/swadgeHero/swadgeHero_gameEnd.c | 59 ++++- 5 files changed, 197 insertions(+), 103 deletions(-) diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index cde463ed6..9d55861ca 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -250,27 +250,7 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) case SH_GAME: { // Load the chart data - const char* chartFile; - switch (sh->difficulty) - { - default: - case SH_EASY: - { - chartFile = sh->menuSong->easy; - break; - } - case SH_MEDIUM: - { - chartFile = sh->menuSong->med; - break; - } - case SH_HARD: - { - chartFile = sh->menuSong->hard; - break; - } - } - shLoadSong(sh, sh->menuSong->midi, chartFile); + shLoadSong(sh, sh->menuSong, sh->difficulty); break; } case SH_MENU: diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index a1b7dc6b3..a895f1285 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -81,6 +81,7 @@ typedef struct midiFile_t midiSong; int32_t leadInUs; char hsKey[16]; + const char* songName; // Chart data int32_t numChartNotes; @@ -104,7 +105,12 @@ typedef struct // Score data int32_t score; int32_t combo; + int32_t maxCombo; bool gameEnd; + int32_t totalNotes; + int32_t notesHit; + const char* grade; + int32_t noteHistogram[6]; // TODO don't hardcode length // Fret line data list_t fretLines; diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 2e0b6e018..069a1ad94 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -2,6 +2,7 @@ // Includes //============================================================================== +#include #include "swadgeHero_game.h" #include "swadgeHero_menu.h" @@ -15,6 +16,15 @@ #define SH_TEXT_TIME 500000 +//============================================================================== +// Enums +//============================================================================== +typedef struct +{ + int32_t val; + const char* letter; +} shLetterGrade_t; + //============================================================================== // Const Variables //============================================================================== @@ -34,16 +44,26 @@ static const buttonBit_t noteToBtn_h[] = {PB_LEFT, PB_DOWN, PB_UP, PB_RIGHT, PB_ static const int32_t btnToNote_h[] = {2, 1, 0, 3, 5, 4}; static const int32_t noteToIcon_h[] = {0, 1, 2, 3, 4, 5}; -static const char hit_fantastic[] = "Fantastic"; -static const char hit_marvelous[] = "Marvelous"; -static const char hit_great[] = "Great"; -static const char hit_decent[] = "Decent"; -static const char hit_way_off[] = "Way Off"; -static const char hit_miss[] = "Miss"; - static const char hit_early[] = "Early"; static const char hit_late[] = "Late"; +static const shLetterGrade_t grades[] = { + {.val = 100, .letter = "S"}, // + {.val = 97, .letter = "A+"}, {.val = 93, .letter = "A"}, {.val = 90, .letter = "A-"}, {.val = 87, .letter = "B+"}, + {.val = 83, .letter = "B"}, {.val = 80, .letter = "B-"}, {.val = 77, .letter = "C+"}, {.val = 73, .letter = "C"}, + {.val = 70, .letter = "C-"}, {.val = 67, .letter = "D+"}, {.val = 63, .letter = "D"}, {.val = 0, .letter = "F"}, +}; + +// TODO don't hardcode +const shTimingGrade_t timings[6] = { + {.timing = 21500, .label = "Fantastic"}, // + {.timing = 43000, .label = "Marvelous"}, // + {.timing = 102000, .label = "Great"}, // + {.timing = 135000, .label = "Decent"}, // + {.timing = 180000, .label = "Way Off"}, // + {.timing = INT32_MAX, .label = "Miss"}, // +}; + //============================================================================== // Function Declarations //============================================================================== @@ -61,10 +81,10 @@ static void shHitNote(shVars_t* sh, int32_t baseScore); * @brief TODO doc * * @param sh - * @param midi - * @param chart + * @param song + * @param difficulty */ -void shLoadSong(shVars_t* sh, const char* midi, const char* chart) +void shLoadSong(shVars_t* sh, const shSong_t* song, shDifficulty_t difficulty) { // Don't immediately quit sh->gameEnd = false; @@ -73,45 +93,56 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) sh->failOn = shGetSettingFail(); sh->scrollTime = shGetSettingSpeed(); - // Load song - size_t sz = 0; - sh->numFrets = 1 + shLoadChartData(sh, cnfsGetFile(chart, &sz), sz); + // Save song name + sh->songName = song->name; - shDifficulty_t difficulty; - if (4 <= sh->numFrets) + // Pick variables based on difficulty + const char* chartFile; + switch (difficulty) { - difficulty = SH_EASY; - sh->btnToNote = btnToNote_e; - sh->noteToBtn = noteToBtn_e; - sh->colors = colors_e; - sh->noteToIcon = noteToIcon_e; - } - else if (5 == sh->numFrets) - { - difficulty = SH_MEDIUM; - sh->btnToNote = btnToNote_m; - sh->noteToBtn = noteToBtn_m; - sh->colors = colors_m; - sh->noteToIcon = noteToIcon_m; - } - else // >= 6 - { - difficulty = SH_HARD; - sh->btnToNote = btnToNote_h; - sh->noteToBtn = noteToBtn_h; - sh->colors = colors_h; - sh->noteToIcon = noteToIcon_h; + default: + case SH_EASY: + { + chartFile = song->easy; + sh->btnToNote = btnToNote_e; + sh->noteToBtn = noteToBtn_e; + sh->colors = colors_e; + sh->noteToIcon = noteToIcon_e; + break; + } + case SH_MEDIUM: + { + chartFile = song->med; + sh->btnToNote = btnToNote_m; + sh->noteToBtn = noteToBtn_m; + sh->colors = colors_m; + sh->noteToIcon = noteToIcon_m; + break; + } + case SH_HARD: + { + chartFile = song->hard; + sh->btnToNote = btnToNote_h; + sh->noteToBtn = noteToBtn_h; + sh->colors = colors_h; + sh->noteToIcon = noteToIcon_h; + break; + } } + // Load chart data + size_t sz = 0; + sh->numFrets = 1 + shLoadChartData(sh, cnfsGetFile(chartFile, &sz), sz); + // Save the key for the score - shGetNvsKey(midi, difficulty, sh->hsKey); + shGetNvsKey(song->midi, difficulty, sh->hsKey); // Make sure MIDI player is initialized initGlobalMidiPlayer(); midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); // Load the MIDI file - loadMidiFile(midi, &sh->midiSong, true); + loadMidiFile(song->midi, &sh->midiSong, true); // Set the file, but don't play yet midiPause(player, true); @@ -141,6 +172,7 @@ void shLoadSong(shVars_t* sh, const char* midi, const char* chart) // Set score, combo, and fail sh->score = 0; sh->combo = 0; + sh->maxCombo = 0; sh->failMeter = 50; } @@ -159,6 +191,8 @@ uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size) sh->numChartNotes = (data[dIdx++] << 8); sh->numChartNotes |= (data[dIdx++]); + sh->totalNotes = sh->numChartNotes; + sh->chartNotes = heap_caps_calloc(sh->numChartNotes, sizeof(shChartNote_t), MALLOC_CAP_SPIRAM); for (int32_t nIdx = 0; nIdx < sh->numChartNotes; nIdx++) @@ -184,6 +218,9 @@ uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size) sh->chartNotes[nIdx].hold = (data[dIdx + 0] << 8) | // (data[dIdx + 1] << 0); dIdx += 2; + + // Holds count as another note for letter ranking + sh->totalNotes++; } } @@ -202,6 +239,17 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) { if (sh->gameEnd) { + // Grade the performance + int32_t notePct = (100 * sh->notesHit) / sh->totalNotes; + for (int32_t gIdx = 0; gIdx < ARRAY_SIZE(grades); gIdx++) + { + if (notePct >= grades[gIdx].val) + { + sh->grade = grades[gIdx].letter; + break; + } + } + // Save score to NVS int32_t oldHs = 0; if (!readNvs32(sh->hsKey, &oldHs)) @@ -214,6 +262,7 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) if (sh->score > oldHs) { writeNvs32(sh->hsKey, sh->score); + // TODO also write grade? } // Switch to the game end screen @@ -397,7 +446,7 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) removeEntry(&sh->gameNotes, toRemove); // Note that it was missed - sh->hitText = hit_miss; + sh->hitText = timings[ARRAY_SIZE(timings) - 1].label; // Set a timer to not show the text forever sh->textTimerUs = SH_TEXT_TIME; } @@ -505,7 +554,7 @@ void shDrawGame(shVars_t* sh) int16_t tWidth = textWidth(&sh->ibm, sh->hitText); drawText(&sh->ibm, c555, sh->hitText, (TFT_WIDTH - tWidth) / 2, 100); - if ((hit_fantastic != sh->hitText) && (hit_miss != sh->hitText)) + if ((timings[0].label != sh->hitText) && (timings[ARRAY_SIZE(timings) - 1].label != sh->hitText)) { tWidth = textWidth(&sh->ibm, sh->timingText); drawText(&sh->ibm, sh->timingText == hit_early ? c335 : c533, sh->timingText, (TFT_WIDTH - tWidth) / 2, @@ -570,6 +619,12 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) sh->btnState = evt->state; + if (PB_B < evt->button) + { + // TODO handle non-face buttons + return; + } + int32_t notePressed = sh->btnToNote[31 - __builtin_clz(evt->button)]; if (-1 != notePressed) { @@ -601,46 +656,28 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) usOff = ABS(usOff); // Check if this button hit a note - bool gameNoteHit = false; - + bool gameNoteHit = false; int32_t baseScore = 0; // Classify the time off - if (usOff < 21500) - { - sh->hitText = hit_fantastic; - gameNoteHit = true; - baseScore = 5; - } - else if (usOff < 43000) + for (int32_t tIdx = 0; tIdx < ARRAY_SIZE(timings); tIdx++) { - sh->hitText = hit_marvelous; - gameNoteHit = true; - baseScore = 4; - } - else if (usOff < 102000) - { - sh->hitText = hit_great; - gameNoteHit = true; - baseScore = 3; - } - else if (usOff < 135000) - { - sh->hitText = hit_decent; - gameNoteHit = true; - baseScore = 2; - } - else if (usOff < 180000) - { - sh->hitText = hit_way_off; - gameNoteHit = true; - baseScore = 1; - } - else - { - sh->hitText = hit_miss; - // Break the combo - shMissNote(sh); + if (usOff <= timings[tIdx].timing) + { + sh->hitText = timings[tIdx].label; + if (INT32_MAX != timings[tIdx].timing) + { + // Note hit + gameNoteHit = true; + baseScore = ARRAY_SIZE(timings) - tIdx - 1; + } + else + { + // Break the combo + shMissNote(sh); + } + break; + } } // Set a timer to not show the text forever @@ -689,7 +726,7 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) if (false == noteMatch && evt->down) { // Total miss - sh->hitText = hit_miss; + sh->hitText = timings[ARRAY_SIZE(timings) - 1].label; shMissNote(sh); } } @@ -717,7 +754,7 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) */ static int32_t getMultiplier(shVars_t* sh) { - return MIN(4, (sh->combo + 10) / 10); + return (sh->combo + 10) / 10; } /** @@ -738,11 +775,19 @@ static void shSongOver(void) */ static void shHitNote(shVars_t* sh, int32_t baseScore) { + sh->noteHistogram[ARRAY_SIZE(timings) - 1 - baseScore]++; + // Increment the score sh->score += getMultiplier(sh) * baseScore; + sh->notesHit++; + // Increment the combo & fail meter sh->combo++; + if (sh->combo > sh->maxCombo) + { + sh->maxCombo = sh->combo; + } if (sh->failOn) { @@ -757,6 +802,8 @@ static void shHitNote(shVars_t* sh, int32_t baseScore) */ static void shMissNote(shVars_t* sh) { + sh->noteHistogram[ARRAY_SIZE(timings) - 1]++; + sh->combo = 0; if (sh->failOn) { diff --git a/main/modes/games/swadgeHero/swadgeHero_game.h b/main/modes/games/swadgeHero/swadgeHero_game.h index 7bdc32b0a..2cb37db2b 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.h +++ b/main/modes/games/swadgeHero/swadgeHero_game.h @@ -4,7 +4,15 @@ #define NUM_FAIL_METER_SAMPLES 200 -void shLoadSong(shVars_t* sh, const char* midi, const char* chart); +typedef struct +{ + int32_t timing; + const char* label; +} shTimingGrade_t; + +extern const shTimingGrade_t timings[6]; // TODO don't hardcode + +void shLoadSong(shVars_t* sh, const shSong_t* song, shDifficulty_t difficulty); uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size); void shGameInput(shVars_t* sh, buttonEvt_t* evt); bool shRunTimers(shVars_t* sh, uint32_t elapsedUs); diff --git a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c index 843275a6a..834a722cc 100644 --- a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c +++ b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c @@ -49,6 +49,8 @@ void shGameEndInput(shVars_t* sh, buttonEvt_t* evt) /** * @brief TODO doc * + * TODO better measurement + * * @param sh * @param elapsedUs */ @@ -56,11 +58,62 @@ void shGameEndDraw(shVars_t* sh, int32_t elapsedUs) { clearPxTft(); - // Draw the score + int32_t yOff = 8; + + // Draw the name + int16_t tWidth = textWidth(&sh->rodin, sh->songName); + drawText(&sh->rodin, c555, sh->songName, (TFT_WIDTH - tWidth) / 2, yOff); + yOff += sh->rodin.height + 19; + + // Draw all note count labels + int32_t yCounts = yOff; + int32_t maxXoff = 0; + for (int32_t i = 0; i < 6; i++) + { + int32_t xOff = drawText(&sh->ibm, c555, timings[i].label, 8, yOff); + if (xOff > maxXoff) + { + maxXoff = xOff; + } + yOff += sh->ibm.height + 8; + } + + // Draw all note counts + yOff = yCounts; + int32_t xVals = maxXoff + 8; + for (int32_t i = 0; i < 6; i++) + { + char histText[32]; + snprintf(histText, sizeof(histText), "%" PRId32, sh->noteHistogram[i]); + int32_t xOff = drawText(&sh->ibm, c555, histText, xVals, yOff); + if (xOff > maxXoff) + { + maxXoff = xOff; + } + yOff += sh->ibm.height + 8; + } + + // Reset Y offset + yOff = 52; + + // Draw letter char scoreStr[32]; + snprintf(scoreStr, sizeof(scoreStr) - 1, "%s", sh->grade); + tWidth = textWidth(&sh->rodin, scoreStr); + drawText(&sh->rodin, c555, scoreStr, maxXoff + (TFT_WIDTH - maxXoff - tWidth) / 2, yOff); + yOff += sh->rodin.height + 8; + + // Draw the score snprintf(scoreStr, sizeof(scoreStr) - 1, "%" PRId32, sh->score); - int16_t tWidth = textWidth(&sh->rodin, scoreStr); - drawText(&sh->rodin, c555, scoreStr, (TFT_WIDTH - tWidth) / 2, (TFT_HEIGHT - sh->rodin.height) / 2); + tWidth = textWidth(&sh->rodin, scoreStr); + drawText(&sh->rodin, c555, scoreStr, maxXoff + (TFT_WIDTH - maxXoff - tWidth) / 2, yOff); + yOff += sh->rodin.height + 8; + + // Draw max combo + snprintf(scoreStr, sizeof(scoreStr) - 1, "Combo: %" PRId32, sh->maxCombo); + tWidth = textWidth(&sh->rodin, scoreStr); + drawText(&sh->rodin, c555, scoreStr, maxXoff + (TFT_WIDTH - maxXoff - tWidth) / 2, yOff); + yOff += sh->rodin.height + 8; // Draw a graph of the fail meter if (sh->failOn) From 2ce94da278dd657f29bcc8ed28652d6671f38707 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sat, 5 Oct 2024 00:37:47 +0000 Subject: [PATCH 48/54] Use a define for NUM_NOTE_TIMINGS Save letter grade to high scores Better measurement for game over screen Fix bug with bad default speed --- main/modes/games/swadgeHero/mode_swadgeHero.h | 8 ++- main/modes/games/swadgeHero/swadgeHero_game.c | 35 +++++++---- main/modes/games/swadgeHero/swadgeHero_game.h | 3 +- .../games/swadgeHero/swadgeHero_gameEnd.c | 51 ++++++++++------ main/modes/games/swadgeHero/swadgeHero_menu.c | 61 +++++++++---------- 5 files changed, 93 insertions(+), 65 deletions(-) diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index a895f1285..0bc897ec4 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -7,6 +7,12 @@ #include #include +//============================================================================== +// Defines +//============================================================================== + +#define NUM_NOTE_TIMINGS 6 + //============================================================================== // Enums //============================================================================== @@ -110,7 +116,7 @@ typedef struct int32_t totalNotes; int32_t notesHit; const char* grade; - int32_t noteHistogram[6]; // TODO don't hardcode length + int32_t noteHistogram[NUM_NOTE_TIMINGS]; // Fret line data list_t fretLines; diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 069a1ad94..1cc97002f 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -48,14 +48,14 @@ static const char hit_early[] = "Early"; static const char hit_late[] = "Late"; static const shLetterGrade_t grades[] = { - {.val = 100, .letter = "S"}, // - {.val = 97, .letter = "A+"}, {.val = 93, .letter = "A"}, {.val = 90, .letter = "A-"}, {.val = 87, .letter = "B+"}, - {.val = 83, .letter = "B"}, {.val = 80, .letter = "B-"}, {.val = 77, .letter = "C+"}, {.val = 73, .letter = "C"}, - {.val = 70, .letter = "C-"}, {.val = 67, .letter = "D+"}, {.val = 63, .letter = "D"}, {.val = 0, .letter = "F"}, + {.val = 100, .letter = "S"}, // + {.val = 97, .letter = "A+"}, {.val = 93, .letter = "A"}, {.val = 90, .letter = "A-"}, // + {.val = 87, .letter = "B+"}, {.val = 83, .letter = "B"}, {.val = 80, .letter = "B-"}, // + {.val = 77, .letter = "C+"}, {.val = 73, .letter = "C"}, {.val = 70, .letter = "C-"}, // + {.val = 67, .letter = "D+"}, {.val = 63, .letter = "D"}, {.val = 0, .letter = "F"}, }; -// TODO don't hardcode -const shTimingGrade_t timings[6] = { +const shTimingGrade_t timings[NUM_NOTE_TIMINGS] = { {.timing = 21500, .label = "Fantastic"}, // {.timing = 43000, .label = "Marvelous"}, // {.timing = 102000, .label = "Great"}, // @@ -241,11 +241,12 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) { // Grade the performance int32_t notePct = (100 * sh->notesHit) / sh->totalNotes; - for (int32_t gIdx = 0; gIdx < ARRAY_SIZE(grades); gIdx++) + int gradeIdx; + for (gradeIdx = 0; gradeIdx < ARRAY_SIZE(grades); gradeIdx++) { - if (notePct >= grades[gIdx].val) + if (notePct >= grades[gradeIdx].val) { - sh->grade = grades[gIdx].letter; + sh->grade = grades[gradeIdx].letter; break; } } @@ -261,8 +262,9 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) // Write the high score to NVS if it's larger if (sh->score > oldHs) { - writeNvs32(sh->hsKey, sh->score); - // TODO also write grade? + // four top bits are letter, bottom 28 bits are score + int32_t nvsScore = ((gradeIdx & 0x0F) << 28) | (sh->score & 0x0FFFFFFF); + writeNvs32(sh->hsKey, nvsScore); } // Switch to the game end screen @@ -814,3 +816,14 @@ static void shMissNote(shVars_t* sh) } } } + +/** + * @brief TODO + * + * @param gradeIdx + * @return const char* + */ +const char* getLetterGrade(int32_t gradeIdx) +{ + return grades[gradeIdx].letter; +} diff --git a/main/modes/games/swadgeHero/swadgeHero_game.h b/main/modes/games/swadgeHero/swadgeHero_game.h index 2cb37db2b..789e47560 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.h +++ b/main/modes/games/swadgeHero/swadgeHero_game.h @@ -10,10 +10,11 @@ typedef struct const char* label; } shTimingGrade_t; -extern const shTimingGrade_t timings[6]; // TODO don't hardcode +extern const shTimingGrade_t timings[NUM_NOTE_TIMINGS]; void shLoadSong(shVars_t* sh, const shSong_t* song, shDifficulty_t difficulty); uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size); void shGameInput(shVars_t* sh, buttonEvt_t* evt); bool shRunTimers(shVars_t* sh, uint32_t elapsedUs); void shDrawGame(shVars_t* sh); +const char* getLetterGrade(int32_t gradeIdx); diff --git a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c index 834a722cc..61f69b07b 100644 --- a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c +++ b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c @@ -49,39 +49,55 @@ void shGameEndInput(shVars_t* sh, buttonEvt_t* evt) /** * @brief TODO doc * - * TODO better measurement - * * @param sh * @param elapsedUs */ void shGameEndDraw(shVars_t* sh, int32_t elapsedUs) { +// Space between lines +#define TEXT_Y_SPACING 8 +// Spacing for the fail chart +#define Y_MARGIN 14 +#define Y_HEIGHT 50 +#define X_MARGIN ((TFT_WIDTH - NUM_FAIL_METER_SAMPLES) / 2) +#define BOX_MARGIN 2 + clearPxTft(); - int32_t yOff = 8; + int32_t yOff = Y_MARGIN; // Draw the name int16_t tWidth = textWidth(&sh->rodin, sh->songName); drawText(&sh->rodin, c555, sh->songName, (TFT_WIDTH - tWidth) / 2, yOff); - yOff += sh->rodin.height + 19; + yOff += sh->rodin.height; + + // This is the text area between the title and fail chart + int32_t textAreaTop = yOff; + int32_t textAreaBottom = TFT_HEIGHT - Y_MARGIN - Y_HEIGHT - BOX_MARGIN; + + // Vertically center the six timings + yOff = textAreaTop + + ((textAreaBottom - textAreaTop) - (NUM_NOTE_TIMINGS * sh->ibm.height) + - ((NUM_NOTE_TIMINGS - 1) * TEXT_Y_SPACING)) + / 2; // Draw all note count labels int32_t yCounts = yOff; int32_t maxXoff = 0; - for (int32_t i = 0; i < 6; i++) + for (int32_t i = 0; i < NUM_NOTE_TIMINGS; i++) { - int32_t xOff = drawText(&sh->ibm, c555, timings[i].label, 8, yOff); + int32_t xOff = drawText(&sh->ibm, c555, timings[i].label, TEXT_Y_SPACING, yOff); if (xOff > maxXoff) { maxXoff = xOff; } - yOff += sh->ibm.height + 8; + yOff += sh->ibm.height + TEXT_Y_SPACING; } // Draw all note counts yOff = yCounts; - int32_t xVals = maxXoff + 8; - for (int32_t i = 0; i < 6; i++) + int32_t xVals = maxXoff + TEXT_Y_SPACING; + for (int32_t i = 0; i < NUM_NOTE_TIMINGS; i++) { char histText[32]; snprintf(histText, sizeof(histText), "%" PRId32, sh->noteHistogram[i]); @@ -90,39 +106,34 @@ void shGameEndDraw(shVars_t* sh, int32_t elapsedUs) { maxXoff = xOff; } - yOff += sh->ibm.height + 8; + yOff += sh->ibm.height + TEXT_Y_SPACING; } - // Reset Y offset - yOff = 52; + // Vertically center the three score parts + yOff = textAreaTop + ((textAreaBottom - textAreaTop) - (3 * sh->rodin.height) - (2 * TEXT_Y_SPACING)) / 2; // Draw letter char scoreStr[32]; snprintf(scoreStr, sizeof(scoreStr) - 1, "%s", sh->grade); tWidth = textWidth(&sh->rodin, scoreStr); drawText(&sh->rodin, c555, scoreStr, maxXoff + (TFT_WIDTH - maxXoff - tWidth) / 2, yOff); - yOff += sh->rodin.height + 8; + yOff += sh->rodin.height + TEXT_Y_SPACING; // Draw the score snprintf(scoreStr, sizeof(scoreStr) - 1, "%" PRId32, sh->score); tWidth = textWidth(&sh->rodin, scoreStr); drawText(&sh->rodin, c555, scoreStr, maxXoff + (TFT_WIDTH - maxXoff - tWidth) / 2, yOff); - yOff += sh->rodin.height + 8; + yOff += sh->rodin.height + TEXT_Y_SPACING; // Draw max combo snprintf(scoreStr, sizeof(scoreStr) - 1, "Combo: %" PRId32, sh->maxCombo); tWidth = textWidth(&sh->rodin, scoreStr); drawText(&sh->rodin, c555, scoreStr, maxXoff + (TFT_WIDTH - maxXoff - tWidth) / 2, yOff); - yOff += sh->rodin.height + 8; + yOff += sh->rodin.height + TEXT_Y_SPACING; // Draw a graph of the fail meter if (sh->failOn) { -#define Y_MARGIN 20 -#define Y_HEIGHT 50 -#define X_MARGIN ((TFT_WIDTH - NUM_FAIL_METER_SAMPLES) / 2) -#define BOX_MARGIN 2 - drawRect(X_MARGIN - BOX_MARGIN, TFT_HEIGHT - Y_MARGIN - Y_HEIGHT - BOX_MARGIN, // TFT_WIDTH - X_MARGIN + BOX_MARGIN, TFT_HEIGHT - Y_MARGIN + BOX_MARGIN, c220); diff --git a/main/modes/games/swadgeHero/swadgeHero_menu.c b/main/modes/games/swadgeHero/swadgeHero_menu.c index f8449f5ab..87622ccab 100644 --- a/main/modes/games/swadgeHero/swadgeHero_menu.c +++ b/main/modes/games/swadgeHero/swadgeHero_menu.c @@ -3,6 +3,7 @@ //============================================================================== #include "swadgeHero_menu.h" +#include "swadgeHero_game.h" #include "mainMenu.h" //============================================================================== @@ -39,6 +40,10 @@ static const char strHighScores[] = "High Scores"; static const char strSettings[] = "Settings"; static const char strExit[] = "Exit"; +static const char easyStr_noscore[] = "Easy:"; +static const char mediumStr_noscore[] = "Medium:"; +static const char hardStr_noscore[] = "Hard:"; + const char* shs_fail_key = "shs_fail"; const char* shs_fail_label = "Song Fail: "; const char* shs_fail_opts[] = {"On", "Off"}; @@ -92,42 +97,34 @@ void shSetupMenu(shVars_t* sh) { sh->menu = startSubMenu(sh->menu, shSongList[sIdx].name); - // Allocate and print high score strings - // Save them all to a linked list to free later - - // Variables for reading from NVS - int32_t tmpScore; - char nvsKey[16]; + // Helpers for building the high score menu + const shDifficulty_t difficulties[] = {SH_EASY, SH_MEDIUM, SH_HARD}; + const char* labels[] = {easyStr_noscore, mediumStr_noscore, hardStr_noscore}; - shGetNvsKey(shSongList[sIdx].midi, SH_EASY, nvsKey); - if (!readNvs32(nvsKey, &tmpScore)) + for (int32_t i = 0; i < ARRAY_SIZE(difficulties); i++) { - tmpScore = 0; - } - char* easyStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); - snprintf(easyStr, HS_STR_LEN - 1, "Easy: %" PRId32, tmpScore); - addSingleItemToMenu(sh->menu, easyStr); - push(&sh->hsStrs, easyStr); + char nvsKey[16]; + shGetNvsKey(shSongList[sIdx].midi, difficulties[i], nvsKey); - shGetNvsKey(shSongList[sIdx].name, SH_MEDIUM, nvsKey); - if (!readNvs32(nvsKey, &tmpScore)) - { - tmpScore = 0; - } - char* mediumStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); - snprintf(mediumStr, HS_STR_LEN - 1, "Medium: %" PRId32, tmpScore); - addSingleItemToMenu(sh->menu, mediumStr); - push(&sh->hsStrs, mediumStr); + int32_t tmpScore; + if (readNvs32(nvsKey, &tmpScore)) + { + int32_t gradeIdx = (tmpScore >> 28) & 0x0F; + tmpScore &= 0x0FFFFFFF; - shGetNvsKey(shSongList[sIdx].name, SH_HARD, nvsKey); - if (!readNvs32(nvsKey, &tmpScore)) - { - tmpScore = 0; + // Allocate and print high score strings + char* hsStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); + snprintf(hsStr, HS_STR_LEN - 1, "%s %" PRId32 " %s", labels[i], tmpScore, getLetterGrade(gradeIdx)); + addSingleItemToMenu(sh->menu, hsStr); + + // Save them all to a linked list to free later + push(&sh->hsStrs, hsStr); + } + else + { + addSingleItemToMenu(sh->menu, labels[i]); + } } - char* hardStr = heap_caps_calloc(1, sizeof(char) * HS_STR_LEN, MALLOC_CAP_SPIRAM); - snprintf(hardStr, HS_STR_LEN - 1, "Hard: %" PRId32, tmpScore); - addSingleItemToMenu(sh->menu, hardStr); - push(&sh->hsStrs, hardStr); sh->menu = endSubMenu(sh->menu); } @@ -315,5 +312,5 @@ int32_t shGetSettingSpeed(void) { return speedSetting; } - return 0; + return shs_speed_vals[1]; } \ No newline at end of file From 0e5b70dba10e69e2d540e89ac1b060142325e828 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sat, 5 Oct 2024 22:17:00 +0000 Subject: [PATCH 49/54] Style game end screen --- .../games/swadgeHero/swadgeHero_gameEnd.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c index 61f69b07b..e74b19966 100644 --- a/main/modes/games/swadgeHero/swadgeHero_gameEnd.c +++ b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c @@ -57,19 +57,21 @@ void shGameEndDraw(shVars_t* sh, int32_t elapsedUs) // Space between lines #define TEXT_Y_SPACING 8 // Spacing for the fail chart -#define Y_MARGIN 14 +#define Y_MARGIN 8 #define Y_HEIGHT 50 #define X_MARGIN ((TFT_WIDTH - NUM_FAIL_METER_SAMPLES) / 2) #define BOX_MARGIN 2 - clearPxTft(); + // Draw background + fillDisplayArea(0, 0, TFT_WIDTH, 2 * Y_MARGIN + sh->rodin.height, c000); + fillDisplayArea(0, 2 * Y_MARGIN + sh->rodin.height, TFT_WIDTH, TFT_HEIGHT, c111); int32_t yOff = Y_MARGIN; // Draw the name int16_t tWidth = textWidth(&sh->rodin, sh->songName); drawText(&sh->rodin, c555, sh->songName, (TFT_WIDTH - tWidth) / 2, yOff); - yOff += sh->rodin.height; + yOff += sh->rodin.height + Y_MARGIN; // This is the text area between the title and fail chart int32_t textAreaTop = yOff; @@ -84,9 +86,11 @@ void shGameEndDraw(shVars_t* sh, int32_t elapsedUs) // Draw all note count labels int32_t yCounts = yOff; int32_t maxXoff = 0; + + const paletteColor_t timingColors[NUM_NOTE_TIMINGS] = {c051, c141, c231, c321, c411, c501}; for (int32_t i = 0; i < NUM_NOTE_TIMINGS; i++) { - int32_t xOff = drawText(&sh->ibm, c555, timings[i].label, TEXT_Y_SPACING, yOff); + int32_t xOff = drawText(&sh->ibm, timingColors[i], timings[i].label, TEXT_Y_SPACING, yOff); if (xOff > maxXoff) { maxXoff = xOff; @@ -115,8 +119,9 @@ void shGameEndDraw(shVars_t* sh, int32_t elapsedUs) // Draw letter char scoreStr[32]; snprintf(scoreStr, sizeof(scoreStr) - 1, "%s", sh->grade); - tWidth = textWidth(&sh->rodin, scoreStr); - drawText(&sh->rodin, c555, scoreStr, maxXoff + (TFT_WIDTH - maxXoff - tWidth) / 2, yOff); + tWidth = textWidth(&sh->rodin, scoreStr); + paletteColor_t letterColor = 'S' == sh->grade[0] ? c225 : timingColors[sh->grade[0] - 'A']; + drawText(&sh->rodin, letterColor, scoreStr, maxXoff + (TFT_WIDTH - maxXoff - tWidth) / 2, yOff); yOff += sh->rodin.height + TEXT_Y_SPACING; // Draw the score @@ -126,7 +131,7 @@ void shGameEndDraw(shVars_t* sh, int32_t elapsedUs) yOff += sh->rodin.height + TEXT_Y_SPACING; // Draw max combo - snprintf(scoreStr, sizeof(scoreStr) - 1, "Combo: %" PRId32, sh->maxCombo); + snprintf(scoreStr, sizeof(scoreStr) - 1, "%" PRId32 " Combo", sh->maxCombo); tWidth = textWidth(&sh->rodin, scoreStr); drawText(&sh->rodin, c555, scoreStr, maxXoff + (TFT_WIDTH - maxXoff - tWidth) / 2, yOff); yOff += sh->rodin.height + TEXT_Y_SPACING; From 4f757a125cb562c91adbcc3d6e799190c7001c3c Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sat, 5 Oct 2024 23:24:37 +0000 Subject: [PATCH 50/54] Draw a star on note hit --- assets/swadgeHero/star.png | Bin 0 -> 196 bytes main/modes/games/swadgeHero/mode_swadgeHero.c | 6 ++ main/modes/games/swadgeHero/mode_swadgeHero.h | 2 + main/modes/games/swadgeHero/swadgeHero_game.c | 57 +++++++++++++++++- 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 assets/swadgeHero/star.png diff --git a/assets/swadgeHero/star.png b/assets/swadgeHero/star.png new file mode 100644 index 0000000000000000000000000000000000000000..178d7f277154fcb7cc2223cd3af3c2714d95ae60 GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8gy!3HEdYt9+~DVAa<&kznEsNqQI0P?FnT^vI! zdhdof@--;%MECyxUt~K`VbfaYy%(fCSH^~$2s=u7Euu|5m&O>eYRClx7F0-eF&>FVdQ&MBb@0AWu^EdT%j literal 0 HcmV?d00001 diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.c b/main/modes/games/swadgeHero/mode_swadgeHero.c index 9d55861ca..ed1cbb5e7 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.c +++ b/main/modes/games/swadgeHero/mode_swadgeHero.c @@ -81,6 +81,7 @@ static void shEnterMode(void) { loadWsg(icons[i], &shv->icons[i], true); } + loadWsg("star.wsg", &shv->star, true); // Show initial menu shChangeScreen(shv, SH_MENU); @@ -103,6 +104,7 @@ static void shExitMode(void) { freeWsg(&shv->icons[i]); } + freeWsg(&shv->star); // Free mode memory free(shv); @@ -226,6 +228,10 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) { free(val); } + while ((val = pop(&shv->starList))) + { + free(val); + } break; } case SH_GAME_END: diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index 0bc897ec4..ff27dd330 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -130,6 +130,8 @@ typedef struct const char* timingText; int32_t textTimerUs; wsg_t icons[6]; + wsg_t star; + list_t starList; // High score display list_t hsStrs; diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 1cc97002f..ede317d76 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -25,6 +25,13 @@ typedef struct const char* letter; } shLetterGrade_t; +typedef struct +{ + int32_t x; + int32_t y; + int32_t timer; +} drawStar_t; + //============================================================================== // Const Variables //============================================================================== @@ -72,6 +79,7 @@ static int32_t getMultiplier(shVars_t* sh); static void shSongOver(void); static void shMissNote(shVars_t* sh); static void shHitNote(shVars_t* sh, int32_t baseScore); +static int32_t getXoffset(shVars_t* sh, int32_t note); //============================================================================== // Functions @@ -290,6 +298,25 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) sh->textTimerUs -= elapsedUs; } + // Run timers for stars + node_t* starNode = sh->starList.first; + while (starNode) + { + drawStar_t* ds = starNode->val; + ds->timer -= elapsedUs; + if (0 >= ds->timer) + { + node_t* toRemove = starNode; + starNode = starNode->next; + free(toRemove->val); + removeEntry(&sh->starList, toRemove); + } + else + { + starNode = starNode->next; + } + } + // Get a reference to the player midiPlayer_t* player = globalMidiPlayerGet(MIDI_BGM); @@ -519,7 +546,7 @@ void shDrawGame(shVars_t* sh) while (gameNoteNode) { shGameNote_t* gameNote = gameNoteNode->val; - int32_t xOffset = ((gameNote->note * TFT_WIDTH) / sh->numFrets) + (TFT_WIDTH / (2 * sh->numFrets)); + int32_t xOffset = getXoffset(sh, gameNote->note); // If there is a tail if (gameNote->tailPosY >= 0) @@ -550,6 +577,15 @@ void shDrawGame(shVars_t* sh) } } + // Draw stars + node_t* starNode = sh->starList.first; + while (starNode) + { + drawStar_t* ds = starNode->val; + drawWsgSimple(&sh->star, ds->x, ds->y); + starNode = starNode->next; + } + // Draw text if (sh->textTimerUs > 0) { @@ -690,6 +726,13 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) { shHitNote(sh, baseScore); + // Draw a star for a moment + drawStar_t* ds = heap_caps_calloc(1, sizeof(drawStar_t), MALLOC_CAP_SPIRAM); + ds->x = getXoffset(sh, gameNote->note) - sh->star.w / 2; + ds->y = gameNote->headPosY - (sh->star.h / 2); + ds->timer = 250000; + push(&sh->starList, ds); + if (gameNote->tailPosY >= 0) { // There is a tail, don't remove the note yet @@ -827,3 +870,15 @@ const char* getLetterGrade(int32_t gradeIdx) { return grades[gradeIdx].letter; } + +/** + * @brief TODO + * + * @param sh + * @param note + * @return int32_t + */ +static int32_t getXoffset(shVars_t* sh, int32_t note) +{ + return ((note * TFT_WIDTH) / sh->numFrets) + (TFT_WIDTH / (2 * sh->numFrets)); +} From 0ac18cb0cf84d946a8daaa5a78c17a1ad6e13cf2 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sun, 6 Oct 2024 00:35:31 +0000 Subject: [PATCH 51/54] Add LED blink to tempo --- main/modes/games/swadgeHero/mode_swadgeHero.h | 6 ++++ main/modes/games/swadgeHero/swadgeHero_game.c | 30 ++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index ff27dd330..b138a9e6b 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -122,6 +122,12 @@ typedef struct list_t fretLines; int32_t lastFretLineUs; + // LED data + int32_t nextBlinkUs; + int8_t ledBaseVal; + int32_t ledDecayTimer; + int32_t usPerLedDecay; + // Drawing data list_t gameNotes; buttonBit_t btnState; diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index ede317d76..0754a2df5 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -163,6 +163,9 @@ void shLoadSong(shVars_t* sh, const shSong_t* song, shDifficulty_t difficulty) int32_t songLenUs = SAMPLES_TO_US(player->sampleCount); globalMidiPlayerStop(true); + // Save the LED decay rate based on tempo + sh->usPerLedDecay = sh->tempo / (2 * 0xFF); + // Figure out how often to sample the fail meter for the chart after the song sh->failSampleInterval = songLenUs / NUM_FAIL_METER_SAMPLES; @@ -352,6 +355,24 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) push(&sh->fretLines, fretLine); } + // Decay LEDs + sh->ledDecayTimer += elapsedUs; + while (sh->ledDecayTimer >= sh->usPerLedDecay) + { + sh->ledDecayTimer -= sh->usPerLedDecay; + if (sh->ledBaseVal) + { + sh->ledBaseVal--; + } + } + + // Blink based on tempo + if (songUs >= sh->nextBlinkUs) + { + sh->nextBlinkUs += sh->tempo; + sh->ledBaseVal = 0xFF; + } + // Check events until one hasn't happened yet or the song ends while (sh->currentChartNote < sh->numChartNotes) { @@ -626,13 +647,8 @@ void shDrawGame(shVars_t* sh) } // Set LEDs - led_t leds[CONFIG_NUM_LEDS] = {0}; - // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) - // { - // leds[i].r = (255 * ((i + 0) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // leds[i].g = (255 * ((i + 3) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // leds[i].b = (255 * ((i + 6) % CONFIG_NUM_LEDS)) / (CONFIG_NUM_LEDS - 1); - // } + led_t leds[CONFIG_NUM_LEDS]; + memset(leds, sh->ledBaseVal, sizeof(leds)); setLeds(leds, CONFIG_NUM_LEDS); } From eb593b676c1ff5a13d8d2c6dca10a2507312ec5c Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sun, 6 Oct 2024 18:54:13 +0000 Subject: [PATCH 52/54] Adjust emulated LEDs to match hotdog --- components/hdw-led/hdw-led.c | 37 +++++-------------- components/hdw-led/include/hdw-led.h | 4 --- emulator/src/components/hdw-led/hdw-led.c | 8 ++--- emulator/src/extensions/leds/ext_leds.c | 44 ++++++++++++++--------- makefile | 3 +- sdkconfig | 2 +- 6 files changed, 41 insertions(+), 57 deletions(-) diff --git a/components/hdw-led/hdw-led.c b/components/hdw-led/hdw-led.c index f50f8ca6c..96ea2109a 100644 --- a/components/hdw-led/hdw-led.c +++ b/components/hdw-led/hdw-led.c @@ -21,10 +21,10 @@ // Variables //============================================================================== -static rmt_channel_handle_t led_chan = NULL; -static rmt_encoder_handle_t led_encoder = NULL; -static uint8_t ledBrightness = 0; -static led_t localLeds[CONFIG_NUM_LEDS + 1] = {0}; +static rmt_channel_handle_t led_chan = NULL; +static rmt_encoder_handle_t led_encoder = NULL; +static uint8_t ledBrightness = 0; +static led_t localLeds[CONFIG_NUM_LEDS] = {0}; //============================================================================== // Functions @@ -111,9 +111,9 @@ esp_err_t setLeds(led_t* leds, uint8_t numLeds) }; // Make sure to not overflow - if (numLeds > CONFIG_NUM_LEDS + 1) + if (numLeds > CONFIG_NUM_LEDS) { - numLeds = CONFIG_NUM_LEDS + 1; + numLeds = CONFIG_NUM_LEDS; } // Fill a local copy of LEDs with brightness applied @@ -124,27 +124,6 @@ esp_err_t setLeds(led_t* leds, uint8_t numLeds) localLeds[i].b = (leds[i].b >> ledBrightness); } - // If all eight LEDs are being set, but not the 9th - if (CONFIG_NUM_LEDS == numLeds) - { - // Set the 9th LED to the average of the 6th, 7th, and 8th - int32_t avgR = 0; - int32_t avgG = 0; - int32_t avgB = 0; - for (int32_t lIdx = 5; lIdx < 8; lIdx++) - { - avgR += leds[lIdx].r; - avgG += leds[lIdx].g; - avgB += leds[lIdx].b; - } - localLeds[CONFIG_NUM_LEDS].r = (avgR / 3) >> ledBrightness; - localLeds[CONFIG_NUM_LEDS].g = (avgG / 3) >> ledBrightness; - localLeds[CONFIG_NUM_LEDS].b = (avgB / 3) >> ledBrightness; - - // Set the 9th LED too - numLeds = CONFIG_NUM_LEDS + 1; - } - // Write RGB values to LEDs return rmt_transmit(led_chan, led_encoder, (uint8_t*)localLeds, numLeds * sizeof(led_t), &tx_config); } @@ -160,9 +139,9 @@ uint8_t getLedState(led_t* leds, uint8_t numLeds) { if (NULL != leds && numLeds > 0) { - if (numLeds > CONFIG_NUM_LEDS + 1) + if (numLeds > CONFIG_NUM_LEDS) { - numLeds = CONFIG_NUM_LEDS + 1; + numLeds = CONFIG_NUM_LEDS; } memcpy(leds, localLeds, sizeof(led_t) * numLeds); diff --git a/components/hdw-led/include/hdw-led.h b/components/hdw-led/include/hdw-led.h index 085c34ffd..b66c8aa4e 100644 --- a/components/hdw-led/include/hdw-led.h +++ b/components/hdw-led/include/hdw-led.h @@ -30,10 +30,6 @@ * must be flushed before entering light sleep. If they are not, garbage data may be sent after light sleep begins, * resulting in indeterminate LED behavior. * - * Even though \c CONFIG_NUM_LEDS declares eight LEDs, there is a ninth LED which is controllable. By default, setting - * \c CONFIG_NUM_LEDS LEDs will automatically set the ninth to the average of the sixth, seventh, and eighth, which - * surround it on the PCB. To set the ninth LED, set `CONFIG_NUM_LEDS + 1` LEDs. - * * \section led_example Example * * Set the LEDs to a rough rainbow: diff --git a/emulator/src/components/hdw-led/hdw-led.c b/emulator/src/components/hdw-led/hdw-led.c index 70b783f45..e85e7e5f8 100644 --- a/emulator/src/components/hdw-led/hdw-led.c +++ b/emulator/src/components/hdw-led/hdw-led.c @@ -11,8 +11,8 @@ // Variables //============================================================================== -static led_t rdLeds[CONFIG_NUM_LEDS + 1] = {0}; -static uint8_t ledBrightness = 0; +static led_t rdLeds[CONFIG_NUM_LEDS] = {0}; +static uint8_t ledBrightness = 0; //============================================================================== // Functions @@ -82,9 +82,9 @@ uint8_t getLedState(led_t* leds, uint8_t numLeds) { if (NULL != leds && numLeds > 0) { - if (numLeds > CONFIG_NUM_LEDS + 1) + if (numLeds > CONFIG_NUM_LEDS) { - numLeds = CONFIG_NUM_LEDS + 1; + numLeds = CONFIG_NUM_LEDS; } memcpy(leds, rdLeds, sizeof(led_t) * numLeds); diff --git a/emulator/src/extensions/leds/ext_leds.c b/emulator/src/extensions/leds/ext_leds.c index bd0af1b02..9d4d0e3b6 100644 --- a/emulator/src/extensions/leds/ext_leds.c +++ b/emulator/src/extensions/leds/ext_leds.c @@ -3,6 +3,7 @@ #include "hdw-led.h" #include "hdw-led_emu.h" #include "macros.h" +#include "vector2d.h" #include "rawdraw_sf.h" @@ -10,7 +11,7 @@ // Defines //============================================================================== -#define MIN_LED_WIDTH 64 +#define MIN_LED_HEIGHT 64 //============================================================================== // Static Function Prototypes @@ -37,9 +38,9 @@ emuExtension_t ledEmuExtension = { // Where LEDs are drawn, kinda // first value is the LED column (top-to-bottom(?)) // second value is the row -static const int16_t ledOffsets[8][2] = { - {1, 2}, {0, 3}, {0, 1}, {1, 0}, // Left side LEDs - {2, 0}, {3, 1}, {3, 3}, {2, 2}, // Right side LEDs +static const vec_t ledOffsets[CONFIG_NUM_LEDS] = { + {.x = 4, .y = 1}, {.x = 5, .y = 0}, {.x = 3, .y = 0}, {.x = 1, .y = 0}, {.x = 2, .y = 1}, + {.x = 0, .y = 2}, {.x = 2, .y = 2}, {.x = 4, .y = 2}, {.x = 6, .y = 2}, }; //============================================================================== @@ -47,7 +48,7 @@ static const int16_t ledOffsets[8][2] = { //============================================================================== /** - * @brief Initializes the + * @brief Initializes the LED panes * * @param args * @return true if the extension is enabled @@ -57,8 +58,8 @@ static bool ledsExtInit(emuArgs_t* args) { if (!args->hideLeds) { - requestPane(&ledEmuExtension, PANE_LEFT, MIN_LED_WIDTH * 2, 1); - requestPane(&ledEmuExtension, PANE_RIGHT, MIN_LED_WIDTH * 2, 1); + requestPane(&ledEmuExtension, PANE_TOP, 1, MIN_LED_HEIGHT); + requestPane(&ledEmuExtension, PANE_BOTTOM, 1, MIN_LED_HEIGHT * 2); return true; } @@ -91,17 +92,26 @@ static void drawLeds(uint32_t winW, uint32_t winH, const emuPane_t* panes, uint8 { for (int i = 0; i < MIN(numLeds, ARRAY_SIZE(ledOffsets)); i++) { - // Use left pane for offsets 0 and 1, right for offsets 2 and 3 - const emuPane_t* pane = (ledOffsets[i][0] < 2) ? (panes + 0) : (panes + 1); - - int16_t ledH = pane->paneH / (numLeds / 2); - int16_t ledW = pane->paneW / 2; - - int16_t xOffset = pane->paneX + (ledOffsets[i][0] % 2) * (ledW / 2); - int16_t yOffset = ledOffsets[i][1] * ledH; - + int16_t xOffset, yOffset, ledW, ledH; + // Use top pane for offset 0, bottom for offsets 1 & 2 + if (0 == ledOffsets[i].y) + { + ledH = panes[0].paneH; + ledW = panes[0].paneW / 4; + + xOffset = panes[0].paneX + (ledOffsets[i].x * ledW) / 2; + yOffset = panes[0].paneY + (ledOffsets[i].y * ledH); + } + else + { + ledH = panes[1].paneH / 2; + ledW = panes[1].paneW / 4; + + xOffset = panes[1].paneX + (ledOffsets[i].x * ledW) / 2; + yOffset = panes[1].paneY + ((ledOffsets[i].y - 1) * ledH); + } CNFGColor((leds[i].r << 24) | (leds[i].g << 16) | (leds[i].b << 8) | 0xFF); - CNFGTackRectangle(xOffset, yOffset, xOffset + ledW * 3 / 2, yOffset + ledH); + CNFGTackRectangle(xOffset, yOffset, xOffset + ledW, yOffset + ledH); } } } diff --git a/makefile b/makefile index 83837a17b..4d176d25a 100644 --- a/makefile +++ b/makefile @@ -100,7 +100,6 @@ CFLAGS = \ -fdata-sections \ -gdwarf-4 \ -ggdb \ - -O2 \ -fno-jump-tables \ -finline-functions \ -std=gnu17 @@ -215,7 +214,7 @@ DEFINES_LIST = \ CONFIG_GC9307_240x280=y \ CONFIG_TFT_MAX_BRIGHTNESS=200 \ CONFIG_TFT_MIN_BRIGHTNESS=10 \ - CONFIG_NUM_LEDS=8 \ + CONFIG_NUM_LEDS=9 \ configENABLE_FREERTOS_DEBUG_OCDAWARE=1 \ _GNU_SOURCE \ IDF_VER="v5.3.1" \ diff --git a/sdkconfig b/sdkconfig index 0f2bc6a7d..b246fc5f3 100644 --- a/sdkconfig +++ b/sdkconfig @@ -466,7 +466,7 @@ CONFIG_SOUND_OUTPUT_SPEAKER=y # # LED Configuration # -CONFIG_NUM_LEDS=8 +CONFIG_NUM_LEDS=9 # end of LED Configuration # From a57530ed8d3f57d8a32a5c0f1b5e4a03f116f9b1 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sun, 6 Oct 2024 19:41:48 +0000 Subject: [PATCH 53/54] Recolor menu LEDs --- main/menu/menuManiaRenderer.c | 21 ++++++++++++------- main/menu/menuManiaRenderer.h | 3 ++- main/modes/games/swadgeHero/swadgeHero_menu.c | 3 ++- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/main/menu/menuManiaRenderer.c b/main/menu/menuManiaRenderer.c index a3c6fe6e9..d74497d7f 100644 --- a/main/menu/menuManiaRenderer.c +++ b/main/menu/menuManiaRenderer.c @@ -93,6 +93,12 @@ menuManiaRenderer_t* initMenuManiaRenderer(font_t* titleFont, font_t* titleFontO renderer->shadowColors = defaultShadowColors; renderer->shadowColorsLen = ARRAY_SIZE(defaultShadowColors); + // LED color + uint32_t ledColor = paletteToRGB(renderer->bgColor); + renderer->baseLedColor.r = ((ledColor >> 16) & 0xFF) / 2; + renderer->baseLedColor.g = ((ledColor >> 8) & 0xFF) / 2; + renderer->baseLedColor.b = ((ledColor >> 0) & 0xFF) / 2; + // Save or allocate title font if (NULL == titleFont) { @@ -379,14 +385,11 @@ void drawMenuMania(menu_t* menu, menuManiaRenderer_t* renderer, int64_t elapsedU { // Run timer for LED excitation renderer->ledExciteTimer += elapsedUs; - while (renderer->ledExciteTimer >= 40000 * 8) + while (renderer->ledExciteTimer >= 40000 * CONFIG_NUM_LEDS) { - renderer->ledExciteTimer -= 40000 * 8; - uint32_t ledColor = paletteToRGB(renderer->bgColor); - renderer->leds[renderer->currentLed].r = ((ledColor >> 16) & 0xFF) / 2; - renderer->leds[renderer->currentLed].g = ((ledColor >> 8) & 0xFF) / 2; - renderer->leds[renderer->currentLed].b = ((ledColor >> 0) & 0xFF) / 2; - renderer->currentLed = (renderer->currentLed + 1) % CONFIG_NUM_LEDS; + renderer->ledExciteTimer -= 40000 * CONFIG_NUM_LEDS; + renderer->leds[renderer->currentLed] = renderer->baseLedColor; + renderer->currentLed = (renderer->currentLed + 1) % CONFIG_NUM_LEDS; } // Run timer for LED decay @@ -662,11 +665,12 @@ void setManiaLedsOn(menuManiaRenderer_t* renderer, bool ledsOn) * @param rowTextColor The color of the row text * @param shadowColors The colors cycled through as the selected shadow * @param shadowColorsLen The number of selected shadow colors to cycle through + * @param baseLedColor The color of the LED illumination */ void recolorMenuManiaRenderer(menuManiaRenderer_t* renderer, paletteColor_t titleBgColor, paletteColor_t titleTextColor, paletteColor_t textOutlineColor, paletteColor_t bgColor, paletteColor_t outerRingColor, paletteColor_t innerRingColor, paletteColor_t rowColor, paletteColor_t rowTextColor, - const paletteColor_t* shadowColors, int32_t shadowColorsLen) + const paletteColor_t* shadowColors, int32_t shadowColorsLen, led_t baseLedColor) { renderer->titleBgColor = titleBgColor; renderer->titleTextColor = titleTextColor; @@ -681,4 +685,5 @@ void recolorMenuManiaRenderer(menuManiaRenderer_t* renderer, paletteColor_t titl renderer->shadowColors = shadowColors; renderer->shadowColorsLen = shadowColorsLen; renderer->selectedShadowIdx = 0; + renderer->baseLedColor = baseLedColor; } diff --git a/main/menu/menuManiaRenderer.h b/main/menu/menuManiaRenderer.h index e24d618e8..a0a6e65f0 100644 --- a/main/menu/menuManiaRenderer.h +++ b/main/menu/menuManiaRenderer.h @@ -94,6 +94,7 @@ typedef struct paletteColor_t rowTextColor; ///< The color of the row text const paletteColor_t* shadowColors; ///< The colors cycled through as the selected shadow int32_t shadowColorsLen; ///< The number of selected shadow colors to cycle through + led_t baseLedColor; ///< The base color of the LED rotation } menuManiaRenderer_t; menuManiaRenderer_t* initMenuManiaRenderer(font_t* titleFont, font_t* titleFontOutline, font_t* menuFont); @@ -103,6 +104,6 @@ void setManiaLedsOn(menuManiaRenderer_t* renderer, bool ledsOn); void recolorMenuManiaRenderer(menuManiaRenderer_t* renderer, paletteColor_t titleBgColor, paletteColor_t titleTextColor, paletteColor_t textOutlineColor, paletteColor_t bgColor, paletteColor_t outerRingColor, paletteColor_t innerRingColor, paletteColor_t rowColor, paletteColor_t rowTextColor, - const paletteColor_t* shadowColors, int32_t shadowColorsLen); + const paletteColor_t* shadowColors, int32_t shadowColorsLen, led_t baseLedColor); #endif \ No newline at end of file diff --git a/main/modes/games/swadgeHero/swadgeHero_menu.c b/main/modes/games/swadgeHero/swadgeHero_menu.c index 87622ccab..3381818bd 100644 --- a/main/modes/games/swadgeHero/swadgeHero_menu.c +++ b/main/modes/games/swadgeHero/swadgeHero_menu.c @@ -70,12 +70,13 @@ void shSetupMenu(shVars_t* sh) sh->renderer = initMenuManiaRenderer(&sh->righteous, NULL, &sh->rodin); static const paletteColor_t shadowColors[] = {c110, c210, c220, c320, c330, c430, c330, c320, c220, c210}; + led_t ledColor = {.r = 0x80, .g = 0x00, .b = 0x00}; recolorMenuManiaRenderer(sh->renderer, // c431, c100, c100, // Title colors (bg, text, outline) c111, // Background c200, c210, // Rings c000, c444, // Rows - shadowColors, ARRAY_SIZE(shadowColors)); + shadowColors, ARRAY_SIZE(shadowColors), ledColor); setManiaLedsOn(sh->renderer, true); // Add songs to play From fd81b8ebf6ce2a38982c60fdc8f7f8e983dfea34 Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Sun, 6 Oct 2024 22:28:05 +0000 Subject: [PATCH 54/54] Add LED effects on note hit --- main/modes/games/swadgeHero/mode_swadgeHero.h | 1 + main/modes/games/swadgeHero/swadgeHero_game.c | 46 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/main/modes/games/swadgeHero/mode_swadgeHero.h b/main/modes/games/swadgeHero/mode_swadgeHero.h index b138a9e6b..6de93eb55 100644 --- a/main/modes/games/swadgeHero/mode_swadgeHero.h +++ b/main/modes/games/swadgeHero/mode_swadgeHero.h @@ -127,6 +127,7 @@ typedef struct int8_t ledBaseVal; int32_t ledDecayTimer; int32_t usPerLedDecay; + led_t ledHitVal; // Drawing data list_t gameNotes; diff --git a/main/modes/games/swadgeHero/swadgeHero_game.c b/main/modes/games/swadgeHero/swadgeHero_game.c index 0754a2df5..8b3e90b27 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -360,10 +360,26 @@ bool shRunTimers(shVars_t* sh, uint32_t elapsedUs) while (sh->ledDecayTimer >= sh->usPerLedDecay) { sh->ledDecayTimer -= sh->usPerLedDecay; + + // Tempo LEDs if (sh->ledBaseVal) { sh->ledBaseVal--; } + + // LEDs from note hits + if (sh->ledHitVal.r) + { + sh->ledHitVal.r--; + } + if (sh->ledHitVal.g) + { + sh->ledHitVal.g--; + } + if (sh->ledHitVal.b) + { + sh->ledHitVal.b--; + } } // Blink based on tempo @@ -648,7 +664,12 @@ void shDrawGame(shVars_t* sh) // Set LEDs led_t leds[CONFIG_NUM_LEDS]; - memset(leds, sh->ledBaseVal, sizeof(leds)); + memset(leds, sh->ledBaseVal, sizeof(led_t) * 5); + + for (int32_t i = 5; i < CONFIG_NUM_LEDS; i++) + { + leds[i] = sh->ledHitVal; + } setLeds(leds, CONFIG_NUM_LEDS); } @@ -749,6 +770,29 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) ds->timer = 250000; push(&sh->starList, ds); + // Light some LEDs + if (4 <= baseScore) + { + // Greenish + sh->ledHitVal.r = 0x00; + sh->ledHitVal.g = 0xFF; + sh->ledHitVal.b = 0x00; + } + else if (hit_late == sh->timingText) + { + // Reddish + sh->ledHitVal.r = 0xFF; + sh->ledHitVal.g = 0x00; + sh->ledHitVal.b = 0x00; + } + else + { + // Blueish + sh->ledHitVal.r = 0x00; + sh->ledHitVal.g = 0x00; + sh->ledHitVal.b = 0xFF; + } + if (gameNote->tailPosY >= 0) { // There is a tail, don't remove the note yet