Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a84137b5ff | |||
| ca02ee0352 | |||
| fbaa54145e | |||
| 28754ffbf2 | |||
| 470c0eba7a | |||
| 7098dcec43 | |||
| 07137f57af | |||
| 8b7491a3d3 | |||
| 8cfa8ddfeb | |||
| ef284a15a1 | |||
| 3723921573 | |||
| 195399635e | |||
| 46e2a924d3 | |||
| b693ea4102 |
@@ -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+0000–U+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`.
|
||||
@@ -43,3 +43,24 @@ msgstr "Apply"
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "Discard unsaved changes?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "Characters"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "Items"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Settings"
|
||||
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
msgid "item.potato.name"
|
||||
msgstr "Potato"
|
||||
|
||||
msgid "item.apple.name"
|
||||
msgstr "Apple"
|
||||
@@ -44,3 +44,27 @@ msgstr "Aplicar"
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "¿Descartar los cambios no guardados?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "Personajes"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "Objetos"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Configuración"
|
||||
|
||||
#: src/dusk/rpg/item/item.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"
|
||||
|
||||
@@ -44,3 +44,27 @@ msgstr "適用"
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "未保存の変更を破棄しますか?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "キャラクター"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "アイテム"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/dusk/rpg/item/item.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 "リンゴ"
|
||||
|
||||
@@ -16,6 +16,9 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DOL=1
|
||||
ISO=2
|
||||
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
|
||||
|
||||
@@ -56,6 +56,7 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
DUSK_DISPLAY_HEIGHT=272
|
||||
DUSK_THREAD_PTHREAD
|
||||
DUSK_TIME_DYNAMIC
|
||||
DUSK_DISPLAY_OVERSCAN=6
|
||||
)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
|
||||
+48
-57
@@ -59,20 +59,28 @@ assetentry_t * assetGetEntry(
|
||||
entry++;
|
||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||
|
||||
// We did not find one existing, Find first available slot.
|
||||
entry = ASSET.entries;
|
||||
do {
|
||||
if(entry->type != ASSET_LOADER_TYPE_NULL) {
|
||||
entry++;
|
||||
continue;
|
||||
}
|
||||
// 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;
|
||||
do {
|
||||
if(entry->type != ASSET_LOADER_TYPE_NULL) {
|
||||
entry++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
|
||||
assetEntryInit(entry, name, type, input);
|
||||
return entry;
|
||||
}
|
||||
entry++;
|
||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
|
||||
assetEntryInit(entry, name, type, input);
|
||||
return entry;
|
||||
}
|
||||
entry++;
|
||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||
|
||||
if(reaped) break;
|
||||
reaped = true;
|
||||
errorCatch(assetReapUnused());
|
||||
}
|
||||
|
||||
assertUnreachable("No available asset entry slots.");
|
||||
return NULL;
|
||||
@@ -191,6 +199,32 @@ void assetUnlockEntry(assetentry_t *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) {
|
||||
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);
|
||||
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -413,26 +423,7 @@ errorret_t assetDispose(void) {
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
threadStop(&ASSET.loadThread);
|
||||
|
||||
// Drain-dispose: 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 *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);
|
||||
errorChain(assetReapUnused());
|
||||
|
||||
// Cleanup zip file.
|
||||
if(ASSET.zip != NULL) {
|
||||
|
||||
+12
-2
@@ -23,8 +23,8 @@
|
||||
#define ASSET_FILE_NAME "dusk.dsk"
|
||||
#define ASSET_HEADER_SIZE 3
|
||||
|
||||
#define ASSET_LOADING_COUNT_MAX 20
|
||||
#define ASSET_ENTRY_COUNT_MAX 128
|
||||
#define ASSET_LOADING_COUNT_MAX 10
|
||||
#define ASSET_ENTRY_COUNT_MAX 64
|
||||
|
||||
typedef struct asset_s {
|
||||
zip_t *zip;
|
||||
@@ -112,6 +112,16 @@ void assetUnlock(const char_t *name);
|
||||
*/
|
||||
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
|
||||
* is fully loaded.
|
||||
|
||||
@@ -111,9 +111,15 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
size_t offset = 8;
|
||||
|
||||
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
|
||||
out->tiles = memoryAllocate(tileSize);
|
||||
memoryCopy(out->tiles, data + 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];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
@@ -135,6 +141,9 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
|
||||
memoryCopy(out->meshOffsets[m], data + 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);
|
||||
@@ -157,6 +166,12 @@ errorret_t assetChunkDispose(assetentry_t *entry) {
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
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++) {
|
||||
if(out->modelEntries[m] == NULL) continue;
|
||||
assetUnlockEntry(out->modelEntries[m]);
|
||||
|
||||
@@ -34,7 +34,7 @@ typedef struct {
|
||||
} assetchunkloaderloading_t;
|
||||
|
||||
typedef struct {
|
||||
tile_t tiles[CHUNK_TILE_COUNT];
|
||||
tile_t *tiles;
|
||||
uint8_t meshCount;
|
||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||
|
||||
@@ -48,8 +48,20 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
|
||||
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
|
||||
meshvertex_t *vertices = NULL;
|
||||
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));
|
||||
|
||||
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);
|
||||
|
||||
@@ -103,18 +115,22 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
|
||||
out->vertices = NULL;
|
||||
errorChain(ret);
|
||||
}
|
||||
out->meshInitialized = true;
|
||||
|
||||
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
|
||||
if(errorIsNotOk(ret)) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
meshDispose(&out->mesh);
|
||||
out->meshInitialized = false;
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
#ifndef DUSK_OPENGL_LEGACY
|
||||
// VBO owns the data now; CPU copy is no longer needed.
|
||||
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
|
||||
// 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);
|
||||
out->vertices = NULL;
|
||||
#endif
|
||||
@@ -129,8 +145,11 @@ errorret_t assetMeshDispose(assetentry_t *entry) {
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetmeshoutput_t *out = &entry->data.mesh;
|
||||
if(out->vertices != NULL) {
|
||||
if(out->meshInitialized) {
|
||||
errorChain(meshDispose(&out->mesh));
|
||||
out->meshInitialized = false;
|
||||
}
|
||||
if(out->vertices != NULL) {
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
mesh_t mesh;
|
||||
bool_t meshInitialized;
|
||||
meshvertex_t *vertices;
|
||||
} assetmeshoutput_t;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ console_t CONSOLE;
|
||||
|
||||
void consoleInit(void) {
|
||||
memoryZero(&CONSOLE, sizeof(console_t));
|
||||
CONSOLE.visible = true;
|
||||
CONSOLE.visible = false;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexInit(&CONSOLE.printMutex);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "battle.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
battle_t BATTLE;
|
||||
@@ -40,10 +41,183 @@ battlefighter_t *battleAddFighter(
|
||||
return fighter;
|
||||
}
|
||||
|
||||
void battleStart(void) {
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
) {
|
||||
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
|
||||
|
||||
BATTLE.encounterType = encounterType;
|
||||
BATTLE.fleeAvailable = fleeAvailable;
|
||||
BATTLE.result = BATTLE_RESULT_NONE;
|
||||
BATTLE.round = 1;
|
||||
BATTLE.turnIndex = 0;
|
||||
battleBuildTurnOrder(true);
|
||||
|
||||
BATTLE.active = true;
|
||||
}
|
||||
|
||||
void battleDispose(void) {
|
||||
battleInit();
|
||||
}
|
||||
|
||||
battlefighter_t *battleGetCurrentFighter(void) {
|
||||
if(!BATTLE.active) return NULL;
|
||||
if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
|
||||
return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
|
||||
}
|
||||
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
||||
uint8_t count = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].team != team) continue;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
) {
|
||||
assertNotNull(attacker, "Attacker cannot be NULL");
|
||||
assertNotNull(defender, "Defender cannot be NULL");
|
||||
|
||||
const int32_t rawDamage =
|
||||
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
|
||||
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
|
||||
|
||||
defender->health = damage >= defender->health ? 0 : defender->health - damage;
|
||||
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
void battleNextTurn(void) {
|
||||
BATTLE.turnIndex++;
|
||||
if(BATTLE.turnIndex < BATTLE.turnCount) return;
|
||||
|
||||
BATTLE.round++;
|
||||
BATTLE.turnIndex = 0;
|
||||
battleBuildTurnOrder(false);
|
||||
}
|
||||
|
||||
battleresult_t battleCheckResult(void) {
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
||||
|
||||
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
||||
BATTLE.result = BATTLE_RESULT_LOSS;
|
||||
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
||||
BATTLE.result = BATTLE_RESULT_WIN;
|
||||
}
|
||||
|
||||
return BATTLE.result;
|
||||
}
|
||||
|
||||
void battlePlayerAttack(const uint8_t targetIndex) {
|
||||
battlefighter_t *attacker = battleGetCurrentFighter();
|
||||
if(attacker == NULL) return;
|
||||
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
||||
|
||||
battlefighter_t *defender = &BATTLE.fighters[targetIndex];
|
||||
if(!battleFighterIsAlive(defender)) return;
|
||||
|
||||
battleResolveAttack(attacker, defender);
|
||||
battleCheckResult();
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||
}
|
||||
|
||||
void battlePlayerFlee(void) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||
if(!BATTLE.fleeAvailable) return;
|
||||
|
||||
BATTLE.result = BATTLE_RESULT_FLED;
|
||||
}
|
||||
|
||||
void battleUpdate(void) {
|
||||
if(!BATTLE.active) return;
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return;
|
||||
|
||||
battlefighter_t *current = battleGetCurrentFighter();
|
||||
if(current == NULL) return;
|
||||
|
||||
if(!battleFighterIsAlive(current)) {
|
||||
battleNextTurn();
|
||||
return;
|
||||
}
|
||||
|
||||
if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
|
||||
|
||||
battlefighter_t *target = battleAIChooseTarget(current);
|
||||
if(target != NULL) battleResolveAttack(current, target);
|
||||
|
||||
battleCheckResult();
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||
}
|
||||
|
||||
void battleBuildTurnOrder(const bool_t applyEncounterBias) {
|
||||
BATTLE.turnCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
BATTLE.turnOrder[BATTLE.turnCount++] = i;
|
||||
}
|
||||
|
||||
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
||||
for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
|
||||
const uint8_t key = BATTLE.turnOrder[i];
|
||||
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
||||
|
||||
int8_t j = (int8_t)i - 1;
|
||||
while(
|
||||
j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
|
||||
) {
|
||||
BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
|
||||
j--;
|
||||
}
|
||||
BATTLE.turnOrder[j + 1] = key;
|
||||
}
|
||||
|
||||
if(!applyEncounterBias) return;
|
||||
|
||||
if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY);
|
||||
} else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY);
|
||||
}
|
||||
}
|
||||
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team) {
|
||||
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t count = 0;
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
|
||||
sorted[count++] = BATTLE.turnOrder[i];
|
||||
}
|
||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
|
||||
sorted[count++] = BATTLE.turnOrder[i];
|
||||
}
|
||||
|
||||
memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
|
||||
}
|
||||
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||
const battlefighterteam_t enemyTeam =
|
||||
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
||||
|
||||
battlefighter_t *weakest = NULL;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
battlefighter_t *candidate = &BATTLE.fighters[i];
|
||||
if(candidate->team != enemyTeam) continue;
|
||||
if(!battleFighterIsAlive(candidate)) continue;
|
||||
if(weakest == NULL || candidate->health < weakest->health) {
|
||||
weakest = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return weakest;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,36 @@
|
||||
|
||||
#define BATTLE_FIGHTER_COUNT_MAX 8
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ENCOUNTER_REGULAR,
|
||||
BATTLE_ENCOUNTER_PLAYER_ADVANTAGE,
|
||||
BATTLE_ENCOUNTER_BACK_ATTACK,
|
||||
|
||||
BATTLE_ENCOUNTER_COUNT
|
||||
} battleencountertype_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_RESULT_NONE,
|
||||
BATTLE_RESULT_WIN,
|
||||
BATTLE_RESULT_LOSS,
|
||||
BATTLE_RESULT_FLED,
|
||||
|
||||
BATTLE_RESULT_COUNT
|
||||
} battleresult_t;
|
||||
|
||||
typedef struct {
|
||||
bool_t active;
|
||||
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
||||
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
battleresult_t result;
|
||||
|
||||
// Fighter indices (into fighters[]), sorted for the current round.
|
||||
uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t turnCount;
|
||||
uint8_t turnIndex;
|
||||
uint16_t round;
|
||||
} battle_t;
|
||||
|
||||
extern battle_t BATTLE;
|
||||
@@ -50,12 +77,118 @@ battlefighter_t *battleAddFighter(
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts a battle, marking it active. Any fighters already added via
|
||||
* battleAddFighter remain in place.
|
||||
* Starts the battle: builds the opening turn order (biased by
|
||||
* encounterType for the first round only) and marks the battle active.
|
||||
* Call once every fighter has been added via battleAddFighter.
|
||||
*
|
||||
* @param encounterType Determines the opening round's turn order.
|
||||
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
||||
*/
|
||||
void battleStart(void);
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of the battle, clearing all fighters and marking it inactive.
|
||||
*/
|
||||
void battleDispose(void);
|
||||
|
||||
/**
|
||||
* Returns the fighter whose turn it currently is.
|
||||
*
|
||||
* @return Pointer to the active fighter, or NULL if the battle isn't
|
||||
* active or has no living fighters left to act.
|
||||
*/
|
||||
battlefighter_t *battleGetCurrentFighter(void);
|
||||
|
||||
/**
|
||||
* Returns the number of living fighters on a team.
|
||||
*
|
||||
* @param team The team to count.
|
||||
* @return Count of living fighters on that team.
|
||||
*/
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Resolves a physical attack from attacker onto defender: damage is the
|
||||
* attacker's attack stat minus the defender's defense stat (minimum 1),
|
||||
* subtracted from the defender's health. The defender is marked dead
|
||||
* once health reaches 0.
|
||||
*
|
||||
* @param attacker The attacking fighter.
|
||||
* @param defender The defending fighter.
|
||||
*/
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
);
|
||||
|
||||
/**
|
||||
* Ends the current fighter's turn and advances to the next fighter in
|
||||
* the turn order, starting a new round (rebuilding turn order purely by
|
||||
* speed) once every fighter in the current round has acted.
|
||||
*/
|
||||
void battleNextTurn(void);
|
||||
|
||||
/**
|
||||
* Checks whether the battle has been won or lost, updating and
|
||||
* returning BATTLE.result. Does nothing if a result has already been
|
||||
* set (e.g. by a successful flee).
|
||||
*
|
||||
* @return The battle's current result.
|
||||
*/
|
||||
battleresult_t battleCheckResult(void);
|
||||
|
||||
/**
|
||||
* Submits the current fighter's attack against a target, if it is
|
||||
* currently a player-controlled fighter's turn. Resolves the attack,
|
||||
* checks for a battle result, and advances the turn.
|
||||
*
|
||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||
*/
|
||||
void battlePlayerAttack(const uint8_t targetIndex);
|
||||
|
||||
/**
|
||||
* Submits a flee attempt for the current fighter's turn, if it is
|
||||
* currently a player-controlled fighter's turn and fleeing is
|
||||
* available for this battle. Always succeeds, ending the battle with
|
||||
* BATTLE_RESULT_FLED.
|
||||
*/
|
||||
void battlePlayerFlee(void);
|
||||
|
||||
/**
|
||||
* Updates the battle simulation for one frame: resolves the current
|
||||
* fighter's turn automatically if AI-controlled, otherwise waits for a
|
||||
* player action via battlePlayerAttack/battlePlayerFlee. No-op if the
|
||||
* battle isn't active or already has a result.
|
||||
*/
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Rebuilds BATTLE.turnOrder/turnCount from every currently living
|
||||
* fighter, sorted by speed descending.
|
||||
*
|
||||
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
||||
* queue so BATTLE.encounterType's favoured team goes first (used only
|
||||
* for the opening round).
|
||||
*/
|
||||
void battleBuildTurnOrder(const bool_t applyEncounterBias);
|
||||
|
||||
/**
|
||||
* Stably partitions BATTLE.turnOrder so every fighter on the given team
|
||||
* comes first, preserving each side's relative (speed-sorted) order.
|
||||
*
|
||||
* @param team The team to move to the front of the turn order.
|
||||
*/
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Picks an AI target for fighter: the lowest-health living fighter on
|
||||
* the opposing team.
|
||||
*
|
||||
* @param fighter The AI-controlled fighter choosing a target.
|
||||
* @return The chosen target, or NULL if the opposing team has no
|
||||
* living fighters.
|
||||
*/
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
||||
|
||||
@@ -34,6 +34,22 @@ typedef struct cutscene_s {
|
||||
#define CUTSCENE_TEXT(TEXT) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { .text = TEXT } }
|
||||
|
||||
#define CUTSCENE_TEXT_MINI(TEXT, X, Y, Z, DURATION) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI, \
|
||||
.textMini = { \
|
||||
.text = TEXT, \
|
||||
.position = { X, Y, Z }, \
|
||||
.duration = DURATION \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_TEXT_MINI_HIDE(INDEX) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE, \
|
||||
.textMiniHide = { .index = INDEX } \
|
||||
}
|
||||
|
||||
#define CUTSCENE_WAIT(WAIT) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
|
||||
|
||||
@@ -128,6 +144,24 @@ typedef struct cutscene_s {
|
||||
#define CUTSCENE_FADE_FROM_WHITE(DURATION) \
|
||||
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) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ void cutsceneSystemStartCutsceneWith(
|
||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
|
||||
cutsceneSystemNext();
|
||||
}
|
||||
@@ -65,6 +66,7 @@ void cutsceneSystemNext() {
|
||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,6 +132,18 @@ uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
|
||||
return areaId;
|
||||
}
|
||||
|
||||
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
|
||||
if(index == CUTSCENE_TEXT_MINI_LAST_CREATED) {
|
||||
assertTrue(
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated != CUTSCENE_TEXT_MINI_LAST_CREATED,
|
||||
"CUTSCENE_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
|
||||
"shown"
|
||||
);
|
||||
return CUTSCENE_SYSTEM.textMiniLastCreated;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
void cutsceneSystemDispose() {
|
||||
CUTSCENE_SYSTEM.scene = NULL;
|
||||
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
||||
@@ -139,4 +153,5 @@ void cutsceneSystemDispose() {
|
||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ typedef struct entity_s entity_t;
|
||||
#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
|
||||
#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
|
||||
#define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF)
|
||||
#define CUTSCENE_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA)
|
||||
|
||||
// Maximum number of bytes a running cutscene may request via
|
||||
// cutscene_t.dataSize.
|
||||
@@ -29,6 +30,7 @@ typedef struct {
|
||||
entity_t *entityLastCreated;
|
||||
entity_t *entityLastRef;
|
||||
uint8_t areaLastCreated;
|
||||
uint8_t textMiniLastCreated;
|
||||
|
||||
// Data (used by the current item).
|
||||
cutsceneitemdata_t data;
|
||||
@@ -86,6 +88,15 @@ entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
|
||||
*/
|
||||
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
|
||||
|
||||
/**
|
||||
* Resolves a raw mini textbox slot index (or CUTSCENE_TEXT_MINI_LAST_CREATED
|
||||
* sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index.
|
||||
*
|
||||
* @param index Raw slot index or sentinel value.
|
||||
* @returns The resolved slot index.
|
||||
*/
|
||||
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
|
||||
|
||||
/**
|
||||
* Advance to the next item in the cutscene.
|
||||
*/
|
||||
|
||||
@@ -14,3 +14,4 @@ add_subdirectory(entity)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(maparea)
|
||||
add_subdirectory(ui)
|
||||
add_subdirectory(battle)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscenestartbattle.c
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/battle/party.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
void cutsceneStartBattleStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
const cutscenestartbattle_t *config = &item->startBattle;
|
||||
|
||||
battleInit();
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
battlefighter_t *member = partyGetOrderMember(i);
|
||||
if(member == NULL) continue;
|
||||
|
||||
battlefighter_t *fighter = battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
member->stats, member->healthMax, member->mpMax
|
||||
);
|
||||
if(fighter == NULL) continue;
|
||||
|
||||
fighter->health = member->health;
|
||||
fighter->mp = member->mp;
|
||||
fighter->status = member->status;
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < config->enemyCount; i++) {
|
||||
const cutscenestartbattleenemy_t *enemy = &config->enemies[i];
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemy->stats, enemy->healthMax, enemy->mpMax
|
||||
);
|
||||
}
|
||||
|
||||
battleStart(config->encounterType, config->fleeAvailable);
|
||||
sceneSet(SCENE_TYPE_BATTLE);
|
||||
}
|
||||
|
||||
bool_t cutsceneStartBattleUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) return false;
|
||||
|
||||
// Sync ally HP/MP back to the persistent party roster. Relies on
|
||||
// ally fighters having been added to BATTLE.fighters in the same
|
||||
// order partyGetOrderMember() iterates, starting at index 0 (see
|
||||
// cutsceneStartBattleStart).
|
||||
uint8_t allySlot = 0;
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
battlefighter_t *member = partyGetOrderMember(i);
|
||||
if(member == NULL) continue;
|
||||
|
||||
battlefighter_t *fighter = &BATTLE.fighters[allySlot++];
|
||||
member->health = fighter->health;
|
||||
member->mp = fighter->mp;
|
||||
member->status = fighter->status;
|
||||
}
|
||||
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
battleDispose();
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
#define CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX 4
|
||||
|
||||
typedef struct {
|
||||
battlefighterstats_t stats;
|
||||
uint16_t healthMax;
|
||||
uint16_t mpMax;
|
||||
} cutscenestartbattleenemy_t;
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
typedef struct {
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
uint8_t enemyCount;
|
||||
cutscenestartbattleenemy_t enemies[CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX];
|
||||
} cutscenestartbattle_t;
|
||||
|
||||
/**
|
||||
* Starts a battle: seeds BATTLE with the party's active order members
|
||||
* and the item's configured enemies, then switches to the battle
|
||||
* scene.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneStartBattleStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Waits for the battle to produce a result, syncs ally HP/MP back to
|
||||
* the party roster, then returns to the overworld scene.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true once the battle has ended.
|
||||
*/
|
||||
bool_t cutsceneStartBattleUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -7,132 +7,146 @@
|
||||
|
||||
#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) {
|
||||
switch(item->type) {
|
||||
case CUTSCENE_ITEM_TYPE_TEXT:
|
||||
cutsceneTextStart(item, data);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_CALLBACK:
|
||||
cutsceneCallbackStart(item, data);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_WAIT:
|
||||
cutsceneWaitStart(item, data);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_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;
|
||||
}
|
||||
cutsceneiteminitcallback_t *init = CUTSCENE_ITEM_CALLBACKS[item->type].init;
|
||||
if(init != NULL) init(item, data);
|
||||
}
|
||||
|
||||
bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
|
||||
switch(item->type) {
|
||||
case CUTSCENE_ITEM_TYPE_TEXT:
|
||||
return cutsceneTextUpdate(item, data);
|
||||
bool_t cutsceneItemUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
cutsceneitemupdatecallback_t *update =
|
||||
CUTSCENE_ITEM_CALLBACKS[item->type].update;
|
||||
if(update == NULL) return false;
|
||||
|
||||
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 update(item, data);
|
||||
}
|
||||
|
||||
void cutsceneCutsceneStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene);
|
||||
}
|
||||
|
||||
bool_t cutsceneCutsceneUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -17,17 +17,25 @@
|
||||
#include "entity/cutsceneentityturn.h"
|
||||
#include "entity/cutsceneentitywalktoentity.h"
|
||||
#include "ui/cutscenetext.h"
|
||||
#include "ui/cutscenetextmini.h"
|
||||
#include "ui/cutscenetextminihide.h"
|
||||
#include "ui/cutscenefade.h"
|
||||
#include "ui/cutsceneemoji.h"
|
||||
#include "ui/cutsceneshake.h"
|
||||
#include "item/cutsceneitemgive.h"
|
||||
#include "maparea/cutscenemapareaadd.h"
|
||||
#include "maparea/cutscenemaparearemove.h"
|
||||
#include "maparea/cutscenemapareawait.h"
|
||||
#include "battle/cutscenestartbattle.h"
|
||||
|
||||
typedef struct cutscene_s cutscene_t;
|
||||
|
||||
typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_NULL,
|
||||
|
||||
CUTSCENE_ITEM_TYPE_TEXT,
|
||||
CUTSCENE_ITEM_TYPE_TEXT_MINI,
|
||||
CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE,
|
||||
CUTSCENE_ITEM_TYPE_CALLBACK,
|
||||
CUTSCENE_ITEM_TYPE_WAIT,
|
||||
CUTSCENE_ITEM_TYPE_CUTSCENE,
|
||||
@@ -43,7 +51,12 @@ typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY,
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
|
||||
CUTSCENE_ITEM_TYPE_START_BATTLE,
|
||||
CUTSCENE_ITEM_TYPE_EMOJI,
|
||||
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||
|
||||
CUTSCENE_ITEM_TYPE_COUNT
|
||||
} cutsceneitemtype_t;
|
||||
|
||||
struct cutsceneitem_s {
|
||||
@@ -51,6 +64,8 @@ struct cutsceneitem_s {
|
||||
|
||||
union {
|
||||
cutscenetext_t text;
|
||||
cutscenetextmini_t textMini;
|
||||
cutscenetextminihide_t textMiniHide;
|
||||
cutscenecallback_t callback;
|
||||
cutscenewait_t wait;
|
||||
const cutscene_t *cutscene;
|
||||
@@ -67,6 +82,9 @@ struct cutsceneitem_s {
|
||||
cutscenemapareaadd_t mapAreaAdd;
|
||||
cutscenemaparearemove_t mapAreaRemove;
|
||||
cutscenemapareawait_t mapAreaWait;
|
||||
cutscenestartbattle_t startBattle;
|
||||
cutsceneemoji_t emoji;
|
||||
cutsceneshake_t shake;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -77,6 +95,24 @@ typedef union cutsceneitemdata_u {
|
||||
cutscenemapareawaitdata_t mapAreaWait;
|
||||
} 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.
|
||||
*
|
||||
@@ -99,3 +135,29 @@ bool_t cutsceneItemUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
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/item/itemgive.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
|
||||
void cutsceneItemGiveStart(
|
||||
const cutsceneitem_t *item,
|
||||
|
||||
@@ -6,5 +6,9 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscenetext.c
|
||||
cutscenetextmini.c
|
||||
cutscenetextminihide.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
|
||||
);
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
|
||||
void cutsceneTextStart(
|
||||
const cutsceneitem_t *item,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
|
||||
void cutsceneTextMiniStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
uint8_t index = uiTextboxMiniListGetNext();
|
||||
uiTextboxMiniShow(
|
||||
&UI_TEXTBOX_MINI_LIST[index],
|
||||
item->textMini.text,
|
||||
item->textMini.position,
|
||||
item->textMini.duration,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
CUTSCENE_SYSTEM.textMiniLastCreated = index;
|
||||
}
|
||||
|
||||
bool_t cutsceneTextMiniUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
#define CUTSCENE_TEXT_MINI_MAX_CHARS 128
|
||||
|
||||
typedef struct {
|
||||
char_t text[CUTSCENE_TEXT_MINI_MAX_CHARS];
|
||||
vec3 position;
|
||||
float_t duration;
|
||||
} cutscenetextmini_t;
|
||||
|
||||
/**
|
||||
* Starts a mini text item (shows a mini textbox at the given world
|
||||
* position for the given duration, then completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneTextMiniStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a mini text item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneTextMiniUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
|
||||
void cutsceneTextMiniHideStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
uint8_t index = cutsceneSystemGetTextMiniId(item->textMiniHide.index);
|
||||
uiTextboxMiniClose(&UI_TEXTBOX_MINI_LIST[index]);
|
||||
}
|
||||
|
||||
bool_t cutsceneTextMiniHideUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t index;
|
||||
} cutscenetextminihide_t;
|
||||
|
||||
/**
|
||||
* Starts a mini text hide step (closes the mini textbox immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneTextMiniHideStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a mini text hide step (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneTextMiniHideUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -16,6 +16,10 @@ CUTSCENE(TEST_ONE, 0, DEFAULT,
|
||||
CUTSCENE(TEST_TWO, 0, DEFAULT,
|
||||
CUTSCENE_TEXT("Test Two."),
|
||||
CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0),
|
||||
CUTSCENE_TEXT_MINI("Hello!", 4, 4, 0, 3.0f),
|
||||
CUTSCENE_EMOJI(
|
||||
CUTSCENE_ENTITY_LAST_CREATED, UI_EMOJI_EXCLAMATION_MARK, 2.0f
|
||||
),
|
||||
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_LAST_CREATED, 8, 2, 0),
|
||||
// CUTSCENE_CONCURRENT(
|
||||
// CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "assert/assert.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
|
||||
void entityInteractWith(entity_t *player, entity_t *target) {
|
||||
assertNotNull(player, "Player entity pointer cannot be NULL");
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "assert/assert.h"
|
||||
#include "rpg/item/itemgive.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
|
||||
void entityItemInit(entity_t *entity) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
item.c
|
||||
inventory.c
|
||||
backpack.c
|
||||
itemgive.c
|
||||
@@ -13,9 +14,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
|
||||
# Item Definitions
|
||||
dusk_run_python(
|
||||
dusk_item_csv_defs
|
||||
dusk_item_json_defs
|
||||
tools.item
|
||||
--csv ${CMAKE_CURRENT_SOURCE_DIR}/item.csv
|
||||
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/item.h
|
||||
--json ${CMAKE_CURRENT_SOURCE_DIR}/item.json
|
||||
--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)
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "item.h"
|
||||
#include "assert/assert.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
|
||||
errorret_t itemGetName(
|
||||
const itemid_t item,
|
||||
char_t *buffer,
|
||||
const size_t bufferSize
|
||||
) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
ITEMS[item].name,
|
||||
0,
|
||||
buffer,
|
||||
bufferSize
|
||||
));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
id,type,weight
|
||||
POTION,MEDICINE,1.0
|
||||
POTATO,FOOD,0.5
|
||||
APPLE,FOOD,0.3
|
||||
|
@@ -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
|
||||
);
|
||||
@@ -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" }
|
||||
]
|
||||
@@ -7,18 +7,25 @@
|
||||
|
||||
#include "itemgive.h"
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "rpg/item/item.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
#include "util/string.h"
|
||||
#include "error/error.h"
|
||||
|
||||
#define ITEM_GIVE_NAME_MAX_CHARS 32
|
||||
|
||||
void itemGive(const itemid_t item, const uint8_t quantity) {
|
||||
backpackAdd(item, quantity);
|
||||
|
||||
char_t name[ITEM_GIVE_NAME_MAX_CHARS];
|
||||
errorCatch(itemGetName(item, name, ITEM_GIVE_NAME_MAX_CHARS));
|
||||
|
||||
char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS];
|
||||
stringFormat(
|
||||
msg,
|
||||
ITEM_GIVE_MESSAGE_MAX_CHARS - 1,
|
||||
"Received %s x%u",
|
||||
ITEMS[item].name,
|
||||
name,
|
||||
(uint32_t)quantity
|
||||
);
|
||||
uiTextboxMainSetText(msg);
|
||||
|
||||
+107
-63
@@ -21,21 +21,13 @@ errorret_t mapInit() {
|
||||
memoryZero(&MAP, sizeof(map_t));
|
||||
MAP.loaded = true;
|
||||
|
||||
MAP.loadPosition = (chunkpos_t){
|
||||
-(MAP_CHUNK_SKIN),
|
||||
-(MAP_CHUNK_SKIN),
|
||||
-(MAP_CHUNK_SKIN)
|
||||
};
|
||||
|
||||
chunkindex_t i = 0;
|
||||
for(chunkunit_t z = 0; z < MAP_LOADED_CHUNK_DEPTH; z++) {
|
||||
for(chunkunit_t y = 0; y < MAP_LOADED_CHUNK_HEIGHT; y++) {
|
||||
for(chunkunit_t x = 0; x < MAP_LOADED_CHUNK_WIDTH; x++) {
|
||||
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
||||
for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
||||
for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
||||
chunk_t *chunk = &MAP.chunks[i++];
|
||||
chunk->position = (chunkpos_t){
|
||||
MAP.loadPosition.x + (chunkunit_t)x,
|
||||
MAP.loadPosition.y + (chunkunit_t)y,
|
||||
MAP.loadPosition.z + (chunkunit_t)z
|
||||
(chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z
|
||||
};
|
||||
errorChain(mapChunkLoad(chunk));
|
||||
}
|
||||
@@ -54,48 +46,23 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
|
||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||
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.
|
||||
chunkindex_t chunksFreed[MAP_LOADED_CHUNK_COUNT];
|
||||
chunkindex_t chunksFreed[MAP_CHUNK_COUNT];
|
||||
uint32_t freedCount = 0;
|
||||
|
||||
// Use a boolean grid so the inner load loop can check O(1).
|
||||
bool_t posLoaded[MAP_LOADED_CHUNK_WIDTH][MAP_LOADED_CHUNK_HEIGHT]
|
||||
[MAP_LOADED_CHUNK_DEPTH];
|
||||
bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH];
|
||||
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];
|
||||
chunkunit_t rx = chunk->position.x - newLoadPos.x;
|
||||
chunkunit_t ry = chunk->position.y - newLoadPos.y;
|
||||
chunkunit_t rz = chunk->position.z - newLoadPos.z;
|
||||
chunkunit_t rx = chunk->position.x - newPos.x;
|
||||
chunkunit_t ry = chunk->position.y - newPos.y;
|
||||
chunkunit_t rz = chunk->position.z - newPos.z;
|
||||
if(
|
||||
rx >= 0 && rx < MAP_LOADED_CHUNK_WIDTH &&
|
||||
ry >= 0 && ry < MAP_LOADED_CHUNK_HEIGHT &&
|
||||
rz >= 0 && rz < MAP_LOADED_CHUNK_DEPTH
|
||||
rx >= 0 && rx < MAP_CHUNK_WIDTH &&
|
||||
ry >= 0 && ry < MAP_CHUNK_HEIGHT &&
|
||||
rz >= 0 && rz < MAP_CHUNK_DEPTH
|
||||
) {
|
||||
posLoaded[rx][ry][rz] = true;
|
||||
} 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 y = 0; y < MAP_LOADED_CHUNK_HEIGHT; y++) {
|
||||
for(chunkunit_t x = 0; x < MAP_LOADED_CHUNK_WIDTH; x++) {
|
||||
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
||||
for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
||||
for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
||||
if(posLoaded[x][y][z]) continue;
|
||||
assertTrue(freedCount > 0, "No free chunk slot available.");
|
||||
chunk_t *chunk = &MAP.chunks[chunksFreed[--freedCount]];
|
||||
chunk->position = (chunkpos_t){
|
||||
newLoadPos.x + (chunkunit_t)x,
|
||||
newLoadPos.y + (chunkunit_t)y,
|
||||
newLoadPos.z + (chunkunit_t)z
|
||||
newPos.x + (chunkunit_t)x,
|
||||
newPos.y + (chunkunit_t)y,
|
||||
newPos.z + (chunkunit_t)z
|
||||
};
|
||||
errorChain(mapChunkLoad(chunk));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MAP.loadPosition = newLoadPos;
|
||||
MAP.chunkPosition = newPos;
|
||||
mapRebuildChunkOrder();
|
||||
errorOk();
|
||||
@@ -131,13 +97,16 @@ errorret_t mapUpdate() {
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
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++) {
|
||||
if(chunk->entities[i] == 0xFF) continue;
|
||||
entity_t *entity = &ENTITIES[chunk->entities[i]];
|
||||
@@ -157,9 +126,10 @@ void mapChunkUnload(chunk_t *chunk) {
|
||||
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++) {
|
||||
if(chunk->modelEntries[m] == NULL) continue;
|
||||
assetUnlockEntry(chunk->modelEntries[m]);
|
||||
chunk->modelEntries[m] = NULL;
|
||||
}
|
||||
chunk->meshCount = 0;
|
||||
@@ -168,6 +138,9 @@ void mapChunkUnload(chunk_t *chunk) {
|
||||
errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||
|
||||
mapChunkLoadQueueRemove(chunk);
|
||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
||||
|
||||
if(chunk->dcfEntry != NULL) {
|
||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
||||
@@ -195,12 +168,65 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
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);
|
||||
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->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() {
|
||||
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];
|
||||
const chunkpos_t rel = {
|
||||
chunk->position.x - MAP.chunkPosition.x,
|
||||
@@ -331,17 +357,23 @@ void mapRebuildChunkOrder() {
|
||||
void mapChunkLoadError(void *params, void *user) {
|
||||
assertNotNull(params, "mapChunkLoadError: params cannot be NULL");
|
||||
assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
chunk_t *chunk = (chunk_t *)user;
|
||||
if(chunk->dcfEntry != (assetentry_t *)params) return;
|
||||
if(chunk->dcfEntry != entry) return;
|
||||
consolePrint(
|
||||
"Chunk load error: %d %d %d",
|
||||
(int32_t)chunk->position.x,
|
||||
(int32_t)chunk->position.y,
|
||||
(int32_t)chunk->position.z
|
||||
);
|
||||
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&entry->onError, mapChunkLoadError);
|
||||
assetUnlockEntry(chunk->dcfEntry);
|
||||
chunk->dcfEntry = NULL;
|
||||
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
||||
|
||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
||||
mapChunkLoadNext();
|
||||
}
|
||||
|
||||
void mapChunkLoaded(void *params, void *user) {
|
||||
@@ -385,10 +417,22 @@ void mapChunkLoaded(void *params, void *user) {
|
||||
vec3 pos;
|
||||
glm_vec3_add(wpf, scaledOffset, 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];
|
||||
entry->data.chunk.modelEntries[m] = NULL;
|
||||
}
|
||||
assetUnlockEntry(chunk->dcfEntry);
|
||||
chunk->dcfEntry = NULL;
|
||||
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
|
||||
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;
|
||||
|
||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
||||
mapChunkLoadNext();
|
||||
}
|
||||
|
||||
@@ -15,10 +15,15 @@
|
||||
typedef struct map_s {
|
||||
bool_t loaded;
|
||||
|
||||
chunk_t chunks[MAP_LOADED_CHUNK_COUNT];
|
||||
chunk_t chunks[MAP_CHUNK_COUNT];
|
||||
chunk_t *chunkOrder[MAP_CHUNK_COUNT];
|
||||
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;
|
||||
|
||||
extern map_t MAP;
|
||||
@@ -74,6 +79,21 @@ void mapChunkUnload(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
|
||||
* chunk tiles with TILE_SHAPE_GROUND as a fallback.
|
||||
|
||||
@@ -73,7 +73,7 @@ bool_t mapAreaIsChunkOverlappingOrInside(
|
||||
bool_t mapAreaCanUnload(const maparea_t *area) {
|
||||
assertNotNull(area, "Map area pointer cannot be NULL");
|
||||
|
||||
for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) {
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,16 +30,6 @@
|
||||
#define MAP_CHUNK_DEPTH 4
|
||||
#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
|
||||
|
||||
typedef int16_t worldunit_t;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "rpg/cutscene/scene/testcutscene.h"
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "rpg/battle/party.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
#include "time/time.h"
|
||||
#include "rpgcamera.h"
|
||||
#include "util/memory.h"
|
||||
@@ -22,6 +23,8 @@
|
||||
#include "assert/assert.h"
|
||||
#include "console/console.h"
|
||||
|
||||
#include "ui/rpg/uiemoji.h"
|
||||
|
||||
void rpgTestAreaCallback(entity_t *entity, const uint8_t trigger) {
|
||||
consolePrint("rpgTestAreaCallback: trigger=%u", trigger);
|
||||
}
|
||||
@@ -59,6 +62,11 @@ errorret_t rpgInit(void) {
|
||||
entityItemSet(itemEnt, ITEM_ID_POTION, 1);
|
||||
entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 });
|
||||
|
||||
// TEST: Give the player a starting assortment of items.
|
||||
backpackAdd(ITEM_ID_POTION, 5);
|
||||
backpackAdd(ITEM_ID_POTATO, 3);
|
||||
backpackAdd(ITEM_ID_APPLE, 8);
|
||||
|
||||
// TEST: Create a test map area.
|
||||
uint8_t areaIndex = mapAreaAdd(
|
||||
(worldpos_t){ 11, 3, 0 },
|
||||
|
||||
@@ -7,12 +7,18 @@
|
||||
|
||||
#include "rpgcamera.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/random.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "assert/assert.h"
|
||||
#include "time/time.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;
|
||||
|
||||
void rpgCameraInit(void) {
|
||||
@@ -20,6 +26,13 @@ void rpgCameraInit(void) {
|
||||
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) {
|
||||
switch(RPG_CAMERA.mode) {
|
||||
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) {
|
||||
if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) {
|
||||
RPG_CAMERA.shakeTime += TIME.delta;
|
||||
}
|
||||
|
||||
if(!mapIsLoaded()) errorOk();
|
||||
|
||||
vec3 pos;
|
||||
|
||||
@@ -30,6 +30,10 @@ typedef struct {
|
||||
mat4 eye;
|
||||
mat4 projection;
|
||||
bool_t projectionDirty;
|
||||
|
||||
float_t shakeAmount;
|
||||
float_t shakeDuration;
|
||||
float_t shakeTime;
|
||||
} rpgcamera_t;
|
||||
|
||||
extern rpgcamera_t RPG_CAMERA;
|
||||
@@ -60,3 +64,31 @@ errorret_t rpgCameraUpdate(void);
|
||||
* is recomputed every call.
|
||||
*/
|
||||
void rpgCameraUpdateProjection(void);
|
||||
|
||||
/**
|
||||
* Recomputes the camera eye/view matrix from the camera's current mode
|
||||
* and position, and stores it in RPG_CAMERA.eye. Unlike the projection
|
||||
* matrix this is never cached, since the camera position can change
|
||||
* every frame.
|
||||
*/
|
||||
void rpgCameraUpdateEye(void);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -11,3 +11,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(overworld)
|
||||
add_subdirectory(battle)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
scenebattle.c
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "scenebattle.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
errorret_t sceneBattleInit(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneBattleUpdate(scenedata_t *sceneData) {
|
||||
battleUpdate();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneBattleRender(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneBattleDispose(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "scene/scenebase.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} scenebattle_t;
|
||||
|
||||
/**
|
||||
* Initializes the battle scene. The battle itself (BATTLE global) is
|
||||
* expected to already be started, e.g. by a StartBattle cutscene item.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the init failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneBattleInit(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Updates the battle scene, ticking the battle simulation.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the update failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneBattleUpdate(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Renders the battle scene.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the render failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneBattleRender(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Disposes the battle scene.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the dispose failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneBattleDispose(scenedata_t *sceneData);
|
||||
@@ -24,6 +24,8 @@
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t sceneOverworldInit(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
errorOk();
|
||||
@@ -31,112 +33,159 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData) {
|
||||
|
||||
errorret_t sceneOverworldUpdate(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
|
||||
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldRender(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
sceneoverworld_t *overworld = &sceneData->overworld;
|
||||
sceneOverworldCullUpdate(overworld);
|
||||
|
||||
mat4 model, eye;
|
||||
|
||||
// Overworld camera
|
||||
errorChain(shaderBind(&SHADER_UNLIT));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
|
||||
));
|
||||
|
||||
// Model
|
||||
glm_mat4_identity(model);
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model));
|
||||
|
||||
// Camera projection
|
||||
rpgCameraUpdateProjection();
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, RPG_CAMERA.projection
|
||||
));
|
||||
|
||||
// Camera Eye
|
||||
float_t fov = glm_rad(RPG_CAMERA_FOV);
|
||||
float_t pixelsPerUnit = TILE_SIZE_PIXELS;
|
||||
float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit;
|
||||
float_t z = (worldH * 0.5f) / tanf(fov * 0.5f);
|
||||
vec3 worldPosVec;
|
||||
rpgCameraGetPosition(worldPosVec);
|
||||
float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS);
|
||||
rpgCameraUpdateEye();
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_VIEW, RPG_CAMERA.eye
|
||||
));
|
||||
|
||||
glm_vec3_add(worldPosVec, (vec3){ 0.5f, 0.5f, 0.5f }, worldPosVec);
|
||||
// Chunk tiles
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
errorChain(sceneOverworldDrawChunksBase(overworld));
|
||||
|
||||
glm_lookat(
|
||||
(vec3){
|
||||
worldPosVec[0],
|
||||
worldPosVec[1] + offset,
|
||||
worldPosVec[2] + z
|
||||
},
|
||||
worldPosVec,
|
||||
(vec3){ 0, 1, 0 }, // up
|
||||
eye
|
||||
);
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, eye));
|
||||
|
||||
// Base terrain meshes, drawn with normal depth testing.
|
||||
errorChain(sceneOverworldDrawChunksBase());
|
||||
|
||||
// Entities are drawn with depth testing fully disabled so sloped tiles
|
||||
// (ramps) never clip them; entity-vs-entity overlap falls back to
|
||||
// array draw order instead of true depth.
|
||||
// Entities
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
|
||||
// Entities
|
||||
{
|
||||
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
|
||||
entity_t *ent = &ENTITIES[i];
|
||||
if(ent->type == ENTITY_TYPE_NULL) continue;
|
||||
|
||||
spritebatchsprite_t sprite;
|
||||
glm_vec3_copy(ent->renderPosition, sprite.min);
|
||||
glm_vec3_add(sprite.min, (vec3){ 0, -0.05f, 0.05f }, sprite.min);// Stop Fight
|
||||
glm_vec3_copy(sprite.min, sprite.max);
|
||||
glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max);
|
||||
glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin);
|
||||
glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax);
|
||||
|
||||
color_t color;
|
||||
switch(ent->direction) {
|
||||
case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break;
|
||||
case ENTITY_DIR_EAST: color = COLOR_RED; break;
|
||||
case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break;
|
||||
case ENTITY_DIR_WEST: color = COLOR_BLUE; break;
|
||||
default: color = COLOR_CYAN; break;
|
||||
}
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = { .color = color, .texture = NULL }
|
||||
};
|
||||
spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material);
|
||||
spriteBatchFlush();
|
||||
}
|
||||
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
|
||||
entity_t *ent = &ENTITIES[i];
|
||||
if(ent->type == ENTITY_TYPE_NULL) continue;
|
||||
errorChain(sceneOverworldDrawEntity(overworld, ent));
|
||||
}
|
||||
|
||||
// Other chunk meshes (trees, buildings, etc), drawn last with normal
|
||||
// depth testing so they correctly occlude entities standing beneath
|
||||
// them.
|
||||
// Chunk props
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
errorChain(sceneOverworldDrawChunksProps());
|
||||
errorChain(sceneOverworldDrawChunksProps(overworld));
|
||||
|
||||
// Weather effects
|
||||
// errorChain(shaderSetMatrix(
|
||||
// &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj
|
||||
// ));
|
||||
// errorChain(shaderSetMatrix(
|
||||
// &SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView
|
||||
// ));
|
||||
// errorChain(displaySetState((displaystate_t){
|
||||
// .flags = DISPLAY_STATE_FLAG_BLEND
|
||||
// }));
|
||||
|
||||
// spritebatchsprite_t skyboxSprite;
|
||||
// memoryZero(&skyboxSprite, sizeof(spritebatchsprite_t));
|
||||
// skyboxSprite.max[0] = (float_t)(SCREEN.width / SCREEN.scaleUi);
|
||||
// skyboxSprite.max[1] = (float_t)(SCREEN.height / SCREEN.scaleUi);
|
||||
// shadermaterial_t skyboxMaterial = {
|
||||
// .unlit = { .color = color(0xFF, 0x00, 0x00, 0xFF/2), .texture = NULL }
|
||||
// };
|
||||
// errorChain(spriteBatchBuffer(&skyboxSprite, 1, &SHADER_UNLIT, skyboxMaterial));
|
||||
// errorChain(spriteBatchFlush());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldDrawChunksBase() {
|
||||
void sceneOverworldCullUpdate(sceneoverworld_t *overworld) {
|
||||
rpgCameraGetPosition(overworld->cullTarget);
|
||||
|
||||
const float_t worldH =
|
||||
(float_t)(SCREEN.height / SCREEN.scale3d) / TILE_SIZE_PIXELS;
|
||||
const float_t worldW = worldH * SCREEN.aspect;
|
||||
|
||||
overworld->cullHalfRangeX =
|
||||
(worldW * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_X;
|
||||
overworld->cullHalfRangeY =
|
||||
(worldH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y;
|
||||
}
|
||||
|
||||
bool_t sceneOverworldChunkShouldRender(
|
||||
const sceneoverworld_t *overworld,
|
||||
const chunk_t *chunk
|
||||
) {
|
||||
worldpos_t worldPos;
|
||||
chunkPosToWorldPos(&chunk->position, &worldPos);
|
||||
|
||||
const float_t chunkCenterX = (float_t)worldPos.x + (CHUNK_WIDTH * 0.5f);
|
||||
const float_t chunkCenterY = (float_t)worldPos.y + (CHUNK_HEIGHT * 0.5f);
|
||||
|
||||
if(fabsf(overworld->cullTarget[0] - chunkCenterX) >
|
||||
overworld->cullHalfRangeX) return false;
|
||||
if(fabsf(overworld->cullTarget[1] - chunkCenterY) >
|
||||
overworld->cullHalfRangeY) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t sceneOverworldEntityShouldRender(
|
||||
const sceneoverworld_t *overworld,
|
||||
const entity_t *ent
|
||||
) {
|
||||
if(fabsf(overworld->cullTarget[0] - ent->renderPosition[0]) >
|
||||
overworld->cullHalfRangeX) return false;
|
||||
if(fabsf(overworld->cullTarget[1] - ent->renderPosition[1]) >
|
||||
overworld->cullHalfRangeY) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldDrawEntity(
|
||||
const sceneoverworld_t *overworld,
|
||||
entity_t *ent
|
||||
) {
|
||||
if(!sceneOverworldEntityShouldRender(overworld, ent)) errorOk();
|
||||
|
||||
spritebatchsprite_t sprite;
|
||||
glm_vec3_copy(ent->renderPosition, sprite.min);
|
||||
glm_vec3_add(sprite.min, (vec3){ 0, -0.05f, 0.05f }, sprite.min);// Stop Fight
|
||||
glm_vec3_copy(sprite.min, sprite.max);
|
||||
glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max);
|
||||
glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin);
|
||||
glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax);
|
||||
|
||||
color_t color;
|
||||
switch(ent->direction) {
|
||||
case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break;
|
||||
case ENTITY_DIR_EAST: color = COLOR_RED; break;
|
||||
case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break;
|
||||
case ENTITY_DIR_WEST: color = COLOR_BLUE; break;
|
||||
default: color = COLOR_CYAN; break;
|
||||
}
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = { .color = color, .texture = NULL }
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = MAP.chunkOrder[i];
|
||||
if(chunk == NULL) continue;
|
||||
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
|
||||
if(chunk->meshCount == 0) continue;
|
||||
if(chunk->modelEntries[0] == NULL) continue;
|
||||
if(chunk->modelEntries[0]->state != ASSET_ENTRY_STATE_LOADED) continue;
|
||||
@@ -169,10 +218,11 @@ errorret_t sceneOverworldDrawChunksBase() {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldDrawChunksProps() {
|
||||
errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) {
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = MAP.chunkOrder[i];
|
||||
if(chunk == NULL) continue;
|
||||
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
|
||||
|
||||
for(uint8_t m = 1; m < chunk->meshCount; m++) {
|
||||
if(chunk->modelEntries[m] == NULL) continue;
|
||||
|
||||
@@ -7,11 +7,35 @@
|
||||
|
||||
#pragma once
|
||||
#include "scene/scenebase.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
// Cached per-frame chunk culling bounds, refreshed by
|
||||
// sceneOverworldCullUpdate() so sceneOverworldChunkShouldRender()
|
||||
// does not need to recompute them for every chunk.
|
||||
vec3 cullTarget;
|
||||
float_t cullHalfRangeX;
|
||||
float_t cullHalfRangeY;
|
||||
} sceneoverworld_t;
|
||||
|
||||
// Extra world-space margin, in tiles, added on top of the rough
|
||||
// screen-size skin below - cheap insurance against the approximation
|
||||
// culling a chunk that is actually still just in view. Y gets its own
|
||||
// value since the camera's tilt makes the vertical check less exact.
|
||||
#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_X 8.0f
|
||||
#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y 4.0f
|
||||
|
||||
// The camera is tilted between the ground-depth (Y) and height (Z)
|
||||
// axes, so a chunk's on-screen vertical extent also depends on its Z
|
||||
// layers. Rather than projecting every corner, fold that into a
|
||||
// generous skin margin on the depth check.
|
||||
#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_X \
|
||||
((CHUNK_WIDTH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_PADDING_X)
|
||||
#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y \
|
||||
((CHUNK_HEIGHT * 0.5f) + (CHUNK_DEPTH * WORLD_LAYER_HEIGHT) + \
|
||||
SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y)
|
||||
|
||||
/**
|
||||
* Initialises the overworld scene.
|
||||
*
|
||||
@@ -28,22 +52,85 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData);
|
||||
*/
|
||||
errorret_t sceneOverworldUpdate(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Refreshes the chunk culling bounds cached on the overworld scene data,
|
||||
* from the current camera position and screen dimensions. Must be
|
||||
* called once per frame before sceneOverworldChunkShouldRender().
|
||||
*
|
||||
* @param overworld The overworld scene data to update.
|
||||
*/
|
||||
void sceneOverworldCullUpdate(sceneoverworld_t *overworld);
|
||||
|
||||
/**
|
||||
* Roughly checks whether a chunk falls within the visible screen area,
|
||||
* based on chunk size and screen dimensions. Cheap approximation, not
|
||||
* an exact frustum check - includes a generous skin margin to account
|
||||
* for the camera's angle tilting height/depth onto the screen's
|
||||
* vertical axis, so it may pass some chunks that are actually just out
|
||||
* of view but will never wrongly cull one that is visible.
|
||||
*
|
||||
* @param overworld The overworld scene data holding the cached culling
|
||||
* bounds, as refreshed by sceneOverworldCullUpdate().
|
||||
* @param chunk The chunk to check.
|
||||
* @return true if the chunk should be rendered, false otherwise.
|
||||
*/
|
||||
bool_t sceneOverworldChunkShouldRender(
|
||||
const sceneoverworld_t *overworld,
|
||||
const chunk_t *chunk
|
||||
);
|
||||
|
||||
/**
|
||||
* Draws the base (tile) mesh of every loaded chunk, with normal depth
|
||||
* testing. Must be called before entities are rendered.
|
||||
*
|
||||
* @param overworld The overworld scene data holding the cached culling
|
||||
* bounds, as refreshed by sceneOverworldCullUpdate().
|
||||
* @return An error if drawing failed, or errorOk() on success.
|
||||
*/
|
||||
errorret_t sceneOverworldDrawChunksBase();
|
||||
errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld);
|
||||
|
||||
/**
|
||||
* Draws every loaded chunk's additional meshes (trees, buildings, etc),
|
||||
* with normal depth testing so they correctly occlude entities standing
|
||||
* beneath them. Must be called after entities are rendered.
|
||||
*
|
||||
* @param overworld The overworld scene data holding the cached culling
|
||||
* bounds, as refreshed by sceneOverworldCullUpdate().
|
||||
* @return An error if drawing failed, or errorOk() on success.
|
||||
*/
|
||||
errorret_t sceneOverworldDrawChunksProps();
|
||||
errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld);
|
||||
|
||||
/**
|
||||
* Roughly checks whether an entity falls within the visible screen
|
||||
* area. Reuses the same cached chunk culling bounds, since an entity
|
||||
* is just a point within that same space - deliberately cheap, a
|
||||
* couple of subtractions and comparisons, since it runs per entity
|
||||
* every frame and it is not worth spending more math than that to
|
||||
* avoid drawing one that is slightly off-screen.
|
||||
*
|
||||
* @param overworld The overworld scene data holding the cached culling
|
||||
* bounds, as refreshed by sceneOverworldCullUpdate().
|
||||
* @param ent The entity to check.
|
||||
* @return true if the entity should be rendered, false otherwise.
|
||||
*/
|
||||
bool_t sceneOverworldEntityShouldRender(
|
||||
const sceneoverworld_t *overworld,
|
||||
const entity_t *ent
|
||||
);
|
||||
|
||||
/**
|
||||
* Draws a single entity as a sprite, colored by its facing direction.
|
||||
* Skips drawing (without error) if the entity should not render.
|
||||
*
|
||||
* @param overworld The overworld scene data holding the cached culling
|
||||
* bounds, as refreshed by sceneOverworldCullUpdate().
|
||||
* @param ent The entity to draw.
|
||||
* @return An error if drawing failed, or errorOk() on success.
|
||||
*/
|
||||
errorret_t sceneOverworldDrawEntity(
|
||||
const sceneoverworld_t *overworld,
|
||||
entity_t *ent
|
||||
);
|
||||
|
||||
/**
|
||||
* Renders the overworld scene.
|
||||
|
||||
+26
-19
@@ -58,6 +58,23 @@ errorret_t sceneUpdate(void) {
|
||||
}
|
||||
|
||||
errorret_t sceneRender(void) {
|
||||
// Setup screen matrices for 3D rendering.
|
||||
glm_mat4_identity(SCENE.screenIdentity);
|
||||
|
||||
glm_ortho(
|
||||
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi),
|
||||
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f,
|
||||
0.1f, 100.0f,
|
||||
SCENE.screenProj
|
||||
);
|
||||
|
||||
glm_lookat(
|
||||
(vec3){ 0.0f, 0.0f, 1.0f },
|
||||
(vec3){ 0.0f, 0.0f, 0.0f },
|
||||
(vec3){ 0.0f, 1.0f, 0.0f },
|
||||
SCENE.screenView
|
||||
);
|
||||
|
||||
// Scene rendering
|
||||
if(
|
||||
SCENE.current != SCENE_TYPE_NULL &&
|
||||
@@ -67,26 +84,16 @@ errorret_t sceneRender(void) {
|
||||
}
|
||||
|
||||
// UI Rendering
|
||||
mat4 proj, view, ident;
|
||||
glm_mat4_identity(ident);
|
||||
errorChain(shaderBind(&SHADER_UNLIT));
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, ident));
|
||||
|
||||
glm_ortho(
|
||||
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi),
|
||||
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f,
|
||||
0.1f, 100.0f,
|
||||
proj
|
||||
);
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj));
|
||||
|
||||
glm_lookat(
|
||||
(vec3){ 0.0f, 0.0f, 1.0f },
|
||||
(vec3){ 0.0f, 0.0f, 0.0f },
|
||||
(vec3){ 0.0f, 1.0f, 0.0f },
|
||||
view
|
||||
);
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
|
||||
));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj
|
||||
));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView
|
||||
));
|
||||
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_BLEND
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "scenetype.h"
|
||||
|
||||
typedef struct {
|
||||
scenetype_t current;
|
||||
scenetype_t next;
|
||||
scenedata_t data;
|
||||
mat4 screenProj;
|
||||
mat4 screenView;
|
||||
mat4 screenIdentity;
|
||||
} scene_t;
|
||||
|
||||
extern scene_t SCENE;
|
||||
|
||||
@@ -16,5 +16,12 @@ scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
|
||||
.render = sceneOverworldRender,
|
||||
.dispose = sceneOverworldDispose
|
||||
},
|
||||
|
||||
[SCENE_TYPE_BATTLE] = {
|
||||
.init = sceneBattleInit,
|
||||
.update = sceneBattleUpdate,
|
||||
.render = sceneBattleRender,
|
||||
.dispose = sceneBattleDispose
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
#pragma once
|
||||
#include "scene/scenebase.h"
|
||||
#include "scene/overworld/sceneoverworld.h"
|
||||
#include "scene/battle/scenebattle.h"
|
||||
|
||||
typedef union scenedata_u {
|
||||
sceneoverworld_t overworld;
|
||||
scenebattle_t battle;
|
||||
} scenedata_t;
|
||||
|
||||
typedef errorret_t (*scenecallback_t)(scenedata_t *);
|
||||
@@ -26,6 +28,7 @@ typedef enum {
|
||||
SCENE_TYPE_NULL,
|
||||
|
||||
SCENE_TYPE_OVERWORLD,
|
||||
SCENE_TYPE_BATTLE,
|
||||
|
||||
SCENE_TYPE_COUNT
|
||||
} scenetype_t;
|
||||
|
||||
@@ -11,3 +11,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
|
||||
add_subdirectory(game)
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(battle)
|
||||
add_subdirectory(backpack)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uibackpack.c
|
||||
)
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uibackpack.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
uibackpack_t UI_BACKPACK;
|
||||
|
||||
void uiBackpackTabChanged(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
const itemtype_t type = (itemtype_t)(index + 1);
|
||||
const inventory_t *inventory = backpackGetInventory(type);
|
||||
|
||||
errorCatch(uiItemListSetItemStacks(
|
||||
&UI_BACKPACK.itemList, inventory->storage, inventory->storageSize
|
||||
));
|
||||
}
|
||||
|
||||
void uiBackpackTabSelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
uiItemListOpen(&UI_BACKPACK.itemList);
|
||||
}
|
||||
|
||||
errorret_t uiBackpackInit(void) {
|
||||
memoryZero(&UI_BACKPACK, sizeof(uibackpack_t));
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs,
|
||||
uiBackpackTabSelected, NULL, uiBackpackTabChanged
|
||||
);
|
||||
for(uint8_t i = 0; i < UI_BACKPACK_TAB_COUNT; i++) {
|
||||
stringFormat(
|
||||
UI_BACKPACK.tabLabels[i], UI_BACKPACK_TAB_LABEL_MAX - 1,
|
||||
"Category %u", i + 1
|
||||
);
|
||||
MENU_TAB(UI_BACKPACK.tabLabels[i]);
|
||||
}
|
||||
MENU_END(UI_BACKPACK.tabs, menuIndex);
|
||||
|
||||
uiItemListInit(
|
||||
&UI_BACKPACK.itemList,
|
||||
UI_BACKPACK_ITEM_LIST_COLUMNS, UI_BACKPACK_ITEM_LIST_ROWS, 1,
|
||||
NULL, NULL, NULL, NULL
|
||||
);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiBackpackDraw(void) {
|
||||
if(!uiMenuIsActive(&UI_BACKPACK.tabsMenu)) errorOk();
|
||||
|
||||
const float_t width = SCREEN.width;
|
||||
const float_t height = SCREEN.height;
|
||||
const float_t x = (float_t)SCREEN.scanX +
|
||||
((float_t)SCREEN.scanWidth - width) * 0.5f;
|
||||
const float_t y = (float_t)SCREEN.scanY +
|
||||
((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
|
||||
const float_t contentX = x + UI_FRAME_START_X;
|
||||
const float_t contentY = y + UI_FRAME_START_Y;
|
||||
const float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
||||
const float_t contentHeight = height - (UI_FRAME_START_Y * 2);
|
||||
|
||||
errorChain(uiMenuDraw(
|
||||
&UI_BACKPACK.tabsMenu,
|
||||
contentX,
|
||||
contentY,
|
||||
contentWidth,
|
||||
contentHeight
|
||||
));
|
||||
|
||||
const float_t tabsRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
const float_t listY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y;
|
||||
|
||||
errorChain(
|
||||
uiItemListDraw(&UI_BACKPACK.itemList, contentX, listY, contentWidth)
|
||||
);
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t uiBackpackIsOpen(void) {
|
||||
return uiMenuIsActive(&UI_BACKPACK.tabsMenu);
|
||||
}
|
||||
|
||||
void uiBackpackOpen(void) {
|
||||
uiMenuOpen(&UI_BACKPACK.tabsMenu);
|
||||
}
|
||||
|
||||
void uiBackpackClose(void) {
|
||||
uiMenuClose(&UI_BACKPACK.tabsMenu);
|
||||
}
|
||||
|
||||
errorret_t uiBackpackDispose(void) {
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uimenu.h"
|
||||
#include "ui/widget/uiitemlist.h"
|
||||
#include "rpg/item/item.h"
|
||||
|
||||
#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_COUNT - 1)
|
||||
#define UI_BACKPACK_TAB_LABEL_MAX 32
|
||||
#define UI_BACKPACK_ITEM_LIST_COLUMNS 4
|
||||
#define UI_BACKPACK_ITEM_LIST_ROWS 5
|
||||
|
||||
typedef struct {
|
||||
uimenu_t tabsMenu;
|
||||
uimenuitem_t tabs[UI_BACKPACK_TAB_COUNT];
|
||||
char_t tabLabels[UI_BACKPACK_TAB_COUNT][UI_BACKPACK_TAB_LABEL_MAX];
|
||||
|
||||
uiitemlist_t itemList;
|
||||
} uibackpack_t;
|
||||
|
||||
extern uibackpack_t UI_BACKPACK;
|
||||
|
||||
/**
|
||||
* Initializes the backpack panel and its item type tabs.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiBackpackInit(void);
|
||||
|
||||
/**
|
||||
* Draws the backpack panel. No-op when not visible.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiBackpackDraw(void);
|
||||
|
||||
/**
|
||||
* Returns true when the backpack panel is currently open.
|
||||
*
|
||||
* @returns True if open.
|
||||
*/
|
||||
bool_t uiBackpackIsOpen(void);
|
||||
|
||||
/**
|
||||
* Opens the backpack panel. No-op when already open.
|
||||
*/
|
||||
void uiBackpackOpen(void);
|
||||
|
||||
/**
|
||||
* Closes the backpack panel. No-op when already closed.
|
||||
*/
|
||||
void uiBackpackClose(void);
|
||||
|
||||
/**
|
||||
* Disposes of the backpack panel.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiBackpackDispose(void);
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uibattlemenu.c
|
||||
)
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uibattlemenu.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/screen/screen.h"
|
||||
|
||||
uibattlemenu_t UI_BATTLE_MENU;
|
||||
|
||||
void uiBattleMenuTargetSelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
battlePlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]);
|
||||
uiMenuClose(&UI_BATTLE_MENU.targetMenu);
|
||||
uiMenuClose(&UI_BATTLE_MENU.actionMenu);
|
||||
}
|
||||
|
||||
void uiBattleMenuOpenTargets(void) {
|
||||
battlefighter_t *current = battleGetCurrentFighter();
|
||||
if(current == NULL) return;
|
||||
|
||||
const battlefighterteam_t enemyTeam =
|
||||
current->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_BATTLE_MENU.targetMenu, UI_BATTLE_MENU.targetItems,
|
||||
uiBattleMenuTargetSelected, NULL, NULL
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
battlefighter_t *candidate = &BATTLE.fighters[i];
|
||||
if(candidate->team != enemyTeam) continue;
|
||||
if(!battleFighterIsAlive(candidate)) continue;
|
||||
|
||||
stringFormat(
|
||||
UI_BATTLE_MENU.targetLabels[menuIndex],
|
||||
UI_BATTLE_MENU_TARGET_LABEL_MAX - 1,
|
||||
"Enemy %u (%u/%u HP)",
|
||||
candidate->id, candidate->health, candidate->healthMax
|
||||
);
|
||||
UI_BATTLE_MENU.targetFighterIndex[menuIndex] = i;
|
||||
MENU_BUTTON(UI_BATTLE_MENU.targetLabels[menuIndex]);
|
||||
}
|
||||
|
||||
MENU_END(UI_BATTLE_MENU.targetItems, 1);
|
||||
|
||||
uiMenuOpen(&UI_BATTLE_MENU.targetMenu);
|
||||
}
|
||||
|
||||
void uiBattleMenuActionSelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
switch(index) {
|
||||
case UI_BATTLE_MENU_ACTION_INDEX_ATTACK:
|
||||
uiBattleMenuOpenTargets();
|
||||
break;
|
||||
|
||||
case UI_BATTLE_MENU_ACTION_INDEX_FLEE:
|
||||
battlePlayerFlee();
|
||||
uiMenuClose(&UI_BATTLE_MENU.actionMenu);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t uiBattleMenuInit(void) {
|
||||
memoryZero(&UI_BATTLE_MENU, sizeof(uibattlemenu_t));
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_BATTLE_MENU.actionMenu, UI_BATTLE_MENU.actionItems,
|
||||
uiBattleMenuActionSelected, NULL, NULL
|
||||
);
|
||||
MENU_BUTTON("Attack");
|
||||
MENU_BUTTON("Flee");
|
||||
MENU_END(UI_BATTLE_MENU.actionItems, 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiBattleMenuUpdate(void) {
|
||||
if(uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
|
||||
|
||||
battlefighter_t *current = battleGetCurrentFighter();
|
||||
if(current == NULL) errorOk();
|
||||
if(current->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) errorOk();
|
||||
|
||||
uiMenuOpen(&UI_BATTLE_MENU.actionMenu);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiBattleMenuDraw(void) {
|
||||
if(!uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
|
||||
|
||||
const float_t width = UI_BATTLE_MENU_WIDTH;
|
||||
const float_t height = UI_BATTLE_MENU_HEIGHT;
|
||||
const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width;
|
||||
const float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - height;
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
errorChain(uiMenuDraw(
|
||||
&UI_BATTLE_MENU.actionMenu,
|
||||
x + UI_FRAME_START_X,
|
||||
y + UI_FRAME_START_Y,
|
||||
width - (UI_FRAME_START_X * 2),
|
||||
height - (UI_FRAME_START_Y * 2)
|
||||
));
|
||||
|
||||
if(uiMenuIsActive(&UI_BATTLE_MENU.targetMenu)) {
|
||||
const float_t targetY = y - height;
|
||||
errorChain(uiFrameDraw(x, targetY, width, height));
|
||||
errorChain(uiMenuDraw(
|
||||
&UI_BATTLE_MENU.targetMenu,
|
||||
x + UI_FRAME_START_X,
|
||||
targetY + UI_FRAME_START_Y,
|
||||
width - (UI_FRAME_START_X * 2),
|
||||
height - (UI_FRAME_START_Y * 2)
|
||||
));
|
||||
}
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uimenu.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
#define UI_BATTLE_MENU_ACTION_ITEM_COUNT 2
|
||||
#define UI_BATTLE_MENU_ACTION_INDEX_ATTACK 0
|
||||
#define UI_BATTLE_MENU_ACTION_INDEX_FLEE 1
|
||||
|
||||
#define UI_BATTLE_MENU_TARGET_ITEM_COUNT BATTLE_FIGHTER_COUNT_MAX
|
||||
#define UI_BATTLE_MENU_TARGET_LABEL_MAX 32
|
||||
|
||||
#define UI_BATTLE_MENU_WIDTH 160.0f
|
||||
#define UI_BATTLE_MENU_HEIGHT 96.0f
|
||||
|
||||
typedef struct {
|
||||
uimenu_t actionMenu;
|
||||
uimenuitem_t actionItems[UI_BATTLE_MENU_ACTION_ITEM_COUNT];
|
||||
|
||||
uimenu_t targetMenu;
|
||||
uimenuitem_t targetItems[UI_BATTLE_MENU_TARGET_ITEM_COUNT];
|
||||
uint8_t targetFighterIndex[UI_BATTLE_MENU_TARGET_ITEM_COUNT];
|
||||
char_t targetLabels[UI_BATTLE_MENU_TARGET_ITEM_COUNT]
|
||||
[UI_BATTLE_MENU_TARGET_LABEL_MAX];
|
||||
} uibattlemenu_t;
|
||||
|
||||
extern uibattlemenu_t UI_BATTLE_MENU;
|
||||
|
||||
/**
|
||||
* Initializes the battle action/target menus.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiBattleMenuInit(void);
|
||||
|
||||
/**
|
||||
* Updates the battle menu: opens the action menu whenever it becomes a
|
||||
* player-controlled fighter's turn.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiBattleMenuUpdate(void);
|
||||
|
||||
/**
|
||||
* Draws the battle action menu, and the target menu above it when
|
||||
* open. No-op when the action menu isn't active.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiBattleMenuDraw(void);
|
||||
@@ -8,13 +8,17 @@
|
||||
#include "uigamemenu.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "ui/frame/settings/uisettings.h"
|
||||
#include "ui/frame/backpack/uibackpack.h"
|
||||
#include "util/memory.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "assert/assert.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
|
||||
#define UI_GAME_MENU_INDEX_CHARACTERS 0
|
||||
#define UI_GAME_MENU_INDEX_SETTINGS 1
|
||||
#define UI_GAME_MENU_INDEX_ITEMS 1
|
||||
#define UI_GAME_MENU_INDEX_SETTINGS 2
|
||||
|
||||
uigamemenu_t UI_GAME_MENU;
|
||||
|
||||
@@ -23,17 +27,41 @@ void uiGameMenuSelected(
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen();
|
||||
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
|
||||
}
|
||||
|
||||
errorret_t uiGameMenuInit(void) {
|
||||
memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t));
|
||||
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
"ui.game_menu.characters",
|
||||
0,
|
||||
UI_GAME_MENU.charactersLabel,
|
||||
UI_GAME_MENU_LABEL_MAX
|
||||
));
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
"ui.game_menu.items",
|
||||
0,
|
||||
UI_GAME_MENU.itemsLabel,
|
||||
UI_GAME_MENU_LABEL_MAX
|
||||
));
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
"ui.game_menu.settings",
|
||||
0,
|
||||
UI_GAME_MENU.settingsLabel,
|
||||
UI_GAME_MENU_LABEL_MAX
|
||||
));
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL
|
||||
);
|
||||
MENU_BUTTON("Characters");
|
||||
MENU_BUTTON("Settings");
|
||||
MENU_BUTTON(UI_GAME_MENU.charactersLabel);
|
||||
MENU_BUTTON(UI_GAME_MENU.itemsLabel);
|
||||
MENU_BUTTON(UI_GAME_MENU.settingsLabel);
|
||||
|
||||
MENU_END(UI_GAME_MENU.items, 1);
|
||||
|
||||
|
||||
@@ -9,12 +9,16 @@
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uimenu.h"
|
||||
|
||||
#define UI_GAME_MENU_ITEM_COUNT 2
|
||||
#define UI_GAME_MENU_ITEM_COUNT 3
|
||||
#define UI_GAME_MENU_WIDTH 150.0f
|
||||
#define UI_GAME_MENU_LABEL_MAX 32
|
||||
|
||||
typedef struct {
|
||||
uimenu_t menu;
|
||||
uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT];
|
||||
char_t charactersLabel[UI_GAME_MENU_LABEL_MAX];
|
||||
char_t itemsLabel[UI_GAME_MENU_LABEL_MAX];
|
||||
char_t settingsLabel[UI_GAME_MENU_LABEL_MAX];
|
||||
} uigamemenu_t;
|
||||
|
||||
extern uigamemenu_t UI_GAME_MENU;
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
#define UI_FRAME_BORDER_WIDTH 6
|
||||
#define UI_FRAME_BORDER_HEIGHT 6
|
||||
#define UI_FRAME_PADDING_X 4
|
||||
#define UI_FRAME_PADDING_Y 4
|
||||
#define UI_FRAME_PADDING_X 2
|
||||
#define UI_FRAME_PADDING_Y 2
|
||||
#define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X)
|
||||
#define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y)
|
||||
#define UI_FRAME_TILE_WIDTH 1
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uitextbox.c
|
||||
uitextboxmain.c
|
||||
uiemoji.c
|
||||
)
|
||||
|
||||
|
||||
add_subdirectory(textbox)
|
||||
@@ -0,0 +1,12 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uitextbox.c
|
||||
uitextboxmain.c
|
||||
uitextboxmini.c
|
||||
uitextboxminilist.c
|
||||
)
|
||||
@@ -16,15 +16,29 @@
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
void uiTextboxInit(uitextbox_t *box) {
|
||||
void uiTextboxInit(
|
||||
uitextbox_t *box,
|
||||
char_t *text,
|
||||
const uint32_t maxLength,
|
||||
uitextboxline_t *lines,
|
||||
const uint32_t linesMax
|
||||
) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
assertNotNull(text, "Text buffer cannot be NULL");
|
||||
assertTrue(maxLength >= 1, "maxLength must be at least 1");
|
||||
assertNotNull(lines, "Lines buffer cannot be NULL");
|
||||
assertTrue(linesMax >= 1, "linesMax must be at least 1");
|
||||
memoryZero(box, sizeof(uitextbox_t));
|
||||
box->text = text;
|
||||
box->maxLength = maxLength;
|
||||
box->lines = lines;
|
||||
box->linesMax = linesMax;
|
||||
}
|
||||
|
||||
void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
stringCopy(box->text, text, UI_TEXTBOX_TEXT_MAX);
|
||||
stringCopy(box->text, text, box->maxLength);
|
||||
box->currentPage = 0;
|
||||
box->scroll = 0;
|
||||
box->layoutWidth = 0.0f;
|
||||
@@ -60,12 +74,12 @@ void uiTextboxBuildLayout(
|
||||
char_t *src = box->text;
|
||||
int32_t i = 0;
|
||||
|
||||
while(src[i] != '\0' && box->lineCount < UI_TEXTBOX_LINES_MAX) {
|
||||
while(src[i] != '\0' && box->lineCount < (int32_t)box->linesMax) {
|
||||
if(src[i] == '\t') {
|
||||
i++;
|
||||
int32_t rem = box->lineCount % box->linesPerPage;
|
||||
int32_t pad = rem > 0 ? box->linesPerPage - rem : 0;
|
||||
while(pad > 0 && box->lineCount < UI_TEXTBOX_LINES_MAX) {
|
||||
while(pad > 0 && box->lineCount < (int32_t)box->linesMax) {
|
||||
box->lines[box->lineCount].start = i;
|
||||
box->lines[box->lineCount].count = 0;
|
||||
box->lineCount++;
|
||||
@@ -196,18 +210,6 @@ errorret_t uiTextboxDraw(
|
||||
charsLeft -= visible;
|
||||
}
|
||||
|
||||
if(uiTextboxPageIsComplete(box)) {
|
||||
spritebatchsprite_t caret = textGetSprite(
|
||||
(vec2){
|
||||
contentX + contentW - fontW,
|
||||
contentY + contentH - fontH
|
||||
},
|
||||
'v',
|
||||
&FONT_DEFAULT
|
||||
);
|
||||
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
|
||||
}
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
@@ -8,8 +8,6 @@
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
#define UI_TEXTBOX_TEXT_MAX 1024
|
||||
#define UI_TEXTBOX_LINES_MAX 64
|
||||
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
|
||||
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
|
||||
#define UI_TEXTBOX_LINE_SPACING 0.0f
|
||||
@@ -20,9 +18,11 @@ typedef struct {
|
||||
} uitextboxline_t;
|
||||
|
||||
typedef struct {
|
||||
char_t text[UI_TEXTBOX_TEXT_MAX];
|
||||
char_t *text;
|
||||
uint32_t maxLength;
|
||||
|
||||
uitextboxline_t lines[UI_TEXTBOX_LINES_MAX];
|
||||
uitextboxline_t *lines;
|
||||
uint32_t linesMax;
|
||||
int32_t lineCount;
|
||||
int32_t charsPerLine;
|
||||
int32_t linesPerPage;
|
||||
@@ -37,11 +37,22 @@ typedef struct {
|
||||
} uitextbox_t;
|
||||
|
||||
/**
|
||||
* Initializes a textbox, zeroing all state.
|
||||
* Initializes a textbox, zeroing all state and binding it to caller-owned
|
||||
* text and line storage.
|
||||
*
|
||||
* @param box The textbox to initialize.
|
||||
* @param text Caller-owned buffer the textbox copies its text into.
|
||||
* @param maxLength Capacity of text, in characters.
|
||||
* @param lines Caller-owned buffer the textbox lays lines out into.
|
||||
* @param linesMax Capacity of lines, in entries.
|
||||
*/
|
||||
void uiTextboxInit(uitextbox_t *box);
|
||||
void uiTextboxInit(
|
||||
uitextbox_t *box,
|
||||
char_t *text,
|
||||
const uint32_t maxLength,
|
||||
uitextboxline_t *lines,
|
||||
const uint32_t linesMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Copies text into the textbox and marks layout as dirty.
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uitextboxmain.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
uitextboxmain_t UI_TEXTBOX_MAIN;
|
||||
static uifocusitem_t *focusItem = NULL;
|
||||
|
||||
errorret_t uiTextboxMainInit(void) {
|
||||
uiTextboxInit(
|
||||
&UI_TEXTBOX_MAIN.box,
|
||||
UI_TEXTBOX_MAIN.text, UI_TEXTBOX_MAIN_TEXT_MAX,
|
||||
UI_TEXTBOX_MAIN.lines, UI_TEXTBOX_MAIN_LINES_MAX
|
||||
);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiTextboxMainSetText(const char_t *text) {
|
||||
uiTextboxSetText(&UI_TEXTBOX_MAIN.box, text);
|
||||
if(focusItem != NULL) return;
|
||||
focusItem = uiFocusPush(
|
||||
1, 1,
|
||||
uiTextboxMainFocusSelected,
|
||||
NULL,
|
||||
uiTextboxMainFocusClosed,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMainUpdate(void) {
|
||||
if(focusItem == NULL) errorOk();
|
||||
return uiTextboxUpdate(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMainDraw(void) {
|
||||
if(focusItem == NULL) errorOk();
|
||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH +
|
||||
(float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING +
|
||||
2.0f * (float_t)UI_FRAME_START_Y;
|
||||
float_t w = (float_t)SCREEN.scanWidth;
|
||||
float_t x = (float_t)SCREEN.scanX;
|
||||
float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h;
|
||||
errorChain(uiTextboxDraw(&UI_TEXTBOX_MAIN.box, x, y, w, h));
|
||||
|
||||
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) errorOk();
|
||||
|
||||
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
||||
float_t contentX = x + (float_t)UI_FRAME_START_X;
|
||||
float_t contentY = y + (float_t)UI_FRAME_START_Y;
|
||||
float_t contentW = w - 2.0f * (float_t)UI_FRAME_START_X;
|
||||
float_t contentH = h - 2.0f * (float_t)UI_FRAME_START_Y;
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = COLOR_WHITE,
|
||||
.texture = FONT_DEFAULT.texture
|
||||
}
|
||||
};
|
||||
|
||||
spritebatchsprite_t caret = textGetSprite(
|
||||
(vec2){
|
||||
contentX + contentW - fontW,
|
||||
contentY + contentH - fontH
|
||||
},
|
||||
'v',
|
||||
&FONT_DEFAULT
|
||||
);
|
||||
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainPageIsComplete(void) {
|
||||
return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainHasNextPage(void) {
|
||||
return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
void uiTextboxMainNextPage(void) {
|
||||
uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainIsActive(void) {
|
||||
return focusItem != NULL;
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) {
|
||||
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) {
|
||||
UI_TEXTBOX_MAIN.box.scroll =
|
||||
uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN.box);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box)) {
|
||||
uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
return true;
|
||||
}
|
||||
|
||||
uiFocusPopItem(focusItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) {
|
||||
focusItem = NULL;
|
||||
return true;
|
||||
}
|
||||
@@ -6,12 +6,20 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "ui/rpg/uitextbox.h"
|
||||
#include "ui/rpg/textbox/uitextbox.h"
|
||||
#include "ui/focus/uifocusitem.h"
|
||||
|
||||
#define UI_TEXTBOX_MAIN_LINES 4
|
||||
#define UI_TEXTBOX_MAIN_TEXT_MAX 1024
|
||||
#define UI_TEXTBOX_MAIN_LINES_MAX 64
|
||||
|
||||
extern uitextbox_t UI_TEXTBOX_MAIN;
|
||||
typedef struct {
|
||||
uitextbox_t box;
|
||||
char_t text[UI_TEXTBOX_MAIN_TEXT_MAX];
|
||||
uitextboxline_t lines[UI_TEXTBOX_MAIN_LINES_MAX];
|
||||
} uitextboxmain_t;
|
||||
|
||||
extern uitextboxmain_t UI_TEXTBOX_MAIN;
|
||||
|
||||
/**
|
||||
* Initializes UI_TEXTBOX_MAIN.
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uitextboxmini.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "time/time.h"
|
||||
#include "rpg/rpgcamera.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
void uiTextboxMiniInit(uitextboxmini_t *mini) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
memoryZero(mini, sizeof(uitextboxmini_t));
|
||||
uiTextboxInit(
|
||||
&mini->box,
|
||||
mini->text, UI_TEXTBOX_MINI_TEXT_MAX,
|
||||
mini->lines, UI_TEXTBOX_MINI_LINES_MAX
|
||||
);
|
||||
}
|
||||
|
||||
void uiTextboxMiniShow(
|
||||
uitextboxmini_t *mini,
|
||||
const char_t *text,
|
||||
vec3 position,
|
||||
const float_t duration,
|
||||
uitextboxminiclosedcallback_t closed,
|
||||
void *user
|
||||
) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
uiTextboxSetText(&mini->box, text);
|
||||
glm_vec3_copy(position, mini->position);
|
||||
|
||||
int32_t textWidth, textHeight;
|
||||
textMeasure(text, &FONT_DEFAULT, &textWidth, &textHeight);
|
||||
float_t width = (float_t)textWidth + 2.0f * (float_t)UI_FRAME_START_X;
|
||||
mini->width = width < UI_TEXTBOX_MINI_WIDTH_MAX
|
||||
? width : UI_TEXTBOX_MINI_WIDTH_MAX;
|
||||
|
||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
float_t maxHeight = (float_t)UI_TEXTBOX_MINI_LINES_MAX * fontH +
|
||||
(float_t)(UI_TEXTBOX_MINI_LINES_MAX - 1) * UI_TEXTBOX_LINE_SPACING +
|
||||
2.0f * (float_t)UI_FRAME_START_Y;
|
||||
uiTextboxBuildLayout(
|
||||
&mini->box,
|
||||
mini->width - 2.0f * (float_t)UI_FRAME_START_X,
|
||||
maxHeight - 2.0f * (float_t)UI_FRAME_START_Y
|
||||
);
|
||||
int32_t lineCount = mini->box.lineCount > 0 ? mini->box.lineCount : 1;
|
||||
mini->height = (float_t)lineCount * fontH +
|
||||
(float_t)(lineCount - 1) * UI_TEXTBOX_LINE_SPACING +
|
||||
2.0f * (float_t)UI_FRAME_START_Y;
|
||||
|
||||
mini->active = true;
|
||||
mini->timer = duration;
|
||||
mini->closed = closed;
|
||||
mini->user = user;
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
if(!mini->active) errorOk();
|
||||
|
||||
errorChain(uiTextboxUpdate(&mini->box));
|
||||
|
||||
if(mini->timer != UI_TEXTBOX_MINI_DURATION_INFINITE) {
|
||||
mini->timer -= TIME.delta;
|
||||
if(mini->timer <= 0.0f) uiTextboxMiniClose(mini);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
if(!mini->active) errorOk();
|
||||
|
||||
vec2 screenPos;
|
||||
rpgCameraToScreen(mini->position, screenPos);
|
||||
|
||||
if(
|
||||
mini->clamp == UI_TEXTBOX_MINI_CLAMP_X ||
|
||||
mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH
|
||||
) {
|
||||
float_t minX = (float_t)SCREEN.scanX;
|
||||
float_t maxX = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - mini->width;
|
||||
if(screenPos[0] < minX) screenPos[0] = minX;
|
||||
if(screenPos[0] > maxX) screenPos[0] = maxX;
|
||||
}
|
||||
|
||||
if(
|
||||
mini->clamp == UI_TEXTBOX_MINI_CLAMP_Y ||
|
||||
mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH
|
||||
) {
|
||||
float_t minY = (float_t)SCREEN.scanY;
|
||||
float_t maxY = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - mini->height;
|
||||
if(screenPos[1] < minY) screenPos[1] = minY;
|
||||
if(screenPos[1] > maxY) screenPos[1] = maxY;
|
||||
}
|
||||
|
||||
return uiTextboxDraw(
|
||||
&mini->box, screenPos[0], screenPos[1], mini->width, mini->height
|
||||
);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
return mini->active;
|
||||
}
|
||||
|
||||
void uiTextboxMiniClose(uitextboxmini_t *mini) {
|
||||
assertNotNull(mini, "Mini textbox cannot be NULL");
|
||||
if(!mini->active) return;
|
||||
mini->active = false;
|
||||
if(mini->closed != NULL) mini->closed(mini);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "ui/rpg/textbox/uitextbox.h"
|
||||
|
||||
#define UI_TEXTBOX_MINI_TEXT_MAX 128
|
||||
#define UI_TEXTBOX_MINI_LINES_MAX 2
|
||||
#define UI_TEXTBOX_MINI_WIDTH_MAX 160.0f
|
||||
|
||||
// Magic duration value that keeps a mini textbox visible indefinitely
|
||||
// instead of counting down and auto-closing.
|
||||
#define UI_TEXTBOX_MINI_DURATION_INFINITE -1.0f
|
||||
|
||||
typedef struct uitextboxmini_s uitextboxmini_t;
|
||||
typedef void (*uitextboxminiclosedcallback_t)(const uitextboxmini_t *mini);
|
||||
|
||||
/**
|
||||
* Determines whether a mini textbox's screen position is clamped to stay
|
||||
* fully on screen. Defaults to UI_TEXTBOX_MINI_CLAMP_NONE.
|
||||
*/
|
||||
typedef enum {
|
||||
UI_TEXTBOX_MINI_CLAMP_NONE,
|
||||
UI_TEXTBOX_MINI_CLAMP_X,
|
||||
UI_TEXTBOX_MINI_CLAMP_Y,
|
||||
UI_TEXTBOX_MINI_CLAMP_BOTH
|
||||
} uitextboxminiclamp_t;
|
||||
|
||||
typedef struct uitextboxmini_s {
|
||||
uitextbox_t box;
|
||||
char_t text[UI_TEXTBOX_MINI_TEXT_MAX];
|
||||
uitextboxline_t lines[UI_TEXTBOX_MINI_LINES_MAX];
|
||||
|
||||
vec3 position;
|
||||
float_t width;
|
||||
float_t height;
|
||||
uitextboxminiclamp_t clamp;
|
||||
bool_t active;
|
||||
float_t timer;
|
||||
|
||||
uitextboxminiclosedcallback_t closed;
|
||||
void *user;
|
||||
} uitextboxmini_t;
|
||||
|
||||
/**
|
||||
* Initializes a mini textbox, zeroing all state and binding its internal
|
||||
* uitextbox_t to its own fixed-size text and line storage.
|
||||
*
|
||||
* @param mini The mini textbox to initialize.
|
||||
*/
|
||||
void uiTextboxMiniInit(uitextboxmini_t *mini);
|
||||
|
||||
/**
|
||||
* Shows a mini textbox with the given text at the given world position for
|
||||
* the given duration. Resets the typewriter scroll, measures the text to
|
||||
* size the box (width capped at UI_TEXTBOX_MINI_WIDTH_MAX, height derived
|
||||
* from the resulting wrapped line count), and starts the visibility timer.
|
||||
*
|
||||
* @param mini The mini textbox to show.
|
||||
* @param text Null-terminated source string.
|
||||
* @param position World-space position the box is anchored to on screen.
|
||||
* @param duration How long the mini textbox stays visible, in seconds, or
|
||||
* UI_TEXTBOX_MINI_DURATION_INFINITE to never auto-close.
|
||||
* @param closed Called once the timer elapses and the box closes. May be
|
||||
* NULL.
|
||||
* @param user Opaque pointer passed back through the closed callback.
|
||||
*/
|
||||
void uiTextboxMiniShow(
|
||||
uitextboxmini_t *mini,
|
||||
const char_t *text,
|
||||
vec3 position,
|
||||
const float_t duration,
|
||||
uitextboxminiclosedcallback_t closed,
|
||||
void *user
|
||||
);
|
||||
|
||||
/**
|
||||
* Advances the typewriter scroll and counts down the visibility timer.
|
||||
* Closes the mini textbox and fires its closed callback once the timer
|
||||
* elapses. Has no effect if not active or if the duration was set to
|
||||
* UI_TEXTBOX_MINI_DURATION_INFINITE.
|
||||
*
|
||||
* @param mini The mini textbox to update.
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini);
|
||||
|
||||
/**
|
||||
* Draws the mini textbox at its text-measured size, anchored on screen to
|
||||
* its world-space position via the RPG camera. Has no effect if not
|
||||
* active.
|
||||
*
|
||||
* @param mini The mini textbox to draw.
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini);
|
||||
|
||||
/**
|
||||
* Returns true when the mini textbox is currently visible.
|
||||
*
|
||||
* @param mini The mini textbox to query.
|
||||
* @returns True if active.
|
||||
*/
|
||||
bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini);
|
||||
|
||||
/**
|
||||
* Immediately closes the mini textbox and fires its closed callback.
|
||||
* Has no effect if not active.
|
||||
*
|
||||
* @param mini The mini textbox to close.
|
||||
*/
|
||||
void uiTextboxMiniClose(uitextboxmini_t *mini);
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uitextboxminilist.h"
|
||||
|
||||
uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT];
|
||||
uint8_t UI_TEXTBOX_MINI_LIST_NEXT;
|
||||
|
||||
errorret_t uiTextboxMiniListInit(void) {
|
||||
for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
|
||||
uiTextboxMiniInit(&UI_TEXTBOX_MINI_LIST[i]);
|
||||
}
|
||||
UI_TEXTBOX_MINI_LIST_NEXT = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t uiTextboxMiniListGetNext(void) {
|
||||
uint8_t index = UI_TEXTBOX_MINI_LIST_NEXT;
|
||||
UI_TEXTBOX_MINI_LIST_NEXT =
|
||||
(UI_TEXTBOX_MINI_LIST_NEXT + 1) % UI_TEXTBOX_MINI_LIST_COUNT;
|
||||
return index;
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMiniListUpdate(void) {
|
||||
for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
|
||||
errorChain(uiTextboxMiniUpdate(&UI_TEXTBOX_MINI_LIST[i]));
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMiniListDraw(void) {
|
||||
for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
|
||||
errorChain(uiTextboxMiniDraw(&UI_TEXTBOX_MINI_LIST[i]));
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "ui/rpg/textbox/uitextboxmini.h"
|
||||
|
||||
#define UI_TEXTBOX_MINI_LIST_COUNT 4
|
||||
|
||||
extern uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT];
|
||||
extern uint8_t UI_TEXTBOX_MINI_LIST_NEXT;
|
||||
|
||||
/**
|
||||
* Initializes all UI_TEXTBOX_MINI_LIST slots and resets
|
||||
* UI_TEXTBOX_MINI_LIST_NEXT.
|
||||
*
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMiniListInit(void);
|
||||
|
||||
/**
|
||||
* Returns the index of the next UI_TEXTBOX_MINI_LIST slot to use, cycling
|
||||
* through all slots in round-robin order via UI_TEXTBOX_MINI_LIST_NEXT.
|
||||
*
|
||||
* @returns The next slot index.
|
||||
*/
|
||||
uint8_t uiTextboxMiniListGetNext(void);
|
||||
|
||||
/**
|
||||
* Updates all UI_TEXTBOX_MINI_LIST slots.
|
||||
*
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMiniListUpdate(void);
|
||||
|
||||
/**
|
||||
* Draws all active UI_TEXTBOX_MINI_LIST slots.
|
||||
*
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMiniListDraw(void);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
@@ -18,8 +18,12 @@
|
||||
#include "ui/debug/uiconsole.h"
|
||||
#include "ui/frame/settings/uisettings.h"
|
||||
#include "ui/frame/game/uigamemenu.h"
|
||||
#include "ui/frame/battle/uibattlemenu.h"
|
||||
#include "ui/frame/backpack/uibackpack.h"
|
||||
#include "ui/frame/uiconfirm.h"
|
||||
#include "ui/rpg/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
#include "ui/rpg/uiemoji.h"
|
||||
|
||||
uielement_t UI_ELEMENTS[] = {
|
||||
{
|
||||
@@ -27,7 +31,6 @@ uielement_t UI_ELEMENTS[] = {
|
||||
.dispose = uiFrameDispose
|
||||
},
|
||||
|
||||
|
||||
// Fullbox under: above scene, below system UI.
|
||||
{
|
||||
.init = uiFullboxUnderInit,
|
||||
@@ -35,12 +38,33 @@ uielement_t UI_ELEMENTS[] = {
|
||||
.draw = uiFullboxUnderDraw
|
||||
},
|
||||
|
||||
// in world stuff
|
||||
{
|
||||
.init = uiEmojiInit,
|
||||
.update = uiEmojiUpdate,
|
||||
.draw = uiEmojiDraw,
|
||||
.dispose = uiEmojiDispose
|
||||
},
|
||||
|
||||
// Ingame menus
|
||||
{
|
||||
.init = uiGameMenuInit,
|
||||
.draw = uiGameMenuDraw,
|
||||
.dispose = uiGameMenuDispose
|
||||
},
|
||||
|
||||
{
|
||||
.init = uiBattleMenuInit,
|
||||
.update = uiBattleMenuUpdate,
|
||||
.draw = uiBattleMenuDraw
|
||||
},
|
||||
|
||||
{
|
||||
.init = uiBackpackInit,
|
||||
.draw = uiBackpackDraw,
|
||||
.dispose = uiBackpackDispose
|
||||
},
|
||||
|
||||
{
|
||||
.init = uiSettingsInit,
|
||||
.update = uiSettingsUpdate,
|
||||
@@ -48,6 +72,7 @@ uielement_t UI_ELEMENTS[] = {
|
||||
.dispose = uiSettingsDispose
|
||||
},
|
||||
|
||||
// Text stuffs
|
||||
{
|
||||
.init = uiConfirmInit,
|
||||
.draw = uiConfirmDraw,
|
||||
@@ -60,6 +85,12 @@ uielement_t UI_ELEMENTS[] = {
|
||||
.draw = uiTextboxMainDraw
|
||||
},
|
||||
|
||||
{
|
||||
.init = uiTextboxMiniListInit,
|
||||
.update = uiTextboxMiniListUpdate,
|
||||
.draw = uiTextboxMiniListDraw
|
||||
},
|
||||
|
||||
{
|
||||
.init = uiTransitionInit,
|
||||
.update = uiTransitionUpdate,
|
||||
|
||||
@@ -10,5 +10,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
uitab.c
|
||||
uislider.c
|
||||
uidropdown.c
|
||||
uiscrolling.c
|
||||
uiitem.c
|
||||
uiitemlist.c
|
||||
uimenu.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uiitem.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
|
||||
#define UI_ITEM_LABEL_MAX 48
|
||||
|
||||
errorret_t uiItemInit(
|
||||
uiitem_t *item,
|
||||
const itemid_t itemId,
|
||||
const uint8_t quantity
|
||||
) {
|
||||
assertNotNull(item, "Item cannot be NULL");
|
||||
memoryZero(item, sizeof(uiitem_t));
|
||||
item->item = itemId;
|
||||
item->quantity = quantity;
|
||||
|
||||
if(itemId == ITEM_ID_NULL) errorOk();
|
||||
|
||||
errorChain(itemGetName(itemId, item->nameLabel, UI_ITEM_NAME_LABEL_MAX));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
itemid_t uiItemGetItem(const uiitem_t *item) {
|
||||
assertNotNull(item, "Item cannot be NULL");
|
||||
return item->item;
|
||||
}
|
||||
|
||||
uint8_t uiItemGetQuantity(const uiitem_t *item) {
|
||||
assertNotNull(item, "Item cannot be NULL");
|
||||
return item->quantity;
|
||||
}
|
||||
|
||||
void uiItemSetQuantity(uiitem_t *item, const uint8_t quantity) {
|
||||
assertNotNull(item, "Item cannot be NULL");
|
||||
item->quantity = quantity;
|
||||
}
|
||||
|
||||
bool_t uiItemIsHighlighted(const uiitem_t *item) {
|
||||
assertNotNull(item, "Item cannot be NULL");
|
||||
return item->highlighted;
|
||||
}
|
||||
|
||||
void uiItemSetHighlighted(uiitem_t *item, const bool_t highlighted) {
|
||||
assertNotNull(item, "Item cannot be NULL");
|
||||
item->highlighted = highlighted;
|
||||
}
|
||||
|
||||
errorret_t uiItemDraw(
|
||||
const uiitem_t *item,
|
||||
const float_t x,
|
||||
const float_t y
|
||||
) {
|
||||
assertNotNull(item, "Item cannot be NULL");
|
||||
if(item->item == ITEM_ID_NULL) errorOk();
|
||||
|
||||
const color_t color = item->highlighted ? COLOR_RED : COLOR_WHITE;
|
||||
|
||||
char_t text[UI_ITEM_LABEL_MAX];
|
||||
stringFormat(
|
||||
text, UI_ITEM_LABEL_MAX - 1,
|
||||
"%s x%u", item->nameLabel, item->quantity
|
||||
);
|
||||
|
||||
errorChain(textDraw(x, y, text, color, &FONT_DEFAULT));
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "rpg/item/item.h"
|
||||
|
||||
#define UI_ITEM_NAME_LABEL_MAX 32
|
||||
|
||||
typedef struct {
|
||||
itemid_t item;
|
||||
uint8_t quantity;
|
||||
bool_t highlighted;
|
||||
char_t nameLabel[UI_ITEM_NAME_LABEL_MAX];
|
||||
} uiitem_t;
|
||||
|
||||
/**
|
||||
* Initializes an item widget, resolving the item's localized name.
|
||||
*
|
||||
* @param item The item widget to initialize.
|
||||
* @param itemId The item ID to display. ITEM_ID_NULL renders as an
|
||||
* empty slot.
|
||||
* @param quantity The stack quantity to display.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiItemInit(
|
||||
uiitem_t *item,
|
||||
const itemid_t itemId,
|
||||
const uint8_t quantity
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the item ID this widget displays.
|
||||
*
|
||||
* @param item The item widget to query.
|
||||
* @returns The item ID.
|
||||
*/
|
||||
itemid_t uiItemGetItem(const uiitem_t *item);
|
||||
|
||||
/**
|
||||
* Returns the stack quantity this widget displays.
|
||||
*
|
||||
* @param item The item widget to query.
|
||||
* @returns The quantity.
|
||||
*/
|
||||
uint8_t uiItemGetQuantity(const uiitem_t *item);
|
||||
|
||||
/**
|
||||
* Sets the stack quantity this widget displays.
|
||||
*
|
||||
* @param item The item widget to update.
|
||||
* @param quantity The new quantity.
|
||||
*/
|
||||
void uiItemSetQuantity(uiitem_t *item, const uint8_t quantity);
|
||||
|
||||
/**
|
||||
* Returns whether the item widget is highlighted.
|
||||
*
|
||||
* @param item The item widget to query.
|
||||
* @returns True if highlighted.
|
||||
*/
|
||||
bool_t uiItemIsHighlighted(const uiitem_t *item);
|
||||
|
||||
/**
|
||||
* Sets the highlighted state of the item widget.
|
||||
*
|
||||
* @param item The item widget to update.
|
||||
* @param highlighted The new highlighted state.
|
||||
*/
|
||||
void uiItemSetHighlighted(uiitem_t *item, const bool_t highlighted);
|
||||
|
||||
/**
|
||||
* Draws the item widget at the given screen position: item name and
|
||||
* quantity. No-op for an empty (ITEM_ID_NULL) slot.
|
||||
*
|
||||
* @param item The item widget to draw.
|
||||
* @param x Screen x position.
|
||||
* @param y Screen y position.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiItemDraw(
|
||||
const uiitem_t *item,
|
||||
const float_t x,
|
||||
const float_t y
|
||||
);
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uiitemlist.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "display/text/text.h"
|
||||
|
||||
void uiItemListInit(
|
||||
uiitemlist_t *list,
|
||||
const uint8_t columns,
|
||||
const uint8_t rows,
|
||||
const uint8_t itemColumns,
|
||||
uiitemlistselectedcallback_t selected,
|
||||
uiitemlistchangedcallback_t changed,
|
||||
uiitemlistclosedcallback_t closed,
|
||||
uiitemlistcolumncallback_t columnDraw
|
||||
) {
|
||||
assertNotNull(list, "Item list cannot be NULL");
|
||||
assertTrue(columns > 0, "Item list columns must be > 0");
|
||||
assertTrue(rows > 0, "Item list rows must be > 0");
|
||||
assertTrue(itemColumns > 0, "Item list itemColumns must be > 0");
|
||||
|
||||
memoryZero(list, sizeof(uiitemlist_t));
|
||||
list->columns = columns;
|
||||
list->rows = rows;
|
||||
list->itemColumns = itemColumns;
|
||||
list->selected = selected;
|
||||
list->changed = changed;
|
||||
list->closed = closed;
|
||||
list->columnDraw = columnDraw;
|
||||
|
||||
uiScrollingInit(&list->scroll);
|
||||
}
|
||||
|
||||
void uiItemListSetItems(
|
||||
uiitemlist_t *list,
|
||||
const uiitem_t *items,
|
||||
const uint8_t itemCount
|
||||
) {
|
||||
assertNotNull(list, "Item list cannot be NULL");
|
||||
assertTrue(
|
||||
itemCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list"
|
||||
);
|
||||
|
||||
memoryCopy(list->items, items, sizeof(uiitem_t) * itemCount);
|
||||
list->itemCount = itemCount;
|
||||
}
|
||||
|
||||
errorret_t uiItemListSetItemStacks(
|
||||
uiitemlist_t *list,
|
||||
const inventorystack_t *stacks,
|
||||
const uint8_t stackCount
|
||||
) {
|
||||
assertNotNull(list, "Item list cannot be NULL");
|
||||
assertTrue(
|
||||
stackCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list"
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < stackCount; i++) {
|
||||
errorChain(
|
||||
uiItemInit(&list->items[i], stacks[i].item, stacks[i].quantity)
|
||||
);
|
||||
}
|
||||
list->itemCount = stackCount;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiItemListOpen(uiitemlist_t *list) {
|
||||
assertNotNull(list, "Item list cannot be NULL");
|
||||
if(list->focusItem != NULL) return;
|
||||
|
||||
list->focusItem = uiFocusPush(
|
||||
list->columns, list->rows,
|
||||
uiItemListFocusSelected,
|
||||
uiItemListFocusChanged,
|
||||
uiItemListFocusClosed,
|
||||
NULL,
|
||||
list
|
||||
);
|
||||
}
|
||||
|
||||
void uiItemListClose(uiitemlist_t *list) {
|
||||
assertNotNull(list, "Item list cannot be NULL");
|
||||
if(list->focusItem == NULL) return;
|
||||
uiFocusPopItem(list->focusItem);
|
||||
list->focusItem = NULL;
|
||||
}
|
||||
|
||||
bool_t uiItemListIsActive(const uiitemlist_t *list) {
|
||||
assertNotNull(list, "Item list cannot be NULL");
|
||||
return list->focusItem != NULL;
|
||||
}
|
||||
|
||||
errorret_t uiItemListDraw(
|
||||
const uiitemlist_t *list,
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const float_t width
|
||||
) {
|
||||
assertNotNull(list, "Item list cannot be NULL");
|
||||
if(list->itemCount == 0) errorOk();
|
||||
|
||||
const float_t colStep = width / (float_t)list->columns;
|
||||
const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
const float_t itemColStep = colStep / (float_t)list->itemColumns;
|
||||
|
||||
const uint8_t visibleMax = list->columns * list->rows;
|
||||
const uint8_t drawCount =
|
||||
list->itemCount < visibleMax ? list->itemCount : visibleMax;
|
||||
|
||||
for(uint8_t i = 0; i < drawCount; i++) {
|
||||
const uint8_t col = i % list->columns;
|
||||
const uint8_t row = i / list->columns;
|
||||
|
||||
const float_t ix = x + (float_t)col * colStep;
|
||||
const float_t iy = y + (float_t)row * rowHeight;
|
||||
|
||||
errorChain(uiItemDraw(&list->items[i], ix, iy));
|
||||
|
||||
for(uint8_t c = 1; c < list->itemColumns; c++) {
|
||||
if(list->columnDraw == NULL) continue;
|
||||
errorChain(list->columnDraw(
|
||||
list, &list->items[i], c, ix + (float_t)c * itemColStep, iy
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t uiItemListFocusSelected(const uifocusitem_t *focusItem) {
|
||||
assertNotNull(focusItem, "Focus item cannot be NULL");
|
||||
assertNotNull(focusItem->user, "Focus item user cannot be NULL");
|
||||
uiitemlist_t *list = (uiitemlist_t *)focusItem->user;
|
||||
if(list->selected == NULL) return true;
|
||||
|
||||
const uint8_t index = focusItem->y * list->columns + focusItem->x;
|
||||
if(index >= list->itemCount) return true;
|
||||
|
||||
list->selected(list, index, &list->items[index]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t uiItemListFocusChanged(const uifocusitem_t *focusItem) {
|
||||
assertNotNull(focusItem, "Focus item cannot be NULL");
|
||||
assertNotNull(focusItem->user, "Focus item user cannot be NULL");
|
||||
uiitemlist_t *list = (uiitemlist_t *)focusItem->user;
|
||||
|
||||
const uint8_t focusIndex = focusItem->y * list->columns + focusItem->x;
|
||||
for(uint8_t i = 0; i < list->itemCount; i++) {
|
||||
uiItemSetHighlighted(&list->items[i], i == focusIndex);
|
||||
}
|
||||
|
||||
if(list->changed == NULL) return true;
|
||||
if(focusIndex >= list->itemCount) return true;
|
||||
|
||||
list->changed(list, focusIndex, &list->items[focusIndex]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t uiItemListFocusClosed(const uifocusitem_t *focusItem) {
|
||||
assertNotNull(focusItem, "Focus item cannot be NULL");
|
||||
assertNotNull(focusItem->user, "Focus item user cannot be NULL");
|
||||
uiitemlist_t *list = (uiitemlist_t *)focusItem->user;
|
||||
list->focusItem = NULL;
|
||||
|
||||
for(uint8_t i = 0; i < list->itemCount; i++) {
|
||||
uiItemSetHighlighted(&list->items[i], false);
|
||||
}
|
||||
|
||||
if(list->closed != NULL) list->closed(list);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uiitem.h"
|
||||
#include "ui/widget/uiscrolling.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "rpg/item/inventory.h"
|
||||
|
||||
#define UI_ITEM_LIST_CAPACITY_MAX 40
|
||||
|
||||
typedef struct uiitemlist_s uiitemlist_t;
|
||||
|
||||
typedef void (*uiitemlistselectedcallback_t)(
|
||||
const uiitemlist_t *list,
|
||||
const uint8_t index,
|
||||
const uiitem_t *item
|
||||
);
|
||||
|
||||
typedef void (*uiitemlistchangedcallback_t)(
|
||||
const uiitemlist_t *list,
|
||||
const uint8_t index,
|
||||
const uiitem_t *item
|
||||
);
|
||||
|
||||
typedef void (*uiitemlistclosedcallback_t)(const uiitemlist_t *list);
|
||||
|
||||
/**
|
||||
* Called to draw an extra per-item info column (columnIndex >= 1)
|
||||
* beyond the item's own default uiItemDraw rendering -- e.g. a shop
|
||||
* menu drawing a price alongside the item name.
|
||||
*
|
||||
* @param list The item list being drawn.
|
||||
* @param item The item slot being drawn.
|
||||
* @param columnIndex The info column being drawn (>= 1).
|
||||
* @param x Screen x position for this column.
|
||||
* @param y Screen y position for this column.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
typedef errorret_t (*uiitemlistcolumncallback_t)(
|
||||
const uiitemlist_t *list,
|
||||
const uiitem_t *item,
|
||||
const uint8_t columnIndex,
|
||||
const float_t x,
|
||||
const float_t y
|
||||
);
|
||||
|
||||
struct uiitemlist_s {
|
||||
uiitem_t items[UI_ITEM_LIST_CAPACITY_MAX];
|
||||
uint8_t itemCount;
|
||||
|
||||
// Grid layout: how many item slots wide/tall the list displays.
|
||||
uint8_t columns;
|
||||
uint8_t rows;
|
||||
|
||||
// How many info columns are rendered per item slot. 1 means only the
|
||||
// item's own default name/quantity is drawn; anything beyond that is
|
||||
// rendered via columnDraw.
|
||||
uint8_t itemColumns;
|
||||
|
||||
uiscrolling_t scroll;
|
||||
uifocusitem_t *focusItem;
|
||||
|
||||
uiitemlistselectedcallback_t selected;
|
||||
uiitemlistchangedcallback_t changed;
|
||||
uiitemlistclosedcallback_t closed;
|
||||
uiitemlistcolumncallback_t columnDraw;
|
||||
|
||||
void *user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes an item list.
|
||||
*
|
||||
* @param list The item list to initialize.
|
||||
* @param columns Number of item slots per row.
|
||||
* @param rows Number of item slot rows.
|
||||
* @param itemColumns Number of info columns rendered per item slot.
|
||||
* @param selected Called when an item slot is selected.
|
||||
* @param changed Called when the focused item slot changes.
|
||||
* @param closed Called when the item list is closed.
|
||||
* @param columnDraw Called to draw each extra info column (index >= 1);
|
||||
* may be NULL if itemColumns is 1.
|
||||
*/
|
||||
void uiItemListInit(
|
||||
uiitemlist_t *list,
|
||||
const uint8_t columns,
|
||||
const uint8_t rows,
|
||||
const uint8_t itemColumns,
|
||||
uiitemlistselectedcallback_t selected,
|
||||
uiitemlistchangedcallback_t changed,
|
||||
uiitemlistclosedcallback_t closed,
|
||||
uiitemlistcolumncallback_t columnDraw
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the items displayed by the list directly, copying them into
|
||||
* the list's own storage.
|
||||
*
|
||||
* @param list The item list to update.
|
||||
* @param items The items to display.
|
||||
* @param itemCount Number of entries in items. Must be <=
|
||||
* UI_ITEM_LIST_CAPACITY_MAX.
|
||||
*/
|
||||
void uiItemListSetItems(
|
||||
uiitemlist_t *list,
|
||||
const uiitem_t *items,
|
||||
const uint8_t itemCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the items displayed by the list from an array of item stacks,
|
||||
* internally creating a uiitem_t for each stack.
|
||||
*
|
||||
* @param list The item list to update.
|
||||
* @param stacks The item stacks to display.
|
||||
* @param stackCount Number of entries in stacks. Must be <=
|
||||
* UI_ITEM_LIST_CAPACITY_MAX.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiItemListSetItemStacks(
|
||||
uiitemlist_t *list,
|
||||
const inventorystack_t *stacks,
|
||||
const uint8_t stackCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Pushes the item list onto the UI focus stack, making it navigable.
|
||||
* No-op if already open.
|
||||
*
|
||||
* @param list The item list to open.
|
||||
*/
|
||||
void uiItemListOpen(uiitemlist_t *list);
|
||||
|
||||
/**
|
||||
* Pops the item list from the UI focus stack. No-op if already closed.
|
||||
*
|
||||
* @param list The item list to close.
|
||||
*/
|
||||
void uiItemListClose(uiitemlist_t *list);
|
||||
|
||||
/**
|
||||
* Returns whether the item list is currently on the UI focus stack.
|
||||
*
|
||||
* @param list The item list to query.
|
||||
* @returns True if active.
|
||||
*/
|
||||
bool_t uiItemListIsActive(const uiitemlist_t *list);
|
||||
|
||||
/**
|
||||
* Draws the item list's grid of item slots at the given position.
|
||||
* Only the first columns * rows items are drawn.
|
||||
*
|
||||
* @param list The item list to draw.
|
||||
* @param x Screen x position.
|
||||
* @param y Screen y position.
|
||||
* @param width Content width, used to lay out columns.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiItemListDraw(
|
||||
const uiitemlist_t *list,
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const float_t width
|
||||
);
|
||||
|
||||
/**
|
||||
* Internal focus callback -- forwards selection to the list's selected
|
||||
* handler.
|
||||
*
|
||||
* @param focusItem The active focus item; user field must point to
|
||||
* uiitemlist_t.
|
||||
* @returns True.
|
||||
*/
|
||||
bool_t uiItemListFocusSelected(const uifocusitem_t *focusItem);
|
||||
|
||||
/**
|
||||
* Internal focus callback -- updates item highlights and fires
|
||||
* changed.
|
||||
*
|
||||
* @param focusItem The active focus item; user field must point to
|
||||
* uiitemlist_t.
|
||||
* @returns True.
|
||||
*/
|
||||
bool_t uiItemListFocusChanged(const uifocusitem_t *focusItem);
|
||||
|
||||
/**
|
||||
* Internal focus callback -- clears focusItem and fires the closed
|
||||
* handler.
|
||||
*
|
||||
* @param focusItem The active focus item; user field must point to
|
||||
* uiitemlist_t.
|
||||
* @returns True.
|
||||
*/
|
||||
bool_t uiItemListFocusClosed(const uifocusitem_t *focusItem);
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uiscrolling.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void uiScrollingInit(uiscrolling_t *scrolling) {
|
||||
assertNotNull(scrolling, "Scrolling container cannot be NULL");
|
||||
// Nothing to initialize yet -- uiscrolling_t is currently a
|
||||
// placeholder with no fields.
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} uiscrolling_t;
|
||||
|
||||
/**
|
||||
* Initializes a scrolling container. Currently a placeholder -- no
|
||||
* scrolling behavior is implemented yet.
|
||||
*
|
||||
* @param scrolling The scrolling container to initialize.
|
||||
*/
|
||||
void uiScrollingInit(uiscrolling_t *scrolling);
|
||||
@@ -78,6 +78,12 @@ errorret_t displayInitDolphin(void) {
|
||||
GX_SetDispCopyGamma(GX_GM_1_0);
|
||||
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.
|
||||
GX_ClearVtxDesc();
|
||||
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) {
|
||||
GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE);
|
||||
GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_FALSE);
|
||||
} else {
|
||||
GX_SetZMode(GX_FALSE, GX_ALWAYS, GX_FALSE);
|
||||
}
|
||||
|
||||
@@ -468,6 +468,102 @@ static void test_requireLoaded_propagates_error(void **state) {
|
||||
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
|
||||
// ============================================================
|
||||
@@ -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_spins_to_loaded, 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);
|
||||
}
|
||||
|
||||
+20
-16
@@ -1,9 +1,9 @@
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
|
||||
parser = argparse.ArgumentParser(description="Item CSV to .h defines")
|
||||
parser.add_argument("--csv", required=True, help="Path to item CSV file")
|
||||
parser = argparse.ArgumentParser(description="Item JSON to .h defines")
|
||||
parser.add_argument("--json", required=True, help="Path to item JSON file")
|
||||
parser.add_argument("--output", required=True, help="Path to output .h file")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -13,22 +13,26 @@ def type_enum(name):
|
||||
def id_enum(name):
|
||||
return "ITEM_ID_" + name.upper()
|
||||
|
||||
# Load CSV
|
||||
# Load JSON
|
||||
item_ids = []
|
||||
item_types = []
|
||||
rows = {}
|
||||
|
||||
with open(args.csv, newline="", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
if "id" not in reader.fieldnames or "type" not in reader.fieldnames:
|
||||
raise ValueError("CSV must have 'id' and 'type' columns")
|
||||
for row in reader:
|
||||
item_id, item_type = row["id"], row["type"]
|
||||
if item_id not in item_ids:
|
||||
item_ids.append(item_id)
|
||||
if item_type not in item_types:
|
||||
item_types.append(item_type)
|
||||
rows[item_id] = row
|
||||
with open(args.json, encoding="utf-8") as f:
|
||||
entries = json.load(f)
|
||||
|
||||
if not all(
|
||||
"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"]
|
||||
if item_id not in item_ids:
|
||||
item_ids.append(item_id)
|
||||
if item_type not in item_types:
|
||||
item_types.append(item_type)
|
||||
rows[item_id] = row
|
||||
|
||||
# Assign enum values: types and IDs each start from 1 with NULL = 0.
|
||||
type_values = {}
|
||||
@@ -85,7 +89,7 @@ for i in item_ids:
|
||||
f" [{id_enum(i)}] = {{",
|
||||
f" .id = {id_enum(i)},",
|
||||
f" .type = {type_enum(row['type'])},",
|
||||
f" .name = \"{i}\",",
|
||||
f" .name = \"item.{row['name']}.name\",",
|
||||
" },",
|
||||
]
|
||||
out += [
|
||||
|
||||
Vendored
-61
@@ -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;
|
||||
Vendored
-72
@@ -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;
|
||||
Vendored
-97
@@ -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;
|
||||
Vendored
-65
@@ -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;
|
||||
Vendored
-23
@@ -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 0–3).
|
||||
* 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
Reference in New Issue
Block a user