diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index b74392b5..f168b077 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -53,7 +53,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { consolePrint("Assertions real"); #endif - sceneSet(SCENE_TYPE_OVERWORLD); + sceneSet(SCENE_TYPE_INITIAL); errorOk(); diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index f84298ba..8b5c4d93 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -16,7 +16,6 @@ #include "time/time.h" #include "rpgcamera.h" #include "util/memory.h" -#include "util/string.h" #include "assert/assert.h" #include "save/save.h" #include "error/error.h" @@ -24,10 +23,6 @@ #include "ui/rpg/uiemoji.h" #include "rpg/story/storyflag.h" -static void rpgTestSaveComplete(errorret_t result, void *user) { - if(errorIsNotOk(result)) errorCatch(errorPrint(result)); -} - errorret_t rpgInit(void) { memoryZero(ENTITIES, sizeof(ENTITIES)); memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); @@ -71,13 +66,6 @@ errorret_t rpgInit(void) { backpackAdd(ITEM_ID_POTATO, 3); backpackAdd(ITEM_ID_APPLE, 8); - // TEST: Verify the save system round-trips real game data, not just the - // header/version. Remove once there's an actual name-entry flow. On PSP - // this shows the real native save dialog every boot - expected while - // testing that path, not something to ship as-is. - stringCopy(saveSlot->playerName, "Dusk", SAVE_PLAYER_NAME_MAX); - saveWriteSlot(SAVE_ACTIVE_SLOT, rpgTestSaveComplete, NULL); - // All Good! errorOk(); } diff --git a/src/dusk/save/save.c b/src/dusk/save/save.c index 709ddc67..a236f377 100644 --- a/src/dusk/save/save.c +++ b/src/dusk/save/save.c @@ -51,7 +51,15 @@ errorret_t saveInit(void) { } bool_t saveIsAvailable(void) { - return SAVE.available; + return SAVE.available && !SAVE.temporary; +} + +void saveMarkTemporary(void) { + SAVE.temporary = true; +} + +bool_t saveIsTemporary(void) { + return SAVE.temporary; } errorret_t saveDispose(void) { diff --git a/src/dusk/save/save.h b/src/dusk/save/save.h index 144ff587..19809b80 100644 --- a/src/dusk/save/save.h +++ b/src/dusk/save/save.h @@ -26,6 +26,15 @@ typedef struct { * conditions here, not fatal errors - see saveIsAvailable(). */ bool_t available; + /** + * True once the player has explicitly acknowledged there's no save + * device and chosen to continue anyway (see the initial scene's "no + * card" prompt) - sticky for the rest of the session, folded into + * saveIsAvailable() so every existing/future call site that already + * gates on that automatically refuses to save/load from here on. See + * saveMarkTemporary()/saveIsTemporary(). + */ + bool_t temporary; /** * Scratch error state used by platforms whose save/load completes * asynchronously (see saveIsBusy()) to construct a result to hand to a @@ -60,14 +69,35 @@ errorret_t saveInit(void); /** * Checks whether the save medium was reachable as of the last load/write - * attempt (or saveInit(), if none has been attempted yet). Intended for UI - * to decide whether to offer saving/loading at all, or to explain why it - * isn't available right now - e.g. "No memory card inserted". + * attempt (or saveInit(), if none has been attempted yet), AND the + * session hasn't been marked temporary (see saveMarkTemporary()). + * Intended for UI to decide whether to offer saving/loading at all, or to + * explain why it isn't available right now - e.g. "No memory card + * inserted". * - * @return true if the save medium was available last time it was checked. + * @return true if saving/loading can be attempted right now. */ bool_t saveIsAvailable(void); +/** + * Marks this session as temporary - the player explicitly acknowledged + * there's no save device and chose to continue anyway. One-way: nothing + * currently re-probes the save medium mid-session (see saveInit()'s doc + * comment), so there's no legitimate way to un-stick this once set short + * of restarting the game. + */ +void saveMarkTemporary(void); + +/** + * True once saveMarkTemporary() has been called this session. Distinct + * from saveIsAvailable() so UI can give a more specific message (e.g. + * "this session is temporary" rather than the generic "no save device + * found") once the player has already made that choice. + * + * @return true if this session has been marked temporary. + */ +bool_t saveIsTemporary(void); + /** * Disposes of the save system. * diff --git a/src/dusk/scene/CMakeLists.txt b/src/dusk/scene/CMakeLists.txt index b8480d21..8273e118 100644 --- a/src/dusk/scene/CMakeLists.txt +++ b/src/dusk/scene/CMakeLists.txt @@ -10,5 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} ) # Subdirs +add_subdirectory(initial) add_subdirectory(overworld) add_subdirectory(battle) \ No newline at end of file diff --git a/src/dusk/scene/initial/CMakeLists.txt b/src/dusk/scene/initial/CMakeLists.txt new file mode 100644 index 00000000..97ded407 --- /dev/null +++ b/src/dusk/scene/initial/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + sceneinitial.c +) diff --git a/src/dusk/scene/initial/sceneinitial.c b/src/dusk/scene/initial/sceneinitial.c new file mode 100644 index 00000000..9c3c5afb --- /dev/null +++ b/src/dusk/scene/initial/sceneinitial.c @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scene/scene.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "error/error.h" +#include "save/save.h" +#include "ui/frame/initial/uiinitialnocard.h" +#include "ui/frame/initial/uiinitialcreatesave.h" + +static void sceneInitialCheckSave(void); + +static void sceneInitialCreateSaveWriteComplete(errorret_t result, void *user) { + if(errorIsNotOk(result)) errorCatch(errorPrint(result)); + sceneSet(SCENE_TYPE_OVERWORLD); +} + +static void sceneInitialCreateSaveResult(const bool_t create, void *user) { + if(!create) { + sceneSet(SCENE_TYPE_OVERWORLD); + return; + } + + saveWriteSlot(SAVE_ACTIVE_SLOT, sceneInitialCreateSaveWriteComplete, NULL); +} + +static void sceneInitialNoCardResult(const bool_t retry, void *user) { + if(retry) { + sceneInitialCheckSave(); + return; + } + + // The player explicitly acknowledged there's no save device and chose + // to proceed anyway - stick with that for the rest of the session (see + // saveMarkTemporary()'s doc comment for why this can't be un-set later). + saveMarkTemporary(); + sceneSet(SCENE_TYPE_OVERWORLD); +} + +static void sceneInitialLoadComplete(errorret_t result, void *user) { + if(errorIsNotOk(result) || !saveIsAvailable()) { + errorCatch(result); + uiInitialNoCardOpen(sceneInitialNoCardResult, NULL); + return; + } + + if(saveSlotExists(SAVE_ACTIVE_SLOT)) { + sceneSet(SCENE_TYPE_OVERWORLD); + return; + } + + uiInitialCreateSaveOpen(sceneInitialCreateSaveResult, NULL); +} + +static void sceneInitialCheckSave(void) { + if(!saveIsAvailable()) { + uiInitialNoCardOpen(sceneInitialNoCardResult, NULL); + return; + } + + if(saveIsBusy()) return; + + saveLoadSlot(SAVE_ACTIVE_SLOT, sceneInitialLoadComplete, NULL); +} + +errorret_t sceneInitialInit(scenedata_t *sceneData) { + assertNotNull(sceneData, "Scene data cannot be null"); + memoryZero(&sceneData->initial, sizeof(sceneinitial_t)); + + sceneInitialCheckSave(); + + errorOk(); +} + +errorret_t sceneInitialUpdate(scenedata_t *sceneData) { + assertNotNull(sceneData, "Scene data cannot be null"); + errorOk(); +} + +errorret_t sceneInitialRender(scenedata_t *sceneData) { + assertNotNull(sceneData, "Scene data cannot be null"); + errorOk(); +} + +errorret_t sceneInitialDispose(scenedata_t *sceneData) { + assertNotNull(sceneData, "Scene data cannot be null"); + errorOk(); +} diff --git a/src/dusk/scene/initial/sceneinitial.h b/src/dusk/scene/initial/sceneinitial.h new file mode 100644 index 00000000..73b875e7 --- /dev/null +++ b/src/dusk/scene/initial/sceneinitial.h @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "scene/scenebase.h" + +// No per-scene state needed - the save globals and the two modal UI +// elements (see ui/frame/initial/) carry everything this scene cares +// about. A byte placeholder keeps the struct non-empty for portability. +typedef struct { + uint8_t reserved; +} sceneinitial_t; + +/** + * Initialises the initial (boot) scene - kicks off the save + * availability/existence check that decides which prompt, if any, to + * show before proceeding to the overworld. + * + * @param sceneData The scene data used for this scene. + * @return An error if the init failed, or errorOk() if it succeeded. + */ +errorret_t sceneInitialInit(scenedata_t *sceneData); + +/** + * Updates the initial scene. Currently a no-op - all the work happens in + * save callbacks and the two modal UI elements' own button handling. + * + * @param sceneData The scene data used for this scene. + * @return An error if the update failed, or errorOk() if it succeeded. + */ +errorret_t sceneInitialUpdate(scenedata_t *sceneData); + +/** + * Renders the initial scene. Currently a no-op - the modals draw + * themselves via the global UI element pipeline. + * + * @param sceneData The scene data used for this scene. + * @return An error if the render failed, or errorOk() if it succeeded. + */ +errorret_t sceneInitialRender(scenedata_t *sceneData); + +/** + * Disposes the initial scene. + * + * @param sceneData The scene data used for this scene. + * @return An error if the dispose failed, or errorOk() if it succeeded. + */ +errorret_t sceneInitialDispose(scenedata_t *sceneData); diff --git a/src/dusk/scene/scenetype.c b/src/dusk/scene/scenetype.c index da285e33..3dad6627 100644 --- a/src/dusk/scene/scenetype.c +++ b/src/dusk/scene/scenetype.c @@ -10,6 +10,13 @@ scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = { [SCENE_TYPE_NULL] = { 0 }, + [SCENE_TYPE_INITIAL] = { + .init = sceneInitialInit, + .update = sceneInitialUpdate, + .render = sceneInitialRender, + .dispose = sceneInitialDispose + }, + [SCENE_TYPE_OVERWORLD] = { .init = sceneOverworldInit, .update = sceneOverworldUpdate, diff --git a/src/dusk/scene/scenetype.h b/src/dusk/scene/scenetype.h index f3dd88bf..677959ee 100644 --- a/src/dusk/scene/scenetype.h +++ b/src/dusk/scene/scenetype.h @@ -7,10 +7,12 @@ #pragma once #include "scene/scenebase.h" +#include "scene/initial/sceneinitial.h" #include "scene/overworld/sceneoverworld.h" #include "scene/battle/scenebattle.h" typedef union scenedata_u { + sceneinitial_t initial; sceneoverworld_t overworld; scenebattle_t battle; } scenedata_t; @@ -27,6 +29,7 @@ typedef struct { typedef enum { SCENE_TYPE_NULL, + SCENE_TYPE_INITIAL, SCENE_TYPE_OVERWORLD, SCENE_TYPE_BATTLE, diff --git a/src/dusk/ui/frame/CMakeLists.txt b/src/dusk/ui/frame/CMakeLists.txt index 906cf620..6fe291b6 100644 --- a/src/dusk/ui/frame/CMakeLists.txt +++ b/src/dusk/ui/frame/CMakeLists.txt @@ -13,3 +13,4 @@ add_subdirectory(game) add_subdirectory(settings) add_subdirectory(battle) add_subdirectory(backpack) +add_subdirectory(initial) diff --git a/src/dusk/ui/frame/game/uigamemenu.c b/src/dusk/ui/frame/game/uigamemenu.c index 21582071..795f9a3d 100644 --- a/src/dusk/ui/frame/game/uigamemenu.c +++ b/src/dusk/ui/frame/game/uigamemenu.c @@ -81,6 +81,13 @@ static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) { } static void uiGameMenuSave(void) { + if(saveIsTemporary()) { + uiTextboxMainSetText( + "This session is temporary - no save device was found, so saving " + "is disabled." + ); + return; + } if(!saveIsAvailable()) { uiTextboxMainSetText("Can't save - no save device found."); return; diff --git a/src/dusk/ui/frame/initial/CMakeLists.txt b/src/dusk/ui/frame/initial/CMakeLists.txt new file mode 100644 index 00000000..a711dfa1 --- /dev/null +++ b/src/dusk/ui/frame/initial/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + uiinitialnocard.c + uiinitialcreatesave.c +) diff --git a/src/dusk/ui/frame/initial/uiinitialcreatesave.c b/src/dusk/ui/frame/initial/uiinitialcreatesave.c new file mode 100644 index 00000000..814b59ff --- /dev/null +++ b/src/dusk/ui/frame/initial/uiinitialcreatesave.c @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uiinitialcreatesave.h" +#include "ui/frame/uiframe.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/math.h" +#include "display/screen/screen.h" +#include "display/text/text.h" +#include "display/color.h" +#include "display/spritebatch/spritebatch.h" +#include "display/texture/texture.h" +#include "display/shader/shaderunlit.h" + +#define UI_INITIAL_CREATE_SAVE_BACKDROP_COLOR color4b(0, 0, 0, 160) +#define UI_INITIAL_CREATE_SAVE_TEXT "No save data found. Create a new save?" + +uiinitialcreatesave_t UI_INITIAL_CREATE_SAVE; + +static void uiInitialCreateSaveSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + UI_INITIAL_CREATE_SAVE.create = index == UI_INITIAL_CREATE_SAVE_INDEX_YES; + uiInitialCreateSaveClose(); +} + +static void uiInitialCreateSaveClosed(const uimenu_t *menu) { + if(UI_INITIAL_CREATE_SAVE.callback != NULL) { + UI_INITIAL_CREATE_SAVE.callback( + UI_INITIAL_CREATE_SAVE.create, UI_INITIAL_CREATE_SAVE.user + ); + } +} + +errorret_t uiInitialCreateSaveInit(void) { + memoryZero(&UI_INITIAL_CREATE_SAVE, sizeof(uiinitialcreatesave_t)); + + MENU_BEGIN( + &UI_INITIAL_CREATE_SAVE.menu, UI_INITIAL_CREATE_SAVE.items, + uiInitialCreateSaveSelected, uiInitialCreateSaveClosed, NULL + ); + MENU_BUTTON("Yes"); + MENU_BUTTON("No"); + + MENU_END(UI_INITIAL_CREATE_SAVE.items, menuIndex); + + errorOk(); +} + +errorret_t uiInitialCreateSaveDraw(void) { + if(!uiMenuIsActive(&UI_INITIAL_CREATE_SAVE.menu)) errorOk(); + + spritebatchsprite_t backdropSprite = { + .min = { 0.0f, 0.0f, 0.0f }, + .max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + shadermaterial_t backdropMaterial = { + .unlit = { + .color = UI_INITIAL_CREATE_SAVE_BACKDROP_COLOR, + .texture = &TEXTURE_WHITE + } + }; + errorChain( + spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial) + ); + errorChain(spriteBatchFlush()); + + int32_t textW, textH; + textMeasure(UI_INITIAL_CREATE_SAVE_TEXT, &FONT_DEFAULT, &textW, &textH); + + float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; + float_t width = mathMax( + (float_t)textW + (UI_FRAME_START_X * 2), UI_INITIAL_CREATE_SAVE_MIN_WIDTH + ); + float_t height = (UI_FRAME_START_Y * 2) + rowHeight + UI_FRAME_PADDING_Y + + rowHeight; + float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f; + float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f; + + errorChain(uiFrameDraw(x, y, width, height)); + + float_t contentX = x + UI_FRAME_START_X; + float_t contentY = y + UI_FRAME_START_Y; + float_t contentWidth = width - (UI_FRAME_START_X * 2); + + errorChain(textDraw( + contentX, contentY, UI_INITIAL_CREATE_SAVE_TEXT, COLOR_WHITE, &FONT_DEFAULT + )); + + float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y; + errorChain(uiMenuDraw( + &UI_INITIAL_CREATE_SAVE.menu, contentX, buttonsY, contentWidth, rowHeight + )); + + errorChain(spriteBatchFlush()); + errorOk(); +} + +bool_t uiInitialCreateSaveIsOpen(void) { + return uiMenuIsActive(&UI_INITIAL_CREATE_SAVE.menu); +} + +void uiInitialCreateSaveOpen( + uiinitialcreatesavecallback_t callback, void *user +) { + UI_INITIAL_CREATE_SAVE.callback = callback; + UI_INITIAL_CREATE_SAVE.user = user; + UI_INITIAL_CREATE_SAVE.create = false; + uiMenuOpen(&UI_INITIAL_CREATE_SAVE.menu); +} + +void uiInitialCreateSaveClose(void) { + uiMenuClose(&UI_INITIAL_CREATE_SAVE.menu); +} + +errorret_t uiInitialCreateSaveDispose(void) { + errorOk(); +} diff --git a/src/dusk/ui/frame/initial/uiinitialcreatesave.h b/src/dusk/ui/frame/initial/uiinitialcreatesave.h new file mode 100644 index 00000000..62eece1f --- /dev/null +++ b/src/dusk/ui/frame/initial/uiinitialcreatesave.h @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "ui/widget/uimenu.h" + +#define UI_INITIAL_CREATE_SAVE_INDEX_YES 0 +#define UI_INITIAL_CREATE_SAVE_INDEX_NO 1 +#define UI_INITIAL_CREATE_SAVE_ITEM_COUNT 2 +#define UI_INITIAL_CREATE_SAVE_MIN_WIDTH 160.0f + +/** + * Callback invoked once the create-save modal is dismissed. + * + * @param create True if Yes was selected, false if No was selected or + * the dialog was backed out of. + * @param user Arbitrary pointer passed to uiInitialCreateSaveOpen. + */ +typedef void (*uiinitialcreatesavecallback_t)(const bool_t create, void *user); + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_INITIAL_CREATE_SAVE_ITEM_COUNT]; + uiinitialcreatesavecallback_t callback; + void *user; + bool_t create; +} uiinitialcreatesave_t; + +extern uiinitialcreatesave_t UI_INITIAL_CREATE_SAVE; + +/** + * Initializes the create-save modal. + * + * @return Any error that occurs. + */ +errorret_t uiInitialCreateSaveInit(void); + +/** + * Draws the create-save modal: a semi-transparent black backdrop covering + * the whole screen, then its own centered frame with a fixed message and + * Yes/No buttons. No-op when not open. + * + * @return Any error that occurs. + */ +errorret_t uiInitialCreateSaveDraw(void); + +/** + * Returns true when the modal is currently open. + * + * @return True if open. + */ +bool_t uiInitialCreateSaveIsOpen(void); + +/** + * Opens the create-save modal. callback is invoked exactly once with the + * result, whether dismissed by selecting a button or backing out. + * + * @param callback Called with the result once the modal closes. May be + * NULL. + * @param user Arbitrary pointer passed through to callback. + */ +void uiInitialCreateSaveOpen(uiinitialcreatesavecallback_t callback, void *user); + +/** + * Closes the modal. No-op when already closed. + */ +void uiInitialCreateSaveClose(void); + +/** + * Disposes of the modal. + * + * @return Any error that occurs. + */ +errorret_t uiInitialCreateSaveDispose(void); diff --git a/src/dusk/ui/frame/initial/uiinitialnocard.c b/src/dusk/ui/frame/initial/uiinitialnocard.c new file mode 100644 index 00000000..83a60e70 --- /dev/null +++ b/src/dusk/ui/frame/initial/uiinitialnocard.c @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uiinitialnocard.h" +#include "ui/frame/uiframe.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/math.h" +#include "display/screen/screen.h" +#include "display/text/text.h" +#include "display/color.h" +#include "display/spritebatch/spritebatch.h" +#include "display/texture/texture.h" +#include "display/shader/shaderunlit.h" + +#define UI_INITIAL_NO_CARD_BACKDROP_COLOR color4b(0, 0, 0, 160) +#define UI_INITIAL_NO_CARD_TEXT \ + "No save device found. You can continue, but\nprogress will not be saved." + +uiinitialnocard_t UI_INITIAL_NO_CARD; + +static void uiInitialNoCardSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + UI_INITIAL_NO_CARD.retry = index == UI_INITIAL_NO_CARD_INDEX_RETRY; + uiInitialNoCardClose(); +} + +static void uiInitialNoCardClosed(const uimenu_t *menu) { + if(UI_INITIAL_NO_CARD.callback != NULL) { + UI_INITIAL_NO_CARD.callback(UI_INITIAL_NO_CARD.retry, UI_INITIAL_NO_CARD.user); + } +} + +errorret_t uiInitialNoCardInit(void) { + memoryZero(&UI_INITIAL_NO_CARD, sizeof(uiinitialnocard_t)); + + MENU_BEGIN( + &UI_INITIAL_NO_CARD.menu, UI_INITIAL_NO_CARD.items, + uiInitialNoCardSelected, uiInitialNoCardClosed, NULL + ); + MENU_BUTTON("Retry"); + MENU_BUTTON("Continue Anyway"); + + MENU_END(UI_INITIAL_NO_CARD.items, menuIndex); + + errorOk(); +} + +errorret_t uiInitialNoCardDraw(void) { + if(!uiMenuIsActive(&UI_INITIAL_NO_CARD.menu)) errorOk(); + + spritebatchsprite_t backdropSprite = { + .min = { 0.0f, 0.0f, 0.0f }, + .max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + shadermaterial_t backdropMaterial = { + .unlit = { + .color = UI_INITIAL_NO_CARD_BACKDROP_COLOR, + .texture = &TEXTURE_WHITE + } + }; + errorChain( + spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial) + ); + errorChain(spriteBatchFlush()); + + int32_t textW, textH; + textMeasure(UI_INITIAL_NO_CARD_TEXT, &FONT_DEFAULT, &textW, &textH); + + float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; + float_t width = mathMax( + (float_t)textW + (UI_FRAME_START_X * 2), UI_INITIAL_NO_CARD_MIN_WIDTH + ); + float_t height = (UI_FRAME_START_Y * 2) + (float_t)textH + + UI_FRAME_PADDING_Y + rowHeight; + float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f; + float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f; + + errorChain(uiFrameDraw(x, y, width, height)); + + float_t contentX = x + UI_FRAME_START_X; + float_t contentY = y + UI_FRAME_START_Y; + float_t contentWidth = width - (UI_FRAME_START_X * 2); + + errorChain(textDraw( + contentX, contentY, UI_INITIAL_NO_CARD_TEXT, COLOR_WHITE, &FONT_DEFAULT + )); + + float_t buttonsY = contentY + (float_t)textH + UI_FRAME_PADDING_Y; + errorChain(uiMenuDraw( + &UI_INITIAL_NO_CARD.menu, contentX, buttonsY, contentWidth, rowHeight + )); + + errorChain(spriteBatchFlush()); + errorOk(); +} + +bool_t uiInitialNoCardIsOpen(void) { + return uiMenuIsActive(&UI_INITIAL_NO_CARD.menu); +} + +void uiInitialNoCardOpen(uiinitialnocardcallback_t callback, void *user) { + UI_INITIAL_NO_CARD.callback = callback; + UI_INITIAL_NO_CARD.user = user; + UI_INITIAL_NO_CARD.retry = false; + uiMenuOpen(&UI_INITIAL_NO_CARD.menu); +} + +void uiInitialNoCardClose(void) { + uiMenuClose(&UI_INITIAL_NO_CARD.menu); +} + +errorret_t uiInitialNoCardDispose(void) { + errorOk(); +} diff --git a/src/dusk/ui/frame/initial/uiinitialnocard.h b/src/dusk/ui/frame/initial/uiinitialnocard.h new file mode 100644 index 00000000..e9c442a2 --- /dev/null +++ b/src/dusk/ui/frame/initial/uiinitialnocard.h @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "ui/widget/uimenu.h" + +#define UI_INITIAL_NO_CARD_INDEX_RETRY 0 +#define UI_INITIAL_NO_CARD_INDEX_CONTINUE 1 +#define UI_INITIAL_NO_CARD_ITEM_COUNT 2 +#define UI_INITIAL_NO_CARD_MIN_WIDTH 220.0f + +/** + * Callback invoked once the no-save-device modal is dismissed. + * + * @param retry True if Retry was selected, false if Continue Anyway was. + * @param user Arbitrary pointer passed to uiInitialNoCardOpen. + */ +typedef void (*uiinitialnocardcallback_t)(const bool_t retry, void *user); + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_INITIAL_NO_CARD_ITEM_COUNT]; + uiinitialnocardcallback_t callback; + void *user; + bool_t retry; +} uiinitialnocard_t; + +extern uiinitialnocard_t UI_INITIAL_NO_CARD; + +/** + * Initializes the no-save-device modal. + * + * @return Any error that occurs. + */ +errorret_t uiInitialNoCardInit(void); + +/** + * Draws the no-save-device modal: a semi-transparent black backdrop + * covering the whole screen, then its own centered frame with a fixed + * message and Retry/Continue Anyway buttons. No-op when not open. + * + * @return Any error that occurs. + */ +errorret_t uiInitialNoCardDraw(void); + +/** + * Returns true when the modal is currently open. + * + * @return True if open. + */ +bool_t uiInitialNoCardIsOpen(void); + +/** + * Opens the no-save-device modal. callback is invoked exactly once with + * the result, whether dismissed by selecting a button or backing out. + * + * @param callback Called with the result once the modal closes. May be + * NULL. + * @param user Arbitrary pointer passed through to callback. + */ +void uiInitialNoCardOpen(uiinitialnocardcallback_t callback, void *user); + +/** + * Closes the modal. No-op when already closed. + */ +void uiInitialNoCardClose(void); + +/** + * Disposes of the modal. + * + * @return Any error that occurs. + */ +errorret_t uiInitialNoCardDispose(void); diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index ce39a599..e85767e5 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -21,6 +21,8 @@ #include "ui/frame/battle/uibattlemenu.h" #include "ui/frame/backpack/uibackpack.h" #include "ui/frame/uiconfirm.h" +#include "ui/frame/initial/uiinitialnocard.h" +#include "ui/frame/initial/uiinitialcreatesave.h" #include "ui/rpg/textbox/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxminilist.h" #include "ui/rpg/uiemoji.h" @@ -79,6 +81,18 @@ uielement_t UI_ELEMENTS[] = { .dispose = uiConfirmDispose }, + { + .init = uiInitialNoCardInit, + .draw = uiInitialNoCardDraw, + .dispose = uiInitialNoCardDispose + }, + + { + .init = uiInitialCreateSaveInit, + .draw = uiInitialCreateSaveDraw, + .dispose = uiInitialCreateSaveDispose + }, + { .init = uiTextboxMainInit, .update = uiTextboxMainUpdate,