From b693ea41029f04fd6d224f2613732cdc6a674190 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Wed, 8 Jul 2026 10:05:21 -0500 Subject: [PATCH] Starting item and battle stuff --- assets/locale/en_US.po | 23 +- assets/locale/es_MX.po | 24 +++ assets/locale/jp_JP.po | 24 +++ src/dusk/rpg/battle/battle.c | 176 ++++++++++++++- src/dusk/rpg/battle/battle.h | 139 +++++++++++- src/dusk/rpg/cutscene/item/CMakeLists.txt | 1 + .../rpg/cutscene/item/battle/CMakeLists.txt | 9 + .../item/battle/cutscenestartbattle.c | 71 +++++++ .../item/battle/cutscenestartbattle.h | 53 +++++ src/dusk/rpg/cutscene/item/cutsceneitem.c | 7 + src/dusk/rpg/cutscene/item/cutsceneitem.h | 5 +- src/dusk/rpg/item/CMakeLists.txt | 3 +- src/dusk/rpg/item/item.c | 30 +++ src/dusk/rpg/item/item.csv | 8 +- src/dusk/rpg/item/item.h | 24 +++ src/dusk/rpg/item/itemgive.c | 8 +- src/dusk/rpg/rpg.c | 5 + src/dusk/scene/CMakeLists.txt | 3 +- src/dusk/scene/battle/CMakeLists.txt | 9 + src/dusk/scene/battle/scenebattle.c | 26 +++ src/dusk/scene/battle/scenebattle.h | 46 ++++ src/dusk/scene/scenetype.c | 7 + src/dusk/scene/scenetype.h | 3 + src/dusk/ui/frame/CMakeLists.txt | 2 + src/dusk/ui/frame/backpack/CMakeLists.txt | 9 + src/dusk/ui/frame/backpack/uibackpack.c | 116 ++++++++++ src/dusk/ui/frame/backpack/uibackpack.h | 65 ++++++ src/dusk/ui/frame/battle/CMakeLists.txt | 9 + src/dusk/ui/frame/battle/uibattlemenu.c | 137 ++++++++++++ src/dusk/ui/frame/battle/uibattlemenu.h | 57 +++++ src/dusk/ui/frame/game/uigamemenu.c | 34 ++- src/dusk/ui/frame/game/uigamemenu.h | 6 +- src/dusk/ui/uielement.c | 14 ++ src/dusk/ui/widget/CMakeLists.txt | 3 + src/dusk/ui/widget/uiitem.c | 77 +++++++ src/dusk/ui/widget/uiitem.h | 89 ++++++++ src/dusk/ui/widget/uiitemlist.c | 179 ++++++++++++++++ src/dusk/ui/widget/uiitemlist.h | 200 ++++++++++++++++++ src/dusk/ui/widget/uiscrolling.c | 15 ++ src/dusk/ui/widget/uiscrolling.h | 21 ++ tools/item.py | 10 +- 41 files changed, 1727 insertions(+), 20 deletions(-) create mode 100644 src/dusk/rpg/cutscene/item/battle/CMakeLists.txt create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c create mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h create mode 100644 src/dusk/rpg/item/item.c create mode 100644 src/dusk/rpg/item/item.h create mode 100644 src/dusk/scene/battle/CMakeLists.txt create mode 100644 src/dusk/scene/battle/scenebattle.c create mode 100644 src/dusk/scene/battle/scenebattle.h create mode 100644 src/dusk/ui/frame/backpack/CMakeLists.txt create mode 100644 src/dusk/ui/frame/backpack/uibackpack.c create mode 100644 src/dusk/ui/frame/backpack/uibackpack.h create mode 100644 src/dusk/ui/frame/battle/CMakeLists.txt create mode 100644 src/dusk/ui/frame/battle/uibattlemenu.c create mode 100644 src/dusk/ui/frame/battle/uibattlemenu.h create mode 100644 src/dusk/ui/widget/uiitem.c create mode 100644 src/dusk/ui/widget/uiitem.h create mode 100644 src/dusk/ui/widget/uiitemlist.c create mode 100644 src/dusk/ui/widget/uiitemlist.h create mode 100644 src/dusk/ui/widget/uiscrolling.c create mode 100644 src/dusk/ui/widget/uiscrolling.h diff --git a/assets/locale/en_US.po b/assets/locale/en_US.po index 14643432..8452fa19 100644 --- a/assets/locale/en_US.po +++ b/assets/locale/en_US.po @@ -42,4 +42,25 @@ msgstr "Apply" #: src/dusk/ui/frame/uiconfirm.c msgid "ui.confirm.discard_changes" -msgstr "Discard unsaved changes?" \ No newline at end of file +msgstr "Discard unsaved changes?" + +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.characters" +msgstr "Characters" + +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.items" +msgstr "Items" + +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.settings" +msgstr "Settings" + +msgid "item.potion.name" +msgstr "Potion" + +msgid "item.potato.name" +msgstr "Potato" + +msgid "item.apple.name" +msgstr "Apple" \ No newline at end of file diff --git a/assets/locale/es_MX.po b/assets/locale/es_MX.po index f01e1e41..3e43adef 100644 --- a/assets/locale/es_MX.po +++ b/assets/locale/es_MX.po @@ -44,3 +44,27 @@ msgstr "Aplicar" #: src/dusk/ui/frame/uiconfirm.c msgid "ui.confirm.discard_changes" msgstr "¿Descartar los cambios no guardados?" + +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.characters" +msgstr "Personajes" + +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.items" +msgstr "Objetos" + +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.settings" +msgstr "Configuración" + +#: src/dusk/rpg/item/item.csv +msgid "item.potion.name" +msgstr "Poción" + +#: src/dusk/rpg/item/item.csv +msgid "item.potato.name" +msgstr "Papa" + +#: src/dusk/rpg/item/item.csv +msgid "item.apple.name" +msgstr "Manzana" diff --git a/assets/locale/jp_JP.po b/assets/locale/jp_JP.po index 04e1bfb2..ed503e2f 100644 --- a/assets/locale/jp_JP.po +++ b/assets/locale/jp_JP.po @@ -44,3 +44,27 @@ msgstr "適用" #: src/dusk/ui/frame/uiconfirm.c msgid "ui.confirm.discard_changes" msgstr "未保存の変更を破棄しますか?" + +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.characters" +msgstr "キャラクター" + +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.items" +msgstr "アイテム" + +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.settings" +msgstr "設定" + +#: src/dusk/rpg/item/item.csv +msgid "item.potion.name" +msgstr "ポーション" + +#: src/dusk/rpg/item/item.csv +msgid "item.potato.name" +msgstr "ジャガイモ" + +#: src/dusk/rpg/item/item.csv +msgid "item.apple.name" +msgstr "リンゴ" diff --git a/src/dusk/rpg/battle/battle.c b/src/dusk/rpg/battle/battle.c index 813cbc19..cdba1b95 100644 --- a/src/dusk/rpg/battle/battle.c +++ b/src/dusk/rpg/battle/battle.c @@ -6,6 +6,7 @@ */ #include "battle.h" +#include "assert/assert.h" #include "util/memory.h" battle_t BATTLE; @@ -40,10 +41,183 @@ battlefighter_t *battleAddFighter( return fighter; } -void battleStart(void) { +void battleStart( + const battleencountertype_t encounterType, + const bool_t fleeAvailable +) { + assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type"); + + BATTLE.encounterType = encounterType; + BATTLE.fleeAvailable = fleeAvailable; + BATTLE.result = BATTLE_RESULT_NONE; + BATTLE.round = 1; + BATTLE.turnIndex = 0; + battleBuildTurnOrder(true); + BATTLE.active = true; } void battleDispose(void) { battleInit(); } + +battlefighter_t *battleGetCurrentFighter(void) { + if(!BATTLE.active) return NULL; + if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL; + return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]]; +} + +uint8_t battleGetAliveCount(const battlefighterteam_t team) { + uint8_t count = 0; + for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) { + if(BATTLE.fighters[i].team != team) continue; + if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue; + count++; + } + return count; +} + +void battleResolveAttack( + battlefighter_t *attacker, + battlefighter_t *defender +) { + assertNotNull(attacker, "Attacker cannot be NULL"); + assertNotNull(defender, "Defender cannot be NULL"); + + const int32_t rawDamage = + (int32_t)attacker->stats.attack - (int32_t)defender->stats.defense; + const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1; + + defender->health = damage >= defender->health ? 0 : defender->health - damage; + if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD; +} + +void battleNextTurn(void) { + BATTLE.turnIndex++; + if(BATTLE.turnIndex < BATTLE.turnCount) return; + + BATTLE.round++; + BATTLE.turnIndex = 0; + battleBuildTurnOrder(false); +} + +battleresult_t battleCheckResult(void) { + if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result; + + if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) { + BATTLE.result = BATTLE_RESULT_LOSS; + } else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) { + BATTLE.result = BATTLE_RESULT_WIN; + } + + return BATTLE.result; +} + +void battlePlayerAttack(const uint8_t targetIndex) { + battlefighter_t *attacker = battleGetCurrentFighter(); + if(attacker == NULL) return; + if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return; + if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return; + + battlefighter_t *defender = &BATTLE.fighters[targetIndex]; + if(!battleFighterIsAlive(defender)) return; + + battleResolveAttack(attacker, defender); + battleCheckResult(); + if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn(); +} + +void battlePlayerFlee(void) { + battlefighter_t *fighter = battleGetCurrentFighter(); + if(fighter == NULL) return; + if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return; + if(!BATTLE.fleeAvailable) return; + + BATTLE.result = BATTLE_RESULT_FLED; +} + +void battleUpdate(void) { + if(!BATTLE.active) return; + if(BATTLE.result != BATTLE_RESULT_NONE) return; + + battlefighter_t *current = battleGetCurrentFighter(); + if(current == NULL) return; + + if(!battleFighterIsAlive(current)) { + battleNextTurn(); + return; + } + + if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return; + + battlefighter_t *target = battleAIChooseTarget(current); + if(target != NULL) battleResolveAttack(current, target); + + battleCheckResult(); + if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn(); +} + +void battleBuildTurnOrder(const bool_t applyEncounterBias) { + BATTLE.turnCount = 0; + for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) { + if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue; + BATTLE.turnOrder[BATTLE.turnCount++] = i; + } + + // Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX. + for(uint8_t i = 1; i < BATTLE.turnCount; i++) { + const uint8_t key = BATTLE.turnOrder[i]; + const uint16_t keySpeed = BATTLE.fighters[key].stats.speed; + + int8_t j = (int8_t)i - 1; + while( + j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed + ) { + BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j]; + j--; + } + BATTLE.turnOrder[j + 1] = key; + } + + if(!applyEncounterBias) return; + + if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) { + battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY); + } else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) { + battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY); + } +} + +void battleMoveTeamFirst(const battlefighterteam_t team) { + uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX]; + uint8_t count = 0; + + for(uint8_t i = 0; i < BATTLE.turnCount; i++) { + if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue; + sorted[count++] = BATTLE.turnOrder[i]; + } + for(uint8_t i = 0; i < BATTLE.turnCount; i++) { + if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue; + sorted[count++] = BATTLE.turnOrder[i]; + } + + memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount); +} + +battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) { + const battlefighterteam_t enemyTeam = + fighter->team == BATTLE_FIGHTER_TEAM_ALLY ? + BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY; + + battlefighter_t *weakest = NULL; + for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) { + battlefighter_t *candidate = &BATTLE.fighters[i]; + if(candidate->team != enemyTeam) continue; + if(!battleFighterIsAlive(candidate)) continue; + if(weakest == NULL || candidate->health < weakest->health) { + weakest = candidate; + } + } + + return weakest; +} diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h index 7d52c173..786a874e 100644 --- a/src/dusk/rpg/battle/battle.h +++ b/src/dusk/rpg/battle/battle.h @@ -10,9 +10,36 @@ #define BATTLE_FIGHTER_COUNT_MAX 8 +typedef enum { + BATTLE_ENCOUNTER_REGULAR, + BATTLE_ENCOUNTER_PLAYER_ADVANTAGE, + BATTLE_ENCOUNTER_BACK_ATTACK, + + BATTLE_ENCOUNTER_COUNT +} battleencountertype_t; + +typedef enum { + BATTLE_RESULT_NONE, + BATTLE_RESULT_WIN, + BATTLE_RESULT_LOSS, + BATTLE_RESULT_FLED, + + BATTLE_RESULT_COUNT +} battleresult_t; + typedef struct { bool_t active; battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX]; + + battleencountertype_t encounterType; + bool_t fleeAvailable; + battleresult_t result; + + // Fighter indices (into fighters[]), sorted for the current round. + uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX]; + uint8_t turnCount; + uint8_t turnIndex; + uint16_t round; } battle_t; extern battle_t BATTLE; @@ -50,12 +77,118 @@ battlefighter_t *battleAddFighter( ); /** - * Starts a battle, marking it active. Any fighters already added via - * battleAddFighter remain in place. + * Starts the battle: builds the opening turn order (biased by + * encounterType for the first round only) and marks the battle active. + * Call once every fighter has been added via battleAddFighter. + * + * @param encounterType Determines the opening round's turn order. + * @param fleeAvailable Whether the party may attempt to flee this battle. */ -void battleStart(void); +void battleStart( + const battleencountertype_t encounterType, + const bool_t fleeAvailable +); /** * Disposes of the battle, clearing all fighters and marking it inactive. */ void battleDispose(void); + +/** + * Returns the fighter whose turn it currently is. + * + * @return Pointer to the active fighter, or NULL if the battle isn't + * active or has no living fighters left to act. + */ +battlefighter_t *battleGetCurrentFighter(void); + +/** + * Returns the number of living fighters on a team. + * + * @param team The team to count. + * @return Count of living fighters on that team. + */ +uint8_t battleGetAliveCount(const battlefighterteam_t team); + +/** + * Resolves a physical attack from attacker onto defender: damage is the + * attacker's attack stat minus the defender's defense stat (minimum 1), + * subtracted from the defender's health. The defender is marked dead + * once health reaches 0. + * + * @param attacker The attacking fighter. + * @param defender The defending fighter. + */ +void battleResolveAttack( + battlefighter_t *attacker, + battlefighter_t *defender +); + +/** + * Ends the current fighter's turn and advances to the next fighter in + * the turn order, starting a new round (rebuilding turn order purely by + * speed) once every fighter in the current round has acted. + */ +void battleNextTurn(void); + +/** + * Checks whether the battle has been won or lost, updating and + * returning BATTLE.result. Does nothing if a result has already been + * set (e.g. by a successful flee). + * + * @return The battle's current result. + */ +battleresult_t battleCheckResult(void); + +/** + * Submits the current fighter's attack against a target, if it is + * currently a player-controlled fighter's turn. Resolves the attack, + * checks for a battle result, and advances the turn. + * + * @param targetIndex Index into BATTLE.fighters of the target. + */ +void battlePlayerAttack(const uint8_t targetIndex); + +/** + * Submits a flee attempt for the current fighter's turn, if it is + * currently a player-controlled fighter's turn and fleeing is + * available for this battle. Always succeeds, ending the battle with + * BATTLE_RESULT_FLED. + */ +void battlePlayerFlee(void); + +/** + * Updates the battle simulation for one frame: resolves the current + * fighter's turn automatically if AI-controlled, otherwise waits for a + * player action via battlePlayerAttack/battlePlayerFlee. No-op if the + * battle isn't active or already has a result. + */ +void battleUpdate(void); + +/** + * Rebuilds BATTLE.turnOrder/turnCount from every currently living + * fighter, sorted by speed descending. + * + * @param applyEncounterBias If true, reorders the freshly speed-sorted + * queue so BATTLE.encounterType's favoured team goes first (used only + * for the opening round). + */ +void battleBuildTurnOrder(const bool_t applyEncounterBias); + +/** + * Stably partitions BATTLE.turnOrder so every fighter on the given team + * comes first, preserving each side's relative (speed-sorted) order. + * + * @param team The team to move to the front of the turn order. + */ +void battleMoveTeamFirst(const battlefighterteam_t team); + +/** + * Picks an AI target for fighter: the lowest-health living fighter on + * the opposing team. + * + * @param fighter The AI-controlled fighter choosing a target. + * @return The chosen target, or NULL if the opposing team has no + * living fighters. + */ +battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter); diff --git a/src/dusk/rpg/cutscene/item/CMakeLists.txt b/src/dusk/rpg/cutscene/item/CMakeLists.txt index a3ce80fe..5a0f884f 100755 --- a/src/dusk/rpg/cutscene/item/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/CMakeLists.txt @@ -14,3 +14,4 @@ add_subdirectory(entity) add_subdirectory(item) add_subdirectory(maparea) add_subdirectory(ui) +add_subdirectory(battle) diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt new file mode 100644 index 00000000..8c48be10 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/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 + cutscenestartbattle.c +) diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c b/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c new file mode 100644 index 00000000..8d8aab55 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.c @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "rpg/battle/party.h" +#include "scene/scene.h" + +void cutsceneStartBattleStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + const cutscenestartbattle_t *config = &item->startBattle; + + battleInit(); + + for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) { + battlefighter_t *member = partyGetOrderMember(i); + if(member == NULL) continue; + + battlefighter_t *fighter = battleAddFighter( + BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, + member->stats, member->healthMax, member->mpMax + ); + if(fighter == NULL) continue; + + fighter->health = member->health; + fighter->mp = member->mp; + fighter->status = member->status; + } + + for(uint8_t i = 0; i < config->enemyCount; i++) { + const cutscenestartbattleenemy_t *enemy = &config->enemies[i]; + battleAddFighter( + BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, + enemy->stats, enemy->healthMax, enemy->mpMax + ); + } + + battleStart(config->encounterType, config->fleeAvailable); + sceneSet(SCENE_TYPE_BATTLE); +} + +bool_t cutsceneStartBattleUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + if(BATTLE.result == BATTLE_RESULT_NONE) return false; + + // Sync ally HP/MP back to the persistent party roster. Relies on + // ally fighters having been added to BATTLE.fighters in the same + // order partyGetOrderMember() iterates, starting at index 0 (see + // cutsceneStartBattleStart). + uint8_t allySlot = 0; + for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) { + battlefighter_t *member = partyGetOrderMember(i); + if(member == NULL) continue; + + battlefighter_t *fighter = &BATTLE.fighters[allySlot++]; + member->health = fighter->health; + member->mp = fighter->mp; + member->status = fighter->status; + } + + sceneSet(SCENE_TYPE_OVERWORLD); + battleDispose(); + return true; +} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h b/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h new file mode 100644 index 00000000..770d1690 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/battle/cutscenestartbattle.h @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "rpg/battle/battle.h" + +#define CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX 4 + +typedef struct { + battlefighterstats_t stats; + uint16_t healthMax; + uint16_t mpMax; +} cutscenestartbattleenemy_t; + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + battleencountertype_t encounterType; + bool_t fleeAvailable; + uint8_t enemyCount; + cutscenestartbattleenemy_t enemies[CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX]; +} cutscenestartbattle_t; + +/** + * Starts a battle: seeds BATTLE with the party's active order members + * and the item's configured enemies, then switches to the battle + * scene. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneStartBattleStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Waits for the battle to produce a result, syncs ally HP/MP back to + * the party roster, then returns to the overworld scene. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true once the battle has ended. + */ +bool_t cutsceneStartBattleUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index 190fae80..a733f8c8 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -77,6 +77,10 @@ void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) { cutsceneMapAreaWaitStart(item, data); break; + case CUTSCENE_ITEM_TYPE_START_BATTLE: + cutsceneStartBattleStart(item, data); + break; + default: break; } @@ -132,6 +136,9 @@ bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT: return cutsceneMapAreaWaitUpdate(item, data); + case CUTSCENE_ITEM_TYPE_START_BATTLE: + return cutsceneStartBattleUpdate(item, data); + default: return false; } diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index c211c7f8..62914438 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -22,6 +22,7 @@ #include "maparea/cutscenemapareaadd.h" #include "maparea/cutscenemaparearemove.h" #include "maparea/cutscenemapareawait.h" +#include "battle/cutscenestartbattle.h" typedef struct cutscene_s cutscene_t; @@ -43,7 +44,8 @@ typedef enum { CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, - CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT + CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, + CUTSCENE_ITEM_TYPE_START_BATTLE } cutsceneitemtype_t; struct cutsceneitem_s { @@ -67,6 +69,7 @@ struct cutsceneitem_s { cutscenemapareaadd_t mapAreaAdd; cutscenemaparearemove_t mapAreaRemove; cutscenemapareawait_t mapAreaWait; + cutscenestartbattle_t startBattle; }; }; diff --git a/src/dusk/rpg/item/CMakeLists.txt b/src/dusk/rpg/item/CMakeLists.txt index a5b03b66..eacbfffe 100644 --- a/src/dusk/rpg/item/CMakeLists.txt +++ b/src/dusk/rpg/item/CMakeLists.txt @@ -6,6 +6,7 @@ # Sources target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + item.c inventory.c backpack.c itemgive.c @@ -16,6 +17,6 @@ dusk_run_python( dusk_item_csv_defs tools.item --csv ${CMAKE_CURRENT_SOURCE_DIR}/item.csv - --output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/item.h + --output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/itemdef.h ) add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_csv_defs) \ No newline at end of file diff --git a/src/dusk/rpg/item/item.c b/src/dusk/rpg/item/item.c new file mode 100644 index 00000000..d80a6b67 --- /dev/null +++ b/src/dusk/rpg/item/item.c @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "item.h" +#include "assert/assert.h" +#include "locale/localemanager.h" +#include "asset/loader/locale/assetlocaleloader.h" + +errorret_t itemGetName( + const itemid_t item, + char_t *buffer, + const size_t bufferSize +) { + assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); + assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); + + errorChain(assetLocaleGetString( + &LOCALE.entry->data.locale, + ITEMS[item].name, + 0, + buffer, + bufferSize + )); + + errorOk(); +} diff --git a/src/dusk/rpg/item/item.csv b/src/dusk/rpg/item/item.csv index 5376a99d..56f5453d 100644 --- a/src/dusk/rpg/item/item.csv +++ b/src/dusk/rpg/item/item.csv @@ -1,4 +1,4 @@ -id,type,weight -POTION,MEDICINE,1.0 -POTATO,FOOD,0.5 -APPLE,FOOD,0.3 \ No newline at end of file +id,type,weight,name +POTION,MEDICINE,1.0,potion +POTATO,FOOD,0.5,potato +APPLE,FOOD,0.3,apple \ No newline at end of file diff --git a/src/dusk/rpg/item/item.h b/src/dusk/rpg/item/item.h new file mode 100644 index 00000000..1c1beda5 --- /dev/null +++ b/src/dusk/rpg/item/item.h @@ -0,0 +1,24 @@ +/** + * 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 "rpg/item/itemdef.h" + +/** + * Gets the localized display name for an item. + * + * @param item The item ID to look up. Must not be ITEM_ID_NULL. + * @param buffer Buffer to write the localized name into. + * @param bufferSize Size of the buffer. + * @return Any error that occurs. + */ +errorret_t itemGetName( + const itemid_t item, + char_t *buffer, + const size_t bufferSize +); diff --git a/src/dusk/rpg/item/itemgive.c b/src/dusk/rpg/item/itemgive.c index ac8eec1a..8872fff7 100644 --- a/src/dusk/rpg/item/itemgive.c +++ b/src/dusk/rpg/item/itemgive.c @@ -9,16 +9,22 @@ #include "rpg/item/backpack.h" #include "ui/rpg/uitextboxmain.h" #include "util/string.h" +#include "error/error.h" + +#define ITEM_GIVE_NAME_MAX_CHARS 32 void itemGive(const itemid_t item, const uint8_t quantity) { backpackAdd(item, quantity); + char_t name[ITEM_GIVE_NAME_MAX_CHARS]; + errorCatch(itemGetName(item, name, ITEM_GIVE_NAME_MAX_CHARS)); + char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS]; stringFormat( msg, ITEM_GIVE_MESSAGE_MAX_CHARS - 1, "Received %s x%u", - ITEMS[item].name, + name, (uint32_t)quantity ); uiTextboxMainSetText(msg); diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index b6b8f99d..26d7c6a1 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -59,6 +59,11 @@ errorret_t rpgInit(void) { entityItemSet(itemEnt, ITEM_ID_POTION, 1); entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 }); + // TEST: Give the player a starting assortment of items. + backpackAdd(ITEM_ID_POTION, 5); + backpackAdd(ITEM_ID_POTATO, 3); + backpackAdd(ITEM_ID_APPLE, 8); + // TEST: Create a test map area. uint8_t areaIndex = mapAreaAdd( (worldpos_t){ 11, 3, 0 }, diff --git a/src/dusk/scene/CMakeLists.txt b/src/dusk/scene/CMakeLists.txt index 9444af7b..b8480d21 100644 --- a/src/dusk/scene/CMakeLists.txt +++ b/src/dusk/scene/CMakeLists.txt @@ -10,4 +10,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} ) # Subdirs -add_subdirectory(overworld) \ No newline at end of file +add_subdirectory(overworld) +add_subdirectory(battle) \ No newline at end of file diff --git a/src/dusk/scene/battle/CMakeLists.txt b/src/dusk/scene/battle/CMakeLists.txt new file mode 100644 index 00000000..a743129d --- /dev/null +++ b/src/dusk/scene/battle/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 + scenebattle.c +) diff --git a/src/dusk/scene/battle/scenebattle.c b/src/dusk/scene/battle/scenebattle.c new file mode 100644 index 00000000..0fabef58 --- /dev/null +++ b/src/dusk/scene/battle/scenebattle.c @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scenebattle.h" +#include "rpg/battle/battle.h" + +errorret_t sceneBattleInit(scenedata_t *sceneData) { + errorOk(); +} + +errorret_t sceneBattleUpdate(scenedata_t *sceneData) { + battleUpdate(); + errorOk(); +} + +errorret_t sceneBattleRender(scenedata_t *sceneData) { + errorOk(); +} + +errorret_t sceneBattleDispose(scenedata_t *sceneData) { + errorOk(); +} diff --git a/src/dusk/scene/battle/scenebattle.h b/src/dusk/scene/battle/scenebattle.h new file mode 100644 index 00000000..17046a35 --- /dev/null +++ b/src/dusk/scene/battle/scenebattle.h @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "scene/scenebase.h" + +typedef struct { + +} scenebattle_t; + +/** + * Initializes the battle scene. The battle itself (BATTLE global) is + * expected to already be started, e.g. by a StartBattle cutscene item. + * + * @param sceneData The scene data used for this scene. + * @return An error if the init failed, or errorOk() if it succeeded. + */ +errorret_t sceneBattleInit(scenedata_t *sceneData); + +/** + * Updates the battle scene, ticking the battle simulation. + * + * @param sceneData The scene data used for this scene. + * @return An error if the update failed, or errorOk() if it succeeded. + */ +errorret_t sceneBattleUpdate(scenedata_t *sceneData); + +/** + * Renders the battle scene. + * + * @param sceneData The scene data used for this scene. + * @return An error if the render failed, or errorOk() if it succeeded. + */ +errorret_t sceneBattleRender(scenedata_t *sceneData); + +/** + * Disposes the battle scene. + * + * @param sceneData The scene data used for this scene. + * @return An error if the dispose failed, or errorOk() if it succeeded. + */ +errorret_t sceneBattleDispose(scenedata_t *sceneData); diff --git a/src/dusk/scene/scenetype.c b/src/dusk/scene/scenetype.c index e2e76d7e..da285e33 100644 --- a/src/dusk/scene/scenetype.c +++ b/src/dusk/scene/scenetype.c @@ -16,5 +16,12 @@ scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = { .render = sceneOverworldRender, .dispose = sceneOverworldDispose }, + + [SCENE_TYPE_BATTLE] = { + .init = sceneBattleInit, + .update = sceneBattleUpdate, + .render = sceneBattleRender, + .dispose = sceneBattleDispose + }, }; diff --git a/src/dusk/scene/scenetype.h b/src/dusk/scene/scenetype.h index 9e981678..f3dd88bf 100644 --- a/src/dusk/scene/scenetype.h +++ b/src/dusk/scene/scenetype.h @@ -8,9 +8,11 @@ #pragma once #include "scene/scenebase.h" #include "scene/overworld/sceneoverworld.h" +#include "scene/battle/scenebattle.h" typedef union scenedata_u { sceneoverworld_t overworld; + scenebattle_t battle; } scenedata_t; typedef errorret_t (*scenecallback_t)(scenedata_t *); @@ -26,6 +28,7 @@ typedef enum { SCENE_TYPE_NULL, SCENE_TYPE_OVERWORLD, + SCENE_TYPE_BATTLE, SCENE_TYPE_COUNT } scenetype_t; diff --git a/src/dusk/ui/frame/CMakeLists.txt b/src/dusk/ui/frame/CMakeLists.txt index 758ad917..906cf620 100644 --- a/src/dusk/ui/frame/CMakeLists.txt +++ b/src/dusk/ui/frame/CMakeLists.txt @@ -11,3 +11,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} add_subdirectory(game) add_subdirectory(settings) +add_subdirectory(battle) +add_subdirectory(backpack) diff --git a/src/dusk/ui/frame/backpack/CMakeLists.txt b/src/dusk/ui/frame/backpack/CMakeLists.txt new file mode 100644 index 00000000..570abb53 --- /dev/null +++ b/src/dusk/ui/frame/backpack/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 + uibackpack.c +) diff --git a/src/dusk/ui/frame/backpack/uibackpack.c b/src/dusk/ui/frame/backpack/uibackpack.c new file mode 100644 index 00000000..d2cbddbf --- /dev/null +++ b/src/dusk/ui/frame/backpack/uibackpack.c @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uibackpack.h" +#include "ui/frame/uiframe.h" +#include "rpg/item/backpack.h" +#include "util/memory.h" +#include "util/string.h" +#include "display/spritebatch/spritebatch.h" +#include "display/screen/screen.h" +#include "display/text/text.h" +#include "assert/assert.h" + +uibackpack_t UI_BACKPACK; + +void uiBackpackTabChanged( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + const itemtype_t type = (itemtype_t)(index + 1); + const inventory_t *inventory = backpackGetInventory(type); + + errorCatch(uiItemListSetItemStacks( + &UI_BACKPACK.itemList, inventory->storage, inventory->storageSize + )); +} + +void uiBackpackTabSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + uiItemListOpen(&UI_BACKPACK.itemList); +} + +errorret_t uiBackpackInit(void) { + memoryZero(&UI_BACKPACK, sizeof(uibackpack_t)); + + MENU_BEGIN( + &UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs, + uiBackpackTabSelected, NULL, uiBackpackTabChanged + ); + for(uint8_t i = 0; i < UI_BACKPACK_TAB_COUNT; i++) { + stringFormat( + UI_BACKPACK.tabLabels[i], UI_BACKPACK_TAB_LABEL_MAX - 1, + "Category %u", i + 1 + ); + MENU_TAB(UI_BACKPACK.tabLabels[i]); + } + MENU_END(UI_BACKPACK.tabs, menuIndex); + + uiItemListInit( + &UI_BACKPACK.itemList, + UI_BACKPACK_ITEM_LIST_COLUMNS, UI_BACKPACK_ITEM_LIST_ROWS, 1, + NULL, NULL, NULL, NULL + ); + + errorOk(); +} + +errorret_t uiBackpackDraw(void) { + if(!uiMenuIsActive(&UI_BACKPACK.tabsMenu)) errorOk(); + + const float_t width = SCREEN.width; + const float_t height = SCREEN.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)); + + const float_t contentX = x + UI_FRAME_START_X; + const float_t contentY = y + UI_FRAME_START_Y; + const float_t contentWidth = width - (UI_FRAME_START_X * 2); + const float_t contentHeight = height - (UI_FRAME_START_Y * 2); + + errorChain(uiMenuDraw( + &UI_BACKPACK.tabsMenu, + contentX, + contentY, + contentWidth, + contentHeight + )); + + const float_t tabsRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; + const float_t listY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y; + + errorChain( + uiItemListDraw(&UI_BACKPACK.itemList, contentX, listY, contentWidth) + ); + + errorChain(spriteBatchFlush()); + errorOk(); +} + +bool_t uiBackpackIsOpen(void) { + return uiMenuIsActive(&UI_BACKPACK.tabsMenu); +} + +void uiBackpackOpen(void) { + uiMenuOpen(&UI_BACKPACK.tabsMenu); +} + +void uiBackpackClose(void) { + uiMenuClose(&UI_BACKPACK.tabsMenu); +} + +errorret_t uiBackpackDispose(void) { + errorOk(); +} diff --git a/src/dusk/ui/frame/backpack/uibackpack.h b/src/dusk/ui/frame/backpack/uibackpack.h new file mode 100644 index 00000000..bbd56ee0 --- /dev/null +++ b/src/dusk/ui/frame/backpack/uibackpack.h @@ -0,0 +1,65 @@ +/** + * 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" +#include "ui/widget/uiitemlist.h" +#include "rpg/item/item.h" + +#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_COUNT - 1) +#define UI_BACKPACK_TAB_LABEL_MAX 32 +#define UI_BACKPACK_ITEM_LIST_COLUMNS 4 +#define UI_BACKPACK_ITEM_LIST_ROWS 5 + +typedef struct { + uimenu_t tabsMenu; + uimenuitem_t tabs[UI_BACKPACK_TAB_COUNT]; + char_t tabLabels[UI_BACKPACK_TAB_COUNT][UI_BACKPACK_TAB_LABEL_MAX]; + + uiitemlist_t itemList; +} uibackpack_t; + +extern uibackpack_t UI_BACKPACK; + +/** + * Initializes the backpack panel and its item type tabs. + * + * @return Any error that occurs. + */ +errorret_t uiBackpackInit(void); + +/** + * Draws the backpack panel. No-op when not visible. + * + * @return Any error that occurs. + */ +errorret_t uiBackpackDraw(void); + +/** + * Returns true when the backpack panel is currently open. + * + * @returns True if open. + */ +bool_t uiBackpackIsOpen(void); + +/** + * Opens the backpack panel. No-op when already open. + */ +void uiBackpackOpen(void); + +/** + * Closes the backpack panel. No-op when already closed. + */ +void uiBackpackClose(void); + +/** + * Disposes of the backpack panel. + * + * @return Any error that occurs. + */ +errorret_t uiBackpackDispose(void); diff --git a/src/dusk/ui/frame/battle/CMakeLists.txt b/src/dusk/ui/frame/battle/CMakeLists.txt new file mode 100644 index 00000000..4b6bd9e2 --- /dev/null +++ b/src/dusk/ui/frame/battle/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 + uibattlemenu.c +) diff --git a/src/dusk/ui/frame/battle/uibattlemenu.c b/src/dusk/ui/frame/battle/uibattlemenu.c new file mode 100644 index 00000000..af947c1f --- /dev/null +++ b/src/dusk/ui/frame/battle/uibattlemenu.c @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uibattlemenu.h" +#include "ui/frame/uiframe.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "display/spritebatch/spritebatch.h" +#include "display/screen/screen.h" + +uibattlemenu_t UI_BATTLE_MENU; + +void uiBattleMenuTargetSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + battlePlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]); + uiMenuClose(&UI_BATTLE_MENU.targetMenu); + uiMenuClose(&UI_BATTLE_MENU.actionMenu); +} + +void uiBattleMenuOpenTargets(void) { + battlefighter_t *current = battleGetCurrentFighter(); + if(current == NULL) return; + + const battlefighterteam_t enemyTeam = + current->team == BATTLE_FIGHTER_TEAM_ALLY ? + BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY; + + MENU_BEGIN( + &UI_BATTLE_MENU.targetMenu, UI_BATTLE_MENU.targetItems, + uiBattleMenuTargetSelected, NULL, NULL + ); + + for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) { + battlefighter_t *candidate = &BATTLE.fighters[i]; + if(candidate->team != enemyTeam) continue; + if(!battleFighterIsAlive(candidate)) continue; + + stringFormat( + UI_BATTLE_MENU.targetLabels[menuIndex], + UI_BATTLE_MENU_TARGET_LABEL_MAX - 1, + "Enemy %u (%u/%u HP)", + candidate->id, candidate->health, candidate->healthMax + ); + UI_BATTLE_MENU.targetFighterIndex[menuIndex] = i; + MENU_BUTTON(UI_BATTLE_MENU.targetLabels[menuIndex]); + } + + MENU_END(UI_BATTLE_MENU.targetItems, 1); + + uiMenuOpen(&UI_BATTLE_MENU.targetMenu); +} + +void uiBattleMenuActionSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + switch(index) { + case UI_BATTLE_MENU_ACTION_INDEX_ATTACK: + uiBattleMenuOpenTargets(); + break; + + case UI_BATTLE_MENU_ACTION_INDEX_FLEE: + battlePlayerFlee(); + uiMenuClose(&UI_BATTLE_MENU.actionMenu); + break; + + default: + break; + } +} + +errorret_t uiBattleMenuInit(void) { + memoryZero(&UI_BATTLE_MENU, sizeof(uibattlemenu_t)); + + MENU_BEGIN( + &UI_BATTLE_MENU.actionMenu, UI_BATTLE_MENU.actionItems, + uiBattleMenuActionSelected, NULL, NULL + ); + MENU_BUTTON("Attack"); + MENU_BUTTON("Flee"); + MENU_END(UI_BATTLE_MENU.actionItems, 1); + + errorOk(); +} + +errorret_t uiBattleMenuUpdate(void) { + if(uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk(); + + battlefighter_t *current = battleGetCurrentFighter(); + if(current == NULL) errorOk(); + if(current->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) errorOk(); + + uiMenuOpen(&UI_BATTLE_MENU.actionMenu); + errorOk(); +} + +errorret_t uiBattleMenuDraw(void) { + if(!uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk(); + + const float_t width = UI_BATTLE_MENU_WIDTH; + const float_t height = UI_BATTLE_MENU_HEIGHT; + const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width; + const float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - height; + + errorChain(uiFrameDraw(x, y, width, height)); + errorChain(uiMenuDraw( + &UI_BATTLE_MENU.actionMenu, + x + UI_FRAME_START_X, + y + UI_FRAME_START_Y, + width - (UI_FRAME_START_X * 2), + height - (UI_FRAME_START_Y * 2) + )); + + if(uiMenuIsActive(&UI_BATTLE_MENU.targetMenu)) { + const float_t targetY = y - height; + errorChain(uiFrameDraw(x, targetY, width, height)); + errorChain(uiMenuDraw( + &UI_BATTLE_MENU.targetMenu, + x + UI_FRAME_START_X, + targetY + UI_FRAME_START_Y, + width - (UI_FRAME_START_X * 2), + height - (UI_FRAME_START_Y * 2) + )); + } + + errorChain(spriteBatchFlush()); + errorOk(); +} diff --git a/src/dusk/ui/frame/battle/uibattlemenu.h b/src/dusk/ui/frame/battle/uibattlemenu.h new file mode 100644 index 00000000..024d5d6e --- /dev/null +++ b/src/dusk/ui/frame/battle/uibattlemenu.h @@ -0,0 +1,57 @@ +/** + * 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" +#include "rpg/battle/battle.h" + +#define UI_BATTLE_MENU_ACTION_ITEM_COUNT 2 +#define UI_BATTLE_MENU_ACTION_INDEX_ATTACK 0 +#define UI_BATTLE_MENU_ACTION_INDEX_FLEE 1 + +#define UI_BATTLE_MENU_TARGET_ITEM_COUNT BATTLE_FIGHTER_COUNT_MAX +#define UI_BATTLE_MENU_TARGET_LABEL_MAX 32 + +#define UI_BATTLE_MENU_WIDTH 160.0f +#define UI_BATTLE_MENU_HEIGHT 96.0f + +typedef struct { + uimenu_t actionMenu; + uimenuitem_t actionItems[UI_BATTLE_MENU_ACTION_ITEM_COUNT]; + + uimenu_t targetMenu; + uimenuitem_t targetItems[UI_BATTLE_MENU_TARGET_ITEM_COUNT]; + uint8_t targetFighterIndex[UI_BATTLE_MENU_TARGET_ITEM_COUNT]; + char_t targetLabels[UI_BATTLE_MENU_TARGET_ITEM_COUNT] + [UI_BATTLE_MENU_TARGET_LABEL_MAX]; +} uibattlemenu_t; + +extern uibattlemenu_t UI_BATTLE_MENU; + +/** + * Initializes the battle action/target menus. + * + * @return Any error that occurs. + */ +errorret_t uiBattleMenuInit(void); + +/** + * Updates the battle menu: opens the action menu whenever it becomes a + * player-controlled fighter's turn. + * + * @return Any error that occurs. + */ +errorret_t uiBattleMenuUpdate(void); + +/** + * Draws the battle action menu, and the target menu above it when + * open. No-op when the action menu isn't active. + * + * @return Any error that occurs. + */ +errorret_t uiBattleMenuDraw(void); diff --git a/src/dusk/ui/frame/game/uigamemenu.c b/src/dusk/ui/frame/game/uigamemenu.c index 4d5d7c96..bfdb2203 100644 --- a/src/dusk/ui/frame/game/uigamemenu.c +++ b/src/dusk/ui/frame/game/uigamemenu.c @@ -8,13 +8,17 @@ #include "uigamemenu.h" #include "ui/frame/uiframe.h" #include "ui/frame/settings/uisettings.h" +#include "ui/frame/backpack/uibackpack.h" #include "util/memory.h" #include "display/spritebatch/spritebatch.h" #include "display/screen/screen.h" #include "assert/assert.h" +#include "locale/localemanager.h" +#include "asset/loader/locale/assetlocaleloader.h" #define UI_GAME_MENU_INDEX_CHARACTERS 0 -#define UI_GAME_MENU_INDEX_SETTINGS 1 +#define UI_GAME_MENU_INDEX_ITEMS 1 +#define UI_GAME_MENU_INDEX_SETTINGS 2 uigamemenu_t UI_GAME_MENU; @@ -23,17 +27,41 @@ void uiGameMenuSelected( const uint8_t index, const uimenuitem_t *item ) { + if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen(); if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen(); } errorret_t uiGameMenuInit(void) { memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t)); + errorChain(assetLocaleGetString( + &LOCALE.entry->data.locale, + "ui.game_menu.characters", + 0, + UI_GAME_MENU.charactersLabel, + UI_GAME_MENU_LABEL_MAX + )); + errorChain(assetLocaleGetString( + &LOCALE.entry->data.locale, + "ui.game_menu.items", + 0, + UI_GAME_MENU.itemsLabel, + UI_GAME_MENU_LABEL_MAX + )); + errorChain(assetLocaleGetString( + &LOCALE.entry->data.locale, + "ui.game_menu.settings", + 0, + UI_GAME_MENU.settingsLabel, + UI_GAME_MENU_LABEL_MAX + )); + MENU_BEGIN( &UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL ); - MENU_BUTTON("Characters"); - MENU_BUTTON("Settings"); + MENU_BUTTON(UI_GAME_MENU.charactersLabel); + MENU_BUTTON(UI_GAME_MENU.itemsLabel); + MENU_BUTTON(UI_GAME_MENU.settingsLabel); MENU_END(UI_GAME_MENU.items, 1); diff --git a/src/dusk/ui/frame/game/uigamemenu.h b/src/dusk/ui/frame/game/uigamemenu.h index 45235f47..ce6307fe 100644 --- a/src/dusk/ui/frame/game/uigamemenu.h +++ b/src/dusk/ui/frame/game/uigamemenu.h @@ -9,12 +9,16 @@ #include "error/error.h" #include "ui/widget/uimenu.h" -#define UI_GAME_MENU_ITEM_COUNT 2 +#define UI_GAME_MENU_ITEM_COUNT 3 #define UI_GAME_MENU_WIDTH 150.0f +#define UI_GAME_MENU_LABEL_MAX 32 typedef struct { uimenu_t menu; uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT]; + char_t charactersLabel[UI_GAME_MENU_LABEL_MAX]; + char_t itemsLabel[UI_GAME_MENU_LABEL_MAX]; + char_t settingsLabel[UI_GAME_MENU_LABEL_MAX]; } uigamemenu_t; extern uigamemenu_t UI_GAME_MENU; diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index 7ddf2cba..14e75a78 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -18,6 +18,8 @@ #include "ui/debug/uiconsole.h" #include "ui/frame/settings/uisettings.h" #include "ui/frame/game/uigamemenu.h" +#include "ui/frame/battle/uibattlemenu.h" +#include "ui/frame/backpack/uibackpack.h" #include "ui/frame/uiconfirm.h" #include "ui/rpg/uitextboxmain.h" @@ -48,6 +50,18 @@ uielement_t UI_ELEMENTS[] = { .dispose = uiSettingsDispose }, + { + .init = uiBattleMenuInit, + .update = uiBattleMenuUpdate, + .draw = uiBattleMenuDraw + }, + + { + .init = uiBackpackInit, + .draw = uiBackpackDraw, + .dispose = uiBackpackDispose + }, + { .init = uiConfirmInit, .draw = uiConfirmDraw, diff --git a/src/dusk/ui/widget/CMakeLists.txt b/src/dusk/ui/widget/CMakeLists.txt index 65825936..2e3eaf06 100644 --- a/src/dusk/ui/widget/CMakeLists.txt +++ b/src/dusk/ui/widget/CMakeLists.txt @@ -10,5 +10,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} uitab.c uislider.c uidropdown.c + uiscrolling.c + uiitem.c + uiitemlist.c uimenu.c ) diff --git a/src/dusk/ui/widget/uiitem.c b/src/dusk/ui/widget/uiitem.c new file mode 100644 index 00000000..9e73b676 --- /dev/null +++ b/src/dusk/ui/widget/uiitem.c @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uiitem.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "display/text/text.h" +#include "display/color.h" + +#define UI_ITEM_LABEL_MAX 48 + +errorret_t uiItemInit( + uiitem_t *item, + const itemid_t itemId, + const uint8_t quantity +) { + assertNotNull(item, "Item cannot be NULL"); + memoryZero(item, sizeof(uiitem_t)); + item->item = itemId; + item->quantity = quantity; + + if(itemId == ITEM_ID_NULL) errorOk(); + + errorChain(itemGetName(itemId, item->nameLabel, UI_ITEM_NAME_LABEL_MAX)); + + errorOk(); +} + +itemid_t uiItemGetItem(const uiitem_t *item) { + assertNotNull(item, "Item cannot be NULL"); + return item->item; +} + +uint8_t uiItemGetQuantity(const uiitem_t *item) { + assertNotNull(item, "Item cannot be NULL"); + return item->quantity; +} + +void uiItemSetQuantity(uiitem_t *item, const uint8_t quantity) { + assertNotNull(item, "Item cannot be NULL"); + item->quantity = quantity; +} + +bool_t uiItemIsHighlighted(const uiitem_t *item) { + assertNotNull(item, "Item cannot be NULL"); + return item->highlighted; +} + +void uiItemSetHighlighted(uiitem_t *item, const bool_t highlighted) { + assertNotNull(item, "Item cannot be NULL"); + item->highlighted = highlighted; +} + +errorret_t uiItemDraw( + const uiitem_t *item, + const float_t x, + const float_t y +) { + assertNotNull(item, "Item cannot be NULL"); + if(item->item == ITEM_ID_NULL) errorOk(); + + const color_t color = item->highlighted ? COLOR_RED : COLOR_WHITE; + + char_t text[UI_ITEM_LABEL_MAX]; + stringFormat( + text, UI_ITEM_LABEL_MAX - 1, + "%s x%u", item->nameLabel, item->quantity + ); + + errorChain(textDraw(x, y, text, color, &FONT_DEFAULT)); + errorOk(); +} diff --git a/src/dusk/ui/widget/uiitem.h b/src/dusk/ui/widget/uiitem.h new file mode 100644 index 00000000..787884c8 --- /dev/null +++ b/src/dusk/ui/widget/uiitem.h @@ -0,0 +1,89 @@ +/** + * 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 "rpg/item/item.h" + +#define UI_ITEM_NAME_LABEL_MAX 32 + +typedef struct { + itemid_t item; + uint8_t quantity; + bool_t highlighted; + char_t nameLabel[UI_ITEM_NAME_LABEL_MAX]; +} uiitem_t; + +/** + * Initializes an item widget, resolving the item's localized name. + * + * @param item The item widget to initialize. + * @param itemId The item ID to display. ITEM_ID_NULL renders as an + * empty slot. + * @param quantity The stack quantity to display. + * @return Any error that occurs. + */ +errorret_t uiItemInit( + uiitem_t *item, + const itemid_t itemId, + const uint8_t quantity +); + +/** + * Returns the item ID this widget displays. + * + * @param item The item widget to query. + * @returns The item ID. + */ +itemid_t uiItemGetItem(const uiitem_t *item); + +/** + * Returns the stack quantity this widget displays. + * + * @param item The item widget to query. + * @returns The quantity. + */ +uint8_t uiItemGetQuantity(const uiitem_t *item); + +/** + * Sets the stack quantity this widget displays. + * + * @param item The item widget to update. + * @param quantity The new quantity. + */ +void uiItemSetQuantity(uiitem_t *item, const uint8_t quantity); + +/** + * Returns whether the item widget is highlighted. + * + * @param item The item widget to query. + * @returns True if highlighted. + */ +bool_t uiItemIsHighlighted(const uiitem_t *item); + +/** + * Sets the highlighted state of the item widget. + * + * @param item The item widget to update. + * @param highlighted The new highlighted state. + */ +void uiItemSetHighlighted(uiitem_t *item, const bool_t highlighted); + +/** + * Draws the item widget at the given screen position: item name and + * quantity. No-op for an empty (ITEM_ID_NULL) slot. + * + * @param item The item widget to draw. + * @param x Screen x position. + * @param y Screen y position. + * @return Any error that occurs. + */ +errorret_t uiItemDraw( + const uiitem_t *item, + const float_t x, + const float_t y +); diff --git a/src/dusk/ui/widget/uiitemlist.c b/src/dusk/ui/widget/uiitemlist.c new file mode 100644 index 00000000..2ab2f62a --- /dev/null +++ b/src/dusk/ui/widget/uiitemlist.c @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uiitemlist.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "display/text/text.h" + +void uiItemListInit( + uiitemlist_t *list, + const uint8_t columns, + const uint8_t rows, + const uint8_t itemColumns, + uiitemlistselectedcallback_t selected, + uiitemlistchangedcallback_t changed, + uiitemlistclosedcallback_t closed, + uiitemlistcolumncallback_t columnDraw +) { + assertNotNull(list, "Item list cannot be NULL"); + assertTrue(columns > 0, "Item list columns must be > 0"); + assertTrue(rows > 0, "Item list rows must be > 0"); + assertTrue(itemColumns > 0, "Item list itemColumns must be > 0"); + + memoryZero(list, sizeof(uiitemlist_t)); + list->columns = columns; + list->rows = rows; + list->itemColumns = itemColumns; + list->selected = selected; + list->changed = changed; + list->closed = closed; + list->columnDraw = columnDraw; + + uiScrollingInit(&list->scroll); +} + +void uiItemListSetItems( + uiitemlist_t *list, + const uiitem_t *items, + const uint8_t itemCount +) { + assertNotNull(list, "Item list cannot be NULL"); + assertTrue( + itemCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list" + ); + + memoryCopy(list->items, items, sizeof(uiitem_t) * itemCount); + list->itemCount = itemCount; +} + +errorret_t uiItemListSetItemStacks( + uiitemlist_t *list, + const inventorystack_t *stacks, + const uint8_t stackCount +) { + assertNotNull(list, "Item list cannot be NULL"); + assertTrue( + stackCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list" + ); + + for(uint8_t i = 0; i < stackCount; i++) { + errorChain( + uiItemInit(&list->items[i], stacks[i].item, stacks[i].quantity) + ); + } + list->itemCount = stackCount; + + errorOk(); +} + +void uiItemListOpen(uiitemlist_t *list) { + assertNotNull(list, "Item list cannot be NULL"); + if(list->focusItem != NULL) return; + + list->focusItem = uiFocusPush( + list->columns, list->rows, + uiItemListFocusSelected, + uiItemListFocusChanged, + uiItemListFocusClosed, + NULL, + list + ); +} + +void uiItemListClose(uiitemlist_t *list) { + assertNotNull(list, "Item list cannot be NULL"); + if(list->focusItem == NULL) return; + uiFocusPopItem(list->focusItem); + list->focusItem = NULL; +} + +bool_t uiItemListIsActive(const uiitemlist_t *list) { + assertNotNull(list, "Item list cannot be NULL"); + return list->focusItem != NULL; +} + +errorret_t uiItemListDraw( + const uiitemlist_t *list, + const float_t x, + const float_t y, + const float_t width +) { + assertNotNull(list, "Item list cannot be NULL"); + if(list->itemCount == 0) errorOk(); + + const float_t colStep = width / (float_t)list->columns; + const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; + const float_t itemColStep = colStep / (float_t)list->itemColumns; + + const uint8_t visibleMax = list->columns * list->rows; + const uint8_t drawCount = + list->itemCount < visibleMax ? list->itemCount : visibleMax; + + for(uint8_t i = 0; i < drawCount; i++) { + const uint8_t col = i % list->columns; + const uint8_t row = i / list->columns; + + const float_t ix = x + (float_t)col * colStep; + const float_t iy = y + (float_t)row * rowHeight; + + errorChain(uiItemDraw(&list->items[i], ix, iy)); + + for(uint8_t c = 1; c < list->itemColumns; c++) { + if(list->columnDraw == NULL) continue; + errorChain(list->columnDraw( + list, &list->items[i], c, ix + (float_t)c * itemColStep, iy + )); + } + } + + errorOk(); +} + +bool_t uiItemListFocusSelected(const uifocusitem_t *focusItem) { + assertNotNull(focusItem, "Focus item cannot be NULL"); + assertNotNull(focusItem->user, "Focus item user cannot be NULL"); + uiitemlist_t *list = (uiitemlist_t *)focusItem->user; + if(list->selected == NULL) return true; + + const uint8_t index = focusItem->y * list->columns + focusItem->x; + if(index >= list->itemCount) return true; + + list->selected(list, index, &list->items[index]); + return true; +} + +bool_t uiItemListFocusChanged(const uifocusitem_t *focusItem) { + assertNotNull(focusItem, "Focus item cannot be NULL"); + assertNotNull(focusItem->user, "Focus item user cannot be NULL"); + uiitemlist_t *list = (uiitemlist_t *)focusItem->user; + + const uint8_t focusIndex = focusItem->y * list->columns + focusItem->x; + for(uint8_t i = 0; i < list->itemCount; i++) { + uiItemSetHighlighted(&list->items[i], i == focusIndex); + } + + if(list->changed == NULL) return true; + if(focusIndex >= list->itemCount) return true; + + list->changed(list, focusIndex, &list->items[focusIndex]); + return true; +} + +bool_t uiItemListFocusClosed(const uifocusitem_t *focusItem) { + assertNotNull(focusItem, "Focus item cannot be NULL"); + assertNotNull(focusItem->user, "Focus item user cannot be NULL"); + uiitemlist_t *list = (uiitemlist_t *)focusItem->user; + list->focusItem = NULL; + + for(uint8_t i = 0; i < list->itemCount; i++) { + uiItemSetHighlighted(&list->items[i], false); + } + + if(list->closed != NULL) list->closed(list); + return true; +} diff --git a/src/dusk/ui/widget/uiitemlist.h b/src/dusk/ui/widget/uiitemlist.h new file mode 100644 index 00000000..26b3e350 --- /dev/null +++ b/src/dusk/ui/widget/uiitemlist.h @@ -0,0 +1,200 @@ +/** + * 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/uiitem.h" +#include "ui/widget/uiscrolling.h" +#include "ui/focus/uifocus.h" +#include "rpg/item/inventory.h" + +#define UI_ITEM_LIST_CAPACITY_MAX 40 + +typedef struct uiitemlist_s uiitemlist_t; + +typedef void (*uiitemlistselectedcallback_t)( + const uiitemlist_t *list, + const uint8_t index, + const uiitem_t *item +); + +typedef void (*uiitemlistchangedcallback_t)( + const uiitemlist_t *list, + const uint8_t index, + const uiitem_t *item +); + +typedef void (*uiitemlistclosedcallback_t)(const uiitemlist_t *list); + +/** + * Called to draw an extra per-item info column (columnIndex >= 1) + * beyond the item's own default uiItemDraw rendering -- e.g. a shop + * menu drawing a price alongside the item name. + * + * @param list The item list being drawn. + * @param item The item slot being drawn. + * @param columnIndex The info column being drawn (>= 1). + * @param x Screen x position for this column. + * @param y Screen y position for this column. + * @return Any error that occurs. + */ +typedef errorret_t (*uiitemlistcolumncallback_t)( + const uiitemlist_t *list, + const uiitem_t *item, + const uint8_t columnIndex, + const float_t x, + const float_t y +); + +struct uiitemlist_s { + uiitem_t items[UI_ITEM_LIST_CAPACITY_MAX]; + uint8_t itemCount; + + // Grid layout: how many item slots wide/tall the list displays. + uint8_t columns; + uint8_t rows; + + // How many info columns are rendered per item slot. 1 means only the + // item's own default name/quantity is drawn; anything beyond that is + // rendered via columnDraw. + uint8_t itemColumns; + + uiscrolling_t scroll; + uifocusitem_t *focusItem; + + uiitemlistselectedcallback_t selected; + uiitemlistchangedcallback_t changed; + uiitemlistclosedcallback_t closed; + uiitemlistcolumncallback_t columnDraw; + + void *user; +}; + +/** + * Initializes an item list. + * + * @param list The item list to initialize. + * @param columns Number of item slots per row. + * @param rows Number of item slot rows. + * @param itemColumns Number of info columns rendered per item slot. + * @param selected Called when an item slot is selected. + * @param changed Called when the focused item slot changes. + * @param closed Called when the item list is closed. + * @param columnDraw Called to draw each extra info column (index >= 1); + * may be NULL if itemColumns is 1. + */ +void uiItemListInit( + uiitemlist_t *list, + const uint8_t columns, + const uint8_t rows, + const uint8_t itemColumns, + uiitemlistselectedcallback_t selected, + uiitemlistchangedcallback_t changed, + uiitemlistclosedcallback_t closed, + uiitemlistcolumncallback_t columnDraw +); + +/** + * Sets the items displayed by the list directly, copying them into + * the list's own storage. + * + * @param list The item list to update. + * @param items The items to display. + * @param itemCount Number of entries in items. Must be <= + * UI_ITEM_LIST_CAPACITY_MAX. + */ +void uiItemListSetItems( + uiitemlist_t *list, + const uiitem_t *items, + const uint8_t itemCount +); + +/** + * Sets the items displayed by the list from an array of item stacks, + * internally creating a uiitem_t for each stack. + * + * @param list The item list to update. + * @param stacks The item stacks to display. + * @param stackCount Number of entries in stacks. Must be <= + * UI_ITEM_LIST_CAPACITY_MAX. + * @return Any error that occurs. + */ +errorret_t uiItemListSetItemStacks( + uiitemlist_t *list, + const inventorystack_t *stacks, + const uint8_t stackCount +); + +/** + * Pushes the item list onto the UI focus stack, making it navigable. + * No-op if already open. + * + * @param list The item list to open. + */ +void uiItemListOpen(uiitemlist_t *list); + +/** + * Pops the item list from the UI focus stack. No-op if already closed. + * + * @param list The item list to close. + */ +void uiItemListClose(uiitemlist_t *list); + +/** + * Returns whether the item list is currently on the UI focus stack. + * + * @param list The item list to query. + * @returns True if active. + */ +bool_t uiItemListIsActive(const uiitemlist_t *list); + +/** + * Draws the item list's grid of item slots at the given position. + * Only the first columns * rows items are drawn. + * + * @param list The item list to draw. + * @param x Screen x position. + * @param y Screen y position. + * @param width Content width, used to lay out columns. + * @return Any error that occurs. + */ +errorret_t uiItemListDraw( + const uiitemlist_t *list, + const float_t x, + const float_t y, + const float_t width +); + +/** + * Internal focus callback -- forwards selection to the list's selected + * handler. + * + * @param focusItem The active focus item; user field must point to + * uiitemlist_t. + * @returns True. + */ +bool_t uiItemListFocusSelected(const uifocusitem_t *focusItem); + +/** + * Internal focus callback -- updates item highlights and fires + * changed. + * + * @param focusItem The active focus item; user field must point to + * uiitemlist_t. + * @returns True. + */ +bool_t uiItemListFocusChanged(const uifocusitem_t *focusItem); + +/** + * Internal focus callback -- clears focusItem and fires the closed + * handler. + * + * @param focusItem The active focus item; user field must point to + * uiitemlist_t. + * @returns True. + */ +bool_t uiItemListFocusClosed(const uifocusitem_t *focusItem); diff --git a/src/dusk/ui/widget/uiscrolling.c b/src/dusk/ui/widget/uiscrolling.c new file mode 100644 index 00000000..168ddc04 --- /dev/null +++ b/src/dusk/ui/widget/uiscrolling.c @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uiscrolling.h" +#include "assert/assert.h" + +void uiScrollingInit(uiscrolling_t *scrolling) { + assertNotNull(scrolling, "Scrolling container cannot be NULL"); + // Nothing to initialize yet -- uiscrolling_t is currently a + // placeholder with no fields. +} diff --git a/src/dusk/ui/widget/uiscrolling.h b/src/dusk/ui/widget/uiscrolling.h new file mode 100644 index 00000000..7c11380f --- /dev/null +++ b/src/dusk/ui/widget/uiscrolling.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +typedef struct { + +} uiscrolling_t; + +/** + * Initializes a scrolling container. Currently a placeholder -- no + * scrolling behavior is implemented yet. + * + * @param scrolling The scrolling container to initialize. + */ +void uiScrollingInit(uiscrolling_t *scrolling); diff --git a/tools/item.py b/tools/item.py index 70defe19..c0637237 100644 --- a/tools/item.py +++ b/tools/item.py @@ -20,8 +20,12 @@ rows = {} with open(args.csv, newline="", encoding="utf-8") as f: reader = csv.DictReader(f) - if "id" not in reader.fieldnames or "type" not in reader.fieldnames: - raise ValueError("CSV must have 'id' and 'type' columns") + if ( + "id" not in reader.fieldnames or + "type" not in reader.fieldnames or + "name" not in reader.fieldnames + ): + raise ValueError("CSV must have 'id', 'type', and 'name' columns") for row in reader: item_id, item_type = row["id"], row["type"] if item_id not in item_ids: @@ -85,7 +89,7 @@ for i in item_ids: f" [{id_enum(i)}] = {{", f" .id = {id_enum(i)},", f" .type = {type_enum(row['type'])},", - f" .name = \"{i}\",", + f" .name = \"item.{row['name']}.name\",", " },", ] out += [