14 Commits

Author SHA1 Message Date
YourWishes a84137b5ff del md 2026-08-04 06:58:03 -05:00
YourWishes ca02ee0352 Add camera shake 2026-07-11 11:02:09 -05:00
YourWishes fbaa54145e Fixed dolphin rendering. 2026-07-10 20:15:26 -05:00
YourWishes 28754ffbf2 Removed old scripted types 2026-07-10 13:24:21 -05:00
YourWishes 470c0eba7a Whatever, some minor map chunking improvements 2026-07-10 12:57:31 -05:00
YourWishes 7098dcec43 Cleaned some log 2026-07-09 23:25:43 -05:00
YourWishes 07137f57af Assets are slightly optimized 2026-07-09 23:25:26 -05:00
YourWishes 8b7491a3d3 Emoji support to characters 2026-07-09 13:18:48 -05:00
YourWishes 8cfa8ddfeb Weaather baseline 2026-07-08 12:57:13 -05:00
YourWishes ef284a15a1 pre-cache shader matrices 2026-07-08 12:36:40 -05:00
YourWishes 3723921573 Render culling on sceneoverworld.h 2026-07-08 12:02:41 -05:00
YourWishes 195399635e Updating mini textbox 2026-07-08 11:45:10 -05:00
YourWishes 46e2a924d3 Mini textboxes 2026-07-08 10:53:53 -05:00
YourWishes b693ea4102 Starting item and battle stuff 2026-07-08 10:05:21 -05:00
132 changed files with 3675 additions and 2460 deletions
-455
View File
@@ -1,455 +0,0 @@
# Dusk — Claude Code rules
## File headers
Every C, H, and JS file starts with:
```c
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
```
JS files use `//` comment style instead.
---
## C conventions
### Types
Always use the project-defined aliases instead of bare C primitives:
| Use | Not |
|-----------|--------------|
| `bool_t` | `bool` |
| `int_t` | `int` |
| `float_t` | `float` |
| `char_t` | `char` |
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
### Naming
- **Functions** — snake_case, prefixed with their module:
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
- **Macros / constants** — UPPER_SNAKE_CASE:
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
- **Files** — snake_case matching the primary type: `entityposition.c`,
`moduleassetbatch.c`
### Header files (`.h`)
- Use `#pragma once` — no include guards.
- Declare every public function, `#define`, and `extern` global.
- Write a JSDoc block (`/** … */`) above every declaration explaining
purpose, `@param`s, and `@returns`.
- Only include headers that the `.h` file itself strictly requires for
the types it exposes. Move everything else to the `.c` file.
Do not use forward declarations as a workaround — use the real
include in the `.c` file instead.
### Implementation files (`.c`)
- Contain function bodies only; no declarations.
- Pull in whatever additional includes the implementation needs.
- Do not use `static` or `inline` on **functions**. Every function,
including internal helpers, must be declared in the matching `.h` and
defined in the `.c` file. Internal helpers belong near the bottom of
the `.c` file, not at the top with a `static` qualifier.
`static` and `inline` on functions are only appropriate when the
function body is written directly inside a `.h` file.
`static` on **variables** (file-scope state) is fine and expected.
### Formatting
- Hard-wrap all lines at **80 characters**.
### Error handling
Return `errorret_t` from fallible functions. Use these macros:
```c
errorOk(); // return success
errorThrow("msg %d", val); // return failure with message
errorChain(someCall()); // propagate failure, continue on success
errorIsOk(ret) / errorIsNotOk(ret) // test a result
errorCatch(ret); // handle + free an error
```
Never return raw error codes or use `errno` for in-engine errors.
### Memory
Use the project allocator — never raw `malloc`/`free`:
```c
memoryAllocate(size) // allocate
memoryFree(ptr) // free
memoryZero(dest, size) // zero a block
memoryCopy(dest, src, size) // copy
```
### Asserts
Prefer specific assert macros over bare `assert()`:
```c
assertNotNull(ptr, "msg");
assertTrue(cond, "msg");
assertFalse(cond, "msg");
assertUnreachable("msg");
assertIsMainThread("msg");
```
---
## Build system
Each subdirectory has its own `CMakeLists.txt` that adds sources with:
```cmake
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
myfile.c
)
```
Never add source files to the root `CMakeLists.txt` directly.
---
## Platform support
### Targets
Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
| `DUSK_TARGET_SYSTEM` | Macro defined | Platform |
|----------------------|-------------------|------------------|
| `linux` | `DUSK_LINUX` | Linux desktop |
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
| `psp` | `DUSK_PSP` | Sony PSP |
| `vita` | `DUSK_VITA` | PlayStation Vita |
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
| `wii` | `DUSK_WII` | Nintendo Wii |
### Layer structure
```
src/dusk/ core, platform-agnostic game logic
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
src/dusklinux/ Linux + Knulli platform impl
src/duskpsp/ PSP platform impl
src/duskvita/ Vita platform impl
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
```
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
it uses native GameCube/Wii rendering and input APIs.
### Platform guards
Use the compile-time macros for platform-specific code:
```c
#ifdef DUSK_PSP
// PSP-only path
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
// GameCube / Wii path
#else
// Generic / Linux fallback
#endif
```
Additional capability macros set per-target:
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
### Abstraction pattern
Platform-specific implementations are wired in via `#define` macros in
each platform's `displayplatform.h` / `inputplatform.h` etc., which
the core calls through. Functions that a platform does not support are
simply left undefined — the core guards calls with `#ifdef`.
### Adding platform-specific code
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
or capability macro.
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
the platform header macros instead.
---
## Adding a new asset loader type
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
`src/dusk/asset/loader/assetloader.h`.
2. Add fields to the input/loading/output unions in `assetloader.h`.
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
`src/dusk/asset/loader/assetloader.c`.
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
---
## Adding a new entity component
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
`entityMyCompDispose()`.
2. Add the include to `src/dusk/entity/componentlist.h` header block.
3. Add a row to `src/dusk/entity/componentlist.h`:
```c
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
```
This auto-generates the enum, union field, and definition entry.
4. If JS-facing, create the script module and `.d.ts` (see below).
---
## Adding a new script (JS) module
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
- Use `moduleBaseFunction(name)` to define JS-callable functions.
- Register props/funcs in `moduleMyModInit()` with
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
`scriptProtoDefineStaticFunc`.
2. `#include` the header in
`src/dusk/script/module/modulelist.c` and call
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
`moduleListDispose()`).
3. For component modules also register in
`src/dusk/script/module/entity/component/modulecomponentlist.c`
so `entity.add()` returns the typed wrapper.
4. Create `types/<category>/mymod.d.ts` and add a
`/// <reference path="..." />` line to `types/index.d.ts`.
---
## Script module type declarations
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
check whether the corresponding `types/**/*.d.ts` needs updating and
apply any changes before finishing the task.
---
## JavaScript (asset scripts)
- Use `var` for module-level state; `const` for values that never
change.
- Always use semicolons.
- Scene objects are plain objects (`var scene = {}`) with assigned
methods.
- Export via `module.exports = scene`.
- Async scene init should use `async function` and `await`.
---
## Coding style
### ASCII only
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000U+007F).
Non-ASCII characters are banned even in comments and string literals.
Use ASCII-only substitutes instead:
- `--` or `-` instead of `` (em dash)
- `->` instead of `` (arrow)
- `x` or `*` instead of `×` (multiplication)
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
### Indentation
2 spaces. No tabs.
### Keyword and operator spacing
No space between a keyword or function name and its opening parenthesis:
```c
if(!ptr) return;
for(uint8_t i = 0; i < count; i++) {
while(entry->state != DONE) {
switch(type) {
sizeof(assetbatch_t)
memoryZero(ptr, size)
```
Spaces around all binary operators and after every comma:
```c
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
(size_t)end - (size_t)start
foo(a, b, c)
```
### Braces
Opening brace on the **same line** as the statement (K&R style) for all
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
```c
void assetEntryLock(assetentry_t *entry) {
...
}
if(dirty) {
...
} else {
...
}
```
### Guard returns
Short guards go on one line with no braces:
```c
if(!ptr) return;
if(!b || !b->batch) return jerry_undefined();
if(!(flags & DIRTY)) return;
```
### Blank lines
- One blank line between functions; no blank line at the start or end of
a function body.
- One blank line between logical blocks inside a function body.
- No trailing blank lines at the end of a file.
### Pointer placement
`*` is attached to the variable name, not the type:
```c
assetentry_t *entry
const char_t *name
void *ptr
uint8_t *d = (uint8_t *)dest;
```
### Casts
Space between cast and operand:
```c
(assetbatch_t *)user
(uint8_t *)dest
(textureformat_t)v
```
### Return
No parentheses around the return value:
```c
return ptr;
return MEMORY_POINTERS_IN_USE;
```
### switch / case
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
```c
switch(type) {
case ASSET_LOADER_TYPE_TEXTURE:
descs[i].input.texture = (textureformat_t)v;
break;
default:
break;
}
```
### Multi-line function signatures
When parameters don't fit on one line, put each on its own line indented
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
its own line at column 0:
```c
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
errorret_t memoryCompare(
const void *a,
const void *b,
const size_t size
);
```
### Structs and enums
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
brace and name on the same line:
```c
typedef struct {
errorcode_t code;
char_t *message;
} errorstate_t;
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
```
### Designated initialisers
Spaces inside braces; `.field = value`:
```c
jsassetentry_t e = { .entry = entry };
assetbatchloadedpend_t init = { .batch = batch };
```
### Ternary operator
Spaces around `?` and `:`:
```c
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
```
### const placement
`const` before the type, `*` attached to the variable:
```c
const char_t *name
const void *src
const size_t size
```
### Comments in `.c` files
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
functions follow one another with a single blank line between them.
- Multi-line explanatory comments inside function bodies use `//` lines:
```c
// Script modules are freed; orphaned JS wrapper objects now get GC'd
// so their finalizers fire before assetDispose() checks ref counts.
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
```
- Do not use `/* */` for inline or inline-block comments inside `.c`
function bodies.
### Comments in `.h` files
Every public declaration gets a Javadoc block (`/** … */`) with
`@param` and `@returns` where relevant. Keep it on the lines immediately
above the declaration with no blank line in between.
---
## Color system
Colors are defined in `src/dusk/display/color.csv` and code-generated
into a `color.h` header by `tools/color/csv/__main__.py`.
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
The script emits four `#define` variants per color plus a bare alias:
```
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
COLOR_<NAME>_3B color3b(r8, g8, b8)
COLOR_<NAME>_3F color3f(rf, gf, bf)
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
COLOR_<NAME> COLOR_<NAME>_4B
```
`color_t` is `color4b_t` (four `uint8_t` channels).
To add a new color, append a row to `color.csv` and rebuild — do not
hand-edit the generated header.
---
## Tests
- Tests live in `test/` mirroring `src/dusk/` structure.
- Use cmocka; include `dusktest.h`.
- Test functions: `static void test_something(void **state)`.
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
leaks.
- Build with `-DDUSK_BUILD_TESTS=ON`.
+21
View File
@@ -43,3 +43,24 @@ msgstr "Apply"
#: src/dusk/ui/frame/uiconfirm.c #: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes" msgid "ui.confirm.discard_changes"
msgstr "Discard unsaved changes?" msgstr "Discard unsaved changes?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "Characters"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "Items"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "Settings"
msgid "item.potion.name"
msgstr "Potion"
msgid "item.potato.name"
msgstr "Potato"
msgid "item.apple.name"
msgstr "Apple"
+24
View File
@@ -44,3 +44,27 @@ msgstr "Aplicar"
#: src/dusk/ui/frame/uiconfirm.c #: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes" msgid "ui.confirm.discard_changes"
msgstr "¿Descartar los cambios no guardados?" msgstr "¿Descartar los cambios no guardados?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "Personajes"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "Objetos"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "Configuración"
#: src/dusk/rpg/item/item.json
msgid "item.potion.name"
msgstr "Poción"
#: src/dusk/rpg/item/item.json
msgid "item.potato.name"
msgstr "Papa"
#: src/dusk/rpg/item/item.json
msgid "item.apple.name"
msgstr "Manzana"
+24
View File
@@ -44,3 +44,27 @@ msgstr "適用"
#: src/dusk/ui/frame/uiconfirm.c #: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes" msgid "ui.confirm.discard_changes"
msgstr "未保存の変更を破棄しますか?" msgstr "未保存の変更を破棄しますか?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "キャラクター"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "アイテム"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "設定"
#: src/dusk/rpg/item/item.json
msgid "item.potion.name"
msgstr "ポーション"
#: src/dusk/rpg/item/item.json
msgid "item.potato.name"
msgstr "ジャガイモ"
#: src/dusk/rpg/item/item.json
msgid "item.apple.name"
msgstr "リンゴ"
+3
View File
@@ -16,6 +16,9 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DOL=1 DOL=1
ISO=2 ISO=2
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE} DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
# GameCube/Wii PowerPC is always big-endian; declare it at compile time
# like every other target instead of relying on endian.h's runtime probe.
DUSK_PLATFORM_ENDIAN_BIG
) )
# Custom compiler flags # Custom compiler flags
+1
View File
@@ -56,6 +56,7 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_DISPLAY_HEIGHT=272 DUSK_DISPLAY_HEIGHT=272
DUSK_THREAD_PTHREAD DUSK_THREAD_PTHREAD
DUSK_TIME_DYNAMIC DUSK_TIME_DYNAMIC
DUSK_DISPLAY_OVERSCAN=6
) )
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
+36 -45
View File
@@ -59,7 +59,10 @@ assetentry_t * assetGetEntry(
entry++; entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// We did not find one existing, Find first available slot. // We did not find one existing. Find first available slot, reaping
// zero-ref entries to make room if none are immediately available.
bool_t reaped = false;
for(;;) {
entry = ASSET.entries; entry = ASSET.entries;
do { do {
if(entry->type != ASSET_LOADER_TYPE_NULL) { if(entry->type != ASSET_LOADER_TYPE_NULL) {
@@ -74,6 +77,11 @@ assetentry_t * assetGetEntry(
entry++; entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
if(reaped) break;
reaped = true;
errorCatch(assetReapUnused());
}
assertUnreachable("No available asset entry slots."); assertUnreachable("No available asset entry slots.");
return NULL; return NULL;
} }
@@ -191,6 +199,32 @@ void assetUnlockEntry(assetentry_t *entry) {
assetEntryUnlock(entry); assetEntryUnlock(entry);
} }
errorret_t assetReapUnused(void) {
assertIsMainThread("assetReapUnused must be called from the main thread.");
// Repeatedly find and dispose zero-ref LOADED entries until none remain.
// This handles dependency chains where an entry (e.g. a model) holds refs
// on child entries (mesh, texture): dispose parents first so child ref
// counts drop to zero, then pick up the children on the next pass. Without
// this, a forward-only scan fails when a shared child entry appears before
// a parent that still holds a ref to it.
bool_t any;
do {
any = false;
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; }
if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; }
if(entry->refs.count > 0) { entry++; continue; }
errorChain(assetEntryDispose(entry));
any = true;
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
errorOk();
}
errorret_t assetUpdate(void) { errorret_t assetUpdate(void) {
assertIsMainThread("assetUpdate must be called from the main thread."); assertIsMainThread("assetUpdate must be called from the main thread.");
@@ -324,30 +358,6 @@ errorret_t assetUpdate(void) {
} }
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Reap unused entries.
entry = ASSET.entries;
do {
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
entry++;
continue;
}
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->refs.count > 0) {
entry++;
continue;
}
// consolePrint("Reaping asset %s", entry->name);
errorChain(assetEntryDispose(entry));
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
errorOk(); errorOk();
} }
@@ -413,26 +423,7 @@ errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread."); assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread); threadStop(&ASSET.loadThread);
// Drain-dispose: repeatedly find and dispose zero-ref LOADED entries errorChain(assetReapUnused());
// until none remain. This handles dependency chains where an entry
// (e.g. a model) holds refs on child entries (mesh, texture): dispose
// parents first so child ref counts drop to zero, then pick up the
// children on the next pass. Without this, a forward-only scan fails
// when a shared child entry appears before a parent that still holds a
// ref to it.
bool_t any;
do {
any = false;
assetentry_t *e = ASSET.entries;
do {
if(e->type == ASSET_LOADER_TYPE_NULL) { e++; continue; }
if(e->state != ASSET_ENTRY_STATE_LOADED) { e++; continue; }
if(e->refs.count > 0) { e++; continue; }
errorChain(assetEntryDispose(e));
any = true;
e++;
} while(e < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
// Cleanup zip file. // Cleanup zip file.
if(ASSET.zip != NULL) { if(ASSET.zip != NULL) {
+12 -2
View File
@@ -23,8 +23,8 @@
#define ASSET_FILE_NAME "dusk.dsk" #define ASSET_FILE_NAME "dusk.dsk"
#define ASSET_HEADER_SIZE 3 #define ASSET_HEADER_SIZE 3
#define ASSET_LOADING_COUNT_MAX 20 #define ASSET_LOADING_COUNT_MAX 10
#define ASSET_ENTRY_COUNT_MAX 128 #define ASSET_ENTRY_COUNT_MAX 64
typedef struct asset_s { typedef struct asset_s {
zip_t *zip; zip_t *zip;
@@ -112,6 +112,16 @@ void assetUnlock(const char_t *name);
*/ */
void assetUnlockEntry(assetentry_t *entry); void assetUnlockEntry(assetentry_t *entry);
/**
* Frees every currently unreferenced (zero-ref) loaded asset entry. Repeats
* until a full pass frees nothing further, since disposing a parent entry
* (e.g. a model) may drop a child entry's (e.g. a mesh) ref count to zero,
* making it eligible for reaping too.
*
* @return An error code if any entry could not be disposed properly.
*/
errorret_t assetReapUnused(void);
/** /**
* Requires an asset entry to be loaded. This will block until the asset entry * Requires an asset entry to be loaded. This will block until the asset entry
* is fully loaded. * is fully loaded.
@@ -111,9 +111,15 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
size_t offset = 8; size_t offset = 8;
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t); size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
out->tiles = memoryAllocate(tileSize);
memoryCopy(out->tiles, data + offset, tileSize); memoryCopy(out->tiles, data + offset, tileSize);
offset += tileSize; offset += tileSize;
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) {
uint32_t *shape = (uint32_t *)&out->tiles[t].shape;
*shape = endianLittleToHost32(*shape);
}
out->meshCount = data[offset]; out->meshCount = data[offset];
offset += sizeof(uint8_t); offset += sizeof(uint8_t);
assertTrue( assertTrue(
@@ -135,6 +141,9 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3)); memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
offset += sizeof(vec3); offset += sizeof(vec3);
out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]);
out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]);
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
} }
memoryFree(data); memoryFree(data);
@@ -157,6 +166,12 @@ errorret_t assetChunkDispose(assetentry_t *entry) {
assertIsMainThread("Must be called from the main thread."); assertIsMainThread("Must be called from the main thread.");
assetchunkoutput_t *out = &entry->data.chunk; assetchunkoutput_t *out = &entry->data.chunk;
if(out->tiles != NULL) {
memoryFree(out->tiles);
out->tiles = NULL;
}
for(uint8_t m = 0; m < out->meshCount; m++) { for(uint8_t m = 0; m < out->meshCount; m++) {
if(out->modelEntries[m] == NULL) continue; if(out->modelEntries[m] == NULL) continue;
assetUnlockEntry(out->modelEntries[m]); assetUnlockEntry(out->modelEntries[m]);
@@ -34,7 +34,7 @@ typedef struct {
} assetchunkloaderloading_t; } assetchunkloaderloading_t;
typedef struct { typedef struct {
tile_t tiles[CHUNK_TILE_COUNT]; tile_t *tiles;
uint8_t meshCount; uint8_t meshCount;
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX]; char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX]; vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
+23 -4
View File
@@ -48,8 +48,20 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8)); uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
meshvertex_t *vertices = NULL; meshvertex_t *vertices = NULL;
if(vertCount > 0) { if(vertCount > 0) {
vertices = memoryAllocate(vertCount * sizeof(meshvertex_t)); // 32-byte (cache-line) aligned: GX_SetArray + DCFlushRange on Dolphin
// require this for the DMA'd vertex data to actually reach the GPU
// coherently. Static compiled-in vertex arrays happen to get this from
// the linker; a plain memoryAllocate here would not.
vertices = memoryAlign(32, vertCount * sizeof(meshvertex_t));
memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t)); memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t));
for(uint32_t v = 0; v < vertCount; v++) {
vertices[v].uv[0] = endianLittleToHostFloat(vertices[v].uv[0]);
vertices[v].uv[1] = endianLittleToHostFloat(vertices[v].uv[1]);
vertices[v].pos[0] = endianLittleToHostFloat(vertices[v].pos[0]);
vertices[v].pos[1] = endianLittleToHostFloat(vertices[v].pos[1]);
vertices[v].pos[2] = endianLittleToHostFloat(vertices[v].pos[2]);
}
} }
memoryFree(raw); memoryFree(raw);
@@ -103,18 +115,22 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
out->vertices = NULL; out->vertices = NULL;
errorChain(ret); errorChain(ret);
} }
out->meshInitialized = true;
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount); ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
if(errorIsNotOk(ret)) { if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR; loading->entry->state = ASSET_ENTRY_STATE_ERROR;
meshDispose(&out->mesh); meshDispose(&out->mesh);
out->meshInitialized = false;
memoryFree(out->vertices); memoryFree(out->vertices);
out->vertices = NULL; out->vertices = NULL;
errorChain(ret); errorChain(ret);
} }
#ifndef DUSK_OPENGL_LEGACY #if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
// VBO owns the data now; CPU copy is no longer needed. // VBO owns the data now; CPU copy is no longer needed. The platform
// mesh object itself still needs meshDispose later - tracked via
// out->meshInitialized, independent of the CPU buffer's lifetime.
memoryFree(out->vertices); memoryFree(out->vertices);
out->vertices = NULL; out->vertices = NULL;
#endif #endif
@@ -129,8 +145,11 @@ errorret_t assetMeshDispose(assetentry_t *entry) {
assertIsMainThread("Must be called from the main thread."); assertIsMainThread("Must be called from the main thread.");
assetmeshoutput_t *out = &entry->data.mesh; assetmeshoutput_t *out = &entry->data.mesh;
if(out->vertices != NULL) { if(out->meshInitialized) {
errorChain(meshDispose(&out->mesh)); errorChain(meshDispose(&out->mesh));
out->meshInitialized = false;
}
if(out->vertices != NULL) {
memoryFree(out->vertices); memoryFree(out->vertices);
out->vertices = NULL; out->vertices = NULL;
} }
@@ -30,6 +30,7 @@ typedef struct {
typedef struct { typedef struct {
mesh_t mesh; mesh_t mesh;
bool_t meshInitialized;
meshvertex_t *vertices; meshvertex_t *vertices;
} assetmeshoutput_t; } assetmeshoutput_t;
+1 -1
View File
@@ -17,7 +17,7 @@ console_t CONSOLE;
void consoleInit(void) { void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t)); memoryZero(&CONSOLE, sizeof(console_t));
CONSOLE.visible = true; CONSOLE.visible = false;
#ifdef DUSK_CONSOLE_POSIX #ifdef DUSK_CONSOLE_POSIX
threadMutexInit(&CONSOLE.printMutex); threadMutexInit(&CONSOLE.printMutex);
+175 -1
View File
@@ -6,6 +6,7 @@
*/ */
#include "battle.h" #include "battle.h"
#include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
battle_t BATTLE; battle_t BATTLE;
@@ -40,10 +41,183 @@ battlefighter_t *battleAddFighter(
return fighter; return fighter;
} }
void battleStart(void) { void battleStart(
const battleencountertype_t encounterType,
const bool_t fleeAvailable
) {
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
BATTLE.encounterType = encounterType;
BATTLE.fleeAvailable = fleeAvailable;
BATTLE.result = BATTLE_RESULT_NONE;
BATTLE.round = 1;
BATTLE.turnIndex = 0;
battleBuildTurnOrder(true);
BATTLE.active = true; BATTLE.active = true;
} }
void battleDispose(void) { void battleDispose(void) {
battleInit(); battleInit();
} }
battlefighter_t *battleGetCurrentFighter(void) {
if(!BATTLE.active) return NULL;
if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
}
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
uint8_t count = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(BATTLE.fighters[i].team != team) continue;
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
count++;
}
return count;
}
void battleResolveAttack(
battlefighter_t *attacker,
battlefighter_t *defender
) {
assertNotNull(attacker, "Attacker cannot be NULL");
assertNotNull(defender, "Defender cannot be NULL");
const int32_t rawDamage =
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
defender->health = damage >= defender->health ? 0 : defender->health - damage;
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
}
void battleNextTurn(void) {
BATTLE.turnIndex++;
if(BATTLE.turnIndex < BATTLE.turnCount) return;
BATTLE.round++;
BATTLE.turnIndex = 0;
battleBuildTurnOrder(false);
}
battleresult_t battleCheckResult(void) {
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
BATTLE.result = BATTLE_RESULT_LOSS;
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
BATTLE.result = BATTLE_RESULT_WIN;
}
return BATTLE.result;
}
void battlePlayerAttack(const uint8_t targetIndex) {
battlefighter_t *attacker = battleGetCurrentFighter();
if(attacker == NULL) return;
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
battlefighter_t *defender = &BATTLE.fighters[targetIndex];
if(!battleFighterIsAlive(defender)) return;
battleResolveAttack(attacker, defender);
battleCheckResult();
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
}
void battlePlayerFlee(void) {
battlefighter_t *fighter = battleGetCurrentFighter();
if(fighter == NULL) return;
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
if(!BATTLE.fleeAvailable) return;
BATTLE.result = BATTLE_RESULT_FLED;
}
void battleUpdate(void) {
if(!BATTLE.active) return;
if(BATTLE.result != BATTLE_RESULT_NONE) return;
battlefighter_t *current = battleGetCurrentFighter();
if(current == NULL) return;
if(!battleFighterIsAlive(current)) {
battleNextTurn();
return;
}
if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
battlefighter_t *target = battleAIChooseTarget(current);
if(target != NULL) battleResolveAttack(current, target);
battleCheckResult();
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
}
void battleBuildTurnOrder(const bool_t applyEncounterBias) {
BATTLE.turnCount = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
BATTLE.turnOrder[BATTLE.turnCount++] = i;
}
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
const uint8_t key = BATTLE.turnOrder[i];
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
int8_t j = (int8_t)i - 1;
while(
j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
) {
BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
j--;
}
BATTLE.turnOrder[j + 1] = key;
}
if(!applyEncounterBias) return;
if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) {
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY);
} else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) {
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY);
}
}
void battleMoveTeamFirst(const battlefighterteam_t team) {
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
uint8_t count = 0;
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
sorted[count++] = BATTLE.turnOrder[i];
}
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
sorted[count++] = BATTLE.turnOrder[i];
}
memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
}
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
const battlefighterteam_t enemyTeam =
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
battlefighter_t *weakest = NULL;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
battlefighter_t *candidate = &BATTLE.fighters[i];
if(candidate->team != enemyTeam) continue;
if(!battleFighterIsAlive(candidate)) continue;
if(weakest == NULL || candidate->health < weakest->health) {
weakest = candidate;
}
}
return weakest;
}
+136 -3
View File
@@ -10,9 +10,36 @@
#define BATTLE_FIGHTER_COUNT_MAX 8 #define BATTLE_FIGHTER_COUNT_MAX 8
typedef enum {
BATTLE_ENCOUNTER_REGULAR,
BATTLE_ENCOUNTER_PLAYER_ADVANTAGE,
BATTLE_ENCOUNTER_BACK_ATTACK,
BATTLE_ENCOUNTER_COUNT
} battleencountertype_t;
typedef enum {
BATTLE_RESULT_NONE,
BATTLE_RESULT_WIN,
BATTLE_RESULT_LOSS,
BATTLE_RESULT_FLED,
BATTLE_RESULT_COUNT
} battleresult_t;
typedef struct { typedef struct {
bool_t active; bool_t active;
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX]; battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
battleencountertype_t encounterType;
bool_t fleeAvailable;
battleresult_t result;
// Fighter indices (into fighters[]), sorted for the current round.
uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
uint8_t turnCount;
uint8_t turnIndex;
uint16_t round;
} battle_t; } battle_t;
extern battle_t BATTLE; extern battle_t BATTLE;
@@ -50,12 +77,118 @@ battlefighter_t *battleAddFighter(
); );
/** /**
* Starts a battle, marking it active. Any fighters already added via * Starts the battle: builds the opening turn order (biased by
* battleAddFighter remain in place. * encounterType for the first round only) and marks the battle active.
* Call once every fighter has been added via battleAddFighter.
*
* @param encounterType Determines the opening round's turn order.
* @param fleeAvailable Whether the party may attempt to flee this battle.
*/ */
void battleStart(void); void battleStart(
const battleencountertype_t encounterType,
const bool_t fleeAvailable
);
/** /**
* Disposes of the battle, clearing all fighters and marking it inactive. * Disposes of the battle, clearing all fighters and marking it inactive.
*/ */
void battleDispose(void); void battleDispose(void);
/**
* Returns the fighter whose turn it currently is.
*
* @return Pointer to the active fighter, or NULL if the battle isn't
* active or has no living fighters left to act.
*/
battlefighter_t *battleGetCurrentFighter(void);
/**
* Returns the number of living fighters on a team.
*
* @param team The team to count.
* @return Count of living fighters on that team.
*/
uint8_t battleGetAliveCount(const battlefighterteam_t team);
/**
* Resolves a physical attack from attacker onto defender: damage is the
* attacker's attack stat minus the defender's defense stat (minimum 1),
* subtracted from the defender's health. The defender is marked dead
* once health reaches 0.
*
* @param attacker The attacking fighter.
* @param defender The defending fighter.
*/
void battleResolveAttack(
battlefighter_t *attacker,
battlefighter_t *defender
);
/**
* Ends the current fighter's turn and advances to the next fighter in
* the turn order, starting a new round (rebuilding turn order purely by
* speed) once every fighter in the current round has acted.
*/
void battleNextTurn(void);
/**
* Checks whether the battle has been won or lost, updating and
* returning BATTLE.result. Does nothing if a result has already been
* set (e.g. by a successful flee).
*
* @return The battle's current result.
*/
battleresult_t battleCheckResult(void);
/**
* Submits the current fighter's attack against a target, if it is
* currently a player-controlled fighter's turn. Resolves the attack,
* checks for a battle result, and advances the turn.
*
* @param targetIndex Index into BATTLE.fighters of the target.
*/
void battlePlayerAttack(const uint8_t targetIndex);
/**
* Submits a flee attempt for the current fighter's turn, if it is
* currently a player-controlled fighter's turn and fleeing is
* available for this battle. Always succeeds, ending the battle with
* BATTLE_RESULT_FLED.
*/
void battlePlayerFlee(void);
/**
* Updates the battle simulation for one frame: resolves the current
* fighter's turn automatically if AI-controlled, otherwise waits for a
* player action via battlePlayerAttack/battlePlayerFlee. No-op if the
* battle isn't active or already has a result.
*/
void battleUpdate(void);
/**
* Rebuilds BATTLE.turnOrder/turnCount from every currently living
* fighter, sorted by speed descending.
*
* @param applyEncounterBias If true, reorders the freshly speed-sorted
* queue so BATTLE.encounterType's favoured team goes first (used only
* for the opening round).
*/
void battleBuildTurnOrder(const bool_t applyEncounterBias);
/**
* Stably partitions BATTLE.turnOrder so every fighter on the given team
* comes first, preserving each side's relative (speed-sorted) order.
*
* @param team The team to move to the front of the turn order.
*/
void battleMoveTeamFirst(const battlefighterteam_t team);
/**
* Picks an AI target for fighter: the lowest-health living fighter on
* the opposing team.
*
* @param fighter The AI-controlled fighter choosing a target.
* @return The chosen target, or NULL if the opposing team has no
* living fighters.
*/
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
+34
View File
@@ -34,6 +34,22 @@ typedef struct cutscene_s {
#define CUTSCENE_TEXT(TEXT) \ #define CUTSCENE_TEXT(TEXT) \
{ .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { .text = TEXT } } { .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { .text = TEXT } }
#define CUTSCENE_TEXT_MINI(TEXT, X, Y, Z, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI, \
.textMini = { \
.text = TEXT, \
.position = { X, Y, Z }, \
.duration = DURATION \
} \
}
#define CUTSCENE_TEXT_MINI_HIDE(INDEX) \
{ \
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE, \
.textMiniHide = { .index = INDEX } \
}
#define CUTSCENE_WAIT(WAIT) \ #define CUTSCENE_WAIT(WAIT) \
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT } { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
@@ -128,6 +144,24 @@ typedef struct cutscene_s {
#define CUTSCENE_FADE_FROM_WHITE(DURATION) \ #define CUTSCENE_FADE_FROM_WHITE(DURATION) \
CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR) CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR)
#define CUTSCENE_EMOJI(ENTITY_INDEX, EMOJI_TYPE, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_EMOJI, \
.emoji = { \
.entityIndex = ENTITY_INDEX, \
.emojiType = EMOJI_TYPE, \
.duration = DURATION \
} \
}
// AMOUNT ranges 0 (no shake) to 4 (three tiles): 1 is half a tile, 2 is
// a full tile, 3 is two tiles, and 4 is three tiles.
#define CUTSCENE_SHAKE(AMOUNT, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_SHAKE, \
.shake = { .amount = AMOUNT, .duration = DURATION } \
}
#define CUTSCENE_SET_PAUSE(FLAGS) \ #define CUTSCENE_SET_PAUSE(FLAGS) \
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) } { .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
+15
View File
@@ -37,6 +37,7 @@ void cutsceneSystemStartCutsceneWith(
CUTSCENE_SYSTEM.entityLastCreated = NULL; CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL; CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0. CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
cutsceneSystemNext(); cutsceneSystemNext();
} }
@@ -65,6 +66,7 @@ void cutsceneSystemNext() {
CUTSCENE_SYSTEM.entityLastCreated = NULL; CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL; CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
return; return;
} }
@@ -130,6 +132,18 @@ uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
return areaId; return areaId;
} }
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
if(index == CUTSCENE_TEXT_MINI_LAST_CREATED) {
assertTrue(
CUTSCENE_SYSTEM.textMiniLastCreated != CUTSCENE_TEXT_MINI_LAST_CREATED,
"CUTSCENE_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
"shown"
);
return CUTSCENE_SYSTEM.textMiniLastCreated;
}
return index;
}
void cutsceneSystemDispose() { void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.scene = NULL; CUTSCENE_SYSTEM.scene = NULL;
CUTSCENE_SYSTEM.currentItem = 0xFF; CUTSCENE_SYSTEM.currentItem = 0xFF;
@@ -139,4 +153,5 @@ void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.entityLastCreated = NULL; CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL; CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
} }
+11
View File
@@ -15,6 +15,7 @@ typedef struct entity_s entity_t;
#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC) #define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB) #define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
#define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF) #define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF)
#define CUTSCENE_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA)
// Maximum number of bytes a running cutscene may request via // Maximum number of bytes a running cutscene may request via
// cutscene_t.dataSize. // cutscene_t.dataSize.
@@ -29,6 +30,7 @@ typedef struct {
entity_t *entityLastCreated; entity_t *entityLastCreated;
entity_t *entityLastRef; entity_t *entityLastRef;
uint8_t areaLastCreated; uint8_t areaLastCreated;
uint8_t textMiniLastCreated;
// Data (used by the current item). // Data (used by the current item).
cutsceneitemdata_t data; cutsceneitemdata_t data;
@@ -86,6 +88,15 @@ entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
*/ */
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId); uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
/**
* Resolves a raw mini textbox slot index (or CUTSCENE_TEXT_MINI_LAST_CREATED
* sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index.
*
* @param index Raw slot index or sentinel value.
* @returns The resolved slot index.
*/
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
/** /**
* Advance to the next item in the cutscene. * Advance to the next item in the cutscene.
*/ */
@@ -14,3 +14,4 @@ add_subdirectory(entity)
add_subdirectory(item) add_subdirectory(item)
add_subdirectory(maparea) add_subdirectory(maparea)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(battle)
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscenestartbattle.c
)
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/battle/party.h"
#include "scene/scene.h"
void cutsceneStartBattleStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
const cutscenestartbattle_t *config = &item->startBattle;
battleInit();
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
battlefighter_t *member = partyGetOrderMember(i);
if(member == NULL) continue;
battlefighter_t *fighter = battleAddFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
member->stats, member->healthMax, member->mpMax
);
if(fighter == NULL) continue;
fighter->health = member->health;
fighter->mp = member->mp;
fighter->status = member->status;
}
for(uint8_t i = 0; i < config->enemyCount; i++) {
const cutscenestartbattleenemy_t *enemy = &config->enemies[i];
battleAddFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
enemy->stats, enemy->healthMax, enemy->mpMax
);
}
battleStart(config->encounterType, config->fleeAvailable);
sceneSet(SCENE_TYPE_BATTLE);
}
bool_t cutsceneStartBattleUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
if(BATTLE.result == BATTLE_RESULT_NONE) return false;
// Sync ally HP/MP back to the persistent party roster. Relies on
// ally fighters having been added to BATTLE.fighters in the same
// order partyGetOrderMember() iterates, starting at index 0 (see
// cutsceneStartBattleStart).
uint8_t allySlot = 0;
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
battlefighter_t *member = partyGetOrderMember(i);
if(member == NULL) continue;
battlefighter_t *fighter = &BATTLE.fighters[allySlot++];
member->health = fighter->health;
member->mp = fighter->mp;
member->status = fighter->status;
}
sceneSet(SCENE_TYPE_OVERWORLD);
battleDispose();
return true;
}
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/battle/battle.h"
#define CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX 4
typedef struct {
battlefighterstats_t stats;
uint16_t healthMax;
uint16_t mpMax;
} cutscenestartbattleenemy_t;
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
battleencountertype_t encounterType;
bool_t fleeAvailable;
uint8_t enemyCount;
cutscenestartbattleenemy_t enemies[CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX];
} cutscenestartbattle_t;
/**
* Starts a battle: seeds BATTLE with the party's active order members
* and the item's configured enemies, then switches to the battle
* scene.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneStartBattleStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Waits for the battle to produce a result, syncs ally HP/MP back to
* the party roster, then returns to the overworld scene.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once the battle has ended.
*/
bool_t cutsceneStartBattleUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+134 -120
View File
@@ -7,132 +7,146 @@
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
[CUTSCENE_ITEM_TYPE_NULL] = { 0 },
[CUTSCENE_ITEM_TYPE_TEXT] = {
.init = cutsceneTextStart,
.update = cutsceneTextUpdate
},
[CUTSCENE_ITEM_TYPE_TEXT_MINI] = {
.init = cutsceneTextMiniStart,
.update = cutsceneTextMiniUpdate
},
[CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE] = {
.init = cutsceneTextMiniHideStart,
.update = cutsceneTextMiniHideUpdate
},
[CUTSCENE_ITEM_TYPE_CALLBACK] = {
.init = cutsceneCallbackStart,
.update = cutsceneCallbackUpdate
},
[CUTSCENE_ITEM_TYPE_WAIT] = {
.init = cutsceneWaitStart,
.update = cutsceneWaitUpdate
},
[CUTSCENE_ITEM_TYPE_CUTSCENE] = {
.init = cutsceneCutsceneStart,
.update = cutsceneCutsceneUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT] = {
.init = cutsceneEntityTeleportStart,
.update = cutsceneEntityTeleportUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO] = {
.init = cutsceneEntityWalkToStart,
.update = cutsceneEntityWalkToUpdate
},
[CUTSCENE_ITEM_TYPE_FADE] = {
.init = cutsceneFadeStart,
.update = cutsceneFadeUpdate
},
[CUTSCENE_ITEM_TYPE_SET_PAUSE] = {
.init = cutsceneSetPauseStart,
.update = cutsceneSetPauseUpdate
},
[CUTSCENE_ITEM_TYPE_CONCURRENT] = {
.init = cutsceneConcurrentStart,
.update = cutsceneConcurrentUpdate
},
[CUTSCENE_ITEM_TYPE_ITEM_GIVE] = {
.init = cutsceneItemGiveStart,
.update = cutsceneItemGiveUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_REMOVE] = {
.init = cutsceneEntityRemoveStart,
.update = cutsceneEntityRemoveUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_ADD] = {
.init = cutsceneEntityAddStart,
.update = cutsceneEntityAddUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_TURN] = {
.init = cutsceneEntityTurnStart,
.update = cutsceneEntityTurnUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY] = {
.init = cutsceneEntityWalkToEntityStart,
.update = cutsceneEntityWalkToEntityUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_ADD] = {
.init = cutsceneMapAreaAddStart,
.update = cutsceneMapAreaAddUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE] = {
.init = cutsceneMapAreaRemoveStart,
.update = cutsceneMapAreaRemoveUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT] = {
.init = cutsceneMapAreaWaitStart,
.update = cutsceneMapAreaWaitUpdate
},
[CUTSCENE_ITEM_TYPE_START_BATTLE] = {
.init = cutsceneStartBattleStart,
.update = cutsceneStartBattleUpdate
},
[CUTSCENE_ITEM_TYPE_EMOJI] = {
.init = cutsceneEmojiStart,
.update = cutsceneEmojiUpdate
},
[CUTSCENE_ITEM_TYPE_SHAKE] = {
.init = cutsceneShakeStart,
.update = cutsceneShakeUpdate
}
};
void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) { void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
switch(item->type) { cutsceneiteminitcallback_t *init = CUTSCENE_ITEM_CALLBACKS[item->type].init;
case CUTSCENE_ITEM_TYPE_TEXT: if(init != NULL) init(item, data);
cutsceneTextStart(item, data); }
break;
case CUTSCENE_ITEM_TYPE_CALLBACK: bool_t cutsceneItemUpdate(
cutsceneCallbackStart(item, data); const cutsceneitem_t *item,
break; cutsceneitemdata_t *data
) {
cutsceneitemupdatecallback_t *update =
CUTSCENE_ITEM_CALLBACKS[item->type].update;
if(update == NULL) return false;
case CUTSCENE_ITEM_TYPE_WAIT: return update(item, data);
cutsceneWaitStart(item, data); }
break;
case CUTSCENE_ITEM_TYPE_CUTSCENE: void cutsceneCutsceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene); if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
cutsceneEntityTeleportStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO:
cutsceneEntityWalkToStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_FADE:
cutsceneFadeStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
cutsceneSetPauseStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_CONCURRENT:
cutsceneConcurrentStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
cutsceneItemGiveStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
cutsceneEntityRemoveStart(item, data);
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:
break;
}
} }
bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) { bool_t cutsceneCutsceneUpdate(
switch(item->type) { const cutsceneitem_t *item,
case CUTSCENE_ITEM_TYPE_TEXT: cutsceneitemdata_t *data
return cutsceneTextUpdate(item, data); ) {
case CUTSCENE_ITEM_TYPE_CALLBACK:
return cutsceneCallbackUpdate(item, data);
case CUTSCENE_ITEM_TYPE_WAIT:
return cutsceneWaitUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
return cutsceneEntityTeleportUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO:
return cutsceneEntityWalkToUpdate(item, data);
case CUTSCENE_ITEM_TYPE_FADE:
return cutsceneFadeUpdate(item, data);
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
return cutsceneSetPauseUpdate(item, data);
case CUTSCENE_ITEM_TYPE_CONCURRENT:
return cutsceneConcurrentUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
return cutsceneItemGiveUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
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:
return false; return false;
}
} }
+63 -1
View File
@@ -17,17 +17,25 @@
#include "entity/cutsceneentityturn.h" #include "entity/cutsceneentityturn.h"
#include "entity/cutsceneentitywalktoentity.h" #include "entity/cutsceneentitywalktoentity.h"
#include "ui/cutscenetext.h" #include "ui/cutscenetext.h"
#include "ui/cutscenetextmini.h"
#include "ui/cutscenetextminihide.h"
#include "ui/cutscenefade.h" #include "ui/cutscenefade.h"
#include "ui/cutsceneemoji.h"
#include "ui/cutsceneshake.h"
#include "item/cutsceneitemgive.h" #include "item/cutsceneitemgive.h"
#include "maparea/cutscenemapareaadd.h" #include "maparea/cutscenemapareaadd.h"
#include "maparea/cutscenemaparearemove.h" #include "maparea/cutscenemaparearemove.h"
#include "maparea/cutscenemapareawait.h" #include "maparea/cutscenemapareawait.h"
#include "battle/cutscenestartbattle.h"
typedef struct cutscene_s cutscene_t; typedef struct cutscene_s cutscene_t;
typedef enum { typedef enum {
CUTSCENE_ITEM_TYPE_NULL, CUTSCENE_ITEM_TYPE_NULL,
CUTSCENE_ITEM_TYPE_TEXT, CUTSCENE_ITEM_TYPE_TEXT,
CUTSCENE_ITEM_TYPE_TEXT_MINI,
CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE,
CUTSCENE_ITEM_TYPE_CALLBACK, CUTSCENE_ITEM_TYPE_CALLBACK,
CUTSCENE_ITEM_TYPE_WAIT, CUTSCENE_ITEM_TYPE_WAIT,
CUTSCENE_ITEM_TYPE_CUTSCENE, CUTSCENE_ITEM_TYPE_CUTSCENE,
@@ -43,7 +51,12 @@ typedef enum {
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY,
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
CUTSCENE_ITEM_TYPE_START_BATTLE,
CUTSCENE_ITEM_TYPE_EMOJI,
CUTSCENE_ITEM_TYPE_SHAKE,
CUTSCENE_ITEM_TYPE_COUNT
} cutsceneitemtype_t; } cutsceneitemtype_t;
struct cutsceneitem_s { struct cutsceneitem_s {
@@ -51,6 +64,8 @@ struct cutsceneitem_s {
union { union {
cutscenetext_t text; cutscenetext_t text;
cutscenetextmini_t textMini;
cutscenetextminihide_t textMiniHide;
cutscenecallback_t callback; cutscenecallback_t callback;
cutscenewait_t wait; cutscenewait_t wait;
const cutscene_t *cutscene; const cutscene_t *cutscene;
@@ -67,6 +82,9 @@ struct cutsceneitem_s {
cutscenemapareaadd_t mapAreaAdd; cutscenemapareaadd_t mapAreaAdd;
cutscenemaparearemove_t mapAreaRemove; cutscenemaparearemove_t mapAreaRemove;
cutscenemapareawait_t mapAreaWait; cutscenemapareawait_t mapAreaWait;
cutscenestartbattle_t startBattle;
cutsceneemoji_t emoji;
cutsceneshake_t shake;
}; };
}; };
@@ -77,6 +95,24 @@ typedef union cutsceneitemdata_u {
cutscenemapareawaitdata_t mapAreaWait; cutscenemapareawaitdata_t mapAreaWait;
} cutsceneitemdata_t; } cutsceneitemdata_t;
typedef void (cutsceneiteminitcallback_t)(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
typedef bool_t (cutsceneitemupdatecallback_t)(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
typedef struct {
cutsceneiteminitcallback_t *init;
cutsceneitemupdatecallback_t *update;
} cutsceneitemcallbacks_t;
extern cutsceneitemcallbacks_t
CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT];
/** /**
* Start the given cutscene item. * Start the given cutscene item.
* *
@@ -99,3 +135,29 @@ bool_t cutsceneItemUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
cutsceneitemdata_t *data cutsceneitemdata_t *data
); );
/**
* Starts a nested-cutscene item, handing control over to the
* referenced cutscene.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneCutsceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a nested-cutscene item. By the time this would run, control
* has already moved on to the referenced cutscene, so this always
* reports incomplete.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns false always.
*/
bool_t cutsceneCutsceneUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -7,7 +7,7 @@
#include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/item/itemgive.h" #include "rpg/item/itemgive.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
void cutsceneItemGiveStart( void cutsceneItemGiveStart(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -6,5 +6,9 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
cutscenetext.c cutscenetext.c
cutscenetextmini.c
cutscenetextminihide.c
cutscenefade.c cutscenefade.c
cutsceneemoji.c
cutsceneshake.c
) )
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
void cutsceneEmojiStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
entity_t *entity = cutsceneSystemGetEntity(item->emoji.entityIndex);
uiEmojiAdd(entity->id, item->emoji.duration, item->emoji.emojiType);
}
bool_t cutsceneEmojiUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "ui/rpg/uiemoji.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t entityIndex;
float_t duration;
uiemojitype_t emojiType;
} cutsceneemoji_t;
/**
* Starts an emoji step (shows an emoji above the entity for the given
* duration, then completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEmojiStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an emoji step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneEmojiUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,23 @@
/**
* 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/rpgcamera.h"
void cutsceneShakeStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
rpgCameraShake(item->shake.amount, item->shake.duration);
}
bool_t cutsceneShakeUpdate(
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 "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t amount;
float_t duration;
} cutsceneshake_t;
/**
* Starts a camera shake item (kicks off the shake on the RPG camera).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneShakeStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a camera shake item. The shake itself runs asynchronously on
* the RPG camera, so this always completes immediately.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneShakeUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+1 -1
View File
@@ -6,7 +6,7 @@
*/ */
#include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/item/cutsceneitem.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
void cutsceneTextStart( void cutsceneTextStart(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
void cutsceneTextMiniStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t index = uiTextboxMiniListGetNext();
uiTextboxMiniShow(
&UI_TEXTBOX_MINI_LIST[index],
item->textMini.text,
item->textMini.position,
item->textMini.duration,
NULL,
NULL
);
CUTSCENE_SYSTEM.textMiniLastCreated = index;
}
bool_t cutsceneTextMiniUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
#define CUTSCENE_TEXT_MINI_MAX_CHARS 128
typedef struct {
char_t text[CUTSCENE_TEXT_MINI_MAX_CHARS];
vec3 position;
float_t duration;
} cutscenetextmini_t;
/**
* Starts a mini text item (shows a mini textbox at the given world
* position for the given duration, then completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneTextMiniStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a mini text item (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneTextMiniUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
void cutsceneTextMiniHideStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t index = cutsceneSystemGetTextMiniId(item->textMiniHide.index);
uiTextboxMiniClose(&UI_TEXTBOX_MINI_LIST[index]);
}
bool_t cutsceneTextMiniHideUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t index;
} cutscenetextminihide_t;
/**
* Starts a mini text hide step (closes the mini textbox immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneTextMiniHideStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a mini text hide step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneTextMiniHideUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -16,6 +16,10 @@ CUTSCENE(TEST_ONE, 0, DEFAULT,
CUTSCENE(TEST_TWO, 0, DEFAULT, CUTSCENE(TEST_TWO, 0, DEFAULT,
CUTSCENE_TEXT("Test Two."), CUTSCENE_TEXT("Test Two."),
CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0), CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0),
CUTSCENE_TEXT_MINI("Hello!", 4, 4, 0, 3.0f),
CUTSCENE_EMOJI(
CUTSCENE_ENTITY_LAST_CREATED, UI_EMOJI_EXCLAMATION_MARK, 2.0f
),
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_LAST_CREATED, 8, 2, 0), CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_LAST_CREATED, 8, 2, 0),
// CUTSCENE_CONCURRENT( // CUTSCENE_CONCURRENT(
// CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0), // CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0),
@@ -8,7 +8,7 @@
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
void entityInteractWith(entity_t *player, entity_t *target) { void entityInteractWith(entity_t *player, entity_t *target) {
assertNotNull(player, "Player entity pointer cannot be NULL"); assertNotNull(player, "Player entity pointer cannot be NULL");
+1 -1
View File
@@ -9,7 +9,7 @@
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "rpg/item/itemgive.h" #include "rpg/item/itemgive.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
void entityItemInit(entity_t *entity) { void entityItemInit(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL"); assertNotNull(entity, "Entity pointer cannot be NULL");
+5 -4
View File
@@ -6,6 +6,7 @@
# Sources # Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
item.c
inventory.c inventory.c
backpack.c backpack.c
itemgive.c itemgive.c
@@ -13,9 +14,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Item Definitions # Item Definitions
dusk_run_python( dusk_run_python(
dusk_item_csv_defs dusk_item_json_defs
tools.item tools.item
--csv ${CMAKE_CURRENT_SOURCE_DIR}/item.csv --json ${CMAKE_CURRENT_SOURCE_DIR}/item.json
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/item.h --output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/itemdef.h
) )
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_csv_defs) add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_json_defs)
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "item.h"
#include "assert/assert.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
errorret_t itemGetName(
const itemid_t item,
char_t *buffer,
const size_t bufferSize
) {
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
ITEMS[item].name,
0,
buffer,
bufferSize
));
errorOk();
}
-4
View File
@@ -1,4 +0,0 @@
id,type,weight
POTION,MEDICINE,1.0
POTATO,FOOD,0.5
APPLE,FOOD,0.3
1 id type weight
2 POTION MEDICINE 1.0
3 POTATO FOOD 0.5
4 APPLE FOOD 0.3
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "rpg/item/itemdef.h"
/**
* Gets the localized display name for an item.
*
* @param item The item ID to look up. Must not be ITEM_ID_NULL.
* @param buffer Buffer to write the localized name into.
* @param bufferSize Size of the buffer.
* @return Any error that occurs.
*/
errorret_t itemGetName(
const itemid_t item,
char_t *buffer,
const size_t bufferSize
);
+5
View File
@@ -0,0 +1,5 @@
[
{ "id": "POTION", "type": "MEDICINE", "weight": 1.0, "name": "potion" },
{ "id": "POTATO", "type": "FOOD", "weight": 0.5, "name": "potato" },
{ "id": "APPLE", "type": "FOOD", "weight": 0.3, "name": "apple" }
]
+9 -2
View File
@@ -7,18 +7,25 @@
#include "itemgive.h" #include "itemgive.h"
#include "rpg/item/backpack.h" #include "rpg/item/backpack.h"
#include "ui/rpg/uitextboxmain.h" #include "rpg/item/item.h"
#include "ui/rpg/textbox/uitextboxmain.h"
#include "util/string.h" #include "util/string.h"
#include "error/error.h"
#define ITEM_GIVE_NAME_MAX_CHARS 32
void itemGive(const itemid_t item, const uint8_t quantity) { void itemGive(const itemid_t item, const uint8_t quantity) {
backpackAdd(item, quantity); backpackAdd(item, quantity);
char_t name[ITEM_GIVE_NAME_MAX_CHARS];
errorCatch(itemGetName(item, name, ITEM_GIVE_NAME_MAX_CHARS));
char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS]; char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS];
stringFormat( stringFormat(
msg, msg,
ITEM_GIVE_MESSAGE_MAX_CHARS - 1, ITEM_GIVE_MESSAGE_MAX_CHARS - 1,
"Received %s x%u", "Received %s x%u",
ITEMS[item].name, name,
(uint32_t)quantity (uint32_t)quantity
); );
uiTextboxMainSetText(msg); uiTextboxMainSetText(msg);
+107 -63
View File
@@ -21,21 +21,13 @@ errorret_t mapInit() {
memoryZero(&MAP, sizeof(map_t)); memoryZero(&MAP, sizeof(map_t));
MAP.loaded = true; MAP.loaded = true;
MAP.loadPosition = (chunkpos_t){
-(MAP_CHUNK_SKIN),
-(MAP_CHUNK_SKIN),
-(MAP_CHUNK_SKIN)
};
chunkindex_t i = 0; chunkindex_t i = 0;
for(chunkunit_t z = 0; z < MAP_LOADED_CHUNK_DEPTH; z++) { for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
for(chunkunit_t y = 0; y < MAP_LOADED_CHUNK_HEIGHT; y++) { for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
for(chunkunit_t x = 0; x < MAP_LOADED_CHUNK_WIDTH; x++) { for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
chunk_t *chunk = &MAP.chunks[i++]; chunk_t *chunk = &MAP.chunks[i++];
chunk->position = (chunkpos_t){ chunk->position = (chunkpos_t){
MAP.loadPosition.x + (chunkunit_t)x, (chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z
MAP.loadPosition.y + (chunkunit_t)y,
MAP.loadPosition.z + (chunkunit_t)z
}; };
errorChain(mapChunkLoad(chunk)); errorChain(mapChunkLoad(chunk));
} }
@@ -54,48 +46,23 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
if(!mapIsLoaded()) errorThrow("No map loaded"); if(!mapIsLoaded()) errorThrow("No map loaded");
if(chunkPositionIsEqual(newPos, MAP.chunkPosition)) errorOk(); if(chunkPositionIsEqual(newPos, MAP.chunkPosition)) errorOk();
// If the new render window still fits inside the current loaded area,
// just remap chunkOrder — no asset I/O needed.
const chunkpos_t lp = MAP.loadPosition;
if(
newPos.x >= lp.x &&
newPos.y >= lp.y &&
newPos.z >= lp.z &&
newPos.x + MAP_CHUNK_WIDTH <= lp.x + MAP_LOADED_CHUNK_WIDTH &&
newPos.y + MAP_CHUNK_HEIGHT <= lp.y + MAP_LOADED_CHUNK_HEIGHT &&
newPos.z + MAP_CHUNK_DEPTH <= lp.z + MAP_LOADED_CHUNK_DEPTH
) {
MAP.chunkPosition = newPos;
mapRebuildChunkOrder();
errorOk();
}
// Render window fell outside the loaded area — re-centre the load
// window on the new render position (skin buffer on every side).
const chunkpos_t newLoadPos = {
newPos.x - MAP_CHUNK_SKIN,
newPos.y - MAP_CHUNK_SKIN,
newPos.z - MAP_CHUNK_SKIN
};
// Separate loaded chunks into "keep" and "free" buckets. // Separate loaded chunks into "keep" and "free" buckets.
chunkindex_t chunksFreed[MAP_LOADED_CHUNK_COUNT]; chunkindex_t chunksFreed[MAP_CHUNK_COUNT];
uint32_t freedCount = 0; uint32_t freedCount = 0;
// Use a boolean grid so the inner load loop can check O(1). // Use a boolean grid so the inner load loop can check O(1).
bool_t posLoaded[MAP_LOADED_CHUNK_WIDTH][MAP_LOADED_CHUNK_HEIGHT] bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH];
[MAP_LOADED_CHUNK_DEPTH];
memoryZero(posLoaded, sizeof(posLoaded)); memoryZero(posLoaded, sizeof(posLoaded));
for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i]; chunk_t *chunk = &MAP.chunks[i];
chunkunit_t rx = chunk->position.x - newLoadPos.x; chunkunit_t rx = chunk->position.x - newPos.x;
chunkunit_t ry = chunk->position.y - newLoadPos.y; chunkunit_t ry = chunk->position.y - newPos.y;
chunkunit_t rz = chunk->position.z - newLoadPos.z; chunkunit_t rz = chunk->position.z - newPos.z;
if( if(
rx >= 0 && rx < MAP_LOADED_CHUNK_WIDTH && rx >= 0 && rx < MAP_CHUNK_WIDTH &&
ry >= 0 && ry < MAP_LOADED_CHUNK_HEIGHT && ry >= 0 && ry < MAP_CHUNK_HEIGHT &&
rz >= 0 && rz < MAP_LOADED_CHUNK_DEPTH rz >= 0 && rz < MAP_CHUNK_DEPTH
) { ) {
posLoaded[rx][ry][rz] = true; posLoaded[rx][ry][rz] = true;
} else { } else {
@@ -104,23 +71,22 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
} }
} }
for(chunkunit_t z = 0; z < MAP_LOADED_CHUNK_DEPTH; z++) { for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
for(chunkunit_t y = 0; y < MAP_LOADED_CHUNK_HEIGHT; y++) { for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
for(chunkunit_t x = 0; x < MAP_LOADED_CHUNK_WIDTH; x++) { for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
if(posLoaded[x][y][z]) continue; if(posLoaded[x][y][z]) continue;
assertTrue(freedCount > 0, "No free chunk slot available."); assertTrue(freedCount > 0, "No free chunk slot available.");
chunk_t *chunk = &MAP.chunks[chunksFreed[--freedCount]]; chunk_t *chunk = &MAP.chunks[chunksFreed[--freedCount]];
chunk->position = (chunkpos_t){ chunk->position = (chunkpos_t){
newLoadPos.x + (chunkunit_t)x, newPos.x + (chunkunit_t)x,
newLoadPos.y + (chunkunit_t)y, newPos.y + (chunkunit_t)y,
newLoadPos.z + (chunkunit_t)z newPos.z + (chunkunit_t)z
}; };
errorChain(mapChunkLoad(chunk)); errorChain(mapChunkLoad(chunk));
} }
} }
} }
MAP.loadPosition = newLoadPos;
MAP.chunkPosition = newPos; MAP.chunkPosition = newPos;
mapRebuildChunkOrder(); mapRebuildChunkOrder();
errorOk(); errorOk();
@@ -131,13 +97,16 @@ errorret_t mapUpdate() {
} }
errorret_t mapDispose() { errorret_t mapDispose() {
for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
mapChunkUnload(&MAP.chunks[i]); mapChunkUnload(&MAP.chunks[i]);
} }
errorOk(); errorOk();
} }
void mapChunkUnload(chunk_t *chunk) { void mapChunkUnload(chunk_t *chunk) {
mapChunkLoadQueueRemove(chunk);
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
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]];
@@ -157,9 +126,10 @@ void mapChunkUnload(chunk_t *chunk) {
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
} }
// modelEntries are borrowed pointers, not independently locked - the
// chunk asset entry (released above) is what actually holds the ref on
// each model, so nothing to unlock here, just drop our own copies.
for(uint8_t m = 0; m < chunk->meshCount; m++) { for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->modelEntries[m] == NULL) continue;
assetUnlockEntry(chunk->modelEntries[m]);
chunk->modelEntries[m] = NULL; chunk->modelEntries[m] = NULL;
} }
chunk->meshCount = 0; chunk->meshCount = 0;
@@ -168,6 +138,9 @@ void mapChunkUnload(chunk_t *chunk) {
errorret_t mapChunkLoad(chunk_t *chunk) { errorret_t mapChunkLoad(chunk_t *chunk) {
if(!mapIsLoaded()) errorThrow("No map loaded"); if(!mapIsLoaded()) errorThrow("No map loaded");
mapChunkLoadQueueRemove(chunk);
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
if(chunk->dcfEntry != NULL) { if(chunk->dcfEntry != NULL) {
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded); eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError); eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
@@ -195,12 +168,65 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
errorOk(); errorOk();
} }
assertTrue(
MAP.loadQueueCount < MAP_CHUNK_COUNT,
"Chunk load queue overflow"
);
MAP.loadQueue[MAP.loadQueueCount++] = chunk;
mapChunkLoadNext();
errorOk();
}
void mapChunkLoadNext() {
if(MAP.loadingChunk != NULL) return;
if(MAP.loadQueueCount == 0) return;
chunk_t *chunk = MAP.loadQueue[0];
for(uint32_t i = 1; i < MAP.loadQueueCount; i++) {
MAP.loadQueue[i - 1] = MAP.loadQueue[i];
}
MAP.loadQueueCount--;
MAP.loadingChunk = chunk;
char_t name[64];
stringFormat(
name, sizeof(name),
"chunks/%d_%d_%d.dcf",
(int32_t)chunk->position.x,
(int32_t)chunk->position.y,
(int32_t)chunk->position.z
);
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL); assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL);
assertNotNull(entry, "Failed to get chunk asset entry"); assertNotNull(entry, "Failed to get chunk asset entry");
chunk->dcfEntry = entry;
// The entry may already be resident from an earlier load that hasn't been
// reaped yet - in that case onLoaded/onError already fired once and never
// will again, so handle the terminal state directly instead of waiting on
// a subscription that would never trigger.
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
mapChunkLoaded(entry, chunk);
return;
}
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
mapChunkLoadError(entry, chunk);
return;
}
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk); eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
eventSubscribe(&entry->onError, mapChunkLoadError, chunk); eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
chunk->dcfEntry = entry; }
errorOk();
void mapChunkLoadQueueRemove(chunk_t *chunk) {
for(uint32_t i = 0; i < MAP.loadQueueCount; i++) {
if(MAP.loadQueue[i] != chunk) continue;
for(uint32_t j = i + 1; j < MAP.loadQueueCount; j++) {
MAP.loadQueue[j - 1] = MAP.loadQueue[j];
}
MAP.loadQueueCount--;
return;
}
} }
@@ -312,7 +338,7 @@ entity_t * mapSpawnEntity(
void mapRebuildChunkOrder() { void mapRebuildChunkOrder() {
memoryZero(MAP.chunkOrder, sizeof(MAP.chunkOrder)); memoryZero(MAP.chunkOrder, sizeof(MAP.chunkOrder));
for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i]; chunk_t *chunk = &MAP.chunks[i];
const chunkpos_t rel = { const chunkpos_t rel = {
chunk->position.x - MAP.chunkPosition.x, chunk->position.x - MAP.chunkPosition.x,
@@ -331,17 +357,23 @@ void mapRebuildChunkOrder() {
void mapChunkLoadError(void *params, void *user) { void mapChunkLoadError(void *params, void *user) {
assertNotNull(params, "mapChunkLoadError: params cannot be NULL"); assertNotNull(params, "mapChunkLoadError: params cannot be NULL");
assertNotNull(user, "mapChunkLoadError: user cannot be NULL"); assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
assetentry_t *entry = (assetentry_t *)params;
chunk_t *chunk = (chunk_t *)user; chunk_t *chunk = (chunk_t *)user;
if(chunk->dcfEntry != (assetentry_t *)params) return; if(chunk->dcfEntry != entry) return;
consolePrint( consolePrint(
"Chunk load error: %d %d %d", "Chunk load error: %d %d %d",
(int32_t)chunk->position.x, (int32_t)chunk->position.x,
(int32_t)chunk->position.y, (int32_t)chunk->position.y,
(int32_t)chunk->position.z (int32_t)chunk->position.z
); );
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
eventUnsubscribe(&entry->onError, mapChunkLoadError);
assetUnlockEntry(chunk->dcfEntry); assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles)); memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
mapChunkLoadNext();
} }
void mapChunkLoaded(void *params, void *user) { void mapChunkLoaded(void *params, void *user) {
@@ -385,10 +417,22 @@ void mapChunkLoaded(void *params, void *user) {
vec3 pos; vec3 pos;
glm_vec3_add(wpf, scaledOffset, pos); glm_vec3_add(wpf, scaledOffset, pos);
glm_translate_make(chunk->meshModels[m], pos); glm_translate_make(chunk->meshModels[m], pos);
// Borrow the pointer rather than stealing it - the chunk asset entry
// keeps its own lock on each model (taken once while it loaded) and we
// keep the chunk asset entry itself locked (see below), so the models
// stay valid for as long as this chunk_t is using them. The entry may
// now be reused by a later mapChunkLoad for a different chunk_t once we
// eventually unlock it in mapChunkUnload, at which point its
// modelEntries must still be intact for that next reuse to copy from.
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m]; chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
entry->data.chunk.modelEntries[m] = NULL;
} }
assetUnlockEntry(chunk->dcfEntry); eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
chunk->dcfEntry = NULL; eventUnsubscribe(&entry->onError, mapChunkLoadError);
// Deliberately keep chunk->dcfEntry locked and set - it is what keeps the
// chunk asset entry (and therefore its model locks) alive for as long as
// this chunk_t is displaying it. Released in mapChunkUnload instead.
chunk->meshCount = meshCount; chunk->meshCount = meshCount;
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
mapChunkLoadNext();
} }
+22 -2
View File
@@ -15,10 +15,15 @@
typedef struct map_s { typedef struct map_s {
bool_t loaded; bool_t loaded;
chunk_t chunks[MAP_LOADED_CHUNK_COUNT]; chunk_t chunks[MAP_CHUNK_COUNT];
chunk_t *chunkOrder[MAP_CHUNK_COUNT]; chunk_t *chunkOrder[MAP_CHUNK_COUNT];
chunkpos_t chunkPosition; chunkpos_t chunkPosition;
chunkpos_t loadPosition;
// Only one chunk may be mid-load (asset locked & awaiting onLoaded/
// onError) at any given time - everything else waits here in FIFO order.
chunk_t *loadQueue[MAP_CHUNK_COUNT];
uint32_t loadQueueCount;
chunk_t *loadingChunk;
} map_t; } map_t;
extern map_t MAP; extern map_t MAP;
@@ -74,6 +79,21 @@ void mapChunkUnload(chunk_t* chunk);
*/ */
errorret_t mapChunkLoad(chunk_t* chunk); errorret_t mapChunkLoad(chunk_t* chunk);
/**
* Starts loading the next queued chunk, if no chunk is currently mid-load.
* Called after mapChunkLoad enqueues a chunk, and again after the
* currently-loading chunk finishes (or is unloaded) to advance the queue.
*/
void mapChunkLoadNext();
/**
* Removes a chunk from the load queue if present. Used when a chunk is
* re-queued or unloaded before its turn to load has come up.
*
* @param chunk The chunk to remove from the load queue.
*/
void mapChunkLoadQueueRemove(chunk_t *chunk);
/** /**
* Callback invoked when a chunk DCF asset fails to load. Fills the * Callback invoked when a chunk DCF asset fails to load. Fills the
* chunk tiles with TILE_SHAPE_GROUND as a fallback. * chunk tiles with TILE_SHAPE_GROUND as a fallback.
+1 -1
View File
@@ -73,7 +73,7 @@ bool_t mapAreaIsChunkOverlappingOrInside(
bool_t mapAreaCanUnload(const maparea_t *area) { bool_t mapAreaCanUnload(const maparea_t *area) {
assertNotNull(area, "Map area pointer cannot be NULL"); assertNotNull(area, "Map area pointer cannot be NULL");
for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false; if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false;
} }
-10
View File
@@ -30,16 +30,6 @@
#define MAP_CHUNK_DEPTH 4 #define MAP_CHUNK_DEPTH 4
#define MAP_CHUNK_COUNT (MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT * MAP_CHUNK_DEPTH) #define MAP_CHUNK_COUNT (MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT * MAP_CHUNK_DEPTH)
// Extra chunks loaded on every side beyond the render window.
// The render window can drift MAP_CHUNK_SKIN chunks in any direction
// before a load/unload cycle is triggered.
#define MAP_CHUNK_SKIN 1
#define MAP_LOADED_CHUNK_WIDTH (MAP_CHUNK_WIDTH + MAP_CHUNK_SKIN)
#define MAP_LOADED_CHUNK_HEIGHT (MAP_CHUNK_HEIGHT + MAP_CHUNK_SKIN)
#define MAP_LOADED_CHUNK_DEPTH (MAP_CHUNK_DEPTH + MAP_CHUNK_SKIN)
#define MAP_LOADED_CHUNK_COUNT \
(MAP_LOADED_CHUNK_WIDTH * MAP_LOADED_CHUNK_HEIGHT * MAP_LOADED_CHUNK_DEPTH)
#define ENTITY_COUNT 32 #define ENTITY_COUNT 32
typedef int16_t worldunit_t; typedef int16_t worldunit_t;
+8
View File
@@ -15,6 +15,7 @@
#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 "rpg/battle/party.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
#include "time/time.h" #include "time/time.h"
#include "rpgcamera.h" #include "rpgcamera.h"
#include "util/memory.h" #include "util/memory.h"
@@ -22,6 +23,8 @@
#include "assert/assert.h" #include "assert/assert.h"
#include "console/console.h" #include "console/console.h"
#include "ui/rpg/uiemoji.h"
void rpgTestAreaCallback(entity_t *entity, const uint8_t trigger) { void rpgTestAreaCallback(entity_t *entity, const uint8_t trigger) {
consolePrint("rpgTestAreaCallback: trigger=%u", trigger); consolePrint("rpgTestAreaCallback: trigger=%u", trigger);
} }
@@ -59,6 +62,11 @@ errorret_t rpgInit(void) {
entityItemSet(itemEnt, ITEM_ID_POTION, 1); entityItemSet(itemEnt, ITEM_ID_POTION, 1);
entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 }); entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 });
// TEST: Give the player a starting assortment of items.
backpackAdd(ITEM_ID_POTION, 5);
backpackAdd(ITEM_ID_POTATO, 3);
backpackAdd(ITEM_ID_APPLE, 8);
// TEST: Create a test map area. // TEST: Create a test map area.
uint8_t areaIndex = mapAreaAdd( uint8_t areaIndex = mapAreaAdd(
(worldpos_t){ 11, 3, 0 }, (worldpos_t){ 11, 3, 0 },
+57
View File
@@ -7,12 +7,18 @@
#include "rpgcamera.h" #include "rpgcamera.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/random.h"
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "rpg/overworld/map.h" #include "rpg/overworld/map.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "time/time.h"
#include "display/screen/screen.h" #include "display/screen/screen.h"
static const float_t RPG_CAMERA_SHAKE_AMOUNTS[] = {
0.0f, 0.5f, 1.0f, 2.0f, 3.0f
};
rpgcamera_t RPG_CAMERA; rpgcamera_t RPG_CAMERA;
void rpgCameraInit(void) { void rpgCameraInit(void) {
@@ -20,6 +26,13 @@ void rpgCameraInit(void) {
RPG_CAMERA.projectionDirty = true; RPG_CAMERA.projectionDirty = true;
} }
void rpgCameraShake(uint8_t amount, float_t duration) {
assertTrue(amount <= 4, "Camera shake amount must be between 0 and 4");
RPG_CAMERA.shakeAmount = RPG_CAMERA_SHAKE_AMOUNTS[amount];
RPG_CAMERA.shakeDuration = duration;
RPG_CAMERA.shakeTime = 0.0f;
}
void rpgCameraGetPosition(vec3 out) { void rpgCameraGetPosition(vec3 out) {
switch(RPG_CAMERA.mode) { switch(RPG_CAMERA.mode) {
case RPG_CAMERA_MODE_FREE: case RPG_CAMERA_MODE_FREE:
@@ -56,7 +69,51 @@ void rpgCameraUpdateProjection(void) {
); );
} }
void rpgCameraUpdateEye(void) {
float_t fov = glm_rad(RPG_CAMERA_FOV);
float_t pixelsPerUnit = TILE_SIZE_PIXELS;
float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit;
float_t z = (worldH * 0.5f) / tanf(fov * 0.5f);
float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS);
vec3 target;
rpgCameraGetPosition(target);
glm_vec3_add(target, (vec3){ 0.5f, 0.5f, 0.5f }, target);
if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) {
float_t t = 1.0f - (RPG_CAMERA.shakeTime / RPG_CAMERA.shakeDuration);
float_t magnitude = RPG_CAMERA.shakeAmount * t;
target[0] += randomFloat(-magnitude, magnitude);
target[2] += randomFloat(-magnitude, magnitude);
}
glm_lookat(
(vec3){ target[0], target[1] + offset, target[2] + z },
target,
(vec3){ 0, 1, 0 }, // up
RPG_CAMERA.eye
);
}
void rpgCameraToScreen(vec3 worldPos, vec2 out) {
mat4 viewProj;
glm_mat4_mul(RPG_CAMERA.projection, RPG_CAMERA.eye, viewProj);
vec4 viewport = {
0.0f, 0.0f, (float_t)SCREEN.width, (float_t)SCREEN.height
};
vec3 window;
glm_project(worldPos, viewProj, viewport, window);
out[0] = window[0];
out[1] = (float_t)SCREEN.height - window[1];
}
errorret_t rpgCameraUpdate(void) { errorret_t rpgCameraUpdate(void) {
if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) {
RPG_CAMERA.shakeTime += TIME.delta;
}
if(!mapIsLoaded()) errorOk(); if(!mapIsLoaded()) errorOk();
vec3 pos; vec3 pos;
+32
View File
@@ -30,6 +30,10 @@ typedef struct {
mat4 eye; mat4 eye;
mat4 projection; mat4 projection;
bool_t projectionDirty; bool_t projectionDirty;
float_t shakeAmount;
float_t shakeDuration;
float_t shakeTime;
} rpgcamera_t; } rpgcamera_t;
extern rpgcamera_t RPG_CAMERA; extern rpgcamera_t RPG_CAMERA;
@@ -60,3 +64,31 @@ errorret_t rpgCameraUpdate(void);
* is recomputed every call. * is recomputed every call.
*/ */
void rpgCameraUpdateProjection(void); void rpgCameraUpdateProjection(void);
/**
* Recomputes the camera eye/view matrix from the camera's current mode
* and position, and stores it in RPG_CAMERA.eye. Unlike the projection
* matrix this is never cached, since the camera position can change
* every frame.
*/
void rpgCameraUpdateEye(void);
/**
* Shakes the RPG camera, randomly offsetting its position by a
* decreasing amount over the given duration.
*
* @param amount Shake strength from 0 (no shake) to 4 (three tiles).
* 1 is half a tile, 2 is a full tile, 3 is two tiles, and 4 is three
* tiles.
* @param duration How long the shake lasts, in seconds.
*/
void rpgCameraShake(uint8_t amount, float_t duration);
/**
* Converts a world-space position to screen-space pixel coordinates,
* using the camera's current eye and projection matrices.
*
* @param worldPos The world-space position to convert.
* @param out Output vec2 filled with the screen-space pixel position.
*/
void rpgCameraToScreen(vec3 worldPos, vec2 out);
+1
View File
@@ -11,3 +11,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Subdirs # Subdirs
add_subdirectory(overworld) add_subdirectory(overworld)
add_subdirectory(battle)
+9
View File
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
scenebattle.c
)
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "scenebattle.h"
#include "rpg/battle/battle.h"
errorret_t sceneBattleInit(scenedata_t *sceneData) {
errorOk();
}
errorret_t sceneBattleUpdate(scenedata_t *sceneData) {
battleUpdate();
errorOk();
}
errorret_t sceneBattleRender(scenedata_t *sceneData) {
errorOk();
}
errorret_t sceneBattleDispose(scenedata_t *sceneData) {
errorOk();
}
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "scene/scenebase.h"
typedef struct {
} scenebattle_t;
/**
* Initializes the battle scene. The battle itself (BATTLE global) is
* expected to already be started, e.g. by a StartBattle cutscene item.
*
* @param sceneData The scene data used for this scene.
* @return An error if the init failed, or errorOk() if it succeeded.
*/
errorret_t sceneBattleInit(scenedata_t *sceneData);
/**
* Updates the battle scene, ticking the battle simulation.
*
* @param sceneData The scene data used for this scene.
* @return An error if the update failed, or errorOk() if it succeeded.
*/
errorret_t sceneBattleUpdate(scenedata_t *sceneData);
/**
* Renders the battle scene.
*
* @param sceneData The scene data used for this scene.
* @return An error if the render failed, or errorOk() if it succeeded.
*/
errorret_t sceneBattleRender(scenedata_t *sceneData);
/**
* Disposes the battle scene.
*
* @param sceneData The scene data used for this scene.
* @return An error if the dispose failed, or errorOk() if it succeeded.
*/
errorret_t sceneBattleDispose(scenedata_t *sceneData);
+105 -55
View File
@@ -24,6 +24,8 @@
#include "asset/loader/assetloader.h" #include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h" #include "asset/loader/assetentry.h"
#include "util/memory.h"
errorret_t sceneOverworldInit(scenedata_t *sceneData) { errorret_t sceneOverworldInit(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null"); assertNotNull(sceneData, "Scene data cannot be null");
errorOk(); errorOk();
@@ -31,71 +33,127 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData) {
errorret_t sceneOverworldUpdate(scenedata_t *sceneData) { errorret_t sceneOverworldUpdate(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null"); assertNotNull(sceneData, "Scene data cannot be null");
errorOk(); errorOk();
} }
errorret_t sceneOverworldRender(scenedata_t *sceneData) { errorret_t sceneOverworldRender(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null"); assertNotNull(sceneData, "Scene data cannot be null");
errorChain(displaySetState((displaystate_t){ sceneoverworld_t *overworld = &sceneData->overworld;
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL sceneOverworldCullUpdate(overworld);
}));
mat4 model, eye;
// Overworld camera
errorChain(shaderBind(&SHADER_UNLIT)); errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
));
// Model
glm_mat4_identity(model);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model));
// Camera projection
rpgCameraUpdateProjection(); rpgCameraUpdateProjection();
errorChain(shaderSetMatrix( errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, RPG_CAMERA.projection &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, RPG_CAMERA.projection
)); ));
// Camera Eye rpgCameraUpdateEye();
float_t fov = glm_rad(RPG_CAMERA_FOV); errorChain(shaderSetMatrix(
float_t pixelsPerUnit = TILE_SIZE_PIXELS; &SHADER_UNLIT, SHADER_UNLIT_VIEW, RPG_CAMERA.eye
float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit; ));
float_t z = (worldH * 0.5f) / tanf(fov * 0.5f);
vec3 worldPosVec;
rpgCameraGetPosition(worldPosVec);
float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS);
glm_vec3_add(worldPosVec, (vec3){ 0.5f, 0.5f, 0.5f }, worldPosVec); // Chunk tiles
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
}));
errorChain(sceneOverworldDrawChunksBase(overworld));
glm_lookat( // Entities
(vec3){
worldPosVec[0],
worldPosVec[1] + offset,
worldPosVec[2] + z
},
worldPosVec,
(vec3){ 0, 1, 0 }, // up
eye
);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, eye));
// Base terrain meshes, drawn with normal depth testing.
errorChain(sceneOverworldDrawChunksBase());
// Entities are drawn with depth testing fully disabled so sloped tiles
// (ramps) never clip them; entity-vs-entity overlap falls back to
// array draw order instead of true depth.
errorChain(displaySetState((displaystate_t){ errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_CULL .flags = DISPLAY_STATE_FLAG_CULL
})); }));
// Entities
{
for(uint8_t i = 0; i < ENTITY_COUNT; i++) { for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
entity_t *ent = &ENTITIES[i]; entity_t *ent = &ENTITIES[i];
if(ent->type == ENTITY_TYPE_NULL) continue; if(ent->type == ENTITY_TYPE_NULL) continue;
errorChain(sceneOverworldDrawEntity(overworld, ent));
}
// Chunk props
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
}));
errorChain(sceneOverworldDrawChunksProps(overworld));
// Weather effects
// errorChain(shaderSetMatrix(
// &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj
// ));
// errorChain(shaderSetMatrix(
// &SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView
// ));
// errorChain(displaySetState((displaystate_t){
// .flags = DISPLAY_STATE_FLAG_BLEND
// }));
// spritebatchsprite_t skyboxSprite;
// memoryZero(&skyboxSprite, sizeof(spritebatchsprite_t));
// skyboxSprite.max[0] = (float_t)(SCREEN.width / SCREEN.scaleUi);
// skyboxSprite.max[1] = (float_t)(SCREEN.height / SCREEN.scaleUi);
// shadermaterial_t skyboxMaterial = {
// .unlit = { .color = color(0xFF, 0x00, 0x00, 0xFF/2), .texture = NULL }
// };
// errorChain(spriteBatchBuffer(&skyboxSprite, 1, &SHADER_UNLIT, skyboxMaterial));
// errorChain(spriteBatchFlush());
errorOk();
}
void sceneOverworldCullUpdate(sceneoverworld_t *overworld) {
rpgCameraGetPosition(overworld->cullTarget);
const float_t worldH =
(float_t)(SCREEN.height / SCREEN.scale3d) / TILE_SIZE_PIXELS;
const float_t worldW = worldH * SCREEN.aspect;
overworld->cullHalfRangeX =
(worldW * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_X;
overworld->cullHalfRangeY =
(worldH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y;
}
bool_t sceneOverworldChunkShouldRender(
const sceneoverworld_t *overworld,
const chunk_t *chunk
) {
worldpos_t worldPos;
chunkPosToWorldPos(&chunk->position, &worldPos);
const float_t chunkCenterX = (float_t)worldPos.x + (CHUNK_WIDTH * 0.5f);
const float_t chunkCenterY = (float_t)worldPos.y + (CHUNK_HEIGHT * 0.5f);
if(fabsf(overworld->cullTarget[0] - chunkCenterX) >
overworld->cullHalfRangeX) return false;
if(fabsf(overworld->cullTarget[1] - chunkCenterY) >
overworld->cullHalfRangeY) return false;
return true;
}
bool_t sceneOverworldEntityShouldRender(
const sceneoverworld_t *overworld,
const entity_t *ent
) {
if(fabsf(overworld->cullTarget[0] - ent->renderPosition[0]) >
overworld->cullHalfRangeX) return false;
if(fabsf(overworld->cullTarget[1] - ent->renderPosition[1]) >
overworld->cullHalfRangeY) return false;
return true;
}
errorret_t sceneOverworldDrawEntity(
const sceneoverworld_t *overworld,
entity_t *ent
) {
if(!sceneOverworldEntityShouldRender(overworld, ent)) errorOk();
spritebatchsprite_t sprite; spritebatchsprite_t sprite;
glm_vec3_copy(ent->renderPosition, sprite.min); glm_vec3_copy(ent->renderPosition, sprite.min);
@@ -117,26 +175,17 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
shadermaterial_t material = { shadermaterial_t material = {
.unlit = { .color = color, .texture = NULL } .unlit = { .color = color, .texture = NULL }
}; };
spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material); errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
spriteBatchFlush(); errorChain(spriteBatchFlush());
}
}
// Other chunk meshes (trees, buildings, etc), drawn last with normal
// depth testing so they correctly occlude entities standing beneath
// them.
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
}));
errorChain(sceneOverworldDrawChunksProps());
errorOk(); errorOk();
} }
errorret_t sceneOverworldDrawChunksBase() { errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = MAP.chunkOrder[i]; chunk_t *chunk = MAP.chunkOrder[i];
if(chunk == NULL) continue; if(chunk == NULL) continue;
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
if(chunk->meshCount == 0) continue; if(chunk->meshCount == 0) continue;
if(chunk->modelEntries[0] == NULL) continue; if(chunk->modelEntries[0] == NULL) continue;
if(chunk->modelEntries[0]->state != ASSET_ENTRY_STATE_LOADED) continue; if(chunk->modelEntries[0]->state != ASSET_ENTRY_STATE_LOADED) continue;
@@ -169,10 +218,11 @@ errorret_t sceneOverworldDrawChunksBase() {
errorOk(); errorOk();
} }
errorret_t sceneOverworldDrawChunksProps() { errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = MAP.chunkOrder[i]; chunk_t *chunk = MAP.chunkOrder[i];
if(chunk == NULL) continue; if(chunk == NULL) continue;
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
for(uint8_t m = 1; m < chunk->meshCount; m++) { for(uint8_t m = 1; m < chunk->meshCount; m++) {
if(chunk->modelEntries[m] == NULL) continue; if(chunk->modelEntries[m] == NULL) continue;
+90 -3
View File
@@ -7,11 +7,35 @@
#pragma once #pragma once
#include "scene/scenebase.h" #include "scene/scenebase.h"
#include "rpg/overworld/chunk.h"
#include "rpg/entity/entity.h"
typedef struct { typedef struct {
// Cached per-frame chunk culling bounds, refreshed by
// sceneOverworldCullUpdate() so sceneOverworldChunkShouldRender()
// does not need to recompute them for every chunk.
vec3 cullTarget;
float_t cullHalfRangeX;
float_t cullHalfRangeY;
} sceneoverworld_t; } sceneoverworld_t;
// Extra world-space margin, in tiles, added on top of the rough
// screen-size skin below - cheap insurance against the approximation
// culling a chunk that is actually still just in view. Y gets its own
// value since the camera's tilt makes the vertical check less exact.
#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_X 8.0f
#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y 4.0f
// The camera is tilted between the ground-depth (Y) and height (Z)
// axes, so a chunk's on-screen vertical extent also depends on its Z
// layers. Rather than projecting every corner, fold that into a
// generous skin margin on the depth check.
#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_X \
((CHUNK_WIDTH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_PADDING_X)
#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y \
((CHUNK_HEIGHT * 0.5f) + (CHUNK_DEPTH * WORLD_LAYER_HEIGHT) + \
SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y)
/** /**
* Initialises the overworld scene. * Initialises the overworld scene.
* *
@@ -28,22 +52,85 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData);
*/ */
errorret_t sceneOverworldUpdate(scenedata_t *sceneData); errorret_t sceneOverworldUpdate(scenedata_t *sceneData);
/**
* Refreshes the chunk culling bounds cached on the overworld scene data,
* from the current camera position and screen dimensions. Must be
* called once per frame before sceneOverworldChunkShouldRender().
*
* @param overworld The overworld scene data to update.
*/
void sceneOverworldCullUpdate(sceneoverworld_t *overworld);
/**
* Roughly checks whether a chunk falls within the visible screen area,
* based on chunk size and screen dimensions. Cheap approximation, not
* an exact frustum check - includes a generous skin margin to account
* for the camera's angle tilting height/depth onto the screen's
* vertical axis, so it may pass some chunks that are actually just out
* of view but will never wrongly cull one that is visible.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @param chunk The chunk to check.
* @return true if the chunk should be rendered, false otherwise.
*/
bool_t sceneOverworldChunkShouldRender(
const sceneoverworld_t *overworld,
const chunk_t *chunk
);
/** /**
* Draws the base (tile) mesh of every loaded chunk, with normal depth * Draws the base (tile) mesh of every loaded chunk, with normal depth
* testing. Must be called before entities are rendered. * testing. Must be called before entities are rendered.
* *
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @return An error if drawing failed, or errorOk() on success. * @return An error if drawing failed, or errorOk() on success.
*/ */
errorret_t sceneOverworldDrawChunksBase(); errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld);
/** /**
* Draws every loaded chunk's additional meshes (trees, buildings, etc), * Draws every loaded chunk's additional meshes (trees, buildings, etc),
* with normal depth testing so they correctly occlude entities standing * with normal depth testing so they correctly occlude entities standing
* beneath them. Must be called after entities are rendered. * beneath them. Must be called after entities are rendered.
* *
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @return An error if drawing failed, or errorOk() on success. * @return An error if drawing failed, or errorOk() on success.
*/ */
errorret_t sceneOverworldDrawChunksProps(); errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld);
/**
* Roughly checks whether an entity falls within the visible screen
* area. Reuses the same cached chunk culling bounds, since an entity
* is just a point within that same space - deliberately cheap, a
* couple of subtractions and comparisons, since it runs per entity
* every frame and it is not worth spending more math than that to
* avoid drawing one that is slightly off-screen.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @param ent The entity to check.
* @return true if the entity should be rendered, false otherwise.
*/
bool_t sceneOverworldEntityShouldRender(
const sceneoverworld_t *overworld,
const entity_t *ent
);
/**
* Draws a single entity as a sprite, colored by its facing direction.
* Skips drawing (without error) if the entity should not render.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @param ent The entity to draw.
* @return An error if drawing failed, or errorOk() on success.
*/
errorret_t sceneOverworldDrawEntity(
const sceneoverworld_t *overworld,
entity_t *ent
);
/** /**
* Renders the overworld scene. * Renders the overworld scene.
+26 -19
View File
@@ -58,6 +58,23 @@ errorret_t sceneUpdate(void) {
} }
errorret_t sceneRender(void) { errorret_t sceneRender(void) {
// Setup screen matrices for 3D rendering.
glm_mat4_identity(SCENE.screenIdentity);
glm_ortho(
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi),
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f,
0.1f, 100.0f,
SCENE.screenProj
);
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
SCENE.screenView
);
// Scene rendering // Scene rendering
if( if(
SCENE.current != SCENE_TYPE_NULL && SCENE.current != SCENE_TYPE_NULL &&
@@ -67,26 +84,16 @@ errorret_t sceneRender(void) {
} }
// UI Rendering // UI Rendering
mat4 proj, view, ident;
glm_mat4_identity(ident);
errorChain(shaderBind(&SHADER_UNLIT)); errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, ident)); errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
glm_ortho( ));
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi), errorChain(shaderSetMatrix(
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f, &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj
0.1f, 100.0f, ));
proj errorChain(shaderSetMatrix(
); &SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj)); ));
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
view
);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view));
errorChain(displaySetState((displaystate_t){ errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND .flags = DISPLAY_STATE_FLAG_BLEND
+4
View File
@@ -6,12 +6,16 @@
*/ */
#pragma once #pragma once
#include "dusk.h"
#include "scenetype.h" #include "scenetype.h"
typedef struct { typedef struct {
scenetype_t current; scenetype_t current;
scenetype_t next; scenetype_t next;
scenedata_t data; scenedata_t data;
mat4 screenProj;
mat4 screenView;
mat4 screenIdentity;
} scene_t; } scene_t;
extern scene_t SCENE; extern scene_t SCENE;
+7
View File
@@ -16,5 +16,12 @@ scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
.render = sceneOverworldRender, .render = sceneOverworldRender,
.dispose = sceneOverworldDispose .dispose = sceneOverworldDispose
}, },
[SCENE_TYPE_BATTLE] = {
.init = sceneBattleInit,
.update = sceneBattleUpdate,
.render = sceneBattleRender,
.dispose = sceneBattleDispose
},
}; };
+3
View File
@@ -8,9 +8,11 @@
#pragma once #pragma once
#include "scene/scenebase.h" #include "scene/scenebase.h"
#include "scene/overworld/sceneoverworld.h" #include "scene/overworld/sceneoverworld.h"
#include "scene/battle/scenebattle.h"
typedef union scenedata_u { typedef union scenedata_u {
sceneoverworld_t overworld; sceneoverworld_t overworld;
scenebattle_t battle;
} scenedata_t; } scenedata_t;
typedef errorret_t (*scenecallback_t)(scenedata_t *); typedef errorret_t (*scenecallback_t)(scenedata_t *);
@@ -26,6 +28,7 @@ typedef enum {
SCENE_TYPE_NULL, SCENE_TYPE_NULL,
SCENE_TYPE_OVERWORLD, SCENE_TYPE_OVERWORLD,
SCENE_TYPE_BATTLE,
SCENE_TYPE_COUNT SCENE_TYPE_COUNT
} scenetype_t; } scenetype_t;
+2
View File
@@ -11,3 +11,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(game) add_subdirectory(game)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(battle)
add_subdirectory(backpack)
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uibackpack.c
)
+116
View File
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uibackpack.h"
#include "ui/frame/uiframe.h"
#include "rpg/item/backpack.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
#include "assert/assert.h"
uibackpack_t UI_BACKPACK;
void uiBackpackTabChanged(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
const itemtype_t type = (itemtype_t)(index + 1);
const inventory_t *inventory = backpackGetInventory(type);
errorCatch(uiItemListSetItemStacks(
&UI_BACKPACK.itemList, inventory->storage, inventory->storageSize
));
}
void uiBackpackTabSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
uiItemListOpen(&UI_BACKPACK.itemList);
}
errorret_t uiBackpackInit(void) {
memoryZero(&UI_BACKPACK, sizeof(uibackpack_t));
MENU_BEGIN(
&UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs,
uiBackpackTabSelected, NULL, uiBackpackTabChanged
);
for(uint8_t i = 0; i < UI_BACKPACK_TAB_COUNT; i++) {
stringFormat(
UI_BACKPACK.tabLabels[i], UI_BACKPACK_TAB_LABEL_MAX - 1,
"Category %u", i + 1
);
MENU_TAB(UI_BACKPACK.tabLabels[i]);
}
MENU_END(UI_BACKPACK.tabs, menuIndex);
uiItemListInit(
&UI_BACKPACK.itemList,
UI_BACKPACK_ITEM_LIST_COLUMNS, UI_BACKPACK_ITEM_LIST_ROWS, 1,
NULL, NULL, NULL, NULL
);
errorOk();
}
errorret_t uiBackpackDraw(void) {
if(!uiMenuIsActive(&UI_BACKPACK.tabsMenu)) errorOk();
const float_t width = SCREEN.width;
const float_t height = SCREEN.height;
const float_t x = (float_t)SCREEN.scanX +
((float_t)SCREEN.scanWidth - width) * 0.5f;
const float_t y = (float_t)SCREEN.scanY +
((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
const float_t contentX = x + UI_FRAME_START_X;
const float_t contentY = y + UI_FRAME_START_Y;
const float_t contentWidth = width - (UI_FRAME_START_X * 2);
const float_t contentHeight = height - (UI_FRAME_START_Y * 2);
errorChain(uiMenuDraw(
&UI_BACKPACK.tabsMenu,
contentX,
contentY,
contentWidth,
contentHeight
));
const float_t tabsRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
const float_t listY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y;
errorChain(
uiItemListDraw(&UI_BACKPACK.itemList, contentX, listY, contentWidth)
);
errorChain(spriteBatchFlush());
errorOk();
}
bool_t uiBackpackIsOpen(void) {
return uiMenuIsActive(&UI_BACKPACK.tabsMenu);
}
void uiBackpackOpen(void) {
uiMenuOpen(&UI_BACKPACK.tabsMenu);
}
void uiBackpackClose(void) {
uiMenuClose(&UI_BACKPACK.tabsMenu);
}
errorret_t uiBackpackDispose(void) {
errorOk();
}
+65
View File
@@ -0,0 +1,65 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#include "ui/widget/uiitemlist.h"
#include "rpg/item/item.h"
#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_COUNT - 1)
#define UI_BACKPACK_TAB_LABEL_MAX 32
#define UI_BACKPACK_ITEM_LIST_COLUMNS 4
#define UI_BACKPACK_ITEM_LIST_ROWS 5
typedef struct {
uimenu_t tabsMenu;
uimenuitem_t tabs[UI_BACKPACK_TAB_COUNT];
char_t tabLabels[UI_BACKPACK_TAB_COUNT][UI_BACKPACK_TAB_LABEL_MAX];
uiitemlist_t itemList;
} uibackpack_t;
extern uibackpack_t UI_BACKPACK;
/**
* Initializes the backpack panel and its item type tabs.
*
* @return Any error that occurs.
*/
errorret_t uiBackpackInit(void);
/**
* Draws the backpack panel. No-op when not visible.
*
* @return Any error that occurs.
*/
errorret_t uiBackpackDraw(void);
/**
* Returns true when the backpack panel is currently open.
*
* @returns True if open.
*/
bool_t uiBackpackIsOpen(void);
/**
* Opens the backpack panel. No-op when already open.
*/
void uiBackpackOpen(void);
/**
* Closes the backpack panel. No-op when already closed.
*/
void uiBackpackClose(void);
/**
* Disposes of the backpack panel.
*
* @return Any error that occurs.
*/
errorret_t uiBackpackDispose(void);
+9
View File
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uibattlemenu.c
)
+137
View File
@@ -0,0 +1,137 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uibattlemenu.h"
#include "ui/frame/uiframe.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
uibattlemenu_t UI_BATTLE_MENU;
void uiBattleMenuTargetSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
battlePlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]);
uiMenuClose(&UI_BATTLE_MENU.targetMenu);
uiMenuClose(&UI_BATTLE_MENU.actionMenu);
}
void uiBattleMenuOpenTargets(void) {
battlefighter_t *current = battleGetCurrentFighter();
if(current == NULL) return;
const battlefighterteam_t enemyTeam =
current->team == BATTLE_FIGHTER_TEAM_ALLY ?
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
MENU_BEGIN(
&UI_BATTLE_MENU.targetMenu, UI_BATTLE_MENU.targetItems,
uiBattleMenuTargetSelected, NULL, NULL
);
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
battlefighter_t *candidate = &BATTLE.fighters[i];
if(candidate->team != enemyTeam) continue;
if(!battleFighterIsAlive(candidate)) continue;
stringFormat(
UI_BATTLE_MENU.targetLabels[menuIndex],
UI_BATTLE_MENU_TARGET_LABEL_MAX - 1,
"Enemy %u (%u/%u HP)",
candidate->id, candidate->health, candidate->healthMax
);
UI_BATTLE_MENU.targetFighterIndex[menuIndex] = i;
MENU_BUTTON(UI_BATTLE_MENU.targetLabels[menuIndex]);
}
MENU_END(UI_BATTLE_MENU.targetItems, 1);
uiMenuOpen(&UI_BATTLE_MENU.targetMenu);
}
void uiBattleMenuActionSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
switch(index) {
case UI_BATTLE_MENU_ACTION_INDEX_ATTACK:
uiBattleMenuOpenTargets();
break;
case UI_BATTLE_MENU_ACTION_INDEX_FLEE:
battlePlayerFlee();
uiMenuClose(&UI_BATTLE_MENU.actionMenu);
break;
default:
break;
}
}
errorret_t uiBattleMenuInit(void) {
memoryZero(&UI_BATTLE_MENU, sizeof(uibattlemenu_t));
MENU_BEGIN(
&UI_BATTLE_MENU.actionMenu, UI_BATTLE_MENU.actionItems,
uiBattleMenuActionSelected, NULL, NULL
);
MENU_BUTTON("Attack");
MENU_BUTTON("Flee");
MENU_END(UI_BATTLE_MENU.actionItems, 1);
errorOk();
}
errorret_t uiBattleMenuUpdate(void) {
if(uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
battlefighter_t *current = battleGetCurrentFighter();
if(current == NULL) errorOk();
if(current->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) errorOk();
uiMenuOpen(&UI_BATTLE_MENU.actionMenu);
errorOk();
}
errorret_t uiBattleMenuDraw(void) {
if(!uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
const float_t width = UI_BATTLE_MENU_WIDTH;
const float_t height = UI_BATTLE_MENU_HEIGHT;
const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width;
const float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - height;
errorChain(uiFrameDraw(x, y, width, height));
errorChain(uiMenuDraw(
&UI_BATTLE_MENU.actionMenu,
x + UI_FRAME_START_X,
y + UI_FRAME_START_Y,
width - (UI_FRAME_START_X * 2),
height - (UI_FRAME_START_Y * 2)
));
if(uiMenuIsActive(&UI_BATTLE_MENU.targetMenu)) {
const float_t targetY = y - height;
errorChain(uiFrameDraw(x, targetY, width, height));
errorChain(uiMenuDraw(
&UI_BATTLE_MENU.targetMenu,
x + UI_FRAME_START_X,
targetY + UI_FRAME_START_Y,
width - (UI_FRAME_START_X * 2),
height - (UI_FRAME_START_Y * 2)
));
}
errorChain(spriteBatchFlush());
errorOk();
}
+57
View File
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#include "rpg/battle/battle.h"
#define UI_BATTLE_MENU_ACTION_ITEM_COUNT 2
#define UI_BATTLE_MENU_ACTION_INDEX_ATTACK 0
#define UI_BATTLE_MENU_ACTION_INDEX_FLEE 1
#define UI_BATTLE_MENU_TARGET_ITEM_COUNT BATTLE_FIGHTER_COUNT_MAX
#define UI_BATTLE_MENU_TARGET_LABEL_MAX 32
#define UI_BATTLE_MENU_WIDTH 160.0f
#define UI_BATTLE_MENU_HEIGHT 96.0f
typedef struct {
uimenu_t actionMenu;
uimenuitem_t actionItems[UI_BATTLE_MENU_ACTION_ITEM_COUNT];
uimenu_t targetMenu;
uimenuitem_t targetItems[UI_BATTLE_MENU_TARGET_ITEM_COUNT];
uint8_t targetFighterIndex[UI_BATTLE_MENU_TARGET_ITEM_COUNT];
char_t targetLabels[UI_BATTLE_MENU_TARGET_ITEM_COUNT]
[UI_BATTLE_MENU_TARGET_LABEL_MAX];
} uibattlemenu_t;
extern uibattlemenu_t UI_BATTLE_MENU;
/**
* Initializes the battle action/target menus.
*
* @return Any error that occurs.
*/
errorret_t uiBattleMenuInit(void);
/**
* Updates the battle menu: opens the action menu whenever it becomes a
* player-controlled fighter's turn.
*
* @return Any error that occurs.
*/
errorret_t uiBattleMenuUpdate(void);
/**
* Draws the battle action menu, and the target menu above it when
* open. No-op when the action menu isn't active.
*
* @return Any error that occurs.
*/
errorret_t uiBattleMenuDraw(void);
+31 -3
View File
@@ -8,13 +8,17 @@
#include "uigamemenu.h" #include "uigamemenu.h"
#include "ui/frame/uiframe.h" #include "ui/frame/uiframe.h"
#include "ui/frame/settings/uisettings.h" #include "ui/frame/settings/uisettings.h"
#include "ui/frame/backpack/uibackpack.h"
#include "util/memory.h" #include "util/memory.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h" #include "display/screen/screen.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
#define UI_GAME_MENU_INDEX_CHARACTERS 0 #define UI_GAME_MENU_INDEX_CHARACTERS 0
#define UI_GAME_MENU_INDEX_SETTINGS 1 #define UI_GAME_MENU_INDEX_ITEMS 1
#define UI_GAME_MENU_INDEX_SETTINGS 2
uigamemenu_t UI_GAME_MENU; uigamemenu_t UI_GAME_MENU;
@@ -23,17 +27,41 @@ void uiGameMenuSelected(
const uint8_t index, const uint8_t index,
const uimenuitem_t *item const uimenuitem_t *item
) { ) {
if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen();
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen(); if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
} }
errorret_t uiGameMenuInit(void) { errorret_t uiGameMenuInit(void) {
memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t)); memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.game_menu.characters",
0,
UI_GAME_MENU.charactersLabel,
UI_GAME_MENU_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.game_menu.items",
0,
UI_GAME_MENU.itemsLabel,
UI_GAME_MENU_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.game_menu.settings",
0,
UI_GAME_MENU.settingsLabel,
UI_GAME_MENU_LABEL_MAX
));
MENU_BEGIN( MENU_BEGIN(
&UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL &UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL
); );
MENU_BUTTON("Characters"); MENU_BUTTON(UI_GAME_MENU.charactersLabel);
MENU_BUTTON("Settings"); MENU_BUTTON(UI_GAME_MENU.itemsLabel);
MENU_BUTTON(UI_GAME_MENU.settingsLabel);
MENU_END(UI_GAME_MENU.items, 1); MENU_END(UI_GAME_MENU.items, 1);
+5 -1
View File
@@ -9,12 +9,16 @@
#include "error/error.h" #include "error/error.h"
#include "ui/widget/uimenu.h" #include "ui/widget/uimenu.h"
#define UI_GAME_MENU_ITEM_COUNT 2 #define UI_GAME_MENU_ITEM_COUNT 3
#define UI_GAME_MENU_WIDTH 150.0f #define UI_GAME_MENU_WIDTH 150.0f
#define UI_GAME_MENU_LABEL_MAX 32
typedef struct { typedef struct {
uimenu_t menu; uimenu_t menu;
uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT]; uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT];
char_t charactersLabel[UI_GAME_MENU_LABEL_MAX];
char_t itemsLabel[UI_GAME_MENU_LABEL_MAX];
char_t settingsLabel[UI_GAME_MENU_LABEL_MAX];
} uigamemenu_t; } uigamemenu_t;
extern uigamemenu_t UI_GAME_MENU; extern uigamemenu_t UI_GAME_MENU;
+2 -2
View File
@@ -12,8 +12,8 @@
#define UI_FRAME_BORDER_WIDTH 6 #define UI_FRAME_BORDER_WIDTH 6
#define UI_FRAME_BORDER_HEIGHT 6 #define UI_FRAME_BORDER_HEIGHT 6
#define UI_FRAME_PADDING_X 4 #define UI_FRAME_PADDING_X 2
#define UI_FRAME_PADDING_Y 4 #define UI_FRAME_PADDING_Y 2
#define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X) #define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X)
#define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y) #define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y)
#define UI_FRAME_TILE_WIDTH 1 #define UI_FRAME_TILE_WIDTH 1
+4 -2
View File
@@ -5,6 +5,8 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
uitextbox.c uiemoji.c
uitextboxmain.c
) )
add_subdirectory(textbox)
+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
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uitextbox.c
uitextboxmain.c
uitextboxmini.c
uitextboxminilist.c
)
@@ -16,15 +16,29 @@
#include "display/shader/shaderunlit.h" #include "display/shader/shaderunlit.h"
#include "ui/frame/uiframe.h" #include "ui/frame/uiframe.h"
void uiTextboxInit(uitextbox_t *box) { void uiTextboxInit(
uitextbox_t *box,
char_t *text,
const uint32_t maxLength,
uitextboxline_t *lines,
const uint32_t linesMax
) {
assertNotNull(box, "Textbox cannot be NULL"); assertNotNull(box, "Textbox cannot be NULL");
assertNotNull(text, "Text buffer cannot be NULL");
assertTrue(maxLength >= 1, "maxLength must be at least 1");
assertNotNull(lines, "Lines buffer cannot be NULL");
assertTrue(linesMax >= 1, "linesMax must be at least 1");
memoryZero(box, sizeof(uitextbox_t)); memoryZero(box, sizeof(uitextbox_t));
box->text = text;
box->maxLength = maxLength;
box->lines = lines;
box->linesMax = linesMax;
} }
void uiTextboxSetText(uitextbox_t *box, const char_t *text) { void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
assertNotNull(box, "Textbox cannot be NULL"); assertNotNull(box, "Textbox cannot be NULL");
assertNotNull(text, "Text cannot be NULL"); assertNotNull(text, "Text cannot be NULL");
stringCopy(box->text, text, UI_TEXTBOX_TEXT_MAX); stringCopy(box->text, text, box->maxLength);
box->currentPage = 0; box->currentPage = 0;
box->scroll = 0; box->scroll = 0;
box->layoutWidth = 0.0f; box->layoutWidth = 0.0f;
@@ -60,12 +74,12 @@ void uiTextboxBuildLayout(
char_t *src = box->text; char_t *src = box->text;
int32_t i = 0; int32_t i = 0;
while(src[i] != '\0' && box->lineCount < UI_TEXTBOX_LINES_MAX) { while(src[i] != '\0' && box->lineCount < (int32_t)box->linesMax) {
if(src[i] == '\t') { if(src[i] == '\t') {
i++; i++;
int32_t rem = box->lineCount % box->linesPerPage; int32_t rem = box->lineCount % box->linesPerPage;
int32_t pad = rem > 0 ? box->linesPerPage - rem : 0; int32_t pad = rem > 0 ? box->linesPerPage - rem : 0;
while(pad > 0 && box->lineCount < UI_TEXTBOX_LINES_MAX) { while(pad > 0 && box->lineCount < (int32_t)box->linesMax) {
box->lines[box->lineCount].start = i; box->lines[box->lineCount].start = i;
box->lines[box->lineCount].count = 0; box->lines[box->lineCount].count = 0;
box->lineCount++; box->lineCount++;
@@ -196,18 +210,6 @@ errorret_t uiTextboxDraw(
charsLeft -= visible; charsLeft -= visible;
} }
if(uiTextboxPageIsComplete(box)) {
spritebatchsprite_t caret = textGetSprite(
(vec2){
contentX + contentW - fontW,
contentY + contentH - fontH
},
'v',
&FONT_DEFAULT
);
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
}
errorChain(spriteBatchFlush()); errorChain(spriteBatchFlush());
errorOk(); errorOk();
} }
@@ -8,8 +8,6 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#define UI_TEXTBOX_TEXT_MAX 1024
#define UI_TEXTBOX_LINES_MAX 64
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4 #define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1 #define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
#define UI_TEXTBOX_LINE_SPACING 0.0f #define UI_TEXTBOX_LINE_SPACING 0.0f
@@ -20,9 +18,11 @@ typedef struct {
} uitextboxline_t; } uitextboxline_t;
typedef struct { typedef struct {
char_t text[UI_TEXTBOX_TEXT_MAX]; char_t *text;
uint32_t maxLength;
uitextboxline_t lines[UI_TEXTBOX_LINES_MAX]; uitextboxline_t *lines;
uint32_t linesMax;
int32_t lineCount; int32_t lineCount;
int32_t charsPerLine; int32_t charsPerLine;
int32_t linesPerPage; int32_t linesPerPage;
@@ -37,11 +37,22 @@ typedef struct {
} uitextbox_t; } uitextbox_t;
/** /**
* Initializes a textbox, zeroing all state. * Initializes a textbox, zeroing all state and binding it to caller-owned
* text and line storage.
* *
* @param box The textbox to initialize. * @param box The textbox to initialize.
* @param text Caller-owned buffer the textbox copies its text into.
* @param maxLength Capacity of text, in characters.
* @param lines Caller-owned buffer the textbox lays lines out into.
* @param linesMax Capacity of lines, in entries.
*/ */
void uiTextboxInit(uitextbox_t *box); void uiTextboxInit(
uitextbox_t *box,
char_t *text,
const uint32_t maxLength,
uitextboxline_t *lines,
const uint32_t linesMax
);
/** /**
* Copies text into the textbox and marks layout as dirty. * Copies text into the textbox and marks layout as dirty.
+122
View File
@@ -0,0 +1,122 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uitextboxmain.h"
#include "ui/focus/uifocus.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
#include "display/color.h"
#include "display/spritebatch/spritebatch.h"
#include "display/shader/shaderunlit.h"
#include "ui/frame/uiframe.h"
uitextboxmain_t UI_TEXTBOX_MAIN;
static uifocusitem_t *focusItem = NULL;
errorret_t uiTextboxMainInit(void) {
uiTextboxInit(
&UI_TEXTBOX_MAIN.box,
UI_TEXTBOX_MAIN.text, UI_TEXTBOX_MAIN_TEXT_MAX,
UI_TEXTBOX_MAIN.lines, UI_TEXTBOX_MAIN_LINES_MAX
);
errorOk();
}
void uiTextboxMainSetText(const char_t *text) {
uiTextboxSetText(&UI_TEXTBOX_MAIN.box, text);
if(focusItem != NULL) return;
focusItem = uiFocusPush(
1, 1,
uiTextboxMainFocusSelected,
NULL,
uiTextboxMainFocusClosed,
NULL,
NULL
);
}
errorret_t uiTextboxMainUpdate(void) {
if(focusItem == NULL) errorOk();
return uiTextboxUpdate(&UI_TEXTBOX_MAIN.box);
}
errorret_t uiTextboxMainDraw(void) {
if(focusItem == NULL) errorOk();
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH +
(float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING +
2.0f * (float_t)UI_FRAME_START_Y;
float_t w = (float_t)SCREEN.scanWidth;
float_t x = (float_t)SCREEN.scanX;
float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h;
errorChain(uiTextboxDraw(&UI_TEXTBOX_MAIN.box, x, y, w, h));
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) errorOk();
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
float_t contentX = x + (float_t)UI_FRAME_START_X;
float_t contentY = y + (float_t)UI_FRAME_START_Y;
float_t contentW = w - 2.0f * (float_t)UI_FRAME_START_X;
float_t contentH = h - 2.0f * (float_t)UI_FRAME_START_Y;
shadermaterial_t material = {
.unlit = {
.color = COLOR_WHITE,
.texture = FONT_DEFAULT.texture
}
};
spritebatchsprite_t caret = textGetSprite(
(vec2){
contentX + contentW - fontW,
contentY + contentH - fontH
},
'v',
&FONT_DEFAULT
);
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
errorChain(spriteBatchFlush());
errorOk();
}
bool_t uiTextboxMainPageIsComplete(void) {
return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box);
}
bool_t uiTextboxMainHasNextPage(void) {
return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box);
}
void uiTextboxMainNextPage(void) {
uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
}
bool_t uiTextboxMainIsActive(void) {
return focusItem != NULL;
}
bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) {
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) {
UI_TEXTBOX_MAIN.box.scroll =
uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN.box);
return true;
}
if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box)) {
uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
return true;
}
uiFocusPopItem(focusItem);
return true;
}
bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) {
focusItem = NULL;
return true;
}
@@ -6,12 +6,20 @@
*/ */
#pragma once #pragma once
#include "ui/rpg/uitextbox.h" #include "ui/rpg/textbox/uitextbox.h"
#include "ui/focus/uifocusitem.h" #include "ui/focus/uifocusitem.h"
#define UI_TEXTBOX_MAIN_LINES 4 #define UI_TEXTBOX_MAIN_LINES 4
#define UI_TEXTBOX_MAIN_TEXT_MAX 1024
#define UI_TEXTBOX_MAIN_LINES_MAX 64
extern uitextbox_t UI_TEXTBOX_MAIN; typedef struct {
uitextbox_t box;
char_t text[UI_TEXTBOX_MAIN_TEXT_MAX];
uitextboxline_t lines[UI_TEXTBOX_MAIN_LINES_MAX];
} uitextboxmain_t;
extern uitextboxmain_t UI_TEXTBOX_MAIN;
/** /**
* Initializes UI_TEXTBOX_MAIN. * Initializes UI_TEXTBOX_MAIN.
+122
View File
@@ -0,0 +1,122 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uitextboxmini.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "time/time.h"
#include "rpg/rpgcamera.h"
#include "display/text/text.h"
#include "display/screen/screen.h"
#include "ui/frame/uiframe.h"
void uiTextboxMiniInit(uitextboxmini_t *mini) {
assertNotNull(mini, "Mini textbox cannot be NULL");
memoryZero(mini, sizeof(uitextboxmini_t));
uiTextboxInit(
&mini->box,
mini->text, UI_TEXTBOX_MINI_TEXT_MAX,
mini->lines, UI_TEXTBOX_MINI_LINES_MAX
);
}
void uiTextboxMiniShow(
uitextboxmini_t *mini,
const char_t *text,
vec3 position,
const float_t duration,
uitextboxminiclosedcallback_t closed,
void *user
) {
assertNotNull(mini, "Mini textbox cannot be NULL");
assertNotNull(text, "Text cannot be NULL");
uiTextboxSetText(&mini->box, text);
glm_vec3_copy(position, mini->position);
int32_t textWidth, textHeight;
textMeasure(text, &FONT_DEFAULT, &textWidth, &textHeight);
float_t width = (float_t)textWidth + 2.0f * (float_t)UI_FRAME_START_X;
mini->width = width < UI_TEXTBOX_MINI_WIDTH_MAX
? width : UI_TEXTBOX_MINI_WIDTH_MAX;
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
float_t maxHeight = (float_t)UI_TEXTBOX_MINI_LINES_MAX * fontH +
(float_t)(UI_TEXTBOX_MINI_LINES_MAX - 1) * UI_TEXTBOX_LINE_SPACING +
2.0f * (float_t)UI_FRAME_START_Y;
uiTextboxBuildLayout(
&mini->box,
mini->width - 2.0f * (float_t)UI_FRAME_START_X,
maxHeight - 2.0f * (float_t)UI_FRAME_START_Y
);
int32_t lineCount = mini->box.lineCount > 0 ? mini->box.lineCount : 1;
mini->height = (float_t)lineCount * fontH +
(float_t)(lineCount - 1) * UI_TEXTBOX_LINE_SPACING +
2.0f * (float_t)UI_FRAME_START_Y;
mini->active = true;
mini->timer = duration;
mini->closed = closed;
mini->user = user;
}
errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini) {
assertNotNull(mini, "Mini textbox cannot be NULL");
if(!mini->active) errorOk();
errorChain(uiTextboxUpdate(&mini->box));
if(mini->timer != UI_TEXTBOX_MINI_DURATION_INFINITE) {
mini->timer -= TIME.delta;
if(mini->timer <= 0.0f) uiTextboxMiniClose(mini);
}
errorOk();
}
errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini) {
assertNotNull(mini, "Mini textbox cannot be NULL");
if(!mini->active) errorOk();
vec2 screenPos;
rpgCameraToScreen(mini->position, screenPos);
if(
mini->clamp == UI_TEXTBOX_MINI_CLAMP_X ||
mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH
) {
float_t minX = (float_t)SCREEN.scanX;
float_t maxX = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - mini->width;
if(screenPos[0] < minX) screenPos[0] = minX;
if(screenPos[0] > maxX) screenPos[0] = maxX;
}
if(
mini->clamp == UI_TEXTBOX_MINI_CLAMP_Y ||
mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH
) {
float_t minY = (float_t)SCREEN.scanY;
float_t maxY = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - mini->height;
if(screenPos[1] < minY) screenPos[1] = minY;
if(screenPos[1] > maxY) screenPos[1] = maxY;
}
return uiTextboxDraw(
&mini->box, screenPos[0], screenPos[1], mini->width, mini->height
);
}
bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini) {
assertNotNull(mini, "Mini textbox cannot be NULL");
return mini->active;
}
void uiTextboxMiniClose(uitextboxmini_t *mini) {
assertNotNull(mini, "Mini textbox cannot be NULL");
if(!mini->active) return;
mini->active = false;
if(mini->closed != NULL) mini->closed(mini);
}
+116
View File
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "ui/rpg/textbox/uitextbox.h"
#define UI_TEXTBOX_MINI_TEXT_MAX 128
#define UI_TEXTBOX_MINI_LINES_MAX 2
#define UI_TEXTBOX_MINI_WIDTH_MAX 160.0f
// Magic duration value that keeps a mini textbox visible indefinitely
// instead of counting down and auto-closing.
#define UI_TEXTBOX_MINI_DURATION_INFINITE -1.0f
typedef struct uitextboxmini_s uitextboxmini_t;
typedef void (*uitextboxminiclosedcallback_t)(const uitextboxmini_t *mini);
/**
* Determines whether a mini textbox's screen position is clamped to stay
* fully on screen. Defaults to UI_TEXTBOX_MINI_CLAMP_NONE.
*/
typedef enum {
UI_TEXTBOX_MINI_CLAMP_NONE,
UI_TEXTBOX_MINI_CLAMP_X,
UI_TEXTBOX_MINI_CLAMP_Y,
UI_TEXTBOX_MINI_CLAMP_BOTH
} uitextboxminiclamp_t;
typedef struct uitextboxmini_s {
uitextbox_t box;
char_t text[UI_TEXTBOX_MINI_TEXT_MAX];
uitextboxline_t lines[UI_TEXTBOX_MINI_LINES_MAX];
vec3 position;
float_t width;
float_t height;
uitextboxminiclamp_t clamp;
bool_t active;
float_t timer;
uitextboxminiclosedcallback_t closed;
void *user;
} uitextboxmini_t;
/**
* Initializes a mini textbox, zeroing all state and binding its internal
* uitextbox_t to its own fixed-size text and line storage.
*
* @param mini The mini textbox to initialize.
*/
void uiTextboxMiniInit(uitextboxmini_t *mini);
/**
* Shows a mini textbox with the given text at the given world position for
* the given duration. Resets the typewriter scroll, measures the text to
* size the box (width capped at UI_TEXTBOX_MINI_WIDTH_MAX, height derived
* from the resulting wrapped line count), and starts the visibility timer.
*
* @param mini The mini textbox to show.
* @param text Null-terminated source string.
* @param position World-space position the box is anchored to on screen.
* @param duration How long the mini textbox stays visible, in seconds, or
* UI_TEXTBOX_MINI_DURATION_INFINITE to never auto-close.
* @param closed Called once the timer elapses and the box closes. May be
* NULL.
* @param user Opaque pointer passed back through the closed callback.
*/
void uiTextboxMiniShow(
uitextboxmini_t *mini,
const char_t *text,
vec3 position,
const float_t duration,
uitextboxminiclosedcallback_t closed,
void *user
);
/**
* Advances the typewriter scroll and counts down the visibility timer.
* Closes the mini textbox and fires its closed callback once the timer
* elapses. Has no effect if not active or if the duration was set to
* UI_TEXTBOX_MINI_DURATION_INFINITE.
*
* @param mini The mini textbox to update.
* @returns Any error that occurs.
*/
errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini);
/**
* Draws the mini textbox at its text-measured size, anchored on screen to
* its world-space position via the RPG camera. Has no effect if not
* active.
*
* @param mini The mini textbox to draw.
* @returns Any error that occurs.
*/
errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini);
/**
* Returns true when the mini textbox is currently visible.
*
* @param mini The mini textbox to query.
* @returns True if active.
*/
bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini);
/**
* Immediately closes the mini textbox and fires its closed callback.
* Has no effect if not active.
*
* @param mini The mini textbox to close.
*/
void uiTextboxMiniClose(uitextboxmini_t *mini);
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uitextboxminilist.h"
uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT];
uint8_t UI_TEXTBOX_MINI_LIST_NEXT;
errorret_t uiTextboxMiniListInit(void) {
for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
uiTextboxMiniInit(&UI_TEXTBOX_MINI_LIST[i]);
}
UI_TEXTBOX_MINI_LIST_NEXT = 0;
errorOk();
}
uint8_t uiTextboxMiniListGetNext(void) {
uint8_t index = UI_TEXTBOX_MINI_LIST_NEXT;
UI_TEXTBOX_MINI_LIST_NEXT =
(UI_TEXTBOX_MINI_LIST_NEXT + 1) % UI_TEXTBOX_MINI_LIST_COUNT;
return index;
}
errorret_t uiTextboxMiniListUpdate(void) {
for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
errorChain(uiTextboxMiniUpdate(&UI_TEXTBOX_MINI_LIST[i]));
}
errorOk();
}
errorret_t uiTextboxMiniListDraw(void) {
for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
errorChain(uiTextboxMiniDraw(&UI_TEXTBOX_MINI_LIST[i]));
}
errorOk();
}
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "ui/rpg/textbox/uitextboxmini.h"
#define UI_TEXTBOX_MINI_LIST_COUNT 4
extern uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT];
extern uint8_t UI_TEXTBOX_MINI_LIST_NEXT;
/**
* Initializes all UI_TEXTBOX_MINI_LIST slots and resets
* UI_TEXTBOX_MINI_LIST_NEXT.
*
* @returns Any error that occurs.
*/
errorret_t uiTextboxMiniListInit(void);
/**
* Returns the index of the next UI_TEXTBOX_MINI_LIST slot to use, cycling
* through all slots in round-robin order via UI_TEXTBOX_MINI_LIST_NEXT.
*
* @returns The next slot index.
*/
uint8_t uiTextboxMiniListGetNext(void);
/**
* Updates all UI_TEXTBOX_MINI_LIST slots.
*
* @returns Any error that occurs.
*/
errorret_t uiTextboxMiniListUpdate(void);
/**
* Draws all active UI_TEXTBOX_MINI_LIST slots.
*
* @returns Any error that occurs.
*/
errorret_t uiTextboxMiniListDraw(void);
+119
View File
@@ -0,0 +1,119 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiemoji.h"
#include "display/spritebatch/spritebatch.h"
#include "time/time.h"
#include "util/memory.h"
#include "display/shader/shaderunlit.h"
#include "rpg/entity/entity.h"
#include "rpg/rpgcamera.h"
uiemoji_t UI_EMOJI;
errorret_t uiEmojiInit(void) {
memoryZero(&UI_EMOJI, sizeof(UI_EMOJI));
errorOk();
}
void uiEmojiAdd(
const uint8_t entity,
const float_t duration,
const uiemojitype_t type
) {
uint8_t slot = 0xFF;
// Find an available slot if any
for(uint8_t i = 0; i < UI_EMOJI_ACTIVE_COUNT; i++) {
if(UI_EMOJI.items[i].time <= 0.0f) {
slot = i;
break;
}
if(UI_EMOJI.items[i].entity == entity) {
slot = i;
break;
}
if(UI_EMOJI.items[i].type == UI_EMOJI_NULL) {
slot = i;
break;
}
}
if(slot == 0xFF) {
// Whichever ends soonest
float_t minTime = UI_EMOJI.items[0].time;
slot = 0;
for(uint8_t i = 1; i < UI_EMOJI_ACTIVE_COUNT; i++) {
if(UI_EMOJI.items[i].time >= minTime) continue;
minTime = UI_EMOJI.items[i].time;
slot = i;
}
}
uiemojiitem_t *item = &UI_EMOJI.items[slot];
item->entity = entity;
item->time = duration;
item->type = type;
}
errorret_t uiEmojiUpdate(void) {
for(uint8_t i = 0; i < UI_EMOJI_ACTIVE_COUNT; i++) {
uiemojiitem_t *item = &UI_EMOJI.items[i];
if(item->type == UI_EMOJI_NULL) continue;
item->time -= TIME.delta;
if(item->time <= 0.0f) {
item->type = UI_EMOJI_NULL;
item->entity = 0xFF;
}
}
errorOk();
}
errorret_t uiEmojiDraw(void) {
spritebatchsprite_t sprites[UI_EMOJI_ACTIVE_COUNT];
uint8_t count = 0;
for(uint8_t i = 0; i < UI_EMOJI_ACTIVE_COUNT; i++) {
uiemojiitem_t *item = &UI_EMOJI.items[i];
if(item->type == UI_EMOJI_NULL) continue;
vec2 screenPos;
rpgCameraToScreen(ENTITIES[item->entity].renderPosition, screenPos);
spritebatchsprite_t *sprite = &sprites[count++];
sprite->min[0] = screenPos[0];
sprite->min[1] = screenPos[1] - UI_EMOJI_SIZE;
sprite->min[2] = 0.0f;
sprite->max[0] = screenPos[0] + UI_EMOJI_SIZE;
sprite->max[1] = screenPos[1];
sprite->max[2] = 0.0f;
sprite->uvMin[0] = 0.0f;
sprite->uvMin[1] = 0.0f;
sprite->uvMax[0] = 1.0f;
sprite->uvMax[1] = 1.0f;
}
if(count == 0) errorOk();
shadermaterial_t mat = {
.unlit = {
.color = COLOR_WHITE,
.texture = NULL
}
};
errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, mat));
errorChain(spriteBatchFlush());
errorOk();
}
errorret_t uiEmojiDispose(void) {
errorOk();
}
+66
View File
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#define UI_EMOJI_ACTIVE_COUNT 4
#define UI_EMOJI_SIZE 32.0f
typedef enum {
UI_EMOJI_NULL,
UI_EMOJI_QUESTION_MARK,
UI_EMOJI_EXCLAMATION_MARK,
UI_EMOJI_COUNT,
} uiemojitype_t;
typedef struct {
uint8_t entity;
float_t time;
uiemojitype_t type;
} uiemojiitem_t;
typedef struct {
uiemojiitem_t items[UI_EMOJI_ACTIVE_COUNT];
} uiemoji_t;
extern uiemoji_t UI_EMOJI;
/**
* Initialize the UI emoji.
*/
errorret_t uiEmojiInit(void);
/**
* Show a UI emoji for a specific entity.
*
* @param entity The entity to show the emoji for.
* @param duration The duration to display the emoji.
* @param type The type of emoji to display.
*/
void uiEmojiAdd(
const uint8_t entity,
const float_t duration,
const uiemojitype_t type
);
/**
* Update the UI emoji.
*/
errorret_t uiEmojiUpdate(void);
/**
* Draw the UI emoji.
*/
errorret_t uiEmojiDraw(void);
/**
* Dispose of the UI emoji.
*/
errorret_t uiEmojiDispose(void);
-86
View File
@@ -1,86 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uitextboxmain.h"
#include "ui/focus/uifocus.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
#include "ui/frame/uiframe.h"
uitextbox_t UI_TEXTBOX_MAIN;
static uifocusitem_t *focusItem = NULL;
errorret_t uiTextboxMainInit(void) {
uiTextboxInit(&UI_TEXTBOX_MAIN);
errorOk();
}
void uiTextboxMainSetText(const char_t *text) {
uiTextboxSetText(&UI_TEXTBOX_MAIN, text);
if(focusItem != NULL) return;
focusItem = uiFocusPush(
1, 1,
uiTextboxMainFocusSelected,
NULL,
uiTextboxMainFocusClosed,
NULL,
NULL
);
}
errorret_t uiTextboxMainUpdate(void) {
if(focusItem == NULL) errorOk();
return uiTextboxUpdate(&UI_TEXTBOX_MAIN);
}
errorret_t uiTextboxMainDraw(void) {
if(focusItem == NULL) errorOk();
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH +
(float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING +
2.0f * (float_t)UI_FRAME_START_Y;
float_t w = (float_t)SCREEN.scanWidth;
float_t x = (float_t)SCREEN.scanX;
float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h;
return uiTextboxDraw(&UI_TEXTBOX_MAIN, x, y, w, h);
}
bool_t uiTextboxMainPageIsComplete(void) {
return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN);
}
bool_t uiTextboxMainHasNextPage(void) {
return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN);
}
void uiTextboxMainNextPage(void) {
uiTextboxNextPage(&UI_TEXTBOX_MAIN);
}
bool_t uiTextboxMainIsActive(void) {
return focusItem != NULL;
}
bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) {
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN)) {
UI_TEXTBOX_MAIN.scroll = uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN);
return true;
}
if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN)) {
uiTextboxNextPage(&UI_TEXTBOX_MAIN);
return true;
}
uiFocusPopItem(focusItem);
return true;
}
bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) {
focusItem = NULL;
return true;
}
+33 -2
View File
@@ -18,8 +18,12 @@
#include "ui/debug/uiconsole.h" #include "ui/debug/uiconsole.h"
#include "ui/frame/settings/uisettings.h" #include "ui/frame/settings/uisettings.h"
#include "ui/frame/game/uigamemenu.h" #include "ui/frame/game/uigamemenu.h"
#include "ui/frame/battle/uibattlemenu.h"
#include "ui/frame/backpack/uibackpack.h"
#include "ui/frame/uiconfirm.h" #include "ui/frame/uiconfirm.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
#include "ui/rpg/uiemoji.h"
uielement_t UI_ELEMENTS[] = { uielement_t UI_ELEMENTS[] = {
{ {
@@ -27,7 +31,6 @@ uielement_t UI_ELEMENTS[] = {
.dispose = uiFrameDispose .dispose = uiFrameDispose
}, },
// Fullbox under: above scene, below system UI. // Fullbox under: above scene, below system UI.
{ {
.init = uiFullboxUnderInit, .init = uiFullboxUnderInit,
@@ -35,12 +38,33 @@ uielement_t UI_ELEMENTS[] = {
.draw = uiFullboxUnderDraw .draw = uiFullboxUnderDraw
}, },
// in world stuff
{
.init = uiEmojiInit,
.update = uiEmojiUpdate,
.draw = uiEmojiDraw,
.dispose = uiEmojiDispose
},
// Ingame menus
{ {
.init = uiGameMenuInit, .init = uiGameMenuInit,
.draw = uiGameMenuDraw, .draw = uiGameMenuDraw,
.dispose = uiGameMenuDispose .dispose = uiGameMenuDispose
}, },
{
.init = uiBattleMenuInit,
.update = uiBattleMenuUpdate,
.draw = uiBattleMenuDraw
},
{
.init = uiBackpackInit,
.draw = uiBackpackDraw,
.dispose = uiBackpackDispose
},
{ {
.init = uiSettingsInit, .init = uiSettingsInit,
.update = uiSettingsUpdate, .update = uiSettingsUpdate,
@@ -48,6 +72,7 @@ uielement_t UI_ELEMENTS[] = {
.dispose = uiSettingsDispose .dispose = uiSettingsDispose
}, },
// Text stuffs
{ {
.init = uiConfirmInit, .init = uiConfirmInit,
.draw = uiConfirmDraw, .draw = uiConfirmDraw,
@@ -60,6 +85,12 @@ uielement_t UI_ELEMENTS[] = {
.draw = uiTextboxMainDraw .draw = uiTextboxMainDraw
}, },
{
.init = uiTextboxMiniListInit,
.update = uiTextboxMiniListUpdate,
.draw = uiTextboxMiniListDraw
},
{ {
.init = uiTransitionInit, .init = uiTransitionInit,
.update = uiTransitionUpdate, .update = uiTransitionUpdate,
+3
View File
@@ -10,5 +10,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
uitab.c uitab.c
uislider.c uislider.c
uidropdown.c uidropdown.c
uiscrolling.c
uiitem.c
uiitemlist.c
uimenu.c uimenu.c
) )
+77
View File
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiitem.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/text/text.h"
#include "display/color.h"
#define UI_ITEM_LABEL_MAX 48
errorret_t uiItemInit(
uiitem_t *item,
const itemid_t itemId,
const uint8_t quantity
) {
assertNotNull(item, "Item cannot be NULL");
memoryZero(item, sizeof(uiitem_t));
item->item = itemId;
item->quantity = quantity;
if(itemId == ITEM_ID_NULL) errorOk();
errorChain(itemGetName(itemId, item->nameLabel, UI_ITEM_NAME_LABEL_MAX));
errorOk();
}
itemid_t uiItemGetItem(const uiitem_t *item) {
assertNotNull(item, "Item cannot be NULL");
return item->item;
}
uint8_t uiItemGetQuantity(const uiitem_t *item) {
assertNotNull(item, "Item cannot be NULL");
return item->quantity;
}
void uiItemSetQuantity(uiitem_t *item, const uint8_t quantity) {
assertNotNull(item, "Item cannot be NULL");
item->quantity = quantity;
}
bool_t uiItemIsHighlighted(const uiitem_t *item) {
assertNotNull(item, "Item cannot be NULL");
return item->highlighted;
}
void uiItemSetHighlighted(uiitem_t *item, const bool_t highlighted) {
assertNotNull(item, "Item cannot be NULL");
item->highlighted = highlighted;
}
errorret_t uiItemDraw(
const uiitem_t *item,
const float_t x,
const float_t y
) {
assertNotNull(item, "Item cannot be NULL");
if(item->item == ITEM_ID_NULL) errorOk();
const color_t color = item->highlighted ? COLOR_RED : COLOR_WHITE;
char_t text[UI_ITEM_LABEL_MAX];
stringFormat(
text, UI_ITEM_LABEL_MAX - 1,
"%s x%u", item->nameLabel, item->quantity
);
errorChain(textDraw(x, y, text, color, &FONT_DEFAULT));
errorOk();
}
+89
View File
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "rpg/item/item.h"
#define UI_ITEM_NAME_LABEL_MAX 32
typedef struct {
itemid_t item;
uint8_t quantity;
bool_t highlighted;
char_t nameLabel[UI_ITEM_NAME_LABEL_MAX];
} uiitem_t;
/**
* Initializes an item widget, resolving the item's localized name.
*
* @param item The item widget to initialize.
* @param itemId The item ID to display. ITEM_ID_NULL renders as an
* empty slot.
* @param quantity The stack quantity to display.
* @return Any error that occurs.
*/
errorret_t uiItemInit(
uiitem_t *item,
const itemid_t itemId,
const uint8_t quantity
);
/**
* Returns the item ID this widget displays.
*
* @param item The item widget to query.
* @returns The item ID.
*/
itemid_t uiItemGetItem(const uiitem_t *item);
/**
* Returns the stack quantity this widget displays.
*
* @param item The item widget to query.
* @returns The quantity.
*/
uint8_t uiItemGetQuantity(const uiitem_t *item);
/**
* Sets the stack quantity this widget displays.
*
* @param item The item widget to update.
* @param quantity The new quantity.
*/
void uiItemSetQuantity(uiitem_t *item, const uint8_t quantity);
/**
* Returns whether the item widget is highlighted.
*
* @param item The item widget to query.
* @returns True if highlighted.
*/
bool_t uiItemIsHighlighted(const uiitem_t *item);
/**
* Sets the highlighted state of the item widget.
*
* @param item The item widget to update.
* @param highlighted The new highlighted state.
*/
void uiItemSetHighlighted(uiitem_t *item, const bool_t highlighted);
/**
* Draws the item widget at the given screen position: item name and
* quantity. No-op for an empty (ITEM_ID_NULL) slot.
*
* @param item The item widget to draw.
* @param x Screen x position.
* @param y Screen y position.
* @return Any error that occurs.
*/
errorret_t uiItemDraw(
const uiitem_t *item,
const float_t x,
const float_t y
);
+179
View File
@@ -0,0 +1,179 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiitemlist.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "display/text/text.h"
void uiItemListInit(
uiitemlist_t *list,
const uint8_t columns,
const uint8_t rows,
const uint8_t itemColumns,
uiitemlistselectedcallback_t selected,
uiitemlistchangedcallback_t changed,
uiitemlistclosedcallback_t closed,
uiitemlistcolumncallback_t columnDraw
) {
assertNotNull(list, "Item list cannot be NULL");
assertTrue(columns > 0, "Item list columns must be > 0");
assertTrue(rows > 0, "Item list rows must be > 0");
assertTrue(itemColumns > 0, "Item list itemColumns must be > 0");
memoryZero(list, sizeof(uiitemlist_t));
list->columns = columns;
list->rows = rows;
list->itemColumns = itemColumns;
list->selected = selected;
list->changed = changed;
list->closed = closed;
list->columnDraw = columnDraw;
uiScrollingInit(&list->scroll);
}
void uiItemListSetItems(
uiitemlist_t *list,
const uiitem_t *items,
const uint8_t itemCount
) {
assertNotNull(list, "Item list cannot be NULL");
assertTrue(
itemCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list"
);
memoryCopy(list->items, items, sizeof(uiitem_t) * itemCount);
list->itemCount = itemCount;
}
errorret_t uiItemListSetItemStacks(
uiitemlist_t *list,
const inventorystack_t *stacks,
const uint8_t stackCount
) {
assertNotNull(list, "Item list cannot be NULL");
assertTrue(
stackCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list"
);
for(uint8_t i = 0; i < stackCount; i++) {
errorChain(
uiItemInit(&list->items[i], stacks[i].item, stacks[i].quantity)
);
}
list->itemCount = stackCount;
errorOk();
}
void uiItemListOpen(uiitemlist_t *list) {
assertNotNull(list, "Item list cannot be NULL");
if(list->focusItem != NULL) return;
list->focusItem = uiFocusPush(
list->columns, list->rows,
uiItemListFocusSelected,
uiItemListFocusChanged,
uiItemListFocusClosed,
NULL,
list
);
}
void uiItemListClose(uiitemlist_t *list) {
assertNotNull(list, "Item list cannot be NULL");
if(list->focusItem == NULL) return;
uiFocusPopItem(list->focusItem);
list->focusItem = NULL;
}
bool_t uiItemListIsActive(const uiitemlist_t *list) {
assertNotNull(list, "Item list cannot be NULL");
return list->focusItem != NULL;
}
errorret_t uiItemListDraw(
const uiitemlist_t *list,
const float_t x,
const float_t y,
const float_t width
) {
assertNotNull(list, "Item list cannot be NULL");
if(list->itemCount == 0) errorOk();
const float_t colStep = width / (float_t)list->columns;
const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
const float_t itemColStep = colStep / (float_t)list->itemColumns;
const uint8_t visibleMax = list->columns * list->rows;
const uint8_t drawCount =
list->itemCount < visibleMax ? list->itemCount : visibleMax;
for(uint8_t i = 0; i < drawCount; i++) {
const uint8_t col = i % list->columns;
const uint8_t row = i / list->columns;
const float_t ix = x + (float_t)col * colStep;
const float_t iy = y + (float_t)row * rowHeight;
errorChain(uiItemDraw(&list->items[i], ix, iy));
for(uint8_t c = 1; c < list->itemColumns; c++) {
if(list->columnDraw == NULL) continue;
errorChain(list->columnDraw(
list, &list->items[i], c, ix + (float_t)c * itemColStep, iy
));
}
}
errorOk();
}
bool_t uiItemListFocusSelected(const uifocusitem_t *focusItem) {
assertNotNull(focusItem, "Focus item cannot be NULL");
assertNotNull(focusItem->user, "Focus item user cannot be NULL");
uiitemlist_t *list = (uiitemlist_t *)focusItem->user;
if(list->selected == NULL) return true;
const uint8_t index = focusItem->y * list->columns + focusItem->x;
if(index >= list->itemCount) return true;
list->selected(list, index, &list->items[index]);
return true;
}
bool_t uiItemListFocusChanged(const uifocusitem_t *focusItem) {
assertNotNull(focusItem, "Focus item cannot be NULL");
assertNotNull(focusItem->user, "Focus item user cannot be NULL");
uiitemlist_t *list = (uiitemlist_t *)focusItem->user;
const uint8_t focusIndex = focusItem->y * list->columns + focusItem->x;
for(uint8_t i = 0; i < list->itemCount; i++) {
uiItemSetHighlighted(&list->items[i], i == focusIndex);
}
if(list->changed == NULL) return true;
if(focusIndex >= list->itemCount) return true;
list->changed(list, focusIndex, &list->items[focusIndex]);
return true;
}
bool_t uiItemListFocusClosed(const uifocusitem_t *focusItem) {
assertNotNull(focusItem, "Focus item cannot be NULL");
assertNotNull(focusItem->user, "Focus item user cannot be NULL");
uiitemlist_t *list = (uiitemlist_t *)focusItem->user;
list->focusItem = NULL;
for(uint8_t i = 0; i < list->itemCount; i++) {
uiItemSetHighlighted(&list->items[i], false);
}
if(list->closed != NULL) list->closed(list);
return true;
}
+200
View File
@@ -0,0 +1,200 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uiitem.h"
#include "ui/widget/uiscrolling.h"
#include "ui/focus/uifocus.h"
#include "rpg/item/inventory.h"
#define UI_ITEM_LIST_CAPACITY_MAX 40
typedef struct uiitemlist_s uiitemlist_t;
typedef void (*uiitemlistselectedcallback_t)(
const uiitemlist_t *list,
const uint8_t index,
const uiitem_t *item
);
typedef void (*uiitemlistchangedcallback_t)(
const uiitemlist_t *list,
const uint8_t index,
const uiitem_t *item
);
typedef void (*uiitemlistclosedcallback_t)(const uiitemlist_t *list);
/**
* Called to draw an extra per-item info column (columnIndex >= 1)
* beyond the item's own default uiItemDraw rendering -- e.g. a shop
* menu drawing a price alongside the item name.
*
* @param list The item list being drawn.
* @param item The item slot being drawn.
* @param columnIndex The info column being drawn (>= 1).
* @param x Screen x position for this column.
* @param y Screen y position for this column.
* @return Any error that occurs.
*/
typedef errorret_t (*uiitemlistcolumncallback_t)(
const uiitemlist_t *list,
const uiitem_t *item,
const uint8_t columnIndex,
const float_t x,
const float_t y
);
struct uiitemlist_s {
uiitem_t items[UI_ITEM_LIST_CAPACITY_MAX];
uint8_t itemCount;
// Grid layout: how many item slots wide/tall the list displays.
uint8_t columns;
uint8_t rows;
// How many info columns are rendered per item slot. 1 means only the
// item's own default name/quantity is drawn; anything beyond that is
// rendered via columnDraw.
uint8_t itemColumns;
uiscrolling_t scroll;
uifocusitem_t *focusItem;
uiitemlistselectedcallback_t selected;
uiitemlistchangedcallback_t changed;
uiitemlistclosedcallback_t closed;
uiitemlistcolumncallback_t columnDraw;
void *user;
};
/**
* Initializes an item list.
*
* @param list The item list to initialize.
* @param columns Number of item slots per row.
* @param rows Number of item slot rows.
* @param itemColumns Number of info columns rendered per item slot.
* @param selected Called when an item slot is selected.
* @param changed Called when the focused item slot changes.
* @param closed Called when the item list is closed.
* @param columnDraw Called to draw each extra info column (index >= 1);
* may be NULL if itemColumns is 1.
*/
void uiItemListInit(
uiitemlist_t *list,
const uint8_t columns,
const uint8_t rows,
const uint8_t itemColumns,
uiitemlistselectedcallback_t selected,
uiitemlistchangedcallback_t changed,
uiitemlistclosedcallback_t closed,
uiitemlistcolumncallback_t columnDraw
);
/**
* Sets the items displayed by the list directly, copying them into
* the list's own storage.
*
* @param list The item list to update.
* @param items The items to display.
* @param itemCount Number of entries in items. Must be <=
* UI_ITEM_LIST_CAPACITY_MAX.
*/
void uiItemListSetItems(
uiitemlist_t *list,
const uiitem_t *items,
const uint8_t itemCount
);
/**
* Sets the items displayed by the list from an array of item stacks,
* internally creating a uiitem_t for each stack.
*
* @param list The item list to update.
* @param stacks The item stacks to display.
* @param stackCount Number of entries in stacks. Must be <=
* UI_ITEM_LIST_CAPACITY_MAX.
* @return Any error that occurs.
*/
errorret_t uiItemListSetItemStacks(
uiitemlist_t *list,
const inventorystack_t *stacks,
const uint8_t stackCount
);
/**
* Pushes the item list onto the UI focus stack, making it navigable.
* No-op if already open.
*
* @param list The item list to open.
*/
void uiItemListOpen(uiitemlist_t *list);
/**
* Pops the item list from the UI focus stack. No-op if already closed.
*
* @param list The item list to close.
*/
void uiItemListClose(uiitemlist_t *list);
/**
* Returns whether the item list is currently on the UI focus stack.
*
* @param list The item list to query.
* @returns True if active.
*/
bool_t uiItemListIsActive(const uiitemlist_t *list);
/**
* Draws the item list's grid of item slots at the given position.
* Only the first columns * rows items are drawn.
*
* @param list The item list to draw.
* @param x Screen x position.
* @param y Screen y position.
* @param width Content width, used to lay out columns.
* @return Any error that occurs.
*/
errorret_t uiItemListDraw(
const uiitemlist_t *list,
const float_t x,
const float_t y,
const float_t width
);
/**
* Internal focus callback -- forwards selection to the list's selected
* handler.
*
* @param focusItem The active focus item; user field must point to
* uiitemlist_t.
* @returns True.
*/
bool_t uiItemListFocusSelected(const uifocusitem_t *focusItem);
/**
* Internal focus callback -- updates item highlights and fires
* changed.
*
* @param focusItem The active focus item; user field must point to
* uiitemlist_t.
* @returns True.
*/
bool_t uiItemListFocusChanged(const uifocusitem_t *focusItem);
/**
* Internal focus callback -- clears focusItem and fires the closed
* handler.
*
* @param focusItem The active focus item; user field must point to
* uiitemlist_t.
* @returns True.
*/
bool_t uiItemListFocusClosed(const uifocusitem_t *focusItem);
+15
View File
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiscrolling.h"
#include "assert/assert.h"
void uiScrollingInit(uiscrolling_t *scrolling) {
assertNotNull(scrolling, "Scrolling container cannot be NULL");
// Nothing to initialize yet -- uiscrolling_t is currently a
// placeholder with no fields.
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef struct {
} uiscrolling_t;
/**
* Initializes a scrolling container. Currently a placeholder -- no
* scrolling behavior is implemented yet.
*
* @param scrolling The scrolling container to initialize.
*/
void uiScrollingInit(uiscrolling_t *scrolling);
+7 -1
View File
@@ -78,6 +78,12 @@ errorret_t displayInitDolphin(void) {
GX_SetDispCopyGamma(GX_GM_1_0); GX_SetDispCopyGamma(GX_GM_1_0);
GX_SetColorUpdate(GX_TRUE); GX_SetColorUpdate(GX_TRUE);
// Without this, the EFB's Z-buffer format/compression is left at whatever
// GX_Init() defaulted to, so depth testing and the GX_MAX_Z24 clear value
// used every frame in frameBufferClearDolphin aren't guaranteed to line up
// with the actual EFB Z format.
GX_SetPixelFmt(GX_PF_RGB8_Z24, GX_ZC_LINEAR);
// Describe mesh vertex format. // Describe mesh vertex format.
GX_ClearVtxDesc(); GX_ClearVtxDesc();
GX_SetVtxDesc(GX_VA_POS, GX_INDEX16); GX_SetVtxDesc(GX_VA_POS, GX_INDEX16);
@@ -101,7 +107,7 @@ errorret_t displaySetStateDolphin(displaystate_t state) {
} }
if(state.flags & DISPLAY_STATE_FLAG_DEPTH_TEST) { if(state.flags & DISPLAY_STATE_FLAG_DEPTH_TEST) {
GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE); GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_FALSE);
} else { } else {
GX_SetZMode(GX_FALSE, GX_ALWAYS, GX_FALSE); GX_SetZMode(GX_FALSE, GX_ALWAYS, GX_FALSE);
} }
+102
View File
@@ -468,6 +468,102 @@ static void test_requireLoaded_propagates_error(void **state) {
assert_int_equal(memoryGetAllocatedCount(), 0); assert_int_equal(memoryGetAllocatedCount(), 0);
} }
// ============================================================
// assetReapUnused tests
// ============================================================
static void test_update_does_not_reap_automatically(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetEntryLock(entry);
assetUpdate();
assetUpdate(); // slot freed
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assetEntryUnlock(entry);
// Unlike the old behavior, assetUpdate no longer reaps zero-ref entries on
// its own - a LOADED entry must survive further updates untouched.
assetUpdate();
assetUpdate();
assetUpdate();
assert_int_equal(entry->type, ASSET_LOADER_TYPE_LOCALE);
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
errorret_t ret = assetEntryDispose(entry);
assert_true(errorIsOk(ret));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_reapUnused_disposes_zero_ref_loaded(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetEntryLock(entry);
assetUpdate();
assetUpdate();
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assetEntryUnlock(entry);
errorret_t ret = assetReapUnused();
assert_true(errorIsOk(ret));
assert_int_equal(entry->type, ASSET_LOADER_TYPE_NULL);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_reapUnused_ignores_referenced_entries(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetEntryLock(entry);
assetUpdate();
assetUpdate();
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
// Still locked - a reap must leave it alone.
errorret_t ret = assetReapUnused();
assert_true(errorIsOk(ret));
assert_int_equal(entry->type, ASSET_LOADER_TYPE_LOCALE);
assetEntryUnlock(entry);
errorret_t disposeRet = assetEntryDispose(entry);
assert_true(errorIsOk(disposeRet));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getEntry_reaps_when_pool_full(void **state) {
// Fill every entry slot with a zero-ref LOADED entry.
for(int i = 0; i < ASSET_ENTRY_COUNT_MAX; i++) {
char_t name[ASSET_FILE_NAME_MAX];
snprintf(name, sizeof(name), "full%d.locale", i);
assetentry_t *entry = assetGetEntry(name, ASSET_LOADER_TYPE_LOCALE, NULL);
assetEntryLock(entry);
assetUpdate();
assetUpdate();
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assetEntryUnlock(entry);
}
// The pool is now completely full of zero-ref LOADED entries with no
// ASSET_LOADER_TYPE_NULL slots left. Requesting one more must trigger an
// implicit reap instead of asserting.
assetentry_t *fresh = assetGetEntry("fresh.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assert_non_null(fresh);
assert_int_equal(fresh->state, ASSET_ENTRY_STATE_NOT_STARTED);
assetEntryLock(fresh);
assetUpdate();
assetUpdate();
assert_int_equal(fresh->state, ASSET_ENTRY_STATE_LOADED);
assetEntryUnlock(fresh);
errorret_t ret = assetEntryDispose(fresh);
assert_true(errorIsOk(ret));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================ // ============================================================
// main // main
// ============================================================ // ============================================================
@@ -503,6 +599,12 @@ int main(void) {
cmocka_unit_test_setup_teardown(test_requireLoaded_already_loaded, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_requireLoaded_already_loaded, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_requireLoaded_spins_to_loaded, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_requireLoaded_spins_to_loaded, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_requireLoaded_propagates_error, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_requireLoaded_propagates_error, asset_setup, asset_teardown),
// assetReapUnused
cmocka_unit_test_setup_teardown(test_update_does_not_reap_automatically, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_reapUnused_disposes_zero_ref_loaded, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_reapUnused_ignores_referenced_entries, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_getEntry_reaps_when_pool_full, asset_setup, asset_teardown),
}; };
return cmocka_run_group_tests(tests, NULL, NULL); return cmocka_run_group_tests(tests, NULL, NULL);
} }
+14 -10
View File
@@ -1,9 +1,9 @@
import argparse import argparse
import csv import json
import os import os
parser = argparse.ArgumentParser(description="Item CSV to .h defines") parser = argparse.ArgumentParser(description="Item JSON to .h defines")
parser.add_argument("--csv", required=True, help="Path to item CSV file") parser.add_argument("--json", required=True, help="Path to item JSON file")
parser.add_argument("--output", required=True, help="Path to output .h file") parser.add_argument("--output", required=True, help="Path to output .h file")
args = parser.parse_args() args = parser.parse_args()
@@ -13,16 +13,20 @@ def type_enum(name):
def id_enum(name): def id_enum(name):
return "ITEM_ID_" + name.upper() return "ITEM_ID_" + name.upper()
# Load CSV # Load JSON
item_ids = [] item_ids = []
item_types = [] item_types = []
rows = {} rows = {}
with open(args.csv, newline="", encoding="utf-8") as f: with open(args.json, encoding="utf-8") as f:
reader = csv.DictReader(f) entries = json.load(f)
if "id" not in reader.fieldnames or "type" not in reader.fieldnames:
raise ValueError("CSV must have 'id' and 'type' columns") if not all(
for row in reader: "id" in row and "type" in row and "name" in row for row in entries
):
raise ValueError("Each item must have 'id', 'type', and 'name' fields")
for row in entries:
item_id, item_type = row["id"], row["type"] item_id, item_type = row["id"], row["type"]
if item_id not in item_ids: if item_id not in item_ids:
item_ids.append(item_id) item_ids.append(item_id)
@@ -85,7 +89,7 @@ for i in item_ids:
f" [{id_enum(i)}] = {{", f" [{id_enum(i)}] = {{",
f" .id = {id_enum(i)},", f" .id = {id_enum(i)},",
f" .type = {type_enum(row['type'])},", f" .type = {type_enum(row['type'])},",
f" .name = \"{i}\",", f" .name = \"item.{row['name']}.name\",",
" },", " },",
] ]
out += [ out += [
-61
View File
@@ -1,61 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
/** A single keyframe descriptor passed to the Animation constructor. */
interface AnimationKeyframe {
/** Time offset in seconds. */
time: number;
/** Value at this keyframe. */
value: number;
/** Easing type (EASING_* constant). Defaults to EASING_LINEAR. */
easing?: number;
}
/** Keyframe-based float animation. */
interface AnimationInstance {
/**
* Interpolates the animation at the given time.
* @param time Time in seconds.
* @returns Interpolated value.
*/
getValue(time: number): number;
}
/** Constructs a new Animation from an array of keyframe descriptors. */
declare var Animation: {
new (keyframes: AnimationKeyframe[]): AnimationInstance;
};
/** Easing function utilities. */
interface EasingNamespace {
/**
* Applies the given easing function to normalized time t.
* @param type An EASING_* constant.
* @param t Normalized time in [0, 1].
* @returns Eased value in [0, 1].
*/
apply(type: number, t: number): number;
}
declare var Easing: EasingNamespace;
declare var EASING_LINEAR: number;
declare var EASING_IN_SINE: number;
declare var EASING_OUT_SINE: number;
declare var EASING_IN_OUT_SINE: number;
declare var EASING_IN_QUAD: number;
declare var EASING_OUT_QUAD: number;
declare var EASING_IN_OUT_QUAD: number;
declare var EASING_IN_CUBIC: number;
declare var EASING_OUT_CUBIC: number;
declare var EASING_IN_OUT_CUBIC: number;
declare var EASING_IN_QUART: number;
declare var EASING_OUT_QUART: number;
declare var EASING_IN_OUT_QUART: number;
declare var EASING_IN_BACK: number;
declare var EASING_OUT_BACK: number;
declare var EASING_IN_OUT_BACK: number;
-72
View File
@@ -1,72 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
/** Asset archive queries and cache management. */
interface AssetNamespace {
// Loader type constants
readonly TYPE_MESH: number;
readonly TYPE_TEXTURE: number;
readonly TYPE_TILESET: number;
readonly TYPE_LOCALE: number;
readonly TYPE_JSON: number;
readonly TYPE_SCRIPT: number;
// Mesh axis input constants (pass as `input` to lock with TYPE_MESH)
readonly MESH_AXIS_Y_UP: number;
readonly MESH_AXIS_Z_UP: number;
readonly MESH_AXIS_X_UP: number;
readonly MESH_AXIS_Y_DOWN: number;
readonly MESH_AXIS_Z_DOWN: number;
readonly MESH_AXIS_X_DOWN: number;
/**
* Returns `true` if the given path exists in the asset archive (`dusk.dsk`).
*
* @param path - Archive-relative path, e.g. `"init.js"` or `"ui/hud.png"`.
*/
exists(path: string): boolean;
/**
* Locks an entry in the asset cache and returns an `AssetEntry`.
* The entry begins loading in the background. Call `entry.requireLoaded()`
* to block until it is ready.
*
* The lock is released when the `AssetEntry` is GC'd or `entry.unlock()`
* is called explicitly.
*
* @param path - Archive-relative path.
* @param type - Loader type constant (`Asset.TYPE_*`).
* @param input - Optional loader-specific input constant.
* `TYPE_TEXTURE` → `Texture.FORMAT_*`
* `TYPE_MESH` → `Asset.MESH_AXIS_*`
*
* @example
* const entry = Asset.lock('data/map.json');
* entry.requireLoaded();
*/
lock(path: string, type: number, input?: number): AssetEntry;
/**
* Blocks until the given entry is fully loaded.
* Returns the entry for chaining.
* @throws If the load fails.
*
* @example
* const entry = Asset.requireLoaded(Asset.lock('map.json', Asset.TYPE_JSON));
*/
requireLoaded(entry: AssetEntry): AssetEntry;
/**
* Releases the lock on an asset by path.
* Prefer calling `entry.unlock()` on the `AssetEntry` object directly.
*
* @param path - The path originally passed to `lock`.
*/
unlock(path: string): void;
}
declare var Asset: AssetNamespace;
-97
View File
@@ -1,97 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
/**
* Descriptor for one entry in an `AssetBatch`.
*
* `format` - texture format constant (`Texture.FORMAT_*`). Alias for `input`
* when the type is `Asset.TYPE_TEXTURE`.
* `axis` - mesh axis constant (`Asset.MESH_AXIS_*`). Alias for `input`
* when the type is `Asset.TYPE_MESH`.
* `input` - generic numeric input for all other loader types.
*/
interface AssetBatchDescriptor {
path: string;
type: number;
format?: number;
axis?: number;
input?: number;
}
/** A group of asset entries locked and queued for loading together. */
interface AssetBatch {
/** Number of entries in the batch. */
readonly count: number;
/** `true` when every entry has reached `LOADED`. */
readonly isLoaded: boolean;
/** `true` if any entry is in an `ERROR` state. */
readonly hasError: boolean;
/**
* Returns a Promise that resolves when all entries have loaded, or rejects
* if any entry errors. Use with `await`.
*/
loaded(): Promise<void>;
/**
* Blocks (spin-waits) until every entry is loaded.
* Returns `this` for chaining.
* @throws If any entry fails to load.
*/
requireLoaded(): this;
/**
* Acquires one additional lock on every entry.
* Returns `this` for chaining.
*/
lock(): this;
/**
* Releases all locks and clears the batch.
* After this call the object is invalid - do not use it again.
*/
unlock(): void;
/**
* Returns the `AssetEntry` at `index`, adding an independent lock.
* The returned entry must be unlocked separately when no longer needed.
* Returns `undefined` if `index` is out of range or the batch is disposed.
*/
entry(index: number): AssetEntry | undefined;
/**
* Returns the first `AssetEntry` whose path matches, adding an
* independent lock. Returns `undefined` if no entry matches.
* The returned entry must be unlocked separately when no longer needed.
*/
getAssetByPath(path: string): AssetEntry | undefined;
/**
* Fires once when every entry has loaded successfully.
* Subscribe with `onLoaded[0] = () => { ... }`.
*/
readonly onLoaded: AssetEventProxy;
/**
* Fires each time a single entry finishes loading.
* Subscribe with `onEntryLoaded[0] = () => { ... }`.
*/
readonly onEntryLoaded: AssetEventProxy;
/**
* Fires once when all entries have finished but at least one errored.
* Subscribe with `onError[0] = () => { ... }`.
*/
readonly onError: AssetEventProxy;
/**
* Fires each time a single entry transitions to an error state.
* Subscribe with `onEntryError[0] = () => { ... }`.
*/
readonly onEntryError: AssetEventProxy;
toString(): string;
}
interface AssetBatchConstructor {
/** Creates a batch from an array of descriptors. Works with or without `new`. */
(descriptors: AssetBatchDescriptor[]): AssetBatch;
new(descriptors: AssetBatchDescriptor[]): AssetBatch;
}
declare var AssetBatch: AssetBatchConstructor;
-65
View File
@@ -1,65 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
/**
* A live reference to an entry in the asset cache.
* Holds a lock that keeps the entry alive; the lock is released automatically
* when the object is garbage collected, or immediately via `unlock()`.
*/
interface AssetEntry {
/** Archive-relative path used as the cache key. */
readonly name: string;
/** Current loading state - compare against `AssetEntry.*` state constants. */
readonly state: number;
/** Loader type - one of the `AssetEntry.TYPE_*` constants. */
readonly type: number;
/** `true` when the entry has fully loaded (`state === AssetEntry.LOADED`). */
readonly isLoaded: boolean;
/**
* Returns a `Texture` for this entry when it is a loaded texture asset.
* The `Texture` holds its own asset lock - independent of this `AssetEntry`.
* Returns `undefined` if the entry is not of type `Asset.TYPE_TEXTURE` or
* is not yet loaded.
*/
readonly texture: Texture | undefined;
/** Event proxy - subscribe up to 4 callbacks for when loading completes. */
readonly onLoaded: AssetEventProxy;
/** Event proxy - subscribe up to 4 callbacks for when the entry is disposed. */
readonly onUnloaded: AssetEventProxy;
/** Event proxy - subscribe up to 4 callbacks for when loading fails. */
readonly onError: AssetEventProxy;
/**
* Returns a Promise that resolves when the entry is loaded, or rejects on
* error. Use with `await`.
*/
loaded(): Promise<void>;
/**
* Blocks (spin-waits) until the entry reaches `LOADED` (or `ERROR`).
* Returns `this` for chaining.
* @throws If the load fails.
*/
requireLoaded(): this;
/**
* Releases the lock immediately.
* After this call the object is invalid - do not use it again.
*/
unlock(): void;
toString(): string;
}
interface AssetEntryConstructor {
// Loading state constants
readonly NOT_STARTED: number;
readonly PENDING: number;
readonly LOADING: number;
readonly LOADED: number;
readonly ERROR: number;
new(): never;
}
declare var AssetEntry: AssetEntryConstructor;
-23
View File
@@ -1,23 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
/**
* An event proxy with up to 4 subscribable callback slots (indices 03).
* Assign a function to subscribe; assign `null` to unsubscribe.
*
* @example
* assets.onLoaded[0] = () => { Console.print('all loaded'); };
* assets.onLoaded[0] = null; // unsubscribe
*/
interface AssetEventProxy {
0: (() => void) | null;
1: (() => void) | null;
2: (() => void) | null;
3: (() => void) | null;
/** Number of available slots (always 4). */
readonly length: number;
}

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