3 Commits

Author SHA1 Message Date
YourWishes 9551e222dc Chunks as json 2026-07-11 17:43:26 -05:00
YourWishes 60dfb89b53 Map as a file 2026-07-11 17:07:03 -05:00
YourWishes b08ad308e4 item but as a file 2026-07-11 11:44:34 -05:00
258 changed files with 7097 additions and 22371 deletions
+455
View File
@@ -0,0 +1,455 @@
# Dusk — Claude Code rules
## File headers
Every C, H, and JS file starts with:
```c
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
```
JS files use `//` comment style instead.
---
## C conventions
### Types
Always use the project-defined aliases instead of bare C primitives:
| Use | Not |
|-----------|--------------|
| `bool_t` | `bool` |
| `int_t` | `int` |
| `float_t` | `float` |
| `char_t` | `char` |
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
### Naming
- **Functions** — snake_case, prefixed with their module:
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
- **Macros / constants** — UPPER_SNAKE_CASE:
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
- **Files** — snake_case matching the primary type: `entityposition.c`,
`moduleassetbatch.c`
### Header files (`.h`)
- Use `#pragma once` — no include guards.
- Declare every public function, `#define`, and `extern` global.
- Write a JSDoc block (`/** … */`) above every declaration explaining
purpose, `@param`s, and `@returns`.
- Only include headers that the `.h` file itself strictly requires for
the types it exposes. Move everything else to the `.c` file.
Do not use forward declarations as a workaround — use the real
include in the `.c` file instead.
### Implementation files (`.c`)
- Contain function bodies only; no declarations.
- Pull in whatever additional includes the implementation needs.
- Do not use `static` or `inline` on **functions**. Every function,
including internal helpers, must be declared in the matching `.h` and
defined in the `.c` file. Internal helpers belong near the bottom of
the `.c` file, not at the top with a `static` qualifier.
`static` and `inline` on functions are only appropriate when the
function body is written directly inside a `.h` file.
`static` on **variables** (file-scope state) is fine and expected.
### Formatting
- Hard-wrap all lines at **80 characters**.
### Error handling
Return `errorret_t` from fallible functions. Use these macros:
```c
errorOk(); // return success
errorThrow("msg %d", val); // return failure with message
errorChain(someCall()); // propagate failure, continue on success
errorIsOk(ret) / errorIsNotOk(ret) // test a result
errorCatch(ret); // handle + free an error
```
Never return raw error codes or use `errno` for in-engine errors.
### Memory
Use the project allocator — never raw `malloc`/`free`:
```c
memoryAllocate(size) // allocate
memoryFree(ptr) // free
memoryZero(dest, size) // zero a block
memoryCopy(dest, src, size) // copy
```
### Asserts
Prefer specific assert macros over bare `assert()`:
```c
assertNotNull(ptr, "msg");
assertTrue(cond, "msg");
assertFalse(cond, "msg");
assertUnreachable("msg");
assertIsMainThread("msg");
```
---
## Build system
Each subdirectory has its own `CMakeLists.txt` that adds sources with:
```cmake
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
myfile.c
)
```
Never add source files to the root `CMakeLists.txt` directly.
---
## Platform support
### Targets
Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
| `DUSK_TARGET_SYSTEM` | Macro defined | Platform |
|----------------------|-------------------|------------------|
| `linux` | `DUSK_LINUX` | Linux desktop |
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
| `psp` | `DUSK_PSP` | Sony PSP |
| `vita` | `DUSK_VITA` | PlayStation Vita |
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
| `wii` | `DUSK_WII` | Nintendo Wii |
### Layer structure
```
src/dusk/ core, platform-agnostic game logic
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
src/dusklinux/ Linux + Knulli platform impl
src/duskpsp/ PSP platform impl
src/duskvita/ Vita platform impl
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
```
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
it uses native GameCube/Wii rendering and input APIs.
### Platform guards
Use the compile-time macros for platform-specific code:
```c
#ifdef DUSK_PSP
// PSP-only path
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
// GameCube / Wii path
#else
// Generic / Linux fallback
#endif
```
Additional capability macros set per-target:
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
### Abstraction pattern
Platform-specific implementations are wired in via `#define` macros in
each platform's `displayplatform.h` / `inputplatform.h` etc., which
the core calls through. Functions that a platform does not support are
simply left undefined — the core guards calls with `#ifdef`.
### Adding platform-specific code
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
or capability macro.
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
the platform header macros instead.
---
## Adding a new asset loader type
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
`src/dusk/asset/loader/assetloader.h`.
2. Add fields to the input/loading/output unions in `assetloader.h`.
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
`src/dusk/asset/loader/assetloader.c`.
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
---
## Adding a new entity component
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
`entityMyCompDispose()`.
2. Add the include to `src/dusk/entity/componentlist.h` header block.
3. Add a row to `src/dusk/entity/componentlist.h`:
```c
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
```
This auto-generates the enum, union field, and definition entry.
4. If JS-facing, create the script module and `.d.ts` (see below).
---
## Adding a new script (JS) module
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
- Use `moduleBaseFunction(name)` to define JS-callable functions.
- Register props/funcs in `moduleMyModInit()` with
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
`scriptProtoDefineStaticFunc`.
2. `#include` the header in
`src/dusk/script/module/modulelist.c` and call
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
`moduleListDispose()`).
3. For component modules also register in
`src/dusk/script/module/entity/component/modulecomponentlist.c`
so `entity.add()` returns the typed wrapper.
4. Create `types/<category>/mymod.d.ts` and add a
`/// <reference path="..." />` line to `types/index.d.ts`.
---
## Script module type declarations
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
check whether the corresponding `types/**/*.d.ts` needs updating and
apply any changes before finishing the task.
---
## JavaScript (asset scripts)
- Use `var` for module-level state; `const` for values that never
change.
- Always use semicolons.
- Scene objects are plain objects (`var scene = {}`) with assigned
methods.
- Export via `module.exports = scene`.
- Async scene init should use `async function` and `await`.
---
## Coding style
### ASCII only
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000U+007F).
Non-ASCII characters are banned even in comments and string literals.
Use ASCII-only substitutes instead:
- `--` or `-` instead of `` (em dash)
- `->` instead of `` (arrow)
- `x` or `*` instead of `×` (multiplication)
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
### Indentation
2 spaces. No tabs.
### Keyword and operator spacing
No space between a keyword or function name and its opening parenthesis:
```c
if(!ptr) return;
for(uint8_t i = 0; i < count; i++) {
while(entry->state != DONE) {
switch(type) {
sizeof(assetbatch_t)
memoryZero(ptr, size)
```
Spaces around all binary operators and after every comma:
```c
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
(size_t)end - (size_t)start
foo(a, b, c)
```
### Braces
Opening brace on the **same line** as the statement (K&R style) for all
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
```c
void assetEntryLock(assetentry_t *entry) {
...
}
if(dirty) {
...
} else {
...
}
```
### Guard returns
Short guards go on one line with no braces:
```c
if(!ptr) return;
if(!b || !b->batch) return jerry_undefined();
if(!(flags & DIRTY)) return;
```
### Blank lines
- One blank line between functions; no blank line at the start or end of
a function body.
- One blank line between logical blocks inside a function body.
- No trailing blank lines at the end of a file.
### Pointer placement
`*` is attached to the variable name, not the type:
```c
assetentry_t *entry
const char_t *name
void *ptr
uint8_t *d = (uint8_t *)dest;
```
### Casts
Space between cast and operand:
```c
(assetbatch_t *)user
(uint8_t *)dest
(textureformat_t)v
```
### Return
No parentheses around the return value:
```c
return ptr;
return MEMORY_POINTERS_IN_USE;
```
### switch / case
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
```c
switch(type) {
case ASSET_LOADER_TYPE_TEXTURE:
descs[i].input.texture = (textureformat_t)v;
break;
default:
break;
}
```
### Multi-line function signatures
When parameters don't fit on one line, put each on its own line indented
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
its own line at column 0:
```c
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
errorret_t memoryCompare(
const void *a,
const void *b,
const size_t size
);
```
### Structs and enums
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
brace and name on the same line:
```c
typedef struct {
errorcode_t code;
char_t *message;
} errorstate_t;
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
```
### Designated initialisers
Spaces inside braces; `.field = value`:
```c
jsassetentry_t e = { .entry = entry };
assetbatchloadedpend_t init = { .batch = batch };
```
### Ternary operator
Spaces around `?` and `:`:
```c
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
```
### const placement
`const` before the type, `*` attached to the variable:
```c
const char_t *name
const void *src
const size_t size
```
### Comments in `.c` files
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
functions follow one another with a single blank line between them.
- Multi-line explanatory comments inside function bodies use `//` lines:
```c
// Script modules are freed; orphaned JS wrapper objects now get GC'd
// so their finalizers fire before assetDispose() checks ref counts.
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
```
- Do not use `/* */` for inline or inline-block comments inside `.c`
function bodies.
### Comments in `.h` files
Every public declaration gets a Javadoc block (`/** … */`) with
`@param` and `@returns` where relevant. Keep it on the lines immediately
above the declaration with no blank line in between.
---
## Color system
Colors are defined in `src/dusk/display/color.csv` and code-generated
into a `color.h` header by `tools/color/csv/__main__.py`.
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
The script emits four `#define` variants per color plus a bare alias:
```
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
COLOR_<NAME>_3B color3b(r8, g8, b8)
COLOR_<NAME>_3F color3f(rf, gf, bf)
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
COLOR_<NAME> COLOR_<NAME>_4B
```
`color_t` is `color4b_t` (four `uint8_t` channels).
To add a new color, append a row to `color.csv` and rebuild — do not
hand-edit the generated header.
---
## Tests
- Tests live in `test/` mirroring `src/dusk/` structure.
- Use cmocka; include `dusktest.h`.
- Test functions: `static void test_something(void **state)`.
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
leaks.
- Build with `-DDUSK_BUILD_TESTS=ON`.
-7
View File
@@ -13,7 +13,6 @@ cmake_policy(SET CMP0079 NEW)
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON) # set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
option(DUSK_BUILD_TESTS "Enable tests" OFF) option(DUSK_BUILD_TESTS "Enable tests" OFF)
option(DUSK_NETWORK "Enable network support" ON)
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name") set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder") set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
@@ -91,12 +90,6 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
DUSK_VERSION="${DUSK_VERSION}" DUSK_VERSION="${DUSK_VERSION}"
) )
if(DUSK_NETWORK)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_NETWORK
)
endif()
# Toolchains # Toolchains
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake) include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
{
"pause": "DEFAULT",
"items": [
{ "type": "text", "text": "Test Two." },
{ "type": "entityAdd", "entityType": "npc", "position": [4, 4, 0] },
{
"type": "textMini",
"text": "Hello!",
"position": [4, 4, 0],
"duration": 3.0
},
{
"type": "emoji",
"entityIndex": "lastCreated",
"emojiType": "exclamation",
"duration": 2.0
},
{
"type": "entityWalkTo",
"entityIndex": "lastCreated",
"positions": [[8, 2, 0]]
},
{ "type": "text", "text": "Done." }
]
}
-127
View File
@@ -10,9 +10,6 @@ msgid "ui.title"
msgstr "" msgstr ""
"Welcome" "Welcome"
msgid "save.linux.mkdirp_failed"
msgstr "Failed to create save directory, check the disk is not full or write-protected."
#: src/dusk/ui/frame/settings/uisettings.c #: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general" msgid "ui.settings.tabs.general"
msgstr "General" msgstr "General"
@@ -59,130 +56,6 @@ msgstr "Items"
msgid "ui.game_menu.settings" msgid "ui.game_menu.settings"
msgstr "Settings" msgstr "Settings"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "Save"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_success"
msgstr "Game saved."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_cancelled"
msgstr "Save cancelled."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_unavailable"
msgstr "Can't save - no save device found."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_temporary"
msgstr "This session is temporary - no save device was found, so saving is disabled."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_create_confirm"
msgstr "No save data found. Create a new save?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_failed_format"
msgstr "Save failed: %s"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_check_failed_format"
msgstr "Can't save: %s"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.message"
msgstr "No save device found. You can continue, but\nprogress will not be saved."
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.retry"
msgstr "Retry"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.continue"
msgstr "Continue Anyway"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.message"
msgstr "No save data found. Create a new save?"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.yes"
msgstr "Yes"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.no"
msgstr "No"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.new_game"
msgstr "New Game"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.load_game"
msgstr "Load Game"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.options"
msgstr "Options"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.quit"
msgstr "Quit Game"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.confirm"
msgstr "Confirm"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.cancel"
msgstr "Cancel"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.attack"
msgstr "Attack"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.flee"
msgstr "Flee"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.target_format"
msgstr "Enemy %u (%u/%u HP)"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.hp_format"
msgstr "HP %u/%u"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.mp_format"
msgstr "MP %u/%u"
#: src/dusk/ui/frame/settings/uisettingsaudio.c
msgid "ui.settings.audio.placeholder"
msgstr "No audio settings yet"
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
msgid "ui.settings.display.placeholder"
msgstr "No display settings yet"
#: src/dusk/ui/frame/settings/uisettingsinput.c
msgid "ui.settings.input.placeholder"
msgstr "No input settings yet"
#: src/dusk/ui/frame/backpack/uibackpack.c
msgid "ui.backpack.category_format"
msgstr "Category %u"
#: src/dusk/ui/overlay/uiloading.c
msgid "ui.loading.text"
msgstr "loading"
#: src/dusk/ui/overlay/uiautosave.c
msgid "ui.autosave.saving"
msgstr "SAVING"
msgid "item.potion.name" msgid "item.potion.name"
msgstr "Potion" msgstr "Potion"
-124
View File
@@ -57,130 +57,6 @@ msgstr "Objetos"
msgid "ui.game_menu.settings" msgid "ui.game_menu.settings"
msgstr "Configuración" msgstr "Configuración"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "Guardar"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_success"
msgstr "Partida guardada."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_cancelled"
msgstr "Guardado cancelado."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_unavailable"
msgstr "No se puede guardar: no se encontró ningún dispositivo de guardado."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_temporary"
msgstr "Esta sesión es temporal - no se encontró ningún dispositivo de guardado, por lo que guardar está deshabilitado."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_create_confirm"
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_failed_format"
msgstr "Error al guardar: %s"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_check_failed_format"
msgstr "No se puede guardar: %s"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.message"
msgstr "No se encontró ningún dispositivo de guardado. Puedes continuar, pero\nel progreso no se guardará."
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.retry"
msgstr "Reintentar"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.continue"
msgstr "Continuar de todos modos"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.message"
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.yes"
msgstr "Sí"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.no"
msgstr "No"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.new_game"
msgstr "Nueva Partida"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.load_game"
msgstr "Cargar Partida"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.options"
msgstr "Opciones"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.quit"
msgstr "Salir del Juego"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.confirm"
msgstr "Confirmar"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.cancel"
msgstr "Cancelar"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.attack"
msgstr "Atacar"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.flee"
msgstr "Huir"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.target_format"
msgstr "Enemigo %u (%u/%u PS)"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.hp_format"
msgstr "PS %u/%u"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.mp_format"
msgstr "PM %u/%u"
#: src/dusk/ui/frame/settings/uisettingsaudio.c
msgid "ui.settings.audio.placeholder"
msgstr "Aún no hay opciones de audio"
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
msgid "ui.settings.display.placeholder"
msgstr "Aún no hay opciones de pantalla"
#: src/dusk/ui/frame/settings/uisettingsinput.c
msgid "ui.settings.input.placeholder"
msgstr "Aún no hay opciones de entrada"
#: src/dusk/ui/frame/backpack/uibackpack.c
msgid "ui.backpack.category_format"
msgstr "Categoría %u"
#: src/dusk/ui/overlay/uiloading.c
msgid "ui.loading.text"
msgstr "cargando"
#: src/dusk/ui/overlay/uiautosave.c
msgid "ui.autosave.saving"
msgstr "GUARDANDO"
#: src/dusk/rpg/item/item.json #: src/dusk/rpg/item/item.json
msgid "item.potion.name" msgid "item.potion.name"
msgstr "Poción" msgstr "Poción"
-124
View File
@@ -57,130 +57,6 @@ msgstr "アイテム"
msgid "ui.game_menu.settings" msgid "ui.game_menu.settings"
msgstr "設定" msgstr "設定"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "セーブ"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_success"
msgstr "セーブしました。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_cancelled"
msgstr "セーブをキャンセルしました。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_unavailable"
msgstr "セーブできません - セーブデバイスが見つかりません。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_temporary"
msgstr "このセッションは一時的です - セーブデバイスが見つからなかったため、セーブは無効になっています。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_create_confirm"
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_failed_format"
msgstr "セーブに失敗しました: %s"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_check_failed_format"
msgstr "セーブできません: %s"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.message"
msgstr "セーブデバイスが見つかりません。続行できますが、\n進行状況は保存されません。"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.retry"
msgstr "再試行"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.continue"
msgstr "続行する"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.message"
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.yes"
msgstr "はい"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.no"
msgstr "いいえ"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.new_game"
msgstr "ニューゲーム"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.load_game"
msgstr "ロードゲーム"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.options"
msgstr "オプション"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.quit"
msgstr "ゲームを終了"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.confirm"
msgstr "確認"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.cancel"
msgstr "キャンセル"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.attack"
msgstr "攻撃"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.flee"
msgstr "逃げる"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.target_format"
msgstr "敵%u (%u/%u HP)"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.hp_format"
msgstr "HP %u/%u"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.mp_format"
msgstr "MP %u/%u"
#: src/dusk/ui/frame/settings/uisettingsaudio.c
msgid "ui.settings.audio.placeholder"
msgstr "オーディオ設定はまだありません"
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
msgid "ui.settings.display.placeholder"
msgstr "表示設定はまだありません"
#: src/dusk/ui/frame/settings/uisettingsinput.c
msgid "ui.settings.input.placeholder"
msgstr "入力設定はまだありません"
#: src/dusk/ui/frame/backpack/uibackpack.c
msgid "ui.backpack.category_format"
msgstr "カテゴリー%u"
#: src/dusk/ui/overlay/uiloading.c
msgid "ui.loading.text"
msgstr "読み込み中"
#: src/dusk/ui/overlay/uiautosave.c
msgid "ui.autosave.saving"
msgstr "保存中"
#: src/dusk/rpg/item/item.json #: src/dusk/rpg/item/item.json
msgid "item.potion.name" msgid "item.potion.name"
msgstr "ポーション" msgstr "ポーション"
+1
View File
@@ -0,0 +1 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_-1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_4_4.json", "offset": [2.0, 1.0, 0.0]}, {"model": "models/buildings/house_6_3.json", "offset": [1.0, 9.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [12.0, 6.0, 0.0]}]}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_0_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_8_4.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_3_3.json", "offset": [11.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [3.0, 11.0, 0.0]}]}
+1
View File
@@ -0,0 +1 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_3_2.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [7.0, 2.0, 0.0]}, {"model": "models/buildings/house_4_2.json", "offset": [1.0, 8.0, 0.0]}, {"model": "models/buildings/house_1_1.json", "offset": [11.0, 10.0, 0.0]}, {"model": "models/buildings/house_5_2.json", "offset": [13.0, 6.0, 0.0]}]}
+1
View File
@@ -0,0 +1 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_1_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_5_5.json", "offset": [3.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [10.0, 3.0, 0.0]}, {"model": "models/buildings/house_3_1.json", "offset": [1.0, 12.0, 0.0]}]}
+1
View File
@@ -0,0 +1 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 0], [4, 0], [4, 0], [4, 0], [4, 0], [4, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_2_0_0_0.json", "offset": [0.0, 0.0, 0.0]}]}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Test Map",
"entities": [
{ "type": "player", "position": [10, 2, 0], "direction": "north" },
{ "type": "item", "position": [12, 2, 0], "item": "POTION", "quantity": 1 },
{
"type": "npc",
"position": [8, 8, 1],
"path": [[4, 4, 0], [10, 10, 1], [4, 4, 0], [10, 10, 1]],
"cutscene": "test_npc"
}
]
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-11
View File
@@ -4,17 +4,6 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_WII DUSK_WII
) )
# Wii save storage method - see src/duskdolphin/save/savedeviceplatform.h.
set(DUSK_SAVE_WII_METHOD "NAND" CACHE STRING
"Wii save storage: NAND (internal storage via ISFS), CARD (GameCube-\
compatible memory card emulation), or SD (SD card via libfat)"
)
set_property(CACHE DUSK_SAVE_WII_METHOD PROPERTY STRINGS "NAND" "CARD" "SD")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SAVE_WII_METHOD_${DUSK_SAVE_WII_METHOD}
)
# Generate Homebrew Channel meta.xml from project identity variables # Generate Homebrew Channel meta.xml from project identity variables
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC) string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
configure_file( configure_file(
-4
View File
@@ -5,10 +5,6 @@
add_subdirectory(dusk) add_subdirectory(dusk)
if(DUSK_NETWORK)
add_subdirectory(dusknetwork)
endif()
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli") if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
add_subdirectory(dusklinux) add_subdirectory(dusklinux)
add_subdirectory(dusksdl2) add_subdirectory(dusksdl2)
+1
View File
@@ -68,6 +68,7 @@ add_subdirectory(scene)
add_subdirectory(system) add_subdirectory(system)
add_subdirectory(time) add_subdirectory(time)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(network)
add_subdirectory(save) add_subdirectory(save)
add_subdirectory(util) add_subdirectory(util)
add_subdirectory(thread) add_subdirectory(thread)
-1
View File
@@ -7,5 +7,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
easing.c easing.c
animation.c animation.c
keyframe.c
) )
+25 -105
View File
@@ -11,122 +11,42 @@
void animationInit( void animationInit(
animation_t *anim, animation_t *anim,
keyframe_t *keyframes, keyframe_t *keyframes,
uint16_t *keyframeCounts, uint16_t keyframeCount
const uint16_t layerCount
) { ) {
assertNotNull(anim, "Animation pointer cannot be null."); assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(keyframes, "Keyframes pointer cannot be null."); assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null."); assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
assertTrue(layerCount > 0, "Layer count must be greater than zero.");
memoryZero(anim, sizeof(animation_t));
anim->keyframes = keyframes; anim->keyframes = keyframes;
anim->keyframeCounts = keyframeCounts; anim->keyframeCount = keyframeCount;
anim->layerCount = layerCount;
// Determine duration
float_t duration = 0.0f;
for(uint16_t layer = 0; layer < layerCount; layer++) {
uint16_t keyframeCount = keyframeCounts[layer];
assertTrue(keyframeCount > 0, "Keyframe count invalid.");
keyframe_t *layerKeyframes = keyframes + layer * keyframeCount;
#ifdef DUSK_ASSERTIONS
// Check that the keyframes are sorted by time.
for(uint16_t i = 1; i < keyframeCount; i++) {
assertTrue(
layerKeyframes[i].time >= layerKeyframes[i - 1].time,
"Keyframes must be sorted by time."
);
}
#endif
keyframe_t *lastKeyframe = layerKeyframes + keyframeCount - 1;
duration = mathMax(duration, lastKeyframe->time);
}
assertTrue(duration > 0, "Animation duration must be greater than 0.");
anim->duration = duration;
} }
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) { float_t animationGetValue(animation_t *anim, const float_t time) {
assertNotNull(anim, "Animation pointer cannot be null."); assertNotNull(anim, "Animation pointer cannot be null.");
assertTrue(layer < anim->layerCount, "Layer index out of bounds."); assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
assertTrue(time >= 0, "Time must be non-negative.");
keyframe_t *start;
keyframe_t *end;
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
keyframe_t *current = anim->keyframes;
start = current;
uint16_t keyframeCount = anim->keyframeCounts[layer]; do {
keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount; if(current->time > time) {
return keyframeGetValue(layerKeyframes, keyframeCount, anim->time); end = current;
} break;
void animationUpdate(
animation_t *anim,
const float_t deltaTime
) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertTrue(deltaTime >= 0, "Delta time must be non-negative.");
bool_t justCompleted = false;
if(!(anim->flags & ANIMATION_FLAG_INTERNAL_COMPLETED)) {
bool_t loop = (anim->flags & ANIMATION_FLAG_LOOP) != 0;
bool_t pingpong = (anim->flags & ANIMATION_FLAG_PINGPONG) != 0;
assertFalse(
loop && pingpong,
"Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG."
);
bool_t backward;
if(pingpong) {
backward = (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0;
} else {
backward = (anim->flags & ANIMATION_FLAG_REVERSE) != 0;
} }
start = current;
current++;
// Resolve boundary crossings one at a time, so a single large deltaTime if(current > last) {
// can correctly loop/pingpong across multiple boundaries in one call. end = start;
float_t remaining = deltaTime; break;
while(remaining > 0.0f) {
float_t toBoundary = (
backward ? anim->time : (anim->duration - anim->time)
);
if(remaining < toBoundary) {
anim->time += backward ? -remaining : remaining;
break;
}
remaining -= toBoundary;
anim->time = backward ? 0.0f : anim->duration;
bool_t stopHere;
if(backward) {
stopHere = (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0;
} else {
stopHere = (anim->flags & ANIMATION_FLAG_STOP_END) != 0;
}
if(stopHere) {
justCompleted = true;
break;
} else if(pingpong) {
backward = !backward;
if(backward) anim->flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
else anim->flags &= ~ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
} else if(loop) {
anim->time = backward ? anim->duration : 0.0f;
if(anim->onLoop) anim->onLoop(anim->user);
} else {
justCompleted = true;
break;
}
} }
} while(true);
if(justCompleted) anim->flags |= ANIMATION_FLAG_INTERNAL_COMPLETED; float_t t = (time - start->time) / (end->time - start->time);
} return mathLerp(start->value, end->value, easingApply(start->easing, t));
// Call onUpdate for each layer.
for(uint16_t layer = 0; layer < anim->layerCount; layer++) {
float_t value = animationGetLayerValue(anim, layer);
if(anim->onUpdate) anim->onUpdate(layer, value, anim->user);
}
if(justCompleted && anim->onComplete) anim->onComplete(anim->user);
} }
+12 -72
View File
@@ -6,89 +6,29 @@
#pragma once #pragma once
#include "keyframe.h" #include "keyframe.h"
#define ANIMATION_FLAG_LOOP (1 << 0)
#define ANIMATION_FLAG_REVERSE (1 << 1)
#define ANIMATION_FLAG_PINGPONG (1 << 2)
#define ANIMATION_FLAG_STOP_BEGINNING (1 << 3)
#define ANIMATION_FLAG_STOP_END (1 << 4)
// Internal - tracks which direction a pingponging animation is currently
// travelling. Do not set this manually, it is managed by animationUpdate().
#define ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD (1 << 7)
// Internal - set once the animation has stopped advancing (see
// animationUpdate()). Do not set this manually. There is currently no way to
// restart a completed animation short of clearing this bit and resetting
// anim->time by hand.
#define ANIMATION_FLAG_INTERNAL_COMPLETED (1 << 6)
typedef struct { typedef struct {
keyframe_t *keyframes; keyframe_t *keyframes;
uint16_t *keyframeCounts; uint16_t keyframeCount;
uint16_t layerCount;
float_t time;
float_t duration;
uint8_t flags;
void *user;
void (*onUpdate)(const uint16_t layer, const float_t value, void *user);
void (*onComplete)(void *user);
void (*onLoop)(void *user);
} animation_t; } animation_t;
/** /**
* Initializes an animation with the given keyframes and layer count. * Initializes an animation.
* *
* @param anim Pointer to the animation to initialize. * @param anim The animation to initialize.
* @param keyframes Pointer to the array of keyframes for each layer. * @param keyframes The keyframes to use for the animation.
* @param keyframeCount Number of keyframes in each layer. * @param keyframeCount The number of keyframes in the animation.
* @param layerCount Number of layers in the animation.
*/ */
void animationInit( void animationInit(
animation_t *anim, animation_t *anim,
keyframe_t *keyframes, keyframe_t *keyframes,
uint16_t *keyframeCounts, uint16_t keyframeCount
const uint16_t layerCount
); );
/** /**
* Sets the current time of the animation, clamping it to the valid range. * Gets the value of the animation at a given time.
* This will call the onUpdate callback but none of the other callbacks.
* *
* @param anim Pointer to the animation to set the time for. * @param anim The animation to get the value from.
* @param time The new time to set for the animation. * @param time The time at which to get the value, in seconds.
* @return The value of the animation at the given time.
*/ */
void animationSetTime(animation_t *anim, const float_t time); float_t animationGetValue(animation_t *anim, const float_t time);
/**
* Gets the current value of a specific layer in the animation based on the
* current animation time.
*/
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer);
/**
* Updates the animation state based on the elapsed time. Advances anim->time
* by deltaTime (or against it, if ANIMATION_FLAG_REVERSE is set), then
* resolves whatever happens when it reaches the 0 or duration boundary:
*
* - ANIMATION_FLAG_PINGPONG: reflects off the boundary and continues playing
* in the opposite direction, forever, unless stopped (see below).
* - ANIMATION_FLAG_LOOP: wraps back around to the other boundary and keeps
* playing in the same direction, forever, unless stopped (see below).
* - ANIMATION_FLAG_STOP_BEGINNING / ANIMATION_FLAG_STOP_END: when the
* animation reaches that specific boundary, it clamps there and stops
* (firing onComplete) instead of looping/pingponging past it.
* - If none of the above apply at a boundary, the animation clamps there and
* stops, firing onComplete.
*
* onUpdate is called for every layer on every call. onLoop is called each
* time a loop wraps around. onComplete is called at most once, the moment
* the animation stops advancing.
*
* @param anim Pointer to the animation to update.
* @param deltaTime Time elapsed since the last update (in seconds).
*/
void animationUpdate(
animation_t *anim,
const float_t deltaTime
);
-45
View File
@@ -1,45 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "keyframe.h"
#include "assert/assert.h"
#include "util/math.h"
float_t keyframeGetValue(
const keyframe_t *keyframes,
const uint32_t keyframeCount,
const float_t time
) {
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
assertTrue(time >= 0, "Time must be non-negative.");
#ifdef DUSK_ASSERTIONS
// Checks that the keyframes are sorted by time.
for(uint32_t i = 1; i < keyframeCount; i++) {
assertTrue(
keyframes[i].time >= keyframes[i - 1].time,
"Keyframes must be sorted by time."
);
}
#endif
keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1);
if(time >= last->time) return last->value;
// Since time < last->time (checked above), current is guaranteed to stop
// at or before reaching last, so no separate end-of-array check is needed.
keyframe_t *current = (keyframe_t *)keyframes;
keyframe_t *start = current;
while(current->time <= time) {
start = current;
current++;
}
keyframe_t *end = current;
float_t t = (time - start->time) / (end->time - start->time);
return mathLerp(start->value, end->value, easingApply(start->easing, t));
}
+1 -15
View File
@@ -10,18 +10,4 @@ typedef struct {
float_t time; float_t time;
float_t value; float_t value;
easingtype_t easing; easingtype_t easing;
} keyframe_t; } keyframe_t;
/**
* Gets the value of a keyframe at a given time.
*
* @param keyframes The keyframes to get the value from.
* @param keyframeCount The number of keyframes in the array.
* @param time The time at which to get the value, in seconds.
* @return The value of the keyframe at the given time.
*/
float_t keyframeGetValue(
const keyframe_t *keyframes,
const uint32_t keyframeCount,
const float_t time
);
-2
View File
@@ -22,8 +22,6 @@
#endif #endif
#ifndef DUSK_ASSERTIONS_FAKED #ifndef DUSK_ASSERTIONS_FAKED
#define DUSK_ASSERTIONS 1
/** /**
* Initializes the assert system. Must be the very first call in engine * Initializes the assert system. Must be the very first call in engine
* startup. * startup.
+136 -173
View File
@@ -8,60 +8,17 @@
#include "assetchunkloader.h" #include "assetchunkloader.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/endian.h" #include "util/string.h"
#include "asset/loader/assetloading.h" #include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h" #include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h" #include "asset/loader/assetloader.h"
#include "asset/loader/json/assetjsonloader.h"
#include "asset/asset.h" #include "asset/asset.h"
#include "yyjson.h"
// Reads a little-endian int16 from a potentially-unaligned offset into a
// worldunit_t, advancing *offset past it.
static worldunit_t assetChunkReadWorldUnit(
const uint8_t *data,
size_t *offset
) {
int16_t value;
memoryCopy(&value, data + *offset, sizeof(int16_t));
*offset += sizeof(int16_t);
return (worldunit_t)endianLittleToHost16((uint16_t)value);
}
static worldpos_t assetChunkReadWorldPos(const uint8_t *data, size_t *offset) {
worldpos_t pos;
pos.x = assetChunkReadWorldUnit(data, offset);
pos.y = assetChunkReadWorldUnit(data, offset);
pos.z = assetChunkReadWorldUnit(data, offset);
return pos;
}
errorret_t assetChunkLoaderAsync(assetloading_t *loading) { errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread."); assertNotMainThread("Should be called from an async thread.");
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
errorOk();
}
assertNull(loading->loading.chunk.data, "Data already defined?");
assetfile_t *file = &loading->loading.chunk.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
uint8_t *data = memoryAllocate(file->size);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
assertTrue(
file->lastRead == file->size,
"Failed to read entire chunk file."
);
loading->loading.chunk.data = data;
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk(); errorOk();
} }
@@ -74,12 +31,141 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
switch(loading->loading.chunk.state) { switch(loading->loading.chunk.state) {
case ASSET_CHUNK_LOADING_STATE_INITIAL: case ASSET_CHUNK_LOADING_STATE_INITIAL:
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE; loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_JSON;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk(); errorOk();
case ASSET_CHUNK_LOADING_STATE_PARSE: case ASSET_CHUNK_LOADING_STATE_LOAD_JSON: {
break; // Lock the chunk's JSON as a sub-asset. The entry key is prefixed
// with "json:" to avoid a type-collision with the chunk entry
// itself (both share the same filename). The JSON loader reads
// the real file path supplied in the input.
char_t jsonKey[ASSET_FILE_NAME_MAX];
stringFormat(
jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name
);
assetloaderinput_t jsonInput;
memoryZero(&jsonInput, sizeof(jsonInput));
stringCopy(
jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
);
assetentry_t *jsonEntry = assetLock(
jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput
);
errorret_t ret = assetRequireLoaded(jsonEntry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorChain(ret);
}
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
yyjson_val *tilesVal = yyjson_obj_get(root, "tiles");
if(
!tilesVal || !yyjson_is_arr(tilesVal) ||
yyjson_arr_size(tilesVal) != CHUNK_TILE_COUNT
) {
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow(
"Chunk JSON 'tiles' must have exactly %d entries",
CHUNK_TILE_COUNT
);
}
out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t));
size_t tileIdx, tileMax;
yyjson_val *tileVal;
yyjson_arr_foreach(tilesVal, tileIdx, tileMax, tileVal) {
yyjson_val *shapeVal = yyjson_arr_get(tileVal, 0);
yyjson_val *zVal = yyjson_arr_get(tileVal, 1);
if(
!yyjson_is_arr(tileVal) || yyjson_arr_size(tileVal) != 2 ||
!yyjson_is_int(shapeVal) || !yyjson_is_int(zVal)
) {
memoryFree(out->tiles);
out->tiles = NULL;
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Chunk JSON tile entries must be [shape, z] arrays");
}
out->tiles[tileIdx] = (tile_t){
.shape = (tileshape_t)yyjson_get_int(shapeVal),
.z = (uint8_t)yyjson_get_int(zVal)
};
}
yyjson_val *meshesVal = yyjson_obj_get(root, "meshes");
out->meshCount = 0;
if(meshesVal && yyjson_is_arr(meshesVal)) {
size_t meshCount = yyjson_arr_size(meshesVal);
if(meshCount > CHUNK_MESH_COUNT_MAX) {
memoryFree(out->tiles);
out->tiles = NULL;
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow(
"Chunk JSON 'meshes' exceeds CHUNK_MESH_COUNT_MAX (%d)",
CHUNK_MESH_COUNT_MAX
);
}
size_t meshIdx, meshMax;
yyjson_val *meshVal;
yyjson_arr_foreach(meshesVal, meshIdx, meshMax, meshVal) {
yyjson_val *modelVal = yyjson_obj_get(meshVal, "model");
if(!modelVal || !yyjson_is_str(modelVal)) {
memoryFree(out->tiles);
out->tiles = NULL;
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Chunk JSON mesh entry missing 'model' string");
}
const char_t *modelStr = yyjson_get_str(modelVal);
size_t modelLen = yyjson_get_len(modelVal);
if(modelLen >= CHUNK_MESH_NAME_MAX) {
memoryFree(out->tiles);
out->tiles = NULL;
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow(
"Chunk JSON model path '%s' exceeds max length", modelStr
);
}
memoryCopy(out->modelNames[meshIdx], modelStr, modelLen + 1);
vec3 offset = { 0.0f, 0.0f, 0.0f };
yyjson_val *offsetVal = yyjson_obj_get(meshVal, "offset");
if(
offsetVal && yyjson_is_arr(offsetVal) &&
yyjson_arr_size(offsetVal) == 3
) {
size_t offIdx, offMax;
yyjson_val *offElem;
yyjson_arr_foreach(offsetVal, offIdx, offMax, offElem) {
if(yyjson_is_num(offElem)) {
offset[offIdx] = (float_t)yyjson_get_num(offElem);
}
}
}
glm_vec3_copy(offset, out->meshOffsets[meshIdx]);
}
out->meshCount = (uint8_t)meshCount;
}
assetUnlockEntry(jsonEntry);
if(out->meshCount == 0) {
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
loading->loading.chunk.modelIndex = 0;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS: case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
while(loading->loading.chunk.modelIndex < out->meshCount) { while(loading->loading.chunk.modelIndex < out->meshCount) {
@@ -111,129 +197,6 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
default: default:
errorOk(); errorOk();
} }
uint8_t *data = loading->loading.chunk.data;
assertNotNull(data, "Chunk data should have been loaded by now.");
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid chunk file header");
}
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
if(version != ASSET_CHUNK_FILE_VERSION) {
memoryFree(data);
assetLoaderErrorThrow(
loading, "Unsupported chunk file version %u", version
);
}
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(
out->meshCount <= CHUNK_MESH_COUNT_MAX,
"Chunk mesh count exceeds maximum."
);
for(uint8_t m = 0; m < out->meshCount; m++) {
uint8_t nameLen = 0;
while(
data[offset + nameLen] != '\0' &&
nameLen < CHUNK_MESH_NAME_MAX - 1
) {
nameLen++;
}
memoryCopy(out->modelNames[m], data + offset, nameLen);
out->modelNames[m][nameLen] = '\0';
offset += nameLen + 1;
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]);
}
out->entitySpawnCount = data[offset];
offset += sizeof(uint8_t);
assertTrue(
out->entitySpawnCount <= CHUNK_ENTITY_SPAWN_COUNT_MAX,
"Chunk entity spawn count exceeds maximum."
);
for(uint8_t s = 0; s < out->entitySpawnCount; s++) {
chunkentityspawn_t *spawn = &out->entitySpawns[s];
spawn->kind = (chunkentityspawnkind_t)data[offset];
offset += sizeof(uint8_t);
uint16_t a;
memoryCopy(&a, data + offset, sizeof(uint16_t));
a = endianLittleToHost16(a);
offset += sizeof(uint16_t);
uint8_t b = data[offset];
offset += sizeof(uint8_t);
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_ITEM) {
spawn->globalId = 0;
spawn->itemId = a;
spawn->itemQuantity = b;
} else {
spawn->globalId = a;
spawn->itemId = 0;
spawn->itemQuantity = 0;
}
spawn->position = assetChunkReadWorldPos(data, &offset);
}
out->areaSpawnCount = data[offset];
offset += sizeof(uint8_t);
assertTrue(
out->areaSpawnCount <= CHUNK_AREA_COUNT_MAX,
"Chunk area spawn count exceeds maximum."
);
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
chunkareaspawn_t *area = &out->areaSpawns[s];
area->min = assetChunkReadWorldPos(data, &offset);
area->max = assetChunkReadWorldPos(data, &offset);
uint16_t callbackId;
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
area->callbackId = endianLittleToHost16(callbackId);
offset += sizeof(uint16_t);
area->notify = data[offset];
offset += sizeof(uint8_t);
area->trigger = data[offset];
offset += sizeof(uint8_t);
}
memoryFree(data);
loading->loading.chunk.data = NULL;
if(out->meshCount == 0) {
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
loading->loading.chunk.modelIndex = 0;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
} }
errorret_t assetChunkDispose(assetentry_t *entry) { errorret_t assetChunkDispose(assetentry_t *entry) {
+21 -41
View File
@@ -9,8 +9,6 @@
#include "asset/assetfile.h" #include "asset/assetfile.h"
#include "rpg/overworld/chunk.h" #include "rpg/overworld/chunk.h"
#define ASSET_CHUNK_FILE_VERSION 5
typedef struct assetloading_s assetloading_t; typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
@@ -20,58 +18,27 @@ typedef struct {
typedef enum { typedef enum {
ASSET_CHUNK_LOADING_STATE_INITIAL, ASSET_CHUNK_LOADING_STATE_INITIAL,
ASSET_CHUNK_LOADING_STATE_READ_FILE, ASSET_CHUNK_LOADING_STATE_LOAD_JSON,
ASSET_CHUNK_LOADING_STATE_PARSE, ASSET_CHUNK_LOADING_STATE_LOAD_MODELS
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
ASSET_CHUNK_LOADING_STATE_DONE
} assetchunkloadingstate_t; } assetchunkloadingstate_t;
typedef struct { typedef struct {
assetfile_t file;
assetchunkloadingstate_t state; assetchunkloadingstate_t state;
uint8_t *data;
uint8_t modelIndex; uint8_t modelIndex;
} assetchunkloaderloading_t; } assetchunkloaderloading_t;
typedef enum {
CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
CHUNK_ENTITY_SPAWN_KIND_ITEM
} chunkentityspawnkind_t;
typedef struct {
chunkentityspawnkind_t kind;
uint16_t globalId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL.
uint16_t itemId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
uint8_t itemQuantity; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
worldpos_t position;
} chunkentityspawn_t;
typedef struct {
worldpos_t min;
worldpos_t max;
uint16_t callbackId; // Index into MAP_AREA_CALLBACK_LIST.
uint8_t notify;
uint8_t trigger;
} chunkareaspawn_t;
typedef struct { typedef struct {
tile_t *tiles; tile_t *tiles;
uint8_t meshCount; uint8_t meshCount;
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX]; char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX]; vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX]; assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
uint8_t entitySpawnCount;
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
uint8_t areaSpawnCount;
chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX];
} assetchunkoutput_t; } assetchunkoutput_t;
/** /**
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into * Asynchronous loader for chunk assets. No-op - the chunk's JSON file is
* the loading buffer so the sync phase can parse without blocking the * loaded via a JSON sub-asset in the sync phase (see assetChunkLoaderSync),
* main thread on I/O. * which handles its own async file I/O.
* *
* @param loading Loading information for the asset being loaded. * @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation. * @return Error code indicating success or failure of the load operation.
@@ -79,9 +46,22 @@ typedef struct {
errorret_t assetChunkLoaderAsync(assetloading_t *loading); errorret_t assetChunkLoaderAsync(assetloading_t *loading);
/** /**
* Synchronous loader for chunk assets. Validates the DCF binary previously * Synchronous loader for chunk assets. Locks and parses the chunk's JSON
* read by the async phase and populates the output assetchunkoutput_t with * file (tiles plus referenced model paths/offsets), then locks each
* tile data and model paths. * referenced model asset before marking the entry loaded.
*
* Expected JSON shape:
* {
* "tiles": [ [shape, z], ... ] // exactly CHUNK_TILE_COUNT entries,
* // one per (x, y) column, x-major
* "meshes": [
* { "model": "models/chunks/chunk_0_0_0_0.json",
* "offset": [0.0, 0.0, 0.0] }
* ]
* }
* "meshes" is optional; each entry's "offset" defaults to [0, 0, 0] if
* omitted. By convention mesh index 0 is the chunk's terrain and the
* rest are props (see sceneOverworldDrawChunksBase/Props).
* *
* @param loading Loading information for the asset being loaded. * @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation. * @return Error code indicating success or failure of the load operation.
+15 -4
View File
@@ -18,7 +18,10 @@ console_t CONSOLE;
void consoleInit(void) { void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t)); memoryZero(&CONSOLE, sizeof(console_t));
CONSOLE.visible = false; CONSOLE.visible = false;
threadMutexInit(&CONSOLE.printMutex);
#ifdef DUSK_CONSOLE_POSIX
threadMutexInit(&CONSOLE.printMutex);
#endif
} }
void consolePrint(const char_t *message, ...) { void consolePrint(const char_t *message, ...) {
@@ -29,14 +32,20 @@ void consolePrint(const char_t *message, ...) {
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args); int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
va_end(args); va_end(args);
threadMutexLock(&CONSOLE.printMutex); #ifdef DUSK_CONSOLE_POSIX
threadMutexLock(&CONSOLE.printMutex);
#endif
memoryMove( memoryMove(
CONSOLE.line[0], CONSOLE.line[0],
CONSOLE.line[1], CONSOLE.line[1],
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX (CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
); );
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1); memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
threadMutexUnlock(&CONSOLE.printMutex);
#ifdef DUSK_CONSOLE_POSIX
threadMutexUnlock(&CONSOLE.printMutex);
#endif
logDebug("%s\n", buffer); logDebug("%s\n", buffer);
} }
@@ -52,5 +61,7 @@ void consoleUpdate(void) {
} }
void consoleDispose(void) { void consoleDispose(void) {
threadMutexDispose(&CONSOLE.printMutex); #ifdef DUSK_CONSOLE_POSIX
threadMutexDispose(&CONSOLE.printMutex);
#endif
} }
+11 -5
View File
@@ -6,18 +6,24 @@
*/ */
#pragma once #pragma once
#include "consoledefs.h"
#include "error/error.h" #include "error/error.h"
#include "dusk.h" #include "dusk.h"
#include "thread/thread.h"
#define CONSOLE_LINE_MAX 512 #ifdef DUSK_CONSOLE_POSIX
#define CONSOLE_HISTORY_MAX 16 #include "thread/thread.h"
#define CONSOLE_EXEC_BUFFER_MAX 32 #include <poll.h>
#include <unistd.h>
#define CONSOLE_POSIX_POLL_RATE 75
#endif
typedef struct { typedef struct {
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX]; char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
bool_t visible; bool_t visible;
threadmutex_t printMutex;
#ifdef DUSK_CONSOLE_POSIX
threadmutex_t printMutex;
#endif
} console_t; } console_t;
extern console_t CONSOLE; extern console_t CONSOLE;
@@ -6,6 +6,7 @@
*/ */
#pragma once #pragma once
#include "ui/uielement.h"
extern uielement_t UI_ELEMENTS[]; #define CONSOLE_LINE_MAX 512
#define CONSOLE_HISTORY_MAX 16
#define CONSOLE_EXEC_BUFFER_MAX 32
+4 -8
View File
@@ -33,17 +33,13 @@ errorret_t displayInit(void) {
#ifdef displayPlatformInit #ifdef displayPlatformInit
errorChain(displayPlatformInit()); errorChain(displayPlatformInit());
#endif #endif
// Set initial state
errorChain(displaySetState((displaystate_t){ .flags = 0 })); errorChain(displaySetState((displaystate_t){ .flags = 0 }));
// Init the fixed textures
errorChain(textureInit( errorChain(textureInit(
&TEXTURE_WHITE, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT, &TEXTURE_WHITE, 4, 4,
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS } TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
)); ));
errorChain(textureInit( errorChain(textureInit(
&TEXTURE_TEST, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT, &TEXTURE_TEST, 4, 4,
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS } TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS }
)); ));
@@ -54,14 +50,14 @@ errorret_t displayInit(void) {
errorChain(planeInit()); errorChain(planeInit());
errorChain(capsuleInit()); errorChain(capsuleInit());
errorChain(triPrismInit()); errorChain(triPrismInit());
// Init the subsystems
errorChain(frameBufferInitBackBuffer()); errorChain(frameBufferInitBackBuffer());
errorChain(spriteBatchInit()); errorChain(spriteBatchInit());
errorChain(textInit()); errorChain(textInit());
errorChain(screenInit()); errorChain(screenInit());
// Setup initial shader with default values // Setup initial shader with default values
errorChain(shaderListInit()); errorChain(shaderListInit());
errorOk(); errorOk();
-1
View File
@@ -7,5 +7,4 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
text.c text.c
font.c
) )
-168
View File
@@ -1,168 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "font.h"
#include "util/memory.h"
#include "util/math.h"
#include "display/color.h"
font_t FONT_DEFAULT;
static texture_t FONT_DEFAULT_TEXTURE;
static tileset_t FONT_DEFAULT_TILESET;
const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
FONT_DEFAULT_TILE_HEIGHT
] = {
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00 }, // !
{ 0x00, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // "
{ 0x00, 0x14, 0x14, 0x3E, 0x14, 0x3E, 0x14, 0x14, 0x00, 0x00 }, // #
{ 0x00, 0x08, 0x1E, 0x28, 0x1C, 0x0A, 0x3C, 0x08, 0x00, 0x00 }, // $
{ 0x00, 0x00, 0x22, 0x24, 0x08, 0x12, 0x22, 0x00, 0x00, 0x00 }, // %
{ 0x00, 0x08, 0x14, 0x14, 0x1A, 0x24, 0x24, 0x1A, 0x00, 0x00 }, // &
{ 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '
{ 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x00, 0x00 }, // (
{ 0x00, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x10, 0x00, 0x00 }, // )
{ 0x00, 0x00, 0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00, 0x00, 0x00 }, // *
{ 0x00, 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, 0x00, 0x00 }, // +
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00 }, // ,
{ 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00 }, // -
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00 }, // .
{ 0x00, 0x04, 0x04, 0x08, 0x08, 0x08, 0x10, 0x10, 0x00, 0x00 }, // /
{ 0x00, 0x1C, 0x22, 0x26, 0x2A, 0x32, 0x22, 0x1C, 0x00, 0x00 }, // 0
{ 0x00, 0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // 1
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // 2
{ 0x00, 0x1C, 0x22, 0x02, 0x0C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 3
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x02, 0x02, 0x02, 0x00, 0x00 }, // 4
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 5
{ 0x00, 0x1C, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 6
{ 0x00, 0x3E, 0x02, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00 }, // 7
{ 0x00, 0x1C, 0x22, 0x22, 0x1C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 8
{ 0x00, 0x1C, 0x22, 0x22, 0x1E, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 9
{ 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00 }, // :
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ;
{ 0x00, 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00 }, // <
{ 0x00, 0x00, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00 }, // =
{ 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 }, // >
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x00, 0x08, 0x00, 0x00 }, // ?
{ 0x00, 0x1C, 0x26, 0x2A, 0x2A, 0x26, 0x20, 0x1C, 0x00, 0x00 }, // @
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x00, 0x00 }, // A
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // B
{ 0x00, 0x1C, 0x22, 0x20, 0x20, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // C
{ 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // D
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // E
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // F
{ 0x00, 0x1C, 0x22, 0x20, 0x2E, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // G
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x22, 0x00, 0x00 }, // H
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // I
{ 0x00, 0x02, 0x02, 0x02, 0x02, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // J
{ 0x00, 0x22, 0x24, 0x28, 0x30, 0x28, 0x24, 0x22, 0x00, 0x00 }, // K
{ 0x00, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // L
{ 0x00, 0x22, 0x36, 0x2A, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // M
{ 0x00, 0x22, 0x22, 0x32, 0x2A, 0x26, 0x22, 0x22, 0x00, 0x00 }, // N
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // O
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // P
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x06, 0x00 }, // Q
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x22, 0x00, 0x00 }, // R
{ 0x00, 0x1C, 0x22, 0x20, 0x1C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // S
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // T
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // U
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, 0x00 }, // V
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x2A, 0x36, 0x22, 0x00, 0x00 }, // W
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x14, 0x22, 0x22, 0x00, 0x00 }, // X
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // backslash (not drawn in source font)
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // _ (not drawn in source font)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ` (not drawn in source font)
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
{ 0x00, 0x02, 0x02, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // d
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x3E, 0x20, 0x1C, 0x00, 0x00 }, // e
{ 0x00, 0x0C, 0x12, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x00, 0x00 }, // f
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // g
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // h
{ 0x00, 0x08, 0x00, 0x18, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // i
{ 0x00, 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x02, 0x22, 0x1C }, // j
{ 0x00, 0x20, 0x20, 0x22, 0x24, 0x38, 0x24, 0x22, 0x00, 0x00 }, // k
{ 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // l
{ 0x00, 0x00, 0x00, 0x3C, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00 }, // m
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // n
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // o
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x20, 0x20 }, // p
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x02 }, // q
{ 0x00, 0x00, 0x00, 0x2C, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 }, // r
{ 0x00, 0x00, 0x00, 0x1E, 0x20, 0x1C, 0x02, 0x3C, 0x00, 0x00 }, // s
{ 0x00, 0x10, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // t
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // u
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x14, 0x08, 0x00, 0x00 }, // v
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x2A, 0x2A, 0x14, 0x00, 0x00 }, // w
{ 0x00, 0x00, 0x00, 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, 0x00 }, // x
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // | (not drawn in source font)
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ~ (not drawn in source font)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
};
errorret_t fontDefaultInit(void) {
const int32_t width = (int32_t)mathNextPowTwo(
FONT_DEFAULT_COLUMNS * FONT_DEFAULT_TILE_WIDTH
);
const int32_t height = (int32_t)mathNextPowTwo(
FONT_DEFAULT_ROWS * FONT_DEFAULT_TILE_HEIGHT
);
color_t *pixels = memoryAllocate(sizeof(color_t) * width * height);
memoryZero(pixels, sizeof(color_t) * width * height);
for(uint16_t i = 0; i < FONT_DEFAULT_TILE_COUNT; i++) {
const uint16_t tileX = (i % FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_WIDTH;
const uint16_t tileY = (i / FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_HEIGHT;
for(uint8_t row = 0; row < FONT_DEFAULT_TILE_HEIGHT; row++) {
const uint8_t bits = FONT_DEFAULT_GLYPHS[i][row];
for(uint8_t col = 0; col < FONT_DEFAULT_TILE_WIDTH; col++) {
if(!((bits >> (FONT_DEFAULT_TILE_WIDTH - 1 - col)) & 1)) continue;
pixels[((tileY + row) * width) + (tileX + col)] = COLOR_WHITE;
}
}
}
FONT_DEFAULT_TILESET.tileWidth = FONT_DEFAULT_TILE_WIDTH;
FONT_DEFAULT_TILESET.tileHeight = FONT_DEFAULT_TILE_HEIGHT;
FONT_DEFAULT_TILESET.columns = FONT_DEFAULT_COLUMNS;
FONT_DEFAULT_TILESET.rows = FONT_DEFAULT_ROWS;
FONT_DEFAULT_TILESET.tileCount = FONT_DEFAULT_TILE_COUNT;
FONT_DEFAULT_TILESET.uv[0] = (float_t)FONT_DEFAULT_TILE_WIDTH / (float_t)width;
FONT_DEFAULT_TILESET.uv[1] = (float_t)FONT_DEFAULT_TILE_HEIGHT / (float_t)height;
const texturedata_t data = { .rgbaColors = pixels };
errorret_t textureResult = textureInit(
&FONT_DEFAULT_TEXTURE, width, height, TEXTURE_FORMAT_RGBA, data
);
memoryFree(pixels);
errorChain(textureResult);
FONT_DEFAULT.texture = &FONT_DEFAULT_TEXTURE;
FONT_DEFAULT.tileset = &FONT_DEFAULT_TILESET;
errorOk();
}
errorret_t fontDefaultDispose(void) {
errorChain(textureDispose(&FONT_DEFAULT_TEXTURE));
FONT_DEFAULT.texture = NULL;
FONT_DEFAULT.tileset = NULL;
errorOk();
}
-49
View File
@@ -6,7 +6,6 @@
*/ */
#pragma once #pragma once
#include "error/error.h"
#include "display/texture/texture.h" #include "display/texture/texture.h"
#include "display/texture/tileset.h" #include "display/texture/tileset.h"
@@ -14,51 +13,3 @@ typedef struct {
texture_t *texture; texture_t *texture;
tileset_t *tileset; tileset_t *tileset;
} font_t; } font_t;
/**
* Pixel width/height of a single default-font glyph tile.
*/
#define FONT_DEFAULT_TILE_WIDTH 6
#define FONT_DEFAULT_TILE_HEIGHT 10
/** Grid layout of the generated default-font texture, in tiles. */
#define FONT_DEFAULT_COLUMNS 16
#define FONT_DEFAULT_ROWS 6
/**
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
* TEXT_CHAR_START ('!') plus a couple of unused trailing tiles.
*/
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
extern font_t FONT_DEFAULT;
/**
* Hard coded bitmap data for the built-in default font. Indexed
* [glyph][row], where glyph 0 corresponds to TEXT_CHAR_START ('!') and
* glyphs run consecutively through the printable ASCII range. Each row
* byte holds FONT_DEFAULT_TILE_WIDTH bit flags, one per pixel column:
* bit (FONT_DEFAULT_TILE_WIDTH - 1) is the leftmost pixel and bit 0 is
* the rightmost; 1 means the pixel is set, 0 means it is not.
*/
extern const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
FONT_DEFAULT_TILE_HEIGHT
];
/**
* Builds the default font's texture + tileset directly from
* FONT_DEFAULT_GLYPHS, without going through the asset system - so the
* engine always has a usable font to render with regardless of whether
* asset loading (e.g. the packed .dsk archive) succeeds.
*
* @return Either an error or success result.
*/
errorret_t fontDefaultInit(void);
/**
* Disposes of the default font created by fontDefaultInit().
*
* @return Either an error or success result.
*/
errorret_t fontDefaultDispose(void);
+21 -2
View File
@@ -9,15 +9,34 @@
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "asset/asset.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "display/shader/shaderunlit.h" #include "display/shader/shaderunlit.h"
font_t FONT_DEFAULT;
errorret_t textInit(void) { errorret_t textInit(void) {
errorChain(fontDefaultInit()); assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA };
assetentry_t *entryTexture = assetLock(
"ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
);
assetentry_t *entryTileset = assetLock(
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
);
errorChain(assetRequireLoaded(entryTexture));
errorChain(assetRequireLoaded(entryTileset));
FONT_DEFAULT.texture = &entryTexture->data.texture;
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
errorOk(); errorOk();
} }
errorret_t textDispose(void) { errorret_t textDispose(void) {
errorChain(fontDefaultDispose()); FONT_DEFAULT.texture = NULL;
FONT_DEFAULT.tileset = NULL;
assetUnlock("ui/minogram.png");
assetUnlock("ui/minogram.dtf");
errorOk(); errorOk();
} }
+2
View File
@@ -12,6 +12,8 @@
#define TEXT_CHAR_START '!' #define TEXT_CHAR_START '!'
extern font_t FONT_DEFAULT;
/** /**
* Initializes the text system. * Initializes the text system.
* *
+2 -2
View File
@@ -12,7 +12,7 @@
#include "display/display.h" #include "display/display.h"
texture_t TEXTURE_WHITE; texture_t TEXTURE_WHITE;
color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = { color_t TEXTURE_WHITE_PIXELS[4*4] = {
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
@@ -20,7 +20,7 @@ color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
}; };
texture_t TEXTURE_TEST; texture_t TEXTURE_TEST;
color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = { color_t TEXTURE_TEST_PIXELS[4*4] = {
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK,
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
+2 -5
View File
@@ -17,9 +17,6 @@
#error "textureDisposePlatform should not be defined." #error "textureDisposePlatform should not be defined."
#endif #endif
#define TEXTURE_FIXED_WIDTH 4
#define TEXTURE_FIXED_HEIGHT 4
typedef textureformatplatform_t textureformat_t; typedef textureformatplatform_t textureformat_t;
typedef textureplatform_t texture_t; typedef textureplatform_t texture_t;
@@ -32,9 +29,9 @@ typedef union texturedata_u {
} texturedata_t; } texturedata_t;
extern texture_t TEXTURE_WHITE; extern texture_t TEXTURE_WHITE;
extern color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT]; extern color_t TEXTURE_WHITE_PIXELS[4*4];
extern texture_t TEXTURE_TEST; extern texture_t TEXTURE_TEST;
extern color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT]; extern color_t TEXTURE_TEST_PIXELS[4*4];
/** /**
* Initializes a texture. * Initializes a texture.
+11 -17
View File
@@ -10,18 +10,17 @@
#include "time/time.h" #include "time/time.h"
#include "input/input.h" #include "input/input.h"
#include "locale/localemanager.h" #include "locale/localemanager.h"
#include "rpg/item/item.h"
#include "rpg/rpg.h" #include "rpg/rpg.h"
#include "display/display.h" #include "display/display.h"
#include "scene/scene.h" #include "scene/scene.h"
#include "asset/asset.h" #include "asset/asset.h"
#include "ui/ui.h" #include "ui/ui.h"
#include "assert/assert.h" #include "assert/assert.h"
#ifdef DUSK_NETWORK #include "network/network.h"
#include "network/network.h"
#endif
#include "system/system.h" #include "system/system.h"
#include "console/console.h" #include "console/console.h"
#include "save/savemanager.h"\ #include "save/save.h"
engine_t ENGINE; engine_t ENGINE;
@@ -39,14 +38,13 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(systemInit()); errorChain(systemInit());
errorChain(inputInit()); errorChain(inputInit());
errorChain(assetInit()); errorChain(assetInit());
errorChain(saveManagerInit()); // errorChain(saveInit());
errorChain(localeManagerInit()); errorChain(localeManagerInit());
errorChain(itemInit());
errorChain(displayInit()); errorChain(displayInit());
errorChain(uiInit()); errorChain(uiInit());
errorChain(rpgInit()); errorChain(rpgInit());
#ifdef DUSK_NETWORK errorChain(networkInit());
errorChain(networkInit());
#endif
errorChain(sceneInit()); errorChain(sceneInit());
consolePrint("Engine initialized"); consolePrint("Engine initialized");
@@ -57,17 +55,15 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
consolePrint("Assertions real"); consolePrint("Assertions real");
#endif #endif
sceneSet(SCENE_TYPE_INITIAL); sceneSet(SCENE_TYPE_OVERWORLD);
errorOk(); errorOk();
} }
errorret_t engineUpdate(void) { errorret_t engineUpdate(void) {
// Order here is important. // Order here is important.
#ifdef DUSK_NETWORK errorChain(networkUpdate());
errorChain(networkUpdate());
#endif
errorChain(saveManagerUpdate());
timeUpdate(); timeUpdate();
inputUpdate(); inputUpdate();
consoleUpdate(); consoleUpdate();
@@ -88,15 +84,13 @@ void engineExit(void) {
errorret_t engineDispose(void) { errorret_t engineDispose(void) {
errorChain(sceneDispose()); errorChain(sceneDispose());
#ifdef DUSK_NETWORK errorChain(networkDispose());
errorChain(networkDispose());
#endif
errorChain(rpgDispose()); errorChain(rpgDispose());
localeManagerDispose(); localeManagerDispose();
errorChain(uiDispose()); errorChain(uiDispose());
consoleDispose(); consoleDispose();
errorChain(displayDispose()); errorChain(displayDispose());
errorChain(saveManagerDispose()); // errorChain(saveDispose());
errorChain(assetDispose()); errorChain(assetDispose());
errorOk(); errorOk();
+17
View File
@@ -17,11 +17,26 @@ input_t INPUT;
errorret_t inputInit(void) { errorret_t inputInit(void) {
memoryZero(&INPUT, sizeof(input_t)); memoryZero(&INPUT, sizeof(input_t));
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) { for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
INPUT.actions[i].action = (inputaction_t)i; INPUT.actions[i].action = (inputaction_t)i;
INPUT.actions[i].lastValue = 0.0f; INPUT.actions[i].lastValue = 0.0f;
INPUT.actions[i].currentValue = 0.0f; INPUT.actions[i].currentValue = 0.0f;
eventInit(
&INPUT.actions[i].onPressed,
INPUT.actions[i].onPressedCallbacks,
INPUT.actions[i].onPressedUsers,
INPUT_ACTION_CALLBACK_COUNT_MAX
);
eventInit(
&INPUT.actions[i].onReleased,
INPUT.actions[i].onReleasedCallbacks,
INPUT.actions[i].onReleasedUsers,
INPUT_ACTION_CALLBACK_COUNT_MAX
);
} }
#ifdef inputInitPlatform #ifdef inputInitPlatform
@@ -92,6 +107,8 @@ void inputUpdate(void) {
inputactiondata_t *act = &INPUT.actions[i]; inputactiondata_t *act = &INPUT.actions[i];
bool_t isDown = act->currentValue > 0.0f; bool_t isDown = act->currentValue > 0.0f;
bool_t wasDown = act->lastValue > 0.0f; bool_t wasDown = act->lastValue > 0.0f;
if(isDown && !wasDown) eventInvoke(&act->onPressed, act);
if(!isDown && wasDown) eventInvoke(&act->onReleased, act);
} }
} }
+8
View File
@@ -10,9 +10,17 @@
#include "inputbutton.h" #include "inputbutton.h"
#include "inputaction.h" #include "inputaction.h"
#define INPUT_LISTENER_PRESSED_MAX 16
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
#define INPUT_DEADZONE_DEFAULT 0.1f
typedef struct { typedef struct {
inputactiondata_t actions[INPUT_ACTION_COUNT]; inputactiondata_t actions[INPUT_ACTION_COUNT];
inputplatform_t platform; inputplatform_t platform;
/** User-configured gamepad axis deadzone (0.0f to 1.0f). */
float_t deadzone;
} input_t; } input_t;
extern input_t INPUT; extern input_t INPUT;
+8
View File
@@ -8,6 +8,7 @@
#pragma once #pragma once
#include "time/time.h" #include "time/time.h"
#include "input/inputactiondefs.h" #include "input/inputactiondefs.h"
#include "event/event.h"
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4 #define INPUT_ACTION_CALLBACK_COUNT_MAX 4
@@ -20,6 +21,13 @@ typedef struct {
float_t lastDynamicValue; float_t lastDynamicValue;
float_t currentDynamicValue; float_t currentDynamicValue;
#endif #endif
eventcallback_t onPressedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
void *onPressedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
event_t onPressed;
eventcallback_t onReleasedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
void *onReleasedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
event_t onReleased;
} inputactiondata_t; } inputactiondata_t;
/** /**
+4 -18
View File
@@ -13,31 +13,17 @@ typedef struct {
const char_t *file; const char_t *file;
} localeinfo_t; } localeinfo_t;
static const localeinfo_t LOCALE_INFO_EN_US = { static const localeinfo_t LOCALE_EN_US = {
.name = "en-US", .name = "en-US",
.file = "locale/en_US.po", .file = "locale/en_US.po",
}; };
static const localeinfo_t LOCALE_INFO_JP_JP = { static const localeinfo_t LOCALE_JP_JP = {
.name = "ja-JP", .name = "ja-JP",
.file = "locale/jp_JP.po", .file = "locale/jp_JP.po",
}; };
static const localeinfo_t LOCALE_INFO_ES_MX = { static const localeinfo_t LOCALE_ES_MX = {
.name = "es-MX", .name = "es-MX",
.file = "locale/es_MX.po", .file = "locale/es_MX.po",
}; };
static const localeinfo_t * const LOCALE_INFO_LIST[] = {
&LOCALE_INFO_EN_US,
&LOCALE_INFO_JP_JP,
&LOCALE_INFO_ES_MX
};
#define LOCALE_INFO_LIST_COUNT ( \
sizeof(LOCALE_INFO_LIST) / sizeof(LOCALE_INFO_LIST[0]) \
)
#define LOCALE_DEFAULT LOCALE_INFO_EN_US
// EOF
+1 -13
View File
@@ -7,22 +7,13 @@
#include "localemanager.h" #include "localemanager.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "ui/ui.h"
#include "system/system.h"
#include "console/console.h"
localemanager_t LOCALE; localemanager_t LOCALE;
errorret_t localeManagerInit() { errorret_t localeManagerInit() {
memoryZero(&LOCALE, sizeof(localemanager_t)); memoryZero(&LOCALE, sizeof(localemanager_t));
errorChain(localeManagerSetLocale(&LOCALE_EN_US));
// TODO: Set locale based on system locale.
const localeinfo_t *locale = systemGetLocale();
errorChain(localeManagerSetLocale(locale));
consolePrint("Locale set to: %s", locale->name);
errorOk(); errorOk();
} }
@@ -39,9 +30,6 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
assetEntryLock(LOCALE.entry); assetEntryLock(LOCALE.entry);
errorChain(assetRequireLoaded(LOCALE.entry)); errorChain(assetRequireLoaded(LOCALE.entry));
// TODO : Trigger UI update.
errorChain(uiUpdateTranslations());
errorOk(); errorOk();
} }
+3 -3
View File
@@ -7,7 +7,7 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include "locale/localemanager.h" #include "localemanager.h"
#include "locale/localeinfo.h" #include "locale/localeinfo.h"
#include "asset/asset.h" #include "asset/asset.h"
@@ -20,14 +20,14 @@ extern localemanager_t LOCALE;
/** /**
* Initialize the locale system. * Initialize the locale system.
* *
* @return An error code if a failure occurs. * @return An error code if a failure occurs.
*/ */
errorret_t localeManagerInit(); errorret_t localeManagerInit();
/** /**
* Set the current locale. * Set the current locale.
* *
* @param locale The locale to set. * @param locale The locale to set.
* @return An error code if a failure occurs. * @return An error code if a failure occurs.
*/ */
-3
View File
@@ -10,6 +10,3 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
battlefighter.c battlefighter.c
party.c party.c
) )
# Subdirs
add_subdirectory(testbattle)
+58 -161
View File
@@ -8,7 +8,6 @@
#include "battle.h" #include "battle.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "rpg/cutscene/cutscenesystem.h"
battle_t BATTLE; battle_t BATTLE;
@@ -52,8 +51,10 @@ void battleStart(
BATTLE.fleeAvailable = fleeAvailable; BATTLE.fleeAvailable = fleeAvailable;
BATTLE.result = BATTLE_RESULT_NONE; BATTLE.result = BATTLE_RESULT_NONE;
BATTLE.round = 1; BATTLE.round = 1;
BATTLE.turnIndex = 0;
battleBuildTurnOrder(true);
battleSetState(BATTLE_STATE_OPENING); BATTLE.active = true;
} }
void battleDispose(void) { void battleDispose(void) {
@@ -61,9 +62,9 @@ void battleDispose(void) {
} }
battlefighter_t *battleGetCurrentFighter(void) { battlefighter_t *battleGetCurrentFighter(void) {
if(BATTLE.state != BATTLE_STATE_PLAYER_SELECTION) return NULL; if(!BATTLE.active) return NULL;
if(BATTLE.selectionIndex >= BATTLE.executionCount) return NULL; if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
return &BATTLE.fighters[BATTLE.executionOrder[BATTLE.selectionIndex]]; return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
} }
uint8_t battleGetAliveCount(const battlefighterteam_t team) { uint8_t battleGetAliveCount(const battlefighterteam_t team) {
@@ -91,105 +92,91 @@ void battleResolveAttack(
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD; 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) { battleresult_t battleCheckResult(void) {
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result; if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) { if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
battleSetResult(BATTLE_RESULT_LOSS); BATTLE.result = BATTLE_RESULT_LOSS;
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) { } else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
battleSetResult(BATTLE_RESULT_WIN); BATTLE.result = BATTLE_RESULT_WIN;
} }
return BATTLE.result; return BATTLE.result;
} }
void battleQueueAction(
const uint8_t fighterIndex,
const battleactiontype_t type,
const uint8_t targetIndex
) {
battleaction_t *action = &BATTLE.actions[fighterIndex];
action->type = type;
action->targetIndex = targetIndex;
if(BATTLE.onActionDecided != NULL) {
BATTLE.onActionDecided(&BATTLE.fighters[fighterIndex], action);
}
}
void battlePlayerAttack(const uint8_t targetIndex) { void battlePlayerAttack(const uint8_t targetIndex) {
battlefighter_t *fighter = battleGetCurrentFighter(); battlefighter_t *attacker = battleGetCurrentFighter();
if(fighter == NULL) return; if(attacker == NULL) return;
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return; if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
if(!battleFighterIsAlive(&BATTLE.fighters[targetIndex])) return;
battleQueueAction(fighter->id, BATTLE_ACTION_ATTACK, targetIndex); battlefighter_t *defender = &BATTLE.fighters[targetIndex];
BATTLE.selectionIndex++; if(!battleFighterIsAlive(defender)) return;
battleAdvanceSelection();
battleResolveAttack(attacker, defender);
battleCheckResult();
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
} }
void battlePlayerFlee(void) { void battlePlayerFlee(void) {
battlefighter_t *fighter = battleGetCurrentFighter(); battlefighter_t *fighter = battleGetCurrentFighter();
if(fighter == NULL) return; if(fighter == NULL) return;
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
if(!BATTLE.fleeAvailable) return; if(!BATTLE.fleeAvailable) return;
battleSetResult(BATTLE_RESULT_FLED); BATTLE.result = BATTLE_RESULT_FLED;
} }
void battleUpdate(void) { void battleUpdate(void) {
if(BATTLE.state == BATTLE_STATE_NONE) return; if(!BATTLE.active) return;
if(BATTLE.state == BATTLE_STATE_ENDED) return; if(BATTLE.result != BATTLE_RESULT_NONE) return;
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE) return;
switch(BATTLE.state) { battlefighter_t *current = battleGetCurrentFighter();
case BATTLE_STATE_OPENING: if(current == NULL) return;
battleSetState(BATTLE_STATE_PRE_ROUND);
break;
case BATTLE_STATE_PRE_ROUND: if(!battleFighterIsAlive(current)) {
battleUpdatePreRound(); battleNextTurn();
break; return;
case BATTLE_STATE_AI_SELECTION:
battleUpdateAiSelection();
break;
case BATTLE_STATE_MOVES_EXECUTING:
battleUpdateMovesExecuting();
break;
case BATTLE_STATE_POST_ROUND:
battleUpdatePostRound();
break;
default:
// BATTLE_STATE_PLAYER_SELECTION: waits on battlePlayerAttack/Flee.
// BATTLE_STATE_NONE/ENDED: handled above.
break;
} }
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 battleBuildExecutionOrder(const bool_t applyEncounterBias) { void battleBuildTurnOrder(const bool_t applyEncounterBias) {
BATTLE.executionCount = 0; BATTLE.turnCount = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) { for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue; if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
BATTLE.executionOrder[BATTLE.executionCount++] = i; BATTLE.turnOrder[BATTLE.turnCount++] = i;
} }
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX. // Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
for(uint8_t i = 1; i < BATTLE.executionCount; i++) { for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
const uint8_t key = BATTLE.executionOrder[i]; const uint8_t key = BATTLE.turnOrder[i];
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed; const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
int8_t j = (int8_t)i - 1; int8_t j = (int8_t)i - 1;
while( while(
j >= 0 && j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
BATTLE.fighters[BATTLE.executionOrder[j]].stats.speed < keySpeed
) { ) {
BATTLE.executionOrder[j + 1] = BATTLE.executionOrder[j]; BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
j--; j--;
} }
BATTLE.executionOrder[j + 1] = key; BATTLE.turnOrder[j + 1] = key;
} }
if(!applyEncounterBias) return; if(!applyEncounterBias) return;
@@ -205,16 +192,16 @@ void battleMoveTeamFirst(const battlefighterteam_t team) {
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX]; uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
uint8_t count = 0; uint8_t count = 0;
for(uint8_t i = 0; i < BATTLE.executionCount; i++) { for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
if(BATTLE.fighters[BATTLE.executionOrder[i]].team != team) continue; if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
sorted[count++] = BATTLE.executionOrder[i]; sorted[count++] = BATTLE.turnOrder[i];
} }
for(uint8_t i = 0; i < BATTLE.executionCount; i++) { for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
if(BATTLE.fighters[BATTLE.executionOrder[i]].team == team) continue; if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
sorted[count++] = BATTLE.executionOrder[i]; sorted[count++] = BATTLE.turnOrder[i];
} }
memoryCopy(BATTLE.executionOrder, sorted, sizeof(uint8_t) * BATTLE.executionCount); memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
} }
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) { battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
@@ -234,93 +221,3 @@ battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
return weakest; return weakest;
} }
void battleSetState(const battlestate_t next) {
const battlestate_t previous = BATTLE.state;
BATTLE.state = next;
if(BATTLE.onStateChanged != NULL) BATTLE.onStateChanged(previous, next);
}
void battleSetResult(const battleresult_t result) {
BATTLE.result = result;
battleSetState(BATTLE_STATE_ENDED);
}
bool_t battleFighterNeedsDecision(
const uint8_t fighterIndex,
const battlefightercontroller_t controller
) {
return battleFighterIsAlive(&BATTLE.fighters[fighterIndex])
&& BATTLE.fighters[fighterIndex].controller == controller
&& BATTLE.actions[fighterIndex].type == BATTLE_ACTION_NONE;
}
void battleAdvanceSelection(void) {
while(BATTLE.selectionIndex < BATTLE.executionCount) {
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.selectionIndex];
if(
battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_PLAYER)
) {
return;
}
BATTLE.selectionIndex++;
}
battleSetState(BATTLE_STATE_AI_SELECTION);
}
void battleUpdatePreRound(void) {
// No need to clear BATTLE.actions here: every living fighter's action is
// unconditionally reset to BATTLE_ACTION_NONE as it's processed in
// battleUpdateMovesExecuting, and round 1 starts pre-zeroed by
// battleInit(). Clearing it here would also wipe out any action a
// cutscene force-queued while parked at BATTLE_STATE_PRE_ROUND.
battleBuildExecutionOrder(BATTLE.round == 1);
BATTLE.selectionIndex = 0;
battleSetState(BATTLE_STATE_PLAYER_SELECTION);
battleAdvanceSelection();
}
void battleUpdateAiSelection(void) {
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
const uint8_t fighterIndex = BATTLE.executionOrder[i];
if(
!battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_AI)
) continue;
battlefighter_t *target =
battleAIChooseTarget(&BATTLE.fighters[fighterIndex]);
if(target == NULL) continue;
battleQueueAction(fighterIndex, BATTLE_ACTION_ATTACK, target->id);
}
BATTLE.executionIndex = 0;
battleSetState(BATTLE_STATE_MOVES_EXECUTING);
}
void battleUpdateMovesExecuting(void) {
if(BATTLE.executionIndex >= BATTLE.executionCount) {
battleSetState(BATTLE_STATE_POST_ROUND);
return;
}
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.executionIndex++];
battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
if(!battleFighterIsAlive(fighter)) return;
battleaction_t *action = &BATTLE.actions[fighterIndex];
if(action->type == BATTLE_ACTION_ATTACK) {
battlefighter_t *target = &BATTLE.fighters[action->targetIndex];
if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target);
}
action->type = BATTLE_ACTION_NONE;
battleCheckResult();
}
void battleUpdatePostRound(void) {
BATTLE.round++;
battleSetState(BATTLE_STATE_PRE_ROUND);
}
+36 -155
View File
@@ -27,68 +27,19 @@ typedef enum {
BATTLE_RESULT_COUNT BATTLE_RESULT_COUNT
} battleresult_t; } battleresult_t;
// Where BATTLE currently is within a round. A cutscene can pause progression
// (CUTSCENE_PAUSE_BATTLE) and use CUTSCENE_BATTLE_WAIT_STATE to synchronize
// with any of these, or CUTSCENE_BATTLE_FORCE_ACTION to decide a fighter's
// action ahead of PLAYER_SELECTION/AI_SELECTION reaching them.
typedef enum {
BATTLE_STATE_NONE, // Battle inactive.
BATTLE_STATE_OPENING, // Entered once by battleStart().
BATTLE_STATE_PRE_ROUND, // Rebuilds execution order, clears the action queue.
BATTLE_STATE_PLAYER_SELECTION, // Waits on battlePlayerAttack/Flee.
BATTLE_STATE_AI_SELECTION, // Auto-queues every undecided AI fighter.
BATTLE_STATE_MOVES_EXECUTING, // Resolves one queued action per update.
BATTLE_STATE_POST_ROUND, // Round wrap-up; loops back to PRE_ROUND.
BATTLE_STATE_ENDED, // Terminal for WIN/LOSS/FLED alike -- see BATTLE.result.
BATTLE_STATE_COUNT
} battlestate_t;
typedef enum {
BATTLE_ACTION_NONE, // No action decided yet for this fighter this round.
BATTLE_ACTION_ATTACK,
BATTLE_ACTION_COUNT
} battleactiontype_t;
typedef struct { typedef struct {
battleactiontype_t type; bool_t active;
uint8_t targetIndex; // Meaningful for BATTLE_ACTION_ATTACK.
} battleaction_t;
typedef void (*battlestatechangedcallback_t)(
const battlestate_t previous,
const battlestate_t next
);
typedef void (*battleactiondecidedcallback_t)(
const battlefighter_t *fighter,
const battleaction_t *action
);
typedef struct {
battlestate_t state;
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX]; battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
battleaction_t actions[BATTLE_FIGHTER_COUNT_MAX];
battleencountertype_t encounterType; battleencountertype_t encounterType;
bool_t fleeAvailable; bool_t fleeAvailable;
battleresult_t result; battleresult_t result;
// Fighter indices (into fighters[]), sorted for the current round. // Fighter indices (into fighters[]), sorted for the current round.
uint8_t executionOrder[BATTLE_FIGHTER_COUNT_MAX]; uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
uint8_t executionCount; uint8_t turnCount;
uint8_t executionIndex; uint8_t turnIndex;
uint16_t round; uint16_t round;
// Cursor into executionOrder used by BATTLE_STATE_PLAYER_SELECTION to find
// the next player-controlled fighter that still needs a decision.
uint8_t selectionIndex;
battlestatechangedcallback_t onStateChanged;
battleactiondecidedcallback_t onActionDecided;
} battle_t; } battle_t;
extern battle_t BATTLE; extern battle_t BATTLE;
@@ -126,10 +77,11 @@ battlefighter_t *battleAddFighter(
); );
/** /**
* Starts the battle: enters BATTLE_STATE_OPENING and marks the battle * Starts the battle: builds the opening turn order (biased by
* active. Call once every fighter has been added via battleAddFighter. * 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 execution order. * @param encounterType Determines the opening round's turn order.
* @param fleeAvailable Whether the party may attempt to flee this battle. * @param fleeAvailable Whether the party may attempt to flee this battle.
*/ */
void battleStart( void battleStart(
@@ -143,11 +95,10 @@ void battleStart(
void battleDispose(void); void battleDispose(void);
/** /**
* Returns the fighter currently awaiting a player decision. * Returns the fighter whose turn it currently is.
* *
* @return Pointer to the fighter awaiting a decision, or NULL if the battle * @return Pointer to the active fighter, or NULL if the battle isn't
* isn't in BATTLE_STATE_PLAYER_SELECTION or every player-controlled * active or has no living fighters left to act.
* fighter has already decided.
*/ */
battlefighter_t *battleGetCurrentFighter(void); battlefighter_t *battleGetCurrentFighter(void);
@@ -174,68 +125,61 @@ void battleResolveAttack(
); );
/** /**
* Checks whether the battle has been won or lost, transitioning to * Ends the current fighter's turn and advances to the next fighter in
* BATTLE_STATE_ENDED and updating BATTLE.result if so. Does nothing if a * the turn order, starting a new round (rebuilding turn order purely by
* result has already been set (e.g. by a successful flee). * 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. * @return The battle's current result.
*/ */
battleresult_t battleCheckResult(void); battleresult_t battleCheckResult(void);
/** /**
* Queues an action for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING * Submits the current fighter's attack against a target, if it is
* reaches them this round, overwriting any action already queued for that * currently a player-controlled fighter's turn. Resolves the attack,
* fighter. Fires BATTLE.onActionDecided. * checks for a battle result, and advances the turn.
*
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
* @param type The type of action to perform.
* @param targetIndex Index into BATTLE.fighters of the target, meaningful
* for BATTLE_ACTION_ATTACK.
*/
void battleQueueAction(
const uint8_t fighterIndex,
const battleactiontype_t type,
const uint8_t targetIndex
);
/**
* Submits the currently-selecting fighter's attack against a target, if the
* battle is in BATTLE_STATE_PLAYER_SELECTION and awaiting a decision.
* Queues the action and advances the selection cursor.
* *
* @param targetIndex Index into BATTLE.fighters of the target. * @param targetIndex Index into BATTLE.fighters of the target.
*/ */
void battlePlayerAttack(const uint8_t targetIndex); void battlePlayerAttack(const uint8_t targetIndex);
/** /**
* Submits a flee attempt for the currently-selecting fighter, if the battle * Submits a flee attempt for the current fighter's turn, if it is
* is in BATTLE_STATE_PLAYER_SELECTION and fleeing is available for this * currently a player-controlled fighter's turn and fleeing is
* battle. Always succeeds, ending the battle with BATTLE_RESULT_FLED. * available for this battle. Always succeeds, ending the battle with
* BATTLE_RESULT_FLED.
*/ */
void battlePlayerFlee(void); void battlePlayerFlee(void);
/** /**
* Updates the battle simulation for one frame, dispatching on BATTLE.state. * Updates the battle simulation for one frame: resolves the current
* No-op if the battle isn't active, has already ended, or * fighter's turn automatically if AI-controlled, otherwise waits for a
* CUTSCENE_PAUSE_BATTLE is set. * player action via battlePlayerAttack/battlePlayerFlee. No-op if the
* battle isn't active or already has a result.
*/ */
void battleUpdate(void); void battleUpdate(void);
/** /**
* Rebuilds BATTLE.executionOrder/executionCount from every currently living * Rebuilds BATTLE.turnOrder/turnCount from every currently living
* fighter, sorted by speed descending. * fighter, sorted by speed descending.
* *
* @param applyEncounterBias If true, reorders the freshly speed-sorted * @param applyEncounterBias If true, reorders the freshly speed-sorted
* queue so BATTLE.encounterType's favoured team goes first (used only * queue so BATTLE.encounterType's favoured team goes first (used only
* for the opening round). * for the opening round).
*/ */
void battleBuildExecutionOrder(const bool_t applyEncounterBias); void battleBuildTurnOrder(const bool_t applyEncounterBias);
/** /**
* Stably partitions BATTLE.executionOrder so every fighter on the given team * Stably partitions BATTLE.turnOrder so every fighter on the given team
* comes first, preserving each side's relative (speed-sorted) order. * comes first, preserving each side's relative (speed-sorted) order.
* *
* @param team The team to move to the front of the execution order. * @param team The team to move to the front of the turn order.
*/ */
void battleMoveTeamFirst(const battlefighterteam_t team); void battleMoveTeamFirst(const battlefighterteam_t team);
@@ -248,66 +192,3 @@ void battleMoveTeamFirst(const battlefighterteam_t team);
* living fighters. * living fighters.
*/ */
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter); battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
/**
* Sets BATTLE.state and fires BATTLE.onStateChanged with the previous and
* new state.
*
* @param next The state to transition to.
*/
void battleSetState(const battlestate_t next);
/**
* Sets BATTLE.result and transitions to BATTLE_STATE_ENDED.
*
* @param result The result to end the battle with.
*/
void battleSetResult(const battleresult_t result);
/**
* Checks whether a fighter is a live, undecided candidate for the given
* controller -- i.e. whether PLAYER_SELECTION or AI_SELECTION should still
* be deciding an action for it this round.
*
* @param fighterIndex Index into BATTLE.fighters to check.
* @param controller The controller PLAYER_SELECTION/AI_SELECTION is
* currently deciding for.
* @return True if the fighter is alive, matches controller, and has no
* action queued yet.
*/
bool_t battleFighterNeedsDecision(
const uint8_t fighterIndex,
const battlefightercontroller_t controller
);
/**
* Advances BATTLE.selectionIndex to the next player-controlled fighter that
* still needs a decision, or transitions to BATTLE_STATE_AI_SELECTION once
* none remain.
*/
void battleAdvanceSelection(void);
/**
* Handles BATTLE_STATE_PRE_ROUND: rebuilds the execution order and moves on
* to BATTLE_STATE_PLAYER_SELECTION, positioning the selection cursor.
*/
void battleUpdatePreRound(void);
/**
* Handles BATTLE_STATE_AI_SELECTION: queues an attack for every undecided
* AI-controlled fighter, then moves on to BATTLE_STATE_MOVES_EXECUTING.
*/
void battleUpdateAiSelection(void);
/**
* Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued action from
* BATTLE.executionOrder per call, or transitions to BATTLE_STATE_POST_ROUND
* once every fighter this round has been processed.
*/
void battleUpdateMovesExecuting(void);
/**
* Handles BATTLE_STATE_POST_ROUND: advances BATTLE.round and transitions
* back to BATTLE_STATE_PRE_ROUND.
*/
void battleUpdatePostRound(void);
@@ -1,43 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "testbattle.h"
#include "rpg/battle/battle.h"
#include "scene/scene.h"
void testBattleStart(void) {
battleInit();
const battlefighterstats_t allyOneStats =
{ .attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0 };
const battlefighterstats_t allyTwoStats =
{ .attack = 8, .defense = 4, .magic = 0, .speed = 8, .luck = 0 };
const battlefighterstats_t enemyOneStats =
{ .attack = 6, .defense = 3, .magic = 0, .speed = 6, .luck = 0 };
const battlefighterstats_t enemyTwoStats =
{ .attack = 7, .defense = 3, .magic = 0, .speed = 5, .luck = 0 };
battleAddFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
allyOneStats, 30, 10
);
battleAddFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
allyTwoStats, 25, 10
);
battleAddFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
enemyOneStats, 20, 5
);
battleAddFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
enemyTwoStats, 20, 5
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
sceneSet(SCENE_TYPE_BATTLE);
}
@@ -1,16 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
/**
* TEMPORARY test hook: sets up a hardcoded mock battle and switches to the
* battle scene, so the battle scene (camera/fighters/HUD) can be seen and
* played without a real encounter trigger yet.
*/
void testBattleStart(void);
+1
View File
@@ -6,6 +6,7 @@
# Sources # Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
cutscene.c
cutscenesystem.c cutscenesystem.c
) )
+444
View File
@@ -0,0 +1,444 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "cutscene.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/json/assetjsonloader.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entitytype.h"
#include "rpg/entity/entitydir.h"
#include "ui/rpg/uiemoji.h"
#include "yyjson.h"
static cutscenejsoncacheentry_t CUTSCENE_JSON_CACHE[CUTSCENE_JSON_CACHE_MAX];
static uint32_t CUTSCENE_JSON_CACHE_COUNT;
errorret_t cutsceneJsonParsePause(yyjson_val *val, cutscenepause_t *outPause) {
assertNotNull(outPause, "Output pause pointer cannot be NULL");
if(!val || !yyjson_is_str(val)) {
errorThrow("Cutscene pause value must be a string");
}
const char_t *str = yyjson_get_str(val);
if(stringEquals(str, "NONE")) {
*outPause = CUTSCENE_PAUSE_NONE;
} else if(stringEquals(str, "DEFAULT")) {
*outPause = CUTSCENE_PAUSE_DEFAULT;
} else if(stringEquals(str, "ALL")) {
*outPause = CUTSCENE_PAUSE_ALL;
} else {
errorThrow("Unknown cutscene pause value '%s'", str);
}
errorOk();
}
errorret_t cutsceneJsonParseEntityIndex(yyjson_val *val, uint8_t *outIndex) {
assertNotNull(outIndex, "Output entity index pointer cannot be NULL");
if(val && yyjson_is_str(val)) {
const char_t *str = yyjson_get_str(val);
if(stringEquals(str, "interact")) {
*outIndex = CUTSCENE_ENTITY_INTERACT;
} else if(stringEquals(str, "interacted")) {
*outIndex = CUTSCENE_ENTITY_INTERACTED;
} else if(stringEquals(str, "lastCreated")) {
*outIndex = CUTSCENE_ENTITY_LAST_CREATED;
} else {
errorThrow("Unknown entity index sentinel '%s'", str);
}
errorOk();
}
if(!val || !yyjson_is_int(val)) {
errorThrow("Entity index must be a number or sentinel string");
}
*outIndex = (uint8_t)yyjson_get_int(val);
errorOk();
}
errorret_t cutsceneJsonParseWorldPos(yyjson_val *val, worldpos_t *outPos) {
assertNotNull(outPos, "Output position pointer cannot be NULL");
if(!val || !yyjson_is_arr(val) || yyjson_arr_size(val) != 3) {
errorThrow("Position must be a [x, y, z] array");
}
worldunit_t comps[3];
size_t idx, max;
yyjson_val *elem;
yyjson_arr_foreach(val, idx, max, elem) {
if(!yyjson_is_num(elem)) {
errorThrow("Position elements must be numbers");
}
comps[idx] = (worldunit_t)yyjson_get_num(elem);
}
*outPos = (worldpos_t){ comps[0], comps[1], comps[2] };
errorOk();
}
errorret_t cutsceneItemCreateFromJson(
yyjson_val *obj,
cutsceneitem_t *outItem,
worldpos_t *waypoints,
const uint8_t waypointsMax
) {
assertNotNull(obj, "Cutscene item JSON object cannot be NULL");
assertNotNull(outItem, "Output item pointer cannot be NULL");
yyjson_val *typeVal = yyjson_obj_get(obj, "type");
if(!typeVal || !yyjson_is_str(typeVal)) {
errorThrow("Cutscene item JSON missing 'type' string");
}
const char_t *typeStr = yyjson_get_str(typeVal);
memoryZero(outItem, sizeof(cutsceneitem_t));
if(stringEquals(typeStr, "text")) {
yyjson_val *textVal = yyjson_obj_get(obj, "text");
if(!textVal || !yyjson_is_str(textVal)) {
errorThrow("Cutscene 'text' item missing 'text' string");
}
outItem->type = CUTSCENE_ITEM_TYPE_TEXT;
stringCopy(
outItem->text.text, yyjson_get_str(textVal), CUTSCENE_TEXT_MAX_CHARS
);
} else if(stringEquals(typeStr, "textMini")) {
yyjson_val *textVal = yyjson_obj_get(obj, "text");
if(!textVal || !yyjson_is_str(textVal)) {
errorThrow("Cutscene 'textMini' item missing 'text' string");
}
worldpos_t pos;
errorChain(
cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos)
);
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
if(!durVal || !yyjson_is_num(durVal)) {
errorThrow("Cutscene 'textMini' item missing 'duration' number");
}
outItem->type = CUTSCENE_ITEM_TYPE_TEXT_MINI;
stringCopy(
outItem->textMini.text, yyjson_get_str(textVal),
CUTSCENE_TEXT_MINI_MAX_CHARS
);
outItem->textMini.position[0] = (float_t)pos.x;
outItem->textMini.position[1] = (float_t)pos.y;
outItem->textMini.position[2] = (float_t)pos.z;
outItem->textMini.duration = (float_t)yyjson_get_num(durVal);
} else if(stringEquals(typeStr, "textMiniHide")) {
yyjson_val *indexVal = yyjson_obj_get(obj, "index");
if(!indexVal || !yyjson_is_int(indexVal)) {
errorThrow("Cutscene 'textMiniHide' item missing 'index' number");
}
outItem->type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE;
outItem->textMiniHide.index = (uint8_t)yyjson_get_int(indexVal);
} else if(stringEquals(typeStr, "wait")) {
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
if(!durVal || !yyjson_is_num(durVal)) {
errorThrow("Cutscene 'wait' item missing 'duration' number");
}
outItem->type = CUTSCENE_ITEM_TYPE_WAIT;
outItem->wait = (float_t)yyjson_get_num(durVal);
} else if(stringEquals(typeStr, "entityAdd")) {
yyjson_val *entityTypeVal = yyjson_obj_get(obj, "entityType");
if(!entityTypeVal || !yyjson_is_str(entityTypeVal)) {
errorThrow("Cutscene 'entityAdd' item missing 'entityType' string");
}
const char_t *entityTypeStr = yyjson_get_str(entityTypeVal);
entitytype_t entityType;
if(stringEquals(entityTypeStr, "player")) {
entityType = ENTITY_TYPE_PLAYER;
} else if(stringEquals(entityTypeStr, "npc")) {
entityType = ENTITY_TYPE_NPC;
} else if(stringEquals(entityTypeStr, "item")) {
entityType = ENTITY_TYPE_ITEM;
} else {
errorThrow(
"Cutscene 'entityAdd' has unknown entityType '%s'", entityTypeStr
);
}
worldpos_t pos;
errorChain(
cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos)
);
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_ADD;
outItem->entityAdd.entityType = entityType;
outItem->entityAdd.position = pos;
} else if(stringEquals(typeStr, "entityRemove")) {
uint8_t entityIndex;
errorChain(cutsceneJsonParseEntityIndex(
yyjson_obj_get(obj, "entityIndex"), &entityIndex
));
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_REMOVE;
outItem->entityRemove.entityIndex = entityIndex;
} else if(stringEquals(typeStr, "entityTurn")) {
uint8_t entityIndex;
errorChain(cutsceneJsonParseEntityIndex(
yyjson_obj_get(obj, "entityIndex"), &entityIndex
));
yyjson_val *dirVal = yyjson_obj_get(obj, "direction");
if(!dirVal || !yyjson_is_str(dirVal)) {
errorThrow("Cutscene 'entityTurn' item missing 'direction' string");
}
const char_t *dirStr = yyjson_get_str(dirVal);
entitydir_t direction;
if(stringEquals(dirStr, "north")) {
direction = ENTITY_DIR_NORTH;
} else if(stringEquals(dirStr, "east")) {
direction = ENTITY_DIR_EAST;
} else if(stringEquals(dirStr, "south")) {
direction = ENTITY_DIR_SOUTH;
} else if(stringEquals(dirStr, "west")) {
direction = ENTITY_DIR_WEST;
} else {
errorThrow("Cutscene 'entityTurn' has unknown direction '%s'", dirStr);
}
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_TURN;
outItem->entityTurn.entityIndex = entityIndex;
outItem->entityTurn.direction = direction;
} else if(stringEquals(typeStr, "entityWalkTo")) {
uint8_t entityIndex;
errorChain(cutsceneJsonParseEntityIndex(
yyjson_obj_get(obj, "entityIndex"), &entityIndex
));
yyjson_val *positionsVal = yyjson_obj_get(obj, "positions");
if(!positionsVal || !yyjson_is_arr(positionsVal)) {
errorThrow("Cutscene 'entityWalkTo' item missing 'positions' array");
}
size_t count = yyjson_arr_size(positionsVal);
if(count == 0 || count > waypointsMax) {
errorThrow(
"Cutscene 'entityWalkTo' 'positions' must have 1-%d entries",
waypointsMax
);
}
assertNotNull(waypoints, "Waypoint storage cannot be NULL");
size_t idx, max;
yyjson_val *posVal;
yyjson_arr_foreach(positionsVal, idx, max, posVal) {
errorChain(cutsceneJsonParseWorldPos(posVal, &waypoints[idx]));
}
bool_t walkAround = true;
yyjson_val *walkAroundVal = yyjson_obj_get(obj, "walkAround");
if(walkAroundVal && yyjson_is_bool(walkAroundVal)) {
walkAround = yyjson_get_bool(walkAroundVal);
}
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO;
outItem->entityWalkTo.entityIndex = entityIndex;
outItem->entityWalkTo.positions = waypoints;
outItem->entityWalkTo.count = (uint8_t)count;
outItem->entityWalkTo.walkAround = walkAround;
} else if(stringEquals(typeStr, "entityWalkToEntity")) {
uint8_t entityIndex, targetEntityIndex;
errorChain(cutsceneJsonParseEntityIndex(
yyjson_obj_get(obj, "entityIndex"), &entityIndex
));
errorChain(cutsceneJsonParseEntityIndex(
yyjson_obj_get(obj, "targetEntityIndex"), &targetEntityIndex
));
yyjson_val *offsetXVal = yyjson_obj_get(obj, "offsetX");
yyjson_val *offsetYVal = yyjson_obj_get(obj, "offsetY");
if(
!offsetXVal || !yyjson_is_num(offsetXVal) ||
!offsetYVal || !yyjson_is_num(offsetYVal)
) {
errorThrow(
"Cutscene 'entityWalkToEntity' item missing "
"'offsetX'/'offsetY' numbers"
);
}
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY;
outItem->entityWalkToEntity.entityIndex = entityIndex;
outItem->entityWalkToEntity.targetEntityIndex = targetEntityIndex;
outItem->entityWalkToEntity.offsetX =
(worldunit_t)yyjson_get_num(offsetXVal);
outItem->entityWalkToEntity.offsetY =
(worldunit_t)yyjson_get_num(offsetYVal);
} else if(stringEquals(typeStr, "entityTeleport")) {
uint8_t entityIndex;
errorChain(cutsceneJsonParseEntityIndex(
yyjson_obj_get(obj, "entityIndex"), &entityIndex
));
worldpos_t pos;
errorChain(
cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos)
);
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT;
outItem->entityTeleport.entityIndex = entityIndex;
outItem->entityTeleport.target = pos;
} else if(stringEquals(typeStr, "emoji")) {
uint8_t entityIndex;
errorChain(cutsceneJsonParseEntityIndex(
yyjson_obj_get(obj, "entityIndex"), &entityIndex
));
yyjson_val *emojiTypeVal = yyjson_obj_get(obj, "emojiType");
if(!emojiTypeVal || !yyjson_is_str(emojiTypeVal)) {
errorThrow("Cutscene 'emoji' item missing 'emojiType' string");
}
const char_t *emojiTypeStr = yyjson_get_str(emojiTypeVal);
uiemojitype_t emojiType;
if(stringEquals(emojiTypeStr, "question")) {
emojiType = UI_EMOJI_QUESTION_MARK;
} else if(stringEquals(emojiTypeStr, "exclamation")) {
emojiType = UI_EMOJI_EXCLAMATION_MARK;
} else {
errorThrow(
"Cutscene 'emoji' has unknown emojiType '%s'", emojiTypeStr
);
}
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
if(!durVal || !yyjson_is_num(durVal)) {
errorThrow("Cutscene 'emoji' item missing 'duration' number");
}
outItem->type = CUTSCENE_ITEM_TYPE_EMOJI;
outItem->emoji.entityIndex = entityIndex;
outItem->emoji.emojiType = emojiType;
outItem->emoji.duration = (float_t)yyjson_get_num(durVal);
} else if(stringEquals(typeStr, "shake")) {
yyjson_val *amountVal = yyjson_obj_get(obj, "amount");
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
if(
!amountVal || !yyjson_is_int(amountVal) ||
!durVal || !yyjson_is_num(durVal)
) {
errorThrow("Cutscene 'shake' item missing 'amount'/'duration' numbers");
}
outItem->type = CUTSCENE_ITEM_TYPE_SHAKE;
outItem->shake.amount = (uint8_t)yyjson_get_int(amountVal);
outItem->shake.duration = (float_t)yyjson_get_num(durVal);
} else if(stringEquals(typeStr, "setPause")) {
cutscenepause_t pause;
errorChain(
cutsceneJsonParsePause(yyjson_obj_get(obj, "pause"), &pause)
);
outItem->type = CUTSCENE_ITEM_TYPE_SET_PAUSE;
outItem->setPause = pause;
} else {
errorThrow("Cutscene item JSON has unknown 'type': %s", typeStr);
}
errorOk();
}
errorret_t cutsceneGetByName(
const char_t *name, const cutscene_t **outCutscene
) {
assertNotNull(name, "Cutscene name cannot be NULL");
assertNotNull(outCutscene, "Output cutscene pointer cannot be NULL");
assertStrLenMax(name, CUTSCENE_JSON_NAME_MAX, "Cutscene name too long");
for(uint32_t i = 0; i < CUTSCENE_JSON_CACHE_COUNT; i++) {
if(!stringEquals(CUTSCENE_JSON_CACHE[i].name, name)) continue;
*outCutscene = &CUTSCENE_JSON_CACHE[i].cutscene;
errorOk();
}
if(CUTSCENE_JSON_CACHE_COUNT >= CUTSCENE_JSON_CACHE_MAX) {
errorThrow(
"Too many cutscenes loaded: exceeds CUTSCENE_JSON_CACHE_MAX (%d)",
CUTSCENE_JSON_CACHE_MAX
);
}
char_t path[CUTSCENE_JSON_NAME_MAX + 24];
stringFormat(path, sizeof(path), "cutscene/%s.json", name);
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL);
errorret_t ret = assetRequireLoaded(entry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(entry);
errorChain(ret);
}
yyjson_val *root = yyjson_doc_get_root(entry->data.json);
cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT;
yyjson_val *pauseVal = yyjson_obj_get(root, "pause");
if(pauseVal) {
errorret_t pauseRet = cutsceneJsonParsePause(pauseVal, &pause);
if(errorIsNotOk(pauseRet)) {
assetUnlockEntry(entry);
errorChain(pauseRet);
}
}
yyjson_val *itemsVal = yyjson_obj_get(root, "items");
if(!itemsVal || !yyjson_is_arr(itemsVal)) {
assetUnlockEntry(entry);
errorThrow("Cutscene '%s' JSON missing 'items' array", name);
}
size_t itemCount = yyjson_arr_size(itemsVal);
if(itemCount == 0 || itemCount > CUTSCENE_JSON_ITEM_COUNT_MAX) {
assetUnlockEntry(entry);
errorThrow(
"Cutscene '%s' 'items' must have 1-%d entries",
name, CUTSCENE_JSON_ITEM_COUNT_MAX
);
}
cutscenejsoncacheentry_t *cache =
&CUTSCENE_JSON_CACHE[CUTSCENE_JSON_CACHE_COUNT];
memoryZero(cache, sizeof(cutscenejsoncacheentry_t));
stringCopy(cache->name, name, CUTSCENE_JSON_NAME_MAX);
size_t idx, max;
yyjson_val *itemVal;
yyjson_arr_foreach(itemsVal, idx, max, itemVal) {
errorret_t itemRet = cutsceneItemCreateFromJson(
itemVal, &cache->items[idx], cache->waypoints[idx],
CUTSCENE_JSON_WAYPOINT_COUNT_MAX
);
if(errorIsNotOk(itemRet)) {
assetUnlockEntry(entry);
errorChain(itemRet);
}
}
assetUnlockEntry(entry);
cache->cutscene.items = cache->items;
cache->cutscene.itemCount = (uint8_t)itemCount;
cache->cutscene.pause = pause;
cache->cutscene.dataSize = 0;
CUTSCENE_JSON_CACHE_COUNT++;
*outCutscene = &cache->cutscene;
errorOk();
}
+119 -24
View File
@@ -9,6 +9,8 @@
#include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenepause.h" #include "rpg/cutscene/cutscenepause.h"
typedef struct yyjson_val yyjson_val;
typedef struct cutscene_s { typedef struct cutscene_s {
const cutsceneitem_t *items; const cutsceneitem_t *items;
uint8_t itemCount; uint8_t itemCount;
@@ -162,35 +164,13 @@ typedef struct cutscene_s {
.shake = { .amount = AMOUNT, .duration = DURATION } \ .shake = { .amount = AMOUNT, .duration = DURATION } \
} }
// Waits until BATTLE.state reaches STATE. Put this BEFORE
// CUTSCENE_SET_PAUSE(CUTSCENE_PAUSE_BATTLE), not after -- pausing first
// freezes BATTLE.state wherever it already is, so it would never reach
// STATE on its own to satisfy the wait. Waiting unpaused, then pausing the
// moment it's satisfied, catches the battle right at STATE before it can
// advance further.
#define CUTSCENE_BATTLE_WAIT_STATE(STATE) \
{ \
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, \
.battleWaitState = { .state = STATE } \
}
// Immediately queues an attack for FIGHTER_INDEX against TARGET_INDEX,
// bypassing normal player/AI selection for that fighter this round.
#define CUTSCENE_BATTLE_FORCE_ACTION(FIGHTER_INDEX, TARGET_INDEX) \
{ \
.type = CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION, \
.battleForceAction = { \
.fighterIndex = FIGHTER_INDEX, .targetIndex = TARGET_INDEX \
} \
}
#define CUTSCENE_SET_PAUSE(FLAGS) \ #define CUTSCENE_SET_PAUSE(FLAGS) \
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) } { .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
#define CUTSCENE_ITEM_GIVE(ITEM_ID, QUANTITY) \ #define CUTSCENE_ITEM_GIVE(ITEM_NAME, QUANTITY) \
{ \ { \
.type = CUTSCENE_ITEM_TYPE_ITEM_GIVE, \ .type = CUTSCENE_ITEM_TYPE_ITEM_GIVE, \
.itemGive = { .item = ITEM_ID, .quantity = QUANTITY } \ .itemGive = { .itemName = ITEM_NAME, .quantity = QUANTITY } \
} }
// Runs all listed items simultaneously and waits until all are done. // Runs all listed items simultaneously and waits until all are done.
@@ -252,3 +232,118 @@ typedef struct cutscene_s {
), \ ), \
CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \ CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \
CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED) CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED)
#define CUTSCENE_JSON_NAME_MAX 32
#define CUTSCENE_JSON_ITEM_COUNT_MAX 32
#define CUTSCENE_JSON_WAYPOINT_COUNT_MAX 8
#define CUTSCENE_JSON_CACHE_MAX 16
// One slot of the cutsceneGetByName cache: a parsed cutscene plus the
// backing storage its items point into (items themselves, and the
// waypoint lists any "entityWalkTo" items reference).
typedef struct {
char_t name[CUTSCENE_JSON_NAME_MAX];
cutscene_t cutscene;
cutsceneitem_t items[CUTSCENE_JSON_ITEM_COUNT_MAX];
worldpos_t waypoints
[CUTSCENE_JSON_ITEM_COUNT_MAX][CUTSCENE_JSON_WAYPOINT_COUNT_MAX];
} cutscenejsoncacheentry_t;
/**
* Parses a cutscene pause value ("NONE", "DEFAULT" or "ALL") from a
* yyjson string value.
*
* @param val The yyjson value to parse.
* @param outPause Output pointer, set to the parsed pause flags.
* @return Any error that occurs (missing/invalid/unknown value).
*/
errorret_t cutsceneJsonParsePause(yyjson_val *val, cutscenepause_t *outPause);
/**
* Parses an entityIndex-shaped yyjson value - a raw number, or one of
* "interact", "interacted", "lastCreated" - into a raw uint8_t index or
* the matching CUTSCENE_ENTITY_* sentinel (see cutscenesystem.h).
*
* @param val The yyjson value to parse.
* @param outIndex Output pointer, set to the parsed index.
* @return Any error that occurs (missing/invalid/unknown value).
*/
errorret_t cutsceneJsonParseEntityIndex(yyjson_val *val, uint8_t *outIndex);
/**
* Parses a [x, y, z] yyjson array into a worldpos_t.
*
* @param val The yyjson value to parse.
* @param outPos Output pointer, set to the parsed position.
* @return Any error that occurs (missing/invalid value).
*/
errorret_t cutsceneJsonParseWorldPos(yyjson_val *val, worldpos_t *outPos);
/**
* Parses a single cutscene item from a yyjson object into outItem.
* "type" selects the item shape (required):
* { "type": "text", "text": "Hello!" }
* { "type": "textMini", "text": "Hi", "position": [x, y, z],
* "duration": 3.0 }
* { "type": "textMiniHide", "index": 0 }
* { "type": "wait", "duration": 1.5 }
* { "type": "entityAdd", "entityType": "npc", "position": [x, y, z] }
* { "type": "entityRemove", "entityIndex": 0 }
* { "type": "entityTurn", "entityIndex": 0, "direction": "south" }
* { "type": "entityWalkTo", "entityIndex": 0,
* "positions": [[x, y, z], ...], "walkAround": true }
* { "type": "entityWalkToEntity", "entityIndex": 0,
* "targetEntityIndex": 1, "offsetX": 1, "offsetY": 0 }
* { "type": "entityTeleport", "entityIndex": 0, "position": [x, y, z] }
* { "type": "emoji", "entityIndex": 0, "emojiType": "exclamation",
* "duration": 2.0 }
* { "type": "shake", "amount": 2, "duration": 0.5 }
* { "type": "setPause", "pause": "ALL" }
* "entityIndex"/"targetEntityIndex" accept a raw number, or one of
* "interact", "interacted", "lastCreated" for the matching
* CUTSCENE_ENTITY_* sentinel. "entityType" uses the same strings as
* entityCreateFromJson ("player", "npc", "item"); "direction" is one of
* "north"/"east"/"south"/"west"; "emojiType" is "question" or
* "exclamation"; "pause" is "NONE", "DEFAULT" or "ALL".
*
* Not supported (would need persistent string/array storage or function
* pointers this parser doesn't provide): itemGive, concurrent, fade,
* map area items, callbacks, nested cutscene references.
*
* @param obj The yyjson object describing a single cutscene item.
* @param outItem Output pointer, filled with the parsed item.
* @param waypoints Backing storage for an "entityWalkTo" item's waypoint
* list; must remain valid for as long as the parsed item is used.
* Unused (may be NULL) for any other item type.
* @param waypointsMax Capacity of waypoints.
* @return Any error that occurs (missing/invalid/unknown fields).
*/
errorret_t cutsceneItemCreateFromJson(
yyjson_val *obj,
cutsceneitem_t *outItem,
worldpos_t *waypoints,
const uint8_t waypointsMax
);
/**
* Loads (or returns the cached result of an earlier load of) the named
* cutscene from assets/cutscene/<name>.json, parsing each entry of its
* "items" array with cutsceneItemCreateFromJson. Parsed once per name
* and cached for the lifetime of the process; holds at most
* CUTSCENE_JSON_CACHE_MAX distinct names, each with at most
* CUTSCENE_JSON_ITEM_COUNT_MAX items.
*
* File shape:
* { "pause": "DEFAULT", "items": [ { "type": "text", ... }, ... ] }
* "pause" is optional (one of "NONE", "DEFAULT", "ALL"; defaults to
* "DEFAULT" when absent).
*
* @param name The cutscene's file name (without extension), under
* assets/cutscene/.
* @param outCutscene Output pointer, set to the loaded cutscene.
* @return Any error that occurs (missing/malformed file, cache full,
* unsupported/invalid item).
*/
errorret_t cutsceneGetByName(
const char_t *name, const cutscene_t **outCutscene
);
+1 -3
View File
@@ -14,13 +14,11 @@ typedef uint8_t cutscenepause_t;
#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0)) #define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0))
#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1)) #define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1))
#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2)) #define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2))
#define CUTSCENE_PAUSE_BATTLE ((cutscenepause_t)(1 << 3))
#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \ #define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \ CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \
)) ))
#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \ #define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD | \ CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD \
CUTSCENE_PAUSE_BATTLE \
)) ))
@@ -6,6 +6,4 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
cutscenestartbattle.c cutscenestartbattle.c
cutscenebattlewaitstate.c
cutscenebattleforceaction.c
) )
@@ -1,25 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
void cutsceneBattleForceActionStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
battleQueueAction(
item->battleForceAction.fighterIndex, BATTLE_ACTION_ATTACK,
item->battleForceAction.targetIndex
);
}
bool_t cutsceneBattleForceActionUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -1,42 +0,0 @@
/**
* 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"
typedef struct {
uint8_t fighterIndex;
uint8_t targetIndex;
} cutscenebattleforceaction_t;
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
/**
* Starts a battle force-action step: immediately queues an attack for the
* given fighter against the given target, bypassing normal player/AI
* selection for that fighter this round.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneBattleForceActionStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a battle force-action step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneBattleForceActionUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -1,15 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
bool_t cutsceneBattleWaitStateUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return BATTLE.state == item->battleWaitState.state;
}
@@ -1,30 +0,0 @@
/**
* 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"
typedef struct {
battlestate_t state;
} cutscenebattlewaitstate_t;
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
/**
* Updates a battle wait-state step, completing once BATTLE.state reaches
* the watched state. Has no Start callback -- there's nothing to do until
* the state is actually reached.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once BATTLE.state equals the watched state.
*/
bool_t cutsceneBattleWaitStateUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -118,15 +118,6 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
[CUTSCENE_ITEM_TYPE_SHAKE] = { [CUTSCENE_ITEM_TYPE_SHAKE] = {
.init = cutsceneShakeStart, .init = cutsceneShakeStart,
.update = cutsceneShakeUpdate .update = cutsceneShakeUpdate
},
[CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE] = {
.update = cutsceneBattleWaitStateUpdate
},
[CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = {
.init = cutsceneBattleForceActionStart,
.update = cutsceneBattleForceActionUpdate
} }
}; };
@@ -27,8 +27,6 @@
#include "maparea/cutscenemaparearemove.h" #include "maparea/cutscenemaparearemove.h"
#include "maparea/cutscenemapareawait.h" #include "maparea/cutscenemapareawait.h"
#include "battle/cutscenestartbattle.h" #include "battle/cutscenestartbattle.h"
#include "battle/cutscenebattlewaitstate.h"
#include "battle/cutscenebattleforceaction.h"
typedef struct cutscene_s cutscene_t; typedef struct cutscene_s cutscene_t;
@@ -57,8 +55,6 @@ typedef enum {
CUTSCENE_ITEM_TYPE_START_BATTLE, CUTSCENE_ITEM_TYPE_START_BATTLE,
CUTSCENE_ITEM_TYPE_EMOJI, CUTSCENE_ITEM_TYPE_EMOJI,
CUTSCENE_ITEM_TYPE_SHAKE, CUTSCENE_ITEM_TYPE_SHAKE,
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE,
CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION,
CUTSCENE_ITEM_TYPE_COUNT CUTSCENE_ITEM_TYPE_COUNT
} cutsceneitemtype_t; } cutsceneitemtype_t;
@@ -89,8 +85,6 @@ struct cutsceneitem_s {
cutscenestartbattle_t startBattle; cutscenestartbattle_t startBattle;
cutsceneemoji_t emoji; cutsceneemoji_t emoji;
cutsceneshake_t shake; cutsceneshake_t shake;
cutscenebattlewaitstate_t battleWaitState;
cutscenebattleforceaction_t battleForceAction;
}; };
}; };
@@ -9,7 +9,7 @@
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "rpg/entity/entitypathstep.h" #include "rpg/entity/entitypathstep.h"
#include "rpg/overworld/map.h" #include "rpg/overworld/chunk.h"
void cutsceneEntityWalkToEntityStart( void cutsceneEntityWalkToEntityStart(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -35,7 +35,7 @@ bool_t cutsceneEntityWalkToEntityUpdate(
}; };
worldunit_t z; worldunit_t z;
if(mapGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z; if(chunkGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
return entityPathStep(entity, dest, true); return entityPathStep(entity, dest, true);
} }
@@ -6,6 +6,7 @@
*/ */
#include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/item/item.h"
#include "rpg/item/itemgive.h" #include "rpg/item/itemgive.h"
#include "ui/rpg/textbox/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
@@ -13,7 +14,8 @@ void cutsceneItemGiveStart(
const cutsceneitem_t *item, const cutsceneitem_t *item,
cutsceneitemdata_t *data cutsceneitemdata_t *data
) { ) {
itemGive(item->itemGive.item, item->itemGive.quantity); itemid_t itemId = itemGetIdByName(item->itemGive.itemName);
itemGive(itemId, item->itemGive.quantity);
} }
bool_t cutsceneItemGiveUpdate( bool_t cutsceneItemGiveUpdate(
@@ -6,13 +6,13 @@
*/ */
#pragma once #pragma once
#include "rpg/item/item.h" #include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t; typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t; typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct { typedef struct {
itemid_t item; const char_t *itemName;
uint8_t quantity; uint8_t quantity;
} cutsceneitemgive_t; } cutsceneitemgive_t;
@@ -11,21 +11,4 @@
CUTSCENE(TEST_ONE, 0, DEFAULT, CUTSCENE(TEST_ONE, 0, DEFAULT,
CUTSCENE_TEXT("Test One."), CUTSCENE_TEXT("Test One."),
);
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),
// CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACTED, 8, 2, 0),
// ),
// CUTSCENE_ITEM_GIVE(ITEM_ID_POTATO, 3),
// CUTSCENE_ENTITY_REMOVE(CUTSCENE_ENTITY_INTERACT),
CUTSCENE_TEXT("Done."),
); );
+1 -2
View File
@@ -15,5 +15,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(anim) add_subdirectory(anim)
add_subdirectory(interact) add_subdirectory(interact)
add_subdirectory(npc) add_subdirectory(npc)
add_subdirectory(item) add_subdirectory(item)
add_subdirectory(global)
+2 -2
View File
@@ -6,7 +6,7 @@
*/ */
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "rpg/overworld/map.h" #include "rpg/overworld/chunk.h"
#include "rpg/overworld/tile.h" #include "rpg/overworld/tile.h"
#include "time/time.h" #include "time/time.h"
#include "entityanimwalk.h" #include "entityanimwalk.h"
@@ -19,7 +19,7 @@ const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
}; };
float_t entityAnimTileZOffset(const worldpos_t pos) { float_t entityAnimTileZOffset(const worldpos_t pos) {
return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f; return tileShapeIsRamp(chunkGetTile(pos).shape) ? 0.5f : 0.0f;
} }
void entityAnimUpdate(entity_t *entity) { void entityAnimUpdate(entity_t *entity) {
+164 -21
View File
@@ -8,13 +8,14 @@
#include "entity.h" #include "entity.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h"
#include "time/time.h" #include "time/time.h"
#include "util/math.h" #include "util/math.h"
#include "console/console.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/maparea.h" #include "rpg/overworld/maparea.h"
#include "rpg/overworld/chunk.h" #include "rpg/overworld/chunk.h"
#include "rpg/overworld/tile.h" #include "rpg/overworld/tile.h"
#include "rpg/cutscene/cutscene.h"
#include "yyjson.h"
entity_t ENTITIES[ENTITY_COUNT]; entity_t ENTITIES[ENTITY_COUNT];
@@ -89,8 +90,8 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
} }
// Get tile under foot // Get tile under foot
tile_t tileCurrent = mapGetTile(entity->position); tile_t tileCurrent = chunkGetTile(entity->position);
tile_t tileNew = mapGetTile(newPos); tile_t tileNew = chunkGetTile(newPos);
bool_t fall = false; bool_t fall = false;
bool_t raise = false; bool_t raise = false;
@@ -139,7 +140,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
tileNew = TILE_NULL; tileNew = TILE_NULL;
worldpos_t abovePos = newPos; worldpos_t abovePos = newPos;
abovePos.z += 1; abovePos.z += 1;
tile_t tileAbove = mapGetTile(abovePos); tile_t tileAbove = chunkGetTile(abovePos);
if( if(
tileAbove.shape != TILE_SHAPE_NULL && tileAbove.shape != TILE_SHAPE_NULL &&
@@ -153,7 +154,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
// Falling down? // Falling down?
worldpos_t belowPos = newPos; worldpos_t belowPos = newPos;
belowPos.z -= 1; belowPos.z -= 1;
tile_t tileBelow = mapGetTile(belowPos); tile_t tileBelow = chunkGetTile(belowPos);
if( if(
tileBelow.shape != TILE_SHAPE_NULL && tileBelow.shape != TILE_SHAPE_NULL &&
tileShapeIsRamp(tileBelow.shape) && tileShapeIsRamp(tileBelow.shape) &&
@@ -283,7 +284,7 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
assertNotNull(entity, "Entity pointer cannot be NULL"); assertNotNull(entity, "Entity pointer cannot be NULL");
if(entity->chunkIndex != 0xFF) { if(entity->chunkIndex != 0xFF) {
chunk_t *old = mapGetChunk(entity->chunkIndex); chunk_t *old = chunkGet(entity->chunkIndex);
if(old != NULL) { if(old != NULL) {
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(old->entities[i] != entity->id) continue; if(old->entities[i] != entity->id) continue;
@@ -293,27 +294,16 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
} }
} }
// Only claim the new chunk once actually inserted into one of its slots - entity->chunkIndex = chunkIndex;
// otherwise entity->chunkIndex would point at a chunk that doesn't know
// about this entity, so it would never be torn down on unload.
entity->chunkIndex = 0xFF;
if(chunkIndex != 0xFF) { if(chunkIndex != 0xFF) {
chunk_t *next = mapGetChunk(chunkIndex); chunk_t *next = chunkGet(chunkIndex);
if(next != NULL) { if(next != NULL) {
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(next->entities[i] != 0xFF) continue; if(next->entities[i] != 0xFF) continue;
next->entities[i] = entity->id; next->entities[i] = entity->id;
entity->chunkIndex = chunkIndex;
break; break;
} }
if(entity->chunkIndex != chunkIndex) {
consolePrint(
"entitySetChunk: chunk %u has no free entity slots, entity %u "
"left untracked",
chunkIndex, entity->id
);
}
} }
} }
} }
@@ -323,6 +313,159 @@ void entityUpdateChunk(entity_t *entity) {
chunkpos_t cp; chunkpos_t cp;
worldPosToChunkPos(&entity->position, &cp); worldPosToChunkPos(&entity->position, &cp);
chunkindex_t ci = mapGetChunkIndexAt(cp); chunkindex_t ci = chunkGetIndexAt(cp);
if(ci != -1) entitySetChunk(entity, (uint8_t)ci); if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
}
errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity) {
assertNotNull(obj, "Entity JSON object cannot be NULL");
assertNotNull(outEntity, "Output entity pointer cannot be NULL");
yyjson_val *typeVal = yyjson_obj_get(obj, "type");
if(!typeVal || !yyjson_is_str(typeVal)) {
errorThrow("Entity JSON missing 'type' string");
}
const char_t *typeStr = yyjson_get_str(typeVal);
entitytype_t type;
if(stringEquals(typeStr, "player")) {
type = ENTITY_TYPE_PLAYER;
} else if(stringEquals(typeStr, "npc")) {
type = ENTITY_TYPE_NPC;
} else if(stringEquals(typeStr, "item")) {
type = ENTITY_TYPE_ITEM;
} else {
errorThrow("Entity JSON has unknown 'type': %s", typeStr);
}
yyjson_val *posVal = yyjson_obj_get(obj, "position");
if(!posVal || !yyjson_is_arr(posVal) || yyjson_arr_size(posVal) != 3) {
errorThrow("Entity JSON missing 'position' [x, y, z] array");
}
worldunit_t pos[3];
size_t posIdx, posLen;
yyjson_val *posElem;
yyjson_arr_foreach(posVal, posIdx, posLen, posElem) {
if(!yyjson_is_num(posElem)) {
errorThrow("Entity JSON 'position' elements must be numbers");
}
pos[posIdx] = (worldunit_t)yyjson_get_num(posElem);
}
entitydir_t direction = ENTITY_DIR_SOUTH;
yyjson_val *dirVal = yyjson_obj_get(obj, "direction");
if(dirVal && yyjson_is_str(dirVal)) {
const char_t *dirStr = yyjson_get_str(dirVal);
if(stringEquals(dirStr, "north")) {
direction = ENTITY_DIR_NORTH;
} else if(stringEquals(dirStr, "east")) {
direction = ENTITY_DIR_EAST;
} else if(stringEquals(dirStr, "south")) {
direction = ENTITY_DIR_SOUTH;
} else if(stringEquals(dirStr, "west")) {
direction = ENTITY_DIR_WEST;
} else {
errorThrow("Entity JSON has unknown 'direction': %s", dirStr);
}
}
// Item entities require a valid item reference, resolved up front so a
// bad reference fails before an entity slot is ever claimed.
itemid_t itemId = ITEM_ID_NULL;
uint8_t itemQuantity = 1;
if(type == ENTITY_TYPE_ITEM) {
yyjson_val *itemVal = yyjson_obj_get(obj, "item");
if(!itemVal || !yyjson_is_str(itemVal)) {
errorThrow("Entity JSON with type 'item' missing 'item' string");
}
const char_t *itemStr = yyjson_get_str(itemVal);
itemId = itemGetIdByName(itemStr);
if(itemId == ITEM_ID_NULL) {
errorThrow("Entity JSON references unknown item '%s'", itemStr);
}
yyjson_val *quantityVal = yyjson_obj_get(obj, "quantity");
if(quantityVal && yyjson_is_int(quantityVal)) {
itemQuantity = (uint8_t)yyjson_get_int(quantityVal);
}
}
// Only one player may exist at a time - it always holds the reserved
// ENTITY_GLOBAL_ID_PLAYER global ID, so that's how callers (e.g. the
// camera) find it regardless of how it was spawned.
if(
type == ENTITY_TYPE_PLAYER &&
entityGetByGlobalId(ENTITY_GLOBAL_ID_PLAYER) != NULL
) {
errorThrow("A player entity has already been spawned");
}
// NPC path waypoints, resolved up front for the same reason as above.
worldpos_t path[NPC_PATH_COUNT_MAX];
uint8_t pathCount = 0;
if(type == ENTITY_TYPE_NPC) {
yyjson_val *pathVal = yyjson_obj_get(obj, "path");
if(pathVal) {
if(!yyjson_is_arr(pathVal)) {
errorThrow("Entity JSON 'path' must be an array");
}
size_t count = yyjson_arr_size(pathVal);
if(count == 0 || count > NPC_PATH_COUNT_MAX) {
errorThrow(
"Entity JSON 'path' must have 1-%d waypoints", NPC_PATH_COUNT_MAX
);
}
size_t pathIdx, pathLen;
yyjson_val *pathElem;
yyjson_arr_foreach(pathVal, pathIdx, pathLen, pathElem) {
if(!yyjson_is_arr(pathElem) || yyjson_arr_size(pathElem) != 3) {
errorThrow("Entity JSON 'path' entries must be [x, y, z] arrays");
}
worldunit_t comps[3];
size_t compIdx, compLen;
yyjson_val *compElem;
yyjson_arr_foreach(pathElem, compIdx, compLen, compElem) {
if(!yyjson_is_num(compElem)) {
errorThrow("Entity JSON 'path' elements must be numbers");
}
comps[compIdx] = (worldunit_t)yyjson_get_num(compElem);
}
path[pathIdx] = (worldpos_t){ comps[0], comps[1], comps[2] };
}
pathCount = (uint8_t)count;
}
}
// Interact cutscene, resolved up front for the same reason as above.
const cutscene_t *cutscene = NULL;
yyjson_val *cutsceneVal = yyjson_obj_get(obj, "cutscene");
if(cutsceneVal) {
if(!yyjson_is_str(cutsceneVal)) {
errorThrow("Entity JSON 'cutscene' must be a string");
}
errorChain(cutsceneGetByName(yyjson_get_str(cutsceneVal), &cutscene));
}
uint8_t index = entityGetAvailable();
if(index == 0xFF) errorThrow("No available entity slots");
entity_t *entity = &ENTITIES[index];
entityInit(entity, type);
entity->direction = direction;
entityPositionSet(entity, (worldpos_t){ pos[0], pos[1], pos[2] });
if(type == ENTITY_TYPE_ITEM) entityItemSet(entity, itemId, itemQuantity);
if(pathCount > 0) {
npcSetMoveType(entity, NPC_MOVE_TYPE_PATH);
for(uint8_t i = 0; i < pathCount; i++) {
npcPathAddNode(&entity->data.npc, path[i]);
}
}
if(cutscene != NULL) {
entity->interact.type = ENTITY_INTERACT_CUTSCENE;
entity->interact.data.cutscene = cutscene;
}
*outEntity = entity;
errorOk();
} }
+41 -5
View File
@@ -13,6 +13,7 @@
#include "npc/npc.h" #include "npc/npc.h"
typedef struct map_s map_t; typedef struct map_s map_t;
typedef struct yyjson_val yyjson_val;
typedef uint16_t entityglobalid_t; typedef uint16_t entityglobalid_t;
@@ -142,10 +143,7 @@ uint8_t entityGetAvailable();
/** /**
* Assigns an entity to a chunk, removing it from its current chunk first. * Assigns an entity to a chunk, removing it from its current chunk first.
* Pass 0xFF as chunkIndex to detach the entity from any chunk. If the * Pass 0xFF as chunkIndex to detach the entity from any chunk.
* target chunk has no free entity slots, the entity is left detached
* (chunkIndex 0xFF) rather than assigned to a chunk that isn't actually
* tracking it - entityUpdateChunk will keep retrying on subsequent moves.
* *
* @param entity Pointer to the entity. * @param entity Pointer to the entity.
* @param chunkIndex Index of the chunk to assign to, or 0xFF for none. * @param chunkIndex Index of the chunk to assign to, or 0xFF for none.
@@ -167,4 +165,42 @@ void entityUpdateChunk(entity_t *entity);
* @param entity Pointer to the entity to move. * @param entity Pointer to the entity to move.
* @param pos The world position to place the entity at. * @param pos The world position to place the entity at.
*/ */
void entityPositionSet(entity_t *entity, const worldpos_t pos); void entityPositionSet(entity_t *entity, const worldpos_t pos);
/**
* Parses an entity descriptor from a yyjson object and spawns it as a new
* entity in an available slot. Expected shape:
* { "type": "npc", "position": [x, y, z], "direction": "south" }
* "type" must be one of "player", "npc", "item". "direction" is optional
* (one of "north", "east", "south", "west") and defaults to
* ENTITY_DIR_SOUTH when absent.
*
* When "type" is "item", two extra fields apply:
* { "type": "item", "position": [x, y, z], "item": "POTION",
* "quantity": 1 }
* "item" (required) is the item's string ID, resolved via
* itemGetIdByName. "quantity" (optional) defaults to 1.
*
* When "type" is "player", the spawned entity is assigned the reserved
* ENTITY_GLOBAL_ID_PLAYER global ID (so entityGetByGlobalId can find it
* regardless of how it was spawned), and it is an error to spawn a
* second one while one is already loaded.
*
* When "type" is "npc", an optional "path" field sets it up with
* NPC_MOVE_TYPE_PATH movement:
* { "type": "npc", "position": [x, y, z],
* "path": [[4, 4, 0], [10, 10, 1]] }
* "path" must have 1-NPC_PATH_COUNT_MAX waypoints.
*
* Any entity type may set an optional "cutscene" field to wire up an
* interact component that starts the named cutscene (resolved via
* cutsceneGetByName) when interacted with:
* { "type": "npc", "position": [x, y, z], "cutscene": "test_npc" }
*
* @param obj The yyjson object describing the entity.
* @param outEntity Output pointer, set to the newly spawned entity on
* success.
* @return Any error that occurs (missing/invalid fields, unknown item,
* duplicate player, unknown/malformed cutscene, no free slots).
*/
errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity);
@@ -1,9 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
)
-17
View File
@@ -1,17 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entityglobaldefs.h"
#include "entitygloballist.h"
#define ENTITY_GLOBAL_LIST_COUNT ( \
sizeof(ENTITY_GLOBAL_LIST) / \
sizeof(ENTITY_GLOBAL_LIST[0]) \
)
//EOF
@@ -1,44 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "rpg/overworld/worldpos.h"
#include "rpg/entity/entity.h"
typedef struct entity_s entity_t;
typedef struct {
entity_t *entity;
worldpos_t position;
} entityglobalcreate_t;
/**
* Callback invoked to initialize a global entity.
*
* @param create Pointer to the entity/position being initialized.
* @returns An error code.
*/
typedef void (*entityglobalinitcallback_t)(
entityglobalcreate_t *create
);
typedef struct {
entitytype_t type;
entityglobalinitcallback_t callback;
} entityglobaldef_t;
#define ENTITY_GLOBAL(id, entType, callbackFn) \
[id] = { .type = entType, .callback = callbackFn }
#define ENTITY_GLOBAL_CALLBACK(id) \
static void ENTTIYT_GLOBAL_CALLBACK_##id(entityglobalcreate_t *create)
#define ENTITY_GLOBAL_REF(id) \
ENTTIYT_GLOBAL_CALLBACK_##id
//EOF
@@ -1,30 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entityglobaldefs.h"
#include "rpg/cutscene/scene/testcutscene.h"
ENTITY_GLOBAL_CALLBACK(3) {
create->entity->data.npc.moveType = NPC_MOVE_TYPE_PATH;
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 });
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 });
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 });
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 });
create->entity->interact.type = ENTITY_INTERACT_CUTSCENE;
create->entity->interact.data.cutscene = CUTSCENE_REFERENCE(TEST_TWO);
}
static const entityglobaldef_t ENTITY_GLOBAL_LIST[] = {
ENTITY_GLOBAL(ENTITY_GLOBAL_ID_NULL, ENTITY_TYPE_NULL, NULL),
ENTITY_GLOBAL(ENTITY_GLOBAL_ID_PLAYER, ENTITY_TYPE_PLAYER, NULL),
ENTITY_GLOBAL(3, ENTITY_TYPE_NPC, ENTITY_GLOBAL_REF(3)),
};
//EOF
+1 -10
View File
@@ -10,13 +10,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
inventory.c inventory.c
backpack.c backpack.c
itemgive.c itemgive.c
) )
# Item Definitions
dusk_run_python(
dusk_item_json_defs
tools.item
--json ${CMAKE_CURRENT_SOURCE_DIR}/item.json
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/itemdef.h
)
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_json_defs)
+14 -14
View File
@@ -11,69 +11,69 @@
backpack_t BACKPACK; backpack_t BACKPACK;
void backpackInit() { void backpackInit() {
for(uint8_t i = 0; i < ITEM_TYPE_COUNT; i++) { for(uint32_t i = 0; i < ITEM_TYPE_COUNT_MAX; i++) {
inventoryInit( inventoryInit(
&BACKPACK.inventories[i], &BACKPACK.inventories[i],
BACKPACK.storage[i], BACKPACK.storage[i],
ITEM_TYPE_COUNT_MAX INVENTORY_CAPACITY_MAX
); );
} }
} }
inventory_t *backpackGetInventory(const itemtype_t type) { inventory_t *backpackGetInventory(const itemtypeid_t type) {
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null"); assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range"); assertTrue(type <= ITEM_TYPE_COUNT, "Item type out of range");
return &BACKPACK.inventories[type]; return &BACKPACK.inventories[type];
} }
void backpackAdd(const itemid_t item, const uint8_t quantity) { void backpackAdd(const itemid_t item, const uint8_t quantity) {
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); assertTrue(item <= ITEM_COUNT, "Item ID out of range");
inventoryAdd(backpackGetInventory(ITEMS[item].type), item, quantity); inventoryAdd(backpackGetInventory(ITEMS[item].type), item, quantity);
} }
void backpackRemove(const itemid_t item) { void backpackRemove(const itemid_t item) {
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); assertTrue(item <= ITEM_COUNT, "Item ID out of range");
inventoryRemove(backpackGetInventory(ITEMS[item].type), item); inventoryRemove(backpackGetInventory(ITEMS[item].type), item);
} }
void backpackSet(const itemid_t item, const uint8_t quantity) { void backpackSet(const itemid_t item, const uint8_t quantity) {
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); assertTrue(item <= ITEM_COUNT, "Item ID out of range");
inventorySet(backpackGetInventory(ITEMS[item].type), item, quantity); inventorySet(backpackGetInventory(ITEMS[item].type), item, quantity);
} }
uint8_t backpackGetCount(const itemid_t item) { uint8_t backpackGetCount(const itemid_t item) {
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); assertTrue(item <= ITEM_COUNT, "Item ID out of range");
return inventoryGetCount(backpackGetInventory(ITEMS[item].type), item); return inventoryGetCount(backpackGetInventory(ITEMS[item].type), item);
} }
bool_t backpackItemExists(const itemid_t item) { bool_t backpackItemExists(const itemid_t item) {
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); assertTrue(item <= ITEM_COUNT, "Item ID out of range");
return inventoryItemExists(backpackGetInventory(ITEMS[item].type), item); return inventoryItemExists(backpackGetInventory(ITEMS[item].type), item);
} }
bool_t backpackIsFull(const itemtype_t type) { bool_t backpackIsFull(const itemtypeid_t type) {
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null"); assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range"); assertTrue(type <= ITEM_TYPE_COUNT, "Item type out of range");
return inventoryIsFull(backpackGetInventory(type)); return inventoryIsFull(backpackGetInventory(type));
} }
bool_t backpackItemFull(const itemid_t item) { bool_t backpackItemFull(const itemid_t item) {
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); assertTrue(item <= ITEM_COUNT, "Item ID out of range");
return inventoryItemFull(backpackGetInventory(ITEMS[item].type), item); return inventoryItemFull(backpackGetInventory(ITEMS[item].type), item);
} }
void backpackSort( void backpackSort(
const itemtype_t type, const itemtypeid_t type,
const inventorysort_t sortBy, const inventorysort_t sortBy,
const bool_t reverse const bool_t reverse
) { ) {
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null"); assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range"); assertTrue(type <= ITEM_TYPE_COUNT, "Item type out of range");
inventorySort(backpackGetInventory(type), sortBy, reverse); inventorySort(backpackGetInventory(type), sortBy, reverse);
} }
+5 -5
View File
@@ -9,8 +9,8 @@
#include "inventory.h" #include "inventory.h"
typedef struct { typedef struct {
inventorystack_t storage[ITEM_TYPE_COUNT][ITEM_TYPE_COUNT_MAX]; inventorystack_t storage[ITEM_TYPE_COUNT_MAX][INVENTORY_CAPACITY_MAX];
inventory_t inventories[ITEM_TYPE_COUNT]; inventory_t inventories[ITEM_TYPE_COUNT_MAX];
} backpack_t; } backpack_t;
extern backpack_t BACKPACK; extern backpack_t BACKPACK;
@@ -26,7 +26,7 @@ void backpackInit();
* @param type The item type. * @param type The item type.
* @returns Pointer to the inventory for that type. * @returns Pointer to the inventory for that type.
*/ */
inventory_t *backpackGetInventory(const itemtype_t type); inventory_t *backpackGetInventory(const itemtypeid_t type);
/** /**
* Adds a quantity of an item to the backpack. * Adds a quantity of an item to the backpack.
@@ -73,7 +73,7 @@ bool_t backpackItemExists(const itemid_t item);
* @param type The item type to check. * @param type The item type to check.
* @returns true if the type's inventory is full. * @returns true if the type's inventory is full.
*/ */
bool_t backpackIsFull(const itemtype_t type); bool_t backpackIsFull(const itemtypeid_t type);
/** /**
* Checks if an item's stack is full in the backpack. * Checks if an item's stack is full in the backpack.
@@ -91,7 +91,7 @@ bool_t backpackItemFull(const itemid_t item);
* @param reverse Whether to sort in reverse order. * @param reverse Whether to sort in reverse order.
*/ */
void backpackSort( void backpackSort(
const itemtype_t type, const itemtypeid_t type,
const inventorysort_t sortBy, const inventorysort_t sortBy,
const bool_t reverse const bool_t reverse
); );
+4 -4
View File
@@ -188,8 +188,8 @@ int_t inventorySortByIdReverse(const void *a, const void *b) {
int_t inventorySortByType(const void *a, const void *b) { int_t inventorySortByType(const void *a, const void *b) {
const inventorystack_t *stackA = (const inventorystack_t*)a; const inventorystack_t *stackA = (const inventorystack_t*)a;
const inventorystack_t *stackB = (const inventorystack_t*)b; const inventorystack_t *stackB = (const inventorystack_t*)b;
const itemtype_t typeA = ITEMS[stackA->item].type; const itemtypeid_t typeA = ITEMS[stackA->item].type;
const itemtype_t typeB = ITEMS[stackB->item].type; const itemtypeid_t typeB = ITEMS[stackB->item].type;
if(typeA < typeB) return -1; if(typeA < typeB) return -1;
if(typeA > typeB) return 1; if(typeA > typeB) return 1;
return 0; return 0;
@@ -198,8 +198,8 @@ int_t inventorySortByType(const void *a, const void *b) {
int_t inventorySortByTypeReverse(const void *a, const void *b) { int_t inventorySortByTypeReverse(const void *a, const void *b) {
const inventorystack_t *stackA = (const inventorystack_t*)a; const inventorystack_t *stackA = (const inventorystack_t*)a;
const inventorystack_t *stackB = (const inventorystack_t*)b; const inventorystack_t *stackB = (const inventorystack_t*)b;
const itemtype_t typeA = ITEMS[stackA->item].type; const itemtypeid_t typeA = ITEMS[stackA->item].type;
const itemtype_t typeB = ITEMS[stackB->item].type; const itemtypeid_t typeB = ITEMS[stackB->item].type;
if(typeA < typeB) return 1; if(typeA < typeB) return 1;
if(typeA > typeB) return -1; if(typeA > typeB) return -1;
return 0; return 0;
+1
View File
@@ -9,6 +9,7 @@
#include "rpg/item/item.h" #include "rpg/item/item.h"
#define ITEM_STACK_QUANTITY_MAX 99 #define ITEM_STACK_QUANTITY_MAX 99
#define INVENTORY_CAPACITY_MAX 250
typedef enum { typedef enum {
INVENTORY_SORT_BY_ID, INVENTORY_SORT_BY_ID,
+132 -1
View File
@@ -7,8 +7,125 @@
#include "item.h" #include "item.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "asset/asset.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/json/assetjsonloader.h"
#include "locale/localemanager.h" #include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h" #include "asset/loader/locale/assetlocaleloader.h"
#include "yyjson.h"
#define ITEM_JSON_PATH "item.json"
itemdef_t ITEMS[ITEM_COUNT_MAX];
uint32_t ITEM_COUNT;
itemtype_t ITEM_TYPES[ITEM_TYPE_COUNT_MAX];
uint32_t ITEM_TYPE_COUNT;
errorret_t itemInit(void) {
memoryZero(ITEMS, sizeof(ITEMS));
memoryZero(ITEM_TYPES, sizeof(ITEM_TYPES));
ITEM_COUNT = 0;
ITEM_TYPE_COUNT = 0;
assetentry_t *jsonEntry = assetLock(
ITEM_JSON_PATH, ASSET_LOADER_TYPE_JSON, NULL
);
errorret_t ret = assetRequireLoaded(jsonEntry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(jsonEntry);
errorChain(ret);
}
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
if(!yyjson_is_arr(root)) {
assetUnlockEntry(jsonEntry);
errorThrow("item.json root must be an array");
}
size_t idx, max;
yyjson_val *entry;
yyjson_arr_foreach(root, idx, max, entry) {
if(ITEM_COUNT >= ITEM_COUNT_MAX - 1) {
assetUnlockEntry(jsonEntry);
errorThrow(
"Too many items defined: exceeds ITEM_COUNT_MAX (%d)",
ITEM_COUNT_MAX
);
}
yyjson_val *idVal = yyjson_obj_get(entry, "id");
yyjson_val *typeVal = yyjson_obj_get(entry, "type");
yyjson_val *nameVal = yyjson_obj_get(entry, "name");
yyjson_val *weightVal = yyjson_obj_get(entry, "weight");
if(!idVal || !yyjson_is_str(idVal)) {
assetUnlockEntry(jsonEntry);
errorThrow("Item entry %zu missing 'id' string", idx);
}
if(!typeVal || !yyjson_is_str(typeVal)) {
assetUnlockEntry(jsonEntry);
errorThrow("Item entry %zu missing 'type' string", idx);
}
if(!nameVal || !yyjson_is_str(nameVal)) {
assetUnlockEntry(jsonEntry);
errorThrow("Item entry %zu missing 'name' string", idx);
}
const char_t *idStr = yyjson_get_str(idVal);
size_t idLen = yyjson_get_len(idVal);
const char_t *typeStr = yyjson_get_str(typeVal);
size_t typeLen = yyjson_get_len(typeVal);
const char_t *nameStr = yyjson_get_str(nameVal);
size_t nameLen = yyjson_get_len(nameVal);
if(idLen >= ITEM_STRING_MAX) {
assetUnlockEntry(jsonEntry);
errorThrow("Item id '%s' exceeds max length", idStr);
}
if(nameLen + 10 >= ITEM_STRING_MAX) {
assetUnlockEntry(jsonEntry);
errorThrow("Item name '%s' exceeds max length", nameStr);
}
if(typeLen >= ITEM_STRING_MAX) {
assetUnlockEntry(jsonEntry);
errorThrow("Item type '%s' exceeds max length", typeStr);
}
itemtypeid_t typeId = itemResolveType(typeStr, typeLen);
if(typeId == ITEM_TYPE_NULL) {
assetUnlockEntry(jsonEntry);
errorThrow(
"Too many item types defined: exceeds ITEM_TYPE_COUNT_MAX (%d)",
ITEM_TYPE_COUNT_MAX
);
}
ITEM_COUNT++;
itemid_t id = (itemid_t)ITEM_COUNT;
itemdef_t *def = &ITEMS[id];
def->id = id;
def->type = typeId;
def->weight = (weightVal && yyjson_is_num(weightVal)) ?
(float_t)yyjson_get_num(weightVal) : 0.0f;
memoryCopy(def->idName, idStr, idLen + 1);
stringFormat(def->name, ITEM_STRING_MAX - 1, "item.%s.name", nameStr);
}
assetUnlockEntry(jsonEntry);
errorOk();
}
itemid_t itemGetIdByName(const char_t *name) {
assertNotNull(name, "Item name cannot be NULL");
for(uint32_t i = 1; i <= ITEM_COUNT; i++) {
if(stringEquals(ITEMS[i].idName, name)) return (itemid_t)i;
}
return ITEM_ID_NULL;
}
errorret_t itemGetName( errorret_t itemGetName(
const itemid_t item, const itemid_t item,
@@ -16,7 +133,7 @@ errorret_t itemGetName(
const size_t bufferSize const size_t bufferSize
) { ) {
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); assertTrue(item <= ITEM_COUNT, "Item ID out of range");
errorChain(assetLocaleGetString( errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale, &LOCALE.entry->data.locale,
@@ -28,3 +145,17 @@ errorret_t itemGetName(
errorOk(); errorOk();
} }
itemtypeid_t itemResolveType(const char_t *name, const size_t nameLen) {
for(uint32_t i = 1; i <= ITEM_TYPE_COUNT; i++) {
if(stringEquals(ITEM_TYPES[i].name, name)) return (itemtypeid_t)i;
}
if(ITEM_TYPE_COUNT >= ITEM_TYPE_COUNT_MAX - 1) return ITEM_TYPE_NULL;
ITEM_TYPE_COUNT++;
itemtypeid_t typeId = (itemtypeid_t)ITEM_TYPE_COUNT;
ITEM_TYPES[typeId].id = typeId;
memoryCopy(ITEM_TYPES[typeId].name, name, nameLen + 1);
return typeId;
}
+56 -1
View File
@@ -7,7 +7,62 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include "rpg/item/itemdef.h"
#define ITEM_COUNT_MAX 256
#define ITEM_TYPE_COUNT_MAX 16
#define ITEM_STRING_MAX 48
typedef uint16_t itemid_t;
typedef uint8_t itemtypeid_t;
#define ITEM_ID_NULL ((itemid_t)0)
#define ITEM_TYPE_NULL ((itemtypeid_t)0)
typedef struct {
itemid_t id;
itemtypeid_t type;
float_t weight;
char_t idName[ITEM_STRING_MAX];
char_t name[ITEM_STRING_MAX];
} itemdef_t;
typedef struct {
itemtypeid_t id;
char_t name[ITEM_STRING_MAX];
} itemtype_t;
extern itemdef_t ITEMS[ITEM_COUNT_MAX];
extern uint32_t ITEM_COUNT;
extern itemtype_t ITEM_TYPES[ITEM_TYPE_COUNT_MAX];
extern uint32_t ITEM_TYPE_COUNT;
/**
* Loads assets/item.json and parses it into the ITEMS/ITEM_TYPES tables.
* Must be called once, before any other item/backpack function.
*
* @return Any error that occurs (missing/malformed JSON, or too many
* items/types defined for ITEM_COUNT_MAX/ITEM_TYPE_COUNT_MAX).
*/
errorret_t itemInit(void);
/**
* Looks up an item's numeric ID from its JSON "id" string.
*
* @param name The item's string ID (e.g. "POTION"), case-sensitive.
* @return The matching item ID, or ITEM_ID_NULL if not found.
*/
itemid_t itemGetIdByName(const char_t *name);
/**
* Resolves a type name string to its numeric type ID, registering it as
* a new type in ITEM_TYPES if not already known.
*
* @param name The type's string name (e.g. "MEDICINE").
* @param nameLen Length of name, excluding the null terminator.
* @return The resolved type ID, or ITEM_TYPE_NULL if ITEM_TYPE_COUNT_MAX
* would be exceeded.
*/
itemtypeid_t itemResolveType(const char_t *name, const size_t nameLen);
/** /**
* Gets the localized display name for an item. * Gets the localized display name for an item.
-2
View File
@@ -14,5 +14,3 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
tileshape.c tileshape.c
) )
add_subdirectory(global)
+383 -2
View File
@@ -1,11 +1,29 @@
/** /**
* Copyright (c) 2025 Dominic Masters * Copyright (c) 2025 Dominic Masters
* *
* This software is released under the MIT License. * This software is released under the MIT License.
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
*/ */
#include "chunk.h" #include "chunk.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#include "console/console.h"
#include "event/event.h"
#include "rpg/entity/entity.h"
#include "rpg/overworld/map.h"
chunk_t CHUNKS[MAP_CHUNK_COUNT];
chunk_t *CHUNK_ORDER[MAP_CHUNK_COUNT];
static chunkpos_t CHUNK_POSITION;
static chunk_t *CHUNK_LOAD_QUEUE[MAP_CHUNK_COUNT];
static uint32_t CHUNK_LOAD_QUEUE_COUNT;
static chunk_t *CHUNK_LOADING;
uint32_t chunkGetTileIndex(const chunkpos_t position) { uint32_t chunkGetTileIndex(const chunkpos_t position) {
return (position.y * CHUNK_WIDTH) + position.x; return (position.y * CHUNK_WIDTH) + position.x;
@@ -13,4 +31,367 @@ uint32_t chunkGetTileIndex(const chunkpos_t position) {
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) { bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) {
return (a.x == b.x) && (a.y == b.y) && (a.z == b.z); return (a.x == b.x) && (a.y == b.y) && (a.z == b.z);
} }
errorret_t chunksLoadGrid(void) {
CHUNK_POSITION = (chunkpos_t){ 0, 0, 0 };
chunkindex_t i = 0;
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 = &CHUNKS[i++];
chunk->position = (chunkpos_t){
(chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z
};
errorChain(chunkLoad(chunk));
}
}
}
chunkRebuildOrder();
errorOk();
}
void chunksUnloadAll(void) {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunkUnload(&CHUNKS[i]);
}
}
errorret_t chunkPositionSet(const chunkpos_t newPos) {
if(!mapIsLoaded()) errorThrow("No map loaded");
if(chunkPositionIsEqual(newPos, CHUNK_POSITION)) errorOk();
// Separate loaded chunks into "keep" and "free" buckets.
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_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH];
memoryZero(posLoaded, sizeof(posLoaded));
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &CHUNKS[i];
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_CHUNK_WIDTH &&
ry >= 0 && ry < MAP_CHUNK_HEIGHT &&
rz >= 0 && rz < MAP_CHUNK_DEPTH
) {
posLoaded[rx][ry][rz] = true;
} else {
chunkUnload(chunk);
chunksFreed[freedCount++] = i;
}
}
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 = &CHUNKS[chunksFreed[--freedCount]];
chunk->position = (chunkpos_t){
newPos.x + (chunkunit_t)x,
newPos.y + (chunkunit_t)y,
newPos.z + (chunkunit_t)z
};
errorChain(chunkLoad(chunk));
}
}
}
CHUNK_POSITION = newPos;
chunkRebuildOrder();
errorOk();
}
void chunkUnload(chunk_t *chunk) {
chunkLoadQueueRemove(chunk);
if(CHUNK_LOADING == chunk) CHUNK_LOADING = 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]];
if(!entityCanUnload(entity)) {
entitySetChunk(entity, 0xFF);
} else {
entity->type = ENTITY_TYPE_NULL;
}
}
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
if(chunk->dataEntry != NULL) {
eventUnsubscribe(&chunk->dataEntry->onLoaded, chunkLoaded);
eventUnsubscribe(&chunk->dataEntry->onError, chunkLoadError);
assetUnlockEntry(chunk->dataEntry);
chunk->dataEntry = 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++) {
chunk->modelEntries[m] = NULL;
}
chunk->meshCount = 0;
}
errorret_t chunkLoad(chunk_t *chunk) {
if(!mapIsLoaded()) errorThrow("No map loaded");
chunkLoadQueueRemove(chunk);
if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL;
if(chunk->dataEntry != NULL) {
eventUnsubscribe(&chunk->dataEntry->onLoaded, chunkLoaded);
eventUnsubscribe(&chunk->dataEntry->onError, chunkLoadError);
assetUnlockEntry(chunk->dataEntry);
chunk->dataEntry = NULL;
}
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
chunk->meshCount = 0;
char_t path[MAP_FILE_PATH_MAX + 64];
stringFormat(
path, sizeof(path),
"map/%s/chunks/%d_%d_%d.json",
MAP.name,
(int32_t)chunk->position.x,
(int32_t)chunk->position.y,
(int32_t)chunk->position.z
);
if(!assetFileExists(path)) {
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
}
errorOk();
}
assertTrue(
CHUNK_LOAD_QUEUE_COUNT < MAP_CHUNK_COUNT,
"Chunk load queue overflow"
);
CHUNK_LOAD_QUEUE[CHUNK_LOAD_QUEUE_COUNT++] = chunk;
chunkLoadNext();
errorOk();
}
void chunkLoadNext(void) {
if(CHUNK_LOADING != NULL) return;
if(CHUNK_LOAD_QUEUE_COUNT == 0) return;
chunk_t *chunk = CHUNK_LOAD_QUEUE[0];
for(uint32_t i = 1; i < CHUNK_LOAD_QUEUE_COUNT; i++) {
CHUNK_LOAD_QUEUE[i - 1] = CHUNK_LOAD_QUEUE[i];
}
CHUNK_LOAD_QUEUE_COUNT--;
CHUNK_LOADING = chunk;
char_t path[MAP_FILE_PATH_MAX + 64];
stringFormat(
path, sizeof(path),
"map/%s/chunks/%d_%d_%d.json",
MAP.name,
(int32_t)chunk->position.x,
(int32_t)chunk->position.y,
(int32_t)chunk->position.z
);
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CHUNK, NULL);
assertNotNull(entry, "Failed to get chunk asset entry");
chunk->dataEntry = 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) {
chunkLoaded(entry, chunk);
return;
}
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
chunkLoadError(entry, chunk);
return;
}
eventSubscribe(&entry->onLoaded, chunkLoaded, chunk);
eventSubscribe(&entry->onError, chunkLoadError, chunk);
}
void chunkLoadQueueRemove(chunk_t *chunk) {
for(uint32_t i = 0; i < CHUNK_LOAD_QUEUE_COUNT; i++) {
if(CHUNK_LOAD_QUEUE[i] != chunk) continue;
for(uint32_t j = i + 1; j < CHUNK_LOAD_QUEUE_COUNT; j++) {
CHUNK_LOAD_QUEUE[j - 1] = CHUNK_LOAD_QUEUE[j];
}
CHUNK_LOAD_QUEUE_COUNT--;
return;
}
}
chunkindex_t chunkGetIndexAt(const chunkpos_t position) {
if(!mapIsLoaded()) return -1;
chunkpos_t relPos = {
position.x - CHUNK_POSITION.x,
position.y - CHUNK_POSITION.y,
position.z - CHUNK_POSITION.z
};
if(
relPos.x < 0 || relPos.y < 0 || relPos.z < 0 ||
relPos.x >= MAP_CHUNK_WIDTH ||
relPos.y >= MAP_CHUNK_HEIGHT ||
relPos.z >= MAP_CHUNK_DEPTH
) {
return -1;
}
return chunkPosToIndex(&relPos);
}
chunk_t *chunkGet(const uint8_t index) {
if(index >= MAP_CHUNK_COUNT) return NULL;
if(!mapIsLoaded()) return NULL;
return CHUNK_ORDER[index];
}
tile_t chunkGetTile(const worldpos_t position) {
if(!mapIsLoaded()) return TILE_NULL;
chunkpos_t chunkPos;
worldPosToChunkPos(&position, &chunkPos);
chunkindex_t chunkIndex = chunkGetIndexAt(chunkPos);
if(chunkIndex == -1) return TILE_NULL;
chunk_t *chunk = chunkGet(chunkIndex);
assertNotNull(chunk, "Chunk pointer cannot be NULL");
chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position);
tile_t tile = chunk->tiles[tileIndex];
if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL;
return tile;
}
bool_t chunkGetWalkableZNear(
const worldunit_t x,
const worldunit_t y,
const worldunit_t nearZ,
worldunit_t *outZ
) {
assertNotNull(outZ, "Output Z pointer cannot be NULL");
const worldunit_t candidates[] = {
nearZ, (worldunit_t)(nearZ + 1), (worldunit_t)(nearZ - 1)
};
for(uint8_t i = 0; i < 3; i++) {
const worldpos_t pos = { x, y, candidates[i] };
if(!tileShapeIsWalkable(chunkGetTile(pos).shape)) continue;
*outZ = candidates[i];
return true;
}
return false;
}
void chunkRebuildOrder(void) {
memoryZero(CHUNK_ORDER, sizeof(CHUNK_ORDER));
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &CHUNKS[i];
const chunkpos_t rel = {
chunk->position.x - CHUNK_POSITION.x,
chunk->position.y - CHUNK_POSITION.y,
chunk->position.z - CHUNK_POSITION.z
};
if(
rel.x < 0 || rel.x >= MAP_CHUNK_WIDTH ||
rel.y < 0 || rel.y >= MAP_CHUNK_HEIGHT ||
rel.z < 0 || rel.z >= MAP_CHUNK_DEPTH
) continue;
CHUNK_ORDER[chunkPosToIndex(&rel)] = chunk;
}
}
void chunkLoadError(void *params, void *user) {
assertNotNull(params, "chunkLoadError: params cannot be NULL");
assertNotNull(user, "chunkLoadError: user cannot be NULL");
assetentry_t *entry = (assetentry_t *)params;
chunk_t *chunk = (chunk_t *)user;
if(chunk->dataEntry != 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, chunkLoaded);
eventUnsubscribe(&entry->onError, chunkLoadError);
assetUnlockEntry(chunk->dataEntry);
chunk->dataEntry = NULL;
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL;
chunkLoadNext();
}
void chunkLoaded(void *params, void *user) {
assertNotNull(params, "chunkLoaded: params cannot be NULL");
assertNotNull(user, "chunkLoaded: user cannot be NULL");
assetentry_t *entry = (assetentry_t *)params;
chunk_t *chunk = (chunk_t *)user;
if(chunk->dataEntry != entry) return;
uint8_t meshCount = entry->data.chunk.meshCount;
memoryCopy(
chunk->tiles,
entry->data.chunk.tiles,
sizeof(chunk->tiles)
);
worldpos_t wp;
chunkPosToWorldPos(&chunk->position, &wp);
vec3 wpf = {
(float_t)wp.x, (float_t)wp.y, (float_t)wp.z * WORLD_LAYER_HEIGHT
};
for(uint8_t m = 0; m < meshCount; m++) {
stringCopy(
chunk->modelNames[m],
entry->data.chunk.modelNames[m],
CHUNK_MESH_NAME_MAX
);
glm_vec3_copy(
entry->data.chunk.meshOffsets[m],
chunk->meshOffsets[m]
);
vec3 scaledOffset = {
chunk->meshOffsets[m][0],
chunk->meshOffsets[m][1],
chunk->meshOffsets[m][2] * WORLD_LAYER_HEIGHT
};
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 chunkLoad for a different chunk_t once we
// eventually unlock it in chunkUnload, at which point its modelEntries
// must still be intact for that next reuse to copy from.
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
}
eventUnsubscribe(&entry->onLoaded, chunkLoaded);
eventUnsubscribe(&entry->onError, chunkLoadError);
// Deliberately keep chunk->dataEntry 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 chunkUnload instead.
chunk->meshCount = meshCount;
if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL;
chunkLoadNext();
}
+134 -10
View File
@@ -12,8 +12,6 @@
#define CHUNK_MESH_COUNT_MAX 10 #define CHUNK_MESH_COUNT_MAX 10
#define CHUNK_MESH_NAME_MAX 64 #define CHUNK_MESH_NAME_MAX 64
#define CHUNK_ENTITY_COUNT_MAX 10 #define CHUNK_ENTITY_COUNT_MAX 10
#define CHUNK_ENTITY_SPAWN_COUNT_MAX 8
#define CHUNK_AREA_COUNT_MAX 4
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
@@ -21,7 +19,7 @@ typedef struct chunk_s {
chunkpos_t position; chunkpos_t position;
tile_t tiles[CHUNK_TILE_COUNT]; tile_t tiles[CHUNK_TILE_COUNT];
assetentry_t *dcfEntry; assetentry_t *dataEntry;
uint8_t meshCount; uint8_t meshCount;
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX]; char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
@@ -30,15 +28,18 @@ typedef struct chunk_s {
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX]; assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
uint8_t entities[CHUNK_ENTITY_COUNT_MAX]; uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
// Map area IDs (into MAP_AREAS) spawned from this chunk's file data.
// Removed via mapAreaRemove when this chunk unloads, and re-added if it
// streams back in - unlike entities (tracked by current position via
// entities[] above), areas have no position-based ownership mechanism of
// their own, so the owning chunk must track and tear them down directly.
uint8_t areas[CHUNK_AREA_COUNT_MAX];
} chunk_t; } chunk_t;
/** Every chunk slot for the currently loaded map. */
extern chunk_t CHUNKS[MAP_CHUNK_COUNT];
/**
* Chunk pointers arranged by position relative to the currently loaded
* window, indexed via chunkPosToIndex(). NULL where no chunk occupies
* that slot. Rebuilt by chunkRebuildOrder() whenever the window moves.
*/
extern chunk_t *CHUNK_ORDER[MAP_CHUNK_COUNT];
/** /**
* Gets the tile index for a tile position within a chunk. * Gets the tile index for a tile position within a chunk.
* *
@@ -55,3 +56,126 @@ uint32_t chunkGetTileIndex(const chunkpos_t position);
* @return true if equal, false otherwise. * @return true if equal, false otherwise.
*/ */
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b); bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b);
/**
* Resets and starts loading the initial MAP_CHUNK_WIDTH x HEIGHT x DEPTH
* grid of chunks (async, see chunkLoad), anchored at chunk position
* (0,0,0). Does not unload chunks already loaded - call chunksUnloadAll()
* first when switching to a different map.
*
* @return Any error that occurs.
*/
errorret_t chunksLoadGrid(void);
/**
* Unloads every chunk slot in CHUNKS.
*/
void chunksUnloadAll(void);
/**
* Moves the loaded chunk window to be centered around newPos, unloading
* chunks that fall outside the new window and loading any newly exposed
* ones. No-op if newPos matches the currently loaded window.
*
* @param newPos The new chunk position.
* @return An error code.
*/
errorret_t chunkPositionSet(const chunkpos_t newPos);
/**
* Unloads a chunk.
*
* @param chunk The chunk to unload.
*/
void chunkUnload(chunk_t *chunk);
/**
* Loads a chunk. Starts async loading without blocking.
*
* @param chunk The chunk to load.
* @return An error code.
*/
errorret_t chunkLoad(chunk_t *chunk);
/**
* Starts loading the next queued chunk, if no chunk is currently mid-load.
* Called after chunkLoad enqueues a chunk, and again after the
* currently-loading chunk finishes (or is unloaded) to advance the queue.
*/
void chunkLoadNext(void);
/**
* 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 chunkLoadQueueRemove(chunk_t *chunk);
/**
* Callback invoked when a chunk JSON asset fails to load. Fills the
* chunk tiles with TILE_SHAPE_GROUND as a fallback.
* Always invoked on the main thread.
*
* @param params The failed assetentry_t.
* @param user The chunk_t that owns the entry.
*/
void chunkLoadError(void *params, void *user);
/**
* Callback invoked when a chunk JSON asset finishes loading.
* Always invoked on the main thread.
*
* @param params The loaded assetentry_t.
* @param user The chunk_t that owns the entry.
*/
void chunkLoaded(void *params, void *user);
/**
* Rebuilds CHUNK_ORDER from the loaded chunks that fall within the
* current render window. Called whenever the chunk position changes.
*/
void chunkRebuildOrder(void);
/**
* Gets the index of a chunk, within the currently loaded window, at the
* given position.
*
* @param position The chunk position.
* @return The index of the chunk, or -1 if out of bounds.
*/
chunkindex_t chunkGetIndexAt(const chunkpos_t position);
/**
* Gets a chunk by its index in CHUNK_ORDER.
*
* @param index The index of the chunk.
* @return A pointer to the chunk.
*/
chunk_t *chunkGet(const uint8_t index);
/**
* Gets the tile at the given world position.
*
* @param position The world position.
* @return The tile at that position, or TILE_NULL if the chunk is unloaded.
*/
tile_t chunkGetTile(const worldpos_t position);
/**
* Finds the closest walkable Z layer to nearZ at the given X/Y. Checks
* nearZ first, then nearZ + 1, then nearZ - 1, since ramps only ever
* change height by one Z layer between adjacent tiles.
*
* @param x The world X coordinate to check.
* @param y The world Y coordinate to check.
* @param nearZ The reference Z layer to search outward from.
* @param outZ Output pointer, set to the resolved Z layer on success.
* @return true if a walkable tile was found, false otherwise.
*/
bool_t chunkGetWalkableZNear(
const worldunit_t x,
const worldunit_t y,
const worldunit_t nearZ,
worldunit_t *outZ
);

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