Clamp keyframe interpolation to last value, add main menu scene/UI, battle HUD, and expanded test coverage

- keyframeGetValue now returns the last keyframe's value for times at or
  beyond it, fixes a missing util/math.h include, and asserts keyframes are
  sorted by time; adds test/animation/test_keyframe.c
- Adds mainmenu scene/UI and a battle HUD UI frame
- Adds save autosave-related fields and battle scene tweaks
- Adds headless test coverage for cutscenes, entities, and map areas
This commit is contained in:
2026-08-06 12:58:07 -05:00
parent fb48285143
commit 1bd73d69fe
42 changed files with 2318 additions and 9 deletions
+1
View File
@@ -7,4 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
easing.c
animation.c
keyframe.c
)
+54
View File
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "keyframe.h"
#include "assert/assert.h"
#include "util/math.h"
float_t keyframeGetValue(
const keyframe_t *keyframes,
const uint32_t keyframeCount,
const float_t time
) {
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
assertTrue(time >= 0, "Time must be non-negative.");
#ifdef DUSK_ASSERTIONS
// Checks that the keyframes are sorted by time.
for(uint32_t i = 1; i < keyframeCount; i++) {
assertTrue(
keyframes[i].time >= keyframes[i - 1].time,
"Keyframes must be sorted by time."
);
}
#endif
keyframe_t *start;
keyframe_t *end;
keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1);
if(time >= last->time) return last->value;
keyframe_t *current = (keyframe_t *)keyframes;
start = current;
do {
if(current->time > time) {
end = current;
break;
}
start = current;
current++;
if(current > last) {
end = start;
break;
}
} while(true);
float_t t = (time - start->time) / (end->time - start->time);
return mathLerp(start->value, end->value, easingApply(start->easing, t));
}
+15 -1
View File
@@ -10,4 +10,18 @@ typedef struct {
float_t time;
float_t value;
easingtype_t easing;
} keyframe_t;
} keyframe_t;
/**
* Gets the value of a keyframe at a given time.
*
* @param keyframes The keyframes to get the value from.
* @param keyframeCount The number of keyframes in the array.
* @param time The time at which to get the value, in seconds.
* @return The value of the keyframe at the given time.
*/
float_t keyframeGetValue(
const keyframe_t *keyframes,
const uint32_t keyframeCount,
const float_t time
);
+2
View File
@@ -22,6 +22,8 @@
#endif
#ifndef DUSK_ASSERTIONS_FAKED
#define DUSK_ASSERTIONS 1
/**
* Initializes the assert system. Must be the very first call in engine
* startup.
+15
View File
@@ -13,6 +13,16 @@
save_t SAVE;
const saveslot_t SAVE_DEFAULT = {
.header = {
SAVE_SLOT_HEADER[0], SAVE_SLOT_HEADER[1], SAVE_SLOT_HEADER[2]
},
.version = SAVE_SLOT_VERSION,
.exists = true,
.playerName = "Player"
// globalItemCollected/storyFlags are left at their zero default.
};
static void _saveEagerLoadComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
}
@@ -105,6 +115,11 @@ void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user) {
#endif
}
void saveLoadDefault(const uint8_t slot) {
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
SAVE.slots[slot] = SAVE_DEFAULT;
}
void saveWriteSlot(const uint8_t slot, savecallback_t onComplete, void *user) {
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
assertNotNull(onComplete, "onComplete cannot be NULL");
+9
View File
@@ -137,6 +137,15 @@ bool_t saveIsBusy(void);
*/
void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user);
/**
* Resets a save slot in memory to SAVE_DEFAULT, for starting a new game
* without reading anything from persistent storage. Synchronous - no
* disk I/O is involved, so there's no callback to wait on.
*
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1) to reset.
*/
void saveLoadDefault(const uint8_t slot);
/**
* Writes the save slot for a given index to persistent storage. Slow/
* async on some platforms (PSP's native save dialog spans multiple
+11
View File
@@ -87,3 +87,14 @@ typedef struct {
* @param user User data passed through from the original call.
*/
typedef void (*savecallback_t)(errorret_t result, void *user);
/**
* The blank template used to (re)initialize a save slot for a brand new
* game via saveLoadDefault(), rather than reading one from persistent
* storage. Deliberately generic at this layer (empty collected-item
* flags, no story flags set) - anything that needs CSV-defined story flag
* defaults layers that on top afterward (see storyFlagInitDefaults()),
* since saveslot.h stays a leaf header with no dependency on generated
* story content (see SAVE_STORY_FLAG_COUNT_MAX's doc comment above).
*/
extern const saveslot_t SAVE_DEFAULT;
+1
View File
@@ -11,5 +11,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Subdirs
add_subdirectory(initial)
add_subdirectory(mainmenu)
add_subdirectory(overworld)
add_subdirectory(battle)
+126 -1
View File
@@ -6,7 +6,14 @@
*/
#include "scenebattle.h"
#include "rpg/battle/battle.h"
#include "display/display.h"
#include "display/displaystate.h"
#include "display/shader/shader.h"
#include "display/shader/shaderunlit.h"
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
#include "display/color.h"
#include "scene/scene.h"
errorret_t sceneBattleInit(scenedata_t *sceneData) {
errorOk();
@@ -18,9 +25,127 @@ errorret_t sceneBattleUpdate(scenedata_t *sceneData) {
}
errorret_t sceneBattleRender(scenedata_t *sceneData) {
scenebattle_t *battle = &sceneData->battle;
sceneBattleCameraUpdateProjection(battle);
sceneBattleCameraUpdateEye(battle);
errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
));
errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, battle->projection
));
errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_VIEW, battle->eye
));
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
}));
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
errorChain(sceneBattleDrawFighter(i));
}
errorOk();
}
errorret_t sceneBattleDispose(scenedata_t *sceneData) {
errorOk();
}
void sceneBattleCameraUpdateProjection(scenebattle_t *battle) {
glm_perspective(
glm_rad(SCENE_BATTLE_CAMERA_FOV),
SCREEN.aspect,
SCENE_BATTLE_CAMERA_NEAR,
SCENE_BATTLE_CAMERA_FAR,
battle->projection
);
}
void sceneBattleCameraUpdateEye(scenebattle_t *battle) {
glm_lookat(
(vec3){ 0.0f, 4.0f, 7.0f },
(vec3){ 0.0f, 0.5f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
battle->eye
);
}
void sceneBattleWorldToScreen(
scenebattle_t *battle,
vec3 renderPos,
vec2 out
) {
mat4 viewProj;
glm_mat4_mul(battle->projection, battle->eye, viewProj);
vec4 viewport = {
0.0f, 0.0f, (float_t)SCREEN.width, (float_t)SCREEN.height
};
vec3 window;
glm_project(renderPos, viewProj, viewport, window);
out[0] = window[0];
out[1] = (float_t)SCREEN.height - window[1];
}
void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) {
const battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
// Ordinal (and count) of this fighter among its living-slot teammates,
// in slot order, so each team lays out as an evenly spaced row.
uint8_t ordinal = 0;
uint8_t teamCount = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) continue;
if(BATTLE.fighters[i].team != fighter->team) continue;
if(i < fighterIndex) ordinal++;
teamCount++;
}
const float_t x =
((float_t)ordinal - ((float_t)(teamCount - 1) * 0.5f)) *
SCENE_BATTLE_FIGHTER_SPACING;
const float_t z = fighter->team == BATTLE_FIGHTER_TEAM_ALLY
? SCENE_BATTLE_ALLY_Z : SCENE_BATTLE_ENEMY_Z;
out[0] = x;
out[1] = 0.0f;
out[2] = z;
}
errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex) {
const battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) errorOk();
vec3 position;
sceneBattleGetFighterPosition(fighterIndex, position);
spritebatchsprite_t sprite;
glm_vec3_copy(position, sprite.min);
glm_vec3_copy(position, sprite.max);
glm_vec3_add(sprite.max, (vec3){ 1.0f, 1.0f, 0.0f }, sprite.max);
glm_vec2_copy((vec2){ 0.0f, 0.0f }, sprite.uvMin);
glm_vec2_copy((vec2){ 1.0f, 1.0f }, sprite.uvMax);
color_t color;
if(fighter->status == BATTLE_FIGHTER_STATUS_DEAD) {
color = color4b(96, 96, 96, 255);
} else if(fighter->team == BATTLE_FIGHTER_TEAM_ALLY) {
color = COLOR_BLUE;
} else {
color = COLOR_RED;
}
shadermaterial_t material = {
.unlit = { .color = color, .texture = NULL }
};
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
errorChain(spriteBatchFlush());
errorOk();
}
+65 -1
View File
@@ -7,9 +7,24 @@
#pragma once
#include "scene/scenebase.h"
#include "rpg/battle/battle.h"
// Fixed mock camera, "to begin": looking down at the arena from above and
// in front. Not yet driven by anything (no shake, no per-fighter framing).
#define SCENE_BATTLE_CAMERA_FOV 45.0f
#define SCENE_BATTLE_CAMERA_NEAR 0.1f
#define SCENE_BATTLE_CAMERA_FAR 100.0f
// Fighter row layout, in render-space units. Allies stand closer to the
// camera (+Z), enemies further away (-Z); each team is spread evenly
// along X, centered on the origin.
#define SCENE_BATTLE_FIGHTER_SPACING 2.0f
#define SCENE_BATTLE_ALLY_Z 2.5f
#define SCENE_BATTLE_ENEMY_Z (-2.5f)
typedef struct {
mat4 eye;
mat4 projection;
} scenebattle_t;
/**
@@ -44,3 +59,52 @@ errorret_t sceneBattleRender(scenedata_t *sceneData);
* @return An error if the dispose failed, or errorOk() if it succeeded.
*/
errorret_t sceneBattleDispose(scenedata_t *sceneData);
/**
* Recomputes the battle camera's projection matrix from the current
* screen aspect ratio, storing it in battle->projection.
*
* @param battle The battle scene data to update.
*/
void sceneBattleCameraUpdateProjection(scenebattle_t *battle);
/**
* Recomputes the battle camera's eye/view matrix, storing it in
* battle->eye. Fixed for now -- doesn't yet track anything.
*
* @param battle The battle scene data to update.
*/
void sceneBattleCameraUpdateEye(scenebattle_t *battle);
/**
* Converts a render-space position to screen-space pixel coordinates,
* using the battle camera's current eye and projection matrices.
*
* @param battle The battle scene data holding the camera matrices.
* @param renderPos The render-space position to convert.
* @param out Output vec2 filled with the screen-space pixel position.
*/
void sceneBattleWorldToScreen(
scenebattle_t *battle,
vec3 renderPos,
vec2 out
);
/**
* Computes the render-space position of a fighter's feet, laying out
* each team as an evenly spaced row facing the camera.
*
* @param fighterIndex Index into BATTLE.fighters.
* @param out Output vec3 filled with the fighter's render-space position.
*/
void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out);
/**
* Draws a single fighter as a colored 1x1 quad, skipping empty slots.
* Color differentiates team (ally/enemy) and status (dead fighters are
* drawn dimmed).
*
* @param fighterIndex Index into BATTLE.fighters.
* @return An error if drawing failed, or errorOk() if it succeeded.
*/
errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex);
+4 -4
View File
@@ -17,12 +17,12 @@ static void sceneInitialCheckSave(void);
static void sceneInitialCreateSaveWriteComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
sceneSet(SCENE_TYPE_OVERWORLD);
sceneSet(SCENE_TYPE_MAIN_MENU);
}
static void sceneInitialCreateSaveResult(const bool_t create, void *user) {
if(!create) {
sceneSet(SCENE_TYPE_OVERWORLD);
sceneSet(SCENE_TYPE_MAIN_MENU);
return;
}
@@ -39,7 +39,7 @@ static void sceneInitialNoCardResult(const bool_t retry, void *user) {
// 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);
sceneSet(SCENE_TYPE_MAIN_MENU);
}
static void sceneInitialLoadComplete(errorret_t result, void *user) {
@@ -50,7 +50,7 @@ static void sceneInitialLoadComplete(errorret_t result, void *user) {
}
if(saveSlotExists(SAVE_ACTIVE_SLOT)) {
sceneSet(SCENE_TYPE_OVERWORLD);
sceneSet(SCENE_TYPE_MAIN_MENU);
return;
}
+1 -1
View File
@@ -18,7 +18,7 @@ typedef struct {
/**
* 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.
* show before proceeding to the main menu.
*
* @param sceneData The scene data used for this scene.
* @return An error if the init failed, or errorOk() if it succeeded.
+9
View File
@@ -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
scenemainmenu.c
)
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "scenemainmenu.h"
errorret_t sceneMainMenuInit(scenedata_t *sceneData) {
errorOk();
}
errorret_t sceneMainMenuUpdate(scenedata_t *sceneData) {
errorOk();
}
errorret_t sceneMainMenuRender(scenedata_t *sceneData) {
errorOk();
}
errorret_t sceneMainMenuDispose(scenedata_t *sceneData) {
errorOk();
}
+51
View File
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "scene/scenebase.h"
// Empty for now -- the menu itself lives in ui/frame/mainmenu/uimainmenu.c,
// driven by the global UI element pipeline (see uimainmenu.c's own
// SCENE.current check for when it shows itself). A byte placeholder keeps
// the struct non-empty for portability.
typedef struct {
uint8_t reserved;
} scenemainmenu_t;
/**
* Initializes the main menu scene.
*
* @param sceneData The scene data used for this scene.
* @return An error if the init failed, or errorOk() if it succeeded.
*/
errorret_t sceneMainMenuInit(scenedata_t *sceneData);
/**
* Updates the main menu scene. Currently a no-op -- the menu drives
* itself via the global UI element pipeline.
*
* @param sceneData The scene data used for this scene.
* @return An error if the update failed, or errorOk() if it succeeded.
*/
errorret_t sceneMainMenuUpdate(scenedata_t *sceneData);
/**
* Renders the main menu scene. Currently a no-op -- the menu draws
* itself 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 sceneMainMenuRender(scenedata_t *sceneData);
/**
* Disposes the main menu scene.
*
* @param sceneData The scene data used for this scene.
* @return An error if the dispose failed, or errorOk() if it succeeded.
*/
errorret_t sceneMainMenuDispose(scenedata_t *sceneData);
+7
View File
@@ -17,6 +17,13 @@ scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
.dispose = sceneInitialDispose
},
[SCENE_TYPE_MAIN_MENU] = {
.init = sceneMainMenuInit,
.update = sceneMainMenuUpdate,
.render = sceneMainMenuRender,
.dispose = sceneMainMenuDispose
},
[SCENE_TYPE_OVERWORLD] = {
.init = sceneOverworldInit,
.update = sceneOverworldUpdate,
+3
View File
@@ -8,11 +8,13 @@
#pragma once
#include "scene/scenebase.h"
#include "scene/initial/sceneinitial.h"
#include "scene/mainmenu/scenemainmenu.h"
#include "scene/overworld/sceneoverworld.h"
#include "scene/battle/scenebattle.h"
typedef union scenedata_u {
sceneinitial_t initial;
scenemainmenu_t mainMenu;
sceneoverworld_t overworld;
scenebattle_t battle;
} scenedata_t;
@@ -30,6 +32,7 @@ typedef enum {
SCENE_TYPE_NULL,
SCENE_TYPE_INITIAL,
SCENE_TYPE_MAIN_MENU,
SCENE_TYPE_OVERWORLD,
SCENE_TYPE_BATTLE,
+1
View File
@@ -10,6 +10,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
)
add_subdirectory(game)
add_subdirectory(mainmenu)
add_subdirectory(settings)
add_subdirectory(battle)
add_subdirectory(backpack)
+1
View File
@@ -6,4 +6,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uibattlemenu.c
uibattlehud.c
)
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uibattlehud.h"
#include "rpg/battle/battle.h"
#include "scene/battle/scenebattle.h"
#include "scene/scene.h"
#include "display/text/text.h"
#include "display/color.h"
#include "display/spritebatch/spritebatch.h"
#include "util/string.h"
#define UI_BATTLE_HUD_TEXT_MAX 32
#define UI_BATTLE_HUD_LINE_HEIGHT 12.0f
errorret_t uiBattleHudDraw(void) {
if(BATTLE.state == BATTLE_STATE_NONE) errorOk();
scenebattle_t *battle = &SCENE.data.battle;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
const battlefighter_t *fighter = &BATTLE.fighters[i];
if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) continue;
vec3 position;
sceneBattleGetFighterPosition(i, position);
glm_vec3_add(position, (vec3){ 0.5f, 1.3f, 0.0f }, position);
vec2 screenPos;
sceneBattleWorldToScreen(battle, position, screenPos);
char_t hpText[UI_BATTLE_HUD_TEXT_MAX];
stringFormat(
hpText, UI_BATTLE_HUD_TEXT_MAX - 1, "HP %u/%u",
fighter->health, fighter->healthMax
);
errorChain(textDraw(screenPos[0], screenPos[1], hpText, COLOR_WHITE, NULL));
char_t mpText[UI_BATTLE_HUD_TEXT_MAX];
stringFormat(
mpText, UI_BATTLE_HUD_TEXT_MAX - 1, "MP %u/%u",
fighter->mp, fighter->mpMax
);
errorChain(textDraw(
screenPos[0], screenPos[1] + UI_BATTLE_HUD_LINE_HEIGHT,
mpText, COLOR_CYAN, NULL
));
}
errorChain(spriteBatchFlush());
errorOk();
}
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
/**
* Draws each living battle fighter's HP/MP as text above its position in
* the battle scene. No-op if no battle is active.
*
* @return An error if drawing failed, or errorOk() if it succeeded.
*/
errorret_t uiBattleHudDraw(void);
@@ -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
uimainmenu.c
)
+142
View File
@@ -0,0 +1,142 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uimainmenu.h"
#include "ui/frame/uiframe.h"
#include "scene/scene.h"
#include "engine/engine.h"
#include "rpg/battle/battle.h"
#include "save/save.h"
#include "util/memory.h"
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
#include "assert/assert.h"
#define UI_MAIN_MENU_INDEX_NEW_GAME 0
#define UI_MAIN_MENU_INDEX_LOAD_GAME 1
#define UI_MAIN_MENU_INDEX_OPTIONS 2
#define UI_MAIN_MENU_INDEX_QUIT 3
uimainmenu_t UI_MAIN_MENU;
// TEMPORARY test hook: New Game starts a hardcoded mock battle instead of
// the overworld, so the battle scene (camera/fighters/HUD) can be seen and
// played without a real encounter trigger yet.
void uiMainMenuStartTestBattle(void) {
battleInit();
const battlefighterstats_t allyOneStats =
{ .attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0 };
const battlefighterstats_t allyTwoStats =
{ .attack = 8, .defense = 4, .magic = 0, .speed = 8, .luck = 0 };
const battlefighterstats_t enemyOneStats =
{ .attack = 6, .defense = 3, .magic = 0, .speed = 6, .luck = 0 };
const battlefighterstats_t enemyTwoStats =
{ .attack = 7, .defense = 3, .magic = 0, .speed = 5, .luck = 0 };
battleAddFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
allyOneStats, 30, 10
);
battleAddFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
allyTwoStats, 25, 10
);
battleAddFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
enemyOneStats, 20, 5
);
battleAddFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
enemyTwoStats, 20, 5
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
sceneSet(SCENE_TYPE_BATTLE);
}
void uiMainMenuSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
uiMenuClose(&UI_MAIN_MENU.menu);
switch(index) {
case UI_MAIN_MENU_INDEX_NEW_GAME:
// Resets the active slot to SAVE_DEFAULT in memory -- no disk I/O,
// unlike Load Game (which will read a real slot via saveLoadSlot()).
saveLoadDefault(SAVE_ACTIVE_SLOT);
uiMainMenuStartTestBattle();
break;
case UI_MAIN_MENU_INDEX_LOAD_GAME:
// TODO: load game.
break;
case UI_MAIN_MENU_INDEX_OPTIONS:
// TODO: options.
break;
case UI_MAIN_MENU_INDEX_QUIT:
ENGINE.running = false;
break;
default:
break;
}
}
errorret_t uiMainMenuInit(void) {
memoryZero(&UI_MAIN_MENU, sizeof(uimainmenu_t));
MENU_BEGIN(
&UI_MAIN_MENU.menu, UI_MAIN_MENU.items, uiMainMenuSelected, NULL, NULL
);
MENU_BUTTON("New Game");
MENU_BUTTON("Load Game");
MENU_BUTTON("Options");
MENU_BUTTON("Quit Game");
MENU_END(UI_MAIN_MENU.items, 1);
errorOk();
}
errorret_t uiMainMenuUpdate(void) {
if(SCENE.current != SCENE_TYPE_MAIN_MENU) errorOk();
if(uiMenuIsActive(&UI_MAIN_MENU.menu)) errorOk();
uiMenuOpen(&UI_MAIN_MENU.menu);
errorOk();
}
errorret_t uiMainMenuDraw(void) {
if(!uiMenuIsActive(&UI_MAIN_MENU.menu)) errorOk();
const float_t width = UI_MAIN_MENU_WIDTH;
const float_t height = UI_MAIN_MENU_HEIGHT;
const float_t x =
(float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
const float_t y =
(float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
errorChain(uiMenuDraw(
&UI_MAIN_MENU.menu,
x + UI_FRAME_START_X,
y + UI_FRAME_START_Y,
width - (UI_FRAME_START_X * 2),
height - (UI_FRAME_START_Y * 2)
));
errorChain(spriteBatchFlush());
errorOk();
}
errorret_t uiMainMenuDispose(void) {
errorOk();
}
+50
View File
@@ -0,0 +1,50 @@
/**
* 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_MAIN_MENU_ITEM_COUNT 4
#define UI_MAIN_MENU_WIDTH 200.0f
#define UI_MAIN_MENU_HEIGHT 160.0f
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_MAIN_MENU_ITEM_COUNT];
} uimainmenu_t;
extern uimainmenu_t UI_MAIN_MENU;
/**
* Initializes the main menu panel.
*
* @return Any error that occurs.
*/
errorret_t uiMainMenuInit(void);
/**
* Updates the main menu panel: opens it whenever SCENE_TYPE_MAIN_MENU
* becomes active and it isn't already open.
*
* @return Any error that occurs.
*/
errorret_t uiMainMenuUpdate(void);
/**
* Draws the main menu panel, centered on screen. No-op when not active.
*
* @return Any error that occurs.
*/
errorret_t uiMainMenuDraw(void);
/**
* Disposes of the main menu panel.
*
* @return Any error that occurs.
*/
errorret_t uiMainMenuDispose(void);
+11
View File
@@ -19,7 +19,9 @@
#include "ui/debug/uiconsole.h"
#include "ui/frame/settings/uisettings.h"
#include "ui/frame/game/uigamemenu.h"
#include "ui/frame/mainmenu/uimainmenu.h"
#include "ui/frame/battle/uibattlemenu.h"
#include "ui/frame/battle/uibattlehud.h"
#include "ui/frame/backpack/uibackpack.h"
#include "ui/frame/uiconfirm.h"
#include "ui/frame/initial/uiinitialnocard.h"
@@ -56,6 +58,15 @@ uielement_t UI_ELEMENTS[] = {
.dispose = uiGameMenuDispose
},
{
.init = uiMainMenuInit,
.update = uiMainMenuUpdate,
.draw = uiMainMenuDraw,
.dispose = uiMainMenuDispose
},
{ .draw = uiBattleHudDraw },
{
.init = uiBattleMenuInit,
.update = uiBattleMenuUpdate,