From 69da2a1c7e092337eda9e2e8361560656e30fd7d Mon Sep 17 00:00:00 2001 From: gelakinetic Date: Fri, 4 Oct 2024 20:53:37 -0400 Subject: [PATCH] Swadge Hero (#300) Lots of Swadge Hero Progress Added recolorMenuManiaRenderer() --- 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/CMakeLists.txt | 1 + main/menu/menuManiaRenderer.c | 104 ++- main/menu/menuManiaRenderer.h | 15 + main/midi/midiPlayer.c | 11 +- main/midi/midiPlayer.h | 3 +- main/modes/games/swadgeHero/mode_swadgeHero.c | 109 ++- main/modes/games/swadgeHero/mode_swadgeHero.h | 51 +- main/modes/games/swadgeHero/swadgeHero_game.c | 706 ++++++++++++++---- main/modes/games/swadgeHero/swadgeHero_game.h | 15 +- .../games/swadgeHero/swadgeHero_gameEnd.c | 164 ++++ .../games/swadgeHero/swadgeHero_gameEnd.h | 6 + main/modes/games/swadgeHero/swadgeHero_menu.c | 138 +++- main/modes/games/swadgeHero/swadgeHero_menu.h | 3 + 19 files changed, 1088 insertions(+), 238 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 create mode 100644 main/modes/games/swadgeHero/swadgeHero_gameEnd.c create mode 100644 main/modes/games/swadgeHero/swadgeHero_gameEnd.h 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/CMakeLists.txt b/main/CMakeLists.txt index 6f5f36cb8..ab30b2288 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/ultimateTTTcpuPlayer.c" 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/midi/midiPlayer.c b/main/midi/midiPlayer.c index 848c1d39b..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; } @@ -2353,7 +2360,7 @@ void midiSeek(midiPlayer_t* player, uint32_t ticks) stopped = !player->eventAvailable && !(player->eventAvailable = midiNextEvent(&player->reader, &player->pendingEvent)); - if (stopped) + if (stopped && (uint32_t)-1 != ticks) { midiSongEnd(player); 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 6b4aec1f8..9d55861ca 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" //============================================================================== @@ -49,7 +50,7 @@ shVars_t* shv; //============================================================================== /** - * @brief TODO + * @brief TODO doc * * @return */ @@ -75,6 +76,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 +99,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); } @@ -122,7 +134,10 @@ static void shMainLoop(int64_t elapsedUs) break; } case SH_GAME_END: - case SH_HIGH_SCORES: + { + shGameEndInput(shv, &evt); + break; + } case SH_NONE: default: { @@ -136,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: @@ -146,7 +163,10 @@ static void shMainLoop(int64_t elapsedUs) break; } case SH_GAME_END: - case SH_HIGH_SCORES: + { + shGameEndDraw(shv, elapsedUs); + break; + } case SH_NONE: default: { @@ -172,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 @@ -190,6 +210,7 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) case SH_GAME: { // Free MIDI data + globalMidiPlayerStop(true); unloadMidiFile(&shv->midiSong); // Free chart data @@ -201,13 +222,22 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) { free(val); } + while ((val = pop(&shv->fretLines))) + { + free(val); + } break; } case SH_GAME_END: - case SH_HIGH_SCORES: + { + // Free fail samples + clear(&shv->failSamples); + break; + } case SH_NONE: default: { + // Nothing tear down break; } } @@ -220,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: @@ -249,11 +259,52 @@ void shChangeScreen(shVars_t* sh, shScreen_t newScreen) break; } case SH_GAME_END: - case SH_HIGH_SCORES: + { + // Nothing to set up + break; + } 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 0147059e9..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 //============================================================================== @@ -17,7 +23,6 @@ typedef enum SH_MENU, SH_GAME, SH_GAME_END, - SH_HIGH_SCORES, } shScreen_t; typedef enum @@ -50,12 +55,19 @@ typedef struct typedef struct { int32_t note; - int32_t timer; + int32_t headTimeUs; + int32_t tailTimeUs; int32_t headPosY; int32_t tailPosY; bool held; } shGameNote_t; +typedef struct +{ + int32_t headTimeUs; + int32_t headPosY; +} shFretLine_t; + typedef struct { // Font and menu @@ -74,6 +86,8 @@ typedef struct // Song being played midiFile_t midiSong; int32_t leadInUs; + char hsKey[16]; + const char* songName; // Chart data int32_t numChartNotes; @@ -82,11 +96,43 @@ typedef struct paletteColor_t const* colors; buttonBit_t const* noteToBtn; int32_t const* btnToNote; + int32_t const* noteToIcon; + int32_t tempo; + + // Fail meter tracking + int32_t failMeter; + int32_t failSampleInterval; + list_t failSamples; + + // Setting data + bool failOn; + int32_t scrollTime; + + // 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[NUM_NOTE_TIMINGS]; + + // 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; + wsg_t icons[6]; + + // High score display + list_t hsStrs; } shVars_t; //============================================================================== @@ -101,3 +147,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 28ce1ccd1..1cc97002f 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.c +++ b/main/modes/games/swadgeHero/swadgeHero_game.c @@ -2,79 +2,182 @@ // Includes //============================================================================== +#include #include "swadgeHero_game.h" +#include "swadgeHero_menu.h" //============================================================================== // Defines //============================================================================== #define HIT_BAR 16 -#define GAME_NOTE_RADIUS 8 +#define GAME_NOTE_HEIGHT 16 +#define GAME_NOTE_WIDTH 24 -#define TRAVEL_US_PER_PX ((TRAVEL_TIME_US) / (TFT_HEIGHT - HIT_BAR + (2 * GAME_NOTE_RADIUS))) +#define SH_TEXT_TIME 500000 + +//============================================================================== +// Enums +//============================================================================== +typedef struct +{ + int32_t val; + const char* letter; +} shLetterGrade_t; //============================================================================== // 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, 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[] = {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_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"}, +}; + +const shTimingGrade_t timings[NUM_NOTE_TIMINGS] = { + {.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 +//============================================================================== + +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 - * @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) { - size_t sz = 0; - sh->numFrets = 1 + shLoadChartData(sh, cnfsGetFile(chart, &sz), sz); + // Don't immediately quit + sh->gameEnd = false; - 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) + // Load settings + sh->failOn = shGetSettingFail(); + sh->scrollTime = shGetSettingSpeed(); + + // Save song name + sh->songName = song->name; + + // Pick variables based on difficulty + const char* chartFile; + switch (difficulty) { - sh->btnToNote = btnToNote_e; - sh->noteToBtn = noteToBtn_e; - sh->colors = colors_e; + 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(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); - globalMidiPlayerPlaySong(&sh->midiSong, MIDI_BGM); - globalMidiPlayerPauseAll(); + loadMidiFile(song->midi, &sh->midiSong, true); + + // Set the file, but don't play yet + midiPause(player, true); + player->sampleCount = 0; + midiSetFile(player, &sh->midiSong); + + // 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); + + // Figure out how often to sample the fail meter for the chart after the song + sh->failSampleInterval = songLenUs / NUM_FAIL_METER_SAMPLES; // 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; + + 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); + + // Set score, combo, and fail + sh->score = 0; + sh->combo = 0; + sh->maxCombo = 0; + sh->failMeter = 50; } /** - * @brief TODO + * @brief TODO doc * * @param sh * @param data @@ -88,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++) @@ -109,10 +214,13 @@ 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; + + // Holds count as another note for letter ranking + sh->totalNotes++; } } @@ -120,13 +228,50 @@ uint32_t shLoadChartData(shVars_t* sh, const uint8_t* data, size_t size) } /** - * @brief TODO + * @brief TODO doc * * @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) + { + // Grade the performance + int32_t notePct = (100 * sh->notesHit) / sh->totalNotes; + int gradeIdx; + for (gradeIdx = 0; gradeIdx < ARRAY_SIZE(grades); gradeIdx++) + { + if (notePct >= grades[gradeIdx].val) + { + sh->grade = grades[gradeIdx].letter; + break; + } + } + + // 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) + { + // 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 + 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) { @@ -134,11 +279,17 @@ void shRunTimers(shVars_t* sh, uint32_t elapsedUs) if (sh->leadInUs <= 0) { - globalMidiPlayerResumeAll(); + globalMidiPlayerPlaySongCb(&sh->midiSong, MIDI_BGM, shSongOver); sh->leadInUs = 0; } } + // 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); @@ -153,6 +304,27 @@ void 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) + { + // Save the sample to display on the game over screen + push(&sh->failSamples, (void*)((intptr_t)sh->failMeter)); + } + } + + // Generate fret lines based on tempo + int32_t nextFretLineUs = sh->lastFretLineUs + sh->tempo; + if (songUs + sh->scrollTime >= 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,33 +333,36 @@ 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); ni->note = sh->chartNotes[sh->currentChartNote].note; - ni->headPosY = TFT_HEIGHT + (GAME_NOTE_RADIUS * 2); + + // Start the game note offscreen + ni->headPosY = TFT_HEIGHT + (GAME_NOTE_HEIGHT / 2); + + // 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 + (GAME_NOTE_HEIGHT / 2); + + // 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 { // No tail - ni->tailPosY = -1; + ni->tailPosY = -1; + ni->tailTimeUs = -1; } - // Start the timer at zero - ni->timer = 0; - // Push into the list of game notes push(&sh->gameNotes, ni); @@ -201,74 +376,81 @@ 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) { // 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 + if (gameNote->held) { - gameNote->timer -= TRAVEL_US_PER_PX; - - bool shouldRemove = false; + // 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)) / sh->scrollTime) + HIT_BAR; + } - // Move the whole game note up - if (!gameNote->held) - { - gameNote->headPosY--; - if (gameNote->tailPosY >= 0) - { - gameNote->tailPosY--; - } + // Update tail position if there is a hold + if (-1 != gameNote->tailPosY) + { + gameNote->tailPosY + = (((TFT_HEIGHT - HIT_BAR) * (gameNote->tailTimeUs - songUs)) / sh->scrollTime) + HIT_BAR; + } - // 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 + // Check if the note should be removed + bool shouldRemove = false; + if (-1 != gameNote->tailPosY) + { + // There is a tail + if (gameNote->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 + // Note is currently being held, tail reached the bar shouldRemove = true; + + // Note was held all the way, so score it. + shHitNote(sh, 5); } } - - // If the game note should be removed - if (shouldRemove) + else if (gameNote->tailPosY < 0) { - // Remove this game note - free(gameNoteNode->val); - removeEntry(&sh->gameNotes, gameNoteNode); - - // Stop the while timer loop - removed = true; - break; + // Note is not held, tail is offscreen + shouldRemove = true; } } + else if (gameNote->headPosY < -(GAME_NOTE_HEIGHT / 2)) + { + // There is no tail and the whole note is offscreen + shouldRemove = true; - // If an game note was removed - if (removed) + // Break the combo + shMissNote(sh); + } + + // If the game note should be removed + if (shouldRemove) { - // Stop iterating through notes - break; + // 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 = timings[ARRAY_SIZE(timings) - 1].label; + // Set a timer to not show the text forever + sh->textTimerUs = SH_TEXT_TIME; } else { @@ -276,10 +458,35 @@ 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)) / sh->scrollTime) + 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; + } + } + return true; } /** - * @brief TODO + * @brief TODO doc * * @param sh */ @@ -288,22 +495,31 @@ 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++) { + 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) @@ -313,6 +529,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; } @@ -322,11 +542,53 @@ 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]); + } + } + + // 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 ((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, + 100 + sh->ibm.height + 16); } } + 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); + + 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); + } + // Set LEDs led_t leds[CONFIG_NUM_LEDS] = {0}; // for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) @@ -339,17 +601,38 @@ void shDrawGame(shVars_t* sh) } /** - * @brief TODO + * @brief TODO doc * * @param sh * @param evt */ 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) + if (PB_B < evt->button) + { + // TODO handle non-face buttons + return; + } + + int32_t notePressed = sh->btnToNote[31 - __builtin_clz(evt->button)]; + if (-1 != notePressed) { + // 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) @@ -357,65 +640,80 @@ 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) { - // 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); + // Button event matches a note on screen somewhere + noteMatch = true; - // 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) + // Button was pressed + if (evt->down) { - 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"); - } + // Find how off the timing is + int32_t usOff = (songUs - gameNote->headTimeUs); - // If it was close enough to hit - if (gameNoteHit) - { - if (gameNote->tailPosY >= 0) + // 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 + for (int32_t tIdx = 0; tIdx < ARRAY_SIZE(timings); tIdx++) { - // There is a tail, don't remove the note yet - gameNote->headPosY = HIT_BAR; - gameNote->held = true; + 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; + } } - else + + // Set a timer to not show the text forever + sh->textTimerUs = SH_TEXT_TIME; + + // If it was close enough to hit + if (gameNoteHit) { - // No tail, remove the game note - node_t* nextNode = gameNoteNode->next; - free(gameNoteNode->val); - removeEntry(&sh->gameNotes, gameNoteNode); - gameNoteNode = nextNode; + 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; @@ -424,10 +722,15 @@ void shGameInput(shVars_t* sh, buttonEvt_t* evt) // Iterate to the next game note gameNoteNode = gameNoteNode->next; } - } - else - { - // TODO handle ups when holding tails + // Done iterating through notes + + // Check if a button down didn't have a matching note on screen + if (false == noteMatch && evt->down) + { + // Total miss + sh->hitText = timings[ARRAY_SIZE(timings) - 1].label; + shMissNote(sh); + } } // Check for analog touch @@ -437,11 +740,90 @@ 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 // int16_t a_x, a_y, a_z; // accelGetAccelVec(&a_x, &a_y, &a_z); } + +/** + * @brief TODO doc + * + * @param sh + * @return int32_t + */ +static int32_t getMultiplier(shVars_t* sh) +{ + return (sh->combo + 10) / 10; +} + +/** + * @brief TODO doc + * + */ +static void shSongOver(void) +{ + // Set a flag, end the game synchronously + getShVars()->gameEnd = true; +} + +/** + * @brief TODO doc + * + * @param sh + * @param baseScore + */ +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) + { + sh->failMeter = MIN(100, sh->failMeter + 1); + } +} + +/** + * @brief TODO doc + * + * @param sh + */ +static void shMissNote(shVars_t* sh) +{ + sh->noteHistogram[ARRAY_SIZE(timings) - 1]++; + + sh->combo = 0; + if (sh->failOn) + { + sh->failMeter = MAX(0, sh->failMeter - 2); + if (0 == sh->failMeter) + { + sh->gameEnd = true; + } + } +} + +/** + * @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 a692b5e35..789e47560 100644 --- a/main/modes/games/swadgeHero/swadgeHero_game.h +++ b/main/modes/games/swadgeHero/swadgeHero_game.h @@ -2,10 +2,19 @@ #include "mode_swadgeHero.h" -#define TRAVEL_TIME_US 2000000 +#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[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); -void shRunTimers(shVars_t* sh, uint32_t elapsedUs); +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 new file mode 100644 index 000000000..61f69b07b --- /dev/null +++ b/main/modes/games/swadgeHero/swadgeHero_gameEnd.c @@ -0,0 +1,164 @@ +//============================================================================== +// Includes +//============================================================================== + +#include "swadgeHero_gameEnd.h" +#include "swadgeHero_game.h" + +//============================================================================== +// Defines +//============================================================================== + +//============================================================================== +// Const Variables +//============================================================================== + +//============================================================================== +// Function Declarations +//============================================================================== + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief TODO doc + * + * @param sh + * @param evt + */ +void shGameEndInput(shVars_t* sh, buttonEvt_t* evt) +{ + if (evt->down) + { + switch (evt->button) + { + case PB_A: + { + shChangeScreen(sh, SH_MENU); + break; + } + default: + { + break; + } + } + } +} + +/** + * @brief TODO doc + * + * @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 = 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; + + // 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 < NUM_NOTE_TIMINGS; i++) + { + int32_t xOff = drawText(&sh->ibm, c555, timings[i].label, TEXT_Y_SPACING, yOff); + if (xOff > maxXoff) + { + maxXoff = xOff; + } + yOff += sh->ibm.height + TEXT_Y_SPACING; + } + + // Draw all note counts + yOff = yCounts; + 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]); + int32_t xOff = drawText(&sh->ibm, c555, histText, xVals, yOff); + if (xOff > maxXoff) + { + maxXoff = xOff; + } + yOff += sh->ibm.height + TEXT_Y_SPACING; + } + + // 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 + 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 + 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 + TEXT_Y_SPACING; + + // Draw a graph of the fail meter + if (sh->failOn) + { + 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; + } + } +} 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 4285887df..87622ccab 100644 --- a/main/modes/games/swadgeHero/swadgeHero_menu.c +++ b/main/modes/games/swadgeHero/swadgeHero_menu.c @@ -3,8 +3,15 @@ //============================================================================== #include "swadgeHero_menu.h" +#include "swadgeHero_game.h" #include "mainMenu.h" +//============================================================================== +// Defines +//============================================================================== + +#define HS_STR_LEN 32 + //============================================================================== // Function Declarations //============================================================================== @@ -33,12 +40,26 @@ 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"}; +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", "Normal", "Fast", "Turbo"}; +const int32_t shs_speed_vals[] = {4000000, 3000000, 2000000, 1000000}; + //============================================================================== // Functions //============================================================================== /** - * @brief TODO + * @brief TODO doc * * @param sh */ @@ -47,6 +68,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 @@ -66,12 +95,61 @@ 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); + + // 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}; + + for (int32_t i = 0; i < ARRAY_SIZE(difficulties); i++) + { + char nvsKey[16]; + shGetNvsKey(shSongList[sIdx].midi, difficulties[i], nvsKey); + + int32_t tmpScore; + if (readNvs32(nvsKey, &tmpScore)) + { + int32_t gradeIdx = (tmpScore >> 28) & 0x0F; + tmpScore &= 0x0FFFFFFF; + + // 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]); + } + } + + 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 @@ -79,19 +157,24 @@ void shSetupMenu(shVars_t* sh) } /** - * @brief TODO + * @brief TODO doc * * @param 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); } /** - * @brief TODO + * @brief TODO doc * * @param sh * @param btn @@ -102,7 +185,7 @@ void shMenuInput(shVars_t* sh, buttonEvt_t* btn) } /** - * @brief TODO + * @brief TODO doc * * @param sh */ @@ -187,4 +270,47 @@ 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 doc + * + * @return true + * @return false + */ +bool shGetSettingFail(void) +{ + int32_t failSetting; + if (readNvs32(shs_fail_key, &failSetting)) + { + return failSetting; + } + return true; } + +/** + * @brief TODO doc + * + * @return true + * @return false + */ +int32_t shGetSettingSpeed(void) +{ + int32_t speedSetting; + if (readNvs32(shs_speed_key, &speedSetting)) + { + return speedSetting; + } + return shs_speed_vals[1]; +} \ 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);