Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cfa8ddfeb | |||
| ef284a15a1 | |||
| 3723921573 | |||
| 195399635e | |||
| 46e2a924d3 | |||
| b693ea4102 |
@@ -43,3 +43,24 @@ msgstr "Apply"
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "リンゴ"
|
||||
|
||||
@@ -17,7 +17,7 @@ console_t CONSOLE;
|
||||
|
||||
void consoleInit(void) {
|
||||
memoryZero(&CONSOLE, sizeof(console_t));
|
||||
CONSOLE.visible = true;
|
||||
CONSOLE.visible = false;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexInit(&CONSOLE.printMutex);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -34,6 +34,22 @@ typedef struct cutscene_s {
|
||||
#define CUTSCENE_TEXT(TEXT) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { .text = TEXT } }
|
||||
|
||||
#define CUTSCENE_TEXT_MINI(TEXT, X, Y, Z, DURATION) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI, \
|
||||
.textMini = { \
|
||||
.text = TEXT, \
|
||||
.position = { X, Y, Z }, \
|
||||
.duration = DURATION \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_TEXT_MINI_HIDE(INDEX) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE, \
|
||||
.textMiniHide = { .index = INDEX } \
|
||||
}
|
||||
|
||||
#define CUTSCENE_WAIT(WAIT) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ void cutsceneSystemStartCutsceneWith(
|
||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
|
||||
cutsceneSystemNext();
|
||||
}
|
||||
@@ -65,6 +66,7 @@ void cutsceneSystemNext() {
|
||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,6 +132,18 @@ uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
|
||||
return areaId;
|
||||
}
|
||||
|
||||
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
|
||||
if(index == CUTSCENE_TEXT_MINI_LAST_CREATED) {
|
||||
assertTrue(
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated != CUTSCENE_TEXT_MINI_LAST_CREATED,
|
||||
"CUTSCENE_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
|
||||
"shown"
|
||||
);
|
||||
return CUTSCENE_SYSTEM.textMiniLastCreated;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
void cutsceneSystemDispose() {
|
||||
CUTSCENE_SYSTEM.scene = NULL;
|
||||
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
||||
@@ -139,4 +153,5 @@ void cutsceneSystemDispose() {
|
||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ typedef struct entity_s entity_t;
|
||||
#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
|
||||
#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
|
||||
#define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF)
|
||||
#define CUTSCENE_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA)
|
||||
|
||||
// Maximum number of bytes a running cutscene may request via
|
||||
// cutscene_t.dataSize.
|
||||
@@ -29,6 +30,7 @@ typedef struct {
|
||||
entity_t *entityLastCreated;
|
||||
entity_t *entityLastRef;
|
||||
uint8_t areaLastCreated;
|
||||
uint8_t textMiniLastCreated;
|
||||
|
||||
// Data (used by the current item).
|
||||
cutsceneitemdata_t data;
|
||||
@@ -86,6 +88,15 @@ entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
|
||||
*/
|
||||
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
|
||||
|
||||
/**
|
||||
* Resolves a raw mini textbox slot index (or CUTSCENE_TEXT_MINI_LAST_CREATED
|
||||
* sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index.
|
||||
*
|
||||
* @param index Raw slot index or sentinel value.
|
||||
* @returns The resolved slot index.
|
||||
*/
|
||||
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
|
||||
|
||||
/**
|
||||
* Advance to the next item in the cutscene.
|
||||
*/
|
||||
|
||||
@@ -14,3 +14,4 @@ add_subdirectory(entity)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(maparea)
|
||||
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
|
||||
);
|
||||
@@ -13,6 +13,14 @@ void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
|
||||
cutsceneTextStart(item, data);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_TEXT_MINI:
|
||||
cutsceneTextMiniStart(item, data);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE:
|
||||
cutsceneTextMiniHideStart(item, data);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_CALLBACK:
|
||||
cutsceneCallbackStart(item, data);
|
||||
break;
|
||||
@@ -77,6 +85,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;
|
||||
}
|
||||
@@ -87,6 +99,12 @@ bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data)
|
||||
case CUTSCENE_ITEM_TYPE_TEXT:
|
||||
return cutsceneTextUpdate(item, data);
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_TEXT_MINI:
|
||||
return cutsceneTextMiniUpdate(item, data);
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE:
|
||||
return cutsceneTextMiniHideUpdate(item, data);
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_CALLBACK:
|
||||
return cutsceneCallbackUpdate(item, data);
|
||||
|
||||
@@ -132,6 +150,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;
|
||||
}
|
||||
|
||||
@@ -17,17 +17,22 @@
|
||||
#include "entity/cutsceneentityturn.h"
|
||||
#include "entity/cutsceneentitywalktoentity.h"
|
||||
#include "ui/cutscenetext.h"
|
||||
#include "ui/cutscenetextmini.h"
|
||||
#include "ui/cutscenetextminihide.h"
|
||||
#include "ui/cutscenefade.h"
|
||||
#include "item/cutsceneitemgive.h"
|
||||
#include "maparea/cutscenemapareaadd.h"
|
||||
#include "maparea/cutscenemaparearemove.h"
|
||||
#include "maparea/cutscenemapareawait.h"
|
||||
#include "battle/cutscenestartbattle.h"
|
||||
|
||||
typedef struct cutscene_s cutscene_t;
|
||||
|
||||
typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_NULL,
|
||||
CUTSCENE_ITEM_TYPE_TEXT,
|
||||
CUTSCENE_ITEM_TYPE_TEXT_MINI,
|
||||
CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE,
|
||||
CUTSCENE_ITEM_TYPE_CALLBACK,
|
||||
CUTSCENE_ITEM_TYPE_WAIT,
|
||||
CUTSCENE_ITEM_TYPE_CUTSCENE,
|
||||
@@ -43,7 +48,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 {
|
||||
@@ -51,6 +57,8 @@ struct cutsceneitem_s {
|
||||
|
||||
union {
|
||||
cutscenetext_t text;
|
||||
cutscenetextmini_t textMini;
|
||||
cutscenetextminihide_t textMiniHide;
|
||||
cutscenecallback_t callback;
|
||||
cutscenewait_t wait;
|
||||
const cutscene_t *cutscene;
|
||||
@@ -67,6 +75,7 @@ struct cutsceneitem_s {
|
||||
cutscenemapareaadd_t mapAreaAdd;
|
||||
cutscenemaparearemove_t mapAreaRemove;
|
||||
cutscenemapareawait_t mapAreaWait;
|
||||
cutscenestartbattle_t startBattle;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/item/itemgive.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
|
||||
void cutsceneItemGiveStart(
|
||||
const cutsceneitem_t *item,
|
||||
|
||||
@@ -6,5 +6,7 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscenetext.c
|
||||
cutscenetextmini.c
|
||||
cutscenetextminihide.c
|
||||
cutscenefade.c
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
|
||||
void cutsceneTextStart(
|
||||
const cutsceneitem_t *item,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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/cutscene/cutscenesystem.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
|
||||
void cutsceneTextMiniStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
uint8_t index = uiTextboxMiniListGetNext();
|
||||
uiTextboxMiniShow(
|
||||
&UI_TEXTBOX_MINI_LIST[index],
|
||||
item->textMini.text,
|
||||
item->textMini.position,
|
||||
item->textMini.duration,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = index;
|
||||
}
|
||||
|
||||
bool_t cutsceneTextMiniUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
#define CUTSCENE_TEXT_MINI_MAX_CHARS 128
|
||||
|
||||
typedef struct {
|
||||
char_t text[CUTSCENE_TEXT_MINI_MAX_CHARS];
|
||||
vec3 position;
|
||||
float_t duration;
|
||||
} cutscenetextmini_t;
|
||||
|
||||
/**
|
||||
* Starts a mini text item (shows a mini textbox at the given world
|
||||
* position for the given duration, then completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneTextMiniStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a mini text item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneTextMiniUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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/cutscene/cutscenesystem.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
|
||||
void cutsceneTextMiniHideStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
uint8_t index = cutsceneSystemGetTextMiniId(item->textMiniHide.index);
|
||||
uiTextboxMiniClose(&UI_TEXTBOX_MINI_LIST[index]);
|
||||
}
|
||||
|
||||
bool_t cutsceneTextMiniHideUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t index;
|
||||
} cutscenetextminihide_t;
|
||||
|
||||
/**
|
||||
* Starts a mini text hide step (closes the mini textbox immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneTextMiniHideStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a mini text hide step (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneTextMiniHideUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -16,6 +16,7 @@ CUTSCENE(TEST_ONE, 0, DEFAULT,
|
||||
CUTSCENE(TEST_TWO, 0, DEFAULT,
|
||||
CUTSCENE_TEXT("Test Two."),
|
||||
CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0),
|
||||
CUTSCENE_TEXT_MINI("Hello!", 4, 4, 0, 3.0f),
|
||||
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_LAST_CREATED, 8, 2, 0),
|
||||
// CUTSCENE_CONCURRENT(
|
||||
// CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "assert/assert.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
|
||||
void entityInteractWith(entity_t *player, entity_t *target) {
|
||||
assertNotNull(player, "Player entity pointer cannot be NULL");
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "assert/assert.h"
|
||||
#include "rpg/item/itemgive.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
|
||||
void entityItemInit(entity_t *entity) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
id,type,weight
|
||||
POTION,MEDICINE,1.0
|
||||
POTATO,FOOD,0.5
|
||||
APPLE,FOOD,0.3
|
||||
id,type,weight,name
|
||||
POTION,MEDICINE,1.0,potion
|
||||
POTATO,FOOD,0.5,potato
|
||||
APPLE,FOOD,0.3,apple
|
||||
|
@@ -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,18 +7,25 @@
|
||||
|
||||
#include "itemgive.h"
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "rpg/item/item.h"
|
||||
#include "ui/rpg/textbox/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);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "rpg/cutscene/scene/testcutscene.h"
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "rpg/battle/party.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
#include "time/time.h"
|
||||
#include "rpgcamera.h"
|
||||
#include "util/memory.h"
|
||||
@@ -59,6 +60,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 },
|
||||
|
||||
@@ -56,6 +56,39 @@ void rpgCameraUpdateProjection(void) {
|
||||
);
|
||||
}
|
||||
|
||||
void rpgCameraUpdateEye(void) {
|
||||
float_t fov = glm_rad(RPG_CAMERA_FOV);
|
||||
float_t pixelsPerUnit = TILE_SIZE_PIXELS;
|
||||
float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit;
|
||||
float_t z = (worldH * 0.5f) / tanf(fov * 0.5f);
|
||||
float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS);
|
||||
|
||||
vec3 target;
|
||||
rpgCameraGetPosition(target);
|
||||
glm_vec3_add(target, (vec3){ 0.5f, 0.5f, 0.5f }, target);
|
||||
|
||||
glm_lookat(
|
||||
(vec3){ target[0], target[1] + offset, target[2] + z },
|
||||
target,
|
||||
(vec3){ 0, 1, 0 }, // up
|
||||
RPG_CAMERA.eye
|
||||
);
|
||||
}
|
||||
|
||||
void rpgCameraToScreen(vec3 worldPos, vec2 out) {
|
||||
mat4 viewProj;
|
||||
glm_mat4_mul(RPG_CAMERA.projection, RPG_CAMERA.eye, viewProj);
|
||||
|
||||
vec4 viewport = {
|
||||
0.0f, 0.0f, (float_t)SCREEN.width, (float_t)SCREEN.height
|
||||
};
|
||||
vec3 window;
|
||||
glm_project(worldPos, viewProj, viewport, window);
|
||||
|
||||
out[0] = window[0];
|
||||
out[1] = (float_t)SCREEN.height - window[1];
|
||||
}
|
||||
|
||||
errorret_t rpgCameraUpdate(void) {
|
||||
if(!mapIsLoaded()) errorOk();
|
||||
|
||||
|
||||
@@ -60,3 +60,20 @@ errorret_t rpgCameraUpdate(void);
|
||||
* is recomputed every call.
|
||||
*/
|
||||
void rpgCameraUpdateProjection(void);
|
||||
|
||||
/**
|
||||
* Recomputes the camera eye/view matrix from the camera's current mode
|
||||
* and position, and stores it in RPG_CAMERA.eye. Unlike the projection
|
||||
* matrix this is never cached, since the camera position can change
|
||||
* every frame.
|
||||
*/
|
||||
void rpgCameraUpdateEye(void);
|
||||
|
||||
/**
|
||||
* Converts a world-space position to screen-space pixel coordinates,
|
||||
* using the camera's current eye and projection matrices.
|
||||
*
|
||||
* @param worldPos The world-space position to convert.
|
||||
* @param out Output vec2 filled with the screen-space pixel position.
|
||||
*/
|
||||
void rpgCameraToScreen(vec3 worldPos, vec2 out);
|
||||
@@ -11,3 +11,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(overworld)
|
||||
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
|
||||
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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -24,6 +24,8 @@
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t sceneOverworldInit(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
errorOk();
|
||||
@@ -31,112 +33,159 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData) {
|
||||
|
||||
errorret_t sceneOverworldUpdate(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
|
||||
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldRender(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
sceneoverworld_t *overworld = &sceneData->overworld;
|
||||
sceneOverworldCullUpdate(overworld);
|
||||
|
||||
mat4 model, eye;
|
||||
|
||||
// Overworld camera
|
||||
errorChain(shaderBind(&SHADER_UNLIT));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
|
||||
));
|
||||
|
||||
// Model
|
||||
glm_mat4_identity(model);
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model));
|
||||
|
||||
// Camera projection
|
||||
rpgCameraUpdateProjection();
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, RPG_CAMERA.projection
|
||||
));
|
||||
|
||||
// Camera Eye
|
||||
float_t fov = glm_rad(RPG_CAMERA_FOV);
|
||||
float_t pixelsPerUnit = TILE_SIZE_PIXELS;
|
||||
float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit;
|
||||
float_t z = (worldH * 0.5f) / tanf(fov * 0.5f);
|
||||
vec3 worldPosVec;
|
||||
rpgCameraGetPosition(worldPosVec);
|
||||
float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS);
|
||||
rpgCameraUpdateEye();
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_VIEW, RPG_CAMERA.eye
|
||||
));
|
||||
|
||||
glm_vec3_add(worldPosVec, (vec3){ 0.5f, 0.5f, 0.5f }, worldPosVec);
|
||||
// Chunk tiles
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
errorChain(sceneOverworldDrawChunksBase(overworld));
|
||||
|
||||
glm_lookat(
|
||||
(vec3){
|
||||
worldPosVec[0],
|
||||
worldPosVec[1] + offset,
|
||||
worldPosVec[2] + z
|
||||
},
|
||||
worldPosVec,
|
||||
(vec3){ 0, 1, 0 }, // up
|
||||
eye
|
||||
);
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, eye));
|
||||
|
||||
// Base terrain meshes, drawn with normal depth testing.
|
||||
errorChain(sceneOverworldDrawChunksBase());
|
||||
|
||||
// Entities are drawn with depth testing fully disabled so sloped tiles
|
||||
// (ramps) never clip them; entity-vs-entity overlap falls back to
|
||||
// array draw order instead of true depth.
|
||||
// Entities
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
|
||||
// Entities
|
||||
{
|
||||
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
|
||||
entity_t *ent = &ENTITIES[i];
|
||||
if(ent->type == ENTITY_TYPE_NULL) continue;
|
||||
|
||||
spritebatchsprite_t sprite;
|
||||
glm_vec3_copy(ent->renderPosition, sprite.min);
|
||||
glm_vec3_add(sprite.min, (vec3){ 0, -0.05f, 0.05f }, sprite.min);// Stop Fight
|
||||
glm_vec3_copy(sprite.min, sprite.max);
|
||||
glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max);
|
||||
glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin);
|
||||
glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax);
|
||||
|
||||
color_t color;
|
||||
switch(ent->direction) {
|
||||
case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break;
|
||||
case ENTITY_DIR_EAST: color = COLOR_RED; break;
|
||||
case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break;
|
||||
case ENTITY_DIR_WEST: color = COLOR_BLUE; break;
|
||||
default: color = COLOR_CYAN; break;
|
||||
}
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = { .color = color, .texture = NULL }
|
||||
};
|
||||
spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material);
|
||||
spriteBatchFlush();
|
||||
}
|
||||
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
|
||||
entity_t *ent = &ENTITIES[i];
|
||||
if(ent->type == ENTITY_TYPE_NULL) continue;
|
||||
errorChain(sceneOverworldDrawEntity(overworld, ent));
|
||||
}
|
||||
|
||||
// Other chunk meshes (trees, buildings, etc), drawn last with normal
|
||||
// depth testing so they correctly occlude entities standing beneath
|
||||
// them.
|
||||
// Chunk props
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
errorChain(sceneOverworldDrawChunksProps());
|
||||
errorChain(sceneOverworldDrawChunksProps(overworld));
|
||||
|
||||
// Weather effects
|
||||
// errorChain(shaderSetMatrix(
|
||||
// &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj
|
||||
// ));
|
||||
// errorChain(shaderSetMatrix(
|
||||
// &SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView
|
||||
// ));
|
||||
// errorChain(displaySetState((displaystate_t){
|
||||
// .flags = DISPLAY_STATE_FLAG_BLEND
|
||||
// }));
|
||||
|
||||
// spritebatchsprite_t skyboxSprite;
|
||||
// memoryZero(&skyboxSprite, sizeof(spritebatchsprite_t));
|
||||
// skyboxSprite.max[0] = (float_t)(SCREEN.width / SCREEN.scaleUi);
|
||||
// skyboxSprite.max[1] = (float_t)(SCREEN.height / SCREEN.scaleUi);
|
||||
// shadermaterial_t skyboxMaterial = {
|
||||
// .unlit = { .color = color(0xFF, 0x00, 0x00, 0xFF/2), .texture = NULL }
|
||||
// };
|
||||
// errorChain(spriteBatchBuffer(&skyboxSprite, 1, &SHADER_UNLIT, skyboxMaterial));
|
||||
// errorChain(spriteBatchFlush());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldDrawChunksBase() {
|
||||
void sceneOverworldCullUpdate(sceneoverworld_t *overworld) {
|
||||
rpgCameraGetPosition(overworld->cullTarget);
|
||||
|
||||
const float_t worldH =
|
||||
(float_t)(SCREEN.height / SCREEN.scale3d) / TILE_SIZE_PIXELS;
|
||||
const float_t worldW = worldH * SCREEN.aspect;
|
||||
|
||||
overworld->cullHalfRangeX =
|
||||
(worldW * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_X;
|
||||
overworld->cullHalfRangeY =
|
||||
(worldH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y;
|
||||
}
|
||||
|
||||
bool_t sceneOverworldChunkShouldRender(
|
||||
const sceneoverworld_t *overworld,
|
||||
const chunk_t *chunk
|
||||
) {
|
||||
worldpos_t worldPos;
|
||||
chunkPosToWorldPos(&chunk->position, &worldPos);
|
||||
|
||||
const float_t chunkCenterX = (float_t)worldPos.x + (CHUNK_WIDTH * 0.5f);
|
||||
const float_t chunkCenterY = (float_t)worldPos.y + (CHUNK_HEIGHT * 0.5f);
|
||||
|
||||
if(fabsf(overworld->cullTarget[0] - chunkCenterX) >
|
||||
overworld->cullHalfRangeX) return false;
|
||||
if(fabsf(overworld->cullTarget[1] - chunkCenterY) >
|
||||
overworld->cullHalfRangeY) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t sceneOverworldEntityShouldRender(
|
||||
const sceneoverworld_t *overworld,
|
||||
const entity_t *ent
|
||||
) {
|
||||
if(fabsf(overworld->cullTarget[0] - ent->renderPosition[0]) >
|
||||
overworld->cullHalfRangeX) return false;
|
||||
if(fabsf(overworld->cullTarget[1] - ent->renderPosition[1]) >
|
||||
overworld->cullHalfRangeY) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldDrawEntity(
|
||||
const sceneoverworld_t *overworld,
|
||||
entity_t *ent
|
||||
) {
|
||||
if(!sceneOverworldEntityShouldRender(overworld, ent)) errorOk();
|
||||
|
||||
spritebatchsprite_t sprite;
|
||||
glm_vec3_copy(ent->renderPosition, sprite.min);
|
||||
glm_vec3_add(sprite.min, (vec3){ 0, -0.05f, 0.05f }, sprite.min);// Stop Fight
|
||||
glm_vec3_copy(sprite.min, sprite.max);
|
||||
glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max);
|
||||
glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin);
|
||||
glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax);
|
||||
|
||||
color_t color;
|
||||
switch(ent->direction) {
|
||||
case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break;
|
||||
case ENTITY_DIR_EAST: color = COLOR_RED; break;
|
||||
case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break;
|
||||
case ENTITY_DIR_WEST: color = COLOR_BLUE; break;
|
||||
default: color = COLOR_CYAN; break;
|
||||
}
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = { .color = color, .texture = NULL }
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = MAP.chunkOrder[i];
|
||||
if(chunk == NULL) continue;
|
||||
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
|
||||
if(chunk->meshCount == 0) continue;
|
||||
if(chunk->modelEntries[0] == NULL) continue;
|
||||
if(chunk->modelEntries[0]->state != ASSET_ENTRY_STATE_LOADED) continue;
|
||||
@@ -169,10 +218,11 @@ errorret_t sceneOverworldDrawChunksBase() {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldDrawChunksProps() {
|
||||
errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) {
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = MAP.chunkOrder[i];
|
||||
if(chunk == NULL) continue;
|
||||
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
|
||||
|
||||
for(uint8_t m = 1; m < chunk->meshCount; m++) {
|
||||
if(chunk->modelEntries[m] == NULL) continue;
|
||||
|
||||
@@ -7,11 +7,35 @@
|
||||
|
||||
#pragma once
|
||||
#include "scene/scenebase.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
// Cached per-frame chunk culling bounds, refreshed by
|
||||
// sceneOverworldCullUpdate() so sceneOverworldChunkShouldRender()
|
||||
// does not need to recompute them for every chunk.
|
||||
vec3 cullTarget;
|
||||
float_t cullHalfRangeX;
|
||||
float_t cullHalfRangeY;
|
||||
} sceneoverworld_t;
|
||||
|
||||
// Extra world-space margin, in tiles, added on top of the rough
|
||||
// screen-size skin below - cheap insurance against the approximation
|
||||
// culling a chunk that is actually still just in view. Y gets its own
|
||||
// value since the camera's tilt makes the vertical check less exact.
|
||||
#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_X 8.0f
|
||||
#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y 4.0f
|
||||
|
||||
// The camera is tilted between the ground-depth (Y) and height (Z)
|
||||
// axes, so a chunk's on-screen vertical extent also depends on its Z
|
||||
// layers. Rather than projecting every corner, fold that into a
|
||||
// generous skin margin on the depth check.
|
||||
#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_X \
|
||||
((CHUNK_WIDTH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_PADDING_X)
|
||||
#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y \
|
||||
((CHUNK_HEIGHT * 0.5f) + (CHUNK_DEPTH * WORLD_LAYER_HEIGHT) + \
|
||||
SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y)
|
||||
|
||||
/**
|
||||
* Initialises the overworld scene.
|
||||
*
|
||||
@@ -28,22 +52,85 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData);
|
||||
*/
|
||||
errorret_t sceneOverworldUpdate(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Refreshes the chunk culling bounds cached on the overworld scene data,
|
||||
* from the current camera position and screen dimensions. Must be
|
||||
* called once per frame before sceneOverworldChunkShouldRender().
|
||||
*
|
||||
* @param overworld The overworld scene data to update.
|
||||
*/
|
||||
void sceneOverworldCullUpdate(sceneoverworld_t *overworld);
|
||||
|
||||
/**
|
||||
* Roughly checks whether a chunk falls within the visible screen area,
|
||||
* based on chunk size and screen dimensions. Cheap approximation, not
|
||||
* an exact frustum check - includes a generous skin margin to account
|
||||
* for the camera's angle tilting height/depth onto the screen's
|
||||
* vertical axis, so it may pass some chunks that are actually just out
|
||||
* of view but will never wrongly cull one that is visible.
|
||||
*
|
||||
* @param overworld The overworld scene data holding the cached culling
|
||||
* bounds, as refreshed by sceneOverworldCullUpdate().
|
||||
* @param chunk The chunk to check.
|
||||
* @return true if the chunk should be rendered, false otherwise.
|
||||
*/
|
||||
bool_t sceneOverworldChunkShouldRender(
|
||||
const sceneoverworld_t *overworld,
|
||||
const chunk_t *chunk
|
||||
);
|
||||
|
||||
/**
|
||||
* Draws the base (tile) mesh of every loaded chunk, with normal depth
|
||||
* testing. Must be called before entities are rendered.
|
||||
*
|
||||
* @param overworld The overworld scene data holding the cached culling
|
||||
* bounds, as refreshed by sceneOverworldCullUpdate().
|
||||
* @return An error if drawing failed, or errorOk() on success.
|
||||
*/
|
||||
errorret_t sceneOverworldDrawChunksBase();
|
||||
errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld);
|
||||
|
||||
/**
|
||||
* Draws every loaded chunk's additional meshes (trees, buildings, etc),
|
||||
* with normal depth testing so they correctly occlude entities standing
|
||||
* beneath them. Must be called after entities are rendered.
|
||||
*
|
||||
* @param overworld The overworld scene data holding the cached culling
|
||||
* bounds, as refreshed by sceneOverworldCullUpdate().
|
||||
* @return An error if drawing failed, or errorOk() on success.
|
||||
*/
|
||||
errorret_t sceneOverworldDrawChunksProps();
|
||||
errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld);
|
||||
|
||||
/**
|
||||
* Roughly checks whether an entity falls within the visible screen
|
||||
* area. Reuses the same cached chunk culling bounds, since an entity
|
||||
* is just a point within that same space - deliberately cheap, a
|
||||
* couple of subtractions and comparisons, since it runs per entity
|
||||
* every frame and it is not worth spending more math than that to
|
||||
* avoid drawing one that is slightly off-screen.
|
||||
*
|
||||
* @param overworld The overworld scene data holding the cached culling
|
||||
* bounds, as refreshed by sceneOverworldCullUpdate().
|
||||
* @param ent The entity to check.
|
||||
* @return true if the entity should be rendered, false otherwise.
|
||||
*/
|
||||
bool_t sceneOverworldEntityShouldRender(
|
||||
const sceneoverworld_t *overworld,
|
||||
const entity_t *ent
|
||||
);
|
||||
|
||||
/**
|
||||
* Draws a single entity as a sprite, colored by its facing direction.
|
||||
* Skips drawing (without error) if the entity should not render.
|
||||
*
|
||||
* @param overworld The overworld scene data holding the cached culling
|
||||
* bounds, as refreshed by sceneOverworldCullUpdate().
|
||||
* @param ent The entity to draw.
|
||||
* @return An error if drawing failed, or errorOk() on success.
|
||||
*/
|
||||
errorret_t sceneOverworldDrawEntity(
|
||||
const sceneoverworld_t *overworld,
|
||||
entity_t *ent
|
||||
);
|
||||
|
||||
/**
|
||||
* Renders the overworld scene.
|
||||
|
||||
+26
-19
@@ -58,6 +58,23 @@ errorret_t sceneUpdate(void) {
|
||||
}
|
||||
|
||||
errorret_t sceneRender(void) {
|
||||
// Setup screen matrices for 3D rendering.
|
||||
glm_mat4_identity(SCENE.screenIdentity);
|
||||
|
||||
glm_ortho(
|
||||
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi),
|
||||
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f,
|
||||
0.1f, 100.0f,
|
||||
SCENE.screenProj
|
||||
);
|
||||
|
||||
glm_lookat(
|
||||
(vec3){ 0.0f, 0.0f, 1.0f },
|
||||
(vec3){ 0.0f, 0.0f, 0.0f },
|
||||
(vec3){ 0.0f, 1.0f, 0.0f },
|
||||
SCENE.screenView
|
||||
);
|
||||
|
||||
// Scene rendering
|
||||
if(
|
||||
SCENE.current != SCENE_TYPE_NULL &&
|
||||
@@ -67,26 +84,16 @@ errorret_t sceneRender(void) {
|
||||
}
|
||||
|
||||
// UI Rendering
|
||||
mat4 proj, view, ident;
|
||||
glm_mat4_identity(ident);
|
||||
errorChain(shaderBind(&SHADER_UNLIT));
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, ident));
|
||||
|
||||
glm_ortho(
|
||||
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi),
|
||||
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f,
|
||||
0.1f, 100.0f,
|
||||
proj
|
||||
);
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj));
|
||||
|
||||
glm_lookat(
|
||||
(vec3){ 0.0f, 0.0f, 1.0f },
|
||||
(vec3){ 0.0f, 0.0f, 0.0f },
|
||||
(vec3){ 0.0f, 1.0f, 0.0f },
|
||||
view
|
||||
);
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
|
||||
));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj
|
||||
));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView
|
||||
));
|
||||
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_BLEND
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "scenetype.h"
|
||||
|
||||
typedef struct {
|
||||
scenetype_t current;
|
||||
scenetype_t next;
|
||||
scenedata_t data;
|
||||
mat4 screenProj;
|
||||
mat4 screenView;
|
||||
mat4 screenIdentity;
|
||||
} scene_t;
|
||||
|
||||
extern scene_t SCENE;
|
||||
|
||||
@@ -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
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,3 +11,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
|
||||
add_subdirectory(game)
|
||||
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
|
||||
)
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
)
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
#define UI_FRAME_BORDER_WIDTH 6
|
||||
#define UI_FRAME_BORDER_HEIGHT 6
|
||||
#define UI_FRAME_PADDING_X 4
|
||||
#define UI_FRAME_PADDING_Y 4
|
||||
#define UI_FRAME_PADDING_X 2
|
||||
#define UI_FRAME_PADDING_Y 2
|
||||
#define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X)
|
||||
#define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y)
|
||||
#define UI_FRAME_TILE_WIDTH 1
|
||||
|
||||
@@ -3,8 +3,4 @@
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uitextbox.c
|
||||
uitextboxmain.c
|
||||
)
|
||||
add_subdirectory(textbox)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# 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
|
||||
uitextbox.c
|
||||
uitextboxmain.c
|
||||
uitextboxmini.c
|
||||
uitextboxminilist.c
|
||||
)
|
||||
@@ -16,15 +16,29 @@
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
void uiTextboxInit(uitextbox_t *box) {
|
||||
void uiTextboxInit(
|
||||
uitextbox_t *box,
|
||||
char_t *text,
|
||||
const uint32_t maxLength,
|
||||
uitextboxline_t *lines,
|
||||
const uint32_t linesMax
|
||||
) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
assertNotNull(text, "Text buffer cannot be NULL");
|
||||
assertTrue(maxLength >= 1, "maxLength must be at least 1");
|
||||
assertNotNull(lines, "Lines buffer cannot be NULL");
|
||||
assertTrue(linesMax >= 1, "linesMax must be at least 1");
|
||||
memoryZero(box, sizeof(uitextbox_t));
|
||||
box->text = text;
|
||||
box->maxLength = maxLength;
|
||||
box->lines = lines;
|
||||
box->linesMax = linesMax;
|
||||
}
|
||||
|
||||
void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
stringCopy(box->text, text, UI_TEXTBOX_TEXT_MAX);
|
||||
stringCopy(box->text, text, box->maxLength);
|
||||
box->currentPage = 0;
|
||||
box->scroll = 0;
|
||||
box->layoutWidth = 0.0f;
|
||||
@@ -60,12 +74,12 @@ void uiTextboxBuildLayout(
|
||||
char_t *src = box->text;
|
||||
int32_t i = 0;
|
||||
|
||||
while(src[i] != '\0' && box->lineCount < UI_TEXTBOX_LINES_MAX) {
|
||||
while(src[i] != '\0' && box->lineCount < (int32_t)box->linesMax) {
|
||||
if(src[i] == '\t') {
|
||||
i++;
|
||||
int32_t rem = box->lineCount % box->linesPerPage;
|
||||
int32_t pad = rem > 0 ? box->linesPerPage - rem : 0;
|
||||
while(pad > 0 && box->lineCount < UI_TEXTBOX_LINES_MAX) {
|
||||
while(pad > 0 && box->lineCount < (int32_t)box->linesMax) {
|
||||
box->lines[box->lineCount].start = i;
|
||||
box->lines[box->lineCount].count = 0;
|
||||
box->lineCount++;
|
||||
@@ -196,18 +210,6 @@ errorret_t uiTextboxDraw(
|
||||
charsLeft -= visible;
|
||||
}
|
||||
|
||||
if(uiTextboxPageIsComplete(box)) {
|
||||
spritebatchsprite_t caret = textGetSprite(
|
||||
(vec2){
|
||||
contentX + contentW - fontW,
|
||||
contentY + contentH - fontH
|
||||
},
|
||||
'v',
|
||||
&FONT_DEFAULT
|
||||
);
|
||||
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
|
||||
}
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
@@ -8,8 +8,6 @@
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
#define UI_TEXTBOX_TEXT_MAX 1024
|
||||
#define UI_TEXTBOX_LINES_MAX 64
|
||||
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
|
||||
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
|
||||
#define UI_TEXTBOX_LINE_SPACING 0.0f
|
||||
@@ -20,9 +18,11 @@ typedef struct {
|
||||
} uitextboxline_t;
|
||||
|
||||
typedef struct {
|
||||
char_t text[UI_TEXTBOX_TEXT_MAX];
|
||||
char_t *text;
|
||||
uint32_t maxLength;
|
||||
|
||||
uitextboxline_t lines[UI_TEXTBOX_LINES_MAX];
|
||||
uitextboxline_t *lines;
|
||||
uint32_t linesMax;
|
||||
int32_t lineCount;
|
||||
int32_t charsPerLine;
|
||||
int32_t linesPerPage;
|
||||
@@ -37,11 +37,22 @@ typedef struct {
|
||||
} uitextbox_t;
|
||||
|
||||
/**
|
||||
* Initializes a textbox, zeroing all state.
|
||||
* Initializes a textbox, zeroing all state and binding it to caller-owned
|
||||
* text and line storage.
|
||||
*
|
||||
* @param box The textbox to initialize.
|
||||
* @param text Caller-owned buffer the textbox copies its text into.
|
||||
* @param maxLength Capacity of text, in characters.
|
||||
* @param lines Caller-owned buffer the textbox lays lines out into.
|
||||
* @param linesMax Capacity of lines, in entries.
|
||||
*/
|
||||
void uiTextboxInit(uitextbox_t *box);
|
||||
void uiTextboxInit(
|
||||
uitextbox_t *box,
|
||||
char_t *text,
|
||||
const uint32_t maxLength,
|
||||
uitextboxline_t *lines,
|
||||
const uint32_t linesMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Copies text into the textbox and marks layout as dirty.
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uitextboxmain.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
uitextboxmain_t UI_TEXTBOX_MAIN;
|
||||
static uifocusitem_t *focusItem = NULL;
|
||||
|
||||
errorret_t uiTextboxMainInit(void) {
|
||||
uiTextboxInit(
|
||||
&UI_TEXTBOX_MAIN.box,
|
||||
UI_TEXTBOX_MAIN.text, UI_TEXTBOX_MAIN_TEXT_MAX,
|
||||
UI_TEXTBOX_MAIN.lines, UI_TEXTBOX_MAIN_LINES_MAX
|
||||
);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiTextboxMainSetText(const char_t *text) {
|
||||
uiTextboxSetText(&UI_TEXTBOX_MAIN.box, text);
|
||||
if(focusItem != NULL) return;
|
||||
focusItem = uiFocusPush(
|
||||
1, 1,
|
||||
uiTextboxMainFocusSelected,
|
||||
NULL,
|
||||
uiTextboxMainFocusClosed,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMainUpdate(void) {
|
||||
if(focusItem == NULL) errorOk();
|
||||
return uiTextboxUpdate(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMainDraw(void) {
|
||||
if(focusItem == NULL) errorOk();
|
||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH +
|
||||
(float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING +
|
||||
2.0f * (float_t)UI_FRAME_START_Y;
|
||||
float_t w = (float_t)SCREEN.scanWidth;
|
||||
float_t x = (float_t)SCREEN.scanX;
|
||||
float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h;
|
||||
errorChain(uiTextboxDraw(&UI_TEXTBOX_MAIN.box, x, y, w, h));
|
||||
|
||||
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) errorOk();
|
||||
|
||||
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
||||
float_t contentX = x + (float_t)UI_FRAME_START_X;
|
||||
float_t contentY = y + (float_t)UI_FRAME_START_Y;
|
||||
float_t contentW = w - 2.0f * (float_t)UI_FRAME_START_X;
|
||||
float_t contentH = h - 2.0f * (float_t)UI_FRAME_START_Y;
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = COLOR_WHITE,
|
||||
.texture = FONT_DEFAULT.texture
|
||||
}
|
||||
};
|
||||
|
||||
spritebatchsprite_t caret = textGetSprite(
|
||||
(vec2){
|
||||
contentX + contentW - fontW,
|
||||
contentY + contentH - fontH
|
||||
},
|
||||
'v',
|
||||
&FONT_DEFAULT
|
||||
);
|
||||
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainPageIsComplete(void) {
|
||||
return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainHasNextPage(void) {
|
||||
return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
void uiTextboxMainNextPage(void) {
|
||||
uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainIsActive(void) {
|
||||
return focusItem != NULL;
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) {
|
||||
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) {
|
||||
UI_TEXTBOX_MAIN.box.scroll =
|
||||
uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN.box);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box)) {
|
||||
uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
return true;
|
||||
}
|
||||
|
||||
uiFocusPopItem(focusItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) {
|
||||
focusItem = NULL;
|
||||
return true;
|
||||
}
|
||||
@@ -6,12 +6,20 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "ui/rpg/uitextbox.h"
|
||||
#include "ui/rpg/textbox/uitextbox.h"
|
||||
#include "ui/focus/uifocusitem.h"
|
||||
|
||||
#define UI_TEXTBOX_MAIN_LINES 4
|
||||
#define UI_TEXTBOX_MAIN_TEXT_MAX 1024
|
||||
#define UI_TEXTBOX_MAIN_LINES_MAX 64
|
||||
|
||||
extern uitextbox_t UI_TEXTBOX_MAIN;
|
||||
typedef struct {
|
||||
uitextbox_t box;
|
||||
char_t text[UI_TEXTBOX_MAIN_TEXT_MAX];
|
||||
uitextboxline_t lines[UI_TEXTBOX_MAIN_LINES_MAX];
|
||||
} uitextboxmain_t;
|
||||
|
||||
extern uitextboxmain_t UI_TEXTBOX_MAIN;
|
||||
|
||||
/**
|
||||
* Initializes UI_TEXTBOX_MAIN.
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uitextboxmini.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "time/time.h"
|
||||
#include "rpg/rpgcamera.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
void uiTextboxMiniInit(uitextboxmini_t *mini) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
memoryZero(mini, sizeof(uitextboxmini_t));
|
||||
uiTextboxInit(
|
||||
&mini->box,
|
||||
mini->text, UI_TEXTBOX_MINI_TEXT_MAX,
|
||||
mini->lines, UI_TEXTBOX_MINI_LINES_MAX
|
||||
);
|
||||
}
|
||||
|
||||
void uiTextboxMiniShow(
|
||||
uitextboxmini_t *mini,
|
||||
const char_t *text,
|
||||
vec3 position,
|
||||
const float_t duration,
|
||||
uitextboxminiclosedcallback_t closed,
|
||||
void *user
|
||||
) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
uiTextboxSetText(&mini->box, text);
|
||||
glm_vec3_copy(position, mini->position);
|
||||
|
||||
int32_t textWidth, textHeight;
|
||||
textMeasure(text, &FONT_DEFAULT, &textWidth, &textHeight);
|
||||
float_t width = (float_t)textWidth + 2.0f * (float_t)UI_FRAME_START_X;
|
||||
mini->width = width < UI_TEXTBOX_MINI_WIDTH_MAX
|
||||
? width : UI_TEXTBOX_MINI_WIDTH_MAX;
|
||||
|
||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
float_t maxHeight = (float_t)UI_TEXTBOX_MINI_LINES_MAX * fontH +
|
||||
(float_t)(UI_TEXTBOX_MINI_LINES_MAX - 1) * UI_TEXTBOX_LINE_SPACING +
|
||||
2.0f * (float_t)UI_FRAME_START_Y;
|
||||
uiTextboxBuildLayout(
|
||||
&mini->box,
|
||||
mini->width - 2.0f * (float_t)UI_FRAME_START_X,
|
||||
maxHeight - 2.0f * (float_t)UI_FRAME_START_Y
|
||||
);
|
||||
int32_t lineCount = mini->box.lineCount > 0 ? mini->box.lineCount : 1;
|
||||
mini->height = (float_t)lineCount * fontH +
|
||||
(float_t)(lineCount - 1) * UI_TEXTBOX_LINE_SPACING +
|
||||
2.0f * (float_t)UI_FRAME_START_Y;
|
||||
|
||||
mini->active = true;
|
||||
mini->timer = duration;
|
||||
mini->closed = closed;
|
||||
mini->user = user;
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
if(!mini->active) errorOk();
|
||||
|
||||
errorChain(uiTextboxUpdate(&mini->box));
|
||||
|
||||
if(mini->timer != UI_TEXTBOX_MINI_DURATION_INFINITE) {
|
||||
mini->timer -= TIME.delta;
|
||||
if(mini->timer <= 0.0f) uiTextboxMiniClose(mini);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
if(!mini->active) errorOk();
|
||||
|
||||
vec2 screenPos;
|
||||
rpgCameraToScreen(mini->position, screenPos);
|
||||
|
||||
if(
|
||||
mini->clamp == UI_TEXTBOX_MINI_CLAMP_X ||
|
||||
mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH
|
||||
) {
|
||||
float_t minX = (float_t)SCREEN.scanX;
|
||||
float_t maxX = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - mini->width;
|
||||
if(screenPos[0] < minX) screenPos[0] = minX;
|
||||
if(screenPos[0] > maxX) screenPos[0] = maxX;
|
||||
}
|
||||
|
||||
if(
|
||||
mini->clamp == UI_TEXTBOX_MINI_CLAMP_Y ||
|
||||
mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH
|
||||
) {
|
||||
float_t minY = (float_t)SCREEN.scanY;
|
||||
float_t maxY = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - mini->height;
|
||||
if(screenPos[1] < minY) screenPos[1] = minY;
|
||||
if(screenPos[1] > maxY) screenPos[1] = maxY;
|
||||
}
|
||||
|
||||
return uiTextboxDraw(
|
||||
&mini->box, screenPos[0], screenPos[1], mini->width, mini->height
|
||||
);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
return mini->active;
|
||||
}
|
||||
|
||||
void uiTextboxMiniClose(uitextboxmini_t *mini) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
if(!mini->active) return;
|
||||
mini->active = false;
|
||||
if(mini->closed != NULL) mini->closed(mini);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "ui/rpg/textbox/uitextbox.h"
|
||||
|
||||
#define UI_TEXTBOX_MINI_TEXT_MAX 128
|
||||
#define UI_TEXTBOX_MINI_LINES_MAX 2
|
||||
#define UI_TEXTBOX_MINI_WIDTH_MAX 160.0f
|
||||
|
||||
// Magic duration value that keeps a mini textbox visible indefinitely
|
||||
// instead of counting down and auto-closing.
|
||||
#define UI_TEXTBOX_MINI_DURATION_INFINITE -1.0f
|
||||
|
||||
typedef struct uitextboxmini_s uitextboxmini_t;
|
||||
typedef void (*uitextboxminiclosedcallback_t)(const uitextboxmini_t *mini);
|
||||
|
||||
/**
|
||||
* Determines whether a mini textbox's screen position is clamped to stay
|
||||
* fully on screen. Defaults to UI_TEXTBOX_MINI_CLAMP_NONE.
|
||||
*/
|
||||
typedef enum {
|
||||
UI_TEXTBOX_MINI_CLAMP_NONE,
|
||||
UI_TEXTBOX_MINI_CLAMP_X,
|
||||
UI_TEXTBOX_MINI_CLAMP_Y,
|
||||
UI_TEXTBOX_MINI_CLAMP_BOTH
|
||||
} uitextboxminiclamp_t;
|
||||
|
||||
typedef struct uitextboxmini_s {
|
||||
uitextbox_t box;
|
||||
char_t text[UI_TEXTBOX_MINI_TEXT_MAX];
|
||||
uitextboxline_t lines[UI_TEXTBOX_MINI_LINES_MAX];
|
||||
|
||||
vec3 position;
|
||||
float_t width;
|
||||
float_t height;
|
||||
uitextboxminiclamp_t clamp;
|
||||
bool_t active;
|
||||
float_t timer;
|
||||
|
||||
uitextboxminiclosedcallback_t closed;
|
||||
void *user;
|
||||
} uitextboxmini_t;
|
||||
|
||||
/**
|
||||
* Initializes a mini textbox, zeroing all state and binding its internal
|
||||
* uitextbox_t to its own fixed-size text and line storage.
|
||||
*
|
||||
* @param mini The mini textbox to initialize.
|
||||
*/
|
||||
void uiTextboxMiniInit(uitextboxmini_t *mini);
|
||||
|
||||
/**
|
||||
* Shows a mini textbox with the given text at the given world position for
|
||||
* the given duration. Resets the typewriter scroll, measures the text to
|
||||
* size the box (width capped at UI_TEXTBOX_MINI_WIDTH_MAX, height derived
|
||||
* from the resulting wrapped line count), and starts the visibility timer.
|
||||
*
|
||||
* @param mini The mini textbox to show.
|
||||
* @param text Null-terminated source string.
|
||||
* @param position World-space position the box is anchored to on screen.
|
||||
* @param duration How long the mini textbox stays visible, in seconds, or
|
||||
* UI_TEXTBOX_MINI_DURATION_INFINITE to never auto-close.
|
||||
* @param closed Called once the timer elapses and the box closes. May be
|
||||
* NULL.
|
||||
* @param user Opaque pointer passed back through the closed callback.
|
||||
*/
|
||||
void uiTextboxMiniShow(
|
||||
uitextboxmini_t *mini,
|
||||
const char_t *text,
|
||||
vec3 position,
|
||||
const float_t duration,
|
||||
uitextboxminiclosedcallback_t closed,
|
||||
void *user
|
||||
);
|
||||
|
||||
/**
|
||||
* Advances the typewriter scroll and counts down the visibility timer.
|
||||
* Closes the mini textbox and fires its closed callback once the timer
|
||||
* elapses. Has no effect if not active or if the duration was set to
|
||||
* UI_TEXTBOX_MINI_DURATION_INFINITE.
|
||||
*
|
||||
* @param mini The mini textbox to update.
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini);
|
||||
|
||||
/**
|
||||
* Draws the mini textbox at its text-measured size, anchored on screen to
|
||||
* its world-space position via the RPG camera. Has no effect if not
|
||||
* active.
|
||||
*
|
||||
* @param mini The mini textbox to draw.
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini);
|
||||
|
||||
/**
|
||||
* Returns true when the mini textbox is currently visible.
|
||||
*
|
||||
* @param mini The mini textbox to query.
|
||||
* @returns True if active.
|
||||
*/
|
||||
bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini);
|
||||
|
||||
/**
|
||||
* Immediately closes the mini textbox and fires its closed callback.
|
||||
* Has no effect if not active.
|
||||
*
|
||||
* @param mini The mini textbox to close.
|
||||
*/
|
||||
void uiTextboxMiniClose(uitextboxmini_t *mini);
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uitextboxminilist.h"
|
||||
|
||||
uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT];
|
||||
uint8_t UI_TEXTBOX_MINI_LIST_NEXT;
|
||||
|
||||
errorret_t uiTextboxMiniListInit(void) {
|
||||
for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
|
||||
uiTextboxMiniInit(&UI_TEXTBOX_MINI_LIST[i]);
|
||||
}
|
||||
UI_TEXTBOX_MINI_LIST_NEXT = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t uiTextboxMiniListGetNext(void) {
|
||||
uint8_t index = UI_TEXTBOX_MINI_LIST_NEXT;
|
||||
UI_TEXTBOX_MINI_LIST_NEXT =
|
||||
(UI_TEXTBOX_MINI_LIST_NEXT + 1) % UI_TEXTBOX_MINI_LIST_COUNT;
|
||||
return index;
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMiniListUpdate(void) {
|
||||
for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
|
||||
errorChain(uiTextboxMiniUpdate(&UI_TEXTBOX_MINI_LIST[i]));
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMiniListDraw(void) {
|
||||
for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
|
||||
errorChain(uiTextboxMiniDraw(&UI_TEXTBOX_MINI_LIST[i]));
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "ui/rpg/textbox/uitextboxmini.h"
|
||||
|
||||
#define UI_TEXTBOX_MINI_LIST_COUNT 4
|
||||
|
||||
extern uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT];
|
||||
extern uint8_t UI_TEXTBOX_MINI_LIST_NEXT;
|
||||
|
||||
/**
|
||||
* Initializes all UI_TEXTBOX_MINI_LIST slots and resets
|
||||
* UI_TEXTBOX_MINI_LIST_NEXT.
|
||||
*
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMiniListInit(void);
|
||||
|
||||
/**
|
||||
* Returns the index of the next UI_TEXTBOX_MINI_LIST slot to use, cycling
|
||||
* through all slots in round-robin order via UI_TEXTBOX_MINI_LIST_NEXT.
|
||||
*
|
||||
* @returns The next slot index.
|
||||
*/
|
||||
uint8_t uiTextboxMiniListGetNext(void);
|
||||
|
||||
/**
|
||||
* Updates all UI_TEXTBOX_MINI_LIST slots.
|
||||
*
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMiniListUpdate(void);
|
||||
|
||||
/**
|
||||
* Draws all active UI_TEXTBOX_MINI_LIST slots.
|
||||
*
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMiniListDraw(void);
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uitextboxmain.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
uitextbox_t UI_TEXTBOX_MAIN;
|
||||
static uifocusitem_t *focusItem = NULL;
|
||||
|
||||
errorret_t uiTextboxMainInit(void) {
|
||||
uiTextboxInit(&UI_TEXTBOX_MAIN);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiTextboxMainSetText(const char_t *text) {
|
||||
uiTextboxSetText(&UI_TEXTBOX_MAIN, text);
|
||||
if(focusItem != NULL) return;
|
||||
focusItem = uiFocusPush(
|
||||
1, 1,
|
||||
uiTextboxMainFocusSelected,
|
||||
NULL,
|
||||
uiTextboxMainFocusClosed,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMainUpdate(void) {
|
||||
if(focusItem == NULL) errorOk();
|
||||
return uiTextboxUpdate(&UI_TEXTBOX_MAIN);
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMainDraw(void) {
|
||||
if(focusItem == NULL) errorOk();
|
||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH +
|
||||
(float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING +
|
||||
2.0f * (float_t)UI_FRAME_START_Y;
|
||||
float_t w = (float_t)SCREEN.scanWidth;
|
||||
float_t x = (float_t)SCREEN.scanX;
|
||||
float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h;
|
||||
return uiTextboxDraw(&UI_TEXTBOX_MAIN, x, y, w, h);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainPageIsComplete(void) {
|
||||
return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainHasNextPage(void) {
|
||||
return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN);
|
||||
}
|
||||
|
||||
void uiTextboxMainNextPage(void) {
|
||||
uiTextboxNextPage(&UI_TEXTBOX_MAIN);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainIsActive(void) {
|
||||
return focusItem != NULL;
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) {
|
||||
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN)) {
|
||||
UI_TEXTBOX_MAIN.scroll = uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN)) {
|
||||
uiTextboxNextPage(&UI_TEXTBOX_MAIN);
|
||||
return true;
|
||||
}
|
||||
|
||||
uiFocusPopItem(focusItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) {
|
||||
focusItem = NULL;
|
||||
return true;
|
||||
}
|
||||
+22
-1
@@ -18,8 +18,11 @@
|
||||
#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"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
|
||||
uielement_t UI_ELEMENTS[] = {
|
||||
{
|
||||
@@ -48,6 +51,18 @@ uielement_t UI_ELEMENTS[] = {
|
||||
.dispose = uiSettingsDispose
|
||||
},
|
||||
|
||||
{
|
||||
.init = uiBattleMenuInit,
|
||||
.update = uiBattleMenuUpdate,
|
||||
.draw = uiBattleMenuDraw
|
||||
},
|
||||
|
||||
{
|
||||
.init = uiBackpackInit,
|
||||
.draw = uiBackpackDraw,
|
||||
.dispose = uiBackpackDispose
|
||||
},
|
||||
|
||||
{
|
||||
.init = uiConfirmInit,
|
||||
.draw = uiConfirmDraw,
|
||||
@@ -60,6 +75,12 @@ uielement_t UI_ELEMENTS[] = {
|
||||
.draw = uiTextboxMainDraw
|
||||
},
|
||||
|
||||
{
|
||||
.init = uiTextboxMiniListInit,
|
||||
.update = uiTextboxMiniListUpdate,
|
||||
.draw = uiTextboxMiniListDraw
|
||||
},
|
||||
|
||||
{
|
||||
.init = uiTransitionInit,
|
||||
.update = uiTransitionUpdate,
|
||||
|
||||
@@ -10,5 +10,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
uitab.c
|
||||
uislider.c
|
||||
uidropdown.c
|
||||
uiscrolling.c
|
||||
uiitem.c
|
||||
uiitemlist.c
|
||||
uimenu.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();
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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
@@ -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 += [
|
||||
|
||||
Reference in New Issue
Block a user