39 Commits

Author SHA1 Message Date
YourWishes 4d95415232 Move global item collected-state, deadzone, and story flags into save file
Adds three new pieces of save-file state, all following the same shape:
the save file is the single source of truth, not a separate live runtime
copy that gets synced in/out.

- globalitemstore.h/.c: per-global-entity-ID "collected" flags
  (savefile_t.globalItemCollected), so a global item entity's init
  callback can check whether it was already picked up in a prior session
  without needing to keep the entity itself alive to remember that.

- Gamepad deadzone: removed input_t.deadzone entirely. The setting UI and
  every platform's actual deadzone-applying code (inputGetDeadzoneDolphin/
  SDL2, previously hardcoded per-platform literals that the settings menu
  didn't actually affect) now read savefile_t.deadzone directly via
  saveGet(SAVE_ACTIVE_SLOT). Default lives in savefile.h
  (SAVE_DEADZONE_DEFAULT), stamped onto every slot in saveInit().

- Story flags: STORY_FLAG_VALUES (a live, codegen-initialized array) is
  replaced by savefile_t.storyFlags, read/written via the existing
  storyFlagGet()/storyFlagSet() call sites (now macros/functions over the
  active save file instead of a separate array). tools/story.py now
  generates STORY_FLAG_DEFAULTS (const) instead; storyFlagInitDefaults()
  stamps those onto a save the first time it's used (file->exists false),
  called from rpgInit().

Added SAVE_ACTIVE_SLOT (0) to savefile.h as the one shared "which slot is
actually being played" constant, replacing three different local/implicit
0s (uigamemenu.c, rpg.c, and now the settings/input call sites).

Verified round-trip on Linux (all three together in one save/load cycle);
both Linux and PSP build clean.
2026-08-04 11:10:44 -05:00
YourWishes 2cbd80a004 Check for existing save data before saving, confirm before creating new
uiGameMenuSave() now attempts a real load first (via the existing generic
saveLoad()/saveExists() primitives) instead of writing blind. If a save
already exists, it saves straight over it as before. If not, it prompts
via the existing uiConfirm dialog ("No save data found. Create a new
save?") before writing - this is exactly the flow GameCube needs (no
native OS save browser to lean on, unlike PSP), but implemented generically
so it also applies correctly on every other platform without any
platform-specific UI code: saveIsAvailable()/saveExists() already reflect
each platform's real state (e.g. Dolphin's memory-card presence and
existing-file checks), so the same logic just does the right thing
everywhere.

Verified the two branches directly on Linux (temporarily wiring the same
saveLoad -> check -> uiConfirmOpen sequence into rpgInit): with no save
file, saveExists() is false and the confirm dialog opens; with one already
written, it's true and the confirm dialog is correctly skipped. Not
verified via actual menu navigation (no input-injection tooling available
here) or on Dolphin (no devkitPPC toolchain in this environment).
2026-08-04 10:33:57 -05:00
YourWishes 9abf8101da PSP: save through the real sceUtilitySavedata API, not raw file I/O
Rewrote savepsp.c/savestreampsp.c to use sceUtilitySavedataInitStart/
Update/GetStatus/ShutdownStart instead of sceIoOpen/Read/Write, so PSP
saves get a proper OS-generated PARAM.SFO (title/savedataTitle/detail) and
show up correctly in the native save browser.

This dialog spans multiple frames and, per this project's prior experience
with the network config dialog, must be pumped non-blocking one step per
real engine frame rather than blocked on synchronously - a raw-sceGu
blocking loop already froze the app on real hardware for that dialog,
since pspGL owns the GU context. So save.h's saveWrite()/saveLoad() are
now callback-based (savecallback_t onComplete) instead of returning a
result directly, mirroring networkRequestConnection()'s shape, with a new
saveUpdate() (wired into engineUpdate()) pumping the active op each frame.
Linux/Dolphin behavior is unchanged - their fallback path in save.c still
completes synchronously, just via an immediate callback call instead of a
direct return.

Two real bugs found via PPSSPP testing (not just code review): SAVE/LOAD
modes show a confirm screen even for brand-new data, which blocks forever
headlessly - switched to AUTOSAVE/AUTOLOAD, which write/read silently and
generate the identical PARAM.SFO. And PPSSPP's dialog status goes straight
from QUIT to NONE without a separately observable FINISHED in between,
which the first version misread as "disappeared without a result" even on
a successful save - fixed by tracking whether QUIT was already seen.

Confirmed end-to-end in PPSSPP: write, dialog completes, PARAM.SFO +
encrypted save.bin appear on the virtual memory stick, and a subsequent
load decrypts/deserializes back to the exact original data. Not tested on
real PSP hardware.
2026-08-04 09:54:45 -05:00
YourWishes 24badd06a5 Add player name field to save file, save it out as a round-trip test
Adds savefile_t.playerName (SAVE_PLAYER_NAME_MAX) serialized via the
existing saveFileReadString/WriteString helpers, and stamps + saves it in
rpgInit() as a test that the save system now persists actual game data,
not just the header/version. Verified manually: written bytes end in
"Dusk\0" immediately after the version field, and loading it back returns
the same string.
2026-08-04 08:59:29 -05:00
YourWishes 7a03ef8eaf Re-enable save system, fix header/version stamping, handle missing media
- Fixed the actual reason saving never worked on any platform: saveWrite()
  never stamped file->header/file->version before serializing, so every
  written save file had a zeroed magic header and failed its own
  validation on the next load. Confirmed via a manual write/load round
  trip that this alone fully explains "saving doesn't work."
- Re-enabled saveInit()/saveDispose() in engine.c (previously commented out
  under "Temporarily disable save code").
- Added SAVE.available + saveIsAvailable(), refreshed by every real
  save/load/delete attempt. saveInit() no longer treats an unreachable
  save medium as fatal to booting - it logs and continues, since a missing
  memory card/stick shouldn't prevent playing.
- Hardened PSP's saveInitPSP() to actually detect a missing memory stick
  (sceIoGetstat on ms0:/) instead of assuming success, and fixed
  single-level sceIoMkdir to build the full PSP/SAVEDATA directory chain.
- Added busy-retry (CARD_ERROR_BUSY) and a not-mounted guard to Dolphin's
  live savestreamdolphin.c path, extending the same handling already
  backported into savedolphin.c.
- Added a "Save" entry to the game menu wired to saveWrite(0), showing a
  clear message on success, on failure, and when saveIsAvailable() is false.
2026-08-04 08:30:24 -05:00
YourWishes f3ea507313 Fixed save crash
Backported from branch ac2 (commit 85b61097) - CARD_Mount was being called
without CARD_Init first, leaving per-channel control blocks and the DSP
unlock sequence unset. On real Dolphin/hardware this surfaced as a hard
MMIO crash instead of a clean CARD_ERROR_* failure.

Co-Authored-By: Dominic Masters <dominic@domsplace.com>
2026-08-04 08:12:57 -05:00
YourWishes 4b0388a0e1 Chunk streaming concurrency, entity slot fix, and map-data-driven spawns
- Allow 2 chunks to be mid-load concurrently instead of 1 (MAP_CHUNK_LOAD_CONCURRENCY).
- Fix entitySetChunk silently losing track of an entity when its target chunk's
  entity slots are full - it now stays detached (and retries later) instead of
  claiming a chunk that never actually registered it.
- DCF format bumped to v5: chunks can now declare entity spawns (global/NPC via
  the existing entityglobal registry, or one-shot item pickups) and map area
  triggers, resolved via a new callback-ID registry (mapareagloballist.h)
  mirroring the entity one. rpg.c's hardcoded TEST entity/item/area spawns are
  gone - chunk_0_0_0.json now carries that data instead. The player is still
  bootstrapped in code since it isn't map-authored content.
2026-08-04 07:37:48 -05:00
YourWishes a84137b5ff del md 2026-08-04 06:58:03 -05:00
YourWishes ca02ee0352 Add camera shake 2026-07-11 11:02:09 -05:00
YourWishes fbaa54145e Fixed dolphin rendering. 2026-07-10 20:15:26 -05:00
YourWishes 28754ffbf2 Removed old scripted types 2026-07-10 13:24:21 -05:00
YourWishes 470c0eba7a Whatever, some minor map chunking improvements 2026-07-10 12:57:31 -05:00
YourWishes 7098dcec43 Cleaned some log 2026-07-09 23:25:43 -05:00
YourWishes 07137f57af Assets are slightly optimized 2026-07-09 23:25:26 -05:00
YourWishes 8b7491a3d3 Emoji support to characters 2026-07-09 13:18:48 -05:00
YourWishes 8cfa8ddfeb Weaather baseline 2026-07-08 12:57:13 -05:00
YourWishes ef284a15a1 pre-cache shader matrices 2026-07-08 12:36:40 -05:00
YourWishes 3723921573 Render culling on sceneoverworld.h 2026-07-08 12:02:41 -05:00
YourWishes 195399635e Updating mini textbox 2026-07-08 11:45:10 -05:00
YourWishes 46e2a924d3 Mini textboxes 2026-07-08 10:53:53 -05:00
YourWishes b693ea4102 Starting item and battle stuff 2026-07-08 10:05:21 -05:00
YourWishes a73f55beb0 Battle stuff
Build Dusk / build-linux (push) Successful in 7m5s
Build Dusk / build-psp (push) Successful in 1m33s
Build Dusk / build-knulli (push) Successful in 4m0s
Build Dusk / build-gamecube (push) Successful in 3m31s
Build Dusk / build-gamecube-iso (push) Successful in 3m39s
Build Dusk / build-wii (push) Successful in 3m5s
Build Dusk / build-wii-iso (push) Successful in 3m25s
2026-07-08 07:55:15 -05:00
YourWishes 0bd2491ab7 Dropdown test. 2026-07-07 15:18:33 -05:00
YourWishes 860c797c9c Last round of cleanup for settings for now. 2026-07-07 14:56:08 -05:00
YourWishes 38c5080f9f Collapsing more ui settings code 2026-07-07 14:36:19 -05:00
YourWishes fae191d8fe Settings improvements 2026-07-07 14:00:37 -05:00
YourWishes d83a953e2d First pass of language 2026-07-07 11:14:02 -05:00
YourWishes 2dcf0d0f0d UI Confirm 2026-07-07 10:12:22 -05:00
YourWishes 6dee37d8c1 Slider widget and some cleanup 2026-07-07 09:58:10 -05:00
YourWishes 3bad03afb3 Item entity 2026-07-07 07:52:22 -05:00
YourWishes 6a6d8448f7 Area cutscene controls 2026-07-07 07:28:12 -05:00
YourWishes 189babd2cf Trigger types on areas 2026-07-07 07:07:32 -05:00
YourWishes 988a0f2294 Area basics 2026-07-06 23:19:05 -05:00
YourWishes 85bf455731 entity pos sets chunk now 2026-07-04 18:43:09 -05:00
YourWishes 589e4224f3 Example global ent 2026-07-04 13:03:24 -05:00
YourWishes bd7fa154b4 Add user data to callbacks 2026-07-04 09:13:37 -05:00
YourWishes a292f1992b Change to use the Z tile system 2026-07-04 08:40:53 -05:00
YourWishes 88ddf429b7 add some entity management in prep for global entities. 2026-07-03 12:25:24 -05:00
YourWishes 7a1f6662df More cutscene tools 2026-07-03 09:17:49 -05:00
252 changed files with 9680 additions and 3174 deletions
-455
View File
@@ -1,455 +0,0 @@
# Dusk — Claude Code rules
## File headers
Every C, H, and JS file starts with:
```c
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
```
JS files use `//` comment style instead.
---
## C conventions
### Types
Always use the project-defined aliases instead of bare C primitives:
| Use | Not |
|-----------|--------------|
| `bool_t` | `bool` |
| `int_t` | `int` |
| `float_t` | `float` |
| `char_t` | `char` |
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
### Naming
- **Functions** — snake_case, prefixed with their module:
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
- **Macros / constants** — UPPER_SNAKE_CASE:
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
- **Files** — snake_case matching the primary type: `entityposition.c`,
`moduleassetbatch.c`
### Header files (`.h`)
- Use `#pragma once` — no include guards.
- Declare every public function, `#define`, and `extern` global.
- Write a JSDoc block (`/** … */`) above every declaration explaining
purpose, `@param`s, and `@returns`.
- Only include headers that the `.h` file itself strictly requires for
the types it exposes. Move everything else to the `.c` file.
Do not use forward declarations as a workaround — use the real
include in the `.c` file instead.
### Implementation files (`.c`)
- Contain function bodies only; no declarations.
- Pull in whatever additional includes the implementation needs.
- Do not use `static` or `inline` on **functions**. Every function,
including internal helpers, must be declared in the matching `.h` and
defined in the `.c` file. Internal helpers belong near the bottom of
the `.c` file, not at the top with a `static` qualifier.
`static` and `inline` on functions are only appropriate when the
function body is written directly inside a `.h` file.
`static` on **variables** (file-scope state) is fine and expected.
### Formatting
- Hard-wrap all lines at **80 characters**.
### Error handling
Return `errorret_t` from fallible functions. Use these macros:
```c
errorOk(); // return success
errorThrow("msg %d", val); // return failure with message
errorChain(someCall()); // propagate failure, continue on success
errorIsOk(ret) / errorIsNotOk(ret) // test a result
errorCatch(ret); // handle + free an error
```
Never return raw error codes or use `errno` for in-engine errors.
### Memory
Use the project allocator — never raw `malloc`/`free`:
```c
memoryAllocate(size) // allocate
memoryFree(ptr) // free
memoryZero(dest, size) // zero a block
memoryCopy(dest, src, size) // copy
```
### Asserts
Prefer specific assert macros over bare `assert()`:
```c
assertNotNull(ptr, "msg");
assertTrue(cond, "msg");
assertFalse(cond, "msg");
assertUnreachable("msg");
assertIsMainThread("msg");
```
---
## Build system
Each subdirectory has its own `CMakeLists.txt` that adds sources with:
```cmake
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
myfile.c
)
```
Never add source files to the root `CMakeLists.txt` directly.
---
## Platform support
### Targets
Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
| `DUSK_TARGET_SYSTEM` | Macro defined | Platform |
|----------------------|-------------------|------------------|
| `linux` | `DUSK_LINUX` | Linux desktop |
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
| `psp` | `DUSK_PSP` | Sony PSP |
| `vita` | `DUSK_VITA` | PlayStation Vita |
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
| `wii` | `DUSK_WII` | Nintendo Wii |
### Layer structure
```
src/dusk/ core, platform-agnostic game logic
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
src/dusklinux/ Linux + Knulli platform impl
src/duskpsp/ PSP platform impl
src/duskvita/ Vita platform impl
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
```
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
it uses native GameCube/Wii rendering and input APIs.
### Platform guards
Use the compile-time macros for platform-specific code:
```c
#ifdef DUSK_PSP
// PSP-only path
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
// GameCube / Wii path
#else
// Generic / Linux fallback
#endif
```
Additional capability macros set per-target:
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
### Abstraction pattern
Platform-specific implementations are wired in via `#define` macros in
each platform's `displayplatform.h` / `inputplatform.h` etc., which
the core calls through. Functions that a platform does not support are
simply left undefined — the core guards calls with `#ifdef`.
### Adding platform-specific code
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
or capability macro.
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
the platform header macros instead.
---
## Adding a new asset loader type
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
`src/dusk/asset/loader/assetloader.h`.
2. Add fields to the input/loading/output unions in `assetloader.h`.
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
`src/dusk/asset/loader/assetloader.c`.
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
---
## Adding a new entity component
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
`entityMyCompDispose()`.
2. Add the include to `src/dusk/entity/componentlist.h` header block.
3. Add a row to `src/dusk/entity/componentlist.h`:
```c
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
```
This auto-generates the enum, union field, and definition entry.
4. If JS-facing, create the script module and `.d.ts` (see below).
---
## Adding a new script (JS) module
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
- Use `moduleBaseFunction(name)` to define JS-callable functions.
- Register props/funcs in `moduleMyModInit()` with
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
`scriptProtoDefineStaticFunc`.
2. `#include` the header in
`src/dusk/script/module/modulelist.c` and call
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
`moduleListDispose()`).
3. For component modules also register in
`src/dusk/script/module/entity/component/modulecomponentlist.c`
so `entity.add()` returns the typed wrapper.
4. Create `types/<category>/mymod.d.ts` and add a
`/// <reference path="..." />` line to `types/index.d.ts`.
---
## Script module type declarations
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
check whether the corresponding `types/**/*.d.ts` needs updating and
apply any changes before finishing the task.
---
## JavaScript (asset scripts)
- Use `var` for module-level state; `const` for values that never
change.
- Always use semicolons.
- Scene objects are plain objects (`var scene = {}`) with assigned
methods.
- Export via `module.exports = scene`.
- Async scene init should use `async function` and `await`.
---
## Coding style
### ASCII only
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000U+007F).
Non-ASCII characters are banned even in comments and string literals.
Use ASCII-only substitutes instead:
- `--` or `-` instead of `` (em dash)
- `->` instead of `` (arrow)
- `x` or `*` instead of `×` (multiplication)
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
### Indentation
2 spaces. No tabs.
### Keyword and operator spacing
No space between a keyword or function name and its opening parenthesis:
```c
if(!ptr) return;
for(uint8_t i = 0; i < count; i++) {
while(entry->state != DONE) {
switch(type) {
sizeof(assetbatch_t)
memoryZero(ptr, size)
```
Spaces around all binary operators and after every comma:
```c
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
(size_t)end - (size_t)start
foo(a, b, c)
```
### Braces
Opening brace on the **same line** as the statement (K&R style) for all
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
```c
void assetEntryLock(assetentry_t *entry) {
...
}
if(dirty) {
...
} else {
...
}
```
### Guard returns
Short guards go on one line with no braces:
```c
if(!ptr) return;
if(!b || !b->batch) return jerry_undefined();
if(!(flags & DIRTY)) return;
```
### Blank lines
- One blank line between functions; no blank line at the start or end of
a function body.
- One blank line between logical blocks inside a function body.
- No trailing blank lines at the end of a file.
### Pointer placement
`*` is attached to the variable name, not the type:
```c
assetentry_t *entry
const char_t *name
void *ptr
uint8_t *d = (uint8_t *)dest;
```
### Casts
Space between cast and operand:
```c
(assetbatch_t *)user
(uint8_t *)dest
(textureformat_t)v
```
### Return
No parentheses around the return value:
```c
return ptr;
return MEMORY_POINTERS_IN_USE;
```
### switch / case
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
```c
switch(type) {
case ASSET_LOADER_TYPE_TEXTURE:
descs[i].input.texture = (textureformat_t)v;
break;
default:
break;
}
```
### Multi-line function signatures
When parameters don't fit on one line, put each on its own line indented
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
its own line at column 0:
```c
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
errorret_t memoryCompare(
const void *a,
const void *b,
const size_t size
);
```
### Structs and enums
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
brace and name on the same line:
```c
typedef struct {
errorcode_t code;
char_t *message;
} errorstate_t;
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
```
### Designated initialisers
Spaces inside braces; `.field = value`:
```c
jsassetentry_t e = { .entry = entry };
assetbatchloadedpend_t init = { .batch = batch };
```
### Ternary operator
Spaces around `?` and `:`:
```c
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
```
### const placement
`const` before the type, `*` attached to the variable:
```c
const char_t *name
const void *src
const size_t size
```
### Comments in `.c` files
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
functions follow one another with a single blank line between them.
- Multi-line explanatory comments inside function bodies use `//` lines:
```c
// Script modules are freed; orphaned JS wrapper objects now get GC'd
// so their finalizers fire before assetDispose() checks ref counts.
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
```
- Do not use `/* */` for inline or inline-block comments inside `.c`
function bodies.
### Comments in `.h` files
Every public declaration gets a Javadoc block (`/** … */`) with
`@param` and `@returns` where relevant. Keep it on the lines immediately
above the declaration with no blank line in between.
---
## Color system
Colors are defined in `src/dusk/display/color.csv` and code-generated
into a `color.h` header by `tools/color/csv/__main__.py`.
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
The script emits four `#define` variants per color plus a bare alias:
```
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
COLOR_<NAME>_3B color3b(r8, g8, b8)
COLOR_<NAME>_3F color3f(rf, gf, bf)
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
COLOR_<NAME> COLOR_<NAME>_4B
```
`color_t` is `color4b_t` (four `uint8_t` channels).
To add a new color, append a row to `color.csv` and rebuild — do not
hand-edit the generated header.
---
## Tests
- Tests live in `test/` mirroring `src/dusk/` structure.
- Use cmocka; include `dusktest.h`.
- Test functions: `static void test_something(void **state)`.
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
leaks.
- Build with `-DDUSK_BUILD_TESTS=ON`.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+53 -43
View File
@@ -10,51 +10,61 @@ msgid "ui.title"
msgstr ""
"Welcome"
#: ui/user.c:22
msgid "ui.greeting"
msgstr "Hello, %s!"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "General"
#: ui/files.c:40
msgid "ui.file_status"
msgstr "%s has %d files."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "Input"
#: ui/cart.c:55
msgid "cart.item_count"
msgid_plural "cart.item_count"
msgstr[0] "%d item"
msgstr[1] "%d items (dual)"
msgstr[2] "%d items (few)"
msgstr[3] "%d items (many)"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "Display"
#: ui/notifications.c:71
msgid ""
"ui.multiline_help"
msgstr ""
"Line one of the help text.\n"
"Line two continues here.\n"
"Line three ends here."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "Audio"
#: ui/errors.c:90
msgid ""
"error.upload_failed.long"
msgstr ""
"Upload failed for file \"%s\".\n"
"Please try again later or contact support."
msgid "ui.settings.input.deadzone"
msgstr "Deadzone"
#: ui/messages.c:110
msgid ""
"user.invite_status"
msgid_plural ""
"user.invite_status"
msgstr[0] ""
"%s invited %d user.\n"
"Please review the request."
msgstr[1] ""
"%s invited %d users (dual).\n"
"Please review the requests."
msgstr[2] ""
"%s invited %d users (few).\n"
"Please review the requests."
msgstr[3] ""
"%s invited %d users (many).\n"
"Please review the requests."
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "Language"
msgid "ui.settings.general.language_detail"
msgstr "Takes effect after restarting the application."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "Apply"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "Discard unsaved changes?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "Characters"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "Items"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "Settings"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "Save"
msgid "item.potion.name"
msgstr "Potion"
msgid "item.potato.name"
msgstr "Potato"
msgid "item.apple.name"
msgstr "Apple"
+74
View File
@@ -0,0 +1,74 @@
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: es\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"Bienvenido"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "General"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "Entrada"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "Pantalla"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "Audio"
msgid "ui.settings.input.deadzone"
msgstr "Deadzone"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "Idioma"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language_detail"
msgstr "Se aplica después de reiniciar la aplicación."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "Aplicar"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "¿Descartar los cambios no guardados?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "Personajes"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "Objetos"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "Configuración"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "Guardar"
#: src/dusk/rpg/item/item.json
msgid "item.potion.name"
msgstr "Poción"
#: src/dusk/rpg/item/item.json
msgid "item.potato.name"
msgstr "Papa"
#: src/dusk/rpg/item/item.json
msgid "item.apple.name"
msgstr "Manzana"
+74
View File
@@ -0,0 +1,74 @@
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: ja\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=1; plural=(0);\n"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"歓迎"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "一般"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "入力"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "表示"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "オーディオ"
msgid "ui.settings.input.deadzone"
msgstr "デッドゾーン"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "言語"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language_detail"
msgstr "アプリケーションを再起動すると適用されます。"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "適用"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "未保存の変更を破棄しますか?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "キャラクター"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "アイテム"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "設定"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "セーブ"
#: src/dusk/rpg/item/item.json
msgid "item.potion.name"
msgstr "ポーション"
#: src/dusk/rpg/item/item.json
msgid "item.potato.name"
msgstr "ジャガイモ"
#: src/dusk/rpg/item/item.json
msgid "item.apple.name"
msgstr "リンゴ"
Binary file not shown.
Binary file not shown.
+22
View File
@@ -2179,5 +2179,27 @@
0
]
}
],
"entities": [
{
"type": "global",
"globalId": 3,
"pos": [8, 8, 1]
},
{
"type": "item",
"itemId": 1,
"quantity": 1,
"pos": [12, 2, 0]
}
],
"areas": [
{
"min": [11, 3, 0],
"max": [16, 9, 10],
"callbackId": 1,
"notify": 3,
"trigger": 6
}
]
}
+3
View File
@@ -16,6 +16,9 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DOL=1
ISO=2
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
# GameCube/Wii PowerPC is always big-endian; declare it at compile time
# like every other target instead of relying on endian.h's runtime probe.
DUSK_PLATFORM_ENDIAN_BIG
)
# Custom compiler flags
+1 -1
View File
@@ -36,7 +36,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
# DUSK_OPENGL_LEGACY
DUSK_LINUX
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640
DUSK_DISPLAY_WIDTH_DEFAULT=854
DUSK_DISPLAY_HEIGHT_DEFAULT=480
DUSK_DISPLAY_SCREEN_HEIGHT=240
DUSK_INPUT_KEYBOARD
+1
View File
@@ -56,6 +56,7 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_DISPLAY_HEIGHT=272
DUSK_THREAD_PTHREAD
DUSK_TIME_DYNAMIC
DUSK_DISPLAY_OVERSCAN=6
)
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
+36 -45
View File
@@ -59,7 +59,10 @@ assetentry_t * assetGetEntry(
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// We did not find one existing, Find first available slot.
// We did not find one existing. Find first available slot, reaping
// zero-ref entries to make room if none are immediately available.
bool_t reaped = false;
for(;;) {
entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
@@ -74,6 +77,11 @@ assetentry_t * assetGetEntry(
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
if(reaped) break;
reaped = true;
errorCatch(assetReapUnused());
}
assertUnreachable("No available asset entry slots.");
return NULL;
}
@@ -191,6 +199,32 @@ void assetUnlockEntry(assetentry_t *entry) {
assetEntryUnlock(entry);
}
errorret_t assetReapUnused(void) {
assertIsMainThread("assetReapUnused must be called from the main thread.");
// Repeatedly find and dispose zero-ref LOADED entries until none remain.
// This handles dependency chains where an entry (e.g. a model) holds refs
// on child entries (mesh, texture): dispose parents first so child ref
// counts drop to zero, then pick up the children on the next pass. Without
// this, a forward-only scan fails when a shared child entry appears before
// a parent that still holds a ref to it.
bool_t any;
do {
any = false;
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; }
if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; }
if(entry->refs.count > 0) { entry++; continue; }
errorChain(assetEntryDispose(entry));
any = true;
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
errorOk();
}
errorret_t assetUpdate(void) {
assertIsMainThread("assetUpdate must be called from the main thread.");
@@ -324,30 +358,6 @@ errorret_t assetUpdate(void) {
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Reap unused entries.
entry = ASSET.entries;
do {
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
entry++;
continue;
}
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->refs.count > 0) {
entry++;
continue;
}
// consolePrint("Reaping asset %s", entry->name);
errorChain(assetEntryDispose(entry));
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
errorOk();
}
@@ -413,26 +423,7 @@ errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread);
// Drain-dispose: repeatedly find and dispose zero-ref LOADED entries
// until none remain. This handles dependency chains where an entry
// (e.g. a model) holds refs on child entries (mesh, texture): dispose
// parents first so child ref counts drop to zero, then pick up the
// children on the next pass. Without this, a forward-only scan fails
// when a shared child entry appears before a parent that still holds a
// ref to it.
bool_t any;
do {
any = false;
assetentry_t *e = ASSET.entries;
do {
if(e->type == ASSET_LOADER_TYPE_NULL) { e++; continue; }
if(e->state != ASSET_ENTRY_STATE_LOADED) { e++; continue; }
if(e->refs.count > 0) { e++; continue; }
errorChain(assetEntryDispose(e));
any = true;
e++;
} while(e < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
errorChain(assetReapUnused());
// Cleanup zip file.
if(ASSET.zip != NULL) {
+12 -2
View File
@@ -23,8 +23,8 @@
#define ASSET_FILE_NAME "dusk.dsk"
#define ASSET_HEADER_SIZE 3
#define ASSET_LOADING_COUNT_MAX 20
#define ASSET_ENTRY_COUNT_MAX 128
#define ASSET_LOADING_COUNT_MAX 10
#define ASSET_ENTRY_COUNT_MAX 64
typedef struct asset_s {
zip_t *zip;
@@ -112,6 +112,16 @@ void assetUnlock(const char_t *name);
*/
void assetUnlockEntry(assetentry_t *entry);
/**
* Frees every currently unreferenced (zero-ref) loaded asset entry. Repeats
* until a full pass frees nothing further, since disposing a parent entry
* (e.g. a model) may drop a child entry's (e.g. a mesh) ref count to zero,
* making it eligible for reaping too.
*
* @return An error code if any entry could not be disposed properly.
*/
errorret_t assetReapUnused(void);
/**
* Requires an asset entry to be loaded. This will block until the asset entry
* is fully loaded.
@@ -14,6 +14,26 @@
#include "asset/loader/assetloader.h"
#include "asset/asset.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) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
@@ -111,9 +131,15 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
size_t offset = 8;
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
out->tiles = memoryAllocate(tileSize);
memoryCopy(out->tiles, data + offset, tileSize);
offset += tileSize;
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) {
uint32_t *shape = (uint32_t *)&out->tiles[t].shape;
*shape = endianLittleToHost32(*shape);
}
out->meshCount = data[offset];
offset += sizeof(uint8_t);
assertTrue(
@@ -135,6 +161,65 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
offset += sizeof(vec3);
out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]);
out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]);
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
}
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);
@@ -157,6 +242,12 @@ errorret_t assetChunkDispose(assetentry_t *entry) {
assertIsMainThread("Must be called from the main thread.");
assetchunkoutput_t *out = &entry->data.chunk;
if(out->tiles != NULL) {
memoryFree(out->tiles);
out->tiles = NULL;
}
for(uint8_t m = 0; m < out->meshCount; m++) {
if(out->modelEntries[m] == NULL) continue;
assetUnlockEntry(out->modelEntries[m]);
+29 -2
View File
@@ -9,7 +9,7 @@
#include "asset/assetfile.h"
#include "rpg/overworld/chunk.h"
#define ASSET_CHUNK_FILE_VERSION 3
#define ASSET_CHUNK_FILE_VERSION 5
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
@@ -33,12 +33,39 @@ typedef struct {
uint8_t modelIndex;
} assetchunkloaderloading_t;
typedef enum {
CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
CHUNK_ENTITY_SPAWN_KIND_ITEM
} chunkentityspawnkind_t;
typedef struct {
tile_t tiles[CHUNK_TILE_COUNT];
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 {
tile_t *tiles;
uint8_t meshCount;
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[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;
/**
+23 -4
View File
@@ -48,8 +48,20 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
meshvertex_t *vertices = NULL;
if(vertCount > 0) {
vertices = memoryAllocate(vertCount * sizeof(meshvertex_t));
// 32-byte (cache-line) aligned: GX_SetArray + DCFlushRange on Dolphin
// require this for the DMA'd vertex data to actually reach the GPU
// coherently. Static compiled-in vertex arrays happen to get this from
// the linker; a plain memoryAllocate here would not.
vertices = memoryAlign(32, vertCount * sizeof(meshvertex_t));
memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t));
for(uint32_t v = 0; v < vertCount; v++) {
vertices[v].uv[0] = endianLittleToHostFloat(vertices[v].uv[0]);
vertices[v].uv[1] = endianLittleToHostFloat(vertices[v].uv[1]);
vertices[v].pos[0] = endianLittleToHostFloat(vertices[v].pos[0]);
vertices[v].pos[1] = endianLittleToHostFloat(vertices[v].pos[1]);
vertices[v].pos[2] = endianLittleToHostFloat(vertices[v].pos[2]);
}
}
memoryFree(raw);
@@ -103,18 +115,22 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
out->vertices = NULL;
errorChain(ret);
}
out->meshInitialized = true;
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
meshDispose(&out->mesh);
out->meshInitialized = false;
memoryFree(out->vertices);
out->vertices = NULL;
errorChain(ret);
}
#ifndef DUSK_OPENGL_LEGACY
// VBO owns the data now; CPU copy is no longer needed.
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
// VBO owns the data now; CPU copy is no longer needed. The platform
// mesh object itself still needs meshDispose later - tracked via
// out->meshInitialized, independent of the CPU buffer's lifetime.
memoryFree(out->vertices);
out->vertices = NULL;
#endif
@@ -129,8 +145,11 @@ errorret_t assetMeshDispose(assetentry_t *entry) {
assertIsMainThread("Must be called from the main thread.");
assetmeshoutput_t *out = &entry->data.mesh;
if(out->vertices != NULL) {
if(out->meshInitialized) {
errorChain(meshDispose(&out->mesh));
out->meshInitialized = false;
}
if(out->vertices != NULL) {
memoryFree(out->vertices);
out->vertices = NULL;
}
@@ -30,6 +30,7 @@ typedef struct {
typedef struct {
mesh_t mesh;
bool_t meshInitialized;
meshvertex_t *vertices;
} assetmeshoutput_t;
@@ -513,9 +513,10 @@ errorret_t assetLocaleGetString(
// Skip blanks, comments, etc and start looking for msgid's
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
while(!reader.eof) {
while(true) {
// Is this msgid?
if(memoryCompare(lineBuffer, "msgid", 5) != 0) {
if(reader.eof) break;
errorChain(assetFileLineReaderNext(&reader));
msgidBuffer[0] = '\0';
continue;
@@ -539,7 +540,7 @@ errorret_t assetLocaleGetString(
}
// We are either going to see a msgstr or a msgid_plural
while(!reader.eof) {
while(true) {
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
// Is msgid_plural?
+1 -1
View File
@@ -17,7 +17,7 @@ console_t CONSOLE;
void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t));
CONSOLE.visible = true;
CONSOLE.visible = false;
#ifdef DUSK_CONSOLE_POSIX
threadMutexInit(&CONSOLE.printMutex);
+3 -2
View File
@@ -37,7 +37,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(systemInit());
errorChain(inputInit());
errorChain(assetInit());
// errorChain(saveInit());
errorChain(saveInit());
errorChain(localeManagerInit());
errorChain(displayInit());
errorChain(uiInit());
@@ -62,6 +62,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorret_t engineUpdate(void) {
// Order here is important.
errorChain(networkUpdate());
errorChain(saveUpdate());
timeUpdate();
inputUpdate();
consoleUpdate();
@@ -88,7 +89,7 @@ errorret_t engineDispose(void) {
errorChain(uiDispose());
consoleDispose();
errorChain(displayDispose());
// errorChain(saveDispose());
errorChain(saveDispose());
errorChain(assetDispose());
errorOk();
+10
View File
@@ -17,3 +17,13 @@ static const localeinfo_t LOCALE_EN_US = {
.name = "en-US",
.file = "locale/en_US.po",
};
static const localeinfo_t LOCALE_JP_JP = {
.name = "ja-JP",
.file = "locale/jp_JP.po",
};
static const localeinfo_t LOCALE_ES_MX = {
.name = "es-MX",
.file = "locale/es_MX.po",
};
+1
View File
@@ -11,6 +11,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
)
# Subdirs
add_subdirectory(battle)
add_subdirectory(cutscene)
add_subdirectory(entity)
add_subdirectory(overworld)
+12
View File
@@ -0,0 +1,12 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
battle.c
battlefighter.c
party.c
)
+223
View File
@@ -0,0 +1,223 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "battle.h"
#include "assert/assert.h"
#include "util/memory.h"
battle_t BATTLE;
void battleInit(void) {
memoryZero(&BATTLE, sizeof(battle_t));
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
BATTLE.fighters[i].id = i;
}
}
uint8_t battleGetAvailableFighter(void) {
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
}
return 0xFF;
}
battlefighter_t *battleAddFighter(
const battlefighterteam_t team,
const battlefightercontroller_t controller,
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
) {
const uint8_t index = battleGetAvailableFighter();
if(index == 0xFF) return NULL;
battlefighter_t *fighter = &BATTLE.fighters[index];
battleFighterInit(fighter, team, controller, stats, healthMax, mpMax);
return fighter;
}
void battleStart(
const battleencountertype_t encounterType,
const bool_t fleeAvailable
) {
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
BATTLE.encounterType = encounterType;
BATTLE.fleeAvailable = fleeAvailable;
BATTLE.result = BATTLE_RESULT_NONE;
BATTLE.round = 1;
BATTLE.turnIndex = 0;
battleBuildTurnOrder(true);
BATTLE.active = true;
}
void battleDispose(void) {
battleInit();
}
battlefighter_t *battleGetCurrentFighter(void) {
if(!BATTLE.active) return NULL;
if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
}
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
uint8_t count = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(BATTLE.fighters[i].team != team) continue;
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
count++;
}
return count;
}
void battleResolveAttack(
battlefighter_t *attacker,
battlefighter_t *defender
) {
assertNotNull(attacker, "Attacker cannot be NULL");
assertNotNull(defender, "Defender cannot be NULL");
const int32_t rawDamage =
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
defender->health = damage >= defender->health ? 0 : defender->health - damage;
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
}
void battleNextTurn(void) {
BATTLE.turnIndex++;
if(BATTLE.turnIndex < BATTLE.turnCount) return;
BATTLE.round++;
BATTLE.turnIndex = 0;
battleBuildTurnOrder(false);
}
battleresult_t battleCheckResult(void) {
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
BATTLE.result = BATTLE_RESULT_LOSS;
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
BATTLE.result = BATTLE_RESULT_WIN;
}
return BATTLE.result;
}
void battlePlayerAttack(const uint8_t targetIndex) {
battlefighter_t *attacker = battleGetCurrentFighter();
if(attacker == NULL) return;
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
battlefighter_t *defender = &BATTLE.fighters[targetIndex];
if(!battleFighterIsAlive(defender)) return;
battleResolveAttack(attacker, defender);
battleCheckResult();
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
}
void battlePlayerFlee(void) {
battlefighter_t *fighter = battleGetCurrentFighter();
if(fighter == NULL) return;
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
if(!BATTLE.fleeAvailable) return;
BATTLE.result = BATTLE_RESULT_FLED;
}
void battleUpdate(void) {
if(!BATTLE.active) return;
if(BATTLE.result != BATTLE_RESULT_NONE) return;
battlefighter_t *current = battleGetCurrentFighter();
if(current == NULL) return;
if(!battleFighterIsAlive(current)) {
battleNextTurn();
return;
}
if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
battlefighter_t *target = battleAIChooseTarget(current);
if(target != NULL) battleResolveAttack(current, target);
battleCheckResult();
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
}
void battleBuildTurnOrder(const bool_t applyEncounterBias) {
BATTLE.turnCount = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
BATTLE.turnOrder[BATTLE.turnCount++] = i;
}
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
const uint8_t key = BATTLE.turnOrder[i];
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
int8_t j = (int8_t)i - 1;
while(
j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
) {
BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
j--;
}
BATTLE.turnOrder[j + 1] = key;
}
if(!applyEncounterBias) return;
if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) {
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY);
} else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) {
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY);
}
}
void battleMoveTeamFirst(const battlefighterteam_t team) {
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
uint8_t count = 0;
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
sorted[count++] = BATTLE.turnOrder[i];
}
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
sorted[count++] = BATTLE.turnOrder[i];
}
memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
}
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
const battlefighterteam_t enemyTeam =
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
battlefighter_t *weakest = NULL;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
battlefighter_t *candidate = &BATTLE.fighters[i];
if(candidate->team != enemyTeam) continue;
if(!battleFighterIsAlive(candidate)) continue;
if(weakest == NULL || candidate->health < weakest->health) {
weakest = candidate;
}
}
return weakest;
}
+194
View File
@@ -0,0 +1,194 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "battlefighter.h"
#define BATTLE_FIGHTER_COUNT_MAX 8
typedef enum {
BATTLE_ENCOUNTER_REGULAR,
BATTLE_ENCOUNTER_PLAYER_ADVANTAGE,
BATTLE_ENCOUNTER_BACK_ATTACK,
BATTLE_ENCOUNTER_COUNT
} battleencountertype_t;
typedef enum {
BATTLE_RESULT_NONE,
BATTLE_RESULT_WIN,
BATTLE_RESULT_LOSS,
BATTLE_RESULT_FLED,
BATTLE_RESULT_COUNT
} battleresult_t;
typedef struct {
bool_t active;
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
battleencountertype_t encounterType;
bool_t fleeAvailable;
battleresult_t result;
// Fighter indices (into fighters[]), sorted for the current round.
uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
uint8_t turnCount;
uint8_t turnIndex;
uint16_t round;
} battle_t;
extern battle_t BATTLE;
/**
* Initializes the battle system. Marks it as inactive with no fighters.
*/
void battleInit(void);
/**
* Gets an available (unused) fighter slot index.
*
* @return The index of an available fighter slot, or 0xFF if none are
* available.
*/
uint8_t battleGetAvailableFighter(void);
/**
* Adds a fighter to the battle in the next available slot.
*
* @param team The team the fighter belongs to.
* @param controller Who makes decisions for the fighter.
* @param stats The fighter's base combat stats.
* @param healthMax The fighter's maximum health.
* @param mpMax The fighter's maximum mp.
* @return Pointer to the newly added fighter, or NULL if the battle is
* already full.
*/
battlefighter_t *battleAddFighter(
const battlefighterteam_t team,
const battlefightercontroller_t controller,
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
);
/**
* Starts the battle: builds the opening turn order (biased by
* encounterType for the first round only) and marks the battle active.
* Call once every fighter has been added via battleAddFighter.
*
* @param encounterType Determines the opening round's turn order.
* @param fleeAvailable Whether the party may attempt to flee this battle.
*/
void battleStart(
const battleencountertype_t encounterType,
const bool_t fleeAvailable
);
/**
* Disposes of the battle, clearing all fighters and marking it inactive.
*/
void battleDispose(void);
/**
* Returns the fighter whose turn it currently is.
*
* @return Pointer to the active fighter, or NULL if the battle isn't
* active or has no living fighters left to act.
*/
battlefighter_t *battleGetCurrentFighter(void);
/**
* Returns the number of living fighters on a team.
*
* @param team The team to count.
* @return Count of living fighters on that team.
*/
uint8_t battleGetAliveCount(const battlefighterteam_t team);
/**
* Resolves a physical attack from attacker onto defender: damage is the
* attacker's attack stat minus the defender's defense stat (minimum 1),
* subtracted from the defender's health. The defender is marked dead
* once health reaches 0.
*
* @param attacker The attacking fighter.
* @param defender The defending fighter.
*/
void battleResolveAttack(
battlefighter_t *attacker,
battlefighter_t *defender
);
/**
* Ends the current fighter's turn and advances to the next fighter in
* the turn order, starting a new round (rebuilding turn order purely by
* speed) once every fighter in the current round has acted.
*/
void battleNextTurn(void);
/**
* Checks whether the battle has been won or lost, updating and
* returning BATTLE.result. Does nothing if a result has already been
* set (e.g. by a successful flee).
*
* @return The battle's current result.
*/
battleresult_t battleCheckResult(void);
/**
* Submits the current fighter's attack against a target, if it is
* currently a player-controlled fighter's turn. Resolves the attack,
* checks for a battle result, and advances the turn.
*
* @param targetIndex Index into BATTLE.fighters of the target.
*/
void battlePlayerAttack(const uint8_t targetIndex);
/**
* Submits a flee attempt for the current fighter's turn, if it is
* currently a player-controlled fighter's turn and fleeing is
* available for this battle. Always succeeds, ending the battle with
* BATTLE_RESULT_FLED.
*/
void battlePlayerFlee(void);
/**
* Updates the battle simulation for one frame: resolves the current
* fighter's turn automatically if AI-controlled, otherwise waits for a
* player action via battlePlayerAttack/battlePlayerFlee. No-op if the
* battle isn't active or already has a result.
*/
void battleUpdate(void);
/**
* Rebuilds BATTLE.turnOrder/turnCount from every currently living
* fighter, sorted by speed descending.
*
* @param applyEncounterBias If true, reorders the freshly speed-sorted
* queue so BATTLE.encounterType's favoured team goes first (used only
* for the opening round).
*/
void battleBuildTurnOrder(const bool_t applyEncounterBias);
/**
* Stably partitions BATTLE.turnOrder so every fighter on the given team
* comes first, preserving each side's relative (speed-sorted) order.
*
* @param team The team to move to the front of the turn order.
*/
void battleMoveTeamFirst(const battlefighterteam_t team);
/**
* Picks an AI target for fighter: the lowest-health living fighter on
* the opposing team.
*
* @param fighter The AI-controlled fighter choosing a target.
* @return The chosen target, or NULL if the opposing team has no
* living fighters.
*/
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
+43
View File
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "battlefighter.h"
#include "assert/assert.h"
#include "util/memory.h"
void battleFighterInit(
battlefighter_t *fighter,
const battlefighterteam_t team,
const battlefightercontroller_t controller,
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
) {
assertNotNull(fighter, "Fighter pointer cannot be NULL");
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
assertTrue(
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
"Invalid fighter controller"
);
const uint8_t id = fighter->id;
memoryZero(fighter, sizeof(battlefighter_t));
fighter->id = id;
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
fighter->team = team;
fighter->controller = controller;
fighter->stats = stats;
fighter->healthMax = healthMax;
fighter->health = healthMax;
fighter->mpMax = mpMax;
fighter->mp = mpMax;
}
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
assertNotNull(fighter, "Fighter pointer cannot be NULL");
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
}
+87
View File
@@ -0,0 +1,87 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
// An empty status means the slot in BATTLE.fighters is unused.
typedef enum {
BATTLE_FIGHTER_STATUS_NULL,
BATTLE_FIGHTER_STATUS_NORMAL,
BATTLE_FIGHTER_STATUS_DEAD,
BATTLE_FIGHTER_STATUS_COUNT
} battlefighterstatus_t;
typedef enum {
BATTLE_FIGHTER_TEAM_ALLY,
BATTLE_FIGHTER_TEAM_ENEMY,
BATTLE_FIGHTER_TEAM_COUNT
} battlefighterteam_t;
typedef enum {
BATTLE_FIGHTER_CONTROLLER_PLAYER,
BATTLE_FIGHTER_CONTROLLER_AI,
BATTLE_FIGHTER_CONTROLLER_COUNT
} battlefightercontroller_t;
// Base combat stats, kept separate from the resource pools (health/mp) on
// battlefighter_t so that equipment/buffs can later modify them without
// touching current health/mp state.
typedef struct {
uint16_t attack;
uint16_t defense;
uint16_t magic;
uint16_t speed;
uint16_t luck;
} battlefighterstats_t;
typedef struct {
uint8_t id;
battlefighterstatus_t status;
battlefighterteam_t team;
battlefightercontroller_t controller;
uint16_t health;
uint16_t healthMax;
uint16_t mp;
uint16_t mpMax;
battlefighterstats_t stats;
} battlefighter_t;
/**
* Initializes a battle fighter in place, filling health/mp to their maximum
* values and setting its status to normal.
*
* @param fighter Pointer to the fighter to initialize.
* @param team The team the fighter belongs to.
* @param controller Who makes decisions for the fighter.
* @param stats The fighter's base combat stats.
* @param healthMax The fighter's maximum health.
* @param mpMax The fighter's maximum mp.
*/
void battleFighterInit(
battlefighter_t *fighter,
const battlefighterteam_t team,
const battlefightercontroller_t controller,
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
);
/**
* Returns true if the fighter is in a state where it can still act (i.e.
* is not dead).
*
* @param fighter Pointer to the fighter to check.
* @returns True if the fighter can act.
*/
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
+71
View File
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "party.h"
#include "assert/assert.h"
#include "util/memory.h"
party_t PARTY;
void partyInit(void) {
memoryZero(&PARTY, sizeof(party_t));
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
PARTY.members[i].id = i;
}
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
PARTY.order[i] = PARTY_ORDER_EMPTY;
}
}
uint8_t partyGetAvailableMember(void) {
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
if(PARTY.members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
}
return 0xFF;
}
battlefighter_t *partyAddMember(
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
) {
const uint8_t index = partyGetAvailableMember();
if(index == 0xFF) return NULL;
battlefighter_t *member = &PARTY.members[index];
battleFighterInit(
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
stats, healthMax, mpMax
);
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
if(PARTY.order[i] != PARTY_ORDER_EMPTY) continue;
PARTY.order[i] = index;
break;
}
return member;
}
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
const uint8_t index = PARTY.order[slot];
if(index == PARTY_ORDER_EMPTY) return NULL;
return &PARTY.members[index];
}
void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
assertTrue(
memberIndex == PARTY_ORDER_EMPTY || memberIndex < PARTY_MEMBER_COUNT_MAX,
"Invalid party member index"
);
PARTY.order[slot] = memberIndex;
}
+73
View File
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "battlefighter.h"
#define PARTY_MEMBER_COUNT_MAX 4
#define PARTY_ACTIVE_SIZE_MAX 3
#define PARTY_ORDER_EMPTY 0xFF
typedef struct {
battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
// Maps an active battle slot to the roster member filling it, or
// PARTY_ORDER_EMPTY if the slot is unfilled. Only the first
// PARTY_ACTIVE_SIZE_MAX of the PARTY_MEMBER_COUNT_MAX roster members
// can be in the active lineup at once.
uint8_t order[PARTY_ACTIVE_SIZE_MAX];
} party_t;
extern party_t PARTY;
/**
* Initializes the party system with an empty roster and order.
*/
void partyInit(void);
/**
* Gets an available (unused) party member slot index.
*
* @return The index of an available slot, or 0xFF if the party is full.
*/
uint8_t partyGetAvailableMember(void);
/**
* Adds a member to the party roster in the next available slot. Party
* members are always allies controlled by the player. If there is a
* free active order slot, the new member is placed into it.
*
* @param stats The member's base combat stats.
* @param healthMax The member's maximum health.
* @param mpMax The member's maximum mp.
* @return Pointer to the newly added party member, or NULL if the party is
* already full.
*/
battlefighter_t *partyAddMember(
const battlefighterstats_t stats,
const uint16_t healthMax,
const uint16_t mpMax
);
/**
* Gets the roster member currently occupying an active order slot.
*
* @param slot The active order slot to query.
* @return Pointer to the member in that slot, or NULL if the slot is
* empty.
*/
battlefighter_t *partyGetOrderMember(const uint8_t slot);
/**
* Assigns a roster member to an active order slot, replacing whatever
* was there. Use PARTY_ORDER_EMPTY to clear a slot.
*
* @param slot The active order slot to assign.
* @param memberIndex The roster member index to place there, or
* PARTY_ORDER_EMPTY to clear the slot.
*/
void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
+116 -2
View File
@@ -13,14 +13,19 @@ typedef struct cutscene_s {
const cutsceneitem_t *items;
uint8_t itemCount;
cutscenepause_t pause;
// Size in bytes of this cutscene's custom user data, carved out of
// CUTSCENE_SYSTEM.data while the cutscene is running.
size_t dataSize;
} cutscene_t;
#define CUTSCENE(NAME, PAUSE_TYPE, ...) \
#define CUTSCENE(NAME, SIZE, PAUSE_TYPE, ...) \
static const cutsceneitem_t CUTSCENE_##NAME##_ITEMS[] = { __VA_ARGS__ }; \
static const cutscene_t CUTSCENE_##NAME = { \
.items = CUTSCENE_##NAME##_ITEMS, \
.itemCount = sizeof(CUTSCENE_##NAME##_ITEMS) / sizeof(cutsceneitem_t), \
.pause = CUTSCENE_PAUSE_##PAUSE_TYPE \
.pause = CUTSCENE_PAUSE_##PAUSE_TYPE, \
.dataSize = SIZE \
};
#define CUTSCENE_REFERENCE(CUTSCENE) \
@@ -29,6 +34,22 @@ typedef struct cutscene_s {
#define CUTSCENE_TEXT(TEXT) \
{ .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { .text = TEXT } }
#define CUTSCENE_TEXT_MINI(TEXT, X, Y, Z, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI, \
.textMini = { \
.text = TEXT, \
.position = { X, Y, Z }, \
.duration = DURATION \
} \
}
#define CUTSCENE_TEXT_MINI_HIDE(INDEX) \
{ \
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE, \
.textMiniHide = { .index = INDEX } \
}
#define CUTSCENE_WAIT(WAIT) \
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
@@ -70,6 +91,35 @@ typedef struct cutscene_s {
.entityRemove = { .entityIndex = ENTITY_INDEX } \
}
#define CUTSCENE_ENTITY_ADD(TYPE, X, Y, Z) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_ADD, \
.entityAdd = { .entityType = TYPE, .position = { X, Y, Z } } \
}
#define CUTSCENE_ENTITY_TURN(ENTITY_INDEX, DIRECTION) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_TURN, \
.entityTurn = { .entityIndex = ENTITY_INDEX, .direction = DIRECTION } \
}
// Walks ENTITY_INDEX to stand beside TARGET_ENTITY_INDEX, offset by
// (OFFSET_X, OFFSET_Y) on the 2D plane. The destination Z is resolved
// from nearby terrain each frame, so ramps between the two entities are
// accounted for automatically.
#define CUTSCENE_ENTITY_WALK_TO_ENTITY( \
ENTITY_INDEX, TARGET_ENTITY_INDEX, OFFSET_X, OFFSET_Y \
) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, \
.entityWalkToEntity = { \
.entityIndex = ENTITY_INDEX, \
.targetEntityIndex = TARGET_ENTITY_INDEX, \
.offsetX = OFFSET_X, \
.offsetY = OFFSET_Y \
} \
}
#define CUTSCENE_ENTITY_TELEPORT(ENTITY_INDEX, X, Y, Z) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, \
@@ -94,6 +144,24 @@ typedef struct cutscene_s {
#define CUTSCENE_FADE_FROM_WHITE(DURATION) \
CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR)
#define CUTSCENE_EMOJI(ENTITY_INDEX, EMOJI_TYPE, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_EMOJI, \
.emoji = { \
.entityIndex = ENTITY_INDEX, \
.emojiType = EMOJI_TYPE, \
.duration = DURATION \
} \
}
// AMOUNT ranges 0 (no shake) to 4 (three tiles): 1 is half a tile, 2 is
// a full tile, 3 is two tiles, and 4 is three tiles.
#define CUTSCENE_SHAKE(AMOUNT, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_SHAKE, \
.shake = { .amount = AMOUNT, .duration = DURATION } \
}
#define CUTSCENE_SET_PAUSE(FLAGS) \
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
@@ -116,3 +184,49 @@ typedef struct cutscene_s {
) \
} \
}
#define CUTSCENE_MAP_AREA_ADD( \
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, CALLBACK, NOTIFY, TRIGGER \
) \
{ \
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, \
.mapAreaAdd = { \
.min = { MIN_X, MIN_Y, MIN_Z }, \
.max = { MAX_X, MAX_Y, MAX_Z }, \
.callback = CALLBACK, \
.notify = NOTIFY, \
.trigger = TRIGGER \
} \
}
#define CUTSCENE_MAP_AREA_REMOVE(AREA_ID) \
{ \
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, \
.mapAreaRemove = { .areaId = AREA_ID } \
}
// Waits until any one of the given map area IDs has its callback invoked.
// Accepts CUTSCENE_AREA_LAST_CREATED in place of a literal area ID.
#define CUTSCENE_MAP_AREA_WAIT(...) \
{ \
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, \
.mapAreaWait = { \
.areaIds = (const uint8_t[]){ __VA_ARGS__ }, \
.count = (uint8_t)( \
sizeof((const uint8_t[]){ __VA_ARGS__ }) / sizeof(uint8_t) \
) \
} \
}
// Adds a map area, waits for it to be triggered once, then removes it
// before the cutscene continues. Uses a no-op callback since the wait is
// driven by the area's trigger count rather than callback logic.
#define CUTSCENE_MAP_AREA_TRIGGER_ONCE( \
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, NOTIFY, TRIGGER \
) \
CUTSCENE_MAP_AREA_ADD( \
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, \
mapAreaNoopCallback, NOTIFY, TRIGGER \
), \
CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \
CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED)
+63 -6
View File
@@ -25,10 +25,19 @@ void cutsceneSystemStartCutsceneWith(
entity_t *interact,
entity_t *interacted
) {
assertTrue(
cutscene->dataSize < CUTSCENE_SYSTEM_SIZE_MAX,
"Cutscene data size exceeds CUTSCENE_SYSTEM_SIZE_MAX"
);
CUTSCENE_SYSTEM.scene = cutscene;
CUTSCENE_SYSTEM.pause = cutscene->pause;
CUTSCENE_SYSTEM.entityInteract = interact;
CUTSCENE_SYSTEM.entityInteracted = interacted;
CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
cutsceneSystemNext();
}
@@ -54,6 +63,10 @@ void cutsceneSystemNext() {
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
CUTSCENE_SYSTEM.entityInteract = NULL;
CUTSCENE_SYSTEM.entityInteracted = NULL;
CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
return;
}
@@ -70,25 +83,65 @@ const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
}
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex) {
entity_t *entity;
if(entityIndex == CUTSCENE_ENTITY_INTERACT) {
assertNotNull(
CUTSCENE_SYSTEM.entityInteract,
"CUTSCENE_ENTITY_INTERACT used but no interact entity is set"
);
return CUTSCENE_SYSTEM.entityInteract;
}
if(entityIndex == CUTSCENE_ENTITY_INTERACTED) {
entity = CUTSCENE_SYSTEM.entityInteract;
} else if(entityIndex == CUTSCENE_ENTITY_INTERACTED) {
assertNotNull(
CUTSCENE_SYSTEM.entityInteracted,
"CUTSCENE_ENTITY_INTERACTED used but no interacted entity is set"
);
return CUTSCENE_SYSTEM.entityInteracted;
}
entity = CUTSCENE_SYSTEM.entityInteracted;
} else if(entityIndex == CUTSCENE_ENTITY_LAST_CREATED) {
assertNotNull(
CUTSCENE_SYSTEM.entityLastCreated,
"CUTSCENE_ENTITY_LAST_CREATED used but no entity has been created"
);
entity = CUTSCENE_SYSTEM.entityLastCreated;
} else if(entityIndex == CUTSCENE_ENTITY_LAST_REF) {
assertNotNull(
CUTSCENE_SYSTEM.entityLastRef,
"CUTSCENE_ENTITY_LAST_REF used but no entity has been referenced"
);
entity = CUTSCENE_SYSTEM.entityLastRef;
} else {
assertTrue(
entityIndex < ENTITY_COUNT,
"Entity index is out of range"
);
return &ENTITIES[entityIndex];
entity = &ENTITIES[entityIndex];
}
CUTSCENE_SYSTEM.entityLastRef = entity;
return entity;
}
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
if(areaId == CUTSCENE_AREA_LAST_CREATED) {
assertTrue(
CUTSCENE_SYSTEM.areaLastCreated != CUTSCENE_AREA_LAST_CREATED,
"CUTSCENE_AREA_LAST_CREATED used but no map area has been created"
);
return CUTSCENE_SYSTEM.areaLastCreated;
}
return areaId;
}
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
if(index == CUTSCENE_TEXT_MINI_LAST_CREATED) {
assertTrue(
CUTSCENE_SYSTEM.textMiniLastCreated != CUTSCENE_TEXT_MINI_LAST_CREATED,
"CUTSCENE_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
"shown"
);
return CUTSCENE_SYSTEM.textMiniLastCreated;
}
return index;
}
void cutsceneSystemDispose() {
@@ -97,4 +150,8 @@ void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
CUTSCENE_SYSTEM.entityInteract = NULL;
CUTSCENE_SYSTEM.entityInteracted = NULL;
CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
}
+37 -1
View File
@@ -12,6 +12,14 @@ typedef struct entity_s entity_t;
#define CUTSCENE_ENTITY_INTERACT ((uint8_t)0xFE)
#define CUTSCENE_ENTITY_INTERACTED ((uint8_t)0xFD)
#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
#define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF)
#define CUTSCENE_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA)
// Maximum number of bytes a running cutscene may request via
// cutscene_t.dataSize.
#define CUTSCENE_SYSTEM_SIZE_MAX 8192
typedef struct {
const cutscene_t *scene;
@@ -19,9 +27,17 @@ typedef struct {
cutscenepause_t pause;
entity_t *entityInteract;
entity_t *entityInteracted;
entity_t *entityLastCreated;
entity_t *entityLastRef;
uint8_t areaLastCreated;
uint8_t textMiniLastCreated;
// Data (used by the current item).
cutsceneitemdata_t data;
// Custom user data for the running cutscene, sized per-scene by
// cutscene_t.dataSize.
uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX];
} cutscenesystem_t;
extern cutscenesystem_t CUTSCENE_SYSTEM;
@@ -53,7 +69,9 @@ void cutsceneSystemStartCutsceneWith(
/**
* Resolves a raw entity index (or sentinel) to an entity pointer.
* Handles CUTSCENE_ENTITY_INTERACT and CUTSCENE_ENTITY_INTERACTED.
* Handles CUTSCENE_ENTITY_INTERACT, CUTSCENE_ENTITY_INTERACTED,
* CUTSCENE_ENTITY_LAST_CREATED and CUTSCENE_ENTITY_LAST_REF.
* Updates CUTSCENE_SYSTEM.entityLastRef to the resolved entity.
* Asserts the resolved entity is within bounds.
*
* @param entityIndex Raw entity index or sentinel value.
@@ -61,6 +79,24 @@ void cutsceneSystemStartCutsceneWith(
*/
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
/**
* Resolves a raw map area ID (or CUTSCENE_AREA_LAST_CREATED sentinel) to
* a concrete map area ID.
*
* @param areaId Raw map area ID or sentinel value.
* @returns The resolved map area ID.
*/
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
/**
* Resolves a raw mini textbox slot index (or CUTSCENE_TEXT_MINI_LAST_CREATED
* sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index.
*
* @param index Raw slot index or sentinel value.
* @returns The resolved slot index.
*/
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
/**
* Advance to the next item in the cutscene.
*/
@@ -12,4 +12,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(control)
add_subdirectory(entity)
add_subdirectory(item)
add_subdirectory(maparea)
add_subdirectory(ui)
add_subdirectory(battle)
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscenestartbattle.c
)
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/battle/party.h"
#include "scene/scene.h"
void cutsceneStartBattleStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
const cutscenestartbattle_t *config = &item->startBattle;
battleInit();
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
battlefighter_t *member = partyGetOrderMember(i);
if(member == NULL) continue;
battlefighter_t *fighter = battleAddFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
member->stats, member->healthMax, member->mpMax
);
if(fighter == NULL) continue;
fighter->health = member->health;
fighter->mp = member->mp;
fighter->status = member->status;
}
for(uint8_t i = 0; i < config->enemyCount; i++) {
const cutscenestartbattleenemy_t *enemy = &config->enemies[i];
battleAddFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
enemy->stats, enemy->healthMax, enemy->mpMax
);
}
battleStart(config->encounterType, config->fleeAvailable);
sceneSet(SCENE_TYPE_BATTLE);
}
bool_t cutsceneStartBattleUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
if(BATTLE.result == BATTLE_RESULT_NONE) return false;
// Sync ally HP/MP back to the persistent party roster. Relies on
// ally fighters having been added to BATTLE.fighters in the same
// order partyGetOrderMember() iterates, starting at index 0 (see
// cutsceneStartBattleStart).
uint8_t allySlot = 0;
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
battlefighter_t *member = partyGetOrderMember(i);
if(member == NULL) continue;
battlefighter_t *fighter = &BATTLE.fighters[allySlot++];
member->health = fighter->health;
member->mp = fighter->mp;
member->status = fighter->status;
}
sceneSet(SCENE_TYPE_OVERWORLD);
battleDispose();
return true;
}
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/battle/battle.h"
#define CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX 4
typedef struct {
battlefighterstats_t stats;
uint16_t healthMax;
uint16_t mpMax;
} cutscenestartbattleenemy_t;
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
battleencountertype_t encounterType;
bool_t fleeAvailable;
uint8_t enemyCount;
cutscenestartbattleenemy_t enemies[CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX];
} cutscenestartbattle_t;
/**
* Starts a battle: seeds BATTLE with the party's active order members
* and the item's configured enemies, then switches to the battle
* scene.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneStartBattleStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Waits for the battle to produce a result, syncs ally HP/MP back to
* the party roster, then returns to the overworld scene.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once the battle has ended.
*/
bool_t cutsceneStartBattleUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -6,12 +6,13 @@
*/
#include "cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
void cutsceneCallbackStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
if(item->callback != NULL) item->callback();
if(item->callback != NULL) item->callback(CUTSCENE_SYSTEM.userData);
}
bool_t cutsceneCallbackUpdate(
@@ -11,7 +11,7 @@
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef void (*cutscenecallback_t)(void);
typedef void (*cutscenecallback_t)(void *userData);
/**
* Starts a callback item (invokes the callback immediately).
+134 -78
View File
@@ -7,90 +7,146 @@
#include "rpg/cutscene/cutscenesystem.h"
cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
[CUTSCENE_ITEM_TYPE_NULL] = { 0 },
[CUTSCENE_ITEM_TYPE_TEXT] = {
.init = cutsceneTextStart,
.update = cutsceneTextUpdate
},
[CUTSCENE_ITEM_TYPE_TEXT_MINI] = {
.init = cutsceneTextMiniStart,
.update = cutsceneTextMiniUpdate
},
[CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE] = {
.init = cutsceneTextMiniHideStart,
.update = cutsceneTextMiniHideUpdate
},
[CUTSCENE_ITEM_TYPE_CALLBACK] = {
.init = cutsceneCallbackStart,
.update = cutsceneCallbackUpdate
},
[CUTSCENE_ITEM_TYPE_WAIT] = {
.init = cutsceneWaitStart,
.update = cutsceneWaitUpdate
},
[CUTSCENE_ITEM_TYPE_CUTSCENE] = {
.init = cutsceneCutsceneStart,
.update = cutsceneCutsceneUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT] = {
.init = cutsceneEntityTeleportStart,
.update = cutsceneEntityTeleportUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO] = {
.init = cutsceneEntityWalkToStart,
.update = cutsceneEntityWalkToUpdate
},
[CUTSCENE_ITEM_TYPE_FADE] = {
.init = cutsceneFadeStart,
.update = cutsceneFadeUpdate
},
[CUTSCENE_ITEM_TYPE_SET_PAUSE] = {
.init = cutsceneSetPauseStart,
.update = cutsceneSetPauseUpdate
},
[CUTSCENE_ITEM_TYPE_CONCURRENT] = {
.init = cutsceneConcurrentStart,
.update = cutsceneConcurrentUpdate
},
[CUTSCENE_ITEM_TYPE_ITEM_GIVE] = {
.init = cutsceneItemGiveStart,
.update = cutsceneItemGiveUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_REMOVE] = {
.init = cutsceneEntityRemoveStart,
.update = cutsceneEntityRemoveUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_ADD] = {
.init = cutsceneEntityAddStart,
.update = cutsceneEntityAddUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_TURN] = {
.init = cutsceneEntityTurnStart,
.update = cutsceneEntityTurnUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY] = {
.init = cutsceneEntityWalkToEntityStart,
.update = cutsceneEntityWalkToEntityUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_ADD] = {
.init = cutsceneMapAreaAddStart,
.update = cutsceneMapAreaAddUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE] = {
.init = cutsceneMapAreaRemoveStart,
.update = cutsceneMapAreaRemoveUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT] = {
.init = cutsceneMapAreaWaitStart,
.update = cutsceneMapAreaWaitUpdate
},
[CUTSCENE_ITEM_TYPE_START_BATTLE] = {
.init = cutsceneStartBattleStart,
.update = cutsceneStartBattleUpdate
},
[CUTSCENE_ITEM_TYPE_EMOJI] = {
.init = cutsceneEmojiStart,
.update = cutsceneEmojiUpdate
},
[CUTSCENE_ITEM_TYPE_SHAKE] = {
.init = cutsceneShakeStart,
.update = cutsceneShakeUpdate
}
};
void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
switch(item->type) {
case CUTSCENE_ITEM_TYPE_TEXT:
cutsceneTextStart(item, data);
break;
cutsceneiteminitcallback_t *init = CUTSCENE_ITEM_CALLBACKS[item->type].init;
if(init != NULL) init(item, data);
}
case CUTSCENE_ITEM_TYPE_CALLBACK:
cutsceneCallbackStart(item, data);
break;
bool_t cutsceneItemUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
cutsceneitemupdatecallback_t *update =
CUTSCENE_ITEM_CALLBACKS[item->type].update;
if(update == NULL) return false;
case CUTSCENE_ITEM_TYPE_WAIT:
cutsceneWaitStart(item, data);
break;
return update(item, data);
}
case CUTSCENE_ITEM_TYPE_CUTSCENE:
void cutsceneCutsceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
cutsceneEntityTeleportStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO:
cutsceneEntityWalkToStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_FADE:
cutsceneFadeStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
cutsceneSetPauseStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_CONCURRENT:
cutsceneConcurrentStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
cutsceneItemGiveStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
cutsceneEntityRemoveStart(item, data);
break;
default:
break;
}
}
bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
switch(item->type) {
case CUTSCENE_ITEM_TYPE_TEXT:
return cutsceneTextUpdate(item, data);
case CUTSCENE_ITEM_TYPE_CALLBACK:
return cutsceneCallbackUpdate(item, data);
case CUTSCENE_ITEM_TYPE_WAIT:
return cutsceneWaitUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
return cutsceneEntityTeleportUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO:
return cutsceneEntityWalkToUpdate(item, data);
case CUTSCENE_ITEM_TYPE_FADE:
return cutsceneFadeUpdate(item, data);
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
return cutsceneSetPauseUpdate(item, data);
case CUTSCENE_ITEM_TYPE_CONCURRENT:
return cutsceneConcurrentUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
return cutsceneItemGiveUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
return cutsceneEntityRemoveUpdate(item, data);
default:
bool_t cutsceneCutsceneUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return false;
}
}
+82 -1
View File
@@ -13,15 +13,29 @@
#include "entity/cutsceneentityteleport.h"
#include "entity/cutsceneentitywalkto.h"
#include "entity/cutsceneentityremove.h"
#include "entity/cutsceneentityadd.h"
#include "entity/cutsceneentityturn.h"
#include "entity/cutsceneentitywalktoentity.h"
#include "ui/cutscenetext.h"
#include "ui/cutscenetextmini.h"
#include "ui/cutscenetextminihide.h"
#include "ui/cutscenefade.h"
#include "ui/cutsceneemoji.h"
#include "ui/cutsceneshake.h"
#include "item/cutsceneitemgive.h"
#include "maparea/cutscenemapareaadd.h"
#include "maparea/cutscenemaparearemove.h"
#include "maparea/cutscenemapareawait.h"
#include "battle/cutscenestartbattle.h"
typedef struct cutscene_s cutscene_t;
typedef enum {
CUTSCENE_ITEM_TYPE_NULL,
CUTSCENE_ITEM_TYPE_TEXT,
CUTSCENE_ITEM_TYPE_TEXT_MINI,
CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE,
CUTSCENE_ITEM_TYPE_CALLBACK,
CUTSCENE_ITEM_TYPE_WAIT,
CUTSCENE_ITEM_TYPE_CUTSCENE,
@@ -31,7 +45,18 @@ typedef enum {
CUTSCENE_ITEM_TYPE_SET_PAUSE,
CUTSCENE_ITEM_TYPE_CONCURRENT,
CUTSCENE_ITEM_TYPE_ITEM_GIVE,
CUTSCENE_ITEM_TYPE_ENTITY_REMOVE
CUTSCENE_ITEM_TYPE_ENTITY_REMOVE,
CUTSCENE_ITEM_TYPE_ENTITY_ADD,
CUTSCENE_ITEM_TYPE_ENTITY_TURN,
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY,
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
CUTSCENE_ITEM_TYPE_START_BATTLE,
CUTSCENE_ITEM_TYPE_EMOJI,
CUTSCENE_ITEM_TYPE_SHAKE,
CUTSCENE_ITEM_TYPE_COUNT
} cutsceneitemtype_t;
struct cutsceneitem_s {
@@ -39,6 +64,8 @@ struct cutsceneitem_s {
union {
cutscenetext_t text;
cutscenetextmini_t textMini;
cutscenetextminihide_t textMiniHide;
cutscenecallback_t callback;
cutscenewait_t wait;
const cutscene_t *cutscene;
@@ -49,6 +76,15 @@ struct cutsceneitem_s {
cutsceneconcurrent_t concurrent;
cutsceneitemgive_t itemGive;
cutsceneentityremove_t entityRemove;
cutsceneentityadd_t entityAdd;
cutsceneentityturn_t entityTurn;
cutsceneentitywalktoentity_t entityWalkToEntity;
cutscenemapareaadd_t mapAreaAdd;
cutscenemaparearemove_t mapAreaRemove;
cutscenemapareawait_t mapAreaWait;
cutscenestartbattle_t startBattle;
cutsceneemoji_t emoji;
cutsceneshake_t shake;
};
};
@@ -56,8 +92,27 @@ typedef union cutsceneitemdata_u {
cutscenewaitdata_t wait;
cutsceneentitywalktodata_t entityWalkTo;
cutsceneconcurrentdata_t concurrent;
cutscenemapareawaitdata_t mapAreaWait;
} cutsceneitemdata_t;
typedef void (cutsceneiteminitcallback_t)(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
typedef bool_t (cutsceneitemupdatecallback_t)(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
typedef struct {
cutsceneiteminitcallback_t *init;
cutsceneitemupdatecallback_t *update;
} cutsceneitemcallbacks_t;
extern cutsceneitemcallbacks_t
CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT];
/**
* Start the given cutscene item.
*
@@ -80,3 +135,29 @@ bool_t cutsceneItemUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Starts a nested-cutscene item, handing control over to the
* referenced cutscene.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneCutsceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a nested-cutscene item. By the time this would run, control
* has already moved on to the referenced cutscene, so this always
* reports incomplete.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns false always.
*/
bool_t cutsceneCutsceneUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -8,4 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
cutsceneentityteleport.c
cutsceneentitywalkto.c
cutsceneentityremove.c
cutsceneentityadd.c
cutsceneentityturn.c
cutsceneentitywalktoentity.c
)
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
#include "assert/assert.h"
void cutsceneEntityAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t entIndex = entityGetAvailable();
assertTrue(entIndex != 0xFF, "No available entity slots for CUTSCENE_ENTITY_ADD");
entity_t *entity = &ENTITIES[entIndex];
entityInit(entity, item->entityAdd.entityType);
entityPositionSet(entity, item->entityAdd.position);// Also assigns chunk.
CUTSCENE_SYSTEM.entityLastCreated = entity;
CUTSCENE_SYSTEM.entityLastRef = entity;
}
bool_t cutsceneEntityAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/entity/entitytype.h"
#include "rpg/overworld/worldpos.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
entitytype_t entityType;
worldpos_t position;
} cutsceneentityadd_t;
/**
* Starts an entity add step (spawns the entity into the world immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEntityAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an entity add step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneEntityAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
void cutsceneEntityTurnStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
}
bool_t cutsceneEntityTurnUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
entity_t *entity = cutsceneSystemGetEntity(item->entityTurn.entityIndex);
if(
entity->direction == item->entityTurn.direction &&
entity->animation == ENTITY_ANIM_IDLE
) return true;
entityTurn(entity, item->entityTurn.direction);
return false;
}
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/entity/entitydir.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t entityIndex;
entitydir_t direction;
} cutsceneentityturn_t;
/**
* Starts an entity turn step. The turn itself is driven from Update, since
* the entity may still be finishing a previous action.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEntityTurnStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an entity turn step, retrying entityTurn until it takes effect
* and its animation completes.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once the entity is idle and facing the target direction.
*/
bool_t cutsceneEntityTurnUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
#include "rpg/entity/entitypathstep.h"
#include "rpg/overworld/map.h"
void cutsceneEntityWalkToEntityStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
}
bool_t cutsceneEntityWalkToEntityUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
entity_t *entity = cutsceneSystemGetEntity(
item->entityWalkToEntity.entityIndex
);
entity_t *target = cutsceneSystemGetEntity(
item->entityWalkToEntity.targetEntityIndex
);
worldpos_t dest = {
.x = (worldunit_t)(target->position.x + item->entityWalkToEntity.offsetX),
.y = (worldunit_t)(target->position.y + item->entityWalkToEntity.offsetY),
.z = target->position.z
};
worldunit_t z;
if(mapGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
return entityPathStep(entity, dest, true);
}
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/overworld/worldpos.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t entityIndex;
uint8_t targetEntityIndex;
worldunit_t offsetX;
worldunit_t offsetY;
} cutsceneentitywalktoentity_t;
/**
* Starts an entity walk-to-entity item. No setup is needed, the destination
* is recomputed from the target's live position every Update.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEntityWalkToEntityStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an entity walk-to-entity item. Re-reads the target entity's
* current position each frame, applies the X/Y offset, resolves the
* destination Z from nearby terrain (to account for ramps), and steps
* the entity toward it.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once the entity has reached the target's side.
*/
bool_t cutsceneEntityWalkToEntityUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -6,25 +6,14 @@
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/item/backpack.h"
#include "ui/rpg/uitextboxmain.h"
#include "util/string.h"
#include "rpg/item/itemgive.h"
#include "ui/rpg/textbox/uitextboxmain.h"
void cutsceneItemGiveStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
backpackAdd(item->itemGive.item, item->itemGive.quantity);
char_t msg[CUTSCENE_TEXT_MAX_CHARS];
stringFormat(
msg,
CUTSCENE_TEXT_MAX_CHARS - 1,
"Received %s x%u",
ITEMS[item->itemGive.item].name,
(uint32_t)item->itemGive.quantity
);
uiTextboxMainSetText(msg);
itemGive(item->itemGive.item, item->itemGive.quantity);
}
bool_t cutsceneItemGiveUpdate(
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscenemapareaadd.c
cutscenemaparearemove.c
cutscenemapareawait.c
)
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
void cutsceneMapAreaAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
CUTSCENE_SYSTEM.areaLastCreated = mapAreaAdd(
item->mapAreaAdd.min,
item->mapAreaAdd.max,
item->mapAreaAdd.callback,
item->mapAreaAdd.notify,
item->mapAreaAdd.trigger
);
}
bool_t cutsceneMapAreaAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/overworld/maparea.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
worldpos_t min;
worldpos_t max;
mapareacallback_t callback;
uint8_t notify;
uint8_t trigger;
} cutscenemapareaadd_t;
/**
* Starts a map area add step (adds the area immediately, storing its ID
* in CUTSCENE_SYSTEM.areaLastCreated).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneMapAreaAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a map area add step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneMapAreaAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/overworld/maparea.h"
void cutsceneMapAreaRemoveStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
mapAreaRemove(cutsceneSystemGetAreaId(item->mapAreaRemove.areaId));
}
bool_t cutsceneMapAreaRemoveUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t areaId;
} cutscenemaparearemove_t;
/**
* Starts a map area remove step (removes the area immediately). Accepts
* CUTSCENE_AREA_LAST_CREATED in place of a literal area ID.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneMapAreaRemoveStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a map area remove step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneMapAreaRemoveUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/overworld/maparea.h"
#include "assert/assert.h"
void cutsceneMapAreaWaitStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
assertTrue(
item->mapAreaWait.count <= CUTSCENE_MAP_AREA_WAIT_MAX,
"Too many areas in CUTSCENE_MAP_AREA_WAIT"
);
for(uint8_t i = 0; i < item->mapAreaWait.count; i++) {
uint8_t areaId = cutsceneSystemGetAreaId(item->mapAreaWait.areaIds[i]);
data->mapAreaWait.baseline[i] = MAP_AREAS[areaId].triggerCount;
}
}
bool_t cutsceneMapAreaWaitUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
for(uint8_t i = 0; i < item->mapAreaWait.count; i++) {
uint8_t areaId = cutsceneSystemGetAreaId(item->mapAreaWait.areaIds[i]);
if(MAP_AREAS[areaId].triggerCount != data->mapAreaWait.baseline[i]) {
return true;
}
}
return false;
}
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
/** Maximum number of areas a single CUTSCENE_MAP_AREA_WAIT may watch. */
#define CUTSCENE_MAP_AREA_WAIT_MAX 4
typedef struct {
const uint8_t *areaIds;
uint8_t count;
} cutscenemapareawait_t;
typedef struct {
uint32_t baseline[CUTSCENE_MAP_AREA_WAIT_MAX];
} cutscenemapareawaitdata_t;
/**
* Starts a map area wait step, snapshotting each watched area's current
* trigger count.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneMapAreaWaitStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a map area wait step, completing once any watched area's
* trigger count has changed since Start (i.e. its callback fired).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once any watched area has been triggered.
*/
bool_t cutsceneMapAreaWaitUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -6,5 +6,9 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscenetext.c
cutscenetextmini.c
cutscenetextminihide.c
cutscenefade.c
cutsceneemoji.c
cutsceneshake.c
)
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
void cutsceneEmojiStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
entity_t *entity = cutsceneSystemGetEntity(item->emoji.entityIndex);
uiEmojiAdd(entity->id, item->emoji.duration, item->emoji.emojiType);
}
bool_t cutsceneEmojiUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "ui/rpg/uiemoji.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t entityIndex;
float_t duration;
uiemojitype_t emojiType;
} cutsceneemoji_t;
/**
* Starts an emoji step (shows an emoji above the entity for the given
* duration, then completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEmojiStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an emoji step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneEmojiUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/rpgcamera.h"
void cutsceneShakeStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
rpgCameraShake(item->shake.amount, item->shake.duration);
}
bool_t cutsceneShakeUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t amount;
float_t duration;
} cutsceneshake_t;
/**
* Starts a camera shake item (kicks off the shake on the RPG camera).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneShakeStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a camera shake item. The shake itself runs asynchronously on
* the RPG camera, so this always completes immediately.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneShakeUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+1 -1
View File
@@ -6,7 +6,7 @@
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "ui/rpg/uitextboxmain.h"
#include "ui/rpg/textbox/uitextboxmain.h"
void cutsceneTextStart(
const cutsceneitem_t *item,
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
void cutsceneTextMiniStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t index = uiTextboxMiniListGetNext();
uiTextboxMiniShow(
&UI_TEXTBOX_MINI_LIST[index],
item->textMini.text,
item->textMini.position,
item->textMini.duration,
NULL,
NULL
);
CUTSCENE_SYSTEM.textMiniLastCreated = index;
}
bool_t cutsceneTextMiniUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
#define CUTSCENE_TEXT_MINI_MAX_CHARS 128
typedef struct {
char_t text[CUTSCENE_TEXT_MINI_MAX_CHARS];
vec3 position;
float_t duration;
} cutscenetextmini_t;
/**
* Starts a mini text item (shows a mini textbox at the given world
* position for the given duration, then completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneTextMiniStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a mini text item (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneTextMiniUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
void cutsceneTextMiniHideStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t index = cutsceneSystemGetTextMiniId(item->textMiniHide.index);
uiTextboxMiniClose(&UI_TEXTBOX_MINI_LIST[index]);
}
bool_t cutsceneTextMiniHideUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t index;
} cutscenetextminihide_t;
/**
* Starts a mini text hide step (closes the mini textbox immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneTextMiniHideStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a mini text hide step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneTextMiniHideUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+13 -7
View File
@@ -9,17 +9,23 @@
#include "rpg/cutscene/cutscene.h"
#include "rpg/cutscene/cutscenesystem.h"
CUTSCENE(TEST_ONE, DEFAULT,
CUTSCENE(TEST_ONE, 0, DEFAULT,
CUTSCENE_TEXT("Test One."),
);
CUTSCENE(TEST_TWO, DEFAULT,
CUTSCENE(TEST_TWO, 0, DEFAULT,
CUTSCENE_TEXT("Test Two."),
CUTSCENE_CONCURRENT(
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0),
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACTED, 8, 2, 0),
CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0),
CUTSCENE_TEXT_MINI("Hello!", 4, 4, 0, 3.0f),
CUTSCENE_EMOJI(
CUTSCENE_ENTITY_LAST_CREATED, UI_EMOJI_EXCLAMATION_MARK, 2.0f
),
CUTSCENE_ITEM_GIVE(ITEM_ID_POTATO, 3),
CUTSCENE_ENTITY_REMOVE(CUTSCENE_ENTITY_INTERACT),
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."),
);
+2
View File
@@ -15,3 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(anim)
add_subdirectory(interact)
add_subdirectory(npc)
add_subdirectory(item)
add_subdirectory(global)
+1 -1
View File
@@ -19,7 +19,7 @@ const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
};
float_t entityAnimTileZOffset(const worldpos_t pos) {
return tileIsRamp(mapGetTile(pos)) ? 0.5f : 0.0f;
return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f;
}
void entityAnimUpdate(entity_t *entity) {
+72 -26
View File
@@ -10,7 +10,9 @@
#include "util/memory.h"
#include "time/time.h"
#include "util/math.h"
#include "console/console.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/maparea.h"
#include "rpg/overworld/chunk.h"
#include "rpg/overworld/tile.h"
@@ -27,6 +29,7 @@ void entityInit(entity_t *entity, const entitytype_t type) {
memoryZero(entity, sizeof(entity_t));
entity->id = (uint8_t)(entity - ENTITIES);
entity->globalId = ENTITY_GLOBAL_ID_NULL;
entity->type = type;
entity->chunkIndex = 0xFF;
@@ -60,6 +63,10 @@ bool_t entityCanRun(entity_t *entity) {
return entity->animation == ENTITY_ANIM_IDLE;
}
bool_t entityCanUnload(entity_t *entity) {
return entity->globalId < ENTITY_GLOBAL_ID_START;
}
void entityTurn(entity_t *entity, const entitydir_t direction) {
if(!entityCanTurn(entity)) return;
entity->direction = direction;
@@ -89,38 +96,38 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
// Are we walking up a ramp?
if(
tileIsRamp(tileCurrent) &&
tileShapeIsRamp(tileCurrent.shape) &&
(
// Can only walk UP the direction the ramp faces.
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent ||
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape ||
// If diagonal ramp, can go up one of two ways only. Inner ramps
// share the same allowed directions as their outer counterparts.
(
(
(
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST ||
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST_INNER
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
) ||
(
(
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST ||
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST_INNER
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) ||
(
(
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST ||
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST_INNER
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST ||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) ||
(
(
tileCurrent == TILE_SHAPE_RAMP_NORTHWEST ||
tileCurrent == TILE_SHAPE_RAMP_NORTHWEST_INNER
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST ||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
)
@@ -129,55 +136,60 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
)
) {
tile_t tileNewSaved = tileNew;
tileNew = TILE_SHAPE_NULL;
tileNew = TILE_NULL;
worldpos_t abovePos = newPos;
abovePos.z += 1;
tile_t tileAbove = mapGetTile(abovePos);
if(tileAbove != TILE_SHAPE_NULL && tileIsWalkable(tileAbove)) {
if(
tileAbove.shape != TILE_SHAPE_NULL &&
tileShapeIsWalkable(tileAbove.shape)
) {
raise = true;
} else {
tileNew = tileNewSaved;
}
} else if(tileNew == TILE_SHAPE_NULL && newPos.z > 0) {
} else if(tileNew.shape == TILE_SHAPE_NULL && newPos.z > 0) {
// Falling down?
worldpos_t belowPos = newPos;
belowPos.z -= 1;
tile_t tileBelow = mapGetTile(belowPos);
if(
tileBelow != TILE_SHAPE_NULL &&
tileIsRamp(tileBelow) &&
tileBelow.shape != TILE_SHAPE_NULL &&
tileShapeIsRamp(tileBelow.shape) &&
(
// This handles regular cardinal ramps
(entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH) == tileBelow ||
(
entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH
) == tileBelow.shape ||
// This handles diagonal ramps. Inner ramps share the same
// allowed directions as their outer counterparts.
(
(
(
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST ||
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST_INNER
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
) ||
(
(
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST ||
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST_INNER
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) ||
(
(
tileBelow == TILE_SHAPE_RAMP_NORTHEAST ||
tileBelow == TILE_SHAPE_RAMP_NORTHEAST_INNER
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST ||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) ||
(
(
tileBelow == TILE_SHAPE_RAMP_NORTHWEST ||
tileBelow == TILE_SHAPE_RAMP_NORTHWEST_INNER
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST ||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
)
@@ -190,7 +202,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
}
// Can we walk here?
if(!raise && !fall && !tileIsWalkable(tileNew)) return;// Blocked
if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked
// Raise/fall must be applied before checking for blocking entities,
// otherwise the check compares against the wrong z-level.
@@ -213,6 +225,8 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
entity->position = newPos;
entity->animation = ENTITY_ANIM_WALK;
entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking
entityUpdateChunk(entity);
mapAreaCheckEntity(entity);
}
void entityRun(entity_t *entity, const entitydir_t direction) {
@@ -235,6 +249,17 @@ entity_t * entityGetAt(const worldpos_t position) {
return NULL;
}
entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
entity_t *ent = ENTITIES;
do {
if(ent->type == ENTITY_TYPE_NULL) continue;
if(ent->globalId != globalId) continue;
return ent;
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
return NULL;
}
uint8_t entityGetAvailable() {
entity_t *ent = ENTITIES;
do {
@@ -251,6 +276,7 @@ void entityPositionSet(entity_t *entity, const worldpos_t pos) {
entity->animation = ENTITY_ANIM_IDLE;
entity->animTime = 0;
entity->walkEndCooldown = 0;
entityUpdateChunk(entity);
}
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
@@ -267,7 +293,10 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
}
}
entity->chunkIndex = chunkIndex;
// Only claim the new chunk once actually inserted into one of its slots -
// 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) {
chunk_t *next = mapGetChunk(chunkIndex);
@@ -275,8 +304,25 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(next->entities[i] != 0xFF) continue;
next->entities[i] = entity->id;
entity->chunkIndex = chunkIndex;
break;
}
if(entity->chunkIndex != chunkIndex) {
consolePrint(
"entitySetChunk: chunk %u has no free entity slots, entity %u "
"left untracked",
chunkIndex, entity->id
);
}
}
}
}
void entityUpdateChunk(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
chunkpos_t cp;
worldPosToChunkPos(&entity->position, &cp);
chunkindex_t ci = mapGetChunkIndexAt(cp);
if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
}
+38 -1
View File
@@ -14,8 +14,15 @@
typedef struct map_s map_t;
typedef uint16_t entityglobalid_t;
#define ENTITY_GLOBAL_ID_NULL 0
#define ENTITY_GLOBAL_ID_START 1
#define ENTITY_GLOBAL_ID_PLAYER 1
typedef struct entity_s {
uint8_t id;
entityglobalid_t globalId;
entitytype_t type;
entitytypedata_t data;
@@ -75,6 +82,16 @@ bool_t entityCanWalk(entity_t *entity);
*/
bool_t entityCanRun(entity_t *entity);
/**
* Returns true if the entity is allowed to be unloaded. By default this is
* true for entities whose global ID falls within the randomly assigned
* range below ENTITY_GLOBAL_ID_START.
*
* @param entity Pointer to the entity to check.
* @returns True if the entity can be unloaded.
*/
bool_t entityCanUnload(entity_t *entity);
/**
* Turn an entity to face a new direction.
*
@@ -108,6 +125,14 @@ void entityRun(entity_t *entity, const entitydir_t direction);
*/
entity_t *entityGetAt(const worldpos_t pos);
/**
* Gets the entity with the given global ID, if one is currently loaded.
*
* @param globalId The global ID to search for.
* @return Pointer to the matching entity, or NULL if none is loaded.
*/
entity_t *entityGetByGlobalId(const entityglobalid_t globalId);
/**
* Gets an available entity index.
*
@@ -117,13 +142,25 @@ uint8_t entityGetAvailable();
/**
* Assigns an entity to a chunk, removing it from its current chunk first.
* Pass 0xFF as chunkIndex to detach the entity from any chunk.
* Pass 0xFF as chunkIndex to detach the entity from any chunk. If the
* 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 chunkIndex Index of the chunk to assign to, or 0xFF for none.
*/
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex);
/**
* Resolves the chunk that an entity's current position falls into and
* assigns the entity to it via entitySetChunk. Leaves the entity's chunk
* unchanged if its position doesn't fall within any loaded chunk.
*
* @param entity Pointer to the entity to update.
*/
void entityUpdateChunk(entity_t *entity);
/**
* Instantly moves an entity to a world position, resetting movement state.
*
+3 -1
View File
@@ -24,7 +24,9 @@ bool_t entityPathStep(
"Entity pointer is out of bounds"
);
if(worldPosIsEqual(entity->position, target)) return true;
if(worldPosIsEqual(entity->position, target) && entityCanWalk(entity)) {
return true;
}
if(!entityCanWalk(entity)) return false;
entitydir_t dir;
+8
View File
@@ -8,12 +8,14 @@
#pragma once
#include "rpg/entity/player.h"
#include "npc/npc.h"
#include "item/entityitem.h"
typedef enum {
ENTITY_TYPE_NULL,
ENTITY_TYPE_PLAYER,
ENTITY_TYPE_NPC,
ENTITY_TYPE_ITEM,
ENTITY_TYPE_COUNT
} entitytype_enum_t;
@@ -23,6 +25,7 @@ typedef uint8_t entitytype_t;
typedef union {
player_t player;
npc_t npc;
entityitem_t item;
} entitytypedata_t;
typedef struct {
@@ -50,5 +53,10 @@ static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = {
[ENTITY_TYPE_NPC] = {
.init = npcInit,
.movement = npcMovement,
},
[ENTITY_TYPE_ITEM] = {
.init = entityItemInit,
.movement = entityItemMovement,
}
};
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
globalitemstore.c
)
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entityglobaldefs.h"
#include "entitygloballist.h"
#define ENTITY_GLOBAL_LIST_COUNT ( \
sizeof(ENTITY_GLOBAL_LIST) / \
sizeof(ENTITY_GLOBAL_LIST[0]) \
)
//EOF
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "rpg/overworld/worldpos.h"
#include "rpg/entity/entity.h"
typedef struct entity_s entity_t;
typedef struct {
entity_t *entity;
worldpos_t position;
} entityglobalcreate_t;
/**
* Callback invoked to initialize a global entity.
*
* @param create Pointer to the entity/position being initialized.
* @returns An error code.
*/
typedef void (*entityglobalinitcallback_t)(
entityglobalcreate_t *create
);
typedef struct {
entitytype_t type;
entityglobalinitcallback_t callback;
} entityglobaldef_t;
#define ENTITY_GLOBAL(id, entType, callbackFn) \
[id] = { .type = entType, .callback = callbackFn }
#define ENTITY_GLOBAL_CALLBACK(id) \
static void ENTTIYT_GLOBAL_CALLBACK_##id(entityglobalcreate_t *create)
#define ENTITY_GLOBAL_REF(id) \
ENTTIYT_GLOBAL_CALLBACK_##id
//EOF
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entityglobaldefs.h"
#include "rpg/cutscene/scene/testcutscene.h"
ENTITY_GLOBAL_CALLBACK(3) {
create->entity->data.npc.moveType = NPC_MOVE_TYPE_PATH;
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 });
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 });
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 });
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 });
create->entity->interact.type = ENTITY_INTERACT_CUTSCENE;
create->entity->interact.data.cutscene = CUTSCENE_REFERENCE(TEST_TWO);
}
static const entityglobaldef_t ENTITY_GLOBAL_LIST[] = {
ENTITY_GLOBAL(ENTITY_GLOBAL_ID_NULL, ENTITY_TYPE_NULL, NULL),
ENTITY_GLOBAL(ENTITY_GLOBAL_ID_PLAYER, ENTITY_TYPE_PLAYER, NULL),
ENTITY_GLOBAL(3, ENTITY_TYPE_NPC, ENTITY_GLOBAL_REF(3)),
};
//EOF
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "globalitemstore.h"
#include "assert/assert.h"
bool_t globalItemStoreIsCollected(
const savefile_t *file, const entityglobalid_t id
) {
assertNotNull(file, "Save file cannot be NULL");
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
return file->globalItemCollected[id];
}
void globalItemStoreSetCollected(
savefile_t *file, const entityglobalid_t id, const bool_t collected
) {
assertNotNull(file, "Save file cannot be NULL");
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
file->globalItemCollected[id] = collected;
}
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "save/savefile.h"
#include "rpg/entity/entity.h"
/**
* Checks whether the global entity with the given ID has already been
* marked collected in the given save file's data - e.g. so a global item
* entity's init callback (see rpg/entity/global/entitygloballist.h) can
* skip spawning itself if the player already picked it up in a prior
* session, without needing to keep the entity itself alive to remember
* that (which would need render/collision special-casing - this doesn't).
*
* @param file The save file to check.
* @param id The global entity ID to check.
* @return True if already marked collected.
*/
bool_t globalItemStoreIsCollected(
const savefile_t *file, const entityglobalid_t id
);
/**
* Marks the global entity with the given ID as collected (or not) in the
* given save file's data. Does not itself write the save to disk - call
* saveWrite() separately once ready to persist it.
*
* @param file The save file to write into.
* @param id The global entity ID to mark.
* @param collected The new collected state.
*/
void globalItemStoreSetCollected(
savefile_t *file, const entityglobalid_t id, const bool_t collected
);
@@ -8,7 +8,7 @@
#include "rpg/entity/entity.h"
#include "assert/assert.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/uitextboxmain.h"
#include "ui/rpg/textbox/uitextboxmain.h"
void entityInteractWith(entity_t *player, entity_t *target) {
assertNotNull(player, "Player entity pointer cannot be NULL");
@@ -40,6 +40,14 @@ void entityInteractWith(entity_t *player, entity_t *target) {
// entityTurn(player, player->direction); // Redundant (for now)
break;
case ENTITY_INTERACT_CALLBACK:
assertNotNull(
target->interact.data.callback,
"Interact callback pointer cannot be NULL"
);
target->interact.data.callback(player, target);
break;
case ENTITY_INTERACT_NULL:
break;
@@ -7,9 +7,9 @@
#pragma once
#include "dusk.h"
#include "rpg/cutscene/cutscene.h"
typedef struct entity_s entity_t;
typedef struct cutscene_s cutscene_t;
/**
* Describes the type of interaction an entity supports.
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
entityitem.c
)
+44
View File
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityitem.h"
#include "rpg/entity/entity.h"
#include "assert/assert.h"
#include "rpg/item/itemgive.h"
#include "ui/rpg/textbox/uitextboxmain.h"
void entityItemInit(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
entity->interact.type = ENTITY_INTERACT_CALLBACK;
entity->interact.data.callback = entityItemInteract;
}
void entityItemSet(
entity_t *entity,
const itemid_t item,
const uint8_t quantity
) {
assertNotNull(entity, "Entity pointer cannot be NULL");
entity->data.item.item = item;
entity->data.item.quantity = quantity;
}
void entityItemInteract(entity_t *player, entity_t *target) {
assertNotNull(player, "Player entity pointer cannot be NULL");
assertNotNull(target, "Target entity pointer cannot be NULL");
itemGive(target->data.item.item, target->data.item.quantity);
target->data.item.collected = true;
}
void entityItemMovement(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
if(!entity->data.item.collected) return;
if(uiTextboxMainIsActive()) return;
entity->type = ENTITY_TYPE_NULL;
}
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/item/item.h"
typedef struct entity_s entity_t;
typedef struct {
itemid_t item;
uint8_t quantity;
bool_t collected;
} entityitem_t;
/**
* Initializes an item entity. Wires up the interact component so that
* interacting with the entity gives its item and removes it.
*
* @param entity Pointer to the entity structure to initialize.
*/
void entityItemInit(entity_t *entity);
/**
* Sets the item and quantity represented by an item entity.
*
* @param entity Pointer to the entity structure.
* @param item The item ID to give when picked up.
* @param quantity The quantity to give when picked up.
*/
void entityItemSet(
entity_t *entity,
const itemid_t item,
const uint8_t quantity
);
/**
* Interact callback for item entities. Gives the entity's item to the
* player and shows the pickup message. The entity itself is removed once
* the message has been dismissed, via entityItemMovement.
*
* @param player Pointer to the player entity.
* @param target Pointer to the item entity being picked up.
*/
void entityItemInteract(entity_t *player, entity_t *target);
/**
* Update callback for item entities. Removes the entity once its pickup
* message has been dismissed. No-op if the item hasn't been collected.
*
* @param entity Pointer to the entity structure to update.
*/
void entityItemMovement(entity_t *entity);
+6 -5
View File
@@ -11,11 +11,12 @@
#include "util/memory.h"
#include "time/time.h"
#include "ui/focus/uifocus.h"
#include "ui/frame/uisettings.h"
#include "ui/frame/game/uigamemenu.h"
#include "rpg/cutscene/cutscenesystem.h"
void playerInit(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
entity->globalId = ENTITY_GLOBAL_ID_PLAYER;
}
bool_t playerCanInteract(entity_t *entity) {
@@ -26,14 +27,14 @@ void playerInput(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return;
// Toggle settings on pause
if(uiSettingsIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) {
uiSettingsClose();
// Toggle game menu on pause
if(uiGameMenuIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) {
uiGameMenuClose();
} else if(
inputPressed(INPUT_ACTION_PAUSE) &&
entity->animation == ENTITY_ANIM_IDLE
) {
uiSettingsOpen();
uiGameMenuOpen();
}
// Can player act?
+1
View File
@@ -7,6 +7,7 @@
#pragma once
#include "input/input.h"
#include "entitydir.h"
typedef struct entity_s entity_t;
+6 -4
View File
@@ -6,15 +6,17 @@
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
item.c
inventory.c
backpack.c
itemgive.c
)
# Item Definitions
dusk_run_python(
dusk_item_csv_defs
dusk_item_json_defs
tools.item
--csv ${CMAKE_CURRENT_SOURCE_DIR}/item.csv
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/item.h
--json ${CMAKE_CURRENT_SOURCE_DIR}/item.json
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/itemdef.h
)
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_csv_defs)
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_json_defs)
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "item.h"
#include "assert/assert.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
errorret_t itemGetName(
const itemid_t item,
char_t *buffer,
const size_t bufferSize
) {
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
ITEMS[item].name,
0,
buffer,
bufferSize
));
errorOk();
}
-4
View File
@@ -1,4 +0,0 @@
id,type,weight
POTION,MEDICINE,1.0
POTATO,FOOD,0.5
APPLE,FOOD,0.3
1 id type weight
2 POTION MEDICINE 1.0
3 POTATO FOOD 0.5
4 APPLE FOOD 0.3
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "rpg/item/itemdef.h"
/**
* Gets the localized display name for an item.
*
* @param item The item ID to look up. Must not be ITEM_ID_NULL.
* @param buffer Buffer to write the localized name into.
* @param bufferSize Size of the buffer.
* @return Any error that occurs.
*/
errorret_t itemGetName(
const itemid_t item,
char_t *buffer,
const size_t bufferSize
);
+5
View File
@@ -0,0 +1,5 @@
[
{ "id": "POTION", "type": "MEDICINE", "weight": 1.0, "name": "potion" },
{ "id": "POTATO", "type": "FOOD", "weight": 0.5, "name": "potato" },
{ "id": "APPLE", "type": "FOOD", "weight": 0.3, "name": "apple" }
]
+32
View File
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "itemgive.h"
#include "rpg/item/backpack.h"
#include "rpg/item/item.h"
#include "ui/rpg/textbox/uitextboxmain.h"
#include "util/string.h"
#include "error/error.h"
#define ITEM_GIVE_NAME_MAX_CHARS 32
void itemGive(const itemid_t item, const uint8_t quantity) {
backpackAdd(item, quantity);
char_t name[ITEM_GIVE_NAME_MAX_CHARS];
errorCatch(itemGetName(item, name, ITEM_GIVE_NAME_MAX_CHARS));
char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS];
stringFormat(
msg,
ITEM_GIVE_MESSAGE_MAX_CHARS - 1,
"Received %s x%u",
name,
(uint32_t)quantity
);
uiTextboxMainSetText(msg);
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/item/item.h"
#define ITEM_GIVE_MESSAGE_MAX_CHARS 256
/**
* Adds a quantity of an item to the player's backpack and shows a
* "Received <item> x<quantity>" message in the main textbox.
*
* @param item The item ID to give.
* @param quantity The quantity to give.
*/
void itemGive(const itemid_t item, const uint8_t quantity);
+4
View File
@@ -8,7 +8,11 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
chunk.c
map.c
maparea.c
worldpos.c
tile.c
tileshape.c
)
add_subdirectory(global)
+1 -5
View File
@@ -8,11 +8,7 @@
#include "chunk.h"
uint32_t chunkGetTileIndex(const chunkpos_t position) {
return (
(position.z * CHUNK_WIDTH * CHUNK_HEIGHT) +
(position.y * CHUNK_WIDTH) +
position.x
);
return (position.y * CHUNK_WIDTH) + position.x;
}
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) {
+9
View File
@@ -12,6 +12,8 @@
#define CHUNK_MESH_COUNT_MAX 10
#define CHUNK_MESH_NAME_MAX 64
#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;
@@ -28,6 +30,13 @@ typedef struct chunk_s {
assetentry_t *modelEntries[CHUNK_MESH_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;
/**

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