18 Commits

Author SHA1 Message Date
YourWishes a73f55beb0 Battle stuff
Build Dusk / build-linux (push) Successful in 7m5s
Build Dusk / build-psp (push) Successful in 1m33s
Build Dusk / build-knulli (push) Successful in 4m0s
Build Dusk / build-gamecube (push) Successful in 3m31s
Build Dusk / build-gamecube-iso (push) Successful in 3m39s
Build Dusk / build-wii (push) Successful in 3m5s
Build Dusk / build-wii-iso (push) Successful in 3m25s
2026-07-08 07:55:15 -05:00
YourWishes 0bd2491ab7 Dropdown test. 2026-07-07 15:18:33 -05:00
YourWishes 860c797c9c Last round of cleanup for settings for now. 2026-07-07 14:56:08 -05:00
YourWishes 38c5080f9f Collapsing more ui settings code 2026-07-07 14:36:19 -05:00
YourWishes fae191d8fe Settings improvements 2026-07-07 14:00:37 -05:00
YourWishes d83a953e2d First pass of language 2026-07-07 11:14:02 -05:00
YourWishes 2dcf0d0f0d UI Confirm 2026-07-07 10:12:22 -05:00
YourWishes 6dee37d8c1 Slider widget and some cleanup 2026-07-07 09:58:10 -05:00
YourWishes 3bad03afb3 Item entity 2026-07-07 07:52:22 -05:00
YourWishes 6a6d8448f7 Area cutscene controls 2026-07-07 07:28:12 -05:00
YourWishes 189babd2cf Trigger types on areas 2026-07-07 07:07:32 -05:00
YourWishes 988a0f2294 Area basics 2026-07-06 23:19:05 -05:00
YourWishes 85bf455731 entity pos sets chunk now 2026-07-04 18:43:09 -05:00
YourWishes 589e4224f3 Example global ent 2026-07-04 13:03:24 -05:00
YourWishes bd7fa154b4 Add user data to callbacks 2026-07-04 09:13:37 -05:00
YourWishes a292f1992b Change to use the Z tile system 2026-07-04 08:40:53 -05:00
YourWishes 88ddf429b7 add some entity management in prep for global entities. 2026-07-03 12:25:24 -05:00
YourWishes 7a1f6662df More cutscene tools 2026-07-03 09:17:49 -05:00
125 changed files with 4615 additions and 494 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+28 -43
View File
@@ -10,51 +10,36 @@ msgid "ui.title"
msgstr "" msgstr ""
"Welcome" "Welcome"
#: ui/user.c:22 #: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.greeting" msgid "ui.settings.tabs.general"
msgstr "Hello, %s!" msgstr "General"
#: ui/files.c:40 #: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.file_status" msgid "ui.settings.tabs.input"
msgstr "%s has %d files." msgstr "Input"
#: ui/cart.c:55 #: src/dusk/ui/frame/settings/uisettings.c
msgid "cart.item_count" msgid "ui.settings.tabs.display"
msgid_plural "cart.item_count" msgstr "Display"
msgstr[0] "%d item"
msgstr[1] "%d items (dual)"
msgstr[2] "%d items (few)"
msgstr[3] "%d items (many)"
#: ui/notifications.c:71 #: src/dusk/ui/frame/settings/uisettings.c
msgid "" msgid "ui.settings.tabs.audio"
"ui.multiline_help" msgstr "Audio"
msgstr ""
"Line one of the help text.\n"
"Line two continues here.\n"
"Line three ends here."
#: ui/errors.c:90 msgid "ui.settings.input.deadzone"
msgid "" msgstr "Deadzone"
"error.upload_failed.long"
msgstr ""
"Upload failed for file \"%s\".\n"
"Please try again later or contact support."
#: ui/messages.c:110 #: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "" msgid "ui.settings.general.language"
"user.invite_status" msgstr "Language"
msgid_plural ""
"user.invite_status" msgid "ui.settings.general.language_detail"
msgstr[0] "" msgstr "Takes effect after restarting the application."
"%s invited %d user.\n"
"Please review the request." #: src/dusk/ui/frame/settings/uisettings.c
msgstr[1] "" msgid "ui.settings.apply"
"%s invited %d users (dual).\n" msgstr "Apply"
"Please review the requests."
msgstr[2] "" #: src/dusk/ui/frame/uiconfirm.c
"%s invited %d users (few).\n" msgid "ui.confirm.discard_changes"
"Please review the requests." msgstr "Discard unsaved changes?"
msgstr[3] ""
"%s invited %d users (many).\n"
"Please review the requests."
+46
View File
@@ -0,0 +1,46 @@
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: es\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"Bienvenido"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "General"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "Entrada"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "Pantalla"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "Audio"
msgid "ui.settings.input.deadzone"
msgstr "Deadzone"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "Idioma"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language_detail"
msgstr "Se aplica después de reiniciar la aplicación."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "Aplicar"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "¿Descartar los cambios no guardados?"
+46
View File
@@ -0,0 +1,46 @@
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: ja\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=1; plural=(0);\n"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"歓迎"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "一般"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "入力"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "表示"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "オーディオ"
msgid "ui.settings.input.deadzone"
msgstr "デッドゾーン"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "言語"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language_detail"
msgstr "アプリケーションを再起動すると適用されます。"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "適用"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "未保存の変更を破棄しますか?"
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -36,7 +36,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
# DUSK_OPENGL_LEGACY # DUSK_OPENGL_LEGACY
DUSK_LINUX DUSK_LINUX
DUSK_DISPLAY_SIZE_DYNAMIC DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640 DUSK_DISPLAY_WIDTH_DEFAULT=854
DUSK_DISPLAY_HEIGHT_DEFAULT=480 DUSK_DISPLAY_HEIGHT_DEFAULT=480
DUSK_DISPLAY_SCREEN_HEIGHT=240 DUSK_DISPLAY_SCREEN_HEIGHT=240
DUSK_INPUT_KEYBOARD DUSK_INPUT_KEYBOARD
@@ -9,7 +9,7 @@
#include "asset/assetfile.h" #include "asset/assetfile.h"
#include "rpg/overworld/chunk.h" #include "rpg/overworld/chunk.h"
#define ASSET_CHUNK_FILE_VERSION 3 #define ASSET_CHUNK_FILE_VERSION 4
typedef struct assetloading_s assetloading_t; typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
@@ -513,9 +513,10 @@ errorret_t assetLocaleGetString(
// Skip blanks, comments, etc and start looking for msgid's // Skip blanks, comments, etc and start looking for msgid's
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer)); errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
while(!reader.eof) { while(true) {
// Is this msgid? // Is this msgid?
if(memoryCompare(lineBuffer, "msgid", 5) != 0) { if(memoryCompare(lineBuffer, "msgid", 5) != 0) {
if(reader.eof) break;
errorChain(assetFileLineReaderNext(&reader)); errorChain(assetFileLineReaderNext(&reader));
msgidBuffer[0] = '\0'; msgidBuffer[0] = '\0';
continue; continue;
@@ -539,7 +540,7 @@ errorret_t assetLocaleGetString(
} }
// We are either going to see a msgstr or a msgid_plural // We are either going to see a msgstr or a msgid_plural
while(!reader.eof) { while(true) {
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer)); errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
// Is msgid_plural? // Is msgid_plural?
+1
View File
@@ -17,6 +17,7 @@ input_t INPUT;
errorret_t inputInit(void) { errorret_t inputInit(void) {
memoryZero(&INPUT, sizeof(input_t)); memoryZero(&INPUT, sizeof(input_t));
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) { for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
INPUT.actions[i].action = (inputaction_t)i; INPUT.actions[i].action = (inputaction_t)i;
+4
View File
@@ -12,11 +12,15 @@
#define INPUT_LISTENER_PRESSED_MAX 16 #define INPUT_LISTENER_PRESSED_MAX 16
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX #define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
#define INPUT_DEADZONE_DEFAULT 0.1f
typedef struct { typedef struct {
inputactiondata_t actions[INPUT_ACTION_COUNT]; inputactiondata_t actions[INPUT_ACTION_COUNT];
inputplatform_t platform; inputplatform_t platform;
/** User-configured gamepad axis deadzone (0.0f to 1.0f). */
float_t deadzone;
} input_t; } input_t;
extern input_t INPUT; extern input_t INPUT;
+10
View File
@@ -16,4 +16,14 @@ typedef struct {
static const localeinfo_t LOCALE_EN_US = { static const localeinfo_t LOCALE_EN_US = {
.name = "en-US", .name = "en-US",
.file = "locale/en_US.po", .file = "locale/en_US.po",
};
static const localeinfo_t LOCALE_JP_JP = {
.name = "ja-JP",
.file = "locale/jp_JP.po",
};
static const localeinfo_t LOCALE_ES_MX = {
.name = "es-MX",
.file = "locale/es_MX.po",
}; };
+1
View File
@@ -11,6 +11,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
) )
# Subdirs # Subdirs
add_subdirectory(battle)
add_subdirectory(cutscene) add_subdirectory(cutscene)
add_subdirectory(entity) add_subdirectory(entity)
add_subdirectory(overworld) add_subdirectory(overworld)
+12
View File
@@ -0,0 +1,12 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
battle.c
battlefighter.c
party.c
)
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "battle.h"
#include "util/memory.h"
battle_t BATTLE;
void battleInit(void) {
memoryZero(&BATTLE, sizeof(battle_t));
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
BATTLE.fighters[i].id = i;
}
}
uint8_t battleGetAvailableFighter(void) {
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
}
return 0xFF;
}
battlefighter_t *battleAddFighter(
const battlefighterteam_t team,
const battlefightercontroller_t controller,
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
) {
const uint8_t index = battleGetAvailableFighter();
if(index == 0xFF) return NULL;
battlefighter_t *fighter = &BATTLE.fighters[index];
battleFighterInit(fighter, team, controller, stats, healthMax, mpMax);
return fighter;
}
void battleStart(void) {
BATTLE.active = true;
}
void battleDispose(void) {
battleInit();
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "battlefighter.h"
#define BATTLE_FIGHTER_COUNT_MAX 8
typedef struct {
bool_t active;
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
} battle_t;
extern battle_t BATTLE;
/**
* Initializes the battle system. Marks it as inactive with no fighters.
*/
void battleInit(void);
/**
* Gets an available (unused) fighter slot index.
*
* @return The index of an available fighter slot, or 0xFF if none are
* available.
*/
uint8_t battleGetAvailableFighter(void);
/**
* Adds a fighter to the battle in the next available slot.
*
* @param team The team the fighter belongs to.
* @param controller Who makes decisions for the fighter.
* @param stats The fighter's base combat stats.
* @param healthMax The fighter's maximum health.
* @param mpMax The fighter's maximum mp.
* @return Pointer to the newly added fighter, or NULL if the battle is
* already full.
*/
battlefighter_t *battleAddFighter(
const battlefighterteam_t team,
const battlefightercontroller_t controller,
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
);
/**
* Starts a battle, marking it active. Any fighters already added via
* battleAddFighter remain in place.
*/
void battleStart(void);
/**
* Disposes of the battle, clearing all fighters and marking it inactive.
*/
void battleDispose(void);
+43
View File
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "battlefighter.h"
#include "assert/assert.h"
#include "util/memory.h"
void battleFighterInit(
battlefighter_t *fighter,
const battlefighterteam_t team,
const battlefightercontroller_t controller,
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
) {
assertNotNull(fighter, "Fighter pointer cannot be NULL");
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
assertTrue(
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
"Invalid fighter controller"
);
const uint8_t id = fighter->id;
memoryZero(fighter, sizeof(battlefighter_t));
fighter->id = id;
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
fighter->team = team;
fighter->controller = controller;
fighter->stats = stats;
fighter->healthMax = healthMax;
fighter->health = healthMax;
fighter->mpMax = mpMax;
fighter->mp = mpMax;
}
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
assertNotNull(fighter, "Fighter pointer cannot be NULL");
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
}
+87
View File
@@ -0,0 +1,87 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
// An empty status means the slot in BATTLE.fighters is unused.
typedef enum {
BATTLE_FIGHTER_STATUS_NULL,
BATTLE_FIGHTER_STATUS_NORMAL,
BATTLE_FIGHTER_STATUS_DEAD,
BATTLE_FIGHTER_STATUS_COUNT
} battlefighterstatus_t;
typedef enum {
BATTLE_FIGHTER_TEAM_ALLY,
BATTLE_FIGHTER_TEAM_ENEMY,
BATTLE_FIGHTER_TEAM_COUNT
} battlefighterteam_t;
typedef enum {
BATTLE_FIGHTER_CONTROLLER_PLAYER,
BATTLE_FIGHTER_CONTROLLER_AI,
BATTLE_FIGHTER_CONTROLLER_COUNT
} battlefightercontroller_t;
// Base combat stats, kept separate from the resource pools (health/mp) on
// battlefighter_t so that equipment/buffs can later modify them without
// touching current health/mp state.
typedef struct {
uint16_t attack;
uint16_t defense;
uint16_t magic;
uint16_t speed;
uint16_t luck;
} battlefighterstats_t;
typedef struct {
uint8_t id;
battlefighterstatus_t status;
battlefighterteam_t team;
battlefightercontroller_t controller;
uint16_t health;
uint16_t healthMax;
uint16_t mp;
uint16_t mpMax;
battlefighterstats_t stats;
} battlefighter_t;
/**
* Initializes a battle fighter in place, filling health/mp to their maximum
* values and setting its status to normal.
*
* @param fighter Pointer to the fighter to initialize.
* @param team The team the fighter belongs to.
* @param controller Who makes decisions for the fighter.
* @param stats The fighter's base combat stats.
* @param healthMax The fighter's maximum health.
* @param mpMax The fighter's maximum mp.
*/
void battleFighterInit(
battlefighter_t *fighter,
const battlefighterteam_t team,
const battlefightercontroller_t controller,
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
);
/**
* Returns true if the fighter is in a state where it can still act (i.e.
* is not dead).
*
* @param fighter Pointer to the fighter to check.
* @returns True if the fighter can act.
*/
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
+71
View File
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "party.h"
#include "assert/assert.h"
#include "util/memory.h"
party_t PARTY;
void partyInit(void) {
memoryZero(&PARTY, sizeof(party_t));
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
PARTY.members[i].id = i;
}
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
PARTY.order[i] = PARTY_ORDER_EMPTY;
}
}
uint8_t partyGetAvailableMember(void) {
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
if(PARTY.members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
}
return 0xFF;
}
battlefighter_t *partyAddMember(
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
) {
const uint8_t index = partyGetAvailableMember();
if(index == 0xFF) return NULL;
battlefighter_t *member = &PARTY.members[index];
battleFighterInit(
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
stats, healthMax, mpMax
);
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
if(PARTY.order[i] != PARTY_ORDER_EMPTY) continue;
PARTY.order[i] = index;
break;
}
return member;
}
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
const uint8_t index = PARTY.order[slot];
if(index == PARTY_ORDER_EMPTY) return NULL;
return &PARTY.members[index];
}
void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
assertTrue(
memberIndex == PARTY_ORDER_EMPTY || memberIndex < PARTY_MEMBER_COUNT_MAX,
"Invalid party member index"
);
PARTY.order[slot] = memberIndex;
}
+73
View File
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "battlefighter.h"
#define PARTY_MEMBER_COUNT_MAX 4
#define PARTY_ACTIVE_SIZE_MAX 3
#define PARTY_ORDER_EMPTY 0xFF
typedef struct {
battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
// Maps an active battle slot to the roster member filling it, or
// PARTY_ORDER_EMPTY if the slot is unfilled. Only the first
// PARTY_ACTIVE_SIZE_MAX of the PARTY_MEMBER_COUNT_MAX roster members
// can be in the active lineup at once.
uint8_t order[PARTY_ACTIVE_SIZE_MAX];
} party_t;
extern party_t PARTY;
/**
* Initializes the party system with an empty roster and order.
*/
void partyInit(void);
/**
* Gets an available (unused) party member slot index.
*
* @return The index of an available slot, or 0xFF if the party is full.
*/
uint8_t partyGetAvailableMember(void);
/**
* Adds a member to the party roster in the next available slot. Party
* members are always allies controlled by the player. If there is a
* free active order slot, the new member is placed into it.
*
* @param stats The member's base combat stats.
* @param healthMax The member's maximum health.
* @param mpMax The member's maximum mp.
* @return Pointer to the newly added party member, or NULL if the party is
* already full.
*/
battlefighter_t *partyAddMember(
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
);
/**
* Gets the roster member currently occupying an active order slot.
*
* @param slot The active order slot to query.
* @return Pointer to the member in that slot, or NULL if the slot is
* empty.
*/
battlefighter_t *partyGetOrderMember(const uint8_t slot);
/**
* Assigns a roster member to an active order slot, replacing whatever
* was there. Use PARTY_ORDER_EMPTY to clear a slot.
*
* @param slot The active order slot to assign.
* @param memberIndex The roster member index to place there, or
* PARTY_ORDER_EMPTY to clear the slot.
*/
void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
+82 -2
View File
@@ -13,14 +13,19 @@ typedef struct cutscene_s {
const cutsceneitem_t *items; const cutsceneitem_t *items;
uint8_t itemCount; uint8_t itemCount;
cutscenepause_t pause; cutscenepause_t pause;
// Size in bytes of this cutscene's custom user data, carved out of
// CUTSCENE_SYSTEM.data while the cutscene is running.
size_t dataSize;
} cutscene_t; } cutscene_t;
#define CUTSCENE(NAME, PAUSE_TYPE, ...) \ #define CUTSCENE(NAME, SIZE, PAUSE_TYPE, ...) \
static const cutsceneitem_t CUTSCENE_##NAME##_ITEMS[] = { __VA_ARGS__ }; \ static const cutsceneitem_t CUTSCENE_##NAME##_ITEMS[] = { __VA_ARGS__ }; \
static const cutscene_t CUTSCENE_##NAME = { \ static const cutscene_t CUTSCENE_##NAME = { \
.items = CUTSCENE_##NAME##_ITEMS, \ .items = CUTSCENE_##NAME##_ITEMS, \
.itemCount = sizeof(CUTSCENE_##NAME##_ITEMS) / sizeof(cutsceneitem_t), \ .itemCount = sizeof(CUTSCENE_##NAME##_ITEMS) / sizeof(cutsceneitem_t), \
.pause = CUTSCENE_PAUSE_##PAUSE_TYPE \ .pause = CUTSCENE_PAUSE_##PAUSE_TYPE, \
.dataSize = SIZE \
}; };
#define CUTSCENE_REFERENCE(CUTSCENE) \ #define CUTSCENE_REFERENCE(CUTSCENE) \
@@ -70,6 +75,35 @@ typedef struct cutscene_s {
.entityRemove = { .entityIndex = ENTITY_INDEX } \ .entityRemove = { .entityIndex = ENTITY_INDEX } \
} }
#define CUTSCENE_ENTITY_ADD(TYPE, X, Y, Z) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_ADD, \
.entityAdd = { .entityType = TYPE, .position = { X, Y, Z } } \
}
#define CUTSCENE_ENTITY_TURN(ENTITY_INDEX, DIRECTION) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_TURN, \
.entityTurn = { .entityIndex = ENTITY_INDEX, .direction = DIRECTION } \
}
// Walks ENTITY_INDEX to stand beside TARGET_ENTITY_INDEX, offset by
// (OFFSET_X, OFFSET_Y) on the 2D plane. The destination Z is resolved
// from nearby terrain each frame, so ramps between the two entities are
// accounted for automatically.
#define CUTSCENE_ENTITY_WALK_TO_ENTITY( \
ENTITY_INDEX, TARGET_ENTITY_INDEX, OFFSET_X, OFFSET_Y \
) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, \
.entityWalkToEntity = { \
.entityIndex = ENTITY_INDEX, \
.targetEntityIndex = TARGET_ENTITY_INDEX, \
.offsetX = OFFSET_X, \
.offsetY = OFFSET_Y \
} \
}
#define CUTSCENE_ENTITY_TELEPORT(ENTITY_INDEX, X, Y, Z) \ #define CUTSCENE_ENTITY_TELEPORT(ENTITY_INDEX, X, Y, Z) \
{ \ { \
.type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, \ .type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, \
@@ -116,3 +150,49 @@ typedef struct cutscene_s {
) \ ) \
} \ } \
} }
#define CUTSCENE_MAP_AREA_ADD( \
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, CALLBACK, NOTIFY, TRIGGER \
) \
{ \
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, \
.mapAreaAdd = { \
.min = { MIN_X, MIN_Y, MIN_Z }, \
.max = { MAX_X, MAX_Y, MAX_Z }, \
.callback = CALLBACK, \
.notify = NOTIFY, \
.trigger = TRIGGER \
} \
}
#define CUTSCENE_MAP_AREA_REMOVE(AREA_ID) \
{ \
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, \
.mapAreaRemove = { .areaId = AREA_ID } \
}
// Waits until any one of the given map area IDs has its callback invoked.
// Accepts CUTSCENE_AREA_LAST_CREATED in place of a literal area ID.
#define CUTSCENE_MAP_AREA_WAIT(...) \
{ \
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, \
.mapAreaWait = { \
.areaIds = (const uint8_t[]){ __VA_ARGS__ }, \
.count = (uint8_t)( \
sizeof((const uint8_t[]){ __VA_ARGS__ }) / sizeof(uint8_t) \
) \
} \
}
// Adds a map area, waits for it to be triggered once, then removes it
// before the cutscene continues. Uses a no-op callback since the wait is
// driven by the area's trigger count rather than callback logic.
#define CUTSCENE_MAP_AREA_TRIGGER_ONCE( \
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, NOTIFY, TRIGGER \
) \
CUTSCENE_MAP_AREA_ADD( \
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, \
mapAreaNoopCallback, NOTIFY, TRIGGER \
), \
CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \
CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED)
+51 -9
View File
@@ -25,10 +25,18 @@ void cutsceneSystemStartCutsceneWith(
entity_t *interact, entity_t *interact,
entity_t *interacted entity_t *interacted
) { ) {
assertTrue(
cutscene->dataSize < CUTSCENE_SYSTEM_SIZE_MAX,
"Cutscene data size exceeds CUTSCENE_SYSTEM_SIZE_MAX"
);
CUTSCENE_SYSTEM.scene = cutscene; CUTSCENE_SYSTEM.scene = cutscene;
CUTSCENE_SYSTEM.pause = cutscene->pause; CUTSCENE_SYSTEM.pause = cutscene->pause;
CUTSCENE_SYSTEM.entityInteract = interact; CUTSCENE_SYSTEM.entityInteract = interact;
CUTSCENE_SYSTEM.entityInteracted = interacted; CUTSCENE_SYSTEM.entityInteracted = interacted;
CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0. CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
cutsceneSystemNext(); cutsceneSystemNext();
} }
@@ -54,6 +62,9 @@ void cutsceneSystemNext() {
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE; CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
CUTSCENE_SYSTEM.entityInteract = NULL; CUTSCENE_SYSTEM.entityInteract = NULL;
CUTSCENE_SYSTEM.entityInteracted = NULL; CUTSCENE_SYSTEM.entityInteracted = NULL;
CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
return; return;
} }
@@ -70,25 +81,53 @@ const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
} }
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex) { entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex) {
entity_t *entity;
if(entityIndex == CUTSCENE_ENTITY_INTERACT) { if(entityIndex == CUTSCENE_ENTITY_INTERACT) {
assertNotNull( assertNotNull(
CUTSCENE_SYSTEM.entityInteract, CUTSCENE_SYSTEM.entityInteract,
"CUTSCENE_ENTITY_INTERACT used but no interact entity is set" "CUTSCENE_ENTITY_INTERACT used but no interact entity is set"
); );
return CUTSCENE_SYSTEM.entityInteract; entity = CUTSCENE_SYSTEM.entityInteract;
} } else if(entityIndex == CUTSCENE_ENTITY_INTERACTED) {
if(entityIndex == CUTSCENE_ENTITY_INTERACTED) {
assertNotNull( assertNotNull(
CUTSCENE_SYSTEM.entityInteracted, CUTSCENE_SYSTEM.entityInteracted,
"CUTSCENE_ENTITY_INTERACTED used but no interacted entity is set" "CUTSCENE_ENTITY_INTERACTED used but no interacted entity is set"
); );
return CUTSCENE_SYSTEM.entityInteracted; entity = CUTSCENE_SYSTEM.entityInteracted;
} else if(entityIndex == CUTSCENE_ENTITY_LAST_CREATED) {
assertNotNull(
CUTSCENE_SYSTEM.entityLastCreated,
"CUTSCENE_ENTITY_LAST_CREATED used but no entity has been created"
);
entity = CUTSCENE_SYSTEM.entityLastCreated;
} else if(entityIndex == CUTSCENE_ENTITY_LAST_REF) {
assertNotNull(
CUTSCENE_SYSTEM.entityLastRef,
"CUTSCENE_ENTITY_LAST_REF used but no entity has been referenced"
);
entity = CUTSCENE_SYSTEM.entityLastRef;
} else {
assertTrue(
entityIndex < ENTITY_COUNT,
"Entity index is out of range"
);
entity = &ENTITIES[entityIndex];
} }
assertTrue(
entityIndex < ENTITY_COUNT, CUTSCENE_SYSTEM.entityLastRef = entity;
"Entity index is out of range" return entity;
); }
return &ENTITIES[entityIndex];
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
if(areaId == CUTSCENE_AREA_LAST_CREATED) {
assertTrue(
CUTSCENE_SYSTEM.areaLastCreated != CUTSCENE_AREA_LAST_CREATED,
"CUTSCENE_AREA_LAST_CREATED used but no map area has been created"
);
return CUTSCENE_SYSTEM.areaLastCreated;
}
return areaId;
} }
void cutsceneSystemDispose() { void cutsceneSystemDispose() {
@@ -97,4 +136,7 @@ void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE; CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
CUTSCENE_SYSTEM.entityInteract = NULL; CUTSCENE_SYSTEM.entityInteract = NULL;
CUTSCENE_SYSTEM.entityInteracted = NULL; CUTSCENE_SYSTEM.entityInteracted = NULL;
CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
} }
+28 -3
View File
@@ -10,8 +10,15 @@
typedef struct entity_s entity_t; typedef struct entity_s entity_t;
#define CUTSCENE_ENTITY_INTERACT ((uint8_t)0xFE) #define CUTSCENE_ENTITY_INTERACT ((uint8_t)0xFE)
#define CUTSCENE_ENTITY_INTERACTED ((uint8_t)0xFD) #define CUTSCENE_ENTITY_INTERACTED ((uint8_t)0xFD)
#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
#define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF)
// Maximum number of bytes a running cutscene may request via
// cutscene_t.dataSize.
#define CUTSCENE_SYSTEM_SIZE_MAX 8192
typedef struct { typedef struct {
const cutscene_t *scene; const cutscene_t *scene;
@@ -19,9 +26,16 @@ typedef struct {
cutscenepause_t pause; cutscenepause_t pause;
entity_t *entityInteract; entity_t *entityInteract;
entity_t *entityInteracted; entity_t *entityInteracted;
entity_t *entityLastCreated;
entity_t *entityLastRef;
uint8_t areaLastCreated;
// Data (used by the current item). // Data (used by the current item).
cutsceneitemdata_t data; cutsceneitemdata_t data;
// Custom user data for the running cutscene, sized per-scene by
// cutscene_t.dataSize.
uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX];
} cutscenesystem_t; } cutscenesystem_t;
extern cutscenesystem_t CUTSCENE_SYSTEM; extern cutscenesystem_t CUTSCENE_SYSTEM;
@@ -53,7 +67,9 @@ void cutsceneSystemStartCutsceneWith(
/** /**
* Resolves a raw entity index (or sentinel) to an entity pointer. * Resolves a raw entity index (or sentinel) to an entity pointer.
* Handles CUTSCENE_ENTITY_INTERACT and CUTSCENE_ENTITY_INTERACTED. * Handles CUTSCENE_ENTITY_INTERACT, CUTSCENE_ENTITY_INTERACTED,
* CUTSCENE_ENTITY_LAST_CREATED and CUTSCENE_ENTITY_LAST_REF.
* Updates CUTSCENE_SYSTEM.entityLastRef to the resolved entity.
* Asserts the resolved entity is within bounds. * Asserts the resolved entity is within bounds.
* *
* @param entityIndex Raw entity index or sentinel value. * @param entityIndex Raw entity index or sentinel value.
@@ -61,6 +77,15 @@ void cutsceneSystemStartCutsceneWith(
*/ */
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex); entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
/**
* Resolves a raw map area ID (or CUTSCENE_AREA_LAST_CREATED sentinel) to
* a concrete map area ID.
*
* @param areaId Raw map area ID or sentinel value.
* @returns The resolved map area ID.
*/
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
/** /**
* Advance to the next item in the cutscene. * Advance to the next item in the cutscene.
*/ */
@@ -12,4 +12,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(control) add_subdirectory(control)
add_subdirectory(entity) add_subdirectory(entity)
add_subdirectory(item) add_subdirectory(item)
add_subdirectory(maparea)
add_subdirectory(ui) add_subdirectory(ui)
@@ -6,12 +6,13 @@
*/ */
#include "cutsceneitem.h" #include "cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
void cutsceneCallbackStart( void cutsceneCallbackStart(
const cutsceneitem_t *item, const cutsceneitem_t *item,
cutsceneitemdata_t *data cutsceneitemdata_t *data
) { ) {
if(item->callback != NULL) item->callback(); if(item->callback != NULL) item->callback(CUTSCENE_SYSTEM.userData);
} }
bool_t cutsceneCallbackUpdate( bool_t cutsceneCallbackUpdate(
@@ -11,7 +11,7 @@
typedef struct cutsceneitem_s cutsceneitem_t; typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t; typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef void (*cutscenecallback_t)(void); typedef void (*cutscenecallback_t)(void *userData);
/** /**
* Starts a callback item (invokes the callback immediately). * Starts a callback item (invokes the callback immediately).
+42
View File
@@ -53,6 +53,30 @@ void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
cutsceneEntityRemoveStart(item, data); cutsceneEntityRemoveStart(item, data);
break; break;
case CUTSCENE_ITEM_TYPE_ENTITY_ADD:
cutsceneEntityAddStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_TURN:
cutsceneEntityTurnStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY:
cutsceneEntityWalkToEntityStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_MAP_AREA_ADD:
cutsceneMapAreaAddStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE:
cutsceneMapAreaRemoveStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT:
cutsceneMapAreaWaitStart(item, data);
break;
default: default:
break; break;
} }
@@ -90,6 +114,24 @@ bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data)
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE: case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
return cutsceneEntityRemoveUpdate(item, data); return cutsceneEntityRemoveUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_ADD:
return cutsceneEntityAddUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_TURN:
return cutsceneEntityTurnUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY:
return cutsceneEntityWalkToEntityUpdate(item, data);
case CUTSCENE_ITEM_TYPE_MAP_AREA_ADD:
return cutsceneMapAreaAddUpdate(item, data);
case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE:
return cutsceneMapAreaRemoveUpdate(item, data);
case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT:
return cutsceneMapAreaWaitUpdate(item, data);
default: default:
return false; return false;
} }
+20 -1
View File
@@ -13,9 +13,15 @@
#include "entity/cutsceneentityteleport.h" #include "entity/cutsceneentityteleport.h"
#include "entity/cutsceneentitywalkto.h" #include "entity/cutsceneentitywalkto.h"
#include "entity/cutsceneentityremove.h" #include "entity/cutsceneentityremove.h"
#include "entity/cutsceneentityadd.h"
#include "entity/cutsceneentityturn.h"
#include "entity/cutsceneentitywalktoentity.h"
#include "ui/cutscenetext.h" #include "ui/cutscenetext.h"
#include "ui/cutscenefade.h" #include "ui/cutscenefade.h"
#include "item/cutsceneitemgive.h" #include "item/cutsceneitemgive.h"
#include "maparea/cutscenemapareaadd.h"
#include "maparea/cutscenemaparearemove.h"
#include "maparea/cutscenemapareawait.h"
typedef struct cutscene_s cutscene_t; typedef struct cutscene_s cutscene_t;
@@ -31,7 +37,13 @@ typedef enum {
CUTSCENE_ITEM_TYPE_SET_PAUSE, CUTSCENE_ITEM_TYPE_SET_PAUSE,
CUTSCENE_ITEM_TYPE_CONCURRENT, CUTSCENE_ITEM_TYPE_CONCURRENT,
CUTSCENE_ITEM_TYPE_ITEM_GIVE, CUTSCENE_ITEM_TYPE_ITEM_GIVE,
CUTSCENE_ITEM_TYPE_ENTITY_REMOVE CUTSCENE_ITEM_TYPE_ENTITY_REMOVE,
CUTSCENE_ITEM_TYPE_ENTITY_ADD,
CUTSCENE_ITEM_TYPE_ENTITY_TURN,
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
} cutsceneitemtype_t; } cutsceneitemtype_t;
struct cutsceneitem_s { struct cutsceneitem_s {
@@ -49,6 +61,12 @@ struct cutsceneitem_s {
cutsceneconcurrent_t concurrent; cutsceneconcurrent_t concurrent;
cutsceneitemgive_t itemGive; cutsceneitemgive_t itemGive;
cutsceneentityremove_t entityRemove; cutsceneentityremove_t entityRemove;
cutsceneentityadd_t entityAdd;
cutsceneentityturn_t entityTurn;
cutsceneentitywalktoentity_t entityWalkToEntity;
cutscenemapareaadd_t mapAreaAdd;
cutscenemaparearemove_t mapAreaRemove;
cutscenemapareawait_t mapAreaWait;
}; };
}; };
@@ -56,6 +74,7 @@ typedef union cutsceneitemdata_u {
cutscenewaitdata_t wait; cutscenewaitdata_t wait;
cutsceneentitywalktodata_t entityWalkTo; cutsceneentitywalktodata_t entityWalkTo;
cutsceneconcurrentdata_t concurrent; cutsceneconcurrentdata_t concurrent;
cutscenemapareawaitdata_t mapAreaWait;
} cutsceneitemdata_t; } cutsceneitemdata_t;
/** /**
@@ -8,4 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
cutsceneentityteleport.c cutsceneentityteleport.c
cutsceneentitywalkto.c cutsceneentitywalkto.c
cutsceneentityremove.c cutsceneentityremove.c
cutsceneentityadd.c
cutsceneentityturn.c
cutsceneentitywalktoentity.c
) )
@@ -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 "rpg/entity/entity.h"
#include "assert/assert.h"
void cutsceneEntityAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t entIndex = entityGetAvailable();
assertTrue(entIndex != 0xFF, "No available entity slots for CUTSCENE_ENTITY_ADD");
entity_t *entity = &ENTITIES[entIndex];
entityInit(entity, item->entityAdd.entityType);
entityPositionSet(entity, item->entityAdd.position);// Also assigns chunk.
CUTSCENE_SYSTEM.entityLastCreated = entity;
CUTSCENE_SYSTEM.entityLastRef = entity;
}
bool_t cutsceneEntityAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/entity/entitytype.h"
#include "rpg/overworld/worldpos.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
entitytype_t entityType;
worldpos_t position;
} cutsceneentityadd_t;
/**
* Starts an entity add step (spawns the entity into the world immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEntityAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an entity add step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneEntityAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,30 @@
/**
* 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 "rpg/entity/entity.h"
void cutsceneEntityTurnStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
}
bool_t cutsceneEntityTurnUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
entity_t *entity = cutsceneSystemGetEntity(item->entityTurn.entityIndex);
if(
entity->direction == item->entityTurn.direction &&
entity->animation == ENTITY_ANIM_IDLE
) return true;
entityTurn(entity, item->entityTurn.direction);
return false;
}
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/entity/entitydir.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t entityIndex;
entitydir_t direction;
} cutsceneentityturn_t;
/**
* Starts an entity turn step. The turn itself is driven from Update, since
* the entity may still be finishing a previous action.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEntityTurnStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an entity turn step, retrying entityTurn until it takes effect
* and its animation completes.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once the entity is idle and facing the target direction.
*/
bool_t cutsceneEntityTurnUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,41 @@
/**
* 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 "rpg/entity/entity.h"
#include "rpg/entity/entitypathstep.h"
#include "rpg/overworld/map.h"
void cutsceneEntityWalkToEntityStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
}
bool_t cutsceneEntityWalkToEntityUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
entity_t *entity = cutsceneSystemGetEntity(
item->entityWalkToEntity.entityIndex
);
entity_t *target = cutsceneSystemGetEntity(
item->entityWalkToEntity.targetEntityIndex
);
worldpos_t dest = {
.x = (worldunit_t)(target->position.x + item->entityWalkToEntity.offsetX),
.y = (worldunit_t)(target->position.y + item->entityWalkToEntity.offsetY),
.z = target->position.z
};
worldunit_t z;
if(mapGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
return entityPathStep(entity, dest, true);
}
@@ -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 "rpg/overworld/worldpos.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t entityIndex;
uint8_t targetEntityIndex;
worldunit_t offsetX;
worldunit_t offsetY;
} cutsceneentitywalktoentity_t;
/**
* Starts an entity walk-to-entity item. No setup is needed, the destination
* is recomputed from the target's live position every Update.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEntityWalkToEntityStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an entity walk-to-entity item. Re-reads the target entity's
* current position each frame, applies the X/Y offset, resolves the
* destination Z from nearby terrain (to account for ramps), and steps
* the entity toward it.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once the entity has reached the target's side.
*/
bool_t cutsceneEntityWalkToEntityUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -6,25 +6,14 @@
*/ */
#include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/item/backpack.h" #include "rpg/item/itemgive.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/uitextboxmain.h"
#include "util/string.h"
void cutsceneItemGiveStart( void cutsceneItemGiveStart(
const cutsceneitem_t *item, const cutsceneitem_t *item,
cutsceneitemdata_t *data cutsceneitemdata_t *data
) { ) {
backpackAdd(item->itemGive.item, item->itemGive.quantity); itemGive(item->itemGive.item, item->itemGive.quantity);
char_t msg[CUTSCENE_TEXT_MAX_CHARS];
stringFormat(
msg,
CUTSCENE_TEXT_MAX_CHARS - 1,
"Received %s x%u",
ITEMS[item->itemGive.item].name,
(uint32_t)item->itemGive.quantity
);
uiTextboxMainSetText(msg);
} }
bool_t cutsceneItemGiveUpdate( bool_t cutsceneItemGiveUpdate(
@@ -0,0 +1,11 @@
# 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
cutscenemapareaadd.c
cutscenemaparearemove.c
cutscenemapareawait.c
)
@@ -0,0 +1,29 @@
/**
* 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"
void cutsceneMapAreaAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
CUTSCENE_SYSTEM.areaLastCreated = mapAreaAdd(
item->mapAreaAdd.min,
item->mapAreaAdd.max,
item->mapAreaAdd.callback,
item->mapAreaAdd.notify,
item->mapAreaAdd.trigger
);
}
bool_t cutsceneMapAreaAddUpdate(
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 "rpg/overworld/maparea.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
worldpos_t min;
worldpos_t max;
mapareacallback_t callback;
uint8_t notify;
uint8_t trigger;
} cutscenemapareaadd_t;
/**
* Starts a map area add step (adds the area immediately, storing its ID
* in CUTSCENE_SYSTEM.areaLastCreated).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneMapAreaAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a map area add step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneMapAreaAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,24 @@
/**
* 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 "rpg/overworld/maparea.h"
void cutsceneMapAreaRemoveStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
mapAreaRemove(cutsceneSystemGetAreaId(item->mapAreaRemove.areaId));
}
bool_t cutsceneMapAreaRemoveUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,40 @@
/**
* 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 areaId;
} cutscenemaparearemove_t;
/**
* Starts a map area remove step (removes the area immediately). Accepts
* CUTSCENE_AREA_LAST_CREATED in place of a literal area ID.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneMapAreaRemoveStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a map area remove step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneMapAreaRemoveUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,40 @@
/**
* 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 "rpg/overworld/maparea.h"
#include "assert/assert.h"
void cutsceneMapAreaWaitStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
assertTrue(
item->mapAreaWait.count <= CUTSCENE_MAP_AREA_WAIT_MAX,
"Too many areas in CUTSCENE_MAP_AREA_WAIT"
);
for(uint8_t i = 0; i < item->mapAreaWait.count; i++) {
uint8_t areaId = cutsceneSystemGetAreaId(item->mapAreaWait.areaIds[i]);
data->mapAreaWait.baseline[i] = MAP_AREAS[areaId].triggerCount;
}
}
bool_t cutsceneMapAreaWaitUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
for(uint8_t i = 0; i < item->mapAreaWait.count; i++) {
uint8_t areaId = cutsceneSystemGetAreaId(item->mapAreaWait.areaIds[i]);
if(MAP_AREAS[areaId].triggerCount != data->mapAreaWait.baseline[i]) {
return true;
}
}
return false;
}
@@ -0,0 +1,49 @@
/**
* 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;
/** Maximum number of areas a single CUTSCENE_MAP_AREA_WAIT may watch. */
#define CUTSCENE_MAP_AREA_WAIT_MAX 4
typedef struct {
const uint8_t *areaIds;
uint8_t count;
} cutscenemapareawait_t;
typedef struct {
uint32_t baseline[CUTSCENE_MAP_AREA_WAIT_MAX];
} cutscenemapareawaitdata_t;
/**
* Starts a map area wait step, snapshotting each watched area's current
* trigger count.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneMapAreaWaitStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a map area wait step, completing once any watched area's
* trigger count has changed since Start (i.e. its callback fired).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once any watched area has been triggered.
*/
bool_t cutsceneMapAreaWaitUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+10 -8
View File
@@ -9,17 +9,19 @@
#include "rpg/cutscene/cutscene.h" #include "rpg/cutscene/cutscene.h"
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
CUTSCENE(TEST_ONE, DEFAULT, CUTSCENE(TEST_ONE, 0, DEFAULT,
CUTSCENE_TEXT("Test One."), CUTSCENE_TEXT("Test One."),
); );
CUTSCENE(TEST_TWO, DEFAULT, CUTSCENE(TEST_TWO, 0, DEFAULT,
CUTSCENE_TEXT("Test Two."), CUTSCENE_TEXT("Test Two."),
CUTSCENE_CONCURRENT( CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0),
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0), CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_LAST_CREATED, 8, 2, 0),
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACTED, 8, 2, 0), // CUTSCENE_CONCURRENT(
), // CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0),
CUTSCENE_ITEM_GIVE(ITEM_ID_POTATO, 3), // CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACTED, 8, 2, 0),
CUTSCENE_ENTITY_REMOVE(CUTSCENE_ENTITY_INTERACT), // ),
// CUTSCENE_ITEM_GIVE(ITEM_ID_POTATO, 3),
// CUTSCENE_ENTITY_REMOVE(CUTSCENE_ENTITY_INTERACT),
CUTSCENE_TEXT("Done."), CUTSCENE_TEXT("Done."),
); );
+3 -1
View File
@@ -14,4 +14,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(anim) add_subdirectory(anim)
add_subdirectory(interact) add_subdirectory(interact)
add_subdirectory(npc) add_subdirectory(npc)
add_subdirectory(item)
add_subdirectory(global)
+2 -2
View File
@@ -19,7 +19,7 @@ const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
}; };
float_t entityAnimTileZOffset(const worldpos_t pos) { float_t entityAnimTileZOffset(const worldpos_t pos) {
return tileIsRamp(mapGetTile(pos)) ? 0.5f : 0.0f; return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f;
} }
void entityAnimUpdate(entity_t *entity) { void entityAnimUpdate(entity_t *entity) {
@@ -32,7 +32,7 @@ void entityAnimUpdate(entity_t *entity) {
) { ) {
entity->walkEndCooldown = ENTITY_ANIM_WALK_TURN_COOLDOWN; entity->walkEndCooldown = ENTITY_ANIM_WALK_TURN_COOLDOWN;
} }
entity->animation = ENTITY_ANIM_IDLE; entity->animation = ENTITY_ANIM_IDLE;
entity->animTime = 0; entity->animTime = 0;
} }
} }
+59 -25
View File
@@ -11,6 +11,7 @@
#include "time/time.h" #include "time/time.h"
#include "util/math.h" #include "util/math.h"
#include "rpg/overworld/map.h" #include "rpg/overworld/map.h"
#include "rpg/overworld/maparea.h"
#include "rpg/overworld/chunk.h" #include "rpg/overworld/chunk.h"
#include "rpg/overworld/tile.h" #include "rpg/overworld/tile.h"
@@ -27,6 +28,7 @@ void entityInit(entity_t *entity, const entitytype_t type) {
memoryZero(entity, sizeof(entity_t)); memoryZero(entity, sizeof(entity_t));
entity->id = (uint8_t)(entity - ENTITIES); entity->id = (uint8_t)(entity - ENTITIES);
entity->globalId = ENTITY_GLOBAL_ID_NULL;
entity->type = type; entity->type = type;
entity->chunkIndex = 0xFF; entity->chunkIndex = 0xFF;
@@ -60,6 +62,10 @@ bool_t entityCanRun(entity_t *entity) {
return entity->animation == ENTITY_ANIM_IDLE; return entity->animation == ENTITY_ANIM_IDLE;
} }
bool_t entityCanUnload(entity_t *entity) {
return entity->globalId < ENTITY_GLOBAL_ID_START;
}
void entityTurn(entity_t *entity, const entitydir_t direction) { void entityTurn(entity_t *entity, const entitydir_t direction) {
if(!entityCanTurn(entity)) return; if(!entityCanTurn(entity)) return;
entity->direction = direction; entity->direction = direction;
@@ -89,38 +95,38 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
// Are we walking up a ramp? // Are we walking up a ramp?
if( if(
tileIsRamp(tileCurrent) && tileShapeIsRamp(tileCurrent.shape) &&
( (
// Can only walk UP the direction the ramp faces. // Can only walk UP the direction the ramp faces.
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent || (direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape ||
// If diagonal ramp, can go up one of two ways only. Inner ramps // If diagonal ramp, can go up one of two ways only. Inner ramps
// share the same allowed directions as their outer counterparts. // share the same allowed directions as their outer counterparts.
( (
( (
( (
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST || tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST_INNER tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) && ) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
) || ) ||
( (
( (
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST || tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST_INNER tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) && ) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) || ) ||
( (
( (
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST || tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST ||
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST_INNER tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
) && ) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) || ) ||
( (
( (
tileCurrent == TILE_SHAPE_RAMP_NORTHWEST || tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST ||
tileCurrent == TILE_SHAPE_RAMP_NORTHWEST_INNER tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
) && ) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
) )
@@ -129,55 +135,60 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
) )
) { ) {
tile_t tileNewSaved = tileNew; tile_t tileNewSaved = tileNew;
tileNew = TILE_SHAPE_NULL; tileNew = TILE_NULL;
worldpos_t abovePos = newPos; worldpos_t abovePos = newPos;
abovePos.z += 1; abovePos.z += 1;
tile_t tileAbove = mapGetTile(abovePos); tile_t tileAbove = mapGetTile(abovePos);
if(tileAbove != TILE_SHAPE_NULL && tileIsWalkable(tileAbove)) { if(
tileAbove.shape != TILE_SHAPE_NULL &&
tileShapeIsWalkable(tileAbove.shape)
) {
raise = true; raise = true;
} else { } else {
tileNew = tileNewSaved; tileNew = tileNewSaved;
} }
} else if(tileNew == TILE_SHAPE_NULL && newPos.z > 0) { } else if(tileNew.shape == TILE_SHAPE_NULL && newPos.z > 0) {
// Falling down? // Falling down?
worldpos_t belowPos = newPos; worldpos_t belowPos = newPos;
belowPos.z -= 1; belowPos.z -= 1;
tile_t tileBelow = mapGetTile(belowPos); tile_t tileBelow = mapGetTile(belowPos);
if( if(
tileBelow != TILE_SHAPE_NULL && tileBelow.shape != TILE_SHAPE_NULL &&
tileIsRamp(tileBelow) && tileShapeIsRamp(tileBelow.shape) &&
( (
// This handles regular cardinal ramps // This handles regular cardinal ramps
(entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH) == tileBelow || (
entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH
) == tileBelow.shape ||
// This handles diagonal ramps. Inner ramps share the same // This handles diagonal ramps. Inner ramps share the same
// allowed directions as their outer counterparts. // allowed directions as their outer counterparts.
( (
( (
( (
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST || tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST_INNER tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) && ) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
) || ) ||
( (
( (
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST || tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST_INNER tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) && ) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) || ) ||
( (
( (
tileBelow == TILE_SHAPE_RAMP_NORTHEAST || tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST ||
tileBelow == TILE_SHAPE_RAMP_NORTHEAST_INNER tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
) && ) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) || ) ||
( (
( (
tileBelow == TILE_SHAPE_RAMP_NORTHWEST || tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST ||
tileBelow == TILE_SHAPE_RAMP_NORTHWEST_INNER tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
) && ) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
) )
@@ -190,7 +201,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
} }
// Can we walk here? // Can we walk here?
if(!raise && !fall && !tileIsWalkable(tileNew)) return;// Blocked if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked
// Raise/fall must be applied before checking for blocking entities, // Raise/fall must be applied before checking for blocking entities,
// otherwise the check compares against the wrong z-level. // otherwise the check compares against the wrong z-level.
@@ -213,6 +224,8 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
entity->position = newPos; entity->position = newPos;
entity->animation = ENTITY_ANIM_WALK; entity->animation = ENTITY_ANIM_WALK;
entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking
entityUpdateChunk(entity);
mapAreaCheckEntity(entity);
} }
void entityRun(entity_t *entity, const entitydir_t direction) { void entityRun(entity_t *entity, const entitydir_t direction) {
@@ -235,6 +248,17 @@ entity_t * entityGetAt(const worldpos_t position) {
return NULL; return NULL;
} }
entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
entity_t *ent = ENTITIES;
do {
if(ent->type == ENTITY_TYPE_NULL) continue;
if(ent->globalId != globalId) continue;
return ent;
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
return NULL;
}
uint8_t entityGetAvailable() { uint8_t entityGetAvailable() {
entity_t *ent = ENTITIES; entity_t *ent = ENTITIES;
do { do {
@@ -251,6 +275,7 @@ void entityPositionSet(entity_t *entity, const worldpos_t pos) {
entity->animation = ENTITY_ANIM_IDLE; entity->animation = ENTITY_ANIM_IDLE;
entity->animTime = 0; entity->animTime = 0;
entity->walkEndCooldown = 0; entity->walkEndCooldown = 0;
entityUpdateChunk(entity);
} }
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) { void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
@@ -279,4 +304,13 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
} }
} }
} }
}
void entityUpdateChunk(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
chunkpos_t cp;
worldPosToChunkPos(&entity->position, &cp);
chunkindex_t ci = mapGetChunkIndexAt(cp);
if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
} }
+35 -1
View File
@@ -14,8 +14,15 @@
typedef struct map_s map_t; typedef struct map_s map_t;
typedef uint16_t entityglobalid_t;
#define ENTITY_GLOBAL_ID_NULL 0
#define ENTITY_GLOBAL_ID_START 1
#define ENTITY_GLOBAL_ID_PLAYER 1
typedef struct entity_s { typedef struct entity_s {
uint8_t id; uint8_t id;
entityglobalid_t globalId;
entitytype_t type; entitytype_t type;
entitytypedata_t data; entitytypedata_t data;
@@ -75,6 +82,16 @@ bool_t entityCanWalk(entity_t *entity);
*/ */
bool_t entityCanRun(entity_t *entity); bool_t entityCanRun(entity_t *entity);
/**
* Returns true if the entity is allowed to be unloaded. By default this is
* true for entities whose global ID falls within the randomly assigned
* range below ENTITY_GLOBAL_ID_START.
*
* @param entity Pointer to the entity to check.
* @returns True if the entity can be unloaded.
*/
bool_t entityCanUnload(entity_t *entity);
/** /**
* Turn an entity to face a new direction. * Turn an entity to face a new direction.
* *
@@ -101,13 +118,21 @@ void entityRun(entity_t *entity, const entitydir_t direction);
/** /**
* Gets the entity at a specific world position. * Gets the entity at a specific world position.
* *
* @param map Pointer to the map to check. * @param map Pointer to the map to check.
* @param pos The world position to check. * @param pos The world position to check.
* @return Pointer to the entity at the position, or NULL if none. * @return Pointer to the entity at the position, or NULL if none.
*/ */
entity_t *entityGetAt(const worldpos_t pos); entity_t *entityGetAt(const worldpos_t pos);
/**
* Gets the entity with the given global ID, if one is currently loaded.
*
* @param globalId The global ID to search for.
* @return Pointer to the matching entity, or NULL if none is loaded.
*/
entity_t *entityGetByGlobalId(const entityglobalid_t globalId);
/** /**
* Gets an available entity index. * Gets an available entity index.
* *
@@ -124,6 +149,15 @@ uint8_t entityGetAvailable();
*/ */
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex); void entitySetChunk(entity_t *entity, const uint8_t chunkIndex);
/**
* Resolves the chunk that an entity's current position falls into and
* assigns the entity to it via entitySetChunk. Leaves the entity's chunk
* unchanged if its position doesn't fall within any loaded chunk.
*
* @param entity Pointer to the entity to update.
*/
void entityUpdateChunk(entity_t *entity);
/** /**
* Instantly moves an entity to a world position, resetting movement state. * Instantly moves an entity to a world position, resetting movement state.
* *
+3 -1
View File
@@ -24,7 +24,9 @@ bool_t entityPathStep(
"Entity pointer is out of bounds" "Entity pointer is out of bounds"
); );
if(worldPosIsEqual(entity->position, target)) return true; if(worldPosIsEqual(entity->position, target) && entityCanWalk(entity)) {
return true;
}
if(!entityCanWalk(entity)) return false; if(!entityCanWalk(entity)) return false;
entitydir_t dir; entitydir_t dir;
+9 -1
View File
@@ -8,12 +8,14 @@
#pragma once #pragma once
#include "rpg/entity/player.h" #include "rpg/entity/player.h"
#include "npc/npc.h" #include "npc/npc.h"
#include "item/entityitem.h"
typedef enum { typedef enum {
ENTITY_TYPE_NULL, ENTITY_TYPE_NULL,
ENTITY_TYPE_PLAYER, ENTITY_TYPE_PLAYER,
ENTITY_TYPE_NPC, ENTITY_TYPE_NPC,
ENTITY_TYPE_ITEM,
ENTITY_TYPE_COUNT ENTITY_TYPE_COUNT
} entitytype_enum_t; } entitytype_enum_t;
@@ -23,6 +25,7 @@ typedef uint8_t entitytype_t;
typedef union { typedef union {
player_t player; player_t player;
npc_t npc; npc_t npc;
entityitem_t item;
} entitytypedata_t; } entitytypedata_t;
typedef struct { typedef struct {
@@ -50,5 +53,10 @@ static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = {
[ENTITY_TYPE_NPC] = { [ENTITY_TYPE_NPC] = {
.init = npcInit, .init = npcInit,
.movement = npcMovement, .movement = npcMovement,
},
[ENTITY_TYPE_ITEM] = {
.init = entityItemInit,
.movement = entityItemMovement,
} }
}; };
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
)
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entityglobaldefs.h"
#include "entitygloballist.h"
#define ENTITY_GLOBAL_LIST_COUNT ( \
sizeof(ENTITY_GLOBAL_LIST) / \
sizeof(ENTITY_GLOBAL_LIST[0]) \
)
//EOF
@@ -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 "error/error.h"
#include "rpg/overworld/worldpos.h"
#include "rpg/entity/entity.h"
typedef struct entity_s entity_t;
typedef struct {
entity_t *entity;
worldpos_t position;
} entityglobalcreate_t;
/**
* Callback invoked to initialize a global entity.
*
* @param create Pointer to the entity/position being initialized.
* @returns An error code.
*/
typedef void (*entityglobalinitcallback_t)(
entityglobalcreate_t *create
);
typedef struct {
entitytype_t type;
entityglobalinitcallback_t callback;
} entityglobaldef_t;
#define ENTITY_GLOBAL(id, entType, callbackFn) \
[id] = { .type = entType, .callback = callbackFn }
#define ENTITY_GLOBAL_CALLBACK(id) \
static void ENTTIYT_GLOBAL_CALLBACK_##id(entityglobalcreate_t *create)
#define ENTITY_GLOBAL_REF(id) \
ENTTIYT_GLOBAL_CALLBACK_##id
//EOF
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entityglobaldefs.h"
#include "rpg/cutscene/scene/testcutscene.h"
ENTITY_GLOBAL_CALLBACK(3) {
create->entity->data.npc.moveType = NPC_MOVE_TYPE_PATH;
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 });
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 });
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 });
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 });
create->entity->interact.type = ENTITY_INTERACT_CUTSCENE;
create->entity->interact.data.cutscene = CUTSCENE_REFERENCE(TEST_TWO);
}
static const entityglobaldef_t ENTITY_GLOBAL_LIST[] = {
ENTITY_GLOBAL(ENTITY_GLOBAL_ID_NULL, ENTITY_TYPE_NULL, NULL),
ENTITY_GLOBAL(ENTITY_GLOBAL_ID_PLAYER, ENTITY_TYPE_PLAYER, NULL),
ENTITY_GLOBAL(3, ENTITY_TYPE_NPC, ENTITY_GLOBAL_REF(3)),
};
//EOF
@@ -40,6 +40,14 @@ void entityInteractWith(entity_t *player, entity_t *target) {
// entityTurn(player, player->direction); // Redundant (for now) // entityTurn(player, player->direction); // Redundant (for now)
break; break;
case ENTITY_INTERACT_CALLBACK:
assertNotNull(
target->interact.data.callback,
"Interact callback pointer cannot be NULL"
);
target->interact.data.callback(player, target);
break;
case ENTITY_INTERACT_NULL: case ENTITY_INTERACT_NULL:
break; break;
@@ -7,9 +7,9 @@
#pragma once #pragma once
#include "dusk.h" #include "dusk.h"
#include "rpg/cutscene/cutscene.h"
typedef struct entity_s entity_t; typedef struct entity_s entity_t;
typedef struct cutscene_s cutscene_t;
/** /**
* Describes the type of interaction an entity supports. * Describes the type of interaction an entity supports.
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
entityitem.c
)
+44
View File
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityitem.h"
#include "rpg/entity/entity.h"
#include "assert/assert.h"
#include "rpg/item/itemgive.h"
#include "ui/rpg/uitextboxmain.h"
void entityItemInit(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
entity->interact.type = ENTITY_INTERACT_CALLBACK;
entity->interact.data.callback = entityItemInteract;
}
void entityItemSet(
entity_t *entity,
const itemid_t item,
const uint8_t quantity
) {
assertNotNull(entity, "Entity pointer cannot be NULL");
entity->data.item.item = item;
entity->data.item.quantity = quantity;
}
void entityItemInteract(entity_t *player, entity_t *target) {
assertNotNull(player, "Player entity pointer cannot be NULL");
assertNotNull(target, "Target entity pointer cannot be NULL");
itemGive(target->data.item.item, target->data.item.quantity);
target->data.item.collected = true;
}
void entityItemMovement(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
if(!entity->data.item.collected) return;
if(uiTextboxMainIsActive()) return;
entity->type = ENTITY_TYPE_NULL;
}
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/item/item.h"
typedef struct entity_s entity_t;
typedef struct {
itemid_t item;
uint8_t quantity;
bool_t collected;
} entityitem_t;
/**
* Initializes an item entity. Wires up the interact component so that
* interacting with the entity gives its item and removes it.
*
* @param entity Pointer to the entity structure to initialize.
*/
void entityItemInit(entity_t *entity);
/**
* Sets the item and quantity represented by an item entity.
*
* @param entity Pointer to the entity structure.
* @param item The item ID to give when picked up.
* @param quantity The quantity to give when picked up.
*/
void entityItemSet(
entity_t *entity,
const itemid_t item,
const uint8_t quantity
);
/**
* Interact callback for item entities. Gives the entity's item to the
* player and shows the pickup message. The entity itself is removed once
* the message has been dismissed, via entityItemMovement.
*
* @param player Pointer to the player entity.
* @param target Pointer to the item entity being picked up.
*/
void entityItemInteract(entity_t *player, entity_t *target);
/**
* Update callback for item entities. Removes the entity once its pickup
* message has been dismissed. No-op if the item hasn't been collected.
*
* @param entity Pointer to the entity structure to update.
*/
void entityItemMovement(entity_t *entity);
+6 -5
View File
@@ -11,11 +11,12 @@
#include "util/memory.h" #include "util/memory.h"
#include "time/time.h" #include "time/time.h"
#include "ui/focus/uifocus.h" #include "ui/focus/uifocus.h"
#include "ui/frame/uisettings.h" #include "ui/frame/game/uigamemenu.h"
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
void playerInit(entity_t *entity) { void playerInit(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL"); assertNotNull(entity, "Entity pointer cannot be NULL");
entity->globalId = ENTITY_GLOBAL_ID_PLAYER;
} }
bool_t playerCanInteract(entity_t *entity) { bool_t playerCanInteract(entity_t *entity) {
@@ -26,14 +27,14 @@ void playerInput(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL"); assertNotNull(entity, "Entity pointer cannot be NULL");
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return; if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return;
// Toggle settings on pause // Toggle game menu on pause
if(uiSettingsIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) { if(uiGameMenuIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) {
uiSettingsClose(); uiGameMenuClose();
} else if( } else if(
inputPressed(INPUT_ACTION_PAUSE) && inputPressed(INPUT_ACTION_PAUSE) &&
entity->animation == ENTITY_ANIM_IDLE entity->animation == ENTITY_ANIM_IDLE
) { ) {
uiSettingsOpen(); uiGameMenuOpen();
} }
// Can player act? // Can player act?
+1
View File
@@ -7,6 +7,7 @@
#pragma once #pragma once
#include "input/input.h" #include "input/input.h"
#include "entitydir.h"
typedef struct entity_s entity_t; typedef struct entity_s entity_t;
+1
View File
@@ -8,6 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
inventory.c inventory.c
backpack.c backpack.c
itemgive.c
) )
# Item Definitions # Item Definitions
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "itemgive.h"
#include "rpg/item/backpack.h"
#include "ui/rpg/uitextboxmain.h"
#include "util/string.h"
void itemGive(const itemid_t item, const uint8_t quantity) {
backpackAdd(item, quantity);
char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS];
stringFormat(
msg,
ITEM_GIVE_MESSAGE_MAX_CHARS - 1,
"Received %s x%u",
ITEMS[item].name,
(uint32_t)quantity
);
uiTextboxMainSetText(msg);
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/item/item.h"
#define ITEM_GIVE_MESSAGE_MAX_CHARS 256
/**
* Adds a quantity of an item to the player's backpack and shows a
* "Received <item> x<quantity>" message in the main textbox.
*
* @param item The item ID to give.
* @param quantity The quantity to give.
*/
void itemGive(const itemid_t item, const uint8_t quantity);
+2
View File
@@ -8,7 +8,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
chunk.c chunk.c
map.c map.c
maparea.c
worldpos.c worldpos.c
tile.c tile.c
tileshape.c
) )
+1 -5
View File
@@ -8,11 +8,7 @@
#include "chunk.h" #include "chunk.h"
uint32_t chunkGetTileIndex(const chunkpos_t position) { uint32_t chunkGetTileIndex(const chunkpos_t position) {
return ( return (position.y * CHUNK_WIDTH) + position.x;
(position.z * CHUNK_WIDTH * CHUNK_HEIGHT) +
(position.y * CHUNK_WIDTH) +
position.x
);
} }
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) { bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) {
+75 -7
View File
@@ -12,8 +12,8 @@
#include "asset/loader/assetloader.h" #include "asset/loader/assetloader.h"
#include "console/console.h" #include "console/console.h"
#include "event/event.h" #include "event/event.h"
#include "rpg/entity/entity.h"
#include "util/string.h" #include "util/string.h"
#include "rpg/entity/global/entityglobal.h"
map_t MAP; map_t MAP;
@@ -141,7 +141,7 @@ void mapChunkUnload(chunk_t *chunk) {
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(chunk->entities[i] == 0xFF) continue; if(chunk->entities[i] == 0xFF) continue;
entity_t *entity = &ENTITIES[chunk->entities[i]]; entity_t *entity = &ENTITIES[chunk->entities[i]];
if(entity->type == ENTITY_TYPE_PLAYER) { if(!entityCanUnload(entity)) {
entitySetChunk(entity, 0xFF); entitySetChunk(entity, 0xFF);
} else { } else {
entity->type = ENTITY_TYPE_NULL; entity->type = ENTITY_TYPE_NULL;
@@ -188,7 +188,10 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
); );
if(!assetFileExists(name)) { if(!assetFileExists(name)) {
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles)); for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
// chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 };
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
}
errorOk(); errorOk();
} }
@@ -229,17 +232,82 @@ chunk_t *mapGetChunk(const uint8_t index) {
} }
tile_t mapGetTile(const worldpos_t position) { tile_t mapGetTile(const worldpos_t position) {
if(!mapIsLoaded()) return TILE_SHAPE_NULL; if(!mapIsLoaded()) return TILE_NULL;
chunkpos_t chunkPos; chunkpos_t chunkPos;
worldPosToChunkPos(&position, &chunkPos); worldPosToChunkPos(&position, &chunkPos);
chunkindex_t chunkIndex = mapGetChunkIndexAt(chunkPos); chunkindex_t chunkIndex = mapGetChunkIndexAt(chunkPos);
if(chunkIndex == -1) return TILE_SHAPE_NULL; if(chunkIndex == -1) return TILE_NULL;
chunk_t *chunk = mapGetChunk(chunkIndex); chunk_t *chunk = mapGetChunk(chunkIndex);
assertNotNull(chunk, "Chunk pointer cannot be NULL"); assertNotNull(chunk, "Chunk pointer cannot be NULL");
chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position); chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position);
return chunk->tiles[tileIndex]; tile_t tile = chunk->tiles[tileIndex];
if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL;
return tile;
}
bool_t mapGetWalkableZNear(
const worldunit_t x,
const worldunit_t y,
const worldunit_t nearZ,
worldunit_t *outZ
) {
assertNotNull(outZ, "Output Z pointer cannot be NULL");
const worldunit_t candidates[] = {
nearZ, (worldunit_t)(nearZ + 1), (worldunit_t)(nearZ - 1)
};
for(uint8_t i = 0; i < 3; i++) {
const worldpos_t pos = { x, y, candidates[i] };
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) continue;
*outZ = candidates[i];
return true;
}
return false;
}
entity_t * mapSpawnEntity(
const entityglobalid_t globalId,
const worldpos_t position
) {
assertTrue(
globalId > ENTITY_GLOBAL_ID_START,
"mapSpawnEntity requires a global ID greater than ENTITY_GLOBAL_ID_START"
);
assertTrue(
globalId < ENTITY_GLOBAL_LIST_COUNT,
"Global ID is out of range for entity global init callbacks"
);
// Already spawned? Reuse the existing entity instead of making a
// duplicate - two entities must never share a global ID.
entity_t *existing = entityGetByGlobalId(globalId);
if(existing != NULL) return existing;
// See if there is a callback for this entity first.
const entityglobaldef_t *def = &ENTITY_GLOBAL_LIST[globalId];
assertNotNull(def, "No global entity definition for this ID");
assertNotNull(def->callback, "No callback registered for this global ID");
// Get available entity.
uint8_t index = entityGetAvailable();
assertTrue(index != 0xFF, "No available entity slots for mapSpawnEntity");
// Get the pointer and do the init.
entity_t *entity = &ENTITIES[index];
entityInit(entity, def->type);
entity->globalId = globalId;
entityPositionSet(entity, position);// Also assigns the entity's chunk.
// Invoke the callback to initialize the entity.
entityglobalcreate_t create = {
.entity = entity,
.position = position
};
def->callback(&create);
return entity;
} }
void mapRebuildChunkOrder() { void mapRebuildChunkOrder() {
@@ -273,7 +341,7 @@ void mapChunkLoadError(void *params, void *user) {
); );
assetUnlockEntry(chunk->dcfEntry); assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles)); memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
} }
void mapChunkLoaded(void *params, void *user) { void mapChunkLoaded(void *params, void *user) {
+36 -14
View File
@@ -8,6 +8,7 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include "rpg/overworld/chunk.h" #include "rpg/overworld/chunk.h"
#include "rpg/entity/entity.h"
#define MAP_FILE_PATH_MAX 128 #define MAP_FILE_PATH_MAX 128
@@ -36,18 +37,6 @@ errorret_t mapInit();
*/ */
bool_t mapIsLoaded(); bool_t mapIsLoaded();
/**
* Loads a map from the given file path.
*
* @param path The file path.
* @param position The initial chunk position.
* @return An error code.
*/
// errorret_t mapLoad(
// const char_t *path,
// const chunkpos_t position
// );
/** /**
* Updates the map. * Updates the map.
* *
@@ -128,8 +117,41 @@ chunk_t * mapGetChunk(const uint8_t chunkIndex);
/** /**
* Gets the tile at the given world position. * Gets the tile at the given world position.
* *
* @param position The world position. * @param position The world position.
* @return The tile at that position, or TILE_NULL if the chunk is unloaded. * @return The tile at that position, or TILE_NULL if the chunk is unloaded.
*/ */
tile_t mapGetTile(const worldpos_t position); tile_t mapGetTile(const worldpos_t position);
/**
* Finds the closest walkable Z layer to nearZ at the given X/Y. Checks
* nearZ first, then nearZ + 1, then nearZ - 1, since ramps only ever
* change height by one Z layer between adjacent tiles.
*
* @param x The world X coordinate to check.
* @param y The world Y coordinate to check.
* @param nearZ The reference Z layer to search outward from.
* @param outZ Output pointer, set to the resolved Z layer on success.
* @return true if a walkable tile was found, false otherwise.
*/
bool_t mapGetWalkableZNear(
const worldunit_t x,
const worldunit_t y,
const worldunit_t nearZ,
worldunit_t *outZ
);
/**
* Spawns a global (persistent) entity into the world at the given position.
* Asserts globalId is greater than ENTITY_GLOBAL_ID_START - use entityInit
* directly for ephemeral, non-global entities.
*
* @param globalId The global entity ID to assign, must be greater than
* ENTITY_GLOBAL_ID_START.
* @param position The world position to spawn the entity at.
* @return Pointer to the spawned entity.
*/
entity_t * mapSpawnEntity(
const entityglobalid_t globalId,
const worldpos_t position
);
+150
View File
@@ -0,0 +1,150 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "maparea.h"
#include "assert/assert.h"
#include "util/math.h"
#include "util/memory.h"
#include "rpg/overworld/map.h"
maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
void mapAreaInit(
maparea_t *area,
const worldpos_t min,
const worldpos_t max,
const mapareacallback_t callback,
const uint8_t notify,
const uint8_t trigger
) {
assertNotNull(area, "Map area pointer cannot be NULL");
assertNotNull(callback, "Map area callback cannot be NULL");
area->min.x = mathMin(min.x, max.x);
area->min.y = mathMin(min.y, max.y);
area->min.z = mathMin(min.z, max.z);
area->max.x = mathMax(min.x, max.x);
area->max.y = mathMax(min.y, max.y);
area->max.z = mathMax(min.z, max.z);
area->callback = callback;
area->notify = notify;
area->trigger = trigger;
area->triggerCount = 0;
memorySet(area->entities, 0, sizeof(area->entities));
}
bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position) {
assertNotNull(area, "Map area pointer cannot be NULL");
return (
position.x >= area->min.x && position.x <= area->max.x &&
position.y >= area->min.y && position.y <= area->max.y &&
position.z >= area->min.z && position.z <= area->max.z
);
}
bool_t mapAreaIsChunkOverlappingOrInside(
const maparea_t *area,
const chunk_t *chunk
) {
assertNotNull(area, "Map area pointer cannot be NULL");
assertNotNull(chunk, "Chunk pointer cannot be NULL");
worldpos_t chunkMin, chunkMax;
chunkPosToWorldPos(&chunk->position, &chunkMin);
chunkMax.x = chunkMin.x + CHUNK_WIDTH - 1;
chunkMax.y = chunkMin.y + CHUNK_HEIGHT - 1;
chunkMax.z = chunkMin.z + CHUNK_DEPTH - 1;
return (
chunkMin.x <= area->max.x && area->min.x <= chunkMax.x &&
chunkMin.y <= area->max.y && area->min.y <= chunkMax.y &&
chunkMin.z <= area->max.z && area->min.z <= chunkMax.z
);
}
bool_t mapAreaCanUnload(const maparea_t *area) {
assertNotNull(area, "Map area pointer cannot be NULL");
for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) {
if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false;
}
return true;
}
bool_t mapAreaShouldNotify(const maparea_t *area, const entity_t *entity) {
assertNotNull(area, "Map area pointer cannot be NULL");
assertNotNull(entity, "Entity pointer cannot be NULL");
switch(entity->type) {
case ENTITY_TYPE_PLAYER:
return (area->notify & MAP_AREA_NOTIFY_PLAYER) != 0;
case ENTITY_TYPE_NPC:
return (area->notify & MAP_AREA_NOTIFY_NPC) != 0;
default:
return false;
}
}
uint8_t mapAreaAdd(
const worldpos_t min,
const worldpos_t max,
const mapareacallback_t callback,
const uint8_t notify,
const uint8_t trigger
) {
for(uint8_t i = 0; i < MAP_AREA_COUNT_MAX; i++) {
if(MAP_AREAS[i].callback != NULL) continue;
mapAreaInit(&MAP_AREAS[i], min, max, callback, notify, trigger);
return i;
}
assertUnreachable("No available map area slots");
return 0xFF;
}
void mapAreaRemove(const uint8_t id) {
assertTrue(id < MAP_AREA_COUNT_MAX, "Map area ID is out of range");
MAP_AREAS[id].callback = NULL;
}
void mapAreaCheckEntity(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
for(uint8_t i = 0; i < MAP_AREA_COUNT_MAX; i++) {
maparea_t *area = &MAP_AREAS[i];
if(area->callback == NULL) continue;
if(!mapAreaShouldNotify(area, entity)) continue;
bool_t wasInside = area->entities[entity->id] != 0;
bool_t isInside = mapAreaIsInside(area, entity->position);
uint8_t trigger = 0;
if(isInside && !wasInside) {
area->entities[entity->id] = 1;
trigger = MAP_TRIGGER_ENTER;
} else if(isInside && wasInside) {
trigger = MAP_TRIGGER_STEP;
} else if(!isInside && wasInside) {
area->entities[entity->id] = 0;
trigger = MAP_TRIGGER_EXIT;
}
if(trigger == 0 || !(area->trigger & trigger)) continue;
area->triggerCount++;
area->callback(entity, trigger);
}
}
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger) {
}
+161
View File
@@ -0,0 +1,161 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "worldpos.h"
#include "rpg/entity/entity.h"
typedef struct chunk_s chunk_t;
typedef struct maparea_s maparea_t;
#define MAP_AREA_COUNT_MAX 32
#define MAP_AREA_NOTIFY_PLAYER (1 << 0)
#define MAP_AREA_NOTIFY_NPC (1 << 1)
#define MAP_AREA_NOTIFY_ALL (MAP_AREA_NOTIFY_PLAYER | MAP_AREA_NOTIFY_NPC)
#define MAP_TRIGGER_STEP (1 << 0)
#define MAP_TRIGGER_ENTER (1 << 1)
#define MAP_TRIGGER_EXIT (1 << 2)
#define MAP_TRIGGER_ALL (MAP_TRIGGER_STEP | MAP_TRIGGER_ENTER | MAP_TRIGGER_EXIT)
/**
* Callback invoked for a map area.
*
* @param entity Pointer to the entity associated with the callback.
* @param trigger Which MAP_TRIGGER_* condition invoked the callback.
*/
typedef void (*mapareacallback_t)(entity_t *entity, const uint8_t trigger);
typedef struct maparea_s {
worldpos_t min;
worldpos_t max;
mapareacallback_t callback;
uint8_t notify;
uint8_t trigger;
uint8_t entities[ENTITY_COUNT];
// Incremented every time this area's callback is invoked. Lets other
// systems (e.g. cutscenes) detect "has this area fired since I last
// checked" without needing to be the callback themselves.
uint32_t triggerCount;
} maparea_t;
extern maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
/**
* Initializes a map area with the given bounds, callback, notify flags
* and trigger flags.
*
* @param area Pointer to the map area to initialize.
* @param min The minimum world position of the area.
* @param max The maximum world position of the area.
* @param callback The callback to invoke for this area. Must not be NULL.
* @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types
* should trigger the callback.
* @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should
* invoke the callback.
*/
void mapAreaInit(
maparea_t *area,
const worldpos_t min,
const worldpos_t max,
const mapareacallback_t callback,
const uint8_t notify,
const uint8_t trigger
);
/**
* Checks whether a world position falls within a map area's bounds
* (inclusive on all axes).
*
* @param area Pointer to the map area to check.
* @param position The world position to check.
* @returns true if the position is inside the area.
*/
bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position);
/**
* Checks whether a chunk's full footprint overlaps a map area's bounds,
* rather than just testing the chunk's origin corner.
*
* @param area Pointer to the map area to check.
* @param chunk Pointer to the chunk to check.
* @returns true if the chunk overlaps the area.
*/
bool_t mapAreaIsChunkOverlappingOrInside(
const maparea_t *area,
const chunk_t *chunk
);
/**
* Checks whether a map area is safe to unload, i.e. no entity currently
* occupies a position within its bounds.
*
* @param area Pointer to the map area to check.
* @returns true if no entity is inside the area.
*/
bool_t mapAreaCanUnload(const maparea_t *area);
/**
* Checks whether an entity's type is included in a map area's notify flags.
*
* @param area Pointer to the map area to check.
* @param entity Pointer to the entity to check.
* @returns true if the entity's type should trigger this area's callback.
*/
bool_t mapAreaShouldNotify(const maparea_t *area, const entity_t *entity);
/**
* Adds a map area to the global MAP_AREAS list, in the first free slot.
* A slot is considered free if its callback is NULL.
*
* @param min The minimum world position of the area.
* @param max The maximum world position of the area.
* @param callback The callback to invoke for this area. Must not be NULL.
* @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types
* should trigger the callback.
* @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should
* invoke the callback.
* @returns The ID of the newly added map area.
*/
uint8_t mapAreaAdd(
const worldpos_t min,
const worldpos_t max,
const mapareacallback_t callback,
const uint8_t notify,
const uint8_t trigger
);
/**
* Removes a map area from the global MAP_AREAS list, freeing its slot by
* clearing its callback to NULL.
*
* @param id The ID of the map area to remove.
*/
void mapAreaRemove(const uint8_t id);
/**
* Checks every active map area against an entity's current position,
* for areas whose notify flags include the entity's type. Invokes the
* area's callback with MAP_TRIGGER_ENTER the frame the entity first
* becomes inside, MAP_TRIGGER_STEP on every subsequent frame it remains
* inside, and MAP_TRIGGER_EXIT the frame it leaves.
*
* @param entity Pointer to the entity to check.
*/
void mapAreaCheckEntity(entity_t *entity);
/**
* A map area callback that does nothing. Useful for areas whose only
* purpose is to be waited on (see CUTSCENE_MAP_AREA_WAIT), since the
* wait is driven by triggerCount rather than callback logic.
*
* @param entity Pointer to the entity associated with the callback.
* @param trigger Which MAP_TRIGGER_* condition invoked the callback.
*/
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger);
-31
View File
@@ -6,34 +6,3 @@
*/ */
#include "tile.h" #include "tile.h"
bool_t tileIsWalkable(const tile_t tile) {
switch(tile) {
case TILE_SHAPE_NULL:
return false;
default:
return true;
}
}
bool_t tileIsRamp(const tile_t tile) {
switch(tile) {
case TILE_SHAPE_RAMP_NORTH:
case TILE_SHAPE_RAMP_SOUTH:
case TILE_SHAPE_RAMP_EAST:
case TILE_SHAPE_RAMP_WEST:
case TILE_SHAPE_RAMP_NORTHEAST:
case TILE_SHAPE_RAMP_NORTHWEST:
case TILE_SHAPE_RAMP_SOUTHEAST:
case TILE_SHAPE_RAMP_SOUTHWEST:
case TILE_SHAPE_RAMP_NORTHEAST_INNER:
case TILE_SHAPE_RAMP_NORTHWEST_INNER:
case TILE_SHAPE_RAMP_SOUTHEAST_INNER:
case TILE_SHAPE_RAMP_SOUTHWEST_INNER:
return true;
default:
return false;
}
}
+8 -34
View File
@@ -6,41 +6,15 @@
*/ */
#pragma once #pragma once
#include "rpg/entity/entitydir.h" #include "tileshape.h"
typedef struct {
tileshape_t shape;
typedef enum { // Local Z layer (0..CHUNK_DEPTH-1) this tile occupies within its chunk's
TILE_SHAPE_NULL = 0, // depth slab. Used for entity navigation since chunk_t only stores one
// tile per X/Y column rather than one per X/Y/Z.
TILE_SHAPE_GROUND = 1, uint8_t z;
TILE_SHAPE_RAMP_NORTH = 2,
TILE_SHAPE_RAMP_EAST = 3,
TILE_SHAPE_RAMP_SOUTH = 4,
TILE_SHAPE_RAMP_WEST = 5,
TILE_SHAPE_RAMP_NORTHEAST = 6,
TILE_SHAPE_RAMP_NORTHWEST = 7,
TILE_SHAPE_RAMP_SOUTHEAST = 8,
TILE_SHAPE_RAMP_SOUTHWEST = 9,
TILE_SHAPE_RAMP_NORTHEAST_INNER = 10,
TILE_SHAPE_RAMP_NORTHWEST_INNER = 11,
TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12,
TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13,
TILE_SHAPE_COUNT
} tile_t; } tile_t;
/** #define TILE_NULL ((tile_t){ .shape = TILE_SHAPE_NULL, .z = 0 })
* Returns whether or not the given tile is walkable.
*
* @param tile The tile to check.
* @return bool_t True if walkable, false if not.
*/
bool_t tileIsWalkable(const tile_t tile);
/**
* Returns whether or not the given tile is a ramp tile.
*
* @param tile The tile to check.
* @return bool_t True if ramp, false if not.
*/
bool_t tileIsRamp(const tile_t tile);
+39
View File
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "tileshape.h"
bool_t tileShapeIsWalkable(const tileshape_t shape) {
switch(shape) {
case TILE_SHAPE_NULL:
return false;
default:
return true;
}
}
bool_t tileShapeIsRamp(const tileshape_t shape) {
switch(shape) {
case TILE_SHAPE_RAMP_NORTH:
case TILE_SHAPE_RAMP_SOUTH:
case TILE_SHAPE_RAMP_EAST:
case TILE_SHAPE_RAMP_WEST:
case TILE_SHAPE_RAMP_NORTHEAST:
case TILE_SHAPE_RAMP_NORTHWEST:
case TILE_SHAPE_RAMP_SOUTHEAST:
case TILE_SHAPE_RAMP_SOUTHWEST:
case TILE_SHAPE_RAMP_NORTHEAST_INNER:
case TILE_SHAPE_RAMP_NORTHWEST_INNER:
case TILE_SHAPE_RAMP_SOUTHEAST_INNER:
case TILE_SHAPE_RAMP_SOUTHWEST_INNER:
return true;
default:
return false;
}
}
+45
View File
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/entity/entitydir.h"
typedef enum {
TILE_SHAPE_NULL = 0,
TILE_SHAPE_GROUND = 1,
TILE_SHAPE_RAMP_NORTH = 2,
TILE_SHAPE_RAMP_EAST = 3,
TILE_SHAPE_RAMP_SOUTH = 4,
TILE_SHAPE_RAMP_WEST = 5,
TILE_SHAPE_RAMP_NORTHEAST = 6,
TILE_SHAPE_RAMP_NORTHWEST = 7,
TILE_SHAPE_RAMP_SOUTHEAST = 8,
TILE_SHAPE_RAMP_SOUTHWEST = 9,
TILE_SHAPE_RAMP_NORTHEAST_INNER = 10,
TILE_SHAPE_RAMP_NORTHWEST_INNER = 11,
TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12,
TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13,
TILE_SHAPE_COUNT
} tileshape_t;
/**
* Returns whether or not the given tile shape is a ramp.
*
* @param shape The tile shape to check.
* @return bool_t True if ramp, false if not.
*/
bool_t tileShapeIsRamp(const tileshape_t shape);
/**
* Returns whether or not the given tile shape is walkable.
*
* @param shape The tile shape to check.
* @return bool_t True if walkable, false if not.
*/
bool_t tileShapeIsWalkable(const tileshape_t shape);
+13 -12
View File
@@ -47,7 +47,7 @@ void worldPosToChunkPos(const worldpos_t* worldPos, chunkpos_t* out) {
chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos) { chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos) {
assertNotNull(worldPos, "World position pointer cannot be NULL"); assertNotNull(worldPos, "World position pointer cannot be NULL");
uint8_t localX, localY, localZ; uint8_t localX, localY;
if(worldPos->x < 0) { if(worldPos->x < 0) {
localX = (uint8_t)( localX = (uint8_t)(
(CHUNK_WIDTH - 1) - ((-worldPos->x - 1) % CHUNK_WIDTH) (CHUNK_WIDTH - 1) - ((-worldPos->x - 1) % CHUNK_WIDTH)
@@ -64,18 +64,8 @@ chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos) {
localY = (uint8_t)(worldPos->y % CHUNK_HEIGHT); localY = (uint8_t)(worldPos->y % CHUNK_HEIGHT);
} }
if(worldPos->z < 0) {
localZ = (uint8_t)(
(CHUNK_DEPTH - 1) - ((-worldPos->z - 1) % CHUNK_DEPTH)
);
} else {
localZ = (uint8_t)(worldPos->z % CHUNK_DEPTH);
}
chunktileindex_t chunkTileIndex = (chunktileindex_t)( chunktileindex_t chunkTileIndex = (chunktileindex_t)(
(localZ * CHUNK_WIDTH * CHUNK_HEIGHT) + (localY * CHUNK_WIDTH) + localX
(localY * CHUNK_WIDTH) +
localX
); );
assertTrue( assertTrue(
chunkTileIndex < CHUNK_TILE_COUNT, chunkTileIndex < CHUNK_TILE_COUNT,
@@ -84,6 +74,17 @@ chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos) {
return chunkTileIndex; return chunkTileIndex;
} }
uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos) {
assertNotNull(worldPos, "World position pointer cannot be NULL");
if(worldPos->z < 0) {
return (uint8_t)(
(CHUNK_DEPTH - 1) - ((-worldPos->z - 1) % CHUNK_DEPTH)
);
}
return (uint8_t)(worldPos->z % CHUNK_DEPTH);
}
chunkindex_t chunkPosToIndex(const chunkpos_t* pos) { chunkindex_t chunkPosToIndex(const chunkpos_t* pos) {
assertNotNull(pos, "Chunk position pointer cannot be NULL"); assertNotNull(pos, "Chunk position pointer cannot be NULL");
+18 -3
View File
@@ -19,9 +19,11 @@
#define CHUNK_WIDTH 16 #define CHUNK_WIDTH 16
#define CHUNK_HEIGHT 16 #define CHUNK_HEIGHT 16
#define CHUNK_DEPTH 4 #define CHUNK_DEPTH 8
#define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH) // Chunks store one tile per X/Y column, not one per X/Y/Z - each tile
// records its own local Z layer (see tile_t) instead.
#define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT)
#define MAP_CHUNK_WIDTH 3 #define MAP_CHUNK_WIDTH 3
#define MAP_CHUNK_HEIGHT 3 #define MAP_CHUNK_HEIGHT 3
@@ -48,6 +50,10 @@ typedef uint32_t chunktileindex_t;
typedef int32_t worldunits_t; typedef int32_t worldunits_t;
typedef int32_t chunkunits_t; typedef int32_t chunkunits_t;
typedef struct worldpos2d_s {
worldunit_t x, y;
} worldpos2d_t;
typedef struct worldpos_s { typedef struct worldpos_s {
worldunit_t x, y, z; worldunit_t x, y, z;
} worldpos_t; } worldpos_t;
@@ -84,12 +90,21 @@ void worldPosToChunkPos(const worldpos_t* worldPos, chunkpos_t* out);
/** /**
* Converts a position in world-space to an index inside a chunk that the tile * Converts a position in world-space to an index inside a chunk that the tile
* resides in. * resides in.
* *
* @param worldPos The world position. * @param worldPos The world position.
* @return The tile index within the chunk. * @return The tile index within the chunk.
*/ */
chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos); chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos);
/**
* Converts a world-space Z coordinate to the local Z layer (0..CHUNK_DEPTH-1)
* within the chunk that owns it, for comparison against tile_t.z.
*
* @param worldPos The world position.
* @return The local Z layer within the owning chunk.
*/
uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos);
/** /**
* Converts a chunk position to a world position. * Converts a chunk position to a world position.
* *
+29 -24
View File
@@ -8,20 +8,30 @@
#include "rpg.h" #include "rpg.h"
#include "entity/entity.h" #include "entity/entity.h"
#include "rpg/entity/npc/npcpath.h" #include "rpg/entity/npc/npcpath.h"
#include "rpg/entity/item/entityitem.h"
#include "rpg/overworld/map.h" #include "rpg/overworld/map.h"
#include "rpg/overworld/maparea.h"
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
#include "rpg/cutscene/scene/testcutscene.h" #include "rpg/cutscene/scene/testcutscene.h"
#include "rpg/item/backpack.h" #include "rpg/item/backpack.h"
#include "rpg/battle/party.h"
#include "time/time.h" #include "time/time.h"
#include "rpgcamera.h" #include "rpgcamera.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "console/console.h"
void rpgTestAreaCallback(entity_t *entity, const uint8_t trigger) {
consolePrint("rpgTestAreaCallback: trigger=%u", trigger);
}
errorret_t rpgInit(void) { errorret_t rpgInit(void) {
memoryZero(ENTITIES, sizeof(ENTITIES)); memoryZero(ENTITIES, sizeof(ENTITIES));
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
backpackInit(); backpackInit();
partyInit();
cutsceneSystemInit(); cutsceneSystemInit();
errorChain(mapInit()); errorChain(mapInit());
@@ -35,34 +45,29 @@ errorret_t rpgInit(void) {
assertTrue(entIndex != 0xFF, "No available entity slots!."); assertTrue(entIndex != 0xFF, "No available entity slots!.");
entity_t *ent = &ENTITIES[entIndex]; entity_t *ent = &ENTITIES[entIndex];
entityInit(ent, ENTITY_TYPE_PLAYER); entityInit(ent, ENTITY_TYPE_PLAYER);
entityPositionSet(ent, (worldpos_t){ 10, 2, 0 }); entityPositionSet(ent, (worldpos_t){ 10, 2, 0 });// Also assigns the chunk.
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY; RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
RPG_CAMERA.followEntity.followEntityId = ent->id; RPG_CAMERA.followEntity.followEntityId = ent->id;
{
chunkpos_t cp;
worldPosToChunkPos(&ent->position, &cp);
chunkindex_t ci = mapGetChunkIndexAt(cp);
if(ci != -1) entitySetChunk(ent, (uint8_t)ci);
}
uint8_t npcIndex = entityGetAvailable(); mapSpawnEntity(3, (worldpos_t){ 8, 8, 1 });
entity_t *npc = &ENTITIES[npcIndex];
entityInit(npc, ENTITY_TYPE_NPC);
npcSetMoveType(npc, NPC_MOVE_TYPE_PATH);
entityPositionSet(npc, (worldpos_t){ 8, 8, 1 });
npc->interact.type = ENTITY_INTERACT_CUTSCENE;
npc->interact.data.cutscene = CUTSCENE_REFERENCE(TEST_TWO);
{
chunkpos_t cp;
worldPosToChunkPos(&npc->position, &cp);
chunkindex_t ci = mapGetChunkIndexAt(cp);
if(ci != -1) entitySetChunk(npc, (uint8_t)ci);
}
npcPathAddNode(&npc->data.npc, (worldpos_t){ 4, 4, 0 }); // TEST: Place an item entity.
npcPathAddNode(&npc->data.npc, (worldpos_t){ 10, 10, 1 }); uint8_t itemEntIndex = entityGetAvailable();
npcPathAddNode(&npc->data.npc, (worldpos_t){ 4, 4, 0 }); assertTrue(itemEntIndex != 0xFF, "No available entity slots!.");
npcPathAddNode(&npc->data.npc, (worldpos_t){ 10, 10, 1 }); entity_t *itemEnt = &ENTITIES[itemEntIndex];
entityInit(itemEnt, ENTITY_TYPE_ITEM);
entityItemSet(itemEnt, ITEM_ID_POTION, 1);
entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 });
// TEST: Create a test map area.
uint8_t areaIndex = mapAreaAdd(
(worldpos_t){ 11, 3, 0 },
(worldpos_t){ 16, 9, 10 },
rpgTestAreaCallback,
MAP_AREA_NOTIFY_ALL,
MAP_TRIGGER_ENTER | MAP_TRIGGER_EXIT
);
assertTrue(areaIndex != 0xFF, "No available map area slots!.");
// All Good! // All Good!
errorOk(); errorOk();
+1 -1
View File
@@ -28,7 +28,7 @@ errorret_t rpgUpdate(void);
/** /**
* Dispose of the RPG subsystem. * Dispose of the RPG subsystem.
* *
* @return An error code. * @return An error code.
*/ */
errorret_t rpgDispose(void); errorret_t rpgDispose(void);
+12 -2
View File
@@ -31,6 +31,7 @@ uifocusitem_t * uiFocusPush(
uifocusitemcallback_t selected, uifocusitemcallback_t selected,
uifocusitemcallback_t changed, uifocusitemcallback_t changed,
uifocusitemcallback_t closed, uifocusitemcallback_t closed,
uifocusitemdirectioncallback_t direction,
void *user void *user
) { ) {
assertTrue( assertTrue(
@@ -47,6 +48,7 @@ uifocusitem_t * uiFocusPush(
item->selected = selected; item->selected = selected;
item->changed = changed; item->changed = changed;
item->closed = closed; item->closed = closed;
item->direction = direction;
item->user = user; item->user = user;
UI_FOCUS.count++; UI_FOCUS.count++;
UI_FOCUS.pushedThisTick = true; UI_FOCUS.pushedThisTick = true;
@@ -118,6 +120,14 @@ void uiFocusMoveDirection(
} }
} }
void uiFocusHandleDirection(
uifocusitem_t *item,
const uifocusdirection_t dir
) {
if(item->direction != NULL && item->direction(item, dir)) return;
uiFocusMoveDirection(item, dir);
}
void uiFocusUpdate(void) { void uiFocusUpdate(void) {
if(UI_FOCUS.count == 0) return; if(UI_FOCUS.count == 0) return;
@@ -150,7 +160,7 @@ void uiFocusUpdate(void) {
if(!inputPressed(m->action)) continue; if(!inputPressed(m->action)) continue;
UI_FOCUS.direction = m->direction; UI_FOCUS.direction = m->direction;
UI_FOCUS.timeHeld = 0.0f; UI_FOCUS.timeHeld = 0.0f;
uiFocusMoveDirection(item, m->direction); uiFocusHandleDirection(item, m->direction);
return; return;
} }
@@ -176,5 +186,5 @@ void uiFocusUpdate(void) {
if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY) return; if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY) return;
if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY + UI_FOCUS_HOLD_REPEAT) return; if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY + UI_FOCUS_HOLD_REPEAT) return;
UI_FOCUS.timeHeld = UI_FOCUS_HOLD_DELAY; UI_FOCUS.timeHeld = UI_FOCUS_HOLD_DELAY;
uiFocusMoveDirection(item, UI_FOCUS.direction); uiFocusHandleDirection(item, UI_FOCUS.direction);
} }
+16 -8
View File
@@ -22,14 +22,6 @@
*/ */
#define UI_FOCUS_HOLD_REPEAT 0.1f #define UI_FOCUS_HOLD_REPEAT 0.1f
typedef enum {
UI_FOCUS_DIRECTION_NONE,
UI_FOCUS_DIRECTION_UP,
UI_FOCUS_DIRECTION_DOWN,
UI_FOCUS_DIRECTION_LEFT,
UI_FOCUS_DIRECTION_RIGHT
} uifocusdirection_t;
typedef struct { typedef struct {
inputaction_t action; inputaction_t action;
uifocusdirection_t direction; uifocusdirection_t direction;
@@ -72,6 +64,8 @@ void uiFocusInit(void);
* @param selected Called when the user selects the focused cell. * @param selected Called when the user selects the focused cell.
* @param changed Called when the focused cell position changes. * @param changed Called when the focused cell position changes.
* @param closed Called when this focus item is popped. * @param closed Called when this focus item is popped.
* @param direction Called on a direction press/hold before the default
* cell-to-cell movement is applied; may be NULL.
* @param user Arbitrary pointer stored on the item before changed fires. * @param user Arbitrary pointer stored on the item before changed fires.
* @returns Pointer to the newly pushed focus item. * @returns Pointer to the newly pushed focus item.
*/ */
@@ -81,6 +75,7 @@ uifocusitem_t * uiFocusPush(
uifocusitemcallback_t selected, uifocusitemcallback_t selected,
uifocusitemcallback_t changed, uifocusitemcallback_t changed,
uifocusitemcallback_t closed, uifocusitemcallback_t closed,
uifocusitemdirectioncallback_t direction,
void *user void *user
); );
@@ -118,6 +113,19 @@ void uiFocusMoveDirection(
const uifocusdirection_t dir const uifocusdirection_t dir
); );
/**
* Handles a direction press/hold for the given item: gives the item's
* direction callback (if any) first refusal, falling back to the
* default cell-to-cell movement if it's unset or returns false.
*
* @param item The focus item to move.
* @param dir Direction that was pressed or held.
*/
void uiFocusHandleDirection(
uifocusitem_t *item,
const uifocusdirection_t dir
);
/** /**
* Updates the focus system. Handles first-press movement and * Updates the focus system. Handles first-press movement and
* held-direction repeating. Called once per game tick. * held-direction repeating. Called once per game tick.
+24
View File
@@ -10,6 +10,14 @@
typedef struct uifocusitem_s uifocusitem_t; typedef struct uifocusitem_s uifocusitem_t;
typedef enum {
UI_FOCUS_DIRECTION_NONE,
UI_FOCUS_DIRECTION_UP,
UI_FOCUS_DIRECTION_DOWN,
UI_FOCUS_DIRECTION_LEFT,
UI_FOCUS_DIRECTION_RIGHT
} uifocusdirection_t;
/** /**
* Callback invoked when a focus item's selected cell changes. * Callback invoked when a focus item's selected cell changes.
* *
@@ -17,6 +25,21 @@ typedef struct uifocusitem_s uifocusitem_t;
*/ */
typedef bool_t (*uifocusitemcallback_t)(const uifocusitem_t *item); typedef bool_t (*uifocusitemcallback_t)(const uifocusitem_t *item);
/**
* Callback invoked when a direction is pressed or held-repeated while
* this item is focused, before the default cell-to-cell movement is
* applied.
*
* @param item The focus item that is currently focused.
* @param direction The direction that was pressed.
* @returns True if the direction was fully handled and the default
* grid movement should be skipped; false to fall through to it.
*/
typedef bool_t (*uifocusitemdirectioncallback_t)(
const uifocusitem_t *item,
const uifocusdirection_t direction
);
/** /**
* A single entry on the UI focus stack. Tracks the focused cell * A single entry on the UI focus stack. Tracks the focused cell
* within a grid of cols x rows, and the current x/y position. * within a grid of cols x rows, and the current x/y position.
@@ -29,5 +52,6 @@ struct uifocusitem_s {
uifocusitemcallback_t selected; uifocusitemcallback_t selected;
uifocusitemcallback_t changed; uifocusitemcallback_t changed;
uifocusitemcallback_t closed; uifocusitemcallback_t closed;
uifocusitemdirectioncallback_t direction;
void *user; void *user;
}; };
+4 -1
View File
@@ -6,5 +6,8 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
uiframe.c uiframe.c
uisettings.c uiconfirm.c
) )
add_subdirectory(game)
add_subdirectory(settings)
+9
View File
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uigamemenu.c
)
+78
View File
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uigamemenu.h"
#include "ui/frame/uiframe.h"
#include "ui/frame/settings/uisettings.h"
#include "util/memory.h"
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
#include "assert/assert.h"
#define UI_GAME_MENU_INDEX_CHARACTERS 0
#define UI_GAME_MENU_INDEX_SETTINGS 1
uigamemenu_t UI_GAME_MENU;
void uiGameMenuSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
}
errorret_t uiGameMenuInit(void) {
memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t));
MENU_BEGIN(
&UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL
);
MENU_BUTTON("Characters");
MENU_BUTTON("Settings");
MENU_END(UI_GAME_MENU.items, 1);
errorOk();
}
errorret_t uiGameMenuDraw(void) {
if(!uiMenuIsActive(&UI_GAME_MENU.menu)) errorOk();
const float_t width = UI_GAME_MENU_WIDTH;
const float_t height = (float_t)SCREEN.scanHeight;
const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width;
const float_t y = (float_t)SCREEN.scanY;
errorChain(uiFrameDraw(x, y, width, height));
errorChain(uiMenuDraw(
&UI_GAME_MENU.menu,
x + UI_FRAME_START_X,
y + UI_FRAME_START_Y,
width - (UI_FRAME_START_X * 2),
height - (UI_FRAME_START_Y * 2)
));
errorChain(spriteBatchFlush());
errorOk();
}
bool_t uiGameMenuIsOpen(void) {
return uiMenuIsActive(&UI_GAME_MENU.menu);
}
void uiGameMenuOpen() {
uiMenuOpen(&UI_GAME_MENU.menu);
}
void uiGameMenuClose() {
uiMenuClose(&UI_GAME_MENU.menu);
}
errorret_t uiGameMenuDispose(void) {
errorOk();
}
+58
View File
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#define UI_GAME_MENU_ITEM_COUNT 2
#define UI_GAME_MENU_WIDTH 150.0f
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT];
} uigamemenu_t;
extern uigamemenu_t UI_GAME_MENU;
/**
* Initializes the game menu panel.
*
* @return Any error that occurs.
*/
errorret_t uiGameMenuInit(void);
/**
* Draws the game menu panel. No-op when not visible.
*
* @return Any error that occurs.
*/
errorret_t uiGameMenuDraw(void);
/**
* Returns true when the game menu panel is currently open.
*
* @returns True if open.
*/
bool_t uiGameMenuIsOpen(void);
/**
* Opens the game menu panel. No-op when already open.
*/
void uiGameMenuOpen();
/**
* Closes the game menu panel. No-op when already closed.
*/
void uiGameMenuClose();
/**
* Disposes of the game menu panel.
*
* @return Any error that occurs.
*/
errorret_t uiGameMenuDispose(void);
+13
View File
@@ -0,0 +1,13 @@
# 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
uisettings.c
uisettingsgeneral.c
uisettingsinput.c
uisettingsdisplay.c
uisettingsaudio.c
)
+250
View File
@@ -0,0 +1,250 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uisettings.h"
#include "uisettingsgeneral.h"
#include "uisettingsinput.h"
#include "uisettingsdisplay.h"
#include "uisettingsaudio.h"
#include "ui/frame/uiframe.h"
#include "ui/frame/uiconfirm.h"
#include "util/memory.h"
#include "display/spritebatch/spritebatch.h"
#include "display/text/text.h"
#include "display/screen/screen.h"
#include "assert/assert.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
uisettings_t UI_SETTINGS;
uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT] = {
{
.localeId = "ui.settings.tabs.general",
.init = uiSettingsGeneralInit,
.menu = (uimenu_t *)&UI_SETTINGS.data,
.load = uiSettingsGeneralLoad,
.apply = uiSettingsGeneralApply,
.hasChanges = uiSettingsGeneralHasChanges
},
{
.localeId = "ui.settings.tabs.input",
.init = uiSettingsInputInit,
.menu = (uimenu_t *)&UI_SETTINGS.data,
.load = uiSettingsInputLoad,
.apply = uiSettingsInputApply,
.hasChanges = uiSettingsInputHasChanges
},
{
.localeId = "ui.settings.tabs.display",
.init = uiSettingsDisplayInit,
.menu = (uimenu_t *)&UI_SETTINGS.data,
.load = uiSettingsDisplayLoad,
.apply = uiSettingsDisplayApply,
.hasChanges = uiSettingsDisplayHasChanges
},
{
.localeId = "ui.settings.tabs.audio",
.init = uiSettingsAudioInit,
.menu = (uimenu_t *)&UI_SETTINGS.data,
.load = uiSettingsAudioLoad,
.apply = uiSettingsAudioApply,
.hasChanges = uiSettingsAudioHasChanges
}
};
static uisettingstabdef_t UI_SETTINGS_TOP_DEF = {
.menu = &UI_SETTINGS.tabsMenu,
.load = uiSettingsLoad,
.hasChanges = uiSettingsHasChanges
};
static uisettingstabdef_t *UI_SETTINGS_PENDING_DISCARD = NULL;
static uisettingstabdef_t *UI_SETTINGS_ACTIVE_TAB = NULL;
errorret_t uiSettingsActivateTab(uisettingstabdef_t *def) {
if(def == UI_SETTINGS_ACTIVE_TAB) errorOk();
errorChain(def->init(&UI_SETTINGS.data));
def->menu->user = def;
def->load();
UI_SETTINGS_ACTIVE_TAB = def;
errorOk();
}
void uiSettingsTabChanged(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index >= UI_SETTINGS_TAB_COUNT) return;
errorCatch(uiSettingsActivateTab(&UI_SETTINGS_TAB_DEFS[index]));
}
void uiSettingsTabSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index >= UI_SETTINGS_TAB_COUNT) return;
uisettingstabdef_t *def = &UI_SETTINGS_TAB_DEFS[index];
errorCatch(uiSettingsActivateTab(def));
uiMenuOpen(def->menu);
}
errorret_t uiSettingsInit(void) {
memoryZero(&UI_SETTINGS, sizeof(uisettings_t));
UI_SETTINGS_ACTIVE_TAB = NULL;
for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) {
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
UI_SETTINGS_TAB_DEFS[i].localeId,
0,
UI_SETTINGS.tabLabels[i],
UI_SETTINGS_TAB_LABEL_MAX
));
}
MENU_BEGIN(
&UI_SETTINGS.tabsMenu, UI_SETTINGS.tabs, uiSettingsTabSelected,
uiSettingsDiscardMenuClosed, uiSettingsTabChanged
);
for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) {
MENU_TAB(UI_SETTINGS.tabLabels[i]);
}
MENU_END(UI_SETTINGS.tabs, menuIndex);
UI_SETTINGS.tabsMenu.user = &UI_SETTINGS_TOP_DEF;
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.settings.apply",
0,
UI_SETTINGS.applyLabel,
UI_SETTINGS_APPLY_LABEL_MAX
));
errorChain(uiSettingsActivateTab(&UI_SETTINGS_TAB_DEFS[0]));
errorOk();
}
errorret_t uiSettingsDraw(void) {
if(!uiMenuIsActive(&UI_SETTINGS.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_SETTINGS.tabsMenu,
contentX,
contentY,
contentWidth,
contentHeight
));
const float_t tabsRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
const float_t pageY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y;
const float_t pageHeight = contentHeight - tabsRowHeight - UI_FRAME_PADDING_Y;
if(UI_SETTINGS_ACTIVE_TAB != NULL) {
errorChain(uiMenuDraw(
UI_SETTINGS_ACTIVE_TAB->menu, contentX, pageY, contentWidth, pageHeight
));
}
errorChain(spriteBatchFlush());
errorOk();
}
errorret_t uiSettingsUpdate(void) {
if(!UI_SETTINGS_PENDING_DISCARD) errorOk();
uisettingstabdef_t *def = UI_SETTINGS_PENDING_DISCARD;
UI_SETTINGS_PENDING_DISCARD = NULL;
return uiSettingsDiscardUpdate(def);
}
bool_t uiSettingsIsOpen(void) {
return uiMenuIsActive(&UI_SETTINGS.tabsMenu);
}
void uiSettingsOpen() {
uiSettingsLoad();
uiMenuOpen(&UI_SETTINGS.tabsMenu);
}
void uiSettingsClose() {
uiMenuClose(&UI_SETTINGS.tabsMenu);
}
errorret_t uiSettingsDispose(void) {
errorOk();
}
void uiSettingsLoad(void) {
if(UI_SETTINGS_ACTIVE_TAB != NULL) UI_SETTINGS_ACTIVE_TAB->load();
}
void uiSettingsApply(void) {
if(UI_SETTINGS_ACTIVE_TAB != NULL) UI_SETTINGS_ACTIVE_TAB->apply();
}
bool_t uiSettingsHasChanges(void) {
return UI_SETTINGS_ACTIVE_TAB != NULL && UI_SETTINGS_ACTIVE_TAB->hasChanges();
}
void uiSettingsDiscardMenuClosed(const uimenu_t *menu) {
uisettingstabdef_t *def = (uisettingstabdef_t *)menu->user;
if(def->hasChanges()) UI_SETTINGS_PENDING_DISCARD = def;
}
errorret_t uiSettingsDiscardUpdate(uisettingstabdef_t *def) {
#define UI_SETTINGS_DISCARD_TEXT_MAX 128
char_t text[UI_SETTINGS_DISCARD_TEXT_MAX];
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.confirm.discard_changes",
0,
text,
UI_SETTINGS_DISCARD_TEXT_MAX
));
#undef UI_SETTINGS_DISCARD_TEXT_MAX
uiConfirmOpen(text, uiSettingsDiscardConfirmResult, def);
errorOk();
}
void uiSettingsDiscardConfirmResult(const bool_t result, void *user) {
uisettingstabdef_t *def = (uisettingstabdef_t *)user;
if(!result) {
uiMenuOpen(def->menu);
return;
}
def->load();
}
+133
View File
@@ -0,0 +1,133 @@
/**
* 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 "uisettingsdata.h"
#define UI_SETTINGS_TAB_COUNT 4
#define UI_SETTINGS_TAB_LABEL_MAX 32
#define UI_SETTINGS_APPLY_LABEL_MAX 32
typedef struct {
const char_t *localeId;
errorret_t (*init)(uisettingsdata_t *data);
uimenu_t *menu;
void (*load)(void);
void (*apply)(void);
bool_t (*hasChanges)(void);
} uisettingstabdef_t;
typedef struct {
uimenu_t tabsMenu;
uimenuitem_t tabs[UI_SETTINGS_TAB_COUNT];
char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX];
char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX];
uisettingsdata_t data;
} uisettings_t;
extern uisettings_t UI_SETTINGS;
extern uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT];
/**
* Menu closed callback that flags a pending discard-changes
* confirmation if the menu's settings page/panel reports unsaved
* changes. Pass this directly as a settings page menu's closed
* callback. The pending def is picked up by uiSettingsUpdate on the
* next tick.
*
* @param menu The menu that was just closed.
*/
void uiSettingsDiscardMenuClosed(const uimenu_t *menu);
/**
* Shows the discard-changes confirmation dialog for def. Called once
* by uiSettingsUpdate when uiSettingsDiscardMenuClosed has flagged a
* pending def.
*
* @param def The tab/panel def to show the confirmation for.
* @return Any error that occurs.
*/
errorret_t uiSettingsDiscardUpdate(uisettingstabdef_t *def);
/**
* Callback passed to uiConfirmOpen by uiSettingsDiscardUpdate. Reopens
* the menu if the user chose to keep editing, otherwise reloads the
* widgets back to the actual engine values.
*
* @param result True if the user confirmed discarding changes, false
* to keep editing.
* @param user The uisettingstabdef_t* this confirmation belongs to.
*/
void uiSettingsDiscardConfirmResult(const bool_t result, void *user);
/**
* Initializes the settings panel and all of its category pages.
*
* @return Any error that occurs.
*/
errorret_t uiSettingsInit(void);
/**
* Draws the settings panel. No-op when not visible.
*
* @return Any error that occurs.
*/
errorret_t uiSettingsDraw(void);
/**
* Updates the settings panel. Shows a discard-changes confirmation if
* the panel was just closed with unsaved changes.
*
* @return Any error that occurs.
*/
errorret_t uiSettingsUpdate(void);
/**
* Returns true when the settings panel is currently open.
*
* @returns True if open.
*/
bool_t uiSettingsIsOpen(void);
/**
* Opens the settings panel. No-op when already open.
*/
void uiSettingsOpen();
/**
* Closes the settings panel. No-op when already closed.
*/
void uiSettingsClose();
/**
* Disposes of the settings panel.
*
* @return Any error that occurs.
*/
errorret_t uiSettingsDispose(void);
/**
* Loads every category page's widgets from the current engine settings.
* Called automatically by uiSettingsOpen.
*/
void uiSettingsLoad(void);
/**
* Applies every category page's widget values to the engine settings.
* Called automatically when a page's own Apply button is pressed.
*/
void uiSettingsApply(void);
/**
* Returns whether any category page has a widget value that differs
* from what was loaded.
*
* @returns True if there are unsaved changes.
*/
bool_t uiSettingsHasChanges(void);
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uisettingsaudio.h"
#include "uisettings.h"
#include "assert/assert.h"
#include "util/memory.h"
void uiSettingsAudioSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index != UI_SETTINGS_AUDIO_INDEX_APPLY) return;
uiSettingsAudioApply();
}
errorret_t uiSettingsAudioInit(uisettingsdata_t *data) {
uisettingsaudio_t *audio = &data->audio;
memoryZero(audio, sizeof(uisettingsaudio_t));
MENU_BEGIN(
&audio->menu, audio->items,
uiSettingsAudioSelected, uiSettingsDiscardMenuClosed, NULL
);
MENU_LABEL("No audio settings yet");
MENU_BUTTON(UI_SETTINGS.applyLabel);
MENU_END(audio->items, 1);
errorOk();
}
void uiSettingsAudioLoad(void) {
}
void uiSettingsAudioApply(void) {
uiMenuClose(&UI_SETTINGS.data.audio.menu);
}
bool_t uiSettingsAudioHasChanges(void) {
return false;
}
@@ -0,0 +1,36 @@
/**
* 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 "uisettingsdata.h"
/**
* Initializes the audio settings page into the shared settings data
* union.
*
* @param data The shared settings data union to initialize into.
* @return Any error that occurs.
*/
errorret_t uiSettingsAudioInit(uisettingsdata_t *data);
/**
* Loads the page's widgets from the current engine audio settings.
*/
void uiSettingsAudioLoad(void);
/**
* Applies the page's widget values to the engine audio settings.
*/
void uiSettingsAudioApply(void);
/**
* Returns whether any widget's value differs from what was loaded.
*
* @returns True if there are unsaved changes.
*/
bool_t uiSettingsAudioHasChanges(void);
@@ -0,0 +1,67 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#define UI_SETTINGS_GENERAL_ITEM_COUNT 3
#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE 0
#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE_DETAIL 1
#define UI_SETTINGS_GENERAL_INDEX_APPLY 2
#define UI_SETTINGS_GENERAL_LOCALE_COUNT 3
#define UI_SETTINGS_GENERAL_LABEL_MAX 32
#define UI_SETTINGS_GENERAL_INFO_MAX 64
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_SETTINGS_GENERAL_ITEM_COUNT];
const char_t *localeNames[UI_SETTINGS_GENERAL_LOCALE_COUNT];
char_t languageLabel[UI_SETTINGS_GENERAL_LABEL_MAX];
char_t labelInfo[UI_SETTINGS_GENERAL_INFO_MAX];
} uisettingsgeneral_t;
#define UI_SETTINGS_INPUT_ITEM_COUNT 2
#define UI_SETTINGS_INPUT_LABEL_MAX 32
#define UI_SETTINGS_INPUT_INDEX_DEADZONE 0
#define UI_SETTINGS_INPUT_INDEX_APPLY 1
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_SETTINGS_INPUT_ITEM_COUNT];
char_t deadzoneLabel[UI_SETTINGS_INPUT_LABEL_MAX];
} uisettingsinput_t;
#define UI_SETTINGS_DISPLAY_ITEM_COUNT 2
#define UI_SETTINGS_DISPLAY_INDEX_APPLY 1
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_SETTINGS_DISPLAY_ITEM_COUNT];
} uisettingsdisplay_t;
#define UI_SETTINGS_AUDIO_ITEM_COUNT 2
#define UI_SETTINGS_AUDIO_INDEX_APPLY 1
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_SETTINGS_AUDIO_ITEM_COUNT];
} uisettingsaudio_t;
/**
* Shared, overlapping storage for every settings tab's page data. Only
* one member is ever live at a time -- the active tab's Init writes
* into it, overwriting whatever the previously active tab left there.
* This trades simultaneous access to every tab for a memory footprint
* of only the single largest page.
*/
typedef union {
uisettingsgeneral_t general;
uisettingsinput_t input;
uisettingsdisplay_t display;
uisettingsaudio_t audio;
} uisettingsdata_t;
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uisettingsdisplay.h"
#include "uisettings.h"
#include "assert/assert.h"
#include "util/memory.h"
void uiSettingsDisplaySelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index != UI_SETTINGS_DISPLAY_INDEX_APPLY) return;
uiSettingsDisplayApply();
}
errorret_t uiSettingsDisplayInit(uisettingsdata_t *data) {
uisettingsdisplay_t *display = &data->display;
memoryZero(display, sizeof(uisettingsdisplay_t));
MENU_BEGIN(
&display->menu, display->items,
uiSettingsDisplaySelected, uiSettingsDiscardMenuClosed, NULL
);
MENU_LABEL("No display settings yet");
MENU_BUTTON(UI_SETTINGS.applyLabel);
MENU_END(display->items, 1);
errorOk();
}
void uiSettingsDisplayLoad(void) {
}
void uiSettingsDisplayApply(void) {
uiMenuClose(&UI_SETTINGS.data.display.menu);
}
bool_t uiSettingsDisplayHasChanges(void) {
return false;
}
@@ -0,0 +1,36 @@
/**
* 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 "uisettingsdata.h"
/**
* Initializes the display settings page into the shared settings data
* union.
*
* @param data The shared settings data union to initialize into.
* @return Any error that occurs.
*/
errorret_t uiSettingsDisplayInit(uisettingsdata_t *data);
/**
* Loads the page's widgets from the current engine display settings.
*/
void uiSettingsDisplayLoad(void);
/**
* Applies the page's widget values to the engine display settings.
*/
void uiSettingsDisplayApply(void);
/**
* Returns whether any widget's value differs from what was loaded.
*
* @returns True if there are unsaved changes.
*/
bool_t uiSettingsDisplayHasChanges(void);
@@ -0,0 +1,117 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uisettingsgeneral.h"
#include "uisettings.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "locale/localemanager.h"
#include "locale/localeinfo.h"
#include "asset/loader/locale/assetlocaleloader.h"
static const localeinfo_t *const UI_SETTINGS_GENERAL_LOCALES[
UI_SETTINGS_GENERAL_LOCALE_COUNT
] = {
&LOCALE_EN_US,
&LOCALE_JP_JP,
&LOCALE_ES_MX
};
void uiSettingsGeneralSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
switch(index) {
case UI_SETTINGS_GENERAL_INDEX_APPLY:
uiSettingsGeneralApply();
break;
default:
break;
}
}
errorret_t uiSettingsGeneralInit(uisettingsdata_t *data) {
uisettingsgeneral_t *general = &data->general;
memoryZero(general, sizeof(uisettingsgeneral_t));
for(uint8_t i = 0; i < UI_SETTINGS_GENERAL_LOCALE_COUNT; i++) {
general->localeNames[i] = UI_SETTINGS_GENERAL_LOCALES[i]->name;
}
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.settings.general.language",
0,
general->languageLabel,
UI_SETTINGS_GENERAL_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.settings.general.language_detail",
0,
general->labelInfo,
UI_SETTINGS_GENERAL_INFO_MAX
));
MENU_BEGIN(
&general->menu, general->items,
uiSettingsGeneralSelected, uiSettingsDiscardMenuClosed, NULL
);
MENU_DROPDOWN(
general->languageLabel, general->localeNames,
UI_SETTINGS_GENERAL_LOCALE_COUNT, 0
);
MENU_LABEL(general->labelInfo);
MENU_BUTTON(UI_SETTINGS.applyLabel);
MENU_END(general->items, 1);
errorOk();
}
void uiSettingsGeneralLoad(void) {
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
uint8_t index = 0;
for(uint8_t i = 0; i < UI_SETTINGS_GENERAL_LOCALE_COUNT; i++) {
if(stringCompare(LOCALE.locale->file, UI_SETTINGS_GENERAL_LOCALES[i]->file) != 0) {
continue;
}
index = i;
break;
}
uiDropdownSetSelectedIndex(
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown, index
);
}
void uiSettingsGeneralApply(void) {
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
uint8_t index = uiDropdownGetSelectedIndex(
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown
);
errorCatch(localeManagerSetLocale(UI_SETTINGS_GENERAL_LOCALES[index]));
uiMenuClose(&general->menu);
}
bool_t uiSettingsGeneralHasChanges(void) {
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
uint8_t index = uiDropdownGetSelectedIndex(
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown
);
return stringCompare(
LOCALE.locale->file, UI_SETTINGS_GENERAL_LOCALES[index]->file
) != 0;
}
@@ -0,0 +1,36 @@
/**
* 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 "uisettingsdata.h"
/**
* Initializes the general settings page into the shared settings data
* union.
*
* @param data The shared settings data union to initialize into.
* @return Any error that occurs.
*/
errorret_t uiSettingsGeneralInit(uisettingsdata_t *data);
/**
* Loads the page's widgets from the current engine general settings.
*/
void uiSettingsGeneralLoad(void);
/**
* Applies the page's widget values to the engine general settings.
*/
void uiSettingsGeneralApply(void);
/**
* Returns whether any widget's value differs from what was loaded.
*
* @returns True if there are unsaved changes.
*/
bool_t uiSettingsGeneralHasChanges(void);

Some files were not shown because too many files have changed in this diff Show More