Starting item and battle stuff

This commit is contained in:
2026-07-08 10:05:21 -05:00
parent a73f55beb0
commit b693ea4102
41 changed files with 1727 additions and 20 deletions
+22 -1
View File
@@ -42,4 +42,25 @@ msgstr "Apply"
#: src/dusk/ui/frame/uiconfirm.c #: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes" msgid "ui.confirm.discard_changes"
msgstr "Discard unsaved changes?" 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"
+24
View File
@@ -44,3 +44,27 @@ msgstr "Aplicar"
#: src/dusk/ui/frame/uiconfirm.c #: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes" msgid "ui.confirm.discard_changes"
msgstr "¿Descartar los cambios no guardados?" 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"
+24
View File
@@ -44,3 +44,27 @@ msgstr "適用"
#: src/dusk/ui/frame/uiconfirm.c #: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes" msgid "ui.confirm.discard_changes"
msgstr "未保存の変更を破棄しますか?" 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 "リンゴ"
+175 -1
View File
@@ -6,6 +6,7 @@
*/ */
#include "battle.h" #include "battle.h"
#include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
battle_t BATTLE; battle_t BATTLE;
@@ -40,10 +41,183 @@ battlefighter_t *battleAddFighter(
return fighter; 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; BATTLE.active = true;
} }
void battleDispose(void) { void battleDispose(void) {
battleInit(); 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;
}
+136 -3
View File
@@ -10,9 +10,36 @@
#define BATTLE_FIGHTER_COUNT_MAX 8 #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 { typedef struct {
bool_t active; bool_t active;
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX]; 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; } battle_t;
extern battle_t BATTLE; extern battle_t BATTLE;
@@ -50,12 +77,118 @@ battlefighter_t *battleAddFighter(
); );
/** /**
* Starts a battle, marking it active. Any fighters already added via * Starts the battle: builds the opening turn order (biased by
* battleAddFighter remain in place. * 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. * Disposes of the battle, clearing all fighters and marking it inactive.
*/ */
void battleDispose(void); 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);
@@ -14,3 +14,4 @@ add_subdirectory(entity)
add_subdirectory(item) add_subdirectory(item)
add_subdirectory(maparea) add_subdirectory(maparea)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(battle)
@@ -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
)
@@ -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;
}
@@ -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
);
@@ -77,6 +77,10 @@ void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
cutsceneMapAreaWaitStart(item, data); cutsceneMapAreaWaitStart(item, data);
break; break;
case CUTSCENE_ITEM_TYPE_START_BATTLE:
cutsceneStartBattleStart(item, data);
break;
default: default:
break; break;
} }
@@ -132,6 +136,9 @@ bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data)
case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT: case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT:
return cutsceneMapAreaWaitUpdate(item, data); return cutsceneMapAreaWaitUpdate(item, data);
case CUTSCENE_ITEM_TYPE_START_BATTLE:
return cutsceneStartBattleUpdate(item, data);
default: default:
return false; return false;
} }
+4 -1
View File
@@ -22,6 +22,7 @@
#include "maparea/cutscenemapareaadd.h" #include "maparea/cutscenemapareaadd.h"
#include "maparea/cutscenemaparearemove.h" #include "maparea/cutscenemaparearemove.h"
#include "maparea/cutscenemapareawait.h" #include "maparea/cutscenemapareawait.h"
#include "battle/cutscenestartbattle.h"
typedef struct cutscene_s cutscene_t; typedef struct cutscene_s cutscene_t;
@@ -43,7 +44,8 @@ typedef enum {
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY,
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, 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; } cutsceneitemtype_t;
struct cutsceneitem_s { struct cutsceneitem_s {
@@ -67,6 +69,7 @@ struct cutsceneitem_s {
cutscenemapareaadd_t mapAreaAdd; cutscenemapareaadd_t mapAreaAdd;
cutscenemaparearemove_t mapAreaRemove; cutscenemaparearemove_t mapAreaRemove;
cutscenemapareawait_t mapAreaWait; cutscenemapareawait_t mapAreaWait;
cutscenestartbattle_t startBattle;
}; };
}; };
+2 -1
View File
@@ -6,6 +6,7 @@
# Sources # Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
item.c
inventory.c inventory.c
backpack.c backpack.c
itemgive.c itemgive.c
@@ -16,6 +17,6 @@ dusk_run_python(
dusk_item_csv_defs dusk_item_csv_defs
tools.item tools.item
--csv ${CMAKE_CURRENT_SOURCE_DIR}/item.csv --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) add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_csv_defs)
+30
View File
@@ -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();
}
+4 -4
View File
@@ -1,4 +1,4 @@
id,type,weight id,type,weight,name
POTION,MEDICINE,1.0 POTION,MEDICINE,1.0,potion
POTATO,FOOD,0.5 POTATO,FOOD,0.5,potato
APPLE,FOOD,0.3 APPLE,FOOD,0.3,apple
1 id type weight name
2 POTION MEDICINE 1.0 potion
3 POTATO FOOD 0.5 potato
4 APPLE FOOD 0.3 apple
+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
*/
#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
);
+7 -1
View File
@@ -9,16 +9,22 @@
#include "rpg/item/backpack.h" #include "rpg/item/backpack.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/uitextboxmain.h"
#include "util/string.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) { void itemGive(const itemid_t item, const uint8_t quantity) {
backpackAdd(item, 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]; char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS];
stringFormat( stringFormat(
msg, msg,
ITEM_GIVE_MESSAGE_MAX_CHARS - 1, ITEM_GIVE_MESSAGE_MAX_CHARS - 1,
"Received %s x%u", "Received %s x%u",
ITEMS[item].name, name,
(uint32_t)quantity (uint32_t)quantity
); );
uiTextboxMainSetText(msg); uiTextboxMainSetText(msg);
+5
View File
@@ -59,6 +59,11 @@ errorret_t rpgInit(void) {
entityItemSet(itemEnt, ITEM_ID_POTION, 1); entityItemSet(itemEnt, ITEM_ID_POTION, 1);
entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 }); 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. // TEST: Create a test map area.
uint8_t areaIndex = mapAreaAdd( uint8_t areaIndex = mapAreaAdd(
(worldpos_t){ 11, 3, 0 }, (worldpos_t){ 11, 3, 0 },
+2 -1
View File
@@ -10,4 +10,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
) )
# Subdirs # Subdirs
add_subdirectory(overworld) add_subdirectory(overworld)
add_subdirectory(battle)
+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
scenebattle.c
)
+26
View File
@@ -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();
}
+46
View File
@@ -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);
+7
View File
@@ -16,5 +16,12 @@ scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
.render = sceneOverworldRender, .render = sceneOverworldRender,
.dispose = sceneOverworldDispose .dispose = sceneOverworldDispose
}, },
[SCENE_TYPE_BATTLE] = {
.init = sceneBattleInit,
.update = sceneBattleUpdate,
.render = sceneBattleRender,
.dispose = sceneBattleDispose
},
}; };
+3
View File
@@ -8,9 +8,11 @@
#pragma once #pragma once
#include "scene/scenebase.h" #include "scene/scenebase.h"
#include "scene/overworld/sceneoverworld.h" #include "scene/overworld/sceneoverworld.h"
#include "scene/battle/scenebattle.h"
typedef union scenedata_u { typedef union scenedata_u {
sceneoverworld_t overworld; sceneoverworld_t overworld;
scenebattle_t battle;
} scenedata_t; } scenedata_t;
typedef errorret_t (*scenecallback_t)(scenedata_t *); typedef errorret_t (*scenecallback_t)(scenedata_t *);
@@ -26,6 +28,7 @@ typedef enum {
SCENE_TYPE_NULL, SCENE_TYPE_NULL,
SCENE_TYPE_OVERWORLD, SCENE_TYPE_OVERWORLD,
SCENE_TYPE_BATTLE,
SCENE_TYPE_COUNT SCENE_TYPE_COUNT
} scenetype_t; } scenetype_t;
+2
View File
@@ -11,3 +11,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(game) add_subdirectory(game)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(battle)
add_subdirectory(backpack)
@@ -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
)
+116
View File
@@ -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();
}
+65
View File
@@ -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);
+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
uibattlemenu.c
)
+137
View File
@@ -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();
}
+57
View File
@@ -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);
+31 -3
View File
@@ -8,13 +8,17 @@
#include "uigamemenu.h" #include "uigamemenu.h"
#include "ui/frame/uiframe.h" #include "ui/frame/uiframe.h"
#include "ui/frame/settings/uisettings.h" #include "ui/frame/settings/uisettings.h"
#include "ui/frame/backpack/uibackpack.h"
#include "util/memory.h" #include "util/memory.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h" #include "display/screen/screen.h"
#include "assert/assert.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_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; uigamemenu_t UI_GAME_MENU;
@@ -23,17 +27,41 @@ void uiGameMenuSelected(
const uint8_t index, const uint8_t index,
const uimenuitem_t *item const uimenuitem_t *item
) { ) {
if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen();
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen(); if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
} }
errorret_t uiGameMenuInit(void) { errorret_t uiGameMenuInit(void) {
memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t)); 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( MENU_BEGIN(
&UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL &UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL
); );
MENU_BUTTON("Characters"); MENU_BUTTON(UI_GAME_MENU.charactersLabel);
MENU_BUTTON("Settings"); MENU_BUTTON(UI_GAME_MENU.itemsLabel);
MENU_BUTTON(UI_GAME_MENU.settingsLabel);
MENU_END(UI_GAME_MENU.items, 1); MENU_END(UI_GAME_MENU.items, 1);
+5 -1
View File
@@ -9,12 +9,16 @@
#include "error/error.h" #include "error/error.h"
#include "ui/widget/uimenu.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_WIDTH 150.0f
#define UI_GAME_MENU_LABEL_MAX 32
typedef struct { typedef struct {
uimenu_t menu; uimenu_t menu;
uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT]; 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; } uigamemenu_t;
extern uigamemenu_t UI_GAME_MENU; extern uigamemenu_t UI_GAME_MENU;
+14
View File
@@ -18,6 +18,8 @@
#include "ui/debug/uiconsole.h" #include "ui/debug/uiconsole.h"
#include "ui/frame/settings/uisettings.h" #include "ui/frame/settings/uisettings.h"
#include "ui/frame/game/uigamemenu.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/frame/uiconfirm.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/uitextboxmain.h"
@@ -48,6 +50,18 @@ uielement_t UI_ELEMENTS[] = {
.dispose = uiSettingsDispose .dispose = uiSettingsDispose
}, },
{
.init = uiBattleMenuInit,
.update = uiBattleMenuUpdate,
.draw = uiBattleMenuDraw
},
{
.init = uiBackpackInit,
.draw = uiBackpackDraw,
.dispose = uiBackpackDispose
},
{ {
.init = uiConfirmInit, .init = uiConfirmInit,
.draw = uiConfirmDraw, .draw = uiConfirmDraw,
+3
View File
@@ -10,5 +10,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
uitab.c uitab.c
uislider.c uislider.c
uidropdown.c uidropdown.c
uiscrolling.c
uiitem.c
uiitemlist.c
uimenu.c uimenu.c
) )
+77
View File
@@ -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();
}
+89
View File
@@ -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
);
+179
View File
@@ -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;
}
+200
View File
@@ -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);
+15
View File
@@ -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.
}
+21
View File
@@ -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);
+7 -3
View File
@@ -20,8 +20,12 @@ rows = {}
with open(args.csv, newline="", encoding="utf-8") as f: with open(args.csv, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
if "id" not in reader.fieldnames or "type" not in reader.fieldnames: if (
raise ValueError("CSV must have 'id' and 'type' columns") "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: for row in reader:
item_id, item_type = row["id"], row["type"] item_id, item_type = row["id"], row["type"]
if item_id not in item_ids: if item_id not in item_ids:
@@ -85,7 +89,7 @@ for i in item_ids:
f" [{id_enum(i)}] = {{", f" [{id_enum(i)}] = {{",
f" .id = {id_enum(i)},", f" .id = {id_enum(i)},",
f" .type = {type_enum(row['type'])},", f" .type = {type_enum(row['type'])},",
f" .name = \"{i}\",", f" .name = \"item.{row['name']}.name\",",
" },", " },",
] ]
out += [ out += [